@indigoai-us/hq-cli 5.276.0 → 5.278.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  3. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  4. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  7. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  8. package/dist/command-catalog.generated.d.ts +239 -1
  9. package/dist/command-catalog.generated.js +302 -1
  10. package/dist/commands/core-delegate.d.ts +6 -0
  11. package/dist/commands/core-delegate.js +987 -0
  12. package/dist/commands/core.js +10 -0
  13. package/dist/commands/daemon.d.ts +3 -0
  14. package/dist/commands/daemon.js +173 -1
  15. package/dist/commands/dm.d.ts +2 -0
  16. package/dist/commands/dm.js +11 -4
  17. package/dist/commands/lanes-accounts.js +44 -2
  18. package/dist/commands/lanes-message.js +69 -12
  19. package/dist/commands/lanes-resume.js +2 -0
  20. package/dist/commands/lanes-shared.d.ts +3 -0
  21. package/dist/commands/lanes-shared.js +12 -2
  22. package/dist/commands/lanes.d.ts +5 -1
  23. package/dist/commands/lanes.js +399 -47
  24. package/dist/commands/secrets.d.ts +16 -1
  25. package/dist/commands/secrets.js +77 -48
  26. package/dist/commands/sync-mode.d.ts +11 -0
  27. package/dist/commands/sync-mode.js +30 -0
  28. package/dist/fast-scaffold.d.ts +17 -0
  29. package/dist/fast-scaffold.js +91 -0
  30. package/dist/index.js +17 -1
  31. package/dist/lanes-drain-hook.js +154 -7
  32. package/dist/lib/agent-kit/run/inbox.js +3 -2
  33. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  34. package/dist/lib/checkpoint-ontology/apply.js +111 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +23 -0
  37. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  38. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  39. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  40. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  41. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  42. package/dist/lib/core-utils/secret-patterns.js +33 -0
  43. package/dist/lib/daemon/catalog.d.ts +18 -2
  44. package/dist/lib/daemon/catalog.js +18 -2
  45. package/dist/lib/daemon/config.d.ts +6 -0
  46. package/dist/lib/daemon/config.js +15 -2
  47. package/dist/lib/daemon/requests.d.ts +4 -0
  48. package/dist/lib/daemon/requests.js +2 -0
  49. package/dist/lib/daemon/run.js +94 -8
  50. package/dist/lib/daemon/supervisor.d.ts +8 -0
  51. package/dist/lib/daemon/supervisor.js +101 -1
  52. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  53. package/dist/lib/daemon/sync-runner.js +277 -0
  54. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  55. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  59. package/dist/lib/lanes/claude-balance.js +46 -8
  60. package/dist/lib/lanes/config.d.ts +2 -1
  61. package/dist/lib/lanes/config.js +31 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  63. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  64. package/dist/lib/lanes/index.d.ts +1 -1
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  66. package/dist/lib/lanes/keep-alive.js +3 -0
  67. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  68. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  69. package/dist/lib/lanes/reconcile.d.ts +9 -0
  70. package/dist/lib/lanes/reconcile.js +127 -2
  71. package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
  72. package/dist/lib/lanes/runaway-tool-guard.js +284 -0
  73. package/dist/lib/lanes/spawn.js +17 -10
  74. package/dist/lib/lanes/store.js +45 -1
  75. package/dist/lib/lanes/types.d.ts +37 -1
  76. package/dist/lib/lanes/types.js +2 -2
  77. package/dist/lib/lanes/watcher.d.ts +2 -0
  78. package/dist/lib/lanes/watcher.js +13 -2
  79. package/dist/secrets-env-fast-path.d.ts +5 -0
  80. package/dist/secrets-env-fast-path.js +16 -0
  81. package/dist/secrets-env-fast-route.d.ts +18 -0
  82. package/dist/secrets-env-fast-route.js +54 -0
  83. package/package.json +2 -2
@@ -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";
@@ -31,7 +31,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
31
31
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
32
32
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
33
33
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
34
- import { createOutpostHireGrantContext, parseOutpostHireGrantContext, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
34
+ import { createOutpostHireGrantContext, parseOutpostHireGrantContext, selectOutpostHireGrantHierarchy, sameHireGrantSenior, } from "../lib/lanes/hire-grant-dispatch.js";
35
35
  import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } from "../lib/lanes/outpost-profile-repin.js";
36
36
  import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
37
37
  import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
@@ -51,7 +51,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
51
51
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
52
52
  import { listLaneReviews } from "../lib/lanes/reviews.js";
53
53
  import { effectiveAccountHome } from "../lib/lanes/account-home.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, 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";
55
55
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
56
56
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
57
57
  import { resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -451,14 +451,16 @@ function readQuestionBody(hqRoot, question) {
451
451
  function loadAllLanes(hqRoot) {
452
452
  return listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
453
453
  }
454
- function laneHireFields(lane, lanes) {
455
- const aliveChildren = aliveDirectChildren(lanes, lane.lane_id);
454
+ function laneHireFields(lane, lanes, aliveChildrenByParent) {
455
+ const aliveChildCount = aliveChildrenByParent
456
+ ? aliveChildrenByParent.get(lane.lane_id) ?? 0
457
+ : aliveDirectChildren(lanes, lane.lane_id).length;
456
458
  const hireGrant = lane.hire_grant ?? 0;
457
- const hireSummary = `hire ${aliveChildren.length}/${hireGrant}`;
459
+ const hireSummary = `hire ${aliveChildCount}/${hireGrant}`;
458
460
  return {
459
461
  hire_grant: hireGrant,
460
- hire_alive_children: aliveChildren.length,
461
- hire: `${aliveChildren.length}/${hireGrant}`,
462
+ hire_alive_children: aliveChildCount,
463
+ hire: `${aliveChildCount}/${hireGrant}`,
462
464
  hire_summary: hireSummary,
463
465
  };
464
466
  }
@@ -676,6 +678,39 @@ function validateOwnerLaneHireSummary(input) {
676
678
  hire_summary: `hire ${alive}/${grant}`,
677
679
  };
678
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
+ }
679
714
  async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
680
715
  const fallback = (error) => new Map(lanes.map((lane) => [lane.lane_id, {
681
716
  error: new LanesError(error.error, error.message, {
@@ -684,28 +719,15 @@ async function fetchOutpostListHireSummaries(ctx, outpostId, lanes) {
684
719
  lane_id: lane.lane_id,
685
720
  }),
686
721
  }]));
687
- let hqBinary;
722
+ let dispatched;
688
723
  try {
689
- hqBinary = outpostCliOverride(ctx, outpostId);
724
+ dispatched = await dispatchOutpostLaneList(ctx, outpostId, { laneIds: lanes.map((lane) => lane.lane_id) }, OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS, OUTPOST_HIRE_SUMMARY_WAIT_MS);
690
725
  }
691
726
  catch (err) {
692
727
  if (isLanesError(err))
693
728
  return fallback(err);
694
729
  return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId }));
695
730
  }
696
- let dispatched;
697
- try {
698
- // No filters: the owner list must return every lane requested by this host group.
699
- dispatched = await ctx.dispatchOutpostExec({
700
- outpostId,
701
- command: buildOutpostLaneListCommand({ hqBinary }),
702
- timeoutSeconds: OUTPOST_HIRE_SUMMARY_TIMEOUT_SECONDS,
703
- waitMs: OUTPOST_HIRE_SUMMARY_WAIT_MS,
704
- });
705
- }
706
- catch (err) {
707
- return fallback(new LanesError("outpost_lane_hire_summary_unavailable", `Could not read hire summaries from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId }));
708
- }
709
731
  if (dispatched.done !== true || dispatched.status !== "Success" || dispatched.exitCode !== 0) {
710
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 }));
711
733
  }
@@ -990,18 +1012,26 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
990
1012
  skippedHosts: unavailable.map(({ outpost_id, error }) => ({ outpost_id, error })),
991
1013
  };
992
1014
  }
993
- async function readOutpostLaneRecords(ctx, outpostId) {
994
- const hqBinary = outpostCliOverride(ctx, outpostId);
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) {
995
1028
  let result;
996
1029
  try {
997
- result = await ctx.dispatchOutpostExec({
998
- outpostId,
999
- command: buildOutpostLaneListCommand({ hqBinary }),
1000
- timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
1001
- waitMs: CAPACITY_PROBE_TIMEOUT_MS,
1002
- });
1030
+ result = await dispatchOutpostLaneList(ctx, outpostId, query, CAPACITY_PROBE_TIMEOUT_MS / 1000, CAPACITY_PROBE_TIMEOUT_MS);
1003
1031
  }
1004
1032
  catch (err) {
1033
+ if (isLanesError(err))
1034
+ throw err;
1005
1035
  throw new LanesError("outpost_lane_list_unavailable", `Could not read lanes from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
1006
1036
  }
1007
1037
  if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
@@ -1057,6 +1087,25 @@ async function readOutpostLaneRecords(ctx, outpostId) {
1057
1087
  }
1058
1088
  return lanes;
1059
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
+ }
1060
1109
  async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1061
1110
  try {
1062
1111
  return { lane: readLane(ctx.hqRoot(), laneId), remoteOnly: false };
@@ -1099,7 +1148,10 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1099
1148
  for (const outpostId of lookupOutpostIds) {
1100
1149
  let ownerLanes;
1101
1150
  try {
1102
- ownerLanes = await readOutpostLaneRecords(ctx, outpostId);
1151
+ ownerLanes = await readOutpostLaneRecords(ctx, outpostId, {
1152
+ hierarchy: laneId,
1153
+ snapshot: true,
1154
+ });
1103
1155
  }
1104
1156
  catch (err) {
1105
1157
  if (requestedOutpostId !== undefined)
@@ -1215,7 +1267,9 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
1215
1267
  let lanes = [];
1216
1268
  let laneListAvailable = false;
1217
1269
  try {
1218
- lanes = await readOutpostLaneRecords(ctx, id);
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 });
1219
1273
  laneListAvailable = true;
1220
1274
  }
1221
1275
  catch (err) {
@@ -2723,6 +2777,8 @@ async function runCreate(ctx, opts) {
2723
2777
  hosts: balanceHostLoad.snapshots,
2724
2778
  targetHost,
2725
2779
  config,
2780
+ recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
2781
+ now: ctx.now(),
2726
2782
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
2727
2783
  ...(explicitBalanceAccount !== undefined ? { explicitAccount: explicitBalanceAccount } : {}),
2728
2784
  unavailableHosts: balanceHostLoad.unavailableHosts,
@@ -2768,6 +2824,8 @@ async function runCreate(ctx, opts) {
2768
2824
  hosts,
2769
2825
  targetHost: balanceTargetHost,
2770
2826
  config,
2827
+ recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, company ?? undefined),
2828
+ now: ctx.now(),
2771
2829
  ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
2772
2830
  unavailableHosts: balanceHostLoad.unavailableHosts,
2773
2831
  });
@@ -3504,13 +3562,19 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
3504
3562
  let grantContext;
3505
3563
  let grantHierarchy;
3506
3564
  if (action === "grant") {
3507
- 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
+ }));
3508
3569
  grantHierarchy = hierarchy;
3509
3570
  grantContext = createOutpostHireGrantContext(lane, hierarchy);
3510
3571
  }
3511
3572
  else if ((action === "resume" && lane.senior.kind === "lane") ||
3512
3573
  (action === "message" && (lane.senior.kind === "lane" || lane.senior.kind === "session"))) {
3513
- 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
+ }));
3514
3578
  grantContext = createOutpostHireGrantContext(lane, hierarchy, action === "message" ? options.supersededMessageCallerId : undefined, action === "message" ? options.supersededMessageSessionId : undefined);
3515
3579
  }
3516
3580
  let dispatched;
@@ -4050,6 +4114,21 @@ async function runList(ctx, opts) {
4050
4114
  if (opts.json && opts.human) {
4051
4115
  throw new LanesError("conflicting_output_format", "Use either --json or --human for hq lanes list", { json: true, human: true });
4052
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
+ }
4053
4132
  const hqRoot = ctx.hqRoot();
4054
4133
  const state = opts.state ? parseLaneState(opts.state) : undefined;
4055
4134
  const senior = opts.senior ? parseSeniorRef(opts.senior) : undefined;
@@ -4057,11 +4136,25 @@ async function runList(ctx, opts) {
4057
4136
  if (opts.company) {
4058
4137
  company = await ctx.resolveCompany(opts.company);
4059
4138
  }
4060
- const now = ctx.now();
4061
- const host = reconcileHostFromCtx(ctx);
4062
- const allLanes = loadAllLanes(hqRoot);
4063
- const selectedLanes = allLanes
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
4064
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;
4065
4158
  if (state && lane.state !== state)
4066
4159
  return false;
4067
4160
  const carriesFilter = laneStories(lane).some((story) => (!opts.project || story.project_id === opts.project) &&
@@ -4079,6 +4172,19 @@ async function runList(ctx, opts) {
4079
4172
  return true;
4080
4173
  })
4081
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
+ }
4082
4188
  const ownerEligibleLaneIds = new Set();
4083
4189
  for (const lane of selectedLanes) {
4084
4190
  if (lane.host.kind !== "outpost" ||
@@ -4093,7 +4199,7 @@ async function runList(ctx, opts) {
4093
4199
  for (const lane of selectedLanes) {
4094
4200
  if (!ownerEligibleLaneIds.has(lane.lane_id)) {
4095
4201
  hireByLane.set(lane.lane_id, {
4096
- ...laneHireFields(lane, allLanes),
4202
+ ...laneHireFields(lane, allLanes, aliveChildrenByParent),
4097
4203
  hire_source: "local",
4098
4204
  });
4099
4205
  continue;
@@ -4111,12 +4217,14 @@ async function runList(ctx, opts) {
4111
4217
  ? outcome.error
4112
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 });
4113
4219
  hireByLane.set(lane.lane_id, {
4114
- ...laneHireFields(lane, allLanes),
4220
+ ...laneHireFields(lane, allLanes, aliveChildrenByParent),
4115
4221
  hire_source: "local",
4116
4222
  hire_warning: `${error.error}: ${error.message}`,
4117
4223
  });
4118
4224
  }
4119
4225
  }
4226
+ const livenessBySession = new Map();
4227
+ let livenessDeps;
4120
4228
  const lanes = selectedLanes.map((lane) => {
4121
4229
  const remoteQueued = lane.state === "queued" &&
4122
4230
  lane.host.kind === "outpost" &&
@@ -4135,12 +4243,19 @@ async function runList(ctx, opts) {
4135
4243
  // Listing is observational. Reconcile owns durable liveness transitions.
4136
4244
  senior_lost: lane.state === "running" && lane.senior.kind === "session" && lane.senior.id
4137
4245
  ? (() => {
4138
- const state = sessionLiveness(hqRoot, lane.senior.id, {
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, {
4139
4252
  nowMs: now.getTime(),
4140
- deps: livenessDepsFromCtx(ctx),
4253
+ deps: livenessDeps,
4141
4254
  presenceConnected: ctx.presenceConnected,
4142
4255
  });
4143
- return state === null || state === "ended" || state === "lost";
4256
+ const lost = state === null || state === "ended" || state === "lost";
4257
+ livenessBySession.set(sessionId, lost);
4258
+ return lost;
4144
4259
  })()
4145
4260
  : false,
4146
4261
  };
@@ -4414,6 +4529,7 @@ async function runShow(ctx, laneId) {
4414
4529
  model: lane.model ?? null,
4415
4530
  effort: lane.effort ?? null,
4416
4531
  effort_source: lane.effort_source ?? null,
4532
+ settings_history: lane.settings_history ?? [],
4417
4533
  terminal_reason: lane.terminal_reason ?? null,
4418
4534
  terminal_details: lane.terminal_details ?? null,
4419
4535
  keep_alive_state: {
@@ -4645,6 +4761,107 @@ function printGrantResult(hqRoot, updated, hierarchy) {
4645
4761
  const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot));
4646
4762
  printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
4647
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
+ }
4648
4865
  async function runKeepAlive(ctx, laneId, opts) {
4649
4866
  if (opts.standing && !opts.on) {
4650
4867
  throw new LanesError("keep_alive_standing_requires_on", "Pass --standing together with --on", { lane_id: laneId });
@@ -5160,7 +5377,8 @@ function recordResumeSetup(ctx, lane, input) {
5160
5377
  input.effort === undefined &&
5161
5378
  input.keepAlive === undefined &&
5162
5379
  input.cliModel === undefined &&
5163
- input.meshCompany === undefined)
5380
+ input.meshCompany === undefined &&
5381
+ lane.keep_alive_blocked_reason !== "terminal_state")
5164
5382
  return lane;
5165
5383
  return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
5166
5384
  if (current.version !== lane.version) {
@@ -5170,6 +5388,10 @@ function recordResumeSetup(ctx, lane, input) {
5170
5388
  assertResumeProfileRepinStillAuthorized(ctx, current, input.profileRepin);
5171
5389
  }
5172
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;
5173
5395
  applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
5174
5396
  if (input.cliModel !== undefined) {
5175
5397
  next.model = input.cliModel.trim();
@@ -5450,6 +5672,8 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, opti
5450
5672
  hosts: balanceHosts.snapshots,
5451
5673
  targetHost: lane.host,
5452
5674
  config,
5675
+ recentUsageEnabled: await readClaudeAccountBalanceSignalsEnabled(ctx, lane.company ?? undefined),
5676
+ now: ctx.now(),
5453
5677
  ...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
5454
5678
  ...(model !== undefined ? { model } : {}),
5455
5679
  ...(recordedModelAccount !== undefined ? { recordedModelAccount } : {}),
@@ -6344,6 +6568,8 @@ async function dispatchOutpostLaneStop(ctx, lane, reason, quiet = false) {
6344
6568
  ok: true,
6345
6569
  action: "stop",
6346
6570
  lane: updated,
6571
+ terminal: true,
6572
+ resumable: false,
6347
6573
  cancelled_questions: cancelledQuestions,
6348
6574
  outpost_id: outpostId,
6349
6575
  outpost_command_id: commandId,
@@ -6365,6 +6591,9 @@ async function runStop(ctx, laneId, reason, quiet = false) {
6365
6591
  assertNoSecretText("stop reason", operatorReason);
6366
6592
  const hqRoot = ctx.hqRoot();
6367
6593
  const lane = readLane(hqRoot, laneId);
6594
+ if (!quiet && lane.state !== "queued" && !isTerminalLaneState(lane.state)) {
6595
+ process.stderr.write(stopTerminalWarning(laneId));
6596
+ }
6368
6597
  if (lane.host.kind === "outpost" && !laneRunsHereForOperatorMutation(ctx, lane)) {
6369
6598
  await dispatchOutpostLaneStop(ctx, lane, operatorReason, quiet);
6370
6599
  return;
@@ -6409,9 +6638,108 @@ async function runStop(ctx, laneId, reason, quiet = false) {
6409
6638
  ok: true,
6410
6639
  action: "stop",
6411
6640
  lane: updated,
6641
+ terminal: true,
6642
+ resumable: false,
6412
6643
  cancelled_questions: cancelled.map((q) => q.question_id),
6413
6644
  });
6414
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
+ }
6415
6743
  async function runAsk(ctx, opts) {
6416
6744
  const hqRoot = ctx.hqRoot();
6417
6745
  const laneId = ctx.env.HQ_LANE_ID?.trim();
@@ -6740,6 +7068,7 @@ export function registerLanesCommand(parent, deps = {}) {
6740
7068
  parseRemoteJson,
6741
7069
  prepareAccountMove,
6742
7070
  printJson,
7071
+ readClaudeAccountBalanceSignalsEnabled,
6743
7072
  reconcileHostFromCtx,
6744
7073
  resolveCallerPath,
6745
7074
  });
@@ -6948,6 +7277,17 @@ export function registerLanesCommand(parent, deps = {}) {
6948
7277
  .action(wrap(async (laneId, opts) => {
6949
7278
  await runKeepAlive(ctx, laneId, opts);
6950
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
+ }));
6951
7291
  lanes
6952
7292
  .command("grant")
6953
7293
  .description("Set a lane's alive child-lane grant as its senior")
@@ -6973,6 +7313,10 @@ export function registerLanesCommand(parent, deps = {}) {
6973
7313
  .option("--story <id>", "Filter by story")
6974
7314
  .option("--state <state>", `Filter by state (${LANE_STATES.join("|")})`)
6975
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())
6976
7320
  .option("--json", "Emit JSON on stdout (default)")
6977
7321
  .option("--human", "Emit human-readable output")
6978
7322
  .action(wrap(async (opts) => {
@@ -7237,12 +7581,20 @@ export function registerLanesCommand(parent, deps = {}) {
7237
7581
  }));
7238
7582
  lanes
7239
7583
  .command("stop")
7240
- .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)")
7241
7585
  .argument("<lane-id>", "Lane id")
7242
7586
  .option("--reason <text>", "Record the reason in the lane's terminal history")
7243
7587
  .action(wrap(async (laneId, options) => {
7244
7588
  await runStop(ctx, laneId, options.reason);
7245
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
+ }));
7246
7598
  lanes
7247
7599
  .command("approve")
7248
7600
  .description("Record senior approval for a registry worker checkpoint")