sisyphi 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -33
- package/dist/{chunk-FWHTKXN5.js → chunk-N2BPQOO2.js} +23 -3
- package/dist/chunk-N2BPQOO2.js.map +1 -0
- package/dist/cli.js +85 -162
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +603 -186
- package/dist/daemon.js.map +1 -1
- package/dist/templates/CLAUDE.md +50 -0
- package/dist/templates/agent-plugin/.claude/agents/debug.md +39 -0
- package/dist/templates/agent-plugin/.claude/agents/plan.md +101 -0
- package/dist/templates/agent-plugin/.claude/agents/review-plan.md +81 -0
- package/dist/templates/agent-plugin/.claude/agents/review.md +56 -0
- package/dist/templates/agent-plugin/.claude/agents/spec-draft.md +73 -0
- package/dist/templates/agent-plugin/.claude/agents/test-spec.md +56 -0
- package/dist/templates/agent-plugin/.claude-plugin/plugin.json +5 -0
- package/dist/templates/agent-plugin/agents/CLAUDE.md +52 -0
- package/dist/templates/agent-plugin/agents/debug.md +39 -0
- package/dist/templates/agent-plugin/agents/operator.md +56 -0
- package/dist/templates/agent-plugin/agents/plan.md +101 -0
- package/dist/templates/agent-plugin/agents/review-plan.md +81 -0
- package/dist/templates/agent-plugin/agents/review.md +56 -0
- package/dist/templates/agent-plugin/agents/spec-draft.md +73 -0
- package/dist/templates/agent-plugin/agents/test-spec.md +56 -0
- package/dist/templates/agent-suffix.md +3 -1
- package/dist/templates/banner.txt +24 -6
- package/dist/templates/orchestrator-plugin/.claude/commands/begin.md +62 -0
- package/dist/templates/orchestrator-plugin/.claude/skills/orchestration/SKILL.md +40 -0
- package/dist/templates/orchestrator-plugin/.claude/skills/orchestration/task-patterns.md +222 -0
- package/dist/templates/orchestrator-plugin/.claude/skills/orchestration/workflow-examples.md +208 -0
- package/dist/templates/orchestrator-plugin/.claude-plugin/plugin.json +5 -0
- package/dist/templates/orchestrator-plugin/hooks/hooks.json +25 -0
- package/dist/templates/orchestrator-plugin/scripts/block-task.sh +4 -0
- package/dist/templates/orchestrator-plugin/scripts/stop-suggest.sh +4 -0
- package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +111 -0
- package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +40 -0
- package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +248 -0
- package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +237 -0
- package/dist/templates/orchestrator-settings.json +2 -0
- package/dist/templates/orchestrator.md +56 -49
- package/dist/templates/resources/.claude/agents/debug.md +39 -0
- package/dist/templates/resources/.claude/agents/plan.md +101 -0
- package/dist/templates/resources/.claude/agents/review-plan.md +81 -0
- package/dist/templates/resources/.claude/agents/review.md +56 -0
- package/dist/templates/resources/.claude/agents/spec-draft.md +73 -0
- package/dist/templates/resources/.claude/agents/test-spec.md +56 -0
- package/dist/templates/resources/.claude/commands/begin.md +62 -0
- package/dist/templates/resources/.claude/skills/orchestration/SKILL.md +40 -0
- package/dist/templates/resources/.claude/skills/orchestration/task-patterns.md +222 -0
- package/dist/templates/resources/.claude/skills/orchestration/workflow-examples.md +208 -0
- package/dist/templates/resources/.claude-plugin/plugin.json +8 -0
- package/package.json +2 -2
- package/templates/CLAUDE.md +50 -0
- package/templates/agent-plugin/.claude-plugin/plugin.json +5 -0
- package/templates/agent-plugin/agents/CLAUDE.md +52 -0
- package/templates/agent-plugin/agents/debug.md +39 -0
- package/templates/agent-plugin/agents/operator.md +56 -0
- package/templates/agent-plugin/agents/plan.md +101 -0
- package/templates/agent-plugin/agents/review-plan.md +81 -0
- package/templates/agent-plugin/agents/review.md +56 -0
- package/templates/agent-plugin/agents/spec-draft.md +73 -0
- package/templates/agent-plugin/agents/test-spec.md +56 -0
- package/templates/agent-suffix.md +3 -1
- package/templates/banner.txt +24 -6
- package/templates/orchestrator-plugin/.claude-plugin/plugin.json +5 -0
- package/templates/orchestrator-plugin/hooks/hooks.json +25 -0
- package/templates/orchestrator-plugin/scripts/block-task.sh +4 -0
- package/templates/orchestrator-plugin/scripts/stop-suggest.sh +4 -0
- package/templates/orchestrator-plugin/skills/git-management/SKILL.md +111 -0
- package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +40 -0
- package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +248 -0
- package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +237 -0
- package/templates/orchestrator-settings.json +2 -0
- package/templates/orchestrator.md +56 -49
- package/dist/chunk-FWHTKXN5.js.map +0 -1
package/dist/daemon.js
CHANGED
|
@@ -4,18 +4,23 @@ import {
|
|
|
4
4
|
daemonPidPath,
|
|
5
5
|
globalConfigPath,
|
|
6
6
|
globalDir,
|
|
7
|
+
logsPath,
|
|
8
|
+
planPath,
|
|
7
9
|
projectConfigPath,
|
|
8
10
|
projectOrchestratorPromptPath,
|
|
11
|
+
promptsDir,
|
|
9
12
|
reportFilePath,
|
|
10
13
|
reportsDir,
|
|
11
14
|
sessionDir,
|
|
12
15
|
sessionsDir,
|
|
13
16
|
socketPath,
|
|
14
|
-
statePath
|
|
15
|
-
|
|
17
|
+
statePath,
|
|
18
|
+
worktreeBaseDir,
|
|
19
|
+
worktreeConfigPath
|
|
20
|
+
} from "./chunk-N2BPQOO2.js";
|
|
16
21
|
|
|
17
22
|
// src/daemon/index.ts
|
|
18
|
-
import { mkdirSync as
|
|
23
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
|
|
19
24
|
|
|
20
25
|
// src/shared/config.ts
|
|
21
26
|
import { readFileSync } from "fs";
|
|
@@ -38,17 +43,33 @@ function loadConfig(cwd) {
|
|
|
38
43
|
|
|
39
44
|
// src/daemon/server.ts
|
|
40
45
|
import { createServer } from "net";
|
|
41
|
-
import { unlinkSync, existsSync as
|
|
42
|
-
import { join as
|
|
46
|
+
import { unlinkSync, existsSync as existsSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
47
|
+
import { join as join4 } from "path";
|
|
43
48
|
|
|
44
49
|
// src/daemon/session-manager.ts
|
|
45
50
|
import { v4 as uuidv4 } from "uuid";
|
|
46
|
-
import { existsSync as
|
|
51
|
+
import { existsSync as existsSync4, readdirSync as readdirSync4 } from "fs";
|
|
47
52
|
|
|
48
53
|
// src/daemon/state.ts
|
|
49
54
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
50
55
|
import { dirname, join } from "path";
|
|
51
56
|
import { randomUUID } from "crypto";
|
|
57
|
+
var PLAN_SEED = `---
|
|
58
|
+
description: >
|
|
59
|
+
Living document of what still needs to happen. Write your remaining work plan
|
|
60
|
+
here: phases, next steps, file references, open questions. Remove or collapse
|
|
61
|
+
items as they're completed so this file only reflects outstanding work. The
|
|
62
|
+
orchestrator sees this every cycle \u2014 keep it focused and current.
|
|
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. Unlike plan.md, entries here accumulate \u2014 they're a log.
|
|
71
|
+
---
|
|
72
|
+
`;
|
|
52
73
|
var sessionLocks = /* @__PURE__ */ new Map();
|
|
53
74
|
async function withSessionLock(sessionId, fn) {
|
|
54
75
|
const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
|
|
@@ -74,13 +95,15 @@ function createSession(id, task, cwd) {
|
|
|
74
95
|
const dir = sessionDir(cwd, id);
|
|
75
96
|
mkdirSync(dir, { recursive: true });
|
|
76
97
|
mkdirSync(contextDir(cwd, id), { recursive: true });
|
|
98
|
+
mkdirSync(promptsDir(cwd, id), { recursive: true });
|
|
99
|
+
writeFileSync(planPath(cwd, id), PLAN_SEED, "utf-8");
|
|
100
|
+
writeFileSync(logsPath(cwd, id), LOGS_SEED, "utf-8");
|
|
77
101
|
const session = {
|
|
78
102
|
id,
|
|
79
103
|
task,
|
|
80
104
|
cwd,
|
|
81
105
|
status: "active",
|
|
82
106
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
83
|
-
tasks: [],
|
|
84
107
|
agents: [],
|
|
85
108
|
orchestratorCycles: []
|
|
86
109
|
};
|
|
@@ -94,30 +117,6 @@ function getSession(cwd, sessionId) {
|
|
|
94
117
|
function saveSession(session) {
|
|
95
118
|
atomicWrite(statePath(session.cwd, session.id), JSON.stringify(session, null, 2));
|
|
96
119
|
}
|
|
97
|
-
async function addTask(cwd, sessionId, description, initialStatus) {
|
|
98
|
-
return withSessionLock(sessionId, () => {
|
|
99
|
-
const session = getSession(cwd, sessionId);
|
|
100
|
-
const nextNum = session.tasks.length + 1;
|
|
101
|
-
const task = {
|
|
102
|
-
id: `t${nextNum}`,
|
|
103
|
-
description,
|
|
104
|
-
status: initialStatus !== void 0 ? initialStatus : "pending"
|
|
105
|
-
};
|
|
106
|
-
session.tasks.push(task);
|
|
107
|
-
saveSession(session);
|
|
108
|
-
return task;
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
async function updateTask(cwd, sessionId, taskId, updates) {
|
|
112
|
-
return withSessionLock(sessionId, () => {
|
|
113
|
-
const session = getSession(cwd, sessionId);
|
|
114
|
-
const task = session.tasks.find((t) => t.id === taskId);
|
|
115
|
-
if (!task) throw new Error(`Task ${taskId} not found in session ${sessionId}`);
|
|
116
|
-
if (updates.status) task.status = updates.status;
|
|
117
|
-
if (updates.description) task.description = updates.description;
|
|
118
|
-
saveSession(session);
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
120
|
async function addAgent(cwd, sessionId, agent) {
|
|
122
121
|
return withSessionLock(sessionId, () => {
|
|
123
122
|
const session = getSession(cwd, sessionId);
|
|
@@ -128,7 +127,7 @@ async function addAgent(cwd, sessionId, agent) {
|
|
|
128
127
|
async function updateAgent(cwd, sessionId, agentId, updates) {
|
|
129
128
|
return withSessionLock(sessionId, () => {
|
|
130
129
|
const session = getSession(cwd, sessionId);
|
|
131
|
-
const agent = session.agents.find((a) => a.id === agentId);
|
|
130
|
+
const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
|
|
132
131
|
if (!agent) throw new Error(`Agent ${agentId} not found in session ${sessionId}`);
|
|
133
132
|
Object.assign(agent, updates);
|
|
134
133
|
saveSession(session);
|
|
@@ -172,12 +171,20 @@ async function completeSession(cwd, sessionId, report) {
|
|
|
172
171
|
async function appendAgentReport(cwd, sessionId, agentId, entry) {
|
|
173
172
|
return withSessionLock(sessionId, () => {
|
|
174
173
|
const session = getSession(cwd, sessionId);
|
|
175
|
-
const agent = session.agents.find((a) => a.id === agentId);
|
|
174
|
+
const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
|
|
176
175
|
if (!agent) throw new Error(`Agent ${agentId} not found in session ${sessionId}`);
|
|
177
176
|
agent.reports.push(entry);
|
|
178
177
|
saveSession(session);
|
|
179
178
|
});
|
|
180
179
|
}
|
|
180
|
+
async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId) {
|
|
181
|
+
return withSessionLock(sessionId, () => {
|
|
182
|
+
const session = getSession(cwd, sessionId);
|
|
183
|
+
session.tmuxSessionName = tmuxSessionName;
|
|
184
|
+
session.tmuxWindowId = tmuxWindowId;
|
|
185
|
+
saveSession(session);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
181
188
|
async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
|
|
182
189
|
return withSessionLock(sessionId, () => {
|
|
183
190
|
const session = getSession(cwd, sessionId);
|
|
@@ -191,9 +198,83 @@ async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
|
|
|
191
198
|
}
|
|
192
199
|
|
|
193
200
|
// src/daemon/orchestrator.ts
|
|
194
|
-
import { readFileSync as
|
|
201
|
+
import { existsSync, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
195
202
|
import { resolve } from "path";
|
|
196
203
|
|
|
204
|
+
// src/daemon/colors.ts
|
|
205
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
206
|
+
import { homedir } from "os";
|
|
207
|
+
import { join as join2 } from "path";
|
|
208
|
+
var ORCHESTRATOR_COLOR = "yellow";
|
|
209
|
+
var AGENT_PALETTE = ["blue", "green", "magenta", "cyan", "red", "white"];
|
|
210
|
+
var TMUX_COLOR_MAP = {
|
|
211
|
+
orange: "colour208",
|
|
212
|
+
teal: "colour6"
|
|
213
|
+
};
|
|
214
|
+
function normalizeTmuxColor(color) {
|
|
215
|
+
return TMUX_COLOR_MAP[color] ?? color;
|
|
216
|
+
}
|
|
217
|
+
var sessionColorIndex = /* @__PURE__ */ new Map();
|
|
218
|
+
function getNextColor(sessionId) {
|
|
219
|
+
const idx = sessionColorIndex.get(sessionId) ?? 0;
|
|
220
|
+
const color = AGENT_PALETTE[idx % AGENT_PALETTE.length];
|
|
221
|
+
sessionColorIndex.set(sessionId, idx + 1);
|
|
222
|
+
return color;
|
|
223
|
+
}
|
|
224
|
+
function resetColors(sessionId) {
|
|
225
|
+
sessionColorIndex.delete(sessionId);
|
|
226
|
+
}
|
|
227
|
+
function extractFrontmatterColor(content) {
|
|
228
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
229
|
+
if (!match) return null;
|
|
230
|
+
const colorMatch = match[1].match(/^color:\s*(.+)$/m);
|
|
231
|
+
return colorMatch ? colorMatch[1].trim() : null;
|
|
232
|
+
}
|
|
233
|
+
function findPluginInstallPath(namespace) {
|
|
234
|
+
try {
|
|
235
|
+
const registryPath2 = join2(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
236
|
+
const registry = JSON.parse(readFileSync3(registryPath2, "utf-8"));
|
|
237
|
+
for (const key of Object.keys(registry)) {
|
|
238
|
+
if (key.startsWith(`${namespace}@`)) {
|
|
239
|
+
return registry[key].installPath ?? null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function resolveAgentTypeColor(agentType, pluginDir, cwd) {
|
|
247
|
+
if (!agentType) return null;
|
|
248
|
+
let namespace;
|
|
249
|
+
let name;
|
|
250
|
+
if (agentType.includes(":")) {
|
|
251
|
+
[namespace, name] = agentType.split(":", 2);
|
|
252
|
+
} else {
|
|
253
|
+
name = agentType;
|
|
254
|
+
}
|
|
255
|
+
const searchPaths = [];
|
|
256
|
+
if (namespace) {
|
|
257
|
+
searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
|
|
258
|
+
const installPath = findPluginInstallPath(namespace);
|
|
259
|
+
if (installPath) {
|
|
260
|
+
searchPaths.push(join2(installPath, "agents", `${name}.md`));
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
searchPaths.push(join2(cwd, ".claude", "agents", `${name}.md`));
|
|
264
|
+
searchPaths.push(join2(homedir(), ".claude", "agents", `${name}.md`));
|
|
265
|
+
searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
|
|
266
|
+
}
|
|
267
|
+
for (const path of searchPaths) {
|
|
268
|
+
try {
|
|
269
|
+
const content = readFileSync3(path, "utf-8");
|
|
270
|
+
const color = extractFrontmatterColor(content);
|
|
271
|
+
if (color) return normalizeTmuxColor(color);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
197
278
|
// src/daemon/tmux.ts
|
|
198
279
|
import { execSync } from "child_process";
|
|
199
280
|
var EXEC_ENV = {
|
|
@@ -216,8 +297,8 @@ function createPane(windowTarget, cwd) {
|
|
|
216
297
|
execSafe(`tmux select-layout -t "${windowTarget}" even-horizontal`);
|
|
217
298
|
return paneId;
|
|
218
299
|
}
|
|
219
|
-
function sendKeys(paneTarget,
|
|
220
|
-
exec(`tmux send-keys -t "${paneTarget}" ${shellQuote(
|
|
300
|
+
function sendKeys(paneTarget, command2) {
|
|
301
|
+
exec(`tmux send-keys -t "${paneTarget}" ${shellQuote(command2)} Enter`);
|
|
221
302
|
}
|
|
222
303
|
function killPane(paneTarget) {
|
|
223
304
|
execSafe(`tmux kill-pane -t "${paneTarget}"`);
|
|
@@ -239,48 +320,43 @@ function setPaneTitle(paneTarget, title) {
|
|
|
239
320
|
function setPaneStyle(paneTarget, color) {
|
|
240
321
|
const fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path} #[default]`;
|
|
241
322
|
execSafe(`tmux set -p -t "${paneTarget}" pane-border-format ${shellQuote(fmt)}`);
|
|
242
|
-
execSafe(`tmux set -p -t "${paneTarget}"
|
|
243
|
-
execSafe(`tmux set -
|
|
323
|
+
execSafe(`tmux set -p -t "${paneTarget}" @pane_color "${color}"`);
|
|
324
|
+
execSafe(`tmux set -w -t "${paneTarget}" pane-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
|
|
325
|
+
execSafe(`tmux set -w -t "${paneTarget}" pane-active-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
|
|
326
|
+
}
|
|
327
|
+
function selectLayout(windowTarget, layout = "even-horizontal") {
|
|
328
|
+
execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
|
|
244
329
|
}
|
|
245
330
|
function shellQuote(s) {
|
|
246
331
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
247
332
|
}
|
|
248
333
|
|
|
249
|
-
// src/daemon/colors.ts
|
|
250
|
-
var ORCHESTRATOR_COLOR = "yellow";
|
|
251
|
-
var AGENT_PALETTE = ["blue", "green", "magenta", "cyan", "red", "white"];
|
|
252
|
-
var sessionColorIndex = /* @__PURE__ */ new Map();
|
|
253
|
-
function getNextColor(sessionId) {
|
|
254
|
-
const idx = sessionColorIndex.get(sessionId) ?? 0;
|
|
255
|
-
const color = AGENT_PALETTE[idx % AGENT_PALETTE.length];
|
|
256
|
-
sessionColorIndex.set(sessionId, idx + 1);
|
|
257
|
-
return color;
|
|
258
|
-
}
|
|
259
|
-
function resetColors(sessionId) {
|
|
260
|
-
sessionColorIndex.delete(sessionId);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
334
|
// src/daemon/orchestrator.ts
|
|
264
335
|
var sessionWindowMap = /* @__PURE__ */ new Map();
|
|
265
336
|
var sessionOrchestratorPane = /* @__PURE__ */ new Map();
|
|
266
337
|
function getWindowId(sessionId) {
|
|
267
338
|
return sessionWindowMap.get(sessionId);
|
|
268
339
|
}
|
|
340
|
+
function setWindowId(sessionId, windowId) {
|
|
341
|
+
sessionWindowMap.set(sessionId, windowId);
|
|
342
|
+
}
|
|
269
343
|
function getOrchestratorPaneId(sessionId) {
|
|
270
344
|
return sessionOrchestratorPane.get(sessionId);
|
|
271
345
|
}
|
|
346
|
+
function setOrchestratorPaneId(sessionId, paneId) {
|
|
347
|
+
sessionOrchestratorPane.set(sessionId, paneId);
|
|
348
|
+
}
|
|
272
349
|
function loadOrchestratorPrompt(cwd) {
|
|
273
350
|
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
274
351
|
if (existsSync(projectPath)) {
|
|
275
|
-
return
|
|
352
|
+
return readFileSync4(projectPath, "utf-8");
|
|
276
353
|
}
|
|
277
354
|
const bundledPath = resolve(import.meta.dirname, "../templates/orchestrator.md");
|
|
278
|
-
return
|
|
355
|
+
return readFileSync4(bundledPath, "utf-8");
|
|
279
356
|
}
|
|
280
357
|
function formatStateForOrchestrator(session) {
|
|
281
358
|
const shortId = session.id.slice(0, 8);
|
|
282
359
|
const cycleNum = session.orchestratorCycles.length;
|
|
283
|
-
const taskLines = session.tasks.length > 0 ? session.tasks.map((t) => `- ${t.id}: [${t.status}] ${t.description}`).join("\n") : " (none)";
|
|
284
360
|
const ctxDir = contextDir(session.cwd, session.id);
|
|
285
361
|
let contextLines;
|
|
286
362
|
if (existsSync(ctxDir)) {
|
|
@@ -289,6 +365,10 @@ function formatStateForOrchestrator(session) {
|
|
|
289
365
|
} else {
|
|
290
366
|
contextLines = " (none)";
|
|
291
367
|
}
|
|
368
|
+
const planFile = planPath(session.cwd, session.id);
|
|
369
|
+
const planRef = existsSync(planFile) ? `@${planFile}` : "(empty)";
|
|
370
|
+
const logsFile = logsPath(session.cwd, session.id);
|
|
371
|
+
const logsRef = existsSync(logsFile) ? `@${logsFile}` : "(empty)";
|
|
292
372
|
const agentLines = session.agents.length > 0 ? session.agents.map((a) => {
|
|
293
373
|
const header = `- ${a.id} (${a.name}): ${a.status} \u2014 ${a.reports.length} report(s)`;
|
|
294
374
|
if (a.reports.length === 0) return header;
|
|
@@ -303,32 +383,54 @@ function formatStateForOrchestrator(session) {
|
|
|
303
383
|
const spawnedList = c.agentsSpawned.length > 0 ? c.agentsSpawned.join(", ") : "(none)";
|
|
304
384
|
return `Cycle ${c.cycle}: Spawned ${spawnedList}`;
|
|
305
385
|
}).join("\n") : " (none)";
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
""
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
386
|
+
const worktreeAgents = session.agents.filter((a) => a.worktreePath);
|
|
387
|
+
let worktreeSection = "";
|
|
388
|
+
if (worktreeAgents.length > 0) {
|
|
389
|
+
const wtLines = worktreeAgents.map((a) => {
|
|
390
|
+
if (a.mergeStatus === "conflict") {
|
|
391
|
+
return `- ${a.id}: conflict \u2014 ${a.mergeDetails ?? "unknown"}
|
|
392
|
+
Branch: ${a.branchName}
|
|
393
|
+
Worktree: ${a.worktreePath}`;
|
|
394
|
+
}
|
|
395
|
+
const status = a.mergeStatus ?? "pending";
|
|
396
|
+
return `- ${a.id}: ${status} (branch ${a.branchName})`;
|
|
397
|
+
}).join("\n");
|
|
398
|
+
worktreeSection = `
|
|
399
|
+
|
|
400
|
+
## Worktrees
|
|
401
|
+
${wtLines}`;
|
|
402
|
+
}
|
|
403
|
+
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.";
|
|
404
|
+
return `<state>
|
|
405
|
+
session: ${shortId} (cycle ${cycleNum})
|
|
406
|
+
task: ${session.task}
|
|
407
|
+
status: ${session.status}
|
|
408
|
+
|
|
409
|
+
## Plan
|
|
410
|
+
${planRef}
|
|
411
|
+
|
|
412
|
+
## Logs
|
|
413
|
+
${logsRef}
|
|
414
|
+
|
|
415
|
+
## Agents
|
|
416
|
+
${agentLines}${worktreeSection}
|
|
417
|
+
|
|
418
|
+
## Previous Cycles
|
|
419
|
+
${cycleLines}
|
|
420
|
+
|
|
421
|
+
## Context Files
|
|
422
|
+
${contextLines}
|
|
423
|
+
|
|
424
|
+
## Git Worktrees
|
|
425
|
+
${worktreeHint}
|
|
426
|
+
</state>`;
|
|
325
427
|
}
|
|
326
428
|
async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
327
429
|
const session = getSession(cwd, sessionId);
|
|
328
430
|
const basePrompt = loadOrchestratorPrompt(cwd);
|
|
329
431
|
const formattedState = formatStateForOrchestrator(session);
|
|
330
432
|
const cycleNum = session.orchestratorCycles.length + 1;
|
|
331
|
-
const promptFilePath = `${
|
|
433
|
+
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
332
434
|
writeFileSync2(promptFilePath, basePrompt, "utf-8");
|
|
333
435
|
sessionWindowMap.set(sessionId, windowId);
|
|
334
436
|
const envExports = [
|
|
@@ -350,12 +452,14 @@ ${storedPrompt}`;
|
|
|
350
452
|
} else {
|
|
351
453
|
userPrompt = `${formattedState}
|
|
352
454
|
|
|
353
|
-
Review the current session
|
|
455
|
+
Review the current session and delegate the next cycle of work.`;
|
|
354
456
|
}
|
|
355
457
|
}
|
|
356
|
-
const userPromptFilePath = `${
|
|
458
|
+
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
357
459
|
writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
|
|
358
|
-
const
|
|
460
|
+
const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
|
|
461
|
+
const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
|
|
462
|
+
const claudeCmd = `claude --dangerously-skip-permissions --settings "${settingsPath}" --plugin-dir "${pluginPath}" --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
359
463
|
const paneId = createPane(windowId, cwd);
|
|
360
464
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
361
465
|
setPaneTitle(paneId, `orchestrator (${sessionId.slice(0, 8)})`);
|
|
@@ -383,6 +487,8 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
383
487
|
killPane(paneId);
|
|
384
488
|
sessionOrchestratorPane.delete(sessionId);
|
|
385
489
|
}
|
|
490
|
+
const windowId = sessionWindowMap.get(sessionId);
|
|
491
|
+
if (windowId) selectLayout(windowId);
|
|
386
492
|
await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
|
|
387
493
|
const session = getSession(cwd, sessionId);
|
|
388
494
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
@@ -391,59 +497,237 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
391
497
|
}
|
|
392
498
|
}
|
|
393
499
|
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
394
|
-
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
395
500
|
await completeOrchestratorCycle(cwd, sessionId);
|
|
396
501
|
await completeSession(cwd, sessionId, report);
|
|
397
|
-
if (paneId) {
|
|
398
|
-
killPane(paneId);
|
|
399
|
-
sessionOrchestratorPane.delete(sessionId);
|
|
400
|
-
}
|
|
401
|
-
sessionWindowMap.delete(sessionId);
|
|
402
502
|
console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
|
|
403
503
|
}
|
|
504
|
+
function cleanupSessionMaps(sessionId) {
|
|
505
|
+
sessionOrchestratorPane.delete(sessionId);
|
|
506
|
+
sessionWindowMap.delete(sessionId);
|
|
507
|
+
}
|
|
404
508
|
|
|
405
509
|
// src/daemon/agent.ts
|
|
406
|
-
import { readFileSync as
|
|
510
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
|
|
407
511
|
import { resolve as resolve2 } from "path";
|
|
512
|
+
|
|
513
|
+
// src/daemon/worktree.ts
|
|
514
|
+
import { execSync as execSync2 } from "child_process";
|
|
515
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, rmSync } from "fs";
|
|
516
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
517
|
+
var EXEC_ENV2 = {
|
|
518
|
+
...process.env,
|
|
519
|
+
PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
|
|
520
|
+
};
|
|
521
|
+
function exec2(cmd, cwd) {
|
|
522
|
+
return execSync2(cmd, { encoding: "utf-8", env: EXEC_ENV2, cwd }).trim();
|
|
523
|
+
}
|
|
524
|
+
function execSafe2(cmd, cwd) {
|
|
525
|
+
try {
|
|
526
|
+
return exec2(cmd, cwd);
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function shellQuote2(s) {
|
|
532
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
533
|
+
}
|
|
534
|
+
function loadWorktreeConfig(cwd) {
|
|
535
|
+
try {
|
|
536
|
+
const content = readFileSync5(worktreeConfigPath(cwd), "utf-8");
|
|
537
|
+
return JSON.parse(content);
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function createWorktree(cwd, sessionId, agentId) {
|
|
543
|
+
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
544
|
+
const worktreePath = join3(worktreeBaseDir(cwd), agentId);
|
|
545
|
+
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
546
|
+
exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
|
|
547
|
+
exec2(`git -C ${shellQuote2(cwd)} worktree add ${shellQuote2(worktreePath)} ${shellQuote2(branchName)}`);
|
|
548
|
+
const symlinks = [".sisyphus", ".claude"];
|
|
549
|
+
for (const entry of symlinks) {
|
|
550
|
+
const src = join3(cwd, entry);
|
|
551
|
+
if (existsSync2(src)) {
|
|
552
|
+
execSafe2(`ln -s ${shellQuote2(src)} ${shellQuote2(join3(worktreePath, entry))}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const config = loadWorktreeConfig(cwd);
|
|
556
|
+
if (config) {
|
|
557
|
+
bootstrapWorktree(cwd, worktreePath, config);
|
|
558
|
+
}
|
|
559
|
+
return { worktreePath, branchName };
|
|
560
|
+
}
|
|
561
|
+
function bootstrapWorktree(cwd, worktreePath, config) {
|
|
562
|
+
if (config.copy) {
|
|
563
|
+
for (const entry of config.copy) {
|
|
564
|
+
const dest = join3(worktreePath, entry);
|
|
565
|
+
mkdirSync2(dirname2(dest), { recursive: true });
|
|
566
|
+
execSafe2(`cp -r ${shellQuote2(join3(cwd, entry))} ${shellQuote2(dest)}`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (config.clone) {
|
|
570
|
+
for (const entry of config.clone) {
|
|
571
|
+
const dest = join3(worktreePath, entry);
|
|
572
|
+
mkdirSync2(dirname2(dest), { recursive: true });
|
|
573
|
+
const src = shellQuote2(join3(cwd, entry));
|
|
574
|
+
const dstQ = shellQuote2(dest);
|
|
575
|
+
if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
|
|
576
|
+
execSafe2(`cp -r ${src} ${dstQ}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (config.symlink) {
|
|
581
|
+
for (const entry of config.symlink) {
|
|
582
|
+
const dest = join3(worktreePath, entry);
|
|
583
|
+
mkdirSync2(dirname2(dest), { recursive: true });
|
|
584
|
+
execSafe2(`ln -s ${shellQuote2(join3(cwd, entry))} ${shellQuote2(dest)}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (config.init) {
|
|
588
|
+
try {
|
|
589
|
+
exec2(config.init, worktreePath);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function resolveWorktreeBranch(cwd, worktreePath) {
|
|
596
|
+
const output = execSafe2(`git -C ${shellQuote2(cwd)} worktree list --porcelain`);
|
|
597
|
+
if (!output) return null;
|
|
598
|
+
const lines = output.split("\n");
|
|
599
|
+
for (let i = 0; i < lines.length; i++) {
|
|
600
|
+
if (lines[i] === `worktree ${worktreePath}`) {
|
|
601
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
602
|
+
const line = lines[j];
|
|
603
|
+
if (line === "") break;
|
|
604
|
+
if (line.startsWith("branch refs/heads/")) {
|
|
605
|
+
return line.slice("branch refs/heads/".length);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
function mergeWorktrees(cwd, agents) {
|
|
614
|
+
const pending = agents.filter(
|
|
615
|
+
(a) => a.worktreePath && a.mergeStatus === "pending"
|
|
616
|
+
);
|
|
617
|
+
const results = [];
|
|
618
|
+
for (const agent of pending) {
|
|
619
|
+
const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
|
|
620
|
+
if (!branch) {
|
|
621
|
+
results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
|
|
622
|
+
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)} --force`);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
const aheadLog = execSafe2(`git -C ${shellQuote2(cwd)} log HEAD..${shellQuote2(branch)} --oneline`);
|
|
626
|
+
if (!aheadLog) {
|
|
627
|
+
results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
|
|
628
|
+
cleanupWorktree(cwd, agent.worktreePath, branch);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
|
|
632
|
+
const mergeCmd = `git -C ${shellQuote2(cwd)} merge --no-ff ${shellQuote2(branch)} -m ${shellQuote2(mergeMsg)}`;
|
|
633
|
+
try {
|
|
634
|
+
exec2(mergeCmd);
|
|
635
|
+
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)}`);
|
|
636
|
+
execSafe2(`git -C ${shellQuote2(cwd)} branch -d ${shellQuote2(branch)}`);
|
|
637
|
+
results.push({ agentId: agent.id, name: agent.name, status: "merged" });
|
|
638
|
+
} catch (err) {
|
|
639
|
+
execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
|
|
640
|
+
const stderr = err?.stderr;
|
|
641
|
+
const conflictDetails = stderr ? (typeof stderr === "string" ? stderr : stderr.toString("utf-8")).trim() : err instanceof Error ? err.message : String(err);
|
|
642
|
+
results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return results;
|
|
646
|
+
}
|
|
647
|
+
function cleanupWorktree(cwd, worktreePath, branchName) {
|
|
648
|
+
execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(worktreePath)} --force`);
|
|
649
|
+
execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
|
|
650
|
+
const baseDir = dirname2(worktreePath);
|
|
651
|
+
try {
|
|
652
|
+
const entries = readdirSync2(baseDir);
|
|
653
|
+
if (entries.length === 0) {
|
|
654
|
+
rmSync(baseDir, { recursive: true });
|
|
655
|
+
}
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function countWorktreeAgents(agents) {
|
|
660
|
+
return agents.filter((a) => a.worktreePath && a.status === "running").length;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/daemon/agent.ts
|
|
408
664
|
var agentCounters = /* @__PURE__ */ new Map();
|
|
409
|
-
function
|
|
410
|
-
|
|
665
|
+
function resetAgentCounterFromState(sessionId, agents) {
|
|
666
|
+
let max = 0;
|
|
667
|
+
for (const a of agents) {
|
|
668
|
+
const match = a.id.match(/^agent-(\d+)$/);
|
|
669
|
+
if (match) max = Math.max(max, parseInt(match[1], 10));
|
|
670
|
+
}
|
|
671
|
+
agentCounters.set(sessionId, max);
|
|
411
672
|
}
|
|
412
673
|
function clearAgentCounter(sessionId) {
|
|
413
674
|
agentCounters.delete(sessionId);
|
|
414
675
|
}
|
|
415
|
-
function renderAgentSuffix(sessionId, instruction) {
|
|
676
|
+
function renderAgentSuffix(sessionId, instruction, worktreeContext) {
|
|
416
677
|
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
417
678
|
let template;
|
|
418
679
|
try {
|
|
419
|
-
template =
|
|
680
|
+
template = readFileSync6(templatePath, "utf-8");
|
|
420
681
|
} catch {
|
|
421
682
|
template = `# Sisyphus Agent
|
|
422
683
|
Session: {{SESSION_ID}}
|
|
423
684
|
Task: {{INSTRUCTION}}`;
|
|
424
685
|
}
|
|
425
|
-
|
|
686
|
+
let worktreeBlock = "";
|
|
687
|
+
if (worktreeContext) {
|
|
688
|
+
worktreeBlock = [
|
|
689
|
+
"## Worktree Context",
|
|
690
|
+
`You are working in worktree ${worktreeContext.offset} of ${worktreeContext.total} concurrent worktrees on branch \`${worktreeContext.branchName}\`.`,
|
|
691
|
+
`If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
|
|
692
|
+
].join("\n");
|
|
693
|
+
}
|
|
694
|
+
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
|
|
426
695
|
}
|
|
427
696
|
async function spawnAgent(opts) {
|
|
428
697
|
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
429
698
|
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
430
699
|
agentCounters.set(sessionId, count);
|
|
431
700
|
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
432
|
-
const
|
|
433
|
-
const
|
|
701
|
+
const pluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
702
|
+
const color = resolveAgentTypeColor(agentType, pluginPath, cwd) ?? getNextColor(sessionId);
|
|
703
|
+
let paneCwd = cwd;
|
|
704
|
+
let worktreePath;
|
|
705
|
+
let branchName;
|
|
706
|
+
let worktreeContext;
|
|
707
|
+
if (opts.worktree) {
|
|
708
|
+
const wt = createWorktree(cwd, sessionId, agentId);
|
|
709
|
+
worktreePath = wt.worktreePath;
|
|
710
|
+
branchName = wt.branchName;
|
|
711
|
+
paneCwd = worktreePath;
|
|
712
|
+
const session = getSession(cwd, sessionId);
|
|
713
|
+
const portOffset = countWorktreeAgents(session.agents) + 1;
|
|
714
|
+
worktreeContext = { offset: portOffset, total: portOffset, branchName };
|
|
715
|
+
}
|
|
716
|
+
const paneId = createPane(windowId, paneCwd);
|
|
434
717
|
setPaneTitle(paneId, `${name} (${agentId})`);
|
|
435
718
|
setPaneStyle(paneId, color);
|
|
436
|
-
const suffix = renderAgentSuffix(sessionId, instruction);
|
|
437
|
-
const suffixFilePath = `${
|
|
719
|
+
const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
|
|
720
|
+
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
438
721
|
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
439
722
|
const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
|
|
440
|
-
const bannerCmd =
|
|
723
|
+
const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
441
724
|
const envExports = [
|
|
442
725
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
443
|
-
`export SISYPHUS_AGENT_ID='${agentId}'
|
|
726
|
+
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
727
|
+
...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : []
|
|
444
728
|
].join(" && ");
|
|
445
|
-
const agentFlag = agentType ? ` --agent ${
|
|
446
|
-
const claudeCmd = `claude --dangerously-skip-permissions${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${
|
|
729
|
+
const agentFlag = agentType ? ` --agent ${shellQuote3(agentType)}` : "";
|
|
730
|
+
const claudeCmd = `claude --dangerously-skip-permissions --plugin-dir "${pluginPath}"${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
|
|
447
731
|
sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
|
|
448
732
|
const agent = {
|
|
449
733
|
id: agentId,
|
|
@@ -455,7 +739,8 @@ async function spawnAgent(opts) {
|
|
|
455
739
|
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
456
740
|
completedAt: null,
|
|
457
741
|
reports: [],
|
|
458
|
-
paneId
|
|
742
|
+
paneId,
|
|
743
|
+
...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
|
|
459
744
|
};
|
|
460
745
|
await addAgent(cwd, sessionId, agent);
|
|
461
746
|
return agent;
|
|
@@ -463,7 +748,7 @@ async function spawnAgent(opts) {
|
|
|
463
748
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
464
749
|
const dir = reportsDir(cwd, sessionId);
|
|
465
750
|
try {
|
|
466
|
-
const files =
|
|
751
|
+
const files = readdirSync3(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
467
752
|
return String(files.length + 1).padStart(3, "0");
|
|
468
753
|
} catch {
|
|
469
754
|
return "001";
|
|
@@ -471,7 +756,7 @@ function nextReportNumber(cwd, sessionId, agentId) {
|
|
|
471
756
|
}
|
|
472
757
|
async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
473
758
|
const dir = reportsDir(cwd, sessionId);
|
|
474
|
-
|
|
759
|
+
mkdirSync3(dir, { recursive: true });
|
|
475
760
|
const num = nextReportNumber(cwd, sessionId, agentId);
|
|
476
761
|
const filePath = reportFilePath(cwd, sessionId, agentId, num);
|
|
477
762
|
writeFileSync3(filePath, content, "utf-8");
|
|
@@ -485,7 +770,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
|
485
770
|
}
|
|
486
771
|
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
487
772
|
const dir = reportsDir(cwd, sessionId);
|
|
488
|
-
|
|
773
|
+
mkdirSync3(dir, { recursive: true });
|
|
489
774
|
const filePath = reportFilePath(cwd, sessionId, agentId, "final");
|
|
490
775
|
writeFileSync3(filePath, report, "utf-8");
|
|
491
776
|
const entry = {
|
|
@@ -500,10 +785,13 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
500
785
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
501
786
|
});
|
|
502
787
|
const session = getSession(cwd, sessionId);
|
|
503
|
-
const
|
|
788
|
+
const agentArr = session.agents;
|
|
789
|
+
const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
|
|
504
790
|
if (agent) {
|
|
505
791
|
killPane(agent.paneId);
|
|
506
792
|
}
|
|
793
|
+
const windowId = getWindowId(sessionId);
|
|
794
|
+
if (windowId) selectLayout(windowId);
|
|
507
795
|
return allAgentsDone(session);
|
|
508
796
|
}
|
|
509
797
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
@@ -519,7 +807,7 @@ function allAgentsDone(session) {
|
|
|
519
807
|
const running = session.agents.filter((a) => a.status === "running");
|
|
520
808
|
return running.length === 0 && session.agents.length > 0;
|
|
521
809
|
}
|
|
522
|
-
function
|
|
810
|
+
function shellQuote3(s) {
|
|
523
811
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
524
812
|
}
|
|
525
813
|
|
|
@@ -571,19 +859,38 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
571
859
|
console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
|
|
572
860
|
return;
|
|
573
861
|
}
|
|
862
|
+
if (session.status === "completed") {
|
|
863
|
+
const orchPaneId2 = getOrchestratorPaneId(sessionId);
|
|
864
|
+
if (orchPaneId2) {
|
|
865
|
+
const livePanes2 = listPanes(windowId);
|
|
866
|
+
const livePaneIds2 = new Set(livePanes2.map((p) => p.paneId));
|
|
867
|
+
if (!livePaneIds2.has(orchPaneId2)) {
|
|
868
|
+
cleanupSessionMaps(sessionId);
|
|
869
|
+
untrackSession(sessionId);
|
|
870
|
+
console.log(`[sisyphus] Session ${sessionId} cleaned up: orchestrator pane closed by user`);
|
|
871
|
+
}
|
|
872
|
+
} else {
|
|
873
|
+
cleanupSessionMaps(sessionId);
|
|
874
|
+
untrackSession(sessionId);
|
|
875
|
+
}
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
574
878
|
if (session.status !== "active") return;
|
|
575
879
|
const livePanes = listPanes(windowId);
|
|
576
880
|
if (livePanes.length === 0) return;
|
|
577
881
|
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
882
|
+
let paneRemoved = false;
|
|
578
883
|
for (const agent of session.agents) {
|
|
579
884
|
if (agent.status !== "running") continue;
|
|
580
885
|
if (!livePaneIds.has(agent.paneId)) {
|
|
886
|
+
paneRemoved = true;
|
|
581
887
|
const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
|
|
582
888
|
if (allDone && onAllAgentsDone) {
|
|
583
889
|
onAllAgentsDone(sessionId, cwd, windowId);
|
|
584
890
|
}
|
|
585
891
|
}
|
|
586
892
|
}
|
|
893
|
+
if (paneRemoved) selectLayout(windowId);
|
|
587
894
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
588
895
|
if (orchPaneId && !livePaneIds.has(orchPaneId)) {
|
|
589
896
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
@@ -592,12 +899,18 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
592
899
|
console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
|
|
593
900
|
}
|
|
594
901
|
}
|
|
902
|
+
session = getSession(cwd, sessionId);
|
|
903
|
+
if (session.status === "active" && session.agents.length > 0 && session.agents.every((a) => a.status !== "running") && (!orchPaneId || !livePaneIds.has(orchPaneId)) && onAllAgentsDone) {
|
|
904
|
+
console.log(`[sisyphus] Detected stuck session ${sessionId}: all agents done, no orchestrator \u2014 triggering respawn`);
|
|
905
|
+
onAllAgentsDone(sessionId, cwd, windowId);
|
|
906
|
+
}
|
|
595
907
|
}
|
|
596
908
|
|
|
597
909
|
// src/daemon/session-manager.ts
|
|
598
910
|
async function startSession(task, cwd, tmuxSession, windowId) {
|
|
599
911
|
const sessionId = uuidv4();
|
|
600
912
|
const session = createSession(sessionId, task, cwd);
|
|
913
|
+
await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
|
|
601
914
|
trackSession(sessionId, cwd, tmuxSession);
|
|
602
915
|
await spawnOrchestrator(sessionId, cwd, windowId);
|
|
603
916
|
updateTrackedWindow(sessionId, windowId);
|
|
@@ -605,17 +918,30 @@ async function startSession(task, cwd, tmuxSession, windowId) {
|
|
|
605
918
|
}
|
|
606
919
|
async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
|
|
607
920
|
const session = getSession(cwd, sessionId);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
921
|
+
if (session.status !== "active") {
|
|
922
|
+
const livePaneIds = /* @__PURE__ */ new Set();
|
|
923
|
+
if (session.tmuxWindowId) {
|
|
924
|
+
const panes = listPanes(session.tmuxWindowId);
|
|
925
|
+
for (const pane of panes) {
|
|
926
|
+
livePaneIds.add(pane.paneId);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
for (const agent of session.agents) {
|
|
930
|
+
if (agent.status === "running") {
|
|
931
|
+
const isAlive = agent.paneId != null && livePaneIds.has(agent.paneId);
|
|
932
|
+
if (!isAlive) {
|
|
933
|
+
await updateAgent(cwd, sessionId, agent.id, {
|
|
934
|
+
status: "lost",
|
|
935
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
936
|
+
killedReason: "session resumed \u2014 agent was still running"
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
615
940
|
}
|
|
616
941
|
}
|
|
617
942
|
await updateSessionStatus(cwd, sessionId, "active");
|
|
618
|
-
|
|
943
|
+
await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
|
|
944
|
+
resetAgentCounterFromState(sessionId, session.agents);
|
|
619
945
|
resetColors(sessionId);
|
|
620
946
|
trackSession(sessionId, cwd, tmuxSession);
|
|
621
947
|
await spawnOrchestrator(sessionId, cwd, windowId, message);
|
|
@@ -627,8 +953,8 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
627
953
|
}
|
|
628
954
|
function listSessions(cwd) {
|
|
629
955
|
const dir = sessionsDir(cwd);
|
|
630
|
-
if (!
|
|
631
|
-
const entries =
|
|
956
|
+
if (!existsSync4(dir)) return [];
|
|
957
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
632
958
|
const sessions = [];
|
|
633
959
|
for (const entry of entries) {
|
|
634
960
|
if (!entry.isDirectory()) continue;
|
|
@@ -650,11 +976,22 @@ function listSessions(cwd) {
|
|
|
650
976
|
function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
651
977
|
const session = getSession(cwd, sessionId);
|
|
652
978
|
if (session.status !== "active") return;
|
|
979
|
+
const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
|
|
980
|
+
if (worktreeAgents.length > 0) {
|
|
981
|
+
const results = mergeWorktrees(cwd, worktreeAgents);
|
|
982
|
+
for (const result of results) {
|
|
983
|
+
const mergeStatus = result.status === "conflict" ? "conflict" : "merged";
|
|
984
|
+
updateAgent(cwd, sessionId, result.agentId, {
|
|
985
|
+
mergeStatus,
|
|
986
|
+
mergeDetails: result.conflictDetails
|
|
987
|
+
}).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
653
990
|
setTimeout(() => {
|
|
654
991
|
spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
|
|
655
992
|
}, 2e3);
|
|
656
993
|
}
|
|
657
|
-
async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
|
|
994
|
+
async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
|
|
658
995
|
const windowId = getWindowId(sessionId);
|
|
659
996
|
if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
|
|
660
997
|
const agent = await spawnAgent({
|
|
@@ -663,7 +1000,8 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
|
|
|
663
1000
|
agentType,
|
|
664
1001
|
name,
|
|
665
1002
|
instruction,
|
|
666
|
-
windowId
|
|
1003
|
+
windowId,
|
|
1004
|
+
worktree
|
|
667
1005
|
});
|
|
668
1006
|
await appendAgentToLastCycle(cwd, sessionId, agent.id);
|
|
669
1007
|
return { agentId: agent.id };
|
|
@@ -681,29 +1019,8 @@ async function handleYield(sessionId, cwd, nextPrompt) {
|
|
|
681
1019
|
await handleOrchestratorYield(sessionId, cwd, nextPrompt);
|
|
682
1020
|
}
|
|
683
1021
|
async function handleComplete(sessionId, cwd, report) {
|
|
684
|
-
untrackSession(sessionId);
|
|
685
1022
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
686
1023
|
}
|
|
687
|
-
async function handleTaskAdd(cwd, sessionId, description, initialStatus) {
|
|
688
|
-
const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
|
|
689
|
-
const status = initialStatus !== void 0 && VALID_STATUSES.has(initialStatus) ? initialStatus : void 0;
|
|
690
|
-
const task = await addTask(cwd, sessionId, description, status);
|
|
691
|
-
return { taskId: task.id };
|
|
692
|
-
}
|
|
693
|
-
async function handleTaskUpdate(cwd, sessionId, taskId, status, description) {
|
|
694
|
-
const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
|
|
695
|
-
const updates = {};
|
|
696
|
-
if (status !== void 0) {
|
|
697
|
-
if (!VALID_STATUSES.has(status)) throw new Error(`Invalid status: ${status}. Valid: draft, pending, in_progress, done`);
|
|
698
|
-
updates.status = status;
|
|
699
|
-
}
|
|
700
|
-
if (description !== void 0) updates.description = description;
|
|
701
|
-
await updateTask(cwd, sessionId, taskId, updates);
|
|
702
|
-
}
|
|
703
|
-
function handleTasksList(cwd, sessionId) {
|
|
704
|
-
const session = getSession(cwd, sessionId);
|
|
705
|
-
return { tasks: session.tasks };
|
|
706
|
-
}
|
|
707
1024
|
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
708
1025
|
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
709
1026
|
}
|
|
@@ -721,6 +1038,11 @@ async function handleKill(sessionId, cwd) {
|
|
|
721
1038
|
killedAgents++;
|
|
722
1039
|
}
|
|
723
1040
|
}
|
|
1041
|
+
for (const agent of session.agents) {
|
|
1042
|
+
if (agent.worktreePath && agent.branchName) {
|
|
1043
|
+
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
724
1046
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
725
1047
|
if (orchPaneId) {
|
|
726
1048
|
killPane(orchPaneId);
|
|
@@ -740,11 +1062,11 @@ var sessionCwdMap = /* @__PURE__ */ new Map();
|
|
|
740
1062
|
var sessionTmuxMap = /* @__PURE__ */ new Map();
|
|
741
1063
|
var sessionWindowMap2 = /* @__PURE__ */ new Map();
|
|
742
1064
|
function registryPath() {
|
|
743
|
-
return
|
|
1065
|
+
return join4(globalDir(), "session-registry.json");
|
|
744
1066
|
}
|
|
745
1067
|
function persistSessionRegistry() {
|
|
746
1068
|
const dir = globalDir();
|
|
747
|
-
|
|
1069
|
+
mkdirSync4(dir, { recursive: true });
|
|
748
1070
|
const registry = {};
|
|
749
1071
|
for (const [id, cwd] of sessionCwdMap) {
|
|
750
1072
|
registry[id] = cwd;
|
|
@@ -753,9 +1075,9 @@ function persistSessionRegistry() {
|
|
|
753
1075
|
}
|
|
754
1076
|
function loadSessionRegistry() {
|
|
755
1077
|
const p = registryPath();
|
|
756
|
-
if (!
|
|
1078
|
+
if (!existsSync5(p)) return {};
|
|
757
1079
|
try {
|
|
758
|
-
return JSON.parse(
|
|
1080
|
+
return JSON.parse(readFileSync7(p, "utf-8"));
|
|
759
1081
|
} catch {
|
|
760
1082
|
return {};
|
|
761
1083
|
}
|
|
@@ -764,6 +1086,10 @@ function registerSessionCwd(sessionId, cwd) {
|
|
|
764
1086
|
sessionCwdMap.set(sessionId, cwd);
|
|
765
1087
|
persistSessionRegistry();
|
|
766
1088
|
}
|
|
1089
|
+
function registerSessionTmux(sessionId, tmuxSession, windowId) {
|
|
1090
|
+
sessionTmuxMap.set(sessionId, tmuxSession);
|
|
1091
|
+
sessionWindowMap2.set(sessionId, windowId);
|
|
1092
|
+
}
|
|
767
1093
|
async function handleRequest(req) {
|
|
768
1094
|
try {
|
|
769
1095
|
switch (req.type) {
|
|
@@ -777,7 +1103,7 @@ async function handleRequest(req) {
|
|
|
777
1103
|
case "spawn": {
|
|
778
1104
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
779
1105
|
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
780
|
-
const result = await handleSpawn(req.sessionId, cwd, req.agentType, req.name, req.instruction);
|
|
1106
|
+
const result = await handleSpawn(req.sessionId, cwd, req.agentType, req.name, req.instruction, req.worktree);
|
|
781
1107
|
return { ok: true, data: { agentId: result.agentId } };
|
|
782
1108
|
}
|
|
783
1109
|
case "submit": {
|
|
@@ -815,32 +1141,29 @@ async function handleRequest(req) {
|
|
|
815
1141
|
}
|
|
816
1142
|
return { ok: true, data: { message: "daemon running" } };
|
|
817
1143
|
}
|
|
818
|
-
case "tasks_add": {
|
|
819
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
820
|
-
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
821
|
-
const result = await handleTaskAdd(cwd, req.sessionId, req.description, req.status);
|
|
822
|
-
return { ok: true, data: { taskId: result.taskId } };
|
|
823
|
-
}
|
|
824
|
-
case "tasks_update": {
|
|
825
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
826
|
-
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
827
|
-
await handleTaskUpdate(cwd, req.sessionId, req.taskId, req.status, req.description);
|
|
828
|
-
return { ok: true };
|
|
829
|
-
}
|
|
830
|
-
case "tasks_list": {
|
|
831
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
832
|
-
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
833
|
-
const result = handleTasksList(cwd, req.sessionId);
|
|
834
|
-
return { ok: true, data: { tasks: result.tasks } };
|
|
835
|
-
}
|
|
836
1144
|
case "list": {
|
|
837
1145
|
const allSessions = [];
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1146
|
+
if (req.all) {
|
|
1147
|
+
const seenCwds = /* @__PURE__ */ new Set();
|
|
1148
|
+
for (const cwd of sessionCwdMap.values()) {
|
|
1149
|
+
if (seenCwds.has(cwd)) continue;
|
|
1150
|
+
seenCwds.add(cwd);
|
|
1151
|
+
const sessions = listSessions(cwd);
|
|
1152
|
+
allSessions.push(...sessions.map((s) => ({ ...s, cwd })));
|
|
1153
|
+
}
|
|
1154
|
+
} else {
|
|
1155
|
+
const sessions = listSessions(req.cwd);
|
|
1156
|
+
allSessions.push(...sessions.map((s) => ({ ...s, cwd: req.cwd })));
|
|
1157
|
+
let totalCount = allSessions.length;
|
|
1158
|
+
const seenCwds = /* @__PURE__ */ new Set([req.cwd]);
|
|
1159
|
+
for (const cwd of sessionCwdMap.values()) {
|
|
1160
|
+
if (seenCwds.has(cwd)) continue;
|
|
1161
|
+
seenCwds.add(cwd);
|
|
1162
|
+
totalCount += listSessions(cwd).length;
|
|
1163
|
+
}
|
|
1164
|
+
if (totalCount > allSessions.length) {
|
|
1165
|
+
return { ok: true, data: { sessions: allSessions, totalCount, filtered: true } };
|
|
1166
|
+
}
|
|
844
1167
|
}
|
|
845
1168
|
return { ok: true, data: { sessions: allSessions } };
|
|
846
1169
|
}
|
|
@@ -848,7 +1171,7 @@ async function handleRequest(req) {
|
|
|
848
1171
|
let cwd = sessionCwdMap.get(req.sessionId);
|
|
849
1172
|
if (!cwd) {
|
|
850
1173
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
851
|
-
if (
|
|
1174
|
+
if (existsSync5(stateFile)) {
|
|
852
1175
|
cwd = req.cwd;
|
|
853
1176
|
registerSessionCwd(req.sessionId, cwd);
|
|
854
1177
|
} else {
|
|
@@ -887,7 +1210,7 @@ async function handleRequest(req) {
|
|
|
887
1210
|
function startServer() {
|
|
888
1211
|
return new Promise((resolve3, reject) => {
|
|
889
1212
|
const sock = socketPath();
|
|
890
|
-
if (
|
|
1213
|
+
if (existsSync5(sock)) {
|
|
891
1214
|
unlinkSync(sock);
|
|
892
1215
|
}
|
|
893
1216
|
server = createServer((conn) => {
|
|
@@ -929,7 +1252,7 @@ function stopServer() {
|
|
|
929
1252
|
}
|
|
930
1253
|
server.close(() => {
|
|
931
1254
|
const sock = socketPath();
|
|
932
|
-
if (
|
|
1255
|
+
if (existsSync5(sock)) {
|
|
933
1256
|
unlinkSync(sock);
|
|
934
1257
|
}
|
|
935
1258
|
server = null;
|
|
@@ -940,7 +1263,7 @@ function stopServer() {
|
|
|
940
1263
|
|
|
941
1264
|
// src/daemon/index.ts
|
|
942
1265
|
function ensureDirs() {
|
|
943
|
-
|
|
1266
|
+
mkdirSync5(globalDir(), { recursive: true });
|
|
944
1267
|
}
|
|
945
1268
|
function isProcessAlive(pid) {
|
|
946
1269
|
try {
|
|
@@ -950,17 +1273,22 @@ function isProcessAlive(pid) {
|
|
|
950
1273
|
return false;
|
|
951
1274
|
}
|
|
952
1275
|
}
|
|
953
|
-
function
|
|
1276
|
+
function readPid() {
|
|
954
1277
|
const pidFile = daemonPidPath();
|
|
955
1278
|
try {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
console.error(`[sisyphus] Daemon already running (pid ${existing}). Kill it first or remove ${pidFile}`);
|
|
959
|
-
process.exit(1);
|
|
960
|
-
}
|
|
1279
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
1280
|
+
return pid && isProcessAlive(pid) ? pid : null;
|
|
961
1281
|
} catch {
|
|
1282
|
+
return null;
|
|
962
1283
|
}
|
|
963
|
-
|
|
1284
|
+
}
|
|
1285
|
+
function acquirePidLock() {
|
|
1286
|
+
const pid = readPid();
|
|
1287
|
+
if (pid) {
|
|
1288
|
+
console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
|
|
1289
|
+
process.exit(0);
|
|
1290
|
+
}
|
|
1291
|
+
writeFileSync5(daemonPidPath(), String(process.pid), "utf-8");
|
|
964
1292
|
}
|
|
965
1293
|
function releasePidLock() {
|
|
966
1294
|
try {
|
|
@@ -968,7 +1296,40 @@ function releasePidLock() {
|
|
|
968
1296
|
} catch {
|
|
969
1297
|
}
|
|
970
1298
|
}
|
|
971
|
-
function
|
|
1299
|
+
function stopDaemon() {
|
|
1300
|
+
const pid = readPid();
|
|
1301
|
+
if (!pid) {
|
|
1302
|
+
console.log("[sisyphus] Daemon is not running");
|
|
1303
|
+
releasePidLock();
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
console.log(`[sisyphus] Stopping daemon (pid ${pid})...`);
|
|
1307
|
+
try {
|
|
1308
|
+
process.kill(pid, "SIGTERM");
|
|
1309
|
+
} catch {
|
|
1310
|
+
console.error(`[sisyphus] Failed to send SIGTERM to pid ${pid}`);
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
const deadline = Date.now() + 5e3;
|
|
1314
|
+
while (Date.now() < deadline) {
|
|
1315
|
+
if (!isProcessAlive(pid)) {
|
|
1316
|
+
console.log("[sisyphus] Daemon stopped");
|
|
1317
|
+
releasePidLock();
|
|
1318
|
+
return true;
|
|
1319
|
+
}
|
|
1320
|
+
const wait = Date.now() + 100;
|
|
1321
|
+
while (Date.now() < wait) {
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
console.error(`[sisyphus] Daemon (pid ${pid}) did not exit within 5s, sending SIGKILL`);
|
|
1325
|
+
try {
|
|
1326
|
+
process.kill(pid, "SIGKILL");
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
releasePidLock();
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
async function recoverSessions() {
|
|
972
1333
|
const registry = loadSessionRegistry();
|
|
973
1334
|
const entries = Object.entries(registry);
|
|
974
1335
|
if (entries.length === 0) {
|
|
@@ -978,13 +1339,45 @@ function recoverSessions() {
|
|
|
978
1339
|
let recovered = 0;
|
|
979
1340
|
for (const [sessionId, cwd] of entries) {
|
|
980
1341
|
const stateFile = statePath(cwd, sessionId);
|
|
981
|
-
if (!
|
|
1342
|
+
if (!existsSync6(stateFile)) {
|
|
982
1343
|
continue;
|
|
983
1344
|
}
|
|
984
1345
|
try {
|
|
985
|
-
const session = JSON.parse(
|
|
1346
|
+
const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
|
|
986
1347
|
if (session.status === "active" || session.status === "paused") {
|
|
987
1348
|
registerSessionCwd(sessionId, cwd);
|
|
1349
|
+
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
1350
|
+
if (session.tmuxSessionName && session.tmuxWindowId) {
|
|
1351
|
+
const livePanes = listPanes(session.tmuxWindowId);
|
|
1352
|
+
if (livePanes.length > 0) {
|
|
1353
|
+
registerSessionTmux(sessionId, session.tmuxSessionName, session.tmuxWindowId);
|
|
1354
|
+
setWindowId(sessionId, session.tmuxWindowId);
|
|
1355
|
+
trackSession(sessionId, cwd, session.tmuxSessionName);
|
|
1356
|
+
updateTrackedWindow(sessionId, session.tmuxWindowId);
|
|
1357
|
+
const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
|
|
1358
|
+
if (lastIncompleteCycle?.paneId) {
|
|
1359
|
+
setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
|
|
1360
|
+
}
|
|
1361
|
+
console.log(`[sisyphus] Reconnected session ${sessionId} to tmux window ${session.tmuxWindowId}`);
|
|
1362
|
+
if (session.status === "active" && session.agents.length > 0) {
|
|
1363
|
+
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1364
|
+
if (!hasRunningAgents) {
|
|
1365
|
+
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1366
|
+
const orchestratorPaneId = getOrchestratorPaneId(sessionId);
|
|
1367
|
+
const orchestratorAlive = orchestratorPaneId && livePaneIds.has(orchestratorPaneId);
|
|
1368
|
+
if (!orchestratorAlive) {
|
|
1369
|
+
console.log(`[sisyphus] Detected stuck session ${sessionId} on recovery: triggering orchestrator respawn`);
|
|
1370
|
+
await onAllAgentsDone2(sessionId, cwd, session.tmuxWindowId);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
} else {
|
|
1375
|
+
if (session.status === "active") {
|
|
1376
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
1377
|
+
console.log(`[sisyphus] Session ${sessionId} paused: tmux window no longer exists`);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
988
1381
|
recovered++;
|
|
989
1382
|
}
|
|
990
1383
|
} catch {
|
|
@@ -993,7 +1386,7 @@ function recoverSessions() {
|
|
|
993
1386
|
}
|
|
994
1387
|
console.log(`[sisyphus] Recovered ${recovered} session(s) from registry`);
|
|
995
1388
|
}
|
|
996
|
-
async function
|
|
1389
|
+
async function startDaemon() {
|
|
997
1390
|
console.log("[sisyphus] Starting daemon...");
|
|
998
1391
|
ensureDirs();
|
|
999
1392
|
acquirePidLock();
|
|
@@ -1001,7 +1394,7 @@ async function main() {
|
|
|
1001
1394
|
setRespawnCallback(onAllAgentsDone2);
|
|
1002
1395
|
await startServer();
|
|
1003
1396
|
startMonitor(config.pollIntervalMs);
|
|
1004
|
-
recoverSessions();
|
|
1397
|
+
await recoverSessions();
|
|
1005
1398
|
const shutdown = async () => {
|
|
1006
1399
|
console.log("[sisyphus] Shutting down...");
|
|
1007
1400
|
stopMonitor();
|
|
@@ -1012,8 +1405,32 @@ async function main() {
|
|
|
1012
1405
|
process.on("SIGTERM", shutdown);
|
|
1013
1406
|
process.on("SIGINT", shutdown);
|
|
1014
1407
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1408
|
+
var command = process.argv[2];
|
|
1409
|
+
switch (command) {
|
|
1410
|
+
case "stop":
|
|
1411
|
+
stopDaemon();
|
|
1412
|
+
break;
|
|
1413
|
+
case "restart": {
|
|
1414
|
+
stopDaemon();
|
|
1415
|
+
const wait = Date.now() + 500;
|
|
1416
|
+
while (Date.now() < wait) {
|
|
1417
|
+
}
|
|
1418
|
+
startDaemon().catch((err) => {
|
|
1419
|
+
console.error("[sisyphus] Fatal error:", err);
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
});
|
|
1422
|
+
break;
|
|
1423
|
+
}
|
|
1424
|
+
case "start":
|
|
1425
|
+
case void 0:
|
|
1426
|
+
startDaemon().catch((err) => {
|
|
1427
|
+
console.error("[sisyphus] Fatal error:", err);
|
|
1428
|
+
process.exit(1);
|
|
1429
|
+
});
|
|
1430
|
+
break;
|
|
1431
|
+
default:
|
|
1432
|
+
console.error(`[sisyphus] Unknown command: ${command}`);
|
|
1433
|
+
console.error("Usage: sisyphusd [start|stop|restart]");
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1019
1436
|
//# sourceMappingURL=daemon.js.map
|