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