@indigoai-us/hq-cli 5.188.0 → 5.189.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 +16 -0
- package/dist/command-catalog.generated.d.ts +28 -0
- package/dist/command-catalog.generated.js +37 -0
- package/dist/commands/lanes.d.ts +5 -1
- package/dist/commands/lanes.js +395 -69
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +21 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +167 -0
- package/dist/lib/lanes/hire-grants.d.ts +3 -2
- package/dist/lib/lanes/outpost-dispatch.d.ts +6 -1
- package/dist/lib/lanes/outpost-dispatch.js +9 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +8 -2
- package/dist/lib/lanes/ssm-command-context.d.ts +12 -0
- package/dist/lib/lanes/ssm-command-context.js +52 -0
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -13,12 +13,14 @@ import * as path from "node:path";
|
|
|
13
13
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
14
14
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
15
15
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
16
|
+
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
16
17
|
import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
|
|
17
18
|
import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
19
|
+
import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
|
|
18
20
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
19
21
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
20
22
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
21
|
-
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
23
|
+
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
22
24
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
23
25
|
import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForReport, assertBalanceApplyEnabled, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-account-balance.js";
|
|
24
26
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
@@ -32,7 +34,7 @@ import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib
|
|
|
32
34
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
33
35
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
34
36
|
import { providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
35
|
-
import { parseCliEffort, resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
37
|
+
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
36
38
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
37
39
|
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
38
40
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
@@ -81,7 +83,7 @@ function failJson(err) {
|
|
|
81
83
|
if (err.error === "hire_grant_exhausted") {
|
|
82
84
|
process.stderr.write(`hq lanes: ${err.message}\n`);
|
|
83
85
|
}
|
|
84
|
-
printJson(err.toJSON());
|
|
86
|
+
printJson({ ...err.toJSON(), message: err.message });
|
|
85
87
|
process.exit(1);
|
|
86
88
|
}
|
|
87
89
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -255,6 +257,8 @@ function resolveDeps(deps = {}) {
|
|
|
255
257
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
256
258
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
257
259
|
laneHireGrantsEnabled: deps.laneHireGrantsEnabled ?? defaultLaneHireGrantsEnabled,
|
|
260
|
+
isSsmOwnerDispatch: deps.isSsmOwnerDispatch ?? (() => isSsmRunCommandProcess()),
|
|
261
|
+
withGrantHireLocks: deps.withGrantHireLocks ?? withLaneHireLocks,
|
|
258
262
|
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
259
263
|
};
|
|
260
264
|
}
|
|
@@ -451,6 +455,81 @@ function laneHireFields(lane, lanes) {
|
|
|
451
455
|
hire_summary: hireSummary,
|
|
452
456
|
};
|
|
453
457
|
}
|
|
458
|
+
function outpostCliOverride(ctx, outpostId) {
|
|
459
|
+
const envKey = `HQ_LANES_OUTPOST_CLI_${outpostId.toUpperCase().replace(/[^A-Z0-9_]/g, "_")}`;
|
|
460
|
+
const configured = ctx.env[envKey];
|
|
461
|
+
const hqBinary = configured === undefined ? undefined : configured.trim();
|
|
462
|
+
if (configured !== undefined &&
|
|
463
|
+
(!hqBinary ||
|
|
464
|
+
!path.posix.isAbsolute(hqBinary) ||
|
|
465
|
+
hqBinary.includes("\0") ||
|
|
466
|
+
hqBinary.includes("\r") ||
|
|
467
|
+
hqBinary.includes("\n"))) {
|
|
468
|
+
throw new LanesError("outpost_lane_cli_invalid", `${envKey} must be an absolute remote executable path`, { outpost_id: outpostId });
|
|
469
|
+
}
|
|
470
|
+
return hqBinary;
|
|
471
|
+
}
|
|
472
|
+
function safeOwnerErrorValue(value, key = "") {
|
|
473
|
+
if (/(?:env|environment|token|secret|password|credential|authorization|api[_-]?key|access[_-]?key|private[_-]?key)/i.test(key)) {
|
|
474
|
+
return undefined;
|
|
475
|
+
}
|
|
476
|
+
if (typeof value === "string") {
|
|
477
|
+
return value
|
|
478
|
+
.replace(/\b(Bearer|Basic)\s+[^\s,;]+/gi, "$1 [redacted]")
|
|
479
|
+
.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[A-Z0-9]{16})\b/g, "[redacted]")
|
|
480
|
+
.replace(/\b["']?([A-Z][A-Z0-9_]{1,})["']?\s*[:=]\s*["']?([^\s"',;}]+)["']?/g, "$1=[redacted]");
|
|
481
|
+
}
|
|
482
|
+
if (Array.isArray(value)) {
|
|
483
|
+
return value.map((item) => safeOwnerErrorValue(item));
|
|
484
|
+
}
|
|
485
|
+
if (value === null || typeof value !== "object")
|
|
486
|
+
return value;
|
|
487
|
+
const safe = {};
|
|
488
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
489
|
+
const nested = safeOwnerErrorValue(childValue, childKey);
|
|
490
|
+
if (nested !== undefined)
|
|
491
|
+
safe[childKey] = nested;
|
|
492
|
+
}
|
|
493
|
+
return safe;
|
|
494
|
+
}
|
|
495
|
+
function ownerErrorFromOutput(stdout) {
|
|
496
|
+
const trimmed = stdout.trim();
|
|
497
|
+
if (!trimmed)
|
|
498
|
+
return undefined;
|
|
499
|
+
let parsed;
|
|
500
|
+
try {
|
|
501
|
+
parsed = JSON.parse(trimmed);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
process.stderr.write("hq lanes: Outpost lanes dispatch returned malformed JSON\n");
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
508
|
+
return undefined;
|
|
509
|
+
}
|
|
510
|
+
const owner = parsed;
|
|
511
|
+
if (typeof owner.error !== "string" || !/^[a-z][a-z0-9_]{0,99}$/.test(owner.error)) {
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
if (safeOwnerErrorValue(owner.error) !== owner.error)
|
|
515
|
+
return undefined;
|
|
516
|
+
if (owner.error === "already_terminal" || owner.error === "outpost_self_target") {
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
const rawDetails = {};
|
|
520
|
+
for (const [key, value] of Object.entries(owner)) {
|
|
521
|
+
if (key !== "error" && key !== "message" && key !== "details")
|
|
522
|
+
rawDetails[key] = value;
|
|
523
|
+
}
|
|
524
|
+
if (owner.details !== null && typeof owner.details === "object" && !Array.isArray(owner.details)) {
|
|
525
|
+
Object.assign(rawDetails, owner.details);
|
|
526
|
+
}
|
|
527
|
+
const details = safeOwnerErrorValue(rawDetails);
|
|
528
|
+
const message = typeof owner.message === "string" && owner.message.trim()
|
|
529
|
+
? String(safeOwnerErrorValue(owner.message))
|
|
530
|
+
: `Outpost owner returned ${owner.error}`;
|
|
531
|
+
return { error: owner.error, message, details };
|
|
532
|
+
}
|
|
454
533
|
async function ownerLaneHireFields(ctx, lane) {
|
|
455
534
|
if (lane.host.kind !== "outpost") {
|
|
456
535
|
throw new LanesError("outpost_lane_hire_summary_invalid", "Owner summary requires an Outpost-hosted lane", {
|
|
@@ -458,17 +537,7 @@ async function ownerLaneHireFields(ctx, lane) {
|
|
|
458
537
|
});
|
|
459
538
|
}
|
|
460
539
|
const outpostId = lane.host.id;
|
|
461
|
-
const
|
|
462
|
-
const configuredHqBinary = ctx.env[outpostCliEnvKey];
|
|
463
|
-
const hqBinary = configuredHqBinary === undefined ? undefined : configuredHqBinary.trim();
|
|
464
|
-
if (configuredHqBinary !== undefined &&
|
|
465
|
-
(!hqBinary ||
|
|
466
|
-
!path.posix.isAbsolute(hqBinary) ||
|
|
467
|
-
hqBinary.includes("\0") ||
|
|
468
|
-
hqBinary.includes("\r") ||
|
|
469
|
-
hqBinary.includes("\n"))) {
|
|
470
|
-
throw new LanesError("outpost_lane_cli_invalid", `${outpostCliEnvKey} must be an absolute remote executable path`, { outpost_id: outpostId });
|
|
471
|
-
}
|
|
540
|
+
const hqBinary = outpostCliOverride(ctx, outpostId);
|
|
472
541
|
let dispatched;
|
|
473
542
|
try {
|
|
474
543
|
dispatched = await ctx.dispatchOutpostExec({
|
|
@@ -2783,7 +2852,8 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
2783
2852
|
const staged = await ctx.stageOutpostPayload(outpostId, bytes);
|
|
2784
2853
|
const command = buildOutpostPickupCommand(staged.getUrl);
|
|
2785
2854
|
const dispatched = await ctx.dispatchOutpostExec({ outpostId, command });
|
|
2786
|
-
|
|
2855
|
+
const dispatchedOwnerError = ownerErrorFromOutput(dispatched.stdout);
|
|
2856
|
+
if (preserveQueuedOnFailure && dispatched.done !== true && !dispatchedOwnerError) {
|
|
2787
2857
|
throw new LanesError("outpost_dispatch_failed", "Outpost pickup exec has not finished; its outcome is unknown", {
|
|
2788
2858
|
outpost_id: outpostId,
|
|
2789
2859
|
lane_id: lane.lane_id,
|
|
@@ -2813,6 +2883,40 @@ async function dispatchCreatedOutpostLane(ctx, input) {
|
|
|
2813
2883
|
await emitLaneTerminalStateSoft(ctx, cancelled);
|
|
2814
2884
|
return cancelled;
|
|
2815
2885
|
}
|
|
2886
|
+
const ownerError = dispatchedOwnerError;
|
|
2887
|
+
if (ownerError) {
|
|
2888
|
+
if (preserveQueuedOnFailure) {
|
|
2889
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected lane pickup: ${ownerError.message}`, {
|
|
2890
|
+
outpost_id: outpostId,
|
|
2891
|
+
lane_id: lane.lane_id,
|
|
2892
|
+
command_id: dispatched.commandId,
|
|
2893
|
+
status: dispatched.status,
|
|
2894
|
+
done: dispatched.done ?? false,
|
|
2895
|
+
exit_code: dispatched.exitCode,
|
|
2896
|
+
...(remote?.pickup_persisted === false ||
|
|
2897
|
+
ownerError.details.pickup_persisted === false
|
|
2898
|
+
? { pickup_persisted: false }
|
|
2899
|
+
: {}),
|
|
2900
|
+
owner_error: ownerError,
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
const failed = markDispatchFailure(hqRoot, lane.lane_id, {
|
|
2904
|
+
outpost_id: outpostId,
|
|
2905
|
+
command_id: dispatched.commandId,
|
|
2906
|
+
exit_code: dispatched.exitCode,
|
|
2907
|
+
status: dispatched.status,
|
|
2908
|
+
owner_error: ownerError,
|
|
2909
|
+
});
|
|
2910
|
+
await emitLaneTerminalStateSoft(ctx, failed);
|
|
2911
|
+
throw new LanesError(DISPATCH_TERMINAL_REASON, `Outpost ${outpostId} rejected lane pickup: ${ownerError.message}`, {
|
|
2912
|
+
outpost_id: outpostId,
|
|
2913
|
+
lane_id: failed.lane_id,
|
|
2914
|
+
state: failed.state,
|
|
2915
|
+
terminal_reason: DISPATCH_TERMINAL_REASON,
|
|
2916
|
+
command_id: dispatched.commandId,
|
|
2917
|
+
owner_error: ownerError,
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2816
2920
|
if (preserveQueuedOnFailure && remote?.pickup_persisted === false) {
|
|
2817
2921
|
throw new LanesError("outpost_dispatch_failed", "Outpost pickup finished and reported that it did not persist the lane", {
|
|
2818
2922
|
outpost_id: outpostId,
|
|
@@ -2980,19 +3084,26 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
2980
3084
|
throw new LanesError("outpost_dispatch_failed", `Lane ${lane.lane_id} has no Outpost host to dispatch to`, { lane_id: lane.lane_id });
|
|
2981
3085
|
}
|
|
2982
3086
|
const outpostId = lane.host.id;
|
|
3087
|
+
const grantContext = action === "grant"
|
|
3088
|
+
? createOutpostHireGrantContext(lane, loadAllLanes(ctx.hqRoot()), await ctx.laneHireGrantsEnabled(lane.company))
|
|
3089
|
+
: undefined;
|
|
2983
3090
|
let dispatched;
|
|
2984
3091
|
try {
|
|
2985
3092
|
dispatched = await ctx.dispatchOutpostExec({
|
|
2986
3093
|
outpostId,
|
|
2987
3094
|
command: buildOutpostLaneCommand({
|
|
2988
|
-
action,
|
|
3095
|
+
action: action === "grant" ? "grant-target" : action,
|
|
2989
3096
|
laneId: lane.lane_id,
|
|
3097
|
+
hqBinary: action === "grant" ? outpostCliOverride(ctx, outpostId) : undefined,
|
|
2990
3098
|
text: options.text,
|
|
2991
3099
|
project: options.project,
|
|
2992
3100
|
story: options.story,
|
|
2993
3101
|
account: options.account,
|
|
2994
3102
|
model: options.model,
|
|
3103
|
+
effort: options.effort,
|
|
3104
|
+
worker: options.worker,
|
|
2995
3105
|
hire: options.hire,
|
|
3106
|
+
grantContext,
|
|
2996
3107
|
compact: options.compact,
|
|
2997
3108
|
}),
|
|
2998
3109
|
});
|
|
@@ -3000,6 +3111,30 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3000
3111
|
catch (err) {
|
|
3001
3112
|
throw new LanesError("outpost_dispatch_failed", `Could not dispatch ${action} for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
3002
3113
|
}
|
|
3114
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
3115
|
+
if (options.effort !== undefined && outpostRejectedEffortOption(dispatched)) {
|
|
3116
|
+
throw new LanesError("outpost_effort_unsupported", `Outpost ${outpostId} rejected --effort; update hq-cli on that Outpost before changing a lane's effort remotely`, {
|
|
3117
|
+
lane_id: lane.lane_id,
|
|
3118
|
+
outpost_id: outpostId,
|
|
3119
|
+
command_id: dispatched.commandId,
|
|
3120
|
+
effort: options.effort,
|
|
3121
|
+
status: dispatched.status,
|
|
3122
|
+
done: dispatched.done ?? false,
|
|
3123
|
+
exit_code: dispatched.exitCode,
|
|
3124
|
+
owner_error: ownerError,
|
|
3125
|
+
});
|
|
3126
|
+
}
|
|
3127
|
+
if (ownerError) {
|
|
3128
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command: ${ownerError.message}`, {
|
|
3129
|
+
lane_id: lane.lane_id,
|
|
3130
|
+
outpost_id: outpostId,
|
|
3131
|
+
command_id: dispatched.commandId,
|
|
3132
|
+
status: dispatched.status,
|
|
3133
|
+
done: dispatched.done ?? false,
|
|
3134
|
+
exit_code: dispatched.exitCode,
|
|
3135
|
+
owner_error: ownerError,
|
|
3136
|
+
});
|
|
3137
|
+
}
|
|
3003
3138
|
if (dispatched.done !== true ||
|
|
3004
3139
|
dispatched.status !== "Success" ||
|
|
3005
3140
|
dispatched.exitCode !== 0) {
|
|
@@ -3014,13 +3149,11 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3014
3149
|
}
|
|
3015
3150
|
const remote = parseRemoteJson(dispatched.stdout, "Outpost lane command");
|
|
3016
3151
|
if (remote?.error !== undefined) {
|
|
3017
|
-
throw new LanesError("
|
|
3018
|
-
? remote.message
|
|
3019
|
-
: `Outpost ${outpostId} rejected the ${action} command`, {
|
|
3152
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the ${action} command`, {
|
|
3020
3153
|
lane_id: lane.lane_id,
|
|
3021
3154
|
outpost_id: outpostId,
|
|
3022
3155
|
command_id: dispatched.commandId,
|
|
3023
|
-
|
|
3156
|
+
owner_error: ownerErrorFromOutput(dispatched.stdout),
|
|
3024
3157
|
});
|
|
3025
3158
|
}
|
|
3026
3159
|
const resultConfirmed = remote !== null &&
|
|
@@ -3642,6 +3775,9 @@ async function commitLaneMove(ctx, laneId, target, account) {
|
|
|
3642
3775
|
outcome: "unknown",
|
|
3643
3776
|
...(commandId !== undefined ? { command_id: commandId } : {}),
|
|
3644
3777
|
check_command: checkCommand,
|
|
3778
|
+
...(dispatchDetails.owner_error !== undefined
|
|
3779
|
+
? { owner_error: dispatchDetails.owner_error }
|
|
3780
|
+
: {}),
|
|
3645
3781
|
});
|
|
3646
3782
|
}
|
|
3647
3783
|
let restored = false;
|
|
@@ -3682,6 +3818,9 @@ async function commitLaneMove(ctx, laneId, target, account) {
|
|
|
3682
3818
|
source_host: before.host,
|
|
3683
3819
|
target_host: target,
|
|
3684
3820
|
source_restored: restored,
|
|
3821
|
+
...(dispatchDetails.owner_error !== undefined
|
|
3822
|
+
? { owner_error: dispatchDetails.owner_error }
|
|
3823
|
+
: {}),
|
|
3685
3824
|
...(rollbackError !== undefined
|
|
3686
3825
|
? { rollback_error: dispatchErrorMessage(rollbackError) }
|
|
3687
3826
|
: {}),
|
|
@@ -3711,6 +3850,18 @@ async function dispatchRemoteLaneMove(ctx, source, target, account) {
|
|
|
3711
3850
|
catch (err) {
|
|
3712
3851
|
throw new LanesError("lane_move_dispatch_failed", `Could not ask Outpost ${source.host.id} to move lane ${source.lane_id}: ${dispatchErrorMessage(err)}`, { lane_id: source.lane_id, source_outpost: source.host.id });
|
|
3713
3852
|
}
|
|
3853
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
3854
|
+
if (ownerError) {
|
|
3855
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${source.host.id} rejected the lane move: ${ownerError.message}`, {
|
|
3856
|
+
lane_id: source.lane_id,
|
|
3857
|
+
source_outpost: source.host.id,
|
|
3858
|
+
command_id: dispatched.commandId,
|
|
3859
|
+
status: dispatched.status,
|
|
3860
|
+
done: dispatched.done ?? false,
|
|
3861
|
+
exit_code: dispatched.exitCode,
|
|
3862
|
+
owner_error: ownerError,
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3714
3865
|
if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
|
|
3715
3866
|
throw new LanesError("lane_move_dispatch_failed", `Outpost ${source.host.id} did not confirm the lane move`, {
|
|
3716
3867
|
lane_id: source.lane_id,
|
|
@@ -3830,40 +3981,54 @@ function callerMatchesSenior(ctx, seniorRef) {
|
|
|
3830
3981
|
function forceAdmissionAuthorized(ctx, lane) {
|
|
3831
3982
|
return callerMatchesSenior(ctx, lane.senior);
|
|
3832
3983
|
}
|
|
3833
|
-
async function
|
|
3834
|
-
if (opts.hire === undefined) {
|
|
3835
|
-
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
3836
|
-
}
|
|
3837
|
-
const hireGrant = parseHireGrant(opts.hire);
|
|
3984
|
+
async function applyHireGrant(ctx, laneId, hireGrant, authorization, grantContext) {
|
|
3838
3985
|
const hqRoot = ctx.hqRoot();
|
|
3839
3986
|
const initial = readLane(hqRoot, laneId);
|
|
3840
|
-
if (!callerMatchesSenior(ctx, initial.senior)) {
|
|
3987
|
+
if (authorization === "senior" && !callerMatchesSenior(ctx, initial.senior)) {
|
|
3841
3988
|
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
|
|
3842
3989
|
}
|
|
3843
|
-
if (
|
|
3844
|
-
|
|
3845
|
-
|
|
3990
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
3991
|
+
(initial.host.kind !== "outpost" || !laneIsHostedHere(ctx, initial))) {
|
|
3992
|
+
throw new LanesError("lane_remote", `Outpost grant dispatch cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: initial.host });
|
|
3846
3993
|
}
|
|
3847
|
-
|
|
3994
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
3995
|
+
(!grantContext || !sameHireGrantSenior(initial.senior, grantContext.expected_senior))) {
|
|
3996
|
+
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 });
|
|
3997
|
+
}
|
|
3998
|
+
const enforce = grantContext?.enforcement_enabled ??
|
|
3999
|
+
await ctx.laneHireGrantsEnabled(initial.company);
|
|
3848
4000
|
const lockedLaneIds = [
|
|
3849
4001
|
laneId,
|
|
3850
4002
|
...(initial.senior.kind === "lane" && initial.senior.id
|
|
3851
4003
|
? [initial.senior.id]
|
|
3852
4004
|
: []),
|
|
3853
4005
|
];
|
|
3854
|
-
const updated =
|
|
4006
|
+
const updated = ctx.withGrantHireLocks(hqRoot, lockedLaneIds, () => {
|
|
3855
4007
|
const current = readLane(hqRoot, laneId);
|
|
3856
|
-
if (!callerMatchesSenior(ctx, current.senior)) {
|
|
4008
|
+
if (authorization === "senior" && !callerMatchesSenior(ctx, current.senior)) {
|
|
3857
4009
|
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(current.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: current.senior });
|
|
3858
4010
|
}
|
|
3859
|
-
|
|
4011
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
4012
|
+
(current.host.kind !== "outpost" || !laneIsHostedHere(ctx, current))) {
|
|
4013
|
+
throw new LanesError("lane_remote", `Outpost grant dispatch cannot mutate lane ${laneId} on this host`, { lane_id: laneId, host: current.host });
|
|
4014
|
+
}
|
|
4015
|
+
if (authorization === "ssm-owner-dispatch" &&
|
|
4016
|
+
(!grantContext || !sameHireGrantSenior(current.senior, grantContext.expected_senior))) {
|
|
4017
|
+
throw new LanesError("hire_grant_senior_changed", `Lane ${laneId}'s senior changed before the Outpost applied the grant update`, { lane_id: laneId, current_senior: current.senior });
|
|
4018
|
+
}
|
|
4019
|
+
const hierarchy = grantContext?.hierarchy ?? loadAllLanes(hqRoot);
|
|
3860
4020
|
if (enforce && current.senior.kind === "lane") {
|
|
3861
4021
|
if (!current.senior.id) {
|
|
3862
4022
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${laneId} has a lane senior without an id`, {
|
|
3863
4023
|
lane_id: laneId,
|
|
3864
4024
|
});
|
|
3865
4025
|
}
|
|
3866
|
-
const parent =
|
|
4026
|
+
const parent = hierarchy.find((lane) => lane.lane_id === current.senior.id);
|
|
4027
|
+
if (!parent) {
|
|
4028
|
+
throw new LanesError(authorization === "ssm-owner-dispatch"
|
|
4029
|
+
? "hire_grant_context_invalid"
|
|
4030
|
+
: "hire_grant_senior_invalid", `Lane ${laneId}'s senior lane is missing from the grant hierarchy`, { lane_id: laneId, senior_lane_id: current.senior.id });
|
|
4031
|
+
}
|
|
3867
4032
|
if (isTerminalLaneState(parent.state)) {
|
|
3868
4033
|
throw new LanesError("hire_grant_senior_inactive", `Senior lane ${parent.lane_id} is ${parent.state} and cannot change grants`, { senior_lane_id: parent.lane_id, state: parent.state });
|
|
3869
4034
|
}
|
|
@@ -3901,9 +4066,44 @@ async function runGrant(ctx, laneId, opts) {
|
|
|
3901
4066
|
hire_grant: hireGrant,
|
|
3902
4067
|
}));
|
|
3903
4068
|
});
|
|
3904
|
-
|
|
4069
|
+
return updated;
|
|
4070
|
+
}
|
|
4071
|
+
function printGrantResult(hqRoot, updated, hierarchy) {
|
|
4072
|
+
const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot));
|
|
3905
4073
|
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
3906
4074
|
}
|
|
4075
|
+
async function runGrant(ctx, laneId, opts) {
|
|
4076
|
+
if (opts.hire === undefined) {
|
|
4077
|
+
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
4078
|
+
}
|
|
4079
|
+
const hireGrant = parseHireGrant(opts.hire);
|
|
4080
|
+
const hqRoot = ctx.hqRoot();
|
|
4081
|
+
const initial = readLane(hqRoot, laneId);
|
|
4082
|
+
if (!callerMatchesSenior(ctx, initial.senior)) {
|
|
4083
|
+
throw new LanesError("hire_grant_unauthorized", `Only senior ${seniorKey(initial.senior)} may change lane ${laneId}'s hire grant`, { lane_id: laneId, senior: initial.senior });
|
|
4084
|
+
}
|
|
4085
|
+
if (initial.host.kind === "outpost" && !laneRunsHere(initial, reconcileHostFromCtx(ctx))) {
|
|
4086
|
+
await dispatchOutpostLaneCommand(ctx, initial, "grant", { hire: String(hireGrant) });
|
|
4087
|
+
return;
|
|
4088
|
+
}
|
|
4089
|
+
const updated = await applyHireGrant(ctx, laneId, hireGrant, "senior");
|
|
4090
|
+
printGrantResult(hqRoot, updated);
|
|
4091
|
+
}
|
|
4092
|
+
async function runGrantOwnerTarget(ctx, laneId, opts) {
|
|
4093
|
+
if (!ctx.isSsmOwnerDispatch()) {
|
|
4094
|
+
throw new LanesError("hire_grant_dispatch_unauthorized", "The Outpost grant target is available only to an SSM Run Command dispatch", { lane_id: laneId });
|
|
4095
|
+
}
|
|
4096
|
+
if (opts.hire === undefined) {
|
|
4097
|
+
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
4098
|
+
}
|
|
4099
|
+
if (opts.grantContext === undefined) {
|
|
4100
|
+
throw new LanesError("hire_grant_context_invalid", "--grant-context is required", { lane_id: laneId });
|
|
4101
|
+
}
|
|
4102
|
+
const hireGrant = parseHireGrant(opts.hire);
|
|
4103
|
+
const grantContext = parseOutpostHireGrantContext(opts.grantContext, laneId);
|
|
4104
|
+
const hqRoot = ctx.hqRoot();
|
|
4105
|
+
printGrantResult(hqRoot, await applyHireGrant(ctx, laneId, hireGrant, "ssm-owner-dispatch", grantContext), grantContext.hierarchy);
|
|
4106
|
+
}
|
|
3907
4107
|
async function runBackfillGrants(ctx, opts) {
|
|
3908
4108
|
if (opts.apply && opts.dryRun) {
|
|
3909
4109
|
throw new LanesError("hire_grant_backfill_mode_conflict", "Pass either --dry-run or --apply, not both", {});
|
|
@@ -4150,34 +4350,43 @@ function validateResumeProfileModel(ctx, lane, profile) {
|
|
|
4150
4350
|
env: ctx.env,
|
|
4151
4351
|
});
|
|
4152
4352
|
}
|
|
4353
|
+
function applyResumeProfileAndEffort(current, next, input) {
|
|
4354
|
+
if (input.profile !== undefined) {
|
|
4355
|
+
const profile = input.profile.profile;
|
|
4356
|
+
next.worker = input.profile.id;
|
|
4357
|
+
next.worker_path = input.profile.path;
|
|
4358
|
+
next.worker_pack = input.profile.pack;
|
|
4359
|
+
next.worker_profile = profile.id;
|
|
4360
|
+
next.worker_profile_hash = profile.contentHash;
|
|
4361
|
+
next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
|
|
4362
|
+
next.worker_policy_delivery = undefined;
|
|
4363
|
+
if (next.model_source !== "cli") {
|
|
4364
|
+
if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
|
|
4365
|
+
next.model = profile.meta.model;
|
|
4366
|
+
next.model_source = "profile";
|
|
4367
|
+
}
|
|
4368
|
+
else if (profile.meta.provider === current.provider) {
|
|
4369
|
+
next.model = undefined;
|
|
4370
|
+
next.model_source = "none";
|
|
4371
|
+
}
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
if (input.effort !== undefined) {
|
|
4375
|
+
next.effort = input.effort.effort;
|
|
4376
|
+
next.effort_source = input.effort.source;
|
|
4377
|
+
}
|
|
4378
|
+
}
|
|
4153
4379
|
function recordResumeSetup(ctx, lane, input) {
|
|
4154
|
-
if (input.profile === undefined &&
|
|
4380
|
+
if (input.profile === undefined &&
|
|
4381
|
+
input.effort === undefined &&
|
|
4382
|
+
input.meshCompany === undefined)
|
|
4155
4383
|
return lane;
|
|
4156
4384
|
return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
|
|
4157
4385
|
if (current.version !== lane.version) {
|
|
4158
4386
|
throw new LanesError("lane_resume_setup_conflict", `Lane ${lane.lane_id} changed while resume settings were prepared; run the resume again`, { lane_id: lane.lane_id, prepared_version: lane.version, version: current.version });
|
|
4159
4387
|
}
|
|
4160
4388
|
const next = { ...current };
|
|
4161
|
-
|
|
4162
|
-
const profile = input.profile.profile;
|
|
4163
|
-
next.worker = input.profile.id;
|
|
4164
|
-
next.worker_path = input.profile.path;
|
|
4165
|
-
next.worker_pack = input.profile.pack;
|
|
4166
|
-
next.worker_profile = profile.id;
|
|
4167
|
-
next.worker_profile_hash = profile.contentHash;
|
|
4168
|
-
next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
|
|
4169
|
-
next.worker_policy_delivery = undefined;
|
|
4170
|
-
if (current.model_source !== "cli") {
|
|
4171
|
-
if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
|
|
4172
|
-
next.model = profile.meta.model;
|
|
4173
|
-
next.model_source = "profile";
|
|
4174
|
-
}
|
|
4175
|
-
else if (profile.meta.provider === current.provider) {
|
|
4176
|
-
next.model = undefined;
|
|
4177
|
-
next.model_source = "none";
|
|
4178
|
-
}
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4389
|
+
applyResumeProfileAndEffort(current, next, input);
|
|
4181
4390
|
if (input.meshCompany !== undefined) {
|
|
4182
4391
|
if (current.company === null)
|
|
4183
4392
|
next.mesh_company = input.meshCompany;
|
|
@@ -4187,6 +4396,40 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4187
4396
|
return next;
|
|
4188
4397
|
});
|
|
4189
4398
|
}
|
|
4399
|
+
/** Resolve an explicit resume override or newly attached profile like create does. */
|
|
4400
|
+
function resolveResumeEffort(ctx, lane, input) {
|
|
4401
|
+
if (input.cliEffort === undefined && input.profile === undefined)
|
|
4402
|
+
return undefined;
|
|
4403
|
+
const config = loadLanesConfig(ctx.hqRoot());
|
|
4404
|
+
const providerConfig = config.providers[lane.provider];
|
|
4405
|
+
const accountId = input.account?.trim() || lane.account;
|
|
4406
|
+
const accountConfig = accountId === undefined
|
|
4407
|
+
? undefined
|
|
4408
|
+
: providerConfig.accounts[accountId];
|
|
4409
|
+
const profileDefaultsApply = input.profile?.profile.meta.provider === lane.provider;
|
|
4410
|
+
const pinnedModel = lane.model_source === undefined || lane.model_source === "cli"
|
|
4411
|
+
? lane.model
|
|
4412
|
+
: undefined;
|
|
4413
|
+
const model = resolveLaneModel({
|
|
4414
|
+
cliModel: input.model?.trim() || pinnedModel,
|
|
4415
|
+
profileModel: profileDefaultsApply ? input.profile?.profile.meta.model : undefined,
|
|
4416
|
+
accountModel: accountConfig?.model,
|
|
4417
|
+
providerModel: providerConfig.model,
|
|
4418
|
+
});
|
|
4419
|
+
return resolveLaneEffort({
|
|
4420
|
+
cliEffort: input.cliEffort,
|
|
4421
|
+
profileEffort: profileDefaultsApply ? input.profile?.profile.meta.effort : undefined,
|
|
4422
|
+
accountEffort: accountConfig?.effort,
|
|
4423
|
+
providerEffort: providerConfig.effort,
|
|
4424
|
+
model,
|
|
4425
|
+
});
|
|
4426
|
+
}
|
|
4427
|
+
function recordedResumeEffort(lane) {
|
|
4428
|
+
return {
|
|
4429
|
+
...(lane.effort !== undefined ? { effort: lane.effort } : {}),
|
|
4430
|
+
source: lane.effort_source ?? "none",
|
|
4431
|
+
};
|
|
4432
|
+
}
|
|
4190
4433
|
/**
|
|
4191
4434
|
* US-023: move the lane's provider session to `--account` (same provider)
|
|
4192
4435
|
* before a resume or a message resume. Every check runs before anything is
|
|
@@ -4254,7 +4497,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
4254
4497
|
}).logged_in !== true) {
|
|
4255
4498
|
throw new LanesError("account_not_logged_in", `Account ${target} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: target, lane_id: laneId });
|
|
4256
4499
|
}
|
|
4257
|
-
const profile = input.workerProfile ?? (lane.worker_profile !== undefined
|
|
4500
|
+
const profile = input.workerProfile?.profile ?? (lane.worker_profile !== undefined
|
|
4258
4501
|
? loadWorkerProfile(hqRoot, lane.worker_profile)
|
|
4259
4502
|
: undefined);
|
|
4260
4503
|
const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
|
|
@@ -4356,6 +4599,10 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
4356
4599
|
? undefined
|
|
4357
4600
|
: { session_id: sessionId, at, ...input.pending },
|
|
4358
4601
|
};
|
|
4602
|
+
applyResumeProfileAndEffort(current, next, {
|
|
4603
|
+
...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
|
|
4604
|
+
...(input.effort !== undefined ? { effort: input.effort } : {}),
|
|
4605
|
+
});
|
|
4359
4606
|
if (input.deferTurnStart) {
|
|
4360
4607
|
next.state = current.state;
|
|
4361
4608
|
}
|
|
@@ -4432,6 +4679,10 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4432
4679
|
return;
|
|
4433
4680
|
}
|
|
4434
4681
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "message");
|
|
4682
|
+
const cliEffort = parseCliEffort(lane.provider, opts.effort);
|
|
4683
|
+
if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
4684
|
+
throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
|
|
4685
|
+
}
|
|
4435
4686
|
// Check before transcript recovery, account probing, or message writes.
|
|
4436
4687
|
// The final state transition repeats this check under the senior lock.
|
|
4437
4688
|
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
@@ -4463,12 +4714,24 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4463
4714
|
if (opts.worker !== undefined && mechanism === "dropbox") {
|
|
4464
4715
|
throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
4465
4716
|
}
|
|
4717
|
+
if (cliEffort !== undefined && mechanism === "dropbox") {
|
|
4718
|
+
throw new LanesError("lane_effort_requires_resume", `Lane ${laneId} is still running; --effort can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
|
|
4719
|
+
}
|
|
4466
4720
|
const resumeProfile = opts.worker !== undefined
|
|
4467
4721
|
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
4468
4722
|
: undefined;
|
|
4469
4723
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
4470
4724
|
throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
|
|
4471
4725
|
}
|
|
4726
|
+
const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
|
|
4727
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
4728
|
+
: undefined;
|
|
4729
|
+
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
4730
|
+
cliEffort,
|
|
4731
|
+
profile: resumeProfile,
|
|
4732
|
+
account: opts.account ?? automaticAccountSelection?.account,
|
|
4733
|
+
model: opts.model,
|
|
4734
|
+
});
|
|
4472
4735
|
const messageId = mintMessageId();
|
|
4473
4736
|
const at = ctx.now().toISOString();
|
|
4474
4737
|
const projectId = opts.project?.trim() || lane.project_id;
|
|
@@ -4489,7 +4752,8 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4489
4752
|
move = await prepareAccountMove(ctx, lane, {
|
|
4490
4753
|
account: opts.account,
|
|
4491
4754
|
cliModel: opts.model?.trim() || undefined,
|
|
4492
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile
|
|
4755
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
4756
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
4493
4757
|
pending: { trigger: "message", text, message_id: messageId, compact: opts.compact !== false },
|
|
4494
4758
|
});
|
|
4495
4759
|
lane = move.lane;
|
|
@@ -4498,13 +4762,14 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4498
4762
|
}
|
|
4499
4763
|
}
|
|
4500
4764
|
else if (mechanism === "resume") {
|
|
4501
|
-
const selection =
|
|
4765
|
+
const selection = automaticAccountSelection;
|
|
4502
4766
|
if (selection !== undefined) {
|
|
4503
4767
|
if (selection.account !== lane.account) {
|
|
4504
4768
|
move = await prepareAccountMove(ctx, lane, {
|
|
4505
4769
|
account: selection.account,
|
|
4506
4770
|
cliModel: undefined,
|
|
4507
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile
|
|
4771
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
4772
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
4508
4773
|
pending: {
|
|
4509
4774
|
trigger: "message",
|
|
4510
4775
|
text,
|
|
@@ -4522,10 +4787,14 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4522
4787
|
}
|
|
4523
4788
|
}
|
|
4524
4789
|
}
|
|
4525
|
-
if (resumeProfile !== undefined) {
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4790
|
+
if (resumeProfile !== undefined && opts.account === undefined) {
|
|
4791
|
+
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
4792
|
+
}
|
|
4793
|
+
if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined)) {
|
|
4794
|
+
lane = recordResumeSetup(ctx, lane, {
|
|
4795
|
+
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
4796
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
4797
|
+
});
|
|
4529
4798
|
}
|
|
4530
4799
|
if (move?.queued) {
|
|
4531
4800
|
// Parked at the target's cap: reconcile resumes the session there.
|
|
@@ -4599,6 +4868,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4599
4868
|
}, {
|
|
4600
4869
|
trigger: { kind: "message", text, message_id: messageId },
|
|
4601
4870
|
compact: opts.compact !== false,
|
|
4871
|
+
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
4602
4872
|
});
|
|
4603
4873
|
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
4604
4874
|
return started;
|
|
@@ -4885,6 +5155,10 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4885
5155
|
// slot that its senior has already assigned elsewhere.
|
|
4886
5156
|
await withResumeHireAdmission(ctx, lane, () => undefined);
|
|
4887
5157
|
assertLaneRunsHereForOperatorMutation(ctx, lane, "resume");
|
|
5158
|
+
const cliEffort = parseCliEffort(lane.provider, opts.effort);
|
|
5159
|
+
if (cliEffort !== undefined && !providerSupportsSessionResume(lane.provider)) {
|
|
5160
|
+
throw new LanesError("effort_resume_unsupported", `Provider ${lane.provider} cannot change effort on a resumed session`, { lane_id: laneId, provider: lane.provider });
|
|
5161
|
+
}
|
|
4888
5162
|
lane = laneWithRecoveredProviderSession(hqRoot, lane);
|
|
4889
5163
|
if (opts.model !== undefined && opts.account === undefined) {
|
|
4890
5164
|
throw new LanesError("model_requires_account", "--model on hq lanes resume is only accepted together with --account", {});
|
|
@@ -4941,12 +5215,19 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4941
5215
|
const automaticAccountSelection = opts.account === undefined
|
|
4942
5216
|
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
4943
5217
|
: undefined;
|
|
5218
|
+
const resumeEffort = resolveResumeEffort(ctx, lane, {
|
|
5219
|
+
cliEffort,
|
|
5220
|
+
profile: resumeProfile,
|
|
5221
|
+
account: opts.account ?? automaticAccountSelection?.account,
|
|
5222
|
+
model: opts.model,
|
|
5223
|
+
});
|
|
4944
5224
|
let move = null;
|
|
4945
5225
|
if (opts.account !== undefined) {
|
|
4946
5226
|
move = await prepareAccountMove(ctx, lane, {
|
|
4947
5227
|
account: opts.account,
|
|
4948
5228
|
cliModel: opts.model?.trim() || undefined,
|
|
4949
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile
|
|
5229
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5230
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
4950
5231
|
pending: {
|
|
4951
5232
|
trigger: "resume",
|
|
4952
5233
|
resumed_from: resumedFrom,
|
|
@@ -4962,7 +5243,8 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4962
5243
|
move = await prepareAccountMove(ctx, lane, {
|
|
4963
5244
|
account: automaticAccountSelection.account,
|
|
4964
5245
|
cliModel: undefined,
|
|
4965
|
-
...(resumeProfile !== undefined ? { workerProfile: resumeProfile
|
|
5246
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5247
|
+
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
4966
5248
|
pending: {
|
|
4967
5249
|
trigger: "resume",
|
|
4968
5250
|
resumed_from: resumedFrom,
|
|
@@ -4979,7 +5261,8 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4979
5261
|
}
|
|
4980
5262
|
}
|
|
4981
5263
|
lane = recordResumeSetup(ctx, lane, {
|
|
4982
|
-
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5264
|
+
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5265
|
+
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
4983
5266
|
...(requestedMeshCompany !== undefined
|
|
4984
5267
|
? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
|
|
4985
5268
|
: {}),
|
|
@@ -5053,6 +5336,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5053
5336
|
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
5054
5337
|
},
|
|
5055
5338
|
compact: opts.compact !== false,
|
|
5339
|
+
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
5056
5340
|
});
|
|
5057
5341
|
});
|
|
5058
5342
|
}, ctx.sleep);
|
|
@@ -5718,6 +6002,16 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
|
5718
6002
|
!commandId ||
|
|
5719
6003
|
dispatched.status !== "Success" ||
|
|
5720
6004
|
dispatched.exitCode !== 0) {
|
|
6005
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
6006
|
+
if (ownerError) {
|
|
6007
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the lane read after stop: ${ownerError.message}`, {
|
|
6008
|
+
lane_id: laneId,
|
|
6009
|
+
outpost_id: outpostId,
|
|
6010
|
+
command_id: stopCommandId,
|
|
6011
|
+
sync_command_id: commandId || null,
|
|
6012
|
+
owner_error: ownerError,
|
|
6013
|
+
});
|
|
6014
|
+
}
|
|
5721
6015
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not confirm a lane read after stop ${laneId}`, {
|
|
5722
6016
|
lane_id: laneId,
|
|
5723
6017
|
outpost_id: outpostId,
|
|
@@ -5741,6 +6035,16 @@ async function readOutpostTerminalLane(ctx, laneId, outpostId, stopCommandId) {
|
|
|
5741
6035
|
cause: err instanceof Error ? err.message : String(err),
|
|
5742
6036
|
});
|
|
5743
6037
|
}
|
|
6038
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
6039
|
+
if (ownerError) {
|
|
6040
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the lane read after stop: ${ownerError.message}`, {
|
|
6041
|
+
lane_id: laneId,
|
|
6042
|
+
outpost_id: outpostId,
|
|
6043
|
+
command_id: stopCommandId,
|
|
6044
|
+
sync_command_id: commandId,
|
|
6045
|
+
owner_error: ownerError,
|
|
6046
|
+
});
|
|
6047
|
+
}
|
|
5744
6048
|
if (remote?.ok !== true || remote.action !== "show") {
|
|
5745
6049
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return lane ${laneId} after stop`, {
|
|
5746
6050
|
lane_id: laneId,
|
|
@@ -5771,6 +6075,18 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
5771
6075
|
catch (err) {
|
|
5772
6076
|
throw new LanesError("outpost_dispatch_failed", `Could not dispatch stop for lane ${lane.lane_id} to Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { lane_id: lane.lane_id, outpost_id: outpostId });
|
|
5773
6077
|
}
|
|
6078
|
+
const ownerError = ownerErrorFromOutput(dispatched.stdout);
|
|
6079
|
+
if (ownerError) {
|
|
6080
|
+
throw new LanesError("outpost_dispatch_failed", `Outpost ${outpostId} rejected the stop command: ${ownerError.message}`, {
|
|
6081
|
+
lane_id: lane.lane_id,
|
|
6082
|
+
outpost_id: outpostId,
|
|
6083
|
+
command_id: dispatched.commandId,
|
|
6084
|
+
status: dispatched.status,
|
|
6085
|
+
done: dispatched.done ?? false,
|
|
6086
|
+
exit_code: dispatched.exitCode,
|
|
6087
|
+
owner_error: ownerError,
|
|
6088
|
+
});
|
|
6089
|
+
}
|
|
5774
6090
|
const commandId = dispatched.commandId?.trim();
|
|
5775
6091
|
if (dispatched.done !== true || !commandId) {
|
|
5776
6092
|
throw new LanesError("outpost_delivery_unconfirmed", `Outpost ${outpostId} did not return a completed stop command for lane ${lane.lane_id}`, {
|
|
@@ -5814,13 +6130,13 @@ async function dispatchOutpostLaneStop(ctx, lane) {
|
|
|
5814
6130
|
syncCommandId = owner.commandId;
|
|
5815
6131
|
}
|
|
5816
6132
|
else if (remote?.error !== undefined) {
|
|
5817
|
-
throw new LanesError("
|
|
6133
|
+
throw new LanesError("outpost_dispatch_failed", typeof remote.message === "string"
|
|
5818
6134
|
? remote.message
|
|
5819
6135
|
: `Outpost ${outpostId} rejected the stop command for lane ${lane.lane_id}`, {
|
|
5820
6136
|
lane_id: lane.lane_id,
|
|
5821
6137
|
outpost_id: outpostId,
|
|
5822
6138
|
command_id: commandId,
|
|
5823
|
-
|
|
6139
|
+
owner_error: ownerErrorFromOutput(dispatched.stdout),
|
|
5824
6140
|
});
|
|
5825
6141
|
}
|
|
5826
6142
|
else {
|
|
@@ -6661,6 +6977,14 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6661
6977
|
.action(wrap(async (opts) => {
|
|
6662
6978
|
await runCreate(ctx, opts);
|
|
6663
6979
|
}));
|
|
6980
|
+
lanes
|
|
6981
|
+
.command("grant-target", { hidden: true })
|
|
6982
|
+
.argument("<lane-id>", "Lane id")
|
|
6983
|
+
.option("--hire <n>", "Non-negative safe integer hire grant")
|
|
6984
|
+
.option("--grant-context <base64url>", "Primary-authorized hire hierarchy and gate decision")
|
|
6985
|
+
.action(wrap(async (laneId, opts) => {
|
|
6986
|
+
await runGrantOwnerTarget(ctx, laneId, opts);
|
|
6987
|
+
}));
|
|
6664
6988
|
lanes
|
|
6665
6989
|
.command("grant")
|
|
6666
6990
|
.description("Set a lane's alive child-lane grant as its senior")
|
|
@@ -6823,6 +7147,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6823
7147
|
.option("--story <id>", "Re-point story attribution from this message")
|
|
6824
7148
|
.option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
|
|
6825
7149
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
7150
|
+
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
6826
7151
|
.option("--worker <profile>", "Attach a personal worker profile when this message resumes the lane")
|
|
6827
7152
|
.option("--no-compact", "Skip the resume-time compaction check for this message")
|
|
6828
7153
|
.action(wrap(async (laneId, opts) => {
|
|
@@ -6837,6 +7162,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6837
7162
|
.option("--story <id>", "Re-point story attribution from this resume")
|
|
6838
7163
|
.option("--account <id>", "Move the lane's session to this account of its provider before resuming")
|
|
6839
7164
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
7165
|
+
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
6840
7166
|
.option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
|
|
6841
7167
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
6842
7168
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|