sisyphi 1.0.13 → 1.1.0

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