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.
Files changed (30) hide show
  1. package/dist/agent-scheduler/index.js +106 -12
  2. package/dist/auth-broker/index.js +71 -9
  3. package/dist/cli/notion-write-pretool.mjs +68 -7
  4. package/dist/cli/switchroom.js +387 -30
  5. package/dist/host-control/main.js +72 -10
  6. package/dist/vault/approvals/kernel-server.js +71 -9
  7. package/dist/vault/broker/server.js +71 -9
  8. package/package.json +1 -1
  9. package/telegram-plugin/bridge/ipc-client.ts +17 -1
  10. package/telegram-plugin/dist/bridge/bridge.js +5 -2
  11. package/telegram-plugin/dist/gateway/gateway.js +342 -120
  12. package/telegram-plugin/dist/server.js +5 -2
  13. package/telegram-plugin/gateway/boot-reason.ts +61 -0
  14. package/telegram-plugin/gateway/cron-session.ts +66 -0
  15. package/telegram-plugin/gateway/gateway.ts +50 -54
  16. package/telegram-plugin/gateway/narrative-lane.ts +21 -1
  17. package/telegram-plugin/gateway/obligation-ledger.ts +9 -0
  18. package/telegram-plugin/gateway/obligation-wiring.ts +19 -4
  19. package/telegram-plugin/gateway/pending-inbound-buffer.ts +43 -3
  20. package/telegram-plugin/gateway/represent-delivery-guard.ts +188 -0
  21. package/telegram-plugin/gateway/stream-render.ts +24 -2
  22. package/telegram-plugin/tests/boot-card-reason.test.ts +88 -0
  23. package/telegram-plugin/tests/cron-bridge-drain-spool-ack.test.ts +150 -0
  24. package/telegram-plugin/tests/ipc-client-reconnect-rejection.test.ts +70 -0
  25. package/telegram-plugin/tests/narrative-lane-golden.test.ts +86 -0
  26. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +145 -9
  27. package/telegram-plugin/tests/queued-card-surface.test.ts +66 -0
  28. package/telegram-plugin/tests/represent-guard.test.ts +295 -0
  29. package/telegram-plugin/tests/turn-flush-safety.test.ts +83 -0
  30. package/telegram-plugin/turn-flush-safety.ts +79 -0
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.20.6", COMMIT_SHA = "4b1ad9b1";
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
- execFileSync3("chown", ["-h", "-R", `${uid}:${gid}`, rootDir], {
20533
- stdio: ["ignore", "ignore", "pipe"]
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 (manifest ${manifest.switchroom_version})`
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 ${manifest.switchroom_version} \u00b7 tested ${manifest.tested_at}`));
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
- if (plan.orphans.length)
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
- ` + "PR is MERGED and whose tree is clean.").option("--root <dir>", "Scan root (repeatable). Default: ~/code", (val, acc) => [...acc, val], []).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) => {
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. Orphans are MOVED to the trash dir, not deleted.`));
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} orphan(s) \u2192 ${plan.trashDir}`));
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 chownSync11, existsSync as existsSync112, mkdirSync as mkdirSync65, writeFileSync as writeFileSync48, copyFileSync as copyFileSync15 } from "node:fs";
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
- chownSync11(dir, operatorUid, operatorUid);
124168
- chownSync11(composePath, operatorUid, operatorUid);
124524
+ chownSync12(dir, operatorUid, operatorUid);
124525
+ chownSync12(composePath, operatorUid, operatorUid);
124169
124526
  if (bak)
124170
- chownSync11(bak, operatorUid, operatorUid);
124527
+ chownSync12(bak, operatorUid, operatorUid);
124171
124528
  }
124172
124529
  } catch {}
124173
124530
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));