sisyphi 0.1.1 → 0.1.3

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 +9 -0
  2. package/dist/{chunk-T6Z5F4SP.js → chunk-N2BPQOO2.js} +27 -3
  3. package/dist/chunk-N2BPQOO2.js.map +1 -0
  4. package/dist/cli.js +241 -161
  5. package/dist/cli.js.map +1 -1
  6. package/dist/daemon.js +608 -187
  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 +25 -0
  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 +25 -0
  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-T6Z5F4SP.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-T6Z5F4SP.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 existsSync4 } 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 existsSync3, 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 existsSync2, 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,17 +452,21 @@ ${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)})`);
362
466
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
363
- sendKeys(paneId, `${envExports} && ${claudeCmd}`);
467
+ const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
468
+ const bannerCmd = existsSync(bannerPath) ? `cat '${bannerPath}' &&` : "";
469
+ sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
364
470
  await addOrchestratorCycle(cwd, sessionId, {
365
471
  cycle: cycleNum,
366
472
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -381,6 +487,8 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
381
487
  killPane(paneId);
382
488
  sessionOrchestratorPane.delete(sessionId);
383
489
  }
490
+ const windowId = sessionWindowMap.get(sessionId);
491
+ if (windowId) selectLayout(windowId);
384
492
  await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
385
493
  const session = getSession(cwd, sessionId);
386
494
  const runningAgents = session.agents.filter((a) => a.status === "running");
@@ -389,58 +497,238 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
389
497
  }
390
498
  }
391
499
  async function handleOrchestratorComplete(sessionId, cwd, report) {
392
- const paneId = resolveOrchestratorPane(sessionId, cwd);
393
500
  await completeOrchestratorCycle(cwd, sessionId);
394
501
  await completeSession(cwd, sessionId, report);
395
- if (paneId) {
396
- killPane(paneId);
397
- sessionOrchestratorPane.delete(sessionId);
398
- }
399
- sessionWindowMap.delete(sessionId);
400
502
  console.log(`[sisyphus] Session ${sessionId} completed: ${report}`);
401
503
  }
504
+ function cleanupSessionMaps(sessionId) {
505
+ sessionOrchestratorPane.delete(sessionId);
506
+ sessionWindowMap.delete(sessionId);
507
+ }
402
508
 
403
509
  // src/daemon/agent.ts
404
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
510
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, existsSync as existsSync3 } from "fs";
405
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
406
664
  var agentCounters = /* @__PURE__ */ new Map();
407
- function resetAgentCounter(sessionId, value = 0) {
408
- 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);
409
672
  }
410
673
  function clearAgentCounter(sessionId) {
411
674
  agentCounters.delete(sessionId);
412
675
  }
413
- function renderAgentSuffix(sessionId, instruction) {
676
+ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
414
677
  const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
415
678
  let template;
416
679
  try {
417
- template = readFileSync4(templatePath, "utf-8");
680
+ template = readFileSync6(templatePath, "utf-8");
418
681
  } catch {
419
682
  template = `# Sisyphus Agent
420
683
  Session: {{SESSION_ID}}
421
684
  Task: {{INSTRUCTION}}`;
422
685
  }
423
- 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);
424
695
  }
425
696
  async function spawnAgent(opts) {
426
697
  const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
427
698
  const count = (agentCounters.get(sessionId) ?? 0) + 1;
428
699
  agentCounters.set(sessionId, count);
429
700
  const agentId = `agent-${String(count).padStart(3, "0")}`;
430
- const color = getNextColor(sessionId);
431
- 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);
432
717
  setPaneTitle(paneId, `${name} (${agentId})`);
433
718
  setPaneStyle(paneId, color);
434
- const suffix = renderAgentSuffix(sessionId, instruction);
435
- const suffixFilePath = `${sessionDir(cwd, sessionId)}/${agentId}-system.md`;
719
+ const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
720
+ const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
436
721
  writeFileSync3(suffixFilePath, suffix, "utf-8");
722
+ const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
723
+ const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
437
724
  const envExports = [
438
725
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
439
- `export SISYPHUS_AGENT_ID='${agentId}'`
726
+ `export SISYPHUS_AGENT_ID='${agentId}'`,
727
+ ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : []
440
728
  ].join(" && ");
441
- const agentFlag = agentType ? ` --agent ${shellQuote2(agentType)}` : "";
442
- const claudeCmd = `claude --dangerously-skip-permissions${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote2(instruction)}`;
443
- sendKeys(paneId, `${envExports} && ${claudeCmd}`);
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)}`;
731
+ sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
444
732
  const agent = {
445
733
  id: agentId,
446
734
  name,
@@ -451,7 +739,8 @@ async function spawnAgent(opts) {
451
739
  spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
452
740
  completedAt: null,
453
741
  reports: [],
454
- paneId
742
+ paneId,
743
+ ...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
455
744
  };
456
745
  await addAgent(cwd, sessionId, agent);
457
746
  return agent;
@@ -459,7 +748,7 @@ async function spawnAgent(opts) {
459
748
  function nextReportNumber(cwd, sessionId, agentId) {
460
749
  const dir = reportsDir(cwd, sessionId);
461
750
  try {
462
- 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"));
463
752
  return String(files.length + 1).padStart(3, "0");
464
753
  } catch {
465
754
  return "001";
@@ -467,7 +756,7 @@ function nextReportNumber(cwd, sessionId, agentId) {
467
756
  }
468
757
  async function handleAgentReport(cwd, sessionId, agentId, content) {
469
758
  const dir = reportsDir(cwd, sessionId);
470
- mkdirSync2(dir, { recursive: true });
759
+ mkdirSync3(dir, { recursive: true });
471
760
  const num = nextReportNumber(cwd, sessionId, agentId);
472
761
  const filePath = reportFilePath(cwd, sessionId, agentId, num);
473
762
  writeFileSync3(filePath, content, "utf-8");
@@ -481,7 +770,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
481
770
  }
482
771
  async function handleAgentSubmit(cwd, sessionId, agentId, report) {
483
772
  const dir = reportsDir(cwd, sessionId);
484
- mkdirSync2(dir, { recursive: true });
773
+ mkdirSync3(dir, { recursive: true });
485
774
  const filePath = reportFilePath(cwd, sessionId, agentId, "final");
486
775
  writeFileSync3(filePath, report, "utf-8");
487
776
  const entry = {
@@ -496,10 +785,13 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
496
785
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
497
786
  });
498
787
  const session = getSession(cwd, sessionId);
499
- 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);
500
790
  if (agent) {
501
791
  killPane(agent.paneId);
502
792
  }
793
+ const windowId = getWindowId(sessionId);
794
+ if (windowId) selectLayout(windowId);
503
795
  return allAgentsDone(session);
504
796
  }
505
797
  async function handleAgentKilled(cwd, sessionId, agentId, reason) {
@@ -515,7 +807,7 @@ function allAgentsDone(session) {
515
807
  const running = session.agents.filter((a) => a.status === "running");
516
808
  return running.length === 0 && session.agents.length > 0;
517
809
  }
518
- function shellQuote2(s) {
810
+ function shellQuote3(s) {
519
811
  return `'${s.replace(/'/g, "'\\''")}'`;
520
812
  }
521
813
 
@@ -567,19 +859,38 @@ async function pollSession(sessionId, cwd, windowId) {
567
859
  console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
568
860
  return;
569
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
+ }
570
878
  if (session.status !== "active") return;
571
879
  const livePanes = listPanes(windowId);
572
880
  if (livePanes.length === 0) return;
573
881
  const livePaneIds = new Set(livePanes.map((p) => p.paneId));
882
+ let paneRemoved = false;
574
883
  for (const agent of session.agents) {
575
884
  if (agent.status !== "running") continue;
576
885
  if (!livePaneIds.has(agent.paneId)) {
886
+ paneRemoved = true;
577
887
  const allDone = await handleAgentKilled(cwd, sessionId, agent.id, "pane closed by user");
578
888
  if (allDone && onAllAgentsDone) {
579
889
  onAllAgentsDone(sessionId, cwd, windowId);
580
890
  }
581
891
  }
582
892
  }
893
+ if (paneRemoved) selectLayout(windowId);
583
894
  const orchPaneId = getOrchestratorPaneId(sessionId);
584
895
  if (orchPaneId && !livePaneIds.has(orchPaneId)) {
585
896
  const runningAgents = session.agents.filter((a) => a.status === "running");
@@ -588,12 +899,18 @@ async function pollSession(sessionId, cwd, windowId) {
588
899
  console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
589
900
  }
590
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
+ }
591
907
  }
592
908
 
593
909
  // src/daemon/session-manager.ts
594
910
  async function startSession(task, cwd, tmuxSession, windowId) {
595
911
  const sessionId = uuidv4();
596
912
  const session = createSession(sessionId, task, cwd);
913
+ await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
597
914
  trackSession(sessionId, cwd, tmuxSession);
598
915
  await spawnOrchestrator(sessionId, cwd, windowId);
599
916
  updateTrackedWindow(sessionId, windowId);
@@ -601,17 +918,30 @@ async function startSession(task, cwd, tmuxSession, windowId) {
601
918
  }
602
919
  async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
603
920
  const session = getSession(cwd, sessionId);
604
- for (const agent of session.agents) {
605
- if (agent.status === "running") {
606
- await updateAgent(cwd, sessionId, agent.id, {
607
- status: "lost",
608
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
609
- killedReason: "session resumed \u2014 agent was still running"
610
- });
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
+ }
611
940
  }
612
941
  }
613
942
  await updateSessionStatus(cwd, sessionId, "active");
614
- resetAgentCounter(sessionId, session.agents.length);
943
+ await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
944
+ resetAgentCounterFromState(sessionId, session.agents);
615
945
  resetColors(sessionId);
616
946
  trackSession(sessionId, cwd, tmuxSession);
617
947
  await spawnOrchestrator(sessionId, cwd, windowId, message);
@@ -623,8 +953,8 @@ function getSessionStatus(cwd, sessionId) {
623
953
  }
624
954
  function listSessions(cwd) {
625
955
  const dir = sessionsDir(cwd);
626
- if (!existsSync2(dir)) return [];
627
- const entries = readdirSync3(dir, { withFileTypes: true });
956
+ if (!existsSync4(dir)) return [];
957
+ const entries = readdirSync4(dir, { withFileTypes: true });
628
958
  const sessions = [];
629
959
  for (const entry of entries) {
630
960
  if (!entry.isDirectory()) continue;
@@ -646,11 +976,22 @@ function listSessions(cwd) {
646
976
  function onAllAgentsDone2(sessionId, cwd, windowId) {
647
977
  const session = getSession(cwd, sessionId);
648
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
+ }
649
990
  setTimeout(() => {
650
991
  spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
651
992
  }, 2e3);
652
993
  }
653
- async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
994
+ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
654
995
  const windowId = getWindowId(sessionId);
655
996
  if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
656
997
  const agent = await spawnAgent({
@@ -659,7 +1000,8 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction) {
659
1000
  agentType,
660
1001
  name,
661
1002
  instruction,
662
- windowId
1003
+ windowId,
1004
+ worktree
663
1005
  });
664
1006
  await appendAgentToLastCycle(cwd, sessionId, agent.id);
665
1007
  return { agentId: agent.id };
@@ -677,29 +1019,8 @@ async function handleYield(sessionId, cwd, nextPrompt) {
677
1019
  await handleOrchestratorYield(sessionId, cwd, nextPrompt);
678
1020
  }
679
1021
  async function handleComplete(sessionId, cwd, report) {
680
- untrackSession(sessionId);
681
1022
  await handleOrchestratorComplete(sessionId, cwd, report);
682
1023
  }
683
- async function handleTaskAdd(cwd, sessionId, description, initialStatus) {
684
- const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
685
- const status = initialStatus !== void 0 && VALID_STATUSES.has(initialStatus) ? initialStatus : void 0;
686
- const task = await addTask(cwd, sessionId, description, status);
687
- return { taskId: task.id };
688
- }
689
- async function handleTaskUpdate(cwd, sessionId, taskId, status, description) {
690
- const VALID_STATUSES = /* @__PURE__ */ new Set(["draft", "pending", "in_progress", "done"]);
691
- const updates = {};
692
- if (status !== void 0) {
693
- if (!VALID_STATUSES.has(status)) throw new Error(`Invalid status: ${status}. Valid: draft, pending, in_progress, done`);
694
- updates.status = status;
695
- }
696
- if (description !== void 0) updates.description = description;
697
- await updateTask(cwd, sessionId, taskId, updates);
698
- }
699
- function handleTasksList(cwd, sessionId) {
700
- const session = getSession(cwd, sessionId);
701
- return { tasks: session.tasks };
702
- }
703
1024
  async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
704
1025
  await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
705
1026
  }
@@ -717,6 +1038,11 @@ async function handleKill(sessionId, cwd) {
717
1038
  killedAgents++;
718
1039
  }
719
1040
  }
1041
+ for (const agent of session.agents) {
1042
+ if (agent.worktreePath && agent.branchName) {
1043
+ cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1044
+ }
1045
+ }
720
1046
  const orchPaneId = getOrchestratorPaneId(sessionId);
721
1047
  if (orchPaneId) {
722
1048
  killPane(orchPaneId);
@@ -736,11 +1062,11 @@ var sessionCwdMap = /* @__PURE__ */ new Map();
736
1062
  var sessionTmuxMap = /* @__PURE__ */ new Map();
737
1063
  var sessionWindowMap2 = /* @__PURE__ */ new Map();
738
1064
  function registryPath() {
739
- return join2(globalDir(), "session-registry.json");
1065
+ return join4(globalDir(), "session-registry.json");
740
1066
  }
741
1067
  function persistSessionRegistry() {
742
1068
  const dir = globalDir();
743
- mkdirSync3(dir, { recursive: true });
1069
+ mkdirSync4(dir, { recursive: true });
744
1070
  const registry = {};
745
1071
  for (const [id, cwd] of sessionCwdMap) {
746
1072
  registry[id] = cwd;
@@ -749,9 +1075,9 @@ function persistSessionRegistry() {
749
1075
  }
750
1076
  function loadSessionRegistry() {
751
1077
  const p = registryPath();
752
- if (!existsSync3(p)) return {};
1078
+ if (!existsSync5(p)) return {};
753
1079
  try {
754
- return JSON.parse(readFileSync5(p, "utf-8"));
1080
+ return JSON.parse(readFileSync7(p, "utf-8"));
755
1081
  } catch {
756
1082
  return {};
757
1083
  }
@@ -760,6 +1086,10 @@ function registerSessionCwd(sessionId, cwd) {
760
1086
  sessionCwdMap.set(sessionId, cwd);
761
1087
  persistSessionRegistry();
762
1088
  }
1089
+ function registerSessionTmux(sessionId, tmuxSession, windowId) {
1090
+ sessionTmuxMap.set(sessionId, tmuxSession);
1091
+ sessionWindowMap2.set(sessionId, windowId);
1092
+ }
763
1093
  async function handleRequest(req) {
764
1094
  try {
765
1095
  switch (req.type) {
@@ -773,7 +1103,7 @@ async function handleRequest(req) {
773
1103
  case "spawn": {
774
1104
  const cwd = sessionCwdMap.get(req.sessionId);
775
1105
  if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
776
- 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);
777
1107
  return { ok: true, data: { agentId: result.agentId } };
778
1108
  }
779
1109
  case "submit": {
@@ -811,32 +1141,29 @@ async function handleRequest(req) {
811
1141
  }
812
1142
  return { ok: true, data: { message: "daemon running" } };
813
1143
  }
814
- case "tasks_add": {
815
- const cwd = sessionCwdMap.get(req.sessionId);
816
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
817
- const result = await handleTaskAdd(cwd, req.sessionId, req.description, req.status);
818
- return { ok: true, data: { taskId: result.taskId } };
819
- }
820
- case "tasks_update": {
821
- const cwd = sessionCwdMap.get(req.sessionId);
822
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
823
- await handleTaskUpdate(cwd, req.sessionId, req.taskId, req.status, req.description);
824
- return { ok: true };
825
- }
826
- case "tasks_list": {
827
- const cwd = sessionCwdMap.get(req.sessionId);
828
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
829
- const result = handleTasksList(cwd, req.sessionId);
830
- return { ok: true, data: { tasks: result.tasks } };
831
- }
832
1144
  case "list": {
833
1145
  const allSessions = [];
834
- const seenCwds = /* @__PURE__ */ new Set();
835
- for (const cwd of sessionCwdMap.values()) {
836
- if (seenCwds.has(cwd)) continue;
837
- seenCwds.add(cwd);
838
- const sessions = listSessions(cwd);
839
- 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
+ }
840
1167
  }
841
1168
  return { ok: true, data: { sessions: allSessions } };
842
1169
  }
@@ -844,7 +1171,7 @@ async function handleRequest(req) {
844
1171
  let cwd = sessionCwdMap.get(req.sessionId);
845
1172
  if (!cwd) {
846
1173
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
847
- if (existsSync3(stateFile)) {
1174
+ if (existsSync5(stateFile)) {
848
1175
  cwd = req.cwd;
849
1176
  registerSessionCwd(req.sessionId, cwd);
850
1177
  } else {
@@ -883,7 +1210,7 @@ async function handleRequest(req) {
883
1210
  function startServer() {
884
1211
  return new Promise((resolve3, reject) => {
885
1212
  const sock = socketPath();
886
- if (existsSync3(sock)) {
1213
+ if (existsSync5(sock)) {
887
1214
  unlinkSync(sock);
888
1215
  }
889
1216
  server = createServer((conn) => {
@@ -925,7 +1252,7 @@ function stopServer() {
925
1252
  }
926
1253
  server.close(() => {
927
1254
  const sock = socketPath();
928
- if (existsSync3(sock)) {
1255
+ if (existsSync5(sock)) {
929
1256
  unlinkSync(sock);
930
1257
  }
931
1258
  server = null;
@@ -936,7 +1263,7 @@ function stopServer() {
936
1263
 
937
1264
  // src/daemon/index.ts
938
1265
  function ensureDirs() {
939
- mkdirSync4(globalDir(), { recursive: true });
1266
+ mkdirSync5(globalDir(), { recursive: true });
940
1267
  }
941
1268
  function isProcessAlive(pid) {
942
1269
  try {
@@ -946,17 +1273,22 @@ function isProcessAlive(pid) {
946
1273
  return false;
947
1274
  }
948
1275
  }
949
- function acquirePidLock() {
1276
+ function readPid() {
950
1277
  const pidFile = daemonPidPath();
951
1278
  try {
952
- const existing = parseInt(readFileSync6(pidFile, "utf-8").trim(), 10);
953
- if (existing && isProcessAlive(existing)) {
954
- console.error(`[sisyphus] Daemon already running (pid ${existing}). Kill it first or remove ${pidFile}`);
955
- process.exit(1);
956
- }
1279
+ const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
1280
+ return pid && isProcessAlive(pid) ? pid : null;
957
1281
  } catch {
1282
+ return null;
958
1283
  }
959
- 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");
960
1292
  }
961
1293
  function releasePidLock() {
962
1294
  try {
@@ -964,7 +1296,40 @@ function releasePidLock() {
964
1296
  } catch {
965
1297
  }
966
1298
  }
967
- 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() {
968
1333
  const registry = loadSessionRegistry();
969
1334
  const entries = Object.entries(registry);
970
1335
  if (entries.length === 0) {
@@ -974,13 +1339,45 @@ function recoverSessions() {
974
1339
  let recovered = 0;
975
1340
  for (const [sessionId, cwd] of entries) {
976
1341
  const stateFile = statePath(cwd, sessionId);
977
- if (!existsSync4(stateFile)) {
1342
+ if (!existsSync6(stateFile)) {
978
1343
  continue;
979
1344
  }
980
1345
  try {
981
- const session = JSON.parse(readFileSync6(stateFile, "utf-8"));
1346
+ const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
982
1347
  if (session.status === "active" || session.status === "paused") {
983
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
+ }
984
1381
  recovered++;
985
1382
  }
986
1383
  } catch {
@@ -989,7 +1386,7 @@ function recoverSessions() {
989
1386
  }
990
1387
  console.log(`[sisyphus] Recovered ${recovered} session(s) from registry`);
991
1388
  }
992
- async function main() {
1389
+ async function startDaemon() {
993
1390
  console.log("[sisyphus] Starting daemon...");
994
1391
  ensureDirs();
995
1392
  acquirePidLock();
@@ -997,7 +1394,7 @@ async function main() {
997
1394
  setRespawnCallback(onAllAgentsDone2);
998
1395
  await startServer();
999
1396
  startMonitor(config.pollIntervalMs);
1000
- recoverSessions();
1397
+ await recoverSessions();
1001
1398
  const shutdown = async () => {
1002
1399
  console.log("[sisyphus] Shutting down...");
1003
1400
  stopMonitor();
@@ -1008,8 +1405,32 @@ async function main() {
1008
1405
  process.on("SIGTERM", shutdown);
1009
1406
  process.on("SIGINT", shutdown);
1010
1407
  }
1011
- main().catch((err) => {
1012
- console.error("[sisyphus] Fatal error:", err);
1013
- process.exit(1);
1014
- });
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
+ }
1015
1436
  //# sourceMappingURL=daemon.js.map