sisyphi 1.2.18 → 1.2.20

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
@@ -1058,6 +1058,31 @@ var init_spawn_helpers = __esm({
1058
1058
  }
1059
1059
  });
1060
1060
 
1061
+ // src/cli/help-rubric.ts
1062
+ var ROOT_AFTER_HELP;
1063
+ var init_help_rubric = __esm({
1064
+ "src/cli/help-rubric.ts"() {
1065
+ "use strict";
1066
+ ROOT_AFTER_HELP = `
1067
+ I/O contract: flags and positional args on input, JSON on stdout (JSONL for streams).
1068
+
1069
+ Exit codes:
1070
+ 0 success
1071
+ 1 permanent error (fallback)
1072
+ 2 usage error (bad args/shape)
1073
+ 3 not found
1074
+ 4 ambiguous (multiple matches \u2014 see error.candidates)
1075
+ 5 conflict (already-exists, wrong-state)
1076
+ 60 transient (retry-safe: daemon down, timeout, lock contention)
1077
+
1078
+ Errors:
1079
+ {"ok": false,
1080
+ "error": {"code": "<stable-enum>", "kind": "<usage|not_found|ambiguous|conflict|transient|permanent>",
1081
+ "message": "...", "received"?: ..., "expected"?: ..., "next"?: "...", "candidates"?: [...]}}
1082
+ `;
1083
+ }
1084
+ });
1085
+
1061
1086
  // src/daemon/help-inject.ts
1062
1087
  import { execSync } from "child_process";
1063
1088
  function injectHelp(text) {
@@ -1078,6 +1103,12 @@ function injectHelp(text) {
1078
1103
  };
1079
1104
  return text.replace(/\{\{HELP:([^}]+)\}\}/g, (_m, cmd) => {
1080
1105
  const c = cmd.trim();
1106
+ if (c === ".") {
1107
+ const root = renderHelp("").replace(ROOT_AFTER_HELP.trim(), "").trimEnd();
1108
+ return `<cli-guide bash="sis -h">
1109
+ ${root}
1110
+ </cli-guide>`;
1111
+ }
1081
1112
  return `<cli-guide bash="sis ${c} -h">
1082
1113
  ${renderHelp(c)}
1083
1114
  </cli-guide>`;
@@ -1087,6 +1118,7 @@ var init_help_inject = __esm({
1087
1118
  "src/daemon/help-inject.ts"() {
1088
1119
  "use strict";
1089
1120
  init_spawn_helpers();
1121
+ init_help_rubric();
1090
1122
  }
1091
1123
  });
1092
1124
 
@@ -2472,7 +2504,7 @@ var init_history = __esm({
2472
2504
  });
2473
2505
 
2474
2506
  // src/shared/platform.ts
2475
- import { execSync as execSync4 } from "child_process";
2507
+ import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
2476
2508
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
2477
2509
  function detectPlatform() {
2478
2510
  if (cachedPlatform) return cachedPlatform;
@@ -2662,13 +2694,14 @@ var init_notify = __esm({
2662
2694
  });
2663
2695
 
2664
2696
  // src/daemon/ask-store.ts
2665
- 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";
2666
2698
  import { basename as basename4 } from "path";
2667
- function maybeNotifyOnAskCreated(cwd, sessionId, meta) {
2699
+ function maybeNotifyOnAskCreated(cwd, sessionId, meta, suppress = false) {
2668
2700
  if (process.env.NODE_ENV === "test" || process.env.SISYPHUS_DISABLE_NOTIFY === "1") return;
2669
2701
  const isActionable = meta.kind !== void 0 && ACTIONABLE_KINDS.has(meta.kind);
2670
2702
  const isHeartbeat = meta.askedBy === HEARTBEAT_ASKED_BY;
2671
2703
  if (!isActionable && !isHeartbeat) return;
2704
+ if (suppress) return;
2672
2705
  try {
2673
2706
  const config = loadConfig(cwd);
2674
2707
  if (config.notifications?.enabled === false) return;
@@ -2694,8 +2727,7 @@ function createAsk(cwd, sessionId, params) {
2694
2727
  ...params.title !== void 0 ? { title: params.title } : {},
2695
2728
  ...params.subtitle !== void 0 ? { subtitle: params.subtitle } : {},
2696
2729
  ...params.kind !== void 0 ? { kind: params.kind } : {},
2697
- ...params.orphanTarget !== void 0 ? { orphanTarget: params.orphanTarget } : {},
2698
- ...params.modeTransition !== void 0 ? { modeTransition: params.modeTransition } : {}
2730
+ ...params.orphanTarget !== void 0 ? { orphanTarget: params.orphanTarget } : {}
2699
2731
  };
2700
2732
  atomicWrite(askMetaPath(cwd, sessionId, params.askId), JSON.stringify(meta, null, 2));
2701
2733
  emitHistoryEvent(sessionId, "ask-issued", {
@@ -2704,7 +2736,7 @@ function createAsk(cwd, sessionId, params) {
2704
2736
  blocking: params.blocking,
2705
2737
  askedAt
2706
2738
  });
2707
- maybeNotifyOnAskCreated(cwd, sessionId, meta);
2739
+ maybeNotifyOnAskCreated(cwd, sessionId, meta, params.suppressTerminalNotification === true);
2708
2740
  return meta;
2709
2741
  }
2710
2742
  function writeDecisions(cwd, sessionId, askId, deck) {
@@ -2839,6 +2871,26 @@ async function maybeAutoResolveAsk(cwd, sessionId, askId, deck) {
2839
2871
  } catch {
2840
2872
  }
2841
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
+ }
2842
2894
  function listOpenAsksFor(cwd, sessionId, askedBy) {
2843
2895
  const out = [];
2844
2896
  for (const askId of listAsks(cwd, sessionId)) {
@@ -3261,7 +3313,7 @@ var init_orphan_sweep = __esm({
3261
3313
  });
3262
3314
 
3263
3315
  // src/daemon/agent.ts
3264
- 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";
3265
3317
  import { execSync as execSync5 } from "child_process";
3266
3318
  import { randomUUID as randomUUID3 } from "crypto";
3267
3319
  import { resolve as resolve5, relative as relative2, dirname as dirname3, join as join12 } from "path";
@@ -3628,7 +3680,7 @@ function gcBgTasks(cwd, sessionId, agentId) {
3628
3680
  console.warn(`[bg-tasks] ${agentId} exited with ${leftover.length} untracked background task(s): ${leftover.join(", ")}`);
3629
3681
  emitHistoryEvent(sessionId, "bg-tasks-leftover", { agentId, leftover });
3630
3682
  }
3631
- unlinkSync(file);
3683
+ unlinkSync2(file);
3632
3684
  } catch (err) {
3633
3685
  console.warn(`[bg-tasks] ${agentId} cleanup failed:`, err instanceof Error ? err.message : err);
3634
3686
  }
@@ -5108,13 +5160,12 @@ function buildPersonality(stats) {
5108
5160
  function shouldGenerateCommentary(event) {
5109
5161
  switch (event) {
5110
5162
  case "session-start":
5163
+ case "mode-transition":
5111
5164
  case "session-complete":
5112
5165
  case "level-up":
5113
5166
  case "achievement":
5114
5167
  case "late-night":
5115
5168
  return true;
5116
- case "cycle-boundary":
5117
- return Math.random() < 0.5;
5118
5169
  case "idle-wake":
5119
5170
  return Math.random() < 0.5;
5120
5171
  case "agent-crash":
@@ -5306,18 +5357,17 @@ var init_companion_commentary = __esm({
5306
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." },
5307
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." },
5308
5359
  { event: "late-night", mood: "existential", context: "3:14am, 2 sessions active", output: "Something about 3am makes every function look like a confession." },
5309
- { 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." },
5310
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."' },
5311
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." },
5312
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." },
5313
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." },
5314
- { 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." },
5315
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." },
5316
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." },
5317
5368
  { event: "agent-crash", mood: "zen", context: "agent-001 crashed during linting", output: "Lost one to the linter. Not the worst way to go." },
5318
5369
  { event: "idle-wake", mood: "grinding", context: "Idle for 2 hours", output: "Two hours away and nothing changed. Exactly as expected, exactly as disappointing." },
5319
5370
  { event: "session-start", mood: "happy", context: "add dark mode", output: "Dark mode. Finally a task that matches the terminal aesthetic." },
5320
- { 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." },
5321
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." },
5322
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." },
5323
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." },
@@ -6143,7 +6193,7 @@ var init_companion_render = __esm({
6143
6193
  });
6144
6194
 
6145
6195
  // src/daemon/companion-popup.ts
6146
- 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";
6147
6197
  import { tmpdir } from "os";
6148
6198
  import { join as join15, resolve as resolve6 } from "path";
6149
6199
  function wrapText(text, width) {
@@ -6236,7 +6286,7 @@ fi
6236
6286
  `;
6237
6287
  writeFileSync12(POPUP_SCRIPT, script, { mode: 493 });
6238
6288
  try {
6239
- unlinkSync2(POPUP_RESULT_PREFIX);
6289
+ unlinkSync3(POPUP_RESULT_PREFIX);
6240
6290
  } catch {
6241
6291
  }
6242
6292
  const clientsRaw = execSafe('tmux list-clients -F "#{client_name} #{client_width}"');
@@ -6246,14 +6296,14 @@ fi
6246
6296
  const client = line.slice(0, lastSpace);
6247
6297
  const clientWidth = parseInt(line.slice(lastSpace + 1), 10);
6248
6298
  if (!clientWidth) continue;
6249
- const x = Math.max(0, clientWidth - POPUP_WIDTH);
6299
+ const x = Math.max(0, clientWidth - POPUP_WIDTH - 1);
6250
6300
  const args = [
6251
6301
  `-c ${shellQuote(client)}`,
6252
6302
  "-E -b rounded",
6253
6303
  `-T ${shellQuote(initialTitle)}`,
6254
6304
  `-S "fg=${moodColor}"`,
6255
6305
  `-s "fg=${moodColor}"`,
6256
- `-x ${x} -y 0`,
6306
+ `-x ${x} -y 2`,
6257
6307
  `-w ${POPUP_WIDTH} -h ${maxContentHeight}`,
6258
6308
  shellQuote(POPUP_SCRIPT)
6259
6309
  ].join(" ");
@@ -6266,7 +6316,7 @@ fi
6266
6316
  return null;
6267
6317
  } finally {
6268
6318
  try {
6269
- unlinkSync2(POPUP_RESULT_PREFIX);
6319
+ unlinkSync3(POPUP_RESULT_PREFIX);
6270
6320
  } catch {
6271
6321
  }
6272
6322
  }
@@ -6737,152 +6787,15 @@ var init_pane_monitor = __esm({
6737
6787
  }
6738
6788
  });
6739
6789
 
6740
- // src/daemon/mode-notify.ts
6741
- import { existsSync as existsSync17 } from "fs";
6742
- import { ulid as ulid2 } from "ulid";
6743
- function capitalize(s) {
6744
- return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
6745
- }
6746
- function formatDuration(ms) {
6747
- const sec = Math.round(ms / 1e3);
6748
- if (sec < 60) return `${sec}s`;
6749
- const min = Math.round(sec / 60);
6750
- if (min < 60) return `${min}m`;
6751
- const h = Math.floor(min / 60);
6752
- const remM = min % 60;
6753
- return remM ? `${h}h ${remM}m` : `${h}h`;
6754
- }
6755
- function findOpenModeTransitionAsk(cwd, sessionId) {
6756
- for (const askId of listAsks(cwd, sessionId)) {
6757
- const meta = readMeta(cwd, sessionId, askId);
6758
- if (!meta) continue;
6759
- if (meta.askedBy !== ORCHESTRATOR_ASKED_BY) continue;
6760
- if (meta.modeTransition !== true) continue;
6761
- if (meta.status === "answered") continue;
6762
- if (meta.orphaned === true) continue;
6763
- if (existsSync17(askOutputPath(cwd, sessionId, askId))) continue;
6764
- return askId;
6765
- }
6766
- return null;
6767
- }
6768
- function buildNextChain(prevChain, prevMode, nextMode, prevModeStats) {
6769
- const stats = prevModeStats ? { cycles: prevModeStats.cycles, activeMs: prevModeStats.activeMs } : {};
6770
- if (prevChain && prevChain.length > 0) {
6771
- const updated = prevChain.map(
6772
- (e, i) => i === prevChain.length - 1 && prevModeStats ? { ...e, ...stats } : e
6773
- );
6774
- updated.push({ mode: nextMode });
6775
- return updated;
6776
- }
6777
- if (prevMode !== void 0) {
6778
- return [{ mode: prevMode, ...stats }, { mode: nextMode }];
6779
- }
6780
- return [{ mode: "unknown" }, { mode: nextMode }];
6781
- }
6782
- function renderBody(chain, cwd) {
6783
- const current = chain[chain.length - 1];
6784
- const description = discoverOrchestratorModes(cwd).find((m) => m.name === current.mode)?.description?.trim();
6785
- const lines = [];
6786
- if (description) {
6787
- lines.push(`**${capitalize(current.mode)}** \u2014 ${description}`);
6788
- } else {
6789
- lines.push(`Now in **${capitalize(current.mode)}** mode.`);
6790
- }
6791
- for (let i = 0; i < chain.length - 1; i++) {
6792
- const e = chain[i];
6793
- if (e.cycles === void 0) continue;
6794
- const label = e.cycles === 1 ? "cycle" : "cycles";
6795
- lines.push(
6796
- `${capitalize(e.mode)}: ${e.cycles} ${label} \xB7 ${formatDuration(e.activeMs ?? 0)} active`
6797
- );
6798
- }
6799
- return lines.join("\n\n");
6800
- }
6801
- async function emitModeTransitionNotify(cwd, sessionId, prevMode, nextMode, prevModeStats) {
6802
- let sessionName;
6803
- try {
6804
- sessionName = getSession(cwd, sessionId).name;
6805
- } catch {
6806
- }
6807
- const existingAskId = findOpenModeTransitionAsk(cwd, sessionId);
6808
- const existingDeck = existingAskId ? readDecisions(cwd, sessionId, existingAskId) : null;
6809
- const chain = buildNextChain(
6810
- existingDeck?.source?.modeChain,
6811
- prevMode,
6812
- nextMode,
6813
- prevModeStats
6814
- );
6815
- const subtitle = chain.map((e) => e.mode).join(" \u2192 ");
6816
- const title = "Mode change";
6817
- const deckTitle = `Mode: ${subtitle}`;
6818
- const body = renderBody(chain, cwd);
6819
- const interaction = {
6820
- id: "mode-transition",
6821
- title,
6822
- subtitle,
6823
- body,
6824
- kind: "notify",
6825
- options: [{ id: "ack", label: "Acknowledged" }]
6826
- };
6827
- const deckSource = {
6828
- ...sessionName !== void 0 ? { sessionName } : {},
6829
- askedBy: ORCHESTRATOR_ASKED_BY,
6830
- modeChain: chain
6831
- };
6832
- const deck = {
6833
- title: deckTitle,
6834
- source: deckSource,
6835
- interactions: [interaction]
6836
- };
6837
- try {
6838
- if (existingAskId) {
6839
- writeDecisions(cwd, sessionId, existingAskId, deck);
6840
- await updateMeta(cwd, sessionId, existingAskId, {
6841
- title,
6842
- subtitle,
6843
- askedAt: (/* @__PURE__ */ new Date()).toISOString()
6844
- });
6845
- return;
6846
- }
6847
- const askId = ulid2();
6848
- createAsk(cwd, sessionId, {
6849
- askId,
6850
- askedBy: ORCHESTRATOR_ASKED_BY,
6851
- blocking: false,
6852
- cwd,
6853
- title,
6854
- subtitle,
6855
- kind: "notify",
6856
- modeTransition: true
6857
- });
6858
- writeDecisions(cwd, sessionId, askId, deck);
6859
- } catch (err) {
6860
- console.warn(
6861
- `[sisyphus] mode-notify: failed to emit mode transition ask for ${sessionId}:`,
6862
- err instanceof Error ? err.message : err
6863
- );
6864
- }
6865
- }
6866
- var init_mode_notify = __esm({
6867
- "src/daemon/mode-notify.ts"() {
6868
- "use strict";
6869
- init_ask_store();
6870
- init_state();
6871
- init_orchestrator_modes();
6872
- init_paths();
6873
- init_types();
6874
- }
6875
- });
6876
-
6877
6790
  // src/daemon/orchestrator.ts
6878
- 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";
6879
6792
  import { execSync as execSync6 } from "child_process";
6880
6793
  import { randomUUID as randomUUID6 } from "crypto";
6881
6794
  import { resolve as resolve7, join as join16, relative as relative3 } from "path";
6882
6795
  function detectRepos(cwd) {
6883
6796
  const config = loadConfig(cwd);
6884
6797
  const repos = [];
6885
- if (existsSync18(join16(cwd, ".git"))) {
6798
+ if (existsSync17(join16(cwd, ".git"))) {
6886
6799
  try {
6887
6800
  repos.push(getRepoInfo(cwd, "."));
6888
6801
  } catch {
@@ -6894,7 +6807,7 @@ function detectRepos(cwd) {
6894
6807
  if (!entry.isDirectory()) continue;
6895
6808
  if (entry.name.startsWith(".")) continue;
6896
6809
  const childPath = join16(cwd, entry.name);
6897
- if (existsSync18(join16(childPath, ".git"))) {
6810
+ if (existsSync17(join16(childPath, ".git"))) {
6898
6811
  try {
6899
6812
  repos.push(getRepoInfo(childPath, entry.name));
6900
6813
  } catch {
@@ -6932,13 +6845,13 @@ function resolveOrchestratorSettings(cwd, sessionId) {
6932
6845
  const bundled = resolve7(import.meta.dirname, "../templates/orchestrator-settings.json");
6933
6846
  const projectSettings = projectOrchestratorSettingsPath(cwd);
6934
6847
  const userSettings = userOrchestratorSettingsPath();
6935
- const hasProject = existsSync18(projectSettings);
6936
- const hasUser = existsSync18(userSettings);
6848
+ const hasProject = existsSync17(projectSettings);
6849
+ const hasUser = existsSync17(userSettings);
6937
6850
  const digestVerbs = digestSpinnerVerbs(cwd, sessionId);
6938
6851
  if (!hasProject && !hasUser && !digestVerbs) return bundled;
6939
6852
  let merged = {};
6940
6853
  for (const path of [bundled, hasUser ? userSettings : null, hasProject ? projectSettings : null]) {
6941
- if (!path || !existsSync18(path)) continue;
6854
+ if (!path || !existsSync17(path)) continue;
6942
6855
  try {
6943
6856
  const parsed = JSON.parse(readFileSync17(path, "utf-8"));
6944
6857
  merged = { ...merged, ...parsed };
@@ -6955,11 +6868,11 @@ function resolveOrchestratorSettings(cwd, sessionId) {
6955
6868
  }
6956
6869
  function loadOrchestratorPrompt(cwd, sessionId, mode) {
6957
6870
  const projectPath = projectOrchestratorPromptPath(cwd);
6958
- if (existsSync18(projectPath)) {
6871
+ if (existsSync17(projectPath)) {
6959
6872
  return readFileSync17(projectPath, "utf-8");
6960
6873
  }
6961
6874
  const userPath = userOrchestratorPromptPath();
6962
- if (existsSync18(userPath)) {
6875
+ if (existsSync17(userPath)) {
6963
6876
  return readFileSync17(userPath, "utf-8");
6964
6877
  }
6965
6878
  const basePath = resolve7(import.meta.dirname, "../templates/orchestrator-base.md");
@@ -6987,7 +6900,7 @@ function buildCompletionContent(session) {
6987
6900
  lines.push("");
6988
6901
  }
6989
6902
  const logsDirPath = logsDir(session.cwd, session.id);
6990
- if (existsSync18(logsDirPath)) {
6903
+ if (existsSync17(logsDirPath)) {
6991
6904
  const logFiles = readdirSync10(logsDirPath).filter((f) => f.startsWith("cycle-") && f.endsWith(".md")).sort();
6992
6905
  if (logFiles.length > 0) {
6993
6906
  lines.push("### Cycle Logs\n");
@@ -7039,7 +6952,7 @@ function buildCompletionContent(session) {
7039
6952
  }
7040
6953
  }
7041
6954
  const reportsDirPath = reportsDir(session.cwd, session.id);
7042
- if (existsSync18(reportsDirPath)) {
6955
+ if (existsSync17(reportsDirPath)) {
7043
6956
  const reportFiles = readdirSync10(reportsDirPath).filter((f) => f.endsWith(".md"));
7044
6957
  if (reportFiles.length > 0) {
7045
6958
  lines.push("### Detailed Reports\n");
@@ -7065,7 +6978,7 @@ ${session.context}
7065
6978
  }
7066
6979
  } else {
7067
6980
  let ctxFiles = [];
7068
- if (existsSync18(ctxDir)) {
6981
+ if (existsSync17(ctxDir)) {
7069
6982
  ctxFiles = readdirSync10(ctxDir).filter((f) => f !== "CLAUDE.md");
7070
6983
  }
7071
6984
  if (ctxFiles.length > 0) {
@@ -7107,10 +7020,10 @@ ${agentLines}
7107
7020
  }
7108
7021
  }
7109
7022
  const strategyFile = strategyPath(session.cwd, session.id);
7110
- const strategyRef = existsSync18(strategyFile) ? `@${relative3(session.cwd, strategyFile)}` : "(empty)";
7111
- 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)";
7112
7025
  const digestFile = digestPath(session.cwd, session.id);
7113
- const digestRef = existsSync18(digestFile) ? `@${relative3(session.cwd, digestFile)}` : "(not yet created)";
7026
+ const digestRef = existsSync17(digestFile) ? `@${relative3(session.cwd, digestFile)}` : "(not yet created)";
7114
7027
  const repos = detectRepos(session.cwd);
7115
7028
  let repositoriesSection = "\n\n## Repositories\n";
7116
7029
  if (repos.length === 0) {
@@ -7137,7 +7050,7 @@ ${agentLines}
7137
7050
  }
7138
7051
  }
7139
7052
  const goalFile = goalPath(session.cwd, session.id);
7140
- const goalContent = existsSync18(goalFile) ? readFileSync17(goalFile, "utf-8").trim() : session.task;
7053
+ const goalContent = existsSync17(goalFile) ? readFileSync17(goalFile, "utf-8").trim() : session.task;
7141
7054
  const modeContent = modeContentBuilders[mode]?.(session) ?? "";
7142
7055
  return `## Goal
7143
7056
 
@@ -7330,14 +7243,12 @@ async function handleOrchestratorYield(sessionId, cwd, nextPrompt, mode) {
7330
7243
  prevModeStats = { cycles, activeMs };
7331
7244
  }
7332
7245
  await completeOrchestratorCycle(cwd, sessionId, nextPrompt, mode, cycleActiveMs);
7333
- if (mode && mode !== prevMode) {
7334
- await emitModeTransitionNotify(cwd, sessionId, prevMode, mode, prevModeStats);
7335
- }
7336
7246
  const freshSession = getSession(cwd, sessionId);
7337
7247
  const runningAgents = freshSession.agents.filter((a) => a.status === "running");
7338
7248
  if (runningAgents.length === 0) {
7339
7249
  console.log(`[sisyphus] Orchestrator yielded with no running agents for session ${sessionId}`);
7340
7250
  }
7251
+ return prevModeStats ? { prevMode, nextMode: mode, prevModeStats } : void 0;
7341
7252
  }
7342
7253
  async function handleOrchestratorComplete(sessionId, cwd, report) {
7343
7254
  const session = getSession(cwd, sessionId);
@@ -7373,7 +7284,6 @@ var init_orchestrator = __esm({
7373
7284
  init_tmux();
7374
7285
  init_pane_registry();
7375
7286
  init_pane_monitor();
7376
- init_mode_notify();
7377
7287
  init_plugins();
7378
7288
  sessionWindowMap = /* @__PURE__ */ new Map();
7379
7289
  sessionOrchestratorPane = /* @__PURE__ */ new Map();
@@ -7541,6 +7451,36 @@ var init_status_dots = __esm({
7541
7451
  }
7542
7452
  });
7543
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
+
7544
7484
  // src/shared/manifest.ts
7545
7485
  import os from "os";
7546
7486
  function mapEffortFallback(level) {
@@ -7581,7 +7521,7 @@ var init_manifest = __esm({
7581
7521
  // src/shared/session-export.ts
7582
7522
  import { execFile as execFile3 } from "child_process";
7583
7523
  import { promisify as promisify2 } from "util";
7584
- 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";
7585
7525
  import { homedir as homedir6 } from "os";
7586
7526
  import { join as join17 } from "path";
7587
7527
  function sanitizeName(name) {
@@ -7593,7 +7533,7 @@ function buildOutputPath(label, dir) {
7593
7533
  const base = `sisyphus-${label}-${date}`;
7594
7534
  let candidate = join17(dir, `${base}.zip`);
7595
7535
  let counter = 1;
7596
- while (existsSync19(candidate)) {
7536
+ while (existsSync18(candidate)) {
7597
7537
  counter++;
7598
7538
  candidate = join17(dir, `${base}-${counter}.zip`);
7599
7539
  }
@@ -7656,14 +7596,14 @@ async function exportSessionToZip(sessionId, cwd, options) {
7656
7596
  const reveal = options?.reveal ?? true;
7657
7597
  const sessDir = sessionDir(cwd, sessionId);
7658
7598
  const histDir = historySessionDir(sessionId);
7659
- const sessExists = existsSync19(sessDir);
7660
- const histExists = existsSync19(histDir);
7599
+ const sessExists = existsSync18(sessDir);
7600
+ const histExists = existsSync18(histDir);
7661
7601
  if (!sessExists && !histExists) {
7662
7602
  throw new Error(`No data found for session ${sessionId}`);
7663
7603
  }
7664
7604
  let label = sessionId.slice(0, 8);
7665
7605
  const stPath = statePath(cwd, sessionId);
7666
- if (existsSync19(stPath)) {
7606
+ if (existsSync18(stPath)) {
7667
7607
  try {
7668
7608
  const state = JSON.parse(readFileSync19(stPath, "utf-8"));
7669
7609
  if (state.name) {
@@ -7855,7 +7795,7 @@ var init_format = __esm({
7855
7795
  });
7856
7796
 
7857
7797
  // src/cli/deploy/creds.ts
7858
- 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";
7859
7799
  import { createInterface } from "readline";
7860
7800
  function isValidProvider(value) {
7861
7801
  return PROVIDERS.includes(value);
@@ -7896,10 +7836,10 @@ var init_pricing = __esm({
7896
7836
  });
7897
7837
 
7898
7838
  // src/cli/deploy/runtime.ts
7899
- 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";
7900
7840
  function readRuntimeState(provider) {
7901
7841
  const path = deployRuntimePath(provider);
7902
- if (!existsSync21(path)) return null;
7842
+ if (!existsSync20(path)) return null;
7903
7843
  try {
7904
7844
  return JSON.parse(readFileSync22(path, "utf-8"));
7905
7845
  } catch {
@@ -7923,15 +7863,15 @@ var init_tailnet = __esm({
7923
7863
  });
7924
7864
 
7925
7865
  // src/cli/deploy/templates.ts
7926
- import { existsSync as existsSync22 } from "fs";
7866
+ import { existsSync as existsSync21 } from "fs";
7927
7867
  import { dirname as dirname6, resolve as resolve9 } from "path";
7928
7868
  import { fileURLToPath } from "url";
7929
7869
  function deployRoot() {
7930
7870
  const here = dirname6(fileURLToPath(import.meta.url));
7931
7871
  const bundled = resolve9(here, "..", "deploy");
7932
- if (existsSync22(bundled)) return bundled;
7872
+ if (existsSync21(bundled)) return bundled;
7933
7873
  const sourceRoot = resolve9(here, "..", "..", "..", "deploy");
7934
- if (existsSync22(sourceRoot)) return sourceRoot;
7874
+ if (existsSync21(sourceRoot)) return sourceRoot;
7935
7875
  throw new Error(
7936
7876
  `Could not locate deploy/ templates. Looked at:
7937
7877
  ${bundled}
@@ -7960,7 +7900,7 @@ var init_tailscale = __esm({
7960
7900
 
7961
7901
  // src/cli/deploy/runner.ts
7962
7902
  import { spawn as spawn2, spawnSync } from "child_process";
7963
- 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";
7964
7904
  function readOutputs(provider) {
7965
7905
  const result = spawnSync("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
7966
7906
  cwd: providerModuleDir(provider),
@@ -7986,7 +7926,7 @@ function readOutputs(provider) {
7986
7926
  }
7987
7927
  }
7988
7928
  function isProvisioned(provider) {
7989
- if (!existsSync23(deployStatePath(provider))) return false;
7929
+ if (!existsSync22(deployStatePath(provider))) return false;
7990
7930
  return readOutputs(provider) !== null;
7991
7931
  }
7992
7932
  function effectiveSshTarget(provider) {
@@ -8105,7 +8045,7 @@ var init_grove = __esm({
8105
8045
 
8106
8046
  // src/cli/cloud/repo.ts
8107
8047
  import { spawnSync as spawnSync3 } from "child_process";
8108
- import { existsSync as existsSync24 } from "fs";
8048
+ import { existsSync as existsSync23 } from "fs";
8109
8049
  import { basename as basename6, join as join18 } from "path";
8110
8050
  function captureGit(args, cwd) {
8111
8051
  const result = spawnSync3("git", args, {
@@ -8146,10 +8086,10 @@ function buildRsyncArgs(localDir, remoteTarget) {
8146
8086
  ];
8147
8087
  }
8148
8088
  function detectPackageManager(toplevel) {
8149
- if (existsSync24(join18(toplevel, "pnpm-lock.yaml"))) return "pnpm";
8150
- if (existsSync24(join18(toplevel, "bun.lockb"))) return "bun";
8151
- if (existsSync24(join18(toplevel, "yarn.lock"))) return "yarn";
8152
- 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";
8153
8093
  return null;
8154
8094
  }
8155
8095
  function packageManagerInstallCmd(pm) {
@@ -8350,7 +8290,7 @@ __export(cloud_handoff_exports, {
8350
8290
  triggerForceHandoff: () => triggerForceHandoff
8351
8291
  });
8352
8292
  import { spawn as spawn5 } from "child_process";
8353
- import { existsSync as existsSync25 } from "fs";
8293
+ import { existsSync as existsSync24 } from "fs";
8354
8294
  import { join as join19 } from "path";
8355
8295
  function runRsync2(args) {
8356
8296
  return new Promise((resolve13) => {
@@ -8390,7 +8330,7 @@ async function syncSessionState(cwd, sessionId, repo, target, provider) {
8390
8330
  const candidates = ["config.json", "orchestrator.md", "orchestrator-settings.json"];
8391
8331
  for (const name of candidates) {
8392
8332
  const localPath = join19(localProject, name);
8393
- if (!existsSync25(localPath)) continue;
8333
+ if (!existsSync24(localPath)) continue;
8394
8334
  const remotePath = `${boxRepoPath(repo)}/.sisyphus/${name}`;
8395
8335
  const args = [
8396
8336
  "-avz",
@@ -8557,14 +8497,14 @@ var init_cloud_handoff = __esm({
8557
8497
 
8558
8498
  // src/daemon/session-manager.ts
8559
8499
  import { v4 as uuidv4 } from "uuid";
8560
- 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";
8561
8501
  function truncate(s, max) {
8562
8502
  return s.length <= max ? s : s.slice(0, max) + "...";
8563
8503
  }
8564
8504
  function readGoal(cwd, sessionId, fallback) {
8565
8505
  try {
8566
8506
  const p = goalPath(cwd, sessionId);
8567
- if (existsSync26(p)) return readFileSync24(p, "utf-8").trim();
8507
+ if (existsSync25(p)) return readFileSync24(p, "utf-8").trim();
8568
8508
  } catch {
8569
8509
  }
8570
8510
  return fallback;
@@ -8618,7 +8558,7 @@ function fireHaikuNaming(sessionId, cwd, fallbackTmuxName, task) {
8618
8558
  console.error(`[sisyphus] Name generation failed for session ${sessionId}:`, err);
8619
8559
  });
8620
8560
  }
8621
- function fireCommentary(event, companion, context, flash = false, repo, sessionId) {
8561
+ function fireCommentary(event, companion, context, flash = false, repo, sessionId, popupTitle) {
8622
8562
  const memoryCtx = buildMemoryContext(repo);
8623
8563
  generateCommentary(event, companion, context, memoryCtx).then((text) => {
8624
8564
  if (text) {
@@ -8626,7 +8566,7 @@ function fireCommentary(event, companion, context, flash = false, repo, sessionI
8626
8566
  const c = loadCompanion();
8627
8567
  recordCommentary(c, text, event);
8628
8568
  if (flash) {
8629
- const feedback = showCommentaryPopup(text);
8569
+ const feedback = showCommentaryPopupQueue([{ text, title: popupTitle }]);
8630
8570
  if (feedback) {
8631
8571
  recordFeedback(c, text, feedback.rating, event, feedback.comment);
8632
8572
  if (sessionId) {
@@ -8803,7 +8743,7 @@ It is the other session's responsibility. You do not need to monitor it.
8803
8743
  function pruneOldSessions(cwd) {
8804
8744
  try {
8805
8745
  const dir = sessionsDir(cwd);
8806
- if (!existsSync26(dir)) return;
8746
+ if (!existsSync25(dir)) return;
8807
8747
  const entries = readdirSync11(dir, { withFileTypes: true });
8808
8748
  const candidates = [];
8809
8749
  for (const entry of entries) {
@@ -8941,7 +8881,7 @@ function getSessionStatus(cwd, sessionId) {
8941
8881
  }
8942
8882
  function countSessions(cwd) {
8943
8883
  const dir = sessionsDir(cwd);
8944
- if (!existsSync26(dir)) return 0;
8884
+ if (!existsSync25(dir)) return 0;
8945
8885
  let n = 0;
8946
8886
  for (const entry of readdirSync11(dir, { withFileTypes: true })) {
8947
8887
  if (entry.isDirectory()) n++;
@@ -8950,7 +8890,7 @@ function countSessions(cwd) {
8950
8890
  }
8951
8891
  function listSessions(cwd) {
8952
8892
  const dir = sessionsDir(cwd);
8953
- if (!existsSync26(dir)) return [];
8893
+ if (!existsSync25(dir)) return [];
8954
8894
  const entries = readdirSync11(dir, { withFileTypes: true });
8955
8895
  const sessions = [];
8956
8896
  for (const entry of entries) {
@@ -9014,23 +8954,6 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
9014
8954
  const companion = loadCompanion();
9015
8955
  companion.spinnerVerbIndex = (companion.spinnerVerbIndex + 1) % SPINNER_VERBS.length;
9016
8956
  saveCompanion(companion);
9017
- const goal = readGoal(cwd, sessionId, session.task);
9018
- const modeLabel = lastCycle?.mode ? ` (${lastCycle.mode})` : "";
9019
- const agentMap = new Map(session.agents.map((a) => [a.id, a]));
9020
- const spawnedThisCycle = (lastCycle?.agentsSpawned ?? []).map((id) => agentMap.get(id)).filter(Boolean).map((a) => `${a.name} (${a.agentType.replace(/^sisyphus:/, "")}, ${a.status})`).join(", ");
9021
- let cycleCtx = `Cycle ${cycleNumber}${modeLabel} complete. Goal: ${truncate(goal, 80)}`;
9022
- if (spawnedThisCycle) cycleCtx += `
9023
- Agents: ${truncate(spawnedThisCycle, 200)}`;
9024
- try {
9025
- const logPath2 = cycleLogPath(cwd, sessionId, cycleNumber);
9026
- if (existsSync26(logPath2)) {
9027
- const log = readFileSync24(logPath2, "utf-8").trim();
9028
- if (log) cycleCtx += `
9029
- Cycle log: ${truncate(log, 200)}`;
9030
- }
9031
- } catch {
9032
- }
9033
- fireCommentary("cycle-boundary", companion, cycleCtx, true, session.cwd, sessionId);
9034
8957
  } catch {
9035
8958
  }
9036
8959
  setImmediate(async () => {
@@ -9203,8 +9126,23 @@ async function handleYield(sessionId, cwd, nextPrompt, mode) {
9203
9126
  await updateSessionStatus(cwd, sessionId, "active");
9204
9127
  }
9205
9128
  respawningSessions.add(sessionId);
9206
- await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
9129
+ const transition = await handleOrchestratorYield(sessionId, cwd, nextPrompt, mode);
9207
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
+ }
9208
9146
  try {
9209
9147
  recomputeDots();
9210
9148
  } catch {
@@ -9259,6 +9197,7 @@ async function handleComplete(sessionId, cwd, report) {
9259
9197
  console.warn("[sisyphus] Sentiment generation failed:", err instanceof Error ? err.message : err);
9260
9198
  });
9261
9199
  const config = loadConfig(cwd);
9200
+ const uploadCfg = config.upload;
9262
9201
  if (isUploadConfigured(config.upload)) {
9263
9202
  runSessionUploadAndPersist({
9264
9203
  sessionId,
@@ -9270,11 +9209,10 @@ async function handleComplete(sessionId, cwd, report) {
9270
9209
  }).catch(() => {
9271
9210
  console.warn("[sisyphus] upload pipeline crashed; check uploadError on session state");
9272
9211
  });
9273
- } else if (config.upload) {
9274
- const partialUpload = config.upload;
9212
+ } else if (uploadCfg) {
9275
9213
  const missing = [];
9276
- if (!partialUpload.url) missing.push("upload.url");
9277
- if (!partialUpload.token) missing.push("upload.token");
9214
+ if (!uploadCfg.url) missing.push("upload.url");
9215
+ if (!uploadCfg.token) missing.push("upload.token");
9278
9216
  const error = `upload skipped: missing ${missing.join(", ")}`;
9279
9217
  console.warn(`[sisyphus] ${error}`);
9280
9218
  updateSession(cwd, sessionId, { uploadStatus: "failed", uploadError: error }).catch(() => console.warn("[sisyphus] failed to persist upload skip status"));
@@ -9548,6 +9486,7 @@ var init_session_manager = __esm({
9548
9486
  init_companion_render();
9549
9487
  init_companion_commentary();
9550
9488
  init_companion_popup();
9489
+ init_mode_transition();
9551
9490
  init_history();
9552
9491
  init_uploader();
9553
9492
  init_upload();
@@ -9669,7 +9608,7 @@ var init_transcript_digest = __esm({
9669
9608
  import {
9670
9609
  closeSync as closeSync2,
9671
9610
  constants,
9672
- existsSync as existsSync27,
9611
+ existsSync as existsSync26,
9673
9612
  fstatSync as fstatSync2,
9674
9613
  lstatSync,
9675
9614
  openSync as openSync2,
@@ -9691,7 +9630,7 @@ async function generateVisualForQuestion(opts) {
9691
9630
  if (!question) return { ok: false, error: `qid ${opts.qid} not found in decisions` };
9692
9631
  const mdPath = askVisualMarkdownPath(opts.cwd, opts.sessionId, opts.askId, opts.qid);
9693
9632
  const ansiPath = askVisualAnsiPath(opts.cwd, opts.sessionId, opts.askId, opts.qid);
9694
- if (!opts.force && existsSync27(mdPath) && existsSync27(ansiPath)) {
9633
+ if (!opts.force && existsSync26(mdPath) && existsSync26(ansiPath)) {
9695
9634
  return { ok: true, markdownPath: mdPath, ansiPath, turns: 0 };
9696
9635
  }
9697
9636
  if (opts.force) {
@@ -9765,7 +9704,7 @@ function buildUserPrompt(q, askedBy, ctx) {
9765
9704
  }
9766
9705
  function readSystemPrompt() {
9767
9706
  if (cachedSystemPrompt !== void 0) return cachedSystemPrompt;
9768
- const found = SYSTEM_PROMPT_CANDIDATES.find((p) => existsSync27(p));
9707
+ const found = SYSTEM_PROMPT_CANDIDATES.find((p) => existsSync26(p));
9769
9708
  if (found === void 0) {
9770
9709
  throw new Error(
9771
9710
  `termrender-haiku-system.md not found in any candidate location: ${SYSTEM_PROMPT_CANDIDATES.join(", ")}`
@@ -9912,7 +9851,7 @@ var init_ask_visual = __esm({
9912
9851
 
9913
9852
  // src/daemon/server.ts
9914
9853
  import { createServer } from "net";
9915
- 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";
9916
9855
  import { join as join21, basename as basename7, dirname as dirname8 } from "path";
9917
9856
  import { scanInbox } from "@crouton-kit/humanloop";
9918
9857
  function setCompositor(c) {
@@ -9932,7 +9871,7 @@ function persistSessionRegistry() {
9932
9871
  }
9933
9872
  function loadSessionRegistry() {
9934
9873
  const p = registryPath();
9935
- if (!existsSync28(p)) return {};
9874
+ if (!existsSync27(p)) return {};
9936
9875
  try {
9937
9876
  return JSON.parse(readFileSync26(p, "utf-8"));
9938
9877
  } catch (err) {
@@ -9988,7 +9927,7 @@ function collectAllSessionIds() {
9988
9927
  scannedCwds.add(cwd);
9989
9928
  try {
9990
9929
  const dir = sessionsDir(cwd);
9991
- if (!existsSync28(dir)) continue;
9930
+ if (!existsSync27(dir)) continue;
9992
9931
  for (const entry of readdirSync12(dir, { withFileTypes: true })) {
9993
9932
  if (entry.isDirectory() && !idToCwd.has(entry.name)) {
9994
9933
  idToCwd.set(entry.name, cwd);
@@ -10220,7 +10159,7 @@ async function handleRequest(req) {
10220
10159
  let tracking = sessionTrackingMap.get(req.sessionId);
10221
10160
  if (!tracking) {
10222
10161
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10223
- if (existsSync28(stateFile)) {
10162
+ if (existsSync27(stateFile)) {
10224
10163
  tracking = { cwd: req.cwd, messageCounter: 0 };
10225
10164
  sessionTrackingMap.set(req.sessionId, tracking);
10226
10165
  persistSessionRegistry();
@@ -10242,7 +10181,7 @@ async function handleRequest(req) {
10242
10181
  }
10243
10182
  case "clear-orphan": {
10244
10183
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10245
- if (!existsSync28(stateFile)) {
10184
+ if (!existsSync27(stateFile)) {
10246
10185
  return {
10247
10186
  ok: false,
10248
10187
  error: errNotFound("unknown_session", {
@@ -10300,7 +10239,7 @@ async function handleRequest(req) {
10300
10239
  let tracking = sessionTrackingMap.get(req.sessionId);
10301
10240
  if (!tracking) {
10302
10241
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10303
- if (existsSync28(stateFile)) {
10242
+ if (existsSync27(stateFile)) {
10304
10243
  registerSessionCwd(req.sessionId, req.cwd);
10305
10244
  tracking = sessionTrackingMap.get(req.sessionId);
10306
10245
  } else {
@@ -10314,7 +10253,7 @@ async function handleRequest(req) {
10314
10253
  let tracking = sessionTrackingMap.get(req.sessionId);
10315
10254
  if (!tracking) {
10316
10255
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10317
- if (existsSync28(stateFile)) {
10256
+ if (existsSync27(stateFile)) {
10318
10257
  registerSessionCwd(req.sessionId, req.cwd);
10319
10258
  tracking = sessionTrackingMap.get(req.sessionId);
10320
10259
  } else {
@@ -10331,7 +10270,7 @@ async function handleRequest(req) {
10331
10270
  let tracking = sessionTrackingMap.get(req.sessionId);
10332
10271
  if (!tracking) {
10333
10272
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10334
- if (existsSync28(stateFile)) {
10273
+ if (existsSync27(stateFile)) {
10335
10274
  tracking = { cwd: req.cwd, messageCounter: 0 };
10336
10275
  sessionTrackingMap.set(req.sessionId, tracking);
10337
10276
  persistSessionRegistry();
@@ -10401,7 +10340,7 @@ async function handleRequest(req) {
10401
10340
  }
10402
10341
  case "set-upload-status": {
10403
10342
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
10404
- if (!existsSync28(stateFile)) {
10343
+ if (!existsSync27(stateFile)) {
10405
10344
  return unknownSessionError(req.sessionId);
10406
10345
  }
10407
10346
  try {
@@ -10600,6 +10539,7 @@ async function handleRequest(req) {
10600
10539
  const reviewItems = [];
10601
10540
  for (const [sessionId, tracking] of sessionTrackingMap) {
10602
10541
  if (!tracking.cwd) continue;
10542
+ if (tracking.cwd !== req.cwd) continue;
10603
10543
  askDirs.push(askDir(tracking.cwd, sessionId));
10604
10544
  reviewItems.push(...listReviewInboxItems(tracking.cwd, sessionId));
10605
10545
  }
@@ -10623,6 +10563,26 @@ async function handleRequest(req) {
10623
10563
  });
10624
10564
  return { ok: true, data: { items: itemsWithName } };
10625
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
+ }
10626
10586
  case "cloud-handoff": {
10627
10587
  const tracking = sessionTrackingMap.get(req.sessionId);
10628
10588
  if (!tracking) return unknownSessionError(req.sessionId);
@@ -10770,8 +10730,8 @@ async function handleRequest(req) {
10770
10730
  function startServer() {
10771
10731
  return new Promise((resolve13, reject) => {
10772
10732
  const sock = socketPath();
10773
- if (existsSync28(sock)) {
10774
- unlinkSync4(sock);
10733
+ if (existsSync27(sock)) {
10734
+ unlinkSync5(sock);
10775
10735
  }
10776
10736
  server = createServer((conn) => {
10777
10737
  let buffer = "";
@@ -10825,15 +10785,15 @@ function stopServer() {
10825
10785
  }
10826
10786
  server.close(() => {
10827
10787
  const sock = socketPath();
10828
- if (existsSync28(sock)) {
10829
- unlinkSync4(sock);
10788
+ if (existsSync27(sock)) {
10789
+ unlinkSync5(sock);
10830
10790
  }
10831
10791
  server = null;
10832
10792
  resolve13();
10833
10793
  });
10834
10794
  });
10835
10795
  }
10836
- var server, compositor, sessionTrackingMap;
10796
+ var server, compositor, sessionTrackingMap, pendingFocus, FOCUS_TTL_MS;
10837
10797
  var init_server = __esm({
10838
10798
  "src/daemon/server.ts"() {
10839
10799
  "use strict";
@@ -10857,6 +10817,8 @@ var init_server = __esm({
10857
10817
  server = null;
10858
10818
  compositor = null;
10859
10819
  sessionTrackingMap = /* @__PURE__ */ new Map();
10820
+ pendingFocus = null;
10821
+ FOCUS_TTL_MS = 3e4;
10860
10822
  }
10861
10823
  });
10862
10824
 
@@ -10865,7 +10827,7 @@ init_paths();
10865
10827
  init_config();
10866
10828
  init_server();
10867
10829
  init_orphan_sweep();
10868
- 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";
10869
10831
  import { execSync as execSync8 } from "child_process";
10870
10832
  import { setTimeout as sleep } from "timers/promises";
10871
10833
  import { Command } from "commander";
@@ -10875,8 +10837,8 @@ init_ask_store();
10875
10837
  init_state();
10876
10838
  init_server();
10877
10839
  init_paths();
10878
- import { ulid as ulid3 } from "ulid";
10879
- import { existsSync as existsSync29 } from "fs";
10840
+ import { ulid as ulid2 } from "ulid";
10841
+ import { existsSync as existsSync28 } from "fs";
10880
10842
  var HEARTBEAT_ASKED_BY2 = "system:heartbeat";
10881
10843
  var HEARTBEAT_THRESHOLD_MS = 60 * 60 * 1e3;
10882
10844
  var HEARTBEAT_SCAN_INTERVAL_MS = 15 * 60 * 1e3;
@@ -10919,7 +10881,7 @@ async function emitHeartbeatAsk(cwd, sessionId, original) {
10919
10881
  },
10920
10882
  interactions: [interaction]
10921
10883
  };
10922
- const askId = ulid3();
10884
+ const askId = ulid2();
10923
10885
  createAsk(cwd, sessionId, {
10924
10886
  askId,
10925
10887
  askedBy: HEARTBEAT_ASKED_BY2,
@@ -10935,43 +10897,14 @@ async function emitHeartbeatAsk(cwd, sessionId, original) {
10935
10897
  heartbeatAskId: askId
10936
10898
  });
10937
10899
  }
10938
- function isModeGateStale(deck, currentMode) {
10939
- const source = deck.source;
10940
- const chain = source?.modeChain;
10941
- if (!chain || chain.length === 0) return false;
10942
- if (!currentMode) return false;
10943
- return !chain.some((e) => e.mode === currentMode);
10944
- }
10945
10900
  async function scanSessionForStaleAsks(cwd, sessionId) {
10946
10901
  const now = Date.now();
10947
- let currentMode;
10948
- try {
10949
- const session = getSession(cwd, sessionId);
10950
- currentMode = session.orchestratorCycles[session.orchestratorCycles.length - 1]?.mode;
10951
- } catch {
10952
- }
10953
10902
  for (const askId of listAsks(cwd, sessionId)) {
10954
10903
  try {
10955
10904
  const meta = readMeta(cwd, sessionId, askId);
10956
10905
  if (!meta) continue;
10957
10906
  if (meta.status === "answered") continue;
10958
10907
  if (meta.orphaned) continue;
10959
- if (meta.modeTransition === true) {
10960
- const deck = readDecisions(cwd, sessionId, askId);
10961
- if (deck && isModeGateStale(deck, currentMode)) {
10962
- writeOutput(cwd, sessionId, askId, [{
10963
- id: "mode-transition",
10964
- selectedOptionId: "ack",
10965
- freetext: "auto-resolved: session advanced past mode-transition"
10966
- }]);
10967
- await updateMeta(cwd, sessionId, askId, {
10968
- status: "answered",
10969
- completedAt: (/* @__PURE__ */ new Date()).toISOString()
10970
- });
10971
- console.log(`[sisyphus] mode-gate auto-resolved ask ${askId} for session ${sessionId} (currentMode: ${currentMode})`);
10972
- }
10973
- continue;
10974
- }
10975
10908
  if (meta.heartbeatNotifiedAt) continue;
10976
10909
  if (meta.askedBy === HEARTBEAT_ASKED_BY2) continue;
10977
10910
  const askedAtMs = new Date(meta.askedAt).getTime();
@@ -10989,7 +10922,7 @@ async function scanSessionForStaleAsks(cwd, sessionId) {
10989
10922
  async function scanAllSessionsForStaleAsks() {
10990
10923
  const reg = loadSessionRegistry();
10991
10924
  for (const [sessionId, cwd] of Object.entries(reg)) {
10992
- if (!existsSync29(statePath(cwd, sessionId))) continue;
10925
+ if (!existsSync28(statePath(cwd, sessionId))) continue;
10993
10926
  try {
10994
10927
  await scanSessionForStaleAsks(cwd, sessionId);
10995
10928
  } catch (err) {
@@ -11052,14 +10985,14 @@ var DEFAULT_STATUS_BAR_CONFIG = {
11052
10985
  init_tmux();
11053
10986
  init_status_dots();
11054
10987
  init_companion();
11055
- import { readFileSync as readFileSync27, existsSync as existsSync30 } from "fs";
10988
+ import { readFileSync as readFileSync27, existsSync as existsSync29 } from "fs";
11056
10989
  import { homedir as homedir8 } from "os";
11057
10990
  import { join as join22 } from "path";
11058
10991
  var STATUS_BAR_BG = "#1d1e21";
11059
10992
  var SESSION_ORDER_PATH = join22(homedir8(), ".config", "tmux", "session-order");
11060
10993
  function getSessionOrder() {
11061
10994
  try {
11062
- if (!existsSync30(SESSION_ORDER_PATH)) return [];
10995
+ if (!existsSync29(SESSION_ORDER_PATH)) return [];
11063
10996
  return readFileSync27(SESSION_ORDER_PATH, "utf-8").split("\n").filter(Boolean);
11064
10997
  } catch {
11065
10998
  return [];
@@ -11612,6 +11545,7 @@ function writeEmptyManifest() {
11612
11545
 
11613
11546
  // src/daemon/index.ts
11614
11547
  init_agent();
11548
+ init_ask_store();
11615
11549
  init_orphan_asks();
11616
11550
  init_process();
11617
11551
  init_orchestrator();
@@ -11623,7 +11557,7 @@ init_state();
11623
11557
  init_paths();
11624
11558
  init_version();
11625
11559
  import { execSync as execSync7 } from "child_process";
11626
- 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";
11627
11561
  import { resolve as resolve11 } from "path";
11628
11562
  import { get } from "https";
11629
11563
  function isNewer(latest, current) {
@@ -11700,7 +11634,7 @@ function markUpdating(version) {
11700
11634
  }
11701
11635
  function clearUpdating() {
11702
11636
  try {
11703
- unlinkSync5(daemonUpdatingPath());
11637
+ unlinkSync6(daemonUpdatingPath());
11704
11638
  } catch {
11705
11639
  }
11706
11640
  }
@@ -11750,7 +11684,7 @@ function stopPeriodicUpdateCheck() {
11750
11684
  }
11751
11685
 
11752
11686
  // src/daemon/plugin-install.ts
11753
- 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";
11754
11688
  import { join as join23, resolve as resolve12 } from "path";
11755
11689
  import { homedir as homedir9 } from "os";
11756
11690
  var PLUGIN_NAME = "sisyphus-tmux";
@@ -11764,7 +11698,7 @@ function copyDir(src, dest) {
11764
11698
  copyDir(srcPath, destPath);
11765
11699
  } else {
11766
11700
  const srcMtime = statSync4(srcPath).mtimeMs;
11767
- const destMtime = existsSync31(destPath) ? statSync4(destPath).mtimeMs : 0;
11701
+ const destMtime = existsSync30(destPath) ? statSync4(destPath).mtimeMs : 0;
11768
11702
  if (srcMtime > destMtime) {
11769
11703
  copyFileSync6(srcPath, destPath);
11770
11704
  }
@@ -11774,7 +11708,7 @@ function copyDir(src, dest) {
11774
11708
  function pluginNeedsUpdate(sourceDir) {
11775
11709
  const srcHooks = join23(sourceDir, "hooks", "hooks.json");
11776
11710
  const destHooks = join23(INSTALL_DIR, "hooks", "hooks.json");
11777
- if (!existsSync31(destHooks)) return true;
11711
+ if (!existsSync30(destHooks)) return true;
11778
11712
  try {
11779
11713
  return readFileSync28(srcHooks, "utf-8") !== readFileSync28(destHooks, "utf-8");
11780
11714
  } catch {
@@ -11783,7 +11717,7 @@ function pluginNeedsUpdate(sourceDir) {
11783
11717
  }
11784
11718
  function installPlugin() {
11785
11719
  const sourceDir = resolve12(import.meta.dirname, "../templates/sisyphus-tmux-plugin");
11786
- if (!existsSync31(sourceDir)) {
11720
+ if (!existsSync30(sourceDir)) {
11787
11721
  console.error(`[plugin-install] Source dir not found: ${sourceDir}`);
11788
11722
  return;
11789
11723
  }
@@ -11791,7 +11725,7 @@ function installPlugin() {
11791
11725
  try {
11792
11726
  copyDir(sourceDir, INSTALL_DIR);
11793
11727
  const hookScript = join23(INSTALL_DIR, "hooks", "tmux-state.sh");
11794
- if (existsSync31(hookScript)) {
11728
+ if (existsSync30(hookScript)) {
11795
11729
  try {
11796
11730
  chmodSync3(hookScript, 493);
11797
11731
  } catch {
@@ -11847,7 +11781,7 @@ init_upload();
11847
11781
  init_version();
11848
11782
  init_state();
11849
11783
  init_uploader();
11850
- 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";
11851
11785
  var backfillStarted = false;
11852
11786
  async function backfillUploads() {
11853
11787
  if (backfillStarted) return;
@@ -11880,7 +11814,7 @@ async function backfillUploads() {
11880
11814
  let failed = 0;
11881
11815
  let unexportable = 0;
11882
11816
  for (const { sessionId, cwd } of candidates) {
11883
- if (!existsSync32(statePath(cwd, sessionId))) {
11817
+ if (!existsSync31(statePath(cwd, sessionId))) {
11884
11818
  unexportable++;
11885
11819
  continue;
11886
11820
  }
@@ -11958,7 +11892,7 @@ function isLaunchdManaged() {
11958
11892
  }
11959
11893
  function releasePidLock() {
11960
11894
  try {
11961
- unlinkSync6(daemonPidPath());
11895
+ unlinkSync7(daemonPidPath());
11962
11896
  } catch {
11963
11897
  }
11964
11898
  }
@@ -11997,7 +11931,7 @@ function stopDaemon() {
11997
11931
  }
11998
11932
  async function recoverOneSession(sessionId, cwd, tmuxIdSet, tmuxNameToId, panesByWindow) {
11999
11933
  const stateFile = statePath(cwd, sessionId);
12000
- if (!existsSync33(stateFile)) return false;
11934
+ if (!existsSync32(stateFile)) return false;
12001
11935
  let session;
12002
11936
  try {
12003
11937
  session = JSON.parse(readFileSync30(stateFile, "utf-8"));
@@ -12008,6 +11942,10 @@ async function recoverOneSession(sessionId, cwd, tmuxIdSet, tmuxNameToId, panesB
12008
11942
  if (session.status !== "active" && session.status !== "paused") return false;
12009
11943
  registerSessionCwd(sessionId, cwd);
12010
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
+ }
12011
11949
  if (!session.tmuxSessionName) return true;
12012
11950
  let sessionAlive = false;
12013
11951
  let currentTmuxId = session.tmuxSessionId;