agent-hitch 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +229 -132
  2. package/README.zh-CN.md +330 -0
  3. package/dist/bin/hitch.js +9 -0
  4. package/dist/bin/hitch.js.map +1 -0
  5. package/dist/scripts/check-release.js +25 -0
  6. package/dist/scripts/check-release.js.map +1 -0
  7. package/dist/scripts/check-syntax.js +32 -0
  8. package/dist/scripts/check-syntax.js.map +1 -0
  9. package/dist/src/adapters.js +461 -0
  10. package/dist/src/adapters.js.map +1 -0
  11. package/dist/src/artifacts.js +1081 -0
  12. package/dist/src/artifacts.js.map +1 -0
  13. package/dist/src/cli.js +699 -0
  14. package/dist/src/cli.js.map +1 -0
  15. package/dist/src/config.js +53 -0
  16. package/dist/src/config.js.map +1 -0
  17. package/dist/src/controller-runtime/hash.js +353 -0
  18. package/dist/src/controller-runtime/hash.js.map +1 -0
  19. package/dist/src/controller-runtime/store.js +323 -0
  20. package/dist/src/controller-runtime/store.js.map +1 -0
  21. package/dist/src/daemon.js +461 -0
  22. package/dist/src/daemon.js.map +1 -0
  23. package/dist/src/domain/types.js +10 -0
  24. package/dist/src/domain/types.js.map +1 -0
  25. package/dist/src/domain/validate.js +240 -0
  26. package/dist/src/domain/validate.js.map +1 -0
  27. package/dist/src/engine.js +465 -0
  28. package/dist/src/engine.js.map +1 -0
  29. package/dist/src/errors.js +14 -0
  30. package/dist/src/errors.js.map +1 -0
  31. package/dist/src/eval-tools.js +318 -0
  32. package/dist/src/eval-tools.js.map +1 -0
  33. package/dist/src/evals.js +313 -0
  34. package/dist/src/evals.js.map +1 -0
  35. package/dist/src/events.js +80 -0
  36. package/dist/src/events.js.map +1 -0
  37. package/dist/src/feedback/service.js +315 -0
  38. package/dist/src/feedback/service.js.map +1 -0
  39. package/dist/src/fs.js +45 -0
  40. package/dist/src/fs.js.map +1 -0
  41. package/dist/src/harbor-backend.js +280 -0
  42. package/dist/src/harbor-backend.js.map +1 -0
  43. package/dist/src/harness-reference.js +83 -0
  44. package/dist/src/harness-reference.js.map +1 -0
  45. package/dist/src/line-stream.js +20 -0
  46. package/dist/src/line-stream.js.map +1 -0
  47. package/dist/src/locks.js +38 -0
  48. package/dist/src/locks.js.map +1 -0
  49. package/dist/src/package-root.js +40 -0
  50. package/dist/src/package-root.js.map +1 -0
  51. package/dist/src/process.js +50 -0
  52. package/dist/src/process.js.map +1 -0
  53. package/dist/src/registry.js +81 -0
  54. package/dist/src/registry.js.map +1 -0
  55. package/dist/src/scheduler.js +162 -0
  56. package/dist/src/scheduler.js.map +1 -0
  57. package/dist/src/trajectories/contract.js +17 -0
  58. package/dist/src/trajectories/contract.js.map +1 -0
  59. package/dist/src/trajectories/format.js +127 -0
  60. package/dist/src/trajectories/format.js.map +1 -0
  61. package/dist/src/trajectories/projector.js +385 -0
  62. package/dist/src/trajectories/projector.js.map +1 -0
  63. package/dist/src/trajectories/store.js +281 -0
  64. package/dist/src/trajectories/store.js.map +1 -0
  65. package/dist/src/workspaces.js +858 -0
  66. package/dist/src/workspaces.js.map +1 -0
  67. package/docs/schemas/controller-runtime-manifest.schema.json +57 -0
  68. package/docs/schemas/controller-runtime-ref.schema.json +16 -0
  69. package/docs/schemas/message-feedback.schema.json +37 -0
  70. package/docs/schemas/trajectory-ref.schema.json +30 -0
  71. package/integrations/harbor/hitch_harbor_agent.py +176 -4
  72. package/package.json +19 -11
  73. package/bin/hitch.js +0 -9
  74. package/src/adapters.js +0 -435
  75. package/src/artifacts.js +0 -949
  76. package/src/cli.js +0 -505
  77. package/src/config.js +0 -51
  78. package/src/daemon.js +0 -416
  79. package/src/engine.js +0 -400
  80. package/src/errors.js +0 -12
  81. package/src/eval-tools.js +0 -334
  82. package/src/evals.js +0 -276
  83. package/src/events.js +0 -69
  84. package/src/fs.js +0 -43
  85. package/src/harbor-backend.js +0 -285
  86. package/src/harness-reference.js +0 -78
  87. package/src/line-stream.js +0 -17
  88. package/src/locks.js +0 -33
  89. package/src/process.js +0 -49
  90. package/src/registry.js +0 -84
  91. package/src/scheduler.js +0 -156
  92. package/src/workspaces.js +0 -893
package/src/cli.js DELETED
@@ -1,505 +0,0 @@
1
- import { openSync, readFileSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { spawn } from "node:child_process";
6
- import { DaemonServer, daemonClient } from "./daemon.js";
7
- import { defaultRoot, DEFAULT_MAX_CONCURRENT, DEFAULT_PORT, parseDuration, positiveInteger, SCHEMA_VERSION, statePaths } from "./config.js";
8
- import { discoverAgents, inspectAgent } from "./registry.js";
9
- import { executeRun, newRunId } from "./engine.js";
10
- import { inspectEval, listEvals, runEval } from "./evals.js";
11
- import { DEFAULT_HARBOR_VERSION, doctorHarbor, setupHarbor } from "./eval-tools.js";
12
- import { HitchError, invalidInput } from "./errors.js";
13
- import { delay } from "./process.js";
14
- import { ensureDir, readJSON } from "./fs.js";
15
- import { listPreparedArtifacts, prepareHarness, resolveHarness } from "./artifacts.js";
16
- import { inspectWorkspace, removeWorkspace, WORKSPACE_MODES } from "./workspaces.js";
17
-
18
- const executable = fileURLToPath(new URL("../bin/hitch.js", import.meta.url));
19
- const packageVersion = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
20
-
21
- export async function main(argv) {
22
- const args = [...argv];
23
- const root = path.resolve(takeOption(args, "--root") || defaultRoot());
24
- const command = args.shift();
25
-
26
- switch (command) {
27
- case "list": return listCommand(args);
28
- case "inspect": return inspectCommand(args, root);
29
- case "resolve": return resolveCommand(args, root);
30
- case "prepare": return prepareCommand(args, root);
31
- case "run": return runCommand(args, root);
32
- case "eval": return evalCommand(args, root);
33
- case "workspace": return workspaceCommand(args, root);
34
- case "daemon": return daemonCommand(args, root);
35
- case "help":
36
- case "--help":
37
- case "-h":
38
- case undefined:
39
- process.stdout.write(helpText());
40
- return;
41
- case "--version":
42
- case "-V":
43
- process.stdout.write(`hitch ${packageVersion}\n`);
44
- return;
45
- default:
46
- throw invalidInput(`unknown command: ${command}`);
47
- }
48
- }
49
-
50
- async function listCommand(args) {
51
- const json = takeFlag(args, "--json");
52
- assertNoArgs(args);
53
- const agents = await discoverAgents();
54
- if (json) {
55
- process.stdout.write(`${JSON.stringify({ schema_version: SCHEMA_VERSION, harnesses: agents }, null, 2)}\n`);
56
- return;
57
- }
58
- for (const agent of agents) {
59
- const detail = agent.status === "available" ? `${agent.version || "version unknown"} ${agent.executable}` : "not installed";
60
- process.stdout.write(`${agent.id.padEnd(10)} ${agent.status.padEnd(11)} ${detail}\n`);
61
- }
62
- }
63
-
64
- async function inspectCommand(args, root) {
65
- const json = takeFlag(args, "--json");
66
- const id = args.shift();
67
- if (!id) throw invalidInput("inspect requires a harness name");
68
- assertNoArgs(args);
69
- const harness = {
70
- ...await inspectAgent(id),
71
- prepared_artifacts: await listPreparedArtifacts(id, { root }),
72
- };
73
- if (json) process.stdout.write(`${JSON.stringify({ schema_version: SCHEMA_VERSION, harness }, null, 2)}\n`);
74
- else process.stdout.write(`${harness.display_name}: ${harness.status}${harness.executable ? `\n executable: ${harness.executable}\n version: ${harness.version || "unknown"}` : ""}\n prepared artifacts: ${harness.prepared_artifacts.length}\n`);
75
- }
76
-
77
- async function resolveCommand(args, root) {
78
- const json = takeFlag(args, "--json");
79
- const reference = args.shift();
80
- if (!reference) throw invalidInput("resolve requires a harness reference");
81
- assertNoArgs(args);
82
- const resolved = await resolveHarness(reference, { root });
83
- if (json) process.stdout.write(`${JSON.stringify(resolved, null, 2)}\n`);
84
- else process.stdout.write(`${resolved.canonical_ref} -> ${revisionLabel(resolved)} (${resolved.identity})\n`);
85
- }
86
-
87
- async function prepareCommand(args, root) {
88
- const json = takeFlag(args, "--json");
89
- const reference = args.shift();
90
- if (!reference) throw invalidInput("prepare requires a harness reference");
91
- assertNoArgs(args);
92
- const resolved = await resolveHarness(reference, { root });
93
- const artifact = await prepareHarness(resolved, { root });
94
- if (json) process.stdout.write(`${JSON.stringify({ schema_version: SCHEMA_VERSION, resolved_revision: resolved, artifact }, null, 2)}\n`);
95
- else process.stdout.write(`${artifact.cache_hit ? "Cached" : "Prepared"} ${resolved.canonical_ref} as ${artifact.artifact_id}\n`);
96
- }
97
-
98
- async function runCommand(args, root) {
99
- const useDaemon = takeFlag(args, "--daemon");
100
- const output = takeOption(args, "--output") || "jsonl";
101
- const request = await parseRunRequest(args);
102
- assertNoArgs(args);
103
- if (!new Set(["json", "jsonl"]).has(output)) throw invalidInput("--output must be json or jsonl");
104
-
105
- if (useDaemon) {
106
- const client = await daemonClient(root);
107
- const accepted = await client.request("/v1/runs", { method: "POST", body: JSON.stringify(request) });
108
- const result = await waitForDaemonRun(client, accepted.run_id, output);
109
- process.exitCode = result.exit_code;
110
- return;
111
- }
112
-
113
- const runId = newRunId();
114
- const result = await executeRun({
115
- runId,
116
- request,
117
- runsRoot: statePaths(root).runs,
118
- root,
119
- onEvent: output === "jsonl" ? (event) => process.stdout.write(`${JSON.stringify(event)}\n`) : undefined,
120
- });
121
- if (output === "json") process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
122
- process.exitCode = result.exit_code;
123
- }
124
-
125
- async function evalCommand(args, root) {
126
- const action = args.shift();
127
- switch (action) {
128
- case "run": return evalRunCommand(args, root);
129
- case "list": return evalListCommand(args, root);
130
- case "inspect": return evalInspectCommand(args, root);
131
- case "setup": return evalSetupCommand(args, root);
132
- case "doctor": return evalDoctorCommand(args, root);
133
- default: throw invalidInput("eval requires run, list, inspect, setup, or doctor");
134
- }
135
- }
136
-
137
- async function evalSetupCommand(args, root) {
138
- const backend = args.shift();
139
- if (backend !== "harbor") throw invalidInput("eval setup currently supports only harbor");
140
- const version = takeOption(args, "--version") || DEFAULT_HARBOR_VERSION;
141
- const python = takeOption(args, "--python");
142
- const force = takeFlag(args, "--force");
143
- const json = takeFlag(args, "--json");
144
- assertNoArgs(args);
145
- const result = await setupHarbor({
146
- root,
147
- version,
148
- python,
149
- force,
150
- onProgress: json ? undefined : (message) => process.stderr.write(`${message}\n`),
151
- });
152
- if (json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
153
- else process.stdout.write(`${result.cache_hit ? "Using" : "Installed"} Harbor ${result.version} at ${result.executable}\n`);
154
- }
155
-
156
- async function evalDoctorCommand(args, root) {
157
- const json = takeFlag(args, "--json");
158
- const python = takeOption(args, "--python");
159
- const harbor = takeOption(args, "--harbor");
160
- const docker = takeOption(args, "--docker");
161
- assertNoArgs(args);
162
- const result = await doctorHarbor({ root, python, harbor, docker });
163
- if (json) {
164
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
165
- } else {
166
- process.stdout.write(`Harbor eval: ${result.status}\n`);
167
- for (const [name, check] of Object.entries(result.checks)) {
168
- const detail = check.version || check.message || (check.present?.length ? check.present.join(", ") : "");
169
- process.stdout.write(` ${name.padEnd(12)} ${check.status}${detail ? ` ${detail}` : ""}\n`);
170
- }
171
- }
172
- if (!result.ready) process.exitCode = 3;
173
- }
174
-
175
- async function evalRunCommand(args, root) {
176
- const output = takeOption(args, "--output") || "json";
177
- const harborExecutable = takeOption(args, "--harbor");
178
- const request = parseEvalRequest(args);
179
- assertNoArgs(args);
180
- if (!new Set(["json", "jsonl"]).has(output)) throw invalidInput("--output must be json or jsonl");
181
- const controller = new AbortController();
182
- const cancel = () => controller.abort();
183
- process.once("SIGINT", cancel);
184
- process.once("SIGTERM", cancel);
185
- try {
186
- const result = await runEval({
187
- request,
188
- root,
189
- harborExecutable,
190
- signal: controller.signal,
191
- onEvent: output === "jsonl" ? (event) => process.stdout.write(`${JSON.stringify(event)}\n`) : undefined,
192
- });
193
- if (output === "json") process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
194
- process.exitCode = result.exit_code;
195
- } finally {
196
- process.removeListener("SIGINT", cancel);
197
- process.removeListener("SIGTERM", cancel);
198
- }
199
- }
200
-
201
- async function evalListCommand(args, root) {
202
- const json = takeFlag(args, "--json");
203
- assertNoArgs(args);
204
- const evals = await listEvals({ root });
205
- if (json) {
206
- process.stdout.write(`${JSON.stringify({ schema_version: SCHEMA_VERSION, evals }, null, 2)}\n`);
207
- return;
208
- }
209
- if (evals.length === 0) {
210
- process.stdout.write("No evals found\n");
211
- return;
212
- }
213
- for (const evaluation of evals) {
214
- const reward = evaluation.primary_reward === null ? "-" : String(evaluation.primary_reward);
215
- process.stdout.write(`${evaluation.eval_id} ${String(evaluation.status).padEnd(9)} reward=${reward} ${evaluation.harness_ref || "-"} ${evaluation.dataset || "-"}\n`);
216
- }
217
- }
218
-
219
- async function evalInspectCommand(args, root) {
220
- const json = takeFlag(args, "--json");
221
- const evalId = args.shift();
222
- if (!evalId) throw invalidInput("eval inspect requires an eval ID");
223
- assertNoArgs(args);
224
- const evaluation = await inspectEval(evalId, { root });
225
- if (json) {
226
- process.stdout.write(`${JSON.stringify(evaluation, null, 2)}\n`);
227
- return;
228
- }
229
- const result = evaluation.result;
230
- process.stdout.write(`${evaluation.eval_id}: ${result?.status || "running"}\n`);
231
- process.stdout.write(` dataset: ${evaluation.request.dataset}\n`);
232
- process.stdout.write(` harness: ${evaluation.plan?.candidate?.harness_ref || evaluation.request.harness_ref}\n`);
233
- process.stdout.write(` primary reward: ${result?.summary?.primary_reward ?? "-"}\n`);
234
- process.stdout.write(` directory: ${evaluation.directory}\n`);
235
- }
236
-
237
- async function daemonCommand(args, root) {
238
- const action = args.shift();
239
- switch (action) {
240
- case "serve": return daemonServe(args, root);
241
- case "start": return daemonStart(args, root);
242
- case "stop": return daemonStop(args, root);
243
- case "status": return daemonStatus(args, root);
244
- case "submit": return daemonSubmit(args, root);
245
- case "cancel": return daemonCancel(args, root);
246
- case "logs": return daemonLogs(args, root);
247
- default: throw invalidInput("daemon requires start, serve, stop, status, submit, cancel, or logs");
248
- }
249
- }
250
-
251
- async function workspaceCommand(args, root) {
252
- const action = args.shift();
253
- const runId = args.shift();
254
- if (!runId) throw invalidInput("workspace requires a run ID");
255
- if (action === "inspect") {
256
- const json = takeFlag(args, "--json");
257
- assertNoArgs(args);
258
- const workspace = await inspectWorkspace({ root, runId });
259
- if (!workspace) throw new HitchError(`workspace record not found: ${runId}`, { code: "workspace_not_found", exitCode: 3 });
260
- if (json) process.stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
261
- else process.stdout.write(`${runId}: ${workspace.mode} ${workspace.status}\n source: ${workspace.source_workspace}\n execution: ${workspace.execution_workspace}\n retained: ${workspace.retained ? "yes" : "no"}${workspace.changed === undefined || workspace.changed === null ? "" : `\n changed: ${workspace.changed ? "yes" : "no"}`}\n`);
262
- return;
263
- }
264
- if (action === "path") {
265
- assertNoArgs(args);
266
- const workspace = await inspectWorkspace({ root, runId });
267
- if (!workspace) throw new HitchError(`workspace record not found: ${runId}`, { code: "workspace_not_found", exitCode: 3 });
268
- if (!workspace.retained) throw new HitchError(`run ${runId} has no retained workspace`, { code: "workspace_not_retained", exitCode: 3 });
269
- process.stdout.write(`${workspace.execution_workspace}\n`);
270
- return;
271
- }
272
- if (action === "remove") {
273
- const force = takeFlag(args, "--force");
274
- const json = takeFlag(args, "--json");
275
- assertNoArgs(args);
276
- const workspace = await removeWorkspace({ root, runId, force });
277
- if (json) process.stdout.write(`${JSON.stringify(workspace, null, 2)}\n`);
278
- else process.stdout.write(`Removed workspace for ${runId}\n`);
279
- return;
280
- }
281
- throw invalidInput("workspace requires inspect, path, or remove");
282
- }
283
-
284
- async function daemonServe(args, root) {
285
- const port = Number(takeOption(args, "--port") || DEFAULT_PORT);
286
- const maxConcurrent = positiveInteger(takeOption(args, "--max-concurrent") || DEFAULT_MAX_CONCURRENT, "--max-concurrent");
287
- assertNoArgs(args);
288
- if (!Number.isInteger(port) || port < 0 || port > 65535) throw invalidInput("--port must be between 0 and 65535");
289
- const server = new DaemonServer({ root, port, maxConcurrent });
290
- await server.start();
291
- const shutdown = () => server.close().catch((error) => process.stderr.write(`shutdown error: ${error.message}\n`));
292
- process.once("SIGINT", shutdown);
293
- process.once("SIGTERM", shutdown);
294
- await server.closed;
295
- }
296
-
297
- async function daemonStart(args, root) {
298
- const foreground = takeFlag(args, "--foreground");
299
- const port = Number(takeOption(args, "--port") || DEFAULT_PORT);
300
- const maxConcurrent = positiveInteger(takeOption(args, "--max-concurrent") || DEFAULT_MAX_CONCURRENT, "--max-concurrent");
301
- assertNoArgs(args);
302
- const health = await probeHealth(root);
303
- if (health?.status === "running") throw new HitchError(`daemon is already running (pid ${health.pid})`, { code: "already_running", exitCode: 2 });
304
- if (foreground) return daemonServe(["--port", String(port), "--max-concurrent", String(maxConcurrent)], root);
305
-
306
- const paths = statePaths(root);
307
- await ensureDir(root);
308
- const stdout = openSync(paths.log, "a", 0o600);
309
- const stderr = openSync(paths.errorLog, "a", 0o600);
310
- const child = spawn(process.execPath, [executable, "--root", root, "daemon", "serve", "--port", String(port), "--max-concurrent", String(maxConcurrent)], {
311
- detached: true,
312
- stdio: ["ignore", stdout, stderr],
313
- windowsHide: true,
314
- });
315
- child.unref();
316
-
317
- for (let attempt = 0; attempt < 50; attempt += 1) {
318
- await delay(100);
319
- const current = await probeHealth(root);
320
- if (current?.status === "running" && current.pid === child.pid) {
321
- process.stdout.write(`Hitch daemon started (pid ${current.pid}, port ${current.port})\n`);
322
- return;
323
- }
324
- }
325
- throw new HitchError(`daemon did not become ready; see ${paths.errorLog}`, { code: "daemon_start_failed", exitCode: 12 });
326
- }
327
-
328
- async function daemonStop(args, root) {
329
- assertNoArgs(args);
330
- const client = await daemonClient(root);
331
- const response = await client.request("/shutdown", { method: "POST" });
332
- process.stdout.write(`${response.status}\n`);
333
- }
334
-
335
- async function daemonStatus(args, root) {
336
- const json = takeFlag(args, "--json");
337
- assertNoArgs(args);
338
- const health = await probeHealth(root);
339
- if (!health) {
340
- if (json) process.stdout.write(`${JSON.stringify({ schema_version: SCHEMA_VERSION, status: "stopped" })}\n`);
341
- else process.stdout.write("Hitch daemon is stopped\n");
342
- process.exitCode = 3;
343
- return;
344
- }
345
- if (json) process.stdout.write(`${JSON.stringify(health, null, 2)}\n`);
346
- else process.stdout.write(`Hitch daemon is ${health.status} (pid ${health.pid}, port ${health.port}, ${health.scheduler.running} running, ${health.scheduler.queued} queued)\n`);
347
- }
348
-
349
- async function daemonSubmit(args, root) {
350
- const wait = takeFlag(args, "--wait");
351
- const output = takeOption(args, "--output") || "json";
352
- const request = await parseRunRequest(args);
353
- assertNoArgs(args);
354
- const client = await daemonClient(root);
355
- const accepted = await client.request("/v1/runs", { method: "POST", body: JSON.stringify(request) });
356
- if (!wait) {
357
- process.stdout.write(`${JSON.stringify(accepted, null, 2)}\n`);
358
- return;
359
- }
360
- const result = await waitForDaemonRun(client, accepted.run_id, output);
361
- process.exitCode = result.exit_code;
362
- }
363
-
364
- async function daemonCancel(args, root) {
365
- const runId = args.shift();
366
- if (!runId) throw invalidInput("daemon cancel requires a run ID");
367
- assertNoArgs(args);
368
- const client = await daemonClient(root);
369
- const response = await client.request(`/v1/runs/${runId}/cancel`, { method: "POST" });
370
- process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
371
- }
372
-
373
- async function daemonLogs(args, root) {
374
- const lines = positiveInteger(takeOption(args, "-n") || 50, "-n");
375
- assertNoArgs(args);
376
- let content = "";
377
- try { content = await readFile(statePaths(root).log, "utf8"); } catch (error) { if (error?.code !== "ENOENT") throw error; }
378
- process.stdout.write(`${content.trimEnd().split(/\r?\n/).slice(-lines).join("\n")}\n`);
379
- }
380
-
381
- async function parseRunRequest(args) {
382
- const harness = takeOption(args, "--harness");
383
- const agent = takeOption(args, "--agent");
384
- const model = takeOption(args, "--model") || "";
385
- const cwd = takeOption(args, "--cwd") || process.cwd();
386
- const workspaceMode = takeOption(args, "--workspace-mode") || "shared";
387
- const promptValue = takeOption(args, "--prompt");
388
- const promptFile = takeOption(args, "--prompt-file");
389
- const timeout = parseDuration(takeOption(args, "--timeout") || "0");
390
- const agentArgs = takeRepeatedOption(args, "--agent-arg");
391
- if (harness && agent) throw invalidInput("use only one of --harness and the legacy --agent option");
392
- if (!harness && !agent) throw invalidInput("--harness is required");
393
- if (!WORKSPACE_MODES.has(workspaceMode)) throw invalidInput(`--workspace-mode must be one of: ${[...WORKSPACE_MODES].join(", ")}`);
394
- if (agent?.includes("@")) throw invalidInput("--agent accepts only a harness name; use --harness for revision selection");
395
- if (promptValue !== undefined && promptFile) throw invalidInput("use only one of --prompt and --prompt-file");
396
- let prompt = promptValue;
397
- if (promptFile) prompt = await readFile(path.resolve(promptFile), "utf8");
398
- if (prompt === undefined && !process.stdin.isTTY) prompt = readFileSync(0, "utf8");
399
- if (!prompt) throw invalidInput("provide --prompt, --prompt-file, or stdin");
400
- return { harness_ref: harness || `${agent}@installed`, model, cwd, workspace_mode: workspaceMode, prompt, timeout_ms: timeout, agent_args: agentArgs };
401
- }
402
-
403
- function parseEvalRequest(args) {
404
- const backend = takeOption(args, "--backend") || "harbor";
405
- const dataset = takeOption(args, "--dataset");
406
- const harness = takeOption(args, "--harness");
407
- const model = takeOption(args, "--model") || "";
408
- const attempts = positiveInteger(takeOption(args, "--attempts") || 1, "--attempts");
409
- const maxConcurrent = positiveInteger(takeOption(args, "--max-concurrent") || DEFAULT_MAX_CONCURRENT, "--max-concurrent");
410
- const timeoutValue = takeOption(args, "--timeout");
411
- const setupTimeoutValue = takeOption(args, "--setup-timeout");
412
- const agentArgs = takeRepeatedOption(args, "--agent-arg");
413
- const passEnv = takeRepeatedOption(args, "--pass-env");
414
- if (!dataset) throw invalidInput("--dataset is required");
415
- if (!harness) throw invalidInput("--harness is required");
416
- return {
417
- backend,
418
- dataset,
419
- harness_ref: harness,
420
- model,
421
- attempts,
422
- max_concurrent: maxConcurrent,
423
- timeout_ms: timeoutValue === undefined ? undefined : parseDuration(timeoutValue),
424
- setup_timeout_ms: setupTimeoutValue === undefined ? undefined : parseDuration(setupTimeoutValue),
425
- agent_args: agentArgs,
426
- pass_env: passEnv,
427
- };
428
- }
429
-
430
- function revisionLabel(resolved) {
431
- if (resolved.revision.type === "commit") return resolved.revision.commit;
432
- if (resolved.revision.type === "version") return resolved.revision.version;
433
- return resolved.revision.version || resolved.source.integrity;
434
- }
435
-
436
- async function waitForDaemonRun(client, runId, output) {
437
- let eventOffset = 0;
438
- for (;;) {
439
- const status = await client.request(`/v1/runs/${runId}`);
440
- if (output === "jsonl") {
441
- try {
442
- const response = await client.requestWithMetadata(`/v1/runs/${runId}/events?offset=${eventOffset}`);
443
- const raw = response.payload;
444
- for (const line of raw.trim().split(/\r?\n/).filter(Boolean)) {
445
- JSON.parse(line);
446
- process.stdout.write(`${line}\n`);
447
- }
448
- eventOffset = Number(response.headers.get("x-hitch-next-offset") || eventOffset);
449
- } catch (error) {
450
- if (error.status !== 404) throw error;
451
- }
452
- }
453
- if (status.result) {
454
- if (output === "json") process.stdout.write(`${JSON.stringify(status.result, null, 2)}\n`);
455
- return status.result;
456
- }
457
- await delay(200);
458
- }
459
- }
460
-
461
- async function probeHealth(root) {
462
- const state = await readJSON(statePaths(root).daemon, null);
463
- if (!state?.port) return null;
464
- try {
465
- const response = await fetch(`http://127.0.0.1:${state.port}/health`, { signal: AbortSignal.timeout(1_000) });
466
- if (!response.ok) return null;
467
- const health = await response.json();
468
- return health.instance_id === state.instance_id ? health : null;
469
- } catch {
470
- return null;
471
- }
472
- }
473
-
474
- function takeOption(args, name) {
475
- const index = args.indexOf(name);
476
- if (index < 0) return undefined;
477
- if (index === args.length - 1) throw invalidInput(`${name} requires a value`);
478
- const [value] = args.splice(index + 1, 1);
479
- args.splice(index, 1);
480
- return value;
481
- }
482
-
483
- function takeRepeatedOption(args, name) {
484
- const values = [];
485
- for (;;) {
486
- const value = takeOption(args, name);
487
- if (value === undefined) return values;
488
- values.push(value);
489
- }
490
- }
491
-
492
- function takeFlag(args, name) {
493
- const index = args.indexOf(name);
494
- if (index < 0) return false;
495
- args.splice(index, 1);
496
- return true;
497
- }
498
-
499
- function assertNoArgs(args) {
500
- if (args.length > 0) throw invalidInput(`unexpected argument: ${args[0]}`);
501
- }
502
-
503
- function helpText() {
504
- return `Hitch — one local runtime for coding agents\n\nUsage:\n hitch list [--json]\n hitch inspect <harness> [--json]\n hitch resolve <harness-ref> [--json]\n hitch prepare <harness-ref> [--json]\n hitch run --harness <ref> [--model <id>] [--workspace-mode <mode>] --prompt <text> [--daemon]\n hitch eval setup harbor [--version <version>] [--python <path>] [--force] [--json]\n hitch eval doctor [--harbor <path>] [--python <path>] [--docker <path>] [--json]\n hitch eval run [--backend harbor] --dataset <ref> --harness <immutable-ref> [--model <id>] [--attempts <n>]\n hitch eval list [--json]\n hitch eval inspect <eval-id> [--json]\n hitch workspace inspect <run-id> [--json]\n hitch workspace path <run-id>\n hitch workspace remove <run-id> [--force] [--json]\n hitch daemon start [--foreground] [--port <port>] [--max-concurrent <n>]\n hitch daemon stop | status [--json] | logs [-n <lines>]\n hitch daemon submit --harness <ref> --prompt <text> [--workspace-mode <mode>] [--wait]\n hitch daemon cancel <run-id>\n\nEval:\n Harbor runs each task in Docker; Hitch executes the selected harness inside that task container.\n Use 'hitch eval setup harbor' for an isolated managed install and 'hitch eval doctor' to verify it.\n Eval accepts exact version: or registered commit: refs. Use --pass-env NAME for extra credentials.\n\nWorkspace modes:\n shared Use the source directory directly (compatibility default)\n worktree Create a detached worktree from a clean Git HEAD\n copy Copy the current filesystem state into an independent workspace\n\nHarness refs:\n codex Installed executable (compatibility default)\n codex@installed Installed executable\n codex@version:0.42.1 Exact published version\n codex@commit:abc1234 Commit from the registered Git source\n codex@git+file:///src#abc1234 Commit from a clean local Git repository\n\nGlobal:\n --root <path> Relocate all Hitch state (default: ~/.hitch)\n`;
505
- }
package/src/config.js DELETED
@@ -1,51 +0,0 @@
1
- import { homedir } from "node:os";
2
- import path from "node:path";
3
- import { invalidInput } from "./errors.js";
4
-
5
- export const SCHEMA_VERSION = "1";
6
- export const DEFAULT_PORT = 7463;
7
- export const DEFAULT_MAX_CONCURRENT = 4;
8
-
9
- export function defaultRoot() {
10
- return path.resolve(process.env.HITCH_ROOT || path.join(homedir(), ".hitch"));
11
- }
12
-
13
- export function parseDuration(value) {
14
- if (value === undefined || value === null || value === "") return 0;
15
- if (typeof value === "number" && Number.isFinite(value)) return value;
16
- const match = String(value).trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
17
- if (!match) throw invalidInput(`invalid duration: ${value}`);
18
- const scale = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }[match[2] || "ms"];
19
- return Math.round(Number(match[1]) * scale);
20
- }
21
-
22
- export function positiveInteger(value, name) {
23
- const number = Number(value);
24
- if (!Number.isInteger(number) || number <= 0) {
25
- throw invalidInput(`${name} must be a positive integer`);
26
- }
27
- return number;
28
- }
29
-
30
- export function statePaths(root) {
31
- return {
32
- root,
33
- daemon: path.join(root, "daemon.json"),
34
- lock: path.join(root, "daemon.lock"),
35
- token: path.join(root, "daemon.token"),
36
- log: path.join(root, "daemon.log"),
37
- errorLog: path.join(root, "daemon.err.log"),
38
- runs: path.join(root, "runs"),
39
- evals: path.join(root, "evals"),
40
- tools: path.join(root, "tools"),
41
- store: path.join(root, "store"),
42
- artifacts: path.join(root, "store", "artifacts"),
43
- artifactIndex: path.join(root, "store", "refs"),
44
- sourceCache: path.join(root, "cache", "sources"),
45
- artifactLocks: path.join(root, "locks", "artifacts"),
46
- sourceLocks: path.join(root, "locks", "sources"),
47
- workspaceLocks: path.join(root, "locks", "workspaces"),
48
- workspaces: path.join(root, "workspaces"),
49
- temporary: path.join(root, "tmp"),
50
- };
51
- }