@indigoai-us/hq-cli 5.342.13 → 5.342.15
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.
- package/CHANGELOG.md +8 -0
- package/assets/scaffold/core/scripts/hq-delegate-grant.sh +9 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +9 -0
- package/assets/scaffold/core/scripts/hq-job-probe.sh +9 -0
- package/assets/scaffold/core/scripts/jobs-validate.sh +9 -0
- package/dist/commands/core-delegate-w1b.js +19 -10
- package/dist/commands/core-delegate.d.ts +2 -0
- package/dist/commands/core-delegate.js +16 -4
- package/dist/commands/core-job-probe-native.d.ts +4 -1
- package/dist/commands/core-job-probe-native.js +62 -14
- package/dist/commands/core-job-run-native.js +14 -2
- package/dist/commands/core-jobs-native.js +2 -1
- package/dist/commands/core-ontology-native.js +18 -2
- package/dist/commands/core-worker-share-native.js +2 -1
- package/dist/commands/integrations-core.d.ts +5 -4
- package/dist/commands/integrations-core.js +64 -10
- package/dist/commands/lanes-accounts.js +8 -0
- package/dist/lib/core-utils/shell-display-path.d.ts +3 -0
- package/dist/lib/core-utils/shell-display-path.js +12 -0
- package/dist/lib/lanes/admission.d.ts +7 -1
- package/dist/lib/lanes/admission.js +34 -0
- package/dist/lib/lanes/config.js +11 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/reconcile.js +52 -6
- package/dist/lib/lanes/types.d.ts +3 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/main.js +16 -0
- package/dist/utils/run-bundled-script.d.ts +2 -0
- package/dist/utils/run-bundled-script.js +1 -0
- package/dist/utils/windows-spawn.d.ts +2 -0
- package/dist/utils/windows-spawn.js +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.342.15] — 2026-10-04
|
|
6
|
+
- Preserve bounded integration subcommand and RPC error context on asynchronous gateway failures, and retain diagnostics for malformed HTTP 200 gateway errors (HQ-CLI-C8/C9).
|
|
7
|
+
- Claude admission can prioritize lanes that are seniors of live lanes and keep configurable per-host capacity reserved for them when `lanes.lead-admission-priority` is enabled.
|
|
8
|
+
|
|
9
|
+
## [5.342.14] — 2026-10-04
|
|
10
|
+
- Fixed native HQ subprocess launching and job-lock command resolution on Windows.
|
|
11
|
+
- Fixed Windows delegate/job shell parity while preserving embedded carriage returns and POSIX command/path bytes.
|
|
12
|
+
|
|
5
13
|
## [5.342.13] — 2026-10-04
|
|
6
14
|
- Run independent core migration differential cases with bounded parallel child processes while preserving per-case reports and failures.
|
|
7
15
|
|
|
@@ -35,6 +35,15 @@ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
|
|
|
35
35
|
|
|
36
36
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
37
37
|
|
|
38
|
+
# jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
|
|
39
|
+
case "${OSTYPE:-}" in
|
|
40
|
+
msys*)
|
|
41
|
+
jq() {
|
|
42
|
+
command jq "$@" | command sed -b 's/\r$//'
|
|
43
|
+
}
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
|
|
38
47
|
MANIFEST="" YES=0
|
|
39
48
|
while [ $# -gt 0 ]; do
|
|
40
49
|
case "$1" in
|
|
@@ -40,6 +40,15 @@ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
|
|
|
40
40
|
|
|
41
41
|
command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
|
|
42
42
|
|
|
43
|
+
# jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
|
|
44
|
+
case "${OSTYPE:-}" in
|
|
45
|
+
msys*)
|
|
46
|
+
jq() {
|
|
47
|
+
command jq "$@" | command sed -b 's/\r$//'
|
|
48
|
+
}
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
43
52
|
MANIFEST="" SEND=0 HEADLINE="" NOTE=""
|
|
44
53
|
while [ $# -gt 0 ]; do
|
|
45
54
|
case "$1" in
|
|
@@ -120,6 +120,15 @@ done
|
|
|
120
120
|
command -v yq >/dev/null 2>&1 || die "yq is required (mikefarah/yq)"
|
|
121
121
|
command -v jq >/dev/null 2>&1 || die "jq is required"
|
|
122
122
|
|
|
123
|
+
# jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
|
|
124
|
+
case "${OSTYPE:-}" in
|
|
125
|
+
msys*)
|
|
126
|
+
jq() {
|
|
127
|
+
command jq "$@" | command sed -b 's/\r$//'
|
|
128
|
+
}
|
|
129
|
+
;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
123
132
|
iso_now() {
|
|
124
133
|
if [ -n "${HQ_JOB_PROBE_NOW:-}" ]; then
|
|
125
134
|
printf '%s' "$HQ_JOB_PROBE_NOW"
|
|
@@ -40,6 +40,15 @@ command -v jq >/dev/null 2>&1 || {
|
|
|
40
40
|
exit 2
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
# jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
|
|
44
|
+
case "${OSTYPE:-}" in
|
|
45
|
+
msys*)
|
|
46
|
+
jq() {
|
|
47
|
+
command jq "$@" | command sed -b 's/\r$//'
|
|
48
|
+
}
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
|
|
43
52
|
[ "$#" -ge 1 ] || die_usage
|
|
44
53
|
|
|
45
54
|
collect_files() {
|
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
|
+
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
7
8
|
import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
|
|
8
9
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
9
10
|
import { commandExists, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
|
|
@@ -171,12 +172,20 @@ function parseFlags(args, allowed) {
|
|
|
171
172
|
}
|
|
172
173
|
function usage(name) { return USAGE[name] ?? ""; }
|
|
173
174
|
function usageFailure(name) { err(usage(name)); return 1; }
|
|
175
|
+
function bashPath(value) {
|
|
176
|
+
if (process.platform !== "win32")
|
|
177
|
+
return value;
|
|
178
|
+
const normalized = value.replaceAll("\\", "/");
|
|
179
|
+
const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
|
|
180
|
+
return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
|
|
181
|
+
}
|
|
174
182
|
function runShellFallback(name, args, context) {
|
|
183
|
+
const root = context.root ?? context.cwd;
|
|
175
184
|
const result = runBundledScript({
|
|
176
185
|
asset: ASSETS[name],
|
|
177
186
|
args,
|
|
178
187
|
cwd: context.cwd,
|
|
179
|
-
hqRoot:
|
|
188
|
+
hqRoot: bashPath(root),
|
|
180
189
|
captureOutput: true,
|
|
181
190
|
});
|
|
182
191
|
if (result.stdout?.length)
|
|
@@ -392,16 +401,16 @@ function runGrant(args, context) {
|
|
|
392
401
|
if (!manifestPath)
|
|
393
402
|
return fail("grant", "--manifest is required");
|
|
394
403
|
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
|
|
395
|
-
return fail("grant", `manifest not found: ${manifestPath}`);
|
|
404
|
+
return fail("grant", `manifest not found: ${shellDisplayPath(manifestPath)}`);
|
|
396
405
|
let manifest;
|
|
397
406
|
try {
|
|
398
407
|
manifest = readJsonFile(manifestPath);
|
|
399
408
|
}
|
|
400
409
|
catch {
|
|
401
|
-
return fail("grant", `manifest is not valid JSON: ${manifestPath}`);
|
|
410
|
+
return fail("grant", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
|
|
402
411
|
}
|
|
403
412
|
if (manifest === null || manifest === false)
|
|
404
|
-
return fail("grant", `manifest is not valid JSON: ${manifestPath}`);
|
|
413
|
+
return fail("grant", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
|
|
405
414
|
if (!isRecord(manifest))
|
|
406
415
|
return fail("grant", "manifest has no company");
|
|
407
416
|
const company = jqRawPath(manifest, "company");
|
|
@@ -778,13 +787,13 @@ function runSend(args, context) {
|
|
|
778
787
|
if (!manifestPath)
|
|
779
788
|
return fail("send", "--manifest is required");
|
|
780
789
|
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
|
|
781
|
-
return fail("send", `manifest not found: ${manifestPath}`);
|
|
790
|
+
return fail("send", `manifest not found: ${shellDisplayPath(manifestPath)}`);
|
|
782
791
|
const manifest = jsonFile(manifestPath);
|
|
783
792
|
if (!manifest)
|
|
784
|
-
return fail("send", `manifest is not valid JSON: ${manifestPath}`);
|
|
793
|
+
return fail("send", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
|
|
785
794
|
if (send) {
|
|
786
795
|
const publishedPath = path.join(context.root ?? context.cwd, jqRawPath(manifest, "publication.manifestPath"));
|
|
787
|
-
if (
|
|
796
|
+
if (malformedJsonFile(publishedPath)) {
|
|
788
797
|
return runShellFallback("send", args, context);
|
|
789
798
|
}
|
|
790
799
|
}
|
|
@@ -894,7 +903,7 @@ function runSend(args, context) {
|
|
|
894
903
|
}
|
|
895
904
|
moveFile(stagedPrompt, prompt);
|
|
896
905
|
fs.rmSync(stage, { recursive: true, force: true });
|
|
897
|
-
out(`hq-delegate-send: pickup prompt written: ${prompt}\n`);
|
|
906
|
+
out(`hq-delegate-send: pickup prompt written: ${shellDisplayPath(prompt)}\n`);
|
|
898
907
|
if (!send)
|
|
899
908
|
return 0;
|
|
900
909
|
if (jqRawPath(manifest, "status") !== "verified")
|
|
@@ -1035,12 +1044,12 @@ function runVerify(args, context) {
|
|
|
1035
1044
|
if (!manifestPath)
|
|
1036
1045
|
return fail("verify", "--manifest is required (or --dry-run with --company/--project/--to)");
|
|
1037
1046
|
if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
|
|
1038
|
-
return fail("verify", `manifest not found: ${manifestPath}`);
|
|
1047
|
+
return fail("verify", `manifest not found: ${shellDisplayPath(manifestPath)}`);
|
|
1039
1048
|
if (malformedJsonFile(manifestPath))
|
|
1040
1049
|
return runShellFallback("verify", args, context);
|
|
1041
1050
|
const manifest = jsonFile(manifestPath);
|
|
1042
1051
|
if (!manifest)
|
|
1043
|
-
return fail("verify", `jq: error: ${manifestPath}: Invalid JSON`);
|
|
1052
|
+
return fail("verify", `jq: error: ${shellDisplayPath(manifestPath)}: Invalid JSON`);
|
|
1044
1053
|
const lock = `${manifestPath}.send-lock`;
|
|
1045
1054
|
try {
|
|
1046
1055
|
fs.mkdirSync(lock);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Native implementations of the five `hq core delegate` shell companions. */
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import { type SpawnPlan } from "../utils/windows-spawn.js";
|
|
3
4
|
export declare const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
|
|
4
5
|
export type DelegateContext = {
|
|
5
6
|
cwd: string;
|
|
@@ -18,6 +19,7 @@ export type CommandExistsOptions = {
|
|
|
18
19
|
existsSync?: (candidate: string) => boolean;
|
|
19
20
|
};
|
|
20
21
|
export declare function commandExists(name: string, cwd?: string, options?: CommandExistsOptions): boolean;
|
|
22
|
+
export declare function delegateSubprocessSpawnPlan(command: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
|
|
21
23
|
export declare function subprocess(command: string, args: string[], cwd: string, options?: {
|
|
22
24
|
input?: Buffer | string;
|
|
23
25
|
stdinFd?: number;
|
|
@@ -4,7 +4,9 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
|
+
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
7
8
|
import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
|
|
9
|
+
import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
|
|
8
10
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
9
11
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
10
12
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
@@ -86,13 +88,23 @@ function commandPath(name, cwd = process.cwd(), options = {}) {
|
|
|
86
88
|
export function commandExists(name, cwd = process.cwd(), options = {}) {
|
|
87
89
|
return commandPath(name, cwd, options) !== undefined;
|
|
88
90
|
}
|
|
91
|
+
export function delegateSubprocessSpawnPlan(command, args, env = process.env, platform = process.platform) {
|
|
92
|
+
return isWindowsBatchFile(command, platform)
|
|
93
|
+
? buildCmdExeSpawnPlan(command, args, env, platform)
|
|
94
|
+
: { cmd: command, args: [...args], shell: false };
|
|
95
|
+
}
|
|
89
96
|
export function subprocess(command, args, cwd, options = {}) {
|
|
90
|
-
const
|
|
97
|
+
const childEnv = { ...process.env, ...options.env };
|
|
98
|
+
const resolvedCommand = process.platform === "win32" ? commandPath(command, cwd, { env: childEnv }) : undefined;
|
|
99
|
+
const spawn = resolvedCommand ? delegateSubprocessSpawnPlan(resolvedCommand, args, childEnv) : { cmd: command, args, shell: false };
|
|
100
|
+
const result = spawnSync(spawn.cmd, spawn.args, {
|
|
91
101
|
cwd,
|
|
92
|
-
env:
|
|
102
|
+
env: childEnv,
|
|
93
103
|
input: options.input,
|
|
94
104
|
maxBuffer: options.maxBuffer,
|
|
95
105
|
encoding: "buffer",
|
|
106
|
+
shell: spawn.shell,
|
|
107
|
+
...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
96
108
|
stdio: [options.input === undefined ? options.stdinFd ?? "inherit" : "pipe", options.stdout === "inherit" ? "inherit" : options.stdout === "ignore" ? "ignore" : "pipe", options.stderr === "pipe" ? "pipe" : options.stderr ?? "inherit"],
|
|
97
109
|
});
|
|
98
110
|
if (result.error) {
|
|
@@ -563,11 +575,11 @@ function existingManifest(name, args, context) {
|
|
|
563
575
|
}
|
|
564
576
|
const file = path.resolve(context.cwd, manifestArg);
|
|
565
577
|
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
566
|
-
writeErr(`${prefix} manifest not found: ${manifestArg}\n`);
|
|
578
|
+
writeErr(`${prefix} manifest not found: ${shellDisplayPath(manifestArg)}\n`);
|
|
567
579
|
return { code: 1, parsed };
|
|
568
580
|
}
|
|
569
581
|
if (!jqValid(file)) {
|
|
570
|
-
writeErr(`${prefix} manifest is not valid JSON: ${manifestArg}\n`);
|
|
582
|
+
writeErr(`${prefix} manifest is not valid JSON: ${shellDisplayPath(manifestArg)}\n`);
|
|
571
583
|
return { code: 1, parsed };
|
|
572
584
|
}
|
|
573
585
|
return { file, parsed, document: readJsonFile(file) };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
export declare const JOB_PROBE_ASSET = "core/scripts/hq-job-probe.sh";
|
|
3
|
+
export declare function displayJobFile(value: string, platform?: NodeJS.Platform): string;
|
|
4
|
+
export declare function displayShellPath(value: string, platform?: NodeJS.Platform): string;
|
|
3
5
|
export declare const JOB_PROBE_USAGE: string;
|
|
4
6
|
export type Checks = {
|
|
5
7
|
runtime: string;
|
|
@@ -30,7 +32,7 @@ export type ProbeDeps = {
|
|
|
30
32
|
isDirectory: (file: string) => boolean;
|
|
31
33
|
realpath: (file: string) => string | undefined;
|
|
32
34
|
readText: (file: string) => string;
|
|
33
|
-
writeState: (file: string, body: string) => void;
|
|
35
|
+
writeState: (file: string, body: string, mode: number) => void;
|
|
34
36
|
writeStderr: (line: string) => void;
|
|
35
37
|
listSecrets: (company?: string) => Promise<{
|
|
36
38
|
code: number;
|
|
@@ -39,6 +41,7 @@ export type ProbeDeps = {
|
|
|
39
41
|
commandExists: (name: string) => boolean;
|
|
40
42
|
fetcher: (url: string, init: RequestInit) => Promise<Response>;
|
|
41
43
|
};
|
|
44
|
+
export declare function hasProbeCommand(name: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, existsSync?: (candidate: string) => boolean): boolean;
|
|
42
45
|
export declare function defaultProbeDeps(root: string, env?: Record<string, string | undefined>): ProbeDeps;
|
|
43
46
|
export declare function runJobProbe(argv: string[], injected?: ProbeDeps): Promise<ProbeResult>;
|
|
44
47
|
export declare function registerCoreJobProbeCommand(job: Command): void;
|
|
@@ -8,7 +8,21 @@ import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
|
8
8
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
9
9
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
10
10
|
import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
|
|
11
|
+
import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
|
|
12
|
+
import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
|
|
11
13
|
export const JOB_PROBE_ASSET = "core/scripts/hq-job-probe.sh";
|
|
14
|
+
export function displayJobFile(value, platform = process.platform) {
|
|
15
|
+
if (platform !== "win32")
|
|
16
|
+
return value;
|
|
17
|
+
return path.win32.resolve(value).replaceAll("\\", "/");
|
|
18
|
+
}
|
|
19
|
+
export function displayShellPath(value, platform = process.platform) {
|
|
20
|
+
if (platform !== "win32")
|
|
21
|
+
return value;
|
|
22
|
+
const normalized = value.replaceAll("\\", "/");
|
|
23
|
+
const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
|
|
24
|
+
return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
|
|
25
|
+
}
|
|
12
26
|
export const JOB_PROBE_USAGE = [
|
|
13
27
|
"hq-job-probe.sh — Outpost requirements probe → hq-pro jobs status (US-008).", "",
|
|
14
28
|
"On the Outpost, deterministically checks a job's requirements checklist and",
|
|
@@ -79,8 +93,8 @@ const realpath = (f) => { try {
|
|
|
79
93
|
catch {
|
|
80
94
|
return undefined;
|
|
81
95
|
} };
|
|
82
|
-
function writeState(file, body) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${body}\n`, { mode
|
|
83
|
-
fs.chmodSync(file,
|
|
96
|
+
function writeState(file, body, mode) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${body}\n`, { mode }); try {
|
|
97
|
+
fs.chmodSync(file, mode);
|
|
84
98
|
}
|
|
85
99
|
catch { /* legacy chmod is best-effort */ } }
|
|
86
100
|
function hasCommand(name, env) {
|
|
@@ -97,10 +111,29 @@ function hasCommand(name, env) {
|
|
|
97
111
|
}
|
|
98
112
|
return false;
|
|
99
113
|
}
|
|
114
|
+
export function hasProbeCommand(name, env, platform = process.platform, existsSync = (candidate) => { try {
|
|
115
|
+
return fs.statSync(candidate).isFile();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
} }) {
|
|
120
|
+
return platform === "win32"
|
|
121
|
+
? resolveWindowsExecutable(name, env, existsSync, process.cwd()) !== null
|
|
122
|
+
: hasCommand(name, env);
|
|
123
|
+
}
|
|
100
124
|
function listSecrets(company) {
|
|
101
125
|
// Keep the `hq secrets list` subprocess: no in-process API produces the same names-only table.
|
|
102
126
|
const args = company ? ["secrets", "list", "--company", company] : ["secrets", "list", "--personal"];
|
|
103
|
-
const
|
|
127
|
+
const executable = process.platform === "win32"
|
|
128
|
+
? resolveWindowsExecutable("hq", process.env, (candidate) => { try {
|
|
129
|
+
return fs.statSync(candidate).isFile();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return false;
|
|
133
|
+
} }, process.cwd()) ?? "hq"
|
|
134
|
+
: "hq";
|
|
135
|
+
const plan = planResolvedCommandSpawn(executable, args, process.env);
|
|
136
|
+
const r = spawnSync(plan.cmd, plan.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], shell: plan.shell, ...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}) });
|
|
104
137
|
return Promise.resolve({ code: r.status ?? (r.error ? 127 : 1), stdout: r.stdout ?? "" });
|
|
105
138
|
}
|
|
106
139
|
export function defaultProbeDeps(root, env = process.env) {
|
|
@@ -111,7 +144,7 @@ export function defaultProbeDeps(root, env = process.env) {
|
|
|
111
144
|
statSize, isFile, isDirectory, realpath, readText: (f) => fs.readFileSync(f, "utf8"), writeState,
|
|
112
145
|
writeStderr: (line) => process.stderr.write(line),
|
|
113
146
|
listSecrets: stubs ? async () => { throw new Error("HQ_JOB_TEST_STUB blocked real hq secrets list"); } : listSecrets,
|
|
114
|
-
commandExists: (name) =>
|
|
147
|
+
commandExists: (name) => hasProbeCommand(name, env),
|
|
115
148
|
fetcher: stubs ? async () => { throw new Error("HQ_JOB_TEST_STUB blocked real network fetch"); } : (url, init) => fetch(url, init) };
|
|
116
149
|
}
|
|
117
150
|
const isoNow = (d) => d.env.HQ_JOB_PROBE_NOW || d.now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
@@ -148,9 +181,21 @@ function legacyYamlParseDiagnostics(file, d) {
|
|
|
148
181
|
// The shell evaluates each field with yq before checking the now-empty id;
|
|
149
182
|
// preserve the six parser diagnostics it emits on malformed input.
|
|
150
183
|
const expressions = [".id // \"\"", ".name // \"\"", ".runtime // \"\"", ".requirements.runtime // \"\"", ".requirements.company // \"\"", ".requirements.cwd // .cwd // \"\""];
|
|
151
|
-
return expressions.map((expression) =>
|
|
152
|
-
|
|
153
|
-
|
|
184
|
+
return expressions.map((expression) => {
|
|
185
|
+
const executable = process.platform === "win32"
|
|
186
|
+
? resolveWindowsExecutable("yq", d.env, (candidate) => { try {
|
|
187
|
+
return fs.statSync(candidate).isFile();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return false;
|
|
191
|
+
} }, d.root) ?? "yq"
|
|
192
|
+
: "yq";
|
|
193
|
+
const plan = planResolvedCommandSpawn(executable, ["-r", expression, file], d.env);
|
|
194
|
+
return spawnSync(plan.cmd, plan.args, {
|
|
195
|
+
cwd: d.root, env: d.env, encoding: "utf8", shell: plan.shell,
|
|
196
|
+
...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
197
|
+
}).stderr ?? "";
|
|
198
|
+
}).join("");
|
|
154
199
|
}
|
|
155
200
|
function findById(id, d) {
|
|
156
201
|
let found;
|
|
@@ -240,8 +285,8 @@ function auth(runtime, d) {
|
|
|
240
285
|
const file = runtime === "claude" ? d.env.HQ_JOB_PROBE_CLAUDE_CRED || path.join(d.home, ".claude/.credentials.json") : d.env.HQ_JOB_PROBE_CODEX_AUTH || path.join(d.home, ".codex/auth.json");
|
|
241
286
|
return (d.statSize(file) ?? 0) > 0 ? "ok" : "missing";
|
|
242
287
|
}
|
|
243
|
-
function writeAttempt(d, id, data) { d.writeState(path.join(d.env.HQ_JOB_PROBE_STATE_DIR || path.join(d.home, ".hq/jobs/probes"), id, "last-attempt.json"), compact(data)); }
|
|
244
|
-
function writeCache(d, id, readiness, source) { d.writeState(path.join(d.env.HQ_JOB_STATUS_CACHE_DIR || path.join(d.home, ".hq/jobs/status-cache"), `${id}.json`), compact({ job_id: id, readiness, updated_at: isoNow(d), source })); }
|
|
288
|
+
function writeAttempt(d, id, data) { d.writeState(path.join(d.env.HQ_JOB_PROBE_STATE_DIR || path.join(d.home, ".hq/jobs/probes"), id, "last-attempt.json"), compact(data), 0o600); }
|
|
289
|
+
function writeCache(d, id, readiness, source) { d.writeState(path.join(d.env.HQ_JOB_STATUS_CACHE_DIR || path.join(d.home, ".hq/jobs/status-cache"), `${id}.json`), compact({ job_id: id, readiness, updated_at: isoNow(d), source }), 0o600); }
|
|
245
290
|
function resolveCwd(root, cwd, d) {
|
|
246
291
|
const canon = d.realpath(root) ?? path.resolve(root), candidate = path.resolve(root, cwd), file = d.realpath(candidate) ?? candidate;
|
|
247
292
|
return { file, allowed: file === canon || file.startsWith(`${canon}${path.sep}`) };
|
|
@@ -293,13 +338,13 @@ async function probeFile(file, dry, d) {
|
|
|
293
338
|
catch (e) {
|
|
294
339
|
const rawStderr = legacyYamlParseDiagnostics(file, d);
|
|
295
340
|
if (rawStderr)
|
|
296
|
-
throw new Fatal(`job missing id: ${file}`, 2, "hq-job-probe", rawStderr);
|
|
341
|
+
throw new Fatal(`job missing id: ${displayShellPath(file)}`, 2, "hq-job-probe", rawStderr);
|
|
297
342
|
throw new Fatal(`invalid job YAML: ${file}${e instanceof Error ? `: ${e.message}` : ""}`);
|
|
298
343
|
}
|
|
299
344
|
const id = scalar(job.id), name = scalar(job.name), runtime = scalar(nested(job, "requirements", "runtime")) || scalar(job.runtime);
|
|
300
345
|
const company = scalar(nested(job, "requirements", "company")), cwd = scalar(nested(job, "requirements", "cwd") ?? job.cwd);
|
|
301
346
|
if (!id)
|
|
302
|
-
throw new Fatal(`job missing id: ${file}`);
|
|
347
|
+
throw new Fatal(`job missing id: ${displayShellPath(file)}`);
|
|
303
348
|
if (!runtime)
|
|
304
349
|
throw new Fatal(`job missing runtime: ${file}`);
|
|
305
350
|
const at = isoNow(d), actions = [], authState = auth(runtime, d);
|
|
@@ -411,7 +456,7 @@ async function probeFile(file, dry, d) {
|
|
|
411
456
|
}
|
|
412
457
|
}
|
|
413
458
|
}
|
|
414
|
-
return { code: ingestFailed ? 3 : blocked ? 1 : 0, value: { job_id: id, name, job_file: file, readiness: finalReadiness, probed_at: at, ingest, http, dry_run: dry, checks, next_actions: finalActions } };
|
|
459
|
+
return { code: ingestFailed ? 3 : blocked ? 1 : 0, value: { job_id: id, name, job_file: displayJobFile(file), readiness: finalReadiness, probed_at: at, ingest, http, dry_run: dry, checks, next_actions: finalActions } };
|
|
415
460
|
}
|
|
416
461
|
function parseArgs(argv) {
|
|
417
462
|
const a = [...argv];
|
|
@@ -470,7 +515,7 @@ export async function runJobProbe(argv, injected) {
|
|
|
470
515
|
return { code: 2, stdout: "", stderr: "hq-job-probe: --all does not take a job argument\n" };
|
|
471
516
|
const files = allFiles(root);
|
|
472
517
|
if (!files.length)
|
|
473
|
-
return { code: 2, stdout: "", stderr: `hq-job-probe: no job YAML found under personal/jobs or companies/*/jobs in ${root}\n` };
|
|
518
|
+
return { code: 2, stdout: "", stderr: `hq-job-probe: no job YAML found under personal/jobs or companies/*/jobs in ${displayShellPath(root)}\n` };
|
|
474
519
|
let code = 0;
|
|
475
520
|
try {
|
|
476
521
|
for (const f of files) {
|
|
@@ -500,7 +545,10 @@ export async function runJobProbe(argv, injected) {
|
|
|
500
545
|
// then emits the ordinary not-found error after the process substitution
|
|
501
546
|
// closes. Preserve both stderr lines for byte parity.
|
|
502
547
|
if (x instanceof Fatal && x.message.startsWith("duplicate job id '")) {
|
|
503
|
-
|
|
548
|
+
const shellMessage = process.platform === "win32"
|
|
549
|
+
? x.message.replace(/([A-Za-z]):[\\/]/gu, (_match, drive) => "/" + drive.toLowerCase() + "/").replaceAll("\\", "/")
|
|
550
|
+
: x.message;
|
|
551
|
+
return { code: x.code, stdout: "", stderr: `${x.prefix}: ${shellMessage}\n${x.prefix}: job id not found in registry: ${opts.target}\n` };
|
|
504
552
|
}
|
|
505
553
|
return failure(x);
|
|
506
554
|
}
|
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
|
|
6
|
+
import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
6
8
|
import { JSON_SCHEMA, load } from "js-yaml";
|
|
7
9
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
@@ -256,14 +258,24 @@ export async function spawnProcess(request) {
|
|
|
256
258
|
}
|
|
257
259
|
/** Holds the same kernel flock(2) lock used by the shell companion. */
|
|
258
260
|
export async function acquireFlock(file, env = process.env) {
|
|
259
|
-
const binary = env.HQ_JOB_FLOCK_BIN ||
|
|
261
|
+
const binary = env.HQ_JOB_FLOCK_BIN || (process.platform === "win32"
|
|
262
|
+
? resolveWindowsExecutable("flock", env, (candidate) => { try {
|
|
263
|
+
return fs.statSync(candidate).isFile();
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
} }, process.cwd())
|
|
268
|
+
: pathExecutable("flock", env));
|
|
260
269
|
if (!binary)
|
|
261
270
|
throw new Error("Outpost/Linux requires util-linux flock(1) for serialized job runs");
|
|
262
271
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
263
272
|
return await new Promise((resolve, reject) => {
|
|
264
|
-
const
|
|
273
|
+
const spawnPlan = planResolvedCommandSpawn(binary, ["-x", file, process.execPath, "-e", LOCK_HOLDER], env);
|
|
274
|
+
const child = spawn(spawnPlan.cmd, spawnPlan.args, {
|
|
265
275
|
env,
|
|
266
276
|
stdio: ["pipe", "pipe", "pipe"],
|
|
277
|
+
shell: spawnPlan.shell,
|
|
278
|
+
...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
267
279
|
});
|
|
268
280
|
let stdout = "";
|
|
269
281
|
let stderr = "";
|
|
@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { JSON_SCHEMA, load, loadAll } from "js-yaml";
|
|
5
5
|
import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
|
|
6
6
|
import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
|
|
7
|
+
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
7
8
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
8
9
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
9
10
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
@@ -972,7 +973,7 @@ function systemdQuote(value) {
|
|
|
972
973
|
}
|
|
973
974
|
function serviceUnit(root, runner, id) {
|
|
974
975
|
const pathValue = systemdQuote(`PATH=${servicePathValue()}`);
|
|
975
|
-
return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${runner} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
|
|
976
|
+
return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${shellDisplayPath(runner)} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
|
|
976
977
|
}
|
|
977
978
|
function timerUnit(id, onCalendar, randomize, root) {
|
|
978
979
|
return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job timer: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\n\n[Timer]\nOnCalendar=${onCalendar}\nPersistent=true\nRandomizedDelaySec=${randomize}\nUnit=hq-job-${id}.service\n\n[Install]\nWantedBy=timers.target\n`;
|
|
@@ -3,8 +3,10 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
|
|
6
7
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
7
8
|
import { resolveBundledAsset } from "../utils/hq-roots.js";
|
|
9
|
+
import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
|
|
8
10
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
9
11
|
import { runKnowledgePrefs } from "./core-knowledge-prefs.js";
|
|
10
12
|
const FLAG = "core-native-ontology";
|
|
@@ -55,10 +57,23 @@ export function runOntologyNativeFast(args, _stdin, cwd, hqRoot, commandName) {
|
|
|
55
57
|
}
|
|
56
58
|
function runHq(args, output = "capture") {
|
|
57
59
|
const executable = process.env.HQ_BIN || "hq";
|
|
58
|
-
const
|
|
60
|
+
const resolvedExecutable = process.platform === "win32" && !path.isAbsolute(executable) && !executable.includes(path.sep)
|
|
61
|
+
? resolveWindowsExecutable(executable, process.env, (candidate) => {
|
|
62
|
+
try {
|
|
63
|
+
return fs.statSync(candidate).isFile();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}, process.cwd()) ?? executable
|
|
69
|
+
: executable;
|
|
70
|
+
const plan = planResolvedCommandSpawn(resolvedExecutable, args, process.env);
|
|
71
|
+
const result = spawnSync(plan.cmd, plan.args, {
|
|
59
72
|
cwd: process.cwd(),
|
|
60
73
|
env: process.env,
|
|
61
74
|
encoding: "utf8",
|
|
75
|
+
shell: plan.shell,
|
|
76
|
+
...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
62
77
|
stdio: output === "ignore" ? ["ignore", "ignore", "ignore"] : ["ignore", "pipe", "ignore"],
|
|
63
78
|
});
|
|
64
79
|
return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "" };
|
|
@@ -522,7 +537,8 @@ export function gardenCommand(args, defaultRoot) {
|
|
|
522
537
|
source_ref: JSON.stringify(fm.source_ref || ""), created_at: String(fm.created_at || isoNow()),
|
|
523
538
|
}, body, dryRun);
|
|
524
539
|
report.signals_promoted += 1;
|
|
525
|
-
|
|
540
|
+
const outputPath = path.relative(companyDir, outputFile);
|
|
541
|
+
index.push({ signal_id: id, type, audience_key: key, path: process.platform === "win32" ? outputPath.replaceAll("\\", "/") : outputPath });
|
|
526
542
|
if (key !== "company") {
|
|
527
543
|
const audienceFile = path.join(scopeDirectory, "_audience.yaml");
|
|
528
544
|
if (!fs.existsSync(audienceFile) && !dryRun) {
|
|
@@ -56,7 +56,8 @@ function isRegularFile(filePath, fileSystem) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
function readLink(filePath, fileSystem) {
|
|
59
|
-
|
|
59
|
+
const target = fileSystem.readlinkSync(filePath);
|
|
60
|
+
return process.platform === "win32" ? target.replaceAll("\\", "/") : target;
|
|
60
61
|
}
|
|
61
62
|
function sameBytes(leftPath, rightPath, fileSystem) {
|
|
62
63
|
try {
|
|
@@ -110,11 +110,9 @@ export interface AdminSurface {
|
|
|
110
110
|
}
|
|
111
111
|
interface GatewayMessage {
|
|
112
112
|
result?: unknown;
|
|
113
|
-
error?:
|
|
114
|
-
code?: number;
|
|
115
|
-
message?: string;
|
|
116
|
-
};
|
|
113
|
+
error?: unknown;
|
|
117
114
|
}
|
|
115
|
+
export type GatewayErrorShape = "string_error" | "object_no_code" | "object_code" | "string_code" | "nested_code" | "empty" | "redacted_empty";
|
|
118
116
|
export declare class IntegrationsCliError extends Error {
|
|
119
117
|
/**
|
|
120
118
|
* True when the error is the caller's request/state/permission (a client 4xx
|
|
@@ -152,6 +150,8 @@ export declare class IntegrationsCliError extends Error {
|
|
|
152
150
|
readonly rpcCode?: number;
|
|
153
151
|
/** Fixed vocabulary for the integrations subcommand that called the gateway. */
|
|
154
152
|
readonly gatewaySubcommand?: "tools_list" | "tools_call" | "other";
|
|
153
|
+
/** Fixed vocabulary describing the shape of an upstream JSON-RPC error. */
|
|
154
|
+
readonly gatewayErrorShape?: GatewayErrorShape;
|
|
155
155
|
/**
|
|
156
156
|
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
157
157
|
* refused to register HQ automatically. The provider must register the exact
|
|
@@ -167,6 +167,7 @@ export declare class IntegrationsCliError extends Error {
|
|
|
167
167
|
oauthProtected?: boolean;
|
|
168
168
|
rpcCode?: number;
|
|
169
169
|
gatewaySubcommand?: "tools_list" | "tools_call" | "other";
|
|
170
|
+
gatewayErrorShape?: GatewayErrorShape;
|
|
170
171
|
redirectUri?: string;
|
|
171
172
|
});
|
|
172
173
|
}
|
|
@@ -57,6 +57,8 @@ export class IntegrationsCliError extends Error {
|
|
|
57
57
|
rpcCode;
|
|
58
58
|
/** Fixed vocabulary for the integrations subcommand that called the gateway. */
|
|
59
59
|
gatewaySubcommand;
|
|
60
|
+
/** Fixed vocabulary describing the shape of an upstream JSON-RPC error. */
|
|
61
|
+
gatewayErrorShape;
|
|
60
62
|
/**
|
|
61
63
|
* HQ's own OAuth callback URL, carried on the errors that say a provider
|
|
62
64
|
* refused to register HQ automatically. The provider must register the exact
|
|
@@ -79,6 +81,8 @@ export class IntegrationsCliError extends Error {
|
|
|
79
81
|
this.rpcCode = opts.rpcCode;
|
|
80
82
|
if (opts.gatewaySubcommand !== undefined)
|
|
81
83
|
this.gatewaySubcommand = opts.gatewaySubcommand;
|
|
84
|
+
if (opts.gatewayErrorShape !== undefined)
|
|
85
|
+
this.gatewayErrorShape = opts.gatewayErrorShape;
|
|
82
86
|
if (opts.redirectUri !== undefined) {
|
|
83
87
|
Object.defineProperty(this, "redirectUri", {
|
|
84
88
|
value: opts.redirectUri,
|
|
@@ -191,6 +195,54 @@ function expectedGatewayErrorFallback(code) {
|
|
|
191
195
|
return undefined;
|
|
192
196
|
}
|
|
193
197
|
}
|
|
198
|
+
function recordValue(value) {
|
|
199
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
200
|
+
? value
|
|
201
|
+
: undefined;
|
|
202
|
+
}
|
|
203
|
+
function numericGatewayCode(value) {
|
|
204
|
+
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
205
|
+
return value;
|
|
206
|
+
if (typeof value === "string" && /^-?\d+$/.test(value.trim())) {
|
|
207
|
+
const parsed = Number(value.trim());
|
|
208
|
+
if (Number.isSafeInteger(parsed))
|
|
209
|
+
return parsed;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
/** Read the known JSON-RPC error fields without stringifying arbitrary payload data. */
|
|
214
|
+
function gatewayErrorDetails(error) {
|
|
215
|
+
if (typeof error === "string") {
|
|
216
|
+
const message = redactErrorText(error);
|
|
217
|
+
return {
|
|
218
|
+
message,
|
|
219
|
+
shape: message ? "string_error" : "redacted_empty",
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const record = recordValue(error);
|
|
223
|
+
if (!record)
|
|
224
|
+
return { message: "", shape: "empty" };
|
|
225
|
+
const data = recordValue(record.data);
|
|
226
|
+
const rawMessage = typeof record.message === "string" ? record.message : undefined;
|
|
227
|
+
const message = rawMessage === undefined ? "" : redactErrorText(rawMessage);
|
|
228
|
+
const directCode = record.code;
|
|
229
|
+
const nestedCode = data?.code;
|
|
230
|
+
const directRpcCode = numericGatewayCode(directCode);
|
|
231
|
+
const nestedRpcCode = numericGatewayCode(nestedCode);
|
|
232
|
+
const rpcCode = directRpcCode ?? nestedRpcCode;
|
|
233
|
+
const hasStringCode = typeof directCode === "string" || typeof nestedCode === "string";
|
|
234
|
+
const redactedEmptyMessage = rawMessage !== undefined && rawMessage.length > 0 && !message;
|
|
235
|
+
const shape = redactedEmptyMessage
|
|
236
|
+
? "redacted_empty"
|
|
237
|
+
: nestedCode !== undefined
|
|
238
|
+
? "nested_code"
|
|
239
|
+
: hasStringCode
|
|
240
|
+
? "string_code"
|
|
241
|
+
: directCode !== undefined
|
|
242
|
+
? "object_code"
|
|
243
|
+
: message ? "object_no_code" : "empty";
|
|
244
|
+
return { message, ...(rpcCode === undefined ? {} : { rpcCode }), shape };
|
|
245
|
+
}
|
|
194
246
|
// A 401 from ANY integration-gateway vault call means the caller's HQ session
|
|
195
247
|
// is expired or missing — an expected auth state fixed by `hq login`, not an
|
|
196
248
|
// hq-cli defect. Raise the same typed AuthError the vault company-resolution
|
|
@@ -446,16 +498,16 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
446
498
|
const accepted = message;
|
|
447
499
|
const jobId = accepted && typeof accepted.jobId === "string" ? accepted.jobId : "";
|
|
448
500
|
if (accepted?.status !== "pending" || !/^imj_[A-Za-z0-9_-]+$/.test(jobId)) {
|
|
449
|
-
throw new IntegrationsCliError(`Integration gateway returned an invalid async job response (job id: ${jobId || "missing"}).`, { expected: false, status: 202 });
|
|
501
|
+
throw new IntegrationsCliError(`Integration gateway returned an invalid async job response (job id: ${jobId || "missing"}).`, { expected: false, status: 202, gatewaySubcommand });
|
|
450
502
|
}
|
|
451
503
|
const companyUid = typeof params.companyUid === "string" ? params.companyUid : "";
|
|
452
504
|
if (!companyUid) {
|
|
453
505
|
throw new IntegrationsCliError(`Cannot poll integration job ${jobId}: the company id is missing.`, { expected: true });
|
|
454
506
|
}
|
|
455
507
|
if (!idempotencyKey) {
|
|
456
|
-
throw new IntegrationsCliError(`Integration gateway accepted async job ${jobId} without an idempotency key.`, { expected: false, status: 202 });
|
|
508
|
+
throw new IntegrationsCliError(`Integration gateway accepted async job ${jobId} without an idempotency key.`, { expected: false, status: 202, gatewaySubcommand });
|
|
457
509
|
}
|
|
458
|
-
const terminal = await pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, deadline);
|
|
510
|
+
const terminal = await pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, deadline, gatewaySubcommand);
|
|
459
511
|
terminalMessage = terminal.message;
|
|
460
512
|
terminalStatus = terminal.status;
|
|
461
513
|
}
|
|
@@ -470,6 +522,7 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
470
522
|
throw new IntegrationsCliError(`Integration gateway request failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status, gatewaySubcommand });
|
|
471
523
|
}
|
|
472
524
|
if (terminalMessage?.error) {
|
|
525
|
+
const gatewayError = gatewayErrorDetails(terminalMessage.error);
|
|
473
526
|
// The gateway's error text is minted UPSTREAM (hq-pro, the integration
|
|
474
527
|
// factory, and beyond it the third-party provider), so it is untrusted:
|
|
475
528
|
// scrub credentials and bound the length here, at the throw site, because
|
|
@@ -478,21 +531,22 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
478
531
|
// The chain is idempotent, so the unexpected path scrubbing again is a
|
|
479
532
|
// no-op. This preserves PR #298's user-visible diagnostic; it only makes
|
|
480
533
|
// it safe on the newly-expected path.
|
|
481
|
-
throw new IntegrationsCliError(
|
|
482
|
-
expectedGatewayErrorFallback(
|
|
534
|
+
throw new IntegrationsCliError(gatewayError.message ||
|
|
535
|
+
expectedGatewayErrorFallback(gatewayError.rpcCode) ||
|
|
483
536
|
"Integration gateway returned an error.", {
|
|
484
|
-
expected: isExpectedGatewayError(
|
|
537
|
+
expected: isExpectedGatewayError(gatewayError.rpcCode),
|
|
485
538
|
status: terminalStatus,
|
|
486
539
|
gatewaySubcommand,
|
|
540
|
+
gatewayErrorShape: gatewayError.shape,
|
|
487
541
|
// Carry the protocol code to the capture site as a bounded grouping
|
|
488
542
|
// discriminator (HQ-CLI collision, Sentry 7642756130). This does not
|
|
489
543
|
// alter the `expected` decision above; it only labels the error.
|
|
490
|
-
...(
|
|
544
|
+
...(gatewayError.rpcCode === undefined ? {} : { rpcCode: gatewayError.rpcCode }),
|
|
491
545
|
});
|
|
492
546
|
}
|
|
493
547
|
return terminalMessage;
|
|
494
548
|
}
|
|
495
|
-
async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, deadline) {
|
|
549
|
+
async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, deadline, gatewaySubcommand) {
|
|
496
550
|
const jobPath = `/v1/integrations/mcp/jobs/${encodeURIComponent(jobId)}`;
|
|
497
551
|
while (Date.now() < deadline) {
|
|
498
552
|
const requestRemainingMs = deadline - Date.now();
|
|
@@ -526,7 +580,7 @@ async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, dea
|
|
|
526
580
|
if (res.status === 404) {
|
|
527
581
|
throw new IntegrationsCliError(`Integration job ${jobId} was not found or is no longer available.`, { expected: true, status: 404 });
|
|
528
582
|
}
|
|
529
|
-
throw new IntegrationsCliError(`Integration job ${jobId} polling failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status });
|
|
583
|
+
throw new IntegrationsCliError(`Integration job ${jobId} polling failed (HTTP ${res.status}).`, { expected: isClientError(res.status), status: res.status, gatewaySubcommand });
|
|
530
584
|
}
|
|
531
585
|
if (!poll || typeof poll !== "object") {
|
|
532
586
|
throw new IntegrationsCliError(`Integration job ${jobId} returned an invalid poll response.`, { expected: true });
|
|
@@ -549,7 +603,7 @@ async function pollAsyncGatewayJob(token, companyUid, jobId, idempotencyKey, dea
|
|
|
549
603
|
hasResult !== hasError &&
|
|
550
604
|
(!hasError || validError);
|
|
551
605
|
if (!hasJsonRpcOutcome) {
|
|
552
|
-
throw new IntegrationsCliError(`Integration job ${jobId} returned an invalid JSON-RPC response.`, { expected: false });
|
|
606
|
+
throw new IntegrationsCliError(`Integration job ${jobId} returned an invalid JSON-RPC response.`, { expected: false, gatewaySubcommand });
|
|
553
607
|
}
|
|
554
608
|
return { message: response, status: res.status };
|
|
555
609
|
}
|
|
@@ -10,6 +10,7 @@ import { resolveLaneEffort } from "../lib/lanes/effort.js";
|
|
|
10
10
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
11
11
|
import { accountQuotaView, clearAccountQuota, loadQuotaState } from "../lib/lanes/quota.js";
|
|
12
12
|
import { resolveLaneModel } from "../lib/lanes/spawn.js";
|
|
13
|
+
import { leadLaneIds } from "../lib/lanes/admission.js";
|
|
13
14
|
export function createLaneAccountsHandlers(deps) {
|
|
14
15
|
const { automaticBalanceMoveHistoryReason, ensureDir, laneWithRecoveredProviderSession, loadClaudeBalanceHostSnapshots, parseRemoteJson, prepareAccountMove, printJson, reconcileHostFromCtx, resolveCallerPath } = deps;
|
|
15
16
|
const VENDOR_LOGIN = {
|
|
@@ -269,6 +270,13 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
269
270
|
balance_gate_enabled: balanceGateEnabled,
|
|
270
271
|
accounts,
|
|
271
272
|
selections,
|
|
273
|
+
lead_reserve_per_host: config.lead_reserve_per_host,
|
|
274
|
+
queued_leads: loaded.snapshots.flatMap((host) => {
|
|
275
|
+
const leads = leadLaneIds(host.lanes);
|
|
276
|
+
return host.lanes
|
|
277
|
+
.filter((lane) => lane.provider === "claude" && lane.state === "queued" && leads.has(lane.lane_id))
|
|
278
|
+
.map((lane) => ({ lane_id: lane.lane_id, host: host.capacity.host, queued_reason: lane.queued_reason ?? null }));
|
|
279
|
+
}),
|
|
272
280
|
unavailable_hosts: loaded.unavailableHosts,
|
|
273
281
|
suggestions,
|
|
274
282
|
...(opts.apply ? { moved, skipped } : {}),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
/** Render a native path with the path spelling used by the bundled Bash scripts. */
|
|
3
|
+
export function shellDisplayPath(value) {
|
|
4
|
+
if (process.platform !== "win32")
|
|
5
|
+
return value;
|
|
6
|
+
const absolute = path.win32.resolve(value);
|
|
7
|
+
const match = /^([A-Za-z]):[\\/](.*)$/.exec(absolute);
|
|
8
|
+
if (!match?.[1] || match[2] === undefined)
|
|
9
|
+
return value;
|
|
10
|
+
return `/${match[1].toLowerCase()}/${match[2].replaceAll("\\", "/")}`;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=shell-display-path.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type QuotaState } from "./quota.js";
|
|
2
|
+
import { type LaneRecord, type LanesConfig } from "./types.js";
|
|
2
3
|
export interface AdmissionHost {
|
|
3
4
|
hostname: string;
|
|
4
5
|
outpostId: string | null;
|
|
@@ -11,6 +12,11 @@ export declare function withLaneAdmissionLock<T>(hqRoot: string, provider: LaneR
|
|
|
11
12
|
export declare function withAllLaneAdmissionLocks<T>(hqRoot: string, fn: () => Promise<T>, sleep?: (ms: number) => Promise<void>): Promise<T>;
|
|
12
13
|
export declare function laneRunsOnHost(lane: LaneRecord, host: AdmissionHost): boolean;
|
|
13
14
|
export declare function compareLaneQueueOrder(a: LaneRecord, b: LaneRecord): number;
|
|
15
|
+
/** A lead is identified by another live lane naming it as its lane senior. */
|
|
16
|
+
export declare function leadLaneIds(lanes: readonly LaneRecord[]): Set<string>;
|
|
17
|
+
export declare function compareLaneQueueOrderWithLeads(a: LaneRecord, b: LaneRecord, leads: ReadonlySet<string>): number;
|
|
18
|
+
/** Capacity remaining across enabled Claude accounts on one host. */
|
|
19
|
+
export declare function freeClaudeAccountSlots(lanes: readonly LaneRecord[], config: LanesConfig, host: AdmissionHost, nowMs?: number, quotaState?: QuotaState): number;
|
|
14
20
|
export declare function hasActiveAdmissionReservation(lane: LaneRecord, nowMs: number): boolean;
|
|
15
21
|
export declare function hasActivePendingResume(lane: LaneRecord, nowMs: number): boolean;
|
|
16
22
|
/** Whether a lane currently owns a running slot for its host/provider caps. */
|
|
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { accountMaxRunning } from "./config.js";
|
|
4
4
|
import { LanesError } from "./errors.js";
|
|
5
|
+
import { quotaBlockFor } from "./quota.js";
|
|
6
|
+
import { isTerminalLaneState } from "./types.js";
|
|
5
7
|
export const ADMISSION_LOCK_STALE_MS = 30_000;
|
|
6
8
|
export const ADMISSION_LOCK_TIMEOUT_MS = 5_000;
|
|
7
9
|
export const ADMISSION_RESERVATION_GRACE_MS = 30_000;
|
|
@@ -123,6 +125,38 @@ export function compareLaneQueueOrder(a, b) {
|
|
|
123
125
|
const byQueueAge = Date.parse(a.queued_at ?? a.created_at) - Date.parse(b.queued_at ?? b.created_at);
|
|
124
126
|
return byQueueAge || a.lane_id.localeCompare(b.lane_id);
|
|
125
127
|
}
|
|
128
|
+
/** A lead is identified by another live lane naming it as its lane senior. */
|
|
129
|
+
export function leadLaneIds(lanes) {
|
|
130
|
+
const leads = new Set(lanes.filter((lane) => lane.keep_alive === true).map((lane) => lane.lane_id));
|
|
131
|
+
for (const lane of lanes) {
|
|
132
|
+
if (!isTerminalLaneState(lane.state) && lane.senior.kind === "lane" && lane.senior.id) {
|
|
133
|
+
leads.add(lane.senior.id);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return leads;
|
|
137
|
+
}
|
|
138
|
+
export function compareLaneQueueOrderWithLeads(a, b, leads) {
|
|
139
|
+
if (a.provider === "claude" && b.provider === "claude") {
|
|
140
|
+
const aLead = leads.has(a.lane_id);
|
|
141
|
+
const bLead = leads.has(b.lane_id);
|
|
142
|
+
if (aLead !== bLead)
|
|
143
|
+
return aLead ? -1 : 1;
|
|
144
|
+
}
|
|
145
|
+
return compareLaneQueueOrder(a, b);
|
|
146
|
+
}
|
|
147
|
+
/** Capacity remaining across enabled Claude accounts on one host. */
|
|
148
|
+
export function freeClaudeAccountSlots(lanes, config, host, nowMs = Date.now(), quotaState) {
|
|
149
|
+
return Object.entries(config.providers.claude.accounts).reduce((total, [account, accountConfig]) => {
|
|
150
|
+
if (accountConfig.enabled !== true)
|
|
151
|
+
return total;
|
|
152
|
+
const model = accountConfig.model ?? config.providers.claude.model;
|
|
153
|
+
if (quotaState && quotaBlockFor(quotaState, "claude", account, model, new Date(nowMs)).blocked)
|
|
154
|
+
return total;
|
|
155
|
+
const occupied = lanes.filter((lane) => lane.provider === "claude" && lane.account === account &&
|
|
156
|
+
laneOccupiesRunningSlot(lane, nowMs) && laneRunsOnHost(lane, host)).length;
|
|
157
|
+
return total + Math.max(0, accountMaxRunning(config.providers.claude, account) - occupied);
|
|
158
|
+
}, 0);
|
|
159
|
+
}
|
|
126
160
|
export function hasActiveAdmissionReservation(lane, nowMs) {
|
|
127
161
|
if (lane.state !== "queued" || lane.queued_reason !== undefined || !lane.admission_reserved_at) {
|
|
128
162
|
return false;
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -152,6 +152,14 @@ function positiveInt(value, field, fallback) {
|
|
|
152
152
|
}
|
|
153
153
|
return value;
|
|
154
154
|
}
|
|
155
|
+
function nonNegativeInt(value, field, fallback) {
|
|
156
|
+
if (value === undefined)
|
|
157
|
+
return fallback;
|
|
158
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
159
|
+
throw new LanesConfigError("invalid_config", `${field} must be a non-negative integer`, { field, value });
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
155
163
|
function positiveNumber(value, field, fallback) {
|
|
156
164
|
if (value === undefined)
|
|
157
165
|
return fallback;
|
|
@@ -459,6 +467,7 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
|
459
467
|
tracker: { checks: { ...DEFAULT_TRACKER_CHECKS } },
|
|
460
468
|
runaway_search_guard: { ...DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG },
|
|
461
469
|
low_disk_min_free_bytes: DEFAULT_LOW_DISK_MIN_FREE_BYTES,
|
|
470
|
+
lead_reserve_per_host: 1,
|
|
462
471
|
codex_rollout_compress_after_hours: DEFAULT_CODEX_ROLLOUT_COMPRESS_AFTER_HOURS,
|
|
463
472
|
codex_rollout_delete_after_days: DEFAULT_CODEX_ROLLOUT_DELETE_AFTER_DAYS,
|
|
464
473
|
session_question_timeout_secs: DEFAULT_SESSION_QUESTION_TIMEOUT_SECS,
|
|
@@ -499,6 +508,7 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
|
|
|
499
508
|
}
|
|
500
509
|
const compressAfterHours = positiveInt(raw.codex_rollout_compress_after_hours, "codex_rollout_compress_after_hours", DEFAULT_CODEX_ROLLOUT_COMPRESS_AFTER_HOURS);
|
|
501
510
|
const deleteAfterDays = positiveInt(raw.codex_rollout_delete_after_days, "codex_rollout_delete_after_days", DEFAULT_CODEX_ROLLOUT_DELETE_AFTER_DAYS);
|
|
511
|
+
const leadReservePerHost = nonNegativeInt(raw.lead_reserve_per_host, "lead_reserve_per_host", 1);
|
|
502
512
|
if (deleteAfterDays * 24 < compressAfterHours) {
|
|
503
513
|
throw new LanesConfigError("invalid_config", "codex_rollout_delete_after_days must be at least codex_rollout_compress_after_hours / 24", { codex_rollout_compress_after_hours: compressAfterHours, codex_rollout_delete_after_days: deleteAfterDays });
|
|
504
514
|
}
|
|
@@ -512,6 +522,7 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
|
|
|
512
522
|
tracker: { checks: parseTrackerChecks(raw.tracker) },
|
|
513
523
|
runaway_search_guard: parseRunawaySearchGuard(raw.runaway_search_guard),
|
|
514
524
|
low_disk_min_free_bytes: positiveInt(raw.low_disk_min_free_bytes, "low_disk_min_free_bytes", DEFAULT_LOW_DISK_MIN_FREE_BYTES),
|
|
525
|
+
lead_reserve_per_host: leadReservePerHost,
|
|
515
526
|
codex_rollout_compress_after_hours: compressAfterHours,
|
|
516
527
|
codex_rollout_delete_after_days: deleteAfterDays,
|
|
517
528
|
session_question_timeout_secs: positiveInt(raw.session_question_timeout_secs, "session_question_timeout_secs", DEFAULT_SESSION_QUESTION_TIMEOUT_SECS),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** hq-flags keys used by lane command behavior. */
|
|
2
2
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Gives live senior lanes Claude admission precedence and reserves host capacity for them. */
|
|
4
|
+
export declare const LANE_LEAD_ADMISSION_PRIORITY_FLAG = "lanes.lead-admission-priority";
|
|
3
5
|
/** Refresh provider/model/effort from a worker profile when a done lane takes a new story. */
|
|
4
6
|
export declare const LANE_REUSE_REFRESHES_PROFILE_FLAG = "lanes.reuse-refreshes-profile";
|
|
5
7
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** hq-flags keys used by lane command behavior. */
|
|
2
2
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
|
+
/** Gives live senior lanes Claude admission precedence and reserves host capacity for them. */
|
|
4
|
+
export const LANE_LEAD_ADMISSION_PRIORITY_FLAG = "lanes.lead-admission-priority";
|
|
3
5
|
/** Refresh provider/model/effort from a worker profile when a done lane takes a new story. */
|
|
4
6
|
export const LANE_REUSE_REFRESHES_PROFILE_FLAG = "lanes.reuse-refreshes-profile";
|
|
5
7
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
11
|
-
import { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
|
|
11
|
+
import { compareLaneQueueOrder, compareLaneQueueOrderWithLeads, freeClaudeAccountSlots, leadLaneIds, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
|
|
12
12
|
import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
|
|
13
13
|
import { effectiveAccountHome } from "./account-home.js";
|
|
14
14
|
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
@@ -22,7 +22,7 @@ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecor
|
|
|
22
22
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
|
|
25
|
-
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG } from "./flag-keys.js";
|
|
25
|
+
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG, LANE_LEAD_ADMISSION_PRIORITY_FLAG } from "./flag-keys.js";
|
|
26
26
|
import { checkLaneDiskSpace, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "./disk-guard.js";
|
|
27
27
|
import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
|
|
28
28
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
@@ -30,7 +30,7 @@ import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolG
|
|
|
30
30
|
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
31
31
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
32
32
|
import { isIsoDateTime } from "./types.js";
|
|
33
|
-
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
33
|
+
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, loadQuotaState, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
34
34
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
35
35
|
import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
36
36
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
|
|
@@ -1808,11 +1808,35 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
1808
1808
|
(excludeLaneIds === undefined || !excludeLaneIds.has(lane.lane_id)) &&
|
|
1809
1809
|
lane.pickup_validation_pending !== true &&
|
|
1810
1810
|
laneRunsHere(lane, deps.host))
|
|
1811
|
-
.filter((lane) => !hasFreshAdmissionReservation(lane, now.getTime()))
|
|
1812
|
-
.sort(compareLaneQueueOrder);
|
|
1811
|
+
.filter((lane) => !hasFreshAdmissionReservation(lane, now.getTime()));
|
|
1813
1812
|
if (localQueued.length === 0 || deps.skipAdmissionWithoutCpuSample === true)
|
|
1814
1813
|
return;
|
|
1815
1814
|
const config = loadLanesConfig(hqRoot);
|
|
1815
|
+
const admissionQuotaState = loadQuotaState(hqRoot);
|
|
1816
|
+
const candidateLeadIds = leadLaneIds(lanes);
|
|
1817
|
+
const flagReader = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
1818
|
+
const leadAdmissionEnabledById = new Map();
|
|
1819
|
+
const leadFlagByCompany = new Map();
|
|
1820
|
+
for (const leadId of candidateLeadIds) {
|
|
1821
|
+
const lead = lanes.find((lane) => lane.lane_id === leadId && lane.provider === "claude");
|
|
1822
|
+
if (!lead)
|
|
1823
|
+
continue;
|
|
1824
|
+
const resolution = resolveLaneCompany(hqRoot, lead);
|
|
1825
|
+
const company = resolution.company ?? undefined;
|
|
1826
|
+
const key = company?.uid ?? company?.slug ?? lead.company?.uid ?? lead.company?.slug ?? lead.lane_id;
|
|
1827
|
+
let enabled = leadFlagByCompany.get(key);
|
|
1828
|
+
if (!enabled) {
|
|
1829
|
+
enabled = readLanesRegistryFlagFailOff(LANE_LEAD_ADMISSION_PRIORITY_FLAG, {
|
|
1830
|
+
company,
|
|
1831
|
+
...(company ? { companyUidSource: laneCompanyUidSource(resolution.source) } : {}),
|
|
1832
|
+
env: deps.env,
|
|
1833
|
+
}, flagReader);
|
|
1834
|
+
leadFlagByCompany.set(key, enabled);
|
|
1835
|
+
}
|
|
1836
|
+
leadAdmissionEnabledById.set(leadId, await enabled);
|
|
1837
|
+
}
|
|
1838
|
+
const priorityLeads = new Set([...leadAdmissionEnabledById].filter(([, enabled]) => enabled).map(([id]) => id));
|
|
1839
|
+
localQueued.sort((a, b) => compareLaneQueueOrderWithLeads(a, b, priorityLeads));
|
|
1816
1840
|
const throttleDeps = deps.throttleDeps ?? defaultThrottleDeps(deps.env ?? process.env);
|
|
1817
1841
|
const filePath = throttleStatePath(hqRoot);
|
|
1818
1842
|
const loaded = readThrottleState(filePath, throttleDeps);
|
|
@@ -1884,6 +1908,25 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
1884
1908
|
}
|
|
1885
1909
|
}
|
|
1886
1910
|
const projected = [...running, ...spawned];
|
|
1911
|
+
const laneResolution = lane.provider === "claude" ? resolveLaneCompany(hqRoot, lane) : undefined;
|
|
1912
|
+
const laneCompany = laneResolution?.company ?? undefined;
|
|
1913
|
+
const laneCompanyKey = laneCompany?.uid ?? laneCompany?.slug ?? lane.company?.uid ?? lane.company?.slug;
|
|
1914
|
+
let reserveEnabled = false;
|
|
1915
|
+
if (lane.provider === "claude" && laneCompanyKey) {
|
|
1916
|
+
let enabled = leadFlagByCompany.get(laneCompanyKey);
|
|
1917
|
+
if (!enabled) {
|
|
1918
|
+
enabled = readLanesRegistryFlagFailOff(LANE_LEAD_ADMISSION_PRIORITY_FLAG, {
|
|
1919
|
+
company: laneCompany,
|
|
1920
|
+
...(laneCompany && laneResolution ? { companyUidSource: laneCompanyUidSource(laneResolution.source) } : {}),
|
|
1921
|
+
env: deps.env,
|
|
1922
|
+
}, flagReader);
|
|
1923
|
+
leadFlagByCompany.set(laneCompanyKey, enabled);
|
|
1924
|
+
}
|
|
1925
|
+
reserveEnabled = await enabled;
|
|
1926
|
+
}
|
|
1927
|
+
const reserveBlocked = reserveEnabled && lane.provider === "claude" &&
|
|
1928
|
+
!priorityLeads.has(lane.lane_id) &&
|
|
1929
|
+
freeClaudeAccountSlots(projected, config, deps.host, now.getTime(), admissionQuotaState) <= config.lead_reserve_per_host;
|
|
1887
1930
|
const alternateAccount = availableBalancedAccount(hqRoot, lane, projected, config, deps.host, profileMaxConcurrent, now.getTime());
|
|
1888
1931
|
const admittedLane = alternateAccount !== undefined
|
|
1889
1932
|
? writeAdmissionAccount(hqRoot, lane, alternateAccount, deps.dryRun)
|
|
@@ -1891,7 +1934,10 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
1891
1934
|
const baseCap = laneAdmissionCapReason(running, admittedLane, config, deps.host, profileMaxConcurrent, now.getTime());
|
|
1892
1935
|
const currentCap = laneAdmissionCapReason([...running, ...spawned], admittedLane, config, deps.host, profileMaxConcurrent, now.getTime());
|
|
1893
1936
|
let blockedBy;
|
|
1894
|
-
if (
|
|
1937
|
+
if (reserveBlocked) {
|
|
1938
|
+
blockedBy = "lead_reserve";
|
|
1939
|
+
}
|
|
1940
|
+
else if (currentCap !== undefined) {
|
|
1895
1941
|
blockedBy = baseCap ?? "queue_position";
|
|
1896
1942
|
}
|
|
1897
1943
|
if (!blockedBy && !decision.admit)
|
|
@@ -177,7 +177,7 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
|
|
|
177
177
|
export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
|
|
178
178
|
export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
|
|
179
179
|
export type LaneProvider = (typeof LANE_PROVIDERS)[number];
|
|
180
|
-
export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed", "low_disk", "admit_failed"];
|
|
180
|
+
export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "lead_reserve", "worker_profile_changed", "low_disk", "admit_failed"];
|
|
181
181
|
export type QueuedReason = (typeof QUEUED_REASONS)[number];
|
|
182
182
|
export type QueuedDetails = {
|
|
183
183
|
reason: "worker_profile_changed";
|
|
@@ -1029,6 +1029,8 @@ export interface LanesConfig {
|
|
|
1029
1029
|
runaway_search_guard?: RunawaySearchGuardConfig;
|
|
1030
1030
|
/** Optional free-space threshold override for the low-disk admission gate. */
|
|
1031
1031
|
low_disk_min_free_bytes?: number;
|
|
1032
|
+
/** Claude account slots kept available per host for senior lead lanes. */
|
|
1033
|
+
lead_reserve_per_host: number;
|
|
1032
1034
|
/** Retention ages for Codex session rollout maintenance. */
|
|
1033
1035
|
codex_rollout_compress_after_hours?: number;
|
|
1034
1036
|
codex_rollout_delete_after_days?: number;
|
package/dist/lib/lanes/types.js
CHANGED
package/dist/main.js
CHANGED
|
@@ -362,12 +362,23 @@ function qmdSpawnFailureCaptureContext(err) {
|
|
|
362
362
|
return { contexts: { qmd_spawn_failure: diagnostics } };
|
|
363
363
|
}
|
|
364
364
|
const GATEWAY_RPC_CODE_TAGS = new Map([
|
|
365
|
+
[-32700, "parse_error"],
|
|
366
|
+
[-32601, "method_not_found"],
|
|
365
367
|
[-32603, "internal_error"],
|
|
366
368
|
[-32009, "conflict"],
|
|
367
369
|
[-32003, "unauthorized"],
|
|
368
370
|
[-32602, "invalid_params"],
|
|
369
371
|
[-32050, "provider_error"],
|
|
370
372
|
]);
|
|
373
|
+
const GATEWAY_ERROR_SHAPES = new Set([
|
|
374
|
+
"string_error",
|
|
375
|
+
"object_no_code",
|
|
376
|
+
"object_code",
|
|
377
|
+
"string_code",
|
|
378
|
+
"nested_code",
|
|
379
|
+
"empty",
|
|
380
|
+
"redacted_empty",
|
|
381
|
+
]);
|
|
371
382
|
function integrationsGatewayCaptureContext(err) {
|
|
372
383
|
if (err === null || typeof err !== "object")
|
|
373
384
|
return undefined;
|
|
@@ -386,11 +397,16 @@ function integrationsGatewayCaptureContext(err) {
|
|
|
386
397
|
const codeTag = typeof rpcCode === "number" && Number.isInteger(rpcCode)
|
|
387
398
|
? GATEWAY_RPC_CODE_TAGS.get(rpcCode) ?? "other"
|
|
388
399
|
: rpcCode === undefined ? "missing" : "other";
|
|
400
|
+
const errorShape = record.gatewayErrorShape;
|
|
401
|
+
const errorShapeTag = typeof errorShape === "string" && GATEWAY_ERROR_SHAPES.has(errorShape)
|
|
402
|
+
? errorShape
|
|
403
|
+
: "unknown";
|
|
389
404
|
return {
|
|
390
405
|
tags: {
|
|
391
406
|
integration_gateway_status: statusTag,
|
|
392
407
|
integration_gateway_code: codeTag,
|
|
393
408
|
integration_subcommand: subcommandTag,
|
|
409
|
+
integration_gateway_error_shape: errorShapeTag,
|
|
394
410
|
},
|
|
395
411
|
};
|
|
396
412
|
}
|
|
@@ -57,6 +57,8 @@ export type RunBundledScriptOptions = {
|
|
|
57
57
|
captureOutput?: boolean;
|
|
58
58
|
/** Bytes supplied on stdin. With no value, stdin is inherited as before. */
|
|
59
59
|
stdin?: string | Buffer;
|
|
60
|
+
/** Optional child-process deadline for callers that need bounded script execution. */
|
|
61
|
+
timeoutMs?: number;
|
|
60
62
|
};
|
|
61
63
|
export type RunBundledScriptResult = {
|
|
62
64
|
/** The child's exit code, or 128+signal if it was terminated by one. */
|
|
@@ -47,4 +47,6 @@ export declare function isWindowsBatchFile(command: string, platform?: NodeJS.Pl
|
|
|
47
47
|
* passed through untouched.
|
|
48
48
|
*/
|
|
49
49
|
export declare function planCommandSpawn(command: string, args: readonly string[], platform?: NodeJS.Platform): SpawnPlan;
|
|
50
|
+
/** Spawn a resolved executable, routing only Windows batch shims through the escaped cmd.exe plan. */
|
|
51
|
+
export declare function planResolvedCommandSpawn(command: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
|
|
50
52
|
//# sourceMappingURL=windows-spawn.d.ts.map
|
|
@@ -108,4 +108,10 @@ export function planCommandSpawn(command, args, platform = process.platform) {
|
|
|
108
108
|
return buildSpawnPlan(command, args, platform);
|
|
109
109
|
return { cmd: command, args: [...args], shell: false };
|
|
110
110
|
}
|
|
111
|
+
/** Spawn a resolved executable, routing only Windows batch shims through the escaped cmd.exe plan. */
|
|
112
|
+
export function planResolvedCommandSpawn(command, args, env = process.env, platform = process.platform) {
|
|
113
|
+
if (isWindowsBatchFile(command, platform))
|
|
114
|
+
return buildCmdExeSpawnPlan(command, args, env, platform);
|
|
115
|
+
return { cmd: command, args: [...args], shell: false };
|
|
116
|
+
}
|
|
111
117
|
//# sourceMappingURL=windows-spawn.js.map
|