@indigoai-us/hq-cli 5.276.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 (77) hide show
  1. package/CHANGELOG.md +76 -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 +60 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +13 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +13 -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.js +15 -0
  61. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  63. package/dist/lib/lanes/index.d.ts +1 -1
  64. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  65. package/dist/lib/lanes/keep-alive.js +3 -0
  66. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  67. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  68. package/dist/lib/lanes/spawn.js +17 -10
  69. package/dist/lib/lanes/store.js +45 -1
  70. package/dist/lib/lanes/types.d.ts +30 -1
  71. package/dist/lib/lanes/types.js +2 -2
  72. package/dist/lib/lanes/watcher.js +3 -2
  73. package/dist/secrets-env-fast-path.d.ts +5 -0
  74. package/dist/secrets-env-fast-path.js +16 -0
  75. package/dist/secrets-env-fast-route.d.ts +18 -0
  76. package/dist/secrets-env-fast-route.js +54 -0
  77. package/package.json +2 -2
@@ -3,6 +3,7 @@ import type { HostCapacityAccount, HostCapacitySnapshot } from "./host-capacity.
3
3
  import type { LaneHost, LaneRecord, LanesConfig } from "./types.js";
4
4
  export declare const BALANCE_LIVE_STATES: readonly ["running", "queued", "awaiting_input"];
5
5
  export declare const BALANCE_MOVABLE_STATES: readonly ["interrupted", "done", "awaiting_input"];
6
+ export declare const CLAUDE_RECENT_USAGE_WINDOW_MS: number;
6
7
  export interface BalanceHostSnapshot {
7
8
  capacity: HostCapacitySnapshot;
8
9
  lanes: readonly LaneRecord[];
@@ -39,11 +40,12 @@ export interface ClaudeAccountLoad {
39
40
  live_lanes: number;
40
41
  counts_complete: boolean;
41
42
  usage: NonNullable<HostCapacityAccount["usage"]> | null;
43
+ recent_usage_at: string | null;
42
44
  hosts: AccountHostLoad[];
43
45
  }
44
46
  export interface ClaudeAccountSelection {
45
47
  account: string;
46
- signal: "usage" | "lane_count" | "explicit";
48
+ signal: "usage" | "recent_usage" | "lane_count" | "explicit";
47
49
  reason: "least_loaded" | "explicit";
48
50
  target_host: LaneHost;
49
51
  considered: ClaudeAccountLoad[];
@@ -72,6 +74,9 @@ export declare function selectClaudeAccount(input: {
72
74
  model?: string;
73
75
  /** Keep the recorded account eligible for its already-running CLI model. */
74
76
  recordedModelAccount?: string;
77
+ /** Recent completed-run tiebreak; callers pass the default-off hq-flags result. */
78
+ recentUsageEnabled?: boolean;
79
+ now?: Date;
75
80
  explicitAccount?: string;
76
81
  unavailableHosts?: readonly BalanceHostGap[];
77
82
  }): ClaudeAccountSelection;
@@ -1,6 +1,7 @@
1
1
  import { LanesError } from "./errors.js";
2
2
  export const BALANCE_LIVE_STATES = ["running", "queued", "awaiting_input"];
3
3
  export const BALANCE_MOVABLE_STATES = ["interrupted", "done", "awaiting_input"];
4
+ export const CLAUDE_RECENT_USAGE_WINDOW_MS = 5 * 60 * 60 * 1000;
4
5
  function sameHost(a, b) {
5
6
  return a.kind === b.kind && a.id === b.id;
6
7
  }
@@ -74,10 +75,31 @@ function usageScore(usage) {
74
75
  return null;
75
76
  return Math.max(usage.five_hour_percent, usage.weekly_percent);
76
77
  }
77
- function selectionSignal(loads) {
78
- return loads.length > 0 && loads.every((load) => usageScore(load.usage) !== null)
79
- ? "usage"
80
- : "lane_count";
78
+ function recentUsageAt(hosts, account, now) {
79
+ if (hosts.some((host) => !host.laneListAvailable))
80
+ return null;
81
+ const nowMs = now.getTime();
82
+ const cutoff = nowMs - CLAUDE_RECENT_USAGE_WINDOW_MS;
83
+ let latestAt = null;
84
+ let latestMs = cutoff - 1;
85
+ for (const host of hosts) {
86
+ for (const lane of host.lanes) {
87
+ if (lane.provider !== "claude" || lane.account !== account ||
88
+ lane.last_run_usage === undefined || lane.last_run_usage_at === undefined)
89
+ continue;
90
+ const atMs = Date.parse(lane.last_run_usage_at);
91
+ if (!Number.isFinite(atMs) || atMs < cutoff || atMs > nowMs || atMs <= latestMs)
92
+ continue;
93
+ latestMs = atMs;
94
+ latestAt = lane.last_run_usage_at;
95
+ }
96
+ }
97
+ return latestAt;
98
+ }
99
+ function selectionSignal(loads, recentUsageEnabled, laneSnapshotsComplete) {
100
+ if (loads.length > 0 && loads.every((load) => usageScore(load.usage) !== null))
101
+ return "usage";
102
+ return recentUsageEnabled && laneSnapshotsComplete ? "recent_usage" : "lane_count";
81
103
  }
82
104
  function targetIneligibilityReason(accountId, globalEnabled, allowedAccounts, target) {
83
105
  if (!globalEnabled)
@@ -120,6 +142,7 @@ function buildLoads(input) {
120
142
  live_lanes: knownCounts.reduce((sum, count) => sum + count, 0),
121
143
  counts_complete: hosts.every((host) => host.live_lanes !== null && host.count_source !== "capacity_partial"),
122
144
  usage: input.target.capacity.providers.claude.accounts[account]?.usage ?? null,
145
+ recent_usage_at: recentUsageAt(input.hosts, account, input.now),
123
146
  hosts,
124
147
  };
125
148
  });
@@ -133,7 +156,8 @@ export function selectClaudeAccount(input) {
133
156
  unavailable_hosts: input.unavailableHosts ?? [],
134
157
  });
135
158
  }
136
- const { loads, skipped } = buildLoads({ ...input, target });
159
+ const now = input.now ?? new Date();
160
+ const { loads, skipped } = buildLoads({ ...input, target, now });
137
161
  if (input.explicitAccount !== undefined) {
138
162
  const selected = loads.find((load) => load.account === input.explicitAccount);
139
163
  if (!selected || !selected.eligible) {
@@ -173,14 +197,28 @@ export function selectClaudeAccount(input) {
173
197
  unavailable_hosts: input.unavailableHosts ?? [],
174
198
  });
175
199
  }
176
- const signal = selectionSignal(eligible);
200
+ const signal = selectionSignal(eligible, input.recentUsageEnabled === true, input.hosts.every((host) => host.laneListAvailable) && (input.unavailableHosts?.length ?? 0) === 0);
177
201
  eligible.sort((a, b) => {
178
202
  if (signal === "usage") {
179
203
  const byUsage = usageScore(a.usage) - usageScore(b.usage);
180
204
  if (byUsage !== 0)
181
205
  return byUsage;
182
206
  }
183
- return a.live_lanes - b.live_lanes || a.account.localeCompare(b.account);
207
+ const byLiveLanes = a.live_lanes - b.live_lanes;
208
+ if (byLiveLanes !== 0)
209
+ return byLiveLanes;
210
+ if (signal === "recent_usage") {
211
+ if (a.recent_usage_at === null && b.recent_usage_at !== null)
212
+ return -1;
213
+ if (a.recent_usage_at !== null && b.recent_usage_at === null)
214
+ return 1;
215
+ if (a.recent_usage_at !== null && b.recent_usage_at !== null) {
216
+ const byRecentUsage = Date.parse(a.recent_usage_at) - Date.parse(b.recent_usage_at);
217
+ if (byRecentUsage !== 0)
218
+ return byRecentUsage;
219
+ }
220
+ }
221
+ return a.account.localeCompare(b.account);
184
222
  });
185
223
  return {
186
224
  account: eligible[0].account,
@@ -196,7 +234,7 @@ export function accountLoadsForReport(input) {
196
234
  const first = input.hosts[0];
197
235
  if (!first)
198
236
  return [];
199
- return buildLoads({ hosts: input.hosts, target: first, config: input.config }).loads.map((load) => ({
237
+ return buildLoads({ hosts: input.hosts, target: first, config: input.config, now: new Date() }).loads.map((load) => ({
200
238
  ...load,
201
239
  eligible: false,
202
240
  ineligible_reason: undefined,
@@ -252,6 +252,21 @@ function parsePreferredAccounts(provider, value, accounts) {
252
252
  return [...preferred];
253
253
  }
254
254
  export function assertUniqueAccountHomes(provider, accounts) {
255
+ // An override must not let one account reuse another account's shared default home.
256
+ const declaredHomes = new Map();
257
+ for (const [id, account] of Object.entries(accounts)) {
258
+ const homes = new Set([
259
+ account.home,
260
+ ...Object.values(account.hosts ?? {}).map(({ home }) => home),
261
+ ]);
262
+ for (const home of homes) {
263
+ const prior = declaredHomes.get(home);
264
+ if (prior !== undefined && prior !== id) {
265
+ throw new LanesConfigError("duplicate_account_home", `providers.${provider} accounts ${JSON.stringify(prior)} and ${JSON.stringify(id)} declare the same home ${home}`, { provider, accounts: [prior, id], home });
266
+ }
267
+ declaredHomes.set(home, id);
268
+ }
269
+ }
255
270
  const hostnames = new Set(Object.values(accounts).flatMap((account) => Object.keys(account.hosts ?? {})));
256
271
  const check = (hostname) => {
257
272
  const seen = new Map();
@@ -18,6 +18,8 @@ export interface OutpostHireGrantContext {
18
18
  superseded_message_caller_id?: string;
19
19
  superseded_message_session_id?: string;
20
20
  }
21
+ /** Select the owner records needed to reproduce the primary's grant hierarchy. */
22
+ export declare function selectOutpostHireGrantHierarchy(targetLaneId: string, lanes: readonly LaneRecord[]): LaneRecord[];
21
23
  /** Encode only the owner-relevant hierarchy from the primary's complete store. */
22
24
  export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[], supersededMessageCallerId?: string, supersededMessageSessionId?: string): string;
23
25
  /** Decode and validate an owner grant context, binding it to the target lane. */
@@ -15,6 +15,47 @@ function copySenior(senior) {
15
15
  ? { kind: senior.kind }
16
16
  : { kind: senior.kind, id: senior.id };
17
17
  }
18
+ // Terminal relatives are included so the owner's final state overwrites a stale
19
+ // primary copy; the context builder counts only alive children afterwards.
20
+ function directChildren(lanes, parentLaneId) {
21
+ return lanes.filter((lane) => lane.senior.kind === "lane" && lane.senior.id === parentLaneId);
22
+ }
23
+ /** Select the owner records needed to reproduce the primary's grant hierarchy. */
24
+ export function selectOutpostHireGrantHierarchy(targetLaneId, lanes) {
25
+ const current = lanes.find((lane) => lane.lane_id === targetLaneId);
26
+ if (!current)
27
+ return [];
28
+ const relevant = new Set([current.lane_id]);
29
+ if (current.senior.kind === "lane" && current.senior.id) {
30
+ relevant.add(current.senior.id);
31
+ for (const sibling of directChildren(lanes, current.senior.id)) {
32
+ relevant.add(sibling.lane_id);
33
+ }
34
+ }
35
+ for (const child of directChildren(lanes, current.lane_id)) {
36
+ relevant.add(child.lane_id);
37
+ }
38
+ const companyUid = current.company?.uid;
39
+ const projectId = current.project_id;
40
+ if (companyUid && projectId) {
41
+ // A superseded-message caller must be checked against the same project and
42
+ // company rows that the complete hierarchy builder sees.
43
+ for (const candidate of lanes) {
44
+ if (candidate.company?.uid === companyUid && candidate.project_id === projectId) {
45
+ relevant.add(candidate.lane_id);
46
+ }
47
+ }
48
+ }
49
+ if (current.senior.kind === "session" && companyUid) {
50
+ for (const candidate of lanes) {
51
+ if (candidate.company?.uid === companyUid && ((current.worker_profile !== undefined && candidate.worker_profile === current.worker_profile) ||
52
+ (projectId !== undefined && candidate.project_id === projectId && (candidate.hire_grant ?? 0) > 0))) {
53
+ relevant.add(candidate.lane_id);
54
+ }
55
+ }
56
+ }
57
+ return lanes.filter((lane) => relevant.has(lane.lane_id));
58
+ }
18
59
  function snapshot(lane) {
19
60
  return {
20
61
  lane_id: lane.lane_id,
@@ -10,7 +10,7 @@ export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
10
10
  export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
11
11
  export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, HostPlacementChoice, } from "./host-capacity.js";
12
12
  export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
13
- export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
13
+ export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, LaneSettingsHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
14
14
  export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
15
15
  export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
16
16
  export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
@@ -4,6 +4,9 @@ export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
4
4
  /** Missing or unreadable registry values retain the legacy supersession behavior. */
5
5
  export declare const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
6
6
  export declare const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
7
+ /** Missing or unreadable registry values keep rejecting superseded messages. */
8
+ export declare const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG = "lanes.running-superseded-message-delivery";
9
+ export declare const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_DEFAULT_ENABLED = false;
7
10
  export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
8
11
  export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
9
12
  export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
@@ -8,6 +8,9 @@ export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
8
8
  /** Missing or unreadable registry values retain the legacy supersession behavior. */
9
9
  export const LANE_SUPERSESSION_LINEAGE_AWARE_FLAG = "lanes.supersession-lineage-aware";
10
10
  export const LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED = false;
11
+ /** Missing or unreadable registry values keep rejecting superseded messages. */
12
+ export const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG = "lanes.running-superseded-message-delivery";
13
+ export const LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_DEFAULT_ENABLED = false;
11
14
  export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
12
15
  export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
13
16
  export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
@@ -82,6 +82,10 @@ export declare function buildOutpostCapacityCommand(): string;
82
82
  /** Read lane records owned by an Outpost, using its configured HQ binary when set. */
83
83
  export declare function buildOutpostLaneListCommand(input?: {
84
84
  hqBinary?: string;
85
+ laneIds?: readonly string[];
86
+ active?: boolean;
87
+ hierarchy?: string;
88
+ snapshot?: boolean;
85
89
  }): string;
86
90
  /** Read questions on one Outpost, scoped to a company when supplied. */
87
91
  export declare function buildOutpostQuestionListCommand(input?: {
@@ -67,12 +67,21 @@ export function buildOutpostCapacityCommand() {
67
67
  }
68
68
  /** Read lane records owned by an Outpost, using its configured HQ binary when set. */
69
69
  export function buildOutpostLaneListCommand(input = {}) {
70
- return withRemoteHqDir(joinCommandParts([
70
+ const args = [
71
71
  input.hqBinary?.trim() || "hq",
72
72
  "lanes",
73
73
  "list",
74
74
  "--json",
75
- ]));
75
+ ];
76
+ for (const laneId of input.laneIds ?? [])
77
+ args.push("--lane", laneId);
78
+ if (input.active)
79
+ args.push("--active");
80
+ if (input.hierarchy !== undefined)
81
+ args.push("--hierarchy", input.hierarchy);
82
+ if (input.snapshot)
83
+ args.push("--snapshot");
84
+ return withRemoteHqDir(joinCommandParts(args));
76
85
  }
77
86
  /** Read questions on one Outpost, scoped to a company when supplied. */
78
87
  export function buildOutpostQuestionListCommand(input = {}) {
@@ -1726,9 +1726,11 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1726
1726
  const recordedModel = repairRecordedAccount && existing.model_source === "account"
1727
1727
  ? existing.model
1728
1728
  : undefined;
1729
- let pinnedModel = existing.model_source === undefined || existing.model_source === "cli" || existing.model_source === "preserved"
1729
+ // `hq lanes set` on a running lane leaves the change pending until this spawn.
1730
+ const pendingSettings = existing.pending_settings;
1731
+ let pinnedModel = pendingSettings?.model ?? (existing.model_source === undefined || existing.model_source === "cli" || existing.model_source === "preserved"
1730
1732
  ? existing.model
1731
- : undefined;
1733
+ : undefined);
1732
1734
  const accountDeps = {
1733
1735
  probe: modelDeps.probe ?? defaultProbe,
1734
1736
  env,
@@ -1860,13 +1862,15 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1860
1862
  providerModel: config.providers[lane.provider].model,
1861
1863
  });
1862
1864
  const pendingResumeEffort = pendingResume !== undefined
1863
- ? {
1864
- ...(existing.effort !== undefined ? { effort: existing.effort } : {}),
1865
- source: existing.effort_source ?? "none",
1866
- }
1865
+ ? pendingSettings?.effort !== undefined
1866
+ ? { effort: pendingSettings.effort, source: "cli" }
1867
+ : {
1868
+ ...(existing.effort !== undefined ? { effort: existing.effort } : {}),
1869
+ source: existing.effort_source ?? "none",
1870
+ }
1867
1871
  : undefined;
1868
1872
  const effort = launch.effort ?? pendingResumeEffort ?? resolveLaneEffort({
1869
- cliEffort: existing.effort_source === "cli" ? existing.effort : undefined,
1873
+ cliEffort: pendingSettings?.effort ?? (existing.effort_source === "cli" ? existing.effort : undefined),
1870
1874
  profileEffort,
1871
1875
  accountEffort: accountCfg?.effort,
1872
1876
  providerEffort: config.providers[lane.provider].effort,
@@ -2026,15 +2030,18 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
2026
2030
  terminal_details: undefined,
2027
2031
  // An account move's reservation ends with the claim.
2028
2032
  pending_resume: undefined,
2033
+ pending_settings: undefined,
2029
2034
  account: resolved.id,
2030
2035
  provider_home_at_spawn: resolved.home,
2031
2036
  ...(model !== undefined ? { model } : {}),
2032
2037
  ...(model !== undefined
2033
2038
  ? {
2034
2039
  model_source: pinnedModel !== undefined
2035
- ? preservedAccountModel !== undefined || existing.model_source === "preserved"
2036
- ? "preserved"
2037
- : "cli"
2040
+ ? preservedAccountModel === undefined && pendingSettings?.model !== undefined
2041
+ ? "cli"
2042
+ : preservedAccountModel !== undefined || existing.model_source === "preserved"
2043
+ ? "preserved"
2044
+ : "cli"
2038
2045
  : profileModel !== undefined
2039
2046
  ? "profile"
2040
2047
  : accountCfg?.model !== undefined
@@ -475,6 +475,7 @@ export function parseLaneRecord(raw, filePath) {
475
475
  "keep_alive_next_eligible_at",
476
476
  "keep_alive_exhausted_at",
477
477
  "interrupted_at",
478
+ "last_run_usage_at",
478
479
  ]) {
479
480
  if (raw[field] !== undefined)
480
481
  requireTimestamp(raw[field], field, filePath);
@@ -694,6 +695,40 @@ export function parseLaneRecord(raw, filePath) {
694
695
  requireTimestamp(entry.at, `${field}.at`, filePath);
695
696
  }
696
697
  }
698
+ if (raw.settings_history !== undefined) {
699
+ if (!Array.isArray(raw.settings_history)) {
700
+ throw new LaneSchemaError("settings_history must be an array when present", {
701
+ path: filePath,
702
+ field: "settings_history",
703
+ });
704
+ }
705
+ for (const [index, entry] of raw.settings_history.entries()) {
706
+ const field = `settings_history[${index}]`;
707
+ if (!isPlainObject(entry) ||
708
+ Object.keys(entry).some((key) => !["at", "actor", "model", "effort", "reason"].includes(key)) ||
709
+ (entry.model === undefined && entry.effort === undefined)) {
710
+ throw new LaneSchemaError(`${field} must be a settings history entry`, { path: filePath, field });
711
+ }
712
+ requireTimestamp(entry.at, `${field}.at`, filePath);
713
+ if (typeof entry.actor !== "string" || entry.actor.trim() === "") {
714
+ throw new LaneSchemaError(`${field}.actor must be a non-empty string`, { path: filePath, field });
715
+ }
716
+ if (entry.reason !== undefined && (typeof entry.reason !== "string" || entry.reason.trim() === "")) {
717
+ throw new LaneSchemaError(`${field}.reason must be a non-empty string when present`, { path: filePath, field });
718
+ }
719
+ for (const key of ["model", "effort"]) {
720
+ const change = entry[key];
721
+ if (change === undefined)
722
+ continue;
723
+ if (!isPlainObject(change) ||
724
+ Object.keys(change).some((k) => k !== "from" && k !== "to") ||
725
+ typeof change.to !== "string" || change.to.trim() === "" ||
726
+ (change.from !== null && typeof change.from !== "string")) {
727
+ throw new LaneSchemaError(`${field}.${key} must be a {from, to} change`, { path: filePath, field });
728
+ }
729
+ }
730
+ }
731
+ }
697
732
  if (raw.worker_profile_repin_history !== undefined) {
698
733
  if (!Array.isArray(raw.worker_profile_repin_history)) {
699
734
  throw new LaneSchemaError("worker_profile_repin_history must be an array when present", {
@@ -751,7 +786,7 @@ function parseLaneAccountSelection(value, filePath) {
751
786
  const selection = value;
752
787
  if (typeof selection.account !== "string" || selection.account.trim() === "")
753
788
  throw failure("account");
754
- if (selection.signal !== "usage" && selection.signal !== "lane_count" && selection.signal !== "explicit")
789
+ if (selection.signal !== "usage" && selection.signal !== "recent_usage" && selection.signal !== "lane_count" && selection.signal !== "explicit")
755
790
  throw failure("signal");
756
791
  if (selection.reason !== "least_loaded" && selection.reason !== "explicit")
757
792
  throw failure("reason");
@@ -773,6 +808,10 @@ function parseLaneAccountSelection(value, filePath) {
773
808
  throw failure(`considered.${index}`);
774
809
  if (row.ineligible_reason !== undefined && typeof row.ineligible_reason !== "string")
775
810
  throw failure(`considered.${index}.ineligible_reason`);
811
+ if (row.recent_usage_at !== undefined && row.recent_usage_at !== null &&
812
+ (typeof row.recent_usage_at !== "string" || !Number.isFinite(Date.parse(row.recent_usage_at)))) {
813
+ throw failure(`considered.${index}.recent_usage_at`);
814
+ }
776
815
  if (row.usage !== null) {
777
816
  if (!isPlainObject(row.usage))
778
817
  throw failure(`considered.${index}.usage`);
@@ -1015,6 +1054,11 @@ export function mergeLaneRecords(live, sidecars) {
1015
1054
  failed_resume_history: unionHistory(all.map((record) => record.failed_resume_history ?? []), entryIdentity, base.failed_resume_history ?? []),
1016
1055
  }
1017
1056
  : {}),
1057
+ ...(all.some((record) => record.settings_history !== undefined)
1058
+ ? {
1059
+ settings_history: unionHistory(all.map((record) => record.settings_history ?? []), entryIdentity, base.settings_history ?? []),
1060
+ }
1061
+ : {}),
1018
1062
  ...(all.some((record) => record.stop_history !== undefined)
1019
1063
  ? {
1020
1064
  stop_history: unionHistory(all.map((record) => record.stop_history ?? []), entryIdentity, base.stop_history ?? []),
@@ -253,12 +253,14 @@ export interface LaneAccountSelectionConsidered {
253
253
  weekly_percent?: number;
254
254
  sampled_at?: string;
255
255
  } | null;
256
+ /** Latest completed run in the recent-usage window, when lane snapshots are complete. */
257
+ recent_usage_at?: string | null;
256
258
  hosts: LaneAccountSelectionHostCount[];
257
259
  }
258
260
  /** Decision evidence saved with a Claude lane when account balancing is active. */
259
261
  export interface LaneAccountSelection {
260
262
  account: string;
261
- signal: "usage" | "lane_count" | "explicit";
263
+ signal: "usage" | "recent_usage" | "lane_count" | "explicit";
262
264
  reason: "least_loaded" | "explicit";
263
265
  target_host: LaneHost;
264
266
  selected_at: string;
@@ -287,6 +289,20 @@ export interface LaneStopHistoryEntry {
287
289
  at: string;
288
290
  reason: string;
289
291
  }
292
+ /** One `hq lanes set` change; it takes effect when the lane next resumes. */
293
+ export interface LaneSettingsHistoryEntry {
294
+ at: string;
295
+ actor: string;
296
+ model?: {
297
+ from: string | null;
298
+ to: string;
299
+ };
300
+ effort?: {
301
+ from: string | null;
302
+ to: string;
303
+ };
304
+ reason?: string;
305
+ }
290
306
  /** Evidence for a senior-authorized Outpost worker-profile content re-pin. */
291
307
  export interface WorkerProfileRepinHistoryEntry {
292
308
  old_hash: string | null;
@@ -564,6 +580,17 @@ export interface LaneRecord {
564
580
  terminal_reason?: string;
565
581
  /** Reasons supplied to explicit stop commands, retained independently of optional project ledgers. */
566
582
  stop_history?: LaneStopHistoryEntry[];
583
+ /** Model and effort changes made with `hq lanes set`. */
584
+ settings_history?: LaneSettingsHistoryEntry[];
585
+ /**
586
+ * Model or effort from `hq lanes set` on a running lane. The running turn
587
+ * keeps its launched values (quota classification reads `model`); the next
588
+ * spawn applies these and clears them.
589
+ */
590
+ pending_settings?: {
591
+ model?: string;
592
+ effort?: string;
593
+ };
567
594
  /** Classification time when this lane entered interrupted; stable across later metadata writes. */
568
595
  interrupted_at?: string;
569
596
  /**
@@ -598,6 +625,8 @@ export interface LaneRecord {
598
625
  last_resume?: LaneResumeRecord;
599
626
  /** Cache counters from the last exited run's result. Nothing else from the payload. */
600
627
  last_run_usage?: LaneRunUsage;
628
+ /** Completion time paired with last_run_usage; written by the watcher. */
629
+ last_run_usage_at?: string;
601
630
  /**
602
631
  * Spec §4: one repair turn per expected envelope slot when the final output
603
632
  * is invalid or missing. Written before repair spawn and updated on repair
@@ -230,9 +230,9 @@ export const LANE_FIELD_WRITERS = {
230
230
  last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
231
231
  last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
232
232
  overdue: ["reconcile"], overdue_at: ["reconcile"], last_host_report_at: ["watcher", "spawn", "reconcile"], stalled: ["watcher"], stalled_at: ["watcher"], stalled_reason: ["watcher"],
233
- admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], interrupted_at: ["watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
233
+ admitted_reason: ["command", "reconcile"], terminal_reason: ["command", "watcher", "spawn", "reconcile"], stop_history: ["command"], settings_history: ["command"], pending_settings: ["command", "spawn"], interrupted_at: ["command", "watcher", "reconcile"], terminal_details: ["command", "spawn", "watcher", "reconcile"],
234
234
  quota_reset_at: ["watcher", "reconcile"], model_quota_marker: ["watcher", "reconcile"], outpost_command_id: ["command"],
235
- provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
235
+ provider_session_id: ["watcher", "command", "spawn"], provider_session_id_absent: ["watcher", "command"], provider_home_at_spawn: ["spawn"], last_resume: ["spawn"], last_run_usage: ["watcher"], last_run_usage_at: ["watcher"], envelope_repair: ["watcher", "reconcile"], envelope_validation_errors: ["watcher"], last_progress_event_at: ["watcher"],
236
236
  };
237
237
  function isPlainObject(value) {
238
238
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -245,7 +245,7 @@ export function notifySlotFreed(_input) {
245
245
  * or unparseable id is recorded as absent with its reason, never invented.
246
246
  * Non-resumable providers return null and the record is left as it was.
247
247
  */
248
- function runResultFields(hqRoot, lane) {
248
+ function runResultFields(hqRoot, lane, at) {
249
249
  const runDir = laneRunDir(hqRoot, lane.lane_id);
250
250
  const result = readWorkerRunResult(runDir, lane.provider, readSpawnManifest(runDir).worker_out_offset ?? 0);
251
251
  if (result === null)
@@ -255,6 +255,7 @@ function runResultFields(hqRoot, lane) {
255
255
  cache_read_input_tokens: result.cache_read_input_tokens,
256
256
  cache_creation_input_tokens: result.cache_creation_input_tokens,
257
257
  },
258
+ last_run_usage_at: at,
258
259
  };
259
260
  if (result.session_id !== null) {
260
261
  fields.provider_session_id = result.session_id;
@@ -366,7 +367,7 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
366
367
  }
367
368
  const next = {
368
369
  ...current,
369
- ...runResultFields(hqRoot, current),
370
+ ...runResultFields(hqRoot, current, at),
370
371
  state: effectiveClassification.state,
371
372
  queued_reason: undefined,
372
373
  stalled: undefined,
@@ -0,0 +1,5 @@
1
+ import type { ParsedSecretsEnvFastRequest } from "./fast-scaffold.js";
2
+ import type { SecretsEnvFastResult } from "./secrets-env-fast-route.js";
3
+ /** Run the shared secret action without importing the full CLI command graph. */
4
+ export declare function runSecretsEnvFastPath(request: ParsedSecretsEnvFastRequest): Promise<SecretsEnvFastResult>;
5
+ //# sourceMappingURL=secrets-env-fast-path.d.ts.map
@@ -0,0 +1,16 @@
1
+ /** Run the shared secret action without importing the full CLI command graph. */
2
+ export async function runSecretsEnvFastPath(request) {
3
+ if (process.env.HQ_AGENT_DIR?.trim()) {
4
+ const [os, { agentKitPaths }, { adoptAgentIdentityEnv }] = await Promise.all([
5
+ import("node:os"),
6
+ import("./lib/agent-kit/paths.js"),
7
+ import("./lib/agent-kit/adopt-identity.js"),
8
+ ]);
9
+ adoptAgentIdentityEnv(agentKitPaths(os.homedir(), process.env), process.env);
10
+ }
11
+ const { runSecretsEnvCommand } = await import("./commands/secrets.js");
12
+ return runSecretsEnvCommand(request.options, request.scope, {
13
+ fallbackOnAuthFailure: true,
14
+ });
15
+ }
16
+ //# sourceMappingURL=secrets-env-fast-path.js.map
@@ -0,0 +1,18 @@
1
+ /** Default-off hq-flags gate for the `secrets env` entry fast path. */
2
+ import type { ParsedSecretsEnvFastRequest } from "./fast-scaffold.js";
3
+ export declare const CLI_SECRETS_ENV_FAST_PATH_FLAG = "cli.secrets-env-fast-path";
4
+ export type SecretsEnvFastResult = "handled" | "fallback";
5
+ export interface SecretsEnvFastDispatchDeps {
6
+ isEnabled: () => Promise<boolean>;
7
+ runFast: () => Promise<SecretsEnvFastResult>;
8
+ runCurrent: () => Promise<void>;
9
+ }
10
+ /** Keep the existing command graph in control when the gate is off or declines. */
11
+ export declare function dispatchSecretsEnvFastRequest(_request: ParsedSecretsEnvFastRequest, deps: SecretsEnvFastDispatchDeps): Promise<void>;
12
+ /**
13
+ * Fast routes only use an explicit local override or the latest company flag
14
+ * snapshot. A missing company, stale snapshot, or unreadable cache leaves the
15
+ * current CLI path in control; it never performs a registry network request.
16
+ */
17
+ export declare function secretsEnvFastRouteEnabled(request: ParsedSecretsEnvFastRequest, env?: NodeJS.ProcessEnv): Promise<boolean>;
18
+ //# sourceMappingURL=secrets-env-fast-route.d.ts.map
@@ -0,0 +1,54 @@
1
+ /** Default-off hq-flags gate for the `secrets env` entry fast path. */
2
+ import { localOverrideEnvVar, readLocalOverride } from "@indigoai-us/hq-flags-client";
3
+ export const CLI_SECRETS_ENV_FAST_PATH_FLAG = "cli.secrets-env-fast-path";
4
+ /** Keep the existing command graph in control when the gate is off or declines. */
5
+ export async function dispatchSecretsEnvFastRequest(_request, deps) {
6
+ if (!(await deps.isEnabled())) {
7
+ await deps.runCurrent();
8
+ return;
9
+ }
10
+ if ((await deps.runFast()) === "fallback") {
11
+ await deps.runCurrent();
12
+ }
13
+ }
14
+ /**
15
+ * Fast routes only use an explicit local override or the latest company flag
16
+ * snapshot. A missing company, stale snapshot, or unreadable cache leaves the
17
+ * current CLI path in control; it never performs a registry network request.
18
+ */
19
+ export async function secretsEnvFastRouteEnabled(request, env = process.env) {
20
+ const override = readLocalOverride(CLI_SECRETS_ENV_FAST_PATH_FLAG, env);
21
+ if (override !== null)
22
+ return override;
23
+ const rawOverride = env[localOverrideEnvVar(CLI_SECRETS_ENV_FAST_PATH_FLAG)];
24
+ if (rawOverride === "1")
25
+ return true;
26
+ if (rawOverride === "0")
27
+ return false;
28
+ if (request.scope.personal)
29
+ return false;
30
+ try {
31
+ const scopedEnv = { ...env };
32
+ if (request.scope.company) {
33
+ // An explicit --company scope must override any ambient binding for the
34
+ // fast-path flag lookup, just as it overrides the secrets command scope.
35
+ delete scopedEnv.HQ_COMPANY_UID;
36
+ scopedEnv.HQ_COMPANY_SLUG = request.scope.company;
37
+ }
38
+ const [{ resolveProcessFlagCompanyContext }, { readFlagSnapshotFile }] = await Promise.all([
39
+ import("./lib/process-flag-company-context.js"),
40
+ import("./lib/flag-snapshot-file.js"),
41
+ ]);
42
+ const { companyUid } = resolveProcessFlagCompanyContext(scopedEnv);
43
+ if (!companyUid)
44
+ return false;
45
+ return readFlagSnapshotFile(companyUid, { env: scopedEnv })?.flags[CLI_SECRETS_ENV_FAST_PATH_FLAG] === true;
46
+ }
47
+ catch (error) {
48
+ // This is a performance rollout gate. Failure to read it preserves the
49
+ // existing full CLI behavior.
50
+ console.warn("[hq] secrets env fast-path flag lookup failed; using the full CLI path", error instanceof Error ? error.name : typeof error);
51
+ return false;
52
+ }
53
+ }
54
+ //# sourceMappingURL=secrets-env-fast-route.js.map