sisyphi 1.0.4 → 1.0.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
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ EXEC_ENV,
4
+ exec,
5
+ execEnv,
6
+ execSafe,
3
7
  loadConfig
4
- } from "./chunk-KQBSC5KY.js";
8
+ } from "./chunk-LWWRGQWM.js";
9
+ import {
10
+ shellQuote
11
+ } from "./chunk-6G226ZK7.js";
5
12
  import {
6
13
  contextDir,
7
14
  cycleLogPath,
@@ -25,21 +32,21 @@ import {
25
32
  statePath,
26
33
  worktreeBaseDir,
27
34
  worktreeConfigPath
28
- } from "./chunk-YGBGKMTF.js";
35
+ } from "./chunk-JXKUI4P6.js";
29
36
 
30
37
  // src/daemon/index.ts
31
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, existsSync as existsSync8 } from "fs";
38
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
32
39
  import { execSync as execSync4 } from "child_process";
33
40
  import { setTimeout as sleep } from "timers/promises";
34
41
 
35
42
  // src/daemon/server.ts
36
43
  import { createServer } from "net";
37
- import { unlinkSync, existsSync as existsSync7, writeFileSync as writeFileSync4, readFileSync as readFileSync6, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
44
+ import { unlinkSync, existsSync as existsSync8, writeFileSync as writeFileSync5, readFileSync as readFileSync6, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
38
45
  import { join as join5 } from "path";
39
46
 
40
47
  // src/daemon/session-manager.ts
41
48
  import { v4 as uuidv4 } from "uuid";
42
- import { existsSync as existsSync6, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
49
+ import { existsSync as existsSync7, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
43
50
 
44
51
  // src/daemon/state.ts
45
52
  import { randomUUID } from "crypto";
@@ -57,16 +64,16 @@ Agents save exploration findings, architectural notes, and reference material he
57
64
  var sessionLocks = /* @__PURE__ */ new Map();
58
65
  async function withSessionLock(sessionId, fn) {
59
66
  const prev = sessionLocks.get(sessionId) ?? Promise.resolve();
60
- let resolve4;
67
+ let resolve5;
61
68
  const next = new Promise((r) => {
62
- resolve4 = r;
69
+ resolve5 = r;
63
70
  });
64
71
  sessionLocks.set(sessionId, next);
65
72
  await prev;
66
73
  try {
67
74
  return fn();
68
75
  } finally {
69
- resolve4();
76
+ resolve5();
70
77
  }
71
78
  }
72
79
  function atomicWrite(filePath, data) {
@@ -293,8 +300,35 @@ function deleteSnapshotsAfter(cwd, sessionId, afterCycle) {
293
300
  }
294
301
 
295
302
  // src/daemon/orchestrator.ts
296
- import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
297
- import { resolve, join as join3 } from "path";
303
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
304
+ import { execSync } from "child_process";
305
+ import { resolve as resolve2, join as join3 } from "path";
306
+
307
+ // src/daemon/spawn-helpers.ts
308
+ import { writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
309
+ import { resolve } from "path";
310
+ function resolveCliBin() {
311
+ return resolve(import.meta.dirname, "cli.js");
312
+ }
313
+ function resolveNpmBinDir() {
314
+ return resolve(import.meta.dirname, "../../.bin");
315
+ }
316
+ function resolveBannerCmd() {
317
+ const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
318
+ return existsSync2(bannerPath) ? `cat '${bannerPath}'` : null;
319
+ }
320
+ function buildEnvExports(statements) {
321
+ return statements.join(" && ");
322
+ }
323
+ function buildNotifyCmd(paneId) {
324
+ const cliBin = resolveCliBin();
325
+ return `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
326
+ }
327
+ function writeRunScript(dir, name, lines) {
328
+ const scriptPath = `${dir}/${name}.sh`;
329
+ writeFileSync2(scriptPath, lines.join("\n"), { mode: 493 });
330
+ return scriptPath;
331
+ }
298
332
 
299
333
  // src/daemon/colors.ts
300
334
  var ORCHESTRATOR_COLOR = "yellow";
@@ -318,7 +352,7 @@ function resetColors(sessionId) {
318
352
  }
319
353
 
320
354
  // src/daemon/frontmatter.ts
321
- import { readFileSync as readFileSync2, existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
355
+ import { readFileSync as readFileSync2, existsSync as existsSync3, readdirSync as readdirSync2 } from "fs";
322
356
  import { homedir } from "os";
323
357
  import { join as join2, basename } from "path";
324
358
  function detectProvider(model) {
@@ -386,7 +420,7 @@ function resolveAgentTypePath(agentType, pluginDir, cwd) {
386
420
  searchPaths.push(join2(pluginDir, "agents", `${name}.md`));
387
421
  }
388
422
  for (const path of searchPaths) {
389
- if (existsSync2(path)) return path;
423
+ if (existsSync3(path)) return path;
390
424
  }
391
425
  return null;
392
426
  }
@@ -452,21 +486,6 @@ function resolveAgentConfig(agentType, pluginDir, cwd) {
452
486
  }
453
487
 
454
488
  // src/daemon/tmux.ts
455
- import { execSync } from "child_process";
456
- var EXEC_ENV = {
457
- ...process.env,
458
- PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
459
- };
460
- function exec(cmd) {
461
- return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV }).trim();
462
- }
463
- function execSafe(cmd) {
464
- try {
465
- return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV, stdio: ["pipe", "pipe", "pipe"] }).trim();
466
- } catch {
467
- return null;
468
- }
469
- }
470
489
  function createPane(windowTarget, cwd, position = "right") {
471
490
  const cwdFlag = cwd ? ` -c ${shellQuote(cwd)}` : "";
472
491
  const panes = listPanes(windowTarget);
@@ -525,9 +544,6 @@ function setPaneStyle(paneTarget, color) {
525
544
  function selectLayout(windowTarget, layout = "even-horizontal") {
526
545
  execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
527
546
  }
528
- function shellQuote(s) {
529
- return `'${s.replace(/'/g, "'\\''")}'`;
530
- }
531
547
 
532
548
  // src/daemon/pane-registry.ts
533
549
  var paneMap = /* @__PURE__ */ new Map();
@@ -573,16 +589,16 @@ function setOrchestratorPaneId(sessionId, paneId) {
573
589
  }
574
590
  function loadOrchestratorPrompt(cwd, mode) {
575
591
  const projectPath = projectOrchestratorPromptPath(cwd);
576
- if (existsSync3(projectPath)) {
592
+ if (existsSync4(projectPath)) {
577
593
  return readFileSync3(projectPath, "utf-8");
578
594
  }
579
- const basePath = resolve(import.meta.dirname, "../templates/orchestrator-base.md");
595
+ const basePath = resolve2(import.meta.dirname, "../templates/orchestrator-base.md");
580
596
  const base = readFileSync3(basePath, "utf-8");
581
597
  if (mode === "implementation") {
582
- const implPath = resolve(import.meta.dirname, "../templates/orchestrator-impl.md");
598
+ const implPath = resolve2(import.meta.dirname, "../templates/orchestrator-impl.md");
583
599
  return base + "\n\n" + readFileSync3(implPath, "utf-8");
584
600
  }
585
- const planningPath = resolve(import.meta.dirname, "../templates/orchestrator-planning.md");
601
+ const planningPath = resolve2(import.meta.dirname, "../templates/orchestrator-planning.md");
586
602
  return base + "\n\n" + readFileSync3(planningPath, "utf-8");
587
603
  }
588
604
  function formatStateForOrchestrator(session) {
@@ -601,7 +617,7 @@ ${session.context}
601
617
  }
602
618
  } else {
603
619
  let ctxFiles = [];
604
- if (existsSync3(ctxDir)) {
620
+ if (existsSync4(ctxDir)) {
605
621
  ctxFiles = readdirSync3(ctxDir).filter((f) => f !== "CLAUDE.md");
606
622
  }
607
623
  if (ctxFiles.length > 0) {
@@ -667,10 +683,10 @@ ${agentBlocks}
667
683
  </last-cycle>
668
684
  `;
669
685
  }
670
- const roadmapRef = existsSync3(roadmapFile) ? `@${roadmapFile}` : "(empty)";
686
+ const roadmapRef = existsSync4(roadmapFile) ? `@${roadmapFile}` : "(empty)";
671
687
  const worktreeAgents = session.agents.filter((a) => a.worktreePath);
672
688
  let worktreeSection = "";
673
- if (worktreeAgents.length > 0 || existsSync3(worktreeConfigPath(session.cwd))) {
689
+ if (worktreeAgents.length > 0 || existsSync4(worktreeConfigPath(session.cwd))) {
674
690
  let wtLines = "";
675
691
  if (worktreeAgents.length > 0) {
676
692
  wtLines = "\n" + worktreeAgents.map((a) => {
@@ -686,7 +702,7 @@ ${agentBlocks}
686
702
  return `- ${a.id}: ${status} (branch ${a.branchName})`;
687
703
  }).join("\n");
688
704
  }
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.";
705
+ const worktreeHint = existsSync4(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.";
690
706
  worktreeSection = `
691
707
 
692
708
  ## Git Worktrees
@@ -694,7 +710,7 @@ ${agentBlocks}
694
710
  ${worktreeHint}${wtLines}`;
695
711
  }
696
712
  const goalFile = goalPath(session.cwd, session.id);
697
- const goalContent = existsSync3(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
713
+ const goalContent = existsSync4(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
698
714
  return `## Goal
699
715
 
700
716
  ${goalContent}
@@ -709,12 +725,17 @@ ${roadmapRef}
709
725
  ${worktreeSection}`;
710
726
  }
711
727
  async function spawnOrchestrator(sessionId, cwd, windowId, message) {
728
+ try {
729
+ execSync("which claude", { stdio: "pipe", env: EXEC_ENV });
730
+ } catch {
731
+ throw new Error("Claude CLI not found on PATH. Run `sisyphus doctor` to diagnose.");
732
+ }
712
733
  const session = getSession(cwd, sessionId);
713
734
  const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
714
735
  const mode = lastCycle?.mode ?? "planning";
715
736
  const basePrompt = loadOrchestratorPrompt(cwd, mode);
716
737
  const formattedState = formatStateForOrchestrator(session);
717
- const agentPluginPath = resolve(import.meta.dirname, "../templates/agent-plugin");
738
+ const agentPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
718
739
  const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
719
740
  agentTypes.push(
720
741
  { 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." }
@@ -727,16 +748,15 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
727
748
  const systemPrompt = basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines);
728
749
  const cycleNum = session.orchestratorCycles.length + 1;
729
750
  const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
730
- writeFileSync2(promptFilePath, systemPrompt, "utf-8");
751
+ writeFileSync3(promptFilePath, systemPrompt, "utf-8");
731
752
  sessionWindowMap.set(sessionId, windowId);
732
- const cliBin = resolve(import.meta.dirname, "cli.js");
733
- const npmBinDir = resolve(import.meta.dirname, "../../.bin");
734
- const envExports = [
753
+ const npmBinDir = resolveNpmBinDir();
754
+ const envExports = buildEnvExports([
735
755
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
736
756
  `export SISYPHUS_AGENT_ID='orchestrator'`,
737
757
  `export SISYPHUS_CWD='${cwd}'`,
738
758
  `export PATH="${npmBinDir}:$PATH"`
739
- ].join(" && ");
759
+ ]);
740
760
  let userPrompt = formattedState;
741
761
  if (message) {
742
762
  userPrompt += `
@@ -754,12 +774,12 @@ The user resumed this session with new instructions: ${message}`;
754
774
  ${continuationText}`;
755
775
  }
756
776
  const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
757
- writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
777
+ writeFileSync3(userPromptFilePath, userPrompt, "utf-8");
758
778
  if (session.messages && session.messages.length > 0) {
759
779
  await drainMessages(cwd, sessionId, session.messages.length);
760
780
  }
761
- const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
762
- const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
781
+ const pluginPath = resolve2(import.meta.dirname, "../templates/orchestrator-plugin");
782
+ const settingsPath = resolve2(import.meta.dirname, "../templates/orchestrator-settings.json");
763
783
  const config = loadConfig(cwd);
764
784
  const effort = config.orchestratorEffort ?? "high";
765
785
  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}')"`;
@@ -768,10 +788,16 @@ ${continuationText}`;
768
788
  registerPane(paneId, sessionId, "orchestrator");
769
789
  setPaneTitle(paneId, `Sisyphus`);
770
790
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
771
- const bannerPath = resolve(import.meta.dirname, "../templates/banner.txt");
772
- const bannerCmd = existsSync3(bannerPath) ? `cat '${bannerPath}' &&` : "";
773
- const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
774
- sendKeys(paneId, `${bannerCmd} ${envExports} && ${claudeCmd}; ${notifyCmd}`);
791
+ const bannerCmd = resolveBannerCmd();
792
+ const notifyCmd = buildNotifyCmd(paneId);
793
+ const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `orchestrator-run-${cycleNum}`, [
794
+ "#!/usr/bin/env bash",
795
+ ...bannerCmd ? [bannerCmd] : [],
796
+ envExports,
797
+ claudeCmd,
798
+ notifyCmd
799
+ ]);
800
+ sendKeys(paneId, `bash '${scriptPath}'`);
775
801
  await addOrchestratorCycle(cwd, sessionId, {
776
802
  cycle: cycleNum,
777
803
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -814,30 +840,13 @@ function cleanupSessionMaps(sessionId) {
814
840
  }
815
841
 
816
842
  // src/daemon/agent.ts
817
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5, existsSync as existsSync5 } from "fs";
818
- import { resolve as resolve2 } from "path";
843
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5 } from "fs";
844
+ import { execSync as execSync2 } from "child_process";
845
+ import { resolve as resolve3 } from "path";
819
846
 
820
847
  // src/daemon/worktree.ts
821
- import { execSync as execSync2 } from "child_process";
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";
824
- var EXEC_ENV2 = {
825
- ...process.env,
826
- PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
827
- };
828
- function exec2(cmd, cwd) {
829
- return execSync2(cmd, { encoding: "utf-8", env: EXEC_ENV2, cwd }).trim();
830
- }
831
- function execSafe2(cmd, cwd) {
832
- try {
833
- return exec2(cmd, cwd);
834
- } catch {
835
- return null;
836
- }
837
- }
838
- function shellQuote2(s) {
839
- return `'${s.replace(/'/g, "'\\''")}'`;
840
- }
848
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
849
+ import { dirname as dirname2, join as join4 } from "path";
841
850
  function loadWorktreeConfig(cwd) {
842
851
  try {
843
852
  const content = readFileSync4(worktreeConfigPath(cwd), "utf-8");
@@ -849,52 +858,52 @@ function loadWorktreeConfig(cwd) {
849
858
  function createWorktreeShell(cwd, sessionId, agentId) {
850
859
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
851
860
  const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
852
- mkdirSync2(dirname3(worktreePath), { recursive: true });
853
- execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
854
- if (existsSync4(worktreePath)) {
855
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
856
- }
857
- execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
858
- exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
859
- exec2(`git -C ${shellQuote2(cwd)} worktree add ${shellQuote2(worktreePath)} ${shellQuote2(branchName)}`);
861
+ mkdirSync2(dirname2(worktreePath), { recursive: true });
862
+ execSafe(`git -C ${shellQuote(cwd)} worktree prune`);
863
+ if (existsSync5(worktreePath)) {
864
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove --force ${shellQuote(worktreePath)}`);
865
+ }
866
+ execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
867
+ exec(`git -C ${shellQuote(cwd)} branch ${shellQuote(branchName)} HEAD`);
868
+ exec(`git -C ${shellQuote(cwd)} worktree add ${shellQuote(worktreePath)} ${shellQuote(branchName)}`);
860
869
  return { worktreePath, branchName };
861
870
  }
862
871
  function bootstrapWorktree(cwd, worktreePath, config) {
863
872
  if (config.copy) {
864
873
  for (const entry of config.copy) {
865
874
  const dest = join4(worktreePath, entry);
866
- mkdirSync2(dirname3(dest), { recursive: true });
867
- execSafe2(`cp -r ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
875
+ mkdirSync2(dirname2(dest), { recursive: true });
876
+ execSafe(`cp -r ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
868
877
  }
869
878
  }
870
879
  if (config.clone) {
871
880
  for (const entry of config.clone) {
872
881
  const dest = join4(worktreePath, entry);
873
- mkdirSync2(dirname3(dest), { recursive: true });
874
- const src = shellQuote2(join4(cwd, entry));
875
- const dstQ = shellQuote2(dest);
876
- if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
877
- execSafe2(`cp -r ${src} ${dstQ}`);
882
+ mkdirSync2(dirname2(dest), { recursive: true });
883
+ const src = shellQuote(join4(cwd, entry));
884
+ const dstQ = shellQuote(dest);
885
+ if (execSafe(`cp -Rc ${src} ${dstQ}`) === null) {
886
+ execSafe(`cp -r ${src} ${dstQ}`);
878
887
  }
879
888
  }
880
889
  }
881
890
  if (config.symlink) {
882
891
  for (const entry of config.symlink) {
883
892
  const dest = join4(worktreePath, entry);
884
- mkdirSync2(dirname3(dest), { recursive: true });
885
- execSafe2(`ln -s ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
893
+ mkdirSync2(dirname2(dest), { recursive: true });
894
+ execSafe(`ln -s ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
886
895
  }
887
896
  }
888
897
  if (config.init) {
889
898
  try {
890
- exec2(config.init, worktreePath);
899
+ exec(config.init, worktreePath);
891
900
  } catch (err) {
892
901
  console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
893
902
  }
894
903
  }
895
904
  }
896
905
  function resolveWorktreeBranch(cwd, worktreePath) {
897
- const output = execSafe2(`git -C ${shellQuote2(cwd)} worktree list --porcelain`);
906
+ const output = execSafe(`git -C ${shellQuote(cwd)} worktree list --porcelain`);
898
907
  if (!output) return null;
899
908
  const lines = output.split("\n");
900
909
  for (let i = 0; i < lines.length; i++) {
@@ -916,30 +925,30 @@ function mergeWorktrees(cwd, agents) {
916
925
  (a) => a.worktreePath && a.mergeStatus === "pending"
917
926
  );
918
927
  const results = [];
919
- execSafe2(`git -C ${shellQuote2(cwd)} add .sisyphus`);
920
- execSafe2(`git -C ${shellQuote2(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
928
+ execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
929
+ execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
921
930
  for (const agent of pending) {
922
931
  const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
923
932
  if (!branch) {
924
933
  results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
925
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)} --force`);
934
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
926
935
  continue;
927
936
  }
928
- const aheadLog = execSafe2(`git -C ${shellQuote2(cwd)} log HEAD..${shellQuote2(branch)} --oneline`);
937
+ const aheadLog = execSafe(`git -C ${shellQuote(cwd)} log HEAD..${shellQuote(branch)} --oneline`);
929
938
  if (!aheadLog) {
930
939
  results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
931
940
  cleanupWorktree(cwd, agent.worktreePath, branch);
932
941
  continue;
933
942
  }
934
943
  const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
935
- const mergeCmd = `git -C ${shellQuote2(cwd)} merge --no-ff ${shellQuote2(branch)} -m ${shellQuote2(mergeMsg)}`;
944
+ const mergeCmd = `git -C ${shellQuote(cwd)} merge --no-ff ${shellQuote(branch)} -m ${shellQuote(mergeMsg)}`;
936
945
  try {
937
- exec2(mergeCmd);
938
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)}`);
939
- execSafe2(`git -C ${shellQuote2(cwd)} branch -d ${shellQuote2(branch)}`);
946
+ exec(mergeCmd);
947
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)}`);
948
+ execSafe(`git -C ${shellQuote(cwd)} branch -d ${shellQuote(branch)}`);
940
949
  results.push({ agentId: agent.id, name: agent.name, status: "merged" });
941
950
  } catch (err) {
942
- execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
951
+ execSafe(`git -C ${shellQuote(cwd)} merge --abort`);
943
952
  const errObj = err;
944
953
  const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
945
954
  const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
@@ -950,9 +959,9 @@ function mergeWorktrees(cwd, agents) {
950
959
  return results;
951
960
  }
952
961
  function cleanupWorktree(cwd, worktreePath, branchName) {
953
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(worktreePath)} --force`);
954
- execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
955
- const baseDir = dirname3(worktreePath);
962
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(worktreePath)} --force`);
963
+ execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
964
+ const baseDir = dirname2(worktreePath);
956
965
  try {
957
966
  const entries = readdirSync4(baseDir);
958
967
  if (entries.length === 0) {
@@ -1014,7 +1023,7 @@ function clearAgentCounter(sessionId) {
1014
1023
  agentCounters.delete(sessionId);
1015
1024
  }
1016
1025
  function renderAgentSuffix(sessionId, instruction, worktreeContext) {
1017
- const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
1026
+ const templatePath = resolve3(import.meta.dirname, "../templates/agent-suffix.md");
1018
1027
  let template;
1019
1028
  try {
1020
1029
  template = readFileSync5(templatePath, "utf-8");
@@ -1038,7 +1047,7 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1038
1047
  mkdirSync3(`${base}/.claude-plugin`, { recursive: true });
1039
1048
  mkdirSync3(`${base}/agents`, { recursive: true });
1040
1049
  mkdirSync3(`${base}/hooks`, { recursive: true });
1041
- writeFileSync3(
1050
+ writeFileSync4(
1042
1051
  `${base}/.claude-plugin/plugin.json`,
1043
1052
  JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
1044
1053
  "utf-8"
@@ -1047,34 +1056,14 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1047
1056
  const shortName = agentType.replace(/^sisyphus:/, "");
1048
1057
  copyFileSync2(agentConfig.filePath, `${base}/agents/${shortName}.md`);
1049
1058
  }
1050
- const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
1059
+ const srcHooks = resolve3(import.meta.dirname, "../templates/agent-plugin/hooks");
1051
1060
  for (const f of ["hooks.json", "require-submit.sh", "intercept-send-message.sh"]) {
1052
1061
  copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
1053
1062
  }
1054
1063
  return base;
1055
1064
  }
1056
- async function spawnAgent(opts) {
1057
- const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
1058
- const count = (agentCounters.get(sessionId) ?? 0) + 1;
1059
- agentCounters.set(sessionId, count);
1060
- const agentId = `agent-${String(count).padStart(3, "0")}`;
1061
- const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1062
- const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1063
- const provider = detectProvider(agentConfig?.frontmatter.model);
1064
- const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
1065
- let paneCwd = cwd;
1066
- let worktreePath;
1067
- let branchName;
1068
- let worktreeContext;
1069
- if (opts.worktree) {
1070
- const wt = createWorktreeShell(cwd, sessionId, agentId);
1071
- worktreePath = wt.worktreePath;
1072
- branchName = wt.branchName;
1073
- paneCwd = worktreePath;
1074
- const session = getSession(cwd, sessionId);
1075
- const portOffset = countWorktreeAgents(session.agents) + 1;
1076
- worktreeContext = { offset: portOffset, total: portOffset, branchName };
1077
- }
1065
+ function setupAgentPane(opts) {
1066
+ const { sessionId, cwd, agentId, agentType, name, instruction, windowId, color, provider, agentConfig, worktreeContext, paneCwd } = opts;
1078
1067
  const paneId = createPane(windowId, paneCwd);
1079
1068
  registerPane(paneId, sessionId, "agent", agentId);
1080
1069
  const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
@@ -1083,41 +1072,88 @@ async function spawnAgent(opts) {
1083
1072
  setPaneStyle(paneId, color);
1084
1073
  const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
1085
1074
  const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
1086
- writeFileSync3(suffixFilePath, suffix, "utf-8");
1087
- const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
1088
- const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
1089
- const cliBin = resolve2(import.meta.dirname, "cli.js");
1090
- const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
1091
- const envExports = [
1075
+ writeFileSync4(suffixFilePath, suffix, "utf-8");
1076
+ const bannerCmd = resolveBannerCmd();
1077
+ const npmBinDir = resolveNpmBinDir();
1078
+ const envExports = buildEnvExports([
1092
1079
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
1093
1080
  `export SISYPHUS_AGENT_ID='${agentId}'`,
1094
1081
  `export SISYPHUS_CWD='${cwd}'`,
1095
1082
  ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1096
1083
  `export PATH="${npmBinDir}:$PATH"`
1097
- ].join(" && ");
1098
- const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
1084
+ ]);
1085
+ const notifyCmd = buildNotifyCmd(paneId);
1099
1086
  let mainCmd;
1100
1087
  if (provider === "openai") {
1101
1088
  const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
1102
1089
  const parts = [];
1103
- if (agentConfig?.body) {
1104
- parts.push(agentConfig.body);
1105
- }
1090
+ if (agentConfig?.body) parts.push(agentConfig.body);
1106
1091
  parts.push(suffix);
1107
1092
  parts.push(`## Task
1108
1093
 
1109
1094
  ${instruction}`);
1110
- writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
1095
+ writeFileSync4(codexPromptPath, parts.join("\n\n"), "utf-8");
1111
1096
  const model = agentConfig?.frontmatter.model ?? "codex-mini";
1112
- mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
1097
+ mainCmd = `codex -m ${shellQuote(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
1113
1098
  } else {
1114
- const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
1099
+ const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote(agentType)}` : "";
1115
1100
  const config = loadConfig(cwd);
1116
1101
  const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
1117
1102
  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)}`;
1103
+ mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote(instruction)}`;
1104
+ }
1105
+ const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
1106
+ "#!/usr/bin/env bash",
1107
+ ...bannerCmd ? [bannerCmd] : [],
1108
+ envExports,
1109
+ mainCmd,
1110
+ notifyCmd
1111
+ ]);
1112
+ const fullCmd = `bash '${scriptPath}'`;
1113
+ return { paneId, fullCmd };
1114
+ }
1115
+ async function spawnAgent(opts) {
1116
+ const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
1117
+ const count = (agentCounters.get(sessionId) ?? 0) + 1;
1118
+ agentCounters.set(sessionId, count);
1119
+ const agentId = `agent-${String(count).padStart(3, "0")}`;
1120
+ const bundledPluginPath = resolve3(import.meta.dirname, "../templates/agent-plugin");
1121
+ const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1122
+ const provider = detectProvider(agentConfig?.frontmatter.model);
1123
+ const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
1124
+ const cliToCheck = provider === "openai" ? "codex" : "claude";
1125
+ try {
1126
+ execSync2(`which ${cliToCheck}`, { stdio: "pipe", env: execEnv() });
1127
+ } catch {
1128
+ throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sisyphus doctor\` to diagnose.`);
1129
+ }
1130
+ let paneCwd = cwd;
1131
+ let worktreePath;
1132
+ let branchName;
1133
+ let worktreeContext;
1134
+ if (opts.worktree) {
1135
+ const wt = createWorktreeShell(cwd, sessionId, agentId);
1136
+ worktreePath = wt.worktreePath;
1137
+ branchName = wt.branchName;
1138
+ paneCwd = worktreePath;
1139
+ const session = getSession(cwd, sessionId);
1140
+ const portOffset = countWorktreeAgents(session.agents) + 1;
1141
+ worktreeContext = { offset: portOffset, total: portOffset, branchName };
1119
1142
  }
1120
- const fullCmd = `${bannerCmd} ${envExports} && ${mainCmd}; ${notifyCmd}`;
1143
+ const { paneId, fullCmd } = setupAgentPane({
1144
+ sessionId,
1145
+ cwd,
1146
+ agentId,
1147
+ agentType,
1148
+ name,
1149
+ instruction,
1150
+ windowId,
1151
+ color,
1152
+ provider,
1153
+ agentConfig,
1154
+ worktreeContext,
1155
+ paneCwd
1156
+ });
1121
1157
  const agent = {
1122
1158
  id: agentId,
1123
1159
  name,
@@ -1169,7 +1205,7 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1169
1205
  });
1170
1206
  }
1171
1207
  const { instruction, agentType, name, color } = agent;
1172
- const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1208
+ const bundledPluginPath = resolve3(import.meta.dirname, "../templates/agent-plugin");
1173
1209
  const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1174
1210
  const provider = detectProvider(agentConfig?.frontmatter.model);
1175
1211
  let paneCwd = cwd;
@@ -1190,47 +1226,20 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1190
1226
  }
1191
1227
  unregisterAgentPane(sessionId, agentId);
1192
1228
  }
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}`;
1229
+ const { paneId, fullCmd } = setupAgentPane({
1230
+ sessionId,
1231
+ cwd,
1232
+ agentId,
1233
+ agentType,
1234
+ name,
1235
+ instruction,
1236
+ windowId,
1237
+ color,
1238
+ provider,
1239
+ agentConfig,
1240
+ worktreeContext,
1241
+ paneCwd
1242
+ });
1234
1243
  await updateAgent(cwd, sessionId, agentId, {
1235
1244
  status: "running",
1236
1245
  paneId,
@@ -1255,7 +1264,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
1255
1264
  mkdirSync3(dir, { recursive: true });
1256
1265
  const num = nextReportNumber(cwd, sessionId, agentId);
1257
1266
  const filePath = reportFilePath(cwd, sessionId, agentId, num);
1258
- writeFileSync3(filePath, content, "utf-8");
1267
+ writeFileSync4(filePath, content, "utf-8");
1259
1268
  const entry = {
1260
1269
  type: "update",
1261
1270
  filePath,
@@ -1274,7 +1283,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
1274
1283
  const dir = reportsDir(cwd, sessionId);
1275
1284
  mkdirSync3(dir, { recursive: true });
1276
1285
  const filePath = reportFilePath(cwd, sessionId, agentId, "final");
1277
- writeFileSync3(filePath, report, "utf-8");
1286
+ writeFileSync4(filePath, report, "utf-8");
1278
1287
  const entry = {
1279
1288
  type: "final",
1280
1289
  filePath,
@@ -1317,9 +1326,6 @@ function allAgentsDone(session) {
1317
1326
  const running = session.agents.filter((a) => a.status === "running");
1318
1327
  return running.length === 0 && session.agents.length > 0;
1319
1328
  }
1320
- function shellQuote3(s) {
1321
- return `'${s.replace(/'/g, "'\\''")}'`;
1322
- }
1323
1329
 
1324
1330
  // src/daemon/pane-monitor.ts
1325
1331
  var monitorInterval = null;
@@ -1464,7 +1470,7 @@ var PRUNE_KEEP_DAYS = 7;
1464
1470
  function pruneOldSessions(cwd) {
1465
1471
  try {
1466
1472
  const dir = sessionsDir(cwd);
1467
- if (!existsSync6(dir)) return;
1473
+ if (!existsSync7(dir)) return;
1468
1474
  const entries = readdirSync6(dir, { withFileTypes: true });
1469
1475
  const candidates = [];
1470
1476
  for (const entry of entries) {
@@ -1546,7 +1552,7 @@ function getSessionStatus(cwd, sessionId) {
1546
1552
  }
1547
1553
  function listSessions(cwd) {
1548
1554
  const dir = sessionsDir(cwd);
1549
- if (!existsSync6(dir)) return [];
1555
+ if (!existsSync7(dir)) return [];
1550
1556
  const entries = readdirSync6(dir, { withFileTypes: true });
1551
1557
  const sessions = [];
1552
1558
  for (const entry of entries) {
@@ -1826,10 +1832,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1826
1832
 
1827
1833
  // src/daemon/server.ts
1828
1834
  var server = null;
1829
- var sessionCwdMap = /* @__PURE__ */ new Map();
1830
- var sessionMessageCounters = /* @__PURE__ */ new Map();
1831
- var sessionTmuxMap = /* @__PURE__ */ new Map();
1832
- var sessionWindowMap2 = /* @__PURE__ */ new Map();
1835
+ var sessionTrackingMap = /* @__PURE__ */ new Map();
1833
1836
  function registryPath() {
1834
1837
  return join5(globalDir(), "session-registry.json");
1835
1838
  }
@@ -1837,14 +1840,14 @@ function persistSessionRegistry() {
1837
1840
  const dir = globalDir();
1838
1841
  mkdirSync4(dir, { recursive: true });
1839
1842
  const registry = {};
1840
- for (const [id, cwd] of sessionCwdMap) {
1841
- registry[id] = cwd;
1843
+ for (const [id, tracking] of sessionTrackingMap) {
1844
+ registry[id] = tracking.cwd;
1842
1845
  }
1843
- writeFileSync4(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
1846
+ writeFileSync5(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
1844
1847
  }
1845
1848
  function loadSessionRegistry() {
1846
1849
  const p = registryPath();
1847
- if (!existsSync7(p)) return {};
1850
+ if (!existsSync8(p)) return {};
1848
1851
  try {
1849
1852
  return JSON.parse(readFileSync6(p, "utf-8"));
1850
1853
  } catch {
@@ -1852,65 +1855,81 @@ function loadSessionRegistry() {
1852
1855
  }
1853
1856
  }
1854
1857
  function registerSessionCwd(sessionId, cwd) {
1855
- sessionCwdMap.set(sessionId, cwd);
1858
+ const existing = sessionTrackingMap.get(sessionId);
1859
+ if (existing) {
1860
+ existing.cwd = cwd;
1861
+ } else {
1862
+ sessionTrackingMap.set(sessionId, { cwd, messageCounter: 0 });
1863
+ }
1856
1864
  persistSessionRegistry();
1857
1865
  }
1858
1866
  function registerSessionTmux(sessionId, tmuxSession, windowId) {
1859
- sessionTmuxMap.set(sessionId, tmuxSession);
1860
- sessionWindowMap2.set(sessionId, windowId);
1867
+ const existing = sessionTrackingMap.get(sessionId);
1868
+ if (existing) {
1869
+ existing.tmuxSession = tmuxSession;
1870
+ existing.windowId = windowId;
1871
+ } else {
1872
+ sessionTrackingMap.set(sessionId, { cwd: "", tmuxSession, windowId, messageCounter: 0 });
1873
+ }
1874
+ }
1875
+ function unknownSessionError(sessionId) {
1876
+ return { ok: false, error: `Unknown session: ${sessionId}. Run \`sisyphus list --all\` to see available sessions.` };
1861
1877
  }
1862
1878
  async function handleRequest(req) {
1863
1879
  try {
1864
1880
  switch (req.type) {
1865
1881
  case "start": {
1866
1882
  const session = await startSession(req.task, req.cwd, req.context, req.name);
1867
- registerSessionCwd(session.id, req.cwd);
1868
- if (session.tmuxSessionName) sessionTmuxMap.set(session.id, session.tmuxSessionName);
1869
- if (session.tmuxWindowId) sessionWindowMap2.set(session.id, session.tmuxWindowId);
1883
+ sessionTrackingMap.set(session.id, {
1884
+ cwd: req.cwd,
1885
+ tmuxSession: session.tmuxSessionName,
1886
+ windowId: session.tmuxWindowId,
1887
+ messageCounter: 0
1888
+ });
1889
+ persistSessionRegistry();
1870
1890
  return { ok: true, data: { sessionId: session.id, tmuxSessionName: session.tmuxSessionName } };
1871
1891
  }
1872
1892
  case "spawn": {
1873
- const cwd = sessionCwdMap.get(req.sessionId);
1874
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1875
- const result = await handleSpawn(req.sessionId, cwd, req.agentType, req.name, req.instruction, req.worktree);
1893
+ const tracking = sessionTrackingMap.get(req.sessionId);
1894
+ if (!tracking) return unknownSessionError(req.sessionId);
1895
+ const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.worktree);
1876
1896
  return { ok: true, data: { agentId: result.agentId } };
1877
1897
  }
1878
1898
  case "submit": {
1879
- const cwd = sessionCwdMap.get(req.sessionId);
1880
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1881
- const windowId = sessionWindowMap2.get(req.sessionId);
1882
- if (!windowId) return { ok: false, error: `No tmux window found for session: ${req.sessionId}` };
1883
- await handleSubmit(cwd, req.sessionId, req.agentId, req.report, windowId);
1899
+ const tracking = sessionTrackingMap.get(req.sessionId);
1900
+ if (!tracking) return unknownSessionError(req.sessionId);
1901
+ if (!tracking.windowId) return { ok: false, error: `No tmux window found for session: ${req.sessionId}` };
1902
+ await handleSubmit(tracking.cwd, req.sessionId, req.agentId, req.report, tracking.windowId);
1884
1903
  return { ok: true };
1885
1904
  }
1886
1905
  case "report": {
1887
- const cwd = sessionCwdMap.get(req.sessionId);
1888
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1889
- await handleReport(cwd, req.sessionId, req.agentId, req.content);
1906
+ const tracking = sessionTrackingMap.get(req.sessionId);
1907
+ if (!tracking) return unknownSessionError(req.sessionId);
1908
+ await handleReport(tracking.cwd, req.sessionId, req.agentId, req.content);
1890
1909
  return { ok: true };
1891
1910
  }
1892
1911
  case "yield": {
1893
- const cwd = sessionCwdMap.get(req.sessionId);
1894
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1895
- await handleYield(req.sessionId, cwd, req.nextPrompt, req.mode);
1912
+ const tracking = sessionTrackingMap.get(req.sessionId);
1913
+ if (!tracking) return unknownSessionError(req.sessionId);
1914
+ await handleYield(req.sessionId, tracking.cwd, req.nextPrompt, req.mode);
1896
1915
  return { ok: true };
1897
1916
  }
1898
1917
  case "complete": {
1899
- const cwd = sessionCwdMap.get(req.sessionId);
1900
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1901
- await handleComplete(req.sessionId, cwd, req.report);
1918
+ const tracking = sessionTrackingMap.get(req.sessionId);
1919
+ if (!tracking) return unknownSessionError(req.sessionId);
1920
+ await handleComplete(req.sessionId, tracking.cwd, req.report);
1902
1921
  return { ok: true };
1903
1922
  }
1904
1923
  case "continue": {
1905
- const cwd = sessionCwdMap.get(req.sessionId);
1906
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1907
- await handleContinue(req.sessionId, cwd);
1924
+ const tracking = sessionTrackingMap.get(req.sessionId);
1925
+ if (!tracking) return unknownSessionError(req.sessionId);
1926
+ await handleContinue(req.sessionId, tracking.cwd);
1908
1927
  return { ok: true };
1909
1928
  }
1910
1929
  case "status": {
1911
1930
  if (req.sessionId) {
1912
- const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
1913
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1931
+ const cwd = sessionTrackingMap.get(req.sessionId)?.cwd ?? req.cwd;
1932
+ if (!cwd) return unknownSessionError(req.sessionId);
1914
1933
  const session = getSessionStatus(cwd, req.sessionId);
1915
1934
  return { ok: true, data: { session } };
1916
1935
  }
@@ -1920,21 +1939,21 @@ async function handleRequest(req) {
1920
1939
  const allSessions = [];
1921
1940
  if (req.all) {
1922
1941
  const seenCwds = /* @__PURE__ */ new Set();
1923
- for (const cwd of sessionCwdMap.values()) {
1924
- if (seenCwds.has(cwd)) continue;
1925
- seenCwds.add(cwd);
1926
- const sessions = listSessions(cwd);
1927
- allSessions.push(...sessions.map((s) => ({ ...s, cwd })));
1942
+ for (const tracking of sessionTrackingMap.values()) {
1943
+ if (seenCwds.has(tracking.cwd)) continue;
1944
+ seenCwds.add(tracking.cwd);
1945
+ const sessions = listSessions(tracking.cwd);
1946
+ allSessions.push(...sessions.map((s) => ({ ...s, cwd: tracking.cwd })));
1928
1947
  }
1929
1948
  } else {
1930
1949
  const sessions = listSessions(req.cwd);
1931
1950
  allSessions.push(...sessions.map((s) => ({ ...s, cwd: req.cwd })));
1932
1951
  let totalCount = allSessions.length;
1933
1952
  const seenCwds = /* @__PURE__ */ new Set([req.cwd]);
1934
- for (const cwd of sessionCwdMap.values()) {
1935
- if (seenCwds.has(cwd)) continue;
1936
- seenCwds.add(cwd);
1937
- totalCount += listSessions(cwd).length;
1953
+ for (const tracking of sessionTrackingMap.values()) {
1954
+ if (seenCwds.has(tracking.cwd)) continue;
1955
+ seenCwds.add(tracking.cwd);
1956
+ totalCount += listSessions(tracking.cwd).length;
1938
1957
  }
1939
1958
  if (totalCount > allSessions.length) {
1940
1959
  return { ok: true, data: { sessions: allSessions, totalCount, filtered: true } };
@@ -1943,114 +1962,109 @@ async function handleRequest(req) {
1943
1962
  return { ok: true, data: { sessions: allSessions } };
1944
1963
  }
1945
1964
  case "resume": {
1946
- let cwd = sessionCwdMap.get(req.sessionId);
1947
- if (!cwd) {
1965
+ let tracking = sessionTrackingMap.get(req.sessionId);
1966
+ if (!tracking) {
1948
1967
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1949
- if (existsSync7(stateFile)) {
1950
- cwd = req.cwd;
1951
- registerSessionCwd(req.sessionId, cwd);
1968
+ if (existsSync8(stateFile)) {
1969
+ tracking = { cwd: req.cwd, messageCounter: 0 };
1970
+ sessionTrackingMap.set(req.sessionId, tracking);
1971
+ persistSessionRegistry();
1952
1972
  } else {
1953
- return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
1973
+ return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}. Run \`sisyphus list --all\` to see available sessions.` };
1954
1974
  }
1955
1975
  }
1956
- const session = await resumeSession(req.sessionId, cwd, req.message);
1957
- if (session.tmuxSessionName) sessionTmuxMap.set(req.sessionId, session.tmuxSessionName);
1958
- if (session.tmuxWindowId) sessionWindowMap2.set(req.sessionId, session.tmuxWindowId);
1976
+ const session = await resumeSession(req.sessionId, tracking.cwd, req.message);
1977
+ if (session.tmuxSessionName) tracking.tmuxSession = session.tmuxSessionName;
1978
+ if (session.tmuxWindowId) tracking.windowId = session.tmuxWindowId;
1959
1979
  return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
1960
1980
  }
1961
1981
  case "register_claude_session": {
1962
- const cwd = sessionCwdMap.get(req.sessionId);
1963
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1964
- await handleRegisterClaudeSession(cwd, req.sessionId, req.agentId, req.claudeSessionId);
1982
+ const tracking = sessionTrackingMap.get(req.sessionId);
1983
+ if (!tracking) return unknownSessionError(req.sessionId);
1984
+ await handleRegisterClaudeSession(tracking.cwd, req.sessionId, req.agentId, req.claudeSessionId);
1965
1985
  return { ok: true };
1966
1986
  }
1967
1987
  case "kill": {
1968
- const cwd = sessionCwdMap.get(req.sessionId);
1969
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1970
- const killedAgents = await handleKill(req.sessionId, cwd);
1971
- sessionCwdMap.delete(req.sessionId);
1972
- sessionTmuxMap.delete(req.sessionId);
1973
- sessionWindowMap2.delete(req.sessionId);
1988
+ const tracking = sessionTrackingMap.get(req.sessionId);
1989
+ if (!tracking) return unknownSessionError(req.sessionId);
1990
+ const killedAgents = await handleKill(req.sessionId, tracking.cwd);
1991
+ sessionTrackingMap.delete(req.sessionId);
1974
1992
  persistSessionRegistry();
1975
1993
  return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
1976
1994
  }
1977
1995
  case "kill-agent": {
1978
- const cwd = sessionCwdMap.get(req.sessionId);
1979
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1980
- await handleKillAgent(req.sessionId, cwd, req.agentId);
1996
+ const tracking = sessionTrackingMap.get(req.sessionId);
1997
+ if (!tracking) return unknownSessionError(req.sessionId);
1998
+ await handleKillAgent(req.sessionId, tracking.cwd, req.agentId);
1981
1999
  return { ok: true, data: { agentId: req.agentId } };
1982
2000
  }
1983
2001
  case "restart-agent": {
1984
- const cwd = sessionCwdMap.get(req.sessionId);
1985
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1986
- await handleRestartAgent(req.sessionId, cwd, req.agentId);
2002
+ const tracking = sessionTrackingMap.get(req.sessionId);
2003
+ if (!tracking) return unknownSessionError(req.sessionId);
2004
+ await handleRestartAgent(req.sessionId, tracking.cwd, req.agentId);
1987
2005
  return { ok: true, data: { agentId: req.agentId } };
1988
2006
  }
1989
2007
  case "rollback": {
1990
- let cwd = sessionCwdMap.get(req.sessionId);
1991
- if (!cwd) {
2008
+ let tracking = sessionTrackingMap.get(req.sessionId);
2009
+ if (!tracking) {
1992
2010
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1993
- if (existsSync7(stateFile)) {
1994
- cwd = req.cwd;
1995
- registerSessionCwd(req.sessionId, cwd);
2011
+ if (existsSync8(stateFile)) {
2012
+ registerSessionCwd(req.sessionId, req.cwd);
2013
+ tracking = sessionTrackingMap.get(req.sessionId);
1996
2014
  } else {
1997
- return { ok: false, error: `Unknown session: ${req.sessionId}` };
2015
+ return unknownSessionError(req.sessionId);
1998
2016
  }
1999
2017
  }
2000
- const result = await handleRollback(req.sessionId, cwd, req.toCycle);
2018
+ const result = await handleRollback(req.sessionId, tracking.cwd, req.toCycle);
2001
2019
  return { ok: true, data: result };
2002
2020
  }
2003
2021
  case "delete": {
2004
- const activeCwd = sessionCwdMap.get(req.sessionId);
2005
- if (activeCwd) {
2022
+ const activeTracking = sessionTrackingMap.get(req.sessionId);
2023
+ if (activeTracking) {
2006
2024
  try {
2007
- await handleKill(req.sessionId, activeCwd);
2025
+ await handleKill(req.sessionId, activeTracking.cwd);
2008
2026
  } catch {
2009
2027
  }
2010
- sessionCwdMap.delete(req.sessionId);
2011
- sessionTmuxMap.delete(req.sessionId);
2012
- sessionWindowMap2.delete(req.sessionId);
2013
- sessionMessageCounters.delete(req.sessionId);
2028
+ sessionTrackingMap.delete(req.sessionId);
2014
2029
  persistSessionRegistry();
2015
2030
  }
2016
- const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
2031
+ const { sessionDir: sessionDir2 } = await import("./paths-NUUALUVP.js");
2017
2032
  rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
2018
2033
  return { ok: true };
2019
2034
  }
2020
2035
  case "pane-exited": {
2021
2036
  const entry = lookupPane(req.paneId);
2022
2037
  if (!entry) return { ok: true };
2023
- const cwd = sessionCwdMap.get(entry.sessionId);
2024
- if (!cwd) {
2038
+ const tracking = sessionTrackingMap.get(entry.sessionId);
2039
+ if (!tracking) {
2025
2040
  unregisterPane(req.paneId);
2026
2041
  return { ok: true };
2027
2042
  }
2028
2043
  unregisterPane(req.paneId);
2029
- await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
2044
+ await handlePaneExited(req.paneId, tracking.cwd, entry.sessionId, entry.role, entry.agentId);
2030
2045
  return { ok: true };
2031
2046
  }
2032
2047
  case "update-task": {
2033
- const cwd = sessionCwdMap.get(req.sessionId);
2034
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2035
- await updateTask(cwd, req.sessionId, req.task);
2048
+ const tracking = sessionTrackingMap.get(req.sessionId);
2049
+ if (!tracking) return unknownSessionError(req.sessionId);
2050
+ await updateTask(tracking.cwd, req.sessionId, req.task);
2036
2051
  return { ok: true };
2037
2052
  }
2038
2053
  case "message": {
2039
- const cwd = sessionCwdMap.get(req.sessionId);
2040
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2041
- const counter = (sessionMessageCounters.get(req.sessionId) ?? 0) + 1;
2042
- sessionMessageCounters.set(req.sessionId, counter);
2043
- const id = `msg-${String(counter).padStart(3, "0")}`;
2054
+ const tracking = sessionTrackingMap.get(req.sessionId);
2055
+ if (!tracking) return unknownSessionError(req.sessionId);
2056
+ tracking.messageCounter += 1;
2057
+ const id = `msg-${String(tracking.messageCounter).padStart(3, "0")}`;
2044
2058
  const source = req.source ?? { type: "user" };
2045
2059
  const summary = req.content.length > 200 ? req.content.slice(0, 200) + "..." : req.content;
2046
2060
  let filePath;
2047
2061
  if (req.content.length > 200) {
2048
- const dir = messagesDir(cwd, req.sessionId);
2062
+ const dir = messagesDir(tracking.cwd, req.sessionId);
2049
2063
  mkdirSync4(dir, { recursive: true });
2050
2064
  filePath = join5(dir, `${id}.md`);
2051
- writeFileSync4(filePath, req.content, "utf-8");
2065
+ writeFileSync5(filePath, req.content, "utf-8");
2052
2066
  }
2053
- await appendMessage(cwd, req.sessionId, {
2067
+ await appendMessage(tracking.cwd, req.sessionId, {
2054
2068
  id,
2055
2069
  source,
2056
2070
  content: req.content,
@@ -2069,9 +2083,9 @@ async function handleRequest(req) {
2069
2083
  }
2070
2084
  }
2071
2085
  function startServer() {
2072
- return new Promise((resolve4, reject) => {
2086
+ return new Promise((resolve5, reject) => {
2073
2087
  const sock = socketPath();
2074
- if (existsSync7(sock)) {
2088
+ if (existsSync8(sock)) {
2075
2089
  unlinkSync(sock);
2076
2090
  }
2077
2091
  server = createServer((conn) => {
@@ -2101,31 +2115,31 @@ function startServer() {
2101
2115
  server.on("error", reject);
2102
2116
  server.listen(sock, () => {
2103
2117
  console.log(`[sisyphus] Daemon listening on ${sock}`);
2104
- resolve4(server);
2118
+ resolve5(server);
2105
2119
  });
2106
2120
  });
2107
2121
  }
2108
2122
  function stopServer() {
2109
- return new Promise((resolve4) => {
2123
+ return new Promise((resolve5) => {
2110
2124
  if (!server) {
2111
- resolve4();
2125
+ resolve5();
2112
2126
  return;
2113
2127
  }
2114
2128
  server.close(() => {
2115
2129
  const sock = socketPath();
2116
- if (existsSync7(sock)) {
2130
+ if (existsSync8(sock)) {
2117
2131
  unlinkSync(sock);
2118
2132
  }
2119
2133
  server = null;
2120
- resolve4();
2134
+ resolve5();
2121
2135
  });
2122
2136
  });
2123
2137
  }
2124
2138
 
2125
2139
  // src/daemon/updater.ts
2126
2140
  import { execSync as execSync3 } from "child_process";
2127
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
2128
- import { resolve as resolve3 } from "path";
2141
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2 } from "fs";
2142
+ import { resolve as resolve4 } from "path";
2129
2143
  import { get } from "https";
2130
2144
  function isNewer(latest, current) {
2131
2145
  const a = latest.split(".").map(Number);
@@ -2141,7 +2155,7 @@ function isNewer(latest, current) {
2141
2155
  function readPackageVersion() {
2142
2156
  for (const rel of ["../package.json", "../../package.json"]) {
2143
2157
  try {
2144
- const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
2158
+ const raw = readFileSync7(resolve4(import.meta.dirname, rel), "utf-8");
2145
2159
  const pkg = JSON.parse(raw);
2146
2160
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
2147
2161
  } catch {
@@ -2151,9 +2165,9 @@ function readPackageVersion() {
2151
2165
  }
2152
2166
  var currentVersion = readPackageVersion();
2153
2167
  function checkForUpdate() {
2154
- return new Promise((resolve4) => {
2168
+ return new Promise((resolve5) => {
2155
2169
  const timeout = setTimeout(() => {
2156
- resolve4(null);
2170
+ resolve5(null);
2157
2171
  }, 5e3);
2158
2172
  const req = get("https://registry.npmjs.org/sisyphi/latest", (res) => {
2159
2173
  let data = "";
@@ -2165,24 +2179,24 @@ function checkForUpdate() {
2165
2179
  try {
2166
2180
  const { version: latest } = JSON.parse(data);
2167
2181
  if (latest && isNewer(latest, currentVersion)) {
2168
- resolve4({ current: currentVersion, latest });
2182
+ resolve5({ current: currentVersion, latest });
2169
2183
  } else {
2170
- resolve4(null);
2184
+ resolve5(null);
2171
2185
  }
2172
2186
  } catch {
2173
- resolve4(null);
2187
+ resolve5(null);
2174
2188
  }
2175
2189
  });
2176
2190
  });
2177
2191
  req.on("error", () => {
2178
2192
  clearTimeout(timeout);
2179
- resolve4(null);
2193
+ resolve5(null);
2180
2194
  });
2181
2195
  });
2182
2196
  }
2183
2197
  function applyUpdate(expectedVersion) {
2184
2198
  try {
2185
- const nodeDir = resolve3(process.execPath, "..");
2199
+ const nodeDir = resolve4(process.execPath, "..");
2186
2200
  const env = { ...process.env, PATH: `${nodeDir}:${process.env.PATH ?? ""}` };
2187
2201
  execSync3("npm install -g sisyphi", { timeout: 15e3, stdio: "pipe", env });
2188
2202
  const result = execSync3("npm ls -g sisyphi --json --depth=0", {
@@ -2204,7 +2218,7 @@ function applyUpdate(expectedVersion) {
2204
2218
  }
2205
2219
  function markUpdating(version) {
2206
2220
  try {
2207
- writeFileSync5(daemonUpdatingPath(), version, "utf-8");
2221
+ writeFileSync6(daemonUpdatingPath(), version, "utf-8");
2208
2222
  } catch {
2209
2223
  }
2210
2224
  }
@@ -2234,6 +2248,11 @@ async function checkAndApply() {
2234
2248
  }
2235
2249
 
2236
2250
  // src/daemon/index.ts
2251
+ var nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
2252
+ if (nodeVersion < 22) {
2253
+ console.error(`[sisyphus] Node.js v22+ required (current: v${process.versions.node})`);
2254
+ process.exit(1);
2255
+ }
2237
2256
  var ts = () => (/* @__PURE__ */ new Date()).toISOString();
2238
2257
  var origLog = console.log.bind(console);
2239
2258
  var origError = console.error.bind(console);
@@ -2265,7 +2284,7 @@ function acquirePidLock() {
2265
2284
  console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
2266
2285
  process.exit(0);
2267
2286
  }
2268
- writeFileSync6(daemonPidPath(), String(process.pid), "utf-8");
2287
+ writeFileSync7(daemonPidPath(), String(process.pid), "utf-8");
2269
2288
  }
2270
2289
  function isLaunchdManaged() {
2271
2290
  try {
@@ -2324,7 +2343,7 @@ async function recoverSessions() {
2324
2343
  let recovered = 0;
2325
2344
  for (const [sessionId, cwd] of entries) {
2326
2345
  const stateFile = statePath(cwd, sessionId);
2327
- if (!existsSync8(stateFile)) {
2346
+ if (!existsSync9(stateFile)) {
2328
2347
  continue;
2329
2348
  }
2330
2349
  try {