sisyphi 0.1.22 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +900 -280
  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,137 +571,205 @@ 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);
427
732
  const cliBin = resolve(import.meta.dirname, "cli.js");
428
733
  const npmBinDir = resolve(import.meta.dirname, "../../.bin");
429
734
  const envExports = [
430
735
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
431
736
  `export SISYPHUS_AGENT_ID='orchestrator'`,
737
+ `export SISYPHUS_CWD='${cwd}'`,
432
738
  `export PATH="${npmBinDir}:$PATH"`
433
739
  ].join(" && ");
434
- let userPrompt;
740
+ let userPrompt = formattedState;
435
741
  if (message) {
436
- userPrompt = `${formattedState}
742
+ userPrompt += `
743
+
744
+ ## Continuation Instructions
437
745
 
438
746
  The user resumed this session with new instructions: ${message}`;
439
747
  } else {
440
- const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
441
748
  const storedPrompt = lastCycle?.nextPrompt;
442
- if (storedPrompt) {
443
- userPrompt = `${formattedState}
749
+ const continuationText = storedPrompt ? storedPrompt : "Review the current session and delegate the next cycle of work.";
750
+ userPrompt += `
444
751
 
445
- ${storedPrompt}`;
446
- } else {
447
- userPrompt = `${formattedState}
752
+ ## Continuation Instructions
448
753
 
449
- Review the current session and delegate the next cycle of work.`;
450
- }
754
+ ${continuationText}`;
451
755
  }
452
756
  const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
453
757
  writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
758
+ if (session.messages && session.messages.length > 0) {
759
+ await drainMessages(cwd, sessionId, session.messages.length);
760
+ }
454
761
  const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
455
762
  const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
456
- 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}')"`;
457
766
  const paneId = createPane(windowId, cwd, "left");
458
767
  sessionOrchestratorPane.set(sessionId, paneId);
459
768
  registerPane(paneId, sessionId, "orchestrator");
460
769
  setPaneTitle(paneId, `Sisyphus`);
461
770
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
462
771
  const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
463
- const bannerCmd = existsSync(bannerPath) ? `cat '${bannerPath}' &&` : "";
772
+ const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
464
773
  const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
465
774
  sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
466
775
  await addOrchestratorCycle(cwd, sessionId, {
@@ -477,7 +786,7 @@ function resolveOrchestratorPane(sessionId, cwd) {
477
786
  const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
478
787
  return lastCycle?.paneId ?? void 0;
479
788
  }
480
- async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
789
+ async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
481
790
  const paneId = resolveOrchestratorPane(sessionId, cwd);
482
791
  if (paneId) {
483
792
  killPane(paneId);
@@ -486,7 +795,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
486
795
  }
487
796
  const windowId = sessionWindowMap.get(sessionId);
488
797
  if (windowId) selectLayout(windowId);
489
- await completeOrchestratorCycle(cwd, sessionId, nextPrompt);
798
+ await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode);
490
799
  const session = getSession(cwd, sessionId);
491
800
  const runningAgents = session.agents.filter((a) => a.status === "running");
492
801
  if (runningAgents.length === 0) {
@@ -505,13 +814,13 @@ function cleanupSessionMaps(sessionId) {
505
814
  }
506
815
 
507
816
  // src/daemon/agent.ts
508
- 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";
509
818
  import { resolve as resolve2 } from "path";
510
819
 
511
820
  // src/daemon/worktree.ts
512
821
  import { execSync as execSync2 } from "child_process";
513
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, rmSync } from "fs";
514
- import { dirname as dirname3, 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";
515
824
  var EXEC_ENV2 = {
516
825
  ...process.env,
517
826
  PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
@@ -539,10 +848,10 @@ function loadWorktreeConfig(cwd) {
539
848
  }
540
849
  function createWorktreeShell(cwd, sessionId, agentId) {
541
850
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
542
- const worktreePath = join2(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
851
+ const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
543
852
  mkdirSync2(dirname3(worktreePath), { recursive: true });
544
853
  execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
545
- if (existsSync2(worktreePath)) {
854
+ if (existsSync4(worktreePath)) {
546
855
  execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
547
856
  }
548
857
  execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
@@ -553,16 +862,16 @@ function createWorktreeShell(cwd, sessionId, agentId) {
553
862
  function bootstrapWorktree(cwd, worktreePath, config) {
554
863
  if (config.copy) {
555
864
  for (const entry of config.copy) {
556
- const dest = join2(worktreePath, entry);
865
+ const dest = join4(worktreePath, entry);
557
866
  mkdirSync2(dirname3(dest), { recursive: true });
558
- execSafe2(`cp -r ${shellQuote2(join2(cwd, entry))} ${shellQuote2(dest)}`);
867
+ execSafe2(`cp -r ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
559
868
  }
560
869
  }
561
870
  if (config.clone) {
562
871
  for (const entry of config.clone) {
563
- const dest = join2(worktreePath, entry);
872
+ const dest = join4(worktreePath, entry);
564
873
  mkdirSync2(dirname3(dest), { recursive: true });
565
- const src = shellQuote2(join2(cwd, entry));
874
+ const src = shellQuote2(join4(cwd, entry));
566
875
  const dstQ = shellQuote2(dest);
567
876
  if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
568
877
  execSafe2(`cp -r ${src} ${dstQ}`);
@@ -571,9 +880,9 @@ function bootstrapWorktree(cwd, worktreePath, config) {
571
880
  }
572
881
  if (config.symlink) {
573
882
  for (const entry of config.symlink) {
574
- const dest = join2(worktreePath, entry);
883
+ const dest = join4(worktreePath, entry);
575
884
  mkdirSync2(dirname3(dest), { recursive: true });
576
- execSafe2(`ln -s ${shellQuote2(join2(cwd, entry))} ${shellQuote2(dest)}`);
885
+ execSafe2(`ln -s ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
577
886
  }
578
887
  }
579
888
  if (config.init) {
@@ -645,9 +954,9 @@ function cleanupWorktree(cwd, worktreePath, branchName) {
645
954
  execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
646
955
  const baseDir = dirname3(worktreePath);
647
956
  try {
648
- const entries = readdirSync2(baseDir);
957
+ const entries = readdirSync4(baseDir);
649
958
  if (entries.length === 0) {
650
- rmSync(baseDir, { recursive: true });
959
+ rmSync2(baseDir, { recursive: true });
651
960
  }
652
961
  } catch {
653
962
  }
@@ -656,89 +965,37 @@ function countWorktreeAgents(agents) {
656
965
  return agents.filter((a) => a.worktreePath && a.status === "running").length;
657
966
  }
658
967
 
659
- // src/daemon/frontmatter.ts
660
- import { readFileSync as readFileSync5, existsSync as existsSync3 } from "fs";
661
- import { homedir } from "os";
662
- import { join as join3 } from "path";
663
- function detectProvider(model) {
664
- if (!model) return "anthropic";
665
- if (/^(gpt-|codex-)/.test(model)) return "openai";
666
- return "anthropic";
667
- }
668
- function parseAgentFrontmatter(content) {
669
- const match = content.match(/^---\n([\s\S]*?)\n---/);
670
- if (!match) return {};
671
- const block = match[1];
672
- const fm = {};
673
- const str = (key) => {
674
- const m = block.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
675
- return m ? m[1].trim() : void 0;
676
- };
677
- fm.name = str("name");
678
- fm.model = str("model");
679
- fm.color = str("color");
680
- fm.description = str("description");
681
- fm.permissionMode = str("permissionMode");
682
- const skillsMatch = block.match(/^skills:\s*\n((?:\s+-\s+.+\n?)*)/m);
683
- if (skillsMatch) {
684
- fm.skills = skillsMatch[1].split("\n").map((line) => line.replace(/^\s+-\s+/, "").trim()).filter(Boolean);
685
- }
686
- return fm;
687
- }
688
- function extractAgentBody(content) {
689
- const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
690
- return match ? match[1].trim() : content.trim();
691
- }
692
- 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;
693
973
  try {
694
- const registryPath2 = join3(homedir(), ".claude", "plugins", "installed_plugins.json");
695
- const registry = JSON.parse(readFileSync5(registryPath2, "utf-8"));
696
- for (const key of Object.keys(registry)) {
697
- if (key.startsWith(`${namespace}@`)) {
698
- 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
+ }
699
989
  }
700
990
  }
701
- } catch {
702
- }
703
- return null;
704
- }
705
- function resolveAgentTypePath(agentType, pluginDir, cwd) {
706
- if (!agentType) return null;
707
- let namespace;
708
- let name;
709
- if (agentType.includes(":")) {
710
- [namespace, name] = agentType.split(":", 2);
711
- } else {
712
- name = agentType;
713
- }
714
- const searchPaths = [];
715
- if (namespace) {
716
- searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
717
- const installPath = findPluginInstallPath(namespace);
718
- if (installPath) {
719
- 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;
720
998
  }
721
- } else {
722
- searchPaths.push(join3(cwd, ".claude", "agents", `${name}.md`));
723
- searchPaths.push(join3(homedir(), ".claude", "agents", `${name}.md`));
724
- searchPaths.push(join3(pluginDir, "agents", `${name}.md`));
725
- }
726
- for (const path of searchPaths) {
727
- if (existsSync3(path)) return path;
728
- }
729
- return null;
730
- }
731
- function resolveAgentConfig(agentType, pluginDir, cwd) {
732
- const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
733
- if (!filePath) return null;
734
- try {
735
- const content = readFileSync5(filePath, "utf-8");
736
- return {
737
- frontmatter: parseAgentFrontmatter(content),
738
- body: extractAgentBody(content),
739
- filePath
740
- };
741
- } catch {
742
999
  return null;
743
1000
  }
744
1001
  }
@@ -760,7 +1017,7 @@ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
760
1017
  const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
761
1018
  let template;
762
1019
  try {
763
- template = readFileSync6(templatePath, "utf-8");
1020
+ template = readFileSync5(templatePath, "utf-8");
764
1021
  } catch {
765
1022
  template = `# Sisyphus Agent
766
1023
  Session: {{SESSION_ID}}
@@ -776,13 +1033,33 @@ Task: {{INSTRUCTION}}`;
776
1033
  }
777
1034
  return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
778
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
+ }
779
1056
  async function spawnAgent(opts) {
780
1057
  const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
781
1058
  const count = (agentCounters.get(sessionId) ?? 0) + 1;
782
1059
  agentCounters.set(sessionId, count);
783
1060
  const agentId = `agent-${String(count).padStart(3, "0")}`;
784
- const pluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
785
- const agentConfig = resolveAgentConfig(agentType, pluginPath, cwd);
1061
+ const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1062
+ const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
786
1063
  const provider = detectProvider(agentConfig?.frontmatter.model);
787
1064
  const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
788
1065
  let paneCwd = cwd;
@@ -808,12 +1085,13 @@ async function spawnAgent(opts) {
808
1085
  const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
809
1086
  writeFileSync3(suffixFilePath, suffix, "utf-8");
810
1087
  const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
811
- const bannerCmd = existsSync4(bannerPath) ? `cat '${bannerPath}' &&` : "";
1088
+ const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
812
1089
  const cliBin = resolve2(import.meta.dirname, "cli.js");
813
1090
  const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
814
1091
  const envExports = [
815
1092
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
816
1093
  `export SISYPHUS_AGENT_ID='${agentId}'`,
1094
+ `export SISYPHUS_CWD='${cwd}'`,
817
1095
  ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
818
1096
  `export PATH="${npmBinDir}:$PATH"`
819
1097
  ].join(" && ");
@@ -834,7 +1112,10 @@ ${instruction}`);
834
1112
  mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
835
1113
  } else {
836
1114
  const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
837
- 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)}`;
838
1119
  }
839
1120
  const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
840
1121
  const agent = {
@@ -872,10 +1153,98 @@ ${instruction}`);
872
1153
  }
873
1154
  return agent;
874
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
+ }
875
1244
  function nextReportNumber(cwd, sessionId, agentId) {
876
1245
  const dir = reportsDir(cwd, sessionId);
877
1246
  try {
878
- 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"));
879
1248
  return String(files.length + 1).padStart(3, "0");
880
1249
  } catch {
881
1250
  return "001";
@@ -894,6 +1263,12 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
894
1263
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
895
1264
  };
896
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
+ });
897
1272
  }
898
1273
  async function handleAgentSubmit(cwd, sessionId, agentId, report) {
899
1274
  const dir = reportsDir(cwd, sessionId);
@@ -907,20 +1282,26 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
907
1282
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
908
1283
  };
909
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
+ });
910
1291
  await updateAgent(cwd, sessionId, agentId, {
911
1292
  status: "completed",
912
1293
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
913
1294
  });
914
1295
  const session = getSession(cwd, sessionId);
915
- const agentArr = session.agents;
916
- const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
917
- if (agent) {
918
- unregisterPane(agent.paneId);
919
- 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
+ }
920
1303
  }
921
- const windowId = getWindowId(sessionId);
922
- if (windowId) selectLayout(windowId);
923
- return allAgentsDone(session);
1304
+ return allAgentsDone(getSession(cwd, sessionId));
924
1305
  }
925
1306
  async function handleAgentKilled(cwd, sessionId, agentId, reason) {
926
1307
  unregisterAgentPane(sessionId, agentId);
@@ -1006,7 +1387,15 @@ async function pollSession(sessionId, cwd, windowId) {
1006
1387
  }
1007
1388
  if (session.status !== "active") return;
1008
1389
  const livePanes = listPanes(windowId);
1009
- 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
+ }
1010
1399
  const livePaneIds = new Set(livePanes.map((p) => p.paneId));
1011
1400
  let paneRemoved = false;
1012
1401
  for (const agent of session.agents) {
@@ -1036,23 +1425,34 @@ async function pollSession(sessionId, cwd, windowId) {
1036
1425
  }
1037
1426
 
1038
1427
  // src/daemon/session-manager.ts
1039
- async function startSession(task, cwd, tmuxSession, windowId, context) {
1428
+ var NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
1429
+ async function startSession(task, cwd, context, name) {
1040
1430
  const sessionId = uuidv4();
1041
- const session = createSession(sessionId, task, cwd, context);
1042
- await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
1043
- 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);
1044
1443
  await spawnOrchestrator(sessionId, cwd, windowId);
1045
1444
  updateTrackedWindow(sessionId, windowId);
1445
+ killPane(initialPaneId);
1046
1446
  pruneOldSessions(cwd);
1047
- return session;
1447
+ return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
1048
1448
  }
1049
1449
  var PRUNE_KEEP_COUNT = 10;
1050
1450
  var PRUNE_KEEP_DAYS = 7;
1051
1451
  function pruneOldSessions(cwd) {
1052
1452
  try {
1053
1453
  const dir = sessionsDir(cwd);
1054
- if (!existsSync5(dir)) return;
1055
- const entries = readdirSync4(dir, { withFileTypes: true });
1454
+ if (!existsSync6(dir)) return;
1455
+ const entries = readdirSync6(dir, { withFileTypes: true });
1056
1456
  const candidates = [];
1057
1457
  for (const entry of entries) {
1058
1458
  if (!entry.isDirectory()) continue;
@@ -1075,14 +1475,25 @@ function pruneOldSessions(cwd) {
1075
1475
  }
1076
1476
  for (const c of candidates) {
1077
1477
  if (keep.has(c.id)) continue;
1078
- rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
1478
+ rmSync3(sessionDir(cwd, c.id), { recursive: true, force: true });
1079
1479
  }
1080
1480
  } catch (err) {
1081
1481
  console.error("[sisyphus] Session pruning failed:", err);
1082
1482
  }
1083
1483
  }
1084
- async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
1484
+ async function resumeSession(sessionId, cwd, message) {
1085
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
+ }
1086
1497
  if (session.status !== "active") {
1087
1498
  const livePaneIds = /* @__PURE__ */ new Set();
1088
1499
  if (session.tmuxWindowId) {
@@ -1105,12 +1516,16 @@ async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
1105
1516
  }
1106
1517
  }
1107
1518
  await updateSessionStatus(cwd, sessionId, "active");
1108
- await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
1519
+ await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
1109
1520
  resetAgentCounterFromState(sessionId, session.agents);
1110
1521
  resetColors(sessionId);
1111
- trackSession(sessionId, cwd, tmuxSession);
1522
+ orchestratorDone.delete(sessionId);
1523
+ trackSession(sessionId, cwd, tmuxName);
1112
1524
  await spawnOrchestrator(sessionId, cwd, windowId, message);
1113
1525
  updateTrackedWindow(sessionId, windowId);
1526
+ if (initialPaneId) {
1527
+ killPane(initialPaneId);
1528
+ }
1114
1529
  return getSession(cwd, sessionId);
1115
1530
  }
1116
1531
  function getSessionStatus(cwd, sessionId) {
@@ -1118,8 +1533,8 @@ function getSessionStatus(cwd, sessionId) {
1118
1533
  }
1119
1534
  function listSessions(cwd) {
1120
1535
  const dir = sessionsDir(cwd);
1121
- if (!existsSync5(dir)) return [];
1122
- const entries = readdirSync4(dir, { withFileTypes: true });
1536
+ if (!existsSync6(dir)) return [];
1537
+ const entries = readdirSync6(dir, { withFileTypes: true });
1123
1538
  const sessions = [];
1124
1539
  for (const entry of entries) {
1125
1540
  if (!entry.isDirectory()) continue;
@@ -1127,10 +1542,13 @@ function listSessions(cwd) {
1127
1542
  const session = getSession(cwd, entry.name);
1128
1543
  sessions.push({
1129
1544
  id: session.id,
1545
+ name: session.name,
1130
1546
  task: session.task,
1131
1547
  status: session.status,
1132
1548
  createdAt: session.createdAt,
1133
- agentCount: session.agents.length
1549
+ agentCount: session.agents.length,
1550
+ tmuxSessionName: session.tmuxSessionName,
1551
+ tmuxWindowId: session.tmuxWindowId
1134
1552
  });
1135
1553
  } catch (err) {
1136
1554
  console.error(`[sisyphus] Failed to read session ${entry.name}:`, err);
@@ -1139,11 +1557,17 @@ function listSessions(cwd) {
1139
1557
  return sessions;
1140
1558
  }
1141
1559
  var pendingRespawns = /* @__PURE__ */ new Set();
1560
+ var orchestratorDone = /* @__PURE__ */ new Set();
1142
1561
  function onAllAgentsDone2(sessionId, cwd, windowId) {
1143
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
+ }
1144
1567
  const session = getSession(cwd, sessionId);
1145
1568
  if (session.status !== "active") return;
1146
1569
  pendingRespawns.add(sessionId);
1570
+ orchestratorDone.delete(sessionId);
1147
1571
  const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
1148
1572
  if (worktreeAgents.length > 0) {
1149
1573
  const results = mergeWorktrees(cwd, worktreeAgents);
@@ -1155,9 +1579,42 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
1155
1579
  }).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
1156
1580
  }
1157
1581
  }
1158
- setImmediate(() => {
1582
+ const cycleNumber = session.orchestratorCycles.length;
1583
+ if (cycleNumber > 0) {
1584
+ createSnapshot(cwd, sessionId, cycleNumber);
1585
+ }
1586
+ setImmediate(async () => {
1159
1587
  pendingRespawns.delete(sessionId);
1160
- 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
+ }
1161
1618
  });
1162
1619
  }
1163
1620
  async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
@@ -1189,16 +1646,17 @@ async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
1189
1646
  async function handleReport(cwd, sessionId, agentId, content) {
1190
1647
  await handleAgentReport(cwd, sessionId, agentId, content);
1191
1648
  }
1192
- async function handleYield(sessionId, cwd, nextPrompt) {
1649
+ async function handleYield(sessionId, cwd, nextPrompt, mode) {
1193
1650
  const pre = getSession(cwd, sessionId);
1194
1651
  if (pre.status === "paused") {
1195
1652
  await updateSessionStatus(cwd, sessionId, "active");
1196
1653
  }
1197
- await handleOrchestratorYield(sessionId, cwd, nextPrompt);
1654
+ await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
1655
+ orchestratorDone.add(sessionId);
1198
1656
  const session = getSession(cwd, sessionId);
1199
1657
  const hasRunningAgents = session.agents.some((a) => a.status === "running");
1200
1658
  if (!hasRunningAgents) {
1201
- const windowId = getWindowId(sessionId);
1659
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1202
1660
  if (windowId) {
1203
1661
  onAllAgentsDone2(sessionId, cwd, windowId);
1204
1662
  }
@@ -1207,6 +1665,9 @@ async function handleYield(sessionId, cwd, nextPrompt) {
1207
1665
  async function handleComplete(sessionId, cwd, report) {
1208
1666
  await handleOrchestratorComplete(sessionId, cwd, report);
1209
1667
  }
1668
+ async function handleContinue(sessionId, cwd) {
1669
+ await continueSession(cwd, sessionId);
1670
+ }
1210
1671
  async function handleRegisterClaudeSession(cwd, sessionId, agentId, claudeSessionId) {
1211
1672
  await updateAgent(cwd, sessionId, agentId, { claudeSessionId });
1212
1673
  }
@@ -1236,12 +1697,81 @@ async function handleKill(sessionId, cwd) {
1236
1697
  await updateSessionStatus(cwd, sessionId, "completed");
1237
1698
  untrackSession(sessionId);
1238
1699
  unregisterSessionPanes(sessionId);
1239
- if (windowId) {
1700
+ if (session.tmuxSessionName) {
1701
+ killSession(session.tmuxSessionName);
1702
+ } else if (windowId) {
1240
1703
  killWindow(windowId);
1241
1704
  }
1242
1705
  clearAgentCounter(sessionId);
1706
+ orchestratorDone.delete(sessionId);
1243
1707
  return killedAgents;
1244
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
+ }
1245
1775
  async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1246
1776
  const session = getSession(cwd, sessionId);
1247
1777
  if (session.status !== "active") return;
@@ -1250,15 +1780,16 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1250
1780
  if (!agent || agent.status !== "running") return;
1251
1781
  const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
1252
1782
  if (allDone) {
1253
- const windowId = getWindowId(sessionId);
1783
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1254
1784
  if (windowId) {
1255
1785
  onAllAgentsDone2(sessionId, cwd, windowId);
1256
1786
  }
1257
1787
  }
1258
1788
  } else if (role === "orchestrator") {
1789
+ orchestratorDone.add(sessionId);
1259
1790
  const hasRunningAgents = session.agents.some((a) => a.status === "running");
1260
1791
  if (!hasRunningAgents && session.agents.length > 0) {
1261
- const windowId = getWindowId(sessionId);
1792
+ const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
1262
1793
  if (windowId) {
1263
1794
  console.log(`[sisyphus] Orchestrator pane exited for session ${sessionId}, all agents done \u2014 triggering respawn`);
1264
1795
  onAllAgentsDone2(sessionId, cwd, windowId);
@@ -1273,10 +1804,11 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1273
1804
  // src/daemon/server.ts
1274
1805
  var server = null;
1275
1806
  var sessionCwdMap = /* @__PURE__ */ new Map();
1807
+ var sessionMessageCounters = /* @__PURE__ */ new Map();
1276
1808
  var sessionTmuxMap = /* @__PURE__ */ new Map();
1277
1809
  var sessionWindowMap2 = /* @__PURE__ */ new Map();
1278
1810
  function registryPath() {
1279
- return join4(globalDir(), "session-registry.json");
1811
+ return join5(globalDir(), "session-registry.json");
1280
1812
  }
1281
1813
  function persistSessionRegistry() {
1282
1814
  const dir = globalDir();
@@ -1289,9 +1821,9 @@ function persistSessionRegistry() {
1289
1821
  }
1290
1822
  function loadSessionRegistry() {
1291
1823
  const p = registryPath();
1292
- if (!existsSync6(p)) return {};
1824
+ if (!existsSync7(p)) return {};
1293
1825
  try {
1294
- return JSON.parse(readFileSync7(p, "utf-8"));
1826
+ return JSON.parse(readFileSync6(p, "utf-8"));
1295
1827
  } catch {
1296
1828
  return {};
1297
1829
  }
@@ -1308,11 +1840,11 @@ async function handleRequest(req) {
1308
1840
  try {
1309
1841
  switch (req.type) {
1310
1842
  case "start": {
1311
- 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);
1312
1844
  registerSessionCwd(session.id, req.cwd);
1313
- sessionTmuxMap.set(session.id, req.tmuxSession);
1314
- sessionWindowMap2.set(session.id, req.tmuxWindow);
1315
- 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 } };
1316
1848
  }
1317
1849
  case "spawn": {
1318
1850
  const cwd = sessionCwdMap.get(req.sessionId);
@@ -1337,7 +1869,7 @@ async function handleRequest(req) {
1337
1869
  case "yield": {
1338
1870
  const cwd = sessionCwdMap.get(req.sessionId);
1339
1871
  if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1340
- await handleYield(req.sessionId, cwd, req.nextPrompt);
1872
+ await handleYield(req.sessionId, cwd, req.nextPrompt, req.mode);
1341
1873
  return { ok: true };
1342
1874
  }
1343
1875
  case "complete": {
@@ -1346,9 +1878,15 @@ async function handleRequest(req) {
1346
1878
  await handleComplete(req.sessionId, cwd, req.report);
1347
1879
  return { ok: true };
1348
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
+ }
1349
1887
  case "status": {
1350
1888
  if (req.sessionId) {
1351
- const cwd = sessionCwdMap.get(req.sessionId);
1889
+ const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
1352
1890
  if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1353
1891
  const session = getSessionStatus(cwd, req.sessionId);
1354
1892
  return { ok: true, data: { session } };
@@ -1385,17 +1923,17 @@ async function handleRequest(req) {
1385
1923
  let cwd = sessionCwdMap.get(req.sessionId);
1386
1924
  if (!cwd) {
1387
1925
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1388
- if (existsSync6(stateFile)) {
1926
+ if (existsSync7(stateFile)) {
1389
1927
  cwd = req.cwd;
1390
1928
  registerSessionCwd(req.sessionId, cwd);
1391
1929
  } else {
1392
1930
  return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
1393
1931
  }
1394
1932
  }
1395
- sessionTmuxMap.set(req.sessionId, req.tmuxSession);
1396
- sessionWindowMap2.set(req.sessionId, req.tmuxWindow);
1397
- const session = await resumeSession(req.sessionId, cwd, req.tmuxSession, req.tmuxWindow, req.message);
1398
- 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 } };
1399
1937
  }
1400
1938
  case "register_claude_session": {
1401
1939
  const cwd = sessionCwdMap.get(req.sessionId);
@@ -1413,6 +1951,49 @@ async function handleRequest(req) {
1413
1951
  persistSessionRegistry();
1414
1952
  return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
1415
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
+ }
1416
1997
  case "pane-exited": {
1417
1998
  const entry = lookupPane(req.paneId);
1418
1999
  if (!entry) return { ok: true };
@@ -1425,6 +2006,37 @@ async function handleRequest(req) {
1425
2006
  await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
1426
2007
  return { ok: true };
1427
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
+ }
1428
2040
  default:
1429
2041
  return { ok: false, error: `Unknown request type: ${req.type}` };
1430
2042
  }
@@ -1436,7 +2048,7 @@ async function handleRequest(req) {
1436
2048
  function startServer() {
1437
2049
  return new Promise((resolve4, reject) => {
1438
2050
  const sock = socketPath();
1439
- if (existsSync6(sock)) {
2051
+ if (existsSync7(sock)) {
1440
2052
  unlinkSync(sock);
1441
2053
  }
1442
2054
  server = createServer((conn) => {
@@ -1478,7 +2090,7 @@ function stopServer() {
1478
2090
  }
1479
2091
  server.close(() => {
1480
2092
  const sock = socketPath();
1481
- if (existsSync6(sock)) {
2093
+ if (existsSync7(sock)) {
1482
2094
  unlinkSync(sock);
1483
2095
  }
1484
2096
  server = null;
@@ -1489,7 +2101,7 @@ function stopServer() {
1489
2101
 
1490
2102
  // src/daemon/updater.ts
1491
2103
  import { execSync as execSync3 } from "child_process";
1492
- 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";
1493
2105
  import { resolve as resolve3 } from "path";
1494
2106
  import { get } from "https";
1495
2107
  function isNewer(latest, current) {
@@ -1506,7 +2118,7 @@ function isNewer(latest, current) {
1506
2118
  function readPackageVersion() {
1507
2119
  for (const rel of ["../package.json", "../../package.json"]) {
1508
2120
  try {
1509
- const raw = readFileSync8(resolve3(import.meta.dirname, rel), "utf-8");
2121
+ const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
1510
2122
  const pkg = JSON.parse(raw);
1511
2123
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
1512
2124
  } catch {
@@ -1613,7 +2225,7 @@ function isProcessAlive(pid) {
1613
2225
  function readPid() {
1614
2226
  const pidFile = daemonPidPath();
1615
2227
  try {
1616
- const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
2228
+ const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
1617
2229
  return pid && isProcessAlive(pid) ? pid : null;
1618
2230
  } catch {
1619
2231
  return null;
@@ -1684,15 +2296,23 @@ async function recoverSessions() {
1684
2296
  let recovered = 0;
1685
2297
  for (const [sessionId, cwd] of entries) {
1686
2298
  const stateFile = statePath(cwd, sessionId);
1687
- if (!existsSync7(stateFile)) {
2299
+ if (!existsSync8(stateFile)) {
1688
2300
  continue;
1689
2301
  }
1690
2302
  try {
1691
- const session = JSON.parse(readFileSync9(stateFile, "utf-8"));
2303
+ const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
1692
2304
  if (session.status === "active" || session.status === "paused") {
1693
2305
  registerSessionCwd(sessionId, cwd);
1694
2306
  resetAgentCounterFromState(sessionId, session.agents ?? []);
1695
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
+ }
1696
2316
  const livePanes = listPanes(session.tmuxWindowId);
1697
2317
  if (livePanes.length > 0) {
1698
2318
  registerSessionTmux(sessionId, session.tmuxSessionName, session.tmuxWindowId);