sisyphi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/chunk-5WP7O7D3.js +56 -0
- package/dist/chunk-5WP7O7D3.js.map +1 -0
- package/dist/cli.js +532 -0
- package/dist/cli.js.map +1 -0
- package/dist/daemon.js +983 -0
- package/dist/daemon.js.map +1 -0
- package/dist/templates/agent-suffix.md +61 -0
- package/dist/templates/orchestrator.md +152 -0
- package/package.json +49 -0
- package/templates/agent-suffix.md +61 -0
- package/templates/orchestrator.md +152 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,983 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
daemonPidPath,
|
|
4
|
+
globalConfigPath,
|
|
5
|
+
globalDir,
|
|
6
|
+
projectConfigPath,
|
|
7
|
+
projectOrchestratorPromptPath,
|
|
8
|
+
reportFilePath,
|
|
9
|
+
reportsDir,
|
|
10
|
+
sessionDir,
|
|
11
|
+
sessionsDir,
|
|
12
|
+
socketPath,
|
|
13
|
+
statePath
|
|
14
|
+
} from "./chunk-5WP7O7D3.js";
|
|
15
|
+
|
|
16
|
+
// src/daemon/index.ts
|
|
17
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync4 } from "fs";
|
|
18
|
+
|
|
19
|
+
// src/shared/config.ts
|
|
20
|
+
import { readFileSync } from "fs";
|
|
21
|
+
var DEFAULT_CONFIG = {
|
|
22
|
+
pollIntervalMs: 1e3
|
|
23
|
+
};
|
|
24
|
+
function readJsonFile(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = readFileSync(filePath, "utf-8");
|
|
27
|
+
return JSON.parse(content);
|
|
28
|
+
} catch {
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function loadConfig(cwd) {
|
|
33
|
+
const global = readJsonFile(globalConfigPath());
|
|
34
|
+
const project = readJsonFile(projectConfigPath(cwd));
|
|
35
|
+
return { ...DEFAULT_CONFIG, ...global, ...project };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/daemon/server.ts
|
|
39
|
+
import { createServer } from "net";
|
|
40
|
+
import { unlinkSync, existsSync as existsSync3, writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync3 } from "fs";
|
|
41
|
+
import { join as join2 } from "path";
|
|
42
|
+
|
|
43
|
+
// src/daemon/session-manager.ts
|
|
44
|
+
import { v4 as uuidv4 } from "uuid";
|
|
45
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
|
|
46
|
+
|
|
47
|
+
// src/daemon/state.ts
|
|
48
|
+
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
49
|
+
import { dirname, join } from "path";
|
|
50
|
+
import { randomUUID } from "crypto";
|
|
51
|
+
var sessionLocks = /* @__PURE__ */ new Map();
|
|
52
|
+
async function withSessionLock(sessionId, fn) {
|
|
53
|
+
const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
|
|
54
|
+
let resolve3;
|
|
55
|
+
const next = new Promise((r) => {
|
|
56
|
+
resolve3 = r;
|
|
57
|
+
});
|
|
58
|
+
sessionLocks.set(sessionId, next);
|
|
59
|
+
await prev;
|
|
60
|
+
try {
|
|
61
|
+
return fn();
|
|
62
|
+
} finally {
|
|
63
|
+
resolve3();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function atomicWrite(filePath, data) {
|
|
67
|
+
const dir = dirname(filePath);
|
|
68
|
+
const tmpPath = join(dir, `.state.${randomUUID()}.tmp`);
|
|
69
|
+
writeFileSync(tmpPath, data, "utf-8");
|
|
70
|
+
renameSync(tmpPath, filePath);
|
|
71
|
+
}
|
|
72
|
+
function createSession(id, task, cwd) {
|
|
73
|
+
const dir = sessionDir(cwd, id);
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
const session = {
|
|
76
|
+
id,
|
|
77
|
+
task,
|
|
78
|
+
cwd,
|
|
79
|
+
status: "active",
|
|
80
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
81
|
+
tasks: [],
|
|
82
|
+
agents: [],
|
|
83
|
+
orchestratorCycles: []
|
|
84
|
+
};
|
|
85
|
+
atomicWrite(statePath(cwd, id), JSON.stringify(session, null, 2));
|
|
86
|
+
return session;
|
|
87
|
+
}
|
|
88
|
+
function getSession(cwd, sessionId) {
|
|
89
|
+
const content = readFileSync2(statePath(cwd, sessionId), "utf-8");
|
|
90
|
+
return JSON.parse(content);
|
|
91
|
+
}
|
|
92
|
+
function saveSession(session) {
|
|
93
|
+
atomicWrite(statePath(session.cwd, session.id), JSON.stringify(session, null, 2));
|
|
94
|
+
}
|
|
95
|
+
async function addTask(cwd, sessionId, description, initialStatus) {
|
|
96
|
+
return withSessionLock(sessionId, () => {
|
|
97
|
+
const session = getSession(cwd, sessionId);
|
|
98
|
+
const nextNum = session.tasks.length + 1;
|
|
99
|
+
const task = {
|
|
100
|
+
id: `t${nextNum}`,
|
|
101
|
+
description,
|
|
102
|
+
status: initialStatus !== void 0 ? initialStatus : "pending"
|
|
103
|
+
};
|
|
104
|
+
session.tasks.push(task);
|
|
105
|
+
saveSession(session);
|
|
106
|
+
return task;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function updateTask(cwd, sessionId, taskId, updates) {
|
|
110
|
+
return withSessionLock(sessionId, () => {
|
|
111
|
+
const session = getSession(cwd, sessionId);
|
|
112
|
+
const task = session.tasks.find((t) => t.id === taskId);
|
|
113
|
+
if (!task) throw new Error(`Task ${taskId} not found in session ${sessionId}`);
|
|
114
|
+
if (updates.status) task.status = updates.status;
|
|
115
|
+
if (updates.description) task.description = updates.description;
|
|
116
|
+
saveSession(session);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async function addAgent(cwd, sessionId, agent) {
|
|
120
|
+
return withSessionLock(sessionId, () => {
|
|
121
|
+
const session = getSession(cwd, sessionId);
|
|
122
|
+
session.agents.push(agent);
|
|
123
|
+
saveSession(session);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async function updateAgent(cwd, sessionId, agentId, updates) {
|
|
127
|
+
return withSessionLock(sessionId, () => {
|
|
128
|
+
const session = getSession(cwd, sessionId);
|
|
129
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
130
|
+
if (!agent) throw new Error(`Agent ${agentId} not found in session ${sessionId}`);
|
|
131
|
+
Object.assign(agent, updates);
|
|
132
|
+
saveSession(session);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async function addOrchestratorCycle(cwd, sessionId, cycle) {
|
|
136
|
+
return withSessionLock(sessionId, () => {
|
|
137
|
+
const session = getSession(cwd, sessionId);
|
|
138
|
+
session.orchestratorCycles.push(cycle);
|
|
139
|
+
saveSession(session);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async function updateSessionStatus(cwd, sessionId, status, completionReport) {
|
|
143
|
+
return withSessionLock(sessionId, () => {
|
|
144
|
+
const session = getSession(cwd, sessionId);
|
|
145
|
+
session.status = status;
|
|
146
|
+
if (completionReport !== void 0) {
|
|
147
|
+
session.completionReport = completionReport;
|
|
148
|
+
}
|
|
149
|
+
saveSession(session);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async function appendAgentToLastCycle(cwd, sessionId, agentId) {
|
|
153
|
+
return withSessionLock(sessionId, () => {
|
|
154
|
+
const session = getSession(cwd, sessionId);
|
|
155
|
+
const cycles = session.orchestratorCycles;
|
|
156
|
+
if (cycles.length === 0) return;
|
|
157
|
+
cycles[cycles.length - 1].agentsSpawned.push(agentId);
|
|
158
|
+
saveSession(session);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function completeSession(cwd, sessionId, report) {
|
|
162
|
+
return withSessionLock(sessionId, () => {
|
|
163
|
+
const session = getSession(cwd, sessionId);
|
|
164
|
+
session.status = "completed";
|
|
165
|
+
session.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
166
|
+
session.completionReport = report;
|
|
167
|
+
saveSession(session);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
async function appendAgentReport(cwd, sessionId, agentId, entry) {
|
|
171
|
+
return withSessionLock(sessionId, () => {
|
|
172
|
+
const session = getSession(cwd, sessionId);
|
|
173
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
174
|
+
if (!agent) throw new Error(`Agent ${agentId} not found in session ${sessionId}`);
|
|
175
|
+
agent.reports.push(entry);
|
|
176
|
+
saveSession(session);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async function completeOrchestratorCycle(cwd, sessionId) {
|
|
180
|
+
return withSessionLock(sessionId, () => {
|
|
181
|
+
const session = getSession(cwd, sessionId);
|
|
182
|
+
const cycles = session.orchestratorCycles;
|
|
183
|
+
if (cycles.length === 0) return;
|
|
184
|
+
cycles[cycles.length - 1].completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
185
|
+
saveSession(session);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/daemon/orchestrator.ts
|
|
190
|
+
import { readFileSync as readFileSync3, existsSync, writeFileSync as writeFileSync2 } from "fs";
|
|
191
|
+
import { resolve } from "path";
|
|
192
|
+
|
|
193
|
+
// src/daemon/tmux.ts
|
|
194
|
+
import { execSync } from "child_process";
|
|
195
|
+
var EXEC_ENV = {
|
|
196
|
+
...process.env,
|
|
197
|
+
PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
|
|
198
|
+
};
|
|
199
|
+
function exec(cmd) {
|
|
200
|
+
return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV }).trim();
|
|
201
|
+
}
|
|
202
|
+
function execSafe(cmd) {
|
|
203
|
+
try {
|
|
204
|
+
return exec(cmd);
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function createPane(windowTarget, cwd) {
|
|
210
|
+
const cwdFlag = cwd ? ` -c ${shellQuote(cwd)}` : "";
|
|
211
|
+
const paneId = exec(`tmux split-window -h -t "${windowTarget}"${cwdFlag} -P -F "#{pane_id}"`);
|
|
212
|
+
execSafe(`tmux select-layout -t "${windowTarget}" even-horizontal`);
|
|
213
|
+
return paneId;
|
|
214
|
+
}
|
|
215
|
+
function sendKeys(paneTarget, command) {
|
|
216
|
+
exec(`tmux send-keys -t "${paneTarget}" ${shellQuote(command)} Enter`);
|
|
217
|
+
}
|
|
218
|
+
function killPane(paneTarget) {
|
|
219
|
+
execSafe(`tmux kill-pane -t "${paneTarget}"`);
|
|
220
|
+
}
|
|
221
|
+
function killWindow(windowTarget) {
|
|
222
|
+
execSafe(`tmux kill-window -t "${windowTarget}"`);
|
|
223
|
+
}
|
|
224
|
+
function listPanes(windowTarget) {
|
|
225
|
+
const output = execSafe(`tmux list-panes -t "${windowTarget}" -F "#{pane_id} #{pane_pid}"`);
|
|
226
|
+
if (!output) return [];
|
|
227
|
+
return output.split("\n").filter(Boolean).map((line) => {
|
|
228
|
+
const [paneId, panePid] = line.split(" ");
|
|
229
|
+
return { paneId, panePid };
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function setPaneTitle(paneTarget, title) {
|
|
233
|
+
execSafe(`tmux select-pane -t "${paneTarget}" -T ${shellQuote(title)}`);
|
|
234
|
+
}
|
|
235
|
+
function setPaneStyle(paneTarget, color) {
|
|
236
|
+
execSafe(`tmux select-pane -t "${paneTarget}" -P "border-style=fg=${color}"`);
|
|
237
|
+
}
|
|
238
|
+
function shellQuote(s) {
|
|
239
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/daemon/colors.ts
|
|
243
|
+
var ORCHESTRATOR_COLOR = "yellow";
|
|
244
|
+
var AGENT_PALETTE = ["blue", "green", "magenta", "cyan", "red", "white"];
|
|
245
|
+
var sessionColorIndex = /* @__PURE__ */ new Map();
|
|
246
|
+
function getNextColor(sessionId) {
|
|
247
|
+
const idx = sessionColorIndex.get(sessionId) ?? 0;
|
|
248
|
+
const color = AGENT_PALETTE[idx % AGENT_PALETTE.length];
|
|
249
|
+
sessionColorIndex.set(sessionId, idx + 1);
|
|
250
|
+
return color;
|
|
251
|
+
}
|
|
252
|
+
function resetColors(sessionId) {
|
|
253
|
+
sessionColorIndex.delete(sessionId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/daemon/orchestrator.ts
|
|
257
|
+
var sessionWindowMap = /* @__PURE__ */ new Map();
|
|
258
|
+
var sessionOrchestratorPane = /* @__PURE__ */ new Map();
|
|
259
|
+
function getWindowId(sessionId) {
|
|
260
|
+
return sessionWindowMap.get(sessionId);
|
|
261
|
+
}
|
|
262
|
+
function getOrchestratorPaneId(sessionId) {
|
|
263
|
+
return sessionOrchestratorPane.get(sessionId);
|
|
264
|
+
}
|
|
265
|
+
function loadOrchestratorPrompt(cwd) {
|
|
266
|
+
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
267
|
+
if (existsSync(projectPath)) {
|
|
268
|
+
return readFileSync3(projectPath, "utf-8");
|
|
269
|
+
}
|
|
270
|
+
const bundledPath = resolve(import.meta.dirname, "../templates/orchestrator.md");
|
|
271
|
+
return readFileSync3(bundledPath, "utf-8");
|
|
272
|
+
}
|
|
273
|
+
function formatStateForOrchestrator(session) {
|
|
274
|
+
const shortId = session.id.slice(0, 8);
|
|
275
|
+
const cycleNum = session.orchestratorCycles.length;
|
|
276
|
+
const taskLines = session.tasks.length > 0 ? session.tasks.map((t) => `- ${t.id}: [${t.status}] ${t.description}`).join("\n") : " (none)";
|
|
277
|
+
const agentLines = session.agents.length > 0 ? session.agents.map((a) => {
|
|
278
|
+
const header = `- ${a.id} (${a.name}): ${a.status} \u2014 ${a.reports.length} report(s)`;
|
|
279
|
+
if (a.reports.length === 0) return header;
|
|
280
|
+
let updateNum = 0;
|
|
281
|
+
const reportLines = a.reports.map((r) => {
|
|
282
|
+
const label = r.type === "final" ? "[final]" : `[update ${String(++updateNum).padStart(3, "0")}]`;
|
|
283
|
+
return ` ${label} "${r.summary}" \u2192 ${r.filePath}`;
|
|
284
|
+
});
|
|
285
|
+
return [header, ...reportLines].join("\n");
|
|
286
|
+
}).join("\n") : " (none)";
|
|
287
|
+
const cycleLines = session.orchestratorCycles.length > 0 ? session.orchestratorCycles.map((c) => {
|
|
288
|
+
const spawnedList = c.agentsSpawned.length > 0 ? c.agentsSpawned.join(", ") : "(none)";
|
|
289
|
+
return `Cycle ${c.cycle}: Spawned ${spawnedList}`;
|
|
290
|
+
}).join("\n") : " (none)";
|
|
291
|
+
return [
|
|
292
|
+
"<state>",
|
|
293
|
+
`session: ${shortId} (cycle ${cycleNum})`,
|
|
294
|
+
`task: ${session.task}`,
|
|
295
|
+
`status: ${session.status}`,
|
|
296
|
+
"",
|
|
297
|
+
"## Tasks",
|
|
298
|
+
taskLines,
|
|
299
|
+
"",
|
|
300
|
+
"## Agents",
|
|
301
|
+
agentLines,
|
|
302
|
+
"",
|
|
303
|
+
"## Previous Cycles",
|
|
304
|
+
cycleLines,
|
|
305
|
+
"</state>"
|
|
306
|
+
].join("\n");
|
|
307
|
+
}
|
|
308
|
+
async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
309
|
+
const session = getSession(cwd, sessionId);
|
|
310
|
+
const basePrompt = loadOrchestratorPrompt(cwd);
|
|
311
|
+
const formattedState = formatStateForOrchestrator(session);
|
|
312
|
+
const fullPrompt = `${basePrompt}
|
|
313
|
+
|
|
314
|
+
${formattedState}`;
|
|
315
|
+
const cycleNum = session.orchestratorCycles.length + 1;
|
|
316
|
+
const promptFilePath = `${sessionDir(cwd, sessionId)}/orchestrator-prompt-${cycleNum}.md`;
|
|
317
|
+
writeFileSync2(promptFilePath, fullPrompt, "utf-8");
|
|
318
|
+
sessionWindowMap.set(sessionId, windowId);
|
|
319
|
+
const envExports = [
|
|
320
|
+
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
321
|
+
`export SISYPHUS_AGENT_ID='orchestrator'`
|
|
322
|
+
].join(" && ");
|
|
323
|
+
const userPrompt = message ? `The user resumed this session with new instructions: ${message}` : "Review the current session state and execute the next cycle of work.";
|
|
324
|
+
const userPromptFilePath = `${sessionDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
325
|
+
writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
|
|
326
|
+
const claudeCmd = `claude --dangerously-skip-permissions --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
327
|
+
const paneId = createPane(windowId, cwd);
|
|
328
|
+
sessionOrchestratorPane.set(sessionId, paneId);
|
|
329
|
+
setPaneTitle(paneId, `orchestrator (${sessionId.slice(0, 8)})`);
|
|
330
|
+
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
331
|
+
sendKeys(paneId, `${envExports} && ${claudeCmd}`);
|
|
332
|
+
await addOrchestratorCycle(cwd, sessionId, {
|
|
333
|
+
cycle: cycleNum,
|
|
334
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
335
|
+
agentsSpawned: [],
|
|
336
|
+
paneId
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function resolveOrchestratorPane(sessionId, cwd) {
|
|
340
|
+
const memPane = sessionOrchestratorPane.get(sessionId);
|
|
341
|
+
if (memPane) return memPane;
|
|
342
|
+
const session = getSession(cwd, sessionId);
|
|
343
|
+
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
344
|
+
return lastCycle?.paneId ?? void 0;
|
|
345
|
+
}
|
|
346
|
+
async function handleOrchestratorYield(sessionId, cwd) {
|
|
347
|
+
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
348
|
+
if (paneId) {
|
|
349
|
+
killPane(paneId);
|
|
350
|
+
sessionOrchestratorPane.delete(sessionId);
|
|
351
|
+
}
|
|
352
|
+
await completeOrchestratorCycle(cwd, sessionId);
|
|
353
|
+
const session = getSession(cwd, sessionId);
|
|
354
|
+
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
355
|
+
if (runningAgents.length === 0) {
|
|
356
|
+
console.error(`[sisyphus] WARNING: Orchestrator yielded but no agents are running for session ${sessionId}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
360
|
+
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
361
|
+
await completeOrchestratorCycle(cwd, sessionId);
|
|
362
|
+
await completeSession(cwd, sessionId, report);
|
|
363
|
+
if (paneId) {
|
|
364
|
+
killPane(paneId);
|
|
365
|
+
sessionOrchestratorPane.delete(sessionId);
|
|
366
|
+
}
|
|
367
|
+
sessionWindowMap.delete(sessionId);
|
|
368
|
+
console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/daemon/agent.ts
|
|
372
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
373
|
+
import { resolve as resolve2 } from "path";
|
|
374
|
+
var agentCounters = /* @__PURE__ */ new Map();
|
|
375
|
+
function resetAgentCounter(sessionId, value = 0) {
|
|
376
|
+
agentCounters.set(sessionId, value);
|
|
377
|
+
}
|
|
378
|
+
function clearAgentCounter(sessionId) {
|
|
379
|
+
agentCounters.delete(sessionId);
|
|
380
|
+
}
|
|
381
|
+
function renderAgentSuffix(sessionId, instruction) {
|
|
382
|
+
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
383
|
+
let template;
|
|
384
|
+
try {
|
|
385
|
+
template = readFileSync4(templatePath, "utf-8");
|
|
386
|
+
} catch {
|
|
387
|
+
template = `# Sisyphus Agent
|
|
388
|
+
Session: {{SESSION_ID}}
|
|
389
|
+
Task: {{INSTRUCTION}}`;
|
|
390
|
+
}
|
|
391
|
+
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction);
|
|
392
|
+
}
|
|
393
|
+
async function spawnAgent(opts) {
|
|
394
|
+
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
395
|
+
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
396
|
+
agentCounters.set(sessionId, count);
|
|
397
|
+
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
398
|
+
const color = getNextColor(sessionId);
|
|
399
|
+
const paneId = createPane(windowId, cwd);
|
|
400
|
+
setPaneTitle(paneId, `${name} (${agentId})`);
|
|
401
|
+
setPaneStyle(paneId, color);
|
|
402
|
+
const suffix = renderAgentSuffix(sessionId, instruction);
|
|
403
|
+
const suffixFilePath = `${sessionDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
404
|
+
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
405
|
+
const envExports = [
|
|
406
|
+
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
407
|
+
`export SISYPHUS_AGENT_ID='${agentId}'`
|
|
408
|
+
].join(" && ");
|
|
409
|
+
const agentFlag = agentType ? ` --agent ${shellQuote2(agentType)}` : "";
|
|
410
|
+
const claudeCmd = `claude --dangerously-skip-permissions${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote2(instruction)}`;
|
|
411
|
+
sendKeys(paneId, `${envExports} && ${claudeCmd}`);
|
|
412
|
+
const agent = {
|
|
413
|
+
id: agentId,
|
|
414
|
+
name,
|
|
415
|
+
agentType,
|
|
416
|
+
color,
|
|
417
|
+
instruction,
|
|
418
|
+
status: "running",
|
|
419
|
+
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
420
|
+
completedAt: null,
|
|
421
|
+
reports: [],
|
|
422
|
+
paneId
|
|
423
|
+
};
|
|
424
|
+
await addAgent(cwd, sessionId, agent);
|
|
425
|
+
return agent;
|
|
426
|
+
}
|
|
427
|
+
function nextReportNumber(cwd, sessionId, agentId) {
|
|
428
|
+
const dir = reportsDir(cwd, sessionId);
|
|
429
|
+
try {
|
|
430
|
+
const files = readdirSync(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
431
|
+
return String(files.length + 1).padStart(3, "0");
|
|
432
|
+
} catch {
|
|
433
|
+
return "001";
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
437
|
+
const dir = reportsDir(cwd, sessionId);
|
|
438
|
+
mkdirSync2(dir, { recursive: true });
|
|
439
|
+
const num = nextReportNumber(cwd, sessionId, agentId);
|
|
440
|
+
const filePath = reportFilePath(cwd, sessionId, agentId, num);
|
|
441
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
442
|
+
const entry = {
|
|
443
|
+
type: "update",
|
|
444
|
+
filePath,
|
|
445
|
+
summary: content.slice(0, 200),
|
|
446
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
447
|
+
};
|
|
448
|
+
await appendAgentReport(cwd, sessionId, agentId, entry);
|
|
449
|
+
}
|
|
450
|
+
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
451
|
+
const dir = reportsDir(cwd, sessionId);
|
|
452
|
+
mkdirSync2(dir, { recursive: true });
|
|
453
|
+
const filePath = reportFilePath(cwd, sessionId, agentId, "final");
|
|
454
|
+
writeFileSync3(filePath, report, "utf-8");
|
|
455
|
+
const entry = {
|
|
456
|
+
type: "final",
|
|
457
|
+
filePath,
|
|
458
|
+
summary: report.slice(0, 200),
|
|
459
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
460
|
+
};
|
|
461
|
+
await appendAgentReport(cwd, sessionId, agentId, entry);
|
|
462
|
+
await updateAgent(cwd, sessionId, agentId, {
|
|
463
|
+
status: "completed",
|
|
464
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
465
|
+
});
|
|
466
|
+
const session = getSession(cwd, sessionId);
|
|
467
|
+
const agent = session.agents.find((a) => a.id === agentId);
|
|
468
|
+
if (agent) {
|
|
469
|
+
killPane(agent.paneId);
|
|
470
|
+
}
|
|
471
|
+
return allAgentsDone(session);
|
|
472
|
+
}
|
|
473
|
+
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
474
|
+
await updateAgent(cwd, sessionId, agentId, {
|
|
475
|
+
status: "killed",
|
|
476
|
+
killedReason: reason,
|
|
477
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
478
|
+
});
|
|
479
|
+
const session = getSession(cwd, sessionId);
|
|
480
|
+
return allAgentsDone(session);
|
|
481
|
+
}
|
|
482
|
+
function allAgentsDone(session) {
|
|
483
|
+
const running = session.agents.filter((a) => a.status === "running");
|
|
484
|
+
return running.length === 0 && session.agents.length > 0;
|
|
485
|
+
}
|
|
486
|
+
function shellQuote2(s) {
|
|
487
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/daemon/pane-monitor.ts
|
|
491
|
+
var monitorInterval = null;
|
|
492
|
+
var onAllAgentsDone = null;
|
|
493
|
+
function setRespawnCallback(cb) {
|
|
494
|
+
onAllAgentsDone = cb;
|
|
495
|
+
}
|
|
496
|
+
function startMonitor(pollIntervalMs = 1e3) {
|
|
497
|
+
if (monitorInterval) return;
|
|
498
|
+
monitorInterval = setInterval(() => {
|
|
499
|
+
pollAllSessions().catch((err) => {
|
|
500
|
+
console.error("[sisyphus] Pane monitor error:", err);
|
|
501
|
+
});
|
|
502
|
+
}, pollIntervalMs);
|
|
503
|
+
}
|
|
504
|
+
function stopMonitor() {
|
|
505
|
+
if (monitorInterval) {
|
|
506
|
+
clearInterval(monitorInterval);
|
|
507
|
+
monitorInterval = null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
var trackedSessions = /* @__PURE__ */ new Map();
|
|
511
|
+
function trackSession(sessionId, cwd, tmuxSession) {
|
|
512
|
+
const existing = trackedSessions.get(sessionId);
|
|
513
|
+
trackedSessions.set(sessionId, { id: sessionId, cwd, tmuxSession, windowId: existing ? existing.windowId : null });
|
|
514
|
+
}
|
|
515
|
+
function updateTrackedWindow(sessionId, windowId) {
|
|
516
|
+
const entry = trackedSessions.get(sessionId);
|
|
517
|
+
if (!entry) throw new Error(`Cannot update window for untracked session: ${sessionId}`);
|
|
518
|
+
entry.windowId = windowId;
|
|
519
|
+
}
|
|
520
|
+
function untrackSession(sessionId) {
|
|
521
|
+
trackedSessions.delete(sessionId);
|
|
522
|
+
}
|
|
523
|
+
async function pollAllSessions() {
|
|
524
|
+
for (const { id: sessionId, cwd, windowId } of trackedSessions.values()) {
|
|
525
|
+
if (windowId) {
|
|
526
|
+
await pollSession(sessionId, cwd, windowId);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async function pollSession(sessionId, cwd, windowId) {
|
|
531
|
+
let session;
|
|
532
|
+
try {
|
|
533
|
+
session = getSession(cwd, sessionId);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (session.status !== "active") return;
|
|
539
|
+
const livePanes = listPanes(windowId);
|
|
540
|
+
if (livePanes.length === 0) return;
|
|
541
|
+
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
542
|
+
for (const agent of session.agents) {
|
|
543
|
+
if (agent.status !== "running") continue;
|
|
544
|
+
if (!livePaneIds.has(agent.paneId)) {
|
|
545
|
+
const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
|
|
546
|
+
if (allDone && onAllAgentsDone) {
|
|
547
|
+
onAllAgentsDone(sessionId, cwd, windowId);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
552
|
+
if (orchPaneId && !livePaneIds.has(orchPaneId)) {
|
|
553
|
+
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
554
|
+
if (runningAgents.length === 0) {
|
|
555
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
556
|
+
console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/daemon/session-manager.ts
|
|
562
|
+
async function startSession(task, cwd, tmuxSession, windowId) {
|
|
563
|
+
const sessionId = uuidv4();
|
|
564
|
+
const session = createSession(sessionId, task, cwd);
|
|
565
|
+
trackSession(sessionId, cwd, tmuxSession);
|
|
566
|
+
await spawnOrchestrator(sessionId, cwd, windowId);
|
|
567
|
+
updateTrackedWindow(sessionId, windowId);
|
|
568
|
+
return session;
|
|
569
|
+
}
|
|
570
|
+
async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
|
|
571
|
+
const session = getSession(cwd, sessionId);
|
|
572
|
+
for (const agent of session.agents) {
|
|
573
|
+
if (agent.status === "running") {
|
|
574
|
+
await updateAgent(cwd, sessionId, agent.id, {
|
|
575
|
+
status: "lost",
|
|
576
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
577
|
+
killedReason: "session resumed \u2014 agent was still running"
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
await updateSessionStatus(cwd, sessionId, "active");
|
|
582
|
+
resetAgentCounter(sessionId, session.agents.length);
|
|
583
|
+
resetColors(sessionId);
|
|
584
|
+
trackSession(sessionId, cwd, tmuxSession);
|
|
585
|
+
await spawnOrchestrator(sessionId, cwd, windowId, message);
|
|
586
|
+
updateTrackedWindow(sessionId, windowId);
|
|
587
|
+
return getSession(cwd, sessionId);
|
|
588
|
+
}
|
|
589
|
+
function getSessionStatus(cwd, sessionId) {
|
|
590
|
+
return getSession(cwd, sessionId);
|
|
591
|
+
}
|
|
592
|
+
function listSessions(cwd) {
|
|
593
|
+
const dir = sessionsDir(cwd);
|
|
594
|
+
if (!existsSync2(dir)) return [];
|
|
595
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
596
|
+
const sessions = [];
|
|
597
|
+
for (const entry of entries) {
|
|
598
|
+
if (!entry.isDirectory()) continue;
|
|
599
|
+
try {
|
|
600
|
+
const session = getSession(cwd, entry.name);
|
|
601
|
+
sessions.push({
|
|
602
|
+
id: session.id,
|
|
603
|
+
task: session.task,
|
|
604
|
+
status: session.status,
|
|
605
|
+
createdAt: session.createdAt,
|
|
606
|
+
agentCount: session.agents.length
|
|
607
|
+
});
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error(`[sisyphus] Failed to read session ${entry.name}:`, err);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return sessions;
|
|
613
|
+
}
|
|
614
|
+
function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
615
|
+
const session = getSession(cwd, sessionId);
|
|
616
|
+
if (session.status !== "active") return;
|
|
617
|
+
setTimeout(() => {
|
|
618
|
+
spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
|
|
619
|
+
}, 2e3);
|
|
620
|
+
}
|
|
621
|
+
async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
|
|
622
|
+
const windowId = getWindowId(sessionId);
|
|
623
|
+
if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
|
|
624
|
+
const agent = await spawnAgent({
|
|
625
|
+
sessionId,
|
|
626
|
+
cwd,
|
|
627
|
+
agentType,
|
|
628
|
+
name,
|
|
629
|
+
instruction,
|
|
630
|
+
windowId
|
|
631
|
+
});
|
|
632
|
+
await appendAgentToLastCycle(cwd, sessionId, agent.id);
|
|
633
|
+
return { agentId: agent.id };
|
|
634
|
+
}
|
|
635
|
+
async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
|
|
636
|
+
const allDone = await handleAgentSubmit(cwd, sessionId, agentId, report);
|
|
637
|
+
if (allDone) {
|
|
638
|
+
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async function handleReport(cwd, sessionId, agentId, content) {
|
|
642
|
+
await handleAgentReport(cwd, sessionId, agentId, content);
|
|
643
|
+
}
|
|
644
|
+
async function handleYield(sessionId, cwd) {
|
|
645
|
+
await handleOrchestratorYield(sessionId, cwd);
|
|
646
|
+
}
|
|
647
|
+
async function handleComplete(sessionId, cwd, report) {
|
|
648
|
+
untrackSession(sessionId);
|
|
649
|
+
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
650
|
+
}
|
|
651
|
+
async function handleTaskAdd(cwd, sessionId, description, initialStatus) {
|
|
652
|
+
const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
|
|
653
|
+
const status = initialStatus !== void 0 && VALID_STATUSES.has(initialStatus) ? initialStatus : void 0;
|
|
654
|
+
const task = await addTask(cwd, sessionId, description, status);
|
|
655
|
+
return { taskId: task.id };
|
|
656
|
+
}
|
|
657
|
+
async function handleTaskUpdate(cwd, sessionId, taskId, status, description) {
|
|
658
|
+
const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
|
|
659
|
+
const updates = {};
|
|
660
|
+
if (status !== void 0) {
|
|
661
|
+
if (!VALID_STATUSES.has(status)) throw new Error(`Invalid status: ${status}. Valid: draft, pending, in_progress, done`);
|
|
662
|
+
updates.status = status;
|
|
663
|
+
}
|
|
664
|
+
if (description !== void 0) updates.description = description;
|
|
665
|
+
await updateTask(cwd, sessionId, taskId, updates);
|
|
666
|
+
}
|
|
667
|
+
function handleTasksList(cwd, sessionId) {
|
|
668
|
+
const session = getSession(cwd, sessionId);
|
|
669
|
+
return { tasks: session.tasks };
|
|
670
|
+
}
|
|
671
|
+
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
672
|
+
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
673
|
+
}
|
|
674
|
+
async function handleKill(sessionId, cwd) {
|
|
675
|
+
const session = getSession(cwd, sessionId);
|
|
676
|
+
const windowId = getWindowId(sessionId);
|
|
677
|
+
let killedAgents = 0;
|
|
678
|
+
for (const agent of session.agents) {
|
|
679
|
+
if (agent.status === "running") {
|
|
680
|
+
await updateAgent(cwd, sessionId, agent.id, {
|
|
681
|
+
status: "killed",
|
|
682
|
+
killedReason: "session killed by user",
|
|
683
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
684
|
+
});
|
|
685
|
+
killedAgents++;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
689
|
+
if (orchPaneId) {
|
|
690
|
+
killPane(orchPaneId);
|
|
691
|
+
}
|
|
692
|
+
await updateSessionStatus(cwd, sessionId, "completed");
|
|
693
|
+
untrackSession(sessionId);
|
|
694
|
+
if (windowId) {
|
|
695
|
+
killWindow(windowId);
|
|
696
|
+
}
|
|
697
|
+
clearAgentCounter(sessionId);
|
|
698
|
+
return killedAgents;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/daemon/server.ts
|
|
702
|
+
var server = null;
|
|
703
|
+
var sessionCwdMap = /* @__PURE__ */ new Map();
|
|
704
|
+
var sessionTmuxMap = /* @__PURE__ */ new Map();
|
|
705
|
+
var sessionWindowMap2 = /* @__PURE__ */ new Map();
|
|
706
|
+
function registryPath() {
|
|
707
|
+
return join2(globalDir(), "session-registry.json");
|
|
708
|
+
}
|
|
709
|
+
function persistSessionRegistry() {
|
|
710
|
+
const dir = globalDir();
|
|
711
|
+
mkdirSync3(dir, { recursive: true });
|
|
712
|
+
const registry = {};
|
|
713
|
+
for (const [id, cwd] of sessionCwdMap) {
|
|
714
|
+
registry[id] = cwd;
|
|
715
|
+
}
|
|
716
|
+
writeFileSync4(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
717
|
+
}
|
|
718
|
+
function loadSessionRegistry() {
|
|
719
|
+
const p = registryPath();
|
|
720
|
+
if (!existsSync3(p)) return {};
|
|
721
|
+
try {
|
|
722
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
723
|
+
} catch {
|
|
724
|
+
return {};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function registerSessionCwd(sessionId, cwd) {
|
|
728
|
+
sessionCwdMap.set(sessionId, cwd);
|
|
729
|
+
persistSessionRegistry();
|
|
730
|
+
}
|
|
731
|
+
async function handleRequest(req) {
|
|
732
|
+
try {
|
|
733
|
+
switch (req.type) {
|
|
734
|
+
case "start": {
|
|
735
|
+
const session = await startSession(req.task, req.cwd, req.tmuxSession, req.tmuxWindow);
|
|
736
|
+
registerSessionCwd(session.id, req.cwd);
|
|
737
|
+
sessionTmuxMap.set(session.id, req.tmuxSession);
|
|
738
|
+
sessionWindowMap2.set(session.id, req.tmuxWindow);
|
|
739
|
+
return { ok: true, data: { sessionId: session.id } };
|
|
740
|
+
}
|
|
741
|
+
case "spawn": {
|
|
742
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
743
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
744
|
+
const result = await handleSpawn(req.sessionId, cwd, req.agentType, req.name, req.instruction);
|
|
745
|
+
return { ok: true, data: { agentId: result.agentId } };
|
|
746
|
+
}
|
|
747
|
+
case "submit": {
|
|
748
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
749
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
750
|
+
const windowId = sessionWindowMap2.get(req.sessionId);
|
|
751
|
+
if (!windowId) return { ok: false, error: `No tmux window found for session: ${req.sessionId}` };
|
|
752
|
+
await handleSubmit(cwd, req.sessionId, req.agentId, req.report, windowId);
|
|
753
|
+
return { ok: true };
|
|
754
|
+
}
|
|
755
|
+
case "report": {
|
|
756
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
757
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
758
|
+
await handleReport(cwd, req.sessionId, req.agentId, req.content);
|
|
759
|
+
return { ok: true };
|
|
760
|
+
}
|
|
761
|
+
case "yield": {
|
|
762
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
763
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
764
|
+
await handleYield(req.sessionId, cwd);
|
|
765
|
+
return { ok: true };
|
|
766
|
+
}
|
|
767
|
+
case "complete": {
|
|
768
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
769
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
770
|
+
await handleComplete(req.sessionId, cwd, req.report);
|
|
771
|
+
return { ok: true };
|
|
772
|
+
}
|
|
773
|
+
case "status": {
|
|
774
|
+
if (req.sessionId) {
|
|
775
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
776
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
777
|
+
const session = getSessionStatus(cwd, req.sessionId);
|
|
778
|
+
return { ok: true, data: { session } };
|
|
779
|
+
}
|
|
780
|
+
return { ok: true, data: { message: "daemon running" } };
|
|
781
|
+
}
|
|
782
|
+
case "tasks_add": {
|
|
783
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
784
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
785
|
+
const result = await handleTaskAdd(cwd, req.sessionId, req.description, req.status);
|
|
786
|
+
return { ok: true, data: { taskId: result.taskId } };
|
|
787
|
+
}
|
|
788
|
+
case "tasks_update": {
|
|
789
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
790
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
791
|
+
await handleTaskUpdate(cwd, req.sessionId, req.taskId, req.status, req.description);
|
|
792
|
+
return { ok: true };
|
|
793
|
+
}
|
|
794
|
+
case "tasks_list": {
|
|
795
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
796
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
797
|
+
const result = handleTasksList(cwd, req.sessionId);
|
|
798
|
+
return { ok: true, data: { tasks: result.tasks } };
|
|
799
|
+
}
|
|
800
|
+
case "list": {
|
|
801
|
+
const allSessions = [];
|
|
802
|
+
const seenCwds = /* @__PURE__ */ new Set();
|
|
803
|
+
for (const cwd of sessionCwdMap.values()) {
|
|
804
|
+
if (seenCwds.has(cwd)) continue;
|
|
805
|
+
seenCwds.add(cwd);
|
|
806
|
+
const sessions = listSessions(cwd);
|
|
807
|
+
allSessions.push(...sessions.map((s) => s));
|
|
808
|
+
}
|
|
809
|
+
return { ok: true, data: { sessions: allSessions } };
|
|
810
|
+
}
|
|
811
|
+
case "resume": {
|
|
812
|
+
let cwd = sessionCwdMap.get(req.sessionId);
|
|
813
|
+
if (!cwd) {
|
|
814
|
+
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
815
|
+
if (existsSync3(stateFile)) {
|
|
816
|
+
cwd = req.cwd;
|
|
817
|
+
registerSessionCwd(req.sessionId, cwd);
|
|
818
|
+
} else {
|
|
819
|
+
return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
sessionTmuxMap.set(req.sessionId, req.tmuxSession);
|
|
823
|
+
sessionWindowMap2.set(req.sessionId, req.tmuxWindow);
|
|
824
|
+
const session = await resumeSession(req.sessionId, cwd, req.tmuxSession, req.tmuxWindow, req.message);
|
|
825
|
+
return { ok: true, data: { sessionId: session.id, status: session.status } };
|
|
826
|
+
}
|
|
827
|
+
case "register_claude_session": {
|
|
828
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
829
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
830
|
+
await handleRegisterClaudeSession(cwd, req.sessionId, req.agentId, req.claudeSessionId);
|
|
831
|
+
return { ok: true };
|
|
832
|
+
}
|
|
833
|
+
case "kill": {
|
|
834
|
+
const cwd = sessionCwdMap.get(req.sessionId);
|
|
835
|
+
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
836
|
+
const killedAgents = await handleKill(req.sessionId, cwd);
|
|
837
|
+
sessionCwdMap.delete(req.sessionId);
|
|
838
|
+
sessionTmuxMap.delete(req.sessionId);
|
|
839
|
+
sessionWindowMap2.delete(req.sessionId);
|
|
840
|
+
persistSessionRegistry();
|
|
841
|
+
return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
|
|
842
|
+
}
|
|
843
|
+
default:
|
|
844
|
+
return { ok: false, error: `Unknown request type: ${req.type}` };
|
|
845
|
+
}
|
|
846
|
+
} catch (err) {
|
|
847
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
848
|
+
return { ok: false, error: message };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function startServer() {
|
|
852
|
+
return new Promise((resolve3, reject) => {
|
|
853
|
+
const sock = socketPath();
|
|
854
|
+
if (existsSync3(sock)) {
|
|
855
|
+
unlinkSync(sock);
|
|
856
|
+
}
|
|
857
|
+
server = createServer((conn) => {
|
|
858
|
+
let buffer = "";
|
|
859
|
+
conn.on("data", (chunk) => {
|
|
860
|
+
buffer += chunk.toString();
|
|
861
|
+
const lines = buffer.split("\n");
|
|
862
|
+
buffer = lines.pop();
|
|
863
|
+
for (const line of lines) {
|
|
864
|
+
if (!line.trim()) continue;
|
|
865
|
+
let req;
|
|
866
|
+
try {
|
|
867
|
+
req = JSON.parse(line);
|
|
868
|
+
} catch {
|
|
869
|
+
conn.write(JSON.stringify({ ok: false, error: "Invalid JSON" }) + "\n");
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
handleRequest(req).then((res) => {
|
|
873
|
+
conn.write(JSON.stringify(res) + "\n");
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
conn.on("error", (err) => {
|
|
878
|
+
console.error("[sisyphus] Connection error:", err.message);
|
|
879
|
+
});
|
|
880
|
+
});
|
|
881
|
+
server.on("error", reject);
|
|
882
|
+
server.listen(sock, () => {
|
|
883
|
+
console.log(`[sisyphus] Daemon listening on ${sock}`);
|
|
884
|
+
resolve3(server);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
function stopServer() {
|
|
889
|
+
return new Promise((resolve3) => {
|
|
890
|
+
if (!server) {
|
|
891
|
+
resolve3();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
server.close(() => {
|
|
895
|
+
const sock = socketPath();
|
|
896
|
+
if (existsSync3(sock)) {
|
|
897
|
+
unlinkSync(sock);
|
|
898
|
+
}
|
|
899
|
+
server = null;
|
|
900
|
+
resolve3();
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// src/daemon/index.ts
|
|
906
|
+
function ensureDirs() {
|
|
907
|
+
mkdirSync4(globalDir(), { recursive: true });
|
|
908
|
+
}
|
|
909
|
+
function isProcessAlive(pid) {
|
|
910
|
+
try {
|
|
911
|
+
process.kill(pid, 0);
|
|
912
|
+
return true;
|
|
913
|
+
} catch {
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
function acquirePidLock() {
|
|
918
|
+
const pidFile = daemonPidPath();
|
|
919
|
+
try {
|
|
920
|
+
const existing = parseInt(readFileSync6(pidFile, "utf-8").trim(), 10);
|
|
921
|
+
if (existing && isProcessAlive(existing)) {
|
|
922
|
+
console.error(`[sisyphus] Daemon already running (pid ${existing}). Kill it first or remove ${pidFile}`);
|
|
923
|
+
process.exit(1);
|
|
924
|
+
}
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
writeFileSync5(pidFile, String(process.pid), "utf-8");
|
|
928
|
+
}
|
|
929
|
+
function releasePidLock() {
|
|
930
|
+
try {
|
|
931
|
+
unlinkSync2(daemonPidPath());
|
|
932
|
+
} catch {
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function recoverSessions() {
|
|
936
|
+
const registry = loadSessionRegistry();
|
|
937
|
+
const entries = Object.entries(registry);
|
|
938
|
+
if (entries.length === 0) {
|
|
939
|
+
console.log("[sisyphus] No sessions to recover");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
let recovered = 0;
|
|
943
|
+
for (const [sessionId, cwd] of entries) {
|
|
944
|
+
const stateFile = statePath(cwd, sessionId);
|
|
945
|
+
if (!existsSync4(stateFile)) {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
try {
|
|
949
|
+
const session = JSON.parse(readFileSync6(stateFile, "utf-8"));
|
|
950
|
+
if (session.status === "active" || session.status === "paused") {
|
|
951
|
+
registerSessionCwd(sessionId, cwd);
|
|
952
|
+
recovered++;
|
|
953
|
+
}
|
|
954
|
+
} catch {
|
|
955
|
+
console.error(`[sisyphus] Failed to read session state for ${sessionId}, skipping`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
console.log(`[sisyphus] Recovered ${recovered} session(s) from registry`);
|
|
959
|
+
}
|
|
960
|
+
async function main() {
|
|
961
|
+
console.log("[sisyphus] Starting daemon...");
|
|
962
|
+
ensureDirs();
|
|
963
|
+
acquirePidLock();
|
|
964
|
+
const config = loadConfig(process.cwd());
|
|
965
|
+
setRespawnCallback(onAllAgentsDone2);
|
|
966
|
+
await startServer();
|
|
967
|
+
startMonitor(config.pollIntervalMs);
|
|
968
|
+
recoverSessions();
|
|
969
|
+
const shutdown = async () => {
|
|
970
|
+
console.log("[sisyphus] Shutting down...");
|
|
971
|
+
stopMonitor();
|
|
972
|
+
await stopServer();
|
|
973
|
+
releasePidLock();
|
|
974
|
+
process.exit(0);
|
|
975
|
+
};
|
|
976
|
+
process.on("SIGTERM", shutdown);
|
|
977
|
+
process.on("SIGINT", shutdown);
|
|
978
|
+
}
|
|
979
|
+
main().catch((err) => {
|
|
980
|
+
console.error("[sisyphus] Fatal error:", err);
|
|
981
|
+
process.exit(1);
|
|
982
|
+
});
|
|
983
|
+
//# sourceMappingURL=daemon.js.map
|