@agntn/harnesses 0.0.1 → 0.1.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.
- package/README.md +26 -6
- package/dist/_chunks/agents-sync.d.mts +22 -5
- package/dist/_chunks/agents-sync.d.mts.map +1 -1
- package/dist/_chunks/agents-sync.mjs +193 -49
- package/dist/_chunks/agents-sync.mjs.map +1 -1
- package/dist/_chunks/mcp.mjs +7 -5
- package/dist/_chunks/mcp.mjs.map +1 -1
- package/dist/_chunks/tool-operations.mjs +40 -7
- package/dist/_chunks/tool-operations.mjs.map +1 -1
- package/dist/_chunks/types.mjs +1 -1
- package/dist/_chunks/types.mjs.map +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/mcp.d.mts.map +1 -1
- package/dist/tool-operations.d.mts +9 -0
- package/dist/tool-operations.d.mts.map +1 -1
- package/package.json +15 -12
- package/packages/omp/extensions/harnesses.ts +14 -8
- package/packages/pi/extensions/harnesses.ts +30 -8
- package/packages/shared/tui.ts +4 -2
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @agntn/harnesses
|
|
2
2
|
|
|
3
|
-
[](https://npmx.dev/package/@agntn/harnesses)
|
|
4
|
+
[](https://npmx.dev/package/@agntn/harnesses)
|
|
5
|
+
[](https://npmx.dev/package/@agntn/harnesses)
|
|
6
6
|
[](https://deepwiki.com/agntn/harnesses)
|
|
7
7
|
|
|
8
8
|
Metadata toolkit for AI coding harnesses. One registry of paths, formats, and detection rules for every major CLI.
|
|
9
9
|
|
|
10
|
+
Docs: [harnesses.agntn.dev](https://harnesses.agntn.dev)
|
|
11
|
+
|
|
10
12
|
## Install
|
|
11
13
|
|
|
12
14
|
```bash
|
|
@@ -24,12 +26,12 @@ console.log(claude.hooks); // [{ path: ".claude/hooks/", scope: "project", ... }
|
|
|
24
26
|
console.log(claude.invocationModes); // advisor and full agent modes, no read-only mode
|
|
25
27
|
|
|
26
28
|
const codex = getHarness("codex");
|
|
27
|
-
await codex.invoke("Review this patch", { readOnly: true });
|
|
29
|
+
await codex.invoke("Review this patch", { readOnly: true, timeoutMs: 60_000 });
|
|
28
30
|
|
|
29
31
|
const pi = getHarness("pi");
|
|
30
32
|
const { models } = await pi.listModels({ search: "gpt-5.4" });
|
|
31
33
|
console.log(models); // [{ provider: "openai-codex", id: "gpt-5.4", ... }]
|
|
32
|
-
await pi.invoke("Review this patch", { model: "openai-codex/gpt-5.4" });
|
|
34
|
+
await pi.invoke("Review this patch", { model: "openai-codex/gpt-5.4", readOnly: true });
|
|
33
35
|
|
|
34
36
|
// Resolve to absolute paths for current platform
|
|
35
37
|
const paths = claude.resolve({ platform: "linux", homeDir: "/home/dev" });
|
|
@@ -89,6 +91,24 @@ import type { ClaudeSessionEntry, CodexThread, GeminiConversationRecord } from "
|
|
|
89
91
|
|
|
90
92
|
Primary references: [Antigravity prompting](https://antigravity.google/docs/cli/prompting/), [Antigravity changelog](https://github.com/google-antigravity/antigravity-cli/blob/main/CHANGELOG.md), [Gemini CLI tools](https://github.com/google-gemini/gemini-cli/blob/main/docs/reference/tools.md), [Gemini CLI video request](https://github.com/google-gemini/gemini-cli/issues/27194), [OpenCode attachments](https://opencode.ai/v2/docs/attachments/), [Cursor prompting](https://cursor.com/docs/agent/prompting), and [Copilot CLI voice input](https://docs.github.com/en/copilot/how-tos/copilot-cli/use-copilot-cli/voice-input).
|
|
91
93
|
|
|
94
|
+
`invoke()` and `listModels()` accept `timeoutMs` and `signal?: AbortSignal`. Unset or `0` means no deadline. An already aborted signal skips spawning. Otherwise, the first cancellation or deadline starts cleanup: Linux and macOS use a dedicated process group, with `SIGTERM` followed by `SIGKILL` after 500 ms even if the root has exited. Windows uses `taskkill /T /F` immediately, with a 2 s budget for that command. Cleanup failures reject the call.
|
|
95
|
+
|
|
96
|
+
Stopped results retain captured output and set `exitCode: null`. Caller cancellation sets `aborted: true`, a deadline sets `timedOut: true`, and the first reason wins. Both flags are false on normal completion. Later aborts do nothing, and cancelled model listings return no parsed models. Pi, OMP, and MCP tools forward their host request signal, not a JSON argument supplied by the model.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const controller = new AbortController();
|
|
100
|
+
const pending = getHarness("pi").invoke("Review this change", {
|
|
101
|
+
tools: true,
|
|
102
|
+
readOnly: true,
|
|
103
|
+
signal: controller.signal,
|
|
104
|
+
});
|
|
105
|
+
controller.abort();
|
|
106
|
+
const result = await pending;
|
|
107
|
+
console.log(result.aborted);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This is command cleanup, not a sandbox. Descendants that leave the POSIX process group, or outlive an already exited root on Windows, cannot be reliably reached by these mechanisms. Inherited output pipes do not extend the wait after cleanup. Scheduling and OS delays can exceed the stated budgets.
|
|
111
|
+
|
|
92
112
|
Each agent is a concrete subclass of the abstract `Harness` class. Custom subclasses can be added with `registerHarness`. Every harness exposes config paths, session locations, instruction files, skills dirs, hooks, commands, persistence formats, capabilities (MCP, vision, audio, video, tools, streaming), detection rules, a normalized non-interactive invocation (`harness.invoke(prompt, { model })`) where the CLI has a headless mode, native model listing (`harness.listModels()`) where the CLI supports it, and its MCP server config files (`listMcpServers`/`addMcpServer`/`removeMcpServer` normalize the dialects; writes rewrite JSON and surgically edit TOML with comments preserved). `syncMcpServers` treats `~/.config/agntn/mcp.jsonc` (JSONC, XDG-aware) as the single source of truth and resets every harness's user-scope MCP config to exactly that list; a top-level `"excludes": ["codex"]` array opts individual harnesses out of the sync (their own servers stay, master-listed names are withdrawn), and `~`/`${HOME}` in commands, args, and env values expand to absolute paths at sync time (harnesses spawn MCP servers without a shell). `syncAgentsFiles` links every harness's global instructions file (CLAUDE.md/AGENTS.md/GEMINI.md) to one master file as symlinks, so an edit made through any harness lands in the single physical copy; `~/.config/agntn/agents.jsonc` sets the `source`, `companions`, and `excludes`, diverged regular files are backed up and relinked, and check mode reports without writing. Companion paths are relative to the source directory and are linked at the same relative path beside each harness target.
|
|
93
113
|
|
|
94
114
|
```jsonc
|
|
@@ -125,7 +145,7 @@ harnesses agents sync --check # doctor: link global AGENTS.md files to one mas
|
|
|
125
145
|
harnesses mcp # run the MCP server over stdio
|
|
126
146
|
```
|
|
127
147
|
|
|
128
|
-
`tools` defaults to `false` in the library and CLI. The MCP, Pi, and OMP tools require agents to choose it explicitly. `false` must use a native CLI flag that removes tools from the model context; it is a lightweight advisor, not an agent constrained only by prompt wording. Set `tools: true` (or CLI `--tools`) whenever the task needs harness tools, including Grok's native X search. Add `readOnly: true` when those tools must stay inside a sandbox enforced by the harness CLI; the agent tools pass it beside `tools: true`, while the library and CLI let it imply tools. Read-only mode is rejected when a harness has no verified native recipe, so it never falls back to broader access. Harnesses whose CLI cannot disable tools reject advisor mode instead of silently running an agent and return an explicit `tools` retry when their full agent mode can handle the request.
|
|
148
|
+
`tools` defaults to `false` in the library and CLI. The MCP, Pi, and OMP tools require agents to choose it explicitly. `false` must use a native CLI flag that removes tools from the model context; it is a lightweight advisor, not an agent constrained only by prompt wording. Set `tools: true` (or CLI `--tools`) whenever the task needs harness tools, including Grok's native X search. Add `readOnly: true` when those tools must stay inside a sandbox enforced by the harness CLI; the agent tools pass it beside `tools: true`, while the library and CLI let it imply tools. Read-only mode is rejected when a harness has no verified native recipe, so it never falls back to broader access. A recipe can also carry the lowest CLI version whose enforcement was verified, and `invoke()` rejects read-only runs on older or unknown versions: Grok runs `--sandbox read-only` from 1.0.13, and Claude Code keeps read-only runs on `Read`, `Glob` and `Grep` with `--strict-mcp-config` from 2.1.175. Harnesses whose CLI cannot disable tools reject advisor mode instead of silently running an agent and return an explicit `tools` retry when their full agent mode can handle the request.
|
|
129
149
|
|
|
130
150
|
## How harnesses compares to unagent
|
|
131
151
|
|
|
@@ -40,6 +40,8 @@ interface HarnessInvocation {
|
|
|
40
40
|
readOnlyArgs?: string[];
|
|
41
41
|
/** Structured agent argument template with read-only tool access. */
|
|
42
42
|
readOnlyJsonArgs?: string[];
|
|
43
|
+
/** Lowest CLI version whose read-only enforcement was verified; older or unknown versions reject read-only runs. */
|
|
44
|
+
readOnlyMinVersion?: string;
|
|
43
45
|
/** Arguments appended when a model is selected; every "{model}" is replaced. */
|
|
44
46
|
modelArgs?: string[];
|
|
45
47
|
level: EvidenceLevel;
|
|
@@ -77,8 +79,10 @@ interface ListModelsOptions {
|
|
|
77
79
|
search?: string;
|
|
78
80
|
cwd?: string;
|
|
79
81
|
env?: Record<string, string>;
|
|
80
|
-
/**
|
|
82
|
+
/** Milliseconds before cleanup starts. Unset or 0 disables the deadline. */
|
|
81
83
|
timeoutMs?: number;
|
|
84
|
+
/** Cancel with the same process cleanup as a timeout. */
|
|
85
|
+
signal?: AbortSignal;
|
|
82
86
|
}
|
|
83
87
|
interface InvokeOptions {
|
|
84
88
|
cwd?: string;
|
|
@@ -89,8 +93,10 @@ interface InvokeOptions {
|
|
|
89
93
|
tools?: boolean;
|
|
90
94
|
/** Require native enforcement of read-only tool access. Implies `tools: true`. */
|
|
91
95
|
readOnly?: boolean;
|
|
92
|
-
/**
|
|
96
|
+
/** Milliseconds before cleanup starts. Unset or 0 disables the deadline. */
|
|
93
97
|
timeoutMs?: number;
|
|
98
|
+
/** Cancel with the same process cleanup as a timeout. */
|
|
99
|
+
signal?: AbortSignal;
|
|
94
100
|
/** Use the harness's structured (JSON) output mode instead of plain text. */
|
|
95
101
|
structured?: boolean;
|
|
96
102
|
}
|
|
@@ -99,9 +105,11 @@ interface InvokeResult {
|
|
|
99
105
|
args: string[];
|
|
100
106
|
stdout: string;
|
|
101
107
|
stderr: string;
|
|
102
|
-
/** Process exit code; null when
|
|
108
|
+
/** Process exit code; null when terminated by a signal, timeout, or cancellation. */
|
|
103
109
|
exitCode: number | null;
|
|
104
110
|
timedOut: boolean;
|
|
111
|
+
/** True when caller cancellation wins over the deadline or completion. */
|
|
112
|
+
aborted: boolean;
|
|
105
113
|
}
|
|
106
114
|
/** Result of one native model-listing command. */
|
|
107
115
|
interface ListModelsResult extends InvokeResult {
|
|
@@ -214,10 +222,19 @@ declare abstract class Harness {
|
|
|
214
222
|
* exits instead of waiting forever.
|
|
215
223
|
*
|
|
216
224
|
* @param prompt - Prompt sent to the harness.
|
|
217
|
-
* @param options - Invocation, environment, and
|
|
225
|
+
* @param options - Invocation, environment, timeout, and cancellation options.
|
|
218
226
|
* @returns {Promise<InvokeResult>} The completed process result.
|
|
219
227
|
*/
|
|
220
228
|
invoke(prompt: string, options?: InvokeOptions): Promise<InvokeResult>;
|
|
229
|
+
/**
|
|
230
|
+
* Explains why the installed CLI cannot run a read-only recipe that carries a
|
|
231
|
+
* version floor, or returns null when no floor applies or the installed
|
|
232
|
+
* version meets it. An unknown version fails closed, like a missing recipe.
|
|
233
|
+
*
|
|
234
|
+
* @param options - Requested execution mode.
|
|
235
|
+
* @returns {string | null} The version incompatibility, or null when the run may proceed.
|
|
236
|
+
*/
|
|
237
|
+
private readOnlyVersionError;
|
|
221
238
|
/**
|
|
222
239
|
* Expands the native model-listing recipe without spawning anything.
|
|
223
240
|
*
|
|
@@ -232,7 +249,7 @@ declare abstract class Harness {
|
|
|
232
249
|
* Runs the harness's native model-listing command and normalizes its output.
|
|
233
250
|
* stdin is closed for the same reason as {@link invoke}.
|
|
234
251
|
*
|
|
235
|
-
* @param options - Search, environment, and
|
|
252
|
+
* @param options - Search, environment, timeout, and cancellation options.
|
|
236
253
|
* @returns {Promise<ListModelsResult>} The normalized command and model result.
|
|
237
254
|
*/
|
|
238
255
|
listModels(options?: ListModelsOptions): Promise<ListModelsResult>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents-sync.d.mts","names":[],"sources":["../../src/types.ts","../../src/harness.ts","../../src/mcp-servers.ts","../../src/agents-sync.ts"],"mappings":"AAEA,cAAa;KAED;KAaA;KACA;UAEK;EACf;EACA;EACA,OAAO;EACP,YAAY;EACZ;;UAGe;EACf;EACA,OAAO;EACP;;UAGe;EACf;EACA;;EAEA;;EAEA;EACA;EACA;;;UAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;EACA,OAAO;EACP;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;;;UAIe;;EAEf;;EAEA;EACA,OAAO;EACP;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA,MAAM;;EAEN;;
|
|
1
|
+
{"version":3,"file":"agents-sync.d.mts","names":[],"sources":["../../src/types.ts","../../src/harness.ts","../../src/mcp-servers.ts","../../src/agents-sync.ts"],"mappings":"AAEA,cAAa;KAED;KAaA;KACA;UAEK;EACf;EACA;EACA,OAAO;EACP,YAAY;EACZ;;UAGe;EACf;EACA,OAAO;EACP;;UAGe;EACf;EACA;;EAEA;;EAEA;EACA;EACA;;;UAIe;;EAEf;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA;EACA,OAAO;EACP;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;;;UAIe;;EAEf;;EAEA;EACA,OAAO;EACP;;;UAIe;EACf;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf;EACA;EACA,MAAM;;EAEN;;EAEA,SAAS;;UAGM;EACf;EACA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA;;EAEA,SAAS;;EAET;;UAGe;EACf;EACA;EACA;EACA;;EAEA;EACA;;EAEA;;;UAIe,yBAAyB;;EAExC,QAAQ;;;UAIO;EACf;EACA;EACA;EACA;EACA,MAAM;EACN;EACA,UAAU;;EAEV;;;UAIe,sBAAsB;EACrC;;EAEA;;EAEA;;UAGe;;EAEf;;EAEA;;UAGe;EACf;EACA;EACA,WAAW;;UAGI;EACf,QAAQ;EACR,UAAU;EACV,cAAc;EACd,QAAQ;EACR,UAAU;EACV,OAAO;;KCnJJ,oBAAoB;EACvB;EACA;EACA;EACA;;uBA2OoB;oBACF,IAAI;oBACJ;oBACA;oBACA,QAAQ;oBACR,UAAU;oBACV,aAAa;oBACb,cAAc;oBACd,QAAQ;oBACR,UAAU;oBACV,OAAO;oBACP,cAAc;oBACd,WAAW;;oBAEX,YAAY;;WAErB,cAAc;;WAEd,YAAY;;;;;;WAMZ;EAET;EAYA;EAIA,cAAc;MAKV;;;;;;MAsBA,mBAAmB;;;;;;;;;;EAoBvB,gBACE,gBACA,UAAS;IACN;IAAiB;;;;;;;;EAmBtB,gBAAgB,UAAS;;;;;;;;;;EAqBzB,OAAO,gBAAgB,UAAS,gBAAqB,QAAQ;;;;;;;;;UA2BrD;;;;;;;EAgBR,yBAAyB;IAAoB;IAAiB;;;;;;;;;EAmBxD,WAAW,UAAS,oBAAyB,QAAQ;;;;;;YAyBjD,wBAAwB,kBAAkB;;;;;;;;;;EAapD,kBAAkB,UAAU,eAC1B,kBAAkB,KAClB,UAAS,iBACR;EAcH,QAAQ,UAAS,iBAAsB;;KAY7B,+BAA+B;;UCxgB1B;EACf;EACA,OAAO;EACP,QAAQ;EACR,OAAO;EACP;EACA;EACA,SAAS;;EAET;;;;;;;;;;iBAmRc,eAAe,SAAS,SAAS,UAAS,iBAAsB;;;;;;;;;;;;iBAiLhE,aACd,SAAS,SACT,QAAQ,iBACR,4BACA,UAAS;EACN;EAAc;;;;;;;;;;;iBA0BH,gBACd,SAAS,SACT,cACA,4BACA,UAAS;EACN;EAAc;;;;;;;;;iBA8FH,WAAW;;;;;;;iBAwBX,cAAc,UAAS;;UAKtB;EACf;EACA;;EAEA;;EAEA;EACA,SAAS;IAAQ;IAAc;;;;UAIhB;EACf;EACA;EACA,SAAS;;;;;;;;iBAuEK,qBAAqB,UAAS;EAC5C;EACA,SAAS;;EAET;;;;;;;;;;;iBA2Hc,eACd,oBAAoB,WACpB,UAAS,iBACR;;UC1zBc;;EAEf;;EAEA;;EAEA;;EAEA;;KAGU;;UAGK;WACN;WACA;WACA,QAAQ,QAAQ;;WAEhB;;;UAIM;EACf;EACA;EACA,QAAQ;;EAER;;EAEA,sBAAsB;;;UAIP;EACf;EACA;EACA,SAAS;;;;;;;;iBA2FK,iBAAiB,UAAS,iBAAsB;;;;;;;;;;iBAiMhD,gBACd,oBAAoB,WACpB,iBACA,UAAS,iBACR"}
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
import { addAbortListener } from "node:events";
|
|
1
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
|
|
4
6
|
import os, { homedir } from "node:os";
|
|
5
7
|
import { stripVTControlCharacters } from "node:util";
|
|
6
8
|
import { parse } from "smol-toml";
|
|
9
|
+
const PATH_MARKERS = /^~(?=\/|$)|\$\{HOME\}|\$\{PROJECT_ROOT\}|%([^%]+)%/g;
|
|
7
10
|
function resolvePathTemplate(template, options = {}) {
|
|
8
11
|
const homeDir = options.homeDir ?? os.homedir();
|
|
9
12
|
const projectRoot = options.projectRoot ?? process.cwd();
|
|
10
|
-
return template.
|
|
13
|
+
return template.replaceAll(PATH_MARKERS, (match, name) => {
|
|
14
|
+
if (match === "${PROJECT_ROOT}") return projectRoot;
|
|
15
|
+
if (name === void 0) return homeDir;
|
|
16
|
+
return process.env[name] ?? match;
|
|
17
|
+
});
|
|
11
18
|
}
|
|
12
19
|
function agntnConfigDir(options = {}) {
|
|
13
20
|
const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME !== "" ? process.env.XDG_CONFIG_HOME : join(options.homeDir ?? os.homedir(), ".config");
|
|
@@ -53,13 +60,75 @@ function invocationTemplate(invocation, mode) {
|
|
|
53
60
|
case "agentStructured": return invocation.jsonArgs;
|
|
54
61
|
}
|
|
55
62
|
}
|
|
63
|
+
function invocationRetryHint(invocation, mode, options) {
|
|
64
|
+
const withoutStructured = requestedInvocationMode({
|
|
65
|
+
...options,
|
|
66
|
+
structured: false
|
|
67
|
+
});
|
|
68
|
+
if (options.structured === true) {
|
|
69
|
+
if (invocationTemplate(invocation, withoutStructured)) return "; retry with structured: false";
|
|
70
|
+
const alternateWithoutStructured = ALTERNATE_INVOCATION_MODE[withoutStructured];
|
|
71
|
+
if (alternateWithoutStructured !== void 0 && invocationTemplate(invocation, alternateWithoutStructured)) return `; retry with structured: false and tools: ${!options.tools}`;
|
|
72
|
+
}
|
|
73
|
+
const alternateMode = ALTERNATE_INVOCATION_MODE[mode];
|
|
74
|
+
return (alternateMode === void 0 ? void 0 : invocationTemplate(invocation, alternateMode)) ? INVOCATION_RETRY_HINT[mode] ?? "" : "";
|
|
75
|
+
}
|
|
76
|
+
function compareVersions(a, b) {
|
|
77
|
+
const [aBase = "", aPre] = a.split("-", 2);
|
|
78
|
+
const [bBase = "", bPre] = b.split("-", 2);
|
|
79
|
+
const left = aBase.split(".").map(Number);
|
|
80
|
+
const right = bBase.split(".").map(Number);
|
|
81
|
+
for (let i = 0; i < Math.max(left.length, right.length); i++) {
|
|
82
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
83
|
+
if (diff !== 0) return diff;
|
|
84
|
+
}
|
|
85
|
+
return Number(aPre === void 0) - Number(bPre === void 0);
|
|
86
|
+
}
|
|
56
87
|
function buildInvocationArgs(template, prompt, modelArgs, model) {
|
|
57
|
-
const args = template.map((arg) => arg.replaceAll("{prompt}", prompt));
|
|
58
|
-
if (model !== void 0 && modelArgs) args.push(...modelArgs.map((arg) => arg.replaceAll("{model}", model)));
|
|
88
|
+
const args = template.map((arg) => arg.replaceAll("{prompt}", () => prompt));
|
|
89
|
+
if (model !== void 0 && modelArgs) args.push(...modelArgs.map((arg) => arg.replaceAll("{model}", () => model)));
|
|
59
90
|
return args;
|
|
60
91
|
}
|
|
92
|
+
function signalProcessGroup(pid, signal) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(-pid, signal);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (!(error instanceof Error && "code" in error && error.code === "ESRCH")) throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function terminateCommand(pid) {
|
|
100
|
+
if (process.platform === "win32") {
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
execFile(join(process.env.SystemRoot ?? "C:\\Windows", "System32", "taskkill.exe"), [
|
|
103
|
+
"/PID",
|
|
104
|
+
String(pid),
|
|
105
|
+
"/T",
|
|
106
|
+
"/F"
|
|
107
|
+
], {
|
|
108
|
+
windowsHide: true,
|
|
109
|
+
timeout: 2e3,
|
|
110
|
+
killSignal: "SIGKILL"
|
|
111
|
+
}, (error) => error ? reject(error) : resolve());
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
signalProcessGroup(pid, "SIGTERM");
|
|
116
|
+
await setTimeout$1(500);
|
|
117
|
+
signalProcessGroup(pid, "SIGKILL");
|
|
118
|
+
}
|
|
61
119
|
function executeCommand(command, args, options) {
|
|
62
120
|
return new Promise((resolve, reject) => {
|
|
121
|
+
let stdout = "";
|
|
122
|
+
let stderr = "";
|
|
123
|
+
let stopped;
|
|
124
|
+
let settled = false;
|
|
125
|
+
let timer;
|
|
126
|
+
let abortListener;
|
|
127
|
+
if (options.signal?.aborted) {
|
|
128
|
+
stopped = "abort";
|
|
129
|
+
finish(null);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
63
132
|
const child = spawn(command, args, {
|
|
64
133
|
cwd: options.cwd,
|
|
65
134
|
env: options.env ? {
|
|
@@ -70,36 +139,60 @@ function executeCommand(command, args, options) {
|
|
|
70
139
|
"ignore",
|
|
71
140
|
"pipe",
|
|
72
141
|
"pipe"
|
|
73
|
-
]
|
|
142
|
+
],
|
|
143
|
+
detached: process.platform !== "win32" && (Boolean(options.timeoutMs) || options.signal !== void 0)
|
|
74
144
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
let timedOut = false;
|
|
78
|
-
const timer = options.timeoutMs ? setTimeout(() => {
|
|
79
|
-
timedOut = true;
|
|
80
|
-
child.kill("SIGTERM");
|
|
81
|
-
}, options.timeoutMs) : void 0;
|
|
145
|
+
if (options.timeoutMs) timer = setTimeout(() => stop("timeout"), options.timeoutMs);
|
|
146
|
+
if (options.signal) abortListener = addAbortListener(options.signal, () => stop("abort"));
|
|
82
147
|
child.stdout.on("data", (chunk) => {
|
|
83
148
|
stdout += chunk.toString("utf8");
|
|
84
149
|
});
|
|
85
150
|
child.stderr.on("data", (chunk) => {
|
|
86
151
|
stderr += chunk.toString("utf8");
|
|
87
152
|
});
|
|
88
|
-
child.on("error",
|
|
89
|
-
if (timer) clearTimeout(timer);
|
|
90
|
-
reject(error);
|
|
91
|
-
});
|
|
153
|
+
child.on("error", fail);
|
|
92
154
|
child.on("close", (code) => {
|
|
155
|
+
if (stopped === void 0) finish(code);
|
|
156
|
+
});
|
|
157
|
+
function cleanup() {
|
|
93
158
|
if (timer) clearTimeout(timer);
|
|
159
|
+
abortListener?.[Symbol.dispose]();
|
|
160
|
+
}
|
|
161
|
+
function fail(error) {
|
|
162
|
+
if (settled) return;
|
|
163
|
+
settled = true;
|
|
164
|
+
cleanup();
|
|
165
|
+
reject(error);
|
|
166
|
+
}
|
|
167
|
+
function stop(reason) {
|
|
168
|
+
if (settled || stopped !== void 0) return;
|
|
169
|
+
stopped = reason;
|
|
170
|
+
cleanup();
|
|
171
|
+
if (child.pid === void 0) return;
|
|
172
|
+
terminateCommand(child.pid).then(() => {
|
|
173
|
+
child.stdout.destroy();
|
|
174
|
+
child.stderr.destroy();
|
|
175
|
+
finish(null);
|
|
176
|
+
}, (error) => {
|
|
177
|
+
child.stdout.destroy();
|
|
178
|
+
child.stderr.destroy();
|
|
179
|
+
fail(error);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function finish(code) {
|
|
183
|
+
if (settled) return;
|
|
184
|
+
settled = true;
|
|
185
|
+
cleanup();
|
|
94
186
|
resolve({
|
|
95
187
|
command,
|
|
96
188
|
args: [...args],
|
|
97
189
|
stdout,
|
|
98
190
|
stderr,
|
|
99
|
-
exitCode:
|
|
100
|
-
timedOut
|
|
191
|
+
exitCode: stopped === void 0 ? code : null,
|
|
192
|
+
timedOut: stopped === "timeout",
|
|
193
|
+
aborted: stopped === "abort"
|
|
101
194
|
});
|
|
102
|
-
}
|
|
195
|
+
}
|
|
103
196
|
});
|
|
104
197
|
}
|
|
105
198
|
var Harness = class {
|
|
@@ -168,9 +261,7 @@ var Harness = class {
|
|
|
168
261
|
if (options.model !== void 0 && !this.invocation.modelArgs) return `Harness ${this.id} does not support model selection`;
|
|
169
262
|
const mode = requestedInvocationMode(options);
|
|
170
263
|
if (invocationTemplate(this.invocation, mode)) return null;
|
|
171
|
-
|
|
172
|
-
const retry = (alternateMode === void 0 ? void 0 : invocationTemplate(this.invocation, alternateMode)) ? INVOCATION_RETRY_HINT[mode] ?? "" : "";
|
|
173
|
-
return `Harness ${this.id} has no ${INVOCATION_MODE_DESCRIPTION[mode]} invocation${retry}`;
|
|
264
|
+
return `${`Harness ${this.id} has no ${INVOCATION_MODE_DESCRIPTION[mode]} invocation`}${invocationRetryHint(this.invocation, mode, options)}`;
|
|
174
265
|
}
|
|
175
266
|
invoke(prompt, options = {}) {
|
|
176
267
|
const invocationOptions = {
|
|
@@ -181,8 +272,19 @@ var Harness = class {
|
|
|
181
272
|
};
|
|
182
273
|
const built = this.buildInvocation(prompt, invocationOptions);
|
|
183
274
|
if (!built) return Promise.reject(new Error(this.invocationError(invocationOptions) ?? "Invalid invocation"));
|
|
275
|
+
const versionError = this.readOnlyVersionError(invocationOptions);
|
|
276
|
+
if (versionError) return Promise.reject(new Error(versionError));
|
|
184
277
|
return executeCommand(built.command, built.args, options);
|
|
185
278
|
}
|
|
279
|
+
readOnlyVersionError(options) {
|
|
280
|
+
const floor = this.invocation?.readOnlyMinVersion;
|
|
281
|
+
if (floor === void 0 || options.readOnly !== true) return null;
|
|
282
|
+
const installed = this.version;
|
|
283
|
+
const error = `Harness ${this.id} requires version ${floor} or newer for read-only runs`;
|
|
284
|
+
if (installed === null) return `${error}; installed version unknown`;
|
|
285
|
+
if (compareVersions(installed, floor) < 0) return `${error}; installed ${installed}`;
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
186
288
|
buildModelListInvocation(search) {
|
|
187
289
|
if (!this.modelListing) return null;
|
|
188
290
|
const command = this.binaries[0];
|
|
@@ -191,7 +293,7 @@ var Harness = class {
|
|
|
191
293
|
if (!template) return null;
|
|
192
294
|
return {
|
|
193
295
|
command,
|
|
194
|
-
args: template.map((arg) => arg.replaceAll("{search}", search ?? ""))
|
|
296
|
+
args: template.map((arg) => arg.replaceAll("{search}", () => search ?? ""))
|
|
195
297
|
};
|
|
196
298
|
}
|
|
197
299
|
async listModels(options = {}) {
|
|
@@ -200,7 +302,7 @@ var Harness = class {
|
|
|
200
302
|
const result = await executeCommand(built.command, built.args, options);
|
|
201
303
|
return {
|
|
202
304
|
...result,
|
|
203
|
-
models: !result.timedOut && result.exitCode === 0 ? this.parseModelListingOutput(result.stdout) : []
|
|
305
|
+
models: !result.timedOut && !result.aborted && result.exitCode === 0 ? this.parseModelListingOutput(result.stdout) : []
|
|
204
306
|
};
|
|
205
307
|
}
|
|
206
308
|
parseModelListingOutput(_stdout) {
|
|
@@ -531,9 +633,26 @@ var Claude = class extends Harness {
|
|
|
531
633
|
"--tools",
|
|
532
634
|
""
|
|
533
635
|
],
|
|
636
|
+
readOnlyArgs: [
|
|
637
|
+
"-p",
|
|
638
|
+
"{prompt}",
|
|
639
|
+
"--strict-mcp-config",
|
|
640
|
+
"--tools",
|
|
641
|
+
"Read,Glob,Grep"
|
|
642
|
+
],
|
|
643
|
+
readOnlyJsonArgs: [
|
|
644
|
+
"-p",
|
|
645
|
+
"--output-format",
|
|
646
|
+
"json",
|
|
647
|
+
"{prompt}",
|
|
648
|
+
"--strict-mcp-config",
|
|
649
|
+
"--tools",
|
|
650
|
+
"Read,Glob,Grep"
|
|
651
|
+
],
|
|
652
|
+
readOnlyMinVersion: "2.1.175",
|
|
534
653
|
modelArgs: ["--model", "{model}"],
|
|
535
654
|
level: "official",
|
|
536
|
-
note: "Headless print mode; add --output-format json for structured output."
|
|
655
|
+
note: "Headless print mode; add --output-format json for structured output. Read-only runs keep the built-in Read, Glob and Grep tools and --strict-mcp-config drops every configured MCP server, so no write tool exists whatever permission mode the settings carry; verified on 2.1.175 and 2.1.268. --permission-mode plan does not qualify: its shell commands run through the auto mode classifier, which let a file write through."
|
|
537
656
|
};
|
|
538
657
|
mcpConfigs = [{
|
|
539
658
|
path: "~/.claude.json",
|
|
@@ -691,20 +810,27 @@ var Codex = class extends Harness {
|
|
|
691
810
|
commands = [];
|
|
692
811
|
hooks = [];
|
|
693
812
|
invocation = {
|
|
694
|
-
args: [
|
|
813
|
+
args: [
|
|
814
|
+
"exec",
|
|
815
|
+
"--skip-git-repo-check",
|
|
816
|
+
"{prompt}"
|
|
817
|
+
],
|
|
695
818
|
jsonArgs: [
|
|
696
819
|
"exec",
|
|
820
|
+
"--skip-git-repo-check",
|
|
697
821
|
"--json",
|
|
698
822
|
"{prompt}"
|
|
699
823
|
],
|
|
700
824
|
readOnlyArgs: [
|
|
701
825
|
"exec",
|
|
826
|
+
"--skip-git-repo-check",
|
|
702
827
|
"--sandbox",
|
|
703
828
|
"read-only",
|
|
704
829
|
"{prompt}"
|
|
705
830
|
],
|
|
706
831
|
readOnlyJsonArgs: [
|
|
707
832
|
"exec",
|
|
833
|
+
"--skip-git-repo-check",
|
|
708
834
|
"--sandbox",
|
|
709
835
|
"read-only",
|
|
710
836
|
"--json",
|
|
@@ -1069,7 +1195,7 @@ var Gemini = class extends Harness {
|
|
|
1069
1195
|
var GitHubCopilot = class extends Harness {
|
|
1070
1196
|
id = "github-copilot";
|
|
1071
1197
|
name = "GitHub Copilot";
|
|
1072
|
-
binaries = [];
|
|
1198
|
+
binaries = ["copilot"];
|
|
1073
1199
|
capabilities = {
|
|
1074
1200
|
mcp: true,
|
|
1075
1201
|
vision: true,
|
|
@@ -1247,9 +1373,24 @@ var Grok = class extends Harness {
|
|
|
1247
1373
|
"--output-format",
|
|
1248
1374
|
"json"
|
|
1249
1375
|
],
|
|
1376
|
+
readOnlyArgs: [
|
|
1377
|
+
"-p",
|
|
1378
|
+
"{prompt}",
|
|
1379
|
+
"--sandbox",
|
|
1380
|
+
"read-only"
|
|
1381
|
+
],
|
|
1382
|
+
readOnlyJsonArgs: [
|
|
1383
|
+
"-p",
|
|
1384
|
+
"{prompt}",
|
|
1385
|
+
"--sandbox",
|
|
1386
|
+
"read-only",
|
|
1387
|
+
"--output-format",
|
|
1388
|
+
"json"
|
|
1389
|
+
],
|
|
1390
|
+
readOnlyMinVersion: "1.0.13",
|
|
1250
1391
|
modelArgs: ["--model", "{model}"],
|
|
1251
1392
|
level: "official",
|
|
1252
|
-
note: "-p is short for --single; add --output-format json for structured output."
|
|
1393
|
+
note: "-p is short for --single; add --output-format json for structured output. --sandbox read-only is kernel-enforced (Landlock, Seatbelt) regardless of the inherited permission mode and still allows writes to ~/.grok and temp dirs; verified on Linux with 1.0.13 and 1.0.25."
|
|
1253
1394
|
};
|
|
1254
1395
|
mcpConfigs = [{
|
|
1255
1396
|
path: "~/.grok/config.toml",
|
|
@@ -1576,21 +1717,9 @@ var Omp = class extends Harness {
|
|
|
1576
1717
|
"json",
|
|
1577
1718
|
"{prompt}"
|
|
1578
1719
|
],
|
|
1579
|
-
noToolsArgs: [
|
|
1580
|
-
"-p",
|
|
1581
|
-
"--no-tools",
|
|
1582
|
-
"{prompt}"
|
|
1583
|
-
],
|
|
1584
|
-
noToolsJsonArgs: [
|
|
1585
|
-
"-p",
|
|
1586
|
-
"--no-tools",
|
|
1587
|
-
"--mode",
|
|
1588
|
-
"json",
|
|
1589
|
-
"{prompt}"
|
|
1590
|
-
],
|
|
1591
1720
|
modelArgs: ["--model={model}"],
|
|
1592
1721
|
level: "official",
|
|
1593
|
-
note: "Add --mode json for structured event output."
|
|
1722
|
+
note: "Add --mode json for structured event output. --no-tools disables only OMP's bundled tools, so it cannot provide an advisor mode without tools."
|
|
1594
1723
|
};
|
|
1595
1724
|
mcpConfigs = [{
|
|
1596
1725
|
path: "~/.omp/agent/mcp.json",
|
|
@@ -1883,6 +2012,20 @@ var Pi = class extends Harness {
|
|
|
1883
2012
|
"json",
|
|
1884
2013
|
"{prompt}"
|
|
1885
2014
|
],
|
|
2015
|
+
readOnlyArgs: [
|
|
2016
|
+
"-p",
|
|
2017
|
+
"--tools",
|
|
2018
|
+
"read,grep,find,ls",
|
|
2019
|
+
"{prompt}"
|
|
2020
|
+
],
|
|
2021
|
+
readOnlyJsonArgs: [
|
|
2022
|
+
"-p",
|
|
2023
|
+
"--tools",
|
|
2024
|
+
"read,grep,find,ls",
|
|
2025
|
+
"--mode",
|
|
2026
|
+
"json",
|
|
2027
|
+
"{prompt}"
|
|
2028
|
+
],
|
|
1886
2029
|
modelArgs: ["--model", "{model}"],
|
|
1887
2030
|
level: "official",
|
|
1888
2031
|
note: "Add --mode json for structured event output."
|
|
@@ -2406,7 +2549,7 @@ function canonical(server) {
|
|
|
2406
2549
|
return JSON.stringify(Object.fromEntries(Object.entries(server).sort(([a], [b]) => a.localeCompare(b))));
|
|
2407
2550
|
}
|
|
2408
2551
|
function expandHome(value, home) {
|
|
2409
|
-
return value.
|
|
2552
|
+
return value.replaceAll(/^~(?=\/|$)|\$\{HOME\}/g, () => home);
|
|
2410
2553
|
}
|
|
2411
2554
|
function expandServerHome(server, home) {
|
|
2412
2555
|
return compact({
|
|
@@ -2544,8 +2687,9 @@ function agentsConfigRecord(raw, configPath) {
|
|
|
2544
2687
|
if (typeof parsed !== "object" || parsed === null) throw new Error(`Agents config at ${configPath} is not an object`);
|
|
2545
2688
|
return parsed;
|
|
2546
2689
|
}
|
|
2547
|
-
function agentsSource(value, defaultSource, options) {
|
|
2548
|
-
if (
|
|
2690
|
+
function agentsSource(value, defaultSource, options, configPath) {
|
|
2691
|
+
if (value === void 0) return defaultSource;
|
|
2692
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`Agents config at ${configPath} has an invalid source: expected a path string`);
|
|
2549
2693
|
const expanded = resolvePathTemplate(value, options);
|
|
2550
2694
|
return isAbsolute(expanded) ? expanded : join(agntnConfigDir(options), expanded);
|
|
2551
2695
|
}
|
|
@@ -2555,9 +2699,9 @@ function agentsExcludes(value, configPath) {
|
|
|
2555
2699
|
return value;
|
|
2556
2700
|
}
|
|
2557
2701
|
function companionPath(value, configPath) {
|
|
2558
|
-
const normalized = normalize(value);
|
|
2559
|
-
if (normalized === "." || normalized === ".." || normalized.startsWith(
|
|
2560
|
-
return normalized.endsWith(
|
|
2702
|
+
const normalized = posix.normalize(value.replaceAll("\\", "/"));
|
|
2703
|
+
if (normalized === "." || normalized === ".." || normalized.startsWith("../") || posix.isAbsolute(normalized) || win32.parse(value).root !== "") throw new Error(`Agents config at ${configPath} has an unsafe companion path: ${JSON.stringify(normalized)}`);
|
|
2704
|
+
return normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
2561
2705
|
}
|
|
2562
2706
|
function comparablePath(path) {
|
|
2563
2707
|
return resolve(path).toLowerCase();
|
|
@@ -2581,7 +2725,7 @@ function readAgentsConfig(options = {}) {
|
|
|
2581
2725
|
if (raw === null) return defaults;
|
|
2582
2726
|
const record = agentsConfigRecord(raw, configPath);
|
|
2583
2727
|
return {
|
|
2584
|
-
source: agentsSource(record.source, defaults.source, options),
|
|
2728
|
+
source: agentsSource(record.source, defaults.source, options, configPath),
|
|
2585
2729
|
companions: agentsCompanions(record.companions, configPath),
|
|
2586
2730
|
excludes: agentsExcludes(record.excludes, configPath),
|
|
2587
2731
|
configPath
|