sisyphi 0.1.1 → 0.1.3
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 +9 -0
- package/dist/{chunk-T6Z5F4SP.js → chunk-N2BPQOO2.js} +27 -3
- package/dist/chunk-N2BPQOO2.js.map +1 -0
- package/dist/cli.js +241 -161
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +608 -187
- 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 +25 -0
- 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 +25 -0
- 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-T6Z5F4SP.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,17 +452,21 @@ ${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)})`);
|
|
362
466
|
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
363
|
-
|
|
467
|
+
const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
|
|
468
|
+
const bannerCmd = existsSync(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
469
|
+
sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
|
|
364
470
|
await addOrchestratorCycle(cwd, sessionId, {
|
|
365
471
|
cycle: cycleNum,
|
|
366
472
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -381,6 +487,8 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
381
487
|
killPane(paneId);
|
|
382
488
|
sessionOrchestratorPane.delete(sessionId);
|
|
383
489
|
}
|
|
490
|
+
const windowId = sessionWindowMap.get(sessionId);
|
|
491
|
+
if (windowId) selectLayout(windowId);
|
|
384
492
|
await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
|
|
385
493
|
const session = getSession(cwd, sessionId);
|
|
386
494
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
@@ -389,58 +497,238 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
|
|
|
389
497
|
}
|
|
390
498
|
}
|
|
391
499
|
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
392
|
-
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
393
500
|
await completeOrchestratorCycle(cwd, sessionId);
|
|
394
501
|
await completeSession(cwd, sessionId, report);
|
|
395
|
-
if (paneId) {
|
|
396
|
-
killPane(paneId);
|
|
397
|
-
sessionOrchestratorPane.delete(sessionId);
|
|
398
|
-
}
|
|
399
|
-
sessionWindowMap.delete(sessionId);
|
|
400
502
|
console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
|
|
401
503
|
}
|
|
504
|
+
function cleanupSessionMaps(sessionId) {
|
|
505
|
+
sessionOrchestratorPane.delete(sessionId);
|
|
506
|
+
sessionWindowMap.delete(sessionId);
|
|
507
|
+
}
|
|
402
508
|
|
|
403
509
|
// src/daemon/agent.ts
|
|
404
|
-
import { readFileSync as
|
|
510
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
|
|
405
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
|
|
406
664
|
var agentCounters = /* @__PURE__ */ new Map();
|
|
407
|
-
function
|
|
408
|
-
|
|
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);
|
|
409
672
|
}
|
|
410
673
|
function clearAgentCounter(sessionId) {
|
|
411
674
|
agentCounters.delete(sessionId);
|
|
412
675
|
}
|
|
413
|
-
function renderAgentSuffix(sessionId, instruction) {
|
|
676
|
+
function renderAgentSuffix(sessionId, instruction, worktreeContext) {
|
|
414
677
|
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
415
678
|
let template;
|
|
416
679
|
try {
|
|
417
|
-
template =
|
|
680
|
+
template = readFileSync6(templatePath, "utf-8");
|
|
418
681
|
} catch {
|
|
419
682
|
template = `# Sisyphus Agent
|
|
420
683
|
Session: {{SESSION_ID}}
|
|
421
684
|
Task: {{INSTRUCTION}}`;
|
|
422
685
|
}
|
|
423
|
-
|
|
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);
|
|
424
695
|
}
|
|
425
696
|
async function spawnAgent(opts) {
|
|
426
697
|
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
427
698
|
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
428
699
|
agentCounters.set(sessionId, count);
|
|
429
700
|
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
430
|
-
const
|
|
431
|
-
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);
|
|
432
717
|
setPaneTitle(paneId, `${name} (${agentId})`);
|
|
433
718
|
setPaneStyle(paneId, color);
|
|
434
|
-
const suffix = renderAgentSuffix(sessionId, instruction);
|
|
435
|
-
const suffixFilePath = `${
|
|
719
|
+
const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
|
|
720
|
+
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
436
721
|
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
722
|
+
const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
|
|
723
|
+
const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
|
|
437
724
|
const envExports = [
|
|
438
725
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
439
|
-
`export SISYPHUS_AGENT_ID='${agentId}'
|
|
726
|
+
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
727
|
+
...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : []
|
|
440
728
|
].join(" && ");
|
|
441
|
-
const agentFlag = agentType ? ` --agent ${
|
|
442
|
-
const claudeCmd = `claude --dangerously-skip-permissions${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${
|
|
443
|
-
sendKeys(paneId, `${envExports} && ${claudeCmd}`);
|
|
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)}`;
|
|
731
|
+
sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
|
|
444
732
|
const agent = {
|
|
445
733
|
id: agentId,
|
|
446
734
|
name,
|
|
@@ -451,7 +739,8 @@ async function spawnAgent(opts) {
|
|
|
451
739
|
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
452
740
|
completedAt: null,
|
|
453
741
|
reports: [],
|
|
454
|
-
paneId
|
|
742
|
+
paneId,
|
|
743
|
+
...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
|
|
455
744
|
};
|
|
456
745
|
await addAgent(cwd, sessionId, agent);
|
|
457
746
|
return agent;
|
|
@@ -459,7 +748,7 @@ async function spawnAgent(opts) {
|
|
|
459
748
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
460
749
|
const dir = reportsDir(cwd, sessionId);
|
|
461
750
|
try {
|
|
462
|
-
const files =
|
|
751
|
+
const files = readdirSync3(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
463
752
|
return String(files.length + 1).padStart(3, "0");
|
|
464
753
|
} catch {
|
|
465
754
|
return "001";
|
|
@@ -467,7 +756,7 @@ function nextReportNumber(cwd, sessionId, agentId) {
|
|
|
467
756
|
}
|
|
468
757
|
async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
469
758
|
const dir = reportsDir(cwd, sessionId);
|
|
470
|
-
|
|
759
|
+
mkdirSync3(dir, { recursive: true });
|
|
471
760
|
const num = nextReportNumber(cwd, sessionId, agentId);
|
|
472
761
|
const filePath = reportFilePath(cwd, sessionId, agentId, num);
|
|
473
762
|
writeFileSync3(filePath, content, "utf-8");
|
|
@@ -481,7 +770,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
|
481
770
|
}
|
|
482
771
|
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
483
772
|
const dir = reportsDir(cwd, sessionId);
|
|
484
|
-
|
|
773
|
+
mkdirSync3(dir, { recursive: true });
|
|
485
774
|
const filePath = reportFilePath(cwd, sessionId, agentId, "final");
|
|
486
775
|
writeFileSync3(filePath, report, "utf-8");
|
|
487
776
|
const entry = {
|
|
@@ -496,10 +785,13 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
496
785
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
497
786
|
});
|
|
498
787
|
const session = getSession(cwd, sessionId);
|
|
499
|
-
const
|
|
788
|
+
const agentArr = session.agents;
|
|
789
|
+
const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
|
|
500
790
|
if (agent) {
|
|
501
791
|
killPane(agent.paneId);
|
|
502
792
|
}
|
|
793
|
+
const windowId = getWindowId(sessionId);
|
|
794
|
+
if (windowId) selectLayout(windowId);
|
|
503
795
|
return allAgentsDone(session);
|
|
504
796
|
}
|
|
505
797
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
@@ -515,7 +807,7 @@ function allAgentsDone(session) {
|
|
|
515
807
|
const running = session.agents.filter((a) => a.status === "running");
|
|
516
808
|
return running.length === 0 && session.agents.length > 0;
|
|
517
809
|
}
|
|
518
|
-
function
|
|
810
|
+
function shellQuote3(s) {
|
|
519
811
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
520
812
|
}
|
|
521
813
|
|
|
@@ -567,19 +859,38 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
567
859
|
console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
|
|
568
860
|
return;
|
|
569
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
|
+
}
|
|
570
878
|
if (session.status !== "active") return;
|
|
571
879
|
const livePanes = listPanes(windowId);
|
|
572
880
|
if (livePanes.length === 0) return;
|
|
573
881
|
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
882
|
+
let paneRemoved = false;
|
|
574
883
|
for (const agent of session.agents) {
|
|
575
884
|
if (agent.status !== "running") continue;
|
|
576
885
|
if (!livePaneIds.has(agent.paneId)) {
|
|
886
|
+
paneRemoved = true;
|
|
577
887
|
const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
|
|
578
888
|
if (allDone && onAllAgentsDone) {
|
|
579
889
|
onAllAgentsDone(sessionId, cwd, windowId);
|
|
580
890
|
}
|
|
581
891
|
}
|
|
582
892
|
}
|
|
893
|
+
if (paneRemoved) selectLayout(windowId);
|
|
583
894
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
584
895
|
if (orchPaneId && !livePaneIds.has(orchPaneId)) {
|
|
585
896
|
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
@@ -588,12 +899,18 @@ async function pollSession(sessionId, cwd, windowId) {
|
|
|
588
899
|
console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
|
|
589
900
|
}
|
|
590
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
|
+
}
|
|
591
907
|
}
|
|
592
908
|
|
|
593
909
|
// src/daemon/session-manager.ts
|
|
594
910
|
async function startSession(task, cwd, tmuxSession, windowId) {
|
|
595
911
|
const sessionId = uuidv4();
|
|
596
912
|
const session = createSession(sessionId, task, cwd);
|
|
913
|
+
await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
|
|
597
914
|
trackSession(sessionId, cwd, tmuxSession);
|
|
598
915
|
await spawnOrchestrator(sessionId, cwd, windowId);
|
|
599
916
|
updateTrackedWindow(sessionId, windowId);
|
|
@@ -601,17 +918,30 @@ async function startSession(task, cwd, tmuxSession, windowId) {
|
|
|
601
918
|
}
|
|
602
919
|
async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
|
|
603
920
|
const session = getSession(cwd, sessionId);
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
}
|
|
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
|
+
}
|
|
611
940
|
}
|
|
612
941
|
}
|
|
613
942
|
await updateSessionStatus(cwd, sessionId, "active");
|
|
614
|
-
|
|
943
|
+
await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
|
|
944
|
+
resetAgentCounterFromState(sessionId, session.agents);
|
|
615
945
|
resetColors(sessionId);
|
|
616
946
|
trackSession(sessionId, cwd, tmuxSession);
|
|
617
947
|
await spawnOrchestrator(sessionId, cwd, windowId, message);
|
|
@@ -623,8 +953,8 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
623
953
|
}
|
|
624
954
|
function listSessions(cwd) {
|
|
625
955
|
const dir = sessionsDir(cwd);
|
|
626
|
-
if (!
|
|
627
|
-
const entries =
|
|
956
|
+
if (!existsSync4(dir)) return [];
|
|
957
|
+
const entries = readdirSync4(dir, { withFileTypes: true });
|
|
628
958
|
const sessions = [];
|
|
629
959
|
for (const entry of entries) {
|
|
630
960
|
if (!entry.isDirectory()) continue;
|
|
@@ -646,11 +976,22 @@ function listSessions(cwd) {
|
|
|
646
976
|
function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
647
977
|
const session = getSession(cwd, sessionId);
|
|
648
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
|
+
}
|
|
649
990
|
setTimeout(() => {
|
|
650
991
|
spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
|
|
651
992
|
}, 2e3);
|
|
652
993
|
}
|
|
653
|
-
async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
|
|
994
|
+
async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
|
|
654
995
|
const windowId = getWindowId(sessionId);
|
|
655
996
|
if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
|
|
656
997
|
const agent = await spawnAgent({
|
|
@@ -659,7 +1000,8 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
|
|
|
659
1000
|
agentType,
|
|
660
1001
|
name,
|
|
661
1002
|
instruction,
|
|
662
|
-
windowId
|
|
1003
|
+
windowId,
|
|
1004
|
+
worktree
|
|
663
1005
|
});
|
|
664
1006
|
await appendAgentToLastCycle(cwd, sessionId, agent.id);
|
|
665
1007
|
return { agentId: agent.id };
|
|
@@ -677,29 +1019,8 @@ async function handleYield(sessionId, cwd, nextPrompt) {
|
|
|
677
1019
|
await handleOrchestratorYield(sessionId, cwd, nextPrompt);
|
|
678
1020
|
}
|
|
679
1021
|
async function handleComplete(sessionId, cwd, report) {
|
|
680
|
-
untrackSession(sessionId);
|
|
681
1022
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
682
1023
|
}
|
|
683
|
-
async function handleTaskAdd(cwd, sessionId, description, initialStatus) {
|
|
684
|
-
const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
|
|
685
|
-
const status = initialStatus !== void 0 && VALID_STATUSES.has(initialStatus) ? initialStatus : void 0;
|
|
686
|
-
const task = await addTask(cwd, sessionId, description, status);
|
|
687
|
-
return { taskId: task.id };
|
|
688
|
-
}
|
|
689
|
-
async function handleTaskUpdate(cwd, sessionId, taskId, status, description) {
|
|
690
|
-
const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
|
|
691
|
-
const updates = {};
|
|
692
|
-
if (status !== void 0) {
|
|
693
|
-
if (!VALID_STATUSES.has(status)) throw new Error(`Invalid status: ${status}. Valid: draft, pending, in_progress, done`);
|
|
694
|
-
updates.status = status;
|
|
695
|
-
}
|
|
696
|
-
if (description !== void 0) updates.description = description;
|
|
697
|
-
await updateTask(cwd, sessionId, taskId, updates);
|
|
698
|
-
}
|
|
699
|
-
function handleTasksList(cwd, sessionId) {
|
|
700
|
-
const session = getSession(cwd, sessionId);
|
|
701
|
-
return { tasks: session.tasks };
|
|
702
|
-
}
|
|
703
1024
|
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
704
1025
|
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
705
1026
|
}
|
|
@@ -717,6 +1038,11 @@ async function handleKill(sessionId, cwd) {
|
|
|
717
1038
|
killedAgents++;
|
|
718
1039
|
}
|
|
719
1040
|
}
|
|
1041
|
+
for (const agent of session.agents) {
|
|
1042
|
+
if (agent.worktreePath && agent.branchName) {
|
|
1043
|
+
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
720
1046
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
721
1047
|
if (orchPaneId) {
|
|
722
1048
|
killPane(orchPaneId);
|
|
@@ -736,11 +1062,11 @@ var sessionCwdMap = /* @__PURE__ */ new Map();
|
|
|
736
1062
|
var sessionTmuxMap = /* @__PURE__ */ new Map();
|
|
737
1063
|
var sessionWindowMap2 = /* @__PURE__ */ new Map();
|
|
738
1064
|
function registryPath() {
|
|
739
|
-
return
|
|
1065
|
+
return join4(globalDir(), "session-registry.json");
|
|
740
1066
|
}
|
|
741
1067
|
function persistSessionRegistry() {
|
|
742
1068
|
const dir = globalDir();
|
|
743
|
-
|
|
1069
|
+
mkdirSync4(dir, { recursive: true });
|
|
744
1070
|
const registry = {};
|
|
745
1071
|
for (const [id, cwd] of sessionCwdMap) {
|
|
746
1072
|
registry[id] = cwd;
|
|
@@ -749,9 +1075,9 @@ function persistSessionRegistry() {
|
|
|
749
1075
|
}
|
|
750
1076
|
function loadSessionRegistry() {
|
|
751
1077
|
const p = registryPath();
|
|
752
|
-
if (!
|
|
1078
|
+
if (!existsSync5(p)) return {};
|
|
753
1079
|
try {
|
|
754
|
-
return JSON.parse(
|
|
1080
|
+
return JSON.parse(readFileSync7(p, "utf-8"));
|
|
755
1081
|
} catch {
|
|
756
1082
|
return {};
|
|
757
1083
|
}
|
|
@@ -760,6 +1086,10 @@ function registerSessionCwd(sessionId, cwd) {
|
|
|
760
1086
|
sessionCwdMap.set(sessionId, cwd);
|
|
761
1087
|
persistSessionRegistry();
|
|
762
1088
|
}
|
|
1089
|
+
function registerSessionTmux(sessionId, tmuxSession, windowId) {
|
|
1090
|
+
sessionTmuxMap.set(sessionId, tmuxSession);
|
|
1091
|
+
sessionWindowMap2.set(sessionId, windowId);
|
|
1092
|
+
}
|
|
763
1093
|
async function handleRequest(req) {
|
|
764
1094
|
try {
|
|
765
1095
|
switch (req.type) {
|
|
@@ -773,7 +1103,7 @@ async function handleRequest(req) {
|
|
|
773
1103
|
case "spawn": {
|
|
774
1104
|
const cwd = sessionCwdMap.get(req.sessionId);
|
|
775
1105
|
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
776
|
-
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);
|
|
777
1107
|
return { ok: true, data: { agentId: result.agentId } };
|
|
778
1108
|
}
|
|
779
1109
|
case "submit": {
|
|
@@ -811,32 +1141,29 @@ async function handleRequest(req) {
|
|
|
811
1141
|
}
|
|
812
1142
|
return { ok: true, data: { message: "daemon running" } };
|
|
813
1143
|
}
|
|
814
|
-
case "tasks_add": {
|
|
815
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
816
|
-
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
817
|
-
const result = await handleTaskAdd(cwd, req.sessionId, req.description, req.status);
|
|
818
|
-
return { ok: true, data: { taskId: result.taskId } };
|
|
819
|
-
}
|
|
820
|
-
case "tasks_update": {
|
|
821
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
822
|
-
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
823
|
-
await handleTaskUpdate(cwd, req.sessionId, req.taskId, req.status, req.description);
|
|
824
|
-
return { ok: true };
|
|
825
|
-
}
|
|
826
|
-
case "tasks_list": {
|
|
827
|
-
const cwd = sessionCwdMap.get(req.sessionId);
|
|
828
|
-
if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
|
|
829
|
-
const result = handleTasksList(cwd, req.sessionId);
|
|
830
|
-
return { ok: true, data: { tasks: result.tasks } };
|
|
831
|
-
}
|
|
832
1144
|
case "list": {
|
|
833
1145
|
const allSessions = [];
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
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
|
+
}
|
|
840
1167
|
}
|
|
841
1168
|
return { ok: true, data: { sessions: allSessions } };
|
|
842
1169
|
}
|
|
@@ -844,7 +1171,7 @@ async function handleRequest(req) {
|
|
|
844
1171
|
let cwd = sessionCwdMap.get(req.sessionId);
|
|
845
1172
|
if (!cwd) {
|
|
846
1173
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
847
|
-
if (
|
|
1174
|
+
if (existsSync5(stateFile)) {
|
|
848
1175
|
cwd = req.cwd;
|
|
849
1176
|
registerSessionCwd(req.sessionId, cwd);
|
|
850
1177
|
} else {
|
|
@@ -883,7 +1210,7 @@ async function handleRequest(req) {
|
|
|
883
1210
|
function startServer() {
|
|
884
1211
|
return new Promise((resolve3, reject) => {
|
|
885
1212
|
const sock = socketPath();
|
|
886
|
-
if (
|
|
1213
|
+
if (existsSync5(sock)) {
|
|
887
1214
|
unlinkSync(sock);
|
|
888
1215
|
}
|
|
889
1216
|
server = createServer((conn) => {
|
|
@@ -925,7 +1252,7 @@ function stopServer() {
|
|
|
925
1252
|
}
|
|
926
1253
|
server.close(() => {
|
|
927
1254
|
const sock = socketPath();
|
|
928
|
-
if (
|
|
1255
|
+
if (existsSync5(sock)) {
|
|
929
1256
|
unlinkSync(sock);
|
|
930
1257
|
}
|
|
931
1258
|
server = null;
|
|
@@ -936,7 +1263,7 @@ function stopServer() {
|
|
|
936
1263
|
|
|
937
1264
|
// src/daemon/index.ts
|
|
938
1265
|
function ensureDirs() {
|
|
939
|
-
|
|
1266
|
+
mkdirSync5(globalDir(), { recursive: true });
|
|
940
1267
|
}
|
|
941
1268
|
function isProcessAlive(pid) {
|
|
942
1269
|
try {
|
|
@@ -946,17 +1273,22 @@ function isProcessAlive(pid) {
|
|
|
946
1273
|
return false;
|
|
947
1274
|
}
|
|
948
1275
|
}
|
|
949
|
-
function
|
|
1276
|
+
function readPid() {
|
|
950
1277
|
const pidFile = daemonPidPath();
|
|
951
1278
|
try {
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
console.error(`[sisyphus] Daemon already running (pid ${existing}). Kill it first or remove ${pidFile}`);
|
|
955
|
-
process.exit(1);
|
|
956
|
-
}
|
|
1279
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
1280
|
+
return pid && isProcessAlive(pid) ? pid : null;
|
|
957
1281
|
} catch {
|
|
1282
|
+
return null;
|
|
958
1283
|
}
|
|
959
|
-
|
|
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");
|
|
960
1292
|
}
|
|
961
1293
|
function releasePidLock() {
|
|
962
1294
|
try {
|
|
@@ -964,7 +1296,40 @@ function releasePidLock() {
|
|
|
964
1296
|
} catch {
|
|
965
1297
|
}
|
|
966
1298
|
}
|
|
967
|
-
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() {
|
|
968
1333
|
const registry = loadSessionRegistry();
|
|
969
1334
|
const entries = Object.entries(registry);
|
|
970
1335
|
if (entries.length === 0) {
|
|
@@ -974,13 +1339,45 @@ function recoverSessions() {
|
|
|
974
1339
|
let recovered = 0;
|
|
975
1340
|
for (const [sessionId, cwd] of entries) {
|
|
976
1341
|
const stateFile = statePath(cwd, sessionId);
|
|
977
|
-
if (!
|
|
1342
|
+
if (!existsSync6(stateFile)) {
|
|
978
1343
|
continue;
|
|
979
1344
|
}
|
|
980
1345
|
try {
|
|
981
|
-
const session = JSON.parse(
|
|
1346
|
+
const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
|
|
982
1347
|
if (session.status === "active" || session.status === "paused") {
|
|
983
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
|
+
}
|
|
984
1381
|
recovered++;
|
|
985
1382
|
}
|
|
986
1383
|
} catch {
|
|
@@ -989,7 +1386,7 @@ function recoverSessions() {
|
|
|
989
1386
|
}
|
|
990
1387
|
console.log(`[sisyphus] Recovered ${recovered} session(s) from registry`);
|
|
991
1388
|
}
|
|
992
|
-
async function
|
|
1389
|
+
async function startDaemon() {
|
|
993
1390
|
console.log("[sisyphus] Starting daemon...");
|
|
994
1391
|
ensureDirs();
|
|
995
1392
|
acquirePidLock();
|
|
@@ -997,7 +1394,7 @@ async function main() {
|
|
|
997
1394
|
setRespawnCallback(onAllAgentsDone2);
|
|
998
1395
|
await startServer();
|
|
999
1396
|
startMonitor(config.pollIntervalMs);
|
|
1000
|
-
recoverSessions();
|
|
1397
|
+
await recoverSessions();
|
|
1001
1398
|
const shutdown = async () => {
|
|
1002
1399
|
console.log("[sisyphus] Shutting down...");
|
|
1003
1400
|
stopMonitor();
|
|
@@ -1008,8 +1405,32 @@ async function main() {
|
|
|
1008
1405
|
process.on("SIGTERM", shutdown);
|
|
1009
1406
|
process.on("SIGINT", shutdown);
|
|
1010
1407
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
+
}
|
|
1015
1436
|
//# sourceMappingURL=daemon.js.map
|