sisyphi 1.0.5 → 1.0.7

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);
@@ -490,6 +509,7 @@ function createSession2(sessionName, windowName, cwd) {
490
509
  exec(`tmux new-session -d -s "${sessionName}" -n "${windowName}" -c ${shellQuote(cwd)}`);
491
510
  const windowId = exec(`tmux display-message -t "${sessionName}:${windowName}" -p "#{window_id}"`);
492
511
  const initialPaneId = exec(`tmux display-message -t "${sessionName}:${windowName}" -p "#{pane_id}"`);
512
+ configureSessionDefaults(sessionName, windowId);
493
513
  return { windowId, initialPaneId };
494
514
  }
495
515
  function paneExists(paneTarget) {
@@ -516,7 +536,8 @@ function setPaneTitle(paneTarget, title) {
516
536
  execSafe(`tmux select-pane -t "${paneTarget}" -T ${shellQuote(title)}`);
517
537
  }
518
538
  function setPaneStyle(paneTarget, color) {
519
- const fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path} #[default]`;
539
+ const gitBranch = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null || echo 'n/a')`;
540
+ const fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path} | ${gitBranch} #[default]`;
520
541
  execSafe(`tmux set -p -t "${paneTarget}" pane-border-format ${shellQuote(fmt)}`);
521
542
  execSafe(`tmux set -p -t "${paneTarget}" @pane_color "${color}"`);
522
543
  execSafe(`tmux set -w -t "${paneTarget}" pane-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
@@ -525,8 +546,12 @@ function setPaneStyle(paneTarget, color) {
525
546
  function selectLayout(windowTarget, layout = "even-horizontal") {
526
547
  execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
527
548
  }
528
- function shellQuote(s) {
529
- return `'${s.replace(/'/g, "'\\''")}'`;
549
+ function configureSessionDefaults(sessionName, windowId) {
550
+ execSafe(`tmux set -w -t "${windowId}" pane-border-status top`);
551
+ execSafe(`tmux set -w -t "${windowId}" allow-rename off`);
552
+ execSafe(`tmux set -w -t "${windowId}" automatic-rename off`);
553
+ execSafe(`tmux set-hook -t "${sessionName}" after-kill-pane "select-layout even-horizontal"`);
554
+ execSafe(`tmux set-hook -t "${sessionName}" pane-exited "select-layout even-horizontal"`);
530
555
  }
531
556
 
532
557
  // src/daemon/pane-registry.ts
@@ -573,16 +598,16 @@ function setOrchestratorPaneId(sessionId, paneId) {
573
598
  }
574
599
  function loadOrchestratorPrompt(cwd, mode) {
575
600
  const projectPath = projectOrchestratorPromptPath(cwd);
576
- if (existsSync3(projectPath)) {
601
+ if (existsSync4(projectPath)) {
577
602
  return readFileSync3(projectPath, "utf-8");
578
603
  }
579
- const basePath = resolve(import.meta.dirname, "../templates/orchestrator-base.md");
604
+ const basePath = resolve2(import.meta.dirname, "../templates/orchestrator-base.md");
580
605
  const base = readFileSync3(basePath, "utf-8");
581
606
  if (mode === "implementation") {
582
- const implPath = resolve(import.meta.dirname, "../templates/orchestrator-impl.md");
607
+ const implPath = resolve2(import.meta.dirname, "../templates/orchestrator-impl.md");
583
608
  return base + "\n\n" + readFileSync3(implPath, "utf-8");
584
609
  }
585
- const planningPath = resolve(import.meta.dirname, "../templates/orchestrator-planning.md");
610
+ const planningPath = resolve2(import.meta.dirname, "../templates/orchestrator-planning.md");
586
611
  return base + "\n\n" + readFileSync3(planningPath, "utf-8");
587
612
  }
588
613
  function formatStateForOrchestrator(session) {
@@ -601,7 +626,7 @@ ${session.context}
601
626
  }
602
627
  } else {
603
628
  let ctxFiles = [];
604
- if (existsSync3(ctxDir)) {
629
+ if (existsSync4(ctxDir)) {
605
630
  ctxFiles = readdirSync3(ctxDir).filter((f) => f !== "CLAUDE.md");
606
631
  }
607
632
  if (ctxFiles.length > 0) {
@@ -640,37 +665,24 @@ ${lines.join("\n")}
640
665
  const lastCycle = session.orchestratorCycles[session.orchestratorCycles.length - 1];
641
666
  if (lastCycle && lastCycle.agentsSpawned.length > 0) {
642
667
  const agentMap = new Map(session.agents.map((a) => [a.id, a]));
643
- const agentBlocks = lastCycle.agentsSpawned.map((id) => {
668
+ const agentLines = lastCycle.agentsSpawned.map((id) => {
644
669
  const agent = agentMap.get(id);
645
- if (!agent) return `<agent-${id} status="unknown">
646
- (no agent data)
647
- </agent-${id}>`;
670
+ if (!agent) return `- **${id}**: unknown (no agent data)`;
648
671
  const finalReport = agent.reports.find((r) => r.type === "final");
649
672
  const reportToUse = finalReport ?? agent.reports[agent.reports.length - 1];
650
- let reportContent = "(no reports)";
651
- if (reportToUse) {
652
- try {
653
- reportContent = readFileSync3(reportToUse.filePath, "utf-8");
654
- } catch {
655
- reportContent = `(could not read report: ${reportToUse.filePath})`;
656
- }
657
- }
658
- return `<agent-${id} name="${agent.name}" status="${agent.status}">
659
- ${reportContent}
660
- </agent-${id}>`;
673
+ const reportRef = reportToUse ? `@${reportToUse.filePath}` : "(no reports)";
674
+ return `- **${id}** (${agent.name}) [${agent.status}]: ${reportRef}`;
661
675
  }).join("\n");
662
676
  mostRecentCycleSection = `
663
677
  ### Most Recent Cycle
664
678
 
665
- <last-cycle>
666
- ${agentBlocks}
667
- </last-cycle>
679
+ ${agentLines}
668
680
  `;
669
681
  }
670
- const roadmapRef = existsSync3(roadmapFile) ? `@${roadmapFile}` : "(empty)";
682
+ const roadmapRef = existsSync4(roadmapFile) ? `@${roadmapFile}` : "(empty)";
671
683
  const worktreeAgents = session.agents.filter((a) => a.worktreePath);
672
684
  let worktreeSection = "";
673
- if (worktreeAgents.length > 0 || existsSync3(worktreeConfigPath(session.cwd))) {
685
+ if (worktreeAgents.length > 0 || existsSync4(worktreeConfigPath(session.cwd))) {
674
686
  let wtLines = "";
675
687
  if (worktreeAgents.length > 0) {
676
688
  wtLines = "\n" + worktreeAgents.map((a) => {
@@ -686,7 +698,7 @@ ${agentBlocks}
686
698
  return `- ${a.id}: ${status} (branch ${a.branchName})`;
687
699
  }).join("\n");
688
700
  }
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.";
701
+ 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
702
  worktreeSection = `
691
703
 
692
704
  ## Git Worktrees
@@ -694,7 +706,7 @@ ${agentBlocks}
694
706
  ${worktreeHint}${wtLines}`;
695
707
  }
696
708
  const goalFile = goalPath(session.cwd, session.id);
697
- const goalContent = existsSync3(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
709
+ const goalContent = existsSync4(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
698
710
  return `## Goal
699
711
 
700
712
  ${goalContent}
@@ -709,12 +721,17 @@ ${roadmapRef}
709
721
  ${worktreeSection}`;
710
722
  }
711
723
  async function spawnOrchestrator(sessionId, cwd, windowId, message) {
724
+ try {
725
+ execSync("which claude", { stdio: "pipe", env: EXEC_ENV });
726
+ } catch {
727
+ throw new Error("Claude CLI not found on PATH. Run `sisyphus doctor` to diagnose.");
728
+ }
712
729
  const session = getSession(cwd, sessionId);
713
730
  const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
714
731
  const mode = lastCycle?.mode ?? "planning";
715
732
  const basePrompt = loadOrchestratorPrompt(cwd, mode);
716
733
  const formattedState = formatStateForOrchestrator(session);
717
- const agentPluginPath = resolve(import.meta.dirname, "../templates/agent-plugin");
734
+ const agentPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
718
735
  const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd);
719
736
  agentTypes.push(
720
737
  { 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 +744,15 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
727
744
  const systemPrompt = basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines);
728
745
  const cycleNum = session.orchestratorCycles.length + 1;
729
746
  const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
730
- writeFileSync2(promptFilePath, systemPrompt, "utf-8");
747
+ writeFileSync3(promptFilePath, systemPrompt, "utf-8");
731
748
  sessionWindowMap.set(sessionId, windowId);
732
- const cliBin = resolve(import.meta.dirname, "cli.js");
733
- const npmBinDir = resolve(import.meta.dirname, "../../.bin");
734
- const envExports = [
749
+ const npmBinDir = resolveNpmBinDir();
750
+ const envExports = buildEnvExports([
735
751
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
736
752
  `export SISYPHUS_AGENT_ID='orchestrator'`,
737
753
  `export SISYPHUS_CWD='${cwd}'`,
738
754
  `export PATH="${npmBinDir}:$PATH"`
739
- ].join(" && ");
755
+ ]);
740
756
  let userPrompt = formattedState;
741
757
  if (message) {
742
758
  userPrompt += `
@@ -754,12 +770,12 @@ The user resumed this session with new instructions: ${message}`;
754
770
  ${continuationText}`;
755
771
  }
756
772
  const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
757
- writeFileSync2(userPromptFilePath, userPrompt, "utf-8");
773
+ writeFileSync3(userPromptFilePath, userPrompt, "utf-8");
758
774
  if (session.messages && session.messages.length > 0) {
759
775
  await drainMessages(cwd, sessionId, session.messages.length);
760
776
  }
761
- const pluginPath = resolve(import.meta.dirname, "../templates/orchestrator-plugin");
762
- const settingsPath = resolve(import.meta.dirname, "../templates/orchestrator-settings.json");
777
+ const pluginPath = resolve2(import.meta.dirname, "../templates/orchestrator-plugin");
778
+ const settingsPath = resolve2(import.meta.dirname, "../templates/orchestrator-settings.json");
763
779
  const config = loadConfig(cwd);
764
780
  const effort = config.orchestratorEffort ?? "high";
765
781
  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 +784,15 @@ ${continuationText}`;
768
784
  registerPane(paneId, sessionId, "orchestrator");
769
785
  setPaneTitle(paneId, `Sisyphus`);
770
786
  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 = [
787
+ const bannerCmd = resolveBannerCmd();
788
+ const notifyCmd = buildNotifyCmd(paneId);
789
+ const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `orchestrator-run-${cycleNum}`, [
775
790
  "#!/usr/bin/env bash",
776
791
  ...bannerCmd ? [bannerCmd] : [],
777
792
  envExports,
778
- `${claudeCmd}`,
793
+ claudeCmd,
779
794
  notifyCmd
780
- ];
781
- const scriptPath = `${promptsDir(cwd, sessionId)}/orchestrator-run-${cycleNum}.sh`;
782
- writeFileSync2(scriptPath, scriptLines.join("\n"), { mode: 493 });
795
+ ]);
783
796
  sendKeys(paneId, `bash '${scriptPath}'`);
784
797
  await addOrchestratorCycle(cwd, sessionId, {
785
798
  cycle: cycleNum,
@@ -823,30 +836,13 @@ function cleanupSessionMaps(sessionId) {
823
836
  }
824
837
 
825
838
  // 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";
839
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, copyFileSync as copyFileSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync5 } from "fs";
840
+ import { execSync as execSync2 } from "child_process";
841
+ import { resolve as resolve3 } from "path";
828
842
 
829
843
  // 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
- }
844
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync4, readdirSync as readdirSync4, rmSync as rmSync2 } from "fs";
845
+ import { dirname as dirname2, join as join4 } from "path";
850
846
  function loadWorktreeConfig(cwd) {
851
847
  try {
852
848
  const content = readFileSync4(worktreeConfigPath(cwd), "utf-8");
@@ -858,52 +854,52 @@ function loadWorktreeConfig(cwd) {
858
854
  function createWorktreeShell(cwd, sessionId, agentId) {
859
855
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
860
856
  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)}`);
857
+ mkdirSync2(dirname2(worktreePath), { recursive: true });
858
+ execSafe(`git -C ${shellQuote(cwd)} worktree prune`);
859
+ if (existsSync5(worktreePath)) {
860
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove --force ${shellQuote(worktreePath)}`);
861
+ }
862
+ execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
863
+ exec(`git -C ${shellQuote(cwd)} branch ${shellQuote(branchName)} HEAD`);
864
+ exec(`git -C ${shellQuote(cwd)} worktree add ${shellQuote(worktreePath)} ${shellQuote(branchName)}`);
869
865
  return { worktreePath, branchName };
870
866
  }
871
867
  function bootstrapWorktree(cwd, worktreePath, config) {
872
868
  if (config.copy) {
873
869
  for (const entry of config.copy) {
874
870
  const dest = join4(worktreePath, entry);
875
- mkdirSync2(dirname3(dest), { recursive: true });
876
- execSafe2(`cp -r ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
871
+ mkdirSync2(dirname2(dest), { recursive: true });
872
+ execSafe(`cp -r ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
877
873
  }
878
874
  }
879
875
  if (config.clone) {
880
876
  for (const entry of config.clone) {
881
877
  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}`);
878
+ mkdirSync2(dirname2(dest), { recursive: true });
879
+ const src = shellQuote(join4(cwd, entry));
880
+ const dstQ = shellQuote(dest);
881
+ if (execSafe(`cp -Rc ${src} ${dstQ}`) === null) {
882
+ execSafe(`cp -r ${src} ${dstQ}`);
887
883
  }
888
884
  }
889
885
  }
890
886
  if (config.symlink) {
891
887
  for (const entry of config.symlink) {
892
888
  const dest = join4(worktreePath, entry);
893
- mkdirSync2(dirname3(dest), { recursive: true });
894
- execSafe2(`ln -s ${shellQuote2(join4(cwd, entry))} ${shellQuote2(dest)}`);
889
+ mkdirSync2(dirname2(dest), { recursive: true });
890
+ execSafe(`ln -s ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
895
891
  }
896
892
  }
897
893
  if (config.init) {
898
894
  try {
899
- exec2(config.init, worktreePath);
895
+ exec(config.init, worktreePath);
900
896
  } catch (err) {
901
897
  console.error(`[sisyphus] worktree init command failed: ${err instanceof Error ? err.message : err}`);
902
898
  }
903
899
  }
904
900
  }
905
901
  function resolveWorktreeBranch(cwd, worktreePath) {
906
- const output = execSafe2(`git -C ${shellQuote2(cwd)} worktree list --porcelain`);
902
+ const output = execSafe(`git -C ${shellQuote(cwd)} worktree list --porcelain`);
907
903
  if (!output) return null;
908
904
  const lines = output.split("\n");
909
905
  for (let i = 0; i < lines.length; i++) {
@@ -925,30 +921,30 @@ function mergeWorktrees(cwd, agents) {
925
921
  (a) => a.worktreePath && a.mergeStatus === "pending"
926
922
  );
927
923
  const results = [];
928
- execSafe2(`git -C ${shellQuote2(cwd)} add .sisyphus`);
929
- execSafe2(`git -C ${shellQuote2(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
924
+ execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
925
+ execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
930
926
  for (const agent of pending) {
931
927
  const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
932
928
  if (!branch) {
933
929
  results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
934
- execSafe2(`git -C ${shellQuote2(cwd)} worktree remove ${shellQuote2(agent.worktreePath)} --force`);
930
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
935
931
  continue;
936
932
  }
937
- const aheadLog = execSafe2(`git -C ${shellQuote2(cwd)} log HEAD..${shellQuote2(branch)} --oneline`);
933
+ const aheadLog = execSafe(`git -C ${shellQuote(cwd)} log HEAD..${shellQuote(branch)} --oneline`);
938
934
  if (!aheadLog) {
939
935
  results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
940
936
  cleanupWorktree(cwd, agent.worktreePath, branch);
941
937
  continue;
942
938
  }
943
939
  const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
944
- const mergeCmd = `git -C ${shellQuote2(cwd)} merge --no-ff ${shellQuote2(branch)} -m ${shellQuote2(mergeMsg)}`;
940
+ const mergeCmd = `git -C ${shellQuote(cwd)} merge --no-ff ${shellQuote(branch)} -m ${shellQuote(mergeMsg)}`;
945
941
  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)}`);
942
+ exec(mergeCmd);
943
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)}`);
944
+ execSafe(`git -C ${shellQuote(cwd)} branch -d ${shellQuote(branch)}`);
949
945
  results.push({ agentId: agent.id, name: agent.name, status: "merged" });
950
946
  } catch (err) {
951
- execSafe2(`git -C ${shellQuote2(cwd)} merge --abort`);
947
+ execSafe(`git -C ${shellQuote(cwd)} merge --abort`);
952
948
  const errObj = err;
953
949
  const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
954
950
  const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
@@ -959,9 +955,9 @@ function mergeWorktrees(cwd, agents) {
959
955
  return results;
960
956
  }
961
957
  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);
958
+ execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(worktreePath)} --force`);
959
+ execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
960
+ const baseDir = dirname2(worktreePath);
965
961
  try {
966
962
  const entries = readdirSync4(baseDir);
967
963
  if (entries.length === 0) {
@@ -1023,7 +1019,7 @@ function clearAgentCounter(sessionId) {
1023
1019
  agentCounters.delete(sessionId);
1024
1020
  }
1025
1021
  function renderAgentSuffix(sessionId, instruction, worktreeContext) {
1026
- const templatePath = resolve2(import.meta.dirname, "../templates/agent-suffix.md");
1022
+ const templatePath = resolve3(import.meta.dirname, "../templates/agent-suffix.md");
1027
1023
  let template;
1028
1024
  try {
1029
1025
  template = readFileSync5(templatePath, "utf-8");
@@ -1047,7 +1043,7 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1047
1043
  mkdirSync3(`${base}/.claude-plugin`, { recursive: true });
1048
1044
  mkdirSync3(`${base}/agents`, { recursive: true });
1049
1045
  mkdirSync3(`${base}/hooks`, { recursive: true });
1050
- writeFileSync3(
1046
+ writeFileSync4(
1051
1047
  `${base}/.claude-plugin/plugin.json`,
1052
1048
  JSON.stringify({ name: `sisyphus-agent-${agentId}`, version: "1.0.0" }),
1053
1049
  "utf-8"
@@ -1056,34 +1052,35 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
1056
1052
  const shortName = agentType.replace(/^sisyphus:/, "");
1057
1053
  copyFileSync2(agentConfig.filePath, `${base}/agents/${shortName}.md`);
1058
1054
  }
1059
- const srcHooks = resolve2(import.meta.dirname, "../templates/agent-plugin/hooks");
1060
- for (const f of ["hooks.json", "require-submit.sh", "intercept-send-message.sh"]) {
1055
+ const srcHooks = resolve3(import.meta.dirname, "../templates/agent-plugin/hooks");
1056
+ for (const f of ["require-submit.sh", "intercept-send-message.sh"]) {
1061
1057
  copyFileSync2(`${srcHooks}/${f}`, `${base}/hooks/${f}`);
1062
1058
  }
1059
+ const hooksConfig = {
1060
+ PreToolUse: [
1061
+ { matcher: "SendMessage", hook: { type: "command", command: "bash hooks/intercept-send-message.sh" } }
1062
+ ],
1063
+ Stop: [
1064
+ { hook: { type: "command", command: "bash hooks/require-submit.sh" } }
1065
+ ]
1066
+ };
1067
+ const normalizedType = agentType?.replace(/^sisyphus:/, "") ?? "";
1068
+ if (normalizedType === "plan") {
1069
+ hooksConfig.UserPromptSubmit = [
1070
+ { hook: { type: "command", command: "bash hooks/plan-user-prompt.sh" } }
1071
+ ];
1072
+ copyFileSync2(`${srcHooks}/plan-user-prompt.sh`, `${base}/hooks/plan-user-prompt.sh`);
1073
+ } else if (normalizedType === "spec-draft") {
1074
+ hooksConfig.UserPromptSubmit = [
1075
+ { hook: { type: "command", command: "bash hooks/spec-user-prompt.sh" } }
1076
+ ];
1077
+ copyFileSync2(`${srcHooks}/spec-user-prompt.sh`, `${base}/hooks/spec-user-prompt.sh`);
1078
+ }
1079
+ writeFileSync4(`${base}/hooks/hooks.json`, JSON.stringify({ hooks: hooksConfig }, null, 2), "utf-8");
1063
1080
  return base;
1064
1081
  }
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
- }
1082
+ function setupAgentPane(opts) {
1083
+ const { sessionId, cwd, agentId, agentType, name, instruction, windowId, color, provider, agentConfig, worktreeContext, paneCwd } = opts;
1087
1084
  const paneId = createPane(windowId, paneCwd);
1088
1085
  registerPane(paneId, sessionId, "agent", agentId);
1089
1086
  const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
@@ -1092,50 +1089,88 @@ async function spawnAgent(opts) {
1092
1089
  setPaneStyle(paneId, color);
1093
1090
  const suffix = renderAgentSuffix(sessionId, instruction, worktreeContext);
1094
1091
  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 = [
1092
+ writeFileSync4(suffixFilePath, suffix, "utf-8");
1093
+ const bannerCmd = resolveBannerCmd();
1094
+ const npmBinDir = resolveNpmBinDir();
1095
+ const envExports = buildEnvExports([
1101
1096
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
1102
1097
  `export SISYPHUS_AGENT_ID='${agentId}'`,
1103
1098
  `export SISYPHUS_CWD='${cwd}'`,
1104
1099
  ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1105
1100
  `export PATH="${npmBinDir}:$PATH"`
1106
- ].join(" && ");
1107
- const notifyCmd = `node "${cliBin}" notify pane-exited --pane-id ${paneId}`;
1101
+ ]);
1102
+ const notifyCmd = buildNotifyCmd(paneId);
1108
1103
  let mainCmd;
1109
1104
  if (provider === "openai") {
1110
1105
  const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
1111
1106
  const parts = [];
1112
- if (agentConfig?.body) {
1113
- parts.push(agentConfig.body);
1114
- }
1107
+ if (agentConfig?.body) parts.push(agentConfig.body);
1115
1108
  parts.push(suffix);
1116
1109
  parts.push(`## Task
1117
1110
 
1118
1111
  ${instruction}`);
1119
- writeFileSync3(codexPromptPath, parts.join("\n\n"), "utf-8");
1112
+ writeFileSync4(codexPromptPath, parts.join("\n\n"), "utf-8");
1120
1113
  const model = agentConfig?.frontmatter.model ?? "codex-mini";
1121
- mainCmd = `codex -m ${shellQuote3(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
1114
+ mainCmd = `codex -m ${shellQuote(model)} --dangerously-bypass-approvals-and-sandbox "$(cat '${codexPromptPath}')"`;
1122
1115
  } else {
1123
- const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote3(agentType)}` : "";
1116
+ const agentFlag = agentType && agentType !== "worker" ? ` --agent ${shellQuote(agentType)}` : "";
1124
1117
  const config = loadConfig(cwd);
1125
1118
  const effort = agentConfig?.frontmatter.effort ?? config.agentEffort ?? "medium";
1126
1119
  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)}`;
1120
+ mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag} --name ${shellQuote(`sisyphus:${name}`)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote(instruction)}`;
1128
1121
  }
1129
- const scriptLines = [
1122
+ const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
1130
1123
  "#!/usr/bin/env bash",
1131
- ...bannerCmd ? [bannerCmd.replace(/ &&$/, "")] : [],
1124
+ ...bannerCmd ? [bannerCmd] : [],
1132
1125
  envExports,
1133
1126
  mainCmd,
1134
1127
  notifyCmd
1135
- ];
1136
- const scriptPath = `${promptsDir(cwd, sessionId)}/${agentId}-run.sh`;
1137
- writeFileSync3(scriptPath, scriptLines.join("\n"), { mode: 493 });
1128
+ ]);
1138
1129
  const fullCmd = `bash '${scriptPath}'`;
1130
+ return { paneId, fullCmd };
1131
+ }
1132
+ async function spawnAgent(opts) {
1133
+ const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
1134
+ const count = (agentCounters.get(sessionId) ?? 0) + 1;
1135
+ agentCounters.set(sessionId, count);
1136
+ const agentId = `agent-${String(count).padStart(3, "0")}`;
1137
+ const bundledPluginPath = resolve3(import.meta.dirname, "../templates/agent-plugin");
1138
+ const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1139
+ const provider = detectProvider(agentConfig?.frontmatter.model);
1140
+ const color = (agentConfig?.frontmatter.color ? normalizeTmuxColor(agentConfig.frontmatter.color) : null) ?? getNextColor(sessionId);
1141
+ const cliToCheck = provider === "openai" ? "codex" : "claude";
1142
+ try {
1143
+ execSync2(`which ${cliToCheck}`, { stdio: "pipe", env: execEnv() });
1144
+ } catch {
1145
+ throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sisyphus doctor\` to diagnose.`);
1146
+ }
1147
+ let paneCwd = cwd;
1148
+ let worktreePath;
1149
+ let branchName;
1150
+ let worktreeContext;
1151
+ if (opts.worktree) {
1152
+ const wt = createWorktreeShell(cwd, sessionId, agentId);
1153
+ worktreePath = wt.worktreePath;
1154
+ branchName = wt.branchName;
1155
+ paneCwd = worktreePath;
1156
+ const session = getSession(cwd, sessionId);
1157
+ const portOffset = countWorktreeAgents(session.agents) + 1;
1158
+ worktreeContext = { offset: portOffset, total: portOffset, branchName };
1159
+ }
1160
+ const { paneId, fullCmd } = setupAgentPane({
1161
+ sessionId,
1162
+ cwd,
1163
+ agentId,
1164
+ agentType,
1165
+ name,
1166
+ instruction,
1167
+ windowId,
1168
+ color,
1169
+ provider,
1170
+ agentConfig,
1171
+ worktreeContext,
1172
+ paneCwd
1173
+ });
1139
1174
  const agent = {
1140
1175
  id: agentId,
1141
1176
  name,
@@ -1187,7 +1222,7 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1187
1222
  });
1188
1223
  }
1189
1224
  const { instruction, agentType, name, color } = agent;
1190
- const bundledPluginPath = resolve2(import.meta.dirname, "../templates/agent-plugin");
1225
+ const bundledPluginPath = resolve3(import.meta.dirname, "../templates/agent-plugin");
1191
1226
  const agentConfig = resolveAgentConfig(agentType, bundledPluginPath, cwd);
1192
1227
  const provider = detectProvider(agentConfig?.frontmatter.model);
1193
1228
  let paneCwd = cwd;
@@ -1208,56 +1243,20 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1208
1243
  }
1209
1244
  unregisterAgentPane(sessionId, agentId);
1210
1245
  }
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}'`;
1246
+ const { paneId, fullCmd } = setupAgentPane({
1247
+ sessionId,
1248
+ cwd,
1249
+ agentId,
1250
+ agentType,
1251
+ name,
1252
+ instruction,
1253
+ windowId,
1254
+ color,
1255
+ provider,
1256
+ agentConfig,
1257
+ worktreeContext,
1258
+ paneCwd
1259
+ });
1261
1260
  await updateAgent(cwd, sessionId, agentId, {
1262
1261
  status: "running",
1263
1262
  paneId,
@@ -1282,7 +1281,7 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
1282
1281
  mkdirSync3(dir, { recursive: true });
1283
1282
  const num = nextReportNumber(cwd, sessionId, agentId);
1284
1283
  const filePath = reportFilePath(cwd, sessionId, agentId, num);
1285
- writeFileSync3(filePath, content, "utf-8");
1284
+ writeFileSync4(filePath, content, "utf-8");
1286
1285
  const entry = {
1287
1286
  type: "update",
1288
1287
  filePath,
@@ -1301,7 +1300,7 @@ async function handleAgentSubmit(cwd, sessionId, agentId, report) {
1301
1300
  const dir = reportsDir(cwd, sessionId);
1302
1301
  mkdirSync3(dir, { recursive: true });
1303
1302
  const filePath = reportFilePath(cwd, sessionId, agentId, "final");
1304
- writeFileSync3(filePath, report, "utf-8");
1303
+ writeFileSync4(filePath, report, "utf-8");
1305
1304
  const entry = {
1306
1305
  type: "final",
1307
1306
  filePath,
@@ -1344,9 +1343,6 @@ function allAgentsDone(session) {
1344
1343
  const running = session.agents.filter((a) => a.status === "running");
1345
1344
  return running.length === 0 && session.agents.length > 0;
1346
1345
  }
1347
- function shellQuote3(s) {
1348
- return `'${s.replace(/'/g, "'\\''")}'`;
1349
- }
1350
1346
 
1351
1347
  // src/daemon/pane-monitor.ts
1352
1348
  var monitorInterval = null;
@@ -1491,7 +1487,7 @@ var PRUNE_KEEP_DAYS = 7;
1491
1487
  function pruneOldSessions(cwd) {
1492
1488
  try {
1493
1489
  const dir = sessionsDir(cwd);
1494
- if (!existsSync6(dir)) return;
1490
+ if (!existsSync7(dir)) return;
1495
1491
  const entries = readdirSync6(dir, { withFileTypes: true });
1496
1492
  const candidates = [];
1497
1493
  for (const entry of entries) {
@@ -1521,6 +1517,17 @@ function pruneOldSessions(cwd) {
1521
1517
  console.error("[sisyphus] Session pruning failed:", err);
1522
1518
  }
1523
1519
  }
1520
+ async function reopenWindow(sessionId, cwd) {
1521
+ const session = getSession(cwd, sessionId);
1522
+ const tmuxName = session.tmuxSessionName ?? `sisyphus-${session.name ?? sessionId.slice(0, 8)}`;
1523
+ if (sessionExists(tmuxName) && session.tmuxWindowId) {
1524
+ return { tmuxSessionName: tmuxName, tmuxWindowId: session.tmuxWindowId };
1525
+ }
1526
+ const created = createSession2(tmuxName, "main", cwd);
1527
+ setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
1528
+ await updateSessionTmux(cwd, sessionId, tmuxName, created.windowId);
1529
+ return { tmuxSessionName: tmuxName, tmuxWindowId: created.windowId };
1530
+ }
1524
1531
  async function resumeSession(sessionId, cwd, message) {
1525
1532
  const session = getSession(cwd, sessionId);
1526
1533
  const tmuxName = session.tmuxSessionName ?? `sisyphus-${sessionId.slice(0, 8)}`;
@@ -1573,7 +1580,7 @@ function getSessionStatus(cwd, sessionId) {
1573
1580
  }
1574
1581
  function listSessions(cwd) {
1575
1582
  const dir = sessionsDir(cwd);
1576
- if (!existsSync6(dir)) return [];
1583
+ if (!existsSync7(dir)) return [];
1577
1584
  const entries = readdirSync6(dir, { withFileTypes: true });
1578
1585
  const sessions = [];
1579
1586
  for (const entry of entries) {
@@ -1853,10 +1860,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1853
1860
 
1854
1861
  // src/daemon/server.ts
1855
1862
  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();
1863
+ var sessionTrackingMap = /* @__PURE__ */ new Map();
1860
1864
  function registryPath() {
1861
1865
  return join5(globalDir(), "session-registry.json");
1862
1866
  }
@@ -1864,14 +1868,14 @@ function persistSessionRegistry() {
1864
1868
  const dir = globalDir();
1865
1869
  mkdirSync4(dir, { recursive: true });
1866
1870
  const registry = {};
1867
- for (const [id, cwd] of sessionCwdMap) {
1868
- registry[id] = cwd;
1871
+ for (const [id, tracking] of sessionTrackingMap) {
1872
+ registry[id] = tracking.cwd;
1869
1873
  }
1870
- writeFileSync4(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
1874
+ writeFileSync5(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
1871
1875
  }
1872
1876
  function loadSessionRegistry() {
1873
1877
  const p = registryPath();
1874
- if (!existsSync7(p)) return {};
1878
+ if (!existsSync8(p)) return {};
1875
1879
  try {
1876
1880
  return JSON.parse(readFileSync6(p, "utf-8"));
1877
1881
  } catch {
@@ -1879,65 +1883,81 @@ function loadSessionRegistry() {
1879
1883
  }
1880
1884
  }
1881
1885
  function registerSessionCwd(sessionId, cwd) {
1882
- sessionCwdMap.set(sessionId, cwd);
1886
+ const existing = sessionTrackingMap.get(sessionId);
1887
+ if (existing) {
1888
+ existing.cwd = cwd;
1889
+ } else {
1890
+ sessionTrackingMap.set(sessionId, { cwd, messageCounter: 0 });
1891
+ }
1883
1892
  persistSessionRegistry();
1884
1893
  }
1885
1894
  function registerSessionTmux(sessionId, tmuxSession, windowId) {
1886
- sessionTmuxMap.set(sessionId, tmuxSession);
1887
- sessionWindowMap2.set(sessionId, windowId);
1895
+ const existing = sessionTrackingMap.get(sessionId);
1896
+ if (existing) {
1897
+ existing.tmuxSession = tmuxSession;
1898
+ existing.windowId = windowId;
1899
+ } else {
1900
+ sessionTrackingMap.set(sessionId, { cwd: "", tmuxSession, windowId, messageCounter: 0 });
1901
+ }
1902
+ }
1903
+ function unknownSessionError(sessionId) {
1904
+ return { ok: false, error: `Unknown session: ${sessionId}. Run \`sisyphus list --all\` to see available sessions.` };
1888
1905
  }
1889
1906
  async function handleRequest(req) {
1890
1907
  try {
1891
1908
  switch (req.type) {
1892
1909
  case "start": {
1893
1910
  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);
1911
+ sessionTrackingMap.set(session.id, {
1912
+ cwd: req.cwd,
1913
+ tmuxSession: session.tmuxSessionName,
1914
+ windowId: session.tmuxWindowId,
1915
+ messageCounter: 0
1916
+ });
1917
+ persistSessionRegistry();
1897
1918
  return { ok: true, data: { sessionId: session.id, tmuxSessionName: session.tmuxSessionName } };
1898
1919
  }
1899
1920
  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);
1921
+ const tracking = sessionTrackingMap.get(req.sessionId);
1922
+ if (!tracking) return unknownSessionError(req.sessionId);
1923
+ const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.worktree);
1903
1924
  return { ok: true, data: { agentId: result.agentId } };
1904
1925
  }
1905
1926
  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);
1927
+ const tracking = sessionTrackingMap.get(req.sessionId);
1928
+ if (!tracking) return unknownSessionError(req.sessionId);
1929
+ if (!tracking.windowId) return { ok: false, error: `No tmux window found for session: ${req.sessionId}` };
1930
+ await handleSubmit(tracking.cwd, req.sessionId, req.agentId, req.report, tracking.windowId);
1911
1931
  return { ok: true };
1912
1932
  }
1913
1933
  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);
1934
+ const tracking = sessionTrackingMap.get(req.sessionId);
1935
+ if (!tracking) return unknownSessionError(req.sessionId);
1936
+ await handleReport(tracking.cwd, req.sessionId, req.agentId, req.content);
1917
1937
  return { ok: true };
1918
1938
  }
1919
1939
  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);
1940
+ const tracking = sessionTrackingMap.get(req.sessionId);
1941
+ if (!tracking) return unknownSessionError(req.sessionId);
1942
+ await handleYield(req.sessionId, tracking.cwd, req.nextPrompt, req.mode);
1923
1943
  return { ok: true };
1924
1944
  }
1925
1945
  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);
1946
+ const tracking = sessionTrackingMap.get(req.sessionId);
1947
+ if (!tracking) return unknownSessionError(req.sessionId);
1948
+ await handleComplete(req.sessionId, tracking.cwd, req.report);
1929
1949
  return { ok: true };
1930
1950
  }
1931
1951
  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);
1952
+ const tracking = sessionTrackingMap.get(req.sessionId);
1953
+ if (!tracking) return unknownSessionError(req.sessionId);
1954
+ await handleContinue(req.sessionId, tracking.cwd);
1935
1955
  return { ok: true };
1936
1956
  }
1937
1957
  case "status": {
1938
1958
  if (req.sessionId) {
1939
- const cwd = sessionCwdMap.get(req.sessionId) ?? req.cwd;
1940
- if (!cwd) return { ok: false, error: `Unknown session: ${req.sessionId}` };
1959
+ const cwd = sessionTrackingMap.get(req.sessionId)?.cwd ?? req.cwd;
1960
+ if (!cwd) return unknownSessionError(req.sessionId);
1941
1961
  const session = getSessionStatus(cwd, req.sessionId);
1942
1962
  return { ok: true, data: { session } };
1943
1963
  }
@@ -1947,21 +1967,21 @@ async function handleRequest(req) {
1947
1967
  const allSessions = [];
1948
1968
  if (req.all) {
1949
1969
  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 })));
1970
+ for (const tracking of sessionTrackingMap.values()) {
1971
+ if (seenCwds.has(tracking.cwd)) continue;
1972
+ seenCwds.add(tracking.cwd);
1973
+ const sessions = listSessions(tracking.cwd);
1974
+ allSessions.push(...sessions.map((s) => ({ ...s, cwd: tracking.cwd })));
1955
1975
  }
1956
1976
  } else {
1957
1977
  const sessions = listSessions(req.cwd);
1958
1978
  allSessions.push(...sessions.map((s) => ({ ...s, cwd: req.cwd })));
1959
1979
  let totalCount = allSessions.length;
1960
1980
  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;
1981
+ for (const tracking of sessionTrackingMap.values()) {
1982
+ if (seenCwds.has(tracking.cwd)) continue;
1983
+ seenCwds.add(tracking.cwd);
1984
+ totalCount += listSessions(tracking.cwd).length;
1965
1985
  }
1966
1986
  if (totalCount > allSessions.length) {
1967
1987
  return { ok: true, data: { sessions: allSessions, totalCount, filtered: true } };
@@ -1970,114 +1990,126 @@ async function handleRequest(req) {
1970
1990
  return { ok: true, data: { sessions: allSessions } };
1971
1991
  }
1972
1992
  case "resume": {
1973
- let cwd = sessionCwdMap.get(req.sessionId);
1974
- if (!cwd) {
1993
+ let tracking = sessionTrackingMap.get(req.sessionId);
1994
+ if (!tracking) {
1975
1995
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
1976
- if (existsSync7(stateFile)) {
1977
- cwd = req.cwd;
1978
- registerSessionCwd(req.sessionId, cwd);
1996
+ if (existsSync8(stateFile)) {
1997
+ tracking = { cwd: req.cwd, messageCounter: 0 };
1998
+ sessionTrackingMap.set(req.sessionId, tracking);
1999
+ persistSessionRegistry();
1979
2000
  } else {
1980
- return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}` };
2001
+ return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}. Run \`sisyphus list --all\` to see available sessions.` };
1981
2002
  }
1982
2003
  }
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);
2004
+ const session = await resumeSession(req.sessionId, tracking.cwd, req.message);
2005
+ if (session.tmuxSessionName) tracking.tmuxSession = session.tmuxSessionName;
2006
+ if (session.tmuxWindowId) tracking.windowId = session.tmuxWindowId;
1986
2007
  return { ok: true, data: { sessionId: session.id, status: session.status, tmuxSessionName: session.tmuxSessionName } };
1987
2008
  }
1988
2009
  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);
2010
+ const tracking = sessionTrackingMap.get(req.sessionId);
2011
+ if (!tracking) return unknownSessionError(req.sessionId);
2012
+ await handleRegisterClaudeSession(tracking.cwd, req.sessionId, req.agentId, req.claudeSessionId);
1992
2013
  return { ok: true };
1993
2014
  }
1994
2015
  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);
2016
+ const tracking = sessionTrackingMap.get(req.sessionId);
2017
+ if (!tracking) return unknownSessionError(req.sessionId);
2018
+ const killedAgents = await handleKill(req.sessionId, tracking.cwd);
2019
+ sessionTrackingMap.delete(req.sessionId);
2001
2020
  persistSessionRegistry();
2002
2021
  return { ok: true, data: { killedAgents, sessionId: req.sessionId } };
2003
2022
  }
2004
2023
  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);
2024
+ const tracking = sessionTrackingMap.get(req.sessionId);
2025
+ if (!tracking) return unknownSessionError(req.sessionId);
2026
+ await handleKillAgent(req.sessionId, tracking.cwd, req.agentId);
2008
2027
  return { ok: true, data: { agentId: req.agentId } };
2009
2028
  }
2010
2029
  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);
2030
+ const tracking = sessionTrackingMap.get(req.sessionId);
2031
+ if (!tracking) return unknownSessionError(req.sessionId);
2032
+ await handleRestartAgent(req.sessionId, tracking.cwd, req.agentId);
2014
2033
  return { ok: true, data: { agentId: req.agentId } };
2015
2034
  }
2016
2035
  case "rollback": {
2017
- let cwd = sessionCwdMap.get(req.sessionId);
2018
- if (!cwd) {
2036
+ let tracking = sessionTrackingMap.get(req.sessionId);
2037
+ if (!tracking) {
2038
+ const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
2039
+ if (existsSync8(stateFile)) {
2040
+ registerSessionCwd(req.sessionId, req.cwd);
2041
+ tracking = sessionTrackingMap.get(req.sessionId);
2042
+ } else {
2043
+ return unknownSessionError(req.sessionId);
2044
+ }
2045
+ }
2046
+ const result = await handleRollback(req.sessionId, tracking.cwd, req.toCycle);
2047
+ return { ok: true, data: result };
2048
+ }
2049
+ case "reopen-window": {
2050
+ let tracking = sessionTrackingMap.get(req.sessionId);
2051
+ if (!tracking) {
2019
2052
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
2020
- if (existsSync7(stateFile)) {
2021
- cwd = req.cwd;
2022
- registerSessionCwd(req.sessionId, cwd);
2053
+ if (existsSync8(stateFile)) {
2054
+ tracking = { cwd: req.cwd, messageCounter: 0 };
2055
+ sessionTrackingMap.set(req.sessionId, tracking);
2056
+ persistSessionRegistry();
2023
2057
  } else {
2024
- return { ok: false, error: `Unknown session: ${req.sessionId}` };
2058
+ return unknownSessionError(req.sessionId);
2025
2059
  }
2026
2060
  }
2027
- const result = await handleRollback(req.sessionId, cwd, req.toCycle);
2061
+ const result = await reopenWindow(req.sessionId, tracking.cwd);
2062
+ tracking.tmuxSession = result.tmuxSessionName;
2063
+ tracking.windowId = result.tmuxWindowId;
2028
2064
  return { ok: true, data: result };
2029
2065
  }
2030
2066
  case "delete": {
2031
- const activeCwd = sessionCwdMap.get(req.sessionId);
2032
- if (activeCwd) {
2067
+ const activeTracking = sessionTrackingMap.get(req.sessionId);
2068
+ if (activeTracking) {
2033
2069
  try {
2034
- await handleKill(req.sessionId, activeCwd);
2070
+ await handleKill(req.sessionId, activeTracking.cwd);
2035
2071
  } catch {
2036
2072
  }
2037
- sessionCwdMap.delete(req.sessionId);
2038
- sessionTmuxMap.delete(req.sessionId);
2039
- sessionWindowMap2.delete(req.sessionId);
2040
- sessionMessageCounters.delete(req.sessionId);
2073
+ sessionTrackingMap.delete(req.sessionId);
2041
2074
  persistSessionRegistry();
2042
2075
  }
2043
- const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
2076
+ const { sessionDir: sessionDir2 } = await import("./paths-NUUALUVP.js");
2044
2077
  rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
2045
2078
  return { ok: true };
2046
2079
  }
2047
2080
  case "pane-exited": {
2048
2081
  const entry = lookupPane(req.paneId);
2049
2082
  if (!entry) return { ok: true };
2050
- const cwd = sessionCwdMap.get(entry.sessionId);
2051
- if (!cwd) {
2083
+ const tracking = sessionTrackingMap.get(entry.sessionId);
2084
+ if (!tracking) {
2052
2085
  unregisterPane(req.paneId);
2053
2086
  return { ok: true };
2054
2087
  }
2055
2088
  unregisterPane(req.paneId);
2056
- await handlePaneExited(req.paneId, cwd, entry.sessionId, entry.role, entry.agentId);
2089
+ await handlePaneExited(req.paneId, tracking.cwd, entry.sessionId, entry.role, entry.agentId);
2057
2090
  return { ok: true };
2058
2091
  }
2059
2092
  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);
2093
+ const tracking = sessionTrackingMap.get(req.sessionId);
2094
+ if (!tracking) return unknownSessionError(req.sessionId);
2095
+ await updateTask(tracking.cwd, req.sessionId, req.task);
2063
2096
  return { ok: true };
2064
2097
  }
2065
2098
  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")}`;
2099
+ const tracking = sessionTrackingMap.get(req.sessionId);
2100
+ if (!tracking) return unknownSessionError(req.sessionId);
2101
+ tracking.messageCounter += 1;
2102
+ const id = `msg-${String(tracking.messageCounter).padStart(3, "0")}`;
2071
2103
  const source = req.source ?? { type: "user" };
2072
2104
  const summary = req.content.length > 200 ? req.content.slice(0, 200) + "..." : req.content;
2073
2105
  let filePath;
2074
2106
  if (req.content.length > 200) {
2075
- const dir = messagesDir(cwd, req.sessionId);
2107
+ const dir = messagesDir(tracking.cwd, req.sessionId);
2076
2108
  mkdirSync4(dir, { recursive: true });
2077
2109
  filePath = join5(dir, `${id}.md`);
2078
- writeFileSync4(filePath, req.content, "utf-8");
2110
+ writeFileSync5(filePath, req.content, "utf-8");
2079
2111
  }
2080
- await appendMessage(cwd, req.sessionId, {
2112
+ await appendMessage(tracking.cwd, req.sessionId, {
2081
2113
  id,
2082
2114
  source,
2083
2115
  content: req.content,
@@ -2096,9 +2128,9 @@ async function handleRequest(req) {
2096
2128
  }
2097
2129
  }
2098
2130
  function startServer() {
2099
- return new Promise((resolve4, reject) => {
2131
+ return new Promise((resolve5, reject) => {
2100
2132
  const sock = socketPath();
2101
- if (existsSync7(sock)) {
2133
+ if (existsSync8(sock)) {
2102
2134
  unlinkSync(sock);
2103
2135
  }
2104
2136
  server = createServer((conn) => {
@@ -2128,31 +2160,31 @@ function startServer() {
2128
2160
  server.on("error", reject);
2129
2161
  server.listen(sock, () => {
2130
2162
  console.log(`[sisyphus] Daemon listening on ${sock}`);
2131
- resolve4(server);
2163
+ resolve5(server);
2132
2164
  });
2133
2165
  });
2134
2166
  }
2135
2167
  function stopServer() {
2136
- return new Promise((resolve4) => {
2168
+ return new Promise((resolve5) => {
2137
2169
  if (!server) {
2138
- resolve4();
2170
+ resolve5();
2139
2171
  return;
2140
2172
  }
2141
2173
  server.close(() => {
2142
2174
  const sock = socketPath();
2143
- if (existsSync7(sock)) {
2175
+ if (existsSync8(sock)) {
2144
2176
  unlinkSync(sock);
2145
2177
  }
2146
2178
  server = null;
2147
- resolve4();
2179
+ resolve5();
2148
2180
  });
2149
2181
  });
2150
2182
  }
2151
2183
 
2152
2184
  // src/daemon/updater.ts
2153
2185
  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";
2186
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, lstatSync } from "fs";
2187
+ import { resolve as resolve4 } from "path";
2156
2188
  import { get } from "https";
2157
2189
  function isNewer(latest, current) {
2158
2190
  const a = latest.split(".").map(Number);
@@ -2168,7 +2200,7 @@ function isNewer(latest, current) {
2168
2200
  function readPackageVersion() {
2169
2201
  for (const rel of ["../package.json", "../../package.json"]) {
2170
2202
  try {
2171
- const raw = readFileSync7(resolve3(import.meta.dirname, rel), "utf-8");
2203
+ const raw = readFileSync7(resolve4(import.meta.dirname, rel), "utf-8");
2172
2204
  const pkg = JSON.parse(raw);
2173
2205
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
2174
2206
  } catch {
@@ -2178,9 +2210,9 @@ function readPackageVersion() {
2178
2210
  }
2179
2211
  var currentVersion = readPackageVersion();
2180
2212
  function checkForUpdate() {
2181
- return new Promise((resolve4) => {
2213
+ return new Promise((resolve5) => {
2182
2214
  const timeout = setTimeout(() => {
2183
- resolve4(null);
2215
+ resolve5(null);
2184
2216
  }, 5e3);
2185
2217
  const req = get("https://registry.npmjs.org/sisyphi/latest", (res) => {
2186
2218
  let data = "";
@@ -2192,24 +2224,24 @@ function checkForUpdate() {
2192
2224
  try {
2193
2225
  const { version: latest } = JSON.parse(data);
2194
2226
  if (latest && isNewer(latest, currentVersion)) {
2195
- resolve4({ current: currentVersion, latest });
2227
+ resolve5({ current: currentVersion, latest });
2196
2228
  } else {
2197
- resolve4(null);
2229
+ resolve5(null);
2198
2230
  }
2199
2231
  } catch {
2200
- resolve4(null);
2232
+ resolve5(null);
2201
2233
  }
2202
2234
  });
2203
2235
  });
2204
2236
  req.on("error", () => {
2205
2237
  clearTimeout(timeout);
2206
- resolve4(null);
2238
+ resolve5(null);
2207
2239
  });
2208
2240
  });
2209
2241
  }
2210
2242
  function applyUpdate(expectedVersion) {
2211
2243
  try {
2212
- const nodeDir = resolve3(process.execPath, "..");
2244
+ const nodeDir = resolve4(process.execPath, "..");
2213
2245
  const env = { ...process.env, PATH: `${nodeDir}:${process.env.PATH ?? ""}` };
2214
2246
  execSync3("npm install -g sisyphi", { timeout: 15e3, stdio: "pipe", env });
2215
2247
  const result = execSync3("npm ls -g sisyphi --json --depth=0", {
@@ -2231,7 +2263,7 @@ function applyUpdate(expectedVersion) {
2231
2263
  }
2232
2264
  function markUpdating(version) {
2233
2265
  try {
2234
- writeFileSync5(daemonUpdatingPath(), version, "utf-8");
2266
+ writeFileSync6(daemonUpdatingPath(), version, "utf-8");
2235
2267
  } catch {
2236
2268
  }
2237
2269
  }
@@ -2241,8 +2273,19 @@ function clearUpdating() {
2241
2273
  } catch {
2242
2274
  }
2243
2275
  }
2276
+ function isLinkedInstall() {
2277
+ try {
2278
+ const nodeDir = resolve4(process.execPath, "..");
2279
+ const globalPrefix = execSync3("npm prefix -g", { timeout: 5e3, encoding: "utf-8", env: { ...process.env, PATH: `${nodeDir}:${process.env.PATH ?? ""}` } }).trim();
2280
+ const globalPkgDir = resolve4(globalPrefix, "lib", "node_modules", "sisyphi");
2281
+ return lstatSync(globalPkgDir).isSymbolicLink();
2282
+ } catch {
2283
+ return false;
2284
+ }
2285
+ }
2244
2286
  async function checkAndApply() {
2245
2287
  clearUpdating();
2288
+ if (isLinkedInstall()) return;
2246
2289
  try {
2247
2290
  const update = await checkForUpdate();
2248
2291
  if (!update) return;
@@ -2261,6 +2304,11 @@ async function checkAndApply() {
2261
2304
  }
2262
2305
 
2263
2306
  // src/daemon/index.ts
2307
+ var nodeVersion = parseInt(process.versions.node.split(".")[0], 10);
2308
+ if (nodeVersion < 22) {
2309
+ console.error(`[sisyphus] Node.js v22+ required (current: v${process.versions.node})`);
2310
+ process.exit(1);
2311
+ }
2264
2312
  var ts = () => (/* @__PURE__ */ new Date()).toISOString();
2265
2313
  var origLog = console.log.bind(console);
2266
2314
  var origError = console.error.bind(console);
@@ -2292,7 +2340,7 @@ function acquirePidLock() {
2292
2340
  console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
2293
2341
  process.exit(0);
2294
2342
  }
2295
- writeFileSync6(daemonPidPath(), String(process.pid), "utf-8");
2343
+ writeFileSync7(daemonPidPath(), String(process.pid), "utf-8");
2296
2344
  }
2297
2345
  function isLaunchdManaged() {
2298
2346
  try {
@@ -2351,7 +2399,7 @@ async function recoverSessions() {
2351
2399
  let recovered = 0;
2352
2400
  for (const [sessionId, cwd] of entries) {
2353
2401
  const stateFile = statePath(cwd, sessionId);
2354
- if (!existsSync8(stateFile)) {
2402
+ if (!existsSync9(stateFile)) {
2355
2403
  continue;
2356
2404
  }
2357
2405
  try {