switchroom 0.20.6 → 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 +106 -12
- package/dist/auth-broker/index.js +71 -9
- package/dist/cli/notion-write-pretool.mjs +68 -7
- package/dist/cli/switchroom.js +387 -30
- package/dist/host-control/main.js +72 -10
- package/dist/vault/approvals/kernel-server.js +71 -9
- package/dist/vault/broker/server.js +71 -9
- 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 +342 -120
- 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 +50 -54
- package/telegram-plugin/gateway/narrative-lane.ts +21 -1
- package/telegram-plugin/gateway/obligation-ledger.ts +9 -0
- package/telegram-plugin/gateway/obligation-wiring.ts +19 -4
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +43 -3
- package/telegram-plugin/gateway/represent-delivery-guard.ts +188 -0
- package/telegram-plugin/gateway/stream-render.ts +24 -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/pending-inbound-buffer.test.ts +145 -9
- package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
- package/telegram-plugin/tests/represent-guard.test.ts +295 -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";
|
|
@@ -14045,7 +14045,7 @@ var init_schema = __esm(() => {
|
|
|
14045
14045
|
reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
|
|
14046
14046
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
|
|
14047
14047
|
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
|
|
14048
|
-
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
14048
|
+
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS \u2014 the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
14049
14049
|
});
|
|
14050
14050
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
14051
14051
|
microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
|
|
@@ -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
|
|
@@ -16107,7 +16168,8 @@ var init_hindsight_perf_defaults = __esm(() => {
|
|
|
16107
16168
|
"HINDSIGHT_API_RETAIN_WALL_TIMEOUT",
|
|
16108
16169
|
"HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
|
|
16109
16170
|
"HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
|
|
16110
|
-
"HINDSIGHT_API_TEMPORAL_LANGUAGES"
|
|
16171
|
+
"HINDSIGHT_API_TEMPORAL_LANGUAGES",
|
|
16172
|
+
"HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
|
|
16111
16173
|
]);
|
|
16112
16174
|
HINDSIGHT_WORKER_SLOT_TYPES = [
|
|
16113
16175
|
"consolidation",
|
|
@@ -20447,6 +20509,15 @@ var init_agent_uid = __esm(() => {
|
|
|
20447
20509
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
20448
20510
|
import { existsSync as existsSync9, lstatSync as lstatSync2, readdirSync as readdirSync3, statSync as statSync6 } from "node:fs";
|
|
20449
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
|
+
}
|
|
20450
20521
|
function scopedAssertCandidates(agentDir) {
|
|
20451
20522
|
return [
|
|
20452
20523
|
join5(agentDir, ".claude", "settings.json"),
|
|
@@ -20517,7 +20588,9 @@ var init_agent_owned_tree = __esm(() => {
|
|
|
20517
20588
|
SCOPED_RECURSIVE_SUBDIRS = [
|
|
20518
20589
|
".claude",
|
|
20519
20590
|
".claude-cron",
|
|
20520
|
-
"telegram"
|
|
20591
|
+
"telegram",
|
|
20592
|
+
"schedule.d",
|
|
20593
|
+
"skills.d"
|
|
20521
20594
|
];
|
|
20522
20595
|
SCOPED_TOP_LEVEL_CRITICAL = [
|
|
20523
20596
|
"start.sh",
|
|
@@ -20529,15 +20602,26 @@ var init_agent_owned_tree = __esm(() => {
|
|
|
20529
20602
|
ownershipRuntime = {
|
|
20530
20603
|
geteuid: () => process.geteuid?.(),
|
|
20531
20604
|
chownTree: (uid, gid, rootDir) => {
|
|
20532
|
-
|
|
20533
|
-
|
|
20534
|
-
|
|
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
|
+
}
|
|
20535
20618
|
},
|
|
20536
20619
|
chownShallow: (uid, gid, paths) => {
|
|
20537
20620
|
if (paths.length === 0)
|
|
20538
20621
|
return;
|
|
20539
|
-
execFileSync3("chown", ["-h", `${uid}:${gid}`, ...paths], {
|
|
20540
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
20622
|
+
execFileSync3("chown", ["-h", "--", `${uid}:${gid}`, ...paths], {
|
|
20623
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
20624
|
+
env: { ...process.env, LC_ALL: "C" }
|
|
20541
20625
|
});
|
|
20542
20626
|
},
|
|
20543
20627
|
scopedSweepEnabled: () => process.env[SCOPED_SWEEP_ENV] === "1",
|
|
@@ -46932,7 +47016,6 @@ var ManifestSchema, WARN_ONLY_COMPONENTS;
|
|
|
46932
47016
|
var init_manifest = __esm(() => {
|
|
46933
47017
|
init_zod();
|
|
46934
47018
|
ManifestSchema = exports_external.object({
|
|
46935
|
-
switchroom_version: exports_external.string().min(1),
|
|
46936
47019
|
tested_at: exports_external.string().min(1),
|
|
46937
47020
|
runtime: exports_external.object({
|
|
46938
47021
|
bun: exports_external.string().min(1),
|
|
@@ -65425,7 +65508,7 @@ async function checkManifestDrift(probers) {
|
|
|
65425
65508
|
{
|
|
65426
65509
|
name: "dependency manifest",
|
|
65427
65510
|
status: "ok",
|
|
65428
|
-
detail: `all versions match (
|
|
65511
|
+
detail: `all versions match (switchroom ${SWITCHROOM_VERSION})`
|
|
65429
65512
|
}
|
|
65430
65513
|
];
|
|
65431
65514
|
}
|
|
@@ -65811,6 +65894,7 @@ var init_doctor = __esm(() => {
|
|
|
65811
65894
|
init_accounts();
|
|
65812
65895
|
init_schema();
|
|
65813
65896
|
init_manifest();
|
|
65897
|
+
init_resolve_version();
|
|
65814
65898
|
init_hindsight2();
|
|
65815
65899
|
init_hindsight();
|
|
65816
65900
|
init_host_capabilities();
|
|
@@ -85382,6 +85466,7 @@ Scaffolding agent: ${name}
|
|
|
85382
85466
|
}
|
|
85383
85467
|
} catch (err) {
|
|
85384
85468
|
console.error(source_default.red(`Failed to restart ${n}: ${err.message}`));
|
|
85469
|
+
process.exitCode = 1;
|
|
85385
85470
|
}
|
|
85386
85471
|
}
|
|
85387
85472
|
if (sawAbort) {
|
|
@@ -113944,6 +114029,16 @@ async function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
113944
114029
|
timedOut: true
|
|
113945
114030
|
});
|
|
113946
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
|
+
}
|
|
113947
114042
|
const got = deps.probeVersion(step.agent);
|
|
113948
114043
|
if (got === null || normalizeVersion(got) !== targetNorm) {
|
|
113949
114044
|
deps.log(` \u2717 ${step.agent} \u2192 ${got ?? "<unreachable>"} (expected ${target}) \u2014 STOPPING`);
|
|
@@ -114634,6 +114729,7 @@ function registerRestartCommand(program3) {
|
|
|
114634
114729
|
}
|
|
114635
114730
|
} catch (err) {
|
|
114636
114731
|
console.error(source_default.red(` ${name}: restart failed: ${err.message}`));
|
|
114732
|
+
process.exitCode = 1;
|
|
114637
114733
|
}
|
|
114638
114734
|
}
|
|
114639
114735
|
console.log();
|
|
@@ -114644,6 +114740,7 @@ function registerRestartCommand(program3) {
|
|
|
114644
114740
|
// src/cli/versions.ts
|
|
114645
114741
|
init_source();
|
|
114646
114742
|
init_manifest();
|
|
114743
|
+
init_resolve_version();
|
|
114647
114744
|
function formatRow(component, declared, installed, isDrift, warnOnly) {
|
|
114648
114745
|
const installedStr = installed ?? source_default.red("(not installed)");
|
|
114649
114746
|
if (!isDrift) {
|
|
@@ -114673,7 +114770,7 @@ function registerVersionsCommand(program3) {
|
|
|
114673
114770
|
const driftMap = new Map(report.drift.map((d) => [d.component, d]));
|
|
114674
114771
|
console.log(source_default.bold(`
|
|
114675
114772
|
Dependency manifest`));
|
|
114676
|
-
console.log(source_default.gray(` switchroom ${
|
|
114773
|
+
console.log(source_default.gray(` switchroom ${SWITCHROOM_VERSION} \u00b7 tested ${manifest.tested_at}`));
|
|
114677
114774
|
console.log();
|
|
114678
114775
|
const bunDrift = driftMap.get("bun");
|
|
114679
114776
|
console.log(formatRow("bun", manifest.runtime.bun, bunDrift ? bunDrift.installed : manifest.runtime.bun, !!bunDrift, false));
|
|
@@ -117366,6 +117463,40 @@ function isEphemeralPath(path10) {
|
|
|
117366
117463
|
}
|
|
117367
117464
|
return false;
|
|
117368
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
|
+
}
|
|
117369
117500
|
var defaultExec3 = (file, args, cwd) => execFileSync31(file, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
117370
117501
|
function defaultPrSignal(repo, branch, exec) {
|
|
117371
117502
|
try {
|
|
@@ -117398,10 +117529,28 @@ function defaultPrSignal(repo, branch, exec) {
|
|
|
117398
117529
|
return "error";
|
|
117399
117530
|
}
|
|
117400
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
|
+
}
|
|
117401
117550
|
function trashRoot() {
|
|
117402
117551
|
return resolve58(process.env.SWITCHROOM_WORKTREE_TRASH ?? join102(homedir55(), ".switchroom", "worktree-gc-trash"));
|
|
117403
117552
|
}
|
|
117404
|
-
function planGc(roots, deps = {}) {
|
|
117553
|
+
function planGc(roots, deps = {}, taskTreeRoots = []) {
|
|
117405
117554
|
const exists = deps.existsSync ?? existsSync100;
|
|
117406
117555
|
const readDir = deps.readDir ?? ((p) => readdirSync33(p));
|
|
117407
117556
|
const readFile4 = deps.readFile ?? ((p) => readFileSync90(p, "utf8"));
|
|
@@ -117410,6 +117559,11 @@ function planGc(roots, deps = {}) {
|
|
|
117410
117559
|
const prSignal = deps.prSignal ?? ((repo, branch) => defaultPrSignal(repo, branch, exec));
|
|
117411
117560
|
const stamp = deps.dateStamp ?? "undated";
|
|
117412
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;
|
|
117413
117567
|
let claimed;
|
|
117414
117568
|
try {
|
|
117415
117569
|
claimed = new Set(listRecords().map((r) => resolve58(r.path)));
|
|
@@ -117521,11 +117675,137 @@ function planGc(roots, deps = {}) {
|
|
|
117521
117675
|
registered.push({ path: wt.path, branch: wt.branch, verdict, prSignal: sig, repo });
|
|
117522
117676
|
}
|
|
117523
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
|
+
}
|
|
117524
117802
|
return {
|
|
117525
117803
|
roots,
|
|
117804
|
+
taskTreeRoots,
|
|
117526
117805
|
trashDir: trash,
|
|
117527
117806
|
orphans,
|
|
117528
117807
|
registered,
|
|
117808
|
+
taskTrees,
|
|
117529
117809
|
reposToPrune: [...reposToPrune],
|
|
117530
117810
|
skipped
|
|
117531
117811
|
};
|
|
@@ -117541,7 +117821,8 @@ function applyGc(plan, deps = {}) {
|
|
|
117541
117821
|
}
|
|
117542
117822
|
});
|
|
117543
117823
|
const res = { quarantined: [], removed: [], branchesDeleted: [], pruned: [], errors: [] };
|
|
117544
|
-
|
|
117824
|
+
const taskTreeActions = (plan.taskTrees ?? []).filter((t) => t.willAct);
|
|
117825
|
+
if (plan.orphans.length || taskTreeActions.length)
|
|
117545
117826
|
mkdirp(plan.trashDir);
|
|
117546
117827
|
for (const o of plan.orphans) {
|
|
117547
117828
|
try {
|
|
@@ -117551,6 +117832,14 @@ function applyGc(plan, deps = {}) {
|
|
|
117551
117832
|
res.errors.push(`quarantine ${o.dir}: ${e.message}`);
|
|
117552
117833
|
}
|
|
117553
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
|
+
}
|
|
117554
117843
|
for (const r of plan.registered) {
|
|
117555
117844
|
if (r.verdict !== "remove")
|
|
117556
117845
|
continue;
|
|
@@ -117622,6 +117911,24 @@ function purgeTrash(paths) {
|
|
|
117622
117911
|
function defaultRoots() {
|
|
117623
117912
|
return [join102(homedir55(), "code")];
|
|
117624
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
|
+
}
|
|
117625
117932
|
|
|
117626
117933
|
// src/cli/worktree.ts
|
|
117627
117934
|
function registerWorktreeCommand(program3) {
|
|
@@ -117755,10 +118062,13 @@ Skipped ${result.skipped.length} stale worktree(s) (kept for safety):`));
|
|
|
117755
118062
|
}
|
|
117756
118063
|
}
|
|
117757
118064
|
});
|
|
117758
|
-
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.
|
|
117759
118066
|
` + `Dry-run by default; pass --yes to act. Quarantines orphaned worktree
|
|
117760
118067
|
` + `directories (move, not delete) and removes registered worktrees whose
|
|
117761
|
-
` +
|
|
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) => {
|
|
117762
118072
|
if (opts.purgeTrash) {
|
|
117763
118073
|
const parsed = Number(opts.olderThan);
|
|
117764
118074
|
const olderThan = isNaN(parsed) ? 14 : parsed;
|
|
@@ -117789,9 +118099,14 @@ Re-run with --yes to delete.`));
|
|
|
117789
118099
|
return;
|
|
117790
118100
|
}
|
|
117791
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;
|
|
117792
118105
|
const dateStamp = new Date().toISOString().slice(0, 10);
|
|
117793
|
-
const plan = planGc(roots, { dateStamp });
|
|
118106
|
+
const plan = planGc(roots, { dateStamp, idleDays, escapeHatch: opts.reclaimDirty }, taskTreeRoots);
|
|
117794
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"));
|
|
117795
118110
|
if (!opts.yes) {
|
|
117796
118111
|
if (opts.json) {
|
|
117797
118112
|
console.log(JSON.stringify({ dryRun: true, plan }));
|
|
@@ -117813,21 +118128,45 @@ Kept (${skips.length}):`));
|
|
|
117813
118128
|
for (const r of skips)
|
|
117814
118129
|
console.log(source_default.dim(` ${r.verdict.replace("skip-", "")}: ${r.path} [${r.branch}] (pr=${r.prSignal})`));
|
|
117815
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
|
+
}
|
|
117816
118151
|
if (plan.skipped.length) {
|
|
117817
118152
|
console.log(source_default.dim(`
|
|
117818
118153
|
Ignored non-switchroom dirs: ${plan.skipped.length}`));
|
|
117819
118154
|
}
|
|
117820
118155
|
console.log(source_default.dim(`
|
|
117821
|
-
Re-run with --yes to act.
|
|
118156
|
+
Re-run with --yes to act. Reclaimed trees are MOVED to the trash dir, not deleted.`));
|
|
117822
118157
|
return;
|
|
117823
118158
|
}
|
|
117824
118159
|
const result = applyGc(plan);
|
|
117825
118160
|
if (opts.json) {
|
|
117826
118161
|
console.log(JSON.stringify(result));
|
|
117827
118162
|
} else {
|
|
117828
|
-
console.log(source_default.green(`Quarantined ${result.quarantined.length}
|
|
118163
|
+
console.log(source_default.green(`Quarantined ${result.quarantined.length} dir(s) \u2192 ${plan.trashDir}`));
|
|
117829
118164
|
console.log(source_default.green(`Removed ${result.removed.length} merged worktree(s); deleted ${result.branchesDeleted.length} branch(es).`));
|
|
117830
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
|
+
}
|
|
117831
118170
|
for (const e of result.errors)
|
|
117832
118171
|
console.warn(source_default.yellow(e));
|
|
117833
118172
|
console.log(source_default.dim(`
|
|
@@ -120404,6 +120743,7 @@ var import_yaml22 = __toESM(require_dist(), 1);
|
|
|
120404
120743
|
// src/config/overlay-writer.ts
|
|
120405
120744
|
init_paths();
|
|
120406
120745
|
import {
|
|
120746
|
+
chownSync as chownSync11,
|
|
120407
120747
|
closeSync as closeSync18,
|
|
120408
120748
|
existsSync as existsSync103,
|
|
120409
120749
|
fsyncSync as fsyncSync8,
|
|
@@ -120480,6 +120820,21 @@ function withAgentLock(paths, fn) {
|
|
|
120480
120820
|
} catch {}
|
|
120481
120821
|
}
|
|
120482
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
|
+
}
|
|
120483
120838
|
function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
120484
120839
|
const paths = overlayPathsFor(agent, opts);
|
|
120485
120840
|
return withAgentLock(paths, () => {
|
|
@@ -120493,6 +120848,7 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
120493
120848
|
} finally {
|
|
120494
120849
|
closeSync18(fd);
|
|
120495
120850
|
}
|
|
120851
|
+
alignStagedOwnerToDir(stagingPath, paths.scheduleDir);
|
|
120496
120852
|
renameSync27(stagingPath, finalPath);
|
|
120497
120853
|
return finalPath;
|
|
120498
120854
|
});
|
|
@@ -120510,6 +120866,7 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
|
|
|
120510
120866
|
} finally {
|
|
120511
120867
|
closeSync18(fd);
|
|
120512
120868
|
}
|
|
120869
|
+
alignStagedOwnerToDir(stagingPath, paths.skillsDir);
|
|
120513
120870
|
renameSync27(stagingPath, finalPath);
|
|
120514
120871
|
return finalPath;
|
|
120515
120872
|
});
|
|
@@ -123984,7 +124341,7 @@ init_source();
|
|
|
123984
124341
|
init_helpers();
|
|
123985
124342
|
init_operator_uid();
|
|
123986
124343
|
init_compose();
|
|
123987
|
-
import { chownSync as
|
|
124344
|
+
import { chownSync as chownSync12, existsSync as existsSync112, mkdirSync as mkdirSync65, writeFileSync as writeFileSync48, copyFileSync as copyFileSync15 } from "node:fs";
|
|
123988
124345
|
import { homedir as homedir62 } from "node:os";
|
|
123989
124346
|
import { join as join115 } from "node:path";
|
|
123990
124347
|
import { spawnSync as spawnSync27 } from "node:child_process";
|
|
@@ -124164,10 +124521,10 @@ async function doInstall2(opts, program3) {
|
|
|
124164
124521
|
writeFileSync48(composePath, yaml, "utf8");
|
|
124165
124522
|
try {
|
|
124166
124523
|
if (typeof process.geteuid === "function" && process.geteuid() === 0) {
|
|
124167
|
-
|
|
124168
|
-
|
|
124524
|
+
chownSync12(dir, operatorUid, operatorUid);
|
|
124525
|
+
chownSync12(composePath, operatorUid, operatorUid);
|
|
124169
124526
|
if (bak)
|
|
124170
|
-
|
|
124527
|
+
chownSync12(bak, operatorUid, operatorUid);
|
|
124171
124528
|
}
|
|
124172
124529
|
} catch {}
|
|
124173
124530
|
console.log(source_default.green(` \u2713 Wrote ${composePath}`));
|