@cozibud/harness 0.1.0 → 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 CHANGED
@@ -1,24 +1,54 @@
1
- # `@cozibud/harness`
2
-
3
- Internal infrastructure for the Cozibud Watch local-client bridge. It invokes a
4
- supported local AI client and exposes the provider-neutral runtime used by the
5
- bridge engineering tools.
6
-
7
- This package is not a released Watch feature. The authenticated server relay,
8
- device pairing, and production delivery path are not included, and the local
9
- HTTP adapter must not be exposed to a LAN or the internet.
10
-
11
- ## Maintainer smoke check
12
-
13
- After building or installing an explicitly reviewed version:
14
-
15
- ```text
16
- cozibud-harness --help
17
- ```
18
-
19
- Provider setup and request commands are internal acceptance tools. They use the
20
- provider's own installed client and sign-in state; the harness does not accept
21
- or store provider passwords.
22
-
23
- Release and bootstrap procedures live in the private repository's
24
- `docs/NPM-HARNESS-PUBLISHING.md` operator runbook.
1
+ # `@cozibud/harness`
2
+
3
+ Connect a signed-in Codex CLI to your Cozibud household for watch voice.
4
+ Keep the computer awake and the outbound relay running while using voice.
5
+ Watch voice requires Codex CLI 0.159.0 or newer and a server supporting relay
6
+ protocol 2. This source version still needs its package and server release.
7
+
8
+ ## Watch voice
9
+
10
+ After installing the released harness, sign in to Codex with `codex login`.
11
+ Run this single command:
12
+
13
+ ```text
14
+ cozibud-harness relay --provider codex
15
+ ```
16
+
17
+ On the first run, open the displayed Cozibud link and approve your computer
18
+ in the household you use on your watch. The command then connects automatically.
19
+ Later runs reuse the saved pairing. Keep this command running and the computer awake.
20
+ If the watch finds no connected computer, **Open guide on phone** shows the setup steps.
21
+
22
+ On the watch, select the child, speak, and check the transcript. **Send & save**
23
+ authorizes the server to validate and save one record. Ambiguous requests ask
24
+ for clarification. A saved-record receipt appears only after server readback.
25
+ No microphone audio is sent to the bridge; the confirmed transcript and selected
26
+ child context go to your signed-in Codex client and its model service.
27
+
28
+ Your Codex login stays local. Pairing credentials are protected with your
29
+ Windows account's DPAPI on Windows, or a user-only file on other systems.
30
+ Disconnect the computer in Cozibud's Watch settings to revoke its access.
31
+ Each Cozibud account can pair two computers. The one connected longest answers
32
+ requests from the web app, the phone app and the watch; the other is a backup
33
+ that takes over when the first stops reporting in. Run `pair` again on a computer
34
+ to replace its pairing, or choose the household and account by signing in to the
35
+ one you want when you approve the link. Codex uses whichever account `codex login`
36
+ signed in (set `CODEX_HOME` to keep a separate Codex profile).
37
+ Use `COZIBUD_RELAY_URL` to choose another HTTPS server before pairing; pairings
38
+ are scoped to that origin. The relay requires no inbound computer port.
39
+ The separate local HTTP adapter must remain on loopback.
40
+
41
+ ## Maintainer smoke check
42
+
43
+ After building or installing an explicitly reviewed version:
44
+
45
+ ```text
46
+ cozibud-harness --help
47
+ ```
48
+
49
+ Provider setup and request commands are internal acceptance tools. They use the
50
+ provider's own installed client and sign-in state; the harness does not accept
51
+ or store provider passwords.
52
+
53
+ Release and bootstrap procedures live in the private repository's
54
+ `docs/NPM-HARNESS-PUBLISHING.md` operator runbook.
package/dist/cli.js CHANGED
@@ -1,6 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { randomBytes } from "node:crypto";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { hostname } from "node:os";
6
+ import { ensureRelayPairing, pairRelay, relayOrigin } from "./pairing.js";
3
7
  import { createBuiltInDrivers } from "./process-driver.js";
8
+ import { FileRelayExecutionStore, OutboundRelayBridge, RelayTransportError } from "./relay-client.js";
4
9
  import { HarnessRuntime } from "./runtime.js";
5
10
  import { createHarnessHttpServer } from "./server.js";
6
11
  import { DEFAULT_COZIBUD_MCP_URL, setupProvider } from "./setup.js";
@@ -20,26 +25,96 @@ function promptFrom(args) {
20
25
  return parts.join(" ").trim();
21
26
  }
22
27
  function printHelp() {
23
- console.log(`Cozibud local LLM harness
24
-
25
- Usage:
26
- cozibud-harness setup [--provider codex|claude|gemini|cursor] [--mcp-url https://cozibud.com/api/mcp]
27
- cozibud-harness ask [--provider codex|claude|gemini|cursor] [--locale en-US] "When was the last bottle?"
28
- cozibud-harness serve [--host 127.0.0.1] [--port 8798]
29
-
30
- Quick start:
31
- setup prepares a signed-in local client and the MCP access its bridge turns need
32
- ask sends one typed request through an already signed-in provider
33
-
34
- Environment:
35
- COZIBUD_HARNESS_TOKEN bearer token (generated for this run when omitted)
36
- COZIBUD_HARNESS_ORIGINS comma-separated browser origins allowed by CORS
37
- COZIBUD_CODEX_PATH optional Codex executable path
38
- COZIBUD_CLAUDE_PATH optional Claude executable path
39
- COZIBUD_GEMINI_PATH optional Gemini executable path
40
- COZIBUD_CURSOR_PATH optional Cursor Agent executable path
28
+ console.log(`Cozibud local LLM harness
29
+
30
+ Usage:
31
+ cozibud-harness setup [--provider codex|claude|gemini|cursor] [--mcp-url https://cozibud.com/api/mcp]
32
+ cozibud-harness ask [--provider codex|claude|gemini|cursor] [--locale en-US] "When was the last bottle?"
33
+ cozibud-harness relay [--provider codex]
34
+ cozibud-harness pair [--name "My computer"]
35
+ cozibud-harness serve [--host 127.0.0.1] [--port 8798]
36
+
37
+ Quick start:
38
+ setup prepares a signed-in local client and the MCP access its bridge turns need
39
+ ask sends one typed request through an already signed-in provider
40
+ relay pairs this computer if needed, then keeps watch voice connected
41
+
42
+ Environment:
43
+ COZIBUD_HARNESS_TOKEN bearer token (generated for this run when omitted)
44
+ COZIBUD_HARNESS_ORIGINS comma-separated browser origins allowed by CORS
45
+ COZIBUD_RELAY_URL canonical Cozibud origin (defaults to https://cozibud.com)
46
+ COZIBUD_RELAY_CREDENTIAL optional paired credential override (never printed)
47
+ COZIBUD_RELAY_PAIRING protected pairing file path
48
+ COZIBUD_RELAY_STATE local idempotency journal path
49
+ COZIBUD_CODEX_PATH optional Codex executable path
50
+ COZIBUD_CLAUDE_PATH optional Claude executable path
51
+ COZIBUD_GEMINI_PATH optional Gemini executable path
52
+ COZIBUD_CURSOR_PATH optional Cursor Agent executable path
53
+ COZIBUD_CURSOR_WORKSPACE stable dedicated empty Cursor bridge workspace
41
54
  COZIBUD_CURSOR_MCP_CONFIG optional Cursor user MCP config path (test/development override)`);
42
55
  }
56
+ async function relay(args) {
57
+ const provider = value(args, "--provider", "codex");
58
+ const driver = createBuiltInDrivers().find((candidate) => candidate.id === provider);
59
+ if (!driver)
60
+ throw new Error(`Unknown provider: ${provider}. Choose codex, claude, gemini, or cursor.`);
61
+ if (provider !== "codex")
62
+ throw new Error("Watch voice currently requires Codex CLI.");
63
+ const baseUrl = relayOrigin(process.env.COZIBUD_RELAY_URL?.trim() || "https://cozibud.com");
64
+ const available = await driver.probe();
65
+ if (available.state === "unavailable")
66
+ throw new Error(available.message);
67
+ const version = available.version?.match(/(\d+)\.(\d+)\.(\d+)/);
68
+ if (!version || (Number(version[1]) === 0 && Number(version[2]) < 159))
69
+ throw new Error("Watch voice requires Codex CLI 0.159.0 or newer. Update Codex, sign in, and retry.");
70
+ const controller = new AbortController();
71
+ process.once("SIGINT", () => controller.abort());
72
+ process.once("SIGTERM", () => controller.abort());
73
+ let credential = process.env.COZIBUD_RELAY_CREDENTIAL?.trim() || await ensureRelayPairing({ origin: baseUrl,
74
+ name: value(args, "--name", hostname()), path: pairingPath(), write: console.log, signal: controller.signal });
75
+ const createBridge = () => {
76
+ const namespace = createHash("sha256").update(`${baseUrl}\n${credential}`).digest("hex").slice(0, 20);
77
+ const statePath = process.env.COZIBUD_RELAY_STATE?.trim() || join(homedir(), ".cozibud", `relay-state-${namespace}.json`);
78
+ return new OutboundRelayBridge({ baseUrl, credential, driver, store: new FileRelayExecutionStore(statePath) });
79
+ };
80
+ let bridge = createBridge();
81
+ let repaired = false;
82
+ let connected = false;
83
+ let backoffMs = 1_000;
84
+ while (!controller.signal.aborted) {
85
+ try {
86
+ if (!connected) {
87
+ await bridge.connect();
88
+ connected = true;
89
+ backoffMs = 1_000;
90
+ console.log(`Cozibud relay connected with ${driver.label}. No local or LAN listener is open.`);
91
+ }
92
+ const state = await bridge.step();
93
+ if (state === "idle")
94
+ await pause(1_000, controller.signal);
95
+ }
96
+ catch (error) {
97
+ if (error instanceof RelayTransportError && [401, 403, 404].includes(error.status)) {
98
+ await bridge.close();
99
+ if ([401, 403].includes(error.status) && !process.env.COZIBUD_RELAY_CREDENTIAL?.trim() && !repaired && !controller.signal.aborted) {
100
+ repaired = true;
101
+ console.log("This computer's pairing is no longer valid. Approve it again to reconnect.");
102
+ credential = await pairRelay({ origin: baseUrl, name: value(args, "--name", hostname()), path: pairingPath(), write: console.log, signal: controller.signal });
103
+ bridge = createBridge();
104
+ connected = false;
105
+ continue;
106
+ }
107
+ throw new Error("Desktop pairing is expired, revoked, or disabled. Run pair again on the selected server.");
108
+ }
109
+ connected = false;
110
+ console.error("Cozibud relay disconnected; retrying the outbound connection.");
111
+ await pause(backoffMs, controller.signal);
112
+ backoffMs = Math.min(15_000, backoffMs * 2);
113
+ }
114
+ }
115
+ await bridge.close();
116
+ }
117
+ function pairingPath() { return process.env.COZIBUD_RELAY_PAIRING?.trim() || join(homedir(), ".cozibud", "relay-pairing.json"); }
43
118
  async function ask(args) {
44
119
  const provider = value(args, "--provider", "codex");
45
120
  const locale = value(args, "--locale", "en-US");
@@ -118,6 +193,11 @@ try {
118
193
  }
119
194
  else if (command === "ask")
120
195
  await ask(args);
196
+ else if (command === "relay")
197
+ await relay(args);
198
+ else if (command === "pair")
199
+ await pairRelay({ origin: process.env.COZIBUD_RELAY_URL || "https://cozibud.com",
200
+ name: value(args, "--name", hostname()), path: pairingPath(), write: console.log });
121
201
  else if (command === "serve")
122
202
  await serve(args);
123
203
  else
@@ -127,3 +207,12 @@ catch (error) {
127
207
  console.error(error instanceof Error ? error.message : "The local harness failed.");
128
208
  process.exitCode = 1;
129
209
  }
210
+ function pause(ms, signal) {
211
+ return new Promise(resolve => {
212
+ const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); };
213
+ const timer = setTimeout(done, ms);
214
+ signal.addEventListener("abort", done, { once: true });
215
+ if (signal.aborted)
216
+ done();
217
+ });
218
+ }
@@ -7,6 +7,10 @@ export interface HarnessTurnInput {
7
7
  source: HarnessInputSource;
8
8
  client: HarnessClient;
9
9
  locale?: string;
10
+ /** Server request-bound identity that every care-record write must use. */
11
+ careRecordIdempotencyKey?: string;
12
+ /** Server-supplied interpretation instructions; no MCP writes in this mode. */
13
+ interpretation?: string;
10
14
  }
11
15
  export interface HarnessTurnRequest extends HarnessTurnInput {
12
16
  harness: string;
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_CURSOR_WORKSPACE: string;
2
+ export declare function cursorWorkspacePath(configured?: string | undefined): string;
3
+ export declare function ensureCursorWorkspace(configured?: string): string;
@@ -0,0 +1,39 @@
1
+ import { chmodSync, lstatSync, mkdirSync, readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, parse, relative, resolve, sep } from "node:path";
4
+ export const DEFAULT_CURSOR_WORKSPACE = join(homedir(), ".cozibud", "cursor-workspace");
5
+ export function cursorWorkspacePath(configured = process.env.COZIBUD_CURSOR_WORKSPACE) {
6
+ const override = configured?.trim();
7
+ if (override && !isAbsolute(override))
8
+ throw new Error("COZIBUD_CURSOR_WORKSPACE must be an absolute path.");
9
+ const candidate = override || DEFAULT_CURSOR_WORKSPACE;
10
+ return resolve(candidate);
11
+ }
12
+ export function ensureCursorWorkspace(configured) {
13
+ const workspace = cursorWorkspacePath(configured);
14
+ const root = parse(workspace).root;
15
+ const current = resolve(process.cwd());
16
+ const isWithin = (parent, candidate) => {
17
+ const path = relative(parent, candidate);
18
+ return path !== "" && !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
19
+ };
20
+ if (workspace === root || workspace === current || isWithin(current, workspace) || isWithin(workspace, current)) {
21
+ throw new Error("Cursor bridge workspace must be dedicated and outside the current repository or worktree.");
22
+ }
23
+ try {
24
+ const state = lstatSync(workspace);
25
+ if (state.isSymbolicLink() || !state.isDirectory()) {
26
+ throw new Error("Cursor bridge workspace must be a real directory, not a file or symbolic link.");
27
+ }
28
+ if (readdirSync(workspace).length !== 0) {
29
+ throw new Error("Cursor bridge workspace must be empty and dedicated to Cozibud.");
30
+ }
31
+ }
32
+ catch (error) {
33
+ if (error.code !== "ENOENT")
34
+ throw error;
35
+ mkdirSync(workspace, { recursive: true, mode: 0o700 });
36
+ }
37
+ chmodSync(workspace, 0o700);
38
+ return workspace;
39
+ }
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export * from "./process-driver.js";
3
3
  export * from "./runtime.js";
4
4
  export * from "./server.js";
5
5
  export * from "./setup.js";
6
+ export * from "./relay-client.js";
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ export * from "./process-driver.js";
3
3
  export * from "./runtime.js";
4
4
  export * from "./server.js";
5
5
  export * from "./setup.js";
6
+ export * from "./relay-client.js";
@@ -0,0 +1,8 @@
1
+ export declare function protectPairing(value: string): Promise<{
2
+ protection: string;
3
+ value: string;
4
+ }>;
5
+ export declare function unprotectPairing(row: {
6
+ protection: string;
7
+ value: string;
8
+ }): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+ /** Windows DPAPI binds the pairing secret to the signed-in OS user. */
4
+ async function dpapi(value, decrypt) {
5
+ const script = [
6
+ "Add-Type -AssemblyName System.Security;",
7
+ "$inputValue = [Console]::In.ReadToEnd();",
8
+ decrypt
9
+ ? "$bytes = [Convert]::FromBase64String($inputValue); $plain = [Security.Cryptography.ProtectedData]::Unprotect($bytes,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser); [Console]::Out.Write([Text.Encoding]::UTF8.GetString($plain));"
10
+ : "$bytes = [Text.Encoding]::UTF8.GetBytes($inputValue); $protected = [Security.Cryptography.ProtectedData]::Protect($bytes,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser); [Console]::Out.Write([Convert]::ToBase64String($protected));",
11
+ ].join("\n");
12
+ return new Promise((resolve, reject) => {
13
+ const child = spawn(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] });
14
+ let output = "";
15
+ const timer = setTimeout(() => child.kill(), 10_000);
16
+ child.stdout.on("data", chunk => { output += chunk.toString(); });
17
+ child.stdin.on("error", () => { });
18
+ child.once("error", () => { clearTimeout(timer); reject(new Error("Could not access the protected desktop pairing.")); });
19
+ child.once("close", code => { clearTimeout(timer); code === 0 ? resolve(output) : reject(new Error("Could not access the protected desktop pairing. Pair again on this computer.")); });
20
+ // Plaintext never appears in argv, environment values or diagnostics.
21
+ child.stdin.end(value);
22
+ });
23
+ }
24
+ export async function protectPairing(value) {
25
+ return process.platform === "win32" ? { protection: "dpapi", value: await dpapi(value, false) }
26
+ : { protection: "user-file", value };
27
+ }
28
+ export async function unprotectPairing(row) {
29
+ if (row.protection === "dpapi" && process.platform === "win32")
30
+ return dpapi(row.value, true);
31
+ if (row.protection === "user-file" && process.platform !== "win32")
32
+ return row.value;
33
+ throw new Error("This pairing cannot be used on this computer. Run pair again.");
34
+ }
@@ -0,0 +1,14 @@
1
+ export declare function relayOrigin(value: string): string;
2
+ export declare function readPairing(path: string, origin: string): Promise<string | null>;
3
+ export declare function pairRelay(options: {
4
+ origin: string;
5
+ name: string;
6
+ path: string;
7
+ write: (message: string) => void;
8
+ fetchImpl?: typeof fetch;
9
+ wait?: (ms: number) => Promise<void>;
10
+ now?: () => number;
11
+ signal?: AbortSignal;
12
+ }): Promise<string>;
13
+ /** Relay is also the first-run setup command; approval remains in the browser. */
14
+ export declare function ensureRelayPairing(options: Parameters<typeof pairRelay>[0]): Promise<string>;
@@ -0,0 +1,89 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { protectPairing, unprotectPairing } from "./pairing-secret.js";
4
+ export function relayOrigin(value) {
5
+ const url = new URL(value);
6
+ if (url.username || url.password || url.pathname !== "/" || url.search || url.hash ||
7
+ (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)))) {
8
+ throw new Error("Use an HTTPS server origin, or HTTP on loopback for local testing.");
9
+ }
10
+ return url.origin;
11
+ }
12
+ export async function readPairing(path, origin) {
13
+ try {
14
+ const row = JSON.parse(await unprotectPairing(JSON.parse(await readFile(path, "utf8"))));
15
+ if (row.origin !== relayOrigin(origin) || !Number.isFinite(Date.parse(row.expiresAt)) || Date.parse(row.expiresAt) <= Date.now())
16
+ return null;
17
+ if (typeof row.credential !== "string" || !row.credential.startsWith("cbr_"))
18
+ throw new Error("Invalid local pairing. Run pair again.");
19
+ return row.credential;
20
+ }
21
+ catch (error) {
22
+ if (error.code === "ENOENT")
23
+ return null;
24
+ throw error;
25
+ }
26
+ }
27
+ export async function pairRelay(options) {
28
+ const origin = relayOrigin(options.origin), fetcher = options.fetchImpl ?? fetch;
29
+ const now = options.now ?? Date.now;
30
+ const wait = options.wait ?? (ms => new Promise((resolve, reject) => {
31
+ const cleanup = () => { clearTimeout(timer); options.signal?.removeEventListener("abort", abort); };
32
+ const done = () => { cleanup(); resolve(); };
33
+ const abort = () => { cleanup(); reject(options.signal?.reason); };
34
+ const timer = setTimeout(done, ms);
35
+ options.signal?.addEventListener("abort", abort, { once: true });
36
+ if (options.signal?.aborted)
37
+ abort();
38
+ }));
39
+ const post = (path, body) => fetcher(`${origin}${path}`, { method: "POST", redirect: "error",
40
+ headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(20_000)]) : AbortSignal.timeout(20_000) });
41
+ const response = await post("/api/watch-relay/v1/bridge-authorizations", { bridgeName: options.name });
42
+ if (!response.ok)
43
+ throw new Error(response.status === 404 ? "Watch voice is not enabled on this server yet." : "Could not start desktop pairing. Try again later.");
44
+ const authorization = await response.json();
45
+ if (!authorization.device_code || !authorization.user_code || !Number.isFinite(authorization.expires_in) || !Number.isFinite(authorization.interval))
46
+ throw new Error("Invalid pairing response.");
47
+ const activation = new URL("/watch/activate", origin);
48
+ activation.searchParams.set("bridge", "1");
49
+ activation.searchParams.set("code", authorization.user_code);
50
+ options.write(`Open ${activation.href}\nSign in to your Cozibud household and approve ${options.name}.`);
51
+ const deadline = now() + authorization.expires_in * 1000;
52
+ let interval = Math.max(5, authorization.interval);
53
+ while (now() < deadline) {
54
+ await wait(interval * 1000);
55
+ options.signal?.throwIfAborted();
56
+ if (now() >= deadline)
57
+ break;
58
+ const exchange = await post("/api/watch-relay/v1/token", { deviceCode: authorization.device_code });
59
+ const result = await exchange.json();
60
+ if (exchange.ok && result.credential?.startsWith("cbr_") && Number.isFinite(result.expiresIn)) {
61
+ await mkdir(dirname(options.path), { recursive: true, mode: 0o700 });
62
+ const protectedPairing = await protectPairing(JSON.stringify({ origin, credential: result.credential, bridgeId: result.bridgeId,
63
+ expiresAt: new Date(now() + result.expiresIn * 1000).toISOString() }));
64
+ await writeFile(options.path, JSON.stringify(protectedPairing), { mode: 0o600 });
65
+ if (process.platform !== "win32")
66
+ await chmod(options.path, 0o600);
67
+ options.write("Desktop paired.");
68
+ return result.credential;
69
+ }
70
+ if (result.error?.code === "authorization_pending")
71
+ continue;
72
+ if (result.error?.code === "slow_down" || exchange.status === 429) {
73
+ interval += 5;
74
+ continue;
75
+ }
76
+ if (result.error?.code === "bridge_limit")
77
+ throw new Error("Your account already has two computers paired. Disconnect one in Cozibud under Settings → Assistants & API → Voice with a local agent, then run pair again.");
78
+ throw new Error("Pairing was denied or expired. Run pair again.");
79
+ }
80
+ throw new Error("Pairing expired. Run pair again.");
81
+ }
82
+ /** Relay is also the first-run setup command; approval remains in the browser. */
83
+ export async function ensureRelayPairing(options) {
84
+ const saved = await readPairing(options.path, options.origin);
85
+ if (saved)
86
+ return saved;
87
+ options.write("This computer needs pairing. Approve it in Cozibud; the relay will start automatically.");
88
+ return pairRelay(options);
89
+ }
@@ -1,4 +1,4 @@
1
- import type { HarnessDriver } from "./contracts.js";
1
+ import type { HarnessDriver, HarnessTurnInput } from "./contracts.js";
2
2
  declare const TEMP_DIRECTORY_CLEANUP_OPTIONS: {
3
3
  readonly recursive: true;
4
4
  readonly force: true;
@@ -13,7 +13,7 @@ export interface ProcessHarnessDefinition {
13
13
  versionArgs?: string[];
14
14
  /** Applied only to a fresh empty directory created by this driver. */
15
15
  emptyDirectoryArgs?: string[];
16
- args(): string[];
16
+ args(input?: HarnessTurnInput): string[];
17
17
  }
18
18
  declare function messageText(value: unknown): string | null;
19
19
  declare function appendNovelText(current: string, candidate: string): {