sisyphi 1.0.12 → 1.0.14

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
@@ -5,7 +5,7 @@ import {
5
5
  execEnv,
6
6
  execSafe,
7
7
  loadConfig
8
- } from "./chunk-LWWRGQWM.js";
8
+ } from "./chunk-MMA43N67.js";
9
9
  import {
10
10
  shellQuote
11
11
  } from "./chunk-6G226ZK7.js";
@@ -32,7 +32,7 @@ import {
32
32
  statePath,
33
33
  worktreeBaseDir,
34
34
  worktreeConfigPath
35
- } from "./chunk-JXKUI4P6.js";
35
+ } from "./chunk-YGBGKMTF.js";
36
36
 
37
37
  // src/daemon/index.ts
38
38
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, existsSync as existsSync9 } from "fs";
@@ -42,11 +42,12 @@ import { setTimeout as sleep } from "timers/promises";
42
42
  // src/daemon/server.ts
43
43
  import { createServer } from "net";
44
44
  import { unlinkSync, existsSync as existsSync8, writeFileSync as writeFileSync5, readFileSync as readFileSync6, mkdirSync as mkdirSync4, rmSync as rmSync4 } from "fs";
45
- import { join as join6 } from "path";
45
+ import { join as join7 } from "path";
46
46
 
47
47
  // src/daemon/session-manager.ts
48
48
  import { v4 as uuidv4 } from "uuid";
49
49
  import { existsSync as existsSync7, readdirSync as readdirSync6, rmSync as rmSync3 } from "fs";
50
+ import { join as join6 } from "path";
50
51
 
51
52
  // src/daemon/state.ts
52
53
  import { randomUUID } from "crypto";
@@ -108,7 +109,11 @@ function createSession(id, task, cwd, context, name) {
108
109
  }
109
110
  function getSession(cwd, sessionId) {
110
111
  const content = readFileSync(statePath(cwd, sessionId), "utf-8");
111
- return JSON.parse(content);
112
+ const session = JSON.parse(content);
113
+ for (const agent of session.agents) {
114
+ if (!agent.repo) agent.repo = ".";
115
+ }
116
+ return session;
112
117
  }
113
118
  function saveSession(session) {
114
119
  atomicWrite(statePath(session.cwd, session.id), JSON.stringify(session, null, 2));
@@ -202,6 +207,13 @@ async function updateReportSummary(cwd, sessionId, agentId, filePath, summary) {
202
207
  }
203
208
  });
204
209
  }
210
+ async function updateSessionName(cwd, sessionId, name) {
211
+ return withSessionLock(sessionId, () => {
212
+ const session = getSession(cwd, sessionId);
213
+ session.name = name;
214
+ saveSession(session);
215
+ });
216
+ }
205
217
  async function updateSessionTmux(cwd, sessionId, tmuxSessionName, tmuxWindowId) {
206
218
  return withSessionLock(sessionId, () => {
207
219
  const session = getSession(cwd, sessionId);
@@ -521,6 +533,9 @@ function sessionExists(sessionName) {
521
533
  function killSession(sessionName) {
522
534
  execSafe(`tmux kill-session -t "${sessionName}"`);
523
535
  }
536
+ function renameSession(oldName, newName) {
537
+ exec(`tmux rename-session -t "${oldName}" "${newName}"`);
538
+ }
524
539
  function setSessionOption(sessionName, option, value) {
525
540
  execSafe(`tmux set-option -t "${sessionName}" ${option} ${shellQuote(value)}`);
526
541
  }
@@ -555,8 +570,9 @@ function setPaneTitle(paneTarget, title) {
555
570
  execSafe(`tmux select-pane -t "${paneTarget}" -T ${shellQuote(title)}`);
556
571
  }
557
572
  function setPaneStyle(paneTarget, color) {
558
- const gitBranch = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null || echo 'n/a')`;
559
- const fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path} | ${gitBranch} #[default]`;
573
+ const gitBranch = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null)`;
574
+ const branchSuffix = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null | grep -q . && echo ' |') ${gitBranch}`;
575
+ const fmt = `#[fg=${color},bold] #{pane_title} #[fg=${color}]#{pane_current_path}${branchSuffix} #[default]`;
560
576
  execSafe(`tmux set -p -t "${paneTarget}" pane-border-format ${shellQuote(fmt)}`);
561
577
  execSafe(`tmux set -p -t "${paneTarget}" @pane_color "${color}"`);
562
578
  execSafe(`tmux set -w -t "${paneTarget}" pane-border-style "fg=#{?#{@pane_color},#{@pane_color},default}"`);
@@ -601,6 +617,43 @@ function lookupPane(paneId) {
601
617
  }
602
618
 
603
619
  // src/daemon/orchestrator.ts
620
+ function detectRepos(cwd) {
621
+ const config = loadConfig(cwd);
622
+ const repos = [];
623
+ if (existsSync4(join3(cwd, ".git"))) {
624
+ try {
625
+ repos.push(getRepoInfo(cwd, "."));
626
+ } catch {
627
+ }
628
+ }
629
+ try {
630
+ const entries = readdirSync3(cwd, { withFileTypes: true });
631
+ for (const entry of entries) {
632
+ if (!entry.isDirectory()) continue;
633
+ if (entry.name.startsWith(".")) continue;
634
+ const childPath = join3(cwd, entry.name);
635
+ if (existsSync4(join3(childPath, ".git"))) {
636
+ try {
637
+ repos.push(getRepoInfo(childPath, entry.name));
638
+ } catch {
639
+ }
640
+ }
641
+ }
642
+ } catch {
643
+ }
644
+ if (config.repos && config.repos.length > 0) {
645
+ const allowed = new Set(config.repos);
646
+ return repos.filter((r) => r.name === "." || allowed.has(r.name));
647
+ }
648
+ return repos;
649
+ }
650
+ function getRepoInfo(repoPath, name) {
651
+ const branchRaw = execSafe(`git -C ${shellQuote(repoPath)} rev-parse --abbrev-ref HEAD`)?.trim();
652
+ if (!branchRaw) throw new Error(`Failed to detect git branch for repo: ${repoPath}`);
653
+ const status = execSafe(`git -C ${shellQuote(repoPath)} status --porcelain`);
654
+ const isDirty = !!(status && status.trim().length > 0);
655
+ return { name, path: repoPath, branch: branchRaw, isDirty };
656
+ }
604
657
  var sessionWindowMap = /* @__PURE__ */ new Map();
605
658
  var sessionOrchestratorPane = /* @__PURE__ */ new Map();
606
659
  function getWindowId(sessionId) {
@@ -699,30 +752,30 @@ ${agentLines}
699
752
  `;
700
753
  }
701
754
  const roadmapRef = existsSync4(roadmapFile) ? `@${roadmapFile}` : "(empty)";
702
- const worktreeAgents = session.agents.filter((a) => a.worktreePath);
703
- let worktreeSection = "";
704
- if (worktreeAgents.length > 0 || existsSync4(worktreeConfigPath(session.cwd))) {
705
- let wtLines = "";
706
- if (worktreeAgents.length > 0) {
707
- wtLines = "\n" + worktreeAgents.map((a) => {
708
- if (a.mergeStatus === "conflict") {
709
- return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
710
- Branch: ${a.branchName}
711
- Worktree: ${a.worktreePath}`;
712
- }
713
- if (a.mergeStatus === "no-changes") {
714
- return `- ${a.id}: NO CHANGES \u2014 agent did not commit any work to branch ${a.branchName}`;
755
+ const repos = detectRepos(session.cwd);
756
+ let repositoriesSection = "\n\n## Repositories\n";
757
+ if (repos.length === 0) {
758
+ repositoriesSection += "\nNo git repositories detected.\n";
759
+ } else {
760
+ for (const repo of repos) {
761
+ const dirtyTag = repo.isDirty ? " (dirty)" : "";
762
+ repositoriesSection += `
763
+ ### ${repo.name === "." ? "Session Root (.)" : repo.name}
764
+ `;
765
+ repositoriesSection += `Branch: \`${repo.branch}\`${dirtyTag}
766
+ `;
767
+ const repoAgents = session.agents.filter((a) => a.repo === repo.name);
768
+ if (repoAgents.length > 0) {
769
+ repositoriesSection += "\nAgents:\n";
770
+ for (const a of repoAgents) {
771
+ repositoriesSection += `- ${a.id} (${a.name}) [${a.status}]
772
+ `;
715
773
  }
716
- const status = a.mergeStatus ?? "pending";
717
- return `- ${a.id}: ${status} (branch ${a.branchName})`;
718
- }).join("\n");
774
+ }
775
+ }
776
+ if (repos.length > 1) {
777
+ repositoriesSection += '\nTarget agents at specific repos:\n```bash\nsisyphus spawn --name "impl" --repo <repo-name> "task"\n```\n';
719
778
  }
720
- 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.";
721
- worktreeSection = `
722
-
723
- ## Git Worktrees
724
-
725
- ${worktreeHint}${wtLines}`;
726
779
  }
727
780
  const goalFile = goalPath(session.cwd, session.id);
728
781
  const goalContent = existsSync4(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
@@ -737,7 +790,7 @@ ${previousCyclesSection}${mostRecentCycleSection}
737
790
  ## Roadmap
738
791
 
739
792
  ${roadmapRef}
740
- ${worktreeSection}`;
793
+ `;
741
794
  }
742
795
  async function spawnOrchestrator(sessionId, cwd, windowId, message) {
743
796
  try {
@@ -766,10 +819,12 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
766
819
  writeFileSync3(promptFilePath, systemPrompt, "utf-8");
767
820
  sessionWindowMap.set(sessionId, windowId);
768
821
  const npmBinDir = resolveNpmBinDir();
822
+ const sesDir = sessionDir(cwd, sessionId);
769
823
  const envExports = buildEnvExports([
770
824
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
771
825
  `export SISYPHUS_AGENT_ID='orchestrator'`,
772
826
  `export SISYPHUS_CWD='${cwd}'`,
827
+ `export SISYPHUS_SESSION_DIR='${sesDir}'`,
773
828
  `export PATH="${npmBinDir}:$PATH"`
774
829
  ]);
775
830
  let userPrompt = formattedState;
@@ -797,11 +852,11 @@ ${continuationText}`;
797
852
  const settingsPath = resolve2(import.meta.dirname, "../templates/orchestrator-settings.json");
798
853
  const config = loadConfig(cwd);
799
854
  const effort = config.orchestratorEffort ?? "high";
800
- 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}')"`;
855
+ const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --settings "${settingsPath}" --plugin-dir "${pluginPath}" --name "sisyphus:orch-${session.name ?? sessionId.slice(0, 8)}-cycle-${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
801
856
  const paneId = createPane(windowId, cwd, "left");
802
857
  sessionOrchestratorPane.set(sessionId, paneId);
803
858
  registerPane(paneId, sessionId, "orchestrator");
804
- setPaneTitle(paneId, `Sisyphus`);
859
+ setPaneTitle(paneId, session.name ? `${session.name} (orch)` : "Sisyphus");
805
860
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
806
861
  const bannerCmd = resolveBannerCmd();
807
862
  const notifyCmd = buildNotifyCmd(paneId);
@@ -865,37 +920,53 @@ import { dirname as dirname2, join as join4 } from "path";
865
920
  function loadWorktreeConfig(cwd) {
866
921
  try {
867
922
  const content = readFileSync4(worktreeConfigPath(cwd), "utf-8");
868
- return JSON.parse(content);
869
- } catch {
870
- return null;
923
+ const parsed = JSON.parse(content);
924
+ const flatKeys = ["copy", "clone", "symlink", "init"];
925
+ if (Object.keys(parsed).some((k) => flatKeys.includes(k))) {
926
+ throw new Error(
927
+ 'Flat worktree.json format is no longer supported. Migrate to keyed format: wrap your config under a "." key.\nExample: { ".": { "symlink": ["node_modules"], "init": "npm install" } }'
928
+ );
929
+ }
930
+ for (const [key, value] of Object.entries(parsed)) {
931
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
932
+ throw new Error(
933
+ `Invalid worktree.json: value for "${key}" must be an object with optional keys: copy, clone, symlink, init`
934
+ );
935
+ }
936
+ }
937
+ return parsed;
938
+ } catch (err) {
939
+ if (err instanceof SyntaxError) return null;
940
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") return null;
941
+ throw err;
871
942
  }
872
943
  }
873
- function createWorktreeShell(cwd, sessionId, agentId) {
944
+ function createWorktreeShell(repoRoot, sessionId, agentId) {
874
945
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
875
- const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
946
+ const worktreePath = join4(worktreeBaseDir(repoRoot), sessionId.slice(0, 8), agentId);
876
947
  mkdirSync2(dirname2(worktreePath), { recursive: true });
877
- execSafe(`git -C ${shellQuote(cwd)} worktree prune`);
948
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree prune`);
878
949
  if (existsSync5(worktreePath)) {
879
- execSafe(`git -C ${shellQuote(cwd)} worktree remove --force ${shellQuote(worktreePath)}`);
950
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree remove --force ${shellQuote(worktreePath)}`);
880
951
  }
881
- execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
882
- exec(`git -C ${shellQuote(cwd)} branch ${shellQuote(branchName)} HEAD`);
883
- exec(`git -C ${shellQuote(cwd)} worktree add ${shellQuote(worktreePath)} ${shellQuote(branchName)}`);
952
+ execSafe(`git -C ${shellQuote(repoRoot)} branch -D ${shellQuote(branchName)}`);
953
+ exec(`git -C ${shellQuote(repoRoot)} branch ${shellQuote(branchName)} HEAD`);
954
+ exec(`git -C ${shellQuote(repoRoot)} worktree add ${shellQuote(worktreePath)} ${shellQuote(branchName)}`);
884
955
  return { worktreePath, branchName };
885
956
  }
886
- function bootstrapWorktree(cwd, worktreePath, config) {
957
+ function bootstrapWorktree(repoRoot, worktreePath, config) {
887
958
  if (config.copy) {
888
959
  for (const entry of config.copy) {
889
960
  const dest = join4(worktreePath, entry);
890
961
  mkdirSync2(dirname2(dest), { recursive: true });
891
- execSafe(`cp -r ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
962
+ execSafe(`cp -r ${shellQuote(join4(repoRoot, entry))} ${shellQuote(dest)}`);
892
963
  }
893
964
  }
894
965
  if (config.clone) {
895
966
  for (const entry of config.clone) {
896
967
  const dest = join4(worktreePath, entry);
897
968
  mkdirSync2(dirname2(dest), { recursive: true });
898
- const src = shellQuote(join4(cwd, entry));
969
+ const src = shellQuote(join4(repoRoot, entry));
899
970
  const dstQ = shellQuote(dest);
900
971
  if (execSafe(`cp -Rc ${src} ${dstQ}`) === null) {
901
972
  execSafe(`cp -r ${src} ${dstQ}`);
@@ -906,7 +977,7 @@ function bootstrapWorktree(cwd, worktreePath, config) {
906
977
  for (const entry of config.symlink) {
907
978
  const dest = join4(worktreePath, entry);
908
979
  mkdirSync2(dirname2(dest), { recursive: true });
909
- execSafe(`ln -s ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
980
+ execSafe(`ln -s ${shellQuote(join4(repoRoot, entry))} ${shellQuote(dest)}`);
910
981
  }
911
982
  }
912
983
  if (config.init) {
@@ -917,8 +988,8 @@ function bootstrapWorktree(cwd, worktreePath, config) {
917
988
  }
918
989
  }
919
990
  }
920
- function resolveWorktreeBranch(cwd, worktreePath) {
921
- const output = execSafe(`git -C ${shellQuote(cwd)} worktree list --porcelain`);
991
+ function resolveWorktreeBranch(repoRoot, worktreePath) {
992
+ const output = execSafe(`git -C ${shellQuote(repoRoot)} worktree list --porcelain`);
922
993
  if (!output) return null;
923
994
  const lines = output.split("\n");
924
995
  for (let i = 0; i < lines.length; i++) {
@@ -940,42 +1011,59 @@ function mergeWorktrees(cwd, agents) {
940
1011
  (a) => a.worktreePath && a.mergeStatus === "pending"
941
1012
  );
942
1013
  const results = [];
943
- execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
944
- execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
1014
+ const byRepo = /* @__PURE__ */ new Map();
945
1015
  for (const agent of pending) {
946
- const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
947
- if (!branch) {
948
- results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
949
- execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
950
- continue;
951
- }
952
- const aheadLog = execSafe(`git -C ${shellQuote(cwd)} log HEAD..${shellQuote(branch)} --oneline`);
953
- if (!aheadLog) {
954
- results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
955
- cleanupWorktree(cwd, agent.worktreePath, branch);
956
- continue;
957
- }
958
- const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
959
- const mergeCmd = `git -C ${shellQuote(cwd)} merge --no-ff ${shellQuote(branch)} -m ${shellQuote(mergeMsg)}`;
960
- try {
961
- exec(mergeCmd);
962
- execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)}`);
963
- execSafe(`git -C ${shellQuote(cwd)} branch -d ${shellQuote(branch)}`);
964
- results.push({ agentId: agent.id, name: agent.name, status: "merged" });
965
- } catch (err) {
966
- execSafe(`git -C ${shellQuote(cwd)} merge --abort`);
967
- const errObj = err;
968
- const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
969
- const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
970
- const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
971
- results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
1016
+ const repo = agent.repo;
1017
+ if (!byRepo.has(repo)) byRepo.set(repo, []);
1018
+ byRepo.get(repo).push(agent);
1019
+ }
1020
+ if (existsSync5(join4(cwd, ".git"))) {
1021
+ execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
1022
+ execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
1023
+ } else {
1024
+ console.log("[sisyphus] Skipping .sisyphus snapshot \u2014 session root is not a git repo");
1025
+ }
1026
+ for (const [repo, repoAgents] of byRepo) {
1027
+ const repoRoot = repo === "." ? cwd : join4(cwd, repo);
1028
+ if (repo !== ".") {
1029
+ execSafe(`git -C ${shellQuote(repoRoot)} add -A`);
1030
+ execSafe(`git -C ${shellQuote(repoRoot)} commit -m 'sisyphus: snapshot before merge'`);
1031
+ }
1032
+ for (const agent of repoAgents) {
1033
+ const branch = resolveWorktreeBranch(repoRoot, agent.worktreePath);
1034
+ if (!branch) {
1035
+ results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
1036
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
1037
+ continue;
1038
+ }
1039
+ const aheadLog = execSafe(`git -C ${shellQuote(repoRoot)} log HEAD..${shellQuote(branch)} --oneline`);
1040
+ if (!aheadLog) {
1041
+ results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
1042
+ cleanupWorktree(repoRoot, agent.worktreePath, branch);
1043
+ continue;
1044
+ }
1045
+ const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
1046
+ const mergeCmd = `git -C ${shellQuote(repoRoot)} merge --no-ff ${shellQuote(branch)} -m ${shellQuote(mergeMsg)}`;
1047
+ try {
1048
+ exec(mergeCmd);
1049
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree remove ${shellQuote(agent.worktreePath)}`);
1050
+ execSafe(`git -C ${shellQuote(repoRoot)} branch -d ${shellQuote(branch)}`);
1051
+ results.push({ agentId: agent.id, name: agent.name, status: "merged" });
1052
+ } catch (err) {
1053
+ execSafe(`git -C ${shellQuote(repoRoot)} merge --abort`);
1054
+ const errObj = err;
1055
+ const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
1056
+ const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
1057
+ const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
1058
+ results.push({ agentId: agent.id, name: agent.name, status: "conflict", conflictDetails });
1059
+ }
972
1060
  }
973
1061
  }
974
1062
  return results;
975
1063
  }
976
- function cleanupWorktree(cwd, worktreePath, branchName) {
977
- execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(worktreePath)} --force`);
978
- execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
1064
+ function cleanupWorktree(repoRoot, worktreePath, branchName) {
1065
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree remove ${shellQuote(worktreePath)} --force`);
1066
+ execSafe(`git -C ${shellQuote(repoRoot)} branch -D ${shellQuote(branchName)}`);
979
1067
  const baseDir = dirname2(worktreePath);
980
1068
  try {
981
1069
  const entries = readdirSync4(baseDir);
@@ -992,6 +1080,38 @@ function countWorktreeAgents(agents) {
992
1080
  // src/daemon/summarize.ts
993
1081
  import { query } from "@r-cli/sdk";
994
1082
  var disabled = false;
1083
+ async function generateSessionName(task) {
1084
+ if (disabled) return null;
1085
+ try {
1086
+ const session = await query({
1087
+ prompt: `Generate a 2-4 word kebab-case name for this task. Output ONLY the name.
1088
+
1089
+ ${task.slice(0, 500)}`,
1090
+ options: {
1091
+ model: "haiku",
1092
+ maxTurns: 1
1093
+ }
1094
+ });
1095
+ let text = "";
1096
+ for await (const msg of session) {
1097
+ if (msg.type === "assistant" && msg.message?.content) {
1098
+ for (const block of msg.message.content) {
1099
+ if (block.type === "text") text += block.text;
1100
+ }
1101
+ }
1102
+ }
1103
+ const name = text.trim().toLowerCase();
1104
+ if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) return null;
1105
+ return name.slice(0, 30);
1106
+ } catch (err) {
1107
+ console.error(`[sisyphus] Haiku name generation failed: ${err instanceof Error ? err.message : err}`);
1108
+ const status = err.status;
1109
+ if (status === 401 || status === 403) {
1110
+ disabled = true;
1111
+ }
1112
+ return null;
1113
+ }
1114
+ }
995
1115
  async function summarizeReport(reportText) {
996
1116
  if (disabled) return null;
997
1117
  try {
@@ -1047,14 +1167,7 @@ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
1047
1167
  Session: {{SESSION_ID}}
1048
1168
  Task: {{INSTRUCTION}}`;
1049
1169
  }
1050
- let worktreeBlock = "";
1051
- if (worktreeContext) {
1052
- worktreeBlock = [
1053
- "## Worktree Context",
1054
- `You are working in an isolated git worktree on branch \`${worktreeContext.branchName}\`.`,
1055
- `If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
1056
- ].join("\n");
1057
- }
1170
+ const worktreeBlock = "";
1058
1171
  return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
1059
1172
  }
1060
1173
  function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
@@ -1124,10 +1237,12 @@ function setupAgentPane(opts) {
1124
1237
  writeFileSync4(suffixFilePath, suffix, "utf-8");
1125
1238
  const bannerCmd = resolveBannerCmd();
1126
1239
  const npmBinDir = resolveNpmBinDir();
1240
+ const sesDir = sessionDir(cwd, sessionId);
1127
1241
  const envExports = buildEnvExports([
1128
1242
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
1129
1243
  `export SISYPHUS_AGENT_ID='${agentId}'`,
1130
1244
  `export SISYPHUS_CWD='${cwd}'`,
1245
+ `export SISYPHUS_SESSION_DIR='${sesDir}'`,
1131
1246
  ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1132
1247
  `export PATH="${npmBinDir}:$PATH"`
1133
1248
  ]);
@@ -1176,12 +1291,14 @@ async function spawnAgent(opts) {
1176
1291
  } catch {
1177
1292
  throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sisyphus doctor\` to diagnose.`);
1178
1293
  }
1179
- let paneCwd = cwd;
1294
+ const repo = opts.repo !== void 0 ? opts.repo : ".";
1295
+ const repoRoot = repo === "." ? cwd : join5(cwd, repo);
1296
+ let paneCwd = repoRoot;
1180
1297
  let worktreePath;
1181
1298
  let branchName;
1182
1299
  let worktreeContext;
1183
1300
  if (opts.worktree) {
1184
- const wt = createWorktreeShell(cwd, sessionId, agentId);
1301
+ const wt = createWorktreeShell(repoRoot, sessionId, agentId);
1185
1302
  worktreePath = wt.worktreePath;
1186
1303
  branchName = wt.branchName;
1187
1304
  paneCwd = worktreePath;
@@ -1215,16 +1332,18 @@ async function spawnAgent(opts) {
1215
1332
  completedAt: null,
1216
1333
  reports: [],
1217
1334
  paneId,
1335
+ repo,
1218
1336
  ...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
1219
1337
  };
1220
1338
  await addAgent(cwd, sessionId, agent);
1221
1339
  if (opts.worktree && worktreePath) {
1222
1340
  const config = loadWorktreeConfig(cwd);
1223
- if (config) {
1341
+ const repoConfig = config ? config[repo] : void 0;
1342
+ if (repoConfig) {
1224
1343
  const wtPath = worktreePath;
1225
1344
  setImmediate(() => {
1226
1345
  try {
1227
- bootstrapWorktree(cwd, wtPath, config);
1346
+ bootstrapWorktree(repoRoot, wtPath, repoConfig);
1228
1347
  } catch (err) {
1229
1348
  console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
1230
1349
  }
@@ -1267,6 +1386,8 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1267
1386
  total: portOffset,
1268
1387
  branchName: agent.branchName
1269
1388
  };
1389
+ } else if (agent.repo !== ".") {
1390
+ paneCwd = join5(cwd, agent.repo);
1270
1391
  }
1271
1392
  if (agent.paneId) {
1272
1393
  try {
@@ -1520,6 +1641,30 @@ async function startSession(task, cwd, context, name) {
1520
1641
  updateTrackedWindow(sessionId, windowId);
1521
1642
  killPane(initialPaneId);
1522
1643
  pruneOldSessions(cwd);
1644
+ if (!name) {
1645
+ generateSessionName(task).then(async (generatedName) => {
1646
+ if (!generatedName) return;
1647
+ let finalName = generatedName;
1648
+ let candidate = `sisyphus-${finalName}`;
1649
+ let attempt = 0;
1650
+ while (sessionExists(candidate) && attempt < 5) {
1651
+ attempt++;
1652
+ finalName = `${generatedName}-${attempt}`;
1653
+ candidate = `sisyphus-${finalName}`;
1654
+ }
1655
+ if (sessionExists(candidate)) return;
1656
+ try {
1657
+ renameSession(tmuxName, candidate);
1658
+ } catch {
1659
+ return;
1660
+ }
1661
+ await updateSessionName(cwd, sessionId, finalName);
1662
+ await updateSessionTmux(cwd, sessionId, candidate, getSession(cwd, sessionId).tmuxWindowId);
1663
+ trackSession(sessionId, cwd, candidate);
1664
+ registerSessionTmux(sessionId, candidate, getSession(cwd, sessionId).tmuxWindowId);
1665
+ }).catch(() => {
1666
+ });
1667
+ }
1523
1668
  return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
1524
1669
  }
1525
1670
  var PRUNE_KEEP_COUNT = 10;
@@ -1710,7 +1855,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
1710
1855
  }
1711
1856
  });
1712
1857
  }
1713
- async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
1858
+ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree, repo) {
1714
1859
  const windowId = getWindowId(sessionId);
1715
1860
  if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
1716
1861
  const session = getSession(cwd, sessionId);
@@ -1725,7 +1870,8 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktre
1725
1870
  name,
1726
1871
  instruction,
1727
1872
  windowId,
1728
- worktree
1873
+ worktree,
1874
+ repo
1729
1875
  });
1730
1876
  await appendAgentToLastCycle(cwd, sessionId, agent.id);
1731
1877
  return { agentId: agent.id };
@@ -1782,7 +1928,8 @@ async function handleKill(sessionId, cwd) {
1782
1928
  }
1783
1929
  for (const agent of session.agents) {
1784
1930
  if (agent.worktreePath && agent.branchName) {
1785
- cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1931
+ const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
1932
+ cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
1786
1933
  }
1787
1934
  }
1788
1935
  const orchPaneId = getOrchestratorPaneId(sessionId);
@@ -1820,7 +1967,8 @@ async function handleKillAgent(sessionId, cwd, agentId) {
1820
1967
  killPane(agent.paneId);
1821
1968
  }
1822
1969
  if (agent.worktreePath && agent.branchName) {
1823
- cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1970
+ const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
1971
+ cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
1824
1972
  }
1825
1973
  await updateAgent(cwd, sessionId, agentId, {
1826
1974
  status: "killed",
@@ -1853,7 +2001,8 @@ async function handleRollback(sessionId, cwd, toCycle) {
1853
2001
  }
1854
2002
  for (const agent of session.agents) {
1855
2003
  if (agent.worktreePath && agent.branchName) {
1856
- cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
2004
+ const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
2005
+ cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
1857
2006
  }
1858
2007
  }
1859
2008
  const orchPaneId = getOrchestratorPaneId(sessionId);
@@ -1905,7 +2054,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1905
2054
  var server = null;
1906
2055
  var sessionTrackingMap = /* @__PURE__ */ new Map();
1907
2056
  function registryPath() {
1908
- return join6(globalDir(), "session-registry.json");
2057
+ return join7(globalDir(), "session-registry.json");
1909
2058
  }
1910
2059
  function persistSessionRegistry() {
1911
2060
  const dir = globalDir();
@@ -1963,7 +2112,7 @@ async function handleRequest(req) {
1963
2112
  case "spawn": {
1964
2113
  const tracking = sessionTrackingMap.get(req.sessionId);
1965
2114
  if (!tracking) return unknownSessionError(req.sessionId);
1966
- const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.worktree);
2115
+ const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.worktree, req.repo);
1967
2116
  return { ok: true, data: { agentId: result.agentId } };
1968
2117
  }
1969
2118
  case "submit": {
@@ -2116,7 +2265,7 @@ async function handleRequest(req) {
2116
2265
  sessionTrackingMap.delete(req.sessionId);
2117
2266
  persistSessionRegistry();
2118
2267
  }
2119
- const { sessionDir: sessionDir2 } = await import("./paths-NUUALUVP.js");
2268
+ const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
2120
2269
  rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
2121
2270
  return { ok: true };
2122
2271
  }
@@ -2149,7 +2298,7 @@ async function handleRequest(req) {
2149
2298
  if (req.content.length > 200) {
2150
2299
  const dir = messagesDir(tracking.cwd, req.sessionId);
2151
2300
  mkdirSync4(dir, { recursive: true });
2152
- filePath = join6(dir, `${id}.md`);
2301
+ filePath = join7(dir, `${id}.md`);
2153
2302
  writeFileSync5(filePath, req.content, "utf-8");
2154
2303
  }
2155
2304
  await appendMessage(tracking.cwd, req.sessionId, {