@multiclawcli/core 0.6.2 → 0.6.3

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/dist/index.js CHANGED
@@ -1,1634 +1,583 @@
1
- // src/agents/arch-design-reviewer.ts
2
- var SYSTEM = `You are an architecture design reviewer. Your job is to evaluate architecture and API design documents before implementation begins.
3
-
4
- Responsibilities:
5
- - Review architecture design for completeness, consistency, and technical soundness
6
- - Review API specifications for clarity, correctness, and REST/contract conventions
7
- - Identify ambiguities, missing edge cases, or risky design decisions
8
- - Do not write code, do not implement \u2014 provide only design-level feedback
9
-
10
- Deliverables (save to the working directory):
11
- - arch-review.md \u2014 verdict (Approved / Changes Required), issue list (blocking / advisory) with specific locations and suggested fixes, overall assessment
12
-
13
- Principles: Be precise \u2014 point to specific sections. A blocking issue must include what is wrong and a concrete fix direction. If the design is sound, say so directly. Do not approve with vague praise.`;
14
- var TASK = `## Architecture Design
15
- {{file:architecture.md}}
16
-
17
- ## API Specification
18
- {{file:api-spec.md}}
19
- {{upstreamOutputs}}
20
-
21
- ## Task
22
- Review the architecture design and API specification above. Output arch-review.md:
23
- 1. Overall verdict: Approved / Changes Required
24
- 2. Blocking issues (must be fixed before implementation): each with location, problem, and fix direction
25
- 3. Advisory issues (should fix but not blocking): each with location and suggestion
26
- 4. Design strengths worth noting
27
- 5. Final summary (1\u20132 sentences)
1
+ // src/providers/catalog.ts
2
+ import * as path from "path";
3
+ function normalizeCommandKey(command) {
4
+ return path.basename(command).toLowerCase().replace(/\.(cmd|exe|bat)$/i, "");
5
+ }
6
+ var PROVIDERS = [
7
+ {
8
+ id: "opencode",
9
+ label: "OpenCode",
10
+ command: "opencode",
11
+ args: ["acp"],
12
+ tools: false,
13
+ modelSelection: "config-option",
14
+ models: [
15
+ { id: "opencode/big-pickle", label: "Big Pickle", tier: "balanced", defaultForTier: true },
16
+ { id: "opencode/exo-free", label: "Exo", tier: "strong", defaultForTier: true },
17
+ { id: "opencode/ling-3.0-flash-fin-free", label: "Ling 3.0 Flash", tier: "fast" },
18
+ {
19
+ id: "opencode/ling-3.1-flash-free",
20
+ label: "Ling 3.1 Flash",
21
+ tier: "fast",
22
+ defaultForTier: true
23
+ },
24
+ { id: "opencode/longcat-2.5-preview-free", label: "Longcat 2.5 Preview", tier: "balanced" },
25
+ { id: "opencode/mimo-v2.6-flash-free", label: "Mimo 2.6 Flash", tier: "fast" },
26
+ { id: "opencode/muse-spark-1.3-contributor-free", label: "Muse Spark 1.3", tier: "fast" },
27
+ { id: "opencode/nemotron-3-ultra-free", label: "Nemotron 3 Ultra", tier: "strong" },
28
+ { id: "opencode/nemotron-3.5-lightning-free", label: "Nemotron 3.5 Lightning", tier: "fast" },
29
+ { id: "opencode/space-bunny-free", label: "Space Bunny", tier: "fast" },
30
+ { id: "opencode/step-5-preview-free", label: "Step 5 Preview", tier: "strong" }
31
+ ],
32
+ defaultModel: "opencode/big-pickle",
33
+ install: { hint: "npm install -g opencode" },
34
+ auth: { kind: "none", hint: "" }
35
+ },
36
+ {
37
+ id: "claude",
38
+ label: "Claude",
39
+ command: "claude-agent-acp",
40
+ args: [],
41
+ tools: true,
42
+ modelSelection: "config-option",
43
+ meta: (task) => {
44
+ const meta = {};
45
+ if (task.systemPrompt) meta.systemPrompt = task.systemPrompt;
46
+ meta.claudeCode = { options: { tools: task.tools } };
47
+ return meta;
48
+ },
49
+ models: [
50
+ { id: "default", label: "Default", tier: "balanced" },
51
+ { id: "opus", label: "Opus", tier: "strong", defaultForTier: true },
52
+ { id: "fable", label: "Fable", tier: "balanced" },
53
+ { id: "sonnet", label: "Sonnet", tier: "balanced", defaultForTier: true },
54
+ { id: "haiku", label: "Haiku", tier: "fast", defaultForTier: true }
55
+ ],
56
+ defaultModel: "sonnet",
57
+ install: {
58
+ hint: "multiclaw bundles the ACP bridge \u2014 install the Claude Code CLI (`claude`) and run `claude` to log in"
59
+ },
60
+ auth: { kind: "ambient", hint: "run `claude` to log in" }
61
+ },
62
+ {
63
+ id: "codex",
64
+ label: "Codex",
65
+ command: "codex-acp",
66
+ args: [],
67
+ tools: false,
68
+ modelSelection: "config-option",
69
+ meta: (task) => task.systemPrompt ? { jetbrains: { air: { customInstructions: task.systemPrompt } } } : void 0,
70
+ // Placeholders — real ids require auth to enumerate.
71
+ models: [
72
+ { id: "gpt-5-codex", label: "GPT-5 Codex", tier: "strong", defaultForTier: true },
73
+ { id: "gpt-5", label: "GPT-5", tier: "balanced", defaultForTier: true },
74
+ { id: "gpt-5-mini", label: "GPT-5 Mini", tier: "fast", defaultForTier: true }
75
+ ],
76
+ defaultModel: "gpt-5",
77
+ install: {
78
+ hint: "multiclaw bundles the ACP bridge \u2014 install the Codex CLI (`codex`) and run `codex login`"
79
+ },
80
+ auth: { kind: "api-key", hint: "set OPENAI_API_KEY or run `codex login`" }
81
+ },
82
+ {
83
+ id: "cursor",
84
+ label: "Cursor",
85
+ command: "cursor-agent",
86
+ aliases: ["agent"],
87
+ args: ["acp"],
88
+ tools: false,
89
+ modelSelection: "config-option",
90
+ // _meta support unverified — falls back to prompt prefix until confirmed.
91
+ meta: void 0,
92
+ // Placeholders — real ids require Cursor CLI + auth to enumerate.
93
+ models: [
94
+ { id: "cursor-pro", label: "Cursor Pro", tier: "strong", defaultForTier: true },
95
+ { id: "cursor-fast", label: "Cursor Fast", tier: "fast", defaultForTier: true }
96
+ ],
97
+ defaultModel: "cursor-pro",
98
+ install: {
99
+ hint: "curl https://cursor.com/install | bash",
100
+ url: "https://cursor.com/docs/cli/acp"
101
+ },
102
+ auth: { kind: "login", hint: "run `agent login` first (or set CURSOR_API_KEY)" },
103
+ confirm: (v) => /cursor/i.test(v)
104
+ }
105
+ ];
106
+ var byProviderId = /* @__PURE__ */ new Map();
107
+ var byCommand = /* @__PURE__ */ new Map();
108
+ for (const spec of PROVIDERS) {
109
+ byProviderId.set(spec.id, spec);
110
+ byCommand.set(normalizeCommandKey(spec.command), spec);
111
+ if (spec.aliases) {
112
+ for (const alias of spec.aliases) {
113
+ byCommand.set(normalizeCommandKey(alias), spec);
114
+ }
115
+ }
116
+ }
117
+ function specForProvider(id) {
118
+ return byProviderId.get(id);
119
+ }
120
+ function specForCommand(command) {
121
+ return byCommand.get(normalizeCommandKey(command));
122
+ }
123
+ function providerIds() {
124
+ return [...byProviderId.keys()];
125
+ }
126
+ function providerCommands() {
127
+ return [...byCommand.keys()];
128
+ }
129
+ function tierModel(spec, tier) {
130
+ const m = spec.models.find((m2) => m2.tier === tier && m2.defaultForTier);
131
+ return m?.id ?? spec.models.find((m2) => m2.tier === tier)?.id;
132
+ }
28
133
 
29
- Be specific. Reference section names or endpoint paths when citing issues.`;
30
- function archDesignReviewer(overrides) {
134
+ // src/providers/resolve.ts
135
+ var DEFAULT_COMMAND = "opencode";
136
+ var DEFAULT_PROVIDER = "opencode";
137
+ function resolveProvider(agent, config) {
138
+ return agent.provider ?? specForCommand(agent.command ?? "")?.id ?? config.defaults?.provider ?? specForCommand(DEFAULT_COMMAND)?.id ?? DEFAULT_PROVIDER;
139
+ }
140
+ function resolveCommand(agent, config) {
141
+ return agent.command ?? config.defaults?.command ?? specForProvider(resolveProvider(agent, config))?.command ?? DEFAULT_COMMAND;
142
+ }
143
+ function resolveArgs(agent, config) {
144
+ if (agent.args) return agent.args;
145
+ const command = agent.command ?? config.defaults?.command;
146
+ if (command) return specForCommand(command)?.args;
147
+ return specForProvider(resolveProvider(agent, config))?.args;
148
+ }
149
+ function resolveModel(agent, config) {
150
+ if (agent.model) return agent.model;
151
+ const provider = resolveProvider(agent, config);
152
+ const spec = specForProvider(provider);
153
+ const fromModels = agent.models?.[provider] ?? agent.models?.acp;
154
+ if (fromModels) return fromModels;
155
+ if (agent.modelTier && spec) {
156
+ const fromTier = tierModel(spec, agent.modelTier);
157
+ if (fromTier) return fromTier;
158
+ }
159
+ if (config.defaults?.model) return config.defaults.model;
160
+ if (spec) return spec.defaultModel;
161
+ return void 0;
162
+ }
163
+ function resolveAgentRuntime(agent, config) {
164
+ const provider = resolveProvider(agent, config);
165
+ const spec = specForProvider(provider);
31
166
  return {
32
- id: "arch-design-reviewer",
33
- name: "Arch Design Reviewer",
34
- icon: "\u25B3",
35
- description: "reviews the design",
36
- runtime: "acp",
37
- models: { acp: "haiku" },
38
- modelTier: "fast",
39
- systemPrompt: SYSTEM,
40
- taskPrompt: TASK,
41
- tools: ["Read", "Write"],
42
- ...overrides
167
+ provider,
168
+ command: resolveCommand(agent, config),
169
+ model: resolveModel(agent, config),
170
+ spec
43
171
  };
44
172
  }
45
173
 
46
- // src/agents/architect.ts
47
- var SYSTEM2 = `You are the technical architect for this project. Your responsibility is technical analysis and design \u2014 you do not write feature code.
48
-
49
- Responsibilities:
50
- - Understand requirements, identify key technical risks and affected components
51
- - Review existing code (if any) and propose minimal-change solutions
52
- - Define module boundaries, interface contracts, and directory structure
53
- - Provide a clear design foundation for developers and testers
54
- - Decompose the work into per-agent subtasks based on who is actually in the pipeline
55
- - For each subtask, write a step-by-step plan concrete enough that the agent can follow it without re-analyzing the full requirement
56
-
57
- Deliverables (save to the working directory):
58
- - architecture.md \u2014 architecture plan (module breakdown, directory structure, technology choices, key component descriptions)
59
- - api-spec.md \u2014 complete API specification (paths, methods, request/response formats, status codes, error handling)
60
- - task-plan.json \u2014 structured task breakdown for all implementation agents in the pipeline (see format below)
174
+ // src/runtime/AcpRuntime.ts
175
+ import { spawn } from "child_process";
176
+ import { randomUUID } from "crypto";
177
+ import { Readable, Writable } from "stream";
178
+ import { StringDecoder } from "string_decoder";
61
179
 
62
- task-plan.json format:
63
- {
64
- "projectName": "<project name>",
65
- "requirement": "<one-line summary>",
66
- "tasks": [
67
- {
68
- "id": "<agent id exactly as listed in pipelineAgents>",
69
- "title": "<short task title>",
70
- "scope": "<concrete description of what this agent should build or produce>",
71
- "plan": [
72
- "Step 1: ...",
73
- "Step 2: ...",
74
- "Step 3: ..."
75
- ],
76
- "dependsOn": ["<other agent id>"]
180
+ // src/providers/bridge.ts
181
+ import * as fs from "fs";
182
+ import { createRequire } from "module";
183
+ import * as path2 from "path";
184
+ var BRIDGE_PACKAGES = {
185
+ "claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
186
+ "codex-acp": "@agentclientprotocol/codex-acp"
187
+ };
188
+ function resolvesOnPath(command) {
189
+ const dirs = (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean);
190
+ const exts = (process.env.PATHEXT ?? "").split(";").filter(Boolean);
191
+ const suffixes = exts.length > 0 ? exts : [""];
192
+ for (const dir of dirs) {
193
+ for (const ext of suffixes) {
194
+ try {
195
+ fs.accessSync(path2.join(dir, command + ext), fs.constants.X_OK);
196
+ return true;
197
+ } catch {
198
+ }
77
199
  }
78
- ]
200
+ }
201
+ return false;
79
202
  }
80
-
81
- Rules for task-plan.json:
82
- - Only create tasks for implementation agents (those that produce code, designs, or infrastructure). Skip review agents (code-reviewer, arch-design-reviewer, ui-design-reviewer, reviewer) and planning agents (product-manager, architect, leader).
83
- - Use the exact agent id from pipelineAgents as the task id.
84
- - If an agent is not in pipelineAgents, do not create a task for it.
85
- - dependsOn references other task ids, not agent names.
86
-
87
- Principles: Execute tasks directly without asking for confirmation. If information is missing, mark the file with BLOCKED and describe what is needed. Do not change the requirement scope. Do not write business feature code. Do not refactor beyond the task scope.`;
88
- var TASK2 = `## Project
89
- {{projectName}}
90
-
91
- ## Requirement
92
- {{requirement}}
93
-
94
- ## Pipeline Agents
95
- The following agents will run in this pipeline (id: name):
96
- {{pipelineAgents}}
97
- {{upstreamOutputs}}
98
-
99
- ## Task
100
- Analyze the requirement and create three files in the current directory:
101
- 1. architecture.md \u2014 architecture plan (module breakdown, directory structure, technology choices, key component descriptions, affected scope)
102
- 2. api-spec.md \u2014 complete API specification (paths, methods, request parameters, response formats, status codes, error handling)
103
- 3. task-plan.json \u2014 task breakdown for each implementation agent listed in the pipeline above
104
-
105
- For task-plan.json: create one task per implementation agent (skip reviewers and planners). Each task must have:
106
- - scope: concrete description of what this agent should build
107
- - plan: ordered list of concrete steps, specific enough that the agent can follow them without re-analyzing the full requirement (e.g. "Create src/models/user.ts with User and UserProfile interfaces", "Implement POST /users endpoint in src/routes/users.ts")
108
- Set dependsOn to reflect actual execution order constraints (e.g. devops depends on backend and frontend).
109
-
110
- Create the files directly. No extra output.`;
111
- function architect(overrides) {
112
- return {
113
- id: "architect",
114
- name: "Architect",
115
- icon: "\u25C6",
116
- description: "designs the architecture",
117
- role: "planner",
118
- runtime: "acp",
119
- models: { acp: "opus" },
120
- modelTier: "strong",
121
- systemPrompt: SYSTEM2,
122
- taskPrompt: TASK2,
123
- tools: ["Read", "Write"],
124
- ...overrides
125
- };
203
+ function resolveBridgeBinary(command) {
204
+ const pkg = BRIDGE_PACKAGES[command];
205
+ if (!pkg) return null;
206
+ try {
207
+ const req = createRequire(import.meta.url);
208
+ const pkgJsonPath = req.resolve(`${pkg}/package.json`);
209
+ const bin = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).bin;
210
+ const binRel = typeof bin === "string" ? bin : bin?.[command] ?? Object.values(bin)[0];
211
+ if (!binRel) return null;
212
+ return path2.join(path2.dirname(pkgJsonPath), binRel);
213
+ } catch {
214
+ return null;
215
+ }
126
216
  }
127
-
128
- // src/agents/backend-developer.ts
129
- var SYSTEM3 = `You are the backend engineer. You own the API contract and all server-side logic.
130
-
131
- Responsibilities:
132
- - Read the architecture design and API specification, implement server-side logic and data models
133
- - Write backend tests (unit tests / integration tests)
134
- - Run validation commands (compile, lint, test) and record the results
135
-
136
- Deliverables (write code to the working directory, and record in implementation-notes.md):
137
- - List of files created or modified
138
- - Commands actually executed and their output
139
- - Known issues and risks
140
-
141
- Principles: Write code directly without asking for confirmation. Follow the API spec strictly \u2014 do not change the contract unilaterally; escalate contract issues upstream. Do not refactor unrelated code. Run commands and report evidence.`;
142
- var TASK3 = `## Task Scope
143
- {{agentScope}}
144
-
145
- ## Plan
146
- {{agentPlan}}
147
- {{upstreamOutputs}}
148
-
149
- ---
150
-
151
- ## Architecture Design
152
- {{file:architecture.md}}
153
-
154
- ## API Specification
155
- {{file:api-spec.md}}
156
-
157
- ## Task
158
- Implement complete backend code following the architecture design and API specification:
159
- 1. Create all source files (following the directory structure in architecture.md)
160
- 2. Implement all API endpoints (strictly following api-spec.md)
161
- 3. Write basic tests
162
- 4. Run compile/test commands to verify (e.g. npx tsc --noEmit, npm test)
163
- 5. Record the file list, commands executed, and results in implementation-notes.md
164
-
165
- Start immediately. Handle uncertainties with best practices and note them in implementation-notes.md.`;
166
- function backendDeveloper(overrides) {
167
- return {
168
- id: "backend-developer",
169
- name: "Backend Developer",
170
- icon: "\u2295",
171
- description: "builds the API",
172
- runtime: "acp",
173
- models: { acp: "sonnet" },
174
- modelTier: "balanced",
175
- systemPrompt: SYSTEM3,
176
- taskPrompt: TASK3,
177
- tools: ["Read", "Write", "Bash"],
178
- ...overrides
179
- };
217
+ function resolveExecutable(command) {
218
+ if (resolvesOnPath(command)) return command;
219
+ return resolveBridgeBinary(command);
180
220
  }
181
221
 
182
- // src/agents/code-reviewer.ts
183
- var SYSTEM4 = `You are a code review expert focused on implementation quality, correctness, and security.
184
-
185
- Responsibilities:
186
- - Read the requirement, architecture design, and all implementation code
187
- - Verify the implementation matches the API specification and satisfies the requirement
188
- - Identify bugs, security vulnerabilities, error handling gaps, and test coverage issues
189
- - Assess code quality: readability, naming, separation of concerns, duplication
190
- - Do not run code, do not modify code \u2014 provide written judgment only
191
-
192
- Deliverables (save to the working directory):
193
- - code-review.md \u2014 score (1\u201310), blocking issues, advisory issues, conclusion (Approved / Changes Required)
194
- - When the task includes a Delivery acceptance review contract, the final response must be only its JSON verdict, not the prose report. Approve only with no unresolved blocking issues; include file locations and fix directions in changes_required issues. The verdict is a review judgment, not proof that unexecuted tests passed.
195
-
196
- Principles: Judge the code, not the developer. A blocking issue must include the file path, the specific problem, and a fix direction. Advisory items should be actionable, not vague. If the code is solid, say so.`;
197
- var TASK4 = `## Project
198
- {{projectName}}
199
-
200
- ## Requirement
201
- {{requirement}}
202
-
203
- ## Architecture Design
204
- {{file:architecture.md}}
205
-
206
- ## API Specification
207
- {{file:api-spec.md}}
208
- {{upstreamOutputs}}
209
-
210
- ## Task
211
- Perform a thorough code review of all implementation in the current directory and output code-review.md:
212
- 1. Overall score (1\u201310) and verdict: Approved / Changes Required
213
- 2. Blocking issues (bugs, security holes, missing requirement coverage, broken tests):
214
- - file path + line range, problem description, fix direction
215
- 3. Advisory issues (code quality, naming, duplication, missing error handling):
216
- - file path, observation, suggestion
217
- 4. Test coverage assessment
218
- 5. Final summary (2\u20133 sentences)
219
-
220
- Start by reading implementation-notes.md (if present), then read source and test files.`;
221
- function codeReviewer(overrides) {
222
- return {
223
- id: "code-reviewer",
224
- name: "Code Reviewer",
225
- icon: "\u2297",
226
- description: "reviews the code",
227
- runtime: "acp",
228
- models: { acp: "haiku" },
229
- modelTier: "fast",
230
- systemPrompt: SYSTEM4,
231
- taskPrompt: TASK4,
232
- tools: ["Read", "Write"],
233
- ...overrides
234
- };
222
+ // src/utils/abort.ts
223
+ var ABORT_ERROR_NAME = "AbortError";
224
+ function abortError(message = "Operation aborted") {
225
+ const err = new Error(message);
226
+ err.name = ABORT_ERROR_NAME;
227
+ return err;
235
228
  }
236
-
237
- // src/agents/devops.ts
238
- var SYSTEM5 = `You are the DevOps engineer. Your job is to containerize the application, set up CI/CD pipelines, and produce deployment-ready infrastructure configuration.
239
-
240
- Responsibilities:
241
- - Read the architecture design and implementation to understand the application structure
242
- - Write a Dockerfile (and docker-compose.yml if multiple services are involved)
243
- - Create a CI/CD pipeline configuration (GitHub Actions, GitLab CI, or similar based on the architecture)
244
- - Write any environment configuration templates (.env.example)
245
- - Document the deployment procedure and environment requirements
246
- - Do not modify application source code
247
-
248
- Deliverables (save to the working directory):
249
- - Dockerfile \u2014 production-ready container image definition
250
- - docker-compose.yml \u2014 local development / staging orchestration (if multiple services)
251
- - .github/workflows/ci.yml (or equivalent) \u2014 CI pipeline: build, test, lint
252
- - .env.example \u2014 required environment variables with descriptions
253
- - devops-notes.md \u2014 deployment procedure, environment requirements, known limitations
254
-
255
- Principles: Follow security best practices (non-root user, minimal base image, no secrets in image). Make CI fail fast. Document every non-obvious decision in devops-notes.md.`;
256
- var TASK5 = `## Task Scope
257
- {{agentScope}}
258
-
259
- ## Plan
260
- {{agentPlan}}
261
- {{upstreamOutputs}}
262
-
263
- ---
264
-
265
- ## Project
266
- {{projectName}}
267
-
268
- ## Architecture Design
269
- {{file:architecture.md}}
270
-
271
- ## Task
272
- Set up the deployment infrastructure for this project:
273
- 1. Write Dockerfile \u2014 multi-stage build if appropriate, non-root user, minimal image
274
- 2. Write docker-compose.yml \u2014 include all services from architecture.md (app, database, cache, etc.)
275
- 3. Write CI pipeline \u2014 build \u2192 lint \u2192 test \u2192 (optional) build image; fail fast
276
- 4. Write .env.example \u2014 all required environment variables with placeholder values and descriptions
277
- 5. Write devops-notes.md \u2014 how to build, run locally, deploy to production; environment requirements; known limitations
278
-
279
- Start by reading implementation-notes.md (if present) to understand what commands are used for build and test.`;
280
- function devops(overrides) {
281
- return {
282
- id: "devops",
283
- name: "DevOps",
284
- icon: "\u25B6",
285
- description: "sets up deployment",
286
- runtime: "acp",
287
- models: { acp: "sonnet" },
288
- modelTier: "balanced",
289
- systemPrompt: SYSTEM5,
290
- taskPrompt: TASK5,
291
- tools: ["Read", "Write"],
292
- ...overrides
293
- };
229
+ function isAbortError(err) {
230
+ return err instanceof Error && err.name === ABORT_ERROR_NAME;
294
231
  }
295
232
 
296
- // src/agents/frontend-developer.ts
297
- var SYSTEM6 = `You are the frontend engineer. Your responsibility is to turn design specs and API contracts into a working user interface.
298
-
299
- Responsibilities:
300
- - Read the architecture design and API specification, implement pages, components, and interactions
301
- - Integrate backend API contracts correctly; use mock data when the backend is not ready
302
- - Write frontend tests and run validation commands
303
-
304
- Deliverables (write code to the working directory, and record results in implementation-notes.md).
305
-
306
- Principles: Write code directly without asking for confirmation. Do not change the API contract \u2014 escalate contract issues upstream. Do not refactor unrelated code. Run commands and report evidence.`;
307
- var TASK6 = `## Task Scope
308
- {{agentScope}}
309
-
310
- ## Plan
311
- {{agentPlan}}
312
- {{upstreamOutputs}}
313
-
314
- ---
315
-
316
- ## UI Design Specification
317
- {{file:ui-design.md}}
318
-
319
- ## Architecture Design
320
- {{file:architecture.md}}
321
-
322
- ## API Specification
323
- {{file:api-spec.md}}
324
-
325
- ## Task
326
- Implement complete frontend code following the UI design specification, architecture design, and API specification:
327
- 1. Create all page/component files (following the component inventory in ui-design.md and directory structure in architecture.md)
328
- 2. Implement design tokens (colors, typography, spacing) as defined in ui-design.md
329
- 3. Integrate API endpoints (following api-spec.md); use mock data if the backend is not ready
330
- 4. Implement all interaction states: loading, empty, error, success
331
- 5. Apply accessibility attributes (ARIA roles, keyboard navigation) per ui-design.md
332
- 6. Write basic tests
333
- 7. Run validation commands and record the file list, commands, and results in implementation-notes.md
334
-
335
- Follow ui-design.md exactly. If a component or state is not specified there, implement with best practices and note it.`;
336
- function frontendDeveloper(overrides) {
337
- return {
338
- id: "frontend-developer",
339
- name: "Frontend Developer",
340
- icon: "\u25C7",
341
- description: "builds the interface",
342
- runtime: "acp",
343
- models: { acp: "sonnet" },
344
- modelTier: "balanced",
345
- systemPrompt: SYSTEM6,
346
- taskPrompt: TASK6,
347
- tools: ["Read", "Write", "Bash"],
348
- ...overrides
349
- };
233
+ // src/utils/errors.ts
234
+ var PermanentError = class extends Error {
235
+ constructor(message) {
236
+ super(message);
237
+ this.name = "PermanentError";
238
+ }
239
+ };
240
+ function isPermanent(err) {
241
+ return err instanceof PermanentError;
350
242
  }
351
243
 
352
- // src/agents/leader.ts
353
- var SYSTEM7 = `You are the project leader. Decide what the requirement actually needs before anyone else starts.
354
-
355
- There are three outcomes, and the first one matters most:
356
- - "reply" \u2014 the message is a greeting, a question, or anything you can answer on the spot. Answer it and stop; do not spend the team on it.
357
- - "ask" \u2014 it is a real task, but too vague to plan. Ask the one question that would unblock it.
358
- - "run" \u2014 it is a task you can scope. Name the minimum set of agents needed.
359
-
360
- Do not apply fixed rules \u2014 use your judgment based on the actual requirement and the code. A minor bug fix may need only a developer and code-reviewer; a complex new feature may need the full pipeline. Read the code to understand the impact before deciding.
361
-
362
- The console hands you the turns that came before this one. A requirement is often only interpretable next to them: "Postgres" is an answer, not a task, and the turn list is what tells you which question it answers. Read it as your own conversation \u2014 "you asked" is you. If you asked and got an answer, decide now; do not ask the same question again. For every "run" decision, include a complete, standalone "requirement" that combines the original task goal with all clarification answers and constraints. Never pass only the latest short answer to the team.
244
+ // src/runtime/RuntimeBase.ts
245
+ var RuntimeBase = class {
246
+ };
363
247
 
364
- Output ONLY a single JSON object on its own line \u2014 no prose, no markdown fences:
365
- {"mode": "reply", "message": "your answer"}
366
- {"mode": "ask", "message": "your question"}
367
- {"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id-1", "agent-id-2"]}`;
368
- var TASK7 = `## Project
369
- {{projectName}}
370
-
371
- ## Requirement
372
- {{requirement}}
373
- {{conversation}}
374
- ## Available Agents
375
- {{availableAgents}}
376
-
377
- ## Task
378
- Read the codebase to understand the current state, then decide how to handle the requirement.
379
- Output a single JSON object \u2014 exactly one of:
380
- {"mode": "reply", "message": "your answer"}
381
- {"mode": "ask", "message": "your question"}
382
- {"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id", ...]}
383
- Agent IDs must come from the Available Agents list above.`;
384
- function leader(overrides) {
385
- return {
386
- id: "leader",
387
- name: "Leader",
388
- icon: "\u2605",
389
- description: "routes the request",
390
- runtime: "acp",
391
- models: { acp: "sonnet" },
392
- modelTier: "balanced",
393
- systemPrompt: SYSTEM7,
394
- taskPrompt: TASK7,
395
- tools: ["Read", "Bash"],
396
- ...overrides
397
- };
248
+ // src/runtime/AcpRuntime.ts
249
+ var discoveredModels = /* @__PURE__ */ new Map();
250
+ var KILL_ESCALATION_MS = 3e3;
251
+ var CANCEL_GRACE_MS = 2e3;
252
+ var CLEANUP_ACK_MS = 3e3;
253
+ var STEER_ACK_MS = 15e3;
254
+ var DEFAULT_TIMEOUT_MS = 6e5;
255
+ var DEFAULT_COMMAND2 = "opencode";
256
+ var DEFAULT_ARGS = ["acp"];
257
+ var STEER_METHOD = "_session/steering";
258
+ function advertisesSteering(init) {
259
+ const steering = init._meta?.steering;
260
+ return isRecord(steering) && steering.supported === true;
398
261
  }
399
-
400
- // src/agents/product-manager.ts
401
- var SYSTEM8 = `You are the product manager. Your responsibility is to transform vague business ideas into clear, development-ready, testable product requirement documents.
402
-
403
- Responsibilities:
404
- - Analyze business goals and user scenarios
405
- - Define feature scope, business rules, and acceptance criteria
406
- - Structure "what is needed" into a PRD that the development team can execute directly
407
-
408
- Deliverables (save to the working directory):
409
- - requirements.md \u2014 product requirements document containing: one-line definition, goals and success metrics, user roles, functional requirements (FR-), business rules (BR-), acceptance criteria (AC-), open questions (OP-)
410
-
411
- Principles: Output the document directly without preamble. Mark uncertain items as OP- (open questions) rather than guessing. Do not do technical architecture design. Do not write feature code.`;
412
- var TASK8 = `## Business Context
413
- {{requirement}}
414
- {{upstreamOutputs}}
415
-
416
- ## Task
417
- Based on the context above, create requirements.md in the current directory containing:
418
- - One-line definition (what this product/feature is)
419
- - Goals and success metrics
420
- - User roles and typical scenarios
421
- - Functional requirements (FR-01, FR-02, ...)
422
- - Business rules (BR-01, ...)
423
- - Acceptance criteria (AC-01, ..., each must be executable and verifiable)
424
- - Open questions (OP-01, ..., list uncertain items here)
425
-
426
- Create the file directly. Mark uncertain items as OP- rather than guessing.`;
427
- function productManager(overrides) {
428
- return {
429
- id: "product-manager",
430
- name: "Product Manager",
431
- icon: "\u2261",
432
- description: "writes the requirements",
433
- runtime: "acp",
434
- models: { acp: "sonnet" },
435
- modelTier: "balanced",
436
- systemPrompt: SYSTEM8,
437
- taskPrompt: TASK8,
438
- tools: ["Write"],
439
- ...overrides
440
- };
262
+ var BASE_CAPABILITIES = {
263
+ streaming: true,
264
+ sessions: true,
265
+ resume: true,
266
+ permissions: true,
267
+ tools: true,
268
+ models: false,
269
+ images: false,
270
+ usage: true,
271
+ cost: true,
272
+ sandbox: false
273
+ };
274
+ function isRecord(value) {
275
+ return typeof value === "object" && value !== null && !Array.isArray(value);
441
276
  }
442
-
443
- // src/agents/reviewer.ts
444
- var SYSTEM9 = `You are an independent code review expert. You evaluate both business correctness and technical quality.
445
-
446
- Responsibilities:
447
- - Read requirements, design documents, and implementation code
448
- - Assess: whether the implementation satisfies requirements, code quality, security risks, and maintainability
449
- - Do not run code, do not modify code \u2014 provide only professional judgment
450
-
451
- Deliverables (save to the working directory):
452
- - review.md \u2014 score (1\u201310), issue list (blocking / advisory), improvement directions, conclusion (Approved / Changes Required)
453
-
454
- Principles: Review independently \u2014 do not approve just because the developer says it is fine. Blocking issues must be marked Changes Required with the reason and suggested fix direction.`;
455
- var TASK9 = `## Project
456
- {{projectName}}
457
-
458
- ## Requirement
459
- {{requirement}}
460
- {{upstreamOutputs}}
461
-
462
- ## Task
463
- Perform an independent code review of all implementation in the current directory and output review.md:
464
- 1. Read architecture.md (if present) to understand the design intent
465
- 2. Read all implementation code
466
- 3. Evaluate across these dimensions:
467
- - Requirement coverage: is every feature implemented?
468
- - Code quality: readability, maintainability, edge case handling
469
- - Security and robustness: input validation, error handling
470
- - Test adequacy (if test code is present)
471
- 4. Output review.md: overall score (1\u201310), issue list (blocking / advisory), conclusion (Approved / Changes Required)
472
-
473
- Judge independently. Do not accept "the developer says it's fine" as a reason to approve.`;
474
- function reviewer(overrides) {
475
- return {
476
- id: "reviewer",
477
- name: "Reviewer",
478
- icon: "\u25C9",
479
- description: "reviews the delivery",
480
- runtime: "acp",
481
- models: { acp: "haiku" },
482
- modelTier: "fast",
483
- systemPrompt: SYSTEM9,
484
- taskPrompt: TASK9,
485
- tools: ["Read", "Write"],
486
- ...overrides
487
- };
277
+ function textOf(content) {
278
+ if (isRecord(content) && content.type === "text" && typeof content.text === "string") {
279
+ return content.text;
280
+ }
281
+ return void 0;
488
282
  }
489
-
490
- // src/agents/tester.ts
491
- var SYSTEM10 = `You are the test engineer. Your responsibility is to verify that requirements have actually been implemented.
492
-
493
- Responsibilities:
494
- - Read requirements (acceptance criteria AC-) and the implementation code
495
- - Write and execute test cases covering: happy path, edge cases, error paths
496
- - Produce a reproducible test report
497
-
498
- Deliverables (save to the working directory):
499
- - test-report.md \u2014 overall verdict (PASS / FAIL / BLOCKED), test results per feature, reproduction steps and evidence for failures
500
-
501
- Principles: Do not PASS just because the code compiles or because a developer says it is fine. Every core feature must be verified with actual execution. When BLOCKED, state exactly what is missing.`;
502
- var TASK10 = `## Task Scope
503
- {{agentScope}}
504
-
505
- ## Plan
506
- {{agentPlan}}
507
- {{upstreamOutputs}}
508
-
509
- ---
510
-
511
- ## Project
512
- {{projectName}}
513
-
514
- ## Requirement
515
- {{requirement}}
516
-
517
- ## Task
518
- Verify the implementation code in the current directory:
519
- 1. Read all source code to understand the implementation
520
- 2. Write and execute tests for each core feature
521
- 3. Output results to test-report.md containing:
522
- - Overall verdict (PASS / FAIL / BLOCKED)
523
- - Test result for each feature
524
- - Reproduction steps and evidence for failures
525
-
526
- Start immediately. Verify actual behavior \u2014 do not just review code.`;
527
- function tester(overrides) {
528
- return {
529
- id: "tester",
530
- name: "Tester",
531
- icon: "\u2299",
532
- description: "verifies the behaviour",
533
- runtime: "acp",
534
- models: { acp: "sonnet" },
535
- modelTier: "balanced",
536
- systemPrompt: SYSTEM10,
537
- taskPrompt: TASK10,
538
- tools: ["Read", "Write", "Bash"],
539
- ...overrides
540
- };
283
+ function stringifyOutput(value) {
284
+ if (typeof value === "string") return value;
285
+ if (value === void 0 || value === null) return "";
286
+ return JSON.stringify(value);
541
287
  }
542
-
543
- // src/agents/ui-design-reviewer.ts
544
- var SYSTEM11 = `You are a UI/UX design reviewer. Your job is to evaluate a UI design specification before implementation begins.
545
-
546
- Responsibilities:
547
- - Review the design specification for completeness, consistency, and feasibility
548
- - Verify all user flows and states are accounted for (loading, empty, error, success)
549
- - Assess accessibility coverage: ARIA roles, keyboard navigation, focus management
550
- - Check that design tokens are consistent and the component inventory covers all requirements
551
- - Do not write implementation code \u2014 provide design-level feedback only
552
-
553
- Deliverables (save to the working directory):
554
- - ui-review.md \u2014 verdict (Approved / Changes Required), issue list (blocking / advisory) with component names and fix directions
555
-
556
- Principles: Be specific \u2014 reference component names or section titles in ui-design.md. A blocking issue must include what is missing or wrong and a fix direction. If the design is thorough and implementable, say so directly.`;
557
- var TASK11 = `## Requirement
558
- {{requirement}}
559
-
560
- ## UI Design Specification
561
- {{file:ui-design.md}}
562
- {{upstreamOutputs}}
563
-
564
- ## Task
565
- Review the UI design specification above and output ui-review.md:
566
- 1. Overall verdict: Approved / Changes Required
567
- 2. Blocking issues (missing components, undefined states, broken flows, accessibility gaps):
568
- - component/section name, problem, fix direction
569
- 3. Advisory issues (inconsistent tokens, unclear props, polish):
570
- - component/section name, observation, suggestion
571
- 4. Design strengths worth noting
572
- 5. Final summary (1\u20132 sentences)
573
-
574
- Reference specific component names or section titles when citing issues.`;
575
- function uiDesignReviewer(overrides) {
288
+ function mapStopReason(reason) {
289
+ switch (reason) {
290
+ case "end_turn":
291
+ return "success";
292
+ case "cancelled":
293
+ return "cancelled";
294
+ case "max_tokens":
295
+ case "max_turn_requests":
296
+ return "max_turns";
297
+ default:
298
+ return "error";
299
+ }
300
+ }
301
+ var KIND_TO_TOOLS = {
302
+ read: ["Read"],
303
+ edit: ["Edit", "Write"],
304
+ execute: ["Bash"],
305
+ search: ["Grep", "Glob"],
306
+ fetch: ["WebFetch"]
307
+ };
308
+ function toolPermitted(params, tools) {
309
+ const allowed = new Set(tools.map((tool) => tool.toLowerCase()));
310
+ const name = params.toolCall.name ?? void 0;
311
+ return name !== void 0 ? allowed.has(name.toLowerCase()) : (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some(
312
+ (tool) => allowed.has(tool.toLowerCase())
313
+ );
314
+ }
315
+ function decidePermission(params, tools, sessionId) {
316
+ if (sessionId !== void 0 && params.sessionId !== sessionId) {
317
+ return { response: { outcome: { outcome: "cancelled" } } };
318
+ }
319
+ const name = params.toolCall.name ?? void 0;
320
+ const kind = params.toolCall.kind ?? void 0;
321
+ const approved = toolPermitted(params, tools);
322
+ const option = params.options.find(
323
+ (o) => approved ? o.kind === "allow_once" : o.kind === "reject_once"
324
+ );
325
+ const response = option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
326
+ if (approved) return { response };
576
327
  return {
577
- id: "ui-design-reviewer",
578
- name: "UI Design Reviewer",
579
- icon: "\u25CE",
580
- description: "reviews the UI design",
581
- runtime: "acp",
582
- models: { acp: "haiku" },
583
- modelTier: "fast",
584
- systemPrompt: SYSTEM11,
585
- taskPrompt: TASK11,
586
- tools: ["Read", "Write"],
587
- ...overrides
328
+ response,
329
+ rejected: {
330
+ toolCallId: params.toolCall.toolCallId,
331
+ name,
332
+ kind,
333
+ title: params.toolCall.title ?? void 0
334
+ }
588
335
  };
589
336
  }
590
-
591
- // src/agents/ui-designer.ts
592
- var SYSTEM12 = `You are a UI/UX designer. Your job is to translate architecture and requirements into a detailed, developer-ready UI design specification.
593
-
594
- Responsibilities:
595
- - Define the component inventory: every page, section, and reusable component
596
- - Specify layouts, spacing principles, and responsive breakpoints
597
- - Define interaction patterns: loading states, empty states, error states, transitions
598
- - Specify design tokens: color palette, typography scale, spacing scale, shadow levels
599
- - Describe accessibility requirements: ARIA roles, keyboard navigation, focus management
600
- - Do not write implementation code \u2014 produce specification documents only
601
-
602
- Deliverables (save to the working directory):
603
- - ui-design.md \u2014 full UI design specification: component inventory, page layouts, interaction flows, design tokens, accessibility requirements
604
-
605
- Principles: Be specific enough that a developer can implement without guessing. Every component gets a name, purpose, props/variants, and states. Flag anything that depends on backend data.`;
606
- var TASK12 = `## Task Scope
607
- {{agentScope}}
608
-
609
- ## Plan
610
- {{agentPlan}}
611
- {{upstreamOutputs}}
612
-
613
- ---
614
-
615
- ## Project
616
- {{projectName}}
617
-
618
- ## Requirement
619
- {{requirement}}
620
-
621
- ## Architecture Design
622
- {{file:architecture.md}}
623
-
624
- ## API Specification
625
- {{file:api-spec.md}}
626
-
627
- ## Task
628
- Create ui-design.md covering:
629
- 1. **Component Inventory** \u2014 list every page and reusable component; for each: name, purpose, variants/states, props
630
- 2. **Page Layouts** \u2014 describe the visual structure of each page (header, main, sidebar, footer zones); include responsive behaviour
631
- 3. **Interaction Flows** \u2014 key user flows with step-by-step state transitions (loading \u2192 success \u2192 error)
632
- 4. **Design Tokens** \u2014 color palette (primary, neutral, semantic), typography scale, spacing scale
633
- 5. **Accessibility** \u2014 ARIA roles/labels for interactive components, keyboard nav, focus order
634
-
635
- Be concrete. Reference API endpoints where components bind to data. Flag any UX decision that needs product clarification.`;
636
- function uiDesigner(overrides) {
637
- return {
638
- id: "ui-designer",
639
- name: "UI Designer",
640
- icon: "\u25A3",
641
- description: "specs the interface",
642
- runtime: "acp",
643
- models: { acp: "sonnet" },
644
- modelTier: "balanced",
645
- systemPrompt: SYSTEM12,
646
- taskPrompt: TASK12,
647
- tools: ["Read", "Write"],
648
- ...overrides
649
- };
337
+ var FORM_ANNOTATIONS = ["title", "description", "default", "_meta"];
338
+ function knownFormKeys(value, keys) {
339
+ return Object.keys(value).every((key) => keys.includes(key) || FORM_ANNOTATIONS.includes(key));
650
340
  }
651
-
652
- // src/agents/index.ts
653
- var agents = {
654
- leader,
655
- architect,
656
- productManager,
657
- backendDeveloper,
658
- frontendDeveloper,
659
- tester,
660
- reviewer,
661
- archDesignReviewer,
662
- uiDesigner,
663
- uiDesignReviewer,
664
- codeReviewer,
665
- devops
666
- };
667
-
668
- // src/config/defineConfig.ts
669
- function defineConfig(config) {
670
- return config;
341
+ function validFormBound(value, integer = false) {
342
+ return value == null || typeof value === "number" && Number.isFinite(value) && (!integer || Number.isInteger(value) && value >= 0);
671
343
  }
672
-
673
- // src/event/EventBus.ts
674
- var EventBus = class {
675
- constructor() {
676
- this.listeners = /* @__PURE__ */ new Set();
677
- }
678
- subscribe(listener) {
679
- this.listeners.add(listener);
680
- return () => this.listeners.delete(listener);
681
- }
682
- emit(event) {
683
- for (const listener of this.listeners) {
684
- try {
685
- listener(event);
686
- } catch (err) {
687
- console.error("EventBus listener error:", err);
688
- }
344
+ function titledChoices(value) {
345
+ return Array.isArray(value) && value.length > 0 && value.every(
346
+ (option) => isRecord(option) && knownFormKeys(option, ["const"]) && typeof option.const === "string"
347
+ );
348
+ }
349
+ function supportedQuestionSchema(schema) {
350
+ if (!isRecord(schema) || schema.type !== void 0 && schema.type !== "object" || !knownFormKeys(schema, ["type", "properties", "required"]))
351
+ return false;
352
+ const properties = schema.properties ?? {};
353
+ if (!isRecord(properties)) return false;
354
+ if (schema.required != null && (!Array.isArray(schema.required) || !schema.required.every(
355
+ (key) => typeof key === "string" && Object.getOwnPropertyDescriptor(properties, key) !== void 0
356
+ )))
357
+ return false;
358
+ return Object.values(properties).every((field) => {
359
+ if (!isRecord(field)) return false;
360
+ if (field.type === "string") {
361
+ return knownFormKeys(field, ["type", "enum", "oneOf", "minLength", "maxLength"]) && validFormBound(field.minLength, true) && validFormBound(field.maxLength, true) && (field.enum == null || Array.isArray(field.enum) && field.enum.length > 0 && field.enum.every((value) => typeof value === "string")) && (field.oneOf == null || titledChoices(field.oneOf));
689
362
  }
363
+ if (field.type === "boolean") return knownFormKeys(field, ["type"]);
364
+ if (field.type === "number" || field.type === "integer") {
365
+ return knownFormKeys(field, ["type", "minimum", "maximum"]) && validFormBound(field.minimum) && validFormBound(field.maximum);
366
+ }
367
+ if (field.type !== "array" || !isRecord(field.items) || !knownFormKeys(field, ["type", "items", "minItems", "maxItems"]) || !validFormBound(field.minItems, true) || !validFormBound(field.maxItems, true))
368
+ return false;
369
+ return knownFormKeys(field.items, ["type", "enum"]) && field.items.type === "string" && Array.isArray(field.items.enum) && field.items.enum.length > 0 && field.items.enum.every((value) => typeof value === "string") || knownFormKeys(field.items, ["anyOf"]) && field.items.type === void 0 && titledChoices(field.items.anyOf);
370
+ });
371
+ }
372
+ function validateRuntimeQuestionContent(schema, content) {
373
+ if (!supportedQuestionSchema(schema) || !isRecord(content)) return false;
374
+ if (Object.getOwnPropertySymbols(content).length > 0 || Object.getOwnPropertyNames(content).some((key) => {
375
+ const descriptor = Object.getOwnPropertyDescriptor(content, key);
376
+ return !descriptor?.enumerable || !("value" in descriptor);
377
+ }))
378
+ return false;
379
+ const properties = schema.properties ?? {};
380
+ if ((schema.required ?? []).some(
381
+ (key) => Object.getOwnPropertyDescriptor(content, key) === void 0
382
+ ))
383
+ return false;
384
+ return Object.entries(content).every(([key, value]) => {
385
+ if (Object.getOwnPropertyDescriptor(properties, key) === void 0) return false;
386
+ const field = properties[key];
387
+ if (field.type === "boolean") return typeof value === "boolean";
388
+ if (field.type === "number" || field.type === "integer") {
389
+ return typeof value === "number" && Number.isFinite(value) && (field.type !== "integer" || Number.isInteger(value)) && (typeof field.minimum !== "number" || value >= field.minimum) && (typeof field.maximum !== "number" || value <= field.maximum);
390
+ }
391
+ if (field.type === "string") {
392
+ return typeof value === "string" && (typeof field.minLength !== "number" || [...value].length >= field.minLength) && (typeof field.maxLength !== "number" || [...value].length <= field.maxLength) && (!Array.isArray(field.enum) || field.enum.includes(value)) && (!Array.isArray(field.oneOf) || field.oneOf.some((option) => isRecord(option) && option.const === value));
393
+ }
394
+ const items = field.items;
395
+ const choices = Array.isArray(items.enum) ? items.enum : items.anyOf.map((option) => option.const);
396
+ return Array.isArray(value) && value.every((item) => typeof item === "string" && choices.includes(item)) && new Set(value).size === value.length && (typeof field.minItems !== "number" || value.length >= field.minItems) && (typeof field.maxItems !== "number" || value.length <= field.maxItems);
397
+ });
398
+ }
399
+ function withAuthHint(acp, err) {
400
+ const error = err instanceof Error ? err : new Error(String(err));
401
+ if (error instanceof acp.RequestError && error.code === acp.RequestError.authRequired().code) {
402
+ return new Error(`${error.message} \u2014 run \`claude\` to log in`);
690
403
  }
691
- clear() {
692
- this.listeners.clear();
693
- }
694
- };
695
-
696
- // src/utils/abort.ts
697
- var ABORT_ERROR_NAME = "AbortError";
698
- function abortError(message = "Operation aborted") {
699
- const err = new Error(message);
700
- err.name = ABORT_ERROR_NAME;
701
- return err;
404
+ return error;
702
405
  }
703
- function isAbortError(err) {
704
- return err instanceof Error && err.name === ABORT_ERROR_NAME;
406
+ function errorMessage(err) {
407
+ return err instanceof Error ? err.message : String(err);
705
408
  }
706
-
707
- // src/orchestrator/AgentSessionManager.ts
708
- var AgentSessionManager = class {
409
+ function timeoutMs(value) {
410
+ const timeout = value ?? DEFAULT_TIMEOUT_MS;
411
+ if (!Number.isFinite(timeout) || timeout < 0 || timeout > 2147483647) {
412
+ throw new PermanentError("ACP timeout must be a finite non-negative timer duration");
413
+ }
414
+ return timeout;
415
+ }
416
+ var AcpRuntime = class extends RuntimeBase {
709
417
  constructor() {
710
- this.entries = /* @__PURE__ */ new Map();
711
- this.closed = false;
418
+ super(...arguments);
419
+ this.name = "acp";
712
420
  }
713
- async execute(runtime, task) {
714
- if (this.closed) throw new Error("AgentSessionManager is closed");
715
- if (this.resetting) throw new Error("AgentSessionManager is resetting");
716
- if (task.signal?.aborted) throw abortError();
717
- let entry3 = this.entries.get(task.agentId);
718
- if (entry3?.busy || entry3?.session && entry3.session.state !== "idle" && entry3.session.state !== "closed") {
719
- throw new Error(`Agent session ${task.agentId} is busy`);
720
- }
721
- if (entry3?.closing) await entry3.closing;
722
- const fingerprint = JSON.stringify([
723
- task.command,
724
- task.args,
725
- task.model,
726
- task.workDir,
727
- task.systemPrompt,
728
- task.tools,
729
- task.sessionId,
730
- task.resume,
731
- Boolean(task.onQuestion)
732
- ]);
733
- if (entry3 && (entry3.runtime !== runtime || entry3.fingerprint !== fingerprint || entry3.session?.state === "closed" || !runtime.openSession)) {
734
- entry3.busy = true;
735
- try {
736
- await this.closeEntry(entry3);
737
- this.entries.delete(task.agentId);
738
- } finally {
739
- entry3.busy = false;
421
+ async describe(options) {
422
+ const command = options?.command ?? DEFAULT_COMMAND2;
423
+ return {
424
+ name: this.name,
425
+ available: resolveExecutable(command) !== null,
426
+ capabilities: {
427
+ ...BASE_CAPABILITIES,
428
+ tools: specForCommand(command)?.tools ?? false,
429
+ models: specForCommand(command)?.modelSelection === "config-option"
740
430
  }
741
- entry3 = void 0;
742
- if (this.closed || this.resetting) throw abortError();
743
- }
744
- if (task.signal?.aborted) throw abortError();
745
- const openSession = runtime.openSession;
746
- if (!openSession) return runtime.execute(task);
747
- let detach;
748
- if (!entry3) {
749
- const startup = new AbortController();
750
- const forwardAbort = () => startup.abort(task.signal?.reason);
751
- task.signal?.addEventListener("abort", forwardAbort, { once: true });
752
- detach = () => task.signal?.removeEventListener("abort", forwardAbort);
753
- const created = {
754
- runtime,
755
- fingerprint,
756
- startup,
757
- busy: true,
758
- onWarning: task.onWarning,
759
- // Defer opening until the cache entry is installed, including synchronous failures.
760
- opening: Promise.resolve().then(
761
- () => openSession.call(runtime, {
762
- agentId: task.agentId,
763
- systemPrompt: task.systemPrompt,
764
- tools: task.tools,
765
- workDir: task.workDir,
766
- command: task.command,
767
- args: task.args,
768
- model: task.model,
769
- sessionId: task.sessionId,
770
- resume: task.resume,
771
- timeout: task.timeout,
772
- questionForms: Boolean(task.onQuestion),
773
- signal: startup.signal,
774
- onWarning: (message) => created.onWarning?.(message)
775
- })
776
- ).then((session) => {
777
- created.session = session;
778
- return session;
779
- })
780
- };
781
- entry3 = created;
782
- this.entries.set(task.agentId, entry3);
783
- } else {
784
- entry3.busy = true;
785
- entry3.onWarning = task.onWarning;
431
+ };
432
+ }
433
+ async execute(task) {
434
+ const { prompt, onEvent, signal, permissionMode, onPermission, onQuestion, ...options } = task;
435
+ const startup = new AbortController();
436
+ const forward = () => startup.abort();
437
+ if (signal?.aborted) startup.abort();
438
+ signal?.addEventListener("abort", forward, { once: true });
439
+ let session;
440
+ try {
441
+ session = await this.openSession({
442
+ ...options,
443
+ questionForms: Boolean(onQuestion),
444
+ signal: startup.signal
445
+ });
446
+ } finally {
447
+ signal?.removeEventListener("abort", forward);
786
448
  }
787
449
  try {
788
- const session = await entry3.opening;
789
- detach?.();
790
- detach = void 0;
791
- if (entry3.startup.signal.aborted || this.closed || this.resetting) {
792
- await this.closeEntry(entry3);
793
- throw abortError();
794
- }
795
- if (session.state !== "idle") throw new Error(`Agent session ${task.agentId} is busy`);
796
- return await session.prompt(task.prompt, {
797
- signal: task.signal,
450
+ return await session.prompt(prompt, {
451
+ signal,
798
452
  timeout: task.timeout,
799
- onEvent: task.onEvent,
800
- permissionMode: task.permissionMode,
801
- onPermission: task.onPermission,
802
- onQuestion: task.onQuestion
453
+ onEvent,
454
+ permissionMode,
455
+ onPermission,
456
+ onQuestion
803
457
  });
804
458
  } finally {
805
- detach?.();
806
- entry3.onWarning = void 0;
807
- try {
808
- if (entry3.session?.state === "closed") {
809
- await this.closeEntry(entry3);
810
- if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
811
- } else if (!entry3.session) {
812
- if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
813
- }
814
- } finally {
815
- entry3.busy = false;
816
- }
459
+ await session.close();
817
460
  }
818
461
  }
819
- /**
820
- * Deliver a message into every session that is running a turn right now.
821
- *
822
- * Only `running` sessions are candidates, and that is the whole safety
823
- * argument: a session between turns has nothing to inject into, and asking a
824
- * provider to steer an idle session makes it *start* a turn that this manager
825
- * never scheduled — one whose output belongs to no turn and which nothing
826
- * would ever settle. Sessions that are idle are therefore not reported as
827
- * failures but simply absent, and the caller sees an empty list.
828
- *
829
- * One agent failing never stops the others: each report stands alone, because
830
- * the caller has to be able to say "architect took it, qa did not" rather than
831
- * "steering failed".
832
- */
833
- async supplement(text) {
834
- if (this.closed) return [];
835
- const targets = [...this.entries.entries()].filter(
836
- ([, entry3]) => entry3.session?.state === "running"
462
+ async openSession(options) {
463
+ if (options.signal?.aborted) throw abortError();
464
+ if (options.resume === "fork") {
465
+ throw new PermanentError("ACP does not support forked sessions");
466
+ }
467
+ const defaultTimeout = timeoutMs(options.timeout);
468
+ const questionForms = options.questionForms === true;
469
+ let startupReject;
470
+ const startupFailure = new Promise((_, reject) => {
471
+ startupReject = reject;
472
+ });
473
+ let failSession;
474
+ const failStartup = (error) => {
475
+ startupReject(error);
476
+ failSession?.(error);
477
+ };
478
+ const onSessionAbort = () => failStartup(abortError());
479
+ options.signal?.addEventListener("abort", onSessionAbort, { once: true });
480
+ const startupTimer = setTimeout(
481
+ () => failStartup(new Error(`acp startup timed out (${defaultTimeout}ms)`)),
482
+ defaultTimeout
837
483
  );
838
- return await Promise.all(
839
- targets.map(async ([agentId, entry3]) => {
840
- const session = entry3.session;
841
- if (!session) return { agentId, status: "idle" };
842
- if (!session.capabilities.supplement || !session.supplement) {
843
- return { agentId, status: "unsupported" };
484
+ let cleanup;
485
+ try {
486
+ const acp = await Promise.race([import("@agentclientprotocol/sdk"), startupFailure]);
487
+ if (options.signal?.aborted) throw abortError();
488
+ const command = options.command ?? DEFAULT_COMMAND2;
489
+ const spec = specForCommand(command);
490
+ const meta = spec?.meta?.({ ...options, prompt: "", timeout: defaultTimeout });
491
+ const child = spawn(resolveExecutable(command) ?? command, options.args ?? DEFAULT_ARGS, {
492
+ cwd: options.workDir,
493
+ env: process.env,
494
+ detached: true
495
+ });
496
+ let state = "idle";
497
+ let sessionId = "";
498
+ let steeringSupported = false;
499
+ let active;
500
+ let childClosed = false;
501
+ let terminalError;
502
+ let errorOutput = "";
503
+ let escalation;
504
+ let acknowledgement;
505
+ let closePromise;
506
+ let closeResolve;
507
+ let closeReject;
508
+ let connection;
509
+ const killTree = (signal) => {
510
+ if (child.pid !== void 0) {
511
+ try {
512
+ process.kill(-child.pid, signal);
513
+ return;
514
+ } catch {
515
+ }
844
516
  }
845
517
  try {
846
- return { agentId, status: (await session.supplement(text)).status };
847
- } catch (error) {
848
- return {
849
- agentId,
850
- status: "error",
851
- error: error instanceof Error ? error.message : String(error)
852
- };
518
+ child.kill(signal);
519
+ } catch {
853
520
  }
854
- })
855
- );
856
- }
857
- /** Closes all sessions and permits a fresh conversation after cleanup completes. */
858
- reset() {
859
- if (this.closing) return this.closing;
860
- if (this.resetting) return this.resetting;
861
- this.resetting = this.drain().finally(() => {
862
- this.resetting = void 0;
863
- });
864
- return this.resetting;
865
- }
866
- /** Terminal, idempotent cleanup barrier. No new executions may start. */
867
- close() {
868
- if (this.closing) return this.closing;
869
- this.closed = true;
870
- this.closing = this.resetting ?? this.drain();
871
- return this.closing;
872
- }
873
- async drain() {
874
- const entries = [...this.entries.entries()];
875
- for (const [, entry3] of entries) entry3.startup.abort();
876
- const results = await Promise.allSettled(
877
- entries.map(async ([id, entry3]) => {
878
- await this.closeEntry(entry3);
879
- if (this.entries.get(id) === entry3) this.entries.delete(id);
880
- })
881
- );
882
- const errors = results.flatMap(
883
- (result) => result.status === "rejected" ? [result.reason] : []
884
- );
885
- if (errors.length === 1) throw errors[0];
886
- if (errors.length > 1) throw new AggregateError(errors, "Agent session cleanup failed");
887
- }
888
- closeEntry(entry3) {
889
- if (!entry3.closing) {
890
- entry3.closing = (async () => {
891
- let session;
521
+ };
522
+ const emitForTurn = (turn, event) => {
523
+ turn.options.onEvent?.({
524
+ ...event,
525
+ runId: turn.runId,
526
+ agentId: options.agentId,
527
+ seq: turn.seq++
528
+ });
529
+ };
530
+ const emit = (event) => {
531
+ if (active) emitForTurn(active, event);
532
+ };
533
+ const settleTurn = (turn, output, error) => {
534
+ if (active !== turn) return;
535
+ turn.decisions.abort();
536
+ if (turn.timer) clearTimeout(turn.timer);
537
+ if (turn.cancelTimer) clearTimeout(turn.cancelTimer);
538
+ if (turn.onAbort) turn.options.signal?.removeEventListener("abort", turn.onAbort);
539
+ active = void 0;
540
+ if (state === "running" || state === "cancelling") state = "idle";
541
+ if (error) {
542
+ if (closePromise) {
543
+ void closePromise.then(
544
+ () => turn.reject(error),
545
+ (cleanupError) => turn.reject(cleanupError)
546
+ );
547
+ } else turn.reject(error);
548
+ } else if (output) turn.resolve(output);
549
+ };
550
+ const clearCleanupTimers = () => {
551
+ if (escalation) clearTimeout(escalation);
552
+ if (acknowledgement) clearTimeout(acknowledgement);
553
+ };
554
+ const close = () => {
555
+ if (closePromise) return closePromise;
556
+ closePromise = new Promise((resolve4, reject) => {
557
+ closeResolve = resolve4;
558
+ closeReject = reject;
559
+ });
560
+ options.signal?.removeEventListener("abort", onSessionAbort);
561
+ clearTimeout(startupTimer);
562
+ state = "closing";
563
+ if (active) settleTurn(active, void 0, active.error ?? terminalError ?? abortError());
564
+ if (childClosed) {
565
+ state = "closed";
566
+ closeResolve?.();
567
+ return closePromise;
568
+ }
569
+ escalation = setTimeout(() => {
570
+ killTree("SIGKILL");
571
+ if (childClosed) return;
572
+ acknowledgement = setTimeout(() => {
573
+ state = "closed";
574
+ clearCleanupTimers();
575
+ closeReject?.(new Error(`${command} cleanup failed: no child close after SIGKILL`));
576
+ }, CLEANUP_ACK_MS);
577
+ }, KILL_ESCALATION_MS);
892
578
  try {
893
- session = await entry3.opening;
894
- } catch (error) {
895
- if (isAbortError(error)) return;
896
- throw error;
897
- }
898
- await session.close();
899
- })();
900
- }
901
- return entry3.closing;
902
- }
903
- };
904
-
905
- // src/orchestrator/Orchestrator.ts
906
- import * as fs4 from "fs";
907
- import * as path5 from "path";
908
-
909
- // src/providers/catalog.ts
910
- import * as path from "path";
911
- function normalizeCommandKey(command) {
912
- return path.basename(command).toLowerCase().replace(/\.(cmd|exe|bat)$/i, "");
913
- }
914
- var PROVIDERS = [
915
- {
916
- id: "opencode",
917
- label: "OpenCode",
918
- command: "opencode",
919
- args: ["acp"],
920
- tools: false,
921
- modelSelection: "config-option",
922
- models: [
923
- { id: "opencode/big-pickle", label: "Big Pickle", tier: "balanced", defaultForTier: true },
924
- { id: "opencode/exo-free", label: "Exo", tier: "strong", defaultForTier: true },
925
- { id: "opencode/ling-3.0-flash-fin-free", label: "Ling 3.0 Flash", tier: "fast" },
926
- {
927
- id: "opencode/ling-3.1-flash-free",
928
- label: "Ling 3.1 Flash",
929
- tier: "fast",
930
- defaultForTier: true
931
- },
932
- { id: "opencode/longcat-2.5-preview-free", label: "Longcat 2.5 Preview", tier: "balanced" },
933
- { id: "opencode/mimo-v2.6-flash-free", label: "Mimo 2.6 Flash", tier: "fast" },
934
- { id: "opencode/muse-spark-1.3-contributor-free", label: "Muse Spark 1.3", tier: "fast" },
935
- { id: "opencode/nemotron-3-ultra-free", label: "Nemotron 3 Ultra", tier: "strong" },
936
- { id: "opencode/nemotron-3.5-lightning-free", label: "Nemotron 3.5 Lightning", tier: "fast" },
937
- { id: "opencode/space-bunny-free", label: "Space Bunny", tier: "fast" },
938
- { id: "opencode/step-5-preview-free", label: "Step 5 Preview", tier: "strong" }
939
- ],
940
- defaultModel: "opencode/big-pickle",
941
- install: { hint: "npm install -g opencode" },
942
- auth: { kind: "none", hint: "" }
943
- },
944
- {
945
- id: "claude",
946
- label: "Claude",
947
- command: "claude-agent-acp",
948
- args: [],
949
- tools: true,
950
- modelSelection: "config-option",
951
- meta: (task) => {
952
- const meta = {};
953
- if (task.systemPrompt) meta.systemPrompt = task.systemPrompt;
954
- meta.claudeCode = { options: { tools: task.tools } };
955
- return meta;
956
- },
957
- models: [
958
- { id: "default", label: "Default", tier: "balanced" },
959
- { id: "opus", label: "Opus", tier: "strong", defaultForTier: true },
960
- { id: "fable", label: "Fable", tier: "balanced" },
961
- { id: "sonnet", label: "Sonnet", tier: "balanced", defaultForTier: true },
962
- { id: "haiku", label: "Haiku", tier: "fast", defaultForTier: true }
963
- ],
964
- defaultModel: "sonnet",
965
- install: {
966
- hint: "multiclaw bundles the ACP bridge \u2014 install the Claude Code CLI (`claude`) and run `claude` to log in"
967
- },
968
- auth: { kind: "ambient", hint: "run `claude` to log in" }
969
- },
970
- {
971
- id: "codex",
972
- label: "Codex",
973
- command: "codex-acp",
974
- args: [],
975
- tools: false,
976
- modelSelection: "config-option",
977
- meta: (task) => task.systemPrompt ? { jetbrains: { air: { customInstructions: task.systemPrompt } } } : void 0,
978
- // Placeholders — real ids require auth to enumerate.
979
- models: [
980
- { id: "gpt-5-codex", label: "GPT-5 Codex", tier: "strong", defaultForTier: true },
981
- { id: "gpt-5", label: "GPT-5", tier: "balanced", defaultForTier: true },
982
- { id: "gpt-5-mini", label: "GPT-5 Mini", tier: "fast", defaultForTier: true }
983
- ],
984
- defaultModel: "gpt-5",
985
- install: {
986
- hint: "multiclaw bundles the ACP bridge \u2014 install the Codex CLI (`codex`) and run `codex login`"
987
- },
988
- auth: { kind: "api-key", hint: "set OPENAI_API_KEY or run `codex login`" }
989
- },
990
- {
991
- id: "cursor",
992
- label: "Cursor",
993
- command: "cursor-agent",
994
- aliases: ["agent"],
995
- args: ["acp"],
996
- tools: false,
997
- modelSelection: "config-option",
998
- // _meta support unverified — falls back to prompt prefix until confirmed.
999
- meta: void 0,
1000
- // Placeholders — real ids require Cursor CLI + auth to enumerate.
1001
- models: [
1002
- { id: "cursor-pro", label: "Cursor Pro", tier: "strong", defaultForTier: true },
1003
- { id: "cursor-fast", label: "Cursor Fast", tier: "fast", defaultForTier: true }
1004
- ],
1005
- defaultModel: "cursor-pro",
1006
- install: {
1007
- hint: "curl https://cursor.com/install | bash",
1008
- url: "https://cursor.com/docs/cli/acp"
1009
- },
1010
- auth: { kind: "login", hint: "run `agent login` first (or set CURSOR_API_KEY)" },
1011
- confirm: (v) => /cursor/i.test(v)
1012
- }
1013
- ];
1014
- var byProviderId = /* @__PURE__ */ new Map();
1015
- var byCommand = /* @__PURE__ */ new Map();
1016
- for (const spec of PROVIDERS) {
1017
- byProviderId.set(spec.id, spec);
1018
- byCommand.set(normalizeCommandKey(spec.command), spec);
1019
- if (spec.aliases) {
1020
- for (const alias of spec.aliases) {
1021
- byCommand.set(normalizeCommandKey(alias), spec);
1022
- }
1023
- }
1024
- }
1025
- function specForProvider(id) {
1026
- return byProviderId.get(id);
1027
- }
1028
- function specForCommand(command) {
1029
- return byCommand.get(normalizeCommandKey(command));
1030
- }
1031
- function providerIds() {
1032
- return [...byProviderId.keys()];
1033
- }
1034
- function providerCommands() {
1035
- return [...byCommand.keys()];
1036
- }
1037
- function tierModel(spec, tier) {
1038
- const m = spec.models.find((m2) => m2.tier === tier && m2.defaultForTier);
1039
- return m?.id ?? spec.models.find((m2) => m2.tier === tier)?.id;
1040
- }
1041
-
1042
- // src/providers/resolve.ts
1043
- var DEFAULT_COMMAND = "opencode";
1044
- var DEFAULT_PROVIDER = "opencode";
1045
- function resolveProvider(agent, config) {
1046
- return agent.provider ?? specForCommand(agent.command ?? "")?.id ?? config.defaults?.provider ?? specForCommand(DEFAULT_COMMAND)?.id ?? DEFAULT_PROVIDER;
1047
- }
1048
- function resolveCommand(agent, config) {
1049
- return agent.command ?? config.defaults?.command ?? specForProvider(resolveProvider(agent, config))?.command ?? DEFAULT_COMMAND;
1050
- }
1051
- function resolveArgs(agent, config) {
1052
- if (agent.args) return agent.args;
1053
- const command = agent.command ?? config.defaults?.command;
1054
- if (command) return specForCommand(command)?.args;
1055
- return specForProvider(resolveProvider(agent, config))?.args;
1056
- }
1057
- function resolveModel(agent, config) {
1058
- if (agent.model) return agent.model;
1059
- const provider = resolveProvider(agent, config);
1060
- const spec = specForProvider(provider);
1061
- const fromModels = agent.models?.[provider] ?? agent.models?.acp;
1062
- if (fromModels) return fromModels;
1063
- if (agent.modelTier && spec) {
1064
- const fromTier = tierModel(spec, agent.modelTier);
1065
- if (fromTier) return fromTier;
1066
- }
1067
- if (config.defaults?.model) return config.defaults.model;
1068
- if (spec) return spec.defaultModel;
1069
- return void 0;
1070
- }
1071
- function resolveAgentRuntime(agent, config) {
1072
- const provider = resolveProvider(agent, config);
1073
- const spec = specForProvider(provider);
1074
- return {
1075
- provider,
1076
- command: resolveCommand(agent, config),
1077
- model: resolveModel(agent, config),
1078
- spec
1079
- };
1080
- }
1081
-
1082
- // src/utils/conversation.ts
1083
- var MAX_CONVERSATION_TURNS = 6;
1084
- var MAX_CONVERSATION_CHARS = 4e3;
1085
- var MAX_ENTRY_CHARS = 600;
1086
- var ELLIPSIS = "\u2026";
1087
- function clip(text, max) {
1088
- const trimmed = text.trim();
1089
- if (trimmed.length <= max) return trimmed;
1090
- return `${trimmed.slice(0, Math.max(0, max - 1)).trimEnd()}${ELLIPSIS}`;
1091
- }
1092
- function entry(turn, index) {
1093
- const lines = [`${index}. user: ${clip(turn.requirement, MAX_ENTRY_CHARS)}`];
1094
- if (turn.reply) {
1095
- const verb = turn.reply.mode === "ask" ? "asked" : "answered";
1096
- lines.push(` you ${verb}: ${clip(turn.reply.message, MAX_ENTRY_CHARS)}`);
1097
- }
1098
- return lines.join("\n");
1099
- }
1100
- function renderConversation(history) {
1101
- if (!history || history.length === 0) return "";
1102
- const recent = history.slice(-MAX_CONVERSATION_TURNS);
1103
- const first = history.length - recent.length;
1104
- const entries = recent.map((turn, i) => entry(turn, first + i + 1));
1105
- while (entries.length > 1 && entries.join("\n").length > MAX_CONVERSATION_CHARS) {
1106
- entries.shift();
1107
- }
1108
- const body = clip(entries.join("\n"), MAX_CONVERSATION_CHARS);
1109
- return `
1110
- ## Conversation so far
1111
-
1112
- Earlier turns of this session, oldest first:
1113
-
1114
- ${body}
1115
- `;
1116
- }
1117
-
1118
- // src/utils/json.ts
1119
- function extractJsonObjects(text) {
1120
- const objects = [];
1121
- let searchFrom = 0;
1122
- while (searchFrom < text.length) {
1123
- const start = text.indexOf("{", searchFrom);
1124
- if (start === -1) break;
1125
- const end = findMatchingBrace(text, start);
1126
- if (end !== -1) {
1127
- try {
1128
- objects.push(JSON.parse(text.slice(start, end + 1)));
1129
- searchFrom = end + 1;
1130
- continue;
1131
- } catch {
1132
- }
1133
- }
1134
- searchFrom = start + 1;
1135
- }
1136
- return objects;
1137
- }
1138
- function findMatchingBrace(text, start) {
1139
- let depth = 0;
1140
- let inString = false;
1141
- let escaped = false;
1142
- for (let i = start; i < text.length; i++) {
1143
- const ch = text[i];
1144
- if (inString) {
1145
- if (escaped) escaped = false;
1146
- else if (ch === "\\") escaped = true;
1147
- else if (ch === '"') inString = false;
1148
- continue;
1149
- }
1150
- if (ch === '"') inString = true;
1151
- else if (ch === "{") depth++;
1152
- else if (ch === "}") {
1153
- depth--;
1154
- if (depth === 0) return i;
1155
- }
1156
- }
1157
- return -1;
1158
- }
1159
-
1160
- // src/utils/leaderDecision.ts
1161
- function parseLeaderDecision(text) {
1162
- for (const value of extractJsonObjects(text)) {
1163
- const decision = asDecision(value);
1164
- if (decision) return decision;
1165
- }
1166
- return void 0;
1167
- }
1168
- function asDecision(value) {
1169
- if (typeof value !== "object" || value === null) return void 0;
1170
- const record = value;
1171
- const mode = record.mode;
1172
- if (mode === "reply" || mode === "ask") {
1173
- const message = typeof record.message === "string" ? record.message.trim() : "";
1174
- return message ? { mode, message } : void 0;
1175
- }
1176
- if (mode === "run" || mode === void 0) {
1177
- const run = record.run;
1178
- if (!Array.isArray(run) || !run.every((id) => typeof id === "string")) return void 0;
1179
- const reason = typeof record.reason === "string" ? record.reason.trim() : "";
1180
- const requirement = typeof record.requirement === "string" ? record.requirement.trim() : "";
1181
- if ("requirement" in record && !requirement) return void 0;
1182
- return {
1183
- mode: "run",
1184
- run,
1185
- ...reason ? { reason } : {},
1186
- ...requirement ? { requirement } : {}
1187
- };
1188
- }
1189
- return void 0;
1190
- }
1191
-
1192
- // src/utils/upstream.ts
1193
- var MAX_UPSTREAM_ENTRY_CHARS = 600;
1194
- var MAX_UPSTREAM_CHARS = 4e3;
1195
- var ELLIPSIS2 = "\u2026";
1196
- var HEADER = "\n## What upstream produced\n\n";
1197
- var TRAILER = "\n";
1198
- function block(entries) {
1199
- return `${HEADER}${entries.join("\n")}${TRAILER}`;
1200
- }
1201
- function compact(text, max) {
1202
- const trimmed = text.trim();
1203
- if (trimmed.length <= max) return trimmed;
1204
- const marker = (count) => `${ELLIPSIS2} (${count} characters elided) ${ELLIPSIS2}`;
1205
- const reserved = marker(trimmed.length).length;
1206
- const keep = Math.max(0, max - reserved);
1207
- const head = Math.ceil(keep / 2);
1208
- const tail = keep - head;
1209
- const elided = trimmed.length - keep;
1210
- return `${trimmed.slice(0, head)}${marker(elided)}${trimmed.slice(trimmed.length - tail)}`;
1211
- }
1212
- function entry2(output) {
1213
- const label = `- ${output.name} (${output.agentId}):
1214
- `;
1215
- const budget = MAX_UPSTREAM_ENTRY_CHARS - label.length;
1216
- let body = compact(output.output, budget);
1217
- const newlines = body.length - body.replace(/\n/g, "").length;
1218
- if (label.length + body.length + 2 * newlines > MAX_UPSTREAM_ENTRY_CHARS) {
1219
- body = compact(output.output, budget - 2 * newlines);
1220
- }
1221
- return `${label}${body.replace(/\n/g, "\n ")}`;
1222
- }
1223
- function renderUpstreamOutputs(outputs) {
1224
- if (outputs.length === 0) return "";
1225
- const entries = outputs.map(entry2);
1226
- while (entries.length > 1 && block(entries).length > MAX_UPSTREAM_CHARS) {
1227
- entries.shift();
1228
- }
1229
- return block(entries);
1230
- }
1231
-
1232
- // src/orchestrator/AgentRunner.ts
1233
- import * as fs3 from "fs";
1234
- import * as path4 from "path";
1235
-
1236
- // src/runtime/AcpRuntime.ts
1237
- import { spawn } from "child_process";
1238
- import { randomUUID } from "crypto";
1239
- import { Readable, Writable } from "stream";
1240
- import { StringDecoder } from "string_decoder";
1241
-
1242
- // src/providers/bridge.ts
1243
- import * as fs from "fs";
1244
- import { createRequire } from "module";
1245
- import * as path2 from "path";
1246
- var BRIDGE_PACKAGES = {
1247
- "claude-agent-acp": "@agentclientprotocol/claude-agent-acp",
1248
- "codex-acp": "@agentclientprotocol/codex-acp"
1249
- };
1250
- function resolvesOnPath(command) {
1251
- const dirs = (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean);
1252
- const exts = (process.env.PATHEXT ?? "").split(";").filter(Boolean);
1253
- const suffixes = exts.length > 0 ? exts : [""];
1254
- for (const dir of dirs) {
1255
- for (const ext of suffixes) {
1256
- try {
1257
- fs.accessSync(path2.join(dir, command + ext), fs.constants.X_OK);
1258
- return true;
1259
- } catch {
1260
- }
1261
- }
1262
- }
1263
- return false;
1264
- }
1265
- function resolveBridgeBinary(command) {
1266
- const pkg = BRIDGE_PACKAGES[command];
1267
- if (!pkg) return null;
1268
- try {
1269
- const req = createRequire(import.meta.url);
1270
- const pkgJsonPath = req.resolve(`${pkg}/package.json`);
1271
- const bin = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8")).bin;
1272
- const binRel = typeof bin === "string" ? bin : bin?.[command] ?? Object.values(bin)[0];
1273
- if (!binRel) return null;
1274
- return path2.join(path2.dirname(pkgJsonPath), binRel);
1275
- } catch {
1276
- return null;
1277
- }
1278
- }
1279
- function resolveExecutable(command) {
1280
- if (resolvesOnPath(command)) return command;
1281
- return resolveBridgeBinary(command);
1282
- }
1283
-
1284
- // src/utils/errors.ts
1285
- var PermanentError = class extends Error {
1286
- constructor(message) {
1287
- super(message);
1288
- this.name = "PermanentError";
1289
- }
1290
- };
1291
- function isPermanent(err) {
1292
- return err instanceof PermanentError;
1293
- }
1294
-
1295
- // src/runtime/RuntimeBase.ts
1296
- var RuntimeBase = class {
1297
- };
1298
-
1299
- // src/runtime/AcpRuntime.ts
1300
- var discoveredModels = /* @__PURE__ */ new Map();
1301
- var KILL_ESCALATION_MS = 3e3;
1302
- var CANCEL_GRACE_MS = 2e3;
1303
- var CLEANUP_ACK_MS = 3e3;
1304
- var STEER_ACK_MS = 15e3;
1305
- var DEFAULT_TIMEOUT_MS = 6e5;
1306
- var DEFAULT_COMMAND2 = "opencode";
1307
- var DEFAULT_ARGS = ["acp"];
1308
- var STEER_METHOD = "_session/steering";
1309
- function advertisesSteering(init) {
1310
- const steering = init._meta?.steering;
1311
- return isRecord(steering) && steering.supported === true;
1312
- }
1313
- var BASE_CAPABILITIES = {
1314
- streaming: true,
1315
- sessions: true,
1316
- resume: true,
1317
- permissions: true,
1318
- tools: true,
1319
- models: false,
1320
- images: false,
1321
- usage: true,
1322
- cost: true,
1323
- sandbox: false
1324
- };
1325
- function isRecord(value) {
1326
- return typeof value === "object" && value !== null && !Array.isArray(value);
1327
- }
1328
- function textOf(content) {
1329
- if (isRecord(content) && content.type === "text" && typeof content.text === "string") {
1330
- return content.text;
1331
- }
1332
- return void 0;
1333
- }
1334
- function stringifyOutput(value) {
1335
- if (typeof value === "string") return value;
1336
- if (value === void 0 || value === null) return "";
1337
- return JSON.stringify(value);
1338
- }
1339
- function mapStopReason(reason) {
1340
- switch (reason) {
1341
- case "end_turn":
1342
- return "success";
1343
- case "cancelled":
1344
- return "cancelled";
1345
- case "max_tokens":
1346
- case "max_turn_requests":
1347
- return "max_turns";
1348
- default:
1349
- return "error";
1350
- }
1351
- }
1352
- var KIND_TO_TOOLS = {
1353
- read: ["Read"],
1354
- edit: ["Edit", "Write"],
1355
- execute: ["Bash"],
1356
- search: ["Grep", "Glob"],
1357
- fetch: ["WebFetch"]
1358
- };
1359
- function toolPermitted(params, tools) {
1360
- const allowed = new Set(tools.map((tool) => tool.toLowerCase()));
1361
- const name = params.toolCall.name ?? void 0;
1362
- return name !== void 0 ? allowed.has(name.toLowerCase()) : (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some(
1363
- (tool) => allowed.has(tool.toLowerCase())
1364
- );
1365
- }
1366
- function decidePermission(params, tools, sessionId) {
1367
- if (sessionId !== void 0 && params.sessionId !== sessionId) {
1368
- return { response: { outcome: { outcome: "cancelled" } } };
1369
- }
1370
- const name = params.toolCall.name ?? void 0;
1371
- const kind = params.toolCall.kind ?? void 0;
1372
- const approved = toolPermitted(params, tools);
1373
- const option = params.options.find(
1374
- (o) => approved ? o.kind === "allow_once" : o.kind === "reject_once"
1375
- );
1376
- const response = option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
1377
- if (approved) return { response };
1378
- return {
1379
- response,
1380
- rejected: {
1381
- toolCallId: params.toolCall.toolCallId,
1382
- name,
1383
- kind,
1384
- title: params.toolCall.title ?? void 0
1385
- }
1386
- };
1387
- }
1388
- var FORM_ANNOTATIONS = ["title", "description", "default", "_meta"];
1389
- function knownFormKeys(value, keys) {
1390
- return Object.keys(value).every((key) => keys.includes(key) || FORM_ANNOTATIONS.includes(key));
1391
- }
1392
- function validFormBound(value, integer = false) {
1393
- return value == null || typeof value === "number" && Number.isFinite(value) && (!integer || Number.isInteger(value) && value >= 0);
1394
- }
1395
- function titledChoices(value) {
1396
- return Array.isArray(value) && value.length > 0 && value.every(
1397
- (option) => isRecord(option) && knownFormKeys(option, ["const"]) && typeof option.const === "string"
1398
- );
1399
- }
1400
- function supportedQuestionSchema(schema) {
1401
- if (!isRecord(schema) || schema.type !== void 0 && schema.type !== "object" || !knownFormKeys(schema, ["type", "properties", "required"]))
1402
- return false;
1403
- const properties = schema.properties ?? {};
1404
- if (!isRecord(properties)) return false;
1405
- if (schema.required != null && (!Array.isArray(schema.required) || !schema.required.every(
1406
- (key) => typeof key === "string" && Object.getOwnPropertyDescriptor(properties, key) !== void 0
1407
- )))
1408
- return false;
1409
- return Object.values(properties).every((field) => {
1410
- if (!isRecord(field)) return false;
1411
- if (field.type === "string") {
1412
- return knownFormKeys(field, ["type", "enum", "oneOf", "minLength", "maxLength"]) && validFormBound(field.minLength, true) && validFormBound(field.maxLength, true) && (field.enum == null || Array.isArray(field.enum) && field.enum.length > 0 && field.enum.every((value) => typeof value === "string")) && (field.oneOf == null || titledChoices(field.oneOf));
1413
- }
1414
- if (field.type === "boolean") return knownFormKeys(field, ["type"]);
1415
- if (field.type === "number" || field.type === "integer") {
1416
- return knownFormKeys(field, ["type", "minimum", "maximum"]) && validFormBound(field.minimum) && validFormBound(field.maximum);
1417
- }
1418
- if (field.type !== "array" || !isRecord(field.items) || !knownFormKeys(field, ["type", "items", "minItems", "maxItems"]) || !validFormBound(field.minItems, true) || !validFormBound(field.maxItems, true))
1419
- return false;
1420
- return knownFormKeys(field.items, ["type", "enum"]) && field.items.type === "string" && Array.isArray(field.items.enum) && field.items.enum.length > 0 && field.items.enum.every((value) => typeof value === "string") || knownFormKeys(field.items, ["anyOf"]) && field.items.type === void 0 && titledChoices(field.items.anyOf);
1421
- });
1422
- }
1423
- function validateRuntimeQuestionContent(schema, content) {
1424
- if (!supportedQuestionSchema(schema) || !isRecord(content)) return false;
1425
- if (Object.getOwnPropertySymbols(content).length > 0 || Object.getOwnPropertyNames(content).some((key) => {
1426
- const descriptor = Object.getOwnPropertyDescriptor(content, key);
1427
- return !descriptor?.enumerable || !("value" in descriptor);
1428
- }))
1429
- return false;
1430
- const properties = schema.properties ?? {};
1431
- if ((schema.required ?? []).some(
1432
- (key) => Object.getOwnPropertyDescriptor(content, key) === void 0
1433
- ))
1434
- return false;
1435
- return Object.entries(content).every(([key, value]) => {
1436
- if (Object.getOwnPropertyDescriptor(properties, key) === void 0) return false;
1437
- const field = properties[key];
1438
- if (field.type === "boolean") return typeof value === "boolean";
1439
- if (field.type === "number" || field.type === "integer") {
1440
- return typeof value === "number" && Number.isFinite(value) && (field.type !== "integer" || Number.isInteger(value)) && (typeof field.minimum !== "number" || value >= field.minimum) && (typeof field.maximum !== "number" || value <= field.maximum);
1441
- }
1442
- if (field.type === "string") {
1443
- return typeof value === "string" && (typeof field.minLength !== "number" || [...value].length >= field.minLength) && (typeof field.maxLength !== "number" || [...value].length <= field.maxLength) && (!Array.isArray(field.enum) || field.enum.includes(value)) && (!Array.isArray(field.oneOf) || field.oneOf.some((option) => isRecord(option) && option.const === value));
1444
- }
1445
- const items = field.items;
1446
- const choices = Array.isArray(items.enum) ? items.enum : items.anyOf.map((option) => option.const);
1447
- return Array.isArray(value) && value.every((item) => typeof item === "string" && choices.includes(item)) && new Set(value).size === value.length && (typeof field.minItems !== "number" || value.length >= field.minItems) && (typeof field.maxItems !== "number" || value.length <= field.maxItems);
1448
- });
1449
- }
1450
- function withAuthHint(acp, err) {
1451
- const error = err instanceof Error ? err : new Error(String(err));
1452
- if (error instanceof acp.RequestError && error.code === acp.RequestError.authRequired().code) {
1453
- return new Error(`${error.message} \u2014 run \`claude\` to log in`);
1454
- }
1455
- return error;
1456
- }
1457
- function errorMessage(err) {
1458
- return err instanceof Error ? err.message : String(err);
1459
- }
1460
- function timeoutMs(value) {
1461
- const timeout = value ?? DEFAULT_TIMEOUT_MS;
1462
- if (!Number.isFinite(timeout) || timeout < 0 || timeout > 2147483647) {
1463
- throw new PermanentError("ACP timeout must be a finite non-negative timer duration");
1464
- }
1465
- return timeout;
1466
- }
1467
- var AcpRuntime = class extends RuntimeBase {
1468
- constructor() {
1469
- super(...arguments);
1470
- this.name = "acp";
1471
- }
1472
- async describe(options) {
1473
- const command = options?.command ?? DEFAULT_COMMAND2;
1474
- return {
1475
- name: this.name,
1476
- available: resolveExecutable(command) !== null,
1477
- capabilities: {
1478
- ...BASE_CAPABILITIES,
1479
- tools: specForCommand(command)?.tools ?? false,
1480
- models: specForCommand(command)?.modelSelection === "config-option"
1481
- }
1482
- };
1483
- }
1484
- async execute(task) {
1485
- const { prompt, onEvent, signal, permissionMode, onPermission, onQuestion, ...options } = task;
1486
- const startup = new AbortController();
1487
- const forward = () => startup.abort();
1488
- if (signal?.aborted) startup.abort();
1489
- signal?.addEventListener("abort", forward, { once: true });
1490
- let session;
1491
- try {
1492
- session = await this.openSession({
1493
- ...options,
1494
- questionForms: Boolean(onQuestion),
1495
- signal: startup.signal
1496
- });
1497
- } finally {
1498
- signal?.removeEventListener("abort", forward);
1499
- }
1500
- try {
1501
- return await session.prompt(prompt, {
1502
- signal,
1503
- timeout: task.timeout,
1504
- onEvent,
1505
- permissionMode,
1506
- onPermission,
1507
- onQuestion
1508
- });
1509
- } finally {
1510
- await session.close();
1511
- }
1512
- }
1513
- async openSession(options) {
1514
- if (options.signal?.aborted) throw abortError();
1515
- if (options.resume === "fork") {
1516
- throw new PermanentError("ACP does not support forked sessions");
1517
- }
1518
- const defaultTimeout = timeoutMs(options.timeout);
1519
- const questionForms = options.questionForms === true;
1520
- let startupReject;
1521
- const startupFailure = new Promise((_, reject) => {
1522
- startupReject = reject;
1523
- });
1524
- let failSession;
1525
- const failStartup = (error) => {
1526
- startupReject(error);
1527
- failSession?.(error);
1528
- };
1529
- const onSessionAbort = () => failStartup(abortError());
1530
- options.signal?.addEventListener("abort", onSessionAbort, { once: true });
1531
- const startupTimer = setTimeout(
1532
- () => failStartup(new Error(`acp startup timed out (${defaultTimeout}ms)`)),
1533
- defaultTimeout
1534
- );
1535
- let cleanup;
1536
- try {
1537
- const acp = await Promise.race([import("@agentclientprotocol/sdk"), startupFailure]);
1538
- if (options.signal?.aborted) throw abortError();
1539
- const command = options.command ?? DEFAULT_COMMAND2;
1540
- const spec = specForCommand(command);
1541
- const meta = spec?.meta?.({ ...options, prompt: "", timeout: defaultTimeout });
1542
- const child = spawn(resolveExecutable(command) ?? command, options.args ?? DEFAULT_ARGS, {
1543
- cwd: options.workDir,
1544
- env: process.env,
1545
- detached: true
1546
- });
1547
- let state = "idle";
1548
- let sessionId = "";
1549
- let steeringSupported = false;
1550
- let active;
1551
- let childClosed = false;
1552
- let terminalError;
1553
- let errorOutput = "";
1554
- let escalation;
1555
- let acknowledgement;
1556
- let closePromise;
1557
- let closeResolve;
1558
- let closeReject;
1559
- let connection;
1560
- const killTree = (signal) => {
1561
- if (child.pid !== void 0) {
1562
- try {
1563
- process.kill(-child.pid, signal);
1564
- return;
1565
- } catch {
1566
- }
1567
- }
1568
- try {
1569
- child.kill(signal);
1570
- } catch {
1571
- }
1572
- };
1573
- const emitForTurn = (turn, event) => {
1574
- turn.options.onEvent?.({
1575
- ...event,
1576
- runId: turn.runId,
1577
- agentId: options.agentId,
1578
- seq: turn.seq++
1579
- });
1580
- };
1581
- const emit = (event) => {
1582
- if (active) emitForTurn(active, event);
1583
- };
1584
- const settleTurn = (turn, output, error) => {
1585
- if (active !== turn) return;
1586
- turn.decisions.abort();
1587
- if (turn.timer) clearTimeout(turn.timer);
1588
- if (turn.cancelTimer) clearTimeout(turn.cancelTimer);
1589
- if (turn.onAbort) turn.options.signal?.removeEventListener("abort", turn.onAbort);
1590
- active = void 0;
1591
- if (state === "running" || state === "cancelling") state = "idle";
1592
- if (error) {
1593
- if (closePromise) {
1594
- void closePromise.then(
1595
- () => turn.reject(error),
1596
- (cleanupError) => turn.reject(cleanupError)
1597
- );
1598
- } else turn.reject(error);
1599
- } else if (output) turn.resolve(output);
1600
- };
1601
- const clearCleanupTimers = () => {
1602
- if (escalation) clearTimeout(escalation);
1603
- if (acknowledgement) clearTimeout(acknowledgement);
1604
- };
1605
- const close = () => {
1606
- if (closePromise) return closePromise;
1607
- closePromise = new Promise((resolve4, reject) => {
1608
- closeResolve = resolve4;
1609
- closeReject = reject;
1610
- });
1611
- options.signal?.removeEventListener("abort", onSessionAbort);
1612
- clearTimeout(startupTimer);
1613
- state = "closing";
1614
- if (active) settleTurn(active, void 0, active.error ?? terminalError ?? abortError());
1615
- if (childClosed) {
1616
- state = "closed";
1617
- closeResolve?.();
1618
- return closePromise;
1619
- }
1620
- escalation = setTimeout(() => {
1621
- killTree("SIGKILL");
1622
- if (childClosed) return;
1623
- acknowledgement = setTimeout(() => {
1624
- state = "closed";
1625
- clearCleanupTimers();
1626
- closeReject?.(new Error(`${command} cleanup failed: no child close after SIGKILL`));
1627
- }, CLEANUP_ACK_MS);
1628
- }, KILL_ESCALATION_MS);
1629
- try {
1630
- connection?.close();
1631
- } catch {
579
+ connection?.close();
580
+ } catch {
1632
581
  }
1633
582
  killTree("SIGTERM");
1634
583
  return closePromise;
@@ -1882,249 +831,1432 @@ var AcpRuntime = class extends RuntimeBase {
1882
831
  });
1883
832
  return turn.cancellation;
1884
833
  };
1885
- const supplement = async (text, supplementOptions = {}) => {
1886
- if (!steeringSupported) {
1887
- throw new Error(`${command} does not accept mid-turn input`);
834
+ const supplement = async (text, supplementOptions = {}) => {
835
+ if (!steeringSupported) {
836
+ throw new Error(`${command} does not accept mid-turn input`);
837
+ }
838
+ const turn = active;
839
+ if (!turn || state !== "running") return { status: "idle" };
840
+ if (supplementOptions.signal?.aborted) throw abortError();
841
+ let timer;
842
+ try {
843
+ const response = await Promise.race([
844
+ ctx.request(STEER_METHOD, {
845
+ sessionId,
846
+ prompt: [{ type: "text", text }],
847
+ _meta: { steering: { idleBehavior: "promptRequired" } }
848
+ }),
849
+ new Promise((_, reject) => {
850
+ timer = setTimeout(
851
+ () => reject(new Error(`steering was not acknowledged within ${STEER_ACK_MS}ms`)),
852
+ STEER_ACK_MS
853
+ );
854
+ })
855
+ ]);
856
+ if (active !== turn) return { status: "idle" };
857
+ const outcome = response?.outcome;
858
+ if (outcome === "injected") return { status: "injected" };
859
+ if (outcome === "promptRequired") return { status: "idle" };
860
+ if (outcome === "startedNewTurn") {
861
+ await Promise.resolve().then(() => ctx.notify(acp.methods.agent.session.cancel, { sessionId })).catch(() => {
862
+ });
863
+ return { status: "idle" };
864
+ }
865
+ if (outcome === "failed") {
866
+ throw new Error("the agent could not apply the mid-turn message");
867
+ }
868
+ throw new Error(`unexpected steering outcome: ${JSON.stringify(outcome ?? null)}`);
869
+ } finally {
870
+ if (timer) clearTimeout(timer);
871
+ }
872
+ };
873
+ const prompt = (text, promptOptions = {}) => {
874
+ if (state !== "idle") {
875
+ return Promise.reject(new Error(`Cannot prompt ACP session while ${state}`));
876
+ }
877
+ if (options.signal?.aborted || promptOptions.signal?.aborted) {
878
+ return Promise.reject(abortError());
879
+ }
880
+ let duration;
881
+ try {
882
+ duration = timeoutMs(promptOptions.timeout ?? defaultTimeout);
883
+ } catch (error) {
884
+ return Promise.reject(error);
885
+ }
886
+ let resolve4;
887
+ let reject;
888
+ const promise = new Promise((res, rej) => {
889
+ resolve4 = res;
890
+ reject = rej;
891
+ });
892
+ const turn = {
893
+ runId: randomUUID(),
894
+ seq: 0,
895
+ options: promptOptions,
896
+ output: "",
897
+ decisions: new AbortController(),
898
+ promise,
899
+ resolve: resolve4,
900
+ reject
901
+ };
902
+ active = turn;
903
+ state = "running";
904
+ turn.onAbort = () => {
905
+ turn.error = abortError();
906
+ void cancelTurn().catch(() => {
907
+ });
908
+ };
909
+ promptOptions.signal?.addEventListener("abort", turn.onAbort, { once: true });
910
+ turn.timer = setTimeout(() => {
911
+ turn.error = new Error(`acp timed out (${duration}ms)`);
912
+ fail(turn.error);
913
+ }, duration);
914
+ const run = async () => {
915
+ emit({ type: "run_started", model: options.model });
916
+ if (active !== turn) return;
917
+ const response = await ctx.request(acp.methods.agent.session.prompt, {
918
+ sessionId,
919
+ prompt: [
920
+ {
921
+ type: "text",
922
+ text: meta === void 0 && options.systemPrompt ? `${options.systemPrompt}
923
+
924
+ ---
925
+
926
+ ${text}` : text
927
+ }
928
+ ]
929
+ });
930
+ if (active !== turn) return;
931
+ if (turn.error) {
932
+ settleTurn(turn, void 0, turn.error);
933
+ return;
934
+ }
935
+ const stopReason = mapStopReason(response.stopReason);
936
+ settleTurn(turn, {
937
+ output: turn.output,
938
+ exitCode: stopReason === "success" ? 0 : 1,
939
+ sessionId,
940
+ stopReason,
941
+ ...turn.usage ? { usage: turn.usage } : {},
942
+ ...turn.cost?.currency.toLowerCase() === "usd" ? { costUsd: turn.cost.amount } : {}
943
+ });
944
+ emitForTurn(turn, { type: "run_finished", stopReason });
945
+ };
946
+ void run().catch((error) => {
947
+ if (active === turn) fail(withAuthHint(acp, error));
948
+ });
949
+ return promise;
950
+ };
951
+ const session = {
952
+ get sessionId() {
953
+ return sessionId;
954
+ },
955
+ get state() {
956
+ return state;
957
+ },
958
+ get capabilities() {
959
+ return { continuation: true, supplement: steeringSupported };
960
+ },
961
+ prompt,
962
+ supplement,
963
+ cancelTurn,
964
+ close
965
+ };
966
+ const warn = (message) => {
967
+ if (options.onWarning) options.onWarning(message);
968
+ else process.stderr.write(`[multiclaw] Warning: ${message}
969
+ `);
970
+ };
971
+ const initialize = async () => {
972
+ const init = await ctx.request(acp.methods.agent.initialize, {
973
+ protocolVersion: acp.PROTOCOL_VERSION,
974
+ ...questionForms ? { clientCapabilities: { elicitation: { form: {} } } } : {}
975
+ });
976
+ if (terminalError) throw terminalError;
977
+ steeringSupported = advertisesSteering(init);
978
+ if (options.sessionId) {
979
+ if (init.agentCapabilities?.loadSession !== true) {
980
+ throw new PermanentError(
981
+ `Agent does not support session/load (loadSession=false); cannot resume session ${options.sessionId}`
982
+ );
983
+ }
984
+ await ctx.request(acp.methods.agent.session.load, {
985
+ sessionId: options.sessionId,
986
+ cwd: options.workDir,
987
+ mcpServers: [],
988
+ ...meta ? { _meta: meta } : {}
989
+ });
990
+ sessionId = options.sessionId;
991
+ } else {
992
+ const created = await ctx.request(acp.methods.agent.session.new, {
993
+ cwd: options.workDir,
994
+ mcpServers: [],
995
+ ...meta ? { _meta: meta } : {}
996
+ });
997
+ sessionId = created.sessionId;
998
+ const modelOpt = created.configOptions?.find(
999
+ (option) => option.category === "model" || option.id === "model"
1000
+ );
1001
+ if (modelOpt && "options" in modelOpt && Array.isArray(modelOpt.options)) {
1002
+ discoveredModels.set(
1003
+ command,
1004
+ (modelOpt.options ?? []).map(
1005
+ (option) => option.value ?? String(option)
1006
+ )
1007
+ );
1008
+ }
1888
1009
  }
1889
- const turn = active;
1890
- if (!turn || state !== "running") return { status: "idle" };
1891
- if (supplementOptions.signal?.aborted) throw abortError();
1892
- let timer;
1893
- try {
1894
- const response = await Promise.race([
1895
- ctx.request(STEER_METHOD, {
1896
- sessionId,
1897
- prompt: [{ type: "text", text }],
1898
- _meta: { steering: { idleBehavior: "promptRequired" } }
1899
- }),
1900
- new Promise((_, reject) => {
1901
- timer = setTimeout(
1902
- () => reject(new Error(`steering was not acknowledged within ${STEER_ACK_MS}ms`)),
1903
- STEER_ACK_MS
1010
+ if (terminalError) throw terminalError;
1011
+ if (options.model && spec?.modelSelection === "config-option") {
1012
+ const known = discoveredModels.get(command);
1013
+ if (known && !known.includes(options.model)) {
1014
+ warn(
1015
+ `model "${options.model}" is not offered by ${command}; running with the provider's default.`
1016
+ );
1017
+ } else {
1018
+ try {
1019
+ await ctx.request(acp.methods.agent.session.setConfigOption, {
1020
+ sessionId,
1021
+ configId: "model",
1022
+ value: options.model
1023
+ });
1024
+ } catch (error) {
1025
+ if (terminalError) throw terminalError;
1026
+ warn(
1027
+ `Could not set model "${options.model}" on ${command}; running with the provider's default (${errorMessage(error)}).`
1904
1028
  );
1905
- })
1906
- ]);
1907
- if (active !== turn) return { status: "idle" };
1908
- const outcome = response?.outcome;
1909
- if (outcome === "injected") return { status: "injected" };
1910
- if (outcome === "promptRequired") return { status: "idle" };
1911
- if (outcome === "startedNewTurn") {
1912
- await Promise.resolve().then(() => ctx.notify(acp.methods.agent.session.cancel, { sessionId })).catch(() => {
1913
- });
1914
- return { status: "idle" };
1915
- }
1916
- if (outcome === "failed") {
1917
- throw new Error("the agent could not apply the mid-turn message");
1029
+ }
1918
1030
  }
1919
- throw new Error(`unexpected steering outcome: ${JSON.stringify(outcome ?? null)}`);
1920
- } finally {
1921
- if (timer) clearTimeout(timer);
1922
1031
  }
1032
+ if (terminalError) throw terminalError;
1033
+ if (options.signal?.aborted) throw abortError();
1034
+ };
1035
+ await Promise.race([
1036
+ initialize().catch((error) => {
1037
+ throw withAuthHint(acp, error);
1038
+ }),
1039
+ startupFailure
1040
+ ]);
1041
+ clearTimeout(startupTimer);
1042
+ return session;
1043
+ } catch (error) {
1044
+ clearTimeout(startupTimer);
1045
+ options.signal?.removeEventListener("abort", onSessionAbort);
1046
+ if (cleanup) await cleanup();
1047
+ throw error;
1048
+ }
1049
+ }
1050
+ };
1051
+
1052
+ // src/runtime/index.ts
1053
+ var RuntimeRegistry = class {
1054
+ constructor() {
1055
+ this.runtimes = /* @__PURE__ */ new Map();
1056
+ this.register(new AcpRuntime());
1057
+ }
1058
+ register(r) {
1059
+ this.runtimes.set(r.name, r);
1060
+ }
1061
+ get(name) {
1062
+ const r = this.runtimes.get(name);
1063
+ if (!r)
1064
+ throw new Error(
1065
+ `Runtime not found: "${name}". Available: ${[...this.runtimes.keys()].join(", ")}`
1066
+ );
1067
+ return r;
1068
+ }
1069
+ has(name) {
1070
+ return this.runtimes.has(name);
1071
+ }
1072
+ list() {
1073
+ return [...this.runtimes.keys()];
1074
+ }
1075
+ };
1076
+ var runtimeRegistry = new RuntimeRegistry();
1077
+
1078
+ // src/config/toolsPolicy.ts
1079
+ var TOOLS_PROVENANCE = /* @__PURE__ */ Symbol.for("multiclaw.toolsProvenance");
1080
+ function markPresetTools(agent, overrides) {
1081
+ const provenance = overrides?.tools !== void 0 ? { explicit: true } : {
1082
+ explicit: false,
1083
+ reference: agent.tools,
1084
+ snapshot: Object.freeze([...agent.tools ?? []])
1085
+ };
1086
+ Object.defineProperty(agent, TOOLS_PROVENANCE, {
1087
+ value: Object.freeze(provenance),
1088
+ enumerable: true
1089
+ });
1090
+ return agent;
1091
+ }
1092
+ function hasExplicitTools(agent) {
1093
+ if (agent.tools === void 0) return false;
1094
+ const provenance = agent[TOOLS_PROVENANCE];
1095
+ if (!provenance || provenance.explicit) return true;
1096
+ return agent.tools !== provenance.reference || agent.tools.length !== provenance.snapshot.length || agent.tools.some((tool, index) => tool !== provenance.snapshot[index]);
1097
+ }
1098
+ async function validateToolsPolicy(config, options = {}) {
1099
+ const { signal } = options;
1100
+ const checkAbort = () => {
1101
+ if (signal?.aborted) throw abortError();
1102
+ };
1103
+ checkAbort();
1104
+ const agents2 = [
1105
+ ...config.leader && config.useLeader !== false ? [config.leader] : [],
1106
+ ...config.agents
1107
+ ];
1108
+ const descriptions = /* @__PURE__ */ new Map();
1109
+ for (const agent of agents2) {
1110
+ checkAbort();
1111
+ if (!hasExplicitTools(agent)) continue;
1112
+ const runtime = runtimeRegistry.get(agent.runtime ?? "acp");
1113
+ const command = resolveCommand(agent, config);
1114
+ let commands = descriptions.get(runtime);
1115
+ if (!commands) {
1116
+ commands = /* @__PURE__ */ new Map();
1117
+ descriptions.set(runtime, commands);
1118
+ }
1119
+ let info = commands.get(command);
1120
+ if (!info) {
1121
+ let onAbort;
1122
+ const cancelled = new Promise((_, reject) => {
1123
+ if (!signal) return;
1124
+ onAbort = () => reject(abortError());
1125
+ signal.addEventListener("abort", onAbort, { once: true });
1126
+ });
1127
+ try {
1128
+ info = await Promise.race([runtime.describe({ command }), cancelled]);
1129
+ } catch (error) {
1130
+ checkAbort();
1131
+ throw error;
1132
+ } finally {
1133
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
1134
+ }
1135
+ checkAbort();
1136
+ commands.set(command, info);
1137
+ }
1138
+ if (!info.capabilities.tools) {
1139
+ throw new Error(
1140
+ `Agent "${agent.name}" (${agent.id}), runtime "${runtime.name}", resolved command "${command}" cannot enforce explicit tools. Select a provider that supports tools restrictions or remove the explicit tools setting; the policy will not be relaxed.`
1141
+ );
1142
+ }
1143
+ }
1144
+ checkAbort();
1145
+ }
1146
+
1147
+ // src/agents/arch-design-reviewer.ts
1148
+ var SYSTEM = `You are an architecture design reviewer. Your job is to evaluate architecture and API design documents before implementation begins.
1149
+
1150
+ Responsibilities:
1151
+ - Review architecture design for completeness, consistency, and technical soundness
1152
+ - Review API specifications for clarity, correctness, and REST/contract conventions
1153
+ - Identify ambiguities, missing edge cases, or risky design decisions
1154
+ - Do not write code, do not implement \u2014 provide only design-level feedback
1155
+
1156
+ Deliverables (save to the working directory):
1157
+ - arch-review.md \u2014 verdict (Approved / Changes Required), issue list (blocking / advisory) with specific locations and suggested fixes, overall assessment
1158
+
1159
+ Principles: Be precise \u2014 point to specific sections. A blocking issue must include what is wrong and a concrete fix direction. If the design is sound, say so directly. Do not approve with vague praise.`;
1160
+ var TASK = `## Architecture Design
1161
+ {{file:architecture.md}}
1162
+
1163
+ ## API Specification
1164
+ {{file:api-spec.md}}
1165
+ {{upstreamOutputs}}
1166
+
1167
+ ## Task
1168
+ Review the architecture design and API specification above. Output arch-review.md:
1169
+ 1. Overall verdict: Approved / Changes Required
1170
+ 2. Blocking issues (must be fixed before implementation): each with location, problem, and fix direction
1171
+ 3. Advisory issues (should fix but not blocking): each with location and suggestion
1172
+ 4. Design strengths worth noting
1173
+ 5. Final summary (1\u20132 sentences)
1174
+
1175
+ Be specific. Reference section names or endpoint paths when citing issues.`;
1176
+ function archDesignReviewer(overrides) {
1177
+ return markPresetTools(
1178
+ {
1179
+ id: "arch-design-reviewer",
1180
+ name: "Arch Design Reviewer",
1181
+ icon: "\u25B3",
1182
+ description: "reviews the design",
1183
+ runtime: "acp",
1184
+ models: { acp: "haiku" },
1185
+ modelTier: "fast",
1186
+ systemPrompt: SYSTEM,
1187
+ taskPrompt: TASK,
1188
+ tools: ["Read", "Write"],
1189
+ ...overrides
1190
+ },
1191
+ overrides
1192
+ );
1193
+ }
1194
+
1195
+ // src/agents/architect.ts
1196
+ var SYSTEM2 = `You are the technical architect for this project. Your responsibility is technical analysis and design \u2014 you do not write feature code.
1197
+
1198
+ Responsibilities:
1199
+ - Understand requirements, identify key technical risks and affected components
1200
+ - Review existing code (if any) and propose minimal-change solutions
1201
+ - Define module boundaries, interface contracts, and directory structure
1202
+ - Provide a clear design foundation for developers and testers
1203
+ - Decompose the work into per-agent subtasks based on who is actually in the pipeline
1204
+ - For each subtask, write a step-by-step plan concrete enough that the agent can follow it without re-analyzing the full requirement
1205
+
1206
+ Deliverables (save to the working directory):
1207
+ - architecture.md \u2014 architecture plan (module breakdown, directory structure, technology choices, key component descriptions)
1208
+ - api-spec.md \u2014 complete API specification (paths, methods, request/response formats, status codes, error handling)
1209
+ - task-plan.json \u2014 structured task breakdown for all implementation agents in the pipeline (see format below)
1210
+
1211
+ task-plan.json format:
1212
+ {
1213
+ "projectName": "<project name>",
1214
+ "requirement": "<one-line summary>",
1215
+ "tasks": [
1216
+ {
1217
+ "id": "<agent id exactly as listed in pipelineAgents>",
1218
+ "title": "<short task title>",
1219
+ "scope": "<concrete description of what this agent should build or produce>",
1220
+ "plan": [
1221
+ "Step 1: ...",
1222
+ "Step 2: ...",
1223
+ "Step 3: ..."
1224
+ ],
1225
+ "dependsOn": ["<other agent id>"]
1226
+ }
1227
+ ]
1228
+ }
1229
+
1230
+ Rules for task-plan.json:
1231
+ - Only create tasks for implementation agents (those that produce code, designs, or infrastructure). Skip review agents (code-reviewer, arch-design-reviewer, ui-design-reviewer, reviewer) and planning agents (product-manager, architect, leader).
1232
+ - Use the exact agent id from pipelineAgents as the task id.
1233
+ - If an agent is not in pipelineAgents, do not create a task for it.
1234
+ - dependsOn references other task ids, not agent names.
1235
+
1236
+ Principles: Execute tasks directly without asking for confirmation. If information is missing, mark the file with BLOCKED and describe what is needed. Do not change the requirement scope. Do not write business feature code. Do not refactor beyond the task scope.`;
1237
+ var TASK2 = `## Project
1238
+ {{projectName}}
1239
+
1240
+ ## Requirement
1241
+ {{requirement}}
1242
+
1243
+ ## Pipeline Agents
1244
+ The following agents will run in this pipeline (id: name):
1245
+ {{pipelineAgents}}
1246
+ {{upstreamOutputs}}
1247
+
1248
+ ## Task
1249
+ Analyze the requirement and create three files in the current directory:
1250
+ 1. architecture.md \u2014 architecture plan (module breakdown, directory structure, technology choices, key component descriptions, affected scope)
1251
+ 2. api-spec.md \u2014 complete API specification (paths, methods, request parameters, response formats, status codes, error handling)
1252
+ 3. task-plan.json \u2014 task breakdown for each implementation agent listed in the pipeline above
1253
+
1254
+ For task-plan.json: create one task per implementation agent (skip reviewers and planners). Each task must have:
1255
+ - scope: concrete description of what this agent should build
1256
+ - plan: ordered list of concrete steps, specific enough that the agent can follow them without re-analyzing the full requirement (e.g. "Create src/models/user.ts with User and UserProfile interfaces", "Implement POST /users endpoint in src/routes/users.ts")
1257
+ Set dependsOn to reflect actual execution order constraints (e.g. devops depends on backend and frontend).
1258
+
1259
+ Create the files directly. No extra output.`;
1260
+ function architect(overrides) {
1261
+ return markPresetTools(
1262
+ {
1263
+ id: "architect",
1264
+ name: "Architect",
1265
+ icon: "\u25C6",
1266
+ description: "designs the architecture",
1267
+ role: "planner",
1268
+ runtime: "acp",
1269
+ models: { acp: "opus" },
1270
+ modelTier: "strong",
1271
+ systemPrompt: SYSTEM2,
1272
+ taskPrompt: TASK2,
1273
+ tools: ["Read", "Write"],
1274
+ ...overrides
1275
+ },
1276
+ overrides
1277
+ );
1278
+ }
1279
+
1280
+ // src/agents/backend-developer.ts
1281
+ var SYSTEM3 = `You are the backend engineer. You own the API contract and all server-side logic.
1282
+
1283
+ Responsibilities:
1284
+ - Read the architecture design and API specification, implement server-side logic and data models
1285
+ - Write backend tests (unit tests / integration tests)
1286
+ - Run validation commands (compile, lint, test) and record the results
1287
+
1288
+ Deliverables (write code to the working directory, and record in implementation-notes.md):
1289
+ - List of files created or modified
1290
+ - Commands actually executed and their output
1291
+ - Known issues and risks
1292
+
1293
+ Principles: Write code directly without asking for confirmation. Follow the API spec strictly \u2014 do not change the contract unilaterally; escalate contract issues upstream. Do not refactor unrelated code. Run commands and report evidence.`;
1294
+ var TASK3 = `## Task Scope
1295
+ {{agentScope}}
1296
+
1297
+ ## Plan
1298
+ {{agentPlan}}
1299
+ {{upstreamOutputs}}
1300
+
1301
+ ---
1302
+
1303
+ ## Architecture Design
1304
+ {{file:architecture.md}}
1305
+
1306
+ ## API Specification
1307
+ {{file:api-spec.md}}
1308
+
1309
+ ## Task
1310
+ Implement complete backend code following the architecture design and API specification:
1311
+ 1. Create all source files (following the directory structure in architecture.md)
1312
+ 2. Implement all API endpoints (strictly following api-spec.md)
1313
+ 3. Write basic tests
1314
+ 4. Run compile/test commands to verify (e.g. npx tsc --noEmit, npm test)
1315
+ 5. Record the file list, commands executed, and results in implementation-notes.md
1316
+
1317
+ Start immediately. Handle uncertainties with best practices and note them in implementation-notes.md.`;
1318
+ function backendDeveloper(overrides) {
1319
+ return markPresetTools(
1320
+ {
1321
+ id: "backend-developer",
1322
+ name: "Backend Developer",
1323
+ icon: "\u2295",
1324
+ description: "builds the API",
1325
+ runtime: "acp",
1326
+ models: { acp: "sonnet" },
1327
+ modelTier: "balanced",
1328
+ systemPrompt: SYSTEM3,
1329
+ taskPrompt: TASK3,
1330
+ tools: ["Read", "Write", "Bash"],
1331
+ ...overrides
1332
+ },
1333
+ overrides
1334
+ );
1335
+ }
1336
+
1337
+ // src/agents/code-reviewer.ts
1338
+ var SYSTEM4 = `You are a code review expert focused on implementation quality, correctness, and security.
1339
+
1340
+ Responsibilities:
1341
+ - Read the requirement, architecture design, and all implementation code
1342
+ - Verify the implementation matches the API specification and satisfies the requirement
1343
+ - Identify bugs, security vulnerabilities, error handling gaps, and test coverage issues
1344
+ - Assess code quality: readability, naming, separation of concerns, duplication
1345
+ - Do not run code, do not modify code \u2014 provide written judgment only
1346
+
1347
+ Deliverables (save to the working directory):
1348
+ - code-review.md \u2014 score (1\u201310), blocking issues, advisory issues, conclusion (Approved / Changes Required)
1349
+ - When the task includes a Delivery acceptance review contract, the final response must be only its JSON verdict, not the prose report. Approve only with no unresolved blocking issues; include file locations and fix directions in changes_required issues. The verdict is a review judgment, not proof that unexecuted tests passed.
1350
+
1351
+ Principles: Judge the code, not the developer. A blocking issue must include the file path, the specific problem, and a fix direction. Advisory items should be actionable, not vague. If the code is solid, say so.`;
1352
+ var TASK4 = `## Project
1353
+ {{projectName}}
1354
+
1355
+ ## Requirement
1356
+ {{requirement}}
1357
+
1358
+ ## Architecture Design
1359
+ {{file:architecture.md}}
1360
+
1361
+ ## API Specification
1362
+ {{file:api-spec.md}}
1363
+ {{upstreamOutputs}}
1364
+
1365
+ ## Task
1366
+ Perform a thorough code review of all implementation in the current directory and output code-review.md:
1367
+ 1. Overall score (1\u201310) and verdict: Approved / Changes Required
1368
+ 2. Blocking issues (bugs, security holes, missing requirement coverage, broken tests):
1369
+ - file path + line range, problem description, fix direction
1370
+ 3. Advisory issues (code quality, naming, duplication, missing error handling):
1371
+ - file path, observation, suggestion
1372
+ 4. Test coverage assessment
1373
+ 5. Final summary (2\u20133 sentences)
1374
+
1375
+ Start by reading implementation-notes.md (if present), then read source and test files.`;
1376
+ function codeReviewer(overrides) {
1377
+ return markPresetTools(
1378
+ {
1379
+ id: "code-reviewer",
1380
+ name: "Code Reviewer",
1381
+ icon: "\u2297",
1382
+ description: "reviews the code",
1383
+ runtime: "acp",
1384
+ models: { acp: "haiku" },
1385
+ modelTier: "fast",
1386
+ systemPrompt: SYSTEM4,
1387
+ taskPrompt: TASK4,
1388
+ tools: ["Read", "Write"],
1389
+ ...overrides
1390
+ },
1391
+ overrides
1392
+ );
1393
+ }
1394
+
1395
+ // src/agents/devops.ts
1396
+ var SYSTEM5 = `You are the DevOps engineer. Your job is to containerize the application, set up CI/CD pipelines, and produce deployment-ready infrastructure configuration.
1397
+
1398
+ Responsibilities:
1399
+ - Read the architecture design and implementation to understand the application structure
1400
+ - Write a Dockerfile (and docker-compose.yml if multiple services are involved)
1401
+ - Create a CI/CD pipeline configuration (GitHub Actions, GitLab CI, or similar based on the architecture)
1402
+ - Write any environment configuration templates (.env.example)
1403
+ - Document the deployment procedure and environment requirements
1404
+ - Do not modify application source code
1405
+
1406
+ Deliverables (save to the working directory):
1407
+ - Dockerfile \u2014 production-ready container image definition
1408
+ - docker-compose.yml \u2014 local development / staging orchestration (if multiple services)
1409
+ - .github/workflows/ci.yml (or equivalent) \u2014 CI pipeline: build, test, lint
1410
+ - .env.example \u2014 required environment variables with descriptions
1411
+ - devops-notes.md \u2014 deployment procedure, environment requirements, known limitations
1412
+
1413
+ Principles: Follow security best practices (non-root user, minimal base image, no secrets in image). Make CI fail fast. Document every non-obvious decision in devops-notes.md.`;
1414
+ var TASK5 = `## Task Scope
1415
+ {{agentScope}}
1416
+
1417
+ ## Plan
1418
+ {{agentPlan}}
1419
+ {{upstreamOutputs}}
1420
+
1421
+ ---
1422
+
1423
+ ## Project
1424
+ {{projectName}}
1425
+
1426
+ ## Architecture Design
1427
+ {{file:architecture.md}}
1428
+
1429
+ ## Task
1430
+ Set up the deployment infrastructure for this project:
1431
+ 1. Write Dockerfile \u2014 multi-stage build if appropriate, non-root user, minimal image
1432
+ 2. Write docker-compose.yml \u2014 include all services from architecture.md (app, database, cache, etc.)
1433
+ 3. Write CI pipeline \u2014 build \u2192 lint \u2192 test \u2192 (optional) build image; fail fast
1434
+ 4. Write .env.example \u2014 all required environment variables with placeholder values and descriptions
1435
+ 5. Write devops-notes.md \u2014 how to build, run locally, deploy to production; environment requirements; known limitations
1436
+
1437
+ Start by reading implementation-notes.md (if present) to understand what commands are used for build and test.`;
1438
+ function devops(overrides) {
1439
+ return markPresetTools(
1440
+ {
1441
+ id: "devops",
1442
+ name: "DevOps",
1443
+ icon: "\u25B6",
1444
+ description: "sets up deployment",
1445
+ runtime: "acp",
1446
+ models: { acp: "sonnet" },
1447
+ modelTier: "balanced",
1448
+ systemPrompt: SYSTEM5,
1449
+ taskPrompt: TASK5,
1450
+ tools: ["Read", "Write"],
1451
+ ...overrides
1452
+ },
1453
+ overrides
1454
+ );
1455
+ }
1456
+
1457
+ // src/agents/frontend-developer.ts
1458
+ var SYSTEM6 = `You are the frontend engineer. Your responsibility is to turn design specs and API contracts into a working user interface.
1459
+
1460
+ Responsibilities:
1461
+ - Read the architecture design and API specification, implement pages, components, and interactions
1462
+ - Integrate backend API contracts correctly; use mock data when the backend is not ready
1463
+ - Write frontend tests and run validation commands
1464
+
1465
+ Deliverables (write code to the working directory, and record results in implementation-notes.md).
1466
+
1467
+ Principles: Write code directly without asking for confirmation. Do not change the API contract \u2014 escalate contract issues upstream. Do not refactor unrelated code. Run commands and report evidence.`;
1468
+ var TASK6 = `## Task Scope
1469
+ {{agentScope}}
1470
+
1471
+ ## Plan
1472
+ {{agentPlan}}
1473
+ {{upstreamOutputs}}
1474
+
1475
+ ---
1476
+
1477
+ ## UI Design Specification
1478
+ {{file:ui-design.md}}
1479
+
1480
+ ## Architecture Design
1481
+ {{file:architecture.md}}
1482
+
1483
+ ## API Specification
1484
+ {{file:api-spec.md}}
1485
+
1486
+ ## Task
1487
+ Implement complete frontend code following the UI design specification, architecture design, and API specification:
1488
+ 1. Create all page/component files (following the component inventory in ui-design.md and directory structure in architecture.md)
1489
+ 2. Implement design tokens (colors, typography, spacing) as defined in ui-design.md
1490
+ 3. Integrate API endpoints (following api-spec.md); use mock data if the backend is not ready
1491
+ 4. Implement all interaction states: loading, empty, error, success
1492
+ 5. Apply accessibility attributes (ARIA roles, keyboard navigation) per ui-design.md
1493
+ 6. Write basic tests
1494
+ 7. Run validation commands and record the file list, commands, and results in implementation-notes.md
1495
+
1496
+ Follow ui-design.md exactly. If a component or state is not specified there, implement with best practices and note it.`;
1497
+ function frontendDeveloper(overrides) {
1498
+ return markPresetTools(
1499
+ {
1500
+ id: "frontend-developer",
1501
+ name: "Frontend Developer",
1502
+ icon: "\u25C7",
1503
+ description: "builds the interface",
1504
+ runtime: "acp",
1505
+ models: { acp: "sonnet" },
1506
+ modelTier: "balanced",
1507
+ systemPrompt: SYSTEM6,
1508
+ taskPrompt: TASK6,
1509
+ tools: ["Read", "Write", "Bash"],
1510
+ ...overrides
1511
+ },
1512
+ overrides
1513
+ );
1514
+ }
1515
+
1516
+ // src/agents/leader.ts
1517
+ var SYSTEM7 = `You are the project leader. Decide what the requirement actually needs before anyone else starts.
1518
+
1519
+ There are three outcomes, and the first one matters most:
1520
+ - "reply" \u2014 the message is a greeting, a question, or anything you can answer on the spot. Answer it and stop; do not spend the team on it.
1521
+ - "ask" \u2014 it is a real task, but too vague to plan. Ask the one question that would unblock it.
1522
+ - "run" \u2014 it is a task you can scope. Name the minimum set of agents needed.
1523
+
1524
+ Do not apply fixed rules \u2014 use your judgment based on the actual requirement and the code. A minor bug fix may need only a developer and code-reviewer; a complex new feature may need the full pipeline. Read the code to understand the impact before deciding.
1525
+
1526
+ The console hands you the turns that came before this one. A requirement is often only interpretable next to them: "Postgres" is an answer, not a task, and the turn list is what tells you which question it answers. Read it as your own conversation \u2014 "you asked" is you. If you asked and got an answer, decide now; do not ask the same question again. For every "run" decision, include a complete, standalone "requirement" that combines the original task goal with all clarification answers and constraints. Never pass only the latest short answer to the team.
1527
+
1528
+ Output ONLY a single JSON object on its own line \u2014 no prose, no markdown fences:
1529
+ {"mode": "reply", "message": "your answer"}
1530
+ {"mode": "ask", "message": "your question"}
1531
+ {"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id-1", "agent-id-2"]}`;
1532
+ var TASK7 = `## Project
1533
+ {{projectName}}
1534
+
1535
+ ## Requirement
1536
+ {{requirement}}
1537
+ {{conversation}}
1538
+ ## Available Agents
1539
+ {{availableAgents}}
1540
+
1541
+ ## Task
1542
+ Read the codebase to understand the current state, then decide how to handle the requirement.
1543
+ Output a single JSON object \u2014 exactly one of:
1544
+ {"mode": "reply", "message": "your answer"}
1545
+ {"mode": "ask", "message": "your question"}
1546
+ {"mode": "run", "reason": "one sentence on why this set", "requirement": "the complete task and constraints", "run": ["agent-id", ...]}
1547
+ Agent IDs must come from the Available Agents list above.`;
1548
+ function leader(overrides) {
1549
+ return markPresetTools(
1550
+ {
1551
+ id: "leader",
1552
+ name: "Leader",
1553
+ icon: "\u2605",
1554
+ description: "routes the request",
1555
+ runtime: "acp",
1556
+ models: { acp: "sonnet" },
1557
+ modelTier: "balanced",
1558
+ systemPrompt: SYSTEM7,
1559
+ taskPrompt: TASK7,
1560
+ tools: ["Read", "Bash"],
1561
+ ...overrides
1562
+ },
1563
+ overrides
1564
+ );
1565
+ }
1566
+
1567
+ // src/agents/product-manager.ts
1568
+ var SYSTEM8 = `You are the product manager. Your responsibility is to transform vague business ideas into clear, development-ready, testable product requirement documents.
1569
+
1570
+ Responsibilities:
1571
+ - Analyze business goals and user scenarios
1572
+ - Define feature scope, business rules, and acceptance criteria
1573
+ - Structure "what is needed" into a PRD that the development team can execute directly
1574
+
1575
+ Deliverables (save to the working directory):
1576
+ - requirements.md \u2014 product requirements document containing: one-line definition, goals and success metrics, user roles, functional requirements (FR-), business rules (BR-), acceptance criteria (AC-), open questions (OP-)
1577
+
1578
+ Principles: Output the document directly without preamble. Mark uncertain items as OP- (open questions) rather than guessing. Do not do technical architecture design. Do not write feature code.`;
1579
+ var TASK8 = `## Business Context
1580
+ {{requirement}}
1581
+ {{upstreamOutputs}}
1582
+
1583
+ ## Task
1584
+ Based on the context above, create requirements.md in the current directory containing:
1585
+ - One-line definition (what this product/feature is)
1586
+ - Goals and success metrics
1587
+ - User roles and typical scenarios
1588
+ - Functional requirements (FR-01, FR-02, ...)
1589
+ - Business rules (BR-01, ...)
1590
+ - Acceptance criteria (AC-01, ..., each must be executable and verifiable)
1591
+ - Open questions (OP-01, ..., list uncertain items here)
1592
+
1593
+ Create the file directly. Mark uncertain items as OP- rather than guessing.`;
1594
+ function productManager(overrides) {
1595
+ return markPresetTools(
1596
+ {
1597
+ id: "product-manager",
1598
+ name: "Product Manager",
1599
+ icon: "\u2261",
1600
+ description: "writes the requirements",
1601
+ runtime: "acp",
1602
+ models: { acp: "sonnet" },
1603
+ modelTier: "balanced",
1604
+ systemPrompt: SYSTEM8,
1605
+ taskPrompt: TASK8,
1606
+ tools: ["Write"],
1607
+ ...overrides
1608
+ },
1609
+ overrides
1610
+ );
1611
+ }
1612
+
1613
+ // src/agents/reviewer.ts
1614
+ var SYSTEM9 = `You are an independent code review expert. You evaluate both business correctness and technical quality.
1615
+
1616
+ Responsibilities:
1617
+ - Read requirements, design documents, and implementation code
1618
+ - Assess: whether the implementation satisfies requirements, code quality, security risks, and maintainability
1619
+ - Do not run code, do not modify code \u2014 provide only professional judgment
1620
+
1621
+ Deliverables (save to the working directory):
1622
+ - review.md \u2014 score (1\u201310), issue list (blocking / advisory), improvement directions, conclusion (Approved / Changes Required)
1623
+
1624
+ Principles: Review independently \u2014 do not approve just because the developer says it is fine. Blocking issues must be marked Changes Required with the reason and suggested fix direction.`;
1625
+ var TASK9 = `## Project
1626
+ {{projectName}}
1627
+
1628
+ ## Requirement
1629
+ {{requirement}}
1630
+ {{upstreamOutputs}}
1631
+
1632
+ ## Task
1633
+ Perform an independent code review of all implementation in the current directory and output review.md:
1634
+ 1. Read architecture.md (if present) to understand the design intent
1635
+ 2. Read all implementation code
1636
+ 3. Evaluate across these dimensions:
1637
+ - Requirement coverage: is every feature implemented?
1638
+ - Code quality: readability, maintainability, edge case handling
1639
+ - Security and robustness: input validation, error handling
1640
+ - Test adequacy (if test code is present)
1641
+ 4. Output review.md: overall score (1\u201310), issue list (blocking / advisory), conclusion (Approved / Changes Required)
1642
+
1643
+ Judge independently. Do not accept "the developer says it's fine" as a reason to approve.`;
1644
+ function reviewer(overrides) {
1645
+ return markPresetTools(
1646
+ {
1647
+ id: "reviewer",
1648
+ name: "Reviewer",
1649
+ icon: "\u25C9",
1650
+ description: "reviews the delivery",
1651
+ runtime: "acp",
1652
+ models: { acp: "haiku" },
1653
+ modelTier: "fast",
1654
+ systemPrompt: SYSTEM9,
1655
+ taskPrompt: TASK9,
1656
+ tools: ["Read", "Write"],
1657
+ ...overrides
1658
+ },
1659
+ overrides
1660
+ );
1661
+ }
1662
+
1663
+ // src/agents/tester.ts
1664
+ var SYSTEM10 = `You are the test engineer. Your responsibility is to verify that requirements have actually been implemented.
1665
+
1666
+ Responsibilities:
1667
+ - Read requirements (acceptance criteria AC-) and the implementation code
1668
+ - Write and execute test cases covering: happy path, edge cases, error paths
1669
+ - Produce a reproducible test report
1670
+
1671
+ Deliverables (save to the working directory):
1672
+ - test-report.md \u2014 overall verdict (PASS / FAIL / BLOCKED), test results per feature, reproduction steps and evidence for failures
1673
+
1674
+ Principles: Do not PASS just because the code compiles or because a developer says it is fine. Every core feature must be verified with actual execution. When BLOCKED, state exactly what is missing.`;
1675
+ var TASK10 = `## Task Scope
1676
+ {{agentScope}}
1677
+
1678
+ ## Plan
1679
+ {{agentPlan}}
1680
+ {{upstreamOutputs}}
1681
+
1682
+ ---
1683
+
1684
+ ## Project
1685
+ {{projectName}}
1686
+
1687
+ ## Requirement
1688
+ {{requirement}}
1689
+
1690
+ ## Task
1691
+ Verify the implementation code in the current directory:
1692
+ 1. Read all source code to understand the implementation
1693
+ 2. Write and execute tests for each core feature
1694
+ 3. Output results to test-report.md containing:
1695
+ - Overall verdict (PASS / FAIL / BLOCKED)
1696
+ - Test result for each feature
1697
+ - Reproduction steps and evidence for failures
1698
+
1699
+ Start immediately. Verify actual behavior \u2014 do not just review code.`;
1700
+ function tester(overrides) {
1701
+ return markPresetTools(
1702
+ {
1703
+ id: "tester",
1704
+ name: "Tester",
1705
+ icon: "\u2299",
1706
+ description: "verifies the behaviour",
1707
+ runtime: "acp",
1708
+ models: { acp: "sonnet" },
1709
+ modelTier: "balanced",
1710
+ systemPrompt: SYSTEM10,
1711
+ taskPrompt: TASK10,
1712
+ tools: ["Read", "Write", "Bash"],
1713
+ ...overrides
1714
+ },
1715
+ overrides
1716
+ );
1717
+ }
1718
+
1719
+ // src/agents/ui-design-reviewer.ts
1720
+ var SYSTEM11 = `You are a UI/UX design reviewer. Your job is to evaluate a UI design specification before implementation begins.
1721
+
1722
+ Responsibilities:
1723
+ - Review the design specification for completeness, consistency, and feasibility
1724
+ - Verify all user flows and states are accounted for (loading, empty, error, success)
1725
+ - Assess accessibility coverage: ARIA roles, keyboard navigation, focus management
1726
+ - Check that design tokens are consistent and the component inventory covers all requirements
1727
+ - Do not write implementation code \u2014 provide design-level feedback only
1728
+
1729
+ Deliverables (save to the working directory):
1730
+ - ui-review.md \u2014 verdict (Approved / Changes Required), issue list (blocking / advisory) with component names and fix directions
1731
+
1732
+ Principles: Be specific \u2014 reference component names or section titles in ui-design.md. A blocking issue must include what is missing or wrong and a fix direction. If the design is thorough and implementable, say so directly.`;
1733
+ var TASK11 = `## Requirement
1734
+ {{requirement}}
1735
+
1736
+ ## UI Design Specification
1737
+ {{file:ui-design.md}}
1738
+ {{upstreamOutputs}}
1739
+
1740
+ ## Task
1741
+ Review the UI design specification above and output ui-review.md:
1742
+ 1. Overall verdict: Approved / Changes Required
1743
+ 2. Blocking issues (missing components, undefined states, broken flows, accessibility gaps):
1744
+ - component/section name, problem, fix direction
1745
+ 3. Advisory issues (inconsistent tokens, unclear props, polish):
1746
+ - component/section name, observation, suggestion
1747
+ 4. Design strengths worth noting
1748
+ 5. Final summary (1\u20132 sentences)
1749
+
1750
+ Reference specific component names or section titles when citing issues.`;
1751
+ function uiDesignReviewer(overrides) {
1752
+ return markPresetTools(
1753
+ {
1754
+ id: "ui-design-reviewer",
1755
+ name: "UI Design Reviewer",
1756
+ icon: "\u25CE",
1757
+ description: "reviews the UI design",
1758
+ runtime: "acp",
1759
+ models: { acp: "haiku" },
1760
+ modelTier: "fast",
1761
+ systemPrompt: SYSTEM11,
1762
+ taskPrompt: TASK11,
1763
+ tools: ["Read", "Write"],
1764
+ ...overrides
1765
+ },
1766
+ overrides
1767
+ );
1768
+ }
1769
+
1770
+ // src/agents/ui-designer.ts
1771
+ var SYSTEM12 = `You are a UI/UX designer. Your job is to translate architecture and requirements into a detailed, developer-ready UI design specification.
1772
+
1773
+ Responsibilities:
1774
+ - Define the component inventory: every page, section, and reusable component
1775
+ - Specify layouts, spacing principles, and responsive breakpoints
1776
+ - Define interaction patterns: loading states, empty states, error states, transitions
1777
+ - Specify design tokens: color palette, typography scale, spacing scale, shadow levels
1778
+ - Describe accessibility requirements: ARIA roles, keyboard navigation, focus management
1779
+ - Do not write implementation code \u2014 produce specification documents only
1780
+
1781
+ Deliverables (save to the working directory):
1782
+ - ui-design.md \u2014 full UI design specification: component inventory, page layouts, interaction flows, design tokens, accessibility requirements
1783
+
1784
+ Principles: Be specific enough that a developer can implement without guessing. Every component gets a name, purpose, props/variants, and states. Flag anything that depends on backend data.`;
1785
+ var TASK12 = `## Task Scope
1786
+ {{agentScope}}
1787
+
1788
+ ## Plan
1789
+ {{agentPlan}}
1790
+ {{upstreamOutputs}}
1791
+
1792
+ ---
1793
+
1794
+ ## Project
1795
+ {{projectName}}
1796
+
1797
+ ## Requirement
1798
+ {{requirement}}
1799
+
1800
+ ## Architecture Design
1801
+ {{file:architecture.md}}
1802
+
1803
+ ## API Specification
1804
+ {{file:api-spec.md}}
1805
+
1806
+ ## Task
1807
+ Create ui-design.md covering:
1808
+ 1. **Component Inventory** \u2014 list every page and reusable component; for each: name, purpose, variants/states, props
1809
+ 2. **Page Layouts** \u2014 describe the visual structure of each page (header, main, sidebar, footer zones); include responsive behaviour
1810
+ 3. **Interaction Flows** \u2014 key user flows with step-by-step state transitions (loading \u2192 success \u2192 error)
1811
+ 4. **Design Tokens** \u2014 color palette (primary, neutral, semantic), typography scale, spacing scale
1812
+ 5. **Accessibility** \u2014 ARIA roles/labels for interactive components, keyboard nav, focus order
1813
+
1814
+ Be concrete. Reference API endpoints where components bind to data. Flag any UX decision that needs product clarification.`;
1815
+ function uiDesigner(overrides) {
1816
+ return markPresetTools(
1817
+ {
1818
+ id: "ui-designer",
1819
+ name: "UI Designer",
1820
+ icon: "\u25A3",
1821
+ description: "specs the interface",
1822
+ runtime: "acp",
1823
+ models: { acp: "sonnet" },
1824
+ modelTier: "balanced",
1825
+ systemPrompt: SYSTEM12,
1826
+ taskPrompt: TASK12,
1827
+ tools: ["Read", "Write"],
1828
+ ...overrides
1829
+ },
1830
+ overrides
1831
+ );
1832
+ }
1833
+
1834
+ // src/agents/index.ts
1835
+ var agents = {
1836
+ leader,
1837
+ architect,
1838
+ productManager,
1839
+ backendDeveloper,
1840
+ frontendDeveloper,
1841
+ tester,
1842
+ reviewer,
1843
+ archDesignReviewer,
1844
+ uiDesigner,
1845
+ uiDesignReviewer,
1846
+ codeReviewer,
1847
+ devops
1848
+ };
1849
+
1850
+ // src/config/defineConfig.ts
1851
+ function defineConfig(config) {
1852
+ return config;
1853
+ }
1854
+
1855
+ // src/event/EventBus.ts
1856
+ var EventBus = class {
1857
+ constructor() {
1858
+ this.listeners = /* @__PURE__ */ new Set();
1859
+ }
1860
+ subscribe(listener) {
1861
+ this.listeners.add(listener);
1862
+ return () => this.listeners.delete(listener);
1863
+ }
1864
+ emit(event) {
1865
+ for (const listener of this.listeners) {
1866
+ try {
1867
+ listener(event);
1868
+ } catch (err) {
1869
+ console.error("EventBus listener error:", err);
1870
+ }
1871
+ }
1872
+ }
1873
+ clear() {
1874
+ this.listeners.clear();
1875
+ }
1876
+ };
1877
+
1878
+ // src/orchestrator/AgentSessionManager.ts
1879
+ var AgentSessionManager = class {
1880
+ constructor() {
1881
+ this.entries = /* @__PURE__ */ new Map();
1882
+ this.closed = false;
1883
+ }
1884
+ async execute(runtime, task) {
1885
+ if (this.closed) throw new Error("AgentSessionManager is closed");
1886
+ if (this.resetting) throw new Error("AgentSessionManager is resetting");
1887
+ if (task.signal?.aborted) throw abortError();
1888
+ let entry3 = this.entries.get(task.agentId);
1889
+ if (entry3?.busy || entry3?.session && entry3.session.state !== "idle" && entry3.session.state !== "closed") {
1890
+ throw new Error(`Agent session ${task.agentId} is busy`);
1891
+ }
1892
+ if (entry3?.closing) await entry3.closing;
1893
+ const fingerprint = JSON.stringify([
1894
+ task.command,
1895
+ task.args,
1896
+ task.model,
1897
+ task.workDir,
1898
+ task.systemPrompt,
1899
+ task.tools,
1900
+ task.sessionId,
1901
+ task.resume,
1902
+ Boolean(task.onQuestion)
1903
+ ]);
1904
+ if (entry3 && (entry3.runtime !== runtime || entry3.fingerprint !== fingerprint || entry3.session?.state === "closed" || !runtime.openSession)) {
1905
+ entry3.busy = true;
1906
+ try {
1907
+ await this.closeEntry(entry3);
1908
+ this.entries.delete(task.agentId);
1909
+ } finally {
1910
+ entry3.busy = false;
1911
+ }
1912
+ entry3 = void 0;
1913
+ if (this.closed || this.resetting) throw abortError();
1914
+ }
1915
+ if (task.signal?.aborted) throw abortError();
1916
+ const openSession = runtime.openSession;
1917
+ if (!openSession) return runtime.execute(task);
1918
+ let detach;
1919
+ if (!entry3) {
1920
+ const startup = new AbortController();
1921
+ const forwardAbort = () => startup.abort(task.signal?.reason);
1922
+ task.signal?.addEventListener("abort", forwardAbort, { once: true });
1923
+ detach = () => task.signal?.removeEventListener("abort", forwardAbort);
1924
+ const created = {
1925
+ runtime,
1926
+ fingerprint,
1927
+ startup,
1928
+ busy: true,
1929
+ onWarning: task.onWarning,
1930
+ // Defer opening until the cache entry is installed, including synchronous failures.
1931
+ opening: Promise.resolve().then(
1932
+ () => openSession.call(runtime, {
1933
+ agentId: task.agentId,
1934
+ systemPrompt: task.systemPrompt,
1935
+ tools: task.tools,
1936
+ workDir: task.workDir,
1937
+ command: task.command,
1938
+ args: task.args,
1939
+ model: task.model,
1940
+ sessionId: task.sessionId,
1941
+ resume: task.resume,
1942
+ timeout: task.timeout,
1943
+ questionForms: Boolean(task.onQuestion),
1944
+ signal: startup.signal,
1945
+ onWarning: (message) => created.onWarning?.(message)
1946
+ })
1947
+ ).then((session) => {
1948
+ created.session = session;
1949
+ return session;
1950
+ })
1923
1951
  };
1924
- const prompt = (text, promptOptions = {}) => {
1925
- if (state !== "idle") {
1926
- return Promise.reject(new Error(`Cannot prompt ACP session while ${state}`));
1952
+ entry3 = created;
1953
+ this.entries.set(task.agentId, entry3);
1954
+ } else {
1955
+ entry3.busy = true;
1956
+ entry3.onWarning = task.onWarning;
1957
+ }
1958
+ try {
1959
+ const session = await entry3.opening;
1960
+ detach?.();
1961
+ detach = void 0;
1962
+ if (entry3.startup.signal.aborted || this.closed || this.resetting) {
1963
+ await this.closeEntry(entry3);
1964
+ throw abortError();
1965
+ }
1966
+ if (session.state !== "idle") throw new Error(`Agent session ${task.agentId} is busy`);
1967
+ return await session.prompt(task.prompt, {
1968
+ signal: task.signal,
1969
+ timeout: task.timeout,
1970
+ onEvent: task.onEvent,
1971
+ permissionMode: task.permissionMode,
1972
+ onPermission: task.onPermission,
1973
+ onQuestion: task.onQuestion
1974
+ });
1975
+ } finally {
1976
+ detach?.();
1977
+ entry3.onWarning = void 0;
1978
+ try {
1979
+ if (entry3.session?.state === "closed") {
1980
+ await this.closeEntry(entry3);
1981
+ if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
1982
+ } else if (!entry3.session) {
1983
+ if (this.entries.get(task.agentId) === entry3) this.entries.delete(task.agentId);
1927
1984
  }
1928
- if (options.signal?.aborted || promptOptions.signal?.aborted) {
1929
- return Promise.reject(abortError());
1985
+ } finally {
1986
+ entry3.busy = false;
1987
+ }
1988
+ }
1989
+ }
1990
+ /**
1991
+ * Deliver a message into every session that is running a turn right now.
1992
+ *
1993
+ * Only `running` sessions are candidates, and that is the whole safety
1994
+ * argument: a session between turns has nothing to inject into, and asking a
1995
+ * provider to steer an idle session makes it *start* a turn that this manager
1996
+ * never scheduled — one whose output belongs to no turn and which nothing
1997
+ * would ever settle. Sessions that are idle are therefore not reported as
1998
+ * failures but simply absent, and the caller sees an empty list.
1999
+ *
2000
+ * One agent failing never stops the others: each report stands alone, because
2001
+ * the caller has to be able to say "architect took it, qa did not" rather than
2002
+ * "steering failed".
2003
+ */
2004
+ async supplement(text) {
2005
+ if (this.closed) return [];
2006
+ const targets = [...this.entries.entries()].filter(
2007
+ ([, entry3]) => entry3.session?.state === "running"
2008
+ );
2009
+ return await Promise.all(
2010
+ targets.map(async ([agentId, entry3]) => {
2011
+ const session = entry3.session;
2012
+ if (!session) return { agentId, status: "idle" };
2013
+ if (!session.capabilities.supplement || !session.supplement) {
2014
+ return { agentId, status: "unsupported" };
1930
2015
  }
1931
- let duration;
1932
2016
  try {
1933
- duration = timeoutMs(promptOptions.timeout ?? defaultTimeout);
1934
- } catch (error) {
1935
- return Promise.reject(error);
1936
- }
1937
- let resolve4;
1938
- let reject;
1939
- const promise = new Promise((res, rej) => {
1940
- resolve4 = res;
1941
- reject = rej;
1942
- });
1943
- const turn = {
1944
- runId: randomUUID(),
1945
- seq: 0,
1946
- options: promptOptions,
1947
- output: "",
1948
- decisions: new AbortController(),
1949
- promise,
1950
- resolve: resolve4,
1951
- reject
1952
- };
1953
- active = turn;
1954
- state = "running";
1955
- turn.onAbort = () => {
1956
- turn.error = abortError();
1957
- void cancelTurn().catch(() => {
1958
- });
1959
- };
1960
- promptOptions.signal?.addEventListener("abort", turn.onAbort, { once: true });
1961
- turn.timer = setTimeout(() => {
1962
- turn.error = new Error(`acp timed out (${duration}ms)`);
1963
- fail(turn.error);
1964
- }, duration);
1965
- const run = async () => {
1966
- emit({ type: "run_started", model: options.model });
1967
- if (active !== turn) return;
1968
- const response = await ctx.request(acp.methods.agent.session.prompt, {
1969
- sessionId,
1970
- prompt: [
1971
- {
1972
- type: "text",
1973
- text: meta === void 0 && options.systemPrompt ? `${options.systemPrompt}
1974
-
1975
- ---
1976
-
1977
- ${text}` : text
1978
- }
1979
- ]
1980
- });
1981
- if (active !== turn) return;
1982
- if (turn.error) {
1983
- settleTurn(turn, void 0, turn.error);
1984
- return;
1985
- }
1986
- const stopReason = mapStopReason(response.stopReason);
1987
- settleTurn(turn, {
1988
- output: turn.output,
1989
- exitCode: stopReason === "success" ? 0 : 1,
1990
- sessionId,
1991
- stopReason,
1992
- ...turn.usage ? { usage: turn.usage } : {},
1993
- ...turn.cost?.currency.toLowerCase() === "usd" ? { costUsd: turn.cost.amount } : {}
1994
- });
1995
- emitForTurn(turn, { type: "run_finished", stopReason });
1996
- };
1997
- void run().catch((error) => {
1998
- if (active === turn) fail(withAuthHint(acp, error));
1999
- });
2000
- return promise;
2001
- };
2002
- const session = {
2003
- get sessionId() {
2004
- return sessionId;
2005
- },
2006
- get state() {
2007
- return state;
2008
- },
2009
- get capabilities() {
2010
- return { continuation: true, supplement: steeringSupported };
2011
- },
2012
- prompt,
2013
- supplement,
2014
- cancelTurn,
2015
- close
2016
- };
2017
- const warn = (message) => {
2018
- if (options.onWarning) options.onWarning(message);
2019
- else process.stderr.write(`[multiclaw] Warning: ${message}
2020
- `);
2021
- };
2022
- const initialize = async () => {
2023
- const init = await ctx.request(acp.methods.agent.initialize, {
2024
- protocolVersion: acp.PROTOCOL_VERSION,
2025
- ...questionForms ? { clientCapabilities: { elicitation: { form: {} } } } : {}
2026
- });
2027
- if (terminalError) throw terminalError;
2028
- steeringSupported = advertisesSteering(init);
2029
- if (options.sessionId) {
2030
- if (init.agentCapabilities?.loadSession !== true) {
2031
- throw new PermanentError(
2032
- `Agent does not support session/load (loadSession=false); cannot resume session ${options.sessionId}`
2033
- );
2034
- }
2035
- await ctx.request(acp.methods.agent.session.load, {
2036
- sessionId: options.sessionId,
2037
- cwd: options.workDir,
2038
- mcpServers: [],
2039
- ...meta ? { _meta: meta } : {}
2040
- });
2041
- sessionId = options.sessionId;
2042
- } else {
2043
- const created = await ctx.request(acp.methods.agent.session.new, {
2044
- cwd: options.workDir,
2045
- mcpServers: [],
2046
- ...meta ? { _meta: meta } : {}
2047
- });
2048
- sessionId = created.sessionId;
2049
- const modelOpt = created.configOptions?.find(
2050
- (option) => option.category === "model" || option.id === "model"
2051
- );
2052
- if (modelOpt && "options" in modelOpt && Array.isArray(modelOpt.options)) {
2053
- discoveredModels.set(
2054
- command,
2055
- (modelOpt.options ?? []).map(
2056
- (option) => option.value ?? String(option)
2057
- )
2058
- );
2059
- }
2060
- }
2061
- if (terminalError) throw terminalError;
2062
- if (options.model && spec?.modelSelection === "config-option") {
2063
- const known = discoveredModels.get(command);
2064
- if (known && !known.includes(options.model)) {
2065
- warn(
2066
- `model "${options.model}" is not offered by ${command}; running with the provider's default.`
2067
- );
2068
- } else {
2069
- try {
2070
- await ctx.request(acp.methods.agent.session.setConfigOption, {
2071
- sessionId,
2072
- configId: "model",
2073
- value: options.model
2074
- });
2075
- } catch (error) {
2076
- if (terminalError) throw terminalError;
2077
- warn(
2078
- `Could not set model "${options.model}" on ${command}; running with the provider's default (${errorMessage(error)}).`
2079
- );
2080
- }
2081
- }
2017
+ return { agentId, status: (await session.supplement(text)).status };
2018
+ } catch (error) {
2019
+ return {
2020
+ agentId,
2021
+ status: "error",
2022
+ error: error instanceof Error ? error.message : String(error)
2023
+ };
2082
2024
  }
2083
- if (terminalError) throw terminalError;
2084
- if (options.signal?.aborted) throw abortError();
2085
- };
2086
- await Promise.race([
2087
- initialize().catch((error) => {
2088
- throw withAuthHint(acp, error);
2089
- }),
2090
- startupFailure
2091
- ]);
2092
- clearTimeout(startupTimer);
2093
- return session;
2094
- } catch (error) {
2095
- clearTimeout(startupTimer);
2096
- options.signal?.removeEventListener("abort", onSessionAbort);
2097
- if (cleanup) await cleanup();
2098
- throw error;
2025
+ })
2026
+ );
2027
+ }
2028
+ /** Closes all sessions and permits a fresh conversation after cleanup completes. */
2029
+ reset() {
2030
+ if (this.closing) return this.closing;
2031
+ if (this.resetting) return this.resetting;
2032
+ this.resetting = this.drain().finally(() => {
2033
+ this.resetting = void 0;
2034
+ });
2035
+ return this.resetting;
2036
+ }
2037
+ /** Terminal, idempotent cleanup barrier. No new executions may start. */
2038
+ close() {
2039
+ if (this.closing) return this.closing;
2040
+ this.closed = true;
2041
+ this.closing = this.resetting ?? this.drain();
2042
+ return this.closing;
2043
+ }
2044
+ async drain() {
2045
+ const entries = [...this.entries.entries()];
2046
+ for (const [, entry3] of entries) entry3.startup.abort();
2047
+ const results = await Promise.allSettled(
2048
+ entries.map(async ([id, entry3]) => {
2049
+ await this.closeEntry(entry3);
2050
+ if (this.entries.get(id) === entry3) this.entries.delete(id);
2051
+ })
2052
+ );
2053
+ const errors = results.flatMap(
2054
+ (result) => result.status === "rejected" ? [result.reason] : []
2055
+ );
2056
+ if (errors.length === 1) throw errors[0];
2057
+ if (errors.length > 1) throw new AggregateError(errors, "Agent session cleanup failed");
2058
+ }
2059
+ closeEntry(entry3) {
2060
+ if (!entry3.closing) {
2061
+ entry3.closing = (async () => {
2062
+ let session;
2063
+ try {
2064
+ session = await entry3.opening;
2065
+ } catch (error) {
2066
+ if (isAbortError(error)) return;
2067
+ throw error;
2068
+ }
2069
+ await session.close();
2070
+ })();
2099
2071
  }
2072
+ return entry3.closing;
2100
2073
  }
2101
2074
  };
2102
2075
 
2103
- // src/runtime/index.ts
2104
- var RuntimeRegistry = class {
2105
- constructor() {
2106
- this.runtimes = /* @__PURE__ */ new Map();
2107
- this.register(new AcpRuntime());
2076
+ // src/orchestrator/Orchestrator.ts
2077
+ import * as fs4 from "fs";
2078
+ import * as path5 from "path";
2079
+
2080
+ // src/utils/conversation.ts
2081
+ var MAX_CONVERSATION_TURNS = 6;
2082
+ var MAX_CONVERSATION_CHARS = 4e3;
2083
+ var MAX_ENTRY_CHARS = 600;
2084
+ var ELLIPSIS = "\u2026";
2085
+ function clip(text, max) {
2086
+ const trimmed = text.trim();
2087
+ if (trimmed.length <= max) return trimmed;
2088
+ return `${trimmed.slice(0, Math.max(0, max - 1)).trimEnd()}${ELLIPSIS}`;
2089
+ }
2090
+ function agentSummary(label, agents2, format) {
2091
+ let text = `${label}: `;
2092
+ if (agents2.length === 0) text += "none";
2093
+ for (let i = 0; i < agents2.length; i++) {
2094
+ text += `${i > 0 ? ", " : ""}${clip(format(agents2[i]), MAX_ENTRY_CHARS)}`;
2095
+ if (text.length >= MAX_ENTRY_CHARS - 3) {
2096
+ if (i < agents2.length - 1) text += ELLIPSIS;
2097
+ break;
2098
+ }
2108
2099
  }
2109
- register(r) {
2110
- this.runtimes.set(r.name, r);
2100
+ return ` ${clip(text, MAX_ENTRY_CHARS - 3)}`;
2101
+ }
2102
+ function entry(turn, index) {
2103
+ const lines = [`${index}. user: ${clip(turn.requirement, MAX_ENTRY_CHARS)}`];
2104
+ if (turn.reply) {
2105
+ const verb = turn.reply.mode === "ask" ? "asked" : "answered";
2106
+ lines.push(` you ${verb}: ${clip(turn.reply.message, MAX_ENTRY_CHARS)}`);
2107
+ } else if (turn.outcome) {
2108
+ const { execution, completedAgents, failedAgents, acceptance } = turn.outcome;
2109
+ lines.push(
2110
+ ` ${clip(`execution: ${execution}`, MAX_ENTRY_CHARS - 3)}`,
2111
+ agentSummary("completed", completedAgents, (agentId) => agentId),
2112
+ agentSummary(
2113
+ "failed",
2114
+ failedAgents,
2115
+ ({ agentId, reason }) => reason ? `${agentId} (${reason})` : agentId
2116
+ ),
2117
+ ` ${clip(
2118
+ `acceptance: ${acceptance ? `${acceptance.status}${acceptance.reason ? ` (${acceptance.reason})` : ""}` : "not configured"}`,
2119
+ MAX_ENTRY_CHARS - 3
2120
+ )}`
2121
+ );
2111
2122
  }
2112
- get(name) {
2113
- const r = this.runtimes.get(name);
2114
- if (!r)
2115
- throw new Error(
2116
- `Runtime not found: "${name}". Available: ${[...this.runtimes.keys()].join(", ")}`
2117
- );
2118
- return r;
2123
+ return lines.join("\n");
2124
+ }
2125
+ function renderConversation(history) {
2126
+ if (!history || history.length === 0) return "";
2127
+ const recent = history.slice(-MAX_CONVERSATION_TURNS);
2128
+ const first = history.length - recent.length;
2129
+ const entries = recent.map((turn, i) => entry(turn, first + i + 1));
2130
+ while (entries.length > 1 && entries.join("\n").length > MAX_CONVERSATION_CHARS) {
2131
+ entries.shift();
2119
2132
  }
2120
- has(name) {
2121
- return this.runtimes.has(name);
2133
+ const body = clip(entries.join("\n"), MAX_CONVERSATION_CHARS);
2134
+ return `
2135
+ ## Conversation so far
2136
+
2137
+ Earlier turns of this session, oldest first:
2138
+
2139
+ ${body}
2140
+ `;
2141
+ }
2142
+
2143
+ // src/utils/json.ts
2144
+ function extractJsonObjects(text) {
2145
+ const objects = [];
2146
+ let searchFrom = 0;
2147
+ while (searchFrom < text.length) {
2148
+ const start = text.indexOf("{", searchFrom);
2149
+ if (start === -1) break;
2150
+ const end = findMatchingBrace(text, start);
2151
+ if (end !== -1) {
2152
+ try {
2153
+ objects.push(JSON.parse(text.slice(start, end + 1)));
2154
+ searchFrom = end + 1;
2155
+ continue;
2156
+ } catch {
2157
+ }
2158
+ }
2159
+ searchFrom = start + 1;
2122
2160
  }
2123
- list() {
2124
- return [...this.runtimes.keys()];
2161
+ return objects;
2162
+ }
2163
+ function findMatchingBrace(text, start) {
2164
+ let depth = 0;
2165
+ let inString = false;
2166
+ let escaped = false;
2167
+ for (let i = start; i < text.length; i++) {
2168
+ const ch = text[i];
2169
+ if (inString) {
2170
+ if (escaped) escaped = false;
2171
+ else if (ch === "\\") escaped = true;
2172
+ else if (ch === '"') inString = false;
2173
+ continue;
2174
+ }
2175
+ if (ch === '"') inString = true;
2176
+ else if (ch === "{") depth++;
2177
+ else if (ch === "}") {
2178
+ depth--;
2179
+ if (depth === 0) return i;
2180
+ }
2125
2181
  }
2126
- };
2127
- var runtimeRegistry = new RuntimeRegistry();
2182
+ return -1;
2183
+ }
2184
+
2185
+ // src/utils/leaderDecision.ts
2186
+ function parseLeaderDecision(text) {
2187
+ for (const value of extractJsonObjects(text)) {
2188
+ const decision = asDecision(value);
2189
+ if (decision) return decision;
2190
+ }
2191
+ return void 0;
2192
+ }
2193
+ function asDecision(value) {
2194
+ if (typeof value !== "object" || value === null) return void 0;
2195
+ const record = value;
2196
+ const mode = record.mode;
2197
+ if (mode === "reply" || mode === "ask") {
2198
+ const message = typeof record.message === "string" ? record.message.trim() : "";
2199
+ return message ? { mode, message } : void 0;
2200
+ }
2201
+ if (mode === "run" || mode === void 0) {
2202
+ const run = record.run;
2203
+ if (!Array.isArray(run) || !run.every((id) => typeof id === "string")) return void 0;
2204
+ const reason = typeof record.reason === "string" ? record.reason.trim() : "";
2205
+ const requirement = typeof record.requirement === "string" ? record.requirement.trim() : "";
2206
+ if ("requirement" in record && !requirement) return void 0;
2207
+ return {
2208
+ mode: "run",
2209
+ run,
2210
+ ...reason ? { reason } : {},
2211
+ ...requirement ? { requirement } : {}
2212
+ };
2213
+ }
2214
+ return void 0;
2215
+ }
2216
+
2217
+ // src/utils/upstream.ts
2218
+ var MAX_UPSTREAM_ENTRY_CHARS = 600;
2219
+ var MAX_UPSTREAM_CHARS = 4e3;
2220
+ var ELLIPSIS2 = "\u2026";
2221
+ var HEADER = "\n## What upstream produced\n\n";
2222
+ var TRAILER = "\n";
2223
+ function block(entries) {
2224
+ return `${HEADER}${entries.join("\n")}${TRAILER}`;
2225
+ }
2226
+ function compact(text, max) {
2227
+ const trimmed = text.trim();
2228
+ if (trimmed.length <= max) return trimmed;
2229
+ const marker = (count) => `${ELLIPSIS2} (${count} characters elided) ${ELLIPSIS2}`;
2230
+ const reserved = marker(trimmed.length).length;
2231
+ const keep = Math.max(0, max - reserved);
2232
+ const head = Math.ceil(keep / 2);
2233
+ const tail = keep - head;
2234
+ const elided = trimmed.length - keep;
2235
+ return `${trimmed.slice(0, head)}${marker(elided)}${trimmed.slice(trimmed.length - tail)}`;
2236
+ }
2237
+ function entry2(output) {
2238
+ const label = `- ${output.name} (${output.agentId}):
2239
+ `;
2240
+ const budget = MAX_UPSTREAM_ENTRY_CHARS - label.length;
2241
+ let body = compact(output.output, budget);
2242
+ const newlines = body.length - body.replace(/\n/g, "").length;
2243
+ if (label.length + body.length + 2 * newlines > MAX_UPSTREAM_ENTRY_CHARS) {
2244
+ body = compact(output.output, budget - 2 * newlines);
2245
+ }
2246
+ return `${label}${body.replace(/\n/g, "\n ")}`;
2247
+ }
2248
+ function renderUpstreamOutputs(outputs) {
2249
+ if (outputs.length === 0) return "";
2250
+ const entries = outputs.map(entry2);
2251
+ while (entries.length > 1 && block(entries).length > MAX_UPSTREAM_CHARS) {
2252
+ entries.shift();
2253
+ }
2254
+ return block(entries);
2255
+ }
2256
+
2257
+ // src/orchestrator/AgentRunner.ts
2258
+ import * as fs3 from "fs";
2259
+ import * as path4 from "path";
2128
2260
 
2129
2261
  // src/utils/prompt.ts
2130
2262
  import * as fs2 from "fs/promises";
@@ -2542,8 +2674,19 @@ var Orchestrator = class {
2542
2674
  );
2543
2675
  }
2544
2676
  async preview(options = {}) {
2677
+ if (options.signal?.aborted)
2678
+ return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
2545
2679
  const policyError = this.reviewPolicyError();
2546
2680
  if (policyError) throw new Error(policyError);
2681
+ try {
2682
+ await validateToolsPolicy(this.config, options);
2683
+ } catch (error) {
2684
+ if (options.signal?.aborted)
2685
+ return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
2686
+ throw error;
2687
+ }
2688
+ if (options.signal?.aborted)
2689
+ return { name: this.config.name, totalAgents: 0, stages: [], ...this.requirementField() };
2547
2690
  this.clearStaleTaskPlan();
2548
2691
  const resolution = await this.resolveTurn(options.signal);
2549
2692
  if (resolution.kind === "cancelled" || options.signal?.aborted) {
@@ -2587,6 +2730,14 @@ var Orchestrator = class {
2587
2730
  });
2588
2731
  return result2;
2589
2732
  }
2733
+ try {
2734
+ await validateToolsPolicy(this.config, options);
2735
+ } catch (error) {
2736
+ if (options.signal?.aborted) return this.finishCancelled(startTime);
2737
+ this.warn(errorMessage2(error));
2738
+ return this.finishEmpty(startTime, void 0, false);
2739
+ }
2740
+ if (options.signal?.aborted) return this.finishCancelled(startTime);
2590
2741
  this.clearStaleTaskPlan();
2591
2742
  const resolution = await this.resolveTurn(options.signal);
2592
2743
  if (resolution.kind === "cancelled" || options.signal?.aborted) {
@@ -3171,9 +3322,9 @@ Address these findings within your assigned scope, preserve the complete task re
3171
3322
  * only honest outcome is to say that none was done. Running the whole team
3172
3323
  * instead would contradict the leader's explicit choice.
3173
3324
  */
3174
- finishEmpty(startTime, acceptance) {
3325
+ finishEmpty(startTime, acceptance, warnEmpty = true) {
3175
3326
  const usedLeader = this.config.leader !== void 0 && this.config.useLeader !== false;
3176
- if (!acceptance?.reason)
3327
+ if (warnEmpty && !acceptance?.reason)
3177
3328
  this.warn(
3178
3329
  usedLeader ? "The leader selected none of the configured agents \u2014 nothing to run." : "The config lists no agents \u2014 nothing to run."
3179
3330
  );
@@ -3358,6 +3509,7 @@ export {
3358
3509
  defineConfig,
3359
3510
  describeResumePlan,
3360
3511
  detectProviders,
3512
+ hasExplicitTools,
3361
3513
  isAbortError,
3362
3514
  presentProviders,
3363
3515
  providerCommands,
@@ -3374,5 +3526,6 @@ export {
3374
3526
  specForCommand,
3375
3527
  specForProvider,
3376
3528
  tierModel,
3377
- validateRuntimeQuestionContent
3529
+ validateRuntimeQuestionContent,
3530
+ validateToolsPolicy
3378
3531
  };