sisyphi 1.2.2 → 1.2.12

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.
Files changed (85) hide show
  1. package/README.md +20 -20
  2. package/dist/cli.js +12450 -11255
  3. package/dist/cli.js.map +1 -1
  4. package/dist/daemon.js +1113 -565
  5. package/dist/daemon.js.map +1 -1
  6. package/dist/templates/agent-plugin/agents/CLAUDE.md +2 -2
  7. package/dist/templates/agent-plugin/agents/operator.md +3 -4
  8. package/dist/templates/agent-plugin/agents/plan.md +1 -1
  9. package/dist/templates/agent-plugin/agents/problem.md +20 -20
  10. package/dist/templates/agent-plugin/agents/research-lead.md +1 -1
  11. package/dist/templates/agent-plugin/agents/spec/engineer.md +9 -7
  12. package/dist/templates/agent-plugin/agents/spec/requirements-writer.md +1 -1
  13. package/dist/templates/agent-plugin/agents/spec.md +31 -25
  14. package/dist/templates/agent-plugin/hooks/CLAUDE.md +0 -1
  15. package/dist/templates/agent-plugin/hooks/ask-background-guard.sh +11 -11
  16. package/dist/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
  17. package/dist/templates/agent-plugin/hooks/operator-user-prompt.sh +2 -2
  18. package/dist/templates/agent-plugin/hooks/plan-validate.sh +3 -3
  19. package/dist/templates/agent-plugin/hooks/require-submit.sh +1 -1
  20. package/dist/templates/agent-plugin/skills/operator/SKILL.md +1 -1
  21. package/dist/templates/agent-suffix.md +4 -18
  22. package/dist/templates/companion-plugin/hooks/user-prompt-context.sh +1 -1
  23. package/dist/templates/dashboard-claude.md +15 -13
  24. package/dist/templates/orchestrator-base.md +44 -78
  25. package/dist/templates/orchestrator-completion.md +9 -11
  26. package/dist/templates/orchestrator-discovery.md +8 -8
  27. package/dist/templates/orchestrator-impl.md +6 -7
  28. package/dist/templates/orchestrator-planning.md +2 -2
  29. package/dist/templates/orchestrator-plugin/commands/sisyphus/scratch.md +1 -1
  30. package/dist/templates/orchestrator-plugin/commands/sisyphus/strategize.md +2 -2
  31. package/dist/templates/orchestrator-validation.md +1 -3
  32. package/dist/templates/termrender-haiku-system.md +5 -3
  33. package/dist/tui.js +1817 -1400
  34. package/dist/tui.js.map +1 -1
  35. package/native/build-notify.sh +2 -2
  36. package/package.json +3 -3
  37. package/templates/agent-plugin/agents/CLAUDE.md +2 -2
  38. package/templates/agent-plugin/agents/operator.md +3 -4
  39. package/templates/agent-plugin/agents/plan.md +1 -1
  40. package/templates/agent-plugin/agents/problem.md +20 -20
  41. package/templates/agent-plugin/agents/research-lead.md +1 -1
  42. package/templates/agent-plugin/agents/spec/engineer.md +9 -7
  43. package/templates/agent-plugin/agents/spec/requirements-writer.md +1 -1
  44. package/templates/agent-plugin/agents/spec.md +31 -25
  45. package/templates/agent-plugin/hooks/CLAUDE.md +0 -1
  46. package/templates/agent-plugin/hooks/ask-background-guard.sh +11 -11
  47. package/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
  48. package/templates/agent-plugin/hooks/operator-user-prompt.sh +2 -2
  49. package/templates/agent-plugin/hooks/plan-validate.sh +3 -3
  50. package/templates/agent-plugin/hooks/require-submit.sh +1 -1
  51. package/templates/agent-plugin/skills/operator/SKILL.md +1 -1
  52. package/templates/agent-suffix.md +4 -18
  53. package/templates/companion-plugin/hooks/user-prompt-context.sh +1 -1
  54. package/templates/dashboard-claude.md +15 -13
  55. package/templates/orchestrator-base.md +44 -78
  56. package/templates/orchestrator-completion.md +9 -11
  57. package/templates/orchestrator-discovery.md +8 -8
  58. package/templates/orchestrator-impl.md +6 -7
  59. package/templates/orchestrator-planning.md +2 -2
  60. package/templates/orchestrator-plugin/commands/sisyphus/scratch.md +1 -1
  61. package/templates/orchestrator-plugin/commands/sisyphus/strategize.md +2 -2
  62. package/templates/orchestrator-validation.md +1 -3
  63. package/templates/termrender-haiku-system.md +5 -3
  64. package/dist/templates/agent-plugin/skills/humanloop/SKILL.md +0 -148
  65. package/dist/templates/agent-plugin/skills/operator-memory/SKILL.md +0 -64
  66. package/dist/templates/agent-plugin/skills/perspective-fanout/SKILL.md +0 -115
  67. package/dist/templates/agent-plugin/skills/problem-document/SKILL.md +0 -105
  68. package/dist/templates/agent-plugin/skills/problem-plateau-breakers/SKILL.md +0 -83
  69. package/dist/templates/orchestrator-plugin/skills/humanloop/SKILL.md +0 -150
  70. package/dist/templates/orchestrator-plugin/skills/orchestration/CLAUDE.md +0 -1
  71. package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +0 -29
  72. package/dist/templates/orchestrator-plugin/skills/orchestration/strategy.md +0 -160
  73. package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +0 -266
  74. package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +0 -428
  75. package/templates/agent-plugin/skills/humanloop/SKILL.md +0 -148
  76. package/templates/agent-plugin/skills/operator-memory/SKILL.md +0 -64
  77. package/templates/agent-plugin/skills/perspective-fanout/SKILL.md +0 -115
  78. package/templates/agent-plugin/skills/problem-document/SKILL.md +0 -105
  79. package/templates/agent-plugin/skills/problem-plateau-breakers/SKILL.md +0 -83
  80. package/templates/orchestrator-plugin/skills/humanloop/SKILL.md +0 -150
  81. package/templates/orchestrator-plugin/skills/orchestration/CLAUDE.md +0 -1
  82. package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +0 -29
  83. package/templates/orchestrator-plugin/skills/orchestration/strategy.md +0 -160
  84. package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +0 -266
  85. package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +0 -428
package/dist/daemon.js CHANGED
@@ -18,6 +18,9 @@ __export(paths_exports, {
18
18
  askMetaPath: () => askMetaPath,
19
19
  askOutputPath: () => askOutputPath,
20
20
  askProgressPath: () => askProgressPath,
21
+ askReviewDraftPath: () => askReviewDraftPath,
22
+ askReviewPath: () => askReviewPath,
23
+ askReviewSubmitFlagPath: () => askReviewSubmitFlagPath,
21
24
  askVisualAnsiPath: () => askVisualAnsiPath,
22
25
  askVisualMarkdownPath: () => askVisualMarkdownPath,
23
26
  askVisualsDir: () => askVisualsDir,
@@ -195,14 +198,23 @@ function askMetaPath(cwd, sessionId, askId) {
195
198
  return join(askEntryDir(cwd, sessionId, askId), "meta.json");
196
199
  }
197
200
  function askDecisionsPath(cwd, sessionId, askId) {
198
- return join(askEntryDir(cwd, sessionId, askId), "decisions.json");
201
+ return join(askEntryDir(cwd, sessionId, askId), "deck.json");
199
202
  }
200
203
  function askOutputPath(cwd, sessionId, askId) {
201
- return join(askEntryDir(cwd, sessionId, askId), "output.json");
204
+ return join(askEntryDir(cwd, sessionId, askId), "response.json");
202
205
  }
203
206
  function askProgressPath(cwd, sessionId, askId) {
204
207
  return join(askEntryDir(cwd, sessionId, askId), "progress.json");
205
208
  }
209
+ function askReviewPath(cwd, sessionId, askId) {
210
+ return join(askEntryDir(cwd, sessionId, askId), "review.json");
211
+ }
212
+ function askReviewDraftPath(cwd, sessionId, askId) {
213
+ return join(askEntryDir(cwd, sessionId, askId), "draft.json");
214
+ }
215
+ function askReviewSubmitFlagPath(cwd, sessionId, askId) {
216
+ return join(askEntryDir(cwd, sessionId, askId), "submitted");
217
+ }
206
218
  function askVisualsDir(cwd, sessionId, askId) {
207
219
  return join(askEntryDir(cwd, sessionId, askId), "visuals");
208
220
  }
@@ -328,6 +340,7 @@ var init_config = __esm({
328
340
  DEFAULT_CONFIG = {
329
341
  model: "claude-opus-4-7[1m]",
330
342
  pollIntervalMs: 5e3,
343
+ statusBarRenderTicks: 4,
331
344
  orchestratorEffort: "xhigh",
332
345
  agentEffort: "medium",
333
346
  notifications: {
@@ -337,11 +350,32 @@ var init_config = __esm({
337
350
  companionPopup: true,
338
351
  requiredPlugins: [
339
352
  { name: "devcore", marketplace: "crouton-kit", owner: "crouton-labs" }
353
+ ],
354
+ requiredCrtrPlugins: [
355
+ {
356
+ marketplace: "sisyphus",
357
+ plugin: "sisyphus",
358
+ gitUrl: "https://github.com/crouton-labs/sisyphus"
359
+ }
340
360
  ]
341
361
  };
342
362
  }
343
363
  });
344
364
 
365
+ // src/shared/protocol.ts
366
+ var errUsage, errNotFound, errAmbiguous, errConflict, errTransient, errPermanent;
367
+ var init_protocol = __esm({
368
+ "src/shared/protocol.ts"() {
369
+ "use strict";
370
+ errUsage = (code, f) => ({ kind: "usage", code, ...f });
371
+ errNotFound = (code, f) => ({ kind: "not_found", code, ...f });
372
+ errAmbiguous = (code, f) => ({ kind: "ambiguous", code, ...f });
373
+ errConflict = (code, f) => ({ kind: "conflict", code, ...f });
374
+ errTransient = (code, f) => ({ kind: "transient", code, ...f });
375
+ errPermanent = (code, f) => ({ kind: "permanent", code, ...f });
376
+ }
377
+ });
378
+
345
379
  // src/shared/shell.ts
346
380
  function shellQuote(s) {
347
381
  return `'${s.replace(/'/g, "'\\''")}'`;
@@ -439,7 +473,7 @@ var init_types = __esm({
439
473
  });
440
474
 
441
475
  // src/daemon/state.ts
442
- import { copyFileSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, readdirSync, rmSync, statSync, writeFileSync as writeFileSync3 } from "fs";
476
+ import { copyFileSync, cpSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, readdirSync, rmSync, statSync, watch as fsWatch, writeFileSync as writeFileSync3 } from "fs";
443
477
  import { join as join4 } from "path";
444
478
  function withSessionLock(sessionId, fn) {
445
479
  return withLock(sessionId, fn);
@@ -481,9 +515,7 @@ function createSession(id, task, cwd, context, name, effort) {
481
515
  atomicWrite(statePath(cwd, id), JSON.stringify(session, null, 2));
482
516
  return session;
483
517
  }
484
- function getSession(cwd, sessionId) {
485
- const content = readFileSync3(statePath(cwd, sessionId), "utf-8");
486
- const session = JSON.parse(content);
518
+ function normalizeSession(session) {
487
519
  if (session.activeMs == null) session.activeMs = 0;
488
520
  if (session.userBlockedMs == null) session.userBlockedMs = 0;
489
521
  for (const agent of session.agents) {
@@ -496,10 +528,106 @@ function getSession(cwd, sessionId) {
496
528
  if (cycle.activeMs == null) cycle.activeMs = 0;
497
529
  if (cycle.userBlockedMs == null) cycle.userBlockedMs = 0;
498
530
  }
531
+ }
532
+ function loadFromDisk(p) {
533
+ const session = JSON.parse(readFileSync3(p, "utf-8"));
534
+ normalizeSession(session);
499
535
  return session;
500
536
  }
537
+ function reloadIntoCache(p) {
538
+ if (!sessionCache.has(p)) return;
539
+ try {
540
+ sessionCache.set(p, loadFromDisk(p));
541
+ } catch (err) {
542
+ console.warn(`[sisyphus] state cache reload failed for ${p}:`, err instanceof Error ? err.message : err);
543
+ }
544
+ }
545
+ function scheduleReload(p) {
546
+ const existing = debounceTimers.get(p);
547
+ if (existing) clearTimeout(existing);
548
+ const t2 = setTimeout(() => {
549
+ debounceTimers.delete(p);
550
+ reloadIntoCache(p);
551
+ }, DEBOUNCE_MS);
552
+ t2.unref?.();
553
+ debounceTimers.set(p, t2);
554
+ }
555
+ function installWatcher(p) {
556
+ if (watchers.has(p)) return;
557
+ let w;
558
+ try {
559
+ w = fsWatch(p, { persistent: false });
560
+ } catch (err) {
561
+ console.warn(`[sisyphus] fs.watch install failed for ${p}:`, err instanceof Error ? err.message : err);
562
+ return;
563
+ }
564
+ w.on("change", (eventType) => {
565
+ if (eventType === "rename") {
566
+ try {
567
+ w.close();
568
+ } catch {
569
+ }
570
+ watchers.delete(p);
571
+ scheduleReload(p);
572
+ const reinstall = setTimeout(() => {
573
+ if (sessionCache.has(p)) installWatcher(p);
574
+ }, DEBOUNCE_MS + 10);
575
+ reinstall.unref?.();
576
+ } else {
577
+ scheduleReload(p);
578
+ }
579
+ });
580
+ w.on("error", (err) => {
581
+ console.warn(`[sisyphus] fs.watch error for ${p}:`, err instanceof Error ? err.message : err);
582
+ try {
583
+ w.close();
584
+ } catch {
585
+ }
586
+ watchers.delete(p);
587
+ });
588
+ watchers.set(p, w);
589
+ }
590
+ function installStateWatcher(cwd, sessionId) {
591
+ const p = statePath(cwd, sessionId);
592
+ try {
593
+ sessionCache.set(p, loadFromDisk(p));
594
+ } catch (err) {
595
+ console.warn(`[sisyphus] state cache prime failed for ${p}:`, err instanceof Error ? err.message : err);
596
+ return;
597
+ }
598
+ setImmediate(() => {
599
+ if (sessionCache.has(p)) installWatcher(p);
600
+ });
601
+ }
602
+ function uninstallStateWatcher(cwd, sessionId) {
603
+ const p = statePath(cwd, sessionId);
604
+ const w = watchers.get(p);
605
+ if (w) {
606
+ try {
607
+ w.close();
608
+ } catch {
609
+ }
610
+ watchers.delete(p);
611
+ }
612
+ const t2 = debounceTimers.get(p);
613
+ if (t2) {
614
+ clearTimeout(t2);
615
+ debounceTimers.delete(p);
616
+ }
617
+ sessionCache.delete(p);
618
+ }
619
+ function getSession(cwd, sessionId) {
620
+ const p = statePath(cwd, sessionId);
621
+ const hit = sessionCache.get(p);
622
+ if (hit) return structuredClone(hit);
623
+ return loadFromDisk(p);
624
+ }
501
625
  function saveSession(session) {
502
- atomicWrite(statePath(session.cwd, session.id), JSON.stringify(session, null, 2));
626
+ const p = statePath(session.cwd, session.id);
627
+ atomicWrite(p, JSON.stringify(session, null, 2));
628
+ if (sessionCache.has(p)) {
629
+ sessionCache.set(p, structuredClone(session));
630
+ }
503
631
  }
504
632
  function isSessionDangerous(cwd, sessionId) {
505
633
  try {
@@ -874,7 +1002,7 @@ async function createCloneState(sourceCwd, sourceId, cloneId, goal, context, con
874
1002
  return clone;
875
1003
  });
876
1004
  }
877
- var ROADMAP_SEED, CONTEXT_CLAUDE_MD;
1005
+ var ROADMAP_SEED, CONTEXT_CLAUDE_MD, sessionCache, watchers, debounceTimers, DEBOUNCE_MS;
878
1006
  var init_state = __esm({
879
1007
  "src/daemon/state.ts"() {
880
1008
  "use strict";
@@ -891,6 +1019,10 @@ description: >
891
1019
 
892
1020
  Agents save exploration findings, architectural notes, and reference material here for use across cycles.
893
1021
  `;
1022
+ sessionCache = /* @__PURE__ */ new Map();
1023
+ watchers = /* @__PURE__ */ new Map();
1024
+ debounceTimers = /* @__PURE__ */ new Map();
1025
+ DEBOUNCE_MS = 25;
894
1026
  }
895
1027
  });
896
1028
 
@@ -926,6 +1058,38 @@ var init_spawn_helpers = __esm({
926
1058
  }
927
1059
  });
928
1060
 
1061
+ // src/daemon/help-inject.ts
1062
+ import { execSync } from "child_process";
1063
+ function injectHelp(text) {
1064
+ if (!text.includes("{{HELP:")) return text;
1065
+ const cliBin = resolveCliBin();
1066
+ const renderHelp = (cmd) => {
1067
+ try {
1068
+ return execSync(`${process.execPath} ${cliBin} ${cmd} -h`, {
1069
+ encoding: "utf-8",
1070
+ stdio: ["ignore", "pipe", "pipe"],
1071
+ timeout: 1e4
1072
+ }).trim();
1073
+ } catch (e) {
1074
+ const out = e.stdout;
1075
+ if (typeof out === "string" && out.length > 0) return out.trim();
1076
+ return `(help unavailable: sis ${cmd} -h)`;
1077
+ }
1078
+ };
1079
+ return text.replace(/\{\{HELP:([^}]+)\}\}/g, (_m, cmd) => {
1080
+ const c = cmd.trim();
1081
+ return `<cli-guide bash="sis ${c} -h">
1082
+ ${renderHelp(c)}
1083
+ </cli-guide>`;
1084
+ });
1085
+ }
1086
+ var init_help_inject = __esm({
1087
+ "src/daemon/help-inject.ts"() {
1088
+ "use strict";
1089
+ init_spawn_helpers();
1090
+ }
1091
+ });
1092
+
929
1093
  // src/shared/env.ts
930
1094
  import { resolve as resolve2 } from "path";
931
1095
  function augmentedPath() {
@@ -963,10 +1127,12 @@ function augmentedPath() {
963
1127
  return prepend.length > 0 ? `${prepend.join(":")}:${basePath}` : basePath;
964
1128
  }
965
1129
  function execEnv() {
966
- return {
1130
+ const env = {
967
1131
  ...process.env,
968
1132
  PATH: augmentedPath()
969
1133
  };
1134
+ if (!env["LC_ALL"] && !env["LANG"]) env["LANG"] = "en_US.UTF-8";
1135
+ return env;
970
1136
  }
971
1137
  var init_env = __esm({
972
1138
  "src/shared/env.ts"() {
@@ -975,13 +1141,13 @@ var init_env = __esm({
975
1141
  });
976
1142
 
977
1143
  // src/shared/exec.ts
978
- import { execSync } from "child_process";
1144
+ import { execSync as execSync2 } from "child_process";
979
1145
  function exec(cmd, cwd, timeoutMs = 3e4) {
980
- return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, timeout: timeoutMs }).trim();
1146
+ return execSync2(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, timeout: timeoutMs }).trim();
981
1147
  }
982
1148
  function execSafe(cmd, cwd, timeoutMs) {
983
1149
  try {
984
- return execSync(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, stdio: ["pipe", "pipe", "pipe"], timeout: timeoutMs }).trim();
1150
+ return execSync2(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, stdio: ["pipe", "pipe", "pipe"], timeout: timeoutMs }).trim();
985
1151
  } catch {
986
1152
  return null;
987
1153
  }
@@ -995,6 +1161,31 @@ var init_exec = __esm({
995
1161
  }
996
1162
  });
997
1163
 
1164
+ // src/shared/digest-verbs.ts
1165
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1166
+ function digestSpinnerVerbs(cwd, sessionId) {
1167
+ try {
1168
+ const dp = digestPath(cwd, sessionId);
1169
+ if (!existsSync4(dp)) return null;
1170
+ const raw = JSON.parse(readFileSync4(dp, "utf-8"));
1171
+ if (!raw || typeof raw.recentWork !== "string" || typeof raw.currentActivity !== "string" || typeof raw.whatsNext !== "string" || !Array.isArray(raw.unusualEvents)) {
1172
+ return null;
1173
+ }
1174
+ const base = [raw.recentWork, raw.currentActivity, raw.whatsNext].map((v) => v.trim()).filter((v) => v.length > 0);
1175
+ const unusual = raw.unusualEvents.filter((e) => typeof e === "string").map((e) => e.trim()).filter((e) => e.length > 0).map((e) => `unusual: ${e}`);
1176
+ const verbs = [...base, ...unusual];
1177
+ return verbs.length > 0 ? verbs : null;
1178
+ } catch {
1179
+ return null;
1180
+ }
1181
+ }
1182
+ var init_digest_verbs = __esm({
1183
+ "src/shared/digest-verbs.ts"() {
1184
+ "use strict";
1185
+ init_paths();
1186
+ }
1187
+ });
1188
+
998
1189
  // src/daemon/colors.ts
999
1190
  function normalizeTmuxColor(color) {
1000
1191
  return TMUX_COLOR_MAP[color] ?? color;
@@ -1023,7 +1214,7 @@ var init_colors = __esm({
1023
1214
  });
1024
1215
 
1025
1216
  // src/daemon/frontmatter.ts
1026
- import { readFileSync as readFileSync4, existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
1217
+ import { readFileSync as readFileSync5, existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
1027
1218
  import { homedir as homedir2 } from "os";
1028
1219
  import { join as join5, basename as basename2 } from "path";
1029
1220
  function detectProvider(model) {
@@ -1070,7 +1261,7 @@ function extractAgentBody(content) {
1070
1261
  function findPluginInstallPath(namespace) {
1071
1262
  try {
1072
1263
  const registryPath2 = join5(homedir2(), ".claude", "plugins", "installed_plugins.json");
1073
- const registry = JSON.parse(readFileSync4(registryPath2, "utf-8"));
1264
+ const registry = JSON.parse(readFileSync5(registryPath2, "utf-8"));
1074
1265
  for (const key of Object.keys(registry)) {
1075
1266
  if (key.startsWith(`${namespace}@`)) {
1076
1267
  return registry[key].installPath ?? null;
@@ -1104,7 +1295,7 @@ function resolveAgentTypePath(agentType, pluginDir, cwd) {
1104
1295
  searchPaths.push(join5(pluginDir, "agents", `${name}.md`));
1105
1296
  }
1106
1297
  for (const path of searchPaths) {
1107
- if (existsSync4(path)) return path;
1298
+ if (existsSync5(path)) return path;
1108
1299
  }
1109
1300
  return null;
1110
1301
  }
@@ -1125,7 +1316,7 @@ function discoverAgentTypes(pluginDir, cwd) {
1125
1316
  if (seen.has(qualifiedName)) continue;
1126
1317
  seen.add(qualifiedName);
1127
1318
  try {
1128
- const content = readFileSync4(join5(dir, file), "utf-8");
1319
+ const content = readFileSync5(join5(dir, file), "utf-8");
1129
1320
  const fm = parseAgentFrontmatter(content);
1130
1321
  results.push({ qualifiedName, source, description: fm.description, model: fm.model });
1131
1322
  } catch {
@@ -1140,7 +1331,7 @@ function discoverAgentTypes(pluginDir, cwd) {
1140
1331
  scanDir(join5(pluginDir, "agents"), "sisyphus", "bundled");
1141
1332
  try {
1142
1333
  const registryPath2 = join5(homedir2(), ".claude", "plugins", "installed_plugins.json");
1143
- const registry = JSON.parse(readFileSync4(registryPath2, "utf-8"));
1334
+ const registry = JSON.parse(readFileSync5(registryPath2, "utf-8"));
1144
1335
  const pluginEntries = registry.plugins ?? registry;
1145
1336
  for (const key of Object.keys(pluginEntries)) {
1146
1337
  const atIdx = key.indexOf("@");
@@ -1160,7 +1351,7 @@ function resolveAgentConfig(agentType, pluginDir, cwd) {
1160
1351
  const filePath = resolveAgentTypePath(agentType, pluginDir, cwd);
1161
1352
  if (!filePath) return null;
1162
1353
  try {
1163
- const content = readFileSync4(filePath, "utf-8");
1354
+ const content = readFileSync5(filePath, "utf-8");
1164
1355
  return {
1165
1356
  frontmatter: parseAgentFrontmatter(content),
1166
1357
  body: extractAgentBody(content),
@@ -1178,22 +1369,22 @@ var init_frontmatter = __esm({
1178
1369
  });
1179
1370
 
1180
1371
  // src/daemon/orchestrator-modes.ts
1181
- import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync5 } from "fs";
1372
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync6 } from "fs";
1182
1373
  import { resolve as resolve3, join as join6 } from "path";
1183
1374
  import { homedir as homedir3 } from "os";
1184
1375
  function resolveTemplatesDir() {
1185
1376
  const distLayout = resolve3(import.meta.dirname, "../templates");
1186
- if (existsSync5(distLayout)) return distLayout;
1377
+ if (existsSync6(distLayout)) return distLayout;
1187
1378
  const srcLayout = resolve3(import.meta.dirname, "../../templates");
1188
- if (existsSync5(srcLayout)) return srcLayout;
1379
+ if (existsSync6(srcLayout)) return srcLayout;
1189
1380
  return void 0;
1190
1381
  }
1191
1382
  function modeLayers(cwd) {
1192
1383
  const layers = [];
1193
1384
  const project = projectDir(cwd);
1194
- if (existsSync5(project)) layers.push({ source: "project", dir: project });
1385
+ if (existsSync6(project)) layers.push({ source: "project", dir: project });
1195
1386
  const user = join6(homedir3(), ".sisyphus");
1196
- if (existsSync5(user)) layers.push({ source: "user", dir: user });
1387
+ if (existsSync6(user)) layers.push({ source: "user", dir: user });
1197
1388
  const bundled = resolveTemplatesDir();
1198
1389
  if (bundled) layers.push({ source: "bundled", dir: bundled });
1199
1390
  return layers;
@@ -1215,7 +1406,7 @@ function discoverOrchestratorModes(cwd) {
1215
1406
  const filePath = join6(layer.dir, file);
1216
1407
  let content = "";
1217
1408
  try {
1218
- content = readFileSync5(filePath, "utf-8");
1409
+ content = readFileSync6(filePath, "utf-8");
1219
1410
  } catch {
1220
1411
  continue;
1221
1412
  }
@@ -1270,7 +1461,7 @@ var init_effort_render = __esm({
1270
1461
  });
1271
1462
 
1272
1463
  // src/daemon/lib/render-plugin.ts
1273
- import { readdirSync as readdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2, rmSync as rmSync2, existsSync as existsSync6 } from "fs";
1464
+ import { readdirSync as readdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2, rmSync as rmSync2, existsSync as existsSync7 } from "fs";
1274
1465
  import { join as join7 } from "path";
1275
1466
  function isFiltered(name) {
1276
1467
  for (const ext of FILTERED_EXTS) {
@@ -1279,7 +1470,7 @@ function isFiltered(name) {
1279
1470
  return false;
1280
1471
  }
1281
1472
  function renderPluginDir(srcDir, destDir, tier) {
1282
- if (!existsSync6(srcDir)) {
1473
+ if (!existsSync7(srcDir)) {
1283
1474
  throw new Error(`renderPluginDir: source dir does not exist: ${srcDir}`);
1284
1475
  }
1285
1476
  rmSync2(destDir, { recursive: true, force: true });
@@ -1295,7 +1486,7 @@ function walk(src, dest, tier) {
1295
1486
  walk(srcPath, destPath, tier);
1296
1487
  } else if (entry.isFile()) {
1297
1488
  if (isFiltered(entry.name)) {
1298
- const rendered = renderEffortMarkers(readFileSync6(srcPath, "utf-8"), tier);
1489
+ const rendered = renderEffortMarkers(readFileSync7(srcPath, "utf-8"), tier);
1299
1490
  writeFileSync5(destPath, rendered, "utf-8");
1300
1491
  } else {
1301
1492
  copyFileSync2(srcPath, destPath);
@@ -1313,21 +1504,21 @@ var init_render_plugin = __esm({
1313
1504
  });
1314
1505
 
1315
1506
  // src/daemon/extensions.ts
1316
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, copyFileSync as copyFileSync3, mkdirSync as mkdirSync3, statSync as statSync2, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
1507
+ import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync5, copyFileSync as copyFileSync3, mkdirSync as mkdirSync3, statSync as statSync2, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
1317
1508
  import { resolve as resolve4, join as join8, basename as basename3, relative } from "path";
1318
1509
  function resolveBundledTemplateDir(kind) {
1319
1510
  const built = resolve4(import.meta.dirname, "../templates", kind);
1320
- if (existsSync7(built)) return built;
1511
+ if (existsSync8(built)) return built;
1321
1512
  const src = resolve4(import.meta.dirname, "../../templates", kind);
1322
- if (existsSync7(src)) return src;
1513
+ if (existsSync8(src)) return src;
1323
1514
  return void 0;
1324
1515
  }
1325
1516
  function agentPluginLayers(cwd) {
1326
1517
  const layers = [];
1327
1518
  const project = projectAgentPluginDir(cwd);
1328
- if (existsSync7(project)) layers.push({ source: "project", root: project });
1519
+ if (existsSync8(project)) layers.push({ source: "project", root: project });
1329
1520
  const user = userAgentPluginDir();
1330
- if (existsSync7(user)) layers.push({ source: "user", root: user });
1521
+ if (existsSync8(user)) layers.push({ source: "user", root: user });
1331
1522
  const bundled = resolveBundledTemplateDir("agent-plugin");
1332
1523
  if (bundled) layers.push({ source: "bundled", root: bundled });
1333
1524
  return layers;
@@ -1335,18 +1526,18 @@ function agentPluginLayers(cwd) {
1335
1526
  function orchestratorPluginLayers(cwd) {
1336
1527
  const layers = [];
1337
1528
  const project = projectOrchestratorPluginDir(cwd);
1338
- if (existsSync7(project)) layers.push({ source: "project", root: project });
1529
+ if (existsSync8(project)) layers.push({ source: "project", root: project });
1339
1530
  const user = userOrchestratorPluginDir();
1340
- if (existsSync7(user)) layers.push({ source: "user", root: user });
1531
+ if (existsSync8(user)) layers.push({ source: "user", root: user });
1341
1532
  const bundled = resolveBundledTemplateDir("orchestrator-plugin");
1342
1533
  if (bundled) layers.push({ source: "bundled", root: bundled });
1343
1534
  return layers;
1344
1535
  }
1345
1536
  function readManifest(layerRoot) {
1346
1537
  const path = join8(layerRoot, "hooks", "hooks.json");
1347
- if (!existsSync7(path)) return null;
1538
+ if (!existsSync8(path)) return null;
1348
1539
  try {
1349
- const raw = readFileSync7(path, "utf-8");
1540
+ const raw = readFileSync8(path, "utf-8");
1350
1541
  const parsed = JSON.parse(raw);
1351
1542
  return parsed;
1352
1543
  } catch (err) {
@@ -1354,8 +1545,8 @@ function readManifest(layerRoot) {
1354
1545
  return null;
1355
1546
  }
1356
1547
  }
1357
- function commandScriptName(command2) {
1358
- const match = command2.match(/([\w.-]+\.[\w]+)\s*$/);
1548
+ function commandScriptName(command) {
1549
+ const match = command.match(/([\w.-]+\.[\w]+)\s*$/);
1359
1550
  return match ? match[1] : null;
1360
1551
  }
1361
1552
  function groupApplies(group, ctx) {
@@ -1402,7 +1593,7 @@ function copyLayered(layers, opts) {
1402
1593
  const written = /* @__PURE__ */ new Set();
1403
1594
  for (const layer of layers) {
1404
1595
  const layerSubdir = join8(layer.root, opts.subdir);
1405
- if (!existsSync7(layerSubdir)) continue;
1596
+ if (!existsSync8(layerSubdir)) continue;
1406
1597
  let entries;
1407
1598
  try {
1408
1599
  entries = readdirSync5(layerSubdir);
@@ -1470,7 +1661,7 @@ function indexAvailableSkills(layers) {
1470
1661
  const index = /* @__PURE__ */ new Map();
1471
1662
  for (const layer of layers) {
1472
1663
  const skillsRoot = join8(layer.root, "skills");
1473
- if (!existsSync7(skillsRoot)) continue;
1664
+ if (!existsSync8(skillsRoot)) continue;
1474
1665
  let entries;
1475
1666
  try {
1476
1667
  entries = readdirSync5(skillsRoot);
@@ -1504,7 +1695,7 @@ function isRenderable(name) {
1504
1695
  function collectOverlay(layers) {
1505
1696
  const overlay = /* @__PURE__ */ new Map();
1506
1697
  for (const layer of layers) {
1507
- if (!existsSync7(layer.root)) continue;
1698
+ if (!existsSync8(layer.root)) continue;
1508
1699
  walkRelative(layer.root, "", (relPath, absPath) => {
1509
1700
  if (overlay.has(relPath)) return;
1510
1701
  overlay.set(relPath, { src: absPath, source: layer.source });
@@ -1544,7 +1735,7 @@ function renderLayeredPluginDir(layers, destDir, tier) {
1544
1735
  const destPath = join8(destDir, relPath);
1545
1736
  mkdirSync3(join8(destPath, ".."), { recursive: true });
1546
1737
  if (isRenderable(basename3(relPath))) {
1547
- const rendered = renderEffortMarkers(readFileSync7(entry.src, "utf-8"), tier);
1738
+ const rendered = renderEffortMarkers(readFileSync8(entry.src, "utf-8"), tier);
1548
1739
  writeFileSync6(destPath, rendered, "utf-8");
1549
1740
  } else {
1550
1741
  copyFileSync3(entry.src, destPath);
@@ -1563,7 +1754,7 @@ var init_extensions = __esm({
1563
1754
  });
1564
1755
 
1565
1756
  // src/daemon/tmux.ts
1566
- import { execSync as execSync2 } from "child_process";
1757
+ import { execSync as execSync3 } from "child_process";
1567
1758
  function planSendKeys(state) {
1568
1759
  if (!state.exists || state.dead) return { action: "abort" };
1569
1760
  if (state.inMode) return { action: "cancel-then-send" };
@@ -1585,14 +1776,14 @@ function createPane(windowTarget, cwd, position = "right") {
1585
1776
  execSafe(`tmux select-layout -t ${t(windowTarget)} even-horizontal`);
1586
1777
  return paneId;
1587
1778
  }
1588
- function sendKeys(paneTarget, command2) {
1779
+ function sendKeys(paneTarget, command) {
1589
1780
  const state = getPaneState(paneTarget);
1590
1781
  const { action } = planSendKeys(state);
1591
1782
  if (action === "abort") throw new PaneUnavailableError(paneTarget, state);
1592
1783
  if (action === "cancel-then-send") {
1593
1784
  execSafe(`tmux send-keys -t ${t(paneTarget)} -X cancel`, void 0, TMUX_TIMEOUT_MS);
1594
1785
  }
1595
- exec(`tmux send-keys -t ${t(paneTarget)} ${shellQuote(command2)} Enter`, void 0, TMUX_TIMEOUT_MS);
1786
+ exec(`tmux send-keys -t ${t(paneTarget)} ${shellQuote(command)} Enter`, void 0, TMUX_TIMEOUT_MS);
1596
1787
  }
1597
1788
  function pasteToPane(paneTarget, text, submit) {
1598
1789
  const state = getPaneState(paneTarget);
@@ -1602,7 +1793,7 @@ function pasteToPane(paneTarget, text, submit) {
1602
1793
  execSafe(`tmux send-keys -t ${t(paneTarget)} -X cancel`, void 0, TMUX_TIMEOUT_MS);
1603
1794
  }
1604
1795
  const bufName = `sisyphus-tell-${Math.random().toString(36).slice(2, 10)}`;
1605
- execSync2(`tmux load-buffer -b ${shellQuote(bufName)} -`, {
1796
+ execSync3(`tmux load-buffer -b ${shellQuote(bufName)} -`, {
1606
1797
  input: text,
1607
1798
  env: EXEC_ENV,
1608
1799
  timeout: TMUX_TIMEOUT_MS,
@@ -1703,13 +1894,27 @@ function getFirstWindowId(sessionTarget) {
1703
1894
  return execSafe(`tmux list-windows -t ${t(sessionTarget)} -F "#{window_id}" -f "#{==:#{window_index},0}"`)?.trim() || null;
1704
1895
  }
1705
1896
  function listPanes(windowTarget) {
1706
- const output = execSafe(`tmux list-panes -t ${t(windowTarget)} -F "#{pane_id} #{pane_pid}"`);
1897
+ const output = execSafe(`tmux list-panes -t ${t(windowTarget)} -F "#{pane_id} #{pane_pid}"`, void 0, TMUX_TIMEOUT_MS);
1707
1898
  if (!output) return [];
1708
1899
  return output.split("\n").filter(Boolean).map((line) => {
1709
1900
  const [paneId, panePid] = line.split(" ");
1710
1901
  return { paneId, panePid };
1711
1902
  });
1712
1903
  }
1904
+ function listAllPanesByWindow() {
1905
+ const output = execSafe('tmux list-panes -a -F "#{window_id} #{pane_id} #{pane_pid}"', void 0, TMUX_TIMEOUT_MS);
1906
+ const map = /* @__PURE__ */ new Map();
1907
+ if (!output) return map;
1908
+ for (const line of output.split("\n")) {
1909
+ if (!line) continue;
1910
+ const [windowId, paneId, panePid] = line.split(" ");
1911
+ if (!windowId || !paneId || !panePid) continue;
1912
+ const bucket = map.get(windowId);
1913
+ if (bucket) bucket.push({ paneId, panePid });
1914
+ else map.set(windowId, [{ paneId, panePid }]);
1915
+ }
1916
+ return map;
1917
+ }
1713
1918
  function setPaneTitle(paneTarget, title) {
1714
1919
  execSafe(`tmux select-pane -t ${t(paneTarget)} -T ${shellQuote(title)}`);
1715
1920
  }
@@ -1754,11 +1959,6 @@ function listAllSessions() {
1754
1959
  if (!output) return [];
1755
1960
  return output.split("\n").filter(Boolean).map(parseSessionLine);
1756
1961
  }
1757
- function listWindowPanes(windowTarget) {
1758
- const output = execSafe(`tmux list-panes -t ${t(windowTarget)} -F '#{pane_id}'`);
1759
- if (!output) return [];
1760
- return output.split("\n").filter(Boolean).map((paneId) => ({ paneId }));
1761
- }
1762
1962
  function listAllPanes() {
1763
1963
  const output = execSafe('tmux list-panes -a -F "#{session_name} #{pane_id}"');
1764
1964
  if (!output) return [];
@@ -1767,6 +1967,23 @@ function listAllPanes() {
1767
1967
  return { sessionName: line.slice(0, spaceIdx), paneId: line.slice(spaceIdx + 1) };
1768
1968
  });
1769
1969
  }
1970
+ function listAllWindowsBySession() {
1971
+ const output = execSafe('tmux list-windows -a -F "#{session_name} #{window_index} #{window_id} #{window_name}"', void 0, TMUX_TIMEOUT_MS);
1972
+ const map = /* @__PURE__ */ new Map();
1973
+ if (!output) return map;
1974
+ for (const line of output.split("\n")) {
1975
+ if (!line) continue;
1976
+ const [sessionName, indexStr, id, ...nameParts] = line.split(" ");
1977
+ if (!sessionName || indexStr == null || !id) continue;
1978
+ const index = parseInt(indexStr, 10);
1979
+ if (!Number.isFinite(index)) continue;
1980
+ const name = nameParts.join(" ");
1981
+ const bucket = map.get(sessionName);
1982
+ if (bucket) bucket.push({ index, id, name });
1983
+ else map.set(sessionName, [{ index, id, name }]);
1984
+ }
1985
+ return map;
1986
+ }
1770
1987
  function configureSessionDefaults(sessionTarget, windowId) {
1771
1988
  execSafe(`tmux set -w -t ${t(windowId)} pane-border-status top`);
1772
1989
  execSafe(`tmux set -w -t ${t(windowId)} allow-rename off`);
@@ -2005,7 +2222,7 @@ var init_summarize = __esm({
2005
2222
  });
2006
2223
 
2007
2224
  // src/daemon/plugins.ts
2008
- import { readFileSync as readFileSync8 } from "fs";
2225
+ import { readFileSync as readFileSync9 } from "fs";
2009
2226
  import { execFileSync } from "child_process";
2010
2227
  import { homedir as homedir4 } from "os";
2011
2228
  import { join as join9 } from "path";
@@ -2015,7 +2232,7 @@ function installedPluginsPath() {
2015
2232
  function resolveInstalledPlugin(name) {
2016
2233
  let data;
2017
2234
  try {
2018
- data = JSON.parse(readFileSync8(installedPluginsPath(), "utf-8"));
2235
+ data = JSON.parse(readFileSync9(installedPluginsPath(), "utf-8"));
2019
2236
  } catch {
2020
2237
  return null;
2021
2238
  }
@@ -2078,7 +2295,7 @@ var init_plugins = __esm({
2078
2295
  });
2079
2296
 
2080
2297
  // src/daemon/history.ts
2081
- import { appendFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7, renameSync as renameSync2, readdirSync as readdirSync6, readFileSync as readFileSync9, rmSync as rmSync4, statSync as statSync3 } from "fs";
2298
+ import { appendFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync7, renameSync as renameSync2, readdirSync as readdirSync6, readFileSync as readFileSync10, rmSync as rmSync4, statSync as statSync3 } from "fs";
2082
2299
  import { randomUUID as randomUUID2 } from "crypto";
2083
2300
  import { dirname as dirname2, join as join10 } from "path";
2084
2301
  function ensureDir(sessionId) {
@@ -2181,7 +2398,7 @@ function getRecentSentiments(count = 5, scanLimit = 30, overrideBaseDir) {
2181
2398
  const limit = Math.min(withMtime.length, scanLimit);
2182
2399
  for (let i = 0; i < limit && results.length < count; i++) {
2183
2400
  try {
2184
- const raw = readFileSync9(join10(base, withMtime[i].name, "session.json"), "utf-8");
2401
+ const raw = readFileSync10(join10(base, withMtime[i].name, "session.json"), "utf-8");
2185
2402
  const summary = JSON.parse(raw);
2186
2403
  if (summary.sentiment && summary.completedAt) {
2187
2404
  results.push({
@@ -2213,13 +2430,13 @@ function pruneHistory() {
2213
2430
  const dir = join10(base, name);
2214
2431
  try {
2215
2432
  const summaryPath = join10(dir, "session.json");
2216
- const raw = readFileSync9(summaryPath, "utf-8");
2433
+ const raw = readFileSync10(summaryPath, "utf-8");
2217
2434
  const summary = JSON.parse(raw);
2218
2435
  sessions.push({ dir, startedAt: new Date(summary.startedAt ?? 0).getTime() });
2219
2436
  } catch {
2220
2437
  try {
2221
2438
  const eventsPath = join10(dir, "events.jsonl");
2222
- const firstLine = readFileSync9(eventsPath, "utf-8").split("\n")[0];
2439
+ const firstLine = readFileSync10(eventsPath, "utf-8").split("\n")[0];
2223
2440
  const firstEvent = JSON.parse(firstLine);
2224
2441
  sessions.push({ dir, startedAt: new Date(firstEvent.ts ?? 0).getTime() });
2225
2442
  } catch {
@@ -2255,8 +2472,8 @@ var init_history = __esm({
2255
2472
  });
2256
2473
 
2257
2474
  // src/shared/platform.ts
2258
- import { execSync as execSync3 } from "child_process";
2259
- import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
2475
+ import { execSync as execSync4 } from "child_process";
2476
+ import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
2260
2477
  function detectPlatform() {
2261
2478
  if (cachedPlatform) return cachedPlatform;
2262
2479
  if (process.platform === "darwin") {
@@ -2273,8 +2490,8 @@ function detectPlatform() {
2273
2490
  function isWsl() {
2274
2491
  if (process.env["WSL_DISTRO_NAME"] || process.env["WSL_INTEROP"]) return true;
2275
2492
  try {
2276
- if (existsSync8("/proc/version")) {
2277
- const v = readFileSync10("/proc/version", "utf-8").toLowerCase();
2493
+ if (existsSync9("/proc/version")) {
2494
+ const v = readFileSync11("/proc/version", "utf-8").toLowerCase();
2278
2495
  if (v.includes("microsoft") || v.includes("wsl")) return true;
2279
2496
  }
2280
2497
  } catch {
@@ -2285,7 +2502,7 @@ function hasCommand(cmd) {
2285
2502
  const cached = cmdCache.get(cmd);
2286
2503
  if (cached !== void 0) return cached;
2287
2504
  try {
2288
- execSync3(`command -v ${cmd}`, { stdio: "pipe", shell: "/bin/sh" });
2505
+ execSync4(`command -v ${cmd}`, { stdio: "pipe", shell: "/bin/sh" });
2289
2506
  cmdCache.set(cmd, true);
2290
2507
  return true;
2291
2508
  } catch {
@@ -2303,7 +2520,7 @@ var init_platform = __esm({
2303
2520
 
2304
2521
  // src/daemon/notify.ts
2305
2522
  import { spawn, execFile } from "child_process";
2306
- import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync9 } from "fs";
2523
+ import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync5, existsSync as existsSync10 } from "fs";
2307
2524
  import { join as join11 } from "path";
2308
2525
  import { homedir as homedir5 } from "os";
2309
2526
  function ensureSwitchScript() {
@@ -2323,7 +2540,7 @@ function ensureNotifyProcess() {
2323
2540
  return notifyProcess;
2324
2541
  }
2325
2542
  const binary = getNotifyBinary();
2326
- if (!existsSync9(binary)) {
2543
+ if (!existsSync10(binary)) {
2327
2544
  return null;
2328
2545
  }
2329
2546
  notifyProcess = spawn(binary, [], {
@@ -2445,7 +2662,7 @@ var init_notify = __esm({
2445
2662
  });
2446
2663
 
2447
2664
  // src/daemon/ask-store.ts
2448
- import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync11, readdirSync as readdirSync7 } from "fs";
2665
+ import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync12, readdirSync as readdirSync7 } from "fs";
2449
2666
  function maybeNotifyOnAskCreated(cwd, sessionId, meta) {
2450
2667
  if (process.env.NODE_ENV === "test" || process.env.SISYPHUS_DISABLE_NOTIFY === "1") return;
2451
2668
  const isActionable = meta.kind !== void 0 && ACTIONABLE_KINDS.has(meta.kind);
@@ -2496,7 +2713,7 @@ function writeDecisions(cwd, sessionId, askId, deck) {
2496
2713
  function readDecisions(cwd, sessionId, askId) {
2497
2714
  const p = askDecisionsPath(cwd, sessionId, askId);
2498
2715
  try {
2499
- return JSON.parse(readFileSync11(p, { encoding: "utf-8" }));
2716
+ return JSON.parse(readFileSync12(p, { encoding: "utf-8" }));
2500
2717
  } catch (_e) {
2501
2718
  return null;
2502
2719
  }
@@ -2509,25 +2726,50 @@ function writeOutput(cwd, sessionId, askId, responses, completedAt) {
2509
2726
  }
2510
2727
  function readMeta(cwd, sessionId, askId) {
2511
2728
  const p = askMetaPath(cwd, sessionId, askId);
2512
- if (!existsSync10(p)) {
2729
+ if (!existsSync11(p)) {
2513
2730
  return null;
2514
2731
  }
2515
- return JSON.parse(readFileSync11(p, "utf-8"));
2732
+ return JSON.parse(readFileSync12(p, "utf-8"));
2516
2733
  }
2517
2734
  async function updateMeta(cwd, sessionId, askId, patch) {
2518
- return withLock(askId, () => {
2735
+ const next = await withLock(askId, () => {
2519
2736
  const cur = readMeta(cwd, sessionId, askId);
2520
2737
  if (!cur) {
2521
2738
  throw new Error(`updateMeta: askId ${askId} not found`);
2522
2739
  }
2523
- const next = { ...cur, ...patch };
2524
- atomicWrite(askMetaPath(cwd, sessionId, askId), JSON.stringify(next, null, 2));
2525
- return next;
2740
+ const updated = { ...cur, ...patch };
2741
+ atomicWrite(askMetaPath(cwd, sessionId, askId), JSON.stringify(updated, null, 2));
2742
+ return updated;
2743
+ });
2744
+ if (patch.status === "answered" && next.heartbeatAskId) {
2745
+ const hbAskId = next.heartbeatAskId;
2746
+ cascadeResolveHeartbeatAsk(cwd, sessionId, hbAskId).catch((err) => {
2747
+ console.warn(
2748
+ `[sisyphus] heartbeat cascade-resolve failed for ${hbAskId}:`,
2749
+ err instanceof Error ? err.message : err
2750
+ );
2751
+ });
2752
+ }
2753
+ return next;
2754
+ }
2755
+ async function cascadeResolveHeartbeatAsk(cwd, sessionId, heartbeatAskId) {
2756
+ const hbMeta = readMeta(cwd, sessionId, heartbeatAskId);
2757
+ if (!hbMeta) return;
2758
+ if (hbMeta.status === "answered") return;
2759
+ if (existsSync11(askOutputPath(cwd, sessionId, heartbeatAskId))) return;
2760
+ writeOutput(cwd, sessionId, heartbeatAskId, [{
2761
+ id: "heartbeat",
2762
+ selectedOptionId: "ack",
2763
+ freetext: "auto-resolved: original ask was answered"
2764
+ }]);
2765
+ await updateMeta(cwd, sessionId, heartbeatAskId, {
2766
+ status: "answered",
2767
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
2526
2768
  });
2527
2769
  }
2528
2770
  function listAsks(cwd, sessionId) {
2529
2771
  const dir = askDir(cwd, sessionId);
2530
- if (!existsSync10(dir)) {
2772
+ if (!existsSync11(dir)) {
2531
2773
  return [];
2532
2774
  }
2533
2775
  return readdirSync7(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
@@ -2543,7 +2785,7 @@ function buildAutoResponses(deck) {
2543
2785
  }
2544
2786
  async function autoResolveAsk(cwd, sessionId, askId, deck) {
2545
2787
  try {
2546
- if (existsSync10(askOutputPath(cwd, sessionId, askId))) return false;
2788
+ if (existsSync11(askOutputPath(cwd, sessionId, askId))) return false;
2547
2789
  const d = deck ?? readDecisions(cwd, sessionId, askId);
2548
2790
  if (!d) return false;
2549
2791
  const responses = buildAutoResponses(d);
@@ -2563,6 +2805,8 @@ async function maybeAutoResolveAsk(cwd, sessionId, askId, deck) {
2563
2805
  try {
2564
2806
  if (!isSessionDangerous(cwd, sessionId)) return;
2565
2807
  if (deck.source?.askedBy === ORPHAN_ASKED_BY) return;
2808
+ const meta = readMeta(cwd, sessionId, askId);
2809
+ if (meta?.kind === "review") return;
2566
2810
  await autoResolveAsk(cwd, sessionId, askId, deck);
2567
2811
  } catch {
2568
2812
  }
@@ -2576,7 +2820,7 @@ function listOpenAsksFor(cwd, sessionId, askedBy) {
2576
2820
  if (meta.orphaned) continue;
2577
2821
  if (!meta.blocking) continue;
2578
2822
  if (meta.status !== "pending" && meta.status !== "in-progress") continue;
2579
- if (existsSync10(askOutputPath(cwd, sessionId, askId))) continue;
2823
+ if (existsSync11(askOutputPath(cwd, sessionId, askId))) continue;
2580
2824
  out.push({ askId, status: meta.status, ...meta.title !== void 0 ? { title: meta.title } : {} });
2581
2825
  }
2582
2826
  return out;
@@ -2596,7 +2840,8 @@ var init_ask_store = __esm({
2596
2840
  "validation",
2597
2841
  "decision",
2598
2842
  "context",
2599
- "error"
2843
+ "error",
2844
+ "review"
2600
2845
  ]);
2601
2846
  HEARTBEAT_ASKED_BY = "system:heartbeat";
2602
2847
  ORPHAN_ASKED_BY = "system:orphan-handler";
@@ -2792,9 +3037,38 @@ async function orphanRunningAgentAsks(cwd, sessionId, session) {
2792
3037
  if (runningAgentIds.length === 0) return;
2793
3038
  await Promise.all(runningAgentIds.map((agentId) => markAgentAsksOrphan(cwd, sessionId, agentId)));
2794
3039
  }
2795
- async function resolveOrchestratorOrphanAsks(cwd, sessionId, selectedOptionId) {
3040
+ async function resolveAgentOrphanAsks(cwd, sessionId, agentId, resolution) {
3041
+ const asks = listAsks(cwd, sessionId);
3042
+ const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3043
+ const selectedOptionId = "dismiss";
3044
+ const freetext = resolution === "respawn" ? "auto-resolved: agent was restarted" : resolution === "superseded" ? "auto-resolved: agent superseded by replacement" : "auto-resolved by system";
3045
+ await mapWithLimit(asks, ASK_FANOUT_LIMIT, async (askId) => {
3046
+ try {
3047
+ const meta = readMeta(cwd, sessionId, askId);
3048
+ if (!meta) return;
3049
+ if (meta.askedBy !== ORPHAN_ASKED_BY2) return;
3050
+ if (meta.status === "answered") return;
3051
+ if (meta.orphanTarget?.kind !== "agent") return;
3052
+ if (meta.orphanTarget.agentId !== agentId) return;
3053
+ writeOutput(cwd, sessionId, askId, [{
3054
+ id: "orphan",
3055
+ selectedOptionId,
3056
+ freetext
3057
+ }], completedAt);
3058
+ await updateMeta(cwd, sessionId, askId, { status: "answered", completedAt });
3059
+ } catch (err) {
3060
+ console.warn(
3061
+ `[sisyphus] resolveAgentOrphanAsks: ${sessionId}/${askId} failed:`,
3062
+ err instanceof Error ? err.message : err
3063
+ );
3064
+ }
3065
+ });
3066
+ }
3067
+ async function resolveOrchestratorOrphanAsks(cwd, sessionId, resolution) {
2796
3068
  const asks = listAsks(cwd, sessionId);
2797
3069
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3070
+ const selectedOptionId = resolution === "respawn" ? "dismiss" : resolution;
3071
+ const freetext = resolution === "resume" ? "auto-resolved by sis session lifecycle resume" : resolution === "respawn" ? "auto-resolved by orchestrator auto-respawn" : "auto-resolved by system";
2798
3072
  await mapWithLimit(asks, ASK_FANOUT_LIMIT, async (askId) => {
2799
3073
  try {
2800
3074
  const meta = readMeta(cwd, sessionId, askId);
@@ -2805,7 +3079,7 @@ async function resolveOrchestratorOrphanAsks(cwd, sessionId, selectedOptionId) {
2805
3079
  writeOutput(cwd, sessionId, askId, [{
2806
3080
  id: "orphan",
2807
3081
  selectedOptionId,
2808
- freetext: `auto-resolved by ${selectedOptionId === "resume" ? "sis session resume" : "system"}`
3082
+ freetext
2809
3083
  }], completedAt);
2810
3084
  await updateMeta(cwd, sessionId, askId, { status: "answered", completedAt });
2811
3085
  } catch (err) {
@@ -2845,17 +3119,18 @@ var init_process = __esm({
2845
3119
  });
2846
3120
 
2847
3121
  // src/daemon/orphan-sweep.ts
2848
- import { existsSync as existsSync11 } from "fs";
2849
- import { execSync as execSync4 } from "child_process";
2850
- function probePidLstart(pid, expectedLstart, psRunner = defaultPsRunner) {
3122
+ import { existsSync as existsSync12 } from "fs";
3123
+ import { execFile as execFile2 } from "child_process";
3124
+ import { promisify } from "util";
3125
+ async function probePidLstart(pid, expectedLstart, psRunner = defaultPsRunner) {
2851
3126
  try {
2852
- const lstart = psRunner(pid, execEnv());
3127
+ const lstart = await psRunner(pid, execEnv());
2853
3128
  if (!lstart) return "gone";
2854
3129
  if (lstart === expectedLstart) return "live";
2855
3130
  return "recycled";
2856
3131
  } catch (err) {
2857
3132
  const e = err;
2858
- if (e.status === 1) return "gone";
3133
+ if (e.code === 1) return "gone";
2859
3134
  return "unknown";
2860
3135
  }
2861
3136
  }
@@ -2865,7 +3140,7 @@ async function capturePanePidLstart(paneId) {
2865
3140
  try {
2866
3141
  const pid = getPanePid(paneId);
2867
3142
  if (pid === null) throw new Error("tmux returned non-integer pane_pid");
2868
- const lstart = defaultPsRunner(pid, env);
3143
+ const lstart = await defaultPsRunner(pid, env);
2869
3144
  if (!lstart) throw new Error("ps returned empty lstart");
2870
3145
  return { pid, lstart };
2871
3146
  } catch (captureErr) {
@@ -2879,18 +3154,22 @@ async function capturePanePidLstart(paneId) {
2879
3154
  }
2880
3155
  async function sweepOrphans(registry) {
2881
3156
  const reg = registry ?? loadSessionRegistry();
2882
- for (const [sessionId, cwd] of Object.entries(reg)) {
2883
- if (!existsSync11(statePath(cwd, sessionId))) continue;
2884
- try {
2885
- await sweepSessionAgents(cwd, sessionId);
2886
- await sweepSessionAsks(cwd, sessionId);
2887
- } catch (err) {
2888
- console.warn(
2889
- `[sisyphus] orphan-sweep failed for ${sessionId}:`,
2890
- err instanceof Error ? err.message : err
2891
- );
2892
- }
2893
- }
3157
+ await Promise.all(
3158
+ Object.entries(reg).map(async ([sessionId, cwd]) => {
3159
+ if (!existsSync12(statePath(cwd, sessionId))) return;
3160
+ try {
3161
+ await Promise.all([
3162
+ sweepSessionAgents(cwd, sessionId),
3163
+ sweepSessionAsks(cwd, sessionId)
3164
+ ]);
3165
+ } catch (err) {
3166
+ console.warn(
3167
+ `[sisyphus] orphan-sweep failed for ${sessionId}:`,
3168
+ err instanceof Error ? err.message : err
3169
+ );
3170
+ }
3171
+ })
3172
+ );
2894
3173
  }
2895
3174
  async function sweepSessionAgents(cwd, sessionId) {
2896
3175
  let session;
@@ -2899,11 +3178,13 @@ async function sweepSessionAgents(cwd, sessionId) {
2899
3178
  } catch {
2900
3179
  return;
2901
3180
  }
2902
- for (const agent of session.agents) {
2903
- if (agent.status !== "running") continue;
2904
- if (agent.orphaned) continue;
2905
- if (agent.pid === void 0 || !agent.pidLstart) continue;
2906
- const probe = probePidLstart(agent.pid, agent.pidLstart);
3181
+ const candidates = session.agents.filter(
3182
+ (a) => a.status === "running" && !a.orphaned && a.pid !== void 0 && a.pidLstart
3183
+ );
3184
+ const probes = await Promise.all(
3185
+ candidates.map(async (a) => ({ agent: a, probe: await probePidLstart(a.pid, a.pidLstart) }))
3186
+ );
3187
+ for (const { agent, probe } of probes) {
2907
3188
  if (probe === "live" || probe === "unknown") continue;
2908
3189
  await markAgentOrphan(cwd, sessionId, agent.id, {
2909
3190
  reason: probe === "recycled" ? "pid recycled (daemon-startup sweep)" : "process gone (daemon-startup sweep)",
@@ -2931,7 +3212,7 @@ async function sweepSessionAsks(cwd, sessionId) {
2931
3212
  await updateMeta(cwd, sessionId, askId, { orphaned: true });
2932
3213
  }
2933
3214
  }
2934
- var defaultPsRunner;
3215
+ var execFileAsync, defaultPsRunner;
2935
3216
  var init_orphan_sweep = __esm({
2936
3217
  "src/daemon/orphan-sweep.ts"() {
2937
3218
  "use strict";
@@ -2943,12 +3224,16 @@ var init_orphan_sweep = __esm({
2943
3224
  init_paths();
2944
3225
  init_orphan_asks();
2945
3226
  init_process();
2946
- defaultPsRunner = (pid, env) => execSync4(`ps -o lstart= -p ${pid}`, { encoding: "utf-8", env, stdio: ["ignore", "pipe", "ignore"] }).trim();
3227
+ execFileAsync = promisify(execFile2);
3228
+ defaultPsRunner = async (pid, env) => {
3229
+ const { stdout } = await execFileAsync("ps", ["-o", "lstart=", "-p", String(pid)], { env });
3230
+ return stdout.trim();
3231
+ };
2947
3232
  }
2948
3233
  });
2949
3234
 
2950
3235
  // src/daemon/agent.ts
2951
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync8, existsSync as existsSync12, unlinkSync } from "fs";
3236
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, mkdirSync as mkdirSync7, readdirSync as readdirSync8, existsSync as existsSync13, unlinkSync } from "fs";
2952
3237
  import { execSync as execSync5 } from "child_process";
2953
3238
  import { randomUUID as randomUUID3 } from "crypto";
2954
3239
  import { resolve as resolve5, relative as relative2, dirname as dirname3, join as join12 } from "path";
@@ -2970,13 +3255,15 @@ function renderAgentSuffix(sessionId, instruction, contextDirRel) {
2970
3255
  const templatePath = resolve5(import.meta.dirname, "../templates/agent-suffix.md");
2971
3256
  let template;
2972
3257
  try {
2973
- template = readFileSync12(templatePath, "utf-8");
3258
+ template = readFileSync13(templatePath, "utf-8");
2974
3259
  } catch {
2975
3260
  template = `# Sisyphus Agent
2976
3261
  Session: {{SESSION_ID}}
2977
3262
  Task: {{INSTRUCTION}}`;
2978
3263
  }
2979
- return template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{CONTEXT_DIR\}\}/g, contextDirRel);
3264
+ return injectHelp(
3265
+ template.replace(/\{\{SESSION_ID\}\}/g, sessionId).replace(/\{\{INSTRUCTION\}\}/g, instruction).replace(/\{\{CONTEXT_DIR\}\}/g, contextDirRel)
3266
+ );
2980
3267
  }
2981
3268
  function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
2982
3269
  const base = `${promptsDir(cwd, sessionId)}/${agentId}-plugin`;
@@ -2994,10 +3281,10 @@ function createAgentPlugin(cwd, sessionId, agentId, agentType, agentConfig) {
2994
3281
  if (agentConfig?.filePath && agentType && agentType !== "worker") {
2995
3282
  const shortName = agentType.replace(/^sisyphus:/, "");
2996
3283
  const subAgentDir = join12(dirname3(agentConfig.filePath), shortName);
2997
- if (existsSync12(subAgentDir)) {
3284
+ if (existsSync13(subAgentDir)) {
2998
3285
  for (const f of readdirSync8(subAgentDir)) {
2999
3286
  if (f.endsWith(".md") && f !== "CLAUDE.md") {
3000
- writeFileSync9(`${base}/agents/${f}`, substituteEnvVars(readFileSync12(join12(subAgentDir, f), "utf-8")), "utf-8");
3287
+ writeFileSync9(`${base}/agents/${f}`, substituteEnvVars(readFileSync13(join12(subAgentDir, f), "utf-8")), "utf-8");
3001
3288
  }
3002
3289
  }
3003
3290
  }
@@ -3094,7 +3381,24 @@ ${instruction}`);
3094
3381
  const sessionIdFlag = claudeSessionId ? ` --session-id "${claudeSessionId}"` : "";
3095
3382
  const promptFlag = agentConfig?.frontmatter.systemPrompt === "replace" ? "--system-prompt" : "--append-system-prompt";
3096
3383
  const siblingSettingsPath = agentConfig?.filePath ? agentConfig.filePath.replace(/\.md$/, ".settings.json") : null;
3097
- const settingsFlag = siblingSettingsPath && existsSync12(siblingSettingsPath) ? ` --settings "${siblingSettingsPath}"` : "";
3384
+ const hasSibling = !!siblingSettingsPath && existsSync13(siblingSettingsPath);
3385
+ const agentDigestVerbs = digestSpinnerVerbs(cwd, sessionId);
3386
+ let resolvedSettingsPath = hasSibling ? siblingSettingsPath : null;
3387
+ if (agentDigestVerbs) {
3388
+ let base = {};
3389
+ if (hasSibling) {
3390
+ try {
3391
+ base = JSON.parse(readFileSync13(siblingSettingsPath, "utf-8"));
3392
+ } catch {
3393
+ base = {};
3394
+ }
3395
+ }
3396
+ base.spinnerVerbs = { mode: "replace", verbs: agentDigestVerbs };
3397
+ const mergedSettingsOut = join12(promptsDir(cwd, sessionId), `${agentId}-settings.merged.json`);
3398
+ writeFileSync9(mergedSettingsOut, JSON.stringify(base, null, 2), "utf-8");
3399
+ resolvedSettingsPath = mergedSettingsOut;
3400
+ }
3401
+ const settingsFlag = resolvedSettingsPath ? ` --settings "${resolvedSettingsPath}"` : "";
3098
3402
  mainCmd = `claude${permFlag} --effort ${effort}${modelFlag} --plugin-dir "${pluginPath}"${sessionIdFlag}${extraPluginFlags ? ` ${extraPluginFlags}` : ""}${settingsFlag} --name ${shellQuote(agentTitle)} ${promptFlag} "$(cat '${suffixFilePath}')" ${shellQuote(instruction)}`;
3099
3403
  resumeArgs = `${permFlag.trimStart()} --effort ${effort}${modelFlag} --plugin-dir "${pluginPath}"${extraPluginFlags ? ` ${extraPluginFlags}` : ""}${settingsFlag}`;
3100
3404
  }
@@ -3128,12 +3432,12 @@ async function spawnAgent(opts) {
3128
3432
  try {
3129
3433
  execSync5(`which ${fallbackCli}`, { stdio: "pipe", env: execEnv() });
3130
3434
  } catch {
3131
- throw new Error(`Neither ${cliToCheck} (model: ${agentConfig?.frontmatter.model}) nor ${fallbackCli} (fallback: ${fallback}) CLI found on PATH. Run \`sis admin doctor\` to diagnose.`);
3435
+ throw new Error(`Neither ${cliToCheck} (model: ${agentConfig?.frontmatter.model}) nor ${fallbackCli} (fallback: ${fallback}) CLI found on PATH. Run \`sis admin check doctor\` to diagnose.`);
3132
3436
  }
3133
3437
  if (agentConfig) agentConfig.frontmatter.model = fallback;
3134
3438
  provider = fallbackProvider;
3135
3439
  } else {
3136
- throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sis admin doctor\` to diagnose.`);
3440
+ throw new Error(`${cliToCheck} CLI not found on PATH. Run \`sis admin check doctor\` to diagnose.`);
3137
3441
  }
3138
3442
  }
3139
3443
  const repo = opts.repo !== void 0 ? opts.repo : ".";
@@ -3250,6 +3554,12 @@ async function restartAgent(sessionId, cwd, agentId, windowId) {
3250
3554
  await setAgentPid(cwd, sessionId, agentId, capturedRestart.pid, capturedRestart.lstart);
3251
3555
  }
3252
3556
  emitHistoryEvent(sessionId, "agent-restarted", { agentId, restartCount, originalSpawnedAt, previousStatus, claudeSessionId });
3557
+ resolveAgentOrphanAsks(cwd, sessionId, agentId, "respawn").catch((err) => {
3558
+ console.warn(
3559
+ `[sisyphus] resolveAgentOrphanAsks on restart failed for ${agentId}:`,
3560
+ err instanceof Error ? err.message : err
3561
+ );
3562
+ });
3253
3563
  }
3254
3564
  function nextReportNumber(cwd, sessionId, agentId) {
3255
3565
  const dir = reportsDir(cwd, sessionId);
@@ -3283,9 +3593,9 @@ async function handleAgentReport(cwd, sessionId, agentId, content) {
3283
3593
  }
3284
3594
  function gcBgTasks(cwd, sessionId, agentId) {
3285
3595
  const file = `${sessionDir(cwd, sessionId)}/runtime/bg-tasks/${agentId}.txt`;
3286
- if (!existsSync12(file)) return;
3596
+ if (!existsSync13(file)) return;
3287
3597
  try {
3288
- const leftover = readFileSync12(file, "utf-8").split("\n").map((s) => s.trim()).filter(Boolean);
3598
+ const leftover = readFileSync13(file, "utf-8").split("\n").map((s) => s.trim()).filter(Boolean);
3289
3599
  if (leftover.length > 0) {
3290
3600
  console.warn(`[bg-tasks] ${agentId} exited with ${leftover.length} untracked background task(s): ${leftover.join(", ")}`);
3291
3601
  emitHistoryEvent(sessionId, "bg-tasks-leftover", { agentId, leftover });
@@ -3392,6 +3702,7 @@ var init_agent = __esm({
3392
3702
  init_tmux();
3393
3703
  init_colors();
3394
3704
  init_paths();
3705
+ init_help_inject();
3395
3706
  init_pane_registry();
3396
3707
  init_pane_monitor();
3397
3708
  init_summarize();
@@ -3401,6 +3712,7 @@ var init_agent = __esm({
3401
3712
  init_shell();
3402
3713
  init_spawn_helpers();
3403
3714
  init_plugins();
3715
+ init_digest_verbs();
3404
3716
  init_history();
3405
3717
  init_orphan_asks();
3406
3718
  init_orphan_sweep();
@@ -3558,7 +3870,7 @@ var init_companion_types = __esm({
3558
3870
  });
3559
3871
 
3560
3872
  // src/daemon/companion-memory.ts
3561
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync13, renameSync as renameSync3, writeFileSync as writeFileSync10 } from "fs";
3873
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync3, writeFileSync as writeFileSync10 } from "fs";
3562
3874
  import { dirname as dirname4, join as join13 } from "path";
3563
3875
  import { randomUUID as randomUUID4 } from "crypto";
3564
3876
  import { z } from "zod";
@@ -3591,10 +3903,10 @@ function fillDefaults(state) {
3591
3903
  }
3592
3904
  function loadMemoryStrict() {
3593
3905
  const path = resolvedMemoryPath();
3594
- if (!existsSync13(path)) return defaultMemoryState();
3906
+ if (!existsSync14(path)) return defaultMemoryState();
3595
3907
  let raw;
3596
3908
  try {
3597
- raw = readFileSync13(path, "utf-8");
3909
+ raw = readFileSync14(path, "utf-8");
3598
3910
  } catch (err) {
3599
3911
  throw new MemoryStoreParseError(err);
3600
3912
  }
@@ -4035,7 +4347,7 @@ var init_companion_memory = __esm({
4035
4347
  });
4036
4348
 
4037
4349
  // src/daemon/companion.ts
4038
- import { existsSync as existsSync14, mkdirSync as mkdirSync9, readFileSync as readFileSync14, renameSync as renameSync4, writeFileSync as writeFileSync11 } from "fs";
4350
+ import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync15, renameSync as renameSync4, writeFileSync as writeFileSync11 } from "fs";
4039
4351
  import { randomUUID as randomUUID5 } from "crypto";
4040
4352
  import { dirname as dirname5, join as join14 } from "path";
4041
4353
  function welfordUpdate(stats, value) {
@@ -4059,12 +4371,12 @@ function zScore(value, stats, metric) {
4059
4371
  }
4060
4372
  function loadCompanion() {
4061
4373
  const path = companionPath();
4062
- if (!existsSync14(path)) {
4374
+ if (!existsSync15(path)) {
4063
4375
  const state2 = createDefaultCompanion();
4064
4376
  saveCompanion(state2);
4065
4377
  return state2;
4066
4378
  }
4067
- const raw = readFileSync14(path, "utf-8");
4379
+ const raw = readFileSync15(path, "utf-8");
4068
4380
  const state = JSON.parse(raw);
4069
4381
  return normalizeCompanion(state);
4070
4382
  }
@@ -4245,7 +4557,7 @@ function computeMood(companion, session, signals) {
4245
4557
  const now = Date.now();
4246
4558
  const weekAgo = now - 7 * 24 * 36e5;
4247
4559
  const weeklyCompletions = companion.recentCompletions.filter(
4248
- (ts2) => new Date(ts2).getTime() > weekAgo
4560
+ (ts) => new Date(ts).getTime() > weekAgo
4249
4561
  ).length;
4250
4562
  const weeklyAvgDaily = weeklyCompletions / 7;
4251
4563
  const weeklyZ = zScore(weeklyAvgDaily, baselines.sessionsPerDay, "sessionsPerDay");
@@ -4638,8 +4950,8 @@ var init_companion = __esm({
4638
4950
  },
4639
4951
  "overdrive": (c) => {
4640
4952
  const dateCounts = {};
4641
- for (const ts2 of c.recentCompletions) {
4642
- const date = ts2.slice(0, 10);
4953
+ for (const ts of c.recentCompletions) {
4954
+ const date = ts.slice(0, 10);
4643
4955
  dateCounts[date] = (dateCounts[date] ?? 0) + 1;
4644
4956
  }
4645
4957
  return Object.values(dateCounts).some((count) => count >= 6);
@@ -5803,7 +6115,7 @@ var init_companion_render = __esm({
5803
6115
  });
5804
6116
 
5805
6117
  // src/daemon/companion-popup.ts
5806
- import { writeFileSync as writeFileSync12, readFileSync as readFileSync15, unlinkSync as unlinkSync2, existsSync as existsSync15 } from "fs";
6118
+ import { writeFileSync as writeFileSync12, readFileSync as readFileSync16, unlinkSync as unlinkSync2, existsSync as existsSync16 } from "fs";
5807
6119
  import { tmpdir } from "os";
5808
6120
  import { join as join15, resolve as resolve6 } from "path";
5809
6121
  function wrapText(text, width) {
@@ -5847,7 +6159,7 @@ function showCommentaryPopupQueue(pages) {
5847
6159
  if (contentHeight > maxContentHeight) maxContentHeight = contentHeight;
5848
6160
  writeFileSync12(`${POPUP_TMP_PREFIX}-${i}.txt`, content);
5849
6161
  }
5850
- const whipAvailable = existsSync15(WHIP_ANIMATION_PATH);
6162
+ const whipAvailable = existsSync16(WHIP_ANIMATION_PATH);
5851
6163
  if (whipAvailable && maxContentHeight < WHIP_ANIMATION_ROWS + 2) {
5852
6164
  maxContentHeight = WHIP_ANIMATION_ROWS + 2;
5853
6165
  }
@@ -5921,7 +6233,7 @@ fi
5921
6233
  }
5922
6234
  let raw;
5923
6235
  try {
5924
- raw = readFileSync15(POPUP_RESULT_PREFIX, "utf8").trim();
6236
+ raw = readFileSync16(POPUP_RESULT_PREFIX, "utf8").trim();
5925
6237
  } catch {
5926
6238
  return null;
5927
6239
  } finally {
@@ -6074,6 +6386,7 @@ function stopMonitor() {
6074
6386
  function trackSession(sessionId, cwd, tmuxSessionId, tmuxSessionName2) {
6075
6387
  const existing = trackedSessions.get(sessionId);
6076
6388
  trackedSessions.set(sessionId, { id: sessionId, cwd, tmuxSessionId, tmuxSessionName: tmuxSessionName2, windowId: existing ? existing.windowId : null });
6389
+ installStateWatcher(cwd, sessionId);
6077
6390
  if (!activeTimers.has(sessionId)) {
6078
6391
  try {
6079
6392
  const session = getSession(cwd, sessionId);
@@ -6088,7 +6401,9 @@ function updateTrackedWindow(sessionId, windowId) {
6088
6401
  entry.windowId = windowId;
6089
6402
  }
6090
6403
  function untrackSession(sessionId) {
6404
+ const entry = trackedSessions.get(sessionId);
6091
6405
  trackedSessions.delete(sessionId);
6406
+ if (entry) uninstallStateWatcher(entry.cwd, sessionId);
6092
6407
  }
6093
6408
  function hasRecentSessionActivity(s, recentCutoffMs) {
6094
6409
  for (const agent of s.agents) {
@@ -6112,13 +6427,16 @@ async function pollAllSessions() {
6112
6427
  const increment = elapsed > threshold ? storedPollIntervalMs : elapsed;
6113
6428
  lastPollTime = now;
6114
6429
  const pollSessionCache = /* @__PURE__ */ new Map();
6115
- for (const { id: sessionId, cwd, windowId } of trackedSessions.values()) {
6116
- if (windowId) {
6117
- await pollSession(sessionId, cwd, windowId, increment, pollSessionCache);
6118
- }
6119
- }
6430
+ const panesByWindow = listAllPanesByWindow();
6431
+ await Promise.all(
6432
+ [...trackedSessions.values()].filter(({ windowId }) => !!windowId).map(
6433
+ ({ id: sessionId, cwd, windowId }) => pollSession(sessionId, cwd, windowId, increment, panesByWindow, pollSessionCache).catch((err) => {
6434
+ console.error(`[sisyphus] pollSession error for ${sessionId}:`, err);
6435
+ })
6436
+ )
6437
+ );
6120
6438
  try {
6121
- onDotsUpdate?.();
6439
+ onDotsUpdate?.(panesByWindow);
6122
6440
  } catch {
6123
6441
  }
6124
6442
  try {
@@ -6269,11 +6587,12 @@ async function pollAllSessions() {
6269
6587
  } catch {
6270
6588
  }
6271
6589
  }
6272
- async function pollSession(sessionId, cwd, windowId, increment, sessionCache) {
6590
+ async function pollSession(sessionId, cwd, windowId, increment, panesByWindow, sessionCache2) {
6591
+ const panesForWindow = () => panesByWindow.get(windowId) ?? [];
6273
6592
  let session;
6274
6593
  try {
6275
6594
  session = getSession(cwd, sessionId);
6276
- sessionCache?.set(sessionId, session);
6595
+ sessionCache2?.set(sessionId, session);
6277
6596
  } catch (err) {
6278
6597
  console.error(`[sisyphus] Failed to read state for session ${sessionId}:`, err);
6279
6598
  return;
@@ -6281,8 +6600,7 @@ async function pollSession(sessionId, cwd, windowId, increment, sessionCache) {
6281
6600
  if (session.status === "completed") {
6282
6601
  const orchPaneId2 = getOrchestratorPaneId(sessionId);
6283
6602
  if (orchPaneId2) {
6284
- const livePanes2 = listPanes(windowId);
6285
- const livePaneIds2 = new Set(livePanes2.map((p) => p.paneId));
6603
+ const livePaneIds2 = new Set(panesForWindow().map((p) => p.paneId));
6286
6604
  if (!livePaneIds2.has(orchPaneId2)) {
6287
6605
  cleanupSessionMaps(sessionId);
6288
6606
  untrackSession(sessionId);
@@ -6295,7 +6613,7 @@ async function pollSession(sessionId, cwd, windowId, increment, sessionCache) {
6295
6613
  return;
6296
6614
  }
6297
6615
  if (session.status !== "active") return;
6298
- const livePanes = listPanes(windowId);
6616
+ const livePanes = panesForWindow();
6299
6617
  if (livePanes.length === 0) {
6300
6618
  if (respawningSessions.has(sessionId)) return;
6301
6619
  const tracked = trackedSessions.get(sessionId);
@@ -6346,9 +6664,9 @@ async function pollSession(sessionId, cwd, windowId, increment, sessionCache) {
6346
6664
  if (orchPaneId && !livePaneIds.has(orchPaneId) && !respawningSessions.has(sessionId)) {
6347
6665
  const cycleActiveMs = flushCycleTimer(sessionId, session.orchestratorCycles.length);
6348
6666
  await completeOrchestratorCycle(cwd, sessionId, void 0, void 0, cycleActiveMs);
6349
- await orphanOrchestrator(cwd, sessionId, "orchestrator pane vanished without yield", "orchestrator-gone");
6350
6667
  const runningAgents = session.agents.filter((a) => a.status === "running");
6351
6668
  if (runningAgents.length === 0) {
6669
+ await orphanOrchestrator(cwd, sessionId, "orchestrator pane vanished without yield", "orchestrator-gone");
6352
6670
  await flushTimers(sessionId);
6353
6671
  await updateSessionStatus(cwd, sessionId, "paused");
6354
6672
  console.log(`[sisyphus] Session ${sessionId} paused: orchestrator pane disappeared`);
@@ -6392,7 +6710,7 @@ var init_pane_monitor = __esm({
6392
6710
  });
6393
6711
 
6394
6712
  // src/daemon/mode-notify.ts
6395
- import { existsSync as existsSync16 } from "fs";
6713
+ import { existsSync as existsSync17 } from "fs";
6396
6714
  import { ulid as ulid2 } from "ulid";
6397
6715
  function capitalize(s) {
6398
6716
  return s.length === 0 ? s : s[0].toUpperCase() + s.slice(1);
@@ -6414,7 +6732,7 @@ function findOpenModeTransitionAsk(cwd, sessionId) {
6414
6732
  if (meta.modeTransition !== true) continue;
6415
6733
  if (meta.status === "answered") continue;
6416
6734
  if (meta.orphaned === true) continue;
6417
- if (existsSync16(askOutputPath(cwd, sessionId, askId))) continue;
6735
+ if (existsSync17(askOutputPath(cwd, sessionId, askId))) continue;
6418
6736
  return askId;
6419
6737
  }
6420
6738
  return null;
@@ -6478,13 +6796,14 @@ async function emitModeTransitionNotify(cwd, sessionId, prevMode, nextMode, prev
6478
6796
  kind: "notify",
6479
6797
  options: [{ id: "ack", label: "Acknowledged" }]
6480
6798
  };
6799
+ const deckSource = {
6800
+ ...sessionName !== void 0 ? { sessionName } : {},
6801
+ askedBy: ORCHESTRATOR_ASKED_BY,
6802
+ modeChain: chain
6803
+ };
6481
6804
  const deck = {
6482
6805
  title: deckTitle,
6483
- source: {
6484
- ...sessionName !== void 0 ? { sessionName } : {},
6485
- askedBy: ORCHESTRATOR_ASKED_BY,
6486
- modeChain: chain
6487
- },
6806
+ source: deckSource,
6488
6807
  interactions: [interaction]
6489
6808
  };
6490
6809
  try {
@@ -6528,14 +6847,14 @@ var init_mode_notify = __esm({
6528
6847
  });
6529
6848
 
6530
6849
  // src/daemon/orchestrator.ts
6531
- import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
6850
+ import { existsSync as existsSync18, readdirSync as readdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
6532
6851
  import { execSync as execSync6 } from "child_process";
6533
6852
  import { randomUUID as randomUUID6 } from "crypto";
6534
6853
  import { resolve as resolve7, join as join16, relative as relative3 } from "path";
6535
6854
  function detectRepos(cwd) {
6536
6855
  const config = loadConfig(cwd);
6537
6856
  const repos = [];
6538
- if (existsSync17(join16(cwd, ".git"))) {
6857
+ if (existsSync18(join16(cwd, ".git"))) {
6539
6858
  try {
6540
6859
  repos.push(getRepoInfo(cwd, "."));
6541
6860
  } catch {
@@ -6547,7 +6866,7 @@ function detectRepos(cwd) {
6547
6866
  if (!entry.isDirectory()) continue;
6548
6867
  if (entry.name.startsWith(".")) continue;
6549
6868
  const childPath = join16(cwd, entry.name);
6550
- if (existsSync17(join16(childPath, ".git"))) {
6869
+ if (existsSync18(join16(childPath, ".git"))) {
6551
6870
  try {
6552
6871
  repos.push(getRepoInfo(childPath, entry.name));
6553
6872
  } catch {
@@ -6585,40 +6904,44 @@ function resolveOrchestratorSettings(cwd, sessionId) {
6585
6904
  const bundled = resolve7(import.meta.dirname, "../templates/orchestrator-settings.json");
6586
6905
  const projectSettings = projectOrchestratorSettingsPath(cwd);
6587
6906
  const userSettings = userOrchestratorSettingsPath();
6588
- const hasProject = existsSync17(projectSettings);
6589
- const hasUser = existsSync17(userSettings);
6590
- if (!hasProject && !hasUser) return bundled;
6907
+ const hasProject = existsSync18(projectSettings);
6908
+ const hasUser = existsSync18(userSettings);
6909
+ const digestVerbs = digestSpinnerVerbs(cwd, sessionId);
6910
+ if (!hasProject && !hasUser && !digestVerbs) return bundled;
6591
6911
  let merged = {};
6592
6912
  for (const path of [bundled, hasUser ? userSettings : null, hasProject ? projectSettings : null]) {
6593
- if (!path || !existsSync17(path)) continue;
6913
+ if (!path || !existsSync18(path)) continue;
6594
6914
  try {
6595
- const parsed = JSON.parse(readFileSync16(path, "utf-8"));
6915
+ const parsed = JSON.parse(readFileSync17(path, "utf-8"));
6596
6916
  merged = { ...merged, ...parsed };
6597
6917
  } catch (err) {
6598
6918
  console.warn(`[sisyphus] Failed to parse settings layer ${path}: ${err instanceof Error ? err.message : err}`);
6599
6919
  }
6600
6920
  }
6921
+ if (digestVerbs) {
6922
+ merged.spinnerVerbs = { mode: "replace", verbs: digestVerbs };
6923
+ }
6601
6924
  const out = join16(promptsDir(cwd, sessionId), "orchestrator-settings.merged.json");
6602
6925
  writeFileSync13(out, JSON.stringify(merged, null, 2), "utf-8");
6603
6926
  return out;
6604
6927
  }
6605
6928
  function loadOrchestratorPrompt(cwd, sessionId, mode) {
6606
6929
  const projectPath = projectOrchestratorPromptPath(cwd);
6607
- if (existsSync17(projectPath)) {
6608
- return readFileSync16(projectPath, "utf-8");
6930
+ if (existsSync18(projectPath)) {
6931
+ return readFileSync17(projectPath, "utf-8");
6609
6932
  }
6610
6933
  const userPath = userOrchestratorPromptPath();
6611
- if (existsSync17(userPath)) {
6612
- return readFileSync16(userPath, "utf-8");
6934
+ if (existsSync18(userPath)) {
6935
+ return readFileSync17(userPath, "utf-8");
6613
6936
  }
6614
6937
  const basePath = resolve7(import.meta.dirname, "../templates/orchestrator-base.md");
6615
- const base = readFileSync16(basePath, "utf-8");
6938
+ const base = readFileSync17(basePath, "utf-8");
6616
6939
  const modes = discoverOrchestratorModes(cwd);
6617
6940
  const selected = modes.find((m) => m.name === mode) ?? modes.find((m) => m.name === "discovery");
6618
6941
  if (!selected) {
6619
6942
  throw new Error(`Unknown orchestrator mode '${mode}' and no fallback found. Available: ${modes.map((m) => m.name).join(", ")}`);
6620
6943
  }
6621
- const modeContent = readFileSync16(selected.filePath, "utf-8");
6944
+ const modeContent = readFileSync17(selected.filePath, "utf-8");
6622
6945
  const modeBody = extractAgentBody(modeContent);
6623
6946
  return base + "\n\n" + modeBody;
6624
6947
  }
@@ -6636,7 +6959,7 @@ function buildCompletionContent(session) {
6636
6959
  lines.push("");
6637
6960
  }
6638
6961
  const logsDirPath = logsDir(session.cwd, session.id);
6639
- if (existsSync17(logsDirPath)) {
6962
+ if (existsSync18(logsDirPath)) {
6640
6963
  const logFiles = readdirSync10(logsDirPath).filter((f) => f.startsWith("cycle-") && f.endsWith(".md")).sort();
6641
6964
  if (logFiles.length > 0) {
6642
6965
  lines.push("### Cycle Logs\n");
@@ -6661,7 +6984,7 @@ function buildCompletionContent(session) {
6661
6984
  while (j < entries.length && entries[j].idle && entries[j].mode === e.mode) j++;
6662
6985
  const runEntries = entries.slice(i, j);
6663
6986
  if (runEntries.length === 1) {
6664
- const content = readFileSync16(join16(logsDirPath, e.file), "utf-8").trim();
6987
+ const content = readFileSync17(join16(logsDirPath, e.file), "utf-8").trim();
6665
6988
  if (content) {
6666
6989
  lines.push(content);
6667
6990
  lines.push("");
@@ -6677,7 +7000,7 @@ function buildCompletionContent(session) {
6677
7000
  }
6678
7001
  i = j;
6679
7002
  } else {
6680
- const content = readFileSync16(join16(logsDirPath, e.file), "utf-8").trim();
7003
+ const content = readFileSync17(join16(logsDirPath, e.file), "utf-8").trim();
6681
7004
  if (content) {
6682
7005
  lines.push(content);
6683
7006
  lines.push("");
@@ -6688,7 +7011,7 @@ function buildCompletionContent(session) {
6688
7011
  }
6689
7012
  }
6690
7013
  const reportsDirPath = reportsDir(session.cwd, session.id);
6691
- if (existsSync17(reportsDirPath)) {
7014
+ if (existsSync18(reportsDirPath)) {
6692
7015
  const reportFiles = readdirSync10(reportsDirPath).filter((f) => f.endsWith(".md"));
6693
7016
  if (reportFiles.length > 0) {
6694
7017
  lines.push("### Detailed Reports\n");
@@ -6714,7 +7037,7 @@ ${session.context}
6714
7037
  }
6715
7038
  } else {
6716
7039
  let ctxFiles = [];
6717
- if (existsSync17(ctxDir)) {
7040
+ if (existsSync18(ctxDir)) {
6718
7041
  ctxFiles = readdirSync10(ctxDir).filter((f) => f !== "CLAUDE.md");
6719
7042
  }
6720
7043
  if (ctxFiles.length > 0) {
@@ -6756,10 +7079,10 @@ ${agentLines}
6756
7079
  }
6757
7080
  }
6758
7081
  const strategyFile = strategyPath(session.cwd, session.id);
6759
- const strategyRef = existsSync17(strategyFile) ? `@${relative3(session.cwd, strategyFile)}` : "(empty)";
6760
- const roadmapRef = existsSync17(roadmapFile) ? `@${relative3(session.cwd, roadmapFile)}` : "(empty)";
7082
+ const strategyRef = existsSync18(strategyFile) ? `@${relative3(session.cwd, strategyFile)}` : "(empty)";
7083
+ const roadmapRef = existsSync18(roadmapFile) ? `@${relative3(session.cwd, roadmapFile)}` : "(empty)";
6761
7084
  const digestFile = digestPath(session.cwd, session.id);
6762
- const digestRef = existsSync17(digestFile) ? `@${relative3(session.cwd, digestFile)}` : "(not yet created)";
7085
+ const digestRef = existsSync18(digestFile) ? `@${relative3(session.cwd, digestFile)}` : "(not yet created)";
6763
7086
  const repos = detectRepos(session.cwd);
6764
7087
  let repositoriesSection = "\n\n## Repositories\n";
6765
7088
  if (repos.length === 0) {
@@ -6786,7 +7109,7 @@ ${agentLines}
6786
7109
  }
6787
7110
  }
6788
7111
  const goalFile = goalPath(session.cwd, session.id);
6789
- const goalContent = existsSync17(goalFile) ? readFileSync16(goalFile, "utf-8").trim() : session.task;
7112
+ const goalContent = existsSync18(goalFile) ? readFileSync17(goalFile, "utf-8").trim() : session.task;
6790
7113
  const modeContent = modeContentBuilders[mode]?.(session) ?? "";
6791
7114
  return `## Goal
6792
7115
 
@@ -6813,7 +7136,7 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message, forceMode) {
6813
7136
  try {
6814
7137
  execSync6("which claude", { stdio: "pipe", env: EXEC_ENV });
6815
7138
  } catch {
6816
- throw new Error("Claude CLI not found on PATH. Run `sis admin doctor` to diagnose.");
7139
+ throw new Error("Claude CLI not found on PATH. Run `sis admin check doctor` to diagnose.");
6817
7140
  }
6818
7141
  const session = getSession(cwd, sessionId);
6819
7142
  const lastCycle = [...session.orchestratorCycles].reverse().find((c) => c.completedAt);
@@ -6823,9 +7146,9 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message, forceMode) {
6823
7146
  const agentPluginPath = resolve7(import.meta.dirname, "../templates/agent-plugin");
6824
7147
  const agentTypes = discoverAgentTypes(agentPluginPath, session.cwd).filter((t2) => t2.source === "bundled");
6825
7148
  const agentTypeLines = agentTypes.length > 0 ? agentTypes.map((t2) => {
6826
- const modelTag = t2.model ? ` (${t2.model})` : "";
7149
+ const tag = t2.model ? `(agent, ${t2.model})` : "(agent)";
6827
7150
  const desc = t2.description ? ` \u2014 ${t2.description}` : "";
6828
- return `- \`${t2.qualifiedName}\`${modelTag}${desc}`;
7151
+ return `- \`${t2.qualifiedName}\` ${tag}${desc}`;
6829
7152
  }).join("\n") : " (none)";
6830
7153
  const sesDir = sessionDir(cwd, sessionId);
6831
7154
  const substituteEnvVars = (text) => text.replace(/\$SISYPHUS_SESSION_DIR/g, sesDir).replace(/\$SISYPHUS_SESSION_ID/g, sessionId);
@@ -6835,7 +7158,9 @@ async function spawnOrchestrator(sessionId, cwd, windowId, message, forceMode) {
6835
7158
  return `- \`${m.name}\`${desc}`;
6836
7159
  }).join("\n");
6837
7160
  const substitutedPrompt = substituteEnvVars(
6838
- basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines).replace("{{ORCHESTRATOR_MODES}}", modeLines)
7161
+ injectHelp(
7162
+ basePrompt.replace("{{AGENT_TYPES}}", agentTypeLines).replace("{{ORCHESTRATOR_MODES}}", modeLines)
7163
+ )
6839
7164
  );
6840
7165
  const sessionEffort = session.effort != null ? session.effort : "high";
6841
7166
  const systemPrompt = renderEffortMarkers(substitutedPrompt, sessionEffort);
@@ -7003,8 +7328,10 @@ var init_orchestrator = __esm({
7003
7328
  "src/daemon/orchestrator.ts"() {
7004
7329
  "use strict";
7005
7330
  init_spawn_helpers();
7331
+ init_help_inject();
7006
7332
  init_paths();
7007
7333
  init_exec();
7334
+ init_digest_verbs();
7008
7335
  init_types();
7009
7336
  init_config();
7010
7337
  init_shell();
@@ -7029,7 +7356,7 @@ var init_orchestrator = __esm({
7029
7356
  });
7030
7357
 
7031
7358
  // src/daemon/status-dots.ts
7032
- import { readFileSync as readFileSync17 } from "fs";
7359
+ import { readFileSync as readFileSync18 } from "fs";
7033
7360
  function renderDots(dots) {
7034
7361
  const sorted = [...dots].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
7035
7362
  return sorted.map((d) => {
@@ -7040,7 +7367,7 @@ function renderDots(dots) {
7040
7367
  function readClaudeState(paneId) {
7041
7368
  const numericId = paneId.replace("%", "");
7042
7369
  try {
7043
- const content = readFileSync17(`${CLAUDE_STATE_DIR}/${numericId}`, "utf-8").trim();
7370
+ const content = readFileSync18(`${CLAUDE_STATE_DIR}/${numericId}`, "utf-8").trim();
7044
7371
  if (content === "idle" || content === "processing" || content === "stopped") {
7045
7372
  return content;
7046
7373
  }
@@ -7099,7 +7426,7 @@ function getDashboardWindowId(cwd) {
7099
7426
  dashboardWindowCache.set(cwd, { windowId, checkedAt: now });
7100
7427
  return windowId;
7101
7428
  }
7102
- function recomputeDots() {
7429
+ function recomputeDots(panesByWindow) {
7103
7430
  if (!getTrackedEntries) return;
7104
7431
  pruneCompleted();
7105
7432
  sisyphusPhases.clear();
@@ -7129,7 +7456,7 @@ function recomputeDots() {
7129
7456
  seenIds.add(sessionId);
7130
7457
  try {
7131
7458
  const session = getSession(cwd, sessionId);
7132
- const livePanes = listPanes(windowId);
7459
+ const livePanes = panesByWindow?.get(windowId) ?? listPanes(windowId);
7133
7460
  const livePaneIds = new Set(livePanes.map((p) => p.paneId));
7134
7461
  const phase = detectPhase(session, livePaneIds);
7135
7462
  dots.push({ phase, createdAt: session.createdAt });
@@ -7147,11 +7474,14 @@ function recomputeDots() {
7147
7474
  const dashboardWindowId = getDashboardWindowId(cwd);
7148
7475
  if (dashboardWindowId) {
7149
7476
  const rendered = dots.length > 0 ? " " + renderDots(dots) : "";
7150
- setWindowOption(dashboardWindowId, "@sisyphus_dots", rendered);
7477
+ if (lastDotsByWindow.get(dashboardWindowId) !== rendered) {
7478
+ setWindowOption(dashboardWindowId, "@sisyphus_dots", rendered);
7479
+ lastDotsByWindow.set(dashboardWindowId, rendered);
7480
+ }
7151
7481
  }
7152
7482
  }
7153
7483
  }
7154
- var CLAUDE_STATE_DIR, DOT_MAP, sisyphusPhases, getTrackedEntries, COMPLETED_TTL_MS, completedSessions, dashboardWindowCache, CACHE_TTL_MS;
7484
+ var CLAUDE_STATE_DIR, DOT_MAP, sisyphusPhases, getTrackedEntries, COMPLETED_TTL_MS, completedSessions, dashboardWindowCache, CACHE_TTL_MS, lastDotsByWindow;
7155
7485
  var init_status_dots = __esm({
7156
7486
  "src/daemon/status-dots.ts"() {
7157
7487
  "use strict";
@@ -7179,6 +7509,7 @@ var init_status_dots = __esm({
7179
7509
  completedSessions = /* @__PURE__ */ new Map();
7180
7510
  dashboardWindowCache = /* @__PURE__ */ new Map();
7181
7511
  CACHE_TTL_MS = 3e4;
7512
+ lastDotsByWindow = /* @__PURE__ */ new Map();
7182
7513
  }
7183
7514
  });
7184
7515
 
@@ -7220,9 +7551,9 @@ var init_manifest = __esm({
7220
7551
  });
7221
7552
 
7222
7553
  // src/shared/session-export.ts
7223
- import { execFile as execFile2 } from "child_process";
7224
- import { promisify } from "util";
7225
- import { existsSync as existsSync18, readFileSync as readFileSync18, mkdirSync as mkdirSync10, symlinkSync, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "fs";
7554
+ import { execFile as execFile3 } from "child_process";
7555
+ import { promisify as promisify2 } from "util";
7556
+ import { existsSync as existsSync19, readFileSync as readFileSync19, mkdirSync as mkdirSync10, symlinkSync, rmSync as rmSync5, writeFileSync as writeFileSync14 } from "fs";
7226
7557
  import { homedir as homedir6 } from "os";
7227
7558
  import { join as join17 } from "path";
7228
7559
  function sanitizeName(name) {
@@ -7234,7 +7565,7 @@ function buildOutputPath(label, dir) {
7234
7565
  const base = `sisyphus-${label}-${date}`;
7235
7566
  let candidate = join17(dir, `${base}.zip`);
7236
7567
  let counter = 1;
7237
- while (existsSync18(candidate)) {
7568
+ while (existsSync19(candidate)) {
7238
7569
  counter++;
7239
7570
  candidate = join17(dir, `${base}-${counter}.zip`);
7240
7571
  }
@@ -7297,16 +7628,16 @@ async function exportSessionToZip(sessionId, cwd, options) {
7297
7628
  const reveal = options?.reveal ?? true;
7298
7629
  const sessDir = sessionDir(cwd, sessionId);
7299
7630
  const histDir = historySessionDir(sessionId);
7300
- const sessExists = existsSync18(sessDir);
7301
- const histExists = existsSync18(histDir);
7631
+ const sessExists = existsSync19(sessDir);
7632
+ const histExists = existsSync19(histDir);
7302
7633
  if (!sessExists && !histExists) {
7303
7634
  throw new Error(`No data found for session ${sessionId}`);
7304
7635
  }
7305
7636
  let label = sessionId.slice(0, 8);
7306
7637
  const stPath = statePath(cwd, sessionId);
7307
- if (existsSync18(stPath)) {
7638
+ if (existsSync19(stPath)) {
7308
7639
  try {
7309
- const state = JSON.parse(readFileSync18(stPath, "utf-8"));
7640
+ const state = JSON.parse(readFileSync19(stPath, "utf-8"));
7310
7641
  if (state.name) {
7311
7642
  label = sanitizeName(state.name);
7312
7643
  }
@@ -7326,24 +7657,24 @@ async function exportSessionToZip(sessionId, cwd, options) {
7326
7657
  symlinkSync(histDir, join17(tmpDir, "history"));
7327
7658
  }
7328
7659
  const parts = ["CLAUDE.md", sessExists ? "session/" : "", histExists ? "history/" : ""].filter(Boolean);
7329
- await execFileAsync("zip", ["-rq", outputPath, ...parts], { cwd: tmpDir });
7660
+ await execFileAsync2("zip", ["-rq", outputPath, ...parts], { cwd: tmpDir });
7330
7661
  } finally {
7331
7662
  rmSync5(tmpDir, { recursive: true, force: true });
7332
7663
  }
7333
7664
  if (reveal) {
7334
7665
  try {
7335
- await execFileAsync("open", ["-R", outputPath]);
7666
+ await execFileAsync2("open", ["-R", outputPath]);
7336
7667
  } catch {
7337
7668
  }
7338
7669
  }
7339
7670
  return outputPath;
7340
7671
  }
7341
- var execFileAsync;
7672
+ var execFileAsync2;
7342
7673
  var init_session_export = __esm({
7343
7674
  "src/shared/session-export.ts"() {
7344
7675
  "use strict";
7345
7676
  init_paths();
7346
- execFileAsync = promisify(execFile2);
7677
+ execFileAsync2 = promisify2(execFile3);
7347
7678
  }
7348
7679
  });
7349
7680
 
@@ -7427,12 +7758,12 @@ var init_uploader = __esm({
7427
7758
  });
7428
7759
 
7429
7760
  // src/shared/version.ts
7430
- import { readFileSync as readFileSync19 } from "fs";
7761
+ import { readFileSync as readFileSync20 } from "fs";
7431
7762
  import { resolve as resolve8 } from "path";
7432
7763
  function readSisyphusVersion() {
7433
7764
  for (const rel of ["../package.json", "../../package.json"]) {
7434
7765
  try {
7435
- const raw = readFileSync19(resolve8(import.meta.dirname, rel), "utf-8");
7766
+ const raw = readFileSync20(resolve8(import.meta.dirname, rel), "utf-8");
7436
7767
  const pkg = JSON.parse(raw);
7437
7768
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
7438
7769
  } catch {
@@ -7470,14 +7801,19 @@ function formatDuration2(startOrMs, endIso) {
7470
7801
  if (minutes > 0) return `${minutes}m${seconds}s`;
7471
7802
  return `${seconds}s`;
7472
7803
  }
7804
+ function colorEnabled() {
7805
+ if (process.env["FORCE_COLOR"] === "1") return true;
7806
+ if (process.env["NO_COLOR"] !== void 0) return false;
7807
+ if (process.env["TERM"] === "dumb") return false;
7808
+ return process.stdout.isTTY === true;
7809
+ }
7473
7810
  function wrap(open, close = "\x1B[0m") {
7474
- return (s) => COLOR_ENABLED ? `${open}${s}${close}` : s;
7811
+ return (s) => colorEnabled() ? `${open}${s}${close}` : s;
7475
7812
  }
7476
- var COLOR_ENABLED, bold, dim, red, green, yellow, cyan, gray, magenta, white;
7813
+ var bold, dim, red, green, yellow, cyan, gray, magenta, white;
7477
7814
  var init_format = __esm({
7478
7815
  "src/shared/format.ts"() {
7479
7816
  "use strict";
7480
- COLOR_ENABLED = process.env["FORCE_COLOR"] === "1" || process.stdout.isTTY === true && process.env["NO_COLOR"] === void 0 && process.env["TERM"] !== "dumb";
7481
7817
  bold = wrap("\x1B[1m");
7482
7818
  dim = wrap("\x1B[2m");
7483
7819
  red = wrap("\x1B[31m");
@@ -7491,7 +7827,7 @@ var init_format = __esm({
7491
7827
  });
7492
7828
 
7493
7829
  // src/cli/deploy/creds.ts
7494
- import { chmodSync, existsSync as existsSync19, mkdirSync as mkdirSync11, readFileSync as readFileSync20 } from "fs";
7830
+ import { chmodSync, existsSync as existsSync20, mkdirSync as mkdirSync11, readFileSync as readFileSync21 } from "fs";
7495
7831
  import { createInterface } from "readline";
7496
7832
  function isValidProvider(value) {
7497
7833
  return PROVIDERS.includes(value);
@@ -7532,12 +7868,12 @@ var init_pricing = __esm({
7532
7868
  });
7533
7869
 
7534
7870
  // src/cli/deploy/runtime.ts
7535
- import { existsSync as existsSync20, readFileSync as readFileSync21, unlinkSync as unlinkSync3 } from "fs";
7871
+ import { existsSync as existsSync21, readFileSync as readFileSync22, unlinkSync as unlinkSync3 } from "fs";
7536
7872
  function readRuntimeState(provider) {
7537
7873
  const path = deployRuntimePath(provider);
7538
- if (!existsSync20(path)) return null;
7874
+ if (!existsSync21(path)) return null;
7539
7875
  try {
7540
- return JSON.parse(readFileSync21(path, "utf-8"));
7876
+ return JSON.parse(readFileSync22(path, "utf-8"));
7541
7877
  } catch {
7542
7878
  return null;
7543
7879
  }
@@ -7559,15 +7895,15 @@ var init_tailnet = __esm({
7559
7895
  });
7560
7896
 
7561
7897
  // src/cli/deploy/templates.ts
7562
- import { existsSync as existsSync21 } from "fs";
7898
+ import { existsSync as existsSync22 } from "fs";
7563
7899
  import { dirname as dirname6, resolve as resolve9 } from "path";
7564
7900
  import { fileURLToPath } from "url";
7565
7901
  function deployRoot() {
7566
7902
  const here = dirname6(fileURLToPath(import.meta.url));
7567
7903
  const bundled = resolve9(here, "..", "deploy");
7568
- if (existsSync21(bundled)) return bundled;
7904
+ if (existsSync22(bundled)) return bundled;
7569
7905
  const sourceRoot = resolve9(here, "..", "..", "..", "deploy");
7570
- if (existsSync21(sourceRoot)) return sourceRoot;
7906
+ if (existsSync22(sourceRoot)) return sourceRoot;
7571
7907
  throw new Error(
7572
7908
  `Could not locate deploy/ templates. Looked at:
7573
7909
  ${bundled}
@@ -7596,7 +7932,7 @@ var init_tailscale = __esm({
7596
7932
 
7597
7933
  // src/cli/deploy/runner.ts
7598
7934
  import { spawn as spawn2, spawnSync } from "child_process";
7599
- import { copyFileSync as copyFileSync5, existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync22 } from "fs";
7935
+ import { copyFileSync as copyFileSync5, existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync23 } from "fs";
7600
7936
  function readOutputs(provider) {
7601
7937
  const result = spawnSync("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
7602
7938
  cwd: providerModuleDir(provider),
@@ -7622,7 +7958,7 @@ function readOutputs(provider) {
7622
7958
  }
7623
7959
  }
7624
7960
  function isProvisioned(provider) {
7625
- if (!existsSync22(deployStatePath(provider))) return false;
7961
+ if (!existsSync23(deployStatePath(provider))) return false;
7626
7962
  return readOutputs(provider) !== null;
7627
7963
  }
7628
7964
  function effectiveSshTarget(provider) {
@@ -7741,7 +8077,7 @@ var init_grove = __esm({
7741
8077
 
7742
8078
  // src/cli/cloud/repo.ts
7743
8079
  import { spawnSync as spawnSync3 } from "child_process";
7744
- import { existsSync as existsSync23 } from "fs";
8080
+ import { existsSync as existsSync24 } from "fs";
7745
8081
  import { basename as basename5, join as join18 } from "path";
7746
8082
  function captureGit(args, cwd) {
7747
8083
  const result = spawnSync3("git", args, {
@@ -7782,10 +8118,10 @@ function buildRsyncArgs(localDir, remoteTarget) {
7782
8118
  ];
7783
8119
  }
7784
8120
  function detectPackageManager(toplevel) {
7785
- if (existsSync23(join18(toplevel, "pnpm-lock.yaml"))) return "pnpm";
7786
- if (existsSync23(join18(toplevel, "bun.lockb"))) return "bun";
7787
- if (existsSync23(join18(toplevel, "yarn.lock"))) return "yarn";
7788
- if (existsSync23(join18(toplevel, "package-lock.json"))) return "npm";
8121
+ if (existsSync24(join18(toplevel, "pnpm-lock.yaml"))) return "pnpm";
8122
+ if (existsSync24(join18(toplevel, "bun.lockb"))) return "bun";
8123
+ if (existsSync24(join18(toplevel, "yarn.lock"))) return "yarn";
8124
+ if (existsSync24(join18(toplevel, "package-lock.json"))) return "npm";
7789
8125
  return null;
7790
8126
  }
7791
8127
  function packageManagerInstallCmd(pm) {
@@ -7952,7 +8288,7 @@ async function cloudInstall(provider, repo, cwd) {
7952
8288
  }
7953
8289
  async function cloudSession(provider, repo) {
7954
8290
  const remoteDir = boxRepoPath(repo);
7955
- const cmd = `sis admin home-init ${shellQuote(repo)} ${shellQuoteHomePath(remoteDir)}`;
8291
+ const cmd = `sis diagnostic home-init ${shellQuote(repo)} ${shellQuoteHomePath(remoteDir)}`;
7956
8292
  console.log(`\u2192 initializing tmux home session "${repo}" on box...`);
7957
8293
  const result = runOnBox(provider, cmd);
7958
8294
  if (result.exitCode !== 0) {
@@ -7986,7 +8322,7 @@ __export(cloud_handoff_exports, {
7986
8322
  triggerForceHandoff: () => triggerForceHandoff
7987
8323
  });
7988
8324
  import { spawn as spawn5 } from "child_process";
7989
- import { existsSync as existsSync24 } from "fs";
8325
+ import { existsSync as existsSync25 } from "fs";
7990
8326
  import { join as join19 } from "path";
7991
8327
  function runRsync2(args) {
7992
8328
  return new Promise((resolve13) => {
@@ -8026,7 +8362,7 @@ async function syncSessionState(cwd, sessionId, repo, target, provider) {
8026
8362
  const candidates = ["config.json", "orchestrator.md", "orchestrator-settings.json"];
8027
8363
  for (const name of candidates) {
8028
8364
  const localPath = join19(localProject, name);
8029
- if (!existsSync24(localPath)) continue;
8365
+ if (!existsSync25(localPath)) continue;
8030
8366
  const remotePath = `${boxRepoPath(repo)}/.sisyphus/${name}`;
8031
8367
  const args = [
8032
8368
  "-avz",
@@ -8087,7 +8423,7 @@ async function pushToCloud(sessionId, cwd) {
8087
8423
  }
8088
8424
  const message = handoff.message;
8089
8425
  const remoteRepoDir = boxRepoPath(repo);
8090
- const baseCmd = message ? `sis session resume ${shellQuote(sessionId)} ${shellQuote(message)}` : `sis session resume ${shellQuote(sessionId)}`;
8426
+ const baseCmd = message ? `sis session lifecycle resume ${shellQuote(sessionId)} ${shellQuote(message)}` : `sis session lifecycle resume ${shellQuote(sessionId)}`;
8091
8427
  const resumeCmd = `cd ${shellQuoteHomePath(remoteRepoDir)} && ${baseCmd}`;
8092
8428
  console.log(`[sisyphus] handoff: ssh sis resume`);
8093
8429
  const resumeResult = runOnBox(provider, resumeCmd);
@@ -8193,14 +8529,14 @@ var init_cloud_handoff = __esm({
8193
8529
 
8194
8530
  // src/daemon/session-manager.ts
8195
8531
  import { v4 as uuidv4 } from "uuid";
8196
- import { existsSync as existsSync25, readFileSync as readFileSync23, readdirSync as readdirSync11, rmSync as rmSync6 } from "fs";
8532
+ import { existsSync as existsSync26, readFileSync as readFileSync24, readdirSync as readdirSync11, rmSync as rmSync6 } from "fs";
8197
8533
  function truncate(s, max) {
8198
8534
  return s.length <= max ? s : s.slice(0, max) + "...";
8199
8535
  }
8200
8536
  function readGoal(cwd, sessionId, fallback) {
8201
8537
  try {
8202
8538
  const p = goalPath(cwd, sessionId);
8203
- if (existsSync25(p)) return readFileSync23(p, "utf-8").trim();
8539
+ if (existsSync26(p)) return readFileSync24(p, "utf-8").trim();
8204
8540
  } catch {
8205
8541
  }
8206
8542
  return fallback;
@@ -8369,7 +8705,7 @@ async function startSession(task, cwd, context, name, effort) {
8369
8705
  async function cloneSession(sourceId, cwd, goal, context, name, strategy) {
8370
8706
  const sourceSession = getSession(cwd, sourceId);
8371
8707
  if (sourceSession.status === "completed") {
8372
- throw new Error("Cannot clone completed session. Use `sis session continue` to resume it first.");
8708
+ throw new Error("Cannot clone completed session. Use `sis session lifecycle continue` to resume it first.");
8373
8709
  }
8374
8710
  const cloneId = uuidv4();
8375
8711
  if (name && !NAME_PATTERN.test(name)) {
@@ -8439,7 +8775,7 @@ It is the other session's responsibility. You do not need to monitor it.
8439
8775
  function pruneOldSessions(cwd) {
8440
8776
  try {
8441
8777
  const dir = sessionsDir(cwd);
8442
- if (!existsSync25(dir)) return;
8778
+ if (!existsSync26(dir)) return;
8443
8779
  const entries = readdirSync11(dir, { withFileTypes: true });
8444
8780
  const candidates = [];
8445
8781
  for (const entry of entries) {
@@ -8484,7 +8820,7 @@ async function reconnectSession(sessionId, cwd) {
8484
8820
  const session = getSession(cwd, sessionId);
8485
8821
  const tmuxName = session.tmuxSessionName ?? tmuxSessionName(cwd, session.name ?? sessionId.slice(0, 8));
8486
8822
  if (!sessionNameTaken(tmuxName)) {
8487
- throw new Error(`No tmux session named "${tmuxName}" exists. Use \`sis session resume\` to create a new one.`);
8823
+ throw new Error(`No tmux session named "${tmuxName}" exists. Use \`sis session lifecycle resume\` to create a new one.`);
8488
8824
  }
8489
8825
  const tmuxSessId = resolveSessionId(tmuxName);
8490
8826
  if (!tmuxSessId) {
@@ -8575,9 +8911,18 @@ async function resumeSession(sessionId, cwd, message) {
8575
8911
  function getSessionStatus(cwd, sessionId) {
8576
8912
  return getSession(cwd, sessionId);
8577
8913
  }
8914
+ function countSessions(cwd) {
8915
+ const dir = sessionsDir(cwd);
8916
+ if (!existsSync26(dir)) return 0;
8917
+ let n = 0;
8918
+ for (const entry of readdirSync11(dir, { withFileTypes: true })) {
8919
+ if (entry.isDirectory()) n++;
8920
+ }
8921
+ return n;
8922
+ }
8578
8923
  function listSessions(cwd) {
8579
8924
  const dir = sessionsDir(cwd);
8580
- if (!existsSync25(dir)) return [];
8925
+ if (!existsSync26(dir)) return [];
8581
8926
  const entries = readdirSync11(dir, { withFileTypes: true });
8582
8927
  const sessions = [];
8583
8928
  for (const entry of entries) {
@@ -8650,8 +8995,8 @@ function onAllAgentsDone2(sessionId, cwd, windowId) {
8650
8995
  Agents: ${truncate(spawnedThisCycle, 200)}`;
8651
8996
  try {
8652
8997
  const logPath2 = cycleLogPath(cwd, sessionId, cycleNumber);
8653
- if (existsSync25(logPath2)) {
8654
- const log = readFileSync23(logPath2, "utf-8").trim();
8998
+ if (existsSync26(logPath2)) {
8999
+ const log = readFileSync24(logPath2, "utf-8").trim();
8655
9000
  if (log) cycleCtx += `
8656
9001
  Cycle log: ${truncate(log, 200)}`;
8657
9002
  }
@@ -8676,6 +9021,20 @@ Cycle log: ${truncate(log, 200)}`;
8676
9021
  await performHandoff2(sessionId, cwd);
8677
9022
  return;
8678
9023
  }
9024
+ const sessionForOrphanResolve = getSession(cwd, sessionId);
9025
+ const supersededAgentIds = sessionForOrphanResolve.agents.filter((a) => a.status !== "running").map((a) => a.id);
9026
+ await Promise.all([
9027
+ clearSessionOrphan(cwd, sessionId),
9028
+ resolveOrchestratorOrphanAsks(cwd, sessionId, "respawn"),
9029
+ ...supersededAgentIds.map(
9030
+ (agentId) => resolveAgentOrphanAsks(cwd, sessionId, agentId, "superseded").catch((err) => {
9031
+ console.warn(
9032
+ `[sisyphus] resolveAgentOrphanAsks (superseded) failed for ${agentId}:`,
9033
+ err instanceof Error ? err.message : err
9034
+ );
9035
+ })
9036
+ )
9037
+ ]);
8679
9038
  let activeWindowId = windowId;
8680
9039
  const tmuxName = freshSession.tmuxSessionName;
8681
9040
  const existingTmuxSessId = freshSession.tmuxSessionId;
@@ -8797,7 +9156,7 @@ async function handleSubmit(cwd, sessionId, agentId, report, windowId) {
8797
9156
  function formatPendingAskError(verb, askedBy, open) {
8798
9157
  const lines = open.map((a) => ` - ${a.askId} (${a.status})${a.title ? ": " + a.title : ""}`);
8799
9158
  const who = askedBy === ORCHESTRATOR_ASKED_BY ? "orchestrator" : `agent ${askedBy}`;
8800
- const recovery = verb === "yield" ? `Resolve before yielding: \`sis ask poll <askId>\` blocks until the user answers, then process the response and yield with a continuation prompt that names the answered branch.` : `Resolve before submitting: \`sis ask poll <askId>\` blocks until the user answers, parse the response, then call \`sis agent submit\` with your final report.`;
9159
+ const recovery = verb === "yield" ? `Resolve before yielding: \`sis ask state poll <askId>\` blocks until the user answers, then process the response and yield with a continuation prompt that names the answered branch.` : `Resolve before submitting: \`sis ask state poll <askId>\` blocks until the user answers, parse the response, then call \`sis agent submit\` with your final report.`;
8801
9160
  return `Cannot ${verb}: ${who} owns ${open.length} open deck${open.length === 1 ? "" : "s"}:
8802
9161
  ${lines.join("\n")}
8803
9162
 
@@ -9231,8 +9590,8 @@ function digestTranscript(opts) {
9231
9590
  const role = entry.message?.role !== void 0 ? entry.message.role : entry.type;
9232
9591
  const text = formatContent(entry.message?.content);
9233
9592
  if (!text) continue;
9234
- const ts2 = entry.timestamp !== void 0 ? entry.timestamp : "";
9235
- kept.push({ ts: ts2, role, text });
9593
+ const ts = entry.timestamp !== void 0 ? entry.timestamp : "";
9594
+ kept.push({ ts, role, text });
9236
9595
  }
9237
9596
  let totalBytes = 0;
9238
9597
  const selected = [];
@@ -9279,11 +9638,10 @@ var init_transcript_digest = __esm({
9279
9638
  });
9280
9639
 
9281
9640
  // src/daemon/ask-visual.ts
9282
- import { spawn as spawn6 } from "child_process";
9283
9641
  import {
9284
9642
  closeSync as closeSync2,
9285
9643
  constants,
9286
- existsSync as existsSync26,
9644
+ existsSync as existsSync27,
9287
9645
  fstatSync as fstatSync2,
9288
9646
  lstatSync,
9289
9647
  openSync as openSync2,
@@ -9295,16 +9653,17 @@ import { resolve as resolve10, dirname as dirname7, isAbsolute, sep } from "path
9295
9653
  import { fileURLToPath as fileURLToPath2 } from "url";
9296
9654
  import { z as z3 } from "zod";
9297
9655
  import { tool } from "@r-cli/sdk";
9656
+ import { renderMarkdown, checkMarkdown } from "@crouton-kit/humanloop";
9298
9657
  async function generateVisualForQuestion(opts) {
9299
9658
  const meta = readMeta(opts.cwd, opts.sessionId, opts.askId);
9300
9659
  if (!meta) return { ok: false, error: `ask not found: ${opts.askId}` };
9301
9660
  const decisions = readDecisions(opts.cwd, opts.sessionId, opts.askId);
9302
- if (!decisions) return { ok: false, error: "decisions.json missing" };
9661
+ if (!decisions) return { ok: false, error: "deck.json missing" };
9303
9662
  const question = decisions.interactions.find((q) => q.id === opts.qid);
9304
9663
  if (!question) return { ok: false, error: `qid ${opts.qid} not found in decisions` };
9305
9664
  const mdPath = askVisualMarkdownPath(opts.cwd, opts.sessionId, opts.askId, opts.qid);
9306
9665
  const ansiPath = askVisualAnsiPath(opts.cwd, opts.sessionId, opts.askId, opts.qid);
9307
- if (!opts.force && existsSync26(mdPath) && existsSync26(ansiPath)) {
9666
+ if (!opts.force && existsSync27(mdPath) && existsSync27(ansiPath)) {
9308
9667
  return { ok: true, markdownPath: mdPath, ansiPath, turns: 0 };
9309
9668
  }
9310
9669
  if (opts.force) {
@@ -9333,7 +9692,7 @@ async function generateVisualForQuestion(opts) {
9333
9692
  );
9334
9693
  const attachVisualTool = tool(
9335
9694
  "attach_visual",
9336
- "Submit final termrender markdown for this question. Validated via `termrender --check` and rendered to ANSI.",
9695
+ "Submit final directive-flavored markdown for this question. Validated and rendered to ANSI via the humanloop SDK (no subprocess).",
9337
9696
  { content: z3.string().min(1) },
9338
9697
  async (args) => {
9339
9698
  const r = await attachVisualHandler({ content: args.content, mdPath, ansiPath, cols: opts.cols });
@@ -9378,7 +9737,7 @@ function buildUserPrompt(q, askedBy, ctx) {
9378
9737
  }
9379
9738
  function readSystemPrompt() {
9380
9739
  if (cachedSystemPrompt !== void 0) return cachedSystemPrompt;
9381
- const found = SYSTEM_PROMPT_CANDIDATES.find((p) => existsSync26(p));
9740
+ const found = SYSTEM_PROMPT_CANDIDATES.find((p) => existsSync27(p));
9382
9741
  if (found === void 0) {
9383
9742
  throw new Error(
9384
9743
  `termrender-haiku-system.md not found in any candidate location: ${SYSTEM_PROMPT_CANDIDATES.join(", ")}`
@@ -9447,48 +9806,20 @@ function readFileHandler(realSessionCwd, requestedPath) {
9447
9806
  function errorResult(msg) {
9448
9807
  return { content: [{ type: "text", text: msg }], isError: true };
9449
9808
  }
9450
- function spawnAsync(cmd, args, input, timeoutMs) {
9451
- return new Promise((resolve13) => {
9452
- const chunks = [];
9453
- const errChunks = [];
9454
- const proc = spawn6(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: execEnv() });
9455
- const timer = setTimeout(() => {
9456
- proc.kill();
9457
- resolve13({ stdout: "", stderr: "", status: null, error: new Error(`timed out after ${timeoutMs}ms`) });
9458
- }, timeoutMs);
9459
- proc.stdout.on("data", (d) => chunks.push(d));
9460
- proc.stderr.on("data", (d) => errChunks.push(d));
9461
- proc.on("error", (err) => {
9462
- clearTimeout(timer);
9463
- resolve13({ stdout: "", stderr: "", status: null, error: err });
9464
- });
9465
- proc.on("close", (code) => {
9466
- clearTimeout(timer);
9467
- resolve13({ stdout: Buffer.concat(chunks).toString("utf-8"), stderr: Buffer.concat(errChunks).toString("utf-8"), status: code });
9468
- });
9469
- proc.stdin.end(input, "utf-8");
9470
- });
9471
- }
9472
9809
  async function attachVisualHandler(args) {
9473
- const check = await spawnAsync("termrender", ["--check"], args.content, 5e3);
9474
- if (check.error) {
9475
- const msg = `termrender invocation failed: ${check.error.message}`;
9810
+ const check = checkMarkdown(args.content);
9811
+ if (!check.ok) {
9812
+ const msg = `directive check rejected the content: ${check.error}`;
9476
9813
  return { ok: false, error: msg, toolResult: errorResult(msg) };
9477
9814
  }
9478
- if (check.status !== 0) {
9479
- const msg = `termrender --check rejected the content: ${check.stderr.trim()}`;
9480
- return { ok: false, error: msg, toolResult: errorResult(msg) };
9481
- }
9482
- const render = await spawnAsync("termrender", ["-w", String(args.cols)], args.content, 8e3);
9483
- if (render.error) {
9484
- const msg = `termrender render failed: ${render.error.message}`;
9485
- return { ok: false, error: msg, toolResult: errorResult(msg) };
9486
- }
9487
- if (render.status !== 0) {
9488
- const msg = `termrender render failed: ${render.stderr.trim()}`;
9815
+ let ansi;
9816
+ try {
9817
+ const r = { lines: renderMarkdown(args.content, args.cols) };
9818
+ ansi = r.lines.join("\n");
9819
+ } catch (e) {
9820
+ const msg = `render failed: ${e.message}`;
9489
9821
  return { ok: false, error: msg, toolResult: errorResult(msg) };
9490
9822
  }
9491
- const ansi = render.stdout;
9492
9823
  const ansiBytes = Buffer.byteLength(ansi, "utf-8");
9493
9824
  if (ansiBytes > ANSI_CAP) {
9494
9825
  const msg = `rendered ANSI exceeds ${ANSI_CAP} byte cap (got ${ansiBytes})`;
@@ -9534,7 +9865,6 @@ var READ_FILE_CAP, ANSI_CAP, SYSTEM_PROMPT_CANDIDATES, cachedSystemPrompt;
9534
9865
  var init_ask_visual = __esm({
9535
9866
  "src/daemon/ask-visual.ts"() {
9536
9867
  "use strict";
9537
- init_env();
9538
9868
  init_haiku();
9539
9869
  init_transcript_digest();
9540
9870
  init_ask_store();
@@ -9554,8 +9884,9 @@ var init_ask_visual = __esm({
9554
9884
 
9555
9885
  // src/daemon/server.ts
9556
9886
  import { createServer } from "net";
9557
- import { unlinkSync as unlinkSync4, existsSync as existsSync27, writeFileSync as writeFileSync15, readFileSync as readFileSync25, mkdirSync as mkdirSync13, readdirSync as readdirSync12, rmSync as rmSync8, chmodSync as chmodSync2 } from "fs";
9558
- import { join as join21 } from "path";
9887
+ 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";
9888
+ import { join as join21, basename as basename6, dirname as dirname8 } from "path";
9889
+ import { scanInbox } from "@crouton-kit/humanloop";
9559
9890
  function setCompositor(c) {
9560
9891
  compositor = c;
9561
9892
  }
@@ -9573,9 +9904,9 @@ function persistSessionRegistry() {
9573
9904
  }
9574
9905
  function loadSessionRegistry() {
9575
9906
  const p = registryPath();
9576
- if (!existsSync27(p)) return {};
9907
+ if (!existsSync28(p)) return {};
9577
9908
  try {
9578
- return JSON.parse(readFileSync25(p, "utf-8"));
9909
+ return JSON.parse(readFileSync26(p, "utf-8"));
9579
9910
  } catch (err) {
9580
9911
  console.warn("[sisyphus] Failed to parse session registry:", err instanceof Error ? err.message : err);
9581
9912
  return {};
@@ -9605,7 +9936,14 @@ function registerSessionTmux(sessionId, tmuxSession, windowId, tmuxSessionId) {
9605
9936
  }
9606
9937
  }
9607
9938
  function unknownSessionError(sessionId) {
9608
- return { ok: false, error: `Unknown session: ${sessionId}. Run \`sis list --all\` to see available sessions.` };
9939
+ return {
9940
+ ok: false,
9941
+ error: errNotFound("unknown_session", {
9942
+ message: `Unknown session: ${sessionId}. Run \`sis session inspect list --all\` to see available sessions.`,
9943
+ received: sessionId,
9944
+ next: "sis session inspect list --all"
9945
+ })
9946
+ };
9609
9947
  }
9610
9948
  function collectAllSessionIds() {
9611
9949
  const idToCwd = /* @__PURE__ */ new Map();
@@ -9622,7 +9960,7 @@ function collectAllSessionIds() {
9622
9960
  scannedCwds.add(cwd);
9623
9961
  try {
9624
9962
  const dir = sessionsDir(cwd);
9625
- if (!existsSync27(dir)) continue;
9963
+ if (!existsSync28(dir)) continue;
9626
9964
  for (const entry of readdirSync12(dir, { withFileTypes: true })) {
9627
9965
  if (entry.isDirectory() && !idToCwd.has(entry.name)) {
9628
9966
  idToCwd.set(entry.name, cwd);
@@ -9647,9 +9985,16 @@ function resolvePartialSessionId(partial) {
9647
9985
  return { id };
9648
9986
  }
9649
9987
  if (matches.length > 1) {
9650
- const list = matches.map((id) => ` ${id}`).join("\n");
9651
- return { error: { ok: false, error: `Ambiguous session prefix "${partial}" matches ${matches.length} sessions:
9652
- ${list}` } };
9988
+ return {
9989
+ error: {
9990
+ ok: false,
9991
+ error: errAmbiguous("ambiguous_session", {
9992
+ message: `Ambiguous session prefix "${partial}" matches ${matches.length} sessions`,
9993
+ received: partial,
9994
+ candidates: matches
9995
+ })
9996
+ }
9997
+ };
9653
9998
  }
9654
9999
  return { id: partial };
9655
10000
  }
@@ -9664,7 +10009,13 @@ async function handleRequest(req) {
9664
10009
  try {
9665
10010
  if ("sessionId" in req && req.sessionId) {
9666
10011
  if (!validateSessionId(req.sessionId)) {
9667
- return { ok: false, error: `Invalid session ID: must contain only alphanumeric characters, hyphens, and underscores` };
10012
+ return {
10013
+ ok: false,
10014
+ error: errUsage("invalid_session_id", {
10015
+ message: "Invalid session ID: must contain only alphanumeric characters, hyphens, and underscores",
10016
+ received: req.sessionId
10017
+ })
10018
+ };
9668
10019
  }
9669
10020
  const resolved = resolvePartialSessionId(req.sessionId);
9670
10021
  if ("error" in resolved) return resolved.error;
@@ -9709,7 +10060,13 @@ async function handleRequest(req) {
9709
10060
  const tracking = sessionTrackingMap.get(req.sessionId);
9710
10061
  if (!tracking) return unknownSessionError(req.sessionId);
9711
10062
  if (req.repo && req.repo !== "." && !validateRepoName(req.repo)) {
9712
- return { ok: false, error: 'Invalid repo name: must be a simple directory name without path separators or ".."' };
10063
+ return {
10064
+ ok: false,
10065
+ error: errUsage("invalid_repo_name", {
10066
+ message: 'Invalid repo name: must be a simple directory name without path separators or ".."',
10067
+ received: req.repo
10068
+ })
10069
+ };
9713
10070
  }
9714
10071
  const result = await handleSpawn(req.sessionId, tracking.cwd, req.agentType, req.name, req.instruction, req.repo);
9715
10072
  return { ok: true, data: { agentId: result.agentId } };
@@ -9717,7 +10074,13 @@ async function handleRequest(req) {
9717
10074
  case "submit": {
9718
10075
  const tracking = sessionTrackingMap.get(req.sessionId);
9719
10076
  if (!tracking) return unknownSessionError(req.sessionId);
9720
- if (!tracking.windowId) return { ok: false, error: `No tmux window found for session: ${req.sessionId}` };
10077
+ if (!tracking.windowId) return {
10078
+ ok: false,
10079
+ error: errNotFound("no_tmux_window", {
10080
+ message: `No tmux window found for session: ${req.sessionId}`,
10081
+ received: req.sessionId
10082
+ })
10083
+ };
9721
10084
  await handleSubmit(tracking.cwd, req.sessionId, req.agentId, req.report, tracking.windowId);
9722
10085
  return { ok: true };
9723
10086
  }
@@ -9737,7 +10100,13 @@ async function handleRequest(req) {
9737
10100
  const tracking = sessionTrackingMap.get(req.sessionId);
9738
10101
  if (!tracking) return unknownSessionError(req.sessionId);
9739
10102
  const result = await handleAwait(tracking.cwd, req.sessionId, req.agentId);
9740
- if (!result) return { ok: false, error: `Unknown agent: ${req.agentId} in session ${req.sessionId}` };
10103
+ if (!result) return {
10104
+ ok: false,
10105
+ error: errNotFound("unknown_agent", {
10106
+ message: `Unknown agent: ${req.agentId} in session ${req.sessionId}`,
10107
+ received: req.agentId
10108
+ })
10109
+ };
9741
10110
  return {
9742
10111
  ok: true,
9743
10112
  data: {
@@ -9811,7 +10180,7 @@ async function handleRequest(req) {
9811
10180
  for (const tracking of sessionTrackingMap.values()) {
9812
10181
  if (seenCwds.has(tracking.cwd)) continue;
9813
10182
  seenCwds.add(tracking.cwd);
9814
- totalCount += listSessions(tracking.cwd).length;
10183
+ totalCount += countSessions(tracking.cwd);
9815
10184
  }
9816
10185
  if (totalCount > allSessions.length) {
9817
10186
  return { ok: true, data: { sessions: allSessions, totalCount, filtered: true } };
@@ -9823,12 +10192,19 @@ async function handleRequest(req) {
9823
10192
  let tracking = sessionTrackingMap.get(req.sessionId);
9824
10193
  if (!tracking) {
9825
10194
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
9826
- if (existsSync27(stateFile)) {
10195
+ if (existsSync28(stateFile)) {
9827
10196
  tracking = { cwd: req.cwd, messageCounter: 0 };
9828
10197
  sessionTrackingMap.set(req.sessionId, tracking);
9829
10198
  persistSessionRegistry();
9830
10199
  } else {
9831
- return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}. Run \`sis list --all\` to see available sessions.` };
10200
+ return {
10201
+ ok: false,
10202
+ error: errNotFound("unknown_session", {
10203
+ message: `Unknown session: ${req.sessionId}. No state.json found at ${stateFile}. Run \`sis session inspect list --all\` to see available sessions.`,
10204
+ received: req.sessionId,
10205
+ next: "sis session inspect list --all"
10206
+ })
10207
+ };
9832
10208
  }
9833
10209
  }
9834
10210
  const session = await resumeSession(req.sessionId, tracking.cwd, req.message);
@@ -9838,8 +10214,15 @@ async function handleRequest(req) {
9838
10214
  }
9839
10215
  case "clear-orphan": {
9840
10216
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
9841
- if (!existsSync27(stateFile)) {
9842
- return { ok: false, error: `Unknown session: ${req.sessionId}. No state.json at ${stateFile}.` };
10217
+ if (!existsSync28(stateFile)) {
10218
+ return {
10219
+ ok: false,
10220
+ error: errNotFound("unknown_session", {
10221
+ message: `Unknown session: ${req.sessionId}. No state.json at ${stateFile}.`,
10222
+ received: req.sessionId,
10223
+ next: "sis session inspect list --all"
10224
+ })
10225
+ };
9843
10226
  }
9844
10227
  await Promise.all([
9845
10228
  clearSessionOrphan(req.cwd, req.sessionId),
@@ -9889,7 +10272,7 @@ async function handleRequest(req) {
9889
10272
  let tracking = sessionTrackingMap.get(req.sessionId);
9890
10273
  if (!tracking) {
9891
10274
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
9892
- if (existsSync27(stateFile)) {
10275
+ if (existsSync28(stateFile)) {
9893
10276
  registerSessionCwd(req.sessionId, req.cwd);
9894
10277
  tracking = sessionTrackingMap.get(req.sessionId);
9895
10278
  } else {
@@ -9903,7 +10286,7 @@ async function handleRequest(req) {
9903
10286
  let tracking = sessionTrackingMap.get(req.sessionId);
9904
10287
  if (!tracking) {
9905
10288
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
9906
- if (existsSync27(stateFile)) {
10289
+ if (existsSync28(stateFile)) {
9907
10290
  registerSessionCwd(req.sessionId, req.cwd);
9908
10291
  tracking = sessionTrackingMap.get(req.sessionId);
9909
10292
  } else {
@@ -9920,7 +10303,7 @@ async function handleRequest(req) {
9920
10303
  let tracking = sessionTrackingMap.get(req.sessionId);
9921
10304
  if (!tracking) {
9922
10305
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
9923
- if (existsSync27(stateFile)) {
10306
+ if (existsSync28(stateFile)) {
9924
10307
  tracking = { cwd: req.cwd, messageCounter: 0 };
9925
10308
  sessionTrackingMap.set(req.sessionId, tracking);
9926
10309
  persistSessionRegistry();
@@ -9990,7 +10373,7 @@ async function handleRequest(req) {
9990
10373
  }
9991
10374
  case "set-upload-status": {
9992
10375
  const stateFile = `${req.cwd}/.sisyphus/sessions/${req.sessionId}/state.json`;
9993
- if (!existsSync27(stateFile)) {
10376
+ if (!existsSync28(stateFile)) {
9994
10377
  return unknownSessionError(req.sessionId);
9995
10378
  }
9996
10379
  try {
@@ -10002,7 +10385,12 @@ async function handleRequest(req) {
10002
10385
  });
10003
10386
  return { ok: true };
10004
10387
  } catch (err) {
10005
- return { ok: false, error: `Failed to persist upload status: ${err instanceof Error ? err.message : String(err)}` };
10388
+ return {
10389
+ ok: false,
10390
+ error: errPermanent("rpc_failed", {
10391
+ message: `Failed to persist upload status: ${err instanceof Error ? err.message : String(err)}`
10392
+ })
10393
+ };
10006
10394
  }
10007
10395
  }
10008
10396
  case "message": {
@@ -10048,13 +10436,32 @@ async function handleRequest(req) {
10048
10436
  const liveCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt);
10049
10437
  paneId = liveCycle?.paneId ?? session.orchestratorCycles.at(-1)?.paneId;
10050
10438
  targetLabel = "orchestrator";
10051
- if (!paneId) return { ok: false, error: "No orchestrator pane found for this session" };
10439
+ if (!paneId) return {
10440
+ ok: false,
10441
+ error: errNotFound("no_orchestrator_pane", {
10442
+ message: "No orchestrator pane found for this session",
10443
+ received: req.sessionId
10444
+ })
10445
+ };
10052
10446
  } else {
10053
10447
  const agentId = req.target.agentId;
10054
10448
  const ag = session.agents.find((a) => a.id === agentId);
10055
- if (!ag) return { ok: false, error: `Unknown agent: ${agentId} in session ${req.sessionId}` };
10449
+ if (!ag) return {
10450
+ ok: false,
10451
+ error: errNotFound("unknown_agent", {
10452
+ message: `Unknown agent: ${agentId} in session ${req.sessionId}`,
10453
+ received: agentId
10454
+ })
10455
+ };
10056
10456
  if (ag.status !== "running") {
10057
- return { ok: false, error: `Agent ${agentId} is ${ag.status}, not running \u2014 cannot tell` };
10457
+ return {
10458
+ ok: false,
10459
+ error: errConflict("agent_not_running", {
10460
+ message: `Agent ${agentId} is ${ag.status}, not running \u2014 cannot tell`,
10461
+ received: ag.status,
10462
+ expected: "running"
10463
+ })
10464
+ };
10058
10465
  }
10059
10466
  paneId = ag.paneId;
10060
10467
  targetLabel = agentId;
@@ -10062,7 +10469,12 @@ async function handleRequest(req) {
10062
10469
  try {
10063
10470
  pasteToPane(paneId, req.text, req.submit);
10064
10471
  } catch (err) {
10065
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
10472
+ return {
10473
+ ok: false,
10474
+ error: errPermanent("rpc_failed", {
10475
+ message: err instanceof Error ? err.message : String(err)
10476
+ })
10477
+ };
10066
10478
  }
10067
10479
  tracking.messageCounter += 1;
10068
10480
  const id = `tell-${String(tracking.messageCounter).padStart(3, "0")}`;
@@ -10093,7 +10505,7 @@ async function handleRequest(req) {
10093
10505
  return { ok: true, data: companion };
10094
10506
  }
10095
10507
  case "register-segment": {
10096
- if (!compositor) return { ok: false, error: "Compositor not initialized" };
10508
+ if (!compositor) return { ok: false, error: errTransient("compositor_uninit", { message: "Compositor not initialized" }) };
10097
10509
  compositor.registerExternal({
10098
10510
  id: req.id,
10099
10511
  side: req.side,
@@ -10104,26 +10516,41 @@ async function handleRequest(req) {
10104
10516
  return { ok: true };
10105
10517
  }
10106
10518
  case "update-segment": {
10107
- if (!compositor) return { ok: false, error: "Compositor not initialized" };
10519
+ if (!compositor) return { ok: false, error: errTransient("compositor_uninit", { message: "Compositor not initialized" }) };
10108
10520
  try {
10109
10521
  compositor.updateExternal(req.id, req.content);
10110
10522
  return { ok: true };
10111
10523
  } catch (e) {
10112
- return { ok: false, error: e.message };
10524
+ return {
10525
+ ok: false,
10526
+ error: errPermanent("rpc_failed", { message: e.message })
10527
+ };
10113
10528
  }
10114
10529
  }
10115
10530
  case "unregister-segment": {
10116
- if (!compositor) return { ok: false, error: "Compositor not initialized" };
10531
+ if (!compositor) return { ok: false, error: errTransient("compositor_uninit", { message: "Compositor not initialized" }) };
10117
10532
  compositor.unregisterExternal(req.id);
10118
10533
  return { ok: true };
10119
10534
  }
10120
10535
  case "ask-generate-visual": {
10121
10536
  const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
10122
- if (!ID_RE.test(req.askId)) return { ok: false, error: `Invalid askId: ${req.askId}` };
10123
- if (!ID_RE.test(req.qid)) return { ok: false, error: `Invalid qid: ${req.qid}` };
10537
+ if (!ID_RE.test(req.askId)) return {
10538
+ ok: false,
10539
+ error: errUsage("invalid_ask_id", { message: `Invalid askId: ${req.askId}`, received: req.askId })
10540
+ };
10541
+ if (!ID_RE.test(req.qid)) return {
10542
+ ok: false,
10543
+ error: errUsage("invalid_qid", { message: `Invalid qid: ${req.qid}`, received: req.qid })
10544
+ };
10124
10545
  const tracking = sessionTrackingMap.get(req.sessionId);
10125
10546
  if (!tracking) return unknownSessionError(req.sessionId);
10126
- if (!tracking.cwd) return { ok: false, error: `No cwd registered for session: ${req.sessionId}` };
10547
+ if (!tracking.cwd) return {
10548
+ ok: false,
10549
+ error: errUsage("no_cwd_registered", {
10550
+ message: `No cwd registered for session: ${req.sessionId}`,
10551
+ received: req.sessionId
10552
+ })
10553
+ };
10127
10554
  const result = await generateVisualForQuestion({
10128
10555
  cwd: tracking.cwd,
10129
10556
  sessionId: req.sessionId,
@@ -10135,62 +10562,31 @@ async function handleRequest(req) {
10135
10562
  if (result.ok) {
10136
10563
  return { ok: true, data: { markdownPath: result.markdownPath, ansiPath: result.ansiPath, turns: result.turns } };
10137
10564
  }
10138
- return { ok: false, error: result.error };
10565
+ return {
10566
+ ok: false,
10567
+ error: errPermanent("rpc_failed", { message: result.error })
10568
+ };
10139
10569
  }
10140
10570
  case "inbox-list": {
10141
- const items = [];
10571
+ const askDirs = [];
10142
10572
  for (const [sessionId, tracking] of sessionTrackingMap) {
10143
10573
  if (!tracking.cwd) continue;
10144
- let askIds = [];
10145
- try {
10146
- askIds = listAsks(tracking.cwd, sessionId);
10147
- } catch (err) {
10148
- console.warn(`[sisyphus] inbox-list: listAsks failed for ${sessionId}:`, err);
10149
- continue;
10150
- }
10151
- const sessionName = tracking.name;
10152
- for (const askId of askIds) {
10153
- try {
10154
- const meta = readMeta(tracking.cwd, sessionId, askId);
10155
- if (!meta) continue;
10156
- if (meta.status === "answered") continue;
10157
- let title = meta.title;
10158
- let subtitle = meta.subtitle;
10159
- let kind = meta.kind;
10160
- if (title === void 0 || kind === void 0) {
10161
- try {
10162
- const decisions = readDecisions(tracking.cwd, sessionId, askId);
10163
- if (decisions) {
10164
- const q0 = decisions.interactions[0];
10165
- if (title === void 0) title = decisions.title !== void 0 ? decisions.title : q0?.title;
10166
- if (subtitle === void 0) subtitle = q0?.subtitle;
10167
- if (kind === void 0) kind = q0?.kind;
10168
- }
10169
- } catch (_err) {
10170
- }
10171
- }
10172
- items.push({
10173
- sessionId,
10174
- sessionName,
10175
- cwd: tracking.cwd,
10176
- askId,
10177
- askedBy: meta.askedBy,
10178
- askedAt: meta.askedAt,
10179
- status: meta.status,
10180
- blocking: meta.blocking,
10181
- orphaned: meta.orphaned,
10182
- title,
10183
- subtitle,
10184
- blockedSince: meta.status === "in-progress" ? meta.startedAt ?? meta.askedAt : meta.askedAt,
10185
- kind
10186
- });
10187
- } catch (err) {
10188
- console.warn(`[sisyphus] inbox-list: readMeta failed for ${sessionId}/${askId}:`, err);
10189
- }
10190
- }
10574
+ askDirs.push(askDir(tracking.cwd, sessionId));
10575
+ }
10576
+ let items;
10577
+ try {
10578
+ items = scanInbox(askDirs);
10579
+ } catch (err) {
10580
+ console.warn("[sisyphus] inbox-list: scanInbox failed:", err);
10581
+ items = [];
10191
10582
  }
10192
- items.sort((a, b) => a.blockedSince.localeCompare(b.blockedSince));
10193
- return { ok: true, data: { items } };
10583
+ const itemsWithName = items.map((item) => {
10584
+ const sessionId = basename6(dirname8(dirname8(dirname8(item.dir))));
10585
+ const tracking = sessionTrackingMap.get(sessionId);
10586
+ const sessionName = tracking?.name ?? item.source?.sessionName;
10587
+ return { ...item, sessionName };
10588
+ });
10589
+ return { ok: true, data: { items: itemsWithName } };
10194
10590
  }
10195
10591
  case "cloud-handoff": {
10196
10592
  const tracking = sessionTrackingMap.get(req.sessionId);
@@ -10198,23 +10594,45 @@ async function handleRequest(req) {
10198
10594
  const session = getSession(tracking.cwd, req.sessionId);
10199
10595
  if (session.handoff?.sentAt) {
10200
10596
  const where = session.handoff.target ? `${session.handoff.target.provider}:${session.handoff.target.repo}` : "cloud";
10201
- return { ok: false, error: `Session ${req.sessionId} is already on ${where}. Use \`sis cloud reclaim\` first.` };
10597
+ return {
10598
+ ok: false,
10599
+ error: errConflict("already_on_cloud", {
10600
+ message: `Session ${req.sessionId} is already on ${where}. Use \`sis cloud handoff pull\` first.`,
10601
+ received: req.sessionId,
10602
+ next: "sis cloud handoff pull"
10603
+ })
10604
+ };
10202
10605
  }
10203
10606
  if (session.handoff && !req.force) {
10204
- return { ok: false, error: `Session ${req.sessionId} already has a queued handoff. Pass --force to override or --cancel to clear.` };
10607
+ return {
10608
+ ok: false,
10609
+ error: errConflict("handoff_already_queued", {
10610
+ message: `Session ${req.sessionId} already has a queued handoff. Pass --force to override or run \`sis cloud handoff cancel ${req.sessionId}\` to clear.`,
10611
+ received: req.sessionId
10612
+ })
10613
+ };
10205
10614
  }
10206
10615
  const { resolveProvider: resolveProvider2, defaultRepoName: defaultRepoName2, checkRepoName: checkRepoName2, triggerForceHandoff: triggerForceHandoff2 } = await Promise.resolve().then(() => (init_cloud_handoff(), cloud_handoff_exports));
10207
10616
  let provider;
10208
10617
  try {
10209
10618
  provider = resolveProvider2(req.provider);
10210
10619
  } catch (err) {
10211
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
10620
+ return {
10621
+ ok: false,
10622
+ error: errPermanent("rpc_failed", { message: err instanceof Error ? err.message : String(err) })
10623
+ };
10212
10624
  }
10213
10625
  const repo = req.repo ?? defaultRepoName2(tracking.cwd);
10214
10626
  try {
10215
10627
  checkRepoName2(repo);
10216
10628
  } catch (err) {
10217
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
10629
+ return {
10630
+ ok: false,
10631
+ error: errUsage("invalid_repo_name", {
10632
+ message: err instanceof Error ? err.message : String(err),
10633
+ received: repo
10634
+ })
10635
+ };
10218
10636
  }
10219
10637
  await updateSession(tracking.cwd, req.sessionId, {
10220
10638
  handoff: {
@@ -10234,10 +10652,23 @@ async function handleRequest(req) {
10234
10652
  if (!tracking) return unknownSessionError(req.sessionId);
10235
10653
  const session = getSession(tracking.cwd, req.sessionId);
10236
10654
  if (!session.handoff) {
10237
- return { ok: false, error: `Session ${req.sessionId} has no queued handoff.` };
10655
+ return {
10656
+ ok: false,
10657
+ error: errNotFound("no_queued_handoff", {
10658
+ message: `Session ${req.sessionId} has no queued handoff.`,
10659
+ received: req.sessionId
10660
+ })
10661
+ };
10238
10662
  }
10239
10663
  if (session.handoff.sentAt) {
10240
- return { ok: false, error: `Session ${req.sessionId} has already shipped to cloud \u2014 use \`sis cloud reclaim\` to bring it back.` };
10664
+ return {
10665
+ ok: false,
10666
+ error: errConflict("already_shipped", {
10667
+ message: `Session ${req.sessionId} has already shipped to cloud \u2014 use \`sis cloud handoff pull\` to bring it back.`,
10668
+ received: req.sessionId,
10669
+ next: "sis cloud handoff pull"
10670
+ })
10671
+ };
10241
10672
  }
10242
10673
  await updateSession(tracking.cwd, req.sessionId, { handoff: void 0 });
10243
10674
  return { ok: true, data: { cancelled: true } };
@@ -10247,7 +10678,13 @@ async function handleRequest(req) {
10247
10678
  if (!tracking) return unknownSessionError(req.sessionId);
10248
10679
  const session = getSession(tracking.cwd, req.sessionId);
10249
10680
  if (!session.handoff?.sentAt) {
10250
- return { ok: false, error: `Session ${req.sessionId} was never sent to cloud; nothing to reclaim.` };
10681
+ return {
10682
+ ok: false,
10683
+ error: errConflict("not_on_cloud", {
10684
+ message: `Session ${req.sessionId} was never sent to cloud; nothing to reclaim.`,
10685
+ received: req.sessionId
10686
+ })
10687
+ };
10251
10688
  }
10252
10689
  await updateSession(tracking.cwd, req.sessionId, {
10253
10690
  handoff: { ...session.handoff, reclaimedAt: (/* @__PURE__ */ new Date()).toISOString() }
@@ -10259,7 +10696,14 @@ async function handleRequest(req) {
10259
10696
  if (!tracking) return unknownSessionError(req.sessionId);
10260
10697
  const session = getSession(tracking.cwd, req.sessionId);
10261
10698
  if (session.handoff?.sentAt) {
10262
- return { ok: false, error: `Session ${req.sessionId} already lives on cloud; nothing to quiesce locally.` };
10699
+ return {
10700
+ ok: false,
10701
+ error: errConflict("already_on_cloud", {
10702
+ message: `Session ${req.sessionId} already lives on cloud; nothing to quiesce locally.`,
10703
+ received: req.sessionId,
10704
+ next: "sis cloud handoff pull"
10705
+ })
10706
+ };
10263
10707
  }
10264
10708
  await updateSession(tracking.cwd, req.sessionId, {
10265
10709
  handoff: {
@@ -10275,17 +10719,23 @@ async function handleRequest(req) {
10275
10719
  return { ok: true, data: { queued: true, force: false } };
10276
10720
  }
10277
10721
  default:
10278
- return { ok: false, error: `Unknown request type: ${req.type}` };
10722
+ return {
10723
+ ok: false,
10724
+ error: errUsage("unknown_request_type", {
10725
+ message: `Unknown request type: ${req.type}`,
10726
+ received: req.type
10727
+ })
10728
+ };
10279
10729
  }
10280
10730
  } catch (err) {
10281
10731
  const message = err instanceof Error ? err.message : String(err);
10282
- return { ok: false, error: message };
10732
+ return { ok: false, error: errPermanent("internal_error", { message }) };
10283
10733
  }
10284
10734
  }
10285
10735
  function startServer() {
10286
10736
  return new Promise((resolve13, reject) => {
10287
10737
  const sock = socketPath();
10288
- if (existsSync27(sock)) {
10738
+ if (existsSync28(sock)) {
10289
10739
  unlinkSync4(sock);
10290
10740
  }
10291
10741
  server = createServer((conn) => {
@@ -10300,7 +10750,7 @@ function startServer() {
10300
10750
  try {
10301
10751
  req = JSON.parse(line);
10302
10752
  } catch {
10303
- conn.write(JSON.stringify({ ok: false, error: "Invalid JSON" }) + "\n");
10753
+ conn.write(JSON.stringify({ ok: false, error: errUsage("invalid_json", { message: "Invalid JSON" }) }) + "\n");
10304
10754
  continue;
10305
10755
  }
10306
10756
  handleRequest(req).then((res) => {
@@ -10310,7 +10760,7 @@ function startServer() {
10310
10760
  }).catch((err) => {
10311
10761
  console.warn("[sisyphus] Unhandled request error:", err instanceof Error ? err.message : err);
10312
10762
  if (!conn.destroyed) {
10313
- conn.write(JSON.stringify({ ok: false, error: "Internal server error" }) + "\n");
10763
+ conn.write(JSON.stringify({ ok: false, error: errPermanent("internal_error", { message: "Internal server error" }) }) + "\n");
10314
10764
  }
10315
10765
  });
10316
10766
  }
@@ -10340,7 +10790,7 @@ function stopServer() {
10340
10790
  }
10341
10791
  server.close(() => {
10342
10792
  const sock = socketPath();
10343
- if (existsSync27(sock)) {
10793
+ if (existsSync28(sock)) {
10344
10794
  unlinkSync4(sock);
10345
10795
  }
10346
10796
  server = null;
@@ -10353,6 +10803,7 @@ var init_server = __esm({
10353
10803
  "src/daemon/server.ts"() {
10354
10804
  "use strict";
10355
10805
  init_paths();
10806
+ init_protocol();
10356
10807
  init_shell();
10357
10808
  init_session_manager();
10358
10809
  init_companion();
@@ -10367,6 +10818,7 @@ var init_server = __esm({
10367
10818
  init_orchestrator();
10368
10819
  init_agent();
10369
10820
  init_tmux();
10821
+ init_paths();
10370
10822
  server = null;
10371
10823
  compositor = null;
10372
10824
  sessionTrackingMap = /* @__PURE__ */ new Map();
@@ -10378,9 +10830,10 @@ init_paths();
10378
10830
  init_config();
10379
10831
  init_server();
10380
10832
  init_orphan_sweep();
10381
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync28, writeFileSync as writeFileSync17, unlinkSync as unlinkSync6, existsSync as existsSync31 } from "fs";
10833
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync29, writeFileSync as writeFileSync17, unlinkSync as unlinkSync6, existsSync as existsSync32 } from "fs";
10382
10834
  import { execSync as execSync8 } from "child_process";
10383
10835
  import { setTimeout as sleep } from "timers/promises";
10836
+ import { Command } from "commander";
10384
10837
 
10385
10838
  // src/daemon/heartbeat-asks.ts
10386
10839
  init_ask_store();
@@ -10388,7 +10841,7 @@ init_state();
10388
10841
  init_server();
10389
10842
  init_paths();
10390
10843
  import { ulid as ulid3 } from "ulid";
10391
- import { existsSync as existsSync28 } from "fs";
10844
+ import { existsSync as existsSync29 } from "fs";
10392
10845
  var HEARTBEAT_ASKED_BY2 = "system:heartbeat";
10393
10846
  var HEARTBEAT_THRESHOLD_MS = 60 * 60 * 1e3;
10394
10847
  var HEARTBEAT_SCAN_INTERVAL_MS = 15 * 60 * 1e3;
@@ -10443,17 +10896,47 @@ async function emitHeartbeatAsk(cwd, sessionId, original) {
10443
10896
  });
10444
10897
  writeDecisions(cwd, sessionId, askId, deck);
10445
10898
  await updateMeta(cwd, sessionId, original.askId, {
10446
- heartbeatNotifiedAt: (/* @__PURE__ */ new Date()).toISOString()
10899
+ heartbeatNotifiedAt: (/* @__PURE__ */ new Date()).toISOString(),
10900
+ heartbeatAskId: askId
10447
10901
  });
10448
10902
  }
10903
+ function isModeGateStale(deck, currentMode) {
10904
+ const source = deck.source;
10905
+ const chain = source?.modeChain;
10906
+ if (!chain || chain.length === 0) return false;
10907
+ if (!currentMode) return false;
10908
+ return !chain.some((e) => e.mode === currentMode);
10909
+ }
10449
10910
  async function scanSessionForStaleAsks(cwd, sessionId) {
10450
10911
  const now = Date.now();
10912
+ let currentMode;
10913
+ try {
10914
+ const session = getSession(cwd, sessionId);
10915
+ currentMode = session.orchestratorCycles[session.orchestratorCycles.length - 1]?.mode;
10916
+ } catch {
10917
+ }
10451
10918
  for (const askId of listAsks(cwd, sessionId)) {
10452
10919
  try {
10453
10920
  const meta = readMeta(cwd, sessionId, askId);
10454
10921
  if (!meta) continue;
10455
10922
  if (meta.status === "answered") continue;
10456
10923
  if (meta.orphaned) continue;
10924
+ if (meta.modeTransition === true) {
10925
+ const deck = readDecisions(cwd, sessionId, askId);
10926
+ if (deck && isModeGateStale(deck, currentMode)) {
10927
+ writeOutput(cwd, sessionId, askId, [{
10928
+ id: "mode-transition",
10929
+ selectedOptionId: "ack",
10930
+ freetext: "auto-resolved: session advanced past mode-transition"
10931
+ }]);
10932
+ await updateMeta(cwd, sessionId, askId, {
10933
+ status: "answered",
10934
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
10935
+ });
10936
+ console.log(`[sisyphus] mode-gate auto-resolved ask ${askId} for session ${sessionId} (currentMode: ${currentMode})`);
10937
+ }
10938
+ continue;
10939
+ }
10457
10940
  if (meta.heartbeatNotifiedAt) continue;
10458
10941
  if (meta.askedBy === HEARTBEAT_ASKED_BY2) continue;
10459
10942
  const askedAtMs = new Date(meta.askedAt).getTime();
@@ -10471,7 +10954,7 @@ async function scanSessionForStaleAsks(cwd, sessionId) {
10471
10954
  async function scanAllSessionsForStaleAsks() {
10472
10955
  const reg = loadSessionRegistry();
10473
10956
  for (const [sessionId, cwd] of Object.entries(reg)) {
10474
- if (!existsSync28(statePath(cwd, sessionId))) continue;
10957
+ if (!existsSync29(statePath(cwd, sessionId))) continue;
10475
10958
  try {
10476
10959
  await scanSessionForStaleAsks(cwd, sessionId);
10477
10960
  } catch (err) {
@@ -10534,17 +11017,15 @@ var DEFAULT_STATUS_BAR_CONFIG = {
10534
11017
  init_tmux();
10535
11018
  init_status_dots();
10536
11019
  init_companion();
10537
- init_exec();
10538
- init_shell();
10539
- import { readFileSync as readFileSync26, existsSync as existsSync29 } from "fs";
11020
+ import { readFileSync as readFileSync27, existsSync as existsSync30 } from "fs";
10540
11021
  import { homedir as homedir8 } from "os";
10541
11022
  import { join as join22 } from "path";
10542
11023
  var STATUS_BAR_BG = "#1d1e21";
10543
11024
  var SESSION_ORDER_PATH = join22(homedir8(), ".config", "tmux", "session-order");
10544
11025
  function getSessionOrder() {
10545
11026
  try {
10546
- if (!existsSync29(SESSION_ORDER_PATH)) return [];
10547
- return readFileSync26(SESSION_ORDER_PATH, "utf-8").split("\n").filter(Boolean);
11027
+ if (!existsSync30(SESSION_ORDER_PATH)) return [];
11028
+ return readFileSync27(SESSION_ORDER_PATH, "utf-8").split("\n").filter(Boolean);
10548
11029
  } catch {
10549
11030
  return [];
10550
11031
  }
@@ -10554,17 +11035,6 @@ var STATE_PRIORITY = {
10554
11035
  stopped: 2,
10555
11036
  idle: 1
10556
11037
  };
10557
- function listWindowsForSession(sessionName) {
10558
- const output = execSafe(`tmux list-windows -t ${shellQuote(sessionName)} -F "#{window_index} #{window_id} #{window_name}"`);
10559
- if (!output) return [];
10560
- return output.split("\n").filter(Boolean).map((line) => {
10561
- const parts = line.split(" ");
10562
- const index = parseInt(parts[0], 10);
10563
- const id = parts[1];
10564
- const name = parts.slice(2).join(" ");
10565
- return { index, id, name };
10566
- });
10567
- }
10568
11038
  function leftArrow(fromBg, toBg) {
10569
11039
  return `#[fg=${fromBg}]#[bg=${toBg}]\uE0B0`;
10570
11040
  }
@@ -10585,6 +11055,11 @@ var Compositor = class {
10585
11055
  segments = /* @__PURE__ */ new Map();
10586
11056
  external = /* @__PURE__ */ new Map();
10587
11057
  config;
11058
+ // Per-session diff cache: skip setSessionOption when the rendered string is
11059
+ // unchanged. With 12 sessions at 5s polling this is the difference between
11060
+ // 24 tmux subprocess spawns per render and 0 on a stable status bar.
11061
+ lastLeftBySession = /* @__PURE__ */ new Map();
11062
+ lastRightBySession = /* @__PURE__ */ new Map();
10588
11063
  constructor(config) {
10589
11064
  this.config = config;
10590
11065
  }
@@ -10602,18 +11077,35 @@ var Compositor = class {
10602
11077
  unregisterExternal(id) {
10603
11078
  this.external.delete(id);
10604
11079
  }
10605
- render() {
10606
- const ctx = this.buildContext();
11080
+ render(panesByWindow) {
11081
+ const ctx = this.buildContext(panesByWindow);
11082
+ const liveNames = /* @__PURE__ */ new Set();
10607
11083
  for (const session of ctx.allSessions) {
11084
+ liveNames.add(session.name);
10608
11085
  const sessionCtx = this.buildSessionContext(ctx, session.name);
10609
- setSessionOption(session.name, "@sisyphus_left", this.composeLeft(sessionCtx));
10610
- setSessionOption(session.name, "@sisyphus_right", this.composeRight(sessionCtx));
11086
+ const left = this.composeLeft(sessionCtx);
11087
+ if (this.lastLeftBySession.get(session.name) !== left) {
11088
+ setSessionOption(session.name, "@sisyphus_left", left);
11089
+ this.lastLeftBySession.set(session.name, left);
11090
+ }
11091
+ const right = this.composeRight(sessionCtx);
11092
+ if (this.lastRightBySession.get(session.name) !== right) {
11093
+ setSessionOption(session.name, "@sisyphus_right", right);
11094
+ this.lastRightBySession.set(session.name, right);
11095
+ }
11096
+ }
11097
+ for (const name of this.lastLeftBySession.keys()) {
11098
+ if (!liveNames.has(name)) this.lastLeftBySession.delete(name);
11099
+ }
11100
+ for (const name of this.lastRightBySession.keys()) {
11101
+ if (!liveNames.has(name)) this.lastRightBySession.delete(name);
10611
11102
  }
10612
11103
  }
10613
- buildContext() {
11104
+ buildContext(panesByWindow) {
10614
11105
  const allPanes = listAllPanes();
10615
11106
  const allSessionEntries = listAllSessions();
10616
11107
  const allSessions = allSessionEntries.map((e) => ({ name: e.name }));
11108
+ const panes = panesByWindow ?? listAllPanesByWindow();
10617
11109
  const sessionStates = /* @__PURE__ */ new Map();
10618
11110
  for (const { sessionName, paneId } of allPanes) {
10619
11111
  const state = readClaudeState(paneId);
@@ -10626,10 +11118,7 @@ var Compositor = class {
10626
11118
  const sisyphusPhases2 = getSisyphusPhases();
10627
11119
  const sessionOrder = getSessionOrder();
10628
11120
  const companion = loadCompanion();
10629
- const windowsBySession = /* @__PURE__ */ new Map();
10630
- for (const session of allSessions) {
10631
- windowsBySession.set(session.name, listWindowsForSession(session.name));
10632
- }
11121
+ const windowsBySession = listAllWindowsBySession();
10633
11122
  return {
10634
11123
  allSessions,
10635
11124
  allPanes,
@@ -10639,6 +11128,7 @@ var Compositor = class {
10639
11128
  companion,
10640
11129
  config: this.config,
10641
11130
  windowsBySession,
11131
+ panesByWindow: panes,
10642
11132
  prevBg: STATUS_BAR_BG,
10643
11133
  currentSession: ""
10644
11134
  // overwritten per-session in buildSessionContext
@@ -10928,7 +11418,6 @@ function createCompanionSegment() {
10928
11418
  }
10929
11419
 
10930
11420
  // src/daemon/segments/windows.ts
10931
- init_tmux();
10932
11421
  init_status_dots();
10933
11422
  function createWindowsSegment() {
10934
11423
  return {
@@ -10947,7 +11436,7 @@ function createWindowsSegment() {
10947
11436
  if (!windows || windows.length === 0) return { content: "" };
10948
11437
  const windowDots = /* @__PURE__ */ new Map();
10949
11438
  for (const win of windows) {
10950
- const panes = listWindowPanes(win.id);
11439
+ const panes = ctx.panesByWindow.get(win.id) ?? [];
10951
11440
  let dots = "";
10952
11441
  for (const { paneId } of panes) {
10953
11442
  const state = readClaudeState(paneId);
@@ -11061,8 +11550,8 @@ function buildEntries() {
11061
11550
  return entries;
11062
11551
  }
11063
11552
  function toTsv(entries) {
11064
- const ts2 = Math.floor(Date.now() / 1e3);
11065
- const lines = [`#ts:${ts2}`];
11553
+ const ts = Math.floor(Date.now() / 1e3);
11554
+ const lines = [`#ts:${ts}`];
11066
11555
  for (const e of entries) {
11067
11556
  lines.push(`${e.type} ${e.tmuxName} ${e.cwd} ${e.phase ?? "-"} ${e.tmuxSessionId}`);
11068
11557
  }
@@ -11080,8 +11569,8 @@ function writeManifest() {
11080
11569
  atomicWrite(sessionsManifestPath(), toJson(entries));
11081
11570
  }
11082
11571
  function writeEmptyManifest() {
11083
- const ts2 = Math.floor(Date.now() / 1e3);
11084
- atomicWrite(sessionsManifestTsvPath(), `#ts:${ts2}
11572
+ const ts = Math.floor(Date.now() / 1e3);
11573
+ atomicWrite(sessionsManifestTsvPath(), `#ts:${ts}
11085
11574
  `);
11086
11575
  atomicWrite(sessionsManifestPath(), JSON.stringify({ updatedAt: Date.now(), sessions: [] }, null, 2));
11087
11576
  }
@@ -11209,7 +11698,7 @@ async function checkAndApply() {
11209
11698
  console.error("[sisyphus] Auto-update check failed:", err);
11210
11699
  }
11211
11700
  }
11212
- var UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1e3;
11701
+ var UPDATE_INTERVAL_MS = 15 * 60 * 1e3;
11213
11702
  var updateTimer = null;
11214
11703
  function startPeriodicUpdateCheck() {
11215
11704
  if (isLinkedInstall()) return;
@@ -11226,7 +11715,7 @@ function stopPeriodicUpdateCheck() {
11226
11715
  }
11227
11716
 
11228
11717
  // src/daemon/plugin-install.ts
11229
- import { copyFileSync as copyFileSync6, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync4, existsSync as existsSync30, readFileSync as readFileSync27, chmodSync as chmodSync3 } from "fs";
11718
+ import { copyFileSync as copyFileSync6, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync4, existsSync as existsSync31, readFileSync as readFileSync28, chmodSync as chmodSync3 } from "fs";
11230
11719
  import { join as join23, resolve as resolve12 } from "path";
11231
11720
  import { homedir as homedir9 } from "os";
11232
11721
  var PLUGIN_NAME = "sisyphus-tmux";
@@ -11240,7 +11729,7 @@ function copyDir(src, dest) {
11240
11729
  copyDir(srcPath, destPath);
11241
11730
  } else {
11242
11731
  const srcMtime = statSync4(srcPath).mtimeMs;
11243
- const destMtime = existsSync30(destPath) ? statSync4(destPath).mtimeMs : 0;
11732
+ const destMtime = existsSync31(destPath) ? statSync4(destPath).mtimeMs : 0;
11244
11733
  if (srcMtime > destMtime) {
11245
11734
  copyFileSync6(srcPath, destPath);
11246
11735
  }
@@ -11250,16 +11739,16 @@ function copyDir(src, dest) {
11250
11739
  function pluginNeedsUpdate(sourceDir) {
11251
11740
  const srcHooks = join23(sourceDir, "hooks", "hooks.json");
11252
11741
  const destHooks = join23(INSTALL_DIR, "hooks", "hooks.json");
11253
- if (!existsSync30(destHooks)) return true;
11742
+ if (!existsSync31(destHooks)) return true;
11254
11743
  try {
11255
- return readFileSync27(srcHooks, "utf-8") !== readFileSync27(destHooks, "utf-8");
11744
+ return readFileSync28(srcHooks, "utf-8") !== readFileSync28(destHooks, "utf-8");
11256
11745
  } catch {
11257
11746
  return true;
11258
11747
  }
11259
11748
  }
11260
11749
  function installPlugin() {
11261
11750
  const sourceDir = resolve12(import.meta.dirname, "../templates/sisyphus-tmux-plugin");
11262
- if (!existsSync30(sourceDir)) {
11751
+ if (!existsSync31(sourceDir)) {
11263
11752
  console.error(`[plugin-install] Source dir not found: ${sourceDir}`);
11264
11753
  return;
11265
11754
  }
@@ -11267,7 +11756,7 @@ function installPlugin() {
11267
11756
  try {
11268
11757
  copyDir(sourceDir, INSTALL_DIR);
11269
11758
  const hookScript = join23(INSTALL_DIR, "hooks", "tmux-state.sh");
11270
- if (existsSync30(hookScript)) {
11759
+ if (existsSync31(hookScript)) {
11271
11760
  try {
11272
11761
  chmodSync3(hookScript, 493);
11273
11762
  } catch {
@@ -11322,18 +11811,13 @@ if (nodeVersion < 22) {
11322
11811
  console.error(`[sisyphus] Node.js v22+ required (current: v${process.versions.node})`);
11323
11812
  process.exit(1);
11324
11813
  }
11325
- var ts = () => (/* @__PURE__ */ new Date()).toISOString();
11326
- var origLog = console.log.bind(console);
11327
- var origError = console.error.bind(console);
11328
- console.log = (...args) => origLog(`[${ts()}]`, ...args);
11329
- console.error = (...args) => origError(`[${ts()}]`, ...args);
11330
11814
  function ensureDirs() {
11331
11815
  mkdirSync15(globalDir(), { recursive: true });
11332
11816
  }
11333
11817
  function readPid() {
11334
11818
  const pidFile = daemonPidPath();
11335
11819
  try {
11336
- const pid = parseInt(readFileSync28(pidFile, "utf-8").trim(), 10);
11820
+ const pid = parseInt(readFileSync29(pidFile, "utf-8").trim(), 10);
11337
11821
  return pid && isProcessAlive(pid) ? pid : null;
11338
11822
  } catch {
11339
11823
  return null;
@@ -11394,6 +11878,94 @@ function stopDaemon() {
11394
11878
  releasePidLock();
11395
11879
  return true;
11396
11880
  }
11881
+ async function recoverOneSession(sessionId, cwd, tmuxIdSet, tmuxNameToId, panesByWindow) {
11882
+ const stateFile = statePath(cwd, sessionId);
11883
+ if (!existsSync32(stateFile)) return false;
11884
+ let session;
11885
+ try {
11886
+ session = JSON.parse(readFileSync29(stateFile, "utf-8"));
11887
+ } catch {
11888
+ console.error(`[sisyphus] Failed to read session state for ${sessionId}, skipping`);
11889
+ return false;
11890
+ }
11891
+ if (session.status !== "active" && session.status !== "paused") return false;
11892
+ registerSessionCwd(sessionId, cwd);
11893
+ resetAgentCounterFromState(sessionId, session.agents ?? []);
11894
+ if (!session.tmuxSessionName) return true;
11895
+ let sessionAlive = false;
11896
+ let currentTmuxId = session.tmuxSessionId;
11897
+ if (currentTmuxId && tmuxIdSet.has(currentTmuxId)) {
11898
+ sessionAlive = true;
11899
+ } else {
11900
+ currentTmuxId = void 0;
11901
+ }
11902
+ if (!sessionAlive) {
11903
+ const resolved = tmuxNameToId.get(session.tmuxSessionName);
11904
+ if (resolved) {
11905
+ currentTmuxId = resolved;
11906
+ sessionAlive = true;
11907
+ await updateSessionTmux(cwd, sessionId, session.tmuxSessionName, session.tmuxWindowId ?? "", currentTmuxId);
11908
+ }
11909
+ }
11910
+ if (!sessionAlive) {
11911
+ if (session.status === "active") {
11912
+ await updateSessionStatus(cwd, sessionId, "paused");
11913
+ await updateSession(cwd, sessionId, { tmuxSessionId: void 0 });
11914
+ console.log(`[sisyphus] Session ${sessionId} paused: tmux session no longer exists`);
11915
+ }
11916
+ return true;
11917
+ }
11918
+ let windowId = session.tmuxWindowId;
11919
+ if (!windowId) {
11920
+ windowId = getFirstWindowId(currentTmuxId) ?? getFirstWindowId(session.tmuxSessionName) ?? void 0;
11921
+ if (windowId) {
11922
+ await updateSessionTmux(cwd, sessionId, session.tmuxSessionName, windowId, currentTmuxId);
11923
+ console.log(`[sisyphus] Discovered missing windowId ${windowId} for session ${sessionId}`);
11924
+ }
11925
+ }
11926
+ const livePanes = windowId ? panesByWindow.get(windowId) ?? listPanes(windowId) : [];
11927
+ if (livePanes.length === 0) {
11928
+ if (session.status === "active") {
11929
+ await updateSessionStatus(cwd, sessionId, "paused");
11930
+ console.log(`[sisyphus] Session ${sessionId} paused: tmux window no longer exists`);
11931
+ }
11932
+ return true;
11933
+ }
11934
+ initSessionMeta(currentTmuxId, cwd, sessionId);
11935
+ registerSessionTmux(sessionId, session.tmuxSessionName, windowId, currentTmuxId);
11936
+ setWindowId(sessionId, windowId);
11937
+ trackSession(sessionId, cwd, currentTmuxId, session.tmuxSessionName);
11938
+ updateTrackedWindow(sessionId, windowId);
11939
+ initTimers(sessionId, session);
11940
+ const livePaneIds = new Set(livePanes.map((p) => p.paneId));
11941
+ const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
11942
+ if (lastIncompleteCycle?.paneId) {
11943
+ setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
11944
+ if (livePaneIds.has(lastIncompleteCycle.paneId)) {
11945
+ registerPane(lastIncompleteCycle.paneId, sessionId, "orchestrator");
11946
+ }
11947
+ }
11948
+ for (const agent of session.agents) {
11949
+ if (agent.status === "running" && agent.paneId && livePaneIds.has(agent.paneId)) {
11950
+ registerPane(agent.paneId, sessionId, "agent", agent.id);
11951
+ }
11952
+ }
11953
+ console.log(`[sisyphus] Reconnected session ${sessionId} to tmux window ${session.tmuxWindowId}`);
11954
+ if (session.status === "active" && session.agents.length > 0) {
11955
+ const hasRunningAgents = session.agents.some((a) => a.status === "running");
11956
+ if (!hasRunningAgents) {
11957
+ const orchestratorPaneId = getOrchestratorPaneId(sessionId);
11958
+ const orchestratorAlive = orchestratorPaneId && livePaneIds.has(orchestratorPaneId);
11959
+ if (!orchestratorAlive) {
11960
+ await orphanOrchestrator(cwd, sessionId, "orchestrator lost while daemon was down", "daemon-startup-stuck");
11961
+ await completeOrchestratorCycle(cwd, sessionId);
11962
+ console.log(`[sisyphus] Detected stuck session ${sessionId} on recovery: triggering orchestrator respawn`);
11963
+ await onAllAgentsDone2(sessionId, cwd, session.tmuxWindowId);
11964
+ }
11965
+ }
11966
+ }
11967
+ return true;
11968
+ }
11397
11969
  async function recoverSessions() {
11398
11970
  const registry = loadSessionRegistry();
11399
11971
  const entries = Object.entries(registry);
@@ -11401,104 +11973,14 @@ async function recoverSessions() {
11401
11973
  console.log("[sisyphus] No sessions to recover");
11402
11974
  return;
11403
11975
  }
11404
- let recovered = 0;
11405
- for (const [sessionId, cwd] of entries) {
11406
- const stateFile = statePath(cwd, sessionId);
11407
- if (!existsSync31(stateFile)) {
11408
- continue;
11409
- }
11410
- try {
11411
- const session = JSON.parse(readFileSync28(stateFile, "utf-8"));
11412
- if (session.status === "active" || session.status === "paused") {
11413
- registerSessionCwd(sessionId, cwd);
11414
- resetAgentCounterFromState(sessionId, session.agents ?? []);
11415
- if (session.tmuxSessionName) {
11416
- let sessionAlive = false;
11417
- let currentTmuxId = session.tmuxSessionId;
11418
- if (currentTmuxId) {
11419
- sessionAlive = sessionExistsById(currentTmuxId);
11420
- if (!sessionAlive) {
11421
- currentTmuxId = void 0;
11422
- }
11423
- }
11424
- if (!sessionAlive && session.tmuxSessionName) {
11425
- if (sessionNameTaken(session.tmuxSessionName)) {
11426
- currentTmuxId = resolveSessionId(session.tmuxSessionName) ?? void 0;
11427
- sessionAlive = !!currentTmuxId;
11428
- if (currentTmuxId) {
11429
- await updateSessionTmux(cwd, sessionId, session.tmuxSessionName, session.tmuxWindowId ?? "", currentTmuxId);
11430
- }
11431
- }
11432
- }
11433
- if (!sessionAlive) {
11434
- if (session.status === "active") {
11435
- await updateSessionStatus(cwd, sessionId, "paused");
11436
- await updateSession(cwd, sessionId, { tmuxSessionId: void 0 });
11437
- console.log(`[sisyphus] Session ${sessionId} paused: tmux session no longer exists`);
11438
- }
11439
- recovered++;
11440
- continue;
11441
- }
11442
- let windowId = session.tmuxWindowId;
11443
- if (!windowId) {
11444
- windowId = getFirstWindowId(currentTmuxId) ?? getFirstWindowId(session.tmuxSessionName) ?? void 0;
11445
- if (windowId) {
11446
- await updateSessionTmux(cwd, sessionId, session.tmuxSessionName, windowId, currentTmuxId);
11447
- console.log(`[sisyphus] Discovered missing windowId ${windowId} for session ${sessionId}`);
11448
- }
11449
- }
11450
- const livePanes = windowId ? listPanes(windowId) : [];
11451
- if (livePanes.length > 0) {
11452
- initSessionMeta(currentTmuxId, cwd, sessionId);
11453
- registerSessionTmux(sessionId, session.tmuxSessionName, windowId, currentTmuxId);
11454
- setWindowId(sessionId, windowId);
11455
- trackSession(sessionId, cwd, currentTmuxId, session.tmuxSessionName);
11456
- updateTrackedWindow(sessionId, windowId);
11457
- initTimers(sessionId, session);
11458
- const lastIncompleteCycle = [...session.orchestratorCycles].reverse().find((c) => !c.completedAt && c.paneId);
11459
- if (lastIncompleteCycle?.paneId) {
11460
- setOrchestratorPaneId(sessionId, lastIncompleteCycle.paneId);
11461
- const livePaneIds = new Set(livePanes.map((p) => p.paneId));
11462
- if (livePaneIds.has(lastIncompleteCycle.paneId)) {
11463
- registerPane(lastIncompleteCycle.paneId, sessionId, "orchestrator");
11464
- }
11465
- }
11466
- for (const agent of session.agents) {
11467
- if (agent.status === "running" && agent.paneId) {
11468
- const livePaneIds = new Set(livePanes.map((p) => p.paneId));
11469
- if (livePaneIds.has(agent.paneId)) {
11470
- registerPane(agent.paneId, sessionId, "agent", agent.id);
11471
- }
11472
- }
11473
- }
11474
- console.log(`[sisyphus] Reconnected session ${sessionId} to tmux window ${session.tmuxWindowId}`);
11475
- if (session.status === "active" && session.agents.length > 0) {
11476
- const hasRunningAgents = session.agents.some((a) => a.status === "running");
11477
- if (!hasRunningAgents) {
11478
- const livePaneIds = new Set(livePanes.map((p) => p.paneId));
11479
- const orchestratorPaneId = getOrchestratorPaneId(sessionId);
11480
- const orchestratorAlive = orchestratorPaneId && livePaneIds.has(orchestratorPaneId);
11481
- if (!orchestratorAlive) {
11482
- await orphanOrchestrator(cwd, sessionId, "orchestrator lost while daemon was down", "daemon-startup-stuck");
11483
- await completeOrchestratorCycle(cwd, sessionId);
11484
- console.log(`[sisyphus] Detected stuck session ${sessionId} on recovery: triggering orchestrator respawn`);
11485
- await onAllAgentsDone2(sessionId, cwd, session.tmuxWindowId);
11486
- }
11487
- }
11488
- }
11489
- } else {
11490
- if (session.status === "active") {
11491
- await updateSessionStatus(cwd, sessionId, "paused");
11492
- console.log(`[sisyphus] Session ${sessionId} paused: tmux window no longer exists`);
11493
- }
11494
- }
11495
- }
11496
- recovered++;
11497
- }
11498
- } catch {
11499
- console.error(`[sisyphus] Failed to read session state for ${sessionId}, skipping`);
11500
- }
11501
- }
11976
+ const allTmuxSessions = listAllSessions();
11977
+ const tmuxIdSet = new Set(allTmuxSessions.map((s) => s.sessionId));
11978
+ const tmuxNameToId = new Map(allTmuxSessions.map((s) => [s.name, s.sessionId]));
11979
+ const panesByWindow = listAllPanesByWindow();
11980
+ const results = await Promise.all(
11981
+ entries.map(([sessionId, cwd]) => recoverOneSession(sessionId, cwd, tmuxIdSet, tmuxNameToId, panesByWindow))
11982
+ );
11983
+ const recovered = results.filter(Boolean).length;
11502
11984
  console.log(`[sisyphus] Recovered ${recovered} session(s) from registry`);
11503
11985
  }
11504
11986
  async function startDaemon() {
@@ -11507,9 +11989,6 @@ async function startDaemon() {
11507
11989
  startLogRotator();
11508
11990
  installPlugin();
11509
11991
  const config = loadConfig(process.cwd());
11510
- if (config.autoUpdate !== false) {
11511
- await checkAndApply();
11512
- }
11513
11992
  acquirePidLock();
11514
11993
  const statusBarConfig = { ...DEFAULT_STATUS_BAR_CONFIG };
11515
11994
  statusBarConfig.colors = { ...DEFAULT_STATUS_BAR_CONFIG.colors, ...config.statusBar?.colors };
@@ -11527,36 +12006,45 @@ async function startDaemon() {
11527
12006
  compositor2.register(createWindowsSegment());
11528
12007
  compositor2.register(createSessionNameSegment());
11529
12008
  setCompositor(compositor2);
11530
- setDotsCallback(() => {
11531
- recomputeDots();
12009
+ let tickCounter = 0;
12010
+ const renderEvery = Math.max(1, config.statusBarRenderTicks ?? 4);
12011
+ setDotsCallback((panesByWindow) => {
11532
12012
  try {
11533
12013
  writeManifest();
11534
12014
  } catch {
11535
12015
  }
12016
+ tickCounter += 1;
12017
+ if (tickCounter % renderEvery !== 0) return;
12018
+ recomputeDots(panesByWindow);
11536
12019
  try {
11537
- compositor2.render();
12020
+ compositor2.render(panesByWindow);
11538
12021
  } catch {
11539
12022
  }
11540
12023
  });
11541
12024
  } else {
11542
- setDotsCallback(() => {
11543
- recomputeDots();
12025
+ let tickCounter = 0;
12026
+ const renderEvery = Math.max(1, config.statusBarRenderTicks ?? 4);
12027
+ setDotsCallback((panesByWindow) => {
11544
12028
  try {
11545
12029
  writeManifest();
11546
12030
  } catch {
11547
12031
  }
12032
+ tickCounter += 1;
12033
+ if (tickCounter % renderEvery !== 0) return;
12034
+ recomputeDots(panesByWindow);
11548
12035
  });
11549
12036
  }
11550
12037
  setRespawnCallback(onAllAgentsDone2);
11551
12038
  setTrackedEntriesProvider(getTrackedSessionEntries);
11552
12039
  await startServer();
11553
12040
  startMonitor(config.pollIntervalMs);
11554
- await recoverSessions();
11555
- await sweepOrphans();
11556
- startHeartbeatScanner();
11557
12041
  if (config.autoUpdate !== false) {
12042
+ void checkAndApply();
11558
12043
  startPeriodicUpdateCheck();
11559
12044
  }
12045
+ await recoverSessions();
12046
+ await sweepOrphans();
12047
+ startHeartbeatScanner();
11560
12048
  const shutdown = async () => {
11561
12049
  console.log("[sisyphus] Shutting down...");
11562
12050
  stopLogRotator();
@@ -11581,50 +12069,110 @@ async function startDaemon() {
11581
12069
  process.on("SIGINT", shutdown);
11582
12070
  }
11583
12071
  process.title = "sisyphusd";
11584
- var command = process.argv[2];
11585
- (async () => {
11586
- switch (command) {
11587
- case "stop":
11588
- stopDaemon();
11589
- break;
11590
- case "restart": {
11591
- stopDaemon();
11592
- if (isLaunchdManaged()) {
11593
- for (let i = 0; i < 6; i++) {
11594
- await sleep(500);
11595
- const respawnedPid = readPid();
11596
- if (respawnedPid) {
11597
- console.log(`[sisyphus] Daemon restarted (pid ${respawnedPid}) by process manager`);
11598
- process.exit(0);
11599
- }
11600
- }
11601
- console.log("[sisyphus] Daemon will be restarted by process manager");
12072
+ var isHelpInvocation = process.argv.includes("--help") || process.argv[2] === "help";
12073
+ if (!isHelpInvocation) {
12074
+ const ts = () => (/* @__PURE__ */ new Date()).toISOString();
12075
+ const origLog = console.log.bind(console);
12076
+ const origError = console.error.bind(console);
12077
+ console.log = (...args) => origLog(`[${ts()}]`, ...args);
12078
+ console.error = (...args) => origError(`[${ts()}]`, ...args);
12079
+ }
12080
+ var rootHelpText = `sisyphusd: long-lived orchestration daemon for sis sessions.
12081
+
12082
+ Subtrees
12083
+ start boot the daemon and recover sessions | use when no daemon is running for this user
12084
+ stop terminate the running daemon | use to free the IPC socket or before an upgrade
12085
+ restart stop, then start | use after a binary upgrade or config change
12086
+
12087
+ Globals
12088
+ --help print this message
12089
+
12090
+ I/O contract: subprocess control (no JSON; this is a process-lifecycle tool, not an agent surface).
12091
+ Exit 0 on success, non-zero on failure. Diagnostic output streams to stderr; structured payloads are absent \u2014 for session/agent state, use \`sis session inspect status\` and friends.
12092
+ `;
12093
+ var program = new Command().name("sisyphusd").description("Long-lived orchestration daemon for sis sessions.").helpOption("--help", "print this message").allowExcessArguments(false).showSuggestionAfterError(false).exitOverride((err) => {
12094
+ if (err.code === "commander.unknownCommand" || err.code === "commander.unknownOption" || err.code === "commander.excessArguments") {
12095
+ const offender = process.argv[2];
12096
+ if (offender) {
12097
+ process.stderr.write(`error: unknown command '${offender}'
12098
+ `);
12099
+ }
12100
+ process.exit(2);
12101
+ }
12102
+ if (err.code === "commander.helpDisplayed" || err.code === "commander.help") {
12103
+ process.exit(0);
12104
+ }
12105
+ process.exit(err.exitCode ?? 1);
12106
+ }).action(async () => {
12107
+ await startDaemon();
12108
+ });
12109
+ program.helpInformation = () => rootHelpText;
12110
+ program.command("help").description("Print sisyphusd help").helpOption("--help", "print this message").action(() => {
12111
+ process.stdout.write(rootHelpText);
12112
+ process.exit(0);
12113
+ });
12114
+ program.command("start").description("Boot the daemon and recover sessions").helpOption("--help", "display help for command").addHelpText("after", `
12115
+ start: launch the sisyphus daemon process.
12116
+
12117
+ Input
12118
+ None.
12119
+
12120
+ Output (subprocess control; diagnostic text to stderr; no JSON \u2014 daemon-control carve-out)
12121
+ Progress messages stream to stderr. Blocks until the daemon event loop is running and sessions are recovered.
12122
+
12123
+ Effects
12124
+ Acquires the IPC socket. Writes pid file. Recovers active sessions from registry. Starts pane monitor and heartbeat scanner.
12125
+
12126
+ Exit codes: 0 ok | non-zero on failure (diagnostic on stderr).
12127
+ `).action(async () => {
12128
+ await startDaemon();
12129
+ });
12130
+ program.command("stop").description("Terminate the running daemon").helpOption("--help", "display help for command").addHelpText("after", `
12131
+ stop: terminate the running daemon process.
12132
+
12133
+ Input
12134
+ None.
12135
+
12136
+ Output (subprocess control; diagnostic text to stderr; no JSON \u2014 daemon-control carve-out)
12137
+ Reports daemon pid and stop status to stderr. Waits up to 5s for graceful SIGTERM; falls back to SIGKILL.
12138
+
12139
+ Effects
12140
+ Sends SIGTERM (then SIGKILL if needed). Removes pid file. Releases IPC socket.
12141
+
12142
+ Exit codes: 0 ok | non-zero on failure (diagnostic on stderr).
12143
+ `).action(() => {
12144
+ stopDaemon();
12145
+ });
12146
+ program.command("restart").description("Stop, then start the daemon").helpOption("--help", "display help for command").addHelpText("after", `
12147
+ restart: stop the running daemon, then start a fresh one.
12148
+
12149
+ Input
12150
+ None.
12151
+
12152
+ Output (subprocess control; diagnostic text to stderr; no JSON \u2014 daemon-control carve-out)
12153
+ Reports stop and start progress to stderr. When launchd manages the daemon, exits after confirming respawn.
12154
+
12155
+ Effects
12156
+ Stops the running daemon (SIGTERM/SIGKILL). If launchd-managed, exits and lets launchd respawn via KeepAlive. Otherwise starts the daemon in-process.
12157
+
12158
+ Exit codes: 0 ok | non-zero on failure (diagnostic on stderr).
12159
+ `).action(async () => {
12160
+ stopDaemon();
12161
+ if (isLaunchdManaged()) {
12162
+ for (let i = 0; i < 6; i++) {
12163
+ await sleep(500);
12164
+ const respawnedPid = readPid();
12165
+ if (respawnedPid) {
12166
+ console.log(`[sisyphus] Daemon restarted (pid ${respawnedPid}) by process manager`);
11602
12167
  process.exit(0);
11603
12168
  }
11604
- await startDaemon();
11605
- break;
11606
- }
11607
- case "start":
11608
- case void 0:
11609
- await startDaemon();
11610
- break;
11611
- case "help":
11612
- case "--help":
11613
- case "-h":
11614
- console.log("Usage: sisyphusd [command]");
11615
- console.log("");
11616
- console.log("Commands:");
11617
- console.log(" start Start the daemon (default if no command given)");
11618
- console.log(" stop Stop the running daemon");
11619
- console.log(" restart Stop and restart the daemon");
11620
- console.log(" help Show this help message");
11621
- break;
11622
- default:
11623
- console.error(`[sisyphus] Unknown command: ${command}`);
11624
- console.error("Usage: sisyphusd [start|stop|restart|help]");
11625
- process.exit(1);
12169
+ }
12170
+ console.log("[sisyphus] Daemon will be restarted by process manager");
12171
+ process.exit(0);
11626
12172
  }
11627
- })().catch((err) => {
12173
+ await startDaemon();
12174
+ });
12175
+ program.parseAsync(process.argv).catch((err) => {
11628
12176
  console.error("[sisyphus] Fatal error:", err);
11629
12177
  process.exit(1);
11630
12178
  });