squadrant 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/index.js +9154 -0
- package/dist/index.js.map +1 -0
- package/dist/squadrantd.js +3928 -0
- package/dist/squadrantd.js.map +1 -0
- package/package.json +68 -0
- package/plugin/.claude-plugin/plugin.json +5 -0
- package/plugin/skills/add-pick-crew-rule/SKILL.md +88 -0
- package/plugin/skills/captain-ops/SKILL.md +390 -0
- package/plugin/skills/command-ops/SKILL.md +157 -0
- package/plugin/skills/config-doctor/SKILL.md +46 -0
- package/plugin/skills/daily-log/SKILL.md +44 -0
- package/plugin/skills/karpathy-principles/SKILL.md +82 -0
- package/plugin/skills/set-effort/SKILL.md +59 -0
- package/plugin/skills/side-session/SKILL.md +113 -0
- package/plugin/skills/squadrant-effort/SKILL.md +8 -0
- package/plugin/skills/squadrant-new-project/SKILL.md +67 -0
- package/plugin/skills/squadrant-register-project/SKILL.md +60 -0
- package/plugin/skills/where-i-am/SKILL.md +102 -0
- package/plugin/skills/wiki-ops/SKILL.md +96 -0
- package/plugin/skills/wim/SKILL.md +8 -0
- package/scripts/acceptance-interactive-codex.sh +56 -0
- package/scripts/capture-skill.sh +32 -0
- package/scripts/claude-iv-smoke.mjs +133 -0
- package/scripts/fix-skill.sh +39 -0
- package/scripts/gen-codex-types.sh +18 -0
- package/scripts/mailbox-injector-smoke.mjs +124 -0
- package/scripts/mark-learning-useful.sh +22 -0
- package/scripts/migrate-to-squadrant.sh +158 -0
- package/scripts/notify-relay-placement-smoke.mjs +59 -0
- package/scripts/read-handoff.sh +22 -0
- package/scripts/record-learning.sh +31 -0
- package/scripts/record-side-handoff.sh +32 -0
- package/scripts/smoke-push-notify.mjs +147 -0
- package/scripts/spawn-crew-pane.sh +17 -0
- package/scripts/spawn-workspace.sh +206 -0
- package/scripts/wiki-ingest.sh +121 -0
- package/scripts/wiki-log.sh +20 -0
- package/scripts/wiki-query.sh +36 -0
- package/scripts/write-handoff.sh +31 -0
- package/templates/captain.claude.md +44 -0
- package/templates/captain.generic.md +45 -0
- package/templates/command.claude.md +32 -0
- package/templates/crew.claude.md +64 -0
- package/templates/crew.generic.md +51 -0
- package/templates/crew.opencode.md +51 -0
- package/templates/learnings.claude.md +40 -0
- package/templates/side.debug.claude.md +78 -0
- package/templates/side.research.claude.md +63 -0
|
@@ -0,0 +1,3928 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// packages/core/dist/snapshot.js
|
|
12
|
+
var snapshot_exports = {};
|
|
13
|
+
__export(snapshot_exports, {
|
|
14
|
+
assembleDaemonSnapshot: () => assembleDaemonSnapshot,
|
|
15
|
+
buildFreshness: () => buildFreshness
|
|
16
|
+
});
|
|
17
|
+
function buildFreshness(processStartedAt, distBuiltAt2) {
|
|
18
|
+
return processStartedAt >= distBuiltAt2 ? "fresh" : "stale";
|
|
19
|
+
}
|
|
20
|
+
function assembleDaemonSnapshot(input, now) {
|
|
21
|
+
return {
|
|
22
|
+
tier0: {
|
|
23
|
+
pid: input.pid,
|
|
24
|
+
uptimeMs: now - input.processStartedAt,
|
|
25
|
+
version: input.version,
|
|
26
|
+
build: {
|
|
27
|
+
state: buildFreshness(input.processStartedAt, input.distBuiltAt),
|
|
28
|
+
processStartedAt: input.processStartedAt,
|
|
29
|
+
distBuiltAt: input.distBuiltAt
|
|
30
|
+
},
|
|
31
|
+
sweep: {
|
|
32
|
+
lastSweepAt: input.lastSweepAt,
|
|
33
|
+
ageMs: input.lastSweepAt == null ? null : now - input.lastSweepAt,
|
|
34
|
+
cadenceMs: input.sweepCadenceMs
|
|
35
|
+
},
|
|
36
|
+
log: input.log
|
|
37
|
+
},
|
|
38
|
+
tier1: input.health,
|
|
39
|
+
tier2: {
|
|
40
|
+
projects: input.projects.map((p) => ({
|
|
41
|
+
project: p.project,
|
|
42
|
+
mailbox: p.mailbox,
|
|
43
|
+
delivery: {
|
|
44
|
+
maxSeq: p.mailbox.maxSeq,
|
|
45
|
+
lastAckedSeq: p.lastAckedSeq,
|
|
46
|
+
behind: Math.max(0, p.mailbox.maxSeq - p.lastAckedSeq)
|
|
47
|
+
},
|
|
48
|
+
store: { byState: p.storeByState, corruptCount: p.corruptCount }
|
|
49
|
+
})),
|
|
50
|
+
results: input.results
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
var init_snapshot = __esm({
|
|
55
|
+
"packages/core/dist/snapshot.js"() {
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// packages/cli/src/control/squadrantd.ts
|
|
60
|
+
import { join as join13, dirname as dirname5 } from "path";
|
|
61
|
+
import { homedir as homedir8 } from "os";
|
|
62
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
63
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
64
|
+
|
|
65
|
+
// packages/shared/dist/config.js
|
|
66
|
+
import fs from "fs";
|
|
67
|
+
import path from "path";
|
|
68
|
+
import os from "os";
|
|
69
|
+
import chalk from "chalk";
|
|
70
|
+
var CONFIG_DIR = path.join(os.homedir(), ".config", "squadrant");
|
|
71
|
+
var DEFAULT_CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
72
|
+
function getDefaultConfig() {
|
|
73
|
+
return {
|
|
74
|
+
commandName: "\u{1F3DB}\uFE0F command",
|
|
75
|
+
hubVault: path.join(os.homedir(), "squadrant-hub"),
|
|
76
|
+
projects: {},
|
|
77
|
+
agents: {
|
|
78
|
+
claude: { cli: "claude", driver: "claude" }
|
|
79
|
+
},
|
|
80
|
+
defaults: {
|
|
81
|
+
maxCrew: 5,
|
|
82
|
+
worktreeDir: ".worktrees",
|
|
83
|
+
teammateMode: "in-process",
|
|
84
|
+
permissions: {
|
|
85
|
+
command: "auto",
|
|
86
|
+
captain: "auto",
|
|
87
|
+
crew: "auto"
|
|
88
|
+
},
|
|
89
|
+
models: {
|
|
90
|
+
command: "opus",
|
|
91
|
+
captain: "opus",
|
|
92
|
+
crew: "opus",
|
|
93
|
+
exploration: "haiku",
|
|
94
|
+
review: "opus"
|
|
95
|
+
},
|
|
96
|
+
roles: {
|
|
97
|
+
command: { agent: "claude", model: "opus" },
|
|
98
|
+
captain: { agent: "claude", model: "opus" },
|
|
99
|
+
crew: { agent: "claude", model: "opus" },
|
|
100
|
+
exploration: { agent: "claude", model: "haiku" },
|
|
101
|
+
side: { agent: "claude", model: "opus" }
|
|
102
|
+
},
|
|
103
|
+
taskTimeoutMs: 8 * 60 * 60 * 1e3,
|
|
104
|
+
cmuxEventsBridge: true,
|
|
105
|
+
// Audit C2: OFF by design — cmux hibernation is global-only and would
|
|
106
|
+
// hibernate the captain. See the field doc above.
|
|
107
|
+
cmuxAgentHibernation: false,
|
|
108
|
+
crewRouting: {
|
|
109
|
+
rules: [
|
|
110
|
+
{ tier: "extreme", match: "redesign|architect|rewrite|from scratch|deep reasoning", agent: "claude", model: "opus" },
|
|
111
|
+
{ tier: "hard", match: "refactor|migrate|implement|feature|daemon|control-plane", agent: "claude", model: "sonnet" },
|
|
112
|
+
{ tier: "mobile", match: "mobile|ios|swift|android|kotlin|react native", agent: "codex" },
|
|
113
|
+
{ tier: "daily", match: "typo|rename|bump|docs|comment|lint|format", agent: "opencode" }
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
metrics: {
|
|
118
|
+
enabled: true,
|
|
119
|
+
path: path.join(CONFIG_DIR, "metrics.json")
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
124
|
+
try {
|
|
125
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
126
|
+
const config = JSON.parse(raw);
|
|
127
|
+
if (config.defaults.models && !config.defaults.roles) {
|
|
128
|
+
const m = config.defaults.models;
|
|
129
|
+
config.defaults.roles = {
|
|
130
|
+
command: { agent: "claude", model: m.command },
|
|
131
|
+
captain: { agent: "claude", model: m.captain },
|
|
132
|
+
crew: { agent: "claude", model: m.crew },
|
|
133
|
+
exploration: { agent: "claude", model: m.exploration }
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (!config.agents) {
|
|
137
|
+
config.agents = { claude: { cli: "claude", driver: "claude" } };
|
|
138
|
+
}
|
|
139
|
+
if (!config.defaults.crewRouting) {
|
|
140
|
+
config.defaults.crewRouting = getDefaultConfig().defaults.crewRouting;
|
|
141
|
+
saveConfig(config, configPath);
|
|
142
|
+
console.error(chalk.cyan("\u2B06 squadrant upgrade: added default crew routing rules to your config (leveled routing now active). Edit defaults.crewRouting in ~/.config/squadrant/config.json or use the squadrant:add-pick-crew-rule skill."));
|
|
143
|
+
}
|
|
144
|
+
return config;
|
|
145
|
+
} catch {
|
|
146
|
+
return getDefaultConfig();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
|
|
150
|
+
const dir = path.dirname(configPath);
|
|
151
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
152
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// packages/shared/dist/types/control.js
|
|
156
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
157
|
+
"done",
|
|
158
|
+
"failed",
|
|
159
|
+
"cancelled"
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// packages/shared/dist/lib/cmux-autoconfig.js
|
|
163
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, rmSync as rmSync2 } from "fs";
|
|
164
|
+
import { homedir as homedir3 } from "os";
|
|
165
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
166
|
+
|
|
167
|
+
// packages/shared/dist/lib/cmux-config.js
|
|
168
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
169
|
+
import { homedir } from "os";
|
|
170
|
+
import { dirname, join } from "path";
|
|
171
|
+
import { parse, modify, applyEdits } from "jsonc-parser";
|
|
172
|
+
function defaultCmuxConfigPath() {
|
|
173
|
+
return join(homedir(), ".config", "cmux", "cmux.json");
|
|
174
|
+
}
|
|
175
|
+
var SOCKET_CONTROL_MODE_PATH = ["automation", "socketControlMode"];
|
|
176
|
+
var AUTOMATION_MODE = "automation";
|
|
177
|
+
var MINIMAL_TEMPLATE = [
|
|
178
|
+
`{`,
|
|
179
|
+
` // [squadrant] file-managed: allow the launchd squadrant daemon to reach the cmux`,
|
|
180
|
+
` // control socket for daemon-direct notification delivery (#348/#332).`,
|
|
181
|
+
` "automation": {`,
|
|
182
|
+
` "socketControlMode": "${AUTOMATION_MODE}"`,
|
|
183
|
+
` }`,
|
|
184
|
+
`}`,
|
|
185
|
+
``
|
|
186
|
+
].join("\n");
|
|
187
|
+
function ensureSocketAutomation(opts = {}) {
|
|
188
|
+
const path13 = opts.path ?? defaultCmuxConfigPath();
|
|
189
|
+
if (!existsSync(path13)) {
|
|
190
|
+
mkdirSync(dirname(path13), { recursive: true });
|
|
191
|
+
writeFileSync(path13, MINIMAL_TEMPLATE);
|
|
192
|
+
return { path: path13, changed: true, alreadySet: false };
|
|
193
|
+
}
|
|
194
|
+
const text = readFileSync(path13, "utf-8");
|
|
195
|
+
const current = parse(text)?.automation?.socketControlMode;
|
|
196
|
+
if (current === AUTOMATION_MODE) {
|
|
197
|
+
return { path: path13, changed: false, alreadySet: true };
|
|
198
|
+
}
|
|
199
|
+
const edits = modify(text, [...SOCKET_CONTROL_MODE_PATH], AUTOMATION_MODE, {
|
|
200
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
201
|
+
});
|
|
202
|
+
writeFileSync(path13, applyEdits(text, edits));
|
|
203
|
+
return { path: path13, changed: true, alreadySet: false };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// packages/shared/dist/lib/cmux-probe.js
|
|
207
|
+
import { spawn } from "child_process";
|
|
208
|
+
import { mkdtempSync, rmSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
209
|
+
import { tmpdir } from "os";
|
|
210
|
+
import { join as join3 } from "path";
|
|
211
|
+
|
|
212
|
+
// packages/shared/dist/lib/cmux-bin.js
|
|
213
|
+
import { execFileSync } from "child_process";
|
|
214
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
215
|
+
import { homedir as homedir2 } from "os";
|
|
216
|
+
import { join as join2 } from "path";
|
|
217
|
+
var _cached;
|
|
218
|
+
function resolveBin() {
|
|
219
|
+
const envBin = process.env.SQUADRANT_CMUX_BIN;
|
|
220
|
+
if (envBin && existsSync2(envBin))
|
|
221
|
+
return envBin;
|
|
222
|
+
try {
|
|
223
|
+
const configPath = join2(homedir2(), ".config", "squadrant", "config.json");
|
|
224
|
+
if (existsSync2(configPath)) {
|
|
225
|
+
const cfg = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
226
|
+
const cfgBin = cfg.cmuxBin;
|
|
227
|
+
if (typeof cfgBin === "string" && existsSync2(cfgBin))
|
|
228
|
+
return cfgBin;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const which = execFileSync("which", ["cmux"], { encoding: "utf-8" }).trim();
|
|
234
|
+
if (which && existsSync2(which))
|
|
235
|
+
return which;
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
return "/Applications/cmux.app/Contents/Resources/bin/cmux";
|
|
239
|
+
}
|
|
240
|
+
function resolveCmuxBin() {
|
|
241
|
+
return _cached ??= resolveBin();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// packages/shared/dist/lib/cmux-probe.js
|
|
245
|
+
var DENIED_RE = /access denied|only processes started inside cmux|permission denied/i;
|
|
246
|
+
function classifyProbe(r) {
|
|
247
|
+
if (r.ok)
|
|
248
|
+
return "reachable";
|
|
249
|
+
if (r.stderr && DENIED_RE.test(r.stderr))
|
|
250
|
+
return "denied";
|
|
251
|
+
return "unknown";
|
|
252
|
+
}
|
|
253
|
+
async function probeCmuxDaemonDirect(opts = {}) {
|
|
254
|
+
const run = opts.run ?? (() => orphanProbe(opts.timeoutMs ?? 8e3));
|
|
255
|
+
try {
|
|
256
|
+
return classifyProbe(await run());
|
|
257
|
+
} catch {
|
|
258
|
+
return "unknown";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
var WORKER_SRC = `
|
|
262
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
263
|
+
import { writeFileSync } from "node:fs";
|
|
264
|
+
const [mode, resultFile, cmuxBin] = process.argv.slice(2);
|
|
265
|
+
if (mode === "launch") {
|
|
266
|
+
const child = spawn(process.execPath, [process.argv[1], "work", resultFile, cmuxBin], {
|
|
267
|
+
detached: true, stdio: "ignore",
|
|
268
|
+
});
|
|
269
|
+
child.unref();
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
// work mode: wait to be reparented to launchd (PPID 1), then probe.
|
|
273
|
+
const deadline = Date.now() + 3000;
|
|
274
|
+
while (process.ppid !== 1 && Date.now() < deadline) {
|
|
275
|
+
const until = Date.now() + 25;
|
|
276
|
+
while (Date.now() < until) { /* tiny busy wait \u2014 no timers in a dying orphan */ }
|
|
277
|
+
}
|
|
278
|
+
let result;
|
|
279
|
+
if (process.ppid !== 1) {
|
|
280
|
+
result = { ok: false, stderr: "orphan-timeout" };
|
|
281
|
+
} else {
|
|
282
|
+
try {
|
|
283
|
+
execFileSync(cmuxBin, ["workspace", "list", "--json"], {
|
|
284
|
+
encoding: "utf-8", timeout: 10000, env: { ...process.env, CMUX_QUIET: "1" },
|
|
285
|
+
});
|
|
286
|
+
result = { ok: true };
|
|
287
|
+
} catch (e) {
|
|
288
|
+
const stderr = (e && (e.stderr?.toString?.() || e.message)) || "probe failed";
|
|
289
|
+
result = { ok: false, stderr };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
writeFileSync(resultFile, JSON.stringify(result));
|
|
293
|
+
`;
|
|
294
|
+
async function orphanProbe(timeoutMs) {
|
|
295
|
+
const dir = mkdtempSync(join3(tmpdir(), "cmux-probe-"));
|
|
296
|
+
const scriptFile = join3(dir, "probe-worker.mjs");
|
|
297
|
+
const resultFile = join3(dir, "result.json");
|
|
298
|
+
writeFileSync2(scriptFile, WORKER_SRC);
|
|
299
|
+
try {
|
|
300
|
+
const launcher = spawn(process.execPath, [scriptFile, "launch", resultFile, resolveCmuxBin()], { detached: true, stdio: "ignore" });
|
|
301
|
+
launcher.unref();
|
|
302
|
+
const deadline = Date.now() + timeoutMs;
|
|
303
|
+
while (Date.now() < deadline) {
|
|
304
|
+
if (existsSync3(resultFile)) {
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(readFileSync3(resultFile, "utf-8"));
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
await sleep(100);
|
|
311
|
+
}
|
|
312
|
+
return { ok: false, stderr: "probe-timeout" };
|
|
313
|
+
} finally {
|
|
314
|
+
rmSync(dir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function sleep(ms) {
|
|
318
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// packages/shared/dist/lib/cmux-autoconfig.js
|
|
322
|
+
function defaultStatePath() {
|
|
323
|
+
return join4(homedir3(), ".config", "squadrant", "state", "cmux-autoconfig.json");
|
|
324
|
+
}
|
|
325
|
+
function readState(path13) {
|
|
326
|
+
try {
|
|
327
|
+
return JSON.parse(readFileSync4(path13, "utf-8"));
|
|
328
|
+
} catch {
|
|
329
|
+
return {};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function ensureCmuxAutoConfig(opts = {}) {
|
|
333
|
+
const statePath = opts.statePath ?? defaultStatePath();
|
|
334
|
+
const ensureConfig = opts.ensureConfig ?? ensureSocketAutomation;
|
|
335
|
+
const probe = opts.probe ?? probeCmuxDaemonDirect;
|
|
336
|
+
const cfg = ensureConfig({ path: opts.configPath });
|
|
337
|
+
const verdict = await probe();
|
|
338
|
+
const needsRestart = verdict === "denied";
|
|
339
|
+
let promptedThisRun = false;
|
|
340
|
+
if (needsRestart) {
|
|
341
|
+
const already = readState(statePath).promptedRestart === true;
|
|
342
|
+
if (!already) {
|
|
343
|
+
mkdirSync2(dirname2(statePath), { recursive: true });
|
|
344
|
+
writeFileSync3(statePath, JSON.stringify({ promptedRestart: true }));
|
|
345
|
+
promptedThisRun = true;
|
|
346
|
+
}
|
|
347
|
+
} else if (verdict === "reachable") {
|
|
348
|
+
if (existsSync4(statePath))
|
|
349
|
+
rmSync2(statePath, { force: true });
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
configPath: cfg.path,
|
|
353
|
+
configChanged: cfg.changed,
|
|
354
|
+
configAlreadySet: cfg.alreadySet,
|
|
355
|
+
verdict,
|
|
356
|
+
needsRestart,
|
|
357
|
+
promptedThisRun
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// packages/shared/dist/lib/compat-manifest.js
|
|
362
|
+
var compatManifest = {
|
|
363
|
+
tools: {
|
|
364
|
+
cmux: { min: "0.64.0", lastVerified: "0.64.16" },
|
|
365
|
+
claude: { min: "2.1.32" },
|
|
366
|
+
node: { min: "18.0.0", lastVerified: "24.6.0" },
|
|
367
|
+
// presence-checked; no floor enforced yet
|
|
368
|
+
codex: { lastVerified: "0.139.0" },
|
|
369
|
+
gemini: { lastVerified: "0.38.2" },
|
|
370
|
+
opencode: { lastVerified: "1.17.4" }
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// packages/shared/dist/lib/git-worktree.js
|
|
375
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
376
|
+
import path2 from "path";
|
|
377
|
+
|
|
378
|
+
// packages/shared/dist/lib/resolve-text-input.js
|
|
379
|
+
import fs2 from "fs";
|
|
380
|
+
|
|
381
|
+
// packages/shared/dist/lib/runtime-sync.js
|
|
382
|
+
import fs3 from "fs";
|
|
383
|
+
import path3 from "path";
|
|
384
|
+
|
|
385
|
+
// packages/shared/dist/lib/tool-compat.js
|
|
386
|
+
function parseSemVer(v) {
|
|
387
|
+
const m = v.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
388
|
+
if (!m)
|
|
389
|
+
return null;
|
|
390
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
|
|
391
|
+
}
|
|
392
|
+
function cmpSemVer(a, b) {
|
|
393
|
+
for (let i = 0; i < 3; i++) {
|
|
394
|
+
if (a[i] !== b[i])
|
|
395
|
+
return a[i] - b[i];
|
|
396
|
+
}
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
function checkToolCompat(name, rawVersion, entry) {
|
|
400
|
+
const installed = parseSemVer(rawVersion);
|
|
401
|
+
if (!installed)
|
|
402
|
+
return null;
|
|
403
|
+
const min = entry.min ? parseSemVer(entry.min) : null;
|
|
404
|
+
if (min && cmpSemVer(installed, min) < 0) {
|
|
405
|
+
return `${name} ${rawVersion} < min ${entry.min} \u2014 upgrade to ${entry.min}+`;
|
|
406
|
+
}
|
|
407
|
+
if (entry.lastVerified) {
|
|
408
|
+
const lastVerified = parseSemVer(entry.lastVerified);
|
|
409
|
+
if (lastVerified && cmpSemVer(installed, lastVerified) > 0) {
|
|
410
|
+
return `${name} ${rawVersion} > last-verified ${entry.lastVerified} \u2014 re-run compat audit`;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// packages/shared/dist/lib/canonical-source.js
|
|
417
|
+
import fs4 from "fs";
|
|
418
|
+
import path4 from "path";
|
|
419
|
+
|
|
420
|
+
// packages/shared/dist/lib/daily-logs.js
|
|
421
|
+
import { execSync } from "child_process";
|
|
422
|
+
import fs5 from "fs";
|
|
423
|
+
import path5 from "path";
|
|
424
|
+
import matter from "gray-matter";
|
|
425
|
+
|
|
426
|
+
// packages/core/dist/state-machine.js
|
|
427
|
+
function stampAttempt(rec, patch, now) {
|
|
428
|
+
const attempts = rec.attempts.slice();
|
|
429
|
+
const last = attempts.at(-1) ?? { attemptId: "a0", startedAt: now, lastHeartbeatAt: now };
|
|
430
|
+
attempts[attempts.length === 0 ? 0 : attempts.length - 1] = { ...last, ...patch, lastHeartbeatAt: now };
|
|
431
|
+
if (attempts.length === 0)
|
|
432
|
+
attempts.push(last);
|
|
433
|
+
return { ...rec, attempts };
|
|
434
|
+
}
|
|
435
|
+
function nextPendingTool(current, ev, now) {
|
|
436
|
+
if (ev.note === "agent.hook.PreToolUse")
|
|
437
|
+
return { name: ev.tool ?? "tool", since: now };
|
|
438
|
+
if (ev.note === "posttooluse" || ev.note === "agent.hook.UserPromptSubmit")
|
|
439
|
+
return void 0;
|
|
440
|
+
return current;
|
|
441
|
+
}
|
|
442
|
+
function reduce(rec, ev, now) {
|
|
443
|
+
if (ev.type === "task.reopened") {
|
|
444
|
+
return { ...rec, state: "working", question: void 0, error: void 0, lastHeartbeat: now, lastEvent: ev.type };
|
|
445
|
+
}
|
|
446
|
+
if (TERMINAL_STATES.has(rec.state))
|
|
447
|
+
return rec;
|
|
448
|
+
const base = { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
449
|
+
switch (ev.type) {
|
|
450
|
+
case "task.started":
|
|
451
|
+
return {
|
|
452
|
+
...stampAttempt(base, { pid: ev.pid }, now),
|
|
453
|
+
state: "working",
|
|
454
|
+
pid: ev.pid ?? rec.pid,
|
|
455
|
+
sessionId: ev.sessionId ?? rec.sessionId,
|
|
456
|
+
question: void 0,
|
|
457
|
+
// resuming after a blocked→reply clears the question
|
|
458
|
+
pendingTool: void 0
|
|
459
|
+
// #354: a new turn closes any prior tool window
|
|
460
|
+
};
|
|
461
|
+
case "task.progress": {
|
|
462
|
+
const pendingTool = nextPendingTool(rec.pendingTool, ev, now);
|
|
463
|
+
if (rec.state === "blocked")
|
|
464
|
+
return { ...rec, lastHeartbeat: now, lastEvent: ev.type, pendingTool };
|
|
465
|
+
const b = { ...base, pendingTool };
|
|
466
|
+
if (rec.state === "awaiting-input" || rec.state === "stalled")
|
|
467
|
+
return { ...stampAttempt(b, {}, now), state: "working" };
|
|
468
|
+
return stampAttempt(b, {}, now);
|
|
469
|
+
}
|
|
470
|
+
case "heartbeat":
|
|
471
|
+
if (rec.state === "blocked")
|
|
472
|
+
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
473
|
+
if (rec.state === "awaiting-input")
|
|
474
|
+
return { ...base, state: "working" };
|
|
475
|
+
return base;
|
|
476
|
+
case "task.blocked":
|
|
477
|
+
if (rec.state === "blocked")
|
|
478
|
+
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
479
|
+
return { ...base, state: "blocked", question: ev.question, pendingTool: void 0 };
|
|
480
|
+
case "task.done":
|
|
481
|
+
return { ...base, state: "done", resultRef: ev.resultRef, parseWarning: ev.parseWarning };
|
|
482
|
+
case "task.failed":
|
|
483
|
+
return { ...base, state: "failed", error: ev.error, exitCode: ev.exitCode };
|
|
484
|
+
case "task.cancelled":
|
|
485
|
+
return { ...base, state: "cancelled" };
|
|
486
|
+
case "task.session.ended":
|
|
487
|
+
return { ...base, state: "cancelled" };
|
|
488
|
+
case "task.session":
|
|
489
|
+
return stampAttempt(base, { resumeRef: ev.resumeRef }, now);
|
|
490
|
+
case "task.turn.started":
|
|
491
|
+
return { ...stampAttempt(base, {}, now), state: "working", pendingTool: void 0 };
|
|
492
|
+
case "task.turn.completed":
|
|
493
|
+
if (rec.state === "blocked")
|
|
494
|
+
return { ...rec, lastHeartbeat: now, lastEvent: ev.type };
|
|
495
|
+
return { ...stampAttempt(base, {}, now), state: "awaiting-input", pendingTool: void 0 };
|
|
496
|
+
case "task.delta":
|
|
497
|
+
return stampAttempt(base, {}, now);
|
|
498
|
+
// heartbeat-only
|
|
499
|
+
case "task.input.requested":
|
|
500
|
+
case "task.approval.requested":
|
|
501
|
+
return { ...stampAttempt(base, {}, now), state: "blocked", question: ev.question, pendingTool: void 0 };
|
|
502
|
+
case "task.reattached":
|
|
503
|
+
return stampAttempt(base, {}, now);
|
|
504
|
+
case "task.stalled":
|
|
505
|
+
case "task.idle":
|
|
506
|
+
case "task.quiet":
|
|
507
|
+
case "task.timeout":
|
|
508
|
+
case "task.reconcile-failed":
|
|
509
|
+
return rec;
|
|
510
|
+
default:
|
|
511
|
+
return rec;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// packages/core/dist/watchdog.js
|
|
516
|
+
var TOOL_STALL_BUDGET_MS = 10 * 60 * 1e3;
|
|
517
|
+
function evaluateStall(rec, now, toolStallMs = TOOL_STALL_BUDGET_MS) {
|
|
518
|
+
if (rec.state !== "working")
|
|
519
|
+
return null;
|
|
520
|
+
if (rec.mode === "interactive") {
|
|
521
|
+
if (!rec.pendingTool)
|
|
522
|
+
return null;
|
|
523
|
+
if (now - rec.pendingTool.since <= toolStallMs)
|
|
524
|
+
return null;
|
|
525
|
+
return { ...rec, state: "stalled", lastEvent: "watchdog.tool-stall" };
|
|
526
|
+
}
|
|
527
|
+
const liveness = rec.attempts.at(-1)?.lastHeartbeatAt ?? rec.lastHeartbeat;
|
|
528
|
+
if (now - liveness <= rec.heartbeatBudgetMs)
|
|
529
|
+
return null;
|
|
530
|
+
return { ...rec, state: "stalled", lastEvent: "watchdog.stall" };
|
|
531
|
+
}
|
|
532
|
+
function recoverStall(rec, now) {
|
|
533
|
+
if (rec.state !== "stalled")
|
|
534
|
+
return null;
|
|
535
|
+
return { ...rec, state: "working", lastHeartbeat: now, lastEvent: "watchdog.recover", pendingTool: void 0 };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// packages/core/dist/daemon.js
|
|
539
|
+
var DEFAULT_TASK_TIMEOUT_MS = 8 * 60 * 60 * 1e3;
|
|
540
|
+
var TERMINAL_RECORD_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
541
|
+
var ATTENTION_STATES = /* @__PURE__ */ new Set(["done", "blocked", "failed", "stalled", "awaiting-input"]);
|
|
542
|
+
var REAPABLE_SURFACE_STATES = /* @__PURE__ */ new Set(["working", "stalled", "awaiting-input", "blocked"]);
|
|
543
|
+
var IDLE_DEBOUNCE_MS = 12e3;
|
|
544
|
+
function shortId(id) {
|
|
545
|
+
return id.slice(0, 8);
|
|
546
|
+
}
|
|
547
|
+
function crewTag(r) {
|
|
548
|
+
const suffix = shortId(r.id);
|
|
549
|
+
if (r.name != null) {
|
|
550
|
+
return `[${r.provider}/${r.name} \xB7 ${suffix}]`;
|
|
551
|
+
}
|
|
552
|
+
return `[${r.provider}/${suffix}]`;
|
|
553
|
+
}
|
|
554
|
+
function formatMessage(rec, event) {
|
|
555
|
+
const tag = crewTag(rec);
|
|
556
|
+
switch (rec.state) {
|
|
557
|
+
case "done": {
|
|
558
|
+
const doneMsg = event?.type === "task.done" ? event.message : void 0;
|
|
559
|
+
const body = doneMsg != null && doneMsg.trim().length > 0 ? doneMsg.split(/\r?\n/)[0].trim().slice(0, 200) : (rec.task ?? "").split(/\r?\n/)[0]?.trim().slice(0, 120) ?? "";
|
|
560
|
+
return `CREW DONE ${tag}: ${body}`;
|
|
561
|
+
}
|
|
562
|
+
case "blocked":
|
|
563
|
+
return `CREW BLOCKED ${tag}: ${(rec.question ?? "(no question)").trim()}`;
|
|
564
|
+
case "failed":
|
|
565
|
+
return `CREW FAILED ${tag}: ${(rec.error ?? "(no error)").trim()}`;
|
|
566
|
+
case "stalled": {
|
|
567
|
+
if (event?.type === "task.stalled" && event.tool) {
|
|
568
|
+
const mins = event.elapsedMs != null ? Math.max(1, Math.round(event.elapsedMs / 6e4)) : null;
|
|
569
|
+
return `CREW STALLED ${tag}: still running ${event.tool}${mins != null ? ` ~${mins}min` : ""} \u2014 possibly hung (no result yet).`;
|
|
570
|
+
}
|
|
571
|
+
return `CREW STALLED ${tag}: no heartbeat in ${rec.heartbeatBudgetMs}ms`;
|
|
572
|
+
}
|
|
573
|
+
case "awaiting-input":
|
|
574
|
+
return `CREW IDLE ${tag}: turn ended / awaiting your input \u2014 review and reply or close.`;
|
|
575
|
+
default:
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function formatDelegationReport(rec, originProject, targetProject) {
|
|
580
|
+
const shortTask = (rec.task ?? "").split(/\r?\n/)[0]?.trim().slice(0, 120) ?? "";
|
|
581
|
+
switch (rec.state) {
|
|
582
|
+
case "done":
|
|
583
|
+
return `\u2705 Cross-project task \u2192 ${targetProject}: done \u2014 ${shortTask}`;
|
|
584
|
+
case "blocked":
|
|
585
|
+
return `\u26D4 Cross-project task \u2192 ${targetProject}: blocked \u2014 ${(rec.question ?? "(no question)").trim()}`;
|
|
586
|
+
case "failed":
|
|
587
|
+
return `\u26D4 Cross-project task \u2192 ${targetProject}: failed \u2014 ${(rec.error ?? "(no error)").trim()}`;
|
|
588
|
+
case "stalled":
|
|
589
|
+
return `\u26A0\uFE0F Cross-project task \u2192 ${targetProject}: stalled (no heartbeat in ${rec.heartbeatBudgetMs}ms)`;
|
|
590
|
+
default:
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
function firePush(deps, project, prev, next, event, lastCaptainTurnAt) {
|
|
595
|
+
if (!deps.notify)
|
|
596
|
+
return;
|
|
597
|
+
if (prev === next.state)
|
|
598
|
+
return;
|
|
599
|
+
if (!ATTENTION_STATES.has(next.state))
|
|
600
|
+
return;
|
|
601
|
+
if (next.state === "awaiting-input" && lastCaptainTurnAt != null && deps.now() - lastCaptainTurnAt <= IDLE_DEBOUNCE_MS) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const message = formatMessage(next, event);
|
|
605
|
+
if (!message)
|
|
606
|
+
return;
|
|
607
|
+
try {
|
|
608
|
+
const r = deps.notify({ project, message, record: next, event });
|
|
609
|
+
if (r && typeof r.catch === "function") {
|
|
610
|
+
r.catch(() => {
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
} catch {
|
|
614
|
+
}
|
|
615
|
+
const reportState = next.state === "done" || next.state === "blocked" || next.state === "failed" || next.state === "stalled" || next.state === "cancelled";
|
|
616
|
+
if (next.originProject && next.originProject !== project && reportState) {
|
|
617
|
+
const originMsg = formatDelegationReport(next, next.originProject, project);
|
|
618
|
+
if (originMsg && deps.notify) {
|
|
619
|
+
try {
|
|
620
|
+
const r = deps.notify({ project: next.originProject, message: originMsg, record: next, event });
|
|
621
|
+
if (r && typeof r.catch === "function")
|
|
622
|
+
r.catch(() => {
|
|
623
|
+
});
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
var KNOWN_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
630
|
+
"task.started",
|
|
631
|
+
"task.progress",
|
|
632
|
+
"heartbeat",
|
|
633
|
+
"task.blocked",
|
|
634
|
+
"task.done",
|
|
635
|
+
"task.failed",
|
|
636
|
+
"task.session",
|
|
637
|
+
"task.turn.started",
|
|
638
|
+
"task.turn.completed",
|
|
639
|
+
"task.delta",
|
|
640
|
+
"task.input.requested",
|
|
641
|
+
"task.approval.requested",
|
|
642
|
+
"task.reattached",
|
|
643
|
+
"task.reopened",
|
|
644
|
+
"task.stalled",
|
|
645
|
+
"task.idle",
|
|
646
|
+
"task.quiet",
|
|
647
|
+
"task.timeout",
|
|
648
|
+
"task.reconcile-failed",
|
|
649
|
+
"task.cancelled",
|
|
650
|
+
"task.session.ended"
|
|
651
|
+
]);
|
|
652
|
+
function createDaemon(deps) {
|
|
653
|
+
const { store, now } = deps;
|
|
654
|
+
const lastCaptainTurnAt = /* @__PURE__ */ new Map();
|
|
655
|
+
const quietNotifiedAt = /* @__PURE__ */ new Map();
|
|
656
|
+
return {
|
|
657
|
+
async handle(req) {
|
|
658
|
+
switch (req.kind) {
|
|
659
|
+
case "dispatch": {
|
|
660
|
+
store.put(req.record);
|
|
661
|
+
if (req.record.originProject) {
|
|
662
|
+
const origin = req.record.originProject;
|
|
663
|
+
const msg = `\u{1F4E8} Cross-project task from ${origin}: ${req.record.task}`;
|
|
664
|
+
if (deps.notify) {
|
|
665
|
+
try {
|
|
666
|
+
const r = deps.notify({ project: req.record.project, message: msg, record: req.record, event: { type: "task.started", id: req.record.id } });
|
|
667
|
+
if (r && typeof r.catch === "function")
|
|
668
|
+
r.catch(() => {
|
|
669
|
+
});
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return req.record;
|
|
674
|
+
}
|
|
675
|
+
if (req.record.mode === "headless" && deps.launchHeadless) {
|
|
676
|
+
deps.launchHeadless(req.record).catch((e) => {
|
|
677
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
678
|
+
store.put({ ...req.record, state: "failed", lastEvent: "launch-error", error });
|
|
679
|
+
});
|
|
680
|
+
return req.record;
|
|
681
|
+
}
|
|
682
|
+
if (req.record.mode === "interactive" && deps.launchInteractive) {
|
|
683
|
+
deps.launchInteractive(req.record).catch((e) => {
|
|
684
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
685
|
+
store.put({ ...req.record, state: "failed", lastEvent: "launch-error", error });
|
|
686
|
+
});
|
|
687
|
+
return req.record;
|
|
688
|
+
}
|
|
689
|
+
const failed = {
|
|
690
|
+
...req.record,
|
|
691
|
+
state: "failed",
|
|
692
|
+
lastEvent: "no-launcher",
|
|
693
|
+
error: req.record.mode === "interactive" ? "interactive mode is not yet implemented (deferred interactive-wiring spec); use --mode headless" : `no launcher available for mode '${req.record.mode}'`
|
|
694
|
+
};
|
|
695
|
+
store.put(failed);
|
|
696
|
+
return failed;
|
|
697
|
+
}
|
|
698
|
+
case "event": {
|
|
699
|
+
if (!KNOWN_EVENT_TYPES.has(req.event.type)) {
|
|
700
|
+
throw new Error(`unknown event type '${req.event.type}' \u2014 not a valid ControlEvent`);
|
|
701
|
+
}
|
|
702
|
+
const cur = store.get(req.project, req.event.id);
|
|
703
|
+
if (!cur)
|
|
704
|
+
throw new Error(`unknown task ${req.event.id}`);
|
|
705
|
+
if (req.event.type === "task.started")
|
|
706
|
+
lastCaptainTurnAt.set(req.event.id, now());
|
|
707
|
+
if (req.event.type === "task.session.ended" && !TERMINAL_STATES.has(cur.state)) {
|
|
708
|
+
const liveness = deps.isSurfaceAlive ? await deps.isSurfaceAlive(cur) : "unknown";
|
|
709
|
+
if (liveness !== "gone")
|
|
710
|
+
return cur;
|
|
711
|
+
}
|
|
712
|
+
const next = reduce(cur, req.event, now());
|
|
713
|
+
if (next !== cur) {
|
|
714
|
+
store.put(next);
|
|
715
|
+
firePush(deps, req.project, cur.state, next, req.event, lastCaptainTurnAt.get(next.id));
|
|
716
|
+
}
|
|
717
|
+
return next;
|
|
718
|
+
}
|
|
719
|
+
case "status": {
|
|
720
|
+
const r = store.get(req.project, req.id);
|
|
721
|
+
if (!r)
|
|
722
|
+
throw new Error(`unknown task ${req.id}`);
|
|
723
|
+
return r;
|
|
724
|
+
}
|
|
725
|
+
case "list":
|
|
726
|
+
return store.list(req.project);
|
|
727
|
+
case "reply": {
|
|
728
|
+
const r = store.get(req.project, req.id);
|
|
729
|
+
if (!r)
|
|
730
|
+
throw new Error(`unknown task ${req.id}`);
|
|
731
|
+
if (r.state !== "blocked")
|
|
732
|
+
throw new Error(`task ${req.id} is not blocked (state=${r.state})`);
|
|
733
|
+
lastCaptainTurnAt.set(r.id, now());
|
|
734
|
+
const next = reduce(r, { type: "task.started", id: r.id }, now());
|
|
735
|
+
store.put(next);
|
|
736
|
+
if (deps.deliverReply)
|
|
737
|
+
await deps.deliverReply(r, req.message);
|
|
738
|
+
return next;
|
|
739
|
+
}
|
|
740
|
+
case "gate-resolve": {
|
|
741
|
+
const owning = deps.store.listAll().find((r) => r.gates?.some((g) => g.gateId === req.gateId));
|
|
742
|
+
if (!owning || !owning.gates)
|
|
743
|
+
throw new Error(`gate ${req.gateId} not found`);
|
|
744
|
+
const updatedGates = owning.gates.map((g) => g.gateId === req.gateId ? { ...g, state: "resolved", resolvedBy: req.resolvedBy, resolution: req.payload } : g);
|
|
745
|
+
deps.store.put({ ...owning, gates: updatedGates });
|
|
746
|
+
if (deps.resolveInteractiveGate)
|
|
747
|
+
await deps.resolveInteractiveGate(owning.id, req.payload);
|
|
748
|
+
return { ...owning, gates: updatedGates };
|
|
749
|
+
}
|
|
750
|
+
case "purge": {
|
|
751
|
+
const r = store.get(req.project, req.id);
|
|
752
|
+
if (!r)
|
|
753
|
+
throw new Error(`unknown task ${req.id}`);
|
|
754
|
+
if (!TERMINAL_STATES.has(r.state) && !req.force) {
|
|
755
|
+
throw new Error(`task ${req.id} is not terminal (state=${r.state}); use --force to purge anyway`);
|
|
756
|
+
}
|
|
757
|
+
store.delete(req.project, req.id);
|
|
758
|
+
return r;
|
|
759
|
+
}
|
|
760
|
+
default: {
|
|
761
|
+
const _exhaustive = req;
|
|
762
|
+
throw new Error(`unhandled request kind`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
async sweep() {
|
|
767
|
+
const t = now();
|
|
768
|
+
const surfaceAlive = deps.isSurfaceAlive ?? (async () => "unknown");
|
|
769
|
+
for (const r of store.listAll()) {
|
|
770
|
+
if (TERMINAL_STATES.has(r.state) && t - r.lastHeartbeat > TERMINAL_RECORD_TTL_MS) {
|
|
771
|
+
store.delete(r.project, r.id);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (!TERMINAL_STATES.has(r.state)) {
|
|
775
|
+
const ceiling = deps.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
|
|
776
|
+
if (t - r.createdAt > ceiling) {
|
|
777
|
+
const prevState = r.state;
|
|
778
|
+
const tag = crewTag(r);
|
|
779
|
+
const hrs = Math.round(ceiling / 36e5);
|
|
780
|
+
const msg = `CREW TIMEOUT ${tag}: wall-clock exceeded ${hrs}h (id: ${r.id}, state: ${prevState})`;
|
|
781
|
+
const synthEvent = { type: "task.timeout", id: r.id, taskTimeoutMs: ceiling };
|
|
782
|
+
store.put({ ...r, state: "cancelled", lastEvent: "sweep.task-timeout" });
|
|
783
|
+
if (deps.notify) {
|
|
784
|
+
try {
|
|
785
|
+
const p = deps.notify({ project: r.project, message: msg, record: r, event: synthEvent });
|
|
786
|
+
if (p && typeof p.catch === "function") {
|
|
787
|
+
p.catch(() => {
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (r.mode === "interactive" && REAPABLE_SURFACE_STATES.has(r.state)) {
|
|
797
|
+
const liveness = await surfaceAlive(r);
|
|
798
|
+
if (liveness === "gone") {
|
|
799
|
+
store.put({ ...r, state: "cancelled", lastEvent: "sweep.surface-gone" });
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const idle = evaluateStall(r, t);
|
|
804
|
+
if (idle) {
|
|
805
|
+
store.put(idle);
|
|
806
|
+
const synthEvent = idle.pendingTool ? { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs, tool: idle.pendingTool.name, elapsedMs: t - idle.pendingTool.since } : { type: "task.stalled", id: r.id, heartbeatBudgetMs: r.heartbeatBudgetMs };
|
|
807
|
+
firePush(deps, r.project, r.state, idle, synthEvent, lastCaptainTurnAt.get(r.id));
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (r.mode === "interactive" && r.state === "working" && !r.pendingTool) {
|
|
811
|
+
const liveness = r.attempts.at(-1)?.lastHeartbeatAt ?? r.lastHeartbeat;
|
|
812
|
+
const quiet = t - liveness;
|
|
813
|
+
if (quiet > r.heartbeatBudgetMs) {
|
|
814
|
+
if (deps.notify && quietNotifiedAt.get(r.id) !== liveness) {
|
|
815
|
+
quietNotifiedAt.set(r.id, liveness);
|
|
816
|
+
const tag = crewTag(r);
|
|
817
|
+
const mins = Math.max(1, Math.round(quiet / 6e4));
|
|
818
|
+
const message = `CREW QUIET ${tag}: working ~${mins}min with no tool activity \u2014 likely deep thinking (no reply expected yet).`;
|
|
819
|
+
const synthEvent = { type: "task.quiet", id: r.id, quietMs: quiet };
|
|
820
|
+
try {
|
|
821
|
+
const p = deps.notify({ project: r.project, message, record: r, event: synthEvent });
|
|
822
|
+
if (p && typeof p.catch === "function")
|
|
823
|
+
p.catch(() => {
|
|
824
|
+
});
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (quietNotifiedAt.has(r.id))
|
|
832
|
+
quietNotifiedAt.delete(r.id);
|
|
833
|
+
const recovered = recoverStall(r, t);
|
|
834
|
+
if (recovered && t - r.lastHeartbeat <= r.heartbeatBudgetMs)
|
|
835
|
+
store.put(recovered);
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
async reconcile() {
|
|
839
|
+
const alive = deps.isPidAlive ?? (() => true);
|
|
840
|
+
const surfaceAlive = deps.isSurfaceAlive ?? (async () => "unknown");
|
|
841
|
+
for (const r of store.listAll()) {
|
|
842
|
+
if (r.state !== "working" && r.state !== "submitted")
|
|
843
|
+
continue;
|
|
844
|
+
if (r.mode === "headless") {
|
|
845
|
+
if (r.pid != null && alive(r.pid))
|
|
846
|
+
continue;
|
|
847
|
+
if (deps.isHeadlessInFlight?.(r.id))
|
|
848
|
+
continue;
|
|
849
|
+
const failed = {
|
|
850
|
+
...r,
|
|
851
|
+
state: "failed",
|
|
852
|
+
lastEvent: "reconcile",
|
|
853
|
+
error: "orphaned by daemon restart; exit unobserved (conservative fail)"
|
|
854
|
+
};
|
|
855
|
+
store.put(failed);
|
|
856
|
+
const synthEvent = {
|
|
857
|
+
type: "task.failed",
|
|
858
|
+
id: r.id,
|
|
859
|
+
error: failed.error ?? "reconcile"
|
|
860
|
+
};
|
|
861
|
+
firePush(deps, r.project, r.state, failed, synthEvent, lastCaptainTurnAt.get(r.id));
|
|
862
|
+
} else {
|
|
863
|
+
const liveness = await surfaceAlive(r);
|
|
864
|
+
if (liveness === "gone") {
|
|
865
|
+
store.put({ ...r, state: "cancelled", lastEvent: "reconcile.surface-gone" });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// packages/core/dist/mailbox.js
|
|
874
|
+
import { promises as fs6 } from "fs";
|
|
875
|
+
import { join as join5 } from "path";
|
|
876
|
+
import { randomUUID } from "crypto";
|
|
877
|
+
function inboxDir(stateRoot) {
|
|
878
|
+
return join5(stateRoot, "inbox");
|
|
879
|
+
}
|
|
880
|
+
function logPath(stateRoot, project) {
|
|
881
|
+
return join5(inboxDir(stateRoot), `${project}.log`);
|
|
882
|
+
}
|
|
883
|
+
function extractPayload(event) {
|
|
884
|
+
const { type: _type, id: _id, ...payload } = event;
|
|
885
|
+
return payload;
|
|
886
|
+
}
|
|
887
|
+
async function listRotatedOldestFirst(stateRoot, project) {
|
|
888
|
+
const dir = inboxDir(stateRoot);
|
|
889
|
+
let entries;
|
|
890
|
+
try {
|
|
891
|
+
entries = await fs6.readdir(dir);
|
|
892
|
+
} catch {
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
const prefix = `${project}.log.`;
|
|
896
|
+
return entries.filter((e) => e.startsWith(prefix) && /^\d+$/.test(e.slice(prefix.length))).map((e) => ({ name: e, n: Number(e.slice(prefix.length)) })).sort((a, b) => b.n - a.n).map((e) => join5(dir, e.name));
|
|
897
|
+
}
|
|
898
|
+
async function readMaxSeqFromFile(file) {
|
|
899
|
+
try {
|
|
900
|
+
const buf = await fs6.readFile(file, "utf-8");
|
|
901
|
+
if (!buf.trim())
|
|
902
|
+
return 0;
|
|
903
|
+
const lines = buf.trim().split("\n");
|
|
904
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
905
|
+
try {
|
|
906
|
+
const obj = JSON.parse(lines[i]);
|
|
907
|
+
return obj.seq;
|
|
908
|
+
} catch {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return 0;
|
|
913
|
+
} catch (e) {
|
|
914
|
+
if (e.code === "ENOENT")
|
|
915
|
+
return 0;
|
|
916
|
+
throw e;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
async function readMaxSeq(stateRoot, project) {
|
|
920
|
+
let max = 0;
|
|
921
|
+
const files = [
|
|
922
|
+
logPath(stateRoot, project),
|
|
923
|
+
...await listRotatedOldestFirst(stateRoot, project)
|
|
924
|
+
];
|
|
925
|
+
for (const file of files) {
|
|
926
|
+
const seq = await readMaxSeqFromFile(file);
|
|
927
|
+
if (seq > max)
|
|
928
|
+
max = seq;
|
|
929
|
+
}
|
|
930
|
+
return max;
|
|
931
|
+
}
|
|
932
|
+
var projectLocks = /* @__PURE__ */ new Map();
|
|
933
|
+
function withProjectLock(project, fn) {
|
|
934
|
+
const prev = projectLocks.get(project) ?? Promise.resolve();
|
|
935
|
+
const next = prev.catch(() => void 0).then(fn);
|
|
936
|
+
projectLocks.set(project, next.catch(() => void 0));
|
|
937
|
+
return next;
|
|
938
|
+
}
|
|
939
|
+
async function appendToMailbox(opts) {
|
|
940
|
+
return withProjectLock(opts.project, async () => {
|
|
941
|
+
const dir = inboxDir(opts.stateRoot);
|
|
942
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
943
|
+
const file = logPath(opts.stateRoot, opts.project);
|
|
944
|
+
const lastSeq = await readMaxSeq(opts.stateRoot, opts.project);
|
|
945
|
+
const seq = lastSeq + 1;
|
|
946
|
+
const entry = {
|
|
947
|
+
seq,
|
|
948
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
949
|
+
taskId: opts.taskRecord.id,
|
|
950
|
+
...opts.taskRecord.name !== void 0 ? { name: opts.taskRecord.name } : {},
|
|
951
|
+
kind: opts.event.type,
|
|
952
|
+
provider: opts.taskRecord.provider,
|
|
953
|
+
payload: extractPayload(opts.event),
|
|
954
|
+
message: opts.message ?? null
|
|
955
|
+
};
|
|
956
|
+
await fs6.appendFile(file, JSON.stringify(entry) + "\n", { encoding: "utf-8" });
|
|
957
|
+
return seq;
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
function cursorPath(stateRoot, project, subscriber) {
|
|
961
|
+
return join5(inboxDir(stateRoot), `${project}.${subscriber}.cursor`);
|
|
962
|
+
}
|
|
963
|
+
async function readCursor(opts) {
|
|
964
|
+
let buf;
|
|
965
|
+
try {
|
|
966
|
+
buf = await fs6.readFile(cursorPath(opts.stateRoot, opts.project, opts.subscriber), "utf-8");
|
|
967
|
+
} catch (e) {
|
|
968
|
+
if (e.code === "ENOENT")
|
|
969
|
+
return null;
|
|
970
|
+
throw e;
|
|
971
|
+
}
|
|
972
|
+
if (!buf.trim())
|
|
973
|
+
return null;
|
|
974
|
+
try {
|
|
975
|
+
return JSON.parse(buf);
|
|
976
|
+
} catch {
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async function writeCursor(opts) {
|
|
981
|
+
await fs6.mkdir(inboxDir(opts.stateRoot), { recursive: true });
|
|
982
|
+
const dest = cursorPath(opts.stateRoot, opts.project, opts.subscriber);
|
|
983
|
+
const tmp = `${dest}.${process.pid}.${randomUUID()}.tmp`;
|
|
984
|
+
const data = {
|
|
985
|
+
lastAckedSeq: opts.lastAckedSeq,
|
|
986
|
+
subscriber: opts.subscriber,
|
|
987
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
988
|
+
};
|
|
989
|
+
const handle = await fs6.open(tmp, "w");
|
|
990
|
+
try {
|
|
991
|
+
await handle.writeFile(JSON.stringify(data), { encoding: "utf-8" });
|
|
992
|
+
await handle.sync();
|
|
993
|
+
} finally {
|
|
994
|
+
await handle.close();
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
await fs6.rename(tmp, dest);
|
|
998
|
+
} catch (e) {
|
|
999
|
+
await fs6.unlink(tmp).catch(() => {
|
|
1000
|
+
});
|
|
1001
|
+
throw e;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
async function* readFromCursor(opts) {
|
|
1005
|
+
const rotated = await listRotatedOldestFirst(opts.stateRoot, opts.project);
|
|
1006
|
+
const files = [...rotated, logPath(opts.stateRoot, opts.project)];
|
|
1007
|
+
for (const file of files) {
|
|
1008
|
+
let buf;
|
|
1009
|
+
try {
|
|
1010
|
+
buf = await fs6.readFile(file, "utf-8");
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
if (e.code === "ENOENT")
|
|
1013
|
+
continue;
|
|
1014
|
+
throw e;
|
|
1015
|
+
}
|
|
1016
|
+
for (const line of buf.split("\n")) {
|
|
1017
|
+
if (!line.trim())
|
|
1018
|
+
continue;
|
|
1019
|
+
let entry;
|
|
1020
|
+
try {
|
|
1021
|
+
entry = JSON.parse(line);
|
|
1022
|
+
} catch {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (entry.seq >= opts.fromSeq)
|
|
1026
|
+
yield entry;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
async function mailboxStats(stateRoot, project) {
|
|
1031
|
+
const file = logPath(stateRoot, project);
|
|
1032
|
+
let sizeBytes = 0;
|
|
1033
|
+
try {
|
|
1034
|
+
sizeBytes = (await fs6.stat(file)).size;
|
|
1035
|
+
} catch (e) {
|
|
1036
|
+
if (e.code !== "ENOENT")
|
|
1037
|
+
throw e;
|
|
1038
|
+
}
|
|
1039
|
+
const rotated = await listRotatedOldestFirst(stateRoot, project);
|
|
1040
|
+
return {
|
|
1041
|
+
maxSeq: await readMaxSeq(stateRoot, project),
|
|
1042
|
+
sizeBytes,
|
|
1043
|
+
oldestEntryAgeMs: await oldestEntryAgeMs(file),
|
|
1044
|
+
rotationCount: rotated.length
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
async function oldestEntryAgeMs(file) {
|
|
1048
|
+
try {
|
|
1049
|
+
const buf = await fs6.readFile(file, "utf-8");
|
|
1050
|
+
const firstLine = buf.split("\n").find((l) => l.trim());
|
|
1051
|
+
if (!firstLine)
|
|
1052
|
+
return 0;
|
|
1053
|
+
const entry = JSON.parse(firstLine);
|
|
1054
|
+
return Date.now() - new Date(entry.ts).getTime();
|
|
1055
|
+
} catch {
|
|
1056
|
+
return 0;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function rotateIfNeeded(opts) {
|
|
1060
|
+
return withProjectLock(opts.project, async () => {
|
|
1061
|
+
const file = logPath(opts.stateRoot, opts.project);
|
|
1062
|
+
let size = 0;
|
|
1063
|
+
try {
|
|
1064
|
+
size = (await fs6.stat(file)).size;
|
|
1065
|
+
} catch (e) {
|
|
1066
|
+
if (e.code === "ENOENT")
|
|
1067
|
+
return { rotated: false };
|
|
1068
|
+
throw e;
|
|
1069
|
+
}
|
|
1070
|
+
const age = await oldestEntryAgeMs(file);
|
|
1071
|
+
if (size < opts.maxBytes && age < opts.maxAgeMs)
|
|
1072
|
+
return { rotated: false };
|
|
1073
|
+
const existing = await listRotatedOldestFirst(opts.stateRoot, opts.project);
|
|
1074
|
+
const nums = existing.map((p) => Number(p.slice(p.lastIndexOf(".") + 1))).sort((a, b) => b - a);
|
|
1075
|
+
for (const n of nums) {
|
|
1076
|
+
const src = `${file}.${n}`;
|
|
1077
|
+
const dst = `${file}.${n + 1}`;
|
|
1078
|
+
if (n + 1 > opts.keepCount) {
|
|
1079
|
+
try {
|
|
1080
|
+
await fs6.unlink(src);
|
|
1081
|
+
} catch (e) {
|
|
1082
|
+
if (e.code !== "ENOENT")
|
|
1083
|
+
throw e;
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
try {
|
|
1087
|
+
await fs6.rename(src, dst);
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
if (e.code !== "ENOENT")
|
|
1090
|
+
throw e;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
await fs6.rename(file, `${file}.1`);
|
|
1095
|
+
await fs6.writeFile(file, "", { encoding: "utf-8" });
|
|
1096
|
+
return { rotated: true, from: file, to: `${file}.1` };
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// packages/core/dist/protocol.js
|
|
1101
|
+
import { createServer, createConnection } from "net";
|
|
1102
|
+
import { existsSync as existsSync5, unlinkSync } from "fs";
|
|
1103
|
+
var PROTOCOL_VERSION = 1;
|
|
1104
|
+
function encodeMsg(obj) {
|
|
1105
|
+
return JSON.stringify(obj) + "\n";
|
|
1106
|
+
}
|
|
1107
|
+
function createDecoder(onParseError) {
|
|
1108
|
+
let buf = "";
|
|
1109
|
+
return {
|
|
1110
|
+
push(chunk) {
|
|
1111
|
+
buf += chunk;
|
|
1112
|
+
const out = [];
|
|
1113
|
+
let idx;
|
|
1114
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
1115
|
+
const line = buf.slice(0, idx);
|
|
1116
|
+
buf = buf.slice(idx + 1);
|
|
1117
|
+
if (!line.trim())
|
|
1118
|
+
continue;
|
|
1119
|
+
try {
|
|
1120
|
+
const parsed = JSON.parse(line);
|
|
1121
|
+
if (typeof parsed === "object" && parsed !== null && parsed.type === "_keepalive")
|
|
1122
|
+
continue;
|
|
1123
|
+
out.push(parsed);
|
|
1124
|
+
} catch {
|
|
1125
|
+
onParseError?.(line);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return out;
|
|
1129
|
+
},
|
|
1130
|
+
// #87: exposes the unprocessed buffer content — bytes received but not yet
|
|
1131
|
+
// terminated with a newline. The server checks this on connection end to
|
|
1132
|
+
// detect newline-less input and reply with a fast structured error.
|
|
1133
|
+
remainder() {
|
|
1134
|
+
return buf;
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function defaultListenError(e) {
|
|
1139
|
+
process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} server error: ${e.message}
|
|
1140
|
+
`);
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
function startServer(sockPath, handlerOrCallbacks, onListenError = defaultListenError, deps = {}) {
|
|
1144
|
+
const callbacks = typeof handlerOrCallbacks === "function" ? { handler: handlerOrCallbacks } : handlerOrCallbacks;
|
|
1145
|
+
const { handler, onAttach, onAttachInbound, onAttachClose } = callbacks;
|
|
1146
|
+
const setIntervalFn = deps.setInterval ?? setInterval;
|
|
1147
|
+
const clearIntervalFn = deps.clearInterval ?? clearInterval;
|
|
1148
|
+
if (existsSync5(sockPath)) {
|
|
1149
|
+
try {
|
|
1150
|
+
unlinkSync(sockPath);
|
|
1151
|
+
} catch {
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const server = createServer((conn) => {
|
|
1155
|
+
conn.setEncoding("utf-8");
|
|
1156
|
+
let claimType = "none";
|
|
1157
|
+
let keepaliveId;
|
|
1158
|
+
const dec = createDecoder((badLine) => {
|
|
1159
|
+
if (claimType !== "attach") {
|
|
1160
|
+
try {
|
|
1161
|
+
conn.write(encodeMsg({ ok: false, error: `malformed request: invalid JSON`, _v: PROTOCOL_VERSION }));
|
|
1162
|
+
} catch {
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
conn.on("data", async (chunk) => {
|
|
1167
|
+
for (const msg of dec.push(chunk)) {
|
|
1168
|
+
if (claimType === "attach") {
|
|
1169
|
+
onAttachInbound?.(conn, msg);
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
if (onAttach && msg != null && typeof msg === "object" && msg.op === "attach" && typeof msg.taskId === "string") {
|
|
1173
|
+
claimType = "attach";
|
|
1174
|
+
onAttach(conn, msg);
|
|
1175
|
+
keepaliveId = setIntervalFn(() => {
|
|
1176
|
+
try {
|
|
1177
|
+
conn.write(encodeFrame({ type: "_keepalive" }));
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
}, 1e4);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
try {
|
|
1184
|
+
const reply = await handler(msg);
|
|
1185
|
+
try {
|
|
1186
|
+
conn.write(encodeMsg({ ok: true, reply, _v: PROTOCOL_VERSION }));
|
|
1187
|
+
} catch {
|
|
1188
|
+
}
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
1191
|
+
try {
|
|
1192
|
+
conn.write(encodeMsg({ ok: false, error: errMsg, _v: PROTOCOL_VERSION }));
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
conn.on("error", () => {
|
|
1199
|
+
});
|
|
1200
|
+
conn.on("end", () => {
|
|
1201
|
+
if (claimType !== "attach" && dec.remainder().trim()) {
|
|
1202
|
+
try {
|
|
1203
|
+
conn.write(encodeMsg({ ok: false, error: `malformed request: missing newline terminator`, _v: PROTOCOL_VERSION }));
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
conn.on("close", () => {
|
|
1209
|
+
if (keepaliveId !== void 0)
|
|
1210
|
+
clearIntervalFn(keepaliveId);
|
|
1211
|
+
if (claimType === "attach")
|
|
1212
|
+
onAttachClose?.(conn);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
server.on("error", onListenError);
|
|
1216
|
+
server.listen(sockPath);
|
|
1217
|
+
return server;
|
|
1218
|
+
}
|
|
1219
|
+
function isDaemonSocketLive(sockPath, timeoutMs = 500) {
|
|
1220
|
+
return new Promise((resolve2) => {
|
|
1221
|
+
if (!existsSync5(sockPath)) {
|
|
1222
|
+
resolve2(false);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const conn = createConnection(sockPath);
|
|
1226
|
+
const finish = (v) => {
|
|
1227
|
+
try {
|
|
1228
|
+
conn.destroy();
|
|
1229
|
+
} catch {
|
|
1230
|
+
}
|
|
1231
|
+
resolve2(v);
|
|
1232
|
+
};
|
|
1233
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
1234
|
+
conn.on("connect", () => {
|
|
1235
|
+
clearTimeout(timer);
|
|
1236
|
+
finish(true);
|
|
1237
|
+
});
|
|
1238
|
+
conn.on("error", () => {
|
|
1239
|
+
clearTimeout(timer);
|
|
1240
|
+
finish(false);
|
|
1241
|
+
});
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
function encodeFrame(f) {
|
|
1245
|
+
return JSON.stringify(f) + "\n";
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// packages/core/dist/liveness.js
|
|
1249
|
+
var CREW_STALE_MS = 5 * 6e4;
|
|
1250
|
+
var CREW_GONE_MS = 30 * 6e4;
|
|
1251
|
+
function classifyHealth(lastSeenMs, now, staleMs, goneMs) {
|
|
1252
|
+
if (lastSeenMs == null)
|
|
1253
|
+
return "unknown";
|
|
1254
|
+
const age = now - lastSeenMs;
|
|
1255
|
+
if (age <= staleMs)
|
|
1256
|
+
return "alive";
|
|
1257
|
+
if (age <= goneMs)
|
|
1258
|
+
return "stale";
|
|
1259
|
+
return "gone";
|
|
1260
|
+
}
|
|
1261
|
+
var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "cancelled"]);
|
|
1262
|
+
function projectHealth(input) {
|
|
1263
|
+
const { project, now, captainName, captainStopped, commandPresent, crews } = input;
|
|
1264
|
+
const out = [];
|
|
1265
|
+
const captainState = captainStopped === true ? "stopped" : captainStopped === false ? "alive" : "unknown";
|
|
1266
|
+
out.push({
|
|
1267
|
+
kind: "captain",
|
|
1268
|
+
project,
|
|
1269
|
+
ref: captainName,
|
|
1270
|
+
state: captainState,
|
|
1271
|
+
lastSeenMs: null,
|
|
1272
|
+
detail: captainState === "stopped" ? "captain workspace closed \u2014 crews reaped; delivery paused" : void 0
|
|
1273
|
+
});
|
|
1274
|
+
if (commandPresent !== null) {
|
|
1275
|
+
out.push({
|
|
1276
|
+
kind: "command",
|
|
1277
|
+
project,
|
|
1278
|
+
ref: "command",
|
|
1279
|
+
state: presence(commandPresent),
|
|
1280
|
+
lastSeenMs: null
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
for (const c of crews) {
|
|
1284
|
+
if (TERMINAL.has(c.state))
|
|
1285
|
+
continue;
|
|
1286
|
+
out.push({
|
|
1287
|
+
kind: "crew",
|
|
1288
|
+
project,
|
|
1289
|
+
ref: c.name ?? c.id.slice(0, 8),
|
|
1290
|
+
state: classifyHealth(c.lastHeartbeat, now, CREW_STALE_MS, CREW_GONE_MS),
|
|
1291
|
+
lastSeenMs: c.lastHeartbeat,
|
|
1292
|
+
detail: c.state
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
return out;
|
|
1296
|
+
}
|
|
1297
|
+
function presence(p) {
|
|
1298
|
+
if (p === null)
|
|
1299
|
+
return "unknown";
|
|
1300
|
+
return p ? "alive" : "gone";
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// packages/core/dist/store.js
|
|
1304
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync, renameSync, writeFileSync as writeFileSync4, existsSync as existsSync6, rmSync as rmSync3, statSync } from "fs";
|
|
1305
|
+
import { join as join6, resolve, sep } from "path";
|
|
1306
|
+
function safeSegment(kind, s) {
|
|
1307
|
+
if (typeof s !== "string" || s.length === 0) {
|
|
1308
|
+
throw new Error(`invalid ${kind}: must be a non-empty string`);
|
|
1309
|
+
}
|
|
1310
|
+
if (s.includes("\0"))
|
|
1311
|
+
throw new Error(`invalid ${kind}: NUL byte not allowed`);
|
|
1312
|
+
if (s === "." || s === ".." || /[/\\]/.test(s)) {
|
|
1313
|
+
throw new Error(`invalid ${kind}: '${s}' \u2014 path separators/traversal not allowed`);
|
|
1314
|
+
}
|
|
1315
|
+
return s;
|
|
1316
|
+
}
|
|
1317
|
+
function createStore(root) {
|
|
1318
|
+
const rootResolved = resolve(root);
|
|
1319
|
+
const assertUnderRoot = (target) => {
|
|
1320
|
+
const r = resolve(target);
|
|
1321
|
+
if (r !== rootResolved && !r.startsWith(rootResolved + sep)) {
|
|
1322
|
+
throw new Error(`path escapes state root: ${target}`);
|
|
1323
|
+
}
|
|
1324
|
+
return target;
|
|
1325
|
+
};
|
|
1326
|
+
const projDir = (p) => assertUnderRoot(join6(root, safeSegment("project", p)));
|
|
1327
|
+
const taskFile = (p, id) => assertUnderRoot(join6(projDir(p), `${safeSegment("id", id)}.json`));
|
|
1328
|
+
return {
|
|
1329
|
+
put(rec) {
|
|
1330
|
+
mkdirSync3(projDir(rec.project), { recursive: true });
|
|
1331
|
+
const dest = taskFile(rec.project, rec.id);
|
|
1332
|
+
const tmp = `${dest}.tmp`;
|
|
1333
|
+
writeFileSync4(tmp, JSON.stringify(rec, null, 2));
|
|
1334
|
+
renameSync(tmp, dest);
|
|
1335
|
+
},
|
|
1336
|
+
get(project, id) {
|
|
1337
|
+
const f = taskFile(project, id);
|
|
1338
|
+
if (!existsSync6(f))
|
|
1339
|
+
return void 0;
|
|
1340
|
+
try {
|
|
1341
|
+
return JSON.parse(readFileSync5(f, "utf-8"));
|
|
1342
|
+
} catch {
|
|
1343
|
+
return void 0;
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
list(project) {
|
|
1347
|
+
const d = projDir(project);
|
|
1348
|
+
if (!existsSync6(d))
|
|
1349
|
+
return [];
|
|
1350
|
+
return readdirSync(d).filter((n) => n.endsWith(".json")).map((n) => {
|
|
1351
|
+
try {
|
|
1352
|
+
return JSON.parse(readFileSync5(join6(d, n), "utf-8"));
|
|
1353
|
+
} catch {
|
|
1354
|
+
return void 0;
|
|
1355
|
+
}
|
|
1356
|
+
}).filter((r) => r !== void 0);
|
|
1357
|
+
},
|
|
1358
|
+
listAll() {
|
|
1359
|
+
if (!existsSync6(root))
|
|
1360
|
+
return [];
|
|
1361
|
+
return readdirSync(root).filter((p) => {
|
|
1362
|
+
try {
|
|
1363
|
+
return statSync(join6(root, p)).isDirectory();
|
|
1364
|
+
} catch {
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1367
|
+
}).flatMap((p) => this.list(p));
|
|
1368
|
+
},
|
|
1369
|
+
quarantine(project, id) {
|
|
1370
|
+
const f = taskFile(project, id);
|
|
1371
|
+
if (existsSync6(f))
|
|
1372
|
+
renameSync(f, `${f}.corrupt.${Date.now()}`);
|
|
1373
|
+
},
|
|
1374
|
+
delete(project, id) {
|
|
1375
|
+
const f = taskFile(project, id);
|
|
1376
|
+
if (existsSync6(f))
|
|
1377
|
+
rmSync3(f);
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// packages/core/dist/index.js
|
|
1383
|
+
init_snapshot();
|
|
1384
|
+
|
|
1385
|
+
// packages/core/dist/launchd.js
|
|
1386
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1387
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync7, openSync, writeSync, closeSync, unlinkSync as unlinkSync2, constants } from "fs";
|
|
1388
|
+
import { homedir as homedir4 } from "os";
|
|
1389
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
1390
|
+
import { fileURLToPath } from "url";
|
|
1391
|
+
|
|
1392
|
+
// packages/core/dist/crew-pane-reader.js
|
|
1393
|
+
var TAIL_LINES = 25;
|
|
1394
|
+
function crewPaneTitle(project, name) {
|
|
1395
|
+
return `\u{1F527} ${project}:${name}`;
|
|
1396
|
+
}
|
|
1397
|
+
function surfaceVerdict(surfaceTitles, wantTitle) {
|
|
1398
|
+
if (!wantTitle)
|
|
1399
|
+
return "unknown";
|
|
1400
|
+
if (surfaceTitles == null)
|
|
1401
|
+
return "unknown";
|
|
1402
|
+
return surfaceTitles.includes(wantTitle) ? "alive" : "gone";
|
|
1403
|
+
}
|
|
1404
|
+
function createDirectSurfaceLivenessProbe(cmux2, getCaptainTitle) {
|
|
1405
|
+
return async (rec) => {
|
|
1406
|
+
try {
|
|
1407
|
+
if (rec.mode !== "interactive" || !rec.name)
|
|
1408
|
+
return "unknown";
|
|
1409
|
+
const wsId = await cmux2.findWorkspaceId(getCaptainTitle(rec.project));
|
|
1410
|
+
if (!wsId)
|
|
1411
|
+
return "unknown";
|
|
1412
|
+
const surfaces = await cmux2.listSurfaces(wsId);
|
|
1413
|
+
if (surfaces.length === 0)
|
|
1414
|
+
return "unknown";
|
|
1415
|
+
return surfaceVerdict(surfaces.map((s) => s.title ?? ""), crewPaneTitle(rec.project, rec.name));
|
|
1416
|
+
} catch {
|
|
1417
|
+
return "unknown";
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function createDirectCrewPaneReader(cmux2, getCaptainTitle) {
|
|
1422
|
+
return async (rec) => {
|
|
1423
|
+
try {
|
|
1424
|
+
if (!rec.name)
|
|
1425
|
+
return null;
|
|
1426
|
+
const wsId = await cmux2.findWorkspaceId(getCaptainTitle(rec.project));
|
|
1427
|
+
if (!wsId)
|
|
1428
|
+
return null;
|
|
1429
|
+
const surfaces = await cmux2.listSurfaces(wsId);
|
|
1430
|
+
const want = crewPaneTitle(rec.project, rec.name);
|
|
1431
|
+
const pane = surfaces.find((s) => s.title === want);
|
|
1432
|
+
if (!pane)
|
|
1433
|
+
return null;
|
|
1434
|
+
const screen = await cmux2.readPaneScreen(pane);
|
|
1435
|
+
if (!screen)
|
|
1436
|
+
return null;
|
|
1437
|
+
return screen.split(/\r?\n/).slice(-TAIL_LINES).join("\n");
|
|
1438
|
+
} catch {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// packages/core/dist/gate.js
|
|
1445
|
+
function makeGate(opts) {
|
|
1446
|
+
return {
|
|
1447
|
+
gateId: opts.mkId(),
|
|
1448
|
+
taskId: opts.taskId,
|
|
1449
|
+
kind: opts.kind,
|
|
1450
|
+
question: opts.question,
|
|
1451
|
+
state: "pending",
|
|
1452
|
+
createdAt: opts.now
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// packages/core/dist/daemon/context.js
|
|
1457
|
+
import { homedir as homedir5 } from "os";
|
|
1458
|
+
import { join as join8 } from "path";
|
|
1459
|
+
import { spawn as realSpawn } from "child_process";
|
|
1460
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
1461
|
+
function defaultIsPidAlive(pid) {
|
|
1462
|
+
try {
|
|
1463
|
+
process.kill(pid, 0);
|
|
1464
|
+
return true;
|
|
1465
|
+
} catch (e) {
|
|
1466
|
+
return e?.code === "EPERM";
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
function buildContext(opts) {
|
|
1470
|
+
const stateRoot = opts.stateRoot ?? join8(homedir5(), ".config", "squadrant", "state");
|
|
1471
|
+
const sockPath = opts.sockPath ?? join8(homedir5(), ".config", "squadrant", "squadrant.sock");
|
|
1472
|
+
const store = createStore(stateRoot);
|
|
1473
|
+
const bootedAt = Date.now();
|
|
1474
|
+
const taskTimeoutMs = loadConfig().defaults.taskTimeoutMs;
|
|
1475
|
+
const isPidAlive = opts.isPidAlive ?? defaultIsPidAlive;
|
|
1476
|
+
const spawn2 = opts.spawn ?? realSpawn;
|
|
1477
|
+
const resultsDir = join8(stateRoot, "_results");
|
|
1478
|
+
mkdirSync5(resultsDir, { recursive: true });
|
|
1479
|
+
const writeResult = (id, payload) => {
|
|
1480
|
+
const p = join8(resultsDir, `${id}.txt`);
|
|
1481
|
+
writeFileSync6(p, payload);
|
|
1482
|
+
return p;
|
|
1483
|
+
};
|
|
1484
|
+
const log = (m) => process.stderr.write(`[squadrantd] ${(/* @__PURE__ */ new Date()).toISOString()} ${m}
|
|
1485
|
+
`);
|
|
1486
|
+
return {
|
|
1487
|
+
opts,
|
|
1488
|
+
stateRoot,
|
|
1489
|
+
sockPath,
|
|
1490
|
+
store,
|
|
1491
|
+
bootedAt,
|
|
1492
|
+
lastSweepAt: { value: null },
|
|
1493
|
+
taskTimeoutMs,
|
|
1494
|
+
isPidAlive,
|
|
1495
|
+
spawn: spawn2,
|
|
1496
|
+
resultsDir,
|
|
1497
|
+
writeResult,
|
|
1498
|
+
log,
|
|
1499
|
+
attachConns: /* @__PURE__ */ new Map(),
|
|
1500
|
+
inFlightHeadlessIds: /* @__PURE__ */ new Set(),
|
|
1501
|
+
activeHeadlessKills: /* @__PURE__ */ new Set(),
|
|
1502
|
+
captainMissingStreak: /* @__PURE__ */ new Map(),
|
|
1503
|
+
stoppedProjects: /* @__PURE__ */ new Set(),
|
|
1504
|
+
// Late-bound — start.ts fills these before first use:
|
|
1505
|
+
d: null,
|
|
1506
|
+
notify: null,
|
|
1507
|
+
daemonCmux: void 0,
|
|
1508
|
+
codexDriver: null,
|
|
1509
|
+
opencodeBridge: null,
|
|
1510
|
+
cmuxEventsBridge: null,
|
|
1511
|
+
broadcast: () => {
|
|
1512
|
+
},
|
|
1513
|
+
schedulePromotion: () => {
|
|
1514
|
+
},
|
|
1515
|
+
cancelPromotionsFor: () => {
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// packages/core/dist/daemon/attach.js
|
|
1521
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1522
|
+
function createAttach(ctx) {
|
|
1523
|
+
const { attachConns, store, log } = ctx;
|
|
1524
|
+
function broadcast(taskId, f) {
|
|
1525
|
+
const conns = attachConns.get(taskId);
|
|
1526
|
+
if (!conns)
|
|
1527
|
+
return;
|
|
1528
|
+
const wire = encodeFrame(f);
|
|
1529
|
+
for (const conn of conns) {
|
|
1530
|
+
try {
|
|
1531
|
+
conn.write(wire);
|
|
1532
|
+
} catch {
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const pendingGateTimers = /* @__PURE__ */ new Map();
|
|
1537
|
+
function schedulePromotion(taskId, requestId, kind, question) {
|
|
1538
|
+
const conns = attachConns.get(taskId);
|
|
1539
|
+
if (conns && conns.size > 0)
|
|
1540
|
+
return;
|
|
1541
|
+
const key = `${taskId}#${requestId}`;
|
|
1542
|
+
const prior = pendingGateTimers.get(key);
|
|
1543
|
+
if (prior)
|
|
1544
|
+
clearTimeout(prior.timer);
|
|
1545
|
+
const timer = setTimeout(() => {
|
|
1546
|
+
pendingGateTimers.delete(key);
|
|
1547
|
+
if (attachConns.get(taskId)?.size)
|
|
1548
|
+
return;
|
|
1549
|
+
const rec = store.listAll().find((r) => r.id === taskId);
|
|
1550
|
+
if (!rec)
|
|
1551
|
+
return;
|
|
1552
|
+
const gate = makeGate({ taskId, kind, question, now: Date.now(), mkId: () => randomUUID2() });
|
|
1553
|
+
const gates = [...rec.gates ?? [], gate];
|
|
1554
|
+
store.put({ ...rec, gates });
|
|
1555
|
+
broadcast(taskId, { type: "gate-promoted", taskId, gateId: gate.gateId });
|
|
1556
|
+
log(`gate promoted gateId=${gate.gateId} taskId=${taskId} kind=${kind}`);
|
|
1557
|
+
}, 5e3);
|
|
1558
|
+
timer.unref?.();
|
|
1559
|
+
pendingGateTimers.set(key, { taskId, timer });
|
|
1560
|
+
}
|
|
1561
|
+
function cancelPromotionsFor(taskId) {
|
|
1562
|
+
for (const [key, slot] of pendingGateTimers.entries()) {
|
|
1563
|
+
if (slot.taskId === taskId) {
|
|
1564
|
+
clearTimeout(slot.timer);
|
|
1565
|
+
pendingGateTimers.delete(key);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return { broadcast, schedulePromotion, cancelPromotionsFor };
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// packages/core/dist/daemon/start.js
|
|
1573
|
+
import { join as join10, dirname as dirname4 } from "path";
|
|
1574
|
+
import { readdir } from "fs/promises";
|
|
1575
|
+
|
|
1576
|
+
// packages/core/dist/daemon/interactive-probe.js
|
|
1577
|
+
var STALE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
1578
|
+
var PROBE_QUIET_MS = 2e4;
|
|
1579
|
+
function detectTrailingQuestion(text) {
|
|
1580
|
+
if (!text)
|
|
1581
|
+
return null;
|
|
1582
|
+
let inFence = false;
|
|
1583
|
+
let lastLine = null;
|
|
1584
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
1585
|
+
const line = raw.trim();
|
|
1586
|
+
if (line.startsWith("```")) {
|
|
1587
|
+
inFence = !inFence;
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (inFence || line === "")
|
|
1591
|
+
continue;
|
|
1592
|
+
lastLine = line;
|
|
1593
|
+
}
|
|
1594
|
+
if (lastLine && lastLine.endsWith("?"))
|
|
1595
|
+
return lastLine;
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
var ERROR_BANNER_RE = [
|
|
1599
|
+
/\bAPI Error\b/i,
|
|
1600
|
+
/\bOverloaded\b/,
|
|
1601
|
+
/\b(?:429|500|502|503|504|529)\b[^?]*\b(?:overloaded|unavailable|internal server error|bad gateway|gateway timeout|too many requests|service unavailable)\b/i,
|
|
1602
|
+
/\bretr(?:y|ies)\s+(?:exhausted|limit\s+(?:reached|exceeded))\b/i,
|
|
1603
|
+
/\bmaximum\s+retries\b/i
|
|
1604
|
+
];
|
|
1605
|
+
var OPTION_RE = /^[❯>›]?\s*(\d+)\.\s+(.*\S)\s*$/;
|
|
1606
|
+
var PURE_CHROME_RE = /^[\s─━│┃╭╮╰╯┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬▁▂▃▄▅▆▇█▔▏▕]+$/;
|
|
1607
|
+
var STATUS_LINE_RE = /accept edits on|shift\+tab|⏵⏵|\? for shortcuts|esc to interrupt|tokens? (used|left)|context left/i;
|
|
1608
|
+
function stripChrome(raw) {
|
|
1609
|
+
let line = raw.replace(/\[[0-9;]*m/g, "");
|
|
1610
|
+
line = line.replace(/^[\s│┃▏▕|]+/, "").replace(/[\s│┃▏▕|]+$/, "");
|
|
1611
|
+
const trimmed = line.trim();
|
|
1612
|
+
if (trimmed === "")
|
|
1613
|
+
return null;
|
|
1614
|
+
if (PURE_CHROME_RE.test(trimmed))
|
|
1615
|
+
return null;
|
|
1616
|
+
if (/^>\s*$/.test(trimmed))
|
|
1617
|
+
return null;
|
|
1618
|
+
if (STATUS_LINE_RE.test(trimmed))
|
|
1619
|
+
return null;
|
|
1620
|
+
return trimmed;
|
|
1621
|
+
}
|
|
1622
|
+
function classifyPaneTail(tail) {
|
|
1623
|
+
if (!tail)
|
|
1624
|
+
return null;
|
|
1625
|
+
const raw = tail.split(/\r?\n/);
|
|
1626
|
+
const cleaned = raw.map(stripChrome);
|
|
1627
|
+
const options = [];
|
|
1628
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
1629
|
+
const c = cleaned[i];
|
|
1630
|
+
if (c == null)
|
|
1631
|
+
continue;
|
|
1632
|
+
const m = c.match(OPTION_RE);
|
|
1633
|
+
if (m)
|
|
1634
|
+
options.push({ label: m[2], ci: i });
|
|
1635
|
+
}
|
|
1636
|
+
const hasYes = options.some((o) => /\byes\b/i.test(o.label));
|
|
1637
|
+
const hasNo = options.some((o) => /\bno\b/i.test(o.label));
|
|
1638
|
+
if (options.length >= 2 && hasYes && hasNo) {
|
|
1639
|
+
const firstOptCi = options[0].ci;
|
|
1640
|
+
for (let i = firstOptCi - 1; i >= 0; i--) {
|
|
1641
|
+
const c = cleaned[i];
|
|
1642
|
+
if (c == null)
|
|
1643
|
+
continue;
|
|
1644
|
+
if (c.endsWith("?"))
|
|
1645
|
+
return { kind: "approval", text: c };
|
|
1646
|
+
}
|
|
1647
|
+
return { kind: "approval", text: "Crew is awaiting permission approval." };
|
|
1648
|
+
}
|
|
1649
|
+
const region = cleaned.filter((c) => c != null).join("\n");
|
|
1650
|
+
const q = detectTrailingQuestion(region);
|
|
1651
|
+
if (q)
|
|
1652
|
+
return { kind: "question", text: q };
|
|
1653
|
+
let errLine = null;
|
|
1654
|
+
for (const c of cleaned) {
|
|
1655
|
+
if (c != null && ERROR_BANNER_RE.some((re) => re.test(c)))
|
|
1656
|
+
errLine = c;
|
|
1657
|
+
}
|
|
1658
|
+
if (errLine)
|
|
1659
|
+
return { kind: "error", text: errLine.slice(0, 200) };
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
function createInteractiveProbe(deps) {
|
|
1663
|
+
const quietMs = deps.quietMs ?? PROBE_QUIET_MS;
|
|
1664
|
+
const lastTail = /* @__PURE__ */ new Map();
|
|
1665
|
+
async function tick() {
|
|
1666
|
+
let tasks;
|
|
1667
|
+
try {
|
|
1668
|
+
tasks = await deps.listTasks();
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
deps.log(`probe listTasks failed: ${e.message}`);
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const now = deps.now();
|
|
1674
|
+
for (const rec of tasks) {
|
|
1675
|
+
if (rec.mode !== "interactive")
|
|
1676
|
+
continue;
|
|
1677
|
+
if (rec.state !== "working")
|
|
1678
|
+
continue;
|
|
1679
|
+
if (!rec.name)
|
|
1680
|
+
continue;
|
|
1681
|
+
if (now - rec.lastHeartbeat <= quietMs)
|
|
1682
|
+
continue;
|
|
1683
|
+
let tail;
|
|
1684
|
+
try {
|
|
1685
|
+
tail = await deps.readPaneTail(rec);
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
deps.log(`probe read failed for ${rec.id}: ${e.message}`);
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
if (!tail)
|
|
1691
|
+
continue;
|
|
1692
|
+
if (lastTail.get(rec.id) === tail)
|
|
1693
|
+
continue;
|
|
1694
|
+
lastTail.set(rec.id, tail);
|
|
1695
|
+
const verdict = classifyPaneTail(tail);
|
|
1696
|
+
if (!verdict)
|
|
1697
|
+
continue;
|
|
1698
|
+
const event = verdict.kind === "error" ? {
|
|
1699
|
+
type: "task.failed",
|
|
1700
|
+
id: rec.id,
|
|
1701
|
+
error: `crew session error (pane-detected): ${verdict.text}`
|
|
1702
|
+
} : {
|
|
1703
|
+
type: "task.blocked",
|
|
1704
|
+
id: rec.id,
|
|
1705
|
+
reason: verdict.kind === "approval" ? "crew awaiting permission (pane-detected)" : "crew asked a question (pane-detected)",
|
|
1706
|
+
question: verdict.text
|
|
1707
|
+
};
|
|
1708
|
+
try {
|
|
1709
|
+
await deps.sendEvent(event);
|
|
1710
|
+
const label = verdict.kind === "error" ? "CREW FAILED" : "CREW BLOCKED";
|
|
1711
|
+
deps.log(`probe -> ${label} ${rec.name} (${verdict.kind})`);
|
|
1712
|
+
} catch (e) {
|
|
1713
|
+
deps.log(`probe sendEvent failed for ${rec.id}: ${e.message}`);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return { tick };
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// packages/core/dist/daemon/probes.js
|
|
1721
|
+
function captainNameForProject(project) {
|
|
1722
|
+
const cfg = loadConfig();
|
|
1723
|
+
return cfg.projects?.[project]?.captainName ?? `${project}-captain`;
|
|
1724
|
+
}
|
|
1725
|
+
function createProbes(ctx) {
|
|
1726
|
+
const { store, log } = ctx;
|
|
1727
|
+
const directSurfaceProbe = (_rec) => {
|
|
1728
|
+
return Promise.resolve("unknown");
|
|
1729
|
+
};
|
|
1730
|
+
function buildInteractiveProbe(deps) {
|
|
1731
|
+
const directPaneReader = createDirectCrewPaneReader(deps.cmux, captainNameForProject);
|
|
1732
|
+
const probe = createInteractiveProbe({
|
|
1733
|
+
project: "_all_",
|
|
1734
|
+
listTasks: async () => store.listAll(),
|
|
1735
|
+
readPaneTail: directPaneReader,
|
|
1736
|
+
sendEvent: async (event) => {
|
|
1737
|
+
const rec = store.listAll().find((r) => r.id === event.id);
|
|
1738
|
+
if (rec) {
|
|
1739
|
+
await ctx.d.handle({ kind: "event", project: rec.project, event });
|
|
1740
|
+
}
|
|
1741
|
+
},
|
|
1742
|
+
now: () => Date.now(),
|
|
1743
|
+
log
|
|
1744
|
+
});
|
|
1745
|
+
let probing = false;
|
|
1746
|
+
return async () => {
|
|
1747
|
+
if (probing)
|
|
1748
|
+
return;
|
|
1749
|
+
probing = true;
|
|
1750
|
+
try {
|
|
1751
|
+
await probe.tick();
|
|
1752
|
+
} finally {
|
|
1753
|
+
probing = false;
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
return { buildInteractiveProbe, directSurfaceProbe };
|
|
1758
|
+
}
|
|
1759
|
+
function buildSurfaceProbe(ctx, probes, daemonCmux) {
|
|
1760
|
+
if (ctx.opts.isSurfaceAlive)
|
|
1761
|
+
return ctx.opts.isSurfaceAlive;
|
|
1762
|
+
if (daemonCmux) {
|
|
1763
|
+
return createDirectSurfaceLivenessProbe(daemonCmux, captainNameForProject);
|
|
1764
|
+
}
|
|
1765
|
+
return probes.directSurfaceProbe;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// packages/core/dist/delivery/defer-delivery.js
|
|
1769
|
+
var DeferDelivery = class extends Error {
|
|
1770
|
+
draft;
|
|
1771
|
+
constructor(draft = null) {
|
|
1772
|
+
super("deferred: captain composing");
|
|
1773
|
+
this.draft = draft;
|
|
1774
|
+
this.name = "DeferDelivery";
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
// packages/core/dist/delivery/captain-delivery.js
|
|
1779
|
+
function deliverable(entry) {
|
|
1780
|
+
const msg = entry.message;
|
|
1781
|
+
if (msg == null)
|
|
1782
|
+
return null;
|
|
1783
|
+
const trimmed = msg.trim();
|
|
1784
|
+
return trimmed.length > 0 ? msg : null;
|
|
1785
|
+
}
|
|
1786
|
+
var CaptainDelivery = class {
|
|
1787
|
+
opts;
|
|
1788
|
+
deferCounts = /* @__PURE__ */ new Map();
|
|
1789
|
+
lastContent = /* @__PURE__ */ new Map();
|
|
1790
|
+
stableCounts = /* @__PURE__ */ new Map();
|
|
1791
|
+
constructor(opts) {
|
|
1792
|
+
this.opts = opts;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Attempt to deliver one mailbox entry to the captain. Calls `send(text, opts)`
|
|
1796
|
+
* and, if the send throws DeferDelivery, tracks defer/stable counts for the
|
|
1797
|
+
* entry's seq and returns {deferred: true} (caller should NOT advance cursor).
|
|
1798
|
+
* On success or null message returns {delivered: true} (caller SHOULD advance).
|
|
1799
|
+
*/
|
|
1800
|
+
async deliver(entry, send) {
|
|
1801
|
+
const msg = deliverable(entry);
|
|
1802
|
+
if (!msg)
|
|
1803
|
+
return { delivered: true };
|
|
1804
|
+
const seq = entry.seq;
|
|
1805
|
+
const deferCount = this.deferCounts.get(seq) ?? 0;
|
|
1806
|
+
const stable = (this.stableCounts.get(seq) ?? 0) >= this.opts.stableProbePolls;
|
|
1807
|
+
const probe = stable || deferCount >= this.opts.maxDefers;
|
|
1808
|
+
try {
|
|
1809
|
+
await send(msg, probe ? { probe: true } : void 0);
|
|
1810
|
+
this.deferCounts.delete(seq);
|
|
1811
|
+
this.stableCounts.delete(seq);
|
|
1812
|
+
this.lastContent.delete(seq);
|
|
1813
|
+
return { delivered: true };
|
|
1814
|
+
} catch (e) {
|
|
1815
|
+
if (e instanceof DeferDelivery) {
|
|
1816
|
+
this.deferCounts.set(seq, deferCount + 1);
|
|
1817
|
+
const content = e.draft;
|
|
1818
|
+
if (content && content === this.lastContent.get(seq)) {
|
|
1819
|
+
this.stableCounts.set(seq, (this.stableCounts.get(seq) ?? 0) + 1);
|
|
1820
|
+
} else {
|
|
1821
|
+
this.stableCounts.set(seq, 0);
|
|
1822
|
+
}
|
|
1823
|
+
this.lastContent.set(seq, content);
|
|
1824
|
+
return { deferred: true };
|
|
1825
|
+
}
|
|
1826
|
+
return { deferred: true };
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// packages/core/dist/daemon/delivery.js
|
|
1832
|
+
var CURSOR_SUBSCRIBER = "captain";
|
|
1833
|
+
var CAPTAIN_GONE_STREAK_K = 3;
|
|
1834
|
+
function discoverCaptainSurface(surfaces, captainTitle) {
|
|
1835
|
+
return surfaces.find((s) => s.title === captainTitle) ?? null;
|
|
1836
|
+
}
|
|
1837
|
+
function reapOrphanedCrews(store, project) {
|
|
1838
|
+
let reaped = 0;
|
|
1839
|
+
for (const r of store.list(project)) {
|
|
1840
|
+
if (TERMINAL_STATES.has(r.state))
|
|
1841
|
+
continue;
|
|
1842
|
+
if (r.mode !== "interactive")
|
|
1843
|
+
continue;
|
|
1844
|
+
store.put({ ...r, state: "cancelled", lastEvent: "captain-stopped" });
|
|
1845
|
+
reaped++;
|
|
1846
|
+
}
|
|
1847
|
+
return reaped;
|
|
1848
|
+
}
|
|
1849
|
+
function createDelivery(ctx, daemonCmux) {
|
|
1850
|
+
const { stateRoot, store, log, captainMissingStreak, stoppedProjects, opts } = ctx;
|
|
1851
|
+
const defaultNotify = async (args) => {
|
|
1852
|
+
try {
|
|
1853
|
+
await appendToMailbox({
|
|
1854
|
+
stateRoot,
|
|
1855
|
+
project: args.project,
|
|
1856
|
+
taskRecord: args.record,
|
|
1857
|
+
event: args.event,
|
|
1858
|
+
// Persist the daemon-rendered message (#214/#210): delivered verbatim
|
|
1859
|
+
// rather than re-derived from the raw event (which drifted).
|
|
1860
|
+
message: args.message
|
|
1861
|
+
});
|
|
1862
|
+
} catch (e) {
|
|
1863
|
+
log(`mailbox append failed project=${args.project}: ${e.message}`);
|
|
1864
|
+
}
|
|
1865
|
+
};
|
|
1866
|
+
if (!daemonCmux) {
|
|
1867
|
+
return { defaultNotify, deliveryTick: void 0 };
|
|
1868
|
+
}
|
|
1869
|
+
const cmux2 = daemonCmux;
|
|
1870
|
+
const cfg = loadConfig();
|
|
1871
|
+
const deliveries = /* @__PURE__ */ new Map();
|
|
1872
|
+
const sessionStartMs = Date.now();
|
|
1873
|
+
let delivering = false;
|
|
1874
|
+
const deliveryCore = async () => {
|
|
1875
|
+
const injectedSurfaces = opts.captainSurfaces ?? {};
|
|
1876
|
+
const allProjects = [.../* @__PURE__ */ new Set([
|
|
1877
|
+
...Object.keys(cfg.projects ?? {}),
|
|
1878
|
+
...Object.keys(injectedSurfaces),
|
|
1879
|
+
...store.listAll().map((t) => t.project)
|
|
1880
|
+
])];
|
|
1881
|
+
for (const project of allProjects) {
|
|
1882
|
+
const projCfg = cfg.projects?.[project];
|
|
1883
|
+
const captainTitle = projCfg?.captainName ?? `${project}-captain`;
|
|
1884
|
+
const wsId = cmux2.findWorkspaceId ? await cmux2.findWorkspaceId(captainTitle) : null;
|
|
1885
|
+
let surface = null;
|
|
1886
|
+
let surfacesLength = 0;
|
|
1887
|
+
if (wsId) {
|
|
1888
|
+
const surfaces = await cmux2.listSurfaces(wsId);
|
|
1889
|
+
surfacesLength = surfaces.length;
|
|
1890
|
+
surface = discoverCaptainSurface(surfaces, captainTitle);
|
|
1891
|
+
}
|
|
1892
|
+
if (!surface)
|
|
1893
|
+
surface = injectedSurfaces[project] ?? null;
|
|
1894
|
+
if (surface) {
|
|
1895
|
+
if (stoppedProjects.has(project)) {
|
|
1896
|
+
stoppedProjects.delete(project);
|
|
1897
|
+
captainMissingStreak.set(project, 0);
|
|
1898
|
+
}
|
|
1899
|
+
captainMissingStreak.set(project, 0);
|
|
1900
|
+
} else {
|
|
1901
|
+
if (surfacesLength > 0) {
|
|
1902
|
+
const streak = (captainMissingStreak.get(project) ?? 0) + 1;
|
|
1903
|
+
captainMissingStreak.set(project, streak);
|
|
1904
|
+
if (streak >= CAPTAIN_GONE_STREAK_K) {
|
|
1905
|
+
if (!stoppedProjects.has(project)) {
|
|
1906
|
+
stoppedProjects.add(project);
|
|
1907
|
+
const reaped = reapOrphanedCrews(store, project);
|
|
1908
|
+
log(`captain ${captainTitle}: surface gone for ${CAPTAIN_GONE_STREAK_K} sweeps \u2014 stopping delivery${reaped > 0 ? `, reaped ${reaped} orphaned crew(s)` : ""}`);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
continue;
|
|
1913
|
+
}
|
|
1914
|
+
const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER });
|
|
1915
|
+
const lastAcked = cursor?.lastAckedSeq ?? 0;
|
|
1916
|
+
let d = deliveries.get(project);
|
|
1917
|
+
if (!d) {
|
|
1918
|
+
d = new CaptainDelivery({
|
|
1919
|
+
maxDefers: cfg.delivery?.maxDeferDeliveries ?? 300,
|
|
1920
|
+
stableProbePolls: cfg.delivery?.stableProbePolls ?? 3
|
|
1921
|
+
});
|
|
1922
|
+
deliveries.set(project, d);
|
|
1923
|
+
}
|
|
1924
|
+
for await (const entry of readFromCursor({ stateRoot, project, fromSeq: lastAcked + 1 })) {
|
|
1925
|
+
if (new Date(entry.ts).getTime() < sessionStartMs - STALE_THRESHOLD_MS) {
|
|
1926
|
+
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
1927
|
+
continue;
|
|
1928
|
+
}
|
|
1929
|
+
const result = await d.deliver(entry, (text, sendOpts) => cmux2.send(surface, text, sendOpts));
|
|
1930
|
+
if ("delivered" in result) {
|
|
1931
|
+
await writeCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER, lastAckedSeq: entry.seq });
|
|
1932
|
+
} else {
|
|
1933
|
+
break;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
const deliveryTick = async () => {
|
|
1939
|
+
if (delivering)
|
|
1940
|
+
return;
|
|
1941
|
+
delivering = true;
|
|
1942
|
+
try {
|
|
1943
|
+
await deliveryCore();
|
|
1944
|
+
} finally {
|
|
1945
|
+
delivering = false;
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
return { defaultNotify, deliveryTick };
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// packages/core/dist/daemon/gates.js
|
|
1952
|
+
function createGateResolver(ctx) {
|
|
1953
|
+
return async (taskId, payload) => {
|
|
1954
|
+
const rec = ctx.store.listAll().find((r) => r.id === taskId);
|
|
1955
|
+
try {
|
|
1956
|
+
if (rec?.provider === "opencode") {
|
|
1957
|
+
const decision = payload?.decision === "approve" ? "approve" : "deny";
|
|
1958
|
+
await ctx.opencodeBridge.answer(taskId, decision);
|
|
1959
|
+
} else {
|
|
1960
|
+
await ctx.codexDriver.answer(taskId, payload);
|
|
1961
|
+
}
|
|
1962
|
+
} catch (e) {
|
|
1963
|
+
ctx.log(`gate-resolve answer failed: ${e.message}`);
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// packages/core/dist/daemon/server.js
|
|
1969
|
+
function createServer2(ctx, handlers) {
|
|
1970
|
+
const { store, log, attachConns } = ctx;
|
|
1971
|
+
const { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast } = handlers;
|
|
1972
|
+
return startServer(ctx.sockPath, {
|
|
1973
|
+
handler: async (msg) => {
|
|
1974
|
+
if (msg.kind === "seed") {
|
|
1975
|
+
store.put(msg.record);
|
|
1976
|
+
return { ok: true };
|
|
1977
|
+
}
|
|
1978
|
+
if (msg.kind === "codex-close") {
|
|
1979
|
+
await ctx.codexDriver.close(msg.taskId).catch((e) => log(`codex close err: ${e}`));
|
|
1980
|
+
return { ok: true };
|
|
1981
|
+
}
|
|
1982
|
+
if (msg.kind === "health") {
|
|
1983
|
+
return buildHealth(msg.project);
|
|
1984
|
+
}
|
|
1985
|
+
if (msg.kind === "snapshot") {
|
|
1986
|
+
const now = Date.now();
|
|
1987
|
+
const { assembleDaemonSnapshot: assembleDaemonSnapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
1988
|
+
return assembleDaemonSnapshot2(await gatherSnapshotInputs(now), now);
|
|
1989
|
+
}
|
|
1990
|
+
if (msg.kind === "event") {
|
|
1991
|
+
return ctx.d.handle(msg);
|
|
1992
|
+
}
|
|
1993
|
+
return ctx.d.handle(msg);
|
|
1994
|
+
},
|
|
1995
|
+
onAttach: (conn, frame) => {
|
|
1996
|
+
let set = attachConns.get(frame.taskId);
|
|
1997
|
+
if (!set) {
|
|
1998
|
+
set = /* @__PURE__ */ new Set();
|
|
1999
|
+
attachConns.set(frame.taskId, set);
|
|
2000
|
+
}
|
|
2001
|
+
set.add(conn);
|
|
2002
|
+
cancelPromotionsFor(frame.taskId);
|
|
2003
|
+
try {
|
|
2004
|
+
conn.write(encodeFrame({ type: "reattached", taskId: frame.taskId }));
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
},
|
|
2008
|
+
onAttachInbound: (_conn, frame) => {
|
|
2009
|
+
const f = frame;
|
|
2010
|
+
if (f.op === "say")
|
|
2011
|
+
void ctx.codexDriver.say(f.taskId, f.text).catch((e) => log(`say err: ${e}`));
|
|
2012
|
+
else if (f.op === "steer")
|
|
2013
|
+
void ctx.codexDriver.steer(f.taskId, f.text).catch((e) => log(`steer err: ${e}`));
|
|
2014
|
+
else if (f.op === "interrupt")
|
|
2015
|
+
void ctx.codexDriver.interrupt(f.taskId).catch((e) => log(`interrupt err: ${e}`));
|
|
2016
|
+
else if (f.op === "answer")
|
|
2017
|
+
void ctx.codexDriver.answer(f.taskId, f.payload).catch((e) => log(`answer err: ${e}`));
|
|
2018
|
+
},
|
|
2019
|
+
onAttachClose: (conn) => {
|
|
2020
|
+
for (const set of attachConns.values())
|
|
2021
|
+
set.delete(conn);
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// packages/core/dist/daemon/snapshot-gather.js
|
|
2027
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2028
|
+
import { join as join9 } from "path";
|
|
2029
|
+
import { statSync as statSync2, openSync as openSync2, readSync, closeSync as closeSync2, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
|
|
2030
|
+
var SELF_PATH = fileURLToPath2(import.meta.url);
|
|
2031
|
+
function distBuiltAt() {
|
|
2032
|
+
try {
|
|
2033
|
+
return statSync2(SELF_PATH).mtimeMs;
|
|
2034
|
+
} catch {
|
|
2035
|
+
return 0;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
function gatherLogStats(path13, now, windowMs) {
|
|
2039
|
+
let sizeBytes = 0;
|
|
2040
|
+
try {
|
|
2041
|
+
sizeBytes = statSync2(path13).size;
|
|
2042
|
+
} catch {
|
|
2043
|
+
return { errorCount: 0, sizeBytes: 0, windowMs };
|
|
2044
|
+
}
|
|
2045
|
+
if (sizeBytes === 0)
|
|
2046
|
+
return { errorCount: 0, sizeBytes, windowMs };
|
|
2047
|
+
const CAP = 256 * 1024;
|
|
2048
|
+
const start = Math.max(0, sizeBytes - CAP);
|
|
2049
|
+
const len = sizeBytes - start;
|
|
2050
|
+
let text = "";
|
|
2051
|
+
try {
|
|
2052
|
+
const fd = openSync2(path13, "r");
|
|
2053
|
+
try {
|
|
2054
|
+
const buf = Buffer.alloc(len);
|
|
2055
|
+
readSync(fd, buf, 0, len, start);
|
|
2056
|
+
text = buf.toString("utf-8");
|
|
2057
|
+
} finally {
|
|
2058
|
+
closeSync2(fd);
|
|
2059
|
+
}
|
|
2060
|
+
} catch {
|
|
2061
|
+
return { errorCount: 0, sizeBytes, windowMs };
|
|
2062
|
+
}
|
|
2063
|
+
const cutoff = now - windowMs;
|
|
2064
|
+
let errorCount = 0;
|
|
2065
|
+
for (const line of text.split("\n")) {
|
|
2066
|
+
if (!/error|failed/i.test(line))
|
|
2067
|
+
continue;
|
|
2068
|
+
const m = line.match(/\d{4}-\d{2}-\d{2}T[\d:.]+Z/);
|
|
2069
|
+
if (m) {
|
|
2070
|
+
const ts = Date.parse(m[0]);
|
|
2071
|
+
if (!Number.isNaN(ts) && ts < cutoff)
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
errorCount++;
|
|
2075
|
+
}
|
|
2076
|
+
return { errorCount, sizeBytes, windowMs };
|
|
2077
|
+
}
|
|
2078
|
+
function gatherStoreStats(store, stateRoot, project) {
|
|
2079
|
+
const byState = {};
|
|
2080
|
+
for (const r of store.list(project))
|
|
2081
|
+
byState[r.state] = (byState[r.state] ?? 0) + 1;
|
|
2082
|
+
let corruptCount = 0;
|
|
2083
|
+
const dir = join9(stateRoot, project);
|
|
2084
|
+
try {
|
|
2085
|
+
for (const n of readdirSync2(dir)) {
|
|
2086
|
+
if (n.includes(".corrupt.")) {
|
|
2087
|
+
corruptCount++;
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (!n.endsWith(".json"))
|
|
2091
|
+
continue;
|
|
2092
|
+
try {
|
|
2093
|
+
JSON.parse(readFileSync7(join9(dir, n), "utf-8"));
|
|
2094
|
+
} catch {
|
|
2095
|
+
corruptCount++;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
} catch {
|
|
2099
|
+
}
|
|
2100
|
+
return { byState, corruptCount };
|
|
2101
|
+
}
|
|
2102
|
+
function gatherResults(resultsDir) {
|
|
2103
|
+
let fileCount = 0;
|
|
2104
|
+
let totalBytes = 0;
|
|
2105
|
+
try {
|
|
2106
|
+
for (const n of readdirSync2(resultsDir)) {
|
|
2107
|
+
try {
|
|
2108
|
+
const s = statSync2(join9(resultsDir, n));
|
|
2109
|
+
if (s.isFile()) {
|
|
2110
|
+
fileCount++;
|
|
2111
|
+
totalBytes += s.size;
|
|
2112
|
+
}
|
|
2113
|
+
} catch {
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
return { fileCount, totalBytes };
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// packages/core/dist/daemon/start.js
|
|
2122
|
+
var CURSOR_SUBSCRIBER2 = "captain";
|
|
2123
|
+
var SNAPSHOT_LOG_WINDOW_MS = 60 * 60 * 1e3;
|
|
2124
|
+
function startDaemon(ctx, opts, pkgVersion) {
|
|
2125
|
+
const { stateRoot, store, log, isPidAlive, resultsDir, taskTimeoutMs, inFlightHeadlessIds, activeHeadlessKills, broadcast, cancelPromotionsFor } = ctx;
|
|
2126
|
+
const { daemonCmux } = ctx;
|
|
2127
|
+
const probes = createProbes(ctx);
|
|
2128
|
+
const { defaultNotify, deliveryTick: initialDeliveryTick } = createDelivery(ctx, daemonCmux);
|
|
2129
|
+
const notify = opts.notify ?? defaultNotify;
|
|
2130
|
+
const surfaceProbe = buildSurfaceProbe(ctx, probes, daemonCmux);
|
|
2131
|
+
const ingest = (project) => (e) => void ctx.d.handle({ kind: "event", project, event: e });
|
|
2132
|
+
const d = createDaemon({
|
|
2133
|
+
store,
|
|
2134
|
+
now: () => Date.now(),
|
|
2135
|
+
isPidAlive,
|
|
2136
|
+
notify,
|
|
2137
|
+
taskTimeoutMs,
|
|
2138
|
+
isSurfaceAlive: surfaceProbe,
|
|
2139
|
+
launchHeadless: opts.launchHeadless,
|
|
2140
|
+
isHeadlessInFlight: (id) => inFlightHeadlessIds.has(id),
|
|
2141
|
+
launchInteractive: async (rec) => {
|
|
2142
|
+
if (rec.provider === "codex") {
|
|
2143
|
+
await ctx.codexDriver.dispatch(rec);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (rec.provider === "claude") {
|
|
2147
|
+
ingest(rec.project)({ type: "task.started", id: rec.id });
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
if (rec.provider === "opencode") {
|
|
2151
|
+
ingest(rec.project)({ type: "task.started", id: rec.id });
|
|
2152
|
+
if (rec.serverPort)
|
|
2153
|
+
ctx.opencodeBridge.start({ taskId: rec.id, port: rec.serverPort });
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
throw new Error(`interactive mode is not yet implemented for provider '${rec.provider}'; only 'codex', 'claude', and 'opencode' are supported`);
|
|
2157
|
+
},
|
|
2158
|
+
resolveInteractiveGate: createGateResolver(ctx)
|
|
2159
|
+
});
|
|
2160
|
+
ctx.d = d;
|
|
2161
|
+
function buildHealth(only) {
|
|
2162
|
+
const config = loadConfig();
|
|
2163
|
+
const now = Date.now();
|
|
2164
|
+
const known = /* @__PURE__ */ new Set([
|
|
2165
|
+
...Object.keys(config.projects),
|
|
2166
|
+
...store.listAll().map((t) => t.project)
|
|
2167
|
+
]);
|
|
2168
|
+
const names = only ? [only] : [...known];
|
|
2169
|
+
const out = [];
|
|
2170
|
+
for (const project of names) {
|
|
2171
|
+
const proj = config.projects[project];
|
|
2172
|
+
const captainName = proj?.captainName ?? `${project}-captain`;
|
|
2173
|
+
const stopped = ctx.stoppedProjects.has(project);
|
|
2174
|
+
const streak = ctx.captainMissingStreak.get(project);
|
|
2175
|
+
const captainStopped = stopped ? true : streak === 0 ? false : null;
|
|
2176
|
+
out.push(...projectHealth({
|
|
2177
|
+
project,
|
|
2178
|
+
now,
|
|
2179
|
+
captainName,
|
|
2180
|
+
captainStopped,
|
|
2181
|
+
commandPresent: null,
|
|
2182
|
+
crews: store.list(project)
|
|
2183
|
+
}));
|
|
2184
|
+
}
|
|
2185
|
+
return out;
|
|
2186
|
+
}
|
|
2187
|
+
async function gatherSnapshotInputs(now) {
|
|
2188
|
+
const logPath2 = join10(dirname4(stateRoot), "squadrantd.log");
|
|
2189
|
+
const tier2Projects = opts.registeredProjects ?? Object.keys(loadConfig().projects);
|
|
2190
|
+
const projects = await Promise.all(tier2Projects.map(async (project) => {
|
|
2191
|
+
const cursor = await readCursor({ stateRoot, project, subscriber: CURSOR_SUBSCRIBER2 });
|
|
2192
|
+
const storeStats = gatherStoreStats(store, stateRoot, project);
|
|
2193
|
+
return {
|
|
2194
|
+
project,
|
|
2195
|
+
mailbox: await mailboxStats(stateRoot, project),
|
|
2196
|
+
lastAckedSeq: cursor?.lastAckedSeq ?? 0,
|
|
2197
|
+
storeByState: storeStats.byState,
|
|
2198
|
+
corruptCount: storeStats.corruptCount
|
|
2199
|
+
};
|
|
2200
|
+
}));
|
|
2201
|
+
return {
|
|
2202
|
+
pid: process.pid,
|
|
2203
|
+
processStartedAt: ctx.bootedAt,
|
|
2204
|
+
version: pkgVersion,
|
|
2205
|
+
distBuiltAt: distBuiltAt(),
|
|
2206
|
+
lastSweepAt: ctx.lastSweepAt.value,
|
|
2207
|
+
sweepCadenceMs: opts.sweepMs ?? 3e4,
|
|
2208
|
+
log: gatherLogStats(logPath2, now, SNAPSHOT_LOG_WINDOW_MS),
|
|
2209
|
+
health: buildHealth(),
|
|
2210
|
+
projects,
|
|
2211
|
+
results: gatherResults(resultsDir)
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
void (async () => {
|
|
2215
|
+
try {
|
|
2216
|
+
await d.reconcile();
|
|
2217
|
+
} catch (e) {
|
|
2218
|
+
log(`reconcile on boot failed: ${e.message}`);
|
|
2219
|
+
}
|
|
2220
|
+
const bootNow = Date.now();
|
|
2221
|
+
const REATTACH_STALE_MS = 10 * 6e4;
|
|
2222
|
+
for (const rec of store.listAll()) {
|
|
2223
|
+
if (rec.provider !== "codex" || rec.mode !== "interactive")
|
|
2224
|
+
continue;
|
|
2225
|
+
if (TERMINAL_STATES.has(rec.state))
|
|
2226
|
+
continue;
|
|
2227
|
+
const lastAttempt = rec.attempts?.at(-1);
|
|
2228
|
+
const last = lastAttempt?.lastHeartbeatAt ?? rec.lastHeartbeat ?? 0;
|
|
2229
|
+
if (bootNow - last > REATTACH_STALE_MS)
|
|
2230
|
+
continue;
|
|
2231
|
+
if (!lastAttempt?.resumeRef)
|
|
2232
|
+
continue;
|
|
2233
|
+
ctx.codexDriver.reattach(rec).catch((e) => {
|
|
2234
|
+
log(`reattach failed for ${rec.id}: ${e.message}`);
|
|
2235
|
+
});
|
|
2236
|
+
}
|
|
2237
|
+
for (const rec of store.listAll()) {
|
|
2238
|
+
if (rec.provider !== "opencode" || rec.mode !== "interactive")
|
|
2239
|
+
continue;
|
|
2240
|
+
if (TERMINAL_STATES.has(rec.state))
|
|
2241
|
+
continue;
|
|
2242
|
+
if (!rec.serverPort)
|
|
2243
|
+
continue;
|
|
2244
|
+
ctx.opencodeBridge.start({ taskId: rec.id, port: rec.serverPort });
|
|
2245
|
+
}
|
|
2246
|
+
const enableCmuxEvents = loadConfig().defaults.cmuxEventsBridge !== false;
|
|
2247
|
+
const cmuxEventsSafe = !!opts.cmuxEventsBridge || !process.env.VITEST;
|
|
2248
|
+
if (enableCmuxEvents && cmuxEventsSafe) {
|
|
2249
|
+
try {
|
|
2250
|
+
ctx.cmuxEventsBridge.start();
|
|
2251
|
+
} catch (e) {
|
|
2252
|
+
log(`cmux events bridge start failed: ${e.message}`);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const autoConfigSafe = !!opts.runCmuxAutoConfig || !process.env.VITEST;
|
|
2256
|
+
if (autoConfigSafe) {
|
|
2257
|
+
try {
|
|
2258
|
+
const r = await (opts.runCmuxAutoConfig ?? ensureCmuxAutoConfig)();
|
|
2259
|
+
if (r.configChanged)
|
|
2260
|
+
log(`cmux autoconfig: wrote automation socket mode to ${r.configPath}`);
|
|
2261
|
+
if (r.needsRestart && r.promptedThisRun) {
|
|
2262
|
+
log("cmux autoconfig: socket still rejects the daemon \u2014 restart cmux to enable daemon-direct delivery");
|
|
2263
|
+
}
|
|
2264
|
+
} catch (e) {
|
|
2265
|
+
log(`cmux autoconfig failed: ${e.message}`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
})();
|
|
2269
|
+
const server = createServer2(ctx, { buildHealth, gatherSnapshotInputs, cancelPromotionsFor, broadcast });
|
|
2270
|
+
log(`started pid=${process.pid} sock=${ctx.sockPath} stateRoot=${stateRoot}`);
|
|
2271
|
+
let deliveryTick = initialDeliveryTick;
|
|
2272
|
+
let probeTick;
|
|
2273
|
+
if (daemonCmux) {
|
|
2274
|
+
probeTick = probes.buildInteractiveProbe({ cmux: daemonCmux });
|
|
2275
|
+
}
|
|
2276
|
+
let deliveryTimer;
|
|
2277
|
+
if (daemonCmux && opts.sweepMs && opts.sweepMs > 0) {
|
|
2278
|
+
deliveryTimer = setInterval(() => {
|
|
2279
|
+
void deliveryTick().catch((e) => log(`delivery tick error: ${e.message}`));
|
|
2280
|
+
}, 1e3);
|
|
2281
|
+
deliveryTimer.unref?.();
|
|
2282
|
+
}
|
|
2283
|
+
let probeTimer;
|
|
2284
|
+
if (daemonCmux && opts.sweepMs && opts.sweepMs > 0) {
|
|
2285
|
+
probeTimer = setInterval(() => {
|
|
2286
|
+
void probeTick().catch((e) => log(`probe tick error: ${e.message}`));
|
|
2287
|
+
}, 1e4);
|
|
2288
|
+
probeTimer.unref?.();
|
|
2289
|
+
}
|
|
2290
|
+
let timer;
|
|
2291
|
+
if (opts.sweepMs && opts.sweepMs > 0) {
|
|
2292
|
+
let sweeping = false;
|
|
2293
|
+
timer = setInterval(() => {
|
|
2294
|
+
if (sweeping)
|
|
2295
|
+
return;
|
|
2296
|
+
sweeping = true;
|
|
2297
|
+
ctx.lastSweepAt.value = Date.now();
|
|
2298
|
+
void d.sweep().catch((e) => log(`sweep failed: ${e.message}`)).finally(() => {
|
|
2299
|
+
sweeping = false;
|
|
2300
|
+
});
|
|
2301
|
+
}, opts.sweepMs);
|
|
2302
|
+
timer.unref?.();
|
|
2303
|
+
}
|
|
2304
|
+
const rotationInterval = opts.rotationIntervalMs ?? 6e4;
|
|
2305
|
+
const mboxCfg = {
|
|
2306
|
+
maxBytes: opts.mailboxConfig?.maxBytes ?? 5 * 1024 * 1024,
|
|
2307
|
+
maxAgeMs: opts.mailboxConfig?.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3,
|
|
2308
|
+
keepCount: opts.mailboxConfig?.keepCount ?? 3
|
|
2309
|
+
};
|
|
2310
|
+
let rotationTimer;
|
|
2311
|
+
if (rotationInterval > 0) {
|
|
2312
|
+
const inboxPath = join10(stateRoot, "inbox");
|
|
2313
|
+
rotationTimer = setInterval(async () => {
|
|
2314
|
+
try {
|
|
2315
|
+
let entries;
|
|
2316
|
+
try {
|
|
2317
|
+
entries = await readdir(inboxPath);
|
|
2318
|
+
} catch {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const projects = new Set(entries.filter((e) => e.endsWith(".log")).map((e) => e.slice(0, -".log".length)));
|
|
2322
|
+
for (const project of projects)
|
|
2323
|
+
await rotateIfNeeded({ stateRoot, project, ...mboxCfg });
|
|
2324
|
+
} catch (e) {
|
|
2325
|
+
log(`rotation timer error: ${e.message}`);
|
|
2326
|
+
}
|
|
2327
|
+
}, rotationInterval);
|
|
2328
|
+
rotationTimer.unref?.();
|
|
2329
|
+
}
|
|
2330
|
+
return {
|
|
2331
|
+
stop() {
|
|
2332
|
+
if (deliveryTimer)
|
|
2333
|
+
clearInterval(deliveryTimer);
|
|
2334
|
+
if (probeTimer)
|
|
2335
|
+
clearInterval(probeTimer);
|
|
2336
|
+
if (timer)
|
|
2337
|
+
clearInterval(timer);
|
|
2338
|
+
if (rotationTimer)
|
|
2339
|
+
clearInterval(rotationTimer);
|
|
2340
|
+
try {
|
|
2341
|
+
ctx.cmuxEventsBridge.stop();
|
|
2342
|
+
} catch {
|
|
2343
|
+
}
|
|
2344
|
+
try {
|
|
2345
|
+
ctx.codexDriver.stop?.();
|
|
2346
|
+
} catch {
|
|
2347
|
+
}
|
|
2348
|
+
for (const kill of ctx.activeHeadlessKills)
|
|
2349
|
+
kill();
|
|
2350
|
+
return new Promise((resolve2) => server.close(() => {
|
|
2351
|
+
log("stopped");
|
|
2352
|
+
resolve2();
|
|
2353
|
+
}));
|
|
2354
|
+
},
|
|
2355
|
+
tickDelivery: deliveryTick,
|
|
2356
|
+
tickProbe: probeTick
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// packages/core/dist/session-freshness.js
|
|
2361
|
+
import crypto from "crypto";
|
|
2362
|
+
import fs7 from "fs";
|
|
2363
|
+
import path6 from "path";
|
|
2364
|
+
|
|
2365
|
+
// packages/core/dist/crew-lifecycle.js
|
|
2366
|
+
import { exec as nodeExec } from "child_process";
|
|
2367
|
+
|
|
2368
|
+
// packages/agents/dist/drivers/claude.js
|
|
2369
|
+
import { execSync as execSync2 } from "child_process";
|
|
2370
|
+
|
|
2371
|
+
// packages/agents/dist/drivers/codex.js
|
|
2372
|
+
import { execSync as execSync3 } from "child_process";
|
|
2373
|
+
|
|
2374
|
+
// packages/agents/dist/drivers/gemini.js
|
|
2375
|
+
import { execSync as execSync4 } from "child_process";
|
|
2376
|
+
|
|
2377
|
+
// packages/agents/dist/drivers/opencode.js
|
|
2378
|
+
import { execSync as execSync5 } from "child_process";
|
|
2379
|
+
|
|
2380
|
+
// packages/agents/dist/drivers/launch-cmd.js
|
|
2381
|
+
import fs8 from "fs";
|
|
2382
|
+
import path7 from "path";
|
|
2383
|
+
|
|
2384
|
+
// packages/agents/dist/projection/cursor.js
|
|
2385
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2386
|
+
import path8 from "path";
|
|
2387
|
+
import os2 from "os";
|
|
2388
|
+
|
|
2389
|
+
// packages/agents/dist/projection/codex.js
|
|
2390
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
2391
|
+
import path9 from "path";
|
|
2392
|
+
import os3 from "os";
|
|
2393
|
+
|
|
2394
|
+
// packages/agents/dist/projection/gemini.js
|
|
2395
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
2396
|
+
import path10 from "path";
|
|
2397
|
+
import os4 from "os";
|
|
2398
|
+
|
|
2399
|
+
// packages/agents/dist/projection/opencode.js
|
|
2400
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2401
|
+
import path11 from "path";
|
|
2402
|
+
import os5 from "os";
|
|
2403
|
+
|
|
2404
|
+
// packages/agents/dist/codex/app-server-client.js
|
|
2405
|
+
import { EventEmitter } from "events";
|
|
2406
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
2407
|
+
function _parseChunk(acc, chunk) {
|
|
2408
|
+
acc.buf += chunk;
|
|
2409
|
+
const out = [];
|
|
2410
|
+
let idx;
|
|
2411
|
+
while ((idx = acc.buf.indexOf("\n")) >= 0) {
|
|
2412
|
+
const line = acc.buf.slice(0, idx);
|
|
2413
|
+
acc.buf = acc.buf.slice(idx + 1);
|
|
2414
|
+
if (!line.trim())
|
|
2415
|
+
continue;
|
|
2416
|
+
try {
|
|
2417
|
+
out.push(JSON.parse(line));
|
|
2418
|
+
} catch {
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return out;
|
|
2422
|
+
}
|
|
2423
|
+
var AppServerClient = class extends EventEmitter {
|
|
2424
|
+
proc;
|
|
2425
|
+
acc = { buf: "" };
|
|
2426
|
+
opts;
|
|
2427
|
+
constructor(opts = {}) {
|
|
2428
|
+
super();
|
|
2429
|
+
this.opts = opts;
|
|
2430
|
+
}
|
|
2431
|
+
start() {
|
|
2432
|
+
if (this.proc)
|
|
2433
|
+
throw new Error("AppServerClient already started");
|
|
2434
|
+
const sp = this.opts.spawn ?? defaultSpawn;
|
|
2435
|
+
this.proc = sp();
|
|
2436
|
+
this.proc.stdout.on("data", (d) => this._onStdout(d.toString()));
|
|
2437
|
+
this.proc.stderr.on("data", (d) => this.emit("stderr", d.toString()));
|
|
2438
|
+
this.proc.on("exit", (code, signal) => {
|
|
2439
|
+
this._onClosed();
|
|
2440
|
+
this.emit("closed", { code, signal });
|
|
2441
|
+
});
|
|
2442
|
+
this.proc.on("error", (e) => this.emit("error", e));
|
|
2443
|
+
}
|
|
2444
|
+
kill() {
|
|
2445
|
+
if (this.proc)
|
|
2446
|
+
this.proc.kill();
|
|
2447
|
+
}
|
|
2448
|
+
async initialize() {
|
|
2449
|
+
if (this._handshakeDone)
|
|
2450
|
+
return;
|
|
2451
|
+
if (!this.proc)
|
|
2452
|
+
throw new Error("AppServerClient not started");
|
|
2453
|
+
const info = this.opts.clientInfo ?? { name: "squadrant", version: "0" };
|
|
2454
|
+
const id = this.nextId++;
|
|
2455
|
+
const env = { jsonrpc: "2.0", id, method: "initialize", params: { clientInfo: info } };
|
|
2456
|
+
const res = await new Promise((resolve2, reject) => {
|
|
2457
|
+
this.pending.set(id, { resolve: resolve2, reject });
|
|
2458
|
+
this.proc.stdin.write(JSON.stringify(env) + "\n");
|
|
2459
|
+
});
|
|
2460
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "initialized" }) + "\n");
|
|
2461
|
+
this._handshakeDone = true;
|
|
2462
|
+
return res;
|
|
2463
|
+
}
|
|
2464
|
+
async startThread(params) {
|
|
2465
|
+
const res = await this._sendRequest("thread/start", params);
|
|
2466
|
+
const id = res?.thread?.id;
|
|
2467
|
+
if (typeof id !== "string")
|
|
2468
|
+
throw new Error(`thread/start: unexpected response shape (no thread.id): ${JSON.stringify(res).slice(0, 200)}`);
|
|
2469
|
+
return { threadId: id };
|
|
2470
|
+
}
|
|
2471
|
+
resumeThread(params) {
|
|
2472
|
+
return this._sendRequest("thread/resume", params);
|
|
2473
|
+
}
|
|
2474
|
+
/** Archive a thread so the app-server tears it down (and reaps any per-thread
|
|
2475
|
+
* MCP servers it spawned). Called when a codex crew closes. */
|
|
2476
|
+
archiveThread(threadId) {
|
|
2477
|
+
return this._sendRequest("thread/archive", { threadId });
|
|
2478
|
+
}
|
|
2479
|
+
readThread(params) {
|
|
2480
|
+
return this._sendRequest("thread/read", params);
|
|
2481
|
+
}
|
|
2482
|
+
async sendTurn(threadId, text) {
|
|
2483
|
+
const ack = await this._sendRequest("turn/start", {
|
|
2484
|
+
threadId,
|
|
2485
|
+
input: [{ type: "text", text }]
|
|
2486
|
+
});
|
|
2487
|
+
const turnId = ack?.turn?.id;
|
|
2488
|
+
if (typeof turnId !== "string")
|
|
2489
|
+
throw new Error(`turn/start: unexpected ack shape (no turn.id): ${JSON.stringify(ack).slice(0, 200)}`);
|
|
2490
|
+
return new Promise((resolve2, reject) => {
|
|
2491
|
+
const onNote = (n) => {
|
|
2492
|
+
if (n.params?.turn?.id !== turnId)
|
|
2493
|
+
return;
|
|
2494
|
+
if (n.method === "turn/completed") {
|
|
2495
|
+
cleanup();
|
|
2496
|
+
resolve2({ turnId });
|
|
2497
|
+
}
|
|
2498
|
+
if (n.method === "turn/failed") {
|
|
2499
|
+
cleanup();
|
|
2500
|
+
reject(new Error(n.params?.error ?? "turn failed"));
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
const onClientClosed = () => {
|
|
2504
|
+
cleanup();
|
|
2505
|
+
reject(new Error("AppServerClient: client closed before turn completed"));
|
|
2506
|
+
};
|
|
2507
|
+
const cleanup = () => {
|
|
2508
|
+
this.off("notification", onNote);
|
|
2509
|
+
this.off("_clientClosed", onClientClosed);
|
|
2510
|
+
};
|
|
2511
|
+
this.on("notification", onNote);
|
|
2512
|
+
this.once("_clientClosed", onClientClosed);
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
steerTurn(threadId, text) {
|
|
2516
|
+
return this._sendRequest("turn/steer", { threadId, input: [{ type: "text", text }] });
|
|
2517
|
+
}
|
|
2518
|
+
interruptTurn(threadId) {
|
|
2519
|
+
return this._sendRequest("turn/interrupt", { threadId });
|
|
2520
|
+
}
|
|
2521
|
+
injectItems(threadId, items) {
|
|
2522
|
+
return this._sendRequest("thread/inject_items", { threadId, items });
|
|
2523
|
+
}
|
|
2524
|
+
respondToServerRequest(id, result) {
|
|
2525
|
+
if (!this.proc)
|
|
2526
|
+
throw new Error("AppServerClient not started");
|
|
2527
|
+
this.proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
2528
|
+
}
|
|
2529
|
+
nextId = 1;
|
|
2530
|
+
pending = /* @__PURE__ */ new Map();
|
|
2531
|
+
_handshakeDone = false;
|
|
2532
|
+
_onClosed() {
|
|
2533
|
+
const closedErr = new Error("AppServerClient: child closed before response");
|
|
2534
|
+
for (const slot of this.pending.values())
|
|
2535
|
+
slot.reject(closedErr);
|
|
2536
|
+
this.pending.clear();
|
|
2537
|
+
this.emit("_clientClosed");
|
|
2538
|
+
}
|
|
2539
|
+
_sendRequest(method, params) {
|
|
2540
|
+
if (!this._handshakeDone && method !== "initialize") {
|
|
2541
|
+
throw new Error(`AppServerClient: cannot call '${method}' before handshake (spec \xA73.2)`);
|
|
2542
|
+
}
|
|
2543
|
+
if (!this.proc)
|
|
2544
|
+
throw new Error("AppServerClient not started");
|
|
2545
|
+
const id = this.nextId++;
|
|
2546
|
+
const env = { jsonrpc: "2.0", id, method, params: params ?? {} };
|
|
2547
|
+
return new Promise((resolve2, reject) => {
|
|
2548
|
+
this.pending.set(id, { resolve: resolve2, reject });
|
|
2549
|
+
this.proc.stdin.write(JSON.stringify(env) + "\n");
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
_dispatchResponse(msg) {
|
|
2553
|
+
if (typeof msg?.id !== "number")
|
|
2554
|
+
return false;
|
|
2555
|
+
const slot = this.pending.get(msg.id);
|
|
2556
|
+
if (!slot)
|
|
2557
|
+
return false;
|
|
2558
|
+
this.pending.delete(msg.id);
|
|
2559
|
+
if (msg.error)
|
|
2560
|
+
slot.reject(new Error(`${msg.error.message ?? "rpc-error"} (code ${msg.error.code})`));
|
|
2561
|
+
else
|
|
2562
|
+
slot.resolve(msg.result);
|
|
2563
|
+
return true;
|
|
2564
|
+
}
|
|
2565
|
+
_onStdout(s) {
|
|
2566
|
+
for (const msg of _parseChunk(this.acc, s))
|
|
2567
|
+
this._dispatch(msg);
|
|
2568
|
+
}
|
|
2569
|
+
_dispatch(msg) {
|
|
2570
|
+
if (this._dispatchResponse(msg))
|
|
2571
|
+
return;
|
|
2572
|
+
const m = msg;
|
|
2573
|
+
if (typeof m?.method === "string" && typeof m?.id === "number") {
|
|
2574
|
+
this.emit("serverRequest", { id: m.id, method: m.method, params: m.params });
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
if (typeof m?.method === "string" && m?.id === void 0) {
|
|
2578
|
+
this.emit("notification", { method: m.method, params: m.params });
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
function defaultSpawn() {
|
|
2583
|
+
return nodeSpawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "pipe"] });
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// packages/agents/dist/codex/config.js
|
|
2587
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2588
|
+
import { homedir as homedir6 } from "os";
|
|
2589
|
+
import { join as join11 } from "path";
|
|
2590
|
+
async function resolveCodexModel() {
|
|
2591
|
+
const home = process.env["CODEX_HOME"] ?? join11(homedir6(), ".codex");
|
|
2592
|
+
const configPath = join11(home, "config.toml");
|
|
2593
|
+
let text;
|
|
2594
|
+
try {
|
|
2595
|
+
text = await readFile5(configPath, "utf8");
|
|
2596
|
+
} catch {
|
|
2597
|
+
return void 0;
|
|
2598
|
+
}
|
|
2599
|
+
const topLevel = text.split(/^\[/m)[0] ?? "";
|
|
2600
|
+
const modelMatch = topLevel.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2601
|
+
if (!modelMatch)
|
|
2602
|
+
return void 0;
|
|
2603
|
+
let model = modelMatch[1];
|
|
2604
|
+
const migSection = text.match(/^\[notice\.model_migrations\]([\s\S]*?)(?=^\[|(?![\s\S]))/m);
|
|
2605
|
+
if (migSection) {
|
|
2606
|
+
const migRe = /^"([^"]+)"\s*=\s*"([^"]+)"/mg;
|
|
2607
|
+
let m;
|
|
2608
|
+
while ((m = migRe.exec(migSection[1])) !== null) {
|
|
2609
|
+
if (m[1] === model) {
|
|
2610
|
+
model = m[2];
|
|
2611
|
+
break;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
return model;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// packages/agents/dist/codex/normalize.js
|
|
2619
|
+
function normalizeAppServerNotification(taskId, n) {
|
|
2620
|
+
const p = n.params ?? {};
|
|
2621
|
+
switch (n.method) {
|
|
2622
|
+
// ── turn lifecycle ────────────────────────────────────────────────────
|
|
2623
|
+
case "turn/started":
|
|
2624
|
+
return {
|
|
2625
|
+
type: "task.turn.started",
|
|
2626
|
+
id: taskId,
|
|
2627
|
+
// TurnStartedNotification carries params.turn.id, not a top-level turnId.
|
|
2628
|
+
turnId: String(p["turn"]?.["id"] ?? "")
|
|
2629
|
+
};
|
|
2630
|
+
case "turn/completed":
|
|
2631
|
+
return {
|
|
2632
|
+
type: "task.turn.completed",
|
|
2633
|
+
id: taskId,
|
|
2634
|
+
// TurnCompletedNotification: same shape as TurnStartedNotification.
|
|
2635
|
+
turnId: String(p["turn"]?.["id"] ?? "")
|
|
2636
|
+
};
|
|
2637
|
+
// ── streaming delta (heartbeat / content) ─────────────────────────────
|
|
2638
|
+
// AgentMessageDeltaNotification: { threadId, turnId, itemId, delta }
|
|
2639
|
+
case "item/agentMessage/delta":
|
|
2640
|
+
// ReasoningTextDeltaNotification: { threadId, turnId, itemId, delta, contentIndex }
|
|
2641
|
+
case "item/reasoning/textDelta":
|
|
2642
|
+
// CommandExecutionOutputDeltaNotification: { threadId, turnId, itemId, delta }
|
|
2643
|
+
case "item/commandExecution/outputDelta":
|
|
2644
|
+
return {
|
|
2645
|
+
type: "task.delta",
|
|
2646
|
+
id: taskId,
|
|
2647
|
+
turnId: String(p["turnId"] ?? ""),
|
|
2648
|
+
chunk: String(p["delta"] ?? "")
|
|
2649
|
+
};
|
|
2650
|
+
// command/exec/outputDelta is connection-scoped (no turnId); map to delta
|
|
2651
|
+
// with empty turnId so the bus can still forward it.
|
|
2652
|
+
// CommandExecOutputDeltaNotification: { processId, stream, deltaBase64, capReached }
|
|
2653
|
+
case "command/exec/outputDelta":
|
|
2654
|
+
return {
|
|
2655
|
+
type: "task.delta",
|
|
2656
|
+
id: taskId,
|
|
2657
|
+
turnId: "",
|
|
2658
|
+
chunk: String(p["deltaBase64"] ?? "")
|
|
2659
|
+
};
|
|
2660
|
+
// ── error ─────────────────────────────────────────────────────────────
|
|
2661
|
+
// ErrorNotification: { error: TurnError, willRetry, threadId, turnId }
|
|
2662
|
+
case "error": {
|
|
2663
|
+
const err = p["error"];
|
|
2664
|
+
const message = String(err?.["message"] ?? p["message"] ?? "error");
|
|
2665
|
+
return { type: "task.failed", id: taskId, error: message };
|
|
2666
|
+
}
|
|
2667
|
+
// ── status-line only — return null ────────────────────────────────────
|
|
2668
|
+
// ThreadTokenUsageUpdatedNotification: { threadId, turnId, tokenUsage }
|
|
2669
|
+
case "thread/tokenUsage/updated":
|
|
2670
|
+
// ContextCompactedNotification (deprecated): { threadId, turnId }
|
|
2671
|
+
case "thread/compacted":
|
|
2672
|
+
// ThreadStatusChangedNotification
|
|
2673
|
+
case "thread/status/changed":
|
|
2674
|
+
return null;
|
|
2675
|
+
// ── unknown / future methods ──────────────────────────────────────────
|
|
2676
|
+
default:
|
|
2677
|
+
return null;
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
// packages/agents/dist/codex/driver.js
|
|
2682
|
+
var CodexInteractiveDriver = class {
|
|
2683
|
+
client;
|
|
2684
|
+
handshakeP;
|
|
2685
|
+
threadByTask = /* @__PURE__ */ new Map();
|
|
2686
|
+
taskByThread = /* @__PURE__ */ new Map();
|
|
2687
|
+
/**
|
|
2688
|
+
* taskId → in-flight dispatch promise. The first-turn say() can arrive while
|
|
2689
|
+
* dispatch() is still awaiting startThread (threadByTask not yet set); say()
|
|
2690
|
+
* awaits this gate before reading threadByTask so the first turn isn't lost
|
|
2691
|
+
* with "no thread for task" (issue #212).
|
|
2692
|
+
*/
|
|
2693
|
+
dispatchByTask = /* @__PURE__ */ new Map();
|
|
2694
|
+
/** taskId → last pending server-request {id, method} (for answer()) */
|
|
2695
|
+
serverRequestByTask = /* @__PURE__ */ new Map();
|
|
2696
|
+
deps;
|
|
2697
|
+
constructor(deps) {
|
|
2698
|
+
this.deps = deps;
|
|
2699
|
+
}
|
|
2700
|
+
async ensureClient() {
|
|
2701
|
+
if (this.client)
|
|
2702
|
+
return this.client;
|
|
2703
|
+
const c = (this.deps.makeClient ?? (() => new AppServerClient({ clientInfo: { name: "squadrant", version: "iv" } })))();
|
|
2704
|
+
this.client = c;
|
|
2705
|
+
c.start();
|
|
2706
|
+
c.on("notification", (n) => this.onNotification(n));
|
|
2707
|
+
c.on("serverRequest", (r) => this.onServerRequest(r));
|
|
2708
|
+
c.on("closed", () => {
|
|
2709
|
+
this.client = void 0;
|
|
2710
|
+
this.handshakeP = void 0;
|
|
2711
|
+
});
|
|
2712
|
+
return c;
|
|
2713
|
+
}
|
|
2714
|
+
async ensureHandshake() {
|
|
2715
|
+
const c = await this.ensureClient();
|
|
2716
|
+
if (!this.handshakeP)
|
|
2717
|
+
this.handshakeP = c.initialize().then(() => {
|
|
2718
|
+
});
|
|
2719
|
+
return this.handshakeP;
|
|
2720
|
+
}
|
|
2721
|
+
async dispatch(rec) {
|
|
2722
|
+
const p = this.runDispatch(rec);
|
|
2723
|
+
this.dispatchByTask.set(rec.id, p.then(() => {
|
|
2724
|
+
}, () => {
|
|
2725
|
+
}));
|
|
2726
|
+
try {
|
|
2727
|
+
await p;
|
|
2728
|
+
} finally {
|
|
2729
|
+
this.dispatchByTask.delete(rec.id);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
async runDispatch(rec) {
|
|
2733
|
+
try {
|
|
2734
|
+
const c = await this.ensureClient();
|
|
2735
|
+
await withTimeout(this.ensureHandshake(), 1e4, "handshake timed out");
|
|
2736
|
+
const model = rec.model ?? await resolveCodexModel();
|
|
2737
|
+
const { threadId } = await c.startThread({
|
|
2738
|
+
cwd: rec.cwd ?? process.cwd(),
|
|
2739
|
+
model,
|
|
2740
|
+
// Parity with claude/opencode crews, which run UNSANDBOXED (no Seatbelt).
|
|
2741
|
+
// Codex was the only agent under `workspace-write`, and that FS sandbox
|
|
2742
|
+
// blocked `squadrant crew signal …` from reaching the daemon socket (which
|
|
2743
|
+
// lives outside the workspace) — breaking the done/blocked/failed
|
|
2744
|
+
// lifecycle. Codex's AF_UNIX-socket allowance has no stable config path
|
|
2745
|
+
// (it's gated behind the experimental_network feature), so the surgical
|
|
2746
|
+
// writable_roots escape is not viable. danger-full-access removes the FS
|
|
2747
|
+
// jail so signals work; approvalPolicy still gates risky ops when set to
|
|
2748
|
+
// "untrusted" (the gate axis is independent of the sandbox axis).
|
|
2749
|
+
sandbox: "danger-full-access",
|
|
2750
|
+
approvalPolicy: rec.approvalPolicy ?? "never",
|
|
2751
|
+
developerInstructions: buildCodexDeveloperInstructions(rec)
|
|
2752
|
+
});
|
|
2753
|
+
this.threadByTask.set(rec.id, threadId);
|
|
2754
|
+
this.taskByThread.set(threadId, rec.id);
|
|
2755
|
+
this.deps.emit({ type: "task.session", id: rec.id, resumeRef: threadId });
|
|
2756
|
+
this.deps.emit({ type: "task.started", id: rec.id });
|
|
2757
|
+
} catch (e) {
|
|
2758
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2759
|
+
this.deps.emit({ type: "task.failed", id: rec.id, error: `handshake/start failed: ${msg}` });
|
|
2760
|
+
throw e;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
async say(taskId, text) {
|
|
2764
|
+
await this.dispatchByTask.get(taskId);
|
|
2765
|
+
const c = this.client;
|
|
2766
|
+
const tid = this.threadByTask.get(taskId);
|
|
2767
|
+
if (!tid)
|
|
2768
|
+
throw new Error(`no thread for task ${taskId}`);
|
|
2769
|
+
await c.sendTurn(tid, text);
|
|
2770
|
+
}
|
|
2771
|
+
async steer(taskId, text) {
|
|
2772
|
+
const c = this.client;
|
|
2773
|
+
const tid = this.threadByTask.get(taskId);
|
|
2774
|
+
if (!tid)
|
|
2775
|
+
throw new Error(`no thread for task ${taskId}`);
|
|
2776
|
+
await c.steerTurn(tid, text);
|
|
2777
|
+
}
|
|
2778
|
+
async interrupt(taskId) {
|
|
2779
|
+
const c = this.client;
|
|
2780
|
+
const tid = this.threadByTask.get(taskId);
|
|
2781
|
+
if (!tid)
|
|
2782
|
+
throw new Error(`no thread for task ${taskId}`);
|
|
2783
|
+
await c.interruptTurn(tid);
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Tear down a task's thread when its crew closes. Squadrant runs ONE shared
|
|
2787
|
+
* app-server with a thread per crew; closing the cmux pane only kills the
|
|
2788
|
+
* `crew attach` renderer, so without this the thread — and the gitnexus/pay
|
|
2789
|
+
* MCP servers it spawned — leak forever (verified: ~53MB per orphaned crew).
|
|
2790
|
+
* Archiving the thread lets the app-server reap it and its MCP children.
|
|
2791
|
+
*/
|
|
2792
|
+
async close(taskId) {
|
|
2793
|
+
const tid = this.threadByTask.get(taskId);
|
|
2794
|
+
this.serverRequestByTask.delete(taskId);
|
|
2795
|
+
if (!tid)
|
|
2796
|
+
return;
|
|
2797
|
+
this.threadByTask.delete(taskId);
|
|
2798
|
+
this.taskByThread.delete(tid);
|
|
2799
|
+
try {
|
|
2800
|
+
await this.client?.archiveThread(tid);
|
|
2801
|
+
} catch {
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
/** Kill the long-lived app-server child process on daemon shutdown. */
|
|
2805
|
+
stop() {
|
|
2806
|
+
this.client?.kill();
|
|
2807
|
+
}
|
|
2808
|
+
async answer(taskId, payload) {
|
|
2809
|
+
const c = this.client;
|
|
2810
|
+
const rec = this.serverRequestByTask.get(taskId);
|
|
2811
|
+
if (rec == null)
|
|
2812
|
+
throw new Error(`no pending server-request for task ${taskId}`);
|
|
2813
|
+
c.respondToServerRequest(rec.id, this.mapAnswerPayload(payload, rec.method));
|
|
2814
|
+
this.serverRequestByTask.delete(taskId);
|
|
2815
|
+
}
|
|
2816
|
+
/**
|
|
2817
|
+
* Map the captain-facing payload ({text, decision}) to the response shape
|
|
2818
|
+
* the codex app-server expects for the specific request method.
|
|
2819
|
+
*
|
|
2820
|
+
* Old protocol (applyPatchApproval / execCommandApproval):
|
|
2821
|
+
* { decision: ReviewDecision } where ReviewDecision = "approved" | "denied" | …
|
|
2822
|
+
*
|
|
2823
|
+
* v2 protocol (item/commandExecution/requestApproval / item/fileChange/requestApproval):
|
|
2824
|
+
* { decision: CommandExecutionApprovalDecision } where decision = "accept" | "decline" | …
|
|
2825
|
+
*
|
|
2826
|
+
* Non-approval requests (text input) pass through unchanged.
|
|
2827
|
+
*/
|
|
2828
|
+
mapAnswerPayload(payload, method) {
|
|
2829
|
+
if (typeof payload !== "object" || !payload)
|
|
2830
|
+
return payload;
|
|
2831
|
+
const p = payload;
|
|
2832
|
+
if (typeof p.decision !== "string")
|
|
2833
|
+
return payload;
|
|
2834
|
+
if (method === "applyPatchApproval" || method === "execCommandApproval") {
|
|
2835
|
+
const d = p.decision === "approve" ? "approved" : "denied";
|
|
2836
|
+
return { decision: d };
|
|
2837
|
+
}
|
|
2838
|
+
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") {
|
|
2839
|
+
const d = p.decision === "approve" ? "accept" : "decline";
|
|
2840
|
+
return { decision: d };
|
|
2841
|
+
}
|
|
2842
|
+
return { decision: p.decision };
|
|
2843
|
+
}
|
|
2844
|
+
async reattach(rec) {
|
|
2845
|
+
await this.ensureHandshake();
|
|
2846
|
+
const c = this.client;
|
|
2847
|
+
const resumeRef = rec.attempts.at(-1)?.resumeRef;
|
|
2848
|
+
if (!resumeRef)
|
|
2849
|
+
throw new Error(`reattach: no resumeRef on task ${rec.id}`);
|
|
2850
|
+
await c.resumeThread({ threadId: resumeRef, cwd: rec.cwd });
|
|
2851
|
+
this.threadByTask.set(rec.id, resumeRef);
|
|
2852
|
+
this.taskByThread.set(resumeRef, rec.id);
|
|
2853
|
+
this.deps.emit({ type: "task.reattached", id: rec.id });
|
|
2854
|
+
}
|
|
2855
|
+
onNotification(n) {
|
|
2856
|
+
const tid = n.params?.threadId ?? n.params?.thread_id;
|
|
2857
|
+
const taskId = tid ? this.taskByThread.get(tid) : void 0;
|
|
2858
|
+
if (!taskId)
|
|
2859
|
+
return;
|
|
2860
|
+
const ev = normalizeAppServerNotification(taskId, n);
|
|
2861
|
+
if (ev)
|
|
2862
|
+
this.deps.emit(ev);
|
|
2863
|
+
}
|
|
2864
|
+
onServerRequest(r) {
|
|
2865
|
+
const tid = r.params?.threadId ?? r.params?.thread_id;
|
|
2866
|
+
let taskId = tid ? this.taskByThread.get(tid) : void 0;
|
|
2867
|
+
if (!taskId && !tid) {
|
|
2868
|
+
if (this.taskByThread.size === 1) {
|
|
2869
|
+
taskId = this.taskByThread.values().next().value;
|
|
2870
|
+
} else {
|
|
2871
|
+
process.stderr.write(`[codex/driver] serverRequest ${r.method} dropped: no threadId and ${this.taskByThread.size} active tasks
|
|
2872
|
+
`);
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
if (!taskId)
|
|
2877
|
+
return;
|
|
2878
|
+
this.serverRequestByTask.set(taskId, { id: r.id, method: r.method });
|
|
2879
|
+
const isApproval = r.method.includes("Approval") || r.method.includes("approval");
|
|
2880
|
+
if (isApproval) {
|
|
2881
|
+
this.deps.emit({
|
|
2882
|
+
type: "task.approval.requested",
|
|
2883
|
+
id: taskId,
|
|
2884
|
+
requestId: r.id,
|
|
2885
|
+
question: String(r.params?.question ?? r.method),
|
|
2886
|
+
kind: r.method
|
|
2887
|
+
});
|
|
2888
|
+
} else {
|
|
2889
|
+
this.deps.emit({
|
|
2890
|
+
type: "task.input.requested",
|
|
2891
|
+
id: taskId,
|
|
2892
|
+
requestId: r.id,
|
|
2893
|
+
question: String(r.params?.question ?? r.method)
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
function buildCodexDeveloperInstructions(rec) {
|
|
2899
|
+
const directive = `You are squadrant crew task ${rec.id} in project ${rec.project}. When you finish, run EXACTLY: squadrant crew signal done --task-id ${rec.id} --project ${rec.project} --message "<one-line summary>". If you are blocked or fail, run squadrant crew signal blocked|failed with the same --task-id ${rec.id} --project ${rec.project} flags.`;
|
|
2900
|
+
return rec.roleInstructions ? `${rec.roleInstructions}
|
|
2901
|
+
|
|
2902
|
+
${directive}` : directive;
|
|
2903
|
+
}
|
|
2904
|
+
function withTimeout(p, ms, msg) {
|
|
2905
|
+
return new Promise((resolve2, reject) => {
|
|
2906
|
+
const t = setTimeout(() => reject(new Error(msg)), ms);
|
|
2907
|
+
p.then((v) => {
|
|
2908
|
+
clearTimeout(t);
|
|
2909
|
+
resolve2(v);
|
|
2910
|
+
}, (e) => {
|
|
2911
|
+
clearTimeout(t);
|
|
2912
|
+
reject(e);
|
|
2913
|
+
});
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// packages/agents/dist/opencode/sse-bridge.js
|
|
2918
|
+
var OpencodeSseBridge = class {
|
|
2919
|
+
controllers = /* @__PURE__ */ new Map();
|
|
2920
|
+
/** taskId → the crew's opencode server port (for permission-reply POSTs). */
|
|
2921
|
+
portByTask = /* @__PURE__ */ new Map();
|
|
2922
|
+
/** taskId → the last unresolved permission on the bus (for answer()). */
|
|
2923
|
+
pendingPermByTask = /* @__PURE__ */ new Map();
|
|
2924
|
+
/** Synthetic monotonic request id. opencode has no numeric id on the bus, but
|
|
2925
|
+
* task.approval.requested carries one (codex parity) to key gate promotion. */
|
|
2926
|
+
nextRequestId = 1;
|
|
2927
|
+
deps;
|
|
2928
|
+
constructor(deps) {
|
|
2929
|
+
this.deps = deps;
|
|
2930
|
+
}
|
|
2931
|
+
/** Begin subscribing to the crew's /event stream. Idempotent per task. */
|
|
2932
|
+
start(o) {
|
|
2933
|
+
if (this.controllers.has(o.taskId))
|
|
2934
|
+
return;
|
|
2935
|
+
this.portByTask.set(o.taskId, o.port);
|
|
2936
|
+
const ac = new AbortController();
|
|
2937
|
+
this.controllers.set(o.taskId, ac);
|
|
2938
|
+
void this.run(o.taskId, o.port, ac);
|
|
2939
|
+
}
|
|
2940
|
+
/** Stop subscribing for a task (crew closed / terminal). */
|
|
2941
|
+
stop(taskId) {
|
|
2942
|
+
const ac = this.controllers.get(taskId);
|
|
2943
|
+
if (ac) {
|
|
2944
|
+
ac.abort();
|
|
2945
|
+
this.controllers.delete(taskId);
|
|
2946
|
+
}
|
|
2947
|
+
this.portByTask.delete(taskId);
|
|
2948
|
+
this.pendingPermByTask.delete(taskId);
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Resolve a pending opencode permission by POSTing the captain's decision to
|
|
2952
|
+
* the crew's server (live-verified, opencode 1.15.13: POST
|
|
2953
|
+
* /session/{sessionID}/permissions/{permissionID} with
|
|
2954
|
+
* { response: "once" | "reject" } → 200, fires permission.replied). Mirrors
|
|
2955
|
+
* codex's driver.answer(). Returns true if there WAS a pending permission (so
|
|
2956
|
+
* the caller knows the answer was an approval, not a reply to a plain `signal
|
|
2957
|
+
* blocked` question); false if nothing was pending (already resolved on the
|
|
2958
|
+
* bus, or no gate).
|
|
2959
|
+
*/
|
|
2960
|
+
async answer(taskId, decision) {
|
|
2961
|
+
const pend = this.pendingPermByTask.get(taskId);
|
|
2962
|
+
const port = this.portByTask.get(taskId);
|
|
2963
|
+
if (!pend || port == null)
|
|
2964
|
+
return false;
|
|
2965
|
+
this.pendingPermByTask.delete(taskId);
|
|
2966
|
+
const fetchImpl = this.deps.fetchImpl ?? fetch;
|
|
2967
|
+
const response = decision === "approve" ? "once" : "reject";
|
|
2968
|
+
try {
|
|
2969
|
+
await fetchImpl(`http://127.0.0.1:${port}/session/${pend.sessionID}/permissions/${pend.permID}`, {
|
|
2970
|
+
method: "POST",
|
|
2971
|
+
headers: { "content-type": "application/json" },
|
|
2972
|
+
body: JSON.stringify({ response })
|
|
2973
|
+
});
|
|
2974
|
+
} catch (e) {
|
|
2975
|
+
this.deps.log?.(`opencode permission reply failed for ${taskId}: ${e.message}`);
|
|
2976
|
+
}
|
|
2977
|
+
this.deps.emit({ type: "task.started", id: taskId });
|
|
2978
|
+
return true;
|
|
2979
|
+
}
|
|
2980
|
+
async run(taskId, port, ac) {
|
|
2981
|
+
const fetchImpl = this.deps.fetchImpl ?? fetch;
|
|
2982
|
+
const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2983
|
+
const reconnectMs = this.deps.reconnectMs ?? 500;
|
|
2984
|
+
const maxBoot = this.deps.maxBootAttempts ?? 60;
|
|
2985
|
+
const url = `http://127.0.0.1:${port}/event`;
|
|
2986
|
+
let booted = false;
|
|
2987
|
+
let bootAttempts = 0;
|
|
2988
|
+
while (!ac.signal.aborted) {
|
|
2989
|
+
try {
|
|
2990
|
+
const res = await fetchImpl(url, {
|
|
2991
|
+
signal: ac.signal,
|
|
2992
|
+
headers: { accept: "text/event-stream" }
|
|
2993
|
+
});
|
|
2994
|
+
if (!res.ok || !res.body)
|
|
2995
|
+
throw new Error(`status ${res.status}`);
|
|
2996
|
+
booted = true;
|
|
2997
|
+
await this.consume(taskId, res.body, ac);
|
|
2998
|
+
break;
|
|
2999
|
+
} catch (e) {
|
|
3000
|
+
if (ac.signal.aborted)
|
|
3001
|
+
return;
|
|
3002
|
+
if (!booted) {
|
|
3003
|
+
bootAttempts++;
|
|
3004
|
+
if (bootAttempts >= maxBoot) {
|
|
3005
|
+
this.deps.log?.(`opencode SSE bridge: gave up connecting to ${url} after ${bootAttempts} attempts: ${e.message}`);
|
|
3006
|
+
this.controllers.delete(taskId);
|
|
3007
|
+
return;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
await sleep2(reconnectMs);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
this.controllers.delete(taskId);
|
|
3014
|
+
}
|
|
3015
|
+
async consume(taskId, body, ac) {
|
|
3016
|
+
const reader = body.getReader();
|
|
3017
|
+
const decoder = new TextDecoder();
|
|
3018
|
+
let buf = "";
|
|
3019
|
+
try {
|
|
3020
|
+
while (!ac.signal.aborted) {
|
|
3021
|
+
const { done, value } = await reader.read();
|
|
3022
|
+
if (done)
|
|
3023
|
+
return;
|
|
3024
|
+
buf += decoder.decode(value, { stream: true });
|
|
3025
|
+
let nl;
|
|
3026
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
3027
|
+
const line = buf.slice(0, nl);
|
|
3028
|
+
buf = buf.slice(nl + 1);
|
|
3029
|
+
this.handleLine(taskId, line);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
} finally {
|
|
3033
|
+
try {
|
|
3034
|
+
await reader.cancel();
|
|
3035
|
+
} catch {
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
handleLine(taskId, rawLine) {
|
|
3040
|
+
let line = rawLine.trim();
|
|
3041
|
+
if (!line)
|
|
3042
|
+
return;
|
|
3043
|
+
if (line.startsWith("data:"))
|
|
3044
|
+
line = line.slice(5).trim();
|
|
3045
|
+
if (!line.startsWith("{"))
|
|
3046
|
+
return;
|
|
3047
|
+
let json;
|
|
3048
|
+
try {
|
|
3049
|
+
json = JSON.parse(line);
|
|
3050
|
+
} catch {
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
if (json?.type === "session.idle") {
|
|
3054
|
+
this.deps.emit({
|
|
3055
|
+
type: "task.turn.completed",
|
|
3056
|
+
id: taskId,
|
|
3057
|
+
turnId: json.properties?.sessionID ?? "opencode"
|
|
3058
|
+
});
|
|
3059
|
+
} else if (json?.type === "permission.asked") {
|
|
3060
|
+
const p = json.properties;
|
|
3061
|
+
if (p?.id && p?.sessionID) {
|
|
3062
|
+
this.pendingPermByTask.set(taskId, { permID: p.id, sessionID: p.sessionID });
|
|
3063
|
+
const tool = p.permission ?? "a tool";
|
|
3064
|
+
const cmd = Array.isArray(p.patterns) && p.patterns.length ? `: ${p.patterns.join(" ")}` : "";
|
|
3065
|
+
this.deps.emit({
|
|
3066
|
+
type: "task.approval.requested",
|
|
3067
|
+
id: taskId,
|
|
3068
|
+
requestId: this.nextRequestId++,
|
|
3069
|
+
question: `opencode requests permission to run ${tool}${cmd}`,
|
|
3070
|
+
kind: tool
|
|
3071
|
+
});
|
|
3072
|
+
}
|
|
3073
|
+
} else if (json?.type === "permission.replied") {
|
|
3074
|
+
this.pendingPermByTask.delete(taskId);
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
// packages/agents/dist/interactive/claude.js
|
|
3080
|
+
import { execSync as execSync6 } from "child_process";
|
|
3081
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
3082
|
+
import { homedir as homedir7 } from "os";
|
|
3083
|
+
import { join as join12 } from "path";
|
|
3084
|
+
|
|
3085
|
+
// packages/agents/dist/headless/types.js
|
|
3086
|
+
var HEADLESS_ERROR_TAIL = 2e3;
|
|
3087
|
+
|
|
3088
|
+
// packages/agents/dist/headless/claude.js
|
|
3089
|
+
var claudeHeadless = {
|
|
3090
|
+
provider: "claude",
|
|
3091
|
+
buildCommand(task, sessionId) {
|
|
3092
|
+
const argv = ["claude", "-p", "--output-format", "json"];
|
|
3093
|
+
if (sessionId)
|
|
3094
|
+
argv.push("--resume", sessionId);
|
|
3095
|
+
argv.push(task);
|
|
3096
|
+
return argv;
|
|
3097
|
+
},
|
|
3098
|
+
parseResult(stdout, exitCode) {
|
|
3099
|
+
if (exitCode !== 0) {
|
|
3100
|
+
return { outcome: "failed", exitCode, error: stdout.slice(-HEADLESS_ERROR_TAIL) };
|
|
3101
|
+
}
|
|
3102
|
+
try {
|
|
3103
|
+
const j = JSON.parse(stdout);
|
|
3104
|
+
if (j.is_error)
|
|
3105
|
+
return { outcome: "failed", error: String(j.result ?? "is_error"), sessionId: j.session_id };
|
|
3106
|
+
const payload = typeof j.result === "string" ? j.result : j.result == null ? "" : JSON.stringify(j.result);
|
|
3107
|
+
return { outcome: "done", sessionId: j.session_id, payload };
|
|
3108
|
+
} catch {
|
|
3109
|
+
return { outcome: "done", parseWarning: true, payload: stdout };
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
// packages/agents/dist/headless/opencode.js
|
|
3115
|
+
var opencodeHeadless = {
|
|
3116
|
+
provider: "opencode",
|
|
3117
|
+
buildCommand(task, sessionId) {
|
|
3118
|
+
const argv = ["opencode", "run", "--format", "json"];
|
|
3119
|
+
if (sessionId)
|
|
3120
|
+
argv.push("--session", sessionId);
|
|
3121
|
+
argv.push(task);
|
|
3122
|
+
return argv;
|
|
3123
|
+
},
|
|
3124
|
+
parseResult(stdout, exitCode) {
|
|
3125
|
+
if (exitCode !== 0)
|
|
3126
|
+
return { outcome: "failed", exitCode, error: stdout.slice(-HEADLESS_ERROR_TAIL) };
|
|
3127
|
+
try {
|
|
3128
|
+
const j = JSON.parse(stdout);
|
|
3129
|
+
const payload = typeof j.result === "string" ? j.result : JSON.stringify(j.result ?? stdout);
|
|
3130
|
+
return { outcome: "done", sessionId: j.sessionID ?? j.session_id, payload };
|
|
3131
|
+
} catch {
|
|
3132
|
+
return { outcome: "done", parseWarning: true, payload: stdout };
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
};
|
|
3136
|
+
|
|
3137
|
+
// packages/agents/dist/headless/codex.js
|
|
3138
|
+
var codexHeadless = {
|
|
3139
|
+
provider: "codex",
|
|
3140
|
+
buildCommand(task, sessionId) {
|
|
3141
|
+
const opts = ["--json", "--skip-git-repo-check", "--sandbox", "workspace-write"];
|
|
3142
|
+
if (sessionId)
|
|
3143
|
+
return ["codex", "exec", "resume", sessionId, ...opts, task];
|
|
3144
|
+
return ["codex", "exec", ...opts, task];
|
|
3145
|
+
},
|
|
3146
|
+
parseResult(stdout, exitCode) {
|
|
3147
|
+
if (exitCode !== 0)
|
|
3148
|
+
return { outcome: "failed", exitCode, error: stdout.slice(-HEADLESS_ERROR_TAIL) };
|
|
3149
|
+
return { outcome: "done", payload: stdout };
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
|
|
3153
|
+
// packages/agents/dist/headless/registry.js
|
|
3154
|
+
var ADAPTERS = {
|
|
3155
|
+
claude: claudeHeadless,
|
|
3156
|
+
opencode: opencodeHeadless,
|
|
3157
|
+
codex: codexHeadless
|
|
3158
|
+
};
|
|
3159
|
+
function getHeadlessAdapter(provider) {
|
|
3160
|
+
const a = ADAPTERS[provider];
|
|
3161
|
+
if (!a)
|
|
3162
|
+
throw new Error(`no headless adapter for provider '${provider}'`);
|
|
3163
|
+
return a;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
// packages/agents/dist/headless-launcher.js
|
|
3167
|
+
var OUT_CAP = 4 * 1024 * 1024;
|
|
3168
|
+
var ERR_CAP = 4 * 1024 * 1024;
|
|
3169
|
+
var PROGRESS_INTERVAL_MS = 250;
|
|
3170
|
+
var PROGRESS_CHUNK_BATCH = 50;
|
|
3171
|
+
function runHeadless(opts) {
|
|
3172
|
+
const adapter = getHeadlessAdapter(opts.provider);
|
|
3173
|
+
const argv = adapter.buildCommand(opts.task, opts.sessionId);
|
|
3174
|
+
const child = opts.spawn(argv[0], argv.slice(1), {
|
|
3175
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3176
|
+
cwd: opts.cwd
|
|
3177
|
+
// undefined → inherit daemon cwd (back-compat)
|
|
3178
|
+
});
|
|
3179
|
+
opts.emit({ type: "task.started", id: opts.id, pid: child.pid ?? void 0 });
|
|
3180
|
+
let out = "";
|
|
3181
|
+
let err = "";
|
|
3182
|
+
let lastProgressAt = 0;
|
|
3183
|
+
let chunksSinceProgress = 0;
|
|
3184
|
+
let debounceTimer = null;
|
|
3185
|
+
function flushProgress() {
|
|
3186
|
+
if (debounceTimer) {
|
|
3187
|
+
clearTimeout(debounceTimer);
|
|
3188
|
+
debounceTimer = null;
|
|
3189
|
+
}
|
|
3190
|
+
lastProgressAt = Date.now();
|
|
3191
|
+
chunksSinceProgress = 0;
|
|
3192
|
+
opts.emit({ type: "task.progress", id: opts.id });
|
|
3193
|
+
}
|
|
3194
|
+
child.stdout?.on("data", (d) => {
|
|
3195
|
+
out += String(d);
|
|
3196
|
+
if (out.length > OUT_CAP)
|
|
3197
|
+
out = out.slice(out.length - OUT_CAP);
|
|
3198
|
+
chunksSinceProgress++;
|
|
3199
|
+
const now = Date.now();
|
|
3200
|
+
if (chunksSinceProgress >= PROGRESS_CHUNK_BATCH || now - lastProgressAt >= PROGRESS_INTERVAL_MS) {
|
|
3201
|
+
flushProgress();
|
|
3202
|
+
} else if (!debounceTimer) {
|
|
3203
|
+
const delay = PROGRESS_INTERVAL_MS - (now - lastProgressAt);
|
|
3204
|
+
debounceTimer = setTimeout(() => {
|
|
3205
|
+
debounceTimer = null;
|
|
3206
|
+
flushProgress();
|
|
3207
|
+
}, delay);
|
|
3208
|
+
}
|
|
3209
|
+
});
|
|
3210
|
+
child.stderr?.on("data", (d) => {
|
|
3211
|
+
err += String(d);
|
|
3212
|
+
if (err.length > ERR_CAP)
|
|
3213
|
+
err = err.slice(err.length - ERR_CAP);
|
|
3214
|
+
});
|
|
3215
|
+
const result = new Promise((resolve2) => {
|
|
3216
|
+
child.once("error", (e) => {
|
|
3217
|
+
if (debounceTimer) {
|
|
3218
|
+
clearTimeout(debounceTimer);
|
|
3219
|
+
debounceTimer = null;
|
|
3220
|
+
}
|
|
3221
|
+
opts.emit({ type: "task.failed", id: opts.id, error: `spawn error: ${e.message}`, exitCode: void 0 });
|
|
3222
|
+
resolve2();
|
|
3223
|
+
});
|
|
3224
|
+
child.on("close", (code) => {
|
|
3225
|
+
if (chunksSinceProgress > 0)
|
|
3226
|
+
flushProgress();
|
|
3227
|
+
else if (debounceTimer) {
|
|
3228
|
+
clearTimeout(debounceTimer);
|
|
3229
|
+
debounceTimer = null;
|
|
3230
|
+
}
|
|
3231
|
+
const parseInput = code !== 0 && err ? err : out || err;
|
|
3232
|
+
const res = adapter.parseResult(parseInput, code ?? 0);
|
|
3233
|
+
if (res.outcome === "failed") {
|
|
3234
|
+
opts.emit({ type: "task.failed", id: opts.id, error: res.error ?? "non-zero exit", exitCode: res.exitCode });
|
|
3235
|
+
} else {
|
|
3236
|
+
const ref = opts.writeResult ? opts.writeResult(opts.id, res.payload ?? "") : "";
|
|
3237
|
+
opts.emit({ type: "task.done", id: opts.id, resultRef: ref, parseWarning: res.parseWarning });
|
|
3238
|
+
}
|
|
3239
|
+
resolve2();
|
|
3240
|
+
});
|
|
3241
|
+
});
|
|
3242
|
+
return { result, kill: () => child.kill("SIGTERM") };
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
// packages/workspaces/dist/runtimes/cmux.js
|
|
3246
|
+
import { execFile, execFileSync as execFileSync4 } from "child_process";
|
|
3247
|
+
var CMUX_TIMEOUT = 15e3;
|
|
3248
|
+
var CmuxTimeoutError = class extends Error {
|
|
3249
|
+
constructor(cmd) {
|
|
3250
|
+
super(`cmux timeout after ${CMUX_TIMEOUT}ms on: ${cmd}`);
|
|
3251
|
+
this.name = "CmuxTimeoutError";
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
function cmux(args) {
|
|
3255
|
+
return new Promise((resolve2, reject) => {
|
|
3256
|
+
execFile(
|
|
3257
|
+
resolveCmuxBin(),
|
|
3258
|
+
args,
|
|
3259
|
+
// CMUX_QUIET=1 silences cmux 0.64's one-time deprecation hints (e.g. the
|
|
3260
|
+
// "list-workspaces is now an alias for cmux workspace list" notice). Those
|
|
3261
|
+
// notices print to the command's stdout and would otherwise pollute the
|
|
3262
|
+
// output we parse. Inherit the rest of the environment unchanged.
|
|
3263
|
+
{ encoding: "utf-8", timeout: CMUX_TIMEOUT, env: { ...process.env, CMUX_QUIET: "1" } },
|
|
3264
|
+
(err, stdout) => {
|
|
3265
|
+
if (err) {
|
|
3266
|
+
reject(err.code === "ETIMEDOUT" ? new CmuxTimeoutError(args.join(" ")) : err);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
resolve2(stdout.trim());
|
|
3270
|
+
}
|
|
3271
|
+
);
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
function parseList(output) {
|
|
3275
|
+
let parsed;
|
|
3276
|
+
try {
|
|
3277
|
+
parsed = JSON.parse(output);
|
|
3278
|
+
} catch {
|
|
3279
|
+
return [];
|
|
3280
|
+
}
|
|
3281
|
+
const refs = [];
|
|
3282
|
+
for (const ws of parsed.workspaces ?? []) {
|
|
3283
|
+
if (!ws.ref)
|
|
3284
|
+
continue;
|
|
3285
|
+
refs.push({
|
|
3286
|
+
id: ws.ref,
|
|
3287
|
+
name: ws.has_custom_title && ws.custom_title ? ws.custom_title : ws.current_directory ?? ws.ref,
|
|
3288
|
+
status: "running"
|
|
3289
|
+
});
|
|
3290
|
+
}
|
|
3291
|
+
return refs;
|
|
3292
|
+
}
|
|
3293
|
+
function sanitizeForCmuxSend(text) {
|
|
3294
|
+
return text.replace(/\\[nrt]/g, " ").replace(/[\n\r\t]+/g, " ").replace(/ {2,}/g, " ").trim();
|
|
3295
|
+
}
|
|
3296
|
+
function parseDraftFromScreen(screen) {
|
|
3297
|
+
if (!screen)
|
|
3298
|
+
return null;
|
|
3299
|
+
const lines = screen.split(/\r?\n/);
|
|
3300
|
+
const HR_RE = /^\s*─{10,}\s*$/;
|
|
3301
|
+
let bottomHR = -1;
|
|
3302
|
+
let topHR = -1;
|
|
3303
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3304
|
+
if (HR_RE.test(lines[i])) {
|
|
3305
|
+
if (bottomHR === -1) {
|
|
3306
|
+
bottomHR = i;
|
|
3307
|
+
} else {
|
|
3308
|
+
topHR = i;
|
|
3309
|
+
break;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (topHR === -1)
|
|
3314
|
+
return null;
|
|
3315
|
+
const inputLines = lines.slice(topHR + 1, bottomHR);
|
|
3316
|
+
for (const line of inputLines) {
|
|
3317
|
+
let extracted;
|
|
3318
|
+
const boxMatch = line.match(/│\s*[>❯]\s+(.*?)\s*│/);
|
|
3319
|
+
if (boxMatch) {
|
|
3320
|
+
extracted = boxMatch[1].trim();
|
|
3321
|
+
} else {
|
|
3322
|
+
const plainMatch = line.match(/^\s*[>❯]\s*(.*)$/);
|
|
3323
|
+
if (plainMatch)
|
|
3324
|
+
extracted = plainMatch[1].trim();
|
|
3325
|
+
}
|
|
3326
|
+
if (extracted !== void 0) {
|
|
3327
|
+
if (/^[▌█▔▎▏▌█]/.test(extracted))
|
|
3328
|
+
continue;
|
|
3329
|
+
const draft = extracted.replace(/\s*[▌█▔▎▏▌█]+\s*$/, "").trim();
|
|
3330
|
+
if (/^Press\s+(?:up|down|left|right|enter|escape|esc|tab|any\s+key|ctrl|shift|alt)\s+to\s+/i.test(draft))
|
|
3331
|
+
continue;
|
|
3332
|
+
if (draft)
|
|
3333
|
+
return draft;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
return "";
|
|
3337
|
+
}
|
|
3338
|
+
function readInputBoxRaw(screen) {
|
|
3339
|
+
if (!screen)
|
|
3340
|
+
return null;
|
|
3341
|
+
const lines = screen.split(/\r?\n/);
|
|
3342
|
+
const HR_RE = /^\s*─{10,}\s*$/;
|
|
3343
|
+
let bottomHR = -1;
|
|
3344
|
+
let topHR = -1;
|
|
3345
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3346
|
+
if (HR_RE.test(lines[i])) {
|
|
3347
|
+
if (bottomHR === -1)
|
|
3348
|
+
bottomHR = i;
|
|
3349
|
+
else {
|
|
3350
|
+
topHR = i;
|
|
3351
|
+
break;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
if (topHR === -1)
|
|
3356
|
+
return null;
|
|
3357
|
+
const parts = [];
|
|
3358
|
+
for (const line of lines.slice(topHR + 1, bottomHR)) {
|
|
3359
|
+
let s = line.replace(/│/g, " ");
|
|
3360
|
+
s = s.replace(/^\s*[>❯]\s?/, "");
|
|
3361
|
+
s = s.replace(/\s*[▌█▔▎▏]+\s*$/, "");
|
|
3362
|
+
parts.push(s);
|
|
3363
|
+
}
|
|
3364
|
+
return parts.join("").replace(/\s+$/, "");
|
|
3365
|
+
}
|
|
3366
|
+
function sendDebugEnabled() {
|
|
3367
|
+
return !!process.env.SQUADRANT_DEBUG_SEND;
|
|
3368
|
+
}
|
|
3369
|
+
function classifySendOutcome(payload, postBox) {
|
|
3370
|
+
if (postBox === null)
|
|
3371
|
+
return "box-gone";
|
|
3372
|
+
if (postBox === "")
|
|
3373
|
+
return "submitted";
|
|
3374
|
+
if (postBox === payload || postBox.includes(payload))
|
|
3375
|
+
return "stuck";
|
|
3376
|
+
return "unknown";
|
|
3377
|
+
}
|
|
3378
|
+
function createCmuxDriver() {
|
|
3379
|
+
return {
|
|
3380
|
+
name: "cmux",
|
|
3381
|
+
async probe() {
|
|
3382
|
+
try {
|
|
3383
|
+
const version = await cmux(["--version"]);
|
|
3384
|
+
const warn = checkToolCompat("cmux", version, compatManifest.tools.cmux);
|
|
3385
|
+
if (warn)
|
|
3386
|
+
process.stderr.write(`[squadrant] ${warn}
|
|
3387
|
+
`);
|
|
3388
|
+
return { installed: true, version };
|
|
3389
|
+
} catch {
|
|
3390
|
+
return { installed: false, version: "" };
|
|
3391
|
+
}
|
|
3392
|
+
},
|
|
3393
|
+
async list() {
|
|
3394
|
+
try {
|
|
3395
|
+
return parseList(await cmux(["workspace", "list", "--json", "--id-format", "refs"]));
|
|
3396
|
+
} catch {
|
|
3397
|
+
return [];
|
|
3398
|
+
}
|
|
3399
|
+
},
|
|
3400
|
+
async status(nameOrId) {
|
|
3401
|
+
const refs = await this.list();
|
|
3402
|
+
const hit = refs.find((r) => r.name === nameOrId || r.id === nameOrId);
|
|
3403
|
+
return hit ?? null;
|
|
3404
|
+
},
|
|
3405
|
+
async spawn(opts) {
|
|
3406
|
+
const newWorkspaceArgs = ["workspace", "create", "--command", opts.command];
|
|
3407
|
+
if (opts.workdir)
|
|
3408
|
+
newWorkspaceArgs.push("--cwd", opts.workdir);
|
|
3409
|
+
const output = await cmux(newWorkspaceArgs);
|
|
3410
|
+
const id = output.match(/workspace:\d+/)?.[0] || output.split(/\s+/).pop() || "";
|
|
3411
|
+
if (!id) {
|
|
3412
|
+
throw new Error(`cmux spawn did not return a workspace id: ${output}`);
|
|
3413
|
+
}
|
|
3414
|
+
await cmux(["workspace", "rename", id, "--title", opts.name]);
|
|
3415
|
+
let initialSurface;
|
|
3416
|
+
try {
|
|
3417
|
+
const tree = await cmux(["tree", "--workspace", id, "--id-format", "refs"]);
|
|
3418
|
+
const m = tree.match(/surface\s+(surface:\d+)\s+\[\w+\]\s+"([^"]*)"/);
|
|
3419
|
+
if (m) {
|
|
3420
|
+
initialSurface = m[1];
|
|
3421
|
+
await cmux(["rename-tab", "--workspace", id, "--surface", m[1], opts.name]);
|
|
3422
|
+
}
|
|
3423
|
+
} catch {
|
|
3424
|
+
}
|
|
3425
|
+
if (opts.pinToTop) {
|
|
3426
|
+
try {
|
|
3427
|
+
await cmux(["workspace-action", "--workspace", id, "--action", "pin"]);
|
|
3428
|
+
} catch {
|
|
3429
|
+
}
|
|
3430
|
+
if (initialSurface) {
|
|
3431
|
+
try {
|
|
3432
|
+
await cmux(["tab-action", "--workspace", id, "--surface", initialSurface, "--action", "pin"]);
|
|
3433
|
+
} catch {
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
return { id, name: opts.name, status: "running" };
|
|
3438
|
+
},
|
|
3439
|
+
async send(ref, message) {
|
|
3440
|
+
const allRefs = await this.list();
|
|
3441
|
+
const ws = allRefs.find((r) => r.id === ref);
|
|
3442
|
+
if (ws) {
|
|
3443
|
+
try {
|
|
3444
|
+
const surfaces = await this.listSurfaces(ws.id);
|
|
3445
|
+
const target = surfaces.find((s) => s.title === ws.name);
|
|
3446
|
+
if (target) {
|
|
3447
|
+
await cmux(["send", "--workspace", ws.id, "--surface", target.surfaceId, sanitizeForCmuxSend(message)]);
|
|
3448
|
+
await cmux(["send-key", "--workspace", ws.id, "--surface", target.surfaceId, "Enter"]);
|
|
3449
|
+
return;
|
|
3450
|
+
}
|
|
3451
|
+
} catch {
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
await cmux(["send", "--workspace", ref, sanitizeForCmuxSend(message)]);
|
|
3455
|
+
await cmux(["send-key", "--workspace", ref, "Enter"]);
|
|
3456
|
+
},
|
|
3457
|
+
async sendKey(ref, key) {
|
|
3458
|
+
await cmux(["send-key", "--workspace", ref, key]);
|
|
3459
|
+
},
|
|
3460
|
+
async readScreen(ref) {
|
|
3461
|
+
try {
|
|
3462
|
+
return await cmux(["read-screen", "--workspace", ref]);
|
|
3463
|
+
} catch {
|
|
3464
|
+
return "";
|
|
3465
|
+
}
|
|
3466
|
+
},
|
|
3467
|
+
async stop(ref) {
|
|
3468
|
+
try {
|
|
3469
|
+
await cmux(["workspace-action", "--workspace", ref, "--action", "unpin"]);
|
|
3470
|
+
} catch {
|
|
3471
|
+
}
|
|
3472
|
+
try {
|
|
3473
|
+
await cmux(["workspace", "close", ref]);
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
},
|
|
3477
|
+
async newPane(opts) {
|
|
3478
|
+
const cmd = opts.direction === "tab" ? ["new-surface", "--type", "terminal", "--workspace", opts.workspaceId, "--focus", "false"] : ["new-pane", "--type", "terminal", "--direction", opts.direction, "--workspace", opts.workspaceId, "--focus", "false"];
|
|
3479
|
+
const output = await cmux(cmd);
|
|
3480
|
+
const surfaceId = output.match(/surface:\d+/)?.[0];
|
|
3481
|
+
if (!surfaceId) {
|
|
3482
|
+
const verb = opts.direction === "tab" ? "new-surface" : "new-pane";
|
|
3483
|
+
throw new Error(`cmux ${verb} did not return a surface id: ${output}`);
|
|
3484
|
+
}
|
|
3485
|
+
if (opts.title) {
|
|
3486
|
+
try {
|
|
3487
|
+
await cmux(["rename-tab", "--workspace", opts.workspaceId, "--surface", surfaceId, "--title", opts.title]);
|
|
3488
|
+
} catch {
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
return { workspaceId: opts.workspaceId, surfaceId };
|
|
3492
|
+
},
|
|
3493
|
+
async closePane(pane) {
|
|
3494
|
+
try {
|
|
3495
|
+
await cmux(["close-surface", "--workspace", pane.workspaceId, "--surface", pane.surfaceId]);
|
|
3496
|
+
} catch {
|
|
3497
|
+
}
|
|
3498
|
+
},
|
|
3499
|
+
async sendToPane(pane, message) {
|
|
3500
|
+
await cmux(["send", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, sanitizeForCmuxSend(message)]);
|
|
3501
|
+
await cmux(["send-key", "--workspace", pane.workspaceId, "--surface", pane.surfaceId, "Enter"]);
|
|
3502
|
+
},
|
|
3503
|
+
async readPaneScreen(pane) {
|
|
3504
|
+
try {
|
|
3505
|
+
return await cmux(["read-screen", "--workspace", pane.workspaceId, "--surface", pane.surfaceId]);
|
|
3506
|
+
} catch {
|
|
3507
|
+
return "";
|
|
3508
|
+
}
|
|
3509
|
+
},
|
|
3510
|
+
async spawnInjector(opts) {
|
|
3511
|
+
const wsId = opts.captainWorkspace.id;
|
|
3512
|
+
const focus = opts.placement === "visible" ? "true" : "false";
|
|
3513
|
+
const output = await cmux(["new-surface", "--type", "terminal", "--workspace", wsId, "--focus", focus]);
|
|
3514
|
+
const surfaceId = output.match(/surface:\d+/)?.[0];
|
|
3515
|
+
if (!surfaceId) {
|
|
3516
|
+
throw new Error(`cmux spawnInjector did not return a surface id: ${output}`);
|
|
3517
|
+
}
|
|
3518
|
+
if (opts.title) {
|
|
3519
|
+
try {
|
|
3520
|
+
await cmux(["rename-tab", "--workspace", wsId, "--surface", surfaceId, "--title", opts.title]);
|
|
3521
|
+
} catch {
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
await cmux(["send", "--workspace", wsId, "--surface", surfaceId, opts.command]);
|
|
3525
|
+
await cmux(["send-key", "--workspace", wsId, "--surface", surfaceId, "Enter"]);
|
|
3526
|
+
return { workspaceId: wsId, surfaceId, title: opts.title };
|
|
3527
|
+
},
|
|
3528
|
+
async sendToSurface(surface, text, opts) {
|
|
3529
|
+
const ws = surface.workspaceId;
|
|
3530
|
+
const sf = surface.surfaceId;
|
|
3531
|
+
const deliver = async () => {
|
|
3532
|
+
const dbg = sendDebugEnabled();
|
|
3533
|
+
let preBox = null;
|
|
3534
|
+
if (dbg) {
|
|
3535
|
+
try {
|
|
3536
|
+
preBox = readInputBoxRaw(await cmux(["read-screen", "--workspace", ws, "--surface", sf]));
|
|
3537
|
+
} catch {
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
const payload = sanitizeForCmuxSend(text);
|
|
3541
|
+
await cmux(["send", "--workspace", ws, "--surface", sf, payload]);
|
|
3542
|
+
await cmux(["send-key", "--workspace", ws, "--surface", sf, "Enter"]);
|
|
3543
|
+
if (dbg) {
|
|
3544
|
+
let postBox = null;
|
|
3545
|
+
try {
|
|
3546
|
+
postBox = readInputBoxRaw(await cmux(["read-screen", "--workspace", ws, "--surface", sf]));
|
|
3547
|
+
} catch {
|
|
3548
|
+
}
|
|
3549
|
+
const verdict = classifySendOutcome(payload, postBox);
|
|
3550
|
+
process.stderr.write(`[squadrant] send-debug ${JSON.stringify({ surface: sf, verdict, payload, preBox, postBox })}
|
|
3551
|
+
`);
|
|
3552
|
+
}
|
|
3553
|
+
};
|
|
3554
|
+
let screen = "";
|
|
3555
|
+
try {
|
|
3556
|
+
screen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
|
|
3557
|
+
} catch {
|
|
3558
|
+
}
|
|
3559
|
+
const draft = parseDraftFromScreen(screen);
|
|
3560
|
+
if (draft === null)
|
|
3561
|
+
throw new DeferDelivery(null);
|
|
3562
|
+
if (draft === "") {
|
|
3563
|
+
await deliver();
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
if (!opts?.probe)
|
|
3567
|
+
throw new DeferDelivery(draft);
|
|
3568
|
+
const before = readInputBoxRaw(screen);
|
|
3569
|
+
await cmux(["send-key", "--workspace", ws, "--surface", sf, "backspace"]);
|
|
3570
|
+
let afterScreen = "";
|
|
3571
|
+
try {
|
|
3572
|
+
afterScreen = await cmux(["read-screen", "--workspace", ws, "--surface", sf]);
|
|
3573
|
+
} catch {
|
|
3574
|
+
}
|
|
3575
|
+
const after = readInputBoxRaw(afterScreen);
|
|
3576
|
+
if (before !== null && after !== null && after.length > 0 && after === before.slice(0, -1)) {
|
|
3577
|
+
await cmux(["send", "--workspace", ws, "--surface", sf, before.slice(-1)]);
|
|
3578
|
+
throw new DeferDelivery(draft);
|
|
3579
|
+
}
|
|
3580
|
+
await deliver();
|
|
3581
|
+
},
|
|
3582
|
+
async listSurfaces(workspaceId) {
|
|
3583
|
+
let output;
|
|
3584
|
+
try {
|
|
3585
|
+
output = await cmux(["tree", "--workspace", workspaceId, "--json", "--id-format", "refs"]);
|
|
3586
|
+
} catch {
|
|
3587
|
+
return [];
|
|
3588
|
+
}
|
|
3589
|
+
let parsed;
|
|
3590
|
+
try {
|
|
3591
|
+
parsed = JSON.parse(output);
|
|
3592
|
+
} catch {
|
|
3593
|
+
return [];
|
|
3594
|
+
}
|
|
3595
|
+
const surfaces = [];
|
|
3596
|
+
for (const win of parsed.windows ?? []) {
|
|
3597
|
+
for (const ws of win.workspaces ?? []) {
|
|
3598
|
+
if (ws.ref !== workspaceId)
|
|
3599
|
+
continue;
|
|
3600
|
+
for (const pane of ws.panes ?? []) {
|
|
3601
|
+
for (const sf of pane.surfaces ?? []) {
|
|
3602
|
+
const ref = sf.ref ?? sf.surface_ref;
|
|
3603
|
+
if (ref)
|
|
3604
|
+
surfaces.push({ workspaceId, surfaceId: ref, title: sf.title ?? "" });
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
return surfaces;
|
|
3610
|
+
}
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// packages/workspaces/dist/notifiers/cmux.js
|
|
3615
|
+
import { execFileSync as execFileSync5, execSync as execSync7 } from "child_process";
|
|
3616
|
+
|
|
3617
|
+
// packages/workspaces/dist/workspaces/obsidian.js
|
|
3618
|
+
import fs9 from "fs/promises";
|
|
3619
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3620
|
+
import path12 from "path";
|
|
3621
|
+
|
|
3622
|
+
// packages/workspaces/dist/cmux/events-bridge.js
|
|
3623
|
+
import { spawn as nodeSpawn2 } from "child_process";
|
|
3624
|
+
function deriveRunState(eventName) {
|
|
3625
|
+
switch (eventName) {
|
|
3626
|
+
case "agent.hook.PreToolUse":
|
|
3627
|
+
case "agent.hook.UserPromptSubmit":
|
|
3628
|
+
return "working";
|
|
3629
|
+
case "agent.hook.Stop":
|
|
3630
|
+
return "idle";
|
|
3631
|
+
default:
|
|
3632
|
+
return null;
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
var CmuxEventsBridge = class {
|
|
3636
|
+
child = null;
|
|
3637
|
+
stopped = false;
|
|
3638
|
+
buf = "";
|
|
3639
|
+
deps;
|
|
3640
|
+
constructor(deps) {
|
|
3641
|
+
this.deps = deps;
|
|
3642
|
+
}
|
|
3643
|
+
/** Begin the subscription. Idempotent. */
|
|
3644
|
+
start() {
|
|
3645
|
+
if (this.child || this.stopped)
|
|
3646
|
+
return;
|
|
3647
|
+
void this.run();
|
|
3648
|
+
}
|
|
3649
|
+
/** Stop the subscription and kill the child (daemon shutdown). */
|
|
3650
|
+
stop() {
|
|
3651
|
+
this.stopped = true;
|
|
3652
|
+
const c = this.child;
|
|
3653
|
+
this.child = null;
|
|
3654
|
+
if (c) {
|
|
3655
|
+
try {
|
|
3656
|
+
c.kill();
|
|
3657
|
+
} catch {
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
async run() {
|
|
3662
|
+
const spawnImpl = this.deps.spawnImpl ?? ((bin2, args2) => nodeSpawn2(bin2, args2, { stdio: ["ignore", "pipe", "ignore"] }));
|
|
3663
|
+
const bin = this.deps.cmuxBin ?? resolveCmuxBin();
|
|
3664
|
+
const sleep2 = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
3665
|
+
const reconnectMs = this.deps.reconnectMs ?? 1e3;
|
|
3666
|
+
const args = [
|
|
3667
|
+
"events",
|
|
3668
|
+
"--reconnect",
|
|
3669
|
+
"--cursor-file",
|
|
3670
|
+
this.deps.cursorFile,
|
|
3671
|
+
"--category",
|
|
3672
|
+
"agent",
|
|
3673
|
+
"--no-heartbeat"
|
|
3674
|
+
];
|
|
3675
|
+
while (!this.stopped) {
|
|
3676
|
+
this.buf = "";
|
|
3677
|
+
let child;
|
|
3678
|
+
try {
|
|
3679
|
+
child = spawnImpl(bin, args);
|
|
3680
|
+
} catch (e) {
|
|
3681
|
+
this.deps.log?.(`cmux events spawn failed: ${e.message}`);
|
|
3682
|
+
if (this.deps.stopAfterFirstRun)
|
|
3683
|
+
return;
|
|
3684
|
+
await sleep2(reconnectMs);
|
|
3685
|
+
continue;
|
|
3686
|
+
}
|
|
3687
|
+
this.child = child;
|
|
3688
|
+
await new Promise((resolve2) => {
|
|
3689
|
+
let settled = false;
|
|
3690
|
+
const done = () => {
|
|
3691
|
+
if (!settled) {
|
|
3692
|
+
settled = true;
|
|
3693
|
+
resolve2();
|
|
3694
|
+
}
|
|
3695
|
+
};
|
|
3696
|
+
child.stdout?.on("data", (b) => this.onData(b));
|
|
3697
|
+
child.stdout?.on("end", done);
|
|
3698
|
+
child.on("exit", done);
|
|
3699
|
+
child.on("error", (err) => {
|
|
3700
|
+
this.deps.log?.(`cmux events child error: ${err.message}`);
|
|
3701
|
+
done();
|
|
3702
|
+
});
|
|
3703
|
+
});
|
|
3704
|
+
this.child = null;
|
|
3705
|
+
if (this.stopped || this.deps.stopAfterFirstRun)
|
|
3706
|
+
break;
|
|
3707
|
+
await sleep2(reconnectMs);
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
onData(chunk) {
|
|
3711
|
+
this.buf += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
3712
|
+
let nl;
|
|
3713
|
+
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
3714
|
+
const line = this.buf.slice(0, nl);
|
|
3715
|
+
this.buf = this.buf.slice(nl + 1);
|
|
3716
|
+
this.handleLine(line);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
handleLine(rawLine) {
|
|
3720
|
+
const line = rawLine.trim();
|
|
3721
|
+
if (!line || line[0] !== "{")
|
|
3722
|
+
return;
|
|
3723
|
+
let f;
|
|
3724
|
+
try {
|
|
3725
|
+
f = JSON.parse(line);
|
|
3726
|
+
} catch {
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
if (f?.type !== "event" || f.category !== "agent")
|
|
3730
|
+
return;
|
|
3731
|
+
const runState = f.name ? deriveRunState(f.name) : null;
|
|
3732
|
+
if (!runState)
|
|
3733
|
+
return;
|
|
3734
|
+
const p = f.payload ?? {};
|
|
3735
|
+
if (p.phase === "received")
|
|
3736
|
+
return;
|
|
3737
|
+
const rec = this.deps.resolve({
|
|
3738
|
+
cwd: p.cwd,
|
|
3739
|
+
source: p._source ?? f.source,
|
|
3740
|
+
sessionId: p.session_id
|
|
3741
|
+
});
|
|
3742
|
+
if (!rec)
|
|
3743
|
+
return;
|
|
3744
|
+
if (runState === "idle") {
|
|
3745
|
+
this.deps.emit({
|
|
3746
|
+
type: "task.turn.completed",
|
|
3747
|
+
id: rec.id,
|
|
3748
|
+
turnId: p.session_id ?? "cmux"
|
|
3749
|
+
});
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
this.deps.emit({ type: "task.progress", id: rec.id, note: f.name, tool: p.tool_name });
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
|
|
3756
|
+
// packages/workspaces/dist/cmux/daemon-cmux.js
|
|
3757
|
+
var DaemonCmux = class {
|
|
3758
|
+
driver;
|
|
3759
|
+
constructor(driver) {
|
|
3760
|
+
this.driver = driver;
|
|
3761
|
+
}
|
|
3762
|
+
async send(surface, text, opts) {
|
|
3763
|
+
try {
|
|
3764
|
+
await this.driver.sendToSurface(surface, text, opts);
|
|
3765
|
+
} catch (e) {
|
|
3766
|
+
if (e instanceof DeferDelivery)
|
|
3767
|
+
throw e;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
async listSurfaces(workspaceId) {
|
|
3771
|
+
try {
|
|
3772
|
+
return await this.driver.listSurfaces(workspaceId);
|
|
3773
|
+
} catch {
|
|
3774
|
+
return [];
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
async readScreen(ref) {
|
|
3778
|
+
try {
|
|
3779
|
+
return await this.driver.readScreen(ref);
|
|
3780
|
+
} catch {
|
|
3781
|
+
return null;
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
async readPaneScreen(pane) {
|
|
3785
|
+
try {
|
|
3786
|
+
return await this.driver.readPaneScreen(pane);
|
|
3787
|
+
} catch {
|
|
3788
|
+
return null;
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
async findWorkspaceId(name) {
|
|
3792
|
+
try {
|
|
3793
|
+
const ref = await this.driver.status(name);
|
|
3794
|
+
return ref?.id ?? null;
|
|
3795
|
+
} catch {
|
|
3796
|
+
return null;
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
async isAvailable() {
|
|
3800
|
+
try {
|
|
3801
|
+
await this.driver.listSurfaces("");
|
|
3802
|
+
return true;
|
|
3803
|
+
} catch {
|
|
3804
|
+
return false;
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
};
|
|
3808
|
+
|
|
3809
|
+
// packages/workspaces/dist/crew-pane.js
|
|
3810
|
+
import net from "net";
|
|
3811
|
+
|
|
3812
|
+
// packages/cli/src/control/squadrantd.ts
|
|
3813
|
+
var SELF_PATH2 = fileURLToPath3(import.meta.url);
|
|
3814
|
+
function readPkgVersion() {
|
|
3815
|
+
try {
|
|
3816
|
+
const pkgPath = join13(dirname5(SELF_PATH2), "..", "package.json");
|
|
3817
|
+
return JSON.parse(readFileSync9(pkgPath, "utf-8")).version ?? "unknown";
|
|
3818
|
+
} catch {
|
|
3819
|
+
return "unknown";
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
var PKG_VERSION = readPkgVersion();
|
|
3823
|
+
function startSquadrantd(opts = {}) {
|
|
3824
|
+
const ctx = buildContext(opts);
|
|
3825
|
+
const { stateRoot, store, log, spawn: spawn2, writeResult, inFlightHeadlessIds, activeHeadlessKills } = ctx;
|
|
3826
|
+
const { broadcast, schedulePromotion, cancelPromotionsFor } = createAttach(ctx);
|
|
3827
|
+
ctx.broadcast = broadcast;
|
|
3828
|
+
ctx.schedulePromotion = schedulePromotion;
|
|
3829
|
+
ctx.cancelPromotionsFor = cancelPromotionsFor;
|
|
3830
|
+
const codexDriver = opts.codexDriver ?? new CodexInteractiveDriver({
|
|
3831
|
+
emit: (ev) => {
|
|
3832
|
+
const found = ctx.store.listAll().find((r) => r.id === ev.id);
|
|
3833
|
+
if (!found) return;
|
|
3834
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: ev });
|
|
3835
|
+
if (ev.type === "task.delta")
|
|
3836
|
+
ctx.broadcast(ev.id, { type: "delta", taskId: ev.id, text: ev.chunk });
|
|
3837
|
+
else if (ev.type === "task.turn.started")
|
|
3838
|
+
ctx.broadcast(ev.id, { type: "turn-started", taskId: ev.id });
|
|
3839
|
+
else if (ev.type === "task.turn.completed")
|
|
3840
|
+
ctx.broadcast(ev.id, { type: "turn-completed", taskId: ev.id });
|
|
3841
|
+
else if (ev.type === "task.input.requested") {
|
|
3842
|
+
ctx.broadcast(ev.id, { type: "input-requested", taskId: ev.id, requestId: ev.requestId, question: ev.question });
|
|
3843
|
+
ctx.schedulePromotion(ev.id, ev.requestId, "input", ev.question);
|
|
3844
|
+
} else if (ev.type === "task.approval.requested") {
|
|
3845
|
+
ctx.broadcast(ev.id, { type: "approval-requested", taskId: ev.id, requestId: ev.requestId, question: ev.question, kind: ev.kind });
|
|
3846
|
+
ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
|
|
3847
|
+
} else if (ev.type === "task.reattached")
|
|
3848
|
+
ctx.broadcast(ev.id, { type: "reattached", taskId: ev.id });
|
|
3849
|
+
}
|
|
3850
|
+
});
|
|
3851
|
+
const opencodeBridge = opts.opencodeBridge ?? new OpencodeSseBridge({
|
|
3852
|
+
emit: (ev) => {
|
|
3853
|
+
const found = store.listAll().find((r) => r.id === ev.id);
|
|
3854
|
+
if (!found) return;
|
|
3855
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: ev });
|
|
3856
|
+
if (ev.type === "task.approval.requested")
|
|
3857
|
+
ctx.schedulePromotion(ev.id, ev.requestId, "approval", ev.question);
|
|
3858
|
+
},
|
|
3859
|
+
log
|
|
3860
|
+
});
|
|
3861
|
+
const cmuxEventsBridge = opts.cmuxEventsBridge ?? new CmuxEventsBridge({
|
|
3862
|
+
emit: (ev) => {
|
|
3863
|
+
const found = store.listAll().find((r) => r.id === ev.id);
|
|
3864
|
+
if (!found) return;
|
|
3865
|
+
void ctx.d.handle({ kind: "event", project: found.project, event: ev });
|
|
3866
|
+
},
|
|
3867
|
+
resolve: (hook) => {
|
|
3868
|
+
if (!hook.cwd) return void 0;
|
|
3869
|
+
return store.listAll().find(
|
|
3870
|
+
(r) => r.mode === "interactive" && !TERMINAL_STATES.has(r.state) && r.cwd === hook.cwd
|
|
3871
|
+
);
|
|
3872
|
+
},
|
|
3873
|
+
cursorFile: join13(stateRoot, "cmux-events.seq"),
|
|
3874
|
+
log
|
|
3875
|
+
});
|
|
3876
|
+
ctx.codexDriver = codexDriver;
|
|
3877
|
+
ctx.opencodeBridge = opencodeBridge;
|
|
3878
|
+
ctx.cmuxEventsBridge = cmuxEventsBridge;
|
|
3879
|
+
ctx.daemonCmux = opts.daemonCmux ?? (opts.makeDaemonCmux ?? (() => new DaemonCmux(createCmuxDriver())))();
|
|
3880
|
+
const launchHeadless = opts.launchHeadless ?? (async (rec) => {
|
|
3881
|
+
const ingest = (e) => void ctx.d.handle({ kind: "event", project: rec.project, event: e });
|
|
3882
|
+
const handle = runHeadless({
|
|
3883
|
+
provider: rec.provider,
|
|
3884
|
+
task: rec.task,
|
|
3885
|
+
id: rec.id,
|
|
3886
|
+
sessionId: rec.sessionId,
|
|
3887
|
+
cwd: rec.cwd,
|
|
3888
|
+
spawn: spawn2,
|
|
3889
|
+
emit: ingest,
|
|
3890
|
+
writeResult
|
|
3891
|
+
});
|
|
3892
|
+
inFlightHeadlessIds.add(rec.id);
|
|
3893
|
+
activeHeadlessKills.add(handle.kill);
|
|
3894
|
+
try {
|
|
3895
|
+
await handle.result;
|
|
3896
|
+
} finally {
|
|
3897
|
+
inFlightHeadlessIds.delete(rec.id);
|
|
3898
|
+
activeHeadlessKills.delete(handle.kill);
|
|
3899
|
+
}
|
|
3900
|
+
});
|
|
3901
|
+
return startDaemon(ctx, { ...opts, launchHeadless }, PKG_VERSION);
|
|
3902
|
+
}
|
|
3903
|
+
if (process.argv[1] && process.argv[1].endsWith("squadrantd.js")) {
|
|
3904
|
+
void (async () => {
|
|
3905
|
+
const arg = process.argv[2];
|
|
3906
|
+
if (arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") {
|
|
3907
|
+
process.stdout.write("squadrantd: launchd-managed daemon entry (no CLI args). Use `squadrant` for commands.\n");
|
|
3908
|
+
process.exit(0);
|
|
3909
|
+
}
|
|
3910
|
+
const sock = join13(homedir8(), ".config", "squadrant", "squadrant.sock");
|
|
3911
|
+
if (await isDaemonSocketLive(sock)) {
|
|
3912
|
+
process.stderr.write(`[squadrantd] refusing to start: a live daemon already owns ${sock}
|
|
3913
|
+
`);
|
|
3914
|
+
process.exit(0);
|
|
3915
|
+
}
|
|
3916
|
+
const h = startSquadrantd({ sweepMs: 3e4 });
|
|
3917
|
+
process.on("SIGTERM", () => {
|
|
3918
|
+
h.stop();
|
|
3919
|
+
process.exit(0);
|
|
3920
|
+
});
|
|
3921
|
+
})();
|
|
3922
|
+
}
|
|
3923
|
+
export {
|
|
3924
|
+
defaultIsPidAlive,
|
|
3925
|
+
discoverCaptainSurface,
|
|
3926
|
+
startSquadrantd
|
|
3927
|
+
};
|
|
3928
|
+
//# sourceMappingURL=squadrantd.js.map
|