squads-cli 0.2.0 → 0.2.1
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 +521 -288
- package/dist/auth-YW3UPFSB.js +23 -0
- package/dist/auth-YW3UPFSB.js.map +1 -0
- package/dist/autonomy-PSVZVX7A.js +105 -0
- package/dist/autonomy-PSVZVX7A.js.map +1 -0
- package/dist/chunk-67RO2HKR.js +174 -0
- package/dist/chunk-67RO2HKR.js.map +1 -0
- package/dist/chunk-7OCVIDC7.js +12 -0
- package/dist/chunk-7OCVIDC7.js.map +1 -0
- package/dist/chunk-BODLDQY7.js +452 -0
- package/dist/chunk-BODLDQY7.js.map +1 -0
- package/dist/chunk-EHQJHRIW.js +103 -0
- package/dist/chunk-EHQJHRIW.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-HF4WR7RA.js +154 -0
- package/dist/chunk-HF4WR7RA.js.map +1 -0
- package/dist/chunk-J6QF4ZQX.js +230 -0
- package/dist/chunk-J6QF4ZQX.js.map +1 -0
- package/dist/chunk-LOA3KWYJ.js +294 -0
- package/dist/chunk-LOA3KWYJ.js.map +1 -0
- package/dist/chunk-M5FXNY6Y.js +384 -0
- package/dist/chunk-M5FXNY6Y.js.map +1 -0
- package/dist/chunk-QHNUMM4V.js +87 -0
- package/dist/chunk-QHNUMM4V.js.map +1 -0
- package/dist/chunk-QJ7C7CMB.js +223 -0
- package/dist/chunk-QJ7C7CMB.js.map +1 -0
- package/dist/chunk-RM6BWILN.js +74 -0
- package/dist/chunk-RM6BWILN.js.map +1 -0
- package/dist/chunk-TYFTF53O.js +613 -0
- package/dist/chunk-TYFTF53O.js.map +1 -0
- package/dist/chunk-TZXD6WFN.js +420 -0
- package/dist/chunk-TZXD6WFN.js.map +1 -0
- package/dist/chunk-WVOIY5GW.js +621 -0
- package/dist/chunk-WVOIY5GW.js.map +1 -0
- package/dist/chunk-Z2UKDBNL.js +162 -0
- package/dist/chunk-Z2UKDBNL.js.map +1 -0
- package/dist/chunk-ZTQ7ISUR.js +338 -0
- package/dist/chunk-ZTQ7ISUR.js.map +1 -0
- package/dist/cli.js +2483 -5902
- package/dist/cli.js.map +1 -1
- package/dist/context-GWPF4SEY.js +291 -0
- package/dist/context-GWPF4SEY.js.map +1 -0
- package/dist/context-feed-AJGVAR6H.js +394 -0
- package/dist/context-feed-AJGVAR6H.js.map +1 -0
- package/dist/cost-XBCDJ7XC.js +275 -0
- package/dist/cost-XBCDJ7XC.js.map +1 -0
- package/dist/create-BLFGG6PF.js +286 -0
- package/dist/create-BLFGG6PF.js.map +1 -0
- package/dist/dashboard-LGT2B2BL.js +951 -0
- package/dist/dashboard-LGT2B2BL.js.map +1 -0
- package/dist/dashboard-RMK2BOD2.js +794 -0
- package/dist/dashboard-RMK2BOD2.js.map +1 -0
- package/dist/doctor-XPUIIBHJ.js +374 -0
- package/dist/doctor-XPUIIBHJ.js.map +1 -0
- package/dist/env-config-SQEI3Y7Y.js +21 -0
- package/dist/env-config-SQEI3Y7Y.js.map +1 -0
- package/dist/exec-OUXM7JBF.js +223 -0
- package/dist/exec-OUXM7JBF.js.map +1 -0
- package/dist/feedback-KNAOG5QK.js +229 -0
- package/dist/feedback-KNAOG5QK.js.map +1 -0
- package/dist/github-UQTM5KMS.js +23 -0
- package/dist/github-UQTM5KMS.js.map +1 -0
- package/dist/goal-BVHV5573.js +168 -0
- package/dist/goal-BVHV5573.js.map +1 -0
- package/dist/health-4UXN44PF.js +218 -0
- package/dist/health-4UXN44PF.js.map +1 -0
- package/dist/history-ILH3SWHB.js +232 -0
- package/dist/history-ILH3SWHB.js.map +1 -0
- package/dist/index.d.ts +736 -8
- package/dist/index.js +1312 -6
- package/dist/index.js.map +1 -1
- package/dist/init-XQZ7BOGT.js +812 -0
- package/dist/init-XQZ7BOGT.js.map +1 -0
- package/dist/kpi-RQIU7WGK.js +413 -0
- package/dist/kpi-RQIU7WGK.js.map +1 -0
- package/dist/learn-OIFUVZAS.js +269 -0
- package/dist/learn-OIFUVZAS.js.map +1 -0
- package/dist/login-DXZANWZY.js +155 -0
- package/dist/login-DXZANWZY.js.map +1 -0
- package/dist/memory-T3ACCS7E.js +560 -0
- package/dist/memory-T3ACCS7E.js.map +1 -0
- package/dist/memory-VNF2VFRB.js +23 -0
- package/dist/memory-VNF2VFRB.js.map +1 -0
- package/dist/progress-DAUZMT3N.js +202 -0
- package/dist/progress-DAUZMT3N.js.map +1 -0
- package/dist/providers-3P5D2XL5.js +65 -0
- package/dist/providers-3P5D2XL5.js.map +1 -0
- package/dist/results-UECWGLTB.js +224 -0
- package/dist/results-UECWGLTB.js.map +1 -0
- package/dist/run-I6KAXU6U.js +4049 -0
- package/dist/run-I6KAXU6U.js.map +1 -0
- package/dist/session-HBU6KZOD.js +64 -0
- package/dist/session-HBU6KZOD.js.map +1 -0
- package/dist/sessions-CK25VGPL.js +333 -0
- package/dist/sessions-CK25VGPL.js.map +1 -0
- package/dist/squad-parser-DCG65BJS.js +35 -0
- package/dist/squad-parser-DCG65BJS.js.map +1 -0
- package/dist/stats-G6NAU5BD.js +334 -0
- package/dist/stats-G6NAU5BD.js.map +1 -0
- package/dist/status-AQNLDZVN.js +352 -0
- package/dist/status-AQNLDZVN.js.map +1 -0
- package/dist/sync-ZI3MHA4G.js +836 -0
- package/dist/sync-ZI3MHA4G.js.map +1 -0
- package/dist/templates/core/AGENTS.md.template +51 -0
- package/dist/templates/core/BUSINESS_BRIEF.md.template +29 -0
- package/dist/templates/core/CLAUDE.md.template +48 -0
- package/dist/templates/core/provider.yaml.template +5 -0
- package/dist/templates/first-squad/SQUAD.md.template +23 -0
- package/dist/templates/first-squad/lead.md.template +44 -0
- package/dist/templates/memory/getting-started/state.md.template +19 -0
- package/dist/templates/seed/BUSINESS_BRIEF.md.template +27 -0
- package/dist/templates/seed/CLAUDE.md.template +119 -0
- package/dist/templates/seed/README.md.template +42 -0
- package/dist/templates/seed/config/SYSTEM.md +52 -0
- package/dist/templates/seed/config/provider.yaml +4 -0
- package/dist/templates/seed/hooks/settings.json.template +31 -0
- package/dist/templates/seed/memory/company/directives.md +37 -0
- package/dist/templates/seed/memory/company/manager/state.md +16 -0
- package/dist/templates/seed/memory/engineering/issue-solver/state.md +12 -0
- package/dist/templates/seed/memory/intelligence/intel-lead/state.md +9 -0
- package/dist/templates/seed/memory/marketing/content-drafter/state.md +12 -0
- package/dist/templates/seed/memory/operations/ops-lead/state.md +12 -0
- package/dist/templates/seed/memory/product/lead/state.md +14 -0
- package/dist/templates/seed/memory/research/lead/state.md +14 -0
- package/dist/templates/seed/skills/gh/SKILL.md +57 -0
- package/dist/templates/seed/skills/squads-cli/SKILL.md +84 -0
- package/dist/templates/seed/squads/company/SQUAD.md +51 -0
- package/dist/templates/seed/squads/company/company-critic.md +49 -0
- package/dist/templates/seed/squads/company/company-eval.md +49 -0
- package/dist/templates/seed/squads/company/event-dispatcher.md +43 -0
- package/dist/templates/seed/squads/company/goal-tracker.md +43 -0
- package/dist/templates/seed/squads/company/manager.md +54 -0
- package/dist/templates/seed/squads/engineering/SQUAD.md +48 -0
- package/dist/templates/seed/squads/engineering/code-reviewer.md +57 -0
- package/dist/templates/seed/squads/engineering/issue-solver.md +58 -0
- package/dist/templates/seed/squads/engineering/test-writer.md +50 -0
- package/dist/templates/seed/squads/intelligence/SQUAD.md +38 -0
- package/dist/templates/seed/squads/intelligence/intel-critic.md +36 -0
- package/dist/templates/seed/squads/intelligence/intel-eval.md +31 -0
- package/dist/templates/seed/squads/intelligence/intel-lead.md +71 -0
- package/dist/templates/seed/squads/marketing/SQUAD.md +47 -0
- package/dist/templates/seed/squads/marketing/content-drafter.md +71 -0
- package/dist/templates/seed/squads/marketing/growth-analyst.md +49 -0
- package/dist/templates/seed/squads/marketing/social-poster.md +44 -0
- package/dist/templates/seed/squads/operations/SQUAD.md +45 -0
- package/dist/templates/seed/squads/operations/finance-tracker.md +47 -0
- package/dist/templates/seed/squads/operations/goal-tracker.md +48 -0
- package/dist/templates/seed/squads/operations/ops-lead.md +58 -0
- package/dist/templates/seed/squads/product/SQUAD.md +41 -0
- package/dist/templates/seed/squads/product/lead.md +56 -0
- package/dist/templates/seed/squads/product/scanner.md +50 -0
- package/dist/templates/seed/squads/product/worker.md +55 -0
- package/dist/templates/seed/squads/research/SQUAD.md +38 -0
- package/dist/templates/seed/squads/research/analyst.md +50 -0
- package/dist/templates/seed/squads/research/lead.md +52 -0
- package/dist/templates/seed/squads/research/synthesizer.md +59 -0
- package/dist/templates/skills/squads-learn/SKILL.md +86 -0
- package/dist/templates/skills/squads-workflow/instruction.md +70 -0
- package/dist/terminal-FBQFQTKZ.js +55 -0
- package/dist/terminal-FBQFQTKZ.js.map +1 -0
- package/dist/update-D7CGIZ3M.js +18 -0
- package/dist/update-D7CGIZ3M.js.map +1 -0
- package/dist/update-STU276HR.js +83 -0
- package/dist/update-STU276HR.js.map +1 -0
- package/package.json +31 -13
- package/templates/core/AGENTS.md.template +51 -0
- package/templates/core/BUSINESS_BRIEF.md.template +29 -0
- package/templates/core/CLAUDE.md.template +48 -0
- package/templates/core/provider.yaml.template +5 -0
- package/templates/first-squad/SQUAD.md.template +23 -0
- package/templates/first-squad/lead.md.template +44 -0
- package/templates/memory/getting-started/state.md.template +19 -0
- package/templates/seed/BUSINESS_BRIEF.md.template +27 -0
- package/templates/seed/CLAUDE.md.template +119 -0
- package/templates/seed/README.md.template +42 -0
- package/templates/seed/config/SYSTEM.md +52 -0
- package/templates/seed/config/provider.yaml +4 -0
- package/templates/seed/hooks/settings.json.template +31 -0
- package/templates/seed/memory/company/directives.md +37 -0
- package/templates/seed/memory/company/manager/state.md +16 -0
- package/templates/seed/memory/engineering/issue-solver/state.md +12 -0
- package/templates/seed/memory/intelligence/intel-lead/state.md +9 -0
- package/templates/seed/memory/marketing/content-drafter/state.md +12 -0
- package/templates/seed/memory/operations/ops-lead/state.md +12 -0
- package/templates/seed/memory/product/lead/state.md +14 -0
- package/templates/seed/memory/research/lead/state.md +14 -0
- package/templates/seed/skills/gh/SKILL.md +57 -0
- package/templates/seed/skills/squads-cli/SKILL.md +84 -0
- package/templates/seed/squads/company/SQUAD.md +51 -0
- package/templates/seed/squads/company/company-critic.md +49 -0
- package/templates/seed/squads/company/company-eval.md +49 -0
- package/templates/seed/squads/company/event-dispatcher.md +43 -0
- package/templates/seed/squads/company/goal-tracker.md +43 -0
- package/templates/seed/squads/company/manager.md +54 -0
- package/templates/seed/squads/engineering/SQUAD.md +48 -0
- package/templates/seed/squads/engineering/code-reviewer.md +57 -0
- package/templates/seed/squads/engineering/issue-solver.md +58 -0
- package/templates/seed/squads/engineering/test-writer.md +50 -0
- package/templates/seed/squads/intelligence/SQUAD.md +38 -0
- package/templates/seed/squads/intelligence/intel-critic.md +36 -0
- package/templates/seed/squads/intelligence/intel-eval.md +31 -0
- package/templates/seed/squads/intelligence/intel-lead.md +71 -0
- package/templates/seed/squads/marketing/SQUAD.md +47 -0
- package/templates/seed/squads/marketing/content-drafter.md +71 -0
- package/templates/seed/squads/marketing/growth-analyst.md +49 -0
- package/templates/seed/squads/marketing/social-poster.md +44 -0
- package/templates/seed/squads/operations/SQUAD.md +45 -0
- package/templates/seed/squads/operations/finance-tracker.md +47 -0
- package/templates/seed/squads/operations/goal-tracker.md +48 -0
- package/templates/seed/squads/operations/ops-lead.md +58 -0
- package/templates/seed/squads/product/SQUAD.md +41 -0
- package/templates/seed/squads/product/lead.md +56 -0
- package/templates/seed/squads/product/scanner.md +50 -0
- package/templates/seed/squads/product/worker.md +55 -0
- package/templates/seed/squads/research/SQUAD.md +38 -0
- package/templates/seed/squads/research/analyst.md +50 -0
- package/templates/seed/squads/research/lead.md +52 -0
- package/templates/seed/squads/research/synthesizer.md +59 -0
- package/templates/skills/squads-learn/SKILL.md +86 -0
- package/templates/skills/squads-workflow/instruction.md +70 -0
|
@@ -0,0 +1,4049 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getCLIConfig,
|
|
4
|
+
isProviderCLIAvailable
|
|
5
|
+
} from "./chunk-QHNUMM4V.js";
|
|
6
|
+
import {
|
|
7
|
+
getOutcomeScoreModifier
|
|
8
|
+
} from "./chunk-HF4WR7RA.js";
|
|
9
|
+
import {
|
|
10
|
+
getBotGhEnv,
|
|
11
|
+
getBotGitEnv,
|
|
12
|
+
getBotPushUrl,
|
|
13
|
+
getCoAuthorTrailer
|
|
14
|
+
} from "./chunk-FIWT2NMM.js";
|
|
15
|
+
import {
|
|
16
|
+
detectProviderFromModel
|
|
17
|
+
} from "./chunk-LOA3KWYJ.js";
|
|
18
|
+
import {
|
|
19
|
+
parseCooldown
|
|
20
|
+
} from "./chunk-RM6BWILN.js";
|
|
21
|
+
import {
|
|
22
|
+
getApiUrl,
|
|
23
|
+
getBridgeUrl
|
|
24
|
+
} from "./chunk-EHQJHRIW.js";
|
|
25
|
+
import {
|
|
26
|
+
isLoggedIn,
|
|
27
|
+
loadSession
|
|
28
|
+
} from "./chunk-Z2UKDBNL.js";
|
|
29
|
+
import {
|
|
30
|
+
Events,
|
|
31
|
+
flushEvents,
|
|
32
|
+
track
|
|
33
|
+
} from "./chunk-QJ7C7CMB.js";
|
|
34
|
+
import {
|
|
35
|
+
findSimilarSquads,
|
|
36
|
+
findSquadsDir,
|
|
37
|
+
listAgents,
|
|
38
|
+
listSquads,
|
|
39
|
+
loadAgentDefinition,
|
|
40
|
+
loadSquad,
|
|
41
|
+
parseAgentProvider,
|
|
42
|
+
resolveMcpConfigPath
|
|
43
|
+
} from "./chunk-TYFTF53O.js";
|
|
44
|
+
import {
|
|
45
|
+
findMemoryDir
|
|
46
|
+
} from "./chunk-ZTQ7ISUR.js";
|
|
47
|
+
import {
|
|
48
|
+
RESET,
|
|
49
|
+
bold,
|
|
50
|
+
colors,
|
|
51
|
+
gradient,
|
|
52
|
+
icons,
|
|
53
|
+
writeLine
|
|
54
|
+
} from "./chunk-M5FXNY6Y.js";
|
|
55
|
+
import "./chunk-7OCVIDC7.js";
|
|
56
|
+
|
|
57
|
+
// src/commands/run.ts
|
|
58
|
+
import ora from "ora";
|
|
59
|
+
import { spawn, execSync as execSync3 } from "child_process";
|
|
60
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
61
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, cpSync, unlinkSync } from "fs";
|
|
62
|
+
|
|
63
|
+
// src/lib/permissions.ts
|
|
64
|
+
import { minimatch } from "minimatch";
|
|
65
|
+
function getDefaultContext(squad, agent) {
|
|
66
|
+
return {
|
|
67
|
+
squad,
|
|
68
|
+
agent,
|
|
69
|
+
permissions: {
|
|
70
|
+
mode: "warn",
|
|
71
|
+
bash: ["*"],
|
|
72
|
+
// All commands allowed by default
|
|
73
|
+
write: ["**"],
|
|
74
|
+
// All paths writable by default
|
|
75
|
+
read: ["**"],
|
|
76
|
+
// All paths readable by default
|
|
77
|
+
mcp: {
|
|
78
|
+
allow: ["*"],
|
|
79
|
+
// All MCP servers allowed
|
|
80
|
+
deny: []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function validateBashCommand(command, allowedCommands) {
|
|
86
|
+
const baseCommand = command.trim().split(/\s+/)[0];
|
|
87
|
+
if (allowedCommands.includes("*")) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const isAllowed = allowedCommands.some((allowed) => {
|
|
91
|
+
if (allowed === baseCommand) return true;
|
|
92
|
+
if (allowed.includes("*")) {
|
|
93
|
+
return minimatch(baseCommand, allowed);
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
});
|
|
97
|
+
if (!isAllowed) {
|
|
98
|
+
return {
|
|
99
|
+
type: "bash",
|
|
100
|
+
requested: baseCommand,
|
|
101
|
+
reason: `Bash command '${baseCommand}' not in allowlist: [${allowedCommands.join(", ")}]`,
|
|
102
|
+
severity: "error"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
function validateFilePath(path, allowedGlobs, operation) {
|
|
108
|
+
if (allowedGlobs.includes("**") || allowedGlobs.includes("*")) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const isAllowed = allowedGlobs.some((glob) => minimatch(path, glob));
|
|
112
|
+
if (!isAllowed) {
|
|
113
|
+
return {
|
|
114
|
+
type: operation,
|
|
115
|
+
requested: path,
|
|
116
|
+
reason: `${operation === "write" ? "Write" : "Read"} to '${path}' not allowed. Permitted paths: [${allowedGlobs.join(", ")}]`,
|
|
117
|
+
severity: "error"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function validateMcpServer(server, allow, deny) {
|
|
123
|
+
const isDenied = deny.some((pattern) => {
|
|
124
|
+
if (pattern === server) return true;
|
|
125
|
+
if (pattern.includes("*")) return minimatch(server, pattern);
|
|
126
|
+
return false;
|
|
127
|
+
});
|
|
128
|
+
if (isDenied) {
|
|
129
|
+
return {
|
|
130
|
+
type: "mcp",
|
|
131
|
+
requested: server,
|
|
132
|
+
reason: `MCP server '${server}' is explicitly denied`,
|
|
133
|
+
severity: "error"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (allow.includes("*")) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const isAllowed = allow.some((pattern) => {
|
|
140
|
+
if (pattern === server) return true;
|
|
141
|
+
if (pattern.includes("*")) return minimatch(server, pattern);
|
|
142
|
+
return false;
|
|
143
|
+
});
|
|
144
|
+
if (!isAllowed) {
|
|
145
|
+
return {
|
|
146
|
+
type: "mcp",
|
|
147
|
+
requested: server,
|
|
148
|
+
reason: `MCP server '${server}' not in allowlist: [${allow.join(", ")}]`,
|
|
149
|
+
severity: "error"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function validateExecution(context, request) {
|
|
155
|
+
const violations = [];
|
|
156
|
+
if (request.bashCommands) {
|
|
157
|
+
for (const cmd of request.bashCommands) {
|
|
158
|
+
const violation = validateBashCommand(cmd, context.permissions.bash);
|
|
159
|
+
if (violation) {
|
|
160
|
+
violations.push(violation);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (request.writePaths) {
|
|
165
|
+
for (const path of request.writePaths) {
|
|
166
|
+
const violation = validateFilePath(path, context.permissions.write, "write");
|
|
167
|
+
if (violation) {
|
|
168
|
+
violations.push(violation);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (request.readPaths) {
|
|
173
|
+
for (const path of request.readPaths) {
|
|
174
|
+
const violation = validateFilePath(path, context.permissions.read, "read");
|
|
175
|
+
if (violation) {
|
|
176
|
+
violations.push(violation);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (request.mcpServers) {
|
|
181
|
+
for (const server of request.mcpServers) {
|
|
182
|
+
const violation = validateMcpServer(
|
|
183
|
+
server,
|
|
184
|
+
context.permissions.mcp.allow,
|
|
185
|
+
context.permissions.mcp.deny
|
|
186
|
+
);
|
|
187
|
+
if (violation) {
|
|
188
|
+
violations.push(violation);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const hasErrors = violations.some((v) => v.severity === "error");
|
|
193
|
+
const allowed = context.permissions.mode !== "strict" || !hasErrors;
|
|
194
|
+
return {
|
|
195
|
+
allowed,
|
|
196
|
+
violations,
|
|
197
|
+
mode: context.permissions.mode
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function formatViolations(result) {
|
|
201
|
+
const lines = [];
|
|
202
|
+
if (result.violations.length === 0) {
|
|
203
|
+
return lines;
|
|
204
|
+
}
|
|
205
|
+
const modeLabel = {
|
|
206
|
+
warn: "\u26A0\uFE0F PERMISSION WARNING",
|
|
207
|
+
strict: "\u{1F6AB} PERMISSION DENIED",
|
|
208
|
+
audit: "\u{1F4DD} PERMISSION AUDIT"
|
|
209
|
+
}[result.mode];
|
|
210
|
+
lines.push(modeLabel);
|
|
211
|
+
lines.push("");
|
|
212
|
+
for (const v of result.violations) {
|
|
213
|
+
const icon = v.severity === "error" ? "\u2717" : "\u26A0";
|
|
214
|
+
lines.push(` ${icon} [${v.type}] ${v.reason}`);
|
|
215
|
+
}
|
|
216
|
+
if (result.mode === "warn") {
|
|
217
|
+
lines.push("");
|
|
218
|
+
lines.push(" Continuing with warnings (mode: warn)");
|
|
219
|
+
} else if (result.mode === "audit") {
|
|
220
|
+
lines.push("");
|
|
221
|
+
lines.push(" Logged for audit, continuing (mode: audit)");
|
|
222
|
+
} else if (!result.allowed) {
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push(" Execution blocked (mode: strict)");
|
|
225
|
+
}
|
|
226
|
+
return lines;
|
|
227
|
+
}
|
|
228
|
+
function parsePermissionsYaml(content) {
|
|
229
|
+
const yamlMatch = content.match(/```ya?ml\n([\s\S]*?)```/);
|
|
230
|
+
if (!yamlMatch) return null;
|
|
231
|
+
const yamlContent = yamlMatch[1];
|
|
232
|
+
const permissions = {};
|
|
233
|
+
const modeMatch = yamlContent.match(/^\s*mode:\s*(warn|strict|audit)/m);
|
|
234
|
+
if (modeMatch) {
|
|
235
|
+
permissions.mode = modeMatch[1];
|
|
236
|
+
}
|
|
237
|
+
const bashMatch = yamlContent.match(/^\s*bash:\s*\[(.*?)\]/m);
|
|
238
|
+
if (bashMatch) {
|
|
239
|
+
permissions.bash = bashMatch[1].split(",").map((s) => s.trim());
|
|
240
|
+
}
|
|
241
|
+
const writeMatch = yamlContent.match(/^\s*write:\s*\[(.*?)\]/m);
|
|
242
|
+
if (writeMatch) {
|
|
243
|
+
permissions.write = writeMatch[1].split(",").map((s) => s.trim());
|
|
244
|
+
}
|
|
245
|
+
const readMatch = yamlContent.match(/^\s*read:\s*\[(.*?)\]/m);
|
|
246
|
+
if (readMatch) {
|
|
247
|
+
permissions.read = readMatch[1].split(",").map((s) => s.trim());
|
|
248
|
+
}
|
|
249
|
+
const mcpAllowMatch = yamlContent.match(/^\s*allow:\s*\[(.*?)\]/m);
|
|
250
|
+
const mcpDenyMatch = yamlContent.match(/^\s*deny:\s*\[(.*?)\]/m);
|
|
251
|
+
if (mcpAllowMatch || mcpDenyMatch) {
|
|
252
|
+
permissions.mcp = {
|
|
253
|
+
allow: mcpAllowMatch ? mcpAllowMatch[1].split(",").map((s) => s.trim()) : ["*"],
|
|
254
|
+
deny: mcpDenyMatch ? mcpDenyMatch[1].split(",").map((s) => s.trim()) : []
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return Object.keys(permissions).length > 0 ? permissions : null;
|
|
258
|
+
}
|
|
259
|
+
function buildContextFromSquad(squadName, squadContent, agentName) {
|
|
260
|
+
const context = getDefaultContext(squadName, agentName);
|
|
261
|
+
const parsed = parsePermissionsYaml(squadContent);
|
|
262
|
+
if (parsed) {
|
|
263
|
+
if (parsed.mode) context.permissions.mode = parsed.mode;
|
|
264
|
+
if (parsed.bash) context.permissions.bash = parsed.bash;
|
|
265
|
+
if (parsed.write) context.permissions.write = parsed.write;
|
|
266
|
+
if (parsed.read) context.permissions.read = parsed.read;
|
|
267
|
+
if (parsed.mcp) context.permissions.mcp = parsed.mcp;
|
|
268
|
+
}
|
|
269
|
+
return context;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/lib/workflow.ts
|
|
273
|
+
import { join as join2 } from "path";
|
|
274
|
+
import { existsSync as existsSync2, writeFileSync, mkdirSync } from "fs";
|
|
275
|
+
import { execSync, exec } from "child_process";
|
|
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 LEAD_COMPLETION_PHRASES = [
|
|
389
|
+
"session complete",
|
|
390
|
+
"session is complete",
|
|
391
|
+
"nothing to do",
|
|
392
|
+
"nothing more to do",
|
|
393
|
+
"nothing left to do",
|
|
394
|
+
"all work is done",
|
|
395
|
+
"all work complete",
|
|
396
|
+
"work is complete",
|
|
397
|
+
"work is done",
|
|
398
|
+
"all tasks complete",
|
|
399
|
+
"all tasks done",
|
|
400
|
+
"approved",
|
|
401
|
+
"approving",
|
|
402
|
+
"declaring convergence",
|
|
403
|
+
"signaling convergence",
|
|
404
|
+
"signal convergence",
|
|
405
|
+
"no further action",
|
|
406
|
+
"no further work",
|
|
407
|
+
"no action needed",
|
|
408
|
+
"no actions needed",
|
|
409
|
+
"wrapping up",
|
|
410
|
+
"closing out",
|
|
411
|
+
"conversation complete",
|
|
412
|
+
"cycle complete"
|
|
413
|
+
];
|
|
414
|
+
var CONTINUATION_PHRASES = [
|
|
415
|
+
"needs review",
|
|
416
|
+
"needs feedback",
|
|
417
|
+
"needs input",
|
|
418
|
+
"need clarification",
|
|
419
|
+
"todo",
|
|
420
|
+
"fixme",
|
|
421
|
+
"blocked",
|
|
422
|
+
"waiting for",
|
|
423
|
+
"waiting on",
|
|
424
|
+
"will continue",
|
|
425
|
+
"will proceed",
|
|
426
|
+
"will work on",
|
|
427
|
+
"next step",
|
|
428
|
+
"in progress"
|
|
429
|
+
];
|
|
430
|
+
function detectConvergence(transcript, maxTurns, costCeiling) {
|
|
431
|
+
if (transcript.turns.length >= maxTurns) {
|
|
432
|
+
return { converged: true, reason: `Max turns reached (${maxTurns})` };
|
|
433
|
+
}
|
|
434
|
+
if (transcript.totalCost >= costCeiling) {
|
|
435
|
+
return { converged: true, reason: `Cost ceiling reached ($${transcript.totalCost.toFixed(2)}/$${costCeiling})` };
|
|
436
|
+
}
|
|
437
|
+
if (transcript.turns.length === 0) {
|
|
438
|
+
return { converged: false, reason: "No turns yet" };
|
|
439
|
+
}
|
|
440
|
+
const lastTurn = transcript.turns[transcript.turns.length - 1];
|
|
441
|
+
const content = lastTurn.content;
|
|
442
|
+
const lower = content.toLowerCase();
|
|
443
|
+
if (lastTurn.role === "verifier") {
|
|
444
|
+
const rejected = VERIFIER_REJECTION_PHRASES.some((phrase) => lower.includes(phrase));
|
|
445
|
+
if (rejected) {
|
|
446
|
+
return { converged: false, reason: "Verifier rejected \u2014 continuing cycle" };
|
|
447
|
+
}
|
|
448
|
+
const approved = VERIFIER_APPROVAL_PHRASES.some((phrase) => lower.includes(phrase));
|
|
449
|
+
if (approved) {
|
|
450
|
+
return { converged: true, reason: "Verifier approved" };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (lastTurn.role === "lead") {
|
|
454
|
+
const leadDone = LEAD_COMPLETION_PHRASES.some((phrase) => lower.includes(phrase));
|
|
455
|
+
if (leadDone) {
|
|
456
|
+
return { converged: true, reason: "Lead signaled completion" };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const hasContinuation = CONTINUATION_PHRASES.some((phrase) => lower.includes(phrase));
|
|
460
|
+
if (hasContinuation) {
|
|
461
|
+
return { converged: false, reason: "Continuation signal detected" };
|
|
462
|
+
}
|
|
463
|
+
const hasConvergence = CONVERGENCE_PHRASES.some((phrase) => lower.includes(phrase));
|
|
464
|
+
if (hasConvergence) {
|
|
465
|
+
return { converged: true, reason: "Convergence signal detected" };
|
|
466
|
+
}
|
|
467
|
+
return { converged: false, reason: "No signals detected, continuing" };
|
|
468
|
+
}
|
|
469
|
+
var COST_PER_TURN = {
|
|
470
|
+
opus: 2.5,
|
|
471
|
+
sonnet: 0.75,
|
|
472
|
+
haiku: 0.1
|
|
473
|
+
};
|
|
474
|
+
function estimateTurnCost(model) {
|
|
475
|
+
const key = model.includes("opus") ? "opus" : model.includes("haiku") ? "haiku" : "sonnet";
|
|
476
|
+
return COST_PER_TURN[key] || COST_PER_TURN.sonnet;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// src/lib/run-context.ts
|
|
480
|
+
import { join, dirname } from "path";
|
|
481
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
482
|
+
var ROLE_BUDGETS = {
|
|
483
|
+
scanner: 4e3,
|
|
484
|
+
// ~1000 tokens — identity + priorities + state
|
|
485
|
+
worker: 12e3,
|
|
486
|
+
// ~3000 tokens — + directives, feedback, active-work
|
|
487
|
+
lead: 24e3,
|
|
488
|
+
// ~6000 tokens — all sections
|
|
489
|
+
coo: 32e3
|
|
490
|
+
// ~8000 tokens — all sections + expanded
|
|
491
|
+
};
|
|
492
|
+
var ROLE_SECTIONS = {
|
|
493
|
+
scanner: /* @__PURE__ */ new Set([1, 2, 5]),
|
|
494
|
+
// SQUAD.md, priorities, state
|
|
495
|
+
worker: /* @__PURE__ */ new Set([1, 2, 3, 4, 5, 6]),
|
|
496
|
+
// + directives, feedback, active-work
|
|
497
|
+
lead: /* @__PURE__ */ new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
|
|
498
|
+
// all sections
|
|
499
|
+
coo: /* @__PURE__ */ new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
500
|
+
// all sections + expanded budget
|
|
501
|
+
};
|
|
502
|
+
function parseAgentFrontmatter(agentPath) {
|
|
503
|
+
if (!agentPath || !existsSync(agentPath)) return {};
|
|
504
|
+
let content;
|
|
505
|
+
try {
|
|
506
|
+
content = readFileSync(agentPath, "utf-8");
|
|
507
|
+
} catch {
|
|
508
|
+
return {};
|
|
509
|
+
}
|
|
510
|
+
if (!content) return {};
|
|
511
|
+
const lines = content.split("\n");
|
|
512
|
+
let inFrontmatter = false;
|
|
513
|
+
const yamlLines = [];
|
|
514
|
+
for (const line of lines) {
|
|
515
|
+
if (line.trim() === "---") {
|
|
516
|
+
if (inFrontmatter) break;
|
|
517
|
+
inFrontmatter = true;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (inFrontmatter) {
|
|
521
|
+
yamlLines.push(line);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (yamlLines.length === 0) return {};
|
|
525
|
+
const yaml = yamlLines.join("\n");
|
|
526
|
+
const result = {};
|
|
527
|
+
const contextMatch = yaml.match(/context_from:\s*\[([^\]]+)\]/);
|
|
528
|
+
if (contextMatch) {
|
|
529
|
+
result.context_from = contextMatch[1].split(",").map((s) => s.trim());
|
|
530
|
+
}
|
|
531
|
+
const criteriaMatch = yaml.match(/acceptance_criteria:\s*\|\n((?:\s+.+\n?)*)/);
|
|
532
|
+
if (criteriaMatch) {
|
|
533
|
+
result.acceptance_criteria = criteriaMatch[1].replace(/^ {2}/gm, "").trim();
|
|
534
|
+
}
|
|
535
|
+
const retriesMatch = yaml.match(/max_retries:\s*(\d+)/);
|
|
536
|
+
if (retriesMatch) {
|
|
537
|
+
result.max_retries = parseInt(retriesMatch[1], 10);
|
|
538
|
+
}
|
|
539
|
+
const cooldownMatch = yaml.match(/cooldown:\s*["']?([^"'\n]+)["']?/);
|
|
540
|
+
if (cooldownMatch) {
|
|
541
|
+
result.cooldown = cooldownMatch[1].trim();
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
function extractMcpServersFromDefinition(definition) {
|
|
546
|
+
const servers = /* @__PURE__ */ new Set();
|
|
547
|
+
const knownServers = [
|
|
548
|
+
"chrome-devtools",
|
|
549
|
+
"firecrawl",
|
|
550
|
+
"context7",
|
|
551
|
+
"huggingface"
|
|
552
|
+
];
|
|
553
|
+
for (const server of knownServers) {
|
|
554
|
+
if (definition.toLowerCase().includes(server)) {
|
|
555
|
+
servers.add(server);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const mcpMatch = definition.match(/mcp:\s*\n((?:\s*-\s*\S+\s*\n?)+)/i);
|
|
559
|
+
if (mcpMatch) {
|
|
560
|
+
const lines = mcpMatch[1].split("\n");
|
|
561
|
+
for (const line of lines) {
|
|
562
|
+
const serverMatch = line.match(/^\s*-\s*(\S+)/);
|
|
563
|
+
if (serverMatch) {
|
|
564
|
+
servers.add(serverMatch[1]);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return Array.from(servers);
|
|
569
|
+
}
|
|
570
|
+
function readAgentsFile(relativePath, warnLabel) {
|
|
571
|
+
const squadsDir = findSquadsDir();
|
|
572
|
+
if (!squadsDir) return "";
|
|
573
|
+
const filePath = join(dirname(squadsDir), relativePath);
|
|
574
|
+
if (!existsSync(filePath)) return "";
|
|
575
|
+
try {
|
|
576
|
+
return readFileSync(filePath, "utf-8").trim();
|
|
577
|
+
} catch (e) {
|
|
578
|
+
writeLine(` ${colors.dim}warn: failed reading ${warnLabel}: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function loadSystemProtocol() {
|
|
583
|
+
const systemMd = readAgentsFile("config/SYSTEM.md", "SYSTEM.md");
|
|
584
|
+
if (systemMd) return systemMd;
|
|
585
|
+
return loadApprovalInstructions();
|
|
586
|
+
}
|
|
587
|
+
function loadApprovalInstructions() {
|
|
588
|
+
return readAgentsFile("config/approval-instructions.md", "approval instructions");
|
|
589
|
+
}
|
|
590
|
+
function safeRead(path) {
|
|
591
|
+
try {
|
|
592
|
+
return existsSync(path) ? readFileSync(path, "utf-8").trim() : "";
|
|
593
|
+
} catch {
|
|
594
|
+
return "";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function readDirMd(dirPath, maxChars) {
|
|
598
|
+
if (!existsSync(dirPath)) return "";
|
|
599
|
+
try {
|
|
600
|
+
const files = readdirSync(dirPath).filter((f) => f.endsWith(".md")).sort();
|
|
601
|
+
const parts = [];
|
|
602
|
+
let totalChars = 0;
|
|
603
|
+
for (const file of files) {
|
|
604
|
+
const content = safeRead(join(dirPath, file));
|
|
605
|
+
if (!content) continue;
|
|
606
|
+
if (totalChars + content.length > maxChars) break;
|
|
607
|
+
parts.push(content);
|
|
608
|
+
totalChars += content.length;
|
|
609
|
+
}
|
|
610
|
+
return parts.join("\n\n");
|
|
611
|
+
} catch {
|
|
612
|
+
return "";
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function gatherSquadContext(squadName, agentName, options = {}) {
|
|
616
|
+
const squadsDir = findSquadsDir();
|
|
617
|
+
if (!squadsDir) return "";
|
|
618
|
+
const memoryDir = findMemoryDir();
|
|
619
|
+
const role = options.role || "worker";
|
|
620
|
+
const budget = options.maxTokens ? options.maxTokens * 4 : ROLE_BUDGETS[role];
|
|
621
|
+
const allowedSections = ROLE_SECTIONS[role];
|
|
622
|
+
const sections = [];
|
|
623
|
+
let usedChars = 0;
|
|
624
|
+
function addSection(sectionNum, header, content, maxChars) {
|
|
625
|
+
if (!allowedSections.has(sectionNum)) return false;
|
|
626
|
+
if (!content) return false;
|
|
627
|
+
let text = content;
|
|
628
|
+
const cap = maxChars || budget - usedChars;
|
|
629
|
+
if (text.length > cap) {
|
|
630
|
+
text = text.substring(0, cap) + "\n...";
|
|
631
|
+
}
|
|
632
|
+
if (usedChars + text.length > budget) {
|
|
633
|
+
if (options.verbose) {
|
|
634
|
+
writeLine(` ${colors.dim}Context budget exhausted at section ${sectionNum} (${header})${RESET}`);
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
sections.push(`## ${header}
|
|
639
|
+
${text}`);
|
|
640
|
+
usedChars += text.length;
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
const squadFile = join(squadsDir, squadName, "SQUAD.md");
|
|
644
|
+
if (existsSync(squadFile)) {
|
|
645
|
+
try {
|
|
646
|
+
const content = readFileSync(squadFile, "utf-8");
|
|
647
|
+
const missionMatch = content.match(/## Mission[\s\S]*?(?=\n## |$)/i);
|
|
648
|
+
const squad = missionMatch ? missionMatch[0] : content.substring(0, 2e3);
|
|
649
|
+
addSection(1, `Squad: ${squadName}`, squad.trim());
|
|
650
|
+
} catch (e) {
|
|
651
|
+
if (options.verbose) writeLine(` ${colors.dim}warn: failed reading SQUAD.md: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (memoryDir) {
|
|
655
|
+
const prioritiesFile = join(memoryDir, squadName, "priorities.md");
|
|
656
|
+
const goalsFile = join(memoryDir, squadName, "goals.md");
|
|
657
|
+
const file = existsSync(prioritiesFile) ? prioritiesFile : goalsFile;
|
|
658
|
+
const content = safeRead(file);
|
|
659
|
+
if (content) {
|
|
660
|
+
addSection(2, "Priorities", content);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (memoryDir) {
|
|
664
|
+
const directivesFile = join(memoryDir, "company", "directives.md");
|
|
665
|
+
const content = safeRead(directivesFile);
|
|
666
|
+
if (content) {
|
|
667
|
+
addSection(3, "Directives", content);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
if (memoryDir) {
|
|
671
|
+
const feedbackFile = join(memoryDir, squadName, "feedback.md");
|
|
672
|
+
const content = safeRead(feedbackFile);
|
|
673
|
+
if (content) {
|
|
674
|
+
addSection(4, "Feedback", content);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (memoryDir) {
|
|
678
|
+
const stateFile = join(memoryDir, squadName, agentName, "state.md");
|
|
679
|
+
const content = safeRead(stateFile);
|
|
680
|
+
if (content) {
|
|
681
|
+
const stateCap = role === "scanner" ? 2e3 : void 0;
|
|
682
|
+
addSection(5, "Previous State", content, stateCap);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (memoryDir) {
|
|
686
|
+
const activeWorkFile = join(memoryDir, squadName, "active-work.md");
|
|
687
|
+
const content = safeRead(activeWorkFile);
|
|
688
|
+
if (content) {
|
|
689
|
+
addSection(6, "Active Work", content);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (memoryDir) {
|
|
693
|
+
const briefsDir = join(memoryDir, squadName, agentName, "briefs");
|
|
694
|
+
const content = readDirMd(briefsDir, 3e3);
|
|
695
|
+
if (content) {
|
|
696
|
+
addSection(7, "Agent Briefs", content);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (memoryDir) {
|
|
700
|
+
const briefsDir = join(memoryDir, squadName, "_briefs");
|
|
701
|
+
const content = readDirMd(briefsDir, 3e3);
|
|
702
|
+
if (content) {
|
|
703
|
+
addSection(8, "Squad Briefs", content);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (memoryDir) {
|
|
707
|
+
const dailyFile = join(memoryDir, "daily-briefing.md");
|
|
708
|
+
const content = safeRead(dailyFile);
|
|
709
|
+
if (content) {
|
|
710
|
+
addSection(9, "Daily Briefing", content);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (memoryDir) {
|
|
714
|
+
const frontmatter = options.agentPath ? parseAgentFrontmatter(options.agentPath) : {};
|
|
715
|
+
const contextSquads = frontmatter.context_from || [];
|
|
716
|
+
const learningParts = [];
|
|
717
|
+
for (const ctx of contextSquads) {
|
|
718
|
+
const learningsFile = join(memoryDir, ctx, "shared", "learnings.md");
|
|
719
|
+
const content = safeRead(learningsFile);
|
|
720
|
+
if (content) {
|
|
721
|
+
learningParts.push(`### ${ctx}
|
|
722
|
+
${content}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
if (learningParts.length > 0) {
|
|
726
|
+
addSection(10, "Cross-Squad Learnings", learningParts.join("\n\n"));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (sections.length === 0) return "";
|
|
730
|
+
if (options.verbose) {
|
|
731
|
+
writeLine(` ${colors.dim}Context: ${sections.length} sections, ~${Math.ceil(usedChars / 4)} tokens (${role} role, budget: ~${Math.ceil(budget / 4)})${RESET}`);
|
|
732
|
+
}
|
|
733
|
+
return `
|
|
734
|
+
# CONTEXT
|
|
735
|
+
${sections.join("\n\n")}
|
|
736
|
+
`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// src/lib/workflow.ts
|
|
740
|
+
var DEFAULT_MAX_TURNS = 20;
|
|
741
|
+
var DEFAULT_COST_CEILING = 25;
|
|
742
|
+
function executeAgentTurn(config) {
|
|
743
|
+
const { agentName, agentPath, role, squadName, model: _model, transcript, task } = config;
|
|
744
|
+
const transcriptContext = serializeTranscript(transcript);
|
|
745
|
+
const contextRole = agentName.includes("company-lead") ? "coo" : role;
|
|
746
|
+
const squadContext = gatherSquadContext(squadName, agentName, {
|
|
747
|
+
agentPath,
|
|
748
|
+
role: contextRole
|
|
749
|
+
});
|
|
750
|
+
let roleInstructions;
|
|
751
|
+
switch (role) {
|
|
752
|
+
case "lead":
|
|
753
|
+
if (transcript.turns.length === 0 && task) {
|
|
754
|
+
roleInstructions = `## Founder Directive
|
|
755
|
+
|
|
756
|
+
${task}
|
|
757
|
+
|
|
758
|
+
Brief the team on this directive. Set priorities and assign work.`;
|
|
759
|
+
} else if (transcript.turns.length === 0) {
|
|
760
|
+
roleInstructions = `## Your Role: Lead
|
|
761
|
+
|
|
762
|
+
You are starting a new squad session. Brief the team:
|
|
763
|
+
1. Review open issues and PRs
|
|
764
|
+
2. Set priorities for this session
|
|
765
|
+
3. Assign work to workers
|
|
766
|
+
4. Be specific about what each worker should do`;
|
|
767
|
+
} else {
|
|
768
|
+
roleInstructions = `## Your Role: Lead (Review)
|
|
769
|
+
|
|
770
|
+
Review the work done so far. Either:
|
|
771
|
+
- Request specific changes from workers
|
|
772
|
+
- Approve and signal completion if quality is sufficient
|
|
773
|
+
- Merge PRs using \`gh pr merge --squash --delete-branch --auto\` (waits for required checks)`;
|
|
774
|
+
}
|
|
775
|
+
break;
|
|
776
|
+
case "scanner":
|
|
777
|
+
roleInstructions = `## Your Role: Scanner
|
|
778
|
+
|
|
779
|
+
Scan for issues, gaps, and opportunities. Report findings concisely. Do NOT fix anything \u2014 just discover and report.`;
|
|
780
|
+
break;
|
|
781
|
+
case "worker":
|
|
782
|
+
roleInstructions = `## Your Role: Worker
|
|
783
|
+
|
|
784
|
+
Execute the work assigned by the lead. Create branches, write code, open PRs to develop. Be focused and efficient.`;
|
|
785
|
+
break;
|
|
786
|
+
case "verifier":
|
|
787
|
+
roleInstructions = `## Your Role: Verifier
|
|
788
|
+
|
|
789
|
+
Verify that work meets quality standards. Check PRs, run tests, validate output. Report pass/fail with specifics.`;
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
const prompt = `You are ${agentName} (${role}) in squad ${squadName}.
|
|
793
|
+
|
|
794
|
+
Read your full agent definition at ${agentPath} and follow its instructions.
|
|
795
|
+
|
|
796
|
+
${roleInstructions}
|
|
797
|
+
${squadContext}
|
|
798
|
+
${transcriptContext}
|
|
799
|
+
|
|
800
|
+
IMPORTANT:
|
|
801
|
+
- Be concise. Your output becomes part of a shared transcript.
|
|
802
|
+
- Reference specific issue numbers, PR numbers, and file paths.
|
|
803
|
+
- If you create a PR, include the PR number in your output.
|
|
804
|
+
- If there's nothing to do, say "Nothing to do" clearly.
|
|
805
|
+
- When done, summarize what you did in 2-3 sentences.`;
|
|
806
|
+
const resolvedModel = config.model || modelForRole(role);
|
|
807
|
+
const { CLAUDECODE: _cc, ANTHROPIC_API_KEY: _ak, ...cleanEnv } = process.env;
|
|
808
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
809
|
+
try {
|
|
810
|
+
const output = execSync(
|
|
811
|
+
`claude --print --dangerously-skip-permissions --model ${resolvedModel} -- '${escapedPrompt}'`,
|
|
812
|
+
{
|
|
813
|
+
cwd: config.cwd || process.cwd(),
|
|
814
|
+
timeout: 15 * 60 * 1e3,
|
|
815
|
+
// 15 min per turn
|
|
816
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
817
|
+
// 10MB
|
|
818
|
+
encoding: "utf-8",
|
|
819
|
+
env: cleanEnv
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
return output.trim();
|
|
823
|
+
} catch (err) {
|
|
824
|
+
const error = err;
|
|
825
|
+
if (error.stdout && error.stdout.trim().length > 0) {
|
|
826
|
+
return error.stdout.trim();
|
|
827
|
+
}
|
|
828
|
+
return `[ERROR] Agent ${agentName} failed: ${error.message || "unknown error"}`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function executeAgentTurnAsync(config) {
|
|
832
|
+
const { agentName, agentPath, role, squadName, model: _model, transcript, task } = config;
|
|
833
|
+
let roleInstructions = "";
|
|
834
|
+
switch (role) {
|
|
835
|
+
case "lead":
|
|
836
|
+
roleInstructions = task ? `FOUNDER DIRECTIVE: ${task}
|
|
837
|
+
|
|
838
|
+
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.";
|
|
839
|
+
break;
|
|
840
|
+
case "scanner":
|
|
841
|
+
roleInstructions = "Scan for issues, data, or signals relevant to the lead's brief. Report findings concisely.";
|
|
842
|
+
break;
|
|
843
|
+
case "worker":
|
|
844
|
+
roleInstructions = "Execute the specific task assigned by the lead. Produce concrete output (PRs, issues, content, analysis).";
|
|
845
|
+
break;
|
|
846
|
+
case "verifier":
|
|
847
|
+
roleInstructions = "Verify the worker's output meets quality standards. Check for errors, omissions, and alignment with goals.";
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
const transcriptContext = transcript.turns.length > 0 ? `
|
|
851
|
+
== CONVERSATION SO FAR ==
|
|
852
|
+
${serializeTranscript(transcript)}
|
|
853
|
+
== END CONVERSATION ==` : "";
|
|
854
|
+
const resolvedModel = config.model || modelForRole(role);
|
|
855
|
+
const prompt = `You are ${agentName} (${role}) in squad ${squadName}.
|
|
856
|
+
|
|
857
|
+
Read your full agent definition at ${agentPath} and follow its instructions.
|
|
858
|
+
|
|
859
|
+
${roleInstructions}
|
|
860
|
+
|
|
861
|
+
${transcriptContext}
|
|
862
|
+
|
|
863
|
+
IMPORTANT:
|
|
864
|
+
- Be concise. Your output becomes part of a shared transcript.
|
|
865
|
+
- Reference specific issue numbers, PR numbers, and file paths.
|
|
866
|
+
- If you create a PR, include the PR number in your output.
|
|
867
|
+
- If there's nothing to do, say "Nothing to do" clearly.
|
|
868
|
+
- When done, summarize what you did in 2-3 sentences.`;
|
|
869
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
870
|
+
const { CLAUDECODE: _cc2, ANTHROPIC_API_KEY: _ak2, ...cleanEnvAsync } = process.env;
|
|
871
|
+
return new Promise((resolve) => {
|
|
872
|
+
exec(
|
|
873
|
+
`claude --print --dangerously-skip-permissions --model ${resolvedModel} -- '${escapedPrompt}'`,
|
|
874
|
+
{
|
|
875
|
+
cwd: config.cwd || process.cwd(),
|
|
876
|
+
timeout: 15 * 60 * 1e3,
|
|
877
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
878
|
+
encoding: "utf-8",
|
|
879
|
+
env: cleanEnvAsync
|
|
880
|
+
},
|
|
881
|
+
(error, stdout, _stderr) => {
|
|
882
|
+
if (stdout && stdout.trim().length > 0) {
|
|
883
|
+
resolve(stdout.trim());
|
|
884
|
+
} else if (error) {
|
|
885
|
+
resolve(`[ERROR] Agent ${agentName} failed: ${error.message || "unknown error"}`);
|
|
886
|
+
} else {
|
|
887
|
+
resolve("[No output]");
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
);
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function buildTurnPlan(squad, squadsDir) {
|
|
894
|
+
const agents = [];
|
|
895
|
+
for (const agent of squad.agents) {
|
|
896
|
+
const role = classifyAgent(agent.name, agent.role);
|
|
897
|
+
if (!role) continue;
|
|
898
|
+
const agentPath = join2(squadsDir, squad.dir, `${agent.name}.md`);
|
|
899
|
+
if (!existsSync2(agentPath)) continue;
|
|
900
|
+
agents.push({ name: agent.name, role, path: agentPath });
|
|
901
|
+
}
|
|
902
|
+
return agents;
|
|
903
|
+
}
|
|
904
|
+
async function runConversation(squad, options = {}) {
|
|
905
|
+
const squadsDir = findSquadsDir();
|
|
906
|
+
if (!squadsDir) {
|
|
907
|
+
return {
|
|
908
|
+
transcript: createTranscript(squad.name),
|
|
909
|
+
turnCount: 0,
|
|
910
|
+
totalCost: 0,
|
|
911
|
+
converged: true,
|
|
912
|
+
reason: "No squads directory found"
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
const maxTurns = options.maxTurns || DEFAULT_MAX_TURNS;
|
|
916
|
+
const costCeiling = options.costCeiling || DEFAULT_COST_CEILING;
|
|
917
|
+
const transcript = createTranscript(squad.name);
|
|
918
|
+
let squadCwd = process.cwd();
|
|
919
|
+
if (squad.repo) {
|
|
920
|
+
const repoName = squad.repo.split("/").pop();
|
|
921
|
+
if (repoName) {
|
|
922
|
+
const reposRoot = join2(squadsDir, "..", "..", "..");
|
|
923
|
+
const candidatePath = join2(reposRoot, repoName);
|
|
924
|
+
if (existsSync2(candidatePath)) {
|
|
925
|
+
squadCwd = candidatePath;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const allAgents = buildTurnPlan(squad, squadsDir);
|
|
930
|
+
const leads = allAgents.filter((a) => a.role === "lead");
|
|
931
|
+
const scanners = allAgents.filter((a) => a.role === "scanner");
|
|
932
|
+
const workers = allAgents.filter((a) => a.role === "worker");
|
|
933
|
+
const verifiers = allAgents.filter((a) => a.role === "verifier");
|
|
934
|
+
if (leads.length === 0) {
|
|
935
|
+
return {
|
|
936
|
+
transcript,
|
|
937
|
+
turnCount: 0,
|
|
938
|
+
totalCost: 0,
|
|
939
|
+
converged: true,
|
|
940
|
+
reason: "No lead agent found \u2014 cannot orchestrate conversation"
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const lead = leads[0];
|
|
944
|
+
const log = (msg) => {
|
|
945
|
+
if (options.verbose) {
|
|
946
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
947
|
+
process.stderr.write(` [${ts}] ${msg}
|
|
948
|
+
`);
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
log(`Conversation: ${squad.name} | ${allAgents.length} agents | max ${maxTurns} turns | $${costCeiling} ceiling`);
|
|
952
|
+
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"}`);
|
|
953
|
+
let cycleCount = 0;
|
|
954
|
+
const MAX_CYCLES = 5;
|
|
955
|
+
while (cycleCount < MAX_CYCLES) {
|
|
956
|
+
cycleCount++;
|
|
957
|
+
log(`
|
|
958
|
+
--- Cycle ${cycleCount} ---`);
|
|
959
|
+
log(`Turn ${transcript.turns.length + 1}: ${lead.name} (lead)`);
|
|
960
|
+
const leadOutput = executeAgentTurn({
|
|
961
|
+
agentName: lead.name,
|
|
962
|
+
agentPath: lead.path,
|
|
963
|
+
role: "lead",
|
|
964
|
+
squadName: squad.name,
|
|
965
|
+
model: options.model || modelForRole("lead"),
|
|
966
|
+
transcript,
|
|
967
|
+
task: cycleCount === 1 ? options.task : void 0,
|
|
968
|
+
cwd: squadCwd
|
|
969
|
+
});
|
|
970
|
+
addTurn(transcript, lead.name, "lead", leadOutput, estimateTurnCost(options.model || "sonnet"));
|
|
971
|
+
let conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
972
|
+
if (conv.converged) {
|
|
973
|
+
log(`Converged after lead: ${conv.reason}`);
|
|
974
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
975
|
+
}
|
|
976
|
+
if (cycleCount === 1 && scanners.length > 0) {
|
|
977
|
+
if (scanners.length === 1) {
|
|
978
|
+
log(`Turn ${transcript.turns.length + 1}: ${scanners[0].name} (scanner)`);
|
|
979
|
+
const output = executeAgentTurn({
|
|
980
|
+
agentName: scanners[0].name,
|
|
981
|
+
agentPath: scanners[0].path,
|
|
982
|
+
role: "scanner",
|
|
983
|
+
squadName: squad.name,
|
|
984
|
+
model: options.model || modelForRole("scanner"),
|
|
985
|
+
transcript,
|
|
986
|
+
cwd: squadCwd
|
|
987
|
+
});
|
|
988
|
+
addTurn(transcript, scanners[0].name, "scanner", output, estimateTurnCost(options.model || "haiku"));
|
|
989
|
+
} else {
|
|
990
|
+
log(`Turns ${transcript.turns.length + 1}-${transcript.turns.length + scanners.length}: ${scanners.map((s) => s.name).join(", ")} (scanners, parallel)`);
|
|
991
|
+
const scannerPromises = scanners.map(
|
|
992
|
+
(scanner) => executeAgentTurnAsync({
|
|
993
|
+
agentName: scanner.name,
|
|
994
|
+
agentPath: scanner.path,
|
|
995
|
+
role: "scanner",
|
|
996
|
+
squadName: squad.name,
|
|
997
|
+
model: options.model || modelForRole("scanner"),
|
|
998
|
+
transcript,
|
|
999
|
+
// snapshot — all scanners see same context
|
|
1000
|
+
cwd: squadCwd
|
|
1001
|
+
}).then((output) => ({ agent: scanner, output }))
|
|
1002
|
+
);
|
|
1003
|
+
const scannerResults = await Promise.all(scannerPromises);
|
|
1004
|
+
for (const { agent, output } of scannerResults) {
|
|
1005
|
+
addTurn(transcript, agent.name, "scanner", output, estimateTurnCost(options.model || "haiku"));
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
1009
|
+
if (conv.converged) {
|
|
1010
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (workers.length === 1) {
|
|
1014
|
+
log(`Turn ${transcript.turns.length + 1}: ${workers[0].name} (worker)`);
|
|
1015
|
+
const output = executeAgentTurn({
|
|
1016
|
+
agentName: workers[0].name,
|
|
1017
|
+
agentPath: workers[0].path,
|
|
1018
|
+
role: "worker",
|
|
1019
|
+
squadName: squad.name,
|
|
1020
|
+
model: options.model || modelForRole("worker"),
|
|
1021
|
+
transcript,
|
|
1022
|
+
cwd: squadCwd
|
|
1023
|
+
});
|
|
1024
|
+
if (output.startsWith("[ERROR]")) {
|
|
1025
|
+
process.stderr.write(` [WARN] Worker ${workers[0].name} errored: ${output}
|
|
1026
|
+
`);
|
|
1027
|
+
}
|
|
1028
|
+
addTurn(transcript, workers[0].name, "worker", output, estimateTurnCost(options.model || "sonnet"));
|
|
1029
|
+
} else if (workers.length > 1) {
|
|
1030
|
+
log(`Turns ${transcript.turns.length + 1}-${transcript.turns.length + workers.length}: ${workers.map((w) => w.name).join(", ")} (workers, parallel)`);
|
|
1031
|
+
const workerPromises = workers.map(
|
|
1032
|
+
(worker) => executeAgentTurnAsync({
|
|
1033
|
+
agentName: worker.name,
|
|
1034
|
+
agentPath: worker.path,
|
|
1035
|
+
role: "worker",
|
|
1036
|
+
squadName: squad.name,
|
|
1037
|
+
model: options.model || modelForRole("worker"),
|
|
1038
|
+
transcript,
|
|
1039
|
+
// snapshot — all workers see same context
|
|
1040
|
+
cwd: squadCwd
|
|
1041
|
+
}).then((output) => ({ agent: worker, output }))
|
|
1042
|
+
);
|
|
1043
|
+
const workerResults = await Promise.all(workerPromises);
|
|
1044
|
+
for (const { agent, output } of workerResults) {
|
|
1045
|
+
if (output.startsWith("[ERROR]")) {
|
|
1046
|
+
process.stderr.write(` [WARN] Worker ${agent.name} errored: ${output}
|
|
1047
|
+
`);
|
|
1048
|
+
}
|
|
1049
|
+
addTurn(transcript, agent.name, "worker", output, estimateTurnCost(options.model || "sonnet"));
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
1053
|
+
if (conv.converged) {
|
|
1054
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
1055
|
+
}
|
|
1056
|
+
log(`Turn ${transcript.turns.length + 1}: ${lead.name} (lead review)`);
|
|
1057
|
+
const reviewOutput = executeAgentTurn({
|
|
1058
|
+
agentName: lead.name,
|
|
1059
|
+
agentPath: lead.path,
|
|
1060
|
+
role: "lead",
|
|
1061
|
+
squadName: squad.name,
|
|
1062
|
+
model: options.model || modelForRole("lead"),
|
|
1063
|
+
transcript,
|
|
1064
|
+
cwd: squadCwd
|
|
1065
|
+
});
|
|
1066
|
+
addTurn(transcript, lead.name, "lead", reviewOutput, estimateTurnCost(options.model || "sonnet"));
|
|
1067
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
1068
|
+
if (conv.converged) {
|
|
1069
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
1070
|
+
}
|
|
1071
|
+
if (verifiers.length === 1) {
|
|
1072
|
+
log(`Turn ${transcript.turns.length + 1}: ${verifiers[0].name} (verifier)`);
|
|
1073
|
+
const output = executeAgentTurn({
|
|
1074
|
+
agentName: verifiers[0].name,
|
|
1075
|
+
agentPath: verifiers[0].path,
|
|
1076
|
+
role: "verifier",
|
|
1077
|
+
squadName: squad.name,
|
|
1078
|
+
model: options.model || modelForRole("verifier"),
|
|
1079
|
+
transcript,
|
|
1080
|
+
cwd: squadCwd
|
|
1081
|
+
});
|
|
1082
|
+
addTurn(transcript, verifiers[0].name, "verifier", output, estimateTurnCost(options.model || "haiku"));
|
|
1083
|
+
} else if (verifiers.length > 1) {
|
|
1084
|
+
log(`Turns ${transcript.turns.length + 1}-${transcript.turns.length + verifiers.length}: ${verifiers.map((v) => v.name).join(", ")} (verifiers, parallel)`);
|
|
1085
|
+
const verifierPromises = verifiers.map(
|
|
1086
|
+
(verifier) => executeAgentTurnAsync({
|
|
1087
|
+
agentName: verifier.name,
|
|
1088
|
+
agentPath: verifier.path,
|
|
1089
|
+
role: "verifier",
|
|
1090
|
+
squadName: squad.name,
|
|
1091
|
+
model: options.model || modelForRole("verifier"),
|
|
1092
|
+
transcript,
|
|
1093
|
+
cwd: squadCwd
|
|
1094
|
+
}).then((output) => ({ agent: verifier, output }))
|
|
1095
|
+
);
|
|
1096
|
+
const verifierResults = await Promise.all(verifierPromises);
|
|
1097
|
+
for (const { agent, output } of verifierResults) {
|
|
1098
|
+
addTurn(transcript, agent.name, "verifier", output, estimateTurnCost(options.model || "haiku"));
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (verifiers.length > 0) {
|
|
1102
|
+
conv = detectConvergence(transcript, maxTurns, costCeiling);
|
|
1103
|
+
if (conv.converged) {
|
|
1104
|
+
return { transcript, turnCount: transcript.turns.length, totalCost: transcript.totalCost, converged: true, reason: conv.reason };
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return {
|
|
1109
|
+
transcript,
|
|
1110
|
+
turnCount: transcript.turns.length,
|
|
1111
|
+
totalCost: transcript.totalCost,
|
|
1112
|
+
converged: false,
|
|
1113
|
+
reason: `Max cycles reached (${MAX_CYCLES})`
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
function saveTranscript(transcript) {
|
|
1117
|
+
const squadsDir = findSquadsDir();
|
|
1118
|
+
if (!squadsDir) return null;
|
|
1119
|
+
const convDir = join2(squadsDir, "..", "conversations", transcript.squad);
|
|
1120
|
+
if (!existsSync2(convDir)) {
|
|
1121
|
+
mkdirSync(convDir, { recursive: true });
|
|
1122
|
+
}
|
|
1123
|
+
const id = Date.now().toString(36);
|
|
1124
|
+
const filePath = join2(convDir, `${id}.md`);
|
|
1125
|
+
const lines = [
|
|
1126
|
+
`# Conversation: ${transcript.squad}`,
|
|
1127
|
+
`Started: ${transcript.startedAt}`,
|
|
1128
|
+
`Turns: ${transcript.turns.length}`,
|
|
1129
|
+
`Estimated cost: $${transcript.totalCost.toFixed(2)}`,
|
|
1130
|
+
"",
|
|
1131
|
+
"---",
|
|
1132
|
+
""
|
|
1133
|
+
];
|
|
1134
|
+
for (const turn of transcript.turns) {
|
|
1135
|
+
lines.push(`## ${turn.agent} (${turn.role}) \u2014 ${turn.timestamp}`);
|
|
1136
|
+
lines.push("");
|
|
1137
|
+
lines.push(turn.content);
|
|
1138
|
+
lines.push("");
|
|
1139
|
+
lines.push("---");
|
|
1140
|
+
lines.push("");
|
|
1141
|
+
}
|
|
1142
|
+
writeFileSync(filePath, lines.join("\n"));
|
|
1143
|
+
return filePath;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/lib/api-client.ts
|
|
1147
|
+
var API_TIMEOUT_MS = 5e3;
|
|
1148
|
+
function getApiConfig() {
|
|
1149
|
+
const session = loadSession();
|
|
1150
|
+
if (!session?.accessToken || session.status !== "active") return null;
|
|
1151
|
+
const apiUrl = getApiUrl();
|
|
1152
|
+
return { apiUrl, token: session.accessToken };
|
|
1153
|
+
}
|
|
1154
|
+
async function apiRequest(path, method, body) {
|
|
1155
|
+
const config = getApiConfig();
|
|
1156
|
+
if (!config) return false;
|
|
1157
|
+
try {
|
|
1158
|
+
const response = await fetch(`${config.apiUrl}${path}`, {
|
|
1159
|
+
method,
|
|
1160
|
+
headers: {
|
|
1161
|
+
"Content-Type": "application/json",
|
|
1162
|
+
Authorization: `Bearer ${config.token}`
|
|
1163
|
+
},
|
|
1164
|
+
body: JSON.stringify(body),
|
|
1165
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
1166
|
+
});
|
|
1167
|
+
return response.ok;
|
|
1168
|
+
} catch {
|
|
1169
|
+
return false;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function reportExecutionStart(squad, agent, executionId, metadata) {
|
|
1173
|
+
const config = getApiConfig();
|
|
1174
|
+
if (!config) return null;
|
|
1175
|
+
try {
|
|
1176
|
+
const response = await fetch(`${config.apiUrl}/agent-executions`, {
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
headers: {
|
|
1179
|
+
"Content-Type": "application/json",
|
|
1180
|
+
Authorization: `Bearer ${config.token}`
|
|
1181
|
+
},
|
|
1182
|
+
body: JSON.stringify({
|
|
1183
|
+
squad,
|
|
1184
|
+
agent,
|
|
1185
|
+
executor: "cli",
|
|
1186
|
+
brief: metadata?.brief,
|
|
1187
|
+
model: metadata?.model,
|
|
1188
|
+
metadata: {
|
|
1189
|
+
local_execution_id: executionId,
|
|
1190
|
+
trigger: metadata?.trigger || "manual"
|
|
1191
|
+
}
|
|
1192
|
+
}),
|
|
1193
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
1194
|
+
});
|
|
1195
|
+
if (!response.ok) return null;
|
|
1196
|
+
const data = await response.json();
|
|
1197
|
+
return data.execution_id;
|
|
1198
|
+
} catch {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
async function reportConversationResult(executionId, result) {
|
|
1203
|
+
return apiRequest(`/agent-executions/${executionId}`, "PATCH", {
|
|
1204
|
+
status: result.converged ? "completed" : "stopped",
|
|
1205
|
+
summary: `${result.converged ? "Converged" : "Stopped"}: ${result.reason}`,
|
|
1206
|
+
cost_usd: result.totalCost,
|
|
1207
|
+
extra_data: {
|
|
1208
|
+
conversation: {
|
|
1209
|
+
turn_count: result.turnCount,
|
|
1210
|
+
total_cost: result.totalCost,
|
|
1211
|
+
converged: result.converged,
|
|
1212
|
+
reason: result.reason,
|
|
1213
|
+
agents_involved: result.agentsInvolved
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
async function pushCognitionSignal(signal) {
|
|
1219
|
+
return apiRequest("/cognition/signals", "POST", signal);
|
|
1220
|
+
}
|
|
1221
|
+
async function ingestMemorySignal(body) {
|
|
1222
|
+
const config = getApiConfig();
|
|
1223
|
+
if (!config) return null;
|
|
1224
|
+
try {
|
|
1225
|
+
const response = await fetch(`${config.apiUrl}/cognition/signals/ingest-memory`, {
|
|
1226
|
+
method: "POST",
|
|
1227
|
+
headers: {
|
|
1228
|
+
"Content-Type": "application/json",
|
|
1229
|
+
Authorization: `Bearer ${config.token}`
|
|
1230
|
+
},
|
|
1231
|
+
body: JSON.stringify(body),
|
|
1232
|
+
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
|
1233
|
+
});
|
|
1234
|
+
if (!response.ok) return null;
|
|
1235
|
+
return await response.json();
|
|
1236
|
+
} catch {
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// src/lib/squad-loop.ts
|
|
1242
|
+
import { createHash } from "crypto";
|
|
1243
|
+
import { execSync as execSync2 } from "child_process";
|
|
1244
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
|
|
1245
|
+
import { join as join3 } from "path";
|
|
1246
|
+
import { homedir } from "os";
|
|
1247
|
+
var MIN_PHANTOM_DURATION_MS = 30 * 1e3;
|
|
1248
|
+
var INGESTIBLE_FILES = ["state", "learnings", "executions"];
|
|
1249
|
+
var STATE_DIR = join3(homedir(), ".squads", "daemon");
|
|
1250
|
+
var STATE_FILE = join3(STATE_DIR, "state.json");
|
|
1251
|
+
function defaultState() {
|
|
1252
|
+
return {
|
|
1253
|
+
lastCycle: "",
|
|
1254
|
+
dailyCost: 0,
|
|
1255
|
+
dailyCostDate: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1256
|
+
recentRuns: [],
|
|
1257
|
+
failCounts: {},
|
|
1258
|
+
memoryHashes: {},
|
|
1259
|
+
cooldowns: {}
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
function loadLoopState() {
|
|
1263
|
+
if (!existsSync3(STATE_DIR)) mkdirSync2(STATE_DIR, { recursive: true });
|
|
1264
|
+
if (!existsSync3(STATE_FILE)) return defaultState();
|
|
1265
|
+
try {
|
|
1266
|
+
const raw = JSON.parse(readFileSync2(STATE_FILE, "utf-8"));
|
|
1267
|
+
if (!raw.cooldowns) raw.cooldowns = {};
|
|
1268
|
+
if (!raw.memoryHashes) raw.memoryHashes = {};
|
|
1269
|
+
return raw;
|
|
1270
|
+
} catch {
|
|
1271
|
+
return defaultState();
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function saveLoopState(state) {
|
|
1275
|
+
if (!existsSync3(STATE_DIR)) mkdirSync2(STATE_DIR, { recursive: true });
|
|
1276
|
+
writeFileSync2(STATE_FILE, JSON.stringify(state, null, 2));
|
|
1277
|
+
}
|
|
1278
|
+
function classifyRunOutcome(exitCode, durationMs) {
|
|
1279
|
+
if (exitCode !== 0) return "failed";
|
|
1280
|
+
if (durationMs < MIN_PHANTOM_DURATION_MS) return "skipped";
|
|
1281
|
+
return "completed";
|
|
1282
|
+
}
|
|
1283
|
+
function checkCooldown(state, squad, agentType, cooldownMs) {
|
|
1284
|
+
const key = `${squad}:${agentType}`;
|
|
1285
|
+
const lastDispatch = state.cooldowns[key];
|
|
1286
|
+
if (lastDispatch === void 0) return true;
|
|
1287
|
+
return Date.now() - lastDispatch >= cooldownMs;
|
|
1288
|
+
}
|
|
1289
|
+
function getOpenIssues(repo, ghEnv = {}) {
|
|
1290
|
+
try {
|
|
1291
|
+
const raw = execSync2(
|
|
1292
|
+
`gh issue list -R ${repo} --state open --json number,title,labels --limit 20`,
|
|
1293
|
+
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...ghEnv } }
|
|
1294
|
+
);
|
|
1295
|
+
const issues = JSON.parse(raw);
|
|
1296
|
+
return issues.map((i) => ({
|
|
1297
|
+
number: i.number,
|
|
1298
|
+
title: i.title,
|
|
1299
|
+
labels: i.labels.map((l) => l.name),
|
|
1300
|
+
repo
|
|
1301
|
+
}));
|
|
1302
|
+
} catch {
|
|
1303
|
+
return [];
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function getLastRunAge(squad, agent) {
|
|
1307
|
+
const memDir = findMemoryDir();
|
|
1308
|
+
if (!memDir) return null;
|
|
1309
|
+
const execPath = join3(memDir, squad, agent, "executions.md");
|
|
1310
|
+
if (!existsSync3(execPath)) return null;
|
|
1311
|
+
try {
|
|
1312
|
+
const content = readFileSync2(execPath, "utf-8");
|
|
1313
|
+
const timestamps = content.match(/\*\*(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\*\*/g);
|
|
1314
|
+
if (!timestamps || timestamps.length === 0) return null;
|
|
1315
|
+
const last = timestamps[timestamps.length - 1].replace(/\*\*/g, "");
|
|
1316
|
+
const lastDate = new Date(last);
|
|
1317
|
+
return Date.now() - lastDate.getTime();
|
|
1318
|
+
} catch {
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
function hasUnresolvedEscalation(repo, ghEnv = {}) {
|
|
1323
|
+
try {
|
|
1324
|
+
const raw = execSync2(
|
|
1325
|
+
`gh issue list -R ${repo} --label "blocked" --state open --json number,title --limit 1`,
|
|
1326
|
+
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...ghEnv } }
|
|
1327
|
+
);
|
|
1328
|
+
const issues = JSON.parse(raw);
|
|
1329
|
+
if (issues.length > 0) {
|
|
1330
|
+
return { blocked: true, issue: issues[0] };
|
|
1331
|
+
}
|
|
1332
|
+
const raw2 = execSync2(
|
|
1333
|
+
`gh issue list -R ${repo} --label "needs-human" --state open --json number,title --limit 1`,
|
|
1334
|
+
{ encoding: "utf-8", timeout: 15e3, stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...ghEnv } }
|
|
1335
|
+
);
|
|
1336
|
+
const issues2 = JSON.parse(raw2);
|
|
1337
|
+
if (issues2.length > 0) {
|
|
1338
|
+
return { blocked: true, issue: issues2[0] };
|
|
1339
|
+
}
|
|
1340
|
+
return { blocked: false };
|
|
1341
|
+
} catch {
|
|
1342
|
+
return { blocked: false };
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function getSquadRepos() {
|
|
1346
|
+
const repos = {};
|
|
1347
|
+
const squadsDir = findSquadsDir();
|
|
1348
|
+
if (!squadsDir) return repos;
|
|
1349
|
+
try {
|
|
1350
|
+
const squads = listSquads(squadsDir);
|
|
1351
|
+
for (const squad of squads) {
|
|
1352
|
+
const squadMd = join3(squadsDir, squad, "SQUAD.md");
|
|
1353
|
+
if (!existsSync3(squadMd)) continue;
|
|
1354
|
+
const content = readFileSync2(squadMd, "utf-8");
|
|
1355
|
+
const repoMatch = content.match(/^repo:\s*(.+)/m);
|
|
1356
|
+
if (repoMatch) {
|
|
1357
|
+
repos[squad] = repoMatch[1].trim();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
} catch {
|
|
1361
|
+
}
|
|
1362
|
+
return repos;
|
|
1363
|
+
}
|
|
1364
|
+
function scoreSquads(state, squadRepos, ghEnv = {}) {
|
|
1365
|
+
const signals = [];
|
|
1366
|
+
const squadsDir = findSquadsDir();
|
|
1367
|
+
if (!squadsDir) return signals;
|
|
1368
|
+
let squads;
|
|
1369
|
+
try {
|
|
1370
|
+
squads = listSquads(squadsDir);
|
|
1371
|
+
} catch {
|
|
1372
|
+
return signals;
|
|
1373
|
+
}
|
|
1374
|
+
for (const squadName of squads) {
|
|
1375
|
+
try {
|
|
1376
|
+
const repo = squadRepos[squadName];
|
|
1377
|
+
if (repo) {
|
|
1378
|
+
const escalation = hasUnresolvedEscalation(repo, ghEnv);
|
|
1379
|
+
if (escalation.blocked) {
|
|
1380
|
+
signals.push({
|
|
1381
|
+
squad: squadName,
|
|
1382
|
+
score: 0,
|
|
1383
|
+
reason: `PAUSED: unresolved escalation #${escalation.issue?.number} \u2014 ${escalation.issue?.title}`,
|
|
1384
|
+
issues: []
|
|
1385
|
+
});
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
const issues = repo ? getOpenIssues(repo, ghEnv) : [];
|
|
1390
|
+
let score = 0;
|
|
1391
|
+
let reason = "";
|
|
1392
|
+
const CONVERSATION_ISSUE_THRESHOLD = 3;
|
|
1393
|
+
const CONVERSATION_COOLDOWN_MS = 48 * 60 * 60 * 1e3;
|
|
1394
|
+
const lastConvAge = getLastRunAge(squadName, "conversation");
|
|
1395
|
+
const conversationStale = lastConvAge === null || lastConvAge > CONVERSATION_COOLDOWN_MS;
|
|
1396
|
+
const useConversation = issues.length >= CONVERSATION_ISSUE_THRESHOLD && conversationStale;
|
|
1397
|
+
const targetAgent = useConversation ? void 0 : "issue-solver";
|
|
1398
|
+
if (repo) {
|
|
1399
|
+
const p0Issues = issues.filter(
|
|
1400
|
+
(i) => i.labels.some((l) => l.includes("P0") || l.includes("priority:P0"))
|
|
1401
|
+
);
|
|
1402
|
+
const p1Issues = issues.filter(
|
|
1403
|
+
(i) => i.labels.some((l) => l.includes("P1") || l.includes("priority:P1"))
|
|
1404
|
+
);
|
|
1405
|
+
if (p0Issues.length > 0) {
|
|
1406
|
+
score += 80;
|
|
1407
|
+
reason = `${p0Issues.length} P0 issues: ${p0Issues[0].title}`;
|
|
1408
|
+
} else if (p1Issues.length > 0) {
|
|
1409
|
+
score += 60;
|
|
1410
|
+
reason = `${p1Issues.length} P1 issues: ${p1Issues[0].title}`;
|
|
1411
|
+
} else if (issues.length > 0) {
|
|
1412
|
+
score += 30;
|
|
1413
|
+
reason = `${issues.length} open issues`;
|
|
1414
|
+
}
|
|
1415
|
+
} else {
|
|
1416
|
+
reason = "no repo configured \u2014 staleness-based dispatch";
|
|
1417
|
+
}
|
|
1418
|
+
if (useConversation) {
|
|
1419
|
+
score += 10;
|
|
1420
|
+
reason += " \u2192 conversation mode";
|
|
1421
|
+
}
|
|
1422
|
+
const agentForStaleness = targetAgent ?? "conversation";
|
|
1423
|
+
const lastAge = getLastRunAge(squadName, agentForStaleness);
|
|
1424
|
+
if (lastAge !== null) {
|
|
1425
|
+
const hoursAgo = lastAge / (1e3 * 60 * 60);
|
|
1426
|
+
if (hoursAgo > 48) {
|
|
1427
|
+
score += 20;
|
|
1428
|
+
reason += ` (stale: ${Math.floor(hoursAgo)}h since last run)`;
|
|
1429
|
+
} else if (hoursAgo > 24) {
|
|
1430
|
+
score += 10;
|
|
1431
|
+
reason += ` (${Math.floor(hoursAgo)}h since last run)`;
|
|
1432
|
+
} else if (hoursAgo < 2) {
|
|
1433
|
+
score -= 30;
|
|
1434
|
+
reason += ` (ran ${Math.floor(hoursAgo * 60)}m ago)`;
|
|
1435
|
+
}
|
|
1436
|
+
} else if (!repo) {
|
|
1437
|
+
score += 15;
|
|
1438
|
+
reason += " (never run)";
|
|
1439
|
+
}
|
|
1440
|
+
const failKey = `${squadName}:${agentForStaleness}`;
|
|
1441
|
+
const failures = state.failCounts[failKey] || 0;
|
|
1442
|
+
if (failures >= 3) {
|
|
1443
|
+
score -= 40;
|
|
1444
|
+
reason += ` (${failures} consecutive failures \u2014 needs human)`;
|
|
1445
|
+
} else if (failures >= 1) {
|
|
1446
|
+
score -= 10 * failures;
|
|
1447
|
+
}
|
|
1448
|
+
const outcomeModifier = getOutcomeScoreModifier(squadName, agentForStaleness);
|
|
1449
|
+
if (outcomeModifier !== 0) {
|
|
1450
|
+
score += outcomeModifier;
|
|
1451
|
+
reason += ` (outcome: ${outcomeModifier > 0 ? "+" : ""}${outcomeModifier})`;
|
|
1452
|
+
}
|
|
1453
|
+
if (score > 0 && (issues.length > 0 || !repo)) {
|
|
1454
|
+
signals.push({ squad: squadName, score, reason, agent: targetAgent, issues });
|
|
1455
|
+
}
|
|
1456
|
+
} catch {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
signals.sort((a, b) => b.score - a.score);
|
|
1461
|
+
return signals;
|
|
1462
|
+
}
|
|
1463
|
+
async function slackNotify(message) {
|
|
1464
|
+
try {
|
|
1465
|
+
const envPath = join3(homedir(), "agents-squads", "hq", ".env");
|
|
1466
|
+
if (!existsSync3(envPath)) return;
|
|
1467
|
+
const env = readFileSync2(envPath, "utf-8");
|
|
1468
|
+
const tokenMatch = env.match(/SLACK_BOT_TOKEN=(.+)/);
|
|
1469
|
+
if (!tokenMatch) return;
|
|
1470
|
+
const token = tokenMatch[1].trim();
|
|
1471
|
+
const founderId = "U0A6NQ3U0JG";
|
|
1472
|
+
await fetch("https://slack.com/api/chat.postMessage", {
|
|
1473
|
+
method: "POST",
|
|
1474
|
+
headers: {
|
|
1475
|
+
"Authorization": `Bearer ${token}`,
|
|
1476
|
+
"Content-Type": "application/json"
|
|
1477
|
+
},
|
|
1478
|
+
body: JSON.stringify({ channel: founderId, text: message }),
|
|
1479
|
+
signal: AbortSignal.timeout(1e4)
|
|
1480
|
+
});
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async function pushMemorySignals(squads, state, verbose) {
|
|
1485
|
+
const memDir = findMemoryDir();
|
|
1486
|
+
if (!memDir) return;
|
|
1487
|
+
if (!state.memoryHashes) {
|
|
1488
|
+
state.memoryHashes = {};
|
|
1489
|
+
}
|
|
1490
|
+
const promises = [];
|
|
1491
|
+
for (const squad of squads) {
|
|
1492
|
+
const squadPath = join3(memDir, squad);
|
|
1493
|
+
if (!existsSync3(squadPath)) continue;
|
|
1494
|
+
let agents;
|
|
1495
|
+
try {
|
|
1496
|
+
agents = readdirSync2(squadPath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1497
|
+
} catch {
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
for (const agent of agents) {
|
|
1501
|
+
for (const fileType of INGESTIBLE_FILES) {
|
|
1502
|
+
const filePath = join3(squadPath, agent, `${fileType}.md`);
|
|
1503
|
+
if (!existsSync3(filePath)) continue;
|
|
1504
|
+
let content;
|
|
1505
|
+
try {
|
|
1506
|
+
content = readFileSync2(filePath, "utf-8");
|
|
1507
|
+
} catch {
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
if (!content.trim()) continue;
|
|
1511
|
+
const hash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
1512
|
+
const key = `${squad}/${agent}/${fileType}`;
|
|
1513
|
+
if (state.memoryHashes[key] === hash) continue;
|
|
1514
|
+
const p = ingestMemorySignal({
|
|
1515
|
+
squad,
|
|
1516
|
+
agent,
|
|
1517
|
+
file_type: fileType,
|
|
1518
|
+
content,
|
|
1519
|
+
content_hash: hash
|
|
1520
|
+
}).then((result) => {
|
|
1521
|
+
if (result) {
|
|
1522
|
+
state.memoryHashes[key] = hash;
|
|
1523
|
+
if (verbose && result.status === "ingested") {
|
|
1524
|
+
writeLine(` ${colors.dim}Memory: ${key} \u2192 ${result.signals_created || 0} signals${RESET}`);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
}).catch(() => {
|
|
1528
|
+
});
|
|
1529
|
+
promises.push(p);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (promises.length > 0) {
|
|
1534
|
+
await Promise.race([
|
|
1535
|
+
Promise.allSettled(promises),
|
|
1536
|
+
new Promise((resolve) => setTimeout(resolve, 1e4))
|
|
1537
|
+
]);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function computePhases(squadNames) {
|
|
1541
|
+
const squadsDir = findSquadsDir();
|
|
1542
|
+
if (!squadsDir) return /* @__PURE__ */ new Map([[0, squadNames || []]]);
|
|
1543
|
+
const names = squadNames || listSquads(squadsDir);
|
|
1544
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1545
|
+
const starSquads = [];
|
|
1546
|
+
for (const name of names) {
|
|
1547
|
+
const squad = loadSquad(name);
|
|
1548
|
+
if (!squad) continue;
|
|
1549
|
+
if (squad.depends_on && squad.depends_on.length === 1 && squad.depends_on[0] === "*") {
|
|
1550
|
+
starSquads.push(name);
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
const validDeps = (squad.depends_on || []).filter((d) => names.includes(d));
|
|
1554
|
+
if (squad.depends_on) {
|
|
1555
|
+
const invalid = squad.depends_on.filter((d) => d !== "*" && !names.includes(d));
|
|
1556
|
+
if (invalid.length > 0) {
|
|
1557
|
+
writeLine(` ${colors.dim}warn: ${name} depends_on unknown squads: ${invalid.join(", ")}${RESET}`);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
deps.set(name, validDeps);
|
|
1561
|
+
}
|
|
1562
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1563
|
+
const adjList = /* @__PURE__ */ new Map();
|
|
1564
|
+
for (const [squad, squadDeps] of deps) {
|
|
1565
|
+
if (!inDegree.has(squad)) inDegree.set(squad, 0);
|
|
1566
|
+
for (const dep of squadDeps) {
|
|
1567
|
+
if (!adjList.has(dep)) adjList.set(dep, []);
|
|
1568
|
+
adjList.get(dep).push(squad);
|
|
1569
|
+
inDegree.set(squad, (inDegree.get(squad) || 0) + 1);
|
|
1570
|
+
if (!inDegree.has(dep)) inDegree.set(dep, 0);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
for (const [squad] of deps) {
|
|
1574
|
+
if (!inDegree.has(squad)) inDegree.set(squad, 0);
|
|
1575
|
+
}
|
|
1576
|
+
const phases = /* @__PURE__ */ new Map();
|
|
1577
|
+
let phase = 0;
|
|
1578
|
+
const processed = /* @__PURE__ */ new Set();
|
|
1579
|
+
const remaining = /* @__PURE__ */ new Set([...deps.keys()]);
|
|
1580
|
+
while (remaining.size > 0) {
|
|
1581
|
+
const ready = [];
|
|
1582
|
+
for (const squad of remaining) {
|
|
1583
|
+
if ((inDegree.get(squad) || 0) <= 0) {
|
|
1584
|
+
ready.push(squad);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
if (ready.length === 0) {
|
|
1588
|
+
const cycled = [...remaining];
|
|
1589
|
+
if (!phases.has(phase)) phases.set(phase, []);
|
|
1590
|
+
phases.get(phase).push(...cycled);
|
|
1591
|
+
for (const s of cycled) processed.add(s);
|
|
1592
|
+
break;
|
|
1593
|
+
}
|
|
1594
|
+
phases.set(phase, ready);
|
|
1595
|
+
for (const squad of ready) {
|
|
1596
|
+
processed.add(squad);
|
|
1597
|
+
remaining.delete(squad);
|
|
1598
|
+
for (const dependent of adjList.get(squad) || []) {
|
|
1599
|
+
inDegree.set(dependent, (inDegree.get(dependent) || 0) - 1);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
phase++;
|
|
1603
|
+
}
|
|
1604
|
+
if (starSquads.length > 0) {
|
|
1605
|
+
phases.set(phase, starSquads);
|
|
1606
|
+
}
|
|
1607
|
+
return phases;
|
|
1608
|
+
}
|
|
1609
|
+
function scoreSquadsForPhase(phaseSquads, state, squadRepos, ghEnv) {
|
|
1610
|
+
const allSignals = scoreSquads(state, squadRepos, ghEnv);
|
|
1611
|
+
return allSignals.filter((s) => phaseSquads.includes(s.squad));
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// src/lib/cognition.ts
|
|
1615
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3 } from "fs";
|
|
1616
|
+
import { join as join4 } from "path";
|
|
1617
|
+
import { createHash as createHash2 } from "crypto";
|
|
1618
|
+
import { spawnSync } from "child_process";
|
|
1619
|
+
var COGNITION_DIR_NAME = "cognition";
|
|
1620
|
+
var STATE_FILE2 = "state.json";
|
|
1621
|
+
var SYNTHESIZE_INTERVAL_MS = 30 * 60 * 1e3;
|
|
1622
|
+
var REFLECT_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
1623
|
+
var MAX_SIGNALS_KEPT = 500;
|
|
1624
|
+
var MAX_REFLECTIONS_KEPT = 50;
|
|
1625
|
+
var CONFIDENCE_PRIOR_WEIGHT = 0.7;
|
|
1626
|
+
var CONFIDENCE_EVIDENCE_WEIGHT = 0.3;
|
|
1627
|
+
var CONFIDENCE_MIN = 0.05;
|
|
1628
|
+
var CONFIDENCE_MAX = 0.95;
|
|
1629
|
+
var BELIEF_SHIFT_THRESHOLD = 0.15;
|
|
1630
|
+
var MAX_SUPPORTING_IDS = 20;
|
|
1631
|
+
var INGESTIBLE_FILES2 = ["state", "learnings", "executions"];
|
|
1632
|
+
var FILE_TYPE_MAPPING = {
|
|
1633
|
+
state: { source: "memory", signal_type: "state_update" },
|
|
1634
|
+
learnings: { source: "memory", signal_type: "learning" },
|
|
1635
|
+
executions: { source: "execution", signal_type: "execution_log" },
|
|
1636
|
+
events: { source: "market", signal_type: "external_event" },
|
|
1637
|
+
directives: { source: "execution", signal_type: "directive" }
|
|
1638
|
+
};
|
|
1639
|
+
function callClaude(prompt, model, timeoutMs) {
|
|
1640
|
+
const { CLAUDECODE: _, ANTHROPIC_API_KEY: _k, ...cleanEnv } = process.env;
|
|
1641
|
+
const result = spawnSync("claude", ["--print", "--model", model], {
|
|
1642
|
+
input: prompt,
|
|
1643
|
+
encoding: "utf-8",
|
|
1644
|
+
timeout: timeoutMs,
|
|
1645
|
+
env: cleanEnv,
|
|
1646
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1647
|
+
});
|
|
1648
|
+
if (result.status !== 0 || !result.stdout) return null;
|
|
1649
|
+
return result.stdout;
|
|
1650
|
+
}
|
|
1651
|
+
function getCognitionDir() {
|
|
1652
|
+
const memDir = findMemoryDir();
|
|
1653
|
+
const dir = memDir ? join4(memDir, COGNITION_DIR_NAME) : join4(process.cwd(), ".agents", "memory", COGNITION_DIR_NAME);
|
|
1654
|
+
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
1655
|
+
return dir;
|
|
1656
|
+
}
|
|
1657
|
+
function defaultState2() {
|
|
1658
|
+
return {
|
|
1659
|
+
signals: [],
|
|
1660
|
+
beliefs: [],
|
|
1661
|
+
decisions: [],
|
|
1662
|
+
reflections: [],
|
|
1663
|
+
last_synthesize: null,
|
|
1664
|
+
last_reflect: null,
|
|
1665
|
+
next_signal_id: 1,
|
|
1666
|
+
next_decision_id: 1,
|
|
1667
|
+
next_reflection_id: 1,
|
|
1668
|
+
memory_hashes: {}
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function loadCognitionState() {
|
|
1672
|
+
const dir = getCognitionDir();
|
|
1673
|
+
const path = join4(dir, STATE_FILE2);
|
|
1674
|
+
if (!existsSync4(path)) return defaultState2();
|
|
1675
|
+
try {
|
|
1676
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
1677
|
+
} catch {
|
|
1678
|
+
return defaultState2();
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
function saveCognitionState(state) {
|
|
1682
|
+
const dir = getCognitionDir();
|
|
1683
|
+
writeFileSync3(join4(dir, STATE_FILE2), JSON.stringify(state, null, 2));
|
|
1684
|
+
}
|
|
1685
|
+
function addSignal(state, signal) {
|
|
1686
|
+
const newSignal = {
|
|
1687
|
+
...signal,
|
|
1688
|
+
id: state.next_signal_id++,
|
|
1689
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1690
|
+
};
|
|
1691
|
+
state.signals.push(newSignal);
|
|
1692
|
+
if (state.signals.length > MAX_SIGNALS_KEPT) {
|
|
1693
|
+
state.signals = state.signals.slice(-MAX_SIGNALS_KEPT);
|
|
1694
|
+
}
|
|
1695
|
+
pushCognitionSignal({
|
|
1696
|
+
source: signal.source,
|
|
1697
|
+
signal_type: signal.signal_type,
|
|
1698
|
+
value: signal.value ?? void 0,
|
|
1699
|
+
unit: signal.unit ?? void 0,
|
|
1700
|
+
data: signal.data,
|
|
1701
|
+
entity_type: signal.entity_type ?? void 0,
|
|
1702
|
+
entity_id: signal.entity_id ?? void 0,
|
|
1703
|
+
confidence: signal.confidence
|
|
1704
|
+
});
|
|
1705
|
+
return newSignal;
|
|
1706
|
+
}
|
|
1707
|
+
function ingestMemoryFiles(state, squads, verbose = false) {
|
|
1708
|
+
const memDir = findMemoryDir();
|
|
1709
|
+
if (!memDir) return 0;
|
|
1710
|
+
let signalsCreated = 0;
|
|
1711
|
+
for (const squad of squads) {
|
|
1712
|
+
const squadPath = join4(memDir, squad);
|
|
1713
|
+
if (!existsSync4(squadPath)) continue;
|
|
1714
|
+
let agents;
|
|
1715
|
+
try {
|
|
1716
|
+
agents = readdirSync3(squadPath, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name !== COGNITION_DIR_NAME).map((e) => e.name);
|
|
1717
|
+
} catch {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
for (const agent of agents) {
|
|
1721
|
+
for (const fileType of INGESTIBLE_FILES2) {
|
|
1722
|
+
const filePath = join4(squadPath, agent, `${fileType}.md`);
|
|
1723
|
+
if (!existsSync4(filePath)) continue;
|
|
1724
|
+
let content;
|
|
1725
|
+
try {
|
|
1726
|
+
content = readFileSync3(filePath, "utf-8");
|
|
1727
|
+
} catch {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
if (!content.trim()) continue;
|
|
1731
|
+
const hash = createHash2("sha256").update(content).digest("hex").slice(0, 16);
|
|
1732
|
+
const key = `${squad}/${agent}/${fileType}`;
|
|
1733
|
+
if (state.memory_hashes[key] === hash) continue;
|
|
1734
|
+
state.memory_hashes[key] = hash;
|
|
1735
|
+
const mapping = FILE_TYPE_MAPPING[fileType] || FILE_TYPE_MAPPING.state;
|
|
1736
|
+
const bullets = content.split("\n").filter((line) => line.trim().startsWith("- ") || line.trim().startsWith("* ")).map((line) => line.trim().replace(/^[-*]\s+/, "")).filter((line) => line.length > 10);
|
|
1737
|
+
if (bullets.length === 0) {
|
|
1738
|
+
addSignal(state, {
|
|
1739
|
+
source: mapping.source,
|
|
1740
|
+
signal_type: mapping.signal_type,
|
|
1741
|
+
value: null,
|
|
1742
|
+
unit: null,
|
|
1743
|
+
data: { content: content.slice(0, 500), content_hash: hash },
|
|
1744
|
+
entity_type: "memory_file",
|
|
1745
|
+
entity_id: key,
|
|
1746
|
+
confidence: 0.8
|
|
1747
|
+
});
|
|
1748
|
+
signalsCreated++;
|
|
1749
|
+
} else {
|
|
1750
|
+
for (const bullet of bullets.slice(0, 10)) {
|
|
1751
|
+
addSignal(state, {
|
|
1752
|
+
source: mapping.source,
|
|
1753
|
+
signal_type: mapping.signal_type,
|
|
1754
|
+
value: null,
|
|
1755
|
+
unit: null,
|
|
1756
|
+
data: { content: bullet, file: key, content_hash: hash },
|
|
1757
|
+
entity_type: "memory_file",
|
|
1758
|
+
entity_id: key,
|
|
1759
|
+
confidence: 0.8
|
|
1760
|
+
});
|
|
1761
|
+
signalsCreated++;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
ingestMemorySignal({ squad, agent, file_type: fileType, content, content_hash: hash });
|
|
1765
|
+
if (verbose) {
|
|
1766
|
+
writeLine(` ${colors.dim}Cognition: ${key} \u2192 ${bullets.length || 1} signals${RESET}`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return signalsCreated;
|
|
1772
|
+
}
|
|
1773
|
+
async function synthesizeSignals(state, verbose = false) {
|
|
1774
|
+
if (state.beliefs.length === 0) return 0;
|
|
1775
|
+
const cutoff = state.last_synthesize ? new Date(state.last_synthesize).getTime() : 0;
|
|
1776
|
+
const recentSignals = state.signals.filter(
|
|
1777
|
+
(s) => new Date(s.created_at).getTime() > cutoff
|
|
1778
|
+
);
|
|
1779
|
+
if (recentSignals.length === 0) return 0;
|
|
1780
|
+
let beliefsUpdated = 0;
|
|
1781
|
+
for (const belief of state.beliefs) {
|
|
1782
|
+
const signalList = recentSignals.map((s, i) => `${i + 1}. [${s.source}] ${s.signal_type}${s.value !== null ? " = " + s.value : ""}${s.unit ? " " + s.unit : ""}: ${(s.data.content || "").slice(0, 100)}`).join("\n");
|
|
1783
|
+
const prompt = `Given this belief: "${belief.statement}"
|
|
1784
|
+
|
|
1785
|
+
Classify each signal as SUPPORTING or CONTRADICTING or NEUTRAL.
|
|
1786
|
+
|
|
1787
|
+
Signals:
|
|
1788
|
+
${signalList}
|
|
1789
|
+
|
|
1790
|
+
Respond with JSON only: {"supporting": [indexes], "contradicting": [indexes], "neutral": [indexes]}`;
|
|
1791
|
+
try {
|
|
1792
|
+
const result = callClaude(prompt, "haiku", 3e4);
|
|
1793
|
+
if (!result) continue;
|
|
1794
|
+
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
|
1795
|
+
if (!jsonMatch) continue;
|
|
1796
|
+
const classification = JSON.parse(jsonMatch[0]);
|
|
1797
|
+
const supportingCount = classification.supporting?.length || 0;
|
|
1798
|
+
const contradictingCount = classification.contradicting?.length || 0;
|
|
1799
|
+
if (supportingCount + contradictingCount === 0) continue;
|
|
1800
|
+
const supportingIds = (classification.supporting || []).map((i) => recentSignals[i - 1]?.id).filter((id) => id !== void 0);
|
|
1801
|
+
const contradictingIds = (classification.contradicting || []).map((i) => recentSignals[i - 1]?.id).filter((id) => id !== void 0);
|
|
1802
|
+
const oldConfidence = belief.confidence;
|
|
1803
|
+
const evidenceRatio = supportingCount / (supportingCount + contradictingCount);
|
|
1804
|
+
let newConfidence = oldConfidence * CONFIDENCE_PRIOR_WEIGHT + evidenceRatio * CONFIDENCE_EVIDENCE_WEIGHT;
|
|
1805
|
+
newConfidence = Math.max(CONFIDENCE_MIN, Math.min(CONFIDENCE_MAX, newConfidence));
|
|
1806
|
+
belief.confidence = newConfidence;
|
|
1807
|
+
belief.supporting_signals = [...belief.supporting_signals, ...supportingIds].slice(-MAX_SUPPORTING_IDS);
|
|
1808
|
+
belief.contradicting_signals = [...belief.contradicting_signals, ...contradictingIds].slice(-MAX_SUPPORTING_IDS);
|
|
1809
|
+
belief.revision++;
|
|
1810
|
+
belief.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1811
|
+
belief.temperature = "hot";
|
|
1812
|
+
beliefsUpdated++;
|
|
1813
|
+
const shift = Math.abs(newConfidence - oldConfidence);
|
|
1814
|
+
if (shift >= BELIEF_SHIFT_THRESHOLD) {
|
|
1815
|
+
const direction = newConfidence > oldConfidence ? "\u2191" : "\u2193";
|
|
1816
|
+
slackNotify(
|
|
1817
|
+
`*Belief shift* ${direction} ${belief.belief_key}: ${(oldConfidence * 100).toFixed(0)}% \u2192 ${(newConfidence * 100).toFixed(0)}%
|
|
1818
|
+
${belief.statement}`
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
if (verbose) {
|
|
1822
|
+
writeLine(` ${colors.dim}Belief: ${belief.belief_key} ${(oldConfidence * 100).toFixed(0)}% \u2192 ${(newConfidence * 100).toFixed(0)}% (+${supportingCount}/-${contradictingCount})${RESET}`);
|
|
1823
|
+
}
|
|
1824
|
+
} catch {
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
state.last_synthesize = (/* @__PURE__ */ new Date()).toISOString();
|
|
1829
|
+
return beliefsUpdated;
|
|
1830
|
+
}
|
|
1831
|
+
function evaluateDecisions(state) {
|
|
1832
|
+
const twoHoursAgo = Date.now() - 2 * 60 * 60 * 1e3;
|
|
1833
|
+
let evaluated = 0;
|
|
1834
|
+
for (const decision of state.decisions) {
|
|
1835
|
+
if (decision.outcome_score !== null) continue;
|
|
1836
|
+
if (new Date(decision.decided_at).getTime() > twoHoursAgo) continue;
|
|
1837
|
+
const decisionTime = new Date(decision.decided_at).getTime();
|
|
1838
|
+
const relevantSignals = state.signals.filter(
|
|
1839
|
+
(s) => new Date(s.created_at).getTime() > decisionTime
|
|
1840
|
+
);
|
|
1841
|
+
const completed = relevantSignals.filter(
|
|
1842
|
+
(s) => s.signal_type === "agent_completed" || s.signal_type === "conversation_converged"
|
|
1843
|
+
).length;
|
|
1844
|
+
const failed = relevantSignals.filter(
|
|
1845
|
+
(s) => s.signal_type === "agent_failed" || s.signal_type === "conversation_stopped"
|
|
1846
|
+
).length;
|
|
1847
|
+
if (completed + failed < 3) continue;
|
|
1848
|
+
decision.outcome_score = completed / (completed + failed) * 2 - 1;
|
|
1849
|
+
decision.actual_outcome = { completed, failed, total: completed + failed };
|
|
1850
|
+
evaluated++;
|
|
1851
|
+
}
|
|
1852
|
+
return evaluated;
|
|
1853
|
+
}
|
|
1854
|
+
async function reflect(state, verbose = false) {
|
|
1855
|
+
if (state.last_reflect) {
|
|
1856
|
+
const elapsed = Date.now() - new Date(state.last_reflect).getTime();
|
|
1857
|
+
if (elapsed < REFLECT_INTERVAL_MS) return null;
|
|
1858
|
+
}
|
|
1859
|
+
const lastReflectTime = state.last_reflect ? new Date(state.last_reflect).getTime() : 0;
|
|
1860
|
+
const newSignals = state.signals.filter((s) => new Date(s.created_at).getTime() > lastReflectTime);
|
|
1861
|
+
if (newSignals.length === 0) return null;
|
|
1862
|
+
const beliefsText = state.beliefs.map((b) => `- [${b.domain}] ${b.belief_key} (${(b.confidence * 100).toFixed(0)}%, ${b.temperature}, r${b.revision}): ${b.statement}`).join("\n");
|
|
1863
|
+
const signalsText = newSignals.slice(-30).map((s) => `- [${s.source}] ${s.signal_type}: ${(s.data.content || "").slice(0, 80)} (${new Date(s.created_at).toLocaleTimeString()})`).join("\n");
|
|
1864
|
+
const decisionsText = state.decisions.map((d) => `- ${d.title} (score: ${d.outcome_score !== null ? d.outcome_score.toFixed(2) : "pending"})`).join("\n");
|
|
1865
|
+
const lastReflection = state.reflections.length > 0 ? state.reflections[state.reflections.length - 1] : null;
|
|
1866
|
+
const prompt = `You are the cognition engine for an AI-native company called Agents Squads.
|
|
1867
|
+
Your job is to reflect on the current state of the business and produce actionable insights.
|
|
1868
|
+
|
|
1869
|
+
## Current Beliefs (world model)
|
|
1870
|
+
${beliefsText || "(none)"}
|
|
1871
|
+
|
|
1872
|
+
## Recent Signals (since last reflection)
|
|
1873
|
+
${signalsText || "(none)"}
|
|
1874
|
+
|
|
1875
|
+
## Decision Journal
|
|
1876
|
+
${decisionsText || "(none)"}
|
|
1877
|
+
|
|
1878
|
+
${lastReflection ? `Previous reflection (${lastReflection.created_at}):
|
|
1879
|
+
${lastReflection.assessment}
|
|
1880
|
+
` : ""}
|
|
1881
|
+
|
|
1882
|
+
## Your Task
|
|
1883
|
+
Produce a business reflection. Respond as JSON only:
|
|
1884
|
+
{
|
|
1885
|
+
"assessment": "2-3 sentence summary of business state",
|
|
1886
|
+
"insights": [{"type": "highlight|warning|recommendation", "message": "..."}],
|
|
1887
|
+
"belief_updates": [{"belief_key": "...", "suggested_confidence": 0.X, "reason": "..."}],
|
|
1888
|
+
"priority_adjustments": [{"description": "...", "urgency": "high|medium|low"}],
|
|
1889
|
+
"founder_escalations": [{"issue": "...", "why_human_needed": "...", "suggested_action": "...", "urgency": "immediate|today|this_week"}]
|
|
1890
|
+
}`;
|
|
1891
|
+
try {
|
|
1892
|
+
const result = callClaude(prompt, "sonnet", 6e4);
|
|
1893
|
+
if (!result) return null;
|
|
1894
|
+
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
|
1895
|
+
if (!jsonMatch) return null;
|
|
1896
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
1897
|
+
const reflection = {
|
|
1898
|
+
id: state.next_reflection_id++,
|
|
1899
|
+
scope: "business",
|
|
1900
|
+
assessment: parsed.assessment || "",
|
|
1901
|
+
insights: parsed.insights || [],
|
|
1902
|
+
belief_updates: parsed.belief_updates || [],
|
|
1903
|
+
priority_adjustments: parsed.priority_adjustments || [],
|
|
1904
|
+
founder_escalations: parsed.founder_escalations || [],
|
|
1905
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1906
|
+
};
|
|
1907
|
+
state.reflections.push(reflection);
|
|
1908
|
+
if (state.reflections.length > MAX_REFLECTIONS_KEPT) {
|
|
1909
|
+
state.reflections = state.reflections.slice(-MAX_REFLECTIONS_KEPT);
|
|
1910
|
+
}
|
|
1911
|
+
for (const update of reflection.belief_updates) {
|
|
1912
|
+
const belief = state.beliefs.find((b) => b.belief_key === update.belief_key);
|
|
1913
|
+
if (belief && update.suggested_confidence >= 0 && update.suggested_confidence <= 1) {
|
|
1914
|
+
belief.confidence = update.suggested_confidence;
|
|
1915
|
+
belief.revision++;
|
|
1916
|
+
belief.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1917
|
+
belief.temperature = "hot";
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
if (reflection.founder_escalations.length > 0) {
|
|
1921
|
+
const escalationText = reflection.founder_escalations.map((e) => `\u2022 *${e.issue}*: ${e.suggested_action} (${e.urgency})`).join("\n");
|
|
1922
|
+
slackNotify(`\u{1F9E0} *Cognition reflection*
|
|
1923
|
+
${reflection.assessment}
|
|
1924
|
+
|
|
1925
|
+
*Escalations:*
|
|
1926
|
+
${escalationText}`);
|
|
1927
|
+
} else if (verbose) {
|
|
1928
|
+
slackNotify(`\u{1F9E0} *Cognition reflection*
|
|
1929
|
+
${reflection.assessment}`);
|
|
1930
|
+
}
|
|
1931
|
+
state.last_reflect = (/* @__PURE__ */ new Date()).toISOString();
|
|
1932
|
+
if (verbose) {
|
|
1933
|
+
writeLine(` ${colors.dim}Reflection: ${reflection.insights.length} insights, ${reflection.belief_updates.length} belief updates, ${reflection.founder_escalations.length} escalations${RESET}`);
|
|
1934
|
+
}
|
|
1935
|
+
return reflection;
|
|
1936
|
+
} catch {
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function updateBeliefTemperatures(state) {
|
|
1941
|
+
const now = Date.now();
|
|
1942
|
+
for (const belief of state.beliefs) {
|
|
1943
|
+
const age = now - new Date(belief.updated_at).getTime();
|
|
1944
|
+
if (age < 4 * 60 * 60 * 1e3) {
|
|
1945
|
+
belief.temperature = "hot";
|
|
1946
|
+
} else if (age < 24 * 60 * 60 * 1e3) {
|
|
1947
|
+
belief.temperature = "warm";
|
|
1948
|
+
} else {
|
|
1949
|
+
belief.temperature = "cold";
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
async function runCognitionCycle(squads, verbose = false) {
|
|
1954
|
+
const state = loadCognitionState();
|
|
1955
|
+
const signalsIngested = ingestMemoryFiles(state, squads, verbose);
|
|
1956
|
+
let beliefsUpdated = 0;
|
|
1957
|
+
const timeSinceSynthesize = state.last_synthesize ? Date.now() - new Date(state.last_synthesize).getTime() : Infinity;
|
|
1958
|
+
if (timeSinceSynthesize >= SYNTHESIZE_INTERVAL_MS && state.signals.length > 0) {
|
|
1959
|
+
beliefsUpdated = await synthesizeSignals(state, verbose);
|
|
1960
|
+
}
|
|
1961
|
+
const decisionsEvaluated = evaluateDecisions(state);
|
|
1962
|
+
const reflection = await reflect(state, verbose);
|
|
1963
|
+
updateBeliefTemperatures(state);
|
|
1964
|
+
saveCognitionState(state);
|
|
1965
|
+
if (verbose || signalsIngested > 0 || beliefsUpdated > 0 || reflection) {
|
|
1966
|
+
writeLine(` ${colors.dim}Cognition: ${signalsIngested} signals, ${beliefsUpdated} beliefs updated${reflection ? ", reflected" : ""}${RESET}`);
|
|
1967
|
+
}
|
|
1968
|
+
return {
|
|
1969
|
+
signalsIngested,
|
|
1970
|
+
beliefsUpdated,
|
|
1971
|
+
decisionsEvaluated,
|
|
1972
|
+
reflected: !!reflection
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
function seedBeliefsIfEmpty(state) {
|
|
1976
|
+
if (state.beliefs.length > 0) return;
|
|
1977
|
+
const seeds = [
|
|
1978
|
+
{ belief_key: "retention_critical", domain: "product", statement: "D1 retention (10%) is the primary blocker to product-market fit. Must reach 30% before monetizing.", confidence: 0.9 },
|
|
1979
|
+
{ belief_key: "cli_is_os", domain: "product", statement: "The CLI is our operating system. Every improvement multiplies autonomous capability.", confidence: 0.85 },
|
|
1980
|
+
{ belief_key: "zero_revenue", domain: "revenue", statement: "Revenue is $0. Consulting is the near-term path. Pro tier gated on retention.", confidence: 0.95 },
|
|
1981
|
+
{ belief_key: "agent_autonomy_low", domain: "operations", statement: "Agents run but do not think autonomously. Scanners and leads never fire. Intelligence loop is broken.", confidence: 0.8 },
|
|
1982
|
+
{ belief_key: "first_run_broken", domain: "product", statement: "First-run experience is broken. v0.7.0 crashes on squads run. Users cannot complete the core flow.", confidence: 0.9 },
|
|
1983
|
+
{ belief_key: "global_developer_focus", domain: "market", statement: "Target market is global developers, not Chilean enterprises. Product-first, not consulting-first.", confidence: 0.75 },
|
|
1984
|
+
{ belief_key: "test_user_simulation", domain: "operations", statement: "Simulating test users (fresh install \u2192 init \u2192 run \u2192 evaluate friction) is the most effective way to find and fix retention blockers.", confidence: 0.7 },
|
|
1985
|
+
{ belief_key: "cognition_engine_needed", domain: "operations", statement: "Without a working cognition engine, the organization cannot learn or improve autonomously. This is the difference between a cron job and intelligence.", confidence: 0.85 }
|
|
1986
|
+
];
|
|
1987
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1988
|
+
for (const seed of seeds) {
|
|
1989
|
+
state.beliefs.push({
|
|
1990
|
+
...seed,
|
|
1991
|
+
supporting_signals: [],
|
|
1992
|
+
contradicting_signals: [],
|
|
1993
|
+
temperature: "warm",
|
|
1994
|
+
revision: 1,
|
|
1995
|
+
updated_at: now
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// src/commands/run.ts
|
|
2001
|
+
var CLOUD_POLL_INTERVAL_MS = 3e3;
|
|
2002
|
+
var CLOUD_POLL_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2003
|
+
var DEFAULT_LEARNINGS_LIMIT = 5;
|
|
2004
|
+
var EXECUTION_EVENT_TIMEOUT_MS = 5e3;
|
|
2005
|
+
var VERIFICATION_STATE_MAX_CHARS = 2e3;
|
|
2006
|
+
var VERIFICATION_EXEC_TIMEOUT_MS = 3e4;
|
|
2007
|
+
var DRYRUN_DEF_MAX_CHARS = 500;
|
|
2008
|
+
var DRYRUN_CONTEXT_MAX_CHARS = 800;
|
|
2009
|
+
var DEFAULT_SCHEDULED_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
|
|
2010
|
+
var DEFAULT_TIMEOUT_MINUTES = 30;
|
|
2011
|
+
var SOFT_DEADLINE_RATIO = 0.7;
|
|
2012
|
+
var LOG_FILE_INIT_DELAY_MS = 500;
|
|
2013
|
+
var VERBOSE_COMMAND_MAX_CHARS = 50;
|
|
2014
|
+
async function registerContextWithBridge(ctx) {
|
|
2015
|
+
const bridgeUrl = getBridgeUrl();
|
|
2016
|
+
try {
|
|
2017
|
+
const response = await fetch(`${bridgeUrl}/api/context/register`, {
|
|
2018
|
+
method: "POST",
|
|
2019
|
+
headers: { "Content-Type": "application/json" },
|
|
2020
|
+
body: JSON.stringify({
|
|
2021
|
+
execution_id: ctx.executionId,
|
|
2022
|
+
squad: ctx.squad,
|
|
2023
|
+
agent: ctx.agent,
|
|
2024
|
+
task_type: ctx.taskType,
|
|
2025
|
+
trigger: ctx.trigger
|
|
2026
|
+
}),
|
|
2027
|
+
signal: AbortSignal.timeout(3e3)
|
|
2028
|
+
});
|
|
2029
|
+
if (!response.ok) {
|
|
2030
|
+
return false;
|
|
2031
|
+
}
|
|
2032
|
+
return true;
|
|
2033
|
+
} catch (e) {
|
|
2034
|
+
writeLine(` ${colors.dim}warn: bridge registration failed: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2035
|
+
return false;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async function checkPreflightGates(squad, agent) {
|
|
2039
|
+
const bridgeUrl = getBridgeUrl();
|
|
2040
|
+
try {
|
|
2041
|
+
const response = await fetch(`${bridgeUrl}/api/execution/preflight`, {
|
|
2042
|
+
method: "POST",
|
|
2043
|
+
headers: { "Content-Type": "application/json" },
|
|
2044
|
+
body: JSON.stringify({ squad, agent }),
|
|
2045
|
+
signal: AbortSignal.timeout(3e3)
|
|
2046
|
+
});
|
|
2047
|
+
if (!response.ok) {
|
|
2048
|
+
return { allowed: true, gates: {} };
|
|
2049
|
+
}
|
|
2050
|
+
return await response.json();
|
|
2051
|
+
} catch (e) {
|
|
2052
|
+
writeLine(` ${colors.dim}warn: preflight gate check failed (allowing execution): ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2053
|
+
return { allowed: true, gates: {} };
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
async function fetchLearnings(squad, limit = DEFAULT_LEARNINGS_LIMIT) {
|
|
2057
|
+
const bridgeUrl = getBridgeUrl();
|
|
2058
|
+
try {
|
|
2059
|
+
const response = await fetch(
|
|
2060
|
+
`${bridgeUrl}/api/learnings/relevant?squad=${encodeURIComponent(squad)}&limit=${limit}`,
|
|
2061
|
+
{ signal: AbortSignal.timeout(3e3) }
|
|
2062
|
+
);
|
|
2063
|
+
if (!response.ok) {
|
|
2064
|
+
return [];
|
|
2065
|
+
}
|
|
2066
|
+
const data = await response.json();
|
|
2067
|
+
return data.learnings || [];
|
|
2068
|
+
} catch (e) {
|
|
2069
|
+
writeLine(` ${colors.dim}warn: learnings fetch failed: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2070
|
+
return [];
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
function generateExecutionId() {
|
|
2074
|
+
const timestamp = Date.now().toString(36);
|
|
2075
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
2076
|
+
return `exec_${timestamp}_${random}`;
|
|
2077
|
+
}
|
|
2078
|
+
function selectMcpConfig(squadName, squad) {
|
|
2079
|
+
if (squad?.context?.mcp && squad.context.mcp.length > 0) {
|
|
2080
|
+
return resolveMcpConfigPath(squadName, squad.context.mcp);
|
|
2081
|
+
}
|
|
2082
|
+
const home = process.env.HOME || "";
|
|
2083
|
+
const configsDir = join5(home, ".claude", "mcp-configs");
|
|
2084
|
+
const squadConfigs = {
|
|
2085
|
+
website: "website.json",
|
|
2086
|
+
research: "research.json",
|
|
2087
|
+
intelligence: "research.json",
|
|
2088
|
+
analytics: "data.json",
|
|
2089
|
+
engineering: "data.json"
|
|
2090
|
+
};
|
|
2091
|
+
const configFile = squadConfigs[squadName.toLowerCase()];
|
|
2092
|
+
if (configFile) {
|
|
2093
|
+
const configPath = join5(configsDir, configFile);
|
|
2094
|
+
if (existsSync5(configPath)) {
|
|
2095
|
+
return configPath;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
return "";
|
|
2099
|
+
}
|
|
2100
|
+
function detectTaskType(agentName) {
|
|
2101
|
+
const name = agentName.toLowerCase();
|
|
2102
|
+
if (name.includes("eval") || name.includes("critic") || name.includes("review") || name.includes("test")) {
|
|
2103
|
+
return "evaluation";
|
|
2104
|
+
}
|
|
2105
|
+
if (name.includes("lead") || name.includes("orchestrator")) {
|
|
2106
|
+
return "lead";
|
|
2107
|
+
}
|
|
2108
|
+
if (name.includes("research") || name.includes("analyst") || name.includes("intel")) {
|
|
2109
|
+
return "research";
|
|
2110
|
+
}
|
|
2111
|
+
return "execution";
|
|
2112
|
+
}
|
|
2113
|
+
function getClaudeModelAlias(model) {
|
|
2114
|
+
const lower = model.toLowerCase();
|
|
2115
|
+
if (lower === "opus" || lower === "sonnet" || lower === "haiku") {
|
|
2116
|
+
return lower;
|
|
2117
|
+
}
|
|
2118
|
+
if (lower.includes("opus")) return "opus";
|
|
2119
|
+
if (lower.includes("sonnet")) return "sonnet";
|
|
2120
|
+
if (lower.includes("haiku")) return "haiku";
|
|
2121
|
+
return void 0;
|
|
2122
|
+
}
|
|
2123
|
+
function resolveModel(explicitModel, squad, taskType) {
|
|
2124
|
+
if (explicitModel) {
|
|
2125
|
+
return explicitModel;
|
|
2126
|
+
}
|
|
2127
|
+
const modelConfig = squad?.context?.model;
|
|
2128
|
+
if (!modelConfig) {
|
|
2129
|
+
return void 0;
|
|
2130
|
+
}
|
|
2131
|
+
switch (taskType) {
|
|
2132
|
+
case "evaluation":
|
|
2133
|
+
return modelConfig.cheap || modelConfig.default;
|
|
2134
|
+
case "lead":
|
|
2135
|
+
return modelConfig.expensive || modelConfig.default;
|
|
2136
|
+
case "research":
|
|
2137
|
+
case "execution":
|
|
2138
|
+
default:
|
|
2139
|
+
return modelConfig.default;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
function ensureProjectTrusted(projectPath) {
|
|
2143
|
+
const configPath = join5(process.env.HOME || "", ".claude.json");
|
|
2144
|
+
if (!existsSync5(configPath)) {
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
try {
|
|
2148
|
+
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2149
|
+
if (!config.projects) {
|
|
2150
|
+
config.projects = {};
|
|
2151
|
+
}
|
|
2152
|
+
if (!config.projects[projectPath]) {
|
|
2153
|
+
config.projects[projectPath] = {};
|
|
2154
|
+
}
|
|
2155
|
+
if (!config.projects[projectPath].hasTrustDialogAccepted) {
|
|
2156
|
+
config.projects[projectPath].hasTrustDialogAccepted = true;
|
|
2157
|
+
writeFileSync4(configPath, JSON.stringify(config, null, 2));
|
|
2158
|
+
}
|
|
2159
|
+
} catch (e) {
|
|
2160
|
+
writeLine(` ${colors.dim}warn: config update failed: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function getProjectRoot() {
|
|
2164
|
+
const squadsDir = findSquadsDir();
|
|
2165
|
+
if (squadsDir) {
|
|
2166
|
+
return dirname2(dirname2(squadsDir));
|
|
2167
|
+
}
|
|
2168
|
+
return process.cwd();
|
|
2169
|
+
}
|
|
2170
|
+
function getExecutionLogPath(squadName, agentName) {
|
|
2171
|
+
const memoryDir = findMemoryDir();
|
|
2172
|
+
if (!memoryDir) return null;
|
|
2173
|
+
return join5(memoryDir, squadName, agentName, "executions.md");
|
|
2174
|
+
}
|
|
2175
|
+
function logExecution(record) {
|
|
2176
|
+
const logPath = getExecutionLogPath(record.squadName, record.agentName);
|
|
2177
|
+
if (!logPath) return;
|
|
2178
|
+
const dir = dirname2(logPath);
|
|
2179
|
+
if (!existsSync5(dir)) {
|
|
2180
|
+
mkdirSync4(dir, { recursive: true });
|
|
2181
|
+
}
|
|
2182
|
+
let content = "";
|
|
2183
|
+
if (existsSync5(logPath)) {
|
|
2184
|
+
content = readFileSync4(logPath, "utf-8").trimEnd();
|
|
2185
|
+
} else {
|
|
2186
|
+
content = `# ${record.squadName}/${record.agentName} - Execution Log`;
|
|
2187
|
+
}
|
|
2188
|
+
const entry = `
|
|
2189
|
+
|
|
2190
|
+
---
|
|
2191
|
+
<!-- exec:${record.executionId} -->
|
|
2192
|
+
**${record.startTime}** | Status: ${record.status}
|
|
2193
|
+
- ID: \`${record.executionId}\`
|
|
2194
|
+
- Trigger: ${record.trigger || "manual"}
|
|
2195
|
+
- Task Type: ${record.taskType || "execution"}
|
|
2196
|
+
`;
|
|
2197
|
+
writeFileSync4(logPath, content + entry);
|
|
2198
|
+
}
|
|
2199
|
+
function updateExecutionStatus(squadName, agentName, executionId, status, details) {
|
|
2200
|
+
const logPath = getExecutionLogPath(squadName, agentName);
|
|
2201
|
+
if (!logPath || !existsSync5(logPath)) return;
|
|
2202
|
+
let content = readFileSync4(logPath, "utf-8");
|
|
2203
|
+
const endTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
2204
|
+
const execMarker = `<!-- exec:${executionId} -->`;
|
|
2205
|
+
const markerIndex = content.indexOf(execMarker);
|
|
2206
|
+
if (markerIndex === -1) return;
|
|
2207
|
+
const nextEntryIndex = content.indexOf("\n---\n", markerIndex + 1);
|
|
2208
|
+
const entryEnd = nextEntryIndex === -1 ? content.length : nextEntryIndex;
|
|
2209
|
+
const entryStart = content.lastIndexOf("\n---\n", markerIndex);
|
|
2210
|
+
const currentEntry = content.slice(entryStart, entryEnd);
|
|
2211
|
+
const durationStr = details?.durationMs ? `${(details.durationMs / 1e3).toFixed(1)}s` : "unknown";
|
|
2212
|
+
let updatedEntry = currentEntry.replace(/Status: running/, `Status: ${status}`) + `- Completed: ${endTime}
|
|
2213
|
+
- Duration: ${durationStr}`;
|
|
2214
|
+
if (details?.outcome) {
|
|
2215
|
+
updatedEntry += `
|
|
2216
|
+
- Outcome: ${details.outcome}`;
|
|
2217
|
+
}
|
|
2218
|
+
if (details?.error) {
|
|
2219
|
+
updatedEntry += `
|
|
2220
|
+
- Error: ${details.error}`;
|
|
2221
|
+
}
|
|
2222
|
+
content = content.slice(0, entryStart) + updatedEntry + content.slice(entryEnd);
|
|
2223
|
+
writeFileSync4(logPath, content);
|
|
2224
|
+
}
|
|
2225
|
+
async function autoCommitAgentWork(squadName, agentName, executionId, provider) {
|
|
2226
|
+
const { execSync: execSync4 } = await import("child_process");
|
|
2227
|
+
const { detectGitHubRepo } = await import("./github-UQTM5KMS.js");
|
|
2228
|
+
const projectRoot = getProjectRoot();
|
|
2229
|
+
try {
|
|
2230
|
+
const status = execSync4("git status --porcelain", {
|
|
2231
|
+
encoding: "utf-8",
|
|
2232
|
+
cwd: projectRoot
|
|
2233
|
+
}).trim();
|
|
2234
|
+
if (!status) {
|
|
2235
|
+
return { committed: false };
|
|
2236
|
+
}
|
|
2237
|
+
const botEnv = await getBotGitEnv();
|
|
2238
|
+
const execOpts = {
|
|
2239
|
+
cwd: projectRoot,
|
|
2240
|
+
env: { ...process.env, ...botEnv }
|
|
2241
|
+
};
|
|
2242
|
+
execSync4("git add -A", execOpts);
|
|
2243
|
+
const shortExecId = executionId.slice(0, 12);
|
|
2244
|
+
const coAuthor = getCoAuthorTrailer(provider || "claude");
|
|
2245
|
+
const msgFile = join5(projectRoot, ".git", "SQUADS_COMMIT_MSG");
|
|
2246
|
+
writeFileSync4(msgFile, `feat(${squadName}/${agentName}): execution ${shortExecId}
|
|
2247
|
+
|
|
2248
|
+
${coAuthor}
|
|
2249
|
+
`);
|
|
2250
|
+
try {
|
|
2251
|
+
execSync4(`git commit --file "${msgFile}"`, execOpts);
|
|
2252
|
+
} finally {
|
|
2253
|
+
try {
|
|
2254
|
+
unlinkSync(msgFile);
|
|
2255
|
+
} catch {
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
2260
|
+
const repo = detectGitHubRepo(projectRoot);
|
|
2261
|
+
if (repo && /^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
2262
|
+
const pushUrl = await getBotPushUrl(repo);
|
|
2263
|
+
if (pushUrl) {
|
|
2264
|
+
spawnSync2("git", ["push", pushUrl, "HEAD"], { ...execOpts, stdio: "pipe" });
|
|
2265
|
+
} else {
|
|
2266
|
+
spawnSync2("git", ["push", "origin", "HEAD"], { ...execOpts, stdio: "pipe" });
|
|
2267
|
+
}
|
|
2268
|
+
} else {
|
|
2269
|
+
spawnSync2("git", ["push", "origin", "HEAD"], { ...execOpts, stdio: "pipe" });
|
|
2270
|
+
}
|
|
2271
|
+
} catch (e) {
|
|
2272
|
+
writeLine(` ${colors.dim}warn: git push failed (commit is still local): ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2273
|
+
}
|
|
2274
|
+
return { committed: true, message: `Committed changes from ${agentName}` };
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
return { committed: false, error: String(error) };
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function getLastExecutionTime(squadName, agentName) {
|
|
2280
|
+
const logPath = getExecutionLogPath(squadName, agentName);
|
|
2281
|
+
if (!logPath || !existsSync5(logPath)) return null;
|
|
2282
|
+
const content = readFileSync4(logPath, "utf-8");
|
|
2283
|
+
const timestamps = content.match(/\*\*(\d{4}-\d{2}-\d{2}T[\d:.]+Z)\*\*/g);
|
|
2284
|
+
if (!timestamps || timestamps.length === 0) return null;
|
|
2285
|
+
const lastTimestamp = timestamps[timestamps.length - 1].replace(/\*\*/g, "");
|
|
2286
|
+
return new Date(lastTimestamp);
|
|
2287
|
+
}
|
|
2288
|
+
function checkLocalCooldown(squadName, agentName, cooldownMs) {
|
|
2289
|
+
const lastExec = getLastExecutionTime(squadName, agentName);
|
|
2290
|
+
if (!lastExec) return { ok: true, cooldownMs };
|
|
2291
|
+
const elapsedMs = Date.now() - lastExec.getTime();
|
|
2292
|
+
if (elapsedMs < cooldownMs) {
|
|
2293
|
+
return { ok: false, elapsedMs, cooldownMs };
|
|
2294
|
+
}
|
|
2295
|
+
return { ok: true, elapsedMs, cooldownMs };
|
|
2296
|
+
}
|
|
2297
|
+
function formatDuration(ms) {
|
|
2298
|
+
const hours = Math.floor(ms / (60 * 60 * 1e3));
|
|
2299
|
+
const minutes = Math.floor(ms % (60 * 60 * 1e3) / (60 * 1e3));
|
|
2300
|
+
if (hours >= 24) {
|
|
2301
|
+
const days = Math.floor(hours / 24);
|
|
2302
|
+
const remainingHours = hours % 24;
|
|
2303
|
+
return remainingHours > 0 ? `${days}d ${remainingHours}h` : `${days}d`;
|
|
2304
|
+
}
|
|
2305
|
+
if (hours > 0) {
|
|
2306
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
2307
|
+
}
|
|
2308
|
+
return `${minutes}m`;
|
|
2309
|
+
}
|
|
2310
|
+
async function emitExecutionEvent(eventType, data) {
|
|
2311
|
+
const apiUrl = getApiUrl();
|
|
2312
|
+
if (apiUrl) {
|
|
2313
|
+
try {
|
|
2314
|
+
await fetch(`${apiUrl}/events/ingest`, {
|
|
2315
|
+
method: "POST",
|
|
2316
|
+
headers: { "Content-Type": "application/json" },
|
|
2317
|
+
body: JSON.stringify({
|
|
2318
|
+
source: "scheduler",
|
|
2319
|
+
event_type: eventType,
|
|
2320
|
+
data: {
|
|
2321
|
+
squad: data.squad,
|
|
2322
|
+
agent: data.agent,
|
|
2323
|
+
execution_id: data.executionId,
|
|
2324
|
+
...data.error ? { error: data.error } : {}
|
|
2325
|
+
}
|
|
2326
|
+
}),
|
|
2327
|
+
signal: AbortSignal.timeout(EXECUTION_EVENT_TIMEOUT_MS)
|
|
2328
|
+
});
|
|
2329
|
+
return;
|
|
2330
|
+
} catch {
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
try {
|
|
2334
|
+
const memDir = findMemoryDir();
|
|
2335
|
+
if (!memDir) return;
|
|
2336
|
+
const eventsDir = join5(memDir, data.squad, data.agent);
|
|
2337
|
+
if (!existsSync5(eventsDir)) {
|
|
2338
|
+
mkdirSync4(eventsDir, { recursive: true });
|
|
2339
|
+
}
|
|
2340
|
+
const eventsPath = join5(eventsDir, "events.md");
|
|
2341
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2342
|
+
const entry = `
|
|
2343
|
+
## ${timestamp}: ${eventType}
|
|
2344
|
+
- execution_id: ${data.executionId}
|
|
2345
|
+
${data.error ? `- error: ${data.error}
|
|
2346
|
+
` : ""}`;
|
|
2347
|
+
let existing = "";
|
|
2348
|
+
if (existsSync5(eventsPath)) {
|
|
2349
|
+
existing = readFileSync4(eventsPath, "utf-8");
|
|
2350
|
+
}
|
|
2351
|
+
writeFileSync4(eventsPath, existing + entry);
|
|
2352
|
+
} catch {
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
async function verifyExecution(squadName, agentName, criteria, options = {}) {
|
|
2356
|
+
const { execSync: execSync4 } = await import("child_process");
|
|
2357
|
+
const projectRoot = getProjectRoot();
|
|
2358
|
+
let stateContent = "";
|
|
2359
|
+
const memDir = findMemoryDir();
|
|
2360
|
+
if (memDir) {
|
|
2361
|
+
const statePath = join5(memDir, squadName, agentName, "state.md");
|
|
2362
|
+
if (existsSync5(statePath)) {
|
|
2363
|
+
stateContent = readFileSync4(statePath, "utf-8").slice(0, VERIFICATION_STATE_MAX_CHARS);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
let recentCommits = "";
|
|
2367
|
+
try {
|
|
2368
|
+
recentCommits = execSync4("git log --oneline -5 --no-color", {
|
|
2369
|
+
encoding: "utf-8",
|
|
2370
|
+
cwd: projectRoot
|
|
2371
|
+
}).trim();
|
|
2372
|
+
} catch (e) {
|
|
2373
|
+
if (options.verbose) writeLine(` ${colors.dim}warn: git log failed: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2374
|
+
recentCommits = "(no commits found)";
|
|
2375
|
+
}
|
|
2376
|
+
const verifyPrompt = `You are verifying whether an agent completed its task successfully.
|
|
2377
|
+
|
|
2378
|
+
Agent: ${squadName}/${agentName}
|
|
2379
|
+
|
|
2380
|
+
## Acceptance Criteria
|
|
2381
|
+
${criteria}
|
|
2382
|
+
|
|
2383
|
+
## Evidence
|
|
2384
|
+
|
|
2385
|
+
### Agent State File
|
|
2386
|
+
${stateContent || "(empty or not found)"}
|
|
2387
|
+
|
|
2388
|
+
### Recent Git Commits
|
|
2389
|
+
${recentCommits}
|
|
2390
|
+
|
|
2391
|
+
## Instructions
|
|
2392
|
+
Evaluate whether the acceptance criteria are met based on the evidence.
|
|
2393
|
+
Respond with EXACTLY one line:
|
|
2394
|
+
PASS: <brief reason>
|
|
2395
|
+
or
|
|
2396
|
+
FAIL: <brief reason>`;
|
|
2397
|
+
try {
|
|
2398
|
+
const escapedPrompt = verifyPrompt.replace(/'/g, "'\\''");
|
|
2399
|
+
const result = execSync4(
|
|
2400
|
+
`unset CLAUDECODE; claude --print --model haiku -- '${escapedPrompt}'`,
|
|
2401
|
+
{ encoding: "utf-8", cwd: projectRoot, timeout: VERIFICATION_EXEC_TIMEOUT_MS, shell: "/bin/sh" }
|
|
2402
|
+
).trim();
|
|
2403
|
+
if (options.verbose) {
|
|
2404
|
+
writeLine(` ${colors.dim}Verification: ${result}${RESET}`);
|
|
2405
|
+
}
|
|
2406
|
+
if (result.startsWith("PASS")) {
|
|
2407
|
+
return { passed: true, reason: result.replace(/^PASS:\s*/, "") };
|
|
2408
|
+
}
|
|
2409
|
+
return { passed: false, reason: result.replace(/^FAIL:\s*/, "") };
|
|
2410
|
+
} catch (error) {
|
|
2411
|
+
if (options.verbose) {
|
|
2412
|
+
writeLine(` ${colors.dim}Verification error (defaulting to PASS): ${error}${RESET}`);
|
|
2413
|
+
}
|
|
2414
|
+
return { passed: true, reason: "Verification unavailable \u2014 defaulting to pass" };
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
async function runCloudDispatch(squadName, agentName, options) {
|
|
2418
|
+
const apiUrl = getApiUrl();
|
|
2419
|
+
if (!apiUrl) {
|
|
2420
|
+
writeLine(` ${colors.red}${icons.error} API URL not configured${RESET}`);
|
|
2421
|
+
writeLine(` ${colors.dim}Run: squads config use staging (or set SQUADS_API_URL)${RESET}`);
|
|
2422
|
+
process.exit(1);
|
|
2423
|
+
}
|
|
2424
|
+
if (!isLoggedIn()) {
|
|
2425
|
+
writeLine(` ${colors.red}${icons.error} Not logged in${RESET}`);
|
|
2426
|
+
writeLine(` ${colors.dim}Run \`squads login\` to authenticate before using --cloud${RESET}`);
|
|
2427
|
+
process.exit(1);
|
|
2428
|
+
}
|
|
2429
|
+
const session = loadSession();
|
|
2430
|
+
const headers = {
|
|
2431
|
+
"Content-Type": "application/json"
|
|
2432
|
+
};
|
|
2433
|
+
if (session?.accessToken) {
|
|
2434
|
+
headers["Authorization"] = `Bearer ${session.accessToken}`;
|
|
2435
|
+
}
|
|
2436
|
+
const apiKey = process.env.SQUADS_PLATFORM_API_TOKEN || process.env.SCHEDULER_API_KEY;
|
|
2437
|
+
if (apiKey) {
|
|
2438
|
+
headers["X-API-Key"] = apiKey;
|
|
2439
|
+
}
|
|
2440
|
+
const spinner = ora(`Dispatching ${squadName}/${agentName} to cloud...`).start();
|
|
2441
|
+
try {
|
|
2442
|
+
const dispatchRes = await fetch(`${apiUrl}/agent-dispatch`, {
|
|
2443
|
+
method: "POST",
|
|
2444
|
+
headers,
|
|
2445
|
+
body: JSON.stringify({
|
|
2446
|
+
squad: squadName,
|
|
2447
|
+
agent: agentName,
|
|
2448
|
+
trigger_type: "manual",
|
|
2449
|
+
trigger_data: {
|
|
2450
|
+
source: "cli",
|
|
2451
|
+
cloud: true,
|
|
2452
|
+
model: options.model,
|
|
2453
|
+
provider: options.provider,
|
|
2454
|
+
effort: options.effort
|
|
2455
|
+
}
|
|
2456
|
+
})
|
|
2457
|
+
});
|
|
2458
|
+
if (!dispatchRes.ok) {
|
|
2459
|
+
const error = await dispatchRes.text();
|
|
2460
|
+
spinner.fail(`Dispatch failed: ${dispatchRes.status}`);
|
|
2461
|
+
writeLine(` ${colors.dim}${error}${RESET}`);
|
|
2462
|
+
process.exit(1);
|
|
2463
|
+
}
|
|
2464
|
+
const dispatch = await dispatchRes.json();
|
|
2465
|
+
spinner.succeed(`Dispatched to cloud`);
|
|
2466
|
+
writeLine();
|
|
2467
|
+
writeLine(` ${colors.cyan}Dispatch ID${RESET} ${dispatch.dispatch_id}`);
|
|
2468
|
+
writeLine(` ${colors.cyan}Squad${RESET} ${squadName}`);
|
|
2469
|
+
writeLine(` ${colors.cyan}Agent${RESET} ${agentName}`);
|
|
2470
|
+
writeLine();
|
|
2471
|
+
const pollSpinner = ora("Waiting for execution to start...").start();
|
|
2472
|
+
const pollStart = Date.now();
|
|
2473
|
+
let executionId = null;
|
|
2474
|
+
let lastStatus = "";
|
|
2475
|
+
while (Date.now() - pollStart < CLOUD_POLL_TIMEOUT_MS) {
|
|
2476
|
+
try {
|
|
2477
|
+
const execRes = await fetch(
|
|
2478
|
+
`${apiUrl}/agent-executions?squad=${encodeURIComponent(squadName)}&agent=${encodeURIComponent(agentName)}&limit=1`,
|
|
2479
|
+
{ headers }
|
|
2480
|
+
);
|
|
2481
|
+
if (execRes.ok) {
|
|
2482
|
+
const executions = await execRes.json();
|
|
2483
|
+
if (executions.length > 0) {
|
|
2484
|
+
const exec2 = executions[0];
|
|
2485
|
+
if (!executionId && exec2.status === "running") {
|
|
2486
|
+
executionId = exec2.execution_id;
|
|
2487
|
+
pollSpinner.text = `Running (${exec2.execution_id})`;
|
|
2488
|
+
}
|
|
2489
|
+
if (executionId && exec2.execution_id === executionId) {
|
|
2490
|
+
if (exec2.status !== lastStatus) {
|
|
2491
|
+
lastStatus = exec2.status;
|
|
2492
|
+
pollSpinner.text = `Status: ${exec2.status}`;
|
|
2493
|
+
}
|
|
2494
|
+
if (exec2.status === "completed") {
|
|
2495
|
+
pollSpinner.succeed("Execution completed");
|
|
2496
|
+
writeLine();
|
|
2497
|
+
writeLine(` ${colors.cyan}Execution${RESET} ${exec2.execution_id}`);
|
|
2498
|
+
if (exec2.summary) {
|
|
2499
|
+
writeLine(` ${colors.cyan}Summary${RESET} ${exec2.summary}`);
|
|
2500
|
+
}
|
|
2501
|
+
if (exec2.duration_seconds) {
|
|
2502
|
+
writeLine(` ${colors.cyan}Duration${RESET} ${Math.round(exec2.duration_seconds)}s`);
|
|
2503
|
+
}
|
|
2504
|
+
if (exec2.cost_usd) {
|
|
2505
|
+
writeLine(` ${colors.cyan}Cost${RESET} $${exec2.cost_usd.toFixed(4)}`);
|
|
2506
|
+
}
|
|
2507
|
+
writeLine();
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
if (exec2.status === "failed") {
|
|
2511
|
+
pollSpinner.fail("Execution failed");
|
|
2512
|
+
writeLine();
|
|
2513
|
+
if (exec2.error) {
|
|
2514
|
+
writeLine(` ${colors.red}Error: ${exec2.error}${RESET}`);
|
|
2515
|
+
}
|
|
2516
|
+
writeLine();
|
|
2517
|
+
process.exit(1);
|
|
2518
|
+
}
|
|
2519
|
+
if (exec2.status === "cancelled") {
|
|
2520
|
+
pollSpinner.warn("Execution cancelled");
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
} catch (e) {
|
|
2527
|
+
if (options.verbose) writeLine(` ${colors.dim}warn: cloud poll failed (retrying): ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
2528
|
+
}
|
|
2529
|
+
await new Promise((resolve) => setTimeout(resolve, CLOUD_POLL_INTERVAL_MS));
|
|
2530
|
+
}
|
|
2531
|
+
pollSpinner.warn("Poll timeout \u2014 execution may still be running");
|
|
2532
|
+
writeLine(` ${colors.dim}Check status: squads trigger status${RESET}`);
|
|
2533
|
+
if (executionId) {
|
|
2534
|
+
writeLine(` ${colors.dim}Execution ID: ${executionId}${RESET}`);
|
|
2535
|
+
}
|
|
2536
|
+
} catch (error) {
|
|
2537
|
+
spinner.fail("Cloud dispatch failed");
|
|
2538
|
+
writeLine(` ${colors.red}${error instanceof Error ? error.message : String(error)}${RESET}`);
|
|
2539
|
+
writeLine();
|
|
2540
|
+
writeLine(` ${colors.dim}Check your network and SQUADS_API_URL setting${RESET}`);
|
|
2541
|
+
process.exit(1);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
async function runCommand(target, options) {
|
|
2545
|
+
const squadsDir = findSquadsDir();
|
|
2546
|
+
if (!squadsDir) {
|
|
2547
|
+
writeLine(` ${colors.red}No .agents/squads directory found${RESET}`);
|
|
2548
|
+
writeLine(` ${colors.dim}Run \`squads init\` to create one.${RESET}`);
|
|
2549
|
+
process.exit(1);
|
|
2550
|
+
}
|
|
2551
|
+
if (!options.dryRun && options.execute === void 0) {
|
|
2552
|
+
options.execute = true;
|
|
2553
|
+
}
|
|
2554
|
+
if (!target) {
|
|
2555
|
+
await runAutopilot(squadsDir, options);
|
|
2556
|
+
return;
|
|
2557
|
+
}
|
|
2558
|
+
let squadName = target;
|
|
2559
|
+
let agentFromSlash;
|
|
2560
|
+
if (target.includes("/")) {
|
|
2561
|
+
const parts = target.split("/");
|
|
2562
|
+
squadName = parts[0];
|
|
2563
|
+
agentFromSlash = parts[1];
|
|
2564
|
+
if (!options.agent) {
|
|
2565
|
+
options.agent = agentFromSlash;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
if (options.cloud) {
|
|
2569
|
+
const agentName = options.agent || agentFromSlash;
|
|
2570
|
+
if (!agentName) {
|
|
2571
|
+
writeLine(` ${colors.red}${icons.error} --cloud requires a specific agent${RESET}`);
|
|
2572
|
+
writeLine(` ${colors.dim}Usage: squads run ${squadName} --cloud -a <agent>${RESET}`);
|
|
2573
|
+
writeLine(` ${colors.dim} or: squads run ${squadName}/<agent> --cloud${RESET}`);
|
|
2574
|
+
process.exit(1);
|
|
2575
|
+
}
|
|
2576
|
+
await track(Events.CLI_RUN, { type: "cloud", target: `${squadName}/${agentName}` });
|
|
2577
|
+
await flushEvents();
|
|
2578
|
+
await runCloudDispatch(squadName, agentName, options);
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const squad = loadSquad(squadName);
|
|
2582
|
+
if (options.execute && !options.dryRun) {
|
|
2583
|
+
const provider = options.provider || squad?.providers?.default || "anthropic";
|
|
2584
|
+
const checksOk = await preflightExecutorCheck(provider);
|
|
2585
|
+
if (!checksOk) {
|
|
2586
|
+
process.exit(1);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
if (squad) {
|
|
2590
|
+
await track(Events.CLI_RUN, { type: "squad", target: squad.name });
|
|
2591
|
+
await flushEvents();
|
|
2592
|
+
await runSquad(squad, squadsDir, options);
|
|
2593
|
+
await runPostEvaluation([squad.name], options);
|
|
2594
|
+
} else {
|
|
2595
|
+
const agents = listAgents(squadsDir);
|
|
2596
|
+
const agent = agents.find((a) => a.name === target);
|
|
2597
|
+
if (agent && agent.filePath) {
|
|
2598
|
+
const pathParts = agent.filePath.split("/");
|
|
2599
|
+
const squadIdx = pathParts.indexOf("squads");
|
|
2600
|
+
const resolvedSquadName = squadIdx >= 0 ? pathParts[squadIdx + 1] : "unknown";
|
|
2601
|
+
await runAgent(agent.name, agent.filePath, resolvedSquadName, options);
|
|
2602
|
+
await runPostEvaluation([resolvedSquadName], options);
|
|
2603
|
+
} else {
|
|
2604
|
+
writeLine(` ${colors.red}Squad or agent "${target}" not found${RESET}`);
|
|
2605
|
+
const similar = findSimilarSquads(target, listSquads(squadsDir));
|
|
2606
|
+
if (similar.length > 0) {
|
|
2607
|
+
writeLine(` ${colors.dim}Did you mean: ${similar.join(", ")}?${RESET}`);
|
|
2608
|
+
}
|
|
2609
|
+
writeLine(` ${colors.dim}Run \`squads status\` to see available squads and agents.${RESET}`);
|
|
2610
|
+
process.exit(1);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
async function runSquad(squad, squadsDir, options) {
|
|
2615
|
+
if (!squad) return;
|
|
2616
|
+
if (!options.effort && squad.effort) {
|
|
2617
|
+
options.effort = squad.effort;
|
|
2618
|
+
}
|
|
2619
|
+
const startTime = (/* @__PURE__ */ new Date()).toISOString();
|
|
2620
|
+
writeLine();
|
|
2621
|
+
writeLine(` ${gradient("squads")} ${colors.dim}run${RESET} ${colors.cyan}${squad.name}${RESET}`);
|
|
2622
|
+
writeLine();
|
|
2623
|
+
if (squad.mission) {
|
|
2624
|
+
writeLine(` ${colors.dim}${squad.mission}${RESET}`);
|
|
2625
|
+
writeLine();
|
|
2626
|
+
}
|
|
2627
|
+
writeLine(` ${colors.dim}Started: ${startTime}${RESET}`);
|
|
2628
|
+
writeLine();
|
|
2629
|
+
if (options.lead) {
|
|
2630
|
+
await runLeadMode(squad, squadsDir, options);
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
if (options.parallel) {
|
|
2634
|
+
const agentFiles = squad.agents.map((a) => ({
|
|
2635
|
+
name: a.name,
|
|
2636
|
+
path: join5(squadsDir, squad.dir, `${a.name}.md`)
|
|
2637
|
+
})).filter((a) => existsSync5(a.path));
|
|
2638
|
+
if (agentFiles.length === 0) {
|
|
2639
|
+
writeLine(` ${icons.error} ${colors.red}No agent files found${RESET}`);
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
writeLine(` ${bold}Parallel execution${RESET} ${colors.dim}${agentFiles.length} agents${RESET}`);
|
|
2643
|
+
writeLine();
|
|
2644
|
+
if (!options.execute) {
|
|
2645
|
+
for (const agent of agentFiles) {
|
|
2646
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET}`);
|
|
2647
|
+
}
|
|
2648
|
+
writeLine();
|
|
2649
|
+
writeLine(` ${colors.dim}Launch all agents in parallel:${RESET}`);
|
|
2650
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --parallel`);
|
|
2651
|
+
writeLine();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
writeLine(` ${gradient("Launching")} ${agentFiles.length} agents in parallel...`);
|
|
2655
|
+
writeLine();
|
|
2656
|
+
const launches = agentFiles.map(
|
|
2657
|
+
(agent) => runAgent(agent.name, agent.path, squad.dir, options)
|
|
2658
|
+
);
|
|
2659
|
+
await Promise.all(launches);
|
|
2660
|
+
writeLine();
|
|
2661
|
+
writeLine(` ${icons.success} All ${agentFiles.length} agents launched`);
|
|
2662
|
+
writeLine(` ${colors.dim}Monitor: tmux ls | grep squads-${squad.name}${RESET}`);
|
|
2663
|
+
writeLine(` ${colors.dim}Attach: tmux attach -t <session>${RESET}`);
|
|
2664
|
+
writeLine();
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
if (squad.pipelines.length > 0) {
|
|
2668
|
+
const pipeline = squad.pipelines[0];
|
|
2669
|
+
writeLine(` ${bold}Pipeline${RESET} ${colors.dim}${pipeline.agents.join(" \u2192 ")}${RESET}`);
|
|
2670
|
+
writeLine();
|
|
2671
|
+
for (let i = 0; i < pipeline.agents.length; i++) {
|
|
2672
|
+
const agentName = pipeline.agents[i];
|
|
2673
|
+
const agentPath = join5(squadsDir, squad.dir, `${agentName}.md`);
|
|
2674
|
+
if (existsSync5(agentPath)) {
|
|
2675
|
+
writeLine(` ${colors.dim}[${i + 1}/${pipeline.agents.length}]${RESET}`);
|
|
2676
|
+
await runAgent(agentName, agentPath, squad.dir, options);
|
|
2677
|
+
writeLine();
|
|
2678
|
+
} else {
|
|
2679
|
+
writeLine(` ${icons.warning} ${colors.yellow}Agent ${agentName} not found, skipping${RESET}`);
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
} else {
|
|
2683
|
+
if (options.agent) {
|
|
2684
|
+
const agentPath = join5(squadsDir, squad.dir, `${options.agent}.md`);
|
|
2685
|
+
if (existsSync5(agentPath)) {
|
|
2686
|
+
await runAgent(options.agent, agentPath, squad.dir, options);
|
|
2687
|
+
} else {
|
|
2688
|
+
writeLine(` ${icons.error} ${colors.red}Agent ${options.agent} not found${RESET}`);
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
} else {
|
|
2692
|
+
if (options.execute) {
|
|
2693
|
+
writeLine(` ${bold}Conversation mode${RESET} ${colors.dim}(lead \u2192 scan \u2192 work \u2192 review \u2192 verify)${RESET}`);
|
|
2694
|
+
writeLine();
|
|
2695
|
+
const convOptions = {
|
|
2696
|
+
task: options.task,
|
|
2697
|
+
maxTurns: options.maxTurns,
|
|
2698
|
+
costCeiling: options.costCeiling,
|
|
2699
|
+
verbose: options.verbose,
|
|
2700
|
+
model: options.model
|
|
2701
|
+
};
|
|
2702
|
+
const apiExecId = await reportExecutionStart(squad.name, "conversation", `conv-${Date.now()}`);
|
|
2703
|
+
const result = await runConversation(squad, convOptions);
|
|
2704
|
+
const transcriptPath = saveTranscript(result.transcript);
|
|
2705
|
+
if (apiExecId) {
|
|
2706
|
+
reportConversationResult(apiExecId, {
|
|
2707
|
+
turnCount: result.turnCount,
|
|
2708
|
+
totalCost: result.totalCost,
|
|
2709
|
+
converged: result.converged,
|
|
2710
|
+
reason: result.reason,
|
|
2711
|
+
agentsInvolved: [...new Set(result.transcript.turns.map((t) => t.agent))]
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
pushCognitionSignal({
|
|
2715
|
+
source: "execution",
|
|
2716
|
+
signal_type: result.converged ? "conversation_converged" : "conversation_stopped",
|
|
2717
|
+
value: result.totalCost,
|
|
2718
|
+
unit: "usd",
|
|
2719
|
+
data: {
|
|
2720
|
+
turn_count: result.turnCount,
|
|
2721
|
+
converged: result.converged,
|
|
2722
|
+
reason: result.reason,
|
|
2723
|
+
agents_involved: [...new Set(result.transcript.turns.map((t) => t.agent))]
|
|
2724
|
+
},
|
|
2725
|
+
entity_type: "squad",
|
|
2726
|
+
entity_id: squad.name,
|
|
2727
|
+
confidence: 0.9
|
|
2728
|
+
});
|
|
2729
|
+
writeLine();
|
|
2730
|
+
writeLine(` ${result.converged ? icons.success : icons.warning} ${result.converged ? "Converged" : "Stopped"}: ${result.reason}`);
|
|
2731
|
+
writeLine(` ${colors.dim}Turns: ${result.turnCount} | Cost: ~$${result.totalCost.toFixed(2)}${RESET}`);
|
|
2732
|
+
if (transcriptPath) {
|
|
2733
|
+
writeLine(` ${colors.dim}Transcript: ${transcriptPath}${RESET}`);
|
|
2734
|
+
}
|
|
2735
|
+
writeLine();
|
|
2736
|
+
} else {
|
|
2737
|
+
writeLine(` ${colors.dim}Default mode: conversation (lead \u2192 scan \u2192 work \u2192 review \u2192 verify)${RESET}`);
|
|
2738
|
+
writeLine();
|
|
2739
|
+
for (const agent of squad.agents) {
|
|
2740
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET} ${colors.dim}${agent.role}${RESET}`);
|
|
2741
|
+
}
|
|
2742
|
+
writeLine();
|
|
2743
|
+
writeLine(` ${colors.dim}Run conversation:${RESET}`);
|
|
2744
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET}`);
|
|
2745
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --task "review and merge open PRs"`);
|
|
2746
|
+
writeLine();
|
|
2747
|
+
writeLine(` ${colors.dim}Run single agent:${RESET}`);
|
|
2748
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} -a ${colors.cyan}<agent>${RESET}`);
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
writeLine();
|
|
2753
|
+
writeLine(` ${colors.dim}After execution, record outcome:${RESET}`);
|
|
2754
|
+
writeLine(` ${colors.dim}$${RESET} squads feedback add ${colors.cyan}${squad.name}${RESET} ${colors.cyan}<1-5>${RESET} ${colors.cyan}"<feedback>"${RESET}`);
|
|
2755
|
+
writeLine();
|
|
2756
|
+
}
|
|
2757
|
+
var EVAL_TIMEOUT_MINUTES = 15;
|
|
2758
|
+
async function runPostEvaluation(squadsRun, options) {
|
|
2759
|
+
if (squadsRun.length === 1 && squadsRun[0] === "company") return;
|
|
2760
|
+
if (options.eval === false) return;
|
|
2761
|
+
if (options.dryRun) return;
|
|
2762
|
+
if (options.background) return;
|
|
2763
|
+
const squadsDir = findSquadsDir();
|
|
2764
|
+
if (!squadsDir) return;
|
|
2765
|
+
const cooPath = join5(squadsDir, "company", "company-lead.md");
|
|
2766
|
+
if (!existsSync5(cooPath)) {
|
|
2767
|
+
if (options.verbose) {
|
|
2768
|
+
writeLine(` ${colors.dim}Skipping evaluation: company-lead.md not found${RESET}`);
|
|
2769
|
+
}
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
const squadList = squadsRun.join(", ");
|
|
2773
|
+
writeLine();
|
|
2774
|
+
writeLine(` ${gradient("eval")} ${colors.dim}COO evaluating: ${squadList}${RESET}`);
|
|
2775
|
+
const evalTask = `Post-run evaluation for: ${squadList}.
|
|
2776
|
+
|
|
2777
|
+
## Evaluation Process
|
|
2778
|
+
|
|
2779
|
+
For each squad (${squadList}):
|
|
2780
|
+
|
|
2781
|
+
### 1. Read previous feedback FIRST
|
|
2782
|
+
Read \`.agents/memory/{squad}/feedback.md\` if it exists. Note the previous grade, identified patterns, and priorities. This is your baseline \u2014 you are measuring CHANGE, not just current state.
|
|
2783
|
+
|
|
2784
|
+
### 2. Gather current evidence
|
|
2785
|
+
- PRs (last 7 days): \`gh pr list --state all --limit 20 --json number,title,state,mergedAt,createdAt\`
|
|
2786
|
+
- Recent commits (last 7 days): \`gh api repos/{owner}/{repo}/commits?since=YYYY-MM-DDT00:00:00Z&per_page=20 --jq '.[].commit.message'\`
|
|
2787
|
+
- Open issues: \`gh issue list --state open --limit 15 --json number,title,labels\`
|
|
2788
|
+
- Read \`.agents/memory/{squad}/priorities.md\` and \`.agents/memory/company/directives.md\`
|
|
2789
|
+
- Read \`.agents/memory/{squad}/active-work.md\` (previous cycle's work tracking)
|
|
2790
|
+
|
|
2791
|
+
### 3. Write feedback.md (APPEND history, don't overwrite)
|
|
2792
|
+
\`\`\`markdown
|
|
2793
|
+
# Feedback \u2014 {squad}
|
|
2794
|
+
|
|
2795
|
+
## Current Assessment (YYYY-MM-DD): [A-F]
|
|
2796
|
+
Merge rate: X% | Noise ratio: Y% | Priority alignment: Z%
|
|
2797
|
+
|
|
2798
|
+
## Trajectory: [improving | stable | declining | new]
|
|
2799
|
+
Previous grade: [grade] \u2192 Current: [grade]. [1-line explanation of why]
|
|
2800
|
+
|
|
2801
|
+
## Valuable (continue)
|
|
2802
|
+
- [specific PR/issue that advanced priorities]
|
|
2803
|
+
|
|
2804
|
+
## Noise (stop)
|
|
2805
|
+
- [specific anti-pattern observed]
|
|
2806
|
+
|
|
2807
|
+
## Next Cycle Priorities
|
|
2808
|
+
1. [specific actionable item]
|
|
2809
|
+
|
|
2810
|
+
## History
|
|
2811
|
+
| Date | Grade | Key Signal |
|
|
2812
|
+
|------|-------|------------|
|
|
2813
|
+
| YYYY-MM-DD | X | [what drove this grade] |
|
|
2814
|
+
[keep last 10 entries, append new row]
|
|
2815
|
+
\`\`\`
|
|
2816
|
+
|
|
2817
|
+
### 4. Write active-work.md
|
|
2818
|
+
\`\`\`markdown
|
|
2819
|
+
# Active Work \u2014 {squad} (YYYY-MM-DD)
|
|
2820
|
+
## Continue (open PRs)
|
|
2821
|
+
- #{number}: {title} \u2014 {status/next action}
|
|
2822
|
+
## Backlog (assigned issues)
|
|
2823
|
+
- #{number}: {title} \u2014 {priority}
|
|
2824
|
+
## Do NOT Create
|
|
2825
|
+
- {description of known duplicate patterns from feedback history}
|
|
2826
|
+
\`\`\`
|
|
2827
|
+
|
|
2828
|
+
### 5. Commit to hq main
|
|
2829
|
+
${squadsRun.length > 1 ? `
|
|
2830
|
+
### 6. Cross-squad assessment
|
|
2831
|
+
Evaluate how outputs from ${squadList} connect:
|
|
2832
|
+
- Duplicated efforts across squads?
|
|
2833
|
+
- Missing handoffs (one squad's output should feed another)?
|
|
2834
|
+
- Coordination gaps (conflicting PRs, redundant issues)?
|
|
2835
|
+
- Combined trajectory: is the org getting more effective or more noisy?
|
|
2836
|
+
Write cross-squad findings to \`.agents/memory/company/cross-squad-review.md\`.
|
|
2837
|
+
` : ""}
|
|
2838
|
+
CRITICAL: You are measuring DIRECTION not just position. A C-grade squad improving from F is better than a B-grade squad declining from A. The history table IS the feedback loop \u2014 agents read it next cycle.`;
|
|
2839
|
+
await runAgent("company-lead", cooPath, "company", {
|
|
2840
|
+
...options,
|
|
2841
|
+
task: evalTask,
|
|
2842
|
+
timeout: EVAL_TIMEOUT_MINUTES,
|
|
2843
|
+
eval: false,
|
|
2844
|
+
// prevent recursion
|
|
2845
|
+
trigger: "manual"
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
var ROLE_COOLDOWNS = {
|
|
2849
|
+
scanner: 60 * 60 * 1e3,
|
|
2850
|
+
// 1h — fast, cheap
|
|
2851
|
+
lead: 4 * 60 * 60 * 1e3,
|
|
2852
|
+
// 4h — orchestration
|
|
2853
|
+
worker: 30 * 60 * 1e3,
|
|
2854
|
+
// 30m — if work exists
|
|
2855
|
+
verifier: 30 * 60 * 1e3,
|
|
2856
|
+
// 30m — follows workers
|
|
2857
|
+
"issue-solver": 30 * 60 * 1e3
|
|
2858
|
+
// 30m — default worker
|
|
2859
|
+
};
|
|
2860
|
+
function classifyAgentRole(name) {
|
|
2861
|
+
return classifyAgent(name) ?? "worker";
|
|
2862
|
+
}
|
|
2863
|
+
async function runAutopilot(squadsDir, options) {
|
|
2864
|
+
const interval = parseInt(String(options.interval || "30"), 10);
|
|
2865
|
+
const maxParallel = parseInt(String(options.maxParallel || "2"), 10);
|
|
2866
|
+
const budget = parseFloat(String(options.budget || "0"));
|
|
2867
|
+
const once = !!options.once;
|
|
2868
|
+
const cognitionState = loadCognitionState();
|
|
2869
|
+
seedBeliefsIfEmpty(cognitionState);
|
|
2870
|
+
saveCognitionState(cognitionState);
|
|
2871
|
+
writeLine();
|
|
2872
|
+
writeLine(` ${gradient("squads")} ${colors.dim}autopilot${RESET}`);
|
|
2873
|
+
writeLine(` ${colors.dim}Interval: ${interval}m | Parallel: ${maxParallel} | Budget: ${budget > 0 ? "$" + budget + "/day" : "unlimited"}${RESET}`);
|
|
2874
|
+
writeLine(` ${colors.dim}Cognition: ${cognitionState.beliefs.length} beliefs, ${cognitionState.signals.length} signals${RESET}`);
|
|
2875
|
+
writeLine();
|
|
2876
|
+
let running = true;
|
|
2877
|
+
const handleSignal = () => {
|
|
2878
|
+
running = false;
|
|
2879
|
+
};
|
|
2880
|
+
process.on("SIGINT", handleSignal);
|
|
2881
|
+
process.on("SIGTERM", handleSignal);
|
|
2882
|
+
while (running) {
|
|
2883
|
+
const cycleStart = Date.now();
|
|
2884
|
+
const state = loadLoopState();
|
|
2885
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2886
|
+
if (state.dailyCostDate !== today) {
|
|
2887
|
+
state.dailyCost = 0;
|
|
2888
|
+
state.dailyCostDate = today;
|
|
2889
|
+
}
|
|
2890
|
+
if (budget > 0 && state.dailyCost >= budget) {
|
|
2891
|
+
writeLine(` ${icons.warning} ${colors.yellow}Daily budget reached ($${state.dailyCost.toFixed(2)}/$${budget})${RESET}`);
|
|
2892
|
+
saveLoopState(state);
|
|
2893
|
+
if (once) break;
|
|
2894
|
+
await sleep(interval * 60 * 1e3);
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
writeLine(` ${colors.dim}\u2500\u2500 Cycle ${(/* @__PURE__ */ new Date()).toLocaleTimeString()} \u2500\u2500${RESET}`);
|
|
2898
|
+
let ghEnv = {};
|
|
2899
|
+
try {
|
|
2900
|
+
ghEnv = await getBotGhEnv();
|
|
2901
|
+
} catch {
|
|
2902
|
+
}
|
|
2903
|
+
const squadRepos = getSquadRepos();
|
|
2904
|
+
let dispatchedSquadNames;
|
|
2905
|
+
const failed = [];
|
|
2906
|
+
const completed = [];
|
|
2907
|
+
if (options.phased) {
|
|
2908
|
+
const phases = computePhases();
|
|
2909
|
+
const phaseCount = phases.size;
|
|
2910
|
+
writeLine(` ${colors.dim}Phased mode: ${phaseCount} phase(s)${RESET}`);
|
|
2911
|
+
dispatchedSquadNames = [];
|
|
2912
|
+
for (const [phaseNum, phaseSquads] of phases) {
|
|
2913
|
+
writeLine(` ${colors.dim}\u2500\u2500 Phase ${phaseNum} (${phaseSquads.join(", ")}) \u2500\u2500${RESET}`);
|
|
2914
|
+
const phaseSignals = scoreSquadsForPhase(phaseSquads, state, squadRepos, ghEnv);
|
|
2915
|
+
const phaseDispatch = phaseSignals.filter((s) => s.score > 0).slice(0, maxParallel);
|
|
2916
|
+
if (phaseDispatch.length === 0) {
|
|
2917
|
+
writeLine(` ${colors.dim}No squads need attention in this phase${RESET}`);
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2920
|
+
for (const sig of phaseDispatch) {
|
|
2921
|
+
writeLine(` ${colors.cyan}${sig.squad}${RESET} (score: ${sig.score}) \u2014 ${sig.reason}`);
|
|
2922
|
+
}
|
|
2923
|
+
if (options.dryRun) {
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
const phaseResults = await Promise.allSettled(
|
|
2927
|
+
phaseDispatch.map((sig) => {
|
|
2928
|
+
const squad = loadSquad(sig.squad);
|
|
2929
|
+
if (!squad) return Promise.resolve();
|
|
2930
|
+
return runSquadLoop(squad, squadsDir, state, ghEnv, options);
|
|
2931
|
+
})
|
|
2932
|
+
);
|
|
2933
|
+
for (let i = 0; i < phaseResults.length; i++) {
|
|
2934
|
+
const name = phaseDispatch[i].squad;
|
|
2935
|
+
dispatchedSquadNames.push(name);
|
|
2936
|
+
if (phaseResults[i].status === "rejected") {
|
|
2937
|
+
failed.push(name);
|
|
2938
|
+
state.failCounts[name] = (state.failCounts[name] || 0) + 1;
|
|
2939
|
+
} else {
|
|
2940
|
+
completed.push(name);
|
|
2941
|
+
delete state.failCounts[name];
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
if (options.dryRun) {
|
|
2946
|
+
writeLine(` ${colors.yellow}[DRY RUN] Would dispatch above squads in phase order${RESET}`);
|
|
2947
|
+
saveLoopState(state);
|
|
2948
|
+
if (once) break;
|
|
2949
|
+
await sleep(interval * 60 * 1e3);
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
} else {
|
|
2953
|
+
const signals = scoreSquads(state, squadRepos, ghEnv);
|
|
2954
|
+
if (signals.length === 0 || signals.every((s) => s.score <= 0)) {
|
|
2955
|
+
writeLine(` ${colors.dim}No squads need attention${RESET}`);
|
|
2956
|
+
saveLoopState(state);
|
|
2957
|
+
if (once) break;
|
|
2958
|
+
await sleep(interval * 60 * 1e3);
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
const toDispatch = signals.filter((s) => s.score > 0).slice(0, maxParallel);
|
|
2962
|
+
writeLine(` ${colors.dim}Dispatching ${toDispatch.length} squad(s):${RESET}`);
|
|
2963
|
+
for (const sig of toDispatch) {
|
|
2964
|
+
writeLine(` ${colors.cyan}${sig.squad}${RESET} (score: ${sig.score}) \u2014 ${sig.reason}`);
|
|
2965
|
+
}
|
|
2966
|
+
if (options.dryRun) {
|
|
2967
|
+
writeLine(` ${colors.yellow}[DRY RUN] Would dispatch above squads${RESET}`);
|
|
2968
|
+
saveLoopState(state);
|
|
2969
|
+
if (once) break;
|
|
2970
|
+
await sleep(interval * 60 * 1e3);
|
|
2971
|
+
continue;
|
|
2972
|
+
}
|
|
2973
|
+
const results = await Promise.allSettled(
|
|
2974
|
+
toDispatch.map((sig) => {
|
|
2975
|
+
const squad = loadSquad(sig.squad);
|
|
2976
|
+
if (!squad) return Promise.resolve();
|
|
2977
|
+
return runSquadLoop(squad, squadsDir, state, ghEnv, options);
|
|
2978
|
+
})
|
|
2979
|
+
);
|
|
2980
|
+
for (let i = 0; i < results.length; i++) {
|
|
2981
|
+
const r = results[i];
|
|
2982
|
+
const name = toDispatch[i].squad;
|
|
2983
|
+
if (r.status === "rejected") {
|
|
2984
|
+
failed.push(name);
|
|
2985
|
+
state.failCounts[name] = (state.failCounts[name] || 0) + 1;
|
|
2986
|
+
} else {
|
|
2987
|
+
completed.push(name);
|
|
2988
|
+
delete state.failCounts[name];
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
dispatchedSquadNames = toDispatch.map((s) => s.squad);
|
|
2992
|
+
}
|
|
2993
|
+
const cycleCost = dispatchedSquadNames.length * 1;
|
|
2994
|
+
state.dailyCost += cycleCost;
|
|
2995
|
+
await pushMemorySignals(dispatchedSquadNames, state, !!options.verbose);
|
|
2996
|
+
state.recentRuns = state.recentRuns.slice(-100);
|
|
2997
|
+
state.lastCycle = (/* @__PURE__ */ new Date()).toISOString();
|
|
2998
|
+
saveLoopState(state);
|
|
2999
|
+
if (failed.length > 0) {
|
|
3000
|
+
slackNotify([
|
|
3001
|
+
`*Autopilot cycle \u2014 failures*`,
|
|
3002
|
+
`Failed: ${failed.join(", ")}`,
|
|
3003
|
+
`Completed: ${completed.join(", ")}`,
|
|
3004
|
+
`Daily: $${state.dailyCost.toFixed(2)}${budget > 0 ? "/$" + budget : ""}`
|
|
3005
|
+
].join("\n"));
|
|
3006
|
+
}
|
|
3007
|
+
for (const [key, count] of Object.entries(state.failCounts)) {
|
|
3008
|
+
if (count >= 3) {
|
|
3009
|
+
slackNotify(`\u{1F6A8} *Escalation*: ${key} has failed ${count} times consecutively.`);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
if (dispatchedSquadNames.length > 0) {
|
|
3013
|
+
await runPostEvaluation(dispatchedSquadNames, options);
|
|
3014
|
+
}
|
|
3015
|
+
writeLine(` ${colors.dim}Cognition cycle...${RESET}`);
|
|
3016
|
+
const cognitionResult = await runCognitionCycle(dispatchedSquadNames, !!options.verbose);
|
|
3017
|
+
if (cognitionResult.signalsIngested > 0 || cognitionResult.beliefsUpdated > 0 || cognitionResult.reflected) {
|
|
3018
|
+
writeLine(` ${colors.dim}\u{1F9E0} ${cognitionResult.signalsIngested} signals \u2192 ${cognitionResult.beliefsUpdated} beliefs updated${cognitionResult.reflected ? " \u2192 reflected" : ""}${RESET}`);
|
|
3019
|
+
}
|
|
3020
|
+
const elapsed = ((Date.now() - cycleStart) / 1e3).toFixed(0);
|
|
3021
|
+
writeLine(` ${colors.dim}Cycle done in ${elapsed}s | Daily: $${state.dailyCost.toFixed(2)}${RESET}`);
|
|
3022
|
+
writeLine();
|
|
3023
|
+
if (once) break;
|
|
3024
|
+
await sleep(interval * 60 * 1e3);
|
|
3025
|
+
}
|
|
3026
|
+
process.off("SIGINT", handleSignal);
|
|
3027
|
+
process.off("SIGTERM", handleSignal);
|
|
3028
|
+
}
|
|
3029
|
+
async function runSquadLoop(squad, squadsDir, state, ghEnv, options) {
|
|
3030
|
+
writeLine(` ${gradient("\u25B8")} ${colors.cyan}${squad.name}${RESET} \u2014 full loop`);
|
|
3031
|
+
const agentsByRole = {
|
|
3032
|
+
scanner: [],
|
|
3033
|
+
lead: [],
|
|
3034
|
+
worker: [],
|
|
3035
|
+
verifier: []
|
|
3036
|
+
};
|
|
3037
|
+
for (const agent of squad.agents) {
|
|
3038
|
+
const role = classifyAgentRole(agent.name);
|
|
3039
|
+
const agentPath = join5(squadsDir, squad.dir, `${agent.name}.md`);
|
|
3040
|
+
if (existsSync5(agentPath)) {
|
|
3041
|
+
agentsByRole[role].push({ name: agent.name, path: agentPath });
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
const loopSteps = [
|
|
3045
|
+
{ role: "scanner", agents: agentsByRole.scanner },
|
|
3046
|
+
{ role: "lead", agents: agentsByRole.lead },
|
|
3047
|
+
{ role: "worker", agents: agentsByRole.worker },
|
|
3048
|
+
{ role: "verifier", agents: agentsByRole.verifier }
|
|
3049
|
+
];
|
|
3050
|
+
for (const step of loopSteps) {
|
|
3051
|
+
if (step.agents.length === 0) continue;
|
|
3052
|
+
for (const agent of step.agents) {
|
|
3053
|
+
const cooldownMs = ROLE_COOLDOWNS[step.role] || ROLE_COOLDOWNS.worker;
|
|
3054
|
+
if (!checkCooldown(state, squad.name, agent.name, cooldownMs)) {
|
|
3055
|
+
if (options.verbose) {
|
|
3056
|
+
writeLine(` ${colors.dim}\u21B3 ${agent.name} (${step.role}) \u2014 in cooldown, skip${RESET}`);
|
|
3057
|
+
}
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
writeLine(` ${colors.dim}\u21B3 ${agent.name} (${step.role})${RESET}`);
|
|
3061
|
+
const startMs = Date.now();
|
|
3062
|
+
try {
|
|
3063
|
+
if (step.role === "worker" && step.agents.length > 1) {
|
|
3064
|
+
const convOptions = {
|
|
3065
|
+
task: options.task,
|
|
3066
|
+
maxTurns: options.maxTurns || 20,
|
|
3067
|
+
costCeiling: options.costCeiling || 25,
|
|
3068
|
+
verbose: options.verbose,
|
|
3069
|
+
model: options.model
|
|
3070
|
+
};
|
|
3071
|
+
await runConversation(squad, convOptions);
|
|
3072
|
+
} else {
|
|
3073
|
+
await runAgent(agent.name, agent.path, squad.dir, {
|
|
3074
|
+
...options,
|
|
3075
|
+
background: false,
|
|
3076
|
+
watch: false,
|
|
3077
|
+
execute: true
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
const durationMs = Date.now() - startMs;
|
|
3081
|
+
const outcome = classifyRunOutcome(0, durationMs);
|
|
3082
|
+
state.cooldowns[`${squad.name}:${agent.name}`] = Date.now();
|
|
3083
|
+
state.recentRuns.push({
|
|
3084
|
+
squad: squad.name,
|
|
3085
|
+
agent: agent.name,
|
|
3086
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3087
|
+
result: outcome === "skipped" ? "completed" : outcome,
|
|
3088
|
+
durationMs
|
|
3089
|
+
});
|
|
3090
|
+
pushCognitionSignal({
|
|
3091
|
+
source: "execution",
|
|
3092
|
+
signal_type: `${step.role}_${outcome}`,
|
|
3093
|
+
value: durationMs / 1e3,
|
|
3094
|
+
unit: "seconds",
|
|
3095
|
+
data: {
|
|
3096
|
+
squad: squad.name,
|
|
3097
|
+
agent: agent.name,
|
|
3098
|
+
role: step.role,
|
|
3099
|
+
duration_ms: durationMs
|
|
3100
|
+
},
|
|
3101
|
+
entity_type: "agent",
|
|
3102
|
+
entity_id: `${squad.name}/${agent.name}`,
|
|
3103
|
+
confidence: 0.9
|
|
3104
|
+
});
|
|
3105
|
+
if (outcome === "skipped") {
|
|
3106
|
+
writeLine(` ${colors.dim}\u21B3 ${agent.name} \u2014 phantom (${(durationMs / 1e3).toFixed(0)}s), skipped${RESET}`);
|
|
3107
|
+
}
|
|
3108
|
+
if (step.role === "worker" && step.agents.length > 1) break;
|
|
3109
|
+
} catch (err) {
|
|
3110
|
+
const durationMs = Date.now() - startMs;
|
|
3111
|
+
state.cooldowns[`${squad.name}:${agent.name}`] = Date.now();
|
|
3112
|
+
state.recentRuns.push({
|
|
3113
|
+
squad: squad.name,
|
|
3114
|
+
agent: agent.name,
|
|
3115
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3116
|
+
result: "failed",
|
|
3117
|
+
durationMs
|
|
3118
|
+
});
|
|
3119
|
+
writeLine(` ${colors.red}\u21B3 ${agent.name} failed: ${err instanceof Error ? err.message : "unknown"}${RESET}`);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
writeLine(` ${colors.dim}\u21B3 ${squad.name} loop complete${RESET}`);
|
|
3124
|
+
}
|
|
3125
|
+
function sleep(ms) {
|
|
3126
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3127
|
+
}
|
|
3128
|
+
async function runLeadMode(squad, squadsDir, options) {
|
|
3129
|
+
if (!squad) return;
|
|
3130
|
+
const agentFiles = squad.agents.map((a) => ({
|
|
3131
|
+
name: a.name,
|
|
3132
|
+
path: join5(squadsDir, squad.dir, `${a.name}.md`),
|
|
3133
|
+
role: a.role || ""
|
|
3134
|
+
})).filter((a) => existsSync5(a.path));
|
|
3135
|
+
if (agentFiles.length === 0) {
|
|
3136
|
+
writeLine(` ${icons.error} ${colors.red}No agent files found${RESET}`);
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
writeLine(` ${bold}Lead mode${RESET} ${colors.dim}orchestrating ${agentFiles.length} agents${RESET}`);
|
|
3140
|
+
writeLine();
|
|
3141
|
+
for (const agent of agentFiles) {
|
|
3142
|
+
writeLine(` ${icons.empty} ${colors.cyan}${agent.name}${RESET} ${colors.dim}${agent.role}${RESET}`);
|
|
3143
|
+
}
|
|
3144
|
+
writeLine();
|
|
3145
|
+
if (!options.execute) {
|
|
3146
|
+
writeLine(` ${colors.dim}Launch lead session:${RESET}`);
|
|
3147
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squad.name}${RESET} --lead`);
|
|
3148
|
+
writeLine();
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
const timeoutMins = options.timeout || DEFAULT_TIMEOUT_MINUTES;
|
|
3152
|
+
const agentList = agentFiles.map((a) => `- ${a.name}: ${a.role}`).join("\n");
|
|
3153
|
+
const agentPaths = agentFiles.map((a) => `- ${a.name}: ${a.path}`).join("\n");
|
|
3154
|
+
const prompt = `You are the Lead of the ${squad.name} squad.
|
|
3155
|
+
|
|
3156
|
+
## Mission
|
|
3157
|
+
${squad.mission || "Execute squad operations efficiently."}
|
|
3158
|
+
|
|
3159
|
+
## Available Agents
|
|
3160
|
+
${agentList}
|
|
3161
|
+
|
|
3162
|
+
## Agent Definition Files
|
|
3163
|
+
${agentPaths}
|
|
3164
|
+
|
|
3165
|
+
## Your Role as Lead
|
|
3166
|
+
|
|
3167
|
+
1. **Assess the situation**: Check for pending work:
|
|
3168
|
+
- Run \`gh issue list --repo agents-squads/hq --label squad:${squad.name}\` for assigned issues
|
|
3169
|
+
- Check .agents/memory/${squad.dir}/ for squad state and pending tasks
|
|
3170
|
+
- Review recent activity with \`git log --oneline -10\`
|
|
3171
|
+
|
|
3172
|
+
2. **Delegate work using Task tool**: For each piece of work:
|
|
3173
|
+
- Use the Task tool with subagent_type="general-purpose"
|
|
3174
|
+
- Include the agent definition file path in the prompt
|
|
3175
|
+
- Spawn multiple Task agents IN PARALLEL when work is independent
|
|
3176
|
+
- Example: "Read ${agentFiles[0]?.path || "agent.md"} and execute its instructions for [specific task]"
|
|
3177
|
+
|
|
3178
|
+
3. **Coordinate parallel execution**:
|
|
3179
|
+
- Independent tasks \u2192 spawn Task agents in parallel (single message, multiple tool calls)
|
|
3180
|
+
- Dependent tasks \u2192 run sequentially
|
|
3181
|
+
- Monitor progress and handle failures
|
|
3182
|
+
|
|
3183
|
+
4. **Report and update memory**:
|
|
3184
|
+
- Update .agents/memory/${squad.dir}/state.md with completed work
|
|
3185
|
+
- Log learnings to learnings.md
|
|
3186
|
+
- Create issues for follow-up work if needed
|
|
3187
|
+
|
|
3188
|
+
## Time Budget
|
|
3189
|
+
You have ${timeoutMins} minutes. Prioritize high-impact work.
|
|
3190
|
+
|
|
3191
|
+
## Critical Instructions
|
|
3192
|
+
- Use Task tool for delegation, NOT direct execution of agent work
|
|
3193
|
+
- Spawn parallel Task agents when work is independent
|
|
3194
|
+
- When done, type /exit to end the session
|
|
3195
|
+
- Do NOT wait for user input - work autonomously
|
|
3196
|
+
|
|
3197
|
+
## Async Mode (CRITICAL)
|
|
3198
|
+
This is ASYNC execution - Task agents must be fully autonomous:
|
|
3199
|
+
- **Findings** \u2192 Create GitHub issues (gh issue create)
|
|
3200
|
+
- **Code changes** \u2192 Create PRs (gh pr create)
|
|
3201
|
+
- **Analysis results** \u2192 Write to .agents/outputs/ or memory files
|
|
3202
|
+
- **NEVER wait for human review** - complete the work and move on
|
|
3203
|
+
- **NEVER ask clarifying questions** - make reasonable decisions
|
|
3204
|
+
|
|
3205
|
+
Instruct each Task agent: "Work autonomously. Output findings to GitHub issues. Output code changes as PRs. Do not wait for review."
|
|
3206
|
+
|
|
3207
|
+
Begin by assessing pending work, then delegate to agents via Task tool.`;
|
|
3208
|
+
const provider = options.provider || squad?.providers?.default || "anthropic";
|
|
3209
|
+
const isAnthropic = provider === "anthropic";
|
|
3210
|
+
if (isAnthropic) {
|
|
3211
|
+
const claudeAvailable = await checkClaudeCliAvailable();
|
|
3212
|
+
if (!claudeAvailable) {
|
|
3213
|
+
writeLine(` ${colors.yellow}Claude CLI not found${RESET}`);
|
|
3214
|
+
writeLine(` ${colors.dim}Install: npm install -g @anthropic-ai/claude-code${RESET}`);
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
} else {
|
|
3218
|
+
if (!isProviderCLIAvailable(provider)) {
|
|
3219
|
+
const cliConfig = getCLIConfig(provider);
|
|
3220
|
+
writeLine(` ${colors.yellow}${cliConfig?.displayName || provider} CLI not found${RESET}`);
|
|
3221
|
+
if (cliConfig?.install) {
|
|
3222
|
+
writeLine(` ${colors.dim}Install: ${cliConfig.install}${RESET}`);
|
|
3223
|
+
}
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const isBackground = options.background === true && !options.watch;
|
|
3228
|
+
const isWatch = options.watch === true;
|
|
3229
|
+
const isForeground = !isBackground && !isWatch;
|
|
3230
|
+
const modeText = isBackground ? " (background)" : isWatch ? " (watch)" : "";
|
|
3231
|
+
const providerDisplay = isAnthropic ? "Claude" : getCLIConfig(provider)?.displayName || provider;
|
|
3232
|
+
writeLine(` ${gradient("Launching")} lead session${modeText} with ${providerDisplay}...`);
|
|
3233
|
+
writeLine();
|
|
3234
|
+
try {
|
|
3235
|
+
const leadAgentName = agentFiles.find((a) => a.name.includes("lead"))?.name || `${squad.dir}-lead`;
|
|
3236
|
+
let result;
|
|
3237
|
+
if (isAnthropic) {
|
|
3238
|
+
result = await executeWithClaude(prompt, {
|
|
3239
|
+
verbose: options.verbose,
|
|
3240
|
+
timeoutMinutes: timeoutMins,
|
|
3241
|
+
foreground: options.foreground,
|
|
3242
|
+
background: options.background,
|
|
3243
|
+
watch: options.watch,
|
|
3244
|
+
useApi: options.useApi,
|
|
3245
|
+
effort: options.effort,
|
|
3246
|
+
skills: options.skills,
|
|
3247
|
+
trigger: options.trigger || "manual",
|
|
3248
|
+
squadName: squad.dir,
|
|
3249
|
+
agentName: leadAgentName,
|
|
3250
|
+
model: options.model
|
|
3251
|
+
});
|
|
3252
|
+
} else {
|
|
3253
|
+
result = await executeWithProvider(provider, prompt, {
|
|
3254
|
+
verbose: options.verbose,
|
|
3255
|
+
foreground: isForeground || isWatch,
|
|
3256
|
+
squadName: squad.dir,
|
|
3257
|
+
agentName: leadAgentName
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
if (isForeground || isWatch) {
|
|
3261
|
+
writeLine();
|
|
3262
|
+
writeLine(` ${icons.success} Lead session completed`);
|
|
3263
|
+
} else {
|
|
3264
|
+
writeLine(` ${icons.success} Lead session launched in background`);
|
|
3265
|
+
writeLine(` ${colors.dim}${result}${RESET}`);
|
|
3266
|
+
writeLine();
|
|
3267
|
+
writeLine(` ${colors.dim}The lead will:${RESET}`);
|
|
3268
|
+
writeLine(` ${colors.dim} 1. Assess pending work (issues, memory)${RESET}`);
|
|
3269
|
+
writeLine(` ${colors.dim} 2. Spawn Task agents for parallel execution${RESET}`);
|
|
3270
|
+
writeLine(` ${colors.dim} 3. Coordinate and report results${RESET}`);
|
|
3271
|
+
writeLine();
|
|
3272
|
+
writeLine(` ${colors.dim}Monitor: squads workers${RESET}`);
|
|
3273
|
+
}
|
|
3274
|
+
} catch (error) {
|
|
3275
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3276
|
+
writeLine(` ${icons.error} ${colors.red}Failed to launch agent${RESET}`);
|
|
3277
|
+
writeLine(` ${colors.dim}${msg}${RESET}`);
|
|
3278
|
+
writeLine(` ${colors.dim}Run \`squads doctor\` to check your setup.${RESET}`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
async function runAgent(agentName, agentPath, squadName, options) {
|
|
3282
|
+
const spinner = ora(`Running agent: ${agentName}`).start();
|
|
3283
|
+
const startMs = Date.now();
|
|
3284
|
+
const startTime = new Date(startMs).toISOString();
|
|
3285
|
+
const executionId = generateExecutionId();
|
|
3286
|
+
const taskType = detectTaskType(agentName);
|
|
3287
|
+
const definition = loadAgentDefinition(agentPath);
|
|
3288
|
+
const learnings = await fetchLearnings(squadName);
|
|
3289
|
+
const learningContext = learnings.length > 0 ? `
|
|
3290
|
+
## Learnings from Previous Runs
|
|
3291
|
+
${learnings.map((l) => `- ${l.content}`).join("\n")}
|
|
3292
|
+
` : "";
|
|
3293
|
+
if (options.dryRun) {
|
|
3294
|
+
spinner.info(`[DRY RUN] Would run ${agentName}`);
|
|
3295
|
+
const dryRunAgentRole = classifyAgent(agentName);
|
|
3296
|
+
const dryRunContextRole = agentName.includes("company-lead") ? "coo" : dryRunAgentRole ?? "worker";
|
|
3297
|
+
const dryRunContext = gatherSquadContext(squadName, agentName, {
|
|
3298
|
+
verbose: options.verbose,
|
|
3299
|
+
agentPath,
|
|
3300
|
+
role: dryRunContextRole
|
|
3301
|
+
});
|
|
3302
|
+
if (options.verbose) {
|
|
3303
|
+
writeLine(` ${colors.dim}Agent definition:${RESET}`);
|
|
3304
|
+
writeLine(` ${colors.dim}${definition.slice(0, DRYRUN_DEF_MAX_CHARS)}...${RESET}`);
|
|
3305
|
+
if (learnings.length > 0) {
|
|
3306
|
+
writeLine(` ${colors.dim}Learnings: ${learnings.length} from bridge${RESET}`);
|
|
3307
|
+
}
|
|
3308
|
+
if (dryRunContext || learningContext) {
|
|
3309
|
+
const fullContext = `${dryRunContext}${learningContext}`;
|
|
3310
|
+
writeLine();
|
|
3311
|
+
writeLine(` ${colors.cyan}Context to inject (${Math.ceil(fullContext.length / 4)} tokens):${RESET}`);
|
|
3312
|
+
writeLine(` ${colors.dim}${fullContext.slice(0, DRYRUN_CONTEXT_MAX_CHARS)}...${RESET}`);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
const squadsDir = findSquadsDir();
|
|
3318
|
+
if (squadsDir) {
|
|
3319
|
+
const squadFilePath = join5(squadsDir, squadName, "SQUAD.md");
|
|
3320
|
+
if (existsSync5(squadFilePath)) {
|
|
3321
|
+
const squadContent = readFileSync4(squadFilePath, "utf-8");
|
|
3322
|
+
const permContext = buildContextFromSquad(squadName, squadContent, agentName);
|
|
3323
|
+
const mcpServers = extractMcpServersFromDefinition(definition);
|
|
3324
|
+
const execRequest = {
|
|
3325
|
+
mcpServers
|
|
3326
|
+
};
|
|
3327
|
+
const permResult = validateExecution(permContext, execRequest);
|
|
3328
|
+
if (permResult.violations.length > 0) {
|
|
3329
|
+
spinner.stop();
|
|
3330
|
+
const violationLines = formatViolations(permResult);
|
|
3331
|
+
for (const line of violationLines) {
|
|
3332
|
+
writeLine(` ${line}`);
|
|
3333
|
+
}
|
|
3334
|
+
writeLine();
|
|
3335
|
+
if (!permResult.allowed) {
|
|
3336
|
+
writeLine(` ${colors.red}Execution blocked due to permission violations.${RESET}`);
|
|
3337
|
+
writeLine(` ${colors.dim}Configure permissions in ${squadFilePath}${RESET}`);
|
|
3338
|
+
return;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
const preflight = await checkPreflightGates(squadName, agentName);
|
|
3344
|
+
if (!preflight.allowed) {
|
|
3345
|
+
spinner.stop();
|
|
3346
|
+
writeLine();
|
|
3347
|
+
writeLine(` ${colors.red}${icons.error} Execution blocked by preflight gates${RESET}`);
|
|
3348
|
+
if (preflight.gates.quota && !preflight.gates.quota.ok) {
|
|
3349
|
+
writeLine(` ${colors.dim}Quota: $${preflight.gates.quota.used.toFixed(2)}/$${preflight.gates.quota.limit}/mo limit exceeded${RESET}`);
|
|
3350
|
+
}
|
|
3351
|
+
if (preflight.gates.cooldown && !preflight.gates.cooldown.ok) {
|
|
3352
|
+
const elapsed = preflight.gates.cooldown.elapsed_sec;
|
|
3353
|
+
const minGap = preflight.gates.cooldown.min_gap_sec;
|
|
3354
|
+
writeLine(` ${colors.dim}Cooldown: ${elapsed}s since last run (min: ${minGap}s)${RESET}`);
|
|
3355
|
+
}
|
|
3356
|
+
writeLine();
|
|
3357
|
+
return;
|
|
3358
|
+
}
|
|
3359
|
+
if (options.verbose && Object.keys(preflight.gates).length > 0) {
|
|
3360
|
+
writeLine(` ${colors.dim}Preflight: quota ${preflight.gates.quota?.ok ? "\u2713" : "\u2717"} cooldown ${preflight.gates.cooldown?.ok ? "\u2713" : "\u2717"}${RESET}`);
|
|
3361
|
+
}
|
|
3362
|
+
const isScheduledRun = options.trigger === "scheduled" || options.trigger === "smart";
|
|
3363
|
+
const bridgeHasNoHistory = preflight.gates.cooldown?.elapsed_sec === null;
|
|
3364
|
+
if (isScheduledRun && (!preflight.gates.cooldown || bridgeHasNoHistory)) {
|
|
3365
|
+
const frontmatterForCooldown = parseAgentFrontmatter(agentPath);
|
|
3366
|
+
const cooldownMs = frontmatterForCooldown.cooldown ? parseCooldown(frontmatterForCooldown.cooldown) || DEFAULT_SCHEDULED_COOLDOWN_MS : DEFAULT_SCHEDULED_COOLDOWN_MS;
|
|
3367
|
+
const localCooldown = checkLocalCooldown(squadName, agentName, cooldownMs);
|
|
3368
|
+
if (!localCooldown.ok) {
|
|
3369
|
+
spinner.stop();
|
|
3370
|
+
writeLine();
|
|
3371
|
+
writeLine(` ${colors.yellow}${icons.warning} Skipping: cooldown not elapsed${RESET}`);
|
|
3372
|
+
writeLine(` ${colors.dim}Last run: ${formatDuration(localCooldown.elapsedMs)} ago (cooldown: ${formatDuration(localCooldown.cooldownMs)})${RESET}`);
|
|
3373
|
+
writeLine();
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (options.verbose) {
|
|
3377
|
+
writeLine(` ${colors.dim}Local cooldown: \u2713 (${formatDuration(localCooldown.elapsedMs || 0)} since last run)${RESET}`);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
logExecution({
|
|
3381
|
+
squadName,
|
|
3382
|
+
agentName,
|
|
3383
|
+
executionId,
|
|
3384
|
+
startTime,
|
|
3385
|
+
status: "running",
|
|
3386
|
+
trigger: options.trigger || "manual",
|
|
3387
|
+
taskType
|
|
3388
|
+
});
|
|
3389
|
+
if (options.verbose && learnings.length > 0) {
|
|
3390
|
+
writeLine(` ${colors.dim}Injecting ${learnings.length} learnings${RESET}`);
|
|
3391
|
+
}
|
|
3392
|
+
const systemProtocol = loadSystemProtocol();
|
|
3393
|
+
const systemContext = systemProtocol ? `
|
|
3394
|
+
${systemProtocol}
|
|
3395
|
+
` : "";
|
|
3396
|
+
const agentRole = classifyAgent(agentName);
|
|
3397
|
+
const contextRole = agentName.includes("company-lead") ? "coo" : agentRole ?? "worker";
|
|
3398
|
+
const squadContext = gatherSquadContext(squadName, agentName, {
|
|
3399
|
+
verbose: options.verbose,
|
|
3400
|
+
agentPath,
|
|
3401
|
+
role: contextRole
|
|
3402
|
+
});
|
|
3403
|
+
let cognitionContext = "";
|
|
3404
|
+
try {
|
|
3405
|
+
const { loadSession: loadSession2 } = await import("./auth-YW3UPFSB.js");
|
|
3406
|
+
const { getApiUrl: getApiUrl2 } = await import("./env-config-SQEI3Y7Y.js");
|
|
3407
|
+
const session = loadSession2();
|
|
3408
|
+
if (session?.accessToken && session.status === "active") {
|
|
3409
|
+
const safeSquadName = encodeURIComponent(squadName);
|
|
3410
|
+
const res = await fetch(`${getApiUrl2()}/cognition/context/squad:${safeSquadName}`, {
|
|
3411
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
3412
|
+
signal: AbortSignal.timeout(3e3)
|
|
3413
|
+
});
|
|
3414
|
+
if (res.ok) {
|
|
3415
|
+
const data = await res.json();
|
|
3416
|
+
if (data.markdown && !data.markdown.includes("No cognition data")) {
|
|
3417
|
+
cognitionContext = `
|
|
3418
|
+
${data.markdown}
|
|
3419
|
+
`;
|
|
3420
|
+
if (options.verbose) {
|
|
3421
|
+
writeLine(` ${colors.dim}Injecting cognition beliefs${RESET}`);
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
} catch (e) {
|
|
3427
|
+
if (options.verbose) writeLine(` ${colors.dim}warn: cognition fetch failed: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
3428
|
+
}
|
|
3429
|
+
const timeoutMins = options.timeout || DEFAULT_TIMEOUT_MINUTES;
|
|
3430
|
+
const taskDirective = options.task ? `
|
|
3431
|
+
## TASK DIRECTIVE (overrides default behavior)
|
|
3432
|
+
${options.task}
|
|
3433
|
+
` : "";
|
|
3434
|
+
const prompt = `Execute the ${agentName} agent from squad ${squadName}.
|
|
3435
|
+
|
|
3436
|
+
Read the agent definition at ${agentPath} and follow its instructions exactly.
|
|
3437
|
+
${taskDirective}
|
|
3438
|
+
The agent definition contains:
|
|
3439
|
+
- Purpose/role
|
|
3440
|
+
- Tools it can use (MCP servers, skills)
|
|
3441
|
+
- Step-by-step instructions
|
|
3442
|
+
- Expected output format
|
|
3443
|
+
|
|
3444
|
+
TOOL PREFERENCE: Always prefer CLI tools over MCP servers when both can accomplish the task:
|
|
3445
|
+
- Use \`squads\` CLI for squad operations (run, memory, status, feedback)
|
|
3446
|
+
- Use \`gh\` CLI for GitHub (issues, PRs, repos)
|
|
3447
|
+
- Use \`git\` CLI for version control
|
|
3448
|
+
- Use Bash for file operations, builds, tests
|
|
3449
|
+
- Only use MCP tools when CLI cannot do it or MCP is significantly better
|
|
3450
|
+
${systemContext}${squadContext}${cognitionContext}${learningContext}
|
|
3451
|
+
TIME LIMIT: You have ${timeoutMins} minutes. Work efficiently:
|
|
3452
|
+
- Focus on the most important tasks first
|
|
3453
|
+
- If a task is taking too long, move on and note it for next run
|
|
3454
|
+
- Aim to complete within ${Math.floor(timeoutMins * SOFT_DEADLINE_RATIO)} minutes`;
|
|
3455
|
+
const agentProvider = parseAgentProvider(agentPath);
|
|
3456
|
+
const squad = loadSquad(squadName);
|
|
3457
|
+
const squadDefaultProvider = squad?.providers?.default;
|
|
3458
|
+
const provider = agentProvider || options.provider || squadDefaultProvider || "anthropic";
|
|
3459
|
+
const isAnthropic = provider === "anthropic";
|
|
3460
|
+
if (options.verbose && (agentProvider || squadDefaultProvider)) {
|
|
3461
|
+
writeLine(` ${colors.dim}Provider resolution:${RESET}`);
|
|
3462
|
+
if (agentProvider) writeLine(` ${colors.dim}Agent: ${agentProvider}${RESET}`);
|
|
3463
|
+
if (options.provider) writeLine(` ${colors.dim}CLI: ${options.provider}${RESET}`);
|
|
3464
|
+
if (squadDefaultProvider) writeLine(` ${colors.dim}Squad: ${squadDefaultProvider}${RESET}`);
|
|
3465
|
+
writeLine(` ${colors.dim}\u2192 Using: ${provider}${RESET}`);
|
|
3466
|
+
}
|
|
3467
|
+
const cliAvailable = isAnthropic ? await checkClaudeCliAvailable() : isProviderCLIAvailable(provider);
|
|
3468
|
+
if (options.execute && cliAvailable) {
|
|
3469
|
+
const cliConfig = getCLIConfig(provider);
|
|
3470
|
+
const cliName = cliConfig?.displayName || provider;
|
|
3471
|
+
const isBackground = options.background === true && !options.watch;
|
|
3472
|
+
const isWatch = options.watch === true;
|
|
3473
|
+
const isForeground = !isBackground && !isWatch;
|
|
3474
|
+
spinner.text = isBackground ? `Launching ${agentName} with ${cliName} in background...` : isWatch ? `Starting ${agentName} with ${cliName} (watch mode)...` : `Running ${agentName} with ${cliName}...`;
|
|
3475
|
+
const frontmatter = parseAgentFrontmatter(agentPath);
|
|
3476
|
+
const hasCriteria = !!frontmatter.acceptance_criteria && options.verify !== false;
|
|
3477
|
+
const maxRetries = frontmatter.max_retries ?? 2;
|
|
3478
|
+
let currentPrompt = prompt;
|
|
3479
|
+
for (let attempt = 0; attempt <= (hasCriteria ? maxRetries : 0); attempt++) {
|
|
3480
|
+
try {
|
|
3481
|
+
let result;
|
|
3482
|
+
if (isAnthropic) {
|
|
3483
|
+
result = await executeWithClaude(currentPrompt, {
|
|
3484
|
+
verbose: options.verbose,
|
|
3485
|
+
timeoutMinutes: options.timeout || 30,
|
|
3486
|
+
foreground: options.foreground,
|
|
3487
|
+
background: options.background,
|
|
3488
|
+
watch: options.watch,
|
|
3489
|
+
useApi: options.useApi,
|
|
3490
|
+
effort: options.effort,
|
|
3491
|
+
skills: options.skills,
|
|
3492
|
+
trigger: options.trigger || "manual",
|
|
3493
|
+
squadName,
|
|
3494
|
+
agentName,
|
|
3495
|
+
model: options.model
|
|
3496
|
+
});
|
|
3497
|
+
} else {
|
|
3498
|
+
result = await executeWithProvider(provider, currentPrompt, {
|
|
3499
|
+
verbose: options.verbose,
|
|
3500
|
+
foreground: !isBackground,
|
|
3501
|
+
squadName,
|
|
3502
|
+
agentName
|
|
3503
|
+
});
|
|
3504
|
+
}
|
|
3505
|
+
if (hasCriteria && (isForeground || isWatch)) {
|
|
3506
|
+
const verification = await verifyExecution(
|
|
3507
|
+
squadName,
|
|
3508
|
+
agentName,
|
|
3509
|
+
frontmatter.acceptance_criteria,
|
|
3510
|
+
{ verbose: options.verbose }
|
|
3511
|
+
);
|
|
3512
|
+
if (!verification.passed && attempt < maxRetries) {
|
|
3513
|
+
writeLine(` ${colors.yellow}Verification: FAIL - ${verification.reason}${RESET}`);
|
|
3514
|
+
writeLine(` ${colors.dim}Retrying (${attempt + 1}/${maxRetries})...${RESET}`);
|
|
3515
|
+
currentPrompt = `${prompt}
|
|
3516
|
+
|
|
3517
|
+
## PREVIOUS ATTEMPT FAILED
|
|
3518
|
+
Verification found: ${verification.reason}
|
|
3519
|
+
Please address this issue and try again.`;
|
|
3520
|
+
continue;
|
|
3521
|
+
}
|
|
3522
|
+
if (verification.passed) {
|
|
3523
|
+
writeLine(` ${colors.green}Verification: PASS - ${verification.reason}${RESET}`);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
emitExecutionEvent("agent.completed", {
|
|
3527
|
+
squad: squadName,
|
|
3528
|
+
agent: agentName,
|
|
3529
|
+
executionId
|
|
3530
|
+
}).catch(() => {
|
|
3531
|
+
});
|
|
3532
|
+
if (isForeground || isWatch) {
|
|
3533
|
+
spinner.succeed(`Agent ${agentName} completed (${cliName})`);
|
|
3534
|
+
} else {
|
|
3535
|
+
spinner.succeed(`Agent ${agentName} launched in background (${cliName})`);
|
|
3536
|
+
writeLine(` ${colors.dim}${result}${RESET}`);
|
|
3537
|
+
writeLine();
|
|
3538
|
+
writeLine(` ${colors.dim}Monitor:${RESET} squads workers`);
|
|
3539
|
+
writeLine(` ${colors.dim}Memory:${RESET} squads memory show ${squadName}`);
|
|
3540
|
+
}
|
|
3541
|
+
break;
|
|
3542
|
+
} catch (error) {
|
|
3543
|
+
emitExecutionEvent("agent.failed", {
|
|
3544
|
+
squad: squadName,
|
|
3545
|
+
agent: agentName,
|
|
3546
|
+
executionId,
|
|
3547
|
+
error: String(error)
|
|
3548
|
+
}).catch(() => {
|
|
3549
|
+
});
|
|
3550
|
+
spinner.fail(`Agent ${agentName} failed to launch`);
|
|
3551
|
+
updateExecutionStatus(squadName, agentName, executionId, "failed", {
|
|
3552
|
+
error: String(error),
|
|
3553
|
+
durationMs: Date.now() - startMs
|
|
3554
|
+
});
|
|
3555
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3556
|
+
const isLikelyBug = error instanceof ReferenceError || error instanceof TypeError || error instanceof SyntaxError;
|
|
3557
|
+
writeLine(` ${colors.red}${msg}${RESET}`);
|
|
3558
|
+
writeLine();
|
|
3559
|
+
if (isLikelyBug) {
|
|
3560
|
+
writeLine(` ${colors.yellow}This looks like a bug. Please try:${RESET}`);
|
|
3561
|
+
writeLine(` ${colors.dim}$${RESET} squads doctor ${colors.dim}\u2014 check your setup${RESET}`);
|
|
3562
|
+
writeLine(` ${colors.dim}$${RESET} squads update ${colors.dim}\u2014 get the latest fixes${RESET}`);
|
|
3563
|
+
writeLine();
|
|
3564
|
+
writeLine(` ${colors.dim}If the problem persists, file an issue:${RESET}`);
|
|
3565
|
+
writeLine(` ${colors.dim}https://github.com/agents-squads/squads-cli/issues${RESET}`);
|
|
3566
|
+
} else {
|
|
3567
|
+
writeLine(` ${colors.dim}Run \`squads doctor\` to check your setup, or \`squads run ${agentName} --verbose\` for details.${RESET}`);
|
|
3568
|
+
}
|
|
3569
|
+
break;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
} else {
|
|
3573
|
+
spinner.succeed(`Agent ${agentName} ready`);
|
|
3574
|
+
writeLine(` ${colors.dim}Execution logged: ${startTime}${RESET}`);
|
|
3575
|
+
if (!cliAvailable) {
|
|
3576
|
+
const cliConfig = getCLIConfig(provider);
|
|
3577
|
+
writeLine();
|
|
3578
|
+
writeLine(` ${colors.yellow}${cliConfig?.command || provider} CLI not found${RESET}`);
|
|
3579
|
+
writeLine(` ${colors.dim}Install: ${cliConfig?.install || "squads providers"}${RESET}`);
|
|
3580
|
+
}
|
|
3581
|
+
writeLine();
|
|
3582
|
+
writeLine(` ${colors.dim}To launch as background task:${RESET}`);
|
|
3583
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} -a ${colors.cyan}${agentName}${RESET}`);
|
|
3584
|
+
if (provider !== "anthropic") {
|
|
3585
|
+
writeLine(` ${colors.dim}$${RESET} squads run ${colors.cyan}${squadName}${RESET} -a ${colors.cyan}${agentName}${RESET} --provider=${provider}`);
|
|
3586
|
+
}
|
|
3587
|
+
writeLine();
|
|
3588
|
+
writeLine(` ${colors.dim}Or run interactively:${RESET}`);
|
|
3589
|
+
writeLine(` ${colors.dim}$${RESET} Run the ${colors.cyan}${agentName}${RESET} agent from ${agentPath}`);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
async function checkClaudeCliAvailable() {
|
|
3593
|
+
return new Promise((resolve) => {
|
|
3594
|
+
const check = spawn("which", ["claude"], { stdio: "pipe" });
|
|
3595
|
+
check.on("close", (code) => resolve(code === 0));
|
|
3596
|
+
check.on("error", () => resolve(false));
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
async function preflightExecutorCheck(provider) {
|
|
3600
|
+
if (process.env.SQUADS_SKIP_CHECKS === "1") {
|
|
3601
|
+
return true;
|
|
3602
|
+
}
|
|
3603
|
+
const isAnthropic = provider === "anthropic";
|
|
3604
|
+
let cliFound;
|
|
3605
|
+
if (isAnthropic) {
|
|
3606
|
+
cliFound = await checkClaudeCliAvailable();
|
|
3607
|
+
} else {
|
|
3608
|
+
cliFound = isProviderCLIAvailable(provider);
|
|
3609
|
+
}
|
|
3610
|
+
if (!cliFound) {
|
|
3611
|
+
const cliConfig = getCLIConfig(provider);
|
|
3612
|
+
const cliName = cliConfig?.command || provider;
|
|
3613
|
+
const installCmd = cliConfig?.install || `See ${provider} documentation`;
|
|
3614
|
+
writeLine();
|
|
3615
|
+
writeLine(` ${icons.error} ${colors.red}${cliName} CLI not found${RESET}`);
|
|
3616
|
+
writeLine();
|
|
3617
|
+
writeLine(` ${colors.dim}The ${cliName} command is required to run agents but was not found on your PATH.${RESET}`);
|
|
3618
|
+
writeLine();
|
|
3619
|
+
writeLine(` ${colors.cyan}Install:${RESET} ${installCmd}`);
|
|
3620
|
+
writeLine();
|
|
3621
|
+
writeLine(` ${colors.dim}Skip this check: SQUADS_SKIP_CHECKS=1 squads run ...${RESET}`);
|
|
3622
|
+
writeLine();
|
|
3623
|
+
return false;
|
|
3624
|
+
}
|
|
3625
|
+
return true;
|
|
3626
|
+
}
|
|
3627
|
+
function buildAgentEnv(baseEnv, execContext, options) {
|
|
3628
|
+
const { CLAUDECODE: _, ...cleanEnv } = baseEnv;
|
|
3629
|
+
const env = {
|
|
3630
|
+
...cleanEnv,
|
|
3631
|
+
SQUADS_SQUAD: execContext.squad,
|
|
3632
|
+
SQUADS_AGENT: execContext.agent,
|
|
3633
|
+
SQUADS_TASK_TYPE: execContext.taskType,
|
|
3634
|
+
SQUADS_TRIGGER: execContext.trigger,
|
|
3635
|
+
SQUADS_EXECUTION_ID: execContext.executionId,
|
|
3636
|
+
BRIDGE_API: getBridgeUrl()
|
|
3637
|
+
};
|
|
3638
|
+
if (options?.ghToken) env.GH_TOKEN = options.ghToken;
|
|
3639
|
+
if (options?.includeOtel) {
|
|
3640
|
+
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}`;
|
|
3641
|
+
}
|
|
3642
|
+
if (options?.effort) env.CLAUDE_EFFORT = options.effort;
|
|
3643
|
+
if (options?.skills && options.skills.length > 0) env.CLAUDE_SKILLS = options.skills.join(",");
|
|
3644
|
+
return env;
|
|
3645
|
+
}
|
|
3646
|
+
function logVerboseExecution(config) {
|
|
3647
|
+
writeLine(` ${colors.dim}Project: ${config.projectRoot}${RESET}`);
|
|
3648
|
+
writeLine(` ${colors.dim}Mode: ${config.mode}${RESET}`);
|
|
3649
|
+
if (config.logFile) writeLine(` ${colors.dim}Log: ${config.logFile}${RESET}`);
|
|
3650
|
+
if (config.mcpConfigPath) writeLine(` ${colors.dim}MCP config: ${config.mcpConfigPath}${RESET}`);
|
|
3651
|
+
if (config.useApi !== void 0) writeLine(` ${colors.dim}Auth: ${config.useApi ? "API credits" : "subscription"}${RESET}`);
|
|
3652
|
+
writeLine(` ${colors.dim}Execution: ${config.execContext.executionId}${RESET}`);
|
|
3653
|
+
writeLine(` ${colors.dim}Task type: ${config.execContext.taskType}${RESET}`);
|
|
3654
|
+
writeLine(` ${colors.dim}Trigger: ${config.execContext.trigger}${RESET}`);
|
|
3655
|
+
if (config.effort) writeLine(` ${colors.dim}Effort: ${config.effort}${RESET}`);
|
|
3656
|
+
if (config.skills && config.skills.length > 0) writeLine(` ${colors.dim}Skills: ${config.skills.join(", ")}${RESET}`);
|
|
3657
|
+
if (config.resolvedModel || config.claudeModelAlias) {
|
|
3658
|
+
const source = config.explicitModel ? "explicit" : "auto-routed";
|
|
3659
|
+
const displayModel = config.resolvedModel || config.claudeModelAlias;
|
|
3660
|
+
writeLine(` ${colors.dim}Model: ${displayModel} (${source})${RESET}`);
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
function resolveTargetRepoRoot(projectRoot, squad) {
|
|
3664
|
+
if (!squad?.repo) return projectRoot;
|
|
3665
|
+
const repoName = squad.repo.split("/").pop();
|
|
3666
|
+
if (!repoName) return projectRoot;
|
|
3667
|
+
const candidatePath = join5(projectRoot, "..", repoName);
|
|
3668
|
+
return existsSync5(candidatePath) ? candidatePath : projectRoot;
|
|
3669
|
+
}
|
|
3670
|
+
function createAgentWorktree(projectRoot, squadName, agentName) {
|
|
3671
|
+
const timestamp = Date.now();
|
|
3672
|
+
const branchName = `agent/${squadName}/${agentName}-${timestamp}`;
|
|
3673
|
+
const worktreePath = join5(projectRoot, "..", ".worktrees", `${squadName}-${agentName}-${timestamp}`);
|
|
3674
|
+
try {
|
|
3675
|
+
mkdirSync4(join5(projectRoot, "..", ".worktrees"), { recursive: true });
|
|
3676
|
+
execSync3(`git worktree add '${worktreePath}' -b '${branchName}' HEAD`, { cwd: projectRoot, stdio: "pipe" });
|
|
3677
|
+
return worktreePath;
|
|
3678
|
+
} catch (e) {
|
|
3679
|
+
writeLine(` ${colors.dim}warn: worktree creation failed, using project root: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
3680
|
+
return projectRoot;
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
function cleanupWorktree(worktreePath, projectRoot) {
|
|
3684
|
+
if (worktreePath === projectRoot) return;
|
|
3685
|
+
try {
|
|
3686
|
+
const branchInfo = execSync3(`git -C '${projectRoot}' worktree list --porcelain`, { encoding: "utf-8" });
|
|
3687
|
+
let branchName = "";
|
|
3688
|
+
const lines = branchInfo.split("\n");
|
|
3689
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3690
|
+
if (lines[i] === `worktree ${worktreePath}` && i + 2 < lines.length) {
|
|
3691
|
+
const branchLine = lines[i + 2];
|
|
3692
|
+
if (branchLine.startsWith("branch refs/heads/")) {
|
|
3693
|
+
branchName = branchLine.replace("branch refs/heads/", "");
|
|
3694
|
+
}
|
|
3695
|
+
break;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
execSync3(`git -C '${projectRoot}' worktree remove '${worktreePath}' --force`, { stdio: "pipe" });
|
|
3699
|
+
if (branchName && branchName.startsWith("agent/")) {
|
|
3700
|
+
execSync3(`git -C '${projectRoot}' branch -D '${branchName}'`, { stdio: "pipe" });
|
|
3701
|
+
}
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
function buildDetachedShellScript(config) {
|
|
3706
|
+
const modelFlag = config.claudeModelAlias ? `--model ${config.claudeModelAlias}` : "";
|
|
3707
|
+
const branchName = `agent/${config.squadName}/${config.agentName}-${config.timestamp}`;
|
|
3708
|
+
const worktreeDir = `${config.projectRoot}/../.worktrees/${config.squadName}-${config.agentName}-${config.timestamp}`;
|
|
3709
|
+
const cleanup = `if [ "\${WORK_DIR}" != '${config.projectRoot}' ]; then git -C '${config.projectRoot}' worktree remove "\${WORK_DIR}" --force 2>/dev/null; BRANCH='${branchName}'; git -C '${config.projectRoot}' branch -D "\${BRANCH}" 2>/dev/null; fi`;
|
|
3710
|
+
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}"; unset CLAUDECODE; claude --print --dangerously-skip-permissions --disable-slash-commands ${modelFlag} -- '${config.escapedPrompt}' > '${config.logFile}' 2>&1; ${cleanup}`;
|
|
3711
|
+
return `echo $$ > '${config.pidFile}'; ${script}`;
|
|
3712
|
+
}
|
|
3713
|
+
function prepareLogFiles(projectRoot, squadName, agentName, timestamp) {
|
|
3714
|
+
const logDir = join5(projectRoot, ".agents", "logs", squadName);
|
|
3715
|
+
const logFile = join5(logDir, `${agentName}-${timestamp}.log`);
|
|
3716
|
+
const pidFile = join5(logDir, `${agentName}-${timestamp}.pid`);
|
|
3717
|
+
if (!existsSync5(logDir)) {
|
|
3718
|
+
mkdirSync4(logDir, { recursive: true });
|
|
3719
|
+
}
|
|
3720
|
+
return { logDir, logFile, pidFile };
|
|
3721
|
+
}
|
|
3722
|
+
function executeForeground(config) {
|
|
3723
|
+
const workDir = createAgentWorktree(config.projectRoot, config.squadName, config.agentName);
|
|
3724
|
+
return new Promise((resolve, reject) => {
|
|
3725
|
+
const claude = spawn("claude", config.claudeArgs, {
|
|
3726
|
+
stdio: "inherit",
|
|
3727
|
+
cwd: workDir,
|
|
3728
|
+
env: config.agentEnv
|
|
3729
|
+
});
|
|
3730
|
+
claude.on("close", async (code) => {
|
|
3731
|
+
const durationMs = Date.now() - config.startMs;
|
|
3732
|
+
if (code === 0) {
|
|
3733
|
+
updateExecutionStatus(config.squadName, config.agentName, config.execContext.executionId, "completed", {
|
|
3734
|
+
outcome: "Session completed successfully",
|
|
3735
|
+
durationMs
|
|
3736
|
+
});
|
|
3737
|
+
const commitResult = await autoCommitAgentWork(config.squadName, config.agentName, config.execContext.executionId, config.provider);
|
|
3738
|
+
if (commitResult.committed) {
|
|
3739
|
+
writeLine();
|
|
3740
|
+
writeLine(` ${colors.green}Auto-committed agent work${RESET}`);
|
|
3741
|
+
}
|
|
3742
|
+
cleanupWorktree(workDir, config.projectRoot);
|
|
3743
|
+
resolve("Session completed");
|
|
3744
|
+
} else {
|
|
3745
|
+
updateExecutionStatus(config.squadName, config.agentName, config.execContext.executionId, "failed", {
|
|
3746
|
+
error: `Claude exited with code ${code}`,
|
|
3747
|
+
durationMs
|
|
3748
|
+
});
|
|
3749
|
+
cleanupWorktree(workDir, config.projectRoot);
|
|
3750
|
+
reject(new Error(`Claude exited with code ${code}`));
|
|
3751
|
+
}
|
|
3752
|
+
});
|
|
3753
|
+
claude.on("error", (err) => {
|
|
3754
|
+
const durationMs = Date.now() - config.startMs;
|
|
3755
|
+
updateExecutionStatus(config.squadName, config.agentName, config.execContext.executionId, "failed", {
|
|
3756
|
+
error: String(err),
|
|
3757
|
+
durationMs
|
|
3758
|
+
});
|
|
3759
|
+
cleanupWorktree(workDir, config.projectRoot);
|
|
3760
|
+
reject(err);
|
|
3761
|
+
});
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
async function executeWatch(config) {
|
|
3765
|
+
const child = spawn("sh", ["-c", config.wrapperScript], {
|
|
3766
|
+
cwd: config.projectRoot,
|
|
3767
|
+
detached: true,
|
|
3768
|
+
stdio: "ignore",
|
|
3769
|
+
env: config.agentEnv
|
|
3770
|
+
});
|
|
3771
|
+
child.unref();
|
|
3772
|
+
await new Promise((resolve) => setTimeout(resolve, LOG_FILE_INIT_DELAY_MS));
|
|
3773
|
+
writeLine(` ${colors.dim}Tailing log (Ctrl+C to stop watching, agent continues)...${RESET}`);
|
|
3774
|
+
writeLine();
|
|
3775
|
+
const tail = spawn("tail", ["-f", config.logFile], { stdio: "inherit" });
|
|
3776
|
+
process.on("SIGINT", () => {
|
|
3777
|
+
tail.kill();
|
|
3778
|
+
writeLine();
|
|
3779
|
+
writeLine(` ${colors.dim}Stopped watching. Agent continues in background.${RESET}`);
|
|
3780
|
+
writeLine(` ${colors.dim}Resume: tail -f ${config.logFile}${RESET}`);
|
|
3781
|
+
process.exit(0);
|
|
3782
|
+
});
|
|
3783
|
+
return new Promise((resolve) => {
|
|
3784
|
+
tail.on("close", () => {
|
|
3785
|
+
resolve(`Agent running in background. Log: ${config.logFile}`);
|
|
3786
|
+
});
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
async function executeWithClaude(prompt, options) {
|
|
3790
|
+
const {
|
|
3791
|
+
verbose,
|
|
3792
|
+
timeoutMinutes: _timeoutMinutes = 30,
|
|
3793
|
+
foreground,
|
|
3794
|
+
background,
|
|
3795
|
+
watch,
|
|
3796
|
+
useApi,
|
|
3797
|
+
effort,
|
|
3798
|
+
skills,
|
|
3799
|
+
trigger = "manual",
|
|
3800
|
+
squadName,
|
|
3801
|
+
agentName,
|
|
3802
|
+
model
|
|
3803
|
+
} = options;
|
|
3804
|
+
const runInBackground = background === true && !watch;
|
|
3805
|
+
const runInWatch = watch === true;
|
|
3806
|
+
const runInForeground = !runInBackground && !runInWatch;
|
|
3807
|
+
const startMs = Date.now();
|
|
3808
|
+
const projectRoot = getProjectRoot();
|
|
3809
|
+
ensureProjectTrusted(projectRoot);
|
|
3810
|
+
const squad = squadName !== "unknown" ? loadSquad(squadName) : null;
|
|
3811
|
+
const mcpConfigPath = selectMcpConfig(squadName, squad);
|
|
3812
|
+
const taskType = detectTaskType(agentName);
|
|
3813
|
+
const resolvedModel = resolveModel(model, squad, taskType);
|
|
3814
|
+
const provider = resolvedModel ? detectProviderFromModel(resolvedModel) : "anthropic";
|
|
3815
|
+
const targetRepoRoot = resolveTargetRepoRoot(projectRoot, squad);
|
|
3816
|
+
if (provider !== "anthropic" && provider !== "unknown") {
|
|
3817
|
+
if (verbose) {
|
|
3818
|
+
const source = model ? "explicit" : "auto-routed";
|
|
3819
|
+
writeLine(` ${colors.dim}Model: ${resolvedModel} (${source})${RESET}`);
|
|
3820
|
+
writeLine(` ${colors.dim}Provider: ${provider}${RESET}`);
|
|
3821
|
+
}
|
|
3822
|
+
return executeWithProvider(provider, prompt, {
|
|
3823
|
+
verbose,
|
|
3824
|
+
foreground,
|
|
3825
|
+
cwd: targetRepoRoot,
|
|
3826
|
+
squadName,
|
|
3827
|
+
agentName
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3830
|
+
const claudeModelAlias = resolvedModel ? getClaudeModelAlias(resolvedModel) : void 0;
|
|
3831
|
+
const execContext = {
|
|
3832
|
+
squad: squadName,
|
|
3833
|
+
agent: agentName,
|
|
3834
|
+
taskType,
|
|
3835
|
+
trigger,
|
|
3836
|
+
executionId: generateExecutionId()
|
|
3837
|
+
};
|
|
3838
|
+
const { ANTHROPIC_API_KEY: _apiKey, CLAUDECODE: _claudeCode, ...envWithoutApiKey } = process.env;
|
|
3839
|
+
const spawnEnv = useApi ? (() => {
|
|
3840
|
+
const { CLAUDECODE: _, ...rest } = process.env;
|
|
3841
|
+
return rest;
|
|
3842
|
+
})() : envWithoutApiKey;
|
|
3843
|
+
const escapedPrompt = prompt.replace(/'/g, "'\\''");
|
|
3844
|
+
await registerContextWithBridge(execContext);
|
|
3845
|
+
let botGhToken;
|
|
3846
|
+
try {
|
|
3847
|
+
const ghEnv = await getBotGhEnv();
|
|
3848
|
+
botGhToken = ghEnv.GH_TOKEN;
|
|
3849
|
+
} catch {
|
|
3850
|
+
}
|
|
3851
|
+
if (runInForeground) {
|
|
3852
|
+
if (verbose) {
|
|
3853
|
+
logVerboseExecution({
|
|
3854
|
+
projectRoot,
|
|
3855
|
+
mode: "foreground",
|
|
3856
|
+
useApi,
|
|
3857
|
+
execContext,
|
|
3858
|
+
effort,
|
|
3859
|
+
skills,
|
|
3860
|
+
resolvedModel,
|
|
3861
|
+
claudeModelAlias,
|
|
3862
|
+
explicitModel: model
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
const claudeArgs = [];
|
|
3866
|
+
if (!process.stdin.isTTY) claudeArgs.push("--print");
|
|
3867
|
+
claudeArgs.push("--dangerously-skip-permissions");
|
|
3868
|
+
claudeArgs.push("--disable-slash-commands");
|
|
3869
|
+
if (mcpConfigPath) claudeArgs.push("--mcp-config", mcpConfigPath);
|
|
3870
|
+
if (claudeModelAlias) claudeArgs.push("--model", claudeModelAlias);
|
|
3871
|
+
claudeArgs.push("--", prompt);
|
|
3872
|
+
const agentEnv2 = buildAgentEnv(spawnEnv, execContext, {
|
|
3873
|
+
effort,
|
|
3874
|
+
skills,
|
|
3875
|
+
includeOtel: true,
|
|
3876
|
+
ghToken: botGhToken
|
|
3877
|
+
});
|
|
3878
|
+
return executeForeground({
|
|
3879
|
+
prompt,
|
|
3880
|
+
claudeArgs,
|
|
3881
|
+
agentEnv: agentEnv2,
|
|
3882
|
+
projectRoot: targetRepoRoot,
|
|
3883
|
+
squadName,
|
|
3884
|
+
agentName,
|
|
3885
|
+
execContext,
|
|
3886
|
+
startMs,
|
|
3887
|
+
provider
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
const timestamp = Date.now();
|
|
3891
|
+
const { logFile, pidFile } = prepareLogFiles(projectRoot, squadName, agentName, timestamp);
|
|
3892
|
+
const agentEnv = buildAgentEnv(spawnEnv, execContext, {
|
|
3893
|
+
effort,
|
|
3894
|
+
skills,
|
|
3895
|
+
includeOtel: !runInWatch,
|
|
3896
|
+
ghToken: botGhToken
|
|
3897
|
+
});
|
|
3898
|
+
const wrapperScript = buildDetachedShellScript({
|
|
3899
|
+
projectRoot: targetRepoRoot,
|
|
3900
|
+
squadName,
|
|
3901
|
+
agentName,
|
|
3902
|
+
timestamp,
|
|
3903
|
+
claudeModelAlias,
|
|
3904
|
+
escapedPrompt,
|
|
3905
|
+
logFile,
|
|
3906
|
+
pidFile
|
|
3907
|
+
});
|
|
3908
|
+
if (runInWatch) {
|
|
3909
|
+
if (verbose) {
|
|
3910
|
+
logVerboseExecution({
|
|
3911
|
+
projectRoot,
|
|
3912
|
+
mode: "watch (background + tail)",
|
|
3913
|
+
execContext,
|
|
3914
|
+
logFile
|
|
3915
|
+
});
|
|
3916
|
+
}
|
|
3917
|
+
return executeWatch({ projectRoot: targetRepoRoot, agentEnv, logFile, wrapperScript });
|
|
3918
|
+
}
|
|
3919
|
+
if (verbose) {
|
|
3920
|
+
logVerboseExecution({
|
|
3921
|
+
projectRoot,
|
|
3922
|
+
mode: "background",
|
|
3923
|
+
useApi,
|
|
3924
|
+
execContext,
|
|
3925
|
+
effort,
|
|
3926
|
+
skills,
|
|
3927
|
+
resolvedModel,
|
|
3928
|
+
claudeModelAlias,
|
|
3929
|
+
explicitModel: model,
|
|
3930
|
+
logFile,
|
|
3931
|
+
mcpConfigPath
|
|
3932
|
+
});
|
|
3933
|
+
}
|
|
3934
|
+
const child = spawn("sh", ["-c", wrapperScript], {
|
|
3935
|
+
cwd: targetRepoRoot,
|
|
3936
|
+
detached: true,
|
|
3937
|
+
stdio: "ignore",
|
|
3938
|
+
env: agentEnv
|
|
3939
|
+
});
|
|
3940
|
+
child.unref();
|
|
3941
|
+
if (verbose) {
|
|
3942
|
+
writeLine(` ${colors.dim}Monitor: tail -f ${logFile}${RESET}`);
|
|
3943
|
+
}
|
|
3944
|
+
return `Log: ${logFile}. Monitor: tail -f ${logFile}`;
|
|
3945
|
+
}
|
|
3946
|
+
async function executeWithProvider(provider, prompt, options) {
|
|
3947
|
+
const cliConfig = getCLIConfig(provider);
|
|
3948
|
+
if (!cliConfig) {
|
|
3949
|
+
throw new Error(`Unknown provider: ${provider}. Run 'squads providers' to see available providers.`);
|
|
3950
|
+
}
|
|
3951
|
+
if (!isProviderCLIAvailable(provider)) {
|
|
3952
|
+
throw new Error(`CLI '${cliConfig.command}' not found. Install: ${cliConfig.install}`);
|
|
3953
|
+
}
|
|
3954
|
+
const projectRoot = options.cwd || getProjectRoot();
|
|
3955
|
+
const squadName = options.squadName || "unknown";
|
|
3956
|
+
const agentName = options.agentName || "unknown";
|
|
3957
|
+
const timestamp = Date.now();
|
|
3958
|
+
const { CLAUDECODE: _claudeCode, ...cleanEnv } = process.env;
|
|
3959
|
+
const providerEnv = {
|
|
3960
|
+
...cleanEnv,
|
|
3961
|
+
SQUADS_SQUAD: squadName,
|
|
3962
|
+
SQUADS_AGENT: agentName,
|
|
3963
|
+
SQUADS_PROVIDER: provider
|
|
3964
|
+
};
|
|
3965
|
+
const branchName = `agent/${squadName}/${agentName}-${timestamp}`;
|
|
3966
|
+
const worktreePath = join5(projectRoot, "..", ".worktrees", `${squadName}-${agentName}-${timestamp}`);
|
|
3967
|
+
let workDir = projectRoot;
|
|
3968
|
+
try {
|
|
3969
|
+
mkdirSync4(join5(projectRoot, "..", ".worktrees"), { recursive: true });
|
|
3970
|
+
execSync3(`git worktree add '${worktreePath}' -b '${branchName}' HEAD`, { cwd: projectRoot, stdio: "pipe" });
|
|
3971
|
+
workDir = worktreePath;
|
|
3972
|
+
} catch (e) {
|
|
3973
|
+
writeLine(` ${colors.dim}warn: worktree creation failed, using project root: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
3974
|
+
}
|
|
3975
|
+
let effectivePrompt = prompt;
|
|
3976
|
+
if (workDir !== projectRoot) {
|
|
3977
|
+
const agentsDir = join5(projectRoot, ".agents");
|
|
3978
|
+
const targetAgentsDir = join5(workDir, ".agents");
|
|
3979
|
+
if (existsSync5(agentsDir) && !existsSync5(targetAgentsDir)) {
|
|
3980
|
+
try {
|
|
3981
|
+
cpSync(agentsDir, targetAgentsDir, { recursive: true });
|
|
3982
|
+
} catch (e) {
|
|
3983
|
+
writeLine(` ${colors.dim}warn: .agents copy failed: ${e instanceof Error ? e.message : String(e)}${RESET}`);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
effectivePrompt = prompt.replaceAll(projectRoot, workDir);
|
|
3987
|
+
}
|
|
3988
|
+
const args = cliConfig.buildArgs(effectivePrompt);
|
|
3989
|
+
if (options.verbose) {
|
|
3990
|
+
writeLine(` ${colors.dim}Provider: ${cliConfig.displayName}${RESET}`);
|
|
3991
|
+
writeLine(` ${colors.dim}Command: ${cliConfig.command} ${args.join(" ").slice(0, VERBOSE_COMMAND_MAX_CHARS)}...${RESET}`);
|
|
3992
|
+
writeLine(` ${colors.dim}CWD: ${workDir}${RESET}`);
|
|
3993
|
+
if (workDir !== projectRoot) {
|
|
3994
|
+
writeLine(` ${colors.dim}Worktree: ${branchName}${RESET}`);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
if (options.foreground) {
|
|
3998
|
+
return new Promise((resolve, reject) => {
|
|
3999
|
+
const proc = spawn(cliConfig.command, args, {
|
|
4000
|
+
stdio: "inherit",
|
|
4001
|
+
cwd: workDir,
|
|
4002
|
+
env: providerEnv
|
|
4003
|
+
});
|
|
4004
|
+
proc.on("close", (code) => {
|
|
4005
|
+
cleanupWorktree(workDir, projectRoot);
|
|
4006
|
+
if (code === 0) {
|
|
4007
|
+
resolve("Session completed");
|
|
4008
|
+
} else {
|
|
4009
|
+
reject(new Error(`${cliConfig.command} exited with code ${code}`));
|
|
4010
|
+
}
|
|
4011
|
+
});
|
|
4012
|
+
proc.on("error", (err) => {
|
|
4013
|
+
cleanupWorktree(workDir, projectRoot);
|
|
4014
|
+
reject(err);
|
|
4015
|
+
});
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
const logDir = join5(projectRoot, ".agents", "logs", squadName);
|
|
4019
|
+
const logFile = join5(logDir, `${agentName}-${timestamp}.log`);
|
|
4020
|
+
const pidFile = join5(logDir, `${agentName}-${timestamp}.pid`);
|
|
4021
|
+
if (!existsSync5(logDir)) {
|
|
4022
|
+
mkdirSync4(logDir, { recursive: true });
|
|
4023
|
+
}
|
|
4024
|
+
const escapedPrompt = effectivePrompt.replace(/'/g, "'\\''");
|
|
4025
|
+
const providerArgs = cliConfig.buildArgs(escapedPrompt).map((a) => `'${a}'`).join(" ");
|
|
4026
|
+
const cleanupCmd = workDir !== projectRoot ? `; git -C '${projectRoot}' worktree remove '${workDir}' --force 2>/dev/null; git -C '${projectRoot}' branch -D '${branchName}' 2>/dev/null` : "";
|
|
4027
|
+
const shellScript = `cd '${workDir}' && ${cliConfig.command} ${providerArgs} > '${logFile}' 2>&1${cleanupCmd}`;
|
|
4028
|
+
const wrapperScript = `echo $$ > '${pidFile}'; ${shellScript}`;
|
|
4029
|
+
const child = spawn("sh", ["-c", wrapperScript], {
|
|
4030
|
+
cwd: workDir,
|
|
4031
|
+
detached: true,
|
|
4032
|
+
stdio: "ignore",
|
|
4033
|
+
env: providerEnv
|
|
4034
|
+
});
|
|
4035
|
+
child.unref();
|
|
4036
|
+
if (options.verbose) {
|
|
4037
|
+
writeLine(` ${colors.dim}Log: ${logFile}${RESET}`);
|
|
4038
|
+
writeLine(` ${colors.dim}PID file: ${pidFile}${RESET}`);
|
|
4039
|
+
}
|
|
4040
|
+
return `Log: ${logFile}. Monitor: tail -f ${logFile}`;
|
|
4041
|
+
}
|
|
4042
|
+
async function runSquadCommand(squadName, options) {
|
|
4043
|
+
return runCommand(squadName, options);
|
|
4044
|
+
}
|
|
4045
|
+
export {
|
|
4046
|
+
runCommand,
|
|
4047
|
+
runSquadCommand
|
|
4048
|
+
};
|
|
4049
|
+
//# sourceMappingURL=run-I6KAXU6U.js.map
|