sisyphi 0.1.4 → 0.1.6

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.
package/dist/daemon.js CHANGED
@@ -20,12 +20,13 @@ import {
20
20
  } from "./chunk-N2BPQOO2.js";
21
21
 
22
22
  // src/daemon/index.ts
23
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
23
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2, existsSync as existsSync6 } from "fs";
24
+ import { setTimeout as sleep } from "timers/promises";
24
25
 
25
26
  // src/shared/config.ts
26
27
  import { readFileSync } from "fs";
27
28
  var DEFAULT_CONFIG = {
28
- pollIntervalMs: 1e3
29
+ pollIntervalMs: 5e3
29
30
  };
30
31
  function readJsonFile(filePath) {
31
32
  try {
@@ -43,23 +44,20 @@ function loadConfig(cwd) {
43
44
 
44
45
  // src/daemon/server.ts
45
46
  import { createServer } from "net";
46
- import { unlinkSync, existsSync as existsSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync7, mkdirSync as mkdirSync4 } from "fs";
47
+ import { unlinkSync, existsSync as existsSync5, writeFileSync as writeFileSync4, readFileSync as readFileSync8, mkdirSync as mkdirSync4 } from "fs";
47
48
  import { join as join4 } from "path";
48
49
 
49
50
  // src/daemon/session-manager.ts
50
51
  import { v4 as uuidv4 } from "uuid";
51
- import { existsSync as existsSync4, readdirSync as readdirSync4 } from "fs";
52
+ import { existsSync as existsSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
52
53
 
53
54
  // src/daemon/state.ts
54
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
55
- import { dirname, join } from "path";
56
55
  import { randomUUID } from "crypto";
56
+ import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
57
+ import { dirname, join } from "path";
57
58
  var PLAN_SEED = `---
58
59
  description: >
59
- Living document of what still needs to happen. Write your remaining work plan
60
- here: phases, next steps, file references, open questions. Remove or collapse
61
- items as they're completed so this file only reflects outstanding work. The
62
- orchestrator sees this every cycle \u2014 keep it focused and current.
60
+ Living document of what still needs to happen. Write out ne
63
61
  ---
64
62
  `;
65
63
  var LOGS_SEED = `---
@@ -67,22 +65,22 @@ description: >
67
65
  Session memory. Record important observations, decisions, and findings here.
68
66
  This is your persistent memory across cycles: things you tried, what
69
67
  worked/failed, design decisions and their rationale, gotchas discovered during
70
- implementation. Unlike plan.md, entries here accumulate \u2014 they're a log.
68
+ implementation.
71
69
  ---
72
70
  `;
73
71
  var sessionLocks = /* @__PURE__ */ new Map();
74
72
  async function withSessionLock(sessionId, fn) {
75
73
  const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
76
- let resolve3;
74
+ let resolve4;
77
75
  const next = new Promise((r) => {
78
- resolve3 = r;
76
+ resolve4 = r;
79
77
  });
80
78
  sessionLocks.set(sessionId, next);
81
79
  await prev;
82
80
  try {
83
81
  return fn();
84
82
  } finally {
85
- resolve3();
83
+ resolve4();
86
84
  }
87
85
  }
88
86
  function atomicWrite(filePath, data) {
@@ -331,6 +329,33 @@ function shellQuote(s) {
331
329
  return `'${s.replace(/'/g, "'\\''")}'`;
332
330
  }
333
331
 
332
+ // src/daemon/pane-registry.ts
333
+ var paneMap = /* @__PURE__ */ new Map();
334
+ function registerPane(paneId, sessionId, role, agentId) {
335
+ paneMap.set(paneId, { sessionId, role, agentId });
336
+ }
337
+ function unregisterPane(paneId) {
338
+ paneMap.delete(paneId);
339
+ }
340
+ function unregisterAgentPane(sessionId, agentId) {
341
+ for (const [paneId, entry] of paneMap) {
342
+ if (entry.sessionId === sessionId && entry.agentId === agentId) {
343
+ paneMap.delete(paneId);
344
+ return;
345
+ }
346
+ }
347
+ }
348
+ function unregisterSessionPanes(sessionId) {
349
+ for (const [paneId, entry] of paneMap) {
350
+ if (entry.sessionId === sessionId) {
351
+ paneMap.delete(paneId);
352
+ }
353
+ }
354
+ }
355
+ function lookupPane(paneId) {
356
+ return paneMap.get(paneId);
357
+ }
358
+
334
359
  // src/daemon/orchestrator.ts
335
360
  var sessionWindowMap = /* @__PURE__ */ new Map();
336
361
  var sessionOrchestratorPane = /* @__PURE__ */ new Map();
@@ -388,10 +413,13 @@ function formatStateForOrchestrator(session) {
388
413
  if (worktreeAgents.length > 0) {
389
414
  const wtLines = worktreeAgents.map((a) => {
390
415
  if (a.mergeStatus === "conflict") {
391
- return `- ${a.id}: conflict \u2014 ${a.mergeDetails ?? "unknown"}
416
+ return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
392
417
  Branch: ${a.branchName}
393
418
  Worktree: ${a.worktreePath}`;
394
419
  }
420
+ if (a.mergeStatus === "no-changes") {
421
+ return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
422
+ }
395
423
  const status = a.mergeStatus ?? "pending";
396
424
  return `- ${a.id}: ${status} (branch ${a.branchName})`;
397
425
  }).join("\n");
@@ -462,11 +490,13 @@ Review the current session and delegate the next cycle of work.`;
462
490
  const claudeCmd = `claude --dangerously-skip-permissions --settings "${settingsPath}" --plugin-dir "${pluginPath}" --append-system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
463
491
  const paneId = createPane(windowId, cwd);
464
492
  sessionOrchestratorPane.set(sessionId, paneId);
493
+ registerPane(paneId, sessionId, "orchestrator");
465
494
  setPaneTitle(paneId, `orchestrator (${sessionId.slice(0, 8)})`);
466
495
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
467
496
  const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
468
497
  const bannerCmd = existsSync(bannerPath) ? `cat '${bannerPath}' &&` : "";
469
- sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
498
+ const notifyCmd = `sisyphus notify pane-exited --pane-id ${paneId}`;
499
+ sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
470
500
  await addOrchestratorCycle(cwd, sessionId, {
471
501
  cycle: cycleNum,
472
502
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -485,6 +515,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
485
515
  const paneId = resolveOrchestratorPane(sessionId, cwd);
486
516
  if (paneId) {
487
517
  killPane(paneId);
518
+ unregisterPane(paneId);
488
519
  sessionOrchestratorPane.delete(sessionId);
489
520
  }
490
521
  const windowId = sessionWindowMap.get(sessionId);
@@ -493,7 +524,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
493
524
  const session = getSession(cwd, sessionId);
494
525
  const runningAgents = session.agents.filter((a) => a.status === "running");
495
526
  if (runningAgents.length === 0) {
496
- console.error(`[sisyphus] WARNING: Orchestrator yielded but no agents are running for session ${sessionId}`);
527
+ console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
497
528
  }
498
529
  }
499
530
  async function handleOrchestratorComplete(sessionId, cwd, report) {
@@ -504,6 +535,7 @@ async function handleOrchestratorComplete(sessionId, cwd, report) {
504
535
  function cleanupSessionMaps(sessionId) {
505
536
  sessionOrchestratorPane.delete(sessionId);
506
537
  sessionWindowMap.delete(sessionId);
538
+ unregisterSessionPanes(sessionId);
507
539
  }
508
540
 
509
541
  // src/daemon/agent.ts
@@ -539,23 +571,17 @@ function loadWorktreeConfig(cwd) {
539
571
  return null;
540
572
  }
541
573
  }
542
- function createWorktree(cwd, sessionId, agentId) {
574
+ function createWorktreeShell(cwd, sessionId, agentId) {
543
575
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
544
- const worktreePath = join3(worktreeBaseDir(cwd), agentId);
576
+ const worktreePath = join3(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
545
577
  mkdirSync2(dirname2(worktreePath), { recursive: true });
578
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
579
+ if (existsSync2(worktreePath)) {
580
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
581
+ }
582
+ execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
546
583
  exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
547
584
  exec2(`git -C ${shellQuote2(cwd)} worktree add ${shellQuote2(worktreePath)} ${shellQuote2(branchName)}`);
548
- const symlinks = [".sisyphus", ".claude"];
549
- for (const entry of symlinks) {
550
- const src = join3(cwd, entry);
551
- if (existsSync2(src)) {
552
- execSafe2(`ln -s ${shellQuote2(src)} ${shellQuote2(join3(worktreePath, entry))}`);
553
- }
554
- }
555
- const config = loadWorktreeConfig(cwd);
556
- if (config) {
557
- bootstrapWorktree(cwd, worktreePath, config);
558
- }
559
585
  return { worktreePath, branchName };
560
586
  }
561
587
  function bootstrapWorktree(cwd, worktreePath, config) {
@@ -615,6 +641,8 @@ function mergeWorktrees(cwd, agents) {
615
641
  (a) => a.worktreePath && a.mergeStatus === "pending"
616
642
  );
617
643
  const results = [];
644
+ execSafe2(`git -C ${shellQuote2(cwd)} add .sisyphus`);
645
+ execSafe2(`git -C ${shellQuote2(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
618
646
  for (const agent of pending) {
619
647
  const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
620
648
  if (!branch) {
@@ -637,8 +665,10 @@ function mergeWorktrees(cwd, agents) {
637
665
  results.push({ agentId: agent.id, name: agent.name, status: "merged" });
638
666
  } catch (err) {
639
667
  execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
640
- const stderr = err?.stderr;
641
- const conflictDetails = stderr ? (typeof stderr === "string" ? stderr : stderr.toString("utf-8")).trim() : err instanceof Error ? err.message : String(err);
668
+ const errObj = err;
669
+ const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
670
+ const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
671
+ const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
642
672
  results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
643
673
  }
644
674
  }
@@ -687,7 +717,7 @@ Task: {{INSTRUCTION}}`;
687
717
  if (worktreeContext) {
688
718
  worktreeBlock = [
689
719
  "## Worktree Context",
690
- `You are working in worktree ${worktreeContext.offset} of ${worktreeContext.total} concurrent worktrees on branch \`${worktreeContext.branchName}\`.`,
720
+ `You are working in an isolated git worktree on branch \`${worktreeContext.branchName}\`.`,
691
721
  `If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
692
722
  ].join("\n");
693
723
  }
@@ -705,7 +735,7 @@ async function spawnAgent(opts) {
705
735
  let branchName;
706
736
  let worktreeContext;
707
737
  if (opts.worktree) {
708
- const wt = createWorktree(cwd, sessionId, agentId);
738
+ const wt = createWorktreeShell(cwd, sessionId, agentId);
709
739
  worktreePath = wt.worktreePath;
710
740
  branchName = wt.branchName;
711
741
  paneCwd = worktreePath;
@@ -714,6 +744,7 @@ async function spawnAgent(opts) {
714
744
  worktreeContext = { offset: portOffset, total: portOffset, branchName };
715
745
  }
716
746
  const paneId = createPane(windowId, paneCwd);
747
+ registerPane(paneId, sessionId, "agent", agentId);
717
748
  setPaneTitle(paneId, `${name} (${agentId})`);
718
749
  setPaneStyle(paneId, color);
719
750
  const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
@@ -728,7 +759,8 @@ async function spawnAgent(opts) {
728
759
  ].join(" && ");
729
760
  const agentFlag = agentType ? ` --agent ${shellQuote3(agentType)}` : "";
730
761
  const claudeCmd = `claude --dangerously-skip-permissions --plugin-dir "${pluginPath}"${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
731
- sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
762
+ const notifyCmd = `sisyphus notify pane-exited --pane-id ${paneId}`;
763
+ const fullCmd = `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`;
732
764
  const agent = {
733
765
  id: agentId,
734
766
  name,
@@ -743,6 +775,24 @@ async function spawnAgent(opts) {
743
775
  ...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
744
776
  };
745
777
  await addAgent(cwd, sessionId, agent);
778
+ if (opts.worktree && worktreePath) {
779
+ const config = loadWorktreeConfig(cwd);
780
+ if (config) {
781
+ const wtPath = worktreePath;
782
+ setImmediate(() => {
783
+ try {
784
+ bootstrapWorktree(cwd, wtPath, config);
785
+ } catch (err) {
786
+ console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
787
+ }
788
+ sendKeys(paneId, fullCmd);
789
+ });
790
+ } else {
791
+ sendKeys(paneId, fullCmd);
792
+ }
793
+ } else {
794
+ sendKeys(paneId, fullCmd);
795
+ }
746
796
  return agent;
747
797
  }
748
798
  function nextReportNumber(cwd, sessionId, agentId) {
@@ -788,6 +838,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
788
838
  const agentArr = session.agents;
789
839
  const agent = agentArr.slice().reverse().find((a) => a.id === agentId);
790
840
  if (agent) {
841
+ unregisterPane(agent.paneId);
791
842
  killPane(agent.paneId);
792
843
  }
793
844
  const windowId = getWindowId(sessionId);
@@ -795,6 +846,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
795
846
  return allAgentsDone(session);
796
847
  }
797
848
  async function handleAgentKilled(cwd, sessionId, agentId, reason) {
849
+ unregisterAgentPane(sessionId, agentId);
798
850
  await updateAgent(cwd, sessionId, agentId, {
799
851
  status: "killed",
800
852
  killedReason: reason,
@@ -817,7 +869,7 @@ var onAllAgentsDone = null;
817
869
  function setRespawnCallback(cb) {
818
870
  onAllAgentsDone = cb;
819
871
  }
820
- function startMonitor(pollIntervalMs = 1e3) {
872
+ function startMonitor(pollIntervalMs = 5e3) {
821
873
  if (monitorInterval) return;
822
874
  monitorInterval = setInterval(() => {
823
875
  pollAllSessions().catch((err) => {
@@ -906,16 +958,129 @@ async function pollSession(sessionId, cwd, windowId) {
906
958
  }
907
959
  }
908
960
 
961
+ // src/daemon/updater.ts
962
+ import { execSync as execSync3 } from "child_process";
963
+ import { readFileSync as readFileSync7 } from "fs";
964
+ import { resolve as resolve3 } from "path";
965
+ import { get } from "https";
966
+ function readPackageVersion() {
967
+ for (const rel of ["../package.json", "../../package.json"]) {
968
+ try {
969
+ const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
970
+ const pkg = JSON.parse(raw);
971
+ if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
972
+ } catch {
973
+ }
974
+ }
975
+ return "0.0.0";
976
+ }
977
+ var currentVersion = readPackageVersion();
978
+ function checkForUpdate() {
979
+ return new Promise((resolve4) => {
980
+ const timeout = setTimeout(() => {
981
+ resolve4(null);
982
+ }, 5e3);
983
+ const req = get("https://registry.npmjs.org/sisyphi/latest", (res) => {
984
+ let data = "";
985
+ res.on("data", (chunk) => {
986
+ data += chunk.toString();
987
+ });
988
+ res.on("end", () => {
989
+ clearTimeout(timeout);
990
+ try {
991
+ const { version: latest } = JSON.parse(data);
992
+ if (latest && latest !== currentVersion) {
993
+ resolve4({ current: currentVersion, latest });
994
+ } else {
995
+ resolve4(null);
996
+ }
997
+ } catch {
998
+ resolve4(null);
999
+ }
1000
+ });
1001
+ });
1002
+ req.on("error", () => {
1003
+ clearTimeout(timeout);
1004
+ resolve4(null);
1005
+ });
1006
+ });
1007
+ }
1008
+ function applyUpdate() {
1009
+ try {
1010
+ const nodeDir = resolve3(process.execPath, "..");
1011
+ const env = { ...process.env, PATH: `${nodeDir}:${process.env.PATH ?? ""}` };
1012
+ execSync3("npm install -g sisyphi", { timeout: 15e3, stdio: "pipe", env });
1013
+ return true;
1014
+ } catch (err) {
1015
+ console.error("[sisyphus] Auto-update failed:", err);
1016
+ return false;
1017
+ }
1018
+ }
1019
+ async function checkAndApply() {
1020
+ try {
1021
+ const update = await checkForUpdate();
1022
+ if (!update) return;
1023
+ console.log(`[sisyphus] Update available: ${update.current} \u2192 ${update.latest}`);
1024
+ const success = applyUpdate();
1025
+ if (success) {
1026
+ console.log(`[sisyphus] Updated to ${update.latest}, restarting daemon...`);
1027
+ process.exit(0);
1028
+ }
1029
+ } catch (err) {
1030
+ console.error("[sisyphus] Auto-update check failed:", err);
1031
+ }
1032
+ }
1033
+
909
1034
  // src/daemon/session-manager.ts
910
1035
  async function startSession(task, cwd, tmuxSession, windowId) {
1036
+ const config = loadConfig(cwd);
1037
+ if (config.autoUpdate !== false) {
1038
+ await checkAndApply();
1039
+ }
911
1040
  const sessionId = uuidv4();
912
1041
  const session = createSession(sessionId, task, cwd);
913
1042
  await updateSessionTmux(cwd, sessionId, tmuxSession, windowId);
914
1043
  trackSession(sessionId, cwd, tmuxSession);
915
1044
  await spawnOrchestrator(sessionId, cwd, windowId);
916
1045
  updateTrackedWindow(sessionId, windowId);
1046
+ pruneOldSessions(cwd);
917
1047
  return session;
918
1048
  }
1049
+ var PRUNE_KEEP_COUNT = 10;
1050
+ var PRUNE_KEEP_DAYS = 7;
1051
+ function pruneOldSessions(cwd) {
1052
+ try {
1053
+ const dir = sessionsDir(cwd);
1054
+ if (!existsSync4(dir)) return;
1055
+ const entries = readdirSync4(dir, { withFileTypes: true });
1056
+ const candidates = [];
1057
+ for (const entry of entries) {
1058
+ if (!entry.isDirectory()) continue;
1059
+ try {
1060
+ const session = getSession(cwd, entry.name);
1061
+ if (session.status === "active" || session.status === "paused") continue;
1062
+ candidates.push({ id: session.id, createdAt: new Date(session.createdAt).getTime() });
1063
+ } catch {
1064
+ }
1065
+ }
1066
+ if (candidates.length <= PRUNE_KEEP_COUNT) return;
1067
+ candidates.sort((a, b) => b.createdAt - a.createdAt);
1068
+ const cutoff = Date.now() - PRUNE_KEEP_DAYS * 24 * 60 * 60 * 1e3;
1069
+ const keep = /* @__PURE__ */ new Set();
1070
+ for (let i = 0; i < Math.min(PRUNE_KEEP_COUNT, candidates.length); i++) {
1071
+ keep.add(candidates[i].id);
1072
+ }
1073
+ for (const c of candidates) {
1074
+ if (c.createdAt >= cutoff) keep.add(c.id);
1075
+ }
1076
+ for (const c of candidates) {
1077
+ if (keep.has(c.id)) continue;
1078
+ rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
1079
+ }
1080
+ } catch (err) {
1081
+ console.error("[sisyphus] Session pruning failed:", err);
1082
+ }
1083
+ }
919
1084
  async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
920
1085
  const session = getSession(cwd, sessionId);
921
1086
  if (session.status !== "active") {
@@ -973,23 +1138,27 @@ function listSessions(cwd) {
973
1138
  }
974
1139
  return sessions;
975
1140
  }
1141
+ var pendingRespawns = /* @__PURE__ */ new Set();
976
1142
  function onAllAgentsDone2(sessionId, cwd, windowId) {
1143
+ if (pendingRespawns.has(sessionId)) return;
977
1144
  const session = getSession(cwd, sessionId);
978
1145
  if (session.status !== "active") return;
1146
+ pendingRespawns.add(sessionId);
979
1147
  const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
980
1148
  if (worktreeAgents.length > 0) {
981
1149
  const results = mergeWorktrees(cwd, worktreeAgents);
982
1150
  for (const result of results) {
983
- const mergeStatus = result.status === "conflict" ? "conflict" : "merged";
1151
+ const mergeStatus = result.status;
984
1152
  updateAgent(cwd, sessionId, result.agentId, {
985
1153
  mergeStatus,
986
1154
  mergeDetails: result.conflictDetails
987
1155
  }).catch((err) => console.error(`[sisyphus] Failed to update merge status for ${result.agentId}:`, err));
988
1156
  }
989
1157
  }
990
- setTimeout(() => {
1158
+ setImmediate(() => {
1159
+ pendingRespawns.delete(sessionId);
991
1160
  spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
992
- }, 2e3);
1161
+ });
993
1162
  }
994
1163
  async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
995
1164
  const windowId = getWindowId(sessionId);
@@ -1016,7 +1185,19 @@ async function handleReport(cwd, sessionId, agentId, content) {
1016
1185
  await handleAgentReport(cwd, sessionId, agentId, content);
1017
1186
  }
1018
1187
  async function handleYield(sessionId, cwd, nextPrompt) {
1188
+ const pre = getSession(cwd, sessionId);
1189
+ if (pre.status === "paused") {
1190
+ await updateSessionStatus(cwd, sessionId, "active");
1191
+ }
1019
1192
  await handleOrchestratorYield(sessionId, cwd, nextPrompt);
1193
+ const session = getSession(cwd, sessionId);
1194
+ const hasRunningAgents = session.agents.some((a) => a.status === "running");
1195
+ if (!hasRunningAgents) {
1196
+ const windowId = getWindowId(sessionId);
1197
+ if (windowId) {
1198
+ onAllAgentsDone2(sessionId, cwd, windowId);
1199
+ }
1200
+ }
1020
1201
  }
1021
1202
  async function handleComplete(sessionId, cwd, report) {
1022
1203
  await handleOrchestratorComplete(sessionId, cwd, report);
@@ -1049,12 +1230,40 @@ async function handleKill(sessionId, cwd) {
1049
1230
  }
1050
1231
  await updateSessionStatus(cwd, sessionId, "completed");
1051
1232
  untrackSession(sessionId);
1233
+ unregisterSessionPanes(sessionId);
1052
1234
  if (windowId) {
1053
1235
  killWindow(windowId);
1054
1236
  }
1055
1237
  clearAgentCounter(sessionId);
1056
1238
  return killedAgents;
1057
1239
  }
1240
+ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1241
+ const session = getSession(cwd, sessionId);
1242
+ if (session.status !== "active") return;
1243
+ if (role === "agent" && agentId) {
1244
+ const agent = session.agents.find((a) => a.id === agentId);
1245
+ if (!agent || agent.status !== "running") return;
1246
+ const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
1247
+ if (allDone) {
1248
+ const windowId = getWindowId(sessionId);
1249
+ if (windowId) {
1250
+ onAllAgentsDone2(sessionId, cwd, windowId);
1251
+ }
1252
+ }
1253
+ } else if (role === "orchestrator") {
1254
+ const hasRunningAgents = session.agents.some((a) => a.status === "running");
1255
+ if (!hasRunningAgents && session.agents.length > 0) {
1256
+ const windowId = getWindowId(sessionId);
1257
+ if (windowId) {
1258
+ console.log(`[sisyphus] Orchestrator pane exited for session ${sessionId}, all agents done \u2014 triggering respawn`);
1259
+ onAllAgentsDone2(sessionId, cwd, windowId);
1260
+ }
1261
+ } else if (!hasRunningAgents) {
1262
+ await updateSessionStatus(cwd, sessionId, "paused");
1263
+ console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane exited with no agents`);
1264
+ }
1265
+ }
1266
+ }
1058
1267
 
1059
1268
  // src/daemon/server.ts
1060
1269
  var server = null;
@@ -1077,7 +1286,7 @@ function loadSessionRegistry() {
1077
1286
  const p = registryPath();
1078
1287
  if (!existsSync5(p)) return {};
1079
1288
  try {
1080
- return JSON.parse(readFileSync7(p, "utf-8"));
1289
+ return JSON.parse(readFileSync8(p, "utf-8"));
1081
1290
  } catch {
1082
1291
  return {};
1083
1292
  }
@@ -1199,6 +1408,18 @@ async function handleRequest(req) {
1199
1408
  persistSessionRegistry();
1200
1409
  return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
1201
1410
  }
1411
+ case "pane-exited": {
1412
+ const entry = lookupPane(req.paneId);
1413
+ if (!entry) return { ok: true };
1414
+ const cwd = sessionCwdMap.get(entry.sessionId);
1415
+ if (!cwd) {
1416
+ unregisterPane(req.paneId);
1417
+ return { ok: true };
1418
+ }
1419
+ unregisterPane(req.paneId);
1420
+ await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
1421
+ return { ok: true };
1422
+ }
1202
1423
  default:
1203
1424
  return { ok: false, error: `Unknown request type: ${req.type}` };
1204
1425
  }
@@ -1208,7 +1429,7 @@ async function handleRequest(req) {
1208
1429
  }
1209
1430
  }
1210
1431
  function startServer() {
1211
- return new Promise((resolve3, reject) => {
1432
+ return new Promise((resolve4, reject) => {
1212
1433
  const sock = socketPath();
1213
1434
  if (existsSync5(sock)) {
1214
1435
  unlinkSync(sock);
@@ -1240,14 +1461,14 @@ function startServer() {
1240
1461
  server.on("error", reject);
1241
1462
  server.listen(sock, () => {
1242
1463
  console.log(`[sisyphus] Daemon listening on ${sock}`);
1243
- resolve3(server);
1464
+ resolve4(server);
1244
1465
  });
1245
1466
  });
1246
1467
  }
1247
1468
  function stopServer() {
1248
- return new Promise((resolve3) => {
1469
+ return new Promise((resolve4) => {
1249
1470
  if (!server) {
1250
- resolve3();
1471
+ resolve4();
1251
1472
  return;
1252
1473
  }
1253
1474
  server.close(() => {
@@ -1256,7 +1477,7 @@ function stopServer() {
1256
1477
  unlinkSync(sock);
1257
1478
  }
1258
1479
  server = null;
1259
- resolve3();
1480
+ resolve4();
1260
1481
  });
1261
1482
  });
1262
1483
  }
@@ -1276,7 +1497,7 @@ function isProcessAlive(pid) {
1276
1497
  function readPid() {
1277
1498
  const pidFile = daemonPidPath();
1278
1499
  try {
1279
- const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
1500
+ const pid = parseInt(readFileSync9(pidFile, "utf-8").trim(), 10);
1280
1501
  return pid && isProcessAlive(pid) ? pid : null;
1281
1502
  } catch {
1282
1503
  return null;
@@ -1343,7 +1564,7 @@ async function recoverSessions() {
1343
1564
  continue;
1344
1565
  }
1345
1566
  try {
1346
- const session = JSON.parse(readFileSync8(stateFile, "utf-8"));
1567
+ const session = JSON.parse(readFileSync9(stateFile, "utf-8"));
1347
1568
  if (session.status === "active" || session.status === "paused") {
1348
1569
  registerSessionCwd(sessionId, cwd);
1349
1570
  resetAgentCounterFromState(sessionId, session.agents ?? []);
@@ -1357,6 +1578,18 @@ async function recoverSessions() {
1357
1578
  const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
1358
1579
  if (lastIncompleteCycle?.paneId) {
1359
1580
  setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
1581
+ const livePaneIds = new Set(livePanes.map((p) => p.paneId));
1582
+ if (livePaneIds.has(lastIncompleteCycle.paneId)) {
1583
+ registerPane(lastIncompleteCycle.paneId, sessionId, "orchestrator");
1584
+ }
1585
+ }
1586
+ for (const agent of session.agents) {
1587
+ if (agent.status === "running" && agent.paneId) {
1588
+ const livePaneIds = new Set(livePanes.map((p) => p.paneId));
1589
+ if (livePaneIds.has(agent.paneId)) {
1590
+ registerPane(agent.paneId, sessionId, "agent", agent.id);
1591
+ }
1592
+ }
1360
1593
  }
1361
1594
  console.log(`[sisyphus] Reconnected session ${sessionId} to tmux window ${session.tmuxWindowId}`);
1362
1595
  if (session.status === "active" && session.agents.length > 0) {
@@ -1406,31 +1639,44 @@ async function startDaemon() {
1406
1639
  process.on("SIGINT", shutdown);
1407
1640
  }
1408
1641
  var command = process.argv[2];
1409
- switch (command) {
1410
- case "stop":
1411
- stopDaemon();
1412
- break;
1413
- case "restart": {
1414
- stopDaemon();
1415
- const wait = Date.now() + 500;
1416
- while (Date.now() < wait) {
1642
+ (async () => {
1643
+ switch (command) {
1644
+ case "stop":
1645
+ stopDaemon();
1646
+ break;
1647
+ case "restart": {
1648
+ stopDaemon();
1649
+ await sleep(500);
1650
+ const respawnedPid = readPid();
1651
+ if (respawnedPid) {
1652
+ console.log(`[sisyphus] Daemon restarted (pid ${respawnedPid}) by process manager`);
1653
+ break;
1654
+ }
1655
+ await startDaemon();
1656
+ break;
1417
1657
  }
1418
- startDaemon().catch((err) => {
1419
- console.error("[sisyphus] Fatal error:", err);
1658
+ case "start":
1659
+ case void 0:
1660
+ await startDaemon();
1661
+ break;
1662
+ case "help":
1663
+ case "--help":
1664
+ case "-h":
1665
+ console.log("Usage: sisyphusd [command]");
1666
+ console.log("");
1667
+ console.log("Commands:");
1668
+ console.log(" start Start the daemon (default if no command given)");
1669
+ console.log(" stop Stop the running daemon");
1670
+ console.log(" restart Stop and restart the daemon");
1671
+ console.log(" help Show this help message");
1672
+ break;
1673
+ default:
1674
+ console.error(`[sisyphus] Unknown command: ${command}`);
1675
+ console.error("Usage: sisyphusd [start|stop|restart|help]");
1420
1676
  process.exit(1);
1421
- });
1422
- break;
1423
1677
  }
1424
- case "start":
1425
- case void 0:
1426
- startDaemon().catch((err) => {
1427
- console.error("[sisyphus] Fatal error:", err);
1428
- process.exit(1);
1429
- });
1430
- break;
1431
- default:
1432
- console.error(`[sisyphus] Unknown command: ${command}`);
1433
- console.error("Usage: sisyphusd [start|stop|restart]");
1434
- process.exit(1);
1435
- }
1678
+ })().catch((err) => {
1679
+ console.error("[sisyphus] Fatal error:", err);
1680
+ process.exit(1);
1681
+ });
1436
1682
  //# sourceMappingURL=daemon.js.map