sisyphi 1.0.14 → 1.1.7
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-Q6VQOUN3.js → chunk-M7LZ2ZHD.js} +3 -27
- package/dist/chunk-M7LZ2ZHD.js.map +1 -0
- package/dist/{chunk-YGBGKMTF.js → chunk-REUQ4B45.js} +7 -11
- package/dist/chunk-REUQ4B45.js.map +1 -0
- package/dist/{chunk-MMA43N67.js → chunk-Z32YVDMY.js} +2 -2
- package/dist/chunk-Z32YVDMY.js.map +1 -0
- package/dist/cli.js +46 -49
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +795 -796
- package/dist/daemon.js.map +1 -1
- package/dist/{paths-FYYSBD27.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 +2 -2
- 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 +69 -2
- 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 +167 -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 +193 -120
- package/dist/tui.js.map +1 -1
- package/package.json +2 -2
- 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 +2 -2
- 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 +69 -2
- 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 +167 -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-MMA43N67.js.map +0 -1
- package/dist/chunk-Q6VQOUN3.js.map +0 -1
- package/dist/chunk-YGBGKMTF.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-FYYSBD27.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,24 +30,22 @@ import {
|
|
|
30
30
|
snapshotsDir,
|
|
31
31
|
socketPath,
|
|
32
32
|
statePath,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
} from "./chunk-YGBGKMTF.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
|
|
50
|
-
import { join as join6 } from "path";
|
|
48
|
+
import { existsSync as existsSync6, readdirSync as readdirSync5, rmSync as rmSync2 } from "fs";
|
|
51
49
|
|
|
52
50
|
// src/daemon/state.ts
|
|
53
51
|
import { randomUUID } from "crypto";
|
|
@@ -92,6 +90,9 @@ function createSession(id, task, cwd, context, name) {
|
|
|
92
90
|
mkdirSync(logsDir(cwd, id), { recursive: true });
|
|
93
91
|
writeFileSync(goalPath(cwd, id), task, "utf-8");
|
|
94
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
|
+
}
|
|
95
96
|
const session = {
|
|
96
97
|
id,
|
|
97
98
|
...name ? { name } : {},
|
|
@@ -100,6 +101,7 @@ function createSession(id, task, cwd, context, name) {
|
|
|
100
101
|
cwd,
|
|
101
102
|
status: "active",
|
|
102
103
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
104
|
+
activeMs: 0,
|
|
103
105
|
agents: [],
|
|
104
106
|
orchestratorCycles: [],
|
|
105
107
|
messages: []
|
|
@@ -110,8 +112,13 @@ function createSession(id, task, cwd, context, name) {
|
|
|
110
112
|
function getSession(cwd, sessionId) {
|
|
111
113
|
const content = readFileSync(statePath(cwd, sessionId), "utf-8");
|
|
112
114
|
const session = JSON.parse(content);
|
|
115
|
+
if (session.activeMs == null) session.activeMs = 0;
|
|
113
116
|
for (const agent of session.agents) {
|
|
114
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;
|
|
115
122
|
}
|
|
116
123
|
return session;
|
|
117
124
|
}
|
|
@@ -246,15 +253,32 @@ async function updateTask(cwd, sessionId, task) {
|
|
|
246
253
|
writeFileSync(goalPath(cwd, sessionId), task, "utf-8");
|
|
247
254
|
});
|
|
248
255
|
}
|
|
249
|
-
async function completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode) {
|
|
256
|
+
async function completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode, activeMs) {
|
|
250
257
|
return withSessionLock(sessionId, () => {
|
|
251
258
|
const session = getSession(cwd, sessionId);
|
|
252
259
|
const cycles = session.orchestratorCycles;
|
|
253
260
|
if (cycles.length === 0) return;
|
|
254
261
|
const cycle = cycles[cycles.length - 1];
|
|
262
|
+
if (cycle.completedAt) return;
|
|
255
263
|
cycle.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
256
264
|
if (nextPrompt) cycle.nextPrompt = nextPrompt;
|
|
257
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
|
+
}
|
|
258
282
|
saveSession(session);
|
|
259
283
|
});
|
|
260
284
|
}
|
|
@@ -312,9 +336,10 @@ function deleteSnapshotsAfter(cwd, sessionId, afterCycle) {
|
|
|
312
336
|
}
|
|
313
337
|
|
|
314
338
|
// src/daemon/orchestrator.ts
|
|
315
|
-
import { existsSync as
|
|
316
|
-
import { execSync } from "child_process";
|
|
317
|
-
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";
|
|
318
343
|
|
|
319
344
|
// src/daemon/spawn-helpers.ts
|
|
320
345
|
import { writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
@@ -387,6 +412,8 @@ function parseAgentFrontmatter(content) {
|
|
|
387
412
|
fm.description = str("description");
|
|
388
413
|
fm.permissionMode = str("permissionMode");
|
|
389
414
|
fm.effort = str("effort");
|
|
415
|
+
const interactive = str("interactive");
|
|
416
|
+
if (interactive === "true") fm.interactive = true;
|
|
390
417
|
const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
391
418
|
if (skillsMatch) {
|
|
392
419
|
fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
|
|
@@ -569,15 +596,30 @@ function listPanes(windowTarget) {
|
|
|
569
596
|
function setPaneTitle(paneTarget, title) {
|
|
570
597
|
execSafe(`tmux select-pane -t "${paneTarget}" -T ${shellQuote(title)}`);
|
|
571
598
|
}
|
|
572
|
-
function setPaneStyle(paneTarget, color) {
|
|
599
|
+
function setPaneStyle(paneTarget, color, meta) {
|
|
573
600
|
const gitBranch = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null)`;
|
|
574
601
|
const branchSuffix = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null | grep -q . && echo ' |') ${gitBranch}`;
|
|
575
|
-
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("");
|
|
576
613
|
execSafe(`tmux set -p -t "${paneTarget}" pane-border-format ${shellQuote(fmt)}`);
|
|
577
614
|
execSafe(`tmux set -p -t "${paneTarget}" @pane_color "${color}"`);
|
|
578
615
|
execSafe(`tmux set -w -t "${paneTarget}" pane-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
|
|
579
616
|
execSafe(`tmux set -w -t "${paneTarget}" pane-active-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
|
|
580
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
|
+
}
|
|
581
623
|
function selectLayout(windowTarget, layout = "even-horizontal") {
|
|
582
624
|
execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
|
|
583
625
|
}
|
|
@@ -615,604 +657,164 @@ function unregisterSessionPanes(sessionId) {
|
|
|
615
657
|
function lookupPane(paneId) {
|
|
616
658
|
return paneMap.get(paneId);
|
|
617
659
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (existsSync4(join3(cwd, ".git"))) {
|
|
624
|
-
try {
|
|
625
|
-
repos.push(getRepoInfo(cwd, "."));
|
|
626
|
-
} catch {
|
|
660
|
+
function getSessionPanes(sessionId) {
|
|
661
|
+
const result = [];
|
|
662
|
+
for (const [paneId, entry] of paneMap) {
|
|
663
|
+
if (entry.sessionId === sessionId) {
|
|
664
|
+
result.push({ paneId, ...entry });
|
|
627
665
|
}
|
|
628
666
|
}
|
|
667
|
+
return result;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/daemon/agent.ts
|
|
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";
|
|
675
|
+
|
|
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;
|
|
629
682
|
try {
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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()
|
|
691
|
+
}
|
|
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;
|
|
639
698
|
}
|
|
640
699
|
}
|
|
641
700
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (!branchRaw) throw new Error(`Failed to detect git branch for repo: ${repoPath}`);
|
|
653
|
-
const status = execSafe(`git -C ${shellQuote(repoPath)} status --porcelain`);
|
|
654
|
-
const isDirty = !!(status && status.trim().length > 0);
|
|
655
|
-
return { name, path: repoPath, branch: branchRaw, isDirty };
|
|
656
|
-
}
|
|
657
|
-
var sessionWindowMap = /* @__PURE__ */ new Map();
|
|
658
|
-
var sessionOrchestratorPane = /* @__PURE__ */ new Map();
|
|
659
|
-
function getWindowId(sessionId) {
|
|
660
|
-
return sessionWindowMap.get(sessionId);
|
|
661
|
-
}
|
|
662
|
-
function setWindowId(sessionId, windowId) {
|
|
663
|
-
sessionWindowMap.set(sessionId, windowId);
|
|
664
|
-
}
|
|
665
|
-
function getOrchestratorPaneId(sessionId) {
|
|
666
|
-
return sessionOrchestratorPane.get(sessionId);
|
|
667
|
-
}
|
|
668
|
-
function setOrchestratorPaneId(sessionId, paneId) {
|
|
669
|
-
sessionOrchestratorPane.set(sessionId, paneId);
|
|
670
|
-
}
|
|
671
|
-
function loadOrchestratorPrompt(cwd, mode) {
|
|
672
|
-
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
673
|
-
if (existsSync4(projectPath)) {
|
|
674
|
-
return readFileSync3(projectPath, "utf-8");
|
|
675
|
-
}
|
|
676
|
-
const basePath = resolve2(import.meta.dirname, "../templates/orchestrator-base.md");
|
|
677
|
-
const base = readFileSync3(basePath, "utf-8");
|
|
678
|
-
if (mode === "implementation") {
|
|
679
|
-
const implPath = resolve2(import.meta.dirname, "../templates/orchestrator-impl.md");
|
|
680
|
-
return base + "\n\n" + readFileSync3(implPath, "utf-8");
|
|
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;
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
681
711
|
}
|
|
682
|
-
const planningPath = resolve2(import.meta.dirname, "../templates/orchestrator-planning.md");
|
|
683
|
-
return base + "\n\n" + readFileSync3(planningPath, "utf-8");
|
|
684
712
|
}
|
|
685
|
-
function
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
let contextSection = "";
|
|
691
|
-
if (cycleNum === 0) {
|
|
692
|
-
if (session.context) {
|
|
693
|
-
contextSection = `
|
|
694
|
-
## Context
|
|
695
|
-
|
|
696
|
-
${session.context}
|
|
697
|
-
`;
|
|
698
|
-
}
|
|
699
|
-
} else {
|
|
700
|
-
let ctxFiles = [];
|
|
701
|
-
if (existsSync4(ctxDir)) {
|
|
702
|
-
ctxFiles = readdirSync3(ctxDir).filter((f) => f !== "CLAUDE.md");
|
|
703
|
-
}
|
|
704
|
-
if (ctxFiles.length > 0) {
|
|
705
|
-
const ctxLines = ctxFiles.map((f) => `- ${join3(ctxDir, f)}`).join("\n");
|
|
706
|
-
contextSection = `
|
|
707
|
-
## Context
|
|
713
|
+
async function summarizeReport(reportText) {
|
|
714
|
+
if (Date.now() < disabledUntil) return null;
|
|
715
|
+
try {
|
|
716
|
+
const session = await query({
|
|
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.
|
|
708
718
|
|
|
709
|
-
${
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const sourceLabel = m.source.type === "agent" ? `agent:${m.source.agentId}` : m.source.type === "system" && m.source.detail ? `system:${m.source.detail}` : m.source.type;
|
|
716
|
-
const fileRef = m.filePath ? ` \u2192 ${m.filePath}` : "";
|
|
717
|
-
return `- [${sourceLabel} @ ${m.timestamp}] "${m.summary}"${fileRef}`;
|
|
718
|
-
}).join("\n") + "\n" : "";
|
|
719
|
-
let previousCyclesSection = "";
|
|
720
|
-
if (session.orchestratorCycles.length > 1) {
|
|
721
|
-
const previousCycles = session.orchestratorCycles.slice(0, -1);
|
|
722
|
-
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
723
|
-
const lines = previousCycles.map((c) => {
|
|
724
|
-
const agentDescs = c.agentsSpawned.map((id) => {
|
|
725
|
-
const agent = agentMap.get(id);
|
|
726
|
-
return agent ? `${id} (${agent.name})` : id;
|
|
727
|
-
}).join(", ");
|
|
728
|
-
return `Cycle ${c.cycle}: ${agentDescs || "(none)"}`;
|
|
719
|
+
${reportText.slice(0, 3e3)}`,
|
|
720
|
+
options: {
|
|
721
|
+
model: "haiku",
|
|
722
|
+
maxTurns: 1,
|
|
723
|
+
env: execEnv()
|
|
724
|
+
}
|
|
729
725
|
});
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
let mostRecentCycleSection = "";
|
|
737
|
-
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
738
|
-
if (lastCycle && lastCycle.agentsSpawned.length > 0) {
|
|
739
|
-
const agentMap = new Map(session.agents.map((a) => [a.id, a]));
|
|
740
|
-
const agentLines = lastCycle.agentsSpawned.map((id) => {
|
|
741
|
-
const agent = agentMap.get(id);
|
|
742
|
-
if (!agent) return `- **${id}**: unknown (no agent data)`;
|
|
743
|
-
const finalReport = agent.reports.find((r) => r.type === "final");
|
|
744
|
-
const reportToUse = finalReport ?? agent.reports[agent.reports.length - 1];
|
|
745
|
-
const reportRef = reportToUse ? `@${reportToUse.filePath}` : "(no reports)";
|
|
746
|
-
return `- **${id}** (${agent.name}) [${agent.status}]: ${reportRef}`;
|
|
747
|
-
}).join("\n");
|
|
748
|
-
mostRecentCycleSection = `
|
|
749
|
-
### Most Recent Cycle
|
|
750
|
-
|
|
751
|
-
${agentLines}
|
|
752
|
-
`;
|
|
753
|
-
}
|
|
754
|
-
const roadmapRef = existsSync4(roadmapFile) ? `@${roadmapFile}` : "(empty)";
|
|
755
|
-
const repos = detectRepos(session.cwd);
|
|
756
|
-
let repositoriesSection = "\n\n## Repositories\n";
|
|
757
|
-
if (repos.length === 0) {
|
|
758
|
-
repositoriesSection += "\nNo git repositories detected.\n";
|
|
759
|
-
} else {
|
|
760
|
-
for (const repo of repos) {
|
|
761
|
-
const dirtyTag = repo.isDirty ? " (dirty)" : "";
|
|
762
|
-
repositoriesSection += `
|
|
763
|
-
### ${repo.name === "." ? "Session Root (.)" : repo.name}
|
|
764
|
-
`;
|
|
765
|
-
repositoriesSection += `Branch: \`${repo.branch}\`${dirtyTag}
|
|
766
|
-
`;
|
|
767
|
-
const repoAgents = session.agents.filter((a) => a.repo === repo.name);
|
|
768
|
-
if (repoAgents.length > 0) {
|
|
769
|
-
repositoriesSection += "\nAgents:\n";
|
|
770
|
-
for (const a of repoAgents) {
|
|
771
|
-
repositoriesSection += `- ${a.id} (${a.name}) [${a.status}]
|
|
772
|
-
`;
|
|
726
|
+
let text = "";
|
|
727
|
+
for await (const msg of session) {
|
|
728
|
+
if (msg.type === "assistant" && msg.message?.content) {
|
|
729
|
+
for (const block of msg.message.content) {
|
|
730
|
+
if (block.type === "text") text += block.text;
|
|
773
731
|
}
|
|
774
732
|
}
|
|
775
733
|
}
|
|
776
|
-
|
|
777
|
-
|
|
734
|
+
const summary = text.trim();
|
|
735
|
+
return summary.length > 0 ? summary : null;
|
|
736
|
+
} catch (err) {
|
|
737
|
+
console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
|
|
738
|
+
const status = err.status;
|
|
739
|
+
if (status === 401 || status === 403) {
|
|
740
|
+
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
778
741
|
}
|
|
742
|
+
return null;
|
|
779
743
|
}
|
|
780
|
-
|
|
781
|
-
const goalContent = existsSync4(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
|
|
782
|
-
return `## Goal
|
|
783
|
-
|
|
784
|
-
${goalContent}
|
|
785
|
-
${contextSection}${messagesSection}
|
|
786
|
-
### Cycle Log
|
|
787
|
-
|
|
788
|
-
Write your cycle summary to: ${logFile}
|
|
789
|
-
${previousCyclesSection}${mostRecentCycleSection}
|
|
790
|
-
## Roadmap
|
|
744
|
+
}
|
|
791
745
|
|
|
792
|
-
|
|
793
|
-
|
|
746
|
+
// src/daemon/agent.ts
|
|
747
|
+
var agentCounters = /* @__PURE__ */ new Map();
|
|
748
|
+
function resetAgentCounterFromState(sessionId, agents) {
|
|
749
|
+
let max = 0;
|
|
750
|
+
for (const a of agents) {
|
|
751
|
+
const match = a.id.match(/^agent-(\d+)$/);
|
|
752
|
+
if (match) max = Math.max(max, parseInt(match[1], 10));
|
|
753
|
+
}
|
|
754
|
+
agentCounters.set(sessionId, max);
|
|
794
755
|
}
|
|
795
|
-
|
|
756
|
+
function clearAgentCounter(sessionId) {
|
|
757
|
+
agentCounters.delete(sessionId);
|
|
758
|
+
}
|
|
759
|
+
function renderAgentSuffix(sessionId, instruction) {
|
|
760
|
+
const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
|
|
761
|
+
let template;
|
|
796
762
|
try {
|
|
797
|
-
|
|
763
|
+
template = readFileSync3(templatePath, "utf-8");
|
|
798
764
|
} catch {
|
|
799
|
-
|
|
765
|
+
template = `# Sisyphus Agent
|
|
766
|
+
Session: {{SESSION_ID}}
|
|
767
|
+
Task: {{INSTRUCTION}}`;
|
|
800
768
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
{
|
|
769
|
+
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, "");
|
|
770
|
+
}
|
|
771
|
+
function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
|
|
772
|
+
const base = `${promptsDir(cwd, sessionId)}/${agentId}-plugin`;
|
|
773
|
+
mkdirSync2(`${base}/.claude-plugin`, { recursive: true });
|
|
774
|
+
mkdirSync2(`${base}/agents`, { recursive: true });
|
|
775
|
+
mkdirSync2(`${base}/hooks`, { recursive: true });
|
|
776
|
+
writeFileSync3(
|
|
777
|
+
`${base}/.claude-plugin/plugin.json`,
|
|
778
|
+
JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
|
|
779
|
+
"utf-8"
|
|
810
780
|
);
|
|
811
|
-
const agentTypeLines = agentTypes.length > 0 ? agentTypes.map((t) => {
|
|
812
|
-
const modelTag = t.model ? ` (${t.model})` : "";
|
|
813
|
-
const desc = t.description ? ` \u2014 ${t.description}` : "";
|
|
814
|
-
return `- \`${t.qualifiedName}\`${modelTag}${desc}`;
|
|
815
|
-
}).join("\n") : " (none)";
|
|
816
|
-
const systemPrompt = basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines);
|
|
817
|
-
const cycleNum = session.orchestratorCycles.length + 1;
|
|
818
|
-
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
819
|
-
writeFileSync3(promptFilePath, systemPrompt, "utf-8");
|
|
820
|
-
sessionWindowMap.set(sessionId, windowId);
|
|
821
|
-
const npmBinDir = resolveNpmBinDir();
|
|
822
781
|
const sesDir = sessionDir(cwd, sessionId);
|
|
823
|
-
const
|
|
824
|
-
`export SISYPHUS_SESSION_ID='${sessionId}'`,
|
|
825
|
-
`export SISYPHUS_AGENT_ID='orchestrator'`,
|
|
826
|
-
`export SISYPHUS_CWD='${cwd}'`,
|
|
827
|
-
`export SISYPHUS_SESSION_DIR='${sesDir}'`,
|
|
828
|
-
`export PATH="${npmBinDir}:$PATH"`
|
|
829
|
-
]);
|
|
830
|
-
let userPrompt = formattedState;
|
|
831
|
-
if (message) {
|
|
832
|
-
userPrompt += `
|
|
833
|
-
|
|
834
|
-
## Continuation Instructions
|
|
835
|
-
|
|
836
|
-
The user resumed this session with new instructions: ${message}`;
|
|
837
|
-
} else {
|
|
838
|
-
const storedPrompt = lastCycle?.nextPrompt;
|
|
839
|
-
const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
|
|
840
|
-
userPrompt += `
|
|
841
|
-
|
|
842
|
-
## Continuation Instructions
|
|
843
|
-
|
|
844
|
-
${continuationText}`;
|
|
845
|
-
}
|
|
846
|
-
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
847
|
-
writeFileSync3(userPromptFilePath, userPrompt, "utf-8");
|
|
848
|
-
if (session.messages && session.messages.length > 0) {
|
|
849
|
-
await drainMessages(cwd, sessionId, session.messages.length);
|
|
850
|
-
}
|
|
851
|
-
const pluginPath = resolve2(import.meta.dirname, "../templates/orchestrator-plugin");
|
|
852
|
-
const settingsPath = resolve2(import.meta.dirname, "../templates/orchestrator-settings.json");
|
|
853
|
-
const config = loadConfig(cwd);
|
|
854
|
-
const effort = config.orchestratorEffort ?? "high";
|
|
855
|
-
const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --settings "${settingsPath}" --plugin-dir "${pluginPath}" --name "sisyphus:orch-${session.name ?? sessionId.slice(0, 8)}-cycle-${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
856
|
-
const paneId = createPane(windowId, cwd, "left");
|
|
857
|
-
sessionOrchestratorPane.set(sessionId, paneId);
|
|
858
|
-
registerPane(paneId, sessionId, "orchestrator");
|
|
859
|
-
setPaneTitle(paneId, session.name ? `${session.name} (orch)` : "Sisyphus");
|
|
860
|
-
setPaneStyle(paneId, ORCHESTRATOR_COLOR);
|
|
861
|
-
const bannerCmd = resolveBannerCmd();
|
|
862
|
-
const notifyCmd = buildNotifyCmd(paneId);
|
|
863
|
-
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `orchestrator-run-${cycleNum}`, [
|
|
864
|
-
"#!/usr/bin/env bash",
|
|
865
|
-
...bannerCmd ? [bannerCmd] : [],
|
|
866
|
-
envExports,
|
|
867
|
-
claudeCmd,
|
|
868
|
-
notifyCmd
|
|
869
|
-
]);
|
|
870
|
-
sendKeys(paneId, `bash '${scriptPath}'`);
|
|
871
|
-
await addOrchestratorCycle(cwd, sessionId, {
|
|
872
|
-
cycle: cycleNum,
|
|
873
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
874
|
-
agentsSpawned: [],
|
|
875
|
-
paneId
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
function resolveOrchestratorPane(sessionId, cwd) {
|
|
879
|
-
const memPane = sessionOrchestratorPane.get(sessionId);
|
|
880
|
-
if (memPane) return memPane;
|
|
881
|
-
const session = getSession(cwd, sessionId);
|
|
882
|
-
const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
|
|
883
|
-
return lastCycle?.paneId ?? void 0;
|
|
884
|
-
}
|
|
885
|
-
async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
|
|
886
|
-
const paneId = resolveOrchestratorPane(sessionId, cwd);
|
|
887
|
-
if (paneId) {
|
|
888
|
-
killPane(paneId);
|
|
889
|
-
unregisterPane(paneId);
|
|
890
|
-
sessionOrchestratorPane.delete(sessionId);
|
|
891
|
-
}
|
|
892
|
-
const windowId = sessionWindowMap.get(sessionId);
|
|
893
|
-
if (windowId) selectLayout(windowId);
|
|
894
|
-
await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode);
|
|
895
|
-
const session = getSession(cwd, sessionId);
|
|
896
|
-
const runningAgents = session.agents.filter((a) => a.status === "running");
|
|
897
|
-
if (runningAgents.length === 0) {
|
|
898
|
-
console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
async function handleOrchestratorComplete(sessionId, cwd, report) {
|
|
902
|
-
await completeOrchestratorCycle(cwd, sessionId);
|
|
903
|
-
await completeSession(cwd, sessionId, report);
|
|
904
|
-
console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
|
|
905
|
-
}
|
|
906
|
-
function cleanupSessionMaps(sessionId) {
|
|
907
|
-
sessionOrchestratorPane.delete(sessionId);
|
|
908
|
-
sessionWindowMap.delete(sessionId);
|
|
909
|
-
unregisterSessionPanes(sessionId);
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
// src/daemon/agent.ts
|
|
913
|
-
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5, existsSync as existsSync6 } from "fs";
|
|
914
|
-
import { execSync as execSync2 } from "child_process";
|
|
915
|
-
import { resolve as resolve3, dirname as dirname3, join as join5 } from "path";
|
|
916
|
-
|
|
917
|
-
// src/daemon/worktree.ts
|
|
918
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
|
|
919
|
-
import { dirname as dirname2, join as join4 } from "path";
|
|
920
|
-
function loadWorktreeConfig(cwd) {
|
|
921
|
-
try {
|
|
922
|
-
const content = readFileSync4(worktreeConfigPath(cwd), "utf-8");
|
|
923
|
-
const parsed = JSON.parse(content);
|
|
924
|
-
const flatKeys = ["copy", "clone", "symlink", "init"];
|
|
925
|
-
if (Object.keys(parsed).some((k) => flatKeys.includes(k))) {
|
|
926
|
-
throw new Error(
|
|
927
|
-
'Flat worktree.json format is no longer supported. Migrate to keyed format: wrap your config under a "." key.\nExample: { ".": { "symlink": ["node_modules"], "init": "npm install" } }'
|
|
928
|
-
);
|
|
929
|
-
}
|
|
930
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
931
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
932
|
-
throw new Error(
|
|
933
|
-
`Invalid worktree.json: value for "${key}" must be an object with optional keys: copy, clone, symlink, init`
|
|
934
|
-
);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
return parsed;
|
|
938
|
-
} catch (err) {
|
|
939
|
-
if (err instanceof SyntaxError) return null;
|
|
940
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") return null;
|
|
941
|
-
throw err;
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
function createWorktreeShell(repoRoot, sessionId, agentId) {
|
|
945
|
-
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
946
|
-
const worktreePath = join4(worktreeBaseDir(repoRoot), sessionId.slice(0, 8), agentId);
|
|
947
|
-
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
948
|
-
execSafe(`git -C ${shellQuote(repoRoot)} worktree prune`);
|
|
949
|
-
if (existsSync5(worktreePath)) {
|
|
950
|
-
execSafe(`git -C ${shellQuote(repoRoot)} worktree remove --force ${shellQuote(worktreePath)}`);
|
|
951
|
-
}
|
|
952
|
-
execSafe(`git -C ${shellQuote(repoRoot)} branch -D ${shellQuote(branchName)}`);
|
|
953
|
-
exec(`git -C ${shellQuote(repoRoot)} branch ${shellQuote(branchName)} HEAD`);
|
|
954
|
-
exec(`git -C ${shellQuote(repoRoot)} worktree add ${shellQuote(worktreePath)} ${shellQuote(branchName)}`);
|
|
955
|
-
return { worktreePath, branchName };
|
|
956
|
-
}
|
|
957
|
-
function bootstrapWorktree(repoRoot, worktreePath, config) {
|
|
958
|
-
if (config.copy) {
|
|
959
|
-
for (const entry of config.copy) {
|
|
960
|
-
const dest = join4(worktreePath, entry);
|
|
961
|
-
mkdirSync2(dirname2(dest), { recursive: true });
|
|
962
|
-
execSafe(`cp -r ${shellQuote(join4(repoRoot, entry))} ${shellQuote(dest)}`);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
if (config.clone) {
|
|
966
|
-
for (const entry of config.clone) {
|
|
967
|
-
const dest = join4(worktreePath, entry);
|
|
968
|
-
mkdirSync2(dirname2(dest), { recursive: true });
|
|
969
|
-
const src = shellQuote(join4(repoRoot, entry));
|
|
970
|
-
const dstQ = shellQuote(dest);
|
|
971
|
-
if (execSafe(`cp -Rc ${src} ${dstQ}`) === null) {
|
|
972
|
-
execSafe(`cp -r ${src} ${dstQ}`);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (config.symlink) {
|
|
977
|
-
for (const entry of config.symlink) {
|
|
978
|
-
const dest = join4(worktreePath, entry);
|
|
979
|
-
mkdirSync2(dirname2(dest), { recursive: true });
|
|
980
|
-
execSafe(`ln -s ${shellQuote(join4(repoRoot, entry))} ${shellQuote(dest)}`);
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
if (config.init) {
|
|
984
|
-
try {
|
|
985
|
-
exec(config.init, worktreePath);
|
|
986
|
-
} catch (err) {
|
|
987
|
-
console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
function resolveWorktreeBranch(repoRoot, worktreePath) {
|
|
992
|
-
const output = execSafe(`git -C ${shellQuote(repoRoot)} worktree list --porcelain`);
|
|
993
|
-
if (!output) return null;
|
|
994
|
-
const lines = output.split("\n");
|
|
995
|
-
for (let i = 0; i < lines.length; i++) {
|
|
996
|
-
if (lines[i] === `worktree ${worktreePath}`) {
|
|
997
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
998
|
-
const line = lines[j];
|
|
999
|
-
if (line === "") break;
|
|
1000
|
-
if (line.startsWith("branch refs/heads/")) {
|
|
1001
|
-
return line.slice("branch refs/heads/".length);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
break;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
return null;
|
|
1008
|
-
}
|
|
1009
|
-
function mergeWorktrees(cwd, agents) {
|
|
1010
|
-
const pending = agents.filter(
|
|
1011
|
-
(a) => a.worktreePath && a.mergeStatus === "pending"
|
|
1012
|
-
);
|
|
1013
|
-
const results = [];
|
|
1014
|
-
const byRepo = /* @__PURE__ */ new Map();
|
|
1015
|
-
for (const agent of pending) {
|
|
1016
|
-
const repo = agent.repo;
|
|
1017
|
-
if (!byRepo.has(repo)) byRepo.set(repo, []);
|
|
1018
|
-
byRepo.get(repo).push(agent);
|
|
1019
|
-
}
|
|
1020
|
-
if (existsSync5(join4(cwd, ".git"))) {
|
|
1021
|
-
execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
|
|
1022
|
-
execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
|
|
1023
|
-
} else {
|
|
1024
|
-
console.log("[sisyphus] Skipping .sisyphus snapshot \u2014 session root is not a git repo");
|
|
1025
|
-
}
|
|
1026
|
-
for (const [repo, repoAgents] of byRepo) {
|
|
1027
|
-
const repoRoot = repo === "." ? cwd : join4(cwd, repo);
|
|
1028
|
-
if (repo !== ".") {
|
|
1029
|
-
execSafe(`git -C ${shellQuote(repoRoot)} add -A`);
|
|
1030
|
-
execSafe(`git -C ${shellQuote(repoRoot)} commit -m 'sisyphus: snapshot before merge'`);
|
|
1031
|
-
}
|
|
1032
|
-
for (const agent of repoAgents) {
|
|
1033
|
-
const branch = resolveWorktreeBranch(repoRoot, agent.worktreePath);
|
|
1034
|
-
if (!branch) {
|
|
1035
|
-
results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
|
|
1036
|
-
execSafe(`git -C ${shellQuote(repoRoot)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
|
|
1037
|
-
continue;
|
|
1038
|
-
}
|
|
1039
|
-
const aheadLog = execSafe(`git -C ${shellQuote(repoRoot)} log HEAD..${shellQuote(branch)} --oneline`);
|
|
1040
|
-
if (!aheadLog) {
|
|
1041
|
-
results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
|
|
1042
|
-
cleanupWorktree(repoRoot, agent.worktreePath, branch);
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
|
|
1046
|
-
const mergeCmd = `git -C ${shellQuote(repoRoot)} merge --no-ff ${shellQuote(branch)} -m ${shellQuote(mergeMsg)}`;
|
|
1047
|
-
try {
|
|
1048
|
-
exec(mergeCmd);
|
|
1049
|
-
execSafe(`git -C ${shellQuote(repoRoot)} worktree remove ${shellQuote(agent.worktreePath)}`);
|
|
1050
|
-
execSafe(`git -C ${shellQuote(repoRoot)} branch -d ${shellQuote(branch)}`);
|
|
1051
|
-
results.push({ agentId: agent.id, name: agent.name, status: "merged" });
|
|
1052
|
-
} catch (err) {
|
|
1053
|
-
execSafe(`git -C ${shellQuote(repoRoot)} merge --abort`);
|
|
1054
|
-
const errObj = err;
|
|
1055
|
-
const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
|
|
1056
|
-
const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
|
|
1057
|
-
const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
|
|
1058
|
-
results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
return results;
|
|
1063
|
-
}
|
|
1064
|
-
function cleanupWorktree(repoRoot, worktreePath, branchName) {
|
|
1065
|
-
execSafe(`git -C ${shellQuote(repoRoot)} worktree remove ${shellQuote(worktreePath)} --force`);
|
|
1066
|
-
execSafe(`git -C ${shellQuote(repoRoot)} branch -D ${shellQuote(branchName)}`);
|
|
1067
|
-
const baseDir = dirname2(worktreePath);
|
|
1068
|
-
try {
|
|
1069
|
-
const entries = readdirSync4(baseDir);
|
|
1070
|
-
if (entries.length === 0) {
|
|
1071
|
-
rmSync2(baseDir, { recursive: true });
|
|
1072
|
-
}
|
|
1073
|
-
} catch {
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
function countWorktreeAgents(agents) {
|
|
1077
|
-
return agents.filter((a) => a.worktreePath && a.status === "running").length;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// src/daemon/summarize.ts
|
|
1081
|
-
import { query } from "@r-cli/sdk";
|
|
1082
|
-
var disabled = false;
|
|
1083
|
-
async function generateSessionName(task) {
|
|
1084
|
-
if (disabled) return null;
|
|
1085
|
-
try {
|
|
1086
|
-
const session = await query({
|
|
1087
|
-
prompt: `Generate a 2-4 word kebab-case name for this task. Output ONLY the name.
|
|
1088
|
-
|
|
1089
|
-
${task.slice(0, 500)}`,
|
|
1090
|
-
options: {
|
|
1091
|
-
model: "haiku",
|
|
1092
|
-
maxTurns: 1
|
|
1093
|
-
}
|
|
1094
|
-
});
|
|
1095
|
-
let text = "";
|
|
1096
|
-
for await (const msg of session) {
|
|
1097
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
1098
|
-
for (const block of msg.message.content) {
|
|
1099
|
-
if (block.type === "text") text += block.text;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
const name = text.trim().toLowerCase();
|
|
1104
|
-
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) return null;
|
|
1105
|
-
return name.slice(0, 30);
|
|
1106
|
-
} catch (err) {
|
|
1107
|
-
console.error(`[sisyphus] Haiku name generation failed: ${err instanceof Error ? err.message : err}`);
|
|
1108
|
-
const status = err.status;
|
|
1109
|
-
if (status === 401 || status === 403) {
|
|
1110
|
-
disabled = true;
|
|
1111
|
-
}
|
|
1112
|
-
return null;
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
async function summarizeReport(reportText) {
|
|
1116
|
-
if (disabled) return null;
|
|
1117
|
-
try {
|
|
1118
|
-
const session = await query({
|
|
1119
|
-
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.
|
|
1120
|
-
|
|
1121
|
-
${reportText.slice(0, 3e3)}`,
|
|
1122
|
-
options: {
|
|
1123
|
-
model: "haiku",
|
|
1124
|
-
maxTurns: 1
|
|
1125
|
-
}
|
|
1126
|
-
});
|
|
1127
|
-
let text = "";
|
|
1128
|
-
for await (const msg of session) {
|
|
1129
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
1130
|
-
for (const block of msg.message.content) {
|
|
1131
|
-
if (block.type === "text") text += block.text;
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
const summary = text.trim();
|
|
1136
|
-
return summary.length > 0 ? summary : null;
|
|
1137
|
-
} catch (err) {
|
|
1138
|
-
console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
|
|
1139
|
-
const status = err.status;
|
|
1140
|
-
if (status === 401 || status === 403) {
|
|
1141
|
-
disabled = true;
|
|
1142
|
-
}
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
// src/daemon/agent.ts
|
|
1148
|
-
var agentCounters = /* @__PURE__ */ new Map();
|
|
1149
|
-
function resetAgentCounterFromState(sessionId, agents) {
|
|
1150
|
-
let max = 0;
|
|
1151
|
-
for (const a of agents) {
|
|
1152
|
-
const match = a.id.match(/^agent-(\d+)$/);
|
|
1153
|
-
if (match) max = Math.max(max, parseInt(match[1], 10));
|
|
1154
|
-
}
|
|
1155
|
-
agentCounters.set(sessionId, max);
|
|
1156
|
-
}
|
|
1157
|
-
function clearAgentCounter(sessionId) {
|
|
1158
|
-
agentCounters.delete(sessionId);
|
|
1159
|
-
}
|
|
1160
|
-
function renderAgentSuffix(sessionId, instruction, worktreeContext) {
|
|
1161
|
-
const templatePath = resolve3(import.meta.dirname, "../templates/agent-suffix.md");
|
|
1162
|
-
let template;
|
|
1163
|
-
try {
|
|
1164
|
-
template = readFileSync5(templatePath, "utf-8");
|
|
1165
|
-
} catch {
|
|
1166
|
-
template = `# Sisyphus Agent
|
|
1167
|
-
Session: {{SESSION_ID}}
|
|
1168
|
-
Task: {{INSTRUCTION}}`;
|
|
1169
|
-
}
|
|
1170
|
-
const worktreeBlock = "";
|
|
1171
|
-
return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
|
|
1172
|
-
}
|
|
1173
|
-
function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
|
|
1174
|
-
const base = `${promptsDir(cwd, sessionId)}/${agentId}-plugin`;
|
|
1175
|
-
mkdirSync3(`${base}/.claude-plugin`, { recursive: true });
|
|
1176
|
-
mkdirSync3(`${base}/agents`, { recursive: true });
|
|
1177
|
-
mkdirSync3(`${base}/hooks`, { recursive: true });
|
|
1178
|
-
writeFileSync4(
|
|
1179
|
-
`${base}/.claude-plugin/plugin.json`,
|
|
1180
|
-
JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
|
|
1181
|
-
"utf-8"
|
|
1182
|
-
);
|
|
782
|
+
const substituteEnvVars = (text) => text.replace(/\$SISYPHUS_SESSION_DIR/g, sesDir).replace(/\$SISYPHUS_SESSION_ID/g, sessionId);
|
|
1183
783
|
if (agentConfig?.filePath && agentType && agentType !== "worker") {
|
|
1184
784
|
const shortName = agentType.replace(/^sisyphus:/, "");
|
|
1185
|
-
|
|
1186
|
-
const subAgentDir =
|
|
1187
|
-
if (
|
|
1188
|
-
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)) {
|
|
1189
789
|
if (f.endsWith(".md") && f !== "CLAUDE.md") {
|
|
1190
|
-
|
|
790
|
+
writeFileSync3(`${base}/agents/${f}`, substituteEnvVars(readFileSync3(join3(subAgentDir, f), "utf-8")), "utf-8");
|
|
1191
791
|
}
|
|
1192
792
|
}
|
|
1193
793
|
}
|
|
1194
794
|
}
|
|
1195
|
-
const srcHooks =
|
|
795
|
+
const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
|
|
1196
796
|
for (const f of ["require-submit.sh", "intercept-send-message.sh"]) {
|
|
1197
797
|
copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
|
|
1198
798
|
}
|
|
1199
799
|
const hooksConfig = {
|
|
1200
800
|
PreToolUse: [
|
|
1201
801
|
{ matcher: "SendMessage", hooks: [{ type: "command", command: "bash ${CLAUDE_PLUGIN_ROOT}/hooks/intercept-send-message.sh" }] }
|
|
1202
|
-
],
|
|
1203
|
-
Stop: [
|
|
1204
|
-
{ hooks: [{ type: "command", command: "bash ${CLAUDE_PLUGIN_ROOT}/hooks/require-submit.sh" }] }
|
|
1205
802
|
]
|
|
1206
803
|
};
|
|
804
|
+
if (!agentConfig?.frontmatter.interactive) {
|
|
805
|
+
hooksConfig.Stop = [
|
|
806
|
+
{ hooks: [{ type: "command", command: "bash ${CLAUDE_PLUGIN_ROOT}/hooks/require-submit.sh" }] }
|
|
807
|
+
];
|
|
808
|
+
}
|
|
1207
809
|
const normalizedType = agentType?.replace(/^sisyphus:/, "") ?? "";
|
|
1208
810
|
const userPromptHooks = {
|
|
1209
811
|
"plan": "plan-user-prompt.sh",
|
|
1210
|
-
"spec-draft": "spec-user-prompt.sh",
|
|
1211
812
|
"review": "review-user-prompt.sh",
|
|
1212
813
|
"review-plan": "review-plan-user-prompt.sh",
|
|
1213
814
|
"debug": "debug-user-prompt.sh",
|
|
1214
815
|
"operator": "operator-user-prompt.sh",
|
|
1215
|
-
"test-spec": "test-spec-user-prompt.sh"
|
|
816
|
+
"test-spec": "test-spec-user-prompt.sh",
|
|
817
|
+
"explore": "explore-user-prompt.sh"
|
|
1216
818
|
};
|
|
1217
819
|
const hookScript = userPromptHooks[normalizedType];
|
|
1218
820
|
if (hookScript) {
|
|
@@ -1221,20 +823,22 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
|
|
|
1221
823
|
];
|
|
1222
824
|
copyFileSync2(`${srcHooks}/${hookScript}`, `${base}/hooks/${hookScript}`);
|
|
1223
825
|
}
|
|
1224
|
-
|
|
826
|
+
writeFileSync3(`${base}/hooks/hooks.json`, JSON.stringify({ hooks: hooksConfig }, null, 2), "utf-8");
|
|
1225
827
|
return base;
|
|
1226
828
|
}
|
|
1227
829
|
function setupAgentPane(opts) {
|
|
1228
|
-
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;
|
|
1229
831
|
const paneId = createPane(windowId, paneCwd);
|
|
1230
832
|
registerPane(paneId, sessionId, "agent", agentId);
|
|
1231
833
|
const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
|
|
1232
834
|
const paneLabel = shortType ? `${name}-${shortType}` : name;
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
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);
|
|
1236
840
|
const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
|
|
1237
|
-
|
|
841
|
+
writeFileSync3(suffixFilePath, suffix, "utf-8");
|
|
1238
842
|
const bannerCmd = resolveBannerCmd();
|
|
1239
843
|
const npmBinDir = resolveNpmBinDir();
|
|
1240
844
|
const sesDir = sessionDir(cwd, sessionId);
|
|
@@ -1243,7 +847,6 @@ function setupAgentPane(opts) {
|
|
|
1243
847
|
`export SISYPHUS_AGENT_ID='${agentId}'`,
|
|
1244
848
|
`export SISYPHUS_CWD='${cwd}'`,
|
|
1245
849
|
`export SISYPHUS_SESSION_DIR='${sesDir}'`,
|
|
1246
|
-
...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
|
|
1247
850
|
`export PATH="${npmBinDir}:$PATH"`
|
|
1248
851
|
]);
|
|
1249
852
|
const notifyCmd = buildNotifyCmd(paneId);
|
|
@@ -1256,7 +859,7 @@ function setupAgentPane(opts) {
|
|
|
1256
859
|
parts.push(`## Task
|
|
1257
860
|
|
|
1258
861
|
${instruction}`);
|
|
1259
|
-
|
|
862
|
+
writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
|
|
1260
863
|
const model = agentConfig?.frontmatter.model ?? "codex-mini";
|
|
1261
864
|
mainCmd = `codex -m ${shellQuote(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
|
|
1262
865
|
} else {
|
|
@@ -1264,7 +867,8 @@ ${instruction}`);
|
|
|
1264
867
|
const config = loadConfig(cwd);
|
|
1265
868
|
const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
|
|
1266
869
|
const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
|
|
1267
|
-
|
|
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)}`;
|
|
1268
872
|
}
|
|
1269
873
|
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
|
|
1270
874
|
"#!/usr/bin/env bash",
|
|
@@ -1281,33 +885,24 @@ async function spawnAgent(opts) {
|
|
|
1281
885
|
const count = (agentCounters.get(sessionId) ?? 0) + 1;
|
|
1282
886
|
agentCounters.set(sessionId, count);
|
|
1283
887
|
const agentId = `agent-${String(count).padStart(3, "0")}`;
|
|
1284
|
-
const bundledPluginPath =
|
|
888
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1285
889
|
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
1286
890
|
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
1287
891
|
const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
|
|
1288
892
|
const cliToCheck = provider === "openai" ? "codex" : "claude";
|
|
1289
893
|
try {
|
|
1290
|
-
|
|
894
|
+
execSync(`which ${cliToCheck}`, { stdio: "pipe", env: execEnv() });
|
|
1291
895
|
} catch {
|
|
1292
896
|
throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sisyphus doctor\` to diagnose.`);
|
|
1293
897
|
}
|
|
1294
898
|
const repo = opts.repo !== void 0 ? opts.repo : ".";
|
|
1295
|
-
const repoRoot = repo === "." ? cwd :
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
let branchName;
|
|
1299
|
-
let worktreeContext;
|
|
1300
|
-
if (opts.worktree) {
|
|
1301
|
-
const wt = createWorktreeShell(repoRoot, sessionId, agentId);
|
|
1302
|
-
worktreePath = wt.worktreePath;
|
|
1303
|
-
branchName = wt.branchName;
|
|
1304
|
-
paneCwd = worktreePath;
|
|
1305
|
-
const session = getSession(cwd, sessionId);
|
|
1306
|
-
const portOffset = countWorktreeAgents(session.agents) + 1;
|
|
1307
|
-
worktreeContext = { offset: portOffset, total: portOffset, branchName };
|
|
1308
|
-
}
|
|
899
|
+
const repoRoot = repo === "." ? cwd : join3(cwd, repo);
|
|
900
|
+
const paneCwd = repoRoot;
|
|
901
|
+
const claudeSessionId = provider !== "openai" ? randomUUID2() : void 0;
|
|
1309
902
|
const { paneId, fullCmd } = setupAgentPane({
|
|
1310
903
|
sessionId,
|
|
904
|
+
sessionName: opts.sessionName,
|
|
905
|
+
cycleNum: opts.cycleNum,
|
|
1311
906
|
cwd,
|
|
1312
907
|
agentId,
|
|
1313
908
|
agentType,
|
|
@@ -1317,44 +912,27 @@ async function spawnAgent(opts) {
|
|
|
1317
912
|
color,
|
|
1318
913
|
provider,
|
|
1319
914
|
agentConfig,
|
|
1320
|
-
|
|
1321
|
-
|
|
915
|
+
paneCwd,
|
|
916
|
+
claudeSessionId
|
|
1322
917
|
});
|
|
1323
918
|
const agent = {
|
|
1324
919
|
id: agentId,
|
|
1325
920
|
name,
|
|
1326
921
|
agentType,
|
|
1327
922
|
provider,
|
|
923
|
+
claudeSessionId,
|
|
1328
924
|
color,
|
|
1329
925
|
instruction,
|
|
1330
926
|
status: "running",
|
|
1331
927
|
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1332
928
|
completedAt: null,
|
|
929
|
+
activeMs: 0,
|
|
1333
930
|
reports: [],
|
|
1334
931
|
paneId,
|
|
1335
|
-
repo
|
|
1336
|
-
...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
|
|
932
|
+
repo
|
|
1337
933
|
};
|
|
1338
934
|
await addAgent(cwd, sessionId, agent);
|
|
1339
|
-
|
|
1340
|
-
const config = loadWorktreeConfig(cwd);
|
|
1341
|
-
const repoConfig = config ? config[repo] : void 0;
|
|
1342
|
-
if (repoConfig) {
|
|
1343
|
-
const wtPath = worktreePath;
|
|
1344
|
-
setImmediate(() => {
|
|
1345
|
-
try {
|
|
1346
|
-
bootstrapWorktree(repoRoot, wtPath, repoConfig);
|
|
1347
|
-
} catch (err) {
|
|
1348
|
-
console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
|
|
1349
|
-
}
|
|
1350
|
-
sendKeys(paneId, fullCmd);
|
|
1351
|
-
});
|
|
1352
|
-
} else {
|
|
1353
|
-
sendKeys(paneId, fullCmd);
|
|
1354
|
-
}
|
|
1355
|
-
} else {
|
|
1356
|
-
sendKeys(paneId, fullCmd);
|
|
1357
|
-
}
|
|
935
|
+
sendKeys(paneId, fullCmd);
|
|
1358
936
|
return agent;
|
|
1359
937
|
}
|
|
1360
938
|
async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
@@ -1373,21 +951,12 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1373
951
|
});
|
|
1374
952
|
}
|
|
1375
953
|
const { instruction, agentType, name, color } = agent;
|
|
1376
|
-
const bundledPluginPath =
|
|
954
|
+
const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
|
|
1377
955
|
const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
|
|
1378
956
|
const provider = detectProvider(agentConfig?.frontmatter.model);
|
|
1379
957
|
let paneCwd = cwd;
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
paneCwd = agent.worktreePath;
|
|
1383
|
-
const portOffset = countWorktreeAgents(session.agents);
|
|
1384
|
-
worktreeContext = {
|
|
1385
|
-
offset: portOffset,
|
|
1386
|
-
total: portOffset,
|
|
1387
|
-
branchName: agent.branchName
|
|
1388
|
-
};
|
|
1389
|
-
} else if (agent.repo !== ".") {
|
|
1390
|
-
paneCwd = join5(cwd, agent.repo);
|
|
958
|
+
if (agent.repo !== ".") {
|
|
959
|
+
paneCwd = join3(cwd, agent.repo);
|
|
1391
960
|
}
|
|
1392
961
|
if (agent.paneId) {
|
|
1393
962
|
try {
|
|
@@ -1396,8 +965,11 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1396
965
|
}
|
|
1397
966
|
unregisterAgentPane(sessionId, agentId);
|
|
1398
967
|
}
|
|
968
|
+
const claudeSessionId = provider !== "openai" ? randomUUID2() : void 0;
|
|
1399
969
|
const { paneId, fullCmd } = setupAgentPane({
|
|
1400
970
|
sessionId,
|
|
971
|
+
sessionName: session.name,
|
|
972
|
+
cycleNum: session.orchestratorCycles.length,
|
|
1401
973
|
cwd,
|
|
1402
974
|
agentId,
|
|
1403
975
|
agentType,
|
|
@@ -1407,13 +979,14 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1407
979
|
color,
|
|
1408
980
|
provider,
|
|
1409
981
|
agentConfig,
|
|
1410
|
-
|
|
1411
|
-
|
|
982
|
+
paneCwd,
|
|
983
|
+
claudeSessionId
|
|
1412
984
|
});
|
|
1413
985
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1414
986
|
status: "running",
|
|
1415
987
|
paneId,
|
|
1416
988
|
provider,
|
|
989
|
+
claudeSessionId,
|
|
1417
990
|
spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1418
991
|
completedAt: null,
|
|
1419
992
|
killedReason: void 0
|
|
@@ -1423,7 +996,7 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
|
|
|
1423
996
|
function nextReportNumber(cwd, sessionId, agentId) {
|
|
1424
997
|
const dir = reportsDir(cwd, sessionId);
|
|
1425
998
|
try {
|
|
1426
|
-
const files =
|
|
999
|
+
const files = readdirSync3(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
|
|
1427
1000
|
return String(files.length + 1).padStart(3, "0");
|
|
1428
1001
|
} catch {
|
|
1429
1002
|
return "001";
|
|
@@ -1431,10 +1004,10 @@ function nextReportNumber(cwd, sessionId, agentId) {
|
|
|
1431
1004
|
}
|
|
1432
1005
|
async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
1433
1006
|
const dir = reportsDir(cwd, sessionId);
|
|
1434
|
-
|
|
1007
|
+
mkdirSync2(dir, { recursive: true });
|
|
1435
1008
|
const num = nextReportNumber(cwd, sessionId, agentId);
|
|
1436
1009
|
const filePath = reportFilePath(cwd, sessionId, agentId, num);
|
|
1437
|
-
|
|
1010
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
1438
1011
|
const entry = {
|
|
1439
1012
|
type: "update",
|
|
1440
1013
|
filePath,
|
|
@@ -1451,9 +1024,9 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
|
|
|
1451
1024
|
}
|
|
1452
1025
|
async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
1453
1026
|
const dir = reportsDir(cwd, sessionId);
|
|
1454
|
-
|
|
1027
|
+
mkdirSync2(dir, { recursive: true });
|
|
1455
1028
|
const filePath = reportFilePath(cwd, sessionId, agentId, "final");
|
|
1456
|
-
|
|
1029
|
+
writeFileSync3(filePath, report, "utf-8");
|
|
1457
1030
|
const entry = {
|
|
1458
1031
|
type: "final",
|
|
1459
1032
|
filePath,
|
|
@@ -1467,9 +1040,11 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
1467
1040
|
}
|
|
1468
1041
|
}).catch(() => {
|
|
1469
1042
|
});
|
|
1043
|
+
const flushedActiveMs = flushAgentTimer(sessionId, agentId);
|
|
1470
1044
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1471
1045
|
status: "completed",
|
|
1472
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1046
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1047
|
+
activeMs: flushedActiveMs
|
|
1473
1048
|
});
|
|
1474
1049
|
const session = getSession(cwd, sessionId);
|
|
1475
1050
|
const agent = session.agents.find((a) => a.id === agentId);
|
|
@@ -1487,10 +1062,12 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
|
|
|
1487
1062
|
}
|
|
1488
1063
|
async function handleAgentKilled(cwd, sessionId, agentId, reason) {
|
|
1489
1064
|
unregisterAgentPane(sessionId, agentId);
|
|
1065
|
+
const flushedActiveMs = flushAgentTimer(sessionId, agentId);
|
|
1490
1066
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1491
1067
|
status: "killed",
|
|
1492
1068
|
killedReason: reason,
|
|
1493
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1069
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1070
|
+
activeMs: flushedActiveMs
|
|
1494
1071
|
});
|
|
1495
1072
|
const session = getSession(cwd, sessionId);
|
|
1496
1073
|
return allAgentsDone(session);
|
|
@@ -1500,108 +1077,518 @@ function allAgentsDone(session) {
|
|
|
1500
1077
|
return running.length === 0 && session.agents.length > 0;
|
|
1501
1078
|
}
|
|
1502
1079
|
|
|
1503
|
-
// src/daemon/pane-monitor.ts
|
|
1504
|
-
var monitorInterval = null;
|
|
1505
|
-
var onAllAgentsDone = null;
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
function
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1080
|
+
// src/daemon/pane-monitor.ts
|
|
1081
|
+
var monitorInterval = null;
|
|
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
|
+
}
|
|
1146
|
+
function setRespawnCallback(cb) {
|
|
1147
|
+
onAllAgentsDone = cb;
|
|
1148
|
+
}
|
|
1149
|
+
function startMonitor(pollIntervalMs = 5e3) {
|
|
1150
|
+
if (monitorInterval) return;
|
|
1151
|
+
storedPollIntervalMs = pollIntervalMs;
|
|
1152
|
+
lastPollTime = Date.now();
|
|
1153
|
+
monitorInterval = setInterval(() => {
|
|
1154
|
+
pollAllSessions().catch((err) => {
|
|
1155
|
+
console.error("[sisyphus] Pane monitor error:", err);
|
|
1156
|
+
});
|
|
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
|
+
`;
|
|
1404
|
+
}
|
|
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
|
+
`;
|
|
1521
1422
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
const
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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';
|
|
1540
1449
|
}
|
|
1541
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
|
+
`;
|
|
1542
1469
|
}
|
|
1543
|
-
async function
|
|
1544
|
-
let session;
|
|
1470
|
+
async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
1545
1471
|
try {
|
|
1546
|
-
|
|
1547
|
-
} catch
|
|
1548
|
-
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
if (session.status === "completed") {
|
|
1552
|
-
const orchPaneId2 = getOrchestratorPaneId(sessionId);
|
|
1553
|
-
if (orchPaneId2) {
|
|
1554
|
-
const livePanes2 = listPanes(windowId);
|
|
1555
|
-
const livePaneIds2 = new Set(livePanes2.map((p) => p.paneId));
|
|
1556
|
-
if (!livePaneIds2.has(orchPaneId2)) {
|
|
1557
|
-
cleanupSessionMaps(sessionId);
|
|
1558
|
-
untrackSession(sessionId);
|
|
1559
|
-
console.log(`[sisyphus] Session ${sessionId} cleaned up: orchestrator pane closed by user`);
|
|
1560
|
-
}
|
|
1561
|
-
} else {
|
|
1562
|
-
cleanupSessionMaps(sessionId);
|
|
1563
|
-
untrackSession(sessionId);
|
|
1564
|
-
}
|
|
1565
|
-
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.");
|
|
1566
1475
|
}
|
|
1567
|
-
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
}
|
|
1576
|
-
|
|
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}`;
|
|
1577
1518
|
}
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
if (!livePaneIds.has(agent.paneId)) {
|
|
1583
|
-
paneRemoved = true;
|
|
1584
|
-
const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
|
|
1585
|
-
if (allDone && onAllAgentsDone) {
|
|
1586
|
-
onAllAgentsDone(sessionId, cwd, windowId);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
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);
|
|
1589
1523
|
}
|
|
1590
|
-
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
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);
|
|
1598
1568
|
}
|
|
1599
|
-
|
|
1600
|
-
if (
|
|
1601
|
-
|
|
1602
|
-
|
|
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}`);
|
|
1603
1578
|
}
|
|
1604
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
|
+
}
|
|
1605
1592
|
|
|
1606
1593
|
// src/daemon/notify.ts
|
|
1607
1594
|
import { execFile } from "child_process";
|
|
@@ -1643,7 +1630,10 @@ async function startSession(task, cwd, context, name) {
|
|
|
1643
1630
|
pruneOldSessions(cwd);
|
|
1644
1631
|
if (!name) {
|
|
1645
1632
|
generateSessionName(task).then(async (generatedName) => {
|
|
1646
|
-
if (!generatedName)
|
|
1633
|
+
if (!generatedName) {
|
|
1634
|
+
console.log(`[sisyphus] Name generation returned null for session ${sessionId}`);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1647
1637
|
let finalName = generatedName;
|
|
1648
1638
|
let candidate = `sisyphus-${finalName}`;
|
|
1649
1639
|
let attempt = 0;
|
|
@@ -1662,7 +1652,23 @@ async function startSession(task, cwd, context, name) {
|
|
|
1662
1652
|
await updateSessionTmux(cwd, sessionId, candidate, getSession(cwd, sessionId).tmuxWindowId);
|
|
1663
1653
|
trackSession(sessionId, cwd, candidate);
|
|
1664
1654
|
registerSessionTmux(sessionId, candidate, getSession(cwd, sessionId).tmuxWindowId);
|
|
1665
|
-
|
|
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);
|
|
1666
1672
|
});
|
|
1667
1673
|
}
|
|
1668
1674
|
return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
|
|
@@ -1672,8 +1678,8 @@ var PRUNE_KEEP_DAYS = 7;
|
|
|
1672
1678
|
function pruneOldSessions(cwd) {
|
|
1673
1679
|
try {
|
|
1674
1680
|
const dir = sessionsDir(cwd);
|
|
1675
|
-
if (!
|
|
1676
|
-
const entries =
|
|
1681
|
+
if (!existsSync6(dir)) return;
|
|
1682
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1677
1683
|
const candidates = [];
|
|
1678
1684
|
for (const entry of entries) {
|
|
1679
1685
|
if (!entry.isDirectory()) continue;
|
|
@@ -1696,7 +1702,7 @@ function pruneOldSessions(cwd) {
|
|
|
1696
1702
|
}
|
|
1697
1703
|
for (const c of candidates) {
|
|
1698
1704
|
if (keep.has(c.id)) continue;
|
|
1699
|
-
|
|
1705
|
+
rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
|
|
1700
1706
|
}
|
|
1701
1707
|
} catch (err) {
|
|
1702
1708
|
console.error("[sisyphus] Session pruning failed:", err);
|
|
@@ -1765,8 +1771,8 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
1765
1771
|
}
|
|
1766
1772
|
function listSessions(cwd) {
|
|
1767
1773
|
const dir = sessionsDir(cwd);
|
|
1768
|
-
if (!
|
|
1769
|
-
const entries =
|
|
1774
|
+
if (!existsSync6(dir)) return [];
|
|
1775
|
+
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1770
1776
|
const sessions = [];
|
|
1771
1777
|
for (const entry of entries) {
|
|
1772
1778
|
if (!entry.isDirectory()) continue;
|
|
@@ -1806,17 +1812,6 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1806
1812
|
if (session.status !== "active") return;
|
|
1807
1813
|
pendingRespawns.add(sessionId);
|
|
1808
1814
|
orchestratorDone.delete(sessionId);
|
|
1809
|
-
const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
|
|
1810
|
-
if (worktreeAgents.length > 0) {
|
|
1811
|
-
const results = mergeWorktrees(cwd, worktreeAgents);
|
|
1812
|
-
for (const result of results) {
|
|
1813
|
-
const mergeStatus = result.status;
|
|
1814
|
-
updateAgent(cwd, sessionId, result.agentId, {
|
|
1815
|
-
mergeStatus,
|
|
1816
|
-
mergeDetails: result.conflictDetails
|
|
1817
|
-
}).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
1815
|
const cycleNumber = session.orchestratorCycles.length;
|
|
1821
1816
|
if (cycleNumber > 0) {
|
|
1822
1817
|
createSnapshot(cwd, sessionId, cycleNumber);
|
|
@@ -1855,7 +1850,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1855
1850
|
}
|
|
1856
1851
|
});
|
|
1857
1852
|
}
|
|
1858
|
-
async function handleSpawn(sessionId, cwd, agentType, name, instruction,
|
|
1853
|
+
async function handleSpawn(sessionId, cwd, agentType, name, instruction, repo) {
|
|
1859
1854
|
const windowId = getWindowId(sessionId);
|
|
1860
1855
|
if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
|
|
1861
1856
|
const session = getSession(cwd, sessionId);
|
|
@@ -1865,12 +1860,13 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktre
|
|
|
1865
1860
|
}
|
|
1866
1861
|
const agent = await spawnAgent({
|
|
1867
1862
|
sessionId,
|
|
1863
|
+
sessionName: session.name,
|
|
1864
|
+
cycleNum: session.orchestratorCycles.length,
|
|
1868
1865
|
cwd,
|
|
1869
1866
|
agentType,
|
|
1870
1867
|
name,
|
|
1871
1868
|
instruction,
|
|
1872
1869
|
windowId,
|
|
1873
|
-
worktree,
|
|
1874
1870
|
repo
|
|
1875
1871
|
});
|
|
1876
1872
|
await appendAgentToLastCycle(cwd, sessionId, agent.id);
|
|
@@ -1903,16 +1899,15 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
|
1903
1899
|
}
|
|
1904
1900
|
async function handleComplete(sessionId, cwd, report) {
|
|
1905
1901
|
const session = getSession(cwd, sessionId);
|
|
1902
|
+
await flushTimers(sessionId);
|
|
1906
1903
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
1907
1904
|
switchToHomeSession(session);
|
|
1908
1905
|
}
|
|
1909
1906
|
async function handleContinue(sessionId, cwd) {
|
|
1910
1907
|
await continueSession(cwd, sessionId);
|
|
1911
1908
|
}
|
|
1912
|
-
async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
|
|
1913
|
-
await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
|
|
1914
|
-
}
|
|
1915
1909
|
async function handleKill(sessionId, cwd) {
|
|
1910
|
+
await flushTimers(sessionId);
|
|
1916
1911
|
const session = getSession(cwd, sessionId);
|
|
1917
1912
|
const windowId = getWindowId(sessionId);
|
|
1918
1913
|
let killedAgents = 0;
|
|
@@ -1926,12 +1921,6 @@ async function handleKill(sessionId, cwd) {
|
|
|
1926
1921
|
killedAgents++;
|
|
1927
1922
|
}
|
|
1928
1923
|
}
|
|
1929
|
-
for (const agent of session.agents) {
|
|
1930
|
-
if (agent.worktreePath && agent.branchName) {
|
|
1931
|
-
const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
|
|
1932
|
-
cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
|
|
1933
|
-
}
|
|
1934
|
-
}
|
|
1935
1924
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
1936
1925
|
if (orchPaneId) {
|
|
1937
1926
|
killPane(orchPaneId);
|
|
@@ -1966,10 +1955,6 @@ async function handleKillAgent(sessionId, cwd, agentId) {
|
|
|
1966
1955
|
if (agent.paneId) {
|
|
1967
1956
|
killPane(agent.paneId);
|
|
1968
1957
|
}
|
|
1969
|
-
if (agent.worktreePath && agent.branchName) {
|
|
1970
|
-
const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
|
|
1971
|
-
cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
|
|
1972
|
-
}
|
|
1973
1958
|
await updateAgent(cwd, sessionId, agentId, {
|
|
1974
1959
|
status: "killed",
|
|
1975
1960
|
killedReason: "killed by user",
|
|
@@ -1999,12 +1984,6 @@ async function handleRollback(sessionId, cwd, toCycle) {
|
|
|
1999
1984
|
});
|
|
2000
1985
|
}
|
|
2001
1986
|
}
|
|
2002
|
-
for (const agent of session.agents) {
|
|
2003
|
-
if (agent.worktreePath && agent.branchName) {
|
|
2004
|
-
const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
|
|
2005
|
-
cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
1987
|
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
2009
1988
|
if (orchPaneId) {
|
|
2010
1989
|
killPane(orchPaneId);
|
|
@@ -2035,6 +2014,8 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2035
2014
|
} else if (role === "orchestrator") {
|
|
2036
2015
|
const sessionName = session.name ?? sessionId.slice(0, 8);
|
|
2037
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);
|
|
2038
2019
|
orchestratorDone.add(sessionId);
|
|
2039
2020
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
2040
2021
|
if (!hasRunningAgents && session.agents.length > 0) {
|
|
@@ -2054,11 +2035,11 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2054
2035
|
var server = null;
|
|
2055
2036
|
var sessionTrackingMap = /* @__PURE__ */ new Map();
|
|
2056
2037
|
function registryPath() {
|
|
2057
|
-
return
|
|
2038
|
+
return join5(globalDir(), "session-registry.json");
|
|
2058
2039
|
}
|
|
2059
2040
|
function persistSessionRegistry() {
|
|
2060
2041
|
const dir = globalDir();
|
|
2061
|
-
|
|
2042
|
+
mkdirSync3(dir, { recursive: true });
|
|
2062
2043
|
const registry = {};
|
|
2063
2044
|
for (const [id, tracking] of sessionTrackingMap) {
|
|
2064
2045
|
registry[id] = tracking.cwd;
|
|
@@ -2067,9 +2048,9 @@ function persistSessionRegistry() {
|
|
|
2067
2048
|
}
|
|
2068
2049
|
function loadSessionRegistry() {
|
|
2069
2050
|
const p = registryPath();
|
|
2070
|
-
if (!
|
|
2051
|
+
if (!existsSync7(p)) return {};
|
|
2071
2052
|
try {
|
|
2072
|
-
return JSON.parse(
|
|
2053
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
2073
2054
|
} catch {
|
|
2074
2055
|
return {};
|
|
2075
2056
|
}
|
|
@@ -2112,7 +2093,7 @@ async function handleRequest(req) {
|
|
|
2112
2093
|
case "spawn": {
|
|
2113
2094
|
const tracking = sessionTrackingMap.get(req.sessionId);
|
|
2114
2095
|
if (!tracking) return unknownSessionError(req.sessionId);
|
|
2115
|
-
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);
|
|
2116
2097
|
return { ok: true, data: { agentId: result.agentId } };
|
|
2117
2098
|
}
|
|
2118
2099
|
case "submit": {
|
|
@@ -2151,6 +2132,18 @@ async function handleRequest(req) {
|
|
|
2151
2132
|
const cwd = sessionTrackingMap.get(req.sessionId)?.cwd ?? req.cwd;
|
|
2152
2133
|
if (!cwd) return unknownSessionError(req.sessionId);
|
|
2153
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
|
+
}
|
|
2154
2147
|
return { ok: true, data: { session } };
|
|
2155
2148
|
}
|
|
2156
2149
|
return { ok: true, data: { message: "daemon running" } };
|
|
@@ -2185,7 +2178,7 @@ async function handleRequest(req) {
|
|
|
2185
2178
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2186
2179
|
if (!tracking) {
|
|
2187
2180
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2188
|
-
if (
|
|
2181
|
+
if (existsSync7(stateFile)) {
|
|
2189
2182
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2190
2183
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2191
2184
|
persistSessionRegistry();
|
|
@@ -2198,12 +2191,6 @@ async function handleRequest(req) {
|
|
|
2198
2191
|
if (session.tmuxWindowId) tracking.windowId = session.tmuxWindowId;
|
|
2199
2192
|
return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
|
|
2200
2193
|
}
|
|
2201
|
-
case "register_claude_session": {
|
|
2202
|
-
const tracking = sessionTrackingMap.get(req.sessionId);
|
|
2203
|
-
if (!tracking) return unknownSessionError(req.sessionId);
|
|
2204
|
-
await handleRegisterClaudeSession(tracking.cwd, req.sessionId, req.agentId, req.claudeSessionId);
|
|
2205
|
-
return { ok: true };
|
|
2206
|
-
}
|
|
2207
2194
|
case "kill": {
|
|
2208
2195
|
const tracking = sessionTrackingMap.get(req.sessionId);
|
|
2209
2196
|
if (!tracking) return unknownSessionError(req.sessionId);
|
|
@@ -2228,7 +2215,7 @@ async function handleRequest(req) {
|
|
|
2228
2215
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2229
2216
|
if (!tracking) {
|
|
2230
2217
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2231
|
-
if (
|
|
2218
|
+
if (existsSync7(stateFile)) {
|
|
2232
2219
|
registerSessionCwd(req.sessionId, req.cwd);
|
|
2233
2220
|
tracking = sessionTrackingMap.get(req.sessionId);
|
|
2234
2221
|
} else {
|
|
@@ -2242,7 +2229,7 @@ async function handleRequest(req) {
|
|
|
2242
2229
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2243
2230
|
if (!tracking) {
|
|
2244
2231
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2245
|
-
if (
|
|
2232
|
+
if (existsSync7(stateFile)) {
|
|
2246
2233
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2247
2234
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2248
2235
|
persistSessionRegistry();
|
|
@@ -2265,8 +2252,8 @@ async function handleRequest(req) {
|
|
|
2265
2252
|
sessionTrackingMap.delete(req.sessionId);
|
|
2266
2253
|
persistSessionRegistry();
|
|
2267
2254
|
}
|
|
2268
|
-
const { sessionDir: sessionDir2 } = await import("./paths-
|
|
2269
|
-
|
|
2255
|
+
const { sessionDir: sessionDir2 } = await import("./paths-IJXOAN4E.js");
|
|
2256
|
+
rmSync3(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
|
|
2270
2257
|
return { ok: true };
|
|
2271
2258
|
}
|
|
2272
2259
|
case "pane-exited": {
|
|
@@ -2297,8 +2284,8 @@ async function handleRequest(req) {
|
|
|
2297
2284
|
let filePath;
|
|
2298
2285
|
if (req.content.length > 200) {
|
|
2299
2286
|
const dir = messagesDir(tracking.cwd, req.sessionId);
|
|
2300
|
-
|
|
2301
|
-
filePath =
|
|
2287
|
+
mkdirSync3(dir, { recursive: true });
|
|
2288
|
+
filePath = join5(dir, `${id}.md`);
|
|
2302
2289
|
writeFileSync5(filePath, req.content, "utf-8");
|
|
2303
2290
|
}
|
|
2304
2291
|
await appendMessage(tracking.cwd, req.sessionId, {
|
|
@@ -2322,7 +2309,7 @@ async function handleRequest(req) {
|
|
|
2322
2309
|
function startServer() {
|
|
2323
2310
|
return new Promise((resolve5, reject) => {
|
|
2324
2311
|
const sock = socketPath();
|
|
2325
|
-
if (
|
|
2312
|
+
if (existsSync7(sock)) {
|
|
2326
2313
|
unlinkSync(sock);
|
|
2327
2314
|
}
|
|
2328
2315
|
server = createServer((conn) => {
|
|
@@ -2341,12 +2328,16 @@ function startServer() {
|
|
|
2341
2328
|
continue;
|
|
2342
2329
|
}
|
|
2343
2330
|
handleRequest(req).then((res) => {
|
|
2344
|
-
conn.
|
|
2331
|
+
if (!conn.destroyed) {
|
|
2332
|
+
conn.write(JSON.stringify(res) + "\n");
|
|
2333
|
+
}
|
|
2345
2334
|
});
|
|
2346
2335
|
}
|
|
2347
2336
|
});
|
|
2348
2337
|
conn.on("error", (err) => {
|
|
2349
|
-
|
|
2338
|
+
if (err.code !== "EPIPE") {
|
|
2339
|
+
console.error("[sisyphus] Connection error:", err.message);
|
|
2340
|
+
}
|
|
2350
2341
|
});
|
|
2351
2342
|
});
|
|
2352
2343
|
server.on("error", reject);
|
|
@@ -2364,7 +2355,7 @@ function stopServer() {
|
|
|
2364
2355
|
}
|
|
2365
2356
|
server.close(() => {
|
|
2366
2357
|
const sock = socketPath();
|
|
2367
|
-
if (
|
|
2358
|
+
if (existsSync7(sock)) {
|
|
2368
2359
|
unlinkSync(sock);
|
|
2369
2360
|
}
|
|
2370
2361
|
server = null;
|
|
@@ -2375,7 +2366,7 @@ function stopServer() {
|
|
|
2375
2366
|
|
|
2376
2367
|
// src/daemon/updater.ts
|
|
2377
2368
|
import { execSync as execSync3 } from "child_process";
|
|
2378
|
-
import { readFileSync as
|
|
2369
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, lstatSync } from "fs";
|
|
2379
2370
|
import { resolve as resolve4 } from "path";
|
|
2380
2371
|
import { get } from "https";
|
|
2381
2372
|
function isNewer(latest, current) {
|
|
@@ -2392,7 +2383,7 @@ function isNewer(latest, current) {
|
|
|
2392
2383
|
function readPackageVersion() {
|
|
2393
2384
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
2394
2385
|
try {
|
|
2395
|
-
const raw =
|
|
2386
|
+
const raw = readFileSync6(resolve4(import.meta.dirname, rel), "utf-8");
|
|
2396
2387
|
const pkg = JSON.parse(raw);
|
|
2397
2388
|
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
2398
2389
|
} catch {
|
|
@@ -2507,7 +2498,7 @@ var origError = console.error.bind(console);
|
|
|
2507
2498
|
console.log = (...args) => origLog(`[${ts()}]`, ...args);
|
|
2508
2499
|
console.error = (...args) => origError(`[${ts()}]`, ...args);
|
|
2509
2500
|
function ensureDirs() {
|
|
2510
|
-
|
|
2501
|
+
mkdirSync4(globalDir(), { recursive: true });
|
|
2511
2502
|
}
|
|
2512
2503
|
function isProcessAlive(pid) {
|
|
2513
2504
|
try {
|
|
@@ -2520,7 +2511,7 @@ function isProcessAlive(pid) {
|
|
|
2520
2511
|
function readPid() {
|
|
2521
2512
|
const pidFile = daemonPidPath();
|
|
2522
2513
|
try {
|
|
2523
|
-
const pid = parseInt(
|
|
2514
|
+
const pid = parseInt(readFileSync7(pidFile, "utf-8").trim(), 10);
|
|
2524
2515
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
2525
2516
|
} catch {
|
|
2526
2517
|
return null;
|
|
@@ -2591,11 +2582,11 @@ async function recoverSessions() {
|
|
|
2591
2582
|
let recovered = 0;
|
|
2592
2583
|
for (const [sessionId, cwd] of entries) {
|
|
2593
2584
|
const stateFile = statePath(cwd, sessionId);
|
|
2594
|
-
if (!
|
|
2585
|
+
if (!existsSync8(stateFile)) {
|
|
2595
2586
|
continue;
|
|
2596
2587
|
}
|
|
2597
2588
|
try {
|
|
2598
|
-
const session = JSON.parse(
|
|
2589
|
+
const session = JSON.parse(readFileSync7(stateFile, "utf-8"));
|
|
2599
2590
|
if (session.status === "active" || session.status === "paused") {
|
|
2600
2591
|
registerSessionCwd(sessionId, cwd);
|
|
2601
2592
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
@@ -2614,6 +2605,7 @@ async function recoverSessions() {
|
|
|
2614
2605
|
setWindowId(sessionId, session.tmuxWindowId);
|
|
2615
2606
|
trackSession(sessionId, cwd, session.tmuxSessionName);
|
|
2616
2607
|
updateTrackedWindow(sessionId, session.tmuxWindowId);
|
|
2608
|
+
initTimers(sessionId, session);
|
|
2617
2609
|
const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
|
|
2618
2610
|
if (lastIncompleteCycle?.paneId) {
|
|
2619
2611
|
setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
|
|
@@ -2638,6 +2630,7 @@ async function recoverSessions() {
|
|
|
2638
2630
|
const orchestratorPaneId = getOrchestratorPaneId(sessionId);
|
|
2639
2631
|
const orchestratorAlive = orchestratorPaneId && livePaneIds.has(orchestratorPaneId);
|
|
2640
2632
|
if (!orchestratorAlive) {
|
|
2633
|
+
await completeOrchestratorCycle(cwd, sessionId);
|
|
2641
2634
|
console.log(`[sisyphus] Detected stuck session ${sessionId} on recovery: triggering orchestrator respawn`);
|
|
2642
2635
|
await onAllAgentsDone2(sessionId, cwd, session.tmuxWindowId);
|
|
2643
2636
|
}
|
|
@@ -2673,6 +2666,12 @@ async function startDaemon() {
|
|
|
2673
2666
|
const shutdown = async () => {
|
|
2674
2667
|
console.log("[sisyphus] Shutting down...");
|
|
2675
2668
|
stopMonitor();
|
|
2669
|
+
for (const sessionId of getTrackedSessionIds()) {
|
|
2670
|
+
try {
|
|
2671
|
+
await flushTimers(sessionId);
|
|
2672
|
+
} catch {
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2676
2675
|
await stopServer();
|
|
2677
2676
|
releasePidLock();
|
|
2678
2677
|
process.exit(0);
|