sisyphi 1.2.19 → 1.2.21

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
@@ -2504,7 +2504,7 @@ var init_history = __esm({
2504
2504
  });
2505
2505
 
2506
2506
  // src/shared/platform.ts
2507
- import { execSync as execSync4 } from "child_process";
2507
+ import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
2508
2508
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
2509
2509
  function detectPlatform() {
2510
2510
  if (cachedPlatform) return cachedPlatform;
@@ -2694,13 +2694,14 @@ var init_notify = __esm({
2694
2694
  });
2695
2695
 
2696
2696
  // src/daemon/ask-store.ts
2697
- import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
2697
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, readdirSync as readdirSync7, unlinkSync } from "fs";
2698
2698
  import { basename as basename4 } from "path";
2699
- function maybeNotifyOnAskCreated(cwd, sessionId, meta) {
2699
+ function maybeNotifyOnAskCreated(cwd, sessionId, meta, suppress = false) {
2700
2700
  if (process.env.NODE_ENV === "test" || process.env.SISYPHUS_DISABLE_NOTIFY === "1") return;
2701
2701
  const isActionable = meta.kind !== void 0 && ACTIONABLE_KINDS.has(meta.kind);
2702
2702
  const isHeartbeat = meta.askedBy === HEARTBEAT_ASKED_BY;
2703
2703
  if (!isActionable && !isHeartbeat) return;
2704
+ if (suppress) return;
2704
2705
  try {
2705
2706
  const config = loadConfig(cwd);
2706
2707
  if (config.notifications?.enabled === false) return;
@@ -2726,8 +2727,7 @@ function createAsk(cwd, sessionId, params) {
2726
2727
  ...params.title !== void 0 ? { title: params.title } : {},
2727
2728
  ...params.subtitle !== void 0 ? { subtitle: params.subtitle } : {},
2728
2729
  ...params.kind !== void 0 ? { kind: params.kind } : {},
2729
- ...params.orphanTarget !== void 0 ? { orphanTarget: params.orphanTarget } : {},
2730
- ...params.modeTransition !== void 0 ? { modeTransition: params.modeTransition } : {}
2730
+ ...params.orphanTarget !== void 0 ? { orphanTarget: params.orphanTarget } : {}
2731
2731
  };
2732
2732
  atomicWrite(askMetaPath(cwd, sessionId, params.askId), JSON.stringify(meta, null, 2));
2733
2733
  emitHistoryEvent(sessionId, "ask-issued", {
@@ -2736,7 +2736,7 @@ function createAsk(cwd, sessionId, params) {
2736
2736
  blocking: params.blocking,
2737
2737
  askedAt
2738
2738
  });
2739
- maybeNotifyOnAskCreated(cwd, sessionId, meta);
2739
+ maybeNotifyOnAskCreated(cwd, sessionId, meta, params.suppressTerminalNotification === true);
2740
2740
  return meta;
2741
2741
  }
2742
2742
  function writeDecisions(cwd, sessionId, askId, deck) {
@@ -2871,6 +2871,26 @@ async function maybeAutoResolveAsk(cwd, sessionId, askId, deck) {
2871
2871
  } catch {
2872
2872
  }
2873
2873
  }
2874
+ async function clearStaleAskClaims(cwd, sessionId) {
2875
+ let cleared = 0;
2876
+ for (const askId of listAsks(cwd, sessionId)) {
2877
+ const progressPath = askProgressPath(cwd, sessionId, askId);
2878
+ if (!existsSync11(progressPath)) continue;
2879
+ if (existsSync11(askOutputPath(cwd, sessionId, askId))) continue;
2880
+ try {
2881
+ unlinkSync(progressPath);
2882
+ } catch {
2883
+ continue;
2884
+ }
2885
+ cleared++;
2886
+ const meta = readMeta(cwd, sessionId, askId);
2887
+ if (meta?.status === "in-progress") {
2888
+ await updateMeta(cwd, sessionId, askId, { status: "pending", startedAt: void 0 }).catch(() => {
2889
+ });
2890
+ }
2891
+ }
2892
+ return cleared;
2893
+ }
2874
2894
  function listOpenAsksFor(cwd, sessionId, askedBy) {
2875
2895
  const out = [];
2876
2896
  for (const askId of listAsks(cwd, sessionId)) {
@@ -3293,7 +3313,7 @@ var init_orphan_sweep = __esm({
3293
3313
  });
3294
3314
 
3295
3315
  // src/daemon/agent.ts
3296
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync8, existsSync as existsSync13, unlinkSync } from "fs";
3316
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync8, existsSync as existsSync13, unlinkSync as unlinkSync2 } from "fs";
3297
3317
  import { execSync as execSync5 } from "child_process";
3298
3318
  import { randomUUID as randomUUID3 } from "crypto";
3299
3319
  import { resolve as resolve5, relative as relative2, dirname as dirname3, join as join12 } from "path";
@@ -3660,7 +3680,7 @@ function gcBgTasks(cwd, sessionId, agentId) {
3660
3680
  console.warn(`[bg-tasks] ${agentId} exited with ${leftover.length} untracked background task(s): ${leftover.join(", ")}`);
3661
3681
  emitHistoryEvent(sessionId, "bg-tasks-leftover", { agentId, leftover });
3662
3682
  }
3663
- unlinkSync(file);
3683
+ unlinkSync2(file);
3664
3684
  } catch (err) {
3665
3685
  console.warn(`[bg-tasks] ${agentId} cleanup failed:`, err instanceof Error ? err.message : err);
3666
3686
  }
@@ -5140,13 +5160,12 @@ function buildPersonality(stats) {
5140
5160
  function shouldGenerateCommentary(event) {
5141
5161
  switch (event) {
5142
5162
  case "session-start":
5163
+ case "mode-transition":
5143
5164
  case "session-complete":
5144
5165
  case "level-up":
5145
5166
  case "achievement":
5146
5167
  case "late-night":
5147
5168
  return true;
5148
- case "cycle-boundary":
5149
- return Math.random() < 0.5;
5150
5169
  case "idle-wake":
5151
5170
  return Math.random() < 0.5;
5152
5171
  case "agent-crash":
@@ -5338,18 +5357,17 @@ var init_companion_commentary = __esm({
5338
5357
  { event: "session-start", mood: "grinding", context: "migrate database schema", output: "Database migration. The kind of task that sounds simple until you're three hours in." },
5339
5358
  { event: "agent-crash", mood: "frustrated", context: "agent-003 (reviewer) crashed. 2/5 agents still running", output: "Down to two out of five. The reviewer went first, which is either ironic or fitting." },
5340
5359
  { event: "late-night", mood: "existential", context: "3:14am, 2 sessions active", output: "Something about 3am makes every function look like a confession." },
5341
- { event: "cycle-boundary", mood: "zen", context: "Cycle 4 complete. 5 agents all submitted clean reports", output: "Five clean reports. No notes. That quiet after the last one lands is the best part." },
5360
+ { event: "mode-transition", mood: "grinding", context: "Entering Implementation mode. Discovery: 3 cycles \xB7 24m active.", output: "Discovery is done pretending it was the hard part. Now we actually build the thing." },
5361
+ { event: "mode-transition", mood: "zen", context: "Entering Validation mode. Implementation: 5 cycles \xB7 1h active.", output: "Code is written. Now the part where we find out which of my assumptions were lies." },
5342
5362
  { event: "level-up", mood: "excited", context: "Level 7 (Boulder Artisan) \u2192 8 (Crag Whisperer)", output: 'Crag Whisperer. Could be worse. Could be "guy who pushes rocks forever."' },
5343
5363
  { event: "idle-wake", mood: "sleepy", context: "Idle for 45 minutes", output: "Forty-five minutes of nothing, and the workspace looks exactly the same. Comforting or depressing." },
5344
5364
  { event: "session-complete", mood: "zen", context: "Task: fix CI pipeline. 1 agent, 1 cycle, 4min", output: "One agent, one cycle, four minutes. If they were all like this, what would I complain about." },
5345
5365
  { event: "session-start", mood: "existential", context: "rewrite the entire test suite", output: "Rewriting every test. Each one a small promise that the code does what someone thinks it does." },
5346
- { event: "cycle-boundary", mood: "grinding", context: "Cycle 7. 3 agents running, 1 crashed, 2 completed", output: "Cycle seven and the survivors are doing the work of five. Nobody asked them to." },
5347
5366
  { event: "late-night", mood: "sleepy", context: "1:30am, 1 session active", output: "Past one. Everything takes twice as long and matters half as much at this hour." },
5348
5367
  { event: "session-complete", mood: "excited", context: "Task: implement search. 8 agents, 3 cycles", output: "Search works. Eight agents and none of them stepped on each other. That never happens." },
5349
5368
  { event: "agent-crash", mood: "zen", context: "agent-001 crashed during linting", output: "Lost one to the linter. Not the worst way to go." },
5350
5369
  { event: "idle-wake", mood: "grinding", context: "Idle for 2 hours", output: "Two hours away and nothing changed. Exactly as expected, exactly as disappointing." },
5351
5370
  { event: "session-start", mood: "happy", context: "add dark mode", output: "Dark mode. Finally a task that matches the terminal aesthetic." },
5352
- { event: "cycle-boundary", mood: "frustrated", context: "Cycle 3. 2 agents crashed, 1 completed with errors", output: "Two crashed, one limped back. If this were a heist movie, we'd be in the part where the plan falls apart." },
5353
5371
  { event: "level-up", mood: "zen", context: "Level 12 (Slope Philosopher) \u2192 13 (Gradient Monk)", output: "Gradient Monk. A title for finding calm in repetition. Accurate enough." },
5354
5372
  { event: "session-complete", mood: "grinding", context: "Task: dependency upgrades. 6 agents, 5 cycles, 40min", output: "Forty minutes on dependencies. The kind of work that feels like running in place until suddenly you're done." },
5355
5373
  { event: "late-night", mood: "grinding", context: "4:22am, 3 sessions running", output: "Three sessions at four in the morning. Either dedication or the absence of better judgment." },
@@ -6175,7 +6193,7 @@ var init_companion_render = __esm({
6175
6193
  });
6176
6194
 
6177
6195
  // src/daemon/companion-popup.ts
6178
- import { writeFileSync as writeFileSync12, readFileSync as readFileSync16, unlinkSync as unlinkSync2, existsSync as existsSync16 } from "fs";
6196
+ import { writeFileSync as writeFileSync12, readFileSync as readFileSync16, unlinkSync as unlinkSync3, existsSync as existsSync16 } from "fs";
6179
6197
  import { tmpdir } from "os";
6180
6198
  import { join as join15, resolve as resolve6 } from "path";
6181
6199
  function wrapText(text, width) {
@@ -6268,7 +6286,7 @@ fi
6268
6286
  `;
6269
6287
  writeFileSync12(POPUP_SCRIPT, script, { mode: 493 });
6270
6288
  try {
6271
- unlinkSync2(POPUP_RESULT_PREFIX);
6289
+ unlinkSync3(POPUP_RESULT_PREFIX);
6272
6290
  } catch {
6273
6291
  }
6274
6292
  const clientsRaw = execSafe('tmux list-clients -F "#{client_name} #{client_width}"');
@@ -6278,14 +6296,14 @@ fi
6278
6296
  const client = line.slice(0, lastSpace);
6279
6297
  const clientWidth = parseInt(line.slice(lastSpace + 1), 10);
6280
6298
  if (!clientWidth) continue;
6281
- const x = Math.max(0, clientWidth - POPUP_WIDTH);
6299
+ const x = Math.max(0, clientWidth - POPUP_WIDTH - 1);
6282
6300
  const args = [
6283
6301
  `-c ${shellQuote(client)}`,
6284
6302
  "-E -b rounded",
6285
6303
  `-T ${shellQuote(initialTitle)}`,
6286
6304
  `-S "fg=${moodColor}"`,
6287
6305
  `-s "fg=${moodColor}"`,
6288
- `-x ${x} -y 0`,
6306
+ `-x ${x} -y 2`,
6289
6307
  `-w ${POPUP_WIDTH} -h ${maxContentHeight}`,
6290
6308
  shellQuote(POPUP_SCRIPT)
6291
6309
  ].join(" ");
@@ -6298,7 +6316,7 @@ fi
6298
6316
  return null;
6299
6317
  } finally {
6300
6318
  try {
6301
- unlinkSync2(POPUP_RESULT_PREFIX);
6319
+ unlinkSync3(POPUP_RESULT_PREFIX);
6302
6320
  } catch {
6303
6321
  }
6304
6322
  }
@@ -6769,152 +6787,15 @@ var init_pane_monitor = __esm({
6769
6787
  }
6770
6788
  });
6771
6789
 
6772
- // src/daemon/mode-notify.ts
6773
- import { existsSync as existsSync17 } from "fs";
6774
- import { ulid as ulid2 } from "ulid";
6775
- function capitalize(s) {
6776
- return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
6777
- }
6778
- function formatDuration(ms) {
6779
- const sec = Math.round(ms / 1e3);
6780
- if (sec < 60) return `${sec}s`;
6781
- const min = Math.round(sec / 60);
6782
- if (min < 60) return `${min}m`;
6783
- const h = Math.floor(min / 60);
6784
- const remM = min % 60;
6785
- return remM ? `${h}h ${remM}m` : `${h}h`;
6786
- }
6787
- function findOpenModeTransitionAsk(cwd, sessionId) {
6788
- for (const askId of listAsks(cwd, sessionId)) {
6789
- const meta = readMeta(cwd, sessionId, askId);
6790
- if (!meta) continue;
6791
- if (meta.askedBy !== ORCHESTRATOR_ASKED_BY) continue;
6792
- if (meta.modeTransition !== true) continue;
6793
- if (meta.status === "answered") continue;
6794
- if (meta.orphaned === true) continue;
6795
- if (existsSync17(askOutputPath(cwd, sessionId, askId))) continue;
6796
- return askId;
6797
- }
6798
- return null;
6799
- }
6800
- function buildNextChain(prevChain, prevMode, nextMode, prevModeStats) {
6801
- const stats = prevModeStats ? { cycles: prevModeStats.cycles, activeMs: prevModeStats.activeMs } : {};
6802
- if (prevChain && prevChain.length > 0) {
6803
- const updated = prevChain.map(
6804
- (e, i) => i === prevChain.length - 1 && prevModeStats ? { ...e, ...stats } : e
6805
- );
6806
- updated.push({ mode: nextMode });
6807
- return updated;
6808
- }
6809
- if (prevMode !== void 0) {
6810
- return [{ mode: prevMode, ...stats }, { mode: nextMode }];
6811
- }
6812
- return [{ mode: "unknown" }, { mode: nextMode }];
6813
- }
6814
- function renderBody(chain, cwd) {
6815
- const current = chain[chain.length - 1];
6816
- const description = discoverOrchestratorModes(cwd).find((m) => m.name === current.mode)?.description?.trim();
6817
- const lines = [];
6818
- if (description) {
6819
- lines.push(`**${capitalize(current.mode)}** \u2014 ${description}`);
6820
- } else {
6821
- lines.push(`Now in **${capitalize(current.mode)}** mode.`);
6822
- }
6823
- for (let i = 0; i < chain.length - 1; i++) {
6824
- const e = chain[i];
6825
- if (e.cycles === void 0) continue;
6826
- const label = e.cycles === 1 ? "cycle" : "cycles";
6827
- lines.push(
6828
- `${capitalize(e.mode)}: ${e.cycles} ${label} \xB7 ${formatDuration(e.activeMs ?? 0)} active`
6829
- );
6830
- }
6831
- return lines.join("\n\n");
6832
- }
6833
- async function emitModeTransitionNotify(cwd, sessionId, prevMode, nextMode, prevModeStats) {
6834
- let sessionName;
6835
- try {
6836
- sessionName = getSession(cwd, sessionId).name;
6837
- } catch {
6838
- }
6839
- const existingAskId = findOpenModeTransitionAsk(cwd, sessionId);
6840
- const existingDeck = existingAskId ? readDecisions(cwd, sessionId, existingAskId) : null;
6841
- const chain = buildNextChain(
6842
- existingDeck?.source?.modeChain,
6843
- prevMode,
6844
- nextMode,
6845
- prevModeStats
6846
- );
6847
- const subtitle = chain.map((e) => e.mode).join(" \u2192 ");
6848
- const title = "Mode change";
6849
- const deckTitle = `Mode: ${subtitle}`;
6850
- const body = renderBody(chain, cwd);
6851
- const interaction = {
6852
- id: "mode-transition",
6853
- title,
6854
- subtitle,
6855
- body,
6856
- kind: "notify",
6857
- options: [{ id: "ack", label: "Acknowledged" }]
6858
- };
6859
- const deckSource = {
6860
- ...sessionName !== void 0 ? { sessionName } : {},
6861
- askedBy: ORCHESTRATOR_ASKED_BY,
6862
- modeChain: chain
6863
- };
6864
- const deck = {
6865
- title: deckTitle,
6866
- source: deckSource,
6867
- interactions: [interaction]
6868
- };
6869
- try {
6870
- if (existingAskId) {
6871
- writeDecisions(cwd, sessionId, existingAskId, deck);
6872
- await updateMeta(cwd, sessionId, existingAskId, {
6873
- title,
6874
- subtitle,
6875
- askedAt: (/* @__PURE__ */ new Date()).toISOString()
6876
- });
6877
- return;
6878
- }
6879
- const askId = ulid2();
6880
- createAsk(cwd, sessionId, {
6881
- askId,
6882
- askedBy: ORCHESTRATOR_ASKED_BY,
6883
- blocking: false,
6884
- cwd,
6885
- title,
6886
- subtitle,
6887
- kind: "notify",
6888
- modeTransition: true
6889
- });
6890
- writeDecisions(cwd, sessionId, askId, deck);
6891
- } catch (err) {
6892
- console.warn(
6893
- `[sisyphus] mode-notify: failed to emit mode transition ask for ${sessionId}:`,
6894
- err instanceof Error ? err.message : err
6895
- );
6896
- }
6897
- }
6898
- var init_mode_notify = __esm({
6899
- "src/daemon/mode-notify.ts"() {
6900
- "use strict";
6901
- init_ask_store();
6902
- init_state();
6903
- init_orchestrator_modes();
6904
- init_paths();
6905
- init_types();
6906
- }
6907
- });
6908
-
6909
6790
  // src/daemon/orchestrator.ts
6910
- import { existsSync as existsSync18, readdirSync as readdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
6791
+ import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
6911
6792
  import { execSync as execSync6 } from "child_process";
6912
6793
  import { randomUUID as randomUUID6 } from "crypto";
6913
6794
  import { resolve as resolve7, join as join16, relative as relative3 } from "path";
6914
6795
  function detectRepos(cwd) {
6915
6796
  const config = loadConfig(cwd);
6916
6797
  const repos = [];
6917
- if (existsSync18(join16(cwd, ".git"))) {
6798
+ if (existsSync17(join16(cwd, ".git"))) {
6918
6799
  try {
6919
6800
  repos.push(getRepoInfo(cwd, "."));
6920
6801
  } catch {
@@ -6926,7 +6807,7 @@ function detectRepos(cwd) {
6926
6807
  if (!entry.isDirectory()) continue;
6927
6808
  if (entry.name.startsWith(".")) continue;
6928
6809
  const childPath = join16(cwd, entry.name);
6929
- if (existsSync18(join16(childPath, ".git"))) {
6810
+ if (existsSync17(join16(childPath, ".git"))) {
6930
6811
  try {
6931
6812
  repos.push(getRepoInfo(childPath, entry.name));
6932
6813
  } catch {
@@ -6964,13 +6845,13 @@ function resolveOrchestratorSettings(cwd, sessionId) {
6964
6845
  const bundled = resolve7(import.meta.dirname, "../templates/orchestrator-settings.json");
6965
6846
  const projectSettings = projectOrchestratorSettingsPath(cwd);
6966
6847
  const userSettings = userOrchestratorSettingsPath();
6967
- const hasProject = existsSync18(projectSettings);
6968
- const hasUser = existsSync18(userSettings);
6848
+ const hasProject = existsSync17(projectSettings);
6849
+ const hasUser = existsSync17(userSettings);
6969
6850
  const digestVerbs = digestSpinnerVerbs(cwd, sessionId);
6970
6851
  if (!hasProject && !hasUser && !digestVerbs) return bundled;
6971
6852
  let merged = {};
6972
6853
  for (const path of [bundled, hasUser ? userSettings : null, hasProject ? projectSettings : null]) {
6973
- if (!path || !existsSync18(path)) continue;
6854
+ if (!path || !existsSync17(path)) continue;
6974
6855
  try {
6975
6856
  const parsed = JSON.parse(readFileSync17(path, "utf-8"));
6976
6857
  merged = { ...merged, ...parsed };
@@ -6987,11 +6868,11 @@ function resolveOrchestratorSettings(cwd, sessionId) {
6987
6868
  }
6988
6869
  function loadOrchestratorPrompt(cwd, sessionId, mode) {
6989
6870
  const projectPath = projectOrchestratorPromptPath(cwd);
6990
- if (existsSync18(projectPath)) {
6871
+ if (existsSync17(projectPath)) {
6991
6872
  return readFileSync17(projectPath, "utf-8");
6992
6873
  }
6993
6874
  const userPath = userOrchestratorPromptPath();
6994
- if (existsSync18(userPath)) {
6875
+ if (existsSync17(userPath)) {
6995
6876
  return readFileSync17(userPath, "utf-8");
6996
6877
  }
6997
6878
  const basePath = resolve7(import.meta.dirname, "../templates/orchestrator-base.md");
@@ -7019,7 +6900,7 @@ function buildCompletionContent(session) {
7019
6900
  lines.push("");
7020
6901
  }
7021
6902
  const logsDirPath = logsDir(session.cwd, session.id);
7022
- if (existsSync18(logsDirPath)) {
6903
+ if (existsSync17(logsDirPath)) {
7023
6904
  const logFiles = readdirSync10(logsDirPath).filter((f) => f.startsWith("cycle-") && f.endsWith(".md")).sort();
7024
6905
  if (logFiles.length > 0) {
7025
6906
  lines.push("### Cycle Logs\n");
@@ -7071,7 +6952,7 @@ function buildCompletionContent(session) {
7071
6952
  }
7072
6953
  }
7073
6954
  const reportsDirPath = reportsDir(session.cwd, session.id);
7074
- if (existsSync18(reportsDirPath)) {
6955
+ if (existsSync17(reportsDirPath)) {
7075
6956
  const reportFiles = readdirSync10(reportsDirPath).filter((f) => f.endsWith(".md"));
7076
6957
  if (reportFiles.length > 0) {
7077
6958
  lines.push("### Detailed Reports\n");
@@ -7097,7 +6978,7 @@ ${session.context}
7097
6978
  }
7098
6979
  } else {
7099
6980
  let ctxFiles = [];
7100
- if (existsSync18(ctxDir)) {
6981
+ if (existsSync17(ctxDir)) {
7101
6982
  ctxFiles = readdirSync10(ctxDir).filter((f) => f !== "CLAUDE.md");
7102
6983
  }
7103
6984
  if (ctxFiles.length > 0) {
@@ -7139,10 +7020,10 @@ ${agentLines}
7139
7020
  }
7140
7021
  }
7141
7022
  const strategyFile = strategyPath(session.cwd, session.id);
7142
- const strategyRef = existsSync18(strategyFile) ? `@${relative3(session.cwd, strategyFile)}` : "(empty)";
7143
- const roadmapRef = existsSync18(roadmapFile) ? `@${relative3(session.cwd, roadmapFile)}` : "(empty)";
7023
+ const strategyRef = existsSync17(strategyFile) ? `@${relative3(session.cwd, strategyFile)}` : "(empty)";
7024
+ const roadmapRef = existsSync17(roadmapFile) ? `@${relative3(session.cwd, roadmapFile)}` : "(empty)";
7144
7025
  const digestFile = digestPath(session.cwd, session.id);
7145
- const digestRef = existsSync18(digestFile) ? `@${relative3(session.cwd, digestFile)}` : "(not yet created)";
7026
+ const digestRef = existsSync17(digestFile) ? `@${relative3(session.cwd, digestFile)}` : "(not yet created)";
7146
7027
  const repos = detectRepos(session.cwd);
7147
7028
  let repositoriesSection = "\n\n## Repositories\n";
7148
7029
  if (repos.length === 0) {
@@ -7169,7 +7050,7 @@ ${agentLines}
7169
7050
  }
7170
7051
  }
7171
7052
  const goalFile = goalPath(session.cwd, session.id);
7172
- const goalContent = existsSync18(goalFile) ? readFileSync17(goalFile, "utf-8").trim() : session.task;
7053
+ const goalContent = existsSync17(goalFile) ? readFileSync17(goalFile, "utf-8").trim() : session.task;
7173
7054
  const modeContent = modeContentBuilders[mode]?.(session) ?? "";
7174
7055
  return `## Goal
7175
7056
 
@@ -7362,14 +7243,12 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
7362
7243
  prevModeStats = { cycles, activeMs };
7363
7244
  }
7364
7245
  await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode, cycleActiveMs);
7365
- if (mode && mode !== prevMode) {
7366
- await emitModeTransitionNotify(cwd, sessionId, prevMode, mode, prevModeStats);
7367
- }
7368
7246
  const freshSession = getSession(cwd, sessionId);
7369
7247
  const runningAgents = freshSession.agents.filter((a) => a.status === "running");
7370
7248
  if (runningAgents.length === 0) {
7371
7249
  console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
7372
7250
  }
7251
+ return prevModeStats ? { prevMode, nextMode: mode, prevModeStats } : void 0;
7373
7252
  }
7374
7253
  async function handleOrchestratorComplete(sessionId, cwd, report) {
7375
7254
  const session = getSession(cwd, sessionId);
@@ -7405,7 +7284,6 @@ var init_orchestrator = __esm({
7405
7284
  init_tmux();
7406
7285
  init_pane_registry();
7407
7286
  init_pane_monitor();
7408
- init_mode_notify();
7409
7287
  init_plugins();
7410
7288
  sessionWindowMap = /* @__PURE__ */ new Map();
7411
7289
  sessionOrchestratorPane = /* @__PURE__ */ new Map();
@@ -7573,6 +7451,36 @@ var init_status_dots = __esm({
7573
7451
  }
7574
7452
  });
7575
7453
 
7454
+ // src/daemon/mode-transition.ts
7455
+ function capitalize(s) {
7456
+ return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
7457
+ }
7458
+ function formatDuration(ms) {
7459
+ const sec = Math.round(ms / 1e3);
7460
+ if (sec < 60) return `${sec}s`;
7461
+ const min = Math.round(sec / 60);
7462
+ if (min < 60) return `${min}m`;
7463
+ const h = Math.floor(min / 60);
7464
+ const remM = min % 60;
7465
+ return remM ? `${h}h ${remM}m` : `${h}h`;
7466
+ }
7467
+ function buildModeTransitionCommentary(cwd, prevMode, nextMode, prevModeStats) {
7468
+ const popupTitle = ` Starting ${capitalize(nextMode)} `;
7469
+ const description = discoverOrchestratorModes(cwd).find((m) => m.name === nextMode)?.description?.trim();
7470
+ const label = prevModeStats.cycles === 1 ? "cycle" : "cycles";
7471
+ const context = [
7472
+ description ? `Entering ${capitalize(nextMode)} mode \u2014 ${description}` : `Entering ${capitalize(nextMode)} mode.`,
7473
+ `${capitalize(prevMode)}: ${prevModeStats.cycles} ${label} \xB7 ${formatDuration(prevModeStats.activeMs)} active.`
7474
+ ].join("\n");
7475
+ return { context, popupTitle };
7476
+ }
7477
+ var init_mode_transition = __esm({
7478
+ "src/daemon/mode-transition.ts"() {
7479
+ "use strict";
7480
+ init_orchestrator_modes();
7481
+ }
7482
+ });
7483
+
7576
7484
  // src/shared/manifest.ts
7577
7485
  import os from "os";
7578
7486
  function mapEffortFallback(level) {
@@ -7613,7 +7521,7 @@ var init_manifest = __esm({
7613
7521
  // src/shared/session-export.ts
7614
7522
  import { execFile as execFile3 } from "child_process";
7615
7523
  import { promisify as promisify2 } from "util";
7616
- import { existsSync as existsSync19, readFileSync as readFileSync19, mkdirSync as mkdirSync10, symlinkSync, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "fs";
7524
+ import { existsSync as existsSync18, readFileSync as readFileSync19, mkdirSync as mkdirSync10, symlinkSync, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "fs";
7617
7525
  import { homedir as homedir6 } from "os";
7618
7526
  import { join as join17 } from "path";
7619
7527
  function sanitizeName(name) {
@@ -7625,7 +7533,7 @@ function buildOutputPath(label, dir) {
7625
7533
  const base = `sisyphus-${label}-${date}`;
7626
7534
  let candidate = join17(dir, `${base}.zip`);
7627
7535
  let counter = 1;
7628
- while (existsSync19(candidate)) {
7536
+ while (existsSync18(candidate)) {
7629
7537
  counter++;
7630
7538
  candidate = join17(dir, `${base}-${counter}.zip`);
7631
7539
  }
@@ -7688,14 +7596,14 @@ async function exportSessionToZip(sessionId, cwd, options) {
7688
7596
  const reveal = options?.reveal ?? true;
7689
7597
  const sessDir = sessionDir(cwd, sessionId);
7690
7598
  const histDir = historySessionDir(sessionId);
7691
- const sessExists = existsSync19(sessDir);
7692
- const histExists = existsSync19(histDir);
7599
+ const sessExists = existsSync18(sessDir);
7600
+ const histExists = existsSync18(histDir);
7693
7601
  if (!sessExists && !histExists) {
7694
7602
  throw new Error(`No data found for session ${sessionId}`);
7695
7603
  }
7696
7604
  let label = sessionId.slice(0, 8);
7697
7605
  const stPath = statePath(cwd, sessionId);
7698
- if (existsSync19(stPath)) {
7606
+ if (existsSync18(stPath)) {
7699
7607
  try {
7700
7608
  const state = JSON.parse(readFileSync19(stPath, "utf-8"));
7701
7609
  if (state.name) {
@@ -7887,7 +7795,7 @@ var init_format = __esm({
7887
7795
  });
7888
7796
 
7889
7797
  // src/cli/deploy/creds.ts
7890
- import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync21 } from "fs";
7798
+ import { chmodSync, existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync21 } from "fs";
7891
7799
  import { createInterface } from "readline";
7892
7800
  function isValidProvider(value) {
7893
7801
  return PROVIDERS.includes(value);
@@ -7928,10 +7836,10 @@ var init_pricing = __esm({
7928
7836
  });
7929
7837
 
7930
7838
  // src/cli/deploy/runtime.ts
7931
- import { existsSync as existsSync21, readFileSync as readFileSync22, unlinkSync as unlinkSync3 } from "fs";
7839
+ import { existsSync as existsSync20, readFileSync as readFileSync22, unlinkSync as unlinkSync4 } from "fs";
7932
7840
  function readRuntimeState(provider) {
7933
7841
  const path = deployRuntimePath(provider);
7934
- if (!existsSync21(path)) return null;
7842
+ if (!existsSync20(path)) return null;
7935
7843
  try {
7936
7844
  return JSON.parse(readFileSync22(path, "utf-8"));
7937
7845
  } catch {
@@ -7955,15 +7863,15 @@ var init_tailnet = __esm({
7955
7863
  });
7956
7864
 
7957
7865
  // src/cli/deploy/templates.ts
7958
- import { existsSync as existsSync22 } from "fs";
7866
+ import { existsSync as existsSync21 } from "fs";
7959
7867
  import { dirname as dirname6, resolve as resolve9 } from "path";
7960
7868
  import { fileURLToPath } from "url";
7961
7869
  function deployRoot() {
7962
7870
  const here = dirname6(fileURLToPath(import.meta.url));
7963
7871
  const bundled = resolve9(here, "..", "deploy");
7964
- if (existsSync22(bundled)) return bundled;
7872
+ if (existsSync21(bundled)) return bundled;
7965
7873
  const sourceRoot = resolve9(here, "..", "..", "..", "deploy");
7966
- if (existsSync22(sourceRoot)) return sourceRoot;
7874
+ if (existsSync21(sourceRoot)) return sourceRoot;
7967
7875
  throw new Error(
7968
7876
  `Could not locate deploy/ templates. Looked at:
7969
7877
  ${bundled}
@@ -7992,7 +7900,7 @@ var init_tailscale = __esm({
7992
7900
 
7993
7901
  // src/cli/deploy/runner.ts
7994
7902
  import { spawn as spawn2, spawnSync } from "child_process";
7995
- import { copyFileSync as copyFileSync5, existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync23 } from "fs";
7903
+ import { copyFileSync as copyFileSync5, existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync23 } from "fs";
7996
7904
  function readOutputs(provider) {
7997
7905
  const result = spawnSync("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
7998
7906
  cwd: providerModuleDir(provider),
@@ -8018,7 +7926,7 @@ function readOutputs(provider) {
8018
7926
  }
8019
7927
  }
8020
7928
  function isProvisioned(provider) {
8021
- if (!existsSync23(deployStatePath(provider))) return false;
7929
+ if (!existsSync22(deployStatePath(provider))) return false;
8022
7930
  return readOutputs(provider) !== null;
8023
7931
  }
8024
7932
  function effectiveSshTarget(provider) {
@@ -8137,7 +8045,7 @@ var init_grove = __esm({
8137
8045
 
8138
8046
  // src/cli/cloud/repo.ts
8139
8047
  import { spawnSync as spawnSync3 } from "child_process";
8140
- import { existsSync as existsSync24 } from "fs";
8048
+ import { existsSync as existsSync23 } from "fs";
8141
8049
  import { basename as basename6, join as join18 } from "path";
8142
8050
  function captureGit(args, cwd) {
8143
8051
  const result = spawnSync3("git", args, {
@@ -8178,10 +8086,10 @@ function buildRsyncArgs(localDir, remoteTarget) {
8178
8086
  ];
8179
8087
  }
8180
8088
  function detectPackageManager(toplevel) {
8181
- if (existsSync24(join18(toplevel, "pnpm-lock.yaml"))) return "pnpm";
8182
- if (existsSync24(join18(toplevel, "bun.lockb"))) return "bun";
8183
- if (existsSync24(join18(toplevel, "yarn.lock"))) return "yarn";
8184
- if (existsSync24(join18(toplevel, "package-lock.json"))) return "npm";
8089
+ if (existsSync23(join18(toplevel, "pnpm-lock.yaml"))) return "pnpm";
8090
+ if (existsSync23(join18(toplevel, "bun.lockb"))) return "bun";
8091
+ if (existsSync23(join18(toplevel, "yarn.lock"))) return "yarn";
8092
+ if (existsSync23(join18(toplevel, "package-lock.json"))) return "npm";
8185
8093
  return null;
8186
8094
  }
8187
8095
  function packageManagerInstallCmd(pm) {
@@ -8382,7 +8290,7 @@ __export(cloud_handoff_exports, {
8382
8290
  triggerForceHandoff: () => triggerForceHandoff
8383
8291
  });
8384
8292
  import { spawn as spawn5 } from "child_process";
8385
- import { existsSync as existsSync25 } from "fs";
8293
+ import { existsSync as existsSync24 } from "fs";
8386
8294
  import { join as join19 } from "path";
8387
8295
  function runRsync2(args) {
8388
8296
  return new Promise((resolve13) => {
@@ -8422,7 +8330,7 @@ async function syncSessionState(cwd, sessionId, repo, target, provider) {
8422
8330
  const candidates = ["config.json", "orchestrator.md", "orchestrator-settings.json"];
8423
8331
  for (const name of candidates) {
8424
8332
  const localPath = join19(localProject, name);
8425
- if (!existsSync25(localPath)) continue;
8333
+ if (!existsSync24(localPath)) continue;
8426
8334
  const remotePath = `${boxRepoPath(repo)}/.sisyphus/${name}`;
8427
8335
  const args = [
8428
8336
  "-avz",
@@ -8589,14 +8497,14 @@ var init_cloud_handoff = __esm({
8589
8497
 
8590
8498
  // src/daemon/session-manager.ts
8591
8499
  import { v4 as uuidv4 } from "uuid";
8592
- import { existsSync as existsSync26, readFileSync as readFileSync24, readdirSync as readdirSync11, rmSync as rmSync6 } from "fs";
8500
+ import { existsSync as existsSync25, readFileSync as readFileSync24, readdirSync as readdirSync11, rmSync as rmSync6 } from "fs";
8593
8501
  function truncate(s, max) {
8594
8502
  return s.length <= max ? s : s.slice(0, max) + "...";
8595
8503
  }
8596
8504
  function readGoal(cwd, sessionId, fallback) {
8597
8505
  try {
8598
8506
  const p = goalPath(cwd, sessionId);
8599
- if (existsSync26(p)) return readFileSync24(p, "utf-8").trim();
8507
+ if (existsSync25(p)) return readFileSync24(p, "utf-8").trim();
8600
8508
  } catch {
8601
8509
  }
8602
8510
  return fallback;
@@ -8650,7 +8558,7 @@ function fireHaikuNaming(sessionId, cwd, fallbackTmuxName, task) {
8650
8558
  console.error(`[sisyphus] Name generation failed for session ${sessionId}:`, err);
8651
8559
  });
8652
8560
  }
8653
- function fireCommentary(event, companion, context, flash = false, repo, sessionId) {
8561
+ function fireCommentary(event, companion, context, flash = false, repo, sessionId, popupTitle) {
8654
8562
  const memoryCtx = buildMemoryContext(repo);
8655
8563
  generateCommentary(event, companion, context, memoryCtx).then((text) => {
8656
8564
  if (text) {
@@ -8658,7 +8566,7 @@ function fireCommentary(event, companion, context, flash = false, repo, sessionI
8658
8566
  const c = loadCompanion();
8659
8567
  recordCommentary(c, text, event);
8660
8568
  if (flash) {
8661
- const feedback = showCommentaryPopup(text);
8569
+ const feedback = showCommentaryPopupQueue([{ text, title: popupTitle }]);
8662
8570
  if (feedback) {
8663
8571
  recordFeedback(c, text, feedback.rating, event, feedback.comment);
8664
8572
  if (sessionId) {
@@ -8835,7 +8743,7 @@ It is the other session's responsibility. You do not need to monitor it.
8835
8743
  function pruneOldSessions(cwd) {
8836
8744
  try {
8837
8745
  const dir = sessionsDir(cwd);
8838
- if (!existsSync26(dir)) return;
8746
+ if (!existsSync25(dir)) return;
8839
8747
  const entries = readdirSync11(dir, { withFileTypes: true });
8840
8748
  const candidates = [];
8841
8749
  for (const entry of entries) {
@@ -8973,7 +8881,7 @@ function getSessionStatus(cwd, sessionId) {
8973
8881
  }
8974
8882
  function countSessions(cwd) {
8975
8883
  const dir = sessionsDir(cwd);
8976
- if (!existsSync26(dir)) return 0;
8884
+ if (!existsSync25(dir)) return 0;
8977
8885
  let n = 0;
8978
8886
  for (const entry of readdirSync11(dir, { withFileTypes: true })) {
8979
8887
  if (entry.isDirectory()) n++;
@@ -8982,7 +8890,7 @@ function countSessions(cwd) {
8982
8890
  }
8983
8891
  function listSessions(cwd) {
8984
8892
  const dir = sessionsDir(cwd);
8985
- if (!existsSync26(dir)) return [];
8893
+ if (!existsSync25(dir)) return [];
8986
8894
  const entries = readdirSync11(dir, { withFileTypes: true });
8987
8895
  const sessions = [];
8988
8896
  for (const entry of entries) {
@@ -9046,23 +8954,6 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
9046
8954
  const companion = loadCompanion();
9047
8955
  companion.spinnerVerbIndex = (companion.spinnerVerbIndex + 1) % SPINNER_VERBS.length;
9048
8956
  saveCompanion(companion);
9049
- const goal = readGoal(cwd, sessionId, session.task);
9050
- const modeLabel = lastCycle?.mode ? ` (${lastCycle.mode})` : "";
9051
- const agentMap = new Map(session.agents.map((a) => [a.id, a]));
9052
- const spawnedThisCycle = (lastCycle?.agentsSpawned ?? []).map((id) => agentMap.get(id)).filter(Boolean).map((a) => `${a.name} (${a.agentType.replace(/^sisyphus:/, "")}, ${a.status})`).join(", ");
9053
- let cycleCtx = `Cycle ${cycleNumber}${modeLabel} complete. Goal: ${truncate(goal, 80)}`;
9054
- if (spawnedThisCycle) cycleCtx += `
9055
- Agents: ${truncate(spawnedThisCycle, 200)}`;
9056
- try {
9057
- const logPath2 = cycleLogPath(cwd, sessionId, cycleNumber);
9058
- if (existsSync26(logPath2)) {
9059
- const log = readFileSync24(logPath2, "utf-8").trim();
9060
- if (log) cycleCtx += `
9061
- Cycle log: ${truncate(log, 200)}`;
9062
- }
9063
- } catch {
9064
- }
9065
- fireCommentary("cycle-boundary", companion, cycleCtx, true, session.cwd, sessionId);
9066
8957
  } catch {
9067
8958
  }
9068
8959
  setImmediate(async () => {
@@ -9235,8 +9126,23 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
9235
9126
  await updateSessionStatus(cwd, sessionId, "active");
9236
9127
  }
9237
9128
  respawningSessions.add(sessionId);
9238
- await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
9129
+ const transition = await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
9239
9130
  orchestratorDone.add(sessionId);
9131
+ if (transition) {
9132
+ try {
9133
+ const companion = loadCompanion();
9134
+ const goal = readGoal(cwd, sessionId, getSession(cwd, sessionId).task);
9135
+ const { context, popupTitle } = buildModeTransitionCommentary(
9136
+ cwd,
9137
+ transition.prevMode,
9138
+ transition.nextMode,
9139
+ transition.prevModeStats
9140
+ );
9141
+ fireCommentary("mode-transition", companion, `Goal: ${truncate(goal, 100)}
9142
+ ${context}`, true, cwd, sessionId, popupTitle);
9143
+ } catch {
9144
+ }
9145
+ }
9240
9146
  try {
9241
9147
  recomputeDots();
9242
9148
  } catch {
@@ -9291,6 +9197,7 @@ async function handleComplete(sessionId, cwd, report) {
9291
9197
  console.warn("[sisyphus] Sentiment generation failed:", err instanceof Error ? err.message : err);
9292
9198
  });
9293
9199
  const config = loadConfig(cwd);
9200
+ const uploadCfg = config.upload;
9294
9201
  if (isUploadConfigured(config.upload)) {
9295
9202
  runSessionUploadAndPersist({
9296
9203
  sessionId,
@@ -9302,11 +9209,10 @@ async function handleComplete(sessionId, cwd, report) {
9302
9209
  }).catch(() => {
9303
9210
  console.warn("[sisyphus] upload pipeline crashed; check uploadError on session state");
9304
9211
  });
9305
- } else if (config.upload) {
9306
- const partialUpload = config.upload;
9212
+ } else if (uploadCfg) {
9307
9213
  const missing = [];
9308
- if (!partialUpload.url) missing.push("upload.url");
9309
- if (!partialUpload.token) missing.push("upload.token");
9214
+ if (!uploadCfg.url) missing.push("upload.url");
9215
+ if (!uploadCfg.token) missing.push("upload.token");
9310
9216
  const error = `upload skipped: missing ${missing.join(", ")}`;
9311
9217
  console.warn(`[sisyphus] ${error}`);
9312
9218
  updateSession(cwd, sessionId, { uploadStatus: "failed", uploadError: error }).catch(() => console.warn("[sisyphus] failed to persist upload skip status"));
@@ -9580,6 +9486,7 @@ var init_session_manager = __esm({
9580
9486
  init_companion_render();
9581
9487
  init_companion_commentary();
9582
9488
  init_companion_popup();
9489
+ init_mode_transition();
9583
9490
  init_history();
9584
9491
  init_uploader();
9585
9492
  init_upload();
@@ -9701,7 +9608,7 @@ var init_transcript_digest = __esm({
9701
9608
  import {
9702
9609
  closeSync as closeSync2,
9703
9610
  constants,
9704
- existsSync as existsSync27,
9611
+ existsSync as existsSync26,
9705
9612
  fstatSync as fstatSync2,
9706
9613
  lstatSync,
9707
9614
  openSync as openSync2,
@@ -9723,7 +9630,7 @@ async function generateVisualForQuestion(opts) {
9723
9630
  if (!question) return { ok: false, error: `qid ${opts.qid} not found in decisions` };
9724
9631
  const mdPath = askVisualMarkdownPath(opts.cwd, opts.sessionId, opts.askId, opts.qid);
9725
9632
  const ansiPath = askVisualAnsiPath(opts.cwd, opts.sessionId, opts.askId, opts.qid);
9726
- if (!opts.force && existsSync27(mdPath) && existsSync27(ansiPath)) {
9633
+ if (!opts.force && existsSync26(mdPath) && existsSync26(ansiPath)) {
9727
9634
  return { ok: true, markdownPath: mdPath, ansiPath, turns: 0 };
9728
9635
  }
9729
9636
  if (opts.force) {
@@ -9797,7 +9704,7 @@ function buildUserPrompt(q, askedBy, ctx) {
9797
9704
  }
9798
9705
  function readSystemPrompt() {
9799
9706
  if (cachedSystemPrompt !== void 0) return cachedSystemPrompt;
9800
- const found = SYSTEM_PROMPT_CANDIDATES.find((p) => existsSync27(p));
9707
+ const found = SYSTEM_PROMPT_CANDIDATES.find((p) => existsSync26(p));
9801
9708
  if (found === void 0) {
9802
9709
  throw new Error(
9803
9710
  `termrender-haiku-system.md not found in any candidate location: ${SYSTEM_PROMPT_CANDIDATES.join(", ")}`
@@ -9944,7 +9851,7 @@ var init_ask_visual = __esm({
9944
9851
 
9945
9852
  // src/daemon/server.ts
9946
9853
  import { createServer } from "net";
9947
- import { unlinkSync as unlinkSync4, existsSync as existsSync28, writeFileSync as writeFileSync15, readFileSync as readFileSync26, mkdirSync as mkdirSync13, readdirSync as readdirSync12, rmSync as rmSync8, chmodSync as chmodSync2 } from "fs";
9854
+ import { unlinkSync as unlinkSync5, existsSync as existsSync27, writeFileSync as writeFileSync15, readFileSync as readFileSync26, mkdirSync as mkdirSync13, readdirSync as readdirSync12, rmSync as rmSync8, chmodSync as chmodSync2 } from "fs";
9948
9855
  import { join as join21, basename as basename7, dirname as dirname8 } from "path";
9949
9856
  import { scanInbox } from "@crouton-kit/humanloop";
9950
9857
  function setCompositor(c) {
@@ -9964,7 +9871,7 @@ function persistSessionRegistry() {
9964
9871
  }
9965
9872
  function loadSessionRegistry() {
9966
9873
  const p = registryPath();
9967
- if (!existsSync28(p)) return {};
9874
+ if (!existsSync27(p)) return {};
9968
9875
  try {
9969
9876
  return JSON.parse(readFileSync26(p, "utf-8"));
9970
9877
  } catch (err) {
@@ -10020,7 +9927,7 @@ function collectAllSessionIds() {
10020
9927
  scannedCwds.add(cwd);
10021
9928
  try {
10022
9929
  const dir = sessionsDir(cwd);
10023
- if (!existsSync28(dir)) continue;
9930
+ if (!existsSync27(dir)) continue;
10024
9931
  for (const entry of readdirSync12(dir, { withFileTypes: true })) {
10025
9932
  if (entry.isDirectory() && !idToCwd.has(entry.name)) {
10026
9933
  idToCwd.set(entry.name, cwd);
@@ -10252,7 +10159,7 @@ async function handleRequest(req) {
10252
10159
  let tracking = sessionTrackingMap.get(req.sessionId);
10253
10160
  if (!tracking) {
10254
10161
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10255
- if (existsSync28(stateFile)) {
10162
+ if (existsSync27(stateFile)) {
10256
10163
  tracking = { cwd: req.cwd, messageCounter: 0 };
10257
10164
  sessionTrackingMap.set(req.sessionId, tracking);
10258
10165
  persistSessionRegistry();
@@ -10274,7 +10181,7 @@ async function handleRequest(req) {
10274
10181
  }
10275
10182
  case "clear-orphan": {
10276
10183
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10277
- if (!existsSync28(stateFile)) {
10184
+ if (!existsSync27(stateFile)) {
10278
10185
  return {
10279
10186
  ok: false,
10280
10187
  error: errNotFound("unknown_session", {
@@ -10332,7 +10239,7 @@ async function handleRequest(req) {
10332
10239
  let tracking = sessionTrackingMap.get(req.sessionId);
10333
10240
  if (!tracking) {
10334
10241
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10335
- if (existsSync28(stateFile)) {
10242
+ if (existsSync27(stateFile)) {
10336
10243
  registerSessionCwd(req.sessionId, req.cwd);
10337
10244
  tracking = sessionTrackingMap.get(req.sessionId);
10338
10245
  } else {
@@ -10346,7 +10253,7 @@ async function handleRequest(req) {
10346
10253
  let tracking = sessionTrackingMap.get(req.sessionId);
10347
10254
  if (!tracking) {
10348
10255
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10349
- if (existsSync28(stateFile)) {
10256
+ if (existsSync27(stateFile)) {
10350
10257
  registerSessionCwd(req.sessionId, req.cwd);
10351
10258
  tracking = sessionTrackingMap.get(req.sessionId);
10352
10259
  } else {
@@ -10363,7 +10270,7 @@ async function handleRequest(req) {
10363
10270
  let tracking = sessionTrackingMap.get(req.sessionId);
10364
10271
  if (!tracking) {
10365
10272
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10366
- if (existsSync28(stateFile)) {
10273
+ if (existsSync27(stateFile)) {
10367
10274
  tracking = { cwd: req.cwd, messageCounter: 0 };
10368
10275
  sessionTrackingMap.set(req.sessionId, tracking);
10369
10276
  persistSessionRegistry();
@@ -10433,7 +10340,7 @@ async function handleRequest(req) {
10433
10340
  }
10434
10341
  case "set-upload-status": {
10435
10342
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10436
- if (!existsSync28(stateFile)) {
10343
+ if (!existsSync27(stateFile)) {
10437
10344
  return unknownSessionError(req.sessionId);
10438
10345
  }
10439
10346
  try {
@@ -10632,6 +10539,7 @@ async function handleRequest(req) {
10632
10539
  const reviewItems = [];
10633
10540
  for (const [sessionId, tracking] of sessionTrackingMap) {
10634
10541
  if (!tracking.cwd) continue;
10542
+ if (tracking.cwd !== req.cwd) continue;
10635
10543
  askDirs.push(askDir(tracking.cwd, sessionId));
10636
10544
  reviewItems.push(...listReviewInboxItems(tracking.cwd, sessionId));
10637
10545
  }
@@ -10655,6 +10563,26 @@ async function handleRequest(req) {
10655
10563
  });
10656
10564
  return { ok: true, data: { items: itemsWithName } };
10657
10565
  }
10566
+ case "focus-set": {
10567
+ const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
10568
+ if (!ID_RE.test(req.askId)) return {
10569
+ ok: false,
10570
+ error: errUsage("invalid_ask_id", { message: `Invalid askId: ${req.askId}`, received: req.askId })
10571
+ };
10572
+ pendingFocus = { cwd: req.cwd, sessionId: req.sessionId, askId: req.askId, requestedAt: Date.now() };
10573
+ return { ok: true };
10574
+ }
10575
+ case "focus-get": {
10576
+ if (pendingFocus && pendingFocus.cwd === req.cwd) {
10577
+ const captured = pendingFocus;
10578
+ pendingFocus = null;
10579
+ if (Date.now() - captured.requestedAt > FOCUS_TTL_MS) {
10580
+ return { ok: true, data: { focus: null } };
10581
+ }
10582
+ return { ok: true, data: { focus: captured } };
10583
+ }
10584
+ return { ok: true, data: { focus: null } };
10585
+ }
10658
10586
  case "cloud-handoff": {
10659
10587
  const tracking = sessionTrackingMap.get(req.sessionId);
10660
10588
  if (!tracking) return unknownSessionError(req.sessionId);
@@ -10802,8 +10730,8 @@ async function handleRequest(req) {
10802
10730
  function startServer() {
10803
10731
  return new Promise((resolve13, reject) => {
10804
10732
  const sock = socketPath();
10805
- if (existsSync28(sock)) {
10806
- unlinkSync4(sock);
10733
+ if (existsSync27(sock)) {
10734
+ unlinkSync5(sock);
10807
10735
  }
10808
10736
  server = createServer((conn) => {
10809
10737
  let buffer = "";
@@ -10857,15 +10785,15 @@ function stopServer() {
10857
10785
  }
10858
10786
  server.close(() => {
10859
10787
  const sock = socketPath();
10860
- if (existsSync28(sock)) {
10861
- unlinkSync4(sock);
10788
+ if (existsSync27(sock)) {
10789
+ unlinkSync5(sock);
10862
10790
  }
10863
10791
  server = null;
10864
10792
  resolve13();
10865
10793
  });
10866
10794
  });
10867
10795
  }
10868
- var server, compositor, sessionTrackingMap;
10796
+ var server, compositor, sessionTrackingMap, pendingFocus, FOCUS_TTL_MS;
10869
10797
  var init_server = __esm({
10870
10798
  "src/daemon/server.ts"() {
10871
10799
  "use strict";
@@ -10889,6 +10817,8 @@ var init_server = __esm({
10889
10817
  server = null;
10890
10818
  compositor = null;
10891
10819
  sessionTrackingMap = /* @__PURE__ */ new Map();
10820
+ pendingFocus = null;
10821
+ FOCUS_TTL_MS = 3e4;
10892
10822
  }
10893
10823
  });
10894
10824
 
@@ -10897,7 +10827,7 @@ init_paths();
10897
10827
  init_config();
10898
10828
  init_server();
10899
10829
  init_orphan_sweep();
10900
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync30, writeFileSync as writeFileSync17, unlinkSync as unlinkSync6, existsSync as existsSync33 } from "fs";
10830
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync30, writeFileSync as writeFileSync17, unlinkSync as unlinkSync7, existsSync as existsSync32 } from "fs";
10901
10831
  import { execSync as execSync8 } from "child_process";
10902
10832
  import { setTimeout as sleep } from "timers/promises";
10903
10833
  import { Command } from "commander";
@@ -10907,8 +10837,8 @@ init_ask_store();
10907
10837
  init_state();
10908
10838
  init_server();
10909
10839
  init_paths();
10910
- import { ulid as ulid3 } from "ulid";
10911
- import { existsSync as existsSync29 } from "fs";
10840
+ import { ulid as ulid2 } from "ulid";
10841
+ import { existsSync as existsSync28 } from "fs";
10912
10842
  var HEARTBEAT_ASKED_BY2 = "system:heartbeat";
10913
10843
  var HEARTBEAT_THRESHOLD_MS = 60 * 60 * 1e3;
10914
10844
  var HEARTBEAT_SCAN_INTERVAL_MS = 15 * 60 * 1e3;
@@ -10951,7 +10881,7 @@ async function emitHeartbeatAsk(cwd, sessionId, original) {
10951
10881
  },
10952
10882
  interactions: [interaction]
10953
10883
  };
10954
- const askId = ulid3();
10884
+ const askId = ulid2();
10955
10885
  createAsk(cwd, sessionId, {
10956
10886
  askId,
10957
10887
  askedBy: HEARTBEAT_ASKED_BY2,
@@ -10967,43 +10897,14 @@ async function emitHeartbeatAsk(cwd, sessionId, original) {
10967
10897
  heartbeatAskId: askId
10968
10898
  });
10969
10899
  }
10970
- function isModeGateStale(deck, currentMode) {
10971
- const source = deck.source;
10972
- const chain = source?.modeChain;
10973
- if (!chain || chain.length === 0) return false;
10974
- if (!currentMode) return false;
10975
- return !chain.some((e) => e.mode === currentMode);
10976
- }
10977
10900
  async function scanSessionForStaleAsks(cwd, sessionId) {
10978
10901
  const now = Date.now();
10979
- let currentMode;
10980
- try {
10981
- const session = getSession(cwd, sessionId);
10982
- currentMode = session.orchestratorCycles[session.orchestratorCycles.length - 1]?.mode;
10983
- } catch {
10984
- }
10985
10902
  for (const askId of listAsks(cwd, sessionId)) {
10986
10903
  try {
10987
10904
  const meta = readMeta(cwd, sessionId, askId);
10988
10905
  if (!meta) continue;
10989
10906
  if (meta.status === "answered") continue;
10990
10907
  if (meta.orphaned) continue;
10991
- if (meta.modeTransition === true) {
10992
- const deck = readDecisions(cwd, sessionId, askId);
10993
- if (deck && isModeGateStale(deck, currentMode)) {
10994
- writeOutput(cwd, sessionId, askId, [{
10995
- id: "mode-transition",
10996
- selectedOptionId: "ack",
10997
- freetext: "auto-resolved: session advanced past mode-transition"
10998
- }]);
10999
- await updateMeta(cwd, sessionId, askId, {
11000
- status: "answered",
11001
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
11002
- });
11003
- console.log(`[sisyphus] mode-gate auto-resolved ask ${askId} for session ${sessionId} (currentMode: ${currentMode})`);
11004
- }
11005
- continue;
11006
- }
11007
10908
  if (meta.heartbeatNotifiedAt) continue;
11008
10909
  if (meta.askedBy === HEARTBEAT_ASKED_BY2) continue;
11009
10910
  const askedAtMs = new Date(meta.askedAt).getTime();
@@ -11021,7 +10922,7 @@ async function scanSessionForStaleAsks(cwd, sessionId) {
11021
10922
  async function scanAllSessionsForStaleAsks() {
11022
10923
  const reg = loadSessionRegistry();
11023
10924
  for (const [sessionId, cwd] of Object.entries(reg)) {
11024
- if (!existsSync29(statePath(cwd, sessionId))) continue;
10925
+ if (!existsSync28(statePath(cwd, sessionId))) continue;
11025
10926
  try {
11026
10927
  await scanSessionForStaleAsks(cwd, sessionId);
11027
10928
  } catch (err) {
@@ -11084,14 +10985,14 @@ var DEFAULT_STATUS_BAR_CONFIG = {
11084
10985
  init_tmux();
11085
10986
  init_status_dots();
11086
10987
  init_companion();
11087
- import { readFileSync as readFileSync27, existsSync as existsSync30 } from "fs";
10988
+ import { readFileSync as readFileSync27, existsSync as existsSync29 } from "fs";
11088
10989
  import { homedir as homedir8 } from "os";
11089
10990
  import { join as join22 } from "path";
11090
10991
  var STATUS_BAR_BG = "#1d1e21";
11091
10992
  var SESSION_ORDER_PATH = join22(homedir8(), ".config", "tmux", "session-order");
11092
10993
  function getSessionOrder() {
11093
10994
  try {
11094
- if (!existsSync30(SESSION_ORDER_PATH)) return [];
10995
+ if (!existsSync29(SESSION_ORDER_PATH)) return [];
11095
10996
  return readFileSync27(SESSION_ORDER_PATH, "utf-8").split("\n").filter(Boolean);
11096
10997
  } catch {
11097
10998
  return [];
@@ -11644,6 +11545,7 @@ function writeEmptyManifest() {
11644
11545
 
11645
11546
  // src/daemon/index.ts
11646
11547
  init_agent();
11548
+ init_ask_store();
11647
11549
  init_orphan_asks();
11648
11550
  init_process();
11649
11551
  init_orchestrator();
@@ -11655,7 +11557,7 @@ init_state();
11655
11557
  init_paths();
11656
11558
  init_version();
11657
11559
  import { execSync as execSync7 } from "child_process";
11658
- import { writeFileSync as writeFileSync16, unlinkSync as unlinkSync5, lstatSync as lstatSync2 } from "fs";
11560
+ import { writeFileSync as writeFileSync16, unlinkSync as unlinkSync6, lstatSync as lstatSync2 } from "fs";
11659
11561
  import { resolve as resolve11 } from "path";
11660
11562
  import { get } from "https";
11661
11563
  function isNewer(latest, current) {
@@ -11732,7 +11634,7 @@ function markUpdating(version) {
11732
11634
  }
11733
11635
  function clearUpdating() {
11734
11636
  try {
11735
- unlinkSync5(daemonUpdatingPath());
11637
+ unlinkSync6(daemonUpdatingPath());
11736
11638
  } catch {
11737
11639
  }
11738
11640
  }
@@ -11782,7 +11684,7 @@ function stopPeriodicUpdateCheck() {
11782
11684
  }
11783
11685
 
11784
11686
  // src/daemon/plugin-install.ts
11785
- import { copyFileSync as copyFileSync6, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync4, existsSync as existsSync31, readFileSync as readFileSync28, chmodSync as chmodSync3 } from "fs";
11687
+ import { copyFileSync as copyFileSync6, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync4, existsSync as existsSync30, readFileSync as readFileSync28, chmodSync as chmodSync3 } from "fs";
11786
11688
  import { join as join23, resolve as resolve12 } from "path";
11787
11689
  import { homedir as homedir9 } from "os";
11788
11690
  var PLUGIN_NAME = "sisyphus-tmux";
@@ -11796,7 +11698,7 @@ function copyDir(src, dest) {
11796
11698
  copyDir(srcPath, destPath);
11797
11699
  } else {
11798
11700
  const srcMtime = statSync4(srcPath).mtimeMs;
11799
- const destMtime = existsSync31(destPath) ? statSync4(destPath).mtimeMs : 0;
11701
+ const destMtime = existsSync30(destPath) ? statSync4(destPath).mtimeMs : 0;
11800
11702
  if (srcMtime > destMtime) {
11801
11703
  copyFileSync6(srcPath, destPath);
11802
11704
  }
@@ -11806,7 +11708,7 @@ function copyDir(src, dest) {
11806
11708
  function pluginNeedsUpdate(sourceDir) {
11807
11709
  const srcHooks = join23(sourceDir, "hooks", "hooks.json");
11808
11710
  const destHooks = join23(INSTALL_DIR, "hooks", "hooks.json");
11809
- if (!existsSync31(destHooks)) return true;
11711
+ if (!existsSync30(destHooks)) return true;
11810
11712
  try {
11811
11713
  return readFileSync28(srcHooks, "utf-8") !== readFileSync28(destHooks, "utf-8");
11812
11714
  } catch {
@@ -11815,7 +11717,7 @@ function pluginNeedsUpdate(sourceDir) {
11815
11717
  }
11816
11718
  function installPlugin() {
11817
11719
  const sourceDir = resolve12(import.meta.dirname, "../templates/sisyphus-tmux-plugin");
11818
- if (!existsSync31(sourceDir)) {
11720
+ if (!existsSync30(sourceDir)) {
11819
11721
  console.error(`[plugin-install] Source dir not found: ${sourceDir}`);
11820
11722
  return;
11821
11723
  }
@@ -11823,7 +11725,7 @@ function installPlugin() {
11823
11725
  try {
11824
11726
  copyDir(sourceDir, INSTALL_DIR);
11825
11727
  const hookScript = join23(INSTALL_DIR, "hooks", "tmux-state.sh");
11826
- if (existsSync31(hookScript)) {
11728
+ if (existsSync30(hookScript)) {
11827
11729
  try {
11828
11730
  chmodSync3(hookScript, 493);
11829
11731
  } catch {
@@ -11879,7 +11781,7 @@ init_upload();
11879
11781
  init_version();
11880
11782
  init_state();
11881
11783
  init_uploader();
11882
- import { existsSync as existsSync32, readdirSync as readdirSync14, readFileSync as readFileSync29 } from "fs";
11784
+ import { existsSync as existsSync31, readdirSync as readdirSync14, readFileSync as readFileSync29 } from "fs";
11883
11785
  var backfillStarted = false;
11884
11786
  async function backfillUploads() {
11885
11787
  if (backfillStarted) return;
@@ -11912,7 +11814,7 @@ async function backfillUploads() {
11912
11814
  let failed = 0;
11913
11815
  let unexportable = 0;
11914
11816
  for (const { sessionId, cwd } of candidates) {
11915
- if (!existsSync32(statePath(cwd, sessionId))) {
11817
+ if (!existsSync31(statePath(cwd, sessionId))) {
11916
11818
  unexportable++;
11917
11819
  continue;
11918
11820
  }
@@ -11990,7 +11892,7 @@ function isLaunchdManaged() {
11990
11892
  }
11991
11893
  function releasePidLock() {
11992
11894
  try {
11993
- unlinkSync6(daemonPidPath());
11895
+ unlinkSync7(daemonPidPath());
11994
11896
  } catch {
11995
11897
  }
11996
11898
  }
@@ -12029,7 +11931,7 @@ function stopDaemon() {
12029
11931
  }
12030
11932
  async function recoverOneSession(sessionId, cwd, tmuxIdSet, tmuxNameToId, panesByWindow) {
12031
11933
  const stateFile = statePath(cwd, sessionId);
12032
- if (!existsSync33(stateFile)) return false;
11934
+ if (!existsSync32(stateFile)) return false;
12033
11935
  let session;
12034
11936
  try {
12035
11937
  session = JSON.parse(readFileSync30(stateFile, "utf-8"));
@@ -12040,6 +11942,10 @@ async function recoverOneSession(sessionId, cwd, tmuxIdSet, tmuxNameToId, panesB
12040
11942
  if (session.status !== "active" && session.status !== "paused") return false;
12041
11943
  registerSessionCwd(sessionId, cwd);
12042
11944
  resetAgentCounterFromState(sessionId, session.agents ?? []);
11945
+ const clearedClaims = await clearStaleAskClaims(cwd, sessionId);
11946
+ if (clearedClaims > 0) {
11947
+ console.log(`[sisyphus] Cleared ${clearedClaims} stale ask claim(s) for session ${sessionId} on recovery`);
11948
+ }
12043
11949
  if (!session.tmuxSessionName) return true;
12044
11950
  let sessionAlive = false;
12045
11951
  let currentTmuxId = session.tmuxSessionId;