@indigoai-us/hq-cli 5.227.1 → 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 +20 -0
- package/dist/command-catalog.generated.d.ts +23 -1
- package/dist/command-catalog.generated.js +30 -1
- package/dist/commands/lanes.d.ts +1 -6
- package/dist/commands/lanes.js +201 -231
- package/dist/lib/lanes/{claude-account-balance.d.ts → claude-balance.d.ts} +1 -3
- package/dist/lib/lanes/{claude-account-balance.js → claude-balance.js} +1 -7
- package/dist/lib/lanes/{hire-grants.d.ts → grants.d.ts} +1 -2
- package/dist/lib/lanes/{hire-grants.js → grants.js} +1 -2
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +1 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +15 -17
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +3 -1
- package/dist/lib/lanes/outpost-dispatch.js +2 -0
- package/dist/lib/lanes/reconcile.js +10 -24
- package/dist/lib/lanes/spawn.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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
|
+
|
|
18
|
+
## [5.228.0] — 2026-09-27
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Lane hire grants and Claude account balancing are always active.
|
|
23
|
+
`backfill-grants --apply` refuses to write; dry-run output remains available.
|
|
24
|
+
|
|
5
25
|
## [5.227.1] — 2026-09-27
|
|
6
26
|
|
|
7
27
|
### Fixed
|
|
@@ -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: "";
|
|
@@ -8285,7 +8307,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
8285
8307
|
readonly description: "claude";
|
|
8286
8308
|
}, {
|
|
8287
8309
|
readonly flags: "--apply";
|
|
8288
|
-
readonly description: "Move eligible between-turn sessions
|
|
8310
|
+
readonly description: "Move eligible between-turn sessions to balance Claude accounts";
|
|
8289
8311
|
}, {
|
|
8290
8312
|
readonly flags: "--json";
|
|
8291
8313
|
readonly description: "JSON on stdout (always)";
|
|
@@ -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": "",
|
|
@@ -10717,7 +10746,7 @@ export const COMMAND_CATALOG = [
|
|
|
10717
10746
|
},
|
|
10718
10747
|
{
|
|
10719
10748
|
"flags": "--apply",
|
|
10720
|
-
"description": "Move eligible between-turn sessions
|
|
10749
|
+
"description": "Move eligible between-turn sessions to balance Claude accounts"
|
|
10721
10750
|
},
|
|
10722
10751
|
{
|
|
10723
10752
|
"flags": "--json",
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -10,10 +10,9 @@ import { type TrackerMeshRequest } from "../lib/lanes/tracker-mesh.js";
|
|
|
10
10
|
import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
11
11
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
12
12
|
export type { OutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
13
|
-
export { LANE_HIRE_GRANTS_FLAG };
|
|
14
13
|
import { type OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
|
|
15
14
|
import { emitLaneDecisionEvents, type LaneMesh } from "../lib/lanes/mesh-events.js";
|
|
16
|
-
import {
|
|
15
|
+
import { MAX_LANE_DEPTH, withLaneHireLocks, type LaneCompanyRef, type QuestionBody, type LaneRecord, type QuestionRecord, type ResolvedLaneWorker, type SeniorRef, type LivenessDeps, type MachineIdentity, type MonitorEvidenceReader, type RunCommand, type ThrottleDeps } from "../lib/lanes/index.js";
|
|
17
16
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
18
17
|
import { type RunCompactionFn } from "../lib/lanes/compaction.js";
|
|
19
18
|
import type { WatcherLoopDeps } from "../lib/lanes/watcher.js";
|
|
@@ -87,16 +86,12 @@ export interface LanesCommandDeps {
|
|
|
87
86
|
monitorEvidence?: MonitorEvidenceReader;
|
|
88
87
|
/** Test seam for the default-off outpost placement rollout flag. */
|
|
89
88
|
lanePlacementEnabled?: () => boolean;
|
|
90
|
-
/** Test seam for the default-off lane hire-grants rollout gate. */
|
|
91
|
-
laneHireGrantsEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
92
89
|
/** Test seam for the default-off story ledger flag. */
|
|
93
90
|
storyLedgerEnabled?: () => boolean;
|
|
94
91
|
/** Test seam for the SSM-only Outpost owner grant path. */
|
|
95
92
|
isSsmOwnerDispatch?: () => boolean;
|
|
96
93
|
/** Test seam for changing a lane between pre-lock and locked grant reads. */
|
|
97
94
|
withGrantHireLocks?: typeof withLaneHireLocks;
|
|
98
|
-
/** Test seam for the Claude account-balancing gate. */
|
|
99
|
-
laneClaudeAccountBalanceEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
100
95
|
/** Test seam for the default-off Outpost question visibility gate. */
|
|
101
96
|
laneOutpostQuestionVisibilityEnabled?: (company?: LaneCompanyRef | null) => boolean | Promise<boolean>;
|
|
102
97
|
/** Test seam for the hq-flags gated tracker commands. */
|
package/dist/commands/lanes.js
CHANGED
|
@@ -27,11 +27,10 @@ import { defaultTrackerActor, LANES_TRACKER_FLAG, LANES_TRACKER_MESH_FLAG, regis
|
|
|
27
27
|
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
28
28
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
29
29
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
30
|
-
export { LANE_HIRE_GRANTS_FLAG };
|
|
31
30
|
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostQuestionAnswerCommand, buildOutpostQuestionListCommand, buildOutpostQuestionShowCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
32
31
|
import { outpostHireSummaryCacheKey, readOutpostHireSummaryCache, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
33
32
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
34
|
-
import { BALANCE_MOVABLE_STATES,
|
|
33
|
+
import { BALANCE_MOVABLE_STATES, accountLoadsForReport, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
|
|
35
34
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
36
35
|
import { backfillOutpostIdentityMarker as backfillOutpostIdentityMarkerImpl, createOutpostIdentityMarkerIfMissing, validateOutpostIdentityId, } from "../lib/lanes/outpost-identity.js";
|
|
37
36
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
@@ -42,7 +41,7 @@ import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
|
42
41
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
43
42
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
44
43
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
45
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError,
|
|
44
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
46
45
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
47
46
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
48
47
|
import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
@@ -77,20 +76,12 @@ export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-ho
|
|
|
77
76
|
function defaultLanePlacementEnabled() {
|
|
78
77
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
79
78
|
}
|
|
80
|
-
function defaultLaneClaudeAccountBalanceEnabled(company) {
|
|
81
|
-
// Keep the local HQ_FLAG override and the registry reader's fail-closed
|
|
82
|
-
// behavior, matching the lane hire-grants gate.
|
|
83
|
-
return readLanesRegistryFlag(CLAUDE_ACCOUNT_BALANCE_FLAG, { company });
|
|
84
|
-
}
|
|
85
79
|
function defaultLaneTrackerEnabled(company) {
|
|
86
80
|
return readLanesRegistryFlag(LANES_TRACKER_FLAG, { company });
|
|
87
81
|
}
|
|
88
82
|
function defaultLaneTrackerMeshEnabled(company) {
|
|
89
83
|
return readLanesRegistryFlag(LANES_TRACKER_MESH_FLAG, { company });
|
|
90
84
|
}
|
|
91
|
-
function defaultLaneHireGrantsEnabled(company) {
|
|
92
|
-
return readLanesRegistryFlag(LANE_HIRE_GRANTS_FLAG, { company });
|
|
93
|
-
}
|
|
94
85
|
function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
|
|
95
86
|
return readLanesRegistryFlag(LANE_OUTPOST_QUESTION_VISIBILITY_FLAG, {
|
|
96
87
|
company,
|
|
@@ -320,11 +311,9 @@ function resolveDeps(deps = {}) {
|
|
|
320
311
|
})),
|
|
321
312
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
322
313
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
323
|
-
laneHireGrantsEnabled: deps.laneHireGrantsEnabled ?? defaultLaneHireGrantsEnabled,
|
|
324
314
|
storyLedgerEnabled: deps.storyLedgerEnabled ?? laneStoryLedgerEnabled,
|
|
325
315
|
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
326
316
|
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
327
|
-
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
328
317
|
laneOutpostQuestionVisibilityEnabled: deps.laneOutpostQuestionVisibilityEnabled ??
|
|
329
318
|
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
330
319
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
@@ -1022,9 +1011,6 @@ async function laneHirePresentation(ctx, lane, lanes) {
|
|
|
1022
1011
|
laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
|
|
1023
1012
|
return { ...local, hire_source: "local" };
|
|
1024
1013
|
}
|
|
1025
|
-
if (!await ctx.laneHireGrantsEnabled(lane.company)) {
|
|
1026
|
-
return { ...local, hire_source: "local" };
|
|
1027
|
-
}
|
|
1028
1014
|
try {
|
|
1029
1015
|
return { ...(await ownerLaneHireFields(ctx, lane)), hire_source: "owner" };
|
|
1030
1016
|
}
|
|
@@ -1059,8 +1045,6 @@ async function withResumeHireAdmission(ctx, lane, fn) {
|
|
|
1059
1045
|
!lane.senior.id) {
|
|
1060
1046
|
return fn();
|
|
1061
1047
|
}
|
|
1062
|
-
if (!await ctx.laneHireGrantsEnabled(lane.company))
|
|
1063
|
-
return fn();
|
|
1064
1048
|
return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
|
|
1065
1049
|
const current = readLane(hqRoot, lane.lane_id);
|
|
1066
1050
|
// A different resume already reactivated this lane; it occupies the slot
|
|
@@ -1684,7 +1668,11 @@ function depthOfRef(hqRoot, ref, ctx, visited = new Set(), override) {
|
|
|
1684
1668
|
senior: ref,
|
|
1685
1669
|
});
|
|
1686
1670
|
}
|
|
1687
|
-
|
|
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);
|
|
1688
1676
|
}
|
|
1689
1677
|
if (!ref.id) {
|
|
1690
1678
|
throw new LanesError("invalid_senior_ref", "Lane senior is missing an id", {
|
|
@@ -2664,18 +2652,14 @@ async function runCreate(ctx, opts) {
|
|
|
2664
2652
|
}
|
|
2665
2653
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : defaultSenior(ctx);
|
|
2666
2654
|
assertSeniorExists(hqRoot, senior, ctx);
|
|
2667
|
-
// Resolve tenant context before the registry read so the flag evaluator
|
|
2668
|
-
// applies the same company scope as this lane.
|
|
2669
2655
|
const company = companyFlag
|
|
2670
2656
|
? await ctx.resolveCompany(companyFlag)
|
|
2671
2657
|
: null;
|
|
2672
|
-
const hireGrantsEnabled = await ctx.laneHireGrantsEnabled(company);
|
|
2673
2658
|
if (requestedHireGrant !== undefined &&
|
|
2674
2659
|
!callerMatchesSenior(ctx, senior)) {
|
|
2675
2660
|
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(senior)} may set a lane's hire grant`, { senior, hire_grant: requestedHireGrant });
|
|
2676
2661
|
}
|
|
2677
|
-
if (
|
|
2678
|
-
ctx.env.HQ_LANE_ID?.trim() &&
|
|
2662
|
+
if (ctx.env.HQ_LANE_ID?.trim() &&
|
|
2679
2663
|
!callerMatchesSenior(ctx, senior)) {
|
|
2680
2664
|
throw new LanesError("hire_grant_unauthorized", `Only senior lane ${senior.id} may create a direct child lane`, { senior_lane_id: senior.id });
|
|
2681
2665
|
}
|
|
@@ -3031,7 +3015,7 @@ async function runCreate(ctx, opts) {
|
|
|
3031
3015
|
}
|
|
3032
3016
|
const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
|
|
3033
3017
|
let accountSelection;
|
|
3034
|
-
if (provider === "claude"
|
|
3018
|
+
if (provider === "claude") {
|
|
3035
3019
|
const targetHost = placementHostOverride ?? (outpostId !== undefined
|
|
3036
3020
|
? { kind: "outpost", id: outpostId }
|
|
3037
3021
|
: laneHostFromCtx(ctx));
|
|
@@ -3223,13 +3207,13 @@ async function runCreate(ctx, opts) {
|
|
|
3223
3207
|
};
|
|
3224
3208
|
const lane = senior.kind === "lane"
|
|
3225
3209
|
? withHireGrantAdmission(hqRoot, senior.id, ({ parent, children, parentGrant, availableChildGrant }) => {
|
|
3226
|
-
if (
|
|
3210
|
+
if (isTerminalLaneState(parent.state)) {
|
|
3227
3211
|
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot hire lanes`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
3228
3212
|
}
|
|
3229
|
-
if (
|
|
3213
|
+
if (children.length >= parentGrant) {
|
|
3230
3214
|
throw hireGrantCapacityError(parent, children);
|
|
3231
3215
|
}
|
|
3232
|
-
if (
|
|
3216
|
+
if (hireGrant > availableChildGrant) {
|
|
3233
3217
|
throw new LanesError("hire_grant_carve_exceeded", `Lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining after its other alive children. Ask its senior to raise its grant before assigning ${hireGrant}.`, {
|
|
3234
3218
|
senior_lane_id: parent.lane_id,
|
|
3235
3219
|
parent_grant: parentGrant,
|
|
@@ -3622,7 +3606,6 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3622
3606
|
let grantContext;
|
|
3623
3607
|
let grantHierarchy;
|
|
3624
3608
|
if (action === "grant") {
|
|
3625
|
-
const enforcementEnabled = await ctx.laneHireGrantsEnabled(lane.company);
|
|
3626
3609
|
let hierarchy = loadAllLanes(ctx.hqRoot());
|
|
3627
3610
|
const ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
|
|
3628
3611
|
const byId = new Map(hierarchy.map((record) => [record.lane_id, record]));
|
|
@@ -3631,16 +3614,22 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3631
3614
|
}
|
|
3632
3615
|
hierarchy = [...byId.values()];
|
|
3633
3616
|
grantHierarchy = hierarchy;
|
|
3634
|
-
grantContext = createOutpostHireGrantContext(lane, hierarchy
|
|
3617
|
+
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
3635
3618
|
}
|
|
3636
3619
|
let dispatched;
|
|
3637
3620
|
try {
|
|
3638
3621
|
dispatched = await ctx.dispatchOutpostExec({
|
|
3639
3622
|
outpostId,
|
|
3640
3623
|
command: buildOutpostLaneCommand({
|
|
3641
|
-
action: action === "grant"
|
|
3624
|
+
action: action === "grant"
|
|
3625
|
+
? "grant-target"
|
|
3626
|
+
: action === "reparent"
|
|
3627
|
+
? "reparent-target"
|
|
3628
|
+
: action,
|
|
3642
3629
|
laneId: lane.lane_id,
|
|
3643
|
-
hqBinary: action === "grant"
|
|
3630
|
+
hqBinary: action === "grant" || action === "reparent"
|
|
3631
|
+
? outpostCliOverride(ctx, outpostId)
|
|
3632
|
+
: undefined,
|
|
3644
3633
|
text: options.text,
|
|
3645
3634
|
project: options.project,
|
|
3646
3635
|
story: options.story,
|
|
@@ -3650,6 +3639,8 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3650
3639
|
worker: options.worker,
|
|
3651
3640
|
keepAlive: options.keepAlive,
|
|
3652
3641
|
hire: options.hire,
|
|
3642
|
+
senior: options.senior,
|
|
3643
|
+
reason: options.reason,
|
|
3653
3644
|
grantContext,
|
|
3654
3645
|
compact: options.compact,
|
|
3655
3646
|
}),
|
|
@@ -3703,6 +3694,9 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3703
3694
|
owner_error: ownerErrorFromOutput(dispatched.stdout),
|
|
3704
3695
|
});
|
|
3705
3696
|
}
|
|
3697
|
+
const changedDescendantIds = action === "reparent"
|
|
3698
|
+
? parseReparentDescendantIds(remote?.changed_descendant_ids, lane.lane_id, outpostId)
|
|
3699
|
+
: [];
|
|
3706
3700
|
const resultConfirmed = remote !== null &&
|
|
3707
3701
|
remote.ok === true &&
|
|
3708
3702
|
remote.action === action &&
|
|
@@ -3711,8 +3705,11 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3711
3705
|
? remote.accepted === true
|
|
3712
3706
|
: action === "grant"
|
|
3713
3707
|
? remote.hire_grant === Number(options.hire)
|
|
3714
|
-
:
|
|
3715
|
-
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");
|
|
3716
3713
|
if (!resultConfirmed) {
|
|
3717
3714
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} completed exec without a confirmed ${action} result for lane ${lane.lane_id}`, {
|
|
3718
3715
|
lane_id: lane.lane_id,
|
|
@@ -3733,6 +3730,25 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3733
3730
|
});
|
|
3734
3731
|
return;
|
|
3735
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
|
+
}
|
|
3736
3752
|
printJson({
|
|
3737
3753
|
...remote,
|
|
3738
3754
|
outpost_id: outpostId,
|
|
@@ -3740,6 +3756,42 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3740
3756
|
outpost_transport: "outposts_exec",
|
|
3741
3757
|
});
|
|
3742
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
|
+
}
|
|
3743
3795
|
async function dispatchOutpostQuestionAnswer(ctx, lane, questionId, options) {
|
|
3744
3796
|
if (lane.host.kind !== "outpost") {
|
|
3745
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 });
|
|
@@ -4177,25 +4229,15 @@ async function runList(ctx, opts) {
|
|
|
4177
4229
|
})
|
|
4178
4230
|
.sort(compareLaneQueueOrder);
|
|
4179
4231
|
const ownerEligibleLaneIds = new Set();
|
|
4180
|
-
const companyContexts = new Map();
|
|
4181
4232
|
for (const lane of selectedLanes) {
|
|
4182
4233
|
if (lane.host.kind !== "outpost" ||
|
|
4183
4234
|
isTerminalLaneState(lane.state) ||
|
|
4184
4235
|
laneRunsHere(lane, host)) {
|
|
4185
4236
|
continue;
|
|
4186
4237
|
}
|
|
4187
|
-
const companyKey = JSON.stringify([
|
|
4188
|
-
lane.company?.uid ?? null,
|
|
4189
|
-
lane.company?.slug ?? null,
|
|
4190
|
-
]);
|
|
4191
4238
|
ownerEligibleLaneIds.add(lane.lane_id);
|
|
4192
|
-
companyContexts.set(companyKey, lane.company);
|
|
4193
4239
|
}
|
|
4194
|
-
const
|
|
4195
|
-
companyKey,
|
|
4196
|
-
await ctx.laneHireGrantsEnabled(company),
|
|
4197
|
-
])));
|
|
4198
|
-
const ownerLanes = [];
|
|
4240
|
+
const ownerLanes = selectedLanes.filter((lane) => ownerEligibleLaneIds.has(lane.lane_id));
|
|
4199
4241
|
const hireByLane = new Map();
|
|
4200
4242
|
for (const lane of selectedLanes) {
|
|
4201
4243
|
if (!ownerEligibleLaneIds.has(lane.lane_id)) {
|
|
@@ -4205,18 +4247,6 @@ async function runList(ctx, opts) {
|
|
|
4205
4247
|
});
|
|
4206
4248
|
continue;
|
|
4207
4249
|
}
|
|
4208
|
-
const companyKey = JSON.stringify([
|
|
4209
|
-
lane.company?.uid ?? null,
|
|
4210
|
-
lane.company?.slug ?? null,
|
|
4211
|
-
]);
|
|
4212
|
-
if (enabledByCompany.get(companyKey))
|
|
4213
|
-
ownerLanes.push(lane);
|
|
4214
|
-
else {
|
|
4215
|
-
hireByLane.set(lane.lane_id, {
|
|
4216
|
-
...laneHireFields(lane, allLanes),
|
|
4217
|
-
hire_source: "local",
|
|
4218
|
-
});
|
|
4219
|
-
}
|
|
4220
4250
|
}
|
|
4221
4251
|
if (ownerLanes.length > 0) {
|
|
4222
4252
|
const ownerSummaries = await batchOwnerLaneHireFields(ctx, ownerLanes);
|
|
@@ -4760,9 +4790,38 @@ async function runMoveTarget(ctx, laneId, opts) {
|
|
|
4760
4790
|
account: moved.account ?? null,
|
|
4761
4791
|
});
|
|
4762
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
|
+
}
|
|
4763
4820
|
async function runShow(ctx, laneId) {
|
|
4764
4821
|
const hqRoot = ctx.hqRoot();
|
|
4765
|
-
const
|
|
4822
|
+
const initialLane = readLane(hqRoot, laneId);
|
|
4823
|
+
const initialLanes = loadAllLanes(hqRoot);
|
|
4824
|
+
const storedLane = await refreshOutpostLaneCopiesForShow(ctx, initialLane, initialLanes);
|
|
4766
4825
|
const allLanes = loadAllLanes(hqRoot);
|
|
4767
4826
|
const hire = await laneHirePresentation(ctx, storedLane, allLanes);
|
|
4768
4827
|
const remoteQueued = storedLane.state === "queued" &&
|
|
@@ -4852,8 +4911,6 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
4852
4911
|
(!grantContext || !sameHireGrantSenior(initial.senior, grantContext.expected_senior))) {
|
|
4853
4912
|
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed after the primary authorized its grant update`, { lane_id: laneId, current_senior: initial.senior });
|
|
4854
4913
|
}
|
|
4855
|
-
const enforce = grantContext?.enforcement_enabled ??
|
|
4856
|
-
await ctx.laneHireGrantsEnabled(initial.company);
|
|
4857
4914
|
const lockedLaneIds = [
|
|
4858
4915
|
laneId,
|
|
4859
4916
|
...(initial.senior.kind === "lane" && initial.senior.id
|
|
@@ -4878,7 +4935,7 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
4878
4935
|
? mergeOwnerLaneHierarchy(grantContext.hierarchy, loadAllLanes(hqRoot))
|
|
4879
4936
|
: grantContext.hierarchy
|
|
4880
4937
|
: loadAllLanes(hqRoot);
|
|
4881
|
-
if (
|
|
4938
|
+
if (current.senior.kind === "lane") {
|
|
4882
4939
|
if (!current.senior.id) {
|
|
4883
4940
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
4884
4941
|
lane_id: laneId,
|
|
@@ -4908,19 +4965,17 @@ async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContex
|
|
|
4908
4965
|
});
|
|
4909
4966
|
}
|
|
4910
4967
|
}
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
});
|
|
4923
|
-
}
|
|
4968
|
+
const children = aliveDirectChildren(hierarchy, current.lane_id);
|
|
4969
|
+
const allocated = assignedChildHireGrants(children);
|
|
4970
|
+
const minimumGrant = Math.max(allocated, children.length);
|
|
4971
|
+
if (hireGrant < minimumGrant) {
|
|
4972
|
+
throw new LanesError(allocated > children.length ? "hire_grant_below_allocated" : "hire_grant_below_occupancy", `Lane ${laneId} cannot lower its grant to ${hireGrant}; it has ${children.length} alive children and ${allocated} grant allocated to them`, {
|
|
4973
|
+
lane_id: laneId,
|
|
4974
|
+
requested_hire_grant: hireGrant,
|
|
4975
|
+
alive_children: children.map((child) => child.lane_id),
|
|
4976
|
+
alive_child_count: children.length,
|
|
4977
|
+
assigned_child_grants: allocated,
|
|
4978
|
+
});
|
|
4924
4979
|
}
|
|
4925
4980
|
return retryUpdateLane(hqRoot, laneId, "command", (record) => ({
|
|
4926
4981
|
...record,
|
|
@@ -5011,11 +5066,8 @@ async function runBackfillGrants(ctx, opts) {
|
|
|
5011
5066
|
}
|
|
5012
5067
|
const apply = Boolean(opts.apply);
|
|
5013
5068
|
const hqRoot = ctx.hqRoot();
|
|
5014
|
-
if (apply
|
|
5015
|
-
throw new LanesError("hire_grant_backfill_gate_enabled", "
|
|
5016
|
-
}
|
|
5017
|
-
if (apply && ctx.env.HQ_LANE_ID?.trim()) {
|
|
5018
|
-
throw new LanesError("hire_grant_backfill_unauthorized", "A lane cannot apply the hire-grant backfill; run it from an operator session", {});
|
|
5069
|
+
if (apply) {
|
|
5070
|
+
throw new LanesError("hire_grant_backfill_gate_enabled", "Grant backfill can only be applied before always-on enforcement is released", {});
|
|
5019
5071
|
}
|
|
5020
5072
|
const allLanes = loadAllLanes(hqRoot);
|
|
5021
5073
|
const profileIds = new Set(allLanes
|
|
@@ -5040,76 +5092,11 @@ async function runBackfillGrants(ctx, opts) {
|
|
|
5040
5092
|
if (maxConcurrent !== undefined)
|
|
5041
5093
|
profileMaxConcurrent.set(profileId, maxConcurrent);
|
|
5042
5094
|
}
|
|
5043
|
-
|
|
5044
|
-
if (apply) {
|
|
5045
|
-
const lanesById = new Map(allLanes.map((lane) => [lane.lane_id, lane]));
|
|
5046
|
-
const targetCompanies = new Map();
|
|
5047
|
-
for (const row of rows) {
|
|
5048
|
-
if (row.action === "unchanged")
|
|
5049
|
-
continue;
|
|
5050
|
-
const lane = lanesById.get(row.lane_id);
|
|
5051
|
-
if (!lane) {
|
|
5052
|
-
throw new LanesError("hire_grant_backfill_target_changed", `Backfill target lane ${row.lane_id} is unavailable`, { lane_id: row.lane_id });
|
|
5053
|
-
}
|
|
5054
|
-
const company = lane.company;
|
|
5055
|
-
if (!company)
|
|
5056
|
-
continue;
|
|
5057
|
-
targetCompanies.set(JSON.stringify([company.uid ?? null, company.slug ?? null]), company);
|
|
5058
|
-
}
|
|
5059
|
-
const companyGates = await Promise.all([...targetCompanies.values()].map((company) => ctx.laneHireGrantsEnabled(company)));
|
|
5060
|
-
if (companyGates.some(Boolean)) {
|
|
5061
|
-
throw new LanesError("hire_grant_backfill_gate_enabled", "Backfill must be applied and verified before lanes.hire-grants is enabled", { flag: LANE_HIRE_GRANTS_FLAG });
|
|
5062
|
-
}
|
|
5063
|
-
const shortfalls = rows.filter((row) => row.grant_shortfall > 0);
|
|
5064
|
-
if (shortfalls.length > 0) {
|
|
5065
|
-
throw new LanesError("hire_grant_backfill_allocation_shortfall", "Existing parent grants do not cover the grants allocated to alive children; resolve these rows before applying the backfill", { lanes: shortfalls });
|
|
5066
|
-
}
|
|
5067
|
-
rows = rows.map((row) => {
|
|
5068
|
-
if (row.action === "unchanged")
|
|
5069
|
-
return row;
|
|
5070
|
-
const lane = readLane(hqRoot, row.lane_id);
|
|
5071
|
-
const lockIds = [
|
|
5072
|
-
lane.lane_id,
|
|
5073
|
-
...(lane.senior.kind === "lane" && lane.senior.id ? [lane.senior.id] : []),
|
|
5074
|
-
];
|
|
5075
|
-
return withLaneHireLocks(hqRoot, lockIds, () => {
|
|
5076
|
-
const current = readLane(hqRoot, row.lane_id);
|
|
5077
|
-
if (current.hire_grant !== undefined) {
|
|
5078
|
-
return {
|
|
5079
|
-
...row,
|
|
5080
|
-
existing_grant: current.hire_grant,
|
|
5081
|
-
proposed_grant: current.hire_grant,
|
|
5082
|
-
action: "unchanged",
|
|
5083
|
-
};
|
|
5084
|
-
}
|
|
5085
|
-
const latestLanes = loadAllLanes(hqRoot);
|
|
5086
|
-
const latestRow = planHireGrantBackfill(latestLanes, profileMaxConcurrent)
|
|
5087
|
-
.find((candidate) => candidate.lane_id === current.lane_id);
|
|
5088
|
-
if (!latestRow) {
|
|
5089
|
-
throw new LanesError("hire_grant_backfill_target_changed", `Lane ${current.lane_id} no longer qualifies for the hire-grant backfill`, { lane_id: current.lane_id });
|
|
5090
|
-
}
|
|
5091
|
-
if (latestRow.grant_shortfall > 0) {
|
|
5092
|
-
throw new LanesError("hire_grant_backfill_allocation_shortfall", `Lane ${current.lane_id}'s existing grant does not cover grants allocated to its alive children`, { lane: latestRow });
|
|
5093
|
-
}
|
|
5094
|
-
const proposed = latestRow.proposed_grant;
|
|
5095
|
-
const written = retryUpdateLane(hqRoot, current.lane_id, "command", (record) => ({
|
|
5096
|
-
...record,
|
|
5097
|
-
...(record.hire_grant === undefined ? { hire_grant: proposed } : {}),
|
|
5098
|
-
}));
|
|
5099
|
-
const value = written.hire_grant ?? 0;
|
|
5100
|
-
return {
|
|
5101
|
-
...row,
|
|
5102
|
-
existing_grant: value,
|
|
5103
|
-
proposed_grant: value,
|
|
5104
|
-
action: value === proposed ? "set" : "unchanged",
|
|
5105
|
-
};
|
|
5106
|
-
});
|
|
5107
|
-
});
|
|
5108
|
-
}
|
|
5095
|
+
const rows = planHireGrantBackfill(allLanes, profileMaxConcurrent);
|
|
5109
5096
|
printJson({
|
|
5110
5097
|
ok: true,
|
|
5111
5098
|
action: "backfill-grants",
|
|
5112
|
-
mode:
|
|
5099
|
+
mode: "dry-run",
|
|
5113
5100
|
lanes: rows,
|
|
5114
5101
|
});
|
|
5115
5102
|
}
|
|
@@ -5584,7 +5571,6 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5584
5571
|
async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, options = {}) {
|
|
5585
5572
|
if (lane.provider !== "claude")
|
|
5586
5573
|
return undefined;
|
|
5587
|
-
const balanceEnabled = await ctx.laneClaudeAccountBalanceEnabled(lane.company);
|
|
5588
5574
|
const hqRoot = ctx.hqRoot();
|
|
5589
5575
|
const config = loadLanesConfig(hqRoot);
|
|
5590
5576
|
const currentConfig = lane.account === undefined
|
|
@@ -5601,19 +5587,6 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5601
5587
|
const accountNeedsRepair = lane.account !== undefined &&
|
|
5602
5588
|
(currentConfig?.enabled !== true || currentLogin === false);
|
|
5603
5589
|
const repairUnavailableAccount = options.repairUnavailableAccount === true && accountNeedsRepair;
|
|
5604
|
-
if (!balanceEnabled && !(options.repairUnavailableAccount && accountNeedsRepair)) {
|
|
5605
|
-
if (lane.account === undefined)
|
|
5606
|
-
return undefined;
|
|
5607
|
-
if (currentConfig === undefined || !currentConfig.enabled) {
|
|
5608
|
-
throw new AccountResolutionError(currentConfig === undefined ? "unknown_account" : "account_disabled", currentConfig === undefined
|
|
5609
|
-
? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for claude`
|
|
5610
|
-
: `Account ${JSON.stringify(lane.account)} is unavailable for claude`, { provider: "claude", account: lane.account });
|
|
5611
|
-
}
|
|
5612
|
-
if (currentLogin === false) {
|
|
5613
|
-
throw new LanesError("account_not_logged_in", `Account ${lane.account} for claude is not logged in`, { provider: "claude", account: lane.account, lane_id: lane.lane_id });
|
|
5614
|
-
}
|
|
5615
|
-
return undefined;
|
|
5616
|
-
}
|
|
5617
5590
|
const profile = workerProfile?.profile ?? (lane.worker_profile !== undefined
|
|
5618
5591
|
? loadWorkerProfile(hqRoot, lane.worker_profile)
|
|
5619
5592
|
: undefined);
|
|
@@ -5641,28 +5614,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5641
5614
|
const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
5642
5615
|
if (!accountNeedsRepair && !mayRebalance)
|
|
5643
5616
|
return undefined;
|
|
5644
|
-
const balanceHosts =
|
|
5645
|
-
? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
|
|
5646
|
-
: (() => {
|
|
5647
|
-
const lanes = loadAllLanes(hqRoot).filter((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
|
|
5648
|
-
return {
|
|
5649
|
-
snapshots: [{
|
|
5650
|
-
capacity: buildHostCapacitySnapshot({
|
|
5651
|
-
host: lane.host,
|
|
5652
|
-
lanes,
|
|
5653
|
-
config,
|
|
5654
|
-
existsSync: ctx.existsSync,
|
|
5655
|
-
probe: ctx.probeProvider,
|
|
5656
|
-
env: ctx.env,
|
|
5657
|
-
hostname: ctx.hostname(),
|
|
5658
|
-
now: ctx.now(),
|
|
5659
|
-
}),
|
|
5660
|
-
lanes,
|
|
5661
|
-
laneListAvailable: true,
|
|
5662
|
-
}],
|
|
5663
|
-
unavailableHosts: [],
|
|
5664
|
-
};
|
|
5665
|
-
})();
|
|
5617
|
+
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
5666
5618
|
const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
|
|
5667
5619
|
const currentTargetAccount = lane.account === undefined
|
|
5668
5620
|
? undefined
|
|
@@ -6294,7 +6246,6 @@ async function applyClaudeBalanceMove(ctx, suggestion) {
|
|
|
6294
6246
|
return { lane_id: suggestion.lane_id, host: suggestion.host, from: suggestion.from, to: suggestion.to, state: suggestion.state };
|
|
6295
6247
|
}
|
|
6296
6248
|
const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), suggestion.lane_id));
|
|
6297
|
-
assertBalanceApplyEnabled(await ctx.laneClaudeAccountBalanceEnabled(lane.company));
|
|
6298
6249
|
if (lane.state === "running") {
|
|
6299
6250
|
throw new LanesError("lane_running", `Lane ${lane.lane_id} is running; balance never moves a running turn`, {
|
|
6300
6251
|
lane_id: lane.lane_id,
|
|
@@ -6324,7 +6275,6 @@ async function applyClaudeBalanceMove(ctx, suggestion) {
|
|
|
6324
6275
|
}
|
|
6325
6276
|
async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
|
|
6326
6277
|
const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
|
|
6327
|
-
assertBalanceApplyEnabled(await ctx.laneClaudeAccountBalanceEnabled(lane.company));
|
|
6328
6278
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
|
|
6329
6279
|
if (lane.provider !== "claude") {
|
|
6330
6280
|
throw new LanesError("claude_account_balance_provider_mismatch", `Lane ${laneId} does not use Claude`, {
|
|
@@ -7065,6 +7015,29 @@ function archiveResumeMessage(hqRoot, laneId, message) {
|
|
|
7065
7015
|
fs.writeFileSync(path.join(dir, `${message.message_id}.resume.jsonl`), `${JSON.stringify(message)}\n`, "utf8");
|
|
7066
7016
|
}
|
|
7067
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) {
|
|
7068
7041
|
const hqRoot = ctx.hqRoot();
|
|
7069
7042
|
if (!opts.senior) {
|
|
7070
7043
|
throw new LanesError("missing_senior", "--senior is required", {});
|
|
@@ -7072,19 +7045,32 @@ async function runReparent(ctx, laneId, opts) {
|
|
|
7072
7045
|
const nextSenior = parseSeniorRef(opts.senior);
|
|
7073
7046
|
assertSeniorExists(hqRoot, nextSenior, ctx);
|
|
7074
7047
|
const initial = readLane(hqRoot, laneId);
|
|
7075
|
-
const needsHireGrantGate = nextSenior.kind === "lane" &&
|
|
7076
|
-
seniorKey(initial.senior) !== seniorKey(nextSenior);
|
|
7077
|
-
const hireGrantsEnabled = needsHireGrantGate
|
|
7078
|
-
? await ctx.laneHireGrantsEnabled(initial.company)
|
|
7079
|
-
: false;
|
|
7080
7048
|
const lockIds = [
|
|
7081
7049
|
laneId,
|
|
7082
7050
|
...(initial.senior.kind === "lane" && initial.senior.id ? [initial.senior.id] : []),
|
|
7083
7051
|
...(nextSenior.kind === "lane" && nextSenior.id ? [nextSenior.id] : []),
|
|
7084
7052
|
];
|
|
7085
|
-
withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts
|
|
7053
|
+
return withLaneHireLocks(hqRoot, lockIds, () => runReparentLocked(ctx, laneId, opts));
|
|
7086
7054
|
}
|
|
7087
|
-
function
|
|
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
|
+
});
|
|
7072
|
+
}
|
|
7073
|
+
function runReparentLocked(ctx, laneId, opts) {
|
|
7088
7074
|
const hqRoot = ctx.hqRoot();
|
|
7089
7075
|
if (!opts.senior) {
|
|
7090
7076
|
throw new LanesError("missing_senior", "--senior is required", {});
|
|
@@ -7098,8 +7084,7 @@ function runReparentLocked(ctx, laneId, opts, hireGrantsEnabled) {
|
|
|
7098
7084
|
}
|
|
7099
7085
|
const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
|
|
7100
7086
|
assertSameTenant(lane.company, nextSenior, seniorCompany, { lane_id: laneId });
|
|
7101
|
-
if (
|
|
7102
|
-
nextSenior.kind === "lane" &&
|
|
7087
|
+
if (nextSenior.kind === "lane" &&
|
|
7103
7088
|
seniorKey(lane.senior) !== seniorKey(nextSenior)) {
|
|
7104
7089
|
const parent = readLane(hqRoot, nextSenior.id);
|
|
7105
7090
|
if (isTerminalLaneState(parent.state)) {
|
|
@@ -7154,7 +7139,11 @@ function runReparentLocked(ctx, laneId, opts, hireGrantsEnabled) {
|
|
|
7154
7139
|
to_depth: descendantDepth,
|
|
7155
7140
|
});
|
|
7156
7141
|
}
|
|
7157
|
-
descendants.push({
|
|
7142
|
+
descendants.push({
|
|
7143
|
+
laneId: other.lane_id,
|
|
7144
|
+
depth: descendantDepth,
|
|
7145
|
+
changed: descendantDepth !== oldDescendantDepth,
|
|
7146
|
+
});
|
|
7158
7147
|
}
|
|
7159
7148
|
const at = ctx.now().toISOString();
|
|
7160
7149
|
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
@@ -7199,12 +7188,13 @@ function runReparentLocked(ctx, laneId, opts, hireGrantsEnabled) {
|
|
|
7199
7188
|
};
|
|
7200
7189
|
});
|
|
7201
7190
|
}
|
|
7202
|
-
|
|
7203
|
-
ok: true,
|
|
7204
|
-
action: "reparent",
|
|
7191
|
+
return {
|
|
7205
7192
|
lane: updated,
|
|
7206
|
-
|
|
7207
|
-
|
|
7193
|
+
pendingQuestions: pending.length,
|
|
7194
|
+
changedDescendantIds: descendants
|
|
7195
|
+
.filter((descendant) => descendant.changed)
|
|
7196
|
+
.map((descendant) => descendant.laneId),
|
|
7197
|
+
};
|
|
7208
7198
|
}
|
|
7209
7199
|
const CANONICAL_LOG_STREAMS = [
|
|
7210
7200
|
"watcher.err",
|
|
@@ -8558,27 +8548,9 @@ async function runAccountsBalance(ctx, opts) {
|
|
|
8558
8548
|
const hqRoot = ctx.hqRoot();
|
|
8559
8549
|
const config = loadLanesConfig(hqRoot);
|
|
8560
8550
|
const loaded = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
const
|
|
8564
|
-
for (const lane of lanes) {
|
|
8565
|
-
const companyKey = JSON.stringify([lane.company?.uid ?? null, lane.company?.slug ?? null]);
|
|
8566
|
-
let enabled = companyBalanceGateEnabled.get(companyKey);
|
|
8567
|
-
if (enabled === undefined) {
|
|
8568
|
-
enabled = await ctx.laneClaudeAccountBalanceEnabled(lane.company);
|
|
8569
|
-
companyBalanceGateEnabled.set(companyKey, enabled);
|
|
8570
|
-
}
|
|
8571
|
-
laneBalanceGateEnabled.set(lane.lane_id, enabled);
|
|
8572
|
-
}
|
|
8573
|
-
// With no lanes there is no tenant scope to resolve, so report and enforce
|
|
8574
|
-
// the unscoped/default gate. Otherwise the report reflects whether any
|
|
8575
|
-
// represented lane company has the gate enabled.
|
|
8576
|
-
if (lanes.length === 0) {
|
|
8577
|
-
companyBalanceGateEnabled.set(JSON.stringify([null, null]), await ctx.laneClaudeAccountBalanceEnabled());
|
|
8578
|
-
}
|
|
8579
|
-
const balanceGateEnabled = [...companyBalanceGateEnabled.values()].some(Boolean);
|
|
8580
|
-
if (opts.apply)
|
|
8581
|
-
assertBalanceApplyEnabled(balanceGateEnabled);
|
|
8551
|
+
// Retain the established response field for clients; balancing is now
|
|
8552
|
+
// unconditional, so this compatibility value is always true.
|
|
8553
|
+
const balanceGateEnabled = true;
|
|
8582
8554
|
const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
|
|
8583
8555
|
const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
|
|
8584
8556
|
const selections = loaded.snapshots.map((host) => {
|
|
@@ -8640,16 +8612,6 @@ async function runAccountsBalance(ctx, opts) {
|
|
|
8640
8612
|
});
|
|
8641
8613
|
continue;
|
|
8642
8614
|
}
|
|
8643
|
-
if (laneBalanceGateEnabled.get(suggestion.lane_id) !== true) {
|
|
8644
|
-
skipped.push({
|
|
8645
|
-
lane_id: suggestion.lane_id,
|
|
8646
|
-
host: suggestion.host,
|
|
8647
|
-
from: suggestion.from,
|
|
8648
|
-
to: suggestion.to,
|
|
8649
|
-
reason: "claude_account_balance_disabled",
|
|
8650
|
-
});
|
|
8651
|
-
continue;
|
|
8652
|
-
}
|
|
8653
8615
|
try {
|
|
8654
8616
|
moved.push(await applyClaudeBalanceMove(ctx, suggestion));
|
|
8655
8617
|
}
|
|
@@ -9165,6 +9127,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9165
9127
|
.action(wrap(async (opts) => {
|
|
9166
9128
|
await runCreate(ctx, opts);
|
|
9167
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
|
+
}));
|
|
9168
9138
|
lanes
|
|
9169
9139
|
.command("grant-target", { hidden: true })
|
|
9170
9140
|
.argument("<lane-id>", "Lane id")
|
|
@@ -9514,7 +9484,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9514
9484
|
.command("balance")
|
|
9515
9485
|
.description("Report Claude account load and suggest safe between-turn moves")
|
|
9516
9486
|
.requiredOption("--provider <name>", "claude")
|
|
9517
|
-
.option("--apply", "Move eligible between-turn sessions
|
|
9487
|
+
.option("--apply", "Move eligible between-turn sessions to balance Claude accounts")
|
|
9518
9488
|
.option("--json", "JSON on stdout (always)")
|
|
9519
9489
|
.action(wrap(async (opts) => {
|
|
9520
9490
|
await runAccountsBalance(ctx, opts);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AuthState } from "./accounts.js";
|
|
2
2
|
import type { HostCapacityAccount, HostCapacitySnapshot } from "./host-capacity.js";
|
|
3
3
|
import type { LaneHost, LaneRecord, LanesConfig } from "./types.js";
|
|
4
|
-
export declare const CLAUDE_ACCOUNT_BALANCE_FLAG = "lanes.claude-account-balance";
|
|
5
4
|
export declare const BALANCE_LIVE_STATES: readonly ["running", "queued", "awaiting_input"];
|
|
6
5
|
export declare const BALANCE_MOVABLE_STATES: readonly ["interrupted", "done", "awaiting_input"];
|
|
7
6
|
export interface BalanceHostSnapshot {
|
|
@@ -82,5 +81,4 @@ export declare function planClaudeBalanceMoves(input: {
|
|
|
82
81
|
hosts: readonly BalanceHostSnapshot[];
|
|
83
82
|
config: LanesConfig;
|
|
84
83
|
}): BalanceMoveSuggestion[];
|
|
85
|
-
|
|
86
|
-
//# sourceMappingURL=claude-account-balance.d.ts.map
|
|
84
|
+
//# sourceMappingURL=claude-balance.d.ts.map
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { LanesError } from "./errors.js";
|
|
2
|
-
export const CLAUDE_ACCOUNT_BALANCE_FLAG = "lanes.claude-account-balance";
|
|
3
2
|
export const BALANCE_LIVE_STATES = ["running", "queued", "awaiting_input"];
|
|
4
3
|
export const BALANCE_MOVABLE_STATES = ["interrupted", "done", "awaiting_input"];
|
|
5
4
|
function sameHost(a, b) {
|
|
@@ -263,9 +262,4 @@ export function planClaudeBalanceMoves(input) {
|
|
|
263
262
|
}
|
|
264
263
|
return suggestions;
|
|
265
264
|
}
|
|
266
|
-
|
|
267
|
-
if (!enabled) {
|
|
268
|
-
throw new LanesError("claude_account_balance_disabled", `Applying Claude account balance moves requires hq-flags gate ${CLAUDE_ACCOUNT_BALANCE_FLAG}`, { flag: CLAUDE_ACCOUNT_BALANCE_FLAG });
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
//# sourceMappingURL=claude-account-balance.js.map
|
|
265
|
+
//# sourceMappingURL=claude-balance.js.map
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type LaneRecord } from "./types.js";
|
|
2
2
|
import { LanesError } from "./errors.js";
|
|
3
|
-
export declare const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
|
|
4
3
|
export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" | "hire_grant">;
|
|
5
4
|
export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
|
|
6
5
|
export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
|
|
@@ -27,4 +26,4 @@ export interface HireGrantBackfillRow {
|
|
|
27
26
|
}
|
|
28
27
|
/** Select alive manager lanes and calculate the non-destructive one-time backfill. */
|
|
29
28
|
export declare function planHireGrantBackfill(lanes: readonly LaneRecord[], profileMaxConcurrent: ReadonlyMap<string, number>): HireGrantBackfillRow[];
|
|
30
|
-
//# sourceMappingURL=
|
|
29
|
+
//# sourceMappingURL=grants.d.ts.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isTerminalLaneState } from "./types.js";
|
|
2
2
|
import { listLaneIds, readLane, withLaneHireLocks } from "./store.js";
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
|
-
export const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
|
|
5
4
|
export function aliveDirectChildren(lanes, parentLaneId) {
|
|
6
5
|
return lanes.filter((lane) => lane.senior.kind === "lane" &&
|
|
7
6
|
lane.senior.id === parentLaneId &&
|
|
@@ -144,4 +143,4 @@ export function planHireGrantBackfill(lanes, profileMaxConcurrent) {
|
|
|
144
143
|
})
|
|
145
144
|
.sort((a, b) => a.lane_id.localeCompare(b.lane_id));
|
|
146
145
|
}
|
|
147
|
-
//# sourceMappingURL=
|
|
146
|
+
//# sourceMappingURL=grants.js.map
|
|
@@ -13,7 +13,7 @@ export interface OutpostHireGrantContext {
|
|
|
13
13
|
hierarchy: HireGrantLaneSnapshot[];
|
|
14
14
|
}
|
|
15
15
|
/** Encode only the owner-relevant hierarchy from the primary's complete store. */
|
|
16
|
-
export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[]
|
|
16
|
+
export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[]): string;
|
|
17
17
|
/** Decode and validate an owner grant context, binding it to the target lane. */
|
|
18
18
|
export declare function parseOutpostHireGrantContext(encoded: string, targetLaneId: string): OutpostHireGrantContext;
|
|
19
19
|
export declare function sameHireGrantSenior(a: SeniorRef, b: SeniorRef): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { aliveDirectChildren } from "./
|
|
1
|
+
import { aliveDirectChildren } from "./grants.js";
|
|
2
2
|
import { LanesError } from "./errors.js";
|
|
3
3
|
import { isLaneState, isSeniorKind, } from "./types.js";
|
|
4
4
|
const HIRE_GRANT_CONTEXT_SCHEMA = "hq-lane-hire-grant-context.v1";
|
|
@@ -24,29 +24,27 @@ function snapshot(lane) {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
/** Encode only the owner-relevant hierarchy from the primary's complete store. */
|
|
27
|
-
export function createOutpostHireGrantContext(target, lanes
|
|
27
|
+
export function createOutpostHireGrantContext(target, lanes) {
|
|
28
28
|
const current = lanes.find((lane) => lane.lane_id === target.lane_id);
|
|
29
29
|
if (!current || !sameSenior(current.senior, target.senior)) {
|
|
30
30
|
throw new LanesError("hire_grant_hierarchy_unavailable", `Cannot build an authoritative hire-grant hierarchy for lane ${target.lane_id}`, { lane_id: target.lane_id });
|
|
31
31
|
}
|
|
32
32
|
const relevant = new Map();
|
|
33
33
|
relevant.set(current.lane_id, current);
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new LanesError("hire_grant_senior_invalid", `Lane ${target.lane_id} has no senior lane in the primary hierarchy`, { lane_id: target.lane_id, senior_lane_id: parentId ?? null });
|
|
40
|
-
}
|
|
41
|
-
relevant.set(parent.lane_id, parent);
|
|
42
|
-
for (const sibling of aliveDirectChildren(lanes, parent.lane_id)) {
|
|
43
|
-
relevant.set(sibling.lane_id, sibling);
|
|
44
|
-
}
|
|
34
|
+
if (current.senior.kind === "lane") {
|
|
35
|
+
const parentId = current.senior.id;
|
|
36
|
+
const parent = parentId ? lanes.find((lane) => lane.lane_id === parentId) : undefined;
|
|
37
|
+
if (!parent) {
|
|
38
|
+
throw new LanesError("hire_grant_senior_invalid", `Lane ${target.lane_id} has no senior lane in the primary hierarchy`, { lane_id: target.lane_id, senior_lane_id: parentId ?? null });
|
|
45
39
|
}
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
relevant.set(parent.lane_id, parent);
|
|
41
|
+
for (const sibling of aliveDirectChildren(lanes, parent.lane_id)) {
|
|
42
|
+
relevant.set(sibling.lane_id, sibling);
|
|
48
43
|
}
|
|
49
44
|
}
|
|
45
|
+
for (const child of aliveDirectChildren(lanes, current.lane_id)) {
|
|
46
|
+
relevant.set(child.lane_id, child);
|
|
47
|
+
}
|
|
50
48
|
const hierarchy = [...relevant.values()]
|
|
51
49
|
.map(snapshot)
|
|
52
50
|
.sort((a, b) => a.lane_id.localeCompare(b.lane_id));
|
|
@@ -55,7 +53,7 @@ export function createOutpostHireGrantContext(target, lanes, enforcementEnabled)
|
|
|
55
53
|
}
|
|
56
54
|
const context = {
|
|
57
55
|
schema_version: HIRE_GRANT_CONTEXT_SCHEMA,
|
|
58
|
-
enforcement_enabled:
|
|
56
|
+
enforcement_enabled: true,
|
|
59
57
|
expected_senior: copySenior(current.senior),
|
|
60
58
|
hierarchy,
|
|
61
59
|
};
|
|
@@ -150,7 +148,7 @@ export function parseOutpostHireGrantContext(encoded, targetLaneId) {
|
|
|
150
148
|
if (!target || !sameSenior(target.senior, expectedSenior)) {
|
|
151
149
|
throw contextError("Outpost grant context does not match the target's expected senior");
|
|
152
150
|
}
|
|
153
|
-
if (
|
|
151
|
+
if (expectedSenior.kind === "lane" &&
|
|
154
152
|
(!expectedSenior.id || !byId.has(expectedSenior.id))) {
|
|
155
153
|
throw contextError("Outpost grant context is missing the target's senior lane");
|
|
156
154
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError,
|
|
7
|
-
export type { HireGrantBackfillRow } from "./
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
7
|
+
export type { HireGrantBackfillRow } from "./grants.js";
|
|
8
8
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
9
9
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
10
10
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError,
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
7
7
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
8
8
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
9
9
|
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";
|
|
@@ -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);
|
|
@@ -15,7 +15,7 @@ import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
|
|
|
15
15
|
import { MAX_LANE_DEPTH } from "./constants.js";
|
|
16
16
|
import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
|
|
17
17
|
import { loadWorkerProfile } from "./profile.js";
|
|
18
|
-
import {
|
|
18
|
+
import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
19
19
|
import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
|
|
20
20
|
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
21
21
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, updateLane, updateQuestion, } from "./store.js";
|
|
@@ -807,7 +807,6 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
807
807
|
}
|
|
808
808
|
const readRegistryFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
809
809
|
const keepAliveFlagCompanies = new Map();
|
|
810
|
-
const keepAliveHireGrantFlagCompanies = new Map();
|
|
811
810
|
for (const lane of lanes) {
|
|
812
811
|
if (!(lane.state === "done" || lane.state === "interrupted" || lane.state === "killed" || lane.state === "cancelled" || lane.state === "failed") ||
|
|
813
812
|
(lane.keep_alive !== true && lane.worker_profile === undefined) ||
|
|
@@ -824,30 +823,18 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
824
823
|
}
|
|
825
824
|
const key = keepAliveCompanyKey(lane.company);
|
|
826
825
|
keepAliveFlagCompanies.set(key, lane.company);
|
|
827
|
-
if (lane.senior.kind === "lane" && lane.senior.id) {
|
|
828
|
-
keepAliveHireGrantFlagCompanies.set(key, lane.company);
|
|
829
|
-
}
|
|
830
826
|
}
|
|
831
827
|
const keepAliveFlagByCompany = new Map();
|
|
832
|
-
const keepAliveHireGrantFlagByCompany = new Map();
|
|
833
828
|
const env = deps.env ?? process.env;
|
|
834
|
-
await Promise.all([
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}),
|
|
841
|
-
...[...keepAliveHireGrantFlagCompanies.entries()].map(async ([key, company]) => {
|
|
842
|
-
keepAliveHireGrantFlagByCompany.set(key, await readRegistryFlag(LANE_HIRE_GRANTS_FLAG, {
|
|
843
|
-
company: company ?? undefined,
|
|
844
|
-
env,
|
|
845
|
-
}));
|
|
846
|
-
}),
|
|
847
|
-
]);
|
|
829
|
+
await Promise.all([...keepAliveFlagCompanies.entries()].map(async ([key, company]) => {
|
|
830
|
+
keepAliveFlagByCompany.set(key, await readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
|
|
831
|
+
company: company ?? undefined,
|
|
832
|
+
env,
|
|
833
|
+
}));
|
|
834
|
+
}));
|
|
848
835
|
await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
849
836
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
850
|
-
resumeKeepAlive(hqRoot, freshLanes, summary, deps, now, keepAliveFlagByCompany
|
|
837
|
+
resumeKeepAlive(hqRoot, freshLanes, summary, deps, now, keepAliveFlagByCompany);
|
|
851
838
|
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
852
839
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
853
840
|
await admitQueued(hqRoot, drainLanes, summary, deps, now);
|
|
@@ -1545,7 +1532,7 @@ function latestEnvelopeMtimeMs(hqRoot, lane, ref) {
|
|
|
1545
1532
|
throw new LanesError("keep_alive_envelope_timestamp_unavailable", `Cannot stat latest envelope for lane ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: lane.lane_id, path: filePath });
|
|
1546
1533
|
}
|
|
1547
1534
|
}
|
|
1548
|
-
function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany
|
|
1535
|
+
function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany) {
|
|
1549
1536
|
const nowMs = now.getTime();
|
|
1550
1537
|
const nowIso = now.toISOString();
|
|
1551
1538
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -1882,8 +1869,7 @@ function resumeKeepAlive(hqRoot, lanes, summary, deps, now, flagByCompany, hireG
|
|
|
1882
1869
|
}), deps.dryRun);
|
|
1883
1870
|
const seniorId = current.senior.kind === "lane" ? current.senior.id : undefined;
|
|
1884
1871
|
let queued;
|
|
1885
|
-
if (seniorId
|
|
1886
|
-
hireGrantFlagByCompany.get(keepAliveCompanyKey(current.company)) === true) {
|
|
1872
|
+
if (seniorId) {
|
|
1887
1873
|
try {
|
|
1888
1874
|
queued = withHireGrantAdmission(hqRoot, seniorId, (snapshot) => {
|
|
1889
1875
|
const latestLane = readLane(hqRoot, current.lane_id);
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import { PROVIDER_HOME_ENV, accountStatus, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
|
|
16
16
|
import { effectiveAccountHome } from "./account-home.js";
|
|
17
17
|
import { buildHostCapacitySnapshot } from "./host-capacity.js";
|
|
18
|
-
import { selectClaudeAccount } from "./claude-
|
|
18
|
+
import { selectClaudeAccount } from "./claude-balance.js";
|
|
19
19
|
import { COMPACTION_PROMPT, compactionAfterUsage, compactionDue, compactLaneSession, } from "./compaction.js";
|
|
20
20
|
import { installDrainHook } from "./hooks-install.js";
|
|
21
21
|
import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
|