switchroom 0.20.7 → 0.20.8
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/agent-scheduler/index.js +105 -11
- package/dist/auth-broker/index.js +68 -7
- package/dist/cli/notion-write-pretool.mjs +67 -6
- package/dist/cli/switchroom.js +384 -28
- package/dist/host-control/main.js +69 -8
- package/dist/vault/approvals/kernel-server.js +68 -7
- package/dist/vault/broker/server.js +68 -7
- package/package.json +1 -1
- package/telegram-plugin/bridge/ipc-client.ts +17 -1
- package/telegram-plugin/dist/bridge/bridge.js +5 -2
- package/telegram-plugin/dist/gateway/gateway.js +237 -122
- package/telegram-plugin/dist/server.js +5 -2
- package/telegram-plugin/gateway/boot-reason.ts +61 -0
- package/telegram-plugin/gateway/cron-session.ts +66 -0
- package/telegram-plugin/gateway/gateway.ts +28 -30
- package/telegram-plugin/gateway/narrative-lane.ts +21 -1
- package/telegram-plugin/gateway/represent-delivery-guard.ts +33 -2
- package/telegram-plugin/gateway/stream-render.ts +11 -2
- package/telegram-plugin/tests/boot-card-reason.test.ts +88 -0
- package/telegram-plugin/tests/cron-bridge-drain-spool-ack.test.ts +150 -0
- package/telegram-plugin/tests/ipc-client-reconnect-rejection.test.ts +70 -0
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +86 -0
- package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
- package/telegram-plugin/tests/represent-guard.test.ts +45 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +83 -0
- package/telegram-plugin/turn-flush-safety.ts +79 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.20.
|
|
2123
|
+
var VERSION = "0.20.8", COMMIT_SHA = "a6efc10f";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -14641,6 +14641,20 @@ var init_overlay_schema = __esm(() => {
|
|
|
14641
14641
|
// src/config/overlay-loader.ts
|
|
14642
14642
|
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "node:fs";
|
|
14643
14643
|
import { basename, resolve as resolve2 } from "node:path";
|
|
14644
|
+
function recordReadFailure(agentCfg, failure) {
|
|
14645
|
+
const node = agentCfg;
|
|
14646
|
+
const existing = node[OVERLAY_READ_FAILURES];
|
|
14647
|
+
if (Array.isArray(existing)) {
|
|
14648
|
+
existing.push(failure);
|
|
14649
|
+
return;
|
|
14650
|
+
}
|
|
14651
|
+
Object.defineProperty(agentCfg, OVERLAY_READ_FAILURES, {
|
|
14652
|
+
value: [failure],
|
|
14653
|
+
enumerable: false,
|
|
14654
|
+
configurable: true,
|
|
14655
|
+
writable: false
|
|
14656
|
+
});
|
|
14657
|
+
}
|
|
14644
14658
|
function deriveOverlayTitle(raw, fileName) {
|
|
14645
14659
|
const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
|
|
14646
14660
|
if (titleFromComment)
|
|
@@ -14650,17 +14664,39 @@ function deriveOverlayTitle(raw, fileName) {
|
|
|
14650
14664
|
return;
|
|
14651
14665
|
return base.length > 0 ? base : undefined;
|
|
14652
14666
|
}
|
|
14667
|
+
function readOverlayFile(agentName, file, agentCfg, warnings) {
|
|
14668
|
+
try {
|
|
14669
|
+
return readFileSync2(file, "utf-8");
|
|
14670
|
+
} catch (err) {
|
|
14671
|
+
const code = err.code;
|
|
14672
|
+
if (code === "ENOENT")
|
|
14673
|
+
return;
|
|
14674
|
+
const w = {
|
|
14675
|
+
agent: agentName,
|
|
14676
|
+
file,
|
|
14677
|
+
reason: `read error: ${err.message}`,
|
|
14678
|
+
code: code ?? "EUNKNOWN"
|
|
14679
|
+
};
|
|
14680
|
+
recordReadFailure(agentCfg, { file, code: w.code });
|
|
14681
|
+
warnings.push(w);
|
|
14682
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
|
|
14683
|
+
return;
|
|
14684
|
+
}
|
|
14685
|
+
}
|
|
14653
14686
|
function overlayDirFor(agentName, subdir) {
|
|
14654
14687
|
const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
|
|
14655
14688
|
return resolve2(base);
|
|
14656
14689
|
}
|
|
14657
|
-
function listYamlFiles(dir) {
|
|
14690
|
+
function listYamlFiles(dir, onUnreadableDir) {
|
|
14658
14691
|
if (!existsSync3(dir))
|
|
14659
14692
|
return [];
|
|
14660
14693
|
let entries;
|
|
14661
14694
|
try {
|
|
14662
14695
|
entries = readdirSync(dir);
|
|
14663
|
-
} catch {
|
|
14696
|
+
} catch (err) {
|
|
14697
|
+
const code = err.code;
|
|
14698
|
+
if (code !== "ENOENT")
|
|
14699
|
+
onUnreadableDir?.(code ?? "EUNKNOWN");
|
|
14664
14700
|
return [];
|
|
14665
14701
|
}
|
|
14666
14702
|
const out = [];
|
|
@@ -14698,12 +14734,24 @@ function applyAgentOverlays(config) {
|
|
|
14698
14734
|
for (const [agentName, agentCfg] of Object.entries(agents)) {
|
|
14699
14735
|
try {
|
|
14700
14736
|
const scheduleDir = overlayDirFor(agentName, "schedule.d");
|
|
14701
|
-
const files = listYamlFiles(scheduleDir)
|
|
14737
|
+
const files = listYamlFiles(scheduleDir, (code) => {
|
|
14738
|
+
const w = {
|
|
14739
|
+
agent: agentName,
|
|
14740
|
+
file: scheduleDir,
|
|
14741
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
14742
|
+
code
|
|
14743
|
+
};
|
|
14744
|
+
recordReadFailure(agentCfg, { file: scheduleDir, code });
|
|
14745
|
+
warnings.push(w);
|
|
14746
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${scheduleDir}': ${w.reason}`);
|
|
14747
|
+
});
|
|
14702
14748
|
if (files.length > 0) {
|
|
14703
14749
|
const merged = [...agentCfg.schedule ?? []];
|
|
14704
14750
|
for (const file of files) {
|
|
14751
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings);
|
|
14752
|
+
if (raw === undefined)
|
|
14753
|
+
continue;
|
|
14705
14754
|
try {
|
|
14706
|
-
const raw = readFileSync2(file, "utf-8");
|
|
14707
14755
|
const parsed = import_yaml.parse(raw);
|
|
14708
14756
|
const doc = OverlayDocSchema.parse(parsed);
|
|
14709
14757
|
const title = deriveOverlayTitle(raw, basename(file));
|
|
@@ -14738,13 +14786,25 @@ function applyAgentOverlays(config) {
|
|
|
14738
14786
|
}
|
|
14739
14787
|
try {
|
|
14740
14788
|
const skillsDir = overlayDirFor(agentName, "skills.d");
|
|
14741
|
-
const skillFiles = listYamlFiles(skillsDir)
|
|
14789
|
+
const skillFiles = listYamlFiles(skillsDir, (code) => {
|
|
14790
|
+
const w = {
|
|
14791
|
+
agent: agentName,
|
|
14792
|
+
file: skillsDir,
|
|
14793
|
+
reason: `read error: cannot list overlay directory (${code})`,
|
|
14794
|
+
code
|
|
14795
|
+
};
|
|
14796
|
+
recordReadFailure(agentCfg, { file: skillsDir, code });
|
|
14797
|
+
warnings.push(w);
|
|
14798
|
+
console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${skillsDir}': ${w.reason}`);
|
|
14799
|
+
});
|
|
14742
14800
|
if (skillFiles.length === 0) {} else {
|
|
14743
14801
|
const merged = [...agentCfg.skills ?? []];
|
|
14744
14802
|
const seen = new Set(merged);
|
|
14745
14803
|
for (const file of skillFiles) {
|
|
14804
|
+
const raw = readOverlayFile(agentName, file, agentCfg, warnings);
|
|
14805
|
+
if (raw === undefined)
|
|
14806
|
+
continue;
|
|
14746
14807
|
try {
|
|
14747
|
-
const raw = readFileSync2(file, "utf-8");
|
|
14748
14808
|
const parsed = import_yaml.parse(raw);
|
|
14749
14809
|
const doc = OverlayDocSchema.parse(parsed);
|
|
14750
14810
|
for (const skillName of doc.skills ?? []) {
|
|
@@ -14772,7 +14832,7 @@ function applyAgentOverlays(config) {
|
|
|
14772
14832
|
}
|
|
14773
14833
|
return { config, warnings };
|
|
14774
14834
|
}
|
|
14775
|
-
var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE;
|
|
14835
|
+
var import_yaml, OVERLAY_SOURCE, OVERLAY_TITLE, OVERLAY_READ_FAILURES;
|
|
14776
14836
|
var init_overlay_loader = __esm(() => {
|
|
14777
14837
|
init_zod();
|
|
14778
14838
|
init_overlay_schema();
|
|
@@ -14780,6 +14840,7 @@ var init_overlay_loader = __esm(() => {
|
|
|
14780
14840
|
import_yaml = __toESM(require_dist(), 1);
|
|
14781
14841
|
OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
|
|
14782
14842
|
OVERLAY_TITLE = Symbol.for("switchroom.config.overlay-title");
|
|
14843
|
+
OVERLAY_READ_FAILURES = Symbol.for("switchroom.config.overlay-read-failures");
|
|
14783
14844
|
});
|
|
14784
14845
|
|
|
14785
14846
|
// src/config/merge.ts
|
|
@@ -20448,6 +20509,15 @@ var init_agent_uid = __esm(() => {
|
|
|
20448
20509
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
20449
20510
|
import { existsSync as existsSync9, lstatSync as lstatSync2, readdirSync as readdirSync3, statSync as statSync6 } from "node:fs";
|
|
20450
20511
|
import { join as join5 } from "node:path";
|
|
20512
|
+
function isChownVanishedRaceOnly(err) {
|
|
20513
|
+
const e = err;
|
|
20514
|
+
const raw = e?.stderr == null ? "" : e.stderr.toString();
|
|
20515
|
+
const lines = raw.split(`
|
|
20516
|
+
`).map((l) => l.trim()).filter(Boolean);
|
|
20517
|
+
if (lines.length === 0)
|
|
20518
|
+
return false;
|
|
20519
|
+
return lines.every((l) => /: No such file or directory$/.test(l));
|
|
20520
|
+
}
|
|
20451
20521
|
function scopedAssertCandidates(agentDir) {
|
|
20452
20522
|
return [
|
|
20453
20523
|
join5(agentDir, ".claude", "settings.json"),
|
|
@@ -20518,7 +20588,9 @@ var init_agent_owned_tree = __esm(() => {
|
|
|
20518
20588
|
SCOPED_RECURSIVE_SUBDIRS = [
|
|
20519
20589
|
".claude",
|
|
20520
20590
|
".claude-cron",
|
|
20521
|
-
"telegram"
|
|
20591
|
+
"telegram",
|
|
20592
|
+
"schedule.d",
|
|
20593
|
+
"skills.d"
|
|
20522
20594
|
];
|
|
20523
20595
|
SCOPED_TOP_LEVEL_CRITICAL = [
|
|
20524
20596
|
"start.sh",
|
|
@@ -20530,15 +20602,26 @@ var init_agent_owned_tree = __esm(() => {
|
|
|
20530
20602
|
ownershipRuntime = {
|
|
20531
20603
|
geteuid: () => process.geteuid?.(),
|
|
20532
20604
|
chownTree: (uid, gid, rootDir) => {
|
|
20533
|
-
|
|
20534
|
-
|
|
20535
|
-
|
|
20605
|
+
try {
|
|
20606
|
+
execFileSync3("chown", ["-h", "-R", "--", `${uid}:${gid}`, rootDir], {
|
|
20607
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
20608
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
20609
|
+
maxBuffer: 64 * 1024 * 1024
|
|
20610
|
+
});
|
|
20611
|
+
} catch (err) {
|
|
20612
|
+
const e = err;
|
|
20613
|
+
if (e?.signal != null || typeof e?.status !== "number")
|
|
20614
|
+
throw err;
|
|
20615
|
+
if (!isChownVanishedRaceOnly(err))
|
|
20616
|
+
throw err;
|
|
20617
|
+
}
|
|
20536
20618
|
},
|
|
20537
20619
|
chownShallow: (uid, gid, paths) => {
|
|
20538
20620
|
if (paths.length === 0)
|
|
20539
20621
|
return;
|
|
20540
|
-
execFileSync3("chown", ["-h", `${uid}:${gid}`, ...paths], {
|
|
20541
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
20622
|
+
execFileSync3("chown", ["-h", "--", `${uid}:${gid}`, ...paths], {
|
|
20623
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
20624
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
20542
20625
|
});
|
|
20543
20626
|
},
|
|
20544
20627
|
scopedSweepEnabled: () => process.env[SCOPED_SWEEP_ENV] === "1",
|
|
@@ -46933,7 +47016,6 @@ var ManifestSchema, WARN_ONLY_COMPONENTS;
|
|
|
46933
47016
|
var init_manifest = __esm(() => {
|
|
46934
47017
|
init_zod();
|
|
46935
47018
|
ManifestSchema = exports_external.object({
|
|
46936
|
-
switchroom_version: exports_external.string().min(1),
|
|
46937
47019
|
tested_at: exports_external.string().min(1),
|
|
46938
47020
|
runtime: exports_external.object({
|
|
46939
47021
|
bun: exports_external.string().min(1),
|
|
@@ -65426,7 +65508,7 @@ async function checkManifestDrift(probers) {
|
|
|
65426
65508
|
{
|
|
65427
65509
|
name: "dependency manifest",
|
|
65428
65510
|
status: "ok",
|
|
65429
|
-
detail: `all versions match (
|
|
65511
|
+
detail: `all versions match (switchroom ${SWITCHROOM_VERSION})`
|
|
65430
65512
|
}
|
|
65431
65513
|
];
|
|
65432
65514
|
}
|
|
@@ -65812,6 +65894,7 @@ var init_doctor = __esm(() => {
|
|
|
65812
65894
|
init_accounts();
|
|
65813
65895
|
init_schema();
|
|
65814
65896
|
init_manifest();
|
|
65897
|
+
init_resolve_version();
|
|
65815
65898
|
init_hindsight2();
|
|
65816
65899
|
init_hindsight();
|
|
65817
65900
|
init_host_capabilities();
|
|
@@ -85383,6 +85466,7 @@ Scaffolding agent: ${name}
|
|
|
85383
85466
|
}
|
|
85384
85467
|
} catch (err) {
|
|
85385
85468
|
console.error(source_default.red(`Failed to restart ${n}: ${err.message}`));
|
|
85469
|
+
process.exitCode = 1;
|
|
85386
85470
|
}
|
|
85387
85471
|
}
|
|
85388
85472
|
if (sawAbort) {
|
|
@@ -113945,6 +114029,16 @@ async function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
113945
114029
|
timedOut: true
|
|
113946
114030
|
});
|
|
113947
114031
|
}
|
|
114032
|
+
if (restartRes.status !== 0) {
|
|
114033
|
+
const gotAfterExit = deps.probeVersion(step.agent);
|
|
114034
|
+
deps.log(` \u2717 ${step.agent} \u2192 restart exited ${restartRes.status} ` + `(probe says ${gotAfterExit ?? "<unreachable>"}) \u2014 STOPPING`);
|
|
114035
|
+
emit(isCanary ? { phase: "canary-fail", target, agent: step.agent, n: agentIndex, m: totalAgents } : { phase: "agent-done", target, agent: step.agent, n: agentIndex, m: totalAgents });
|
|
114036
|
+
return fail4({
|
|
114037
|
+
failedStep: "restart-agent",
|
|
114038
|
+
failedAgent: step.agent,
|
|
114039
|
+
got: gotAfterExit
|
|
114040
|
+
});
|
|
114041
|
+
}
|
|
113948
114042
|
const got = deps.probeVersion(step.agent);
|
|
113949
114043
|
if (got === null || normalizeVersion(got) !== targetNorm) {
|
|
113950
114044
|
deps.log(` \u2717 ${step.agent} \u2192 ${got ?? "<unreachable>"} (expected ${target}) \u2014 STOPPING`);
|
|
@@ -114635,6 +114729,7 @@ function registerRestartCommand(program3) {
|
|
|
114635
114729
|
}
|
|
114636
114730
|
} catch (err) {
|
|
114637
114731
|
console.error(source_default.red(` ${name}: restart failed: ${err.message}`));
|
|
114732
|
+
process.exitCode = 1;
|
|
114638
114733
|
}
|
|
114639
114734
|
}
|
|
114640
114735
|
console.log();
|
|
@@ -114645,6 +114740,7 @@ function registerRestartCommand(program3) {
|
|
|
114645
114740
|
// src/cli/versions.ts
|
|
114646
114741
|
init_source();
|
|
114647
114742
|
init_manifest();
|
|
114743
|
+
init_resolve_version();
|
|
114648
114744
|
function formatRow(component, declared, installed, isDrift, warnOnly) {
|
|
114649
114745
|
const installedStr = installed ?? source_default.red("(not installed)");
|
|
114650
114746
|
if (!isDrift) {
|
|
@@ -114674,7 +114770,7 @@ function registerVersionsCommand(program3) {
|
|
|
114674
114770
|
const driftMap = new Map(report.drift.map((d) => [d.component, d]));
|
|
114675
114771
|
console.log(source_default.bold(`
|
|
114676
114772
|
Dependency manifest`));
|
|
114677
|
-
console.log(source_default.gray(` switchroom ${
|
|
114773
|
+
console.log(source_default.gray(` switchroom ${SWITCHROOM_VERSION} \u00b7 tested ${manifest.tested_at}`));
|
|
114678
114774
|
console.log();
|
|
114679
114775
|
const bunDrift = driftMap.get("bun");
|
|
114680
114776
|
console.log(formatRow("bun", manifest.runtime.bun, bunDrift ? bunDrift.installed : manifest.runtime.bun, !!bunDrift, false));
|
|
@@ -117367,6 +117463,40 @@ function isEphemeralPath(path10) {
|
|
|
117367
117463
|
}
|
|
117368
117464
|
return false;
|
|
117369
117465
|
}
|
|
117466
|
+
function pushStateFrom(opts) {
|
|
117467
|
+
if (opts.detached)
|
|
117468
|
+
return "detached";
|
|
117469
|
+
if (!opts.upstream)
|
|
117470
|
+
return "no-upstream";
|
|
117471
|
+
if (opts.aheadCount < 0)
|
|
117472
|
+
return "error";
|
|
117473
|
+
if (opts.aheadCount > 0)
|
|
117474
|
+
return "unpushed";
|
|
117475
|
+
return "pushed";
|
|
117476
|
+
}
|
|
117477
|
+
function isStablePerRepoBranch(branch) {
|
|
117478
|
+
return !!branch && /^agent\/[^/]+\/main$/.test(branch);
|
|
117479
|
+
}
|
|
117480
|
+
function classifyTaskTree(i2) {
|
|
117481
|
+
if (i2.isRegistryClaimed || i2.isStablePerRepoTree || i2.isEphemeralPath) {
|
|
117482
|
+
return "skip-protected";
|
|
117483
|
+
}
|
|
117484
|
+
if (i2.inUse === "unavailable")
|
|
117485
|
+
return "skip-probe-unavailable";
|
|
117486
|
+
if (i2.inUse === "in-use")
|
|
117487
|
+
return "skip-in-use";
|
|
117488
|
+
if (!i2.clean)
|
|
117489
|
+
return "skip-dirty";
|
|
117490
|
+
if (i2.pushed !== "pushed")
|
|
117491
|
+
return "skip-unpushed";
|
|
117492
|
+
if (!i2.idle)
|
|
117493
|
+
return "skip-active";
|
|
117494
|
+
if (i2.prSignal === "error")
|
|
117495
|
+
return "skip-unknown";
|
|
117496
|
+
if (i2.prSignal !== "merged" && i2.prSignal !== "closed")
|
|
117497
|
+
return "skip-unmerged";
|
|
117498
|
+
return "reap";
|
|
117499
|
+
}
|
|
117370
117500
|
var defaultExec3 = (file, args, cwd) => execFileSync31(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
117371
117501
|
function defaultPrSignal(repo, branch, exec) {
|
|
117372
117502
|
try {
|
|
@@ -117399,10 +117529,28 @@ function defaultPrSignal(repo, branch, exec) {
|
|
|
117399
117529
|
return "error";
|
|
117400
117530
|
}
|
|
117401
117531
|
}
|
|
117532
|
+
function defaultNewestTrackedMtimeMs(dir, exec) {
|
|
117533
|
+
let out;
|
|
117534
|
+
try {
|
|
117535
|
+
out = exec("git", ["-C", dir, "ls-files", "-z"]);
|
|
117536
|
+
} catch {
|
|
117537
|
+
return Date.now();
|
|
117538
|
+
}
|
|
117539
|
+
const files = out.split("\x00").filter(Boolean);
|
|
117540
|
+
let newest = 0;
|
|
117541
|
+
for (const f of files) {
|
|
117542
|
+
try {
|
|
117543
|
+
const m = statSync52(join102(dir, f)).mtimeMs;
|
|
117544
|
+
if (m > newest)
|
|
117545
|
+
newest = m;
|
|
117546
|
+
} catch {}
|
|
117547
|
+
}
|
|
117548
|
+
return newest || Date.now();
|
|
117549
|
+
}
|
|
117402
117550
|
function trashRoot() {
|
|
117403
117551
|
return resolve58(process.env.SWITCHROOM_WORKTREE_TRASH ?? join102(homedir55(), ".switchroom", "worktree-gc-trash"));
|
|
117404
117552
|
}
|
|
117405
|
-
function planGc(roots, deps = {}) {
|
|
117553
|
+
function planGc(roots, deps = {}, taskTreeRoots = []) {
|
|
117406
117554
|
const exists = deps.existsSync ?? existsSync100;
|
|
117407
117555
|
const readDir = deps.readDir ?? ((p) => readdirSync33(p));
|
|
117408
117556
|
const readFile4 = deps.readFile ?? ((p) => readFileSync90(p, "utf8"));
|
|
@@ -117411,6 +117559,11 @@ function planGc(roots, deps = {}) {
|
|
|
117411
117559
|
const prSignal = deps.prSignal ?? ((repo, branch) => defaultPrSignal(repo, branch, exec));
|
|
117412
117560
|
const stamp = deps.dateStamp ?? "undated";
|
|
117413
117561
|
const trash = join102(trashRoot(), stamp);
|
|
117562
|
+
const probeInUse = deps.probeInUse ?? probePathInUse;
|
|
117563
|
+
const newestMtime = deps.newestTrackedMtimeMs ?? ((dir) => defaultNewestTrackedMtimeMs(dir, exec));
|
|
117564
|
+
const idleDays = deps.idleDays ?? 14;
|
|
117565
|
+
const nowMs = deps.nowMs ?? Date.now();
|
|
117566
|
+
const escapeHatch = deps.escapeHatch ?? false;
|
|
117414
117567
|
let claimed;
|
|
117415
117568
|
try {
|
|
117416
117569
|
claimed = new Set(listRecords().map((r) => resolve58(r.path)));
|
|
@@ -117522,11 +117675,137 @@ function planGc(roots, deps = {}) {
|
|
|
117522
117675
|
registered.push({ path: wt.path, branch: wt.branch, verdict, prSignal: sig, repo });
|
|
117523
117676
|
}
|
|
117524
117677
|
}
|
|
117678
|
+
const taskTrees = [];
|
|
117679
|
+
for (const root of taskTreeRoots) {
|
|
117680
|
+
if (!exists(root))
|
|
117681
|
+
continue;
|
|
117682
|
+
let entries;
|
|
117683
|
+
try {
|
|
117684
|
+
entries = readDir(root);
|
|
117685
|
+
} catch {
|
|
117686
|
+
continue;
|
|
117687
|
+
}
|
|
117688
|
+
for (const name of entries) {
|
|
117689
|
+
const dir = join102(root, name);
|
|
117690
|
+
if (isEphemeralPath(dir))
|
|
117691
|
+
continue;
|
|
117692
|
+
const dotGit = join102(dir, ".git");
|
|
117693
|
+
if (!exists(dotGit))
|
|
117694
|
+
continue;
|
|
117695
|
+
let shape;
|
|
117696
|
+
try {
|
|
117697
|
+
shape = stat3(dotGit).isDirectory() ? "clone" : "worktree";
|
|
117698
|
+
} catch {
|
|
117699
|
+
continue;
|
|
117700
|
+
}
|
|
117701
|
+
let remoteOk = false;
|
|
117702
|
+
try {
|
|
117703
|
+
remoteOk = isSwitchroomRemote(exec("git", ["-C", dir, "remote", "get-url", "origin"]));
|
|
117704
|
+
} catch {
|
|
117705
|
+
remoteOk = false;
|
|
117706
|
+
}
|
|
117707
|
+
if (!remoteOk) {
|
|
117708
|
+
skipped.push({ dir, reason: "not a switchroom task tree" });
|
|
117709
|
+
continue;
|
|
117710
|
+
}
|
|
117711
|
+
let ownerRepo = null;
|
|
117712
|
+
if (shape === "worktree") {
|
|
117713
|
+
try {
|
|
117714
|
+
const ptr = parseGitdirPointer(readFile4(dotGit));
|
|
117715
|
+
if (ptr)
|
|
117716
|
+
ownerRepo = repoRootFromWorktreeGitdir(ptr);
|
|
117717
|
+
} catch {
|
|
117718
|
+
ownerRepo = null;
|
|
117719
|
+
}
|
|
117720
|
+
}
|
|
117721
|
+
let branch = null;
|
|
117722
|
+
let detached = false;
|
|
117723
|
+
try {
|
|
117724
|
+
const b = exec("git", ["-C", dir, "rev-parse", "--abbrev-ref", "HEAD"]).trim();
|
|
117725
|
+
if (b === "HEAD")
|
|
117726
|
+
detached = true;
|
|
117727
|
+
else
|
|
117728
|
+
branch = b;
|
|
117729
|
+
} catch {
|
|
117730
|
+
detached = true;
|
|
117731
|
+
}
|
|
117732
|
+
const claimedWt = claimed.has(resolve58(dir));
|
|
117733
|
+
const stableTree = isStablePerRepoBranch(branch);
|
|
117734
|
+
const ephemeral = isEphemeralPath(dir);
|
|
117735
|
+
const protectedTree = claimedWt || stableTree || ephemeral;
|
|
117736
|
+
let clean3 = false;
|
|
117737
|
+
try {
|
|
117738
|
+
clean3 = isEffectivelyClean(exec("git", ["-C", dir, "status", "--porcelain"]).split(`
|
|
117739
|
+
`));
|
|
117740
|
+
} catch {
|
|
117741
|
+
clean3 = false;
|
|
117742
|
+
}
|
|
117743
|
+
let upstream = null;
|
|
117744
|
+
let aheadCount = 0;
|
|
117745
|
+
if (!detached) {
|
|
117746
|
+
try {
|
|
117747
|
+
upstream = exec("git", ["-C", dir, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]).trim() || null;
|
|
117748
|
+
} catch {
|
|
117749
|
+
upstream = null;
|
|
117750
|
+
}
|
|
117751
|
+
if (upstream) {
|
|
117752
|
+
try {
|
|
117753
|
+
aheadCount = parseInt(exec("git", ["-C", dir, "rev-list", "--count", "@{upstream}..HEAD"]).trim(), 10);
|
|
117754
|
+
if (!Number.isFinite(aheadCount))
|
|
117755
|
+
aheadCount = -1;
|
|
117756
|
+
} catch {
|
|
117757
|
+
aheadCount = -1;
|
|
117758
|
+
}
|
|
117759
|
+
}
|
|
117760
|
+
}
|
|
117761
|
+
const pushed = pushStateFrom({ detached, upstream, aheadCount });
|
|
117762
|
+
let idle = false;
|
|
117763
|
+
try {
|
|
117764
|
+
idle = (nowMs - newestMtime(dir)) / 86400000 >= idleDays;
|
|
117765
|
+
} catch {
|
|
117766
|
+
idle = false;
|
|
117767
|
+
}
|
|
117768
|
+
let inUse = "unavailable";
|
|
117769
|
+
let sig = "error";
|
|
117770
|
+
if (!protectedTree) {
|
|
117771
|
+
inUse = probeInUse(dir);
|
|
117772
|
+
if (inUse === "free" && clean3 && pushed === "pushed" && idle && branch) {
|
|
117773
|
+
sig = prSignal(dir, branch);
|
|
117774
|
+
}
|
|
117775
|
+
}
|
|
117776
|
+
const verdict = classifyTaskTree({
|
|
117777
|
+
isRegistryClaimed: claimedWt,
|
|
117778
|
+
isStablePerRepoTree: stableTree,
|
|
117779
|
+
isEphemeralPath: ephemeral,
|
|
117780
|
+
clean: clean3,
|
|
117781
|
+
pushed,
|
|
117782
|
+
prSignal: sig,
|
|
117783
|
+
idle,
|
|
117784
|
+
inUse
|
|
117785
|
+
});
|
|
117786
|
+
const willAct = verdict === "reap" || escapeHatch && idle && (verdict === "skip-dirty" || verdict === "skip-unpushed");
|
|
117787
|
+
if (willAct && ownerRepo)
|
|
117788
|
+
reposToPrune.add(ownerRepo);
|
|
117789
|
+
taskTrees.push({
|
|
117790
|
+
dir,
|
|
117791
|
+
shape,
|
|
117792
|
+
ownerRepo,
|
|
117793
|
+
branch,
|
|
117794
|
+
verdict,
|
|
117795
|
+
prSignal: sig,
|
|
117796
|
+
idle,
|
|
117797
|
+
dest: join102(trash, name),
|
|
117798
|
+
willAct
|
|
117799
|
+
});
|
|
117800
|
+
}
|
|
117801
|
+
}
|
|
117525
117802
|
return {
|
|
117526
117803
|
roots,
|
|
117804
|
+
taskTreeRoots,
|
|
117527
117805
|
trashDir: trash,
|
|
117528
117806
|
orphans,
|
|
117529
117807
|
registered,
|
|
117808
|
+
taskTrees,
|
|
117530
117809
|
reposToPrune: [...reposToPrune],
|
|
117531
117810
|
skipped
|
|
117532
117811
|
};
|
|
@@ -117542,7 +117821,8 @@ function applyGc(plan, deps = {}) {
|
|
|
117542
117821
|
}
|
|
117543
117822
|
});
|
|
117544
117823
|
const res = { quarantined: [], removed: [], branchesDeleted: [], pruned: [], errors: [] };
|
|
117545
|
-
|
|
117824
|
+
const taskTreeActions = (plan.taskTrees ?? []).filter((t) => t.willAct);
|
|
117825
|
+
if (plan.orphans.length || taskTreeActions.length)
|
|
117546
117826
|
mkdirp(plan.trashDir);
|
|
117547
117827
|
for (const o of plan.orphans) {
|
|
117548
117828
|
try {
|
|
@@ -117552,6 +117832,14 @@ function applyGc(plan, deps = {}) {
|
|
|
117552
117832
|
res.errors.push(`quarantine ${o.dir}: ${e.message}`);
|
|
117553
117833
|
}
|
|
117554
117834
|
}
|
|
117835
|
+
for (const t of taskTreeActions) {
|
|
117836
|
+
try {
|
|
117837
|
+
move(t.dir, t.dest);
|
|
117838
|
+
res.quarantined.push(t.dir);
|
|
117839
|
+
} catch (e) {
|
|
117840
|
+
res.errors.push(`quarantine ${t.dir}: ${e.message}`);
|
|
117841
|
+
}
|
|
117842
|
+
}
|
|
117555
117843
|
for (const r of plan.registered) {
|
|
117556
117844
|
if (r.verdict !== "remove")
|
|
117557
117845
|
continue;
|
|
@@ -117623,6 +117911,24 @@ function purgeTrash(paths) {
|
|
|
117623
117911
|
function defaultRoots() {
|
|
117624
117912
|
return [join102(homedir55(), "code")];
|
|
117625
117913
|
}
|
|
117914
|
+
function defaultTaskTreeRoots() {
|
|
117915
|
+
const agentsDir = join102(homedir55(), ".switchroom", "agents");
|
|
117916
|
+
let names;
|
|
117917
|
+
try {
|
|
117918
|
+
names = readdirSync33(agentsDir);
|
|
117919
|
+
} catch {
|
|
117920
|
+
return [];
|
|
117921
|
+
}
|
|
117922
|
+
const roots = [];
|
|
117923
|
+
for (const name of names.sort()) {
|
|
117924
|
+
for (const sub of ["home/work", "home/workspace"]) {
|
|
117925
|
+
const r = join102(agentsDir, name, sub);
|
|
117926
|
+
if (existsSync100(r))
|
|
117927
|
+
roots.push(r);
|
|
117928
|
+
}
|
|
117929
|
+
}
|
|
117930
|
+
return roots;
|
|
117931
|
+
}
|
|
117626
117932
|
|
|
117627
117933
|
// src/cli/worktree.ts
|
|
117628
117934
|
function registerWorktreeCommand(program3) {
|
|
@@ -117756,10 +118062,13 @@ Skipped ${result.skipped.length} stale worktree(s) (kept for safety):`));
|
|
|
117756
118062
|
}
|
|
117757
118063
|
}
|
|
117758
118064
|
});
|
|
117759
|
-
worktree.command("gc").description(`Reclaim dev worktrees that outlived their PR.
|
|
118065
|
+
worktree.command("gc").description(`Reclaim dev worktrees that outlived their PR, and per-agent task trees.
|
|
117760
118066
|
` + `Dry-run by default; pass --yes to act. Quarantines orphaned worktree
|
|
117761
118067
|
` + `directories (move, not delete) and removes registered worktrees whose
|
|
117762
|
-
` +
|
|
118068
|
+
` + `PR is MERGED and whose tree is clean. Also sweeps per-agent home/work
|
|
118069
|
+
` + `task trees (all shapes) that are clean + pushed + idle + merged/closed;
|
|
118070
|
+
` + `dead-but-dirty/unpushed trees are surfaced and reclaimed only with
|
|
118071
|
+
` + "--reclaim-dirty (reversible quarantine).").option("--root <dir>", "Dev-worktree scan root (repeatable). Default: ~/code", (val, acc) => [...acc, val], []).option("--task-root <dir>", "Task-tree scan root (repeatable). Default: every agent's home/work + home/workspace", (val, acc) => [...acc, val], []).option("--no-task-roots", "Skip the per-agent task-tree sweep entirely").option("--idle-days <days>", "Idle threshold for task trees (default 14)", "14").option("--reclaim-dirty", "Operator escape hatch: quarantine IDLE dirty/unpushed task trees (reversible)").option("--yes", "Actually act (default is dry-run)").option("--json", "Output raw JSON").option("--purge-trash", "Hard-delete quarantined dirs older than --older-than").option("--older-than <days>", "Age threshold for --purge-trash (default 14)", "14").action((opts) => {
|
|
117763
118072
|
if (opts.purgeTrash) {
|
|
117764
118073
|
const parsed = Number(opts.olderThan);
|
|
117765
118074
|
const olderThan = isNaN(parsed) ? 14 : parsed;
|
|
@@ -117790,9 +118099,14 @@ Re-run with --yes to delete.`));
|
|
|
117790
118099
|
return;
|
|
117791
118100
|
}
|
|
117792
118101
|
const roots = opts.root.length > 0 ? opts.root : defaultRoots();
|
|
118102
|
+
const taskTreeRoots = opts.taskRoots === false ? [] : opts.taskRoot.length > 0 ? opts.taskRoot : defaultTaskTreeRoots();
|
|
118103
|
+
const parsedIdle = Number(opts.idleDays);
|
|
118104
|
+
const idleDays = Number.isFinite(parsedIdle) && parsedIdle >= 0 ? parsedIdle : 14;
|
|
117793
118105
|
const dateStamp = new Date().toISOString().slice(0, 10);
|
|
117794
|
-
const plan = planGc(roots, { dateStamp });
|
|
118106
|
+
const plan = planGc(roots, { dateStamp, idleDays, escapeHatch: opts.reclaimDirty }, taskTreeRoots);
|
|
117795
118107
|
const toRemove = plan.registered.filter((r) => r.verdict === "remove");
|
|
118108
|
+
const taskReap = plan.taskTrees.filter((t) => t.willAct);
|
|
118109
|
+
const taskEscape = plan.taskTrees.filter((t) => !t.willAct && t.idle && (t.verdict === "skip-dirty" || t.verdict === "skip-unpushed"));
|
|
117796
118110
|
if (!opts.yes) {
|
|
117797
118111
|
if (opts.json) {
|
|
117798
118112
|
console.log(JSON.stringify({ dryRun: true, plan }));
|
|
@@ -117814,21 +118128,45 @@ Kept (${skips.length}):`));
|
|
|
117814
118128
|
for (const r of skips)
|
|
117815
118129
|
console.log(source_default.dim(` ${r.verdict.replace("skip-", "")}: ${r.path} [${r.branch}] (pr=${r.prSignal})`));
|
|
117816
118130
|
}
|
|
118131
|
+
if (taskTreeRoots.length) {
|
|
118132
|
+
const label = opts.reclaimDirty ? "reclaim (reap + escape-hatch)" : "reap (clean + pushed + idle + merged/closed)";
|
|
118133
|
+
console.log(`
|
|
118134
|
+
Task trees to ${label}: ${source_default.bold(taskReap.length)}`);
|
|
118135
|
+
for (const t of taskReap)
|
|
118136
|
+
console.log(` ${t.dir} ${source_default.dim(`[${t.branch ?? "detached"}] ${t.shape} ${t.verdict} \u2192 ${t.dest}`)}`);
|
|
118137
|
+
if (taskEscape.length) {
|
|
118138
|
+
console.log(source_default.yellow(`
|
|
118139
|
+
Dead-but-dirty/unpushed task trees kept (${taskEscape.length}) \u2014 reclaim with --reclaim-dirty:`));
|
|
118140
|
+
for (const t of taskEscape)
|
|
118141
|
+
console.log(source_default.dim(` ${t.verdict.replace("skip-", "")}: ${t.dir} [${t.branch ?? "detached"}] ${t.shape}`));
|
|
118142
|
+
}
|
|
118143
|
+
const taskKept = plan.taskTrees.filter((t) => !t.willAct && !(t.idle && (t.verdict === "skip-dirty" || t.verdict === "skip-unpushed")) && t.verdict !== "skip-protected");
|
|
118144
|
+
if (taskKept.length) {
|
|
118145
|
+
console.log(source_default.dim(`
|
|
118146
|
+
Other task trees kept (${taskKept.length}):`));
|
|
118147
|
+
for (const t of taskKept)
|
|
118148
|
+
console.log(source_default.dim(` ${t.verdict.replace("skip-", "")}: ${t.dir} [${t.branch ?? "detached"}] (pr=${t.prSignal})`));
|
|
118149
|
+
}
|
|
118150
|
+
}
|
|
117817
118151
|
if (plan.skipped.length) {
|
|
117818
118152
|
console.log(source_default.dim(`
|
|
117819
118153
|
Ignored non-switchroom dirs: ${plan.skipped.length}`));
|
|
117820
118154
|
}
|
|
117821
118155
|
console.log(source_default.dim(`
|
|
117822
|
-
Re-run with --yes to act.
|
|
118156
|
+
Re-run with --yes to act. Reclaimed trees are MOVED to the trash dir, not deleted.`));
|
|
117823
118157
|
return;
|
|
117824
118158
|
}
|
|
117825
118159
|
const result = applyGc(plan);
|
|
117826
118160
|
if (opts.json) {
|
|
117827
118161
|
console.log(JSON.stringify(result));
|
|
117828
118162
|
} else {
|
|
117829
|
-
console.log(source_default.green(`Quarantined ${result.quarantined.length}
|
|
118163
|
+
console.log(source_default.green(`Quarantined ${result.quarantined.length} dir(s) \u2192 ${plan.trashDir}`));
|
|
117830
118164
|
console.log(source_default.green(`Removed ${result.removed.length} merged worktree(s); deleted ${result.branchesDeleted.length} branch(es).`));
|
|
117831
118165
|
console.log(source_default.dim(`Pruned metadata in ${result.pruned.length} repo(s).`));
|
|
118166
|
+
if (taskEscape.length && !opts.reclaimDirty) {
|
|
118167
|
+
console.log(source_default.yellow(`
|
|
118168
|
+
${taskEscape.length} dead-but-dirty/unpushed task tree(s) were kept. Reclaim reversibly with --reclaim-dirty.`));
|
|
118169
|
+
}
|
|
117832
118170
|
for (const e of result.errors)
|
|
117833
118171
|
console.warn(source_default.yellow(e));
|
|
117834
118172
|
console.log(source_default.dim(`
|
|
@@ -120405,6 +120743,7 @@ var import_yaml22 = __toESM(require_dist(), 1);
|
|
|
120405
120743
|
// src/config/overlay-writer.ts
|
|
120406
120744
|
init_paths();
|
|
120407
120745
|
import {
|
|
120746
|
+
chownSync as chownSync11,
|
|
120408
120747
|
closeSync as closeSync18,
|
|
120409
120748
|
existsSync as existsSync103,
|
|
120410
120749
|
fsyncSync as fsyncSync8,
|
|
@@ -120481,6 +120820,21 @@ function withAgentLock(paths, fn) {
|
|
|
120481
120820
|
} catch {}
|
|
120482
120821
|
}
|
|
120483
120822
|
}
|
|
120823
|
+
var overlayWriterRuntime = {
|
|
120824
|
+
geteuid: () => process.geteuid?.(),
|
|
120825
|
+
chown: (path10, uid, gid) => chownSync11(path10, uid, gid)
|
|
120826
|
+
};
|
|
120827
|
+
function alignStagedOwnerToDir(stagingPath, targetDir) {
|
|
120828
|
+
try {
|
|
120829
|
+
const euid = overlayWriterRuntime.geteuid();
|
|
120830
|
+
if (euid === undefined)
|
|
120831
|
+
return;
|
|
120832
|
+
const dirStat = statSync54(targetDir);
|
|
120833
|
+
if (dirStat.uid === euid)
|
|
120834
|
+
return;
|
|
120835
|
+
overlayWriterRuntime.chown(stagingPath, dirStat.uid, dirStat.gid);
|
|
120836
|
+
} catch {}
|
|
120837
|
+
}
|
|
120484
120838
|
function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
120485
120839
|
const paths = overlayPathsFor(agent, opts);
|
|
120486
120840
|
return withAgentLock(paths, () => {
|
|
@@ -120494,6 +120848,7 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
120494
120848
|
} finally {
|
|
120495
120849
|
closeSync18(fd);
|
|
120496
120850
|
}
|
|
120851
|
+
alignStagedOwnerToDir(stagingPath, paths.scheduleDir);
|
|
120497
120852
|
renameSync27(stagingPath, finalPath);
|
|
120498
120853
|
return finalPath;
|
|
120499
120854
|
});
|
|
@@ -120511,6 +120866,7 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
120511
120866
|
} finally {
|
|
120512
120867
|
closeSync18(fd);
|
|
120513
120868
|
}
|
|
120869
|
+
alignStagedOwnerToDir(stagingPath, paths.skillsDir);
|
|
120514
120870
|
renameSync27(stagingPath, finalPath);
|
|
120515
120871
|
return finalPath;
|
|
120516
120872
|
});
|
|
@@ -123985,7 +124341,7 @@ init_source();
|
|
|
123985
124341
|
init_helpers();
|
|
123986
124342
|
init_operator_uid();
|
|
123987
124343
|
init_compose();
|
|
123988
|
-
import { chownSync as
|
|
124344
|
+
import { chownSync as chownSync12, existsSync as existsSync112, mkdirSync as mkdirSync65, writeFileSync as writeFileSync48, copyFileSync as copyFileSync15 } from "node:fs";
|
|
123989
124345
|
import { homedir as homedir62 } from "node:os";
|
|
123990
124346
|
import { join as join115 } from "node:path";
|
|
123991
124347
|
import { spawnSync as spawnSync27 } from "node:child_process";
|
|
@@ -124165,10 +124521,10 @@ async function doInstall2(opts, program3) {
|
|
|
124165
124521
|
writeFileSync48(composePath, yaml, "utf8");
|
|
124166
124522
|
try {
|
|
124167
124523
|
if (typeof process.geteuid === "function" && process.geteuid() === 0) {
|
|
124168
|
-
|
|
124169
|
-
|
|
124524
|
+
chownSync12(dir, operatorUid, operatorUid);
|
|
124525
|
+
chownSync12(composePath, operatorUid, operatorUid);
|
|
124170
124526
|
if (bak)
|
|
124171
|
-
|
|
124527
|
+
chownSync12(bak, operatorUid, operatorUid);
|
|
124172
124528
|
}
|
|
124173
124529
|
} catch {}
|
|
124174
124530
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|