sisyphi 1.1.16 → 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 +166 -8
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +1464 -129
- 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
|
}
|
|
@@ -632,6 +647,35 @@ function updatePaneMeta(paneTarget, updates) {
|
|
|
632
647
|
function selectLayout(windowTarget, layout = "even-horizontal") {
|
|
633
648
|
execSafe(`tmux select-layout -t "${windowTarget}" ${layout}`);
|
|
634
649
|
}
|
|
650
|
+
function setWindowOption(windowTarget, option, value) {
|
|
651
|
+
execSafe(`tmux set-option -w -t "${windowTarget}" ${option} ${shellQuote(value)}`);
|
|
652
|
+
}
|
|
653
|
+
function getSessionOption(sessionName, option) {
|
|
654
|
+
return execSafe(`tmux show-options -t "${sessionName}" -v ${option}`);
|
|
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
|
+
}
|
|
635
679
|
function configureSessionDefaults(sessionName, windowId) {
|
|
636
680
|
execSafe(`tmux set -w -t "${windowId}" pane-border-status top`);
|
|
637
681
|
execSafe(`tmux set -w -t "${windowId}" allow-rename off`);
|
|
@@ -682,17 +726,15 @@ import { execSync } from "child_process";
|
|
|
682
726
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
683
727
|
import { resolve as resolve2, dirname as dirname2, join as join3 } from "path";
|
|
684
728
|
|
|
685
|
-
// src/daemon/
|
|
729
|
+
// src/daemon/haiku.ts
|
|
686
730
|
import { query } from "@r-cli/sdk";
|
|
687
731
|
var COOLDOWN_MS = 5 * 60 * 1e3;
|
|
688
732
|
var disabledUntil = 0;
|
|
689
|
-
async function
|
|
733
|
+
async function callHaiku(prompt) {
|
|
690
734
|
if (Date.now() < disabledUntil) return null;
|
|
691
735
|
try {
|
|
692
736
|
const session = await query({
|
|
693
|
-
prompt
|
|
694
|
-
|
|
695
|
-
${task.slice(0, 500)}`,
|
|
737
|
+
prompt,
|
|
696
738
|
options: {
|
|
697
739
|
model: "haiku",
|
|
698
740
|
maxTurns: 1,
|
|
@@ -707,11 +749,9 @@ ${task.slice(0, 500)}`,
|
|
|
707
749
|
}
|
|
708
750
|
}
|
|
709
751
|
}
|
|
710
|
-
|
|
711
|
-
if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) return null;
|
|
712
|
-
return name.slice(0, 30);
|
|
752
|
+
return text.trim() || null;
|
|
713
753
|
} catch (err) {
|
|
714
|
-
console.error(`[sisyphus] Haiku
|
|
754
|
+
console.error(`[sisyphus] Haiku call failed: ${err instanceof Error ? err.message : err}`);
|
|
715
755
|
const status = err.status;
|
|
716
756
|
if (status === 401 || status === 403) {
|
|
717
757
|
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
@@ -719,37 +759,27 @@ ${task.slice(0, 500)}`,
|
|
|
719
759
|
return null;
|
|
720
760
|
}
|
|
721
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
|
+
}
|
|
722
775
|
async function summarizeReport(reportText) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const session = await query({
|
|
726
|
-
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.
|
|
727
778
|
|
|
728
|
-
${reportText.slice(0, 3e3)}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
env: execEnv()
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
let text = "";
|
|
736
|
-
for await (const msg of session) {
|
|
737
|
-
if (msg.type === "assistant" && msg.message?.content) {
|
|
738
|
-
for (const block of msg.message.content) {
|
|
739
|
-
if (block.type === "text") text += block.text;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
const summary = text.trim();
|
|
744
|
-
return summary.length > 0 ? summary : null;
|
|
745
|
-
} catch (err) {
|
|
746
|
-
console.error(`[sisyphus] Haiku summarization failed: ${err instanceof Error ? err.message : err}`);
|
|
747
|
-
const status = err.status;
|
|
748
|
-
if (status === 401 || status === 403) {
|
|
749
|
-
disabledUntil = Date.now() + COOLDOWN_MS;
|
|
750
|
-
}
|
|
751
|
-
return null;
|
|
752
|
-
}
|
|
779
|
+
${reportText.slice(0, 3e3)}`
|
|
780
|
+
);
|
|
781
|
+
if (!text) return null;
|
|
782
|
+
return text.length > 0 ? text : null;
|
|
753
783
|
}
|
|
754
784
|
|
|
755
785
|
// src/daemon/agent.ts
|
|
@@ -860,6 +890,7 @@ function setupAgentPane(opts) {
|
|
|
860
890
|
]);
|
|
861
891
|
const notifyCmd = buildNotifyCmd(paneId);
|
|
862
892
|
let mainCmd;
|
|
893
|
+
let resumeArgs;
|
|
863
894
|
if (provider === "openai") {
|
|
864
895
|
const codexPromptPath = `${promptsDir(cwd, sessionId)}/${agentId}-codex-prompt.md`;
|
|
865
896
|
const parts = [];
|
|
@@ -880,6 +911,7 @@ ${instruction}`);
|
|
|
880
911
|
const extraPluginFlags = requiredPluginDirs.map((p) => `--plugin-dir "${p}"`).join(" ");
|
|
881
912
|
const sessionIdFlag = claudeSessionId ? ` --session-id "${claudeSessionId}"` : "";
|
|
882
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}` : ""}`;
|
|
883
915
|
}
|
|
884
916
|
const scriptPath = writeRunScript(promptsDir(cwd, sessionId), `${agentId}-run`, [
|
|
885
917
|
"#!/usr/bin/env bash",
|
|
@@ -889,7 +921,7 @@ ${instruction}`);
|
|
|
889
921
|
notifyCmd
|
|
890
922
|
]);
|
|
891
923
|
const fullCmd = `bash '${scriptPath}'`;
|
|
892
|
-
return { paneId, fullCmd };
|
|
924
|
+
return { paneId, fullCmd, resumeEnv: envExports, resumeArgs };
|
|
893
925
|
}
|
|
894
926
|
async function spawnAgent(opts) {
|
|
895
927
|
const { sessionId, cwd, agentType, name, instruction, windowId } = opts;
|
|
@@ -910,7 +942,7 @@ async function spawnAgent(opts) {
|
|
|
910
942
|
const repoRoot = repo === "." ? cwd : join3(cwd, repo);
|
|
911
943
|
const paneCwd = repoRoot;
|
|
912
944
|
const claudeSessionId = provider !== "openai" ? randomUUID2() : void 0;
|
|
913
|
-
const { paneId, fullCmd } = setupAgentPane({
|
|
945
|
+
const { paneId, fullCmd, resumeEnv, resumeArgs } = setupAgentPane({
|
|
914
946
|
sessionId,
|
|
915
947
|
sessionName: opts.sessionName,
|
|
916
948
|
cycleNum: opts.cycleNum,
|
|
@@ -940,7 +972,9 @@ async function spawnAgent(opts) {
|
|
|
940
972
|
activeMs: 0,
|
|
941
973
|
reports: [],
|
|
942
974
|
paneId,
|
|
943
|
-
repo
|
|
975
|
+
repo,
|
|
976
|
+
resumeEnv,
|
|
977
|
+
resumeArgs
|
|
944
978
|
};
|
|
945
979
|
await addAgent(cwd, sessionId, agent);
|
|
946
980
|
sendKeys(paneId, fullCmd);
|
|
@@ -1091,11 +1125,973 @@ function allAgentsDone(session) {
|
|
|
1091
1125
|
// src/daemon/respawn-guard.ts
|
|
1092
1126
|
var respawningSessions = /* @__PURE__ */ new Set();
|
|
1093
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
|
+
|
|
1094
2070
|
// src/daemon/pane-monitor.ts
|
|
1095
2071
|
var monitorInterval = null;
|
|
1096
2072
|
var onAllAgentsDone = null;
|
|
2073
|
+
var onDotsUpdate = null;
|
|
1097
2074
|
var lastPollTime = 0;
|
|
1098
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
|
+
}
|
|
1099
2095
|
var activeTimers = /* @__PURE__ */ new Map();
|
|
1100
2096
|
function initTimers(sessionId, session) {
|
|
1101
2097
|
const entry = {
|
|
@@ -1160,6 +2156,12 @@ function getTrackedSessionIds() {
|
|
|
1160
2156
|
function setRespawnCallback(cb) {
|
|
1161
2157
|
onAllAgentsDone = cb;
|
|
1162
2158
|
}
|
|
2159
|
+
function setDotsCallback(cb) {
|
|
2160
|
+
onDotsUpdate = cb;
|
|
2161
|
+
}
|
|
2162
|
+
function getTrackedSessionEntries() {
|
|
2163
|
+
return trackedSessions.values();
|
|
2164
|
+
}
|
|
1163
2165
|
function startMonitor(pollIntervalMs = 5e3) {
|
|
1164
2166
|
if (monitorInterval) return;
|
|
1165
2167
|
storedPollIntervalMs = pollIntervalMs;
|
|
@@ -1195,16 +2197,111 @@ async function pollAllSessions() {
|
|
|
1195
2197
|
const threshold = storedPollIntervalMs * 3;
|
|
1196
2198
|
const increment = elapsed > threshold ? storedPollIntervalMs : elapsed;
|
|
1197
2199
|
lastPollTime = now;
|
|
2200
|
+
const pollSessionCache = /* @__PURE__ */ new Map();
|
|
1198
2201
|
for (const { id: sessionId, cwd, windowId } of trackedSessions.values()) {
|
|
1199
2202
|
if (windowId) {
|
|
1200
|
-
await pollSession(sessionId, cwd, windowId, increment);
|
|
2203
|
+
await pollSession(sessionId, cwd, windowId, increment, pollSessionCache);
|
|
1201
2204
|
}
|
|
1202
2205
|
}
|
|
2206
|
+
try {
|
|
2207
|
+
onDotsUpdate?.();
|
|
2208
|
+
} catch {
|
|
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
|
+
}
|
|
1203
2299
|
}
|
|
1204
|
-
async function pollSession(sessionId, cwd, windowId, increment) {
|
|
2300
|
+
async function pollSession(sessionId, cwd, windowId, increment, sessionCache) {
|
|
1205
2301
|
let session;
|
|
1206
2302
|
try {
|
|
1207
2303
|
session = getSession(cwd, sessionId);
|
|
2304
|
+
sessionCache?.set(sessionId, session);
|
|
1208
2305
|
} catch (err) {
|
|
1209
2306
|
console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
|
|
1210
2307
|
return;
|
|
@@ -1295,7 +2392,7 @@ async function pollSession(sessionId, cwd, windowId, increment) {
|
|
|
1295
2392
|
function detectRepos(cwd) {
|
|
1296
2393
|
const config = loadConfig(cwd);
|
|
1297
2394
|
const repos = [];
|
|
1298
|
-
if (
|
|
2395
|
+
if (existsSync7(join6(cwd, ".git"))) {
|
|
1299
2396
|
try {
|
|
1300
2397
|
repos.push(getRepoInfo(cwd, "."));
|
|
1301
2398
|
} catch {
|
|
@@ -1306,8 +2403,8 @@ function detectRepos(cwd) {
|
|
|
1306
2403
|
for (const entry of entries) {
|
|
1307
2404
|
if (!entry.isDirectory()) continue;
|
|
1308
2405
|
if (entry.name.startsWith(".")) continue;
|
|
1309
|
-
const childPath =
|
|
1310
|
-
if (
|
|
2406
|
+
const childPath = join6(cwd, entry.name);
|
|
2407
|
+
if (existsSync7(join6(childPath, ".git"))) {
|
|
1311
2408
|
try {
|
|
1312
2409
|
repos.push(getRepoInfo(childPath, entry.name));
|
|
1313
2410
|
} catch {
|
|
@@ -1349,25 +2446,25 @@ function discoverOrchestratorModes() {
|
|
|
1349
2446
|
(f) => f.startsWith("orchestrator-") && f.endsWith(".md") && f !== "orchestrator-base.md"
|
|
1350
2447
|
);
|
|
1351
2448
|
return files.map((file) => {
|
|
1352
|
-
const content =
|
|
2449
|
+
const content = readFileSync7(join6(templatesDir, file), "utf-8");
|
|
1353
2450
|
const fm = parseAgentFrontmatter(content);
|
|
1354
2451
|
const name = fm.name ?? file.replace(/^orchestrator-/, "").replace(/\.md$/, "");
|
|
1355
|
-
return { name, description: fm.description, filePath:
|
|
2452
|
+
return { name, description: fm.description, filePath: join6(templatesDir, file) };
|
|
1356
2453
|
});
|
|
1357
2454
|
}
|
|
1358
2455
|
function loadOrchestratorPrompt(cwd, sessionId, mode) {
|
|
1359
2456
|
const projectPath = projectOrchestratorPromptPath(cwd);
|
|
1360
|
-
if (
|
|
1361
|
-
return
|
|
2457
|
+
if (existsSync7(projectPath)) {
|
|
2458
|
+
return readFileSync7(projectPath, "utf-8");
|
|
1362
2459
|
}
|
|
1363
2460
|
const basePath = resolve3(import.meta.dirname, "../templates/orchestrator-base.md");
|
|
1364
|
-
const base =
|
|
2461
|
+
const base = readFileSync7(basePath, "utf-8");
|
|
1365
2462
|
const modes = discoverOrchestratorModes();
|
|
1366
2463
|
const selected = modes.find((m) => m.name === mode) ?? modes.find((m) => m.name === "strategy");
|
|
1367
2464
|
if (!selected) {
|
|
1368
2465
|
throw new Error(`Unknown orchestrator mode '${mode}' and no fallback found. Available: ${modes.map((m) => m.name).join(", ")}`);
|
|
1369
2466
|
}
|
|
1370
|
-
const modeContent =
|
|
2467
|
+
const modeContent = readFileSync7(selected.filePath, "utf-8");
|
|
1371
2468
|
const modeBody = extractAgentBody(modeContent);
|
|
1372
2469
|
return base + "\n\n" + modeBody;
|
|
1373
2470
|
}
|
|
@@ -1387,7 +2484,7 @@ ${session.context}
|
|
|
1387
2484
|
}
|
|
1388
2485
|
} else {
|
|
1389
2486
|
let ctxFiles = [];
|
|
1390
|
-
if (
|
|
2487
|
+
if (existsSync7(ctxDir)) {
|
|
1391
2488
|
ctxFiles = readdirSync4(ctxDir).filter((f) => f !== "CLAUDE.md");
|
|
1392
2489
|
}
|
|
1393
2490
|
if (ctxFiles.length > 0) {
|
|
@@ -1423,8 +2520,8 @@ ${agentLines}
|
|
|
1423
2520
|
`;
|
|
1424
2521
|
}
|
|
1425
2522
|
const strategyFile = strategyPath(session.cwd, session.id);
|
|
1426
|
-
const strategyRef =
|
|
1427
|
-
const roadmapRef =
|
|
2523
|
+
const strategyRef = existsSync7(strategyFile) ? `@${relative(session.cwd, strategyFile)}` : "(empty)";
|
|
2524
|
+
const roadmapRef = existsSync7(roadmapFile) ? `@${relative(session.cwd, roadmapFile)}` : "(empty)";
|
|
1428
2525
|
const repos = detectRepos(session.cwd);
|
|
1429
2526
|
let repositoriesSection = "\n\n## Repositories\n";
|
|
1430
2527
|
if (repos.length === 0) {
|
|
@@ -1451,7 +2548,7 @@ ${agentLines}
|
|
|
1451
2548
|
}
|
|
1452
2549
|
}
|
|
1453
2550
|
const goalFile = goalPath(session.cwd, session.id);
|
|
1454
|
-
const goalContent =
|
|
2551
|
+
const goalContent = existsSync7(goalFile) ? readFileSync7(goalFile, "utf-8").trim() : session.task;
|
|
1455
2552
|
return `## Goal
|
|
1456
2553
|
|
|
1457
2554
|
${goalContent}
|
|
@@ -1499,7 +2596,7 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message) {
|
|
|
1499
2596
|
);
|
|
1500
2597
|
const cycleNum = session.orchestratorCycles.length + 1;
|
|
1501
2598
|
const promptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-system-${cycleNum}.md`;
|
|
1502
|
-
|
|
2599
|
+
writeFileSync5(promptFilePath, systemPrompt, "utf-8");
|
|
1503
2600
|
sessionWindowMap.set(sessionId, windowId);
|
|
1504
2601
|
const npmBinDir = resolveNpmBinDir();
|
|
1505
2602
|
const envExports = buildEnvExports([
|
|
@@ -1526,7 +2623,7 @@ The user resumed this session with new instructions: ${message}`;
|
|
|
1526
2623
|
${continuationText}`;
|
|
1527
2624
|
}
|
|
1528
2625
|
const userPromptFilePath = `${promptsDir(cwd, sessionId)}/orchestrator-user-${cycleNum}.md`;
|
|
1529
|
-
|
|
2626
|
+
writeFileSync5(userPromptFilePath, substituteEnvVars(userPrompt), "utf-8");
|
|
1530
2627
|
if (session.messages && session.messages.length > 0) {
|
|
1531
2628
|
await drainMessages(cwd, sessionId, session.messages.length);
|
|
1532
2629
|
}
|
|
@@ -1536,7 +2633,7 @@ ${continuationText}`;
|
|
|
1536
2633
|
const effort = config.orchestratorEffort ?? "high";
|
|
1537
2634
|
const requiredPluginDirs = resolveRequiredPluginDirs(cwd);
|
|
1538
2635
|
const extraPluginFlags = requiredPluginDirs.map((p) => `--plugin-dir "${p}"`).join(" ");
|
|
1539
|
-
const claudeSessionId =
|
|
2636
|
+
const claudeSessionId = randomUUID4();
|
|
1540
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}')"`;
|
|
1541
2638
|
const paneId = createPane(windowId, cwd, "left");
|
|
1542
2639
|
sessionOrchestratorPane.set(sessionId, paneId);
|
|
@@ -1562,6 +2659,8 @@ ${continuationText}`;
|
|
|
1562
2659
|
notifyCmd
|
|
1563
2660
|
]);
|
|
1564
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}`;
|
|
1565
2664
|
await addOrchestratorCycle(cwd, sessionId, {
|
|
1566
2665
|
cycle: cycleNum,
|
|
1567
2666
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1569,7 +2668,9 @@ ${continuationText}`;
|
|
|
1569
2668
|
agentsSpawned: [],
|
|
1570
2669
|
paneId,
|
|
1571
2670
|
claudeSessionId,
|
|
1572
|
-
mode
|
|
2671
|
+
mode,
|
|
2672
|
+
resumeEnv,
|
|
2673
|
+
resumeArgs
|
|
1573
2674
|
});
|
|
1574
2675
|
}
|
|
1575
2676
|
function resolveOrchestratorPane(sessionId, cwd) {
|
|
@@ -1611,20 +2712,124 @@ function cleanupSessionMaps(sessionId) {
|
|
|
1611
2712
|
}
|
|
1612
2713
|
|
|
1613
2714
|
// src/daemon/notify.ts
|
|
1614
|
-
import { execFile } from "child_process";
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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");
|
|
2755
|
+
try {
|
|
2756
|
+
mkdirSync4(dir, { recursive: true });
|
|
2757
|
+
writeFileSync6(scriptPath, SWITCH_SCRIPT, { mode: 493 });
|
|
2758
|
+
} catch {
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
var notifyProcess = null;
|
|
2762
|
+
function getNotifyBinary() {
|
|
2763
|
+
return join7(homedir3(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
|
|
2764
|
+
}
|
|
2765
|
+
function ensureNotifyProcess() {
|
|
2766
|
+
if (notifyProcess && !notifyProcess.killed && notifyProcess.stdin?.writable) {
|
|
2767
|
+
return notifyProcess;
|
|
2768
|
+
}
|
|
2769
|
+
const binary = getNotifyBinary();
|
|
2770
|
+
if (!existsSync8(binary)) {
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
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;
|
|
2805
|
+
}
|
|
2806
|
+
execFile("terminal-notifier", ["-title", title, "-message", msg], (err) => {
|
|
1620
2807
|
if (err) {
|
|
1621
|
-
|
|
2808
|
+
execFile("osascript", [
|
|
2809
|
+
"-e",
|
|
2810
|
+
`display notification "${msg.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`
|
|
2811
|
+
], () => {
|
|
2812
|
+
});
|
|
1622
2813
|
}
|
|
1623
2814
|
});
|
|
1624
2815
|
}
|
|
1625
2816
|
|
|
1626
2817
|
// src/daemon/session-manager.ts
|
|
1627
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) {
|
|
2822
|
+
try {
|
|
2823
|
+
const c = loadCompanion();
|
|
2824
|
+
c.lastCommentary = { text, event, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2825
|
+
saveCompanion(c);
|
|
2826
|
+
if (flash) flashCompanion(text);
|
|
2827
|
+
} catch {
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}).catch(() => {
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
1628
2833
|
function switchToHomeSession(session) {
|
|
1629
2834
|
if (!session.tmuxSessionName) return;
|
|
1630
2835
|
const home = findHomeSession(session.cwd);
|
|
@@ -1635,12 +2840,22 @@ async function startSession(task, cwd, context, name) {
|
|
|
1635
2840
|
if (name && !NAME_PATTERN.test(name)) {
|
|
1636
2841
|
throw new Error(`Invalid session name "${name}": only alphanumeric, hyphens, and underscores allowed`);
|
|
1637
2842
|
}
|
|
1638
|
-
const tmuxName =
|
|
2843
|
+
const tmuxName = tmuxSessionName(cwd, name ?? sessionId.slice(0, 8));
|
|
1639
2844
|
if (sessionExists(tmuxName)) {
|
|
1640
2845
|
throw new Error(`Tmux session "${tmuxName}" already exists. Choose a different name.`);
|
|
1641
2846
|
}
|
|
1642
2847
|
const session = createSession(sessionId, task, cwd, context, name);
|
|
1643
|
-
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);
|
|
1644
2859
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1645
2860
|
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
1646
2861
|
trackSession(sessionId, cwd, tmuxName);
|
|
@@ -1655,12 +2870,12 @@ async function startSession(task, cwd, context, name) {
|
|
|
1655
2870
|
return;
|
|
1656
2871
|
}
|
|
1657
2872
|
let finalName = generatedName;
|
|
1658
|
-
let candidate =
|
|
2873
|
+
let candidate = tmuxSessionName(cwd, finalName);
|
|
1659
2874
|
let attempt = 0;
|
|
1660
2875
|
while (sessionExists(candidate) && attempt < 5) {
|
|
1661
2876
|
attempt++;
|
|
1662
2877
|
finalName = `${generatedName}-${attempt}`;
|
|
1663
|
-
candidate =
|
|
2878
|
+
candidate = tmuxSessionName(cwd, finalName);
|
|
1664
2879
|
}
|
|
1665
2880
|
if (sessionExists(candidate)) return;
|
|
1666
2881
|
try {
|
|
@@ -1691,6 +2906,17 @@ async function startSession(task, cwd, context, name) {
|
|
|
1691
2906
|
console.error(`[sisyphus] Name generation failed for session ${sessionId}:`, err);
|
|
1692
2907
|
});
|
|
1693
2908
|
}
|
|
2909
|
+
try {
|
|
2910
|
+
recomputeDots();
|
|
2911
|
+
} catch {
|
|
2912
|
+
}
|
|
2913
|
+
try {
|
|
2914
|
+
const companion = loadCompanion();
|
|
2915
|
+
onSessionStart(companion, cwd);
|
|
2916
|
+
saveCompanion(companion);
|
|
2917
|
+
fireCommentary("session-start", companion, task);
|
|
2918
|
+
} catch {
|
|
2919
|
+
}
|
|
1694
2920
|
return { ...getSession(cwd, sessionId), tmuxSessionName: tmuxName };
|
|
1695
2921
|
}
|
|
1696
2922
|
var PRUNE_KEEP_COUNT = 10;
|
|
@@ -1698,7 +2924,7 @@ var PRUNE_KEEP_DAYS = 7;
|
|
|
1698
2924
|
function pruneOldSessions(cwd) {
|
|
1699
2925
|
try {
|
|
1700
2926
|
const dir = sessionsDir(cwd);
|
|
1701
|
-
if (!
|
|
2927
|
+
if (!existsSync9(dir)) return;
|
|
1702
2928
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1703
2929
|
const candidates = [];
|
|
1704
2930
|
for (const entry of entries) {
|
|
@@ -1730,23 +2956,23 @@ function pruneOldSessions(cwd) {
|
|
|
1730
2956
|
}
|
|
1731
2957
|
async function reopenWindow(sessionId, cwd) {
|
|
1732
2958
|
const session = getSession(cwd, sessionId);
|
|
1733
|
-
const tmuxName = session.tmuxSessionName ??
|
|
2959
|
+
const tmuxName = session.tmuxSessionName ?? tmuxSessionName(cwd, session.name ?? sessionId.slice(0, 8));
|
|
1734
2960
|
if (sessionExists(tmuxName) && session.tmuxWindowId) {
|
|
1735
2961
|
return { tmuxSessionName: tmuxName, tmuxWindowId: session.tmuxWindowId };
|
|
1736
2962
|
}
|
|
1737
|
-
const created = createSession2(tmuxName,
|
|
2963
|
+
const created = createSession2(tmuxName, cwd);
|
|
1738
2964
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1739
2965
|
await updateSessionTmux(cwd, sessionId, tmuxName, created.windowId);
|
|
1740
2966
|
return { tmuxSessionName: tmuxName, tmuxWindowId: created.windowId };
|
|
1741
2967
|
}
|
|
1742
2968
|
async function resumeSession(sessionId, cwd, message) {
|
|
1743
2969
|
const session = getSession(cwd, sessionId);
|
|
1744
|
-
const tmuxName = session.tmuxSessionName ??
|
|
2970
|
+
const tmuxName = session.tmuxSessionName ?? tmuxSessionName(cwd, session.name ?? sessionId.slice(0, 8));
|
|
1745
2971
|
let windowId;
|
|
1746
2972
|
if (sessionExists(tmuxName) && session.tmuxWindowId) {
|
|
1747
2973
|
windowId = session.tmuxWindowId;
|
|
1748
2974
|
} else {
|
|
1749
|
-
const created = createSession2(tmuxName,
|
|
2975
|
+
const created = createSession2(tmuxName, cwd);
|
|
1750
2976
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1751
2977
|
windowId = created.windowId;
|
|
1752
2978
|
await updateSessionTmux(cwd, sessionId, tmuxName, windowId);
|
|
@@ -1784,6 +3010,10 @@ async function resumeSession(sessionId, cwd, message) {
|
|
|
1784
3010
|
if (initialPaneId) {
|
|
1785
3011
|
killPane(initialPaneId);
|
|
1786
3012
|
}
|
|
3013
|
+
try {
|
|
3014
|
+
recomputeDots();
|
|
3015
|
+
} catch {
|
|
3016
|
+
}
|
|
1787
3017
|
return getSession(cwd, sessionId);
|
|
1788
3018
|
}
|
|
1789
3019
|
function getSessionStatus(cwd, sessionId) {
|
|
@@ -1791,7 +3021,7 @@ function getSessionStatus(cwd, sessionId) {
|
|
|
1791
3021
|
}
|
|
1792
3022
|
function listSessions(cwd) {
|
|
1793
3023
|
const dir = sessionsDir(cwd);
|
|
1794
|
-
if (!
|
|
3024
|
+
if (!existsSync9(dir)) return [];
|
|
1795
3025
|
const entries = readdirSync5(dir, { withFileTypes: true });
|
|
1796
3026
|
const sessions = [];
|
|
1797
3027
|
for (const entry of entries) {
|
|
@@ -1839,6 +3069,11 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1839
3069
|
if (cycleNumber > 0) {
|
|
1840
3070
|
createSnapshot(cwd, sessionId, cycleNumber);
|
|
1841
3071
|
}
|
|
3072
|
+
try {
|
|
3073
|
+
const companion = loadCompanion();
|
|
3074
|
+
fireCommentary("cycle-boundary", companion, `Cycle ${cycleNumber} complete, respawning orchestrator`);
|
|
3075
|
+
} catch {
|
|
3076
|
+
}
|
|
1842
3077
|
setImmediate(async () => {
|
|
1843
3078
|
pendingRespawns.delete(sessionId);
|
|
1844
3079
|
try {
|
|
@@ -1857,7 +3092,7 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1857
3092
|
if (sessionExists(tmuxName)) {
|
|
1858
3093
|
killSession(tmuxName);
|
|
1859
3094
|
}
|
|
1860
|
-
const created = createSession2(tmuxName,
|
|
3095
|
+
const created = createSession2(tmuxName, cwd);
|
|
1861
3096
|
setSessionOption(tmuxName, "@sisyphus_cwd", cwd.replace(/\/+$/, ""));
|
|
1862
3097
|
activeWindowId = created.windowId;
|
|
1863
3098
|
initialPaneId = created.initialPaneId;
|
|
@@ -1874,6 +3109,10 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
|
|
|
1874
3109
|
}
|
|
1875
3110
|
}
|
|
1876
3111
|
selectLayout(activeWindowId);
|
|
3112
|
+
try {
|
|
3113
|
+
recomputeDots();
|
|
3114
|
+
} catch {
|
|
3115
|
+
}
|
|
1877
3116
|
} catch (err) {
|
|
1878
3117
|
console.error(`[sisyphus] Failed to respawn orchestrator for session ${sessionId}:`, err);
|
|
1879
3118
|
} finally {
|
|
@@ -1901,10 +3140,31 @@ async function handleSpawn(sessionId, cwd, agentType, name, instruction, repo) {
|
|
|
1901
3140
|
repo
|
|
1902
3141
|
});
|
|
1903
3142
|
await appendAgentToLastCycle(cwd, sessionId, agent.id);
|
|
3143
|
+
try {
|
|
3144
|
+
recomputeDots();
|
|
3145
|
+
} catch {
|
|
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
|
+
}
|
|
1904
3160
|
return { agentId: agent.id };
|
|
1905
3161
|
}
|
|
1906
3162
|
async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
|
|
1907
3163
|
const allDone = await handleAgentSubmit(cwd, sessionId, agentId, report);
|
|
3164
|
+
try {
|
|
3165
|
+
recomputeDots();
|
|
3166
|
+
} catch {
|
|
3167
|
+
}
|
|
1908
3168
|
if (allDone) {
|
|
1909
3169
|
onAllAgentsDone2(sessionId, cwd, windowId);
|
|
1910
3170
|
}
|
|
@@ -1920,7 +3180,12 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
|
1920
3180
|
respawningSessions.add(sessionId);
|
|
1921
3181
|
await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
|
|
1922
3182
|
orchestratorDone.add(sessionId);
|
|
3183
|
+
try {
|
|
3184
|
+
recomputeDots();
|
|
3185
|
+
} catch {
|
|
3186
|
+
}
|
|
1923
3187
|
const session = getSession(cwd, sessionId);
|
|
3188
|
+
updateCycleCount(session.orchestratorCycles.length);
|
|
1924
3189
|
const hasRunningAgents = session.agents.some((a) => a.status === "running");
|
|
1925
3190
|
if (!hasRunningAgents) {
|
|
1926
3191
|
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
@@ -1934,10 +3199,42 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
|
|
|
1934
3199
|
}
|
|
1935
3200
|
}
|
|
1936
3201
|
async function handleComplete(sessionId, cwd, report) {
|
|
1937
|
-
const session = getSession(cwd, sessionId);
|
|
1938
3202
|
await flushTimers(sessionId);
|
|
1939
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 });
|
|
3207
|
+
markSessionCompleted(sessionId, session.createdAt, cwd);
|
|
3208
|
+
untrackSession(sessionId);
|
|
3209
|
+
unregisterSessionPanes(sessionId);
|
|
3210
|
+
clearAgentCounter(sessionId);
|
|
3211
|
+
orchestratorDone.delete(sessionId);
|
|
3212
|
+
try {
|
|
3213
|
+
recomputeDots();
|
|
3214
|
+
} catch {
|
|
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
|
+
}
|
|
1940
3234
|
switchToHomeSession(session);
|
|
3235
|
+
if (session.tmuxSessionName) {
|
|
3236
|
+
killSession(session.tmuxSessionName);
|
|
3237
|
+
}
|
|
1941
3238
|
}
|
|
1942
3239
|
async function handleContinue(sessionId, cwd) {
|
|
1943
3240
|
await continueSession(cwd, sessionId);
|
|
@@ -1972,6 +3269,10 @@ async function handleKill(sessionId, cwd) {
|
|
|
1972
3269
|
}
|
|
1973
3270
|
clearAgentCounter(sessionId);
|
|
1974
3271
|
orchestratorDone.delete(sessionId);
|
|
3272
|
+
try {
|
|
3273
|
+
recomputeDots();
|
|
3274
|
+
} catch {
|
|
3275
|
+
}
|
|
1975
3276
|
return killedAgents;
|
|
1976
3277
|
}
|
|
1977
3278
|
async function handleRestartAgent(sessionId, cwd, agentId) {
|
|
@@ -2039,8 +3340,16 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2039
3340
|
const agent = session.agents.find((a) => a.id === agentId);
|
|
2040
3341
|
if (!agent || agent.status !== "running") return;
|
|
2041
3342
|
const label = agent.name ? `${agent.name} (${agentId})` : agentId;
|
|
2042
|
-
sendTerminalNotification("Sisyphus", `Agent ${label} exited without submitting a report
|
|
3343
|
+
sendTerminalNotification("Sisyphus", `Agent ${label} exited without submitting a report`, session.tmuxSessionName);
|
|
2043
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
|
+
}
|
|
2044
3353
|
if (allDone) {
|
|
2045
3354
|
const windowId = getWindowId(sessionId) ?? session.tmuxWindowId;
|
|
2046
3355
|
if (windowId) {
|
|
@@ -2049,7 +3358,7 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2049
3358
|
}
|
|
2050
3359
|
} else if (role === "orchestrator") {
|
|
2051
3360
|
const sessionName = session.name ?? sessionId.slice(0, 8);
|
|
2052
|
-
sendTerminalNotification("Sisyphus", `Orchestrator exited without yielding (${sessionName})
|
|
3361
|
+
sendTerminalNotification("Sisyphus", `Orchestrator exited without yielding (${sessionName})`, session.tmuxSessionName);
|
|
2053
3362
|
respawningSessions.add(sessionId);
|
|
2054
3363
|
const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
|
|
2055
3364
|
await completeOrchestratorCycle(cwd, sessionId, void 0, void 0, cycleActiveMs);
|
|
@@ -2077,22 +3386,22 @@ async function handlePaneExited(paneId, cwd, sessionId, role, agentId) {
|
|
|
2077
3386
|
var server = null;
|
|
2078
3387
|
var sessionTrackingMap = /* @__PURE__ */ new Map();
|
|
2079
3388
|
function registryPath() {
|
|
2080
|
-
return
|
|
3389
|
+
return join8(globalDir(), "session-registry.json");
|
|
2081
3390
|
}
|
|
2082
3391
|
function persistSessionRegistry() {
|
|
2083
3392
|
const dir = globalDir();
|
|
2084
|
-
|
|
3393
|
+
mkdirSync5(dir, { recursive: true });
|
|
2085
3394
|
const registry = {};
|
|
2086
3395
|
for (const [id, tracking] of sessionTrackingMap) {
|
|
2087
3396
|
registry[id] = tracking.cwd;
|
|
2088
3397
|
}
|
|
2089
|
-
|
|
3398
|
+
writeFileSync7(registryPath(), JSON.stringify(registry, null, 2), "utf-8");
|
|
2090
3399
|
}
|
|
2091
3400
|
function loadSessionRegistry() {
|
|
2092
3401
|
const p = registryPath();
|
|
2093
|
-
if (!
|
|
3402
|
+
if (!existsSync10(p)) return {};
|
|
2094
3403
|
try {
|
|
2095
|
-
return JSON.parse(
|
|
3404
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
2096
3405
|
} catch {
|
|
2097
3406
|
return {};
|
|
2098
3407
|
}
|
|
@@ -2170,11 +3479,17 @@ async function handleRequest(req) {
|
|
|
2170
3479
|
return { ok: true };
|
|
2171
3480
|
}
|
|
2172
3481
|
case "status": {
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
const
|
|
2177
|
-
|
|
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);
|
|
2178
3493
|
if (timers) {
|
|
2179
3494
|
session.activeMs = timers.sessionMs;
|
|
2180
3495
|
for (const agent of session.agents) {
|
|
@@ -2220,7 +3535,7 @@ async function handleRequest(req) {
|
|
|
2220
3535
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2221
3536
|
if (!tracking) {
|
|
2222
3537
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2223
|
-
if (
|
|
3538
|
+
if (existsSync10(stateFile)) {
|
|
2224
3539
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2225
3540
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2226
3541
|
persistSessionRegistry();
|
|
@@ -2257,7 +3572,7 @@ async function handleRequest(req) {
|
|
|
2257
3572
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2258
3573
|
if (!tracking) {
|
|
2259
3574
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2260
|
-
if (
|
|
3575
|
+
if (existsSync10(stateFile)) {
|
|
2261
3576
|
registerSessionCwd(req.sessionId, req.cwd);
|
|
2262
3577
|
tracking = sessionTrackingMap.get(req.sessionId);
|
|
2263
3578
|
} else {
|
|
@@ -2271,7 +3586,7 @@ async function handleRequest(req) {
|
|
|
2271
3586
|
let tracking = sessionTrackingMap.get(req.sessionId);
|
|
2272
3587
|
if (!tracking) {
|
|
2273
3588
|
const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
|
|
2274
|
-
if (
|
|
3589
|
+
if (existsSync10(stateFile)) {
|
|
2275
3590
|
tracking = { cwd: req.cwd, messageCounter: 0 };
|
|
2276
3591
|
sessionTrackingMap.set(req.sessionId, tracking);
|
|
2277
3592
|
persistSessionRegistry();
|
|
@@ -2294,7 +3609,7 @@ async function handleRequest(req) {
|
|
|
2294
3609
|
sessionTrackingMap.delete(req.sessionId);
|
|
2295
3610
|
persistSessionRegistry();
|
|
2296
3611
|
}
|
|
2297
|
-
const { sessionDir: sessionDir2 } = await import("./paths-
|
|
3612
|
+
const { sessionDir: sessionDir2 } = await import("./paths-XRDEEJ5R.js");
|
|
2298
3613
|
rmSync3(sessionDir2(req.cwd, req.sessionId), { recursive: true, force: true });
|
|
2299
3614
|
return { ok: true };
|
|
2300
3615
|
}
|
|
@@ -2326,9 +3641,9 @@ async function handleRequest(req) {
|
|
|
2326
3641
|
let filePath;
|
|
2327
3642
|
if (req.content.length > 200) {
|
|
2328
3643
|
const dir = messagesDir(tracking.cwd, req.sessionId);
|
|
2329
|
-
|
|
2330
|
-
filePath =
|
|
2331
|
-
|
|
3644
|
+
mkdirSync5(dir, { recursive: true });
|
|
3645
|
+
filePath = join8(dir, `${id}.md`);
|
|
3646
|
+
writeFileSync7(filePath, req.content, "utf-8");
|
|
2332
3647
|
}
|
|
2333
3648
|
await appendMessage(tracking.cwd, req.sessionId, {
|
|
2334
3649
|
id,
|
|
@@ -2340,6 +3655,14 @@ async function handleRequest(req) {
|
|
|
2340
3655
|
});
|
|
2341
3656
|
return { ok: true };
|
|
2342
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
|
+
}
|
|
2343
3666
|
default:
|
|
2344
3667
|
return { ok: false, error: `Unknown request type: ${req.type}` };
|
|
2345
3668
|
}
|
|
@@ -2351,7 +3674,7 @@ async function handleRequest(req) {
|
|
|
2351
3674
|
function startServer() {
|
|
2352
3675
|
return new Promise((resolve5, reject) => {
|
|
2353
3676
|
const sock = socketPath();
|
|
2354
|
-
if (
|
|
3677
|
+
if (existsSync10(sock)) {
|
|
2355
3678
|
unlinkSync(sock);
|
|
2356
3679
|
}
|
|
2357
3680
|
server = createServer((conn) => {
|
|
@@ -2397,7 +3720,7 @@ function stopServer() {
|
|
|
2397
3720
|
}
|
|
2398
3721
|
server.close(() => {
|
|
2399
3722
|
const sock = socketPath();
|
|
2400
|
-
if (
|
|
3723
|
+
if (existsSync10(sock)) {
|
|
2401
3724
|
unlinkSync(sock);
|
|
2402
3725
|
}
|
|
2403
3726
|
server = null;
|
|
@@ -2408,7 +3731,7 @@ function stopServer() {
|
|
|
2408
3731
|
|
|
2409
3732
|
// src/daemon/updater.ts
|
|
2410
3733
|
import { execSync as execSync3 } from "child_process";
|
|
2411
|
-
import { readFileSync as
|
|
3734
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync8, unlinkSync as unlinkSync2, lstatSync } from "fs";
|
|
2412
3735
|
import { resolve as resolve4 } from "path";
|
|
2413
3736
|
import { get } from "https";
|
|
2414
3737
|
function isNewer(latest, current) {
|
|
@@ -2425,7 +3748,7 @@ function isNewer(latest, current) {
|
|
|
2425
3748
|
function readPackageVersion() {
|
|
2426
3749
|
for (const rel of ["../package.json", "../../package.json"]) {
|
|
2427
3750
|
try {
|
|
2428
|
-
const raw =
|
|
3751
|
+
const raw = readFileSync9(resolve4(import.meta.dirname, rel), "utf-8");
|
|
2429
3752
|
const pkg = JSON.parse(raw);
|
|
2430
3753
|
if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
|
|
2431
3754
|
} catch {
|
|
@@ -2491,7 +3814,7 @@ function applyUpdate(expectedVersion) {
|
|
|
2491
3814
|
}
|
|
2492
3815
|
function markUpdating(version) {
|
|
2493
3816
|
try {
|
|
2494
|
-
|
|
3817
|
+
writeFileSync8(daemonUpdatingPath(), version, "utf-8");
|
|
2495
3818
|
} catch {
|
|
2496
3819
|
}
|
|
2497
3820
|
}
|
|
@@ -2558,7 +3881,7 @@ var origError = console.error.bind(console);
|
|
|
2558
3881
|
console.log = (...args) => origLog(`[${ts()}]`, ...args);
|
|
2559
3882
|
console.error = (...args) => origError(`[${ts()}]`, ...args);
|
|
2560
3883
|
function ensureDirs() {
|
|
2561
|
-
|
|
3884
|
+
mkdirSync6(globalDir(), { recursive: true });
|
|
2562
3885
|
}
|
|
2563
3886
|
function isProcessAlive(pid) {
|
|
2564
3887
|
try {
|
|
@@ -2571,7 +3894,7 @@ function isProcessAlive(pid) {
|
|
|
2571
3894
|
function readPid() {
|
|
2572
3895
|
const pidFile = daemonPidPath();
|
|
2573
3896
|
try {
|
|
2574
|
-
const pid = parseInt(
|
|
3897
|
+
const pid = parseInt(readFileSync10(pidFile, "utf-8").trim(), 10);
|
|
2575
3898
|
return pid && isProcessAlive(pid) ? pid : null;
|
|
2576
3899
|
} catch {
|
|
2577
3900
|
return null;
|
|
@@ -2583,7 +3906,7 @@ function acquirePidLock() {
|
|
|
2583
3906
|
console.error(`[sisyphus] Daemon already running (pid ${pid}). Use 'sisyphusd restart' or 'sisyphusd stop' first.`);
|
|
2584
3907
|
process.exit(0);
|
|
2585
3908
|
}
|
|
2586
|
-
|
|
3909
|
+
writeFileSync9(daemonPidPath(), String(process.pid), "utf-8");
|
|
2587
3910
|
}
|
|
2588
3911
|
function isLaunchdManaged() {
|
|
2589
3912
|
try {
|
|
@@ -2642,11 +3965,11 @@ async function recoverSessions() {
|
|
|
2642
3965
|
let recovered = 0;
|
|
2643
3966
|
for (const [sessionId, cwd] of entries) {
|
|
2644
3967
|
const stateFile = statePath(cwd, sessionId);
|
|
2645
|
-
if (!
|
|
3968
|
+
if (!existsSync11(stateFile)) {
|
|
2646
3969
|
continue;
|
|
2647
3970
|
}
|
|
2648
3971
|
try {
|
|
2649
|
-
const session = JSON.parse(
|
|
3972
|
+
const session = JSON.parse(readFileSync10(stateFile, "utf-8"));
|
|
2650
3973
|
if (session.status === "active" || session.status === "paused") {
|
|
2651
3974
|
registerSessionCwd(sessionId, cwd);
|
|
2652
3975
|
resetAgentCounterFromState(sessionId, session.agents ?? []);
|
|
@@ -2720,9 +4043,21 @@ async function startDaemon() {
|
|
|
2720
4043
|
}
|
|
2721
4044
|
acquirePidLock();
|
|
2722
4045
|
setRespawnCallback(onAllAgentsDone2);
|
|
4046
|
+
setDotsCallback(() => {
|
|
4047
|
+
recomputeDots();
|
|
4048
|
+
try {
|
|
4049
|
+
writeStatusBar();
|
|
4050
|
+
} catch {
|
|
4051
|
+
}
|
|
4052
|
+
});
|
|
4053
|
+
setTrackedEntriesProvider(getTrackedSessionEntries);
|
|
2723
4054
|
await startServer();
|
|
2724
4055
|
startMonitor(config.pollIntervalMs);
|
|
2725
4056
|
await recoverSessions();
|
|
4057
|
+
try {
|
|
4058
|
+
writeStatusBar();
|
|
4059
|
+
} catch {
|
|
4060
|
+
}
|
|
2726
4061
|
if (config.autoUpdate !== false) {
|
|
2727
4062
|
startPeriodicUpdateCheck();
|
|
2728
4063
|
}
|