sisyphi 1.1.16 → 1.1.18

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