@indigoai-us/hq-cli 5.345.106 → 5.345.108

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 (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/assets/scaffold/core/scripts/hq-task.mjs +259 -0
  3. package/assets/scaffold/core/scripts/hq-task.sh +31 -0
  4. package/assets/scaffold/core/scripts/pipeline-worker-table.sh +285 -0
  5. package/dist/command-catalog.generated.d.ts +175 -6
  6. package/dist/command-catalog.generated.js +209 -6
  7. package/dist/commands/auth.js +42 -0
  8. package/dist/commands/capacity.d.ts +1 -1
  9. package/dist/commands/capacity.js +74 -11
  10. package/dist/commands/core-checkpoint-stop-gate-native.js +17 -5
  11. package/dist/commands/core-hq-task-fast.d.ts +4 -0
  12. package/dist/commands/core-hq-task-fast.js +11 -0
  13. package/dist/commands/core-hq-task-native.d.ts +20 -0
  14. package/dist/commands/core-hq-task-native.js +275 -0
  15. package/dist/commands/core-native-bucket-c-flag.d.ts +3 -0
  16. package/dist/commands/core-native-bucket-c-flag.js +6 -0
  17. package/dist/commands/core-native-family-flag.d.ts +0 -2
  18. package/dist/commands/core-native-family-flag.js +0 -4
  19. package/dist/commands/core-native-pipeline-flag.d.ts +3 -0
  20. package/dist/commands/core-native-pipeline-flag.js +6 -0
  21. package/dist/commands/core-ontology-native.js +24 -30
  22. package/dist/commands/core-pipeline-workers-command.d.ts +3 -0
  23. package/dist/commands/core-pipeline-workers-command.js +37 -0
  24. package/dist/commands/core-pipeline-workers-native.d.ts +13 -0
  25. package/dist/commands/core-pipeline-workers-native.js +361 -0
  26. package/dist/commands/core.js +12 -10
  27. package/dist/commands/lanes.js +104 -19
  28. package/dist/commands/outposts.d.ts +5 -0
  29. package/dist/commands/outposts.js +5 -0
  30. package/dist/fast-scaffold.d.ts +1 -1
  31. package/dist/fast-scaffold.js +13 -5
  32. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +56015 -63476
  33. package/dist/hq-cloud-sync-runner/package.json +1 -1
  34. package/dist/lib/capacity/ledger.d.ts +1 -0
  35. package/dist/lib/capacity/ledger.js +10 -0
  36. package/dist/lib/capacity/route.d.ts +146 -0
  37. package/dist/lib/capacity/route.js +452 -0
  38. package/dist/lib/daemon/remote-control-pools.d.ts +2 -2
  39. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  40. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  41. package/dist/lib/lanes/reconcile.js +4 -0
  42. package/dist/lib/lanes/remote-control-lane.d.ts +33 -0
  43. package/dist/lib/lanes/remote-control-lane.js +276 -0
  44. package/dist/lib/lanes/spawn.d.ts +5 -0
  45. package/dist/lib/lanes/spawn.js +65 -14
  46. package/dist/lib/lanes/store.js +11 -4
  47. package/dist/lib/lanes/types.d.ts +22 -4
  48. package/dist/lib/lanes/types.js +1 -1
  49. package/dist/lib/lanes/watcher.js +8 -1
  50. package/dist/utils/cognito-session.d.ts +12 -4
  51. package/dist/utils/cognito-session.js +42 -9
  52. package/dist/utils/token-keychain.d.ts +96 -0
  53. package/dist/utils/token-keychain.js +218 -0
  54. package/package.json +2 -2
  55. package/assets/scaffold/core/scripts/ontology-audience-grant.sh +0 -81
  56. package/assets/scaffold/core/scripts/ontology-brief.mjs +0 -93
  57. package/assets/scaffold/core/scripts/ontology-candidate.sh +0 -100
  58. package/assets/scaffold/core/scripts/ontology-garden.mjs +0 -327
  59. package/assets/scaffold/core/scripts/ontology-source-worklist.sh +0 -226
  60. package/assets/scaffold/core/scripts/source-yaml-validate.sh +0 -36
@@ -1 +1 @@
1
- {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.61","bundleSha256":"08a41f64915b26432d804644a2d516ef670e6d2133e8937a46c3a93f7398ab65","formatVersion":1}
1
+ {"name":"hq-cli-daemon-sync-runner","type":"module","version":"6.18.67","bundleSha256":"6d94ad9a9dbfa34fd81040eff69d68a1da68ab0d1318566832fe19393bffe589","formatVersion":1}
@@ -369,6 +369,7 @@ export interface LedgerOptions {
369
369
  pid?: number;
370
370
  lockTtlMs?: number;
371
371
  }
372
+ export declare function capacityOptionsFromEnv(env?: NodeJS.ProcessEnv): LedgerOptions;
372
373
  export declare function capacityRoot(env?: NodeJS.ProcessEnv, home?: string): string;
373
374
  /** Shown for a credential that exists and is past expiry. Never a login. */
374
375
  export declare const EXPIRED_REASON = "expired: refreshes the next time Claude Code is used with this account";
@@ -169,6 +169,16 @@ export const nowViewJsonSchema = {
169
169
  },
170
170
  },
171
171
  };
172
+ export function capacityOptionsFromEnv(env = process.env) {
173
+ const raw = env.HQ_CAPACITY_NOW?.trim();
174
+ if (!raw)
175
+ return { env };
176
+ const parsed = Date.parse(raw);
177
+ if (Number.isNaN(parsed)) {
178
+ throw new Error(`HQ_CAPACITY_NOW must be a UTC timestamp, got '${raw}'`);
179
+ }
180
+ return { env, now: () => new Date(raw) };
181
+ }
172
182
  export function capacityRoot(env = process.env, home = os.homedir()) {
173
183
  const override = env.HQ_CAPACITY_ROOT?.trim();
174
184
  if (override)
@@ -0,0 +1,146 @@
1
+ import type { LaneRecord, LaneState, LanesConfig } from "../lanes/types.js";
2
+ import { type LaneProvider } from "../lanes/types.js";
3
+ import type { NowLimit, NowView } from "./ledger.js";
4
+ /** All lane providers, in the stable order boss-route uses for ranking ties. */
5
+ export declare const ROUTE_PROVIDERS: readonly LaneProvider[];
6
+ /** Lane states that count against an account's live load. */
7
+ export declare const LIVE_STATES: ReadonlySet<LaneState>;
8
+ /** Limit kinds the router scores against. */
9
+ export declare const LIMIT_KINDS: Set<string>;
10
+ /** A reading older than this is treated as no reading at all. */
11
+ export declare const STALE_SKIP_MS: number;
12
+ /** Headroom below this percent is "nearly used up"; the account is skipped. */
13
+ export declare const NEARLY_OUT = 5;
14
+ /** Score penalty per live lane already on the account. */
15
+ export declare const LANE_PENALTY = 3;
16
+ /** Weekly reset within this window boosts an otherwise idle account. */
17
+ export declare const RESET_SOON_MS: number;
18
+ /** Score bonus for a weekly that resets within RESET_SOON_MS. */
19
+ export declare const RESET_SOON_BONUS = 10;
20
+ /** A limit reading older than this is dropped as unknown until its window resets. */
21
+ export declare const LIMIT_STALE_MS: number;
22
+ /** Readings older than this rank below fresh ones by OLD_READING_PENALTY. */
23
+ export declare const FRESH_MS: number;
24
+ /** Score penalty for an account whose binding limit reading is not fresh. */
25
+ export declare const OLD_READING_PENALTY = 10;
26
+ /** Default --floor applied by the CLI (percent headroom to clear). */
27
+ export declare const DEFAULT_FLOOR = 15;
28
+ /** Default --prefer order applied by the CLI. */
29
+ export declare const DEFAULT_PREFER: readonly LaneProvider[];
30
+ /** Sign-in states a lane can still run on. */
31
+ export declare const RUNNABLE_AUTH: Set<string>;
32
+ /** Map a model id to its lane provider, or null when the model is unmapped. */
33
+ export declare function providerForModel(model: string): LaneProvider | null;
34
+ /** True when the limit should factor into the pick for this request. */
35
+ export declare function limitApplies(provider: LaneProvider, limit: NowLimit, model: string): boolean;
36
+ /** Human label for the limit a reason or binding row quotes. */
37
+ export declare function labelLimit(limit: NowLimit): string;
38
+ /** Lane account whose canonical home matches one of the ledger account's homes. */
39
+ export declare function laneAccountFor(provider: LaneProvider, homes: readonly string[] | undefined, lanesConfig: LanesConfig): {
40
+ id: string;
41
+ enabled: boolean;
42
+ } | null;
43
+ /** Request payload echoed in the output. */
44
+ export interface RouteRequest {
45
+ providers: LaneProvider[];
46
+ model: string | null;
47
+ prefer: LaneProvider[];
48
+ floor: number;
49
+ }
50
+ export interface RouteBindingSummary {
51
+ limit: string;
52
+ used_percent: number;
53
+ resets_at: string | null;
54
+ }
55
+ /** One account the router could not route to, with the reason the operator sees. */
56
+ export interface RouteSkipped {
57
+ provider: LaneProvider;
58
+ account: string | null;
59
+ label: string;
60
+ organization: string | null;
61
+ reason: string;
62
+ fix?: string | null;
63
+ resets_at?: string | null;
64
+ }
65
+ /** One account the router scored, in rank order after sorting. */
66
+ export interface RouteRanked {
67
+ provider: LaneProvider;
68
+ account: string;
69
+ label: string;
70
+ organization: string | null;
71
+ headroom: number;
72
+ score: number;
73
+ live_lanes: number;
74
+ reset_soon: boolean;
75
+ token_expired: boolean;
76
+ reading_age_h: number;
77
+ binding: RouteBindingSummary;
78
+ weekly_resets_at: string | null;
79
+ why: string;
80
+ lane_flags: string;
81
+ }
82
+ /** Live lane on an account the router would avoid; command names a replacement. */
83
+ export interface RouteMove {
84
+ lane_id: string;
85
+ provider: LaneProvider;
86
+ from: string | null;
87
+ reason: string;
88
+ to: string | null;
89
+ command: string | null;
90
+ }
91
+ export interface RouteCapacityMeta {
92
+ generated_at: string | null;
93
+ stale: boolean;
94
+ collector_enabled: boolean | null;
95
+ }
96
+ export interface RouteResult {
97
+ ok: boolean;
98
+ at: string;
99
+ request: RouteRequest;
100
+ pick: RouteRanked | null;
101
+ ranked: RouteRanked[];
102
+ skipped: RouteSkipped[];
103
+ moves: RouteMove[];
104
+ next_reset: string | null;
105
+ capacity: RouteCapacityMeta;
106
+ lanes_error: string | null;
107
+ }
108
+ export interface PickAccountInput {
109
+ view: NowView;
110
+ lanes: readonly LaneRecord[];
111
+ lanesConfig: LanesConfig;
112
+ /** Requested providers; "any" expands to the model's provider or every provider. */
113
+ providers?: readonly string[];
114
+ model?: string | null;
115
+ prefer?: readonly string[];
116
+ floor?: number;
117
+ now: Date;
118
+ /** Reserved for future use; routing today does not read env. */
119
+ env?: NodeJS.ProcessEnv;
120
+ /** Collector enabled flag threaded from the mark file. */
121
+ collectorEnabled?: boolean | null;
122
+ /** Overall ledger freshness flag threaded from AdvisoryNow. */
123
+ capacityStale?: boolean;
124
+ /** Error summary when the lanes list could not be read. */
125
+ lanesError?: string | null;
126
+ }
127
+ export declare class RouteUsageError extends Error {
128
+ readonly code = "route_usage";
129
+ constructor(message: string);
130
+ }
131
+ /** Parse `--provider any|claude|codex|grok[,..]` honouring the model expansion rule. */
132
+ export declare function resolveRequestedProviders(raw: readonly string[] | undefined, model: string): LaneProvider[];
133
+ /** Parse `--prefer claude,codex,grok`. */
134
+ export declare function resolvePreferOrder(raw: readonly string[] | undefined): LaneProvider[];
135
+ /** Parse `--floor <n>`; 0..100 inclusive, default DEFAULT_FLOOR when missing. */
136
+ export declare function resolveFloor(raw: string | number | undefined): number;
137
+ /**
138
+ * Pure routing decision over a snapshot: scores every signed-in lane account,
139
+ * applies the --prefer order under the --floor headroom, returns the pick (or
140
+ * null when nothing has room), and the lane-move suggestions for live lanes
141
+ * sitting on bad accounts.
142
+ */
143
+ export declare function pickAccount(input: PickAccountInput): RouteResult;
144
+ /** One-line text output used by `--text` and by `--account auto` admission messages. */
145
+ export declare function formatRouteText(result: RouteResult): string;
146
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Pure router for `hq capacity route` and `hq lanes create --account auto`.
3
+ *
4
+ * Port of personal/skills/boss-mode/boss-route.mjs into hq-cli. Owner decision
5
+ * 2026-10-11: all accounts are one pool across companies; the pick names the
6
+ * account so the board shows which account a lane ran on.
7
+ *
8
+ * Reads the ledger via src/lib/capacity/read.ts (NowView), the lane account
9
+ * registry via src/lib/lanes/config.ts (LanesConfig), and the live lanes list
10
+ * via src/lib/lanes/store.ts (readAllLanes). No shell to `hq`, no network.
11
+ *
12
+ * Returns the same shape as boss-route.mjs:
13
+ * { ok, at, request, pick, ranked, skipped, moves, next_reset, capacity,
14
+ * lanes_error }
15
+ */
16
+ import { canonicalizeHome } from "../lanes/config.js";
17
+ import { LANE_PROVIDERS } from "../lanes/types.js";
18
+ /** All lane providers, in the stable order boss-route uses for ranking ties. */
19
+ export const ROUTE_PROVIDERS = LANE_PROVIDERS;
20
+ /** Lane states that count against an account's live load. */
21
+ export const LIVE_STATES = new Set([
22
+ "running",
23
+ "queued",
24
+ "awaiting_input",
25
+ ]);
26
+ /** Limit kinds the router scores against. */
27
+ export const LIMIT_KINDS = new Set(["session", "weekly_all", "weekly_scoped"]);
28
+ /** A reading older than this is treated as no reading at all. */
29
+ export const STALE_SKIP_MS = 24 * 36e5;
30
+ /** Headroom below this percent is "nearly used up"; the account is skipped. */
31
+ export const NEARLY_OUT = 5;
32
+ /** Score penalty per live lane already on the account. */
33
+ export const LANE_PENALTY = 3;
34
+ /** Weekly reset within this window boosts an otherwise idle account. */
35
+ export const RESET_SOON_MS = 24 * 36e5;
36
+ /** Score bonus for a weekly that resets within RESET_SOON_MS. */
37
+ export const RESET_SOON_BONUS = 10;
38
+ /** A limit reading older than this is dropped as unknown until its window resets. */
39
+ export const LIMIT_STALE_MS = 24 * 36e5;
40
+ /** Readings older than this rank below fresh ones by OLD_READING_PENALTY. */
41
+ export const FRESH_MS = 60 * 6e4;
42
+ /** Score penalty for an account whose binding limit reading is not fresh. */
43
+ export const OLD_READING_PENALTY = 10;
44
+ /** Default --floor applied by the CLI (percent headroom to clear). */
45
+ export const DEFAULT_FLOOR = 15;
46
+ /** Default --prefer order applied by the CLI. */
47
+ export const DEFAULT_PREFER = LANE_PROVIDERS;
48
+ /** Sign-in states a lane can still run on. */
49
+ export const RUNNABLE_AUTH = new Set(["ok", "expired"]);
50
+ /** Map a model id to its lane provider, or null when the model is unmapped. */
51
+ export function providerForModel(model) {
52
+ const s = model.toLowerCase();
53
+ if (/claude|fable|opus|sonnet|haiku/.test(s))
54
+ return "claude";
55
+ if (/gpt|codex|^o\d/.test(s))
56
+ return "codex";
57
+ if (/grok/.test(s))
58
+ return "grok";
59
+ return null;
60
+ }
61
+ /** True when the limit should factor into the pick for this request. */
62
+ export function limitApplies(provider, limit, model) {
63
+ if (!LIMIT_KINDS.has(limit.kind))
64
+ return false;
65
+ if (limit.kind !== "weekly_scoped")
66
+ return true;
67
+ if (provider === "grok")
68
+ return true;
69
+ if (!model || !limit.scope)
70
+ return false;
71
+ return model.toLowerCase().includes(String(limit.scope).toLowerCase());
72
+ }
73
+ /** Human label for the limit a reason or binding row quotes. */
74
+ export function labelLimit(limit) {
75
+ if (limit.kind === "session")
76
+ return "session (5h)";
77
+ if (limit.kind === "weekly_all")
78
+ return "weekly";
79
+ return `${limit.scope ?? "scoped"} weekly`;
80
+ }
81
+ /** Lane account whose canonical home matches one of the ledger account's homes. */
82
+ export function laneAccountFor(provider, homes, lanesConfig) {
83
+ const accounts = lanesConfig.providers?.[provider]?.accounts ?? {};
84
+ if (!homes || homes.length === 0)
85
+ return null;
86
+ const wanted = new Set(homes.map((h) => safeCanonical(h)));
87
+ for (const [id, cfg] of Object.entries(accounts)) {
88
+ if (!cfg?.home)
89
+ continue;
90
+ if (wanted.has(safeCanonical(cfg.home))) {
91
+ return { id, enabled: cfg.enabled !== false };
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ function safeCanonical(home) {
97
+ try {
98
+ return canonicalizeHome(home);
99
+ }
100
+ catch {
101
+ return home;
102
+ }
103
+ }
104
+ export class RouteUsageError extends Error {
105
+ code = "route_usage";
106
+ constructor(message) {
107
+ super(message);
108
+ this.name = "RouteUsageError";
109
+ }
110
+ }
111
+ /** Parse `--provider any|claude|codex|grok[,..]` honouring the model expansion rule. */
112
+ export function resolveRequestedProviders(raw, model) {
113
+ const values = (raw ?? ["any"]).map((p) => p.trim()).filter(Boolean);
114
+ if (values.length === 0)
115
+ return [...ROUTE_PROVIDERS];
116
+ if (values.includes("any")) {
117
+ if (model) {
118
+ const mapped = providerForModel(model);
119
+ if (mapped)
120
+ return [mapped];
121
+ }
122
+ return [...ROUTE_PROVIDERS];
123
+ }
124
+ const out = [];
125
+ for (const value of values) {
126
+ if (!isLaneProvider(value)) {
127
+ throw new RouteUsageError(`unknown provider: ${value}`);
128
+ }
129
+ if (!out.includes(value))
130
+ out.push(value);
131
+ }
132
+ return out;
133
+ }
134
+ /** Parse `--prefer claude,codex,grok`. */
135
+ export function resolvePreferOrder(raw) {
136
+ const values = (raw ?? [...ROUTE_PROVIDERS]).map((p) => p.trim()).filter(Boolean);
137
+ const out = [];
138
+ for (const value of values) {
139
+ if (!isLaneProvider(value)) {
140
+ throw new RouteUsageError(`unknown provider in --prefer: ${value}`);
141
+ }
142
+ if (!out.includes(value))
143
+ out.push(value);
144
+ }
145
+ if (out.length === 0)
146
+ return [...ROUTE_PROVIDERS];
147
+ return out;
148
+ }
149
+ /** Parse `--floor <n>`; 0..100 inclusive, default DEFAULT_FLOOR when missing. */
150
+ export function resolveFloor(raw) {
151
+ if (raw === undefined || raw === "")
152
+ return DEFAULT_FLOOR;
153
+ const value = Number(raw);
154
+ if (!Number.isFinite(value) || value < 0 || value > 100) {
155
+ throw new RouteUsageError("--floor must be 0 to 100");
156
+ }
157
+ return value;
158
+ }
159
+ function isLaneProvider(value) {
160
+ return ROUTE_PROVIDERS.includes(value);
161
+ }
162
+ function liveLaneCount(lanes, provider, account) {
163
+ let count = 0;
164
+ for (const lane of lanes) {
165
+ if (lane.provider !== provider)
166
+ continue;
167
+ if (lane.account !== account)
168
+ continue;
169
+ if (!LIVE_STATES.has(lane.state))
170
+ continue;
171
+ count += 1;
172
+ }
173
+ return count;
174
+ }
175
+ function limitAgeMs(view, nowMs) {
176
+ if (!view.resetPassed || !view.limit.resetsAt)
177
+ return view.readAge;
178
+ const resetMs = Date.parse(view.limit.resetsAt);
179
+ if (!Number.isFinite(resetMs))
180
+ return view.readAge;
181
+ return Math.min(view.readAge, Math.max(0, nowMs - resetMs));
182
+ }
183
+ /**
184
+ * Pure routing decision over a snapshot: scores every signed-in lane account,
185
+ * applies the --prefer order under the --floor headroom, returns the pick (or
186
+ * null when nothing has room), and the lane-move suggestions for live lanes
187
+ * sitting on bad accounts.
188
+ */
189
+ export function pickAccount(input) {
190
+ const nowDate = input.now;
191
+ const nowMs = nowDate.getTime();
192
+ const model = (input.model ?? "").trim();
193
+ const requested = resolveRequestedProviders(input.providers, model);
194
+ const prefer = resolvePreferOrder(input.prefer);
195
+ const floor = resolveFloor(input.floor ?? DEFAULT_FLOOR);
196
+ const lanes = input.lanes ?? [];
197
+ const ranked = [];
198
+ const skipped = [];
199
+ for (const provider of input.view.providers) {
200
+ const providerId = provider.provider;
201
+ if (!isLaneProvider(providerId))
202
+ continue;
203
+ const accounts = provider.accounts ?? [];
204
+ for (const account of accounts) {
205
+ if (!requested.includes(providerId))
206
+ continue;
207
+ const laneMatch = laneAccountFor(providerId, account.homes, input.lanesConfig);
208
+ const label = account.labels?.[0] ?? account.accountId;
209
+ const base = {
210
+ provider: providerId,
211
+ account: laneMatch?.id ?? null,
212
+ label,
213
+ organization: account.organization ?? null,
214
+ };
215
+ if (!laneMatch) {
216
+ skipped.push({
217
+ ...base,
218
+ reason: "no lane account uses this sign-in",
219
+ fix: `hq capacity login ${providerId}`,
220
+ });
221
+ continue;
222
+ }
223
+ const authState = account.authState ?? "unknown";
224
+ if (account.signedOut || !RUNNABLE_AUTH.has(authState)) {
225
+ skipped.push({
226
+ ...base,
227
+ reason: account.reason ?? `sign-in ${authState}`,
228
+ fix: account.fixCommand ?? null,
229
+ });
230
+ continue;
231
+ }
232
+ const tokenExpired = authState === "expired";
233
+ if (!laneMatch.enabled) {
234
+ skipped.push({
235
+ ...base,
236
+ reason: "usage-only, lanes cannot run on it",
237
+ fix: `hq lanes accounts enable ${providerId} ${laneMatch.id}`,
238
+ });
239
+ continue;
240
+ }
241
+ const observedMs = account.observedAt ? Date.parse(account.observedAt) : NaN;
242
+ const accountAge = Number.isFinite(observedMs)
243
+ ? Math.max(0, nowMs - observedMs)
244
+ : Infinity;
245
+ if (accountAge > STALE_SKIP_MS) {
246
+ skipped.push({
247
+ ...base,
248
+ reason: "no reading in the last 24 hours",
249
+ fix: "hq capacity doctor",
250
+ });
251
+ continue;
252
+ }
253
+ const applicable = (account.limits ?? []).filter((l) => limitApplies(providerId, l, model));
254
+ const viewRows = applicable.map((limit) => {
255
+ const resetMs = limit.resetsAt ? Date.parse(limit.resetsAt) : NaN;
256
+ const resetPassed = limit.state === "reset-pending"
257
+ || (Number.isFinite(resetMs) && resetMs <= nowMs);
258
+ const observedLimit = Date.parse(limit.observedAt || account.observedAt || "");
259
+ const readAge = Number.isFinite(observedLimit)
260
+ ? Math.max(0, nowMs - observedLimit)
261
+ : Infinity;
262
+ if (resetPassed) {
263
+ return { limit, used: 0, readAge, resetPassed: true };
264
+ }
265
+ if (readAge > LIMIT_STALE_MS) {
266
+ return { limit, used: null, readAge, resetPassed: false };
267
+ }
268
+ const used = typeof limit.usedPercent === "number" ? limit.usedPercent : null;
269
+ return { limit, used, readAge, resetPassed: false };
270
+ });
271
+ if (tokenExpired) {
272
+ const readingTimes = applicable
273
+ .map((limit) => Date.parse(limit.observedAt || ""))
274
+ .filter((ms) => Number.isFinite(ms));
275
+ const newestReadingMs = readingTimes.length > 0 ? Math.max(...readingTimes) : -Infinity;
276
+ const accountObservedMs = account.observedAt ? Date.parse(account.observedAt) : NaN;
277
+ const noFreshReading = !Number.isFinite(newestReadingMs)
278
+ || (Number.isFinite(accountObservedMs) && newestReadingMs < accountObservedMs);
279
+ if (noFreshReading) {
280
+ skipped.push({
281
+ ...base,
282
+ reason: "no fresh reading (sign-in expired)",
283
+ fix: account.fixCommand ?? null,
284
+ });
285
+ continue;
286
+ }
287
+ }
288
+ const exhausted = viewRows.find((row) => !row.resetPassed
289
+ && row.readAge <= LIMIT_STALE_MS
290
+ && (row.limit.state === "exhausted" || (typeof row.used === "number" && row.used >= 100)));
291
+ if (exhausted) {
292
+ skipped.push({
293
+ ...base,
294
+ reason: `${labelLimit(exhausted.limit)} used up`,
295
+ resets_at: exhausted.limit.resetsAt ?? null,
296
+ });
297
+ continue;
298
+ }
299
+ const known = viewRows.filter((row) => typeof row.used === "number");
300
+ if (known.length === 0) {
301
+ skipped.push({
302
+ ...base,
303
+ reason: "no usage numbers for this account in the last day",
304
+ fix: tokenExpired ? (account.fixCommand ?? null) : "hq capacity doctor",
305
+ });
306
+ continue;
307
+ }
308
+ const oldest = Math.max(0, ...known.map((row) => limitAgeMs(row, nowMs)));
309
+ const oldReading = oldest > FRESH_MS;
310
+ let binding = known[0];
311
+ for (const row of known)
312
+ if (row.used > binding.used)
313
+ binding = row;
314
+ const headroom = Math.max(0, 100 - binding.used);
315
+ if (headroom < NEARLY_OUT) {
316
+ skipped.push({
317
+ ...base,
318
+ reason: `${labelLimit(binding.limit)} nearly used up`,
319
+ resets_at: binding.limit.resetsAt ?? null,
320
+ });
321
+ continue;
322
+ }
323
+ const live = liveLaneCount(lanes, providerId, laneMatch.id);
324
+ const weekly = applicable.find((l) => l.kind === "weekly_all" || (providerId === "grok" && l.kind === "weekly_scoped"));
325
+ const weeklyResetMs = weekly?.resetsAt ? Date.parse(weekly.resetsAt) : NaN;
326
+ const resetMs = Number.isFinite(weeklyResetMs) ? weeklyResetMs - nowMs : Infinity;
327
+ const resetSoon = resetMs > 0 && resetMs <= RESET_SOON_MS && headroom >= 30;
328
+ const score = headroom
329
+ - live * LANE_PENALTY
330
+ + (resetSoon ? RESET_SOON_BONUS : 0)
331
+ - (oldReading ? OLD_READING_PENALTY : 0);
332
+ const whyParts = [
333
+ binding.used === 0
334
+ ? "nothing used yet on any limit"
335
+ : `${headroom}% left on ${labelLimit(binding.limit)}`,
336
+ ];
337
+ if (live)
338
+ whyParts.push(`${live} live lane${live === 1 ? "" : "s"} already on it`);
339
+ if (resetSoon)
340
+ whyParts.push("weekly resets within a day, so unused room is lost");
341
+ if (oldReading)
342
+ whyParts.push(`last reading ${Math.round(oldest / 36e5)}h old`);
343
+ if (tokenExpired)
344
+ whyParts.push("sign-in renews when the lane starts");
345
+ ranked.push({
346
+ provider: providerId,
347
+ account: laneMatch.id,
348
+ label,
349
+ organization: account.organization ?? null,
350
+ headroom,
351
+ score,
352
+ live_lanes: live,
353
+ reset_soon: resetSoon,
354
+ token_expired: tokenExpired,
355
+ reading_age_h: Math.round(oldest / 36e5),
356
+ binding: {
357
+ limit: labelLimit(binding.limit),
358
+ used_percent: binding.used,
359
+ resets_at: binding.limit.resetsAt ?? null,
360
+ },
361
+ weekly_resets_at: weekly?.resetsAt ?? null,
362
+ why: whyParts.join("; "),
363
+ lane_flags: `--provider ${providerId} --account ${laneMatch.id}`,
364
+ });
365
+ }
366
+ }
367
+ ranked.sort((a, b) => b.score - a.score
368
+ || b.headroom - a.headroom
369
+ || a.reading_age_h - b.reading_age_h
370
+ || a.provider.localeCompare(b.provider));
371
+ let pick = null;
372
+ for (const p of prefer) {
373
+ if (!requested.includes(p))
374
+ continue;
375
+ const best = ranked.find((r) => r.provider === p);
376
+ if (best && best.headroom >= floor) {
377
+ pick = best;
378
+ break;
379
+ }
380
+ }
381
+ if (!pick && ranked.length > 0)
382
+ pick = ranked[0];
383
+ const bad = new Map();
384
+ for (const row of skipped) {
385
+ if (row.account)
386
+ bad.set(`${row.provider}:${row.account}`, row.reason);
387
+ }
388
+ for (const row of ranked) {
389
+ if (row.headroom < floor) {
390
+ bad.set(`${row.provider}:${row.account}`, `${row.binding.limit} has ${row.headroom}% left`);
391
+ }
392
+ }
393
+ const moves = [];
394
+ for (const lane of lanes) {
395
+ if (!LIVE_STATES.has(lane.state))
396
+ continue;
397
+ const providerId = lane.provider;
398
+ if (!isLaneProvider(providerId))
399
+ continue;
400
+ if (!lane.account)
401
+ continue;
402
+ const key = `${providerId}:${lane.account}`;
403
+ if (!bad.has(key))
404
+ continue;
405
+ const to = ranked.find((r) => r.provider === providerId && r.account !== lane.account && r.headroom >= floor);
406
+ moves.push({
407
+ lane_id: lane.lane_id,
408
+ provider: providerId,
409
+ from: lane.account,
410
+ reason: bad.get(key),
411
+ to: to?.account ?? null,
412
+ command: to
413
+ ? `hq lanes message ${lane.lane_id} --account ${to.account} --text "Continue on account ${to.account}; ${lane.account} is out of room."`
414
+ : null,
415
+ });
416
+ }
417
+ const soonest = skipped
418
+ .map((row) => row.resets_at ?? null)
419
+ .filter((v) => !!v)
420
+ .sort()[0] ?? null;
421
+ return {
422
+ ok: !!pick,
423
+ at: nowDate.toISOString(),
424
+ request: {
425
+ providers: requested,
426
+ model: model || null,
427
+ prefer,
428
+ floor,
429
+ },
430
+ pick,
431
+ ranked,
432
+ skipped,
433
+ moves,
434
+ next_reset: pick ? null : soonest,
435
+ capacity: {
436
+ generated_at: input.view.generatedAt ?? null,
437
+ stale: !!input.capacityStale,
438
+ collector_enabled: input.collectorEnabled ?? null,
439
+ },
440
+ lanes_error: input.lanesError ?? null,
441
+ };
442
+ }
443
+ /** One-line text output used by `--text` and by `--account auto` admission messages. */
444
+ export function formatRouteText(result) {
445
+ if (result.pick) {
446
+ const org = result.pick.organization || result.pick.label;
447
+ return `${result.pick.provider} ${result.pick.account} (${org}): ${result.pick.why}. Use ${result.pick.lane_flags}`;
448
+ }
449
+ const soonest = result.next_reset;
450
+ return `No account has room${soonest ? `; next reset ${soonest}` : ""}.`;
451
+ }
452
+ //# sourceMappingURL=route.js.map
@@ -28,8 +28,8 @@ export declare function remoteControlPoolSecretsEnv(env: NodeJS.ProcessEnv, serv
28
28
  /** Require an explicit, existing capability; never inherit the mutable .current session. */
29
29
  export declare function assertPersonalServiceSessionCapability(root: string, serviceSessionId: string): void;
30
30
  /** Hosts file overlay consumed inside this pool's private mount namespace. */
31
- export declare function remoteControlPoolHostsOverlay(pool: RemoteControlPool): string;
32
- export declare function poolHostsFile(baseHosts: string, pool: RemoteControlPool): string;
31
+ export declare function remoteControlPoolHostsOverlay(pool: Pick<RemoteControlPool, "frontendAddress">): string;
32
+ export declare function poolHostsFile(baseHosts: string, pool: Pick<RemoteControlPool, "frontendAddress">): string;
33
33
  export declare function readRemoteControlPoolsConfig(file: string, knownAccountIds?: Iterable<string>, home?: string): RemoteControlPoolsConfig;
34
34
  export declare function writeRemoteControlPoolsConfig(file: string, config: RemoteControlPoolsConfig, home?: string): void;
35
35
  export declare function remoteControlPoolArgs(pool: RemoteControlPool): string[];