sisyphi 0.1.2 → 0.1.4

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 (74) hide show
  1. package/README.md +103 -33
  2. package/dist/{chunk-FWHTKXN5.js → chunk-N2BPQOO2.js} +23 -3
  3. package/dist/chunk-N2BPQOO2.js.map +1 -0
  4. package/dist/cli.js +85 -162
  5. package/dist/cli.js.map +1 -1
  6. package/dist/daemon.js +603 -186
  7. package/dist/daemon.js.map +1 -1
  8. package/dist/templates/CLAUDE.md +50 -0
  9. package/dist/templates/agent-plugin/.claude/agents/debug.md +39 -0
  10. package/dist/templates/agent-plugin/.claude/agents/plan.md +101 -0
  11. package/dist/templates/agent-plugin/.claude/agents/review-plan.md +81 -0
  12. package/dist/templates/agent-plugin/.claude/agents/review.md +56 -0
  13. package/dist/templates/agent-plugin/.claude/agents/spec-draft.md +73 -0
  14. package/dist/templates/agent-plugin/.claude/agents/test-spec.md +56 -0
  15. package/dist/templates/agent-plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/templates/agent-plugin/agents/CLAUDE.md +52 -0
  17. package/dist/templates/agent-plugin/agents/debug.md +39 -0
  18. package/dist/templates/agent-plugin/agents/operator.md +56 -0
  19. package/dist/templates/agent-plugin/agents/plan.md +101 -0
  20. package/dist/templates/agent-plugin/agents/review-plan.md +81 -0
  21. package/dist/templates/agent-plugin/agents/review.md +56 -0
  22. package/dist/templates/agent-plugin/agents/spec-draft.md +73 -0
  23. package/dist/templates/agent-plugin/agents/test-spec.md +56 -0
  24. package/dist/templates/agent-suffix.md +3 -1
  25. package/dist/templates/banner.txt +24 -6
  26. package/dist/templates/orchestrator-plugin/.claude/commands/begin.md +62 -0
  27. package/dist/templates/orchestrator-plugin/.claude/skills/orchestration/SKILL.md +40 -0
  28. package/dist/templates/orchestrator-plugin/.claude/skills/orchestration/task-patterns.md +222 -0
  29. package/dist/templates/orchestrator-plugin/.claude/skills/orchestration/workflow-examples.md +208 -0
  30. package/dist/templates/orchestrator-plugin/.claude-plugin/plugin.json +5 -0
  31. package/dist/templates/orchestrator-plugin/hooks/hooks.json +25 -0
  32. package/dist/templates/orchestrator-plugin/scripts/block-task.sh +4 -0
  33. package/dist/templates/orchestrator-plugin/scripts/stop-suggest.sh +4 -0
  34. package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +111 -0
  35. package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +40 -0
  36. package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +248 -0
  37. package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +237 -0
  38. package/dist/templates/orchestrator-settings.json +2 -0
  39. package/dist/templates/orchestrator.md +56 -49
  40. package/dist/templates/resources/.claude/agents/debug.md +39 -0
  41. package/dist/templates/resources/.claude/agents/plan.md +101 -0
  42. package/dist/templates/resources/.claude/agents/review-plan.md +81 -0
  43. package/dist/templates/resources/.claude/agents/review.md +56 -0
  44. package/dist/templates/resources/.claude/agents/spec-draft.md +73 -0
  45. package/dist/templates/resources/.claude/agents/test-spec.md +56 -0
  46. package/dist/templates/resources/.claude/commands/begin.md +62 -0
  47. package/dist/templates/resources/.claude/skills/orchestration/SKILL.md +40 -0
  48. package/dist/templates/resources/.claude/skills/orchestration/task-patterns.md +222 -0
  49. package/dist/templates/resources/.claude/skills/orchestration/workflow-examples.md +208 -0
  50. package/dist/templates/resources/.claude-plugin/plugin.json +8 -0
  51. package/package.json +2 -2
  52. package/templates/CLAUDE.md +50 -0
  53. package/templates/agent-plugin/.claude-plugin/plugin.json +5 -0
  54. package/templates/agent-plugin/agents/CLAUDE.md +52 -0
  55. package/templates/agent-plugin/agents/debug.md +39 -0
  56. package/templates/agent-plugin/agents/operator.md +56 -0
  57. package/templates/agent-plugin/agents/plan.md +101 -0
  58. package/templates/agent-plugin/agents/review-plan.md +81 -0
  59. package/templates/agent-plugin/agents/review.md +56 -0
  60. package/templates/agent-plugin/agents/spec-draft.md +73 -0
  61. package/templates/agent-plugin/agents/test-spec.md +56 -0
  62. package/templates/agent-suffix.md +3 -1
  63. package/templates/banner.txt +24 -6
  64. package/templates/orchestrator-plugin/.claude-plugin/plugin.json +5 -0
  65. package/templates/orchestrator-plugin/hooks/hooks.json +25 -0
  66. package/templates/orchestrator-plugin/scripts/block-task.sh +4 -0
  67. package/templates/orchestrator-plugin/scripts/stop-suggest.sh +4 -0
  68. package/templates/orchestrator-plugin/skills/git-management/SKILL.md +111 -0
  69. package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +40 -0
  70. package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +248 -0
  71. package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +237 -0
  72. package/templates/orchestrator-settings.json +2 -0
  73. package/templates/orchestrator.md +56 -49
  74. package/dist/chunk-FWHTKXN5.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -4,18 +4,23 @@ import {
4
4
  daemonPidPath,
5
5
  globalConfigPath,
6
6
  globalDir,
7
+ logsPath,
8
+ planPath,
7
9
  projectConfigPath,
8
10
  projectOrchestratorPromptPath,
11
+ promptsDir,
9
12
  reportFilePath,
10
13
  reportsDir,
11
14
  sessionDir,
12
15
  sessionsDir,
13
16
  socketPath,
14
- statePath
15
- } from "./chunk-FWHTKXN5.js";
17
+ statePath,
18
+ worktreeBaseDir,
19
+ worktreeConfigPath
20
+ } from "./chunk-N2BPQOO2.js";
16
21
 
17
22
  // src/daemon/index.ts
18
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync5 } from "fs";
23
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
19
24
 
20
25
  // src/shared/config.ts
21
26
  import { readFileSync } from "fs";
@@ -38,17 +43,33 @@ function loadConfig(cwd) {
38
43
 
39
44
  // src/daemon/server.ts
40
45
  import { createServer } from "net";
41
- import { unlinkSync, existsSync as existsSync4, writeFileSync as writeFileSync4, readFileSync as readFileSync5, mkdirSync as mkdirSync3 } from "fs";
42
- import { join as join2 } from "path";
46
+ import { unlinkSync, existsSync as existsSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync7, mkdirSync as mkdirSync4 } from "fs";
47
+ import { join as join4 } from "path";
43
48
 
44
49
  // src/daemon/session-manager.ts
45
50
  import { v4 as uuidv4 } from "uuid";
46
- import { existsSync as existsSync3, readdirSync as readdirSync3 } from "fs";
51
+ import { existsSync as existsSync4, readdirSync as readdirSync4 } from "fs";
47
52
 
48
53
  // src/daemon/state.ts
49
54
  import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
50
55
  import { dirname, join } from "path";
51
56
  import { randomUUID } from "crypto";
57
+ var PLAN_SEED = `---
58
+ description: >
59
+ Living document of what still needs to happen. Write your remaining work plan
60
+ here: phases, next steps, file references, open questions. Remove or collapse
61
+ items as they're completed so this file only reflects outstanding work. The
62
+ orchestrator sees this every cycle \u2014 keep it focused and current.
63
+ ---
64
+ `;
65
+ var LOGS_SEED = `---
66
+ description: >
67
+ Session memory. Record important observations, decisions, and findings here.
68
+ This is your persistent memory across cycles: things you tried, what
69
+ worked/failed, design decisions and their rationale, gotchas discovered during
70
+ implementation. Unlike plan.md, entries here accumulate \u2014 they're a log.
71
+ ---
72
+ `;
52
73
  var sessionLocks = /* @__PURE__ */ new Map();
53
74
  async function withSessionLock(sessionId, fn) {
54
75
  const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
@@ -74,13 +95,15 @@ function createSession(id, task, cwd) {
74
95
  const dir = sessionDir(cwd, id);
75
96
  mkdirSync(dir, { recursive: true });
76
97
  mkdirSync(contextDir(cwd, id), { recursive: true });
98
+ mkdirSync(promptsDir(cwd, id), { recursive: true });
99
+ writeFileSync(planPath(cwd, id), PLAN_SEED, "utf-8");
100
+ writeFileSync(logsPath(cwd, id), LOGS_SEED, "utf-8");
77
101
  const session = {
78
102
  id,
79
103
  task,
80
104
  cwd,
81
105
  status: "active",
82
106
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
83
- tasks: [],
84
107
  agents: [],
85
108
  orchestratorCycles: []
86
109
  };
@@ -94,30 +117,6 @@ function getSession(cwd, sessionId) {
94
117
  function saveSession(session) {
95
118
  atomicWrite(statePath(session.cwd, session.id), JSON.stringify(session, null, 2));
96
119
  }
97
- async function addTask(cwd, sessionId, description, initialStatus) {
98
- return withSessionLock(sessionId, () => {
99
- const session = getSession(cwd, sessionId);
100
- const nextNum = session.tasks.length + 1;
101
- const task = {
102
- id: `t${nextNum}`,
103
- description,
104
- status: initialStatus !== void 0 ? initialStatus : "pending"
105
- };
106
- session.tasks.push(task);
107
- saveSession(session);
108
- return task;
109
- });
110
- }
111
- async function updateTask(cwd, sessionId, taskId, updates) {
112
- return withSessionLock(sessionId, () => {
113
- const session = getSession(cwd, sessionId);
114
- const task = session.tasks.find((t) => t.id === taskId);
115
- if (!task) throw new Error(`Task ${taskId} not found in session ${sessionId}`);
116
- if (updates.status) task.status = updates.status;
117
- if (updates.description) task.description = updates.description;
118
- saveSession(session);
119
- });
120
- }
121
120
  async function addAgent(cwd, sessionId, agent) {
122
121
  return withSessionLock(sessionId, () => {
123
122
  const session = getSession(cwd, sessionId);
@@ -128,7 +127,7 @@ async function addAgent(cwd, sessionId, agent) {
128
127
  async function updateAgent(cwd, sessionId, agentId, updates) {
129
128
  return withSessionLock(sessionId, () => {
130
129
  const session = getSession(cwd, sessionId);
131
- const agent = session.agents.find((a) => a.id === agentId);
130
+ const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
132
131
  if (!agent) throw new Error(`Agent ${agentId} not found in session ${sessionId}`);
133
132
  Object.assign(agent, updates);
134
133
  saveSession(session);
@@ -172,12 +171,20 @@ async function completeSession(cwd, sessionId, report) {
172
171
  async function appendAgentReport(cwd, sessionId, agentId, entry) {
173
172
  return withSessionLock(sessionId, () => {
174
173
  const session = getSession(cwd, sessionId);
175
- const agent = session.agents.find((a) => a.id === agentId);
174
+ const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
176
175
  if (!agent) throw new Error(`Agent ${agentId} not found in session ${sessionId}`);
177
176
  agent.reports.push(entry);
178
177
  saveSession(session);
179
178
  });
180
179
  }
180
+ async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId) {
181
+ return withSessionLock(sessionId, () => {
182
+ const session = getSession(cwd, sessionId);
183
+ session.tmuxSessionName = tmuxSessionName;
184
+ session.tmuxWindowId = tmuxWindowId;
185
+ saveSession(session);
186
+ });
187
+ }
181
188
  async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
182
189
  return withSessionLock(sessionId, () => {
183
190
  const session = getSession(cwd, sessionId);
@@ -191,9 +198,83 @@ async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
191
198
  }
192
199
 
193
200
  // src/daemon/orchestrator.ts
194
- import { readFileSync as readFileSync3, existsSync, writeFileSync as writeFileSync2, readdirSync } from "fs";
201
+ import { existsSync, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
195
202
  import { resolve } from "path";
196
203
 
204
+ // src/daemon/colors.ts
205
+ import { readFileSync as readFileSync3 } from "fs";
206
+ import { homedir } from "os";
207
+ import { join as join2 } from "path";
208
+ var ORCHESTRATOR_COLOR = "yellow";
209
+ var AGENT_PALETTE = ["blue", "green", "magenta", "cyan", "red", "white"];
210
+ var TMUX_COLOR_MAP = {
211
+ orange: "colour208",
212
+ teal: "colour6"
213
+ };
214
+ function normalizeTmuxColor(color) {
215
+ return TMUX_COLOR_MAP[color] ?? color;
216
+ }
217
+ var sessionColorIndex = /* @__PURE__ */ new Map();
218
+ function getNextColor(sessionId) {
219
+ const idx = sessionColorIndex.get(sessionId) ?? 0;
220
+ const color = AGENT_PALETTE[idx % AGENT_PALETTE.length];
221
+ sessionColorIndex.set(sessionId, idx + 1);
222
+ return color;
223
+ }
224
+ function resetColors(sessionId) {
225
+ sessionColorIndex.delete(sessionId);
226
+ }
227
+ function extractFrontmatterColor(content) {
228
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
229
+ if (!match) return null;
230
+ const colorMatch = match[1].match(/^color:\s*(.+)$/m);
231
+ return colorMatch ? colorMatch[1].trim() : null;
232
+ }
233
+ function findPluginInstallPath(namespace) {
234
+ try {
235
+ const registryPath2 = join2(homedir(), ".claude", "plugins", "installed_plugins.json");
236
+ const registry = JSON.parse(readFileSync3(registryPath2, "utf-8"));
237
+ for (const key of Object.keys(registry)) {
238
+ if (key.startsWith(`${namespace}@`)) {
239
+ return registry[key].installPath ?? null;
240
+ }
241
+ }
242
+ } catch {
243
+ }
244
+ return null;
245
+ }
246
+ function resolveAgentTypeColor(agentType, pluginDir, cwd) {
247
+ if (!agentType) return null;
248
+ let namespace;
249
+ let name;
250
+ if (agentType.includes(":")) {
251
+ [namespace, name] = agentType.split(":", 2);
252
+ } else {
253
+ name = agentType;
254
+ }
255
+ const searchPaths = [];
256
+ if (namespace) {
257
+ searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
258
+ const installPath = findPluginInstallPath(namespace);
259
+ if (installPath) {
260
+ searchPaths.push(join2(installPath, "agents", `${name}.md`));
261
+ }
262
+ } else {
263
+ searchPaths.push(join2(cwd, ".claude", "agents", `${name}.md`));
264
+ searchPaths.push(join2(homedir(), ".claude", "agents", `${name}.md`));
265
+ searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
266
+ }
267
+ for (const path of searchPaths) {
268
+ try {
269
+ const content = readFileSync3(path, "utf-8");
270
+ const color = extractFrontmatterColor(content);
271
+ if (color) return normalizeTmuxColor(color);
272
+ } catch {
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+
197
278
  // src/daemon/tmux.ts
198
279
  import { execSync } from "child_process";
199
280
  var EXEC_ENV = {
@@ -216,8 +297,8 @@ function createPane(windowTarget, cwd) {
216
297
  execSafe(`tmux select-layout -t "${windowTarget}" even-horizontal`);
217
298
  return paneId;
218
299
  }
219
- function sendKeys(paneTarget, command) {
220
- exec(`tmux send-keys -t "${paneTarget}" ${shellQuote(command)} Enter`);
300
+ function sendKeys(paneTarget, command2) {
301
+ exec(`tmux send-keys -t "${paneTarget}" ${shellQuote(command2)} Enter`);
221
302
  }
222
303
  function killPane(paneTarget) {
223
304
  execSafe(`tmux kill-pane -t "${paneTarget}"`);
@@ -239,48 +320,43 @@ function setPaneTitle(paneTarget, title) {
239
320
  function setPaneStyle(paneTarget, color) {
240
321
  const fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path} #[default]`;
241
322
  execSafe(`tmux set -p -t "${paneTarget}" pane-border-format ${shellQuote(fmt)}`);
242
- execSafe(`tmux set -p -t "${paneTarget}" pane-border-style "fg=${color}"`);
243
- execSafe(`tmux set -p -t "${paneTarget}" pane-active-border-style "fg=${color}"`);
323
+ execSafe(`tmux set -p -t "${paneTarget}" @pane_color "${color}"`);
324
+ execSafe(`tmux set -w -t "${paneTarget}" pane-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
325
+ execSafe(`tmux set -w -t "${paneTarget}" pane-active-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
326
+ }
327
+ function selectLayout(windowTarget, layout = "even-horizontal") {
328
+ execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
244
329
  }
245
330
  function shellQuote(s) {
246
331
  return `'${s.replace(/'/g, "'\\''")}'`;
247
332
  }
248
333
 
249
- // src/daemon/colors.ts
250
- var ORCHESTRATOR_COLOR = "yellow";
251
- var AGENT_PALETTE = ["blue", "green", "magenta", "cyan", "red", "white"];
252
- var sessionColorIndex = /* @__PURE__ */ new Map();
253
- function getNextColor(sessionId) {
254
- const idx = sessionColorIndex.get(sessionId) ?? 0;
255
- const color = AGENT_PALETTE[idx % AGENT_PALETTE.length];
256
- sessionColorIndex.set(sessionId, idx + 1);
257
- return color;
258
- }
259
- function resetColors(sessionId) {
260
- sessionColorIndex.delete(sessionId);
261
- }
262
-
263
334
  // src/daemon/orchestrator.ts
264
335
  var sessionWindowMap = /* @__PURE__ */ new Map();
265
336
  var sessionOrchestratorPane = /* @__PURE__ */ new Map();
266
337
  function getWindowId(sessionId) {
267
338
  return sessionWindowMap.get(sessionId);
268
339
  }
340
+ function setWindowId(sessionId, windowId) {
341
+ sessionWindowMap.set(sessionId, windowId);
342
+ }
269
343
  function getOrchestratorPaneId(sessionId) {
270
344
  return sessionOrchestratorPane.get(sessionId);
271
345
  }
346
+ function setOrchestratorPaneId(sessionId, paneId) {
347
+ sessionOrchestratorPane.set(sessionId, paneId);
348
+ }
272
349
  function loadOrchestratorPrompt(cwd) {
273
350
  const projectPath = projectOrchestratorPromptPath(cwd);
274
351
  if (existsSync(projectPath)) {
275
- return readFileSync3(projectPath, "utf-8");
352
+ return readFileSync4(projectPath, "utf-8");
276
353
  }
277
354
  const bundledPath = resolve(import.meta.dirname, "../templates/orchestrator.md");
278
- return readFileSync3(bundledPath, "utf-8");
355
+ return readFileSync4(bundledPath, "utf-8");
279
356
  }
280
357
  function formatStateForOrchestrator(session) {
281
358
  const shortId = session.id.slice(0, 8);
282
359
  const cycleNum = session.orchestratorCycles.length;
283
- const taskLines = session.tasks.length > 0 ? session.tasks.map((t) => `- ${t.id}: [${t.status}] ${t.description}`).join("\n") : " (none)";
284
360
  const ctxDir = contextDir(session.cwd, session.id);
285
361
  let contextLines;
286
362
  if (existsSync(ctxDir)) {
@@ -289,6 +365,10 @@ function formatStateForOrchestrator(session) {
289
365
  } else {
290
366
  contextLines = " (none)";
291
367
  }
368
+ const planFile = planPath(session.cwd, session.id);
369
+ const planRef = existsSync(planFile) ? `@${planFile}` : "(empty)";
370
+ const logsFile = logsPath(session.cwd, session.id);
371
+ const logsRef = existsSync(logsFile) ? `@${logsFile}` : "(empty)";
292
372
  const agentLines = session.agents.length > 0 ? session.agents.map((a) => {
293
373
  const header = `- ${a.id} (${a.name}): ${a.status} \u2014 ${a.reports.length} report(s)`;
294
374
  if (a.reports.length === 0) return header;
@@ -303,32 +383,54 @@ function formatStateForOrchestrator(session) {
303
383
  const spawnedList = c.agentsSpawned.length > 0 ? c.agentsSpawned.join(", ") : "(none)";
304
384
  return `Cycle ${c.cycle}: Spawned ${spawnedList}`;
305
385
  }).join("\n") : " (none)";
306
- return [
307
- "<state>",
308
- `session: ${shortId} (cycle ${cycleNum})`,
309
- `task: ${session.task}`,
310
- `status: ${session.status}`,
311
- "",
312
- "## Tasks",
313
- taskLines,
314
- "",
315
- "## Context Files",
316
- contextLines,
317
- "",
318
- "## Agents",
319
- agentLines,
320
- "",
321
- "## Previous Cycles",
322
- cycleLines,
323
- "</state>"
324
- ].join("\n");
386
+ const worktreeAgents = session.agents.filter((a) => a.worktreePath);
387
+ let worktreeSection = "";
388
+ if (worktreeAgents.length > 0) {
389
+ const wtLines = worktreeAgents.map((a) => {
390
+ if (a.mergeStatus === "conflict") {
391
+ return `- ${a.id}: conflict \u2014 ${a.mergeDetails ?? "unknown"}
392
+ Branch: ${a.branchName}
393
+ Worktree: ${a.worktreePath}`;
394
+ }
395
+ const status = a.mergeStatus ?? "pending";
396
+ return `- ${a.id}: ${status} (branch ${a.branchName})`;
397
+ }).join("\n");
398
+ worktreeSection = `
399
+
400
+ ## Worktrees
401
+ ${wtLines}`;
402
+ }
403
+ const worktreeHint = existsSync(worktreeConfigPath(session.cwd)) ? "Worktree config active (`.sisyphus/worktree.json`). Use `--worktree` flag with `sisyphus spawn` to isolate agents in their own worktrees. Recommended for feature work, especially with potential file overlap." : "No worktree configuration found. If this session involves parallel work where agents may edit overlapping files, use the `git-management` skill to set up `.sisyphus/worktree.json` and enable worktree isolation.";
404
+ return `<state>
405
+ session: ${shortId} (cycle ${cycleNum})
406
+ task: ${session.task}
407
+ status: ${session.status}
408
+
409
+ ## Plan
410
+ ${planRef}
411
+
412
+ ## Logs
413
+ ${logsRef}
414
+
415
+ ## Agents
416
+ ${agentLines}${worktreeSection}
417
+
418
+ ## Previous Cycles
419
+ ${cycleLines}
420
+
421
+ ## Context Files
422
+ ${contextLines}
423
+
424
+ ## Git Worktrees
425
+ ${worktreeHint}
426
+ </state>`;
325
427
  }
326
428
  async function spawnOrchestrator(sessionId, cwd, windowId, message) {
327
429
  const session = getSession(cwd, sessionId);
328
430
  const basePrompt = loadOrchestratorPrompt(cwd);
329
431
  const formattedState = formatStateForOrchestrator(session);
330
432
  const cycleNum = session.orchestratorCycles.length + 1;
331
- const promptFilePath = `${sessionDir(cwd, sessionId)}/orchestrator-prompt-${cycleNum}.md`;
433
+ const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
332
434
  writeFileSync2(promptFilePath, basePrompt, "utf-8");
333
435
  sessionWindowMap.set(sessionId, windowId);
334
436
  const envExports = [
@@ -350,12 +452,14 @@ ${storedPrompt}`;
350
452
  } else {
351
453
  userPrompt = `${formattedState}
352
454
 
353
- Review the current session state and execute the next cycle of work.`;
455
+ Review the current session and delegate the next cycle of work.`;
354
456
  }
355
457
  }
356
- const userPromptFilePath = `${sessionDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
458
+ const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
357
459
  writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
358
- const claudeCmd = `claude --dangerously-skip-permissions --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
460
+ const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
461
+ const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
462
+ const claudeCmd = `claude --dangerously-skip-permissions --settings "${settingsPath}" --plugin-dir "${pluginPath}" --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
359
463
  const paneId = createPane(windowId, cwd);
360
464
  sessionOrchestratorPane.set(sessionId, paneId);
361
465
  setPaneTitle(paneId, `orchestrator (${sessionId.slice(0, 8)})`);
@@ -383,6 +487,8 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
383
487
  killPane(paneId);
384
488
  sessionOrchestratorPane.delete(sessionId);
385
489
  }
490
+ const windowId = sessionWindowMap.get(sessionId);
491
+ if (windowId) selectLayout(windowId);
386
492
  await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
387
493
  const session = getSession(cwd, sessionId);
388
494
  const runningAgents = session.agents.filter((a) => a.status === "running");
@@ -391,59 +497,237 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
391
497
  }
392
498
  }
393
499
  async function handleOrchestratorComplete(sessionId, cwd, report) {
394
- const paneId = resolveOrchestratorPane(sessionId, cwd);
395
500
  await completeOrchestratorCycle(cwd, sessionId);
396
501
  await completeSession(cwd, sessionId, report);
397
- if (paneId) {
398
- killPane(paneId);
399
- sessionOrchestratorPane.delete(sessionId);
400
- }
401
- sessionWindowMap.delete(sessionId);
402
502
  console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
403
503
  }
504
+ function cleanupSessionMaps(sessionId) {
505
+ sessionOrchestratorPane.delete(sessionId);
506
+ sessionWindowMap.delete(sessionId);
507
+ }
404
508
 
405
509
  // src/daemon/agent.ts
406
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
510
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
407
511
  import { resolve as resolve2 } from "path";
512
+
513
+ // src/daemon/worktree.ts
514
+ import { execSync as execSync2 } from "child_process";
515
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync as readdirSync2, rmSync } from "fs";
516
+ import { dirname as dirname2, join as join3 } from "path";
517
+ var EXEC_ENV2 = {
518
+ ...process.env,
519
+ PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
520
+ };
521
+ function exec2(cmd, cwd) {
522
+ return execSync2(cmd, { encoding: "utf-8", env: EXEC_ENV2, cwd }).trim();
523
+ }
524
+ function execSafe2(cmd, cwd) {
525
+ try {
526
+ return exec2(cmd, cwd);
527
+ } catch {
528
+ return null;
529
+ }
530
+ }
531
+ function shellQuote2(s) {
532
+ return `'${s.replace(/'/g, "'\\''")}'`;
533
+ }
534
+ function loadWorktreeConfig(cwd) {
535
+ try {
536
+ const content = readFileSync5(worktreeConfigPath(cwd), "utf-8");
537
+ return JSON.parse(content);
538
+ } catch {
539
+ return null;
540
+ }
541
+ }
542
+ function createWorktree(cwd, sessionId, agentId) {
543
+ const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
544
+ const worktreePath = join3(worktreeBaseDir(cwd), agentId);
545
+ mkdirSync2(dirname2(worktreePath), { recursive: true });
546
+ exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
547
+ exec2(`git -C ${shellQuote2(cwd)} worktree add ${shellQuote2(worktreePath)} ${shellQuote2(branchName)}`);
548
+ const symlinks = [".sisyphus", ".claude"];
549
+ for (const entry of symlinks) {
550
+ const src = join3(cwd, entry);
551
+ if (existsSync2(src)) {
552
+ execSafe2(`ln -s ${shellQuote2(src)} ${shellQuote2(join3(worktreePath, entry))}`);
553
+ }
554
+ }
555
+ const config = loadWorktreeConfig(cwd);
556
+ if (config) {
557
+ bootstrapWorktree(cwd, worktreePath, config);
558
+ }
559
+ return { worktreePath, branchName };
560
+ }
561
+ function bootstrapWorktree(cwd, worktreePath, config) {
562
+ if (config.copy) {
563
+ for (const entry of config.copy) {
564
+ const dest = join3(worktreePath, entry);
565
+ mkdirSync2(dirname2(dest), { recursive: true });
566
+ execSafe2(`cp -r ${shellQuote2(join3(cwd, entry))} ${shellQuote2(dest)}`);
567
+ }
568
+ }
569
+ if (config.clone) {
570
+ for (const entry of config.clone) {
571
+ const dest = join3(worktreePath, entry);
572
+ mkdirSync2(dirname2(dest), { recursive: true });
573
+ const src = shellQuote2(join3(cwd, entry));
574
+ const dstQ = shellQuote2(dest);
575
+ if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
576
+ execSafe2(`cp -r ${src} ${dstQ}`);
577
+ }
578
+ }
579
+ }
580
+ if (config.symlink) {
581
+ for (const entry of config.symlink) {
582
+ const dest = join3(worktreePath, entry);
583
+ mkdirSync2(dirname2(dest), { recursive: true });
584
+ execSafe2(`ln -s ${shellQuote2(join3(cwd, entry))} ${shellQuote2(dest)}`);
585
+ }
586
+ }
587
+ if (config.init) {
588
+ try {
589
+ exec2(config.init, worktreePath);
590
+ } catch (err) {
591
+ console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
592
+ }
593
+ }
594
+ }
595
+ function resolveWorktreeBranch(cwd, worktreePath) {
596
+ const output = execSafe2(`git -C ${shellQuote2(cwd)} worktree list --porcelain`);
597
+ if (!output) return null;
598
+ const lines = output.split("\n");
599
+ for (let i = 0; i < lines.length; i++) {
600
+ if (lines[i] === `worktree ${worktreePath}`) {
601
+ for (let j = i + 1; j < lines.length; j++) {
602
+ const line = lines[j];
603
+ if (line === "") break;
604
+ if (line.startsWith("branch refs/heads/")) {
605
+ return line.slice("branch refs/heads/".length);
606
+ }
607
+ }
608
+ break;
609
+ }
610
+ }
611
+ return null;
612
+ }
613
+ function mergeWorktrees(cwd, agents) {
614
+ const pending = agents.filter(
615
+ (a) => a.worktreePath && a.mergeStatus === "pending"
616
+ );
617
+ const results = [];
618
+ for (const agent of pending) {
619
+ const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
620
+ if (!branch) {
621
+ results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
622
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)} --force`);
623
+ continue;
624
+ }
625
+ const aheadLog = execSafe2(`git -C ${shellQuote2(cwd)} log HEAD..${shellQuote2(branch)} --oneline`);
626
+ if (!aheadLog) {
627
+ results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
628
+ cleanupWorktree(cwd, agent.worktreePath, branch);
629
+ continue;
630
+ }
631
+ const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
632
+ const mergeCmd = `git -C ${shellQuote2(cwd)} merge --no-ff ${shellQuote2(branch)} -m ${shellQuote2(mergeMsg)}`;
633
+ try {
634
+ exec2(mergeCmd);
635
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)}`);
636
+ execSafe2(`git -C ${shellQuote2(cwd)} branch -d ${shellQuote2(branch)}`);
637
+ results.push({ agentId: agent.id, name: agent.name, status: "merged" });
638
+ } catch (err) {
639
+ execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
640
+ const stderr = err?.stderr;
641
+ const conflictDetails = stderr ? (typeof stderr === "string" ? stderr : stderr.toString("utf-8")).trim() : err instanceof Error ? err.message : String(err);
642
+ results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
643
+ }
644
+ }
645
+ return results;
646
+ }
647
+ function cleanupWorktree(cwd, worktreePath, branchName) {
648
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(worktreePath)} --force`);
649
+ execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
650
+ const baseDir = dirname2(worktreePath);
651
+ try {
652
+ const entries = readdirSync2(baseDir);
653
+ if (entries.length === 0) {
654
+ rmSync(baseDir, { recursive: true });
655
+ }
656
+ } catch {
657
+ }
658
+ }
659
+ function countWorktreeAgents(agents) {
660
+ return agents.filter((a) => a.worktreePath && a.status === "running").length;
661
+ }
662
+
663
+ // src/daemon/agent.ts
408
664
  var agentCounters = /* @__PURE__ */ new Map();
409
- function resetAgentCounter(sessionId, value = 0) {
410
- agentCounters.set(sessionId, value);
665
+ function resetAgentCounterFromState(sessionId, agents) {
666
+ let max = 0;
667
+ for (const a of agents) {
668
+ const match = a.id.match(/^agent-(\d+)$/);
669
+ if (match) max = Math.max(max, parseInt(match[1], 10));
670
+ }
671
+ agentCounters.set(sessionId, max);
411
672
  }
412
673
  function clearAgentCounter(sessionId) {
413
674
  agentCounters.delete(sessionId);
414
675
  }
415
- function renderAgentSuffix(sessionId, instruction) {
676
+ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
416
677
  const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
417
678
  let template;
418
679
  try {
419
- template = readFileSync4(templatePath, "utf-8");
680
+ template = readFileSync6(templatePath, "utf-8");
420
681
  } catch {
421
682
  template = `# Sisyphus Agent
422
683
  Session: {{SESSION_ID}}
423
684
  Task: {{INSTRUCTION}}`;
424
685
  }
425
- return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction);
686
+ let worktreeBlock = "";
687
+ if (worktreeContext) {
688
+ worktreeBlock = [
689
+ "## Worktree Context",
690
+ `You are working in worktree ${worktreeContext.offset} of ${worktreeContext.total} concurrent worktrees on branch \`${worktreeContext.branchName}\`.`,
691
+ `If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
692
+ ].join("\n");
693
+ }
694
+ return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
426
695
  }
427
696
  async function spawnAgent(opts) {
428
697
  const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
429
698
  const count = (agentCounters.get(sessionId) ?? 0) + 1;
430
699
  agentCounters.set(sessionId, count);
431
700
  const agentId = `agent-${String(count).padStart(3, "0")}`;
432
- const color = getNextColor(sessionId);
433
- const paneId = createPane(windowId, cwd);
701
+ const pluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
702
+ const color = resolveAgentTypeColor(agentType, pluginPath, cwd) ?? getNextColor(sessionId);
703
+ let paneCwd = cwd;
704
+ let worktreePath;
705
+ let branchName;
706
+ let worktreeContext;
707
+ if (opts.worktree) {
708
+ const wt = createWorktree(cwd, sessionId, agentId);
709
+ worktreePath = wt.worktreePath;
710
+ branchName = wt.branchName;
711
+ paneCwd = worktreePath;
712
+ const session = getSession(cwd, sessionId);
713
+ const portOffset = countWorktreeAgents(session.agents) + 1;
714
+ worktreeContext = { offset: portOffset, total: portOffset, branchName };
715
+ }
716
+ const paneId = createPane(windowId, paneCwd);
434
717
  setPaneTitle(paneId, `${name} (${agentId})`);
435
718
  setPaneStyle(paneId, color);
436
- const suffix = renderAgentSuffix(sessionId, instruction);
437
- const suffixFilePath = `${sessionDir(cwd, sessionId)}/${agentId}-system.md`;
719
+ const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
720
+ const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
438
721
  writeFileSync3(suffixFilePath, suffix, "utf-8");
439
722
  const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
440
- const bannerCmd = existsSync2(bannerPath) ? `cat '${bannerPath}' &&` : "";
723
+ const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
441
724
  const envExports = [
442
725
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
443
- `export SISYPHUS_AGENT_ID='${agentId}'`
726
+ `export SISYPHUS_AGENT_ID='${agentId}'`,
727
+ ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : []
444
728
  ].join(" && ");
445
- const agentFlag = agentType ? ` --agent ${shellQuote2(agentType)}` : "";
446
- const claudeCmd = `claude --dangerously-skip-permissions${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote2(instruction)}`;
729
+ const agentFlag = agentType ? ` --agent ${shellQuote3(agentType)}` : "";
730
+ const claudeCmd = `claude --dangerously-skip-permissions --plugin-dir "${pluginPath}"${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
447
731
  sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
448
732
  const agent = {
449
733
  id: agentId,
@@ -455,7 +739,8 @@ async function spawnAgent(opts) {
455
739
  spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
456
740
  completedAt: null,
457
741
  reports: [],
458
- paneId
742
+ paneId,
743
+ ...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
459
744
  };
460
745
  await addAgent(cwd, sessionId, agent);
461
746
  return agent;
@@ -463,7 +748,7 @@ async function spawnAgent(opts) {
463
748
  function nextReportNumber(cwd, sessionId, agentId) {
464
749
  const dir = reportsDir(cwd, sessionId);
465
750
  try {
466
- const files = readdirSync2(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
751
+ const files = readdirSync3(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
467
752
  return String(files.length + 1).padStart(3, "0");
468
753
  } catch {
469
754
  return "001";
@@ -471,7 +756,7 @@ function nextReportNumber(cwd, sessionId, agentId) {
471
756
  }
472
757
  async function handleAgentReport(cwd, sessionId, agentId, content) {
473
758
  const dir = reportsDir(cwd, sessionId);
474
- mkdirSync2(dir, { recursive: true });
759
+ mkdirSync3(dir, { recursive: true });
475
760
  const num = nextReportNumber(cwd, sessionId, agentId);
476
761
  const filePath = reportFilePath(cwd, sessionId, agentId, num);
477
762
  writeFileSync3(filePath, content, "utf-8");
@@ -485,7 +770,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
485
770
  }
486
771
  async function handleAgentSubmit(cwd, sessionId, agentId, report) {
487
772
  const dir = reportsDir(cwd, sessionId);
488
- mkdirSync2(dir, { recursive: true });
773
+ mkdirSync3(dir, { recursive: true });
489
774
  const filePath = reportFilePath(cwd, sessionId, agentId, "final");
490
775
  writeFileSync3(filePath, report, "utf-8");
491
776
  const entry = {
@@ -500,10 +785,13 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
500
785
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
501
786
  });
502
787
  const session = getSession(cwd, sessionId);
503
- const agent = session.agents.find((a) => a.id === agentId);
788
+ const agentArr = session.agents;
789
+ const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
504
790
  if (agent) {
505
791
  killPane(agent.paneId);
506
792
  }
793
+ const windowId = getWindowId(sessionId);
794
+ if (windowId) selectLayout(windowId);
507
795
  return allAgentsDone(session);
508
796
  }
509
797
  async function handleAgentKilled(cwd, sessionId, agentId, reason) {
@@ -519,7 +807,7 @@ function allAgentsDone(session) {
519
807
  const running = session.agents.filter((a) => a.status === "running");
520
808
  return running.length === 0 && session.agents.length > 0;
521
809
  }
522
- function shellQuote2(s) {
810
+ function shellQuote3(s) {
523
811
  return `'${s.replace(/'/g, "'\\''")}'`;
524
812
  }
525
813
 
@@ -571,19 +859,38 @@ async function pollSession(sessionId, cwd, windowId) {
571
859
  console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
572
860
  return;
573
861
  }
862
+ if (session.status === "completed") {
863
+ const orchPaneId2 = getOrchestratorPaneId(sessionId);
864
+ if (orchPaneId2) {
865
+ const livePanes2 = listPanes(windowId);
866
+ const livePaneIds2 = new Set(livePanes2.map((p) => p.paneId));
867
+ if (!livePaneIds2.has(orchPaneId2)) {
868
+ cleanupSessionMaps(sessionId);
869
+ untrackSession(sessionId);
870
+ console.log(`[sisyphus] Session ${sessionId} cleaned up: orchestrator pane closed by user`);
871
+ }
872
+ } else {
873
+ cleanupSessionMaps(sessionId);
874
+ untrackSession(sessionId);
875
+ }
876
+ return;
877
+ }
574
878
  if (session.status !== "active") return;
575
879
  const livePanes = listPanes(windowId);
576
880
  if (livePanes.length === 0) return;
577
881
  const livePaneIds = new Set(livePanes.map((p) => p.paneId));
882
+ let paneRemoved = false;
578
883
  for (const agent of session.agents) {
579
884
  if (agent.status !== "running") continue;
580
885
  if (!livePaneIds.has(agent.paneId)) {
886
+ paneRemoved = true;
581
887
  const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
582
888
  if (allDone && onAllAgentsDone) {
583
889
  onAllAgentsDone(sessionId, cwd, windowId);
584
890
  }
585
891
  }
586
892
  }
893
+ if (paneRemoved) selectLayout(windowId);
587
894
  const orchPaneId = getOrchestratorPaneId(sessionId);
588
895
  if (orchPaneId && !livePaneIds.has(orchPaneId)) {
589
896
  const runningAgents = session.agents.filter((a) => a.status === "running");
@@ -592,12 +899,18 @@ async function pollSession(sessionId, cwd, windowId) {
592
899
  console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
593
900
  }
594
901
  }
902
+ session = getSession(cwd, sessionId);
903
+ if (session.status === "active" && session.agents.length > 0 && session.agents.every((a) => a.status !== "running") && (!orchPaneId || !livePaneIds.has(orchPaneId)) && onAllAgentsDone) {
904
+ console.log(`[sisyphus] Detected stuck session ${sessionId}: all agents done, no orchestrator \u2014 triggering respawn`);
905
+ onAllAgentsDone(sessionId, cwd, windowId);
906
+ }
595
907
  }
596
908
 
597
909
  // src/daemon/session-manager.ts
598
910
  async function startSession(task, cwd, tmuxSession, windowId) {
599
911
  const sessionId = uuidv4();
600
912
  const session = createSession(sessionId, task, cwd);
913
+ await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
601
914
  trackSession(sessionId, cwd, tmuxSession);
602
915
  await spawnOrchestrator(sessionId, cwd, windowId);
603
916
  updateTrackedWindow(sessionId, windowId);
@@ -605,17 +918,30 @@ async function startSession(task, cwd, tmuxSession, windowId) {
605
918
  }
606
919
  async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
607
920
  const session = getSession(cwd, sessionId);
608
- for (const agent of session.agents) {
609
- if (agent.status === "running") {
610
- await updateAgent(cwd, sessionId, agent.id, {
611
- status: "lost",
612
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
613
- killedReason: "session resumed \u2014 agent was still running"
614
- });
921
+ if (session.status !== "active") {
922
+ const livePaneIds = /* @__PURE__ */ new Set();
923
+ if (session.tmuxWindowId) {
924
+ const panes = listPanes(session.tmuxWindowId);
925
+ for (const pane of panes) {
926
+ livePaneIds.add(pane.paneId);
927
+ }
928
+ }
929
+ for (const agent of session.agents) {
930
+ if (agent.status === "running") {
931
+ const isAlive = agent.paneId != null && livePaneIds.has(agent.paneId);
932
+ if (!isAlive) {
933
+ await updateAgent(cwd, sessionId, agent.id, {
934
+ status: "lost",
935
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
936
+ killedReason: "session resumed \u2014 agent was still running"
937
+ });
938
+ }
939
+ }
615
940
  }
616
941
  }
617
942
  await updateSessionStatus(cwd, sessionId, "active");
618
- resetAgentCounter(sessionId, session.agents.length);
943
+ await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
944
+ resetAgentCounterFromState(sessionId, session.agents);
619
945
  resetColors(sessionId);
620
946
  trackSession(sessionId, cwd, tmuxSession);
621
947
  await spawnOrchestrator(sessionId, cwd, windowId, message);
@@ -627,8 +953,8 @@ function getSessionStatus(cwd, sessionId) {
627
953
  }
628
954
  function listSessions(cwd) {
629
955
  const dir = sessionsDir(cwd);
630
- if (!existsSync3(dir)) return [];
631
- const entries = readdirSync3(dir, { withFileTypes: true });
956
+ if (!existsSync4(dir)) return [];
957
+ const entries = readdirSync4(dir, { withFileTypes: true });
632
958
  const sessions = [];
633
959
  for (const entry of entries) {
634
960
  if (!entry.isDirectory()) continue;
@@ -650,11 +976,22 @@ function listSessions(cwd) {
650
976
  function onAllAgentsDone2(sessionId, cwd, windowId) {
651
977
  const session = getSession(cwd, sessionId);
652
978
  if (session.status !== "active") return;
979
+ const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
980
+ if (worktreeAgents.length > 0) {
981
+ const results = mergeWorktrees(cwd, worktreeAgents);
982
+ for (const result of results) {
983
+ const mergeStatus = result.status === "conflict" ? "conflict" : "merged";
984
+ updateAgent(cwd, sessionId, result.agentId, {
985
+ mergeStatus,
986
+ mergeDetails: result.conflictDetails
987
+ }).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
988
+ }
989
+ }
653
990
  setTimeout(() => {
654
991
  spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
655
992
  }, 2e3);
656
993
  }
657
- async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
994
+ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
658
995
  const windowId = getWindowId(sessionId);
659
996
  if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
660
997
  const agent = await spawnAgent({
@@ -663,7 +1000,8 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
663
1000
  agentType,
664
1001
  name,
665
1002
  instruction,
666
- windowId
1003
+ windowId,
1004
+ worktree
667
1005
  });
668
1006
  await appendAgentToLastCycle(cwd, sessionId, agent.id);
669
1007
  return { agentId: agent.id };
@@ -681,29 +1019,8 @@ async function handleYield(sessionId, cwd, nextPrompt) {
681
1019
  await handleOrchestratorYield(sessionId, cwd, nextPrompt);
682
1020
  }
683
1021
  async function handleComplete(sessionId, cwd, report) {
684
- untrackSession(sessionId);
685
1022
  await handleOrchestratorComplete(sessionId, cwd, report);
686
1023
  }
687
- async function handleTaskAdd(cwd, sessionId, description, initialStatus) {
688
- const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
689
- const status = initialStatus !== void 0 && VALID_STATUSES.has(initialStatus) ? initialStatus : void 0;
690
- const task = await addTask(cwd, sessionId, description, status);
691
- return { taskId: task.id };
692
- }
693
- async function handleTaskUpdate(cwd, sessionId, taskId, status, description) {
694
- const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
695
- const updates = {};
696
- if (status !== void 0) {
697
- if (!VALID_STATUSES.has(status)) throw new Error(`Invalid status: ${status}. Valid: draft, pending, in_progress, done`);
698
- updates.status = status;
699
- }
700
- if (description !== void 0) updates.description = description;
701
- await updateTask(cwd, sessionId, taskId, updates);
702
- }
703
- function handleTasksList(cwd, sessionId) {
704
- const session = getSession(cwd, sessionId);
705
- return { tasks: session.tasks };
706
- }
707
1024
  async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
708
1025
  await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
709
1026
  }
@@ -721,6 +1038,11 @@ async function handleKill(sessionId, cwd) {
721
1038
  killedAgents++;
722
1039
  }
723
1040
  }
1041
+ for (const agent of session.agents) {
1042
+ if (agent.worktreePath && agent.branchName) {
1043
+ cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1044
+ }
1045
+ }
724
1046
  const orchPaneId = getOrchestratorPaneId(sessionId);
725
1047
  if (orchPaneId) {
726
1048
  killPane(orchPaneId);
@@ -740,11 +1062,11 @@ var sessionCwdMap = /* @__PURE__ */ new Map();
740
1062
  var sessionTmuxMap = /* @__PURE__ */ new Map();
741
1063
  var sessionWindowMap2 = /* @__PURE__ */ new Map();
742
1064
  function registryPath() {
743
- return join2(globalDir(), "session-registry.json");
1065
+ return join4(globalDir(), "session-registry.json");
744
1066
  }
745
1067
  function persistSessionRegistry() {
746
1068
  const dir = globalDir();
747
- mkdirSync3(dir, { recursive: true });
1069
+ mkdirSync4(dir, { recursive: true });
748
1070
  const registry = {};
749
1071
  for (const [id, cwd] of sessionCwdMap) {
750
1072
  registry[id] = cwd;
@@ -753,9 +1075,9 @@ function persistSessionRegistry() {
753
1075
  }
754
1076
  function loadSessionRegistry() {
755
1077
  const p = registryPath();
756
- if (!existsSync4(p)) return {};
1078
+ if (!existsSync5(p)) return {};
757
1079
  try {
758
- return JSON.parse(readFileSync5(p, "utf-8"));
1080
+ return JSON.parse(readFileSync7(p, "utf-8"));
759
1081
  } catch {
760
1082
  return {};
761
1083
  }
@@ -764,6 +1086,10 @@ function registerSessionCwd(sessionId, cwd) {
764
1086
  sessionCwdMap.set(sessionId, cwd);
765
1087
  persistSessionRegistry();
766
1088
  }
1089
+ function registerSessionTmux(sessionId, tmuxSession, windowId) {
1090
+ sessionTmuxMap.set(sessionId, tmuxSession);
1091
+ sessionWindowMap2.set(sessionId, windowId);
1092
+ }
767
1093
  async function handleRequest(req) {
768
1094
  try {
769
1095
  switch (req.type) {
@@ -777,7 +1103,7 @@ async function handleRequest(req) {
777
1103
  case "spawn": {
778
1104
  const cwd = sessionCwdMap.get(req.sessionId);
779
1105
  if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
780
- const result = await handleSpawn(req.sessionId, cwd, req.agentType, req.name, req.instruction);
1106
+ const result = await handleSpawn(req.sessionId, cwd, req.agentType, req.name, req.instruction, req.worktree);
781
1107
  return { ok: true, data: { agentId: result.agentId } };
782
1108
  }
783
1109
  case "submit": {
@@ -815,32 +1141,29 @@ async function handleRequest(req) {
815
1141
  }
816
1142
  return { ok: true, data: { message: "daemon running" } };
817
1143
  }
818
- case "tasks_add": {
819
- const cwd = sessionCwdMap.get(req.sessionId);
820
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
821
- const result = await handleTaskAdd(cwd, req.sessionId, req.description, req.status);
822
- return { ok: true, data: { taskId: result.taskId } };
823
- }
824
- case "tasks_update": {
825
- const cwd = sessionCwdMap.get(req.sessionId);
826
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
827
- await handleTaskUpdate(cwd, req.sessionId, req.taskId, req.status, req.description);
828
- return { ok: true };
829
- }
830
- case "tasks_list": {
831
- const cwd = sessionCwdMap.get(req.sessionId);
832
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
833
- const result = handleTasksList(cwd, req.sessionId);
834
- return { ok: true, data: { tasks: result.tasks } };
835
- }
836
1144
  case "list": {
837
1145
  const allSessions = [];
838
- const seenCwds = /* @__PURE__ */ new Set();
839
- for (const cwd of sessionCwdMap.values()) {
840
- if (seenCwds.has(cwd)) continue;
841
- seenCwds.add(cwd);
842
- const sessions = listSessions(cwd);
843
- allSessions.push(...sessions.map((s) => s));
1146
+ if (req.all) {
1147
+ const seenCwds = /* @__PURE__ */ new Set();
1148
+ for (const cwd of sessionCwdMap.values()) {
1149
+ if (seenCwds.has(cwd)) continue;
1150
+ seenCwds.add(cwd);
1151
+ const sessions = listSessions(cwd);
1152
+ allSessions.push(...sessions.map((s) => ({ ...s, cwd })));
1153
+ }
1154
+ } else {
1155
+ const sessions = listSessions(req.cwd);
1156
+ allSessions.push(...sessions.map((s) => ({ ...s, cwd: req.cwd })));
1157
+ let totalCount = allSessions.length;
1158
+ const seenCwds = /* @__PURE__ */ new Set([req.cwd]);
1159
+ for (const cwd of sessionCwdMap.values()) {
1160
+ if (seenCwds.has(cwd)) continue;
1161
+ seenCwds.add(cwd);
1162
+ totalCount += listSessions(cwd).length;
1163
+ }
1164
+ if (totalCount > allSessions.length) {
1165
+ return { ok: true, data: { sessions: allSessions, totalCount, filtered: true } };
1166
+ }
844
1167
  }
845
1168
  return { ok: true, data: { sessions: allSessions } };
846
1169
  }
@@ -848,7 +1171,7 @@ async function handleRequest(req) {
848
1171
  let cwd = sessionCwdMap.get(req.sessionId);
849
1172
  if (!cwd) {
850
1173
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
851
- if (existsSync4(stateFile)) {
1174
+ if (existsSync5(stateFile)) {
852
1175
  cwd = req.cwd;
853
1176
  registerSessionCwd(req.sessionId, cwd);
854
1177
  } else {
@@ -887,7 +1210,7 @@ async function handleRequest(req) {
887
1210
  function startServer() {
888
1211
  return new Promise((resolve3, reject) => {
889
1212
  const sock = socketPath();
890
- if (existsSync4(sock)) {
1213
+ if (existsSync5(sock)) {
891
1214
  unlinkSync(sock);
892
1215
  }
893
1216
  server = createServer((conn) => {
@@ -929,7 +1252,7 @@ function stopServer() {
929
1252
  }
930
1253
  server.close(() => {
931
1254
  const sock = socketPath();
932
- if (existsSync4(sock)) {
1255
+ if (existsSync5(sock)) {
933
1256
  unlinkSync(sock);
934
1257
  }
935
1258
  server = null;
@@ -940,7 +1263,7 @@ function stopServer() {
940
1263
 
941
1264
  // src/daemon/index.ts
942
1265
  function ensureDirs() {
943
- mkdirSync4(globalDir(), { recursive: true });
1266
+ mkdirSync5(globalDir(), { recursive: true });
944
1267
  }
945
1268
  function isProcessAlive(pid) {
946
1269
  try {
@@ -950,17 +1273,22 @@ function isProcessAlive(pid) {
950
1273
  return false;
951
1274
  }
952
1275
  }
953
- function acquirePidLock() {
1276
+ function readPid() {
954
1277
  const pidFile = daemonPidPath();
955
1278
  try {
956
- const existing = parseInt(readFileSync6(pidFile, "utf-8").trim(), 10);
957
- if (existing && isProcessAlive(existing)) {
958
- console.error(`[sisyphus] Daemon already running (pid ${existing}). Kill it first or remove ${pidFile}`);
959
- process.exit(1);
960
- }
1279
+ const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
1280
+ return pid && isProcessAlive(pid) ? pid : null;
961
1281
  } catch {
1282
+ return null;
962
1283
  }
963
- writeFileSync5(pidFile, String(process.pid), "utf-8");
1284
+ }
1285
+ function acquirePidLock() {
1286
+ const pid = readPid();
1287
+ if (pid) {
1288
+ console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
1289
+ process.exit(0);
1290
+ }
1291
+ writeFileSync5(daemonPidPath(), String(process.pid), "utf-8");
964
1292
  }
965
1293
  function releasePidLock() {
966
1294
  try {
@@ -968,7 +1296,40 @@ function releasePidLock() {
968
1296
  } catch {
969
1297
  }
970
1298
  }
971
- function recoverSessions() {
1299
+ function stopDaemon() {
1300
+ const pid = readPid();
1301
+ if (!pid) {
1302
+ console.log("[sisyphus] Daemon is not running");
1303
+ releasePidLock();
1304
+ return false;
1305
+ }
1306
+ console.log(`[sisyphus] Stopping daemon (pid ${pid})...`);
1307
+ try {
1308
+ process.kill(pid, "SIGTERM");
1309
+ } catch {
1310
+ console.error(`[sisyphus] Failed to send SIGTERM to pid ${pid}`);
1311
+ return false;
1312
+ }
1313
+ const deadline = Date.now() + 5e3;
1314
+ while (Date.now() < deadline) {
1315
+ if (!isProcessAlive(pid)) {
1316
+ console.log("[sisyphus] Daemon stopped");
1317
+ releasePidLock();
1318
+ return true;
1319
+ }
1320
+ const wait = Date.now() + 100;
1321
+ while (Date.now() < wait) {
1322
+ }
1323
+ }
1324
+ console.error(`[sisyphus] Daemon (pid ${pid}) did not exit within 5s, sending SIGKILL`);
1325
+ try {
1326
+ process.kill(pid, "SIGKILL");
1327
+ } catch {
1328
+ }
1329
+ releasePidLock();
1330
+ return true;
1331
+ }
1332
+ async function recoverSessions() {
972
1333
  const registry = loadSessionRegistry();
973
1334
  const entries = Object.entries(registry);
974
1335
  if (entries.length === 0) {
@@ -978,13 +1339,45 @@ function recoverSessions() {
978
1339
  let recovered = 0;
979
1340
  for (const [sessionId, cwd] of entries) {
980
1341
  const stateFile = statePath(cwd, sessionId);
981
- if (!existsSync5(stateFile)) {
1342
+ if (!existsSync6(stateFile)) {
982
1343
  continue;
983
1344
  }
984
1345
  try {
985
- const session = JSON.parse(readFileSync6(stateFile, "utf-8"));
1346
+ const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
986
1347
  if (session.status === "active" || session.status === "paused") {
987
1348
  registerSessionCwd(sessionId, cwd);
1349
+ resetAgentCounterFromState(sessionId, session.agents ?? []);
1350
+ if (session.tmuxSessionName && session.tmuxWindowId) {
1351
+ const livePanes = listPanes(session.tmuxWindowId);
1352
+ if (livePanes.length > 0) {
1353
+ registerSessionTmux(sessionId, session.tmuxSessionName, session.tmuxWindowId);
1354
+ setWindowId(sessionId, session.tmuxWindowId);
1355
+ trackSession(sessionId, cwd, session.tmuxSessionName);
1356
+ updateTrackedWindow(sessionId, session.tmuxWindowId);
1357
+ const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
1358
+ if (lastIncompleteCycle?.paneId) {
1359
+ setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
1360
+ }
1361
+ console.log(`[sisyphus] Reconnected session ${sessionId} to tmux window ${session.tmuxWindowId}`);
1362
+ if (session.status === "active" && session.agents.length > 0) {
1363
+ const hasRunningAgents = session.agents.some((a) => a.status === "running");
1364
+ if (!hasRunningAgents) {
1365
+ const livePaneIds = new Set(livePanes.map((p) => p.paneId));
1366
+ const orchestratorPaneId = getOrchestratorPaneId(sessionId);
1367
+ const orchestratorAlive = orchestratorPaneId && livePaneIds.has(orchestratorPaneId);
1368
+ if (!orchestratorAlive) {
1369
+ console.log(`[sisyphus] Detected stuck session ${sessionId} on recovery: triggering orchestrator respawn`);
1370
+ await onAllAgentsDone2(sessionId, cwd, session.tmuxWindowId);
1371
+ }
1372
+ }
1373
+ }
1374
+ } else {
1375
+ if (session.status === "active") {
1376
+ await updateSessionStatus(cwd, sessionId, "paused");
1377
+ console.log(`[sisyphus] Session ${sessionId} paused: tmux window no longer exists`);
1378
+ }
1379
+ }
1380
+ }
988
1381
  recovered++;
989
1382
  }
990
1383
  } catch {
@@ -993,7 +1386,7 @@ function recoverSessions() {
993
1386
  }
994
1387
  console.log(`[sisyphus] Recovered ${recovered} session(s) from registry`);
995
1388
  }
996
- async function main() {
1389
+ async function startDaemon() {
997
1390
  console.log("[sisyphus] Starting daemon...");
998
1391
  ensureDirs();
999
1392
  acquirePidLock();
@@ -1001,7 +1394,7 @@ async function main() {
1001
1394
  setRespawnCallback(onAllAgentsDone2);
1002
1395
  await startServer();
1003
1396
  startMonitor(config.pollIntervalMs);
1004
- recoverSessions();
1397
+ await recoverSessions();
1005
1398
  const shutdown = async () => {
1006
1399
  console.log("[sisyphus] Shutting down...");
1007
1400
  stopMonitor();
@@ -1012,8 +1405,32 @@ async function main() {
1012
1405
  process.on("SIGTERM", shutdown);
1013
1406
  process.on("SIGINT", shutdown);
1014
1407
  }
1015
- main().catch((err) => {
1016
- console.error("[sisyphus] Fatal error:", err);
1017
- process.exit(1);
1018
- });
1408
+ var command = process.argv[2];
1409
+ switch (command) {
1410
+ case "stop":
1411
+ stopDaemon();
1412
+ break;
1413
+ case "restart": {
1414
+ stopDaemon();
1415
+ const wait = Date.now() + 500;
1416
+ while (Date.now() < wait) {
1417
+ }
1418
+ startDaemon().catch((err) => {
1419
+ console.error("[sisyphus] Fatal error:", err);
1420
+ process.exit(1);
1421
+ });
1422
+ break;
1423
+ }
1424
+ case "start":
1425
+ case void 0:
1426
+ startDaemon().catch((err) => {
1427
+ console.error("[sisyphus] Fatal error:", err);
1428
+ process.exit(1);
1429
+ });
1430
+ break;
1431
+ default:
1432
+ console.error(`[sisyphus] Unknown command: ${command}`);
1433
+ console.error("Usage: sisyphusd [start|stop|restart]");
1434
+ process.exit(1);
1435
+ }
1019
1436
  //# sourceMappingURL=daemon.js.map