squads-cli 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -1152
- package/dist/auth-YW3UPFSB.js +23 -0
- package/dist/autonomy-BWTVDEAT.js +102 -0
- package/dist/autonomy-BWTVDEAT.js.map +1 -0
- package/dist/chunk-3KCWNZWW.js +401 -0
- package/dist/chunk-3KCWNZWW.js.map +1 -0
- package/dist/chunk-67RO2HKR.js +174 -0
- package/dist/chunk-67RO2HKR.js.map +1 -0
- package/dist/chunk-7JVD7RD4.js +275 -0
- package/dist/chunk-7JVD7RD4.js.map +1 -0
- package/dist/chunk-BODLDQY7.js +452 -0
- package/dist/chunk-BODLDQY7.js.map +1 -0
- package/dist/chunk-FFFCFZ6A.js +121 -0
- package/dist/chunk-FFFCFZ6A.js.map +1 -0
- package/dist/chunk-FIWT2NMM.js +165 -0
- package/dist/chunk-FIWT2NMM.js.map +1 -0
- package/dist/chunk-L6GQCHDF.js +222 -0
- package/dist/chunk-L6GQCHDF.js.map +1 -0
- package/dist/{chunk-O7UV3FWI.js → chunk-LDM62TIX.js} +2 -2
- package/dist/chunk-LDM62TIX.js.map +1 -0
- package/dist/chunk-LOA3KWYJ.js +294 -0
- package/dist/chunk-LOA3KWYJ.js.map +1 -0
- package/dist/chunk-NA45DFXY.js +616 -0
- package/dist/chunk-NA45DFXY.js.map +1 -0
- package/dist/{chunk-4CMAEQQY.js → chunk-NQN6JPI7.js} +4 -3
- package/dist/chunk-NQN6JPI7.js.map +1 -0
- package/dist/chunk-OQJHPULO.js +103 -0
- package/dist/chunk-OQJHPULO.js.map +1 -0
- package/dist/chunk-QHNUMM4V.js +87 -0
- package/dist/chunk-QHNUMM4V.js.map +1 -0
- package/dist/chunk-RM6BWILN.js +74 -0
- package/dist/chunk-RM6BWILN.js.map +1 -0
- package/dist/chunk-WBR5J7EX.js +90 -0
- package/dist/chunk-WBR5J7EX.js.map +1 -0
- package/dist/chunk-Z2UKDBNL.js +162 -0
- package/dist/chunk-Z2UKDBNL.js.map +1 -0
- package/dist/cli.js +2136 -12600
- package/dist/cli.js.map +1 -1
- package/dist/context-M2A2DOFV.js +291 -0
- package/dist/context-M2A2DOFV.js.map +1 -0
- package/dist/context-feed-JMNW4GAM.js +391 -0
- package/dist/context-feed-JMNW4GAM.js.map +1 -0
- package/dist/cost-N37I4UTA.js +274 -0
- package/dist/cost-N37I4UTA.js.map +1 -0
- package/dist/create-554W5HNU.js +286 -0
- package/dist/create-554W5HNU.js.map +1 -0
- package/dist/daemon-XWPQPPPN.js +546 -0
- package/dist/daemon-XWPQPPPN.js.map +1 -0
- package/dist/dashboard-L7YKVQEB.js +945 -0
- package/dist/dashboard-L7YKVQEB.js.map +1 -0
- package/dist/dashboard-MFNRLCEE.js +794 -0
- package/dist/dashboard-MFNRLCEE.js.map +1 -0
- package/dist/doctor-RG75M5RO.js +346 -0
- package/dist/doctor-RG75M5RO.js.map +1 -0
- package/dist/env-config-KCLDBKYX.js +21 -0
- package/dist/exec-JQKBF7BL.js +197 -0
- package/dist/exec-JQKBF7BL.js.map +1 -0
- package/dist/feedback-KA2UYBZG.js +229 -0
- package/dist/feedback-KA2UYBZG.js.map +1 -0
- package/dist/github-UQTM5KMS.js +23 -0
- package/dist/goal-EOPC5ZCD.js +168 -0
- package/dist/goal-EOPC5ZCD.js.map +1 -0
- package/dist/health-3FZDOSR5.js +209 -0
- package/dist/health-3FZDOSR5.js.map +1 -0
- package/dist/history-TFVXJEDH.js +229 -0
- package/dist/history-TFVXJEDH.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/init-UOWTNMIE.js +747 -0
- package/dist/init-UOWTNMIE.js.map +1 -0
- package/dist/kpi-2SQ2WCVT.js +413 -0
- package/dist/kpi-2SQ2WCVT.js.map +1 -0
- package/dist/learn-6ERTERAO.js +269 -0
- package/dist/learn-6ERTERAO.js.map +1 -0
- package/dist/list-KSOMUBMB.js +92 -0
- package/dist/list-KSOMUBMB.js.map +1 -0
- package/dist/login-ST6PAXYE.js +155 -0
- package/dist/login-ST6PAXYE.js.map +1 -0
- package/dist/memory-3CSNKXIL.js +562 -0
- package/dist/memory-3CSNKXIL.js.map +1 -0
- package/dist/progress-FKG4V2VH.js +202 -0
- package/dist/progress-FKG4V2VH.js.map +1 -0
- package/dist/providers-66PDCORB.js +65 -0
- package/dist/providers-66PDCORB.js.map +1 -0
- package/dist/results-2MJFLWEO.js +224 -0
- package/dist/results-2MJFLWEO.js.map +1 -0
- package/dist/run-72OQLH5A.js +2685 -0
- package/dist/run-72OQLH5A.js.map +1 -0
- package/dist/session-6H67XPAQ.js +64 -0
- package/dist/session-6H67XPAQ.js.map +1 -0
- package/dist/{chunk-NHGLXN2F.js → sessions-GVQIMN4W.js} +23 -459
- package/dist/sessions-GVQIMN4W.js.map +1 -0
- package/dist/{squad-parser-4BI3G4RS.js → squad-parser-CM3HOIWM.js} +2 -2
- package/dist/squad-parser-CM3HOIWM.js.map +1 -0
- package/dist/stats-ONZI557Q.js +335 -0
- package/dist/stats-ONZI557Q.js.map +1 -0
- package/dist/status-FYH42FTB.js +346 -0
- package/dist/status-FYH42FTB.js.map +1 -0
- package/dist/sync-HJZJNXHW.js +800 -0
- package/dist/sync-HJZJNXHW.js.map +1 -0
- package/dist/update-B4WMUOPO.js +83 -0
- package/dist/update-B4WMUOPO.js.map +1 -0
- package/dist/{update-ALJKFFM7.js → update-L7FGHN6W.js} +2 -2
- package/dist/update-L7FGHN6W.js.map +1 -0
- package/package.json +18 -10
- package/dist/chunk-4CMAEQQY.js.map +0 -1
- package/dist/chunk-NHGLXN2F.js.map +0 -1
- package/dist/chunk-O7UV3FWI.js.map +0 -1
- package/dist/sessions-6PB7ALCE.js +0 -16
- /package/dist/{sessions-6PB7ALCE.js.map → auth-YW3UPFSB.js.map} +0 -0
- /package/dist/{squad-parser-4BI3G4RS.js.map → env-config-KCLDBKYX.js.map} +0 -0
- /package/dist/{update-ALJKFFM7.js.map → github-UQTM5KMS.js.map} +0 -0
|
@@ -0,0 +1,2685 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getCLIConfig,
|
|
4
|
+
isProviderCLIAvailable
|
|
5
|
+
} from "./chunk-QHNUMM4V.js";
|
|
6
|
+
import {
|
|
7
|
+
pushCognitionSignal,
|
|
8
|
+
reportConversationResult,
|
|
9
|
+
reportExecutionStart
|
|
10
|
+
} from "./chunk-WBR5J7EX.js";
|
|
11
|
+
import {
|
|
12
|
+
detectProviderFromModel
|
|
13
|
+
} from "./chunk-LOA3KWYJ.js";
|
|
14
|
+
import {
|
|
15
|
+
getBotGitEnv,
|
|
16
|
+
getBotPushUrl,
|
|
17
|
+
getCoAuthorTrailer
|
|
18
|
+
} from "./chunk-FIWT2NMM.js";
|
|
19
|
+
import {
|
|
20
|
+
parseCooldown
|
|
21
|
+
} from "./chunk-RM6BWILN.js";
|
|
22
|
+
import {
|
|
23
|
+
isLoggedIn,
|
|
24
|
+
loadSession
|
|
25
|
+
} from "./chunk-Z2UKDBNL.js";
|
|
26
|
+
import {
|
|
27
|
+
getApiUrl,
|
|
28
|
+
getBridgeUrl
|
|
29
|
+
} from "./chunk-OQJHPULO.js";
|
|
30
|
+
import {
|
|
31
|
+
Events,
|
|
32
|
+
flushEvents,
|
|
33
|
+
track
|
|
34
|
+
} from "./chunk-L6GQCHDF.js";
|
|
35
|
+
import {
|
|
36
|
+
findSquadsDir,
|
|
37
|
+
listAgents,
|
|
38
|
+
loadAgentDefinition,
|
|
39
|
+
loadSquad,
|
|
40
|
+
parseAgentProvider,
|
|
41
|
+
resolveMcpConfigPath
|
|
42
|
+
} from "./chunk-LDM62TIX.js";
|
|
43
|
+
import {
|
|
44
|
+
findMemoryDir
|
|
45
|
+
} from "./chunk-ZTQ7ISUR.js";
|
|
46
|
+
import {
|
|
47
|
+
RESET,
|
|
48
|
+
bold,
|
|
49
|
+
colors,
|
|
50
|
+
gradient,
|
|
51
|
+
icons,
|
|
52
|
+
writeLine
|
|
53
|
+
} from "./chunk-N7KDWU4W.js";
|
|
54
|
+
import "./chunk-7OCVIDC7.js";
|
|
55
|
+
|
|
56
|
+
// src/commands/run.ts
|
|
57
|
+
import ora from "ora";
|
|
58
|
+
import { spawn, execSync as execSync2 } from "child_process";
|
|
59
|
+
import { join as join2, dirname } from "path";
|
|
60
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync, cpSync, unlinkSync } from "fs";
|
|
61
|
+
|
|
62
|
+
// src/lib/permissions.ts
|
|
63
|
+
import { minimatch } from "minimatch";
|
|
64
|
+
function getDefaultContext(squad, agent) {
|
|
65
|
+
return {
|
|
66
|
+
squad,
|
|
67
|
+
agent,
|
|
68
|
+
permissions: {
|
|
69
|
+
mode: "warn",
|
|
70
|
+
bash: ["*"],
|
|
71
|
+
// All commands allowed by default
|
|
72
|
+
write: ["**"],
|
|
73
|
+
// All paths writable by default
|
|
74
|
+
read: ["**"],
|
|
75
|
+
// All paths readable by default
|
|
76
|
+
mcp: {
|
|
77
|
+
allow: ["*"],
|
|
78
|
+
// All MCP servers allowed
|
|
79
|
+
deny: []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function validateBashCommand(command, allowedCommands) {
|
|
85
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
86
|
+
if (allowedCommands.includes("*")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const isAllowed = allowedCommands.some((allowed) => {
|
|
90
|
+
if (allowed === baseCommand) return true;
|
|
91
|
+
if (allowed.includes("*")) {
|
|
92
|
+
return minimatch(baseCommand, allowed);
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
if (!isAllowed) {
|
|
97
|
+
return {
|
|
98
|
+
type: "bash",
|
|
99
|
+
requested: baseCommand,
|
|
100
|
+
reason: `Bash command '${baseCommand}' not in allowlist: [${allowedCommands.join(", ")}]`,
|
|
101
|
+
severity: "error"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function validateFilePath(path, allowedGlobs, operation) {
|
|
107
|
+
if (allowedGlobs.includes("**") || allowedGlobs.includes("*")) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const isAllowed = allowedGlobs.some((glob) => minimatch(path, glob));
|
|
111
|
+
if (!isAllowed) {
|
|
112
|
+
return {
|
|
113
|
+
type: operation,
|
|
114
|
+
requested: path,
|
|
115
|
+
reason: `${operation === "write" ? "Write" : "Read"} to '${path}' not allowed. Permitted paths: [${allowedGlobs.join(", ")}]`,
|
|
116
|
+
severity: "error"
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
function validateMcpServer(server, allow, deny) {
|
|
122
|
+
const isDenied = deny.some((pattern) => {
|
|
123
|
+
if (pattern === server) return true;
|
|
124
|
+
if (pattern.includes("*")) return minimatch(server, pattern);
|
|
125
|
+
return false;
|
|
126
|
+
});
|
|
127
|
+
if (isDenied) {
|
|
128
|
+
return {
|
|
129
|
+
type: "mcp",
|
|
130
|
+
requested: server,
|
|
131
|
+
reason: `MCP server '${server}' is explicitly denied`,
|
|
132
|
+
severity: "error"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (allow.includes("*")) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const isAllowed = allow.some((pattern) => {
|
|
139
|
+
if (pattern === server) return true;
|
|
140
|
+
if (pattern.includes("*")) return minimatch(server, pattern);
|
|
141
|
+
return false;
|
|
142
|
+
});
|
|
143
|
+
if (!isAllowed) {
|
|
144
|
+
return {
|
|
145
|
+
type: "mcp",
|
|
146
|
+
requested: server,
|
|
147
|
+
reason: `MCP server '${server}' not in allowlist: [${allow.join(", ")}]`,
|
|
148
|
+
severity: "error"
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function validateExecution(context, request) {
|
|
154
|
+
const violations = [];
|
|
155
|
+
if (request.bashCommands) {
|
|
156
|
+
for (const cmd of request.bashCommands) {
|
|
157
|
+
const violation = validateBashCommand(cmd, context.permissions.bash);
|
|
158
|
+
if (violation) {
|
|
159
|
+
violations.push(violation);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (request.writePaths) {
|
|
164
|
+
for (const path of request.writePaths) {
|
|
165
|
+
const violation = validateFilePath(path, context.permissions.write, "write");
|
|
166
|
+
if (violation) {
|
|
167
|
+
violations.push(violation);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (request.readPaths) {
|
|
172
|
+
for (const path of request.readPaths) {
|
|
173
|
+
const violation = validateFilePath(path, context.permissions.read, "read");
|
|
174
|
+
if (violation) {
|
|
175
|
+
violations.push(violation);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (request.mcpServers) {
|
|
180
|
+
for (const server of request.mcpServers) {
|
|
181
|
+
const violation = validateMcpServer(
|
|
182
|
+
server,
|
|
183
|
+
context.permissions.mcp.allow,
|
|
184
|
+
context.permissions.mcp.deny
|
|
185
|
+
);
|
|
186
|
+
if (violation) {
|
|
187
|
+
violations.push(violation);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const hasErrors = violations.some((v) => v.severity === "error");
|
|
192
|
+
const allowed = context.permissions.mode !== "strict" || !hasErrors;
|
|
193
|
+
return {
|
|
194
|
+
allowed,
|
|
195
|
+
violations,
|
|
196
|
+
mode: context.permissions.mode
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function formatViolations(result) {
|
|
200
|
+
const lines = [];
|
|
201
|
+
if (result.violations.length === 0) {
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
const modeLabel = {
|
|
205
|
+
warn: "\u26A0\uFE0F PERMISSION WARNING",
|
|
206
|
+
strict: "\u{1F6AB} PERMISSION DENIED",
|
|
207
|
+
audit: "\u{1F4DD} PERMISSION AUDIT"
|
|
208
|
+
}[result.mode];
|
|
209
|
+
lines.push(modeLabel);
|
|
210
|
+
lines.push("");
|
|
211
|
+
for (const v of result.violations) {
|
|
212
|
+
const icon = v.severity === "error" ? "\u2717" : "\u26A0";
|
|
213
|
+
lines.push(` ${icon} [${v.type}] ${v.reason}`);
|
|
214
|
+
}
|
|
215
|
+
if (result.mode === "warn") {
|
|
216
|
+
lines.push("");
|
|
217
|
+
lines.push(" Continuing with warnings (mode: warn)");
|
|
218
|
+
} else if (result.mode === "audit") {
|
|
219
|
+
lines.push("");
|
|
220
|
+
lines.push(" Logged for audit, continuing (mode: audit)");
|
|
221
|
+
} else if (!result.allowed) {
|
|
222
|
+
lines.push("");
|
|
223
|
+
lines.push(" Execution blocked (mode: strict)");
|
|
224
|
+
}
|
|
225
|
+
return lines;
|
|
226
|
+
}
|
|
227
|
+
function parsePermissionsYaml(content) {
|
|
228
|
+
const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)```/);
|
|
229
|
+
if (!yamlMatch) return null;
|
|
230
|
+
const yamlContent = yamlMatch[1];
|
|
231
|
+
const permissions = {};
|
|
232
|
+
const modeMatch = yamlContent.match(/^\s*mode:\s*(warn|strict|audit)/m);
|
|
233
|
+
if (modeMatch) {
|
|
234
|
+
permissions.mode = modeMatch[1];
|
|
235
|
+
}
|
|
236
|
+
const bashMatch = yamlContent.match(/^\s*bash:\s*\[(.*?)\]/m);
|
|
237
|
+
if (bashMatch) {
|
|
238
|
+
permissions.bash = bashMatch[1].split(",").map((s) => s.trim());
|
|
239
|
+
}
|
|
240
|
+
const writeMatch = yamlContent.match(/^\s*write:\s*\[(.*?)\]/m);
|
|
241
|
+
if (writeMatch) {
|
|
242
|
+
permissions.write = writeMatch[1].split(",").map((s) => s.trim());
|
|
243
|
+
}
|
|
244
|
+
const readMatch = yamlContent.match(/^\s*read:\s*\[(.*?)\]/m);
|
|
245
|
+
if (readMatch) {
|
|
246
|
+
permissions.read = readMatch[1].split(",").map((s) => s.trim());
|
|
247
|
+
}
|
|
248
|
+
const mcpAllowMatch = yamlContent.match(/^\s*allow:\s*\[(.*?)\]/m);
|
|
249
|
+
const mcpDenyMatch = yamlContent.match(/^\s*deny:\s*\[(.*?)\]/m);
|
|
250
|
+
if (mcpAllowMatch || mcpDenyMatch) {
|
|
251
|
+
permissions.mcp = {
|
|
252
|
+
allow: mcpAllowMatch ? mcpAllowMatch[1].split(",").map((s) => s.trim()) : ["*"],
|
|
253
|
+
deny: mcpDenyMatch ? mcpDenyMatch[1].split(",").map((s) => s.trim()) : []
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return Object.keys(permissions).length > 0 ? permissions : null;
|
|
257
|
+
}
|
|
258
|
+
function buildContextFromSquad(squadName, squadContent, agentName) {
|
|
259
|
+
const context = getDefaultContext(squadName, agentName);
|
|
260
|
+
const parsed = parsePermissionsYaml(squadContent);
|
|
261
|
+
if (parsed) {
|
|
262
|
+
if (parsed.mode) context.permissions.mode = parsed.mode;
|
|
263
|
+
if (parsed.bash) context.permissions.bash = parsed.bash;
|
|
264
|
+
if (parsed.write) context.permissions.write = parsed.write;
|
|
265
|
+
if (parsed.read) context.permissions.read = parsed.read;
|
|
266
|
+
if (parsed.mcp) context.permissions.mcp = parsed.mcp;
|
|
267
|
+
}
|
|
268
|
+
return context;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/lib/workflow.ts
|
|
272
|
+
import { join } from "path";
|
|
273
|
+
import { existsSync, writeFileSync, mkdirSync } from "fs";
|
|
274
|
+
import { execSync, exec } from "child_process";
|
|
275
|
+
import { promisify } from "util";
|
|
276
|
+
|
|
277
|
+
// src/lib/conversation.ts
|
|
278
|
+
function classifyAgent(agentName, roleDescription) {
|
|
279
|
+
if (roleDescription) {
|
|
280
|
+
const lower = roleDescription.toLowerCase();
|
|
281
|
+
if (lower.includes("orchestrat") || lower.includes("triage") || lower.includes("coordinat")) return "lead";
|
|
282
|
+
if (lower.includes("scan") || lower.includes("monitor") || lower.includes("detect")) return "scanner";
|
|
283
|
+
if (lower.includes("verif") || lower.includes("review") || lower.includes("check") || lower.includes("critic")) return "verifier";
|
|
284
|
+
return "worker";
|
|
285
|
+
}
|
|
286
|
+
const name = agentName.toLowerCase();
|
|
287
|
+
if (name.includes("lead") || name.includes("orchestrator")) return "lead";
|
|
288
|
+
if (name.includes("scanner") || name.includes("scout") || name.includes("monitor")) return "scanner";
|
|
289
|
+
if (name.includes("verifier") || name.includes("critic") || name.includes("reviewer")) return "verifier";
|
|
290
|
+
if (name.includes("worker") || name.includes("solver") || name.includes("builder")) return "worker";
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function modelForRole(role) {
|
|
294
|
+
switch (role) {
|
|
295
|
+
case "lead":
|
|
296
|
+
return "sonnet";
|
|
297
|
+
case "worker":
|
|
298
|
+
return "sonnet";
|
|
299
|
+
case "scanner":
|
|
300
|
+
return "haiku";
|
|
301
|
+
case "verifier":
|
|
302
|
+
return "haiku";
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function createTranscript(squad) {
|
|
306
|
+
return {
|
|
307
|
+
squad,
|
|
308
|
+
turns: [],
|
|
309
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
310
|
+
totalCost: 0
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function serializeTranscript(transcript) {
|
|
314
|
+
if (transcript.turns.length === 0) return "";
|
|
315
|
+
let turns = transcript.turns;
|
|
316
|
+
if (turns.length > 5) {
|
|
317
|
+
const firstBrief = turns[0];
|
|
318
|
+
let lastReviewIdx = -1;
|
|
319
|
+
for (let i = turns.length - 1; i > 0; i--) {
|
|
320
|
+
if (turns[i].role === "lead") {
|
|
321
|
+
lastReviewIdx = i;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (lastReviewIdx > 0) {
|
|
326
|
+
turns = [firstBrief, ...turns.slice(lastReviewIdx)];
|
|
327
|
+
} else {
|
|
328
|
+
turns = [firstBrief, ...turns.slice(-3)];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const lines = ["## Conversation So Far\n"];
|
|
332
|
+
if (turns.length < transcript.turns.length) {
|
|
333
|
+
lines.push(`*(${transcript.turns.length - turns.length} earlier turns compacted \u2014 lead review below summarizes prior work)*
|
|
334
|
+
`);
|
|
335
|
+
}
|
|
336
|
+
for (const turn of turns) {
|
|
337
|
+
lines.push(`### ${turn.agent} (${turn.role}) \u2014 ${turn.timestamp}`);
|
|
338
|
+
lines.push(turn.content);
|
|
339
|
+
lines.push("");
|
|
340
|
+
}
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
function addTurn(transcript, agent, role, content, estimatedCost) {
|
|
344
|
+
transcript.turns.push({
|
|
345
|
+
agent,
|
|
346
|
+
role,
|
|
347
|
+
content,
|
|
348
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
349
|
+
estimatedCost
|
|
350
|
+
});
|
|
351
|
+
transcript.totalCost += estimatedCost;
|
|
352
|
+
}
|
|
353
|
+
var CONVERGENCE_PHRASES = [
|
|
354
|
+
"pr created",
|
|
355
|
+
"pr merged",
|
|
356
|
+
"issue closed",
|
|
357
|
+
"issue resolved",
|
|
358
|
+
"all tasks complete",
|
|
359
|
+
"all items complete",
|
|
360
|
+
"all issues resolved",
|
|
361
|
+
"nothing left to do",
|
|
362
|
+
"nothing remaining",
|
|
363
|
+
"session complete",
|
|
364
|
+
"queue empty",
|
|
365
|
+
"no open issues",
|
|
366
|
+
"no pending tasks",
|
|
367
|
+
"no pending issues"
|
|
368
|
+
];
|
|
369
|
+
var VERIFIER_APPROVAL_PHRASES = [
|
|
370
|
+
"approved",
|
|
371
|
+
"lgtm",
|
|
372
|
+
"looks good",
|
|
373
|
+
"all checks pass",
|
|
374
|
+
"all tests pass",
|
|
375
|
+
"tests pass",
|
|
376
|
+
"passed",
|
|
377
|
+
"quality standards met"
|
|
378
|
+
];
|
|
379
|
+
var VERIFIER_REJECTION_PHRASES = [
|
|
380
|
+
"failed",
|
|
381
|
+
"rejected",
|
|
382
|
+
"needs fixes",
|
|
383
|
+
"needs changes",
|
|
384
|
+
"does not pass",
|
|
385
|
+
"did not pass",
|
|
386
|
+
"failing"
|
|
387
|
+
];
|
|
388
|
+
var CONTINUATION_PHRASES = [
|
|
389
|
+
"needs review",
|
|
390
|
+
"needs feedback",
|
|
391
|
+
"needs input",
|
|
392
|
+
"need clarification",
|
|
393
|
+
"todo",
|
|
394
|
+
"fixme",
|
|
395
|
+
"blocked",
|
|
396
|
+
"waiting for",
|
|
397
|
+
"waiting on",
|
|
398
|
+
"will continue",
|
|
399
|
+
"will proceed",
|
|
400
|
+
"will work on",
|
|
401
|
+
"next step",
|
|
402
|
+
"in progress"
|
|
403
|
+
];
|
|
404
|
+
function detectConvergence(transcript, maxTurns, costCeiling) {
|
|
405
|
+
if (transcript.turns.length >= maxTurns) {
|
|
406
|
+
return { converged: true, reason: `Max turns reached (${maxTurns})` };
|
|
407
|
+
}
|
|
408
|
+
if (transcript.totalCost >= costCeiling) {
|
|
409
|
+
return { converged: true, reason: `Cost ceiling reached ($${transcript.totalCost.toFixed(2)}/$${costCeiling})` };
|
|
410
|
+
}
|
|
411
|
+
if (transcript.turns.length === 0) {
|
|
412
|
+
return { converged: false, reason: "No turns yet" };
|
|
413
|
+
}
|
|
414
|
+
const lastTurn = transcript.turns[transcript.turns.length - 1];
|
|
415
|
+
const content = lastTurn.content;
|
|
416
|
+
const lower = content.toLowerCase();
|
|
417
|
+
if (lastTurn.role === "verifier") {
|
|
418
|
+
const rejected = VERIFIER_REJECTION_PHRASES.some((phrase) => lower.includes(phrase));
|
|
419
|
+
if (rejected) {
|
|
420
|
+
return { converged: false, reason: "Verifier rejected \u2014 continuing cycle" };
|
|
421
|
+
}
|
|
422
|
+
const approved = VERIFIER_APPROVAL_PHRASES.some((phrase) => lower.includes(phrase));
|
|
423
|
+
if (approved) {
|
|
424
|
+
return { converged: true, reason: "Verifier approved" };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const hasContinuation = CONTINUATION_PHRASES.some((phrase) => lower.includes(phrase));
|
|
428
|
+
if (hasContinuation) {
|
|
429
|
+
return { converged: false, reason: "Continuation signal detected" };
|
|
430
|
+
}
|
|
431
|
+
const hasConvergence = CONVERGENCE_PHRASES.some((phrase) => lower.includes(phrase));
|
|
432
|
+
if (hasConvergence) {
|
|
433
|
+
return { converged: true, reason: "Convergence signal detected" };
|
|
434
|
+
}
|
|
435
|
+
return { converged: false, reason: "No signals detected, continuing" };
|
|
436
|
+
}
|
|
437
|
+
var COST_PER_TURN = {
|
|
438
|
+
opus: 2.5,
|
|
439
|
+
sonnet: 0.75,
|
|
440
|
+
haiku: 0.1
|
|
441
|
+
};
|
|
442
|
+
function estimateTurnCost(model) {
|
|
443
|
+
const key = model.includes("opus") ? "opus" : model.includes("haiku") ? "haiku" : "sonnet";
|
|
444
|
+
return COST_PER_TURN[key] || COST_PER_TURN.sonnet;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/lib/workflow.ts
|
|
448
|
+
var execAsync = promisify(exec);
|
|
449
|
+
var DEFAULT_MAX_TURNS = 20;
|
|
450
|
+
var DEFAULT_COST_CEILING = 25;
|
|
451
|
+
function executeAgentTurn(config) {
|
|
452
|
+
const { agentName, agentPath, role, squadName, model, transcript, task } = config;
|
|
453
|
+
const transcriptContext = serializeTranscript(transcript);
|
|
454
|
+
let roleInstructions;
|
|
455
|
+
switch (role) {
|
|
456
|
+
case "lead":
|
|
457
|
+
if (transcript.turns.length === 0 && task) {
|
|
458
|
+
roleInstructions = `## Founder Directive
|
|
459
|
+
|
|
460
|
+
${task}
|
|
461
|
+
|
|
462
|
+
Brief the team on this directive. Set priorities and assign work.`;
|
|
463
|
+
} else if (transcript.turns.length === 0) {
|
|
464
|
+
roleInstructions = `## Your Role: Lead
|
|
465
|
+
|
|
466
|
+
You are starting a new squad session. Brief the team:
|
|
467
|
+
1. Review open issues and PRs
|
|
468
|
+
2. Set priorities for this session
|
|
469
|
+
3. Assign work to workers
|
|
470
|
+
4. Be specific about what each worker should do`;
|
|
471
|
+
} else {
|
|
472
|
+
roleInstructions = `## Your Role: Lead (Review)
|
|
473
|
+
|
|
474
|
+
Review the work done so far. Either:
|
|
475
|
+
- Request specific changes from workers
|
|
476
|
+
- Approve and signal completion if quality is sufficient
|
|
477
|
+
- Merge PRs that pass CI using \`gh pr merge --squash --delete-branch\``;
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
case "scanner":
|
|
481
|
+
roleInstructions = `## Your Role: Scanner
|
|
482
|
+
|
|
483
|
+
Scan for issues, gaps, and opportunities. Report findings concisely. Do NOT fix anything \u2014 just discover and report.`;
|
|
484
|
+
break;
|
|
485
|
+
case "worker":
|
|
486
|
+
roleInstructions = `## Your Role: Worker
|
|
487
|
+
|
|
488
|
+
Execute the work assigned by the lead. Create branches, write code, open PRs to develop. Be focused and efficient.`;
|
|
489
|
+
break;
|
|
490
|
+
case "verifier":
|
|
491
|
+
roleInstructions = `## Your Role: Verifier
|
|
492
|
+
|
|
493
|
+
Verify that work meets quality standards. Check PRs, run tests, validate output. Report pass/fail with specifics.`;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
const prompt = `You are ${agentName} (${role}) in squad ${squadName}.
|
|
497
|
+
|
|
498
|
+
Read your full agent definition at ${agentPath} and follow its instructions.
|
|
499
|
+
|
|
500
|
+
${roleInstructions}
|
|
501
|
+
|
|
502
|
+
${transcriptContext}
|
|
503
|
+
|
|
504
|
+
IMPORTANT:
|
|
505
|
+
- Be concise. Your output becomes part of a shared transcript.
|
|
506
|
+
- Reference specific issue numbers, PR numbers, and file paths.
|
|
507
|
+
- If you create a PR, include the PR number in your output.
|
|
508
|
+
- If there's nothing to do, say "Nothing to do" clearly.
|
|
509
|
+
- When done, summarize what you did in 2-3 sentences.`;
|
|
510
|
+
const resolvedModel = config.model || modelForRole(role);
|
|
511
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
512
|
+
try {
|
|
513
|
+
const output = execSync(
|
|
514
|
+
`claude --print --dangerously-skip-permissions --model ${resolvedModel} -- '${escapedPrompt}'`,
|
|
515
|
+
{
|
|
516
|
+
cwd: config.cwd || process.cwd(),
|
|
517
|
+
timeout: 15 * 60 * 1e3,
|
|
518
|
+
// 15 min per turn
|
|
519
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
520
|
+
// 10MB
|
|
521
|
+
encoding: "utf-8",
|
|
522
|
+
env: {
|
|
523
|
+
...process.env,
|
|
524
|
+
CLAUDECODE: "",
|
|
525
|
+
// Allow nested sessions
|
|
526
|
+
ANTHROPIC_API_KEY: void 0
|
|
527
|
+
// Use Max subscription
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
return output.trim();
|
|
532
|
+
} catch (err) {
|
|
533
|
+
const error = err;
|
|
534
|
+
if (error.stdout && error.stdout.trim().length > 0) {
|
|
535
|
+
return error.stdout.trim();
|
|
536
|
+
}
|
|
537
|
+
return `[ERROR] Agent ${agentName} failed: ${error.message || "unknown error"}`;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function executeAgentTurnAsync(config) {
|
|
541
|
+
const { agentName, agentPath, role, squadName, model, transcript, task } = config;
|
|
542
|
+
let roleInstructions = "";
|
|
543
|
+
switch (role) {
|
|
544
|
+
case "lead":
|
|
545
|
+
roleInstructions = task ? `FOUNDER DIRECTIVE: ${task}
|
|
546
|
+
|
|
547
|
+
Brief the team on this directive. Assign specific tasks to scanners and workers.` : "Review the conversation so far. Assess worker output. Direct next actions or declare convergence.";
|
|
548
|
+
break;
|
|
549
|
+
case "scanner":
|
|
550
|
+
roleInstructions = "Scan for issues, data, or signals relevant to the lead's brief. Report findings concisely.";
|
|
551
|
+
break;
|
|
552
|
+
case "worker":
|
|
553
|
+
roleInstructions = "Execute the specific task assigned by the lead. Produce concrete output (PRs, issues, content, analysis).";
|
|
554
|
+
break;
|
|
555
|
+
case "verifier":
|
|
556
|
+
roleInstructions = "Verify the worker's output meets quality standards. Check for errors, omissions, and alignment with goals.";
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
const transcriptContext = transcript.turns.length > 0 ? `
|
|
560
|
+
== CONVERSATION SO FAR ==
|
|
561
|
+
${serializeTranscript(transcript)}
|
|
562
|
+
== END CONVERSATION ==` : "";
|
|
563
|
+
const resolvedModel = config.model || modelForRole(role);
|
|
564
|
+
const prompt = `You are ${agentName} (${role}) in squad ${squadName}.
|
|
565
|
+
|
|
566
|
+
Read your full agent definition at ${agentPath} and follow its instructions.
|
|
567
|
+
|
|
568
|
+
${roleInstructions}
|
|
569
|
+
|
|
570
|
+
${transcriptContext}
|
|
571
|
+
|
|
572
|
+
IMPORTANT:
|
|
573
|
+
- Be concise. Your output becomes part of a shared transcript.
|
|
574
|
+
- Reference specific issue numbers, PR numbers, and file paths.
|
|
575
|
+
- If you create a PR, include the PR number in your output.
|
|
576
|
+
- If there's nothing to do, say "Nothing to do" clearly.
|
|
577
|
+
- When done, summarize what you did in 2-3 sentences.`;
|
|
578
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
579
|
+
return new Promise((resolve) => {
|
|
580
|
+
exec(
|
|
581
|
+
`claude --print --dangerously-skip-permissions --model ${resolvedModel} -- '${escapedPrompt}'`,
|
|
582
|
+
{
|
|
583
|
+
cwd: config.cwd || process.cwd(),
|
|
584
|
+
timeout: 15 * 60 * 1e3,
|
|
585
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
586
|
+
encoding: "utf-8",
|
|
587
|
+
env: {
|
|
588
|
+
...process.env,
|
|
589
|
+
CLAUDECODE: "",
|
|
590
|
+
ANTHROPIC_API_KEY: void 0
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
(error, stdout, stderr) => {
|
|
594
|
+
if (stdout && stdout.trim().length > 0) {
|
|
595
|
+
resolve(stdout.trim());
|
|
596
|
+
} else if (error) {
|
|
597
|
+
resolve(`[ERROR] Agent ${agentName} failed: ${error.message || "unknown error"}`);
|
|
598
|
+
} else {
|
|
599
|
+
resolve("[No output]");
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function buildTurnPlan(squad, squadsDir) {
|
|
606
|
+
const agents = [];
|
|
607
|
+
for (const agent of squad.agents) {
|
|
608
|
+
const role = classifyAgent(agent.name, agent.role);
|
|
609
|
+
if (!role) continue;
|
|
610
|
+
const agentPath = join(squadsDir, squad.dir, `${agent.name}.md`);
|
|
611
|
+
if (!existsSync(agentPath)) continue;
|
|
612
|
+
agents.push({ name: agent.name, role, path: agentPath });
|
|
613
|
+
}
|
|
614
|
+
return agents;
|
|
615
|
+
}
|
|
616
|
+
async function runConversation(squad, options = {}) {
|
|
617
|
+
const squadsDir = findSquadsDir();
|
|
618
|
+
if (!squadsDir) {
|
|
619
|
+
return {
|
|
620
|
+
transcript: createTranscript(squad.name),
|
|
621
|
+
turnCount: 0,
|
|
622
|
+
totalCost: 0,
|
|
623
|
+
converged: true,
|
|
624
|
+
reason: "No squads directory found"
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const maxTurns = options.maxTurns || DEFAULT_MAX_TURNS;
|
|
628
|
+
const costCeiling = options.costCeiling || DEFAULT_COST_CEILING;
|
|
629
|
+
const transcript = createTranscript(squad.name);
|
|
630
|
+
let squadCwd = process.cwd();
|
|
631
|
+
if (squad.repo) {
|
|
632
|
+
const repoName = squad.repo.split("/").pop();
|
|
633
|
+
if (repoName) {
|
|
634
|
+
const reposRoot = join(squadsDir, "..", "..", "..");
|
|
635
|
+
const candidatePath = join(reposRoot, repoName);
|
|
636
|
+
if (existsSync(candidatePath)) {
|
|
637
|
+
squadCwd = candidatePath;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
const allAgents = buildTurnPlan(squad, squadsDir);
|
|
642
|
+
const leads = allAgents.filter((a) => a.role === "lead");
|
|
643
|
+
const scanners = allAgents.filter((a) => a.role === "scanner");
|
|
644
|
+
const workers = allAgents.filter((a) => a.role === "worker");
|
|
645
|
+
const verifiers = allAgents.filter((a) => a.role === "verifier");
|
|
646
|
+
if (leads.length === 0) {
|
|
647
|
+
return {
|
|
648
|
+
transcript,
|
|
649
|
+
turnCount: 0,
|
|
650
|
+
totalCost: 0,
|
|
651
|
+
converged: true,
|
|
652
|
+
reason: "No lead agent found \u2014 cannot orchestrate conversation"
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const lead = leads[0];
|
|
656
|
+
const log = (msg) => {
|
|
657
|
+
if (options.verbose) {
|
|
658
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
659
|
+
process.stderr.write(` [${ts}] ${msg}
|
|
660
|
+
`);
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
log(`Conversation: ${squad.name} | ${allAgents.length} agents | max ${maxTurns} turns | $${costCeiling} ceiling`);
|
|
664
|
+
log(` Lead: ${lead.name} | Scanners: ${scanners.map((s) => s.name).join(", ") || "none"} | Workers: ${workers.map((w) => w.name).join(", ") || "none"} | Verifiers: ${verifiers.map((v) => v.name).join(", ") || "none"}`);
|
|
665
|
+
let cycleCount = 0;
|
|
666
|
+
const MAX_CYCLES = 5;
|
|
667
|
+
while (cycleCount < MAX_CYCLES) {
|
|
668
|
+
cycleCount++;
|
|
669
|
+
log(`
|
|
670
|
+
--- Cycle ${cycleCount} ---`);
|
|
671
|
+
log(`Turn ${transcript.turns.length + 1}: ${lead.name} (lead)`);
|
|
672
|
+
const leadOutput = executeAgentTurn({
|
|
673
|
+
agentName: lead.name,
|
|
674
|
+
agentPath: lead.path,
|
|
675
|
+
role: "lead",
|
|
676
|
+
squadName: squad.name,
|
|
677
|
+
model: options.model || modelForRole("lead"),
|
|
678
|
+
transcript,
|
|
679
|
+
task: cycleCount === 1 ? options.task : void 0,
|
|
680
|
+
cwd: squadCwd
|
|
681
|
+
});
|
|
682
|
+
addTurn(transcript, lead.name, "lead", leadOutput, estimateTurnCost(options.model || "sonnet"));
|
|
683
|
+
let conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
684
|
+
if (conv.converged) {
|
|
685
|
+
log(`Converged after lead: ${conv.reason}`);
|
|
686
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
687
|
+
}
|
|
688
|
+
if (cycleCount === 1 && scanners.length > 0) {
|
|
689
|
+
if (scanners.length === 1) {
|
|
690
|
+
log(`Turn ${transcript.turns.length + 1}: ${scanners[0].name} (scanner)`);
|
|
691
|
+
const output = executeAgentTurn({
|
|
692
|
+
agentName: scanners[0].name,
|
|
693
|
+
agentPath: scanners[0].path,
|
|
694
|
+
role: "scanner",
|
|
695
|
+
squadName: squad.name,
|
|
696
|
+
model: options.model || modelForRole("scanner"),
|
|
697
|
+
transcript,
|
|
698
|
+
cwd: squadCwd
|
|
699
|
+
});
|
|
700
|
+
addTurn(transcript, scanners[0].name, "scanner", output, estimateTurnCost(options.model || "haiku"));
|
|
701
|
+
} else {
|
|
702
|
+
log(`Turns ${transcript.turns.length + 1}-${transcript.turns.length + scanners.length}: ${scanners.map((s) => s.name).join(", ")} (scanners, parallel)`);
|
|
703
|
+
const scannerPromises = scanners.map(
|
|
704
|
+
(scanner) => executeAgentTurnAsync({
|
|
705
|
+
agentName: scanner.name,
|
|
706
|
+
agentPath: scanner.path,
|
|
707
|
+
role: "scanner",
|
|
708
|
+
squadName: squad.name,
|
|
709
|
+
model: options.model || modelForRole("scanner"),
|
|
710
|
+
transcript,
|
|
711
|
+
// snapshot — all scanners see same context
|
|
712
|
+
cwd: squadCwd
|
|
713
|
+
}).then((output) => ({ agent: scanner, output }))
|
|
714
|
+
);
|
|
715
|
+
const scannerResults = await Promise.all(scannerPromises);
|
|
716
|
+
for (const { agent, output } of scannerResults) {
|
|
717
|
+
addTurn(transcript, agent.name, "scanner", output, estimateTurnCost(options.model || "haiku"));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
721
|
+
if (conv.converged) {
|
|
722
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (workers.length === 1) {
|
|
726
|
+
log(`Turn ${transcript.turns.length + 1}: ${workers[0].name} (worker)`);
|
|
727
|
+
const output = executeAgentTurn({
|
|
728
|
+
agentName: workers[0].name,
|
|
729
|
+
agentPath: workers[0].path,
|
|
730
|
+
role: "worker",
|
|
731
|
+
squadName: squad.name,
|
|
732
|
+
model: options.model || modelForRole("worker"),
|
|
733
|
+
transcript,
|
|
734
|
+
cwd: squadCwd
|
|
735
|
+
});
|
|
736
|
+
if (output.startsWith("[ERROR]")) {
|
|
737
|
+
process.stderr.write(` [WARN] Worker ${workers[0].name} errored: ${output}
|
|
738
|
+
`);
|
|
739
|
+
}
|
|
740
|
+
addTurn(transcript, workers[0].name, "worker", output, estimateTurnCost(options.model || "sonnet"));
|
|
741
|
+
} else if (workers.length > 1) {
|
|
742
|
+
log(`Turns ${transcript.turns.length + 1}-${transcript.turns.length + workers.length}: ${workers.map((w) => w.name).join(", ")} (workers, parallel)`);
|
|
743
|
+
const workerPromises = workers.map(
|
|
744
|
+
(worker) => executeAgentTurnAsync({
|
|
745
|
+
agentName: worker.name,
|
|
746
|
+
agentPath: worker.path,
|
|
747
|
+
role: "worker",
|
|
748
|
+
squadName: squad.name,
|
|
749
|
+
model: options.model || modelForRole("worker"),
|
|
750
|
+
transcript,
|
|
751
|
+
// snapshot — all workers see same context
|
|
752
|
+
cwd: squadCwd
|
|
753
|
+
}).then((output) => ({ agent: worker, output }))
|
|
754
|
+
);
|
|
755
|
+
const workerResults = await Promise.all(workerPromises);
|
|
756
|
+
for (const { agent, output } of workerResults) {
|
|
757
|
+
if (output.startsWith("[ERROR]")) {
|
|
758
|
+
process.stderr.write(` [WARN] Worker ${agent.name} errored: ${output}
|
|
759
|
+
`);
|
|
760
|
+
}
|
|
761
|
+
addTurn(transcript, agent.name, "worker", output, estimateTurnCost(options.model || "sonnet"));
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
765
|
+
if (conv.converged) {
|
|
766
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
767
|
+
}
|
|
768
|
+
log(`Turn ${transcript.turns.length + 1}: ${lead.name} (lead review)`);
|
|
769
|
+
const reviewOutput = executeAgentTurn({
|
|
770
|
+
agentName: lead.name,
|
|
771
|
+
agentPath: lead.path,
|
|
772
|
+
role: "lead",
|
|
773
|
+
squadName: squad.name,
|
|
774
|
+
model: options.model || modelForRole("lead"),
|
|
775
|
+
transcript,
|
|
776
|
+
cwd: squadCwd
|
|
777
|
+
});
|
|
778
|
+
addTurn(transcript, lead.name, "lead", reviewOutput, estimateTurnCost(options.model || "sonnet"));
|
|
779
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
780
|
+
if (conv.converged) {
|
|
781
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
782
|
+
}
|
|
783
|
+
if (verifiers.length === 1) {
|
|
784
|
+
log(`Turn ${transcript.turns.length + 1}: ${verifiers[0].name} (verifier)`);
|
|
785
|
+
const output = executeAgentTurn({
|
|
786
|
+
agentName: verifiers[0].name,
|
|
787
|
+
agentPath: verifiers[0].path,
|
|
788
|
+
role: "verifier",
|
|
789
|
+
squadName: squad.name,
|
|
790
|
+
model: options.model || modelForRole("verifier"),
|
|
791
|
+
transcript,
|
|
792
|
+
cwd: squadCwd
|
|
793
|
+
});
|
|
794
|
+
addTurn(transcript, verifiers[0].name, "verifier", output, estimateTurnCost(options.model || "haiku"));
|
|
795
|
+
} else if (verifiers.length > 1) {
|
|
796
|
+
log(`Turns ${transcript.turns.length + 1}-${transcript.turns.length + verifiers.length}: ${verifiers.map((v) => v.name).join(", ")} (verifiers, parallel)`);
|
|
797
|
+
const verifierPromises = verifiers.map(
|
|
798
|
+
(verifier) => executeAgentTurnAsync({
|
|
799
|
+
agentName: verifier.name,
|
|
800
|
+
agentPath: verifier.path,
|
|
801
|
+
role: "verifier",
|
|
802
|
+
squadName: squad.name,
|
|
803
|
+
model: options.model || modelForRole("verifier"),
|
|
804
|
+
transcript,
|
|
805
|
+
cwd: squadCwd
|
|
806
|
+
}).then((output) => ({ agent: verifier, output }))
|
|
807
|
+
);
|
|
808
|
+
const verifierResults = await Promise.all(verifierPromises);
|
|
809
|
+
for (const { agent, output } of verifierResults) {
|
|
810
|
+
addTurn(transcript, agent.name, "verifier", output, estimateTurnCost(options.model || "haiku"));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (verifiers.length > 0) {
|
|
814
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
815
|
+
if (conv.converged) {
|
|
816
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
transcript,
|
|
822
|
+
turnCount: transcript.turns.length,
|
|
823
|
+
totalCost: transcript.totalCost,
|
|
824
|
+
converged: false,
|
|
825
|
+
reason: `Max cycles reached (${MAX_CYCLES})`
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function saveTranscript(transcript) {
|
|
829
|
+
const squadsDir = findSquadsDir();
|
|
830
|
+
if (!squadsDir) return null;
|
|
831
|
+
const convDir = join(squadsDir, "..", "conversations", transcript.squad);
|
|
832
|
+
if (!existsSync(convDir)) {
|
|
833
|
+
mkdirSync(convDir, { recursive: true });
|
|
834
|
+
}
|
|
835
|
+
const id = Date.now().toString(36);
|
|
836
|
+
const filePath = join(convDir, `${id}.md`);
|
|
837
|
+
const lines = [
|
|
838
|
+
`# Conversation: ${transcript.squad}`,
|
|
839
|
+
`Started: ${transcript.startedAt}`,
|
|
840
|
+
`Turns: ${transcript.turns.length}`,
|
|
841
|
+
`Estimated cost: $${transcript.totalCost.toFixed(2)}`,
|
|
842
|
+
"",
|
|
843
|
+
"---",
|
|
844
|
+
""
|
|
845
|
+
];
|
|
846
|
+
for (const turn of transcript.turns) {
|
|
847
|
+
lines.push(`## ${turn.agent} (${turn.role}) \u2014 ${turn.timestamp}`);
|
|
848
|
+
lines.push("");
|
|
849
|
+
lines.push(turn.content);
|
|
850
|
+
lines.push("");
|
|
851
|
+
lines.push("---");
|
|
852
|
+
lines.push("");
|
|
853
|
+
}
|
|
854
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
855
|
+
return filePath;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/commands/run.ts
|
|
859
|
+
import { homedir } from "os";
|
|
860
|
+
var CLOUD_POLL_INTERVAL_MS = 3e3;
|
|
861
|
+
var CLOUD_POLL_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
862
|
+
var DEFAULT_LEARNINGS_LIMIT = 5;
|
|
863
|
+
var DEFAULT_CONTEXT_TOKENS = 8e3;
|
|
864
|
+
var DEFAULT_FALLBACK_CHARS = 2e3;
|
|
865
|
+
var MAX_AGENT_BRIEFS = 3;
|
|
866
|
+
var MAX_SQUAD_BRIEFS = 2;
|
|
867
|
+
var MAX_LEARNINGS_CHARS = 1500;
|
|
868
|
+
var MAX_LEAD_STATE_CHARS = 1e3;
|
|
869
|
+
var EXECUTION_EVENT_TIMEOUT_MS = 5e3;
|
|
870
|
+
var VERIFICATION_STATE_MAX_CHARS = 2e3;
|
|
871
|
+
var VERIFICATION_EXEC_TIMEOUT_MS = 3e4;
|
|
872
|
+
var DRYRUN_DEF_MAX_CHARS = 500;
|
|
873
|
+
var DRYRUN_CONTEXT_MAX_CHARS = 800;
|
|
874
|
+
var DEFAULT_SCHEDULED_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
875
|
+
var DEFAULT_TIMEOUT_MINUTES = 30;
|
|
876
|
+
var SOFT_DEADLINE_RATIO = 0.7;
|
|
877
|
+
var LOG_FILE_INIT_DELAY_MS = 500;
|
|
878
|
+
var VERBOSE_COMMAND_MAX_CHARS = 50;
|
|
879
|
+
async function registerContextWithBridge(ctx) {
|
|
880
|
+
const bridgeUrl = getBridgeUrl();
|
|
881
|
+
try {
|
|
882
|
+
const response = await fetch(`${bridgeUrl}/api/context/register`, {
|
|
883
|
+
method: "POST",
|
|
884
|
+
headers: { "Content-Type": "application/json" },
|
|
885
|
+
body: JSON.stringify({
|
|
886
|
+
execution_id: ctx.executionId,
|
|
887
|
+
squad: ctx.squad,
|
|
888
|
+
agent: ctx.agent,
|
|
889
|
+
task_type: ctx.taskType,
|
|
890
|
+
trigger: ctx.trigger
|
|
891
|
+
})
|
|
892
|
+
});
|
|
893
|
+
if (!response.ok) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
return true;
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
async function checkPreflightGates(squad, agent) {
|
|
902
|
+
const bridgeUrl = getBridgeUrl();
|
|
903
|
+
try {
|
|
904
|
+
const response = await fetch(`${bridgeUrl}/api/execution/preflight`, {
|
|
905
|
+
method: "POST",
|
|
906
|
+
headers: { "Content-Type": "application/json" },
|
|
907
|
+
body: JSON.stringify({ squad, agent })
|
|
908
|
+
});
|
|
909
|
+
if (!response.ok) {
|
|
910
|
+
return { allowed: true, gates: {} };
|
|
911
|
+
}
|
|
912
|
+
return await response.json();
|
|
913
|
+
} catch {
|
|
914
|
+
return { allowed: true, gates: {} };
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async function fetchLearnings(squad, limit = DEFAULT_LEARNINGS_LIMIT) {
|
|
918
|
+
const bridgeUrl = getBridgeUrl();
|
|
919
|
+
try {
|
|
920
|
+
const response = await fetch(
|
|
921
|
+
`${bridgeUrl}/api/learnings/relevant?squad=${encodeURIComponent(squad)}&limit=${limit}`
|
|
922
|
+
);
|
|
923
|
+
if (!response.ok) {
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
const data = await response.json();
|
|
927
|
+
return data.learnings || [];
|
|
928
|
+
} catch {
|
|
929
|
+
return [];
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function loadApprovalInstructions() {
|
|
933
|
+
const squadsDir = findSquadsDir();
|
|
934
|
+
if (!squadsDir) return "";
|
|
935
|
+
const instructionsPath = join2(dirname(squadsDir), "config", "approval-instructions.md");
|
|
936
|
+
if (existsSync2(instructionsPath)) {
|
|
937
|
+
try {
|
|
938
|
+
return readFileSync2(instructionsPath, "utf-8");
|
|
939
|
+
} catch {
|
|
940
|
+
return "";
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return "";
|
|
944
|
+
}
|
|
945
|
+
function loadPostExecution(squadName, agentName) {
|
|
946
|
+
const squadsDir = findSquadsDir();
|
|
947
|
+
if (squadsDir) {
|
|
948
|
+
const postExecPath = join2(dirname(squadsDir), "config", "post-execution.md");
|
|
949
|
+
if (existsSync2(postExecPath)) {
|
|
950
|
+
try {
|
|
951
|
+
const template = readFileSync2(postExecPath, "utf-8");
|
|
952
|
+
return template.replace(/\{\{squadName\}\}/g, squadName).replace(/\{\{agentName\}\}/g, agentName);
|
|
953
|
+
} catch {
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return `After completion:
|
|
958
|
+
- Create a branch, commit with Conventional Commits, push, and open a PR targeting develop
|
|
959
|
+
- NEVER commit to main directly
|
|
960
|
+
- Type /exit when done`;
|
|
961
|
+
}
|
|
962
|
+
function gatherSquadContext(squadName, agentName, options = {}) {
|
|
963
|
+
const squadsDir = findSquadsDir();
|
|
964
|
+
if (!squadsDir) return "";
|
|
965
|
+
const memoryDir = findMemoryDir();
|
|
966
|
+
const maxTokens = options.maxTokens || DEFAULT_CONTEXT_TOKENS;
|
|
967
|
+
const sections = [];
|
|
968
|
+
let estimatedTokens = 0;
|
|
969
|
+
const estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
970
|
+
const squadFile = join2(squadsDir, squadName, "SQUAD.md");
|
|
971
|
+
if (existsSync2(squadFile)) {
|
|
972
|
+
try {
|
|
973
|
+
const squadContent = readFileSync2(squadFile, "utf-8");
|
|
974
|
+
const missionMatch = squadContent.match(/## Mission[\s\S]*?(?=\n## |$)/i);
|
|
975
|
+
const goalsMatch = squadContent.match(/## (?:Goals|Objectives)[\s\S]*?(?=\n## |$)/i);
|
|
976
|
+
const outputMatch = squadContent.match(/## Output[\s\S]*?(?=\n## |$)/i);
|
|
977
|
+
const contextMatch = squadContent.match(/## Context[\s\S]*?(?=\n## |$)/i);
|
|
978
|
+
let squadContext = "";
|
|
979
|
+
if (missionMatch) squadContext += missionMatch[0] + "\n";
|
|
980
|
+
if (goalsMatch) squadContext += goalsMatch[0] + "\n";
|
|
981
|
+
if (outputMatch) squadContext += outputMatch[0] + "\n";
|
|
982
|
+
if (contextMatch) squadContext += contextMatch[0] + "\n";
|
|
983
|
+
if (!squadContext && squadContent.length > 0) {
|
|
984
|
+
squadContext = squadContent.substring(0, DEFAULT_FALLBACK_CHARS);
|
|
985
|
+
}
|
|
986
|
+
if (squadContext) {
|
|
987
|
+
const tokens = estimateTokens(squadContext);
|
|
988
|
+
if (estimatedTokens + tokens < maxTokens) {
|
|
989
|
+
sections.push(`## Squad Context (${squadName})
|
|
990
|
+
${squadContext.trim()}`);
|
|
991
|
+
estimatedTokens += tokens;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (memoryDir) {
|
|
998
|
+
const stateFile = join2(memoryDir, squadName, agentName, "state.md");
|
|
999
|
+
if (existsSync2(stateFile)) {
|
|
1000
|
+
try {
|
|
1001
|
+
const stateContent = readFileSync2(stateFile, "utf-8");
|
|
1002
|
+
const tokens = estimateTokens(stateContent);
|
|
1003
|
+
if (estimatedTokens + tokens < maxTokens && stateContent.trim()) {
|
|
1004
|
+
sections.push(`## Your Previous State
|
|
1005
|
+
This is your memory from your last execution:
|
|
1006
|
+
|
|
1007
|
+
${stateContent.trim()}`);
|
|
1008
|
+
estimatedTokens += tokens;
|
|
1009
|
+
}
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (memoryDir) {
|
|
1015
|
+
const briefsDir = join2(memoryDir, squadName, agentName, "briefs");
|
|
1016
|
+
if (existsSync2(briefsDir)) {
|
|
1017
|
+
try {
|
|
1018
|
+
const briefFiles = readdirSync(briefsDir).filter((f) => f.endsWith(".md")).slice(0, MAX_AGENT_BRIEFS);
|
|
1019
|
+
for (const briefFile of briefFiles) {
|
|
1020
|
+
const briefPath = join2(briefsDir, briefFile);
|
|
1021
|
+
const briefContent = readFileSync2(briefPath, "utf-8");
|
|
1022
|
+
const tokens = estimateTokens(briefContent);
|
|
1023
|
+
if (estimatedTokens + tokens < maxTokens) {
|
|
1024
|
+
sections.push(`## Brief: ${briefFile.replace(".md", "")}
|
|
1025
|
+
${briefContent.trim()}`);
|
|
1026
|
+
estimatedTokens += tokens;
|
|
1027
|
+
} else {
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (memoryDir) {
|
|
1036
|
+
const squadBriefsDir = join2(memoryDir, squadName, "_briefs");
|
|
1037
|
+
if (existsSync2(squadBriefsDir)) {
|
|
1038
|
+
try {
|
|
1039
|
+
const squadBriefs = readdirSync(squadBriefsDir).filter((f) => f.endsWith(".md")).slice(0, MAX_SQUAD_BRIEFS);
|
|
1040
|
+
for (const briefFile of squadBriefs) {
|
|
1041
|
+
const briefPath = join2(squadBriefsDir, briefFile);
|
|
1042
|
+
const briefContent = readFileSync2(briefPath, "utf-8");
|
|
1043
|
+
const tokens = estimateTokens(briefContent);
|
|
1044
|
+
if (estimatedTokens + tokens < maxTokens) {
|
|
1045
|
+
sections.push(`## Squad Brief: ${briefFile.replace(".md", "")}
|
|
1046
|
+
${briefContent.trim()}`);
|
|
1047
|
+
estimatedTokens += tokens;
|
|
1048
|
+
} else {
|
|
1049
|
+
break;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
} catch {
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (memoryDir) {
|
|
1057
|
+
const briefingPath = join2(memoryDir, "daily-briefing.md");
|
|
1058
|
+
if (existsSync2(briefingPath)) {
|
|
1059
|
+
try {
|
|
1060
|
+
const briefingContent = readFileSync2(briefingPath, "utf-8");
|
|
1061
|
+
if (briefingContent.trim()) {
|
|
1062
|
+
const tokens = estimateTokens(briefingContent);
|
|
1063
|
+
if (estimatedTokens + tokens < maxTokens) {
|
|
1064
|
+
sections.push(`## Daily Briefing
|
|
1065
|
+
${briefingContent.trim()}`);
|
|
1066
|
+
estimatedTokens += tokens;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
} catch {
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (memoryDir && options.agentPath) {
|
|
1074
|
+
const frontmatter = parseAgentFrontmatter(options.agentPath);
|
|
1075
|
+
if (frontmatter.context_from && frontmatter.context_from.length > 0) {
|
|
1076
|
+
for (const relatedSquad of frontmatter.context_from) {
|
|
1077
|
+
const learningsPath = join2(memoryDir, relatedSquad, "shared", "learnings.md");
|
|
1078
|
+
if (existsSync2(learningsPath)) {
|
|
1079
|
+
try {
|
|
1080
|
+
let learningsContent = readFileSync2(learningsPath, "utf-8");
|
|
1081
|
+
if (learningsContent.trim()) {
|
|
1082
|
+
if (learningsContent.length > MAX_LEARNINGS_CHARS) {
|
|
1083
|
+
learningsContent = learningsContent.slice(0, MAX_LEARNINGS_CHARS) + "\n...(truncated)";
|
|
1084
|
+
}
|
|
1085
|
+
const tokens = estimateTokens(learningsContent);
|
|
1086
|
+
if (estimatedTokens + tokens < maxTokens) {
|
|
1087
|
+
sections.push(`## ${relatedSquad} Squad Learnings
|
|
1088
|
+
${learningsContent.trim()}`);
|
|
1089
|
+
estimatedTokens += tokens;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} catch {
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const leadStatePath = join2(memoryDir, relatedSquad, `${relatedSquad}-lead`, "state.md");
|
|
1096
|
+
if (existsSync2(leadStatePath)) {
|
|
1097
|
+
try {
|
|
1098
|
+
let leadState = readFileSync2(leadStatePath, "utf-8");
|
|
1099
|
+
if (leadState.trim()) {
|
|
1100
|
+
if (leadState.length > MAX_LEAD_STATE_CHARS) {
|
|
1101
|
+
leadState = leadState.slice(0, MAX_LEAD_STATE_CHARS) + "\n...(truncated)";
|
|
1102
|
+
}
|
|
1103
|
+
const tokens = estimateTokens(leadState);
|
|
1104
|
+
if (estimatedTokens + tokens < maxTokens) {
|
|
1105
|
+
sections.push(`## ${relatedSquad} Lead State
|
|
1106
|
+
${leadState.trim()}`);
|
|
1107
|
+
estimatedTokens += tokens;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
} catch {
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (sections.length === 0) {
|
|
1117
|
+
return "";
|
|
1118
|
+
}
|
|
1119
|
+
if (options.verbose) {
|
|
1120
|
+
writeLine(` ${colors.dim}Context: ${sections.length} sections (~${estimatedTokens} tokens)${RESET}`);
|
|
1121
|
+
}
|
|
1122
|
+
return `
|
|
1123
|
+
# EXISTING CONTEXT
|
|
1124
|
+
Build on this existing knowledge - do NOT start from scratch:
|
|
1125
|
+
|
|
1126
|
+
${sections.join("\n\n")}
|
|
1127
|
+
`;
|
|
1128
|
+
}
|
|
1129
|
+
function generateExecutionId() {
|
|
1130
|
+
const timestamp = Date.now().toString(36);
|
|
1131
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
1132
|
+
return `exec_${timestamp}_${random}`;
|
|
1133
|
+
}
|
|
1134
|
+
function selectMcpConfig(squadName, squad) {
|
|
1135
|
+
if (squad?.context?.mcp && squad.context.mcp.length > 0) {
|
|
1136
|
+
return resolveMcpConfigPath(squadName, squad.context.mcp);
|
|
1137
|
+
}
|
|
1138
|
+
const home = process.env.HOME || "";
|
|
1139
|
+
const configsDir = join2(home, ".claude", "mcp-configs");
|
|
1140
|
+
const squadConfigs = {
|
|
1141
|
+
website: "website.json",
|
|
1142
|
+
research: "research.json",
|
|
1143
|
+
intelligence: "research.json",
|
|
1144
|
+
analytics: "data.json",
|
|
1145
|
+
engineering: "data.json"
|
|
1146
|
+
};
|
|
1147
|
+
const configFile = squadConfigs[squadName.toLowerCase()];
|
|
1148
|
+
if (configFile) {
|
|
1149
|
+
const configPath = join2(configsDir, configFile);
|
|
1150
|
+
if (existsSync2(configPath)) {
|
|
1151
|
+
return configPath;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return "";
|
|
1155
|
+
}
|
|
1156
|
+
function detectTaskType(agentName) {
|
|
1157
|
+
const name = agentName.toLowerCase();
|
|
1158
|
+
if (name.includes("eval") || name.includes("critic") || name.includes("review") || name.includes("test")) {
|
|
1159
|
+
return "evaluation";
|
|
1160
|
+
}
|
|
1161
|
+
if (name.includes("lead") || name.includes("orchestrator")) {
|
|
1162
|
+
return "lead";
|
|
1163
|
+
}
|
|
1164
|
+
if (name.includes("research") || name.includes("analyst") || name.includes("intel")) {
|
|
1165
|
+
return "research";
|
|
1166
|
+
}
|
|
1167
|
+
return "execution";
|
|
1168
|
+
}
|
|
1169
|
+
function getClaudeModelAlias(model) {
|
|
1170
|
+
const lower = model.toLowerCase();
|
|
1171
|
+
if (lower === "opus" || lower === "sonnet" || lower === "haiku") {
|
|
1172
|
+
return lower;
|
|
1173
|
+
}
|
|
1174
|
+
if (lower.includes("opus")) return "opus";
|
|
1175
|
+
if (lower.includes("sonnet")) return "sonnet";
|
|
1176
|
+
if (lower.includes("haiku")) return "haiku";
|
|
1177
|
+
return void 0;
|
|
1178
|
+
}
|
|
1179
|
+
function resolveModel(explicitModel, squad, taskType) {
|
|
1180
|
+
if (explicitModel) {
|
|
1181
|
+
return explicitModel;
|
|
1182
|
+
}
|
|
1183
|
+
const modelConfig = squad?.context?.model;
|
|
1184
|
+
if (!modelConfig) {
|
|
1185
|
+
return void 0;
|
|
1186
|
+
}
|
|
1187
|
+
switch (taskType) {
|
|
1188
|
+
case "evaluation":
|
|
1189
|
+
return modelConfig.cheap || modelConfig.default;
|
|
1190
|
+
case "lead":
|
|
1191
|
+
return modelConfig.expensive || modelConfig.default;
|
|
1192
|
+
case "research":
|
|
1193
|
+
case "execution":
|
|
1194
|
+
default:
|
|
1195
|
+
return modelConfig.default;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function ensureProjectTrusted(projectPath) {
|
|
1199
|
+
const configPath = join2(process.env.HOME || "", ".claude.json");
|
|
1200
|
+
if (!existsSync2(configPath)) {
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1205
|
+
if (!config.projects) {
|
|
1206
|
+
config.projects = {};
|
|
1207
|
+
}
|
|
1208
|
+
if (!config.projects[projectPath]) {
|
|
1209
|
+
config.projects[projectPath] = {};
|
|
1210
|
+
}
|
|
1211
|
+
if (!config.projects[projectPath].hasTrustDialogAccepted) {
|
|
1212
|
+
config.projects[projectPath].hasTrustDialogAccepted = true;
|
|
1213
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function getProjectRoot() {
|
|
1219
|
+
const squadsDir = findSquadsDir();
|
|
1220
|
+
if (squadsDir) {
|
|
1221
|
+
return dirname(dirname(squadsDir));
|
|
1222
|
+
}
|
|
1223
|
+
return process.cwd();
|
|
1224
|
+
}
|
|
1225
|
+
function getExecutionLogPath(squadName, agentName) {
|
|
1226
|
+
const memoryDir = findMemoryDir();
|
|
1227
|
+
if (!memoryDir) return null;
|
|
1228
|
+
return join2(memoryDir, squadName, agentName, "executions.md");
|
|
1229
|
+
}
|
|
1230
|
+
function logExecution(record) {
|
|
1231
|
+
const logPath = getExecutionLogPath(record.squadName, record.agentName);
|
|
1232
|
+
if (!logPath) return;
|
|
1233
|
+
const dir = dirname(logPath);
|
|
1234
|
+
if (!existsSync2(dir)) {
|
|
1235
|
+
mkdirSync2(dir, { recursive: true });
|
|
1236
|
+
}
|
|
1237
|
+
let content = "";
|
|
1238
|
+
if (existsSync2(logPath)) {
|
|
1239
|
+
content = readFileSync2(logPath, "utf-8").trimEnd();
|
|
1240
|
+
} else {
|
|
1241
|
+
content = `# ${record.squadName}/${record.agentName} - Execution Log`;
|
|
1242
|
+
}
|
|
1243
|
+
const entry = `
|
|
1244
|
+
|
|
1245
|
+
---
|
|
1246
|
+
<!-- exec:${record.executionId} -->
|
|
1247
|
+
**${record.startTime}** | Status: ${record.status}
|
|
1248
|
+
- ID: \`${record.executionId}\`
|
|
1249
|
+
- Trigger: ${record.trigger || "manual"}
|
|
1250
|
+
- Task Type: ${record.taskType || "execution"}
|
|
1251
|
+
`;
|
|
1252
|
+
writeFileSync2(logPath, content + entry);
|
|
1253
|
+
}
|
|
1254
|
+
function updateExecutionStatus(squadName, agentName, executionId, status, details) {
|
|
1255
|
+
const logPath = getExecutionLogPath(squadName, agentName);
|
|
1256
|
+
if (!logPath || !existsSync2(logPath)) return;
|
|
1257
|
+
let content = readFileSync2(logPath, "utf-8");
|
|
1258
|
+
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1259
|
+
const execMarker = `<!-- exec:${executionId} -->`;
|
|
1260
|
+
const markerIndex = content.indexOf(execMarker);
|
|
1261
|
+
if (markerIndex === -1) return;
|
|
1262
|
+
const nextEntryIndex = content.indexOf("\n---\n", markerIndex + 1);
|
|
1263
|
+
const entryEnd = nextEntryIndex === -1 ? content.length : nextEntryIndex;
|
|
1264
|
+
const entryStart = content.lastIndexOf("\n---\n", markerIndex);
|
|
1265
|
+
const currentEntry = content.slice(entryStart, entryEnd);
|
|
1266
|
+
const durationStr = details?.durationMs ? `${(details.durationMs / 1e3).toFixed(1)}s` : "unknown";
|
|
1267
|
+
let updatedEntry = currentEntry.replace(/Status: running/, `Status: ${status}`) + `- Completed: ${endTime}
|
|
1268
|
+
- Duration: ${durationStr}`;
|
|
1269
|
+
if (details?.outcome) {
|
|
1270
|
+
updatedEntry += `
|
|
1271
|
+
- Outcome: ${details.outcome}`;
|
|
1272
|
+
}
|
|
1273
|
+
if (details?.error) {
|
|
1274
|
+
updatedEntry += `
|
|
1275
|
+
- Error: ${details.error}`;
|
|
1276
|
+
}
|
|
1277
|
+
content = content.slice(0, entryStart) + updatedEntry + content.slice(entryEnd);
|
|
1278
|
+
writeFileSync2(logPath, content);
|
|
1279
|
+
}
|
|
1280
|
+
async function autoCommitAgentWork(squadName, agentName, executionId, provider2) {
|
|
1281
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
1282
|
+
const { detectGitHubRepo } = await import("./github-UQTM5KMS.js");
|
|
1283
|
+
const projectRoot = getProjectRoot();
|
|
1284
|
+
try {
|
|
1285
|
+
const status = execSync3("git status --porcelain", {
|
|
1286
|
+
encoding: "utf-8",
|
|
1287
|
+
cwd: projectRoot
|
|
1288
|
+
}).trim();
|
|
1289
|
+
if (!status) {
|
|
1290
|
+
return { committed: false };
|
|
1291
|
+
}
|
|
1292
|
+
const botEnv = await getBotGitEnv();
|
|
1293
|
+
const execOpts = {
|
|
1294
|
+
cwd: projectRoot,
|
|
1295
|
+
env: { ...process.env, ...botEnv }
|
|
1296
|
+
};
|
|
1297
|
+
execSync3("git add -A", execOpts);
|
|
1298
|
+
const shortExecId = executionId.slice(0, 12);
|
|
1299
|
+
const coAuthor = getCoAuthorTrailer(provider2 || "claude");
|
|
1300
|
+
const msgFile = join2(projectRoot, ".git", "SQUADS_COMMIT_MSG");
|
|
1301
|
+
writeFileSync2(msgFile, `feat(${squadName}/${agentName}): execution ${shortExecId}
|
|
1302
|
+
|
|
1303
|
+
${coAuthor}
|
|
1304
|
+
`);
|
|
1305
|
+
try {
|
|
1306
|
+
execSync3(`git commit --file "${msgFile}"`, execOpts);
|
|
1307
|
+
} finally {
|
|
1308
|
+
try {
|
|
1309
|
+
unlinkSync(msgFile);
|
|
1310
|
+
} catch {
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
try {
|
|
1314
|
+
const { spawnSync } = await import("child_process");
|
|
1315
|
+
const repo = detectGitHubRepo(projectRoot);
|
|
1316
|
+
if (repo && /^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
1317
|
+
const pushUrl = await getBotPushUrl(repo);
|
|
1318
|
+
if (pushUrl) {
|
|
1319
|
+
spawnSync("git", ["push", pushUrl, "HEAD"], { ...execOpts, stdio: "pipe" });
|
|
1320
|
+
} else {
|
|
1321
|
+
spawnSync("git", ["push", "origin", "HEAD"], { ...execOpts, stdio: "pipe" });
|
|
1322
|
+
}
|
|
1323
|
+
} else {
|
|
1324
|
+
spawnSync("git", ["push", "origin", "HEAD"], { ...execOpts, stdio: "pipe" });
|
|
1325
|
+
}
|
|
1326
|
+
} catch {
|
|
1327
|
+
}
|
|
1328
|
+
return { committed: true, message: `Committed changes from ${agentName}` };
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
return { committed: false, error: String(error) };
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function getLastExecutionTime(squadName, agentName) {
|
|
1334
|
+
const logPath = getExecutionLogPath(squadName, agentName);
|
|
1335
|
+
if (!logPath || !existsSync2(logPath)) return null;
|
|
1336
|
+
const content = readFileSync2(logPath, "utf-8");
|
|
1337
|
+
const timestamps = content.match(/\*\*(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\*\*/g);
|
|
1338
|
+
if (!timestamps || timestamps.length === 0) return null;
|
|
1339
|
+
const lastTimestamp = timestamps[timestamps.length - 1].replace(/\*\*/g, "");
|
|
1340
|
+
return new Date(lastTimestamp);
|
|
1341
|
+
}
|
|
1342
|
+
function checkLocalCooldown(squadName, agentName, cooldownMs) {
|
|
1343
|
+
const lastExec = getLastExecutionTime(squadName, agentName);
|
|
1344
|
+
if (!lastExec) return { ok: true, cooldownMs };
|
|
1345
|
+
const elapsedMs = Date.now() - lastExec.getTime();
|
|
1346
|
+
if (elapsedMs < cooldownMs) {
|
|
1347
|
+
return { ok: false, elapsedMs, cooldownMs };
|
|
1348
|
+
}
|
|
1349
|
+
return { ok: true, elapsedMs, cooldownMs };
|
|
1350
|
+
}
|
|
1351
|
+
function formatDuration(ms) {
|
|
1352
|
+
const hours = Math.floor(ms / (60 * 60 * 1e3));
|
|
1353
|
+
const minutes = Math.floor(ms % (60 * 60 * 1e3) / (60 * 1e3));
|
|
1354
|
+
if (hours >= 24) {
|
|
1355
|
+
const days = Math.floor(hours / 24);
|
|
1356
|
+
const remainingHours = hours % 24;
|
|
1357
|
+
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
|
1358
|
+
}
|
|
1359
|
+
if (hours > 0) {
|
|
1360
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
1361
|
+
}
|
|
1362
|
+
return `${minutes}m`;
|
|
1363
|
+
}
|
|
1364
|
+
function extractMcpServersFromDefinition(definition) {
|
|
1365
|
+
const servers = /* @__PURE__ */ new Set();
|
|
1366
|
+
const knownServers = [
|
|
1367
|
+
"chrome-devtools",
|
|
1368
|
+
"firecrawl",
|
|
1369
|
+
"context7",
|
|
1370
|
+
"huggingface"
|
|
1371
|
+
];
|
|
1372
|
+
for (const server of knownServers) {
|
|
1373
|
+
if (definition.toLowerCase().includes(server)) {
|
|
1374
|
+
servers.add(server);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const mcpMatch = definition.match(/mcp:\s*\n((?:\s*-\s*\S+\s*\n?)+)/i);
|
|
1378
|
+
if (mcpMatch) {
|
|
1379
|
+
const lines = mcpMatch[1].split("\n");
|
|
1380
|
+
for (const line of lines) {
|
|
1381
|
+
const serverMatch = line.match(/^\s*-\s*(\S+)/);
|
|
1382
|
+
if (serverMatch) {
|
|
1383
|
+
servers.add(serverMatch[1]);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
return Array.from(servers);
|
|
1388
|
+
}
|
|
1389
|
+
function parseAgentFrontmatter(agentPath) {
|
|
1390
|
+
if (!existsSync2(agentPath)) return {};
|
|
1391
|
+
const content = readFileSync2(agentPath, "utf-8");
|
|
1392
|
+
const lines = content.split("\n");
|
|
1393
|
+
let inFrontmatter = false;
|
|
1394
|
+
const yamlLines = [];
|
|
1395
|
+
for (const line of lines) {
|
|
1396
|
+
if (line.trim() === "---") {
|
|
1397
|
+
if (inFrontmatter) break;
|
|
1398
|
+
inFrontmatter = true;
|
|
1399
|
+
continue;
|
|
1400
|
+
}
|
|
1401
|
+
if (inFrontmatter) {
|
|
1402
|
+
yamlLines.push(line);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (yamlLines.length === 0) return {};
|
|
1406
|
+
const yaml = yamlLines.join("\n");
|
|
1407
|
+
const result = {};
|
|
1408
|
+
const contextMatch = yaml.match(/context_from:\s*\[([^\]]+)\]/);
|
|
1409
|
+
if (contextMatch) {
|
|
1410
|
+
result.context_from = contextMatch[1].split(",").map((s) => s.trim());
|
|
1411
|
+
}
|
|
1412
|
+
const criteriaMatch = yaml.match(/acceptance_criteria:\s*\|\n((?:\s+.+\n?)*)/);
|
|
1413
|
+
if (criteriaMatch) {
|
|
1414
|
+
result.acceptance_criteria = criteriaMatch[1].replace(/^ {2}/gm, "").trim();
|
|
1415
|
+
}
|
|
1416
|
+
const retriesMatch = yaml.match(/max_retries:\s*(\d+)/);
|
|
1417
|
+
if (retriesMatch) {
|
|
1418
|
+
result.max_retries = parseInt(retriesMatch[1], 10);
|
|
1419
|
+
}
|
|
1420
|
+
const cooldownMatch = yaml.match(/cooldown:\s*["']?([^"'\n]+)["']?/);
|
|
1421
|
+
if (cooldownMatch) {
|
|
1422
|
+
result.cooldown = cooldownMatch[1].trim();
|
|
1423
|
+
}
|
|
1424
|
+
return result;
|
|
1425
|
+
}
|
|
1426
|
+
async function emitExecutionEvent(eventType, data) {
|
|
1427
|
+
const apiUrl = getApiUrl();
|
|
1428
|
+
if (apiUrl) {
|
|
1429
|
+
try {
|
|
1430
|
+
await fetch(`${apiUrl}/events/ingest`, {
|
|
1431
|
+
method: "POST",
|
|
1432
|
+
headers: { "Content-Type": "application/json" },
|
|
1433
|
+
body: JSON.stringify({
|
|
1434
|
+
source: "scheduler",
|
|
1435
|
+
event_type: eventType,
|
|
1436
|
+
data: {
|
|
1437
|
+
squad: data.squad,
|
|
1438
|
+
agent: data.agent,
|
|
1439
|
+
execution_id: data.executionId,
|
|
1440
|
+
...data.error ? { error: data.error } : {}
|
|
1441
|
+
}
|
|
1442
|
+
}),
|
|
1443
|
+
signal: AbortSignal.timeout(EXECUTION_EVENT_TIMEOUT_MS)
|
|
1444
|
+
});
|
|
1445
|
+
return;
|
|
1446
|
+
} catch {
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
const memDir = findMemoryDir();
|
|
1451
|
+
if (!memDir) return;
|
|
1452
|
+
const eventsDir = join2(memDir, data.squad, data.agent);
|
|
1453
|
+
if (!existsSync2(eventsDir)) {
|
|
1454
|
+
mkdirSync2(eventsDir, { recursive: true });
|
|
1455
|
+
}
|
|
1456
|
+
const eventsPath = join2(eventsDir, "events.md");
|
|
1457
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1458
|
+
const entry = `
|
|
1459
|
+
## ${timestamp}: ${eventType}
|
|
1460
|
+
- execution_id: ${data.executionId}
|
|
1461
|
+
${data.error ? `- error: ${data.error}
|
|
1462
|
+
` : ""}`;
|
|
1463
|
+
let existing = "";
|
|
1464
|
+
if (existsSync2(eventsPath)) {
|
|
1465
|
+
existing = readFileSync2(eventsPath, "utf-8");
|
|
1466
|
+
}
|
|
1467
|
+
writeFileSync2(eventsPath, existing + entry);
|
|
1468
|
+
} catch {
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
async function verifyExecution(squadName, agentName, criteria, options = {}) {
|
|
1472
|
+
const { execSync: execSync3 } = await import("child_process");
|
|
1473
|
+
const projectRoot = getProjectRoot();
|
|
1474
|
+
let stateContent = "";
|
|
1475
|
+
const memDir = findMemoryDir();
|
|
1476
|
+
if (memDir) {
|
|
1477
|
+
const statePath = join2(memDir, squadName, agentName, "state.md");
|
|
1478
|
+
if (existsSync2(statePath)) {
|
|
1479
|
+
stateContent = readFileSync2(statePath, "utf-8").slice(0, VERIFICATION_STATE_MAX_CHARS);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
let recentCommits = "";
|
|
1483
|
+
try {
|
|
1484
|
+
recentCommits = execSync3("git log --oneline -5 --no-color", {
|
|
1485
|
+
encoding: "utf-8",
|
|
1486
|
+
cwd: projectRoot
|
|
1487
|
+
}).trim();
|
|
1488
|
+
} catch {
|
|
1489
|
+
recentCommits = "(no commits found)";
|
|
1490
|
+
}
|
|
1491
|
+
const verifyPrompt = `You are verifying whether an agent completed its task successfully.
|
|
1492
|
+
|
|
1493
|
+
Agent: ${squadName}/${agentName}
|
|
1494
|
+
|
|
1495
|
+
## Acceptance Criteria
|
|
1496
|
+
${criteria}
|
|
1497
|
+
|
|
1498
|
+
## Evidence
|
|
1499
|
+
|
|
1500
|
+
### Agent State File
|
|
1501
|
+
${stateContent || "(empty or not found)"}
|
|
1502
|
+
|
|
1503
|
+
### Recent Git Commits
|
|
1504
|
+
${recentCommits}
|
|
1505
|
+
|
|
1506
|
+
## Instructions
|
|
1507
|
+
Evaluate whether the acceptance criteria are met based on the evidence.
|
|
1508
|
+
Respond with EXACTLY one line:
|
|
1509
|
+
PASS: <brief reason>
|
|
1510
|
+
or
|
|
1511
|
+
FAIL: <brief reason>`;
|
|
1512
|
+
try {
|
|
1513
|
+
const escapedPrompt = verifyPrompt.replace(/'/g, "'\\''");
|
|
1514
|
+
const result = execSync3(
|
|
1515
|
+
`claude --print --model haiku -- '${escapedPrompt}'`,
|
|
1516
|
+
{ encoding: "utf-8", cwd: projectRoot, timeout: VERIFICATION_EXEC_TIMEOUT_MS }
|
|
1517
|
+
).trim();
|
|
1518
|
+
if (options.verbose) {
|
|
1519
|
+
writeLine(` ${colors.dim}Verification: ${result}${RESET}`);
|
|
1520
|
+
}
|
|
1521
|
+
if (result.startsWith("PASS")) {
|
|
1522
|
+
return { passed: true, reason: result.replace(/^PASS:\s*/, "") };
|
|
1523
|
+
}
|
|
1524
|
+
return { passed: false, reason: result.replace(/^FAIL:\s*/, "") };
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
if (options.verbose) {
|
|
1527
|
+
writeLine(` ${colors.dim}Verification error (defaulting to PASS): ${error}${RESET}`);
|
|
1528
|
+
}
|
|
1529
|
+
return { passed: true, reason: "Verification unavailable \u2014 defaulting to pass" };
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
async function runCloudDispatch(squadName, agentName, options) {
|
|
1533
|
+
const apiUrl = getApiUrl();
|
|
1534
|
+
if (!apiUrl) {
|
|
1535
|
+
writeLine(` ${colors.red}${icons.error} API URL not configured${RESET}`);
|
|
1536
|
+
writeLine(` ${colors.dim}Run: squads config use staging (or set SQUADS_API_URL)${RESET}`);
|
|
1537
|
+
process.exit(1);
|
|
1538
|
+
}
|
|
1539
|
+
if (!isLoggedIn()) {
|
|
1540
|
+
writeLine(` ${colors.red}${icons.error} Not logged in${RESET}`);
|
|
1541
|
+
writeLine(` ${colors.dim}Run \`squads login\` to authenticate before using --cloud${RESET}`);
|
|
1542
|
+
process.exit(1);
|
|
1543
|
+
}
|
|
1544
|
+
const session = loadSession();
|
|
1545
|
+
const headers = {
|
|
1546
|
+
"Content-Type": "application/json"
|
|
1547
|
+
};
|
|
1548
|
+
if (session?.accessToken) {
|
|
1549
|
+
headers["Authorization"] = `Bearer ${session.accessToken}`;
|
|
1550
|
+
}
|
|
1551
|
+
const apiKey = process.env.SQUADS_PLATFORM_API_TOKEN || process.env.SCHEDULER_API_KEY;
|
|
1552
|
+
if (apiKey) {
|
|
1553
|
+
headers["X-API-Key"] = apiKey;
|
|
1554
|
+
}
|
|
1555
|
+
const spinner = ora(`Dispatching ${squadName}/${agentName} to cloud...`).start();
|
|
1556
|
+
try {
|
|
1557
|
+
const dispatchRes = await fetch(`${apiUrl}/agent-dispatch`, {
|
|
1558
|
+
method: "POST",
|
|
1559
|
+
headers,
|
|
1560
|
+
body: JSON.stringify({
|
|
1561
|
+
squad: squadName,
|
|
1562
|
+
agent: agentName,
|
|
1563
|
+
trigger_type: "manual",
|
|
1564
|
+
trigger_data: {
|
|
1565
|
+
source: "cli",
|
|
1566
|
+
cloud: true,
|
|
1567
|
+
model: options.model,
|
|
1568
|
+
provider: options.provider,
|
|
1569
|
+
effort: options.effort
|
|
1570
|
+
}
|
|
1571
|
+
})
|
|
1572
|
+
});
|
|
1573
|
+
if (!dispatchRes.ok) {
|
|
1574
|
+
const error = await dispatchRes.text();
|
|
1575
|
+
spinner.fail(`Dispatch failed: ${dispatchRes.status}`);
|
|
1576
|
+
writeLine(` ${colors.dim}${error}${RESET}`);
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
}
|
|
1579
|
+
const dispatch = await dispatchRes.json();
|
|
1580
|
+
spinner.succeed(`Dispatched to cloud`);
|
|
1581
|
+
writeLine();
|
|
1582
|
+
writeLine(` ${colors.cyan}Dispatch ID${RESET} ${dispatch.dispatch_id}`);
|
|
1583
|
+
writeLine(` ${colors.cyan}Squad${RESET} ${squadName}`);
|
|
1584
|
+
writeLine(` ${colors.cyan}Agent${RESET} ${agentName}`);
|
|
1585
|
+
writeLine();
|
|
1586
|
+
const pollSpinner = ora("Waiting for execution to start...").start();
|
|
1587
|
+
const pollStart = Date.now();
|
|
1588
|
+
let executionId = null;
|
|
1589
|
+
let lastStatus = "";
|
|
1590
|
+
while (Date.now() - pollStart < CLOUD_POLL_TIMEOUT_MS) {
|
|
1591
|
+
try {
|
|
1592
|
+
const execRes = await fetch(
|
|
1593
|
+
`${apiUrl}/agent-executions?squad=${encodeURIComponent(squadName)}&agent=${encodeURIComponent(agentName)}&limit=1`,
|
|
1594
|
+
{ headers }
|
|
1595
|
+
);
|
|
1596
|
+
if (execRes.ok) {
|
|
1597
|
+
const executions = await execRes.json();
|
|
1598
|
+
if (executions.length > 0) {
|
|
1599
|
+
const exec2 = executions[0];
|
|
1600
|
+
if (!executionId && exec2.status === "running") {
|
|
1601
|
+
executionId = exec2.execution_id;
|
|
1602
|
+
pollSpinner.text = `Running (${exec2.execution_id})`;
|
|
1603
|
+
}
|
|
1604
|
+
if (executionId && exec2.execution_id === executionId) {
|
|
1605
|
+
if (exec2.status !== lastStatus) {
|
|
1606
|
+
lastStatus = exec2.status;
|
|
1607
|
+
pollSpinner.text = `Status: ${exec2.status}`;
|
|
1608
|
+
}
|
|
1609
|
+
if (exec2.status === "completed") {
|
|
1610
|
+
pollSpinner.succeed("Execution completed");
|
|
1611
|
+
writeLine();
|
|
1612
|
+
writeLine(` ${colors.cyan}Execution${RESET} ${exec2.execution_id}`);
|
|
1613
|
+
if (exec2.summary) {
|
|
1614
|
+
writeLine(` ${colors.cyan}Summary${RESET} ${exec2.summary}`);
|
|
1615
|
+
}
|
|
1616
|
+
if (exec2.duration_seconds) {
|
|
1617
|
+
writeLine(` ${colors.cyan}Duration${RESET} ${Math.round(exec2.duration_seconds)}s`);
|
|
1618
|
+
}
|
|
1619
|
+
if (exec2.cost_usd) {
|
|
1620
|
+
writeLine(` ${colors.cyan}Cost${RESET} $${exec2.cost_usd.toFixed(4)}`);
|
|
1621
|
+
}
|
|
1622
|
+
writeLine();
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (exec2.status === "failed") {
|
|
1626
|
+
pollSpinner.fail("Execution failed");
|
|
1627
|
+
writeLine();
|
|
1628
|
+
if (exec2.error) {
|
|
1629
|
+
writeLine(` ${colors.red}Error: ${exec2.error}${RESET}`);
|
|
1630
|
+
}
|
|
1631
|
+
writeLine();
|
|
1632
|
+
process.exit(1);
|
|
1633
|
+
}
|
|
1634
|
+
if (exec2.status === "cancelled") {
|
|
1635
|
+
pollSpinner.warn("Execution cancelled");
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
} catch {
|
|
1642
|
+
}
|
|
1643
|
+
await new Promise((resolve) => setTimeout(resolve, CLOUD_POLL_INTERVAL_MS));
|
|
1644
|
+
}
|
|
1645
|
+
pollSpinner.warn("Poll timeout \u2014 execution may still be running");
|
|
1646
|
+
writeLine(` ${colors.dim}Check status: squads trigger status${RESET}`);
|
|
1647
|
+
if (executionId) {
|
|
1648
|
+
writeLine(` ${colors.dim}Execution ID: ${executionId}${RESET}`);
|
|
1649
|
+
}
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
spinner.fail("Cloud dispatch failed");
|
|
1652
|
+
writeLine(` ${colors.red}${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
1653
|
+
writeLine();
|
|
1654
|
+
writeLine(` ${colors.dim}Check your network and SQUADS_API_URL setting${RESET}`);
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
async function runCommand(target, options) {
|
|
1659
|
+
const squadsDir = findSquadsDir();
|
|
1660
|
+
if (!squadsDir) {
|
|
1661
|
+
writeLine(` ${colors.red}No .agents/squads directory found${RESET}`);
|
|
1662
|
+
writeLine(` ${colors.dim}Run \`squads init\` to create one.${RESET}`);
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
if (!options.dryRun && options.execute === void 0) {
|
|
1666
|
+
options.execute = true;
|
|
1667
|
+
}
|
|
1668
|
+
let squadName = target;
|
|
1669
|
+
let agentFromSlash;
|
|
1670
|
+
if (target.includes("/")) {
|
|
1671
|
+
const parts = target.split("/");
|
|
1672
|
+
squadName = parts[0];
|
|
1673
|
+
agentFromSlash = parts[1];
|
|
1674
|
+
if (!options.agent) {
|
|
1675
|
+
options.agent = agentFromSlash;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (options.cloud) {
|
|
1679
|
+
const agentName = options.agent || agentFromSlash;
|
|
1680
|
+
if (!agentName) {
|
|
1681
|
+
writeLine(` ${colors.red}${icons.error} --cloud requires a specific agent${RESET}`);
|
|
1682
|
+
writeLine(` ${colors.dim}Usage: squads run ${squadName} --cloud -a <agent>${RESET}`);
|
|
1683
|
+
writeLine(` ${colors.dim} or: squads run ${squadName}/<agent> --cloud${RESET}`);
|
|
1684
|
+
process.exit(1);
|
|
1685
|
+
}
|
|
1686
|
+
await track(Events.CLI_RUN, { type: "cloud", target: `${squadName}/${agentName}` });
|
|
1687
|
+
await flushEvents();
|
|
1688
|
+
await runCloudDispatch(squadName, agentName, options);
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
const squad = loadSquad(squadName);
|
|
1692
|
+
if (options.execute && !options.dryRun) {
|
|
1693
|
+
const provider2 = options.provider || squad?.providers?.default || "anthropic";
|
|
1694
|
+
const checksOk = await preflightExecutorCheck(provider2);
|
|
1695
|
+
if (!checksOk) {
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (squad) {
|
|
1700
|
+
await track(Events.CLI_RUN, { type: "squad", target: squad.name });
|
|
1701
|
+
await flushEvents();
|
|
1702
|
+
await runSquad(squad, squadsDir, options);
|
|
1703
|
+
} else {
|
|
1704
|
+
const agents = listAgents(squadsDir);
|
|
1705
|
+
const agent = agents.find((a) => a.name === target);
|
|
1706
|
+
if (agent && agent.filePath) {
|
|
1707
|
+
const pathParts = agent.filePath.split("/");
|
|
1708
|
+
const squadIdx = pathParts.indexOf("squads");
|
|
1709
|
+
const squadName2 = squadIdx >= 0 ? pathParts[squadIdx + 1] : "unknown";
|
|
1710
|
+
await runAgent(agent.name, agent.filePath, squadName2, options);
|
|
1711
|
+
} else {
|
|
1712
|
+
writeLine(` ${colors.red}Squad or agent "${target}" not found${RESET}`);
|
|
1713
|
+
writeLine(` ${colors.dim}Run \`squads list\` to see available squads and agents.${RESET}`);
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
async function runSquad(squad, squadsDir, options) {
|
|
1719
|
+
if (!squad) return;
|
|
1720
|
+
if (!options.effort && squad.effort) {
|
|
1721
|
+
options.effort = squad.effort;
|
|
1722
|
+
}
|
|
1723
|
+
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
1724
|
+
writeLine();
|
|
1725
|
+
writeLine(` ${gradient("squads")} ${colors.dim}run${RESET} ${colors.cyan}${squad.name}${RESET}`);
|
|
1726
|
+
writeLine();
|
|
1727
|
+
if (squad.mission) {
|
|
1728
|
+
writeLine(` ${colors.dim}${squad.mission}${RESET}`);
|
|
1729
|
+
writeLine();
|
|
1730
|
+
}
|
|
1731
|
+
writeLine(` ${colors.dim}Started: ${startTime}${RESET}`);
|
|
1732
|
+
writeLine();
|
|
1733
|
+
if (options.lead) {
|
|
1734
|
+
await runLeadMode(squad, squadsDir, options);
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (options.parallel) {
|
|
1738
|
+
const agentFiles = squad.agents.map((a) => ({
|
|
1739
|
+
name: a.name,
|
|
1740
|
+
path: join2(squadsDir, squad.dir, `${a.name}.md`)
|
|
1741
|
+
})).filter((a) => existsSync2(a.path));
|
|
1742
|
+
if (agentFiles.length === 0) {
|
|
1743
|
+
writeLine(` ${icons.error} ${colors.red}No agent files found${RESET}`);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
writeLine(` ${bold}Parallel execution${RESET} ${colors.dim}${agentFiles.length} agents${RESET}`);
|
|
1747
|
+
writeLine();
|
|
1748
|
+
if (!options.execute) {
|
|
1749
|
+
for (const agent of agentFiles) {
|
|
1750
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET}`);
|
|
1751
|
+
}
|
|
1752
|
+
writeLine();
|
|
1753
|
+
writeLine(` ${colors.dim}Launch all agents in parallel:${RESET}`);
|
|
1754
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --parallel`);
|
|
1755
|
+
writeLine();
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
writeLine(` ${gradient("Launching")} ${agentFiles.length} agents in parallel...`);
|
|
1759
|
+
writeLine();
|
|
1760
|
+
const launches = agentFiles.map(
|
|
1761
|
+
(agent) => runAgent(agent.name, agent.path, squad.dir, options)
|
|
1762
|
+
);
|
|
1763
|
+
await Promise.all(launches);
|
|
1764
|
+
writeLine();
|
|
1765
|
+
writeLine(` ${icons.success} All ${agentFiles.length} agents launched`);
|
|
1766
|
+
writeLine(` ${colors.dim}Monitor: tmux ls | grep squads-${squad.name}${RESET}`);
|
|
1767
|
+
writeLine(` ${colors.dim}Attach: tmux attach -t <session>${RESET}`);
|
|
1768
|
+
writeLine();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
if (squad.pipelines.length > 0) {
|
|
1772
|
+
const pipeline = squad.pipelines[0];
|
|
1773
|
+
writeLine(` ${bold}Pipeline${RESET} ${colors.dim}${pipeline.agents.join(" \u2192 ")}${RESET}`);
|
|
1774
|
+
writeLine();
|
|
1775
|
+
for (let i = 0; i < pipeline.agents.length; i++) {
|
|
1776
|
+
const agentName = pipeline.agents[i];
|
|
1777
|
+
const agentPath = join2(squadsDir, squad.dir, `${agentName}.md`);
|
|
1778
|
+
if (existsSync2(agentPath)) {
|
|
1779
|
+
writeLine(` ${colors.dim}[${i + 1}/${pipeline.agents.length}]${RESET}`);
|
|
1780
|
+
await runAgent(agentName, agentPath, squad.dir, options);
|
|
1781
|
+
writeLine();
|
|
1782
|
+
} else {
|
|
1783
|
+
writeLine(` ${icons.warning} ${colors.yellow}Agent ${agentName} not found, skipping${RESET}`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
} else {
|
|
1787
|
+
if (options.agent) {
|
|
1788
|
+
const agentPath = join2(squadsDir, squad.dir, `${options.agent}.md`);
|
|
1789
|
+
if (existsSync2(agentPath)) {
|
|
1790
|
+
await runAgent(options.agent, agentPath, squad.dir, options);
|
|
1791
|
+
} else {
|
|
1792
|
+
writeLine(` ${icons.error} ${colors.red}Agent ${options.agent} not found${RESET}`);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
} else {
|
|
1796
|
+
if (options.execute) {
|
|
1797
|
+
writeLine(` ${bold}Conversation mode${RESET} ${colors.dim}(lead \u2192 scan \u2192 work \u2192 review \u2192 verify)${RESET}`);
|
|
1798
|
+
writeLine();
|
|
1799
|
+
const convOptions = {
|
|
1800
|
+
task: options.task,
|
|
1801
|
+
maxTurns: options.maxTurns,
|
|
1802
|
+
costCeiling: options.costCeiling,
|
|
1803
|
+
verbose: options.verbose,
|
|
1804
|
+
model: options.model
|
|
1805
|
+
};
|
|
1806
|
+
const apiExecId = await reportExecutionStart(squad.name, "conversation", `conv-${Date.now()}`);
|
|
1807
|
+
const result = await runConversation(squad, convOptions);
|
|
1808
|
+
const transcriptPath = saveTranscript(result.transcript);
|
|
1809
|
+
if (apiExecId) {
|
|
1810
|
+
reportConversationResult(apiExecId, {
|
|
1811
|
+
turnCount: result.turnCount,
|
|
1812
|
+
totalCost: result.totalCost,
|
|
1813
|
+
converged: result.converged,
|
|
1814
|
+
reason: result.reason,
|
|
1815
|
+
agentsInvolved: [...new Set(result.transcript.turns.map((t) => t.agent))]
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
pushCognitionSignal({
|
|
1819
|
+
source: "execution",
|
|
1820
|
+
signal_type: result.converged ? "conversation_converged" : "conversation_stopped",
|
|
1821
|
+
value: result.totalCost,
|
|
1822
|
+
unit: "usd",
|
|
1823
|
+
data: {
|
|
1824
|
+
turn_count: result.turnCount,
|
|
1825
|
+
converged: result.converged,
|
|
1826
|
+
reason: result.reason,
|
|
1827
|
+
agents_involved: [...new Set(result.transcript.turns.map((t) => t.agent))]
|
|
1828
|
+
},
|
|
1829
|
+
entity_type: "squad",
|
|
1830
|
+
entity_id: squad.name,
|
|
1831
|
+
confidence: 0.9
|
|
1832
|
+
});
|
|
1833
|
+
writeLine();
|
|
1834
|
+
writeLine(` ${result.converged ? icons.success : icons.warning} ${result.converged ? "Converged" : "Stopped"}: ${result.reason}`);
|
|
1835
|
+
writeLine(` ${colors.dim}Turns: ${result.turnCount} | Cost: ~$${result.totalCost.toFixed(2)}${RESET}`);
|
|
1836
|
+
if (transcriptPath) {
|
|
1837
|
+
writeLine(` ${colors.dim}Transcript: ${transcriptPath}${RESET}`);
|
|
1838
|
+
}
|
|
1839
|
+
writeLine();
|
|
1840
|
+
} else {
|
|
1841
|
+
writeLine(` ${colors.dim}Default mode: conversation (lead \u2192 scan \u2192 work \u2192 review \u2192 verify)${RESET}`);
|
|
1842
|
+
writeLine();
|
|
1843
|
+
for (const agent of squad.agents) {
|
|
1844
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET} ${colors.dim}${agent.role}${RESET}`);
|
|
1845
|
+
}
|
|
1846
|
+
writeLine();
|
|
1847
|
+
writeLine(` ${colors.dim}Run conversation:${RESET}`);
|
|
1848
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET}`);
|
|
1849
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --task "review and merge open PRs"`);
|
|
1850
|
+
writeLine();
|
|
1851
|
+
writeLine(` ${colors.dim}Run single agent:${RESET}`);
|
|
1852
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} -a ${colors.cyan}<agent>${RESET}`);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
writeLine();
|
|
1857
|
+
writeLine(` ${colors.dim}After execution, record outcome:${RESET}`);
|
|
1858
|
+
writeLine(` ${colors.dim}$${RESET} squads feedback add ${colors.cyan}${squad.name}${RESET} ${colors.cyan}<1-5>${RESET} ${colors.cyan}"<feedback>"${RESET}`);
|
|
1859
|
+
writeLine();
|
|
1860
|
+
}
|
|
1861
|
+
async function runLeadMode(squad, squadsDir, options) {
|
|
1862
|
+
if (!squad) return;
|
|
1863
|
+
const agentFiles = squad.agents.map((a) => ({
|
|
1864
|
+
name: a.name,
|
|
1865
|
+
path: join2(squadsDir, squad.dir, `${a.name}.md`),
|
|
1866
|
+
role: a.role || ""
|
|
1867
|
+
})).filter((a) => existsSync2(a.path));
|
|
1868
|
+
if (agentFiles.length === 0) {
|
|
1869
|
+
writeLine(` ${icons.error} ${colors.red}No agent files found${RESET}`);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
writeLine(` ${bold}Lead mode${RESET} ${colors.dim}orchestrating ${agentFiles.length} agents${RESET}`);
|
|
1873
|
+
writeLine();
|
|
1874
|
+
for (const agent of agentFiles) {
|
|
1875
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET} ${colors.dim}${agent.role}${RESET}`);
|
|
1876
|
+
}
|
|
1877
|
+
writeLine();
|
|
1878
|
+
if (!options.execute) {
|
|
1879
|
+
writeLine(` ${colors.dim}Launch lead session:${RESET}`);
|
|
1880
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --lead`);
|
|
1881
|
+
writeLine();
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const timeoutMins = options.timeout || DEFAULT_TIMEOUT_MINUTES;
|
|
1885
|
+
const agentList = agentFiles.map((a) => `- ${a.name}: ${a.role}`).join("\n");
|
|
1886
|
+
const agentPaths = agentFiles.map((a) => `- ${a.name}: ${a.path}`).join("\n");
|
|
1887
|
+
const prompt = `You are the Lead of the ${squad.name} squad.
|
|
1888
|
+
|
|
1889
|
+
## Mission
|
|
1890
|
+
${squad.mission || "Execute squad operations efficiently."}
|
|
1891
|
+
|
|
1892
|
+
## Available Agents
|
|
1893
|
+
${agentList}
|
|
1894
|
+
|
|
1895
|
+
## Agent Definition Files
|
|
1896
|
+
${agentPaths}
|
|
1897
|
+
|
|
1898
|
+
## Your Role as Lead
|
|
1899
|
+
|
|
1900
|
+
1. **Assess the situation**: Check for pending work:
|
|
1901
|
+
- Run \`gh issue list --repo agents-squads/hq --label squad:${squad.name}\` for assigned issues
|
|
1902
|
+
- Check .agents/memory/${squad.dir}/ for squad state and pending tasks
|
|
1903
|
+
- Review recent activity with \`git log --oneline -10\`
|
|
1904
|
+
|
|
1905
|
+
2. **Delegate work using Task tool**: For each piece of work:
|
|
1906
|
+
- Use the Task tool with subagent_type="general-purpose"
|
|
1907
|
+
- Include the agent definition file path in the prompt
|
|
1908
|
+
- Spawn multiple Task agents IN PARALLEL when work is independent
|
|
1909
|
+
- Example: "Read ${agentFiles[0]?.path || "agent.md"} and execute its instructions for [specific task]"
|
|
1910
|
+
|
|
1911
|
+
3. **Coordinate parallel execution**:
|
|
1912
|
+
- Independent tasks \u2192 spawn Task agents in parallel (single message, multiple tool calls)
|
|
1913
|
+
- Dependent tasks \u2192 run sequentially
|
|
1914
|
+
- Monitor progress and handle failures
|
|
1915
|
+
|
|
1916
|
+
4. **Report and update memory**:
|
|
1917
|
+
- Update .agents/memory/${squad.dir}/state.md with completed work
|
|
1918
|
+
- Log learnings to learnings.md
|
|
1919
|
+
- Create issues for follow-up work if needed
|
|
1920
|
+
|
|
1921
|
+
## Time Budget
|
|
1922
|
+
You have ${timeoutMins} minutes. Prioritize high-impact work.
|
|
1923
|
+
|
|
1924
|
+
## Critical Instructions
|
|
1925
|
+
- Use Task tool for delegation, NOT direct execution of agent work
|
|
1926
|
+
- Spawn parallel Task agents when work is independent
|
|
1927
|
+
- When done, type /exit to end the session
|
|
1928
|
+
- Do NOT wait for user input - work autonomously
|
|
1929
|
+
|
|
1930
|
+
## Async Mode (CRITICAL)
|
|
1931
|
+
This is ASYNC execution - Task agents must be fully autonomous:
|
|
1932
|
+
- **Findings** \u2192 Create GitHub issues (gh issue create)
|
|
1933
|
+
- **Code changes** \u2192 Create PRs (gh pr create)
|
|
1934
|
+
- **Analysis results** \u2192 Write to .agents/outputs/ or memory files
|
|
1935
|
+
- **NEVER wait for human review** - complete the work and move on
|
|
1936
|
+
- **NEVER ask clarifying questions** - make reasonable decisions
|
|
1937
|
+
|
|
1938
|
+
Instruct each Task agent: "Work autonomously. Output findings to GitHub issues. Output code changes as PRs. Do not wait for review."
|
|
1939
|
+
|
|
1940
|
+
Begin by assessing pending work, then delegate to agents via Task tool.`;
|
|
1941
|
+
const claudeAvailable = await checkClaudeCliAvailable();
|
|
1942
|
+
if (!claudeAvailable) {
|
|
1943
|
+
writeLine(` ${colors.yellow}Claude CLI not found${RESET}`);
|
|
1944
|
+
writeLine(` ${colors.dim}Install: npm install -g @anthropic-ai/claude-code${RESET}`);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
const isBackground = options.background === true && !options.watch;
|
|
1948
|
+
const isWatch = options.watch === true;
|
|
1949
|
+
const isForeground = !isBackground && !isWatch;
|
|
1950
|
+
const modeText = isBackground ? " (background)" : isWatch ? " (watch)" : "";
|
|
1951
|
+
writeLine(` ${gradient("Launching")} lead session${modeText}...`);
|
|
1952
|
+
writeLine();
|
|
1953
|
+
try {
|
|
1954
|
+
const leadAgentName = agentFiles.find((a) => a.name.includes("lead"))?.name || `${squad.dir}-lead`;
|
|
1955
|
+
const result = await executeWithClaude(prompt, {
|
|
1956
|
+
verbose: options.verbose,
|
|
1957
|
+
timeoutMinutes: timeoutMins,
|
|
1958
|
+
foreground: options.foreground,
|
|
1959
|
+
background: options.background,
|
|
1960
|
+
watch: options.watch,
|
|
1961
|
+
useApi: options.useApi,
|
|
1962
|
+
effort: options.effort,
|
|
1963
|
+
skills: options.skills,
|
|
1964
|
+
trigger: options.trigger || "manual",
|
|
1965
|
+
squadName: squad.dir,
|
|
1966
|
+
agentName: leadAgentName,
|
|
1967
|
+
model: options.model
|
|
1968
|
+
});
|
|
1969
|
+
if (isForeground || isWatch) {
|
|
1970
|
+
writeLine();
|
|
1971
|
+
writeLine(` ${icons.success} Lead session completed`);
|
|
1972
|
+
} else {
|
|
1973
|
+
writeLine(` ${icons.success} Lead session launched in background`);
|
|
1974
|
+
writeLine(` ${colors.dim}${result}${RESET}`);
|
|
1975
|
+
writeLine();
|
|
1976
|
+
writeLine(` ${colors.dim}The lead will:${RESET}`);
|
|
1977
|
+
writeLine(` ${colors.dim} 1. Assess pending work (issues, memory)${RESET}`);
|
|
1978
|
+
writeLine(` ${colors.dim} 2. Spawn Task agents for parallel execution${RESET}`);
|
|
1979
|
+
writeLine(` ${colors.dim} 3. Coordinate and report results${RESET}`);
|
|
1980
|
+
writeLine();
|
|
1981
|
+
writeLine(` ${colors.dim}Monitor: squads workers${RESET}`);
|
|
1982
|
+
}
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
writeLine(` ${icons.error} ${colors.red}Failed to launch: ${error}${RESET}`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
async function runAgent(agentName, agentPath, squadName, options) {
|
|
1988
|
+
const spinner = ora(`Running agent: ${agentName}`).start();
|
|
1989
|
+
const startMs = Date.now();
|
|
1990
|
+
const startTime = new Date(startMs).toISOString();
|
|
1991
|
+
const executionId = generateExecutionId();
|
|
1992
|
+
const taskType = detectTaskType(agentName);
|
|
1993
|
+
const definition = loadAgentDefinition(agentPath);
|
|
1994
|
+
const learnings = await fetchLearnings(squadName);
|
|
1995
|
+
const learningContext = learnings.length > 0 ? `
|
|
1996
|
+
## Learnings from Previous Runs
|
|
1997
|
+
${learnings.map((l) => `- ${l.content}`).join("\n")}
|
|
1998
|
+
` : "";
|
|
1999
|
+
if (options.dryRun) {
|
|
2000
|
+
spinner.info(`[DRY RUN] Would run ${agentName}`);
|
|
2001
|
+
const dryRunContext = gatherSquadContext(squadName, agentName, { verbose: options.verbose, agentPath });
|
|
2002
|
+
if (options.verbose) {
|
|
2003
|
+
writeLine(` ${colors.dim}Agent definition:${RESET}`);
|
|
2004
|
+
writeLine(` ${colors.dim}${definition.slice(0, DRYRUN_DEF_MAX_CHARS)}...${RESET}`);
|
|
2005
|
+
if (learnings.length > 0) {
|
|
2006
|
+
writeLine(` ${colors.dim}Learnings: ${learnings.length} from bridge${RESET}`);
|
|
2007
|
+
}
|
|
2008
|
+
if (dryRunContext || learningContext) {
|
|
2009
|
+
const fullContext = `${dryRunContext}${learningContext}`;
|
|
2010
|
+
writeLine();
|
|
2011
|
+
writeLine(` ${colors.cyan}Context to inject (${Math.ceil(fullContext.length / 4)} tokens):${RESET}`);
|
|
2012
|
+
writeLine(` ${colors.dim}${fullContext.slice(0, DRYRUN_CONTEXT_MAX_CHARS)}...${RESET}`);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const squadsDir = findSquadsDir();
|
|
2018
|
+
if (squadsDir) {
|
|
2019
|
+
const squadFilePath = join2(squadsDir, squadName, "SQUAD.md");
|
|
2020
|
+
if (existsSync2(squadFilePath)) {
|
|
2021
|
+
const squadContent = readFileSync2(squadFilePath, "utf-8");
|
|
2022
|
+
const permContext = buildContextFromSquad(squadName, squadContent, agentName);
|
|
2023
|
+
const mcpServers = extractMcpServersFromDefinition(definition);
|
|
2024
|
+
const execRequest = {
|
|
2025
|
+
mcpServers
|
|
2026
|
+
};
|
|
2027
|
+
const permResult = validateExecution(permContext, execRequest);
|
|
2028
|
+
if (permResult.violations.length > 0) {
|
|
2029
|
+
spinner.stop();
|
|
2030
|
+
const violationLines = formatViolations(permResult);
|
|
2031
|
+
for (const line of violationLines) {
|
|
2032
|
+
writeLine(` ${line}`);
|
|
2033
|
+
}
|
|
2034
|
+
writeLine();
|
|
2035
|
+
if (!permResult.allowed) {
|
|
2036
|
+
writeLine(` ${colors.red}Execution blocked due to permission violations.${RESET}`);
|
|
2037
|
+
writeLine(` ${colors.dim}Configure permissions in ${squadFilePath}${RESET}`);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const preflight = await checkPreflightGates(squadName, agentName);
|
|
2044
|
+
if (!preflight.allowed) {
|
|
2045
|
+
spinner.stop();
|
|
2046
|
+
writeLine();
|
|
2047
|
+
writeLine(` ${colors.red}${icons.error} Execution blocked by preflight gates${RESET}`);
|
|
2048
|
+
if (preflight.gates.quota && !preflight.gates.quota.ok) {
|
|
2049
|
+
writeLine(` ${colors.dim}Quota: $${preflight.gates.quota.used.toFixed(2)}/$${preflight.gates.quota.limit}/mo limit exceeded${RESET}`);
|
|
2050
|
+
}
|
|
2051
|
+
if (preflight.gates.cooldown && !preflight.gates.cooldown.ok) {
|
|
2052
|
+
const elapsed = preflight.gates.cooldown.elapsed_sec;
|
|
2053
|
+
const minGap = preflight.gates.cooldown.min_gap_sec;
|
|
2054
|
+
writeLine(` ${colors.dim}Cooldown: ${elapsed}s since last run (min: ${minGap}s)${RESET}`);
|
|
2055
|
+
}
|
|
2056
|
+
writeLine();
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
if (options.verbose && Object.keys(preflight.gates).length > 0) {
|
|
2060
|
+
writeLine(` ${colors.dim}Preflight: quota ${preflight.gates.quota?.ok ? "\u2713" : "\u2717"} cooldown ${preflight.gates.cooldown?.ok ? "\u2713" : "\u2717"}${RESET}`);
|
|
2061
|
+
}
|
|
2062
|
+
const isScheduledRun = options.trigger === "scheduled" || options.trigger === "smart";
|
|
2063
|
+
const bridgeHasNoHistory = preflight.gates.cooldown?.elapsed_sec === null;
|
|
2064
|
+
if (isScheduledRun && (!preflight.gates.cooldown || bridgeHasNoHistory)) {
|
|
2065
|
+
const frontmatterForCooldown = parseAgentFrontmatter(agentPath);
|
|
2066
|
+
const cooldownMs = frontmatterForCooldown.cooldown ? parseCooldown(frontmatterForCooldown.cooldown) || DEFAULT_SCHEDULED_COOLDOWN_MS : DEFAULT_SCHEDULED_COOLDOWN_MS;
|
|
2067
|
+
const localCooldown = checkLocalCooldown(squadName, agentName, cooldownMs);
|
|
2068
|
+
if (!localCooldown.ok) {
|
|
2069
|
+
spinner.stop();
|
|
2070
|
+
writeLine();
|
|
2071
|
+
writeLine(` ${colors.yellow}${icons.warning} Skipping: cooldown not elapsed${RESET}`);
|
|
2072
|
+
writeLine(` ${colors.dim}Last run: ${formatDuration(localCooldown.elapsedMs)} ago (cooldown: ${formatDuration(localCooldown.cooldownMs)})${RESET}`);
|
|
2073
|
+
writeLine();
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
if (options.verbose) {
|
|
2077
|
+
writeLine(` ${colors.dim}Local cooldown: \u2713 (${formatDuration(localCooldown.elapsedMs || 0)} since last run)${RESET}`);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
logExecution({
|
|
2081
|
+
squadName,
|
|
2082
|
+
agentName,
|
|
2083
|
+
executionId,
|
|
2084
|
+
startTime,
|
|
2085
|
+
status: "running",
|
|
2086
|
+
trigger: options.trigger || "manual",
|
|
2087
|
+
taskType
|
|
2088
|
+
});
|
|
2089
|
+
if (options.verbose && learnings.length > 0) {
|
|
2090
|
+
writeLine(` ${colors.dim}Injecting ${learnings.length} learnings${RESET}`);
|
|
2091
|
+
}
|
|
2092
|
+
const approvalInstructions = loadApprovalInstructions();
|
|
2093
|
+
const approvalContext = approvalInstructions ? `
|
|
2094
|
+
${approvalInstructions}
|
|
2095
|
+
` : "";
|
|
2096
|
+
const squadContext = gatherSquadContext(squadName, agentName, { verbose: options.verbose, agentPath });
|
|
2097
|
+
let cognitionContext = "";
|
|
2098
|
+
try {
|
|
2099
|
+
const { loadSession: loadSession2 } = await import("./auth-YW3UPFSB.js");
|
|
2100
|
+
const { getApiUrl: getApiUrl2 } = await import("./env-config-KCLDBKYX.js");
|
|
2101
|
+
const session = loadSession2();
|
|
2102
|
+
if (session?.accessToken && session.status === "active") {
|
|
2103
|
+
const safeSquadName = encodeURIComponent(squadName);
|
|
2104
|
+
const res = await fetch(`${getApiUrl2()}/cognition/context/squad:${safeSquadName}`, {
|
|
2105
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
2106
|
+
signal: AbortSignal.timeout(3e3)
|
|
2107
|
+
});
|
|
2108
|
+
if (res.ok) {
|
|
2109
|
+
const data = await res.json();
|
|
2110
|
+
if (data.markdown && !data.markdown.includes("No cognition data")) {
|
|
2111
|
+
cognitionContext = `
|
|
2112
|
+
${data.markdown}
|
|
2113
|
+
`;
|
|
2114
|
+
if (options.verbose) {
|
|
2115
|
+
writeLine(` ${colors.dim}Injecting cognition beliefs${RESET}`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
} catch {
|
|
2121
|
+
}
|
|
2122
|
+
const timeoutMins = options.timeout || DEFAULT_TIMEOUT_MINUTES;
|
|
2123
|
+
const prompt = `Execute the ${agentName} agent from squad ${squadName}.
|
|
2124
|
+
|
|
2125
|
+
Read the agent definition at ${agentPath} and follow its instructions exactly.
|
|
2126
|
+
|
|
2127
|
+
The agent definition contains:
|
|
2128
|
+
- Purpose/role
|
|
2129
|
+
- Tools it can use (MCP servers, skills)
|
|
2130
|
+
- Step-by-step instructions
|
|
2131
|
+
- Expected output format
|
|
2132
|
+
|
|
2133
|
+
TOOL PREFERENCE: Always prefer CLI tools over MCP servers when both can accomplish the task:
|
|
2134
|
+
- Use \`squads\` CLI for squad operations (run, memory, status, feedback)
|
|
2135
|
+
- Use \`gh\` CLI for GitHub (issues, PRs, repos)
|
|
2136
|
+
- Use \`git\` CLI for version control
|
|
2137
|
+
- Use Bash for file operations, builds, tests
|
|
2138
|
+
- Only use MCP tools when CLI cannot do it or MCP is significantly better
|
|
2139
|
+
${squadContext}${cognitionContext}${learningContext}${approvalContext}
|
|
2140
|
+
TIME LIMIT: You have ${timeoutMins} minutes. Work efficiently:
|
|
2141
|
+
- Focus on the most important tasks first
|
|
2142
|
+
- If a task is taking too long, move on and note it for next run
|
|
2143
|
+
- Aim to complete within ${Math.floor(timeoutMins * SOFT_DEADLINE_RATIO)} minutes
|
|
2144
|
+
|
|
2145
|
+
${loadPostExecution(squadName, agentName)}`;
|
|
2146
|
+
const agentProvider = parseAgentProvider(agentPath);
|
|
2147
|
+
const squad = loadSquad(squadName);
|
|
2148
|
+
const squadDefaultProvider = squad?.providers?.default;
|
|
2149
|
+
const provider2 = agentProvider || options.provider || squadDefaultProvider || "anthropic";
|
|
2150
|
+
const isAnthropic = provider2 === "anthropic";
|
|
2151
|
+
if (options.verbose && (agentProvider || squadDefaultProvider)) {
|
|
2152
|
+
writeLine(` ${colors.dim}Provider resolution:${RESET}`);
|
|
2153
|
+
if (agentProvider) writeLine(` ${colors.dim}Agent: ${agentProvider}${RESET}`);
|
|
2154
|
+
if (options.provider) writeLine(` ${colors.dim}CLI: ${options.provider}${RESET}`);
|
|
2155
|
+
if (squadDefaultProvider) writeLine(` ${colors.dim}Squad: ${squadDefaultProvider}${RESET}`);
|
|
2156
|
+
writeLine(` ${colors.dim}\u2192 Using: ${provider2}${RESET}`);
|
|
2157
|
+
}
|
|
2158
|
+
const cliAvailable = isAnthropic ? await checkClaudeCliAvailable() : isProviderCLIAvailable(provider2);
|
|
2159
|
+
if (options.execute && cliAvailable) {
|
|
2160
|
+
const cliConfig = getCLIConfig(provider2);
|
|
2161
|
+
const cliName = cliConfig?.displayName || provider2;
|
|
2162
|
+
const isBackground = options.background === true && !options.watch;
|
|
2163
|
+
const isWatch = options.watch === true;
|
|
2164
|
+
const isForeground = !isBackground && !isWatch;
|
|
2165
|
+
spinner.text = isBackground ? `Launching ${agentName} with ${cliName} in background...` : isWatch ? `Starting ${agentName} with ${cliName} (watch mode)...` : `Running ${agentName} with ${cliName}...`;
|
|
2166
|
+
const frontmatter = parseAgentFrontmatter(agentPath);
|
|
2167
|
+
const hasCriteria = !!frontmatter.acceptance_criteria && options.verify !== false;
|
|
2168
|
+
const maxRetries = frontmatter.max_retries ?? 2;
|
|
2169
|
+
let currentPrompt = prompt;
|
|
2170
|
+
for (let attempt = 0; attempt <= (hasCriteria ? maxRetries : 0); attempt++) {
|
|
2171
|
+
try {
|
|
2172
|
+
let result;
|
|
2173
|
+
if (isAnthropic) {
|
|
2174
|
+
result = await executeWithClaude(currentPrompt, {
|
|
2175
|
+
verbose: options.verbose,
|
|
2176
|
+
timeoutMinutes: options.timeout || 30,
|
|
2177
|
+
foreground: options.foreground,
|
|
2178
|
+
background: options.background,
|
|
2179
|
+
watch: options.watch,
|
|
2180
|
+
useApi: options.useApi,
|
|
2181
|
+
effort: options.effort,
|
|
2182
|
+
skills: options.skills,
|
|
2183
|
+
trigger: options.trigger || "manual",
|
|
2184
|
+
squadName,
|
|
2185
|
+
agentName,
|
|
2186
|
+
model: options.model
|
|
2187
|
+
});
|
|
2188
|
+
} else {
|
|
2189
|
+
result = await executeWithProvider(provider2, currentPrompt, {
|
|
2190
|
+
verbose: options.verbose,
|
|
2191
|
+
foreground: !isBackground,
|
|
2192
|
+
squadName,
|
|
2193
|
+
agentName
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
if (hasCriteria && (isForeground || isWatch)) {
|
|
2197
|
+
const verification = await verifyExecution(
|
|
2198
|
+
squadName,
|
|
2199
|
+
agentName,
|
|
2200
|
+
frontmatter.acceptance_criteria,
|
|
2201
|
+
{ verbose: options.verbose }
|
|
2202
|
+
);
|
|
2203
|
+
if (!verification.passed && attempt < maxRetries) {
|
|
2204
|
+
writeLine(` ${colors.yellow}Verification: FAIL - ${verification.reason}${RESET}`);
|
|
2205
|
+
writeLine(` ${colors.dim}Retrying (${attempt + 1}/${maxRetries})...${RESET}`);
|
|
2206
|
+
currentPrompt = `${prompt}
|
|
2207
|
+
|
|
2208
|
+
## PREVIOUS ATTEMPT FAILED
|
|
2209
|
+
Verification found: ${verification.reason}
|
|
2210
|
+
Please address this issue and try again.`;
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
if (verification.passed) {
|
|
2214
|
+
writeLine(` ${colors.green}Verification: PASS - ${verification.reason}${RESET}`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
emitExecutionEvent("agent.completed", {
|
|
2218
|
+
squad: squadName,
|
|
2219
|
+
agent: agentName,
|
|
2220
|
+
executionId
|
|
2221
|
+
}).catch(() => {
|
|
2222
|
+
});
|
|
2223
|
+
if (isForeground || isWatch) {
|
|
2224
|
+
spinner.succeed(`Agent ${agentName} completed (${cliName})`);
|
|
2225
|
+
} else {
|
|
2226
|
+
spinner.succeed(`Agent ${agentName} launched in background (${cliName})`);
|
|
2227
|
+
writeLine(` ${colors.dim}${result}${RESET}`);
|
|
2228
|
+
writeLine();
|
|
2229
|
+
writeLine(` ${colors.dim}Monitor:${RESET} squads workers`);
|
|
2230
|
+
writeLine(` ${colors.dim}Memory:${RESET} squads memory show ${squadName}`);
|
|
2231
|
+
}
|
|
2232
|
+
break;
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
emitExecutionEvent("agent.failed", {
|
|
2235
|
+
squad: squadName,
|
|
2236
|
+
agent: agentName,
|
|
2237
|
+
executionId,
|
|
2238
|
+
error: String(error)
|
|
2239
|
+
}).catch(() => {
|
|
2240
|
+
});
|
|
2241
|
+
spinner.fail(`Agent ${agentName} failed to launch`);
|
|
2242
|
+
updateExecutionStatus(squadName, agentName, executionId, "failed", {
|
|
2243
|
+
error: String(error),
|
|
2244
|
+
durationMs: Date.now() - startMs
|
|
2245
|
+
});
|
|
2246
|
+
writeLine(` ${colors.red}${String(error)}${RESET}`);
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
} else {
|
|
2251
|
+
spinner.succeed(`Agent ${agentName} ready`);
|
|
2252
|
+
writeLine(` ${colors.dim}Execution logged: ${startTime}${RESET}`);
|
|
2253
|
+
if (!cliAvailable) {
|
|
2254
|
+
const cliConfig = getCLIConfig(provider2);
|
|
2255
|
+
writeLine();
|
|
2256
|
+
writeLine(` ${colors.yellow}${cliConfig?.command || provider2} CLI not found${RESET}`);
|
|
2257
|
+
writeLine(` ${colors.dim}Install: ${cliConfig?.install || "squads providers"}${RESET}`);
|
|
2258
|
+
}
|
|
2259
|
+
writeLine();
|
|
2260
|
+
writeLine(` ${colors.dim}To launch as background task:${RESET}`);
|
|
2261
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} -a ${colors.cyan}${agentName}${RESET}`);
|
|
2262
|
+
if (provider2 !== "anthropic") {
|
|
2263
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} -a ${colors.cyan}${agentName}${RESET} --provider=${provider2}`);
|
|
2264
|
+
}
|
|
2265
|
+
writeLine();
|
|
2266
|
+
writeLine(` ${colors.dim}Or run interactively:${RESET}`);
|
|
2267
|
+
writeLine(` ${colors.dim}$${RESET} Run the ${colors.cyan}${agentName}${RESET} agent from ${agentPath}`);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
async function checkClaudeCliAvailable() {
|
|
2271
|
+
return new Promise((resolve) => {
|
|
2272
|
+
const check = spawn("which", ["claude"], { stdio: "pipe" });
|
|
2273
|
+
check.on("close", (code) => resolve(code === 0));
|
|
2274
|
+
check.on("error", () => resolve(false));
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
async function preflightExecutorCheck(provider2) {
|
|
2278
|
+
if (process.env.SQUADS_SKIP_CHECKS === "1") {
|
|
2279
|
+
return true;
|
|
2280
|
+
}
|
|
2281
|
+
const isAnthropic = provider2 === "anthropic";
|
|
2282
|
+
let cliFound;
|
|
2283
|
+
if (isAnthropic) {
|
|
2284
|
+
cliFound = await checkClaudeCliAvailable();
|
|
2285
|
+
} else {
|
|
2286
|
+
cliFound = isProviderCLIAvailable(provider2);
|
|
2287
|
+
}
|
|
2288
|
+
if (!cliFound) {
|
|
2289
|
+
const cliConfig = getCLIConfig(provider2);
|
|
2290
|
+
const cliName = cliConfig?.command || provider2;
|
|
2291
|
+
const installCmd = cliConfig?.install || `See ${provider2} documentation`;
|
|
2292
|
+
writeLine();
|
|
2293
|
+
writeLine(` ${icons.error} ${colors.red}${cliName} CLI not found${RESET}`);
|
|
2294
|
+
writeLine();
|
|
2295
|
+
writeLine(` ${colors.dim}The ${cliName} command is required to run agents but was not found on your PATH.${RESET}`);
|
|
2296
|
+
writeLine();
|
|
2297
|
+
writeLine(` ${colors.cyan}Install:${RESET} ${installCmd}`);
|
|
2298
|
+
writeLine();
|
|
2299
|
+
writeLine(` ${colors.dim}Skip this check: SQUADS_SKIP_CHECKS=1 squads run ...${RESET}`);
|
|
2300
|
+
writeLine();
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
if (isAnthropic) {
|
|
2304
|
+
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
2305
|
+
const home = homedir();
|
|
2306
|
+
const credentialsPath = join2(home, ".claude", ".credentials.json");
|
|
2307
|
+
const hasOAuthCreds = existsSync2(credentialsPath);
|
|
2308
|
+
if (!hasApiKey && !hasOAuthCreds) {
|
|
2309
|
+
writeLine(` ${colors.dim}${icons.progress} No API key or credentials file found \u2014 assuming OAuth${RESET}`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return true;
|
|
2313
|
+
}
|
|
2314
|
+
function buildAgentEnv(baseEnv, execContext, options) {
|
|
2315
|
+
const env = {
|
|
2316
|
+
...baseEnv,
|
|
2317
|
+
SQUADS_SQUAD: execContext.squad,
|
|
2318
|
+
SQUADS_AGENT: execContext.agent,
|
|
2319
|
+
SQUADS_TASK_TYPE: execContext.taskType,
|
|
2320
|
+
SQUADS_TRIGGER: execContext.trigger,
|
|
2321
|
+
SQUADS_EXECUTION_ID: execContext.executionId,
|
|
2322
|
+
BRIDGE_API: getBridgeUrl()
|
|
2323
|
+
};
|
|
2324
|
+
if (options?.includeOtel) {
|
|
2325
|
+
env.OTEL_RESOURCE_ATTRIBUTES = `squads.squad=${execContext.squad},squads.agent=${execContext.agent},squads.task_type=${execContext.taskType},squads.trigger=${execContext.trigger},squads.execution_id=${execContext.executionId}`;
|
|
2326
|
+
}
|
|
2327
|
+
if (options?.effort) env.CLAUDE_EFFORT = options.effort;
|
|
2328
|
+
if (options?.skills && options.skills.length > 0) env.CLAUDE_SKILLS = options.skills.join(",");
|
|
2329
|
+
return env;
|
|
2330
|
+
}
|
|
2331
|
+
function logVerboseExecution(config) {
|
|
2332
|
+
writeLine(` ${colors.dim}Project: ${config.projectRoot}${RESET}`);
|
|
2333
|
+
writeLine(` ${colors.dim}Mode: ${config.mode}${RESET}`);
|
|
2334
|
+
if (config.logFile) writeLine(` ${colors.dim}Log: ${config.logFile}${RESET}`);
|
|
2335
|
+
if (config.mcpConfigPath) writeLine(` ${colors.dim}MCP config: ${config.mcpConfigPath}${RESET}`);
|
|
2336
|
+
if (config.useApi !== void 0) writeLine(` ${colors.dim}Auth: ${config.useApi ? "API credits" : "subscription"}${RESET}`);
|
|
2337
|
+
writeLine(` ${colors.dim}Execution: ${config.execContext.executionId}${RESET}`);
|
|
2338
|
+
writeLine(` ${colors.dim}Task type: ${config.execContext.taskType}${RESET}`);
|
|
2339
|
+
writeLine(` ${colors.dim}Trigger: ${config.execContext.trigger}${RESET}`);
|
|
2340
|
+
if (config.effort) writeLine(` ${colors.dim}Effort: ${config.effort}${RESET}`);
|
|
2341
|
+
if (config.skills && config.skills.length > 0) writeLine(` ${colors.dim}Skills: ${config.skills.join(", ")}${RESET}`);
|
|
2342
|
+
if (config.resolvedModel || config.claudeModelAlias) {
|
|
2343
|
+
const source = config.explicitModel ? "explicit" : "auto-routed";
|
|
2344
|
+
const displayModel = config.resolvedModel || config.claudeModelAlias;
|
|
2345
|
+
writeLine(` ${colors.dim}Model: ${displayModel} (${source})${RESET}`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function createAgentWorktree(projectRoot, squadName, agentName) {
|
|
2349
|
+
const timestamp = Date.now();
|
|
2350
|
+
const branchName = `agent/${squadName}/${agentName}-${timestamp}`;
|
|
2351
|
+
const worktreePath = join2(projectRoot, "..", ".worktrees", `${squadName}-${agentName}-${timestamp}`);
|
|
2352
|
+
try {
|
|
2353
|
+
mkdirSync2(join2(projectRoot, "..", ".worktrees"), { recursive: true });
|
|
2354
|
+
execSync2(`git worktree add '${worktreePath}' -b '${branchName}' HEAD`, { cwd: projectRoot, stdio: "pipe" });
|
|
2355
|
+
return worktreePath;
|
|
2356
|
+
} catch {
|
|
2357
|
+
return projectRoot;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function buildDetachedShellScript(config) {
|
|
2361
|
+
const modelFlag = config.claudeModelAlias ? `--model ${config.claudeModelAlias}` : "";
|
|
2362
|
+
const branchName = `agent/${config.squadName}/${config.agentName}-${config.timestamp}`;
|
|
2363
|
+
const worktreeDir = `${config.projectRoot}/../.worktrees/${config.squadName}-${config.agentName}-${config.timestamp}`;
|
|
2364
|
+
const script = `mkdir -p '${config.projectRoot}/../.worktrees'; WORK_DIR='${config.projectRoot}'; if git -C '${config.projectRoot}' worktree add '${worktreeDir}' -b '${branchName}' HEAD 2>/dev/null; then WORK_DIR='${worktreeDir}'; fi; cd "\${WORK_DIR}"; claude --print --dangerously-skip-permissions ${modelFlag} -- '${config.escapedPrompt}' > '${config.logFile}' 2>&1`;
|
|
2365
|
+
return `echo $$ > '${config.pidFile}'; ${script}`;
|
|
2366
|
+
}
|
|
2367
|
+
function prepareLogFiles(projectRoot, squadName, agentName, timestamp) {
|
|
2368
|
+
const logDir = join2(projectRoot, ".agents", "logs", squadName);
|
|
2369
|
+
const logFile = join2(logDir, `${agentName}-${timestamp}.log`);
|
|
2370
|
+
const pidFile = join2(logDir, `${agentName}-${timestamp}.pid`);
|
|
2371
|
+
if (!existsSync2(logDir)) {
|
|
2372
|
+
mkdirSync2(logDir, { recursive: true });
|
|
2373
|
+
}
|
|
2374
|
+
return { logDir, logFile, pidFile };
|
|
2375
|
+
}
|
|
2376
|
+
function executeForeground(config) {
|
|
2377
|
+
const workDir = createAgentWorktree(config.projectRoot, config.squadName, config.agentName);
|
|
2378
|
+
return new Promise((resolve, reject) => {
|
|
2379
|
+
const claude = spawn("claude", config.claudeArgs, {
|
|
2380
|
+
stdio: "inherit",
|
|
2381
|
+
cwd: workDir,
|
|
2382
|
+
env: config.agentEnv
|
|
2383
|
+
});
|
|
2384
|
+
claude.on("close", async (code) => {
|
|
2385
|
+
const durationMs = Date.now() - config.startMs;
|
|
2386
|
+
if (code === 0) {
|
|
2387
|
+
updateExecutionStatus(config.squadName, config.agentName, config.execContext.executionId, "completed", {
|
|
2388
|
+
outcome: "Session completed successfully",
|
|
2389
|
+
durationMs
|
|
2390
|
+
});
|
|
2391
|
+
const commitResult = await autoCommitAgentWork(config.squadName, config.agentName, config.execContext.executionId, config.provider);
|
|
2392
|
+
if (commitResult.committed) {
|
|
2393
|
+
writeLine();
|
|
2394
|
+
writeLine(` ${colors.green}Auto-committed agent work${RESET}`);
|
|
2395
|
+
}
|
|
2396
|
+
resolve("Session completed");
|
|
2397
|
+
} else {
|
|
2398
|
+
updateExecutionStatus(config.squadName, config.agentName, config.execContext.executionId, "failed", {
|
|
2399
|
+
error: `Claude exited with code ${code}`,
|
|
2400
|
+
durationMs
|
|
2401
|
+
});
|
|
2402
|
+
reject(new Error(`Claude exited with code ${code}`));
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
claude.on("error", (err) => {
|
|
2406
|
+
const durationMs = Date.now() - config.startMs;
|
|
2407
|
+
updateExecutionStatus(config.squadName, config.agentName, config.execContext.executionId, "failed", {
|
|
2408
|
+
error: String(err),
|
|
2409
|
+
durationMs
|
|
2410
|
+
});
|
|
2411
|
+
reject(err);
|
|
2412
|
+
});
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
async function executeWatch(config) {
|
|
2416
|
+
const child = spawn("sh", ["-c", config.wrapperScript], {
|
|
2417
|
+
cwd: config.projectRoot,
|
|
2418
|
+
detached: true,
|
|
2419
|
+
stdio: "ignore",
|
|
2420
|
+
env: config.agentEnv
|
|
2421
|
+
});
|
|
2422
|
+
child.unref();
|
|
2423
|
+
await new Promise((resolve) => setTimeout(resolve, LOG_FILE_INIT_DELAY_MS));
|
|
2424
|
+
writeLine(` ${colors.dim}Tailing log (Ctrl+C to stop watching, agent continues)...${RESET}`);
|
|
2425
|
+
writeLine();
|
|
2426
|
+
const tail = spawn("tail", ["-f", config.logFile], { stdio: "inherit" });
|
|
2427
|
+
process.on("SIGINT", () => {
|
|
2428
|
+
tail.kill();
|
|
2429
|
+
writeLine();
|
|
2430
|
+
writeLine(` ${colors.dim}Stopped watching. Agent continues in background.${RESET}`);
|
|
2431
|
+
writeLine(` ${colors.dim}Resume: tail -f ${config.logFile}${RESET}`);
|
|
2432
|
+
process.exit(0);
|
|
2433
|
+
});
|
|
2434
|
+
return new Promise((resolve) => {
|
|
2435
|
+
tail.on("close", () => {
|
|
2436
|
+
resolve(`Agent running in background. Log: ${config.logFile}`);
|
|
2437
|
+
});
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
async function executeWithClaude(prompt, options) {
|
|
2441
|
+
const {
|
|
2442
|
+
verbose,
|
|
2443
|
+
timeoutMinutes: _timeoutMinutes = 30,
|
|
2444
|
+
foreground,
|
|
2445
|
+
background,
|
|
2446
|
+
watch,
|
|
2447
|
+
useApi,
|
|
2448
|
+
effort,
|
|
2449
|
+
skills,
|
|
2450
|
+
trigger = "manual",
|
|
2451
|
+
squadName,
|
|
2452
|
+
agentName,
|
|
2453
|
+
model
|
|
2454
|
+
} = options;
|
|
2455
|
+
const runInBackground = background === true && !watch;
|
|
2456
|
+
const runInWatch = watch === true;
|
|
2457
|
+
const runInForeground = !runInBackground && !runInWatch;
|
|
2458
|
+
const startMs = Date.now();
|
|
2459
|
+
const projectRoot = getProjectRoot();
|
|
2460
|
+
ensureProjectTrusted(projectRoot);
|
|
2461
|
+
const squad = squadName !== "unknown" ? loadSquad(squadName) : null;
|
|
2462
|
+
const mcpConfigPath = selectMcpConfig(squadName, squad);
|
|
2463
|
+
const taskType = detectTaskType(agentName);
|
|
2464
|
+
const resolvedModel = resolveModel(model, squad, taskType);
|
|
2465
|
+
const detectedProvider = resolvedModel ? detectProviderFromModel(resolvedModel) : "anthropic";
|
|
2466
|
+
if (detectedProvider !== "anthropic" && detectedProvider !== "unknown") {
|
|
2467
|
+
if (verbose) {
|
|
2468
|
+
const source = model ? "explicit" : "auto-routed";
|
|
2469
|
+
writeLine(` ${colors.dim}Model: ${resolvedModel} (${source})${RESET}`);
|
|
2470
|
+
writeLine(` ${colors.dim}Provider: ${detectedProvider}${RESET}`);
|
|
2471
|
+
}
|
|
2472
|
+
return executeWithProvider(detectedProvider, prompt, {
|
|
2473
|
+
verbose,
|
|
2474
|
+
foreground,
|
|
2475
|
+
cwd: projectRoot,
|
|
2476
|
+
squadName,
|
|
2477
|
+
agentName
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
const claudeModelAlias = resolvedModel ? getClaudeModelAlias(resolvedModel) : void 0;
|
|
2481
|
+
const execContext = {
|
|
2482
|
+
squad: squadName,
|
|
2483
|
+
agent: agentName,
|
|
2484
|
+
taskType,
|
|
2485
|
+
trigger,
|
|
2486
|
+
executionId: generateExecutionId()
|
|
2487
|
+
};
|
|
2488
|
+
const { ANTHROPIC_API_KEY: _apiKey, CLAUDECODE: _claudeCode, ...envWithoutApiKey } = process.env;
|
|
2489
|
+
const spawnEnv = useApi ? (() => {
|
|
2490
|
+
const { CLAUDECODE: _, ...rest } = process.env;
|
|
2491
|
+
return rest;
|
|
2492
|
+
})() : envWithoutApiKey;
|
|
2493
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
2494
|
+
await registerContextWithBridge(execContext);
|
|
2495
|
+
if (runInForeground) {
|
|
2496
|
+
if (verbose) {
|
|
2497
|
+
logVerboseExecution({
|
|
2498
|
+
projectRoot,
|
|
2499
|
+
mode: "foreground",
|
|
2500
|
+
useApi,
|
|
2501
|
+
execContext,
|
|
2502
|
+
effort,
|
|
2503
|
+
skills,
|
|
2504
|
+
resolvedModel,
|
|
2505
|
+
claudeModelAlias,
|
|
2506
|
+
explicitModel: model
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
const claudeArgs = [];
|
|
2510
|
+
if (!process.stdin.isTTY) claudeArgs.push("--print");
|
|
2511
|
+
claudeArgs.push("--dangerously-skip-permissions");
|
|
2512
|
+
if (mcpConfigPath) claudeArgs.push("--mcp-config", mcpConfigPath);
|
|
2513
|
+
if (claudeModelAlias) claudeArgs.push("--model", claudeModelAlias);
|
|
2514
|
+
claudeArgs.push("--", prompt);
|
|
2515
|
+
const agentEnv2 = buildAgentEnv(spawnEnv, execContext, {
|
|
2516
|
+
effort,
|
|
2517
|
+
skills,
|
|
2518
|
+
includeOtel: true
|
|
2519
|
+
});
|
|
2520
|
+
return executeForeground({
|
|
2521
|
+
prompt,
|
|
2522
|
+
claudeArgs,
|
|
2523
|
+
agentEnv: agentEnv2,
|
|
2524
|
+
projectRoot,
|
|
2525
|
+
squadName,
|
|
2526
|
+
agentName,
|
|
2527
|
+
execContext,
|
|
2528
|
+
startMs,
|
|
2529
|
+
provider
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
const timestamp = Date.now();
|
|
2533
|
+
const { logFile, pidFile } = prepareLogFiles(projectRoot, squadName, agentName, timestamp);
|
|
2534
|
+
const agentEnv = buildAgentEnv(spawnEnv, execContext, {
|
|
2535
|
+
effort,
|
|
2536
|
+
skills,
|
|
2537
|
+
includeOtel: !runInWatch
|
|
2538
|
+
});
|
|
2539
|
+
const wrapperScript = buildDetachedShellScript({
|
|
2540
|
+
projectRoot,
|
|
2541
|
+
squadName,
|
|
2542
|
+
agentName,
|
|
2543
|
+
timestamp,
|
|
2544
|
+
claudeModelAlias,
|
|
2545
|
+
escapedPrompt,
|
|
2546
|
+
logFile,
|
|
2547
|
+
pidFile
|
|
2548
|
+
});
|
|
2549
|
+
if (runInWatch) {
|
|
2550
|
+
if (verbose) {
|
|
2551
|
+
logVerboseExecution({
|
|
2552
|
+
projectRoot,
|
|
2553
|
+
mode: "watch (background + tail)",
|
|
2554
|
+
execContext,
|
|
2555
|
+
logFile
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
return executeWatch({ projectRoot, agentEnv, logFile, wrapperScript });
|
|
2559
|
+
}
|
|
2560
|
+
if (verbose) {
|
|
2561
|
+
logVerboseExecution({
|
|
2562
|
+
projectRoot,
|
|
2563
|
+
mode: "background",
|
|
2564
|
+
useApi,
|
|
2565
|
+
execContext,
|
|
2566
|
+
effort,
|
|
2567
|
+
skills,
|
|
2568
|
+
resolvedModel,
|
|
2569
|
+
claudeModelAlias,
|
|
2570
|
+
explicitModel: model,
|
|
2571
|
+
logFile,
|
|
2572
|
+
mcpConfigPath
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
const child = spawn("sh", ["-c", wrapperScript], {
|
|
2576
|
+
cwd: projectRoot,
|
|
2577
|
+
detached: true,
|
|
2578
|
+
stdio: "ignore",
|
|
2579
|
+
env: agentEnv
|
|
2580
|
+
});
|
|
2581
|
+
child.unref();
|
|
2582
|
+
if (verbose) {
|
|
2583
|
+
writeLine(` ${colors.dim}Monitor: tail -f ${logFile}${RESET}`);
|
|
2584
|
+
}
|
|
2585
|
+
return `Log: ${logFile}. Monitor: tail -f ${logFile}`;
|
|
2586
|
+
}
|
|
2587
|
+
async function executeWithProvider(provider2, prompt, options) {
|
|
2588
|
+
const cliConfig = getCLIConfig(provider2);
|
|
2589
|
+
if (!cliConfig) {
|
|
2590
|
+
throw new Error(`Unknown provider: ${provider2}. Run 'squads providers' to see available providers.`);
|
|
2591
|
+
}
|
|
2592
|
+
if (!isProviderCLIAvailable(provider2)) {
|
|
2593
|
+
throw new Error(`CLI '${cliConfig.command}' not found. Install: ${cliConfig.install}`);
|
|
2594
|
+
}
|
|
2595
|
+
const projectRoot = options.cwd || getProjectRoot();
|
|
2596
|
+
const squadName = options.squadName || "unknown";
|
|
2597
|
+
const agentName = options.agentName || "unknown";
|
|
2598
|
+
const timestamp = Date.now();
|
|
2599
|
+
const { CLAUDECODE: _claudeCode, ...cleanEnv } = process.env;
|
|
2600
|
+
const providerEnv = {
|
|
2601
|
+
...cleanEnv,
|
|
2602
|
+
SQUADS_SQUAD: squadName,
|
|
2603
|
+
SQUADS_AGENT: agentName,
|
|
2604
|
+
SQUADS_PROVIDER: provider2
|
|
2605
|
+
};
|
|
2606
|
+
const branchName = `agent/${squadName}/${agentName}-${timestamp}`;
|
|
2607
|
+
const worktreePath = join2(projectRoot, "..", ".worktrees", `${squadName}-${agentName}-${timestamp}`);
|
|
2608
|
+
let workDir = projectRoot;
|
|
2609
|
+
try {
|
|
2610
|
+
mkdirSync2(join2(projectRoot, "..", ".worktrees"), { recursive: true });
|
|
2611
|
+
execSync2(`git worktree add '${worktreePath}' -b '${branchName}' HEAD`, { cwd: projectRoot, stdio: "pipe" });
|
|
2612
|
+
workDir = worktreePath;
|
|
2613
|
+
} catch {
|
|
2614
|
+
}
|
|
2615
|
+
let effectivePrompt = prompt;
|
|
2616
|
+
if (workDir !== projectRoot) {
|
|
2617
|
+
const agentsDir = join2(projectRoot, ".agents");
|
|
2618
|
+
const targetAgentsDir = join2(workDir, ".agents");
|
|
2619
|
+
if (existsSync2(agentsDir) && !existsSync2(targetAgentsDir)) {
|
|
2620
|
+
try {
|
|
2621
|
+
cpSync(agentsDir, targetAgentsDir, { recursive: true });
|
|
2622
|
+
} catch {
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
effectivePrompt = prompt.replaceAll(projectRoot, workDir);
|
|
2626
|
+
}
|
|
2627
|
+
const args = cliConfig.buildArgs(effectivePrompt);
|
|
2628
|
+
if (options.verbose) {
|
|
2629
|
+
writeLine(` ${colors.dim}Provider: ${cliConfig.displayName}${RESET}`);
|
|
2630
|
+
writeLine(` ${colors.dim}Command: ${cliConfig.command} ${args.join(" ").slice(0, VERBOSE_COMMAND_MAX_CHARS)}...${RESET}`);
|
|
2631
|
+
writeLine(` ${colors.dim}CWD: ${workDir}${RESET}`);
|
|
2632
|
+
if (workDir !== projectRoot) {
|
|
2633
|
+
writeLine(` ${colors.dim}Worktree: ${branchName}${RESET}`);
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
if (options.foreground) {
|
|
2637
|
+
return new Promise((resolve, reject) => {
|
|
2638
|
+
const proc = spawn(cliConfig.command, args, {
|
|
2639
|
+
stdio: "inherit",
|
|
2640
|
+
cwd: workDir,
|
|
2641
|
+
env: providerEnv
|
|
2642
|
+
});
|
|
2643
|
+
proc.on("close", (code) => {
|
|
2644
|
+
if (code === 0) {
|
|
2645
|
+
resolve("Session completed");
|
|
2646
|
+
} else {
|
|
2647
|
+
reject(new Error(`${cliConfig.command} exited with code ${code}`));
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
2650
|
+
proc.on("error", (err) => {
|
|
2651
|
+
reject(err);
|
|
2652
|
+
});
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
const logDir = join2(projectRoot, ".agents", "logs", squadName);
|
|
2656
|
+
const logFile = join2(logDir, `${agentName}-${timestamp}.log`);
|
|
2657
|
+
const pidFile = join2(logDir, `${agentName}-${timestamp}.pid`);
|
|
2658
|
+
if (!existsSync2(logDir)) {
|
|
2659
|
+
mkdirSync2(logDir, { recursive: true });
|
|
2660
|
+
}
|
|
2661
|
+
const escapedPrompt = effectivePrompt.replace(/'/g, "'\\''");
|
|
2662
|
+
const providerArgs = cliConfig.buildArgs(escapedPrompt).map((a) => `'${a}'`).join(" ");
|
|
2663
|
+
const shellScript = `cd '${workDir}' && ${cliConfig.command} ${providerArgs} > '${logFile}' 2>&1`;
|
|
2664
|
+
const wrapperScript = `echo $$ > '${pidFile}'; ${shellScript}`;
|
|
2665
|
+
const child = spawn("sh", ["-c", wrapperScript], {
|
|
2666
|
+
cwd: workDir,
|
|
2667
|
+
detached: true,
|
|
2668
|
+
stdio: "ignore",
|
|
2669
|
+
env: providerEnv
|
|
2670
|
+
});
|
|
2671
|
+
child.unref();
|
|
2672
|
+
if (options.verbose) {
|
|
2673
|
+
writeLine(` ${colors.dim}Log: ${logFile}${RESET}`);
|
|
2674
|
+
writeLine(` ${colors.dim}PID file: ${pidFile}${RESET}`);
|
|
2675
|
+
}
|
|
2676
|
+
return `Log: ${logFile}. Monitor: tail -f ${logFile}`;
|
|
2677
|
+
}
|
|
2678
|
+
async function runSquadCommand(squadName, options) {
|
|
2679
|
+
return runCommand(squadName, options);
|
|
2680
|
+
}
|
|
2681
|
+
export {
|
|
2682
|
+
runCommand,
|
|
2683
|
+
runSquadCommand
|
|
2684
|
+
};
|
|
2685
|
+
//# sourceMappingURL=run-72OQLH5A.js.map
|