@zhijiewang/openharness 2.33.0 → 2.35.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
@@ -542,7 +542,8 @@ Dispatch specialized sub-agents for focused tasks:
542
542
  | `security-auditor` | OWASP, injection, secrets, CVE scanning | Read-only + Bash |
543
543
  | `evaluator` | Evaluate code quality and run tests (read-only) | Read-only + Bash + Diagnostics |
544
544
  | `planner` | Design step-by-step implementation plans | Read-only + Bash |
545
- | `architect` | Analyze architecture and design structural changes | Read-only |
545
+ | `architect` | Analyze architecture and design structural changes (hands off to editor) | Read-only |
546
+ | `editor` | Apply an architect's plan as code edits, no re-planning | Read + Edit + Write + MultiEdit + Bash |
546
547
  | `migrator` | Systematic codebase migrations and upgrades | All file tools + Bash |
547
548
 
548
549
  Each role restricts the sub-agent to only its suggested tools. You can also pass `allowed_tools` explicitly:
@@ -552,6 +553,16 @@ Agent({ subagent_type: 'evaluator', prompt: 'Run all tests and report results' }
552
553
  Agent({ allowed_tools: ['Read', 'Grep'], prompt: 'Search for all TODO comments' })
553
554
  ```
554
555
 
556
+ ### Architect → Editor (cost-saving multi-file edits)
557
+
558
+ For larger changes that span multiple files, dispatch a two-pass `architect` → `editor` workflow. The architect (powerful model) reads the codebase and outputs a structured plan; the editor (fast model) applies it mechanically without re-planning. When `modelRouter` is configured, OH automatically routes the `architect` role to your `powerful` tier and the `editor` role to your `fast` tier — typical cost reduction is 30-50% on multi-file edits versus running both passes on the powerful model.
559
+
560
+ ```
561
+ Agent({ subagent_type: 'architect', prompt: 'Plan a migration from option A to option B across src/' })
562
+ # Hand the resulting plan to:
563
+ Agent({ subagent_type: 'editor', prompt: '<paste plan>' })
564
+ ```
565
+
555
566
  ## Headless Mode
556
567
 
557
568
  Run a single prompt without interactive UI — perfect for CI/CD and scripting:
@@ -684,6 +695,17 @@ oh --model llamacpp/my-model
684
695
  oh models # list available models
685
696
  ```
686
697
 
698
+ ## ACP (Agent Client Protocol)
699
+
700
+ Speak [Agent Client Protocol](https://agentclientprotocol.com/) over stdin/stdout so editors that support ACP — Zed, JetBrains via the ACP plugin, Cline, OpenCode — can drive openHarness as the underlying agent. No bespoke IDE extension required:
701
+
702
+ ```bash
703
+ oh acp # uses provider/model from .oh/config.yaml
704
+ oh acp --provider anthropic --model claude-sonnet-4-6
705
+ ```
706
+
707
+ Configure your editor's ACP integration to launch `oh acp` as the agent command. The session-update events (text chunks, tool calls, tool results) are translated automatically from openHarness's stream protocol; permission prompts currently use openHarness's own flow rather than the ACP `requestPermission` path (filed for follow-up). The `@agentclientprotocol/sdk` package is an `optionalDependency` — if it didn't install, `oh acp` exits with a clear install hint rather than silently failing.
708
+
687
709
  ## Auth
688
710
 
689
711
  Provider-agnostic credential management. Local LLMs (Ollama / llama.cpp / LM Studio) need no auth — configure them via `oh init`.
package/README.zh-CN.md CHANGED
@@ -542,7 +542,8 @@ Cron 执行器每 60 秒检查一次到期任务,并通过子查询运行。
542
542
  | `security-auditor` | OWASP、注入、密钥、CVE 扫描 | 只读 + Bash |
543
543
  | `evaluator` | 评估代码质量并运行测试(只读) | 只读 + Bash + Diagnostics |
544
544
  | `planner` | 设计分步实现计划 | 只读 + Bash |
545
- | `architect` | 分析架构、设计结构性变更 | 只读 |
545
+ | `architect` | 分析架构、设计结构性变更(移交给 editor 落地) | 只读 |
546
+ | `editor` | 按 architect 给出的方案应用代码改动,不再重新规划 | Read + Edit + Write + MultiEdit + Bash |
546
547
  | `migrator` | 系统化的代码库迁移与升级 | 全部文件工具 + Bash |
547
548
 
548
549
  每个角色只会让子代理使用其推荐的工具。你也可以显式传入 `allowed_tools`:
@@ -552,6 +553,16 @@ Agent({ subagent_type: 'evaluator', prompt: 'Run all tests and report results' }
552
553
  Agent({ allowed_tools: ['Read', 'Grep'], prompt: 'Search for all TODO comments' })
553
554
  ```
554
555
 
556
+ ### Architect → Editor(多文件改动的省钱模式)
557
+
558
+ 对于跨多个文件的较大改动,使用 `architect` → `editor` 两遍式工作流:architect(强模型)读懂代码并产出结构化方案;editor(轻量模型)按方案机械落地,不再重新规划。当配置了 `modelRouter` 时,OH 会自动把 `architect` 角色路由到 `powerful` 档、把 `editor` 角色路由到 `fast` 档 —— 相比两遍都跑强模型,多文件改动通常省 30-50% 成本。
559
+
560
+ ```
561
+ Agent({ subagent_type: 'architect', prompt: 'Plan a migration from option A to option B across src/' })
562
+ # 把得到的方案再交给 editor:
563
+ Agent({ subagent_type: 'editor', prompt: '<paste plan>' })
564
+ ```
565
+
555
566
  ## 无头模式
556
567
 
557
568
  跑一次提示词,不走交互 UI —— 适合 CI/CD 和脚本化:
@@ -683,6 +694,17 @@ oh --model llamacpp/my-model
683
694
  oh models # 列出可用模型
684
695
  ```
685
696
 
697
+ ## ACP(Agent Client Protocol)
698
+
699
+ 通过 stdin/stdout 讲 [Agent Client Protocol](https://agentclientprotocol.com/),让支持 ACP 的编辑器 —— Zed、通过 ACP 插件接入的 JetBrains、Cline、OpenCode 等 —— 把 openHarness 当作底层 agent 来驱动,无需为每个 IDE 单独写扩展:
700
+
701
+ ```bash
702
+ oh acp # 读取 .oh/config.yaml 的 provider/model
703
+ oh acp --provider anthropic --model claude-sonnet-4-6
704
+ ```
705
+
706
+ 在编辑器的 ACP 集成里把 `oh acp` 配成 agent 启动命令即可。session-update 事件(文本块、工具调用、工具结果)由 openHarness 的流式协议自动翻译过去;权限确认目前仍走 openHarness 自己的流程,没有走 ACP 的 `requestPermission`(已记入后续跟进)。`@agentclientprotocol/sdk` 是 `optionalDependency` —— 如果没装上,`oh acp` 会带着清楚的安装提示退出,不会静默失败。
707
+
686
708
  ## 鉴权(Auth)
687
709
 
688
710
  提供商无关的凭据管理。本地 LLM(Ollama / llama.cpp / LM Studio)无需鉴权 —— 通过 `oh init` 配置即可。
@@ -0,0 +1,96 @@
1
+ /**
2
+ * ACP (Agent Client Protocol) bridge — exposes openHarness as an agent that
3
+ * Zed / JetBrains / Cursor / Cline can talk to via JSON-RPC over stdio.
4
+ *
5
+ * Spec: https://agentclientprotocol.com/
6
+ * SDK: @agentclientprotocol/sdk (optional dependency)
7
+ *
8
+ * Responsibility split:
9
+ * - This module implements the `Agent` interface from the SDK.
10
+ * - The SDK handles JSON-RPC framing, schema validation, and notification
11
+ * dispatch; we only own the OH ↔ ACP event translation.
12
+ *
13
+ * Event translation (the only interesting part of this file):
14
+ *
15
+ * OH StreamEvent → ACP session/update
16
+ * --------------------------- ------------------------------------------------
17
+ * text_delta → agent_message_chunk { content: { type: text } }
18
+ * thinking_delta → agent_thought_chunk { content: { type: text } }
19
+ * tool_call_start → tool_call { status: pending, kind: <derived> }
20
+ * tool_call_end → tool_call_update { status: completed, content }
21
+ * tool_output_delta → tool_call_update { content: <appended> }
22
+ * error → end-of-turn with stopReason: refusal (logged)
23
+ * turn_complete → prompt response: { stopReason: end_turn }
24
+ *
25
+ * What's NOT bridged in v2.35:
26
+ * - permission_request → ACP requestPermission (uses OH's own permission flow today)
27
+ * - cost_update → ACP _meta passthrough (filed for follow-up)
28
+ * - rate_limited → currently surfaced via stderr only; an ACP
29
+ * session/update with retry hint is filed for follow-up.
30
+ *
31
+ * Why optional dep: the SDK ships ~750KB of generated zod schemas. Most OH
32
+ * users never hit the ACP path; they shouldn't pay that disk + import cost.
33
+ */
34
+ import type { StreamEvent } from "../types/events.js";
35
+ type AcpConnection = {
36
+ sessionUpdate: (params: unknown) => Promise<void>;
37
+ };
38
+ export type AcpAgentConfig = {
39
+ /** OH provider name: "anthropic", "openai", "ollama", … */
40
+ provider: string;
41
+ /** OH model identifier (e.g. "claude-sonnet-4-6") */
42
+ model: string;
43
+ /** Working directory (defaults to process.cwd()) */
44
+ cwd?: string;
45
+ /** Inject API key (otherwise resolved via OH's normal credential chain) */
46
+ apiKey?: string;
47
+ };
48
+ /**
49
+ * Translate one OH StreamEvent into zero-or-more ACP `session/update`
50
+ * notifications. Pure function — no I/O, no SDK dependency. This is the
51
+ * load-bearing piece that the rest of the bridge orchestrates.
52
+ *
53
+ * Returns an array because some OH events map to no ACP update (cost_update,
54
+ * turn_complete) and we always want a uniform shape for callers.
55
+ */
56
+ export declare function bridgeStreamEventToAcp(event: StreamEvent, sessionId: string): Array<{
57
+ sessionId: string;
58
+ update: Record<string, unknown>;
59
+ }>;
60
+ /**
61
+ * Concatenate the text blocks of an ACP PromptRequest's `prompt` array into
62
+ * the single string our `OhAgent.run/stream` expects. Resource-link blocks
63
+ * surface as `[resource: <uri>]` markers so the model is aware of them but
64
+ * doesn't try to inline-include the content (the spec wants us to optionally
65
+ * `readTextFile`-fetch them; that's a v2.36 follow-up).
66
+ */
67
+ export declare function extractPromptText(prompt: ReadonlyArray<{
68
+ type: string;
69
+ [key: string]: unknown;
70
+ }>): string;
71
+ /**
72
+ * Construct an ACP Agent wired to OH's `OhAgent` SDK class.
73
+ *
74
+ * The connection is the AgentSideConnection from the SDK; we pass it in so
75
+ * tests can stub it without loading the SDK.
76
+ */
77
+ export declare function createAcpAgent(connection: AcpConnection, config: AcpAgentConfig): {
78
+ initialize(_params: unknown): Promise<unknown>;
79
+ newSession(_params: unknown): Promise<unknown>;
80
+ authenticate(_params: unknown): Promise<Record<string, never>>;
81
+ setSessionMode(_params: unknown): Promise<Record<string, never>>;
82
+ prompt(params: {
83
+ sessionId: string;
84
+ prompt: ReadonlyArray<{
85
+ type: string;
86
+ [k: string]: unknown;
87
+ }>;
88
+ }): Promise<{
89
+ stopReason: "end_turn" | "cancelled" | "refusal";
90
+ }>;
91
+ cancel(params: {
92
+ sessionId: string;
93
+ }): Promise<void>;
94
+ };
95
+ export {};
96
+ //# sourceMappingURL=agent.d.ts.map
@@ -0,0 +1,246 @@
1
+ /**
2
+ * ACP (Agent Client Protocol) bridge — exposes openHarness as an agent that
3
+ * Zed / JetBrains / Cursor / Cline can talk to via JSON-RPC over stdio.
4
+ *
5
+ * Spec: https://agentclientprotocol.com/
6
+ * SDK: @agentclientprotocol/sdk (optional dependency)
7
+ *
8
+ * Responsibility split:
9
+ * - This module implements the `Agent` interface from the SDK.
10
+ * - The SDK handles JSON-RPC framing, schema validation, and notification
11
+ * dispatch; we only own the OH ↔ ACP event translation.
12
+ *
13
+ * Event translation (the only interesting part of this file):
14
+ *
15
+ * OH StreamEvent → ACP session/update
16
+ * --------------------------- ------------------------------------------------
17
+ * text_delta → agent_message_chunk { content: { type: text } }
18
+ * thinking_delta → agent_thought_chunk { content: { type: text } }
19
+ * tool_call_start → tool_call { status: pending, kind: <derived> }
20
+ * tool_call_end → tool_call_update { status: completed, content }
21
+ * tool_output_delta → tool_call_update { content: <appended> }
22
+ * error → end-of-turn with stopReason: refusal (logged)
23
+ * turn_complete → prompt response: { stopReason: end_turn }
24
+ *
25
+ * What's NOT bridged in v2.35:
26
+ * - permission_request → ACP requestPermission (uses OH's own permission flow today)
27
+ * - cost_update → ACP _meta passthrough (filed for follow-up)
28
+ * - rate_limited → currently surfaced via stderr only; an ACP
29
+ * session/update with retry hint is filed for follow-up.
30
+ *
31
+ * Why optional dep: the SDK ships ~750KB of generated zod schemas. Most OH
32
+ * users never hit the ACP path; they shouldn't pay that disk + import cost.
33
+ */
34
+ import { Agent as OhAgent } from "../sdk/index.js";
35
+ /**
36
+ * Translate one OH StreamEvent into zero-or-more ACP `session/update`
37
+ * notifications. Pure function — no I/O, no SDK dependency. This is the
38
+ * load-bearing piece that the rest of the bridge orchestrates.
39
+ *
40
+ * Returns an array because some OH events map to no ACP update (cost_update,
41
+ * turn_complete) and we always want a uniform shape for callers.
42
+ */
43
+ export function bridgeStreamEventToAcp(event, sessionId) {
44
+ switch (event.type) {
45
+ case "text_delta":
46
+ return [
47
+ {
48
+ sessionId,
49
+ update: {
50
+ sessionUpdate: "agent_message_chunk",
51
+ content: { type: "text", text: event.content },
52
+ },
53
+ },
54
+ ];
55
+ case "thinking_delta":
56
+ return [
57
+ {
58
+ sessionId,
59
+ update: {
60
+ sessionUpdate: "agent_thought_chunk",
61
+ content: { type: "text", text: event.content },
62
+ },
63
+ },
64
+ ];
65
+ case "tool_call_start":
66
+ return [
67
+ {
68
+ sessionId,
69
+ update: {
70
+ sessionUpdate: "tool_call",
71
+ toolCallId: event.callId,
72
+ title: event.toolName,
73
+ kind: deriveToolKind(event.toolName),
74
+ status: "pending",
75
+ },
76
+ },
77
+ ];
78
+ case "tool_call_complete":
79
+ // OH separates "args known" (tool_call_complete) from "result known"
80
+ // (tool_call_end). ACP folds both into tool_call_update. Surface the
81
+ // arguments now so editors can render them while the tool runs.
82
+ return [
83
+ {
84
+ sessionId,
85
+ update: {
86
+ sessionUpdate: "tool_call_update",
87
+ toolCallId: event.callId,
88
+ status: "in_progress",
89
+ rawInput: event.arguments,
90
+ },
91
+ },
92
+ ];
93
+ case "tool_call_end":
94
+ return [
95
+ {
96
+ sessionId,
97
+ update: {
98
+ sessionUpdate: "tool_call_update",
99
+ toolCallId: event.callId,
100
+ status: event.isError ? "failed" : "completed",
101
+ content: [
102
+ {
103
+ type: "content",
104
+ content: { type: "text", text: event.output },
105
+ },
106
+ ],
107
+ },
108
+ },
109
+ ];
110
+ // Everything else has no ACP equivalent in the v2.35 surface.
111
+ case "tool_output_delta":
112
+ case "permission_request":
113
+ case "ask_user":
114
+ case "cost_update":
115
+ case "turn_complete":
116
+ case "error":
117
+ case "rate_limited":
118
+ return [];
119
+ }
120
+ }
121
+ /**
122
+ * Map an OH tool name to an ACP tool kind. The kind drives editor UX —
123
+ * Zed colors "edit" tools differently from "read" or "execute" — so getting
124
+ * this approximately right is worth the if-ladder. Unknown tools fall back
125
+ * to "other" rather than guessing.
126
+ */
127
+ function deriveToolKind(toolName) {
128
+ const name = toolName.toLowerCase();
129
+ if (name === "read" || name.endsWith("read") || name === "imageread")
130
+ return "read";
131
+ if (name === "edit" || name === "write" || name === "multiedit" || name === "notebookedit")
132
+ return "edit";
133
+ if (name === "bash" || name === "powershell" || name === "killprocess")
134
+ return "execute";
135
+ if (name === "glob" || name === "grep" || name === "ls")
136
+ return "search";
137
+ if (name === "webfetch" || name === "websearch" || name === "exasearch")
138
+ return "fetch";
139
+ if (name === "todowrite" || name === "memory")
140
+ return "think";
141
+ return "other";
142
+ }
143
+ /**
144
+ * Concatenate the text blocks of an ACP PromptRequest's `prompt` array into
145
+ * the single string our `OhAgent.run/stream` expects. Resource-link blocks
146
+ * surface as `[resource: <uri>]` markers so the model is aware of them but
147
+ * doesn't try to inline-include the content (the spec wants us to optionally
148
+ * `readTextFile`-fetch them; that's a v2.36 follow-up).
149
+ */
150
+ export function extractPromptText(prompt) {
151
+ const parts = [];
152
+ for (const block of prompt) {
153
+ if (block.type === "text" && typeof block.text === "string") {
154
+ parts.push(block.text);
155
+ }
156
+ else if (block.type === "resource_link" && typeof block.uri === "string") {
157
+ parts.push(`[resource: ${block.uri}]`);
158
+ }
159
+ else if (block.type === "resource") {
160
+ // Embedded resource — we don't fetch the content here; just surface
161
+ // a reference so the model doesn't ignore the attachment.
162
+ const uri = block.resource?.uri;
163
+ parts.push(uri ? `[resource: ${uri}]` : "[embedded resource]");
164
+ }
165
+ }
166
+ return parts.join("\n\n");
167
+ }
168
+ /**
169
+ * Construct an ACP Agent wired to OH's `OhAgent` SDK class.
170
+ *
171
+ * The connection is the AgentSideConnection from the SDK; we pass it in so
172
+ * tests can stub it without loading the SDK.
173
+ */
174
+ export function createAcpAgent(connection, config) {
175
+ const sessions = new Map();
176
+ return {
177
+ async initialize(_params) {
178
+ return {
179
+ // SDK's PROTOCOL_VERSION constant is 1 today; hardcoded so this
180
+ // module doesn't import the SDK at type-check time.
181
+ protocolVersion: 1,
182
+ agentCapabilities: {
183
+ loadSession: false,
184
+ },
185
+ };
186
+ },
187
+ async newSession(_params) {
188
+ const sessionId = crypto.randomUUID();
189
+ const agent = new OhAgent({
190
+ provider: config.provider,
191
+ model: config.model,
192
+ ...(config.apiKey ? { apiKey: config.apiKey } : {}),
193
+ ...(config.cwd ? { cwd: config.cwd } : {}),
194
+ permissionMode: "trust",
195
+ });
196
+ sessions.set(sessionId, { abort: new AbortController(), agent });
197
+ return { sessionId };
198
+ },
199
+ async authenticate(_params) {
200
+ // OH resolves credentials from its own chain (env vars / keychain / config);
201
+ // we don't gate session creation on an explicit ACP authenticate call.
202
+ return {};
203
+ },
204
+ async setSessionMode(_params) {
205
+ // Modes (ask/architect/code) aren't exposed yet — return success so
206
+ // editors that try to set one don't error.
207
+ return {};
208
+ },
209
+ async prompt(params) {
210
+ const session = sessions.get(params.sessionId);
211
+ if (!session)
212
+ throw new Error(`Session ${params.sessionId} not found`);
213
+ // A new prompt cancels any prior in-flight prompt for this session.
214
+ session.abort.abort();
215
+ session.abort = new AbortController();
216
+ const promptText = extractPromptText(params.prompt);
217
+ try {
218
+ for await (const event of session.agent.stream(promptText)) {
219
+ if (session.abort.signal.aborted)
220
+ return { stopReason: "cancelled" };
221
+ for (const update of bridgeStreamEventToAcp(event, params.sessionId)) {
222
+ await connection.sessionUpdate(update);
223
+ }
224
+ }
225
+ return { stopReason: "end_turn" };
226
+ }
227
+ catch (err) {
228
+ if (session.abort.signal.aborted)
229
+ return { stopReason: "cancelled" };
230
+ // Surface unexpected errors as a refusal so the editor stops the spinner.
231
+ await connection.sessionUpdate({
232
+ sessionId: params.sessionId,
233
+ update: {
234
+ sessionUpdate: "agent_message_chunk",
235
+ content: { type: "text", text: `[agent error] ${err.message}` },
236
+ },
237
+ });
238
+ return { stopReason: "refusal" };
239
+ }
240
+ },
241
+ async cancel(params) {
242
+ sessions.get(params.sessionId)?.abort.abort();
243
+ },
244
+ };
245
+ }
246
+ //# sourceMappingURL=agent.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ACP server entry point — `oh acp`.
3
+ *
4
+ * Loads the optional `@agentclientprotocol/sdk` package, wires stdin/stdout
5
+ * via its ndJsonStream to the bridge in `agent.ts`, and runs until the
6
+ * client disconnects.
7
+ *
8
+ * Dies cleanly with an install hint if the SDK isn't available — unlike
9
+ * sandbox-runtime where missing-dep is a silent no-op, here the user
10
+ * explicitly invoked `oh acp` so a clear error is the right UX.
11
+ */
12
+ import { type AcpAgentConfig } from "./agent.js";
13
+ export declare function runAcpServer(config: AcpAgentConfig): Promise<void>;
14
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * ACP server entry point — `oh acp`.
3
+ *
4
+ * Loads the optional `@agentclientprotocol/sdk` package, wires stdin/stdout
5
+ * via its ndJsonStream to the bridge in `agent.ts`, and runs until the
6
+ * client disconnects.
7
+ *
8
+ * Dies cleanly with an install hint if the SDK isn't available — unlike
9
+ * sandbox-runtime where missing-dep is a silent no-op, here the user
10
+ * explicitly invoked `oh acp` so a clear error is the right UX.
11
+ */
12
+ import { Readable, Writable } from "node:stream";
13
+ import { createAcpAgent } from "./agent.js";
14
+ export async function runAcpServer(config) {
15
+ let acp;
16
+ try {
17
+ acp = await import("@agentclientprotocol/sdk");
18
+ }
19
+ catch (err) {
20
+ process.stderr.write("ACP server requires @agentclientprotocol/sdk. Install with:\n npm install -g @agentclientprotocol/sdk\nor reinstall openHarness — the package ships as an optionalDependency.\n");
21
+ process.stderr.write(`Original error: ${err.message}\n`);
22
+ process.exit(1);
23
+ }
24
+ // ACP wire framing: NDJSON over stdio. The SDK gives us a ready-made stream
25
+ // adapter; we just hand it our process pipes.
26
+ // ndJsonStream(input, output) — input is the AGENT's input (= stdin), output
27
+ // is the AGENT's output (= stdout). Names are direction-from-the-stream's
28
+ // perspective, which inverts the Node convention (stdin/stdout are named
29
+ // from the process's perspective). That's why this looks backwards.
30
+ const input = Writable.toWeb(process.stdout);
31
+ const output = Readable.toWeb(process.stdin);
32
+ const stream = acp.ndJsonStream(input, output);
33
+ // The SDK constructs the connection and wires our handlers. We wrap our
34
+ // bridge in a factory because the SDK passes the connection into it; we
35
+ // need the connection to send sessionUpdate notifications back. The
36
+ // returned object retains the wiring internally — we don't need to keep
37
+ // a reference, but we still construct it so the side effects happen.
38
+ void new acp.AgentSideConnection((conn) => createAcpAgent(conn, config), stream);
39
+ // Block until stdin closes (client disconnected). Without this the process
40
+ // would exit immediately after wiring.
41
+ await new Promise((resolve) => {
42
+ process.stdin.on("end", resolve);
43
+ process.stdin.on("close", resolve);
44
+ });
45
+ }
46
+ //# sourceMappingURL=server.js.map
@@ -131,7 +131,7 @@ Do NOT implement anything. Your output is a plan document, not code. Read widely
131
131
  {
132
132
  id: "architect",
133
133
  name: "Architect",
134
- description: "Analyzes system architecture and designs structural changes",
134
+ description: "Analyzes system architecture and designs structural changes (hands off to editor)",
135
135
  systemPromptSupplement: `You are an architecture agent. Your job is to:
136
136
  - Map the current system architecture (modules, dependencies, data flow)
137
137
  - Identify architectural patterns and conventions in use
@@ -139,9 +139,38 @@ Do NOT implement anything. Your output is a plan document, not code. Read widely
139
139
  - Evaluate trade-offs between approaches (performance, maintainability, complexity)
140
140
  - Document interfaces, contracts, and integration points
141
141
 
142
- Focus on the big picture: module boundaries, data flow, dependency graphs. Leave implementation details to other agents.`,
142
+ Focus on the big picture: module boundaries, data flow, dependency graphs. Do NOT apply edits — leave implementation to the editor agent.
143
+
144
+ When you've finished planning, output a structured "Plan" the editor can apply mechanically:
145
+
146
+ ## Plan
147
+ 1. **<file:line>** — <change description>
148
+ - **Before**: <current state, 1-line>
149
+ - **After**: <target state, 1-line>
150
+ - **Why**: <one-sentence rationale>
151
+ 2. <next change…>
152
+
153
+ Keep each step small enough that an editor (a cheaper model) can apply it without re-deriving your reasoning. Group related edits together; surface dependencies between steps.`,
143
154
  suggestedTools: ["Read", "Glob", "Grep", "LS"],
144
155
  },
156
+ {
157
+ id: "editor",
158
+ name: "Editor",
159
+ description: "Applies an architect's plan as code edits — does not re-plan, no discovery",
160
+ systemPromptSupplement: `You are a code editor. You receive a plan from an architect agent and apply it as file edits.
161
+
162
+ Rules:
163
+ - DO apply the changes exactly as described in the plan.
164
+ - DO read each file before editing it (Read → Edit), so your edit anchors land correctly.
165
+ - DO run tests after each batch if test commands are available.
166
+ - DO NOT re-plan, second-guess the architect, or expand scope. If the plan is ambiguous on a specific edit, follow the most literal interpretation; surface the ambiguity in your final summary, don't try to resolve it yourself.
167
+ - DO NOT add features, refactor adjacent code, or change tests beyond what the plan specifies.
168
+
169
+ This role is intentionally tuned for a cheaper/faster model — the architect already did the reasoning. Your job is mechanical accuracy.
170
+
171
+ If the plan can't be applied (e.g. a referenced file doesn't exist, an anchor doesn't match the current code), STOP and report which step failed and why. Do not improvise.`,
172
+ suggestedTools: ["Read", "Edit", "Write", "MultiEdit", "Bash"],
173
+ },
145
174
  {
146
175
  id: "migrator",
147
176
  name: "Migrator",
package/dist/main.js CHANGED
@@ -1078,6 +1078,28 @@ program
1078
1078
  }
1079
1079
  console.log();
1080
1080
  });
1081
+ // ── acp (Agent Client Protocol server) ──
1082
+ //
1083
+ // Speaks ACP (https://agentclientprotocol.com/) over stdin/stdout so editors
1084
+ // like Zed and JetBrains can drive openHarness as a coding agent. The SDK is
1085
+ // an optional dependency to keep the default install footprint small —
1086
+ // `oh acp` exits cleanly with an install hint if the SDK isn't present.
1087
+ program
1088
+ .command("acp")
1089
+ .description("Start an ACP (Agent Client Protocol) server over stdio. Usage: configure your editor (Zed/JetBrains/Cline) to launch `oh acp` as the agent command.")
1090
+ .option("-m, --model <model>", "Model to use (defaults to .oh/config.yaml's model)")
1091
+ .option("-p, --provider <provider>", "Provider name (defaults to .oh/config.yaml's provider)")
1092
+ .action(async (opts) => {
1093
+ const cfg = readOhConfig();
1094
+ const model = opts.model ?? cfg?.model;
1095
+ const provider = opts.provider ?? cfg?.provider;
1096
+ if (!model || !provider) {
1097
+ process.stderr.write("ACP server needs both a model and a provider. Pass --model and --provider, or run `oh init` first.\n");
1098
+ process.exit(1);
1099
+ }
1100
+ const { runAcpServer } = await import("./acp/server.js");
1101
+ await runAcpServer({ provider, model, cwd: process.cwd() });
1102
+ });
1081
1103
  /**
1082
1104
  * Run the interactive setup wizard. Used by both the `oh init` subcommand and
1083
1105
  * the `--init` / `--init-only` flag added to chat / run / session (audit B5).
@@ -26,6 +26,13 @@ export class ModelRouter {
26
26
  if (context.role && powerfulRoles.includes(context.role)) {
27
27
  return this.route("powerful", `role: ${context.role}`);
28
28
  }
29
+ // Roles that apply mechanical changes per a prior plan → fast (cost
30
+ // optimization for the architect → editor pattern; the planner has
31
+ // already done the reasoning, so the editor doesn't need a powerful model)
32
+ const cheapRoles = ["editor"];
33
+ if (context.role && cheapRoles.includes(context.role)) {
34
+ return this.route("fast", `role: ${context.role}`);
35
+ }
29
36
  // Early exploration turns (1-2) → fast
30
37
  if (context.turn <= 2 && context.hadToolCalls) {
31
38
  return this.route("fast", "early exploration");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhijiewang/openharness",
3
- "version": "2.33.0",
3
+ "version": "2.35.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
+ "@agentclientprotocol/sdk": "^0.21.0",
94
95
  "@anthropic-ai/sandbox-runtime": "^0.0.49",
95
96
  "@napi-rs/keyring": "^1.2.0",
96
97
  "sharp": "^0.34.5"