sisyphi 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-T7ETTIQK.js → chunk-M7LZ2ZHD.js} +3 -27
- package/dist/chunk-M7LZ2ZHD.js.map +1 -0
- package/dist/{chunk-JXKUI4P6.js → chunk-REUQ4B45.js} +7 -38
- package/dist/chunk-REUQ4B45.js.map +1 -0
- package/dist/{chunk-LWWRGQWM.js → chunk-Z32YVDMY.js} +2 -2
- package/dist/chunk-Z32YVDMY.js.map +1 -0
- package/dist/cli.js +75 -56
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +776 -629
- package/dist/daemon.js.map +1 -1
- package/dist/{paths-NUUALUVP.js → paths-IJXOAN4E.js} +4 -6
- package/dist/templates/CLAUDE.md +16 -14
- package/dist/templates/agent-plugin/agents/CLAUDE.md +17 -6
- package/dist/templates/agent-plugin/agents/design.md +134 -0
- package/dist/templates/agent-plugin/agents/explore.md +39 -0
- package/dist/templates/agent-plugin/agents/operator.md +24 -0
- package/dist/templates/agent-plugin/agents/plan.md +15 -20
- package/dist/templates/agent-plugin/agents/problem.md +119 -0
- package/dist/templates/agent-plugin/agents/requirements.md +138 -0
- package/dist/templates/agent-plugin/agents/review/CLAUDE.md +29 -0
- package/dist/templates/agent-plugin/agents/review/compliance.md +6 -6
- package/dist/templates/agent-plugin/agents/review-plan/code-smells.md +4 -4
- package/dist/templates/agent-plugin/agents/review-plan/requirements-coverage.md +62 -0
- package/dist/templates/agent-plugin/agents/review-plan/security.md +1 -1
- package/dist/templates/agent-plugin/agents/review-plan.md +9 -8
- package/dist/templates/agent-plugin/agents/review.md +1 -1
- package/dist/templates/agent-plugin/agents/test-spec.md +3 -3
- package/dist/templates/agent-plugin/hooks/CLAUDE.md +2 -2
- package/dist/templates/agent-plugin/hooks/explore-user-prompt.sh +13 -0
- package/dist/templates/agent-plugin/hooks/plan-user-prompt.sh +1 -1
- package/dist/templates/agent-plugin/hooks/require-submit.sh +70 -3
- package/dist/templates/agent-plugin/hooks/review-plan-user-prompt.sh +4 -4
- package/dist/templates/agent-plugin/hooks/review-user-prompt.sh +1 -1
- package/dist/templates/agent-suffix.md +0 -2
- package/dist/templates/orchestrator-base.md +169 -145
- package/dist/templates/orchestrator-impl.md +92 -57
- package/dist/templates/orchestrator-planning.md +46 -56
- package/dist/templates/orchestrator-plugin/commands/sisyphus/design.md +13 -0
- package/dist/templates/orchestrator-plugin/commands/sisyphus/problem.md +13 -0
- package/dist/templates/orchestrator-plugin/commands/sisyphus/requirements.md +13 -0
- package/dist/templates/orchestrator-plugin/commands/sisyphus/strategize.md +19 -0
- package/dist/templates/orchestrator-plugin/hooks/explore-gate.sh +15 -0
- package/dist/templates/orchestrator-plugin/hooks/hooks.json +14 -1
- package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +34 -27
- package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +56 -24
- package/dist/templates/orchestrator-strategy.md +233 -0
- package/dist/templates/orchestrator-validation.md +94 -0
- package/dist/tui.js +2730 -2924
- package/dist/tui.js.map +1 -1
- package/package.json +2 -4
- package/templates/CLAUDE.md +16 -14
- package/templates/agent-plugin/agents/CLAUDE.md +17 -6
- package/templates/agent-plugin/agents/design.md +134 -0
- package/templates/agent-plugin/agents/explore.md +39 -0
- package/templates/agent-plugin/agents/operator.md +24 -0
- package/templates/agent-plugin/agents/plan.md +15 -20
- package/templates/agent-plugin/agents/problem.md +119 -0
- package/templates/agent-plugin/agents/requirements.md +138 -0
- package/templates/agent-plugin/agents/review/CLAUDE.md +29 -0
- package/templates/agent-plugin/agents/review/compliance.md +6 -6
- package/templates/agent-plugin/agents/review-plan/code-smells.md +4 -4
- package/templates/agent-plugin/agents/review-plan/requirements-coverage.md +62 -0
- package/templates/agent-plugin/agents/review-plan/security.md +1 -1
- package/templates/agent-plugin/agents/review-plan.md +9 -8
- package/templates/agent-plugin/agents/review.md +1 -1
- package/templates/agent-plugin/agents/test-spec.md +3 -3
- package/templates/agent-plugin/hooks/CLAUDE.md +2 -2
- package/templates/agent-plugin/hooks/explore-user-prompt.sh +13 -0
- package/templates/agent-plugin/hooks/plan-user-prompt.sh +1 -1
- package/templates/agent-plugin/hooks/require-submit.sh +70 -3
- package/templates/agent-plugin/hooks/review-plan-user-prompt.sh +4 -4
- package/templates/agent-plugin/hooks/review-user-prompt.sh +1 -1
- package/templates/agent-suffix.md +0 -2
- package/templates/orchestrator-base.md +169 -145
- package/templates/orchestrator-impl.md +92 -57
- package/templates/orchestrator-planning.md +46 -56
- package/templates/orchestrator-plugin/commands/sisyphus/design.md +13 -0
- package/templates/orchestrator-plugin/commands/sisyphus/problem.md +13 -0
- package/templates/orchestrator-plugin/commands/sisyphus/requirements.md +13 -0
- package/templates/orchestrator-plugin/commands/sisyphus/strategize.md +19 -0
- package/templates/orchestrator-plugin/hooks/explore-gate.sh +15 -0
- package/templates/orchestrator-plugin/hooks/hooks.json +14 -1
- package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +34 -27
- package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +56 -24
- package/templates/orchestrator-strategy.md +233 -0
- package/templates/orchestrator-validation.md +94 -0
- package/dist/chunk-JXKUI4P6.js.map +0 -1
- package/dist/chunk-LWWRGQWM.js.map +0 -1
- package/dist/chunk-T7ETTIQK.js.map +0 -1
- package/dist/templates/agent-plugin/agents/review-plan/spec-coverage.md +0 -44
- package/dist/templates/agent-plugin/agents/spec-draft.md +0 -78
- package/dist/templates/agent-plugin/hooks/hooks.json +0 -25
- package/dist/templates/agent-plugin/hooks/spec-user-prompt.sh +0 -19
- package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +0 -111
- package/templates/agent-plugin/agents/review-plan/spec-coverage.md +0 -44
- package/templates/agent-plugin/agents/spec-draft.md +0 -78
- package/templates/agent-plugin/hooks/hooks.json +0 -25
- package/templates/agent-plugin/hooks/spec-user-prompt.sh +0 -19
- package/templates/orchestrator-plugin/skills/git-management/SKILL.md +0 -111
- /package/dist/{paths-NUUALUVP.js.map → paths-IJXOAN4E.js.map} +0 -0
package/dist/daemon.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
execEnv,
|
|
6
6
|
execSafe,
|
|
7
7
|
loadConfig
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-Z32YVDMY.js";
|
|
9
9
|
import {
|
|
10
10
|
shellQuote
|
|
11
11
|
} from "./chunk-6G226ZK7.js";
|
|
@@ -30,23 +30,22 @@ import {
|
|
|
30
30
|
snapshotsDir,
|
|
31
31
|
socketPath,
|
|
32
32
|
statePath,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "./chunk-JXKUI4P6.js";
|
|
33
|
+
strategyPath
|
|
34
|
+
} from "./chunk-REUQ4B45.js";
|
|
36
35
|
|
|
37
36
|
// src/daemon/index.ts
|
|
38
|
-
import { mkdirSync as
|
|
37
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
|
|
39
38
|
import { execSync as execSync4 } from "child_process";
|
|
40
39
|
import { setTimeout as sleep } from "timers/promises";
|
|
41
40
|
|
|
42
41
|
// src/daemon/server.ts
|
|
43
42
|
import { createServer } from "net";
|
|
44
|
-
import { unlinkSync, existsSync as
|
|
45
|
-
import { join as
|
|
43
|
+
import { unlinkSync, existsSync as existsSync7, writeFileSync as writeFileSync5, readFileSync as readFileSync5, mkdirSync as mkdirSync3, rmSync as rmSync3 } from "fs";
|
|
44
|
+
import { join as join5 } from "path";
|
|
46
45
|
|
|
47
46
|
// src/daemon/session-manager.ts
|
|
48
47
|
import { v4 as uuidv4 } from "uuid";
|
|
49
|
-
import { existsSync as
|
|
48
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, rmSync as rmSync2 } from "fs";
|
|
50
49
|
|
|
51
50
|
// src/daemon/state.ts
|
|
52
51
|
import { randomUUID } from "crypto";
|
|
@@ -91,6 +90,9 @@ function createSession(id, task, cwd, context, name) {
|
|
|
91
90
|
mkdirSync(logsDir(cwd, id), { recursive: true });
|
|
92
91
|
writeFileSync(goalPath(cwd, id), task, "utf-8");
|
|
93
92
|
writeFileSync(join(contextDir(cwd, id), "CLAUDE.md"), CONTEXT_CLAUDE_MD, "utf-8");
|
|
93
|
+
if (context) {
|
|
94
|
+
writeFileSync(join(contextDir(cwd, id), "initial-context.md"), context, "utf-8");
|
|
95
|
+
}
|
|
94
96
|
const session = {
|
|
95
97
|
id,
|
|
96
98
|
...name ? { name } : {},
|
|
@@ -99,6 +101,7 @@ function createSession(id, task, cwd, context, name) {
|
|
|
99
101
|
cwd,
|
|
100
102
|
status: "active",
|
|
101
103
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
104
|
+
activeMs: 0,
|
|
102
105
|
agents: [],
|
|
103
106
|
orchestratorCycles: [],
|
|
104
107
|
messages: []
|
|
@@ -108,7 +111,16 @@ function createSession(id, task, cwd, context, name) {
|
|
|
108
111
|
}
|
|
109
112
|
function getSession(cwd, sessionId) {
|
|
110
113
|
const content = readFileSync(statePath(cwd, sessionId), "utf-8");
|
|
111
|
-
|
|
114
|
+
const session = JSON.parse(content);
|
|
115
|
+
if (session.activeMs == null) session.activeMs = 0;
|
|
116
|
+
for (const agent of session.agents) {
|
|
117
|
+
if (!agent.repo) agent.repo = ".";
|
|
118
|
+
if (agent.activeMs == null) agent.activeMs = 0;
|
|
119
|
+
}
|
|
120
|
+
for (const cycle of session.orchestratorCycles) {
|
|
121
|
+
if (cycle.activeMs == null) cycle.activeMs = 0;
|
|
122
|
+
}
|
|
123
|
+
return session;
|
|
112
124
|
}
|
|
113
125
|
function saveSession(session) {
|
|
114
126
|
atomicWrite(statePath(session.cwd, session.id), JSON.stringify(session, null, 2));
|
|
@@ -202,6 +214,13 @@ async function updateReportSummary(cwd, sessionId, agentId, filePath, summary) {
|
|
|
202
214
|
}
|
|
203
215
|
});
|
|
204
216
|
}
|
|
217
|
+
async function updateSessionName(cwd, sessionId, name) {
|
|
218
|
+
return withSessionLock(sessionId, () => {
|
|
219
|
+
const session = getSession(cwd, sessionId);
|
|
220
|
+
session.name = name;
|
|
221
|
+
saveSession(session);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
205
224
|
async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId) {
|
|
206
225
|
return withSessionLock(sessionId, () => {
|
|
207
226
|
const session = getSession(cwd, sessionId);
|
|
@@ -234,15 +253,32 @@ async function updateTask(cwd, sessionId, task) {
|
|
|
234
253
|
writeFileSync(goalPath(cwd, sessionId), task, "utf-8");
|
|
235
254
|
});
|
|
236
255
|
}
|
|
237
|
-
async function completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode) {
|
|
256
|
+
async function completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode, activeMs) {
|
|
238
257
|
return withSessionLock(sessionId, () => {
|
|
239
258
|
const session = getSession(cwd, sessionId);
|
|
240
259
|
const cycles = session.orchestratorCycles;
|
|
241
260
|
if (cycles.length === 0) return;
|
|
242
261
|
const cycle = cycles[cycles.length - 1];
|
|
262
|
+
if (cycle.completedAt) return;
|
|
243
263
|
cycle.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
244
264
|
if (nextPrompt) cycle.nextPrompt = nextPrompt;
|
|
245
265
|
if (mode) cycle.mode = mode;
|
|
266
|
+
if (activeMs != null) cycle.activeMs += activeMs;
|
|
267
|
+
saveSession(session);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
async function incrementActiveTime(cwd, sessionId, sessionDelta, agentDeltas, cycleDeltas) {
|
|
271
|
+
return withSessionLock(sessionId, () => {
|
|
272
|
+
const session = getSession(cwd, sessionId);
|
|
273
|
+
session.activeMs += sessionDelta;
|
|
274
|
+
for (const [agentId, delta] of agentDeltas) {
|
|
275
|
+
const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
|
|
276
|
+
if (agent) agent.activeMs += delta;
|
|
277
|
+
}
|
|
278
|
+
for (const [cycleNum, delta] of cycleDeltas) {
|
|
279
|
+
const cycle = session.orchestratorCycles.find((c) => c.cycle === cycleNum);
|
|
280
|
+
if (cycle) cycle.activeMs += delta;
|
|
281
|
+
}
|
|
246
282
|
saveSession(session);
|
|
247
283
|
});
|
|
248
284
|
}
|
|
@@ -300,9 +336,10 @@ function deleteSnapshotsAfter(cwd, sessionId, afterCycle) {
|
|
|
300
336
|
}
|
|
301
337
|
|
|
302
338
|
// src/daemon/orchestrator.ts
|
|
303
|
-
import { existsSync as
|
|
304
|
-
import { execSync } from "child_process";
|
|
305
|
-
import {
|
|
339
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
340
|
+
import { execSync as execSync2 } from "child_process";
|
|
341
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
342
|
+
import { resolve as resolve3, join as join4 } from "path";
|
|
306
343
|
|
|
307
344
|
// src/daemon/spawn-helpers.ts
|
|
308
345
|
import { writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
@@ -375,6 +412,8 @@ function parseAgentFrontmatter(content) {
|
|
|
375
412
|
fm.description = str("description");
|
|
376
413
|
fm.permissionMode = str("permissionMode");
|
|
377
414
|
fm.effort = str("effort");
|
|
415
|
+
const interactive = str("interactive");
|
|
416
|
+
if (interactive === "true") fm.interactive = true;
|
|
378
417
|
const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
379
418
|
if (skillsMatch) {
|
|
380
419
|
fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
|
|
@@ -521,6 +560,9 @@ function sessionExists(sessionName) {
|
|
|
521
560
|
function killSession(sessionName) {
|
|
522
561
|
execSafe(`tmux kill-session -t "${sessionName}"`);
|
|
523
562
|
}
|
|
563
|
+
function renameSession(oldName, newName) {
|
|
564
|
+
exec(`tmux rename-session -t "${oldName}" "${newName}"`);
|
|
565
|
+
}
|
|
524
566
|
function setSessionOption(sessionName, option, value) {
|
|
525
567
|
execSafe(`tmux set-option -t "${sessionName}" ${option} ${shellQuote(value)}`);
|
|
526
568
|
}
|
|
@@ -554,15 +596,30 @@ function listPanes(windowTarget) {
|
|
|
554
596
|
function setPaneTitle(paneTarget, title) {
|
|
555
597
|
execSafe(`tmux select-pane -t "${paneTarget}" -T ${shellQuote(title)}`);
|
|
556
598
|
}
|
|
557
|
-
function setPaneStyle(paneTarget, color) {
|
|
599
|
+
function setPaneStyle(paneTarget, color, meta) {
|
|
558
600
|
const gitBranch = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null)`;
|
|
559
601
|
const branchSuffix = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null | grep -q . && echo ' |') ${gitBranch}`;
|
|
560
|
-
const
|
|
602
|
+
const homePath = `#(echo '#{pane_current_path}' | sed "s|^$HOME|~|")`;
|
|
603
|
+
execSafe(`tmux set -p -t "${paneTarget}" @pane_role ${shellQuote(meta.role)}`);
|
|
604
|
+
execSafe(`tmux set -p -t "${paneTarget}" @pane_session ${shellQuote(meta.session)}`);
|
|
605
|
+
execSafe(`tmux set -p -t "${paneTarget}" @pane_cycle ${shellQuote(meta.cycle)}`);
|
|
606
|
+
const fmt = [
|
|
607
|
+
`#[bg=${color},fg=black,bold] #{@pane_role} #[default]`,
|
|
608
|
+
` #[fg=${color},bold]#{@pane_session}`,
|
|
609
|
+
` #[default,dim]#{@pane_cycle}`,
|
|
610
|
+
` ${homePath}${branchSuffix}`,
|
|
611
|
+
`#[default]`
|
|
612
|
+
].join("");
|
|
561
613
|
execSafe(`tmux set -p -t "${paneTarget}" pane-border-format ${shellQuote(fmt)}`);
|
|
562
614
|
execSafe(`tmux set -p -t "${paneTarget}" @pane_color "${color}"`);
|
|
563
615
|
execSafe(`tmux set -w -t "${paneTarget}" pane-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
|
|
564
616
|
execSafe(`tmux set -w -t "${paneTarget}" pane-active-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
|
|
565
617
|
}
|
|
618
|
+
function updatePaneMeta(paneTarget, updates) {
|
|
619
|
+
if (updates.role !== void 0) execSafe(`tmux set -p -t "${paneTarget}" @pane_role ${shellQuote(updates.role)}`);
|
|
620
|
+
if (updates.session !== void 0) execSafe(`tmux set -p -t "${paneTarget}" @pane_session ${shellQuote(updates.session)}`);
|
|
621
|
+
if (updates.cycle !== void 0) execSafe(`tmux set -p -t "${paneTarget}" @pane_cycle ${shellQuote(updates.cycle)}`);
|
|
622
|
+
}
|
|
566
623
|
function selectLayout(windowTarget, layout = "even-horizontal") {
|
|
567
624
|
execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
|
|
568
625
|
}
|
|
@@ -600,401 +657,61 @@ function unregisterSessionPanes(sessionId) {
|
|
|
600
657
|
function lookupPane(paneId) {
|
|
601
658
|
return paneMap.get(paneId);
|
|
602
659
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
return sessionWindowMap.get(sessionId);
|
|
609
|
-
}
|
|
610
|
-
function setWindowId(sessionId, windowId) {
|
|
611
|
-
sessionWindowMap.set(sessionId, windowId);
|
|
612
|
-
}
|
|
613
|
-
function getOrchestratorPaneId(sessionId) {
|
|
614
|
-
return sessionOrchestratorPane.get(sessionId);
|
|
615
|
-
}
|
|
616
|
-
function setOrchestratorPaneId(sessionId, paneId) {
|
|
617
|
-
sessionOrchestratorPane.set(sessionId, paneId);
|
|
618
|
-
}
|
|
619
|
-
function loadOrchestratorPrompt(cwd, mode) {
|
|
620
|
-
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
621
|
-
if (existsSync4(projectPath)) {
|
|
622
|
-
return readFileSync3(projectPath, "utf-8");
|
|
623
|
-
}
|
|
624
|
-
const basePath = resolve2(import.meta.dirname, "../templates/orchestrator-base.md");
|
|
625
|
-
const base = readFileSync3(basePath, "utf-8");
|
|
626
|
-
if (mode === "implementation") {
|
|
627
|
-
const implPath = resolve2(import.meta.dirname, "../templates/orchestrator-impl.md");
|
|
628
|
-
return base + "\n\n" + readFileSync3(implPath, "utf-8");
|
|
629
|
-
}
|
|
630
|
-
const planningPath = resolve2(import.meta.dirname, "../templates/orchestrator-planning.md");
|
|
631
|
-
return base + "\n\n" + readFileSync3(planningPath, "utf-8");
|
|
632
|
-
}
|
|
633
|
-
function formatStateForOrchestrator(session) {
|
|
634
|
-
const cycleNum = session.orchestratorCycles.length;
|
|
635
|
-
const ctxDir = contextDir(session.cwd, session.id);
|
|
636
|
-
const roadmapFile = roadmapPath(session.cwd, session.id);
|
|
637
|
-
const logFile = cycleLogPath(session.cwd, session.id, cycleNum + 1);
|
|
638
|
-
let contextSection = "";
|
|
639
|
-
if (cycleNum === 0) {
|
|
640
|
-
if (session.context) {
|
|
641
|
-
contextSection = `
|
|
642
|
-
## Context
|
|
643
|
-
|
|
644
|
-
${session.context}
|
|
645
|
-
`;
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
let ctxFiles = [];
|
|
649
|
-
if (existsSync4(ctxDir)) {
|
|
650
|
-
ctxFiles = readdirSync3(ctxDir).filter((f) => f !== "CLAUDE.md");
|
|
651
|
-
}
|
|
652
|
-
if (ctxFiles.length > 0) {
|
|
653
|
-
const ctxLines = ctxFiles.map((f) => `- ${join3(ctxDir, f)}`).join("\n");
|
|
654
|
-
contextSection = `
|
|
655
|
-
## Context
|
|
656
|
-
|
|
657
|
-
${ctxLines}
|
|
658
|
-
`;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
const messages = session.messages ?? [];
|
|
662
|
-
const messagesSection = messages.length > 0 ? "\n### Messages\n\n" + messages.map((m) => {
|
|
663
|
-
const sourceLabel = m.source.type === "agent" ? `agent:${m.source.agentId}` : m.source.type === "system" && m.source.detail ? `system:${m.source.detail}` : m.source.type;
|
|
664
|
-
const fileRef = m.filePath ? ` \u2192 ${m.filePath}` : "";
|
|
665
|
-
return `- [${sourceLabel} @ ${m.timestamp}] "${m.summary}"${fileRef}`;
|
|
666
|
-
}).join("\n") + "\n" : "";
|
|
667
|
-
let previousCyclesSection = "";
|
|
668
|
-
if (session.orchestratorCycles.length > 1) {
|
|
669
|
-
const previousCycles = session.orchestratorCycles.slice(0, -1);
|
|
670
|
-
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
671
|
-
const lines = previousCycles.map((c) => {
|
|
672
|
-
const agentDescs = c.agentsSpawned.map((id) => {
|
|
673
|
-
const agent = agentMap.get(id);
|
|
674
|
-
return agent ? `${id} (${agent.name})` : id;
|
|
675
|
-
}).join(", ");
|
|
676
|
-
return `Cycle ${c.cycle}: ${agentDescs || "(none)"}`;
|
|
677
|
-
});
|
|
678
|
-
previousCyclesSection = `
|
|
679
|
-
### Previous Cycles
|
|
680
|
-
|
|
681
|
-
${lines.join("\n")}
|
|
682
|
-
`;
|
|
683
|
-
}
|
|
684
|
-
let mostRecentCycleSection = "";
|
|
685
|
-
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
686
|
-
if (lastCycle && lastCycle.agentsSpawned.length > 0) {
|
|
687
|
-
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
688
|
-
const agentLines = lastCycle.agentsSpawned.map((id) => {
|
|
689
|
-
const agent = agentMap.get(id);
|
|
690
|
-
if (!agent) return `- **${id}**: unknown (no agent data)`;
|
|
691
|
-
const finalReport = agent.reports.find((r) => r.type === "final");
|
|
692
|
-
const reportToUse = finalReport ?? agent.reports[agent.reports.length - 1];
|
|
693
|
-
const reportRef = reportToUse ? `@${reportToUse.filePath}` : "(no reports)";
|
|
694
|
-
return `- **${id}** (${agent.name}) [${agent.status}]: ${reportRef}`;
|
|
695
|
-
}).join("\n");
|
|
696
|
-
mostRecentCycleSection = `
|
|
697
|
-
### Most Recent Cycle
|
|
698
|
-
|
|
699
|
-
${agentLines}
|
|
700
|
-
`;
|
|
701
|
-
}
|
|
702
|
-
const roadmapRef = existsSync4(roadmapFile) ? `@${roadmapFile}` : "(empty)";
|
|
703
|
-
const worktreeAgents = session.agents.filter((a) => a.worktreePath);
|
|
704
|
-
let worktreeSection = "";
|
|
705
|
-
if (worktreeAgents.length > 0 || existsSync4(worktreeConfigPath(session.cwd))) {
|
|
706
|
-
let wtLines = "";
|
|
707
|
-
if (worktreeAgents.length > 0) {
|
|
708
|
-
wtLines = "\n" + worktreeAgents.map((a) => {
|
|
709
|
-
if (a.mergeStatus === "conflict") {
|
|
710
|
-
return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
|
|
711
|
-
Branch: ${a.branchName}
|
|
712
|
-
Worktree: ${a.worktreePath}`;
|
|
713
|
-
}
|
|
714
|
-
if (a.mergeStatus === "no-changes") {
|
|
715
|
-
return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
|
|
716
|
-
}
|
|
717
|
-
const status = a.mergeStatus ?? "pending";
|
|
718
|
-
return `- ${a.id}: ${status} (branch ${a.branchName})`;
|
|
719
|
-
}).join("\n");
|
|
660
|
+
function getSessionPanes(sessionId) {
|
|
661
|
+
const result = [];
|
|
662
|
+
for (const [paneId, entry] of paneMap) {
|
|
663
|
+
if (entry.sessionId === sessionId) {
|
|
664
|
+
result.push({ paneId, ...entry });
|
|
720
665
|
}
|
|
721
|
-
const worktreeHint = existsSync4(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.";
|
|
722
|
-
worktreeSection = `
|
|
723
|
-
|
|
724
|
-
## Git Worktrees
|
|
725
|
-
|
|
726
|
-
${worktreeHint}${wtLines}`;
|
|
727
666
|
}
|
|
728
|
-
|
|
729
|
-
const goalContent = existsSync4(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
|
|
730
|
-
return `## Goal
|
|
731
|
-
|
|
732
|
-
${goalContent}
|
|
733
|
-
${contextSection}${messagesSection}
|
|
734
|
-
### Cycle Log
|
|
735
|
-
|
|
736
|
-
Write your cycle summary to: ${logFile}
|
|
737
|
-
${previousCyclesSection}${mostRecentCycleSection}
|
|
738
|
-
## Roadmap
|
|
739
|
-
|
|
740
|
-
${roadmapRef}
|
|
741
|
-
${worktreeSection}`;
|
|
742
|
-
}
|
|
743
|
-
async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
744
|
-
try {
|
|
745
|
-
execSync("which claude", { stdio: "pipe", env: EXEC_ENV });
|
|
746
|
-
} catch {
|
|
747
|
-
throw new Error("Claude CLI not found on PATH. Run `sisyphus doctor` to diagnose.");
|
|
748
|
-
}
|
|
749
|
-
const session = getSession(cwd, sessionId);
|
|
750
|
-
const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
|
|
751
|
-
const mode = lastCycle?.mode ?? "planning";
|
|
752
|
-
const basePrompt = loadOrchestratorPrompt(cwd, mode);
|
|
753
|
-
const formattedState = formatStateForOrchestrator(session);
|
|
754
|
-
const agentPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
755
|
-
const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
|
|
756
|
-
agentTypes.push(
|
|
757
|
-
{ qualifiedName: "Explore", source: "bundled", model: "haiku", description: "Fast codebase exploration \u2014 find files, search code, answer questions about architecture. Use for research and context gathering." }
|
|
758
|
-
);
|
|
759
|
-
const agentTypeLines = agentTypes.length > 0 ? agentTypes.map((t) => {
|
|
760
|
-
const modelTag = t.model ? ` (${t.model})` : "";
|
|
761
|
-
const desc = t.description ? ` \u2014 ${t.description}` : "";
|
|
762
|
-
return `- \`${t.qualifiedName}\`${modelTag}${desc}`;
|
|
763
|
-
}).join("\n") : " (none)";
|
|
764
|
-
const systemPrompt = basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines);
|
|
765
|
-
const cycleNum = session.orchestratorCycles.length + 1;
|
|
766
|
-
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
767
|
-
writeFileSync3(promptFilePath, systemPrompt, "utf-8");
|
|
768
|
-
sessionWindowMap.set(sessionId, windowId);
|
|
769
|
-
const npmBinDir = resolveNpmBinDir();
|
|
770
|
-
const envExports = buildEnvExports([
|
|
771
|
-
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
772
|
-
`export SISYPHUS_AGENT_ID='orchestrator'`,
|
|
773
|
-
`export SISYPHUS_CWD='${cwd}'`,
|
|
774
|
-
`export PATH="${npmBinDir}:$PATH"`
|
|
775
|
-
]);
|
|
776
|
-
let userPrompt = formattedState;
|
|
777
|
-
if (message) {
|
|
778
|
-
userPrompt += `
|
|
779
|
-
|
|
780
|
-
## Continuation Instructions
|
|
781
|
-
|
|
782
|
-
The user resumed this session with new instructions: ${message}`;
|
|
783
|
-
} else {
|
|
784
|
-
const storedPrompt = lastCycle?.nextPrompt;
|
|
785
|
-
const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
|
|
786
|
-
userPrompt += `
|
|
787
|
-
|
|
788
|
-
## Continuation Instructions
|
|
789
|
-
|
|
790
|
-
${continuationText}`;
|
|
791
|
-
}
|
|
792
|
-
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
793
|
-
writeFileSync3(userPromptFilePath, userPrompt, "utf-8");
|
|
794
|
-
if (session.messages && session.messages.length > 0) {
|
|
795
|
-
await drainMessages(cwd, sessionId, session.messages.length);
|
|
796
|
-
}
|
|
797
|
-
const pluginPath = resolve2(import.meta.dirname, "../templates/orchestrator-plugin");
|
|
798
|
-
const settingsPath = resolve2(import.meta.dirname, "../templates/orchestrator-settings.json");
|
|
799
|
-
const config = loadConfig(cwd);
|
|
800
|
-
const effort = config.orchestratorEffort ?? "high";
|
|
801
|
-
const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --settings "${settingsPath}" --plugin-dir "${pluginPath}" --name "sisyphus:orch-${sessionId.slice(0, 8)}-cycle-${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
802
|
-
const paneId = createPane(windowId, cwd, "left");
|
|
803
|
-
sessionOrchestratorPane.set(sessionId, paneId);
|
|
804
|
-
registerPane(paneId, sessionId, "orchestrator");
|
|
805
|
-
setPaneTitle(paneId, `Sisyphus`);
|
|
806
|
-
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
807
|
-
const bannerCmd = resolveBannerCmd();
|
|
808
|
-
const notifyCmd = buildNotifyCmd(paneId);
|
|
809
|
-
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `orchestrator-run-${cycleNum}`, [
|
|
810
|
-
"#!/usr/bin/env bash",
|
|
811
|
-
...bannerCmd ? [bannerCmd] : [],
|
|
812
|
-
envExports,
|
|
813
|
-
claudeCmd,
|
|
814
|
-
notifyCmd
|
|
815
|
-
]);
|
|
816
|
-
sendKeys(paneId, `bash '${scriptPath}'`);
|
|
817
|
-
await addOrchestratorCycle(cwd, sessionId, {
|
|
818
|
-
cycle: cycleNum,
|
|
819
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
820
|
-
agentsSpawned: [],
|
|
821
|
-
paneId
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
function resolveOrchestratorPane(sessionId, cwd) {
|
|
825
|
-
const memPane = sessionOrchestratorPane.get(sessionId);
|
|
826
|
-
if (memPane) return memPane;
|
|
827
|
-
const session = getSession(cwd, sessionId);
|
|
828
|
-
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
829
|
-
return lastCycle?.paneId ?? void 0;
|
|
830
|
-
}
|
|
831
|
-
async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
|
|
832
|
-
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
833
|
-
if (paneId) {
|
|
834
|
-
killPane(paneId);
|
|
835
|
-
unregisterPane(paneId);
|
|
836
|
-
sessionOrchestratorPane.delete(sessionId);
|
|
837
|
-
}
|
|
838
|
-
const windowId = sessionWindowMap.get(sessionId);
|
|
839
|
-
if (windowId) selectLayout(windowId);
|
|
840
|
-
await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode);
|
|
841
|
-
const session = getSession(cwd, sessionId);
|
|
842
|
-
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
843
|
-
if (runningAgents.length === 0) {
|
|
844
|
-
console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
848
|
-
await completeOrchestratorCycle(cwd, sessionId);
|
|
849
|
-
await completeSession(cwd, sessionId, report);
|
|
850
|
-
console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
|
|
851
|
-
}
|
|
852
|
-
function cleanupSessionMaps(sessionId) {
|
|
853
|
-
sessionOrchestratorPane.delete(sessionId);
|
|
854
|
-
sessionWindowMap.delete(sessionId);
|
|
855
|
-
unregisterSessionPanes(sessionId);
|
|
667
|
+
return result;
|
|
856
668
|
}
|
|
857
669
|
|
|
858
670
|
// src/daemon/agent.ts
|
|
859
|
-
import { readFileSync as
|
|
860
|
-
import { execSync
|
|
861
|
-
import {
|
|
671
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
|
|
672
|
+
import { execSync } from "child_process";
|
|
673
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
674
|
+
import { resolve as resolve2, dirname as dirname2, join as join3 } from "path";
|
|
862
675
|
|
|
863
|
-
// src/daemon/
|
|
864
|
-
import {
|
|
865
|
-
|
|
866
|
-
|
|
676
|
+
// src/daemon/summarize.ts
|
|
677
|
+
import { query } from "@r-cli/sdk";
|
|
678
|
+
var COOLDOWN_MS = 5 * 60 * 1e3;
|
|
679
|
+
var disabledUntil = 0;
|
|
680
|
+
async function generateSessionName(task) {
|
|
681
|
+
if (Date.now() < disabledUntil) return null;
|
|
867
682
|
try {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
|
|
877
|
-
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
878
|
-
execSafe(`git -C ${shellQuote(cwd)} worktree prune`);
|
|
879
|
-
if (existsSync5(worktreePath)) {
|
|
880
|
-
execSafe(`git -C ${shellQuote(cwd)} worktree remove --force ${shellQuote(worktreePath)}`);
|
|
881
|
-
}
|
|
882
|
-
execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
|
|
883
|
-
exec(`git -C ${shellQuote(cwd)} branch ${shellQuote(branchName)} HEAD`);
|
|
884
|
-
exec(`git -C ${shellQuote(cwd)} worktree add ${shellQuote(worktreePath)} ${shellQuote(branchName)}`);
|
|
885
|
-
return { worktreePath, branchName };
|
|
886
|
-
}
|
|
887
|
-
function bootstrapWorktree(cwd, worktreePath, config) {
|
|
888
|
-
if (config.copy) {
|
|
889
|
-
for (const entry of config.copy) {
|
|
890
|
-
const dest = join4(worktreePath, entry);
|
|
891
|
-
mkdirSync2(dirname2(dest), { recursive: true });
|
|
892
|
-
execSafe(`cp -r ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
if (config.clone) {
|
|
896
|
-
for (const entry of config.clone) {
|
|
897
|
-
const dest = join4(worktreePath, entry);
|
|
898
|
-
mkdirSync2(dirname2(dest), { recursive: true });
|
|
899
|
-
const src = shellQuote(join4(cwd, entry));
|
|
900
|
-
const dstQ = shellQuote(dest);
|
|
901
|
-
if (execSafe(`cp -Rc ${src} ${dstQ}`) === null) {
|
|
902
|
-
execSafe(`cp -r ${src} ${dstQ}`);
|
|
683
|
+
const session = await query({
|
|
684
|
+
prompt: `Generate a 2-4 word kebab-case name for this task. Output ONLY the name.
|
|
685
|
+
|
|
686
|
+
${task.slice(0, 500)}`,
|
|
687
|
+
options: {
|
|
688
|
+
model: "haiku",
|
|
689
|
+
maxTurns: 1,
|
|
690
|
+
env: execEnv()
|
|
903
691
|
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
execSafe(`ln -s ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
if (config.init) {
|
|
914
|
-
try {
|
|
915
|
-
exec(config.init, worktreePath);
|
|
916
|
-
} catch (err) {
|
|
917
|
-
console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
function resolveWorktreeBranch(cwd, worktreePath) {
|
|
922
|
-
const output = execSafe(`git -C ${shellQuote(cwd)} worktree list --porcelain`);
|
|
923
|
-
if (!output) return null;
|
|
924
|
-
const lines = output.split("\n");
|
|
925
|
-
for (let i = 0; i < lines.length; i++) {
|
|
926
|
-
if (lines[i] === `worktree ${worktreePath}`) {
|
|
927
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
928
|
-
const line = lines[j];
|
|
929
|
-
if (line === "") break;
|
|
930
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
931
|
-
return line.slice("branch refs/heads/".length);
|
|
692
|
+
});
|
|
693
|
+
let text = "";
|
|
694
|
+
for await (const msg of session) {
|
|
695
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
696
|
+
for (const block of msg.message.content) {
|
|
697
|
+
if (block.type === "text") text += block.text;
|
|
932
698
|
}
|
|
933
699
|
}
|
|
934
|
-
break;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
return null;
|
|
938
|
-
}
|
|
939
|
-
function mergeWorktrees(cwd, agents) {
|
|
940
|
-
const pending = agents.filter(
|
|
941
|
-
(a) => a.worktreePath && a.mergeStatus === "pending"
|
|
942
|
-
);
|
|
943
|
-
const results = [];
|
|
944
|
-
execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
|
|
945
|
-
execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
|
|
946
|
-
for (const agent of pending) {
|
|
947
|
-
const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
|
|
948
|
-
if (!branch) {
|
|
949
|
-
results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
|
|
950
|
-
execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
|
|
951
|
-
continue;
|
|
952
700
|
}
|
|
953
|
-
const
|
|
954
|
-
if (!
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
try {
|
|
962
|
-
exec(mergeCmd);
|
|
963
|
-
execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)}`);
|
|
964
|
-
execSafe(`git -C ${shellQuote(cwd)} branch -d ${shellQuote(branch)}`);
|
|
965
|
-
results.push({ agentId: agent.id, name: agent.name, status: "merged" });
|
|
966
|
-
} catch (err) {
|
|
967
|
-
execSafe(`git -C ${shellQuote(cwd)} merge --abort`);
|
|
968
|
-
const errObj = err;
|
|
969
|
-
const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
|
|
970
|
-
const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
|
|
971
|
-
const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
|
|
972
|
-
results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
return results;
|
|
976
|
-
}
|
|
977
|
-
function cleanupWorktree(cwd, worktreePath, branchName) {
|
|
978
|
-
execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(worktreePath)} --force`);
|
|
979
|
-
execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
|
|
980
|
-
const baseDir = dirname2(worktreePath);
|
|
981
|
-
try {
|
|
982
|
-
const entries = readdirSync4(baseDir);
|
|
983
|
-
if (entries.length === 0) {
|
|
984
|
-
rmSync2(baseDir, { recursive: true });
|
|
701
|
+
const name = text.trim().toLowerCase();
|
|
702
|
+
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) return null;
|
|
703
|
+
return name.slice(0, 30);
|
|
704
|
+
} catch (err) {
|
|
705
|
+
console.error(`[sisyphus] Haiku name generation failed: ${err instanceof Error ? err.message : err}`);
|
|
706
|
+
const status = err.status;
|
|
707
|
+
if (status === 401 || status === 403) {
|
|
708
|
+
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
985
709
|
}
|
|
986
|
-
|
|
710
|
+
return null;
|
|
987
711
|
}
|
|
988
712
|
}
|
|
989
|
-
function countWorktreeAgents(agents) {
|
|
990
|
-
return agents.filter((a) => a.worktreePath && a.status === "running").length;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
// src/daemon/summarize.ts
|
|
994
|
-
import { query } from "@r-cli/sdk";
|
|
995
|
-
var disabled = false;
|
|
996
713
|
async function summarizeReport(reportText) {
|
|
997
|
-
if (
|
|
714
|
+
if (Date.now() < disabledUntil) return null;
|
|
998
715
|
try {
|
|
999
716
|
const session = await query({
|
|
1000
717
|
prompt: `Summarize this agent work report in one concise sentence (max 120 chars). Focus on what was accomplished and the outcome. Output ONLY the summary sentence, nothing else.
|
|
@@ -1002,7 +719,8 @@ async function summarizeReport(reportText) {
|
|
|
1002
719
|
${reportText.slice(0, 3e3)}`,
|
|
1003
720
|
options: {
|
|
1004
721
|
model: "haiku",
|
|
1005
|
-
maxTurns: 1
|
|
722
|
+
maxTurns: 1,
|
|
723
|
+
env: execEnv()
|
|
1006
724
|
}
|
|
1007
725
|
});
|
|
1008
726
|
let text = "";
|
|
@@ -1019,7 +737,7 @@ ${reportText.slice(0, 3e3)}`,
|
|
|
1019
737
|
console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
|
|
1020
738
|
const status = err.status;
|
|
1021
739
|
if (status === 401 || status === 403) {
|
|
1022
|
-
|
|
740
|
+
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
1023
741
|
}
|
|
1024
742
|
return null;
|
|
1025
743
|
}
|
|
@@ -1038,69 +756,65 @@ function resetAgentCounterFromState(sessionId, agents) {
|
|
|
1038
756
|
function clearAgentCounter(sessionId) {
|
|
1039
757
|
agentCounters.delete(sessionId);
|
|
1040
758
|
}
|
|
1041
|
-
function renderAgentSuffix(sessionId, instruction
|
|
1042
|
-
const templatePath =
|
|
759
|
+
function renderAgentSuffix(sessionId, instruction) {
|
|
760
|
+
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
1043
761
|
let template;
|
|
1044
762
|
try {
|
|
1045
|
-
template =
|
|
763
|
+
template = readFileSync3(templatePath, "utf-8");
|
|
1046
764
|
} catch {
|
|
1047
765
|
template = `# Sisyphus Agent
|
|
1048
766
|
Session: {{SESSION_ID}}
|
|
1049
767
|
Task: {{INSTRUCTION}}`;
|
|
1050
768
|
}
|
|
1051
|
-
|
|
1052
|
-
if (worktreeContext) {
|
|
1053
|
-
worktreeBlock = [
|
|
1054
|
-
"## Worktree Context",
|
|
1055
|
-
`You are working in an isolated git worktree on branch \`${worktreeContext.branchName}\`.`,
|
|
1056
|
-
`If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
|
|
1057
|
-
].join("\n");
|
|
1058
|
-
}
|
|
1059
|
-
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
|
|
769
|
+
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, "");
|
|
1060
770
|
}
|
|
1061
771
|
function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
|
|
1062
772
|
const base = `${promptsDir(cwd, sessionId)}/${agentId}-plugin`;
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
773
|
+
mkdirSync2(`${base}/.claude-plugin`, { recursive: true });
|
|
774
|
+
mkdirSync2(`${base}/agents`, { recursive: true });
|
|
775
|
+
mkdirSync2(`${base}/hooks`, { recursive: true });
|
|
776
|
+
writeFileSync3(
|
|
1067
777
|
`${base}/.claude-plugin/plugin.json`,
|
|
1068
778
|
JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
|
|
1069
779
|
"utf-8"
|
|
1070
780
|
);
|
|
781
|
+
const sesDir = sessionDir(cwd, sessionId);
|
|
782
|
+
const substituteEnvVars = (text) => text.replace(/\$SISYPHUS_SESSION_DIR/g, sesDir).replace(/\$SISYPHUS_SESSION_ID/g, sessionId);
|
|
1071
783
|
if (agentConfig?.filePath && agentType && agentType !== "worker") {
|
|
1072
784
|
const shortName = agentType.replace(/^sisyphus:/, "");
|
|
1073
|
-
|
|
1074
|
-
const subAgentDir =
|
|
1075
|
-
if (
|
|
1076
|
-
for (const f of
|
|
785
|
+
writeFileSync3(`${base}/agents/${shortName}.md`, substituteEnvVars(readFileSync3(agentConfig.filePath, "utf-8")), "utf-8");
|
|
786
|
+
const subAgentDir = join3(dirname2(agentConfig.filePath), shortName);
|
|
787
|
+
if (existsSync4(subAgentDir)) {
|
|
788
|
+
for (const f of readdirSync3(subAgentDir)) {
|
|
1077
789
|
if (f.endsWith(".md") && f !== "CLAUDE.md") {
|
|
1078
|
-
|
|
790
|
+
writeFileSync3(`${base}/agents/${f}`, substituteEnvVars(readFileSync3(join3(subAgentDir, f), "utf-8")), "utf-8");
|
|
1079
791
|
}
|
|
1080
792
|
}
|
|
1081
793
|
}
|
|
1082
794
|
}
|
|
1083
|
-
const srcHooks =
|
|
795
|
+
const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
|
|
1084
796
|
for (const f of ["require-submit.sh", "intercept-send-message.sh"]) {
|
|
1085
797
|
copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
|
|
1086
798
|
}
|
|
1087
799
|
const hooksConfig = {
|
|
1088
800
|
PreToolUse: [
|
|
1089
801
|
{ matcher: "SendMessage", hooks: [{ type: "command", command: "bash ${CLAUDE_PLUGIN_ROOT}/hooks/intercept-send-message.sh" }] }
|
|
1090
|
-
],
|
|
1091
|
-
Stop: [
|
|
1092
|
-
{ hooks: [{ type: "command", command: "bash ${CLAUDE_PLUGIN_ROOT}/hooks/require-submit.sh" }] }
|
|
1093
802
|
]
|
|
1094
803
|
};
|
|
804
|
+
if (!agentConfig?.frontmatter.interactive) {
|
|
805
|
+
hooksConfig.Stop = [
|
|
806
|
+
{ hooks: [{ type: "command", command: "bash ${CLAUDE_PLUGIN_ROOT}/hooks/require-submit.sh" }] }
|
|
807
|
+
];
|
|
808
|
+
}
|
|
1095
809
|
const normalizedType = agentType?.replace(/^sisyphus:/, "") ?? "";
|
|
1096
810
|
const userPromptHooks = {
|
|
1097
811
|
"plan": "plan-user-prompt.sh",
|
|
1098
|
-
"spec-draft": "spec-user-prompt.sh",
|
|
1099
812
|
"review": "review-user-prompt.sh",
|
|
1100
813
|
"review-plan": "review-plan-user-prompt.sh",
|
|
1101
814
|
"debug": "debug-user-prompt.sh",
|
|
1102
815
|
"operator": "operator-user-prompt.sh",
|
|
1103
|
-
"test-spec": "test-spec-user-prompt.sh"
|
|
816
|
+
"test-spec": "test-spec-user-prompt.sh",
|
|
817
|
+
"explore": "explore-user-prompt.sh"
|
|
1104
818
|
};
|
|
1105
819
|
const hookScript = userPromptHooks[normalizedType];
|
|
1106
820
|
if (hookScript) {
|
|
@@ -1109,27 +823,30 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
|
|
|
1109
823
|
];
|
|
1110
824
|
copyFileSync2(`${srcHooks}/${hookScript}`, `${base}/hooks/${hookScript}`);
|
|
1111
825
|
}
|
|
1112
|
-
|
|
826
|
+
writeFileSync3(`${base}/hooks/hooks.json`, JSON.stringify({ hooks: hooksConfig }, null, 2), "utf-8");
|
|
1113
827
|
return base;
|
|
1114
828
|
}
|
|
1115
829
|
function setupAgentPane(opts) {
|
|
1116
|
-
const { sessionId, cwd, agentId, agentType, name, instruction, windowId, color, provider, agentConfig,
|
|
830
|
+
const { sessionId, cycleNum, cwd, agentId, agentType, name, instruction, windowId, color, provider, agentConfig, paneCwd, claudeSessionId } = opts;
|
|
1117
831
|
const paneId = createPane(windowId, paneCwd);
|
|
1118
832
|
registerPane(paneId, sessionId, "agent", agentId);
|
|
1119
833
|
const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
|
|
1120
834
|
const paneLabel = shortType ? `${name}-${shortType}` : name;
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
835
|
+
const sessionLabel = opts.sessionName ?? sessionId.slice(0, 8);
|
|
836
|
+
const agentTitle = `ssph:${sessionLabel} ${paneLabel} c${cycleNum}`;
|
|
837
|
+
setPaneTitle(paneId, agentTitle);
|
|
838
|
+
setPaneStyle(paneId, color, { role: paneLabel, session: sessionLabel, cycle: `c${cycleNum}` });
|
|
839
|
+
const suffix = renderAgentSuffix(sessionId, instruction);
|
|
1124
840
|
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
1125
|
-
|
|
841
|
+
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
1126
842
|
const bannerCmd = resolveBannerCmd();
|
|
1127
843
|
const npmBinDir = resolveNpmBinDir();
|
|
844
|
+
const sesDir = sessionDir(cwd, sessionId);
|
|
1128
845
|
const envExports = buildEnvExports([
|
|
1129
846
|
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
1130
847
|
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
1131
848
|
`export SISYPHUS_CWD='${cwd}'`,
|
|
1132
|
-
|
|
849
|
+
`export SISYPHUS_SESSION_DIR='${sesDir}'`,
|
|
1133
850
|
`export PATH="${npmBinDir}:$PATH"`
|
|
1134
851
|
]);
|
|
1135
852
|
const notifyCmd = buildNotifyCmd(paneId);
|
|
@@ -1142,7 +859,7 @@ function setupAgentPane(opts) {
|
|
|
1142
859
|
parts.push(`## Task
|
|
1143
860
|
|
|
1144
861
|
${instruction}`);
|
|
1145
|
-
|
|
862
|
+
writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
|
|
1146
863
|
const model = agentConfig?.frontmatter.model ?? "codex-mini";
|
|
1147
864
|
mainCmd = `codex -m ${shellQuote(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
|
|
1148
865
|
} else {
|
|
@@ -1150,7 +867,8 @@ ${instruction}`);
|
|
|
1150
867
|
const config = loadConfig(cwd);
|
|
1151
868
|
const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
|
|
1152
869
|
const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
|
|
1153
|
-
|
|
870
|
+
const sessionIdFlag = claudeSessionId ? ` --session-id "${claudeSessionId}"` : "";
|
|
871
|
+
mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag}${sessionIdFlag} --name ${shellQuote(agentTitle)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote(instruction)}`;
|
|
1154
872
|
}
|
|
1155
873
|
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
|
|
1156
874
|
"#!/usr/bin/env bash",
|
|
@@ -1167,31 +885,24 @@ async function spawnAgent(opts) {
|
|
|
1167
885
|
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
1168
886
|
agentCounters.set(sessionId, count);
|
|
1169
887
|
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
1170
|
-
const bundledPluginPath =
|
|
888
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1171
889
|
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
1172
890
|
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
1173
891
|
const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
|
|
1174
892
|
const cliToCheck = provider === "openai" ? "codex" : "claude";
|
|
1175
893
|
try {
|
|
1176
|
-
|
|
894
|
+
execSync(`which ${cliToCheck}`, { stdio: "pipe", env: execEnv() });
|
|
1177
895
|
} catch {
|
|
1178
896
|
throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sisyphus doctor\` to diagnose.`);
|
|
1179
897
|
}
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (opts.worktree) {
|
|
1185
|
-
const wt = createWorktreeShell(cwd, sessionId, agentId);
|
|
1186
|
-
worktreePath = wt.worktreePath;
|
|
1187
|
-
branchName = wt.branchName;
|
|
1188
|
-
paneCwd = worktreePath;
|
|
1189
|
-
const session = getSession(cwd, sessionId);
|
|
1190
|
-
const portOffset = countWorktreeAgents(session.agents) + 1;
|
|
1191
|
-
worktreeContext = { offset: portOffset, total: portOffset, branchName };
|
|
1192
|
-
}
|
|
898
|
+
const repo = opts.repo !== void 0 ? opts.repo : ".";
|
|
899
|
+
const repoRoot = repo === "." ? cwd : join3(cwd, repo);
|
|
900
|
+
const paneCwd = repoRoot;
|
|
901
|
+
const claudeSessionId = provider !== "openai" ? randomUUID2() : void 0;
|
|
1193
902
|
const { paneId, fullCmd } = setupAgentPane({
|
|
1194
903
|
sessionId,
|
|
904
|
+
sessionName: opts.sessionName,
|
|
905
|
+
cycleNum: opts.cycleNum,
|
|
1195
906
|
cwd,
|
|
1196
907
|
agentId,
|
|
1197
908
|
agentType,
|
|
@@ -1201,42 +912,27 @@ async function spawnAgent(opts) {
|
|
|
1201
912
|
color,
|
|
1202
913
|
provider,
|
|
1203
914
|
agentConfig,
|
|
1204
|
-
|
|
1205
|
-
|
|
915
|
+
paneCwd,
|
|
916
|
+
claudeSessionId
|
|
1206
917
|
});
|
|
1207
918
|
const agent = {
|
|
1208
919
|
id: agentId,
|
|
1209
920
|
name,
|
|
1210
921
|
agentType,
|
|
1211
922
|
provider,
|
|
923
|
+
claudeSessionId,
|
|
1212
924
|
color,
|
|
1213
925
|
instruction,
|
|
1214
926
|
status: "running",
|
|
1215
927
|
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1216
928
|
completedAt: null,
|
|
929
|
+
activeMs: 0,
|
|
1217
930
|
reports: [],
|
|
1218
931
|
paneId,
|
|
1219
|
-
|
|
932
|
+
repo
|
|
1220
933
|
};
|
|
1221
934
|
await addAgent(cwd, sessionId, agent);
|
|
1222
|
-
|
|
1223
|
-
const config = loadWorktreeConfig(cwd);
|
|
1224
|
-
if (config) {
|
|
1225
|
-
const wtPath = worktreePath;
|
|
1226
|
-
setImmediate(() => {
|
|
1227
|
-
try {
|
|
1228
|
-
bootstrapWorktree(cwd, wtPath, config);
|
|
1229
|
-
} catch (err) {
|
|
1230
|
-
console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
|
|
1231
|
-
}
|
|
1232
|
-
sendKeys(paneId, fullCmd);
|
|
1233
|
-
});
|
|
1234
|
-
} else {
|
|
1235
|
-
sendKeys(paneId, fullCmd);
|
|
1236
|
-
}
|
|
1237
|
-
} else {
|
|
1238
|
-
sendKeys(paneId, fullCmd);
|
|
1239
|
-
}
|
|
935
|
+
sendKeys(paneId, fullCmd);
|
|
1240
936
|
return agent;
|
|
1241
937
|
}
|
|
1242
938
|
async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
@@ -1255,19 +951,12 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1255
951
|
});
|
|
1256
952
|
}
|
|
1257
953
|
const { instruction, agentType, name, color } = agent;
|
|
1258
|
-
const bundledPluginPath =
|
|
954
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1259
955
|
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
1260
956
|
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
1261
957
|
let paneCwd = cwd;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
paneCwd = agent.worktreePath;
|
|
1265
|
-
const portOffset = countWorktreeAgents(session.agents);
|
|
1266
|
-
worktreeContext = {
|
|
1267
|
-
offset: portOffset,
|
|
1268
|
-
total: portOffset,
|
|
1269
|
-
branchName: agent.branchName
|
|
1270
|
-
};
|
|
958
|
+
if (agent.repo !== ".") {
|
|
959
|
+
paneCwd = join3(cwd, agent.repo);
|
|
1271
960
|
}
|
|
1272
961
|
if (agent.paneId) {
|
|
1273
962
|
try {
|
|
@@ -1276,8 +965,11 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1276
965
|
}
|
|
1277
966
|
unregisterAgentPane(sessionId, agentId);
|
|
1278
967
|
}
|
|
968
|
+
const claudeSessionId = provider !== "openai" ? randomUUID2() : void 0;
|
|
1279
969
|
const { paneId, fullCmd } = setupAgentPane({
|
|
1280
970
|
sessionId,
|
|
971
|
+
sessionName: session.name,
|
|
972
|
+
cycleNum: session.orchestratorCycles.length,
|
|
1281
973
|
cwd,
|
|
1282
974
|
agentId,
|
|
1283
975
|
agentType,
|
|
@@ -1287,13 +979,14 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1287
979
|
color,
|
|
1288
980
|
provider,
|
|
1289
981
|
agentConfig,
|
|
1290
|
-
|
|
1291
|
-
|
|
982
|
+
paneCwd,
|
|
983
|
+
claudeSessionId
|
|
1292
984
|
});
|
|
1293
985
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1294
986
|
status: "running",
|
|
1295
987
|
paneId,
|
|
1296
988
|
provider,
|
|
989
|
+
claudeSessionId,
|
|
1297
990
|
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1298
991
|
completedAt: null,
|
|
1299
992
|
killedReason: void 0
|
|
@@ -1303,7 +996,7 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1303
996
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
1304
997
|
const dir = reportsDir(cwd, sessionId);
|
|
1305
998
|
try {
|
|
1306
|
-
const files =
|
|
999
|
+
const files = readdirSync3(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
1307
1000
|
return String(files.length + 1).padStart(3, "0");
|
|
1308
1001
|
} catch {
|
|
1309
1002
|
return "001";
|
|
@@ -1311,10 +1004,10 @@ function nextReportNumber(cwd, sessionId, agentId) {
|
|
|
1311
1004
|
}
|
|
1312
1005
|
async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
1313
1006
|
const dir = reportsDir(cwd, sessionId);
|
|
1314
|
-
|
|
1007
|
+
mkdirSync2(dir, { recursive: true });
|
|
1315
1008
|
const num = nextReportNumber(cwd, sessionId, agentId);
|
|
1316
1009
|
const filePath = reportFilePath(cwd, sessionId, agentId, num);
|
|
1317
|
-
|
|
1010
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1318
1011
|
const entry = {
|
|
1319
1012
|
type: "update",
|
|
1320
1013
|
filePath,
|
|
@@ -1331,9 +1024,9 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
|
1331
1024
|
}
|
|
1332
1025
|
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
1333
1026
|
const dir = reportsDir(cwd, sessionId);
|
|
1334
|
-
|
|
1027
|
+
mkdirSync2(dir, { recursive: true });
|
|
1335
1028
|
const filePath = reportFilePath(cwd, sessionId, agentId, "final");
|
|
1336
|
-
|
|
1029
|
+
writeFileSync3(filePath, report, "utf-8");
|
|
1337
1030
|
const entry = {
|
|
1338
1031
|
type: "final",
|
|
1339
1032
|
filePath,
|
|
@@ -1347,9 +1040,11 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
1347
1040
|
}
|
|
1348
1041
|
}).catch(() => {
|
|
1349
1042
|
});
|
|
1043
|
+
const flushedActiveMs = flushAgentTimer(sessionId, agentId);
|
|
1350
1044
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1351
1045
|
status: "completed",
|
|
1352
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1046
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1047
|
+
activeMs: flushedActiveMs
|
|
1353
1048
|
});
|
|
1354
1049
|
const session = getSession(cwd, sessionId);
|
|
1355
1050
|
const agent = session.agents.find((a) => a.id === agentId);
|
|
@@ -1367,10 +1062,12 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
1367
1062
|
}
|
|
1368
1063
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
1369
1064
|
unregisterAgentPane(sessionId, agentId);
|
|
1065
|
+
const flushedActiveMs = flushAgentTimer(sessionId, agentId);
|
|
1370
1066
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1371
1067
|
status: "killed",
|
|
1372
1068
|
killedReason: reason,
|
|
1373
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1069
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1070
|
+
activeMs: flushedActiveMs
|
|
1374
1071
|
});
|
|
1375
1072
|
const session = getSession(cwd, sessionId);
|
|
1376
1073
|
return allAgentsDone(session);
|
|
@@ -1383,105 +1080,515 @@ function allAgentsDone(session) {
|
|
|
1383
1080
|
// src/daemon/pane-monitor.ts
|
|
1384
1081
|
var monitorInterval = null;
|
|
1385
1082
|
var onAllAgentsDone = null;
|
|
1083
|
+
var lastPollTime = 0;
|
|
1084
|
+
var storedPollIntervalMs = 5e3;
|
|
1085
|
+
var activeTimers = /* @__PURE__ */ new Map();
|
|
1086
|
+
function initTimers(sessionId, session) {
|
|
1087
|
+
const entry = {
|
|
1088
|
+
sessionMs: session.activeMs,
|
|
1089
|
+
agentMs: /* @__PURE__ */ new Map(),
|
|
1090
|
+
cycleMs: /* @__PURE__ */ new Map()
|
|
1091
|
+
};
|
|
1092
|
+
for (const agent of session.agents) {
|
|
1093
|
+
entry.agentMs.set(agent.id, agent.activeMs);
|
|
1094
|
+
}
|
|
1095
|
+
for (const cycle of session.orchestratorCycles) {
|
|
1096
|
+
entry.cycleMs.set(cycle.cycle, cycle.activeMs);
|
|
1097
|
+
}
|
|
1098
|
+
activeTimers.set(sessionId, entry);
|
|
1099
|
+
}
|
|
1100
|
+
function getActiveTimers(sessionId) {
|
|
1101
|
+
return activeTimers.get(sessionId);
|
|
1102
|
+
}
|
|
1103
|
+
async function flushTimers(sessionId) {
|
|
1104
|
+
const entry = activeTimers.get(sessionId);
|
|
1105
|
+
if (!entry) return;
|
|
1106
|
+
const tracked = trackedSessions.get(sessionId);
|
|
1107
|
+
if (!tracked) return;
|
|
1108
|
+
let session;
|
|
1109
|
+
try {
|
|
1110
|
+
session = getSession(tracked.cwd, sessionId);
|
|
1111
|
+
} catch {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const sessionDelta = entry.sessionMs - session.activeMs;
|
|
1115
|
+
const agentDeltas = /* @__PURE__ */ new Map();
|
|
1116
|
+
for (const [agentId, ms] of entry.agentMs) {
|
|
1117
|
+
const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
|
|
1118
|
+
const persisted = agent?.activeMs ?? 0;
|
|
1119
|
+
const delta = ms - persisted;
|
|
1120
|
+
if (delta > 0) agentDeltas.set(agentId, delta);
|
|
1121
|
+
}
|
|
1122
|
+
const cycleDeltas = /* @__PURE__ */ new Map();
|
|
1123
|
+
for (const [cycleNum, ms] of entry.cycleMs) {
|
|
1124
|
+
const cycle = session.orchestratorCycles.find((c) => c.cycle === cycleNum);
|
|
1125
|
+
const persisted = cycle?.activeMs ?? 0;
|
|
1126
|
+
const delta = ms - persisted;
|
|
1127
|
+
if (delta > 0) cycleDeltas.set(cycleNum, delta);
|
|
1128
|
+
}
|
|
1129
|
+
if (sessionDelta > 0 || agentDeltas.size > 0 || cycleDeltas.size > 0) {
|
|
1130
|
+
await incrementActiveTime(tracked.cwd, sessionId, Math.max(0, sessionDelta), agentDeltas, cycleDeltas);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function flushAgentTimer(sessionId, agentId) {
|
|
1134
|
+
const entry = activeTimers.get(sessionId);
|
|
1135
|
+
if (!entry) return 0;
|
|
1136
|
+
return entry.agentMs.get(agentId) ?? 0;
|
|
1137
|
+
}
|
|
1138
|
+
function flushCycleTimer(sessionId, cycleNumber) {
|
|
1139
|
+
const entry = activeTimers.get(sessionId);
|
|
1140
|
+
if (!entry) return 0;
|
|
1141
|
+
return entry.cycleMs.get(cycleNumber) ?? 0;
|
|
1142
|
+
}
|
|
1143
|
+
function getTrackedSessionIds() {
|
|
1144
|
+
return [...trackedSessions.keys()];
|
|
1145
|
+
}
|
|
1386
1146
|
function setRespawnCallback(cb) {
|
|
1387
1147
|
onAllAgentsDone = cb;
|
|
1388
1148
|
}
|
|
1389
1149
|
function startMonitor(pollIntervalMs = 5e3) {
|
|
1390
1150
|
if (monitorInterval) return;
|
|
1151
|
+
storedPollIntervalMs = pollIntervalMs;
|
|
1152
|
+
lastPollTime = Date.now();
|
|
1391
1153
|
monitorInterval = setInterval(() => {
|
|
1392
1154
|
pollAllSessions().catch((err) => {
|
|
1393
1155
|
console.error("[sisyphus] Pane monitor error:", err);
|
|
1394
1156
|
});
|
|
1395
|
-
}, pollIntervalMs);
|
|
1396
|
-
}
|
|
1397
|
-
function stopMonitor() {
|
|
1398
|
-
if (monitorInterval) {
|
|
1399
|
-
clearInterval(monitorInterval);
|
|
1400
|
-
monitorInterval = null;
|
|
1157
|
+
}, pollIntervalMs);
|
|
1158
|
+
}
|
|
1159
|
+
function stopMonitor() {
|
|
1160
|
+
if (monitorInterval) {
|
|
1161
|
+
clearInterval(monitorInterval);
|
|
1162
|
+
monitorInterval = null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
var trackedSessions = /* @__PURE__ */ new Map();
|
|
1166
|
+
function trackSession(sessionId, cwd, tmuxSession) {
|
|
1167
|
+
const existing = trackedSessions.get(sessionId);
|
|
1168
|
+
trackedSessions.set(sessionId, { id: sessionId, cwd, tmuxSession, windowId: existing ? existing.windowId : null });
|
|
1169
|
+
}
|
|
1170
|
+
function updateTrackedWindow(sessionId, windowId) {
|
|
1171
|
+
const entry = trackedSessions.get(sessionId);
|
|
1172
|
+
if (!entry) throw new Error(`Cannot update window for untracked session: ${sessionId}`);
|
|
1173
|
+
entry.windowId = windowId;
|
|
1174
|
+
}
|
|
1175
|
+
function untrackSession(sessionId) {
|
|
1176
|
+
trackedSessions.delete(sessionId);
|
|
1177
|
+
}
|
|
1178
|
+
async function pollAllSessions() {
|
|
1179
|
+
const now = Date.now();
|
|
1180
|
+
const elapsed = now - lastPollTime;
|
|
1181
|
+
const threshold = storedPollIntervalMs * 3;
|
|
1182
|
+
const increment = elapsed > threshold ? storedPollIntervalMs : elapsed;
|
|
1183
|
+
lastPollTime = now;
|
|
1184
|
+
for (const { id: sessionId, cwd, windowId } of trackedSessions.values()) {
|
|
1185
|
+
if (windowId) {
|
|
1186
|
+
await pollSession(sessionId, cwd, windowId, increment);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
async function pollSession(sessionId, cwd, windowId, increment) {
|
|
1191
|
+
let session;
|
|
1192
|
+
try {
|
|
1193
|
+
session = getSession(cwd, sessionId);
|
|
1194
|
+
} catch (err) {
|
|
1195
|
+
console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (session.status === "completed") {
|
|
1199
|
+
const orchPaneId2 = getOrchestratorPaneId(sessionId);
|
|
1200
|
+
if (orchPaneId2) {
|
|
1201
|
+
const livePanes2 = listPanes(windowId);
|
|
1202
|
+
const livePaneIds2 = new Set(livePanes2.map((p) => p.paneId));
|
|
1203
|
+
if (!livePaneIds2.has(orchPaneId2)) {
|
|
1204
|
+
cleanupSessionMaps(sessionId);
|
|
1205
|
+
untrackSession(sessionId);
|
|
1206
|
+
console.log(`[sisyphus] Session ${sessionId} cleaned up: orchestrator pane closed by user`);
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
cleanupSessionMaps(sessionId);
|
|
1210
|
+
untrackSession(sessionId);
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (session.status !== "active") return;
|
|
1215
|
+
const livePanes = listPanes(windowId);
|
|
1216
|
+
if (livePanes.length === 0) {
|
|
1217
|
+
const tracked = trackedSessions.get(sessionId);
|
|
1218
|
+
if (tracked && !sessionExists(tracked.tmuxSession)) {
|
|
1219
|
+
await flushTimers(sessionId);
|
|
1220
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
1221
|
+
untrackSession(sessionId);
|
|
1222
|
+
console.log(`[sisyphus] Session ${sessionId} paused: tmux session destroyed`);
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1227
|
+
let timerEntry = activeTimers.get(sessionId);
|
|
1228
|
+
if (!timerEntry) {
|
|
1229
|
+
initTimers(sessionId, session);
|
|
1230
|
+
timerEntry = activeTimers.get(sessionId);
|
|
1231
|
+
}
|
|
1232
|
+
let anyAlive = false;
|
|
1233
|
+
for (const agent of session.agents) {
|
|
1234
|
+
if (agent.status === "running" && livePaneIds.has(agent.paneId)) {
|
|
1235
|
+
timerEntry.agentMs.set(agent.id, (timerEntry.agentMs.get(agent.id) ?? 0) + increment);
|
|
1236
|
+
anyAlive = true;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
1240
|
+
if (orchPaneId && livePaneIds.has(orchPaneId)) {
|
|
1241
|
+
const currentCycle = session.orchestratorCycles.length;
|
|
1242
|
+
if (currentCycle > 0) {
|
|
1243
|
+
timerEntry.cycleMs.set(currentCycle, (timerEntry.cycleMs.get(currentCycle) ?? 0) + increment);
|
|
1244
|
+
}
|
|
1245
|
+
anyAlive = true;
|
|
1246
|
+
}
|
|
1247
|
+
if (anyAlive) {
|
|
1248
|
+
timerEntry.sessionMs += increment;
|
|
1249
|
+
}
|
|
1250
|
+
let paneRemoved = false;
|
|
1251
|
+
for (const agent of session.agents) {
|
|
1252
|
+
if (agent.status !== "running") continue;
|
|
1253
|
+
if (!livePaneIds.has(agent.paneId)) {
|
|
1254
|
+
paneRemoved = true;
|
|
1255
|
+
const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
|
|
1256
|
+
if (allDone && onAllAgentsDone) {
|
|
1257
|
+
onAllAgentsDone(sessionId, cwd, windowId);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
if (paneRemoved) selectLayout(windowId);
|
|
1262
|
+
if (orchPaneId && !livePaneIds.has(orchPaneId)) {
|
|
1263
|
+
const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
|
|
1264
|
+
await completeOrchestratorCycle(cwd, sessionId, void 0, void 0, cycleActiveMs);
|
|
1265
|
+
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
1266
|
+
if (runningAgents.length === 0) {
|
|
1267
|
+
await flushTimers(sessionId);
|
|
1268
|
+
await updateSessionStatus(cwd, sessionId, "paused");
|
|
1269
|
+
console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
session = getSession(cwd, sessionId);
|
|
1273
|
+
if (session.status === "active" && session.agents.length > 0 && session.agents.every((a) => a.status !== "running") && (!orchPaneId || !livePaneIds.has(orchPaneId)) && onAllAgentsDone) {
|
|
1274
|
+
console.log(`[sisyphus] Detected stuck session ${sessionId}: all agents done, no orchestrator \u2014 triggering respawn`);
|
|
1275
|
+
onAllAgentsDone(sessionId, cwd, windowId);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/daemon/orchestrator.ts
|
|
1280
|
+
function detectRepos(cwd) {
|
|
1281
|
+
const config = loadConfig(cwd);
|
|
1282
|
+
const repos = [];
|
|
1283
|
+
if (existsSync5(join4(cwd, ".git"))) {
|
|
1284
|
+
try {
|
|
1285
|
+
repos.push(getRepoInfo(cwd, "."));
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
const entries = readdirSync4(cwd, { withFileTypes: true });
|
|
1291
|
+
for (const entry of entries) {
|
|
1292
|
+
if (!entry.isDirectory()) continue;
|
|
1293
|
+
if (entry.name.startsWith(".")) continue;
|
|
1294
|
+
const childPath = join4(cwd, entry.name);
|
|
1295
|
+
if (existsSync5(join4(childPath, ".git"))) {
|
|
1296
|
+
try {
|
|
1297
|
+
repos.push(getRepoInfo(childPath, entry.name));
|
|
1298
|
+
} catch {
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
} catch {
|
|
1303
|
+
}
|
|
1304
|
+
if (config.repos && config.repos.length > 0) {
|
|
1305
|
+
const allowed = new Set(config.repos);
|
|
1306
|
+
return repos.filter((r) => r.name === "." || allowed.has(r.name));
|
|
1307
|
+
}
|
|
1308
|
+
return repos;
|
|
1309
|
+
}
|
|
1310
|
+
function getRepoInfo(repoPath, name) {
|
|
1311
|
+
const branchRaw = execSafe(`git -C ${shellQuote(repoPath)} rev-parse --abbrev-ref HEAD`)?.trim();
|
|
1312
|
+
if (!branchRaw) throw new Error(`Failed to detect git branch for repo: ${repoPath}`);
|
|
1313
|
+
const status = execSafe(`git -C ${shellQuote(repoPath)} status --porcelain`);
|
|
1314
|
+
const isDirty = !!(status && status.trim().length > 0);
|
|
1315
|
+
return { name, path: repoPath, branch: branchRaw, isDirty };
|
|
1316
|
+
}
|
|
1317
|
+
var sessionWindowMap = /* @__PURE__ */ new Map();
|
|
1318
|
+
var sessionOrchestratorPane = /* @__PURE__ */ new Map();
|
|
1319
|
+
function getWindowId(sessionId) {
|
|
1320
|
+
return sessionWindowMap.get(sessionId);
|
|
1321
|
+
}
|
|
1322
|
+
function setWindowId(sessionId, windowId) {
|
|
1323
|
+
sessionWindowMap.set(sessionId, windowId);
|
|
1324
|
+
}
|
|
1325
|
+
function getOrchestratorPaneId(sessionId) {
|
|
1326
|
+
return sessionOrchestratorPane.get(sessionId);
|
|
1327
|
+
}
|
|
1328
|
+
function setOrchestratorPaneId(sessionId, paneId) {
|
|
1329
|
+
sessionOrchestratorPane.set(sessionId, paneId);
|
|
1330
|
+
}
|
|
1331
|
+
function loadOrchestratorPrompt(cwd, sessionId, mode) {
|
|
1332
|
+
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
1333
|
+
if (existsSync5(projectPath)) {
|
|
1334
|
+
return readFileSync4(projectPath, "utf-8");
|
|
1335
|
+
}
|
|
1336
|
+
const basePath = resolve3(import.meta.dirname, "../templates/orchestrator-base.md");
|
|
1337
|
+
const base = readFileSync4(basePath, "utf-8");
|
|
1338
|
+
let modePrompt;
|
|
1339
|
+
if (mode === "strategy") {
|
|
1340
|
+
const strategyTemplatePath = resolve3(import.meta.dirname, "../templates/orchestrator-strategy.md");
|
|
1341
|
+
modePrompt = readFileSync4(strategyTemplatePath, "utf-8");
|
|
1342
|
+
} else if (mode === "implementation") {
|
|
1343
|
+
const implPath = resolve3(import.meta.dirname, "../templates/orchestrator-impl.md");
|
|
1344
|
+
modePrompt = readFileSync4(implPath, "utf-8");
|
|
1345
|
+
} else if (mode === "validation") {
|
|
1346
|
+
const validationPath = resolve3(import.meta.dirname, "../templates/orchestrator-validation.md");
|
|
1347
|
+
modePrompt = readFileSync4(validationPath, "utf-8");
|
|
1348
|
+
} else {
|
|
1349
|
+
const planningPath = resolve3(import.meta.dirname, "../templates/orchestrator-planning.md");
|
|
1350
|
+
modePrompt = readFileSync4(planningPath, "utf-8");
|
|
1351
|
+
}
|
|
1352
|
+
return base + "\n\n" + modePrompt;
|
|
1353
|
+
}
|
|
1354
|
+
function formatStateForOrchestrator(session) {
|
|
1355
|
+
const cycleNum = session.orchestratorCycles.length;
|
|
1356
|
+
const ctxDir = contextDir(session.cwd, session.id);
|
|
1357
|
+
const roadmapFile = roadmapPath(session.cwd, session.id);
|
|
1358
|
+
const logFile = cycleLogPath(session.cwd, session.id, cycleNum + 1);
|
|
1359
|
+
let contextSection = "";
|
|
1360
|
+
if (cycleNum === 0) {
|
|
1361
|
+
if (session.context) {
|
|
1362
|
+
contextSection = `
|
|
1363
|
+
## Context
|
|
1364
|
+
|
|
1365
|
+
${session.context}
|
|
1366
|
+
`;
|
|
1367
|
+
}
|
|
1368
|
+
} else {
|
|
1369
|
+
let ctxFiles = [];
|
|
1370
|
+
if (existsSync5(ctxDir)) {
|
|
1371
|
+
ctxFiles = readdirSync4(ctxDir).filter((f) => f !== "CLAUDE.md");
|
|
1372
|
+
}
|
|
1373
|
+
if (ctxFiles.length > 0) {
|
|
1374
|
+
const ctxLines = ctxFiles.map((f) => `- ${join4(ctxDir, f)}`).join("\n");
|
|
1375
|
+
contextSection = `
|
|
1376
|
+
## Context
|
|
1377
|
+
|
|
1378
|
+
${ctxLines}
|
|
1379
|
+
`;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
const messages = session.messages ?? [];
|
|
1383
|
+
const messagesSection = messages.length > 0 ? "\n### Messages\n\n" + messages.map((m) => {
|
|
1384
|
+
const sourceLabel = m.source.type === "agent" ? `agent:${m.source.agentId}` : m.source.type === "system" && m.source.detail ? `system:${m.source.detail}` : m.source.type;
|
|
1385
|
+
const fileRef = m.filePath ? ` \u2192 ${m.filePath}` : "";
|
|
1386
|
+
return `- [${sourceLabel} @ ${m.timestamp}] "${m.summary}"${fileRef}`;
|
|
1387
|
+
}).join("\n") + "\n" : "";
|
|
1388
|
+
let previousCyclesSection = "";
|
|
1389
|
+
if (session.orchestratorCycles.length > 1) {
|
|
1390
|
+
const previousCycles = session.orchestratorCycles.slice(0, -1);
|
|
1391
|
+
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
1392
|
+
const lines = previousCycles.map((c) => {
|
|
1393
|
+
const agentDescs = c.agentsSpawned.map((id) => {
|
|
1394
|
+
const agent = agentMap.get(id);
|
|
1395
|
+
return agent ? `${id} (${agent.name})` : id;
|
|
1396
|
+
}).join(", ");
|
|
1397
|
+
return `Cycle ${c.cycle}: ${agentDescs || "(none)"}`;
|
|
1398
|
+
});
|
|
1399
|
+
previousCyclesSection = `
|
|
1400
|
+
### Previous Cycles
|
|
1401
|
+
|
|
1402
|
+
${lines.join("\n")}
|
|
1403
|
+
`;
|
|
1401
1404
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1405
|
+
let mostRecentCycleSection = "";
|
|
1406
|
+
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
1407
|
+
if (lastCycle && lastCycle.agentsSpawned.length > 0) {
|
|
1408
|
+
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
1409
|
+
const agentLines = lastCycle.agentsSpawned.map((id) => {
|
|
1410
|
+
const agent = agentMap.get(id);
|
|
1411
|
+
if (!agent) return `- **${id}**: unknown (no agent data)`;
|
|
1412
|
+
const finalReport = agent.reports.find((r) => r.type === "final");
|
|
1413
|
+
const reportToUse = finalReport ?? agent.reports[agent.reports.length - 1];
|
|
1414
|
+
const reportRef = reportToUse ? `@${reportToUse.filePath}` : "(no reports)";
|
|
1415
|
+
return `- **${id}** (${agent.name}) [${agent.status}]: ${reportRef}`;
|
|
1416
|
+
}).join("\n");
|
|
1417
|
+
mostRecentCycleSection = `
|
|
1418
|
+
### Most Recent Cycle
|
|
1419
|
+
|
|
1420
|
+
${agentLines}
|
|
1421
|
+
`;
|
|
1422
|
+
}
|
|
1423
|
+
const strategyFile = strategyPath(session.cwd, session.id);
|
|
1424
|
+
const strategyRef = existsSync5(strategyFile) ? `@${strategyFile}` : "(empty)";
|
|
1425
|
+
const roadmapRef = existsSync5(roadmapFile) ? `@${roadmapFile}` : "(empty)";
|
|
1426
|
+
const repos = detectRepos(session.cwd);
|
|
1427
|
+
let repositoriesSection = "\n\n## Repositories\n";
|
|
1428
|
+
if (repos.length === 0) {
|
|
1429
|
+
repositoriesSection += "\nNo git repositories detected.\n";
|
|
1430
|
+
} else {
|
|
1431
|
+
for (const repo of repos) {
|
|
1432
|
+
const dirtyTag = repo.isDirty ? " (dirty)" : "";
|
|
1433
|
+
repositoriesSection += `
|
|
1434
|
+
### ${repo.name === "." ? "Session Root (.)" : repo.name}
|
|
1435
|
+
`;
|
|
1436
|
+
repositoriesSection += `Branch: \`${repo.branch}\`${dirtyTag}
|
|
1437
|
+
`;
|
|
1438
|
+
const repoAgents = session.agents.filter((a) => a.repo === repo.name);
|
|
1439
|
+
if (repoAgents.length > 0) {
|
|
1440
|
+
repositoriesSection += "\nAgents:\n";
|
|
1441
|
+
for (const a of repoAgents) {
|
|
1442
|
+
repositoriesSection += `- ${a.id} (${a.name}) [${a.status}]
|
|
1443
|
+
`;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (repos.length > 1) {
|
|
1448
|
+
repositoriesSection += '\nTarget agents at specific repos:\n```bash\nsisyphus spawn --name "impl" --repo <repo-name> "task"\n```\n';
|
|
1420
1449
|
}
|
|
1421
1450
|
}
|
|
1451
|
+
const goalFile = goalPath(session.cwd, session.id);
|
|
1452
|
+
const goalContent = existsSync5(goalFile) ? readFileSync4(goalFile, "utf-8").trim() : session.task;
|
|
1453
|
+
return `## Goal
|
|
1454
|
+
|
|
1455
|
+
${goalContent}
|
|
1456
|
+
${contextSection}${messagesSection}
|
|
1457
|
+
### Cycle Log
|
|
1458
|
+
|
|
1459
|
+
Write your cycle summary to: ${logFile}
|
|
1460
|
+
${previousCyclesSection}${mostRecentCycleSection}
|
|
1461
|
+
## Strategy
|
|
1462
|
+
|
|
1463
|
+
${strategyRef}
|
|
1464
|
+
|
|
1465
|
+
## Roadmap
|
|
1466
|
+
|
|
1467
|
+
${roadmapRef}
|
|
1468
|
+
`;
|
|
1422
1469
|
}
|
|
1423
|
-
async function
|
|
1424
|
-
let session;
|
|
1470
|
+
async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
1425
1471
|
try {
|
|
1426
|
-
|
|
1427
|
-
} catch
|
|
1428
|
-
|
|
1429
|
-
return;
|
|
1430
|
-
}
|
|
1431
|
-
if (session.status === "completed") {
|
|
1432
|
-
const orchPaneId2 = getOrchestratorPaneId(sessionId);
|
|
1433
|
-
if (orchPaneId2) {
|
|
1434
|
-
const livePanes2 = listPanes(windowId);
|
|
1435
|
-
const livePaneIds2 = new Set(livePanes2.map((p) => p.paneId));
|
|
1436
|
-
if (!livePaneIds2.has(orchPaneId2)) {
|
|
1437
|
-
cleanupSessionMaps(sessionId);
|
|
1438
|
-
untrackSession(sessionId);
|
|
1439
|
-
console.log(`[sisyphus] Session ${sessionId} cleaned up: orchestrator pane closed by user`);
|
|
1440
|
-
}
|
|
1441
|
-
} else {
|
|
1442
|
-
cleanupSessionMaps(sessionId);
|
|
1443
|
-
untrackSession(sessionId);
|
|
1444
|
-
}
|
|
1445
|
-
return;
|
|
1472
|
+
execSync2("which claude", { stdio: "pipe", env: EXEC_ENV });
|
|
1473
|
+
} catch {
|
|
1474
|
+
throw new Error("Claude CLI not found on PATH. Run `sisyphus doctor` to diagnose.");
|
|
1446
1475
|
}
|
|
1447
|
-
|
|
1448
|
-
const
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1476
|
+
const session = getSession(cwd, sessionId);
|
|
1477
|
+
const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
|
|
1478
|
+
const mode = lastCycle?.mode ?? "strategy";
|
|
1479
|
+
const basePrompt = loadOrchestratorPrompt(cwd, sessionId, mode);
|
|
1480
|
+
const formattedState = formatStateForOrchestrator(session);
|
|
1481
|
+
const agentPluginPath = resolve3(import.meta.dirname, "../templates/agent-plugin");
|
|
1482
|
+
const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
|
|
1483
|
+
const agentTypeLines = agentTypes.length > 0 ? agentTypes.map((t) => {
|
|
1484
|
+
const modelTag = t.model ? ` (${t.model})` : "";
|
|
1485
|
+
const desc = t.description ? ` \u2014 ${t.description}` : "";
|
|
1486
|
+
return `- \`${t.qualifiedName}\`${modelTag}${desc}`;
|
|
1487
|
+
}).join("\n") : " (none)";
|
|
1488
|
+
const sesDir = sessionDir(cwd, sessionId);
|
|
1489
|
+
const substituteEnvVars = (text) => text.replace(/\$SISYPHUS_SESSION_DIR/g, sesDir).replace(/\$SISYPHUS_SESSION_ID/g, sessionId);
|
|
1490
|
+
const systemPrompt = substituteEnvVars(basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines));
|
|
1491
|
+
const cycleNum = session.orchestratorCycles.length + 1;
|
|
1492
|
+
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
1493
|
+
writeFileSync4(promptFilePath, systemPrompt, "utf-8");
|
|
1494
|
+
sessionWindowMap.set(sessionId, windowId);
|
|
1495
|
+
const npmBinDir = resolveNpmBinDir();
|
|
1496
|
+
const envExports = buildEnvExports([
|
|
1497
|
+
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
1498
|
+
`export SISYPHUS_AGENT_ID='orchestrator'`,
|
|
1499
|
+
`export SISYPHUS_CWD='${cwd}'`,
|
|
1500
|
+
`export SISYPHUS_SESSION_DIR='${sesDir}'`,
|
|
1501
|
+
`export PATH="${npmBinDir}:$PATH"`
|
|
1502
|
+
]);
|
|
1503
|
+
let userPrompt = formattedState;
|
|
1504
|
+
if (message) {
|
|
1505
|
+
userPrompt += `
|
|
1506
|
+
|
|
1507
|
+
## Continuation Instructions
|
|
1508
|
+
|
|
1509
|
+
The user resumed this session with new instructions: ${message}`;
|
|
1510
|
+
} else {
|
|
1511
|
+
const storedPrompt = lastCycle?.nextPrompt;
|
|
1512
|
+
const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
|
|
1513
|
+
userPrompt += `
|
|
1514
|
+
|
|
1515
|
+
## Continuation Instructions
|
|
1516
|
+
|
|
1517
|
+
${continuationText}`;
|
|
1457
1518
|
}
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
if (!livePaneIds.has(agent.paneId)) {
|
|
1463
|
-
paneRemoved = true;
|
|
1464
|
-
const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
|
|
1465
|
-
if (allDone && onAllAgentsDone) {
|
|
1466
|
-
onAllAgentsDone(sessionId, cwd, windowId);
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1519
|
+
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
1520
|
+
writeFileSync4(userPromptFilePath, substituteEnvVars(userPrompt), "utf-8");
|
|
1521
|
+
if (session.messages && session.messages.length > 0) {
|
|
1522
|
+
await drainMessages(cwd, sessionId, session.messages.length);
|
|
1469
1523
|
}
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1524
|
+
const pluginPath = resolve3(import.meta.dirname, "../templates/orchestrator-plugin");
|
|
1525
|
+
const settingsPath = resolve3(import.meta.dirname, "../templates/orchestrator-settings.json");
|
|
1526
|
+
const config = loadConfig(cwd);
|
|
1527
|
+
const effort = config.orchestratorEffort ?? "high";
|
|
1528
|
+
const claudeSessionId = randomUUID3();
|
|
1529
|
+
const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --session-id "${claudeSessionId}" --settings "${settingsPath}" --plugin-dir "${pluginPath}" --name "ssph:orch ${session.name ?? sessionId.slice(0, 8)} c${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
1530
|
+
const paneId = createPane(windowId, cwd, "left");
|
|
1531
|
+
sessionOrchestratorPane.set(sessionId, paneId);
|
|
1532
|
+
registerPane(paneId, sessionId, "orchestrator");
|
|
1533
|
+
const sessionLabel = session.name ?? sessionId.slice(0, 8);
|
|
1534
|
+
setPaneTitle(paneId, `ssph:orch ${sessionLabel} c${cycleNum}`);
|
|
1535
|
+
setPaneStyle(paneId, ORCHESTRATOR_COLOR, { role: "orch", session: sessionLabel, cycle: `c${cycleNum}` });
|
|
1536
|
+
const bannerCmd = resolveBannerCmd();
|
|
1537
|
+
const notifyCmd = buildNotifyCmd(paneId);
|
|
1538
|
+
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `orchestrator-run-${cycleNum}`, [
|
|
1539
|
+
"#!/usr/bin/env bash",
|
|
1540
|
+
...bannerCmd ? [bannerCmd] : [],
|
|
1541
|
+
envExports,
|
|
1542
|
+
claudeCmd,
|
|
1543
|
+
notifyCmd
|
|
1544
|
+
]);
|
|
1545
|
+
sendKeys(paneId, `bash '${scriptPath}'`);
|
|
1546
|
+
await addOrchestratorCycle(cwd, sessionId, {
|
|
1547
|
+
cycle: cycleNum,
|
|
1548
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1549
|
+
activeMs: 0,
|
|
1550
|
+
agentsSpawned: [],
|
|
1551
|
+
paneId,
|
|
1552
|
+
claudeSessionId
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
function resolveOrchestratorPane(sessionId, cwd) {
|
|
1556
|
+
const memPane = sessionOrchestratorPane.get(sessionId);
|
|
1557
|
+
if (memPane) return memPane;
|
|
1558
|
+
const session = getSession(cwd, sessionId);
|
|
1559
|
+
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
1560
|
+
return lastCycle?.paneId ?? void 0;
|
|
1561
|
+
}
|
|
1562
|
+
async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
|
|
1563
|
+
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
1564
|
+
if (paneId) {
|
|
1565
|
+
killPane(paneId);
|
|
1566
|
+
unregisterPane(paneId);
|
|
1567
|
+
sessionOrchestratorPane.delete(sessionId);
|
|
1478
1568
|
}
|
|
1479
|
-
|
|
1480
|
-
if (
|
|
1481
|
-
|
|
1482
|
-
|
|
1569
|
+
const windowId = sessionWindowMap.get(sessionId);
|
|
1570
|
+
if (windowId) selectLayout(windowId);
|
|
1571
|
+
const session = getSession(cwd, sessionId);
|
|
1572
|
+
const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
|
|
1573
|
+
await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode, cycleActiveMs);
|
|
1574
|
+
const freshSession = getSession(cwd, sessionId);
|
|
1575
|
+
const runningAgents = freshSession.agents.filter((a) => a.status === "running");
|
|
1576
|
+
if (runningAgents.length === 0) {
|
|
1577
|
+
console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
|
|
1483
1578
|
}
|
|
1484
1579
|
}
|
|
1580
|
+
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
1581
|
+
const session = getSession(cwd, sessionId);
|
|
1582
|
+
const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
|
|
1583
|
+
await completeOrchestratorCycle(cwd, sessionId, void 0, void 0, cycleActiveMs);
|
|
1584
|
+
await completeSession(cwd, sessionId, report);
|
|
1585
|
+
console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
|
|
1586
|
+
}
|
|
1587
|
+
function cleanupSessionMaps(sessionId) {
|
|
1588
|
+
sessionOrchestratorPane.delete(sessionId);
|
|
1589
|
+
sessionWindowMap.delete(sessionId);
|
|
1590
|
+
unregisterSessionPanes(sessionId);
|
|
1591
|
+
}
|
|
1485
1592
|
|
|
1486
1593
|
// src/daemon/notify.ts
|
|
1487
1594
|
import { execFile } from "child_process";
|
|
@@ -1521,6 +1628,49 @@ async function startSession(task, cwd, context, name) {
|
|
|
1521
1628
|
updateTrackedWindow(sessionId, windowId);
|
|
1522
1629
|
killPane(initialPaneId);
|
|
1523
1630
|
pruneOldSessions(cwd);
|
|
1631
|
+
if (!name) {
|
|
1632
|
+
generateSessionName(task).then(async (generatedName) => {
|
|
1633
|
+
if (!generatedName) {
|
|
1634
|
+
console.log(`[sisyphus] Name generation returned null for session ${sessionId}`);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
let finalName = generatedName;
|
|
1638
|
+
let candidate = `sisyphus-${finalName}`;
|
|
1639
|
+
let attempt = 0;
|
|
1640
|
+
while (sessionExists(candidate) && attempt < 5) {
|
|
1641
|
+
attempt++;
|
|
1642
|
+
finalName = `${generatedName}-${attempt}`;
|
|
1643
|
+
candidate = `sisyphus-${finalName}`;
|
|
1644
|
+
}
|
|
1645
|
+
if (sessionExists(candidate)) return;
|
|
1646
|
+
try {
|
|
1647
|
+
renameSession(tmuxName, candidate);
|
|
1648
|
+
} catch {
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
await updateSessionName(cwd, sessionId, finalName);
|
|
1652
|
+
await updateSessionTmux(cwd, sessionId, candidate, getSession(cwd, sessionId).tmuxWindowId);
|
|
1653
|
+
trackSession(sessionId, cwd, candidate);
|
|
1654
|
+
registerSessionTmux(sessionId, candidate, getSession(cwd, sessionId).tmuxWindowId);
|
|
1655
|
+
const session2 = getSession(cwd, sessionId);
|
|
1656
|
+
for (const pane of getSessionPanes(sessionId)) {
|
|
1657
|
+
updatePaneMeta(pane.paneId, { session: finalName });
|
|
1658
|
+
if (pane.role === "orchestrator") {
|
|
1659
|
+
setPaneTitle(pane.paneId, `ssph:orch ${finalName} c${session2.orchestratorCycles.length}`);
|
|
1660
|
+
} else if (pane.role === "agent" && pane.agentId) {
|
|
1661
|
+
const agent = session2.agents.find((a) => a.id === pane.agentId);
|
|
1662
|
+
if (agent) {
|
|
1663
|
+
const shortType = agent.agentType && agent.agentType !== "worker" ? agent.agentType.replace(/^sisyphus:/, "") : "";
|
|
1664
|
+
const paneLabel = shortType ? `${agent.name}-${shortType}` : agent.name;
|
|
1665
|
+
setPaneTitle(pane.paneId, `ssph:${finalName} ${paneLabel} c${session2.orchestratorCycles.length}`);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
console.log(`[sisyphus] Session ${sessionId} named: ${finalName}`);
|
|
1670
|
+
}).catch((err) => {
|
|
1671
|
+
console.error(`[sisyphus] Name generation failed for session ${sessionId}:`, err);
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1524
1674
|
return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
|
|
1525
1675
|
}
|
|
1526
1676
|
var PRUNE_KEEP_COUNT = 10;
|
|
@@ -1528,8 +1678,8 @@ var PRUNE_KEEP_DAYS = 7;
|
|
|
1528
1678
|
function pruneOldSessions(cwd) {
|
|
1529
1679
|
try {
|
|
1530
1680
|
const dir = sessionsDir(cwd);
|
|
1531
|
-
if (!
|
|
1532
|
-
const entries =
|
|
1681
|
+
if (!existsSync6(dir)) return;
|
|
1682
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1533
1683
|
const candidates = [];
|
|
1534
1684
|
for (const entry of entries) {
|
|
1535
1685
|
if (!entry.isDirectory()) continue;
|
|
@@ -1552,7 +1702,7 @@ function pruneOldSessions(cwd) {
|
|
|
1552
1702
|
}
|
|
1553
1703
|
for (const c of candidates) {
|
|
1554
1704
|
if (keep.has(c.id)) continue;
|
|
1555
|
-
|
|
1705
|
+
rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
|
|
1556
1706
|
}
|
|
1557
1707
|
} catch (err) {
|
|
1558
1708
|
console.error("[sisyphus] Session pruning failed:", err);
|
|
@@ -1621,8 +1771,8 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
1621
1771
|
}
|
|
1622
1772
|
function listSessions(cwd) {
|
|
1623
1773
|
const dir = sessionsDir(cwd);
|
|
1624
|
-
if (!
|
|
1625
|
-
const entries =
|
|
1774
|
+
if (!existsSync6(dir)) return [];
|
|
1775
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1626
1776
|
const sessions = [];
|
|
1627
1777
|
for (const entry of entries) {
|
|
1628
1778
|
if (!entry.isDirectory()) continue;
|
|
@@ -1662,17 +1812,6 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1662
1812
|
if (session.status !== "active") return;
|
|
1663
1813
|
pendingRespawns.add(sessionId);
|
|
1664
1814
|
orchestratorDone.delete(sessionId);
|
|
1665
|
-
const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
|
|
1666
|
-
if (worktreeAgents.length > 0) {
|
|
1667
|
-
const results = mergeWorktrees(cwd, worktreeAgents);
|
|
1668
|
-
for (const result of results) {
|
|
1669
|
-
const mergeStatus = result.status;
|
|
1670
|
-
updateAgent(cwd, sessionId, result.agentId, {
|
|
1671
|
-
mergeStatus,
|
|
1672
|
-
mergeDetails: result.conflictDetails
|
|
1673
|
-
}).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
1815
|
const cycleNumber = session.orchestratorCycles.length;
|
|
1677
1816
|
if (cycleNumber > 0) {
|
|
1678
1817
|
createSnapshot(cwd, sessionId, cycleNumber);
|
|
@@ -1711,7 +1850,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1711
1850
|
}
|
|
1712
1851
|
});
|
|
1713
1852
|
}
|
|
1714
|
-
async function handleSpawn(sessionId, cwd, agentType, name, instruction,
|
|
1853
|
+
async function handleSpawn(sessionId, cwd, agentType, name, instruction, repo) {
|
|
1715
1854
|
const windowId = getWindowId(sessionId);
|
|
1716
1855
|
if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
|
|
1717
1856
|
const session = getSession(cwd, sessionId);
|
|
@@ -1721,12 +1860,14 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktre
|
|
|
1721
1860
|
}
|
|
1722
1861
|
const agent = await spawnAgent({
|
|
1723
1862
|
sessionId,
|
|
1863
|
+
sessionName: session.name,
|
|
1864
|
+
cycleNum: session.orchestratorCycles.length,
|
|
1724
1865
|
cwd,
|
|
1725
1866
|
agentType,
|
|
1726
1867
|
name,
|
|
1727
1868
|
instruction,
|
|
1728
1869
|
windowId,
|
|
1729
|
-
|
|
1870
|
+
repo
|
|
1730
1871
|
});
|
|
1731
1872
|
await appendAgentToLastCycle(cwd, sessionId, agent.id);
|
|
1732
1873
|
return { agentId: agent.id };
|
|
@@ -1758,16 +1899,15 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
|
1758
1899
|
}
|
|
1759
1900
|
async function handleComplete(sessionId, cwd, report) {
|
|
1760
1901
|
const session = getSession(cwd, sessionId);
|
|
1902
|
+
await flushTimers(sessionId);
|
|
1761
1903
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
1762
1904
|
switchToHomeSession(session);
|
|
1763
1905
|
}
|
|
1764
1906
|
async function handleContinue(sessionId, cwd) {
|
|
1765
1907
|
await continueSession(cwd, sessionId);
|
|
1766
1908
|
}
|
|
1767
|
-
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
1768
|
-
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
1769
|
-
}
|
|
1770
1909
|
async function handleKill(sessionId, cwd) {
|
|
1910
|
+
await flushTimers(sessionId);
|
|
1771
1911
|
const session = getSession(cwd, sessionId);
|
|
1772
1912
|
const windowId = getWindowId(sessionId);
|
|
1773
1913
|
let killedAgents = 0;
|
|
@@ -1781,11 +1921,6 @@ async function handleKill(sessionId, cwd) {
|
|
|
1781
1921
|
killedAgents++;
|
|
1782
1922
|
}
|
|
1783
1923
|
}
|
|
1784
|
-
for (const agent of session.agents) {
|
|
1785
|
-
if (agent.worktreePath && agent.branchName) {
|
|
1786
|
-
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
1924
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
1790
1925
|
if (orchPaneId) {
|
|
1791
1926
|
killPane(orchPaneId);
|
|
@@ -1820,9 +1955,6 @@ async function handleKillAgent(sessionId, cwd, agentId) {
|
|
|
1820
1955
|
if (agent.paneId) {
|
|
1821
1956
|
killPane(agent.paneId);
|
|
1822
1957
|
}
|
|
1823
|
-
if (agent.worktreePath && agent.branchName) {
|
|
1824
|
-
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1825
|
-
}
|
|
1826
1958
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1827
1959
|
status: "killed",
|
|
1828
1960
|
killedReason: "killed by user",
|
|
@@ -1852,11 +1984,6 @@ async function handleRollback(sessionId, cwd, toCycle) {
|
|
|
1852
1984
|
});
|
|
1853
1985
|
}
|
|
1854
1986
|
}
|
|
1855
|
-
for (const agent of session.agents) {
|
|
1856
|
-
if (agent.worktreePath && agent.branchName) {
|
|
1857
|
-
cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
1987
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
1861
1988
|
if (orchPaneId) {
|
|
1862
1989
|
killPane(orchPaneId);
|
|
@@ -1887,6 +2014,8 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
1887
2014
|
} else if (role === "orchestrator") {
|
|
1888
2015
|
const sessionName = session.name ?? sessionId.slice(0, 8);
|
|
1889
2016
|
sendTerminalNotification("Sisyphus", `Orchestrator exited without yielding (${sessionName})`);
|
|
2017
|
+
const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
|
|
2018
|
+
await completeOrchestratorCycle(cwd, sessionId, void 0, void 0, cycleActiveMs);
|
|
1890
2019
|
orchestratorDone.add(sessionId);
|
|
1891
2020
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1892
2021
|
if (!hasRunningAgents && session.agents.length > 0) {
|
|
@@ -1906,11 +2035,11 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
1906
2035
|
var server = null;
|
|
1907
2036
|
var sessionTrackingMap = /* @__PURE__ */ new Map();
|
|
1908
2037
|
function registryPath() {
|
|
1909
|
-
return
|
|
2038
|
+
return join5(globalDir(), "session-registry.json");
|
|
1910
2039
|
}
|
|
1911
2040
|
function persistSessionRegistry() {
|
|
1912
2041
|
const dir = globalDir();
|
|
1913
|
-
|
|
2042
|
+
mkdirSync3(dir, { recursive: true });
|
|
1914
2043
|
const registry = {};
|
|
1915
2044
|
for (const [id, tracking] of sessionTrackingMap) {
|
|
1916
2045
|
registry[id] = tracking.cwd;
|
|
@@ -1919,9 +2048,9 @@ function persistSessionRegistry() {
|
|
|
1919
2048
|
}
|
|
1920
2049
|
function loadSessionRegistry() {
|
|
1921
2050
|
const p = registryPath();
|
|
1922
|
-
if (!
|
|
2051
|
+
if (!existsSync7(p)) return {};
|
|
1923
2052
|
try {
|
|
1924
|
-
return JSON.parse(
|
|
2053
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
1925
2054
|
} catch {
|
|
1926
2055
|
return {};
|
|
1927
2056
|
}
|
|
@@ -1964,7 +2093,7 @@ async function handleRequest(req) {
|
|
|
1964
2093
|
case "spawn": {
|
|
1965
2094
|
const tracking = sessionTrackingMap.get(req.sessionId);
|
|
1966
2095
|
if (!tracking) return unknownSessionError(req.sessionId);
|
|
1967
|
-
const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.
|
|
2096
|
+
const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.repo);
|
|
1968
2097
|
return { ok: true, data: { agentId: result.agentId } };
|
|
1969
2098
|
}
|
|
1970
2099
|
case "submit": {
|
|
@@ -2003,6 +2132,18 @@ async function handleRequest(req) {
|
|
|
2003
2132
|
const cwd = sessionTrackingMap.get(req.sessionId)?.cwd ?? req.cwd;
|
|
2004
2133
|
if (!cwd) return unknownSessionError(req.sessionId);
|
|
2005
2134
|
const session = getSessionStatus(cwd, req.sessionId);
|
|
2135
|
+
const timers = getActiveTimers(req.sessionId);
|
|
2136
|
+
if (timers) {
|
|
2137
|
+
session.activeMs = timers.sessionMs;
|
|
2138
|
+
for (const agent of session.agents) {
|
|
2139
|
+
const agentMs = timers.agentMs.get(agent.id);
|
|
2140
|
+
if (agentMs != null) agent.activeMs = agentMs;
|
|
2141
|
+
}
|
|
2142
|
+
for (const cycle of session.orchestratorCycles) {
|
|
2143
|
+
const cycleMs = timers.cycleMs.get(cycle.cycle);
|
|
2144
|
+
if (cycleMs != null) cycle.activeMs = cycleMs;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2006
2147
|
return { ok: true, data: { session } };
|
|
2007
2148
|
}
|
|
2008
2149
|
return { ok: true, data: { message: "daemon running" } };
|
|
@@ -2037,7 +2178,7 @@ async function handleRequest(req) {
|
|
|
2037
2178
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2038
2179
|
if (!tracking) {
|
|
2039
2180
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2040
|
-
if (
|
|
2181
|
+
if (existsSync7(stateFile)) {
|
|
2041
2182
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2042
2183
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2043
2184
|
persistSessionRegistry();
|
|
@@ -2050,12 +2191,6 @@ async function handleRequest(req) {
|
|
|
2050
2191
|
if (session.tmuxWindowId) tracking.windowId = session.tmuxWindowId;
|
|
2051
2192
|
return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
|
|
2052
2193
|
}
|
|
2053
|
-
case "register_claude_session": {
|
|
2054
|
-
const tracking = sessionTrackingMap.get(req.sessionId);
|
|
2055
|
-
if (!tracking) return unknownSessionError(req.sessionId);
|
|
2056
|
-
await handleRegisterClaudeSession(tracking.cwd, req.sessionId, req.agentId, req.claudeSessionId);
|
|
2057
|
-
return { ok: true };
|
|
2058
|
-
}
|
|
2059
2194
|
case "kill": {
|
|
2060
2195
|
const tracking = sessionTrackingMap.get(req.sessionId);
|
|
2061
2196
|
if (!tracking) return unknownSessionError(req.sessionId);
|
|
@@ -2080,7 +2215,7 @@ async function handleRequest(req) {
|
|
|
2080
2215
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2081
2216
|
if (!tracking) {
|
|
2082
2217
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2083
|
-
if (
|
|
2218
|
+
if (existsSync7(stateFile)) {
|
|
2084
2219
|
registerSessionCwd(req.sessionId, req.cwd);
|
|
2085
2220
|
tracking = sessionTrackingMap.get(req.sessionId);
|
|
2086
2221
|
} else {
|
|
@@ -2094,7 +2229,7 @@ async function handleRequest(req) {
|
|
|
2094
2229
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2095
2230
|
if (!tracking) {
|
|
2096
2231
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2097
|
-
if (
|
|
2232
|
+
if (existsSync7(stateFile)) {
|
|
2098
2233
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2099
2234
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2100
2235
|
persistSessionRegistry();
|
|
@@ -2117,8 +2252,8 @@ async function handleRequest(req) {
|
|
|
2117
2252
|
sessionTrackingMap.delete(req.sessionId);
|
|
2118
2253
|
persistSessionRegistry();
|
|
2119
2254
|
}
|
|
2120
|
-
const { sessionDir: sessionDir2 } = await import("./paths-
|
|
2121
|
-
|
|
2255
|
+
const { sessionDir: sessionDir2 } = await import("./paths-IJXOAN4E.js");
|
|
2256
|
+
rmSync3(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
|
|
2122
2257
|
return { ok: true };
|
|
2123
2258
|
}
|
|
2124
2259
|
case "pane-exited": {
|
|
@@ -2149,8 +2284,8 @@ async function handleRequest(req) {
|
|
|
2149
2284
|
let filePath;
|
|
2150
2285
|
if (req.content.length > 200) {
|
|
2151
2286
|
const dir = messagesDir(tracking.cwd, req.sessionId);
|
|
2152
|
-
|
|
2153
|
-
filePath =
|
|
2287
|
+
mkdirSync3(dir, { recursive: true });
|
|
2288
|
+
filePath = join5(dir, `${id}.md`);
|
|
2154
2289
|
writeFileSync5(filePath, req.content, "utf-8");
|
|
2155
2290
|
}
|
|
2156
2291
|
await appendMessage(tracking.cwd, req.sessionId, {
|
|
@@ -2174,7 +2309,7 @@ async function handleRequest(req) {
|
|
|
2174
2309
|
function startServer() {
|
|
2175
2310
|
return new Promise((resolve5, reject) => {
|
|
2176
2311
|
const sock = socketPath();
|
|
2177
|
-
if (
|
|
2312
|
+
if (existsSync7(sock)) {
|
|
2178
2313
|
unlinkSync(sock);
|
|
2179
2314
|
}
|
|
2180
2315
|
server = createServer((conn) => {
|
|
@@ -2193,12 +2328,16 @@ function startServer() {
|
|
|
2193
2328
|
continue;
|
|
2194
2329
|
}
|
|
2195
2330
|
handleRequest(req).then((res) => {
|
|
2196
|
-
conn.
|
|
2331
|
+
if (!conn.destroyed) {
|
|
2332
|
+
conn.write(JSON.stringify(res) + "\n");
|
|
2333
|
+
}
|
|
2197
2334
|
});
|
|
2198
2335
|
}
|
|
2199
2336
|
});
|
|
2200
2337
|
conn.on("error", (err) => {
|
|
2201
|
-
|
|
2338
|
+
if (err.code !== "EPIPE") {
|
|
2339
|
+
console.error("[sisyphus] Connection error:", err.message);
|
|
2340
|
+
}
|
|
2202
2341
|
});
|
|
2203
2342
|
});
|
|
2204
2343
|
server.on("error", reject);
|
|
@@ -2216,7 +2355,7 @@ function stopServer() {
|
|
|
2216
2355
|
}
|
|
2217
2356
|
server.close(() => {
|
|
2218
2357
|
const sock = socketPath();
|
|
2219
|
-
if (
|
|
2358
|
+
if (existsSync7(sock)) {
|
|
2220
2359
|
unlinkSync(sock);
|
|
2221
2360
|
}
|
|
2222
2361
|
server = null;
|
|
@@ -2227,7 +2366,7 @@ function stopServer() {
|
|
|
2227
2366
|
|
|
2228
2367
|
// src/daemon/updater.ts
|
|
2229
2368
|
import { execSync as execSync3 } from "child_process";
|
|
2230
|
-
import { readFileSync as
|
|
2369
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, lstatSync } from "fs";
|
|
2231
2370
|
import { resolve as resolve4 } from "path";
|
|
2232
2371
|
import { get } from "https";
|
|
2233
2372
|
function isNewer(latest, current) {
|
|
@@ -2244,7 +2383,7 @@ function isNewer(latest, current) {
|
|
|
2244
2383
|
function readPackageVersion() {
|
|
2245
2384
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
2246
2385
|
try {
|
|
2247
|
-
const raw =
|
|
2386
|
+
const raw = readFileSync6(resolve4(import.meta.dirname, rel), "utf-8");
|
|
2248
2387
|
const pkg = JSON.parse(raw);
|
|
2249
2388
|
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
2250
2389
|
} catch {
|
|
@@ -2359,7 +2498,7 @@ var origError = console.error.bind(console);
|
|
|
2359
2498
|
console.log = (...args) => origLog(`[${ts()}]`, ...args);
|
|
2360
2499
|
console.error = (...args) => origError(`[${ts()}]`, ...args);
|
|
2361
2500
|
function ensureDirs() {
|
|
2362
|
-
|
|
2501
|
+
mkdirSync4(globalDir(), { recursive: true });
|
|
2363
2502
|
}
|
|
2364
2503
|
function isProcessAlive(pid) {
|
|
2365
2504
|
try {
|
|
@@ -2372,7 +2511,7 @@ function isProcessAlive(pid) {
|
|
|
2372
2511
|
function readPid() {
|
|
2373
2512
|
const pidFile = daemonPidPath();
|
|
2374
2513
|
try {
|
|
2375
|
-
const pid = parseInt(
|
|
2514
|
+
const pid = parseInt(readFileSync7(pidFile, "utf-8").trim(), 10);
|
|
2376
2515
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
2377
2516
|
} catch {
|
|
2378
2517
|
return null;
|
|
@@ -2443,11 +2582,11 @@ async function recoverSessions() {
|
|
|
2443
2582
|
let recovered = 0;
|
|
2444
2583
|
for (const [sessionId, cwd] of entries) {
|
|
2445
2584
|
const stateFile = statePath(cwd, sessionId);
|
|
2446
|
-
if (!
|
|
2585
|
+
if (!existsSync8(stateFile)) {
|
|
2447
2586
|
continue;
|
|
2448
2587
|
}
|
|
2449
2588
|
try {
|
|
2450
|
-
const session = JSON.parse(
|
|
2589
|
+
const session = JSON.parse(readFileSync7(stateFile, "utf-8"));
|
|
2451
2590
|
if (session.status === "active" || session.status === "paused") {
|
|
2452
2591
|
registerSessionCwd(sessionId, cwd);
|
|
2453
2592
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
@@ -2466,6 +2605,7 @@ async function recoverSessions() {
|
|
|
2466
2605
|
setWindowId(sessionId, session.tmuxWindowId);
|
|
2467
2606
|
trackSession(sessionId, cwd, session.tmuxSessionName);
|
|
2468
2607
|
updateTrackedWindow(sessionId, session.tmuxWindowId);
|
|
2608
|
+
initTimers(sessionId, session);
|
|
2469
2609
|
const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
|
|
2470
2610
|
if (lastIncompleteCycle?.paneId) {
|
|
2471
2611
|
setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
|
|
@@ -2490,6 +2630,7 @@ async function recoverSessions() {
|
|
|
2490
2630
|
const orchestratorPaneId = getOrchestratorPaneId(sessionId);
|
|
2491
2631
|
const orchestratorAlive = orchestratorPaneId && livePaneIds.has(orchestratorPaneId);
|
|
2492
2632
|
if (!orchestratorAlive) {
|
|
2633
|
+
await completeOrchestratorCycle(cwd, sessionId);
|
|
2493
2634
|
console.log(`[sisyphus] Detected stuck session ${sessionId} on recovery: triggering orchestrator respawn`);
|
|
2494
2635
|
await onAllAgentsDone2(sessionId, cwd, session.tmuxWindowId);
|
|
2495
2636
|
}
|
|
@@ -2525,6 +2666,12 @@ async function startDaemon() {
|
|
|
2525
2666
|
const shutdown = async () => {
|
|
2526
2667
|
console.log("[sisyphus] Shutting down...");
|
|
2527
2668
|
stopMonitor();
|
|
2669
|
+
for (const sessionId of getTrackedSessionIds()) {
|
|
2670
|
+
try {
|
|
2671
|
+
await flushTimers(sessionId);
|
|
2672
|
+
} catch {
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2528
2675
|
await stopServer();
|
|
2529
2676
|
releasePidLock();
|
|
2530
2677
|
process.exit(0);
|