@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +12 -0
  3. package/assets/bot-workers/setup/context/USER-GUIDE.md +4 -4
  4. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  7. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  8. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  9. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  10. package/dist/command-catalog.generated.d.ts +239 -1
  11. package/dist/command-catalog.generated.js +302 -1
  12. package/dist/commands/core-checkpoint.d.ts +4 -2
  13. package/dist/commands/core-checkpoint.js +34 -3
  14. package/dist/commands/core-command-catalog.d.ts +4 -2
  15. package/dist/commands/core-command-catalog.js +5 -2
  16. package/dist/commands/core-delegate.d.ts +6 -0
  17. package/dist/commands/core-delegate.js +987 -0
  18. package/dist/commands/core-ontology.js +3 -3
  19. package/dist/commands/core.js +11 -1
  20. package/dist/commands/daemon.d.ts +4 -0
  21. package/dist/commands/daemon.js +175 -2
  22. package/dist/commands/dm.d.ts +2 -0
  23. package/dist/commands/dm.js +11 -4
  24. package/dist/commands/feedback.d.ts +6 -0
  25. package/dist/commands/feedback.js +86 -37
  26. package/dist/commands/lanes-accounts.js +44 -2
  27. package/dist/commands/lanes-message.js +69 -12
  28. package/dist/commands/lanes-resume.js +2 -0
  29. package/dist/commands/lanes-shared.d.ts +3 -0
  30. package/dist/commands/lanes-shared.js +12 -2
  31. package/dist/commands/lanes.d.ts +5 -1
  32. package/dist/commands/lanes.js +402 -49
  33. package/dist/commands/run-feature-telemetry.d.ts +3 -0
  34. package/dist/commands/run-feature-telemetry.js +6 -0
  35. package/dist/commands/run.js +3 -2
  36. package/dist/commands/secrets.d.ts +16 -1
  37. package/dist/commands/secrets.js +80 -45
  38. package/dist/commands/sync-mode.d.ts +11 -0
  39. package/dist/commands/sync-mode.js +30 -0
  40. package/dist/commands/whoami.d.ts +4 -6
  41. package/dist/commands/whoami.js +39 -8
  42. package/dist/fast-core.d.ts +6 -7
  43. package/dist/fast-core.js +22 -8
  44. package/dist/fast-scaffold.d.ts +33 -7
  45. package/dist/fast-scaffold.js +104 -8
  46. package/dist/index.js +32 -1
  47. package/dist/lanes-drain-hook.js +154 -7
  48. package/dist/lib/agent-kit/run/inbox.js +3 -2
  49. package/dist/lib/billing/plan-lock.d.ts +3 -1
  50. package/dist/lib/billing/plan-lock.js +7 -1
  51. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  52. package/dist/lib/checkpoint-ontology/apply.js +60 -3
  53. package/dist/lib/checkpoint-ontology/company.d.ts +8 -4
  54. package/dist/lib/checkpoint-ontology/company.js +15 -7
  55. package/dist/lib/checkpoint-ontology/plan.d.ts +36 -0
  56. package/dist/lib/checkpoint-ontology/plan.js +66 -4
  57. package/dist/lib/checkpoint-ontology/queued-touches.d.ts +32 -0
  58. package/dist/lib/checkpoint-ontology/queued-touches.js +193 -0
  59. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  60. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  61. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  62. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  63. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  64. package/dist/lib/core-utils/secret-patterns.js +33 -0
  65. package/dist/lib/core-utils/timeout-guard-native.d.ts +7 -0
  66. package/dist/lib/core-utils/timeout-guard-native.js +40 -0
  67. package/dist/lib/core-utils/timeout-guard.d.ts +3 -3
  68. package/dist/lib/core-utils/timeout-guard.js +28 -13
  69. package/dist/lib/daemon/catalog.d.ts +18 -2
  70. package/dist/lib/daemon/catalog.js +18 -2
  71. package/dist/lib/daemon/config.d.ts +6 -0
  72. package/dist/lib/daemon/config.js +15 -2
  73. package/dist/lib/daemon/requests.d.ts +4 -0
  74. package/dist/lib/daemon/requests.js +2 -0
  75. package/dist/lib/daemon/run.js +94 -8
  76. package/dist/lib/daemon/supervisor.d.ts +8 -0
  77. package/dist/lib/daemon/supervisor.js +101 -1
  78. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  79. package/dist/lib/daemon/sync-runner.js +277 -0
  80. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  81. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  82. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  83. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  84. package/dist/lib/flag-registry.d.ts +12 -7
  85. package/dist/lib/flag-registry.js +66 -75
  86. package/dist/lib/flag-snapshot-file.d.ts +20 -0
  87. package/dist/lib/flag-snapshot-file.js +91 -0
  88. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  89. package/dist/lib/lanes/claude-balance.js +46 -8
  90. package/dist/lib/lanes/config.js +15 -0
  91. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  92. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  93. package/dist/lib/lanes/index.d.ts +1 -1
  94. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  95. package/dist/lib/lanes/keep-alive.js +3 -0
  96. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  97. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  98. package/dist/lib/lanes/reconcile.js +20 -4
  99. package/dist/lib/lanes/spawn.js +17 -10
  100. package/dist/lib/lanes/store.js +45 -1
  101. package/dist/lib/lanes/types.d.ts +30 -1
  102. package/dist/lib/lanes/types.js +2 -2
  103. package/dist/lib/lanes/watcher.js +3 -2
  104. package/dist/lib/process-flag-company-context.d.ts +8 -0
  105. package/dist/lib/process-flag-company-context.js +67 -0
  106. package/dist/main.js +3 -1
  107. package/dist/native-fast-route.d.ts +3 -0
  108. package/dist/native-fast-route.js +39 -0
  109. package/dist/secrets-env-fast-path.d.ts +5 -0
  110. package/dist/secrets-env-fast-path.js +16 -0
  111. package/dist/secrets-env-fast-route.d.ts +18 -0
  112. package/dist/secrets-env-fast-route.js +54 -0
  113. package/dist/utils/cli-telemetry.d.ts +26 -2
  114. package/dist/utils/cli-telemetry.js +178 -3
  115. package/dist/utils/hq-root-resolver.d.ts +13 -0
  116. package/dist/utils/hq-root-resolver.js +103 -0
  117. package/dist/utils/scaffold-fast-import-guard.js +6 -1
  118. package/dist/utils/vault-api.d.ts +8 -0
  119. package/dist/utils/vault-api.js +13 -1
  120. 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";
@@ -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 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;
455
458
  const hireGrant = lane.hire_grant ?? 0;
456
- const hireSummary = `hire ${aliveChildren.length}/${hireGrant}`;
459
+ const hireSummary = `hire ${aliveChildCount}/${hireGrant}`;
457
460
  return {
458
461
  hire_grant: hireGrant,
459
- hire_alive_children: aliveChildren.length,
460
- hire: `${aliveChildren.length}/${hireGrant}`,
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 hqBinary;
722
+ let dispatched;
687
723
  try {
688
- 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);
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
- async function readOutpostLaneRecords(ctx, outpostId) {
993
- 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) {
994
1028
  let result;
995
1029
  try {
996
- result = await ctx.dispatchOutpostExec({
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
- 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 });
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 now = ctx.now();
4060
- const host = reconcileHostFromCtx(ctx);
4061
- const allLanes = loadAllLanes(hqRoot);
4062
- 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
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 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, {
4138
4252
  nowMs: now.getTime(),
4139
- deps: livenessDepsFromCtx(ctx),
4253
+ deps: livenessDeps,
4140
4254
  presenceConnected: ctx.presenceConnected,
4141
4255
  });
4142
- return state === null || state === "ended" || state === "lost";
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")