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