sisyphi 1.1.17 → 1.1.18

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