sisyphi 0.1.21 → 0.1.23

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 (60) hide show
  1. package/dist/chunk-KQBSC5KY.js +31 -0
  2. package/dist/chunk-KQBSC5KY.js.map +1 -0
  3. package/dist/{chunk-LTAW6OWS.js → chunk-YGBGKMTF.js} +31 -6
  4. package/dist/chunk-YGBGKMTF.js.map +1 -0
  5. package/dist/chunk-ZE2SKB4B.js +35 -0
  6. package/dist/chunk-ZE2SKB4B.js.map +1 -0
  7. package/dist/cli.js +638 -51
  8. package/dist/cli.js.map +1 -1
  9. package/dist/daemon.js +915 -289
  10. package/dist/daemon.js.map +1 -1
  11. package/dist/paths-FYYSBD27.js +58 -0
  12. package/dist/paths-FYYSBD27.js.map +1 -0
  13. package/dist/templates/CLAUDE.md +21 -20
  14. package/dist/templates/agent-plugin/agents/CLAUDE.md +2 -0
  15. package/dist/templates/agent-plugin/agents/debug.md +1 -0
  16. package/dist/templates/agent-plugin/agents/operator.md +1 -2
  17. package/dist/templates/agent-plugin/agents/plan.md +86 -55
  18. package/dist/templates/agent-plugin/agents/review-plan.md +1 -0
  19. package/dist/templates/agent-plugin/agents/spec-draft.md +1 -0
  20. package/dist/templates/agent-plugin/hooks/hooks.json +19 -1
  21. package/dist/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
  22. package/dist/templates/agent-plugin/hooks/require-submit.sh +24 -0
  23. package/dist/templates/agent-suffix.md +18 -0
  24. package/dist/templates/dashboard-claude.md +38 -0
  25. package/dist/templates/orchestrator-base.md +270 -0
  26. package/dist/templates/orchestrator-impl.md +116 -0
  27. package/dist/templates/orchestrator-planning.md +131 -0
  28. package/dist/templates/orchestrator-plugin/hooks/hooks.json +1 -15
  29. package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +1 -1
  30. package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +4 -16
  31. package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +22 -23
  32. package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +11 -11
  33. package/dist/tui.js +3236 -0
  34. package/dist/tui.js.map +1 -0
  35. package/package.json +5 -1
  36. package/templates/CLAUDE.md +21 -20
  37. package/templates/agent-plugin/agents/CLAUDE.md +2 -0
  38. package/templates/agent-plugin/agents/debug.md +1 -0
  39. package/templates/agent-plugin/agents/operator.md +1 -2
  40. package/templates/agent-plugin/agents/plan.md +86 -55
  41. package/templates/agent-plugin/agents/review-plan.md +1 -0
  42. package/templates/agent-plugin/agents/spec-draft.md +1 -0
  43. package/templates/agent-plugin/hooks/hooks.json +19 -1
  44. package/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
  45. package/templates/agent-plugin/hooks/require-submit.sh +24 -0
  46. package/templates/agent-suffix.md +18 -0
  47. package/templates/dashboard-claude.md +38 -0
  48. package/templates/orchestrator-base.md +270 -0
  49. package/templates/orchestrator-impl.md +116 -0
  50. package/templates/orchestrator-planning.md +131 -0
  51. package/templates/orchestrator-plugin/hooks/hooks.json +1 -15
  52. package/templates/orchestrator-plugin/skills/git-management/SKILL.md +1 -1
  53. package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +4 -16
  54. package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +22 -23
  55. package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +11 -11
  56. package/dist/chunk-LTAW6OWS.js.map +0 -1
  57. package/dist/templates/orchestrator-plugin/scripts/block-task.sh +0 -11
  58. package/dist/templates/orchestrator.md +0 -173
  59. package/templates/orchestrator-plugin/scripts/block-task.sh +0 -11
  60. package/templates/orchestrator.md +0 -173
package/dist/daemon.js CHANGED
@@ -1,73 +1,53 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ loadConfig
4
+ } from "./chunk-KQBSC5KY.js";
2
5
  import {
3
6
  contextDir,
7
+ cycleLogPath,
4
8
  daemonPidPath,
5
9
  daemonUpdatingPath,
6
- globalConfigPath,
7
10
  globalDir,
8
- logsPath,
9
- planPath,
10
- projectConfigPath,
11
+ goalPath,
12
+ legacyLogsPath,
13
+ logsDir,
14
+ messagesDir,
11
15
  projectOrchestratorPromptPath,
12
16
  promptsDir,
13
17
  reportFilePath,
14
18
  reportsDir,
19
+ roadmapPath,
15
20
  sessionDir,
16
21
  sessionsDir,
22
+ snapshotDir,
23
+ snapshotsDir,
17
24
  socketPath,
18
25
  statePath,
19
26
  worktreeBaseDir,
20
27
  worktreeConfigPath
21
- } from "./chunk-LTAW6OWS.js";
28
+ } from "./chunk-YGBGKMTF.js";
22
29
 
23
30
  // src/daemon/index.ts
24
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, existsSync as existsSync7 } from "fs";
31
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
25
32
  import { execSync as execSync4 } from "child_process";
26
33
  import { setTimeout as sleep } from "timers/promises";
27
34
 
28
- // src/shared/config.ts
29
- import { readFileSync } from "fs";
30
- var DEFAULT_CONFIG = {
31
- pollIntervalMs: 5e3
32
- };
33
- function readJsonFile(filePath) {
34
- try {
35
- const content = readFileSync(filePath, "utf-8");
36
- return JSON.parse(content);
37
- } catch {
38
- return {};
39
- }
40
- }
41
- function loadConfig(cwd) {
42
- const global = readJsonFile(globalConfigPath());
43
- const project = readJsonFile(projectConfigPath(cwd));
44
- return { ...DEFAULT_CONFIG, ...global, ...project };
45
- }
46
-
47
35
  // src/daemon/server.ts
48
36
  import { createServer } from "net";
49
- import { unlinkSync, existsSync as existsSync6, writeFileSync as writeFileSync4, readFileSync as readFileSync7, mkdirSync as mkdirSync4 } from "fs";
50
- import { join as join4 } from "path";
37
+ import { unlinkSync, existsSync as existsSync7, writeFileSync as writeFileSync4, readFileSync as readFileSync6, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
38
+ import { join as join5 } from "path";
51
39
 
52
40
  // src/daemon/session-manager.ts
53
41
  import { v4 as uuidv4 } from "uuid";
54
- import { existsSync as existsSync5, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
42
+ import { existsSync as existsSync6, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
55
43
 
56
44
  // src/daemon/state.ts
57
45
  import { randomUUID } from "crypto";
58
- import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
46
+ import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "fs";
59
47
  import { dirname, join } from "path";
60
- var PLAN_SEED = `---
48
+ var ROADMAP_SEED = `---
61
49
  description: >
62
- Living document of what still needs to happen. Write out ne
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.
50
+ Living document tracking development phases and outstanding work.
71
51
  ---
72
52
  `;
73
53
  var CONTEXT_CLAUDE_MD = `# context/
@@ -95,29 +75,32 @@ function atomicWrite(filePath, data) {
95
75
  writeFileSync(tmpPath, data, "utf-8");
96
76
  renameSync(tmpPath, filePath);
97
77
  }
98
- function createSession(id, task, cwd, context) {
78
+ function createSession(id, task, cwd, context, name) {
99
79
  const dir = sessionDir(cwd, id);
100
80
  mkdirSync(dir, { recursive: true });
101
81
  mkdirSync(contextDir(cwd, id), { recursive: true });
102
82
  mkdirSync(promptsDir(cwd, id), { recursive: true });
103
- writeFileSync(planPath(cwd, id), PLAN_SEED, "utf-8");
104
- writeFileSync(logsPath(cwd, id), LOGS_SEED, "utf-8");
83
+ writeFileSync(roadmapPath(cwd, id), ROADMAP_SEED, "utf-8");
84
+ mkdirSync(logsDir(cwd, id), { recursive: true });
85
+ writeFileSync(goalPath(cwd, id), task, "utf-8");
105
86
  writeFileSync(join(contextDir(cwd, id), "CLAUDE.md"), CONTEXT_CLAUDE_MD, "utf-8");
106
87
  const session = {
107
88
  id,
89
+ ...name ? { name } : {},
108
90
  task,
109
91
  ...context ? { context } : {},
110
92
  cwd,
111
93
  status: "active",
112
94
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
113
95
  agents: [],
114
- orchestratorCycles: []
96
+ orchestratorCycles: [],
97
+ messages: []
115
98
  };
116
99
  atomicWrite(statePath(cwd, id), JSON.stringify(session, null, 2));
117
100
  return session;
118
101
  }
119
102
  function getSession(cwd, sessionId) {
120
- const content = readFileSync2(statePath(cwd, sessionId), "utf-8");
103
+ const content = readFileSync(statePath(cwd, sessionId), "utf-8");
121
104
  return JSON.parse(content);
122
105
  }
123
106
  function saveSession(session) {
@@ -174,6 +157,23 @@ async function completeSession(cwd, sessionId, report) {
174
157
  saveSession(session);
175
158
  });
176
159
  }
160
+ async function continueSession(cwd, sessionId) {
161
+ return withSessionLock(sessionId, () => {
162
+ const session = getSession(cwd, sessionId);
163
+ if (session.status !== "completed") {
164
+ throw new Error(`Session ${sessionId} is not completed (status: ${session.status})`);
165
+ }
166
+ session.status = "active";
167
+ session.completedAt = void 0;
168
+ session.completionReport = void 0;
169
+ const cycles = session.orchestratorCycles;
170
+ if (cycles.length > 0) {
171
+ cycles[cycles.length - 1].completedAt = void 0;
172
+ }
173
+ saveSession(session);
174
+ writeFileSync(roadmapPath(cwd, sessionId), "", "utf-8");
175
+ });
176
+ }
177
177
  async function appendAgentReport(cwd, sessionId, agentId, entry) {
178
178
  return withSessionLock(sessionId, () => {
179
179
  const session = getSession(cwd, sessionId);
@@ -183,6 +183,18 @@ async function appendAgentReport(cwd, sessionId, agentId, entry) {
183
183
  saveSession(session);
184
184
  });
185
185
  }
186
+ async function updateReportSummary(cwd, sessionId, agentId, filePath, summary) {
187
+ return withSessionLock(sessionId, () => {
188
+ const session = getSession(cwd, sessionId);
189
+ const agent = session.agents.slice().reverse().find((a) => a.id === agentId);
190
+ if (!agent) return;
191
+ const report = agent.reports.find((r) => r.filePath === filePath);
192
+ if (report) {
193
+ report.summary = summary;
194
+ saveSession(session);
195
+ }
196
+ });
197
+ }
186
198
  async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId) {
187
199
  return withSessionLock(sessionId, () => {
188
200
  const session = getSession(cwd, sessionId);
@@ -191,7 +203,31 @@ async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId)
191
203
  saveSession(session);
192
204
  });
193
205
  }
194
- async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
206
+ async function drainMessages(cwd, sessionId, count) {
207
+ return withSessionLock(sessionId, () => {
208
+ const session = getSession(cwd, sessionId);
209
+ if (!session.messages || count <= 0) return;
210
+ session.messages = session.messages.slice(count);
211
+ saveSession(session);
212
+ });
213
+ }
214
+ async function appendMessage(cwd, sessionId, message) {
215
+ return withSessionLock(sessionId, () => {
216
+ const session = getSession(cwd, sessionId);
217
+ if (!session.messages) session.messages = [];
218
+ session.messages.push(message);
219
+ saveSession(session);
220
+ });
221
+ }
222
+ async function updateTask(cwd, sessionId, task) {
223
+ return withSessionLock(sessionId, () => {
224
+ const session = getSession(cwd, sessionId);
225
+ session.task = task;
226
+ saveSession(session);
227
+ writeFileSync(goalPath(cwd, sessionId), task, "utf-8");
228
+ });
229
+ }
230
+ async function completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode) {
195
231
  return withSessionLock(sessionId, () => {
196
232
  const session = getSession(cwd, sessionId);
197
233
  const cycles = session.orchestratorCycles;
@@ -199,13 +235,66 @@ async function completeOrchestratorCycle(cwd, sessionId, nextPrompt) {
199
235
  const cycle = cycles[cycles.length - 1];
200
236
  cycle.completedAt = (/* @__PURE__ */ new Date()).toISOString();
201
237
  if (nextPrompt) cycle.nextPrompt = nextPrompt;
238
+ if (mode) cycle.mode = mode;
202
239
  saveSession(session);
203
240
  });
204
241
  }
242
+ function createSnapshot(cwd, sessionId, cycleNumber) {
243
+ const dir = snapshotDir(cwd, sessionId, cycleNumber);
244
+ mkdirSync(dir, { recursive: true });
245
+ copyFileSync(statePath(cwd, sessionId), join(dir, "state.json"));
246
+ const roadmap = roadmapPath(cwd, sessionId);
247
+ if (existsSync(roadmap)) copyFileSync(roadmap, join(dir, "roadmap.md"));
248
+ const ld = logsDir(cwd, sessionId);
249
+ if (existsSync(ld)) cpSync(ld, join(dir, "logs"), { recursive: true });
250
+ const legacyLogs = legacyLogsPath(cwd, sessionId);
251
+ if (existsSync(legacyLogs)) copyFileSync(legacyLogs, join(dir, "logs.md"));
252
+ }
253
+ async function restoreSnapshot(cwd, sessionId, toCycle) {
254
+ return withSessionLock(sessionId, () => {
255
+ const dir = snapshotDir(cwd, sessionId, toCycle);
256
+ if (!existsSync(dir)) throw new Error(`No snapshot found for cycle ${toCycle}`);
257
+ const snapshotState = readFileSync(join(dir, "state.json"), "utf-8");
258
+ const session = JSON.parse(snapshotState);
259
+ session.status = "paused";
260
+ session.completedAt = void 0;
261
+ session.completionReport = void 0;
262
+ session.tmuxSessionName = void 0;
263
+ session.tmuxWindowId = void 0;
264
+ atomicWrite(statePath(cwd, sessionId), JSON.stringify(session, null, 2));
265
+ const snapshotRoadmap = join(dir, "roadmap.md");
266
+ if (existsSync(snapshotRoadmap)) copyFileSync(snapshotRoadmap, roadmapPath(cwd, sessionId));
267
+ const snapshotLogsDir = join(dir, "logs");
268
+ if (existsSync(snapshotLogsDir)) {
269
+ const currentLogsDir = logsDir(cwd, sessionId);
270
+ if (existsSync(currentLogsDir)) rmSync(currentLogsDir, { recursive: true, force: true });
271
+ cpSync(snapshotLogsDir, currentLogsDir, { recursive: true });
272
+ } else {
273
+ const snapshotLogs = join(dir, "logs.md");
274
+ if (existsSync(snapshotLogs)) copyFileSync(snapshotLogs, legacyLogsPath(cwd, sessionId));
275
+ }
276
+ });
277
+ }
278
+ function listSnapshots(cwd, sessionId) {
279
+ const dir = snapshotsDir(cwd, sessionId);
280
+ if (!existsSync(dir)) return [];
281
+ return readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("cycle-")).map((e) => parseInt(e.name.replace("cycle-", ""), 10)).filter((n) => !isNaN(n)).sort((a, b) => a - b);
282
+ }
283
+ function deleteSnapshotsAfter(cwd, sessionId, afterCycle) {
284
+ const dir = snapshotsDir(cwd, sessionId);
285
+ if (!existsSync(dir)) return;
286
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
287
+ if (!entry.isDirectory() || !entry.name.startsWith("cycle-")) continue;
288
+ const num = parseInt(entry.name.replace("cycle-", ""), 10);
289
+ if (!isNaN(num) && num > afterCycle) {
290
+ rmSync(join(dir, entry.name), { recursive: true, force: true });
291
+ }
292
+ }
293
+ }
205
294
 
206
295
  // src/daemon/orchestrator.ts
207
- import { existsSync, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
208
- import { resolve } from "path";
296
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
297
+ import { resolve, join as join3 } from "path";
209
298
 
210
299
  // src/daemon/colors.ts
211
300
  var ORCHESTRATOR_COLOR = "yellow";
@@ -228,6 +317,140 @@ function resetColors(sessionId) {
228
317
  sessionColorIndex.delete(sessionId);
229
318
  }
230
319
 
320
+ // src/daemon/frontmatter.ts
321
+ import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
322
+ import { homedir } from "os";
323
+ import { join as join2, basename } from "path";
324
+ function detectProvider(model) {
325
+ if (!model) return "anthropic";
326
+ if (/^(gpt-|codex-)/.test(model)) return "openai";
327
+ return "anthropic";
328
+ }
329
+ function parseAgentFrontmatter(content) {
330
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
331
+ if (!match) return {};
332
+ const block = match[1];
333
+ const fm = {};
334
+ const str = (key) => {
335
+ const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
336
+ return m ? m[1].trim() : void 0;
337
+ };
338
+ fm.name = str("name");
339
+ fm.model = str("model");
340
+ fm.color = str("color");
341
+ fm.description = str("description");
342
+ fm.permissionMode = str("permissionMode");
343
+ fm.effort = str("effort");
344
+ const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
345
+ if (skillsMatch) {
346
+ fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
347
+ }
348
+ return fm;
349
+ }
350
+ function extractAgentBody(content) {
351
+ const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
352
+ return match ? match[1].trim() : content.trim();
353
+ }
354
+ function findPluginInstallPath(namespace) {
355
+ try {
356
+ const registryPath2 = join2(homedir(), ".claude", "plugins", "installed_plugins.json");
357
+ const registry = JSON.parse(readFileSync2(registryPath2, "utf-8"));
358
+ for (const key of Object.keys(registry)) {
359
+ if (key.startsWith(`${namespace}@`)) {
360
+ return registry[key].installPath ?? null;
361
+ }
362
+ }
363
+ } catch {
364
+ }
365
+ return null;
366
+ }
367
+ function resolveAgentTypePath(agentType, pluginDir, cwd) {
368
+ if (!agentType) return null;
369
+ let namespace;
370
+ let name;
371
+ if (agentType.includes(":")) {
372
+ [namespace, name] = agentType.split(":", 2);
373
+ } else {
374
+ name = agentType;
375
+ }
376
+ const searchPaths = [];
377
+ if (namespace) {
378
+ searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
379
+ const installPath = findPluginInstallPath(namespace);
380
+ if (installPath) {
381
+ searchPaths.push(join2(installPath, "agents", `${name}.md`));
382
+ }
383
+ } else {
384
+ searchPaths.push(join2(cwd, ".claude", "agents", `${name}.md`));
385
+ searchPaths.push(join2(homedir(), ".claude", "agents", `${name}.md`));
386
+ searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
387
+ }
388
+ for (const path of searchPaths) {
389
+ if (existsSync2(path)) return path;
390
+ }
391
+ return null;
392
+ }
393
+ function discoverAgentTypes(pluginDir, cwd) {
394
+ const seen = /* @__PURE__ */ new Set();
395
+ const results = [];
396
+ function scanDir(dir, prefix, source) {
397
+ let files;
398
+ try {
399
+ files = readdirSync2(dir);
400
+ } catch {
401
+ return;
402
+ }
403
+ for (const file of files) {
404
+ if (!file.endsWith(".md") || file === "CLAUDE.md") continue;
405
+ const name = basename(file, ".md");
406
+ const qualifiedName = prefix ? `${prefix}:${name}` : name;
407
+ if (seen.has(qualifiedName)) continue;
408
+ seen.add(qualifiedName);
409
+ try {
410
+ const content = readFileSync2(join2(dir, file), "utf-8");
411
+ const fm = parseAgentFrontmatter(content);
412
+ results.push({ qualifiedName, source, description: fm.description, model: fm.model });
413
+ } catch {
414
+ results.push({ qualifiedName, source });
415
+ }
416
+ }
417
+ }
418
+ scanDir(join2(cwd, ".claude", "agents"), null, "project");
419
+ scanDir(join2(homedir(), ".claude", "agents"), null, "user");
420
+ scanDir(join2(pluginDir, "agents"), "sisyphus", "bundled");
421
+ try {
422
+ const registryPath2 = join2(homedir(), ".claude", "plugins", "installed_plugins.json");
423
+ const registry = JSON.parse(readFileSync2(registryPath2, "utf-8"));
424
+ const pluginEntries = registry.plugins ?? registry;
425
+ for (const key of Object.keys(pluginEntries)) {
426
+ const atIdx = key.indexOf("@");
427
+ if (atIdx < 1) continue;
428
+ const namespace = key.slice(0, atIdx);
429
+ const entry = pluginEntries[key];
430
+ const installPath = Array.isArray(entry) ? entry[0]?.installPath : entry?.installPath;
431
+ if (installPath) {
432
+ scanDir(join2(installPath, "agents"), namespace, "plugin");
433
+ }
434
+ }
435
+ } catch {
436
+ }
437
+ return results;
438
+ }
439
+ function resolveAgentConfig(agentType, pluginDir, cwd) {
440
+ const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
441
+ if (!filePath) return null;
442
+ try {
443
+ const content = readFileSync2(filePath, "utf-8");
444
+ return {
445
+ frontmatter: parseAgentFrontmatter(content),
446
+ body: extractAgentBody(content),
447
+ filePath
448
+ };
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+
231
454
  // src/daemon/tmux.ts
232
455
  import { execSync } from "child_process";
233
456
  var EXEC_ENV = {
@@ -239,7 +462,7 @@ function exec(cmd) {
239
462
  }
240
463
  function execSafe(cmd) {
241
464
  try {
242
- return exec(cmd);
465
+ return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV, stdio: ["pipe", "pipe", "pipe"] }).trim();
243
466
  } catch {
244
467
  return null;
245
468
  }
@@ -263,6 +486,24 @@ function killPane(paneTarget) {
263
486
  function killWindow(windowTarget) {
264
487
  execSafe(`tmux kill-window -t "${windowTarget}"`);
265
488
  }
489
+ function createSession2(sessionName, windowName, cwd) {
490
+ exec(`tmux new-session -d -s "${sessionName}" -n "${windowName}" -c ${shellQuote(cwd)}`);
491
+ const windowId = exec(`tmux display-message -t "${sessionName}:${windowName}" -p "#{window_id}"`);
492
+ const initialPaneId = exec(`tmux display-message -t "${sessionName}:${windowName}" -p "#{pane_id}"`);
493
+ return { windowId, initialPaneId };
494
+ }
495
+ function paneExists(paneTarget) {
496
+ return execSafe(`tmux display-message -t "${paneTarget}" -p "#{pane_id}"`) !== null;
497
+ }
498
+ function sessionExists(sessionName) {
499
+ return execSafe(`tmux has-session -t "${sessionName}"`) !== null;
500
+ }
501
+ function killSession(sessionName) {
502
+ execSafe(`tmux kill-session -t "${sessionName}"`);
503
+ }
504
+ function setSessionOption(sessionName, option, value) {
505
+ execSafe(`tmux set-option -t "${sessionName}" ${option} ${shellQuote(value)}`);
506
+ }
266
507
  function listPanes(windowTarget) {
267
508
  const output = execSafe(`tmux list-panes -t "${windowTarget}" -F "#{pane_id} #{pane_pid}"`);
268
509
  if (!output) return [];
@@ -330,135 +571,206 @@ function getOrchestratorPaneId(sessionId) {
330
571
  function setOrchestratorPaneId(sessionId, paneId) {
331
572
  sessionOrchestratorPane.set(sessionId, paneId);
332
573
  }
333
- function loadOrchestratorPrompt(cwd) {
574
+ function loadOrchestratorPrompt(cwd, mode) {
334
575
  const projectPath = projectOrchestratorPromptPath(cwd);
335
- if (existsSync(projectPath)) {
576
+ if (existsSync3(projectPath)) {
336
577
  return readFileSync3(projectPath, "utf-8");
337
578
  }
338
- const bundledPath = resolve(import.meta.dirname, "../templates/orchestrator.md");
339
- return readFileSync3(bundledPath, "utf-8");
579
+ const basePath = resolve(import.meta.dirname, "../templates/orchestrator-base.md");
580
+ const base = readFileSync3(basePath, "utf-8");
581
+ if (mode === "implementation") {
582
+ const implPath = resolve(import.meta.dirname, "../templates/orchestrator-impl.md");
583
+ return base + "\n\n" + readFileSync3(implPath, "utf-8");
584
+ }
585
+ const planningPath = resolve(import.meta.dirname, "../templates/orchestrator-planning.md");
586
+ return base + "\n\n" + readFileSync3(planningPath, "utf-8");
340
587
  }
341
588
  function formatStateForOrchestrator(session) {
342
- const shortId = session.id.slice(0, 8);
343
589
  const cycleNum = session.orchestratorCycles.length;
344
590
  const ctxDir = contextDir(session.cwd, session.id);
345
- let contextLines;
346
- if (existsSync(ctxDir)) {
347
- const files = readdirSync(ctxDir);
348
- contextLines = files.length > 0 ? files.map((f) => `- ${f}`).join("\n") : " (none)";
591
+ const roadmapFile = roadmapPath(session.cwd, session.id);
592
+ const logFile = cycleLogPath(session.cwd, session.id, cycleNum + 1);
593
+ let contextSection = "";
594
+ if (cycleNum === 0) {
595
+ if (session.context) {
596
+ contextSection = `
597
+ ## Context
598
+
599
+ ${session.context}
600
+ `;
601
+ }
349
602
  } else {
350
- contextLines = " (none)";
351
- }
352
- const planFile = planPath(session.cwd, session.id);
353
- const planRef = existsSync(planFile) ? `@${planFile}` : "(empty)";
354
- const logsFile = logsPath(session.cwd, session.id);
355
- const logsRef = existsSync(logsFile) ? `@${logsFile}` : "(empty)";
356
- const agentLines = session.agents.length > 0 ? session.agents.map((a) => {
357
- const header = `- ${a.id} (${a.name}): ${a.status} \u2014 ${a.reports.length} report(s)`;
358
- if (a.reports.length === 0) return header;
359
- let updateNum = 0;
360
- const reportLines = a.reports.map((r) => {
361
- const label = r.type === "final" ? "[final]" : `[update ${String(++updateNum).padStart(3, "0")}]`;
362
- return ` ${label} "${r.summary}" \u2192 ${r.filePath}`;
603
+ let ctxFiles = [];
604
+ if (existsSync3(ctxDir)) {
605
+ ctxFiles = readdirSync3(ctxDir).filter((f) => f !== "CLAUDE.md");
606
+ }
607
+ if (ctxFiles.length > 0) {
608
+ const ctxLines = ctxFiles.map((f) => `- ${join3(ctxDir, f)}`).join("\n");
609
+ contextSection = `
610
+ ## Context
611
+
612
+ ${ctxLines}
613
+ `;
614
+ }
615
+ }
616
+ const messages = session.messages ?? [];
617
+ const messagesSection = messages.length > 0 ? "\n### Messages\n\n" + messages.map((m) => {
618
+ const sourceLabel = m.source.type === "agent" ? `agent:${m.source.agentId}` : m.source.type === "system" && m.source.detail ? `system:${m.source.detail}` : m.source.type;
619
+ const fileRef = m.filePath ? ` \u2192 ${m.filePath}` : "";
620
+ return `- [${sourceLabel} @ ${m.timestamp}] "${m.summary}"${fileRef}`;
621
+ }).join("\n") + "\n" : "";
622
+ let previousCyclesSection = "";
623
+ if (session.orchestratorCycles.length > 1) {
624
+ const previousCycles = session.orchestratorCycles.slice(0, -1);
625
+ const agentMap = new Map(session.agents.map((a) => [a.id, a]));
626
+ const lines = previousCycles.map((c) => {
627
+ const agentDescs = c.agentsSpawned.map((id) => {
628
+ const agent = agentMap.get(id);
629
+ return agent ? `${id} (${agent.name})` : id;
630
+ }).join(", ");
631
+ return `Cycle ${c.cycle}: ${agentDescs || "(none)"}`;
363
632
  });
364
- return [header, ...reportLines].join("\n");
365
- }).join("\n") : " (none)";
366
- const cycleLines = session.orchestratorCycles.length > 0 ? session.orchestratorCycles.map((c) => {
367
- const spawnedList = c.agentsSpawned.length > 0 ? c.agentsSpawned.join(", ") : "(none)";
368
- return `Cycle ${c.cycle}: Spawned ${spawnedList}`;
369
- }).join("\n") : " (none)";
633
+ previousCyclesSection = `
634
+ ### Previous Cycles
635
+
636
+ ${lines.join("\n")}
637
+ `;
638
+ }
639
+ let mostRecentCycleSection = "";
640
+ const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
641
+ if (lastCycle && lastCycle.agentsSpawned.length > 0) {
642
+ const agentMap = new Map(session.agents.map((a) => [a.id, a]));
643
+ const agentBlocks = lastCycle.agentsSpawned.map((id) => {
644
+ const agent = agentMap.get(id);
645
+ if (!agent) return `<agent-${id} status="unknown">
646
+ (no agent data)
647
+ </agent-${id}>`;
648
+ const finalReport = agent.reports.find((r) => r.type === "final");
649
+ const reportToUse = finalReport ?? agent.reports[agent.reports.length - 1];
650
+ let reportContent = "(no reports)";
651
+ if (reportToUse) {
652
+ try {
653
+ reportContent = readFileSync3(reportToUse.filePath, "utf-8");
654
+ } catch {
655
+ reportContent = `(could not read report: ${reportToUse.filePath})`;
656
+ }
657
+ }
658
+ return `<agent-${id} name="${agent.name}" status="${agent.status}">
659
+ ${reportContent}
660
+ </agent-${id}>`;
661
+ }).join("\n");
662
+ mostRecentCycleSection = `
663
+ ### Most Recent Cycle
664
+
665
+ <last-cycle>
666
+ ${agentBlocks}
667
+ </last-cycle>
668
+ `;
669
+ }
670
+ const roadmapRef = existsSync3(roadmapFile) ? `@${roadmapFile}` : "(empty)";
370
671
  const worktreeAgents = session.agents.filter((a) => a.worktreePath);
371
672
  let worktreeSection = "";
372
- if (worktreeAgents.length > 0) {
373
- const wtLines = worktreeAgents.map((a) => {
374
- if (a.mergeStatus === "conflict") {
375
- return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
673
+ if (worktreeAgents.length > 0 || existsSync3(worktreeConfigPath(session.cwd))) {
674
+ let wtLines = "";
675
+ if (worktreeAgents.length > 0) {
676
+ wtLines = "\n" + worktreeAgents.map((a) => {
677
+ if (a.mergeStatus === "conflict") {
678
+ return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
376
679
  Branch: ${a.branchName}
377
680
  Worktree: ${a.worktreePath}`;
378
- }
379
- if (a.mergeStatus === "no-changes") {
380
- return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
381
- }
382
- const status = a.mergeStatus ?? "pending";
383
- return `- ${a.id}: ${status} (branch ${a.branchName})`;
384
- }).join("\n");
681
+ }
682
+ if (a.mergeStatus === "no-changes") {
683
+ return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
684
+ }
685
+ const status = a.mergeStatus ?? "pending";
686
+ return `- ${a.id}: ${status} (branch ${a.branchName})`;
687
+ }).join("\n");
688
+ }
689
+ const worktreeHint = existsSync3(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.";
385
690
  worktreeSection = `
386
691
 
387
- ## Worktrees
388
- ${wtLines}`;
389
- }
390
- 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.";
391
- const contextSection = session.context ? `
392
- ## Background Context
393
- ${session.context}
394
- ` : "";
395
- return `<state>
396
- session: ${shortId} (cycle ${cycleNum})
397
- task: ${session.task}
398
- status: ${session.status}
399
- ${contextSection}
400
- ## Plan
401
- ${planRef}
402
-
403
- ## Logs
404
- ${logsRef}
692
+ ## Git Worktrees
405
693
 
406
- ## Agents
407
- ${agentLines}${worktreeSection}
694
+ ${worktreeHint}${wtLines}`;
695
+ }
696
+ const goalFile = goalPath(session.cwd, session.id);
697
+ const goalContent = existsSync3(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
698
+ return `## Goal
408
699
 
409
- ## Previous Cycles
410
- ${cycleLines}
700
+ ${goalContent}
701
+ ${contextSection}${messagesSection}
702
+ ### Cycle Log
411
703
 
412
- ## Context Files
413
- ${contextLines}
704
+ Write your cycle summary to: ${logFile}
705
+ ${previousCyclesSection}${mostRecentCycleSection}
706
+ ## Roadmap
414
707
 
415
- ## Git Worktrees
416
- ${worktreeHint}
417
- </state>`;
708
+ ${roadmapRef}
709
+ ${worktreeSection}`;
418
710
  }
419
711
  async function spawnOrchestrator(sessionId, cwd, windowId, message) {
420
712
  const session = getSession(cwd, sessionId);
421
- const basePrompt = loadOrchestratorPrompt(cwd);
713
+ const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
714
+ const mode = lastCycle?.mode ?? "planning";
715
+ const basePrompt = loadOrchestratorPrompt(cwd, mode);
422
716
  const formattedState = formatStateForOrchestrator(session);
717
+ const agentPluginPath = resolve(import.meta.dirname, "../templates/agent-plugin");
718
+ const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
719
+ agentTypes.push(
720
+ { qualifiedName: "Explore", source: "bundled", model: "haiku", description: "Fast codebase exploration \u2014 find files, search code, answer questions about architecture. Use for research and context gathering." }
721
+ );
722
+ const agentTypeLines = agentTypes.length > 0 ? agentTypes.map((t) => {
723
+ const modelTag = t.model ? ` (${t.model})` : "";
724
+ const desc = t.description ? ` \u2014 ${t.description}` : "";
725
+ return `- \`${t.qualifiedName}\`${modelTag}${desc}`;
726
+ }).join("\n") : " (none)";
727
+ const systemPrompt = basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines);
423
728
  const cycleNum = session.orchestratorCycles.length + 1;
424
729
  const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
425
- writeFileSync2(promptFilePath, basePrompt, "utf-8");
730
+ writeFileSync2(promptFilePath, systemPrompt, "utf-8");
426
731
  sessionWindowMap.set(sessionId, windowId);
732
+ const cliBin = resolve(import.meta.dirname, "cli.js");
733
+ const npmBinDir = resolve(import.meta.dirname, "../../.bin");
427
734
  const envExports = [
428
735
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
429
- `export SISYPHUS_AGENT_ID='orchestrator'`
736
+ `export SISYPHUS_AGENT_ID='orchestrator'`,
737
+ `export SISYPHUS_CWD='${cwd}'`,
738
+ `export PATH="${npmBinDir}:$PATH"`
430
739
  ].join(" && ");
431
- let userPrompt;
740
+ let userPrompt = formattedState;
432
741
  if (message) {
433
- userPrompt = `${formattedState}
742
+ userPrompt += `
743
+
744
+ ## Continuation Instructions
434
745
 
435
746
  The user resumed this session with new instructions: ${message}`;
436
747
  } else {
437
- const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
438
748
  const storedPrompt = lastCycle?.nextPrompt;
439
- if (storedPrompt) {
440
- userPrompt = `${formattedState}
749
+ const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
750
+ userPrompt += `
441
751
 
442
- ${storedPrompt}`;
443
- } else {
444
- userPrompt = `${formattedState}
752
+ ## Continuation Instructions
445
753
 
446
- Review the current session and delegate the next cycle of work.`;
447
- }
754
+ ${continuationText}`;
448
755
  }
449
756
  const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
450
757
  writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
758
+ if (session.messages && session.messages.length > 0) {
759
+ await drainMessages(cwd, sessionId, session.messages.length);
760
+ }
451
761
  const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
452
762
  const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
453
- const claudeCmd = `claude --dangerously-skip-permissions --settings "${settingsPath}" --plugin-dir "${pluginPath}" --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
763
+ const config = loadConfig(cwd);
764
+ const effort = config.orchestratorEffort ?? "high";
765
+ const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --settings "${settingsPath}" --plugin-dir "${pluginPath}" --name "sisyphus:orch-${sessionId.slice(0, 8)}-cycle-${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
454
766
  const paneId = createPane(windowId, cwd, "left");
455
767
  sessionOrchestratorPane.set(sessionId, paneId);
456
768
  registerPane(paneId, sessionId, "orchestrator");
457
769
  setPaneTitle(paneId, `Sisyphus`);
458
770
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
459
771
  const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
460
- const bannerCmd = existsSync(bannerPath) ? `cat '${bannerPath}' &&` : "";
461
- const notifyCmd = `sisyphus notify pane-exited --pane-id ${paneId}`;
772
+ const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
773
+ const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
462
774
  sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
463
775
  await addOrchestratorCycle(cwd, sessionId, {
464
776
  cycle: cycleNum,
@@ -474,7 +786,7 @@ function resolveOrchestratorPane(sessionId, cwd) {
474
786
  const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
475
787
  return lastCycle?.paneId ?? void 0;
476
788
  }
477
- async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
789
+ async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
478
790
  const paneId = resolveOrchestratorPane(sessionId, cwd);
479
791
  if (paneId) {
480
792
  killPane(paneId);
@@ -483,7 +795,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
483
795
  }
484
796
  const windowId = sessionWindowMap.get(sessionId);
485
797
  if (windowId) selectLayout(windowId);
486
- await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
798
+ await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode);
487
799
  const session = getSession(cwd, sessionId);
488
800
  const runningAgents = session.agents.filter((a) => a.status === "running");
489
801
  if (runningAgents.length === 0) {
@@ -502,13 +814,13 @@ function cleanupSessionMaps(sessionId) {
502
814
  }
503
815
 
504
816
  // src/daemon/agent.ts
505
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
817
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5, existsSync as existsSync5 } from "fs";
506
818
  import { resolve as resolve2 } from "path";
507
819
 
508
820
  // src/daemon/worktree.ts
509
821
  import { execSync as execSync2 } from "child_process";
510
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync } from "fs";
511
- import { dirname as dirname2, join as join2 } from "path";
822
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
823
+ import { dirname as dirname3, join as join4 } from "path";
512
824
  var EXEC_ENV2 = {
513
825
  ...process.env,
514
826
  PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
@@ -536,10 +848,10 @@ function loadWorktreeConfig(cwd) {
536
848
  }
537
849
  function createWorktreeShell(cwd, sessionId, agentId) {
538
850
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
539
- const worktreePath = join2(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
540
- mkdirSync2(dirname2(worktreePath), { recursive: true });
851
+ const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
852
+ mkdirSync2(dirname3(worktreePath), { recursive: true });
541
853
  execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
542
- if (existsSync2(worktreePath)) {
854
+ if (existsSync4(worktreePath)) {
543
855
  execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
544
856
  }
545
857
  execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
@@ -550,16 +862,16 @@ function createWorktreeShell(cwd, sessionId, agentId) {
550
862
  function bootstrapWorktree(cwd, worktreePath, config) {
551
863
  if (config.copy) {
552
864
  for (const entry of config.copy) {
553
- const dest = join2(worktreePath, entry);
554
- mkdirSync2(dirname2(dest), { recursive: true });
555
- execSafe2(`cp -r ${shellQuote2(join2(cwd, entry))} ${shellQuote2(dest)}`);
865
+ const dest = join4(worktreePath, entry);
866
+ mkdirSync2(dirname3(dest), { recursive: true });
867
+ execSafe2(`cp -r ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
556
868
  }
557
869
  }
558
870
  if (config.clone) {
559
871
  for (const entry of config.clone) {
560
- const dest = join2(worktreePath, entry);
561
- mkdirSync2(dirname2(dest), { recursive: true });
562
- const src = shellQuote2(join2(cwd, entry));
872
+ const dest = join4(worktreePath, entry);
873
+ mkdirSync2(dirname3(dest), { recursive: true });
874
+ const src = shellQuote2(join4(cwd, entry));
563
875
  const dstQ = shellQuote2(dest);
564
876
  if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
565
877
  execSafe2(`cp -r ${src} ${dstQ}`);
@@ -568,9 +880,9 @@ function bootstrapWorktree(cwd, worktreePath, config) {
568
880
  }
569
881
  if (config.symlink) {
570
882
  for (const entry of config.symlink) {
571
- const dest = join2(worktreePath, entry);
572
- mkdirSync2(dirname2(dest), { recursive: true });
573
- execSafe2(`ln -s ${shellQuote2(join2(cwd, entry))} ${shellQuote2(dest)}`);
883
+ const dest = join4(worktreePath, entry);
884
+ mkdirSync2(dirname3(dest), { recursive: true });
885
+ execSafe2(`ln -s ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
574
886
  }
575
887
  }
576
888
  if (config.init) {
@@ -640,11 +952,11 @@ function mergeWorktrees(cwd, agents) {
640
952
  function cleanupWorktree(cwd, worktreePath, branchName) {
641
953
  execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(worktreePath)} --force`);
642
954
  execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
643
- const baseDir = dirname2(worktreePath);
955
+ const baseDir = dirname3(worktreePath);
644
956
  try {
645
- const entries = readdirSync2(baseDir);
957
+ const entries = readdirSync4(baseDir);
646
958
  if (entries.length === 0) {
647
- rmSync(baseDir, { recursive: true });
959
+ rmSync2(baseDir, { recursive: true });
648
960
  }
649
961
  } catch {
650
962
  }
@@ -653,89 +965,37 @@ function countWorktreeAgents(agents) {
653
965
  return agents.filter((a) => a.worktreePath && a.status === "running").length;
654
966
  }
655
967
 
656
- // src/daemon/frontmatter.ts
657
- import { readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
658
- import { homedir } from "os";
659
- import { join as join3 } from "path";
660
- function detectProvider(model) {
661
- if (!model) return "anthropic";
662
- if (/^(gpt-|codex-)/.test(model)) return "openai";
663
- return "anthropic";
664
- }
665
- function parseAgentFrontmatter(content) {
666
- const match = content.match(/^---\n([\s\S]*?)\n---/);
667
- if (!match) return {};
668
- const block = match[1];
669
- const fm = {};
670
- const str = (key) => {
671
- const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
672
- return m ? m[1].trim() : void 0;
673
- };
674
- fm.name = str("name");
675
- fm.model = str("model");
676
- fm.color = str("color");
677
- fm.description = str("description");
678
- fm.permissionMode = str("permissionMode");
679
- const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
680
- if (skillsMatch) {
681
- fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
682
- }
683
- return fm;
684
- }
685
- function extractAgentBody(content) {
686
- const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
687
- return match ? match[1].trim() : content.trim();
688
- }
689
- function findPluginInstallPath(namespace) {
968
+ // src/daemon/summarize.ts
969
+ import { query } from "@r-cli/sdk";
970
+ var disabled = false;
971
+ async function summarizeReport(reportText) {
972
+ if (disabled) return null;
690
973
  try {
691
- const registryPath2 = join3(homedir(), ".claude", "plugins", "installed_plugins.json");
692
- const registry = JSON.parse(readFileSync5(registryPath2, "utf-8"));
693
- for (const key of Object.keys(registry)) {
694
- if (key.startsWith(`${namespace}@`)) {
695
- return registry[key].installPath ?? null;
974
+ const session = await query({
975
+ prompt: `Summarize this agent work report in one concise sentence (max 120 chars). Focus on what was accomplished and the outcome. Output ONLY the summary sentence, nothing else.
976
+
977
+ ${reportText.slice(0, 3e3)}`,
978
+ options: {
979
+ model: "haiku",
980
+ maxTurns: 1
981
+ }
982
+ });
983
+ let text = "";
984
+ for await (const msg of session) {
985
+ if (msg.type === "assistant" && msg.message?.content) {
986
+ for (const block of msg.message.content) {
987
+ if (block.type === "text") text += block.text;
988
+ }
696
989
  }
697
990
  }
698
- } catch {
699
- }
700
- return null;
701
- }
702
- function resolveAgentTypePath(agentType, pluginDir, cwd) {
703
- if (!agentType) return null;
704
- let namespace;
705
- let name;
706
- if (agentType.includes(":")) {
707
- [namespace, name] = agentType.split(":", 2);
708
- } else {
709
- name = agentType;
710
- }
711
- const searchPaths = [];
712
- if (namespace) {
713
- searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
714
- const installPath = findPluginInstallPath(namespace);
715
- if (installPath) {
716
- searchPaths.push(join3(installPath, "agents", `${name}.md`));
991
+ const summary = text.trim();
992
+ return summary.length > 0 ? summary : null;
993
+ } catch (err) {
994
+ console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
995
+ const status = err.status;
996
+ if (status === 401 || status === 403) {
997
+ disabled = true;
717
998
  }
718
- } else {
719
- searchPaths.push(join3(cwd, ".claude", "agents", `${name}.md`));
720
- searchPaths.push(join3(homedir(), ".claude", "agents", `${name}.md`));
721
- searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
722
- }
723
- for (const path of searchPaths) {
724
- if (existsSync3(path)) return path;
725
- }
726
- return null;
727
- }
728
- function resolveAgentConfig(agentType, pluginDir, cwd) {
729
- const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
730
- if (!filePath) return null;
731
- try {
732
- const content = readFileSync5(filePath, "utf-8");
733
- return {
734
- frontmatter: parseAgentFrontmatter(content),
735
- body: extractAgentBody(content),
736
- filePath
737
- };
738
- } catch {
739
999
  return null;
740
1000
  }
741
1001
  }
@@ -757,7 +1017,7 @@ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
757
1017
  const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
758
1018
  let template;
759
1019
  try {
760
- template = readFileSync6(templatePath, "utf-8");
1020
+ template = readFileSync5(templatePath, "utf-8");
761
1021
  } catch {
762
1022
  template = `# Sisyphus Agent
763
1023
  Session: {{SESSION_ID}}
@@ -773,13 +1033,33 @@ Task: {{INSTRUCTION}}`;
773
1033
  }
774
1034
  return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
775
1035
  }
1036
+ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1037
+ const base = `${promptsDir(cwd, sessionId)}/${agentId}-plugin`;
1038
+ mkdirSync3(`${base}/.claude-plugin`, { recursive: true });
1039
+ mkdirSync3(`${base}/agents`, { recursive: true });
1040
+ mkdirSync3(`${base}/hooks`, { recursive: true });
1041
+ writeFileSync3(
1042
+ `${base}/.claude-plugin/plugin.json`,
1043
+ JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
1044
+ "utf-8"
1045
+ );
1046
+ if (agentConfig?.filePath && agentType && agentType !== "worker") {
1047
+ const shortName = agentType.replace(/^sisyphus:/, "");
1048
+ copyFileSync2(agentConfig.filePath, `${base}/agents/${shortName}.md`);
1049
+ }
1050
+ const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
1051
+ for (const f of ["hooks.json", "require-submit.sh", "intercept-send-message.sh"]) {
1052
+ copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
1053
+ }
1054
+ return base;
1055
+ }
776
1056
  async function spawnAgent(opts) {
777
1057
  const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
778
1058
  const count = (agentCounters.get(sessionId) ?? 0) + 1;
779
1059
  agentCounters.set(sessionId, count);
780
1060
  const agentId = `agent-${String(count).padStart(3, "0")}`;
781
- const pluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
782
- const agentConfig = resolveAgentConfig(agentType, pluginPath, cwd);
1061
+ const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1062
+ const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
783
1063
  const provider = detectProvider(agentConfig?.frontmatter.model);
784
1064
  const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
785
1065
  let paneCwd = cwd;
@@ -805,13 +1085,17 @@ async function spawnAgent(opts) {
805
1085
  const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
806
1086
  writeFileSync3(suffixFilePath, suffix, "utf-8");
807
1087
  const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
808
- const bannerCmd = existsSync4(bannerPath) ? `cat '${bannerPath}' &&` : "";
1088
+ const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
1089
+ const cliBin = resolve2(import.meta.dirname, "cli.js");
1090
+ const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
809
1091
  const envExports = [
810
1092
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
811
1093
  `export SISYPHUS_AGENT_ID='${agentId}'`,
812
- ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : []
1094
+ `export SISYPHUS_CWD='${cwd}'`,
1095
+ ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1096
+ `export PATH="${npmBinDir}:$PATH"`
813
1097
  ].join(" && ");
814
- const notifyCmd = `sisyphus notify pane-exited --pane-id ${paneId}`;
1098
+ const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
815
1099
  let mainCmd;
816
1100
  if (provider === "openai") {
817
1101
  const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
@@ -828,7 +1112,10 @@ ${instruction}`);
828
1112
  mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
829
1113
  } else {
830
1114
  const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
831
- mainCmd = `claude --dangerously-skip-permissions --plugin-dir "${pluginPath}"${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
1115
+ const config = loadConfig(cwd);
1116
+ const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
1117
+ const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
1118
+ mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote3(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
832
1119
  }
833
1120
  const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
834
1121
  const agent = {
@@ -866,10 +1153,98 @@ ${instruction}`);
866
1153
  }
867
1154
  return agent;
868
1155
  }
1156
+ async function restartAgent(sessionId, cwd, agentId, windowId) {
1157
+ const session = getSession(cwd, sessionId);
1158
+ const agent = session.agents.find((a) => a.id === agentId);
1159
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`);
1160
+ if (agent.status === "running") {
1161
+ const paneAlive = agent.paneId && paneExists(agent.paneId);
1162
+ if (paneAlive) {
1163
+ throw new Error(`Agent ${agentId} is already running`);
1164
+ }
1165
+ await updateAgent(cwd, sessionId, agentId, {
1166
+ status: "lost",
1167
+ killedReason: "pane disappeared (detected on restart)",
1168
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1169
+ });
1170
+ }
1171
+ const { instruction, agentType, name, color } = agent;
1172
+ const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1173
+ const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1174
+ const provider = detectProvider(agentConfig?.frontmatter.model);
1175
+ let paneCwd = cwd;
1176
+ let worktreeContext;
1177
+ if (agent.worktreePath) {
1178
+ paneCwd = agent.worktreePath;
1179
+ const portOffset = countWorktreeAgents(session.agents);
1180
+ worktreeContext = {
1181
+ offset: portOffset,
1182
+ total: portOffset,
1183
+ branchName: agent.branchName
1184
+ };
1185
+ }
1186
+ if (agent.paneId) {
1187
+ try {
1188
+ killPane(agent.paneId);
1189
+ } catch {
1190
+ }
1191
+ unregisterAgentPane(sessionId, agentId);
1192
+ }
1193
+ const paneId = createPane(windowId, paneCwd);
1194
+ registerPane(paneId, sessionId, "agent", agentId);
1195
+ const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
1196
+ const paneLabel = shortType ? `${name}-${shortType}` : name;
1197
+ setPaneTitle(paneId, `${paneLabel} (${agentId})`);
1198
+ setPaneStyle(paneId, color);
1199
+ const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
1200
+ const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
1201
+ writeFileSync3(suffixFilePath, suffix, "utf-8");
1202
+ const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
1203
+ const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
1204
+ const cliBin = resolve2(import.meta.dirname, "cli.js");
1205
+ const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
1206
+ const envExports = [
1207
+ `export SISYPHUS_SESSION_ID='${sessionId}'`,
1208
+ `export SISYPHUS_AGENT_ID='${agentId}'`,
1209
+ `export SISYPHUS_CWD='${cwd}'`,
1210
+ ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1211
+ `export PATH="${npmBinDir}:$PATH"`
1212
+ ].join(" && ");
1213
+ const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
1214
+ let mainCmd;
1215
+ if (provider === "openai") {
1216
+ const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
1217
+ const parts = [];
1218
+ if (agentConfig?.body) parts.push(agentConfig.body);
1219
+ parts.push(suffix);
1220
+ parts.push(`## Task
1221
+
1222
+ ${instruction}`);
1223
+ writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
1224
+ const model = agentConfig?.frontmatter.model ?? "codex-mini";
1225
+ mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
1226
+ } else {
1227
+ const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
1228
+ const config = loadConfig(cwd);
1229
+ const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
1230
+ const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
1231
+ mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote3(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
1232
+ }
1233
+ const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
1234
+ await updateAgent(cwd, sessionId, agentId, {
1235
+ status: "running",
1236
+ paneId,
1237
+ provider,
1238
+ spawnedAt: (/* @__PURE__ */ new Date()).toISOString(),
1239
+ completedAt: null,
1240
+ killedReason: void 0
1241
+ });
1242
+ sendKeys(paneId, fullCmd);
1243
+ }
869
1244
  function nextReportNumber(cwd, sessionId, agentId) {
870
1245
  const dir = reportsDir(cwd, sessionId);
871
1246
  try {
872
- const files = readdirSync3(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
1247
+ const files = readdirSync5(dir).filter((f) => f.startsWith(`${agentId}-`) && !f.endsWith("-final.md"));
873
1248
  return String(files.length + 1).padStart(3, "0");
874
1249
  } catch {
875
1250
  return "001";
@@ -888,6 +1263,12 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
888
1263
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
889
1264
  };
890
1265
  await appendAgentReport(cwd, sessionId, agentId, entry);
1266
+ summarizeReport(content).then(async (aiSummary) => {
1267
+ if (aiSummary) {
1268
+ await updateReportSummary(cwd, sessionId, agentId, filePath, aiSummary);
1269
+ }
1270
+ }).catch(() => {
1271
+ });
891
1272
  }
892
1273
  async function handleAgentSubmit(cwd, sessionId, agentId, report) {
893
1274
  const dir = reportsDir(cwd, sessionId);
@@ -901,20 +1282,26 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
901
1282
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
902
1283
  };
903
1284
  await appendAgentReport(cwd, sessionId, agentId, entry);
1285
+ summarizeReport(report).then(async (aiSummary) => {
1286
+ if (aiSummary) {
1287
+ await updateReportSummary(cwd, sessionId, agentId, filePath, aiSummary);
1288
+ }
1289
+ }).catch(() => {
1290
+ });
904
1291
  await updateAgent(cwd, sessionId, agentId, {
905
1292
  status: "completed",
906
1293
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
907
1294
  });
908
1295
  const session = getSession(cwd, sessionId);
909
- const agentArr = session.agents;
910
- const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
911
- if (agent) {
912
- unregisterPane(agent.paneId);
913
- killPane(agent.paneId);
1296
+ const agent = session.agents.find((a) => a.id === agentId);
1297
+ if (agent?.paneId) {
1298
+ unregisterAgentPane(sessionId, agentId);
1299
+ try {
1300
+ killPane(agent.paneId);
1301
+ } catch {
1302
+ }
914
1303
  }
915
- const windowId = getWindowId(sessionId);
916
- if (windowId) selectLayout(windowId);
917
- return allAgentsDone(session);
1304
+ return allAgentsDone(getSession(cwd, sessionId));
918
1305
  }
919
1306
  async function handleAgentKilled(cwd, sessionId, agentId, reason) {
920
1307
  unregisterAgentPane(sessionId, agentId);
@@ -1000,7 +1387,15 @@ async function pollSession(sessionId, cwd, windowId) {
1000
1387
  }
1001
1388
  if (session.status !== "active") return;
1002
1389
  const livePanes = listPanes(windowId);
1003
- if (livePanes.length === 0) return;
1390
+ if (livePanes.length === 0) {
1391
+ const tracked = trackedSessions.get(sessionId);
1392
+ if (tracked && !sessionExists(tracked.tmuxSession)) {
1393
+ await updateSessionStatus(cwd, sessionId, "paused");
1394
+ untrackSession(sessionId);
1395
+ console.log(`[sisyphus] Session ${sessionId} paused: tmux session destroyed`);
1396
+ }
1397
+ return;
1398
+ }
1004
1399
  const livePaneIds = new Set(livePanes.map((p) => p.paneId));
1005
1400
  let paneRemoved = false;
1006
1401
  for (const agent of session.agents) {
@@ -1030,23 +1425,34 @@ async function pollSession(sessionId, cwd, windowId) {
1030
1425
  }
1031
1426
 
1032
1427
  // src/daemon/session-manager.ts
1033
- async function startSession(task, cwd, tmuxSession, windowId, context) {
1428
+ var NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
1429
+ async function startSession(task, cwd, context, name) {
1034
1430
  const sessionId = uuidv4();
1035
- const session = createSession(sessionId, task, cwd, context);
1036
- await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
1037
- trackSession(sessionId, cwd, tmuxSession);
1431
+ if (name && !NAME_PATTERN.test(name)) {
1432
+ throw new Error(`Invalid session name "${name}": only alphanumeric, hyphens, and underscores allowed`);
1433
+ }
1434
+ const tmuxName = `sisyphus-${name ?? sessionId.slice(0, 8)}`;
1435
+ if (sessionExists(tmuxName)) {
1436
+ throw new Error(`Tmux session "${tmuxName}" already exists. Choose a different name.`);
1437
+ }
1438
+ const session = createSession(sessionId, task, cwd, context, name);
1439
+ const { windowId, initialPaneId } = createSession2(tmuxName, "main", cwd);
1440
+ setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
1441
+ await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
1442
+ trackSession(sessionId, cwd, tmuxName);
1038
1443
  await spawnOrchestrator(sessionId, cwd, windowId);
1039
1444
  updateTrackedWindow(sessionId, windowId);
1445
+ killPane(initialPaneId);
1040
1446
  pruneOldSessions(cwd);
1041
- return session;
1447
+ return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
1042
1448
  }
1043
1449
  var PRUNE_KEEP_COUNT = 10;
1044
1450
  var PRUNE_KEEP_DAYS = 7;
1045
1451
  function pruneOldSessions(cwd) {
1046
1452
  try {
1047
1453
  const dir = sessionsDir(cwd);
1048
- if (!existsSync5(dir)) return;
1049
- const entries = readdirSync4(dir, { withFileTypes: true });
1454
+ if (!existsSync6(dir)) return;
1455
+ const entries = readdirSync6(dir, { withFileTypes: true });
1050
1456
  const candidates = [];
1051
1457
  for (const entry of entries) {
1052
1458
  if (!entry.isDirectory()) continue;
@@ -1069,14 +1475,25 @@ function pruneOldSessions(cwd) {
1069
1475
  }
1070
1476
  for (const c of candidates) {
1071
1477
  if (keep.has(c.id)) continue;
1072
- rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
1478
+ rmSync3(sessionDir(cwd, c.id), { recursive: true, force: true });
1073
1479
  }
1074
1480
  } catch (err) {
1075
1481
  console.error("[sisyphus] Session pruning failed:", err);
1076
1482
  }
1077
1483
  }
1078
- async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
1484
+ async function resumeSession(sessionId, cwd, message) {
1079
1485
  const session = getSession(cwd, sessionId);
1486
+ const tmuxName = session.tmuxSessionName ?? `sisyphus-${sessionId.slice(0, 8)}`;
1487
+ let windowId;
1488
+ if (sessionExists(tmuxName) && session.tmuxWindowId) {
1489
+ windowId = session.tmuxWindowId;
1490
+ } else {
1491
+ const created = createSession2(tmuxName, "main", cwd);
1492
+ setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
1493
+ windowId = created.windowId;
1494
+ await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
1495
+ var initialPaneId = created.initialPaneId;
1496
+ }
1080
1497
  if (session.status !== "active") {
1081
1498
  const livePaneIds = /* @__PURE__ */ new Set();
1082
1499
  if (session.tmuxWindowId) {
@@ -1099,12 +1516,16 @@ async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
1099
1516
  }
1100
1517
  }
1101
1518
  await updateSessionStatus(cwd, sessionId, "active");
1102
- await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
1519
+ await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
1103
1520
  resetAgentCounterFromState(sessionId, session.agents);
1104
1521
  resetColors(sessionId);
1105
- trackSession(sessionId, cwd, tmuxSession);
1522
+ orchestratorDone.delete(sessionId);
1523
+ trackSession(sessionId, cwd, tmuxName);
1106
1524
  await spawnOrchestrator(sessionId, cwd, windowId, message);
1107
1525
  updateTrackedWindow(sessionId, windowId);
1526
+ if (initialPaneId) {
1527
+ killPane(initialPaneId);
1528
+ }
1108
1529
  return getSession(cwd, sessionId);
1109
1530
  }
1110
1531
  function getSessionStatus(cwd, sessionId) {
@@ -1112,8 +1533,8 @@ function getSessionStatus(cwd, sessionId) {
1112
1533
  }
1113
1534
  function listSessions(cwd) {
1114
1535
  const dir = sessionsDir(cwd);
1115
- if (!existsSync5(dir)) return [];
1116
- const entries = readdirSync4(dir, { withFileTypes: true });
1536
+ if (!existsSync6(dir)) return [];
1537
+ const entries = readdirSync6(dir, { withFileTypes: true });
1117
1538
  const sessions = [];
1118
1539
  for (const entry of entries) {
1119
1540
  if (!entry.isDirectory()) continue;
@@ -1121,10 +1542,13 @@ function listSessions(cwd) {
1121
1542
  const session = getSession(cwd, entry.name);
1122
1543
  sessions.push({
1123
1544
  id: session.id,
1545
+ name: session.name,
1124
1546
  task: session.task,
1125
1547
  status: session.status,
1126
1548
  createdAt: session.createdAt,
1127
- agentCount: session.agents.length
1549
+ agentCount: session.agents.length,
1550
+ tmuxSessionName: session.tmuxSessionName,
1551
+ tmuxWindowId: session.tmuxWindowId
1128
1552
  });
1129
1553
  } catch (err) {
1130
1554
  console.error(`[sisyphus] Failed to read session ${entry.name}:`, err);
@@ -1133,11 +1557,17 @@ function listSessions(cwd) {
1133
1557
  return sessions;
1134
1558
  }
1135
1559
  var pendingRespawns = /* @__PURE__ */ new Set();
1560
+ var orchestratorDone = /* @__PURE__ */ new Set();
1136
1561
  function onAllAgentsDone2(sessionId, cwd, windowId) {
1137
1562
  if (pendingRespawns.has(sessionId)) return;
1563
+ if (!orchestratorDone.has(sessionId)) {
1564
+ console.log(`[sisyphus] All agents done for session ${sessionId}, waiting for orchestrator to yield`);
1565
+ return;
1566
+ }
1138
1567
  const session = getSession(cwd, sessionId);
1139
1568
  if (session.status !== "active") return;
1140
1569
  pendingRespawns.add(sessionId);
1570
+ orchestratorDone.delete(sessionId);
1141
1571
  const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
1142
1572
  if (worktreeAgents.length > 0) {
1143
1573
  const results = mergeWorktrees(cwd, worktreeAgents);
@@ -1149,9 +1579,42 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
1149
1579
  }).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
1150
1580
  }
1151
1581
  }
1152
- setImmediate(() => {
1582
+ const cycleNumber = session.orchestratorCycles.length;
1583
+ if (cycleNumber > 0) {
1584
+ createSnapshot(cwd, sessionId, cycleNumber);
1585
+ }
1586
+ setImmediate(async () => {
1153
1587
  pendingRespawns.delete(sessionId);
1154
- spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
1588
+ try {
1589
+ const freshSession = getSession(cwd, sessionId);
1590
+ if (freshSession.status !== "active") return;
1591
+ let activeWindowId = windowId;
1592
+ const tmuxName = freshSession.tmuxSessionName;
1593
+ const needsRecreation = tmuxName && (!sessionExists(tmuxName) || listPanes(activeWindowId).length === 0);
1594
+ let initialPaneId;
1595
+ if (needsRecreation) {
1596
+ if (sessionExists(tmuxName)) {
1597
+ killSession(tmuxName);
1598
+ }
1599
+ const created = createSession2(tmuxName, "main", cwd);
1600
+ setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
1601
+ activeWindowId = created.windowId;
1602
+ initialPaneId = created.initialPaneId;
1603
+ await updateSessionTmux(cwd, sessionId, tmuxName, activeWindowId);
1604
+ trackSession(sessionId, cwd, tmuxName);
1605
+ }
1606
+ await spawnOrchestrator(sessionId, cwd, activeWindowId);
1607
+ updateTrackedWindow(sessionId, activeWindowId);
1608
+ if (initialPaneId) killPane(initialPaneId);
1609
+ for (const agent of freshSession.agents) {
1610
+ if (agent.status !== "running" && agent.paneId) {
1611
+ killPane(agent.paneId);
1612
+ }
1613
+ }
1614
+ selectLayout(activeWindowId);
1615
+ } catch (err) {
1616
+ console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err);
1617
+ }
1155
1618
  });
1156
1619
  }
1157
1620
  async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
@@ -1183,16 +1646,17 @@ async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
1183
1646
  async function handleReport(cwd, sessionId, agentId, content) {
1184
1647
  await handleAgentReport(cwd, sessionId, agentId, content);
1185
1648
  }
1186
- async function handleYield(sessionId, cwd, nextPrompt) {
1649
+ async function handleYield(sessionId, cwd, nextPrompt, mode) {
1187
1650
  const pre = getSession(cwd, sessionId);
1188
1651
  if (pre.status === "paused") {
1189
1652
  await updateSessionStatus(cwd, sessionId, "active");
1190
1653
  }
1191
- await handleOrchestratorYield(sessionId, cwd, nextPrompt);
1654
+ await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
1655
+ orchestratorDone.add(sessionId);
1192
1656
  const session = getSession(cwd, sessionId);
1193
1657
  const hasRunningAgents = session.agents.some((a) => a.status === "running");
1194
1658
  if (!hasRunningAgents) {
1195
- const windowId = getWindowId(sessionId);
1659
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1196
1660
  if (windowId) {
1197
1661
  onAllAgentsDone2(sessionId, cwd, windowId);
1198
1662
  }
@@ -1201,6 +1665,9 @@ async function handleYield(sessionId, cwd, nextPrompt) {
1201
1665
  async function handleComplete(sessionId, cwd, report) {
1202
1666
  await handleOrchestratorComplete(sessionId, cwd, report);
1203
1667
  }
1668
+ async function handleContinue(sessionId, cwd) {
1669
+ await continueSession(cwd, sessionId);
1670
+ }
1204
1671
  async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
1205
1672
  await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
1206
1673
  }
@@ -1230,12 +1697,81 @@ async function handleKill(sessionId, cwd) {
1230
1697
  await updateSessionStatus(cwd, sessionId, "completed");
1231
1698
  untrackSession(sessionId);
1232
1699
  unregisterSessionPanes(sessionId);
1233
- if (windowId) {
1700
+ if (session.tmuxSessionName) {
1701
+ killSession(session.tmuxSessionName);
1702
+ } else if (windowId) {
1234
1703
  killWindow(windowId);
1235
1704
  }
1236
1705
  clearAgentCounter(sessionId);
1706
+ orchestratorDone.delete(sessionId);
1237
1707
  return killedAgents;
1238
1708
  }
1709
+ async function handleRestartAgent(sessionId, cwd, agentId) {
1710
+ const session = getSession(cwd, sessionId);
1711
+ const agent = session.agents.find((a) => a.id === agentId);
1712
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`);
1713
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1714
+ if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
1715
+ await restartAgent(sessionId, cwd, agentId, windowId);
1716
+ }
1717
+ async function handleKillAgent(sessionId, cwd, agentId) {
1718
+ const session = getSession(cwd, sessionId);
1719
+ const agent = session.agents.find((a) => a.id === agentId);
1720
+ if (!agent) throw new Error(`Unknown agent: ${agentId}`);
1721
+ if (agent.status !== "running") throw new Error(`Agent ${agentId} is not running (status: ${agent.status})`);
1722
+ unregisterAgentPane(sessionId, agentId);
1723
+ if (agent.paneId) {
1724
+ killPane(agent.paneId);
1725
+ }
1726
+ if (agent.worktreePath && agent.branchName) {
1727
+ cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1728
+ }
1729
+ await updateAgent(cwd, sessionId, agentId, {
1730
+ status: "killed",
1731
+ killedReason: "killed by user",
1732
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1733
+ });
1734
+ }
1735
+ async function handleRollback(sessionId, cwd, toCycle) {
1736
+ const session = getSession(cwd, sessionId);
1737
+ if (toCycle < 1 || toCycle > session.orchestratorCycles.length) {
1738
+ const available2 = listSnapshots(cwd, sessionId);
1739
+ throw new Error(
1740
+ `Invalid cycle ${toCycle}. Available snapshots: ${available2.length > 0 ? available2.join(", ") : "none"}`
1741
+ );
1742
+ }
1743
+ const available = listSnapshots(cwd, sessionId);
1744
+ if (!available.includes(toCycle)) {
1745
+ throw new Error(
1746
+ `No snapshot for cycle ${toCycle}. Available snapshots: ${available.length > 0 ? available.join(", ") : "none"}`
1747
+ );
1748
+ }
1749
+ for (const agent of session.agents) {
1750
+ if (agent.status === "running") {
1751
+ await updateAgent(cwd, sessionId, agent.id, {
1752
+ status: "killed",
1753
+ killedReason: "session rolled back",
1754
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
1755
+ });
1756
+ }
1757
+ }
1758
+ for (const agent of session.agents) {
1759
+ if (agent.worktreePath && agent.branchName) {
1760
+ cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1761
+ }
1762
+ }
1763
+ const orchPaneId = getOrchestratorPaneId(sessionId);
1764
+ if (orchPaneId) {
1765
+ killPane(orchPaneId);
1766
+ }
1767
+ untrackSession(sessionId);
1768
+ unregisterSessionPanes(sessionId);
1769
+ clearAgentCounter(sessionId);
1770
+ orchestratorDone.delete(sessionId);
1771
+ await restoreSnapshot(cwd, sessionId, toCycle);
1772
+ deleteSnapshotsAfter(cwd, sessionId, toCycle);
1773
+ return { sessionId, restoredToCycle: toCycle };
1774
+ }
1239
1775
  async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1240
1776
  const session = getSession(cwd, sessionId);
1241
1777
  if (session.status !== "active") return;
@@ -1244,15 +1780,16 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1244
1780
  if (!agent || agent.status !== "running") return;
1245
1781
  const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
1246
1782
  if (allDone) {
1247
- const windowId = getWindowId(sessionId);
1783
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1248
1784
  if (windowId) {
1249
1785
  onAllAgentsDone2(sessionId, cwd, windowId);
1250
1786
  }
1251
1787
  }
1252
1788
  } else if (role === "orchestrator") {
1789
+ orchestratorDone.add(sessionId);
1253
1790
  const hasRunningAgents = session.agents.some((a) => a.status === "running");
1254
1791
  if (!hasRunningAgents && session.agents.length > 0) {
1255
- const windowId = getWindowId(sessionId);
1792
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1256
1793
  if (windowId) {
1257
1794
  console.log(`[sisyphus] Orchestrator pane exited for session ${sessionId}, all agents done \u2014 triggering respawn`);
1258
1795
  onAllAgentsDone2(sessionId, cwd, windowId);
@@ -1267,10 +1804,11 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1267
1804
  // src/daemon/server.ts
1268
1805
  var server = null;
1269
1806
  var sessionCwdMap = /* @__PURE__ */ new Map();
1807
+ var sessionMessageCounters = /* @__PURE__ */ new Map();
1270
1808
  var sessionTmuxMap = /* @__PURE__ */ new Map();
1271
1809
  var sessionWindowMap2 = /* @__PURE__ */ new Map();
1272
1810
  function registryPath() {
1273
- return join4(globalDir(), "session-registry.json");
1811
+ return join5(globalDir(), "session-registry.json");
1274
1812
  }
1275
1813
  function persistSessionRegistry() {
1276
1814
  const dir = globalDir();
@@ -1283,9 +1821,9 @@ function persistSessionRegistry() {
1283
1821
  }
1284
1822
  function loadSessionRegistry() {
1285
1823
  const p = registryPath();
1286
- if (!existsSync6(p)) return {};
1824
+ if (!existsSync7(p)) return {};
1287
1825
  try {
1288
- return JSON.parse(readFileSync7(p, "utf-8"));
1826
+ return JSON.parse(readFileSync6(p, "utf-8"));
1289
1827
  } catch {
1290
1828
  return {};
1291
1829
  }
@@ -1302,11 +1840,11 @@ async function handleRequest(req) {
1302
1840
  try {
1303
1841
  switch (req.type) {
1304
1842
  case "start": {
1305
- const session = await startSession(req.task, req.cwd, req.tmuxSession, req.tmuxWindow, req.context);
1843
+ const session = await startSession(req.task, req.cwd, req.context, req.name);
1306
1844
  registerSessionCwd(session.id, req.cwd);
1307
- sessionTmuxMap.set(session.id, req.tmuxSession);
1308
- sessionWindowMap2.set(session.id, req.tmuxWindow);
1309
- return { ok: true, data: { sessionId: session.id } };
1845
+ if (session.tmuxSessionName) sessionTmuxMap.set(session.id, session.tmuxSessionName);
1846
+ if (session.tmuxWindowId) sessionWindowMap2.set(session.id, session.tmuxWindowId);
1847
+ return { ok: true, data: { sessionId: session.id, tmuxSessionName: session.tmuxSessionName } };
1310
1848
  }
1311
1849
  case "spawn": {
1312
1850
  const cwd = sessionCwdMap.get(req.sessionId);
@@ -1331,7 +1869,7 @@ async function handleRequest(req) {
1331
1869
  case "yield": {
1332
1870
  const cwd = sessionCwdMap.get(req.sessionId);
1333
1871
  if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1334
- await handleYield(req.sessionId, cwd, req.nextPrompt);
1872
+ await handleYield(req.sessionId, cwd, req.nextPrompt, req.mode);
1335
1873
  return { ok: true };
1336
1874
  }
1337
1875
  case "complete": {
@@ -1340,9 +1878,15 @@ async function handleRequest(req) {
1340
1878
  await handleComplete(req.sessionId, cwd, req.report);
1341
1879
  return { ok: true };
1342
1880
  }
1881
+ case "continue": {
1882
+ const cwd = sessionCwdMap.get(req.sessionId);
1883
+ if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1884
+ await handleContinue(req.sessionId, cwd);
1885
+ return { ok: true };
1886
+ }
1343
1887
  case "status": {
1344
1888
  if (req.sessionId) {
1345
- const cwd = sessionCwdMap.get(req.sessionId);
1889
+ const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
1346
1890
  if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1347
1891
  const session = getSessionStatus(cwd, req.sessionId);
1348
1892
  return { ok: true, data: { session } };
@@ -1379,17 +1923,17 @@ async function handleRequest(req) {
1379
1923
  let cwd = sessionCwdMap.get(req.sessionId);
1380
1924
  if (!cwd) {
1381
1925
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1382
- if (existsSync6(stateFile)) {
1926
+ if (existsSync7(stateFile)) {
1383
1927
  cwd = req.cwd;
1384
1928
  registerSessionCwd(req.sessionId, cwd);
1385
1929
  } else {
1386
1930
  return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
1387
1931
  }
1388
1932
  }
1389
- sessionTmuxMap.set(req.sessionId, req.tmuxSession);
1390
- sessionWindowMap2.set(req.sessionId, req.tmuxWindow);
1391
- const session = await resumeSession(req.sessionId, cwd, req.tmuxSession, req.tmuxWindow, req.message);
1392
- return { ok: true, data: { sessionId: session.id, status: session.status } };
1933
+ const session = await resumeSession(req.sessionId, cwd, req.message);
1934
+ if (session.tmuxSessionName) sessionTmuxMap.set(req.sessionId, session.tmuxSessionName);
1935
+ if (session.tmuxWindowId) sessionWindowMap2.set(req.sessionId, session.tmuxWindowId);
1936
+ return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
1393
1937
  }
1394
1938
  case "register_claude_session": {
1395
1939
  const cwd = sessionCwdMap.get(req.sessionId);
@@ -1407,6 +1951,49 @@ async function handleRequest(req) {
1407
1951
  persistSessionRegistry();
1408
1952
  return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
1409
1953
  }
1954
+ case "kill-agent": {
1955
+ const cwd = sessionCwdMap.get(req.sessionId);
1956
+ if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1957
+ await handleKillAgent(req.sessionId, cwd, req.agentId);
1958
+ return { ok: true, data: { agentId: req.agentId } };
1959
+ }
1960
+ case "restart-agent": {
1961
+ const cwd = sessionCwdMap.get(req.sessionId);
1962
+ if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1963
+ await handleRestartAgent(req.sessionId, cwd, req.agentId);
1964
+ return { ok: true, data: { agentId: req.agentId } };
1965
+ }
1966
+ case "rollback": {
1967
+ let cwd = sessionCwdMap.get(req.sessionId);
1968
+ if (!cwd) {
1969
+ const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1970
+ if (existsSync7(stateFile)) {
1971
+ cwd = req.cwd;
1972
+ registerSessionCwd(req.sessionId, cwd);
1973
+ } else {
1974
+ return { ok: false, error: `Unknown session: ${req.sessionId}` };
1975
+ }
1976
+ }
1977
+ const result = await handleRollback(req.sessionId, cwd, req.toCycle);
1978
+ return { ok: true, data: result };
1979
+ }
1980
+ case "delete": {
1981
+ const activeCwd = sessionCwdMap.get(req.sessionId);
1982
+ if (activeCwd) {
1983
+ try {
1984
+ await handleKill(req.sessionId, activeCwd);
1985
+ } catch {
1986
+ }
1987
+ sessionCwdMap.delete(req.sessionId);
1988
+ sessionTmuxMap.delete(req.sessionId);
1989
+ sessionWindowMap2.delete(req.sessionId);
1990
+ sessionMessageCounters.delete(req.sessionId);
1991
+ persistSessionRegistry();
1992
+ }
1993
+ const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
1994
+ rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
1995
+ return { ok: true };
1996
+ }
1410
1997
  case "pane-exited": {
1411
1998
  const entry = lookupPane(req.paneId);
1412
1999
  if (!entry) return { ok: true };
@@ -1419,6 +2006,37 @@ async function handleRequest(req) {
1419
2006
  await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
1420
2007
  return { ok: true };
1421
2008
  }
2009
+ case "update-task": {
2010
+ const cwd = sessionCwdMap.get(req.sessionId);
2011
+ if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2012
+ await updateTask(cwd, req.sessionId, req.task);
2013
+ return { ok: true };
2014
+ }
2015
+ case "message": {
2016
+ const cwd = sessionCwdMap.get(req.sessionId);
2017
+ if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2018
+ const counter = (sessionMessageCounters.get(req.sessionId) ?? 0) + 1;
2019
+ sessionMessageCounters.set(req.sessionId, counter);
2020
+ const id = `msg-${String(counter).padStart(3, "0")}`;
2021
+ const source = req.source ?? { type: "user" };
2022
+ const summary = req.content.length > 200 ? req.content.slice(0, 200) + "..." : req.content;
2023
+ let filePath;
2024
+ if (req.content.length > 200) {
2025
+ const dir = messagesDir(cwd, req.sessionId);
2026
+ mkdirSync4(dir, { recursive: true });
2027
+ filePath = join5(dir, `${id}.md`);
2028
+ writeFileSync4(filePath, req.content, "utf-8");
2029
+ }
2030
+ await appendMessage(cwd, req.sessionId, {
2031
+ id,
2032
+ source,
2033
+ content: req.content,
2034
+ summary,
2035
+ ...filePath ? { filePath } : {},
2036
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2037
+ });
2038
+ return { ok: true };
2039
+ }
1422
2040
  default:
1423
2041
  return { ok: false, error: `Unknown request type: ${req.type}` };
1424
2042
  }
@@ -1430,7 +2048,7 @@ async function handleRequest(req) {
1430
2048
  function startServer() {
1431
2049
  return new Promise((resolve4, reject) => {
1432
2050
  const sock = socketPath();
1433
- if (existsSync6(sock)) {
2051
+ if (existsSync7(sock)) {
1434
2052
  unlinkSync(sock);
1435
2053
  }
1436
2054
  server = createServer((conn) => {
@@ -1472,7 +2090,7 @@ function stopServer() {
1472
2090
  }
1473
2091
  server.close(() => {
1474
2092
  const sock = socketPath();
1475
- if (existsSync6(sock)) {
2093
+ if (existsSync7(sock)) {
1476
2094
  unlinkSync(sock);
1477
2095
  }
1478
2096
  server = null;
@@ -1483,7 +2101,7 @@ function stopServer() {
1483
2101
 
1484
2102
  // src/daemon/updater.ts
1485
2103
  import { execSync as execSync3 } from "child_process";
1486
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
2104
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
1487
2105
  import { resolve as resolve3 } from "path";
1488
2106
  import { get } from "https";
1489
2107
  function isNewer(latest, current) {
@@ -1500,7 +2118,7 @@ function isNewer(latest, current) {
1500
2118
  function readPackageVersion() {
1501
2119
  for (const rel of ["../package.json", "../../package.json"]) {
1502
2120
  try {
1503
- const raw = readFileSync8(resolve3(import.meta.dirname, rel), "utf-8");
2121
+ const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
1504
2122
  const pkg = JSON.parse(raw);
1505
2123
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
1506
2124
  } catch {
@@ -1607,7 +2225,7 @@ function isProcessAlive(pid) {
1607
2225
  function readPid() {
1608
2226
  const pidFile = daemonPidPath();
1609
2227
  try {
1610
- const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
2228
+ const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
1611
2229
  return pid && isProcessAlive(pid) ? pid : null;
1612
2230
  } catch {
1613
2231
  return null;
@@ -1678,15 +2296,23 @@ async function recoverSessions() {
1678
2296
  let recovered = 0;
1679
2297
  for (const [sessionId, cwd] of entries) {
1680
2298
  const stateFile = statePath(cwd, sessionId);
1681
- if (!existsSync7(stateFile)) {
2299
+ if (!existsSync8(stateFile)) {
1682
2300
  continue;
1683
2301
  }
1684
2302
  try {
1685
- const session = JSON.parse(readFileSync9(stateFile, "utf-8"));
2303
+ const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
1686
2304
  if (session.status === "active" || session.status === "paused") {
1687
2305
  registerSessionCwd(sessionId, cwd);
1688
2306
  resetAgentCounterFromState(sessionId, session.agents ?? []);
1689
2307
  if (session.tmuxSessionName && session.tmuxWindowId) {
2308
+ if (!sessionExists(session.tmuxSessionName)) {
2309
+ if (session.status === "active") {
2310
+ await updateSessionStatus(cwd, sessionId, "paused");
2311
+ console.log(`[sisyphus] Session ${sessionId} paused: tmux session no longer exists`);
2312
+ }
2313
+ recovered++;
2314
+ continue;
2315
+ }
1690
2316
  const livePanes = listPanes(session.tmuxWindowId);
1691
2317
  if (livePanes.length > 0) {
1692
2318
  registerSessionTmux(sessionId, session.tmuxSessionName, session.tmuxWindowId);