@indigoai-us/hq-cli 5.275.0 → 5.277.0
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 +112 -0
- package/README.md +12 -0
- package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
- package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
- package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
- package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
- package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
- package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
- package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
- package/dist/command-catalog.generated.d.ts +239 -1
- package/dist/command-catalog.generated.js +302 -1
- package/dist/commands/core-checkpoint.d.ts +4 -2
- package/dist/commands/core-checkpoint.js +34 -3
- package/dist/commands/core-command-catalog.d.ts +4 -2
- package/dist/commands/core-command-catalog.js +5 -2
- package/dist/commands/core-delegate.d.ts +6 -0
- package/dist/commands/core-delegate.js +987 -0
- package/dist/commands/core-ontology.js +3 -3
- package/dist/commands/core.js +11 -1
- package/dist/commands/daemon.d.ts +4 -0
- package/dist/commands/daemon.js +175 -2
- package/dist/commands/dm.d.ts +2 -0
- package/dist/commands/dm.js +11 -4
- package/dist/commands/feedback.d.ts +6 -0
- package/dist/commands/feedback.js +86 -37
- package/dist/commands/lanes-accounts.js +44 -2
- package/dist/commands/lanes-message.js +69 -12
- package/dist/commands/lanes-resume.js +2 -0
- package/dist/commands/lanes-shared.d.ts +3 -0
- package/dist/commands/lanes-shared.js +12 -2
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +402 -49
- package/dist/commands/run-feature-telemetry.d.ts +3 -0
- package/dist/commands/run-feature-telemetry.js +6 -0
- package/dist/commands/run.js +3 -2
- package/dist/commands/secrets.d.ts +16 -1
- package/dist/commands/secrets.js +80 -45
- package/dist/commands/sync-mode.d.ts +11 -0
- package/dist/commands/sync-mode.js +30 -0
- package/dist/commands/whoami.d.ts +4 -6
- package/dist/commands/whoami.js +39 -8
- package/dist/fast-core.d.ts +6 -7
- package/dist/fast-core.js +22 -8
- package/dist/fast-scaffold.d.ts +33 -7
- package/dist/fast-scaffold.js +104 -8
- package/dist/index.js +32 -1
- package/dist/lanes-drain-hook.js +154 -7
- package/dist/lib/agent-kit/run/inbox.js +3 -2
- package/dist/lib/billing/plan-lock.d.ts +3 -1
- package/dist/lib/billing/plan-lock.js +7 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
- package/dist/lib/checkpoint-ontology/apply.js +60 -3
- package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
- package/dist/lib/checkpoint-ontology/company.js +15 -7
- package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
- package/dist/lib/checkpoint-ontology/plan.js +66 -4
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
- package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
- package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
- package/dist/lib/checkpoint-ontology/tree.js +34 -0
- package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
- package/dist/lib/core-utils/jq-json-serializer.js +10 -0
- package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
- package/dist/lib/core-utils/secret-patterns.js +33 -0
- package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
- package/dist/lib/core-utils/timeout-guard-native.js +40 -0
- package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
- package/dist/lib/core-utils/timeout-guard.js +28 -13
- package/dist/lib/daemon/catalog.d.ts +18 -2
- package/dist/lib/daemon/catalog.js +18 -2
- package/dist/lib/daemon/config.d.ts +6 -0
- package/dist/lib/daemon/config.js +15 -2
- package/dist/lib/daemon/requests.d.ts +4 -0
- package/dist/lib/daemon/requests.js +2 -0
- package/dist/lib/daemon/run.js +94 -8
- package/dist/lib/daemon/supervisor.d.ts +8 -0
- package/dist/lib/daemon/supervisor.js +101 -1
- package/dist/lib/daemon/sync-runner.d.ts +58 -0
- package/dist/lib/daemon/sync-runner.js +277 -0
- package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
- package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/flag-registry.d.ts +12 -7
- package/dist/lib/flag-registry.js +66 -75
- package/dist/lib/flag-snapshot-file.d.ts +20 -0
- package/dist/lib/flag-snapshot-file.js +91 -0
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +46 -8
- package/dist/lib/lanes/config.js +15 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +3 -0
- package/dist/lib/lanes/keep-alive.js +3 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
- package/dist/lib/lanes/outpost-dispatch.js +11 -2
- package/dist/lib/lanes/reconcile.js +20 -4
- package/dist/lib/lanes/spawn.js +17 -10
- package/dist/lib/lanes/store.js +45 -1
- package/dist/lib/lanes/types.d.ts +30 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.js +3 -2
- package/dist/lib/process-flag-company-context.d.ts +8 -0
- package/dist/lib/process-flag-company-context.js +67 -0
- package/dist/main.js +3 -1
- package/dist/native-fast-route.d.ts +3 -0
- package/dist/native-fast-route.js +39 -0
- package/dist/secrets-env-fast-path.d.ts +5 -0
- package/dist/secrets-env-fast-path.js +16 -0
- package/dist/secrets-env-fast-route.d.ts +18 -0
- package/dist/secrets-env-fast-route.js +54 -0
- package/dist/utils/cli-telemetry.d.ts +26 -2
- package/dist/utils/cli-telemetry.js +178 -3
- package/dist/utils/hq-root-resolver.d.ts +13 -0
- package/dist/utils/hq-root-resolver.js +103 -0
- package/dist/utils/scaffold-fast-import-guard.js +6 -1
- package/dist/utils/vault-api.d.ts +8 -0
- package/dist/utils/vault-api.js +13 -1
- package/package.json +2 -2
package/dist/commands/lanes.js
CHANGED
|
@@ -17,8 +17,8 @@ import { createLaneMoveHandlers } from "./lanes-move.js";
|
|
|
17
17
|
import { createLaneQuestionsHandlers } from "./lanes-questions.js";
|
|
18
18
|
import { createLaneResumeHandlers } from "./lanes-resume.js";
|
|
19
19
|
import { createLaneWatchHandlers } from "./lanes-watch.js";
|
|
20
|
-
import { callerMatchesSessionSenior, assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readLanePlacementEnabled, } from "./lanes-shared.js";
|
|
21
|
-
export { LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
20
|
+
import { callerMatchesSessionSenior, assertPlacementEnabled, defaultLanePlacementEnabled, parsePlacementPolicy, processLanePlacementCompany, readClaudeAccountBalanceSignalsEnabled, readLanePlacementEnabled, } from "./lanes-shared.js";
|
|
21
|
+
export { CLAUDE_ACCOUNT_BALANCE_SIGNALS_FLAG, LANE_OUTPOST_LOAD_BALANCING_FLAG } from "./lanes-shared.js";
|
|
22
22
|
export { resolveAnswerChoice } from "./lanes-questions.js";
|
|
23
23
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
24
24
|
import { registerLaneKpisCommand } from "./lanes-kpis.js";
|
|
@@ -27,10 +27,11 @@ import { registerLaneReportCommands } from "./lanes-report.js";
|
|
|
27
27
|
import { registerLaneRepoMergeHoldCommand } from "./lanes-hold.js";
|
|
28
28
|
import { registerLaneReviewCommand } from "./lanes-review.js";
|
|
29
29
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
30
|
+
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
30
31
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
31
32
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
32
33
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
33
|
-
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
34
|
+
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
34
35
|
import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
|
|
35
36
|
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
36
37
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
@@ -50,7 +51,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
|
|
|
50
51
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
51
52
|
import { listLaneReviews } from "../lib/lanes/reviews.js";
|
|
52
53
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
53
|
-
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
54
|
+
import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
|
|
54
55
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
55
56
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
56
57
|
import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
|
|
@@ -450,14 +451,16 @@ function readQuestionBody(hqRoot, question) {
|
|
|
450
451
|
function loadAllLanes(hqRoot) {
|
|
451
452
|
return listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
|
|
452
453
|
}
|
|
453
|
-
function laneHireFields(lane, lanes) {
|
|
454
|
-
const
|
|
454
|
+
function laneHireFields(lane, lanes, aliveChildrenByParent) {
|
|
455
|
+
const aliveChildCount = aliveChildrenByParent
|
|
456
|
+
? aliveChildrenByParent.get(lane.lane_id) ?? 0
|
|
457
|
+
: aliveDirectChildren(lanes, lane.lane_id).length;
|
|
455
458
|
const hireGrant = lane.hire_grant ?? 0;
|
|
456
|
-
const hireSummary = `hire ${
|
|
459
|
+
const hireSummary = `hire ${aliveChildCount}/${hireGrant}`;
|
|
457
460
|
return {
|
|
458
461
|
hire_grant: hireGrant,
|
|
459
|
-
hire_alive_children:
|
|
460
|
-
hire: `${
|
|
462
|
+
hire_alive_children: aliveChildCount,
|
|
463
|
+
hire: `${aliveChildCount}/${hireGrant}`,
|
|
461
464
|
hire_summary: hireSummary,
|
|
462
465
|
};
|
|
463
466
|
}
|
|
@@ -675,6 +678,39 @@ function validateOwnerLaneHireSummary(input) {
|
|
|
675
678
|
hire_summary: `hire ${alive}/${grant}`,
|
|
676
679
|
};
|
|
677
680
|
}
|
|
681
|
+
function laneListQueryFlags(query) {
|
|
682
|
+
return [
|
|
683
|
+
...(query.laneIds?.length ? ["--lane"] : []),
|
|
684
|
+
...(query.active ? ["--active"] : []),
|
|
685
|
+
...(query.hierarchy !== undefined ? ["--hierarchy"] : []),
|
|
686
|
+
...(query.snapshot ? ["--snapshot"] : []),
|
|
687
|
+
];
|
|
688
|
+
}
|
|
689
|
+
function rejectedLaneListFlag(result, query) {
|
|
690
|
+
if (result.done === true && result.status === "Success" && result.exitCode === 0)
|
|
691
|
+
return undefined;
|
|
692
|
+
const output = `${result.stderr}\n${result.stdout}`;
|
|
693
|
+
const match = /unknown\s+(?:option|argument)\s+["']?(--[a-z-]+)/i.exec(output);
|
|
694
|
+
const rejected = match?.[1];
|
|
695
|
+
return rejected && laneListQueryFlags(query).includes(rejected) ? rejected : undefined;
|
|
696
|
+
}
|
|
697
|
+
async function dispatchOutpostLaneList(ctx, outpostId, query, timeoutSeconds, waitMs) {
|
|
698
|
+
const hqBinary = outpostCliOverride(ctx, outpostId);
|
|
699
|
+
const filtered = await ctx.dispatchOutpostExec({
|
|
700
|
+
outpostId,
|
|
701
|
+
command: buildOutpostLaneListCommand({ hqBinary, ...query }),
|
|
702
|
+
timeoutSeconds,
|
|
703
|
+
waitMs,
|
|
704
|
+
});
|
|
705
|
+
if (!rejectedLaneListFlag(filtered, query))
|
|
706
|
+
return filtered;
|
|
707
|
+
return ctx.dispatchOutpostExec({
|
|
708
|
+
outpostId,
|
|
709
|
+
command: buildOutpostLaneListCommand({ hqBinary }),
|
|
710
|
+
timeoutSeconds,
|
|
711
|
+
waitMs,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
678
714
|
async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
|
|
679
715
|
const fallback = (error) => new Map(lanes.map((lane) => [lane.lane_id, {
|
|
680
716
|
error: new LanesError(error.error, error.message, {
|
|
@@ -683,28 +719,15 @@ async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
|
|
|
683
719
|
lane_id: lane.lane_id,
|
|
684
720
|
}),
|
|
685
721
|
}]));
|
|
686
|
-
let
|
|
722
|
+
let dispatched;
|
|
687
723
|
try {
|
|
688
|
-
|
|
724
|
+
dispatched = await dispatchOutpostLaneList(ctx, outpostId, { laneIds: lanes.map((lane) => lane.lane_id) }, OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS, OUTPOST_HIRE_SUMMARY_WAIT_MS);
|
|
689
725
|
}
|
|
690
726
|
catch (err) {
|
|
691
727
|
if (isLanesError(err))
|
|
692
728
|
return fallback(err);
|
|
693
729
|
return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId }));
|
|
694
730
|
}
|
|
695
|
-
let dispatched;
|
|
696
|
-
try {
|
|
697
|
-
// No filters: the owner list must return every lane requested by this host group.
|
|
698
|
-
dispatched = await ctx.dispatchOutpostExec({
|
|
699
|
-
outpostId,
|
|
700
|
-
command: buildOutpostLaneListCommand({ hqBinary }),
|
|
701
|
-
timeoutSeconds: OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS,
|
|
702
|
-
waitMs: OUTPOST_HIRE_SUMMARY_WAIT_MS,
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
catch (err) {
|
|
706
|
-
return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId }));
|
|
707
|
-
}
|
|
708
731
|
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
709
732
|
return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Outpost ${outpostId} did not confirm its lane list for hire summaries`, { outpost_id: outpostId, command_id: dispatched.commandId }));
|
|
710
733
|
}
|
|
@@ -989,18 +1012,26 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
989
1012
|
skippedHosts: unavailable.map(({ outpost_id, error }) => ({ outpost_id, error })),
|
|
990
1013
|
};
|
|
991
1014
|
}
|
|
992
|
-
|
|
993
|
-
|
|
1015
|
+
const outpostLaneListMemo = new WeakMap();
|
|
1016
|
+
function normalizedLaneListQuery(query) {
|
|
1017
|
+
const laneIds = query.laneIds === undefined
|
|
1018
|
+
? undefined
|
|
1019
|
+
: [...new Set(query.laneIds)].sort((a, b) => a.localeCompare(b));
|
|
1020
|
+
return {
|
|
1021
|
+
...(laneIds === undefined ? {} : { laneIds }),
|
|
1022
|
+
...(query.active ? { active: true } : {}),
|
|
1023
|
+
...(query.hierarchy === undefined ? {} : { hierarchy: query.hierarchy }),
|
|
1024
|
+
...(query.snapshot ? { snapshot: true } : {}),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
async function readOutpostLaneRecordsUncached(ctx, outpostId, query) {
|
|
994
1028
|
let result;
|
|
995
1029
|
try {
|
|
996
|
-
result = await ctx
|
|
997
|
-
outpostId,
|
|
998
|
-
command: buildOutpostLaneListCommand({ hqBinary }),
|
|
999
|
-
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
1000
|
-
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
1001
|
-
});
|
|
1030
|
+
result = await dispatchOutpostLaneList(ctx, outpostId, query, CAPACITY_PROBE_TIMEOUT_MS / 1000, CAPACITY_PROBE_TIMEOUT_MS);
|
|
1002
1031
|
}
|
|
1003
1032
|
catch (err) {
|
|
1033
|
+
if (isLanesError(err))
|
|
1034
|
+
throw err;
|
|
1004
1035
|
throw new LanesError("outpost_lane_list_unavailable", `Could not read lanes from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
|
|
1005
1036
|
}
|
|
1006
1037
|
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
@@ -1056,6 +1087,25 @@ async function readOutpostLaneRecords(ctx, outpostId) {
|
|
|
1056
1087
|
}
|
|
1057
1088
|
return lanes;
|
|
1058
1089
|
}
|
|
1090
|
+
async function readOutpostLaneRecords(ctx, outpostId, requestedQuery = {}) {
|
|
1091
|
+
const query = normalizedLaneListQuery(requestedQuery);
|
|
1092
|
+
const queryKey = JSON.stringify(query);
|
|
1093
|
+
let byOutpost = outpostLaneListMemo.get(ctx);
|
|
1094
|
+
if (!byOutpost) {
|
|
1095
|
+
byOutpost = new Map();
|
|
1096
|
+
outpostLaneListMemo.set(ctx, byOutpost);
|
|
1097
|
+
}
|
|
1098
|
+
const existing = byOutpost.get(outpostId);
|
|
1099
|
+
if (existing) {
|
|
1100
|
+
if (existing.queryKey !== queryKey) {
|
|
1101
|
+
throw new LanesError("outpost_lane_list_query_conflict", `Outpost ${outpostId} was already read with a different lane-list query in this invocation`, { outpost_id: outpostId });
|
|
1102
|
+
}
|
|
1103
|
+
return existing.promise;
|
|
1104
|
+
}
|
|
1105
|
+
const promise = readOutpostLaneRecordsUncached(ctx, outpostId, query);
|
|
1106
|
+
byOutpost.set(outpostId, { queryKey, promise });
|
|
1107
|
+
return promise;
|
|
1108
|
+
}
|
|
1059
1109
|
async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
1060
1110
|
try {
|
|
1061
1111
|
return { lane: readLane(ctx.hqRoot(), laneId), remoteOnly: false };
|
|
@@ -1098,7 +1148,10 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
|
|
|
1098
1148
|
for (const outpostId of lookupOutpostIds) {
|
|
1099
1149
|
let ownerLanes;
|
|
1100
1150
|
try {
|
|
1101
|
-
ownerLanes = await readOutpostLaneRecords(ctx, outpostId
|
|
1151
|
+
ownerLanes = await readOutpostLaneRecords(ctx, outpostId, {
|
|
1152
|
+
hierarchy: laneId,
|
|
1153
|
+
snapshot: true,
|
|
1154
|
+
});
|
|
1102
1155
|
}
|
|
1103
1156
|
catch (err) {
|
|
1104
1157
|
if (requestedOutpostId !== undefined)
|
|
@@ -1214,7 +1267,9 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1214
1267
|
let lanes = [];
|
|
1215
1268
|
let laneListAvailable = false;
|
|
1216
1269
|
try {
|
|
1217
|
-
|
|
1270
|
+
// Balance can move done and interrupted lanes, so the snapshot must keep
|
|
1271
|
+
// terminal rows; `--active` would drop them.
|
|
1272
|
+
lanes = await readOutpostLaneRecords(ctx, id, { snapshot: true });
|
|
1218
1273
|
laneListAvailable = true;
|
|
1219
1274
|
}
|
|
1220
1275
|
catch (err) {
|
|
@@ -2722,6 +2777,8 @@ async function runCreate(ctx, opts) {
|
|
|
2722
2777
|
hosts: balanceHostLoad.snapshots,
|
|
2723
2778
|
targetHost,
|
|
2724
2779
|
config,
|
|
2780
|
+
recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
|
|
2781
|
+
now: ctx.now(),
|
|
2725
2782
|
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
2726
2783
|
...(explicitBalanceAccount !== undefined ? { explicitAccount: explicitBalanceAccount } : {}),
|
|
2727
2784
|
unavailableHosts: balanceHostLoad.unavailableHosts,
|
|
@@ -2767,6 +2824,8 @@ async function runCreate(ctx, opts) {
|
|
|
2767
2824
|
hosts,
|
|
2768
2825
|
targetHost: balanceTargetHost,
|
|
2769
2826
|
config,
|
|
2827
|
+
recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
|
|
2828
|
+
now: ctx.now(),
|
|
2770
2829
|
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
2771
2830
|
unavailableHosts: balanceHostLoad.unavailableHosts,
|
|
2772
2831
|
});
|
|
@@ -3503,13 +3562,19 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
3503
3562
|
let grantContext;
|
|
3504
3563
|
let grantHierarchy;
|
|
3505
3564
|
if (action === "grant") {
|
|
3506
|
-
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId
|
|
3565
|
+
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3566
|
+
hierarchy: lane.lane_id,
|
|
3567
|
+
snapshot: true,
|
|
3568
|
+
}));
|
|
3507
3569
|
grantHierarchy = hierarchy;
|
|
3508
3570
|
grantContext = createOutpostHireGrantContext(lane, hierarchy);
|
|
3509
3571
|
}
|
|
3510
3572
|
else if ((action === "resume" && lane.senior.kind === "lane") ||
|
|
3511
3573
|
(action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
|
|
3512
|
-
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId
|
|
3574
|
+
const hierarchy = mergeOwnerLaneRecords(loadAllLanes(ctx.hqRoot()), await readOutpostLaneRecords(ctx, outpostId, {
|
|
3575
|
+
hierarchy: lane.lane_id,
|
|
3576
|
+
snapshot: true,
|
|
3577
|
+
}));
|
|
3513
3578
|
grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
|
|
3514
3579
|
}
|
|
3515
3580
|
let dispatched;
|
|
@@ -4049,6 +4114,21 @@ async function runList(ctx, opts) {
|
|
|
4049
4114
|
if (opts.json && opts.human) {
|
|
4050
4115
|
throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
|
|
4051
4116
|
}
|
|
4117
|
+
const requestedLaneIds = opts.lane ?? [];
|
|
4118
|
+
if (!Array.isArray(requestedLaneIds) ||
|
|
4119
|
+
requestedLaneIds.some((laneId) => typeof laneId !== "string" || !laneId.trim() || laneId.trim() !== laneId) ||
|
|
4120
|
+
new Set(requestedLaneIds).size !== requestedLaneIds.length) {
|
|
4121
|
+
throw new LanesError("lane_list_filter_invalid", "--lane must be repeated with distinct, non-empty lane ids", { lane_ids: requestedLaneIds });
|
|
4122
|
+
}
|
|
4123
|
+
if (opts.hierarchy !== undefined && (!opts.hierarchy.trim() || opts.hierarchy.trim() !== opts.hierarchy)) {
|
|
4124
|
+
throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy requires one non-empty lane id without surrounding whitespace", {});
|
|
4125
|
+
}
|
|
4126
|
+
if (opts.snapshot && (opts.human || opts.company || opts.project || opts.story || opts.state || opts.senior)) {
|
|
4127
|
+
throw new LanesError("lane_list_snapshot_invalid", "--snapshot only supports owner lane filters and JSON output", {});
|
|
4128
|
+
}
|
|
4129
|
+
if (opts.hierarchy !== undefined && !opts.snapshot) {
|
|
4130
|
+
throw new LanesError("lane_list_hierarchy_invalid", "--hierarchy requires --snapshot", {});
|
|
4131
|
+
}
|
|
4052
4132
|
const hqRoot = ctx.hqRoot();
|
|
4053
4133
|
const state = opts.state ? parseLaneState(opts.state) : undefined;
|
|
4054
4134
|
const senior = opts.senior ? parseSeniorRef(opts.senior) : undefined;
|
|
@@ -4056,11 +4136,25 @@ async function runList(ctx, opts) {
|
|
|
4056
4136
|
if (opts.company) {
|
|
4057
4137
|
company = await ctx.resolveCompany(opts.company);
|
|
4058
4138
|
}
|
|
4059
|
-
const
|
|
4060
|
-
const
|
|
4061
|
-
|
|
4062
|
-
const
|
|
4139
|
+
const requestedIds = new Set(requestedLaneIds);
|
|
4140
|
+
const canReadOnlyRequestedIds = opts.snapshot && requestedLaneIds.length > 0 &&
|
|
4141
|
+
!opts.active && opts.hierarchy === undefined;
|
|
4142
|
+
const laneSource = canReadOnlyRequestedIds
|
|
4143
|
+
? listLaneIds(hqRoot)
|
|
4144
|
+
.filter((laneId) => requestedIds.has(laneId))
|
|
4145
|
+
.map((laneId) => readLane(hqRoot, laneId))
|
|
4146
|
+
: loadAllLanes(hqRoot);
|
|
4147
|
+
const hierarchyIds = opts.hierarchy === undefined
|
|
4148
|
+
? undefined
|
|
4149
|
+
: new Set(selectOutpostHireGrantHierarchy(opts.hierarchy, laneSource).map((lane) => lane.lane_id));
|
|
4150
|
+
const selectedLanes = laneSource
|
|
4063
4151
|
.filter((lane) => {
|
|
4152
|
+
if (requestedLaneIds.length > 0 && !requestedIds.has(lane.lane_id))
|
|
4153
|
+
return false;
|
|
4154
|
+
if (opts.active && isTerminalLaneState(lane.state))
|
|
4155
|
+
return false;
|
|
4156
|
+
if (hierarchyIds && !hierarchyIds.has(lane.lane_id))
|
|
4157
|
+
return false;
|
|
4064
4158
|
if (state && lane.state !== state)
|
|
4065
4159
|
return false;
|
|
4066
4160
|
const carriesFilter = laneStories(lane).some((story) => (!opts.project || story.project_id === opts.project) &&
|
|
@@ -4078,6 +4172,19 @@ async function runList(ctx, opts) {
|
|
|
4078
4172
|
return true;
|
|
4079
4173
|
})
|
|
4080
4174
|
.sort(compareLaneQueueOrder);
|
|
4175
|
+
if (opts.snapshot) {
|
|
4176
|
+
printJson({ ok: true, action: "list", lanes: selectedLanes });
|
|
4177
|
+
return;
|
|
4178
|
+
}
|
|
4179
|
+
const now = ctx.now();
|
|
4180
|
+
const host = reconcileHostFromCtx(ctx);
|
|
4181
|
+
const allLanes = laneSource;
|
|
4182
|
+
const aliveChildrenByParent = new Map();
|
|
4183
|
+
for (const child of allLanes) {
|
|
4184
|
+
if (child.senior.kind !== "lane" || !child.senior.id || isTerminalLaneState(child.state))
|
|
4185
|
+
continue;
|
|
4186
|
+
aliveChildrenByParent.set(child.senior.id, (aliveChildrenByParent.get(child.senior.id) ?? 0) + 1);
|
|
4187
|
+
}
|
|
4081
4188
|
const ownerEligibleLaneIds = new Set();
|
|
4082
4189
|
for (const lane of selectedLanes) {
|
|
4083
4190
|
if (lane.host.kind !== "outpost" ||
|
|
@@ -4092,7 +4199,7 @@ async function runList(ctx, opts) {
|
|
|
4092
4199
|
for (const lane of selectedLanes) {
|
|
4093
4200
|
if (!ownerEligibleLaneIds.has(lane.lane_id)) {
|
|
4094
4201
|
hireByLane.set(lane.lane_id, {
|
|
4095
|
-
...laneHireFields(lane, allLanes),
|
|
4202
|
+
...laneHireFields(lane, allLanes, aliveChildrenByParent),
|
|
4096
4203
|
hire_source: "local",
|
|
4097
4204
|
});
|
|
4098
4205
|
continue;
|
|
@@ -4110,12 +4217,14 @@ async function runList(ctx, opts) {
|
|
|
4110
4217
|
? outcome.error
|
|
4111
4218
|
: new LanesError("outpost_lane_hire_summary_missing", `No owner hire summary was returned for lane ${lane.lane_id}`, { lane_id: lane.lane_id, outpost_id: lane.host.id });
|
|
4112
4219
|
hireByLane.set(lane.lane_id, {
|
|
4113
|
-
...laneHireFields(lane, allLanes),
|
|
4220
|
+
...laneHireFields(lane, allLanes, aliveChildrenByParent),
|
|
4114
4221
|
hire_source: "local",
|
|
4115
4222
|
hire_warning: `${error.error}: ${error.message}`,
|
|
4116
4223
|
});
|
|
4117
4224
|
}
|
|
4118
4225
|
}
|
|
4226
|
+
const livenessBySession = new Map();
|
|
4227
|
+
let livenessDeps;
|
|
4119
4228
|
const lanes = selectedLanes.map((lane) => {
|
|
4120
4229
|
const remoteQueued = lane.state === "queued" &&
|
|
4121
4230
|
lane.host.kind === "outpost" &&
|
|
@@ -4134,12 +4243,19 @@ async function runList(ctx, opts) {
|
|
|
4134
4243
|
// Listing is observational. Reconcile owns durable liveness transitions.
|
|
4135
4244
|
senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
|
|
4136
4245
|
? (() => {
|
|
4137
|
-
const
|
|
4246
|
+
const sessionId = lane.senior.id;
|
|
4247
|
+
const cached = livenessBySession.get(sessionId);
|
|
4248
|
+
if (cached !== undefined)
|
|
4249
|
+
return cached;
|
|
4250
|
+
livenessDeps ??= livenessDepsFromCtx(ctx);
|
|
4251
|
+
const state = sessionLiveness(hqRoot, sessionId, {
|
|
4138
4252
|
nowMs: now.getTime(),
|
|
4139
|
-
deps:
|
|
4253
|
+
deps: livenessDeps,
|
|
4140
4254
|
presenceConnected: ctx.presenceConnected,
|
|
4141
4255
|
});
|
|
4142
|
-
|
|
4256
|
+
const lost = state === null || state === "ended" || state === "lost";
|
|
4257
|
+
livenessBySession.set(sessionId, lost);
|
|
4258
|
+
return lost;
|
|
4143
4259
|
})()
|
|
4144
4260
|
: false,
|
|
4145
4261
|
};
|
|
@@ -4413,6 +4529,7 @@ async function runShow(ctx, laneId) {
|
|
|
4413
4529
|
model: lane.model ?? null,
|
|
4414
4530
|
effort: lane.effort ?? null,
|
|
4415
4531
|
effort_source: lane.effort_source ?? null,
|
|
4532
|
+
settings_history: lane.settings_history ?? [],
|
|
4416
4533
|
terminal_reason: lane.terminal_reason ?? null,
|
|
4417
4534
|
terminal_details: lane.terminal_details ?? null,
|
|
4418
4535
|
keep_alive_state: {
|
|
@@ -4644,6 +4761,107 @@ function printGrantResult(hqRoot, updated, hierarchy) {
|
|
|
4644
4761
|
const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot));
|
|
4645
4762
|
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
4646
4763
|
}
|
|
4764
|
+
/**
|
|
4765
|
+
* `hq lanes set`: change the model or effort a lane uses from its next
|
|
4766
|
+
* resume. The running turn keeps its values. Both are stored as explicit
|
|
4767
|
+
* (`cli`) choices, so keep-alive and `hq lanes resume` keep them until the
|
|
4768
|
+
* next `set` or an explicit resume `--effort`/`--worker`.
|
|
4769
|
+
*/
|
|
4770
|
+
async function runSet(ctx, laneId, opts) {
|
|
4771
|
+
if (opts.model === undefined && opts.effort === undefined) {
|
|
4772
|
+
throw new LanesError("lane_set_empty", "hq lanes set needs --model or --effort", { lane_id: laneId });
|
|
4773
|
+
}
|
|
4774
|
+
const reason = opts.reason?.trim();
|
|
4775
|
+
if (opts.reason !== undefined && !reason) {
|
|
4776
|
+
throw new LanesError("lane_set_reason_invalid", "Set reason must not be empty", { lane_id: laneId });
|
|
4777
|
+
}
|
|
4778
|
+
if (reason !== undefined)
|
|
4779
|
+
assertNoSecretText("set reason", reason);
|
|
4780
|
+
const hqRoot = ctx.hqRoot();
|
|
4781
|
+
const lane = readLane(hqRoot, laneId);
|
|
4782
|
+
const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
|
|
4783
|
+
const authorization = callerLaneId === laneId
|
|
4784
|
+
? { authorized: true }
|
|
4785
|
+
: callerSeniorAuthorization(ctx, lane.senior);
|
|
4786
|
+
if (!authorization.authorized) {
|
|
4787
|
+
throw new LanesError("lane_set_unauthorized", appendCallerSeniorReason(`Only lane ${laneId} or its senior ${seniorKey(lane.senior)} may change its model or effort`, authorization), { lane_id: laneId, senior: lane.senior, reason: authorization.reason });
|
|
4788
|
+
}
|
|
4789
|
+
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
4790
|
+
throw new LanesError("set_remote_unsupported", `Lane ${laneId} runs on Outpost ${lane.host.id}; run hq lanes set on that Outpost`, { lane_id: laneId, outpost_id: lane.host.id });
|
|
4791
|
+
}
|
|
4792
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "set");
|
|
4793
|
+
if (lane.state === "killed" || lane.state === "cancelled") {
|
|
4794
|
+
throw new LanesError("lane_not_resumable", `Lane ${laneId} is ${lane.state} and will not run again`, { lane_id: laneId, state: lane.state });
|
|
4795
|
+
}
|
|
4796
|
+
const model = opts.model?.trim();
|
|
4797
|
+
if (opts.model !== undefined && !model) {
|
|
4798
|
+
throw new LanesError("empty_model", "--model is empty or whitespace-only", { value: opts.model });
|
|
4799
|
+
}
|
|
4800
|
+
if (model !== undefined) {
|
|
4801
|
+
const config = loadLanesConfig(hqRoot);
|
|
4802
|
+
const account = lane.account === undefined
|
|
4803
|
+
? undefined
|
|
4804
|
+
: config.providers[lane.provider].accounts[lane.account];
|
|
4805
|
+
if (account === undefined) {
|
|
4806
|
+
throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
4807
|
+
}
|
|
4808
|
+
validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
|
|
4809
|
+
readFileSync: ctx.readFileSync,
|
|
4810
|
+
probe: ctx.probeProvider,
|
|
4811
|
+
env: ctx.env,
|
|
4812
|
+
});
|
|
4813
|
+
}
|
|
4814
|
+
const effort = parseCliEffort(lane.provider, opts.effort);
|
|
4815
|
+
const at = ctx.now().toISOString();
|
|
4816
|
+
const actor = callerLaneId !== undefined
|
|
4817
|
+
? `lane:${callerLaneId}`
|
|
4818
|
+
: `session:${sessionIdFromEnv(ctx.env) ?? "unknown"}`;
|
|
4819
|
+
const updated = casUpdateLane(hqRoot, laneId, (current) => {
|
|
4820
|
+
if (current.version !== lane.version) {
|
|
4821
|
+
throw new LanesError("lane_set_conflict", `Lane ${laneId} changed while the settings were checked; run hq lanes set again`, { lane_id: laneId, prepared_version: lane.version, version: current.version });
|
|
4822
|
+
}
|
|
4823
|
+
const entry = { at, actor, ...(reason !== undefined ? { reason } : {}) };
|
|
4824
|
+
const next = { ...current };
|
|
4825
|
+
const pending = { ...(current.pending_settings ?? {}) };
|
|
4826
|
+
// A running turn keeps the model and effort it launched with, because the
|
|
4827
|
+
// watcher classifies that turn's exit (including model quota) by them.
|
|
4828
|
+
const deferred = current.state === "running";
|
|
4829
|
+
if (model !== undefined) {
|
|
4830
|
+
entry.model = { from: pending.model ?? current.model ?? null, to: model };
|
|
4831
|
+
if (deferred) {
|
|
4832
|
+
pending.model = model;
|
|
4833
|
+
}
|
|
4834
|
+
else {
|
|
4835
|
+
next.model = model;
|
|
4836
|
+
next.model_source = "cli";
|
|
4837
|
+
delete pending.model;
|
|
4838
|
+
}
|
|
4839
|
+
}
|
|
4840
|
+
if (effort !== undefined) {
|
|
4841
|
+
entry.effort = { from: pending.effort ?? current.effort ?? null, to: effort };
|
|
4842
|
+
if (deferred) {
|
|
4843
|
+
pending.effort = effort;
|
|
4844
|
+
}
|
|
4845
|
+
else {
|
|
4846
|
+
next.effort = effort;
|
|
4847
|
+
next.effort_source = "cli";
|
|
4848
|
+
delete pending.effort;
|
|
4849
|
+
}
|
|
4850
|
+
}
|
|
4851
|
+
next.pending_settings = Object.keys(pending).length > 0 ? pending : undefined;
|
|
4852
|
+
next.settings_history = appendCappedHistory(current.settings_history ?? [], entry);
|
|
4853
|
+
return next;
|
|
4854
|
+
});
|
|
4855
|
+
printJson({
|
|
4856
|
+
ok: true,
|
|
4857
|
+
action: "set",
|
|
4858
|
+
lane: updated,
|
|
4859
|
+
model: updated.pending_settings?.model ?? updated.model ?? null,
|
|
4860
|
+
effort: updated.pending_settings?.effort ?? updated.effort ?? null,
|
|
4861
|
+
pending: updated.pending_settings !== undefined,
|
|
4862
|
+
applies: "next_resume",
|
|
4863
|
+
});
|
|
4864
|
+
}
|
|
4647
4865
|
async function runKeepAlive(ctx, laneId, opts) {
|
|
4648
4866
|
if (opts.standing && !opts.on) {
|
|
4649
4867
|
throw new LanesError("keep_alive_standing_requires_on", "Pass --standing together with --on", { lane_id: laneId });
|
|
@@ -5159,7 +5377,8 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
5159
5377
|
input.effort === undefined &&
|
|
5160
5378
|
input.keepAlive === undefined &&
|
|
5161
5379
|
input.cliModel === undefined &&
|
|
5162
|
-
input.meshCompany === undefined
|
|
5380
|
+
input.meshCompany === undefined &&
|
|
5381
|
+
lane.keep_alive_blocked_reason !== "terminal_state")
|
|
5163
5382
|
return lane;
|
|
5164
5383
|
return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
|
|
5165
5384
|
if (current.version !== lane.version) {
|
|
@@ -5169,6 +5388,10 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
5169
5388
|
assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
|
|
5170
5389
|
}
|
|
5171
5390
|
const next = { ...current };
|
|
5391
|
+
// An operator resume takes the lane out of the terminal state reconcile
|
|
5392
|
+
// recorded, so keep-alive may continue it again after this turn.
|
|
5393
|
+
if (next.keep_alive_blocked_reason === "terminal_state")
|
|
5394
|
+
next.keep_alive_blocked_reason = undefined;
|
|
5172
5395
|
applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
|
|
5173
5396
|
if (input.cliModel !== undefined) {
|
|
5174
5397
|
next.model = input.cliModel.trim();
|
|
@@ -5449,6 +5672,8 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
|
|
|
5449
5672
|
hosts: balanceHosts.snapshots,
|
|
5450
5673
|
targetHost: lane.host,
|
|
5451
5674
|
config,
|
|
5675
|
+
recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, lane.company ?? undefined),
|
|
5676
|
+
now: ctx.now(),
|
|
5452
5677
|
...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
5453
5678
|
...(model !== undefined ? { model } : {}),
|
|
5454
5679
|
...(recordedModelAccount !== undefined ? { recordedModelAccount } : {}),
|
|
@@ -6343,6 +6568,8 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
|
|
|
6343
6568
|
ok: true,
|
|
6344
6569
|
action: "stop",
|
|
6345
6570
|
lane: updated,
|
|
6571
|
+
terminal: true,
|
|
6572
|
+
resumable: false,
|
|
6346
6573
|
cancelled_questions: cancelledQuestions,
|
|
6347
6574
|
outpost_id: outpostId,
|
|
6348
6575
|
outpost_command_id: commandId,
|
|
@@ -6364,6 +6591,9 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
6364
6591
|
assertNoSecretText("stop reason", operatorReason);
|
|
6365
6592
|
const hqRoot = ctx.hqRoot();
|
|
6366
6593
|
const lane = readLane(hqRoot, laneId);
|
|
6594
|
+
if (!quiet && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
|
|
6595
|
+
process.stderr.write(stopTerminalWarning(laneId));
|
|
6596
|
+
}
|
|
6367
6597
|
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
6368
6598
|
await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet);
|
|
6369
6599
|
return;
|
|
@@ -6408,9 +6638,108 @@ async function runStop(ctx, laneId, reason, quiet = false) {
|
|
|
6408
6638
|
ok: true,
|
|
6409
6639
|
action: "stop",
|
|
6410
6640
|
lane: updated,
|
|
6641
|
+
terminal: true,
|
|
6642
|
+
resumable: false,
|
|
6411
6643
|
cancelled_questions: cancelled.map((q) => q.question_id),
|
|
6412
6644
|
});
|
|
6413
6645
|
}
|
|
6646
|
+
/** Stderr notice that `stop` ends a lane for good, naming the resumable path. */
|
|
6647
|
+
export function stopTerminalWarning(laneId) {
|
|
6648
|
+
return `hq lanes: warning: stop kills lane ${laneId} and it cannot be resumed. ` +
|
|
6649
|
+
`To pause it and keep it resumable, use hq lanes interrupt ${laneId}.\n`;
|
|
6650
|
+
}
|
|
6651
|
+
/**
|
|
6652
|
+
* Stop a running lane's worker and leave it `interrupted`, keeping the provider
|
|
6653
|
+
* session so `hq lanes resume` continues it. Keep-alive resumes only
|
|
6654
|
+
* `exited_with_partial_output` interruptions, so it leaves this lane alone.
|
|
6655
|
+
*/
|
|
6656
|
+
async function runInterrupt(ctx, laneId, reason) {
|
|
6657
|
+
const operatorReason = reason?.trim();
|
|
6658
|
+
if (reason !== undefined && !operatorReason) {
|
|
6659
|
+
throw new LanesError("lane_interrupt_reason_invalid", "Interrupt reason must not be empty", {});
|
|
6660
|
+
}
|
|
6661
|
+
if (operatorReason !== undefined)
|
|
6662
|
+
assertNoSecretText("interrupt reason", operatorReason);
|
|
6663
|
+
const hqRoot = ctx.hqRoot();
|
|
6664
|
+
const lane = readLane(hqRoot, laneId);
|
|
6665
|
+
if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
|
|
6666
|
+
throw new LanesError("interrupt_remote_unsupported", `Lane ${laneId} runs on Outpost ${lane.host.id}; run hq lanes interrupt on that Outpost`, { lane_id: laneId, outpost_id: lane.host.id });
|
|
6667
|
+
}
|
|
6668
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "interrupt");
|
|
6669
|
+
const assertInterruptible = (current) => {
|
|
6670
|
+
if (current.state !== "running") {
|
|
6671
|
+
throw new LanesError("lane_not_running", current.state === "queued"
|
|
6672
|
+
? `Lane ${laneId} is queued and has no running turn; use hq lanes stop to cancel it`
|
|
6673
|
+
: `Lane ${laneId} is ${current.state}; only a running lane can be interrupted`, { lane_id: laneId, state: current.state });
|
|
6674
|
+
}
|
|
6675
|
+
};
|
|
6676
|
+
assertInterruptible(lane);
|
|
6677
|
+
// Hold the provider's admission lock across the state change, the process
|
|
6678
|
+
// stop and the session write, so a resume cannot spawn a second worker on
|
|
6679
|
+
// the same session while the old one is still exiting.
|
|
6680
|
+
const updated = await withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
6681
|
+
const at = ctx.now().toISOString();
|
|
6682
|
+
const interrupted = casUpdateLane(hqRoot, laneId, (current) => {
|
|
6683
|
+
assertLaneRunsHereForOperatorMutation(ctx, current, "interrupt");
|
|
6684
|
+
assertInterruptible(current);
|
|
6685
|
+
return {
|
|
6686
|
+
...current,
|
|
6687
|
+
state: "interrupted",
|
|
6688
|
+
queued_reason: undefined,
|
|
6689
|
+
interrupted_at: at,
|
|
6690
|
+
terminal_reason: "operator_interrupt",
|
|
6691
|
+
...(operatorReason === undefined ? {} : {
|
|
6692
|
+
stop_history: [
|
|
6693
|
+
...(current.stop_history ?? []),
|
|
6694
|
+
{ at, reason: operatorReason },
|
|
6695
|
+
],
|
|
6696
|
+
}),
|
|
6697
|
+
};
|
|
6698
|
+
});
|
|
6699
|
+
stopLaneProcesses(hqRoot, laneId, {
|
|
6700
|
+
killProcess: ctx.killProcess,
|
|
6701
|
+
isAlive: ctx.isAlive,
|
|
6702
|
+
sleepMs: ctx.stopSleepMs,
|
|
6703
|
+
});
|
|
6704
|
+
return persistInterruptedSession(hqRoot, interrupted);
|
|
6705
|
+
}, ctx.sleep);
|
|
6706
|
+
await emitLaneTerminalStateSoft(ctx, updated);
|
|
6707
|
+
const resumable = resumeCommandRefusal(laneWithRecoveredProviderSession(hqRoot, updated)) === null;
|
|
6708
|
+
if (!resumable)
|
|
6709
|
+
process.stderr.write(interruptNotResumableWarning(laneId));
|
|
6710
|
+
printJson({
|
|
6711
|
+
ok: true,
|
|
6712
|
+
action: "interrupt",
|
|
6713
|
+
lane: updated,
|
|
6714
|
+
terminal: false,
|
|
6715
|
+
resumable,
|
|
6716
|
+
});
|
|
6717
|
+
}
|
|
6718
|
+
/**
|
|
6719
|
+
* The watcher skips lanes that are already terminal, so it never records the
|
|
6720
|
+
* session id of a turn the operator interrupted. Read it from this run's
|
|
6721
|
+
* worker output here; a lane with no session id cannot be resumed.
|
|
6722
|
+
*/
|
|
6723
|
+
function persistInterruptedSession(hqRoot, lane) {
|
|
6724
|
+
const runDir = laneRunDir(hqRoot, lane.lane_id);
|
|
6725
|
+
let sessionId = null;
|
|
6726
|
+
try {
|
|
6727
|
+
sessionId = readWorkerRunResult(runDir, lane.provider, readSpawnManifest(runDir).worker_out_offset ?? 0)?.session_id ?? null;
|
|
6728
|
+
}
|
|
6729
|
+
catch (err) {
|
|
6730
|
+
process.stderr.write(`hq lanes: could not read the session id of interrupted lane ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
6731
|
+
}
|
|
6732
|
+
if (sessionId === null || sessionId === lane.provider_session_id)
|
|
6733
|
+
return lane;
|
|
6734
|
+
return casUpdateLane(hqRoot, lane.lane_id, (current) => current.state !== "interrupted"
|
|
6735
|
+
? current
|
|
6736
|
+
: { ...current, provider_session_id: sessionId, provider_session_id_absent: undefined });
|
|
6737
|
+
}
|
|
6738
|
+
/** Stderr notice when an interrupted lane has no session to resume from. */
|
|
6739
|
+
export function interruptNotResumableWarning(laneId) {
|
|
6740
|
+
return `hq lanes: warning: lane ${laneId} was interrupted before its provider reported a session id, ` +
|
|
6741
|
+
`so hq lanes resume cannot continue it. Start a new lane for the remaining work.\n`;
|
|
6742
|
+
}
|
|
6414
6743
|
async function runAsk(ctx, opts) {
|
|
6415
6744
|
const hqRoot = ctx.hqRoot();
|
|
6416
6745
|
const laneId = ctx.env.HQ_LANE_ID?.trim();
|
|
@@ -6739,6 +7068,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6739
7068
|
parseRemoteJson,
|
|
6740
7069
|
prepareAccountMove,
|
|
6741
7070
|
printJson,
|
|
7071
|
+
readClaudeAccountBalanceSignalsEnabled,
|
|
6742
7072
|
reconcileHostFromCtx,
|
|
6743
7073
|
resolveCallerPath,
|
|
6744
7074
|
});
|
|
@@ -6911,7 +7241,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6911
7241
|
.option("--placement <mode>", "local | auto | outpost:<id>")
|
|
6912
7242
|
.option("--default-placement <mode>", "Default placement for lanes launched by this lane: local | auto | outpost:<id>")
|
|
6913
7243
|
.action(wrap(async (opts) => {
|
|
6914
|
-
await runCreate(ctx, opts);
|
|
7244
|
+
await withCliFeatureUsed("lane_create", () => runCreate(ctx, opts));
|
|
6915
7245
|
}));
|
|
6916
7246
|
lanes
|
|
6917
7247
|
.command("approve-target", { hidden: true })
|
|
@@ -6947,6 +7277,17 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6947
7277
|
.action(wrap(async (laneId, opts) => {
|
|
6948
7278
|
await runKeepAlive(ctx, laneId, opts);
|
|
6949
7279
|
}));
|
|
7280
|
+
lanes
|
|
7281
|
+
.command("set")
|
|
7282
|
+
.description("Change the model or effort a lane uses from its next resume")
|
|
7283
|
+
.argument("<lane-id>", "Lane id")
|
|
7284
|
+
.option("--model <model>", "Model for the lane's next and later turns")
|
|
7285
|
+
.option("--effort <level>", "Effort for the lane's next and later turns")
|
|
7286
|
+
.option("--reason <text>", "Why the settings changed; recorded in settings_history")
|
|
7287
|
+
.option("--json", "Emit JSON on stdout (default)")
|
|
7288
|
+
.action(wrap(async (laneId, opts) => {
|
|
7289
|
+
await runSet(ctx, laneId, opts);
|
|
7290
|
+
}));
|
|
6950
7291
|
lanes
|
|
6951
7292
|
.command("grant")
|
|
6952
7293
|
.description("Set a lane's alive child-lane grant as its senior")
|
|
@@ -6972,6 +7313,10 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6972
7313
|
.option("--story <id>", "Filter by story")
|
|
6973
7314
|
.option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
|
|
6974
7315
|
.option("--senior <ref>", "Filter by senior ref")
|
|
7316
|
+
.option("--lane <id>", "Filter by lane id (repeatable)", (value, previous = []) => [...previous, value], [])
|
|
7317
|
+
.option("--active", "Include only lanes that are not terminal")
|
|
7318
|
+
.addOption(new Option("--hierarchy <lane-id>").hideHelp())
|
|
7319
|
+
.addOption(new Option("--snapshot").hideHelp())
|
|
6975
7320
|
.option("--json", "Emit JSON on stdout (default)")
|
|
6976
7321
|
.option("--human", "Emit human-readable output")
|
|
6977
7322
|
.action(wrap(async (opts) => {
|
|
@@ -7215,7 +7560,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7215
7560
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
7216
7561
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
7217
7562
|
.action(wrap(async (laneId, opts) => {
|
|
7218
|
-
await resumeHandlers.runResume(ctx, laneId, opts);
|
|
7563
|
+
await withCliFeatureUsed("lane_resume", () => resumeHandlers.runResume(ctx, laneId, opts));
|
|
7219
7564
|
}));
|
|
7220
7565
|
lanes
|
|
7221
7566
|
.command("reparent")
|
|
@@ -7236,12 +7581,20 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7236
7581
|
}));
|
|
7237
7582
|
lanes
|
|
7238
7583
|
.command("stop")
|
|
7239
|
-
.description("Cancel a queued lane or kill a running one")
|
|
7584
|
+
.description("Cancel a queued lane or kill a running one (terminal; use interrupt to keep it resumable)")
|
|
7240
7585
|
.argument("<lane-id>", "Lane id")
|
|
7241
7586
|
.option("--reason <text>", "Record the reason in the lane's terminal history")
|
|
7242
7587
|
.action(wrap(async (laneId, options) => {
|
|
7243
7588
|
await runStop(ctx, laneId, options.reason);
|
|
7244
7589
|
}));
|
|
7590
|
+
lanes
|
|
7591
|
+
.command("interrupt")
|
|
7592
|
+
.description("Stop a running lane's turn and keep it resumable with hq lanes resume")
|
|
7593
|
+
.argument("<lane-id>", "Lane id")
|
|
7594
|
+
.option("--reason <text>", "Record the reason in the lane's stop history")
|
|
7595
|
+
.action(wrap(async (laneId, options) => {
|
|
7596
|
+
await runInterrupt(ctx, laneId, options.reason);
|
|
7597
|
+
}));
|
|
7245
7598
|
lanes
|
|
7246
7599
|
.command("approve")
|
|
7247
7600
|
.description("Record senior approval for a registry worker checkpoint")
|