sisyphi 0.1.21 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-KQBSC5KY.js +31 -0
- package/dist/chunk-KQBSC5KY.js.map +1 -0
- package/dist/{chunk-LTAW6OWS.js → chunk-YGBGKMTF.js} +31 -6
- package/dist/chunk-YGBGKMTF.js.map +1 -0
- package/dist/chunk-ZE2SKB4B.js +35 -0
- package/dist/chunk-ZE2SKB4B.js.map +1 -0
- package/dist/cli.js +638 -51
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +915 -289
- package/dist/daemon.js.map +1 -1
- package/dist/paths-FYYSBD27.js +58 -0
- package/dist/paths-FYYSBD27.js.map +1 -0
- package/dist/templates/CLAUDE.md +21 -20
- package/dist/templates/agent-plugin/agents/CLAUDE.md +2 -0
- package/dist/templates/agent-plugin/agents/debug.md +1 -0
- package/dist/templates/agent-plugin/agents/operator.md +1 -2
- package/dist/templates/agent-plugin/agents/plan.md +86 -55
- package/dist/templates/agent-plugin/agents/review-plan.md +1 -0
- package/dist/templates/agent-plugin/agents/spec-draft.md +1 -0
- package/dist/templates/agent-plugin/hooks/hooks.json +19 -1
- package/dist/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
- package/dist/templates/agent-plugin/hooks/require-submit.sh +24 -0
- package/dist/templates/agent-suffix.md +18 -0
- package/dist/templates/dashboard-claude.md +38 -0
- package/dist/templates/orchestrator-base.md +270 -0
- package/dist/templates/orchestrator-impl.md +116 -0
- package/dist/templates/orchestrator-planning.md +131 -0
- package/dist/templates/orchestrator-plugin/hooks/hooks.json +1 -15
- package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +1 -1
- package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +4 -16
- package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +22 -23
- package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +11 -11
- package/dist/tui.js +3236 -0
- package/dist/tui.js.map +1 -0
- package/package.json +5 -1
- package/templates/CLAUDE.md +21 -20
- package/templates/agent-plugin/agents/CLAUDE.md +2 -0
- package/templates/agent-plugin/agents/debug.md +1 -0
- package/templates/agent-plugin/agents/operator.md +1 -2
- package/templates/agent-plugin/agents/plan.md +86 -55
- package/templates/agent-plugin/agents/review-plan.md +1 -0
- package/templates/agent-plugin/agents/spec-draft.md +1 -0
- package/templates/agent-plugin/hooks/hooks.json +19 -1
- package/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
- package/templates/agent-plugin/hooks/require-submit.sh +24 -0
- package/templates/agent-suffix.md +18 -0
- package/templates/dashboard-claude.md +38 -0
- package/templates/orchestrator-base.md +270 -0
- package/templates/orchestrator-impl.md +116 -0
- package/templates/orchestrator-planning.md +131 -0
- package/templates/orchestrator-plugin/hooks/hooks.json +1 -15
- package/templates/orchestrator-plugin/skills/git-management/SKILL.md +1 -1
- package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +4 -16
- package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +22 -23
- package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +11 -11
- package/dist/chunk-LTAW6OWS.js.map +0 -1
- package/dist/templates/orchestrator-plugin/scripts/block-task.sh +0 -11
- package/dist/templates/orchestrator.md +0 -173
- package/templates/orchestrator-plugin/scripts/block-task.sh +0 -11
- package/templates/orchestrator.md +0 -173
package/dist/daemon.js
CHANGED
|
@@ -1,73 +1,53 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadConfig
|
|
4
|
+
} from "./chunk-KQBSC5KY.js";
|
|
2
5
|
import {
|
|
3
6
|
contextDir,
|
|
7
|
+
cycleLogPath,
|
|
4
8
|
daemonPidPath,
|
|
5
9
|
daemonUpdatingPath,
|
|
6
|
-
globalConfigPath,
|
|
7
10
|
globalDir,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
goalPath,
|
|
12
|
+
legacyLogsPath,
|
|
13
|
+
logsDir,
|
|
14
|
+
messagesDir,
|
|
11
15
|
projectOrchestratorPromptPath,
|
|
12
16
|
promptsDir,
|
|
13
17
|
reportFilePath,
|
|
14
18
|
reportsDir,
|
|
19
|
+
roadmapPath,
|
|
15
20
|
sessionDir,
|
|
16
21
|
sessionsDir,
|
|
22
|
+
snapshotDir,
|
|
23
|
+
snapshotsDir,
|
|
17
24
|
socketPath,
|
|
18
25
|
statePath,
|
|
19
26
|
worktreeBaseDir,
|
|
20
27
|
worktreeConfigPath
|
|
21
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-YGBGKMTF.js";
|
|
22
29
|
|
|
23
30
|
// src/daemon/index.ts
|
|
24
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
31
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
25
32
|
import { execSync as execSync4 } from "child_process";
|
|
26
33
|
import { setTimeout as sleep } from "timers/promises";
|
|
27
34
|
|
|
28
|
-
// src/shared/config.ts
|
|
29
|
-
import { readFileSync } from "fs";
|
|
30
|
-
var DEFAULT_CONFIG = {
|
|
31
|
-
pollIntervalMs: 5e3
|
|
32
|
-
};
|
|
33
|
-
function readJsonFile(filePath) {
|
|
34
|
-
try {
|
|
35
|
-
const content = readFileSync(filePath, "utf-8");
|
|
36
|
-
return JSON.parse(content);
|
|
37
|
-
} catch {
|
|
38
|
-
return {};
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function loadConfig(cwd) {
|
|
42
|
-
const global = readJsonFile(globalConfigPath());
|
|
43
|
-
const project = readJsonFile(projectConfigPath(cwd));
|
|
44
|
-
return { ...DEFAULT_CONFIG, ...global, ...project };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
35
|
// src/daemon/server.ts
|
|
48
36
|
import { createServer } from "net";
|
|
49
|
-
import { unlinkSync, existsSync as
|
|
50
|
-
import { join as
|
|
37
|
+
import { unlinkSync, existsSync as existsSync7, writeFileSync as writeFileSync4, readFileSync as readFileSync6, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
|
|
38
|
+
import { join as join5 } from "path";
|
|
51
39
|
|
|
52
40
|
// src/daemon/session-manager.ts
|
|
53
41
|
import { v4 as uuidv4 } from "uuid";
|
|
54
|
-
import { existsSync as
|
|
42
|
+
import { existsSync as existsSync6, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
|
|
55
43
|
|
|
56
44
|
// src/daemon/state.ts
|
|
57
45
|
import { randomUUID } from "crypto";
|
|
58
|
-
import { mkdirSync, readFileSync
|
|
46
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
59
47
|
import { dirname, join } from "path";
|
|
60
|
-
var
|
|
48
|
+
var ROADMAP_SEED = `---
|
|
61
49
|
description: >
|
|
62
|
-
Living document
|
|
63
|
-
---
|
|
64
|
-
`;
|
|
65
|
-
var LOGS_SEED = `---
|
|
66
|
-
description: >
|
|
67
|
-
Session memory. Record important observations, decisions, and findings here.
|
|
68
|
-
This is your persistent memory across cycles: things you tried, what
|
|
69
|
-
worked/failed, design decisions and their rationale, gotchas discovered during
|
|
70
|
-
implementation.
|
|
50
|
+
Living document tracking development phases and outstanding work.
|
|
71
51
|
---
|
|
72
52
|
`;
|
|
73
53
|
var CONTEXT_CLAUDE_MD = `# context/
|
|
@@ -95,29 +75,32 @@ function atomicWrite(filePath, data) {
|
|
|
95
75
|
writeFileSync(tmpPath, data, "utf-8");
|
|
96
76
|
renameSync(tmpPath, filePath);
|
|
97
77
|
}
|
|
98
|
-
function createSession(id, task, cwd, context) {
|
|
78
|
+
function createSession(id, task, cwd, context, name) {
|
|
99
79
|
const dir = sessionDir(cwd, id);
|
|
100
80
|
mkdirSync(dir, { recursive: true });
|
|
101
81
|
mkdirSync(contextDir(cwd, id), { recursive: true });
|
|
102
82
|
mkdirSync(promptsDir(cwd, id), { recursive: true });
|
|
103
|
-
writeFileSync(
|
|
104
|
-
|
|
83
|
+
writeFileSync(roadmapPath(cwd, id), ROADMAP_SEED, "utf-8");
|
|
84
|
+
mkdirSync(logsDir(cwd, id), { recursive: true });
|
|
85
|
+
writeFileSync(goalPath(cwd, id), task, "utf-8");
|
|
105
86
|
writeFileSync(join(contextDir(cwd, id), "CLAUDE.md"), CONTEXT_CLAUDE_MD, "utf-8");
|
|
106
87
|
const session = {
|
|
107
88
|
id,
|
|
89
|
+
...name ? { name } : {},
|
|
108
90
|
task,
|
|
109
91
|
...context ? { context } : {},
|
|
110
92
|
cwd,
|
|
111
93
|
status: "active",
|
|
112
94
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
113
95
|
agents: [],
|
|
114
|
-
orchestratorCycles: []
|
|
96
|
+
orchestratorCycles: [],
|
|
97
|
+
messages: []
|
|
115
98
|
};
|
|
116
99
|
atomicWrite(statePath(cwd, id), JSON.stringify(session, null, 2));
|
|
117
100
|
return session;
|
|
118
101
|
}
|
|
119
102
|
function getSession(cwd, sessionId) {
|
|
120
|
-
const content =
|
|
103
|
+
const content = readFileSync(statePath(cwd, sessionId), "utf-8");
|
|
121
104
|
return JSON.parse(content);
|
|
122
105
|
}
|
|
123
106
|
function saveSession(session) {
|
|
@@ -174,6 +157,23 @@ async function completeSession(cwd, sessionId, report) {
|
|
|
174
157
|
saveSession(session);
|
|
175
158
|
});
|
|
176
159
|
}
|
|
160
|
+
async function continueSession(cwd, sessionId) {
|
|
161
|
+
return withSessionLock(sessionId, () => {
|
|
162
|
+
const session = getSession(cwd, sessionId);
|
|
163
|
+
if (session.status !== "completed") {
|
|
164
|
+
throw new Error(`Session ${sessionId} is not completed (status: ${session.status})`);
|
|
165
|
+
}
|
|
166
|
+
session.status = "active";
|
|
167
|
+
session.completedAt = void 0;
|
|
168
|
+
session.completionReport = void 0;
|
|
169
|
+
const cycles = session.orchestratorCycles;
|
|
170
|
+
if (cycles.length > 0) {
|
|
171
|
+
cycles[cycles.length - 1].completedAt = void 0;
|
|
172
|
+
}
|
|
173
|
+
saveSession(session);
|
|
174
|
+
writeFileSync(roadmapPath(cwd, sessionId), "", "utf-8");
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
177
|
async function appendAgentReport(cwd, sessionId, agentId, entry) {
|
|
178
178
|
return withSessionLock(sessionId, () => {
|
|
179
179
|
const session = getSession(cwd, sessionId);
|
|
@@ -183,6 +183,18 @@ async function appendAgentReport(cwd, sessionId, agentId, entry) {
|
|
|
183
183
|
saveSession(session);
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
+
async function updateReportSummary(cwd, sessionId, agentId, filePath, summary) {
|
|
187
|
+
return withSessionLock(sessionId, () => {
|
|
188
|
+
const session = getSession(cwd, sessionId);
|
|
189
|
+
const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
|
|
190
|
+
if (!agent) return;
|
|
191
|
+
const report = agent.reports.find((r) => r.filePath === filePath);
|
|
192
|
+
if (report) {
|
|
193
|
+
report.summary = summary;
|
|
194
|
+
saveSession(session);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
186
198
|
async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId) {
|
|
187
199
|
return withSessionLock(sessionId, () => {
|
|
188
200
|
const session = getSession(cwd, sessionId);
|
|
@@ -191,7 +203,31 @@ async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId)
|
|
|
191
203
|
saveSession(session);
|
|
192
204
|
});
|
|
193
205
|
}
|
|
194
|
-
async function
|
|
206
|
+
async function drainMessages(cwd, sessionId, count) {
|
|
207
|
+
return withSessionLock(sessionId, () => {
|
|
208
|
+
const session = getSession(cwd, sessionId);
|
|
209
|
+
if (!session.messages || count <= 0) return;
|
|
210
|
+
session.messages = session.messages.slice(count);
|
|
211
|
+
saveSession(session);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async function appendMessage(cwd, sessionId, message) {
|
|
215
|
+
return withSessionLock(sessionId, () => {
|
|
216
|
+
const session = getSession(cwd, sessionId);
|
|
217
|
+
if (!session.messages) session.messages = [];
|
|
218
|
+
session.messages.push(message);
|
|
219
|
+
saveSession(session);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async function updateTask(cwd, sessionId, task) {
|
|
223
|
+
return withSessionLock(sessionId, () => {
|
|
224
|
+
const session = getSession(cwd, sessionId);
|
|
225
|
+
session.task = task;
|
|
226
|
+
saveSession(session);
|
|
227
|
+
writeFileSync(goalPath(cwd, sessionId), task, "utf-8");
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async function completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode) {
|
|
195
231
|
return withSessionLock(sessionId, () => {
|
|
196
232
|
const session = getSession(cwd, sessionId);
|
|
197
233
|
const cycles = session.orchestratorCycles;
|
|
@@ -199,13 +235,66 @@ async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
|
|
|
199
235
|
const cycle = cycles[cycles.length - 1];
|
|
200
236
|
cycle.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
201
237
|
if (nextPrompt) cycle.nextPrompt = nextPrompt;
|
|
238
|
+
if (mode) cycle.mode = mode;
|
|
202
239
|
saveSession(session);
|
|
203
240
|
});
|
|
204
241
|
}
|
|
242
|
+
function createSnapshot(cwd, sessionId, cycleNumber) {
|
|
243
|
+
const dir = snapshotDir(cwd, sessionId, cycleNumber);
|
|
244
|
+
mkdirSync(dir, { recursive: true });
|
|
245
|
+
copyFileSync(statePath(cwd, sessionId), join(dir, "state.json"));
|
|
246
|
+
const roadmap = roadmapPath(cwd, sessionId);
|
|
247
|
+
if (existsSync(roadmap)) copyFileSync(roadmap, join(dir, "roadmap.md"));
|
|
248
|
+
const ld = logsDir(cwd, sessionId);
|
|
249
|
+
if (existsSync(ld)) cpSync(ld, join(dir, "logs"), { recursive: true });
|
|
250
|
+
const legacyLogs = legacyLogsPath(cwd, sessionId);
|
|
251
|
+
if (existsSync(legacyLogs)) copyFileSync(legacyLogs, join(dir, "logs.md"));
|
|
252
|
+
}
|
|
253
|
+
async function restoreSnapshot(cwd, sessionId, toCycle) {
|
|
254
|
+
return withSessionLock(sessionId, () => {
|
|
255
|
+
const dir = snapshotDir(cwd, sessionId, toCycle);
|
|
256
|
+
if (!existsSync(dir)) throw new Error(`No snapshot found for cycle ${toCycle}`);
|
|
257
|
+
const snapshotState = readFileSync(join(dir, "state.json"), "utf-8");
|
|
258
|
+
const session = JSON.parse(snapshotState);
|
|
259
|
+
session.status = "paused";
|
|
260
|
+
session.completedAt = void 0;
|
|
261
|
+
session.completionReport = void 0;
|
|
262
|
+
session.tmuxSessionName = void 0;
|
|
263
|
+
session.tmuxWindowId = void 0;
|
|
264
|
+
atomicWrite(statePath(cwd, sessionId), JSON.stringify(session, null, 2));
|
|
265
|
+
const snapshotRoadmap = join(dir, "roadmap.md");
|
|
266
|
+
if (existsSync(snapshotRoadmap)) copyFileSync(snapshotRoadmap, roadmapPath(cwd, sessionId));
|
|
267
|
+
const snapshotLogsDir = join(dir, "logs");
|
|
268
|
+
if (existsSync(snapshotLogsDir)) {
|
|
269
|
+
const currentLogsDir = logsDir(cwd, sessionId);
|
|
270
|
+
if (existsSync(currentLogsDir)) rmSync(currentLogsDir, { recursive: true, force: true });
|
|
271
|
+
cpSync(snapshotLogsDir, currentLogsDir, { recursive: true });
|
|
272
|
+
} else {
|
|
273
|
+
const snapshotLogs = join(dir, "logs.md");
|
|
274
|
+
if (existsSync(snapshotLogs)) copyFileSync(snapshotLogs, legacyLogsPath(cwd, sessionId));
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function listSnapshots(cwd, sessionId) {
|
|
279
|
+
const dir = snapshotsDir(cwd, sessionId);
|
|
280
|
+
if (!existsSync(dir)) return [];
|
|
281
|
+
return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("cycle-")).map((e) => parseInt(e.name.replace("cycle-", ""), 10)).filter((n) => !isNaN(n)).sort((a, b) => a - b);
|
|
282
|
+
}
|
|
283
|
+
function deleteSnapshotsAfter(cwd, sessionId, afterCycle) {
|
|
284
|
+
const dir = snapshotsDir(cwd, sessionId);
|
|
285
|
+
if (!existsSync(dir)) return;
|
|
286
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
287
|
+
if (!entry.isDirectory() || !entry.name.startsWith("cycle-")) continue;
|
|
288
|
+
const num = parseInt(entry.name.replace("cycle-", ""), 10);
|
|
289
|
+
if (!isNaN(num) && num > afterCycle) {
|
|
290
|
+
rmSync(join(dir, entry.name), { recursive: true, force: true });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
205
294
|
|
|
206
295
|
// src/daemon/orchestrator.ts
|
|
207
|
-
import { existsSync, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
208
|
-
import { resolve } from "path";
|
|
296
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
297
|
+
import { resolve, join as join3 } from "path";
|
|
209
298
|
|
|
210
299
|
// src/daemon/colors.ts
|
|
211
300
|
var ORCHESTRATOR_COLOR = "yellow";
|
|
@@ -228,6 +317,140 @@ function resetColors(sessionId) {
|
|
|
228
317
|
sessionColorIndex.delete(sessionId);
|
|
229
318
|
}
|
|
230
319
|
|
|
320
|
+
// src/daemon/frontmatter.ts
|
|
321
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
322
|
+
import { homedir } from "os";
|
|
323
|
+
import { join as join2, basename } from "path";
|
|
324
|
+
function detectProvider(model) {
|
|
325
|
+
if (!model) return "anthropic";
|
|
326
|
+
if (/^(gpt-|codex-)/.test(model)) return "openai";
|
|
327
|
+
return "anthropic";
|
|
328
|
+
}
|
|
329
|
+
function parseAgentFrontmatter(content) {
|
|
330
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
331
|
+
if (!match) return {};
|
|
332
|
+
const block = match[1];
|
|
333
|
+
const fm = {};
|
|
334
|
+
const str = (key) => {
|
|
335
|
+
const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
336
|
+
return m ? m[1].trim() : void 0;
|
|
337
|
+
};
|
|
338
|
+
fm.name = str("name");
|
|
339
|
+
fm.model = str("model");
|
|
340
|
+
fm.color = str("color");
|
|
341
|
+
fm.description = str("description");
|
|
342
|
+
fm.permissionMode = str("permissionMode");
|
|
343
|
+
fm.effort = str("effort");
|
|
344
|
+
const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
345
|
+
if (skillsMatch) {
|
|
346
|
+
fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
|
|
347
|
+
}
|
|
348
|
+
return fm;
|
|
349
|
+
}
|
|
350
|
+
function extractAgentBody(content) {
|
|
351
|
+
const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
352
|
+
return match ? match[1].trim() : content.trim();
|
|
353
|
+
}
|
|
354
|
+
function findPluginInstallPath(namespace) {
|
|
355
|
+
try {
|
|
356
|
+
const registryPath2 = join2(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
357
|
+
const registry = JSON.parse(readFileSync2(registryPath2, "utf-8"));
|
|
358
|
+
for (const key of Object.keys(registry)) {
|
|
359
|
+
if (key.startsWith(`${namespace}@`)) {
|
|
360
|
+
return registry[key].installPath ?? null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} catch {
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function resolveAgentTypePath(agentType, pluginDir, cwd) {
|
|
368
|
+
if (!agentType) return null;
|
|
369
|
+
let namespace;
|
|
370
|
+
let name;
|
|
371
|
+
if (agentType.includes(":")) {
|
|
372
|
+
[namespace, name] = agentType.split(":", 2);
|
|
373
|
+
} else {
|
|
374
|
+
name = agentType;
|
|
375
|
+
}
|
|
376
|
+
const searchPaths = [];
|
|
377
|
+
if (namespace) {
|
|
378
|
+
searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
|
|
379
|
+
const installPath = findPluginInstallPath(namespace);
|
|
380
|
+
if (installPath) {
|
|
381
|
+
searchPaths.push(join2(installPath, "agents", `${name}.md`));
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
searchPaths.push(join2(cwd, ".claude", "agents", `${name}.md`));
|
|
385
|
+
searchPaths.push(join2(homedir(), ".claude", "agents", `${name}.md`));
|
|
386
|
+
searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
|
|
387
|
+
}
|
|
388
|
+
for (const path of searchPaths) {
|
|
389
|
+
if (existsSync2(path)) return path;
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
function discoverAgentTypes(pluginDir, cwd) {
|
|
394
|
+
const seen = /* @__PURE__ */ new Set();
|
|
395
|
+
const results = [];
|
|
396
|
+
function scanDir(dir, prefix, source) {
|
|
397
|
+
let files;
|
|
398
|
+
try {
|
|
399
|
+
files = readdirSync2(dir);
|
|
400
|
+
} catch {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
for (const file of files) {
|
|
404
|
+
if (!file.endsWith(".md") || file === "CLAUDE.md") continue;
|
|
405
|
+
const name = basename(file, ".md");
|
|
406
|
+
const qualifiedName = prefix ? `${prefix}:${name}` : name;
|
|
407
|
+
if (seen.has(qualifiedName)) continue;
|
|
408
|
+
seen.add(qualifiedName);
|
|
409
|
+
try {
|
|
410
|
+
const content = readFileSync2(join2(dir, file), "utf-8");
|
|
411
|
+
const fm = parseAgentFrontmatter(content);
|
|
412
|
+
results.push({ qualifiedName, source, description: fm.description, model: fm.model });
|
|
413
|
+
} catch {
|
|
414
|
+
results.push({ qualifiedName, source });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
scanDir(join2(cwd, ".claude", "agents"), null, "project");
|
|
419
|
+
scanDir(join2(homedir(), ".claude", "agents"), null, "user");
|
|
420
|
+
scanDir(join2(pluginDir, "agents"), "sisyphus", "bundled");
|
|
421
|
+
try {
|
|
422
|
+
const registryPath2 = join2(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
423
|
+
const registry = JSON.parse(readFileSync2(registryPath2, "utf-8"));
|
|
424
|
+
const pluginEntries = registry.plugins ?? registry;
|
|
425
|
+
for (const key of Object.keys(pluginEntries)) {
|
|
426
|
+
const atIdx = key.indexOf("@");
|
|
427
|
+
if (atIdx < 1) continue;
|
|
428
|
+
const namespace = key.slice(0, atIdx);
|
|
429
|
+
const entry = pluginEntries[key];
|
|
430
|
+
const installPath = Array.isArray(entry) ? entry[0]?.installPath : entry?.installPath;
|
|
431
|
+
if (installPath) {
|
|
432
|
+
scanDir(join2(installPath, "agents"), namespace, "plugin");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
438
|
+
}
|
|
439
|
+
function resolveAgentConfig(agentType, pluginDir, cwd) {
|
|
440
|
+
const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
|
|
441
|
+
if (!filePath) return null;
|
|
442
|
+
try {
|
|
443
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
444
|
+
return {
|
|
445
|
+
frontmatter: parseAgentFrontmatter(content),
|
|
446
|
+
body: extractAgentBody(content),
|
|
447
|
+
filePath
|
|
448
|
+
};
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
231
454
|
// src/daemon/tmux.ts
|
|
232
455
|
import { execSync } from "child_process";
|
|
233
456
|
var EXEC_ENV = {
|
|
@@ -239,7 +462,7 @@ function exec(cmd) {
|
|
|
239
462
|
}
|
|
240
463
|
function execSafe(cmd) {
|
|
241
464
|
try {
|
|
242
|
-
return
|
|
465
|
+
return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
243
466
|
} catch {
|
|
244
467
|
return null;
|
|
245
468
|
}
|
|
@@ -263,6 +486,24 @@ function killPane(paneTarget) {
|
|
|
263
486
|
function killWindow(windowTarget) {
|
|
264
487
|
execSafe(`tmux kill-window -t "${windowTarget}"`);
|
|
265
488
|
}
|
|
489
|
+
function createSession2(sessionName, windowName, cwd) {
|
|
490
|
+
exec(`tmux new-session -d -s "${sessionName}" -n "${windowName}" -c ${shellQuote(cwd)}`);
|
|
491
|
+
const windowId = exec(`tmux display-message -t "${sessionName}:${windowName}" -p "#{window_id}"`);
|
|
492
|
+
const initialPaneId = exec(`tmux display-message -t "${sessionName}:${windowName}" -p "#{pane_id}"`);
|
|
493
|
+
return { windowId, initialPaneId };
|
|
494
|
+
}
|
|
495
|
+
function paneExists(paneTarget) {
|
|
496
|
+
return execSafe(`tmux display-message -t "${paneTarget}" -p "#{pane_id}"`) !== null;
|
|
497
|
+
}
|
|
498
|
+
function sessionExists(sessionName) {
|
|
499
|
+
return execSafe(`tmux has-session -t "${sessionName}"`) !== null;
|
|
500
|
+
}
|
|
501
|
+
function killSession(sessionName) {
|
|
502
|
+
execSafe(`tmux kill-session -t "${sessionName}"`);
|
|
503
|
+
}
|
|
504
|
+
function setSessionOption(sessionName, option, value) {
|
|
505
|
+
execSafe(`tmux set-option -t "${sessionName}" ${option} ${shellQuote(value)}`);
|
|
506
|
+
}
|
|
266
507
|
function listPanes(windowTarget) {
|
|
267
508
|
const output = execSafe(`tmux list-panes -t "${windowTarget}" -F "#{pane_id} #{pane_pid}"`);
|
|
268
509
|
if (!output) return [];
|
|
@@ -330,135 +571,206 @@ function getOrchestratorPaneId(sessionId) {
|
|
|
330
571
|
function setOrchestratorPaneId(sessionId, paneId) {
|
|
331
572
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
332
573
|
}
|
|
333
|
-
function loadOrchestratorPrompt(cwd) {
|
|
574
|
+
function loadOrchestratorPrompt(cwd, mode) {
|
|
334
575
|
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
335
|
-
if (
|
|
576
|
+
if (existsSync3(projectPath)) {
|
|
336
577
|
return readFileSync3(projectPath, "utf-8");
|
|
337
578
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
579
|
+
const basePath = resolve(import.meta.dirname, "../templates/orchestrator-base.md");
|
|
580
|
+
const base = readFileSync3(basePath, "utf-8");
|
|
581
|
+
if (mode === "implementation") {
|
|
582
|
+
const implPath = resolve(import.meta.dirname, "../templates/orchestrator-impl.md");
|
|
583
|
+
return base + "\n\n" + readFileSync3(implPath, "utf-8");
|
|
584
|
+
}
|
|
585
|
+
const planningPath = resolve(import.meta.dirname, "../templates/orchestrator-planning.md");
|
|
586
|
+
return base + "\n\n" + readFileSync3(planningPath, "utf-8");
|
|
340
587
|
}
|
|
341
588
|
function formatStateForOrchestrator(session) {
|
|
342
|
-
const shortId = session.id.slice(0, 8);
|
|
343
589
|
const cycleNum = session.orchestratorCycles.length;
|
|
344
590
|
const ctxDir = contextDir(session.cwd, session.id);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
591
|
+
const roadmapFile = roadmapPath(session.cwd, session.id);
|
|
592
|
+
const logFile = cycleLogPath(session.cwd, session.id, cycleNum + 1);
|
|
593
|
+
let contextSection = "";
|
|
594
|
+
if (cycleNum === 0) {
|
|
595
|
+
if (session.context) {
|
|
596
|
+
contextSection = `
|
|
597
|
+
## Context
|
|
598
|
+
|
|
599
|
+
${session.context}
|
|
600
|
+
`;
|
|
601
|
+
}
|
|
349
602
|
} else {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
603
|
+
let ctxFiles = [];
|
|
604
|
+
if (existsSync3(ctxDir)) {
|
|
605
|
+
ctxFiles = readdirSync3(ctxDir).filter((f) => f !== "CLAUDE.md");
|
|
606
|
+
}
|
|
607
|
+
if (ctxFiles.length > 0) {
|
|
608
|
+
const ctxLines = ctxFiles.map((f) => `- ${join3(ctxDir, f)}`).join("\n");
|
|
609
|
+
contextSection = `
|
|
610
|
+
## Context
|
|
611
|
+
|
|
612
|
+
${ctxLines}
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
const messages = session.messages ?? [];
|
|
617
|
+
const messagesSection = messages.length > 0 ? "\n### Messages\n\n" + messages.map((m) => {
|
|
618
|
+
const sourceLabel = m.source.type === "agent" ? `agent:${m.source.agentId}` : m.source.type === "system" && m.source.detail ? `system:${m.source.detail}` : m.source.type;
|
|
619
|
+
const fileRef = m.filePath ? ` \u2192 ${m.filePath}` : "";
|
|
620
|
+
return `- [${sourceLabel} @ ${m.timestamp}] "${m.summary}"${fileRef}`;
|
|
621
|
+
}).join("\n") + "\n" : "";
|
|
622
|
+
let previousCyclesSection = "";
|
|
623
|
+
if (session.orchestratorCycles.length > 1) {
|
|
624
|
+
const previousCycles = session.orchestratorCycles.slice(0, -1);
|
|
625
|
+
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
626
|
+
const lines = previousCycles.map((c) => {
|
|
627
|
+
const agentDescs = c.agentsSpawned.map((id) => {
|
|
628
|
+
const agent = agentMap.get(id);
|
|
629
|
+
return agent ? `${id} (${agent.name})` : id;
|
|
630
|
+
}).join(", ");
|
|
631
|
+
return `Cycle ${c.cycle}: ${agentDescs || "(none)"}`;
|
|
363
632
|
});
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
633
|
+
previousCyclesSection = `
|
|
634
|
+
### Previous Cycles
|
|
635
|
+
|
|
636
|
+
${lines.join("\n")}
|
|
637
|
+
`;
|
|
638
|
+
}
|
|
639
|
+
let mostRecentCycleSection = "";
|
|
640
|
+
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
641
|
+
if (lastCycle && lastCycle.agentsSpawned.length > 0) {
|
|
642
|
+
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
643
|
+
const agentBlocks = lastCycle.agentsSpawned.map((id) => {
|
|
644
|
+
const agent = agentMap.get(id);
|
|
645
|
+
if (!agent) return `<agent-${id} status="unknown">
|
|
646
|
+
(no agent data)
|
|
647
|
+
</agent-${id}>`;
|
|
648
|
+
const finalReport = agent.reports.find((r) => r.type === "final");
|
|
649
|
+
const reportToUse = finalReport ?? agent.reports[agent.reports.length - 1];
|
|
650
|
+
let reportContent = "(no reports)";
|
|
651
|
+
if (reportToUse) {
|
|
652
|
+
try {
|
|
653
|
+
reportContent = readFileSync3(reportToUse.filePath, "utf-8");
|
|
654
|
+
} catch {
|
|
655
|
+
reportContent = `(could not read report: ${reportToUse.filePath})`;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return `<agent-${id} name="${agent.name}" status="${agent.status}">
|
|
659
|
+
${reportContent}
|
|
660
|
+
</agent-${id}>`;
|
|
661
|
+
}).join("\n");
|
|
662
|
+
mostRecentCycleSection = `
|
|
663
|
+
### Most Recent Cycle
|
|
664
|
+
|
|
665
|
+
<last-cycle>
|
|
666
|
+
${agentBlocks}
|
|
667
|
+
</last-cycle>
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
const roadmapRef = existsSync3(roadmapFile) ? `@${roadmapFile}` : "(empty)";
|
|
370
671
|
const worktreeAgents = session.agents.filter((a) => a.worktreePath);
|
|
371
672
|
let worktreeSection = "";
|
|
372
|
-
if (worktreeAgents.length > 0) {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
673
|
+
if (worktreeAgents.length > 0 || existsSync3(worktreeConfigPath(session.cwd))) {
|
|
674
|
+
let wtLines = "";
|
|
675
|
+
if (worktreeAgents.length > 0) {
|
|
676
|
+
wtLines = "\n" + worktreeAgents.map((a) => {
|
|
677
|
+
if (a.mergeStatus === "conflict") {
|
|
678
|
+
return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
|
|
376
679
|
Branch: ${a.branchName}
|
|
377
680
|
Worktree: ${a.worktreePath}`;
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
681
|
+
}
|
|
682
|
+
if (a.mergeStatus === "no-changes") {
|
|
683
|
+
return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
|
|
684
|
+
}
|
|
685
|
+
const status = a.mergeStatus ?? "pending";
|
|
686
|
+
return `- ${a.id}: ${status} (branch ${a.branchName})`;
|
|
687
|
+
}).join("\n");
|
|
688
|
+
}
|
|
689
|
+
const worktreeHint = existsSync3(worktreeConfigPath(session.cwd)) ? "Worktree config active (`.sisyphus/worktree.json`). Use `--worktree` flag with `sisyphus spawn` to isolate agents in their own worktrees. Recommended for feature work, especially with potential file overlap." : "No worktree configuration found. If this session involves parallel work where agents may edit overlapping files, use the `git-management` skill to set up `.sisyphus/worktree.json` and enable worktree isolation.";
|
|
385
690
|
worktreeSection = `
|
|
386
691
|
|
|
387
|
-
## Worktrees
|
|
388
|
-
${wtLines}`;
|
|
389
|
-
}
|
|
390
|
-
const worktreeHint = existsSync(worktreeConfigPath(session.cwd)) ? "Worktree config active (`.sisyphus/worktree.json`). Use `--worktree` flag with `sisyphus spawn` to isolate agents in their own worktrees. Recommended for feature work, especially with potential file overlap." : "No worktree configuration found. If this session involves parallel work where agents may edit overlapping files, use the `git-management` skill to set up `.sisyphus/worktree.json` and enable worktree isolation.";
|
|
391
|
-
const contextSection = session.context ? `
|
|
392
|
-
## Background Context
|
|
393
|
-
${session.context}
|
|
394
|
-
` : "";
|
|
395
|
-
return `<state>
|
|
396
|
-
session: ${shortId} (cycle ${cycleNum})
|
|
397
|
-
task: ${session.task}
|
|
398
|
-
status: ${session.status}
|
|
399
|
-
${contextSection}
|
|
400
|
-
## Plan
|
|
401
|
-
${planRef}
|
|
402
|
-
|
|
403
|
-
## Logs
|
|
404
|
-
${logsRef}
|
|
692
|
+
## Git Worktrees
|
|
405
693
|
|
|
406
|
-
|
|
407
|
-
|
|
694
|
+
${worktreeHint}${wtLines}`;
|
|
695
|
+
}
|
|
696
|
+
const goalFile = goalPath(session.cwd, session.id);
|
|
697
|
+
const goalContent = existsSync3(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
|
|
698
|
+
return `## Goal
|
|
408
699
|
|
|
409
|
-
|
|
410
|
-
${
|
|
700
|
+
${goalContent}
|
|
701
|
+
${contextSection}${messagesSection}
|
|
702
|
+
### Cycle Log
|
|
411
703
|
|
|
412
|
-
|
|
413
|
-
${
|
|
704
|
+
Write your cycle summary to: ${logFile}
|
|
705
|
+
${previousCyclesSection}${mostRecentCycleSection}
|
|
706
|
+
## Roadmap
|
|
414
707
|
|
|
415
|
-
|
|
416
|
-
${
|
|
417
|
-
</state>`;
|
|
708
|
+
${roadmapRef}
|
|
709
|
+
${worktreeSection}`;
|
|
418
710
|
}
|
|
419
711
|
async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
420
712
|
const session = getSession(cwd, sessionId);
|
|
421
|
-
const
|
|
713
|
+
const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
|
|
714
|
+
const mode = lastCycle?.mode ?? "planning";
|
|
715
|
+
const basePrompt = loadOrchestratorPrompt(cwd, mode);
|
|
422
716
|
const formattedState = formatStateForOrchestrator(session);
|
|
717
|
+
const agentPluginPath = resolve(import.meta.dirname, "../templates/agent-plugin");
|
|
718
|
+
const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
|
|
719
|
+
agentTypes.push(
|
|
720
|
+
{ qualifiedName: "Explore", source: "bundled", model: "haiku", description: "Fast codebase exploration \u2014 find files, search code, answer questions about architecture. Use for research and context gathering." }
|
|
721
|
+
);
|
|
722
|
+
const agentTypeLines = agentTypes.length > 0 ? agentTypes.map((t) => {
|
|
723
|
+
const modelTag = t.model ? ` (${t.model})` : "";
|
|
724
|
+
const desc = t.description ? ` \u2014 ${t.description}` : "";
|
|
725
|
+
return `- \`${t.qualifiedName}\`${modelTag}${desc}`;
|
|
726
|
+
}).join("\n") : " (none)";
|
|
727
|
+
const systemPrompt = basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines);
|
|
423
728
|
const cycleNum = session.orchestratorCycles.length + 1;
|
|
424
729
|
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
425
|
-
writeFileSync2(promptFilePath,
|
|
730
|
+
writeFileSync2(promptFilePath, systemPrompt, "utf-8");
|
|
426
731
|
sessionWindowMap.set(sessionId, windowId);
|
|
732
|
+
const cliBin = resolve(import.meta.dirname, "cli.js");
|
|
733
|
+
const npmBinDir = resolve(import.meta.dirname, "../../.bin");
|
|
427
734
|
const envExports = [
|
|
428
735
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
429
|
-
`export SISYPHUS_AGENT_ID='orchestrator'
|
|
736
|
+
`export SISYPHUS_AGENT_ID='orchestrator'`,
|
|
737
|
+
`export SISYPHUS_CWD='${cwd}'`,
|
|
738
|
+
`export PATH="${npmBinDir}:$PATH"`
|
|
430
739
|
].join(" && ");
|
|
431
|
-
let userPrompt;
|
|
740
|
+
let userPrompt = formattedState;
|
|
432
741
|
if (message) {
|
|
433
|
-
userPrompt
|
|
742
|
+
userPrompt += `
|
|
743
|
+
|
|
744
|
+
## Continuation Instructions
|
|
434
745
|
|
|
435
746
|
The user resumed this session with new instructions: ${message}`;
|
|
436
747
|
} else {
|
|
437
|
-
const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
|
|
438
748
|
const storedPrompt = lastCycle?.nextPrompt;
|
|
439
|
-
|
|
440
|
-
|
|
749
|
+
const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
|
|
750
|
+
userPrompt += `
|
|
441
751
|
|
|
442
|
-
|
|
443
|
-
} else {
|
|
444
|
-
userPrompt = `${formattedState}
|
|
752
|
+
## Continuation Instructions
|
|
445
753
|
|
|
446
|
-
|
|
447
|
-
}
|
|
754
|
+
${continuationText}`;
|
|
448
755
|
}
|
|
449
756
|
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
450
757
|
writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
|
|
758
|
+
if (session.messages && session.messages.length > 0) {
|
|
759
|
+
await drainMessages(cwd, sessionId, session.messages.length);
|
|
760
|
+
}
|
|
451
761
|
const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
|
|
452
762
|
const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
|
|
453
|
-
const
|
|
763
|
+
const config = loadConfig(cwd);
|
|
764
|
+
const effort = config.orchestratorEffort ?? "high";
|
|
765
|
+
const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --settings "${settingsPath}" --plugin-dir "${pluginPath}" --name "sisyphus:orch-${sessionId.slice(0, 8)}-cycle-${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
454
766
|
const paneId = createPane(windowId, cwd, "left");
|
|
455
767
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
456
768
|
registerPane(paneId, sessionId, "orchestrator");
|
|
457
769
|
setPaneTitle(paneId, `Sisyphus`);
|
|
458
770
|
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
459
771
|
const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
|
|
460
|
-
const bannerCmd =
|
|
461
|
-
const notifyCmd = `
|
|
772
|
+
const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
773
|
+
const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
|
|
462
774
|
sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
|
|
463
775
|
await addOrchestratorCycle(cwd, sessionId, {
|
|
464
776
|
cycle: cycleNum,
|
|
@@ -474,7 +786,7 @@ function resolveOrchestratorPane(sessionId, cwd) {
|
|
|
474
786
|
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
475
787
|
return lastCycle?.paneId ?? void 0;
|
|
476
788
|
}
|
|
477
|
-
async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
789
|
+
async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
|
|
478
790
|
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
479
791
|
if (paneId) {
|
|
480
792
|
killPane(paneId);
|
|
@@ -483,7 +795,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
483
795
|
}
|
|
484
796
|
const windowId = sessionWindowMap.get(sessionId);
|
|
485
797
|
if (windowId) selectLayout(windowId);
|
|
486
|
-
await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
|
|
798
|
+
await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode);
|
|
487
799
|
const session = getSession(cwd, sessionId);
|
|
488
800
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
489
801
|
if (runningAgents.length === 0) {
|
|
@@ -502,13 +814,13 @@ function cleanupSessionMaps(sessionId) {
|
|
|
502
814
|
}
|
|
503
815
|
|
|
504
816
|
// src/daemon/agent.ts
|
|
505
|
-
import { readFileSync as
|
|
817
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5, existsSync as existsSync5 } from "fs";
|
|
506
818
|
import { resolve as resolve2 } from "path";
|
|
507
819
|
|
|
508
820
|
// src/daemon/worktree.ts
|
|
509
821
|
import { execSync as execSync2 } from "child_process";
|
|
510
|
-
import { existsSync as
|
|
511
|
-
import { dirname as
|
|
822
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
|
|
823
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
512
824
|
var EXEC_ENV2 = {
|
|
513
825
|
...process.env,
|
|
514
826
|
PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
|
|
@@ -536,10 +848,10 @@ function loadWorktreeConfig(cwd) {
|
|
|
536
848
|
}
|
|
537
849
|
function createWorktreeShell(cwd, sessionId, agentId) {
|
|
538
850
|
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
539
|
-
const worktreePath =
|
|
540
|
-
mkdirSync2(
|
|
851
|
+
const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
|
|
852
|
+
mkdirSync2(dirname3(worktreePath), { recursive: true });
|
|
541
853
|
execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
|
|
542
|
-
if (
|
|
854
|
+
if (existsSync4(worktreePath)) {
|
|
543
855
|
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
|
|
544
856
|
}
|
|
545
857
|
execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
|
|
@@ -550,16 +862,16 @@ function createWorktreeShell(cwd, sessionId, agentId) {
|
|
|
550
862
|
function bootstrapWorktree(cwd, worktreePath, config) {
|
|
551
863
|
if (config.copy) {
|
|
552
864
|
for (const entry of config.copy) {
|
|
553
|
-
const dest =
|
|
554
|
-
mkdirSync2(
|
|
555
|
-
execSafe2(`cp -r ${shellQuote2(
|
|
865
|
+
const dest = join4(worktreePath, entry);
|
|
866
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
867
|
+
execSafe2(`cp -r ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
|
|
556
868
|
}
|
|
557
869
|
}
|
|
558
870
|
if (config.clone) {
|
|
559
871
|
for (const entry of config.clone) {
|
|
560
|
-
const dest =
|
|
561
|
-
mkdirSync2(
|
|
562
|
-
const src = shellQuote2(
|
|
872
|
+
const dest = join4(worktreePath, entry);
|
|
873
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
874
|
+
const src = shellQuote2(join4(cwd, entry));
|
|
563
875
|
const dstQ = shellQuote2(dest);
|
|
564
876
|
if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
|
|
565
877
|
execSafe2(`cp -r ${src} ${dstQ}`);
|
|
@@ -568,9 +880,9 @@ function bootstrapWorktree(cwd, worktreePath, config) {
|
|
|
568
880
|
}
|
|
569
881
|
if (config.symlink) {
|
|
570
882
|
for (const entry of config.symlink) {
|
|
571
|
-
const dest =
|
|
572
|
-
mkdirSync2(
|
|
573
|
-
execSafe2(`ln -s ${shellQuote2(
|
|
883
|
+
const dest = join4(worktreePath, entry);
|
|
884
|
+
mkdirSync2(dirname3(dest), { recursive: true });
|
|
885
|
+
execSafe2(`ln -s ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
|
|
574
886
|
}
|
|
575
887
|
}
|
|
576
888
|
if (config.init) {
|
|
@@ -640,11 +952,11 @@ function mergeWorktrees(cwd, agents) {
|
|
|
640
952
|
function cleanupWorktree(cwd, worktreePath, branchName) {
|
|
641
953
|
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(worktreePath)} --force`);
|
|
642
954
|
execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
|
|
643
|
-
const baseDir =
|
|
955
|
+
const baseDir = dirname3(worktreePath);
|
|
644
956
|
try {
|
|
645
|
-
const entries =
|
|
957
|
+
const entries = readdirSync4(baseDir);
|
|
646
958
|
if (entries.length === 0) {
|
|
647
|
-
|
|
959
|
+
rmSync2(baseDir, { recursive: true });
|
|
648
960
|
}
|
|
649
961
|
} catch {
|
|
650
962
|
}
|
|
@@ -653,89 +965,37 @@ function countWorktreeAgents(agents) {
|
|
|
653
965
|
return agents.filter((a) => a.worktreePath && a.status === "running").length;
|
|
654
966
|
}
|
|
655
967
|
|
|
656
|
-
// src/daemon/
|
|
657
|
-
import {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
if (!model) return "anthropic";
|
|
662
|
-
if (/^(gpt-|codex-)/.test(model)) return "openai";
|
|
663
|
-
return "anthropic";
|
|
664
|
-
}
|
|
665
|
-
function parseAgentFrontmatter(content) {
|
|
666
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
667
|
-
if (!match) return {};
|
|
668
|
-
const block = match[1];
|
|
669
|
-
const fm = {};
|
|
670
|
-
const str = (key) => {
|
|
671
|
-
const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
672
|
-
return m ? m[1].trim() : void 0;
|
|
673
|
-
};
|
|
674
|
-
fm.name = str("name");
|
|
675
|
-
fm.model = str("model");
|
|
676
|
-
fm.color = str("color");
|
|
677
|
-
fm.description = str("description");
|
|
678
|
-
fm.permissionMode = str("permissionMode");
|
|
679
|
-
const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
680
|
-
if (skillsMatch) {
|
|
681
|
-
fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
|
|
682
|
-
}
|
|
683
|
-
return fm;
|
|
684
|
-
}
|
|
685
|
-
function extractAgentBody(content) {
|
|
686
|
-
const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
687
|
-
return match ? match[1].trim() : content.trim();
|
|
688
|
-
}
|
|
689
|
-
function findPluginInstallPath(namespace) {
|
|
968
|
+
// src/daemon/summarize.ts
|
|
969
|
+
import { query } from "@r-cli/sdk";
|
|
970
|
+
var disabled = false;
|
|
971
|
+
async function summarizeReport(reportText) {
|
|
972
|
+
if (disabled) return null;
|
|
690
973
|
try {
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
974
|
+
const session = await query({
|
|
975
|
+
prompt: `Summarize this agent work report in one concise sentence (max 120 chars). Focus on what was accomplished and the outcome. Output ONLY the summary sentence, nothing else.
|
|
976
|
+
|
|
977
|
+
${reportText.slice(0, 3e3)}`,
|
|
978
|
+
options: {
|
|
979
|
+
model: "haiku",
|
|
980
|
+
maxTurns: 1
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
let text = "";
|
|
984
|
+
for await (const msg of session) {
|
|
985
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
986
|
+
for (const block of msg.message.content) {
|
|
987
|
+
if (block.type === "text") text += block.text;
|
|
988
|
+
}
|
|
696
989
|
}
|
|
697
990
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
let name;
|
|
706
|
-
if (agentType.includes(":")) {
|
|
707
|
-
[namespace, name] = agentType.split(":", 2);
|
|
708
|
-
} else {
|
|
709
|
-
name = agentType;
|
|
710
|
-
}
|
|
711
|
-
const searchPaths = [];
|
|
712
|
-
if (namespace) {
|
|
713
|
-
searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
|
|
714
|
-
const installPath = findPluginInstallPath(namespace);
|
|
715
|
-
if (installPath) {
|
|
716
|
-
searchPaths.push(join3(installPath, "agents", `${name}.md`));
|
|
991
|
+
const summary = text.trim();
|
|
992
|
+
return summary.length > 0 ? summary : null;
|
|
993
|
+
} catch (err) {
|
|
994
|
+
console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
|
|
995
|
+
const status = err.status;
|
|
996
|
+
if (status === 401 || status === 403) {
|
|
997
|
+
disabled = true;
|
|
717
998
|
}
|
|
718
|
-
} else {
|
|
719
|
-
searchPaths.push(join3(cwd, ".claude", "agents", `${name}.md`));
|
|
720
|
-
searchPaths.push(join3(homedir(), ".claude", "agents", `${name}.md`));
|
|
721
|
-
searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
|
|
722
|
-
}
|
|
723
|
-
for (const path of searchPaths) {
|
|
724
|
-
if (existsSync3(path)) return path;
|
|
725
|
-
}
|
|
726
|
-
return null;
|
|
727
|
-
}
|
|
728
|
-
function resolveAgentConfig(agentType, pluginDir, cwd) {
|
|
729
|
-
const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
|
|
730
|
-
if (!filePath) return null;
|
|
731
|
-
try {
|
|
732
|
-
const content = readFileSync5(filePath, "utf-8");
|
|
733
|
-
return {
|
|
734
|
-
frontmatter: parseAgentFrontmatter(content),
|
|
735
|
-
body: extractAgentBody(content),
|
|
736
|
-
filePath
|
|
737
|
-
};
|
|
738
|
-
} catch {
|
|
739
999
|
return null;
|
|
740
1000
|
}
|
|
741
1001
|
}
|
|
@@ -757,7 +1017,7 @@ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
|
|
|
757
1017
|
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
758
1018
|
let template;
|
|
759
1019
|
try {
|
|
760
|
-
template =
|
|
1020
|
+
template = readFileSync5(templatePath, "utf-8");
|
|
761
1021
|
} catch {
|
|
762
1022
|
template = `# Sisyphus Agent
|
|
763
1023
|
Session: {{SESSION_ID}}
|
|
@@ -773,13 +1033,33 @@ Task: {{INSTRUCTION}}`;
|
|
|
773
1033
|
}
|
|
774
1034
|
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
|
|
775
1035
|
}
|
|
1036
|
+
function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
|
|
1037
|
+
const base = `${promptsDir(cwd, sessionId)}/${agentId}-plugin`;
|
|
1038
|
+
mkdirSync3(`${base}/.claude-plugin`, { recursive: true });
|
|
1039
|
+
mkdirSync3(`${base}/agents`, { recursive: true });
|
|
1040
|
+
mkdirSync3(`${base}/hooks`, { recursive: true });
|
|
1041
|
+
writeFileSync3(
|
|
1042
|
+
`${base}/.claude-plugin/plugin.json`,
|
|
1043
|
+
JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
|
|
1044
|
+
"utf-8"
|
|
1045
|
+
);
|
|
1046
|
+
if (agentConfig?.filePath && agentType && agentType !== "worker") {
|
|
1047
|
+
const shortName = agentType.replace(/^sisyphus:/, "");
|
|
1048
|
+
copyFileSync2(agentConfig.filePath, `${base}/agents/${shortName}.md`);
|
|
1049
|
+
}
|
|
1050
|
+
const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
|
|
1051
|
+
for (const f of ["hooks.json", "require-submit.sh", "intercept-send-message.sh"]) {
|
|
1052
|
+
copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
|
|
1053
|
+
}
|
|
1054
|
+
return base;
|
|
1055
|
+
}
|
|
776
1056
|
async function spawnAgent(opts) {
|
|
777
1057
|
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
778
1058
|
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
779
1059
|
agentCounters.set(sessionId, count);
|
|
780
1060
|
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
781
|
-
const
|
|
782
|
-
const agentConfig = resolveAgentConfig(agentType,
|
|
1061
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1062
|
+
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
783
1063
|
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
784
1064
|
const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
|
|
785
1065
|
let paneCwd = cwd;
|
|
@@ -805,13 +1085,17 @@ async function spawnAgent(opts) {
|
|
|
805
1085
|
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
806
1086
|
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
807
1087
|
const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
|
|
808
|
-
const bannerCmd =
|
|
1088
|
+
const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
1089
|
+
const cliBin = resolve2(import.meta.dirname, "cli.js");
|
|
1090
|
+
const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
|
|
809
1091
|
const envExports = [
|
|
810
1092
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
811
1093
|
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
812
|
-
|
|
1094
|
+
`export SISYPHUS_CWD='${cwd}'`,
|
|
1095
|
+
...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
|
|
1096
|
+
`export PATH="${npmBinDir}:$PATH"`
|
|
813
1097
|
].join(" && ");
|
|
814
|
-
const notifyCmd = `
|
|
1098
|
+
const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
|
|
815
1099
|
let mainCmd;
|
|
816
1100
|
if (provider === "openai") {
|
|
817
1101
|
const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
|
|
@@ -828,7 +1112,10 @@ ${instruction}`);
|
|
|
828
1112
|
mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
|
|
829
1113
|
} else {
|
|
830
1114
|
const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
|
|
831
|
-
|
|
1115
|
+
const config = loadConfig(cwd);
|
|
1116
|
+
const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
|
|
1117
|
+
const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
|
|
1118
|
+
mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote3(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
|
|
832
1119
|
}
|
|
833
1120
|
const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
|
|
834
1121
|
const agent = {
|
|
@@ -866,10 +1153,98 @@ ${instruction}`);
|
|
|
866
1153
|
}
|
|
867
1154
|
return agent;
|
|
868
1155
|
}
|
|
1156
|
+
async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
1157
|
+
const session = getSession(cwd, sessionId);
|
|
1158
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
1159
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}`);
|
|
1160
|
+
if (agent.status === "running") {
|
|
1161
|
+
const paneAlive = agent.paneId && paneExists(agent.paneId);
|
|
1162
|
+
if (paneAlive) {
|
|
1163
|
+
throw new Error(`Agent ${agentId} is already running`);
|
|
1164
|
+
}
|
|
1165
|
+
await updateAgent(cwd, sessionId, agentId, {
|
|
1166
|
+
status: "lost",
|
|
1167
|
+
killedReason: "pane disappeared (detected on restart)",
|
|
1168
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const { instruction, agentType, name, color } = agent;
|
|
1172
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1173
|
+
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
1174
|
+
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
1175
|
+
let paneCwd = cwd;
|
|
1176
|
+
let worktreeContext;
|
|
1177
|
+
if (agent.worktreePath) {
|
|
1178
|
+
paneCwd = agent.worktreePath;
|
|
1179
|
+
const portOffset = countWorktreeAgents(session.agents);
|
|
1180
|
+
worktreeContext = {
|
|
1181
|
+
offset: portOffset,
|
|
1182
|
+
total: portOffset,
|
|
1183
|
+
branchName: agent.branchName
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
if (agent.paneId) {
|
|
1187
|
+
try {
|
|
1188
|
+
killPane(agent.paneId);
|
|
1189
|
+
} catch {
|
|
1190
|
+
}
|
|
1191
|
+
unregisterAgentPane(sessionId, agentId);
|
|
1192
|
+
}
|
|
1193
|
+
const paneId = createPane(windowId, paneCwd);
|
|
1194
|
+
registerPane(paneId, sessionId, "agent", agentId);
|
|
1195
|
+
const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
|
|
1196
|
+
const paneLabel = shortType ? `${name}-${shortType}` : name;
|
|
1197
|
+
setPaneTitle(paneId, `${paneLabel} (${agentId})`);
|
|
1198
|
+
setPaneStyle(paneId, color);
|
|
1199
|
+
const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
|
|
1200
|
+
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
1201
|
+
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
1202
|
+
const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
|
|
1203
|
+
const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
1204
|
+
const cliBin = resolve2(import.meta.dirname, "cli.js");
|
|
1205
|
+
const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
|
|
1206
|
+
const envExports = [
|
|
1207
|
+
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
1208
|
+
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
1209
|
+
`export SISYPHUS_CWD='${cwd}'`,
|
|
1210
|
+
...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
|
|
1211
|
+
`export PATH="${npmBinDir}:$PATH"`
|
|
1212
|
+
].join(" && ");
|
|
1213
|
+
const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
|
|
1214
|
+
let mainCmd;
|
|
1215
|
+
if (provider === "openai") {
|
|
1216
|
+
const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
|
|
1217
|
+
const parts = [];
|
|
1218
|
+
if (agentConfig?.body) parts.push(agentConfig.body);
|
|
1219
|
+
parts.push(suffix);
|
|
1220
|
+
parts.push(`## Task
|
|
1221
|
+
|
|
1222
|
+
${instruction}`);
|
|
1223
|
+
writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
|
|
1224
|
+
const model = agentConfig?.frontmatter.model ?? "codex-mini";
|
|
1225
|
+
mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
|
|
1226
|
+
} else {
|
|
1227
|
+
const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
|
|
1228
|
+
const config = loadConfig(cwd);
|
|
1229
|
+
const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
|
|
1230
|
+
const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
|
|
1231
|
+
mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote3(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
|
|
1232
|
+
}
|
|
1233
|
+
const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
|
|
1234
|
+
await updateAgent(cwd, sessionId, agentId, {
|
|
1235
|
+
status: "running",
|
|
1236
|
+
paneId,
|
|
1237
|
+
provider,
|
|
1238
|
+
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1239
|
+
completedAt: null,
|
|
1240
|
+
killedReason: void 0
|
|
1241
|
+
});
|
|
1242
|
+
sendKeys(paneId, fullCmd);
|
|
1243
|
+
}
|
|
869
1244
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
870
1245
|
const dir = reportsDir(cwd, sessionId);
|
|
871
1246
|
try {
|
|
872
|
-
const files =
|
|
1247
|
+
const files = readdirSync5(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
873
1248
|
return String(files.length + 1).padStart(3, "0");
|
|
874
1249
|
} catch {
|
|
875
1250
|
return "001";
|
|
@@ -888,6 +1263,12 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
|
888
1263
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
889
1264
|
};
|
|
890
1265
|
await appendAgentReport(cwd, sessionId, agentId, entry);
|
|
1266
|
+
summarizeReport(content).then(async (aiSummary) => {
|
|
1267
|
+
if (aiSummary) {
|
|
1268
|
+
await updateReportSummary(cwd, sessionId, agentId, filePath, aiSummary);
|
|
1269
|
+
}
|
|
1270
|
+
}).catch(() => {
|
|
1271
|
+
});
|
|
891
1272
|
}
|
|
892
1273
|
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
893
1274
|
const dir = reportsDir(cwd, sessionId);
|
|
@@ -901,20 +1282,26 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
901
1282
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
902
1283
|
};
|
|
903
1284
|
await appendAgentReport(cwd, sessionId, agentId, entry);
|
|
1285
|
+
summarizeReport(report).then(async (aiSummary) => {
|
|
1286
|
+
if (aiSummary) {
|
|
1287
|
+
await updateReportSummary(cwd, sessionId, agentId, filePath, aiSummary);
|
|
1288
|
+
}
|
|
1289
|
+
}).catch(() => {
|
|
1290
|
+
});
|
|
904
1291
|
await updateAgent(cwd, sessionId, agentId, {
|
|
905
1292
|
status: "completed",
|
|
906
1293
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
907
1294
|
});
|
|
908
1295
|
const session = getSession(cwd, sessionId);
|
|
909
|
-
const
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1296
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
1297
|
+
if (agent?.paneId) {
|
|
1298
|
+
unregisterAgentPane(sessionId, agentId);
|
|
1299
|
+
try {
|
|
1300
|
+
killPane(agent.paneId);
|
|
1301
|
+
} catch {
|
|
1302
|
+
}
|
|
914
1303
|
}
|
|
915
|
-
|
|
916
|
-
if (windowId) selectLayout(windowId);
|
|
917
|
-
return allAgentsDone(session);
|
|
1304
|
+
return allAgentsDone(getSession(cwd, sessionId));
|
|
918
1305
|
}
|
|
919
1306
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
920
1307
|
unregisterAgentPane(sessionId, agentId);
|
|
@@ -1000,7 +1387,15 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
1000
1387
|
}
|
|
1001
1388
|
if (session.status !== "active") return;
|
|
1002
1389
|
const livePanes = listPanes(windowId);
|
|
1003
|
-
if (livePanes.length === 0)
|
|
1390
|
+
if (livePanes.length === 0) {
|
|
1391
|
+
const tracked = trackedSessions.get(sessionId);
|
|
1392
|
+
if (tracked && !sessionExists(tracked.tmuxSession)) {
|
|
1393
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
1394
|
+
untrackSession(sessionId);
|
|
1395
|
+
console.log(`[sisyphus] Session ${sessionId} paused: tmux session destroyed`);
|
|
1396
|
+
}
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1004
1399
|
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1005
1400
|
let paneRemoved = false;
|
|
1006
1401
|
for (const agent of session.agents) {
|
|
@@ -1030,23 +1425,34 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
1030
1425
|
}
|
|
1031
1426
|
|
|
1032
1427
|
// src/daemon/session-manager.ts
|
|
1033
|
-
|
|
1428
|
+
var NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
1429
|
+
async function startSession(task, cwd, context, name) {
|
|
1034
1430
|
const sessionId = uuidv4();
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1431
|
+
if (name && !NAME_PATTERN.test(name)) {
|
|
1432
|
+
throw new Error(`Invalid session name "${name}": only alphanumeric, hyphens, and underscores allowed`);
|
|
1433
|
+
}
|
|
1434
|
+
const tmuxName = `sisyphus-${name ?? sessionId.slice(0, 8)}`;
|
|
1435
|
+
if (sessionExists(tmuxName)) {
|
|
1436
|
+
throw new Error(`Tmux session "${tmuxName}" already exists. Choose a different name.`);
|
|
1437
|
+
}
|
|
1438
|
+
const session = createSession(sessionId, task, cwd, context, name);
|
|
1439
|
+
const { windowId, initialPaneId } = createSession2(tmuxName, "main", cwd);
|
|
1440
|
+
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1441
|
+
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
1442
|
+
trackSession(sessionId, cwd, tmuxName);
|
|
1038
1443
|
await spawnOrchestrator(sessionId, cwd, windowId);
|
|
1039
1444
|
updateTrackedWindow(sessionId, windowId);
|
|
1445
|
+
killPane(initialPaneId);
|
|
1040
1446
|
pruneOldSessions(cwd);
|
|
1041
|
-
return
|
|
1447
|
+
return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
|
|
1042
1448
|
}
|
|
1043
1449
|
var PRUNE_KEEP_COUNT = 10;
|
|
1044
1450
|
var PRUNE_KEEP_DAYS = 7;
|
|
1045
1451
|
function pruneOldSessions(cwd) {
|
|
1046
1452
|
try {
|
|
1047
1453
|
const dir = sessionsDir(cwd);
|
|
1048
|
-
if (!
|
|
1049
|
-
const entries =
|
|
1454
|
+
if (!existsSync6(dir)) return;
|
|
1455
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
1050
1456
|
const candidates = [];
|
|
1051
1457
|
for (const entry of entries) {
|
|
1052
1458
|
if (!entry.isDirectory()) continue;
|
|
@@ -1069,14 +1475,25 @@ function pruneOldSessions(cwd) {
|
|
|
1069
1475
|
}
|
|
1070
1476
|
for (const c of candidates) {
|
|
1071
1477
|
if (keep.has(c.id)) continue;
|
|
1072
|
-
|
|
1478
|
+
rmSync3(sessionDir(cwd, c.id), { recursive: true, force: true });
|
|
1073
1479
|
}
|
|
1074
1480
|
} catch (err) {
|
|
1075
1481
|
console.error("[sisyphus] Session pruning failed:", err);
|
|
1076
1482
|
}
|
|
1077
1483
|
}
|
|
1078
|
-
async function resumeSession(sessionId, cwd,
|
|
1484
|
+
async function resumeSession(sessionId, cwd, message) {
|
|
1079
1485
|
const session = getSession(cwd, sessionId);
|
|
1486
|
+
const tmuxName = session.tmuxSessionName ?? `sisyphus-${sessionId.slice(0, 8)}`;
|
|
1487
|
+
let windowId;
|
|
1488
|
+
if (sessionExists(tmuxName) && session.tmuxWindowId) {
|
|
1489
|
+
windowId = session.tmuxWindowId;
|
|
1490
|
+
} else {
|
|
1491
|
+
const created = createSession2(tmuxName, "main", cwd);
|
|
1492
|
+
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1493
|
+
windowId = created.windowId;
|
|
1494
|
+
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
1495
|
+
var initialPaneId = created.initialPaneId;
|
|
1496
|
+
}
|
|
1080
1497
|
if (session.status !== "active") {
|
|
1081
1498
|
const livePaneIds = /* @__PURE__ */ new Set();
|
|
1082
1499
|
if (session.tmuxWindowId) {
|
|
@@ -1099,12 +1516,16 @@ async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
|
|
|
1099
1516
|
}
|
|
1100
1517
|
}
|
|
1101
1518
|
await updateSessionStatus(cwd, sessionId, "active");
|
|
1102
|
-
await updateSessionTmux(cwd, sessionId,
|
|
1519
|
+
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
1103
1520
|
resetAgentCounterFromState(sessionId, session.agents);
|
|
1104
1521
|
resetColors(sessionId);
|
|
1105
|
-
|
|
1522
|
+
orchestratorDone.delete(sessionId);
|
|
1523
|
+
trackSession(sessionId, cwd, tmuxName);
|
|
1106
1524
|
await spawnOrchestrator(sessionId, cwd, windowId, message);
|
|
1107
1525
|
updateTrackedWindow(sessionId, windowId);
|
|
1526
|
+
if (initialPaneId) {
|
|
1527
|
+
killPane(initialPaneId);
|
|
1528
|
+
}
|
|
1108
1529
|
return getSession(cwd, sessionId);
|
|
1109
1530
|
}
|
|
1110
1531
|
function getSessionStatus(cwd, sessionId) {
|
|
@@ -1112,8 +1533,8 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
1112
1533
|
}
|
|
1113
1534
|
function listSessions(cwd) {
|
|
1114
1535
|
const dir = sessionsDir(cwd);
|
|
1115
|
-
if (!
|
|
1116
|
-
const entries =
|
|
1536
|
+
if (!existsSync6(dir)) return [];
|
|
1537
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
1117
1538
|
const sessions = [];
|
|
1118
1539
|
for (const entry of entries) {
|
|
1119
1540
|
if (!entry.isDirectory()) continue;
|
|
@@ -1121,10 +1542,13 @@ function listSessions(cwd) {
|
|
|
1121
1542
|
const session = getSession(cwd, entry.name);
|
|
1122
1543
|
sessions.push({
|
|
1123
1544
|
id: session.id,
|
|
1545
|
+
name: session.name,
|
|
1124
1546
|
task: session.task,
|
|
1125
1547
|
status: session.status,
|
|
1126
1548
|
createdAt: session.createdAt,
|
|
1127
|
-
agentCount: session.agents.length
|
|
1549
|
+
agentCount: session.agents.length,
|
|
1550
|
+
tmuxSessionName: session.tmuxSessionName,
|
|
1551
|
+
tmuxWindowId: session.tmuxWindowId
|
|
1128
1552
|
});
|
|
1129
1553
|
} catch (err) {
|
|
1130
1554
|
console.error(`[sisyphus] Failed to read session ${entry.name}:`, err);
|
|
@@ -1133,11 +1557,17 @@ function listSessions(cwd) {
|
|
|
1133
1557
|
return sessions;
|
|
1134
1558
|
}
|
|
1135
1559
|
var pendingRespawns = /* @__PURE__ */ new Set();
|
|
1560
|
+
var orchestratorDone = /* @__PURE__ */ new Set();
|
|
1136
1561
|
function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
1137
1562
|
if (pendingRespawns.has(sessionId)) return;
|
|
1563
|
+
if (!orchestratorDone.has(sessionId)) {
|
|
1564
|
+
console.log(`[sisyphus] All agents done for session ${sessionId}, waiting for orchestrator to yield`);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1138
1567
|
const session = getSession(cwd, sessionId);
|
|
1139
1568
|
if (session.status !== "active") return;
|
|
1140
1569
|
pendingRespawns.add(sessionId);
|
|
1570
|
+
orchestratorDone.delete(sessionId);
|
|
1141
1571
|
const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
|
|
1142
1572
|
if (worktreeAgents.length > 0) {
|
|
1143
1573
|
const results = mergeWorktrees(cwd, worktreeAgents);
|
|
@@ -1149,9 +1579,42 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1149
1579
|
}).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
|
|
1150
1580
|
}
|
|
1151
1581
|
}
|
|
1152
|
-
|
|
1582
|
+
const cycleNumber = session.orchestratorCycles.length;
|
|
1583
|
+
if (cycleNumber > 0) {
|
|
1584
|
+
createSnapshot(cwd, sessionId, cycleNumber);
|
|
1585
|
+
}
|
|
1586
|
+
setImmediate(async () => {
|
|
1153
1587
|
pendingRespawns.delete(sessionId);
|
|
1154
|
-
|
|
1588
|
+
try {
|
|
1589
|
+
const freshSession = getSession(cwd, sessionId);
|
|
1590
|
+
if (freshSession.status !== "active") return;
|
|
1591
|
+
let activeWindowId = windowId;
|
|
1592
|
+
const tmuxName = freshSession.tmuxSessionName;
|
|
1593
|
+
const needsRecreation = tmuxName && (!sessionExists(tmuxName) || listPanes(activeWindowId).length === 0);
|
|
1594
|
+
let initialPaneId;
|
|
1595
|
+
if (needsRecreation) {
|
|
1596
|
+
if (sessionExists(tmuxName)) {
|
|
1597
|
+
killSession(tmuxName);
|
|
1598
|
+
}
|
|
1599
|
+
const created = createSession2(tmuxName, "main", cwd);
|
|
1600
|
+
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1601
|
+
activeWindowId = created.windowId;
|
|
1602
|
+
initialPaneId = created.initialPaneId;
|
|
1603
|
+
await updateSessionTmux(cwd, sessionId, tmuxName, activeWindowId);
|
|
1604
|
+
trackSession(sessionId, cwd, tmuxName);
|
|
1605
|
+
}
|
|
1606
|
+
await spawnOrchestrator(sessionId, cwd, activeWindowId);
|
|
1607
|
+
updateTrackedWindow(sessionId, activeWindowId);
|
|
1608
|
+
if (initialPaneId) killPane(initialPaneId);
|
|
1609
|
+
for (const agent of freshSession.agents) {
|
|
1610
|
+
if (agent.status !== "running" && agent.paneId) {
|
|
1611
|
+
killPane(agent.paneId);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
selectLayout(activeWindowId);
|
|
1615
|
+
} catch (err) {
|
|
1616
|
+
console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err);
|
|
1617
|
+
}
|
|
1155
1618
|
});
|
|
1156
1619
|
}
|
|
1157
1620
|
async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
|
|
@@ -1183,16 +1646,17 @@ async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
|
|
|
1183
1646
|
async function handleReport(cwd, sessionId, agentId, content) {
|
|
1184
1647
|
await handleAgentReport(cwd, sessionId, agentId, content);
|
|
1185
1648
|
}
|
|
1186
|
-
async function handleYield(sessionId, cwd, nextPrompt) {
|
|
1649
|
+
async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
1187
1650
|
const pre = getSession(cwd, sessionId);
|
|
1188
1651
|
if (pre.status === "paused") {
|
|
1189
1652
|
await updateSessionStatus(cwd, sessionId, "active");
|
|
1190
1653
|
}
|
|
1191
|
-
await handleOrchestratorYield(sessionId, cwd, nextPrompt);
|
|
1654
|
+
await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
|
|
1655
|
+
orchestratorDone.add(sessionId);
|
|
1192
1656
|
const session = getSession(cwd, sessionId);
|
|
1193
1657
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1194
1658
|
if (!hasRunningAgents) {
|
|
1195
|
-
const windowId = getWindowId(sessionId);
|
|
1659
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1196
1660
|
if (windowId) {
|
|
1197
1661
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1198
1662
|
}
|
|
@@ -1201,6 +1665,9 @@ async function handleYield(sessionId, cwd, nextPrompt) {
|
|
|
1201
1665
|
async function handleComplete(sessionId, cwd, report) {
|
|
1202
1666
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
1203
1667
|
}
|
|
1668
|
+
async function handleContinue(sessionId, cwd) {
|
|
1669
|
+
await continueSession(cwd, sessionId);
|
|
1670
|
+
}
|
|
1204
1671
|
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
1205
1672
|
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
1206
1673
|
}
|
|
@@ -1230,12 +1697,81 @@ async function handleKill(sessionId, cwd) {
|
|
|
1230
1697
|
await updateSessionStatus(cwd, sessionId, "completed");
|
|
1231
1698
|
untrackSession(sessionId);
|
|
1232
1699
|
unregisterSessionPanes(sessionId);
|
|
1233
|
-
if (
|
|
1700
|
+
if (session.tmuxSessionName) {
|
|
1701
|
+
killSession(session.tmuxSessionName);
|
|
1702
|
+
} else if (windowId) {
|
|
1234
1703
|
killWindow(windowId);
|
|
1235
1704
|
}
|
|
1236
1705
|
clearAgentCounter(sessionId);
|
|
1706
|
+
orchestratorDone.delete(sessionId);
|
|
1237
1707
|
return killedAgents;
|
|
1238
1708
|
}
|
|
1709
|
+
async function handleRestartAgent(sessionId, cwd, agentId) {
|
|
1710
|
+
const session = getSession(cwd, sessionId);
|
|
1711
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
1712
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}`);
|
|
1713
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1714
|
+
if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
|
|
1715
|
+
await restartAgent(sessionId, cwd, agentId, windowId);
|
|
1716
|
+
}
|
|
1717
|
+
async function handleKillAgent(sessionId, cwd, agentId) {
|
|
1718
|
+
const session = getSession(cwd, sessionId);
|
|
1719
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
1720
|
+
if (!agent) throw new Error(`Unknown agent: ${agentId}`);
|
|
1721
|
+
if (agent.status !== "running") throw new Error(`Agent ${agentId} is not running (status: ${agent.status})`);
|
|
1722
|
+
unregisterAgentPane(sessionId, agentId);
|
|
1723
|
+
if (agent.paneId) {
|
|
1724
|
+
killPane(agent.paneId);
|
|
1725
|
+
}
|
|
1726
|
+
if (agent.worktreePath && agent.branchName) {
|
|
1727
|
+
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1728
|
+
}
|
|
1729
|
+
await updateAgent(cwd, sessionId, agentId, {
|
|
1730
|
+
status: "killed",
|
|
1731
|
+
killedReason: "killed by user",
|
|
1732
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
async function handleRollback(sessionId, cwd, toCycle) {
|
|
1736
|
+
const session = getSession(cwd, sessionId);
|
|
1737
|
+
if (toCycle < 1 || toCycle > session.orchestratorCycles.length) {
|
|
1738
|
+
const available2 = listSnapshots(cwd, sessionId);
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
`Invalid cycle ${toCycle}. Available snapshots: ${available2.length > 0 ? available2.join(", ") : "none"}`
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
const available = listSnapshots(cwd, sessionId);
|
|
1744
|
+
if (!available.includes(toCycle)) {
|
|
1745
|
+
throw new Error(
|
|
1746
|
+
`No snapshot for cycle ${toCycle}. Available snapshots: ${available.length > 0 ? available.join(", ") : "none"}`
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
for (const agent of session.agents) {
|
|
1750
|
+
if (agent.status === "running") {
|
|
1751
|
+
await updateAgent(cwd, sessionId, agent.id, {
|
|
1752
|
+
status: "killed",
|
|
1753
|
+
killedReason: "session rolled back",
|
|
1754
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
for (const agent of session.agents) {
|
|
1759
|
+
if (agent.worktreePath && agent.branchName) {
|
|
1760
|
+
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
1764
|
+
if (orchPaneId) {
|
|
1765
|
+
killPane(orchPaneId);
|
|
1766
|
+
}
|
|
1767
|
+
untrackSession(sessionId);
|
|
1768
|
+
unregisterSessionPanes(sessionId);
|
|
1769
|
+
clearAgentCounter(sessionId);
|
|
1770
|
+
orchestratorDone.delete(sessionId);
|
|
1771
|
+
await restoreSnapshot(cwd, sessionId, toCycle);
|
|
1772
|
+
deleteSnapshotsAfter(cwd, sessionId, toCycle);
|
|
1773
|
+
return { sessionId, restoredToCycle: toCycle };
|
|
1774
|
+
}
|
|
1239
1775
|
async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
1240
1776
|
const session = getSession(cwd, sessionId);
|
|
1241
1777
|
if (session.status !== "active") return;
|
|
@@ -1244,15 +1780,16 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
1244
1780
|
if (!agent || agent.status !== "running") return;
|
|
1245
1781
|
const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
|
|
1246
1782
|
if (allDone) {
|
|
1247
|
-
const windowId = getWindowId(sessionId);
|
|
1783
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1248
1784
|
if (windowId) {
|
|
1249
1785
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1250
1786
|
}
|
|
1251
1787
|
}
|
|
1252
1788
|
} else if (role === "orchestrator") {
|
|
1789
|
+
orchestratorDone.add(sessionId);
|
|
1253
1790
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1254
1791
|
if (!hasRunningAgents && session.agents.length > 0) {
|
|
1255
|
-
const windowId = getWindowId(sessionId);
|
|
1792
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1256
1793
|
if (windowId) {
|
|
1257
1794
|
console.log(`[sisyphus] Orchestrator pane exited for session ${sessionId}, all agents done \u2014 triggering respawn`);
|
|
1258
1795
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
@@ -1267,10 +1804,11 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
1267
1804
|
// src/daemon/server.ts
|
|
1268
1805
|
var server = null;
|
|
1269
1806
|
var sessionCwdMap = /* @__PURE__ */ new Map();
|
|
1807
|
+
var sessionMessageCounters = /* @__PURE__ */ new Map();
|
|
1270
1808
|
var sessionTmuxMap = /* @__PURE__ */ new Map();
|
|
1271
1809
|
var sessionWindowMap2 = /* @__PURE__ */ new Map();
|
|
1272
1810
|
function registryPath() {
|
|
1273
|
-
return
|
|
1811
|
+
return join5(globalDir(), "session-registry.json");
|
|
1274
1812
|
}
|
|
1275
1813
|
function persistSessionRegistry() {
|
|
1276
1814
|
const dir = globalDir();
|
|
@@ -1283,9 +1821,9 @@ function persistSessionRegistry() {
|
|
|
1283
1821
|
}
|
|
1284
1822
|
function loadSessionRegistry() {
|
|
1285
1823
|
const p = registryPath();
|
|
1286
|
-
if (!
|
|
1824
|
+
if (!existsSync7(p)) return {};
|
|
1287
1825
|
try {
|
|
1288
|
-
return JSON.parse(
|
|
1826
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
1289
1827
|
} catch {
|
|
1290
1828
|
return {};
|
|
1291
1829
|
}
|
|
@@ -1302,11 +1840,11 @@ async function handleRequest(req) {
|
|
|
1302
1840
|
try {
|
|
1303
1841
|
switch (req.type) {
|
|
1304
1842
|
case "start": {
|
|
1305
|
-
const session = await startSession(req.task, req.cwd, req.
|
|
1843
|
+
const session = await startSession(req.task, req.cwd, req.context, req.name);
|
|
1306
1844
|
registerSessionCwd(session.id, req.cwd);
|
|
1307
|
-
sessionTmuxMap.set(session.id,
|
|
1308
|
-
sessionWindowMap2.set(session.id,
|
|
1309
|
-
return { ok: true, data: { sessionId: session.id } };
|
|
1845
|
+
if (session.tmuxSessionName) sessionTmuxMap.set(session.id, session.tmuxSessionName);
|
|
1846
|
+
if (session.tmuxWindowId) sessionWindowMap2.set(session.id, session.tmuxWindowId);
|
|
1847
|
+
return { ok: true, data: { sessionId: session.id, tmuxSessionName: session.tmuxSessionName } };
|
|
1310
1848
|
}
|
|
1311
1849
|
case "spawn": {
|
|
1312
1850
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
@@ -1331,7 +1869,7 @@ async function handleRequest(req) {
|
|
|
1331
1869
|
case "yield": {
|
|
1332
1870
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1333
1871
|
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1334
|
-
await handleYield(req.sessionId, cwd, req.nextPrompt);
|
|
1872
|
+
await handleYield(req.sessionId, cwd, req.nextPrompt, req.mode);
|
|
1335
1873
|
return { ok: true };
|
|
1336
1874
|
}
|
|
1337
1875
|
case "complete": {
|
|
@@ -1340,9 +1878,15 @@ async function handleRequest(req) {
|
|
|
1340
1878
|
await handleComplete(req.sessionId, cwd, req.report);
|
|
1341
1879
|
return { ok: true };
|
|
1342
1880
|
}
|
|
1881
|
+
case "continue": {
|
|
1882
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1883
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1884
|
+
await handleContinue(req.sessionId, cwd);
|
|
1885
|
+
return { ok: true };
|
|
1886
|
+
}
|
|
1343
1887
|
case "status": {
|
|
1344
1888
|
if (req.sessionId) {
|
|
1345
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1889
|
+
const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
|
|
1346
1890
|
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1347
1891
|
const session = getSessionStatus(cwd, req.sessionId);
|
|
1348
1892
|
return { ok: true, data: { session } };
|
|
@@ -1379,17 +1923,17 @@ async function handleRequest(req) {
|
|
|
1379
1923
|
let cwd = sessionCwdMap.get(req.sessionId);
|
|
1380
1924
|
if (!cwd) {
|
|
1381
1925
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
1382
|
-
if (
|
|
1926
|
+
if (existsSync7(stateFile)) {
|
|
1383
1927
|
cwd = req.cwd;
|
|
1384
1928
|
registerSessionCwd(req.sessionId, cwd);
|
|
1385
1929
|
} else {
|
|
1386
1930
|
return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
|
|
1387
1931
|
}
|
|
1388
1932
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
return { ok: true, data: { sessionId: session.id, status: session.status } };
|
|
1933
|
+
const session = await resumeSession(req.sessionId, cwd, req.message);
|
|
1934
|
+
if (session.tmuxSessionName) sessionTmuxMap.set(req.sessionId, session.tmuxSessionName);
|
|
1935
|
+
if (session.tmuxWindowId) sessionWindowMap2.set(req.sessionId, session.tmuxWindowId);
|
|
1936
|
+
return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
|
|
1393
1937
|
}
|
|
1394
1938
|
case "register_claude_session": {
|
|
1395
1939
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
@@ -1407,6 +1951,49 @@ async function handleRequest(req) {
|
|
|
1407
1951
|
persistSessionRegistry();
|
|
1408
1952
|
return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
|
|
1409
1953
|
}
|
|
1954
|
+
case "kill-agent": {
|
|
1955
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1956
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1957
|
+
await handleKillAgent(req.sessionId, cwd, req.agentId);
|
|
1958
|
+
return { ok: true, data: { agentId: req.agentId } };
|
|
1959
|
+
}
|
|
1960
|
+
case "restart-agent": {
|
|
1961
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1962
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1963
|
+
await handleRestartAgent(req.sessionId, cwd, req.agentId);
|
|
1964
|
+
return { ok: true, data: { agentId: req.agentId } };
|
|
1965
|
+
}
|
|
1966
|
+
case "rollback": {
|
|
1967
|
+
let cwd = sessionCwdMap.get(req.sessionId);
|
|
1968
|
+
if (!cwd) {
|
|
1969
|
+
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
1970
|
+
if (existsSync7(stateFile)) {
|
|
1971
|
+
cwd = req.cwd;
|
|
1972
|
+
registerSessionCwd(req.sessionId, cwd);
|
|
1973
|
+
} else {
|
|
1974
|
+
return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const result = await handleRollback(req.sessionId, cwd, req.toCycle);
|
|
1978
|
+
return { ok: true, data: result };
|
|
1979
|
+
}
|
|
1980
|
+
case "delete": {
|
|
1981
|
+
const activeCwd = sessionCwdMap.get(req.sessionId);
|
|
1982
|
+
if (activeCwd) {
|
|
1983
|
+
try {
|
|
1984
|
+
await handleKill(req.sessionId, activeCwd);
|
|
1985
|
+
} catch {
|
|
1986
|
+
}
|
|
1987
|
+
sessionCwdMap.delete(req.sessionId);
|
|
1988
|
+
sessionTmuxMap.delete(req.sessionId);
|
|
1989
|
+
sessionWindowMap2.delete(req.sessionId);
|
|
1990
|
+
sessionMessageCounters.delete(req.sessionId);
|
|
1991
|
+
persistSessionRegistry();
|
|
1992
|
+
}
|
|
1993
|
+
const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
|
|
1994
|
+
rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
|
|
1995
|
+
return { ok: true };
|
|
1996
|
+
}
|
|
1410
1997
|
case "pane-exited": {
|
|
1411
1998
|
const entry = lookupPane(req.paneId);
|
|
1412
1999
|
if (!entry) return { ok: true };
|
|
@@ -1419,6 +2006,37 @@ async function handleRequest(req) {
|
|
|
1419
2006
|
await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
|
|
1420
2007
|
return { ok: true };
|
|
1421
2008
|
}
|
|
2009
|
+
case "update-task": {
|
|
2010
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
2011
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
2012
|
+
await updateTask(cwd, req.sessionId, req.task);
|
|
2013
|
+
return { ok: true };
|
|
2014
|
+
}
|
|
2015
|
+
case "message": {
|
|
2016
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
2017
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
2018
|
+
const counter = (sessionMessageCounters.get(req.sessionId) ?? 0) + 1;
|
|
2019
|
+
sessionMessageCounters.set(req.sessionId, counter);
|
|
2020
|
+
const id = `msg-${String(counter).padStart(3, "0")}`;
|
|
2021
|
+
const source = req.source ?? { type: "user" };
|
|
2022
|
+
const summary = req.content.length > 200 ? req.content.slice(0, 200) + "..." : req.content;
|
|
2023
|
+
let filePath;
|
|
2024
|
+
if (req.content.length > 200) {
|
|
2025
|
+
const dir = messagesDir(cwd, req.sessionId);
|
|
2026
|
+
mkdirSync4(dir, { recursive: true });
|
|
2027
|
+
filePath = join5(dir, `${id}.md`);
|
|
2028
|
+
writeFileSync4(filePath, req.content, "utf-8");
|
|
2029
|
+
}
|
|
2030
|
+
await appendMessage(cwd, req.sessionId, {
|
|
2031
|
+
id,
|
|
2032
|
+
source,
|
|
2033
|
+
content: req.content,
|
|
2034
|
+
summary,
|
|
2035
|
+
...filePath ? { filePath } : {},
|
|
2036
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2037
|
+
});
|
|
2038
|
+
return { ok: true };
|
|
2039
|
+
}
|
|
1422
2040
|
default:
|
|
1423
2041
|
return { ok: false, error: `Unknown request type: ${req.type}` };
|
|
1424
2042
|
}
|
|
@@ -1430,7 +2048,7 @@ async function handleRequest(req) {
|
|
|
1430
2048
|
function startServer() {
|
|
1431
2049
|
return new Promise((resolve4, reject) => {
|
|
1432
2050
|
const sock = socketPath();
|
|
1433
|
-
if (
|
|
2051
|
+
if (existsSync7(sock)) {
|
|
1434
2052
|
unlinkSync(sock);
|
|
1435
2053
|
}
|
|
1436
2054
|
server = createServer((conn) => {
|
|
@@ -1472,7 +2090,7 @@ function stopServer() {
|
|
|
1472
2090
|
}
|
|
1473
2091
|
server.close(() => {
|
|
1474
2092
|
const sock = socketPath();
|
|
1475
|
-
if (
|
|
2093
|
+
if (existsSync7(sock)) {
|
|
1476
2094
|
unlinkSync(sock);
|
|
1477
2095
|
}
|
|
1478
2096
|
server = null;
|
|
@@ -1483,7 +2101,7 @@ function stopServer() {
|
|
|
1483
2101
|
|
|
1484
2102
|
// src/daemon/updater.ts
|
|
1485
2103
|
import { execSync as execSync3 } from "child_process";
|
|
1486
|
-
import { readFileSync as
|
|
2104
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
1487
2105
|
import { resolve as resolve3 } from "path";
|
|
1488
2106
|
import { get } from "https";
|
|
1489
2107
|
function isNewer(latest, current) {
|
|
@@ -1500,7 +2118,7 @@ function isNewer(latest, current) {
|
|
|
1500
2118
|
function readPackageVersion() {
|
|
1501
2119
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
1502
2120
|
try {
|
|
1503
|
-
const raw =
|
|
2121
|
+
const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
|
|
1504
2122
|
const pkg = JSON.parse(raw);
|
|
1505
2123
|
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
1506
2124
|
} catch {
|
|
@@ -1607,7 +2225,7 @@ function isProcessAlive(pid) {
|
|
|
1607
2225
|
function readPid() {
|
|
1608
2226
|
const pidFile = daemonPidPath();
|
|
1609
2227
|
try {
|
|
1610
|
-
const pid = parseInt(
|
|
2228
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
1611
2229
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
1612
2230
|
} catch {
|
|
1613
2231
|
return null;
|
|
@@ -1678,15 +2296,23 @@ async function recoverSessions() {
|
|
|
1678
2296
|
let recovered = 0;
|
|
1679
2297
|
for (const [sessionId, cwd] of entries) {
|
|
1680
2298
|
const stateFile = statePath(cwd, sessionId);
|
|
1681
|
-
if (!
|
|
2299
|
+
if (!existsSync8(stateFile)) {
|
|
1682
2300
|
continue;
|
|
1683
2301
|
}
|
|
1684
2302
|
try {
|
|
1685
|
-
const session = JSON.parse(
|
|
2303
|
+
const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
|
|
1686
2304
|
if (session.status === "active" || session.status === "paused") {
|
|
1687
2305
|
registerSessionCwd(sessionId, cwd);
|
|
1688
2306
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
1689
2307
|
if (session.tmuxSessionName && session.tmuxWindowId) {
|
|
2308
|
+
if (!sessionExists(session.tmuxSessionName)) {
|
|
2309
|
+
if (session.status === "active") {
|
|
2310
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
2311
|
+
console.log(`[sisyphus] Session ${sessionId} paused: tmux session no longer exists`);
|
|
2312
|
+
}
|
|
2313
|
+
recovered++;
|
|
2314
|
+
continue;
|
|
2315
|
+
}
|
|
1690
2316
|
const livePanes = listPanes(session.tmuxWindowId);
|
|
1691
2317
|
if (livePanes.length > 0) {
|
|
1692
2318
|
registerSessionTmux(sessionId, session.tmuxSessionName, session.tmuxWindowId);
|