sisyphi 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/chunk-KQBSC5KY.js +31 -0
  2. package/dist/chunk-KQBSC5KY.js.map +1 -0
  3. package/dist/{chunk-LTAW6OWS.js → chunk-YGBGKMTF.js} +31 -6
  4. package/dist/chunk-YGBGKMTF.js.map +1 -0
  5. package/dist/chunk-ZE2SKB4B.js +35 -0
  6. package/dist/chunk-ZE2SKB4B.js.map +1 -0
  7. package/dist/cli.js +638 -51
  8. package/dist/cli.js.map +1 -1
  9. package/dist/daemon.js +915 -289
  10. package/dist/daemon.js.map +1 -1
  11. package/dist/paths-FYYSBD27.js +58 -0
  12. package/dist/paths-FYYSBD27.js.map +1 -0
  13. package/dist/templates/CLAUDE.md +21 -20
  14. package/dist/templates/agent-plugin/agents/CLAUDE.md +2 -0
  15. package/dist/templates/agent-plugin/agents/debug.md +1 -0
  16. package/dist/templates/agent-plugin/agents/operator.md +1 -2
  17. package/dist/templates/agent-plugin/agents/plan.md +86 -55
  18. package/dist/templates/agent-plugin/agents/review-plan.md +1 -0
  19. package/dist/templates/agent-plugin/agents/spec-draft.md +1 -0
  20. package/dist/templates/agent-plugin/hooks/hooks.json +19 -1
  21. package/dist/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
  22. package/dist/templates/agent-plugin/hooks/require-submit.sh +24 -0
  23. package/dist/templates/agent-suffix.md +18 -0
  24. package/dist/templates/dashboard-claude.md +38 -0
  25. package/dist/templates/orchestrator-base.md +270 -0
  26. package/dist/templates/orchestrator-impl.md +116 -0
  27. package/dist/templates/orchestrator-planning.md +131 -0
  28. package/dist/templates/orchestrator-plugin/hooks/hooks.json +1 -15
  29. package/dist/templates/orchestrator-plugin/skills/git-management/SKILL.md +1 -1
  30. package/dist/templates/orchestrator-plugin/skills/orchestration/SKILL.md +4 -16
  31. package/dist/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +22 -23
  32. package/dist/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +11 -11
  33. package/dist/tui.js +3236 -0
  34. package/dist/tui.js.map +1 -0
  35. package/package.json +5 -1
  36. package/templates/CLAUDE.md +21 -20
  37. package/templates/agent-plugin/agents/CLAUDE.md +2 -0
  38. package/templates/agent-plugin/agents/debug.md +1 -0
  39. package/templates/agent-plugin/agents/operator.md +1 -2
  40. package/templates/agent-plugin/agents/plan.md +86 -55
  41. package/templates/agent-plugin/agents/review-plan.md +1 -0
  42. package/templates/agent-plugin/agents/spec-draft.md +1 -0
  43. package/templates/agent-plugin/hooks/hooks.json +19 -1
  44. package/templates/agent-plugin/hooks/intercept-send-message.sh +1 -1
  45. package/templates/agent-plugin/hooks/require-submit.sh +24 -0
  46. package/templates/agent-suffix.md +18 -0
  47. package/templates/dashboard-claude.md +38 -0
  48. package/templates/orchestrator-base.md +270 -0
  49. package/templates/orchestrator-impl.md +116 -0
  50. package/templates/orchestrator-planning.md +131 -0
  51. package/templates/orchestrator-plugin/hooks/hooks.json +1 -15
  52. package/templates/orchestrator-plugin/skills/git-management/SKILL.md +1 -1
  53. package/templates/orchestrator-plugin/skills/orchestration/SKILL.md +4 -16
  54. package/templates/orchestrator-plugin/skills/orchestration/task-patterns.md +22 -23
  55. package/templates/orchestrator-plugin/skills/orchestration/workflow-examples.md +11 -11
  56. package/dist/chunk-LTAW6OWS.js.map +0 -1
  57. package/dist/templates/orchestrator-plugin/scripts/block-task.sh +0 -11
  58. package/dist/templates/orchestrator.md +0 -173
  59. package/templates/orchestrator-plugin/scripts/block-task.sh +0 -11
  60. package/templates/orchestrator.md +0 -173
package/dist/cli.js CHANGED
@@ -1,31 +1,172 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ computeActiveTimeMs
4
+ } from "./chunk-ZE2SKB4B.js";
2
5
  import {
3
6
  daemonLogPath,
7
+ daemonPidPath,
4
8
  daemonUpdatingPath,
5
9
  globalDir,
10
+ roadmapPath,
6
11
  socketPath
7
- } from "./chunk-LTAW6OWS.js";
12
+ } from "./chunk-YGBGKMTF.js";
8
13
 
9
14
  // src/cli/index.ts
10
15
  import { Command } from "commander";
11
16
 
17
+ // src/cli/commands/start.ts
18
+ import { execSync as execSync5 } from "child_process";
19
+
12
20
  // src/cli/client.ts
13
21
  import { connect as connect2 } from "net";
14
22
 
15
23
  // src/cli/install.ts
16
- import { execSync } from "child_process";
17
- import { existsSync, mkdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "fs";
24
+ import { execSync as execSync2 } from "child_process";
25
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
18
26
  import { connect } from "net";
19
- import { homedir } from "os";
20
- import { dirname, join, resolve } from "path";
27
+ import { homedir as homedir2 } from "os";
28
+ import { dirname, join as join2, resolve } from "path";
21
29
  import { fileURLToPath } from "url";
30
+
31
+ // src/cli/tmux-setup.ts
32
+ import { execSync } from "child_process";
33
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, unlinkSync } from "fs";
34
+ import { homedir } from "os";
35
+ import { join } from "path";
36
+ var DEFAULT_KEY = "M-s";
37
+ var SISYPHUS_CONF_MARKER = "# sisyphus-managed \u2014 do not edit";
38
+ function cycleScriptPath() {
39
+ return join(globalDir(), "bin", "sisyphus-cycle");
40
+ }
41
+ function sisyphusTmuxConfPath() {
42
+ return join(globalDir(), "tmux.conf");
43
+ }
44
+ function userTmuxConfPath() {
45
+ const dotfile = join(homedir(), ".tmux.conf");
46
+ const xdg = join(homedir(), ".config", "tmux", "tmux.conf");
47
+ if (existsSync(xdg)) return xdg;
48
+ if (existsSync(dotfile)) return dotfile;
49
+ return null;
50
+ }
51
+ var CYCLE_SCRIPT = `#!/bin/bash
52
+ cwd=$(tmux show-option -v @sisyphus_cwd 2>/dev/null)
53
+ [ -z "$cwd" ] && exit 0
54
+ current=$(tmux display-message -p '#{session_name}')
55
+ sessions=()
56
+ while IFS= read -r name; do
57
+ scwd=$(tmux show-option -t "$name" -v @sisyphus_cwd 2>/dev/null)
58
+ [ "$scwd" = "$cwd" ] && sessions+=("$name")
59
+ done < <(tmux list-sessions -F '#{session_name}')
60
+ (( \${#sessions[@]} <= 1 )) && exit 0
61
+ for (( i=0; i<\${#sessions[@]}; i++ )); do
62
+ if [ "\${sessions[$i]}" = "$current" ]; then
63
+ next=$(( (i + 1) % \${#sessions[@]} ))
64
+ tmux switch-client -t "\${sessions[$next]}"
65
+ exit 0
66
+ fi
67
+ done
68
+ tmux switch-client -t "\${sessions[0]}"
69
+ `;
70
+ function installCycleScript() {
71
+ const scriptPath = cycleScriptPath();
72
+ mkdirSync(join(globalDir(), "bin"), { recursive: true });
73
+ writeFileSync(scriptPath, CYCLE_SCRIPT, "utf8");
74
+ chmodSync(scriptPath, 493);
75
+ }
76
+ function getExistingBinding(key) {
77
+ try {
78
+ const output = execSync("tmux list-keys", { stdio: ["pipe", "pipe", "pipe"] }).toString();
79
+ for (const line of output.split("\n")) {
80
+ if (line.includes(key)) {
81
+ const parts = line.trim().split(/\s+/);
82
+ const keyIdx = parts.indexOf(key);
83
+ if (keyIdx !== -1) {
84
+ return line.trim();
85
+ }
86
+ }
87
+ }
88
+ return null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ function isSisyphusBinding(binding) {
94
+ return binding.includes("sisyphus");
95
+ }
96
+ function setupTmuxKeybind(key = DEFAULT_KEY) {
97
+ installCycleScript();
98
+ const existing = getExistingBinding(key);
99
+ if (existing !== null && !isSisyphusBinding(existing)) {
100
+ return {
101
+ status: "conflict",
102
+ message: `Tmux key ${key} is already bound to something else. Run "sisyphus setup-keybind <key>" to use a different key.`,
103
+ existingBinding: existing
104
+ };
105
+ }
106
+ const confPath = sisyphusTmuxConfPath();
107
+ const bindingLine = `bind-key -T root ${key} run-shell ${cycleScriptPath()}`;
108
+ writeFileSync(confPath, `${SISYPHUS_CONF_MARKER}
109
+ ${bindingLine}
110
+ `, "utf8");
111
+ const userConf = userTmuxConfPath();
112
+ const sourceLine = `source-file ${confPath}`;
113
+ const markedSourceLine = `${sourceLine} ${SISYPHUS_CONF_MARKER}`;
114
+ let persistedToConf = false;
115
+ if (userConf !== null) {
116
+ const contents = readFileSync(userConf, "utf8");
117
+ if (!contents.includes(confPath)) {
118
+ const separator = contents.endsWith("\n") ? "" : "\n";
119
+ writeFileSync(userConf, `${contents}${separator}${markedSourceLine}
120
+ `, "utf8");
121
+ }
122
+ persistedToConf = true;
123
+ }
124
+ try {
125
+ execSync(`tmux bind-key -T root ${key} run-shell ${cycleScriptPath()}`, { stdio: "pipe" });
126
+ } catch {
127
+ }
128
+ if (existing !== null && isSisyphusBinding(existing)) {
129
+ return {
130
+ status: "already-installed",
131
+ message: `Tmux keybinding ${key} already configured for sisyphus.`
132
+ };
133
+ }
134
+ const persistNote = persistedToConf ? "" : `
135
+ Note: No tmux.conf found. Add this to your tmux config for persistence:
136
+ source-file ${confPath}`;
137
+ return {
138
+ status: "installed",
139
+ message: `Tmux keybinding set: ${key} cycles sisyphus sessions${persistNote}`
140
+ };
141
+ }
142
+ function removeTmuxKeybind() {
143
+ const confPath = sisyphusTmuxConfPath();
144
+ for (const candidate of [join(homedir(), ".tmux.conf"), join(homedir(), ".config", "tmux", "tmux.conf")]) {
145
+ if (existsSync(candidate)) {
146
+ const contents = readFileSync(candidate, "utf8");
147
+ const filtered = contents.split("\n").filter((line) => !line.includes(confPath)).join("\n");
148
+ if (filtered !== contents) {
149
+ writeFileSync(candidate, filtered, "utf8");
150
+ }
151
+ }
152
+ }
153
+ if (existsSync(confPath)) {
154
+ unlinkSync(confPath);
155
+ }
156
+ const scriptPath = cycleScriptPath();
157
+ if (existsSync(scriptPath)) {
158
+ unlinkSync(scriptPath);
159
+ }
160
+ }
161
+
162
+ // src/cli/install.ts
22
163
  var PLIST_LABEL = "com.sisyphus.daemon";
23
164
  var PLIST_FILENAME = `${PLIST_LABEL}.plist`;
24
165
  function launchAgentDir() {
25
- return join(homedir(), "Library", "LaunchAgents");
166
+ return join2(homedir2(), "Library", "LaunchAgents");
26
167
  }
27
168
  function plistPath() {
28
- return join(launchAgentDir(), PLIST_FILENAME);
169
+ return join2(launchAgentDir(), PLIST_FILENAME);
29
170
  }
30
171
  function daemonBinPath() {
31
172
  const installDir = dirname(fileURLToPath(import.meta.url));
@@ -56,7 +197,7 @@ function generatePlist(nodePath, daemonPath, logPath) {
56
197
  `;
57
198
  }
58
199
  function isInstalled() {
59
- return existsSync(plistPath());
200
+ return existsSync2(plistPath());
60
201
  }
61
202
  async function ensureDaemonInstalled() {
62
203
  if (process.platform !== "darwin") return;
@@ -64,11 +205,13 @@ async function ensureDaemonInstalled() {
64
205
  const nodePath = process.execPath;
65
206
  const daemonPath = daemonBinPath();
66
207
  const logPath = daemonLogPath();
67
- mkdirSync(globalDir(), { recursive: true });
68
- mkdirSync(launchAgentDir(), { recursive: true });
208
+ mkdirSync2(globalDir(), { recursive: true });
209
+ mkdirSync2(launchAgentDir(), { recursive: true });
69
210
  const plist = generatePlist(nodePath, daemonPath, logPath);
70
- writeFileSync(plistPath(), plist, "utf8");
71
- execSync(`launchctl load -w ${plistPath()}`);
211
+ writeFileSync2(plistPath(), plist, "utf8");
212
+ execSync2(`launchctl load -w ${plistPath()}`);
213
+ const keybindResult = setupTmuxKeybind();
214
+ printGettingStarted(keybindResult);
72
215
  }
73
216
  await waitForDaemon();
74
217
  }
@@ -78,24 +221,60 @@ async function uninstallDaemon(purge) {
78
221
  return;
79
222
  }
80
223
  const plist = plistPath();
81
- if (existsSync(plist)) {
224
+ if (existsSync2(plist)) {
82
225
  try {
83
- execSync(`launchctl unload -w ${plist}`, { stdio: "pipe" });
226
+ execSync2(`launchctl unload -w ${plist}`, { stdio: "pipe" });
84
227
  } catch {
85
228
  }
86
- unlinkSync(plist);
229
+ unlinkSync2(plist);
87
230
  console.log("Daemon unloaded and plist removed.");
88
231
  } else {
89
232
  console.log("Daemon is not installed (plist not found).");
90
233
  }
234
+ removeTmuxKeybind();
91
235
  if (purge) {
92
236
  const dir = globalDir();
93
- if (existsSync(dir)) {
237
+ if (existsSync2(dir)) {
94
238
  rmSync(dir, { recursive: true, force: true });
95
239
  console.log(`Removed ${dir}`);
96
240
  }
97
241
  }
98
242
  }
243
+ function printGettingStarted(keybindResult) {
244
+ const lines = [
245
+ "",
246
+ "Sisyphus installed \u2014 daemon running via launchd.",
247
+ "",
248
+ "Sisyphus is a tmux-integrated orchestration daemon for Claude Code multi-agent workflows.",
249
+ "A background daemon manages sessions where an orchestrator Claude breaks tasks into",
250
+ "subtasks, spawns agent Claude instances in tmux panes, and coordinates their lifecycle.",
251
+ "",
252
+ "Quick start:",
253
+ ' sisyphus start "task description" Start a session (must be inside tmux)',
254
+ " sisyphus list List sessions",
255
+ " sisyphus status Show current session status",
256
+ " sisyphus kill <id> Kill a session",
257
+ "",
258
+ "Monitoring:",
259
+ " sisyphus dashboard Open TUI dashboard",
260
+ " tail -f ~/.sisyphus/daemon.log Watch daemon logs",
261
+ ""
262
+ ];
263
+ if (keybindResult.status === "installed") {
264
+ lines.push(`Tmux keybind: ${keybindResult.message}`);
265
+ } else if (keybindResult.status === "conflict") {
266
+ lines.push(`Keybind: ${keybindResult.message}`);
267
+ }
268
+ lines.push(
269
+ "",
270
+ "Troubleshooting:",
271
+ " sisyphus doctor Check installation health",
272
+ " sisyphus setup-keybind [key] Configure tmux session-cycling keybind",
273
+ " sisyphus uninstall [--purge] Remove daemon and optionally all data",
274
+ ""
275
+ );
276
+ console.log(lines.join("\n"));
277
+ }
99
278
  function testConnection() {
100
279
  return new Promise((resolve2, reject) => {
101
280
  const sock = connect(socketPath());
@@ -117,10 +296,10 @@ async function waitForDaemon(maxWaitMs = 6e3) {
117
296
  let updatingLogged = false;
118
297
  while (Date.now() - start < maxWaitMs) {
119
298
  const updatingPath = daemonUpdatingPath();
120
- if (existsSync(updatingPath)) {
299
+ if (existsSync2(updatingPath)) {
121
300
  if (!updatingLogged) {
122
301
  try {
123
- const version = readFileSync(updatingPath, "utf-8").trim();
302
+ const version = readFileSync2(updatingPath, "utf-8").trim();
124
303
  console.log(`Updating sisyphus to ${version}...`);
125
304
  } catch {
126
305
  console.log("Updating sisyphus...");
@@ -129,7 +308,7 @@ async function waitForDaemon(maxWaitMs = 6e3) {
129
308
  }
130
309
  maxWaitMs = Math.max(maxWaitMs, 3e4);
131
310
  }
132
- if (existsSync(socketPath())) {
311
+ if (existsSync2(socketPath())) {
133
312
  try {
134
313
  await testConnection();
135
314
  return;
@@ -212,7 +391,7 @@ async function sendRequest(request) {
212
391
  }
213
392
 
214
393
  // src/cli/tmux.ts
215
- import { execSync as execSync2 } from "child_process";
394
+ import { execSync as execSync3 } from "child_process";
216
395
  function assertTmux() {
217
396
  if (!process.env.TMUX) {
218
397
  throw new Error("Not running inside a tmux pane. Sisyphus requires tmux.");
@@ -220,24 +399,79 @@ function assertTmux() {
220
399
  }
221
400
  function getTmuxSession() {
222
401
  assertTmux();
223
- return execSync2('tmux display-message -p "#{session_name}"', { encoding: "utf8" }).trim();
402
+ return execSync3('tmux display-message -p "#{session_name}"', { encoding: "utf8" }).trim();
224
403
  }
225
- function getTmuxWindow() {
226
- assertTmux();
227
- return execSync2('tmux display-message -p "#{window_id}"', { encoding: "utf8" }).trim();
404
+
405
+ // src/cli/commands/dashboard.ts
406
+ import { join as join3 } from "path";
407
+ import { execSync as execSync4 } from "child_process";
408
+ function shellQuote(s) {
409
+ return `'${s.replace(/'/g, "'\\''")}'`;
410
+ }
411
+ function isDashboardOpen(tmuxSession) {
412
+ try {
413
+ const windows = execSync4(
414
+ `tmux list-windows -t ${shellQuote(tmuxSession)} -F "#{window_name}"`,
415
+ { encoding: "utf-8" }
416
+ );
417
+ return windows.split("\n").some((name) => name.trim() === "sisyphus-dashboard");
418
+ } catch {
419
+ return false;
420
+ }
421
+ }
422
+ function launchDashboard(tmuxSession, cwd) {
423
+ const tuiPath = join3(import.meta.dirname, "tui.js");
424
+ const windowId = execSync4(
425
+ `tmux new-window -n "sisyphus-dashboard" -c ${shellQuote(cwd)} -P -F "#{window_id}"`,
426
+ { encoding: "utf-8" }
427
+ ).trim();
428
+ const cmd = `node ${shellQuote(tuiPath)} --cwd ${shellQuote(cwd)}`;
429
+ execSync4(
430
+ `tmux send-keys -t ${shellQuote(windowId)} ${shellQuote(cmd)} Enter`
431
+ );
432
+ }
433
+ function registerDashboard(program2) {
434
+ program2.command("dashboard").description("Launch the TUI dashboard for monitoring and managing sessions").action(async () => {
435
+ assertTmux();
436
+ const tmuxSession = getTmuxSession();
437
+ const cwd = process.cwd();
438
+ launchDashboard(tmuxSession, cwd);
439
+ });
228
440
  }
229
441
 
230
442
  // src/cli/commands/start.ts
443
+ function shellQuote2(s) {
444
+ return `'${s.replace(/'/g, "'\\''")}'`;
445
+ }
231
446
  function registerStart(program2) {
232
- program2.command("start").description("Start a new sisyphus session").argument("<task>", "Task description for the orchestrator").option("-c, --context <context>", "Background context for the orchestrator").action(async (task, opts) => {
233
- const tmuxSession = getTmuxSession();
234
- const tmuxWindow = getTmuxWindow();
235
- const request = { type: "start", task, context: opts.context, cwd: process.cwd(), tmuxSession, tmuxWindow };
447
+ program2.command("start").description("Start a new sisyphus session").argument("<task>", "Task description for the orchestrator").option("-c, --context <context>", "Background context for the orchestrator").option("-n, --name <name>", "Human-readable name for the session").action(async (task, opts) => {
448
+ const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
449
+ const request = { type: "start", task, context: opts.context, cwd, name: opts.name };
236
450
  const response = await sendRequest(request);
237
451
  if (response.ok) {
238
452
  const sessionId = response.data?.sessionId;
453
+ const tmuxSessionName = response.data?.tmuxSessionName;
454
+ if (process.env["TMUX"]) {
455
+ try {
456
+ execSync5(`tmux set-option @sisyphus_cwd ${shellQuote2(cwd)}`, { stdio: "ignore" });
457
+ } catch {
458
+ }
459
+ try {
460
+ const tmuxSession = getTmuxSession();
461
+ if (!isDashboardOpen(tmuxSession)) {
462
+ launchDashboard(tmuxSession, cwd);
463
+ console.log(`Dashboard opened in tmux window "sisyphus-dashboard"`);
464
+ }
465
+ } catch {
466
+ }
467
+ }
239
468
  console.log(`Task handed off to sisyphus orchestrator (session ${sessionId})`);
240
469
  console.log(`The orchestrator and its agents will handle this task autonomously \u2014 no further action needed from you.`);
470
+ if (tmuxSessionName) {
471
+ console.log(`
472
+ Tmux session: ${tmuxSessionName}`);
473
+ console.log(` tmux attach -t ${tmuxSessionName}`);
474
+ }
241
475
  console.log(`
242
476
  Monitor:`);
243
477
  console.log(` sisyphus status ${sessionId} # agents, cycles, reports`);
@@ -303,11 +537,11 @@ function registerSpawn(program2) {
303
537
  }
304
538
 
305
539
  // src/cli/commands/submit.ts
306
- import { execSync as execSync3 } from "child_process";
540
+ import { execSync as execSync6 } from "child_process";
307
541
  function isInWorktree() {
308
542
  try {
309
- const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
310
- const commonDir = execSync3("git rev-parse --git-common-dir", { encoding: "utf-8" }).trim();
543
+ const gitDir = execSync6("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
544
+ const commonDir = execSync6("git rev-parse --git-common-dir", { encoding: "utf-8" }).trim();
311
545
  return gitDir !== commonDir;
312
546
  } catch {
313
547
  return false;
@@ -315,7 +549,7 @@ function isInWorktree() {
315
549
  }
316
550
  function getUncommittedChanges() {
317
551
  try {
318
- const status = execSync3("git status --porcelain", { encoding: "utf-8" }).trim();
552
+ const status = execSync6("git status --porcelain", { encoding: "utf-8" }).trim();
319
553
  return status || null;
320
554
  } catch {
321
555
  return null;
@@ -360,7 +594,7 @@ function registerSubmit(program2) {
360
594
 
361
595
  // src/cli/commands/yield.ts
362
596
  function registerYield(program2) {
363
- program2.command("yield").description("Yield control back to daemon (orchestrator only)").option("--prompt <text>", "Instructions for the next orchestrator cycle (or pipe via stdin)").action(async (opts) => {
597
+ program2.command("yield").description("Yield control back to daemon (orchestrator only)").option("--prompt <text>", "Instructions for the next orchestrator cycle (or pipe via stdin)").option("--mode <mode>", "System prompt mode for next cycle (planning, implementation)").action(async (opts) => {
364
598
  assertTmux();
365
599
  const sessionId = process.env.SISYPHUS_SESSION_ID;
366
600
  if (!sessionId) {
@@ -368,7 +602,7 @@ function registerYield(program2) {
368
602
  process.exit(1);
369
603
  }
370
604
  const nextPrompt = opts.prompt ?? await readStdin() ?? void 0;
371
- const request = { type: "yield", sessionId, agentId: "orchestrator", nextPrompt };
605
+ const request = { type: "yield", sessionId, agentId: "orchestrator", nextPrompt, mode: opts.mode };
372
606
  const response = await sendRequest(request);
373
607
  if (response.ok) {
374
608
  console.log("Yielded. Waiting for agents to complete.");
@@ -394,7 +628,28 @@ function registerComplete(program2) {
394
628
  console.log("Session completed.");
395
629
  console.log(`
396
630
  Follow up:`);
397
- console.log(` sisyphus resume ${sessionId} "new instructions" # respawn orchestrator with follow-up`);
631
+ console.log(` sisyphus continue # reactivate and keep working`);
632
+ console.log(` sisyphus resume ${sessionId} "new instructions" # respawn orchestrator externally`);
633
+ } else {
634
+ console.error(`Error: ${response.error}`);
635
+ process.exit(1);
636
+ }
637
+ });
638
+ }
639
+
640
+ // src/cli/commands/continue.ts
641
+ function registerContinue(program2) {
642
+ program2.command("continue").description("Reactivate a completed session (orchestrator only)").action(async () => {
643
+ assertTmux();
644
+ const sessionId = process.env.SISYPHUS_SESSION_ID;
645
+ if (!sessionId) {
646
+ console.error("Error: SISYPHUS_SESSION_ID environment variable not set");
647
+ process.exit(1);
648
+ }
649
+ const request = { type: "continue", sessionId };
650
+ const response = await sendRequest(request);
651
+ if (response.ok) {
652
+ console.log("Session reactivated. Plan cleared.");
398
653
  } else {
399
654
  console.error(`Error: ${response.error}`);
400
655
  process.exit(1);
@@ -403,6 +658,7 @@ Follow up:`);
403
658
  }
404
659
 
405
660
  // src/cli/commands/status.ts
661
+ import { readFileSync as readFileSync3 } from "fs";
406
662
  var STATUS_COLORS = {
407
663
  active: "\x1B[32m",
408
664
  // green
@@ -423,13 +679,12 @@ var RESET = "\x1B[0m";
423
679
  var BOLD = "\x1B[1m";
424
680
  var DIM = "\x1B[2m";
425
681
  function colorize(text, status) {
426
- const color = STATUS_COLORS[status] ?? "";
682
+ const color = STATUS_COLORS[status];
683
+ if (!color) return `${text}${RESET}`;
427
684
  return `${color}${text}${RESET}`;
428
685
  }
429
- function formatDuration(startIso, endIso) {
430
- const start = new Date(startIso).getTime();
431
- const end = endIso ? new Date(endIso).getTime() : Date.now();
432
- const totalSeconds = Math.floor((end - start) / 1e3);
686
+ function formatMs(ms) {
687
+ const totalSeconds = Math.floor(ms / 1e3);
433
688
  if (totalSeconds < 0) return "0s";
434
689
  const hours = Math.floor(totalSeconds / 3600);
435
690
  const minutes = Math.floor(totalSeconds % 3600 / 60);
@@ -440,6 +695,30 @@ function formatDuration(startIso, endIso) {
440
695
  parts.push(`${seconds}s`);
441
696
  return parts.join(" ");
442
697
  }
698
+ function formatDuration(startOrMs, endIso) {
699
+ if (typeof startOrMs === "number") return formatMs(startOrMs);
700
+ const start = new Date(startOrMs).getTime();
701
+ const end = endIso ? new Date(endIso).getTime() : Date.now();
702
+ return formatMs(end - start);
703
+ }
704
+ function inferOrchestratorPhase(session) {
705
+ const cycles = session.orchestratorCycles;
706
+ if (cycles.length === 0) return "planning";
707
+ const lastCycle = cycles[cycles.length - 1];
708
+ if (!lastCycle.completedAt) {
709
+ const elapsed = Date.now() - new Date(lastCycle.timestamp).getTime();
710
+ if (elapsed < 5e3 || lastCycle.agentsSpawned.length === 0) return "planning";
711
+ return "spawning";
712
+ } else {
713
+ const runningAgents = session.agents.filter(
714
+ (a) => lastCycle.agentsSpawned.includes(a.id) && a.status === "running"
715
+ );
716
+ if (runningAgents.length > 0) {
717
+ return `waiting on ${runningAgents.map((a) => a.id).join(", ")}`;
718
+ }
719
+ return "starting";
720
+ }
721
+ }
443
722
  function formatAgent(agent) {
444
723
  const status = colorize(agent.status, agent.status);
445
724
  const name = `${BOLD}${agent.name}${RESET}`;
@@ -459,10 +738,41 @@ function formatAgent(agent) {
459
738
  }
460
739
  return line;
461
740
  }
462
- function formatCycle(cycle) {
463
- const duration = cycle.completedAt ? ` ${DIM}(${formatDuration(cycle.timestamp, cycle.completedAt)})${RESET}` : ` ${DIM}(running)${RESET}`;
741
+ function formatCycle(cycle, phase) {
742
+ let duration;
743
+ if (cycle.completedAt) {
744
+ duration = ` ${DIM}(${formatDuration(cycle.timestamp, cycle.completedAt)})${RESET}`;
745
+ } else {
746
+ const elapsed = formatDuration(cycle.timestamp, null);
747
+ duration = ` ${DIM}(running, ${elapsed})${RESET}`;
748
+ }
464
749
  const agents = cycle.agentsSpawned.length > 0 ? ` \u2014 agents: ${cycle.agentsSpawned.join(", ")}` : "";
465
- return ` Cycle ${cycle.cycle}${duration}${agents}`;
750
+ const phaseStr = phase ? ` \u2014 orchestrator: ${phase}` : "";
751
+ return ` Cycle ${cycle.cycle}${duration}${agents}${phaseStr}`;
752
+ }
753
+ function computeLastActivity(session) {
754
+ const timestamps = [];
755
+ for (const cycle of session.orchestratorCycles) {
756
+ timestamps.push(new Date(cycle.timestamp).getTime());
757
+ if (cycle.completedAt) timestamps.push(new Date(cycle.completedAt).getTime());
758
+ }
759
+ for (const agent of session.agents) {
760
+ timestamps.push(new Date(agent.spawnedAt).getTime());
761
+ if (agent.completedAt) timestamps.push(new Date(agent.completedAt).getTime());
762
+ for (const r of agent.reports) {
763
+ timestamps.push(new Date(r.timestamp).getTime());
764
+ }
765
+ }
766
+ if (timestamps.length === 0) return null;
767
+ return new Date(Math.max(...timestamps));
768
+ }
769
+ function readRoadmapTodos(cwd, sessionId) {
770
+ try {
771
+ const content = readFileSync3(roadmapPath(cwd, sessionId), "utf8");
772
+ return content.split("\n").filter((line) => /^\s*- \[ \]/.test(line)).map((line) => line.replace(/^\s*- \[ \]\s*/, "")).slice(0, 5);
773
+ } catch {
774
+ return [];
775
+ }
466
776
  }
467
777
  function printSession(session) {
468
778
  const status = colorize(session.status, session.status);
@@ -477,13 +787,40 @@ ${BOLD}Session: ${session.id}${RESET}`);
477
787
  }
478
788
  console.log(` CWD: ${session.cwd}`);
479
789
  console.log(` Created: ${session.createdAt}`);
480
- console.log(` Duration: ${sessionDuration}${session.completedAt ? "" : " (ongoing)"}`);
790
+ const activeTime = formatDuration(computeActiveTimeMs(session));
791
+ console.log(` Duration: ${sessionDuration}${session.completedAt ? "" : " (ongoing)"} (${activeTime} active)`);
792
+ const lastActivity = computeLastActivity(session);
793
+ if (lastActivity) {
794
+ console.log(` Last activity: ${formatMs(Date.now() - lastActivity.getTime())} ago`);
795
+ }
481
796
  console.log(` Orchestrator cycles: ${session.orchestratorCycles.length}`);
797
+ const runningAgents = session.agents.filter((a) => a.status === "running");
798
+ if (runningAgents.length > 0) {
799
+ console.log(`
800
+ ${BOLD}Active agents (${runningAgents.length}):${RESET}`);
801
+ for (const agent of runningAgents) {
802
+ const name = `${BOLD}${agent.name}${RESET}`;
803
+ const type = `${DIM}(${agent.agentType})${RESET}`;
804
+ const duration = formatDuration(agent.spawnedAt, null);
805
+ console.log(` ${agent.id} ${name} ${type} running ${duration}`);
806
+ }
807
+ }
808
+ const todos = readRoadmapTodos(session.cwd, session.id);
809
+ if (todos.length > 0) {
810
+ console.log(`
811
+ ${BOLD}Remaining (${todos.length} unchecked):${RESET}`);
812
+ for (const todo of todos) {
813
+ console.log(` - ${todo}`);
814
+ }
815
+ }
482
816
  if (session.orchestratorCycles.length > 0) {
483
817
  console.log(`
484
818
  ${BOLD}Cycles:${RESET}`);
485
- for (const cycle of session.orchestratorCycles) {
486
- console.log(formatCycle(cycle));
819
+ const cycles = session.orchestratorCycles;
820
+ for (let i = 0; i < cycles.length; i++) {
821
+ const isLast = i === cycles.length - 1;
822
+ const phase = isLast && session.status === "active" ? inferOrchestratorPhase(session) : void 0;
823
+ console.log(formatCycle(cycles[i], phase));
487
824
  }
488
825
  }
489
826
  if (session.agents.length > 0) {
@@ -594,15 +931,15 @@ function registerReport(program2) {
594
931
  // src/cli/commands/resume.ts
595
932
  function registerResume(program2) {
596
933
  program2.command("resume").description("Resume a paused session").argument("<session-id>", "Session ID to resume").argument("[message]", "Additional instructions for the orchestrator").action(async (sessionId, message) => {
597
- const tmuxSession = getTmuxSession();
598
- const tmuxWindow = getTmuxWindow();
599
934
  const cwd = process.cwd();
600
- const request = { type: "resume", sessionId, cwd, tmuxSession, tmuxWindow, message };
935
+ const request = { type: "resume", sessionId, cwd, message };
601
936
  const response = await sendRequest(request);
602
937
  if (response.ok) {
938
+ const tmuxSessionName = response.data?.tmuxSessionName;
603
939
  console.log(`Session ${sessionId} resumed`);
604
- if (response.data?.tmuxWindow) {
605
- console.log(`Orchestrator respawned in tmux window: ${response.data.tmuxWindow}`);
940
+ if (tmuxSessionName) {
941
+ console.log(`Tmux session: ${tmuxSessionName}`);
942
+ console.log(` tmux attach -t ${tmuxSessionName}`);
606
943
  }
607
944
  } else {
608
945
  console.error(`Error: ${response.error}`);
@@ -666,6 +1003,248 @@ function registerNotify(program2) {
666
1003
  });
667
1004
  }
668
1005
 
1006
+ // src/cli/commands/message.ts
1007
+ function registerMessage(program2) {
1008
+ program2.command("message <content>").description("Queue a message for the orchestrator to see on next cycle").option("--session <sessionId>", "Session ID (defaults to SISYPHUS_SESSION_ID env var)").action(async (content, opts) => {
1009
+ const sessionId = opts.session ?? process.env.SISYPHUS_SESSION_ID;
1010
+ if (!sessionId) {
1011
+ console.error("Error: provide --session or set SISYPHUS_SESSION_ID environment variable");
1012
+ process.exit(1);
1013
+ }
1014
+ const source = process.env.SISYPHUS_AGENT_ID ? { type: "agent", agentId: process.env.SISYPHUS_AGENT_ID } : void 0;
1015
+ const request = { type: "message", sessionId, content, source };
1016
+ const response = await sendRequest(request);
1017
+ if (response.ok) {
1018
+ console.log("Message queued");
1019
+ } else {
1020
+ console.error(`Error: ${response.error}`);
1021
+ process.exit(1);
1022
+ }
1023
+ });
1024
+ }
1025
+
1026
+ // src/cli/commands/update-task.ts
1027
+ function registerUpdateTask(program2) {
1028
+ program2.command("update-task <task>").description("Update the session task/goal").option("--session <sessionId>", "Session ID (defaults to SISYPHUS_SESSION_ID env var)").action(async (task, opts) => {
1029
+ const sessionId = opts.session ?? process.env.SISYPHUS_SESSION_ID;
1030
+ if (!sessionId) {
1031
+ console.error("Error: provide --session or set SISYPHUS_SESSION_ID environment variable");
1032
+ process.exit(1);
1033
+ }
1034
+ const request = { type: "update-task", sessionId, task };
1035
+ const response = await sendRequest(request);
1036
+ if (response.ok) {
1037
+ console.log("Task updated");
1038
+ } else {
1039
+ console.error(`Error: ${response.error}`);
1040
+ process.exit(1);
1041
+ }
1042
+ });
1043
+ }
1044
+
1045
+ // src/cli/commands/rollback.ts
1046
+ function registerRollback(program2) {
1047
+ program2.command("rollback <sessionId> <cycle>").description("Roll back a session to a previous cycle boundary").action(async (sessionId, cycleStr) => {
1048
+ const toCycle = parseInt(cycleStr, 10);
1049
+ if (isNaN(toCycle) || toCycle < 1) {
1050
+ console.error("Error: cycle must be a positive integer");
1051
+ process.exit(1);
1052
+ }
1053
+ const request = { type: "rollback", sessionId, cwd: process.cwd(), toCycle };
1054
+ const response = await sendRequest(request);
1055
+ if (response.ok) {
1056
+ const data = response.data;
1057
+ console.log(`Session ${sessionId} rolled back to cycle ${data.restoredToCycle}.`);
1058
+ console.log(`Session is now paused. Use 'sisyphus resume ${sessionId}' to respawn the orchestrator.`);
1059
+ } else {
1060
+ console.error(`Error: ${response.error}`);
1061
+ process.exit(1);
1062
+ }
1063
+ });
1064
+ }
1065
+
1066
+ // src/cli/commands/restart-agent.ts
1067
+ function registerRestartAgent(program2) {
1068
+ program2.command("restart-agent <agentId>").description("Restart a failed/killed/lost agent in a new tmux pane").option("-s, --session <sessionId>", "Session ID (defaults to SISYPHUS_SESSION_ID)").action(async (agentId, opts) => {
1069
+ const sessionId = opts.session ?? process.env.SISYPHUS_SESSION_ID;
1070
+ if (!sessionId) {
1071
+ console.error("Error: No session ID. Use --session or set SISYPHUS_SESSION_ID.");
1072
+ process.exit(1);
1073
+ }
1074
+ const request = { type: "restart-agent", sessionId, agentId };
1075
+ const response = await sendRequest(request);
1076
+ if (response.ok) {
1077
+ console.log(`Agent ${agentId} restarted.`);
1078
+ } else {
1079
+ console.error(`Error: ${response.error}`);
1080
+ process.exit(1);
1081
+ }
1082
+ });
1083
+ }
1084
+
1085
+ // src/cli/commands/setup-keybind.ts
1086
+ function registerSetupKeybind(program2) {
1087
+ program2.command("setup-keybind [key]").description("Install the sisyphus-cycle tmux keybinding (default: M-s)").action(async (key) => {
1088
+ const resolvedKey = key ?? DEFAULT_KEY;
1089
+ const result = setupTmuxKeybind(resolvedKey);
1090
+ switch (result.status) {
1091
+ case "installed":
1092
+ console.log(result.message);
1093
+ break;
1094
+ case "already-installed":
1095
+ console.log(result.message);
1096
+ break;
1097
+ case "conflict":
1098
+ console.log(`Key ${resolvedKey} is already bound:`);
1099
+ console.log(` ${result.existingBinding}`);
1100
+ console.log("");
1101
+ console.log("Use a different key, e.g.:");
1102
+ console.log(" sisyphus setup-keybind M-S");
1103
+ console.log(" sisyphus setup-keybind M-w");
1104
+ console.log(" sisyphus setup-keybind M-j");
1105
+ break;
1106
+ }
1107
+ });
1108
+ }
1109
+
1110
+ // src/cli/commands/doctor.ts
1111
+ import { execSync as execSync7 } from "child_process";
1112
+ import { existsSync as existsSync3, statSync } from "fs";
1113
+ function checkDaemonInstalled() {
1114
+ if (isInstalled()) {
1115
+ return { name: "Daemon plist", status: "ok", detail: "Installed in LaunchAgents" };
1116
+ }
1117
+ return {
1118
+ name: "Daemon plist",
1119
+ status: "fail",
1120
+ detail: "Not installed",
1121
+ fix: 'Run any sisyphus command to auto-install, or: sisyphus start "test"'
1122
+ };
1123
+ }
1124
+ function checkDaemonRunning() {
1125
+ const pid = daemonPidPath();
1126
+ if (!existsSync3(pid)) {
1127
+ return {
1128
+ name: "Daemon process",
1129
+ status: "fail",
1130
+ detail: "No PID file found",
1131
+ fix: "launchctl load -w ~/Library/LaunchAgents/com.sisyphus.daemon.plist"
1132
+ };
1133
+ }
1134
+ try {
1135
+ const sock = socketPath();
1136
+ execSync7(`test -S "${sock}"`, { stdio: "pipe" });
1137
+ return { name: "Daemon process", status: "ok", detail: `Socket at ${sock}` };
1138
+ } catch {
1139
+ return {
1140
+ name: "Daemon process",
1141
+ status: "warn",
1142
+ detail: "PID file exists but socket not found",
1143
+ fix: `Check logs: tail -20 ${daemonLogPath()}`
1144
+ };
1145
+ }
1146
+ }
1147
+ function checkTmux() {
1148
+ try {
1149
+ execSync7("which tmux", { stdio: "pipe" });
1150
+ } catch {
1151
+ return { name: "tmux", status: "fail", detail: "Not found on PATH", fix: "brew install tmux" };
1152
+ }
1153
+ try {
1154
+ execSync7("tmux list-sessions", { stdio: "pipe" });
1155
+ return { name: "tmux", status: "ok", detail: "Running" };
1156
+ } catch {
1157
+ return { name: "tmux", status: "warn", detail: "Installed but no server running" };
1158
+ }
1159
+ }
1160
+ function checkCycleScript() {
1161
+ const path = cycleScriptPath();
1162
+ if (!existsSync3(path)) {
1163
+ return {
1164
+ name: "Cycle script",
1165
+ status: "fail",
1166
+ detail: `Not found at ${path}`,
1167
+ fix: "sisyphus setup-keybind"
1168
+ };
1169
+ }
1170
+ try {
1171
+ const mode = statSync(path).mode;
1172
+ if ((mode & 73) === 0) {
1173
+ return {
1174
+ name: "Cycle script",
1175
+ status: "fail",
1176
+ detail: "Not executable",
1177
+ fix: `chmod +x ${path}`
1178
+ };
1179
+ }
1180
+ } catch {
1181
+ }
1182
+ return { name: "Cycle script", status: "ok", detail: path };
1183
+ }
1184
+ function checkTmuxKeybind() {
1185
+ const existing = getExistingBinding(DEFAULT_KEY);
1186
+ if (existing === null) {
1187
+ if (existsSync3(sisyphusTmuxConfPath())) {
1188
+ return {
1189
+ name: `Tmux keybind (${DEFAULT_KEY})`,
1190
+ status: "warn",
1191
+ detail: "Configured in sisyphus tmux.conf but not active (tmux may not be running)"
1192
+ };
1193
+ }
1194
+ return {
1195
+ name: `Tmux keybind (${DEFAULT_KEY})`,
1196
+ status: "fail",
1197
+ detail: "Not bound",
1198
+ fix: "sisyphus setup-keybind"
1199
+ };
1200
+ }
1201
+ if (isSisyphusBinding(existing)) {
1202
+ return { name: `Tmux keybind (${DEFAULT_KEY})`, status: "ok", detail: "Bound to sisyphus-cycle" };
1203
+ }
1204
+ return {
1205
+ name: `Tmux keybind (${DEFAULT_KEY})`,
1206
+ status: "warn",
1207
+ detail: `Bound to something else: ${existing}`,
1208
+ fix: "sisyphus setup-keybind M-S (or another free key)"
1209
+ };
1210
+ }
1211
+ function checkGlobalDir() {
1212
+ const dir = globalDir();
1213
+ if (existsSync3(dir)) {
1214
+ return { name: "Data directory", status: "ok", detail: dir };
1215
+ }
1216
+ return { name: "Data directory", status: "warn", detail: `${dir} does not exist (created on first use)` };
1217
+ }
1218
+ var SYMBOLS = { ok: "\u2713", warn: "!", fail: "\u2717" };
1219
+ function registerDoctor(program2) {
1220
+ program2.command("doctor").description("Check sisyphus installation health").action(async () => {
1221
+ const checks = [
1222
+ checkGlobalDir(),
1223
+ checkDaemonInstalled(),
1224
+ checkDaemonRunning(),
1225
+ checkTmux(),
1226
+ checkCycleScript(),
1227
+ checkTmuxKeybind()
1228
+ ];
1229
+ let hasIssues = false;
1230
+ for (const c of checks) {
1231
+ const sym = SYMBOLS[c.status];
1232
+ console.log(` ${sym} ${c.name}: ${c.detail}`);
1233
+ if (c.status !== "ok") hasIssues = true;
1234
+ }
1235
+ const fixable = checks.filter((c) => c.fix);
1236
+ if (fixable.length > 0) {
1237
+ console.log("\nFixes:");
1238
+ for (const c of fixable) {
1239
+ console.log(` ${c.name}: ${c.fix}`);
1240
+ }
1241
+ }
1242
+ if (!hasIssues) {
1243
+ console.log("\nAll checks passed.");
1244
+ }
1245
+ });
1246
+ }
1247
+
669
1248
  // src/cli/index.ts
670
1249
  var program = new Command();
671
1250
  program.name("sisyphus").description("tmux-integrated orchestration daemon for Claude Code").version("0.1.0");
@@ -675,12 +1254,20 @@ registerSubmit(program);
675
1254
  registerReport(program);
676
1255
  registerYield(program);
677
1256
  registerComplete(program);
1257
+ registerContinue(program);
678
1258
  registerStatus(program);
679
1259
  registerList(program);
680
1260
  registerResume(program);
681
1261
  registerKill(program);
682
1262
  registerUninstall(program);
683
1263
  registerNotify(program);
1264
+ registerMessage(program);
1265
+ registerUpdateTask(program);
1266
+ registerDashboard(program);
1267
+ registerRollback(program);
1268
+ registerRestartAgent(program);
1269
+ registerSetupKeybind(program);
1270
+ registerDoctor(program);
684
1271
  program.parseAsync(process.argv).catch((err) => {
685
1272
  console.error(err.message);
686
1273
  process.exit(1);