sisyphi 1.0.13 → 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
  }
@@ -602,6 +617,43 @@ function lookupPane(paneId) {
602
617
  }
603
618
 
604
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
+ }
605
657
  var sessionWindowMap = /* @__PURE__ */ new Map();
606
658
  var sessionOrchestratorPane = /* @__PURE__ */ new Map();
607
659
  function getWindowId(sessionId) {
@@ -700,30 +752,30 @@ ${agentLines}
700
752
  `;
701
753
  }
702
754
  const roadmapRef = existsSync4(roadmapFile) ? `@${roadmapFile}` : "(empty)";
703
- const worktreeAgents = session.agents.filter((a) => a.worktreePath);
704
- let worktreeSection = "";
705
- if (worktreeAgents.length > 0 || existsSync4(worktreeConfigPath(session.cwd))) {
706
- let wtLines = "";
707
- if (worktreeAgents.length > 0) {
708
- wtLines = "\n" + worktreeAgents.map((a) => {
709
- if (a.mergeStatus === "conflict") {
710
- return `- ${a.id}: CONFLICT \u2014 ${a.mergeDetails ?? "unknown"}
711
- Branch: ${a.branchName}
712
- Worktree: ${a.worktreePath}`;
713
- }
714
- if (a.mergeStatus === "no-changes") {
715
- 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
+ `;
716
773
  }
717
- const status = a.mergeStatus ?? "pending";
718
- return `- ${a.id}: ${status} (branch ${a.branchName})`;
719
- }).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';
720
778
  }
721
- 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.";
722
- worktreeSection = `
723
-
724
- ## Git Worktrees
725
-
726
- ${worktreeHint}${wtLines}`;
727
779
  }
728
780
  const goalFile = goalPath(session.cwd, session.id);
729
781
  const goalContent = existsSync4(goalFile) ? readFileSync3(goalFile, "utf-8").trim() : session.task;
@@ -738,7 +790,7 @@ ${previousCyclesSection}${mostRecentCycleSection}
738
790
  ## Roadmap
739
791
 
740
792
  ${roadmapRef}
741
- ${worktreeSection}`;
793
+ `;
742
794
  }
743
795
  async function spawnOrchestrator(sessionId, cwd, windowId, message) {
744
796
  try {
@@ -767,10 +819,12 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
767
819
  writeFileSync3(promptFilePath, systemPrompt, "utf-8");
768
820
  sessionWindowMap.set(sessionId, windowId);
769
821
  const npmBinDir = resolveNpmBinDir();
822
+ const sesDir = sessionDir(cwd, sessionId);
770
823
  const envExports = buildEnvExports([
771
824
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
772
825
  `export SISYPHUS_AGENT_ID='orchestrator'`,
773
826
  `export SISYPHUS_CWD='${cwd}'`,
827
+ `export SISYPHUS_SESSION_DIR='${sesDir}'`,
774
828
  `export PATH="${npmBinDir}:$PATH"`
775
829
  ]);
776
830
  let userPrompt = formattedState;
@@ -798,11 +852,11 @@ ${continuationText}`;
798
852
  const settingsPath = resolve2(import.meta.dirname, "../templates/orchestrator-settings.json");
799
853
  const config = loadConfig(cwd);
800
854
  const effort = config.orchestratorEffort ?? "high";
801
- 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}')"`;
802
856
  const paneId = createPane(windowId, cwd, "left");
803
857
  sessionOrchestratorPane.set(sessionId, paneId);
804
858
  registerPane(paneId, sessionId, "orchestrator");
805
- setPaneTitle(paneId, `Sisyphus`);
859
+ setPaneTitle(paneId, session.name ? `${session.name} (orch)` : "Sisyphus");
806
860
  setPaneStyle(paneId, ORCHESTRATOR_COLOR);
807
861
  const bannerCmd = resolveBannerCmd();
808
862
  const notifyCmd = buildNotifyCmd(paneId);
@@ -866,37 +920,53 @@ import { dirname as dirname2, join as join4 } from "path";
866
920
  function loadWorktreeConfig(cwd) {
867
921
  try {
868
922
  const content = readFileSync4(worktreeConfigPath(cwd), "utf-8");
869
- return JSON.parse(content);
870
- } catch {
871
- 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;
872
942
  }
873
943
  }
874
- function createWorktreeShell(cwd, sessionId, agentId) {
944
+ function createWorktreeShell(repoRoot, sessionId, agentId) {
875
945
  const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
876
- const worktreePath = join4(worktreeBaseDir(cwd), sessionId.slice(0, 8), agentId);
946
+ const worktreePath = join4(worktreeBaseDir(repoRoot), sessionId.slice(0, 8), agentId);
877
947
  mkdirSync2(dirname2(worktreePath), { recursive: true });
878
- execSafe(`git -C ${shellQuote(cwd)} worktree prune`);
948
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree prune`);
879
949
  if (existsSync5(worktreePath)) {
880
- execSafe(`git -C ${shellQuote(cwd)} worktree remove --force ${shellQuote(worktreePath)}`);
950
+ execSafe(`git -C ${shellQuote(repoRoot)} worktree remove --force ${shellQuote(worktreePath)}`);
881
951
  }
882
- execSafe(`git -C ${shellQuote(cwd)} branch -D ${shellQuote(branchName)}`);
883
- exec(`git -C ${shellQuote(cwd)} branch ${shellQuote(branchName)} HEAD`);
884
- 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)}`);
885
955
  return { worktreePath, branchName };
886
956
  }
887
- function bootstrapWorktree(cwd, worktreePath, config) {
957
+ function bootstrapWorktree(repoRoot, worktreePath, config) {
888
958
  if (config.copy) {
889
959
  for (const entry of config.copy) {
890
960
  const dest = join4(worktreePath, entry);
891
961
  mkdirSync2(dirname2(dest), { recursive: true });
892
- execSafe(`cp -r ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
962
+ execSafe(`cp -r ${shellQuote(join4(repoRoot, entry))} ${shellQuote(dest)}`);
893
963
  }
894
964
  }
895
965
  if (config.clone) {
896
966
  for (const entry of config.clone) {
897
967
  const dest = join4(worktreePath, entry);
898
968
  mkdirSync2(dirname2(dest), { recursive: true });
899
- const src = shellQuote(join4(cwd, entry));
969
+ const src = shellQuote(join4(repoRoot, entry));
900
970
  const dstQ = shellQuote(dest);
901
971
  if (execSafe(`cp -Rc ${src} ${dstQ}`) === null) {
902
972
  execSafe(`cp -r ${src} ${dstQ}`);
@@ -907,7 +977,7 @@ function bootstrapWorktree(cwd, worktreePath, config) {
907
977
  for (const entry of config.symlink) {
908
978
  const dest = join4(worktreePath, entry);
909
979
  mkdirSync2(dirname2(dest), { recursive: true });
910
- execSafe(`ln -s ${shellQuote(join4(cwd, entry))} ${shellQuote(dest)}`);
980
+ execSafe(`ln -s ${shellQuote(join4(repoRoot, entry))} ${shellQuote(dest)}`);
911
981
  }
912
982
  }
913
983
  if (config.init) {
@@ -918,8 +988,8 @@ function bootstrapWorktree(cwd, worktreePath, config) {
918
988
  }
919
989
  }
920
990
  }
921
- function resolveWorktreeBranch(cwd, worktreePath) {
922
- 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`);
923
993
  if (!output) return null;
924
994
  const lines = output.split("\n");
925
995
  for (let i = 0; i < lines.length; i++) {
@@ -941,42 +1011,59 @@ function mergeWorktrees(cwd, agents) {
941
1011
  (a) => a.worktreePath && a.mergeStatus === "pending"
942
1012
  );
943
1013
  const results = [];
944
- execSafe(`git -C ${shellQuote(cwd)} add .sisyphus`);
945
- execSafe(`git -C ${shellQuote(cwd)} commit -m 'sisyphus: snapshot session state before merge'`);
1014
+ const byRepo = /* @__PURE__ */ new Map();
946
1015
  for (const agent of pending) {
947
- const branch = resolveWorktreeBranch(cwd, agent.worktreePath);
948
- if (!branch) {
949
- results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
950
- execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)} --force`);
951
- continue;
952
- }
953
- const aheadLog = execSafe(`git -C ${shellQuote(cwd)} log HEAD..${shellQuote(branch)} --oneline`);
954
- if (!aheadLog) {
955
- results.push({ agentId: agent.id, name: agent.name, status: "no-changes" });
956
- cleanupWorktree(cwd, agent.worktreePath, branch);
957
- continue;
958
- }
959
- const mergeMsg = `sisyphus: merge ${agent.id} (${agent.name})`;
960
- const mergeCmd = `git -C ${shellQuote(cwd)} merge --no-ff ${shellQuote(branch)} -m ${shellQuote(mergeMsg)}`;
961
- try {
962
- exec(mergeCmd);
963
- execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(agent.worktreePath)}`);
964
- execSafe(`git -C ${shellQuote(cwd)} branch -d ${shellQuote(branch)}`);
965
- results.push({ agentId: agent.id, name: agent.name, status: "merged" });
966
- } catch (err) {
967
- execSafe(`git -C ${shellQuote(cwd)} merge --abort`);
968
- const errObj = err;
969
- const stdout = errObj.stdout ? (typeof errObj.stdout === "string" ? errObj.stdout : errObj.stdout.toString("utf-8")).trim() : "";
970
- const stderr = errObj.stderr ? (typeof errObj.stderr === "string" ? errObj.stderr : errObj.stderr.toString("utf-8")).trim() : "";
971
- const conflictDetails = stdout || stderr || (err instanceof Error ? err.message : String(err));
972
- 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
+ }
973
1060
  }
974
1061
  }
975
1062
  return results;
976
1063
  }
977
- function cleanupWorktree(cwd, worktreePath, branchName) {
978
- execSafe(`git -C ${shellQuote(cwd)} worktree remove ${shellQuote(worktreePath)} --force`);
979
- 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)}`);
980
1067
  const baseDir = dirname2(worktreePath);
981
1068
  try {
982
1069
  const entries = readdirSync4(baseDir);
@@ -993,6 +1080,38 @@ function countWorktreeAgents(agents) {
993
1080
  // src/daemon/summarize.ts
994
1081
  import { query } from "@r-cli/sdk";
995
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
+ }
996
1115
  async function summarizeReport(reportText) {
997
1116
  if (disabled) return null;
998
1117
  try {
@@ -1048,14 +1167,7 @@ function renderAgentSuffix(sessionId, instruction, worktreeContext) {
1048
1167
  Session: {{SESSION_ID}}
1049
1168
  Task: {{INSTRUCTION}}`;
1050
1169
  }
1051
- let worktreeBlock = "";
1052
- if (worktreeContext) {
1053
- worktreeBlock = [
1054
- "## Worktree Context",
1055
- `You are working in an isolated git worktree on branch \`${worktreeContext.branchName}\`.`,
1056
- `If you start any services that require ports, add ${worktreeContext.offset} to the default port.`
1057
- ].join("\n");
1058
- }
1170
+ const worktreeBlock = "";
1059
1171
  return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{WORKTREE_CONTEXT\}\}/g, worktreeBlock);
1060
1172
  }
1061
1173
  function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
@@ -1125,10 +1237,12 @@ function setupAgentPane(opts) {
1125
1237
  writeFileSync4(suffixFilePath, suffix, "utf-8");
1126
1238
  const bannerCmd = resolveBannerCmd();
1127
1239
  const npmBinDir = resolveNpmBinDir();
1240
+ const sesDir = sessionDir(cwd, sessionId);
1128
1241
  const envExports = buildEnvExports([
1129
1242
  `export SISYPHUS_SESSION_ID='${sessionId}'`,
1130
1243
  `export SISYPHUS_AGENT_ID='${agentId}'`,
1131
1244
  `export SISYPHUS_CWD='${cwd}'`,
1245
+ `export SISYPHUS_SESSION_DIR='${sesDir}'`,
1132
1246
  ...worktreeContext ? [`export SISYPHUS_PORT_OFFSET='${worktreeContext.offset}'`] : [],
1133
1247
  `export PATH="${npmBinDir}:$PATH"`
1134
1248
  ]);
@@ -1177,12 +1291,14 @@ async function spawnAgent(opts) {
1177
1291
  } catch {
1178
1292
  throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sisyphus doctor\` to diagnose.`);
1179
1293
  }
1180
- let paneCwd = cwd;
1294
+ const repo = opts.repo !== void 0 ? opts.repo : ".";
1295
+ const repoRoot = repo === "." ? cwd : join5(cwd, repo);
1296
+ let paneCwd = repoRoot;
1181
1297
  let worktreePath;
1182
1298
  let branchName;
1183
1299
  let worktreeContext;
1184
1300
  if (opts.worktree) {
1185
- const wt = createWorktreeShell(cwd, sessionId, agentId);
1301
+ const wt = createWorktreeShell(repoRoot, sessionId, agentId);
1186
1302
  worktreePath = wt.worktreePath;
1187
1303
  branchName = wt.branchName;
1188
1304
  paneCwd = worktreePath;
@@ -1216,16 +1332,18 @@ async function spawnAgent(opts) {
1216
1332
  completedAt: null,
1217
1333
  reports: [],
1218
1334
  paneId,
1335
+ repo,
1219
1336
  ...worktreePath ? { worktreePath, branchName, mergeStatus: "pending" } : {}
1220
1337
  };
1221
1338
  await addAgent(cwd, sessionId, agent);
1222
1339
  if (opts.worktree && worktreePath) {
1223
1340
  const config = loadWorktreeConfig(cwd);
1224
- if (config) {
1341
+ const repoConfig = config ? config[repo] : void 0;
1342
+ if (repoConfig) {
1225
1343
  const wtPath = worktreePath;
1226
1344
  setImmediate(() => {
1227
1345
  try {
1228
- bootstrapWorktree(cwd, wtPath, config);
1346
+ bootstrapWorktree(repoRoot, wtPath, repoConfig);
1229
1347
  } catch (err) {
1230
1348
  console.error(`[sisyphus] worktree bootstrap failed for ${agentId}: ${err instanceof Error ? err.message : err}`);
1231
1349
  }
@@ -1268,6 +1386,8 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
1268
1386
  total: portOffset,
1269
1387
  branchName: agent.branchName
1270
1388
  };
1389
+ } else if (agent.repo !== ".") {
1390
+ paneCwd = join5(cwd, agent.repo);
1271
1391
  }
1272
1392
  if (agent.paneId) {
1273
1393
  try {
@@ -1521,6 +1641,30 @@ async function startSession(task, cwd, context, name) {
1521
1641
  updateTrackedWindow(sessionId, windowId);
1522
1642
  killPane(initialPaneId);
1523
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
+ }
1524
1668
  return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
1525
1669
  }
1526
1670
  var PRUNE_KEEP_COUNT = 10;
@@ -1711,7 +1855,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
1711
1855
  }
1712
1856
  });
1713
1857
  }
1714
- async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree) {
1858
+ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktree, repo) {
1715
1859
  const windowId = getWindowId(sessionId);
1716
1860
  if (!windowId) throw new Error(`No tmux window found for session ${sessionId}`);
1717
1861
  const session = getSession(cwd, sessionId);
@@ -1726,7 +1870,8 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction, worktre
1726
1870
  name,
1727
1871
  instruction,
1728
1872
  windowId,
1729
- worktree
1873
+ worktree,
1874
+ repo
1730
1875
  });
1731
1876
  await appendAgentToLastCycle(cwd, sessionId, agent.id);
1732
1877
  return { agentId: agent.id };
@@ -1783,7 +1928,8 @@ async function handleKill(sessionId, cwd) {
1783
1928
  }
1784
1929
  for (const agent of session.agents) {
1785
1930
  if (agent.worktreePath && agent.branchName) {
1786
- cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1931
+ const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
1932
+ cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
1787
1933
  }
1788
1934
  }
1789
1935
  const orchPaneId = getOrchestratorPaneId(sessionId);
@@ -1821,7 +1967,8 @@ async function handleKillAgent(sessionId, cwd, agentId) {
1821
1967
  killPane(agent.paneId);
1822
1968
  }
1823
1969
  if (agent.worktreePath && agent.branchName) {
1824
- cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
1970
+ const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
1971
+ cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
1825
1972
  }
1826
1973
  await updateAgent(cwd, sessionId, agentId, {
1827
1974
  status: "killed",
@@ -1854,7 +2001,8 @@ async function handleRollback(sessionId, cwd, toCycle) {
1854
2001
  }
1855
2002
  for (const agent of session.agents) {
1856
2003
  if (agent.worktreePath && agent.branchName) {
1857
- cleanupWorktree(cwd, agent.worktreePath, agent.branchName);
2004
+ const repoRoot = agent.repo !== "." ? join6(cwd, agent.repo) : cwd;
2005
+ cleanupWorktree(repoRoot, agent.worktreePath, agent.branchName);
1858
2006
  }
1859
2007
  }
1860
2008
  const orchPaneId = getOrchestratorPaneId(sessionId);
@@ -1906,7 +2054,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
1906
2054
  var server = null;
1907
2055
  var sessionTrackingMap = /* @__PURE__ */ new Map();
1908
2056
  function registryPath() {
1909
- return join6(globalDir(), "session-registry.json");
2057
+ return join7(globalDir(), "session-registry.json");
1910
2058
  }
1911
2059
  function persistSessionRegistry() {
1912
2060
  const dir = globalDir();
@@ -1964,7 +2112,7 @@ async function handleRequest(req) {
1964
2112
  case "spawn": {
1965
2113
  const tracking = sessionTrackingMap.get(req.sessionId);
1966
2114
  if (!tracking) return unknownSessionError(req.sessionId);
1967
- 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);
1968
2116
  return { ok: true, data: { agentId: result.agentId } };
1969
2117
  }
1970
2118
  case "submit": {
@@ -2117,7 +2265,7 @@ async function handleRequest(req) {
2117
2265
  sessionTrackingMap.delete(req.sessionId);
2118
2266
  persistSessionRegistry();
2119
2267
  }
2120
- const { sessionDir: sessionDir2 } = await import("./paths-NUUALUVP.js");
2268
+ const { sessionDir: sessionDir2 } = await import("./paths-FYYSBD27.js");
2121
2269
  rmSync4(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
2122
2270
  return { ok: true };
2123
2271
  }
@@ -2150,7 +2298,7 @@ async function handleRequest(req) {
2150
2298
  if (req.content.length > 200) {
2151
2299
  const dir = messagesDir(tracking.cwd, req.sessionId);
2152
2300
  mkdirSync4(dir, { recursive: true });
2153
- filePath = join6(dir, `${id}.md`);
2301
+ filePath = join7(dir, `${id}.md`);
2154
2302
  writeFileSync5(filePath, req.content, "utf-8");
2155
2303
  }
2156
2304
  await appendMessage(tracking.cwd, req.sessionId, {