sisyphi 0.1.3 → 0.1.5

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
@@ -48,18 +48,15 @@ import { join as join4 } from "path";
48
48
 
49
49
  // src/daemon/session-manager.ts
50
50
  import { v4 as uuidv4 } from "uuid";
51
- import { existsSync as existsSync4, readdirSync as readdirSync4 } from "fs";
51
+ import { existsSync as existsSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
52
52
 
53
53
  // src/daemon/state.ts
54
- import { readFileSync as readFileSync2, writeFileSync, mkdirSync, renameSync } from "fs";
55
- import { dirname, join } from "path";
56
54
  import { randomUUID } from "crypto";
55
+ import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
56
+ import { dirname, join } from "path";
57
57
  var PLAN_SEED = `---
58
58
  description: >
59
- Living document of what still needs to happen. Write your remaining work plan
60
- here: phases, next steps, file references, open questions. Remove or collapse
61
- items as they're completed so this file only reflects outstanding work. The
62
- orchestrator sees this every cycle \u2014 keep it focused and current.
59
+ Living document of what still needs to happen. Write out ne
63
60
  ---
64
61
  `;
65
62
  var LOGS_SEED = `---
@@ -67,7 +64,7 @@ description: >
67
64
  Session memory. Record important observations, decisions, and findings here.
68
65
  This is your persistent memory across cycles: things you tried, what
69
66
  worked/failed, design decisions and their rationale, gotchas discovered during
70
- implementation. Unlike plan.md, entries here accumulate \u2014 they're a log.
67
+ implementation.
71
68
  ---
72
69
  `;
73
70
  var sessionLocks = /* @__PURE__ */ new Map();
@@ -388,10 +385,13 @@ function formatStateForOrchestrator(session) {
388
385
  if (worktreeAgents.length > 0) {
389
386
  const wtLines = worktreeAgents.map((a) => {
390
387
  if (a.mergeStatus === "conflict") {
391
- return `- ${a.id}: conflict \u2014 ${a.mergeDetails ?? "unknown"}
388
+ return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
392
389
  Branch: ${a.branchName}
393
390
  Worktree: ${a.worktreePath}`;
394
391
  }
392
+ if (a.mergeStatus === "no-changes") {
393
+ return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
394
+ }
395
395
  const status = a.mergeStatus ?? "pending";
396
396
  return `- ${a.id}: ${status} (branch ${a.branchName})`;
397
397
  }).join("\n");
@@ -493,7 +493,7 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt) {
493
493
  const session = getSession(cwd, sessionId);
494
494
  const runningAgents = session.agents.filter((a) => a.status === "running");
495
495
  if (runningAgents.length === 0) {
496
- console.error(`[sisyphus] WARNING: Orchestrator yielded but no agents are running for session ${sessionId}`);
496
+ console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
497
497
  }
498
498
  }
499
499
  async function handleOrchestratorComplete(sessionId, cwd, report) {
@@ -539,23 +539,17 @@ function loadWorktreeConfig(cwd) {
539
539
  return null;
540
540
  }
541
541
  }
542
- function createWorktree(cwd, sessionId, agentId) {
542
+ function createWorktreeShell(cwd, sessionId, agentId) {
543
543
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
544
- const worktreePath = join3(worktreeBaseDir(cwd), agentId);
544
+ const worktreePath = join3(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
545
545
  mkdirSync2(dirname2(worktreePath), { recursive: true });
546
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
547
+ if (existsSync2(worktreePath)) {
548
+ execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
549
+ }
550
+ execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
546
551
  exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
547
552
  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
553
  return { worktreePath, branchName };
560
554
  }
561
555
  function bootstrapWorktree(cwd, worktreePath, config) {
@@ -615,6 +609,8 @@ function mergeWorktrees(cwd, agents) {
615
609
  (a) => a.worktreePath && a.mergeStatus === "pending"
616
610
  );
617
611
  const results = [];
612
+ execSafe2(`git -C ${shellQuote2(cwd)} add .sisyphus`);
613
+ execSafe2(`git -C ${shellQuote2(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
618
614
  for (const agent of pending) {
619
615
  const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
620
616
  if (!branch) {
@@ -637,8 +633,10 @@ function mergeWorktrees(cwd, agents) {
637
633
  results.push({ agentId: agent.id, name: agent.name, status: "merged" });
638
634
  } catch (err) {
639
635
  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);
636
+ const errObj = err;
637
+ const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
638
+ const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
639
+ const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
642
640
  results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
643
641
  }
644
642
  }
@@ -687,7 +685,7 @@ Task: {{INSTRUCTION}}`;
687
685
  if (worktreeContext) {
688
686
  worktreeBlock = [
689
687
  "## Worktree Context",
690
- `You are working in worktree ${worktreeContext.offset} of ${worktreeContext.total} concurrent worktrees on branch \`${worktreeContext.branchName}\`.`,
688
+ `You are working in an isolated git worktree on branch \`${worktreeContext.branchName}\`.`,
691
689
  `If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
692
690
  ].join("\n");
693
691
  }
@@ -705,7 +703,7 @@ async function spawnAgent(opts) {
705
703
  let branchName;
706
704
  let worktreeContext;
707
705
  if (opts.worktree) {
708
- const wt = createWorktree(cwd, sessionId, agentId);
706
+ const wt = createWorktreeShell(cwd, sessionId, agentId);
709
707
  worktreePath = wt.worktreePath;
710
708
  branchName = wt.branchName;
711
709
  paneCwd = worktreePath;
@@ -728,7 +726,7 @@ async function spawnAgent(opts) {
728
726
  ].join(" && ");
729
727
  const agentFlag = agentType ? ` --agent ${shellQuote3(agentType)}` : "";
730
728
  const claudeCmd = `claude --dangerously-skip-permissions --plugin-dir "${pluginPath}"${agentFlag} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
731
- sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}`);
729
+ const fullCmd = `${bannerCmd} ${envExports} && ${claudeCmd}`;
732
730
  const agent = {
733
731
  id: agentId,
734
732
  name,
@@ -743,6 +741,24 @@ async function spawnAgent(opts) {
743
741
  ...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
744
742
  };
745
743
  await addAgent(cwd, sessionId, agent);
744
+ if (opts.worktree && worktreePath) {
745
+ const config = loadWorktreeConfig(cwd);
746
+ if (config) {
747
+ const wtPath = worktreePath;
748
+ setImmediate(() => {
749
+ try {
750
+ bootstrapWorktree(cwd, wtPath, config);
751
+ } catch (err) {
752
+ console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
753
+ }
754
+ sendKeys(paneId, fullCmd);
755
+ });
756
+ } else {
757
+ sendKeys(paneId, fullCmd);
758
+ }
759
+ } else {
760
+ sendKeys(paneId, fullCmd);
761
+ }
746
762
  return agent;
747
763
  }
748
764
  function nextReportNumber(cwd, sessionId, agentId) {
@@ -914,8 +930,44 @@ async function startSession(task, cwd, tmuxSession, windowId) {
914
930
  trackSession(sessionId, cwd, tmuxSession);
915
931
  await spawnOrchestrator(sessionId, cwd, windowId);
916
932
  updateTrackedWindow(sessionId, windowId);
933
+ pruneOldSessions(cwd);
917
934
  return session;
918
935
  }
936
+ var PRUNE_KEEP_COUNT = 10;
937
+ var PRUNE_KEEP_DAYS = 7;
938
+ function pruneOldSessions(cwd) {
939
+ try {
940
+ const dir = sessionsDir(cwd);
941
+ if (!existsSync4(dir)) return;
942
+ const entries = readdirSync4(dir, { withFileTypes: true });
943
+ const candidates = [];
944
+ for (const entry of entries) {
945
+ if (!entry.isDirectory()) continue;
946
+ try {
947
+ const session = getSession(cwd, entry.name);
948
+ if (session.status === "active" || session.status === "paused") continue;
949
+ candidates.push({ id: session.id, createdAt: new Date(session.createdAt).getTime() });
950
+ } catch {
951
+ }
952
+ }
953
+ if (candidates.length <= PRUNE_KEEP_COUNT) return;
954
+ candidates.sort((a, b) => b.createdAt - a.createdAt);
955
+ const cutoff = Date.now() - PRUNE_KEEP_DAYS * 24 * 60 * 60 * 1e3;
956
+ const keep = /* @__PURE__ */ new Set();
957
+ for (let i = 0; i < Math.min(PRUNE_KEEP_COUNT, candidates.length); i++) {
958
+ keep.add(candidates[i].id);
959
+ }
960
+ for (const c of candidates) {
961
+ if (c.createdAt >= cutoff) keep.add(c.id);
962
+ }
963
+ for (const c of candidates) {
964
+ if (keep.has(c.id)) continue;
965
+ rmSync2(sessionDir(cwd, c.id), { recursive: true, force: true });
966
+ }
967
+ } catch (err) {
968
+ console.error("[sisyphus] Session pruning failed:", err);
969
+ }
970
+ }
919
971
  async function resumeSession(sessionId, cwd, tmuxSession, windowId, message) {
920
972
  const session = getSession(cwd, sessionId);
921
973
  if (session.status !== "active") {
@@ -973,14 +1025,17 @@ function listSessions(cwd) {
973
1025
  }
974
1026
  return sessions;
975
1027
  }
1028
+ var pendingRespawns = /* @__PURE__ */ new Set();
976
1029
  function onAllAgentsDone2(sessionId, cwd, windowId) {
1030
+ if (pendingRespawns.has(sessionId)) return;
977
1031
  const session = getSession(cwd, sessionId);
978
1032
  if (session.status !== "active") return;
1033
+ pendingRespawns.add(sessionId);
979
1034
  const worktreeAgents = session.agents.filter((a) => a.worktreePath && a.mergeStatus === "pending");
980
1035
  if (worktreeAgents.length > 0) {
981
1036
  const results = mergeWorktrees(cwd, worktreeAgents);
982
1037
  for (const result of results) {
983
- const mergeStatus = result.status === "conflict" ? "conflict" : "merged";
1038
+ const mergeStatus = result.status;
984
1039
  updateAgent(cwd, sessionId, result.agentId, {
985
1040
  mergeStatus,
986
1041
  mergeDetails: result.conflictDetails
@@ -988,6 +1043,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
988
1043
  }
989
1044
  }
990
1045
  setTimeout(() => {
1046
+ pendingRespawns.delete(sessionId);
991
1047
  spawnOrchestrator(sessionId, cwd, windowId).then(() => updateTrackedWindow(sessionId, windowId)).catch((err) => console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err));
992
1048
  }, 2e3);
993
1049
  }
@@ -1016,7 +1072,19 @@ async function handleReport(cwd, sessionId, agentId, content) {
1016
1072
  await handleAgentReport(cwd, sessionId, agentId, content);
1017
1073
  }
1018
1074
  async function handleYield(sessionId, cwd, nextPrompt) {
1075
+ const pre = getSession(cwd, sessionId);
1076
+ if (pre.status === "paused") {
1077
+ await updateSessionStatus(cwd, sessionId, "active");
1078
+ }
1019
1079
  await handleOrchestratorYield(sessionId, cwd, nextPrompt);
1080
+ const session = getSession(cwd, sessionId);
1081
+ const hasRunningAgents = session.agents.some((a) => a.status === "running");
1082
+ if (!hasRunningAgents) {
1083
+ const windowId = getWindowId(sessionId);
1084
+ if (windowId) {
1085
+ onAllAgentsDone2(sessionId, cwd, windowId);
1086
+ }
1087
+ }
1020
1088
  }
1021
1089
  async function handleComplete(sessionId, cwd, report) {
1022
1090
  await handleOrchestratorComplete(sessionId, cwd, report);
@@ -1415,6 +1483,11 @@ switch (command) {
1415
1483
  const wait = Date.now() + 500;
1416
1484
  while (Date.now() < wait) {
1417
1485
  }
1486
+ const respawnedPid = readPid();
1487
+ if (respawnedPid) {
1488
+ console.log(`[sisyphus] Daemon restarted (pid ${respawnedPid}) by process manager`);
1489
+ break;
1490
+ }
1418
1491
  startDaemon().catch((err) => {
1419
1492
  console.error("[sisyphus] Fatal error:", err);
1420
1493
  process.exit(1);
@@ -1428,9 +1501,20 @@ switch (command) {
1428
1501
  process.exit(1);
1429
1502
  });
1430
1503
  break;
1504
+ case "help":
1505
+ case "--help":
1506
+ case "-h":
1507
+ console.log("Usage: sisyphusd [command]");
1508
+ console.log("");
1509
+ console.log("Commands:");
1510
+ console.log(" start Start the daemon (default if no command given)");
1511
+ console.log(" stop Stop the running daemon");
1512
+ console.log(" restart Stop and restart the daemon");
1513
+ console.log(" help Show this help message");
1514
+ break;
1431
1515
  default:
1432
1516
  console.error(`[sisyphus] Unknown command: ${command}`);
1433
- console.error("Usage: sisyphusd [start|stop|restart]");
1517
+ console.error("Usage: sisyphusd [start|stop|restart|help]");
1434
1518
  process.exit(1);
1435
1519
  }
1436
1520
  //# sourceMappingURL=daemon.js.map