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/{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 +147 -32
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +248 -99
- 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
|
}
|
|
@@ -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
|
|
559
|
-
const
|
|
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
|
|
703
|
-
let
|
|
704
|
-
if (
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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(
|
|
944
|
+
function createWorktreeShell(repoRoot, sessionId, agentId) {
|
|
874
945
|
const branchName = `sisyphus/${sessionId.slice(0, 8)}/${agentId}`;
|
|
875
|
-
const worktreePath = join4(worktreeBaseDir(
|
|
946
|
+
const worktreePath = join4(worktreeBaseDir(repoRoot), sessionId.slice(0, 8), agentId);
|
|
876
947
|
mkdirSync2(dirname2(worktreePath), { recursive: true });
|
|
877
|
-
execSafe(`git -C ${shellQuote(
|
|
948
|
+
execSafe(`git -C ${shellQuote(repoRoot)} worktree prune`);
|
|
878
949
|
if (existsSync5(worktreePath)) {
|
|
879
|
-
execSafe(`git -C ${shellQuote(
|
|
950
|
+
execSafe(`git -C ${shellQuote(repoRoot)} worktree remove --force ${shellQuote(worktreePath)}`);
|
|
880
951
|
}
|
|
881
|
-
execSafe(`git -C ${shellQuote(
|
|
882
|
-
exec(`git -C ${shellQuote(
|
|
883
|
-
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)}`);
|
|
884
955
|
return { worktreePath, branchName };
|
|
885
956
|
}
|
|
886
|
-
function bootstrapWorktree(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
921
|
-
const output = execSafe(`git -C ${shellQuote(
|
|
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
|
-
|
|
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
|
|
947
|
-
if (!
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
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(
|
|
977
|
-
execSafe(`git -C ${shellQuote(
|
|
978
|
-
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)}`);
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1341
|
+
const repoConfig = config ? config[repo] : void 0;
|
|
1342
|
+
if (repoConfig) {
|
|
1224
1343
|
const wtPath = worktreePath;
|
|
1225
1344
|
setImmediate(() => {
|
|
1226
1345
|
try {
|
|
1227
|
-
bootstrapWorktree(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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 =
|
|
2301
|
+
filePath = join7(dir, `${id}.md`);
|
|
2153
2302
|
writeFileSync5(filePath, req.content, "utf-8");
|
|
2154
2303
|
}
|
|
2155
2304
|
await appendMessage(tracking.cwd, req.sessionId, {
|