sisyphi 1.1.36 → 1.1.37

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
@@ -668,8 +668,8 @@ var init_creds = __esm({
668
668
 
669
669
  // src/cli/index.ts
670
670
  import { Command } from "commander";
671
- import { existsSync as existsSync33, mkdirSync as mkdirSync17, readFileSync as readFileSync35 } from "fs";
672
- import { dirname as dirname13, join as join31 } from "path";
671
+ import { existsSync as existsSync34, mkdirSync as mkdirSync17, readFileSync as readFileSync36 } from "fs";
672
+ import { dirname as dirname13, join as join32 } from "path";
673
673
  import { fileURLToPath as fileURLToPath5 } from "url";
674
674
 
675
675
  // src/cli/commands/start.ts
@@ -2628,7 +2628,7 @@ function rawSend2(request, timeoutMs = 1e4) {
2628
2628
  return rawSend(request, timeoutMs);
2629
2629
  }
2630
2630
  async function sendRequest(request, timeoutMs) {
2631
- const sleep2 = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
2631
+ const sleep3 = (ms) => new Promise((resolve12) => setTimeout(resolve12, ms));
2632
2632
  const MAX_ATTEMPTS = 5;
2633
2633
  const RETRY_DELAY_MS = 2e3;
2634
2634
  let installedDaemon = false;
@@ -2650,7 +2650,7 @@ async function sendRequest(request, timeoutMs) {
2650
2650
  } else {
2651
2651
  process.stderr.write(`Daemon not ready, retrying (${attempt}/${MAX_ATTEMPTS - 1})...
2652
2652
  `);
2653
- await sleep2(RETRY_DELAY_MS);
2653
+ await sleep3(RETRY_DELAY_MS);
2654
2654
  }
2655
2655
  }
2656
2656
  }
@@ -3102,6 +3102,21 @@ ${BOLD}Session: ${session2.id}${RESET}`);
3102
3102
  console.log(` Last activity: ${formatDuration(Date.now() - lastActivity.getTime())} ago`);
3103
3103
  }
3104
3104
  console.log(` Orchestrator cycles: ${session2.orchestratorCycles.length}`);
3105
+ if (session2.handoff) {
3106
+ const h = session2.handoff;
3107
+ if (h.lastError) {
3108
+ console.log(` Handoff: ${COLOR_CODES.red}error${RESET} \u2014 ${h.lastError}`);
3109
+ } else if (h.reclaimedAt) {
3110
+ const where = h.target ? `${h.target.provider}:${h.target.repo}` : "cloud";
3111
+ console.log(` Handoff: reclaimed from ${where} at ${h.reclaimedAt}`);
3112
+ } else if (h.sentAt && h.target) {
3113
+ console.log(` Handoff: running on ${h.target.provider}:${h.target.repo} since ${h.sentAt}`);
3114
+ } else if (h.target) {
3115
+ console.log(` Handoff: queued \u2192 ${h.target.provider}:${h.target.repo} (since ${h.queuedAt})`);
3116
+ } else {
3117
+ console.log(` Handoff: quiesce queued (since ${h.queuedAt})`);
3118
+ }
3119
+ }
3105
3120
  const runningAgents = session2.agents.filter((a) => a.status === "running");
3106
3121
  if (runningAgents.length > 0) {
3107
3122
  console.log(`
@@ -3219,6 +3234,24 @@ var STATUS_COLORS = {
3219
3234
  var RESET2 = "\x1B[0m";
3220
3235
  var BOLD2 = "\x1B[1m";
3221
3236
  var DIM2 = "\x1B[2m";
3237
+ var CLOUD = "\x1B[35m";
3238
+ var RED = "\x1B[31m";
3239
+ function handoffAnnotation(h) {
3240
+ if (!h) return "";
3241
+ if (h.lastError) {
3242
+ return ` ${RED}handoff error: ${h.lastError}${RESET2}`;
3243
+ }
3244
+ if (h.reclaimedAt) {
3245
+ return ` ${DIM2}(reclaimed)${RESET2}`;
3246
+ }
3247
+ if (h.sentAt && h.target) {
3248
+ return ` ${CLOUD}\u2192 ${h.target.provider}:${h.target.repo}${RESET2}`;
3249
+ }
3250
+ if (h.target) {
3251
+ return ` ${CLOUD}handoff queued \u2192 ${h.target.provider}:${h.target.repo}${RESET2}`;
3252
+ }
3253
+ return ` ${CLOUD}quiesce queued${RESET2}`;
3254
+ }
3222
3255
  function truncateTask(task, max) {
3223
3256
  if (task.length <= max) return task;
3224
3257
  return task.slice(0, max - 1) + "\u2026";
@@ -3248,7 +3281,8 @@ function registerList(program2) {
3248
3281
  const task = truncateTask(s.task, 60);
3249
3282
  const label = s.name ? `${s.name} ${DIM2}(${s.id.slice(0, 8)})${RESET2}` : s.id;
3250
3283
  const cwdLabel = opts.all && s.cwd ? ` ${DIM2}${basename3(s.cwd)}${RESET2}` : "";
3251
- console.log(` ${BOLD2}${label}${RESET2} ${status} ${agents} ${task}${cwdLabel}`);
3284
+ const handoffLabel = handoffAnnotation(s.handoff);
3285
+ console.log(` ${BOLD2}${label}${RESET2} ${status} ${agents} ${task}${cwdLabel}${handoffLabel}`);
3252
3286
  }
3253
3287
  if (filtered && totalCount && totalCount > sessions.length) {
3254
3288
  const otherCount = totalCount - sessions.length;
@@ -3877,6 +3911,7 @@ var ACTIONABLE_KINDS = /* @__PURE__ */ new Set([
3877
3911
  "error"
3878
3912
  ]);
3879
3913
  var HEARTBEAT_ASKED_BY = "system:heartbeat";
3914
+ var ORPHAN_ASKED_BY = "system:orphan-handler";
3880
3915
  function maybeNotifyOnAskCreated(cwd, sessionId, meta) {
3881
3916
  if (process.env.NODE_ENV === "test" || process.env.SISYPHUS_DISABLE_NOTIFY === "1") return;
3882
3917
  const isActionable = meta.kind !== void 0 && ACTIONABLE_KINDS.has(meta.kind);
@@ -3986,6 +4021,7 @@ async function autoResolveAsk(cwd, sessionId, askId, deck) {
3986
4021
  async function maybeAutoResolveAsk(cwd, sessionId, askId, deck) {
3987
4022
  try {
3988
4023
  if (!isSessionDangerous(cwd, sessionId)) return;
4024
+ if (deck.source?.askedBy === ORPHAN_ASKED_BY) return;
3989
4025
  await autoResolveAsk(cwd, sessionId, askId, deck);
3990
4026
  } catch {
3991
4027
  }
@@ -6216,6 +6252,30 @@ function setSessionCwd(name, cwd) {
6216
6252
  );
6217
6253
  }
6218
6254
 
6255
+ // src/cli/commands/quiesce.ts
6256
+ function registerQuiesce(parent) {
6257
+ parent.command("quiesce <session-id>").description("Pause a session at the next quiesce point (or now with --force). No cloud push.").option("--force", "Interrupt running orchestrator/agents immediately.").action(async (sessionId, opts) => {
6258
+ const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
6259
+ const request = {
6260
+ type: "admin-quiesce",
6261
+ sessionId,
6262
+ cwd,
6263
+ force: opts.force === true
6264
+ };
6265
+ const response = await sendRequest(request);
6266
+ if (!response.ok) {
6267
+ console.error(`Error: ${response.error}`);
6268
+ process.exit(1);
6269
+ }
6270
+ const data = response.data;
6271
+ if (data?.force) {
6272
+ console.log(`Session ${sessionId} quiescing now (--force).`);
6273
+ } else {
6274
+ console.log(`Session ${sessionId} will pause at next quiesce point.`);
6275
+ }
6276
+ });
6277
+ }
6278
+
6219
6279
  // src/cli/commands/doctor.ts
6220
6280
  init_paths();
6221
6281
  import { execSync as execSync14 } from "child_process";
@@ -11421,30 +11481,31 @@ init_exec();
11421
11481
  import { spawnSync as spawnSync5 } from "child_process";
11422
11482
  import { existsSync as existsSync31 } from "fs";
11423
11483
  import { basename as basename6, join as join30 } from "path";
11424
- function captureGit(args2) {
11484
+ function captureGit(args2, cwd) {
11425
11485
  const result = spawnSync5("git", args2, {
11426
11486
  encoding: "utf-8",
11427
- env: EXEC_ENV
11487
+ env: EXEC_ENV,
11488
+ cwd: cwd ?? process.cwd()
11428
11489
  });
11429
11490
  if (typeof result.stdout !== "string") {
11430
11491
  throw new Error("Internal: git spawn did not capture stdout as string");
11431
11492
  }
11432
11493
  return { stdout: result.stdout.trim(), ok: result.status === 0 };
11433
11494
  }
11434
- function inferRepoName() {
11435
- const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"]);
11495
+ function inferRepoName(cwd) {
11496
+ const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"], cwd);
11436
11497
  if (ok && stdout) return basename6(stdout);
11437
- return basename6(process.cwd());
11498
+ return basename6(cwd ?? process.cwd());
11438
11499
  }
11439
- function getOriginUrl() {
11440
- const { stdout, ok } = captureGit(["remote", "get-url", "origin"]);
11500
+ function getOriginUrl(cwd) {
11501
+ const { stdout, ok } = captureGit(["remote", "get-url", "origin"], cwd);
11441
11502
  if (!ok) return null;
11442
11503
  return stdout.length > 0 ? stdout : null;
11443
11504
  }
11444
- function getRepoToplevel() {
11445
- const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"]);
11505
+ function getRepoToplevel(cwd) {
11506
+ const { stdout, ok } = captureGit(["rev-parse", "--show-toplevel"], cwd);
11446
11507
  if (ok && stdout) return stdout;
11447
- return process.cwd();
11508
+ return cwd ?? process.cwd();
11448
11509
  }
11449
11510
  var DEFAULT_EXCLUDES = [
11450
11511
  ".sisyphus/",
@@ -11523,10 +11584,10 @@ function writeSidecar(provider, repo, data) {
11523
11584
  }
11524
11585
 
11525
11586
  // src/cli/cloud/runner.ts
11526
- async function cloudSync(provider, repo, opts) {
11587
+ async function cloudSync(provider, repo, opts, cwd) {
11527
11588
  const target = effectiveSshTarget(provider);
11528
11589
  const remoteDir = boxRepoPath(repo);
11529
- const localOrigin = getOriginUrl();
11590
+ const localOrigin = getOriginUrl(cwd);
11530
11591
  ensureGroveInstalled(provider);
11531
11592
  const existing = readSidecar(provider, repo);
11532
11593
  if (existing && existing.originUrl && localOrigin && existing.originUrl !== localOrigin) {
@@ -11564,7 +11625,7 @@ Pass --name <slug> to disambiguate, or --fresh to overwrite.`
11564
11625
  if (mkdir.exitCode !== 0) {
11565
11626
  throw new Error(`Failed to mkdir on box: ${mkdir.stderr}`);
11566
11627
  }
11567
- const toplevel = getRepoToplevel();
11628
+ const toplevel = getRepoToplevel(cwd);
11568
11629
  const args2 = buildRsyncArgs(toplevel, `${target}:${remoteDir}/`);
11569
11630
  console.log(`\u2192 rsync ${toplevel}/ \u2192 ${target}:${remoteDir}/`);
11570
11631
  const code = await runRsync(args2);
@@ -11588,9 +11649,9 @@ function runRsync(args2) {
11588
11649
  child.on("exit", (code) => resolve12(code === null ? 1 : code));
11589
11650
  });
11590
11651
  }
11591
- async function cloudInstall(provider, repo) {
11652
+ async function cloudInstall(provider, repo, cwd) {
11592
11653
  const remoteDir = boxRepoPath(repo);
11593
- const toplevel = getRepoToplevel();
11654
+ const toplevel = getRepoToplevel(cwd);
11594
11655
  const pm = detectPackageManager(toplevel);
11595
11656
  const cmd = packageManagerInstallCmd(pm);
11596
11657
  if (!cmd) {
@@ -11603,7 +11664,7 @@ async function cloudInstall(provider, repo) {
11603
11664
  if (code !== 0) throw new Error(`${pm} install failed (exit ${code})`);
11604
11665
  const existing = readSidecar(provider, repo);
11605
11666
  const sidecar = {
11606
- originUrl: existing && existing.originUrl !== void 0 ? existing.originUrl : getOriginUrl(),
11667
+ originUrl: existing && existing.originUrl !== void 0 ? existing.originUrl : getOriginUrl(cwd),
11607
11668
  localHostname: existing ? existing.localHostname : hostname(),
11608
11669
  lastSync: existing?.lastSync,
11609
11670
  lastInstall: (/* @__PURE__ */ new Date()).toISOString(),
@@ -11681,6 +11742,13 @@ function cloudStatus(provider, repo) {
11681
11742
  }
11682
11743
  }
11683
11744
 
11745
+ // src/cli/cloud/handoff.ts
11746
+ import { spawn as spawn5 } from "child_process";
11747
+ import { existsSync as existsSync32, readFileSync as readFileSync34 } from "fs";
11748
+ init_exec();
11749
+ init_paths();
11750
+ init_shell();
11751
+
11684
11752
  // src/cli/deploy/provider-pick.ts
11685
11753
  init_creds();
11686
11754
  function pickProvider(explicit) {
@@ -11702,6 +11770,188 @@ function pickProvider(explicit) {
11702
11770
  );
11703
11771
  }
11704
11772
 
11773
+ // src/cli/cloud/handoff.ts
11774
+ import { join as join31 } from "path";
11775
+ async function cloudHandoff(sessionId, opts) {
11776
+ const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
11777
+ const request = {
11778
+ type: "cloud-handoff",
11779
+ sessionId,
11780
+ cwd,
11781
+ provider: opts.provider,
11782
+ repo: opts.repo,
11783
+ force: opts.force
11784
+ };
11785
+ const response = await sendRequest(request);
11786
+ if (!response.ok) {
11787
+ console.error(`Error: ${response.error}`);
11788
+ process.exit(1);
11789
+ }
11790
+ const data = response.data;
11791
+ const where = `${data?.provider ?? opts.provider}:${data?.repo ?? opts.repo}`;
11792
+ if (data?.force) {
11793
+ console.log(`Forcing handoff of ${sessionId} \u2192 ${where} (interrupting in-flight work).`);
11794
+ } else {
11795
+ console.log(`Handoff of ${sessionId} \u2192 ${where} queued; will fire at next quiesce point.`);
11796
+ }
11797
+ if (!opts.wait) {
11798
+ if (!opts.force) {
11799
+ console.log(`Tip: run \`sis cloud handoff ${sessionId} --cancel\` to cancel before quiesce.`);
11800
+ }
11801
+ return;
11802
+ }
11803
+ console.log("Waiting for handoff to complete...");
11804
+ await waitForSentOrError(cwd, sessionId);
11805
+ }
11806
+ async function cloudHandoffCancel(sessionId) {
11807
+ const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
11808
+ const response = await sendRequest({ type: "cloud-handoff-cancel", sessionId, cwd });
11809
+ if (!response.ok) {
11810
+ console.error(`Error: ${response.error}`);
11811
+ process.exit(1);
11812
+ }
11813
+ console.log(`Handoff for ${sessionId} cancelled.`);
11814
+ }
11815
+ async function waitForSentOrError(cwd, sessionId) {
11816
+ const POLL_INTERVAL_MS = 2e3;
11817
+ const MAX_WAIT_MS = 30 * 60 * 1e3;
11818
+ const start = Date.now();
11819
+ const path = statePath(cwd, sessionId);
11820
+ while (Date.now() - start < MAX_WAIT_MS) {
11821
+ await sleep2(POLL_INTERVAL_MS);
11822
+ if (!existsSync32(path)) continue;
11823
+ let session2;
11824
+ try {
11825
+ session2 = JSON.parse(readFileSync34(path, "utf-8"));
11826
+ } catch (err) {
11827
+ void err;
11828
+ continue;
11829
+ }
11830
+ if (session2.handoff?.lastError) {
11831
+ console.error(`Handoff failed: ${session2.handoff.lastError}`);
11832
+ process.exit(1);
11833
+ }
11834
+ if (session2.handoff?.sentAt) {
11835
+ const where = session2.handoff.target ? `${session2.handoff.target.provider}:${session2.handoff.target.repo}` : "cloud";
11836
+ console.log(`Handoff complete \u2192 ${where} at ${session2.handoff.sentAt}.`);
11837
+ return;
11838
+ }
11839
+ }
11840
+ console.error(`Timed out waiting for handoff after ${Math.round(MAX_WAIT_MS / 6e4)}m.`);
11841
+ process.exit(1);
11842
+ }
11843
+ function sleep2(ms) {
11844
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
11845
+ }
11846
+ async function cloudReclaim(sessionId, opts) {
11847
+ const cwd = process.env["SISYPHUS_CWD"] ?? process.cwd();
11848
+ const local = readLocalSession(cwd, sessionId);
11849
+ if (!local.handoff?.sentAt) {
11850
+ console.error(`Session ${sessionId} is not on cloud (no handoff.sentAt). Nothing to reclaim.`);
11851
+ process.exit(1);
11852
+ }
11853
+ if (local.handoff.reclaimedAt) {
11854
+ console.error(`Session ${sessionId} was already reclaimed at ${local.handoff.reclaimedAt}.`);
11855
+ process.exit(1);
11856
+ }
11857
+ if (!local.handoff.target) {
11858
+ console.error(`Session ${sessionId} has handoff.sentAt but no target \u2014 corrupted state.`);
11859
+ process.exit(1);
11860
+ }
11861
+ const provider = pickProvider(opts.providerOverride ?? local.handoff.target.provider);
11862
+ const repo = local.handoff.target.repo;
11863
+ const target = effectiveSshTarget(provider);
11864
+ const remoteSessionDir = `${boxRepoPath(repo)}/.sisyphus/sessions/${sessionId}`;
11865
+ const remoteRepoDir = boxRepoPath(repo);
11866
+ console.log(`Reclaiming ${sessionId} from ${provider}:${repo}...`);
11867
+ const quiesceBase = opts.force ? `sis admin quiesce ${shellQuote(sessionId)} --force` : `sis admin quiesce ${shellQuote(sessionId)}`;
11868
+ const quiesceCmd = `cd ${shellQuoteHomePath(remoteRepoDir)} && ${quiesceBase}`;
11869
+ console.log(`\u2192 ssh box: ${quiesceCmd}`);
11870
+ const quiesceCode = await runOnBoxStreaming(provider, quiesceCmd);
11871
+ if (quiesceCode !== 0) {
11872
+ console.error(`Box-side quiesce failed (exit ${quiesceCode}).`);
11873
+ process.exit(1);
11874
+ }
11875
+ console.log(`\u2192 waiting for box-side session to reach paused...`);
11876
+ await waitForBoxPaused(provider, remoteSessionDir);
11877
+ console.log(`\u2192 rsync session state down`);
11878
+ await rsyncDown(target, `${remoteSessionDir}/`, `${sessionDir(cwd, sessionId)}/`, { withDelete: true });
11879
+ console.log(`\u2192 rsync working tree down`);
11880
+ await rsyncDown(target, `${remoteRepoDir}/`, `${cwd}/`, { withDelete: false });
11881
+ for (const name of ["config.json", "orchestrator.md", "orchestrator-settings.json"]) {
11882
+ const remotePath = `${remoteRepoDir}/.sisyphus/${name}`;
11883
+ const localPath = join31(projectDir(cwd), name);
11884
+ const probe = runOnBox(provider, `test -f ${shellQuote(remotePath.replace(/^~\//, ""))} && echo y || echo n`);
11885
+ if (probe.stdout.trim() !== "y") continue;
11886
+ await rsyncDown(target, remotePath, localPath, { withDelete: false });
11887
+ }
11888
+ const reclaimMessage = `Session reclaimed from cloud (${provider}:${repo}). Resuming locally.`;
11889
+ console.log(`\u2192 local sis resume`);
11890
+ const resumeResp = await sendRequest({
11891
+ type: "resume",
11892
+ sessionId,
11893
+ cwd,
11894
+ message: reclaimMessage
11895
+ });
11896
+ if (!resumeResp.ok) {
11897
+ console.error(`Local resume failed: ${resumeResp.error}`);
11898
+ process.exit(1);
11899
+ }
11900
+ const killCmd = `cd ${shellQuoteHomePath(remoteRepoDir)} && sis session kill ${shellQuote(sessionId)}`;
11901
+ console.log(`\u2192 ssh box: ${killCmd}`);
11902
+ const killResult = runOnBox(provider, killCmd);
11903
+ if (killResult.exitCode !== 0) {
11904
+ console.warn(`Warning: box-side kill exited ${killResult.exitCode}: ${killResult.stderr.trim()}`);
11905
+ }
11906
+ const finalizeResp = await sendRequest({ type: "cloud-reclaim-finalize", sessionId, cwd });
11907
+ if (!finalizeResp.ok) {
11908
+ console.warn(`Warning: failed to finalize reclaim state: ${finalizeResp.error}`);
11909
+ }
11910
+ console.log(`\u2713 ${sessionId} reclaimed; orchestrator respawning locally.`);
11911
+ }
11912
+ function readLocalSession(cwd, sessionId) {
11913
+ const path = statePath(cwd, sessionId);
11914
+ if (!existsSync32(path)) {
11915
+ console.error(`No local state.json for ${sessionId} at ${path}.`);
11916
+ process.exit(1);
11917
+ }
11918
+ return JSON.parse(readFileSync34(path, "utf-8"));
11919
+ }
11920
+ async function waitForBoxPaused(provider, remoteSessionDir) {
11921
+ const POLL_INTERVAL_MS = 2e3;
11922
+ const MAX_WAIT_MS = 30 * 60 * 1e3;
11923
+ const start = Date.now();
11924
+ const cmd = `cat ${shellQuote(remoteSessionDir.replace(/^~\//, ""))}/state.json 2>/dev/null | head -c 200000`;
11925
+ while (Date.now() - start < MAX_WAIT_MS) {
11926
+ const r = runOnBox(provider, cmd);
11927
+ if (r.exitCode === 0 && r.stdout.trim()) {
11928
+ try {
11929
+ const s = JSON.parse(r.stdout);
11930
+ if (s.status === "paused") return;
11931
+ } catch (err) {
11932
+ console.warn(`Could not parse box-side state.json yet: ${err instanceof Error ? err.message : String(err)}`);
11933
+ }
11934
+ }
11935
+ await sleep2(POLL_INTERVAL_MS);
11936
+ }
11937
+ console.error(`Timed out waiting for box-side session to pause.`);
11938
+ process.exit(1);
11939
+ }
11940
+ function rsyncDown(target, remotePath, localPath, opts) {
11941
+ return new Promise((resolve12, reject) => {
11942
+ const args2 = ["-avz"];
11943
+ if (opts.withDelete) args2.push("--delete");
11944
+ args2.push("--exclude=.terraform/", "--exclude=node_modules/", "--exclude=dist/", "--exclude=.next/", "--exclude=.turbo/", "--exclude=coverage/", "--exclude=tmp/", "--exclude=.git/lfs/", "--exclude=.DS_Store");
11945
+ args2.push("-e", "ssh", `${target}:${remotePath}`, localPath);
11946
+ const child = spawn5("rsync", args2, { stdio: "inherit", env: EXEC_ENV });
11947
+ child.on("error", reject);
11948
+ child.on("exit", (code) => {
11949
+ if (code === 0) resolve12();
11950
+ else reject(new Error(`rsync exited ${code} for ${remotePath}`));
11951
+ });
11952
+ });
11953
+ }
11954
+
11705
11955
  // src/cli/commands/cloud.ts
11706
11956
  init_shell();
11707
11957
  function resolve11(raw) {
@@ -11742,6 +11992,21 @@ function registerCloud(program2) {
11742
11992
  const { provider, repo } = resolve11(raw);
11743
11993
  cloudStatus(provider, repo);
11744
11994
  });
11995
+ cloud.command("handoff <session-id>").description("Hand off a live session to the cloud box. Queues at next quiesce; --force interrupts now.").option("--provider <name>", "Cloud provider (default: auto-pick).").option("--name <repo>", "Override the repo name on the box.").option("--force", "Interrupt in-flight orchestrator/agents now instead of queueing.").option("--cancel", "Cancel a queued handoff before it fires.").option("--wait", "Block until the handoff completes (or fails).").action(async (sessionId, raw) => {
11996
+ if (raw.cancel) {
11997
+ await cloudHandoffCancel(sessionId);
11998
+ return;
11999
+ }
12000
+ const provider = pickProvider(raw.provider);
12001
+ const repo = raw.name ? raw.name : inferRepoName();
12002
+ if (!validateRepoName(repo)) {
12003
+ throw new Error(`Invalid --name "${repo}": must not contain '/' '\\' or '..'.`);
12004
+ }
12005
+ await cloudHandoff(sessionId, { provider, repo, force: raw.force === true, wait: raw.wait === true });
12006
+ });
12007
+ cloud.command("reclaim <session-id>").description("Pull a handed-off session back from the cloud box and resume locally.").option("--provider <name>", "Override the cloud provider (default: read from session.handoff).").option("--force", "Force the box-side session to stop now instead of waiting for quiesce.").action(async (sessionId, raw) => {
12008
+ await cloudReclaim(sessionId, { providerOverride: raw.provider, force: raw.force === true });
12009
+ });
11745
12010
  }
11746
12011
 
11747
12012
  // src/cli/commands/notify.ts
@@ -11759,7 +12024,7 @@ function attachNotify(diagnostic2) {
11759
12024
  // src/cli/commands/tmux-sessions.ts
11760
12025
  init_paths();
11761
12026
  import { execSync as execSync18 } from "child_process";
11762
- import { readFileSync as readFileSync34, existsSync as existsSync32 } from "fs";
12027
+ import { readFileSync as readFileSync35, existsSync as existsSync33 } from "fs";
11763
12028
  var DOT_MAP = {
11764
12029
  "orchestrator:processing": { icon: "\u25CF", color: "#d4ad6a" },
11765
12030
  "orchestrator:idle": { icon: "\u25CF", color: "#d47766" },
@@ -11770,9 +12035,9 @@ var DOT_MAP = {
11770
12035
  };
11771
12036
  function readManifest() {
11772
12037
  const p = sessionsManifestPath();
11773
- if (!existsSync32(p)) return null;
12038
+ if (!existsSync33(p)) return null;
11774
12039
  try {
11775
- return JSON.parse(readFileSync34(p, "utf-8"));
12040
+ return JSON.parse(readFileSync35(p, "utf-8"));
11776
12041
  } catch {
11777
12042
  return null;
11778
12043
  }
@@ -11818,7 +12083,7 @@ if (nodeVersion < 22) {
11818
12083
  var program = new Command();
11819
12084
  program.name("sis").description("tmux-integrated orchestration daemon for Claude Code").version(
11820
12085
  JSON.parse(
11821
- readFileSync35(join31(dirname13(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
12086
+ readFileSync36(join32(dirname13(fileURLToPath5(import.meta.url)), "..", "package.json"), "utf-8")
11822
12087
  ).version
11823
12088
  );
11824
12089
  program.configureHelp({
@@ -11863,6 +12128,7 @@ registerSetupKeybind(admin);
11863
12128
  registerCheckKeybinds(admin);
11864
12129
  registerCheckStatusbar(admin);
11865
12130
  registerHomeInit(admin);
12131
+ registerQuiesce(admin);
11866
12132
  registerDoctor(admin);
11867
12133
  registerInit(admin);
11868
12134
  registerUninstall(admin);
@@ -11892,7 +12158,7 @@ Run 'sis admin getting-started' for a complete usage guide.
11892
12158
  var args = process.argv.slice(2);
11893
12159
  var firstArg = args[0];
11894
12160
  var skipWelcome = ["admin", "help", "--help", "-h", "--version", "-V"];
11895
- if (!existsSync33(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
12161
+ if (!existsSync34(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
11896
12162
  mkdirSync17(globalDir(), { recursive: true });
11897
12163
  console.log("");
11898
12164
  console.log(" Welcome to Sisyphus. Run 'sis admin setup' to get started.");