agent-hitch 0.1.1 → 0.2.1
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/README.md +235 -132
- package/README.zh-CN.md +335 -0
- package/dist/bin/hitch.js +9 -0
- package/dist/bin/hitch.js.map +1 -0
- package/dist/scripts/check-architecture.js +159 -0
- package/dist/scripts/check-architecture.js.map +1 -0
- package/dist/scripts/check-release.js +25 -0
- package/dist/scripts/check-release.js.map +1 -0
- package/dist/scripts/check-syntax.js +32 -0
- package/dist/scripts/check-syntax.js.map +1 -0
- package/dist/src/adapters/catalog.js +55 -0
- package/dist/src/adapters/catalog.js.map +1 -0
- package/dist/src/adapters/contract.js +2 -0
- package/dist/src/adapters/contract.js.map +1 -0
- package/dist/src/adapters/discovery.js +22 -0
- package/dist/src/adapters/discovery.js.map +1 -0
- package/dist/src/adapters/index.js +3 -0
- package/dist/src/adapters/index.js.map +1 -0
- package/dist/src/adapters/providers/claude.js +69 -0
- package/dist/src/adapters/providers/claude.js.map +1 -0
- package/dist/src/adapters/providers/codex.js +74 -0
- package/dist/src/adapters/providers/codex.js.map +1 -0
- package/dist/src/adapters/providers/deepseek.js +51 -0
- package/dist/src/adapters/providers/deepseek.js.map +1 -0
- package/dist/src/adapters/providers/opencode.js +73 -0
- package/dist/src/adapters/providers/opencode.js.map +1 -0
- package/dist/src/adapters/providers/pi.js +82 -0
- package/dist/src/adapters/providers/pi.js.map +1 -0
- package/dist/src/adapters/providers/shared.js +82 -0
- package/dist/src/adapters/providers/shared.js.map +1 -0
- package/dist/src/artifacts/index.js +3 -0
- package/dist/src/artifacts/index.js.map +1 -0
- package/dist/src/artifacts/integrity.js +188 -0
- package/dist/src/artifacts/integrity.js.map +1 -0
- package/dist/src/artifacts/preparer.js +379 -0
- package/dist/src/artifacts/preparer.js.map +1 -0
- package/dist/src/artifacts/store.js +87 -0
- package/dist/src/artifacts/store.js.map +1 -0
- package/dist/src/artifacts/types.js +2 -0
- package/dist/src/artifacts/types.js.map +1 -0
- package/dist/src/backends/contract.js +2 -0
- package/dist/src/backends/contract.js.map +1 -0
- package/dist/src/backends/harbor/backend.js +305 -0
- package/dist/src/backends/harbor/backend.js.map +1 -0
- package/dist/src/backends/harbor/index.js +4 -0
- package/dist/src/backends/harbor/index.js.map +1 -0
- package/dist/src/backends/harbor/local-git-transport.js +447 -0
- package/dist/src/backends/harbor/local-git-transport.js.map +1 -0
- package/dist/src/backends/harbor/tools.js +314 -0
- package/dist/src/backends/harbor/tools.js.map +1 -0
- package/dist/src/backends/index.js +4 -0
- package/dist/src/backends/index.js.map +1 -0
- package/dist/src/cli/arguments.js +123 -0
- package/dist/src/cli/arguments.js.map +1 -0
- package/dist/src/cli/commands/compare.js +55 -0
- package/dist/src/cli/commands/compare.js.map +1 -0
- package/dist/src/cli/commands/daemon.js +106 -0
- package/dist/src/cli/commands/daemon.js.map +1 -0
- package/dist/src/cli/commands/eval.js +126 -0
- package/dist/src/cli/commands/eval.js.map +1 -0
- package/dist/src/cli/commands/feedback.js +84 -0
- package/dist/src/cli/commands/feedback.js.map +1 -0
- package/dist/src/cli/commands/inspect.js +20 -0
- package/dist/src/cli/commands/inspect.js.map +1 -0
- package/dist/src/cli/commands/list.js +17 -0
- package/dist/src/cli/commands/list.js.map +1 -0
- package/dist/src/cli/commands/prepare.js +21 -0
- package/dist/src/cli/commands/prepare.js.map +1 -0
- package/dist/src/cli/commands/resolve.js +17 -0
- package/dist/src/cli/commands/resolve.js.map +1 -0
- package/dist/src/cli/commands/run.js +83 -0
- package/dist/src/cli/commands/run.js.map +1 -0
- package/dist/src/cli/commands/runs.js +45 -0
- package/dist/src/cli/commands/runs.js.map +1 -0
- package/dist/src/cli/commands/trajectory.js +31 -0
- package/dist/src/cli/commands/trajectory.js.map +1 -0
- package/dist/src/cli/commands/workspace.js +44 -0
- package/dist/src/cli/commands/workspace.js.map +1 -0
- package/dist/src/cli/index.js +2 -0
- package/dist/src/cli/index.js.map +1 -0
- package/dist/src/cli/main.js +48 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/output.js +96 -0
- package/dist/src/cli/output.js.map +1 -0
- package/dist/src/controller-runtime/hash.js +353 -0
- package/dist/src/controller-runtime/hash.js.map +1 -0
- package/dist/src/controller-runtime/index.js +3 -0
- package/dist/src/controller-runtime/index.js.map +1 -0
- package/dist/src/controller-runtime/store.js +318 -0
- package/dist/src/controller-runtime/store.js.map +1 -0
- package/dist/src/daemon/auth.js +108 -0
- package/dist/src/daemon/auth.js.map +1 -0
- package/dist/src/daemon/client.js +76 -0
- package/dist/src/daemon/client.js.map +1 -0
- package/dist/src/daemon/index.js +5 -0
- package/dist/src/daemon/index.js.map +1 -0
- package/dist/src/daemon/launcher.js +24 -0
- package/dist/src/daemon/launcher.js.map +1 -0
- package/dist/src/daemon/scheduler.js +161 -0
- package/dist/src/daemon/scheduler.js.map +1 -0
- package/dist/src/daemon/server.js +316 -0
- package/dist/src/daemon/server.js.map +1 -0
- package/dist/src/domain/artifacts.js +2 -0
- package/dist/src/domain/artifacts.js.map +1 -0
- package/dist/src/domain/controller-runtime.js +2 -0
- package/dist/src/domain/controller-runtime.js.map +1 -0
- package/dist/src/domain/eval-records.js +2 -0
- package/dist/src/domain/eval-records.js.map +1 -0
- package/dist/src/domain/evals.js +2 -0
- package/dist/src/domain/evals.js.map +1 -0
- package/dist/src/domain/feedback.js +2 -0
- package/dist/src/domain/feedback.js.map +1 -0
- package/dist/src/domain/ids.js +10 -0
- package/dist/src/domain/ids.js.map +1 -0
- package/dist/src/domain/index.js +11 -0
- package/dist/src/domain/index.js.map +1 -0
- package/dist/src/domain/revisions.js +2 -0
- package/dist/src/domain/revisions.js.map +1 -0
- package/dist/src/domain/runs.js +3 -0
- package/dist/src/domain/runs.js.map +1 -0
- package/dist/src/domain/trajectories.js +2 -0
- package/dist/src/domain/trajectories.js.map +1 -0
- package/dist/src/domain/validation.js +429 -0
- package/dist/src/domain/validation.js.map +1 -0
- package/dist/src/domain/workspaces.js +2 -0
- package/dist/src/domain/workspaces.js.map +1 -0
- package/dist/src/evals/events.js +67 -0
- package/dist/src/evals/events.js.map +1 -0
- package/dist/src/evals/index.js +5 -0
- package/dist/src/evals/index.js.map +1 -0
- package/dist/src/evals/records.js +55 -0
- package/dist/src/evals/records.js.map +1 -0
- package/dist/src/evals/request.js +109 -0
- package/dist/src/evals/request.js.map +1 -0
- package/dist/src/evals/service.js +227 -0
- package/dist/src/evals/service.js.map +1 -0
- package/dist/src/evals/trial-import.js +394 -0
- package/dist/src/evals/trial-import.js.map +1 -0
- package/dist/src/feedback/index.js +2 -0
- package/dist/src/feedback/index.js.map +1 -0
- package/dist/src/feedback/service.js +314 -0
- package/dist/src/feedback/service.js.map +1 -0
- package/dist/src/foundation/config.js +54 -0
- package/dist/src/foundation/config.js.map +1 -0
- package/dist/src/foundation/errors.js +14 -0
- package/dist/src/foundation/errors.js.map +1 -0
- package/dist/src/foundation/executable.js +135 -0
- package/dist/src/foundation/executable.js.map +1 -0
- package/dist/src/foundation/fs.js +45 -0
- package/dist/src/foundation/fs.js.map +1 -0
- package/dist/src/foundation/hash.js +24 -0
- package/dist/src/foundation/hash.js.map +1 -0
- package/dist/src/foundation/index.js +11 -0
- package/dist/src/foundation/index.js.map +1 -0
- package/dist/src/foundation/line-stream.js +20 -0
- package/dist/src/foundation/line-stream.js.map +1 -0
- package/dist/src/foundation/locks.js +114 -0
- package/dist/src/foundation/locks.js.map +1 -0
- package/dist/src/foundation/package-root.js +41 -0
- package/dist/src/foundation/package-root.js.map +1 -0
- package/dist/src/foundation/process.js +50 -0
- package/dist/src/foundation/process.js.map +1 -0
- package/dist/src/revisions/index.js +4 -0
- package/dist/src/revisions/index.js.map +1 -0
- package/dist/src/revisions/reference.js +97 -0
- package/dist/src/revisions/reference.js.map +1 -0
- package/dist/src/revisions/resolver.js +25 -0
- package/dist/src/revisions/resolver.js.map +1 -0
- package/dist/src/revisions/sources/git.js +180 -0
- package/dist/src/revisions/sources/git.js.map +1 -0
- package/dist/src/revisions/sources/installed.js +45 -0
- package/dist/src/revisions/sources/installed.js.map +1 -0
- package/dist/src/revisions/sources/npm.js +97 -0
- package/dist/src/revisions/sources/npm.js.map +1 -0
- package/dist/src/runs/compare.js +132 -0
- package/dist/src/runs/compare.js.map +1 -0
- package/dist/src/runs/events.js +79 -0
- package/dist/src/runs/events.js.map +1 -0
- package/dist/src/runs/executor.js +480 -0
- package/dist/src/runs/executor.js.map +1 -0
- package/dist/src/runs/finalizer.js +22 -0
- package/dist/src/runs/finalizer.js.map +1 -0
- package/dist/src/runs/identity.js +35 -0
- package/dist/src/runs/identity.js.map +1 -0
- package/dist/src/runs/index.js +11 -0
- package/dist/src/runs/index.js.map +1 -0
- package/dist/src/runs/manifest.js +70 -0
- package/dist/src/runs/manifest.js.map +1 -0
- package/dist/src/runs/outcome.js +49 -0
- package/dist/src/runs/outcome.js.map +1 -0
- package/dist/src/runs/query.js +104 -0
- package/dist/src/runs/query.js.map +1 -0
- package/dist/src/runs/queued.js +54 -0
- package/dist/src/runs/queued.js.map +1 -0
- package/dist/src/runs/records.js +288 -0
- package/dist/src/runs/records.js.map +1 -0
- package/dist/src/runs/request.js +169 -0
- package/dist/src/runs/request.js.map +1 -0
- package/dist/src/trajectories/contract.js +17 -0
- package/dist/src/trajectories/contract.js.map +1 -0
- package/dist/src/trajectories/format.js +127 -0
- package/dist/src/trajectories/format.js.map +1 -0
- package/dist/src/trajectories/index.js +7 -0
- package/dist/src/trajectories/index.js.map +1 -0
- package/dist/src/trajectories/projector.js +385 -0
- package/dist/src/trajectories/projector.js.map +1 -0
- package/dist/src/trajectories/provider-capture.js +133 -0
- package/dist/src/trajectories/provider-capture.js.map +1 -0
- package/dist/src/trajectories/providers/deepseek.js +179 -0
- package/dist/src/trajectories/providers/deepseek.js.map +1 -0
- package/dist/src/trajectories/store.js +421 -0
- package/dist/src/trajectories/store.js.map +1 -0
- package/dist/src/workspaces/copy.js +142 -0
- package/dist/src/workspaces/copy.js.map +1 -0
- package/dist/src/workspaces/digest.js +47 -0
- package/dist/src/workspaces/digest.js.map +1 -0
- package/dist/src/workspaces/git.js +164 -0
- package/dist/src/workspaces/git.js.map +1 -0
- package/dist/src/workspaces/index.js +6 -0
- package/dist/src/workspaces/index.js.map +1 -0
- package/dist/src/workspaces/lifecycle.js +313 -0
- package/dist/src/workspaces/lifecycle.js.map +1 -0
- package/dist/src/workspaces/planner.js +80 -0
- package/dist/src/workspaces/planner.js.map +1 -0
- package/dist/src/workspaces/store.js +25 -0
- package/dist/src/workspaces/store.js.map +1 -0
- package/dist/src/workspaces/types.js +4 -0
- package/dist/src/workspaces/types.js.map +1 -0
- package/dist/src/workspaces/utils.js +56 -0
- package/dist/src/workspaces/utils.js.map +1 -0
- package/docs/schemas/controller-runtime-manifest.schema.json +57 -0
- package/docs/schemas/controller-runtime-ref.schema.json +16 -0
- package/docs/schemas/eval-request.schema.json +2 -0
- package/docs/schemas/eval-result.schema.json +36 -1
- package/docs/schemas/local-git-transport-manifest.schema.json +34 -0
- package/docs/schemas/message-feedback.schema.json +37 -0
- package/docs/schemas/result.schema.json +9 -0
- package/docs/schemas/run-context.schema.json +39 -0
- package/docs/schemas/run-manifest.schema.json +81 -0
- package/docs/schemas/run-request.schema.json +16 -1
- package/docs/schemas/trajectory-ref.schema.json +73 -0
- package/integrations/harbor/hitch_harbor_agent.py +551 -5
- package/package.json +37 -11
- package/bin/hitch.js +0 -9
- package/src/adapters.js +0 -435
- package/src/artifacts.js +0 -949
- package/src/cli.js +0 -505
- package/src/config.js +0 -51
- package/src/daemon.js +0 -416
- package/src/engine.js +0 -400
- package/src/errors.js +0 -12
- package/src/eval-tools.js +0 -334
- package/src/evals.js +0 -276
- package/src/events.js +0 -69
- package/src/fs.js +0 -43
- package/src/harbor-backend.js +0 -285
- package/src/harness-reference.js +0 -78
- package/src/line-stream.js +0 -17
- package/src/locks.js +0 -33
- package/src/process.js +0 -49
- package/src/registry.js +0 -84
- package/src/scheduler.js +0 -156
- package/src/workspaces.js +0 -893
package/src/fs.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export async function ensureDir(directory) {
|
|
6
|
-
await mkdir(directory, { recursive: true });
|
|
7
|
-
return directory;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function readJSON(file, fallback = undefined) {
|
|
11
|
-
try {
|
|
12
|
-
return JSON.parse(await readFile(file, "utf8"));
|
|
13
|
-
} catch (error) {
|
|
14
|
-
if (error?.code === "ENOENT" && fallback !== undefined) return fallback;
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function atomicWriteJSON(file, value, mode = 0o600) {
|
|
20
|
-
await ensureDir(path.dirname(file));
|
|
21
|
-
const temporary = `${file}.${process.pid}.${Date.now()}.${randomBytes(6).toString("hex")}.tmp`;
|
|
22
|
-
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode });
|
|
23
|
-
await rename(temporary, file);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function appendLine(file, value) {
|
|
27
|
-
await ensureDir(path.dirname(file));
|
|
28
|
-
const handle = await open(file, "a", 0o600);
|
|
29
|
-
try {
|
|
30
|
-
await handle.write(`${value}\n`);
|
|
31
|
-
} finally {
|
|
32
|
-
await handle.close();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export async function removeIfExists(file) {
|
|
37
|
-
await rm(file, { force: true });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function writePrivateFile(file, value) {
|
|
41
|
-
await ensureDir(path.dirname(file));
|
|
42
|
-
await writeFile(file, value, { mode: 0o600 });
|
|
43
|
-
}
|
package/src/harbor-backend.js
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { createWriteStream } from "node:fs";
|
|
3
|
-
import { readdir, stat } from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { HitchError, invalidInput } from "./errors.js";
|
|
7
|
-
import { atomicWriteJSON, ensureDir, readJSON } from "./fs.js";
|
|
8
|
-
import { consumeLines } from "./line-stream.js";
|
|
9
|
-
import { terminateProcess } from "./process.js";
|
|
10
|
-
import { detectVersion, fingerprintExecutable } from "./registry.js";
|
|
11
|
-
import { HARBOR_CREDENTIAL_ENV, locateHarbor } from "./eval-tools.js";
|
|
12
|
-
|
|
13
|
-
const BRIDGE_DIRECTORY = fileURLToPath(new URL("../integrations/harbor", import.meta.url));
|
|
14
|
-
export async function runHarborBackend({
|
|
15
|
-
evalDirectory,
|
|
16
|
-
request,
|
|
17
|
-
root,
|
|
18
|
-
resolvedRevision,
|
|
19
|
-
runtimeDirectory,
|
|
20
|
-
env = process.env,
|
|
21
|
-
harborExecutable,
|
|
22
|
-
signal,
|
|
23
|
-
emit = () => {},
|
|
24
|
-
}) {
|
|
25
|
-
const backendDirectory = await ensureDir(path.join(evalDirectory, "harbor"));
|
|
26
|
-
const executable = await discoverHarbor(harborExecutable, root, env);
|
|
27
|
-
const version = await detectVersion(executable, ["--version"]);
|
|
28
|
-
const identity = await fingerprintExecutable(executable);
|
|
29
|
-
const jobName = "job";
|
|
30
|
-
const configPath = path.join(backendDirectory, "job.json");
|
|
31
|
-
const jobDirectory = path.join(backendDirectory, jobName);
|
|
32
|
-
const resultPath = path.join(jobDirectory, "result.json");
|
|
33
|
-
const config = await buildHarborJobConfig({
|
|
34
|
-
request,
|
|
35
|
-
resolvedRevision,
|
|
36
|
-
runtimeDirectory,
|
|
37
|
-
backendDirectory,
|
|
38
|
-
jobName,
|
|
39
|
-
env,
|
|
40
|
-
});
|
|
41
|
-
await atomicWriteJSON(configPath, config);
|
|
42
|
-
emit({ type: "eval.backend.started", backend: "harbor", executable, version: version || null });
|
|
43
|
-
|
|
44
|
-
const invocation = await invokeHarbor(executable, ["run", "--config", configPath, "--yes"], {
|
|
45
|
-
cwd: evalDirectory,
|
|
46
|
-
env: withBridgePythonPath(env),
|
|
47
|
-
stdoutPath: path.join(backendDirectory, "stdout.log"),
|
|
48
|
-
stderrPath: path.join(backendDirectory, "stderr.log"),
|
|
49
|
-
signal,
|
|
50
|
-
emit,
|
|
51
|
-
});
|
|
52
|
-
const jobResult = await readJSON(resultPath, null);
|
|
53
|
-
const rawResult = jobResult ? await attachTrialResults(jobDirectory, jobResult) : null;
|
|
54
|
-
emit({
|
|
55
|
-
type: "eval.backend.completed",
|
|
56
|
-
backend: "harbor",
|
|
57
|
-
process_exit_code: invocation.code,
|
|
58
|
-
signal: invocation.signal,
|
|
59
|
-
result_available: rawResult !== null,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
backend: {
|
|
64
|
-
name: "harbor",
|
|
65
|
-
executable,
|
|
66
|
-
version: version || null,
|
|
67
|
-
identity,
|
|
68
|
-
config_path: configPath,
|
|
69
|
-
result_path: rawResult ? resultPath : null,
|
|
70
|
-
stdout_path: path.join(backendDirectory, "stdout.log"),
|
|
71
|
-
stderr_path: path.join(backendDirectory, "stderr.log"),
|
|
72
|
-
process_exit_code: invocation.code,
|
|
73
|
-
signal: invocation.signal,
|
|
74
|
-
},
|
|
75
|
-
rawResult,
|
|
76
|
-
summary: rawResult ? normalizeHarborResult(rawResult) : null,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async function attachTrialResults(jobDirectory, jobResult) {
|
|
81
|
-
if (Array.isArray(jobResult.trial_results)) return jobResult;
|
|
82
|
-
let entries;
|
|
83
|
-
try {
|
|
84
|
-
entries = await readdir(jobDirectory, { withFileTypes: true });
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (error?.code === "ENOENT") return jobResult;
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
const trialResults = [];
|
|
90
|
-
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
91
|
-
if (!entry.isDirectory()) continue;
|
|
92
|
-
const trial = await readJSON(path.join(jobDirectory, entry.name, "result.json"), null);
|
|
93
|
-
if (trial?.trial_name && trial?.task_name) trialResults.push(trial);
|
|
94
|
-
}
|
|
95
|
-
return { ...jobResult, trial_results: trialResults };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function buildHarborJobConfig({
|
|
99
|
-
request,
|
|
100
|
-
resolvedRevision,
|
|
101
|
-
runtimeDirectory,
|
|
102
|
-
backendDirectory,
|
|
103
|
-
jobName = "job",
|
|
104
|
-
env = process.env,
|
|
105
|
-
}) {
|
|
106
|
-
const timeoutSeconds = request.timeout_ms > 0 ? Math.ceil(request.timeout_ms / 1_000) : null;
|
|
107
|
-
const setupTimeoutSeconds = request.setup_timeout_ms > 0 ? Math.ceil(request.setup_timeout_ms / 1_000) : null;
|
|
108
|
-
const agent = {
|
|
109
|
-
import_path: "hitch_harbor_agent:HitchHarborAgent",
|
|
110
|
-
model_name: request.model || null,
|
|
111
|
-
kwargs: {
|
|
112
|
-
candidate_id: "candidate-1",
|
|
113
|
-
harness_ref: lockedHarnessRef(resolvedRevision),
|
|
114
|
-
revision_identity: resolvedRevision.identity,
|
|
115
|
-
hitch_runtime_dir: runtimeDirectory,
|
|
116
|
-
hitch_timeout_ms: request.timeout_ms,
|
|
117
|
-
agent_args: request.agent_args,
|
|
118
|
-
workdir: "/app",
|
|
119
|
-
},
|
|
120
|
-
env: credentialEnvironment(request.pass_env, env),
|
|
121
|
-
include_logs: ["hitch-*"],
|
|
122
|
-
};
|
|
123
|
-
if (timeoutSeconds !== null) agent.override_timeout_sec = timeoutSeconds + 30;
|
|
124
|
-
if (setupTimeoutSeconds !== null) agent.override_setup_timeout_sec = setupTimeoutSeconds;
|
|
125
|
-
|
|
126
|
-
return compact({
|
|
127
|
-
job_name: jobName,
|
|
128
|
-
jobs_dir: backendDirectory,
|
|
129
|
-
n_attempts: request.attempts,
|
|
130
|
-
n_concurrent_trials: request.max_concurrent,
|
|
131
|
-
environment: { type: "docker", delete: true },
|
|
132
|
-
agents: [agent],
|
|
133
|
-
datasets: [await datasetConfig(request.dataset)],
|
|
134
|
-
tasks: [],
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function lockedHarnessRef(resolvedRevision) {
|
|
139
|
-
if (resolvedRevision?.revision?.type === "version" && resolvedRevision.revision.version) {
|
|
140
|
-
return `${resolvedRevision.harness_id}@version:${resolvedRevision.revision.version}`;
|
|
141
|
-
}
|
|
142
|
-
if (resolvedRevision?.revision?.type === "commit" && resolvedRevision.revision.commit) {
|
|
143
|
-
return `${resolvedRevision.harness_id}@commit:${resolvedRevision.revision.commit}`;
|
|
144
|
-
}
|
|
145
|
-
throw invalidInput("Harbor eval requires a resolved exact version or commit");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function normalizeHarborResult(raw) {
|
|
149
|
-
const trials = Array.isArray(raw?.trial_results) ? raw.trial_results : [];
|
|
150
|
-
const rewardValues = new Map();
|
|
151
|
-
const normalizedTrials = trials.map((trial) => {
|
|
152
|
-
const rewards = trial?.verifier_result?.rewards || {};
|
|
153
|
-
for (const [name, value] of Object.entries(rewards)) {
|
|
154
|
-
if (typeof value !== "number" || !Number.isFinite(value)) continue;
|
|
155
|
-
const values = rewardValues.get(name) || [];
|
|
156
|
-
values.push(value);
|
|
157
|
-
rewardValues.set(name, values);
|
|
158
|
-
}
|
|
159
|
-
return {
|
|
160
|
-
task_name: trial?.task_name || null,
|
|
161
|
-
trial_name: trial?.trial_name || null,
|
|
162
|
-
status: trial?.exception_info ? "errored" : "completed",
|
|
163
|
-
rewards,
|
|
164
|
-
exception: trial?.exception_info || null,
|
|
165
|
-
};
|
|
166
|
-
});
|
|
167
|
-
const rewards = Object.fromEntries([...rewardValues.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([name, values]) => [name, {
|
|
168
|
-
count: values.length,
|
|
169
|
-
mean: values.reduce((sum, value) => sum + value, 0) / values.length,
|
|
170
|
-
min: Math.min(...values),
|
|
171
|
-
max: Math.max(...values),
|
|
172
|
-
}]));
|
|
173
|
-
const primary = rewards.reward || Object.values(rewards)[0] || null;
|
|
174
|
-
const stats = raw?.stats || {};
|
|
175
|
-
return {
|
|
176
|
-
n_trials: raw?.n_total_trials ?? trials.length,
|
|
177
|
-
n_completed: stats.n_completed_trials ?? normalizedTrials.filter((trial) => trial.status === "completed").length,
|
|
178
|
-
n_errored: stats.n_errored_trials ?? normalizedTrials.filter((trial) => trial.status === "errored").length,
|
|
179
|
-
n_cancelled: stats.n_cancelled_trials ?? 0,
|
|
180
|
-
primary_reward: primary?.mean ?? null,
|
|
181
|
-
rewards,
|
|
182
|
-
trials: normalizedTrials,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function discoverHarbor(explicit, root, env) {
|
|
187
|
-
const located = await locateHarbor({ root, explicit, env });
|
|
188
|
-
if (!located.executable) {
|
|
189
|
-
throw new HitchError(`Harbor executable not found: ${located.requested}`, {
|
|
190
|
-
code: "harbor_unavailable",
|
|
191
|
-
exitCode: 3,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
return located.executable;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
async function datasetConfig(value) {
|
|
198
|
-
if (typeof value !== "string" || !value.trim()) throw invalidInput("dataset must be a non-empty string");
|
|
199
|
-
const raw = value.trim();
|
|
200
|
-
const localPath = path.resolve(raw);
|
|
201
|
-
try {
|
|
202
|
-
if ((await stat(localPath)).isDirectory()) return { path: localPath };
|
|
203
|
-
} catch (error) {
|
|
204
|
-
if (error?.code !== "ENOENT") throw error;
|
|
205
|
-
}
|
|
206
|
-
const separator = raw.lastIndexOf("@");
|
|
207
|
-
const name = separator > 0 ? raw.slice(0, separator) : raw;
|
|
208
|
-
const version = separator > 0 ? raw.slice(separator + 1) : "";
|
|
209
|
-
if (!name) throw invalidInput(`invalid Harbor dataset: ${value}`);
|
|
210
|
-
return name.includes("/")
|
|
211
|
-
? { name, ref: version || "latest" }
|
|
212
|
-
: compact({ name, version: version || undefined });
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function credentialEnvironment(explicitNames, env) {
|
|
216
|
-
const names = new Set(HARBOR_CREDENTIAL_ENV.filter((name) => env[name] !== undefined));
|
|
217
|
-
for (const name of explicitNames || []) {
|
|
218
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw invalidInput(`invalid environment variable name: ${name}`);
|
|
219
|
-
if (env[name] === undefined) throw invalidInput(`environment variable is not set: ${name}`);
|
|
220
|
-
names.add(name);
|
|
221
|
-
}
|
|
222
|
-
return Object.fromEntries([...names].sort().map((name) => [name, `\${${name}}`]));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function withBridgePythonPath(env) {
|
|
226
|
-
return {
|
|
227
|
-
...env,
|
|
228
|
-
PYTHONPATH: [BRIDGE_DIRECTORY, env.PYTHONPATH].filter(Boolean).join(path.delimiter),
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function invokeHarbor(executable, args, { cwd, env, stdoutPath, stderrPath, signal, emit }) {
|
|
233
|
-
return new Promise((resolve, reject) => {
|
|
234
|
-
const stdout = createWriteStream(stdoutPath, { flags: "w", mode: 0o600 });
|
|
235
|
-
const stderr = createWriteStream(stderrPath, { flags: "w", mode: 0o600 });
|
|
236
|
-
const child = spawn(executable, args, {
|
|
237
|
-
cwd,
|
|
238
|
-
env,
|
|
239
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
240
|
-
detached: process.platform !== "win32",
|
|
241
|
-
windowsHide: true,
|
|
242
|
-
});
|
|
243
|
-
child.stdout.pipe(stdout);
|
|
244
|
-
child.stderr.pipe(stderr);
|
|
245
|
-
consumeLines(child.stdout, (line) => emit({ type: "eval.backend.output", stream: "stdout", text: line }));
|
|
246
|
-
consumeLines(child.stderr, (line) => emit({ type: "eval.backend.output", stream: "stderr", text: line }));
|
|
247
|
-
let settled = false;
|
|
248
|
-
const abort = () => terminateProcess(child).catch(() => {});
|
|
249
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
250
|
-
child.once("error", (error) => {
|
|
251
|
-
if (settled) return;
|
|
252
|
-
settled = true;
|
|
253
|
-
signal?.removeEventListener("abort", abort);
|
|
254
|
-
stdout.destroy();
|
|
255
|
-
stderr.destroy();
|
|
256
|
-
reject(new HitchError(`failed to launch Harbor: ${error.message}`, {
|
|
257
|
-
code: "harbor_launch_failed",
|
|
258
|
-
exitCode: 6,
|
|
259
|
-
cause: error,
|
|
260
|
-
}));
|
|
261
|
-
});
|
|
262
|
-
child.once("close", (code, processSignal) => {
|
|
263
|
-
if (settled) return;
|
|
264
|
-
settled = true;
|
|
265
|
-
signal?.removeEventListener("abort", abort);
|
|
266
|
-
Promise.all([closeWriteStream(stdout), closeWriteStream(stderr)])
|
|
267
|
-
.then(() => resolve({ code, signal: processSignal }))
|
|
268
|
-
.catch(reject);
|
|
269
|
-
});
|
|
270
|
-
if (signal?.aborted) abort();
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function closeWriteStream(stream) {
|
|
275
|
-
return new Promise((resolve, reject) => {
|
|
276
|
-
if (stream.closed) return resolve();
|
|
277
|
-
stream.once("error", reject);
|
|
278
|
-
stream.once("close", resolve);
|
|
279
|
-
stream.end();
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function compact(value) {
|
|
284
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== null));
|
|
285
|
-
}
|
package/src/harness-reference.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { invalidInput } from "./errors.js";
|
|
4
|
-
|
|
5
|
-
const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6
|
-
const COMMIT_PATTERN = /^[0-9a-fA-F]{7,64}$/;
|
|
7
|
-
const HARNESS_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
8
|
-
|
|
9
|
-
export function parseHarnessReference(value) {
|
|
10
|
-
if (typeof value !== "string" || !value.trim()) throw invalidInput("harness reference must be a non-empty string");
|
|
11
|
-
const raw = value.trim();
|
|
12
|
-
const separator = raw.indexOf("@");
|
|
13
|
-
const harnessId = separator < 0 ? raw : raw.slice(0, separator);
|
|
14
|
-
const revision = separator < 0 ? "installed" : raw.slice(separator + 1);
|
|
15
|
-
if (!HARNESS_ID_PATTERN.test(harnessId)) throw invalidInput(`invalid harness name: ${harnessId || raw}`);
|
|
16
|
-
if (!revision) throw invalidInput(`harness reference is missing a selector: ${raw}`);
|
|
17
|
-
|
|
18
|
-
if (revision === "installed") {
|
|
19
|
-
return canonicalReference(raw, harnessId, { type: "installed" });
|
|
20
|
-
}
|
|
21
|
-
if (revision.startsWith("version:")) {
|
|
22
|
-
const version = revision.slice("version:".length);
|
|
23
|
-
if (!VERSION_PATTERN.test(version)) {
|
|
24
|
-
throw invalidInput(`version selector must be an exact semantic version: ${version || "(empty)"}`);
|
|
25
|
-
}
|
|
26
|
-
return canonicalReference(raw, harnessId, { type: "version", value: version });
|
|
27
|
-
}
|
|
28
|
-
if (revision.startsWith("commit:")) {
|
|
29
|
-
const commit = revision.slice("commit:".length);
|
|
30
|
-
assertCommit(commit);
|
|
31
|
-
return canonicalReference(raw, harnessId, { type: "commit", value: commit.toLowerCase() });
|
|
32
|
-
}
|
|
33
|
-
if (revision.startsWith("git+")) {
|
|
34
|
-
const hash = revision.lastIndexOf("#");
|
|
35
|
-
if (hash < 0) throw invalidInput("explicit Git references require #<commit>");
|
|
36
|
-
const sourceValue = revision.slice("git+".length, hash);
|
|
37
|
-
const commit = revision.slice(hash + 1);
|
|
38
|
-
assertCommit(commit);
|
|
39
|
-
let source;
|
|
40
|
-
try {
|
|
41
|
-
source = new URL(sourceValue);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
throw invalidInput(`invalid Git source URL: ${sourceValue}`, { cause: error });
|
|
44
|
-
}
|
|
45
|
-
if (source.protocol !== "file:") {
|
|
46
|
-
throw invalidInput("explicit Git sources are limited to file:// URLs; use the registered source for remote commits");
|
|
47
|
-
}
|
|
48
|
-
const localPath = path.resolve(fileURLToPath(source));
|
|
49
|
-
const sourceUrl = pathToFileURL(localPath).href;
|
|
50
|
-
return canonicalReference(raw, harnessId, {
|
|
51
|
-
type: "commit",
|
|
52
|
-
value: commit.toLowerCase(),
|
|
53
|
-
source: { type: "git", url: sourceUrl, local_path: localPath, explicit: true },
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
throw invalidInput(`unsupported harness selector in ${raw}; use installed, version:<exact>, or commit:<sha>`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function canonicalReference(raw, harnessId, selector) {
|
|
61
|
-
const suffix = selector.type === "installed"
|
|
62
|
-
? "installed"
|
|
63
|
-
: selector.source
|
|
64
|
-
? `git+${selector.source.url}#${selector.value}`
|
|
65
|
-
: `${selector.type}:${selector.value}`;
|
|
66
|
-
return {
|
|
67
|
-
raw,
|
|
68
|
-
canonical: `${harnessId}@${suffix}`,
|
|
69
|
-
harness_id: harnessId,
|
|
70
|
-
selector,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function assertCommit(value) {
|
|
75
|
-
if (!COMMIT_PATTERN.test(value || "")) {
|
|
76
|
-
throw invalidInput(`commit selector must be a 7-64 character hexadecimal ID: ${value || "(empty)"}`);
|
|
77
|
-
}
|
|
78
|
-
}
|
package/src/line-stream.js
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export function consumeLines(stream, onLine) {
|
|
2
|
-
let buffer = "";
|
|
3
|
-
stream.setEncoding("utf8");
|
|
4
|
-
stream.on("data", (chunk) => {
|
|
5
|
-
buffer += chunk;
|
|
6
|
-
for (;;) {
|
|
7
|
-
const newline = buffer.indexOf("\n");
|
|
8
|
-
if (newline < 0) break;
|
|
9
|
-
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
10
|
-
buffer = buffer.slice(newline + 1);
|
|
11
|
-
onLine(line);
|
|
12
|
-
}
|
|
13
|
-
});
|
|
14
|
-
stream.on("end", () => {
|
|
15
|
-
if (buffer.length > 0) onLine(buffer.replace(/\r$/, ""));
|
|
16
|
-
});
|
|
17
|
-
}
|
package/src/locks.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { open, readFile, rm } from "node:fs/promises";
|
|
3
|
-
|
|
4
|
-
// Serializes stale-lock reclamation so an observation about an old owner can
|
|
5
|
-
// never be used to remove a lock that a new owner has just acquired.
|
|
6
|
-
export async function reclaimStaleLock(file, isStale) {
|
|
7
|
-
const guard = `${file}.reclaim`;
|
|
8
|
-
const owner = randomBytes(12).toString("hex");
|
|
9
|
-
let handle;
|
|
10
|
-
try {
|
|
11
|
-
handle = await open(guard, "wx", 0o600);
|
|
12
|
-
} catch (error) {
|
|
13
|
-
if (error?.code === "EEXIST") return false;
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
await handle.writeFile(`${JSON.stringify({ owner, pid: process.pid, created_at: new Date().toISOString() })}\n`);
|
|
19
|
-
await handle.sync();
|
|
20
|
-
if (!await isStale(file)) return false;
|
|
21
|
-
await rm(file, { force: true });
|
|
22
|
-
return true;
|
|
23
|
-
} finally {
|
|
24
|
-
await handle.close().catch(() => {});
|
|
25
|
-
let current;
|
|
26
|
-
try {
|
|
27
|
-
current = JSON.parse(await readFile(guard, "utf8"));
|
|
28
|
-
} catch {
|
|
29
|
-
current = null;
|
|
30
|
-
}
|
|
31
|
-
if (current?.owner === owner) await rm(guard, { force: true });
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/process.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export async function terminateProcess(child, graceMs = 3_000) {
|
|
4
|
-
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
5
|
-
|
|
6
|
-
if (process.platform === "win32") {
|
|
7
|
-
const killer = spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
8
|
-
stdio: "ignore",
|
|
9
|
-
windowsHide: true,
|
|
10
|
-
});
|
|
11
|
-
await waitForExitOrTimeout(killer, graceMs);
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
process.kill(-child.pid, "SIGTERM");
|
|
17
|
-
} catch (error) {
|
|
18
|
-
if (error?.code !== "ESRCH") throw error;
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
await waitForExitOrTimeout(child, graceMs);
|
|
23
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
24
|
-
try {
|
|
25
|
-
process.kill(-child.pid, "SIGKILL");
|
|
26
|
-
} catch (error) {
|
|
27
|
-
if (error?.code !== "ESRCH") throw error;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function delay(milliseconds) {
|
|
33
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function waitForExitOrTimeout(child, milliseconds) {
|
|
37
|
-
return new Promise((resolve) => {
|
|
38
|
-
let settled = false;
|
|
39
|
-
const finish = () => {
|
|
40
|
-
if (settled) return;
|
|
41
|
-
settled = true;
|
|
42
|
-
clearTimeout(timer);
|
|
43
|
-
child.removeListener("exit", finish);
|
|
44
|
-
resolve();
|
|
45
|
-
};
|
|
46
|
-
const timer = setTimeout(finish, milliseconds);
|
|
47
|
-
child.once("exit", finish);
|
|
48
|
-
});
|
|
49
|
-
}
|
package/src/registry.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { constants, createReadStream } from "node:fs";
|
|
2
|
-
import { access, lstat, realpath } from "node:fs/promises";
|
|
3
|
-
import { delimiter, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { getAdapter, listDefinitions, publicDefinition } from "./adapters.js";
|
|
7
|
-
|
|
8
|
-
export async function discoverAgents({ env = process.env, probeVersions = true } = {}) {
|
|
9
|
-
return Promise.all(listDefinitions().map((definition) => inspectDefinition(definition, { env, probeVersions })));
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function inspectAgent(id, { env = process.env, probeVersions = true } = {}) {
|
|
13
|
-
const definition = publicDefinition(getAdapter(id));
|
|
14
|
-
return inspectDefinition(definition, { env, probeVersions });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function inspectDefinition(definition, { env, probeVersions }) {
|
|
18
|
-
const configured = env[definition.path_env]?.trim() || definition.command;
|
|
19
|
-
const executable = await resolveExecutable(configured, env.PATH || "");
|
|
20
|
-
if (!executable) return { ...definition, status: "unavailable" };
|
|
21
|
-
|
|
22
|
-
const version = probeVersions
|
|
23
|
-
? await detectVersion(executable, getAdapter(definition.id).version_args)
|
|
24
|
-
: "";
|
|
25
|
-
const identity = await fingerprintExecutable(executable);
|
|
26
|
-
return { ...definition, status: "available", executable, version, identity };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function resolveExecutable(command, searchPath) {
|
|
30
|
-
const candidates = isAbsolute(command) || command.includes("/") || command.includes("\\")
|
|
31
|
-
? [resolve(command)]
|
|
32
|
-
: searchPath.split(delimiter).filter(Boolean).map((directory) => join(directory, command));
|
|
33
|
-
|
|
34
|
-
for (const candidate of candidates) {
|
|
35
|
-
try {
|
|
36
|
-
await access(candidate, constants.X_OK);
|
|
37
|
-
const info = await lstat(candidate);
|
|
38
|
-
if (info.isDirectory()) continue;
|
|
39
|
-
return await realpath(candidate);
|
|
40
|
-
} catch {
|
|
41
|
-
// Keep probing PATH entries.
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export async function detectVersion(executable, args, timeoutMs = 5_000) {
|
|
48
|
-
return new Promise((resolveVersion) => {
|
|
49
|
-
const child = spawn(executable, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
50
|
-
let stdout = "";
|
|
51
|
-
let stderr = "";
|
|
52
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
53
|
-
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
|
54
|
-
child.stderr.on("data", (chunk) => { stderr += chunk; });
|
|
55
|
-
child.once("error", () => {
|
|
56
|
-
clearTimeout(timer);
|
|
57
|
-
resolveVersion("");
|
|
58
|
-
});
|
|
59
|
-
child.once("close", () => {
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
resolveVersion(selectVersionLine(stdout, stderr));
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function fingerprintExecutable(executable) {
|
|
67
|
-
const hash = createHash("sha256");
|
|
68
|
-
for await (const chunk of createReadStream(executable)) hash.update(chunk);
|
|
69
|
-
const digest = hash.digest("hex");
|
|
70
|
-
return `sha256:${digest}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function selectVersionLine(stdout, stderr) {
|
|
74
|
-
const streams = [stdout, stderr];
|
|
75
|
-
const versionPattern = /(?:^|\s)v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?(?:\s|$)/;
|
|
76
|
-
for (const stream of streams) {
|
|
77
|
-
const match = String(stream || "")
|
|
78
|
-
.split(/\r?\n/)
|
|
79
|
-
.map((line) => line.trim())
|
|
80
|
-
.find((line) => versionPattern.test(line));
|
|
81
|
-
if (match) return match;
|
|
82
|
-
}
|
|
83
|
-
return "";
|
|
84
|
-
}
|