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.
Files changed (100) hide show
  1. package/dist/{chunk-Q6VQOUN3.js → chunk-M7LZ2ZHD.js} +3 -27
  2. package/dist/chunk-M7LZ2ZHD.js.map +1 -0
  3. package/dist/{chunk-YGBGKMTF.js → chunk-REUQ4B45.js} +7 -11
  4. package/dist/chunk-REUQ4B45.js.map +1 -0
  5. package/dist/{chunk-MMA43N67.js → chunk-Z32YVDMY.js} +2 -2
  6. package/dist/chunk-Z32YVDMY.js.map +1 -0
  7. package/dist/cli.js +46 -49
  8. package/dist/cli.js.map +1 -1
  9. package/dist/daemon.js +795 -796
  10. package/dist/daemon.js.map +1 -1
  11. package/dist/{paths-FYYSBD27.js → paths-IJXOAN4E.js} +4 -6
  12. package/dist/templates/CLAUDE.md +16 -14
  13. package/dist/templates/agent-plugin/agents/CLAUDE.md +17 -6
  14. package/dist/templates/agent-plugin/agents/design.md +134 -0
  15. package/dist/templates/agent-plugin/agents/explore.md +39 -0
  16. package/dist/templates/agent-plugin/agents/operator.md +24 -0
  17. package/dist/templates/agent-plugin/agents/plan.md +15 -20
  18. package/dist/templates/agent-plugin/agents/problem.md +119 -0
  19. package/dist/templates/agent-plugin/agents/requirements.md +138 -0
  20. package/dist/templates/agent-plugin/agents/review/CLAUDE.md +29 -0
  21. package/dist/templates/agent-plugin/agents/review/compliance.md +6 -6
  22. package/dist/templates/agent-plugin/agents/review-plan/code-smells.md +4 -4
  23. package/dist/templates/agent-plugin/agents/review-plan/requirements-coverage.md +62 -0
  24. package/dist/templates/agent-plugin/agents/review-plan/security.md +1 -1
  25. package/dist/templates/agent-plugin/agents/review-plan.md +9 -8
  26. package/dist/templates/agent-plugin/agents/review.md +1 -1
  27. package/dist/templates/agent-plugin/agents/test-spec.md +2 -2
  28. package/dist/templates/agent-plugin/hooks/CLAUDE.md +2 -2
  29. package/dist/templates/agent-plugin/hooks/explore-user-prompt.sh +13 -0
  30. package/dist/templates/agent-plugin/hooks/plan-user-prompt.sh +1 -1
  31. package/dist/templates/agent-plugin/hooks/require-submit.sh +69 -2
  32. package/dist/templates/agent-plugin/hooks/review-plan-user-prompt.sh +4 -4
  33. package/dist/templates/agent-plugin/hooks/review-user-prompt.sh +1 -1
  34. package/dist/templates/agent-suffix.md +0 -2
  35. package/dist/templates/orchestrator-base.md +167 -145
  36. package/dist/templates/orchestrator-impl.md +92 -57
  37. package/dist/templates/orchestrator-planning.md +46 -56
  38. package/dist/templates/orchestrator-plugin/commands/sisyphus/design.md +13 -0
  39. package/dist/templates/orchestrator-plugin/commands/sisyphus/problem.md +13 -0
  40. package/dist/templates/orchestrator-plugin/commands/sisyphus/requirements.md +13 -0
  41. package/dist/templates/orchestrator-plugin/commands/sisyphus/strategize.md +19 -0
  42. package/dist/templates/orchestrator-plugin/hooks/explore-gate.sh +15 -0
  43. package/dist/templates/orchestrator-plugin/hooks/hooks.json +14 -1
  44. package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +34 -27
  45. package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +56 -24
  46. package/dist/templates/orchestrator-strategy.md +233 -0
  47. package/dist/templates/orchestrator-validation.md +94 -0
  48. package/dist/tui.js +193 -120
  49. package/dist/tui.js.map +1 -1
  50. package/package.json +2 -2
  51. package/templates/CLAUDE.md +16 -14
  52. package/templates/agent-plugin/agents/CLAUDE.md +17 -6
  53. package/templates/agent-plugin/agents/design.md +134 -0
  54. package/templates/agent-plugin/agents/explore.md +39 -0
  55. package/templates/agent-plugin/agents/operator.md +24 -0
  56. package/templates/agent-plugin/agents/plan.md +15 -20
  57. package/templates/agent-plugin/agents/problem.md +119 -0
  58. package/templates/agent-plugin/agents/requirements.md +138 -0
  59. package/templates/agent-plugin/agents/review/CLAUDE.md +29 -0
  60. package/templates/agent-plugin/agents/review/compliance.md +6 -6
  61. package/templates/agent-plugin/agents/review-plan/code-smells.md +4 -4
  62. package/templates/agent-plugin/agents/review-plan/requirements-coverage.md +62 -0
  63. package/templates/agent-plugin/agents/review-plan/security.md +1 -1
  64. package/templates/agent-plugin/agents/review-plan.md +9 -8
  65. package/templates/agent-plugin/agents/review.md +1 -1
  66. package/templates/agent-plugin/agents/test-spec.md +2 -2
  67. package/templates/agent-plugin/hooks/CLAUDE.md +2 -2
  68. package/templates/agent-plugin/hooks/explore-user-prompt.sh +13 -0
  69. package/templates/agent-plugin/hooks/plan-user-prompt.sh +1 -1
  70. package/templates/agent-plugin/hooks/require-submit.sh +69 -2
  71. package/templates/agent-plugin/hooks/review-plan-user-prompt.sh +4 -4
  72. package/templates/agent-plugin/hooks/review-user-prompt.sh +1 -1
  73. package/templates/agent-suffix.md +0 -2
  74. package/templates/orchestrator-base.md +167 -145
  75. package/templates/orchestrator-impl.md +92 -57
  76. package/templates/orchestrator-planning.md +46 -56
  77. package/templates/orchestrator-plugin/commands/sisyphus/design.md +13 -0
  78. package/templates/orchestrator-plugin/commands/sisyphus/problem.md +13 -0
  79. package/templates/orchestrator-plugin/commands/sisyphus/requirements.md +13 -0
  80. package/templates/orchestrator-plugin/commands/sisyphus/strategize.md +19 -0
  81. package/templates/orchestrator-plugin/hooks/explore-gate.sh +15 -0
  82. package/templates/orchestrator-plugin/hooks/hooks.json +14 -1
  83. package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +34 -27
  84. package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +56 -24
  85. package/templates/orchestrator-strategy.md +233 -0
  86. package/templates/orchestrator-validation.md +94 -0
  87. package/dist/chunk-MMA43N67.js.map +0 -1
  88. package/dist/chunk-Q6VQOUN3.js.map +0 -1
  89. package/dist/chunk-YGBGKMTF.js.map +0 -1
  90. package/dist/templates/agent-plugin/agents/review-plan/spec-coverage.md +0 -44
  91. package/dist/templates/agent-plugin/agents/spec-draft.md +0 -78
  92. package/dist/templates/agent-plugin/hooks/hooks.json +0 -25
  93. package/dist/templates/agent-plugin/hooks/spec-user-prompt.sh +0 -19
  94. package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +0 -111
  95. package/templates/agent-plugin/agents/review-plan/spec-coverage.md +0 -44
  96. package/templates/agent-plugin/agents/spec-draft.md +0 -78
  97. package/templates/agent-plugin/hooks/hooks.json +0 -25
  98. package/templates/agent-plugin/hooks/spec-user-prompt.sh +0 -19
  99. package/templates/orchestrator-plugin/skills/git-management/SKILL.md +0 -111
  100. /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-MMA43N67.js";
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
- worktreeBaseDir,
34
- worktreeConfigPath
35
- } from "./chunk-YGBGKMTF.js";
33
+ strategyPath
34
+ } from "./chunk-REUQ4B45.js";
36
35
 
37
36
  // src/daemon/index.ts
38
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
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 existsSync8, writeFileSync as writeFileSync5, readFileSync as readFileSync6, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
45
- import { join as join7 } from "path";
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 existsSync7, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
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 existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
316
- import { execSync } from "child_process";
317
- import { resolve as resolve2, join as join3 } from "path";
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 fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path}${branchSuffix} #[default]`;
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
- // src/daemon/orchestrator.ts
620
- function detectRepos(cwd) {
621
- const config = loadConfig(cwd);
622
- const repos = [];
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 entries = readdirSync3(cwd, { withFileTypes: true });
631
- for (const entry of entries) {
632
- if (!entry.isDirectory()) continue;
633
- if (entry.name.startsWith(".")) continue;
634
- const childPath = join3(cwd, entry.name);
635
- if (existsSync4(join3(childPath, ".git"))) {
636
- try {
637
- repos.push(getRepoInfo(childPath, entry.name));
638
- } catch {
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
- } catch {
643
- }
644
- if (config.repos && config.repos.length > 0) {
645
- const allowed = new Set(config.repos);
646
- return repos.filter((r) => r.name === "." || allowed.has(r.name));
647
- }
648
- return repos;
649
- }
650
- function getRepoInfo(repoPath, name) {
651
- const branchRaw = execSafe(`git -C ${shellQuote(repoPath)} rev-parse --abbrev-ref HEAD`)?.trim();
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 formatStateForOrchestrator(session) {
686
- const cycleNum = session.orchestratorCycles.length;
687
- const ctxDir = contextDir(session.cwd, session.id);
688
- const roadmapFile = roadmapPath(session.cwd, session.id);
689
- const logFile = cycleLogPath(session.cwd, session.id, cycleNum + 1);
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
- ${ctxLines}
710
- `;
711
- }
712
- }
713
- const messages = session.messages ?? [];
714
- const messagesSection = messages.length > 0 ? "\n### Messages\n\n" + messages.map((m) => {
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
- previousCyclesSection = `
731
- ### Previous Cycles
732
-
733
- ${lines.join("\n")}
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
- if (repos.length > 1) {
777
- repositoriesSection += '\nTarget agents at specific repos:\n```bash\nsisyphus spawn --name "impl" --repo <repo-name> "task"\n```\n';
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
- const goalFile = goalPath(session.cwd, session.id);
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
- ${roadmapRef}
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
- async function spawnOrchestrator(sessionId, cwd, windowId, message) {
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
- execSync("which claude", { stdio: "pipe", env: EXEC_ENV });
763
+ template = readFileSync3(templatePath, "utf-8");
798
764
  } catch {
799
- throw new Error("Claude CLI not found on PATH. Run `sisyphus doctor` to diagnose.");
765
+ template = `# Sisyphus Agent
766
+ Session: {{SESSION_ID}}
767
+ Task: {{INSTRUCTION}}`;
800
768
  }
801
- const session = getSession(cwd, sessionId);
802
- const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
803
- const mode = lastCycle?.mode ?? "planning";
804
- const basePrompt = loadOrchestratorPrompt(cwd, mode);
805
- const formattedState = formatStateForOrchestrator(session);
806
- const agentPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
807
- const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
808
- agentTypes.push(
809
- { qualifiedName: "Explore", source: "bundled", model: "haiku", description: "Fast codebase exploration \u2014 find files, search code, answer questions about architecture. Use for research and context gathering." }
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 envExports = buildEnvExports([
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
- copyFileSync2(agentConfig.filePath, `${base}/agents/${shortName}.md`);
1186
- const subAgentDir = join5(dirname3(agentConfig.filePath), shortName);
1187
- if (existsSync6(subAgentDir)) {
1188
- for (const f of readdirSync5(subAgentDir)) {
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
- copyFileSync2(join5(subAgentDir, f), `${base}/agents/${f}`);
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 = resolve3(import.meta.dirname, "../templates/agent-plugin/hooks");
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
- writeFileSync4(`${base}/hooks/hooks.json`, JSON.stringify({ hooks: hooksConfig }, null, 2), "utf-8");
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, worktreeContext, paneCwd } = opts;
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
- setPaneTitle(paneId, `${paneLabel} (${agentId})`);
1234
- setPaneStyle(paneId, color);
1235
- const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
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
- writeFileSync4(suffixFilePath, suffix, "utf-8");
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
- writeFileSync4(codexPromptPath, parts.join("\n\n"), "utf-8");
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
- mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote(instruction)}`;
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 = resolve3(import.meta.dirname, "../templates/agent-plugin");
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
- execSync2(`which ${cliToCheck}`, { stdio: "pipe", env: execEnv() });
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 : join5(cwd, repo);
1296
- let paneCwd = repoRoot;
1297
- let worktreePath;
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
- worktreeContext,
1321
- paneCwd
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
- if (opts.worktree && worktreePath) {
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 = resolve3(import.meta.dirname, "../templates/agent-plugin");
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
- let worktreeContext;
1381
- if (agent.worktreePath) {
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
- worktreeContext,
1411
- paneCwd
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 = readdirSync5(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
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
- mkdirSync3(dir, { recursive: true });
1007
+ mkdirSync2(dir, { recursive: true });
1435
1008
  const num = nextReportNumber(cwd, sessionId, agentId);
1436
1009
  const filePath = reportFilePath(cwd, sessionId, agentId, num);
1437
- writeFileSync4(filePath, content, "utf-8");
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
- mkdirSync3(dir, { recursive: true });
1027
+ mkdirSync2(dir, { recursive: true });
1455
1028
  const filePath = reportFilePath(cwd, sessionId, agentId, "final");
1456
- writeFileSync4(filePath, report, "utf-8");
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
- function setRespawnCallback(cb) {
1507
- onAllAgentsDone = cb;
1508
- }
1509
- function startMonitor(pollIntervalMs = 5e3) {
1510
- if (monitorInterval) return;
1511
- monitorInterval = setInterval(() => {
1512
- pollAllSessions().catch((err) => {
1513
- console.error("[sisyphus] Pane monitor error:", err);
1514
- });
1515
- }, pollIntervalMs);
1516
- }
1517
- function stopMonitor() {
1518
- if (monitorInterval) {
1519
- clearInterval(monitorInterval);
1520
- monitorInterval = null;
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
- var trackedSessions = /* @__PURE__ */ new Map();
1524
- function trackSession(sessionId, cwd, tmuxSession) {
1525
- const existing = trackedSessions.get(sessionId);
1526
- trackedSessions.set(sessionId, { id: sessionId, cwd, tmuxSession, windowId: existing ? existing.windowId : null });
1527
- }
1528
- function updateTrackedWindow(sessionId, windowId) {
1529
- const entry = trackedSessions.get(sessionId);
1530
- if (!entry) throw new Error(`Cannot update window for untracked session: ${sessionId}`);
1531
- entry.windowId = windowId;
1532
- }
1533
- function untrackSession(sessionId) {
1534
- trackedSessions.delete(sessionId);
1535
- }
1536
- async function pollAllSessions() {
1537
- for (const { id: sessionId, cwd, windowId } of trackedSessions.values()) {
1538
- if (windowId) {
1539
- await pollSession(sessionId, cwd, windowId);
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 pollSession(sessionId, cwd, windowId) {
1544
- let session;
1470
+ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
1545
1471
  try {
1546
- session = getSession(cwd, sessionId);
1547
- } catch (err) {
1548
- console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
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
- if (session.status !== "active") return;
1568
- const livePanes = listPanes(windowId);
1569
- if (livePanes.length === 0) {
1570
- const tracked = trackedSessions.get(sessionId);
1571
- if (tracked && !sessionExists(tracked.tmuxSession)) {
1572
- await updateSessionStatus(cwd, sessionId, "paused");
1573
- untrackSession(sessionId);
1574
- console.log(`[sisyphus] Session ${sessionId} paused: tmux session destroyed`);
1575
- }
1576
- return;
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 livePaneIds = new Set(livePanes.map((p) => p.paneId));
1579
- let paneRemoved = false;
1580
- for (const agent of session.agents) {
1581
- if (agent.status !== "running") continue;
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
- if (paneRemoved) selectLayout(windowId);
1591
- const orchPaneId = getOrchestratorPaneId(sessionId);
1592
- if (orchPaneId && !livePaneIds.has(orchPaneId)) {
1593
- const runningAgents = session.agents.filter((a) => a.status === "running");
1594
- if (runningAgents.length === 0) {
1595
- await updateSessionStatus(cwd, sessionId, "paused");
1596
- console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
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
- session = getSession(cwd, sessionId);
1600
- if (session.status === "active" && session.agents.length > 0 && session.agents.every((a) => a.status !== "running") && (!orchPaneId || !livePaneIds.has(orchPaneId)) && onAllAgentsDone) {
1601
- console.log(`[sisyphus] Detected stuck session ${sessionId}: all agents done, no orchestrator \u2014 triggering respawn`);
1602
- onAllAgentsDone(sessionId, cwd, windowId);
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) return;
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
- }).catch(() => {
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 (!existsSync7(dir)) return;
1676
- const entries = readdirSync6(dir, { withFileTypes: true });
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
- rmSync3(sessionDir(cwd, c.id), { recursive: true, force: true });
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 (!existsSync7(dir)) return [];
1769
- const entries = readdirSync6(dir, { withFileTypes: true });
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, worktree, repo) {
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 join7(globalDir(), "session-registry.json");
2038
+ return join5(globalDir(), "session-registry.json");
2058
2039
  }
2059
2040
  function persistSessionRegistry() {
2060
2041
  const dir = globalDir();
2061
- mkdirSync4(dir, { recursive: true });
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 (!existsSync8(p)) return {};
2051
+ if (!existsSync7(p)) return {};
2071
2052
  try {
2072
- return JSON.parse(readFileSync6(p, "utf-8"));
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.worktree, req.repo);
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 (existsSync8(stateFile)) {
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 (existsSync8(stateFile)) {
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 (existsSync8(stateFile)) {
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-FYYSBD27.js");
2269
- rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
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
- mkdirSync4(dir, { recursive: true });
2301
- filePath = join7(dir, `${id}.md`);
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 (existsSync8(sock)) {
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.write(JSON.stringify(res) + "\n");
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
- console.error("[sisyphus] Connection error:", err.message);
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 (existsSync8(sock)) {
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 readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, lstatSync } from "fs";
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 = readFileSync7(resolve4(import.meta.dirname, rel), "utf-8");
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
- mkdirSync5(globalDir(), { recursive: true });
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(readFileSync8(pidFile, "utf-8").trim(), 10);
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 (!existsSync9(stateFile)) {
2585
+ if (!existsSync8(stateFile)) {
2595
2586
  continue;
2596
2587
  }
2597
2588
  try {
2598
- const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
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);