sisyphi 1.0.5 → 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,18 +788,15 @@ ${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
- const scriptLines = [
791
+ const bannerCmd = resolveBannerCmd();
792
+ const notifyCmd = buildNotifyCmd(paneId);
793
+ const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `orchestrator-run-${cycleNum}`, [
775
794
  "#!/usr/bin/env bash",
776
795
  ...bannerCmd ? [bannerCmd] : [],
777
796
  envExports,
778
- `${claudeCmd}`,
797
+ claudeCmd,
779
798
  notifyCmd
780
- ];
781
- const scriptPath = `${promptsDir(cwd, sessionId)}/orchestrator-run-${cycleNum}.sh`;
782
- writeFileSync2(scriptPath, scriptLines.join("\n"), { mode: 493 });
799
+ ]);
783
800
  sendKeys(paneId, `bash '${scriptPath}'`);
784
801
  await addOrchestratorCycle(cwd, sessionId, {
785
802
  cycle: cycleNum,
@@ -823,30 +840,13 @@ function cleanupSessionMaps(sessionId) {
823
840
  }
824
841
 
825
842
  // src/daemon/agent.ts
826
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5, existsSync as existsSync5 } from "fs";
827
- 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";
828
846
 
829
847
  // src/daemon/worktree.ts
830
- import { execSync as execSync2 } from "child_process";
831
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
832
- import { dirname as dirname3, join as join4 } from "path";
833
- var EXEC_ENV2 = {
834
- ...process.env,
835
- PATH: `/opt/homebrew/bin:/usr/local/bin:${process.env["PATH"] ?? "/usr/bin:/bin"}`
836
- };
837
- function exec2(cmd, cwd) {
838
- return execSync2(cmd, { encoding: "utf-8", env: EXEC_ENV2, cwd }).trim();
839
- }
840
- function execSafe2(cmd, cwd) {
841
- try {
842
- return exec2(cmd, cwd);
843
- } catch {
844
- return null;
845
- }
846
- }
847
- function shellQuote2(s) {
848
- return `'${s.replace(/'/g, "'\\''")}'`;
849
- }
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";
850
850
  function loadWorktreeConfig(cwd) {
851
851
  try {
852
852
  const content = readFileSync4(worktreeConfigPath(cwd), "utf-8");
@@ -858,52 +858,52 @@ function loadWorktreeConfig(cwd) {
858
858
  function createWorktreeShell(cwd, sessionId, agentId) {
859
859
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
860
860
  const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
861
- mkdirSync2(dirname3(worktreePath), { recursive: true });
862
- execSafe2(`git -C ${shellQuote2(cwd)} worktree prune`);
863
- if (existsSync4(worktreePath)) {
864
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove --force ${shellQuote2(worktreePath)}`);
865
- }
866
- execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
867
- exec2(`git -C ${shellQuote2(cwd)} branch ${shellQuote2(branchName)} HEAD`);
868
- 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)}`);
869
869
  return { worktreePath, branchName };
870
870
  }
871
871
  function bootstrapWorktree(cwd, worktreePath, config) {
872
872
  if (config.copy) {
873
873
  for (const entry of config.copy) {
874
874
  const dest = join4(worktreePath, entry);
875
- mkdirSync2(dirname3(dest), { recursive: true });
876
- 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)}`);
877
877
  }
878
878
  }
879
879
  if (config.clone) {
880
880
  for (const entry of config.clone) {
881
881
  const dest = join4(worktreePath, entry);
882
- mkdirSync2(dirname3(dest), { recursive: true });
883
- const src = shellQuote2(join4(cwd, entry));
884
- const dstQ = shellQuote2(dest);
885
- if (execSafe2(`cp -Rc ${src} ${dstQ}`) === null) {
886
- 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}`);
887
887
  }
888
888
  }
889
889
  }
890
890
  if (config.symlink) {
891
891
  for (const entry of config.symlink) {
892
892
  const dest = join4(worktreePath, entry);
893
- mkdirSync2(dirname3(dest), { recursive: true });
894
- 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)}`);
895
895
  }
896
896
  }
897
897
  if (config.init) {
898
898
  try {
899
- exec2(config.init, worktreePath);
899
+ exec(config.init, worktreePath);
900
900
  } catch (err) {
901
901
  console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
902
902
  }
903
903
  }
904
904
  }
905
905
  function resolveWorktreeBranch(cwd, worktreePath) {
906
- const output = execSafe2(`git -C ${shellQuote2(cwd)} worktree list --porcelain`);
906
+ const output = execSafe(`git -C ${shellQuote(cwd)} worktree list --porcelain`);
907
907
  if (!output) return null;
908
908
  const lines = output.split("\n");
909
909
  for (let i = 0; i < lines.length; i++) {
@@ -925,30 +925,30 @@ function mergeWorktrees(cwd, agents) {
925
925
  (a) => a.worktreePath && a.mergeStatus === "pending"
926
926
  );
927
927
  const results = [];
928
- execSafe2(`git -C ${shellQuote2(cwd)} add .sisyphus`);
929
- 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'`);
930
930
  for (const agent of pending) {
931
931
  const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
932
932
  if (!branch) {
933
933
  results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
934
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)} --force`);
934
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
935
935
  continue;
936
936
  }
937
- 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`);
938
938
  if (!aheadLog) {
939
939
  results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
940
940
  cleanupWorktree(cwd, agent.worktreePath, branch);
941
941
  continue;
942
942
  }
943
943
  const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
944
- 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)}`;
945
945
  try {
946
- exec2(mergeCmd);
947
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)}`);
948
- 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)}`);
949
949
  results.push({ agentId: agent.id, name: agent.name, status: "merged" });
950
950
  } catch (err) {
951
- execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
951
+ execSafe(`git -C ${shellQuote(cwd)} merge --abort`);
952
952
  const errObj = err;
953
953
  const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
954
954
  const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
@@ -959,9 +959,9 @@ function mergeWorktrees(cwd, agents) {
959
959
  return results;
960
960
  }
961
961
  function cleanupWorktree(cwd, worktreePath, branchName) {
962
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(worktreePath)} --force`);
963
- execSafe2(`git -C ${shellQuote2(cwd)} branch -D ${shellQuote2(branchName)}`);
964
- 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);
965
965
  try {
966
966
  const entries = readdirSync4(baseDir);
967
967
  if (entries.length === 0) {
@@ -1023,7 +1023,7 @@ function clearAgentCounter(sessionId) {
1023
1023
  agentCounters.delete(sessionId);
1024
1024
  }
1025
1025
  function renderAgentSuffix(sessionId, instruction, worktreeContext) {
1026
- const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
1026
+ const templatePath = resolve3(import.meta.dirname, "../templates/agent-suffix.md");
1027
1027
  let template;
1028
1028
  try {
1029
1029
  template = readFileSync5(templatePath, "utf-8");
@@ -1047,7 +1047,7 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1047
1047
  mkdirSync3(`${base}/.claude-plugin`, { recursive: true });
1048
1048
  mkdirSync3(`${base}/agents`, { recursive: true });
1049
1049
  mkdirSync3(`${base}/hooks`, { recursive: true });
1050
- writeFileSync3(
1050
+ writeFileSync4(
1051
1051
  `${base}/.claude-plugin/plugin.json`,
1052
1052
  JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
1053
1053
  "utf-8"
@@ -1056,34 +1056,14 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1056
1056
  const shortName = agentType.replace(/^sisyphus:/, "");
1057
1057
  copyFileSync2(agentConfig.filePath, `${base}/agents/${shortName}.md`);
1058
1058
  }
1059
- const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
1059
+ const srcHooks = resolve3(import.meta.dirname, "../templates/agent-plugin/hooks");
1060
1060
  for (const f of ["hooks.json", "require-submit.sh", "intercept-send-message.sh"]) {
1061
1061
  copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
1062
1062
  }
1063
1063
  return base;
1064
1064
  }
1065
- async function spawnAgent(opts) {
1066
- const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
1067
- const count = (agentCounters.get(sessionId) ?? 0) + 1;
1068
- agentCounters.set(sessionId, count);
1069
- const agentId = `agent-${String(count).padStart(3, "0")}`;
1070
- const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1071
- const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1072
- const provider = detectProvider(agentConfig?.frontmatter.model);
1073
- const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
1074
- let paneCwd = cwd;
1075
- let worktreePath;
1076
- let branchName;
1077
- let worktreeContext;
1078
- if (opts.worktree) {
1079
- const wt = createWorktreeShell(cwd, sessionId, agentId);
1080
- worktreePath = wt.worktreePath;
1081
- branchName = wt.branchName;
1082
- paneCwd = worktreePath;
1083
- const session = getSession(cwd, sessionId);
1084
- const portOffset = countWorktreeAgents(session.agents) + 1;
1085
- worktreeContext = { offset: portOffset, total: portOffset, branchName };
1086
- }
1065
+ function setupAgentPane(opts) {
1066
+ const { sessionId, cwd, agentId, agentType, name, instruction, windowId, color, provider, agentConfig, worktreeContext, paneCwd } = opts;
1087
1067
  const paneId = createPane(windowId, paneCwd);
1088
1068
  registerPane(paneId, sessionId, "agent", agentId);
1089
1069
  const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
@@ -1092,50 +1072,88 @@ async function spawnAgent(opts) {
1092
1072
  setPaneStyle(paneId, color);
1093
1073
  const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
1094
1074
  const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
1095
- writeFileSync3(suffixFilePath, suffix, "utf-8");
1096
- const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
1097
- const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
1098
- const cliBin = resolve2(import.meta.dirname, "cli.js");
1099
- const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
1100
- const envExports = [
1075
+ writeFileSync4(suffixFilePath, suffix, "utf-8");
1076
+ const bannerCmd = resolveBannerCmd();
1077
+ const npmBinDir = resolveNpmBinDir();
1078
+ const envExports = buildEnvExports([
1101
1079
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
1102
1080
  `export SISYPHUS_AGENT_ID='${agentId}'`,
1103
1081
  `export SISYPHUS_CWD='${cwd}'`,
1104
1082
  ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1105
1083
  `export PATH="${npmBinDir}:$PATH"`
1106
- ].join(" && ");
1107
- const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
1084
+ ]);
1085
+ const notifyCmd = buildNotifyCmd(paneId);
1108
1086
  let mainCmd;
1109
1087
  if (provider === "openai") {
1110
1088
  const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
1111
1089
  const parts = [];
1112
- if (agentConfig?.body) {
1113
- parts.push(agentConfig.body);
1114
- }
1090
+ if (agentConfig?.body) parts.push(agentConfig.body);
1115
1091
  parts.push(suffix);
1116
1092
  parts.push(`## Task
1117
1093
 
1118
1094
  ${instruction}`);
1119
- writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
1095
+ writeFileSync4(codexPromptPath, parts.join("\n\n"), "utf-8");
1120
1096
  const model = agentConfig?.frontmatter.model ?? "codex-mini";
1121
- 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}')"`;
1122
1098
  } else {
1123
- const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
1099
+ const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote(agentType)}` : "";
1124
1100
  const config = loadConfig(cwd);
1125
1101
  const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
1126
1102
  const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
1127
- 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)}`;
1128
1104
  }
1129
- const scriptLines = [
1105
+ const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
1130
1106
  "#!/usr/bin/env bash",
1131
- ...bannerCmd ? [bannerCmd.replace(/ &&$/, "")] : [],
1107
+ ...bannerCmd ? [bannerCmd] : [],
1132
1108
  envExports,
1133
1109
  mainCmd,
1134
1110
  notifyCmd
1135
- ];
1136
- const scriptPath = `${promptsDir(cwd, sessionId)}/${agentId}-run.sh`;
1137
- writeFileSync3(scriptPath, scriptLines.join("\n"), { mode: 493 });
1111
+ ]);
1138
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 };
1142
+ }
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
+ });
1139
1157
  const agent = {
1140
1158
  id: agentId,
1141
1159
  name,
@@ -1187,7 +1205,7 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1187
1205
  });
1188
1206
  }
1189
1207
  const { instruction, agentType, name, color } = agent;
1190
- const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1208
+ const bundledPluginPath = resolve3(import.meta.dirname, "../templates/agent-plugin");
1191
1209
  const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1192
1210
  const provider = detectProvider(agentConfig?.frontmatter.model);
1193
1211
  let paneCwd = cwd;
@@ -1208,56 +1226,20 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1208
1226
  }
1209
1227
  unregisterAgentPane(sessionId, agentId);
1210
1228
  }
1211
- const paneId = createPane(windowId, paneCwd);
1212
- registerPane(paneId, sessionId, "agent", agentId);
1213
- const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
1214
- const paneLabel = shortType ? `${name}-${shortType}` : name;
1215
- setPaneTitle(paneId, `${paneLabel} (${agentId})`);
1216
- setPaneStyle(paneId, color);
1217
- const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
1218
- const suffixFilePath = `${promptsDir(cwd, sessionId)}/${agentId}-system.md`;
1219
- writeFileSync3(suffixFilePath, suffix, "utf-8");
1220
- const bannerPath = resolve2(import.meta.dirname, "../templates/banner.txt");
1221
- const bannerCmd = existsSync5(bannerPath) ? `cat '${bannerPath}' &&` : "";
1222
- const cliBin = resolve2(import.meta.dirname, "cli.js");
1223
- const npmBinDir = resolve2(import.meta.dirname, "../../.bin");
1224
- const envExports = [
1225
- `export SISYPHUS_SESSION_ID='${sessionId}'`,
1226
- `export SISYPHUS_AGENT_ID='${agentId}'`,
1227
- `export SISYPHUS_CWD='${cwd}'`,
1228
- ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1229
- `export PATH="${npmBinDir}:$PATH"`
1230
- ].join(" && ");
1231
- const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
1232
- let mainCmd;
1233
- if (provider === "openai") {
1234
- const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
1235
- const parts = [];
1236
- if (agentConfig?.body) parts.push(agentConfig.body);
1237
- parts.push(suffix);
1238
- parts.push(`## Task
1239
-
1240
- ${instruction}`);
1241
- writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
1242
- const model = agentConfig?.frontmatter.model ?? "codex-mini";
1243
- mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
1244
- } else {
1245
- const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
1246
- const config = loadConfig(cwd);
1247
- const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
1248
- const pluginPath = createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig);
1249
- mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote3(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote3(instruction)}`;
1250
- }
1251
- const scriptLines = [
1252
- "#!/usr/bin/env bash",
1253
- ...bannerCmd ? [bannerCmd.replace(/ &&$/, "")] : [],
1254
- envExports,
1255
- mainCmd,
1256
- notifyCmd
1257
- ];
1258
- const scriptPath = `${promptsDir(cwd, sessionId)}/${agentId}-run.sh`;
1259
- writeFileSync3(scriptPath, scriptLines.join("\n"), { mode: 493 });
1260
- const fullCmd = `bash '${scriptPath}'`;
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
+ });
1261
1243
  await updateAgent(cwd, sessionId, agentId, {
1262
1244
  status: "running",
1263
1245
  paneId,
@@ -1282,7 +1264,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
1282
1264
  mkdirSync3(dir, { recursive: true });
1283
1265
  const num = nextReportNumber(cwd, sessionId, agentId);
1284
1266
  const filePath = reportFilePath(cwd, sessionId, agentId, num);
1285
- writeFileSync3(filePath, content, "utf-8");
1267
+ writeFileSync4(filePath, content, "utf-8");
1286
1268
  const entry = {
1287
1269
  type: "update",
1288
1270
  filePath,
@@ -1301,7 +1283,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
1301
1283
  const dir = reportsDir(cwd, sessionId);
1302
1284
  mkdirSync3(dir, { recursive: true });
1303
1285
  const filePath = reportFilePath(cwd, sessionId, agentId, "final");
1304
- writeFileSync3(filePath, report, "utf-8");
1286
+ writeFileSync4(filePath, report, "utf-8");
1305
1287
  const entry = {
1306
1288
  type: "final",
1307
1289
  filePath,
@@ -1344,9 +1326,6 @@ function allAgentsDone(session) {
1344
1326
  const running = session.agents.filter((a) => a.status === "running");
1345
1327
  return running.length === 0 && session.agents.length > 0;
1346
1328
  }
1347
- function shellQuote3(s) {
1348
- return `'${s.replace(/'/g, "'\\''")}'`;
1349
- }
1350
1329
 
1351
1330
  // src/daemon/pane-monitor.ts
1352
1331
  var monitorInterval = null;
@@ -1491,7 +1470,7 @@ var PRUNE_KEEP_DAYS = 7;
1491
1470
  function pruneOldSessions(cwd) {
1492
1471
  try {
1493
1472
  const dir = sessionsDir(cwd);
1494
- if (!existsSync6(dir)) return;
1473
+ if (!existsSync7(dir)) return;
1495
1474
  const entries = readdirSync6(dir, { withFileTypes: true });
1496
1475
  const candidates = [];
1497
1476
  for (const entry of entries) {
@@ -1573,7 +1552,7 @@ function getSessionStatus(cwd, sessionId) {
1573
1552
  }
1574
1553
  function listSessions(cwd) {
1575
1554
  const dir = sessionsDir(cwd);
1576
- if (!existsSync6(dir)) return [];
1555
+ if (!existsSync7(dir)) return [];
1577
1556
  const entries = readdirSync6(dir, { withFileTypes: true });
1578
1557
  const sessions = [];
1579
1558
  for (const entry of entries) {
@@ -1853,10 +1832,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1853
1832
 
1854
1833
  // src/daemon/server.ts
1855
1834
  var server = null;
1856
- var sessionCwdMap = /* @__PURE__ */ new Map();
1857
- var sessionMessageCounters = /* @__PURE__ */ new Map();
1858
- var sessionTmuxMap = /* @__PURE__ */ new Map();
1859
- var sessionWindowMap2 = /* @__PURE__ */ new Map();
1835
+ var sessionTrackingMap = /* @__PURE__ */ new Map();
1860
1836
  function registryPath() {
1861
1837
  return join5(globalDir(), "session-registry.json");
1862
1838
  }
@@ -1864,14 +1840,14 @@ function persistSessionRegistry() {
1864
1840
  const dir = globalDir();
1865
1841
  mkdirSync4(dir, { recursive: true });
1866
1842
  const registry = {};
1867
- for (const [id, cwd] of sessionCwdMap) {
1868
- registry[id] = cwd;
1843
+ for (const [id, tracking] of sessionTrackingMap) {
1844
+ registry[id] = tracking.cwd;
1869
1845
  }
1870
- writeFileSync4(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
1846
+ writeFileSync5(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
1871
1847
  }
1872
1848
  function loadSessionRegistry() {
1873
1849
  const p = registryPath();
1874
- if (!existsSync7(p)) return {};
1850
+ if (!existsSync8(p)) return {};
1875
1851
  try {
1876
1852
  return JSON.parse(readFileSync6(p, "utf-8"));
1877
1853
  } catch {
@@ -1879,65 +1855,81 @@ function loadSessionRegistry() {
1879
1855
  }
1880
1856
  }
1881
1857
  function registerSessionCwd(sessionId, cwd) {
1882
- 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
+ }
1883
1864
  persistSessionRegistry();
1884
1865
  }
1885
1866
  function registerSessionTmux(sessionId, tmuxSession, windowId) {
1886
- sessionTmuxMap.set(sessionId, tmuxSession);
1887
- 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.` };
1888
1877
  }
1889
1878
  async function handleRequest(req) {
1890
1879
  try {
1891
1880
  switch (req.type) {
1892
1881
  case "start": {
1893
1882
  const session = await startSession(req.task, req.cwd, req.context, req.name);
1894
- registerSessionCwd(session.id, req.cwd);
1895
- if (session.tmuxSessionName) sessionTmuxMap.set(session.id, session.tmuxSessionName);
1896
- 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();
1897
1890
  return { ok: true, data: { sessionId: session.id, tmuxSessionName: session.tmuxSessionName } };
1898
1891
  }
1899
1892
  case "spawn": {
1900
- const cwd = sessionCwdMap.get(req.sessionId);
1901
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1902
- 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);
1903
1896
  return { ok: true, data: { agentId: result.agentId } };
1904
1897
  }
1905
1898
  case "submit": {
1906
- const cwd = sessionCwdMap.get(req.sessionId);
1907
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1908
- const windowId = sessionWindowMap2.get(req.sessionId);
1909
- if (!windowId) return { ok: false, error: `No tmux window found for session: ${req.sessionId}` };
1910
- 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);
1911
1903
  return { ok: true };
1912
1904
  }
1913
1905
  case "report": {
1914
- const cwd = sessionCwdMap.get(req.sessionId);
1915
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1916
- 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);
1917
1909
  return { ok: true };
1918
1910
  }
1919
1911
  case "yield": {
1920
- const cwd = sessionCwdMap.get(req.sessionId);
1921
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1922
- 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);
1923
1915
  return { ok: true };
1924
1916
  }
1925
1917
  case "complete": {
1926
- const cwd = sessionCwdMap.get(req.sessionId);
1927
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1928
- 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);
1929
1921
  return { ok: true };
1930
1922
  }
1931
1923
  case "continue": {
1932
- const cwd = sessionCwdMap.get(req.sessionId);
1933
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1934
- 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);
1935
1927
  return { ok: true };
1936
1928
  }
1937
1929
  case "status": {
1938
1930
  if (req.sessionId) {
1939
- const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
1940
- 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);
1941
1933
  const session = getSessionStatus(cwd, req.sessionId);
1942
1934
  return { ok: true, data: { session } };
1943
1935
  }
@@ -1947,21 +1939,21 @@ async function handleRequest(req) {
1947
1939
  const allSessions = [];
1948
1940
  if (req.all) {
1949
1941
  const seenCwds = /* @__PURE__ */ new Set();
1950
- for (const cwd of sessionCwdMap.values()) {
1951
- if (seenCwds.has(cwd)) continue;
1952
- seenCwds.add(cwd);
1953
- const sessions = listSessions(cwd);
1954
- 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 })));
1955
1947
  }
1956
1948
  } else {
1957
1949
  const sessions = listSessions(req.cwd);
1958
1950
  allSessions.push(...sessions.map((s) => ({ ...s, cwd: req.cwd })));
1959
1951
  let totalCount = allSessions.length;
1960
1952
  const seenCwds = /* @__PURE__ */ new Set([req.cwd]);
1961
- for (const cwd of sessionCwdMap.values()) {
1962
- if (seenCwds.has(cwd)) continue;
1963
- seenCwds.add(cwd);
1964
- 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;
1965
1957
  }
1966
1958
  if (totalCount > allSessions.length) {
1967
1959
  return { ok: true, data: { sessions: allSessions, totalCount, filtered: true } };
@@ -1970,114 +1962,109 @@ async function handleRequest(req) {
1970
1962
  return { ok: true, data: { sessions: allSessions } };
1971
1963
  }
1972
1964
  case "resume": {
1973
- let cwd = sessionCwdMap.get(req.sessionId);
1974
- if (!cwd) {
1965
+ let tracking = sessionTrackingMap.get(req.sessionId);
1966
+ if (!tracking) {
1975
1967
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1976
- if (existsSync7(stateFile)) {
1977
- cwd = req.cwd;
1978
- registerSessionCwd(req.sessionId, cwd);
1968
+ if (existsSync8(stateFile)) {
1969
+ tracking = { cwd: req.cwd, messageCounter: 0 };
1970
+ sessionTrackingMap.set(req.sessionId, tracking);
1971
+ persistSessionRegistry();
1979
1972
  } else {
1980
- 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.` };
1981
1974
  }
1982
1975
  }
1983
- const session = await resumeSession(req.sessionId, cwd, req.message);
1984
- if (session.tmuxSessionName) sessionTmuxMap.set(req.sessionId, session.tmuxSessionName);
1985
- 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;
1986
1979
  return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
1987
1980
  }
1988
1981
  case "register_claude_session": {
1989
- const cwd = sessionCwdMap.get(req.sessionId);
1990
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1991
- 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);
1992
1985
  return { ok: true };
1993
1986
  }
1994
1987
  case "kill": {
1995
- const cwd = sessionCwdMap.get(req.sessionId);
1996
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1997
- const killedAgents = await handleKill(req.sessionId, cwd);
1998
- sessionCwdMap.delete(req.sessionId);
1999
- sessionTmuxMap.delete(req.sessionId);
2000
- 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);
2001
1992
  persistSessionRegistry();
2002
1993
  return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
2003
1994
  }
2004
1995
  case "kill-agent": {
2005
- const cwd = sessionCwdMap.get(req.sessionId);
2006
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2007
- 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);
2008
1999
  return { ok: true, data: { agentId: req.agentId } };
2009
2000
  }
2010
2001
  case "restart-agent": {
2011
- const cwd = sessionCwdMap.get(req.sessionId);
2012
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2013
- 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);
2014
2005
  return { ok: true, data: { agentId: req.agentId } };
2015
2006
  }
2016
2007
  case "rollback": {
2017
- let cwd = sessionCwdMap.get(req.sessionId);
2018
- if (!cwd) {
2008
+ let tracking = sessionTrackingMap.get(req.sessionId);
2009
+ if (!tracking) {
2019
2010
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
2020
- if (existsSync7(stateFile)) {
2021
- cwd = req.cwd;
2022
- registerSessionCwd(req.sessionId, cwd);
2011
+ if (existsSync8(stateFile)) {
2012
+ registerSessionCwd(req.sessionId, req.cwd);
2013
+ tracking = sessionTrackingMap.get(req.sessionId);
2023
2014
  } else {
2024
- return { ok: false, error: `Unknown session: ${req.sessionId}` };
2015
+ return unknownSessionError(req.sessionId);
2025
2016
  }
2026
2017
  }
2027
- const result = await handleRollback(req.sessionId, cwd, req.toCycle);
2018
+ const result = await handleRollback(req.sessionId, tracking.cwd, req.toCycle);
2028
2019
  return { ok: true, data: result };
2029
2020
  }
2030
2021
  case "delete": {
2031
- const activeCwd = sessionCwdMap.get(req.sessionId);
2032
- if (activeCwd) {
2022
+ const activeTracking = sessionTrackingMap.get(req.sessionId);
2023
+ if (activeTracking) {
2033
2024
  try {
2034
- await handleKill(req.sessionId, activeCwd);
2025
+ await handleKill(req.sessionId, activeTracking.cwd);
2035
2026
  } catch {
2036
2027
  }
2037
- sessionCwdMap.delete(req.sessionId);
2038
- sessionTmuxMap.delete(req.sessionId);
2039
- sessionWindowMap2.delete(req.sessionId);
2040
- sessionMessageCounters.delete(req.sessionId);
2028
+ sessionTrackingMap.delete(req.sessionId);
2041
2029
  persistSessionRegistry();
2042
2030
  }
2043
- const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
2031
+ const { sessionDir: sessionDir2 } = await import("./paths-NUUALUVP.js");
2044
2032
  rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
2045
2033
  return { ok: true };
2046
2034
  }
2047
2035
  case "pane-exited": {
2048
2036
  const entry = lookupPane(req.paneId);
2049
2037
  if (!entry) return { ok: true };
2050
- const cwd = sessionCwdMap.get(entry.sessionId);
2051
- if (!cwd) {
2038
+ const tracking = sessionTrackingMap.get(entry.sessionId);
2039
+ if (!tracking) {
2052
2040
  unregisterPane(req.paneId);
2053
2041
  return { ok: true };
2054
2042
  }
2055
2043
  unregisterPane(req.paneId);
2056
- await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
2044
+ await handlePaneExited(req.paneId, tracking.cwd, entry.sessionId, entry.role, entry.agentId);
2057
2045
  return { ok: true };
2058
2046
  }
2059
2047
  case "update-task": {
2060
- const cwd = sessionCwdMap.get(req.sessionId);
2061
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2062
- 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);
2063
2051
  return { ok: true };
2064
2052
  }
2065
2053
  case "message": {
2066
- const cwd = sessionCwdMap.get(req.sessionId);
2067
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
2068
- const counter = (sessionMessageCounters.get(req.sessionId) ?? 0) + 1;
2069
- sessionMessageCounters.set(req.sessionId, counter);
2070
- 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")}`;
2071
2058
  const source = req.source ?? { type: "user" };
2072
2059
  const summary = req.content.length > 200 ? req.content.slice(0, 200) + "..." : req.content;
2073
2060
  let filePath;
2074
2061
  if (req.content.length > 200) {
2075
- const dir = messagesDir(cwd, req.sessionId);
2062
+ const dir = messagesDir(tracking.cwd, req.sessionId);
2076
2063
  mkdirSync4(dir, { recursive: true });
2077
2064
  filePath = join5(dir, `${id}.md`);
2078
- writeFileSync4(filePath, req.content, "utf-8");
2065
+ writeFileSync5(filePath, req.content, "utf-8");
2079
2066
  }
2080
- await appendMessage(cwd, req.sessionId, {
2067
+ await appendMessage(tracking.cwd, req.sessionId, {
2081
2068
  id,
2082
2069
  source,
2083
2070
  content: req.content,
@@ -2096,9 +2083,9 @@ async function handleRequest(req) {
2096
2083
  }
2097
2084
  }
2098
2085
  function startServer() {
2099
- return new Promise((resolve4, reject) => {
2086
+ return new Promise((resolve5, reject) => {
2100
2087
  const sock = socketPath();
2101
- if (existsSync7(sock)) {
2088
+ if (existsSync8(sock)) {
2102
2089
  unlinkSync(sock);
2103
2090
  }
2104
2091
  server = createServer((conn) => {
@@ -2128,31 +2115,31 @@ function startServer() {
2128
2115
  server.on("error", reject);
2129
2116
  server.listen(sock, () => {
2130
2117
  console.log(`[sisyphus] Daemon listening on ${sock}`);
2131
- resolve4(server);
2118
+ resolve5(server);
2132
2119
  });
2133
2120
  });
2134
2121
  }
2135
2122
  function stopServer() {
2136
- return new Promise((resolve4) => {
2123
+ return new Promise((resolve5) => {
2137
2124
  if (!server) {
2138
- resolve4();
2125
+ resolve5();
2139
2126
  return;
2140
2127
  }
2141
2128
  server.close(() => {
2142
2129
  const sock = socketPath();
2143
- if (existsSync7(sock)) {
2130
+ if (existsSync8(sock)) {
2144
2131
  unlinkSync(sock);
2145
2132
  }
2146
2133
  server = null;
2147
- resolve4();
2134
+ resolve5();
2148
2135
  });
2149
2136
  });
2150
2137
  }
2151
2138
 
2152
2139
  // src/daemon/updater.ts
2153
2140
  import { execSync as execSync3 } from "child_process";
2154
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, unlinkSync as unlinkSync2 } from "fs";
2155
- 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";
2156
2143
  import { get } from "https";
2157
2144
  function isNewer(latest, current) {
2158
2145
  const a = latest.split(".").map(Number);
@@ -2168,7 +2155,7 @@ function isNewer(latest, current) {
2168
2155
  function readPackageVersion() {
2169
2156
  for (const rel of ["../package.json", "../../package.json"]) {
2170
2157
  try {
2171
- const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
2158
+ const raw = readFileSync7(resolve4(import.meta.dirname, rel), "utf-8");
2172
2159
  const pkg = JSON.parse(raw);
2173
2160
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
2174
2161
  } catch {
@@ -2178,9 +2165,9 @@ function readPackageVersion() {
2178
2165
  }
2179
2166
  var currentVersion = readPackageVersion();
2180
2167
  function checkForUpdate() {
2181
- return new Promise((resolve4) => {
2168
+ return new Promise((resolve5) => {
2182
2169
  const timeout = setTimeout(() => {
2183
- resolve4(null);
2170
+ resolve5(null);
2184
2171
  }, 5e3);
2185
2172
  const req = get("https://registry.npmjs.org/sisyphi/latest", (res) => {
2186
2173
  let data = "";
@@ -2192,24 +2179,24 @@ function checkForUpdate() {
2192
2179
  try {
2193
2180
  const { version: latest } = JSON.parse(data);
2194
2181
  if (latest && isNewer(latest, currentVersion)) {
2195
- resolve4({ current: currentVersion, latest });
2182
+ resolve5({ current: currentVersion, latest });
2196
2183
  } else {
2197
- resolve4(null);
2184
+ resolve5(null);
2198
2185
  }
2199
2186
  } catch {
2200
- resolve4(null);
2187
+ resolve5(null);
2201
2188
  }
2202
2189
  });
2203
2190
  });
2204
2191
  req.on("error", () => {
2205
2192
  clearTimeout(timeout);
2206
- resolve4(null);
2193
+ resolve5(null);
2207
2194
  });
2208
2195
  });
2209
2196
  }
2210
2197
  function applyUpdate(expectedVersion) {
2211
2198
  try {
2212
- const nodeDir = resolve3(process.execPath, "..");
2199
+ const nodeDir = resolve4(process.execPath, "..");
2213
2200
  const env = { ...process.env, PATH: `${nodeDir}:${process.env.PATH ?? ""}` };
2214
2201
  execSync3("npm install -g sisyphi", { timeout: 15e3, stdio: "pipe", env });
2215
2202
  const result = execSync3("npm ls -g sisyphi --json --depth=0", {
@@ -2231,7 +2218,7 @@ function applyUpdate(expectedVersion) {
2231
2218
  }
2232
2219
  function markUpdating(version) {
2233
2220
  try {
2234
- writeFileSync5(daemonUpdatingPath(), version, "utf-8");
2221
+ writeFileSync6(daemonUpdatingPath(), version, "utf-8");
2235
2222
  } catch {
2236
2223
  }
2237
2224
  }
@@ -2261,6 +2248,11 @@ async function checkAndApply() {
2261
2248
  }
2262
2249
 
2263
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
+ }
2264
2256
  var ts = () => (/* @__PURE__ */ new Date()).toISOString();
2265
2257
  var origLog = console.log.bind(console);
2266
2258
  var origError = console.error.bind(console);
@@ -2292,7 +2284,7 @@ function acquirePidLock() {
2292
2284
  console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
2293
2285
  process.exit(0);
2294
2286
  }
2295
- writeFileSync6(daemonPidPath(), String(process.pid), "utf-8");
2287
+ writeFileSync7(daemonPidPath(), String(process.pid), "utf-8");
2296
2288
  }
2297
2289
  function isLaunchdManaged() {
2298
2290
  try {
@@ -2351,7 +2343,7 @@ async function recoverSessions() {
2351
2343
  let recovered = 0;
2352
2344
  for (const [sessionId, cwd] of entries) {
2353
2345
  const stateFile = statePath(cwd, sessionId);
2354
- if (!existsSync8(stateFile)) {
2346
+ if (!existsSync9(stateFile)) {
2355
2347
  continue;
2356
2348
  }
2357
2349
  try {