sisyphi 1.1.34 → 1.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -84,6 +84,9 @@ function askOutputPath(cwd, sessionId, askId) {
84
84
  function askVisualsDir(cwd, sessionId, askId) {
85
85
  return join(askEntryDir(cwd, sessionId, askId), "visuals");
86
86
  }
87
+ function tmuxSessionName(cwd, sessionLabel) {
88
+ return `ssyph_${basename(cwd)}_${sessionLabel}`;
89
+ }
87
90
  function sessionsManifestPath() {
88
91
  return join(globalDir(), "sessions-manifest.json");
89
92
  }
@@ -141,13 +144,34 @@ var init_paths = __esm({
141
144
  }
142
145
  });
143
146
 
147
+ // src/shared/shell.ts
148
+ function shellQuote(s) {
149
+ return `'${s.replace(/'/g, "'\\''")}'`;
150
+ }
151
+ function shellQuoteHomePath(path) {
152
+ if (path === "~") return "~";
153
+ if (path.startsWith("~/")) return `~/${shellQuote(path.slice(2))}`;
154
+ return shellQuote(path);
155
+ }
156
+ function validateRepoName(repo) {
157
+ return !repo.includes("/") && !repo.includes("\\") && !repo.includes("..");
158
+ }
159
+ function escapeAppleScript(s) {
160
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
161
+ }
162
+ var init_shell = __esm({
163
+ "src/shared/shell.ts"() {
164
+ "use strict";
165
+ }
166
+ });
167
+
144
168
  // src/daemon/lib/atomic.ts
145
169
  import { randomUUID as randomUUID2 } from "crypto";
146
- import { dirname as dirname4, join as join8 } from "path";
170
+ import { dirname as dirname4, join as join9 } from "path";
147
171
  import { renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
148
172
  function atomicWrite(filePath, data) {
149
173
  const dir = dirname4(filePath);
150
- const tmpPath = join8(dir, `.atomic.${randomUUID2()}.tmp`);
174
+ const tmpPath = join9(dir, `.atomic.${randomUUID2()}.tmp`);
151
175
  writeFileSync4(tmpPath, data, "utf-8");
152
176
  renameSync2(tmpPath, filePath);
153
177
  }
@@ -176,6 +200,333 @@ var init_atomic = __esm({
176
200
  }
177
201
  });
178
202
 
203
+ // src/shared/env.ts
204
+ import { resolve as resolve3 } from "path";
205
+ function augmentedPath() {
206
+ const rawPath = process.env["PATH"];
207
+ const basePath = rawPath !== void 0 && rawPath.length > 0 ? rawPath : "/usr/bin:/bin";
208
+ const home = process.env["HOME"];
209
+ const candidates = [
210
+ ...home ? [`${home}/.local/bin`] : [],
211
+ // Claude CLI, pipx, user-local installs
212
+ resolve3(process.execPath, ".."),
213
+ // Node.js bin dir (ensures node/npm available)
214
+ "/opt/homebrew/bin",
215
+ // Homebrew (Apple Silicon macOS)
216
+ "/opt/homebrew/sbin",
217
+ // Homebrew sbin
218
+ "/usr/local/bin",
219
+ // Homebrew (Intel macOS), manual installs
220
+ "/usr/local/sbin",
221
+ // Manual installs
222
+ "/opt/local/bin",
223
+ // MacPorts
224
+ "/opt/local/sbin",
225
+ // MacPorts
226
+ "/home/linuxbrew/.linuxbrew/bin"
227
+ // Linuxbrew
228
+ ];
229
+ const nixProfile = process.env["NIX_PROFILES"];
230
+ if (nixProfile) {
231
+ for (const p of nixProfile.split(" ").reverse()) {
232
+ candidates.push(`${p}/bin`);
233
+ }
234
+ }
235
+ const existing = new Set(basePath.split(":"));
236
+ const prepend = candidates.filter((dir) => !existing.has(dir));
237
+ return prepend.length > 0 ? `${prepend.join(":")}:${basePath}` : basePath;
238
+ }
239
+ function execEnv() {
240
+ return {
241
+ ...process.env,
242
+ PATH: augmentedPath()
243
+ };
244
+ }
245
+ var init_env = __esm({
246
+ "src/shared/env.ts"() {
247
+ "use strict";
248
+ }
249
+ });
250
+
251
+ // src/shared/exec.ts
252
+ import { execSync as execSync8 } from "child_process";
253
+ function exec2(cmd, cwd, timeoutMs = 3e4) {
254
+ return execSync8(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, timeout: timeoutMs }).trim();
255
+ }
256
+ function execSafe(cmd, cwd, timeoutMs) {
257
+ try {
258
+ return execSync8(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, stdio: ["pipe", "pipe", "pipe"], timeout: timeoutMs }).trim();
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ var EXEC_ENV;
264
+ var init_exec = __esm({
265
+ "src/shared/exec.ts"() {
266
+ "use strict";
267
+ init_env();
268
+ EXEC_ENV = execEnv();
269
+ }
270
+ });
271
+
272
+ // src/tui/lib/tmux.ts
273
+ var tmux_exports = {};
274
+ __export(tmux_exports, {
275
+ editInPopup: () => editInPopup,
276
+ getWindowId: () => getWindowId,
277
+ listAllWindowIds: () => listAllWindowIds,
278
+ openClaudeResumePopup: () => openClaudeResumePopup,
279
+ openClaudeResumeSession: () => openClaudeResumeSession,
280
+ openCompanionPane: () => openCompanionPane,
281
+ openEditorPopup: () => openEditorPopup,
282
+ openInFileManager: () => openInFileManager,
283
+ openLogPopup: () => openLogPopup,
284
+ openShellPopup: () => openShellPopup,
285
+ paneExists: () => paneExists,
286
+ promptInPopup: () => promptInPopup,
287
+ registerDashboardWindow: () => registerDashboardWindow,
288
+ selectPane: () => selectPane,
289
+ selectWindow: () => selectWindow,
290
+ switchToSession: () => switchToSession,
291
+ windowExists: () => windowExists
292
+ });
293
+ import { execSync as execSync17 } from "child_process";
294
+ import { join as join27 } from "path";
295
+ import { readFileSync as readFileSync29, writeFileSync as writeFileSync16, mkdtempSync, rmSync as rmSync6, cpSync as cpSync3, existsSync as existsSync26, mkdirSync as mkdirSync13 } from "fs";
296
+ import { tmpdir as tmpdir3 } from "os";
297
+ function getWindowId() {
298
+ const pane = process.env["TMUX_PANE"];
299
+ if (pane) {
300
+ return exec2(`tmux display-message -t ${shellQuote(pane)} -p "#{window_id}"`);
301
+ }
302
+ return exec2('tmux display-message -p "#{window_id}"');
303
+ }
304
+ function selectWindow(windowId) {
305
+ execSafe(`tmux select-window -t ${shellQuote(windowId)}`);
306
+ }
307
+ function selectPane(paneId) {
308
+ execSafe(`tmux select-pane -t ${shellQuote(paneId)}`);
309
+ }
310
+ function windowExists(windowId) {
311
+ return execSafe(`tmux display-message -t ${shellQuote(windowId)} -p "#{window_id}"`) !== null;
312
+ }
313
+ function listAllWindowIds() {
314
+ try {
315
+ const output = execSync17('tmux list-windows -a -F "#{window_id}"', { encoding: "utf-8", env: EXEC_ENV });
316
+ return new Set(output.trim().split("\n").filter(Boolean));
317
+ } catch {
318
+ return /* @__PURE__ */ new Set();
319
+ }
320
+ }
321
+ function registerDashboardWindow(cwd) {
322
+ const wid = getWindowId();
323
+ const pane = process.env["TMUX_PANE"];
324
+ let sessionTarget = null;
325
+ if (pane) {
326
+ sessionTarget = execSafe(`tmux display-message -t ${shellQuote(pane)} -p "#{session_id}"`)?.trim() || null;
327
+ }
328
+ if (sessionTarget) {
329
+ execSafe(`tmux set-option -t ${shellQuote(sessionTarget)} @sisyphus_dashboard ${wid}`);
330
+ } else {
331
+ execSafe(`tmux set-option @sisyphus_dashboard ${wid}`);
332
+ }
333
+ if (cwd) {
334
+ const normalizedCwd = cwd.replace(/\/+$/, "");
335
+ const target = sessionTarget;
336
+ const existing = target ? execSafe(`tmux show-options -t ${shellQuote(target)} -v @sisyphus_cwd`)?.trim() : execSafe(`tmux show-options -v @sisyphus_cwd`)?.trim();
337
+ if (!existing) {
338
+ if (target) {
339
+ execSafe(`tmux set-option -t ${shellQuote(target)} @sisyphus_cwd ${shellQuote(normalizedCwd)}`);
340
+ } else {
341
+ execSafe(`tmux set-option @sisyphus_cwd ${shellQuote(normalizedCwd)}`);
342
+ }
343
+ }
344
+ }
345
+ }
346
+ function setupCompanionPlugin() {
347
+ const srcDir = join27(import.meta.dirname, "templates", "companion-plugin");
348
+ const destDir = join27(globalDir(), "companion-plugin");
349
+ if (!existsSync26(destDir)) mkdirSync13(destDir, { recursive: true });
350
+ cpSync3(srcDir, destDir, { recursive: true });
351
+ return destDir;
352
+ }
353
+ function paneExists(paneId) {
354
+ return execSafe(`tmux display-message -t ${shellQuote(paneId)} -p "#{pane_id}"`) !== null;
355
+ }
356
+ function findCompanionPaneForCwd(normalizedCwd) {
357
+ const out = execSafe(`tmux list-panes -aF "#{pane_id} #{@sisyphus_companion}"`);
358
+ if (!out) return null;
359
+ for (const line of out.split("\n")) {
360
+ const tabIdx = line.indexOf(" ");
361
+ if (tabIdx < 0) continue;
362
+ const paneId = line.slice(0, tabIdx);
363
+ const marker = line.slice(tabIdx + 1);
364
+ if (paneId && marker === normalizedCwd) return paneId;
365
+ }
366
+ return null;
367
+ }
368
+ function openCompanionPane(cwd) {
369
+ const normalizedCwd = cwd.replace(/\/+$/, "");
370
+ const existing = findCompanionPaneForCwd(normalizedCwd);
371
+ if (existing) {
372
+ execSafe(`tmux select-pane -t ${shellQuote(existing)}`);
373
+ return;
374
+ }
375
+ const pluginDir = setupCompanionPlugin();
376
+ const templatePath2 = join27(import.meta.dirname, "templates", "dashboard-claude.md");
377
+ let template;
378
+ try {
379
+ template = readFileSync29(templatePath2, "utf-8");
380
+ } catch {
381
+ template = `You are a Sisyphus dashboard companion. Help the user manage multi-agent sessions.
382
+ Project: ${cwd}
383
+ Run \`sis list\` and \`sis status\` to see current state.`;
384
+ }
385
+ const rendered = template.replace(/\{\{CWD\}\}/g, cwd);
386
+ const promptPath = join27(globalDir(), "dashboard-companion-prompt.md");
387
+ writeFileSync16(promptPath, rendered, "utf-8");
388
+ const pathEnv = augmentedPath();
389
+ const claudeCmd = `SISYPHUS_COMPANION_CWD=${shellQuote(cwd)} PATH=${shellQuote(pathEnv)} claude --dangerously-skip-permissions --plugin-dir ${shellQuote(pluginDir)} --append-system-prompt "$(cat ${shellQuote(promptPath)})"`;
390
+ const result = exec2(
391
+ `tmux split-window -h -l 33% -P -F "#{pane_id}" -c ${shellQuote(cwd)} ${shellQuote(claudeCmd)}`
392
+ );
393
+ const newPaneId = result.trim();
394
+ if (newPaneId) {
395
+ execSafe(`tmux set-option -p -t ${shellQuote(newPaneId)} @sisyphus_companion ${shellQuote(normalizedCwd)}`);
396
+ }
397
+ }
398
+ function switchToSession(sessionName) {
399
+ execSafe(`tmux switch-client -t ${shellQuote(sessionName)}`);
400
+ }
401
+ function editInPopup(cwd, editor, opts) {
402
+ const tmpDir = mkdtempSync(join27(tmpdir3(), "sisyphus-"));
403
+ const filePath = join27(tmpDir, "input.md");
404
+ try {
405
+ writeFileSync16(filePath, opts?.content ? opts.content : "", "utf-8");
406
+ openEditorPopup(cwd, editor, filePath, opts?.size);
407
+ const result = readFileSync29(filePath, "utf-8").trim();
408
+ return result || null;
409
+ } finally {
410
+ rmSync6(tmpDir, { recursive: true, force: true });
411
+ }
412
+ }
413
+ function promptInPopup(prompt, opts) {
414
+ const { w = "50%", h = "3" } = opts ?? {};
415
+ const tmpDir = mkdtempSync(join27(tmpdir3(), "sisyphus-"));
416
+ const outFile = join27(tmpDir, "result");
417
+ try {
418
+ const script = `printf ${shellQuote(prompt + " ")} && read -r line && printf '%s' "$line" > ${shellQuote(outFile)}`;
419
+ execSync17(
420
+ `tmux display-popup -E -w ${w} -h ${h} ${shellQuote(`bash -c ${shellQuote(script)}`)}`,
421
+ { stdio: "inherit", env: EXEC_ENV }
422
+ );
423
+ if (!existsSync26(outFile)) return null;
424
+ const result = readFileSync29(outFile, "utf-8").trim();
425
+ return result || null;
426
+ } finally {
427
+ rmSync6(tmpDir, { recursive: true, force: true });
428
+ }
429
+ }
430
+ function openLogPopup() {
431
+ execSync17(
432
+ `tmux display-popup -E -w 90% -h 80% ${shellQuote("tail -f ~/.sisyphus/daemon.log")}`,
433
+ { stdio: "inherit", env: EXEC_ENV }
434
+ );
435
+ }
436
+ function openShellPopup(cwd, command) {
437
+ execSync17(
438
+ `tmux display-popup -E -w 90% -h 80% -d ${shellQuote(cwd)} ${shellQuote(`sh -c '${command.replace(/'/g, "'\\''")}; echo; echo "Press enter to close"; read'`)}`,
439
+ { stdio: "inherit", env: EXEC_ENV }
440
+ );
441
+ }
442
+ function openInFileManager(path) {
443
+ execSync17(`open ${shellQuote(path)}`, { stdio: "inherit", env: EXEC_ENV });
444
+ }
445
+ function openClaudeResumePopup(cwd, claudeSessionId, resumeEnv, resumeArgs) {
446
+ const pathEnv = augmentedPath();
447
+ const envPrefix = resumeEnv ? `${resumeEnv} && ` : "";
448
+ const args2 = resumeArgs ? `${resumeArgs} --resume ${shellQuote(claudeSessionId)}` : `--resume ${shellQuote(claudeSessionId)}`;
449
+ const cmd = `${envPrefix}PATH=${shellQuote(pathEnv)} claude ${args2}`;
450
+ execSync17(
451
+ `tmux display-popup -E -w 90% -h 80% -d ${shellQuote(cwd)} ${shellQuote(cmd)}`,
452
+ { stdio: "inherit", env: EXEC_ENV }
453
+ );
454
+ }
455
+ function openClaudeResumeSession(cwd, sessionId, claudeSessionId, sessionLabel, resumeEnv, resumeArgs, cycleNum, mode) {
456
+ const sessionName = tmuxSessionName(cwd, sessionLabel);
457
+ const cycleLabel = cycleNum != null ? `c${cycleNum}` : "";
458
+ const paneTitle = cycleLabel ? `ssph:orch ${sessionLabel} ${cycleLabel}` : `ssph:orch ${sessionLabel}`;
459
+ const existing = execSafe('tmux list-sessions -F "#{session_id}|#{session_name}"');
460
+ const existingLine = existing?.split("\n").find((line) => line.slice(line.indexOf("|") + 1) === sessionName);
461
+ if (existingLine) {
462
+ const existingSessId = existingLine.slice(0, existingLine.indexOf("|"));
463
+ execSafe(`tmux set-option -t ${shellQuote(existingSessId)} @sisyphus_cwd ${shellQuote(cwd.replace(/\/+$/, ""))}`);
464
+ execSafe(`tmux set-option -t ${shellQuote(existingSessId)} @sisyphus_session_id ${shellQuote(sessionId)}`);
465
+ const firstPaneId2 = execSafe(`tmux list-panes -t ${shellQuote(existingSessId)} -F '#{pane_id}'`)?.split("\n")[0];
466
+ if (firstPaneId2) applyOrchestratorPaneStyle(firstPaneId2, paneTitle, sessionLabel, cycleLabel, mode);
467
+ return sessionName;
468
+ }
469
+ const pathEnv = augmentedPath();
470
+ const envPrefix = resumeEnv ? `${resumeEnv} && ` : "";
471
+ const args2 = resumeArgs ? `${resumeArgs} --resume ${shellQuote(claudeSessionId)}` : `--resume ${shellQuote(claudeSessionId)}`;
472
+ const cmd = `${envPrefix}PATH=${shellQuote(pathEnv)} claude ${args2}`;
473
+ const createOut = exec2(`tmux new-session -d -s ${shellQuote(sessionName)} -n main -c ${shellQuote(cwd)} -P -F '#{session_id}|#{pane_id}' ${shellQuote(cmd)}`).trim();
474
+ const pipeIdx = createOut.indexOf("|");
475
+ const newSessId = createOut.slice(0, pipeIdx);
476
+ const firstPaneId = createOut.slice(pipeIdx + 1);
477
+ execSafe(`tmux set-option -t ${shellQuote(newSessId)} @sisyphus_cwd ${shellQuote(cwd.replace(/\/+$/, ""))}`);
478
+ execSafe(`tmux set-option -t ${shellQuote(newSessId)} @sisyphus_session_id ${shellQuote(sessionId)}`);
479
+ execSafe(`tmux set -w -t ${shellQuote(newSessId + ":")} pane-border-status top`);
480
+ execSafe(`tmux set -w -t ${shellQuote(newSessId + ":")} allow-rename off`);
481
+ execSafe(`tmux set -w -t ${shellQuote(newSessId + ":")} automatic-rename off`);
482
+ if (firstPaneId) applyOrchestratorPaneStyle(firstPaneId, paneTitle, sessionLabel, cycleLabel, mode);
483
+ return sessionName;
484
+ }
485
+ function applyOrchestratorPaneStyle(paneId, title, sessionLabel, cycleLabel, mode) {
486
+ const color = "yellow";
487
+ execSafe(`tmux select-pane -t ${shellQuote(paneId)} -T ${shellQuote(title)}`);
488
+ execSafe(`tmux set -p -t ${shellQuote(paneId)} @pane_role ${shellQuote("orch")}`);
489
+ execSafe(`tmux set -p -t ${shellQuote(paneId)} @pane_session ${shellQuote(sessionLabel)}`);
490
+ if (cycleLabel) execSafe(`tmux set -p -t ${shellQuote(paneId)} @pane_cycle ${shellQuote(cycleLabel)}`);
491
+ if (mode) execSafe(`tmux set -p -t ${shellQuote(paneId)} @pane_mode ${shellQuote(mode)}`);
492
+ const gitBranch = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null)`;
493
+ const branchSuffix = `#(cd #{pane_current_path} && git branch --show-current 2>/dev/null | grep -q . && echo ' |') ${gitBranch}`;
494
+ const homePath = `#(echo '#{pane_current_path}' | sed "s|^$HOME|~|")`;
495
+ const modeSegment = `#{?#{@pane_mode}, #[fg=${color}\\,italics]#{@pane_mode}#[default],}`;
496
+ const fmt = [
497
+ `#[bg=${color},fg=black,bold] #{@pane_role} #[default]`,
498
+ ` #[fg=${color},bold]#{@pane_session}`,
499
+ modeSegment,
500
+ ` #[default,dim]#{@pane_cycle}`,
501
+ ` ${homePath}${branchSuffix}`,
502
+ `#[default]`
503
+ ].join("");
504
+ execSafe(`tmux set -p -t ${shellQuote(paneId)} pane-border-format ${shellQuote(fmt)}`);
505
+ }
506
+ function openEditorPopup(cwd, editor, filePath, size) {
507
+ const { w = "90%", h = "90%" } = size ?? {};
508
+ const editorBin = editor.split(/\s+/)[0].split("/").pop();
509
+ if (TERMINAL_EDITORS.has(editorBin)) {
510
+ execSync17(
511
+ `tmux display-popup -E -w ${w} -h ${h} -d ${shellQuote(cwd)} ${shellQuote(`${editor} ${shellQuote(filePath)}`)}`,
512
+ { stdio: "inherit", env: EXEC_ENV }
513
+ );
514
+ } else {
515
+ execSync17(`${editor} ${shellQuote(filePath)}`, { stdio: "inherit", cwd, env: EXEC_ENV });
516
+ }
517
+ }
518
+ var TERMINAL_EDITORS;
519
+ var init_tmux = __esm({
520
+ "src/tui/lib/tmux.ts"() {
521
+ "use strict";
522
+ init_paths();
523
+ init_env();
524
+ init_shell();
525
+ init_exec();
526
+ TERMINAL_EDITORS = /* @__PURE__ */ new Set(["nvim", "vim", "vi", "nano", "emacs", "micro", "helix", "hx", "joe", "ne", "kak"]);
527
+ }
528
+ });
529
+
179
530
  // src/cli/deploy/creds.ts
180
531
  var creds_exports = {};
181
532
  __export(creds_exports, {
@@ -188,14 +539,14 @@ __export(creds_exports, {
188
539
  readTailscaleEnv: () => readTailscaleEnv,
189
540
  writeTailscaleEnv: () => writeTailscaleEnv
190
541
  });
191
- import { chmodSync as chmodSync3, existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync27 } from "fs";
542
+ import { chmodSync as chmodSync3, existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync30 } from "fs";
192
543
  import { createInterface as createInterface4 } from "readline";
193
544
  function isValidProvider(value) {
194
545
  return PROVIDERS.includes(value);
195
546
  }
196
547
  function ensureDeployDir() {
197
548
  const dir = deployDir();
198
- if (!existsSync24(dir)) mkdirSync12(dir, { recursive: true, mode: 448 });
549
+ if (!existsSync27(dir)) mkdirSync14(dir, { recursive: true, mode: 448 });
199
550
  }
200
551
  function parseEnvFile(text) {
201
552
  const out = {};
@@ -221,8 +572,8 @@ function serializeEnvFile(values) {
221
572
  return lines.join("\n") + "\n";
222
573
  }
223
574
  function readEnvFile(path) {
224
- if (!existsSync24(path)) return null;
225
- return parseEnvFile(readFileSync27(path, "utf-8"));
575
+ if (!existsSync27(path)) return null;
576
+ return parseEnvFile(readFileSync30(path, "utf-8"));
226
577
  }
227
578
  function writeEnvFile(path, values) {
228
579
  ensureDeployDir();
@@ -317,8 +668,8 @@ var init_creds = __esm({
317
668
 
318
669
  // src/cli/index.ts
319
670
  import { Command } from "commander";
320
- import { existsSync as existsSync30, mkdirSync as mkdirSync14, readFileSync as readFileSync31 } from "fs";
321
- import { dirname as dirname12, join as join27 } from "path";
671
+ import { existsSync as existsSync33, mkdirSync as mkdirSync16, readFileSync as readFileSync34 } from "fs";
672
+ import { dirname as dirname12, join as join30 } from "path";
322
673
  import { fileURLToPath as fileURLToPath5 } from "url";
323
674
 
324
675
  // src/cli/commands/start.ts
@@ -366,18 +717,18 @@ function rawSend(request, timeoutMs = 1e4) {
366
717
  // src/cli/install.ts
367
718
  init_paths();
368
719
  import { execSync as execSync3 } from "child_process";
369
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
720
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
370
721
  import { connect as connect2 } from "net";
371
- import { homedir as homedir4 } from "os";
372
- import { dirname, join as join4, resolve } from "path";
722
+ import { homedir as homedir5 } from "os";
723
+ import { dirname, join as join5, resolve } from "path";
373
724
  import { fileURLToPath } from "url";
374
725
 
375
726
  // src/cli/tmux-setup.ts
376
727
  init_paths();
377
728
  import { execSync } from "child_process";
378
- import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync } from "fs";
379
- import { homedir as homedir2 } from "os";
380
- import { join as join2 } from "path";
729
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync, chmodSync, unlinkSync } from "fs";
730
+ import { homedir as homedir3 } from "os";
731
+ import { join as join3 } from "path";
381
732
  import { createInterface } from "readline";
382
733
 
383
734
  // src/shared/keymap.ts
@@ -438,7 +789,8 @@ var KEYMAP = {
438
789
  { key: "/", label: " Search / filter", action: { type: "script", name: "sisyphus-search-reports" }, tuiAction: "search" },
439
790
  { key: " ", label: " Open popup explicitly", action: { type: "tui", action: "show-leader" } },
440
791
  { key: "y", label: " Yank \u203A", action: { type: "submenu", ref: "copy" } },
441
- { key: "c", label: " Companion \u203A", action: { type: "submenu", ref: "companion" } },
792
+ { key: "c", label: " Side claude pane", action: { type: "script", name: "sisyphus-companion-pane" }, tuiAction: "companion-pane" },
793
+ { key: "C", label: " Companion (gamification) \u203A", action: { type: "submenu", ref: "companion" } },
442
794
  { key: "o", label: " Open \u203A", action: { type: "submenu", ref: "open" } },
443
795
  { key: "a", label: " Agent \u203A", action: { type: "submenu", ref: "agent" } },
444
796
  { key: "S", label: " Session \u203A", action: { type: "submenu", ref: "session" } },
@@ -450,8 +802,7 @@ var KEYMAP = {
450
802
  title: " Companion ",
451
803
  items: [
452
804
  { key: "p", label: " profile (overlay)", action: { type: "tui", action: "companion-overlay" } },
453
- { key: "d", label: " debug (mood signals)", action: { type: "tui", action: "companion-debug" } },
454
- { key: "t", label: " open in tmux pane", action: { type: "tui", action: "companion-pane" } }
805
+ { key: "d", label: " debug (mood signals)", action: { type: "tui", action: "companion-debug" } }
455
806
  ]
456
807
  },
457
808
  copy: {
@@ -521,22 +872,41 @@ var KEYMAP = {
521
872
  }
522
873
  };
523
874
 
875
+ // src/shared/sisyphus-init-lua.ts
876
+ import { mkdirSync, existsSync, cpSync } from "fs";
877
+ import { join as join2 } from "path";
878
+ import { homedir as homedir2 } from "os";
879
+ var initLuaEnsured = false;
880
+ function ensureSisyphusInitLua() {
881
+ if (initLuaEnsured) return;
882
+ initLuaEnsured = true;
883
+ try {
884
+ const destDir = join2(homedir2(), ".config", "sisyphus");
885
+ const destPath = join2(destDir, "init.lua");
886
+ if (existsSync(destPath)) return;
887
+ mkdirSync(destDir, { recursive: true });
888
+ const srcPath = join2(import.meta.dirname, "templates", "sisyphus-init.lua");
889
+ cpSync(srcPath, destPath);
890
+ } catch {
891
+ }
892
+ }
893
+
524
894
  // src/cli/tmux-setup.ts
525
895
  var DEFAULT_CYCLE_KEY = "M-s";
526
896
  var DEFAULT_PREFIX_KEY = "C-s";
527
897
  var KEY_TABLE = "sisyphus";
528
898
  var SISYPHUS_CONF_MARKER = "# sisyphus-managed \u2014 do not edit";
529
899
  function scriptPath(name) {
530
- return join2(globalDir(), "bin", name);
900
+ return join3(globalDir(), "bin", name);
531
901
  }
532
902
  function cycleScriptPath() {
533
903
  return scriptPath("sisyphus-cycle");
534
904
  }
535
905
  function keymapJsonPath() {
536
- return join2(globalDir(), "keymap.json");
906
+ return join3(globalDir(), "keymap.json");
537
907
  }
538
908
  function writeKeymapJson() {
539
- mkdirSync(globalDir(), { recursive: true });
909
+ mkdirSync2(globalDir(), { recursive: true });
540
910
  writeFileSync(keymapJsonPath(), JSON.stringify(KEYMAP, null, 2), "utf8");
541
911
  }
542
912
  function tmuxVersionAtLeast(major, minor) {
@@ -553,7 +923,7 @@ function tmuxVersionAtLeast(major, minor) {
553
923
  function menuItemCommand(action, scriptsDir) {
554
924
  switch (action.type) {
555
925
  case "script":
556
- return `run-shell ${join2(scriptsDir, action.name)}`;
926
+ return `run-shell ${join3(scriptsDir, action.name)}`;
557
927
  case "popup": {
558
928
  const { w, h, borderStyle, title, cwd } = action.popup;
559
929
  let args2 = "-E";
@@ -562,10 +932,10 @@ function menuItemCommand(action, scriptsDir) {
562
932
  if (borderStyle) args2 += ` -S '${borderStyle}'`;
563
933
  if (title) args2 += ` -T '${title}'`;
564
934
  if (cwd === "current") args2 += ` -d '#{pane_current_path}'`;
565
- return `display-popup ${args2} ${join2(scriptsDir, action.name)}`;
935
+ return `display-popup ${args2} ${join3(scriptsDir, action.name)}`;
566
936
  }
567
937
  case "submenu":
568
- return `run-shell ${join2(scriptsDir, `sisyphus-menu-${action.ref}`)}`;
938
+ return `run-shell ${join3(scriptsDir, `sisyphus-menu-${action.ref}`)}`;
569
939
  case "tmux":
570
940
  return action.cmd;
571
941
  case "tui":
@@ -591,13 +961,13 @@ function generateTopLevelBinding(prefixKey, def, scriptsDir) {
591
961
  return `bind-key -T root ${prefixKey} display-menu -T '${def.title}' -x R -y S ${args2}`;
592
962
  }
593
963
  function sisyphusTmuxConfPath() {
594
- return join2(globalDir(), "tmux.conf");
964
+ return join3(globalDir(), "tmux.conf");
595
965
  }
596
966
  function userTmuxConfPath() {
597
- const dotfile = join2(homedir2(), ".tmux.conf");
598
- const xdg = join2(homedir2(), ".config", "tmux", "tmux.conf");
599
- if (existsSync(xdg)) return xdg;
600
- if (existsSync(dotfile)) return dotfile;
967
+ const dotfile = join3(homedir3(), ".tmux.conf");
968
+ const xdg = join3(homedir3(), ".config", "tmux", "tmux.conf");
969
+ if (existsSync2(xdg)) return xdg;
970
+ if (existsSync2(dotfile)) return dotfile;
601
971
  return null;
602
972
  }
603
973
  var CYCLE_SCRIPT = `#!/bin/bash
@@ -663,7 +1033,7 @@ resolve_home() {
663
1033
  return 1
664
1034
  }`.trim();
665
1035
  function homeScript() {
666
- const tuiPath = join2(import.meta.dirname, "tui.js");
1036
+ const tuiPath = join3(import.meta.dirname, "tui.js");
667
1037
  return `#!/bin/bash
668
1038
  # Jump to the dashboard window for the home session matching this cwd.
669
1039
  ${RESOLVE_HOME}
@@ -716,9 +1086,9 @@ fi
716
1086
  `;
717
1087
  var NEW_PROMPT_SCRIPT = `#!/bin/bash
718
1088
  # Open nvim to compose a new sisyphus task, then start a session
719
- tmpfile=$(mktemp /tmp/sisyphus-new-XXXXXX.md)
1089
+ tmpfile=$(mktemp /tmp/sisyphus-new.XXXXXX)
720
1090
  trap 'rm -f "$tmpfile"' EXIT
721
- nvim "$tmpfile"
1091
+ NVIM_APPNAME=sisyphus nvim "$tmpfile"
722
1092
  grep -q '[^[:space:]]' "$tmpfile" || exit 0
723
1093
  exec sis start "$(cat "$tmpfile")"
724
1094
  `;
@@ -747,9 +1117,9 @@ fi
747
1117
 
748
1118
  [ -z "$session_id" ] && { echo "No active sisyphus session found"; sleep 1; exit 1; }
749
1119
 
750
- tmpfile=$(mktemp /tmp/sisyphus-msg-XXXXXX.md)
1120
+ tmpfile=$(mktemp /tmp/sisyphus-msg.XXXXXX)
751
1121
  trap 'rm -f "$tmpfile"' EXIT
752
- nvim "$tmpfile"
1122
+ NVIM_APPNAME=sisyphus nvim "$tmpfile"
753
1123
  grep -q '[^[:space:]]' "$tmpfile" || exit 0
754
1124
  exec sis message --session "$session_id" "$(cat "$tmpfile")"
755
1125
  `;
@@ -806,6 +1176,12 @@ ${formatHelpForKeymap(KEYMAP)}
806
1176
  EOF_HELP
807
1177
  read -n 1 -s -r -p " Press any key to close"
808
1178
  `;
1179
+ var COMPANION_PANE_SCRIPT = `#!/bin/bash
1180
+ tmux_sid=$(tmux display-message -p '#{session_id}')
1181
+ cwd=$(tmux show-options -t "$tmux_sid" -v @sisyphus_cwd 2>/dev/null)
1182
+ [ -z "$cwd" ] && cwd=$(tmux display-message -p '#{pane_current_path}')
1183
+ exec sis companion pane --cwd "$cwd"
1184
+ `;
809
1185
  var STATUS_POPUP_SCRIPT = `#!/bin/bash
810
1186
  # Show session status \u2014 if no sisyphus session here, list all.
811
1187
  # -t targeting uses $N session id \u2014 -t <name> can substring-match under sparse env.
@@ -1005,10 +1381,10 @@ ${SESSION_RESOLVE}
1005
1381
  short_id="\${session_id:0:8}"
1006
1382
 
1007
1383
  # Optional message \u2014 leave empty to resume with no extra instructions.
1008
- tmpfile=$(mktemp /tmp/sisyphus-resume-XXXXXX.md)
1384
+ tmpfile=$(mktemp /tmp/sisyphus-resume.XXXXXX)
1009
1385
  trap 'rm -f "$tmpfile"' EXIT
1010
1386
  printf "# Resume session %s\\n# (Optional) Add follow-up instructions for the orchestrator below.\\n# Save & quit empty to resume with no message.\\n\\n" "$short_id" > "$tmpfile"
1011
- nvim "$tmpfile"
1387
+ NVIM_APPNAME=sisyphus nvim "$tmpfile"
1012
1388
 
1013
1389
  # Strip comment + blank lines to detect empty submission
1014
1390
  body=$(grep -v '^[[:space:]]*#' "$tmpfile" | sed '/^[[:space:]]*$/d')
@@ -1099,10 +1475,10 @@ var SPAWN_AGENT_SCRIPT = `#!/bin/bash
1099
1475
  # Run from a sisyphus session pane, not the home dashboard.
1100
1476
  ${SESSION_RESOLVE}
1101
1477
 
1102
- tmpfile=$(mktemp /tmp/sisyphus-spawn-XXXXXX.md)
1478
+ tmpfile=$(mktemp /tmp/sisyphus-spawn.XXXXXX)
1103
1479
  trap 'rm -f "$tmpfile"' EXIT
1104
1480
  printf "# Spawn agent in session %s\\n# Write the agent's instruction below. Empty = abort.\\n\\n" "\${session_id:0:8}" > "$tmpfile"
1105
- nvim "$tmpfile"
1481
+ NVIM_APPNAME=sisyphus nvim "$tmpfile"
1106
1482
 
1107
1483
  body=$(grep -v '^[[:space:]]*#' "$tmpfile" | sed '/^[[:space:]]*$/d')
1108
1484
  [ -z "$body" ] && exit 0
@@ -1226,9 +1602,9 @@ else
1226
1602
  (( idx >= 0 && idx < \${#entries[@]} )) || exit 0
1227
1603
  fi
1228
1604
 
1229
- tmpfile=$(mktemp /tmp/sisyphus-msg-agent-XXXX.md)
1605
+ tmpfile=$(mktemp /tmp/sisyphus-msg-agent.XXXXXX)
1230
1606
  trap 'rm -f "$tmpfile"' EXIT
1231
- nvim "$tmpfile"
1607
+ NVIM_APPNAME=sisyphus nvim "$tmpfile"
1232
1608
  grep -q '[^[:space:]]' "$tmpfile" || exit 0
1233
1609
  exec sis message --session "$session_id" --agent "\${ids[$idx]}" "$(cat "$tmpfile")"
1234
1610
  `;
@@ -1668,12 +2044,13 @@ var OPEN_SCRATCH_SCRIPT = `#!/bin/bash
1668
2044
  exec sis admin scratch
1669
2045
  `;
1670
2046
  function installScript(name, content) {
1671
- mkdirSync(join2(globalDir(), "bin"), { recursive: true });
2047
+ mkdirSync2(join3(globalDir(), "bin"), { recursive: true });
1672
2048
  const path = scriptPath(name);
1673
2049
  writeFileSync(path, content, "utf8");
1674
2050
  chmodSync(path, 493);
1675
2051
  }
1676
2052
  function installAllScripts() {
2053
+ ensureSisyphusInitLua();
1677
2054
  installScript("sisyphus-cycle", CYCLE_SCRIPT);
1678
2055
  installScript("sisyphus-home", homeScript());
1679
2056
  installScript("sisyphus-kill-pane", KILL_PANE_SCRIPT);
@@ -1688,7 +2065,7 @@ function installAllScripts() {
1688
2065
  installScript("sisyphus-open-roadmap", OPEN_ROADMAP_SCRIPT);
1689
2066
  installScript("sisyphus-open-strategy", OPEN_STRATEGY_SCRIPT);
1690
2067
  installScript("sisyphus-export-session", EXPORT_SESSION_SCRIPT);
1691
- const scriptsDir = join2(globalDir(), "bin");
2068
+ const scriptsDir = join3(globalDir(), "bin");
1692
2069
  for (const [id, def] of Object.entries(KEYMAP.submenus)) {
1693
2070
  installScript(`sisyphus-menu-${id}`, generateSubmenuScript(id, def, scriptsDir));
1694
2071
  }
@@ -1721,6 +2098,7 @@ function installAllScripts() {
1721
2098
  installScript("sisyphus-reconnect", RECONNECT_SCRIPT);
1722
2099
  installScript("sisyphus-open-scratch", OPEN_SCRATCH_SCRIPT);
1723
2100
  installScript("sisyphus-help", HELP_SCRIPT);
2101
+ installScript("sisyphus-companion-pane", COMPANION_PANE_SCRIPT);
1724
2102
  }
1725
2103
  function getExistingBinding(key, table = "root") {
1726
2104
  try {
@@ -1770,18 +2148,43 @@ async function setupTmuxKeybind(cycleKey = DEFAULT_CYCLE_KEY, prefixKey = DEFAUL
1770
2148
  message: `tmux 3.2+ required for sisyphus keybindings; got ${version}`
1771
2149
  };
1772
2150
  }
1773
- for (const [label, key] of [["cycle", cycleKey], ["prefix", prefixKey]]) {
1774
- const existing = getExistingBinding(key);
1775
- if (existing !== null && !isSisyphusBinding(existing)) {
2151
+ if (!opts.force) {
2152
+ for (const [label, key] of [["cycle", cycleKey], ["prefix", prefixKey]]) {
2153
+ const existing = getExistingBinding(key);
2154
+ if (existing !== null && !isSisyphusBinding(existing)) {
2155
+ return {
2156
+ status: "conflict",
2157
+ message: `Tmux key ${key} (${label}) is already bound to something else. Re-run with --force to overwrite, or pass an alternate cycle key (e.g. "sis admin setup-keybind M-w").`,
2158
+ existingBinding: existing,
2159
+ conflictKey: key
2160
+ };
2161
+ }
2162
+ }
2163
+ }
2164
+ const userConfPreview = userTmuxConfPath();
2165
+ const sisyphusConfPathPreview = sisyphusTmuxConfPath();
2166
+ const markedSourceLinePreview = `source-file ${sisyphusConfPathPreview} ${SISYPHUS_CONF_MARKER}`;
2167
+ if (userConfPreview !== null && !opts.force && !opts.assumeYes) {
2168
+ let alreadySources = false;
2169
+ try {
2170
+ alreadySources = readFileSync(userConfPreview, "utf8").includes(sisyphusConfPathPreview);
2171
+ } catch {
2172
+ }
2173
+ if (!alreadySources) {
1776
2174
  return {
1777
- status: "conflict",
1778
- message: `Tmux key ${key} (${label}) is already bound to something else. Run "sis admin setup-keybind <key>" to use a different key.`,
1779
- existingBinding: existing
2175
+ status: "requires-force",
2176
+ reason: "would-modify-user-conf",
2177
+ userConf: userConfPreview,
2178
+ manualLine: markedSourceLinePreview,
2179
+ message: `Refusing to modify ${userConfPreview} without explicit consent.
2180
+ Re-run with --force (persist) or --yes (same effect, conf-append only) to append:
2181
+ ${markedSourceLinePreview}
2182
+ Or run "sis admin check-keybinds" for the full decision tree (live-only install is also an option).`
1780
2183
  };
1781
2184
  }
1782
2185
  }
1783
2186
  writeKeymapJson();
1784
- const scriptsDir = join2(globalDir(), "bin");
2187
+ const scriptsDir = join3(globalDir(), "bin");
1785
2188
  const bindings = [
1786
2189
  // C-s → display-menu top-level (descriptor-driven)
1787
2190
  generateTopLevelBinding(prefixKey, KEYMAP.topLevel, scriptsDir),
@@ -1803,7 +2206,7 @@ ${bindings.join("\n")}
1803
2206
  if (contents.includes(confPath)) {
1804
2207
  persistedToConf = true;
1805
2208
  } else {
1806
- const shouldAppend = opts.assumeYes ? true : await confirmConfAppend(userConf, markedSourceLine);
2209
+ const shouldAppend = opts.assumeYes || opts.force ? true : await confirmConfAppend(userConf, markedSourceLine);
1807
2210
  if (shouldAppend) {
1808
2211
  const separator = contents.endsWith("\n") ? "" : "\n";
1809
2212
  writeFileSync(userConf, `${contents}${separator}${markedSourceLine}
@@ -1846,8 +2249,8 @@ Note: No tmux.conf found. Add this to your tmux config for persistence:
1846
2249
  }
1847
2250
  function removeTmuxKeybind() {
1848
2251
  const confPath = sisyphusTmuxConfPath();
1849
- for (const candidate of [join2(homedir2(), ".tmux.conf"), join2(homedir2(), ".config", "tmux", "tmux.conf")]) {
1850
- if (existsSync(candidate)) {
2252
+ for (const candidate of [join3(homedir3(), ".tmux.conf"), join3(homedir3(), ".config", "tmux", "tmux.conf")]) {
2253
+ if (existsSync2(candidate)) {
1851
2254
  const contents = readFileSync(candidate, "utf8");
1852
2255
  const filtered = contents.split("\n").filter((line) => !line.includes(confPath)).join("\n");
1853
2256
  if (filtered !== contents) {
@@ -1855,7 +2258,7 @@ function removeTmuxKeybind() {
1855
2258
  }
1856
2259
  }
1857
2260
  }
1858
- if (existsSync(confPath)) {
2261
+ if (existsSync2(confPath)) {
1859
2262
  unlinkSync(confPath);
1860
2263
  }
1861
2264
  try {
@@ -1871,7 +2274,7 @@ function removeTmuxKeybind() {
1871
2274
  } catch {
1872
2275
  }
1873
2276
  const kmPath = keymapJsonPath();
1874
- if (existsSync(kmPath)) unlinkSync(kmPath);
2277
+ if (existsSync2(kmPath)) unlinkSync(kmPath);
1875
2278
  const scripts = [
1876
2279
  "sisyphus-cycle",
1877
2280
  "sisyphus-home",
@@ -1922,7 +2325,7 @@ function removeTmuxKeybind() {
1922
2325
  ];
1923
2326
  for (const name of scripts) {
1924
2327
  const path = scriptPath(name);
1925
- if (existsSync(path)) unlinkSync(path);
2328
+ if (existsSync2(path)) unlinkSync(path);
1926
2329
  }
1927
2330
  }
1928
2331
 
@@ -1987,10 +2390,10 @@ function loadConfig(cwd) {
1987
2390
  // src/daemon/plugins.ts
1988
2391
  import { readFileSync as readFileSync3 } from "fs";
1989
2392
  import { execFileSync } from "child_process";
1990
- import { homedir as homedir3 } from "os";
1991
- import { join as join3 } from "path";
2393
+ import { homedir as homedir4 } from "os";
2394
+ import { join as join4 } from "path";
1992
2395
  function installedPluginsPath() {
1993
- return join3(homedir3(), ".claude", "plugins", "installed_plugins.json");
2396
+ return join4(homedir4(), ".claude", "plugins", "installed_plugins.json");
1994
2397
  }
1995
2398
  function resolveInstalledPlugin(name) {
1996
2399
  let data;
@@ -2071,10 +2474,10 @@ async function ensureRequiredPlugins(cwd) {
2071
2474
  var PLIST_LABEL = "com.sisyphus.daemon";
2072
2475
  var PLIST_FILENAME = `${PLIST_LABEL}.plist`;
2073
2476
  function launchAgentDir() {
2074
- return join4(homedir4(), "Library", "LaunchAgents");
2477
+ return join5(homedir5(), "Library", "LaunchAgents");
2075
2478
  }
2076
2479
  function plistPath() {
2077
- return join4(launchAgentDir(), PLIST_FILENAME);
2480
+ return join5(launchAgentDir(), PLIST_FILENAME);
2078
2481
  }
2079
2482
  function daemonBinPath() {
2080
2483
  const installDir = dirname(fileURLToPath(import.meta.url));
@@ -2105,7 +2508,7 @@ function generatePlist(nodePath, daemonPath, logPath) {
2105
2508
  `;
2106
2509
  }
2107
2510
  function isInstalled() {
2108
- return existsSync2(plistPath());
2511
+ return existsSync3(plistPath());
2109
2512
  }
2110
2513
  async function ensureDaemonInstalled() {
2111
2514
  if (process.platform !== "darwin") return;
@@ -2114,8 +2517,8 @@ async function ensureDaemonInstalled() {
2114
2517
  const nodePath = process.execPath;
2115
2518
  const daemonPath = daemonBinPath();
2116
2519
  const logPath = daemonLogPath();
2117
- mkdirSync2(globalDir(), { recursive: true });
2118
- mkdirSync2(launchAgentDir(), { recursive: true });
2520
+ mkdirSync3(globalDir(), { recursive: true });
2521
+ mkdirSync3(launchAgentDir(), { recursive: true });
2119
2522
  const plist = generatePlist(nodePath, daemonPath, logPath);
2120
2523
  writeFileSync2(plistPath(), plist, "utf8");
2121
2524
  execSync3(`launchctl load -w ${plistPath()}`);
@@ -2131,7 +2534,7 @@ async function uninstallDaemon(purge) {
2131
2534
  return;
2132
2535
  }
2133
2536
  const plist = plistPath();
2134
- if (existsSync2(plist)) {
2537
+ if (existsSync3(plist)) {
2135
2538
  try {
2136
2539
  execSync3(`launchctl unload -w ${plist}`, { stdio: "pipe" });
2137
2540
  } catch {
@@ -2144,7 +2547,7 @@ async function uninstallDaemon(purge) {
2144
2547
  removeTmuxKeybind();
2145
2548
  if (purge) {
2146
2549
  const dir = globalDir();
2147
- if (existsSync2(dir)) {
2550
+ if (existsSync3(dir)) {
2148
2551
  rmSync(dir, { recursive: true, force: true });
2149
2552
  console.log(`Removed ${dir}`);
2150
2553
  }
@@ -2158,8 +2561,9 @@ function printGettingStarted(keybindResult, sisyphusPlugin) {
2158
2561
  ];
2159
2562
  if (keybindResult.status === "installed") {
2160
2563
  lines.push(`Tmux keybind: ${keybindResult.message}`, "");
2161
- } else if (keybindResult.status === "conflict") {
2564
+ } else if (keybindResult.status === "conflict" || keybindResult.status === "requires-force") {
2162
2565
  lines.push(`Keybind: ${keybindResult.message}`, "");
2566
+ lines.push("Run `sis admin check-keybinds` to see options, then `sis admin setup --force`.", "");
2163
2567
  } else if (keybindResult.status === "conf-modification-declined") {
2164
2568
  lines.push(keybindResult.message, "");
2165
2569
  }
@@ -2195,7 +2599,7 @@ async function waitForDaemon(maxWaitMs = 6e3) {
2195
2599
  let updatingLogged = false;
2196
2600
  while (Date.now() - start < maxWaitMs) {
2197
2601
  const updatingPath = daemonUpdatingPath();
2198
- if (existsSync2(updatingPath)) {
2602
+ if (existsSync3(updatingPath)) {
2199
2603
  if (!updatingLogged) {
2200
2604
  try {
2201
2605
  const version = readFileSync4(updatingPath, "utf-8").trim();
@@ -2207,7 +2611,7 @@ async function waitForDaemon(maxWaitMs = 6e3) {
2207
2611
  }
2208
2612
  maxWaitMs = Math.max(maxWaitMs, 3e4);
2209
2613
  }
2210
- if (existsSync2(socketPath())) {
2614
+ if (existsSync3(socketPath())) {
2211
2615
  try {
2212
2616
  await testConnection();
2213
2617
  return;
@@ -2322,25 +2726,13 @@ function getTmuxSessionInfo() {
2322
2726
  return { id: out.slice(0, pipeIdx), name: out.slice(pipeIdx + 1) };
2323
2727
  }
2324
2728
 
2325
- // src/shared/shell.ts
2326
- function shellQuote(s) {
2327
- return `'${s.replace(/'/g, "'\\''")}'`;
2328
- }
2329
- function shellQuoteHomePath(path) {
2330
- if (path === "~") return "~";
2331
- if (path.startsWith("~/")) return `~/${shellQuote(path.slice(2))}`;
2332
- return shellQuote(path);
2333
- }
2334
- function validateRepoName(repo) {
2335
- return !repo.includes("/") && !repo.includes("\\") && !repo.includes("..");
2336
- }
2337
- function escapeAppleScript(s) {
2338
- return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2339
- }
2729
+ // src/cli/commands/start.ts
2730
+ init_shell();
2340
2731
 
2341
2732
  // src/cli/commands/dashboard.ts
2342
- import { join as join5 } from "path";
2733
+ import { join as join6 } from "path";
2343
2734
  import { execSync as execSync5 } from "child_process";
2735
+ init_shell();
2344
2736
  function openDashboardWindow(tmuxSession, cwd) {
2345
2737
  try {
2346
2738
  const storedId = execSync5(
@@ -2360,7 +2752,7 @@ function openDashboardWindow(tmuxSession, cwd) {
2360
2752
  }
2361
2753
  } catch {
2362
2754
  }
2363
- const tuiPath = join5(import.meta.dirname, "tui.js");
2755
+ const tuiPath = join6(import.meta.dirname, "tui.js");
2364
2756
  const windowId = execSync5(
2365
2757
  `tmux new-window -t ${shellQuote(tmuxSession + ":")} -n "sisyphus-dashboard" -c ${shellQuote(cwd)} -P -F "#{window_id}"`,
2366
2758
  { encoding: "utf-8" }
@@ -2378,7 +2770,7 @@ function openDashboardWindow(tmuxSession, cwd) {
2378
2770
  function registerDashboard(program2) {
2379
2771
  program2.command("dashboard").description("Launch the TUI dashboard for monitoring and managing sessions").action(async () => {
2380
2772
  assertTmux();
2381
- const tuiPath = join5(import.meta.dirname, "tui.js");
2773
+ const tuiPath = join6(import.meta.dirname, "tui.js");
2382
2774
  execSync5(`node ${shellQuote(tuiPath)} --cwd ${shellQuote(process.cwd())}`, {
2383
2775
  stdio: "inherit"
2384
2776
  });
@@ -2471,10 +2863,10 @@ function registerStart(program2) {
2471
2863
  const sessionId = response.data?.sessionId;
2472
2864
  console.log(`Task handed off to sisyphus orchestrator (session ${sessionId})`);
2473
2865
  if (opts.tmuxCheck === false) {
2474
- const tmuxSessionName = response.data?.tmuxSessionName;
2475
- if (tmuxSessionName) {
2476
- console.log(`Tmux session: ${tmuxSessionName}`);
2477
- console.log(` tmux attach -t ${tmuxSessionName}`);
2866
+ const tmuxSessionName2 = response.data?.tmuxSessionName;
2867
+ if (tmuxSessionName2) {
2868
+ console.log(`Tmux session: ${tmuxSessionName2}`);
2869
+ console.log(` tmux attach -t ${tmuxSessionName2}`);
2478
2870
  }
2479
2871
  console.log(`Monitor: sis status ${sessionId}`);
2480
2872
  return;
@@ -2526,7 +2918,7 @@ function registerStart(program2) {
2526
2918
 
2527
2919
  // src/cli/commands/status.ts
2528
2920
  import { execSync as execSync7 } from "child_process";
2529
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
2921
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
2530
2922
 
2531
2923
  // src/shared/utils.ts
2532
2924
  function computeActiveTimeMs(session2) {
@@ -2672,7 +3064,7 @@ function readRoadmap(cwd, sessionId) {
2672
3064
  function readCycleLog(cwd, sessionId, cycle) {
2673
3065
  try {
2674
3066
  const path = cycleLogPath(cwd, sessionId, cycle);
2675
- if (!existsSync3(path)) return null;
3067
+ if (!existsSync4(path)) return null;
2676
3068
  return readFileSync5(path, "utf8");
2677
3069
  } catch {
2678
3070
  return null;
@@ -2932,16 +3324,16 @@ function registerTell(program2) {
2932
3324
  }
2933
3325
 
2934
3326
  // src/cli/commands/read.ts
2935
- import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
2936
- import { homedir as homedir5 } from "os";
2937
- import { join as join6 } from "path";
3327
+ import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
3328
+ import { homedir as homedir6 } from "os";
3329
+ import { join as join7 } from "path";
2938
3330
  var ORCH_ALIASES2 = /* @__PURE__ */ new Set(["orchestrator", "orch", "o"]);
2939
3331
  var TURN_TYPES = /* @__PURE__ */ new Set(["user", "assistant"]);
2940
3332
  function projectDirFromCwd(cwd) {
2941
3333
  return cwd.replace(/\//g, "-");
2942
3334
  }
2943
3335
  function transcriptPath(cwd, claudeSessionId) {
2944
- return join6(homedir5(), ".claude", "projects", projectDirFromCwd(cwd), `${claudeSessionId}.jsonl`);
3336
+ return join7(homedir6(), ".claude", "projects", projectDirFromCwd(cwd), `${claudeSessionId}.jsonl`);
2945
3337
  }
2946
3338
  function truncate(s, max) {
2947
3339
  if (s.length <= max) return s;
@@ -3057,7 +3449,7 @@ function registerRead(program2) {
3057
3449
  process.exit(1);
3058
3450
  }
3059
3451
  const path = transcriptPath(session2.cwd, claudeSessionId);
3060
- if (!existsSync4(path)) {
3452
+ if (!existsSync5(path)) {
3061
3453
  console.error(`Error: transcript not found at ${path}`);
3062
3454
  process.exit(1);
3063
3455
  }
@@ -3145,13 +3537,13 @@ function registerMessage(program2) {
3145
3537
  }
3146
3538
 
3147
3539
  // src/cli/commands/ask.ts
3148
- import { existsSync as existsSync10, readFileSync as readFileSync13, watchFile, unwatchFile } from "fs";
3149
- import { join as join12, resolve as resolve4 } from "path";
3540
+ import { existsSync as existsSync11, readFileSync as readFileSync13, watchFile, unwatchFile } from "fs";
3541
+ import { join as join13, resolve as resolve4 } from "path";
3150
3542
  import { ulid } from "ulid";
3151
3543
 
3152
3544
  // src/shared/ask-schema.ts
3153
3545
  import { spawnSync as spawnSync2 } from "child_process";
3154
- import { existsSync as existsSync5, lstatSync, readFileSync as readFileSync8, realpathSync } from "fs";
3546
+ import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync8, realpathSync } from "fs";
3155
3547
  import { dirname as dirname2, resolve as resolve2, sep } from "path";
3156
3548
  import { z } from "zod";
3157
3549
  var interactionOptionSchema = z.object({
@@ -3219,7 +3611,7 @@ function runTermrenderCheck(content) {
3219
3611
  function inlineBodyPath(deckPath, bodyPath) {
3220
3612
  const deckDir = dirname2(deckPath);
3221
3613
  const joined = resolve2(deckDir, bodyPath);
3222
- if (!existsSync5(joined)) {
3614
+ if (!existsSync6(joined)) {
3223
3615
  throw new Error(
3224
3616
  `bodyPath does not exist: '${bodyPath}' (resolved against deck dir '${deckDir}'). bodyPath is interpreted relative to the deck JSON's directory; place the body file there and use a relative path (e.g. "completion-summary.md").`
3225
3617
  );
@@ -3266,17 +3658,17 @@ function parseDeck(deckPath) {
3266
3658
 
3267
3659
  // src/daemon/ask-store.ts
3268
3660
  init_paths();
3269
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync12, readdirSync as readdirSync3 } from "fs";
3661
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync12, readdirSync as readdirSync3 } from "fs";
3270
3662
 
3271
3663
  // src/daemon/history.ts
3272
3664
  init_paths();
3273
- import { appendFileSync, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, renameSync, readdirSync, readFileSync as readFileSync9, rmSync as rmSync2, statSync } from "fs";
3665
+ import { appendFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, renameSync, readdirSync, readFileSync as readFileSync9, rmSync as rmSync2, statSync } from "fs";
3274
3666
  import { randomUUID } from "crypto";
3275
- import { dirname as dirname3, join as join7 } from "path";
3667
+ import { dirname as dirname3, join as join8 } from "path";
3276
3668
  var knownDirs = /* @__PURE__ */ new Set();
3277
3669
  function ensureDir(sessionId) {
3278
3670
  if (knownDirs.has(sessionId)) return;
3279
- mkdirSync3(historySessionDir(sessionId), { recursive: true });
3671
+ mkdirSync4(historySessionDir(sessionId), { recursive: true });
3280
3672
  knownDirs.add(sessionId);
3281
3673
  }
3282
3674
  function emitHistoryEvent(sessionId, event, data) {
@@ -3291,12 +3683,12 @@ function emitHistoryEvent(sessionId, event, data) {
3291
3683
  // src/daemon/state.ts
3292
3684
  init_atomic();
3293
3685
  init_paths();
3294
- import { copyFileSync, cpSync, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync2, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
3295
- import { join as join10 } from "path";
3686
+ import { copyFileSync, cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync11, readdirSync as readdirSync2, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
3687
+ import { join as join11 } from "path";
3296
3688
 
3297
3689
  // src/shared/gitignore.ts
3298
- import { existsSync as existsSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
3299
- import { join as join9 } from "path";
3690
+ import { existsSync as existsSync7, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
3691
+ import { join as join10 } from "path";
3300
3692
 
3301
3693
  // src/shared/types.ts
3302
3694
  var ORCHESTRATOR_ASKED_BY = "orchestrator";
@@ -3358,10 +3750,11 @@ async function incrementUserBlockedMs(cwd, sessionId, deltaMs, askedAt, askedBy)
3358
3750
  init_atomic();
3359
3751
 
3360
3752
  // src/daemon/notify.ts
3753
+ init_shell();
3361
3754
  import { spawn, execFile } from "child_process";
3362
- import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync5, existsSync as existsSync8 } from "fs";
3363
- import { join as join11 } from "path";
3364
- import { homedir as homedir6 } from "os";
3755
+ import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync9 } from "fs";
3756
+ import { join as join12 } from "path";
3757
+ import { homedir as homedir7 } from "os";
3365
3758
  var TMUX_SOCKET = `/tmp/tmux-${process.getuid?.() ?? 0}/default`;
3366
3759
  var SWITCH_SCRIPT = [
3367
3760
  "#!/bin/bash",
@@ -3403,24 +3796,24 @@ var SWITCH_SCRIPT = [
3403
3796
  ""
3404
3797
  ].join("\n");
3405
3798
  function ensureSwitchScript() {
3406
- const dir = join11(homedir6(), ".sisyphus");
3407
- const scriptPath2 = join11(dir, "notify-switch.sh");
3799
+ const dir = join12(homedir7(), ".sisyphus");
3800
+ const scriptPath2 = join12(dir, "notify-switch.sh");
3408
3801
  try {
3409
- mkdirSync5(dir, { recursive: true });
3802
+ mkdirSync6(dir, { recursive: true });
3410
3803
  writeFileSync7(scriptPath2, SWITCH_SCRIPT, { mode: 493 });
3411
3804
  } catch {
3412
3805
  }
3413
3806
  }
3414
3807
  var notifyProcess = null;
3415
3808
  function getNotifyBinary() {
3416
- return join11(homedir6(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
3809
+ return join12(homedir7(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
3417
3810
  }
3418
3811
  function ensureNotifyProcess() {
3419
3812
  if (notifyProcess && !notifyProcess.killed && notifyProcess.stdin?.writable) {
3420
3813
  return notifyProcess;
3421
3814
  }
3422
3815
  const binary = getNotifyBinary();
3423
- if (!existsSync8(binary)) {
3816
+ if (!existsSync9(binary)) {
3424
3817
  return null;
3425
3818
  }
3426
3819
  notifyProcess = spawn(binary, [], {
@@ -3500,7 +3893,7 @@ function maybeNotifyOnAskCreated(cwd, sessionId, meta) {
3500
3893
  }
3501
3894
  }
3502
3895
  function createAsk(cwd, sessionId, params) {
3503
- mkdirSync6(askVisualsDir(cwd, sessionId, params.askId), { recursive: true });
3896
+ mkdirSync7(askVisualsDir(cwd, sessionId, params.askId), { recursive: true });
3504
3897
  const askedAt = (/* @__PURE__ */ new Date()).toISOString();
3505
3898
  const meta = {
3506
3899
  askId: params.askId,
@@ -3547,7 +3940,7 @@ function writeOutput(cwd, sessionId, askId, responses, completedAt) {
3547
3940
  }
3548
3941
  function readMeta(cwd, sessionId, askId) {
3549
3942
  const p = askMetaPath(cwd, sessionId, askId);
3550
- if (!existsSync9(p)) {
3943
+ if (!existsSync10(p)) {
3551
3944
  return null;
3552
3945
  }
3553
3946
  return JSON.parse(readFileSync12(p, "utf-8"));
@@ -3574,7 +3967,7 @@ function buildAutoResponses(deck) {
3574
3967
  }
3575
3968
  async function autoResolveAsk(cwd, sessionId, askId, deck) {
3576
3969
  try {
3577
- if (existsSync9(askOutputPath(cwd, sessionId, askId))) return false;
3970
+ if (existsSync10(askOutputPath(cwd, sessionId, askId))) return false;
3578
3971
  const d = deck ?? readDecisions(cwd, sessionId, askId);
3579
3972
  if (!d) return false;
3580
3973
  const responses = buildAutoResponses(d);
@@ -3600,64 +3993,8 @@ async function maybeAutoResolveAsk(cwd, sessionId, askId, deck) {
3600
3993
 
3601
3994
  // src/cli/commands/ask.ts
3602
3995
  init_paths();
3603
-
3604
- // src/shared/exec.ts
3605
- import { execSync as execSync8 } from "child_process";
3606
-
3607
- // src/shared/env.ts
3608
- import { resolve as resolve3 } from "path";
3609
- function augmentedPath() {
3610
- const rawPath = process.env["PATH"];
3611
- const basePath = rawPath !== void 0 && rawPath.length > 0 ? rawPath : "/usr/bin:/bin";
3612
- const home = process.env["HOME"];
3613
- const candidates = [
3614
- ...home ? [`${home}/.local/bin`] : [],
3615
- // Claude CLI, pipx, user-local installs
3616
- resolve3(process.execPath, ".."),
3617
- // Node.js bin dir (ensures node/npm available)
3618
- "/opt/homebrew/bin",
3619
- // Homebrew (Apple Silicon macOS)
3620
- "/opt/homebrew/sbin",
3621
- // Homebrew sbin
3622
- "/usr/local/bin",
3623
- // Homebrew (Intel macOS), manual installs
3624
- "/usr/local/sbin",
3625
- // Manual installs
3626
- "/opt/local/bin",
3627
- // MacPorts
3628
- "/opt/local/sbin",
3629
- // MacPorts
3630
- "/home/linuxbrew/.linuxbrew/bin"
3631
- // Linuxbrew
3632
- ];
3633
- const nixProfile = process.env["NIX_PROFILES"];
3634
- if (nixProfile) {
3635
- for (const p of nixProfile.split(" ").reverse()) {
3636
- candidates.push(`${p}/bin`);
3637
- }
3638
- }
3639
- const existing = new Set(basePath.split(":"));
3640
- const prepend = candidates.filter((dir) => !existing.has(dir));
3641
- return prepend.length > 0 ? `${prepend.join(":")}:${basePath}` : basePath;
3642
- }
3643
- function execEnv() {
3644
- return {
3645
- ...process.env,
3646
- PATH: augmentedPath()
3647
- };
3648
- }
3649
-
3650
- // src/shared/exec.ts
3651
- var EXEC_ENV = execEnv();
3652
- function execSafe(cmd, cwd, timeoutMs) {
3653
- try {
3654
- return execSync8(cmd, { encoding: "utf-8", env: EXEC_ENV, cwd, stdio: ["pipe", "pipe", "pipe"], timeout: timeoutMs }).trim();
3655
- } catch {
3656
- return null;
3657
- }
3658
- }
3659
-
3660
- // src/cli/commands/ask.ts
3996
+ init_exec();
3997
+ init_shell();
3661
3998
  var ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
3662
3999
  function validateAskId(askId) {
3663
4000
  if (!ULID_RE.test(askId)) {
@@ -3726,7 +4063,7 @@ function mintAskId() {
3726
4063
  return ulid();
3727
4064
  }
3728
4065
  function resolveClaudeSessionId(cwd, sessionId, askedBy) {
3729
- if (!existsSync10(statePath(cwd, sessionId))) return void 0;
4066
+ if (!existsSync11(statePath(cwd, sessionId))) return void 0;
3730
4067
  const session2 = getSession(cwd, sessionId);
3731
4068
  if (askedBy === ORCHESTRATOR_ASKED_BY) {
3732
4069
  const last = session2.orchestratorCycles[session2.orchestratorCycles.length - 1];
@@ -3763,7 +4100,7 @@ async function markAnswered(cwd, sessionId, askId) {
3763
4100
  });
3764
4101
  if (meta.blocking && durationMs > 0) {
3765
4102
  try {
3766
- if (existsSync10(statePath(cwd, sessionId))) {
4103
+ if (existsSync11(statePath(cwd, sessionId))) {
3767
4104
  await incrementUserBlockedMs(cwd, sessionId, durationMs, meta.askedAt, meta.askedBy);
3768
4105
  }
3769
4106
  } catch {
@@ -3772,7 +4109,7 @@ async function markAnswered(cwd, sessionId, askId) {
3772
4109
  }
3773
4110
  function waitForOutput(cwd, sessionId, askId, initialPpid) {
3774
4111
  const outputPath = askOutputPath(cwd, sessionId, askId);
3775
- if (existsSync10(outputPath)) {
4112
+ if (existsSync11(outputPath)) {
3776
4113
  return Promise.resolve(JSON.parse(readFileSync13(outputPath, "utf-8")));
3777
4114
  }
3778
4115
  return new Promise((res, _rej) => {
@@ -3783,7 +4120,7 @@ function waitForOutput(cwd, sessionId, askId, initialPpid) {
3783
4120
  process.removeListener("SIGINT", onSigint);
3784
4121
  };
3785
4122
  const onChange = () => {
3786
- if (!existsSync10(outputPath)) return;
4123
+ if (!existsSync11(outputPath)) return;
3787
4124
  try {
3788
4125
  const out = JSON.parse(readFileSync13(outputPath, "utf-8"));
3789
4126
  cleanup();
@@ -3816,7 +4153,7 @@ function maybeSpawnAskPane(cwd, sessionId, askId) {
3816
4153
  const callerPane = process.env.TMUX_PANE;
3817
4154
  if (!callerPane) return;
3818
4155
  if (process.env.SISYPHUS_DISABLE_ASK_PANE === "1") return;
3819
- const tuiPath = join12(import.meta.dirname, "tui.js");
4156
+ const tuiPath = join13(import.meta.dirname, "tui.js");
3820
4157
  const cmd = `node ${shellQuote(tuiPath)} --cwd ${shellQuote(cwd)} --session-id ${shellQuote(sessionId)} --ask ${shellQuote(askId)}`;
3821
4158
  execSafe(`tmux split-window -d -h -t ${shellQuote(callerPane)} -c ${shellQuote(cwd)} ${shellQuote(cmd)}`);
3822
4159
  }
@@ -3824,7 +4161,7 @@ async function submit(file, opts) {
3824
4161
  const { cwd, sessionId } = resolveSessionEnv(opts);
3825
4162
  const askedBy = process.env.SISYPHUS_AGENT_ID ?? ORCHESTRATOR_ASKED_BY;
3826
4163
  const deckPath = resolve4(file);
3827
- if (!existsSync10(deckPath)) {
4164
+ if (!existsSync11(deckPath)) {
3828
4165
  console.error(`Error: deck file not found: ${deckPath}`);
3829
4166
  process.exit(1);
3830
4167
  }
@@ -3883,7 +4220,7 @@ async function peek(askId, opts) {
3883
4220
  };
3884
4221
  if (meta.completedAt) result.completedAt = meta.completedAt;
3885
4222
  try {
3886
- if (existsSync10(outputPath)) {
4223
+ if (existsSync11(outputPath)) {
3887
4224
  result.output = JSON.parse(readFileSync13(outputPath, "utf-8"));
3888
4225
  }
3889
4226
  } catch (err) {
@@ -3951,11 +4288,11 @@ function registerResume(program2) {
3951
4288
  const request = { type: "resume", sessionId, cwd, message };
3952
4289
  const response = await sendRequest(request);
3953
4290
  if (response.ok) {
3954
- const tmuxSessionName = response.data?.tmuxSessionName;
4291
+ const tmuxSessionName2 = response.data?.tmuxSessionName;
3955
4292
  console.log(`Session ${sessionId} resumed`);
3956
- if (tmuxSessionName) {
3957
- console.log(`Tmux session: ${tmuxSessionName}`);
3958
- console.log(` tmux attach -t ${tmuxSessionName}`);
4293
+ if (tmuxSessionName2) {
4294
+ console.log(`Tmux session: ${tmuxSessionName2}`);
4295
+ console.log(` tmux attach -t ${tmuxSessionName2}`);
3959
4296
  }
3960
4297
  } else {
3961
4298
  console.error(`Error: ${response.error}`);
@@ -4038,9 +4375,9 @@ function registerReconnect(program2) {
4038
4375
  const request = { type: "reconnect", sessionId, cwd };
4039
4376
  const response = await sendRequest(request);
4040
4377
  if (response.ok) {
4041
- const tmuxSessionName = response.data?.tmuxSessionName;
4378
+ const tmuxSessionName2 = response.data?.tmuxSessionName;
4042
4379
  const tmuxWindowId = response.data?.tmuxWindowId;
4043
- console.log(`Reconnected to ${tmuxSessionName} (window ${tmuxWindowId})`);
4380
+ console.log(`Reconnected to ${tmuxSessionName2} (window ${tmuxWindowId})`);
4044
4381
  } else {
4045
4382
  console.error(`Error: ${response.error}`);
4046
4383
  process.exit(1);
@@ -4130,6 +4467,65 @@ function registerSessionEffort(program2) {
4130
4467
  });
4131
4468
  }
4132
4469
 
4470
+ // src/cli/commands/set-dangerous.ts
4471
+ var VALID_STATES = ["on", "off", "toggle"];
4472
+ function registerSessionDangerous(program2) {
4473
+ program2.command("dangerous [sessionId] [state]").description("Toggle dangerous mode (auto-accept first option for every ask). state: on|off|toggle (default: toggle)").action(async (sessionIdArg, stateArg) => {
4474
+ let sessionId;
4475
+ if (sessionIdArg) {
4476
+ sessionId = sessionIdArg;
4477
+ } else if (process.env.SISYPHUS_SESSION_ID) {
4478
+ sessionId = process.env.SISYPHUS_SESSION_ID;
4479
+ } else {
4480
+ console.error("Error: provide <sessionId> or set SISYPHUS_SESSION_ID environment variable");
4481
+ process.exit(1);
4482
+ }
4483
+ let state;
4484
+ if (!stateArg) {
4485
+ state = "toggle";
4486
+ } else {
4487
+ const stateInput = stateArg.toLowerCase();
4488
+ if (!VALID_STATES.includes(stateInput)) {
4489
+ console.error(`Error: state must be one of: ${VALID_STATES.join(", ")}`);
4490
+ process.exit(1);
4491
+ }
4492
+ state = stateInput;
4493
+ }
4494
+ let enabled;
4495
+ if (state === "toggle") {
4496
+ const cwd = process.env["SISYPHUS_CWD"] ? process.env["SISYPHUS_CWD"] : process.cwd();
4497
+ const statusResp = await sendRequest({ type: "status", sessionId, cwd });
4498
+ if (!statusResp.ok) {
4499
+ console.error(`Error: ${statusResp.error}`);
4500
+ process.exit(1);
4501
+ }
4502
+ const session2 = statusResp.data?.session;
4503
+ if (!session2) {
4504
+ console.error(`Error: session ${sessionId} not found`);
4505
+ process.exit(1);
4506
+ }
4507
+ enabled = !session2.dangerousMode;
4508
+ } else {
4509
+ enabled = state === "on";
4510
+ }
4511
+ const request = { type: "set-dangerous-mode", sessionId, enabled };
4512
+ const response = await sendRequest(request);
4513
+ if (response.ok) {
4514
+ const flushedRaw = response.data?.flushed;
4515
+ const flushed = typeof flushedRaw === "number" ? flushedRaw : 0;
4516
+ const label = enabled ? "ON" : "OFF";
4517
+ let msg = `DANGEROUS mode ${label} for session ${sessionId}`;
4518
+ if (enabled && flushed > 0) {
4519
+ msg += ` \u2014 ${flushed} pending ask${flushed === 1 ? "" : "s"} auto-resolved`;
4520
+ }
4521
+ console.log(msg);
4522
+ } else {
4523
+ console.error(`Error: ${response.error}`);
4524
+ process.exit(1);
4525
+ }
4526
+ });
4527
+ }
4528
+
4133
4529
  // src/tui/lib/context.ts
4134
4530
  init_paths();
4135
4531
  import { readFileSync as readFileSync15, readdirSync as readdirSync4 } from "fs";
@@ -4293,14 +4689,14 @@ function registerSessionContext(program2) {
4293
4689
  }
4294
4690
 
4295
4691
  // src/cli/commands/spawn.ts
4296
- import { existsSync as existsSync12 } from "fs";
4297
- import { join as join14, resolve as resolve5 } from "path";
4692
+ import { existsSync as existsSync13 } from "fs";
4693
+ import { join as join15, resolve as resolve5 } from "path";
4298
4694
 
4299
4695
  // src/daemon/frontmatter.ts
4300
4696
  init_paths();
4301
- import { readFileSync as readFileSync16, existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
4302
- import { homedir as homedir7 } from "os";
4303
- import { join as join13, basename as basename4 } from "path";
4697
+ import { readFileSync as readFileSync16, existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
4698
+ import { homedir as homedir8 } from "os";
4699
+ import { join as join14, basename as basename4 } from "path";
4304
4700
  function parseAgentFrontmatter(content) {
4305
4701
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4306
4702
  if (!match) return {};
@@ -4350,7 +4746,7 @@ function discoverAgentTypes(pluginDir, cwd) {
4350
4746
  if (seen.has(qualifiedName)) continue;
4351
4747
  seen.add(qualifiedName);
4352
4748
  try {
4353
- const content = readFileSync16(join13(dir, file), "utf-8");
4749
+ const content = readFileSync16(join14(dir, file), "utf-8");
4354
4750
  const fm = parseAgentFrontmatter(content);
4355
4751
  results.push({ qualifiedName, source, description: fm.description, model: fm.model });
4356
4752
  } catch {
@@ -4358,13 +4754,13 @@ function discoverAgentTypes(pluginDir, cwd) {
4358
4754
  }
4359
4755
  }
4360
4756
  }
4361
- scanDir(join13(projectAgentPluginDir(cwd), "agents"), null, "project-sis");
4362
- scanDir(join13(userAgentPluginDir(), "agents"), null, "user-sis");
4363
- scanDir(join13(cwd, ".claude", "agents"), null, "project");
4364
- scanDir(join13(homedir7(), ".claude", "agents"), null, "user");
4365
- scanDir(join13(pluginDir, "agents"), "sisyphus", "bundled");
4757
+ scanDir(join14(projectAgentPluginDir(cwd), "agents"), null, "project-sis");
4758
+ scanDir(join14(userAgentPluginDir(), "agents"), null, "user-sis");
4759
+ scanDir(join14(cwd, ".claude", "agents"), null, "project");
4760
+ scanDir(join14(homedir8(), ".claude", "agents"), null, "user");
4761
+ scanDir(join14(pluginDir, "agents"), "sisyphus", "bundled");
4366
4762
  try {
4367
- const registryPath = join13(homedir7(), ".claude", "plugins", "installed_plugins.json");
4763
+ const registryPath = join14(homedir8(), ".claude", "plugins", "installed_plugins.json");
4368
4764
  const registry = JSON.parse(readFileSync16(registryPath, "utf-8"));
4369
4765
  const pluginEntries = registry.plugins ?? registry;
4370
4766
  for (const key of Object.keys(pluginEntries)) {
@@ -4374,7 +4770,7 @@ function discoverAgentTypes(pluginDir, cwd) {
4374
4770
  const entry = pluginEntries[key];
4375
4771
  const installPath = Array.isArray(entry) ? entry[0]?.installPath : entry?.installPath;
4376
4772
  if (installPath) {
4377
- scanDir(join13(installPath, "agents"), namespace, "plugin");
4773
+ scanDir(join14(installPath, "agents"), namespace, "plugin");
4378
4774
  }
4379
4775
  }
4380
4776
  } catch {
@@ -4444,8 +4840,8 @@ function registerSpawn(program2) {
4444
4840
  process.exit(1);
4445
4841
  }
4446
4842
  if (opts.repo && opts.repo !== ".") {
4447
- const repoPath = join14(sisyphusCwd, opts.repo);
4448
- if (!existsSync12(repoPath)) {
4843
+ const repoPath = join15(sisyphusCwd, opts.repo);
4844
+ if (!existsSync13(repoPath)) {
4449
4845
  console.error(`Error: repo directory does not exist: ${repoPath}`);
4450
4846
  process.exit(1);
4451
4847
  }
@@ -4552,7 +4948,7 @@ function registerReport(program2) {
4552
4948
  }
4553
4949
 
4554
4950
  // src/cli/commands/await.ts
4555
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
4951
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
4556
4952
  var AWAIT_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
4557
4953
  function registerAwait(program2) {
4558
4954
  program2.command("await").description("Block until an agent reaches a terminal status, then print its final report inline. Marks the agent as consumed-inline so its report is suppressed from the next cycle.").argument("<agentId>", "Agent ID to await").option("--session <sessionId>", "Session ID (defaults to SISYPHUS_SESSION_ID env var)").action(async (agentId, opts) => {
@@ -4576,7 +4972,7 @@ function registerAwait(program2) {
4576
4972
  const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
4577
4973
  const label = shortType ? `${shortType}-${agentName}` : agentName;
4578
4974
  console.log(`[${status}] ${agentId} (${label})`);
4579
- if (reportPath && existsSync13(reportPath)) {
4975
+ if (reportPath && existsSync14(reportPath)) {
4580
4976
  try {
4581
4977
  const body = readFileSync17(reportPath, "utf-8");
4582
4978
  if (body.length > 0) {
@@ -4706,9 +5102,9 @@ import { execSync as execSync10 } from "child_process";
4706
5102
 
4707
5103
  // src/cli/onboard.ts
4708
5104
  import { execSync as execSync9 } from "child_process";
4709
- import { existsSync as existsSync14, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
4710
- import { homedir as homedir8 } from "os";
4711
- import { dirname as dirname5, join as join15 } from "path";
5105
+ import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
5106
+ import { homedir as homedir9 } from "os";
5107
+ import { dirname as dirname5, join as join16 } from "path";
4712
5108
  import { fileURLToPath as fileURLToPath2 } from "url";
4713
5109
  function detectTerminal() {
4714
5110
  const termProgram = process.env["TERM_PROGRAM"] || "";
@@ -4745,8 +5141,8 @@ function checkItermOptionKey() {
4745
5141
  if (process.platform !== "darwin") {
4746
5142
  return { checked: false, allCorrect: true, incorrectProfiles: [] };
4747
5143
  }
4748
- const plistPath2 = join15(homedir8(), "Library", "Preferences", "com.googlecode.iterm2.plist");
4749
- if (!existsSync14(plistPath2)) {
5144
+ const plistPath2 = join16(homedir9(), "Library", "Preferences", "com.googlecode.iterm2.plist");
5145
+ if (!existsSync15(plistPath2)) {
4750
5146
  return { checked: false, allCorrect: false, incorrectProfiles: [] };
4751
5147
  }
4752
5148
  try {
@@ -4770,7 +5166,7 @@ function checkItermOptionKey() {
4770
5166
  }
4771
5167
  }
4772
5168
  function hasExistingTmuxConf() {
4773
- return existsSync14(join15(homedir8(), ".tmux.conf")) || existsSync14(join15(homedir8(), ".config", "tmux", "tmux.conf"));
5169
+ return existsSync15(join16(homedir9(), ".tmux.conf")) || existsSync15(join16(homedir9(), ".config", "tmux", "tmux.conf"));
4774
5170
  }
4775
5171
  var SISYPHUS_DEFAULTS_MARKER = "# sisyphus-managed \u2014 do not edit";
4776
5172
  function buildTmuxDefaults() {
@@ -4882,7 +5278,7 @@ source-file -q ${sisyphusConf} ${SISYPHUS_DEFAULTS_MARKER}
4882
5278
  `;
4883
5279
  }
4884
5280
  function writeTmuxDefaults() {
4885
- const confPath = join15(homedir8(), ".tmux.conf");
5281
+ const confPath = join16(homedir9(), ".tmux.conf");
4886
5282
  writeFileSync8(confPath, buildTmuxDefaults(), "utf8");
4887
5283
  }
4888
5284
  function isNvimAvailable() {
@@ -4901,19 +5297,19 @@ function getNvimVersion() {
4901
5297
  }
4902
5298
  }
4903
5299
  function hasLazyVimConfig() {
4904
- return existsSync14(join15(homedir8(), ".config", "nvim", "lazy-lock.json"));
5300
+ return existsSync15(join16(homedir9(), ".config", "nvim", "lazy-lock.json"));
4905
5301
  }
4906
5302
  function bundledBaleiaPluginPath() {
4907
5303
  const distDir = dirname5(fileURLToPath2(import.meta.url));
4908
- return join15(distDir, "templates", "baleia.lua");
5304
+ return join16(distDir, "templates", "baleia.lua");
4909
5305
  }
4910
5306
  function installBaleiaPlugin() {
4911
- const pluginsDir = join15(homedir8(), ".config", "nvim", "lua", "plugins");
4912
- if (!existsSync14(pluginsDir)) return false;
4913
- const dest = join15(pluginsDir, "sisyphus-baleia.lua");
4914
- if (existsSync14(dest)) return true;
5307
+ const pluginsDir = join16(homedir9(), ".config", "nvim", "lua", "plugins");
5308
+ if (!existsSync15(pluginsDir)) return false;
5309
+ const dest = join16(pluginsDir, "sisyphus-baleia.lua");
5310
+ if (existsSync15(dest)) return true;
4915
5311
  const src = bundledBaleiaPluginPath();
4916
- if (!existsSync14(src)) return false;
5312
+ if (!existsSync15(src)) return false;
4917
5313
  try {
4918
5314
  writeFileSync8(dest, readFileSync18(src, "utf-8"), "utf8");
4919
5315
  return true;
@@ -4938,9 +5334,9 @@ function tryAutoInstallNvim() {
4938
5334
  if (!isNvimAvailable()) {
4939
5335
  return { installed: false, autoInstalled: false, version: "", lazyVimInstalled: false, baleiaInstalled: false };
4940
5336
  }
4941
- const nvimConfigDir = join15(homedir8(), ".config", "nvim");
5337
+ const nvimConfigDir = join16(homedir9(), ".config", "nvim");
4942
5338
  let lazyVimInstalled = false;
4943
- if (!existsSync14(nvimConfigDir)) {
5339
+ if (!existsSync15(nvimConfigDir)) {
4944
5340
  const cloneCmd = [
4945
5341
  "git",
4946
5342
  "-c core.hooksPath=/dev/null",
@@ -4956,8 +5352,8 @@ function tryAutoInstallNvim() {
4956
5352
  stdio: "inherit",
4957
5353
  env: { ...process.env, GIT_LFS_SKIP_SMUDGE: "1" }
4958
5354
  });
4959
- const gitDir = join15(nvimConfigDir, ".git");
4960
- if (existsSync14(gitDir)) {
5355
+ const gitDir = join16(nvimConfigDir, ".git");
5356
+ if (existsSync15(gitDir)) {
4961
5357
  execSync9(`rm -rf "${gitDir}"`, { stdio: "pipe" });
4962
5358
  }
4963
5359
  lazyVimInstalled = true;
@@ -5128,7 +5524,7 @@ function printResults(result, daemonOk, keybindMsg) {
5128
5524
  console.log("");
5129
5525
  }
5130
5526
  function registerSetup(program2) {
5131
- program2.command("setup").description("One-time setup: install dependencies, daemon, keybindings, and commands").option("-y, --yes", "Skip confirmation prompts (e.g. before modifying ~/.tmux.conf)").action(async (opts) => {
5527
+ program2.command("setup").description("One-time setup: install dependencies, daemon, keybindings, and commands").option("-y, --yes", "Skip the y/N prompt before modifying ~/.tmux.conf").option("-f, --force", "Override safety refusals: overwrite existing key bindings AND auto-append source-file to ~/.tmux.conf").action(async (opts) => {
5132
5528
  const result = runOnboarding();
5133
5529
  let daemonOk = false;
5134
5530
  try {
@@ -5140,11 +5536,15 @@ function registerSetup(program2) {
5140
5536
  const keybindResult = await setupTmuxKeybind(
5141
5537
  DEFAULT_CYCLE_KEY,
5142
5538
  DEFAULT_PREFIX_KEY,
5143
- { assumeYes: opts.yes }
5539
+ { assumeYes: opts.yes, force: opts.force }
5144
5540
  );
5145
5541
  let keybindMsg;
5146
5542
  if (keybindResult.status === "installed" || keybindResult.status === "already-installed") {
5147
5543
  keybindMsg = `${DEFAULT_CYCLE_KEY} cycle, ${DEFAULT_PREFIX_KEY} prefix (h=dashboard, x=kill)`;
5544
+ } else if (keybindResult.status === "requires-force" || keybindResult.status === "conflict") {
5545
+ keybindMsg = `${keybindResult.message}
5546
+ Run "sis admin check-keybinds" for the full decision tree, then re-run "sis admin setup --force" to proceed.`;
5547
+ process.exitCode = 1;
5148
5548
  } else {
5149
5549
  keybindMsg = keybindResult.message;
5150
5550
  }
@@ -5154,9 +5554,12 @@ function registerSetup(program2) {
5154
5554
 
5155
5555
  // src/cli/commands/setup-keybind.ts
5156
5556
  function registerSetupKeybind(program2) {
5157
- program2.command("setup-keybind [cycle-key]").description("Install sisyphus tmux keybindings (default: M-s cycle, C-s prefix)").option("-y, --yes", "Skip confirmation prompt before modifying ~/.tmux.conf").action(async (key, opts) => {
5158
- const resolvedKey = key ?? DEFAULT_CYCLE_KEY;
5159
- const result = await setupTmuxKeybind(resolvedKey, void 0, { assumeYes: opts.yes });
5557
+ program2.command("setup-keybind [cycle-key]").description("Install sisyphus tmux keybindings (default: M-s cycle, C-s prefix)").option("-y, --yes", "Auto-accept the y/N prompt before appending source-file to ~/.tmux.conf").option("-f, --force", "Override safety refusals: overwrite existing key bindings AND auto-append the source-file line").action(async (key, opts) => {
5558
+ const resolvedKey = key === void 0 ? DEFAULT_CYCLE_KEY : key;
5559
+ const result = await setupTmuxKeybind(resolvedKey, void 0, {
5560
+ assumeYes: opts.yes,
5561
+ force: opts.force
5562
+ });
5160
5563
  switch (result.status) {
5161
5564
  case "installed":
5162
5565
  console.log(result.message);
@@ -5166,28 +5569,602 @@ function registerSetupKeybind(program2) {
5166
5569
  console.log(result.message);
5167
5570
  break;
5168
5571
  case "conflict":
5169
- console.log(`Key ${resolvedKey} is already bound:`);
5572
+ console.log(`Key ${result.conflictKey} is already bound:`);
5170
5573
  console.log(` ${result.existingBinding}`);
5171
5574
  console.log("");
5172
- console.log("Use a different key, e.g.:");
5173
- console.log(" sis admin setup-keybind M-S");
5174
- console.log(" sis admin setup-keybind M-w");
5175
- console.log(" sis admin setup-keybind M-j");
5575
+ console.log("Options:");
5576
+ console.log(" - Pick a different cycle key: sis admin setup-keybind M-w");
5577
+ console.log(' - Run "sis admin check-keybinds" for a full breakdown');
5578
+ console.log(" - Override and overwrite: sis admin setup-keybind --force");
5579
+ process.exitCode = 1;
5176
5580
  break;
5177
5581
  case "unsupported-tmux":
5178
5582
  console.log(result.message);
5583
+ process.exitCode = 1;
5584
+ break;
5585
+ case "requires-force":
5586
+ console.log(result.message);
5587
+ console.log("");
5588
+ console.log('Run "sis admin check-keybinds" first if you want the full decision tree before deciding.');
5589
+ process.exitCode = 1;
5179
5590
  break;
5180
5591
  case "conf-modification-declined":
5181
5592
  console.log(result.message);
5182
5593
  console.log("");
5183
- console.log("Re-run with --yes to skip the prompt.");
5594
+ console.log("Re-run with --force to append automatically.");
5184
5595
  break;
5185
5596
  }
5186
5597
  });
5187
5598
  }
5188
5599
 
5189
- // src/cli/commands/home-init.ts
5600
+ // src/cli/commands/check-keybinds.ts
5190
5601
  import { execSync as execSync11 } from "child_process";
5602
+ import { readFileSync as readFileSync19 } from "fs";
5603
+ function isTmuxInstalled2() {
5604
+ try {
5605
+ execSync11("which tmux", { stdio: "pipe" });
5606
+ return true;
5607
+ } catch {
5608
+ return false;
5609
+ }
5610
+ }
5611
+ function getTmuxVersion2() {
5612
+ try {
5613
+ return execSync11("tmux -V", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
5614
+ } catch {
5615
+ return null;
5616
+ }
5617
+ }
5618
+ function isTmuxServerRunning() {
5619
+ try {
5620
+ execSync11("tmux list-sessions", { stdio: ["pipe", "pipe", "pipe"] });
5621
+ return true;
5622
+ } catch {
5623
+ return false;
5624
+ }
5625
+ }
5626
+ function getTmuxPrefix() {
5627
+ try {
5628
+ return execSync11("tmux show-options -gv prefix", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim() || null;
5629
+ } catch {
5630
+ return null;
5631
+ }
5632
+ }
5633
+ function classifyKey(key, serverRunning) {
5634
+ if (!serverRunning) return { kind: "unbound" };
5635
+ const binding = getExistingBinding(key);
5636
+ if (binding === null) return { kind: "unbound" };
5637
+ if (isSisyphusBinding(binding)) return { kind: "sisyphus", binding };
5638
+ return { kind: "conflict", binding };
5639
+ }
5640
+ function runCheck() {
5641
+ const tmuxInstalled = isTmuxInstalled2();
5642
+ const tmuxVersion = tmuxInstalled ? getTmuxVersion2() : null;
5643
+ const tmuxVersionOk = tmuxInstalled ? tmuxVersionAtLeast(3, 2) : false;
5644
+ const tmuxServerRunning = tmuxInstalled ? isTmuxServerRunning() : false;
5645
+ const inTmux = !!process.env["TMUX"];
5646
+ const cycleKey = { key: DEFAULT_CYCLE_KEY, ...classifyKey(DEFAULT_CYCLE_KEY, tmuxServerRunning) };
5647
+ const prefixKey = { key: DEFAULT_PREFIX_KEY, ...classifyKey(DEFAULT_PREFIX_KEY, tmuxServerRunning) };
5648
+ const tmuxPrefix = tmuxServerRunning ? getTmuxPrefix() : null;
5649
+ const prefixCollision = tmuxPrefix !== null && tmuxPrefix === DEFAULT_PREFIX_KEY;
5650
+ const sisyphusConfPath = sisyphusTmuxConfPath();
5651
+ const userConfPath = userTmuxConfPath();
5652
+ let userConfAlreadySources = false;
5653
+ if (userConfPath !== null) {
5654
+ try {
5655
+ userConfAlreadySources = readFileSync19(userConfPath, "utf-8").includes(sisyphusConfPath);
5656
+ } catch {
5657
+ }
5658
+ }
5659
+ return {
5660
+ tmuxInstalled,
5661
+ tmuxServerRunning,
5662
+ tmuxVersion,
5663
+ tmuxVersionOk,
5664
+ inTmux,
5665
+ cycleKey,
5666
+ prefixKey,
5667
+ tmuxPrefix,
5668
+ prefixCollision,
5669
+ userConfPath,
5670
+ userConfAlreadySources,
5671
+ sisyphusConfPath
5672
+ };
5673
+ }
5674
+ function fmtKeyState(k) {
5675
+ switch (k.kind) {
5676
+ case "sisyphus":
5677
+ return `${k.key}: sisyphus (${k.binding})`;
5678
+ case "conflict":
5679
+ return `${k.key}: CONFLICT \u2014 already bound: ${k.binding}`;
5680
+ case "unbound":
5681
+ return `${k.key}: unbound`;
5682
+ }
5683
+ }
5684
+ function printInstructions(r) {
5685
+ const bothSisyphus = r.cycleKey.kind === "sisyphus" && r.prefixKey.kind === "sisyphus";
5686
+ const anyConflict = r.cycleKey.kind === "conflict" || r.prefixKey.kind === "conflict";
5687
+ const wouldModifyUserConf = r.userConfPath !== null && !r.userConfAlreadySources;
5688
+ const safeAutoInstall = !anyConflict && !r.prefixCollision && !wouldModifyUserConf;
5689
+ const tmuxVersionLabel = r.tmuxVersion === null ? "(none)" : r.tmuxVersion;
5690
+ const tmuxPrefixLabel = r.tmuxPrefix === null ? "(default C-b)" : r.tmuxPrefix;
5691
+ const userConfLabel = r.userConfPath === null ? "(none found)" : r.userConfPath;
5692
+ const userConfForCopy = r.userConfPath === null ? "~/.tmux.conf" : r.userConfPath;
5693
+ console.log(`
5694
+ <claude-instructions>
5695
+ # Sisyphus Keybind Precheck
5696
+
5697
+ Run this BEFORE asking the user to try any sisyphus keybind (e.g. \`Right Option+s\`,
5698
+ \`C-s h\`, \`C-s ?\`). If the keybinds aren't actually wired up, the user will press
5699
+ the key and nothing will happen \u2014 confusing, looks broken.
5700
+
5701
+ ## Environment Data
5702
+ - tmuxInstalled: ${r.tmuxInstalled}
5703
+ - tmuxVersion: ${tmuxVersionLabel}
5704
+ - tmuxVersionOk: ${r.tmuxVersionOk} (sisyphus needs 3.2+ for display-menu)
5705
+ - tmuxServerRunning: ${r.tmuxServerRunning}
5706
+ - inTmux ($TMUX set): ${r.inTmux}
5707
+ - ${fmtKeyState(r.cycleKey)}
5708
+ - ${fmtKeyState(r.prefixKey)}
5709
+ - tmuxPrefix (\`set -g prefix\`): ${tmuxPrefixLabel}
5710
+ - prefixCollision: ${r.prefixCollision} (true = user's tmux prefix is ${DEFAULT_PREFIX_KEY}; binding it in root would shadow it)
5711
+ - userConfPath: ${userConfLabel}
5712
+ - userConfAlreadySources: ${r.userConfAlreadySources}
5713
+ - sisyphusConfPath: ${r.sisyphusConfPath}
5714
+
5715
+ ## Decision
5716
+
5717
+ Pick exactly one path. Do NOT proceed past this check until both keys read "sisyphus".
5718
+
5719
+ ### Path A \u2014 All wired, proceed
5720
+ **Trigger:** cycleKey is "sisyphus" AND prefixKey is "sisyphus".
5721
+ **Action:** ${bothSisyphus ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Confirm briefly and continue with the onboarding step that needs keybinds.
5722
+
5723
+ ### Path B \u2014 Safe to auto-install
5724
+ **Trigger:** No conflicts, no prefix collision, AND (no user tmux.conf OR it already sources sisyphus).
5725
+ **Action:** ${safeAutoInstall && !bothSisyphus ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Run:
5726
+ \`\`\`
5727
+ sis admin setup-keybind --yes
5728
+ \`\`\`
5729
+ This installs the helper scripts in ~/.sisyphus/bin/ and applies the bindings to the live
5730
+ tmux server. No user files are clobbered (none to clobber, or already wired).
5731
+
5732
+ ### Path C \u2014 Would append a line to the user's tmux.conf
5733
+ **Trigger:** No conflicts, no prefix collision, but ${userConfForCopy} exists and doesn't yet source ${r.sisyphusConfPath}.
5734
+ **Action:** ${!safeAutoInstall && !anyConflict && !r.prefixCollision && wouldModifyUserConf && !bothSisyphus ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Ask the user to choose:
5735
+
5736
+ 1. **Persistent (recommended).** "I'll add one line to ${userConfForCopy} so the bindings survive tmux restarts. The line is marked \`# sisyphus-managed \u2014 do not edit\` and is grep-removable later."
5737
+ Run: \`sis admin setup-keybind --yes\`
5738
+
5739
+ 2. **Live only, no file changes.** "I'll wire the bindings into your current tmux server without touching any config file. They'll vanish when you restart tmux, and you can re-run \`sis admin setup-keybind\` anytime to make them stick."
5740
+ Run: \`sis admin setup-keybind\` (no --yes; non-TTY auto-declines the conf prompt while still applying live bindings + installing helper scripts)
5741
+
5742
+ ### Path D \u2014 Conflict on ${DEFAULT_CYCLE_KEY} or ${DEFAULT_PREFIX_KEY}
5743
+ **Trigger:** cycleKey or prefixKey is "CONFLICT".
5744
+ **Action:** ${anyConflict ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} The user already has these keys bound to something else. Show them the conflicting bindings (above) and offer:
5745
+
5746
+ 1. **Pick alternate keys.** Re-run with a different cycle key \u2014 e.g. \`M-S\`, \`M-w\`, \`M-j\`, \`M-\\\`\`:
5747
+ \`\`\`
5748
+ sis admin setup-keybind M-w
5749
+ \`\`\`
5750
+ This still uses C-s for the prefix; if the prefix also conflicts, you'll need to wire
5751
+ directly (option 3 below), since setup-keybind only takes a custom cycle key.
5752
+
5753
+ 2. **Skip keybinds entirely.** The user can drive sisyphus from the CLI: \`sis dashboard\`,
5754
+ \`sis status\`, \`sis start\`, \`sis session resume\`. Lose tmux quick-actions, keep
5755
+ existing bindings.
5756
+
5757
+ 3. **Wire commands directly (advanced).** Bypass setup-keybind and bind individual
5758
+ sisyphus actions to keys the user picks. Helper scripts must already exist \u2014 if
5759
+ ~/.sisyphus/bin/sisyphus-cycle is missing, you cannot use this path until setup-keybind
5760
+ has run at least once successfully (try option 1 first).
5761
+ \`\`\`
5762
+ # cycle sessions on a key the user chooses (replace M-w):
5763
+ tmux bind-key -T root M-w run-shell "$HOME/.sisyphus/bin/sisyphus-cycle"
5764
+
5765
+ # open the dashboard directly on a key (replace M-h):
5766
+ tmux bind-key -T root M-h run-shell "$HOME/.sisyphus/bin/sisyphus-home"
5767
+ \`\`\`
5768
+ These apply only to the running tmux server. To persist, append the same lines to
5769
+ ${userConfForCopy}.
5770
+
5771
+ ### Path E \u2014 Hidden prefix collision
5772
+ **Trigger:** prefixCollision is true (the user's \`set -g prefix\` is already ${DEFAULT_PREFIX_KEY}).
5773
+ **Action:** ${r.prefixCollision ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} \`tmux list-keys\` won't surface this as a binding conflict, but installing the C-s root-table menu would shadow the user's prefix. Tell the user:
5774
+
5775
+ > "Your tmux prefix is set to ${DEFAULT_PREFIX_KEY}. Sisyphus wants to bind ${DEFAULT_PREFIX_KEY} in the root table for its menu, which would shadow your prefix. Options: (a) move your prefix (e.g. \`set -g prefix C-a\`) and let sisyphus take ${DEFAULT_PREFIX_KEY}, or (b) skip the menu binding \u2014 only \`${DEFAULT_CYCLE_KEY}\` cycle gets installed."
5776
+
5777
+ For (b), wire just the cycle key directly:
5778
+ \`\`\`
5779
+ sis admin setup-keybind ${DEFAULT_CYCLE_KEY}
5780
+ # then unbind C-s if setup-keybind ended up taking it:
5781
+ tmux unbind-key -T root ${DEFAULT_PREFIX_KEY}
5782
+ \`\`\`
5783
+
5784
+ ### Path F \u2014 tmux not ready
5785
+ **Trigger:** any of: tmuxInstalled=false, tmuxVersionOk=false, tmuxServerRunning=false.
5786
+ **Action:** ${!r.tmuxInstalled || !r.tmuxVersionOk || !r.tmuxServerRunning ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Don't install keybinds yet. Fix the precondition:
5787
+ - tmuxInstalled=false \u2192 \`brew install tmux\` (macOS) or your package manager
5788
+ - tmuxVersionOk=false \u2192 upgrade tmux to 3.2+
5789
+ - tmuxServerRunning=false \u2192 user needs to run \`tmux\` (or attach to an existing session) before live bindings can be installed or tested
5790
+
5791
+ After fixing, re-run \`sis admin check-keybinds\`.
5792
+
5793
+ ## After acting
5794
+ Re-run \`sis admin check-keybinds\` to confirm both keys read "sisyphus", THEN ask the
5795
+ user to try the keybind. Don't skip the verification \u2014 \`setup-keybind\` can fail silently
5796
+ if the tmux server dies between commands.
5797
+ </claude-instructions>
5798
+ `);
5799
+ }
5800
+ function registerCheckKeybinds(program2) {
5801
+ program2.command("check-keybinds").description("Verify tmux keybind state before asking the user to try a sisyphus keybind").option("--json", "Print raw JSON state instead of Claude instructions").action((opts) => {
5802
+ const result = runCheck();
5803
+ if (opts.json) {
5804
+ console.log(JSON.stringify(result, null, 2));
5805
+ return;
5806
+ }
5807
+ printInstructions(result);
5808
+ });
5809
+ }
5810
+
5811
+ // src/cli/commands/check-statusbar.ts
5812
+ init_paths();
5813
+ import { execSync as execSync12 } from "child_process";
5814
+ import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
5815
+ import { homedir as homedir10 } from "os";
5816
+ import { join as join17 } from "path";
5817
+ var SISYPHUS_LEFT_TOKEN = "@sisyphus_left";
5818
+ var SISYPHUS_RIGHT_TOKEN = "@sisyphus_right";
5819
+ var TMUX_DEFAULT_STATUS_LEFT = "[#S] ";
5820
+ var TMUX_DEFAULT_STATUS_RIGHT_PREFIX = '"#{=21:pane_title}"';
5821
+ function isTmuxInstalled3() {
5822
+ try {
5823
+ execSync12("which tmux", { stdio: "pipe" });
5824
+ return true;
5825
+ } catch {
5826
+ return false;
5827
+ }
5828
+ }
5829
+ function isTmuxServerRunning2() {
5830
+ try {
5831
+ execSync12("tmux list-sessions", { stdio: ["pipe", "pipe", "pipe"] });
5832
+ return true;
5833
+ } catch {
5834
+ return false;
5835
+ }
5836
+ }
5837
+ function isDaemonRunning() {
5838
+ const pidFile = daemonPidPath();
5839
+ if (!existsSync16(pidFile)) return false;
5840
+ try {
5841
+ const pid = parseInt(readFileSync20(pidFile, "utf-8").trim(), 10);
5842
+ if (Number.isNaN(pid) || pid <= 0) return false;
5843
+ process.kill(pid, 0);
5844
+ return true;
5845
+ } catch {
5846
+ return false;
5847
+ }
5848
+ }
5849
+ function showOption(name) {
5850
+ try {
5851
+ const out = execSync12(`tmux show-options -g ${name}`, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
5852
+ if (out.length === 0) return null;
5853
+ const prefix = `${name} `;
5854
+ const stripped = out.startsWith(prefix) ? out.slice(prefix.length) : out;
5855
+ if (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length >= 2) {
5856
+ return stripped.slice(1, -1);
5857
+ }
5858
+ return stripped;
5859
+ } catch {
5860
+ return null;
5861
+ }
5862
+ }
5863
+ function probeTmuxOptions(serverRunning) {
5864
+ if (!serverRunning) {
5865
+ return {
5866
+ status: null,
5867
+ statusLeft: null,
5868
+ statusRight: null,
5869
+ statusPosition: null,
5870
+ statusStyle: null,
5871
+ statusInterval: null
5872
+ };
5873
+ }
5874
+ return {
5875
+ status: showOption("status"),
5876
+ statusLeft: showOption("status-left"),
5877
+ statusRight: showOption("status-right"),
5878
+ statusPosition: showOption("status-position"),
5879
+ statusStyle: showOption("status-style"),
5880
+ statusInterval: showOption("status-interval")
5881
+ };
5882
+ }
5883
+ function findUserTmuxConf() {
5884
+ const xdg = join17(homedir10(), ".config", "tmux", "tmux.conf");
5885
+ const dotfile = join17(homedir10(), ".tmux.conf");
5886
+ if (existsSync16(xdg)) return xdg;
5887
+ if (existsSync16(dotfile)) return dotfile;
5888
+ return null;
5889
+ }
5890
+ function probeUserConf() {
5891
+ const path = findUserTmuxConf();
5892
+ if (path === null) {
5893
+ return { path: null, setsStatusLeft: false, setsStatusRight: false, sourcesSisyphusManaged: false };
5894
+ }
5895
+ let contents = "";
5896
+ try {
5897
+ contents = readFileSync20(path, "utf-8");
5898
+ } catch {
5899
+ return { path, setsStatusLeft: false, setsStatusRight: false, sourcesSisyphusManaged: false };
5900
+ }
5901
+ const lines = contents.split("\n").filter((line) => !line.trim().startsWith("#"));
5902
+ const setsStatusLeft = lines.some((line) => /^\s*(set|set-option)\s+-g(?:\s+-\w+)*\s+status-left\b/.test(line));
5903
+ const setsStatusRight = lines.some((line) => /^\s*(set|set-option)\s+-g(?:\s+-\w+)*\s+status-right\b/.test(line));
5904
+ const sourcesSisyphusManaged = contents.includes(join17(homedir10(), ".sisyphus", "tmux.conf"));
5905
+ return { path, setsStatusLeft, setsStatusRight, sourcesSisyphusManaged };
5906
+ }
5907
+ function loadGlobalSisyphusConfig() {
5908
+ const path = globalConfigPath();
5909
+ if (!existsSync16(path)) return null;
5910
+ try {
5911
+ const parsed = JSON.parse(readFileSync20(path, "utf-8"));
5912
+ return parsed.statusBar === void 0 ? null : parsed.statusBar;
5913
+ } catch {
5914
+ return null;
5915
+ }
5916
+ }
5917
+ function classifyState(opts, serverRunning) {
5918
+ if (!serverRunning) {
5919
+ return { state: "tmux-not-ready", referencesSisyphusLeft: false, referencesSisyphusRight: false };
5920
+ }
5921
+ if (opts.status === "off") {
5922
+ return { state: "disabled", referencesSisyphusLeft: false, referencesSisyphusRight: false };
5923
+ }
5924
+ const referencesSisyphusLeft = opts.statusLeft !== null && opts.statusLeft.includes(SISYPHUS_LEFT_TOKEN);
5925
+ const referencesSisyphusRight = opts.statusRight !== null && opts.statusRight.includes(SISYPHUS_RIGHT_TOKEN);
5926
+ if (referencesSisyphusLeft && referencesSisyphusRight) {
5927
+ return { state: "wired", referencesSisyphusLeft, referencesSisyphusRight };
5928
+ }
5929
+ if (referencesSisyphusLeft) {
5930
+ return { state: "partial-left-only", referencesSisyphusLeft, referencesSisyphusRight };
5931
+ }
5932
+ if (referencesSisyphusRight) {
5933
+ return { state: "partial-right-only", referencesSisyphusLeft, referencesSisyphusRight };
5934
+ }
5935
+ const isStock = (opts.statusLeft === TMUX_DEFAULT_STATUS_LEFT || opts.statusLeft === null) && (opts.statusRight === null || opts.statusRight.includes(TMUX_DEFAULT_STATUS_RIGHT_PREFIX));
5936
+ return {
5937
+ state: isStock ? "tmux-default" : "custom-no-sisyphus",
5938
+ referencesSisyphusLeft,
5939
+ referencesSisyphusRight
5940
+ };
5941
+ }
5942
+ function runCheck2() {
5943
+ const tmuxInstalled = isTmuxInstalled3();
5944
+ const tmuxServerRunning = tmuxInstalled ? isTmuxServerRunning2() : false;
5945
+ const tmuxOptions = probeTmuxOptions(tmuxServerRunning);
5946
+ const userConf = probeUserConf();
5947
+ const globalConfig = loadGlobalSisyphusConfig();
5948
+ const daemonRunning = isDaemonRunning();
5949
+ const { state, referencesSisyphusLeft, referencesSisyphusRight } = classifyState(tmuxOptions, tmuxServerRunning);
5950
+ return {
5951
+ tmuxInstalled,
5952
+ tmuxServerRunning,
5953
+ daemonRunning,
5954
+ tmuxOptions,
5955
+ userConf,
5956
+ globalConfig,
5957
+ state,
5958
+ referencesSisyphusLeft,
5959
+ referencesSisyphusRight
5960
+ };
5961
+ }
5962
+ function fmtOption(value) {
5963
+ if (value === null) return "(unset)";
5964
+ return JSON.stringify(value);
5965
+ }
5966
+ function renderConfigSummary(cfg) {
5967
+ if (cfg === null) return "(no statusBar block in ~/.sisyphus/config.json \u2014 defaults apply)";
5968
+ const parts = [];
5969
+ if (cfg.enabled === false) parts.push("enabled: false (DISABLED)");
5970
+ if (cfg.left !== void 0) parts.push(`left: [${cfg.left.join(", ")}]`);
5971
+ if (cfg.right !== void 0) parts.push(`right: [${cfg.right.join(", ")}]`);
5972
+ if (cfg.colors !== void 0) parts.push(`colors: ${JSON.stringify(cfg.colors)}`);
5973
+ if (cfg.segments !== void 0) {
5974
+ const segNames = Object.keys(cfg.segments);
5975
+ if (segNames.length > 0) parts.push(`per-segment overrides: ${segNames.join(", ")}`);
5976
+ }
5977
+ if (parts.length === 0) return "(statusBar block exists but is empty \u2014 defaults apply)";
5978
+ return parts.join("\n ");
5979
+ }
5980
+ function printInstructions2(r) {
5981
+ const userConfPath = r.userConf.path === null ? "~/.tmux.conf (none found)" : r.userConf.path;
5982
+ const userConfForCopy = r.userConf.path === null ? "~/.tmux.conf" : r.userConf.path;
5983
+ console.log(`
5984
+ <claude-instructions>
5985
+ # Sisyphus Statusbar Helper
5986
+
5987
+ This is a READ-ONLY check. Use it to figure out how to help the user improve their
5988
+ tmux statusbar with sisyphus segments WITHOUT clobbering their existing setup. The
5989
+ user's tmux config is theirs \u2014 never overwrite it. Append, suggest snippets, or
5990
+ nudge them to edit ~/.sisyphus/config.json.
5991
+
5992
+ ## Environment Data
5993
+ - tmuxInstalled: ${r.tmuxInstalled}
5994
+ - tmuxServerRunning: ${r.tmuxServerRunning}
5995
+ - daemonRunning: ${r.daemonRunning} (false = @sisyphus_left/@sisyphus_right won't get populated, statusbar will look broken until daemon is up)
5996
+
5997
+ ### Live tmux options
5998
+ - status: ${fmtOption(r.tmuxOptions.status)}
5999
+ - status-left: ${fmtOption(r.tmuxOptions.statusLeft)}
6000
+ - status-right: ${fmtOption(r.tmuxOptions.statusRight)}
6001
+ - status-position: ${fmtOption(r.tmuxOptions.statusPosition)}
6002
+ - status-style: ${fmtOption(r.tmuxOptions.statusStyle)}
6003
+ - status-interval: ${fmtOption(r.tmuxOptions.statusInterval)}
6004
+ - referencesSisyphusLeft: ${r.referencesSisyphusLeft}
6005
+ - referencesSisyphusRight: ${r.referencesSisyphusRight}
6006
+
6007
+ ### User tmux config
6008
+ - path: ${userConfPath}
6009
+ - setsStatusLeft: ${r.userConf.setsStatusLeft}
6010
+ - setsStatusRight: ${r.userConf.setsStatusRight}
6011
+ - sourcesSisyphusManaged: ${r.userConf.sourcesSisyphusManaged} (sources ~/.sisyphus/tmux.conf \u2014 keybinds, NOT statusbar; included for context)
6012
+
6013
+ ### Sisyphus statusbar config (~/.sisyphus/config.json \u2192 statusBar)
6014
+ ${renderConfigSummary(r.globalConfig)}
6015
+
6016
+ ### Available segments
6017
+ - session-name (left default) \u2014 current tmux session name
6018
+ - windows (left default) \u2014 tmux window list with active highlight
6019
+ - sessions (right default) \u2014 all tmux sessions with claude-state colors
6020
+ - sisyphus-sessions (right default) \u2014 sisyphus-managed sessions with phase indicators
6021
+ - companion (right default) \u2014 companion mood/state pill
6022
+ - clock \u2014 separate %H:%M (NOT a sisyphus segment; tmux renders it inline; the default
6023
+ ~/.tmux.conf appends \`#[fg=...]#[bg=...] %H:%M \` after #{E:@sisyphus_right})
6024
+
6025
+ ## Detected state: **${r.state}**
6026
+
6027
+ Pick exactly one path. Each path tells you what to ask the user and what to do.
6028
+
6029
+ ### Path A \u2014 Already wired (state: wired)
6030
+ **Trigger:** status-left and status-right both reference @sisyphus_left/@sisyphus_right.
6031
+ **Action:** ${r.state === "wired" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6032
+
6033
+ The plumbing is correct. ${r.daemonRunning ? "" : "BUT daemon is not running \u2014 run `sisyphusd start` (or restart) so the @sisyphus_* options get populated. "}Now offer to **customize** what's shown:
6034
+
6035
+ 1. **Reorder or hide segments.** Edit \`~/.sisyphus/config.json\`, set \`statusBar.left\` / \`statusBar.right\` to the array of segment ids you want (in order). Example to hide windows and reorder:
6036
+ \`\`\`json
6037
+ {
6038
+ "statusBar": {
6039
+ "left": ["session-name"],
6040
+ "right": ["sisyphus-sessions", "companion"]
6041
+ }
6042
+ }
6043
+ \`\`\`
6044
+ 2. **Recolor.** \`statusBar.colors\` overrides processing/stopped/idle/activeBg/activeText/inactiveText. \`statusBar.segments.<id>.bg\` overrides per-segment band background.
6045
+ 3. **Disable a single segment.** Remove its id from the left/right arrays.
6046
+ 4. **Disable the whole bar.** \`statusBar.enabled: false\` \u2014 daemon stops writing the options; the user's status-left/right will then render as literal \`#{E:@sisyphus_left}\` (which tmux silently treats as empty).
6047
+ 5. **Restart the daemon** after any config change: \`sisyphusd restart\`. Changes don't auto-apply.
6048
+
6049
+ Ask the user what they want to change. Edit \`~/.sisyphus/config.json\` for them \u2014 that file is sisyphus-managed config, safe to edit. Don't touch their \`~/.tmux.conf\`.
6050
+
6051
+ ### Path B \u2014 Partial wiring (state: partial-left-only or partial-right-only)
6052
+ **Trigger:** Only one side references sisyphus.
6053
+ **Action:** ${r.state === "partial-left-only" || r.state === "partial-right-only" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6054
+
6055
+ Their config is half-wired. Show them the missing side:
6056
+ - Missing left \u2192 suggest adding \`set -g status-left "#{E:@sisyphus_left}"\` to ${userConfForCopy}
6057
+ - Missing right \u2192 suggest adding \`set -g status-right "#{E:@sisyphus_right}#[fg=#2d2f33]#[bg=#2d2f33,fg=#b0a898] %H:%M "\`
6058
+
6059
+ Don't auto-edit their config \u2014 present the snippet and ask if they want you to append it.
6060
+
6061
+ ### Path C \u2014 Stock tmux statusbar (state: tmux-default)
6062
+ **Trigger:** status-left/right are tmux defaults; user has no custom statusbar.
6063
+ **Action:** ${r.state === "tmux-default" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6064
+
6065
+ Easiest case. Two options to offer:
6066
+
6067
+ 1. **Full sisyphus statusbar.** Append these lines to ${userConfForCopy} (NOT a clobber \u2014 pure additive):
6068
+ \`\`\`tmux
6069
+ # --- Sisyphus statusbar ---
6070
+ set -g status on
6071
+ set -g status-style "bg=#1d1e21,fg=#d4cbb8"
6072
+ set -g status-position bottom
6073
+ set -g status-left "#{E:@sisyphus_left}"
6074
+ set -g status-left-length 250
6075
+ set -g status-right "#{E:@sisyphus_right}#[fg=#2d2f33]#[bg=#2d2f33,fg=#b0a898] %H:%M "
6076
+ set -g status-right-length 250
6077
+ set -g status-interval 2
6078
+ set -g window-status-format ""
6079
+ set -g window-status-current-format ""
6080
+ set -g window-status-separator ""
6081
+ \`\`\`
6082
+ ${r.userConf.path === null ? `Note: no tmux config exists yet. Create ${userConfForCopy} with these lines.` : ""}
6083
+
6084
+ 2. **Minimal sisyphus pill on the right.** If they want to keep the stock left side and just add a single sisyphus pill on the right:
6085
+ \`\`\`tmux
6086
+ set -g status-right "#{E:@sisyphus_right}#[default] %H:%M "
6087
+ set -g status-right-length 200
6088
+ \`\`\`
6089
+
6090
+ After appending, run \`tmux source-file ${userConfForCopy}\` so the change takes effect immediately.
6091
+
6092
+ ### Path D \u2014 User has a custom statusbar (state: custom-no-sisyphus)
6093
+ **Trigger:** status-left/right are user-customized but don't reference sisyphus tokens.
6094
+ **Action:** ${r.state === "custom-no-sisyphus" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6095
+
6096
+ This is the most delicate case \u2014 they care enough to have customized their bar. **Do not overwrite.** Show them the current contents and offer additive integration:
6097
+
6098
+ Their current right side is currently:
6099
+ \`\`\`
6100
+ ${r.tmuxOptions.statusRight === null ? "(unset)" : r.tmuxOptions.statusRight}
6101
+ \`\`\`
6102
+ And left side:
6103
+ \`\`\`
6104
+ ${r.tmuxOptions.statusLeft === null ? "(unset)" : r.tmuxOptions.statusLeft}
6105
+ \`\`\`
6106
+
6107
+ Offer three integration patterns and let the user pick:
6108
+
6109
+ 1. **Append sisyphus to the right side** (most common \u2014 leaves their left alone):
6110
+ \`\`\`tmux
6111
+ set -g status-right "${r.tmuxOptions.statusRight === null ? "" : r.tmuxOptions.statusRight}#{E:@sisyphus_right}"
6112
+ \`\`\`
6113
+ Or prepend it (sisyphus content appears first/leftmost):
6114
+ \`\`\`tmux
6115
+ set -g status-right "#{E:@sisyphus_right}${r.tmuxOptions.statusRight === null ? "" : r.tmuxOptions.statusRight}"
6116
+ \`\`\`
6117
+
6118
+ 2. **Append to the left side** (if they want session/windows pills at the start):
6119
+ \`\`\`tmux
6120
+ set -g status-left "${r.tmuxOptions.statusLeft === null ? "" : r.tmuxOptions.statusLeft}#{E:@sisyphus_left}"
6121
+ \`\`\`
6122
+
6123
+ 3. **Slim sisyphus only.** If they only want one specific signal (e.g., just sisyphus-sessions), they can set \`statusBar.right: ["sisyphus-sessions"]\` in \`~/.sisyphus/config.json\` and then append \`#{E:@sisyphus_right}\` to their existing right side. The composed string will only contain that one segment.
6124
+
6125
+ Important: the user's existing string may include format conditionals or special characters; use the exact value above when proposing the snippet (do not retype). Confirm with the user before editing ${userConfForCopy} \u2014 present the diff first.
6126
+
6127
+ ### Path E \u2014 Statusbar is disabled (state: disabled)
6128
+ **Trigger:** \`set -g status off\`.
6129
+ **Action:** ${r.state === "disabled" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6130
+
6131
+ The user explicitly turned off the statusbar. Don't enable it without asking. Suggest:
6132
+ - "I noticed you have the statusbar disabled (\`set -g status off\` in your config). Sisyphus needs it on to show session state. Want me to enable it and add a minimal sisyphus pill, or leave it as-is?"
6133
+
6134
+ If they say yes \u2192 fall through to Path C, option 2 (minimal pill).
6135
+
6136
+ ### Path F \u2014 tmux not ready (state: tmux-not-ready)
6137
+ **Trigger:** tmux isn't installed or no server is running.
6138
+ **Action:** ${r.state === "tmux-not-ready" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6139
+
6140
+ Don't propose statusbar changes yet:
6141
+ - tmuxInstalled=false \u2192 install tmux first (\`brew install tmux\` on macOS)
6142
+ - tmuxServerRunning=false \u2192 user needs to run \`tmux\` (or attach to a session)
6143
+
6144
+ Re-run \`sis admin check-statusbar\` once tmux is up.
6145
+
6146
+ ## Daemon reminder
6147
+ Even with status-left/right perfectly wired, the bar will show literal \`#{E:@sisyphus_left}\` (rendered as empty) until the daemon is running and has populated the option at least once. Always end this flow by checking \`daemonRunning: true\` above; if false, run \`sisyphusd start\` (or \`sisyphusd restart\` if it's stuck).
6148
+
6149
+ ## After acting
6150
+ Re-run \`sis admin check-statusbar\` to verify the new state. If anything looks wrong, the JSON form (\`sis admin check-statusbar --json\`) is easier to diff against expected values.
6151
+ </claude-instructions>
6152
+ `);
6153
+ }
6154
+ function registerCheckStatusbar(program2) {
6155
+ program2.command("check-statusbar").description("Inspect tmux statusbar state and emit a Claude decision tree for non-clobbering integration").option("--json", "Print raw JSON state instead of Claude instructions").action((opts) => {
6156
+ const result = runCheck2();
6157
+ if (opts.json) {
6158
+ console.log(JSON.stringify(result, null, 2));
6159
+ return;
6160
+ }
6161
+ printInstructions2(result);
6162
+ });
6163
+ }
6164
+
6165
+ // src/cli/commands/home-init.ts
6166
+ init_shell();
6167
+ import { execSync as execSync13 } from "child_process";
5191
6168
  function registerHomeInit(parent) {
5192
6169
  parent.command("home-init <name> <cwd>").description("Bootstrap a tmux home session with the sisyphus dashboard.").action((name, cwd) => {
5193
6170
  ensureSession(name, cwd);
@@ -5197,7 +6174,7 @@ function registerHomeInit(parent) {
5197
6174
  }
5198
6175
  function sessionExists(name) {
5199
6176
  try {
5200
- execSync11(`tmux has-session -t ${shellQuote(name)}`, { stdio: "pipe" });
6177
+ execSync13(`tmux has-session -t ${shellQuote(name)}`, { stdio: "pipe" });
5201
6178
  return true;
5202
6179
  } catch {
5203
6180
  return false;
@@ -5205,13 +6182,13 @@ function sessionExists(name) {
5205
6182
  }
5206
6183
  function ensureSession(name, cwd) {
5207
6184
  if (sessionExists(name)) return;
5208
- execSync11(
6185
+ execSync13(
5209
6186
  `tmux new-session -d -s ${shellQuote(name)} -c ${shellQuote(cwd)}`,
5210
6187
  { stdio: "pipe" }
5211
6188
  );
5212
6189
  }
5213
6190
  function setSessionCwd(name, cwd) {
5214
- execSync11(
6191
+ execSync13(
5215
6192
  `tmux set-option -t ${shellQuote(name)} @sisyphus_cwd ${shellQuote(cwd.replace(/\/+$/, ""))}`,
5216
6193
  { stdio: "pipe" }
5217
6194
  );
@@ -5219,10 +6196,10 @@ function setSessionCwd(name, cwd) {
5219
6196
 
5220
6197
  // src/cli/commands/doctor.ts
5221
6198
  init_paths();
5222
- import { execSync as execSync12 } from "child_process";
5223
- import { existsSync as existsSync15, statSync as statSync3 } from "fs";
5224
- import { homedir as homedir9 } from "os";
5225
- import { join as join16 } from "path";
6199
+ import { execSync as execSync14 } from "child_process";
6200
+ import { existsSync as existsSync17, statSync as statSync3 } from "fs";
6201
+ import { homedir as homedir11 } from "os";
6202
+ import { join as join18 } from "path";
5226
6203
  function checkNodeVersion() {
5227
6204
  const major = parseInt(process.versions.node.split(".")[0], 10);
5228
6205
  if (major < 22) {
@@ -5232,7 +6209,7 @@ function checkNodeVersion() {
5232
6209
  }
5233
6210
  function checkClaudeCli() {
5234
6211
  try {
5235
- execSync12("which claude", { stdio: "pipe" });
6212
+ execSync14("which claude", { stdio: "pipe" });
5236
6213
  return { name: "Claude CLI", status: "ok", detail: "Found on PATH" };
5237
6214
  } catch {
5238
6215
  return {
@@ -5245,7 +6222,7 @@ function checkClaudeCli() {
5245
6222
  }
5246
6223
  function checkGit() {
5247
6224
  try {
5248
- const version = execSync12("git --version", { encoding: "utf-8", stdio: "pipe" }).trim();
6225
+ const version = execSync14("git --version", { encoding: "utf-8", stdio: "pipe" }).trim();
5249
6226
  return { name: "git", status: "ok", detail: version };
5250
6227
  } catch {
5251
6228
  return { name: "git", status: "fail", detail: "Not found on PATH", fix: "Install git: https://git-scm.com/downloads" };
@@ -5253,7 +6230,7 @@ function checkGit() {
5253
6230
  }
5254
6231
  function checkTmuxVersion() {
5255
6232
  try {
5256
- const version = execSync12("tmux -V", { encoding: "utf-8", stdio: "pipe" }).trim();
6233
+ const version = execSync14("tmux -V", { encoding: "utf-8", stdio: "pipe" }).trim();
5257
6234
  const match = version.match(/(\d+\.\d+)/);
5258
6235
  if (!match) return { name: "tmux version", status: "warn", detail: `Could not parse version: ${version}` };
5259
6236
  const ver = parseFloat(match[1]);
@@ -5279,7 +6256,7 @@ function checkDaemonInstalled() {
5279
6256
  };
5280
6257
  }
5281
6258
  const pid = daemonPidPath();
5282
- if (existsSync15(pid)) {
6259
+ if (existsSync17(pid)) {
5283
6260
  return { name: "Daemon setup", status: "ok", detail: `PID file found at ${pid}` };
5284
6261
  }
5285
6262
  return {
@@ -5291,7 +6268,7 @@ function checkDaemonInstalled() {
5291
6268
  }
5292
6269
  function checkDaemonRunning() {
5293
6270
  const pid = daemonPidPath();
5294
- if (!existsSync15(pid)) {
6271
+ if (!existsSync17(pid)) {
5295
6272
  const fix = process.platform === "darwin" ? "launchctl load -w ~/Library/LaunchAgents/com.sisyphus.daemon.plist" : "sisyphusd & \u2014 or check if the process is running";
5296
6273
  return {
5297
6274
  name: "Daemon process",
@@ -5302,7 +6279,7 @@ function checkDaemonRunning() {
5302
6279
  }
5303
6280
  try {
5304
6281
  const sock = socketPath();
5305
- execSync12(`test -S "${sock}"`, { stdio: "pipe" });
6282
+ execSync14(`test -S "${sock}"`, { stdio: "pipe" });
5306
6283
  return { name: "Daemon process", status: "ok", detail: `Socket at ${sock}` };
5307
6284
  } catch {
5308
6285
  return {
@@ -5315,13 +6292,13 @@ function checkDaemonRunning() {
5315
6292
  }
5316
6293
  function checkTmux() {
5317
6294
  try {
5318
- execSync12("which tmux", { stdio: "pipe" });
6295
+ execSync14("which tmux", { stdio: "pipe" });
5319
6296
  } catch {
5320
6297
  const installHint = process.platform === "darwin" ? "brew install tmux" : "apt install tmux (Debian/Ubuntu) or your package manager";
5321
6298
  return { name: "tmux", status: "fail", detail: "Not found on PATH", fix: installHint };
5322
6299
  }
5323
6300
  try {
5324
- execSync12("tmux list-sessions", { stdio: "pipe" });
6301
+ execSync14("tmux list-sessions", { stdio: "pipe" });
5325
6302
  return { name: "tmux", status: "ok", detail: "Running" };
5326
6303
  } catch {
5327
6304
  return { name: "tmux", status: "warn", detail: "Installed but no server running" };
@@ -5329,7 +6306,7 @@ function checkTmux() {
5329
6306
  }
5330
6307
  function checkCycleScript() {
5331
6308
  const path = cycleScriptPath();
5332
- if (!existsSync15(path)) {
6309
+ if (!existsSync17(path)) {
5333
6310
  return {
5334
6311
  name: "Cycle script",
5335
6312
  status: "fail",
@@ -5354,7 +6331,7 @@ function checkCycleScript() {
5354
6331
  function checkTmuxKeybind() {
5355
6332
  const existing = getExistingBinding(DEFAULT_CYCLE_KEY);
5356
6333
  if (existing === null) {
5357
- if (existsSync15(sisyphusTmuxConfPath())) {
6334
+ if (existsSync17(sisyphusTmuxConfPath())) {
5358
6335
  return {
5359
6336
  name: `Tmux keybind (${DEFAULT_CYCLE_KEY})`,
5360
6337
  status: "warn",
@@ -5380,7 +6357,7 @@ function checkTmuxKeybind() {
5380
6357
  }
5381
6358
  function checkGlobalDir() {
5382
6359
  const dir = globalDir();
5383
- if (existsSync15(dir)) {
6360
+ if (existsSync17(dir)) {
5384
6361
  return { name: "Data directory", status: "ok", detail: dir };
5385
6362
  }
5386
6363
  return { name: "Data directory", status: "warn", detail: `${dir} does not exist (created on first use)` };
@@ -5432,7 +6409,7 @@ function checkSisyphusPlugin() {
5432
6409
  function checkTermrender() {
5433
6410
  if (isTermrenderAvailable()) {
5434
6411
  try {
5435
- const version = execSync12("termrender --version", { encoding: "utf-8", stdio: "pipe" }).trim();
6412
+ const version = execSync14("termrender --version", { encoding: "utf-8", stdio: "pipe" }).trim();
5436
6413
  return { name: "termrender", status: "ok", detail: version };
5437
6414
  } catch {
5438
6415
  return { name: "termrender", status: "ok", detail: "installed" };
@@ -5451,7 +6428,7 @@ function checkNvim() {
5451
6428
  return { name: "nvim", status: "warn", detail: "Not installed", fix };
5452
6429
  }
5453
6430
  try {
5454
- const version = execSync12("nvim --version", { encoding: "utf-8", stdio: "pipe" }).split("\n")[0]?.replace("NVIM ", "");
6431
+ const version = execSync14("nvim --version", { encoding: "utf-8", stdio: "pipe" }).split("\n")[0]?.replace("NVIM ", "");
5455
6432
  return { name: "nvim", status: "ok", detail: version ?? "installed" };
5456
6433
  } catch {
5457
6434
  return { name: "nvim", status: "ok", detail: "installed" };
@@ -5459,8 +6436,8 @@ function checkNvim() {
5459
6436
  }
5460
6437
  function checkNotifyBinary() {
5461
6438
  if (process.platform !== "darwin") return null;
5462
- const binary = join16(homedir9(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
5463
- if (existsSync15(binary)) {
6439
+ const binary = join18(homedir11(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
6440
+ if (existsSync17(binary)) {
5464
6441
  return { name: "Notifications", status: "ok", detail: "SisyphusNotify.app built" };
5465
6442
  }
5466
6443
  return {
@@ -5513,8 +6490,8 @@ function registerDoctor(program2) {
5513
6490
  }
5514
6491
 
5515
6492
  // src/cli/commands/init.ts
5516
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
5517
- import { join as join17 } from "path";
6493
+ import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
6494
+ import { join as join19 } from "path";
5518
6495
  var DEFAULT_CONFIG2 = {};
5519
6496
  var ORCHESTRATOR_TEMPLATE = `# Custom Orchestrator Prompt
5520
6497
 
@@ -5525,18 +6502,18 @@ var ORCHESTRATOR_TEMPLATE = `# Custom Orchestrator Prompt
5525
6502
  function registerInit(program2) {
5526
6503
  program2.command("init").description("Initialize sisyphus configuration for this project").option("--orchestrator", "Also create a custom orchestrator prompt template").action((opts) => {
5527
6504
  const cwd = process.cwd();
5528
- const sisDir = join17(cwd, ".sisyphus");
5529
- const configPath = join17(sisDir, "config.json");
5530
- if (existsSync16(configPath)) {
6505
+ const sisDir = join19(cwd, ".sisyphus");
6506
+ const configPath = join19(sisDir, "config.json");
6507
+ if (existsSync18(configPath)) {
5531
6508
  console.log(`Already initialized: ${configPath}`);
5532
6509
  return;
5533
6510
  }
5534
- mkdirSync7(sisDir, { recursive: true });
6511
+ mkdirSync8(sisDir, { recursive: true });
5535
6512
  writeFileSync9(configPath, JSON.stringify(DEFAULT_CONFIG2, null, 2) + "\n", "utf-8");
5536
6513
  console.log(`Created ${configPath}`);
5537
6514
  if (opts.orchestrator) {
5538
- const orchPath = join17(sisDir, "orchestrator.md");
5539
- if (!existsSync16(orchPath)) {
6515
+ const orchPath = join19(sisDir, "orchestrator.md");
6516
+ if (!existsSync18(orchPath)) {
5540
6517
  writeFileSync9(orchPath, ORCHESTRATOR_TEMPLATE, "utf-8");
5541
6518
  console.log(`Created ${orchPath}`);
5542
6519
  }
@@ -5579,7 +6556,7 @@ function registerUninstall(program2) {
5579
6556
 
5580
6557
  // src/cli/commands/configure-upload.ts
5581
6558
  init_paths();
5582
- import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
6559
+ import { chmodSync as chmodSync2, existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync21, writeFileSync as writeFileSync10 } from "fs";
5583
6560
  import { createInterface as createInterface3 } from "readline";
5584
6561
  import { dirname as dirname6 } from "path";
5585
6562
  async function readUrlFromInput(interactive) {
@@ -5637,16 +6614,16 @@ function registerConfigureUpload(program2) {
5637
6614
  const url = parsed.origin + (strippedPath.length > 0 ? strippedPath : "");
5638
6615
  const configPath = globalConfigPath();
5639
6616
  let existing = {};
5640
- if (existsSync17(configPath)) {
6617
+ if (existsSync19(configPath)) {
5641
6618
  try {
5642
- existing = JSON.parse(readFileSync19(configPath, "utf-8"));
6619
+ existing = JSON.parse(readFileSync21(configPath, "utf-8"));
5643
6620
  } catch {
5644
6621
  console.error(`Error: ${configPath} could not be parsed \u2014 fix or delete it first`);
5645
6622
  process.exit(1);
5646
6623
  }
5647
6624
  }
5648
6625
  const merged = { ...existing, upload: { url, token } };
5649
- mkdirSync8(dirname6(configPath), { recursive: true });
6626
+ mkdirSync9(dirname6(configPath), { recursive: true });
5650
6627
  writeFileSync10(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
5651
6628
  chmodSync2(configPath, 384);
5652
6629
  console.log(`\u2713 upload configured (${configPath})`);
@@ -5654,11 +6631,11 @@ function registerConfigureUpload(program2) {
5654
6631
  }
5655
6632
 
5656
6633
  // src/cli/commands/getting-started.ts
5657
- import { execSync as execSync13 } from "child_process";
5658
- import { dirname as dirname7, join as join18 } from "path";
6634
+ import { execSync as execSync15 } from "child_process";
6635
+ import { dirname as dirname7, join as join20 } from "path";
5659
6636
  import { fileURLToPath as fileURLToPath3 } from "url";
5660
6637
  function templatePath(name) {
5661
- return join18(dirname7(fileURLToPath3(import.meta.url)), "templates", name);
6638
+ return join20(dirname7(fileURLToPath3(import.meta.url)), "templates", name);
5662
6639
  }
5663
6640
  function isClaudeCode() {
5664
6641
  return !!process.env["CLAUDECODE"];
@@ -5983,18 +6960,30 @@ Two keybinds to remember (both use the RIGHT Option key):
5983
6960
  | Right Option + s | Cycle through sisyphus sessions |
5984
6961
  | Right Option + Shift + s | Jump back to dashboard |
5985
6962
 
5986
- ### 4. Verify keybinds are installed
6963
+ ### 4. Verify keybinds are installed (REQUIRED before step 5)
6964
+
6965
+ Run \`sis admin check-keybinds\` and follow the decision tree it emits. The output is
6966
+ structured for you \u2014 it tells you the current state and which path to take (Path A
6967
+ through Path F). Do NOT skip this and do NOT ask the user to test a keybind until
6968
+ both \`M-s\` and \`C-s\` read "sisyphus" in that command's output.
5987
6969
 
5988
- Run \`sis admin doctor\` and check the output. Look for:
5989
- - "Cycle script" \u2014 should be \u2713
5990
- - "Tmux keybind" \u2014 should be \u2713
5991
- - "Right Option Key" \u2014 should be "Esc+"
6970
+ Common paths:
6971
+ - **Path A (already wired):** confirm and move on.
6972
+ - **Path B (safe auto-install):** run \`sis admin setup-keybind --yes\`.
6973
+ - **Path C (would touch user's tmux.conf):** ask the user \u2014 persistent (\`--force\`) or
6974
+ live-only (no flag, non-TTY auto-declines the conf write).
6975
+ - **Path D (binding conflict):** pick alternate keys or wire directly.
6976
+ - **Path E (hidden prefix collision):** their tmux prefix is C-s; explain and offer
6977
+ alternatives.
6978
+ - **Path F (tmux not ready):** fix the precondition first.
5992
6979
 
5993
- If cycle script or keybind is missing, run: \`sis admin setup-keybind\`
6980
+ After acting, re-run \`sis admin check-keybinds\` to confirm success.
5994
6981
 
5995
6982
  ### 5. Test the keybind
5996
6983
 
5997
- Have the user try pressing Right Option + s. Nothing should happen yet (no sisyphus session running) \u2014 and that's fine. The important thing is no special character appears.
6984
+ Once check-keybinds reports both keys as "sisyphus", have the user try pressing
6985
+ Right Option + s. Nothing should happen yet (no sisyphus session running) \u2014 and that's
6986
+ fine. The important thing is no special character appears.
5998
6987
 
5999
6988
  If they see \`\xDF\` or similar, circle back to the Right Option Key setup above.
6000
6989
 
@@ -6134,11 +7123,11 @@ function printStep5() {
6134
7123
  let recentCommits = "";
6135
7124
  let topLevelFiles = "";
6136
7125
  try {
6137
- recentCommits = execSync13("git log --oneline -15 2>/dev/null", { encoding: "utf-8" }).trim();
7126
+ recentCommits = execSync15("git log --oneline -15 2>/dev/null", { encoding: "utf-8" }).trim();
6138
7127
  } catch {
6139
7128
  }
6140
7129
  try {
6141
- topLevelFiles = execSync13("ls -1 2>/dev/null", { encoding: "utf-8" }).trim();
7130
+ topLevelFiles = execSync15("ls -1 2>/dev/null", { encoding: "utf-8" }).trim();
6142
7131
  } catch {
6143
7132
  }
6144
7133
  console.log(`
@@ -6616,7 +7605,7 @@ function registerGettingStarted(program2) {
6616
7605
 
6617
7606
  // src/cli/commands/history.ts
6618
7607
  init_paths();
6619
- import { readdirSync as readdirSync6, readFileSync as readFileSync20, existsSync as existsSync18 } from "fs";
7608
+ import { readdirSync as readdirSync6, readFileSync as readFileSync22, existsSync as existsSync20 } from "fs";
6620
7609
  import { resolve as resolve6 } from "path";
6621
7610
  var RESET3 = "\x1B[0m";
6622
7611
  var BOLD3 = "\x1B[1m";
@@ -6656,13 +7645,13 @@ function splitAgentTime(agents) {
6656
7645
  }
6657
7646
  function loadAllSummaries() {
6658
7647
  const base = historyBaseDir();
6659
- if (!existsSync18(base)) return [];
7648
+ if (!existsSync20(base)) return [];
6660
7649
  const results = [];
6661
7650
  for (const name of readdirSync6(base)) {
6662
7651
  const summaryPath = historySessionSummaryPath(name);
6663
- if (existsSync18(summaryPath)) {
7652
+ if (existsSync20(summaryPath)) {
6664
7653
  try {
6665
- const raw = readFileSync20(summaryPath, "utf-8");
7654
+ const raw = readFileSync22(summaryPath, "utf-8");
6666
7655
  results.push({ id: name, summary: JSON.parse(raw) });
6667
7656
  continue;
6668
7657
  } catch {
@@ -6676,10 +7665,10 @@ function loadAllSummaries() {
6676
7665
  }
6677
7666
  function buildLiveSummary(sessionId) {
6678
7667
  const eventsPath = historyEventsPath(sessionId);
6679
- if (!existsSync18(eventsPath)) return null;
7668
+ if (!existsSync20(eventsPath)) return null;
6680
7669
  let cwd = null;
6681
7670
  try {
6682
- const lines = readFileSync20(eventsPath, "utf-8").split("\n");
7671
+ const lines = readFileSync22(eventsPath, "utf-8").split("\n");
6683
7672
  for (const line of lines) {
6684
7673
  if (!line.trim()) continue;
6685
7674
  try {
@@ -6697,10 +7686,10 @@ function buildLiveSummary(sessionId) {
6697
7686
  }
6698
7687
  if (!cwd) return null;
6699
7688
  const sPath = statePath(cwd, sessionId);
6700
- if (!existsSync18(sPath)) return null;
7689
+ if (!existsSync20(sPath)) return null;
6701
7690
  let session2;
6702
7691
  try {
6703
- session2 = JSON.parse(readFileSync20(sPath, "utf-8"));
7692
+ session2 = JSON.parse(readFileSync22(sPath, "utf-8"));
6704
7693
  } catch {
6705
7694
  return null;
6706
7695
  }
@@ -6761,8 +7750,8 @@ function buildLiveSummary(sessionId) {
6761
7750
  }
6762
7751
  function loadEvents(sessionId) {
6763
7752
  const eventsPath = historyEventsPath(sessionId);
6764
- if (!existsSync18(eventsPath)) return [];
6765
- const lines = readFileSync20(eventsPath, "utf-8").split("\n").filter((l) => l.trim());
7753
+ if (!existsSync20(eventsPath)) return [];
7754
+ const lines = readFileSync22(eventsPath, "utf-8").split("\n").filter((l) => l.trim());
6766
7755
  const events = [];
6767
7756
  for (const line of lines) {
6768
7757
  try {
@@ -6775,13 +7764,13 @@ function loadEvents(sessionId) {
6775
7764
  }
6776
7765
  function findSession(idOrName) {
6777
7766
  const summaryPath = historySessionSummaryPath(idOrName);
6778
- if (existsSync18(summaryPath)) {
7767
+ if (existsSync20(summaryPath)) {
6779
7768
  try {
6780
- return { id: idOrName, summary: JSON.parse(readFileSync20(summaryPath, "utf-8")) };
7769
+ return { id: idOrName, summary: JSON.parse(readFileSync22(summaryPath, "utf-8")) };
6781
7770
  } catch {
6782
7771
  }
6783
7772
  }
6784
- if (existsSync18(historySessionDir(idOrName))) {
7773
+ if (existsSync20(historySessionDir(idOrName))) {
6785
7774
  const live = buildLiveSummary(idOrName);
6786
7775
  if (live) return { id: idOrName, summary: live };
6787
7776
  }
@@ -7170,21 +8159,21 @@ function registerHistory(program2) {
7170
8159
  init_paths();
7171
8160
  import { execFile as execFile2 } from "child_process";
7172
8161
  import { promisify } from "util";
7173
- import { existsSync as existsSync19, readFileSync as readFileSync21, mkdirSync as mkdirSync9, symlinkSync, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
7174
- import { homedir as homedir10 } from "os";
7175
- import { join as join20 } from "path";
8162
+ import { existsSync as existsSync21, readFileSync as readFileSync23, mkdirSync as mkdirSync10, symlinkSync, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
8163
+ import { homedir as homedir12 } from "os";
8164
+ import { join as join22 } from "path";
7176
8165
  function sanitizeName(name) {
7177
8166
  return name.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
7178
8167
  }
7179
8168
  function buildOutputPath(label, dir) {
7180
8169
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7181
- mkdirSync9(dir, { recursive: true });
8170
+ mkdirSync10(dir, { recursive: true });
7182
8171
  const base = `sisyphus-${label}-${date}`;
7183
- let candidate = join20(dir, `${base}.zip`);
8172
+ let candidate = join22(dir, `${base}.zip`);
7184
8173
  let counter = 1;
7185
- while (existsSync19(candidate)) {
8174
+ while (existsSync21(candidate)) {
7186
8175
  counter++;
7187
- candidate = join20(dir, `${base}-${counter}.zip`);
8176
+ candidate = join22(dir, `${base}-${counter}.zip`);
7188
8177
  }
7189
8178
  return candidate;
7190
8179
  }
@@ -7246,33 +8235,33 @@ async function exportSessionToZip(sessionId, cwd, options) {
7246
8235
  const reveal = options?.reveal ?? true;
7247
8236
  const sessDir = sessionDir(cwd, sessionId);
7248
8237
  const histDir = historySessionDir(sessionId);
7249
- const sessExists = existsSync19(sessDir);
7250
- const histExists = existsSync19(histDir);
8238
+ const sessExists = existsSync21(sessDir);
8239
+ const histExists = existsSync21(histDir);
7251
8240
  if (!sessExists && !histExists) {
7252
8241
  throw new Error(`No data found for session ${sessionId}`);
7253
8242
  }
7254
8243
  let label = sessionId.slice(0, 8);
7255
8244
  const stPath = statePath(cwd, sessionId);
7256
- if (existsSync19(stPath)) {
8245
+ if (existsSync21(stPath)) {
7257
8246
  try {
7258
- const state = JSON.parse(readFileSync21(stPath, "utf-8"));
8247
+ const state = JSON.parse(readFileSync23(stPath, "utf-8"));
7259
8248
  if (state.name) {
7260
8249
  label = sanitizeName(state.name);
7261
8250
  }
7262
8251
  } catch {
7263
8252
  }
7264
8253
  }
7265
- const dir = options?.outputDir ?? join20(homedir10(), "Downloads");
8254
+ const dir = options?.outputDir ?? join22(homedir12(), "Downloads");
7266
8255
  const outputPath = buildOutputPath(label, dir);
7267
8256
  const tmpDir = `/tmp/sisyphus-export-${sessionId.slice(0, 8)}-${Date.now()}`;
7268
8257
  try {
7269
- mkdirSync9(tmpDir, { recursive: true });
7270
- writeFileSync11(join20(tmpDir, "CLAUDE.md"), generateGuide(), "utf-8");
8258
+ mkdirSync10(tmpDir, { recursive: true });
8259
+ writeFileSync11(join22(tmpDir, "CLAUDE.md"), generateGuide(), "utf-8");
7271
8260
  if (sessExists) {
7272
- symlinkSync(sessDir, join20(tmpDir, "session"));
8261
+ symlinkSync(sessDir, join22(tmpDir, "session"));
7273
8262
  }
7274
8263
  if (histExists) {
7275
- symlinkSync(histDir, join20(tmpDir, "history"));
8264
+ symlinkSync(histDir, join22(tmpDir, "history"));
7276
8265
  }
7277
8266
  const parts = ["CLAUDE.md", sessExists ? "session/" : "", histExists ? "history/" : ""].filter(Boolean);
7278
8267
  await execFileAsync("zip", ["-rq", outputPath, ...parts], { cwd: tmpDir });
@@ -7394,12 +8383,12 @@ function buildManifest(args2) {
7394
8383
  }
7395
8384
 
7396
8385
  // src/shared/version.ts
7397
- import { readFileSync as readFileSync22 } from "fs";
8386
+ import { readFileSync as readFileSync24 } from "fs";
7398
8387
  import { resolve as resolve7 } from "path";
7399
8388
  function readSisyphusVersion() {
7400
8389
  for (const rel of ["../package.json", "../../package.json"]) {
7401
8390
  try {
7402
- const raw = readFileSync22(resolve7(import.meta.dirname, rel), "utf-8");
8391
+ const raw = readFileSync24(resolve7(import.meta.dirname, rel), "utf-8");
7403
8392
  const pkg = JSON.parse(raw);
7404
8393
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
7405
8394
  } catch {
@@ -7494,12 +8483,13 @@ function registerUpload(program2) {
7494
8483
  }
7495
8484
 
7496
8485
  // src/cli/commands/scratch.ts
7497
- import { execSync as execSync14 } from "child_process";
8486
+ import { execSync as execSync16 } from "child_process";
8487
+ init_shell();
7498
8488
  function findHomeSession(cwd) {
7499
8489
  const normalizedCwd = cwd.replace(/\/+$/, "");
7500
8490
  let output;
7501
8491
  try {
7502
- output = execSync14('tmux list-sessions -F "#{session_id}|#{session_name}"', {
8492
+ output = execSync16('tmux list-sessions -F "#{session_id}|#{session_name}"', {
7503
8493
  encoding: "utf-8",
7504
8494
  stdio: ["pipe", "pipe", "pipe"]
7505
8495
  }).trim();
@@ -7513,7 +8503,7 @@ function findHomeSession(cwd) {
7513
8503
  const name = line.slice(pipeIdx + 1);
7514
8504
  if (name.startsWith("ssyph_")) continue;
7515
8505
  try {
7516
- const val = execSync14(
8506
+ const val = execSync16(
7517
8507
  `tmux show-options -t ${shellQuote(sessId)} -v @sisyphus_cwd`,
7518
8508
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7519
8509
  ).trim();
@@ -7529,7 +8519,7 @@ function registerScratch(program2) {
7529
8519
  const cwd = opts.cwd ?? process.env["SISYPHUS_CWD"] ?? process.cwd();
7530
8520
  const homeSession = findHomeSession(cwd);
7531
8521
  if (!homeSession) {
7532
- const current = execSync14('tmux display-message -p "#{session_name}"', {
8522
+ const current = execSync16('tmux display-message -p "#{session_name}"', {
7533
8523
  encoding: "utf-8"
7534
8524
  }).trim();
7535
8525
  openScratchWindow(current, cwd, promptParts.join(" "));
@@ -7539,7 +8529,7 @@ function registerScratch(program2) {
7539
8529
  });
7540
8530
  }
7541
8531
  function openScratchWindow(tmuxSession, cwd, prompt) {
7542
- const windowId = execSync14(
8532
+ const windowId = execSync16(
7543
8533
  `tmux new-window -t ${shellQuote(tmuxSession + ":")} -n "scratch" -c ${shellQuote(cwd)} -P -F "#{window_id}"`,
7544
8534
  { encoding: "utf-8" }
7545
8535
  ).trim();
@@ -7547,7 +8537,7 @@ function openScratchWindow(tmuxSession, cwd, prompt) {
7547
8537
  if (prompt) {
7548
8538
  cmd += ` -p ${shellQuote(prompt)}`;
7549
8539
  }
7550
- execSync14(
8540
+ execSync16(
7551
8541
  `tmux send-keys -t ${shellQuote(windowId)} ${shellQuote(cmd)} Enter`
7552
8542
  );
7553
8543
  console.log(`Scratch session opened in ${tmuxSession}`);
@@ -7555,27 +8545,27 @@ function openScratchWindow(tmuxSession, cwd, prompt) {
7555
8545
 
7556
8546
  // src/cli/commands/review.ts
7557
8547
  init_paths();
7558
- import { join as join21, resolve as resolve8, dirname as dirname8 } from "path";
7559
- import { existsSync as existsSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync12, renameSync as renameSync3, readdirSync as readdirSync7 } from "fs";
8548
+ import { join as join23, resolve as resolve8, dirname as dirname8 } from "path";
8549
+ import { existsSync as existsSync22, readFileSync as readFileSync25, writeFileSync as writeFileSync12, renameSync as renameSync3, readdirSync as readdirSync7 } from "fs";
7560
8550
  var _statusCheck = ["draft", "question", "approved", "rejected", "deferred"];
7561
8551
  function resolveContextArtifact(file, opts, filename, notFoundMessage) {
7562
8552
  const cwd = opts.cwd || process.env.SISYPHUS_CWD || process.cwd();
7563
8553
  if (file) return resolve8(file);
7564
8554
  const sessionId = opts.sessionId || process.env.SISYPHUS_SESSION_ID;
7565
8555
  if (sessionId) {
7566
- const target = join21(contextDir(cwd, sessionId), filename);
7567
- if (!existsSync20(target)) {
8556
+ const target = join23(contextDir(cwd, sessionId), filename);
8557
+ if (!existsSync22(target)) {
7568
8558
  console.error(`Error: File not found: ${target}`);
7569
8559
  process.exit(1);
7570
8560
  }
7571
8561
  return target;
7572
8562
  }
7573
8563
  const dir = sessionsDir(cwd);
7574
- if (existsSync20(dir)) {
8564
+ if (existsSync22(dir)) {
7575
8565
  const sessions = readdirSync7(dir);
7576
8566
  for (const session2 of sessions.reverse()) {
7577
- const candidate = join21(dir, session2, "context", filename);
7578
- if (existsSync20(candidate)) return candidate;
8567
+ const candidate = join23(dir, session2, "context", filename);
8568
+ if (existsSync22(candidate)) return candidate;
7579
8569
  }
7580
8570
  }
7581
8571
  console.error(`Error: ${notFoundMessage}`);
@@ -7617,16 +8607,16 @@ Examples:
7617
8607
  "requirements.json",
7618
8608
  "No requirements.json found. Provide a path or use --session-id."
7619
8609
  );
7620
- if (!existsSync20(targetPath)) {
8610
+ if (!existsSync22(targetPath)) {
7621
8611
  console.error(`Error: File not found: ${targetPath}`);
7622
8612
  process.exit(1);
7623
8613
  }
7624
- const parsed = JSON.parse(readFileSync23(targetPath, "utf-8"));
8614
+ const parsed = JSON.parse(readFileSync25(targetPath, "utf-8"));
7625
8615
  const rendered = renderRequirementsMarkdown(parsed);
7626
- const outPath = join21(dirname8(targetPath), "requirements.md");
8616
+ const outPath = join23(dirname8(targetPath), "requirements.md");
7627
8617
  const tmpPath = outPath + ".tmp";
7628
- if (existsSync20(outPath)) {
7629
- const existing = readFileSync23(outPath, "utf-8");
8618
+ if (existsSync22(outPath)) {
8619
+ const existing = readFileSync25(outPath, "utf-8");
7630
8620
  if (existing !== rendered) {
7631
8621
  if (!opts.force) {
7632
8622
  process.stderr.write(
@@ -9192,20 +10182,21 @@ function createBadgeGallery(unlockedAchievements, startIndex) {
9192
10182
 
9193
10183
  // src/daemon/companion-memory.ts
9194
10184
  init_paths();
9195
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync25, renameSync as renameSync5, writeFileSync as writeFileSync14 } from "fs";
9196
- import { dirname as dirname10, join as join23 } from "path";
10185
+ import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync27, renameSync as renameSync5, writeFileSync as writeFileSync14 } from "fs";
10186
+ import { dirname as dirname10, join as join25 } from "path";
9197
10187
  import { randomUUID as randomUUID4 } from "crypto";
9198
10188
  import { z as z2 } from "zod";
9199
10189
 
9200
10190
  // src/daemon/haiku.ts
10191
+ init_env();
9201
10192
  import { query, createSdkMcpServer } from "@r-cli/sdk";
9202
10193
  var COOLDOWN_MS = 5 * 60 * 1e3;
9203
10194
 
9204
10195
  // src/daemon/companion.ts
9205
10196
  init_paths();
9206
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync24, renameSync as renameSync4, writeFileSync as writeFileSync13 } from "fs";
10197
+ import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync26, renameSync as renameSync4, writeFileSync as writeFileSync13 } from "fs";
9207
10198
  import { randomUUID as randomUUID3 } from "crypto";
9208
- import { dirname as dirname9, join as join22 } from "path";
10199
+ import { dirname as dirname9, join as join24 } from "path";
9209
10200
 
9210
10201
  // src/shared/companion-normalize.ts
9211
10202
  function emptyStats() {
@@ -9254,20 +10245,20 @@ function normalizeCompanion(state) {
9254
10245
  // src/daemon/companion.ts
9255
10246
  function loadCompanion() {
9256
10247
  const path = companionPath();
9257
- if (!existsSync21(path)) {
10248
+ if (!existsSync23(path)) {
9258
10249
  const state2 = createDefaultCompanion();
9259
10250
  saveCompanion(state2);
9260
10251
  return state2;
9261
10252
  }
9262
- const raw = readFileSync24(path, "utf-8");
10253
+ const raw = readFileSync26(path, "utf-8");
9263
10254
  const state = JSON.parse(raw);
9264
10255
  return normalizeCompanion(state);
9265
10256
  }
9266
10257
  function saveCompanion(state) {
9267
10258
  const path = companionPath();
9268
10259
  const dir = dirname9(path);
9269
- mkdirSync10(dir, { recursive: true });
9270
- const tmp = join22(dir, `.companion.${randomUUID3()}.tmp`);
10260
+ mkdirSync11(dir, { recursive: true });
10261
+ const tmp = join24(dir, `.companion.${randomUUID3()}.tmp`);
9271
10262
  writeFileSync13(tmp, JSON.stringify(state, null, 2), "utf-8");
9272
10263
  renameSync4(tmp, path);
9273
10264
  }
@@ -9340,10 +10331,10 @@ function fillDefaults(state) {
9340
10331
  }
9341
10332
  function loadMemoryStrict() {
9342
10333
  const path = resolvedMemoryPath();
9343
- if (!existsSync22(path)) return defaultMemoryState();
10334
+ if (!existsSync24(path)) return defaultMemoryState();
9344
10335
  let raw;
9345
10336
  try {
9346
- raw = readFileSync25(path, "utf-8");
10337
+ raw = readFileSync27(path, "utf-8");
9347
10338
  } catch (err) {
9348
10339
  throw new MemoryStoreParseError(err);
9349
10340
  }
@@ -9386,15 +10377,17 @@ var ObservationZodSchema = z2.object({
9386
10377
  });
9387
10378
 
9388
10379
  // src/daemon/companion-popup.ts
9389
- import { writeFileSync as writeFileSync15, readFileSync as readFileSync26, unlinkSync as unlinkSync3, existsSync as existsSync23 } from "fs";
10380
+ import { writeFileSync as writeFileSync15, readFileSync as readFileSync28, unlinkSync as unlinkSync3, existsSync as existsSync25 } from "fs";
9390
10381
  import { tmpdir as tmpdir2 } from "os";
9391
- import { join as join24, resolve as resolve9 } from "path";
10382
+ import { join as join26, resolve as resolve9 } from "path";
10383
+ init_exec();
10384
+ init_shell();
9392
10385
  var POPUP_WIDTH = 38;
9393
10386
  var INNER_WIDTH = POPUP_WIDTH - 6;
9394
10387
  var POPUP_DURATION = 15;
9395
- var POPUP_TMP_PREFIX = join24(tmpdir2(), "sisyphus-popup");
9396
- var POPUP_SCRIPT = join24(tmpdir2(), "sisyphus-popup.sh");
9397
- var POPUP_RESULT_PREFIX = join24(tmpdir2(), "sisyphus-popup-result");
10388
+ var POPUP_TMP_PREFIX = join26(tmpdir2(), "sisyphus-popup");
10389
+ var POPUP_SCRIPT = join26(tmpdir2(), "sisyphus-popup.sh");
10390
+ var POPUP_RESULT_PREFIX = join26(tmpdir2(), "sisyphus-popup-result");
9398
10391
  var WHIP_ANIMATION_PATH = resolve9(import.meta.dirname, "../templates/whip-animation.sh");
9399
10392
  var WHIP_ANIMATION_ROWS = 12;
9400
10393
  function wrapText2(text, width) {
@@ -9438,7 +10431,7 @@ function showCommentaryPopupQueue(pages) {
9438
10431
  if (contentHeight > maxContentHeight) maxContentHeight = contentHeight;
9439
10432
  writeFileSync15(`${POPUP_TMP_PREFIX}-${i}.txt`, content);
9440
10433
  }
9441
- const whipAvailable = existsSync23(WHIP_ANIMATION_PATH);
10434
+ const whipAvailable = existsSync25(WHIP_ANIMATION_PATH);
9442
10435
  if (whipAvailable && maxContentHeight < WHIP_ANIMATION_ROWS + 2) {
9443
10436
  maxContentHeight = WHIP_ANIMATION_ROWS + 2;
9444
10437
  }
@@ -9512,7 +10505,7 @@ fi
9512
10505
  }
9513
10506
  let raw;
9514
10507
  try {
9515
- raw = readFileSync26(POPUP_RESULT_PREFIX, "utf8").trim();
10508
+ raw = readFileSync28(POPUP_RESULT_PREFIX, "utf8").trim();
9516
10509
  } catch {
9517
10510
  return null;
9518
10511
  } finally {
@@ -9689,6 +10682,10 @@ function registerCompanion(program2) {
9689
10682
  const context = buildCompanionContext(opts.cwd);
9690
10683
  process.stdout.write(JSON.stringify({ additionalContext: context }));
9691
10684
  });
10685
+ companion.command("pane").description("Open (or focus) a side claude pane next to the dashboard").option("--cwd <path>", "Project directory", process.cwd()).action(async (opts) => {
10686
+ const { openCompanionPane: openCompanionPane2 } = await Promise.resolve().then(() => (init_tmux(), tmux_exports));
10687
+ openCompanionPane2(opts.cwd);
10688
+ });
9692
10689
  companion.command("popup-test").description("Show a test commentary popup to validate feedback key handling").option("--text <text>", "Custom popup text", "Cycle complete. Everything went exactly as planned. Nothing suspicious here.").action((opts) => {
9693
10690
  const feedback = showCommentaryPopup(opts.text);
9694
10691
  if (feedback === null) {
@@ -9705,14 +10702,15 @@ function registerCompanion(program2) {
9705
10702
  }
9706
10703
 
9707
10704
  // src/cli/commands/deploy.ts
9708
- import { homedir as homedir11 } from "os";
9709
- import { join as join25 } from "path";
10705
+ import { homedir as homedir13 } from "os";
10706
+ import { join as join28 } from "path";
9710
10707
 
9711
10708
  // src/cli/deploy/runner.ts
9712
10709
  init_paths();
9713
- import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
9714
- import { copyFileSync as copyFileSync2, existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync29 } from "fs";
10710
+ init_exec();
9715
10711
  init_creds();
10712
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
10713
+ import { copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync15, readFileSync as readFileSync32 } from "fs";
9716
10714
 
9717
10715
  // src/cli/deploy/pricing.ts
9718
10716
  var LAST_VERIFIED = "2026-05-06";
@@ -9747,12 +10745,12 @@ function formatCostLine(provider, instanceType) {
9747
10745
  // src/cli/deploy/runtime.ts
9748
10746
  init_atomic();
9749
10747
  init_paths();
9750
- import { existsSync as existsSync25, readFileSync as readFileSync28, unlinkSync as unlinkSync4 } from "fs";
10748
+ import { existsSync as existsSync28, readFileSync as readFileSync31, unlinkSync as unlinkSync4 } from "fs";
9751
10749
  function readRuntimeState(provider) {
9752
10750
  const path = deployRuntimePath(provider);
9753
- if (!existsSync25(path)) return null;
10751
+ if (!existsSync28(path)) return null;
9754
10752
  try {
9755
- return JSON.parse(readFileSync28(path, "utf-8"));
10753
+ return JSON.parse(readFileSync31(path, "utf-8"));
9756
10754
  } catch {
9757
10755
  return null;
9758
10756
  }
@@ -9762,10 +10760,11 @@ function writeRuntimeState(provider, state) {
9762
10760
  }
9763
10761
  function clearRuntimeState(provider) {
9764
10762
  const path = deployRuntimePath(provider);
9765
- if (existsSync25(path)) unlinkSync4(path);
10763
+ if (existsSync28(path)) unlinkSync4(path);
9766
10764
  }
9767
10765
 
9768
10766
  // src/cli/deploy/tailnet.ts
10767
+ init_exec();
9769
10768
  function discoverNode(requestedName) {
9770
10769
  const json = execSafe("tailscale status --json");
9771
10770
  if (!json) return null;
@@ -9811,15 +10810,15 @@ function isTailscaleAvailable() {
9811
10810
  }
9812
10811
 
9813
10812
  // src/cli/deploy/templates.ts
9814
- import { existsSync as existsSync26 } from "fs";
10813
+ import { existsSync as existsSync29 } from "fs";
9815
10814
  import { dirname as dirname11, resolve as resolve10 } from "path";
9816
10815
  import { fileURLToPath as fileURLToPath4 } from "url";
9817
10816
  function deployRoot() {
9818
10817
  const here = dirname11(fileURLToPath4(import.meta.url));
9819
10818
  const bundled = resolve10(here, "..", "deploy");
9820
- if (existsSync26(bundled)) return bundled;
10819
+ if (existsSync29(bundled)) return bundled;
9821
10820
  const sourceRoot = resolve10(here, "..", "..", "..", "deploy");
9822
- if (existsSync26(sourceRoot)) return sourceRoot;
10821
+ if (existsSync29(sourceRoot)) return sourceRoot;
9823
10822
  throw new Error(
9824
10823
  `Could not locate deploy/ templates. Looked at:
9825
10824
  ${bundled}
@@ -10003,14 +11002,14 @@ function ensureTerraformInstalled() {
10003
11002
  function ensureProviderStateDir(provider) {
10004
11003
  ensureDeployDir();
10005
11004
  const dir = deployProviderDir(provider);
10006
- if (!existsSync27(dir)) mkdirSync13(dir, { recursive: true, mode: 448 });
11005
+ if (!existsSync30(dir)) mkdirSync15(dir, { recursive: true, mode: 448 });
10007
11006
  }
10008
11007
  function backupState(provider) {
10009
11008
  const src = deployStatePath(provider);
10010
- if (existsSync27(src)) copyFileSync2(src, deployStateBackupPath(provider));
11009
+ if (existsSync30(src)) copyFileSync2(src, deployStateBackupPath(provider));
10011
11010
  }
10012
11011
  function readSshPubkey(path) {
10013
- if (!existsSync27(path)) {
11012
+ if (!existsSync30(path)) {
10014
11013
  const privateKeyPath = path.replace(/\.pub$/, "");
10015
11014
  throw new Error(
10016
11015
  `SSH pubkey not found at ${path}. Generate one with:
@@ -10018,7 +11017,7 @@ function readSshPubkey(path) {
10018
11017
  or pass --ssh-key <path>.`
10019
11018
  );
10020
11019
  }
10021
- return readFileSync29(path, "utf-8").trim();
11020
+ return readFileSync32(path, "utf-8").trim();
10022
11021
  }
10023
11022
  function readOutputs(provider) {
10024
11023
  const result = spawnSync3("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
@@ -10045,7 +11044,7 @@ function readOutputs(provider) {
10045
11044
  }
10046
11045
  }
10047
11046
  function isProvisioned(provider) {
10048
- if (!existsSync27(deployStatePath(provider))) return false;
11047
+ if (!existsSync30(deployStatePath(provider))) return false;
10049
11048
  return readOutputs(provider) !== null;
10050
11049
  }
10051
11050
  async function deployUp(provider, opts) {
@@ -10131,7 +11130,7 @@ Applied \u2014 but could not parse outputs. Run \`sis deploy ${provider} status\
10131
11130
  console.log("");
10132
11131
  }
10133
11132
  async function deployDown(provider, opts) {
10134
- if (!existsSync27(deployStatePath(provider))) {
11133
+ if (!existsSync30(deployStatePath(provider))) {
10135
11134
  console.log(`No ${provider} state found at ${deployStatePath(provider)}. Nothing to destroy.`);
10136
11135
  return;
10137
11136
  }
@@ -10296,7 +11295,7 @@ function registerDeploy(program2) {
10296
11295
  });
10297
11296
  for (const provider of PROVIDERS) {
10298
11297
  const sub = deploy.command(provider).description(`${provider} commands.`);
10299
- sub.command("up").description(`Provision the ${provider} box (terraform init \u2192 plan \u2192 apply).`).option("--region <region>", `Provider region (defaults: hetzner=nbg1, aws=us-east-1).`).option("--arch <arch>", "'arm' (default) or 'x86'. Picks the default --size and image.", "arm").option("--size <size>", "Instance type override (defaults follow --arch).").option("--ssh-key <path>", "Path to SSH public key.", join25(homedir11(), ".ssh", "id_ed25519.pub")).option("--no-chromium", "Skip headless Chromium install.").option("--no-auto-update", "Skip the daily auto-update systemd timer.").option("--name <name>", "Box hostname / Tailscale node name.", "sisyphus").option("-y, --yes", "Skip the re-provision confirmation prompt when state already exists.").action(async (raw) => {
11298
+ sub.command("up").description(`Provision the ${provider} box (terraform init \u2192 plan \u2192 apply).`).option("--region <region>", `Provider region (defaults: hetzner=nbg1, aws=us-east-1).`).option("--arch <arch>", "'arm' (default) or 'x86'. Picks the default --size and image.", "arm").option("--size <size>", "Instance type override (defaults follow --arch).").option("--ssh-key <path>", "Path to SSH public key.", join28(homedir13(), ".ssh", "id_ed25519.pub")).option("--no-chromium", "Skip headless Chromium install.").option("--no-auto-update", "Skip the daily auto-update systemd timer.").option("--name <name>", "Box hostname / Tailscale node name.", "sisyphus").option("-y, --yes", "Skip the re-provision confirmation prompt when state already exists.").action(async (raw) => {
10300
11299
  const opts = resolveUpOptions(provider, raw);
10301
11300
  await deployUp(provider, opts);
10302
11301
  });
@@ -10320,11 +11319,14 @@ function registerDeploy(program2) {
10320
11319
 
10321
11320
  // src/cli/cloud/runner.ts
10322
11321
  init_paths();
11322
+ init_shell();
11323
+ init_exec();
11324
+ init_creds();
10323
11325
  import { spawn as spawn4 } from "child_process";
10324
11326
  import { hostname } from "os";
10325
- init_creds();
10326
11327
 
10327
11328
  // src/cli/deploy/ssh-exec.ts
11329
+ init_exec();
10328
11330
  import { spawn as spawn3, spawnSync as spawnSync4 } from "child_process";
10329
11331
  function runOnBox(provider, cmd) {
10330
11332
  const target = effectiveSshTarget(provider);
@@ -10355,6 +11357,7 @@ function runOnBoxStreaming(provider, cmd) {
10355
11357
  }
10356
11358
 
10357
11359
  // src/cli/cloud/grove.ts
11360
+ init_shell();
10358
11361
  var GROVE_VERSION = "0.2.13";
10359
11362
  function ensureGroveInstalled(provider) {
10360
11363
  const probe = runOnBox(provider, "command -v grove >/dev/null 2>&1");
@@ -10374,9 +11377,10 @@ function ensureGroveRegistered(provider, repo, instancePath) {
10374
11377
  }
10375
11378
 
10376
11379
  // src/cli/cloud/repo.ts
11380
+ init_exec();
10377
11381
  import { spawnSync as spawnSync5 } from "child_process";
10378
- import { existsSync as existsSync28 } from "fs";
10379
- import { basename as basename6, join as join26 } from "path";
11382
+ import { existsSync as existsSync31 } from "fs";
11383
+ import { basename as basename6, join as join29 } from "path";
10380
11384
  function captureGit(args2) {
10381
11385
  const result = spawnSync5("git", args2, {
10382
11386
  encoding: "utf-8",
@@ -10427,10 +11431,10 @@ function buildRsyncArgs(localDir, remoteTarget) {
10427
11431
  ];
10428
11432
  }
10429
11433
  function detectPackageManager(toplevel) {
10430
- if (existsSync28(join26(toplevel, "pnpm-lock.yaml"))) return "pnpm";
10431
- if (existsSync28(join26(toplevel, "bun.lockb"))) return "bun";
10432
- if (existsSync28(join26(toplevel, "yarn.lock"))) return "yarn";
10433
- if (existsSync28(join26(toplevel, "package-lock.json"))) return "npm";
11434
+ if (existsSync31(join29(toplevel, "pnpm-lock.yaml"))) return "pnpm";
11435
+ if (existsSync31(join29(toplevel, "bun.lockb"))) return "bun";
11436
+ if (existsSync31(join29(toplevel, "yarn.lock"))) return "yarn";
11437
+ if (existsSync31(join29(toplevel, "package-lock.json"))) return "npm";
10434
11438
  return null;
10435
11439
  }
10436
11440
  function packageManagerInstallCmd(pm) {
@@ -10450,6 +11454,7 @@ function packageManagerInstallCmd(pm) {
10450
11454
 
10451
11455
  // src/cli/cloud/sidecar.ts
10452
11456
  init_paths();
11457
+ init_shell();
10453
11458
  function readSidecar(provider, repo) {
10454
11459
  const path = boxCloudSidecarPath(repo);
10455
11460
  const result = runOnBox(provider, `cat ${shellQuoteHomePath(path)} 2>/dev/null`);
@@ -10658,6 +11663,7 @@ function pickProvider(explicit) {
10658
11663
  }
10659
11664
 
10660
11665
  // src/cli/commands/cloud.ts
11666
+ init_shell();
10661
11667
  function resolve11(raw) {
10662
11668
  const provider = pickProvider(raw.provider);
10663
11669
  const repo = raw.name ? raw.name : inferRepoName();
@@ -10712,8 +11718,8 @@ function attachNotify(diagnostic2) {
10712
11718
 
10713
11719
  // src/cli/commands/tmux-sessions.ts
10714
11720
  init_paths();
10715
- import { execSync as execSync15 } from "child_process";
10716
- import { readFileSync as readFileSync30, existsSync as existsSync29 } from "fs";
11721
+ import { execSync as execSync18 } from "child_process";
11722
+ import { readFileSync as readFileSync33, existsSync as existsSync32 } from "fs";
10717
11723
  var DOT_MAP = {
10718
11724
  "orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
10719
11725
  "orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
@@ -10724,16 +11730,16 @@ var DOT_MAP = {
10724
11730
  };
10725
11731
  function readManifest() {
10726
11732
  const p = sessionsManifestPath();
10727
- if (!existsSync29(p)) return null;
11733
+ if (!existsSync32(p)) return null;
10728
11734
  try {
10729
- return JSON.parse(readFileSync30(p, "utf-8"));
11735
+ return JSON.parse(readFileSync33(p, "utf-8"));
10730
11736
  } catch {
10731
11737
  return null;
10732
11738
  }
10733
11739
  }
10734
11740
  function tmuxExec(cmd) {
10735
11741
  try {
10736
- return execSync15(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
11742
+ return execSync18(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10737
11743
  } catch {
10738
11744
  return null;
10739
11745
  }
@@ -10772,7 +11778,7 @@ if (nodeVersion < 22) {
10772
11778
  var program = new Command();
10773
11779
  program.name("sis").description("tmux-integrated orchestration daemon for Claude Code").version(
10774
11780
  JSON.parse(
10775
- readFileSync31(join27(dirname12(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
11781
+ readFileSync34(join30(dirname12(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
10776
11782
  ).version
10777
11783
  );
10778
11784
  program.configureHelp({
@@ -10797,6 +11803,7 @@ registerReconnect(session);
10797
11803
  registerClone(session);
10798
11804
  registerSessionTask(session);
10799
11805
  registerSessionEffort(session);
11806
+ registerSessionDangerous(session);
10800
11807
  registerSessionContext(session);
10801
11808
  var agent = program.command("agent").description("Manage agents");
10802
11809
  registerSpawn(agent);
@@ -10813,6 +11820,8 @@ registerSegmentUnregister(segment);
10813
11820
  var admin = program.command("admin").description("Admin / setup commands");
10814
11821
  registerSetup(admin);
10815
11822
  registerSetupKeybind(admin);
11823
+ registerCheckKeybinds(admin);
11824
+ registerCheckStatusbar(admin);
10816
11825
  registerHomeInit(admin);
10817
11826
  registerDoctor(admin);
10818
11827
  registerInit(admin);
@@ -10843,8 +11852,8 @@ Run 'sis admin getting-started' for a complete usage guide.
10843
11852
  var args = process.argv.slice(2);
10844
11853
  var firstArg = args[0];
10845
11854
  var skipWelcome = ["admin", "help", "--help", "-h", "--version", "-V"];
10846
- if (!existsSync30(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
10847
- mkdirSync14(globalDir(), { recursive: true });
11855
+ if (!existsSync33(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
11856
+ mkdirSync16(globalDir(), { recursive: true });
10848
11857
  console.log("");
10849
11858
  console.log(" Welcome to Sisyphus. Run 'sis admin setup' to get started.");
10850
11859
  console.log("");