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 +292 -26
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +2726 -1410
- package/dist/daemon.js.map +1 -1
- package/dist/templates/orchestrator-base.md +8 -12
- package/dist/templates/orchestrator-discovery.md +1 -1
- package/dist/templates/orchestrator-planning.md +1 -1
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
- package/templates/orchestrator-base.md +8 -12
- package/templates/orchestrator-discovery.md +1 -1
- package/templates/orchestrator-planning.md +1 -1
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
|
|
672
|
-
import { dirname as dirname13, join as
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
12038
|
+
if (!existsSync33(p)) return null;
|
|
11774
12039
|
try {
|
|
11775
|
-
return JSON.parse(
|
|
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
|
-
|
|
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 (!
|
|
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.");
|