sisyphi 0.1.22 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +900 -280
- 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,137 +571,205 @@ 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);
|
|
427
732
|
const cliBin = resolve(import.meta.dirname, "cli.js");
|
|
428
733
|
const npmBinDir = resolve(import.meta.dirname, "../../.bin");
|
|
429
734
|
const envExports = [
|
|
430
735
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
431
736
|
`export SISYPHUS_AGENT_ID='orchestrator'`,
|
|
737
|
+
`export SISYPHUS_CWD='${cwd}'`,
|
|
432
738
|
`export PATH="${npmBinDir}:$PATH"`
|
|
433
739
|
].join(" && ");
|
|
434
|
-
let userPrompt;
|
|
740
|
+
let userPrompt = formattedState;
|
|
435
741
|
if (message) {
|
|
436
|
-
userPrompt
|
|
742
|
+
userPrompt += `
|
|
743
|
+
|
|
744
|
+
## Continuation Instructions
|
|
437
745
|
|
|
438
746
|
The user resumed this session with new instructions: ${message}`;
|
|
439
747
|
} else {
|
|
440
|
-
const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
|
|
441
748
|
const storedPrompt = lastCycle?.nextPrompt;
|
|
442
|
-
|
|
443
|
-
|
|
749
|
+
const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
|
|
750
|
+
userPrompt += `
|
|
444
751
|
|
|
445
|
-
|
|
446
|
-
} else {
|
|
447
|
-
userPrompt = `${formattedState}
|
|
752
|
+
## Continuation Instructions
|
|
448
753
|
|
|
449
|
-
|
|
450
|
-
}
|
|
754
|
+
${continuationText}`;
|
|
451
755
|
}
|
|
452
756
|
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
453
757
|
writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
|
|
758
|
+
if (session.messages && session.messages.length > 0) {
|
|
759
|
+
await drainMessages(cwd, sessionId, session.messages.length);
|
|
760
|
+
}
|
|
454
761
|
const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
|
|
455
762
|
const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
|
|
456
|
-
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}')"`;
|
|
457
766
|
const paneId = createPane(windowId, cwd, "left");
|
|
458
767
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
459
768
|
registerPane(paneId, sessionId, "orchestrator");
|
|
460
769
|
setPaneTitle(paneId, `Sisyphus`);
|
|
461
770
|
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
462
771
|
const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
|
|
463
|
-
const bannerCmd =
|
|
772
|
+
const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
464
773
|
const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
|
|
465
774
|
sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
|
|
466
775
|
await addOrchestratorCycle(cwd, sessionId, {
|
|
@@ -477,7 +786,7 @@ function resolveOrchestratorPane(sessionId, cwd) {
|
|
|
477
786
|
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
478
787
|
return lastCycle?.paneId ?? void 0;
|
|
479
788
|
}
|
|
480
|
-
async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
789
|
+
async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
|
|
481
790
|
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
482
791
|
if (paneId) {
|
|
483
792
|
killPane(paneId);
|
|
@@ -486,7 +795,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
486
795
|
}
|
|
487
796
|
const windowId = sessionWindowMap.get(sessionId);
|
|
488
797
|
if (windowId) selectLayout(windowId);
|
|
489
|
-
await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
|
|
798
|
+
await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode);
|
|
490
799
|
const session = getSession(cwd, sessionId);
|
|
491
800
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
492
801
|
if (runningAgents.length === 0) {
|
|
@@ -505,13 +814,13 @@ function cleanupSessionMaps(sessionId) {
|
|
|
505
814
|
}
|
|
506
815
|
|
|
507
816
|
// src/daemon/agent.ts
|
|
508
|
-
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";
|
|
509
818
|
import { resolve as resolve2 } from "path";
|
|
510
819
|
|
|
511
820
|
// src/daemon/worktree.ts
|
|
512
821
|
import { execSync as execSync2 } from "child_process";
|
|
513
|
-
import { existsSync as
|
|
514
|
-
import { dirname as dirname3, join 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";
|
|
515
824
|
var EXEC_ENV2 = {
|
|
516
825
|
...process.env,
|
|
517
826
|
PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
|
|
@@ -539,10 +848,10 @@ function loadWorktreeConfig(cwd) {
|
|
|
539
848
|
}
|
|
540
849
|
function createWorktreeShell(cwd, sessionId, agentId) {
|
|
541
850
|
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
542
|
-
const worktreePath =
|
|
851
|
+
const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
|
|
543
852
|
mkdirSync2(dirname3(worktreePath), { recursive: true });
|
|
544
853
|
execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
|
|
545
|
-
if (
|
|
854
|
+
if (existsSync4(worktreePath)) {
|
|
546
855
|
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
|
|
547
856
|
}
|
|
548
857
|
execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
|
|
@@ -553,16 +862,16 @@ function createWorktreeShell(cwd, sessionId, agentId) {
|
|
|
553
862
|
function bootstrapWorktree(cwd, worktreePath, config) {
|
|
554
863
|
if (config.copy) {
|
|
555
864
|
for (const entry of config.copy) {
|
|
556
|
-
const dest =
|
|
865
|
+
const dest = join4(worktreePath, entry);
|
|
557
866
|
mkdirSync2(dirname3(dest), { recursive: true });
|
|
558
|
-
execSafe2(`cp -r ${shellQuote2(
|
|
867
|
+
execSafe2(`cp -r ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
|
|
559
868
|
}
|
|
560
869
|
}
|
|
561
870
|
if (config.clone) {
|
|
562
871
|
for (const entry of config.clone) {
|
|
563
|
-
const dest =
|
|
872
|
+
const dest = join4(worktreePath, entry);
|
|
564
873
|
mkdirSync2(dirname3(dest), { recursive: true });
|
|
565
|
-
const src = shellQuote2(
|
|
874
|
+
const src = shellQuote2(join4(cwd, entry));
|
|
566
875
|
const dstQ = shellQuote2(dest);
|
|
567
876
|
if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
|
|
568
877
|
execSafe2(`cp -r ${src} ${dstQ}`);
|
|
@@ -571,9 +880,9 @@ function bootstrapWorktree(cwd, worktreePath, config) {
|
|
|
571
880
|
}
|
|
572
881
|
if (config.symlink) {
|
|
573
882
|
for (const entry of config.symlink) {
|
|
574
|
-
const dest =
|
|
883
|
+
const dest = join4(worktreePath, entry);
|
|
575
884
|
mkdirSync2(dirname3(dest), { recursive: true });
|
|
576
|
-
execSafe2(`ln -s ${shellQuote2(
|
|
885
|
+
execSafe2(`ln -s ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
|
|
577
886
|
}
|
|
578
887
|
}
|
|
579
888
|
if (config.init) {
|
|
@@ -645,9 +954,9 @@ function cleanupWorktree(cwd, worktreePath, branchName) {
|
|
|
645
954
|
execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
|
|
646
955
|
const baseDir = dirname3(worktreePath);
|
|
647
956
|
try {
|
|
648
|
-
const entries =
|
|
957
|
+
const entries = readdirSync4(baseDir);
|
|
649
958
|
if (entries.length === 0) {
|
|
650
|
-
|
|
959
|
+
rmSync2(baseDir, { recursive: true });
|
|
651
960
|
}
|
|
652
961
|
} catch {
|
|
653
962
|
}
|
|
@@ -656,89 +965,37 @@ function countWorktreeAgents(agents) {
|
|
|
656
965
|
return agents.filter((a) => a.worktreePath && a.status === "running").length;
|
|
657
966
|
}
|
|
658
967
|
|
|
659
|
-
// src/daemon/
|
|
660
|
-
import {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (!model) return "anthropic";
|
|
665
|
-
if (/^(gpt-|codex-)/.test(model)) return "openai";
|
|
666
|
-
return "anthropic";
|
|
667
|
-
}
|
|
668
|
-
function parseAgentFrontmatter(content) {
|
|
669
|
-
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
670
|
-
if (!match) return {};
|
|
671
|
-
const block = match[1];
|
|
672
|
-
const fm = {};
|
|
673
|
-
const str = (key) => {
|
|
674
|
-
const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
675
|
-
return m ? m[1].trim() : void 0;
|
|
676
|
-
};
|
|
677
|
-
fm.name = str("name");
|
|
678
|
-
fm.model = str("model");
|
|
679
|
-
fm.color = str("color");
|
|
680
|
-
fm.description = str("description");
|
|
681
|
-
fm.permissionMode = str("permissionMode");
|
|
682
|
-
const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
683
|
-
if (skillsMatch) {
|
|
684
|
-
fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
|
|
685
|
-
}
|
|
686
|
-
return fm;
|
|
687
|
-
}
|
|
688
|
-
function extractAgentBody(content) {
|
|
689
|
-
const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
690
|
-
return match ? match[1].trim() : content.trim();
|
|
691
|
-
}
|
|
692
|
-
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;
|
|
693
973
|
try {
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
+
}
|
|
699
989
|
}
|
|
700
990
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
let name;
|
|
709
|
-
if (agentType.includes(":")) {
|
|
710
|
-
[namespace, name] = agentType.split(":", 2);
|
|
711
|
-
} else {
|
|
712
|
-
name = agentType;
|
|
713
|
-
}
|
|
714
|
-
const searchPaths = [];
|
|
715
|
-
if (namespace) {
|
|
716
|
-
searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
|
|
717
|
-
const installPath = findPluginInstallPath(namespace);
|
|
718
|
-
if (installPath) {
|
|
719
|
-
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;
|
|
720
998
|
}
|
|
721
|
-
} else {
|
|
722
|
-
searchPaths.push(join3(cwd, ".claude", "agents", `${name}.md`));
|
|
723
|
-
searchPaths.push(join3(homedir(), ".claude", "agents", `${name}.md`));
|
|
724
|
-
searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
|
|
725
|
-
}
|
|
726
|
-
for (const path of searchPaths) {
|
|
727
|
-
if (existsSync3(path)) return path;
|
|
728
|
-
}
|
|
729
|
-
return null;
|
|
730
|
-
}
|
|
731
|
-
function resolveAgentConfig(agentType, pluginDir, cwd) {
|
|
732
|
-
const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
|
|
733
|
-
if (!filePath) return null;
|
|
734
|
-
try {
|
|
735
|
-
const content = readFileSync5(filePath, "utf-8");
|
|
736
|
-
return {
|
|
737
|
-
frontmatter: parseAgentFrontmatter(content),
|
|
738
|
-
body: extractAgentBody(content),
|
|
739
|
-
filePath
|
|
740
|
-
};
|
|
741
|
-
} catch {
|
|
742
999
|
return null;
|
|
743
1000
|
}
|
|
744
1001
|
}
|
|
@@ -760,7 +1017,7 @@ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
|
|
|
760
1017
|
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
761
1018
|
let template;
|
|
762
1019
|
try {
|
|
763
|
-
template =
|
|
1020
|
+
template = readFileSync5(templatePath, "utf-8");
|
|
764
1021
|
} catch {
|
|
765
1022
|
template = `# Sisyphus Agent
|
|
766
1023
|
Session: {{SESSION_ID}}
|
|
@@ -776,13 +1033,33 @@ Task: {{INSTRUCTION}}`;
|
|
|
776
1033
|
}
|
|
777
1034
|
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
|
|
778
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
|
+
}
|
|
779
1056
|
async function spawnAgent(opts) {
|
|
780
1057
|
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
781
1058
|
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
782
1059
|
agentCounters.set(sessionId, count);
|
|
783
1060
|
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
784
|
-
const
|
|
785
|
-
const agentConfig = resolveAgentConfig(agentType,
|
|
1061
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1062
|
+
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
786
1063
|
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
787
1064
|
const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
|
|
788
1065
|
let paneCwd = cwd;
|
|
@@ -808,12 +1085,13 @@ async function spawnAgent(opts) {
|
|
|
808
1085
|
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
809
1086
|
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
810
1087
|
const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
|
|
811
|
-
const bannerCmd =
|
|
1088
|
+
const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
812
1089
|
const cliBin = resolve2(import.meta.dirname, "cli.js");
|
|
813
1090
|
const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
|
|
814
1091
|
const envExports = [
|
|
815
1092
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
816
1093
|
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
1094
|
+
`export SISYPHUS_CWD='${cwd}'`,
|
|
817
1095
|
...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
|
|
818
1096
|
`export PATH="${npmBinDir}:$PATH"`
|
|
819
1097
|
].join(" && ");
|
|
@@ -834,7 +1112,10 @@ ${instruction}`);
|
|
|
834
1112
|
mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
|
|
835
1113
|
} else {
|
|
836
1114
|
const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
|
|
837
|
-
|
|
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)}`;
|
|
838
1119
|
}
|
|
839
1120
|
const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
|
|
840
1121
|
const agent = {
|
|
@@ -872,10 +1153,98 @@ ${instruction}`);
|
|
|
872
1153
|
}
|
|
873
1154
|
return agent;
|
|
874
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
|
+
}
|
|
875
1244
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
876
1245
|
const dir = reportsDir(cwd, sessionId);
|
|
877
1246
|
try {
|
|
878
|
-
const files =
|
|
1247
|
+
const files = readdirSync5(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
879
1248
|
return String(files.length + 1).padStart(3, "0");
|
|
880
1249
|
} catch {
|
|
881
1250
|
return "001";
|
|
@@ -894,6 +1263,12 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
|
894
1263
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
895
1264
|
};
|
|
896
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
|
+
});
|
|
897
1272
|
}
|
|
898
1273
|
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
899
1274
|
const dir = reportsDir(cwd, sessionId);
|
|
@@ -907,20 +1282,26 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
907
1282
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
908
1283
|
};
|
|
909
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
|
+
});
|
|
910
1291
|
await updateAgent(cwd, sessionId, agentId, {
|
|
911
1292
|
status: "completed",
|
|
912
1293
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
913
1294
|
});
|
|
914
1295
|
const session = getSession(cwd, sessionId);
|
|
915
|
-
const
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
+
}
|
|
920
1303
|
}
|
|
921
|
-
|
|
922
|
-
if (windowId) selectLayout(windowId);
|
|
923
|
-
return allAgentsDone(session);
|
|
1304
|
+
return allAgentsDone(getSession(cwd, sessionId));
|
|
924
1305
|
}
|
|
925
1306
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
926
1307
|
unregisterAgentPane(sessionId, agentId);
|
|
@@ -1006,7 +1387,15 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
1006
1387
|
}
|
|
1007
1388
|
if (session.status !== "active") return;
|
|
1008
1389
|
const livePanes = listPanes(windowId);
|
|
1009
|
-
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
|
+
}
|
|
1010
1399
|
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1011
1400
|
let paneRemoved = false;
|
|
1012
1401
|
for (const agent of session.agents) {
|
|
@@ -1036,23 +1425,34 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
1036
1425
|
}
|
|
1037
1426
|
|
|
1038
1427
|
// src/daemon/session-manager.ts
|
|
1039
|
-
|
|
1428
|
+
var NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
1429
|
+
async function startSession(task, cwd, context, name) {
|
|
1040
1430
|
const sessionId = uuidv4();
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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);
|
|
1044
1443
|
await spawnOrchestrator(sessionId, cwd, windowId);
|
|
1045
1444
|
updateTrackedWindow(sessionId, windowId);
|
|
1445
|
+
killPane(initialPaneId);
|
|
1046
1446
|
pruneOldSessions(cwd);
|
|
1047
|
-
return
|
|
1447
|
+
return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
|
|
1048
1448
|
}
|
|
1049
1449
|
var PRUNE_KEEP_COUNT = 10;
|
|
1050
1450
|
var PRUNE_KEEP_DAYS = 7;
|
|
1051
1451
|
function pruneOldSessions(cwd) {
|
|
1052
1452
|
try {
|
|
1053
1453
|
const dir = sessionsDir(cwd);
|
|
1054
|
-
if (!
|
|
1055
|
-
const entries =
|
|
1454
|
+
if (!existsSync6(dir)) return;
|
|
1455
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
1056
1456
|
const candidates = [];
|
|
1057
1457
|
for (const entry of entries) {
|
|
1058
1458
|
if (!entry.isDirectory()) continue;
|
|
@@ -1075,14 +1475,25 @@ function pruneOldSessions(cwd) {
|
|
|
1075
1475
|
}
|
|
1076
1476
|
for (const c of candidates) {
|
|
1077
1477
|
if (keep.has(c.id)) continue;
|
|
1078
|
-
|
|
1478
|
+
rmSync3(sessionDir(cwd, c.id), { recursive: true, force: true });
|
|
1079
1479
|
}
|
|
1080
1480
|
} catch (err) {
|
|
1081
1481
|
console.error("[sisyphus] Session pruning failed:", err);
|
|
1082
1482
|
}
|
|
1083
1483
|
}
|
|
1084
|
-
async function resumeSession(sessionId, cwd,
|
|
1484
|
+
async function resumeSession(sessionId, cwd, message) {
|
|
1085
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
|
+
}
|
|
1086
1497
|
if (session.status !== "active") {
|
|
1087
1498
|
const livePaneIds = /* @__PURE__ */ new Set();
|
|
1088
1499
|
if (session.tmuxWindowId) {
|
|
@@ -1105,12 +1516,16 @@ async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
|
|
|
1105
1516
|
}
|
|
1106
1517
|
}
|
|
1107
1518
|
await updateSessionStatus(cwd, sessionId, "active");
|
|
1108
|
-
await updateSessionTmux(cwd, sessionId,
|
|
1519
|
+
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
1109
1520
|
resetAgentCounterFromState(sessionId, session.agents);
|
|
1110
1521
|
resetColors(sessionId);
|
|
1111
|
-
|
|
1522
|
+
orchestratorDone.delete(sessionId);
|
|
1523
|
+
trackSession(sessionId, cwd, tmuxName);
|
|
1112
1524
|
await spawnOrchestrator(sessionId, cwd, windowId, message);
|
|
1113
1525
|
updateTrackedWindow(sessionId, windowId);
|
|
1526
|
+
if (initialPaneId) {
|
|
1527
|
+
killPane(initialPaneId);
|
|
1528
|
+
}
|
|
1114
1529
|
return getSession(cwd, sessionId);
|
|
1115
1530
|
}
|
|
1116
1531
|
function getSessionStatus(cwd, sessionId) {
|
|
@@ -1118,8 +1533,8 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
1118
1533
|
}
|
|
1119
1534
|
function listSessions(cwd) {
|
|
1120
1535
|
const dir = sessionsDir(cwd);
|
|
1121
|
-
if (!
|
|
1122
|
-
const entries =
|
|
1536
|
+
if (!existsSync6(dir)) return [];
|
|
1537
|
+
const entries = readdirSync6(dir, { withFileTypes: true });
|
|
1123
1538
|
const sessions = [];
|
|
1124
1539
|
for (const entry of entries) {
|
|
1125
1540
|
if (!entry.isDirectory()) continue;
|
|
@@ -1127,10 +1542,13 @@ function listSessions(cwd) {
|
|
|
1127
1542
|
const session = getSession(cwd, entry.name);
|
|
1128
1543
|
sessions.push({
|
|
1129
1544
|
id: session.id,
|
|
1545
|
+
name: session.name,
|
|
1130
1546
|
task: session.task,
|
|
1131
1547
|
status: session.status,
|
|
1132
1548
|
createdAt: session.createdAt,
|
|
1133
|
-
agentCount: session.agents.length
|
|
1549
|
+
agentCount: session.agents.length,
|
|
1550
|
+
tmuxSessionName: session.tmuxSessionName,
|
|
1551
|
+
tmuxWindowId: session.tmuxWindowId
|
|
1134
1552
|
});
|
|
1135
1553
|
} catch (err) {
|
|
1136
1554
|
console.error(`[sisyphus] Failed to read session ${entry.name}:`, err);
|
|
@@ -1139,11 +1557,17 @@ function listSessions(cwd) {
|
|
|
1139
1557
|
return sessions;
|
|
1140
1558
|
}
|
|
1141
1559
|
var pendingRespawns = /* @__PURE__ */ new Set();
|
|
1560
|
+
var orchestratorDone = /* @__PURE__ */ new Set();
|
|
1142
1561
|
function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
1143
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
|
+
}
|
|
1144
1567
|
const session = getSession(cwd, sessionId);
|
|
1145
1568
|
if (session.status !== "active") return;
|
|
1146
1569
|
pendingRespawns.add(sessionId);
|
|
1570
|
+
orchestratorDone.delete(sessionId);
|
|
1147
1571
|
const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
|
|
1148
1572
|
if (worktreeAgents.length > 0) {
|
|
1149
1573
|
const results = mergeWorktrees(cwd, worktreeAgents);
|
|
@@ -1155,9 +1579,42 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1155
1579
|
}).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
|
|
1156
1580
|
}
|
|
1157
1581
|
}
|
|
1158
|
-
|
|
1582
|
+
const cycleNumber = session.orchestratorCycles.length;
|
|
1583
|
+
if (cycleNumber > 0) {
|
|
1584
|
+
createSnapshot(cwd, sessionId, cycleNumber);
|
|
1585
|
+
}
|
|
1586
|
+
setImmediate(async () => {
|
|
1159
1587
|
pendingRespawns.delete(sessionId);
|
|
1160
|
-
|
|
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
|
+
}
|
|
1161
1618
|
});
|
|
1162
1619
|
}
|
|
1163
1620
|
async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
|
|
@@ -1189,16 +1646,17 @@ async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
|
|
|
1189
1646
|
async function handleReport(cwd, sessionId, agentId, content) {
|
|
1190
1647
|
await handleAgentReport(cwd, sessionId, agentId, content);
|
|
1191
1648
|
}
|
|
1192
|
-
async function handleYield(sessionId, cwd, nextPrompt) {
|
|
1649
|
+
async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
1193
1650
|
const pre = getSession(cwd, sessionId);
|
|
1194
1651
|
if (pre.status === "paused") {
|
|
1195
1652
|
await updateSessionStatus(cwd, sessionId, "active");
|
|
1196
1653
|
}
|
|
1197
|
-
await handleOrchestratorYield(sessionId, cwd, nextPrompt);
|
|
1654
|
+
await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
|
|
1655
|
+
orchestratorDone.add(sessionId);
|
|
1198
1656
|
const session = getSession(cwd, sessionId);
|
|
1199
1657
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1200
1658
|
if (!hasRunningAgents) {
|
|
1201
|
-
const windowId = getWindowId(sessionId);
|
|
1659
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1202
1660
|
if (windowId) {
|
|
1203
1661
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1204
1662
|
}
|
|
@@ -1207,6 +1665,9 @@ async function handleYield(sessionId, cwd, nextPrompt) {
|
|
|
1207
1665
|
async function handleComplete(sessionId, cwd, report) {
|
|
1208
1666
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
1209
1667
|
}
|
|
1668
|
+
async function handleContinue(sessionId, cwd) {
|
|
1669
|
+
await continueSession(cwd, sessionId);
|
|
1670
|
+
}
|
|
1210
1671
|
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
1211
1672
|
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
1212
1673
|
}
|
|
@@ -1236,12 +1697,81 @@ async function handleKill(sessionId, cwd) {
|
|
|
1236
1697
|
await updateSessionStatus(cwd, sessionId, "completed");
|
|
1237
1698
|
untrackSession(sessionId);
|
|
1238
1699
|
unregisterSessionPanes(sessionId);
|
|
1239
|
-
if (
|
|
1700
|
+
if (session.tmuxSessionName) {
|
|
1701
|
+
killSession(session.tmuxSessionName);
|
|
1702
|
+
} else if (windowId) {
|
|
1240
1703
|
killWindow(windowId);
|
|
1241
1704
|
}
|
|
1242
1705
|
clearAgentCounter(sessionId);
|
|
1706
|
+
orchestratorDone.delete(sessionId);
|
|
1243
1707
|
return killedAgents;
|
|
1244
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
|
+
}
|
|
1245
1775
|
async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
1246
1776
|
const session = getSession(cwd, sessionId);
|
|
1247
1777
|
if (session.status !== "active") return;
|
|
@@ -1250,15 +1780,16 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
1250
1780
|
if (!agent || agent.status !== "running") return;
|
|
1251
1781
|
const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
|
|
1252
1782
|
if (allDone) {
|
|
1253
|
-
const windowId = getWindowId(sessionId);
|
|
1783
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1254
1784
|
if (windowId) {
|
|
1255
1785
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1256
1786
|
}
|
|
1257
1787
|
}
|
|
1258
1788
|
} else if (role === "orchestrator") {
|
|
1789
|
+
orchestratorDone.add(sessionId);
|
|
1259
1790
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1260
1791
|
if (!hasRunningAgents && session.agents.length > 0) {
|
|
1261
|
-
const windowId = getWindowId(sessionId);
|
|
1792
|
+
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
1262
1793
|
if (windowId) {
|
|
1263
1794
|
console.log(`[sisyphus] Orchestrator pane exited for session ${sessionId}, all agents done \u2014 triggering respawn`);
|
|
1264
1795
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
@@ -1273,10 +1804,11 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
1273
1804
|
// src/daemon/server.ts
|
|
1274
1805
|
var server = null;
|
|
1275
1806
|
var sessionCwdMap = /* @__PURE__ */ new Map();
|
|
1807
|
+
var sessionMessageCounters = /* @__PURE__ */ new Map();
|
|
1276
1808
|
var sessionTmuxMap = /* @__PURE__ */ new Map();
|
|
1277
1809
|
var sessionWindowMap2 = /* @__PURE__ */ new Map();
|
|
1278
1810
|
function registryPath() {
|
|
1279
|
-
return
|
|
1811
|
+
return join5(globalDir(), "session-registry.json");
|
|
1280
1812
|
}
|
|
1281
1813
|
function persistSessionRegistry() {
|
|
1282
1814
|
const dir = globalDir();
|
|
@@ -1289,9 +1821,9 @@ function persistSessionRegistry() {
|
|
|
1289
1821
|
}
|
|
1290
1822
|
function loadSessionRegistry() {
|
|
1291
1823
|
const p = registryPath();
|
|
1292
|
-
if (!
|
|
1824
|
+
if (!existsSync7(p)) return {};
|
|
1293
1825
|
try {
|
|
1294
|
-
return JSON.parse(
|
|
1826
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
1295
1827
|
} catch {
|
|
1296
1828
|
return {};
|
|
1297
1829
|
}
|
|
@@ -1308,11 +1840,11 @@ async function handleRequest(req) {
|
|
|
1308
1840
|
try {
|
|
1309
1841
|
switch (req.type) {
|
|
1310
1842
|
case "start": {
|
|
1311
|
-
const session = await startSession(req.task, req.cwd, req.
|
|
1843
|
+
const session = await startSession(req.task, req.cwd, req.context, req.name);
|
|
1312
1844
|
registerSessionCwd(session.id, req.cwd);
|
|
1313
|
-
sessionTmuxMap.set(session.id,
|
|
1314
|
-
sessionWindowMap2.set(session.id,
|
|
1315
|
-
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 } };
|
|
1316
1848
|
}
|
|
1317
1849
|
case "spawn": {
|
|
1318
1850
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
@@ -1337,7 +1869,7 @@ async function handleRequest(req) {
|
|
|
1337
1869
|
case "yield": {
|
|
1338
1870
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1339
1871
|
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1340
|
-
await handleYield(req.sessionId, cwd, req.nextPrompt);
|
|
1872
|
+
await handleYield(req.sessionId, cwd, req.nextPrompt, req.mode);
|
|
1341
1873
|
return { ok: true };
|
|
1342
1874
|
}
|
|
1343
1875
|
case "complete": {
|
|
@@ -1346,9 +1878,15 @@ async function handleRequest(req) {
|
|
|
1346
1878
|
await handleComplete(req.sessionId, cwd, req.report);
|
|
1347
1879
|
return { ok: true };
|
|
1348
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
|
+
}
|
|
1349
1887
|
case "status": {
|
|
1350
1888
|
if (req.sessionId) {
|
|
1351
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
1889
|
+
const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
|
|
1352
1890
|
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
1353
1891
|
const session = getSessionStatus(cwd, req.sessionId);
|
|
1354
1892
|
return { ok: true, data: { session } };
|
|
@@ -1385,17 +1923,17 @@ async function handleRequest(req) {
|
|
|
1385
1923
|
let cwd = sessionCwdMap.get(req.sessionId);
|
|
1386
1924
|
if (!cwd) {
|
|
1387
1925
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
1388
|
-
if (
|
|
1926
|
+
if (existsSync7(stateFile)) {
|
|
1389
1927
|
cwd = req.cwd;
|
|
1390
1928
|
registerSessionCwd(req.sessionId, cwd);
|
|
1391
1929
|
} else {
|
|
1392
1930
|
return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
|
|
1393
1931
|
}
|
|
1394
1932
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
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 } };
|
|
1399
1937
|
}
|
|
1400
1938
|
case "register_claude_session": {
|
|
1401
1939
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
@@ -1413,6 +1951,49 @@ async function handleRequest(req) {
|
|
|
1413
1951
|
persistSessionRegistry();
|
|
1414
1952
|
return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
|
|
1415
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
|
+
}
|
|
1416
1997
|
case "pane-exited": {
|
|
1417
1998
|
const entry = lookupPane(req.paneId);
|
|
1418
1999
|
if (!entry) return { ok: true };
|
|
@@ -1425,6 +2006,37 @@ async function handleRequest(req) {
|
|
|
1425
2006
|
await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
|
|
1426
2007
|
return { ok: true };
|
|
1427
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
|
+
}
|
|
1428
2040
|
default:
|
|
1429
2041
|
return { ok: false, error: `Unknown request type: ${req.type}` };
|
|
1430
2042
|
}
|
|
@@ -1436,7 +2048,7 @@ async function handleRequest(req) {
|
|
|
1436
2048
|
function startServer() {
|
|
1437
2049
|
return new Promise((resolve4, reject) => {
|
|
1438
2050
|
const sock = socketPath();
|
|
1439
|
-
if (
|
|
2051
|
+
if (existsSync7(sock)) {
|
|
1440
2052
|
unlinkSync(sock);
|
|
1441
2053
|
}
|
|
1442
2054
|
server = createServer((conn) => {
|
|
@@ -1478,7 +2090,7 @@ function stopServer() {
|
|
|
1478
2090
|
}
|
|
1479
2091
|
server.close(() => {
|
|
1480
2092
|
const sock = socketPath();
|
|
1481
|
-
if (
|
|
2093
|
+
if (existsSync7(sock)) {
|
|
1482
2094
|
unlinkSync(sock);
|
|
1483
2095
|
}
|
|
1484
2096
|
server = null;
|
|
@@ -1489,7 +2101,7 @@ function stopServer() {
|
|
|
1489
2101
|
|
|
1490
2102
|
// src/daemon/updater.ts
|
|
1491
2103
|
import { execSync as execSync3 } from "child_process";
|
|
1492
|
-
import { readFileSync as
|
|
2104
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
1493
2105
|
import { resolve as resolve3 } from "path";
|
|
1494
2106
|
import { get } from "https";
|
|
1495
2107
|
function isNewer(latest, current) {
|
|
@@ -1506,7 +2118,7 @@ function isNewer(latest, current) {
|
|
|
1506
2118
|
function readPackageVersion() {
|
|
1507
2119
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
1508
2120
|
try {
|
|
1509
|
-
const raw =
|
|
2121
|
+
const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
|
|
1510
2122
|
const pkg = JSON.parse(raw);
|
|
1511
2123
|
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
1512
2124
|
} catch {
|
|
@@ -1613,7 +2225,7 @@ function isProcessAlive(pid) {
|
|
|
1613
2225
|
function readPid() {
|
|
1614
2226
|
const pidFile = daemonPidPath();
|
|
1615
2227
|
try {
|
|
1616
|
-
const pid = parseInt(
|
|
2228
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
1617
2229
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
1618
2230
|
} catch {
|
|
1619
2231
|
return null;
|
|
@@ -1684,15 +2296,23 @@ async function recoverSessions() {
|
|
|
1684
2296
|
let recovered = 0;
|
|
1685
2297
|
for (const [sessionId, cwd] of entries) {
|
|
1686
2298
|
const stateFile = statePath(cwd, sessionId);
|
|
1687
|
-
if (!
|
|
2299
|
+
if (!existsSync8(stateFile)) {
|
|
1688
2300
|
continue;
|
|
1689
2301
|
}
|
|
1690
2302
|
try {
|
|
1691
|
-
const session = JSON.parse(
|
|
2303
|
+
const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
|
|
1692
2304
|
if (session.status === "active" || session.status === "paused") {
|
|
1693
2305
|
registerSessionCwd(sessionId, cwd);
|
|
1694
2306
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
1695
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
|
+
}
|
|
1696
2316
|
const livePanes = listPanes(session.tmuxWindowId);
|
|
1697
2317
|
if (livePanes.length > 0) {
|
|
1698
2318
|
registerSessionTmux(sessionId, session.tmuxSessionName, session.tmuxWindowId);
|