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/{chunk-LWWRGQWM.js → chunk-MMA43N67.js} +2 -2
- package/dist/chunk-MMA43N67.js.map +1 -0
- package/dist/{chunk-T7ETTIQK.js → chunk-Q6VQOUN3.js} +2 -2
- package/dist/{chunk-JXKUI4P6.js → chunk-YGBGKMTF.js} +1 -28
- package/dist/{chunk-JXKUI4P6.js.map → chunk-YGBGKMTF.js.map} +1 -1
- package/dist/cli.js +51 -23
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +245 -97
- package/dist/daemon.js.map +1 -1
- package/dist/{paths-NUUALUVP.js → paths-FYYSBD27.js} +2 -2
- package/dist/templates/agent-plugin/agents/spec-draft.md +1 -1
- package/dist/templates/agent-plugin/agents/test-spec.md +1 -1
- package/dist/templates/agent-plugin/hooks/require-submit.sh +1 -1
- package/dist/templates/orchestrator-base.md +6 -4
- package/dist/templates/orchestrator-planning.md +1 -1
- package/dist/tui.js +2671 -2908
- package/dist/tui.js.map +1 -1
- package/package.json +2 -4
- package/templates/agent-plugin/agents/spec-draft.md +1 -1
- package/templates/agent-plugin/agents/test-spec.md +1 -1
- package/templates/agent-plugin/hooks/require-submit.sh +1 -1
- package/templates/orchestrator-base.md +6 -4
- package/templates/orchestrator-planning.md +1 -1
- package/dist/chunk-LWWRGQWM.js.map +0 -1
- /package/dist/{chunk-T7ETTIQK.js.map → chunk-Q6VQOUN3.js.map} +0 -0
- /package/dist/{paths-NUUALUVP.js.map → paths-FYYSBD27.js.map} +0 -0
package/dist/daemon.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
execEnv,
|
|
6
6
|
execSafe,
|
|
7
7
|
loadConfig
|
|
8
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
704
|
-
let
|
|
705
|
-
if (
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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(
|
|
944
|
+
function createWorktreeShell(repoRoot, sessionId, agentId) {
|
|
875
945
|
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
876
|
-
const worktreePath = join4(worktreeBaseDir(
|
|
946
|
+
const worktreePath = join4(worktreeBaseDir(repoRoot), sessionId.slice(0, 8), agentId);
|
|
877
947
|
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
878
|
-
execSafe(`git -C ${shellQuote(
|
|
948
|
+
execSafe(`git -C ${shellQuote(repoRoot)} worktree prune`);
|
|
879
949
|
if (existsSync5(worktreePath)) {
|
|
880
|
-
execSafe(`git -C ${shellQuote(
|
|
950
|
+
execSafe(`git -C ${shellQuote(repoRoot)} worktree remove --force ${shellQuote(worktreePath)}`);
|
|
881
951
|
}
|
|
882
|
-
execSafe(`git -C ${shellQuote(
|
|
883
|
-
exec(`git -C ${shellQuote(
|
|
884
|
-
exec(`git -C ${shellQuote(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
922
|
-
const output = execSafe(`git -C ${shellQuote(
|
|
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
|
-
|
|
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
|
|
948
|
-
if (!
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
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(
|
|
978
|
-
execSafe(`git -C ${shellQuote(
|
|
979
|
-
execSafe(`git -C ${shellQuote(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1341
|
+
const repoConfig = config ? config[repo] : void 0;
|
|
1342
|
+
if (repoConfig) {
|
|
1225
1343
|
const wtPath = worktreePath;
|
|
1226
1344
|
setImmediate(() => {
|
|
1227
1345
|
try {
|
|
1228
|
-
bootstrapWorktree(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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 =
|
|
2301
|
+
filePath = join7(dir, `${id}.md`);
|
|
2154
2302
|
writeFileSync5(filePath, req.content, "utf-8");
|
|
2155
2303
|
}
|
|
2156
2304
|
await appendMessage(tracking.cwd, req.sessionId, {
|