sisyphi 1.1.34 → 1.1.36

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 mkdirSync15, readFileSync as readFileSync31 } 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)) mkdirSync15(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(readFileSync31(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 mkdirSync17, readFileSync as readFileSync35 } from "fs";
672
+ import { dirname as dirname13, join as join31 } 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";
@@ -4163,16 +4559,55 @@ function readFileSafe(filePath) {
4163
4559
  function escapeXml(s) {
4164
4560
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4165
4561
  }
4166
- function buildCompanionContext(cwd) {
4562
+ function buildSessionBlock(cwd, session2) {
4563
+ const lines = [];
4564
+ const nameAttr = session2.name ? ` name="${escapeXml(session2.name)}"` : "";
4565
+ lines.push(` <session id="${escapeXml(session2.id)}"${nameAttr} status="${escapeXml(session2.status)}">`);
4566
+ lines.push(` <task>${escapeXml(session2.task)}</task>`);
4567
+ lines.push(` <created>${escapeXml(session2.createdAt)}</created>`);
4568
+ lines.push(` <cycles>${session2.orchestratorCycles.length}</cycles>`);
4569
+ if (session2.status === "completed") {
4570
+ if (session2.completionReport) {
4571
+ const snippet = session2.completionReport.slice(0, 300).replace(/\n+/g, " ").trim();
4572
+ lines.push(` <completion-report>${escapeXml(snippet)}${session2.completionReport.length > 300 ? "\u2026" : ""}</completion-report>`);
4573
+ }
4574
+ } else {
4575
+ if (session2.agents.length > 0) {
4576
+ const counts = /* @__PURE__ */ new Map();
4577
+ for (const agent2 of session2.agents) {
4578
+ counts.set(agent2.status, (counts.get(agent2.status) ?? 0) + 1);
4579
+ }
4580
+ const summary = [...counts.entries()].map(([status, n]) => `${n} ${status}`).join(", ");
4581
+ lines.push(` <agents>${escapeXml(summary)}</agents>`);
4582
+ }
4583
+ const goalContent = readFileSafe(goalPath(cwd, session2.id));
4584
+ if (goalContent) {
4585
+ const firstLine = goalContent.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
4586
+ if (firstLine) lines.push(` <goal>${escapeXml(firstLine)}</goal>`);
4587
+ }
4588
+ const roadmapContent = readFileSafe(roadmapPath(cwd, session2.id));
4589
+ if (roadmapContent) {
4590
+ const todos = roadmapContent.split("\n").filter((l) => l.includes("- [ ]")).slice(0, 5).map((l) => l.trim());
4591
+ if (todos.length > 0) {
4592
+ lines.push(" <todos>");
4593
+ for (const todo of todos) lines.push(` ${escapeXml(todo)}`);
4594
+ lines.push(" </todos>");
4595
+ }
4596
+ }
4597
+ }
4598
+ lines.push(" </session>");
4599
+ return lines.join("\n");
4600
+ }
4601
+ function buildCompanionContextBlocks(cwd) {
4167
4602
  let sessionDirs;
4168
4603
  try {
4169
4604
  sessionDirs = readdirSync4(sessionsDir(cwd));
4170
4605
  } catch {
4171
- return "<sessions>No sessions found.</sessions>";
4606
+ return {};
4172
4607
  }
4173
4608
  const now = Date.now();
4174
4609
  const sevenDaysMs = 7 * 24 * 60 * 60 * 1e3;
4175
- const sessionBlocks = [];
4610
+ const blocks = {};
4176
4611
  for (const sessionId of sessionDirs) {
4177
4612
  const stateRaw = readFileSafe(statePath(cwd, sessionId));
4178
4613
  if (!stateRaw) continue;
@@ -4185,48 +4620,31 @@ function buildCompanionContext(cwd) {
4185
4620
  if (session2.status === "completed" && session2.completedAt) {
4186
4621
  if (now - new Date(session2.completedAt).getTime() > sevenDaysMs) continue;
4187
4622
  }
4188
- const lines = [];
4189
- const nameAttr = session2.name ? ` name="${escapeXml(session2.name)}"` : "";
4190
- lines.push(` <session id="${escapeXml(session2.id)}"${nameAttr} status="${escapeXml(session2.status)}">`);
4191
- lines.push(` <task>${escapeXml(session2.task)}</task>`);
4192
- lines.push(` <created>${escapeXml(session2.createdAt)}</created>`);
4193
- lines.push(` <cycles>${session2.orchestratorCycles.length}</cycles>`);
4194
- if (session2.status === "completed") {
4195
- if (session2.completionReport) {
4196
- const snippet = session2.completionReport.slice(0, 300).replace(/\n+/g, " ").trim();
4197
- lines.push(` <completion-report>${escapeXml(snippet)}${session2.completionReport.length > 300 ? "\u2026" : ""}</completion-report>`);
4198
- }
4199
- } else {
4200
- if (session2.agents.length > 0) {
4201
- const counts = /* @__PURE__ */ new Map();
4202
- for (const agent2 of session2.agents) {
4203
- counts.set(agent2.status, (counts.get(agent2.status) ?? 0) + 1);
4204
- }
4205
- const summary = [...counts.entries()].map(([status, n]) => `${n} ${status}`).join(", ");
4206
- lines.push(` <agents>${escapeXml(summary)}</agents>`);
4207
- }
4208
- const goalContent = readFileSafe(goalPath(cwd, session2.id));
4209
- if (goalContent) {
4210
- const firstLine = goalContent.split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
4211
- if (firstLine) lines.push(` <goal>${escapeXml(firstLine)}</goal>`);
4212
- }
4213
- const roadmapContent = readFileSafe(roadmapPath(cwd, session2.id));
4214
- if (roadmapContent) {
4215
- const todos = roadmapContent.split("\n").filter((l) => l.includes("- [ ]")).slice(0, 5).map((l) => l.trim());
4216
- if (todos.length > 0) {
4217
- lines.push(" <todos>");
4218
- for (const todo of todos) lines.push(` ${escapeXml(todo)}`);
4219
- lines.push(" </todos>");
4220
- }
4221
- }
4222
- }
4223
- lines.push(" </session>");
4224
- sessionBlocks.push(lines.join("\n"));
4623
+ blocks[session2.id] = buildSessionBlock(cwd, session2);
4225
4624
  }
4226
- if (sessionBlocks.length === 0) {
4227
- return "<sessions>No sessions found.</sessions>";
4625
+ return blocks;
4626
+ }
4627
+ function renderFullContext(blocks) {
4628
+ const entries = Object.values(blocks);
4629
+ if (entries.length === 0) return "<sessions>No sessions found.</sessions>";
4630
+ return ["<sessions>", ...entries, "</sessions>"].join("\n");
4631
+ }
4632
+ function renderContextDelta(prev, next) {
4633
+ const ids = /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(next)]);
4634
+ const entries = [];
4635
+ for (const id of ids) {
4636
+ const p = prev[id];
4637
+ const n = next[id];
4638
+ if (p === void 0 && n !== void 0) {
4639
+ entries.push(n.replace(/^(\s*)<session /, `$1<session change="added" `));
4640
+ } else if (p !== void 0 && n === void 0) {
4641
+ entries.push(` <session id="${escapeXml(id)}" change="removed" />`);
4642
+ } else if (p !== n && n !== void 0) {
4643
+ entries.push(n.replace(/^(\s*)<session /, `$1<session change="updated" `));
4644
+ }
4228
4645
  }
4229
- return ["<sessions>", ...sessionBlocks, "</sessions>"].join("\n");
4646
+ if (entries.length === 0) return null;
4647
+ return ["<sessions-changed-since-last-prompt>", ...entries, "</sessions-changed-since-last-prompt>"].join("\n");
4230
4648
  }
4231
4649
  function buildSessionContext(session2, cwd) {
4232
4650
  const goal = readFileSafe(goalPath(cwd, session2.id));
@@ -4293,14 +4711,14 @@ function registerSessionContext(program2) {
4293
4711
  }
4294
4712
 
4295
4713
  // src/cli/commands/spawn.ts
4296
- import { existsSync as existsSync12 } from "fs";
4297
- import { join as join14, resolve as resolve5 } from "path";
4714
+ import { existsSync as existsSync13 } from "fs";
4715
+ import { join as join15, resolve as resolve5 } from "path";
4298
4716
 
4299
4717
  // src/daemon/frontmatter.ts
4300
4718
  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";
4719
+ import { readFileSync as readFileSync16, existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
4720
+ import { homedir as homedir8 } from "os";
4721
+ import { join as join14, basename as basename4 } from "path";
4304
4722
  function parseAgentFrontmatter(content) {
4305
4723
  const match = content.match(/^---\n([\s\S]*?)\n---/);
4306
4724
  if (!match) return {};
@@ -4350,7 +4768,7 @@ function discoverAgentTypes(pluginDir, cwd) {
4350
4768
  if (seen.has(qualifiedName)) continue;
4351
4769
  seen.add(qualifiedName);
4352
4770
  try {
4353
- const content = readFileSync16(join13(dir, file), "utf-8");
4771
+ const content = readFileSync16(join14(dir, file), "utf-8");
4354
4772
  const fm = parseAgentFrontmatter(content);
4355
4773
  results.push({ qualifiedName, source, description: fm.description, model: fm.model });
4356
4774
  } catch {
@@ -4358,13 +4776,13 @@ function discoverAgentTypes(pluginDir, cwd) {
4358
4776
  }
4359
4777
  }
4360
4778
  }
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");
4779
+ scanDir(join14(projectAgentPluginDir(cwd), "agents"), null, "project-sis");
4780
+ scanDir(join14(userAgentPluginDir(), "agents"), null, "user-sis");
4781
+ scanDir(join14(cwd, ".claude", "agents"), null, "project");
4782
+ scanDir(join14(homedir8(), ".claude", "agents"), null, "user");
4783
+ scanDir(join14(pluginDir, "agents"), "sisyphus", "bundled");
4366
4784
  try {
4367
- const registryPath = join13(homedir7(), ".claude", "plugins", "installed_plugins.json");
4785
+ const registryPath = join14(homedir8(), ".claude", "plugins", "installed_plugins.json");
4368
4786
  const registry = JSON.parse(readFileSync16(registryPath, "utf-8"));
4369
4787
  const pluginEntries = registry.plugins ?? registry;
4370
4788
  for (const key of Object.keys(pluginEntries)) {
@@ -4374,7 +4792,7 @@ function discoverAgentTypes(pluginDir, cwd) {
4374
4792
  const entry = pluginEntries[key];
4375
4793
  const installPath = Array.isArray(entry) ? entry[0]?.installPath : entry?.installPath;
4376
4794
  if (installPath) {
4377
- scanDir(join13(installPath, "agents"), namespace, "plugin");
4795
+ scanDir(join14(installPath, "agents"), namespace, "plugin");
4378
4796
  }
4379
4797
  }
4380
4798
  } catch {
@@ -4444,8 +4862,8 @@ function registerSpawn(program2) {
4444
4862
  process.exit(1);
4445
4863
  }
4446
4864
  if (opts.repo && opts.repo !== ".") {
4447
- const repoPath = join14(sisyphusCwd, opts.repo);
4448
- if (!existsSync12(repoPath)) {
4865
+ const repoPath = join15(sisyphusCwd, opts.repo);
4866
+ if (!existsSync13(repoPath)) {
4449
4867
  console.error(`Error: repo directory does not exist: ${repoPath}`);
4450
4868
  process.exit(1);
4451
4869
  }
@@ -4552,7 +4970,7 @@ function registerReport(program2) {
4552
4970
  }
4553
4971
 
4554
4972
  // src/cli/commands/await.ts
4555
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
4973
+ import { existsSync as existsSync14, readFileSync as readFileSync17 } from "fs";
4556
4974
  var AWAIT_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
4557
4975
  function registerAwait(program2) {
4558
4976
  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 +4994,7 @@ function registerAwait(program2) {
4576
4994
  const shortType = agentType && agentType !== "worker" ? agentType.replace(/^sisyphus:/, "") : "";
4577
4995
  const label = shortType ? `${shortType}-${agentName}` : agentName;
4578
4996
  console.log(`[${status}] ${agentId} (${label})`);
4579
- if (reportPath && existsSync13(reportPath)) {
4997
+ if (reportPath && existsSync14(reportPath)) {
4580
4998
  try {
4581
4999
  const body = readFileSync17(reportPath, "utf-8");
4582
5000
  if (body.length > 0) {
@@ -4706,9 +5124,9 @@ import { execSync as execSync10 } from "child_process";
4706
5124
 
4707
5125
  // src/cli/onboard.ts
4708
5126
  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";
5127
+ import { existsSync as existsSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync8 } from "fs";
5128
+ import { homedir as homedir9 } from "os";
5129
+ import { dirname as dirname5, join as join16 } from "path";
4712
5130
  import { fileURLToPath as fileURLToPath2 } from "url";
4713
5131
  function detectTerminal() {
4714
5132
  const termProgram = process.env["TERM_PROGRAM"] || "";
@@ -4745,8 +5163,8 @@ function checkItermOptionKey() {
4745
5163
  if (process.platform !== "darwin") {
4746
5164
  return { checked: false, allCorrect: true, incorrectProfiles: [] };
4747
5165
  }
4748
- const plistPath2 = join15(homedir8(), "Library", "Preferences", "com.googlecode.iterm2.plist");
4749
- if (!existsSync14(plistPath2)) {
5166
+ const plistPath2 = join16(homedir9(), "Library", "Preferences", "com.googlecode.iterm2.plist");
5167
+ if (!existsSync15(plistPath2)) {
4750
5168
  return { checked: false, allCorrect: false, incorrectProfiles: [] };
4751
5169
  }
4752
5170
  try {
@@ -4770,7 +5188,7 @@ function checkItermOptionKey() {
4770
5188
  }
4771
5189
  }
4772
5190
  function hasExistingTmuxConf() {
4773
- return existsSync14(join15(homedir8(), ".tmux.conf")) || existsSync14(join15(homedir8(), ".config", "tmux", "tmux.conf"));
5191
+ return existsSync15(join16(homedir9(), ".tmux.conf")) || existsSync15(join16(homedir9(), ".config", "tmux", "tmux.conf"));
4774
5192
  }
4775
5193
  var SISYPHUS_DEFAULTS_MARKER = "# sisyphus-managed \u2014 do not edit";
4776
5194
  function buildTmuxDefaults() {
@@ -4882,7 +5300,7 @@ source-file -q ${sisyphusConf} ${SISYPHUS_DEFAULTS_MARKER}
4882
5300
  `;
4883
5301
  }
4884
5302
  function writeTmuxDefaults() {
4885
- const confPath = join15(homedir8(), ".tmux.conf");
5303
+ const confPath = join16(homedir9(), ".tmux.conf");
4886
5304
  writeFileSync8(confPath, buildTmuxDefaults(), "utf8");
4887
5305
  }
4888
5306
  function isNvimAvailable() {
@@ -4901,19 +5319,19 @@ function getNvimVersion() {
4901
5319
  }
4902
5320
  }
4903
5321
  function hasLazyVimConfig() {
4904
- return existsSync14(join15(homedir8(), ".config", "nvim", "lazy-lock.json"));
5322
+ return existsSync15(join16(homedir9(), ".config", "nvim", "lazy-lock.json"));
4905
5323
  }
4906
5324
  function bundledBaleiaPluginPath() {
4907
5325
  const distDir = dirname5(fileURLToPath2(import.meta.url));
4908
- return join15(distDir, "templates", "baleia.lua");
5326
+ return join16(distDir, "templates", "baleia.lua");
4909
5327
  }
4910
5328
  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;
5329
+ const pluginsDir = join16(homedir9(), ".config", "nvim", "lua", "plugins");
5330
+ if (!existsSync15(pluginsDir)) return false;
5331
+ const dest = join16(pluginsDir, "sisyphus-baleia.lua");
5332
+ if (existsSync15(dest)) return true;
4915
5333
  const src = bundledBaleiaPluginPath();
4916
- if (!existsSync14(src)) return false;
5334
+ if (!existsSync15(src)) return false;
4917
5335
  try {
4918
5336
  writeFileSync8(dest, readFileSync18(src, "utf-8"), "utf8");
4919
5337
  return true;
@@ -4938,9 +5356,9 @@ function tryAutoInstallNvim() {
4938
5356
  if (!isNvimAvailable()) {
4939
5357
  return { installed: false, autoInstalled: false, version: "", lazyVimInstalled: false, baleiaInstalled: false };
4940
5358
  }
4941
- const nvimConfigDir = join15(homedir8(), ".config", "nvim");
5359
+ const nvimConfigDir = join16(homedir9(), ".config", "nvim");
4942
5360
  let lazyVimInstalled = false;
4943
- if (!existsSync14(nvimConfigDir)) {
5361
+ if (!existsSync15(nvimConfigDir)) {
4944
5362
  const cloneCmd = [
4945
5363
  "git",
4946
5364
  "-c core.hooksPath=/dev/null",
@@ -4956,8 +5374,8 @@ function tryAutoInstallNvim() {
4956
5374
  stdio: "inherit",
4957
5375
  env: { ...process.env, GIT_LFS_SKIP_SMUDGE: "1" }
4958
5376
  });
4959
- const gitDir = join15(nvimConfigDir, ".git");
4960
- if (existsSync14(gitDir)) {
5377
+ const gitDir = join16(nvimConfigDir, ".git");
5378
+ if (existsSync15(gitDir)) {
4961
5379
  execSync9(`rm -rf "${gitDir}"`, { stdio: "pipe" });
4962
5380
  }
4963
5381
  lazyVimInstalled = true;
@@ -5128,7 +5546,7 @@ function printResults(result, daemonOk, keybindMsg) {
5128
5546
  console.log("");
5129
5547
  }
5130
5548
  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) => {
5549
+ 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
5550
  const result = runOnboarding();
5133
5551
  let daemonOk = false;
5134
5552
  try {
@@ -5140,11 +5558,15 @@ function registerSetup(program2) {
5140
5558
  const keybindResult = await setupTmuxKeybind(
5141
5559
  DEFAULT_CYCLE_KEY,
5142
5560
  DEFAULT_PREFIX_KEY,
5143
- { assumeYes: opts.yes }
5561
+ { assumeYes: opts.yes, force: opts.force }
5144
5562
  );
5145
5563
  let keybindMsg;
5146
5564
  if (keybindResult.status === "installed" || keybindResult.status === "already-installed") {
5147
5565
  keybindMsg = `${DEFAULT_CYCLE_KEY} cycle, ${DEFAULT_PREFIX_KEY} prefix (h=dashboard, x=kill)`;
5566
+ } else if (keybindResult.status === "requires-force" || keybindResult.status === "conflict") {
5567
+ keybindMsg = `${keybindResult.message}
5568
+ Run "sis admin check-keybinds" for the full decision tree, then re-run "sis admin setup --force" to proceed.`;
5569
+ process.exitCode = 1;
5148
5570
  } else {
5149
5571
  keybindMsg = keybindResult.message;
5150
5572
  }
@@ -5154,9 +5576,12 @@ function registerSetup(program2) {
5154
5576
 
5155
5577
  // src/cli/commands/setup-keybind.ts
5156
5578
  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 });
5579
+ 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) => {
5580
+ const resolvedKey = key === void 0 ? DEFAULT_CYCLE_KEY : key;
5581
+ const result = await setupTmuxKeybind(resolvedKey, void 0, {
5582
+ assumeYes: opts.yes,
5583
+ force: opts.force
5584
+ });
5160
5585
  switch (result.status) {
5161
5586
  case "installed":
5162
5587
  console.log(result.message);
@@ -5166,28 +5591,602 @@ function registerSetupKeybind(program2) {
5166
5591
  console.log(result.message);
5167
5592
  break;
5168
5593
  case "conflict":
5169
- console.log(`Key ${resolvedKey} is already bound:`);
5594
+ console.log(`Key ${result.conflictKey} is already bound:`);
5170
5595
  console.log(` ${result.existingBinding}`);
5171
5596
  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");
5597
+ console.log("Options:");
5598
+ console.log(" - Pick a different cycle key: sis admin setup-keybind M-w");
5599
+ console.log(' - Run "sis admin check-keybinds" for a full breakdown');
5600
+ console.log(" - Override and overwrite: sis admin setup-keybind --force");
5601
+ process.exitCode = 1;
5176
5602
  break;
5177
5603
  case "unsupported-tmux":
5178
5604
  console.log(result.message);
5605
+ process.exitCode = 1;
5606
+ break;
5607
+ case "requires-force":
5608
+ console.log(result.message);
5609
+ console.log("");
5610
+ console.log('Run "sis admin check-keybinds" first if you want the full decision tree before deciding.');
5611
+ process.exitCode = 1;
5179
5612
  break;
5180
5613
  case "conf-modification-declined":
5181
5614
  console.log(result.message);
5182
5615
  console.log("");
5183
- console.log("Re-run with --yes to skip the prompt.");
5616
+ console.log("Re-run with --force to append automatically.");
5184
5617
  break;
5185
5618
  }
5186
5619
  });
5187
5620
  }
5188
5621
 
5189
- // src/cli/commands/home-init.ts
5622
+ // src/cli/commands/check-keybinds.ts
5190
5623
  import { execSync as execSync11 } from "child_process";
5624
+ import { readFileSync as readFileSync19 } from "fs";
5625
+ function isTmuxInstalled2() {
5626
+ try {
5627
+ execSync11("which tmux", { stdio: "pipe" });
5628
+ return true;
5629
+ } catch {
5630
+ return false;
5631
+ }
5632
+ }
5633
+ function getTmuxVersion2() {
5634
+ try {
5635
+ return execSync11("tmux -V", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
5636
+ } catch {
5637
+ return null;
5638
+ }
5639
+ }
5640
+ function isTmuxServerRunning() {
5641
+ try {
5642
+ execSync11("tmux list-sessions", { stdio: ["pipe", "pipe", "pipe"] });
5643
+ return true;
5644
+ } catch {
5645
+ return false;
5646
+ }
5647
+ }
5648
+ function getTmuxPrefix() {
5649
+ try {
5650
+ return execSync11("tmux show-options -gv prefix", { stdio: ["pipe", "pipe", "pipe"] }).toString().trim() || null;
5651
+ } catch {
5652
+ return null;
5653
+ }
5654
+ }
5655
+ function classifyKey(key, serverRunning) {
5656
+ if (!serverRunning) return { kind: "unbound" };
5657
+ const binding = getExistingBinding(key);
5658
+ if (binding === null) return { kind: "unbound" };
5659
+ if (isSisyphusBinding(binding)) return { kind: "sisyphus", binding };
5660
+ return { kind: "conflict", binding };
5661
+ }
5662
+ function runCheck() {
5663
+ const tmuxInstalled = isTmuxInstalled2();
5664
+ const tmuxVersion = tmuxInstalled ? getTmuxVersion2() : null;
5665
+ const tmuxVersionOk = tmuxInstalled ? tmuxVersionAtLeast(3, 2) : false;
5666
+ const tmuxServerRunning = tmuxInstalled ? isTmuxServerRunning() : false;
5667
+ const inTmux = !!process.env["TMUX"];
5668
+ const cycleKey = { key: DEFAULT_CYCLE_KEY, ...classifyKey(DEFAULT_CYCLE_KEY, tmuxServerRunning) };
5669
+ const prefixKey = { key: DEFAULT_PREFIX_KEY, ...classifyKey(DEFAULT_PREFIX_KEY, tmuxServerRunning) };
5670
+ const tmuxPrefix = tmuxServerRunning ? getTmuxPrefix() : null;
5671
+ const prefixCollision = tmuxPrefix !== null && tmuxPrefix === DEFAULT_PREFIX_KEY;
5672
+ const sisyphusConfPath = sisyphusTmuxConfPath();
5673
+ const userConfPath = userTmuxConfPath();
5674
+ let userConfAlreadySources = false;
5675
+ if (userConfPath !== null) {
5676
+ try {
5677
+ userConfAlreadySources = readFileSync19(userConfPath, "utf-8").includes(sisyphusConfPath);
5678
+ } catch {
5679
+ }
5680
+ }
5681
+ return {
5682
+ tmuxInstalled,
5683
+ tmuxServerRunning,
5684
+ tmuxVersion,
5685
+ tmuxVersionOk,
5686
+ inTmux,
5687
+ cycleKey,
5688
+ prefixKey,
5689
+ tmuxPrefix,
5690
+ prefixCollision,
5691
+ userConfPath,
5692
+ userConfAlreadySources,
5693
+ sisyphusConfPath
5694
+ };
5695
+ }
5696
+ function fmtKeyState(k) {
5697
+ switch (k.kind) {
5698
+ case "sisyphus":
5699
+ return `${k.key}: sisyphus (${k.binding})`;
5700
+ case "conflict":
5701
+ return `${k.key}: CONFLICT \u2014 already bound: ${k.binding}`;
5702
+ case "unbound":
5703
+ return `${k.key}: unbound`;
5704
+ }
5705
+ }
5706
+ function printInstructions(r) {
5707
+ const bothSisyphus = r.cycleKey.kind === "sisyphus" && r.prefixKey.kind === "sisyphus";
5708
+ const anyConflict = r.cycleKey.kind === "conflict" || r.prefixKey.kind === "conflict";
5709
+ const wouldModifyUserConf = r.userConfPath !== null && !r.userConfAlreadySources;
5710
+ const safeAutoInstall = !anyConflict && !r.prefixCollision && !wouldModifyUserConf;
5711
+ const tmuxVersionLabel = r.tmuxVersion === null ? "(none)" : r.tmuxVersion;
5712
+ const tmuxPrefixLabel = r.tmuxPrefix === null ? "(default C-b)" : r.tmuxPrefix;
5713
+ const userConfLabel = r.userConfPath === null ? "(none found)" : r.userConfPath;
5714
+ const userConfForCopy = r.userConfPath === null ? "~/.tmux.conf" : r.userConfPath;
5715
+ console.log(`
5716
+ <claude-instructions>
5717
+ # Sisyphus Keybind Precheck
5718
+
5719
+ Run this BEFORE asking the user to try any sisyphus keybind (e.g. \`Right Option+s\`,
5720
+ \`C-s h\`, \`C-s ?\`). If the keybinds aren't actually wired up, the user will press
5721
+ the key and nothing will happen \u2014 confusing, looks broken.
5722
+
5723
+ ## Environment Data
5724
+ - tmuxInstalled: ${r.tmuxInstalled}
5725
+ - tmuxVersion: ${tmuxVersionLabel}
5726
+ - tmuxVersionOk: ${r.tmuxVersionOk} (sisyphus needs 3.2+ for display-menu)
5727
+ - tmuxServerRunning: ${r.tmuxServerRunning}
5728
+ - inTmux ($TMUX set): ${r.inTmux}
5729
+ - ${fmtKeyState(r.cycleKey)}
5730
+ - ${fmtKeyState(r.prefixKey)}
5731
+ - tmuxPrefix (\`set -g prefix\`): ${tmuxPrefixLabel}
5732
+ - prefixCollision: ${r.prefixCollision} (true = user's tmux prefix is ${DEFAULT_PREFIX_KEY}; binding it in root would shadow it)
5733
+ - userConfPath: ${userConfLabel}
5734
+ - userConfAlreadySources: ${r.userConfAlreadySources}
5735
+ - sisyphusConfPath: ${r.sisyphusConfPath}
5736
+
5737
+ ## Decision
5738
+
5739
+ Pick exactly one path. Do NOT proceed past this check until both keys read "sisyphus".
5740
+
5741
+ ### Path A \u2014 All wired, proceed
5742
+ **Trigger:** cycleKey is "sisyphus" AND prefixKey is "sisyphus".
5743
+ **Action:** ${bothSisyphus ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Confirm briefly and continue with the onboarding step that needs keybinds.
5744
+
5745
+ ### Path B \u2014 Safe to auto-install
5746
+ **Trigger:** No conflicts, no prefix collision, AND (no user tmux.conf OR it already sources sisyphus).
5747
+ **Action:** ${safeAutoInstall && !bothSisyphus ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Run:
5748
+ \`\`\`
5749
+ sis admin setup-keybind --yes
5750
+ \`\`\`
5751
+ This installs the helper scripts in ~/.sisyphus/bin/ and applies the bindings to the live
5752
+ tmux server. No user files are clobbered (none to clobber, or already wired).
5753
+
5754
+ ### Path C \u2014 Would append a line to the user's tmux.conf
5755
+ **Trigger:** No conflicts, no prefix collision, but ${userConfForCopy} exists and doesn't yet source ${r.sisyphusConfPath}.
5756
+ **Action:** ${!safeAutoInstall && !anyConflict && !r.prefixCollision && wouldModifyUserConf && !bothSisyphus ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Ask the user to choose:
5757
+
5758
+ 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."
5759
+ Run: \`sis admin setup-keybind --yes\`
5760
+
5761
+ 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."
5762
+ Run: \`sis admin setup-keybind\` (no --yes; non-TTY auto-declines the conf prompt while still applying live bindings + installing helper scripts)
5763
+
5764
+ ### Path D \u2014 Conflict on ${DEFAULT_CYCLE_KEY} or ${DEFAULT_PREFIX_KEY}
5765
+ **Trigger:** cycleKey or prefixKey is "CONFLICT".
5766
+ **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:
5767
+
5768
+ 1. **Pick alternate keys.** Re-run with a different cycle key \u2014 e.g. \`M-S\`, \`M-w\`, \`M-j\`, \`M-\\\`\`:
5769
+ \`\`\`
5770
+ sis admin setup-keybind M-w
5771
+ \`\`\`
5772
+ This still uses C-s for the prefix; if the prefix also conflicts, you'll need to wire
5773
+ directly (option 3 below), since setup-keybind only takes a custom cycle key.
5774
+
5775
+ 2. **Skip keybinds entirely.** The user can drive sisyphus from the CLI: \`sis dashboard\`,
5776
+ \`sis status\`, \`sis start\`, \`sis session resume\`. Lose tmux quick-actions, keep
5777
+ existing bindings.
5778
+
5779
+ 3. **Wire commands directly (advanced).** Bypass setup-keybind and bind individual
5780
+ sisyphus actions to keys the user picks. Helper scripts must already exist \u2014 if
5781
+ ~/.sisyphus/bin/sisyphus-cycle is missing, you cannot use this path until setup-keybind
5782
+ has run at least once successfully (try option 1 first).
5783
+ \`\`\`
5784
+ # cycle sessions on a key the user chooses (replace M-w):
5785
+ tmux bind-key -T root M-w run-shell "$HOME/.sisyphus/bin/sisyphus-cycle"
5786
+
5787
+ # open the dashboard directly on a key (replace M-h):
5788
+ tmux bind-key -T root M-h run-shell "$HOME/.sisyphus/bin/sisyphus-home"
5789
+ \`\`\`
5790
+ These apply only to the running tmux server. To persist, append the same lines to
5791
+ ${userConfForCopy}.
5792
+
5793
+ ### Path E \u2014 Hidden prefix collision
5794
+ **Trigger:** prefixCollision is true (the user's \`set -g prefix\` is already ${DEFAULT_PREFIX_KEY}).
5795
+ **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:
5796
+
5797
+ > "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."
5798
+
5799
+ For (b), wire just the cycle key directly:
5800
+ \`\`\`
5801
+ sis admin setup-keybind ${DEFAULT_CYCLE_KEY}
5802
+ # then unbind C-s if setup-keybind ended up taking it:
5803
+ tmux unbind-key -T root ${DEFAULT_PREFIX_KEY}
5804
+ \`\`\`
5805
+
5806
+ ### Path F \u2014 tmux not ready
5807
+ **Trigger:** any of: tmuxInstalled=false, tmuxVersionOk=false, tmuxServerRunning=false.
5808
+ **Action:** ${!r.tmuxInstalled || !r.tmuxVersionOk || !r.tmuxServerRunning ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"} Don't install keybinds yet. Fix the precondition:
5809
+ - tmuxInstalled=false \u2192 \`brew install tmux\` (macOS) or your package manager
5810
+ - tmuxVersionOk=false \u2192 upgrade tmux to 3.2+
5811
+ - tmuxServerRunning=false \u2192 user needs to run \`tmux\` (or attach to an existing session) before live bindings can be installed or tested
5812
+
5813
+ After fixing, re-run \`sis admin check-keybinds\`.
5814
+
5815
+ ## After acting
5816
+ Re-run \`sis admin check-keybinds\` to confirm both keys read "sisyphus", THEN ask the
5817
+ user to try the keybind. Don't skip the verification \u2014 \`setup-keybind\` can fail silently
5818
+ if the tmux server dies between commands.
5819
+ </claude-instructions>
5820
+ `);
5821
+ }
5822
+ function registerCheckKeybinds(program2) {
5823
+ 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) => {
5824
+ const result = runCheck();
5825
+ if (opts.json) {
5826
+ console.log(JSON.stringify(result, null, 2));
5827
+ return;
5828
+ }
5829
+ printInstructions(result);
5830
+ });
5831
+ }
5832
+
5833
+ // src/cli/commands/check-statusbar.ts
5834
+ init_paths();
5835
+ import { execSync as execSync12 } from "child_process";
5836
+ import { existsSync as existsSync16, readFileSync as readFileSync20 } from "fs";
5837
+ import { homedir as homedir10 } from "os";
5838
+ import { join as join17 } from "path";
5839
+ var SISYPHUS_LEFT_TOKEN = "@sisyphus_left";
5840
+ var SISYPHUS_RIGHT_TOKEN = "@sisyphus_right";
5841
+ var TMUX_DEFAULT_STATUS_LEFT = "[#S] ";
5842
+ var TMUX_DEFAULT_STATUS_RIGHT_PREFIX = '"#{=21:pane_title}"';
5843
+ function isTmuxInstalled3() {
5844
+ try {
5845
+ execSync12("which tmux", { stdio: "pipe" });
5846
+ return true;
5847
+ } catch {
5848
+ return false;
5849
+ }
5850
+ }
5851
+ function isTmuxServerRunning2() {
5852
+ try {
5853
+ execSync12("tmux list-sessions", { stdio: ["pipe", "pipe", "pipe"] });
5854
+ return true;
5855
+ } catch {
5856
+ return false;
5857
+ }
5858
+ }
5859
+ function isDaemonRunning() {
5860
+ const pidFile = daemonPidPath();
5861
+ if (!existsSync16(pidFile)) return false;
5862
+ try {
5863
+ const pid = parseInt(readFileSync20(pidFile, "utf-8").trim(), 10);
5864
+ if (Number.isNaN(pid) || pid <= 0) return false;
5865
+ process.kill(pid, 0);
5866
+ return true;
5867
+ } catch {
5868
+ return false;
5869
+ }
5870
+ }
5871
+ function showOption(name) {
5872
+ try {
5873
+ const out = execSync12(`tmux show-options -g ${name}`, { stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
5874
+ if (out.length === 0) return null;
5875
+ const prefix = `${name} `;
5876
+ const stripped = out.startsWith(prefix) ? out.slice(prefix.length) : out;
5877
+ if (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length >= 2) {
5878
+ return stripped.slice(1, -1);
5879
+ }
5880
+ return stripped;
5881
+ } catch {
5882
+ return null;
5883
+ }
5884
+ }
5885
+ function probeTmuxOptions(serverRunning) {
5886
+ if (!serverRunning) {
5887
+ return {
5888
+ status: null,
5889
+ statusLeft: null,
5890
+ statusRight: null,
5891
+ statusPosition: null,
5892
+ statusStyle: null,
5893
+ statusInterval: null
5894
+ };
5895
+ }
5896
+ return {
5897
+ status: showOption("status"),
5898
+ statusLeft: showOption("status-left"),
5899
+ statusRight: showOption("status-right"),
5900
+ statusPosition: showOption("status-position"),
5901
+ statusStyle: showOption("status-style"),
5902
+ statusInterval: showOption("status-interval")
5903
+ };
5904
+ }
5905
+ function findUserTmuxConf() {
5906
+ const xdg = join17(homedir10(), ".config", "tmux", "tmux.conf");
5907
+ const dotfile = join17(homedir10(), ".tmux.conf");
5908
+ if (existsSync16(xdg)) return xdg;
5909
+ if (existsSync16(dotfile)) return dotfile;
5910
+ return null;
5911
+ }
5912
+ function probeUserConf() {
5913
+ const path = findUserTmuxConf();
5914
+ if (path === null) {
5915
+ return { path: null, setsStatusLeft: false, setsStatusRight: false, sourcesSisyphusManaged: false };
5916
+ }
5917
+ let contents = "";
5918
+ try {
5919
+ contents = readFileSync20(path, "utf-8");
5920
+ } catch {
5921
+ return { path, setsStatusLeft: false, setsStatusRight: false, sourcesSisyphusManaged: false };
5922
+ }
5923
+ const lines = contents.split("\n").filter((line) => !line.trim().startsWith("#"));
5924
+ const setsStatusLeft = lines.some((line) => /^\s*(set|set-option)\s+-g(?:\s+-\w+)*\s+status-left\b/.test(line));
5925
+ const setsStatusRight = lines.some((line) => /^\s*(set|set-option)\s+-g(?:\s+-\w+)*\s+status-right\b/.test(line));
5926
+ const sourcesSisyphusManaged = contents.includes(join17(homedir10(), ".sisyphus", "tmux.conf"));
5927
+ return { path, setsStatusLeft, setsStatusRight, sourcesSisyphusManaged };
5928
+ }
5929
+ function loadGlobalSisyphusConfig() {
5930
+ const path = globalConfigPath();
5931
+ if (!existsSync16(path)) return null;
5932
+ try {
5933
+ const parsed = JSON.parse(readFileSync20(path, "utf-8"));
5934
+ return parsed.statusBar === void 0 ? null : parsed.statusBar;
5935
+ } catch {
5936
+ return null;
5937
+ }
5938
+ }
5939
+ function classifyState(opts, serverRunning) {
5940
+ if (!serverRunning) {
5941
+ return { state: "tmux-not-ready", referencesSisyphusLeft: false, referencesSisyphusRight: false };
5942
+ }
5943
+ if (opts.status === "off") {
5944
+ return { state: "disabled", referencesSisyphusLeft: false, referencesSisyphusRight: false };
5945
+ }
5946
+ const referencesSisyphusLeft = opts.statusLeft !== null && opts.statusLeft.includes(SISYPHUS_LEFT_TOKEN);
5947
+ const referencesSisyphusRight = opts.statusRight !== null && opts.statusRight.includes(SISYPHUS_RIGHT_TOKEN);
5948
+ if (referencesSisyphusLeft && referencesSisyphusRight) {
5949
+ return { state: "wired", referencesSisyphusLeft, referencesSisyphusRight };
5950
+ }
5951
+ if (referencesSisyphusLeft) {
5952
+ return { state: "partial-left-only", referencesSisyphusLeft, referencesSisyphusRight };
5953
+ }
5954
+ if (referencesSisyphusRight) {
5955
+ return { state: "partial-right-only", referencesSisyphusLeft, referencesSisyphusRight };
5956
+ }
5957
+ const isStock = (opts.statusLeft === TMUX_DEFAULT_STATUS_LEFT || opts.statusLeft === null) && (opts.statusRight === null || opts.statusRight.includes(TMUX_DEFAULT_STATUS_RIGHT_PREFIX));
5958
+ return {
5959
+ state: isStock ? "tmux-default" : "custom-no-sisyphus",
5960
+ referencesSisyphusLeft,
5961
+ referencesSisyphusRight
5962
+ };
5963
+ }
5964
+ function runCheck2() {
5965
+ const tmuxInstalled = isTmuxInstalled3();
5966
+ const tmuxServerRunning = tmuxInstalled ? isTmuxServerRunning2() : false;
5967
+ const tmuxOptions = probeTmuxOptions(tmuxServerRunning);
5968
+ const userConf = probeUserConf();
5969
+ const globalConfig = loadGlobalSisyphusConfig();
5970
+ const daemonRunning = isDaemonRunning();
5971
+ const { state, referencesSisyphusLeft, referencesSisyphusRight } = classifyState(tmuxOptions, tmuxServerRunning);
5972
+ return {
5973
+ tmuxInstalled,
5974
+ tmuxServerRunning,
5975
+ daemonRunning,
5976
+ tmuxOptions,
5977
+ userConf,
5978
+ globalConfig,
5979
+ state,
5980
+ referencesSisyphusLeft,
5981
+ referencesSisyphusRight
5982
+ };
5983
+ }
5984
+ function fmtOption(value) {
5985
+ if (value === null) return "(unset)";
5986
+ return JSON.stringify(value);
5987
+ }
5988
+ function renderConfigSummary(cfg) {
5989
+ if (cfg === null) return "(no statusBar block in ~/.sisyphus/config.json \u2014 defaults apply)";
5990
+ const parts = [];
5991
+ if (cfg.enabled === false) parts.push("enabled: false (DISABLED)");
5992
+ if (cfg.left !== void 0) parts.push(`left: [${cfg.left.join(", ")}]`);
5993
+ if (cfg.right !== void 0) parts.push(`right: [${cfg.right.join(", ")}]`);
5994
+ if (cfg.colors !== void 0) parts.push(`colors: ${JSON.stringify(cfg.colors)}`);
5995
+ if (cfg.segments !== void 0) {
5996
+ const segNames = Object.keys(cfg.segments);
5997
+ if (segNames.length > 0) parts.push(`per-segment overrides: ${segNames.join(", ")}`);
5998
+ }
5999
+ if (parts.length === 0) return "(statusBar block exists but is empty \u2014 defaults apply)";
6000
+ return parts.join("\n ");
6001
+ }
6002
+ function printInstructions2(r) {
6003
+ const userConfPath = r.userConf.path === null ? "~/.tmux.conf (none found)" : r.userConf.path;
6004
+ const userConfForCopy = r.userConf.path === null ? "~/.tmux.conf" : r.userConf.path;
6005
+ console.log(`
6006
+ <claude-instructions>
6007
+ # Sisyphus Statusbar Helper
6008
+
6009
+ This is a READ-ONLY check. Use it to figure out how to help the user improve their
6010
+ tmux statusbar with sisyphus segments WITHOUT clobbering their existing setup. The
6011
+ user's tmux config is theirs \u2014 never overwrite it. Append, suggest snippets, or
6012
+ nudge them to edit ~/.sisyphus/config.json.
6013
+
6014
+ ## Environment Data
6015
+ - tmuxInstalled: ${r.tmuxInstalled}
6016
+ - tmuxServerRunning: ${r.tmuxServerRunning}
6017
+ - daemonRunning: ${r.daemonRunning} (false = @sisyphus_left/@sisyphus_right won't get populated, statusbar will look broken until daemon is up)
6018
+
6019
+ ### Live tmux options
6020
+ - status: ${fmtOption(r.tmuxOptions.status)}
6021
+ - status-left: ${fmtOption(r.tmuxOptions.statusLeft)}
6022
+ - status-right: ${fmtOption(r.tmuxOptions.statusRight)}
6023
+ - status-position: ${fmtOption(r.tmuxOptions.statusPosition)}
6024
+ - status-style: ${fmtOption(r.tmuxOptions.statusStyle)}
6025
+ - status-interval: ${fmtOption(r.tmuxOptions.statusInterval)}
6026
+ - referencesSisyphusLeft: ${r.referencesSisyphusLeft}
6027
+ - referencesSisyphusRight: ${r.referencesSisyphusRight}
6028
+
6029
+ ### User tmux config
6030
+ - path: ${userConfPath}
6031
+ - setsStatusLeft: ${r.userConf.setsStatusLeft}
6032
+ - setsStatusRight: ${r.userConf.setsStatusRight}
6033
+ - sourcesSisyphusManaged: ${r.userConf.sourcesSisyphusManaged} (sources ~/.sisyphus/tmux.conf \u2014 keybinds, NOT statusbar; included for context)
6034
+
6035
+ ### Sisyphus statusbar config (~/.sisyphus/config.json \u2192 statusBar)
6036
+ ${renderConfigSummary(r.globalConfig)}
6037
+
6038
+ ### Available segments
6039
+ - session-name (left default) \u2014 current tmux session name
6040
+ - windows (left default) \u2014 tmux window list with active highlight
6041
+ - sessions (right default) \u2014 all tmux sessions with claude-state colors
6042
+ - sisyphus-sessions (right default) \u2014 sisyphus-managed sessions with phase indicators
6043
+ - companion (right default) \u2014 companion mood/state pill
6044
+ - clock \u2014 separate %H:%M (NOT a sisyphus segment; tmux renders it inline; the default
6045
+ ~/.tmux.conf appends \`#[fg=...]#[bg=...] %H:%M \` after #{E:@sisyphus_right})
6046
+
6047
+ ## Detected state: **${r.state}**
6048
+
6049
+ Pick exactly one path. Each path tells you what to ask the user and what to do.
6050
+
6051
+ ### Path A \u2014 Already wired (state: wired)
6052
+ **Trigger:** status-left and status-right both reference @sisyphus_left/@sisyphus_right.
6053
+ **Action:** ${r.state === "wired" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6054
+
6055
+ 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:
6056
+
6057
+ 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:
6058
+ \`\`\`json
6059
+ {
6060
+ "statusBar": {
6061
+ "left": ["session-name"],
6062
+ "right": ["sisyphus-sessions", "companion"]
6063
+ }
6064
+ }
6065
+ \`\`\`
6066
+ 2. **Recolor.** \`statusBar.colors\` overrides processing/stopped/idle/activeBg/activeText/inactiveText. \`statusBar.segments.<id>.bg\` overrides per-segment band background.
6067
+ 3. **Disable a single segment.** Remove its id from the left/right arrays.
6068
+ 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).
6069
+ 5. **Restart the daemon** after any config change: \`sisyphusd restart\`. Changes don't auto-apply.
6070
+
6071
+ 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\`.
6072
+
6073
+ ### Path B \u2014 Partial wiring (state: partial-left-only or partial-right-only)
6074
+ **Trigger:** Only one side references sisyphus.
6075
+ **Action:** ${r.state === "partial-left-only" || r.state === "partial-right-only" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6076
+
6077
+ Their config is half-wired. Show them the missing side:
6078
+ - Missing left \u2192 suggest adding \`set -g status-left "#{E:@sisyphus_left}"\` to ${userConfForCopy}
6079
+ - Missing right \u2192 suggest adding \`set -g status-right "#{E:@sisyphus_right}#[fg=#2d2f33]#[bg=#2d2f33,fg=#b0a898] %H:%M "\`
6080
+
6081
+ Don't auto-edit their config \u2014 present the snippet and ask if they want you to append it.
6082
+
6083
+ ### Path C \u2014 Stock tmux statusbar (state: tmux-default)
6084
+ **Trigger:** status-left/right are tmux defaults; user has no custom statusbar.
6085
+ **Action:** ${r.state === "tmux-default" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6086
+
6087
+ Easiest case. Two options to offer:
6088
+
6089
+ 1. **Full sisyphus statusbar.** Append these lines to ${userConfForCopy} (NOT a clobber \u2014 pure additive):
6090
+ \`\`\`tmux
6091
+ # --- Sisyphus statusbar ---
6092
+ set -g status on
6093
+ set -g status-style "bg=#1d1e21,fg=#d4cbb8"
6094
+ set -g status-position bottom
6095
+ set -g status-left "#{E:@sisyphus_left}"
6096
+ set -g status-left-length 250
6097
+ set -g status-right "#{E:@sisyphus_right}#[fg=#2d2f33]#[bg=#2d2f33,fg=#b0a898] %H:%M "
6098
+ set -g status-right-length 250
6099
+ set -g status-interval 2
6100
+ set -g window-status-format ""
6101
+ set -g window-status-current-format ""
6102
+ set -g window-status-separator ""
6103
+ \`\`\`
6104
+ ${r.userConf.path === null ? `Note: no tmux config exists yet. Create ${userConfForCopy} with these lines.` : ""}
6105
+
6106
+ 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:
6107
+ \`\`\`tmux
6108
+ set -g status-right "#{E:@sisyphus_right}#[default] %H:%M "
6109
+ set -g status-right-length 200
6110
+ \`\`\`
6111
+
6112
+ After appending, run \`tmux source-file ${userConfForCopy}\` so the change takes effect immediately.
6113
+
6114
+ ### Path D \u2014 User has a custom statusbar (state: custom-no-sisyphus)
6115
+ **Trigger:** status-left/right are user-customized but don't reference sisyphus tokens.
6116
+ **Action:** ${r.state === "custom-no-sisyphus" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6117
+
6118
+ 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:
6119
+
6120
+ Their current right side is currently:
6121
+ \`\`\`
6122
+ ${r.tmuxOptions.statusRight === null ? "(unset)" : r.tmuxOptions.statusRight}
6123
+ \`\`\`
6124
+ And left side:
6125
+ \`\`\`
6126
+ ${r.tmuxOptions.statusLeft === null ? "(unset)" : r.tmuxOptions.statusLeft}
6127
+ \`\`\`
6128
+
6129
+ Offer three integration patterns and let the user pick:
6130
+
6131
+ 1. **Append sisyphus to the right side** (most common \u2014 leaves their left alone):
6132
+ \`\`\`tmux
6133
+ set -g status-right "${r.tmuxOptions.statusRight === null ? "" : r.tmuxOptions.statusRight}#{E:@sisyphus_right}"
6134
+ \`\`\`
6135
+ Or prepend it (sisyphus content appears first/leftmost):
6136
+ \`\`\`tmux
6137
+ set -g status-right "#{E:@sisyphus_right}${r.tmuxOptions.statusRight === null ? "" : r.tmuxOptions.statusRight}"
6138
+ \`\`\`
6139
+
6140
+ 2. **Append to the left side** (if they want session/windows pills at the start):
6141
+ \`\`\`tmux
6142
+ set -g status-left "${r.tmuxOptions.statusLeft === null ? "" : r.tmuxOptions.statusLeft}#{E:@sisyphus_left}"
6143
+ \`\`\`
6144
+
6145
+ 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.
6146
+
6147
+ 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.
6148
+
6149
+ ### Path E \u2014 Statusbar is disabled (state: disabled)
6150
+ **Trigger:** \`set -g status off\`.
6151
+ **Action:** ${r.state === "disabled" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6152
+
6153
+ The user explicitly turned off the statusbar. Don't enable it without asking. Suggest:
6154
+ - "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?"
6155
+
6156
+ If they say yes \u2192 fall through to Path C, option 2 (minimal pill).
6157
+
6158
+ ### Path F \u2014 tmux not ready (state: tmux-not-ready)
6159
+ **Trigger:** tmux isn't installed or no server is running.
6160
+ **Action:** ${r.state === "tmux-not-ready" ? "\u2713 THIS IS YOUR PATH." : "(not applicable)"}
6161
+
6162
+ Don't propose statusbar changes yet:
6163
+ - tmuxInstalled=false \u2192 install tmux first (\`brew install tmux\` on macOS)
6164
+ - tmuxServerRunning=false \u2192 user needs to run \`tmux\` (or attach to a session)
6165
+
6166
+ Re-run \`sis admin check-statusbar\` once tmux is up.
6167
+
6168
+ ## Daemon reminder
6169
+ 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).
6170
+
6171
+ ## After acting
6172
+ 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.
6173
+ </claude-instructions>
6174
+ `);
6175
+ }
6176
+ function registerCheckStatusbar(program2) {
6177
+ 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) => {
6178
+ const result = runCheck2();
6179
+ if (opts.json) {
6180
+ console.log(JSON.stringify(result, null, 2));
6181
+ return;
6182
+ }
6183
+ printInstructions2(result);
6184
+ });
6185
+ }
6186
+
6187
+ // src/cli/commands/home-init.ts
6188
+ init_shell();
6189
+ import { execSync as execSync13 } from "child_process";
5191
6190
  function registerHomeInit(parent) {
5192
6191
  parent.command("home-init <name> <cwd>").description("Bootstrap a tmux home session with the sisyphus dashboard.").action((name, cwd) => {
5193
6192
  ensureSession(name, cwd);
@@ -5197,7 +6196,7 @@ function registerHomeInit(parent) {
5197
6196
  }
5198
6197
  function sessionExists(name) {
5199
6198
  try {
5200
- execSync11(`tmux has-session -t ${shellQuote(name)}`, { stdio: "pipe" });
6199
+ execSync13(`tmux has-session -t ${shellQuote(name)}`, { stdio: "pipe" });
5201
6200
  return true;
5202
6201
  } catch {
5203
6202
  return false;
@@ -5205,13 +6204,13 @@ function sessionExists(name) {
5205
6204
  }
5206
6205
  function ensureSession(name, cwd) {
5207
6206
  if (sessionExists(name)) return;
5208
- execSync11(
6207
+ execSync13(
5209
6208
  `tmux new-session -d -s ${shellQuote(name)} -c ${shellQuote(cwd)}`,
5210
6209
  { stdio: "pipe" }
5211
6210
  );
5212
6211
  }
5213
6212
  function setSessionCwd(name, cwd) {
5214
- execSync11(
6213
+ execSync13(
5215
6214
  `tmux set-option -t ${shellQuote(name)} @sisyphus_cwd ${shellQuote(cwd.replace(/\/+$/, ""))}`,
5216
6215
  { stdio: "pipe" }
5217
6216
  );
@@ -5219,10 +6218,10 @@ function setSessionCwd(name, cwd) {
5219
6218
 
5220
6219
  // src/cli/commands/doctor.ts
5221
6220
  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";
6221
+ import { execSync as execSync14 } from "child_process";
6222
+ import { existsSync as existsSync17, statSync as statSync3 } from "fs";
6223
+ import { homedir as homedir11 } from "os";
6224
+ import { join as join18 } from "path";
5226
6225
  function checkNodeVersion() {
5227
6226
  const major = parseInt(process.versions.node.split(".")[0], 10);
5228
6227
  if (major < 22) {
@@ -5232,7 +6231,7 @@ function checkNodeVersion() {
5232
6231
  }
5233
6232
  function checkClaudeCli() {
5234
6233
  try {
5235
- execSync12("which claude", { stdio: "pipe" });
6234
+ execSync14("which claude", { stdio: "pipe" });
5236
6235
  return { name: "Claude CLI", status: "ok", detail: "Found on PATH" };
5237
6236
  } catch {
5238
6237
  return {
@@ -5245,7 +6244,7 @@ function checkClaudeCli() {
5245
6244
  }
5246
6245
  function checkGit() {
5247
6246
  try {
5248
- const version = execSync12("git --version", { encoding: "utf-8", stdio: "pipe" }).trim();
6247
+ const version = execSync14("git --version", { encoding: "utf-8", stdio: "pipe" }).trim();
5249
6248
  return { name: "git", status: "ok", detail: version };
5250
6249
  } catch {
5251
6250
  return { name: "git", status: "fail", detail: "Not found on PATH", fix: "Install git: https://git-scm.com/downloads" };
@@ -5253,7 +6252,7 @@ function checkGit() {
5253
6252
  }
5254
6253
  function checkTmuxVersion() {
5255
6254
  try {
5256
- const version = execSync12("tmux -V", { encoding: "utf-8", stdio: "pipe" }).trim();
6255
+ const version = execSync14("tmux -V", { encoding: "utf-8", stdio: "pipe" }).trim();
5257
6256
  const match = version.match(/(\d+\.\d+)/);
5258
6257
  if (!match) return { name: "tmux version", status: "warn", detail: `Could not parse version: ${version}` };
5259
6258
  const ver = parseFloat(match[1]);
@@ -5279,7 +6278,7 @@ function checkDaemonInstalled() {
5279
6278
  };
5280
6279
  }
5281
6280
  const pid = daemonPidPath();
5282
- if (existsSync15(pid)) {
6281
+ if (existsSync17(pid)) {
5283
6282
  return { name: "Daemon setup", status: "ok", detail: `PID file found at ${pid}` };
5284
6283
  }
5285
6284
  return {
@@ -5291,7 +6290,7 @@ function checkDaemonInstalled() {
5291
6290
  }
5292
6291
  function checkDaemonRunning() {
5293
6292
  const pid = daemonPidPath();
5294
- if (!existsSync15(pid)) {
6293
+ if (!existsSync17(pid)) {
5295
6294
  const fix = process.platform === "darwin" ? "launchctl load -w ~/Library/LaunchAgents/com.sisyphus.daemon.plist" : "sisyphusd & \u2014 or check if the process is running";
5296
6295
  return {
5297
6296
  name: "Daemon process",
@@ -5302,7 +6301,7 @@ function checkDaemonRunning() {
5302
6301
  }
5303
6302
  try {
5304
6303
  const sock = socketPath();
5305
- execSync12(`test -S "${sock}"`, { stdio: "pipe" });
6304
+ execSync14(`test -S "${sock}"`, { stdio: "pipe" });
5306
6305
  return { name: "Daemon process", status: "ok", detail: `Socket at ${sock}` };
5307
6306
  } catch {
5308
6307
  return {
@@ -5315,13 +6314,13 @@ function checkDaemonRunning() {
5315
6314
  }
5316
6315
  function checkTmux() {
5317
6316
  try {
5318
- execSync12("which tmux", { stdio: "pipe" });
6317
+ execSync14("which tmux", { stdio: "pipe" });
5319
6318
  } catch {
5320
6319
  const installHint = process.platform === "darwin" ? "brew install tmux" : "apt install tmux (Debian/Ubuntu) or your package manager";
5321
6320
  return { name: "tmux", status: "fail", detail: "Not found on PATH", fix: installHint };
5322
6321
  }
5323
6322
  try {
5324
- execSync12("tmux list-sessions", { stdio: "pipe" });
6323
+ execSync14("tmux list-sessions", { stdio: "pipe" });
5325
6324
  return { name: "tmux", status: "ok", detail: "Running" };
5326
6325
  } catch {
5327
6326
  return { name: "tmux", status: "warn", detail: "Installed but no server running" };
@@ -5329,7 +6328,7 @@ function checkTmux() {
5329
6328
  }
5330
6329
  function checkCycleScript() {
5331
6330
  const path = cycleScriptPath();
5332
- if (!existsSync15(path)) {
6331
+ if (!existsSync17(path)) {
5333
6332
  return {
5334
6333
  name: "Cycle script",
5335
6334
  status: "fail",
@@ -5354,7 +6353,7 @@ function checkCycleScript() {
5354
6353
  function checkTmuxKeybind() {
5355
6354
  const existing = getExistingBinding(DEFAULT_CYCLE_KEY);
5356
6355
  if (existing === null) {
5357
- if (existsSync15(sisyphusTmuxConfPath())) {
6356
+ if (existsSync17(sisyphusTmuxConfPath())) {
5358
6357
  return {
5359
6358
  name: `Tmux keybind (${DEFAULT_CYCLE_KEY})`,
5360
6359
  status: "warn",
@@ -5380,7 +6379,7 @@ function checkTmuxKeybind() {
5380
6379
  }
5381
6380
  function checkGlobalDir() {
5382
6381
  const dir = globalDir();
5383
- if (existsSync15(dir)) {
6382
+ if (existsSync17(dir)) {
5384
6383
  return { name: "Data directory", status: "ok", detail: dir };
5385
6384
  }
5386
6385
  return { name: "Data directory", status: "warn", detail: `${dir} does not exist (created on first use)` };
@@ -5432,7 +6431,7 @@ function checkSisyphusPlugin() {
5432
6431
  function checkTermrender() {
5433
6432
  if (isTermrenderAvailable()) {
5434
6433
  try {
5435
- const version = execSync12("termrender --version", { encoding: "utf-8", stdio: "pipe" }).trim();
6434
+ const version = execSync14("termrender --version", { encoding: "utf-8", stdio: "pipe" }).trim();
5436
6435
  return { name: "termrender", status: "ok", detail: version };
5437
6436
  } catch {
5438
6437
  return { name: "termrender", status: "ok", detail: "installed" };
@@ -5451,7 +6450,7 @@ function checkNvim() {
5451
6450
  return { name: "nvim", status: "warn", detail: "Not installed", fix };
5452
6451
  }
5453
6452
  try {
5454
- const version = execSync12("nvim --version", { encoding: "utf-8", stdio: "pipe" }).split("\n")[0]?.replace("NVIM ", "");
6453
+ const version = execSync14("nvim --version", { encoding: "utf-8", stdio: "pipe" }).split("\n")[0]?.replace("NVIM ", "");
5455
6454
  return { name: "nvim", status: "ok", detail: version ?? "installed" };
5456
6455
  } catch {
5457
6456
  return { name: "nvim", status: "ok", detail: "installed" };
@@ -5459,8 +6458,8 @@ function checkNvim() {
5459
6458
  }
5460
6459
  function checkNotifyBinary() {
5461
6460
  if (process.platform !== "darwin") return null;
5462
- const binary = join16(homedir9(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
5463
- if (existsSync15(binary)) {
6461
+ const binary = join18(homedir11(), ".sisyphus", "SisyphusNotify.app", "Contents", "MacOS", "sisyphus-notify");
6462
+ if (existsSync17(binary)) {
5464
6463
  return { name: "Notifications", status: "ok", detail: "SisyphusNotify.app built" };
5465
6464
  }
5466
6465
  return {
@@ -5513,8 +6512,8 @@ function registerDoctor(program2) {
5513
6512
  }
5514
6513
 
5515
6514
  // 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";
6515
+ import { existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
6516
+ import { join as join19 } from "path";
5518
6517
  var DEFAULT_CONFIG2 = {};
5519
6518
  var ORCHESTRATOR_TEMPLATE = `# Custom Orchestrator Prompt
5520
6519
 
@@ -5525,18 +6524,18 @@ var ORCHESTRATOR_TEMPLATE = `# Custom Orchestrator Prompt
5525
6524
  function registerInit(program2) {
5526
6525
  program2.command("init").description("Initialize sisyphus configuration for this project").option("--orchestrator", "Also create a custom orchestrator prompt template").action((opts) => {
5527
6526
  const cwd = process.cwd();
5528
- const sisDir = join17(cwd, ".sisyphus");
5529
- const configPath = join17(sisDir, "config.json");
5530
- if (existsSync16(configPath)) {
6527
+ const sisDir = join19(cwd, ".sisyphus");
6528
+ const configPath = join19(sisDir, "config.json");
6529
+ if (existsSync18(configPath)) {
5531
6530
  console.log(`Already initialized: ${configPath}`);
5532
6531
  return;
5533
6532
  }
5534
- mkdirSync7(sisDir, { recursive: true });
6533
+ mkdirSync8(sisDir, { recursive: true });
5535
6534
  writeFileSync9(configPath, JSON.stringify(DEFAULT_CONFIG2, null, 2) + "\n", "utf-8");
5536
6535
  console.log(`Created ${configPath}`);
5537
6536
  if (opts.orchestrator) {
5538
- const orchPath = join17(sisDir, "orchestrator.md");
5539
- if (!existsSync16(orchPath)) {
6537
+ const orchPath = join19(sisDir, "orchestrator.md");
6538
+ if (!existsSync18(orchPath)) {
5540
6539
  writeFileSync9(orchPath, ORCHESTRATOR_TEMPLATE, "utf-8");
5541
6540
  console.log(`Created ${orchPath}`);
5542
6541
  }
@@ -5579,7 +6578,7 @@ function registerUninstall(program2) {
5579
6578
 
5580
6579
  // src/cli/commands/configure-upload.ts
5581
6580
  init_paths();
5582
- import { chmodSync as chmodSync2, existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
6581
+ import { chmodSync as chmodSync2, existsSync as existsSync19, mkdirSync as mkdirSync9, readFileSync as readFileSync21, writeFileSync as writeFileSync10 } from "fs";
5583
6582
  import { createInterface as createInterface3 } from "readline";
5584
6583
  import { dirname as dirname6 } from "path";
5585
6584
  async function readUrlFromInput(interactive) {
@@ -5637,16 +6636,16 @@ function registerConfigureUpload(program2) {
5637
6636
  const url = parsed.origin + (strippedPath.length > 0 ? strippedPath : "");
5638
6637
  const configPath = globalConfigPath();
5639
6638
  let existing = {};
5640
- if (existsSync17(configPath)) {
6639
+ if (existsSync19(configPath)) {
5641
6640
  try {
5642
- existing = JSON.parse(readFileSync19(configPath, "utf-8"));
6641
+ existing = JSON.parse(readFileSync21(configPath, "utf-8"));
5643
6642
  } catch {
5644
6643
  console.error(`Error: ${configPath} could not be parsed \u2014 fix or delete it first`);
5645
6644
  process.exit(1);
5646
6645
  }
5647
6646
  }
5648
6647
  const merged = { ...existing, upload: { url, token } };
5649
- mkdirSync8(dirname6(configPath), { recursive: true });
6648
+ mkdirSync9(dirname6(configPath), { recursive: true });
5650
6649
  writeFileSync10(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
5651
6650
  chmodSync2(configPath, 384);
5652
6651
  console.log(`\u2713 upload configured (${configPath})`);
@@ -5654,11 +6653,11 @@ function registerConfigureUpload(program2) {
5654
6653
  }
5655
6654
 
5656
6655
  // src/cli/commands/getting-started.ts
5657
- import { execSync as execSync13 } from "child_process";
5658
- import { dirname as dirname7, join as join18 } from "path";
6656
+ import { execSync as execSync15 } from "child_process";
6657
+ import { dirname as dirname7, join as join20 } from "path";
5659
6658
  import { fileURLToPath as fileURLToPath3 } from "url";
5660
6659
  function templatePath(name) {
5661
- return join18(dirname7(fileURLToPath3(import.meta.url)), "templates", name);
6660
+ return join20(dirname7(fileURLToPath3(import.meta.url)), "templates", name);
5662
6661
  }
5663
6662
  function isClaudeCode() {
5664
6663
  return !!process.env["CLAUDECODE"];
@@ -5983,18 +6982,30 @@ Two keybinds to remember (both use the RIGHT Option key):
5983
6982
  | Right Option + s | Cycle through sisyphus sessions |
5984
6983
  | Right Option + Shift + s | Jump back to dashboard |
5985
6984
 
5986
- ### 4. Verify keybinds are installed
6985
+ ### 4. Verify keybinds are installed (REQUIRED before step 5)
5987
6986
 
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+"
6987
+ Run \`sis admin check-keybinds\` and follow the decision tree it emits. The output is
6988
+ structured for you \u2014 it tells you the current state and which path to take (Path A
6989
+ through Path F). Do NOT skip this and do NOT ask the user to test a keybind until
6990
+ both \`M-s\` and \`C-s\` read "sisyphus" in that command's output.
5992
6991
 
5993
- If cycle script or keybind is missing, run: \`sis admin setup-keybind\`
6992
+ Common paths:
6993
+ - **Path A (already wired):** confirm and move on.
6994
+ - **Path B (safe auto-install):** run \`sis admin setup-keybind --yes\`.
6995
+ - **Path C (would touch user's tmux.conf):** ask the user \u2014 persistent (\`--force\`) or
6996
+ live-only (no flag, non-TTY auto-declines the conf write).
6997
+ - **Path D (binding conflict):** pick alternate keys or wire directly.
6998
+ - **Path E (hidden prefix collision):** their tmux prefix is C-s; explain and offer
6999
+ alternatives.
7000
+ - **Path F (tmux not ready):** fix the precondition first.
7001
+
7002
+ After acting, re-run \`sis admin check-keybinds\` to confirm success.
5994
7003
 
5995
7004
  ### 5. Test the keybind
5996
7005
 
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.
7006
+ Once check-keybinds reports both keys as "sisyphus", have the user try pressing
7007
+ Right Option + s. Nothing should happen yet (no sisyphus session running) \u2014 and that's
7008
+ fine. The important thing is no special character appears.
5998
7009
 
5999
7010
  If they see \`\xDF\` or similar, circle back to the Right Option Key setup above.
6000
7011
 
@@ -6134,11 +7145,11 @@ function printStep5() {
6134
7145
  let recentCommits = "";
6135
7146
  let topLevelFiles = "";
6136
7147
  try {
6137
- recentCommits = execSync13("git log --oneline -15 2>/dev/null", { encoding: "utf-8" }).trim();
7148
+ recentCommits = execSync15("git log --oneline -15 2>/dev/null", { encoding: "utf-8" }).trim();
6138
7149
  } catch {
6139
7150
  }
6140
7151
  try {
6141
- topLevelFiles = execSync13("ls -1 2>/dev/null", { encoding: "utf-8" }).trim();
7152
+ topLevelFiles = execSync15("ls -1 2>/dev/null", { encoding: "utf-8" }).trim();
6142
7153
  } catch {
6143
7154
  }
6144
7155
  console.log(`
@@ -6616,7 +7627,7 @@ function registerGettingStarted(program2) {
6616
7627
 
6617
7628
  // src/cli/commands/history.ts
6618
7629
  init_paths();
6619
- import { readdirSync as readdirSync6, readFileSync as readFileSync20, existsSync as existsSync18 } from "fs";
7630
+ import { readdirSync as readdirSync6, readFileSync as readFileSync22, existsSync as existsSync20 } from "fs";
6620
7631
  import { resolve as resolve6 } from "path";
6621
7632
  var RESET3 = "\x1B[0m";
6622
7633
  var BOLD3 = "\x1B[1m";
@@ -6656,13 +7667,13 @@ function splitAgentTime(agents) {
6656
7667
  }
6657
7668
  function loadAllSummaries() {
6658
7669
  const base = historyBaseDir();
6659
- if (!existsSync18(base)) return [];
7670
+ if (!existsSync20(base)) return [];
6660
7671
  const results = [];
6661
7672
  for (const name of readdirSync6(base)) {
6662
7673
  const summaryPath = historySessionSummaryPath(name);
6663
- if (existsSync18(summaryPath)) {
7674
+ if (existsSync20(summaryPath)) {
6664
7675
  try {
6665
- const raw = readFileSync20(summaryPath, "utf-8");
7676
+ const raw = readFileSync22(summaryPath, "utf-8");
6666
7677
  results.push({ id: name, summary: JSON.parse(raw) });
6667
7678
  continue;
6668
7679
  } catch {
@@ -6676,10 +7687,10 @@ function loadAllSummaries() {
6676
7687
  }
6677
7688
  function buildLiveSummary(sessionId) {
6678
7689
  const eventsPath = historyEventsPath(sessionId);
6679
- if (!existsSync18(eventsPath)) return null;
7690
+ if (!existsSync20(eventsPath)) return null;
6680
7691
  let cwd = null;
6681
7692
  try {
6682
- const lines = readFileSync20(eventsPath, "utf-8").split("\n");
7693
+ const lines = readFileSync22(eventsPath, "utf-8").split("\n");
6683
7694
  for (const line of lines) {
6684
7695
  if (!line.trim()) continue;
6685
7696
  try {
@@ -6697,10 +7708,10 @@ function buildLiveSummary(sessionId) {
6697
7708
  }
6698
7709
  if (!cwd) return null;
6699
7710
  const sPath = statePath(cwd, sessionId);
6700
- if (!existsSync18(sPath)) return null;
7711
+ if (!existsSync20(sPath)) return null;
6701
7712
  let session2;
6702
7713
  try {
6703
- session2 = JSON.parse(readFileSync20(sPath, "utf-8"));
7714
+ session2 = JSON.parse(readFileSync22(sPath, "utf-8"));
6704
7715
  } catch {
6705
7716
  return null;
6706
7717
  }
@@ -6761,8 +7772,8 @@ function buildLiveSummary(sessionId) {
6761
7772
  }
6762
7773
  function loadEvents(sessionId) {
6763
7774
  const eventsPath = historyEventsPath(sessionId);
6764
- if (!existsSync18(eventsPath)) return [];
6765
- const lines = readFileSync20(eventsPath, "utf-8").split("\n").filter((l) => l.trim());
7775
+ if (!existsSync20(eventsPath)) return [];
7776
+ const lines = readFileSync22(eventsPath, "utf-8").split("\n").filter((l) => l.trim());
6766
7777
  const events = [];
6767
7778
  for (const line of lines) {
6768
7779
  try {
@@ -6775,13 +7786,13 @@ function loadEvents(sessionId) {
6775
7786
  }
6776
7787
  function findSession(idOrName) {
6777
7788
  const summaryPath = historySessionSummaryPath(idOrName);
6778
- if (existsSync18(summaryPath)) {
7789
+ if (existsSync20(summaryPath)) {
6779
7790
  try {
6780
- return { id: idOrName, summary: JSON.parse(readFileSync20(summaryPath, "utf-8")) };
7791
+ return { id: idOrName, summary: JSON.parse(readFileSync22(summaryPath, "utf-8")) };
6781
7792
  } catch {
6782
7793
  }
6783
7794
  }
6784
- if (existsSync18(historySessionDir(idOrName))) {
7795
+ if (existsSync20(historySessionDir(idOrName))) {
6785
7796
  const live = buildLiveSummary(idOrName);
6786
7797
  if (live) return { id: idOrName, summary: live };
6787
7798
  }
@@ -7170,21 +8181,21 @@ function registerHistory(program2) {
7170
8181
  init_paths();
7171
8182
  import { execFile as execFile2 } from "child_process";
7172
8183
  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";
8184
+ import { existsSync as existsSync21, readFileSync as readFileSync23, mkdirSync as mkdirSync10, symlinkSync, rmSync as rmSync4, writeFileSync as writeFileSync11 } from "fs";
8185
+ import { homedir as homedir12 } from "os";
8186
+ import { join as join22 } from "path";
7176
8187
  function sanitizeName(name) {
7177
8188
  return name.replace(/[^a-zA-Z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
7178
8189
  }
7179
8190
  function buildOutputPath(label, dir) {
7180
8191
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7181
- mkdirSync9(dir, { recursive: true });
8192
+ mkdirSync10(dir, { recursive: true });
7182
8193
  const base = `sisyphus-${label}-${date}`;
7183
- let candidate = join20(dir, `${base}.zip`);
8194
+ let candidate = join22(dir, `${base}.zip`);
7184
8195
  let counter = 1;
7185
- while (existsSync19(candidate)) {
8196
+ while (existsSync21(candidate)) {
7186
8197
  counter++;
7187
- candidate = join20(dir, `${base}-${counter}.zip`);
8198
+ candidate = join22(dir, `${base}-${counter}.zip`);
7188
8199
  }
7189
8200
  return candidate;
7190
8201
  }
@@ -7246,33 +8257,33 @@ async function exportSessionToZip(sessionId, cwd, options) {
7246
8257
  const reveal = options?.reveal ?? true;
7247
8258
  const sessDir = sessionDir(cwd, sessionId);
7248
8259
  const histDir = historySessionDir(sessionId);
7249
- const sessExists = existsSync19(sessDir);
7250
- const histExists = existsSync19(histDir);
8260
+ const sessExists = existsSync21(sessDir);
8261
+ const histExists = existsSync21(histDir);
7251
8262
  if (!sessExists && !histExists) {
7252
8263
  throw new Error(`No data found for session ${sessionId}`);
7253
8264
  }
7254
8265
  let label = sessionId.slice(0, 8);
7255
8266
  const stPath = statePath(cwd, sessionId);
7256
- if (existsSync19(stPath)) {
8267
+ if (existsSync21(stPath)) {
7257
8268
  try {
7258
- const state = JSON.parse(readFileSync21(stPath, "utf-8"));
8269
+ const state = JSON.parse(readFileSync23(stPath, "utf-8"));
7259
8270
  if (state.name) {
7260
8271
  label = sanitizeName(state.name);
7261
8272
  }
7262
8273
  } catch {
7263
8274
  }
7264
8275
  }
7265
- const dir = options?.outputDir ?? join20(homedir10(), "Downloads");
8276
+ const dir = options?.outputDir ?? join22(homedir12(), "Downloads");
7266
8277
  const outputPath = buildOutputPath(label, dir);
7267
8278
  const tmpDir = `/tmp/sisyphus-export-${sessionId.slice(0, 8)}-${Date.now()}`;
7268
8279
  try {
7269
- mkdirSync9(tmpDir, { recursive: true });
7270
- writeFileSync11(join20(tmpDir, "CLAUDE.md"), generateGuide(), "utf-8");
8280
+ mkdirSync10(tmpDir, { recursive: true });
8281
+ writeFileSync11(join22(tmpDir, "CLAUDE.md"), generateGuide(), "utf-8");
7271
8282
  if (sessExists) {
7272
- symlinkSync(sessDir, join20(tmpDir, "session"));
8283
+ symlinkSync(sessDir, join22(tmpDir, "session"));
7273
8284
  }
7274
8285
  if (histExists) {
7275
- symlinkSync(histDir, join20(tmpDir, "history"));
8286
+ symlinkSync(histDir, join22(tmpDir, "history"));
7276
8287
  }
7277
8288
  const parts = ["CLAUDE.md", sessExists ? "session/" : "", histExists ? "history/" : ""].filter(Boolean);
7278
8289
  await execFileAsync("zip", ["-rq", outputPath, ...parts], { cwd: tmpDir });
@@ -7394,12 +8405,12 @@ function buildManifest(args2) {
7394
8405
  }
7395
8406
 
7396
8407
  // src/shared/version.ts
7397
- import { readFileSync as readFileSync22 } from "fs";
8408
+ import { readFileSync as readFileSync24 } from "fs";
7398
8409
  import { resolve as resolve7 } from "path";
7399
8410
  function readSisyphusVersion() {
7400
8411
  for (const rel of ["../package.json", "../../package.json"]) {
7401
8412
  try {
7402
- const raw = readFileSync22(resolve7(import.meta.dirname, rel), "utf-8");
8413
+ const raw = readFileSync24(resolve7(import.meta.dirname, rel), "utf-8");
7403
8414
  const pkg = JSON.parse(raw);
7404
8415
  if (pkg.name === "sisyphi" && pkg.version) return pkg.version;
7405
8416
  } catch {
@@ -7494,12 +8505,13 @@ function registerUpload(program2) {
7494
8505
  }
7495
8506
 
7496
8507
  // src/cli/commands/scratch.ts
7497
- import { execSync as execSync14 } from "child_process";
8508
+ import { execSync as execSync16 } from "child_process";
8509
+ init_shell();
7498
8510
  function findHomeSession(cwd) {
7499
8511
  const normalizedCwd = cwd.replace(/\/+$/, "");
7500
8512
  let output;
7501
8513
  try {
7502
- output = execSync14('tmux list-sessions -F "#{session_id}|#{session_name}"', {
8514
+ output = execSync16('tmux list-sessions -F "#{session_id}|#{session_name}"', {
7503
8515
  encoding: "utf-8",
7504
8516
  stdio: ["pipe", "pipe", "pipe"]
7505
8517
  }).trim();
@@ -7513,7 +8525,7 @@ function findHomeSession(cwd) {
7513
8525
  const name = line.slice(pipeIdx + 1);
7514
8526
  if (name.startsWith("ssyph_")) continue;
7515
8527
  try {
7516
- const val = execSync14(
8528
+ const val = execSync16(
7517
8529
  `tmux show-options -t ${shellQuote(sessId)} -v @sisyphus_cwd`,
7518
8530
  { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
7519
8531
  ).trim();
@@ -7529,7 +8541,7 @@ function registerScratch(program2) {
7529
8541
  const cwd = opts.cwd ?? process.env["SISYPHUS_CWD"] ?? process.cwd();
7530
8542
  const homeSession = findHomeSession(cwd);
7531
8543
  if (!homeSession) {
7532
- const current = execSync14('tmux display-message -p "#{session_name}"', {
8544
+ const current = execSync16('tmux display-message -p "#{session_name}"', {
7533
8545
  encoding: "utf-8"
7534
8546
  }).trim();
7535
8547
  openScratchWindow(current, cwd, promptParts.join(" "));
@@ -7539,7 +8551,7 @@ function registerScratch(program2) {
7539
8551
  });
7540
8552
  }
7541
8553
  function openScratchWindow(tmuxSession, cwd, prompt) {
7542
- const windowId = execSync14(
8554
+ const windowId = execSync16(
7543
8555
  `tmux new-window -t ${shellQuote(tmuxSession + ":")} -n "scratch" -c ${shellQuote(cwd)} -P -F "#{window_id}"`,
7544
8556
  { encoding: "utf-8" }
7545
8557
  ).trim();
@@ -7547,7 +8559,7 @@ function openScratchWindow(tmuxSession, cwd, prompt) {
7547
8559
  if (prompt) {
7548
8560
  cmd += ` -p ${shellQuote(prompt)}`;
7549
8561
  }
7550
- execSync14(
8562
+ execSync16(
7551
8563
  `tmux send-keys -t ${shellQuote(windowId)} ${shellQuote(cmd)} Enter`
7552
8564
  );
7553
8565
  console.log(`Scratch session opened in ${tmuxSession}`);
@@ -7555,27 +8567,27 @@ function openScratchWindow(tmuxSession, cwd, prompt) {
7555
8567
 
7556
8568
  // src/cli/commands/review.ts
7557
8569
  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";
8570
+ import { join as join23, resolve as resolve8, dirname as dirname8 } from "path";
8571
+ import { existsSync as existsSync22, readFileSync as readFileSync25, writeFileSync as writeFileSync12, renameSync as renameSync3, readdirSync as readdirSync7 } from "fs";
7560
8572
  var _statusCheck = ["draft", "question", "approved", "rejected", "deferred"];
7561
8573
  function resolveContextArtifact(file, opts, filename, notFoundMessage) {
7562
8574
  const cwd = opts.cwd || process.env.SISYPHUS_CWD || process.cwd();
7563
8575
  if (file) return resolve8(file);
7564
8576
  const sessionId = opts.sessionId || process.env.SISYPHUS_SESSION_ID;
7565
8577
  if (sessionId) {
7566
- const target = join21(contextDir(cwd, sessionId), filename);
7567
- if (!existsSync20(target)) {
8578
+ const target = join23(contextDir(cwd, sessionId), filename);
8579
+ if (!existsSync22(target)) {
7568
8580
  console.error(`Error: File not found: ${target}`);
7569
8581
  process.exit(1);
7570
8582
  }
7571
8583
  return target;
7572
8584
  }
7573
8585
  const dir = sessionsDir(cwd);
7574
- if (existsSync20(dir)) {
8586
+ if (existsSync22(dir)) {
7575
8587
  const sessions = readdirSync7(dir);
7576
8588
  for (const session2 of sessions.reverse()) {
7577
- const candidate = join21(dir, session2, "context", filename);
7578
- if (existsSync20(candidate)) return candidate;
8589
+ const candidate = join23(dir, session2, "context", filename);
8590
+ if (existsSync22(candidate)) return candidate;
7579
8591
  }
7580
8592
  }
7581
8593
  console.error(`Error: ${notFoundMessage}`);
@@ -7617,16 +8629,16 @@ Examples:
7617
8629
  "requirements.json",
7618
8630
  "No requirements.json found. Provide a path or use --session-id."
7619
8631
  );
7620
- if (!existsSync20(targetPath)) {
8632
+ if (!existsSync22(targetPath)) {
7621
8633
  console.error(`Error: File not found: ${targetPath}`);
7622
8634
  process.exit(1);
7623
8635
  }
7624
- const parsed = JSON.parse(readFileSync23(targetPath, "utf-8"));
8636
+ const parsed = JSON.parse(readFileSync25(targetPath, "utf-8"));
7625
8637
  const rendered = renderRequirementsMarkdown(parsed);
7626
- const outPath = join21(dirname8(targetPath), "requirements.md");
8638
+ const outPath = join23(dirname8(targetPath), "requirements.md");
7627
8639
  const tmpPath = outPath + ".tmp";
7628
- if (existsSync20(outPath)) {
7629
- const existing = readFileSync23(outPath, "utf-8");
8640
+ if (existsSync22(outPath)) {
8641
+ const existing = readFileSync25(outPath, "utf-8");
7630
8642
  if (existing !== rendered) {
7631
8643
  if (!opts.force) {
7632
8644
  process.stderr.write(
@@ -8006,7 +9018,9 @@ function renderRequirementsMarkdown(json) {
8006
9018
  }
8007
9019
 
8008
9020
  // src/cli/commands/companion.ts
8009
- import { basename as basename5 } from "path";
9021
+ import { basename as basename5, dirname as dirname11, join as join28 } from "path";
9022
+ import { mkdirSync as mkdirSync14, readFileSync as readFileSync30, writeFileSync as writeFileSync17 } from "fs";
9023
+ init_paths();
8010
9024
 
8011
9025
  // src/shared/companion-render.ts
8012
9026
  import stringWidth from "string-width";
@@ -9192,20 +10206,21 @@ function createBadgeGallery(unlockedAchievements, startIndex) {
9192
10206
 
9193
10207
  // src/daemon/companion-memory.ts
9194
10208
  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";
10209
+ import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync27, renameSync as renameSync5, writeFileSync as writeFileSync14 } from "fs";
10210
+ import { dirname as dirname10, join as join25 } from "path";
9197
10211
  import { randomUUID as randomUUID4 } from "crypto";
9198
10212
  import { z as z2 } from "zod";
9199
10213
 
9200
10214
  // src/daemon/haiku.ts
10215
+ init_env();
9201
10216
  import { query, createSdkMcpServer } from "@r-cli/sdk";
9202
10217
  var COOLDOWN_MS = 5 * 60 * 1e3;
9203
10218
 
9204
10219
  // src/daemon/companion.ts
9205
10220
  init_paths();
9206
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync24, renameSync as renameSync4, writeFileSync as writeFileSync13 } from "fs";
10221
+ import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync26, renameSync as renameSync4, writeFileSync as writeFileSync13 } from "fs";
9207
10222
  import { randomUUID as randomUUID3 } from "crypto";
9208
- import { dirname as dirname9, join as join22 } from "path";
10223
+ import { dirname as dirname9, join as join24 } from "path";
9209
10224
 
9210
10225
  // src/shared/companion-normalize.ts
9211
10226
  function emptyStats() {
@@ -9254,20 +10269,20 @@ function normalizeCompanion(state) {
9254
10269
  // src/daemon/companion.ts
9255
10270
  function loadCompanion() {
9256
10271
  const path = companionPath();
9257
- if (!existsSync21(path)) {
10272
+ if (!existsSync23(path)) {
9258
10273
  const state2 = createDefaultCompanion();
9259
10274
  saveCompanion(state2);
9260
10275
  return state2;
9261
10276
  }
9262
- const raw = readFileSync24(path, "utf-8");
10277
+ const raw = readFileSync26(path, "utf-8");
9263
10278
  const state = JSON.parse(raw);
9264
10279
  return normalizeCompanion(state);
9265
10280
  }
9266
10281
  function saveCompanion(state) {
9267
10282
  const path = companionPath();
9268
10283
  const dir = dirname9(path);
9269
- mkdirSync10(dir, { recursive: true });
9270
- const tmp = join22(dir, `.companion.${randomUUID3()}.tmp`);
10284
+ mkdirSync11(dir, { recursive: true });
10285
+ const tmp = join24(dir, `.companion.${randomUUID3()}.tmp`);
9271
10286
  writeFileSync13(tmp, JSON.stringify(state, null, 2), "utf-8");
9272
10287
  renameSync4(tmp, path);
9273
10288
  }
@@ -9340,10 +10355,10 @@ function fillDefaults(state) {
9340
10355
  }
9341
10356
  function loadMemoryStrict() {
9342
10357
  const path = resolvedMemoryPath();
9343
- if (!existsSync22(path)) return defaultMemoryState();
10358
+ if (!existsSync24(path)) return defaultMemoryState();
9344
10359
  let raw;
9345
10360
  try {
9346
- raw = readFileSync25(path, "utf-8");
10361
+ raw = readFileSync27(path, "utf-8");
9347
10362
  } catch (err) {
9348
10363
  throw new MemoryStoreParseError(err);
9349
10364
  }
@@ -9386,15 +10401,17 @@ var ObservationZodSchema = z2.object({
9386
10401
  });
9387
10402
 
9388
10403
  // src/daemon/companion-popup.ts
9389
- import { writeFileSync as writeFileSync15, readFileSync as readFileSync26, unlinkSync as unlinkSync3, existsSync as existsSync23 } from "fs";
10404
+ import { writeFileSync as writeFileSync15, readFileSync as readFileSync28, unlinkSync as unlinkSync3, existsSync as existsSync25 } from "fs";
9390
10405
  import { tmpdir as tmpdir2 } from "os";
9391
- import { join as join24, resolve as resolve9 } from "path";
10406
+ import { join as join26, resolve as resolve9 } from "path";
10407
+ init_exec();
10408
+ init_shell();
9392
10409
  var POPUP_WIDTH = 38;
9393
10410
  var INNER_WIDTH = POPUP_WIDTH - 6;
9394
10411
  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");
10412
+ var POPUP_TMP_PREFIX = join26(tmpdir2(), "sisyphus-popup");
10413
+ var POPUP_SCRIPT = join26(tmpdir2(), "sisyphus-popup.sh");
10414
+ var POPUP_RESULT_PREFIX = join26(tmpdir2(), "sisyphus-popup-result");
9398
10415
  var WHIP_ANIMATION_PATH = resolve9(import.meta.dirname, "../templates/whip-animation.sh");
9399
10416
  var WHIP_ANIMATION_ROWS = 12;
9400
10417
  function wrapText2(text, width) {
@@ -9438,7 +10455,7 @@ function showCommentaryPopupQueue(pages) {
9438
10455
  if (contentHeight > maxContentHeight) maxContentHeight = contentHeight;
9439
10456
  writeFileSync15(`${POPUP_TMP_PREFIX}-${i}.txt`, content);
9440
10457
  }
9441
- const whipAvailable = existsSync23(WHIP_ANIMATION_PATH);
10458
+ const whipAvailable = existsSync25(WHIP_ANIMATION_PATH);
9442
10459
  if (whipAvailable && maxContentHeight < WHIP_ANIMATION_ROWS + 2) {
9443
10460
  maxContentHeight = WHIP_ANIMATION_ROWS + 2;
9444
10461
  }
@@ -9512,7 +10529,7 @@ fi
9512
10529
  }
9513
10530
  let raw;
9514
10531
  try {
9515
- raw = readFileSync26(POPUP_RESULT_PREFIX, "utf8").trim();
10532
+ raw = readFileSync28(POPUP_RESULT_PREFIX, "utf8").trim();
9516
10533
  } catch {
9517
10534
  return null;
9518
10535
  } finally {
@@ -9685,9 +10702,29 @@ function registerCompanion(program2) {
9685
10702
  companion.command("memory").description("Show accumulated companion observations grouped by category").option("--repo <path>", "Filter observations by repo path").action(async (opts) => {
9686
10703
  await runCompanionMemory(opts);
9687
10704
  });
9688
- companion.command("context").description("Output session context JSON for companion hook").option("--cwd <path>", "Project directory", process.cwd()).action((opts) => {
9689
- const context = buildCompanionContext(opts.cwd);
9690
- process.stdout.write(JSON.stringify({ additionalContext: context }));
10705
+ companion.command("context").description("Emit per-prompt context for the companion plugin hook. Caches the last emission per claude session and writes only the delta on subsequent calls (or nothing, when unchanged).").requiredOption("--cwd <path>", "Project directory whose sessions to summarise").requiredOption("--session-id <id>", "Claude session id (from the UserPromptSubmit stdin payload) \u2014 keys the per-session cache").action((opts) => {
10706
+ const cachePath = join28(globalDir(), "companion-context-cache", `${opts.sessionId}.json`);
10707
+ let prev = {};
10708
+ try {
10709
+ prev = JSON.parse(readFileSync30(cachePath, "utf-8"));
10710
+ } catch {
10711
+ prev = {};
10712
+ }
10713
+ const next = buildCompanionContextBlocks(opts.cwd);
10714
+ const hadPrev = Object.keys(prev).length > 0;
10715
+ if (hadPrev) {
10716
+ const delta = renderContextDelta(prev, next);
10717
+ if (delta === null) return;
10718
+ process.stdout.write(delta);
10719
+ } else {
10720
+ process.stdout.write(renderFullContext(next));
10721
+ }
10722
+ mkdirSync14(dirname11(cachePath), { recursive: true });
10723
+ writeFileSync17(cachePath, JSON.stringify(next), "utf-8");
10724
+ });
10725
+ 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) => {
10726
+ const { openCompanionPane: openCompanionPane2 } = await Promise.resolve().then(() => (init_tmux(), tmux_exports));
10727
+ openCompanionPane2(opts.cwd);
9691
10728
  });
9692
10729
  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
10730
  const feedback = showCommentaryPopup(opts.text);
@@ -9705,14 +10742,15 @@ function registerCompanion(program2) {
9705
10742
  }
9706
10743
 
9707
10744
  // src/cli/commands/deploy.ts
9708
- import { homedir as homedir11 } from "os";
9709
- import { join as join25 } from "path";
10745
+ import { homedir as homedir13 } from "os";
10746
+ import { join as join29 } from "path";
9710
10747
 
9711
10748
  // src/cli/deploy/runner.ts
9712
10749
  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";
10750
+ init_exec();
9715
10751
  init_creds();
10752
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
10753
+ import { copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync16, readFileSync as readFileSync33 } from "fs";
9716
10754
 
9717
10755
  // src/cli/deploy/pricing.ts
9718
10756
  var LAST_VERIFIED = "2026-05-06";
@@ -9747,12 +10785,12 @@ function formatCostLine(provider, instanceType) {
9747
10785
  // src/cli/deploy/runtime.ts
9748
10786
  init_atomic();
9749
10787
  init_paths();
9750
- import { existsSync as existsSync25, readFileSync as readFileSync28, unlinkSync as unlinkSync4 } from "fs";
10788
+ import { existsSync as existsSync28, readFileSync as readFileSync32, unlinkSync as unlinkSync4 } from "fs";
9751
10789
  function readRuntimeState(provider) {
9752
10790
  const path = deployRuntimePath(provider);
9753
- if (!existsSync25(path)) return null;
10791
+ if (!existsSync28(path)) return null;
9754
10792
  try {
9755
- return JSON.parse(readFileSync28(path, "utf-8"));
10793
+ return JSON.parse(readFileSync32(path, "utf-8"));
9756
10794
  } catch {
9757
10795
  return null;
9758
10796
  }
@@ -9762,10 +10800,11 @@ function writeRuntimeState(provider, state) {
9762
10800
  }
9763
10801
  function clearRuntimeState(provider) {
9764
10802
  const path = deployRuntimePath(provider);
9765
- if (existsSync25(path)) unlinkSync4(path);
10803
+ if (existsSync28(path)) unlinkSync4(path);
9766
10804
  }
9767
10805
 
9768
10806
  // src/cli/deploy/tailnet.ts
10807
+ init_exec();
9769
10808
  function discoverNode(requestedName) {
9770
10809
  const json = execSafe("tailscale status --json");
9771
10810
  if (!json) return null;
@@ -9811,15 +10850,15 @@ function isTailscaleAvailable() {
9811
10850
  }
9812
10851
 
9813
10852
  // src/cli/deploy/templates.ts
9814
- import { existsSync as existsSync26 } from "fs";
9815
- import { dirname as dirname11, resolve as resolve10 } from "path";
10853
+ import { existsSync as existsSync29 } from "fs";
10854
+ import { dirname as dirname12, resolve as resolve10 } from "path";
9816
10855
  import { fileURLToPath as fileURLToPath4 } from "url";
9817
10856
  function deployRoot() {
9818
- const here = dirname11(fileURLToPath4(import.meta.url));
10857
+ const here = dirname12(fileURLToPath4(import.meta.url));
9819
10858
  const bundled = resolve10(here, "..", "deploy");
9820
- if (existsSync26(bundled)) return bundled;
10859
+ if (existsSync29(bundled)) return bundled;
9821
10860
  const sourceRoot = resolve10(here, "..", "..", "..", "deploy");
9822
- if (existsSync26(sourceRoot)) return sourceRoot;
10861
+ if (existsSync29(sourceRoot)) return sourceRoot;
9823
10862
  throw new Error(
9824
10863
  `Could not locate deploy/ templates. Looked at:
9825
10864
  ${bundled}
@@ -10003,14 +11042,14 @@ function ensureTerraformInstalled() {
10003
11042
  function ensureProviderStateDir(provider) {
10004
11043
  ensureDeployDir();
10005
11044
  const dir = deployProviderDir(provider);
10006
- if (!existsSync27(dir)) mkdirSync13(dir, { recursive: true, mode: 448 });
11045
+ if (!existsSync30(dir)) mkdirSync16(dir, { recursive: true, mode: 448 });
10007
11046
  }
10008
11047
  function backupState(provider) {
10009
11048
  const src = deployStatePath(provider);
10010
- if (existsSync27(src)) copyFileSync2(src, deployStateBackupPath(provider));
11049
+ if (existsSync30(src)) copyFileSync2(src, deployStateBackupPath(provider));
10011
11050
  }
10012
11051
  function readSshPubkey(path) {
10013
- if (!existsSync27(path)) {
11052
+ if (!existsSync30(path)) {
10014
11053
  const privateKeyPath = path.replace(/\.pub$/, "");
10015
11054
  throw new Error(
10016
11055
  `SSH pubkey not found at ${path}. Generate one with:
@@ -10018,7 +11057,7 @@ function readSshPubkey(path) {
10018
11057
  or pass --ssh-key <path>.`
10019
11058
  );
10020
11059
  }
10021
- return readFileSync29(path, "utf-8").trim();
11060
+ return readFileSync33(path, "utf-8").trim();
10022
11061
  }
10023
11062
  function readOutputs(provider) {
10024
11063
  const result = spawnSync3("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
@@ -10045,7 +11084,7 @@ function readOutputs(provider) {
10045
11084
  }
10046
11085
  }
10047
11086
  function isProvisioned(provider) {
10048
- if (!existsSync27(deployStatePath(provider))) return false;
11087
+ if (!existsSync30(deployStatePath(provider))) return false;
10049
11088
  return readOutputs(provider) !== null;
10050
11089
  }
10051
11090
  async function deployUp(provider, opts) {
@@ -10131,7 +11170,7 @@ Applied \u2014 but could not parse outputs. Run \`sis deploy ${provider} status\
10131
11170
  console.log("");
10132
11171
  }
10133
11172
  async function deployDown(provider, opts) {
10134
- if (!existsSync27(deployStatePath(provider))) {
11173
+ if (!existsSync30(deployStatePath(provider))) {
10135
11174
  console.log(`No ${provider} state found at ${deployStatePath(provider)}. Nothing to destroy.`);
10136
11175
  return;
10137
11176
  }
@@ -10296,7 +11335,7 @@ function registerDeploy(program2) {
10296
11335
  });
10297
11336
  for (const provider of PROVIDERS) {
10298
11337
  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) => {
11338
+ 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.", join29(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
11339
  const opts = resolveUpOptions(provider, raw);
10301
11340
  await deployUp(provider, opts);
10302
11341
  });
@@ -10320,11 +11359,14 @@ function registerDeploy(program2) {
10320
11359
 
10321
11360
  // src/cli/cloud/runner.ts
10322
11361
  init_paths();
11362
+ init_shell();
11363
+ init_exec();
11364
+ init_creds();
10323
11365
  import { spawn as spawn4 } from "child_process";
10324
11366
  import { hostname } from "os";
10325
- init_creds();
10326
11367
 
10327
11368
  // src/cli/deploy/ssh-exec.ts
11369
+ init_exec();
10328
11370
  import { spawn as spawn3, spawnSync as spawnSync4 } from "child_process";
10329
11371
  function runOnBox(provider, cmd) {
10330
11372
  const target = effectiveSshTarget(provider);
@@ -10355,6 +11397,7 @@ function runOnBoxStreaming(provider, cmd) {
10355
11397
  }
10356
11398
 
10357
11399
  // src/cli/cloud/grove.ts
11400
+ init_shell();
10358
11401
  var GROVE_VERSION = "0.2.13";
10359
11402
  function ensureGroveInstalled(provider) {
10360
11403
  const probe = runOnBox(provider, "command -v grove >/dev/null 2>&1");
@@ -10374,9 +11417,10 @@ function ensureGroveRegistered(provider, repo, instancePath) {
10374
11417
  }
10375
11418
 
10376
11419
  // src/cli/cloud/repo.ts
11420
+ init_exec();
10377
11421
  import { spawnSync as spawnSync5 } from "child_process";
10378
- import { existsSync as existsSync28 } from "fs";
10379
- import { basename as basename6, join as join26 } from "path";
11422
+ import { existsSync as existsSync31 } from "fs";
11423
+ import { basename as basename6, join as join30 } from "path";
10380
11424
  function captureGit(args2) {
10381
11425
  const result = spawnSync5("git", args2, {
10382
11426
  encoding: "utf-8",
@@ -10427,10 +11471,10 @@ function buildRsyncArgs(localDir, remoteTarget) {
10427
11471
  ];
10428
11472
  }
10429
11473
  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";
11474
+ if (existsSync31(join30(toplevel, "pnpm-lock.yaml"))) return "pnpm";
11475
+ if (existsSync31(join30(toplevel, "bun.lockb"))) return "bun";
11476
+ if (existsSync31(join30(toplevel, "yarn.lock"))) return "yarn";
11477
+ if (existsSync31(join30(toplevel, "package-lock.json"))) return "npm";
10434
11478
  return null;
10435
11479
  }
10436
11480
  function packageManagerInstallCmd(pm) {
@@ -10450,6 +11494,7 @@ function packageManagerInstallCmd(pm) {
10450
11494
 
10451
11495
  // src/cli/cloud/sidecar.ts
10452
11496
  init_paths();
11497
+ init_shell();
10453
11498
  function readSidecar(provider, repo) {
10454
11499
  const path = boxCloudSidecarPath(repo);
10455
11500
  const result = runOnBox(provider, `cat ${shellQuoteHomePath(path)} 2>/dev/null`);
@@ -10658,6 +11703,7 @@ function pickProvider(explicit) {
10658
11703
  }
10659
11704
 
10660
11705
  // src/cli/commands/cloud.ts
11706
+ init_shell();
10661
11707
  function resolve11(raw) {
10662
11708
  const provider = pickProvider(raw.provider);
10663
11709
  const repo = raw.name ? raw.name : inferRepoName();
@@ -10712,8 +11758,8 @@ function attachNotify(diagnostic2) {
10712
11758
 
10713
11759
  // src/cli/commands/tmux-sessions.ts
10714
11760
  init_paths();
10715
- import { execSync as execSync15 } from "child_process";
10716
- import { readFileSync as readFileSync30, existsSync as existsSync29 } from "fs";
11761
+ import { execSync as execSync18 } from "child_process";
11762
+ import { readFileSync as readFileSync34, existsSync as existsSync32 } from "fs";
10717
11763
  var DOT_MAP = {
10718
11764
  "orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
10719
11765
  "orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
@@ -10724,16 +11770,16 @@ var DOT_MAP = {
10724
11770
  };
10725
11771
  function readManifest() {
10726
11772
  const p = sessionsManifestPath();
10727
- if (!existsSync29(p)) return null;
11773
+ if (!existsSync32(p)) return null;
10728
11774
  try {
10729
- return JSON.parse(readFileSync30(p, "utf-8"));
11775
+ return JSON.parse(readFileSync34(p, "utf-8"));
10730
11776
  } catch {
10731
11777
  return null;
10732
11778
  }
10733
11779
  }
10734
11780
  function tmuxExec(cmd) {
10735
11781
  try {
10736
- return execSync15(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
11782
+ return execSync18(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
10737
11783
  } catch {
10738
11784
  return null;
10739
11785
  }
@@ -10772,7 +11818,7 @@ if (nodeVersion < 22) {
10772
11818
  var program = new Command();
10773
11819
  program.name("sis").description("tmux-integrated orchestration daemon for Claude Code").version(
10774
11820
  JSON.parse(
10775
- readFileSync31(join27(dirname12(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
11821
+ readFileSync35(join31(dirname13(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
10776
11822
  ).version
10777
11823
  );
10778
11824
  program.configureHelp({
@@ -10797,6 +11843,7 @@ registerReconnect(session);
10797
11843
  registerClone(session);
10798
11844
  registerSessionTask(session);
10799
11845
  registerSessionEffort(session);
11846
+ registerSessionDangerous(session);
10800
11847
  registerSessionContext(session);
10801
11848
  var agent = program.command("agent").description("Manage agents");
10802
11849
  registerSpawn(agent);
@@ -10813,6 +11860,8 @@ registerSegmentUnregister(segment);
10813
11860
  var admin = program.command("admin").description("Admin / setup commands");
10814
11861
  registerSetup(admin);
10815
11862
  registerSetupKeybind(admin);
11863
+ registerCheckKeybinds(admin);
11864
+ registerCheckStatusbar(admin);
10816
11865
  registerHomeInit(admin);
10817
11866
  registerDoctor(admin);
10818
11867
  registerInit(admin);
@@ -10843,8 +11892,8 @@ Run 'sis admin getting-started' for a complete usage guide.
10843
11892
  var args = process.argv.slice(2);
10844
11893
  var firstArg = args[0];
10845
11894
  var skipWelcome = ["admin", "help", "--help", "-h", "--version", "-V"];
10846
- if (!existsSync30(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
10847
- mkdirSync14(globalDir(), { recursive: true });
11895
+ if (!existsSync33(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
11896
+ mkdirSync17(globalDir(), { recursive: true });
10848
11897
  console.log("");
10849
11898
  console.log(" Welcome to Sisyphus. Run 'sis admin setup' to get started.");
10850
11899
  console.log("");