@indigoai-us/hq-cli 5.345.129 → 5.345.131

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 (35) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/assets/scaffold/core/scripts/lib/codex-output-schema.mjs +304 -0
  3. package/assets/scaffold/core/scripts/workflow-runner.mjs +2890 -0
  4. package/dist/command-catalog.generated.d.ts +29 -0
  5. package/dist/command-catalog.generated.js +34 -0
  6. package/dist/commands/capacity.d.ts +1 -2
  7. package/dist/commands/capacity.js +7 -9
  8. package/dist/commands/core-pipeline-workflow-runner-native.d.ts +175 -0
  9. package/dist/commands/core-pipeline-workflow-runner-native.js +3237 -0
  10. package/dist/commands/core-workflow-run-command.d.ts +4 -0
  11. package/dist/commands/core-workflow-run-command.js +41 -0
  12. package/dist/commands/core.js +4 -0
  13. package/dist/commands/lanes-accounts.js +0 -1
  14. package/dist/commands/lanes-loop.d.ts +14 -0
  15. package/dist/commands/lanes-loop.js +43 -2
  16. package/dist/commands/lanes.js +8 -4
  17. package/dist/lib/capacity/freshness.d.ts +17 -0
  18. package/dist/lib/capacity/freshness.js +16 -0
  19. package/dist/lib/capacity/route.d.ts +7 -0
  20. package/dist/lib/capacity/route.js +46 -1
  21. package/dist/lib/core-utils/workflow-runner-schema.d.ts +7 -0
  22. package/dist/lib/core-utils/workflow-runner-schema.js +259 -0
  23. package/dist/lib/lanes/config.d.ts +8 -4
  24. package/dist/lib/lanes/config.js +30 -36
  25. package/dist/lib/lanes/index.d.ts +1 -1
  26. package/dist/lib/lanes/index.js +1 -1
  27. package/dist/lib/lanes/loop.d.ts +5 -0
  28. package/dist/lib/lanes/loop.js +8 -0
  29. package/dist/lib/lanes/reconcile.js +21 -27
  30. package/dist/lib/lanes/throttle.js +5 -9
  31. package/dist/lib/lanes/types.d.ts +4 -5
  32. package/dist/lib/lanes/types.js +1 -0
  33. package/dist/lib/lanes/worker-continuity.d.ts +13 -1
  34. package/dist/lib/lanes/worker-continuity.js +14 -0
  35. package/package.json +1 -1
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ export declare const WORKFLOW_RUNNER_ASSET = "core/scripts/workflow-runner.mjs";
3
+ export declare function registerCoreWorkflowRunCommand(core: Command): void;
4
+ //# sourceMappingURL=core-workflow-run-command.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
2
+ import { runBundledScript } from "../utils/run-bundled-script.js";
3
+ import { describeCoreCommand } from "./core-command-catalog.js";
4
+ import { coreNativePipelineEnabled } from "./core-native-pipeline-flag.js";
5
+ export const WORKFLOW_RUNNER_ASSET = "core/scripts/workflow-runner.mjs";
6
+ export function registerCoreWorkflowRunCommand(core) {
7
+ const workflow = core.command("workflow").description("Workflow orchestration");
8
+ const command = workflow.command("run")
9
+ .description("Run a workflow script or pipeline lane")
10
+ .helpOption(false)
11
+ .allowUnknownOption()
12
+ .allowExcessArguments(true)
13
+ .argument("[args...]")
14
+ .action(async (args, _options, current) => {
15
+ const argv = current.args.length > 0 ? current.args : args;
16
+ const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
17
+ if (!await coreNativePipelineEnabled()) {
18
+ const result = runBundledScript({ asset: WORKFLOW_RUNNER_ASSET, args: argv, cwd: process.cwd(), hqRoot: root });
19
+ if (result.code !== 0)
20
+ process.exit(result.code);
21
+ return;
22
+ }
23
+ const previousRoot = process.env.HQ_ROOT;
24
+ process.env.HQ_ROOT = root;
25
+ try {
26
+ const native = await import("./core-pipeline-workflow-runner-native.js");
27
+ const runner = native;
28
+ const code = await runner.runWorkflowRunnerNative(argv, root);
29
+ if (code !== 0)
30
+ process.exit(code);
31
+ }
32
+ finally {
33
+ if (previousRoot === undefined)
34
+ delete process.env.HQ_ROOT;
35
+ else
36
+ process.env.HQ_ROOT = previousRoot;
37
+ }
38
+ });
39
+ describeCoreCommand(command, { execution: "native", asset: WORKFLOW_RUNNER_ASSET, root: "live" });
40
+ }
41
+ //# sourceMappingURL=core-workflow-run-command.js.map
@@ -52,6 +52,7 @@ import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.j
52
52
  import { registerCoreHookBatchCommand } from "./core-hook-batch-command.js";
53
53
  import { registerCorePipelineWorkerTableCommand } from "./core-pipeline-workers-command.js";
54
54
  import { registerCorePipelineRuntimeCommands } from "./core-pipeline-conductor-command.js";
55
+ import { registerCoreWorkflowRunCommand } from "./core-workflow-run-command.js";
55
56
  import { resolveLiveRoot } from "../utils/hq-roots.js";
56
57
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
57
58
  import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
@@ -304,6 +305,8 @@ export const SCAFFOLD_ONLY_ASSETS = [
304
305
  { asset: "core/scripts/session-title.sh", root: "live", summary: "Rollback companion for native session title" },
305
306
  { asset: "core/scripts/session-title-config.sh", root: "live", summary: "Rollback companion for native session-title settings" },
306
307
  { asset: "core/scripts/pipeline-worker-table.sh", root: "live", summary: "Rollback companion for native pipeline worker-table" },
308
+ { asset: "core/scripts/workflow-runner.mjs", root: "live", summary: "Rollback companion for native workflow runner" },
309
+ { asset: "core/scripts/lib/codex-output-schema.mjs", root: "live", summary: "Schema helper required by the bundled workflow-runner rollback companion" },
307
310
  { asset: "core/scripts/share-suggestion-state.sh", root: "live", summary: "Rollback shell for native share-suggestion-state" },
308
311
  { asset: "core/scripts/repo-run-registry.sh", root: "live", summary: "Rollback shell for native repo-run-registry" },
309
312
  { asset: "core/scripts/detect-stale-review-base.sh", root: "live", summary: "Rollback shell for native detect-stale-review-base" },
@@ -456,6 +459,7 @@ export function registerCoreCommands(program) {
456
459
  registerCoreHookStateHelpers(core);
457
460
  registerCoreHookBatchCommand(core);
458
461
  registerCorePipelineRuntimeCommands(core);
462
+ registerCoreWorkflowRunCommand(core);
459
463
  const sessionHelperCommands = [
460
464
  {
461
465
  name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
@@ -290,7 +290,6 @@ export function createLaneAccountsHandlers(deps) {
290
290
  balance_gate_enabled: balanceGateEnabled,
291
291
  accounts,
292
292
  selections,
293
- lead_reserve_per_host: config.lead_reserve_per_host,
294
293
  queued_leads: loaded.snapshots.flatMap((host) => {
295
294
  const leads = leadLaneIds(host.lanes);
296
295
  return host.lanes
@@ -1,5 +1,19 @@
1
1
  import type { Ctx, LaneResumeOptions } from "./lanes-shared.js";
2
2
  import type { LaneRecord } from "../lib/lanes/types.js";
3
+ /**
4
+ * A loop lane that finished a phase and is waiting for its next envelope is
5
+ * alive, not finished. The watcher still writes `done` when the phase's worker
6
+ * exits (the controller reads that as the phase-complete signal); this moves
7
+ * the lane back to `running`, the same state an admitted loop lane holds
8
+ * before its first phase. Only a stop makes a loop lane `done`.
9
+ */
10
+ export declare function settleLoopLaneIdle(hqRoot: string, laneId: string): LaneRecord;
11
+ /**
12
+ * The next phase resumes the provider session, which `hq lanes resume` only
13
+ * accepts from a resumable state. An idle loop lane sits in `running` with no
14
+ * worker, so it is handed to resume as the `done` lane the phase left.
15
+ */
16
+ export declare function prepareIdleLoopLaneForResume(hqRoot: string, laneId: string): LaneRecord;
3
17
  export interface LoopControllerDeps {
4
18
  resumeLane(laneId: string, options: LaneResumeOptions): Promise<unknown>;
5
19
  interruptLane(laneId: string, reason: string, options: {
@@ -1,8 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { LanesError } from "../lib/lanes/errors.js";
5
- import { readLane, retryUpdateLane } from "../lib/lanes/store.js";
4
+ import { readLane, retryUpdateLane, updateLane } from "../lib/lanes/store.js";
5
+ import { LanesError, isLaneVersionConflict } from "../lib/lanes/errors.js";
6
6
  import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
7
7
  import { spawnLaneWorker } from "../lib/lanes/spawn.js";
8
8
  import { raiseLaneQuestion } from "../lib/lanes/questions.js";
@@ -15,6 +15,43 @@ const LOOP_RUNS_REL = path.join("workspace", "lanes-runs");
15
15
  function loopRunDir(hqRoot, laneId) {
16
16
  return path.join(hqRoot, LOOP_RUNS_REL, laneId);
17
17
  }
18
+ /**
19
+ * A loop lane that finished a phase and is waiting for its next envelope is
20
+ * alive, not finished. The watcher still writes `done` when the phase's worker
21
+ * exits (the controller reads that as the phase-complete signal); this moves
22
+ * the lane back to `running`, the same state an admitted loop lane holds
23
+ * before its first phase. Only a stop makes a loop lane `done`.
24
+ */
25
+ export function settleLoopLaneIdle(hqRoot, laneId) {
26
+ return casLoopLaneState(hqRoot, laneId, "done", "running");
27
+ }
28
+ /**
29
+ * The next phase resumes the provider session, which `hq lanes resume` only
30
+ * accepts from a resumable state. An idle loop lane sits in `running` with no
31
+ * worker, so it is handed to resume as the `done` lane the phase left.
32
+ */
33
+ export function prepareIdleLoopLaneForResume(hqRoot, laneId) {
34
+ return casLoopLaneState(hqRoot, laneId, "running", "done");
35
+ }
36
+ function casLoopLaneState(hqRoot, laneId, from, to) {
37
+ for (let attempt = 0;; attempt += 1) {
38
+ const current = readLane(hqRoot, laneId);
39
+ if (current.loop_mode !== true || current.state !== from)
40
+ return current;
41
+ try {
42
+ return updateLane(hqRoot, laneId, current.version, "command", (record) => ({
43
+ ...record,
44
+ state: to,
45
+ terminal_reason: undefined,
46
+ terminal_details: undefined,
47
+ }));
48
+ }
49
+ catch (error) {
50
+ if (!isLaneVersionConflict(error) || attempt >= 4)
51
+ throw error;
52
+ }
53
+ }
54
+ }
18
55
  export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
19
56
  const entry = process.argv[1];
20
57
  if (!entry)
@@ -151,6 +188,8 @@ export async function runLaneLoopController(ctx, laneId, deps) {
151
188
  }));
152
189
  }
153
190
  else {
191
+ if (current.state === "running")
192
+ current = prepareIdleLoopLaneForResume(hqRoot, laneId);
154
193
  await deps.resumeLane(laneId, {
155
194
  text: `Read ${briefPath} and follow it as the complete instruction for this phase.`,
156
195
  project: current.project_id,
@@ -187,6 +226,8 @@ export async function runLaneLoopController(ctx, laneId, deps) {
187
226
  if (data.schema === "hq-phase-envelope/v1" && !handoff) {
188
227
  throw new Error("section 8 phase requires an hq-phase-handoff/v1 object in the lane envelope artifacts");
189
228
  }
229
+ if (latest.state === "done")
230
+ settleLoopLaneIdle(hqRoot, laneId);
190
231
  return { output: JSON.stringify(handoff ?? envelopeRecord.envelope), ...(latest.provider_session_id ? { sessionId: latest.provider_session_id } : {}) };
191
232
  }
192
233
  await new Promise((resolve) => setTimeout(resolve, 250));
@@ -46,8 +46,8 @@ import { registerDaemonLanesReconcileHandler } from "../lib/lanes/daemon-reconci
46
46
  import { appendProjectFocusOverride, readProjectStoryFocusData, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
47
47
  import { appendLaneFocusOverride, assertFocusGuardAllows, makeFocusOverride } from "../lib/lanes/focus-guard.js";
48
48
  import { LANE_FOCUS_GUARD_FLAG, LANE_FOCUS_DASHBOARD_FLAG, LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LIST_SKIP_REMOTE_REFRESH_FLAG, LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, LANE_ORPHANED_BARE_ID_DROPBOX_REPAIR_FLAG, LANE_PROFILE_PIN_EXHAUSTED_FALLBACK_FLAG, LANE_STATUS_ROW_FIELDS_FLAG, LANE_REAP_STOP_FLAG, LANE_REUSE_REFRESHES_PROFILE_FLAG, LANE_WORKER_CONTINUITY_FLAG, LANE_CONDUCT_LOOP_FLAG, LANE_ENVELOPE_REPAIR_GUIDANCE_FLAG, } from "../lib/lanes/flag-keys.js";
49
- import { findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
50
- import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
49
+ import { applyContinuitySettings, findWorkerContinuityLane, sameLaneHost, workerContinuityPrompt } from "../lib/lanes/worker-continuity.js";
50
+ import { commitIdleLoopStop, enqueueLoopEnvelope, interruptLoopQueue, interruptLoopQueueForStory, loopControllerIsAlive, loopLaneIsIdle, loopStatus, validateLoopEnvelope } from "../lib/lanes/loop.js";
51
51
  import { runLaneLoopController, startLaneLoopController } from "./lanes-loop.js";
52
52
  import { configuredCodexRolloutHomes, retainCodexRollouts } from "../lib/lanes/disk-guard.js";
53
53
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
@@ -89,8 +89,9 @@ import { writeQuestionBody } from "../lib/lanes/questions.js";
89
89
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
90
90
  import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
91
91
  import { LanesError } from "../lib/lanes/errors.js";
92
- import { LedgerCorrupt, capacityOptionsFromEnv, readNow, rebuildNow, } from "../lib/capacity/ledger.js";
92
+ import { LedgerCorrupt, capacityOptionsFromEnv, readCollectorMark, readNow, rebuildNow, } from "../lib/capacity/ledger.js";
93
93
  import { pickAccount } from "../lib/capacity/route.js";
94
+ import { capacityViewAge } from "../lib/capacity/freshness.js";
94
95
  import { readLatestEnvelopeRecord } from "../lib/lanes/watcher.js";
95
96
  import { loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
96
97
  import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
@@ -3436,6 +3437,8 @@ async function runCreate(ctx, opts) {
3436
3437
  ...(explicitProvider ? { providers: [explicitProvider] } : {}),
3437
3438
  model: opts.model ?? null,
3438
3439
  now: ctx.now(),
3440
+ collectorEnabled: readCollectorMark(ledgerOptions)?.enabled ?? null,
3441
+ capacityStale: capacityViewAge(view.generatedAt, ctx.now().getTime()).stale,
3439
3442
  lanesError: routeLanesError,
3440
3443
  });
3441
3444
  if (!result.ok || !result.pick) {
@@ -4102,6 +4105,7 @@ async function runCreate(ctx, opts) {
4102
4105
  providerEffort: config.providers[provider].effort,
4103
4106
  model,
4104
4107
  });
4108
+ workerContinuity = applyContinuitySettings(workerContinuity, { project, model, effort: effort.effort });
4105
4109
  if (outpostId) {
4106
4110
  admission = { action: "queue" };
4107
4111
  validation = "pending";
@@ -8914,7 +8918,7 @@ async function runInterrupt(ctx, laneId, reason, options = {}) {
8914
8918
  const queueInterrupt = lane.loop_mode === true && options.preserveLoopQueue !== true
8915
8919
  ? interruptLoopQueue(runDir(hqRoot, lane.lane_id))
8916
8920
  : undefined;
8917
- if (lane.loop_mode === true && lane.state !== "running") {
8921
+ if (lane.loop_mode === true && (lane.state !== "running" || loopLaneIsIdle(runDir(hqRoot, lane.lane_id), lane.lane_id))) {
8918
8922
  if (!options.quiet)
8919
8923
  printJson({ ok: true, action: "interrupt", lane_id: lane.lane_id, terminal: false, resumable: true, ...queueInterrupt });
8920
8924
  return;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Capacity view freshness. Kept in lib (not commands/capacity.ts) so resident
3
+ * paths such as `hq lanes` can check staleness without pulling in the capacity
4
+ * command module and its lazy collector imports.
5
+ */
6
+ /** now.json older than this is stale: the collector is not keeping the view current. */
7
+ export declare const CAPACITY_STALE_MS: number;
8
+ export interface CapacityViewAge {
9
+ stale: boolean;
10
+ /** Milliseconds since generatedAt; 0 when generatedAt is missing or unparseable. */
11
+ ageMs: number;
12
+ /** False when generatedAt is missing or unparseable. */
13
+ known: boolean;
14
+ }
15
+ /** Stale when generatedAt is missing or older than CAPACITY_STALE_MS. */
16
+ export declare function capacityViewAge(generatedAt: string, nowMs: number): CapacityViewAge;
17
+ //# sourceMappingURL=freshness.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Capacity view freshness. Kept in lib (not commands/capacity.ts) so resident
3
+ * paths such as `hq lanes` can check staleness without pulling in the capacity
4
+ * command module and its lazy collector imports.
5
+ */
6
+ /** now.json older than this is stale: the collector is not keeping the view current. */
7
+ export const CAPACITY_STALE_MS = 5 * 60 * 1000;
8
+ /** Stale when generatedAt is missing or older than CAPACITY_STALE_MS. */
9
+ export function capacityViewAge(generatedAt, nowMs) {
10
+ const at = Date.parse(generatedAt);
11
+ if (Number.isNaN(at))
12
+ return { stale: true, ageMs: 0, known: false };
13
+ const ageMs = Math.max(0, nowMs - at);
14
+ return { stale: ageMs > CAPACITY_STALE_MS, ageMs, known: true };
15
+ }
16
+ //# sourceMappingURL=freshness.js.map
@@ -23,6 +23,11 @@ export declare const LIMIT_STALE_MS: number;
23
23
  export declare const FRESH_MS: number;
24
24
  /** Score penalty for an account whose binding limit reading is not fresh. */
25
25
  export declare const OLD_READING_PENALTY = 10;
26
+ /**
27
+ * Score for an account ranked with unknown usage (collector off or capacity
28
+ * view stale). Sits below any account with fresh known headroom.
29
+ */
30
+ export declare const UNKNOWN_USAGE_SCORE = -100;
26
31
  /** Default --floor applied by the CLI (percent headroom to clear). */
27
32
  export declare const DEFAULT_FLOOR = 15;
28
33
  /** Default --prefer order applied by the CLI. */
@@ -78,6 +83,8 @@ export interface RouteRanked {
78
83
  weekly_resets_at: string | null;
79
84
  why: string;
80
85
  lane_flags: string;
86
+ /** True when no usable reading exists and the account is ranked as unknown. */
87
+ usage_unknown: boolean;
81
88
  }
82
89
  /** Live lane on an account the router would avoid; command names a replacement. */
83
90
  export interface RouteMove {
@@ -42,6 +42,11 @@ export const LIMIT_STALE_MS = 24 * 36e5;
42
42
  export const FRESH_MS = 60 * 6e4;
43
43
  /** Score penalty for an account whose binding limit reading is not fresh. */
44
44
  export const OLD_READING_PENALTY = 10;
45
+ /**
46
+ * Score for an account ranked with unknown usage (collector off or capacity
47
+ * view stale). Sits below any account with fresh known headroom.
48
+ */
49
+ export const UNKNOWN_USAGE_SCORE = -100;
45
50
  /** Default --floor applied by the CLI (percent headroom to clear). */
46
51
  export const DEFAULT_FLOOR = 15;
47
52
  /** Default --prefer order applied by the CLI. */
@@ -197,6 +202,37 @@ export function pickAccount(input) {
197
202
  const lanes = input.lanes ?? [];
198
203
  const ranked = [];
199
204
  const skipped = [];
205
+ // With the collector off (or its mark missing) or the view stale, readings
206
+ // never refresh. Missing or old readings then say nothing about the account,
207
+ // so a signed-in, enabled account is ranked as unknown instead of skipped.
208
+ const readingsUnreliable = input.collectorEnabled === false
209
+ || input.collectorEnabled === null
210
+ || input.capacityStale === true;
211
+ const rankUnknown = (base, accountId, tokenExpired) => {
212
+ const live = liveLaneCount(lanes, base.provider, accountId);
213
+ const whyParts = ["usage unknown, capacity readings are stale or the collector is off"];
214
+ if (live)
215
+ whyParts.push(`${live} live lane${live === 1 ? "" : "s"} already on it`);
216
+ if (tokenExpired)
217
+ whyParts.push("sign-in renews when the lane starts");
218
+ ranked.push({
219
+ provider: base.provider,
220
+ account: accountId,
221
+ label: base.label,
222
+ organization: base.organization,
223
+ headroom: 0,
224
+ score: UNKNOWN_USAGE_SCORE - live * LANE_PENALTY,
225
+ live_lanes: live,
226
+ reset_soon: false,
227
+ token_expired: tokenExpired,
228
+ reading_age_h: 0,
229
+ binding: { limit: "unknown", used_percent: 0, resets_at: null },
230
+ weekly_resets_at: null,
231
+ why: whyParts.join("; "),
232
+ lane_flags: `--provider ${base.provider} --account ${accountId}`,
233
+ usage_unknown: true,
234
+ });
235
+ };
200
236
  for (const provider of input.view.providers) {
201
237
  const providerId = provider.provider;
202
238
  if (!isLaneProvider(providerId))
@@ -247,6 +283,10 @@ export function pickAccount(input) {
247
283
  ? Math.max(0, nowMs - observedMs)
248
284
  : Infinity;
249
285
  if (accountAge > STALE_SKIP_MS) {
286
+ if (readingsUnreliable) {
287
+ rankUnknown(base, laneMatch.id, tokenExpired);
288
+ continue;
289
+ }
250
290
  skipped.push({
251
291
  ...base,
252
292
  reason: "no reading in the last 24 hours",
@@ -313,6 +353,10 @@ export function pickAccount(input) {
313
353
  }
314
354
  const known = viewRows.filter((row) => typeof row.used === "number");
315
355
  if (known.length === 0) {
356
+ if (readingsUnreliable) {
357
+ rankUnknown(base, laneMatch.id, tokenExpired);
358
+ continue;
359
+ }
316
360
  skipped.push({
317
361
  ...base,
318
362
  reason: "no usage numbers for this account in the last day",
@@ -376,6 +420,7 @@ export function pickAccount(input) {
376
420
  weekly_resets_at: weekly?.resetsAt ?? null,
377
421
  why: whyParts.join("; "),
378
422
  lane_flags: `--provider ${providerId} --account ${laneMatch.id}`,
423
+ usage_unknown: false,
379
424
  });
380
425
  }
381
426
  }
@@ -401,7 +446,7 @@ export function pickAccount(input) {
401
446
  bad.set(`${row.provider}:${row.account}`, row.reason);
402
447
  }
403
448
  for (const row of ranked) {
404
- if (row.headroom < floor) {
449
+ if (!row.usage_unknown && row.headroom < floor) {
405
450
  bad.set(`${row.provider}:${row.account}`, `${row.binding.limit} has ${row.headroom}% left`);
406
451
  }
407
452
  }
@@ -0,0 +1,7 @@
1
+ /** Port of pipeline-envelope.sh normalize, including its candidate selection rules. */
2
+ export declare function normalizeWorkflowRunnerEnvelope(raw: string): string;
3
+ /** Makes a copy in Codex's strict structured-output dialect. */
4
+ export declare function strictifySchemaForCodex(schema: unknown): unknown;
5
+ /** Removes nulls that Codex had to add for originally optional properties. */
6
+ export declare function stripStrictNulls(value: unknown, schema: unknown): unknown;
7
+ //# sourceMappingURL=workflow-runner-schema.d.ts.map
@@ -0,0 +1,259 @@
1
+ function isJsonObject(value) {
2
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3
+ }
4
+ function jsonDumps(value) {
5
+ if (value === null || typeof value !== "object")
6
+ return JSON.stringify(value) ?? "null";
7
+ if (Array.isArray(value))
8
+ return `[${value.map(jsonDumps).join(", ")}]`;
9
+ const entries = Object.entries(value);
10
+ return `{${entries.map(([key, child]) => `${JSON.stringify(key)}: ${jsonDumps(child)}`).join(", ")}}`;
11
+ }
12
+ function balancedJsonEnd(text, start) {
13
+ let depth = 0;
14
+ let inString = false;
15
+ for (let i = start; i < text.length; i += 1) {
16
+ const char = text[i];
17
+ if (inString) {
18
+ if (char === "\\")
19
+ i += 1;
20
+ else if (char === '"')
21
+ inString = false;
22
+ continue;
23
+ }
24
+ if (char === '"')
25
+ inString = true;
26
+ else if (char === "{" || char === "[")
27
+ depth += 1;
28
+ else if (char === "}" || char === "]") {
29
+ depth -= 1;
30
+ if (depth === 0)
31
+ return i + 1;
32
+ if (depth < 0)
33
+ return -1;
34
+ }
35
+ }
36
+ return -1;
37
+ }
38
+ /** Port of pipeline-envelope.sh normalize, including its candidate selection rules. */
39
+ export function normalizeWorkflowRunnerEnvelope(raw) {
40
+ const text = raw.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
41
+ const candidates = [];
42
+ const fence = /```[A-Za-z0-9_-]*\s*\n([\s\S]*?)```/g;
43
+ let match;
44
+ while ((match = fence.exec(text)) !== null) {
45
+ try {
46
+ const parsed = JSON.parse(match[1]?.trim() ?? "");
47
+ if (isJsonObject(parsed))
48
+ candidates.push(parsed);
49
+ }
50
+ catch { /* malformed fences are skipped by the shell normalizer */ }
51
+ }
52
+ if (candidates.length === 0) {
53
+ let offset = 0;
54
+ while (offset < text.length) {
55
+ const start = text.indexOf("{", offset);
56
+ if (start < 0)
57
+ break;
58
+ const end = balancedJsonEnd(text, start);
59
+ if (end > 0) {
60
+ try {
61
+ const parsed = JSON.parse(text.slice(start, end));
62
+ if (isJsonObject(parsed))
63
+ candidates.push(parsed);
64
+ offset = end;
65
+ continue;
66
+ }
67
+ catch { /* continue scanning after this opening brace */ }
68
+ }
69
+ offset = start + 1;
70
+ }
71
+ }
72
+ if (candidates.length === 0)
73
+ throw new Error("normalize: no JSON object found");
74
+ if (candidates.length > 1) {
75
+ const tagged = candidates.filter((candidate) => candidate.schema === "hq-phase-handoff/v1" || candidate.schema === "hq-phase-envelope/v1");
76
+ if (tagged.length !== 1)
77
+ throw new Error(`normalize: ${candidates.length} JSON objects found, expected exactly one`);
78
+ return `${jsonDumps(tagged[0])}\n`;
79
+ }
80
+ return `${jsonDumps(candidates[0])}\n`;
81
+ }
82
+ function isRecord(value) {
83
+ return value !== null && typeof value === "object" && !Array.isArray(value);
84
+ }
85
+ function typesOf(node) {
86
+ if (node.type === undefined)
87
+ return [];
88
+ return Array.isArray(node.type) ? [...node.type] : [node.type];
89
+ }
90
+ function isObjectNode(node) {
91
+ const types = typesOf(node);
92
+ return types.length ? types.includes("object") : isRecord(node.properties);
93
+ }
94
+ const MAP_KEYS = ["properties", "$defs", "definitions"];
95
+ const LIST_KEYS = ["anyOf", "oneOf", "allOf", "prefixItems"];
96
+ const SINGLE_KEYS = ["items", "not", "if", "then", "else", "contains", "propertyNames"];
97
+ function schemaError(at, message) {
98
+ return new Error(`codex output schema at ${at}: ${message}`);
99
+ }
100
+ function widenToNullable(node, at) {
101
+ if (!isRecord(node))
102
+ return;
103
+ const types = typesOf(node);
104
+ if (types.length) {
105
+ if (!types.includes("null"))
106
+ node.type = [...types, "null"];
107
+ if (Array.isArray(node.enum) && !node.enum.includes(null))
108
+ node.enum = [...node.enum, null];
109
+ return;
110
+ }
111
+ for (const key of ["anyOf", "oneOf"]) {
112
+ const branches = node[key];
113
+ if (!Array.isArray(branches) || branches.length === 0)
114
+ continue;
115
+ if (!branches.some((branch) => isRecord(branch) && typesOf(branch).includes("null")))
116
+ branches.push({ type: "null" });
117
+ return;
118
+ }
119
+ if (typeof node.$ref === "string" || Array.isArray(node.allOf)) {
120
+ const inner = { ...node };
121
+ for (const key of Object.keys(node))
122
+ delete node[key];
123
+ node.anyOf = [inner, { type: "null" }];
124
+ return;
125
+ }
126
+ if (isRecord(node.properties)) {
127
+ node.type = ["object", "null"];
128
+ return;
129
+ }
130
+ throw schemaError(at, "this property is optional (absent from `required`) but its schema declares neither a type nor a composition, so there is no way to express \"no answer\" in the strict dialect. Give it a type, or list it in `required`.");
131
+ }
132
+ function strictifyInPlace(node, at) {
133
+ if (Array.isArray(node)) {
134
+ node.forEach((child, index) => strictifyInPlace(child, `${at}[${index}]`));
135
+ return node;
136
+ }
137
+ if (!isRecord(node))
138
+ return node;
139
+ for (const key of MAP_KEYS) {
140
+ if (!isRecord(node[key]))
141
+ continue;
142
+ for (const [name, child] of Object.entries(node[key]))
143
+ strictifyInPlace(child, `${at}.${key}.${name}`);
144
+ }
145
+ for (const key of LIST_KEYS) {
146
+ if (Array.isArray(node[key]))
147
+ node[key].forEach((child, index) => strictifyInPlace(child, `${at}.${key}[${index}]`));
148
+ }
149
+ for (const key of SINGLE_KEYS)
150
+ if (node[key] !== undefined)
151
+ strictifyInPlace(node[key], `${at}.${key}`);
152
+ if (!isObjectNode(node))
153
+ return node;
154
+ if (node.additionalProperties !== undefined && node.additionalProperties !== false) {
155
+ throw schemaError(at, "strict structured output permits no dynamic keys, so `additionalProperties` must be false (got "
156
+ + `${JSON.stringify(node.additionalProperties)}). Model a map as an array of {key, value} entries, or run this agent on the grok or claude engine, which have no strict-schema flag.`);
157
+ }
158
+ node.additionalProperties = false;
159
+ if (typesOf(node).length === 0)
160
+ node.type = "object";
161
+ const props = isRecord(node.properties) ? node.properties : null;
162
+ if (!props)
163
+ return node;
164
+ const keys = Object.keys(props);
165
+ const declared = Array.isArray(node.required) ? node.required.filter((key) => typeof key === "string") : [];
166
+ const orphans = declared.filter((key) => !keys.includes(key));
167
+ if (orphans.length)
168
+ throw schemaError(at, `\`required\` lists ${orphans.map((key) => JSON.stringify(key)).join(", ")} with no matching property. Strict rejects a required name that is not in \`properties\`, and dropping it would leave the result validator demanding a key the model can no longer emit. Add the property, or remove the name from \`required\`.`);
169
+ const required = [];
170
+ for (const key of declared)
171
+ if (!required.includes(key))
172
+ required.push(key);
173
+ for (const key of keys) {
174
+ if (required.includes(key))
175
+ continue;
176
+ widenToNullable(props[key], `${at}.properties.${key}`);
177
+ required.push(key);
178
+ }
179
+ node.required = required;
180
+ return node;
181
+ }
182
+ /** Makes a copy in Codex's strict structured-output dialect. */
183
+ export function strictifySchemaForCodex(schema) {
184
+ if (!isRecord(schema) && !Array.isArray(schema))
185
+ return schema;
186
+ return strictifyInPlace(JSON.parse(JSON.stringify(schema)), "<root>");
187
+ }
188
+ const MAX_REF_DEPTH = 16;
189
+ function resolveRef(node, root, depth = 0) {
190
+ if (!isRecord(node) || typeof node.$ref !== "string" || depth >= MAX_REF_DEPTH || !isRecord(root))
191
+ return node;
192
+ const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(node.$ref);
193
+ if (!match)
194
+ return node;
195
+ const bucket = root[match[1] ?? ""];
196
+ const target = isRecord(bucket) ? bucket[match[2] ?? ""] : undefined;
197
+ return isRecord(target) ? resolveRef(target, root, depth + 1) : node;
198
+ }
199
+ function allowsNull(node, root, depth = 0) {
200
+ const resolved = resolveRef(node, root, depth);
201
+ if (!isRecord(resolved))
202
+ return true;
203
+ const types = typesOf(resolved);
204
+ if (types.length)
205
+ return types.includes("null");
206
+ for (const key of ["anyOf", "oneOf"]) {
207
+ if (Array.isArray(resolved[key]))
208
+ return resolved[key].some((branch) => depth < MAX_REF_DEPTH && allowsNull(branch, root, depth + 1));
209
+ }
210
+ return isRecord(resolved.properties) ? false : true;
211
+ }
212
+ function effectiveSchema(node, root, depth = 0) {
213
+ const resolved = resolveRef(node, root, depth);
214
+ if (!isRecord(resolved))
215
+ return null;
216
+ for (const key of ["anyOf", "oneOf"]) {
217
+ if (!Array.isArray(resolved[key]))
218
+ continue;
219
+ const branches = resolved[key].map((branch) => resolveRef(branch, root, depth + 1))
220
+ .filter(isRecord).filter((branch) => !(typesOf(branch).length === 1 && typesOf(branch)[0] === "null"));
221
+ if (branches.length !== 1 || depth >= MAX_REF_DEPTH)
222
+ return null;
223
+ return effectiveSchema(branches[0], root, depth + 1);
224
+ }
225
+ return resolved;
226
+ }
227
+ function stripInPlace(value, schema, root, depth = 0) {
228
+ const effective = effectiveSchema(schema, root, depth);
229
+ if (!effective)
230
+ return value;
231
+ if (Array.isArray(value)) {
232
+ if (effective.items !== undefined)
233
+ value.forEach((item) => stripInPlace(item, effective.items, root, depth + 1));
234
+ return value;
235
+ }
236
+ if (!isRecord(value))
237
+ return value;
238
+ const props = isRecord(effective.properties) ? effective.properties : null;
239
+ if (!props)
240
+ return value;
241
+ const required = Array.isArray(effective.required) ? effective.required : [];
242
+ for (const [key, sub] of Object.entries(props)) {
243
+ if (!(key in value))
244
+ continue;
245
+ if (value[key] === null && !required.includes(key) && !allowsNull(sub, root)) {
246
+ delete value[key];
247
+ continue;
248
+ }
249
+ stripInPlace(value[key], sub, root, depth + 1);
250
+ }
251
+ return value;
252
+ }
253
+ /** Removes nulls that Codex had to add for originally optional properties. */
254
+ export function stripStrictNulls(value, schema) {
255
+ if (!isRecord(schema) || value === null || typeof value !== "object")
256
+ return value;
257
+ return stripInPlace(JSON.parse(JSON.stringify(value)), schema, schema);
258
+ }
259
+ //# sourceMappingURL=workflow-runner-schema.js.map
@@ -9,10 +9,14 @@
9
9
  import * as fs from "node:fs";
10
10
  import { type AccountConfig, type LaneProvider, type LanesConfig, type RunawaySearchGuardConfig, type LivenessWindows, type ProviderConfig } from "./types.js";
11
11
  export declare const LANES_CONFIG_REL: string;
12
- export declare const DEFAULT_MAX_QUEUED = 3;
13
- export declare const DEFAULT_CPU_BUSY_PAUSE = 0.85;
14
- export declare const DEFAULT_CPU_BUSY_RESUME = 0.7;
15
- export declare const DEFAULT_MIN_AVAILABLE_MEM_MB = 1024;
12
+ /**
13
+ * Owner directive 2026-10-14: no default lane caps or CPU/memory admission
14
+ * gates. A provider has no running or queued limit unless the operator sets
15
+ * `max_running` / `max_queued` explicitly; this value stands for "no limit"
16
+ * and stays a finite integer so it survives JSON output.
17
+ */
18
+ export declare const UNLIMITED_LANES: number;
19
+ export declare const DEFAULT_MAX_QUEUED: number;
16
20
  /**
17
21
  * Owner directive 2026-09-22: "if it's been an hour since last msg, always
18
22
  * run compact". Override per provider with `compact_after_ms`.