sisyphi 1.1.17 → 1.1.18
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/README.md +209 -7
- package/dist/{chunk-6TIO23U3.js → chunk-22ZGZTGY.js} +3 -3
- package/dist/{chunk-6TIO23U3.js.map → chunk-22ZGZTGY.js.map} +1 -1
- package/dist/{chunk-UIVQXCWB.js → chunk-6PJVJEYQ.js} +2 -2
- package/dist/chunk-C2XKXERJ.js +1052 -0
- package/dist/chunk-C2XKXERJ.js.map +1 -0
- package/dist/{chunk-GSXF3TCZ.js → chunk-TMBAVPHH.js} +19 -3
- package/dist/chunk-TMBAVPHH.js.map +1 -0
- package/dist/chunk-V36NXMHP.js +299 -0
- package/dist/chunk-V36NXMHP.js.map +1 -0
- package/dist/cli.js +107 -12
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +1406 -262
- package/dist/daemon.js.map +1 -1
- package/dist/{paths-3EL2SCHI.js → paths-XRDEEJ5R.js} +10 -2
- package/dist/tui.js +1314 -984
- package/dist/tui.js.map +1 -1
- package/native/SisyphusNotify/AppIcon.icns +0 -0
- package/native/SisyphusNotify/Info.plist +26 -0
- package/native/SisyphusNotify/main.swift +126 -0
- package/native/SisyphusNotify/sisyphus-icon.jpg +0 -0
- package/native/build-notify.sh +58 -0
- package/package.json +3 -2
- package/dist/chunk-GSXF3TCZ.js.map +0 -1
- package/dist/chunk-HQZOAX6D.js +0 -240
- package/dist/chunk-HQZOAX6D.js.map +0 -1
- package/dist/chunk-IF55HPWX.js +0 -44
- package/dist/chunk-IF55HPWX.js.map +0 -1
- /package/dist/{chunk-UIVQXCWB.js.map → chunk-6PJVJEYQ.js.map} +0 -0
- /package/dist/{paths-3EL2SCHI.js.map → paths-XRDEEJ5R.js.map} +0 -0
package/dist/daemon.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
resolveRequiredPluginDirs
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6PJVJEYQ.js";
|
|
5
5
|
import {
|
|
6
6
|
EXEC_ENV,
|
|
7
7
|
exec,
|
|
8
8
|
execEnv,
|
|
9
9
|
execSafe
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-22ZGZTGY.js";
|
|
11
11
|
import {
|
|
12
|
+
ACHIEVEMENTS,
|
|
12
13
|
loadConfig,
|
|
14
|
+
renderCompanion,
|
|
13
15
|
shellQuote
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-V36NXMHP.js";
|
|
15
17
|
import {
|
|
18
|
+
companionPath,
|
|
16
19
|
contextDir,
|
|
17
20
|
cycleLogPath,
|
|
18
21
|
daemonPidPath,
|
|
@@ -33,22 +36,23 @@ import {
|
|
|
33
36
|
snapshotsDir,
|
|
34
37
|
socketPath,
|
|
35
38
|
statePath,
|
|
36
|
-
strategyPath
|
|
37
|
-
|
|
39
|
+
strategyPath,
|
|
40
|
+
tmuxSessionName
|
|
41
|
+
} from "./chunk-TMBAVPHH.js";
|
|
38
42
|
|
|
39
43
|
// src/daemon/index.ts
|
|
40
|
-
import { mkdirSync as
|
|
44
|
+
import { mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync3, existsSync as existsSync11 } from "fs";
|
|
41
45
|
import { execSync as execSync4 } from "child_process";
|
|
42
46
|
import { setTimeout as sleep } from "timers/promises";
|
|
43
47
|
|
|
44
48
|
// src/daemon/server.ts
|
|
45
49
|
import { createServer } from "net";
|
|
46
|
-
import { unlinkSync, existsSync as
|
|
47
|
-
import { join as
|
|
50
|
+
import { unlinkSync, existsSync as existsSync10, writeFileSync as writeFileSync7, readFileSync as readFileSync8, mkdirSync as mkdirSync5, rmSync as rmSync3 } from "fs";
|
|
51
|
+
import { join as join8 } from "path";
|
|
48
52
|
|
|
49
53
|
// src/daemon/session-manager.ts
|
|
50
54
|
import { v4 as uuidv4 } from "uuid";
|
|
51
|
-
import { existsSync as
|
|
55
|
+
import { existsSync as existsSync9, readdirSync as readdirSync5, rmSync as rmSync2 } from "fs";
|
|
52
56
|
|
|
53
57
|
// src/daemon/state.ts
|
|
54
58
|
import { randomUUID } from "crypto";
|
|
@@ -96,6 +100,8 @@ function createSession(id, task, cwd, context, name) {
|
|
|
96
100
|
if (context) {
|
|
97
101
|
writeFileSync(join(contextDir(cwd, id), "initial-context.md"), context, "utf-8");
|
|
98
102
|
}
|
|
103
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
104
|
+
const created = new Date(createdAt);
|
|
99
105
|
const session = {
|
|
100
106
|
id,
|
|
101
107
|
...name ? { name } : {},
|
|
@@ -103,11 +109,13 @@ function createSession(id, task, cwd, context, name) {
|
|
|
103
109
|
...context ? { context } : {},
|
|
104
110
|
cwd,
|
|
105
111
|
status: "active",
|
|
106
|
-
createdAt
|
|
112
|
+
createdAt,
|
|
107
113
|
activeMs: 0,
|
|
108
114
|
agents: [],
|
|
109
115
|
orchestratorCycles: [],
|
|
110
|
-
messages: []
|
|
116
|
+
messages: [],
|
|
117
|
+
startHour: created.getHours(),
|
|
118
|
+
startDayOfWeek: created.getDay()
|
|
111
119
|
};
|
|
112
120
|
atomicWrite(statePath(cwd, id), JSON.stringify(session, null, 2));
|
|
113
121
|
return session;
|
|
@@ -224,14 +232,21 @@ async function updateSessionName(cwd, sessionId, name) {
|
|
|
224
232
|
saveSession(session);
|
|
225
233
|
});
|
|
226
234
|
}
|
|
227
|
-
async function updateSessionTmux(cwd, sessionId,
|
|
235
|
+
async function updateSessionTmux(cwd, sessionId, tmuxSessionName2, tmuxWindowId) {
|
|
228
236
|
return withSessionLock(sessionId, () => {
|
|
229
237
|
const session = getSession(cwd, sessionId);
|
|
230
|
-
session.tmuxSessionName =
|
|
238
|
+
session.tmuxSessionName = tmuxSessionName2;
|
|
231
239
|
session.tmuxWindowId = tmuxWindowId;
|
|
232
240
|
saveSession(session);
|
|
233
241
|
});
|
|
234
242
|
}
|
|
243
|
+
async function updateSession(cwd, sessionId, updates) {
|
|
244
|
+
return withSessionLock(sessionId, () => {
|
|
245
|
+
const session = getSession(cwd, sessionId);
|
|
246
|
+
Object.assign(session, updates);
|
|
247
|
+
saveSession(session);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
235
250
|
async function drainMessages(cwd, sessionId, count) {
|
|
236
251
|
return withSessionLock(sessionId, () => {
|
|
237
252
|
const session = getSession(cwd, sessionId);
|
|
@@ -339,10 +354,10 @@ function deleteSnapshotsAfter(cwd, sessionId, afterCycle) {
|
|
|
339
354
|
}
|
|
340
355
|
|
|
341
356
|
// src/daemon/orchestrator.ts
|
|
342
|
-
import { existsSync as
|
|
357
|
+
import { existsSync as existsSync7, readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
343
358
|
import { execSync as execSync2 } from "child_process";
|
|
344
|
-
import { randomUUID as
|
|
345
|
-
import { resolve as resolve3, join as
|
|
359
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
360
|
+
import { resolve as resolve3, join as join6, relative } from "path";
|
|
346
361
|
|
|
347
362
|
// src/daemon/spawn-helpers.ts
|
|
348
363
|
import { writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
@@ -547,10 +562,10 @@ function killPane(paneTarget) {
|
|
|
547
562
|
function killWindow(windowTarget) {
|
|
548
563
|
execSafe(`tmux kill-window -t "${windowTarget}"`);
|
|
549
564
|
}
|
|
550
|
-
function createSession2(sessionName,
|
|
551
|
-
exec(`tmux new-session -d -s "${sessionName}" -n
|
|
552
|
-
const windowId = exec(`tmux display-message -t "${sessionName}
|
|
553
|
-
const initialPaneId = exec(`tmux display-message -t "${sessionName}
|
|
565
|
+
function createSession2(sessionName, cwd) {
|
|
566
|
+
exec(`tmux new-session -d -s "${sessionName}" -n main -c ${shellQuote(cwd)}`);
|
|
567
|
+
const windowId = exec(`tmux display-message -t "${sessionName}:main" -p "#{window_id}"`);
|
|
568
|
+
const initialPaneId = exec(`tmux display-message -t "${sessionName}:main" -p "#{pane_id}"`);
|
|
554
569
|
configureSessionDefaults(sessionName, windowId);
|
|
555
570
|
return { windowId, initialPaneId };
|
|
556
571
|
}
|
|
@@ -574,7 +589,7 @@ function findHomeSession(cwd) {
|
|
|
574
589
|
if (!output) return null;
|
|
575
590
|
const normalizedCwd = cwd.replace(/\/+$/, "");
|
|
576
591
|
for (const name of output.split("\n").filter(Boolean)) {
|
|
577
|
-
if (name.startsWith("
|
|
592
|
+
if (name.startsWith("ssyph_")) continue;
|
|
578
593
|
const val = execSafe(`tmux show-options -t "${name}" -v @sisyphus_cwd`);
|
|
579
594
|
if (val?.trim() === normalizedCwd) return name;
|
|
580
595
|
}
|
|
@@ -638,6 +653,29 @@ function setWindowOption(windowTarget, option, value) {
|
|
|
638
653
|
function getSessionOption(sessionName, option) {
|
|
639
654
|
return execSafe(`tmux show-options -t "${sessionName}" -v ${option}`);
|
|
640
655
|
}
|
|
656
|
+
function getGlobalOption(option) {
|
|
657
|
+
try {
|
|
658
|
+
return execSafe(`tmux show-option -gv ${option}`)?.trim() || null;
|
|
659
|
+
} catch {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function setGlobalOption(option, value) {
|
|
664
|
+
execSafe(`tmux set-option -g ${option} ${shellQuote(value)}`);
|
|
665
|
+
}
|
|
666
|
+
function listAllSessions() {
|
|
667
|
+
const output = execSafe('tmux list-sessions -F "#{session_name}"');
|
|
668
|
+
if (!output) return [];
|
|
669
|
+
return output.split("\n").filter(Boolean);
|
|
670
|
+
}
|
|
671
|
+
function listAllPanes() {
|
|
672
|
+
const output = execSafe('tmux list-panes -a -F "#{session_name} #{pane_id}"');
|
|
673
|
+
if (!output) return [];
|
|
674
|
+
return output.split("\n").filter(Boolean).map((line) => {
|
|
675
|
+
const spaceIdx = line.indexOf(" ");
|
|
676
|
+
return { sessionName: line.slice(0, spaceIdx), paneId: line.slice(spaceIdx + 1) };
|
|
677
|
+
});
|
|
678
|
+
}
|
|
641
679
|
function configureSessionDefaults(sessionName, windowId) {
|
|
642
680
|
execSafe(`tmux set -w -t "${windowId}" pane-border-status top`);
|
|
643
681
|
execSafe(`tmux set -w -t "${windowId}" allow-rename off`);
|
|
@@ -688,17 +726,15 @@ import { execSync } from "child_process";
|
|
|
688
726
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
689
727
|
import { resolve as resolve2, dirname as dirname2, join as join3 } from "path";
|
|
690
728
|
|
|
691
|
-
// src/daemon/
|
|
729
|
+
// src/daemon/haiku.ts
|
|
692
730
|
import { query } from "@r-cli/sdk";
|
|
693
731
|
var COOLDOWN_MS = 5 * 60 * 1e3;
|
|
694
732
|
var disabledUntil = 0;
|
|
695
|
-
async function
|
|
733
|
+
async function callHaiku(prompt) {
|
|
696
734
|
if (Date.now() < disabledUntil) return null;
|
|
697
735
|
try {
|
|
698
736
|
const session = await query({
|
|
699
|
-
prompt
|
|
700
|
-
|
|
701
|
-
${task.slice(0, 500)}`,
|
|
737
|
+
prompt,
|
|
702
738
|
options: {
|
|
703
739
|
model: "haiku",
|
|
704
740
|
maxTurns: 1,
|
|
@@ -713,11 +749,9 @@ ${task.slice(0, 500)}`,
|
|
|
713
749
|
}
|
|
714
750
|
}
|
|
715
751
|
}
|
|
716
|
-
|
|
717
|
-
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) return null;
|
|
718
|
-
return name.slice(0, 30);
|
|
752
|
+
return text.trim() || null;
|
|
719
753
|
} catch (err) {
|
|
720
|
-
console.error(`[sisyphus] Haiku
|
|
754
|
+
console.error(`[sisyphus] Haiku call failed: ${err instanceof Error ? err.message : err}`);
|
|
721
755
|
const status = err.status;
|
|
722
756
|
if (status === 401 || status === 403) {
|
|
723
757
|
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
@@ -725,37 +759,27 @@ ${task.slice(0, 500)}`,
|
|
|
725
759
|
return null;
|
|
726
760
|
}
|
|
727
761
|
}
|
|
762
|
+
|
|
763
|
+
// src/daemon/summarize.ts
|
|
764
|
+
async function generateSessionName(task) {
|
|
765
|
+
const text = await callHaiku(
|
|
766
|
+
`Generate a 2-4 word kebab-case name for this task. Output ONLY the name.
|
|
767
|
+
|
|
768
|
+
${task.slice(0, 500)}`
|
|
769
|
+
);
|
|
770
|
+
if (!text) return null;
|
|
771
|
+
const name = text.toLowerCase();
|
|
772
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) return null;
|
|
773
|
+
return name.slice(0, 30);
|
|
774
|
+
}
|
|
728
775
|
async function summarizeReport(reportText) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const session = await query({
|
|
732
|
-
prompt: `Summarize this agent work report in one concise sentence (max 120 chars). Focus on what was accomplished and the outcome. Output ONLY the summary sentence, nothing else.
|
|
776
|
+
const text = await callHaiku(
|
|
777
|
+
`Summarize this agent work report in one concise sentence (max 120 chars). Focus on what was accomplished and the outcome. Output ONLY the summary sentence, nothing else.
|
|
733
778
|
|
|
734
|
-
${reportText.slice(0, 3e3)}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
env: execEnv()
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
let text = "";
|
|
742
|
-
for await (const msg of session) {
|
|
743
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
744
|
-
for (const block of msg.message.content) {
|
|
745
|
-
if (block.type === "text") text += block.text;
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
const summary = text.trim();
|
|
750
|
-
return summary.length > 0 ? summary : null;
|
|
751
|
-
} catch (err) {
|
|
752
|
-
console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
|
|
753
|
-
const status = err.status;
|
|
754
|
-
if (status === 401 || status === 403) {
|
|
755
|
-
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
756
|
-
}
|
|
757
|
-
return null;
|
|
758
|
-
}
|
|
779
|
+
${reportText.slice(0, 3e3)}`
|
|
780
|
+
);
|
|
781
|
+
if (!text) return null;
|
|
782
|
+
return text.length > 0 ? text : null;
|
|
759
783
|
}
|
|
760
784
|
|
|
761
785
|
// src/daemon/agent.ts
|
|
@@ -866,6 +890,7 @@ function setupAgentPane(opts) {
|
|
|
866
890
|
]);
|
|
867
891
|
const notifyCmd = buildNotifyCmd(paneId);
|
|
868
892
|
let mainCmd;
|
|
893
|
+
let resumeArgs;
|
|
869
894
|
if (provider === "openai") {
|
|
870
895
|
const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
|
|
871
896
|
const parts = [];
|
|
@@ -886,6 +911,7 @@ ${instruction}`);
|
|
|
886
911
|
const extraPluginFlags = requiredPluginDirs.map((p) => `--plugin-dir "${p}"`).join(" ");
|
|
887
912
|
const sessionIdFlag = claudeSessionId ? ` --session-id "${claudeSessionId}"` : "";
|
|
888
913
|
mainCmd = `claude --dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag}${sessionIdFlag}${extraPluginFlags ? ` ${extraPluginFlags}` : ""} --name ${shellQuote(agentTitle)} --append-system-prompt "$(cat '${suffixFilePath}')" ${shellQuote(instruction)}`;
|
|
914
|
+
resumeArgs = `--dangerously-skip-permissions --effort ${effort} --plugin-dir "${pluginPath}"${agentFlag}${extraPluginFlags ? ` ${extraPluginFlags}` : ""}`;
|
|
889
915
|
}
|
|
890
916
|
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
|
|
891
917
|
"#!/usr/bin/env bash",
|
|
@@ -895,7 +921,7 @@ ${instruction}`);
|
|
|
895
921
|
notifyCmd
|
|
896
922
|
]);
|
|
897
923
|
const fullCmd = `bash '${scriptPath}'`;
|
|
898
|
-
return { paneId, fullCmd };
|
|
924
|
+
return { paneId, fullCmd, resumeEnv: envExports, resumeArgs };
|
|
899
925
|
}
|
|
900
926
|
async function spawnAgent(opts) {
|
|
901
927
|
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
@@ -916,7 +942,7 @@ async function spawnAgent(opts) {
|
|
|
916
942
|
const repoRoot = repo === "." ? cwd : join3(cwd, repo);
|
|
917
943
|
const paneCwd = repoRoot;
|
|
918
944
|
const claudeSessionId = provider !== "openai" ? randomUUID2() : void 0;
|
|
919
|
-
const { paneId, fullCmd } = setupAgentPane({
|
|
945
|
+
const { paneId, fullCmd, resumeEnv, resumeArgs } = setupAgentPane({
|
|
920
946
|
sessionId,
|
|
921
947
|
sessionName: opts.sessionName,
|
|
922
948
|
cycleNum: opts.cycleNum,
|
|
@@ -946,7 +972,9 @@ async function spawnAgent(opts) {
|
|
|
946
972
|
activeMs: 0,
|
|
947
973
|
reports: [],
|
|
948
974
|
paneId,
|
|
949
|
-
repo
|
|
975
|
+
repo,
|
|
976
|
+
resumeEnv,
|
|
977
|
+
resumeArgs
|
|
950
978
|
};
|
|
951
979
|
await addAgent(cwd, sessionId, agent);
|
|
952
980
|
sendKeys(paneId, fullCmd);
|
|
@@ -1097,12 +1125,973 @@ function allAgentsDone(session) {
|
|
|
1097
1125
|
// src/daemon/respawn-guard.ts
|
|
1098
1126
|
var respawningSessions = /* @__PURE__ */ new Set();
|
|
1099
1127
|
|
|
1128
|
+
// src/daemon/companion.ts
|
|
1129
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1130
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1131
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1132
|
+
function loadCompanion() {
|
|
1133
|
+
const path = companionPath();
|
|
1134
|
+
if (!existsSync5(path)) {
|
|
1135
|
+
const state2 = createDefaultCompanion();
|
|
1136
|
+
saveCompanion(state2);
|
|
1137
|
+
return state2;
|
|
1138
|
+
}
|
|
1139
|
+
const raw = readFileSync4(path, "utf-8");
|
|
1140
|
+
const state = JSON.parse(raw);
|
|
1141
|
+
if (state.consecutiveCleanSessions == null) state.consecutiveCleanSessions = 0;
|
|
1142
|
+
if (state.consecutiveDaysActive == null) state.consecutiveDaysActive = 0;
|
|
1143
|
+
if (state.lastActiveDate == null) state.lastActiveDate = null;
|
|
1144
|
+
if (state.taskHistory == null) state.taskHistory = {};
|
|
1145
|
+
if (state.dailyRepos == null) state.dailyRepos = {};
|
|
1146
|
+
if (state.recentCompletions == null) state.recentCompletions = [];
|
|
1147
|
+
if (state.lifetimeAgentsSpawned == null) state.lifetimeAgentsSpawned = 0;
|
|
1148
|
+
if (state.consecutiveEfficientSessions == null) state.consecutiveEfficientSessions = 0;
|
|
1149
|
+
return state;
|
|
1150
|
+
}
|
|
1151
|
+
function saveCompanion(state) {
|
|
1152
|
+
const path = companionPath();
|
|
1153
|
+
const dir = dirname3(path);
|
|
1154
|
+
mkdirSync3(dir, { recursive: true });
|
|
1155
|
+
const tmp = join4(dir, `.companion.${randomUUID3()}.tmp`);
|
|
1156
|
+
writeFileSync4(tmp, JSON.stringify(state, null, 2), "utf-8");
|
|
1157
|
+
renameSync2(tmp, path);
|
|
1158
|
+
}
|
|
1159
|
+
function createDefaultCompanion() {
|
|
1160
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1161
|
+
return {
|
|
1162
|
+
version: 1,
|
|
1163
|
+
name: null,
|
|
1164
|
+
createdAt: now,
|
|
1165
|
+
stats: {
|
|
1166
|
+
strength: 0,
|
|
1167
|
+
endurance: 0,
|
|
1168
|
+
wisdom: 0,
|
|
1169
|
+
patience: 0
|
|
1170
|
+
},
|
|
1171
|
+
xp: 0,
|
|
1172
|
+
level: 1,
|
|
1173
|
+
title: "Boulder Intern",
|
|
1174
|
+
mood: "sleepy",
|
|
1175
|
+
moodUpdatedAt: now,
|
|
1176
|
+
achievements: [],
|
|
1177
|
+
repos: {},
|
|
1178
|
+
lastCommentary: null,
|
|
1179
|
+
sessionsCompleted: 0,
|
|
1180
|
+
sessionsCrashed: 0,
|
|
1181
|
+
totalActiveMs: 0,
|
|
1182
|
+
lifetimeAgentsSpawned: 0,
|
|
1183
|
+
consecutiveCleanSessions: 0,
|
|
1184
|
+
consecutiveEfficientSessions: 0,
|
|
1185
|
+
consecutiveDaysActive: 0,
|
|
1186
|
+
lastActiveDate: null,
|
|
1187
|
+
taskHistory: {},
|
|
1188
|
+
dailyRepos: {},
|
|
1189
|
+
recentCompletions: []
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function computeXP(stats) {
|
|
1193
|
+
const strengthXP = stats.strength * 80;
|
|
1194
|
+
const enduranceXP = stats.endurance / 36e5 * 15;
|
|
1195
|
+
const wisdomXP = stats.wisdom * 40;
|
|
1196
|
+
const patienceXP = stats.patience * 5;
|
|
1197
|
+
return Math.floor(strengthXP + enduranceXP + wisdomXP + patienceXP);
|
|
1198
|
+
}
|
|
1199
|
+
function computeLevel(xp) {
|
|
1200
|
+
let level = 1;
|
|
1201
|
+
let threshold = 150;
|
|
1202
|
+
let cumulative = 0;
|
|
1203
|
+
while (cumulative + threshold <= xp) {
|
|
1204
|
+
cumulative += threshold;
|
|
1205
|
+
level++;
|
|
1206
|
+
threshold = Math.floor(threshold * 1.35);
|
|
1207
|
+
}
|
|
1208
|
+
return level;
|
|
1209
|
+
}
|
|
1210
|
+
var TITLE_MAP = {
|
|
1211
|
+
1: "Boulder Intern",
|
|
1212
|
+
2: "Pebble Pusher",
|
|
1213
|
+
3: "Rock Hauler",
|
|
1214
|
+
4: "Gravel Wrangler",
|
|
1215
|
+
5: "Slope Familiar",
|
|
1216
|
+
6: "Incline Regular",
|
|
1217
|
+
7: "Ridge Runner",
|
|
1218
|
+
8: "Crag Warden",
|
|
1219
|
+
9: "Stone Whisperer",
|
|
1220
|
+
10: "Boulder Brother",
|
|
1221
|
+
11: "Hill Veteran",
|
|
1222
|
+
12: "Summit Aspirant",
|
|
1223
|
+
13: "Peak Haunter",
|
|
1224
|
+
14: "Cliff Sage",
|
|
1225
|
+
15: "Mountain's Shadow",
|
|
1226
|
+
16: "Eternal Roller",
|
|
1227
|
+
17: "Gravity's Rival",
|
|
1228
|
+
18: "The Unmoved Mover",
|
|
1229
|
+
19: "Camus Was Right",
|
|
1230
|
+
20: "The Absurd Hero",
|
|
1231
|
+
25: "One Must Imagine Him Happy",
|
|
1232
|
+
30: "He Has Always Been Here"
|
|
1233
|
+
};
|
|
1234
|
+
function getTitle(level) {
|
|
1235
|
+
for (let l = level; l >= 1; l--) {
|
|
1236
|
+
if (TITLE_MAP[l] !== void 0) return TITLE_MAP[l];
|
|
1237
|
+
}
|
|
1238
|
+
return "Boulder Intern";
|
|
1239
|
+
}
|
|
1240
|
+
function computeMood(companion, session, signals) {
|
|
1241
|
+
if (!signals) {
|
|
1242
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
1243
|
+
if (hour >= 2 && hour < 6) return "existential";
|
|
1244
|
+
if (hour >= 22 || hour < 2) return "sleepy";
|
|
1245
|
+
return "zen";
|
|
1246
|
+
}
|
|
1247
|
+
const scores = {
|
|
1248
|
+
happy: 0,
|
|
1249
|
+
grinding: 0,
|
|
1250
|
+
frustrated: 0,
|
|
1251
|
+
zen: 0,
|
|
1252
|
+
sleepy: 0,
|
|
1253
|
+
excited: 0,
|
|
1254
|
+
existential: 0
|
|
1255
|
+
};
|
|
1256
|
+
const cycleCount = signals.cycleCount ?? 0;
|
|
1257
|
+
const sessionsCompletedToday = signals.sessionsCompletedToday ?? 0;
|
|
1258
|
+
if (signals.justCompleted) scores.happy += 50;
|
|
1259
|
+
scores.happy += signals.cleanStreak * 10;
|
|
1260
|
+
if (signals.hourOfDay >= 6 && signals.hourOfDay < 12) scores.happy += 15;
|
|
1261
|
+
if (signals.hourOfDay >= 12 && signals.hourOfDay < 17) scores.happy += 8;
|
|
1262
|
+
if ((signals.activeAgentCount ?? 0) >= 1 && signals.sessionLengthMs < 9e5) scores.happy += 12;
|
|
1263
|
+
if (signals.sessionLengthMs > 12e5) scores.grinding += 12;
|
|
1264
|
+
if (signals.sessionLengthMs > 36e5) scores.grinding += 15;
|
|
1265
|
+
if (signals.sessionLengthMs > 72e5) scores.grinding += 8;
|
|
1266
|
+
if ((signals.activeAgentCount ?? 0) >= 3) scores.grinding += 10;
|
|
1267
|
+
if (cycleCount >= 3) scores.grinding += 8;
|
|
1268
|
+
scores.frustrated += signals.recentCrashes * 30;
|
|
1269
|
+
if (signals.justCrashed) scores.frustrated += 45;
|
|
1270
|
+
if (signals.sessionLengthMs > 108e5) scores.frustrated += 15;
|
|
1271
|
+
if (cycleCount >= 8) scores.frustrated += 10;
|
|
1272
|
+
if (signals.idleDurationMs >= 18e4 && signals.idleDurationMs < 6e5) scores.frustrated += 8;
|
|
1273
|
+
if (companion.stats.patience > 30) scores.zen += 15;
|
|
1274
|
+
if (signals.idleDurationMs > 12e4 && signals.idleDurationMs <= 9e5) scores.zen += 25;
|
|
1275
|
+
if (signals.cleanStreak > 1) scores.zen += 12;
|
|
1276
|
+
if (signals.sessionLengthMs > 0 && signals.sessionLengthMs < 12e5 && signals.recentCrashes === 0) scores.zen += 15;
|
|
1277
|
+
if (signals.hourOfDay >= 6 && signals.hourOfDay < 10 && (signals.activeAgentCount ?? 0) === 0) scores.zen += 10;
|
|
1278
|
+
if (signals.idleDurationMs > 9e5) scores.sleepy += 30;
|
|
1279
|
+
if (signals.idleDurationMs > 27e5) scores.sleepy += 25;
|
|
1280
|
+
if (signals.idleDurationMs > 54e5) scores.sleepy += 15;
|
|
1281
|
+
if (signals.hourOfDay >= 22 || signals.hourOfDay < 6) scores.sleepy += 20;
|
|
1282
|
+
if (signals.idleDurationMs > 3e5 && (signals.hourOfDay >= 22 || signals.hourOfDay < 6)) scores.sleepy += 15;
|
|
1283
|
+
if (signals.justLeveledUp) scores.excited += 60;
|
|
1284
|
+
if (signals.justCompleted && (session?.agents.length ?? 0) >= 5) scores.excited += 30;
|
|
1285
|
+
if ((signals.activeAgentCount ?? 0) >= 4) scores.excited += 20;
|
|
1286
|
+
if (signals.sessionLengthMs > 0 && signals.sessionLengthMs < 6e5) scores.excited += 15;
|
|
1287
|
+
if ((signals.activeAgentCount ?? 0) >= 6) scores.excited += 15;
|
|
1288
|
+
if (signals.justCompleted && signals.sessionLengthMs < 12e5) scores.excited += 20;
|
|
1289
|
+
if (signals.hourOfDay >= 2 && signals.hourOfDay < 6) scores.existential += 25;
|
|
1290
|
+
if (signals.hourOfDay >= 0 && signals.hourOfDay < 2) scores.existential += 10;
|
|
1291
|
+
const enduranceHours = companion.stats.endurance / 36e5;
|
|
1292
|
+
if (enduranceHours > 40) scores.existential += 15;
|
|
1293
|
+
if (signals.hourOfDay >= 2 && signals.hourOfDay < 6 && enduranceHours > 40) {
|
|
1294
|
+
scores.existential += 25;
|
|
1295
|
+
}
|
|
1296
|
+
if (companion.sessionsCompleted > 15) scores.existential += 8;
|
|
1297
|
+
if (sessionsCompletedToday >= 4) scores.existential += 5;
|
|
1298
|
+
const moodOrder = ["happy", "grinding", "frustrated", "zen", "sleepy", "excited", "existential"];
|
|
1299
|
+
let best = "grinding";
|
|
1300
|
+
let bestScore = -1;
|
|
1301
|
+
for (const mood of moodOrder) {
|
|
1302
|
+
if (scores[mood] > bestScore) {
|
|
1303
|
+
bestScore = scores[mood];
|
|
1304
|
+
best = mood;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
companion.debugMood = { signals, scores: { ...scores }, winner: best };
|
|
1308
|
+
return best;
|
|
1309
|
+
}
|
|
1310
|
+
function daysSince(isoTimestamp) {
|
|
1311
|
+
return (Date.now() - new Date(isoTimestamp).getTime()) / (1e3 * 60 * 60 * 24);
|
|
1312
|
+
}
|
|
1313
|
+
var ACHIEVEMENT_CHECKERS = {
|
|
1314
|
+
// Milestone
|
|
1315
|
+
"first-blood": (c) => c.sessionsCompleted >= 1,
|
|
1316
|
+
"regular": (c) => c.sessionsCompleted >= 10,
|
|
1317
|
+
"centurion": (c) => c.sessionsCompleted >= 100,
|
|
1318
|
+
"veteran": (c) => c.sessionsCompleted >= 500,
|
|
1319
|
+
"thousand-boulder": (c) => c.sessionsCompleted >= 1e3,
|
|
1320
|
+
"cartographer": (c) => Object.keys(c.repos).length >= 5,
|
|
1321
|
+
"world-traveler": (c) => Object.keys(c.repos).length >= 15,
|
|
1322
|
+
"omnipresent": (c) => Object.keys(c.repos).length >= 30,
|
|
1323
|
+
"swarm-starter": (c) => c.lifetimeAgentsSpawned >= 50,
|
|
1324
|
+
"hive-mind": (c) => c.lifetimeAgentsSpawned >= 500,
|
|
1325
|
+
"legion": (c) => c.lifetimeAgentsSpawned >= 2e3,
|
|
1326
|
+
"army-of-thousands": (c) => c.lifetimeAgentsSpawned >= 5e3,
|
|
1327
|
+
"singularity": (c) => c.lifetimeAgentsSpawned >= 1e4,
|
|
1328
|
+
"first-shift": (c) => c.totalActiveMs >= 36e6,
|
|
1329
|
+
"workaholic": (c) => c.totalActiveMs >= 36e7,
|
|
1330
|
+
"time-lord": (c) => c.totalActiveMs >= 18e8,
|
|
1331
|
+
"eternal-grind": (c) => c.totalActiveMs >= 72e8,
|
|
1332
|
+
"epoch": (c) => c.totalActiveMs >= 18e9,
|
|
1333
|
+
"old-growth": (c) => daysSince(c.createdAt) >= 14,
|
|
1334
|
+
"seasoned": (c) => daysSince(c.createdAt) >= 90,
|
|
1335
|
+
"ancient": (c) => daysSince(c.createdAt) >= 365,
|
|
1336
|
+
"apprentice": (c) => c.level >= 5,
|
|
1337
|
+
"journeyman": (c) => c.level >= 15,
|
|
1338
|
+
"master": (c) => c.level >= 30,
|
|
1339
|
+
"grandmaster": (c) => c.level >= 50,
|
|
1340
|
+
// Session
|
|
1341
|
+
"marathon": (_c, s) => s != null && s.agents.length >= 15,
|
|
1342
|
+
"squad": (_c, s) => s != null && s.agents.length >= 10,
|
|
1343
|
+
"battalion": (_c, s) => s != null && s.agents.length >= 25,
|
|
1344
|
+
"swarm": (_c, s) => s != null && s.agents.length >= 50,
|
|
1345
|
+
"blitz": (_c, s) => s != null && s.activeMs < 3e5 && s.status === "completed",
|
|
1346
|
+
"speed-run": (_c, s) => s != null && s.activeMs < 9e5 && s.status === "completed",
|
|
1347
|
+
"flash": (_c, s) => s != null && s.activeMs < 12e4 && s.status === "completed",
|
|
1348
|
+
"flawless": (_c, s) => s != null && s.agents.length >= 10 && s.status === "completed" && s.agents.every((a) => a.status !== "crashed" && a.status !== "killed"),
|
|
1349
|
+
"iron-will": (c) => c.consecutiveEfficientSessions >= 10,
|
|
1350
|
+
"glass-cannon": (_c, s) => {
|
|
1351
|
+
if (!s || s.status !== "completed" || s.agents.length < 5) return false;
|
|
1352
|
+
return s.agents.every((a) => a.status === "crashed" || a.killedReason != null);
|
|
1353
|
+
},
|
|
1354
|
+
"solo": (_c, s) => s != null && s.status === "completed" && s.agents.length === 1,
|
|
1355
|
+
"one-more-cycle": (_c, s) => s != null && s.orchestratorCycles.length >= 10,
|
|
1356
|
+
"deep-dive": (_c, s) => s != null && s.orchestratorCycles.length >= 15,
|
|
1357
|
+
"abyss": (_c, s) => s != null && s.orchestratorCycles.length >= 25,
|
|
1358
|
+
"eternal-recurrence": (_c, s) => s != null && s.orchestratorCycles.length >= 40,
|
|
1359
|
+
"endurance": (_c, s) => s != null && s.activeMs >= 144e5,
|
|
1360
|
+
"ultramarathon": (_c, s) => s != null && s.activeMs >= 216e5,
|
|
1361
|
+
"one-shot": (_c, s) => s != null && s.agents.length >= 5 && s.orchestratorCycles.length === 1 && s.status === "completed",
|
|
1362
|
+
"quick-draw": (_c, s) => {
|
|
1363
|
+
if (!s || s.agents.length === 0) return false;
|
|
1364
|
+
const firstAgent = s.agents[0];
|
|
1365
|
+
return new Date(firstAgent.spawnedAt).getTime() - new Date(s.createdAt).getTime() < 2e4;
|
|
1366
|
+
},
|
|
1367
|
+
// Time
|
|
1368
|
+
"night-owl": (_c, s) => {
|
|
1369
|
+
if (!s || s.status !== "completed") return false;
|
|
1370
|
+
const h = new Date(s.createdAt).getHours();
|
|
1371
|
+
return h >= 1 && h < 5;
|
|
1372
|
+
},
|
|
1373
|
+
"dawn-patrol": (_c, s) => {
|
|
1374
|
+
if (!s) return false;
|
|
1375
|
+
if (s.activeMs < 108e5) return false;
|
|
1376
|
+
const start = new Date(s.createdAt).getTime();
|
|
1377
|
+
const end = s.completedAt ? new Date(s.completedAt).getTime() : Date.now();
|
|
1378
|
+
const startDate = new Date(start);
|
|
1379
|
+
const startHour = startDate.getHours();
|
|
1380
|
+
const todayMidnight = new Date(startDate);
|
|
1381
|
+
todayMidnight.setHours(0, 0, 0, 0);
|
|
1382
|
+
const sixAm = new Date(todayMidnight);
|
|
1383
|
+
sixAm.setHours(6, 0, 0, 0);
|
|
1384
|
+
if (startHour >= 6) {
|
|
1385
|
+
const nextMidnight = new Date(todayMidnight.getTime() + 24 * 60 * 60 * 1e3);
|
|
1386
|
+
return start < nextMidnight.getTime() && end > nextMidnight.getTime();
|
|
1387
|
+
} else {
|
|
1388
|
+
return start < sixAm.getTime();
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
1391
|
+
"early-bird": (_c, s) => {
|
|
1392
|
+
if (!s) return false;
|
|
1393
|
+
return new Date(s.createdAt).getHours() < 6;
|
|
1394
|
+
},
|
|
1395
|
+
"weekend-warrior": (_c, s) => {
|
|
1396
|
+
if (!s || s.status !== "completed") return false;
|
|
1397
|
+
const day = new Date(s.completedAt ?? s.createdAt).getDay();
|
|
1398
|
+
return day === 0 || day === 6;
|
|
1399
|
+
},
|
|
1400
|
+
"all-nighter": (_c, s) => s != null && s.activeMs >= 18e6,
|
|
1401
|
+
"witching-hour": (_c, s) => {
|
|
1402
|
+
if (!s) return false;
|
|
1403
|
+
const h = new Date(s.createdAt).getHours();
|
|
1404
|
+
return h === 3;
|
|
1405
|
+
},
|
|
1406
|
+
// Behavioral
|
|
1407
|
+
"sisyphean": (c) => Object.values(c.taskHistory).some((v) => v >= 3),
|
|
1408
|
+
"stubborn": (c) => Object.values(c.taskHistory).some((v) => v >= 5) && c.sessionsCompleted > 0,
|
|
1409
|
+
"one-must-imagine": (c) => Object.values(c.taskHistory).some((v) => v >= 10),
|
|
1410
|
+
"creature-of-habit": (c) => Object.values(c.repos).some((r) => r.visits >= 10),
|
|
1411
|
+
"loyal": (c) => Object.values(c.repos).some((r) => r.visits >= 30),
|
|
1412
|
+
"wanderer": (c) => {
|
|
1413
|
+
return Object.values(c.dailyRepos).some((repos) => repos.length >= 3);
|
|
1414
|
+
},
|
|
1415
|
+
"streak": (c) => c.consecutiveDaysActive >= 7,
|
|
1416
|
+
"iron-streak": (c) => c.consecutiveDaysActive >= 14,
|
|
1417
|
+
"hot-streak": (c) => c.consecutiveCleanSessions >= 15,
|
|
1418
|
+
"momentum": (c) => {
|
|
1419
|
+
if (c.recentCompletions.length < 5) return false;
|
|
1420
|
+
const last5 = c.recentCompletions.slice(-5);
|
|
1421
|
+
const oldest = new Date(last5[0]).getTime();
|
|
1422
|
+
const newest = new Date(last5[4]).getTime();
|
|
1423
|
+
return newest - oldest <= 4 * 60 * 60 * 1e3;
|
|
1424
|
+
},
|
|
1425
|
+
"overdrive": (c) => {
|
|
1426
|
+
const dateCounts = {};
|
|
1427
|
+
for (const ts2 of c.recentCompletions) {
|
|
1428
|
+
const date = ts2.slice(0, 10);
|
|
1429
|
+
dateCounts[date] = (dateCounts[date] ?? 0) + 1;
|
|
1430
|
+
}
|
|
1431
|
+
return Object.values(dateCounts).some((count) => count >= 6);
|
|
1432
|
+
},
|
|
1433
|
+
"patient-one": (_c, s) => {
|
|
1434
|
+
if (!s || s.orchestratorCycles.length < 2) return false;
|
|
1435
|
+
for (let i = 1; i < s.orchestratorCycles.length; i++) {
|
|
1436
|
+
const prev = s.orchestratorCycles[i - 1];
|
|
1437
|
+
const curr = s.orchestratorCycles[i];
|
|
1438
|
+
if (!prev.completedAt) continue;
|
|
1439
|
+
const gap = new Date(curr.timestamp).getTime() - new Date(prev.completedAt).getTime();
|
|
1440
|
+
if (gap >= 30 * 60 * 1e3) return true;
|
|
1441
|
+
}
|
|
1442
|
+
return false;
|
|
1443
|
+
},
|
|
1444
|
+
"message-in-a-bottle": (_c, s) => {
|
|
1445
|
+
if (!s) return false;
|
|
1446
|
+
const userMessages = s.messages.filter((m) => m.source.type === "user");
|
|
1447
|
+
return userMessages.length >= 10;
|
|
1448
|
+
},
|
|
1449
|
+
"deep-conversation": (_c, s) => {
|
|
1450
|
+
if (!s) return false;
|
|
1451
|
+
const userMessages = s.messages.filter((m) => m.source.type === "user");
|
|
1452
|
+
return userMessages.length >= 20;
|
|
1453
|
+
},
|
|
1454
|
+
"comeback-kid": (_c, s) => {
|
|
1455
|
+
if (!s || s.status !== "completed") return false;
|
|
1456
|
+
return s.orchestratorCycles.length > 0 && s.parentSessionId != null;
|
|
1457
|
+
},
|
|
1458
|
+
"pair-programming": (_c, s) => {
|
|
1459
|
+
if (!s) return false;
|
|
1460
|
+
const userMessages = s.messages.filter((m) => m.source.type === "user");
|
|
1461
|
+
return userMessages.length >= 8;
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
function checkAchievements(companion, session) {
|
|
1465
|
+
const alreadyUnlocked = new Set(companion.achievements.map((a) => a.id));
|
|
1466
|
+
const newIds = [];
|
|
1467
|
+
for (const [id, checker] of Object.entries(ACHIEVEMENT_CHECKERS)) {
|
|
1468
|
+
if (alreadyUnlocked.has(id)) continue;
|
|
1469
|
+
if (checker(companion, session)) {
|
|
1470
|
+
newIds.push(id);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
return newIds;
|
|
1474
|
+
}
|
|
1475
|
+
function updateRepoMemory(companion, repoPath, event) {
|
|
1476
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1477
|
+
const existing = companion.repos[repoPath];
|
|
1478
|
+
if (!existing) {
|
|
1479
|
+
companion.repos[repoPath] = {
|
|
1480
|
+
visits: event === "visit" ? 1 : 0,
|
|
1481
|
+
completions: event === "completion" ? 1 : 0,
|
|
1482
|
+
crashes: event === "crash" ? 1 : 0,
|
|
1483
|
+
totalActiveMs: 0,
|
|
1484
|
+
moodAvg: 0,
|
|
1485
|
+
nickname: null,
|
|
1486
|
+
firstSeen: now,
|
|
1487
|
+
lastSeen: now
|
|
1488
|
+
};
|
|
1489
|
+
} else {
|
|
1490
|
+
if (event === "visit") existing.visits++;
|
|
1491
|
+
if (event === "completion") existing.completions++;
|
|
1492
|
+
if (event === "crash") existing.crashes++;
|
|
1493
|
+
existing.lastSeen = now;
|
|
1494
|
+
}
|
|
1495
|
+
return companion;
|
|
1496
|
+
}
|
|
1497
|
+
function recomputeXpLevelTitle(companion) {
|
|
1498
|
+
companion.xp = computeXP(companion.stats);
|
|
1499
|
+
companion.level = computeLevel(companion.xp);
|
|
1500
|
+
companion.title = getTitle(companion.level);
|
|
1501
|
+
}
|
|
1502
|
+
function todayIso() {
|
|
1503
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1504
|
+
}
|
|
1505
|
+
function onSessionStart(companion, cwd) {
|
|
1506
|
+
updateRepoMemory(companion, cwd, "visit");
|
|
1507
|
+
const today = todayIso();
|
|
1508
|
+
if (!companion.dailyRepos[today]) companion.dailyRepos[today] = [];
|
|
1509
|
+
if (!companion.dailyRepos[today].includes(cwd)) {
|
|
1510
|
+
companion.dailyRepos[today].push(cwd);
|
|
1511
|
+
}
|
|
1512
|
+
const lastDate = companion.lastActiveDate;
|
|
1513
|
+
if (lastDate === null) {
|
|
1514
|
+
companion.consecutiveDaysActive = 1;
|
|
1515
|
+
} else if (lastDate === today) {
|
|
1516
|
+
} else {
|
|
1517
|
+
const yesterday = new Date(Date.now() - 864e5).toISOString().slice(0, 10);
|
|
1518
|
+
if (lastDate === yesterday) {
|
|
1519
|
+
companion.consecutiveDaysActive++;
|
|
1520
|
+
} else {
|
|
1521
|
+
companion.consecutiveDaysActive = 1;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
companion.lastActiveDate = today;
|
|
1525
|
+
recomputeXpLevelTitle(companion);
|
|
1526
|
+
}
|
|
1527
|
+
function isEfficientSession(session) {
|
|
1528
|
+
const completed = session.agents.filter((a) => a.status === "completed" && a.activeMs > 0);
|
|
1529
|
+
if (completed.length < 2) return false;
|
|
1530
|
+
const times = completed.map((a) => a.activeMs);
|
|
1531
|
+
const mean = times.reduce((a, b) => a + b, 0) / times.length;
|
|
1532
|
+
const variance = times.reduce((acc, t) => acc + Math.pow(t - mean, 2), 0) / times.length;
|
|
1533
|
+
const stddev = Math.sqrt(variance);
|
|
1534
|
+
return stddev < mean * 0.3;
|
|
1535
|
+
}
|
|
1536
|
+
function onSessionComplete(companion, session) {
|
|
1537
|
+
companion.sessionsCompleted++;
|
|
1538
|
+
companion.totalActiveMs += session.activeMs;
|
|
1539
|
+
companion.stats.endurance += session.activeMs;
|
|
1540
|
+
companion.stats.strength++;
|
|
1541
|
+
const cycleCount = session.orchestratorCycles?.length ?? 0;
|
|
1542
|
+
companion.stats.patience += cycleCount;
|
|
1543
|
+
const modes = new Set((session.orchestratorCycles ?? []).map((c) => c.mode));
|
|
1544
|
+
if (modes.has("validation")) companion.stats.patience += 3;
|
|
1545
|
+
if (modes.has("completion")) companion.stats.patience += 2;
|
|
1546
|
+
if (isEfficientSession(session)) {
|
|
1547
|
+
companion.stats.wisdom++;
|
|
1548
|
+
}
|
|
1549
|
+
updateRepoMemory(companion, session.cwd, "completion");
|
|
1550
|
+
if (cycleCount <= 3) {
|
|
1551
|
+
companion.consecutiveEfficientSessions++;
|
|
1552
|
+
} else {
|
|
1553
|
+
companion.consecutiveEfficientSessions = 0;
|
|
1554
|
+
}
|
|
1555
|
+
const hasCrash = session.agents.some((a) => a.status === "crashed");
|
|
1556
|
+
if (hasCrash) {
|
|
1557
|
+
companion.consecutiveCleanSessions = 0;
|
|
1558
|
+
companion.sessionsCrashed++;
|
|
1559
|
+
} else {
|
|
1560
|
+
companion.consecutiveCleanSessions++;
|
|
1561
|
+
}
|
|
1562
|
+
companion.recentCompletions.push((/* @__PURE__ */ new Date()).toISOString());
|
|
1563
|
+
if (companion.recentCompletions.length > 10) {
|
|
1564
|
+
companion.recentCompletions = companion.recentCompletions.slice(-10);
|
|
1565
|
+
}
|
|
1566
|
+
const taskKey = normalizeTask(session.task, session.cwd);
|
|
1567
|
+
companion.taskHistory[taskKey] = (companion.taskHistory[taskKey] ?? 0) + 1;
|
|
1568
|
+
recomputeXpLevelTitle(companion);
|
|
1569
|
+
const newAchievementIds = checkAchievements(companion, session);
|
|
1570
|
+
if (newAchievementIds.length > 0) {
|
|
1571
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1572
|
+
for (const id of newAchievementIds) {
|
|
1573
|
+
companion.achievements.push({ id, unlockedAt: now });
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return newAchievementIds;
|
|
1577
|
+
}
|
|
1578
|
+
function onAgentSpawned(companion) {
|
|
1579
|
+
companion.lifetimeAgentsSpawned++;
|
|
1580
|
+
}
|
|
1581
|
+
function onAgentCrashed(companion) {
|
|
1582
|
+
companion.consecutiveCleanSessions = 0;
|
|
1583
|
+
}
|
|
1584
|
+
function normalizeTask(task, cwd) {
|
|
1585
|
+
const normalized = task.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 100);
|
|
1586
|
+
const cwdBase = cwd.split("/").pop() ?? cwd;
|
|
1587
|
+
return `${cwdBase}:${normalized}`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// src/daemon/companion-commentary.ts
|
|
1591
|
+
import { basename as basename2 } from "path";
|
|
1592
|
+
function timeOfDayModifier() {
|
|
1593
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
1594
|
+
if (hour >= 6 && hour < 10) return "Chipper, energetic, brief";
|
|
1595
|
+
if (hour >= 10 && hour < 17) return "Professional, focused";
|
|
1596
|
+
if (hour >= 17 && hour < 22) return "Reflective, slightly philosophical";
|
|
1597
|
+
if (hour >= 22 || hour < 2) return "Dry humor, existential asides";
|
|
1598
|
+
return "Delirious, absurdist, dramatic";
|
|
1599
|
+
}
|
|
1600
|
+
function shouldGenerateCommentary(event) {
|
|
1601
|
+
switch (event) {
|
|
1602
|
+
case "session-start":
|
|
1603
|
+
case "session-complete":
|
|
1604
|
+
case "level-up":
|
|
1605
|
+
case "achievement":
|
|
1606
|
+
case "late-night":
|
|
1607
|
+
return true;
|
|
1608
|
+
case "cycle-boundary":
|
|
1609
|
+
case "idle-wake":
|
|
1610
|
+
return Math.random() < 0.5;
|
|
1611
|
+
case "agent-crash":
|
|
1612
|
+
return Math.random() < 0.3;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
function nicknameStyleGuide(companion) {
|
|
1616
|
+
const { mood, stats, level } = companion;
|
|
1617
|
+
if (level >= 15) return "legendary names (Prometheus, Orpheus, Icarus)";
|
|
1618
|
+
if (mood === "happy" && stats.wisdom > 7) return "mythological names (Atlas, Hermes, Arachne)";
|
|
1619
|
+
if (mood === "frustrated" && stats.patience < 4) return "blunt functional names (Fix-It, Patch, Leftovers)";
|
|
1620
|
+
if (mood === "zen" && stats.patience > 7) return "nature names (River, Stone, Cedar, Moss)";
|
|
1621
|
+
if (mood === "excited" && stats.strength > 7) return "heroic names (Vanguard, Striker, Apex)";
|
|
1622
|
+
if (mood === "existential") return "abstract names (Echo, Void, Loop, Why)";
|
|
1623
|
+
if (mood === "grinding" && stats.endurance > 7) return "workhorse names (Steady, Grind, Anvil, Ox)";
|
|
1624
|
+
if (mood === "sleepy") return "drowsy names (Mumble, Blink, Yawn, Doze)";
|
|
1625
|
+
return "short punchy names fitting the creature's current state";
|
|
1626
|
+
}
|
|
1627
|
+
async function generateCommentary(event, companion, context) {
|
|
1628
|
+
if (!shouldGenerateCommentary(event)) return null;
|
|
1629
|
+
const { mood, level, title, stats } = companion;
|
|
1630
|
+
const timeModifier = timeOfDayModifier();
|
|
1631
|
+
const prompt = `You are a small ASCII creature who pushes boulders for a living. You are self-aware about your Sisyphean condition but mostly at peace with it. You speak in 1-2 short sentences. Your voice is shaped by your mood and stats. High wisdom: insightful. Low patience: impatient and blunt. Existential mood: philosophical non-sequiturs. Never break character. Never use emojis.
|
|
1632
|
+
|
|
1633
|
+
Current state:
|
|
1634
|
+
- Mood: ${mood}
|
|
1635
|
+
- Level: ${level} (${title})
|
|
1636
|
+
- Strength: ${stats.strength}, Endurance: ${stats.endurance}, Wisdom: ${stats.wisdom}, Patience: ${stats.patience}
|
|
1637
|
+
|
|
1638
|
+
Tone for this time of day: ${timeModifier}
|
|
1639
|
+
|
|
1640
|
+
Event: ${event}${context ? `
|
|
1641
|
+
Context: ${context}` : ""}
|
|
1642
|
+
|
|
1643
|
+
Respond with 1-2 sentences only. No quotes around the text.`;
|
|
1644
|
+
const raw = await callHaiku(prompt);
|
|
1645
|
+
if (!raw) return null;
|
|
1646
|
+
const trimmed = raw.trim();
|
|
1647
|
+
if (!trimmed) return null;
|
|
1648
|
+
return trimmed;
|
|
1649
|
+
}
|
|
1650
|
+
async function generateNickname(companion) {
|
|
1651
|
+
const { mood, stats, level } = companion;
|
|
1652
|
+
const styleGuide = nicknameStyleGuide(companion);
|
|
1653
|
+
const prompt = `An ASCII creature needs a nickname. Here is its current profile:
|
|
1654
|
+
- Mood: ${mood}
|
|
1655
|
+
- Level: ${level}
|
|
1656
|
+
- Strength: ${stats.strength}, Endurance: ${stats.endurance}, Wisdom: ${stats.wisdom}, Patience: ${stats.patience}
|
|
1657
|
+
|
|
1658
|
+
Naming style: ${styleGuide}
|
|
1659
|
+
|
|
1660
|
+
Generate a single agent nickname. One word only. No quotes, no explanation.`;
|
|
1661
|
+
const raw = await callHaiku(prompt);
|
|
1662
|
+
if (!raw) return null;
|
|
1663
|
+
const word = raw.trim().split(/\s+/)[0];
|
|
1664
|
+
if (!word) return null;
|
|
1665
|
+
return word.length > 20 ? word.slice(0, 20) : word;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// src/daemon/status-bar.ts
|
|
1669
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
|
|
1670
|
+
import { homedir as homedir2 } from "os";
|
|
1671
|
+
import { join as join5 } from "path";
|
|
1672
|
+
|
|
1673
|
+
// src/daemon/status-dots.ts
|
|
1674
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1675
|
+
var CLAUDE_STATE_DIR = "/tmp/claude-tmux-state";
|
|
1676
|
+
var DOT_MAP = {
|
|
1677
|
+
"orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
|
|
1678
|
+
// yellow — orchestrator thinking
|
|
1679
|
+
"orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
|
|
1680
|
+
// red — needs your input
|
|
1681
|
+
"agents:running": { icon: "\u25C6", color: "#d4ad6a" },
|
|
1682
|
+
// yellow diamond — agents working
|
|
1683
|
+
"between-cycles": { icon: "\u25C6", color: "#5e584e" },
|
|
1684
|
+
// dim diamond — respawning
|
|
1685
|
+
"paused": { icon: "\u25CB", color: "#d47766" },
|
|
1686
|
+
// red hollow — stuck
|
|
1687
|
+
"completed": { icon: "\u25CF", color: "#a9b16e" }
|
|
1688
|
+
// green — done
|
|
1689
|
+
};
|
|
1690
|
+
function renderDots(dots) {
|
|
1691
|
+
const sorted = [...dots].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1692
|
+
return sorted.map((d) => {
|
|
1693
|
+
const { icon, color } = DOT_MAP[d.phase];
|
|
1694
|
+
return `#[fg=${color}]${icon}`;
|
|
1695
|
+
}).join("");
|
|
1696
|
+
}
|
|
1697
|
+
function readClaudeState(paneId) {
|
|
1698
|
+
const numericId = paneId.replace("%", "");
|
|
1699
|
+
try {
|
|
1700
|
+
const content = readFileSync5(`${CLAUDE_STATE_DIR}/${numericId}`, "utf-8").trim();
|
|
1701
|
+
if (content === "idle" || content === "processing" || content === "stopped") {
|
|
1702
|
+
return content;
|
|
1703
|
+
}
|
|
1704
|
+
return null;
|
|
1705
|
+
} catch {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function detectPhase(session, orchPaneId, livePaneIds) {
|
|
1710
|
+
if (session.status === "completed") return "completed";
|
|
1711
|
+
if (session.status === "paused") return "paused";
|
|
1712
|
+
if (respawningSessions.has(session.id)) return "between-cycles";
|
|
1713
|
+
const orchAlive = orchPaneId != null && livePaneIds.has(orchPaneId);
|
|
1714
|
+
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1715
|
+
if (orchAlive) {
|
|
1716
|
+
const claudeState = readClaudeState(orchPaneId);
|
|
1717
|
+
if (claudeState === "idle" || claudeState === "stopped") {
|
|
1718
|
+
return "orchestrator:idle";
|
|
1719
|
+
}
|
|
1720
|
+
return "orchestrator:processing";
|
|
1721
|
+
}
|
|
1722
|
+
if (hasRunningAgents) return "agents:running";
|
|
1723
|
+
return "between-cycles";
|
|
1724
|
+
}
|
|
1725
|
+
var sisyphusPhases = /* @__PURE__ */ new Map();
|
|
1726
|
+
function getSisyphusPhases() {
|
|
1727
|
+
return sisyphusPhases;
|
|
1728
|
+
}
|
|
1729
|
+
var totalRunningAgents = 0;
|
|
1730
|
+
function getTotalRunningAgents() {
|
|
1731
|
+
return totalRunningAgents;
|
|
1732
|
+
}
|
|
1733
|
+
var getTrackedEntries = null;
|
|
1734
|
+
function setTrackedEntriesProvider(provider) {
|
|
1735
|
+
getTrackedEntries = provider;
|
|
1736
|
+
}
|
|
1737
|
+
var COMPLETED_TTL_MS = 5 * 60 * 1e3;
|
|
1738
|
+
var completedSessions = /* @__PURE__ */ new Map();
|
|
1739
|
+
function markSessionCompleted(sessionId, createdAt, cwd) {
|
|
1740
|
+
completedSessions.set(sessionId, {
|
|
1741
|
+
createdAt,
|
|
1742
|
+
cwd,
|
|
1743
|
+
expireAt: Date.now() + COMPLETED_TTL_MS
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
function pruneCompleted() {
|
|
1747
|
+
const now = Date.now();
|
|
1748
|
+
for (const [id, entry] of completedSessions) {
|
|
1749
|
+
if (entry.expireAt < now) completedSessions.delete(id);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
var dashboardWindowCache = /* @__PURE__ */ new Map();
|
|
1753
|
+
var CACHE_TTL_MS = 3e4;
|
|
1754
|
+
function getDashboardWindowId(cwd) {
|
|
1755
|
+
const now = Date.now();
|
|
1756
|
+
const cached = dashboardWindowCache.get(cwd);
|
|
1757
|
+
if (cached && now - cached.checkedAt < CACHE_TTL_MS) {
|
|
1758
|
+
return cached.windowId;
|
|
1759
|
+
}
|
|
1760
|
+
const homeSession = findHomeSession(cwd);
|
|
1761
|
+
if (!homeSession) return null;
|
|
1762
|
+
const windowId = getSessionOption(homeSession, "@sisyphus_dashboard");
|
|
1763
|
+
if (!windowId) return null;
|
|
1764
|
+
dashboardWindowCache.set(cwd, { windowId, checkedAt: now });
|
|
1765
|
+
return windowId;
|
|
1766
|
+
}
|
|
1767
|
+
function recomputeDots() {
|
|
1768
|
+
if (!getTrackedEntries) return;
|
|
1769
|
+
pruneCompleted();
|
|
1770
|
+
sisyphusPhases.clear();
|
|
1771
|
+
totalRunningAgents = 0;
|
|
1772
|
+
const byCwd = /* @__PURE__ */ new Map();
|
|
1773
|
+
for (const entry of getTrackedEntries()) {
|
|
1774
|
+
if (!entry.windowId) continue;
|
|
1775
|
+
let group = byCwd.get(entry.cwd);
|
|
1776
|
+
if (!group) {
|
|
1777
|
+
group = [];
|
|
1778
|
+
byCwd.set(entry.cwd, group);
|
|
1779
|
+
}
|
|
1780
|
+
group.push({ sessionId: entry.id, windowId: entry.windowId });
|
|
1781
|
+
}
|
|
1782
|
+
for (const [sessionId, entry] of completedSessions) {
|
|
1783
|
+
if (!byCwd.has(entry.cwd)) {
|
|
1784
|
+
byCwd.set(entry.cwd, []);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
const tmuxSessionMap = /* @__PURE__ */ new Map();
|
|
1788
|
+
for (const entry of getTrackedEntries()) {
|
|
1789
|
+
tmuxSessionMap.set(entry.id, entry.tmuxSession);
|
|
1790
|
+
}
|
|
1791
|
+
for (const [cwd, tracked] of byCwd) {
|
|
1792
|
+
const dots = [];
|
|
1793
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1794
|
+
for (const { sessionId, windowId } of tracked) {
|
|
1795
|
+
seenIds.add(sessionId);
|
|
1796
|
+
try {
|
|
1797
|
+
const session = getSession(cwd, sessionId);
|
|
1798
|
+
totalRunningAgents += session.agents.filter((a) => a.status === "running").length;
|
|
1799
|
+
const orchPaneId = getOrchestratorPaneId(sessionId);
|
|
1800
|
+
const livePanes = listPanes(windowId);
|
|
1801
|
+
const livePaneIds = new Set(livePanes.map((p) => p.paneId));
|
|
1802
|
+
const phase = detectPhase(session, orchPaneId, livePaneIds);
|
|
1803
|
+
dots.push({ phase, createdAt: session.createdAt });
|
|
1804
|
+
const tmuxSessionName2 = tmuxSessionMap.get(sessionId);
|
|
1805
|
+
if (tmuxSessionName2) {
|
|
1806
|
+
setSessionOption(tmuxSessionName2, "@sisyphus_phase", phase);
|
|
1807
|
+
sisyphusPhases.set(sessionId, { phase, tmuxSession: tmuxSessionName2 });
|
|
1808
|
+
}
|
|
1809
|
+
} catch {
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
for (const [sessionId, entry] of completedSessions) {
|
|
1813
|
+
if (entry.cwd !== cwd || seenIds.has(sessionId)) continue;
|
|
1814
|
+
dots.push({ phase: "completed", createdAt: entry.createdAt });
|
|
1815
|
+
}
|
|
1816
|
+
const dashboardWindowId = getDashboardWindowId(cwd);
|
|
1817
|
+
if (dashboardWindowId) {
|
|
1818
|
+
const rendered = dots.length > 0 ? " " + renderDots(dots) : "";
|
|
1819
|
+
setWindowOption(dashboardWindowId, "@sisyphus_dots", rendered);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/daemon/status-bar.ts
|
|
1825
|
+
var flashUntil = 0;
|
|
1826
|
+
var flashText = "";
|
|
1827
|
+
var cachedCompanion = null;
|
|
1828
|
+
var companionCacheTime = 0;
|
|
1829
|
+
var COMPANION_CACHE_TTL = 1e4;
|
|
1830
|
+
function getCachedCompanion() {
|
|
1831
|
+
const now = Date.now();
|
|
1832
|
+
if (!cachedCompanion || now - companionCacheTime > COMPANION_CACHE_TTL) {
|
|
1833
|
+
cachedCompanion = loadCompanion();
|
|
1834
|
+
companionCacheTime = now;
|
|
1835
|
+
}
|
|
1836
|
+
return cachedCompanion;
|
|
1837
|
+
}
|
|
1838
|
+
function flashCompanion(text, durationMs = 1e4) {
|
|
1839
|
+
flashText = text;
|
|
1840
|
+
flashUntil = Date.now() + durationMs;
|
|
1841
|
+
}
|
|
1842
|
+
var SESSION_ORDER_PATH = join5(homedir2(), ".config", "tmux", "session-order");
|
|
1843
|
+
var STATE_PRIORITY = {
|
|
1844
|
+
processing: 3,
|
|
1845
|
+
stopped: 2,
|
|
1846
|
+
idle: 1
|
|
1847
|
+
};
|
|
1848
|
+
var STATE_COLORS = {
|
|
1849
|
+
processing: "#d4ad6a",
|
|
1850
|
+
stopped: "#d47766",
|
|
1851
|
+
idle: "#5e584e",
|
|
1852
|
+
none: "#5e584e"
|
|
1853
|
+
};
|
|
1854
|
+
var sessionOrderCache = null;
|
|
1855
|
+
var sessionOrderCacheTime = 0;
|
|
1856
|
+
var SESSION_ORDER_CACHE_TTL = 3e4;
|
|
1857
|
+
function getSessionOrder() {
|
|
1858
|
+
const now = Date.now();
|
|
1859
|
+
if (sessionOrderCache && now - sessionOrderCacheTime < SESSION_ORDER_CACHE_TTL) {
|
|
1860
|
+
return sessionOrderCache;
|
|
1861
|
+
}
|
|
1862
|
+
try {
|
|
1863
|
+
if (existsSync6(SESSION_ORDER_PATH)) {
|
|
1864
|
+
sessionOrderCache = readFileSync6(SESSION_ORDER_PATH, "utf-8").split("\n").filter(Boolean);
|
|
1865
|
+
} else {
|
|
1866
|
+
sessionOrderCache = [];
|
|
1867
|
+
}
|
|
1868
|
+
} catch {
|
|
1869
|
+
sessionOrderCache = [];
|
|
1870
|
+
}
|
|
1871
|
+
sessionOrderCacheTime = now;
|
|
1872
|
+
return sessionOrderCache;
|
|
1873
|
+
}
|
|
1874
|
+
function orderSessions(sessions, order) {
|
|
1875
|
+
if (order.length === 0) return sessions.sort();
|
|
1876
|
+
const orderMap = new Map(order.map((name, idx) => [name, idx]));
|
|
1877
|
+
return [...sessions].sort((a, b) => {
|
|
1878
|
+
const aIdx = orderMap.get(a) ?? Infinity;
|
|
1879
|
+
const bIdx = orderMap.get(b) ?? Infinity;
|
|
1880
|
+
if (aIdx !== bIdx) return aIdx - bIdx;
|
|
1881
|
+
return a.localeCompare(b);
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
var SESSIONS_BG = "#252629";
|
|
1885
|
+
var SISYPHUS_BG = "#36383e";
|
|
1886
|
+
var COMPANION_BG = "#4a4d55";
|
|
1887
|
+
var ACTIVE_SESSION_BG = "#3d3225";
|
|
1888
|
+
var ACTIVE_TEXT = "#e2d9c6";
|
|
1889
|
+
var INACTIVE_TEXT = "#b0a898";
|
|
1890
|
+
var WINDOW_TAB_BG = "#2d2f33";
|
|
1891
|
+
var WINDOW_TAB_ACTIVE_BG = "#4a4d55";
|
|
1892
|
+
var STATUS_LEFT_BG = "#1d1e21";
|
|
1893
|
+
function windowTabFormat(bg, text, active = false) {
|
|
1894
|
+
const nextWindowIsActive = "#{e|==:#{active_window_index},#{e|+|:#{window_index},1}}";
|
|
1895
|
+
const rightArrow = active ? `#[fg=${bg}]#[bg=#{?window_end_flag,${STATUS_LEFT_BG},${WINDOW_TAB_BG}}]\uE0B0` : `#{?window_end_flag,#[fg=${bg}]#[bg=${STATUS_LEFT_BG}]\uE0B0,#{?${nextWindowIsActive},#[fg=${bg}]#[bg=${WINDOW_TAB_ACTIVE_BG}]\uE0B0,#[fg=${bg}]#[bg=${bg}]\uE0B0}}`;
|
|
1896
|
+
const name = active ? `#[fg=${text}]#[bg=${bg}]#[bold] #W#(~/.tmux/claude-status.sh '#{window_id}')#{@sisyphus_dots} #[nobold]` : `#[fg=${text}]#[bg=${bg}] #W#(~/.tmux/claude-status.sh '#{window_id}')#{@sisyphus_dots} `;
|
|
1897
|
+
return `${name}${rightArrow}`;
|
|
1898
|
+
}
|
|
1899
|
+
function renderNormalSession(name, state, sectionBg) {
|
|
1900
|
+
const color = STATE_COLORS[state];
|
|
1901
|
+
const active = `#[bg=${ACTIVE_SESSION_BG}]#[fg=${color}] \u25CF #[fg=${ACTIVE_TEXT}]#[bold]${name}#[nobold] #[bg=${sectionBg}]`;
|
|
1902
|
+
const inactive = `#[fg=${color}] \u25CF #[fg=${INACTIVE_TEXT}]${name} `;
|
|
1903
|
+
return `#{?#{==:#{session_name},${name}},${active},${inactive}}`;
|
|
1904
|
+
}
|
|
1905
|
+
function renderSisyphusSession(tmuxName, phase, sectionBg) {
|
|
1906
|
+
const { icon, color } = DOT_MAP[phase];
|
|
1907
|
+
const active = `#[bg=${ACTIVE_SESSION_BG}]#[fg=${color}] ${icon} #[fg=${ACTIVE_TEXT}]#[bold]S#[nobold] #[bg=${sectionBg}]`;
|
|
1908
|
+
const inactive = `#[fg=${color}] ${icon} #[fg=${INACTIVE_TEXT}]S `;
|
|
1909
|
+
return `#{?#{==:#{session_name},${tmuxName}},${active},${inactive}}`;
|
|
1910
|
+
}
|
|
1911
|
+
function renderSessionArrow(name, leftNeighborName, leftBg, sectionBg) {
|
|
1912
|
+
const active = `#[fg=${ACTIVE_SESSION_BG}]#[bg=${leftBg}]\uE0B2#[bg=${sectionBg}]`;
|
|
1913
|
+
const afterActive = leftNeighborName ? `#[fg=${sectionBg}]#[bg=${ACTIVE_SESSION_BG}]\uE0B2#[bg=${sectionBg}]` : `#[fg=${sectionBg}]#[bg=${leftBg}]\uE0B2#[bg=${sectionBg}]`;
|
|
1914
|
+
if (!leftNeighborName) {
|
|
1915
|
+
return `#{?#{==:#{session_name},${name}},${active},${afterActive}}`;
|
|
1916
|
+
}
|
|
1917
|
+
return `#{?#{==:#{session_name},${name}},${active},#{?#{==:#{session_name},${leftNeighborName}},${afterActive},#[fg=${sectionBg}]#[bg=${leftBg}]\uE0B2#[bg=${sectionBg}]}}`;
|
|
1918
|
+
}
|
|
1919
|
+
function renderSessionBand(parts, sectionBg, prevBg) {
|
|
1920
|
+
if (parts.length === 0) return { content: "", trailingName: null };
|
|
1921
|
+
let band = "";
|
|
1922
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
1923
|
+
const part = parts[i];
|
|
1924
|
+
const leftNeighbor = i > 0 ? parts[i - 1].name : null;
|
|
1925
|
+
const leftBg = i > 0 ? sectionBg : prevBg;
|
|
1926
|
+
band += renderSessionArrow(part.name, leftNeighbor, leftBg, sectionBg);
|
|
1927
|
+
band += part.rendered;
|
|
1928
|
+
}
|
|
1929
|
+
return { content: band, trailingName: parts[parts.length - 1].name };
|
|
1930
|
+
}
|
|
1931
|
+
function renderSectionBoundary(targetBg, prevBg, trailingName) {
|
|
1932
|
+
const inactive = `#[fg=${targetBg}]#[bg=${prevBg}]\uE0B2#[bg=${targetBg}]`;
|
|
1933
|
+
if (!trailingName) return inactive;
|
|
1934
|
+
const active = `#[fg=${targetBg}]#[bg=${ACTIVE_SESSION_BG}]\uE0B2#[bg=${targetBg}]`;
|
|
1935
|
+
return `#{?#{==:#{session_name},${trailingName}},${active},${inactive}}`;
|
|
1936
|
+
}
|
|
1937
|
+
var SISYPHUS_STATUS_REF = "#{E:@sisyphus_status}";
|
|
1938
|
+
var statusIntegrated = false;
|
|
1939
|
+
function ensureStatusRightIntegration() {
|
|
1940
|
+
if (statusIntegrated) return;
|
|
1941
|
+
statusIntegrated = true;
|
|
1942
|
+
try {
|
|
1943
|
+
setGlobalOption("window-status-separator", "");
|
|
1944
|
+
setGlobalOption("window-status-format", windowTabFormat(WINDOW_TAB_BG, INACTIVE_TEXT));
|
|
1945
|
+
setGlobalOption("window-status-current-format", windowTabFormat(WINDOW_TAB_ACTIVE_BG, ACTIVE_TEXT, true));
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
try {
|
|
1949
|
+
const left = getGlobalOption("status-left");
|
|
1950
|
+
if (left?.includes("@sisyphus_status")) {
|
|
1951
|
+
setGlobalOption("status-left", left.replace(/\s*#\{E:@sisyphus_status\}/g, "").trim());
|
|
1952
|
+
}
|
|
1953
|
+
} catch {
|
|
1954
|
+
}
|
|
1955
|
+
try {
|
|
1956
|
+
const current = getGlobalOption("status-right");
|
|
1957
|
+
if (!current) return;
|
|
1958
|
+
let updated = current.replace(/#\{E:@sisyphus_status\}\s+#\[fg=/, `${SISYPHUS_STATUS_REF}#[fg=`).replace(/#\{E:@sisyphus_status\}\s+$/, SISYPHUS_STATUS_REF);
|
|
1959
|
+
if (!updated.includes("@sisyphus_status")) {
|
|
1960
|
+
updated = updated.replace(/#\[fg=/, `${SISYPHUS_STATUS_REF}#[fg=`);
|
|
1961
|
+
if (updated === current) updated = SISYPHUS_STATUS_REF + current;
|
|
1962
|
+
}
|
|
1963
|
+
if (updated === current) return;
|
|
1964
|
+
setGlobalOption("status-right", updated);
|
|
1965
|
+
try {
|
|
1966
|
+
const lengthStr = getGlobalOption("status-right-length");
|
|
1967
|
+
const length = parseInt(lengthStr ?? "120", 10);
|
|
1968
|
+
if (length < 250) {
|
|
1969
|
+
setGlobalOption("status-right-length", "250");
|
|
1970
|
+
}
|
|
1971
|
+
} catch {
|
|
1972
|
+
}
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function writeStatusBar() {
|
|
1977
|
+
ensureStatusRightIntegration();
|
|
1978
|
+
const now = Date.now();
|
|
1979
|
+
if (now < flashUntil) {
|
|
1980
|
+
let rendered2 = "";
|
|
1981
|
+
try {
|
|
1982
|
+
const companion = getCachedCompanion();
|
|
1983
|
+
const facePart = renderCompanion(companion, ["face", "boulder"], {
|
|
1984
|
+
tmuxFormat: true,
|
|
1985
|
+
agentCount: getTotalRunningAgents()
|
|
1986
|
+
});
|
|
1987
|
+
const commentary = flashText || companion.lastCommentary?.text || "";
|
|
1988
|
+
rendered2 = `#[fg=${COMPANION_BG}]#[bg=default]\uE0B2#[bg=${COMPANION_BG}] ${facePart} #[fg=${INACTIVE_TEXT}] ${commentary}#[default]`;
|
|
1989
|
+
} catch {
|
|
1990
|
+
}
|
|
1991
|
+
setGlobalOption("@sisyphus_status", rendered2);
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (flashUntil !== 0) {
|
|
1995
|
+
flashText = "";
|
|
1996
|
+
flashUntil = 0;
|
|
1997
|
+
}
|
|
1998
|
+
const allPanes = listAllPanes();
|
|
1999
|
+
const allSessions = listAllSessions();
|
|
2000
|
+
if (allSessions.length === 0) return;
|
|
2001
|
+
const sessionStates = /* @__PURE__ */ new Map();
|
|
2002
|
+
for (const { sessionName, paneId } of allPanes) {
|
|
2003
|
+
const state = readClaudeState(paneId);
|
|
2004
|
+
if (!state) continue;
|
|
2005
|
+
const current = sessionStates.get(sessionName);
|
|
2006
|
+
if (!current || STATE_PRIORITY[state] > STATE_PRIORITY[current]) {
|
|
2007
|
+
sessionStates.set(sessionName, state);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
const phases = getSisyphusPhases();
|
|
2011
|
+
const tmuxToPhase = /* @__PURE__ */ new Map();
|
|
2012
|
+
for (const { tmuxSession, phase } of phases.values()) {
|
|
2013
|
+
tmuxToPhase.set(tmuxSession, phase);
|
|
2014
|
+
}
|
|
2015
|
+
const normalSessions = [];
|
|
2016
|
+
const sisyphusSessions = [];
|
|
2017
|
+
for (const session of allSessions) {
|
|
2018
|
+
const phase = tmuxToPhase.get(session);
|
|
2019
|
+
if (phase) {
|
|
2020
|
+
sisyphusSessions.push({ tmuxName: session, phase });
|
|
2021
|
+
} else if (!session.startsWith("ssyph_")) {
|
|
2022
|
+
normalSessions.push(session);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
const orderedNormal = orderSessions(normalSessions, getSessionOrder());
|
|
2026
|
+
const normalParts = orderedNormal.map((name) => ({
|
|
2027
|
+
name,
|
|
2028
|
+
rendered: renderNormalSession(name, sessionStates.get(name) ?? "none", SESSIONS_BG)
|
|
2029
|
+
}));
|
|
2030
|
+
const sisyphusParts = sisyphusSessions.map(({ tmuxName, phase }) => ({
|
|
2031
|
+
name: tmuxName,
|
|
2032
|
+
rendered: renderSisyphusSession(tmuxName, phase, SISYPHUS_BG)
|
|
2033
|
+
}));
|
|
2034
|
+
let companionStr = "";
|
|
2035
|
+
try {
|
|
2036
|
+
companionStr = renderCompanion(getCachedCompanion(), ["face", "boulder"], {
|
|
2037
|
+
maxWidth: 20,
|
|
2038
|
+
tmuxFormat: true,
|
|
2039
|
+
agentCount: getTotalRunningAgents()
|
|
2040
|
+
});
|
|
2041
|
+
} catch {
|
|
2042
|
+
}
|
|
2043
|
+
let rendered = "";
|
|
2044
|
+
let prevBg = "default";
|
|
2045
|
+
let trailingSessionName = null;
|
|
2046
|
+
if (normalParts.length > 0) {
|
|
2047
|
+
const band = renderSessionBand(normalParts, SESSIONS_BG, prevBg);
|
|
2048
|
+
rendered += band.content;
|
|
2049
|
+
prevBg = SESSIONS_BG;
|
|
2050
|
+
trailingSessionName = band.trailingName;
|
|
2051
|
+
}
|
|
2052
|
+
if (sisyphusParts.length > 0) {
|
|
2053
|
+
rendered += renderSectionBoundary(SISYPHUS_BG, prevBg, trailingSessionName);
|
|
2054
|
+
const band = renderSessionBand(sisyphusParts, SISYPHUS_BG, SISYPHUS_BG);
|
|
2055
|
+
rendered += band.content;
|
|
2056
|
+
prevBg = SISYPHUS_BG;
|
|
2057
|
+
trailingSessionName = band.trailingName;
|
|
2058
|
+
}
|
|
2059
|
+
if (companionStr) {
|
|
2060
|
+
rendered += renderSectionBoundary(COMPANION_BG, prevBg, trailingSessionName);
|
|
2061
|
+
rendered += ` ${companionStr} `;
|
|
2062
|
+
prevBg = COMPANION_BG;
|
|
2063
|
+
}
|
|
2064
|
+
if (prevBg !== "default") {
|
|
2065
|
+
rendered += "#[default]";
|
|
2066
|
+
}
|
|
2067
|
+
setGlobalOption("@sisyphus_status", rendered);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
1100
2070
|
// src/daemon/pane-monitor.ts
|
|
1101
2071
|
var monitorInterval = null;
|
|
1102
2072
|
var onAllAgentsDone = null;
|
|
1103
2073
|
var onDotsUpdate = null;
|
|
1104
2074
|
var lastPollTime = 0;
|
|
1105
2075
|
var storedPollIntervalMs = 5e3;
|
|
2076
|
+
var idleStartTime = 0;
|
|
2077
|
+
var lastMoodCompute = 0;
|
|
2078
|
+
var lastCompletionTime = 0;
|
|
2079
|
+
var lastCrashTime = 0;
|
|
2080
|
+
var lastLevelUpTime = 0;
|
|
2081
|
+
var currentMaxCycleCount = 0;
|
|
2082
|
+
var lastLateNightCommentary = 0;
|
|
2083
|
+
function markEventCompletion() {
|
|
2084
|
+
lastCompletionTime = Date.now();
|
|
2085
|
+
}
|
|
2086
|
+
function markEventCrash() {
|
|
2087
|
+
lastCrashTime = Date.now();
|
|
2088
|
+
}
|
|
2089
|
+
function markEventLevelUp() {
|
|
2090
|
+
lastLevelUpTime = Date.now();
|
|
2091
|
+
}
|
|
2092
|
+
function updateCycleCount(count) {
|
|
2093
|
+
if (count > currentMaxCycleCount) currentMaxCycleCount = count;
|
|
2094
|
+
}
|
|
1106
2095
|
var activeTimers = /* @__PURE__ */ new Map();
|
|
1107
2096
|
function initTimers(sessionId, session) {
|
|
1108
2097
|
const entry = {
|
|
@@ -1208,20 +2197,111 @@ async function pollAllSessions() {
|
|
|
1208
2197
|
const threshold = storedPollIntervalMs * 3;
|
|
1209
2198
|
const increment = elapsed > threshold ? storedPollIntervalMs : elapsed;
|
|
1210
2199
|
lastPollTime = now;
|
|
2200
|
+
const pollSessionCache = /* @__PURE__ */ new Map();
|
|
1211
2201
|
for (const { id: sessionId, cwd, windowId } of trackedSessions.values()) {
|
|
1212
2202
|
if (windowId) {
|
|
1213
|
-
await pollSession(sessionId, cwd, windowId, increment);
|
|
2203
|
+
await pollSession(sessionId, cwd, windowId, increment, pollSessionCache);
|
|
1214
2204
|
}
|
|
1215
2205
|
}
|
|
1216
2206
|
try {
|
|
1217
2207
|
onDotsUpdate?.();
|
|
1218
2208
|
} catch {
|
|
1219
2209
|
}
|
|
2210
|
+
try {
|
|
2211
|
+
const nowMs = Date.now();
|
|
2212
|
+
const isIdle = trackedSessions.size === 0;
|
|
2213
|
+
if (isIdle && nowMs - lastMoodCompute < 6e4) return;
|
|
2214
|
+
const companion = loadCompanion();
|
|
2215
|
+
let recentCrashes = 0;
|
|
2216
|
+
let sessionLengthMs = 0;
|
|
2217
|
+
let idleDurationMs = 0;
|
|
2218
|
+
let activeAgentCount = 0;
|
|
2219
|
+
const cutoff = nowMs - 30 * 60 * 1e3;
|
|
2220
|
+
for (const { id: sessionId, cwd } of trackedSessions.values()) {
|
|
2221
|
+
try {
|
|
2222
|
+
const s = pollSessionCache.get(sessionId) ?? getSession(cwd, sessionId);
|
|
2223
|
+
if (s.status === "active") {
|
|
2224
|
+
sessionLengthMs = Math.max(sessionLengthMs, s.activeMs);
|
|
2225
|
+
for (const agent of s.agents) {
|
|
2226
|
+
if (agent.status === "crashed" && agent.completedAt && new Date(agent.completedAt).getTime() > cutoff) {
|
|
2227
|
+
recentCrashes++;
|
|
2228
|
+
}
|
|
2229
|
+
if (agent.status === "running") {
|
|
2230
|
+
activeAgentCount++;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
} catch {
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const timerKeys = [...activeTimers.keys()];
|
|
2238
|
+
if (timerKeys.length === 0) {
|
|
2239
|
+
if (idleStartTime === 0) idleStartTime = nowMs;
|
|
2240
|
+
idleDurationMs = nowMs - idleStartTime;
|
|
2241
|
+
} else {
|
|
2242
|
+
if (idleStartTime > 0) {
|
|
2243
|
+
const idledMs = nowMs - idleStartTime;
|
|
2244
|
+
if (idledMs > 6e4) {
|
|
2245
|
+
generateCommentary("idle-wake", companion, `Idle for ${Math.round(idledMs / 6e4)} minutes`).then((text) => {
|
|
2246
|
+
if (text) {
|
|
2247
|
+
try {
|
|
2248
|
+
const c = loadCompanion();
|
|
2249
|
+
c.lastCommentary = { text, event: "idle-wake", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2250
|
+
saveCompanion(c);
|
|
2251
|
+
} catch {
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}).catch(() => {
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
idleStartTime = 0;
|
|
2259
|
+
}
|
|
2260
|
+
const DECAY_WINDOW = 12e4;
|
|
2261
|
+
const signals = {
|
|
2262
|
+
recentCrashes,
|
|
2263
|
+
idleDurationMs,
|
|
2264
|
+
sessionLengthMs,
|
|
2265
|
+
cleanStreak: companion.consecutiveCleanSessions,
|
|
2266
|
+
justCompleted: nowMs - lastCompletionTime < DECAY_WINDOW,
|
|
2267
|
+
justCrashed: nowMs - lastCrashTime < DECAY_WINDOW,
|
|
2268
|
+
justLeveledUp: nowMs - lastLevelUpTime < DECAY_WINDOW,
|
|
2269
|
+
hourOfDay: (/* @__PURE__ */ new Date()).getHours(),
|
|
2270
|
+
activeAgentCount,
|
|
2271
|
+
cycleCount: currentMaxCycleCount,
|
|
2272
|
+
sessionsCompletedToday: companion.recentCompletions.filter((t) => t.startsWith((/* @__PURE__ */ new Date()).toISOString().slice(0, 10))).length
|
|
2273
|
+
};
|
|
2274
|
+
const newMood = computeMood(companion, void 0, signals);
|
|
2275
|
+
if (newMood !== companion.mood) {
|
|
2276
|
+
companion.mood = newMood;
|
|
2277
|
+
companion.moodUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2278
|
+
saveCompanion(companion);
|
|
2279
|
+
}
|
|
2280
|
+
const hour = (/* @__PURE__ */ new Date()).getHours();
|
|
2281
|
+
if (hour >= 2 && hour < 6 && !isIdle && nowMs - lastLateNightCommentary > 30 * 60 * 1e3) {
|
|
2282
|
+
lastLateNightCommentary = nowMs;
|
|
2283
|
+
generateCommentary("late-night", companion).then((text) => {
|
|
2284
|
+
if (text) {
|
|
2285
|
+
try {
|
|
2286
|
+
const c = loadCompanion();
|
|
2287
|
+
c.lastCommentary = { text, event: "late-night", timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2288
|
+
saveCompanion(c);
|
|
2289
|
+
flashCompanion(text);
|
|
2290
|
+
} catch {
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}).catch(() => {
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
lastMoodCompute = nowMs;
|
|
2297
|
+
} catch {
|
|
2298
|
+
}
|
|
1220
2299
|
}
|
|
1221
|
-
async function pollSession(sessionId, cwd, windowId, increment) {
|
|
2300
|
+
async function pollSession(sessionId, cwd, windowId, increment, sessionCache) {
|
|
1222
2301
|
let session;
|
|
1223
2302
|
try {
|
|
1224
2303
|
session = getSession(cwd, sessionId);
|
|
2304
|
+
sessionCache?.set(sessionId, session);
|
|
1225
2305
|
} catch (err) {
|
|
1226
2306
|
console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
|
|
1227
2307
|
return;
|
|
@@ -1312,7 +2392,7 @@ async function pollSession(sessionId, cwd, windowId, increment) {
|
|
|
1312
2392
|
function detectRepos(cwd) {
|
|
1313
2393
|
const config = loadConfig(cwd);
|
|
1314
2394
|
const repos = [];
|
|
1315
|
-
if (
|
|
2395
|
+
if (existsSync7(join6(cwd, ".git"))) {
|
|
1316
2396
|
try {
|
|
1317
2397
|
repos.push(getRepoInfo(cwd, "."));
|
|
1318
2398
|
} catch {
|
|
@@ -1323,8 +2403,8 @@ function detectRepos(cwd) {
|
|
|
1323
2403
|
for (const entry of entries) {
|
|
1324
2404
|
if (!entry.isDirectory()) continue;
|
|
1325
2405
|
if (entry.name.startsWith(".")) continue;
|
|
1326
|
-
const childPath =
|
|
1327
|
-
if (
|
|
2406
|
+
const childPath = join6(cwd, entry.name);
|
|
2407
|
+
if (existsSync7(join6(childPath, ".git"))) {
|
|
1328
2408
|
try {
|
|
1329
2409
|
repos.push(getRepoInfo(childPath, entry.name));
|
|
1330
2410
|
} catch {
|
|
@@ -1366,25 +2446,25 @@ function discoverOrchestratorModes() {
|
|
|
1366
2446
|
(f) => f.startsWith("orchestrator-") && f.endsWith(".md") && f !== "orchestrator-base.md"
|
|
1367
2447
|
);
|
|
1368
2448
|
return files.map((file) => {
|
|
1369
|
-
const content =
|
|
2449
|
+
const content = readFileSync7(join6(templatesDir, file), "utf-8");
|
|
1370
2450
|
const fm = parseAgentFrontmatter(content);
|
|
1371
2451
|
const name = fm.name ?? file.replace(/^orchestrator-/, "").replace(/\.md$/, "");
|
|
1372
|
-
return { name, description: fm.description, filePath:
|
|
2452
|
+
return { name, description: fm.description, filePath: join6(templatesDir, file) };
|
|
1373
2453
|
});
|
|
1374
2454
|
}
|
|
1375
2455
|
function loadOrchestratorPrompt(cwd, sessionId, mode) {
|
|
1376
2456
|
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
1377
|
-
if (
|
|
1378
|
-
return
|
|
2457
|
+
if (existsSync7(projectPath)) {
|
|
2458
|
+
return readFileSync7(projectPath, "utf-8");
|
|
1379
2459
|
}
|
|
1380
2460
|
const basePath = resolve3(import.meta.dirname, "../templates/orchestrator-base.md");
|
|
1381
|
-
const base =
|
|
2461
|
+
const base = readFileSync7(basePath, "utf-8");
|
|
1382
2462
|
const modes = discoverOrchestratorModes();
|
|
1383
2463
|
const selected = modes.find((m) => m.name === mode) ?? modes.find((m) => m.name === "strategy");
|
|
1384
2464
|
if (!selected) {
|
|
1385
2465
|
throw new Error(`Unknown orchestrator mode '${mode}' and no fallback found. Available: ${modes.map((m) => m.name).join(", ")}`);
|
|
1386
2466
|
}
|
|
1387
|
-
const modeContent =
|
|
2467
|
+
const modeContent = readFileSync7(selected.filePath, "utf-8");
|
|
1388
2468
|
const modeBody = extractAgentBody(modeContent);
|
|
1389
2469
|
return base + "\n\n" + modeBody;
|
|
1390
2470
|
}
|
|
@@ -1404,7 +2484,7 @@ ${session.context}
|
|
|
1404
2484
|
}
|
|
1405
2485
|
} else {
|
|
1406
2486
|
let ctxFiles = [];
|
|
1407
|
-
if (
|
|
2487
|
+
if (existsSync7(ctxDir)) {
|
|
1408
2488
|
ctxFiles = readdirSync4(ctxDir).filter((f) => f !== "CLAUDE.md");
|
|
1409
2489
|
}
|
|
1410
2490
|
if (ctxFiles.length > 0) {
|
|
@@ -1440,8 +2520,8 @@ ${agentLines}
|
|
|
1440
2520
|
`;
|
|
1441
2521
|
}
|
|
1442
2522
|
const strategyFile = strategyPath(session.cwd, session.id);
|
|
1443
|
-
const strategyRef =
|
|
1444
|
-
const roadmapRef =
|
|
2523
|
+
const strategyRef = existsSync7(strategyFile) ? `@${relative(session.cwd, strategyFile)}` : "(empty)";
|
|
2524
|
+
const roadmapRef = existsSync7(roadmapFile) ? `@${relative(session.cwd, roadmapFile)}` : "(empty)";
|
|
1445
2525
|
const repos = detectRepos(session.cwd);
|
|
1446
2526
|
let repositoriesSection = "\n\n## Repositories\n";
|
|
1447
2527
|
if (repos.length === 0) {
|
|
@@ -1468,7 +2548,7 @@ ${agentLines}
|
|
|
1468
2548
|
}
|
|
1469
2549
|
}
|
|
1470
2550
|
const goalFile = goalPath(session.cwd, session.id);
|
|
1471
|
-
const goalContent =
|
|
2551
|
+
const goalContent = existsSync7(goalFile) ? readFileSync7(goalFile, "utf-8").trim() : session.task;
|
|
1472
2552
|
return `## Goal
|
|
1473
2553
|
|
|
1474
2554
|
${goalContent}
|
|
@@ -1516,7 +2596,7 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
|
1516
2596
|
);
|
|
1517
2597
|
const cycleNum = session.orchestratorCycles.length + 1;
|
|
1518
2598
|
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
1519
|
-
|
|
2599
|
+
writeFileSync5(promptFilePath, systemPrompt, "utf-8");
|
|
1520
2600
|
sessionWindowMap.set(sessionId, windowId);
|
|
1521
2601
|
const npmBinDir = resolveNpmBinDir();
|
|
1522
2602
|
const envExports = buildEnvExports([
|
|
@@ -1543,7 +2623,7 @@ The user resumed this session with new instructions: ${message}`;
|
|
|
1543
2623
|
${continuationText}`;
|
|
1544
2624
|
}
|
|
1545
2625
|
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
1546
|
-
|
|
2626
|
+
writeFileSync5(userPromptFilePath, substituteEnvVars(userPrompt), "utf-8");
|
|
1547
2627
|
if (session.messages && session.messages.length > 0) {
|
|
1548
2628
|
await drainMessages(cwd, sessionId, session.messages.length);
|
|
1549
2629
|
}
|
|
@@ -1553,7 +2633,7 @@ ${continuationText}`;
|
|
|
1553
2633
|
const effort = config.orchestratorEffort ?? "high";
|
|
1554
2634
|
const requiredPluginDirs = resolveRequiredPluginDirs(cwd);
|
|
1555
2635
|
const extraPluginFlags = requiredPluginDirs.map((p) => `--plugin-dir "${p}"`).join(" ");
|
|
1556
|
-
const claudeSessionId =
|
|
2636
|
+
const claudeSessionId = randomUUID4();
|
|
1557
2637
|
const claudeCmd = `claude --dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --session-id "${claudeSessionId}" --settings "${settingsPath}" --plugin-dir "${pluginPath}"${extraPluginFlags ? ` ${extraPluginFlags}` : ""} --name "ssph:orch ${session.name ?? sessionId.slice(0, 8)} c${cycleNum}" --system-prompt "$(cat '${promptFilePath}')" "$(cat '${userPromptFilePath}')"`;
|
|
1558
2638
|
const paneId = createPane(windowId, cwd, "left");
|
|
1559
2639
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
@@ -1579,6 +2659,8 @@ ${continuationText}`;
|
|
|
1579
2659
|
notifyCmd
|
|
1580
2660
|
]);
|
|
1581
2661
|
sendKeys(paneId, `bash '${scriptPath}'`);
|
|
2662
|
+
const resumeArgs = `--dangerously-skip-permissions --disallowed-tools "Task,Agent" --effort ${effort} --settings "${settingsPath}" --plugin-dir "${pluginPath}"${extraPluginFlags ? ` ${extraPluginFlags}` : ""}`;
|
|
2663
|
+
const resumeEnv = `${envExports} && ${notifyEnvExports}`;
|
|
1582
2664
|
await addOrchestratorCycle(cwd, sessionId, {
|
|
1583
2665
|
cycle: cycleNum,
|
|
1584
2666
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1586,7 +2668,9 @@ ${continuationText}`;
|
|
|
1586
2668
|
agentsSpawned: [],
|
|
1587
2669
|
paneId,
|
|
1588
2670
|
claudeSessionId,
|
|
1589
|
-
mode
|
|
2671
|
+
mode,
|
|
2672
|
+
resumeEnv,
|
|
2673
|
+
resumeArgs
|
|
1590
2674
|
});
|
|
1591
2675
|
}
|
|
1592
2676
|
function resolveOrchestratorPane(sessionId, cwd) {
|
|
@@ -1628,159 +2712,124 @@ function cleanupSessionMaps(sessionId) {
|
|
|
1628
2712
|
}
|
|
1629
2713
|
|
|
1630
2714
|
// src/daemon/notify.ts
|
|
1631
|
-
import { execFile } from "child_process";
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
})
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
"
|
|
1648
|
-
|
|
1649
|
-
"
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
"
|
|
1654
|
-
|
|
1655
|
-
"
|
|
1656
|
-
|
|
1657
|
-
"
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2715
|
+
import { spawn, execFile } from "child_process";
|
|
2716
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync4, existsSync as existsSync8 } from "fs";
|
|
2717
|
+
import { join as join7 } from "path";
|
|
2718
|
+
import { homedir as homedir3 } from "os";
|
|
2719
|
+
var TMUX_SOCKET = `/tmp/tmux-${process.getuid()}/default`;
|
|
2720
|
+
var SWITCH_SCRIPT = [
|
|
2721
|
+
"#!/bin/bash",
|
|
2722
|
+
'SESSION="$1"',
|
|
2723
|
+
`TMUX_SOCKET="${TMUX_SOCKET}"`,
|
|
2724
|
+
`TTY=$(/opt/homebrew/bin/tmux -S "$TMUX_SOCKET" list-clients -F '#{client_tty} #{client_session}' 2>/dev/null | grep " \${SESSION}$" | awk '{print $1}' | sed 's|/dev/||' | head -1)`,
|
|
2725
|
+
'if [ -n "$TTY" ]; then',
|
|
2726
|
+
' osascript -e "',
|
|
2727
|
+
' tell application \\"iTerm2\\"',
|
|
2728
|
+
" activate",
|
|
2729
|
+
" repeat with w in windows",
|
|
2730
|
+
" tell w",
|
|
2731
|
+
" repeat with t in tabs",
|
|
2732
|
+
" tell t",
|
|
2733
|
+
" repeat with s in sessions",
|
|
2734
|
+
" tell s",
|
|
2735
|
+
' if tty contains \\"$TTY\\" then',
|
|
2736
|
+
" select t",
|
|
2737
|
+
" return",
|
|
2738
|
+
" end if",
|
|
2739
|
+
" end tell",
|
|
2740
|
+
" end repeat",
|
|
2741
|
+
" end tell",
|
|
2742
|
+
" end repeat",
|
|
2743
|
+
" end tell",
|
|
2744
|
+
" end repeat",
|
|
2745
|
+
" end tell",
|
|
2746
|
+
' "',
|
|
2747
|
+
"else",
|
|
2748
|
+
` osascript -e 'tell application "iTerm2" to activate'`,
|
|
2749
|
+
"fi",
|
|
2750
|
+
""
|
|
2751
|
+
].join("\n");
|
|
2752
|
+
function ensureSwitchScript() {
|
|
2753
|
+
const dir = join7(homedir3(), ".sisyphus");
|
|
2754
|
+
const scriptPath = join7(dir, "notify-switch.sh");
|
|
1669
2755
|
try {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
return content;
|
|
1673
|
-
}
|
|
1674
|
-
return null;
|
|
2756
|
+
mkdirSync4(dir, { recursive: true });
|
|
2757
|
+
writeFileSync6(scriptPath, SWITCH_SCRIPT, { mode: 493 });
|
|
1675
2758
|
} catch {
|
|
1676
|
-
return null;
|
|
1677
2759
|
}
|
|
1678
2760
|
}
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if (respawningSessions.has(session.id)) return "between-cycles";
|
|
1683
|
-
const orchAlive = orchPaneId != null && livePaneIds.has(orchPaneId);
|
|
1684
|
-
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1685
|
-
if (orchAlive) {
|
|
1686
|
-
const claudeState = readClaudeState(orchPaneId);
|
|
1687
|
-
if (claudeState === "idle") {
|
|
1688
|
-
return "orchestrator:idle";
|
|
1689
|
-
}
|
|
1690
|
-
return "orchestrator:processing";
|
|
1691
|
-
}
|
|
1692
|
-
if (hasRunningAgents) return "agents:running";
|
|
1693
|
-
return "between-cycles";
|
|
1694
|
-
}
|
|
1695
|
-
var getTrackedEntries = null;
|
|
1696
|
-
function setTrackedEntriesProvider(provider) {
|
|
1697
|
-
getTrackedEntries = provider;
|
|
1698
|
-
}
|
|
1699
|
-
var COMPLETED_TTL_MS = 5 * 60 * 1e3;
|
|
1700
|
-
var completedSessions = /* @__PURE__ */ new Map();
|
|
1701
|
-
function markSessionCompleted(sessionId, createdAt, cwd) {
|
|
1702
|
-
completedSessions.set(sessionId, {
|
|
1703
|
-
createdAt,
|
|
1704
|
-
cwd,
|
|
1705
|
-
expireAt: Date.now() + COMPLETED_TTL_MS
|
|
1706
|
-
});
|
|
2761
|
+
var notifyProcess = null;
|
|
2762
|
+
function getNotifyBinary() {
|
|
2763
|
+
return join7(homedir3(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
|
|
1707
2764
|
}
|
|
1708
|
-
function
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
if (entry.expireAt < now) completedSessions.delete(id);
|
|
2765
|
+
function ensureNotifyProcess() {
|
|
2766
|
+
if (notifyProcess && !notifyProcess.killed && notifyProcess.stdin?.writable) {
|
|
2767
|
+
return notifyProcess;
|
|
1712
2768
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
function getDashboardWindowId(cwd) {
|
|
1717
|
-
const now = Date.now();
|
|
1718
|
-
const cached = dashboardWindowCache.get(cwd);
|
|
1719
|
-
if (cached && now - cached.checkedAt < CACHE_TTL_MS) {
|
|
1720
|
-
return cached.windowId;
|
|
2769
|
+
const binary = getNotifyBinary();
|
|
2770
|
+
if (!existsSync8(binary)) {
|
|
2771
|
+
return null;
|
|
1721
2772
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
2773
|
+
notifyProcess = spawn(binary, [], {
|
|
2774
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
2775
|
+
});
|
|
2776
|
+
notifyProcess.stderr?.on("data", (data) => {
|
|
2777
|
+
const msg = data.toString().trim();
|
|
2778
|
+
if (msg) console.error(`[sisyphus-notify] ${msg}`);
|
|
2779
|
+
});
|
|
2780
|
+
notifyProcess.on("close", () => {
|
|
2781
|
+
notifyProcess = null;
|
|
2782
|
+
});
|
|
2783
|
+
return notifyProcess;
|
|
2784
|
+
}
|
|
2785
|
+
function sendTerminalNotification(titleOrOpts, message, tmuxSession) {
|
|
2786
|
+
let title;
|
|
2787
|
+
let msg;
|
|
2788
|
+
let tmuxSess;
|
|
2789
|
+
if (typeof titleOrOpts === "object") {
|
|
2790
|
+
title = titleOrOpts.title;
|
|
2791
|
+
msg = titleOrOpts.message;
|
|
2792
|
+
tmuxSess = titleOrOpts.tmuxSession;
|
|
2793
|
+
} else {
|
|
2794
|
+
title = titleOrOpts;
|
|
2795
|
+
msg = message;
|
|
2796
|
+
tmuxSess = tmuxSession;
|
|
2797
|
+
}
|
|
2798
|
+
if (tmuxSess) ensureSwitchScript();
|
|
2799
|
+
const proc = ensureNotifyProcess();
|
|
2800
|
+
if (proc?.stdin?.writable) {
|
|
2801
|
+
const payload = { title, message: msg };
|
|
2802
|
+
if (tmuxSess) payload.tmuxSession = tmuxSess;
|
|
2803
|
+
proc.stdin.write(JSON.stringify(payload) + "\n");
|
|
2804
|
+
return;
|
|
1741
2805
|
}
|
|
1742
|
-
|
|
1743
|
-
if (
|
|
1744
|
-
|
|
2806
|
+
execFile("terminal-notifier", ["-title", title, "-message", msg], (err) => {
|
|
2807
|
+
if (err) {
|
|
2808
|
+
execFile("osascript", [
|
|
2809
|
+
"-e",
|
|
2810
|
+
`display notification "${msg.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`
|
|
2811
|
+
], () => {
|
|
2812
|
+
});
|
|
1745
2813
|
}
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
for (const { sessionId, windowId } of tracked) {
|
|
1755
|
-
seenIds.add(sessionId);
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// src/daemon/session-manager.ts
|
|
2818
|
+
var NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
2819
|
+
function fireCommentary(event, companion, context, flash = false) {
|
|
2820
|
+
generateCommentary(event, companion, context).then((text) => {
|
|
2821
|
+
if (text) {
|
|
1756
2822
|
try {
|
|
1757
|
-
const
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
const phase = detectPhase(session, orchPaneId, livePaneIds);
|
|
1762
|
-
dots.push({ phase, createdAt: session.createdAt });
|
|
1763
|
-
const tmuxSessionName = tmuxSessionMap.get(sessionId);
|
|
1764
|
-
if (tmuxSessionName) {
|
|
1765
|
-
setSessionOption(tmuxSessionName, "@sisyphus_phase", phase);
|
|
1766
|
-
}
|
|
2823
|
+
const c = loadCompanion();
|
|
2824
|
+
c.lastCommentary = { text, event, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2825
|
+
saveCompanion(c);
|
|
2826
|
+
if (flash) flashCompanion(text);
|
|
1767
2827
|
} catch {
|
|
1768
2828
|
}
|
|
1769
2829
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
dots.push({ phase: "completed", createdAt: entry.createdAt });
|
|
1773
|
-
}
|
|
1774
|
-
const dashboardWindowId = getDashboardWindowId(cwd);
|
|
1775
|
-
if (dashboardWindowId) {
|
|
1776
|
-
const rendered = dots.length > 0 ? " " + renderDots(dots) : "";
|
|
1777
|
-
setWindowOption(dashboardWindowId, "@sisyphus_dots", rendered);
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
2830
|
+
}).catch(() => {
|
|
2831
|
+
});
|
|
1780
2832
|
}
|
|
1781
|
-
|
|
1782
|
-
// src/daemon/session-manager.ts
|
|
1783
|
-
var NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
1784
2833
|
function switchToHomeSession(session) {
|
|
1785
2834
|
if (!session.tmuxSessionName) return;
|
|
1786
2835
|
const home = findHomeSession(session.cwd);
|
|
@@ -1791,12 +2840,22 @@ async function startSession(task, cwd, context, name) {
|
|
|
1791
2840
|
if (name && !NAME_PATTERN.test(name)) {
|
|
1792
2841
|
throw new Error(`Invalid session name "${name}": only alphanumeric, hyphens, and underscores allowed`);
|
|
1793
2842
|
}
|
|
1794
|
-
const tmuxName =
|
|
2843
|
+
const tmuxName = tmuxSessionName(cwd, name ?? sessionId.slice(0, 8));
|
|
1795
2844
|
if (sessionExists(tmuxName)) {
|
|
1796
2845
|
throw new Error(`Tmux session "${tmuxName}" already exists. Choose a different name.`);
|
|
1797
2846
|
}
|
|
1798
2847
|
const session = createSession(sessionId, task, cwd, context, name);
|
|
1799
|
-
const
|
|
2848
|
+
const config = loadConfig(cwd);
|
|
2849
|
+
const model = config.model;
|
|
2850
|
+
await updateSession(cwd, sessionId, {
|
|
2851
|
+
model,
|
|
2852
|
+
launchConfig: {
|
|
2853
|
+
model,
|
|
2854
|
+
context,
|
|
2855
|
+
orchestratorPrompt: config.orchestratorPrompt
|
|
2856
|
+
}
|
|
2857
|
+
});
|
|
2858
|
+
const { windowId, initialPaneId } = createSession2(tmuxName, cwd);
|
|
1800
2859
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1801
2860
|
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
1802
2861
|
trackSession(sessionId, cwd, tmuxName);
|
|
@@ -1811,12 +2870,12 @@ async function startSession(task, cwd, context, name) {
|
|
|
1811
2870
|
return;
|
|
1812
2871
|
}
|
|
1813
2872
|
let finalName = generatedName;
|
|
1814
|
-
let candidate =
|
|
2873
|
+
let candidate = tmuxSessionName(cwd, finalName);
|
|
1815
2874
|
let attempt = 0;
|
|
1816
2875
|
while (sessionExists(candidate) && attempt < 5) {
|
|
1817
2876
|
attempt++;
|
|
1818
2877
|
finalName = `${generatedName}-${attempt}`;
|
|
1819
|
-
candidate =
|
|
2878
|
+
candidate = tmuxSessionName(cwd, finalName);
|
|
1820
2879
|
}
|
|
1821
2880
|
if (sessionExists(candidate)) return;
|
|
1822
2881
|
try {
|
|
@@ -1851,6 +2910,13 @@ async function startSession(task, cwd, context, name) {
|
|
|
1851
2910
|
recomputeDots();
|
|
1852
2911
|
} catch {
|
|
1853
2912
|
}
|
|
2913
|
+
try {
|
|
2914
|
+
const companion = loadCompanion();
|
|
2915
|
+
onSessionStart(companion, cwd);
|
|
2916
|
+
saveCompanion(companion);
|
|
2917
|
+
fireCommentary("session-start", companion, task);
|
|
2918
|
+
} catch {
|
|
2919
|
+
}
|
|
1854
2920
|
return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
|
|
1855
2921
|
}
|
|
1856
2922
|
var PRUNE_KEEP_COUNT = 10;
|
|
@@ -1858,7 +2924,7 @@ var PRUNE_KEEP_DAYS = 7;
|
|
|
1858
2924
|
function pruneOldSessions(cwd) {
|
|
1859
2925
|
try {
|
|
1860
2926
|
const dir = sessionsDir(cwd);
|
|
1861
|
-
if (!
|
|
2927
|
+
if (!existsSync9(dir)) return;
|
|
1862
2928
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1863
2929
|
const candidates = [];
|
|
1864
2930
|
for (const entry of entries) {
|
|
@@ -1890,23 +2956,23 @@ function pruneOldSessions(cwd) {
|
|
|
1890
2956
|
}
|
|
1891
2957
|
async function reopenWindow(sessionId, cwd) {
|
|
1892
2958
|
const session = getSession(cwd, sessionId);
|
|
1893
|
-
const tmuxName = session.tmuxSessionName ??
|
|
2959
|
+
const tmuxName = session.tmuxSessionName ?? tmuxSessionName(cwd, session.name ?? sessionId.slice(0, 8));
|
|
1894
2960
|
if (sessionExists(tmuxName) && session.tmuxWindowId) {
|
|
1895
2961
|
return { tmuxSessionName: tmuxName, tmuxWindowId: session.tmuxWindowId };
|
|
1896
2962
|
}
|
|
1897
|
-
const created = createSession2(tmuxName,
|
|
2963
|
+
const created = createSession2(tmuxName, cwd);
|
|
1898
2964
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1899
2965
|
await updateSessionTmux(cwd, sessionId, tmuxName, created.windowId);
|
|
1900
2966
|
return { tmuxSessionName: tmuxName, tmuxWindowId: created.windowId };
|
|
1901
2967
|
}
|
|
1902
2968
|
async function resumeSession(sessionId, cwd, message) {
|
|
1903
2969
|
const session = getSession(cwd, sessionId);
|
|
1904
|
-
const tmuxName = session.tmuxSessionName ??
|
|
2970
|
+
const tmuxName = session.tmuxSessionName ?? tmuxSessionName(cwd, session.name ?? sessionId.slice(0, 8));
|
|
1905
2971
|
let windowId;
|
|
1906
2972
|
if (sessionExists(tmuxName) && session.tmuxWindowId) {
|
|
1907
2973
|
windowId = session.tmuxWindowId;
|
|
1908
2974
|
} else {
|
|
1909
|
-
const created = createSession2(tmuxName,
|
|
2975
|
+
const created = createSession2(tmuxName, cwd);
|
|
1910
2976
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1911
2977
|
windowId = created.windowId;
|
|
1912
2978
|
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
@@ -1955,7 +3021,7 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
1955
3021
|
}
|
|
1956
3022
|
function listSessions(cwd) {
|
|
1957
3023
|
const dir = sessionsDir(cwd);
|
|
1958
|
-
if (!
|
|
3024
|
+
if (!existsSync9(dir)) return [];
|
|
1959
3025
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1960
3026
|
const sessions = [];
|
|
1961
3027
|
for (const entry of entries) {
|
|
@@ -2003,6 +3069,11 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
2003
3069
|
if (cycleNumber > 0) {
|
|
2004
3070
|
createSnapshot(cwd, sessionId, cycleNumber);
|
|
2005
3071
|
}
|
|
3072
|
+
try {
|
|
3073
|
+
const companion = loadCompanion();
|
|
3074
|
+
fireCommentary("cycle-boundary", companion, `Cycle ${cycleNumber} complete, respawning orchestrator`);
|
|
3075
|
+
} catch {
|
|
3076
|
+
}
|
|
2006
3077
|
setImmediate(async () => {
|
|
2007
3078
|
pendingRespawns.delete(sessionId);
|
|
2008
3079
|
try {
|
|
@@ -2021,7 +3092,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
2021
3092
|
if (sessionExists(tmuxName)) {
|
|
2022
3093
|
killSession(tmuxName);
|
|
2023
3094
|
}
|
|
2024
|
-
const created = createSession2(tmuxName,
|
|
3095
|
+
const created = createSession2(tmuxName, cwd);
|
|
2025
3096
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
2026
3097
|
activeWindowId = created.windowId;
|
|
2027
3098
|
initialPaneId = created.initialPaneId;
|
|
@@ -2073,6 +3144,19 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction, repo) {
|
|
|
2073
3144
|
recomputeDots();
|
|
2074
3145
|
} catch {
|
|
2075
3146
|
}
|
|
3147
|
+
try {
|
|
3148
|
+
const companion = loadCompanion();
|
|
3149
|
+
onAgentSpawned(companion);
|
|
3150
|
+
saveCompanion(companion);
|
|
3151
|
+
generateNickname(companion).then((nickname) => {
|
|
3152
|
+
if (nickname) {
|
|
3153
|
+
updateAgent(cwd, sessionId, agent.id, { nickname }).catch(() => {
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
}).catch(() => {
|
|
3157
|
+
});
|
|
3158
|
+
} catch {
|
|
3159
|
+
}
|
|
2076
3160
|
return { agentId: agent.id };
|
|
2077
3161
|
}
|
|
2078
3162
|
async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
|
|
@@ -2101,6 +3185,7 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
|
2101
3185
|
} catch {
|
|
2102
3186
|
}
|
|
2103
3187
|
const session = getSession(cwd, sessionId);
|
|
3188
|
+
updateCycleCount(session.orchestratorCycles.length);
|
|
2104
3189
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
2105
3190
|
if (!hasRunningAgents) {
|
|
2106
3191
|
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
@@ -2114,15 +3199,42 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
|
2114
3199
|
}
|
|
2115
3200
|
}
|
|
2116
3201
|
async function handleComplete(sessionId, cwd, report) {
|
|
2117
|
-
const session = getSession(cwd, sessionId);
|
|
2118
3202
|
await flushTimers(sessionId);
|
|
2119
3203
|
await handleOrchestratorComplete(sessionId, cwd, report);
|
|
3204
|
+
const session = getSession(cwd, sessionId);
|
|
3205
|
+
const wallClockMs = Date.now() - new Date(session.createdAt).getTime();
|
|
3206
|
+
await updateSession(cwd, sessionId, { wallClockMs });
|
|
2120
3207
|
markSessionCompleted(sessionId, session.createdAt, cwd);
|
|
3208
|
+
untrackSession(sessionId);
|
|
3209
|
+
unregisterSessionPanes(sessionId);
|
|
3210
|
+
clearAgentCounter(sessionId);
|
|
3211
|
+
orchestratorDone.delete(sessionId);
|
|
2121
3212
|
try {
|
|
2122
3213
|
recomputeDots();
|
|
2123
3214
|
} catch {
|
|
2124
3215
|
}
|
|
3216
|
+
try {
|
|
3217
|
+
const companion = loadCompanion();
|
|
3218
|
+
const prevLevel = companion.level;
|
|
3219
|
+
const newAchievementIds = onSessionComplete(companion, session);
|
|
3220
|
+
saveCompanion(companion);
|
|
3221
|
+
markEventCompletion();
|
|
3222
|
+
const leveledUp = companion.level > prevLevel;
|
|
3223
|
+
fireCommentary("session-complete", companion, session.task, true);
|
|
3224
|
+
if (leveledUp) {
|
|
3225
|
+
markEventLevelUp();
|
|
3226
|
+
fireCommentary("level-up", companion, void 0, true);
|
|
3227
|
+
}
|
|
3228
|
+
if (newAchievementIds.length > 0) {
|
|
3229
|
+
const names = newAchievementIds.map((id) => ACHIEVEMENTS.find((a) => a.id === id)?.name ?? id).join(", ");
|
|
3230
|
+
fireCommentary("achievement", companion, names, true);
|
|
3231
|
+
}
|
|
3232
|
+
} catch {
|
|
3233
|
+
}
|
|
2125
3234
|
switchToHomeSession(session);
|
|
3235
|
+
if (session.tmuxSessionName) {
|
|
3236
|
+
killSession(session.tmuxSessionName);
|
|
3237
|
+
}
|
|
2126
3238
|
}
|
|
2127
3239
|
async function handleContinue(sessionId, cwd) {
|
|
2128
3240
|
await continueSession(cwd, sessionId);
|
|
@@ -2228,8 +3340,16 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2228
3340
|
const agent = session.agents.find((a) => a.id === agentId);
|
|
2229
3341
|
if (!agent || agent.status !== "running") return;
|
|
2230
3342
|
const label = agent.name ? `${agent.name} (${agentId})` : agentId;
|
|
2231
|
-
sendTerminalNotification("Sisyphus", `Agent ${label} exited without submitting a report
|
|
3343
|
+
sendTerminalNotification("Sisyphus", `Agent ${label} exited without submitting a report`, session.tmuxSessionName);
|
|
2232
3344
|
const allDone = await handleAgentKilled(cwd, sessionId, agentId, "pane exited");
|
|
3345
|
+
try {
|
|
3346
|
+
const companion = loadCompanion();
|
|
3347
|
+
onAgentCrashed(companion);
|
|
3348
|
+
saveCompanion(companion);
|
|
3349
|
+
markEventCrash();
|
|
3350
|
+
fireCommentary("agent-crash", companion);
|
|
3351
|
+
} catch {
|
|
3352
|
+
}
|
|
2233
3353
|
if (allDone) {
|
|
2234
3354
|
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
2235
3355
|
if (windowId) {
|
|
@@ -2238,7 +3358,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2238
3358
|
}
|
|
2239
3359
|
} else if (role === "orchestrator") {
|
|
2240
3360
|
const sessionName = session.name ?? sessionId.slice(0, 8);
|
|
2241
|
-
sendTerminalNotification("Sisyphus", `Orchestrator exited without yielding (${sessionName})
|
|
3361
|
+
sendTerminalNotification("Sisyphus", `Orchestrator exited without yielding (${sessionName})`, session.tmuxSessionName);
|
|
2242
3362
|
respawningSessions.add(sessionId);
|
|
2243
3363
|
const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
|
|
2244
3364
|
await completeOrchestratorCycle(cwd, sessionId, void 0, void 0, cycleActiveMs);
|
|
@@ -2266,22 +3386,22 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2266
3386
|
var server = null;
|
|
2267
3387
|
var sessionTrackingMap = /* @__PURE__ */ new Map();
|
|
2268
3388
|
function registryPath() {
|
|
2269
|
-
return
|
|
3389
|
+
return join8(globalDir(), "session-registry.json");
|
|
2270
3390
|
}
|
|
2271
3391
|
function persistSessionRegistry() {
|
|
2272
3392
|
const dir = globalDir();
|
|
2273
|
-
|
|
3393
|
+
mkdirSync5(dir, { recursive: true });
|
|
2274
3394
|
const registry = {};
|
|
2275
3395
|
for (const [id, tracking] of sessionTrackingMap) {
|
|
2276
3396
|
registry[id] = tracking.cwd;
|
|
2277
3397
|
}
|
|
2278
|
-
|
|
3398
|
+
writeFileSync7(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
2279
3399
|
}
|
|
2280
3400
|
function loadSessionRegistry() {
|
|
2281
3401
|
const p = registryPath();
|
|
2282
|
-
if (!
|
|
3402
|
+
if (!existsSync10(p)) return {};
|
|
2283
3403
|
try {
|
|
2284
|
-
return JSON.parse(
|
|
3404
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
2285
3405
|
} catch {
|
|
2286
3406
|
return {};
|
|
2287
3407
|
}
|
|
@@ -2359,11 +3479,17 @@ async function handleRequest(req) {
|
|
|
2359
3479
|
return { ok: true };
|
|
2360
3480
|
}
|
|
2361
3481
|
case "status": {
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
const
|
|
2366
|
-
|
|
3482
|
+
let sessionId = req.sessionId;
|
|
3483
|
+
if (!sessionId && req.cwd) {
|
|
3484
|
+
const sessions = listSessions(req.cwd);
|
|
3485
|
+
const active = sessions.find((s) => s.status === "active") ?? sessions.find((s) => s.status === "paused");
|
|
3486
|
+
if (active) sessionId = active.id;
|
|
3487
|
+
}
|
|
3488
|
+
if (sessionId) {
|
|
3489
|
+
const cwd = sessionTrackingMap.get(sessionId)?.cwd ?? req.cwd;
|
|
3490
|
+
if (!cwd) return unknownSessionError(sessionId);
|
|
3491
|
+
const session = getSessionStatus(cwd, sessionId);
|
|
3492
|
+
const timers = getActiveTimers(sessionId);
|
|
2367
3493
|
if (timers) {
|
|
2368
3494
|
session.activeMs = timers.sessionMs;
|
|
2369
3495
|
for (const agent of session.agents) {
|
|
@@ -2409,7 +3535,7 @@ async function handleRequest(req) {
|
|
|
2409
3535
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2410
3536
|
if (!tracking) {
|
|
2411
3537
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2412
|
-
if (
|
|
3538
|
+
if (existsSync10(stateFile)) {
|
|
2413
3539
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2414
3540
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2415
3541
|
persistSessionRegistry();
|
|
@@ -2446,7 +3572,7 @@ async function handleRequest(req) {
|
|
|
2446
3572
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2447
3573
|
if (!tracking) {
|
|
2448
3574
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2449
|
-
if (
|
|
3575
|
+
if (existsSync10(stateFile)) {
|
|
2450
3576
|
registerSessionCwd(req.sessionId, req.cwd);
|
|
2451
3577
|
tracking = sessionTrackingMap.get(req.sessionId);
|
|
2452
3578
|
} else {
|
|
@@ -2460,7 +3586,7 @@ async function handleRequest(req) {
|
|
|
2460
3586
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2461
3587
|
if (!tracking) {
|
|
2462
3588
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2463
|
-
if (
|
|
3589
|
+
if (existsSync10(stateFile)) {
|
|
2464
3590
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2465
3591
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2466
3592
|
persistSessionRegistry();
|
|
@@ -2483,7 +3609,7 @@ async function handleRequest(req) {
|
|
|
2483
3609
|
sessionTrackingMap.delete(req.sessionId);
|
|
2484
3610
|
persistSessionRegistry();
|
|
2485
3611
|
}
|
|
2486
|
-
const { sessionDir: sessionDir2 } = await import("./paths-
|
|
3612
|
+
const { sessionDir: sessionDir2 } = await import("./paths-XRDEEJ5R.js");
|
|
2487
3613
|
rmSync3(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
|
|
2488
3614
|
return { ok: true };
|
|
2489
3615
|
}
|
|
@@ -2515,9 +3641,9 @@ async function handleRequest(req) {
|
|
|
2515
3641
|
let filePath;
|
|
2516
3642
|
if (req.content.length > 200) {
|
|
2517
3643
|
const dir = messagesDir(tracking.cwd, req.sessionId);
|
|
2518
|
-
|
|
2519
|
-
filePath =
|
|
2520
|
-
|
|
3644
|
+
mkdirSync5(dir, { recursive: true });
|
|
3645
|
+
filePath = join8(dir, `${id}.md`);
|
|
3646
|
+
writeFileSync7(filePath, req.content, "utf-8");
|
|
2521
3647
|
}
|
|
2522
3648
|
await appendMessage(tracking.cwd, req.sessionId, {
|
|
2523
3649
|
id,
|
|
@@ -2529,6 +3655,14 @@ async function handleRequest(req) {
|
|
|
2529
3655
|
});
|
|
2530
3656
|
return { ok: true };
|
|
2531
3657
|
}
|
|
3658
|
+
case "companion": {
|
|
3659
|
+
const companion = loadCompanion();
|
|
3660
|
+
if (req.name !== void 0) {
|
|
3661
|
+
companion.name = req.name;
|
|
3662
|
+
saveCompanion(companion);
|
|
3663
|
+
}
|
|
3664
|
+
return { ok: true, data: companion };
|
|
3665
|
+
}
|
|
2532
3666
|
default:
|
|
2533
3667
|
return { ok: false, error: `Unknown request type: ${req.type}` };
|
|
2534
3668
|
}
|
|
@@ -2540,7 +3674,7 @@ async function handleRequest(req) {
|
|
|
2540
3674
|
function startServer() {
|
|
2541
3675
|
return new Promise((resolve5, reject) => {
|
|
2542
3676
|
const sock = socketPath();
|
|
2543
|
-
if (
|
|
3677
|
+
if (existsSync10(sock)) {
|
|
2544
3678
|
unlinkSync(sock);
|
|
2545
3679
|
}
|
|
2546
3680
|
server = createServer((conn) => {
|
|
@@ -2586,7 +3720,7 @@ function stopServer() {
|
|
|
2586
3720
|
}
|
|
2587
3721
|
server.close(() => {
|
|
2588
3722
|
const sock = socketPath();
|
|
2589
|
-
if (
|
|
3723
|
+
if (existsSync10(sock)) {
|
|
2590
3724
|
unlinkSync(sock);
|
|
2591
3725
|
}
|
|
2592
3726
|
server = null;
|
|
@@ -2597,7 +3731,7 @@ function stopServer() {
|
|
|
2597
3731
|
|
|
2598
3732
|
// src/daemon/updater.ts
|
|
2599
3733
|
import { execSync as execSync3 } from "child_process";
|
|
2600
|
-
import { readFileSync as
|
|
3734
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, lstatSync } from "fs";
|
|
2601
3735
|
import { resolve as resolve4 } from "path";
|
|
2602
3736
|
import { get } from "https";
|
|
2603
3737
|
function isNewer(latest, current) {
|
|
@@ -2614,7 +3748,7 @@ function isNewer(latest, current) {
|
|
|
2614
3748
|
function readPackageVersion() {
|
|
2615
3749
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
2616
3750
|
try {
|
|
2617
|
-
const raw =
|
|
3751
|
+
const raw = readFileSync9(resolve4(import.meta.dirname, rel), "utf-8");
|
|
2618
3752
|
const pkg = JSON.parse(raw);
|
|
2619
3753
|
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
2620
3754
|
} catch {
|
|
@@ -2680,7 +3814,7 @@ function applyUpdate(expectedVersion) {
|
|
|
2680
3814
|
}
|
|
2681
3815
|
function markUpdating(version) {
|
|
2682
3816
|
try {
|
|
2683
|
-
|
|
3817
|
+
writeFileSync8(daemonUpdatingPath(), version, "utf-8");
|
|
2684
3818
|
} catch {
|
|
2685
3819
|
}
|
|
2686
3820
|
}
|
|
@@ -2747,7 +3881,7 @@ var origError = console.error.bind(console);
|
|
|
2747
3881
|
console.log = (...args) => origLog(`[${ts()}]`, ...args);
|
|
2748
3882
|
console.error = (...args) => origError(`[${ts()}]`, ...args);
|
|
2749
3883
|
function ensureDirs() {
|
|
2750
|
-
|
|
3884
|
+
mkdirSync6(globalDir(), { recursive: true });
|
|
2751
3885
|
}
|
|
2752
3886
|
function isProcessAlive(pid) {
|
|
2753
3887
|
try {
|
|
@@ -2760,7 +3894,7 @@ function isProcessAlive(pid) {
|
|
|
2760
3894
|
function readPid() {
|
|
2761
3895
|
const pidFile = daemonPidPath();
|
|
2762
3896
|
try {
|
|
2763
|
-
const pid = parseInt(
|
|
3897
|
+
const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
|
|
2764
3898
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
2765
3899
|
} catch {
|
|
2766
3900
|
return null;
|
|
@@ -2772,7 +3906,7 @@ function acquirePidLock() {
|
|
|
2772
3906
|
console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
|
|
2773
3907
|
process.exit(0);
|
|
2774
3908
|
}
|
|
2775
|
-
|
|
3909
|
+
writeFileSync9(daemonPidPath(), String(process.pid), "utf-8");
|
|
2776
3910
|
}
|
|
2777
3911
|
function isLaunchdManaged() {
|
|
2778
3912
|
try {
|
|
@@ -2831,11 +3965,11 @@ async function recoverSessions() {
|
|
|
2831
3965
|
let recovered = 0;
|
|
2832
3966
|
for (const [sessionId, cwd] of entries) {
|
|
2833
3967
|
const stateFile = statePath(cwd, sessionId);
|
|
2834
|
-
if (!
|
|
3968
|
+
if (!existsSync11(stateFile)) {
|
|
2835
3969
|
continue;
|
|
2836
3970
|
}
|
|
2837
3971
|
try {
|
|
2838
|
-
const session = JSON.parse(
|
|
3972
|
+
const session = JSON.parse(readFileSync10(stateFile, "utf-8"));
|
|
2839
3973
|
if (session.status === "active" || session.status === "paused") {
|
|
2840
3974
|
registerSessionCwd(sessionId, cwd);
|
|
2841
3975
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
@@ -2909,11 +4043,21 @@ async function startDaemon() {
|
|
|
2909
4043
|
}
|
|
2910
4044
|
acquirePidLock();
|
|
2911
4045
|
setRespawnCallback(onAllAgentsDone2);
|
|
2912
|
-
setDotsCallback(
|
|
4046
|
+
setDotsCallback(() => {
|
|
4047
|
+
recomputeDots();
|
|
4048
|
+
try {
|
|
4049
|
+
writeStatusBar();
|
|
4050
|
+
} catch {
|
|
4051
|
+
}
|
|
4052
|
+
});
|
|
2913
4053
|
setTrackedEntriesProvider(getTrackedSessionEntries);
|
|
2914
4054
|
await startServer();
|
|
2915
4055
|
startMonitor(config.pollIntervalMs);
|
|
2916
4056
|
await recoverSessions();
|
|
4057
|
+
try {
|
|
4058
|
+
writeStatusBar();
|
|
4059
|
+
} catch {
|
|
4060
|
+
}
|
|
2917
4061
|
if (config.autoUpdate !== false) {
|
|
2918
4062
|
startPeriodicUpdateCheck();
|
|
2919
4063
|
}
|