@zhijiewang/openharness 2.31.0 → 2.33.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 CHANGED
@@ -773,6 +773,14 @@ Create `.oh/RULES.md` in any repo (or run `oh init`):
773
773
 
774
774
  Rules load automatically into every session.
775
775
 
776
+ openHarness also reads any of the following project-instruction files if present (additive, parent-first):
777
+
778
+ - `CLAUDE.md` (Anthropic convention) — and hierarchical `CLAUDE.md` from parent dirs, plus `~/.claude/CLAUDE.md` for user-global
779
+ - `AGENTS.md` ([agents.md cross-tool standard](https://agents.md/), used by Codex / Cursor / Copilot / Cline / Aider) — same parent-first walk
780
+ - `CLAUDE.local.md` (gitignored personal overrides)
781
+
782
+ If a repo has `AGENTS.md` already configured for another agent, openHarness picks it up unchanged — no migration step needed.
783
+
776
784
  ## Skills & Plugins
777
785
 
778
786
  ### Skills
package/README.zh-CN.md CHANGED
@@ -772,6 +772,14 @@ description: 专注的代码审查模式
772
772
 
773
773
  规则会自动加载到每次会话中。
774
774
 
775
+ openHarness 还会自动读取以下项目指令文件(如果存在,按父目录优先合并加载):
776
+
777
+ - `CLAUDE.md`(Anthropic 约定)—— 含从父目录到项目根的层级 `CLAUDE.md` 文件,以及全局 `~/.claude/CLAUDE.md`
778
+ - `AGENTS.md`([agents.md 跨工具标准](https://agents.md/),被 Codex / Cursor / Copilot / Cline / Aider 共同采用)—— 同样的父目录优先扫描
779
+ - `CLAUDE.local.md`(gitignore 的个人覆盖)
780
+
781
+ 如果仓库已为其他 agent 配置了 `AGENTS.md`,openHarness 直接读取,无需迁移。
782
+
775
783
  ## 技能与插件
776
784
 
777
785
  ### 技能
@@ -214,6 +214,34 @@ export type OhConfig = {
214
214
  rateLimit?: number;
215
215
  allowedTools?: string[];
216
216
  };
217
+ /**
218
+ * Opt-in OS-level sandbox via `@anthropic-ai/sandbox-runtime` (an optional
219
+ * dependency). When `enabled: true`, BashTool wraps every command in
220
+ * bubblewrap (Linux) or sandbox-exec (macOS) plus a domain-allowlist
221
+ * network proxy. Windows isn't supported by the package — the wrap is a
222
+ * silent passthrough there. Off by default; users opt in via config or the
223
+ * `--sandbox` CLI flag.
224
+ *
225
+ * `network.allowedDomains` is the proxy allowlist (e.g. `["github.com",
226
+ * "registry.npmjs.org"]`); `deniedDomains` blocks specific hosts before
227
+ * the allowlist applies. `filesystem.allowWrite` defaults to `[cwd]` —
228
+ * the sandbox can write to the project tree but nowhere else.
229
+ *
230
+ * See `src/harness/sandbox-runtime.ts` and SECURITY.md for the full
231
+ * threat-model boundary.
232
+ */
233
+ sandbox?: {
234
+ enabled?: boolean;
235
+ network?: {
236
+ allowedDomains?: string[];
237
+ deniedDomains?: string[];
238
+ };
239
+ filesystem?: {
240
+ allowWrite?: string[];
241
+ denyWrite?: string[];
242
+ denyRead?: string[];
243
+ };
244
+ };
217
245
  /**
218
246
  * Environment variables injected into child processes spawned by the harness —
219
247
  * Bash/Monitor/PowerShell tool executions and MCP server subprocesses. Useful
@@ -31,10 +31,10 @@ export type MemoryEntry = {
31
31
  export declare function loadMemories(): MemoryEntry[];
32
32
  /** Build a system prompt section from loaded memories (capped at MEMORY_PROMPT_MAX_CHARS) */
33
33
  export declare function memoriesToPrompt(memories: MemoryEntry[]): string;
34
- /** A single CLAUDE.md source with its resolved content (imports inlined). */
34
+ /** A single project-instructions source with its resolved content (imports inlined). */
35
35
  export type ClaudeMdEntry = {
36
36
  path: string;
37
- source: "project" | "project-local" | "user" | "claude-dir";
37
+ source: "project" | "project-local" | "user" | "claude-dir" | "agents-md";
38
38
  content: string;
39
39
  };
40
40
  /**
@@ -47,11 +47,17 @@ export type ClaudeMdEntry = {
47
47
  */
48
48
  export declare function resolveClaudeMdImports(content: string, baseDir: string, hopsLeft?: number): string;
49
49
  /**
50
- * Load the hierarchical CLAUDE.md set in the order Anthropic documents:
51
- * 1. `./.claude/CLAUDE.md` (project, checked in)
50
+ * Load hierarchical project-instruction files. Order:
51
+ * 1. `./.claude/CLAUDE.md` (project, Anthropic convention)
52
52
  * 2. `./CLAUDE.md` (project, checked in)
53
- * 3. `./CLAUDE.local.md` (project, gitignored)
54
- * 4. `~/.claude/CLAUDE.md` (user-global)
53
+ * 3. `./AGENTS.md` (project, AGENTS.md cross-tool standard — agents.md)
54
+ * 4. `./CLAUDE.local.md` (project, gitignored)
55
+ * 5. `~/.claude/CLAUDE.md` (user-global)
56
+ *
57
+ * AGENTS.md is read alongside CLAUDE.md so OH "just works" in the 60k+ repos
58
+ * already configured for cross-agent compatibility (Codex / Cursor / Copilot
59
+ * / Cline / Aider all read AGENTS.md). Both layers concatenate into the
60
+ * system prompt; if both files exist, both contribute.
55
61
  *
56
62
  * Each file is read, `@imports` are resolved, and the results are returned in
57
63
  * load order. Missing files are skipped. The caller can format these into the
@@ -61,8 +67,11 @@ export declare function resolveClaudeMdImports(content: string, baseDir: string,
61
67
  */
62
68
  export declare function loadClaudeMdHierarchy(root?: string): ClaudeMdEntry[];
63
69
  /**
64
- * Render loaded CLAUDE.md entries as a system-prompt block. Empty when no
65
- * CLAUDE.md files exist — caller should concatenate alongside `memoriesToPrompt`.
70
+ * Render loaded project-instruction entries as a system-prompt block. Empty
71
+ * when no source files exist — caller should concatenate alongside
72
+ * `memoriesToPrompt`. Header is generic ("Project instructions") since both
73
+ * CLAUDE.md and AGENTS.md feed in; the per-entry `source:` comment
74
+ * disambiguates which file each section came from.
66
75
  */
67
76
  export declare function claudeMdToPrompt(entries: ClaudeMdEntry[]): string;
68
77
  /** Save a memory entry to the project memory directory */
@@ -130,11 +130,17 @@ function readClaudeMdIfExists(path, source) {
130
130
  }
131
131
  }
132
132
  /**
133
- * Load the hierarchical CLAUDE.md set in the order Anthropic documents:
134
- * 1. `./.claude/CLAUDE.md` (project, checked in)
133
+ * Load hierarchical project-instruction files. Order:
134
+ * 1. `./.claude/CLAUDE.md` (project, Anthropic convention)
135
135
  * 2. `./CLAUDE.md` (project, checked in)
136
- * 3. `./CLAUDE.local.md` (project, gitignored)
137
- * 4. `~/.claude/CLAUDE.md` (user-global)
136
+ * 3. `./AGENTS.md` (project, AGENTS.md cross-tool standard — agents.md)
137
+ * 4. `./CLAUDE.local.md` (project, gitignored)
138
+ * 5. `~/.claude/CLAUDE.md` (user-global)
139
+ *
140
+ * AGENTS.md is read alongside CLAUDE.md so OH "just works" in the 60k+ repos
141
+ * already configured for cross-agent compatibility (Codex / Cursor / Copilot
142
+ * / Cline / Aider all read AGENTS.md). Both layers concatenate into the
143
+ * system prompt; if both files exist, both contribute.
138
144
  *
139
145
  * Each file is read, `@imports` are resolved, and the results are returned in
140
146
  * load order. Missing files are skipped. The caller can format these into the
@@ -146,6 +152,7 @@ export function loadClaudeMdHierarchy(root = ".") {
146
152
  const candidates = [
147
153
  [join(root, ".claude", "CLAUDE.md"), "claude-dir"],
148
154
  [join(root, "CLAUDE.md"), "project"],
155
+ [join(root, "AGENTS.md"), "agents-md"],
149
156
  [join(root, "CLAUDE.local.md"), "project-local"],
150
157
  [join(homedir(), ".claude", "CLAUDE.md"), "user"],
151
158
  ];
@@ -162,13 +169,16 @@ export function loadClaudeMdHierarchy(root = ".") {
162
169
  return entries;
163
170
  }
164
171
  /**
165
- * Render loaded CLAUDE.md entries as a system-prompt block. Empty when no
166
- * CLAUDE.md files exist — caller should concatenate alongside `memoriesToPrompt`.
172
+ * Render loaded project-instruction entries as a system-prompt block. Empty
173
+ * when no source files exist — caller should concatenate alongside
174
+ * `memoriesToPrompt`. Header is generic ("Project instructions") since both
175
+ * CLAUDE.md and AGENTS.md feed in; the per-entry `source:` comment
176
+ * disambiguates which file each section came from.
167
177
  */
168
178
  export function claudeMdToPrompt(entries) {
169
179
  if (entries.length === 0)
170
180
  return "";
171
- const parts = ["# Project instructions (CLAUDE.md)"];
181
+ const parts = ["# Project instructions"];
172
182
  for (const e of entries) {
173
183
  parts.push(`<!-- source: ${e.source} (${e.path}) -->`);
174
184
  parts.push(e.content.trim());
@@ -2,10 +2,14 @@
2
2
  * Rules system — load project and global rules into agent context.
3
3
  * Discovery order:
4
4
  * 1. ~/.oh/global-rules/*.md
5
- * 2. CLAUDE.md files from parent directories down to project root (hierarchical)
5
+ * 2. CLAUDE.md / AGENTS.md files from parent directories down to project root (hierarchical)
6
6
  * 3. .oh/RULES.md
7
7
  * 4. .oh/rules/*.md
8
8
  * 5. CLAUDE.local.md (gitignored personal overrides)
9
+ *
10
+ * AGENTS.md (https://agents.md/) is read alongside CLAUDE.md at every level
11
+ * of the hierarchy so OH "just works" in repos already configured for the
12
+ * cross-tool standard (Codex, Cursor, Copilot, Cline, Aider all read it).
9
13
  */
10
14
  export declare function loadRules(projectPath?: string): string[];
11
15
  export declare function loadRulesAsPrompt(projectPath?: string): string;
@@ -2,10 +2,14 @@
2
2
  * Rules system — load project and global rules into agent context.
3
3
  * Discovery order:
4
4
  * 1. ~/.oh/global-rules/*.md
5
- * 2. CLAUDE.md files from parent directories down to project root (hierarchical)
5
+ * 2. CLAUDE.md / AGENTS.md files from parent directories down to project root (hierarchical)
6
6
  * 3. .oh/RULES.md
7
7
  * 4. .oh/rules/*.md
8
8
  * 5. CLAUDE.local.md (gitignored personal overrides)
9
+ *
10
+ * AGENTS.md (https://agents.md/) is read alongside CLAUDE.md at every level
11
+ * of the hierarchy so OH "just works" in repos already configured for the
12
+ * cross-tool standard (Codex, Cursor, Copilot, Cline, Aider all read it).
9
13
  */
10
14
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
11
15
  import { homedir } from "node:os";
@@ -13,10 +17,13 @@ import { dirname, join, parse as parsePath, resolve } from "node:path";
13
17
  import { gitRoot as getGitRoot } from "../git/index.js";
14
18
  const OH_HOME = join(homedir(), ".oh");
15
19
  /**
16
- * Walk from git root (or home) down to `projectRoot`, collecting CLAUDE.md files.
17
- * Returns them in parent-first order so more specific rules override general ones.
20
+ * Walk from git root (or home) down to `projectRoot`, collecting CLAUDE.md
21
+ * and AGENTS.md files at every level. Returns them in parent-first order so
22
+ * more specific rules override general ones. Within a single directory,
23
+ * CLAUDE.md is read before AGENTS.md (Anthropic-specific guidance first,
24
+ * then the cross-tool standard layer).
18
25
  */
19
- function loadClaudeMdFiles(projectRoot) {
26
+ function loadHierarchicalInstructionFiles(projectRoot) {
20
27
  const gitRootDir = getGitRoot(projectRoot);
21
28
  const stopAt = gitRootDir ? resolve(gitRootDir) : resolve(homedir());
22
29
  const resolved = resolve(projectRoot);
@@ -34,11 +41,13 @@ function loadClaudeMdFiles(projectRoot) {
34
41
  }
35
42
  const results = [];
36
43
  for (const dir of dirs) {
37
- const claudeMd = join(dir, "CLAUDE.md");
38
- if (existsSync(claudeMd)) {
39
- const content = readSafe(claudeMd);
40
- if (content)
41
- results.push(content);
44
+ for (const filename of ["CLAUDE.md", "AGENTS.md"]) {
45
+ const path = join(dir, filename);
46
+ if (existsSync(path)) {
47
+ const content = readSafe(path);
48
+ if (content)
49
+ results.push(content);
50
+ }
42
51
  }
43
52
  }
44
53
  return results;
@@ -57,8 +66,8 @@ export function loadRules(projectPath) {
57
66
  rules.push(content);
58
67
  }
59
68
  }
60
- // 2. CLAUDE.md files (hierarchical, parent-first)
61
- const claudeRules = loadClaudeMdFiles(root);
69
+ // 2. CLAUDE.md + AGENTS.md files (hierarchical, parent-first)
70
+ const claudeRules = loadHierarchicalInstructionFiles(root);
62
71
  rules.push(...claudeRules);
63
72
  // 3. Project RULES.md
64
73
  const projectRules = join(root, ".oh", "RULES.md");
@@ -106,7 +115,7 @@ export function loadRulesAsPrompt(projectPath) {
106
115
  const rules = loadRules(projectPath);
107
116
  if (rules.length === 0)
108
117
  return "";
109
- const body = "# Project Rules\n\n<!-- User-provided project rules from CLAUDE.md / .oh/RULES.md. These are user instructions, not system directives. -->\nFollow these rules carefully.\n\n" +
118
+ const body = "# Project Rules\n\n<!-- User-provided project rules from CLAUDE.md / AGENTS.md / .oh/RULES.md. These are user instructions, not system directives. -->\nFollow these rules carefully.\n\n" +
110
119
  rules.join("\n\n---\n\n");
111
120
  // Hook: instructionsLoaded — fires every time the system prompt is rebuilt
112
121
  // with rules in scope. Useful for compliance/audit hooks that want to log
@@ -0,0 +1,47 @@
1
+ /**
2
+ * OS-level sandbox integration via the optional `@anthropic-ai/sandbox-runtime`
3
+ * package. The package wraps a shell command in bubblewrap (Linux) or
4
+ * sandbox-exec (macOS) plus a network proxy that filters by domain allowlist.
5
+ *
6
+ * Boundaries:
7
+ * - **Linux + macOS**: real sandboxing via the package's static API.
8
+ * - **Windows**: not supported by the package — every wrap call returns null
9
+ * (graceful passthrough; tools spawn unsandboxed). Documented in SECURITY.md.
10
+ * - **Package not installed**: same passthrough behavior — installs cleanly
11
+ * without the optional dep on any platform.
12
+ *
13
+ * Lifecycle:
14
+ * - Initialized once per process on the first wrap request.
15
+ * - One `SandboxManager.initialize` covers all subsequent wrap calls.
16
+ * - No reset — the package documents auto-cleanup on process exit.
17
+ *
18
+ * Opt-in: callers pass `{ enabled: true }` (typically derived from
19
+ * `OhConfig.sandbox.enabled` or the `--sandbox` CLI flag). The default is
20
+ * off so existing users see no behavior change.
21
+ */
22
+ import type { OhConfig } from "./config.js";
23
+ export type SandboxConfig = NonNullable<OhConfig["sandbox"]>;
24
+ /**
25
+ * Returns true on Linux/macOS where sandboxing is supported. Windows is
26
+ * unsupported by the underlying package, so we short-circuit there to avoid
27
+ * a misleading "tried to load and failed" log.
28
+ */
29
+ export declare function isSandboxAvailable(): boolean;
30
+ /**
31
+ * Wrap a shell command for sandboxed execution.
32
+ *
33
+ * Returns the wrapped command (a single shell string suitable for
34
+ * `spawn(cmd, { shell: "/bin/bash" })`) when sandboxing is enabled and
35
+ * available. Returns null in every other case — Windows, missing package,
36
+ * disabled config, init failure — so the caller falls through to the
37
+ * unsandboxed code path unchanged.
38
+ */
39
+ export declare function wrapForSandbox(command: string, config: SandboxConfig): Promise<string | null>;
40
+ /**
41
+ * Test-only: reset the cached init promise so unit tests can re-init with
42
+ * different configs.
43
+ *
44
+ * @internal
45
+ */
46
+ export declare function _resetSandboxForTest(): void;
47
+ //# sourceMappingURL=sandbox-runtime.d.ts.map
@@ -0,0 +1,100 @@
1
+ /**
2
+ * OS-level sandbox integration via the optional `@anthropic-ai/sandbox-runtime`
3
+ * package. The package wraps a shell command in bubblewrap (Linux) or
4
+ * sandbox-exec (macOS) plus a network proxy that filters by domain allowlist.
5
+ *
6
+ * Boundaries:
7
+ * - **Linux + macOS**: real sandboxing via the package's static API.
8
+ * - **Windows**: not supported by the package — every wrap call returns null
9
+ * (graceful passthrough; tools spawn unsandboxed). Documented in SECURITY.md.
10
+ * - **Package not installed**: same passthrough behavior — installs cleanly
11
+ * without the optional dep on any platform.
12
+ *
13
+ * Lifecycle:
14
+ * - Initialized once per process on the first wrap request.
15
+ * - One `SandboxManager.initialize` covers all subsequent wrap calls.
16
+ * - No reset — the package documents auto-cleanup on process exit.
17
+ *
18
+ * Opt-in: callers pass `{ enabled: true }` (typically derived from
19
+ * `OhConfig.sandbox.enabled` or the `--sandbox` CLI flag). The default is
20
+ * off so existing users see no behavior change.
21
+ */
22
+ // Cached, lazy-initialized handle. We deliberately don't expose this — callers
23
+ // only see `wrapForSandbox` / `isSandboxAvailable` / `resetSandboxForTest`.
24
+ let _initPromise = null;
25
+ /**
26
+ * Returns true on Linux/macOS where sandboxing is supported. Windows is
27
+ * unsupported by the underlying package, so we short-circuit there to avoid
28
+ * a misleading "tried to load and failed" log.
29
+ */
30
+ export function isSandboxAvailable() {
31
+ return process.platform === "linux" || process.platform === "darwin";
32
+ }
33
+ async function loadAndInitialize(config) {
34
+ if (!isSandboxAvailable())
35
+ return null;
36
+ let mod;
37
+ try {
38
+ mod = (await import("@anthropic-ai/sandbox-runtime"));
39
+ }
40
+ catch {
41
+ // Optional dep not installed — graceful passthrough.
42
+ return null;
43
+ }
44
+ try {
45
+ await mod.SandboxManager.initialize({
46
+ network: {
47
+ allowedDomains: config.network?.allowedDomains ?? [],
48
+ deniedDomains: config.network?.deniedDomains ?? [],
49
+ },
50
+ filesystem: {
51
+ allowWrite: config.filesystem?.allowWrite ?? [process.cwd()],
52
+ denyWrite: config.filesystem?.denyWrite ?? [],
53
+ denyRead: config.filesystem?.denyRead ?? [],
54
+ },
55
+ });
56
+ }
57
+ catch {
58
+ // Init can fail when bubblewrap / sandbox-exec aren't installed, or when
59
+ // the user's profile rejects the proxy ports. Falling back to passthrough
60
+ // is correct — opting in promised "use sandbox if you can," not "fail
61
+ // closed" — that's a separate `requireSandbox` mode for a future revision.
62
+ return null;
63
+ }
64
+ return mod;
65
+ }
66
+ /**
67
+ * Wrap a shell command for sandboxed execution.
68
+ *
69
+ * Returns the wrapped command (a single shell string suitable for
70
+ * `spawn(cmd, { shell: "/bin/bash" })`) when sandboxing is enabled and
71
+ * available. Returns null in every other case — Windows, missing package,
72
+ * disabled config, init failure — so the caller falls through to the
73
+ * unsandboxed code path unchanged.
74
+ */
75
+ export async function wrapForSandbox(command, config) {
76
+ if (!config.enabled)
77
+ return null;
78
+ if (!_initPromise) {
79
+ _initPromise = loadAndInitialize(config);
80
+ }
81
+ const mod = await _initPromise;
82
+ if (!mod)
83
+ return null;
84
+ try {
85
+ return await mod.SandboxManager.wrapWithSandbox(command);
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ /**
92
+ * Test-only: reset the cached init promise so unit tests can re-init with
93
+ * different configs.
94
+ *
95
+ * @internal
96
+ */
97
+ export function _resetSandboxForTest() {
98
+ _initPromise = null;
99
+ }
100
+ //# sourceMappingURL=sandbox-runtime.js.map
@@ -1,5 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { z } from "zod";
3
+ import { readOhConfig } from "../../harness/config.js";
4
+ import { wrapForSandbox } from "../../harness/sandbox-runtime.js";
3
5
  import { safeEnv } from "../../utils/safe-env.js";
4
6
  const inputSchema = z.object({
5
7
  command: z.string(),
@@ -21,12 +23,30 @@ export const BashTool = {
21
23
  isConcurrencySafe() {
22
24
  return false;
23
25
  },
24
- call(input, context) {
26
+ async call(input, context) {
25
27
  // input.timeout is in seconds; convert to ms. Default 120s.
26
28
  const timeoutMs = Math.min((input.timeout ?? 120) * 1000, MAX_TIMEOUT);
27
29
  const isWin = process.platform === "win32";
28
- const shell = isWin ? "cmd.exe" : "/bin/bash";
29
- const shellArgs = isWin ? ["/c", input.command] : ["-c", input.command];
30
+ // Optional OS-level sandbox via @anthropic-ai/sandbox-runtime. Returns null
31
+ // when disabled / on Windows / when the optional dep isn't installed —
32
+ // caller falls back to the existing unsandboxed spawn unchanged.
33
+ const sandboxCfg = readOhConfig()?.sandbox;
34
+ const wrappedCommand = sandboxCfg ? await wrapForSandbox(input.command, sandboxCfg) : null;
35
+ let shell;
36
+ let shellArgs;
37
+ let extraSpawnOpts = {};
38
+ if (wrappedCommand) {
39
+ // sandbox-runtime returns a shell-string. Pin the shell to /bin/bash so
40
+ // the surrounding command syntax (heredocs, $((...)) etc.) keeps working
41
+ // — `shell: true` would default to /bin/sh on Linux.
42
+ shell = wrappedCommand;
43
+ shellArgs = [];
44
+ extraSpawnOpts = { shell: "/bin/bash" };
45
+ }
46
+ else {
47
+ shell = isWin ? "cmd.exe" : "/bin/bash";
48
+ shellArgs = isWin ? ["/c", input.command] : ["-c", input.command];
49
+ }
30
50
  // Background execution: spawn and return immediately
31
51
  if (input.run_in_background) {
32
52
  const bgId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
@@ -35,9 +55,13 @@ export const BashTool = {
35
55
  env: safeEnv(),
36
56
  stdio: ["ignore", "pipe", "pipe"],
37
57
  detached: false,
58
+ ...extraSpawnOpts,
38
59
  });
39
60
  let stdout = "";
40
61
  let stderr = "";
62
+ // stdio is fixed to ["ignore", "pipe", "pipe"] above, so stdout/stderr
63
+ // are always streams. Adding `...extraSpawnOpts` widens the spawn
64
+ // overload's return type to potentially-null pipes; assert non-null.
41
65
  proc.stdout.on("data", (chunk) => {
42
66
  stdout += chunk.toString();
43
67
  });
@@ -76,11 +100,15 @@ export const BashTool = {
76
100
  cwd: context.workingDir,
77
101
  env: safeEnv(),
78
102
  stdio: ["ignore", "pipe", "pipe"],
103
+ ...extraSpawnOpts,
79
104
  });
80
105
  const timer = setTimeout(() => {
81
106
  killed = true;
82
107
  proc.kill("SIGTERM");
83
108
  }, timeoutMs);
109
+ // stdio: ["ignore", "pipe", "pipe"] is set above — pipes are always
110
+ // present here; the spread of extraSpawnOpts just widens the return
111
+ // type. Non-null asserts are safe.
84
112
  proc.stdout.on("data", (chunk) => {
85
113
  const text = chunk.toString();
86
114
  stdout += text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.31.0",
3
+ "version": "2.33.0",
4
4
  "description": "Open-source terminal coding agent. Works with any LLM.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -91,6 +91,7 @@
91
91
  },
92
92
  "homepage": "https://github.com/zhijiewong/openharness#readme",
93
93
  "optionalDependencies": {
94
+ "@anthropic-ai/sandbox-runtime": "^0.0.49",
94
95
  "@napi-rs/keyring": "^1.2.0",
95
96
  "sharp": "^0.34.5"
96
97
  }