switchroom 0.21.17 → 0.21.19

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 (33) hide show
  1. package/dist/cli/switchroom.js +1884 -1512
  2. package/dist/host-control/main.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/switchroom-release/SKILL.md +6 -1
  5. package/telegram-plugin/connection-drop.ts +83 -0
  6. package/telegram-plugin/dist/bridge/bridge.js +41 -2
  7. package/telegram-plugin/dist/gateway/gateway.js +48 -6
  8. package/telegram-plugin/dist/server.js +46 -3
  9. package/telegram-plugin/llm-error-present.ts +25 -0
  10. package/telegram-plugin/scripts/bun-test-ci.sh +6 -0
  11. package/telegram-plugin/session-tail.ts +38 -1
  12. package/telegram-plugin/tests/llm-error-present.test.ts +183 -0
  13. package/telegram-plugin/uat/flip/allowlist.test.ts +229 -0
  14. package/telegram-plugin/uat/flip/allowlist.ts +349 -0
  15. package/telegram-plugin/uat/flip/gate.test.ts +153 -0
  16. package/telegram-plugin/uat/flip/gate.ts +232 -0
  17. package/telegram-plugin/uat/flip/probe-scoring.test.ts +210 -0
  18. package/telegram-plugin/uat/flip/probe-scoring.ts +200 -0
  19. package/telegram-plugin/uat/flip/probe-suite.test.ts +95 -0
  20. package/telegram-plugin/uat/flip/probe-suite.ts +155 -0
  21. package/telegram-plugin/uat/flip/probes/kdogg.probes.json +36 -0
  22. package/telegram-plugin/uat/flip/probes/test-harness.probes.json +15 -0
  23. package/telegram-plugin/uat/flip/recall-log.test.ts +131 -0
  24. package/telegram-plugin/uat/flip/recall-log.ts +178 -0
  25. package/telegram-plugin/uat/flip/report.ts +95 -0
  26. package/telegram-plugin/uat/flip/tier1-equivalence.test.ts +470 -0
  27. package/telegram-plugin/uat/flip/tier1-equivalence.ts +697 -0
  28. package/telegram-plugin/uat/flip/tier2-probe-runner.ts +327 -0
  29. package/telegram-plugin/uat/runners/scorer.ts +1 -1
  30. package/vendor/hindsight-memory/scripts/prefetch.py +33 -5
  31. package/vendor/hindsight-memory/scripts/recall.py +131 -17
  32. package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +97 -6
  33. package/vendor/hindsight-memory/scripts/tests/test_recall_buffer_join.py +70 -0
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.21.17", COMMIT_SHA = "ac61272c", COMMIT_DATE = "2026-08-17T16:42:58Z";
2123
+ var VERSION = "0.21.19", COMMIT_SHA = "7e7061ba", COMMIT_DATE = "2026-08-18T15:36:04Z";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -21340,6 +21340,60 @@ function renderHindsightHooksOverrides(raw, tunables) {
21340
21340
  return JSON.stringify(root, null, 2) + `
21341
21341
  `;
21342
21342
  }
21343
+ function readHooksPrefetchAsyncTimeout(raw) {
21344
+ const absent = { present: false, async: false, timeout: null };
21345
+ let parsed;
21346
+ try {
21347
+ parsed = JSON.parse(raw);
21348
+ } catch {
21349
+ return absent;
21350
+ }
21351
+ const hooks = parsed?.hooks;
21352
+ if (hooks == null || typeof hooks !== "object")
21353
+ return absent;
21354
+ const matchers = hooks.Stop;
21355
+ if (!Array.isArray(matchers))
21356
+ return absent;
21357
+ for (const matcher of matchers) {
21358
+ const inner = matcher?.hooks;
21359
+ if (!Array.isArray(inner))
21360
+ continue;
21361
+ for (const hook of inner) {
21362
+ const h = hook;
21363
+ if (h == null || typeof h.command !== "string")
21364
+ continue;
21365
+ if (!h.command.includes(PREFETCH_HOOK_COMMAND_MARKER))
21366
+ continue;
21367
+ return {
21368
+ present: true,
21369
+ async: h.async === true,
21370
+ timeout: typeof h.timeout === "number" ? h.timeout : null
21371
+ };
21372
+ }
21373
+ }
21374
+ return absent;
21375
+ }
21376
+ function validatePrefetchAsyncTimeout(shape, prefetchRecallTimeoutSeconds) {
21377
+ const problems = [];
21378
+ if (!shape.present) {
21379
+ problems.push("hooks/hooks.json registers no Stop hook for prefetch.py (the async " + "recall-prefetch producer)");
21380
+ return problems;
21381
+ }
21382
+ if (!shape.async) {
21383
+ problems.push('the prefetch.py Stop hook is not marked `"async": true` \u2014 a synchronous ' + "prefetch blocks every turn's completion for up to its timeout");
21384
+ }
21385
+ if (shape.timeout === null || shape.timeout <= 0) {
21386
+ problems.push("the prefetch.py Stop hook has no positive `timeout` (async ceiling) \u2014 a " + "wedged producer would never be reaped");
21387
+ return problems;
21388
+ }
21389
+ if (shape.timeout > MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS) {
21390
+ problems.push(`the prefetch.py async ceiling is ${shape.timeout}s, above the ` + `${MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS}s maximum \u2014 a wedged producer holds a ` + `background slot for that long`);
21391
+ }
21392
+ if (prefetchRecallTimeoutSeconds >= shape.timeout) {
21393
+ problems.push(`memoryPrefetchTimeoutSeconds ${prefetchRecallTimeoutSeconds}s is >= the ` + `${shape.timeout}s async hook ceiling \u2014 the producer's own recall can outlive ` + `its hook and be SIGKILLed mid buffer-write, leaving a torn buffer`);
21394
+ }
21395
+ return problems;
21396
+ }
21343
21397
  function readHooksRecallTimeout(raw) {
21344
21398
  let parsed;
21345
21399
  try {
@@ -21368,7 +21422,7 @@ function readHooksRecallTimeout(raw) {
21368
21422
  }
21369
21423
  return null;
21370
21424
  }
21371
- var DEFAULT_RECALL_HOOK_TIMEOUT_SECONDS = 12, DEFAULT_RECALL_MAX_MEMORIES = 8, RECALL_DEADLINE_HEADROOM_SECONDS = 2, MIN_RECALL_HOOK_TIMEOUT_SECONDS, DEFAULT_RECALL_REQUEST_TIMEOUT_SECONDS = 12, MANAGED_HOOK_EVENT = "UserPromptSubmit", RECALL_HOOK_COMMAND_MARKER = "recall.py";
21425
+ var DEFAULT_RECALL_HOOK_TIMEOUT_SECONDS = 12, DEFAULT_RECALL_MAX_MEMORIES = 8, RECALL_DEADLINE_HEADROOM_SECONDS = 2, MIN_RECALL_HOOK_TIMEOUT_SECONDS, DEFAULT_RECALL_REQUEST_TIMEOUT_SECONDS = 12, MANAGED_HOOK_EVENT = "UserPromptSubmit", RECALL_HOOK_COMMAND_MARKER = "recall.py", PREFETCH_HOOK_COMMAND_MARKER = "prefetch.py", MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS = 30;
21372
21426
  var init_hindsight_recall_tunables = __esm(() => {
21373
21427
  init_hindsight_recall_passthrough();
21374
21428
  MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
@@ -28559,6 +28613,25 @@ class DirectiveAdmin {
28559
28613
  });
28560
28614
  return `Deactivated directive '${target.name}' (id ${target.id}) in bank ` + `'${this.opts.bankId}', tagged ${args.tag}.`;
28561
28615
  }
28616
+ async deactivateAllActiveByName(args) {
28617
+ const all = await this.list();
28618
+ const named = all.filter((d) => d.name === args.name);
28619
+ if (named.length === 0) {
28620
+ const known = all.map((d) => d.name).sort().join(", ");
28621
+ throw new Error(`no directive named '${args.name}' in bank '${this.opts.bankId}' ` + `(the directive to deactivate). Known directives: ${known || "(none)"}`);
28622
+ }
28623
+ const active = named.filter((d) => d.is_active !== false);
28624
+ const alreadyInactive = named.filter((d) => d.is_active === false).map((d) => ({ id: d.id, name: d.name, priority: d.priority }));
28625
+ for (const d of active)
28626
+ this.refuseIfRulesBlock(d);
28627
+ const deactivated = [];
28628
+ for (const d of active) {
28629
+ if (!args.dryRun)
28630
+ await this.deactivateResolved(all, d);
28631
+ deactivated.push({ id: d.id, name: d.name, priority: d.priority });
28632
+ }
28633
+ return { deactivated, alreadyInactive, dryRun: args.dryRun ?? false };
28634
+ }
28562
28635
  async reactivate(args) {
28563
28636
  const all = await this.list();
28564
28637
  const target = this.resolve(all, args.name, "the directive to reactivate");
@@ -63701,6 +63774,49 @@ function detectHindsightRecallTunableDrift(name, agentConfig, agentDir, config)
63701
63774
  }
63702
63775
  return findings;
63703
63776
  }
63777
+ function detectPrefetchAsyncTimeoutDrift(name, agentConfig, agentDir, config) {
63778
+ if (!isHindsightEnabled(config))
63779
+ return [];
63780
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
63781
+ if (resolved.memory?.auto_recall === false)
63782
+ return [];
63783
+ const pluginDir = join66(agentDir, ".claude", "plugins", "hindsight-memory");
63784
+ if (!existsSync69(pluginDir))
63785
+ return [];
63786
+ const settingsPath = join66(pluginDir, "settings.json");
63787
+ if (!existsSync69(settingsPath))
63788
+ return [];
63789
+ let settings = null;
63790
+ try {
63791
+ settings = JSON.parse(readFileSync62(settingsPath, "utf-8"));
63792
+ } catch {
63793
+ return [];
63794
+ }
63795
+ if (settings?.memoryPrefetchEnabled !== true)
63796
+ return [];
63797
+ const hooksPath = join66(pluginDir, "hooks", "hooks.json");
63798
+ if (!existsSync69(hooksPath))
63799
+ return [];
63800
+ let shape;
63801
+ try {
63802
+ shape = readHooksPrefetchAsyncTimeout(readFileSync62(hooksPath, "utf-8"));
63803
+ } catch {
63804
+ return [];
63805
+ }
63806
+ const rawPrefetchTimeout = settings.memoryPrefetchTimeoutSeconds;
63807
+ const prefetchRecallTimeout = typeof rawPrefetchTimeout === "number" && Number.isFinite(rawPrefetchTimeout) && rawPrefetchTimeout > 0 ? rawPrefetchTimeout : 5;
63808
+ const problems = validatePrefetchAsyncTimeout(shape, prefetchRecallTimeout);
63809
+ if (problems.length === 0)
63810
+ return [];
63811
+ return [
63812
+ {
63813
+ surface: "memory-prefetch",
63814
+ agent: name,
63815
+ detail: `async recall-prefetch (memoryPrefetchEnabled) is ON but its async-timeout ` + `config is unsafe: ${problems.join("; ")}`,
63816
+ fix: "Re-stamp the plugin (`switchroom apply`) so hooks.json carries the " + '`prefetch.py` Stop hook with `"async": true` and a timeout above ' + "memoryPrefetchTimeoutSeconds, or set memoryPrefetchEnabled off until the " + "async ceiling is corrected \u2014 a wedged prefetch either blocks the turn or " + "is killed mid buffer-write."
63817
+ }
63818
+ ];
63819
+ }
63704
63820
  function writeDriftReport(agentDir, findings) {
63705
63821
  try {
63706
63822
  const report = {
@@ -63724,6 +63840,7 @@ function detectAgentDrift(name, agentConfigRaw, agentsDir, config, configPath, o
63724
63840
  }));
63725
63841
  findings.push(...detectSkillsDrift(name, agentDir));
63726
63842
  findings.push(...detectHindsightRecallTunableDrift(name, agentConfig, agentDir, config));
63843
+ findings.push(...detectPrefetchAsyncTimeoutDrift(name, agentConfig, agentDir, config));
63727
63844
  if (!opts.skipContainerProbes) {
63728
63845
  findings.push(...detectHookScriptDrift(name, {
63729
63846
  binDir: opts.binDir,
@@ -71736,11 +71853,11 @@ var init_apply = __esm(() => {
71736
71853
  });
71737
71854
 
71738
71855
  // src/host-control/audit-reader.ts
71739
- import { closeSync as closeSync15, existsSync as existsSync83, openSync as openSync15, readSync as readSync5, statSync as statSync49 } from "node:fs";
71856
+ import { closeSync as closeSync15, existsSync as existsSync84, openSync as openSync15, readSync as readSync5, statSync as statSync50 } from "node:fs";
71740
71857
  import { homedir as homedir43 } from "node:os";
71741
- import { join as join90 } from "node:path";
71858
+ import { join as join91 } from "node:path";
71742
71859
  function defaultAuditLogPath2(home2 = homedir43()) {
71743
- return join90(home2, ".switchroom", "host-control-audit.log");
71860
+ return join91(home2, ".switchroom", "host-control-audit.log");
71744
71861
  }
71745
71862
  function auditRotation() {
71746
71863
  return resolveRotationConfig({
@@ -71757,14 +71874,14 @@ function auditReadFullWindowBytes() {
71757
71874
  function auditReadMaxGenerations() {
71758
71875
  return auditRotation().maxFiles;
71759
71876
  }
71760
- function tailBytes(path7, n, strict = false) {
71877
+ function tailBytes(path9, n, strict = false) {
71761
71878
  if (n <= 0)
71762
71879
  return "";
71763
71880
  let fd;
71764
71881
  let size;
71765
71882
  try {
71766
- size = statSync49(path7).size;
71767
- fd = openSync15(path7, "r");
71883
+ size = statSync50(path9).size;
71884
+ fd = openSync15(path9, "r");
71768
71885
  } catch (err) {
71769
71886
  if (strict && err.code !== "ENOENT")
71770
71887
  throw err;
@@ -71809,7 +71926,7 @@ function readAuditRaw(logPath, opts = {}) {
71809
71926
  budget -= Buffer.byteLength(active, "utf-8");
71810
71927
  for (let n = 1;n <= maxFiles && budget > 0; n++) {
71811
71928
  const gen = `${logPath}.${n}`;
71812
- if (!existsSync83(gen))
71929
+ if (!existsSync84(gen))
71813
71930
  break;
71814
71931
  const chunk2 = tailBytes(gen, budget, strict);
71815
71932
  if (chunk2.length === 0)
@@ -71987,16 +72104,16 @@ var init_audit_reader = __esm(() => {
71987
72104
  });
71988
72105
 
71989
72106
  // src/web/fleet-health-read.ts
71990
- import { readFileSync as readFileSync78 } from "node:fs";
71991
- import { resolve as resolve45 } from "node:path";
72107
+ import { readFileSync as readFileSync79 } from "node:fs";
72108
+ import { resolve as resolve46 } from "node:path";
71992
72109
  import { homedir as homedir46 } from "node:os";
71993
72110
  function fleetHealthLedgerPath(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir46()) {
71994
- return resolve45(home2, ".switchroom", "fleet-health", "ledger.json");
72111
+ return resolve46(home2, ".switchroom", "fleet-health", "ledger.json");
71995
72112
  }
71996
- function readFleetHealth(path7) {
72113
+ function readFleetHealth(path9) {
71997
72114
  let ledger;
71998
72115
  try {
71999
- ledger = JSON.parse(readFileSync78(path7, "utf-8"));
72116
+ ledger = JSON.parse(readFileSync79(path9, "utf-8"));
72000
72117
  } catch {
72001
72118
  return {
72002
72119
  owner_agent: null,
@@ -72022,8 +72139,8 @@ function readFleetHealth(path7) {
72022
72139
  ...ranked.length === 0 ? { reason: EMPTY_REASON } : {}
72023
72140
  };
72024
72141
  }
72025
- function handleGetFleetHealth(path7 = fleetHealthLedgerPath()) {
72026
- return readFleetHealth(path7);
72142
+ function handleGetFleetHealth(path9 = fleetHealthLedgerPath()) {
72143
+ return readFleetHealth(path9);
72027
72144
  }
72028
72145
  var EMPTY_REASON;
72029
72146
  var init_fleet_health_read = __esm(() => {
@@ -110258,8 +110375,8 @@ init_loader();
110258
110375
  init_hindsight();
110259
110376
  var import_yaml16 = __toESM(require_dist(), 1);
110260
110377
  import { spawn as spawn5 } from "node:child_process";
110261
- import { existsSync as existsSync82, readFileSync as readFileSync74, statSync as statSync48 } from "node:fs";
110262
- import { join as join88 } from "node:path";
110378
+ import { existsSync as existsSync83, readFileSync as readFileSync75, statSync as statSync49 } from "node:fs";
110379
+ import { join as join89 } from "node:path";
110263
110380
 
110264
110381
  // src/cli/memory-rules.ts
110265
110382
  init_source();
@@ -110495,6 +110612,37 @@ function registerMemoryDirectiveCommand(memory, program3) {
110495
110612
  process.exit(1);
110496
110613
  }
110497
110614
  }));
110615
+ directive.command("deactivate <agent> <name>").description("Deactivate every ACTIVE directive named <name> in <agent>'s bank by " + "flipping is_active=false \u2014 the M3 directive-placement campaign entry " + "point for retiring a guardrail's in-bank copy once it has been " + "relocated to the agent's always-loaded CLAUDE.md. Matches by NAME " + "(mirrors reconcile's 'every ACTIVE directive named <name>' selection); " + "if multiple active copies share the name, all are deactivated. " + "REVERSIBLE \u2014 only is_active is touched, never a delete or a " + "content/priority rewrite, so it can be reactivated. Idempotent: a " + "re-run over already-inactive copies is a clean no-op, not an error. " + "REFUSES any directive carrying the rules-block marker tag (that stays " + "active until an M3-flip removes the marker) \u2014 the whole call is " + "refused if even one active copy carries it, mutating nothing.").option("--dry-run", "Print what WOULD deactivate without mutating anything").option("--json", "Machine-readable output").action(withConfigError(async (agent, name, opts) => {
110616
+ const admin = resolveDirectiveAdmin(program3, agent);
110617
+ try {
110618
+ const result = await admin.deactivateAllActiveByName({
110619
+ name,
110620
+ ...opts.dryRun ? { dryRun: true } : {}
110621
+ });
110622
+ if (opts.json) {
110623
+ console.log(JSON.stringify({ ok: true, agent, name, ...result }));
110624
+ return;
110625
+ }
110626
+ const verb = result.dryRun ? "would deactivate" : "deactivated";
110627
+ if (result.deactivated.length === 0) {
110628
+ console.log(source_default.yellow(`\u2022 no-op: no ACTIVE directive named "${name}" in ${agent}'s bank ` + `(${result.alreadyInactive.length} already-inactive ` + `cop${result.alreadyInactive.length === 1 ? "y" : "ies"} left ` + `untouched). Nothing to do.`));
110629
+ return;
110630
+ }
110631
+ for (const d of result.deactivated) {
110632
+ console.log(source_default.green(` ${result.dryRun ? "[dry-run] " : "\u2713 "}${verb} "${d.name}" ` + `(id ${d.id}, prior priority ${d.priority ?? "unset"})`));
110633
+ }
110634
+ const n = result.deactivated.length;
110635
+ console.log(source_default.green(`${result.dryRun ? "[dry-run] " : "\u2713 "}${verb} ${n} directive` + `${n === 1 ? "" : "s"} named "${name}" in ${agent}'s bank` + (result.alreadyInactive.length > 0 ? ` (${result.alreadyInactive.length} already-inactive ` + `cop${result.alreadyInactive.length === 1 ? "y" : "ies"} skipped)` : "") + (result.dryRun ? " \u2014 nothing was mutated" : ". Reverse with reactivate.")));
110636
+ } catch (e) {
110637
+ const msg = e instanceof Error ? e.message : String(e);
110638
+ if (opts.json) {
110639
+ console.log(JSON.stringify({ ok: false, error: msg }));
110640
+ } else {
110641
+ console.error(source_default.red(`\u2717 ${msg}`));
110642
+ }
110643
+ process.exit(1);
110644
+ }
110645
+ }));
110498
110646
  directive.command("mark-rules-block <agent> <id>").description("Stamp the persisted rules-block marker tag on directive <id> in " + "<agent>'s bank \u2014 the SAME DirectiveAdmin.markRulesBlock write path " + "the batch triage executor uses. Once stamped, deactivate_directive " + "(and every other DirectiveAdmin deactivation path) refuses this " + "directive unconditionally until an M3-flip action removes the " + "marker. This is the real entry point the mental-model-curator " + "skill's interactive triage pass calls for every directive it " + "classifies rules-block, BEFORE presenting the card \u2014 that skill " + "has no other write path to this marker (no MCP tool exposes it), " + "so without this call the code-level refusal never actually " + "arms itself on the interactive path (PR #4760 review follow-up).").option("--json", "Machine-readable output").action(withConfigError(async (agent, id, opts) => {
110499
110647
  const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
110500
110648
  if (selfAgent && selfAgent !== agent) {
@@ -110526,305 +110674,1227 @@ function registerMemoryDirectiveCommand(memory, program3) {
110526
110674
  }));
110527
110675
  }
110528
110676
 
110529
- // src/cli/memory.ts
110530
- function persistMemoryConfigUrl(configPath, provider, url) {
110531
- if (!existsSync82(configPath))
110532
- return false;
110533
- const raw = readFileSync74(configPath, "utf-8");
110534
- const doc = import_yaml16.default.parseDocument(raw);
110535
- if (!doc.has("memory")) {
110536
- doc.set("memory", { backend: "hindsight", shared_collection: "shared", config: { provider, url } });
110537
- } else {
110538
- const memNode = doc.get("memory");
110539
- if (!memNode.has("config")) {
110540
- memNode.set("config", { provider, url });
110541
- } else {
110542
- const configNode = memNode.get("config");
110543
- configNode.set("url", url);
110544
- if (!configNode.has("provider")) {
110545
- configNode.set("provider", provider);
110546
- }
110547
- }
110677
+ // src/cli/memory-flip.ts
110678
+ init_source();
110679
+ init_helpers();
110680
+ init_hindsight2();
110681
+ init_hindsight();
110682
+ init_hindsight_directive_admin();
110683
+
110684
+ // src/memory/directive-triage.ts
110685
+ init_hindsight_directive_admin();
110686
+ var SUPERSEDED_BY_TAG_RE = /^superseded-by:(.+)$/;
110687
+ function classifyDirective(directive, override) {
110688
+ const isActive = directive.is_active !== false;
110689
+ const overrideSignal = override?.signal.trim();
110690
+ if (hasRulesBlockMarker(directive.tags)) {
110691
+ return {
110692
+ category: "rules-block",
110693
+ signal: "carries the rules-block marker tag \u2014 staged for M3, never retired",
110694
+ action: "stage-for-m3"
110695
+ };
110548
110696
  }
110549
- let mode = 420;
110550
- try {
110551
- mode = statSync48(configPath).mode & 511;
110552
- } catch {}
110553
- writeConfigFileSync(configPath, doc.toString(), mode);
110554
- return true;
110555
- }
110556
- function resolveMemorySetupTag(input) {
110557
- const explicit = input.explicitTag?.trim();
110558
- if (explicit) {
110559
- if (explicit.toLowerCase() === "latest") {
110560
- return { tag: undefined, reason: "explicit-latest" };
110697
+ if (override && override.category === "rules-block") {
110698
+ return {
110699
+ category: "rules-block",
110700
+ signal: overrideSignal || "rules-block override with no signal text \u2014 staged for M3, never retired",
110701
+ action: "stage-for-m3"
110702
+ };
110703
+ }
110704
+ const supersessionTag = (directive.tags ?? []).map((t) => SUPERSEDED_BY_TAG_RE.exec(t)).find((m) => m !== null);
110705
+ if (supersessionTag) {
110706
+ const winner = supersessionTag[1];
110707
+ return {
110708
+ category: "retire",
110709
+ signal: isActive ? `superseded by '${winner}' (superseded-by tag)` : `superseded by '${winner}' (superseded-by tag) \u2014 already inactive, nothing to retire`,
110710
+ action: isActive ? "retire" : "keep",
110711
+ supersededBy: winner
110712
+ };
110713
+ }
110714
+ if (override && overrideSignal) {
110715
+ if (override.category === "reflect-directive") {
110716
+ return { category: "reflect-directive", signal: overrideSignal, action: "keep" };
110561
110717
  }
110562
- const normalized = normalizeHindsightVersionTag(explicit);
110563
- if (!normalized)
110564
- return { tag: explicit, reason: "invalid" };
110565
- return { tag: normalized, reason: "explicit" };
110718
+ return {
110719
+ category: override.category,
110720
+ signal: isActive ? overrideSignal : `${overrideSignal} \u2014 already inactive, nothing to retire`,
110721
+ action: isActive ? "retire" : "keep"
110722
+ };
110566
110723
  }
110567
- const pin = normalizeHindsightVersionTag(input.releasePin);
110568
- if (pin)
110569
- return { tag: pin, reason: "pin" };
110570
- return { tag: undefined, reason: "latest" };
110571
- }
110572
- function hindsightSecretValues(input) {
110573
- const out = [];
110574
- const push = (v) => {
110575
- if (typeof v === "string" && v.trim().length > 0)
110576
- out.push(v.trim());
110724
+ return {
110725
+ category: "reflect-directive",
110726
+ signal: "no deterministic signal \u2014 defaults to KEEP",
110727
+ action: "keep"
110577
110728
  };
110578
- push(input.cpAccessKey);
110579
- push(input.llm?.api_key);
110580
- for (const op of ["retain", "reflect", "consolidation"]) {
110581
- push(input.llm?.[op]?.api_key);
110582
- }
110583
- return out;
110584
110729
  }
110585
- function hasUnresolvedHindsightVaultRef(config) {
110586
- const hindsight = config.hindsight;
110587
- return hindsightSecretValues({
110588
- llm: hindsight?.llm,
110589
- cpAccessKey: hindsight?.cp_access_key
110590
- }).some((v) => isVaultReference(v));
110730
+ function buildDirectiveTriageRows(directives, overrides) {
110731
+ return directives.map((d) => {
110732
+ const classified = classifyDirective(d, overrides?.get(d.id));
110733
+ return {
110734
+ id: d.id,
110735
+ name: d.name,
110736
+ priority: d.priority ?? 0,
110737
+ isActive: d.is_active !== false,
110738
+ ...classified
110739
+ };
110740
+ });
110591
110741
  }
110592
- function emitsCleartextHindsightSecret(input) {
110593
- return hindsightSecretValues(input).some((v) => !isVaultReference(v));
110742
+
110743
+ // src/cli/debug.ts
110744
+ init_helpers();
110745
+ init_loader();
110746
+ import { existsSync as existsSync82, readFileSync as readFileSync74, readdirSync as readdirSync30, statSync as statSync48 } from "node:fs";
110747
+ import { resolve as resolve43, join as join88 } from "node:path";
110748
+ import { createHash as createHash15 } from "node:crypto";
110749
+
110750
+ // src/agents/workspace.ts
110751
+ import { readFile as readFile2, stat } from "node:fs/promises";
110752
+ import path8 from "node:path";
110753
+
110754
+ // src/agents/bootstrap-budget.ts
110755
+ import path7 from "node:path";
110756
+ var DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
110757
+ var DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES = 3;
110758
+ var DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX = 32;
110759
+
110760
+ class BootstrapBudgetExceededError extends Error {
110761
+ analysis;
110762
+ constructor(analysis, message) {
110763
+ super(message);
110764
+ this.analysis = analysis;
110765
+ this.name = "BootstrapBudgetExceededError";
110766
+ }
110594
110767
  }
110595
- var HINDSIGHT_COMPOSE_CLEARTEXT_SECRETS_WARNING = "This snippet embeds secrets in CLEARTEXT (LLM api_key and/or cp_access_key)." + " Treat the output as sensitive \u2014 do not paste it into shared channels or" + " commit it unredacted.";
110596
- async function resolveLiteLLMForHindsight(config) {
110597
- return resolveHindsightLiteLlm(config);
110768
+ function normalizePositiveLimit(value) {
110769
+ if (!Number.isFinite(value) || value <= 0) {
110770
+ return 1;
110771
+ }
110772
+ return Math.floor(value);
110598
110773
  }
110599
- function readRecallLog(agentDir, limit) {
110600
- const path7 = join88(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
110601
- if (!existsSync82(path7))
110602
- return [];
110603
- let raw;
110604
- try {
110605
- raw = readFileSync74(path7, "utf-8");
110606
- } catch {
110774
+ function formatWarningCause(cause) {
110775
+ return cause === "per-file-limit" ? "max/file" : "max/total";
110776
+ }
110777
+ function normalizeSeenSignatures(signatures) {
110778
+ if (!Array.isArray(signatures) || signatures.length === 0) {
110607
110779
  return [];
110608
110780
  }
110609
- const lines = raw.split(`
110610
- `).filter((l) => l.trim().length > 0);
110611
- const tail = lines.slice(-limit);
110612
- const out = [];
110613
- for (const line of tail) {
110614
- try {
110615
- out.push(JSON.parse(line));
110616
- } catch {}
110781
+ const seen = new Set;
110782
+ const result = [];
110783
+ for (const signature of signatures) {
110784
+ const value = typeof signature === "string" ? signature.trim() : "";
110785
+ if (!value || seen.has(value)) {
110786
+ continue;
110787
+ }
110788
+ seen.add(value);
110789
+ result.push(value);
110617
110790
  }
110618
- return out;
110791
+ return result;
110619
110792
  }
110620
- function formatRecallLogView(name, entries) {
110621
- const lines = [source_default.bold(`
110622
- ${name}:`)];
110623
- const total = entries.length;
110624
- const hits = entries.filter((e) => e.cache_hit).length;
110625
- const cappedTurns = entries.filter((e) => e.capped).length;
110626
- const omittedTurns = entries.filter((e) => typeof e.directives_omitted === "number" && e.directives_omitted > 0).length;
110627
- const memCounts = entries.map((e) => e.result_count).filter((n) => typeof n === "number");
110628
- const avg = memCounts.length > 0 ? Math.round(memCounts.reduce((s, n) => s + n, 0) / memCounts.length * 10) / 10 : null;
110629
- const max = memCounts.length > 0 ? Math.max(...memCounts) : null;
110630
- const medians = entries.map((e) => e.injected_score_median).filter((n) => typeof n === "number");
110631
- const avgScore = medians.length > 0 ? Math.round(medians.reduce((s, n) => s + n, 0) / medians.length * 100) / 100 : null;
110632
- const collapseTurns = entries.filter((e) => typeof e.injected_own_bank_count === "number" && e.injected_own_bank_count === 0 && typeof e.injected_additional_bank_count === "number" && e.injected_additional_bank_count > 0).length;
110633
- lines.push(source_default.gray(` last ${total} turn${total === 1 ? "" : "s"}: ` + `avg=${avg ?? "\u2014"} max=${max ?? "\u2014"} ` + `avg_score=${avgScore ?? "\u2014"} ` + `cache_hits=${hits} capped=${cappedTurns}` + (omittedTurns > 0 ? ` directives_omitted_turns=${omittedTurns}` : "") + (collapseTurns > 0 ? source_default.red(` own_bank_collapse_turns=${collapseTurns}`) : "")));
110634
- for (const e of entries) {
110635
- const flag = e.cache_hit ? source_default.cyan("CACHE") : e.capped ? source_default.yellow("CAP") : source_default.green("OK");
110636
- const dem = e.demoted_count && e.demoted_count > 0 ? source_default.dim(` -${e.demoted_count}d`) : "";
110637
- const omitted = typeof e.directives_omitted === "number" && e.directives_omitted > 0 ? source_default.yellow(` +${e.directives_omitted}omitted`) : "";
110638
- const ids = e.memory_ids && e.memory_ids.length > 0 ? source_default.dim(` ids=${e.memory_ids.slice(0, 3).join(",")}${e.memory_ids.length > 3 ? `\u2026+${e.memory_ids.length - 3}` : ""}`) : "";
110639
- const own = e.injected_own_bank_count;
110640
- const additional = e.injected_additional_bank_count;
110641
- const composition = typeof own === "number" || typeof additional === "number" ? (() => {
110642
- const text = ` own/add=${own ?? "\u2014"}/${additional ?? "\u2014"}`;
110643
- return own === 0 && typeof additional === "number" && additional > 0 ? source_default.red(text) : source_default.dim(text);
110644
- })() : "";
110645
- const score = typeof e.injected_score_min === "number" && typeof e.injected_score_median === "number" && typeof e.injected_score_max === "number" ? source_default.dim(` score=${e.injected_score_min.toFixed(2)}/` + `${e.injected_score_median.toFixed(2)}/` + `${e.injected_score_max.toFixed(2)}`) : "";
110646
- lines.push(` ${source_default.gray(e.ts)} ${flag} ` + `n=${e.result_count ?? "\u2014"}${e.pre_cap_count != null && e.pre_cap_count !== e.result_count ? `/${e.pre_cap_count}` : ""}` + `${dem}${omitted}${score}${composition}${ids}`);
110793
+ function appendSeenSignature(signatures, signature) {
110794
+ if (!signature.trim()) {
110795
+ return signatures;
110647
110796
  }
110648
- return lines;
110797
+ if (signatures.includes(signature)) {
110798
+ return signatures;
110799
+ }
110800
+ const next = [...signatures, signature];
110801
+ if (next.length <= DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX) {
110802
+ return next;
110803
+ }
110804
+ return next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
110649
110805
  }
110650
- function registerMemoryCommand(program3) {
110651
- const memory = program3.command("memory").description("Hindsight memory operations");
110652
- memory.command("search <query>").description("Search agent memories via Hindsight").option("-a, --agent <name>", "Search a specific agent's collection").action(withConfigError(async (query, opts) => {
110653
- const config = getConfig(program3);
110654
- if (opts.agent) {
110655
- if (!config.agents[opts.agent]) {
110656
- console.error(source_default.red(`Agent "${opts.agent}" is not defined in switchroom.yaml`));
110657
- process.exit(1);
110658
- }
110659
- const collection = getCollectionForAgent(opts.agent, config);
110660
- console.log(source_default.bold(`
110661
- Search: ${opts.agent} (collection: ${collection})
110662
- `));
110663
- console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
110664
- console.log();
110665
- return;
110666
- }
110667
- const agentNames = Object.keys(config.agents);
110668
- console.log(source_default.bold(`
110669
- Searching all eligible collections:
110670
- `));
110671
- for (const name of agentNames) {
110672
- const collection = getCollectionForAgent(name, config);
110673
- if (isStrictIsolation(name, config)) {
110674
- console.log(source_default.gray(` ${name} (${collection}) \u2014 skipped (strict isolation)`));
110675
- continue;
110676
- }
110677
- console.log(source_default.cyan(` ${name} (${collection}):`));
110678
- console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
110679
- }
110680
- console.log();
110681
- }));
110682
- memory.command("stats").description("List agents with their collection names and isolation mode").action(withConfigError(async () => {
110683
- const config = getConfig(program3);
110684
- const agentNames = Object.keys(config.agents);
110685
- if (agentNames.length === 0) {
110686
- console.log(source_default.yellow("No agents defined in switchroom.yaml"));
110687
- return;
110688
- }
110689
- const headers = ["Agent", "Collection", "Isolation", "Auto-recall"];
110690
- const widths = [20, 20, 12, 12];
110691
- const headerLine = headers.map((h, i2) => source_default.bold(h.padEnd(widths[i2]))).join(" ");
110692
- console.log(`
110693
- ${headerLine}`);
110694
- for (const name of agentNames) {
110695
- const collection = getCollectionForAgent(name, config);
110696
- const isolation = isStrictIsolation(name, config) ? "strict" : "default";
110697
- const autoRecall = config.agents[name].memory?.auto_recall ?? true;
110698
- const row = [
110699
- name.padEnd(widths[0]),
110700
- collection.padEnd(widths[1]),
110701
- isolation.padEnd(widths[2]),
110702
- (autoRecall ? "yes" : "no").padEnd(widths[3])
110703
- ].join(" ");
110704
- console.log(` ${row}`);
110806
+ function buildBootstrapInjectionStats(params) {
110807
+ const injectedByPath = new Map;
110808
+ const injectedByBaseName = new Map;
110809
+ for (const file of params.injectedFiles) {
110810
+ const pathValue = typeof file.path === "string" ? file.path.trim() : "";
110811
+ if (!pathValue) {
110812
+ continue;
110705
110813
  }
110706
- console.log();
110707
- const profileBanks = collectProfileBanks(config);
110708
- if (profileBanks.size > 0) {
110709
- const owners = new Map;
110710
- for (const [uname, u] of Object.entries(config.users ?? {})) {
110711
- if (u.profile_bank) {
110712
- owners.set(u.profile_bank, [
110713
- ...owners.get(u.profile_bank) ?? [],
110714
- uname
110715
- ]);
110716
- }
110717
- }
110718
- console.log(source_default.bold(` Profile banks (per-user / shared):
110719
- `));
110720
- for (const bank of [...profileBanks].sort()) {
110721
- const who = owners.get(bank);
110722
- const label = who ? `user: ${who.join(", ")}` : "shared";
110723
- console.log(` ${bank.padEnd(widths[1])} ${source_default.gray(label)}`);
110724
- }
110725
- console.log();
110814
+ if (!injectedByPath.has(pathValue)) {
110815
+ injectedByPath.set(pathValue, file.content);
110726
110816
  }
110727
- console.log(source_default.bold(` Hindsight CLI commands:
110728
- `));
110729
- for (const name of agentNames) {
110730
- const collection = getCollectionForAgent(name, config);
110731
- console.log(source_default.gray(` $ ${getMemoryStats(collection)}`));
110817
+ const normalizedPath = pathValue.replace(/\\/g, "/");
110818
+ const baseName = path7.posix.basename(normalizedPath);
110819
+ if (!injectedByBaseName.has(baseName)) {
110820
+ injectedByBaseName.set(baseName, file.content);
110732
110821
  }
110733
- console.log();
110734
- }));
110735
- memory.command("reflect").description("Show cross-agent reflection plan").action(withConfigError(async () => {
110736
- const config = getConfig(program3);
110737
- const { eligible, excluded, commands } = reflectAcrossAgents(config);
110738
- console.log(source_default.bold(`
110739
- Cross-agent reflection plan
110740
- `));
110741
- if (eligible.length > 0) {
110742
- console.log(source_default.green(" Eligible collections:"));
110743
- for (const { agent, collection } of eligible) {
110744
- console.log(source_default.white(` ${agent} -> ${collection}`));
110745
- }
110822
+ }
110823
+ return params.bootstrapFiles.map((file) => {
110824
+ const pathValue = typeof file.path === "string" ? file.path.trim() : "";
110825
+ const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length;
110826
+ const injected = (pathValue ? injectedByPath.get(pathValue) : undefined) ?? injectedByPath.get(file.name) ?? injectedByBaseName.get(file.name);
110827
+ const injectedChars = injected ? injected.length : 0;
110828
+ const truncated = !file.missing && injectedChars < rawChars;
110829
+ return {
110830
+ name: file.name,
110831
+ path: pathValue || file.name,
110832
+ missing: file.missing,
110833
+ rawChars,
110834
+ injectedChars,
110835
+ truncated
110836
+ };
110837
+ });
110838
+ }
110839
+ function analyzeBootstrapBudget(params) {
110840
+ const bootstrapMaxChars = normalizePositiveLimit(params.bootstrapMaxChars);
110841
+ const bootstrapTotalMaxChars = normalizePositiveLimit(params.bootstrapTotalMaxChars);
110842
+ const nearLimitRatio = typeof params.nearLimitRatio === "number" && Number.isFinite(params.nearLimitRatio) && params.nearLimitRatio > 0 && params.nearLimitRatio < 1 ? params.nearLimitRatio : DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO;
110843
+ const nonMissing = params.files.filter((file) => !file.missing);
110844
+ const rawChars = nonMissing.reduce((sum, file) => sum + file.rawChars, 0);
110845
+ const injectedChars = nonMissing.reduce((sum, file) => sum + file.injectedChars, 0);
110846
+ const totalNearLimit = injectedChars >= Math.ceil(bootstrapTotalMaxChars * nearLimitRatio);
110847
+ const totalOverLimit = injectedChars >= bootstrapTotalMaxChars;
110848
+ const files = params.files.map((file) => {
110849
+ if (file.missing) {
110850
+ return { ...file, nearLimit: false, causes: [] };
110746
110851
  }
110747
- if (excluded.length > 0) {
110748
- console.log(source_default.red(`
110749
- Excluded (strict isolation):`));
110750
- for (const { agent, collection } of excluded) {
110751
- console.log(source_default.gray(` ${agent} -> ${collection}`));
110852
+ const perFileOverLimit = file.rawChars > bootstrapMaxChars;
110853
+ const nearLimit = file.rawChars >= Math.ceil(bootstrapMaxChars * nearLimitRatio);
110854
+ const causes = [];
110855
+ if (file.truncated) {
110856
+ if (perFileOverLimit) {
110857
+ causes.push("per-file-limit");
110752
110858
  }
110753
- }
110754
- if (commands.length > 0) {
110755
- console.log(source_default.bold(`
110756
- Hindsight CLI commands:
110757
- `));
110758
- for (const cmd of commands) {
110759
- console.log(source_default.gray(` $ ${cmd}`));
110859
+ if (totalOverLimit) {
110860
+ causes.push("total-limit");
110760
110861
  }
110761
- } else {
110762
- console.log(source_default.yellow(`
110763
- No eligible collections for reflection.`));
110764
110862
  }
110765
- console.log();
110766
- }));
110767
- memory.command("setup").description("Manage the Hindsight Docker container").option("--stop", "Stop and remove the Hindsight container").option("--status", "Show Hindsight container status").option("--recreate", "Pull the latest image and recreate the container (reusing its current port). Used by `switchroom update` to keep the hindsight singleton current. Also required after any `hindsight.llm` model/provider config change \u2014 env is only re-derived on recreate, a plain docker restart is not enough (see docs/operators/hindsight-model-change.md).").option("--tag <version>", "Pin the hindsight image tag to pull + run (e.g. v0.15.18), overriding the " + "default floating `:latest`. Threaded through by `switchroom rollout` so a " + "version-pinned roll recreates hindsight on the SAME tag as the rest of the " + "fleet. Omit for `:latest` (the standalone default).").option("--provider <provider>", "LLM provider (ollama, openai, anthropic)").option("--strict-env", "Refuse the recreate (before the running container is touched) if it would drop " + "env vars that are live on the container but declared in neither `hindsight.env` " + "nor switchroom's managed defaults. Default is to warn loudly and proceed, so a " + "rollout is never wedged half-way with memory down.").option("--gpu", "Force GPU passthrough on for this launch, overriding host autodetection. " + "Use when `~/.switchroom/host-capabilities.json` is wrong or unreadable and " + "you know the host has a working nvidia container toolkit (`docker run --gpus " + "all` hard-fails container create without one). Beats `hindsight.gpu`.").option("--no-gpu", "Force GPU passthrough off for this launch, overriding host autodetection. " + "Also the explicit opt-out for the recreate GPU drop guard.").option("--allow-gpu-drop", "Proceed with a --recreate that would take GPU passthrough away from the " + "running container. Without this the recreate refuses, because that drop " + "moves the embedding model and cross-encoder reranker to CPU on the " + "interactive recall path with no other signal.").action(async (opts) => {
110768
- if (opts.status) {
110769
- if (!isDockerAvailable()) {
110770
- console.log(source_default.red(" Docker is not available."));
110771
- process.exit(1);
110772
- }
110773
- const status = getHindsightStatus();
110774
- if (status) {
110775
- console.log(source_default.bold(`
110776
- Hindsight container status:`));
110777
- console.log(` ${source_default.cyan("switchroom-hindsight")}: ${status}
110778
- `);
110779
- } else {
110780
- console.log(source_default.yellow(`
110781
- Hindsight container not found.
110782
- `));
110783
- console.log(source_default.gray(" Run 'switchroom memory setup' to start it."));
110784
- }
110785
- return;
110863
+ return { ...file, nearLimit, causes };
110864
+ });
110865
+ const truncatedFiles = files.filter((file) => file.truncated);
110866
+ const nearLimitFiles = files.filter((file) => file.nearLimit);
110867
+ return {
110868
+ files,
110869
+ truncatedFiles,
110870
+ nearLimitFiles,
110871
+ totalNearLimit,
110872
+ hasTruncation: truncatedFiles.length > 0,
110873
+ totals: {
110874
+ rawChars,
110875
+ injectedChars,
110876
+ truncatedChars: Math.max(0, rawChars - injectedChars),
110877
+ bootstrapMaxChars,
110878
+ bootstrapTotalMaxChars,
110879
+ nearLimitRatio
110786
110880
  }
110787
- if (opts.stop) {
110788
- if (!isDockerAvailable()) {
110789
- console.log(source_default.red(" Docker is not available."));
110790
- process.exit(1);
110791
- }
110792
- if (!isHindsightContainerExists()) {
110793
- console.log(source_default.yellow(" No switchroom-hindsight container found."));
110794
- return;
110795
- }
110796
- console.log(source_default.gray(" Stopping switchroom-hindsight..."));
110797
- stopHindsightRecallPool();
110798
- stopHindsight();
110799
- console.log(source_default.green(" Hindsight container stopped and removed."));
110800
- return;
110881
+ };
110882
+ }
110883
+ function buildBootstrapTruncationSignature(analysis) {
110884
+ if (!analysis.hasTruncation) {
110885
+ return;
110886
+ }
110887
+ const files = analysis.truncatedFiles.map((file) => ({
110888
+ path: file.path || file.name,
110889
+ rawChars: file.rawChars,
110890
+ injectedChars: file.injectedChars,
110891
+ causes: [...file.causes].sort()
110892
+ })).sort((a, b) => {
110893
+ const pathCmp = a.path.localeCompare(b.path);
110894
+ if (pathCmp !== 0) {
110895
+ return pathCmp;
110801
110896
  }
110802
- if (!isDockerAvailable()) {
110803
- console.log(source_default.red(`
110804
- Docker is not available.`));
110805
- console.log(source_default.gray(` Install Docker: https://docs.docker.com/get-docker/
110806
- `));
110807
- process.exit(1);
110897
+ if (a.rawChars !== b.rawChars) {
110898
+ return a.rawChars - b.rawChars;
110808
110899
  }
110809
- const recreate = opts.recreate === true;
110810
- let resolvedLlmPromise;
110811
- const getResolvedLlm = () => {
110812
- if (!resolvedLlmPromise) {
110813
- resolvedLlmPromise = resolveHindsightLlmSecrets(getConfig(program3).hindsight?.llm);
110814
- }
110815
- return resolvedLlmPromise;
110816
- };
110817
- let releasePin;
110818
- try {
110819
- releasePin = getConfig(program3).release?.pin ?? undefined;
110820
- } catch {
110821
- releasePin = undefined;
110900
+ if (a.injectedChars !== b.injectedChars) {
110901
+ return a.injectedChars - b.injectedChars;
110822
110902
  }
110823
- const { tag: effectiveTag, reason: tagReason } = resolveMemorySetupTag({
110824
- explicitTag: opts.tag,
110825
- releasePin
110826
- });
110827
- switch (tagReason) {
110903
+ return a.causes.join("+").localeCompare(b.causes.join("+"));
110904
+ });
110905
+ return JSON.stringify({
110906
+ bootstrapMaxChars: analysis.totals.bootstrapMaxChars,
110907
+ bootstrapTotalMaxChars: analysis.totals.bootstrapTotalMaxChars,
110908
+ files
110909
+ });
110910
+ }
110911
+ function formatBootstrapTruncationWarningLines(params) {
110912
+ if (!params.analysis.hasTruncation) {
110913
+ return [];
110914
+ }
110915
+ const maxFiles = typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES;
110916
+ const lines = [];
110917
+ const duplicateNameCounts = params.analysis.truncatedFiles.reduce((acc, file) => {
110918
+ acc.set(file.name, (acc.get(file.name) ?? 0) + 1);
110919
+ return acc;
110920
+ }, new Map);
110921
+ const topFiles = params.analysis.truncatedFiles.slice(0, maxFiles);
110922
+ for (const file of topFiles) {
110923
+ const pct = file.rawChars > 0 ? Math.round((file.rawChars - file.injectedChars) / file.rawChars * 100) : 0;
110924
+ const causeText = file.causes.length > 0 ? file.causes.map((cause) => formatWarningCause(cause)).join(", ") : "";
110925
+ const nameLabel = (duplicateNameCounts.get(file.name) ?? 0) > 1 && file.path.trim().length > 0 ? `${file.name} (${file.path})` : file.name;
110926
+ lines.push(`${nameLabel}: ${file.rawChars} raw -> ${file.injectedChars} injected (~${Math.max(0, pct)}% removed${causeText ? `; ${causeText}` : ""}).`);
110927
+ }
110928
+ if (params.analysis.truncatedFiles.length > topFiles.length) {
110929
+ lines.push(`+${params.analysis.truncatedFiles.length - topFiles.length} more truncated file(s).`);
110930
+ }
110931
+ lines.push("If unintentional, raise switchroom workspace bootstrapMaxChars and/or bootstrapTotalMaxChars config.");
110932
+ return lines;
110933
+ }
110934
+ function buildBootstrapPromptWarning(params) {
110935
+ const signature = buildBootstrapTruncationSignature(params.analysis);
110936
+ let seenSignatures = normalizeSeenSignatures(params.seenSignatures);
110937
+ if (params.previousSignature && !seenSignatures.includes(params.previousSignature)) {
110938
+ seenSignatures = appendSeenSignature(seenSignatures, params.previousSignature);
110939
+ }
110940
+ const hasSeenSignature = Boolean(signature && seenSignatures.includes(signature));
110941
+ const warningShown = params.mode !== "off" && Boolean(signature) && (params.mode === "always" || !hasSeenSignature);
110942
+ const warningSignaturesSeen = signature && params.mode !== "off" ? appendSeenSignature(seenSignatures, signature) : seenSignatures;
110943
+ return {
110944
+ signature,
110945
+ warningShown,
110946
+ lines: warningShown ? formatBootstrapTruncationWarningLines({
110947
+ analysis: params.analysis,
110948
+ maxFiles: params.maxFiles
110949
+ }) : [],
110950
+ warningSignaturesSeen
110951
+ };
110952
+ }
110953
+
110954
+ // src/agents/bootstrap-types.ts
110955
+ var DEFAULT_AGENTS_FILENAME = "AGENTS.md";
110956
+ var DEFAULT_SOUL_DEFAULT_FILENAME = "SOUL.default.md";
110957
+ var DEFAULT_SOUL_FILENAME = "SOUL.md";
110958
+ var DEFAULT_TOOLS_FILENAME = "TOOLS.md";
110959
+ var DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
110960
+ var DEFAULT_USER_FILENAME = "USER.md";
110961
+ var DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
110962
+ var DEFAULT_MEMORY_FILENAME = "MEMORY.md";
110963
+
110964
+ // src/agents/workspace.ts
110965
+ var DEFAULT_WORKSPACE_DIR_NAME = "workspace";
110966
+ var DEFAULT_MEMORY_SUBDIR = "memory";
110967
+ var STABLE_BOOTSTRAP_FILENAMES = [
110968
+ DEFAULT_AGENTS_FILENAME,
110969
+ DEFAULT_SOUL_DEFAULT_FILENAME,
110970
+ DEFAULT_SOUL_FILENAME,
110971
+ DEFAULT_IDENTITY_FILENAME,
110972
+ DEFAULT_USER_FILENAME,
110973
+ DEFAULT_TOOLS_FILENAME,
110974
+ DEFAULT_BOOTSTRAP_FILENAME
110975
+ ];
110976
+ var DYNAMIC_BOOTSTRAP_FILENAMES = [
110977
+ DEFAULT_MEMORY_FILENAME
110978
+ ];
110979
+ var DEFAULT_BOOTSTRAP_MAX_CHARS = 12000;
110980
+ var DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 64000;
110981
+ var DEFAULT_DYNAMIC_TOTAL_MAX_CHARS = 24000;
110982
+ function resolveAgentWorkspaceDir(agentDir) {
110983
+ return path8.join(agentDir, DEFAULT_WORKSPACE_DIR_NAME);
110984
+ }
110985
+ async function readOptionalFile(filePath) {
110986
+ try {
110987
+ const content = await readFile2(filePath, "utf8");
110988
+ return content;
110989
+ } catch (err) {
110990
+ if (isErrnoException(err) && (err.code === "ENOENT" || err.code === "EISDIR")) {
110991
+ return;
110992
+ }
110993
+ throw err;
110994
+ }
110995
+ }
110996
+ function isErrnoException(value) {
110997
+ return value instanceof Error && typeof value.code === "string";
110998
+ }
110999
+ async function loadNamedFile(workspaceDir, name, relativePath) {
111000
+ const filePath = path8.join(workspaceDir, relativePath ?? name);
111001
+ const content = await readOptionalFile(filePath);
111002
+ if (content === undefined) {
111003
+ return { name, path: filePath, missing: true };
111004
+ }
111005
+ return { name, path: filePath, content, missing: false };
111006
+ }
111007
+ async function loadStableBootstrapFiles(workspaceDir, options) {
111008
+ const defaultFiles = await Promise.all(STABLE_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
111009
+ const extras = options?.extraStableFiles ?? [];
111010
+ if (extras.length === 0) {
111011
+ return defaultFiles;
111012
+ }
111013
+ const extraFiles = await Promise.all(extras.map((name) => loadNamedFile(workspaceDir, name)));
111014
+ return [...defaultFiles, ...extraFiles];
111015
+ }
111016
+ async function loadDynamicBootstrapFiles(workspaceDir, options) {
111017
+ const now2 = options?.now ?? new Date;
111018
+ const includeYesterday = options?.includeYesterday ?? true;
111019
+ const files = await Promise.all(DYNAMIC_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
111020
+ const todayRelative = dailyMemoryRelativePath(now2);
111021
+ const todayName = DEFAULT_MEMORY_FILENAME;
111022
+ const today = await loadNamedFile(workspaceDir, todayName, todayRelative);
111023
+ today.path = path8.join(workspaceDir, todayRelative);
111024
+ files.push(today);
111025
+ if (includeYesterday) {
111026
+ const yesterdayRelative = dailyMemoryRelativePath(addDays(now2, -1));
111027
+ const yesterday = await loadNamedFile(workspaceDir, todayName, yesterdayRelative);
111028
+ yesterday.path = path8.join(workspaceDir, yesterdayRelative);
111029
+ files.push(yesterday);
111030
+ }
111031
+ return files;
111032
+ }
111033
+ function pad22(n) {
111034
+ return n < 10 ? `0${n}` : String(n);
111035
+ }
111036
+ function dailyMemoryRelativePath(date) {
111037
+ const y = date.getFullYear();
111038
+ const m = pad22(date.getMonth() + 1);
111039
+ const d = pad22(date.getDate());
111040
+ return path8.join(DEFAULT_MEMORY_SUBDIR, `${y}-${m}-${d}.md`);
111041
+ }
111042
+ function addDays(date, days) {
111043
+ const out = new Date(date.getTime());
111044
+ out.setDate(out.getDate() + days);
111045
+ return out;
111046
+ }
111047
+ function truncateContent(name, content, maxChars) {
111048
+ if (content.length <= maxChars) {
111049
+ return content;
111050
+ }
111051
+ if (maxChars < 200) {
111052
+ return content.slice(0, Math.max(0, maxChars));
111053
+ }
111054
+ const headRoomInitial = Math.floor(maxChars * 0.7);
111055
+ const sampleMarker = `
111056
+ \u2026(truncated ${name}: kept ${headRoomInitial}+${Math.max(0, maxChars - headRoomInitial - 96)} chars of ${content.length})\u2026
111057
+ `;
111058
+ const markerReserve = Math.max(64, sampleMarker.length + 8);
111059
+ const headRoom = Math.min(headRoomInitial, Math.max(0, maxChars - markerReserve - 8));
111060
+ const tailRoom = Math.max(0, maxChars - headRoom - markerReserve);
111061
+ const head = content.slice(0, Math.max(0, headRoom));
111062
+ const tail = tailRoom > 0 ? content.slice(content.length - tailRoom) : "";
111063
+ const result = `${head}
111064
+ \u2026(truncated ${name}: kept ${headRoom}+${tailRoom} chars of ${content.length})\u2026
111065
+ ${tail}`;
111066
+ return result.length <= maxChars ? result : result.slice(0, maxChars);
111067
+ }
111068
+ function projectBootstrapFiles(params) {
111069
+ const {
111070
+ files,
111071
+ heading,
111072
+ budget,
111073
+ seenSignatures,
111074
+ warningMode = "error",
111075
+ warningMaxFiles = DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES
111076
+ } = params;
111077
+ const perFileCap = budget.bootstrapMaxChars;
111078
+ let remainingTotal = budget.bootstrapTotalMaxChars;
111079
+ const injectedFiles = [];
111080
+ const parts = [];
111081
+ for (const file of files) {
111082
+ if (file.missing || typeof file.content !== "string") {
111083
+ continue;
111084
+ }
111085
+ const raw = file.content.trimEnd();
111086
+ if (raw.length === 0) {
111087
+ continue;
111088
+ }
111089
+ const perFileAllowed = Math.min(perFileCap, remainingTotal);
111090
+ if (perFileAllowed <= 0) {
111091
+ break;
111092
+ }
111093
+ const injected = truncateContent(file.name, raw, perFileAllowed);
111094
+ const relativePath = file.path;
111095
+ injectedFiles.push({ path: relativePath, content: injected });
111096
+ parts.push(`## ${relativePath}`);
111097
+ parts.push(injected);
111098
+ remainingTotal -= injected.length;
111099
+ if (remainingTotal <= 0) {
111100
+ break;
111101
+ }
111102
+ }
111103
+ const concatenated = parts.length > 0 ? [heading.trim().length > 0 ? `# ${heading}` : null, ...parts].filter((p) => p !== null).join(`
111104
+
111105
+ `) : "";
111106
+ const stats = buildBootstrapInjectionStats({
111107
+ bootstrapFiles: files,
111108
+ injectedFiles
111109
+ });
111110
+ const analysis = analyzeBootstrapBudget({
111111
+ files: stats,
111112
+ bootstrapMaxChars: budget.bootstrapMaxChars,
111113
+ bootstrapTotalMaxChars: budget.bootstrapTotalMaxChars,
111114
+ nearLimitRatio: budget.nearLimitRatio ?? DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO
111115
+ });
111116
+ const warning = buildBootstrapPromptWarning({
111117
+ analysis,
111118
+ mode: warningMode,
111119
+ seenSignatures,
111120
+ maxFiles: warningMaxFiles
111121
+ });
111122
+ if (warningMode === "error" && analysis.hasTruncation) {
111123
+ const errorLines = [
111124
+ "Bootstrap budget exceeded. The following files exceed limits:",
111125
+ ""
111126
+ ];
111127
+ for (const file of analysis.truncatedFiles) {
111128
+ const excessChars = file.rawChars - file.injectedChars;
111129
+ const causes = file.causes.map((c) => c === "per-file-limit" ? `per-file limit (${analysis.totals.bootstrapMaxChars.toLocaleString()} chars)` : `total limit (${analysis.totals.bootstrapTotalMaxChars.toLocaleString()} chars)`);
111130
+ errorLines.push(` ${file.name}: ${file.rawChars.toLocaleString()} bytes (exceeds ${causes.join(", ")})`);
111131
+ errorLines.push(` Trim ${excessChars.toLocaleString()} bytes, or pass --warning-mode warn to proceed with truncation.`);
111132
+ }
111133
+ throw new BootstrapBudgetExceededError(analysis, errorLines.join(`
111134
+ `));
111135
+ }
111136
+ return { files, injectedFiles, concatenated, analysis, warning };
111137
+ }
111138
+ async function buildStableBootstrapPrompt(params) {
111139
+ const files = await loadStableBootstrapFiles(params.workspaceDir, {
111140
+ extraStableFiles: params.extraStableFiles
111141
+ });
111142
+ const budget = {
111143
+ bootstrapMaxChars: params.budget?.bootstrapMaxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS,
111144
+ bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS,
111145
+ nearLimitRatio: params.budget?.nearLimitRatio
111146
+ };
111147
+ return projectBootstrapFiles({
111148
+ files,
111149
+ heading: "Project Context (stable workspace files)",
111150
+ budget,
111151
+ seenSignatures: params.seenSignatures,
111152
+ warningMode: params.budget?.warningMode,
111153
+ warningMaxFiles: params.budget?.warningMaxFiles
111154
+ });
111155
+ }
111156
+ async function buildDynamicBootstrapPrompt(params) {
111157
+ const files = await loadDynamicBootstrapFiles(params.workspaceDir, {
111158
+ now: params.now,
111159
+ includeYesterday: params.includeYesterday
111160
+ });
111161
+ const budget = {
111162
+ bootstrapMaxChars: params.budget?.bootstrapMaxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS,
111163
+ bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_DYNAMIC_TOTAL_MAX_CHARS,
111164
+ nearLimitRatio: params.budget?.nearLimitRatio
111165
+ };
111166
+ return projectBootstrapFiles({
111167
+ files,
111168
+ heading: "Project Context (dynamic workspace files)",
111169
+ budget,
111170
+ seenSignatures: params.seenSignatures,
111171
+ warningMode: params.budget?.warningMode,
111172
+ warningMaxFiles: params.budget?.warningMaxFiles
111173
+ });
111174
+ }
111175
+
111176
+ // src/cli/debug.ts
111177
+ init_merge();
111178
+ init_hindsight2();
111179
+ function formatBytes(bytes) {
111180
+ return `${bytes.toLocaleString()} bytes`;
111181
+ }
111182
+ function estimateTokens(bytes) {
111183
+ return Math.round(bytes / 3.7);
111184
+ }
111185
+ function readMcpServerNames(agentDir) {
111186
+ const mcpPath = join88(agentDir, ".mcp.json");
111187
+ if (!existsSync82(mcpPath))
111188
+ return [];
111189
+ try {
111190
+ const parsed = JSON.parse(readFileSync74(mcpPath, "utf-8"));
111191
+ return Object.keys(parsed.mcpServers ?? {});
111192
+ } catch {
111193
+ return null;
111194
+ }
111195
+ }
111196
+ function sha2562(content) {
111197
+ return createHash15("sha256").update(content).digest("hex").slice(0, 16);
111198
+ }
111199
+ function findLatestTranscriptJsonl(claudeConfigDir) {
111200
+ const projectsDir = join88(claudeConfigDir, "projects");
111201
+ if (!existsSync82(projectsDir))
111202
+ return;
111203
+ try {
111204
+ const entries = readdirSync30(projectsDir, { withFileTypes: true });
111205
+ let latest;
111206
+ for (const entry of entries) {
111207
+ if (!entry.isDirectory())
111208
+ continue;
111209
+ const projectPath = join88(projectsDir, entry.name);
111210
+ const transcriptPath = join88(projectPath, "transcript.jsonl");
111211
+ if (!existsSync82(transcriptPath))
111212
+ continue;
111213
+ const stat2 = statSync48(transcriptPath);
111214
+ if (!latest || stat2.mtimeMs > latest.mtime) {
111215
+ latest = { path: transcriptPath, mtime: stat2.mtimeMs };
111216
+ }
111217
+ }
111218
+ return latest?.path;
111219
+ } catch {
111220
+ return;
111221
+ }
111222
+ }
111223
+ function extractLatestUserMessage(transcriptPath) {
111224
+ try {
111225
+ const content = readFileSync74(transcriptPath, "utf-8");
111226
+ const lines = content.trim().split(`
111227
+ `).filter(Boolean);
111228
+ for (let i2 = lines.length - 1;i2 >= 0; i2--) {
111229
+ const line = lines[i2];
111230
+ try {
111231
+ const event = JSON.parse(line);
111232
+ if (event.type === "message" && event.role === "user" && typeof event.content === "string") {
111233
+ const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : "unknown";
111234
+ return { text: event.content, timestamp };
111235
+ }
111236
+ } catch {
111237
+ continue;
111238
+ }
111239
+ }
111240
+ } catch {
111241
+ return;
111242
+ }
111243
+ }
111244
+ function buildProgressUpdateGuidance() {
111245
+ return `## Progress updates (human-style check-ins)
111246
+
111247
+ You're talking to a human colleague on Telegram. Alongside the emoji status
111248
+ ladder, send a short \`progress_update\` at inflection points, the moments a
111249
+ senior colleague would ping the person who asked them to do something:
111250
+
111251
+ - **Plan formed:** "Got it. Going to do X first, then Y, then Z."
111252
+ - **Pivot or blocker:** "First approach didn't work because <reason>. Trying
111253
+ <alternative> instead."
111254
+ - **Chunk finished:** "Done with X. Starting Y now."
111255
+
111256
+ Keep them short (one or two sentences). Don't narrate every step, the pinned
111257
+ progress card shows that for free. Don't send an update on a trivial one-shot
111258
+ task. Send them when a colleague would genuinely want to know what's happening.
111259
+
111260
+ Final answers still go through \`reply\` as usual,
111261
+ \`progress_update\` is only for mid-turn check-ins.`;
111262
+ }
111263
+ function registerDebugCommand(program3) {
111264
+ const cmd = program3.command("debug", { hidden: true }).description("Observability tools for inspecting agent prompt layering [advanced]");
111265
+ cmd.command("turn <agent>").description("Dump the exact prompt layering the model saw on the most recent turn").option("--last <n>", "Show N-th most recent turn instead of latest", "1").action(withConfigError(async (agentName, opts) => {
111266
+ const config = getConfig(program3);
111267
+ const agentConfig = config.agents[agentName];
111268
+ if (!agentConfig) {
111269
+ console.error(`Agent '${agentName}' not found in switchroom.yaml`);
111270
+ process.exit(1);
111271
+ }
111272
+ const agentsDir = resolveAgentsDir(config);
111273
+ const agentDir = resolve43(agentsDir, agentName);
111274
+ if (!existsSync82(agentDir)) {
111275
+ console.error(`Agent directory not found: ${agentDir}`);
111276
+ process.exit(1);
111277
+ }
111278
+ const workspaceDir = resolveAgentWorkspaceDir(agentDir);
111279
+ const claudeConfigDir = join88(agentDir, ".claude");
111280
+ const claudeMdPath2 = join88(agentDir, "CLAUDE.md");
111281
+ const soulMdPath = join88(agentDir, "SOUL.md");
111282
+ const workspaceSoulMdPath = join88(workspaceDir, "SOUL.md");
111283
+ const handoffPath = join88(agentDir, ".handoff.md");
111284
+ const lastN = parseInt(opts.last, 10);
111285
+ if (isNaN(lastN) || lastN < 1) {
111286
+ console.error("--last must be a positive integer");
111287
+ process.exit(1);
111288
+ }
111289
+ if (lastN > 1) {
111290
+ console.error("Note: --last N where N > 1 not yet implemented (only latest turn supported)");
111291
+ process.exit(1);
111292
+ }
111293
+ console.log(`=== Debug Turn Dump: ${agentName} ===
111294
+ `);
111295
+ console.log(`=== Append System Prompt (stable) ===
111296
+ `);
111297
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
111298
+ const useHotReloadStable = resolved.channels?.telegram?.hotReloadStable === true;
111299
+ const stableResult = await buildStableBootstrapPrompt({
111300
+ workspaceDir,
111301
+ budget: { warningMode: "off" }
111302
+ });
111303
+ if (useHotReloadStable) {
111304
+ console.log(`-- Workspace Stable Render (not used \u2014 stable content is in per-turn hook) --`);
111305
+ console.log();
111306
+ } else if (stableResult.concatenated.trim().length > 0) {
111307
+ console.log(`-- Workspace Stable Render (${formatBytes(stableResult.concatenated.length)}) --`);
111308
+ console.log(stableResult.concatenated);
111309
+ console.log();
111310
+ } else {
111311
+ console.log("-- Workspace Stable Render (0 bytes, not set up) --");
111312
+ console.log();
111313
+ }
111314
+ const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(resolved);
111315
+ const progressGuidance = useSwitchroomPlugin ? buildProgressUpdateGuidance() : "";
111316
+ if (progressGuidance.length > 0) {
111317
+ console.log(`-- Progress Updates Guidance (${formatBytes(progressGuidance.length)}) --`);
111318
+ console.log(progressGuidance);
111319
+ console.log();
111320
+ }
111321
+ const baseSystemPromptAppend = resolved.system_prompt_append ?? "";
111322
+ if (baseSystemPromptAppend.trim().length > 0) {
111323
+ console.log(`-- User System Prompt Append (${formatBytes(baseSystemPromptAppend.length)}) --`);
111324
+ console.log(baseSystemPromptAppend);
111325
+ console.log();
111326
+ }
111327
+ console.log(`=== Append System Prompt (per-session) ===
111328
+ `);
111329
+ const handoffContent = existsSync82(handoffPath) ? readFileSync74(handoffPath, "utf-8") : "";
111330
+ if (handoffContent.trim().length > 0) {
111331
+ console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
111332
+ console.log(handoffContent);
111333
+ console.log();
111334
+ } else {
111335
+ console.log("-- Handoff Briefing (0 bytes, no prior session) --");
111336
+ console.log();
111337
+ }
111338
+ console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
111339
+ `);
111340
+ const claudeMdContent = existsSync82(claudeMdPath2) ? readFileSync74(claudeMdPath2, "utf-8") : "";
111341
+ if (claudeMdContent.trim().length > 0) {
111342
+ console.log(`(${formatBytes(claudeMdContent.length)})`);
111343
+ console.log(claudeMdContent);
111344
+ console.log();
111345
+ } else {
111346
+ console.log("(0 bytes, not present)");
111347
+ console.log();
111348
+ }
111349
+ console.log(`=== Persona (SOUL.md) ===
111350
+ `);
111351
+ const soulMdContent = existsSync82(soulMdPath) ? readFileSync74(soulMdPath, "utf-8") : existsSync82(workspaceSoulMdPath) ? readFileSync74(workspaceSoulMdPath, "utf-8") : "";
111352
+ if (soulMdContent.trim().length > 0) {
111353
+ console.log(`(${formatBytes(soulMdContent.length)})`);
111354
+ console.log(soulMdContent);
111355
+ console.log();
111356
+ } else {
111357
+ console.log("(0 bytes, stale placeholder \u2014 Phase 2 item: single source of truth for persona)");
111358
+ console.log();
111359
+ }
111360
+ console.log(`=== Per-Turn Injections (UserPromptSubmit) ===
111361
+ `);
111362
+ if (useHotReloadStable) {
111363
+ if (stableResult.concatenated.trim().length > 0) {
111364
+ console.log(`-- Workspace Stable (hot-reload hook): ${formatBytes(stableResult.concatenated.length)} --`);
111365
+ console.log(stableResult.concatenated);
111366
+ console.log();
111367
+ } else {
111368
+ console.log("-- Workspace Stable (hot-reload hook): 0 bytes, not set up --");
111369
+ console.log();
111370
+ }
111371
+ }
111372
+ const dynamicResult = await buildDynamicBootstrapPrompt({
111373
+ workspaceDir,
111374
+ budget: { warningMode: "off" }
111375
+ });
111376
+ if (dynamicResult.concatenated.trim().length > 0) {
111377
+ console.log(`-- Workspace Dynamic: fired, ${formatBytes(dynamicResult.concatenated.length)} --`);
111378
+ console.log(dynamicResult.concatenated);
111379
+ console.log();
111380
+ } else {
111381
+ console.log("-- Workspace Dynamic: no content (MEMORY.md and daily notes empty or missing) --");
111382
+ console.log();
111383
+ }
111384
+ const hindsightEnabled = isHindsightEnabled(config) && agentConfig.memory?.auto_recall !== false;
111385
+ if (hindsightEnabled) {
111386
+ console.log("-- Hindsight Recall: enabled (exact content unavailable, check hindsight logs) --");
111387
+ console.log();
111388
+ } else {
111389
+ console.log("-- Hindsight Recall: disabled --");
111390
+ console.log();
111391
+ }
111392
+ console.log(`=== User Message (latest turn) ===
111393
+ `);
111394
+ const transcriptPath = findLatestTranscriptJsonl(claudeConfigDir);
111395
+ const userMessage = transcriptPath ? extractLatestUserMessage(transcriptPath) : undefined;
111396
+ if (userMessage) {
111397
+ console.log(`(Turn timestamp: ${userMessage.timestamp})`);
111398
+ console.log(userMessage.text);
111399
+ console.log();
111400
+ } else {
111401
+ console.log("(unavailable: no transcript found or transcript empty)");
111402
+ console.log();
111403
+ }
111404
+ console.log(`=== Totals ===
111405
+ `);
111406
+ const stableBytes = stableResult.concatenated.length + progressGuidance.length + baseSystemPromptAppend.length;
111407
+ const perSessionBytes = handoffContent.length;
111408
+ const claudeMdBytes = claudeMdContent.length;
111409
+ const soulMdBytes = soulMdContent.length;
111410
+ const perTurnBytes = dynamicResult.concatenated.length;
111411
+ const userBytes = userMessage?.text.length ?? 0;
111412
+ const fleetDir = join88(agentsDir, "..", "fleet");
111413
+ const fleetInvPath = join88(fleetDir, "switchroom-invariants.md");
111414
+ const fleetClaudePath = join88(fleetDir, "CLAUDE.md");
111415
+ const fleetInvBytes = existsSync82(fleetInvPath) ? readFileSync74(fleetInvPath, "utf-8").length : 0;
111416
+ const fleetClaudeBytes = existsSync82(fleetClaudePath) ? readFileSync74(fleetClaudePath, "utf-8").length : 0;
111417
+ const fleetBytes = fleetInvBytes + fleetClaudeBytes;
111418
+ const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
111419
+ console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
111420
+ console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
111421
+ console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
111422
+ console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
111423
+ console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
111424
+ console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
111425
+ console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
111426
+ const mcpServers = readMcpServerNames(agentDir);
111427
+ const mcpCount = mcpServers?.length ?? null;
111428
+ const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
111429
+ const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
111430
+ const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
111431
+ console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
111432
+ if (mcpServers && mcpServers.length > 0) {
111433
+ console.log(` [${mcpServers.join(", ")}]`);
111434
+ }
111435
+ const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
111436
+ console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
111437
+ const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
111438
+ const stableHash = sha2562(stableCacheInput);
111439
+ console.log(`Cache stable hash: sha256:${stableHash}`);
111440
+ console.log();
111441
+ }));
111442
+ }
111443
+
111444
+ // src/memory/directive-residue.ts
111445
+ var RESIDUE_CATEGORIES = new Set([
111446
+ "rules-block",
111447
+ "reflect-directive"
111448
+ ]);
111449
+ function renderInjectedLine(index, directive) {
111450
+ const priority = directive.priority ?? 0;
111451
+ const name = (directive.name ?? "").trim() || "(unnamed)";
111452
+ const content = (directive.content ?? "").trim();
111453
+ return `${index}. [P${priority}] ${name}: ${content}`;
111454
+ }
111455
+ function measureDirectiveResidue(agent, rows, directivesById) {
111456
+ const residueRows = rows.filter((r) => r.isActive && RESIDUE_CATEGORIES.has(r.category));
111457
+ const lines = residueRows.map((row, i2) => {
111458
+ const directive = directivesById.get(row.id);
111459
+ if (!directive) {
111460
+ throw new Error(`measureDirectiveResidue: row '${row.name}' (id ${row.id}) has no ` + "matching directive in directivesById \u2014 pass the SAME list the " + "rows were built from.");
111461
+ }
111462
+ return renderInjectedLine(i2 + 1, directive);
111463
+ });
111464
+ const residueBytes = lines.reduce((sum, line) => sum + Buffer.byteLength(line, "utf8") + 1, 0);
111465
+ return {
111466
+ agent,
111467
+ residueDirectiveCount: residueRows.length,
111468
+ totalDirectiveCount: rows.length,
111469
+ residueBytes,
111470
+ residueTokensEstimate: estimateTokens(residueBytes)
111471
+ };
111472
+ }
111473
+
111474
+ // src/memory/directive-flip.ts
111475
+ init_rules_block();
111476
+ function evaluateFlipReadiness(measurement, opts) {
111477
+ const reasons = [];
111478
+ if (!opts.rulesBlockEnabled) {
111479
+ reasons.push("memory.rules_block (M1) is not enabled \u2014 enable the rules-block " + "toolchain and migrate the directives into the block BEFORE flipping " + "inject_directives off (two-flag ordering; flipping first strips every " + "guardrail).");
111480
+ }
111481
+ if (measurement.residueBytes > RULES_BLOCK_BUDGET_BYTES) {
111482
+ reasons.push(`directive residue is ${measurement.residueBytes}B, over the ` + `${RULES_BLOCK_BUDGET_BYTES}B rules-block budget (` + `${measurement.residueDirectiveCount} active residue directive(s)). ` + "Triage the directive set down \u2014 retire stale rules or shorten bodies \u2014 " + "until the residue fits, then re-check. The budget, not MAX_DIRECTIVES, " + "is the binding gate.");
111483
+ }
111484
+ return {
111485
+ agent: measurement.agent,
111486
+ ready: reasons.length === 0,
111487
+ reasons,
111488
+ residueBytes: measurement.residueBytes,
111489
+ budgetBytes: RULES_BLOCK_BUDGET_BYTES,
111490
+ residueDirectiveCount: measurement.residueDirectiveCount
111491
+ };
111492
+ }
111493
+ async function measureLiveResidue(admin, agent) {
111494
+ const directives = await admin.list();
111495
+ const rows = buildDirectiveTriageRows(directives);
111496
+ const byId = new Map(directives.map((d) => [d.id, d]));
111497
+ return measureDirectiveResidue(agent, rows, byId);
111498
+ }
111499
+ function flipConfigStanza(agent) {
111500
+ return [
111501
+ `# ${agent} \u2014 Memory v2 M3 Surface-A flip (suppress <active_directives>)`,
111502
+ "memory:",
111503
+ " rules_block: true # M1 must already be on (checked)",
111504
+ " inject_directives: false # M3: stop re-injecting migrated directives"
111505
+ ].join(`
111506
+ `);
111507
+ }
111508
+
111509
+ // src/cli/memory-flip.ts
111510
+ function resolveFlipAdmin(program3, agent) {
111511
+ const config = getConfig(program3);
111512
+ if (!config.agents[agent]) {
111513
+ console.error(source_default.red(`Agent "${agent}" is not defined in switchroom.yaml`));
111514
+ process.exit(1);
111515
+ }
111516
+ if (!isHindsightEnabled(config)) {
111517
+ console.error(source_default.red("Hindsight memory is not enabled in this switchroom.yaml."));
111518
+ process.exit(1);
111519
+ }
111520
+ const agentConfig = config.agents[agent];
111521
+ const mcpBaseUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
111522
+ const apiBaseUrl = mcpBaseUrl.replace(/\/mcp\/?$/, "").replace(/\/+$/, "");
111523
+ const bankId = agentConfig?.memory?.collection ?? agent;
111524
+ return new DirectiveAdmin({ apiBaseUrl, bankId });
111525
+ }
111526
+ function readRulesBlockEnabled(program3, agent) {
111527
+ const config = getConfig(program3);
111528
+ return config.agents[agent]?.memory?.rules_block === true;
111529
+ }
111530
+ function buildFlipPreflightJson(readiness, rulesBlockEnabled) {
111531
+ return {
111532
+ ok: true,
111533
+ agent: readiness.agent,
111534
+ ready: readiness.ready,
111535
+ rules_block: rulesBlockEnabled,
111536
+ residue_bytes: readiness.residueBytes,
111537
+ budget_bytes: readiness.budgetBytes,
111538
+ fits_budget: readiness.residueBytes <= readiness.budgetBytes,
111539
+ residue_directive_count: readiness.residueDirectiveCount,
111540
+ reasons: readiness.reasons
111541
+ };
111542
+ }
111543
+ function renderFlipPreflight(readiness, rulesBlockEnabled) {
111544
+ const fits = readiness.residueBytes <= readiness.budgetBytes;
111545
+ const lines = [];
111546
+ const verdict = readiness.ready ? source_default.green("READY") : source_default.red("NOT-READY");
111547
+ lines.push(`${verdict} \u2014 ${readiness.agent} M3 flip preflight`);
111548
+ lines.push("");
111549
+ lines.push(` residue: ${readiness.residueBytes}B / ${readiness.budgetBytes}B budget ` + `(${readiness.residueDirectiveCount} active residue directive(s)) ` + (fits ? source_default.green("fits") : source_default.red("OVER budget")));
111550
+ lines.push(` rules_block: ${rulesBlockEnabled ? source_default.green("on") : source_default.red("off")}`);
111551
+ if (readiness.ready) {
111552
+ lines.push("");
111553
+ lines.push(" Apply this per-agent stanza to switchroom.yaml, then restart:");
111554
+ lines.push("");
111555
+ for (const l of flipConfigStanza(readiness.agent).split(`
111556
+ `)) {
111557
+ lines.push(` ${l}`);
111558
+ }
111559
+ } else {
111560
+ lines.push("");
111561
+ lines.push(source_default.red(" Blocked:"));
111562
+ for (const reason of readiness.reasons) {
111563
+ lines.push(` - ${reason}`);
111564
+ }
111565
+ }
111566
+ return lines.join(`
111567
+ `);
111568
+ }
111569
+ function registerMemoryFlipCommand(memory, program3, deps = {}) {
111570
+ const makeAdmin = deps.makeAdmin ?? resolveFlipAdmin;
111571
+ memory.command("flip-preflight <agent>").description("Deterministic M3 (Surface-A) flip-readiness gate for <agent>: measure " + "the live post-triage directive residue, check it fits the 6144B " + "rules-block budget AND that memory.rules_block is already on, and " + "print READY + the exact switchroom.yaml stanza, or NOT-READY + why. " + "Never mutates config \u2014 the flip itself is an operator-gated edit.").option("--json", "Machine-readable output").action(withConfigError(async (agent, opts) => {
111572
+ const admin = makeAdmin(program3, agent);
111573
+ const rulesBlockEnabled = readRulesBlockEnabled(program3, agent);
111574
+ let ready;
111575
+ try {
111576
+ const measurement = await measureLiveResidue(admin, agent);
111577
+ const readiness = evaluateFlipReadiness(measurement, { rulesBlockEnabled });
111578
+ ready = readiness.ready;
111579
+ if (opts.json) {
111580
+ console.log(JSON.stringify(buildFlipPreflightJson(readiness, rulesBlockEnabled)));
111581
+ } else {
111582
+ console.log(renderFlipPreflight(readiness, rulesBlockEnabled));
111583
+ }
111584
+ } catch (e) {
111585
+ const msg = e instanceof Error ? e.message : String(e);
111586
+ if (opts.json) {
111587
+ console.log(JSON.stringify({ ok: false, error: msg }));
111588
+ } else {
111589
+ console.error(source_default.red(`\u2717 flip preflight for ${agent} failed: ${msg}`));
111590
+ }
111591
+ process.exit(1);
111592
+ return;
111593
+ }
111594
+ if (!ready)
111595
+ process.exit(2);
111596
+ }));
111597
+ }
111598
+
111599
+ // src/cli/memory.ts
111600
+ function persistMemoryConfigUrl(configPath, provider, url) {
111601
+ if (!existsSync83(configPath))
111602
+ return false;
111603
+ const raw = readFileSync75(configPath, "utf-8");
111604
+ const doc = import_yaml16.default.parseDocument(raw);
111605
+ if (!doc.has("memory")) {
111606
+ doc.set("memory", { backend: "hindsight", shared_collection: "shared", config: { provider, url } });
111607
+ } else {
111608
+ const memNode = doc.get("memory");
111609
+ if (!memNode.has("config")) {
111610
+ memNode.set("config", { provider, url });
111611
+ } else {
111612
+ const configNode = memNode.get("config");
111613
+ configNode.set("url", url);
111614
+ if (!configNode.has("provider")) {
111615
+ configNode.set("provider", provider);
111616
+ }
111617
+ }
111618
+ }
111619
+ let mode = 420;
111620
+ try {
111621
+ mode = statSync49(configPath).mode & 511;
111622
+ } catch {}
111623
+ writeConfigFileSync(configPath, doc.toString(), mode);
111624
+ return true;
111625
+ }
111626
+ function resolveMemorySetupTag(input) {
111627
+ const explicit = input.explicitTag?.trim();
111628
+ if (explicit) {
111629
+ if (explicit.toLowerCase() === "latest") {
111630
+ return { tag: undefined, reason: "explicit-latest" };
111631
+ }
111632
+ const normalized = normalizeHindsightVersionTag(explicit);
111633
+ if (!normalized)
111634
+ return { tag: explicit, reason: "invalid" };
111635
+ return { tag: normalized, reason: "explicit" };
111636
+ }
111637
+ const pin = normalizeHindsightVersionTag(input.releasePin);
111638
+ if (pin)
111639
+ return { tag: pin, reason: "pin" };
111640
+ return { tag: undefined, reason: "latest" };
111641
+ }
111642
+ function hindsightSecretValues(input) {
111643
+ const out = [];
111644
+ const push = (v) => {
111645
+ if (typeof v === "string" && v.trim().length > 0)
111646
+ out.push(v.trim());
111647
+ };
111648
+ push(input.cpAccessKey);
111649
+ push(input.llm?.api_key);
111650
+ for (const op of ["retain", "reflect", "consolidation"]) {
111651
+ push(input.llm?.[op]?.api_key);
111652
+ }
111653
+ return out;
111654
+ }
111655
+ function hasUnresolvedHindsightVaultRef(config) {
111656
+ const hindsight = config.hindsight;
111657
+ return hindsightSecretValues({
111658
+ llm: hindsight?.llm,
111659
+ cpAccessKey: hindsight?.cp_access_key
111660
+ }).some((v) => isVaultReference(v));
111661
+ }
111662
+ function emitsCleartextHindsightSecret(input) {
111663
+ return hindsightSecretValues(input).some((v) => !isVaultReference(v));
111664
+ }
111665
+ var HINDSIGHT_COMPOSE_CLEARTEXT_SECRETS_WARNING = "This snippet embeds secrets in CLEARTEXT (LLM api_key and/or cp_access_key)." + " Treat the output as sensitive \u2014 do not paste it into shared channels or" + " commit it unredacted.";
111666
+ async function resolveLiteLLMForHindsight(config) {
111667
+ return resolveHindsightLiteLlm(config);
111668
+ }
111669
+ function readRecallLog(agentDir, limit) {
111670
+ const path9 = join89(agentDir, ".claude", "plugins", "data", "hindsight-memory-inline", "state", "recall_log.jsonl");
111671
+ if (!existsSync83(path9))
111672
+ return [];
111673
+ let raw;
111674
+ try {
111675
+ raw = readFileSync75(path9, "utf-8");
111676
+ } catch {
111677
+ return [];
111678
+ }
111679
+ const lines = raw.split(`
111680
+ `).filter((l) => l.trim().length > 0);
111681
+ const tail = lines.slice(-limit);
111682
+ const out = [];
111683
+ for (const line of tail) {
111684
+ try {
111685
+ out.push(JSON.parse(line));
111686
+ } catch {}
111687
+ }
111688
+ return out;
111689
+ }
111690
+ function formatRecallLogView(name, entries) {
111691
+ const lines = [source_default.bold(`
111692
+ ${name}:`)];
111693
+ const total = entries.length;
111694
+ const hits = entries.filter((e) => e.cache_hit).length;
111695
+ const cappedTurns = entries.filter((e) => e.capped).length;
111696
+ const omittedTurns = entries.filter((e) => typeof e.directives_omitted === "number" && e.directives_omitted > 0).length;
111697
+ const memCounts = entries.map((e) => e.result_count).filter((n) => typeof n === "number");
111698
+ const avg = memCounts.length > 0 ? Math.round(memCounts.reduce((s, n) => s + n, 0) / memCounts.length * 10) / 10 : null;
111699
+ const max = memCounts.length > 0 ? Math.max(...memCounts) : null;
111700
+ const medians = entries.map((e) => e.injected_score_median).filter((n) => typeof n === "number");
111701
+ const avgScore = medians.length > 0 ? Math.round(medians.reduce((s, n) => s + n, 0) / medians.length * 100) / 100 : null;
111702
+ const collapseTurns = entries.filter((e) => typeof e.injected_own_bank_count === "number" && e.injected_own_bank_count === 0 && typeof e.injected_additional_bank_count === "number" && e.injected_additional_bank_count > 0).length;
111703
+ lines.push(source_default.gray(` last ${total} turn${total === 1 ? "" : "s"}: ` + `avg=${avg ?? "\u2014"} max=${max ?? "\u2014"} ` + `avg_score=${avgScore ?? "\u2014"} ` + `cache_hits=${hits} capped=${cappedTurns}` + (omittedTurns > 0 ? ` directives_omitted_turns=${omittedTurns}` : "") + (collapseTurns > 0 ? source_default.red(` own_bank_collapse_turns=${collapseTurns}`) : "")));
111704
+ for (const e of entries) {
111705
+ const flag = e.cache_hit ? source_default.cyan("CACHE") : e.capped ? source_default.yellow("CAP") : source_default.green("OK");
111706
+ const dem = e.demoted_count && e.demoted_count > 0 ? source_default.dim(` -${e.demoted_count}d`) : "";
111707
+ const omitted = typeof e.directives_omitted === "number" && e.directives_omitted > 0 ? source_default.yellow(` +${e.directives_omitted}omitted`) : "";
111708
+ const ids = e.memory_ids && e.memory_ids.length > 0 ? source_default.dim(` ids=${e.memory_ids.slice(0, 3).join(",")}${e.memory_ids.length > 3 ? `\u2026+${e.memory_ids.length - 3}` : ""}`) : "";
111709
+ const own = e.injected_own_bank_count;
111710
+ const additional = e.injected_additional_bank_count;
111711
+ const composition = typeof own === "number" || typeof additional === "number" ? (() => {
111712
+ const text = ` own/add=${own ?? "\u2014"}/${additional ?? "\u2014"}`;
111713
+ return own === 0 && typeof additional === "number" && additional > 0 ? source_default.red(text) : source_default.dim(text);
111714
+ })() : "";
111715
+ const score = typeof e.injected_score_min === "number" && typeof e.injected_score_median === "number" && typeof e.injected_score_max === "number" ? source_default.dim(` score=${e.injected_score_min.toFixed(2)}/` + `${e.injected_score_median.toFixed(2)}/` + `${e.injected_score_max.toFixed(2)}`) : "";
111716
+ lines.push(` ${source_default.gray(e.ts)} ${flag} ` + `n=${e.result_count ?? "\u2014"}${e.pre_cap_count != null && e.pre_cap_count !== e.result_count ? `/${e.pre_cap_count}` : ""}` + `${dem}${omitted}${score}${composition}${ids}`);
111717
+ }
111718
+ return lines;
111719
+ }
111720
+ function registerMemoryCommand(program3) {
111721
+ const memory = program3.command("memory").description("Hindsight memory operations");
111722
+ memory.command("search <query>").description("Search agent memories via Hindsight").option("-a, --agent <name>", "Search a specific agent's collection").action(withConfigError(async (query, opts) => {
111723
+ const config = getConfig(program3);
111724
+ if (opts.agent) {
111725
+ if (!config.agents[opts.agent]) {
111726
+ console.error(source_default.red(`Agent "${opts.agent}" is not defined in switchroom.yaml`));
111727
+ process.exit(1);
111728
+ }
111729
+ const collection = getCollectionForAgent(opts.agent, config);
111730
+ console.log(source_default.bold(`
111731
+ Search: ${opts.agent} (collection: ${collection})
111732
+ `));
111733
+ console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
111734
+ console.log();
111735
+ return;
111736
+ }
111737
+ const agentNames = Object.keys(config.agents);
111738
+ console.log(source_default.bold(`
111739
+ Searching all eligible collections:
111740
+ `));
111741
+ for (const name of agentNames) {
111742
+ const collection = getCollectionForAgent(name, config);
111743
+ if (isStrictIsolation(name, config)) {
111744
+ console.log(source_default.gray(` ${name} (${collection}) \u2014 skipped (strict isolation)`));
111745
+ continue;
111746
+ }
111747
+ console.log(source_default.cyan(` ${name} (${collection}):`));
111748
+ console.log(source_default.gray(` $ ${searchMemory(query, collection)}`));
111749
+ }
111750
+ console.log();
111751
+ }));
111752
+ memory.command("stats").description("List agents with their collection names and isolation mode").action(withConfigError(async () => {
111753
+ const config = getConfig(program3);
111754
+ const agentNames = Object.keys(config.agents);
111755
+ if (agentNames.length === 0) {
111756
+ console.log(source_default.yellow("No agents defined in switchroom.yaml"));
111757
+ return;
111758
+ }
111759
+ const headers = ["Agent", "Collection", "Isolation", "Auto-recall"];
111760
+ const widths = [20, 20, 12, 12];
111761
+ const headerLine = headers.map((h, i2) => source_default.bold(h.padEnd(widths[i2]))).join(" ");
111762
+ console.log(`
111763
+ ${headerLine}`);
111764
+ for (const name of agentNames) {
111765
+ const collection = getCollectionForAgent(name, config);
111766
+ const isolation = isStrictIsolation(name, config) ? "strict" : "default";
111767
+ const autoRecall = config.agents[name].memory?.auto_recall ?? true;
111768
+ const row = [
111769
+ name.padEnd(widths[0]),
111770
+ collection.padEnd(widths[1]),
111771
+ isolation.padEnd(widths[2]),
111772
+ (autoRecall ? "yes" : "no").padEnd(widths[3])
111773
+ ].join(" ");
111774
+ console.log(` ${row}`);
111775
+ }
111776
+ console.log();
111777
+ const profileBanks = collectProfileBanks(config);
111778
+ if (profileBanks.size > 0) {
111779
+ const owners = new Map;
111780
+ for (const [uname, u] of Object.entries(config.users ?? {})) {
111781
+ if (u.profile_bank) {
111782
+ owners.set(u.profile_bank, [
111783
+ ...owners.get(u.profile_bank) ?? [],
111784
+ uname
111785
+ ]);
111786
+ }
111787
+ }
111788
+ console.log(source_default.bold(` Profile banks (per-user / shared):
111789
+ `));
111790
+ for (const bank of [...profileBanks].sort()) {
111791
+ const who = owners.get(bank);
111792
+ const label = who ? `user: ${who.join(", ")}` : "shared";
111793
+ console.log(` ${bank.padEnd(widths[1])} ${source_default.gray(label)}`);
111794
+ }
111795
+ console.log();
111796
+ }
111797
+ console.log(source_default.bold(` Hindsight CLI commands:
111798
+ `));
111799
+ for (const name of agentNames) {
111800
+ const collection = getCollectionForAgent(name, config);
111801
+ console.log(source_default.gray(` $ ${getMemoryStats(collection)}`));
111802
+ }
111803
+ console.log();
111804
+ }));
111805
+ memory.command("reflect").description("Show cross-agent reflection plan").action(withConfigError(async () => {
111806
+ const config = getConfig(program3);
111807
+ const { eligible, excluded, commands } = reflectAcrossAgents(config);
111808
+ console.log(source_default.bold(`
111809
+ Cross-agent reflection plan
111810
+ `));
111811
+ if (eligible.length > 0) {
111812
+ console.log(source_default.green(" Eligible collections:"));
111813
+ for (const { agent, collection } of eligible) {
111814
+ console.log(source_default.white(` ${agent} -> ${collection}`));
111815
+ }
111816
+ }
111817
+ if (excluded.length > 0) {
111818
+ console.log(source_default.red(`
111819
+ Excluded (strict isolation):`));
111820
+ for (const { agent, collection } of excluded) {
111821
+ console.log(source_default.gray(` ${agent} -> ${collection}`));
111822
+ }
111823
+ }
111824
+ if (commands.length > 0) {
111825
+ console.log(source_default.bold(`
111826
+ Hindsight CLI commands:
111827
+ `));
111828
+ for (const cmd of commands) {
111829
+ console.log(source_default.gray(` $ ${cmd}`));
111830
+ }
111831
+ } else {
111832
+ console.log(source_default.yellow(`
111833
+ No eligible collections for reflection.`));
111834
+ }
111835
+ console.log();
111836
+ }));
111837
+ memory.command("setup").description("Manage the Hindsight Docker container").option("--stop", "Stop and remove the Hindsight container").option("--status", "Show Hindsight container status").option("--recreate", "Pull the latest image and recreate the container (reusing its current port). Used by `switchroom update` to keep the hindsight singleton current. Also required after any `hindsight.llm` model/provider config change \u2014 env is only re-derived on recreate, a plain docker restart is not enough (see docs/operators/hindsight-model-change.md).").option("--tag <version>", "Pin the hindsight image tag to pull + run (e.g. v0.15.18), overriding the " + "default floating `:latest`. Threaded through by `switchroom rollout` so a " + "version-pinned roll recreates hindsight on the SAME tag as the rest of the " + "fleet. Omit for `:latest` (the standalone default).").option("--provider <provider>", "LLM provider (ollama, openai, anthropic)").option("--strict-env", "Refuse the recreate (before the running container is touched) if it would drop " + "env vars that are live on the container but declared in neither `hindsight.env` " + "nor switchroom's managed defaults. Default is to warn loudly and proceed, so a " + "rollout is never wedged half-way with memory down.").option("--gpu", "Force GPU passthrough on for this launch, overriding host autodetection. " + "Use when `~/.switchroom/host-capabilities.json` is wrong or unreadable and " + "you know the host has a working nvidia container toolkit (`docker run --gpus " + "all` hard-fails container create without one). Beats `hindsight.gpu`.").option("--no-gpu", "Force GPU passthrough off for this launch, overriding host autodetection. " + "Also the explicit opt-out for the recreate GPU drop guard.").option("--allow-gpu-drop", "Proceed with a --recreate that would take GPU passthrough away from the " + "running container. Without this the recreate refuses, because that drop " + "moves the embedding model and cross-encoder reranker to CPU on the " + "interactive recall path with no other signal.").action(async (opts) => {
111838
+ if (opts.status) {
111839
+ if (!isDockerAvailable()) {
111840
+ console.log(source_default.red(" Docker is not available."));
111841
+ process.exit(1);
111842
+ }
111843
+ const status = getHindsightStatus();
111844
+ if (status) {
111845
+ console.log(source_default.bold(`
111846
+ Hindsight container status:`));
111847
+ console.log(` ${source_default.cyan("switchroom-hindsight")}: ${status}
111848
+ `);
111849
+ } else {
111850
+ console.log(source_default.yellow(`
111851
+ Hindsight container not found.
111852
+ `));
111853
+ console.log(source_default.gray(" Run 'switchroom memory setup' to start it."));
111854
+ }
111855
+ return;
111856
+ }
111857
+ if (opts.stop) {
111858
+ if (!isDockerAvailable()) {
111859
+ console.log(source_default.red(" Docker is not available."));
111860
+ process.exit(1);
111861
+ }
111862
+ if (!isHindsightContainerExists()) {
111863
+ console.log(source_default.yellow(" No switchroom-hindsight container found."));
111864
+ return;
111865
+ }
111866
+ console.log(source_default.gray(" Stopping switchroom-hindsight..."));
111867
+ stopHindsightRecallPool();
111868
+ stopHindsight();
111869
+ console.log(source_default.green(" Hindsight container stopped and removed."));
111870
+ return;
111871
+ }
111872
+ if (!isDockerAvailable()) {
111873
+ console.log(source_default.red(`
111874
+ Docker is not available.`));
111875
+ console.log(source_default.gray(` Install Docker: https://docs.docker.com/get-docker/
111876
+ `));
111877
+ process.exit(1);
111878
+ }
111879
+ const recreate = opts.recreate === true;
111880
+ let resolvedLlmPromise;
111881
+ const getResolvedLlm = () => {
111882
+ if (!resolvedLlmPromise) {
111883
+ resolvedLlmPromise = resolveHindsightLlmSecrets(getConfig(program3).hindsight?.llm);
111884
+ }
111885
+ return resolvedLlmPromise;
111886
+ };
111887
+ let releasePin;
111888
+ try {
111889
+ releasePin = getConfig(program3).release?.pin ?? undefined;
111890
+ } catch {
111891
+ releasePin = undefined;
111892
+ }
111893
+ const { tag: effectiveTag, reason: tagReason } = resolveMemorySetupTag({
111894
+ explicitTag: opts.tag,
111895
+ releasePin
111896
+ });
111897
+ switch (tagReason) {
110828
111898
  case "invalid":
110829
111899
  console.error(source_default.red(`
110830
111900
  Invalid hindsight image tag: ${effectiveTag}
@@ -111366,9 +112436,9 @@ Repairing vector index coverage for ${target}${opts.dryRun ? source_default.yell
111366
112436
  captured += chunk2.toString();
111367
112437
  process.stdout.write(chunk2);
111368
112438
  });
111369
- const childExit = await new Promise((resolve43) => {
111370
- child.on("error", () => resolve43(127));
111371
- child.on("close", (c) => resolve43(c ?? 1));
112439
+ const childExit = await new Promise((resolve44) => {
112440
+ child.on("error", () => resolve44(127));
112441
+ child.on("close", (c) => resolve44(c ?? 1));
111372
112442
  });
111373
112443
  const outcome = classifyRepairOutcome({
111374
112444
  childExit,
@@ -111393,7 +112463,7 @@ Repairing vector index coverage for ${target}${opts.dryRun ? source_default.yell
111393
112463
  process.exit(1);
111394
112464
  })() : Object.keys(config.agents);
111395
112465
  for (const name of targets) {
111396
- const agentDir = join88(agentsDir, name);
112466
+ const agentDir = join89(agentsDir, name);
111397
112467
  const entries = readRecallLog(agentDir, limit);
111398
112468
  if (opts.json) {
111399
112469
  for (const e of entries) {
@@ -111586,11 +112656,12 @@ Observation-scope cleanup \u2014 DRY RUN (no writes)
111586
112656
  }));
111587
112657
  registerMemoryRuleCommand(memory, program3);
111588
112658
  registerMemoryDirectiveCommand(memory, program3);
112659
+ registerMemoryFlipCommand(memory, program3);
111589
112660
  }
111590
112661
 
111591
112662
  // src/cli/web.ts
111592
112663
  init_source();
111593
- import { join as join100 } from "node:path";
112664
+ import { join as join101 } from "node:path";
111594
112665
  import { homedir as homedir50 } from "node:os";
111595
112666
 
111596
112667
  // src/web/server.ts
@@ -111598,8 +112669,8 @@ init_merge();
111598
112669
  init_loader();
111599
112670
  init_client();
111600
112671
  import {
111601
- readFileSync as readFileSync82,
111602
- existsSync as existsSync90,
112672
+ readFileSync as readFileSync83,
112673
+ existsSync as existsSync91,
111603
112674
  realpathSync as realpathSync7,
111604
112675
  mkdirSync as mkdirSync50,
111605
112676
  openSync as openSync17,
@@ -111607,7 +112678,7 @@ import {
111607
112678
  writeSync as writeSync8,
111608
112679
  constants as fsConstants6
111609
112680
  } from "node:fs";
111610
- import { resolve as resolve47, extname, join as join99, relative as relative5, dirname as dirname36 } from "node:path";
112681
+ import { resolve as resolve48, extname, join as join100, relative as relative5, dirname as dirname36 } from "node:path";
111611
112682
  import { homedir as homedir49 } from "node:os";
111612
112683
  import { timingSafeEqual as timingSafeEqual3, randomBytes as randomBytes15 } from "node:crypto";
111613
112684
 
@@ -111616,8 +112687,8 @@ init_lifecycle();
111616
112687
  init_manager();
111617
112688
  init_hindsight2();
111618
112689
  import { spawnSync as spawnSync17 } from "node:child_process";
111619
- import { existsSync as existsSync86, readFileSync as readFileSync77, statSync as statSync51 } from "node:fs";
111620
- import { resolve as resolve44, join as join95 } from "node:path";
112690
+ import { existsSync as existsSync87, readFileSync as readFileSync78, statSync as statSync52 } from "node:fs";
112691
+ import { resolve as resolve45, join as join96 } from "node:path";
111621
112692
 
111622
112693
  // src/web/memory-remediation.ts
111623
112694
  init_hindsight2();
@@ -111685,15 +112756,15 @@ async function buildUserProfile(mcpUrl, bank, opts) {
111685
112756
  init_hindsight();
111686
112757
 
111687
112758
  // src/web/blocked-approvals-read.ts
111688
- import { readFileSync as readFileSync75, readdirSync as readdirSync30 } from "node:fs";
111689
- import { resolve as resolve43, join as join89 } from "node:path";
112759
+ import { readFileSync as readFileSync76, readdirSync as readdirSync31 } from "node:fs";
112760
+ import { resolve as resolve44, join as join90 } from "node:path";
111690
112761
  import { homedir as homedir42 } from "node:os";
111691
112762
  function blockedApprovalsDir(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir42()) {
111692
- return resolve43(home2, ".switchroom", "blocked-approvals");
112763
+ return resolve44(home2, ".switchroom", "blocked-approvals");
111693
112764
  }
111694
112765
  var FALLBACK_RECORD_NAME = "blocked-approval.json";
111695
112766
  function agentStateRoot(sharedDir) {
111696
- return resolve43(sharedDir, "..", "agents");
112767
+ return resolve44(sharedDir, "..", "agents");
111697
112768
  }
111698
112769
  function finiteOrNull(v) {
111699
112770
  const n = Number(v);
@@ -111732,7 +112803,7 @@ function readBlockedApprovalsWithErrors(dir, agentsRoot = agentStateRoot(dir)) {
111732
112803
  function readSharedRecords(dir) {
111733
112804
  let entries;
111734
112805
  try {
111735
- entries = readdirSync30(dir);
112806
+ entries = readdirSync31(dir);
111736
112807
  } catch (err) {
111737
112808
  if (err?.code === "ENOENT") {
111738
112809
  return { blocked: [], unreadable: 0 };
@@ -111746,7 +112817,7 @@ function readSharedRecords(dir) {
111746
112817
  continue;
111747
112818
  let text;
111748
112819
  try {
111749
- text = readFileSync75(join89(dir, name), "utf-8");
112820
+ text = readFileSync76(join90(dir, name), "utf-8");
111750
112821
  } catch {
111751
112822
  unreadable++;
111752
112823
  continue;
@@ -111765,7 +112836,7 @@ function readSharedRecords(dir) {
111765
112836
  function readFallbackRecords(agentsRoot, alreadySeen) {
111766
112837
  let agents;
111767
112838
  try {
111768
- agents = readdirSync30(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
112839
+ agents = readdirSync31(agentsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
111769
112840
  } catch (err) {
111770
112841
  if (err?.code === "ENOENT") {
111771
112842
  return { blocked: [], unreadable: 0 };
@@ -111779,7 +112850,7 @@ function readFallbackRecords(agentsRoot, alreadySeen) {
111779
112850
  continue;
111780
112851
  let text;
111781
112852
  try {
111782
- text = readFileSync75(join89(agentsRoot, agent, FALLBACK_RECORD_NAME), "utf-8");
112853
+ text = readFileSync76(join90(agentsRoot, agent, FALLBACK_RECORD_NAME), "utf-8");
111783
112854
  } catch (err) {
111784
112855
  if (err?.code !== "ENOENT")
111785
112856
  unreadable++;
@@ -111826,7 +112897,7 @@ init_audit_reader();
111826
112897
  // src/scheduler/dispatch.ts
111827
112898
  init_merge();
111828
112899
  init_overlay_loader();
111829
- import { createHash as createHash15 } from "node:crypto";
112900
+ import { createHash as createHash16 } from "node:crypto";
111830
112901
  function collectScheduleEntries(config) {
111831
112902
  const out = [];
111832
112903
  const agentNames = Object.keys(config.agents).sort();
@@ -111846,7 +112917,7 @@ function collectScheduleEntries(config) {
111846
112917
  cron: entry.cron,
111847
112918
  ...typeof title === "string" && title.length > 0 ? { name: title } : {},
111848
112919
  ...entry.prompt !== undefined ? { prompt: entry.prompt } : {},
111849
- promptKey: createHash15("sha256").update(auditMaterial).digest("hex").slice(0, 12),
112920
+ promptKey: createHash16("sha256").update(auditMaterial).digest("hex").slice(0, 12),
111850
112921
  ...entry.topic !== undefined ? { topic: entry.topic } : {},
111851
112922
  ...entry.kind !== undefined ? { kind: entry.kind } : {},
111852
112923
  ...entry.model !== undefined ? { model: entry.model } : {},
@@ -111895,7 +112966,7 @@ import { homedir as homedir45 } from "node:os";
111895
112966
  // src/web/config-edit-plan.ts
111896
112967
  init_schema();
111897
112968
  var import_yaml17 = __toESM(require_dist(), 1);
111898
- import { readFileSync as readFileSync76 } from "node:fs";
112969
+ import { readFileSync as readFileSync77 } from "node:fs";
111899
112970
 
111900
112971
  class ConfigPlanError extends Error {
111901
112972
  }
@@ -111905,7 +112976,7 @@ function composeTransforms(...transforms) {
111905
112976
  function planConfigEdit(configPath, transform) {
111906
112977
  let before;
111907
112978
  try {
111908
- before = readFileSync76(configPath, "utf-8");
112979
+ before = readFileSync77(configPath, "utf-8");
111909
112980
  } catch (err) {
111910
112981
  throw new ConfigPlanError(`could not read config: ${err.message}`);
111911
112982
  }
@@ -111940,19 +113011,19 @@ import {
111940
113011
  } from "node:fs";
111941
113012
  import { spawnSync as spawnSync16 } from "node:child_process";
111942
113013
  import { tmpdir as tmpdir5 } from "node:os";
111943
- import { join as join91 } from "node:path";
113014
+ import { join as join92 } from "node:path";
111944
113015
 
111945
113016
  class ConfigDiffError extends Error {
111946
113017
  }
111947
113018
  function generateUnifiedDiff(before, after, name = "switchroom.yaml", gitBin = "git") {
111948
113019
  if (before === after)
111949
113020
  return "";
111950
- const dir = mkdtemp(join91(tmpdir5(), "switchroom-config-diff-"));
113021
+ const dir = mkdtemp(join92(tmpdir5(), "switchroom-config-diff-"));
111951
113022
  try {
111952
- mkdirSync47(join91(dir, "cur"), { recursive: true });
111953
- mkdirSync47(join91(dir, "new"), { recursive: true });
111954
- writeFileSync32(join91(dir, "cur", name), before);
111955
- writeFileSync32(join91(dir, "new", name), after);
113023
+ mkdirSync47(join92(dir, "cur"), { recursive: true });
113024
+ mkdirSync47(join92(dir, "new"), { recursive: true });
113025
+ writeFileSync32(join92(dir, "cur", name), before);
113026
+ writeFileSync32(join92(dir, "new", name), after);
111956
113027
  const r = spawnSync16(gitBin, ["diff", "--no-index", "--no-color", "--", `cur/${name}`, `new/${name}`], { cwd: dir, encoding: "utf-8", timeout: 1e4 });
111957
113028
  if (r.status === 0)
111958
113029
  return "";
@@ -111984,9 +113055,9 @@ function generateUnifiedDiff(before, after, name = "switchroom.yaml", gitBin = "
111984
113055
 
111985
113056
  // src/web/hostd-config-propose.ts
111986
113057
  init_client5();
111987
- import { existsSync as existsSync84 } from "node:fs";
113058
+ import { existsSync as existsSync85 } from "node:fs";
111988
113059
  import { homedir as homedir44 } from "node:os";
111989
- import { join as join92 } from "node:path";
113060
+ import { join as join93 } from "node:path";
111990
113061
  var PROPOSE_TIMEOUT_MS = 61 * 60 * 1000;
111991
113062
  function resolveHostdOperatorSocket(env2 = process.env) {
111992
113063
  const override = env2.SWITCHROOM_HOSTD_OPERATOR_SOCKET;
@@ -111994,10 +113065,10 @@ function resolveHostdOperatorSocket(env2 = process.env) {
111994
113065
  return override;
111995
113066
  const candidates = [
111996
113067
  "/host-home/.switchroom/hostd/operator/sock",
111997
- join92(homedir44(), ".switchroom", "hostd", "operator", "sock")
113068
+ join93(homedir44(), ".switchroom", "hostd", "operator", "sock")
111998
113069
  ];
111999
113070
  for (const c of candidates) {
112000
- if (existsSync84(c))
113071
+ if (existsSync85(c))
112001
113072
  return c;
112002
113073
  }
112003
113074
  return null;
@@ -112205,7 +113276,7 @@ init_client2();
112205
113276
 
112206
113277
  // telegram-plugin/registry/turns-schema.ts
112207
113278
  import { chmodSync as chmodSync15 } from "fs";
112208
- import { join as join94 } from "path";
113279
+ import { join as join95 } from "path";
112209
113280
 
112210
113281
  // src/util/state-owner.ts
112211
113282
  init_atomic();
@@ -112213,28 +113284,28 @@ import {
112213
113284
  appendFileSync as appendFileSync4,
112214
113285
  closeSync as closeSync16,
112215
113286
  constants as constants2,
112216
- existsSync as existsSync85,
113287
+ existsSync as existsSync86,
112217
113288
  fchownSync as fchownSync2,
112218
113289
  fstatSync as fstatSync5,
112219
113290
  mkdirSync as mkdirSync48,
112220
113291
  openSync as openSync16,
112221
- readdirSync as readdirSync31,
112222
- statSync as statSync50,
113292
+ readdirSync as readdirSync32,
113293
+ statSync as statSync51,
112223
113294
  writeFileSync as writeFileSync33
112224
113295
  } from "node:fs";
112225
- import { dirname as dirname35, join as join93, relative as relative4, sep as sep6 } from "node:path";
113296
+ import { dirname as dirname35, join as join94, relative as relative4, sep as sep6 } from "node:path";
112226
113297
  var NOFOLLOW = constants2.O_NOFOLLOW ?? 0;
112227
113298
  var ownerCache = new Map;
112228
113299
  var loggedFailure = false;
112229
113300
  function defaultLog(line) {
112230
113301
  process.stderr.write(line);
112231
113302
  }
112232
- function noteFailure(op, path7, err, log) {
113303
+ function noteFailure(op, path9, err, log) {
112233
113304
  if (loggedFailure)
112234
113305
  return;
112235
113306
  loggedFailure = true;
112236
113307
  const code = err.code ?? "";
112237
- log(`switchroom state-owner: ${op} could not adopt directory ownership for ${path7}` + `${code ? ` (${code})` : ""}: ${err.message}. ` + `Continuing; state files may stay owned by the writing uid. ` + `Further ownership warnings this process are suppressed.
113308
+ log(`switchroom state-owner: ${op} could not adopt directory ownership for ${path9}` + `${code ? ` (${code})` : ""}: ${err.message}. ` + `Continuing; state files may stay owned by the writing uid. ` + `Further ownership warnings this process are suppressed.
112238
113309
  `);
112239
113310
  }
112240
113311
  function isMultiplyLinked(st) {
@@ -112248,7 +113319,7 @@ function resolveStateOwner(dir) {
112248
113319
  return cached;
112249
113320
  let owner = null;
112250
113321
  try {
112251
- const st = statSync50(dir);
113322
+ const st = statSync51(dir);
112252
113323
  owner = st.uid === 0 && st.gid === 0 ? null : { uid: st.uid, gid: st.gid };
112253
113324
  } catch {
112254
113325
  owner = null;
@@ -112256,13 +113327,13 @@ function resolveStateOwner(dir) {
112256
113327
  ownerCache.set(dir, owner);
112257
113328
  return owner;
112258
113329
  }
112259
- function adoptStateOwnership(path7, owner, log = defaultLog) {
112260
- const target = owner === undefined ? resolveStateOwner(dirname35(path7)) : owner;
113330
+ function adoptStateOwnership(path9, owner, log = defaultLog) {
113331
+ const target = owner === undefined ? resolveStateOwner(dirname35(path9)) : owner;
112261
113332
  if (!target)
112262
113333
  return;
112263
113334
  let fd = null;
112264
113335
  try {
112265
- fd = openSync16(path7, constants2.O_RDONLY | NOFOLLOW);
113336
+ fd = openSync16(path9, constants2.O_RDONLY | NOFOLLOW);
112266
113337
  const st = fstatSync5(fd);
112267
113338
  if (st.uid === target.uid && st.gid === target.gid)
112268
113339
  return;
@@ -112271,7 +113342,7 @@ function adoptStateOwnership(path7, owner, log = defaultLog) {
112271
113342
  fchownSync2(fd, target.uid, target.gid);
112272
113343
  } catch (err) {
112273
113344
  if (err.code !== "ENOENT") {
112274
- noteFailure("adopt", path7, err, log);
113345
+ noteFailure("adopt", path9, err, log);
112275
113346
  }
112276
113347
  } finally {
112277
113348
  if (fd !== null) {
@@ -112289,8 +113360,8 @@ function adoptSqliteOwnership(dbPath, log = defaultLog) {
112289
113360
  adoptStateOwnership(dbPath + suffix, owner, log);
112290
113361
  }
112291
113362
  }
112292
- function mkdirStateSync(path7, options = { recursive: true }, log = defaultLog) {
112293
- const first = mkdirSync48(path7, { ...options, recursive: true });
113363
+ function mkdirStateSync(path9, options = { recursive: true }, log = defaultLog) {
113364
+ const first = mkdirSync48(path9, { ...options, recursive: true });
112294
113365
  if (first === undefined)
112295
113366
  return;
112296
113367
  const owner = resolveStateOwner(dirname35(first));
@@ -112298,13 +113369,13 @@ function mkdirStateSync(path7, options = { recursive: true }, log = defaultLog)
112298
113369
  return;
112299
113370
  let current = first;
112300
113371
  adoptStateOwnership(current, owner, log);
112301
- const tail = relative4(first, path7);
113372
+ const tail = relative4(first, path9);
112302
113373
  if (tail === "" || tail.startsWith(".."))
112303
113374
  return;
112304
113375
  for (const part of tail.split(sep6)) {
112305
113376
  if (part === "")
112306
113377
  continue;
112307
- current = join93(current, part);
113378
+ current = join94(current, part);
112308
113379
  adoptStateOwnership(current, owner, log);
112309
113380
  }
112310
113381
  }
@@ -112381,20 +113452,20 @@ function applySchema(db) {
112381
113452
  }
112382
113453
  function openTurnsDb(agentDir) {
112383
113454
  const Database2 = loadDatabaseClass();
112384
- const dir = join94(agentDir, "telegram");
113455
+ const dir = join95(agentDir, "telegram");
112385
113456
  mkdirStateSync(dir, { recursive: true, mode: 448 });
112386
- const path7 = join94(dir, "registry.db");
112387
- const db = new Database2(path7, { create: true });
113457
+ const path9 = join95(dir, "registry.db");
113458
+ const db = new Database2(path9, { create: true });
112388
113459
  applySchema(db);
112389
113460
  try {
112390
- chmodSync15(path7, 420);
113461
+ chmodSync15(path9, 420);
112391
113462
  for (const suffix of ["-shm", "-wal"]) {
112392
113463
  try {
112393
- chmodSync15(path7 + suffix, 420);
113464
+ chmodSync15(path9 + suffix, 420);
112394
113465
  } catch {}
112395
113466
  }
112396
113467
  } catch {}
112397
- adoptSqliteOwnership(path7);
113468
+ adoptSqliteOwnership(path9);
112398
113469
  return db;
112399
113470
  }
112400
113471
  function mapRow(row) {
@@ -112596,7 +113667,7 @@ var DASHBOARD_CACHE_TTL = {
112596
113667
  };
112597
113668
  function readContextOccupancy(agentsDir, name) {
112598
113669
  try {
112599
- const raw = readFileSync77(resolve44(agentsDir, name, "context-occupancy.json"), "utf-8");
113670
+ const raw = readFileSync78(resolve45(agentsDir, name, "context-occupancy.json"), "utf-8");
112600
113671
  const s = JSON.parse(raw);
112601
113672
  if (typeof s?.occupancy !== "number")
112602
113673
  return null;
@@ -112607,7 +113678,7 @@ function readContextOccupancy(agentsDir, name) {
112607
113678
  }
112608
113679
  function readLastTurnAt(agentsDir, name) {
112609
113680
  try {
112610
- const db = openTurnsDb(resolve44(agentsDir, name));
113681
+ const db = openTurnsDb(resolve45(agentsDir, name));
112611
113682
  try {
112612
113683
  const turns = listTurnsForAgent(db, { limit: 1 });
112613
113684
  return turns.length > 0 ? turns[0].started_at : null;
@@ -112620,8 +113691,8 @@ function readLastTurnAt(agentsDir, name) {
112620
113691
  }
112621
113692
  function agentBridgeAlive(agentsDir, name, maxAgeMs = 30000, now2 = Date.now()) {
112622
113693
  try {
112623
- const f = resolve44(agentsDir, name, "telegram", ".bridge-alive");
112624
- return now2 - statSync51(f).mtimeMs <= maxAgeMs;
113694
+ const f = resolve45(agentsDir, name, "telegram", ".bridge-alive");
113695
+ return now2 - statSync52(f).mtimeMs <= maxAgeMs;
112625
113696
  } catch {
112626
113697
  return false;
112627
113698
  }
@@ -112719,7 +113790,7 @@ function handleGetTurns(config, sessionId, limit) {
112719
113790
  const agentName = parsed.agentName;
112720
113791
  const hasThread = "threadId" in parsed;
112721
113792
  const agentsDir = resolveAgentsDir(config);
112722
- const agentDir = resolve44(agentsDir, agentName);
113793
+ const agentDir = resolve45(agentsDir, agentName);
112723
113794
  const chatId = config.agents?.[agentName]?.channels?.telegram?.chat_id ?? undefined;
112724
113795
  const db = openTurnsDb(agentDir);
112725
113796
  try {
@@ -112742,7 +113813,7 @@ function handleListThreadIds(config, agentName) {
112742
113813
  if (!chatId)
112743
113814
  return [];
112744
113815
  const agentsDir = resolveAgentsDir(config);
112745
- const db = openTurnsDb(resolve44(agentsDir, agentName));
113816
+ const db = openTurnsDb(resolve45(agentsDir, agentName));
112746
113817
  try {
112747
113818
  return listDistinctThreadIds(db, chatId);
112748
113819
  } finally {
@@ -112755,7 +113826,7 @@ function handleListThreadIds(config, agentName) {
112755
113826
  function handleGetSubagents(config, agentName, status) {
112756
113827
  try {
112757
113828
  const agentsDir = resolveAgentsDir(config);
112758
- const agentDir = resolve44(agentsDir, agentName);
113829
+ const agentDir = resolve45(agentsDir, agentName);
112759
113830
  const db = openTurnsDb(agentDir);
112760
113831
  try {
112761
113832
  applySubagentsSchema(db);
@@ -113012,7 +114083,7 @@ async function handleGetSystemHealth(config, home2) {
113012
114083
  };
113013
114084
  try {
113014
114085
  const logPath = defaultAuditLogPath2(home2);
113015
- if (existsSync86(logPath)) {
114086
+ if (existsSync87(logPath)) {
113016
114087
  hostd.auditLogPresent = true;
113017
114088
  const raw = readAuditRaw(logPath);
113018
114089
  hostd.recent = readAndFilter(raw, {}, 1000).filter((e) => !HOSTD_NOISE_OPS.has(e.op)).slice(-10);
@@ -113321,7 +114392,7 @@ async function handleGetSchedule(config, deps = {}) {
113321
114392
  const recentByAgent = {};
113322
114393
  const agents = new Set(entries.map((e) => e.agent));
113323
114394
  for (const agent of agents) {
113324
- const rows = readRecentFires(resolve44(agentsDir, agent, "scheduler.jsonl"));
114395
+ const rows = readRecentFires(resolve45(agentsDir, agent, "scheduler.jsonl"));
113325
114396
  if (rows.length > 0)
113326
114397
  recentByAgent[agent] = rows.slice(-10);
113327
114398
  }
@@ -113353,11 +114424,11 @@ async function handleGetApprovals() {
113353
114424
  return { reachable: true, decisions: sorted };
113354
114425
  }
113355
114426
  function brokerOperatorSocketPath(home2 = process.env.HOME || homedir45()) {
113356
- return join95(home2, ".switchroom", "broker-operator", "sock");
114427
+ return join96(home2, ".switchroom", "broker-operator", "sock");
113357
114428
  }
113358
114429
  function resolveBrokerOperatorSocket(home2 = process.env.HOME || homedir45()) {
113359
114430
  const p = brokerOperatorSocketPath(home2);
113360
- return existsSync86(p) ? p : null;
114431
+ return existsSync87(p) ? p : null;
113361
114432
  }
113362
114433
  async function handleGetGrants(deps = {}) {
113363
114434
  const socket = "socketPath" in deps ? deps.socketPath : resolveBrokerOperatorSocket();
@@ -113748,8 +114819,8 @@ async function handleGetSummary(deps, now2 = Date.now()) {
113748
114819
  init_fleet_health_read();
113749
114820
 
113750
114821
  // src/web/webhook-handler.ts
113751
- import { appendFileSync as appendFileSync5, existsSync as existsSync88, mkdirSync as mkdirSync49, readFileSync as readFileSync80, writeFileSync as writeFileSync34 } from "fs";
113752
- import { join as join97 } from "path";
114822
+ import { appendFileSync as appendFileSync5, existsSync as existsSync89, mkdirSync as mkdirSync49, readFileSync as readFileSync81, writeFileSync as writeFileSync34 } from "fs";
114823
+ import { join as join98 } from "path";
113753
114824
  import { homedir as homedir48 } from "os";
113754
114825
 
113755
114826
  // src/web/webhook-verify.ts
@@ -113917,7 +114988,7 @@ init_log_rotation();
113917
114988
  import net4 from "node:net";
113918
114989
  function forwardToGateway(socketPath, req, opts = {}) {
113919
114990
  const timeoutMs = opts.timeoutMs ?? 5000;
113920
- return new Promise((resolve46) => {
114991
+ return new Promise((resolve47) => {
113921
114992
  let settled = false;
113922
114993
  let buf = "";
113923
114994
  const done = (value) => {
@@ -113927,7 +114998,7 @@ function forwardToGateway(socketPath, req, opts = {}) {
113927
114998
  try {
113928
114999
  conn.destroy();
113929
115000
  } catch {}
113930
- resolve46(value);
115001
+ resolve47(value);
113931
115002
  };
113932
115003
  const conn = net4.createConnection(socketPath);
113933
115004
  conn.setEncoding("utf8");
@@ -113958,20 +115029,20 @@ function forwardToGateway(socketPath, req, opts = {}) {
113958
115029
  }
113959
115030
 
113960
115031
  // src/web/webhook-edge.ts
113961
- import { existsSync as existsSync87, readFileSync as readFileSync79 } from "fs";
113962
- import { join as join96 } from "path";
115032
+ import { existsSync as existsSync88, readFileSync as readFileSync80 } from "fs";
115033
+ import { join as join97 } from "path";
113963
115034
  import { homedir as homedir47 } from "os";
113964
115035
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
113965
115036
  var EDGE_HEADER = "x-switchroom-edge";
113966
115037
  function edgeSecretPath() {
113967
- return join96(homedir47(), ".switchroom", "webhook-edge-secret");
115038
+ return join97(homedir47(), ".switchroom", "webhook-edge-secret");
113968
115039
  }
113969
- function loadEdgeSecret(path7) {
113970
- const p = path7 ?? edgeSecretPath();
115040
+ function loadEdgeSecret(path9) {
115041
+ const p = path9 ?? edgeSecretPath();
113971
115042
  try {
113972
- if (!existsSync87(p))
115043
+ if (!existsSync88(p))
113973
115044
  return null;
113974
- const raw = readFileSync79(p, "utf-8").trim();
115045
+ const raw = readFileSync80(p, "utf-8").trim();
113975
115046
  return raw.length > 0 ? raw : null;
113976
115047
  } catch {
113977
115048
  return null;
@@ -114020,17 +115091,17 @@ function jsonReply(status, body, extraHeaders) {
114020
115091
  }
114021
115092
  var DEDUP_MAX = 1000;
114022
115093
  var DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
114023
- function loadDedupFile(path7) {
115094
+ function loadDedupFile(path9) {
114024
115095
  try {
114025
- if (!existsSync88(path7))
115096
+ if (!existsSync89(path9))
114026
115097
  return {};
114027
- const raw = JSON.parse(readFileSync80(path7, "utf-8"));
115098
+ const raw = JSON.parse(readFileSync81(path9, "utf-8"));
114028
115099
  return typeof raw.deliveries === "object" && raw.deliveries !== null ? raw.deliveries : {};
114029
115100
  } catch {
114030
115101
  return {};
114031
115102
  }
114032
115103
  }
114033
- function saveDedupFile(path7, deliveries, now2) {
115104
+ function saveDedupFile(path9, deliveries, now2) {
114034
115105
  const pruned = {};
114035
115106
  for (const [id, ts] of Object.entries(deliveries)) {
114036
115107
  if (now2 - ts < DEDUP_TTL_MS)
@@ -114038,7 +115109,7 @@ function saveDedupFile(path7, deliveries, now2) {
114038
115109
  }
114039
115110
  const sorted = Object.entries(pruned).sort((a, b) => b[1] - a[1]).slice(0, DEDUP_MAX);
114040
115111
  const final = Object.fromEntries(sorted);
114041
- writeFileSync34(path7, JSON.stringify({ deliveries: final }), {
115112
+ writeFileSync34(path9, JSON.stringify({ deliveries: final }), {
114042
115113
  mode: 384
114043
115114
  });
114044
115115
  }
@@ -114046,8 +115117,8 @@ var agentDedupCache = new Map;
114046
115117
  function createFileDedupStore(resolveAgentDir2) {
114047
115118
  return {
114048
115119
  check(agent, deliveryId, now2) {
114049
- const telegramDir = join97(resolveAgentDir2(agent), "telegram");
114050
- const filePath = join97(telegramDir, "webhook-dedup.json");
115120
+ const telegramDir = join98(resolveAgentDir2(agent), "telegram");
115121
+ const filePath = join98(telegramDir, "webhook-dedup.json");
114051
115122
  if (!agentDedupCache.has(agent)) {
114052
115123
  agentDedupCache.set(agent, loadDedupFile(filePath));
114053
115124
  }
@@ -114099,7 +115170,7 @@ function shouldWriteThrottleIssue(agent, source, now2, windowMap) {
114099
115170
  return true;
114100
115171
  }
114101
115172
  function writeThrottleIssue(agent, source, now2, telegramDir, log) {
114102
- const issuesPath = join97(telegramDir, "issues.jsonl");
115173
+ const issuesPath = join98(telegramDir, "issues.jsonl");
114103
115174
  try {
114104
115175
  mkdirSync49(telegramDir, { recursive: true });
114105
115176
  const record = {
@@ -114124,7 +115195,7 @@ function writeThrottleIssue(agent, source, now2, telegramDir, log) {
114124
115195
  async function handleWebhookIngest(args, deps = {}) {
114125
115196
  const log = deps.log ?? ((s) => process.stderr.write(s));
114126
115197
  const now2 = (deps.now ?? Date.now)();
114127
- const resolveAgentDir2 = deps.resolveAgentDir ?? ((a) => join97(homedir48(), ".switchroom", "agents", a));
115198
+ const resolveAgentDir2 = deps.resolveAgentDir ?? ((a) => join98(homedir48(), ".switchroom", "agents", a));
114128
115199
  const rateLimiter = deps.rateLimiter ?? defaultRateLimiter;
114129
115200
  const dedupStore = deps.dedupStore ?? createFileDedupStore(resolveAgentDir2);
114130
115201
  if (!args.agentExists) {
@@ -114191,7 +115262,7 @@ async function handleWebhookIngest(args, deps = {}) {
114191
115262
  if (retryAfter !== null) {
114192
115263
  if (!args.viaGateway) {
114193
115264
  const agentDir2 = resolveAgentDir2(args.agent);
114194
- const telegramDir2 = join97(agentDir2, "telegram");
115265
+ const telegramDir2 = join98(agentDir2, "telegram");
114195
115266
  if (shouldWriteThrottleIssue(args.agent, source, now2)) {
114196
115267
  writeThrottleIssue(args.agent, source, now2, telegramDir2, log);
114197
115268
  }
@@ -114211,7 +115282,7 @@ async function handleWebhookIngest(args, deps = {}) {
114211
115282
  const eventType = source === "github" ? args.headers.get("x-github-event") ?? "unknown" : source === "linear" ? String(payload.type ?? "unknown").toLowerCase() : args.source;
114212
115283
  const rendered = source === "github" ? renderGithubEvent(eventType, payload) : source === "linear" ? renderLinearEvent(eventType, payload) : renderGenericEvent(args.source, payload);
114213
115284
  if (args.viaGateway) {
114214
- const socketPath = join97(resolveAgentDir2(args.agent), "telegram", "webhook.sock");
115285
+ const socketPath = join98(resolveAgentDir2(args.agent), "telegram", "webhook.sock");
114215
115286
  const forward = deps.forwardFn ?? forwardToGateway;
114216
115287
  const deliveryId = source === "github" ? args.headers.get("x-github-delivery") ?? undefined : undefined;
114217
115288
  let resp;
@@ -114250,8 +115321,8 @@ async function handleWebhookIngest(args, deps = {}) {
114250
115321
  return jsonReply(202, { ok: true, recorded: true, ts: resp.ts });
114251
115322
  }
114252
115323
  const agentDir = resolveAgentDir2(args.agent);
114253
- const telegramDir = join97(agentDir, "telegram");
114254
- const logPath = join97(telegramDir, "webhook-events.jsonl");
115324
+ const telegramDir = join98(agentDir, "telegram");
115325
+ const logPath = join98(telegramDir, "webhook-events.jsonl");
114255
115326
  try {
114256
115327
  mkdirSync49(telegramDir, { recursive: true });
114257
115328
  rotateWebhookLogIfNeeded(logPath);
@@ -114280,9 +115351,9 @@ init_shipped_assets();
114280
115351
  // src/web/hermes-adapter.ts
114281
115352
  init_loader();
114282
115353
  import { createConnection as createConnection3 } from "node:net";
114283
- import { createHash as createHash16 } from "node:crypto";
114284
- import { join as join98, resolve as resolve46 } from "node:path";
114285
- import { existsSync as existsSync89, readFileSync as readFileSync81 } from "node:fs";
115354
+ import { createHash as createHash17 } from "node:crypto";
115355
+ import { join as join99, resolve as resolve47 } from "node:path";
115356
+ import { existsSync as existsSync90, readFileSync as readFileSync82 } from "node:fs";
114286
115357
 
114287
115358
  // src/agent-scheduler/channel-target.ts
114288
115359
  init_merge();
@@ -114326,8 +115397,8 @@ function agentLiveness(config, agentName) {
114326
115397
  const agentsDir = resolveAgentsDir(config);
114327
115398
  if (agentBridgeAlive(agentsDir, agentName))
114328
115399
  return "active";
114329
- const alive = join98(agentsDir, agentName, "telegram", ".bridge-alive");
114330
- if (existsSync89(alive))
115400
+ const alive = join99(agentsDir, agentName, "telegram", ".bridge-alive");
115401
+ if (existsSync90(alive))
114331
115402
  return "idle";
114332
115403
  return "offline";
114333
115404
  }
@@ -114435,11 +115506,11 @@ function resolveAgentChat(config, sessionId, agentsDir) {
114435
115506
  const threadId = topicId !== undefined ? topicId === null ? undefined : parseInt(topicId, 10) || undefined : channel.threadId;
114436
115507
  return { chatId: channel.chatId, threadId };
114437
115508
  }
114438
- const accessPath = resolve46(agentsDir, agentName, "telegram", "access.json");
114439
- if (!existsSync89(accessPath))
115509
+ const accessPath = resolve47(agentsDir, agentName, "telegram", "access.json");
115510
+ if (!existsSync90(accessPath))
114440
115511
  return null;
114441
115512
  try {
114442
- const raw = readFileSync81(accessPath, "utf-8");
115513
+ const raw = readFileSync82(accessPath, "utf-8");
114443
115514
  const access = JSON.parse(raw);
114444
115515
  const chatId = access.allowFrom?.[0];
114445
115516
  if (!chatId)
@@ -114450,8 +115521,8 @@ function resolveAgentChat(config, sessionId, agentsDir) {
114450
115521
  }
114451
115522
  }
114452
115523
  async function injectInbound(agentsDir, agentName, chatId, threadId, text, promptKey) {
114453
- const socketPath = resolve46(agentsDir, agentName, "telegram", "gateway.sock");
114454
- if (!existsSync89(socketPath)) {
115524
+ const socketPath = resolve47(agentsDir, agentName, "telegram", "gateway.sock");
115525
+ if (!existsSync90(socketPath)) {
114455
115526
  return { ok: false, error: `agent ${agentName} gateway socket not found \u2014 is it running?` };
114456
115527
  }
114457
115528
  const ts = Date.now();
@@ -114498,8 +115569,8 @@ async function injectInbound(agentsDir, agentName, chatId, threadId, text, promp
114498
115569
  });
114499
115570
  }
114500
115571
  function readHistoryDb(agentsDir, agentName, limit) {
114501
- const dbPath = resolve46(agentsDir, agentName, "telegram", "history.db");
114502
- if (!existsSync89(dbPath))
115572
+ const dbPath = resolve47(agentsDir, agentName, "telegram", "history.db");
115573
+ if (!existsSync90(dbPath))
114503
115574
  return null;
114504
115575
  try {
114505
115576
  let SqliteDatabase;
@@ -114528,7 +115599,7 @@ function readHistoryDb(agentsDir, agentName, limit) {
114528
115599
  }
114529
115600
  }
114530
115601
  function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs = 2000) {
114531
- const dbPath = resolve46(agentsDir, agentName, "telegram", "history.db");
115602
+ const dbPath = resolve47(agentsDir, agentName, "telegram", "history.db");
114532
115603
  let SqliteDatabase = null;
114533
115604
  try {
114534
115605
  const meta = import.meta;
@@ -114538,7 +115609,7 @@ function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs =
114538
115609
  }
114539
115610
  } catch {}
114540
115611
  let cursor = 0;
114541
- if (SqliteDatabase && existsSync89(dbPath)) {
115612
+ if (SqliteDatabase && existsSync90(dbPath)) {
114542
115613
  try {
114543
115614
  const db = new SqliteDatabase(dbPath, { readonly: true });
114544
115615
  try {
@@ -114550,7 +115621,7 @@ function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs =
114550
115621
  } catch {}
114551
115622
  }
114552
115623
  const timer = setInterval(() => {
114553
- if (!SqliteDatabase || !existsSync89(dbPath))
115624
+ if (!SqliteDatabase || !existsSync90(dbPath))
114554
115625
  return;
114555
115626
  try {
114556
115627
  const db = new SqliteDatabase(dbPath, { readonly: true });
@@ -115213,7 +116284,7 @@ async function onHermesMessage(ctx, raw) {
115213
116284
  sendResponse(ctx, rpcErr(id, -32603, `Could not resolve chat for ${sessionId} \u2014 ensure the agent has telegram.forum_chat_id or channels.telegram.chat_id configured.`));
115214
116285
  break;
115215
116286
  }
115216
- const promptKey = createHash16("sha256").update(`${sessionId}:${Date.now()}:${content}`).digest("hex").slice(0, 12);
116287
+ const promptKey = createHash17("sha256").update(`${sessionId}:${Date.now()}:${content}`).digest("hex").slice(0, 12);
115217
116288
  sendEvent(ctx, "message.start", sessionId, { prompt_key: promptKey });
115218
116289
  pendingPromptKeys(ctx).set(sessionId, promptKey);
115219
116290
  const result = await injectInbound(agentsDir, submitAgent, chat.chatId, chat.threadId, content, promptKey);
@@ -115409,9 +116480,9 @@ function resolveWebToken() {
115409
116480
  if (fromEnv && fromEnv.length > 0)
115410
116481
  return fromEnv;
115411
116482
  const home2 = process.env.HOME ?? homedir49();
115412
- const tokenPath = join99(home2, ".switchroom", "web-token");
115413
- if (existsSync90(tokenPath)) {
115414
- const existing = readFileSync82(tokenPath, "utf8").trim();
116483
+ const tokenPath = join100(home2, ".switchroom", "web-token");
116484
+ if (existsSync91(tokenPath)) {
116485
+ const existing = readFileSync83(tokenPath, "utf8").trim();
115415
116486
  if (existing.length > 0)
115416
116487
  return existing;
115417
116488
  }
@@ -115428,7 +116499,7 @@ function resolveWebToken() {
115428
116499
  return token;
115429
116500
  } catch (err) {
115430
116501
  if (err.code === "EEXIST") {
115431
- const existing = readFileSync82(tokenPath, "utf8").trim();
116502
+ const existing = readFileSync83(tokenPath, "utf8").trim();
115432
116503
  if (existing.length > 0)
115433
116504
  return existing;
115434
116505
  }
@@ -115501,14 +116572,14 @@ function checkWsAuth(req, token, server) {
115501
116572
  return presented !== null && constantTimeEqual(presented, token);
115502
116573
  }
115503
116574
  function loadWebhookSecrets() {
115504
- const path7 = join99(homedir49(), ".switchroom", "webhook-secrets.json");
115505
- if (!existsSync90(path7))
116575
+ const path9 = join100(homedir49(), ".switchroom", "webhook-secrets.json");
116576
+ if (!existsSync91(path9))
115506
116577
  return {};
115507
116578
  try {
115508
- const parsed = JSON.parse(readFileSync82(path7, "utf-8"));
116579
+ const parsed = JSON.parse(readFileSync83(path9, "utf-8"));
115509
116580
  return parsed && typeof parsed === "object" ? parsed : {};
115510
116581
  } catch (err) {
115511
- process.stderr.write(`webhook-ingest: failed to parse ${path7}: ${err.message} \u2014 webhooks will return 401 until fixed
116582
+ process.stderr.write(`webhook-ingest: failed to parse ${path9}: ${err.message} \u2014 webhooks will return 401 until fixed
115512
116583
  `);
115513
116584
  return {};
115514
116585
  }
@@ -115695,11 +116766,11 @@ function resolveUiDir(probe2 = {
115695
116766
  bundleDir: import.meta.dirname,
115696
116767
  execPath: process.execPath
115697
116768
  }) {
115698
- return resolveShippedAsset(WEB_UI_ASSET, probe2).path ?? resolve47(probe2.bundleDir, WEB_UI_ASSET.asset);
116769
+ return resolveShippedAsset(WEB_UI_ASSET, probe2).path ?? resolve48(probe2.bundleDir, WEB_UI_ASSET.asset);
115699
116770
  }
115700
116771
  function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
115701
116772
  const uiDirRaw = resolveUiDir();
115702
- const uiDir = existsSync90(uiDirRaw) ? realpathSync7(uiDirRaw) : uiDirRaw;
116773
+ const uiDir = existsSync91(uiDirRaw) ? realpathSync7(uiDirRaw) : uiDirRaw;
115703
116774
  const token = resolveWebToken();
115704
116775
  const freshConfig = () => {
115705
116776
  if (!configPath)
@@ -115999,8 +117070,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
115999
117070
  })();
116000
117071
  }
116001
117072
  const filePath = resolveDashboardFilePath(pathname);
116002
- const fullPath = join99(uiDir, filePath);
116003
- if (!existsSync90(fullPath)) {
117073
+ const fullPath = join100(uiDir, filePath);
117074
+ if (!existsSync91(fullPath)) {
116004
117075
  return new Response("Not Found", { status: 404 });
116005
117076
  }
116006
117077
  let realFullPath;
@@ -116010,12 +117081,12 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
116010
117081
  return new Response("Not Found", { status: 404 });
116011
117082
  }
116012
117083
  const rel = relative5(uiDir, realFullPath);
116013
- if (rel.startsWith("..") || resolve47(uiDir, rel) !== realFullPath) {
117084
+ if (rel.startsWith("..") || resolve48(uiDir, rel) !== realFullPath) {
116014
117085
  return new Response("Forbidden", { status: 403 });
116015
117086
  }
116016
117087
  const ext = extname(realFullPath);
116017
117088
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
116018
- const content = readFileSync82(realFullPath);
117089
+ const content = readFileSync83(realFullPath);
116019
117090
  const headers = { "Content-Type": contentType };
116020
117091
  const cacheControl = dashboardCacheControl(ext);
116021
117092
  if (cacheControl)
@@ -116138,13 +117209,13 @@ init_helpers();
116138
117209
  init_loader();
116139
117210
 
116140
117211
  // src/web/startup-guard.ts
116141
- import { existsSync as existsSync91, readFileSync as readFileSync83, writeFileSync as writeFileSync35, mkdirSync as mkdirSync51, statSync as statSync52, unlinkSync as unlinkSync18 } from "node:fs";
117212
+ import { existsSync as existsSync92, readFileSync as readFileSync84, writeFileSync as writeFileSync35, mkdirSync as mkdirSync51, statSync as statSync53, unlinkSync as unlinkSync18 } from "node:fs";
116142
117213
  import { dirname as dirname37 } from "node:path";
116143
117214
  function detectConfigMountFault(configPath, deps = {}) {
116144
- const stat = deps.stat ?? ((p) => statSync52(p));
117215
+ const stat2 = deps.stat ?? ((p) => statSync53(p));
116145
117216
  let st;
116146
117217
  try {
116147
- st = stat(configPath);
117218
+ st = stat2(configPath);
116148
117219
  } catch {
116149
117220
  return null;
116150
117221
  }
@@ -116174,9 +117245,9 @@ function nextCrashBackoff(prior, now2, options = {}) {
116174
117245
  }
116175
117246
  function readCrashState(statePath) {
116176
117247
  try {
116177
- if (!existsSync91(statePath))
117248
+ if (!existsSync92(statePath))
116178
117249
  return null;
116179
- const raw = JSON.parse(readFileSync83(statePath, "utf-8"));
117250
+ const raw = JSON.parse(readFileSync84(statePath, "utf-8"));
116180
117251
  if (typeof raw.count !== "number" || typeof raw.lastTs !== "number")
116181
117252
  return null;
116182
117253
  return { count: raw.count, lastTs: raw.lastTs };
@@ -116192,7 +117263,7 @@ function writeCrashState(statePath, state) {
116192
117263
  }
116193
117264
  function clearCrashState(statePath) {
116194
117265
  try {
116195
- if (existsSync91(statePath))
117266
+ if (existsSync92(statePath))
116196
117267
  unlinkSync18(statePath);
116197
117268
  } catch {}
116198
117269
  }
@@ -116215,7 +117286,7 @@ async function handleWebStartupFailure(err, configPath, stateFile) {
116215
117286
  throw err;
116216
117287
  }
116217
117288
  function webCrashStatePath() {
116218
- return join100(homedir50(), ".switchroom", "web", ".crashloop-state.json");
117289
+ return join101(homedir50(), ".switchroom", "web", ".crashloop-state.json");
116219
117290
  }
116220
117291
  function registerWebCommand(program3) {
116221
117292
  program3.command("web").description("Start the web dashboard for monitoring agents").option("-p, --port <port>", "Port to listen on", "8080").option("-b, --bind <host>", "Host/IP to bind to (default: 127.0.0.1, localhost-only)", "127.0.0.1").action(withConfigError(async (opts) => {
@@ -116282,9 +117353,9 @@ init_embedded_examples();
116282
117353
  init_atomic();
116283
117354
  init_loader();
116284
117355
  init_scaffold();
116285
- import { existsSync as existsSync93, readFileSync as readFileSync84, mkdirSync as mkdirSync52, statSync as statSync53, writeFileSync as writeFileSync36 } from "node:fs";
117356
+ import { existsSync as existsSync94, readFileSync as readFileSync85, mkdirSync as mkdirSync52, statSync as statSync54, writeFileSync as writeFileSync36 } from "node:fs";
116286
117357
  import { execFileSync as execFileSync29 } from "node:child_process";
116287
- import { resolve as resolve48, dirname as dirname38 } from "node:path";
117358
+ import { resolve as resolve49, dirname as dirname38 } from "node:path";
116288
117359
  init_state();
116289
117360
  init_vault();
116290
117361
  init_manager();
@@ -116699,7 +117770,7 @@ ${status} ${source_default.bold(`Step ${num3}:`)} ${title}`);
116699
117770
  init_source();
116700
117771
  init_loader();
116701
117772
  init_hindsight();
116702
- import { existsSync as existsSync92 } from "node:fs";
117773
+ import { existsSync as existsSync93 } from "node:fs";
116703
117774
  var HINDSIGHT_READY_RETRIES = 5;
116704
117775
  var HINDSIGHT_READY_INTERVAL_MS = 1000;
116705
117776
  async function resolveLiteLLMForHindsight2(config, deps = {}) {
@@ -116736,7 +117807,7 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath, d
116736
117807
  try {
116737
117808
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
116738
117809
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
116739
- if (passphrase && existsSync92(vaultPath)) {
117810
+ if (passphrase && existsSync93(vaultPath)) {
116740
117811
  const { getStringSecret: getStringSecret2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
116741
117812
  const existing = getStringSecret2(passphrase, vaultPath, "hindsight-api-key");
116742
117813
  if (existing) {
@@ -117006,7 +118077,7 @@ async function stepMemoryBackend(config, nonInteractive, switchroomConfigPath, d
117006
118077
  function writeSwitchroomYaml(configPath, text) {
117007
118078
  let mode = 420;
117008
118079
  try {
117009
- mode = statSync53(configPath).mode & 511;
118080
+ mode = statSync54(configPath).mode & 511;
117010
118081
  } catch {}
117011
118082
  writeConfigFileSync(configPath, text, mode);
117012
118083
  }
@@ -117105,7 +118176,7 @@ async function stepConfigFile(configPath, nonInteractive) {
117105
118176
  existingConfig = null;
117106
118177
  }
117107
118178
  }
117108
- if (existingConfig && existsSync93(existingConfig)) {
118179
+ if (existingConfig && existsSync94(existingConfig)) {
117109
118180
  if (!nonInteractive) {
117110
118181
  const useExisting = await askYesNo(` Found ${source_default.cyan(existingConfig)}. Use it?`, true);
117111
118182
  if (!useExisting) {
@@ -117117,9 +118188,9 @@ async function stepConfigFile(configPath, nonInteractive) {
117117
118188
  console.log(source_default.green(` ${STEP_DONE} Config loaded`) + source_default.gray(` (${Object.keys(config.agents).length} agents)`));
117118
118189
  const hasExplicitTimezone = config.switchroom?.timezone !== undefined || config.defaults?.timezone !== undefined;
117119
118190
  if (!hasExplicitTimezone) {
117120
- await writeDetectedTimezone(resolve48(existingConfig), nonInteractive);
118191
+ await writeDetectedTimezone(resolve49(existingConfig), nonInteractive);
117121
118192
  }
117122
- return { config, configPath: resolve48(existingConfig) };
118193
+ return { config, configPath: resolve49(existingConfig) };
117123
118194
  }
117124
118195
  return await copyExampleConfig2(nonInteractive);
117125
118196
  }
@@ -117146,12 +118217,12 @@ async function copyExampleConfig2(nonInteractive) {
117146
118217
  await writeDetectedTimezone(destFile, nonInteractive);
117147
118218
  const config = loadConfig(destFile);
117148
118219
  console.log(source_default.green(` ${STEP_DONE} Config loaded`) + source_default.gray(` (${Object.keys(config.agents).length} agents)`));
117149
- return { config, configPath: resolve48(destFile) };
118220
+ return { config, configPath: resolve49(destFile) };
117150
118221
  }
117151
118222
  async function writeDetectedTimezone(destFile, nonInteractive, detect = detectServerTimezone, prompt = ask) {
117152
118223
  const detected = detect();
117153
118224
  if (detected !== undefined && detected !== "UTC" && isValidTimezone(detected)) {
117154
- const before2 = readFileSync84(destFile, "utf-8");
118225
+ const before2 = readFileSync85(destFile, "utf-8");
117155
118226
  const after2 = setSwitchroomTimezone(before2, detected);
117156
118227
  if (after2 !== before2)
117157
118228
  writeSwitchroomYaml(destFile, after2);
@@ -117172,7 +118243,7 @@ async function writeDetectedTimezone(destFile, nonInteractive, detect = detectSe
117172
118243
  console.error(source_default.yellow(` \u26a0 "${zone}" is not a valid IANA zone (expected Region/City like ` + '"Australia/Melbourne"). Skipping \u2014 agents will use UTC. ' + "Edit switchroom.timezone in switchroom.yaml by hand."));
117173
118244
  return;
117174
118245
  }
117175
- const before = readFileSync84(destFile, "utf-8");
118246
+ const before = readFileSync85(destFile, "utf-8");
117176
118247
  const after = setSwitchroomTimezone(before, zone);
117177
118248
  if (after !== before)
117178
118249
  writeSwitchroomYaml(destFile, after);
@@ -117251,7 +118322,7 @@ async function resolveOrPromptToken(rawToken, label, config, nonInteractive) {
117251
118322
  try {
117252
118323
  const { openVault: openVault2 } = await Promise.resolve().then(() => (init_vault(), exports_vault));
117253
118324
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
117254
- if (existsSync93(vaultPath)) {
118325
+ if (existsSync94(vaultPath)) {
117255
118326
  const secrets = openVault2(passphrase, vaultPath);
117256
118327
  const key = rawToken.replace("vault:", "");
117257
118328
  const entry = secrets[key];
@@ -117279,7 +118350,7 @@ async function resolveOrPromptToken(rawToken, label, config, nonInteractive) {
117279
118350
  async function storeTokenInVault(config, vaultRef, token) {
117280
118351
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
117281
118352
  const key = vaultRef.replace("vault:", "");
117282
- if (!existsSync93(vaultPath)) {
118353
+ if (!existsSync94(vaultPath)) {
117283
118354
  console.log(source_default.gray(" Creating encrypted vault..."));
117284
118355
  let passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
117285
118356
  if (!passphrase) {
@@ -117562,13 +118633,13 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
117562
118633
  return;
117563
118634
  }
117564
118635
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
117565
- if (!existsSync93(vaultPath)) {
118636
+ if (!existsSync94(vaultPath)) {
117566
118637
  console.log(source_default.gray(" Skipping (vault not created yet)."));
117567
118638
  return;
117568
118639
  }
117569
118640
  const credPathRaw = config.vault?.broker?.autoUnlockCredentialPath ?? "~/.switchroom/vault-auto-unlock";
117570
118641
  const credPath = resolvePath(credPathRaw);
117571
- if (config.vault?.broker?.autoUnlock === true && existsSync93(credPath)) {
118642
+ if (config.vault?.broker?.autoUnlock === true && existsSync94(credPath)) {
117572
118643
  console.log(source_default.green(` ${STEP_DONE} Already configured (${credPath})`));
117573
118644
  return;
117574
118645
  }
@@ -117646,10 +118717,10 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
117646
118717
  }
117647
118718
  }
117648
118719
  function persistApprovalAuthTelegramId(configPath) {
117649
- if (!existsSync93(configPath)) {
118720
+ if (!existsSync94(configPath)) {
117650
118721
  throw new ConfigError(`Cannot persist vault.broker.approvalAuth: config not found at ${configPath}`);
117651
118722
  }
117652
- const content = readFileSync84(configPath, "utf-8");
118723
+ const content = readFileSync85(configPath, "utf-8");
117653
118724
  const result = insertVaultBrokerApprovalAuth(content, "telegram-id");
117654
118725
  if (result.kind === "rewritten") {
117655
118726
  writeSwitchroomYaml(configPath, result.content);
@@ -117657,10 +118728,10 @@ function persistApprovalAuthTelegramId(configPath) {
117657
118728
  return result.kind;
117658
118729
  }
117659
118730
  function persistDangerousMode(config, configPath) {
117660
- if (!existsSync93(configPath)) {
118731
+ if (!existsSync94(configPath)) {
117661
118732
  throw new ConfigError(`Cannot persist dangerous_mode: config not found at ${configPath}`);
117662
118733
  }
117663
- let content = readFileSync84(configPath, "utf-8");
118734
+ let content = readFileSync85(configPath, "utf-8");
117664
118735
  const agentNames = Object.keys(config.agents);
117665
118736
  for (const name of agentNames) {
117666
118737
  const agentPattern = new RegExp(`(^ ${name}:\\s*\\n)`, "m");
@@ -117700,7 +118771,7 @@ async function stepOnboardingGuidance(config, nonInteractive) {
117700
118771
  const agentNames = Object.keys(config.agents);
117701
118772
  let allAuthenticated = true;
117702
118773
  for (const name of agentNames) {
117703
- const agentDir = resolve48(agentsDir, name);
118774
+ const agentDir = resolve49(agentsDir, name);
117704
118775
  const status = getAuthStatus(name, agentDir);
117705
118776
  if (status.authenticated) {
117706
118777
  console.log(` ${source_default.green("OK")} ${source_default.bold(name)}` + source_default.gray(` - authenticated (expires: ${status.timeUntilExpiry ?? "unknown"})`));
@@ -117796,7 +118867,7 @@ async function stepVerification(config, nonInteractive, deps = {}, expect = {})
117796
118867
  const findings = [];
117797
118868
  let applyPending = false;
117798
118869
  if (!nonInteractive && agentNames.length > 0) {
117799
- const composeExists = (deps.composeFileExists ?? (() => existsSync93(composeFilePath())))();
118870
+ const composeExists = (deps.composeFileExists ?? (() => existsSync94(composeFilePath())))();
117800
118871
  if (!composeExists) {
117801
118872
  applyPending = true;
117802
118873
  log(source_default.gray(" Not started: `switchroom apply` has not generated the compose file yet."));
@@ -117893,9 +118964,9 @@ init_source();
117893
118964
  init_loader();
117894
118965
  init_lifecycle();
117895
118966
  init_compose_env();
117896
- import { existsSync as existsSync97, mkdirSync as mkdirSync56, readFileSync as readFileSync88, realpathSync as realpathSync8, statSync as statSync56, chownSync as chownSync13 } from "node:fs";
118967
+ import { existsSync as existsSync98, mkdirSync as mkdirSync56, readFileSync as readFileSync89, realpathSync as realpathSync8, statSync as statSync57, chownSync as chownSync13 } from "node:fs";
117897
118968
  import { spawnSync as spawnSync20 } from "node:child_process";
117898
- import { join as join101, dirname as dirname42 } from "node:path";
118969
+ import { join as join102, dirname as dirname42 } from "node:path";
117899
118970
  import { homedir as homedir51 } from "node:os";
117900
118971
 
117901
118972
  // src/cli/release-yaml.ts
@@ -117956,7 +119027,7 @@ init_operator_uid();
117956
119027
  init_atomic();
117957
119028
 
117958
119029
  // src/cli/preflight-mounts.ts
117959
- import { statSync as statSync54 } from "node:fs";
119030
+ import { statSync as statSync55 } from "node:fs";
117960
119031
  function parseHostBindSources(composeText) {
117961
119032
  const out = [];
117962
119033
  const FILE_HINT = /\.(ya?ml|db|log|toml|json|token|id)$|\/\.vault-token$|machine-id$|localtime$|vault-auto-unlock$|\/webkite$/;
@@ -117983,7 +119054,7 @@ function parseHostBindSources(composeText) {
117983
119054
  return out;
117984
119055
  }
117985
119056
  function validateBindSources(composeText, deps = {}) {
117986
- const stat = deps.stat ?? ((p) => statSync54(p));
119057
+ const stat2 = deps.stat ?? ((p) => statSync55(p));
117987
119058
  const sources = parseHostBindSources(composeText);
117988
119059
  const issues = [];
117989
119060
  const seen = new Set;
@@ -117995,7 +119066,7 @@ function validateBindSources(composeText, deps = {}) {
117995
119066
  checked++;
117996
119067
  let st = null;
117997
119068
  try {
117998
- st = stat(source);
119069
+ st = stat2(source);
117999
119070
  } catch {
118000
119071
  st = null;
118001
119072
  }
@@ -118029,18 +119100,18 @@ init_self_update();
118029
119100
  init_shipped_assets();
118030
119101
 
118031
119102
  // src/cli/self-update-io.ts
118032
- import { createHash as createHash17 } from "node:crypto";
119103
+ import { createHash as createHash18 } from "node:crypto";
118033
119104
  import {
118034
119105
  chmodSync as chmodSync16,
118035
119106
  closeSync as closeSync18,
118036
119107
  copyFileSync as copyFileSync12,
118037
119108
  createWriteStream,
118038
- existsSync as existsSync94,
119109
+ existsSync as existsSync95,
118039
119110
  lstatSync as lstatSync13,
118040
119111
  mkdirSync as mkdirSync53,
118041
119112
  openSync as openSync18,
118042
- readdirSync as readdirSync32,
118043
- readFileSync as readFileSync85,
119113
+ readdirSync as readdirSync33,
119114
+ readFileSync as readFileSync86,
118044
119115
  readSync as readSync6,
118045
119116
  renameSync as renameSync23,
118046
119117
  rmSync as rmSync18,
@@ -118079,9 +119150,9 @@ function defaultSelfUpdateIO() {
118079
119150
  throw new Error(`empty response body for ${url}`);
118080
119151
  await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
118081
119152
  },
118082
- sha256File(path7) {
118083
- const hash2 = createHash17("sha256");
118084
- const fd = openSync18(path7, "r");
119153
+ sha256File(path9) {
119154
+ const hash2 = createHash18("sha256");
119155
+ const fd = openSync18(path9, "r");
118085
119156
  try {
118086
119157
  const buf = Buffer.allocUnsafe(1 << 20);
118087
119158
  for (;; ) {
@@ -118095,8 +119166,8 @@ function defaultSelfUpdateIO() {
118095
119166
  }
118096
119167
  return hash2.digest("hex");
118097
119168
  },
118098
- probeBinary(path7) {
118099
- const r = spawnSync18(path7, ["--version"], {
119169
+ probeBinary(path9) {
119170
+ const r = spawnSync18(path9, ["--version"], {
118100
119171
  encoding: "utf-8",
118101
119172
  timeout: 30000,
118102
119173
  env: { ...process.env, SWITCHROOM_SELF_UPDATED: "" }
@@ -118136,24 +119207,24 @@ function defaultSelfUpdateIO() {
118136
119207
  copyFile(src, dest) {
118137
119208
  copyFileSync12(src, dest);
118138
119209
  },
118139
- chmodExec(path7) {
118140
- chmodSync16(path7, 493);
119210
+ chmodExec(path9) {
119211
+ chmodSync16(path9, 493);
118141
119212
  },
118142
119213
  rename(src, dest) {
118143
119214
  renameSync23(src, dest);
118144
119215
  },
118145
- remove(path7) {
118146
- rmSync18(path7, { force: true });
119216
+ remove(path9) {
119217
+ rmSync18(path9, { force: true });
118147
119218
  },
118148
- exists(path7) {
118149
- return existsSync94(path7);
119219
+ exists(path9) {
119220
+ return existsSync95(path9);
118150
119221
  },
118151
- dirname(path7) {
118152
- return dirname39(path7);
119222
+ dirname(path9) {
119223
+ return dirname39(path9);
118153
119224
  },
118154
- readText(path7) {
119225
+ readText(path9) {
118155
119226
  try {
118156
- return readFileSync85(path7, "utf-8");
119227
+ return readFileSync86(path9, "utf-8");
118157
119228
  } catch {
118158
119229
  return null;
118159
119230
  }
@@ -118173,19 +119244,19 @@ function defaultSelfUpdateIO() {
118173
119244
  symlink(target, linkPath) {
118174
119245
  symlinkSync5(target, linkPath);
118175
119246
  },
118176
- isSymlink(path7) {
119247
+ isSymlink(path9) {
118177
119248
  try {
118178
- return lstatSync13(path7).isSymbolicLink();
119249
+ return lstatSync13(path9).isSymbolicLink();
118179
119250
  } catch {
118180
119251
  return false;
118181
119252
  }
118182
119253
  },
118183
- removeTree(path7) {
118184
- rmSync18(path7, { recursive: true, force: true });
119254
+ removeTree(path9) {
119255
+ rmSync18(path9, { recursive: true, force: true });
118185
119256
  },
118186
119257
  listDir(dir) {
118187
119258
  try {
118188
- return readdirSync32(dir);
119259
+ return readdirSync33(dir);
118189
119260
  } catch {
118190
119261
  return [];
118191
119262
  }
@@ -118200,7 +119271,7 @@ init_install_cron();
118200
119271
 
118201
119272
  // src/memory/mm-refresh-cron.ts
118202
119273
  import { execFileSync as execFileSync30 } from "node:child_process";
118203
- import { chownSync as chownSync12, existsSync as existsSync95, mkdirSync as mkdirSync54, readFileSync as readFileSync86, renameSync as renameSync24, statSync as statSync55, writeFileSync as writeFileSync37 } from "node:fs";
119274
+ import { chownSync as chownSync12, existsSync as existsSync96, mkdirSync as mkdirSync54, readFileSync as readFileSync87, renameSync as renameSync24, statSync as statSync56, writeFileSync as writeFileSync37 } from "node:fs";
118204
119275
  import { dirname as dirname40 } from "node:path";
118205
119276
  var CRON_PATH3 = "/etc/cron.d/mental-model-refresh";
118206
119277
  var CRON_SCHEDULE2 = "27 4 * * *";
@@ -118217,20 +119288,20 @@ function renderCron3(opts) {
118217
119288
  `;
118218
119289
  }
118219
119290
  function installCron3(opts) {
118220
- const path7 = opts.path ?? CRON_PATH3;
119291
+ const path9 = opts.path ?? CRON_PATH3;
118221
119292
  const content = renderCron3(opts);
118222
- if (existsSync95(path7)) {
119293
+ if (existsSync96(path9)) {
118223
119294
  try {
118224
- if (readFileSync86(path7, "utf8") === content) {
118225
- return { status: "unchanged", path: path7, content };
119295
+ if (readFileSync87(path9, "utf8") === content) {
119296
+ return { status: "unchanged", path: path9, content };
118226
119297
  }
118227
119298
  } catch {}
118228
119299
  }
118229
- mkdirSync54(dirname40(path7), { recursive: true });
118230
- const tmp = `${path7}.${process.pid}.tmp`;
119300
+ mkdirSync54(dirname40(path9), { recursive: true });
119301
+ const tmp = `${path9}.${process.pid}.tmp`;
118231
119302
  writeFileSync37(tmp, content, { mode: 420 });
118232
- renameSync24(tmp, path7);
118233
- return { status: "installed", path: path7, content };
119303
+ renameSync24(tmp, path9);
119304
+ return { status: "installed", path: path9, content };
118234
119305
  }
118235
119306
  var defaultIdResolver3 = (user) => {
118236
119307
  const uid = Number(execFileSync30("id", ["-u", user], { encoding: "utf8" }).trim());
@@ -118241,27 +119312,27 @@ var defaultIdResolver3 = (user) => {
118241
119312
  return { uid, gid };
118242
119313
  };
118243
119314
  function ensureLogFile3(opts) {
118244
- const path7 = opts.path ?? CRON_LOG_PATH3;
118245
- if (existsSync95(path7))
118246
- return { status: "unchanged", path: path7 };
119315
+ const path9 = opts.path ?? CRON_LOG_PATH3;
119316
+ if (existsSync96(path9))
119317
+ return { status: "unchanged", path: path9 };
118247
119318
  const { uid, gid } = (opts.resolveIds ?? defaultIdResolver3)(opts.user);
118248
- mkdirSync54(dirname40(path7), { recursive: true });
118249
- writeFileSync37(path7, "", { mode: 420, flag: "a" });
119319
+ mkdirSync54(dirname40(path9), { recursive: true });
119320
+ writeFileSync37(path9, "", { mode: 420, flag: "a" });
118250
119321
  const isRoot = process.getuid?.() === 0;
118251
119322
  let alreadyOwned = false;
118252
119323
  try {
118253
- const st = statSync55(path7);
119324
+ const st = statSync56(path9);
118254
119325
  alreadyOwned = st.uid === uid && st.gid === gid;
118255
119326
  } catch {}
118256
119327
  if (isRoot && !alreadyOwned) {
118257
119328
  try {
118258
- chownSync12(path7, uid, gid);
119329
+ chownSync12(path9, uid, gid);
118259
119330
  } catch (e) {
118260
119331
  if (e.code !== "EPERM")
118261
119332
  throw e;
118262
119333
  }
118263
119334
  }
118264
- return { status: "created", path: path7 };
119335
+ return { status: "created", path: path9 };
118265
119336
  }
118266
119337
  function renderLogrotate3(opts) {
118267
119338
  return `${opts.logPath} {
@@ -118277,20 +119348,20 @@ function renderLogrotate3(opts) {
118277
119348
  `;
118278
119349
  }
118279
119350
  function installLogrotate3(opts) {
118280
- const path7 = opts.path ?? LOGROTATE_PATH3;
119351
+ const path9 = opts.path ?? LOGROTATE_PATH3;
118281
119352
  const content = renderLogrotate3({ logPath: opts.logPath ?? CRON_LOG_PATH3, user: opts.user });
118282
- if (existsSync95(path7)) {
119353
+ if (existsSync96(path9)) {
118283
119354
  try {
118284
- if (readFileSync86(path7, "utf8") === content) {
118285
- return { status: "unchanged", path: path7, content };
119355
+ if (readFileSync87(path9, "utf8") === content) {
119356
+ return { status: "unchanged", path: path9, content };
118286
119357
  }
118287
119358
  } catch {}
118288
119359
  }
118289
- mkdirSync54(dirname40(path7), { recursive: true });
118290
- const tmp = `${path7}.${process.pid}.tmp`;
119360
+ mkdirSync54(dirname40(path9), { recursive: true });
119361
+ const tmp = `${path9}.${process.pid}.tmp`;
118291
119362
  writeFileSync37(tmp, content, { mode: 420 });
118292
- renameSync24(tmp, path7);
118293
- return { status: "installed", path: path7, content };
119363
+ renameSync24(tmp, path9);
119364
+ return { status: "installed", path: path9, content };
118294
119365
  }
118295
119366
  function parseCronUser3(content) {
118296
119367
  for (const line of content.split(`
@@ -118307,20 +119378,20 @@ function parseCronUser3(content) {
118307
119378
  return null;
118308
119379
  }
118309
119380
  function reconcileCron3(opts) {
118310
- const path7 = opts.path ?? CRON_PATH3;
118311
- if (!existsSync95(path7))
118312
- return { status: "absent", path: path7 };
119381
+ const path9 = opts.path ?? CRON_PATH3;
119382
+ if (!existsSync96(path9))
119383
+ return { status: "absent", path: path9 };
118313
119384
  let user = opts.fallbackUser;
118314
119385
  try {
118315
- user = parseCronUser3(readFileSync86(path7, "utf8")) ?? opts.fallbackUser;
119386
+ user = parseCronUser3(readFileSync87(path9, "utf8")) ?? opts.fallbackUser;
118316
119387
  } catch {}
118317
119388
  if (!user) {
118318
- throw new Error(`cannot reconcile ${path7}: no cron-user parseable from the existing fragment and no fallback user available`);
119389
+ throw new Error(`cannot reconcile ${path9}: no cron-user parseable from the existing fragment and no fallback user available`);
118319
119390
  }
118320
- const r = installCron3({ user, binary: opts.binary, path: path7 });
119391
+ const r = installCron3({ user, binary: opts.binary, path: path9 });
118321
119392
  return {
118322
119393
  status: r.status === "installed" ? "reconciled" : "unchanged",
118323
- path: path7,
119394
+ path: path9,
118324
119395
  content: r.content
118325
119396
  };
118326
119397
  }
@@ -118330,7 +119401,7 @@ init_install_cron2();
118330
119401
 
118331
119402
  // src/host-cli-self-heal/install-timer.ts
118332
119403
  import { spawnSync as spawnSync19 } from "node:child_process";
118333
- import { existsSync as existsSync96, mkdirSync as mkdirSync55, readFileSync as readFileSync87, renameSync as renameSync25, writeFileSync as writeFileSync38 } from "node:fs";
119404
+ import { existsSync as existsSync97, mkdirSync as mkdirSync55, readFileSync as readFileSync88, renameSync as renameSync25, writeFileSync as writeFileSync38 } from "node:fs";
118334
119405
  import { dirname as dirname41 } from "node:path";
118335
119406
  var SERVICE_PATH = "/etc/systemd/system/switchroom-self-heal.service";
118336
119407
  var TIMER_PATH = "/etc/systemd/system/switchroom-self-heal.timer";
@@ -118376,22 +119447,22 @@ function renderTimer() {
118376
119447
  ` + `WantedBy=timers.target
118377
119448
  `;
118378
119449
  }
118379
- function writeUnitIdempotent(path7, content) {
118380
- if (existsSync96(path7)) {
119450
+ function writeUnitIdempotent(path9, content) {
119451
+ if (existsSync97(path9)) {
118381
119452
  try {
118382
- if (readFileSync87(path7, "utf8") === content)
119453
+ if (readFileSync88(path9, "utf8") === content)
118383
119454
  return "unchanged";
118384
119455
  } catch {}
118385
119456
  }
118386
- mkdirSync55(dirname41(path7), { recursive: true });
118387
- const tmp = `${path7}.${process.pid}.tmp`;
119457
+ mkdirSync55(dirname41(path9), { recursive: true });
119458
+ const tmp = `${path9}.${process.pid}.tmp`;
118388
119459
  writeFileSync38(tmp, content, { mode: 420 });
118389
- renameSync25(tmp, path7);
119460
+ renameSync25(tmp, path9);
118390
119461
  return "written";
118391
119462
  }
118392
119463
  function homeFromPasswd2(user) {
118393
119464
  try {
118394
- for (const line of readFileSync87("/etc/passwd", "utf8").split(`
119465
+ for (const line of readFileSync88("/etc/passwd", "utf8").split(`
118395
119466
  `)) {
118396
119467
  const f = line.split(":");
118397
119468
  if (f[0] === user && f[5])
@@ -118408,7 +119479,7 @@ function resolveOperatorUser(env2) {
118408
119479
  }
118409
119480
  function installSelfHealTimer(deps = {}) {
118410
119481
  const env2 = deps.env ?? process.env;
118411
- const systemdBooted = deps.systemdBooted ?? (() => existsSync96(SYSTEMD_MARKER));
119482
+ const systemdBooted = deps.systemdBooted ?? (() => existsSync97(SYSTEMD_MARKER));
118412
119483
  const geteuid = deps.geteuid ?? (() => typeof process.geteuid === "function" ? process.geteuid() : undefined);
118413
119484
  const homeForUser = deps.homeForUser ?? homeFromPasswd2;
118414
119485
  const servicePath = deps.servicePath ?? SERVICE_PATH;
@@ -118469,22 +119540,22 @@ function installSelfHealTimer(deps = {}) {
118469
119540
  // src/cli/update.ts
118470
119541
  function defaultPersistPin(configPath) {
118471
119542
  return (pin) => {
118472
- const path7 = configPath ?? findConfigFile();
118473
- const before = readFileSync88(path7, "utf8");
119543
+ const path9 = configPath ?? findConfigFile();
119544
+ const before = readFileSync89(path9, "utf8");
118474
119545
  const after = setReleasePinInConfig(before, pin);
118475
119546
  if (after === before)
118476
119547
  return;
118477
- writeConfigFileSync(path7, after, statSync56(path7).mode & 511);
119548
+ writeConfigFileSync(path9, after, statSync57(path9).mode & 511);
118478
119549
  try {
118479
119550
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
118480
119551
  const uid = resolveOperatorUid();
118481
119552
  if (uid !== undefined)
118482
- chownSync13(path7, uid, uid);
119553
+ chownSync13(path9, uid, uid);
118483
119554
  }
118484
119555
  } catch {}
118485
119556
  };
118486
119557
  }
118487
- var DEFAULT_COMPOSE_PATH2 = join101(homedir51(), ".switchroom", "compose", "docker-compose.yml");
119558
+ var DEFAULT_COMPOSE_PATH2 = join102(homedir51(), ".switchroom", "compose", "docker-compose.yml");
118488
119559
  var PACKAGED_INSTALL_KINDS = new Set([
118489
119560
  "static-binary",
118490
119561
  "npm-global",
@@ -118493,9 +119564,9 @@ var PACKAGED_INSTALL_KINDS = new Set([
118493
119564
  function runningFromSwitchroomCheckout(scriptPath) {
118494
119565
  let dir = dirname42(scriptPath);
118495
119566
  for (let i2 = 0;i2 < 12; i2++) {
118496
- if (existsSync97(join101(dir, ".git"))) {
119567
+ if (existsSync98(join102(dir, ".git"))) {
118497
119568
  try {
118498
- const pkg = JSON.parse(readFileSync88(join101(dir, "package.json"), "utf-8"));
119569
+ const pkg = JSON.parse(readFileSync89(join102(dir, "package.json"), "utf-8"));
118499
119570
  if (pkg.name === "switchroom")
118500
119571
  return true;
118501
119572
  } catch {}
@@ -118562,7 +119633,7 @@ function planUpdate(opts) {
118562
119633
  bundleDir: import.meta.dirname,
118563
119634
  execPath: process.execPath,
118564
119635
  scriptPath,
118565
- inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync97("/.dockerenv")
119636
+ inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync98("/.dockerenv")
118566
119637
  });
118567
119638
  if (detection.kind !== "static-binary") {
118568
119639
  say(source_default.gray(` host CLI not self-updated: ${detection.reason}
@@ -118645,7 +119716,7 @@ function planUpdate(opts) {
118645
119716
  steps.push({
118646
119717
  name: "pull-images",
118647
119718
  description: "Pull broker / kernel / agent images from GHCR",
118648
- skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync97(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
119719
+ skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync98(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
118649
119720
  run: () => {
118650
119721
  const r = runner("docker", [
118651
119722
  "compose",
@@ -118902,13 +119973,13 @@ function planUpdate(opts) {
118902
119973
  bundleDir: import.meta.dirname,
118903
119974
  execPath: process.execPath
118904
119975
  });
118905
- const dest = join101(homedir51(), ".switchroom", "skills", "_bundled");
119976
+ const dest = join102(homedir51(), ".switchroom", "skills", "_bundled");
118906
119977
  if (resolution.path === null) {
118907
119978
  const detection = detectInstallKind(opts.installProbe ?? {
118908
119979
  bundleDir: import.meta.dirname,
118909
119980
  execPath: process.execPath,
118910
119981
  scriptPath,
118911
- inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync97("/.dockerenv")
119982
+ inContainer: process.env.SWITCHROOM_HOSTD_CONTEXT === "1" || existsSync98("/.dockerenv")
118912
119983
  });
118913
119984
  const detail = `no shipped skills/ payload found for this install ` + `(${detection.kind}; ${describeShippedAssetSearch(resolution)})`;
118914
119985
  if (PACKAGED_INSTALL_KINDS.has(detection.kind)) {
@@ -118947,11 +120018,11 @@ function planUpdate(opts) {
118947
120018
  run: () => {
118948
120019
  if (opts.syncBundledSkillsFn)
118949
120020
  return;
118950
- const dest = join101(homedir51(), ".switchroom", "skills", "_bundled");
118951
- if (!existsSync97(dest)) {
120021
+ const dest = join102(homedir51(), ".switchroom", "skills", "_bundled");
120022
+ if (!existsSync98(dest)) {
118952
120023
  return;
118953
120024
  }
118954
- const missing = getBuiltinDefaultSkillEntries().map((e) => e.key).filter((key) => !existsSync97(join101(dest, key)));
120025
+ const missing = getBuiltinDefaultSkillEntries().map((e) => e.key).filter((key) => !existsSync98(join102(dest, key)));
118955
120026
  if (missing.length > 0) {
118956
120027
  throw new Error(`verify-bundled-skills: builtin default skill(s) missing from the pool after sync: ` + `${missing.join(", ")}. These ship in the CLI package and must exist in ${dest}. ` + `This is a broken sync or a packaging regression \u2014 the pool is not converged.`);
118957
120028
  }
@@ -118986,7 +120057,7 @@ function planUpdate(opts) {
118986
120057
  description: "docker compose up -d --remove-orphans (recreates services with new images / compose)",
118987
120058
  run: () => {
118988
120059
  try {
118989
- const composeText = readFileSync88(composePath, "utf8");
120060
+ const composeText = readFileSync89(composePath, "utf8");
118990
120061
  const pf = validateBindSources(composeText);
118991
120062
  if (!pf.ok)
118992
120063
  throw new Error(formatPreflightError(pf));
@@ -119061,14 +120132,14 @@ function defaultStatusProbe(composePath) {
119061
120132
  } catch {}
119062
120133
  if (scriptPath) {
119063
120134
  try {
119064
- cliBuiltAt = new Date(statSync56(scriptPath).mtimeMs).toISOString();
120135
+ cliBuiltAt = new Date(statSync57(scriptPath).mtimeMs).toISOString();
119065
120136
  } catch {}
119066
120137
  let dir = dirname42(scriptPath);
119067
120138
  for (let i2 = 0;i2 < 8; i2++) {
119068
- const pkgPath = join101(dir, "package.json");
119069
- if (existsSync97(pkgPath)) {
120139
+ const pkgPath = join102(dir, "package.json");
120140
+ if (existsSync98(pkgPath)) {
119070
120141
  try {
119071
- const pkg = JSON.parse(readFileSync88(pkgPath, "utf-8"));
120142
+ const pkg = JSON.parse(readFileSync89(pkgPath, "utf-8"));
119072
120143
  if (typeof pkg.version === "string")
119073
120144
  cliVersion = pkg.version;
119074
120145
  } catch (err) {
@@ -119089,7 +120160,7 @@ function defaultStatusProbe(composePath) {
119089
120160
  warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
119090
120161
  }
119091
120162
  const services = [];
119092
- if (!existsSync97(composePath)) {
120163
+ if (!existsSync98(composePath)) {
119093
120164
  warnings.push(`compose file not found at ${composePath}; service status unknown`);
119094
120165
  return { cliVersion, cliBuiltAt, services, warnings };
119095
120166
  }
@@ -119352,22 +120423,22 @@ function registerUpdateCommand(program3) {
119352
120423
  // src/cli/rollout.ts
119353
120424
  init_helpers();
119354
120425
  import { spawnSync as spawnSync22 } from "node:child_process";
119355
- import { readFileSync as readFileSync90, chownSync as chownSync14, statSync as statSync59 } from "node:fs";
120426
+ import { readFileSync as readFileSync91, chownSync as chownSync14, statSync as statSync60 } from "node:fs";
119356
120427
  import { homedir as homedir53 } from "node:os";
119357
120428
 
119358
120429
  // src/cli/rollout-pin-journal.ts
119359
120430
  import {
119360
- existsSync as existsSync98,
119361
- readFileSync as readFileSync89,
120431
+ existsSync as existsSync99,
120432
+ readFileSync as readFileSync90,
119362
120433
  writeFileSync as writeFileSync39,
119363
120434
  renameSync as renameSync26,
119364
120435
  unlinkSync as unlinkSync19,
119365
- statSync as statSync57,
120436
+ statSync as statSync58,
119366
120437
  mkdirSync as mkdirSync57
119367
120438
  } from "node:fs";
119368
120439
  import { homedir as homedir52 } from "node:os";
119369
- import { createHash as createHash18 } from "node:crypto";
119370
- import { join as join102, basename as basename9, resolve as resolve50, dirname as dirname43 } from "node:path";
120440
+ import { createHash as createHash19 } from "node:crypto";
120441
+ import { join as join103, basename as basename9, resolve as resolve51, dirname as dirname43 } from "node:path";
119371
120442
  init_flock();
119372
120443
  var PIN_JOURNAL_MAX_AGE_MS = 15 * 60 * 1000;
119373
120444
  var STATE_DIR_NAME = ".switchroom";
@@ -119376,16 +120447,16 @@ function pinJournalDir(configPath) {
119376
120447
  if (override && override.trim().length > 0)
119377
120448
  return override.trim();
119378
120449
  if (configPath) {
119379
- const dir = dirname43(resolve50(configPath));
120450
+ const dir = dirname43(resolve51(configPath));
119380
120451
  if (basename9(dir) === STATE_DIR_NAME)
119381
120452
  return dir;
119382
120453
  }
119383
- return join102(homedir52(), STATE_DIR_NAME);
120454
+ return join103(homedir52(), STATE_DIR_NAME);
119384
120455
  }
119385
120456
  function pinJournalPath(configPath) {
119386
- const abs = resolve50(configPath);
119387
- const key = createHash18("sha256").update(abs).digest("hex").slice(0, 12);
119388
- return join102(pinJournalDir(abs), `.rollout-pin-journal.${basename9(abs)}.${key}.json`);
120457
+ const abs = resolve51(configPath);
120458
+ const key = createHash19("sha256").update(abs).digest("hex").slice(0, 12);
120459
+ return join103(pinJournalDir(abs), `.rollout-pin-journal.${basename9(abs)}.${key}.json`);
119389
120460
  }
119390
120461
  function isPidAlive(pid) {
119391
120462
  if (!Number.isInteger(pid) || pid <= 0)
@@ -119419,9 +120490,9 @@ function readPinJournal(configPath, warn = (m) => process.stderr.write(m)) {
119419
120490
  const p = pinJournalPath(configPath);
119420
120491
  let raw;
119421
120492
  try {
119422
- raw = readFileSync89(p, "utf8");
120493
+ raw = readFileSync90(p, "utf8");
119423
120494
  } catch (e) {
119424
- if (existsSync98(p)) {
120495
+ if (existsSync99(p)) {
119425
120496
  warn(`\u26a0\ufe0f rollout pin journal: ${p} exists but could not be read ` + `(${e.message}). A provisional \`release.pin\` may be ` + `uncommitted \u2014 verify it host-side before the next reconcile.
119426
120497
  `);
119427
120498
  }
@@ -119472,7 +120543,7 @@ function beginPinPersist(configPath, pin, opts = {}) {
119472
120543
  warn(`\u26a0\ufe0f rollout pin journal: overwriting an ABANDONED journal at ${p} ` + `(pid ${existing.pid} recorded ${existing.at}, provisional pin ` + `${existing.pin}). Its roll never committed or reverted; \`release.pin\` ` + `in ${configPath} may still name that unproven build \u2014 verify it.
119473
120544
  `);
119474
120545
  }
119475
- const priorPin = getReleasePinFromConfig(readFileSync89(configPath, "utf8"));
120546
+ const priorPin = getReleasePinFromConfig(readFileSync90(configPath, "utf8"));
119476
120547
  const journal = {
119477
120548
  v: 1,
119478
120549
  configPath,
@@ -119493,7 +120564,7 @@ function commitPinPersist(configPath) {
119493
120564
  unlinkSync19(p);
119494
120565
  return null;
119495
120566
  } catch (e) {
119496
- if (!existsSync98(p))
120567
+ if (!existsSync99(p))
119497
120568
  return null;
119498
120569
  return `rollout pin journal: FAILED to clear ${p} after a SUCCESSFUL roll ` + `(${e.message}). Delete it host-side \u2014 while it exists, ` + `recovery may revert a proven \`release.pin\`.`;
119499
120570
  }
@@ -119515,11 +120586,11 @@ function rollbackPinPersist(configPath, opts = {}) {
119515
120586
  }
119516
120587
  }
119517
120588
  try {
119518
- const current = readFileSync89(configPath, "utf8");
120589
+ const current = readFileSync90(configPath, "utf8");
119519
120590
  const next = journal.priorPin ? setReleasePinInConfig(current, journal.priorPin) : deleteReleasePinInConfig(current);
119520
120591
  let mode = 384;
119521
120592
  try {
119522
- mode = statSync57(configPath).mode & 511;
120593
+ mode = statSync58(configPath).mode & 511;
119523
120594
  } catch {}
119524
120595
  if (opts.writeConfig) {
119525
120596
  opts.writeConfig(configPath, next, mode);
@@ -119569,8 +120640,8 @@ init_host_cli_stamp();
119569
120640
 
119570
120641
  // src/cli/host-cli-upgrade.ts
119571
120642
  init_self_update();
119572
- import { lchownSync, lstatSync as lstatSync14, readdirSync as readdirSync33, statSync as statSync58 } from "node:fs";
119573
- import { basename as basename10, dirname as dirname44, join as join103 } from "node:path";
120643
+ import { lchownSync, lstatSync as lstatSync14, readdirSync as readdirSync34, statSync as statSync59 } from "node:fs";
120644
+ import { basename as basename10, dirname as dirname44, join as join104 } from "node:path";
119574
120645
  init_shipped_assets();
119575
120646
  var HOST_CLI_UPGRADE_SENTINEL = "SWITCHROOM_HOST_CLI_UPGRADE:";
119576
120647
  function encodeHostCliUpgradeResult(r) {
@@ -119606,7 +120677,7 @@ function defaultIo() {
119606
120677
  return {
119607
120678
  isFile: (p) => {
119608
120679
  try {
119609
- return statSync58(p).isFile();
120680
+ return statSync59(p).isFile();
119610
120681
  } catch {
119611
120682
  return false;
119612
120683
  }
@@ -119636,7 +120707,7 @@ function defaultIo() {
119636
120707
  chown: (p, uid, gid) => lchownSync(p, uid, gid),
119637
120708
  list: (d) => {
119638
120709
  try {
119639
- return readdirSync33(d);
120710
+ return readdirSync34(d);
119640
120711
  } catch {
119641
120712
  return [];
119642
120713
  }
@@ -119667,7 +120738,7 @@ function handBackOwnership(paths, owner, io) {
119667
120738
  if (kind !== "dir")
119668
120739
  continue;
119669
120740
  for (const child of io.list(p))
119670
- stack.push(join103(p, child));
120741
+ stack.push(join104(p, child));
119671
120742
  }
119672
120743
  return failures;
119673
120744
  }
@@ -119706,7 +120777,7 @@ async function runHostCliUpgrade(opts, io = defaultIo(), log = () => {}) {
119706
120777
  const installRoot = payloadInstallRoot(binary);
119707
120778
  const failures = handBackOwnership([
119708
120779
  binary,
119709
- join103(dirname44(binary), ".switchroom-versions"),
120780
+ join104(dirname44(binary), ".switchroom-versions"),
119710
120781
  dirname44(installRoot),
119711
120782
  installRoot,
119712
120783
  payloadVersionDir(installRoot, pin)
@@ -119802,21 +120873,21 @@ function planHostCliHeal(opts) {
119802
120873
  reason: `the host CLI is a ${stamp.installKind} install \u2014 only a static-binary ` + `install can be replaced by swapping one file, so an operator must run ` + `the upgrade host-side`
119803
120874
  };
119804
120875
  }
119805
- const path7 = stamp.path;
119806
- if (!path7.startsWith("/") || path7.includes("/..")) {
119807
- return { action: "skip", reason: `recorded host CLI path "${path7}" is not a plain absolute path` };
120876
+ const path9 = stamp.path;
120877
+ if (!path9.startsWith("/") || path9.includes("/..")) {
120878
+ return { action: "skip", reason: `recorded host CLI path "${path9}" is not a plain absolute path` };
119808
120879
  }
119809
- const installDir = path7.slice(0, path7.lastIndexOf("/"));
119810
- const name = path7.slice(path7.lastIndexOf("/") + 1);
120880
+ const installDir = path9.slice(0, path9.lastIndexOf("/"));
120881
+ const name = path9.slice(path9.lastIndexOf("/") + 1);
119811
120882
  if (name !== "switchroom") {
119812
- return { action: "skip", reason: `recorded host CLI path "${path7}" is not named \`switchroom\`` };
120883
+ return { action: "skip", reason: `recorded host CLI path "${path9}" is not named \`switchroom\`` };
119813
120884
  }
119814
120885
  const prefixHostPath = installDir.slice(0, installDir.lastIndexOf("/"));
119815
120886
  const dirName = installDir.slice(installDir.lastIndexOf("/") + 1);
119816
120887
  if (prefixHostPath.length === 0 || dirName.length === 0) {
119817
120888
  return {
119818
120889
  action: "skip",
119819
- reason: `recorded host CLI path "${path7}" has no install prefix to bind \u2014 ` + `refusing to mount the host root`
120890
+ reason: `recorded host CLI path "${path9}" has no install prefix to bind \u2014 ` + `refusing to mount the host root`
119820
120891
  };
119821
120892
  }
119822
120893
  return {
@@ -120501,13 +121572,13 @@ function createRolloutDeps(params) {
120501
121572
  }
120502
121573
  return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
120503
121574
  };
120504
- const writeConfigPreservingOwnership = (path7, text, mode) => {
120505
- writeConfigFileSync(path7, text, mode);
121575
+ const writeConfigPreservingOwnership = (path9, text, mode) => {
121576
+ writeConfigFileSync(path9, text, mode);
120506
121577
  try {
120507
121578
  if (typeof process.geteuid === "function" && process.geteuid() === 0) {
120508
121579
  const uid = resolveOperatorUid();
120509
121580
  if (uid !== undefined) {
120510
- chownSync14(path7, uid, uid);
121581
+ chownSync14(path9, uid, uid);
120511
121582
  } else if (hostdCtx) {
120512
121583
  warn("\u26a0\ufe0f rollout (hostd path): could not resolve operator uid; " + "skipping config chown-back to avoid leaving switchroom.yaml " + "root-owned. Verify ownership host-side if other yaml verbs " + `EACCES.
120513
121584
  `);
@@ -120566,12 +121637,12 @@ function createRolloutDeps(params) {
120566
121637
  return stamp ? { hostCli: stamp } : {};
120567
121638
  })(),
120568
121639
  persistPin: (pin) => {
120569
- const before = readFileSync90(configPath, "utf8");
121640
+ const before = readFileSync91(configPath, "utf8");
120570
121641
  const after = setReleasePinInConfig(before, pin);
120571
121642
  if (after === before)
120572
121643
  return false;
120573
121644
  beginPinPersist(configPath, pin);
120574
- writeConfigPreservingOwnership(configPath, after, statSync59(configPath).mode & 511);
121645
+ writeConfigPreservingOwnership(configPath, after, statSync60(configPath).mode & 511);
120575
121646
  return true;
120576
121647
  },
120577
121648
  commitPin: () => {
@@ -120847,7 +121918,7 @@ init_source();
120847
121918
  init_helpers();
120848
121919
  init_loader();
120849
121920
  init_lifecycle();
120850
- import { resolve as resolve51 } from "node:path";
121921
+ import { resolve as resolve52 } from "node:path";
120851
121922
 
120852
121923
  // src/cli/version.ts
120853
121924
  init_source();
@@ -120855,8 +121926,8 @@ init_helpers();
120855
121926
  init_lifecycle();
120856
121927
  init_resolve_version();
120857
121928
  import { execSync as execSync3 } from "node:child_process";
120858
- import { existsSync as existsSync99, readFileSync as readFileSync91 } from "node:fs";
120859
- import { dirname as dirname45, join as join104 } from "node:path";
121929
+ import { existsSync as existsSync100, readFileSync as readFileSync92 } from "node:fs";
121930
+ import { dirname as dirname45, join as join105 } from "node:path";
120860
121931
  function getClaudeCodeVersion() {
120861
121932
  try {
120862
121933
  const out = execSync3("claude --version 2>/dev/null", {
@@ -120906,11 +121977,11 @@ function formatUptime3(timestamp) {
120906
121977
  function locateSwitchroomInstallDir() {
120907
121978
  let dir = import.meta.dirname;
120908
121979
  for (let i2 = 0;i2 < 10 && dir && dir !== "/"; i2++) {
120909
- const pkgPath = join104(dir, "package.json");
120910
- if (existsSync99(pkgPath)) {
121980
+ const pkgPath = join105(dir, "package.json");
121981
+ if (existsSync100(pkgPath)) {
120911
121982
  try {
120912
- const pkg = JSON.parse(readFileSync91(pkgPath, "utf-8"));
120913
- if (pkg.name === "switchroom" && existsSync99(join104(dir, ".git"))) {
121983
+ const pkg = JSON.parse(readFileSync92(pkgPath, "utf-8"));
121984
+ if (pkg.name === "switchroom" && existsSync100(join105(dir, ".git"))) {
120914
121985
  return dir;
120915
121986
  }
120916
121987
  } catch {}
@@ -121003,7 +122074,7 @@ function registerRestartCommand(program3) {
121003
122074
  }
121004
122075
  const didRestart = res.restarted || !graceful;
121005
122076
  if (didRestart) {
121006
- const agentDir = resolve51(agentsDir, name);
122077
+ const agentDir = resolve52(agentsDir, name);
121007
122078
  const converged = waitForAuthConverge(name, agentDir);
121008
122079
  if (!converged) {
121009
122080
  console.log(source_default.yellow(` ${name}: agent is up but auth status didn't converge in 30s \u2014 check logs`));
@@ -121086,37 +122157,37 @@ Dependency manifest`));
121086
122157
  // src/cli/handoff.ts
121087
122158
  init_helpers();
121088
122159
  init_loader();
121089
- import { resolve as resolve52 } from "node:path";
122160
+ import { resolve as resolve53 } from "node:path";
121090
122161
  init_merge();
121091
122162
 
121092
122163
  // src/agents/session-retention.ts
121093
122164
  import {
121094
- existsSync as existsSync100,
121095
- readdirSync as readdirSync34,
121096
- statSync as statSync60,
122165
+ existsSync as existsSync101,
122166
+ readdirSync as readdirSync35,
122167
+ statSync as statSync61,
121097
122168
  unlinkSync as unlinkSync20
121098
122169
  } from "node:fs";
121099
- import { join as join105 } from "node:path";
122170
+ import { join as join106 } from "node:path";
121100
122171
  var DEFAULT_SESSION_RETENTION_MAX_COUNT = 20;
121101
122172
  var DEFAULT_SESSION_RETENTION_MAX_AGE_DAYS = 30;
121102
122173
  var MIN_KEEP = 2;
121103
122174
  function collectSessionJsonl(claudeConfigDir) {
121104
- const projects = join105(claudeConfigDir, "projects");
121105
- if (!existsSync100(projects))
122175
+ const projects = join106(claudeConfigDir, "projects");
122176
+ if (!existsSync101(projects))
121106
122177
  return [];
121107
122178
  const found = [];
121108
122179
  const walk2 = (dir) => {
121109
122180
  let entries;
121110
122181
  try {
121111
- entries = readdirSync34(dir);
122182
+ entries = readdirSync35(dir);
121112
122183
  } catch {
121113
122184
  return;
121114
122185
  }
121115
122186
  for (const name of entries) {
121116
- const full = join105(dir, name);
122187
+ const full = join106(dir, name);
121117
122188
  let st;
121118
122189
  try {
121119
- st = statSync60(full);
122190
+ st = statSync61(full);
121120
122191
  } catch {
121121
122192
  continue;
121122
122193
  }
@@ -121187,7 +122258,7 @@ function registerHandoffCommand(program3) {
121187
122258
  }
121188
122259
  agentConfig = resolveAgentConfig(config.defaults, config.profiles, rawAgent);
121189
122260
  const agentsDir = resolveAgentsDir(config);
121190
- agentDir = resolve52(agentsDir, agentName);
122261
+ agentDir = resolve53(agentsDir, agentName);
121191
122262
  } catch (err) {
121192
122263
  if (!(err instanceof ConfigError))
121193
122264
  throw err;
@@ -121202,7 +122273,7 @@ function registerHandoffCommand(program3) {
121202
122273
  `);
121203
122274
  return;
121204
122275
  }
121205
- const claudeConfigDir = resolve52(agentDir, ".claude");
122276
+ const claudeConfigDir = resolve53(agentDir, ".claude");
121206
122277
  const jsonl = findLatestSessionJsonl(claudeConfigDir);
121207
122278
  if (!jsonl) {
121208
122279
  process.stderr.write(`handoff: no session JSONL under ${claudeConfigDir}/projects; skipping
@@ -121234,18 +122305,18 @@ function registerHandoffCommand(program3) {
121234
122305
  // src/issues/store.ts
121235
122306
  import {
121236
122307
  closeSync as closeSync19,
121237
- existsSync as existsSync101,
122308
+ existsSync as existsSync102,
121238
122309
  mkdirSync as mkdirSync58,
121239
122310
  openSync as openSync19,
121240
- readdirSync as readdirSync35,
121241
- readFileSync as readFileSync92,
122311
+ readdirSync as readdirSync36,
122312
+ readFileSync as readFileSync93,
121242
122313
  renameSync as renameSync27,
121243
- statSync as statSync61,
122314
+ statSync as statSync62,
121244
122315
  unlinkSync as unlinkSync21,
121245
122316
  writeFileSync as writeFileSync40,
121246
122317
  writeSync as writeSync9
121247
122318
  } from "node:fs";
121248
- import { join as join106 } from "node:path";
122319
+ import { join as join107 } from "node:path";
121249
122320
  import { randomBytes as randomBytes16 } from "node:crypto";
121250
122321
  import { execSync as execSync4 } from "node:child_process";
121251
122322
 
@@ -121272,12 +122343,12 @@ function computeFingerprint(source, code) {
121272
122343
  var ISSUES_FILE = "issues.jsonl";
121273
122344
  var ISSUES_LOCK = "issues.lock";
121274
122345
  function readAll(stateDir) {
121275
- const path7 = join106(stateDir, ISSUES_FILE);
121276
- if (!existsSync101(path7))
122346
+ const path9 = join107(stateDir, ISSUES_FILE);
122347
+ if (!existsSync102(path9))
121277
122348
  return [];
121278
122349
  let raw;
121279
122350
  try {
121280
- raw = readFileSync92(path7, "utf-8");
122351
+ raw = readFileSync93(path9, "utf-8");
121281
122352
  } catch {
121282
122353
  return [];
121283
122354
  }
@@ -121349,8 +122420,8 @@ function record(stateDir, input, nowFn = Date.now) {
121349
122420
  return result;
121350
122421
  });
121351
122422
  }
121352
- function resolve53(stateDir, fingerprint, nowFn = Date.now) {
121353
- if (!existsSync101(join106(stateDir, ISSUES_FILE)))
122423
+ function resolve54(stateDir, fingerprint, nowFn = Date.now) {
122424
+ if (!existsSync102(join107(stateDir, ISSUES_FILE)))
121354
122425
  return 0;
121355
122426
  return withLock(stateDir, () => {
121356
122427
  const all = readAll(stateDir);
@@ -121368,7 +122439,7 @@ function resolve53(stateDir, fingerprint, nowFn = Date.now) {
121368
122439
  });
121369
122440
  }
121370
122441
  function resolveAllBySource(stateDir, source, nowFn = Date.now) {
121371
- if (!existsSync101(join106(stateDir, ISSUES_FILE)))
122442
+ if (!existsSync102(join107(stateDir, ISSUES_FILE)))
121372
122443
  return 0;
121373
122444
  return withLock(stateDir, () => {
121374
122445
  const all = readAll(stateDir);
@@ -121386,7 +122457,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
121386
122457
  });
121387
122458
  }
121388
122459
  function prune(stateDir, opts = {}) {
121389
- if (!existsSync101(join106(stateDir, ISSUES_FILE)))
122460
+ if (!existsSync102(join107(stateDir, ISSUES_FILE)))
121390
122461
  return 0;
121391
122462
  return withLock(stateDir, () => {
121392
122463
  const all = readAll(stateDir);
@@ -121419,21 +122490,21 @@ function ensureDir2(stateDir) {
121419
122490
  mkdirSync58(stateDir, { recursive: true });
121420
122491
  }
121421
122492
  function writeAll(stateDir, events) {
121422
- const path7 = join106(stateDir, ISSUES_FILE);
122493
+ const path9 = join107(stateDir, ISSUES_FILE);
121423
122494
  sweepOrphanTmpFiles(stateDir);
121424
- const tmp = `${path7}.tmp-${process.pid}-${randomBytes16(4).toString("hex")}`;
122495
+ const tmp = `${path9}.tmp-${process.pid}-${randomBytes16(4).toString("hex")}`;
121425
122496
  const body = events.length === 0 ? "" : events.map((e) => JSON.stringify(e)).join(`
121426
122497
  `) + `
121427
122498
  `;
121428
122499
  writeFileSync40(tmp, body, "utf-8");
121429
- renameSync27(tmp, path7);
122500
+ renameSync27(tmp, path9);
121430
122501
  }
121431
122502
  var ORPHAN_TMP_TTL_MS = 60000;
121432
122503
  var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
121433
122504
  function sweepOrphanTmpFiles(stateDir) {
121434
122505
  let entries;
121435
122506
  try {
121436
- entries = readdirSync35(stateDir);
122507
+ entries = readdirSync36(stateDir);
121437
122508
  } catch {
121438
122509
  return;
121439
122510
  }
@@ -121441,10 +122512,10 @@ function sweepOrphanTmpFiles(stateDir) {
121441
122512
  for (const entry of entries) {
121442
122513
  if (!entry.startsWith(TMP_PREFIX))
121443
122514
  continue;
121444
- const tmpPath = join106(stateDir, entry);
122515
+ const tmpPath = join107(stateDir, entry);
121445
122516
  try {
121446
- const stat = statSync61(tmpPath);
121447
- if (stat.mtimeMs < cutoff) {
122517
+ const stat2 = statSync62(tmpPath);
122518
+ if (stat2.mtimeMs < cutoff) {
121448
122519
  unlinkSync21(tmpPath);
121449
122520
  }
121450
122521
  } catch {}
@@ -121453,7 +122524,7 @@ function sweepOrphanTmpFiles(stateDir) {
121453
122524
  var LOCK_RETRY_MS = 25;
121454
122525
  var LOCK_TIMEOUT_MS = 1e4;
121455
122526
  function withLock(stateDir, fn) {
121456
- const lockPath = join106(stateDir, ISSUES_LOCK);
122527
+ const lockPath = join107(stateDir, ISSUES_LOCK);
121457
122528
  const startedAt = Date.now();
121458
122529
  let fd = null;
121459
122530
  while (fd === null) {
@@ -121488,7 +122559,7 @@ function withLock(stateDir, fn) {
121488
122559
  function tryStealStaleLock(lockPath) {
121489
122560
  let pidStr;
121490
122561
  try {
121491
- pidStr = readFileSync92(lockPath, "utf-8").trim();
122562
+ pidStr = readFileSync93(lockPath, "utf-8").trim();
121492
122563
  } catch {
121493
122564
  return true;
121494
122565
  }
@@ -121637,11 +122708,11 @@ function registerIssuesCommand(program3) {
121637
122708
  const stateDir = resolveStateDir2(opts);
121638
122709
  let flipped;
121639
122710
  if (opts.source && opts.code) {
121640
- flipped = resolve53(stateDir, computeFingerprint(opts.source, opts.code));
122711
+ flipped = resolve54(stateDir, computeFingerprint(opts.source, opts.code));
121641
122712
  } else if (opts.source && !fingerprint) {
121642
122713
  flipped = resolveAllBySource(stateDir, opts.source);
121643
122714
  } else if (fingerprint) {
121644
- flipped = resolve53(stateDir, fingerprint);
122715
+ flipped = resolve54(stateDir, fingerprint);
121645
122716
  } else {
121646
122717
  process.stderr.write(`issues resolve: need either <fingerprint>, --source, or --source + --code
121647
122718
  `);
@@ -121736,20 +122807,20 @@ function relTime(deltaMs) {
121736
122807
 
121737
122808
  // src/cli/deps.ts
121738
122809
  init_source();
121739
- import { existsSync as existsSync104 } from "node:fs";
122810
+ import { existsSync as existsSync105 } from "node:fs";
121740
122811
  import { homedir as homedir56 } from "node:os";
121741
- import { join as join109, resolve as resolve54 } from "node:path";
122812
+ import { join as join110, resolve as resolve55 } from "node:path";
121742
122813
 
121743
122814
  // src/deps/python.ts
121744
- import { createHash as createHash19 } from "node:crypto";
122815
+ import { createHash as createHash20 } from "node:crypto";
121745
122816
  import {
121746
- existsSync as existsSync102,
122817
+ existsSync as existsSync103,
121747
122818
  mkdirSync as mkdirSync59,
121748
- readFileSync as readFileSync93,
122819
+ readFileSync as readFileSync94,
121749
122820
  rmSync as rmSync19,
121750
122821
  writeFileSync as writeFileSync41
121751
122822
  } from "node:fs";
121752
- import { dirname as dirname46, join as join107 } from "node:path";
122823
+ import { dirname as dirname46, join as join108 } from "node:path";
121753
122824
  import { homedir as homedir54 } from "node:os";
121754
122825
  import { execFileSync as execFileSync31 } from "node:child_process";
121755
122826
 
@@ -121762,26 +122833,26 @@ class PythonEnvError extends Error {
121762
122833
  }
121763
122834
  }
121764
122835
  function defaultPythonCacheRoot() {
121765
- return join107(homedir54(), ".switchroom", "deps", "python");
122836
+ return join108(homedir54(), ".switchroom", "deps", "python");
121766
122837
  }
121767
- function hashFile(path7) {
121768
- return createHash19("sha256").update(readFileSync93(path7)).digest("hex");
122838
+ function hashFile(path9) {
122839
+ return createHash20("sha256").update(readFileSync94(path9)).digest("hex");
121769
122840
  }
121770
122841
  function ensurePythonEnv(opts) {
121771
122842
  const { skillName, requirementsPath, force = false } = opts;
121772
122843
  const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
121773
122844
  const hostPython = opts.pythonBin ?? "python3";
121774
- if (!existsSync102(requirementsPath)) {
122845
+ if (!existsSync103(requirementsPath)) {
121775
122846
  throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
121776
122847
  }
121777
- const venvDir = join107(cacheRoot, skillName);
121778
- const stampPath = join107(venvDir, ".requirements.sha256");
121779
- const binDir = join107(venvDir, "bin");
121780
- const pythonBin = join107(binDir, "python");
121781
- const pipBin = join107(binDir, "pip");
122848
+ const venvDir = join108(cacheRoot, skillName);
122849
+ const stampPath = join108(venvDir, ".requirements.sha256");
122850
+ const binDir = join108(venvDir, "bin");
122851
+ const pythonBin = join108(binDir, "python");
122852
+ const pipBin = join108(binDir, "pip");
121782
122853
  const targetHash = hashFile(requirementsPath);
121783
- if (!force && existsSync102(stampPath) && existsSync102(pythonBin)) {
121784
- const existingHash = readFileSync93(stampPath, "utf8").trim();
122854
+ if (!force && existsSync103(stampPath) && existsSync103(pythonBin)) {
122855
+ const existingHash = readFileSync94(stampPath, "utf8").trim();
121785
122856
  if (existingHash === targetHash) {
121786
122857
  return {
121787
122858
  skillName,
@@ -121793,7 +122864,7 @@ function ensurePythonEnv(opts) {
121793
122864
  };
121794
122865
  }
121795
122866
  }
121796
- if (existsSync102(venvDir)) {
122867
+ if (existsSync103(venvDir)) {
121797
122868
  rmSync19(venvDir, { recursive: true, force: true });
121798
122869
  }
121799
122870
  mkdirSync59(dirname46(venvDir), { recursive: true });
@@ -121828,16 +122899,16 @@ function ensurePythonEnv(opts) {
121828
122899
  }
121829
122900
 
121830
122901
  // src/deps/node.ts
121831
- import { createHash as createHash20 } from "node:crypto";
122902
+ import { createHash as createHash21 } from "node:crypto";
121832
122903
  import {
121833
122904
  copyFileSync as copyFileSync13,
121834
- existsSync as existsSync103,
122905
+ existsSync as existsSync104,
121835
122906
  mkdirSync as mkdirSync60,
121836
- readFileSync as readFileSync94,
122907
+ readFileSync as readFileSync95,
121837
122908
  rmSync as rmSync20,
121838
122909
  writeFileSync as writeFileSync42
121839
122910
  } from "node:fs";
121840
- import { dirname as dirname47, join as join108 } from "node:path";
122911
+ import { dirname as dirname47, join as join109 } from "node:path";
121841
122912
  import { homedir as homedir55 } from "node:os";
121842
122913
  import { execFileSync as execFileSync32 } from "node:child_process";
121843
122914
 
@@ -121845,614 +122916,188 @@ class NodeEnvError extends Error {
121845
122916
  stderr;
121846
122917
  constructor(message, stderr) {
121847
122918
  super(message);
121848
- this.name = "NodeEnvError";
121849
- this.stderr = stderr;
121850
- }
121851
- }
121852
- var ALL_LOCKFILES = [
121853
- "bun.lock",
121854
- "bun.lockb",
121855
- "package-lock.json",
121856
- "pnpm-lock.yaml",
121857
- "yarn.lock"
121858
- ];
121859
- var LOCKFILES_FOR = {
121860
- bun: ["bun.lock", "bun.lockb"],
121861
- npm: ["package-lock.json"]
121862
- };
121863
- function defaultNodeCacheRoot() {
121864
- return join108(homedir55(), ".switchroom", "deps", "node");
121865
- }
121866
- function hashDepInputs(packageJsonPath) {
121867
- const sourceDir = dirname47(packageJsonPath);
121868
- const hasher = createHash20("sha256");
121869
- hasher.update(`package.json
121870
- `);
121871
- hasher.update(readFileSync94(packageJsonPath));
121872
- for (const lockName of ALL_LOCKFILES) {
121873
- const lockPath = join108(sourceDir, lockName);
121874
- if (existsSync103(lockPath)) {
121875
- hasher.update(`
121876
- `);
121877
- hasher.update(lockName);
121878
- hasher.update(`
121879
- `);
121880
- hasher.update(readFileSync94(lockPath));
121881
- }
121882
- }
121883
- return hasher.digest("hex");
121884
- }
121885
- function ensureNodeEnv(opts) {
121886
- const { skillName, packageJsonPath, force = false } = opts;
121887
- const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
121888
- const installer = opts.installer ?? "bun";
121889
- if (!existsSync103(packageJsonPath)) {
121890
- throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
121891
- }
121892
- const sourceDir = dirname47(packageJsonPath);
121893
- const envDir = join108(cacheRoot, skillName);
121894
- const stampPath = join108(envDir, ".package.sha256");
121895
- const nodeModulesDir = join108(envDir, "node_modules");
121896
- const binDir = join108(nodeModulesDir, ".bin");
121897
- const targetHash = hashDepInputs(packageJsonPath);
121898
- if (!force && existsSync103(stampPath) && existsSync103(nodeModulesDir)) {
121899
- const existingHash = readFileSync94(stampPath, "utf8").trim();
121900
- if (existingHash === targetHash) {
121901
- return {
121902
- skillName,
121903
- dir: envDir,
121904
- nodeModulesDir,
121905
- binDir,
121906
- rebuilt: false
121907
- };
121908
- }
121909
- }
121910
- if (existsSync103(envDir)) {
121911
- rmSync20(envDir, { recursive: true, force: true });
121912
- }
121913
- mkdirSync60(envDir, { recursive: true });
121914
- copyFileSync13(packageJsonPath, join108(envDir, "package.json"));
121915
- let copiedLockfile = false;
121916
- for (const lockName of LOCKFILES_FOR[installer]) {
121917
- const lockPath = join108(sourceDir, lockName);
121918
- if (existsSync103(lockPath)) {
121919
- copyFileSync13(lockPath, join108(envDir, lockName));
121920
- copiedLockfile = true;
121921
- }
121922
- }
121923
- try {
121924
- if (installer === "bun") {
121925
- const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
121926
- execFileSync32("bun", args, { cwd: envDir, stdio: "pipe" });
121927
- } else {
121928
- const args = copiedLockfile ? ["ci"] : ["install"];
121929
- execFileSync32("npm", args, { cwd: envDir, stdio: "pipe" });
121930
- }
121931
- } catch (err) {
121932
- const e = err;
121933
- throw new NodeEnvError(`Failed to install node deps for skill "${skillName}" with ${installer}: ${e.message}`, e.stderr?.toString());
121934
- }
121935
- writeFileSync42(stampPath, targetHash + `
121936
- `);
121937
- return {
121938
- skillName,
121939
- dir: envDir,
121940
- nodeModulesDir,
121941
- binDir,
121942
- rebuilt: true
121943
- };
121944
- }
121945
-
121946
- // src/cli/deps.ts
121947
- function builtinSkillsRoot() {
121948
- return resolve54(homedir56(), ".switchroom/skills/_bundled");
121949
- }
121950
- function registerDepsCommand(program3) {
121951
- const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
121952
- deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
121953
- const skillsRoot = builtinSkillsRoot();
121954
- if (!existsSync104(skillsRoot)) {
121955
- console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
121956
- process.exit(1);
121957
- }
121958
- const skillDir = join109(skillsRoot, skill);
121959
- if (!existsSync104(skillDir)) {
121960
- console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
121961
- process.exit(1);
121962
- }
121963
- const requirementsPath = join109(skillDir, "requirements.txt");
121964
- const packageJsonPath = join109(skillDir, "package.json");
121965
- const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync104(requirementsPath));
121966
- const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync104(packageJsonPath));
121967
- let did = 0;
121968
- if (wantPython) {
121969
- if (!existsSync104(requirementsPath)) {
121970
- console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
121971
- process.exit(1);
121972
- }
121973
- try {
121974
- console.log(source_default.gray(`Rebuilding Python env for ${skill}...`));
121975
- const env2 = ensurePythonEnv({
121976
- skillName: skill,
121977
- requirementsPath,
121978
- force: true
121979
- });
121980
- console.log(source_default.green(` \u2713 Python env at ${env2.venvDir} (python: ${env2.pythonBin})`));
121981
- did++;
121982
- } catch (err) {
121983
- if (err instanceof PythonEnvError) {
121984
- console.error(source_default.red(` Python env failed: ${err.message}`));
121985
- if (err.stderr)
121986
- console.error(source_default.dim(err.stderr));
121987
- process.exit(1);
121988
- }
121989
- throw err;
121990
- }
121991
- }
121992
- if (wantNode) {
121993
- if (!existsSync104(packageJsonPath)) {
121994
- console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
121995
- process.exit(1);
121996
- }
121997
- try {
121998
- console.log(source_default.gray(`Rebuilding Node env for ${skill}...`));
121999
- const env2 = ensureNodeEnv({
122000
- skillName: skill,
122001
- packageJsonPath,
122002
- force: true
122003
- });
122004
- console.log(source_default.green(` \u2713 Node env at ${env2.dir} (node_modules: ${env2.nodeModulesDir})`));
122005
- did++;
122006
- } catch (err) {
122007
- if (err instanceof NodeEnvError) {
122008
- console.error(source_default.red(` Node env failed: ${err.message}`));
122009
- if (err.stderr)
122010
- console.error(source_default.dim(err.stderr));
122011
- process.exit(1);
122012
- }
122013
- throw err;
122014
- }
122015
- }
122016
- if (did === 0) {
122017
- console.error(source_default.yellow(`Skill "${skill}" has neither requirements.txt nor package.json \u2014 nothing to rebuild.`));
122018
- process.exit(1);
122019
- }
122020
- });
122021
- }
122022
-
122023
- // src/cli/workspace.ts
122024
- init_helpers();
122025
- init_loader();
122026
- import { existsSync as existsSync105 } from "node:fs";
122027
- import { resolve as resolve55, sep as sep7 } from "node:path";
122028
- import { spawnSync as spawnSync23 } from "node:child_process";
122029
-
122030
- // src/agents/workspace.ts
122031
- import { readFile as readFile2, stat } from "node:fs/promises";
122032
- import path8 from "node:path";
122033
-
122034
- // src/agents/bootstrap-budget.ts
122035
- import path7 from "node:path";
122036
- var DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85;
122037
- var DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES = 3;
122038
- var DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX = 32;
122039
-
122040
- class BootstrapBudgetExceededError extends Error {
122041
- analysis;
122042
- constructor(analysis, message) {
122043
- super(message);
122044
- this.analysis = analysis;
122045
- this.name = "BootstrapBudgetExceededError";
122046
- }
122047
- }
122048
- function normalizePositiveLimit(value) {
122049
- if (!Number.isFinite(value) || value <= 0) {
122050
- return 1;
122051
- }
122052
- return Math.floor(value);
122053
- }
122054
- function formatWarningCause(cause) {
122055
- return cause === "per-file-limit" ? "max/file" : "max/total";
122056
- }
122057
- function normalizeSeenSignatures(signatures) {
122058
- if (!Array.isArray(signatures) || signatures.length === 0) {
122059
- return [];
122060
- }
122061
- const seen = new Set;
122062
- const result = [];
122063
- for (const signature of signatures) {
122064
- const value = typeof signature === "string" ? signature.trim() : "";
122065
- if (!value || seen.has(value)) {
122066
- continue;
122067
- }
122068
- seen.add(value);
122069
- result.push(value);
122070
- }
122071
- return result;
122072
- }
122073
- function appendSeenSignature(signatures, signature) {
122074
- if (!signature.trim()) {
122075
- return signatures;
122076
- }
122077
- if (signatures.includes(signature)) {
122078
- return signatures;
122079
- }
122080
- const next = [...signatures, signature];
122081
- if (next.length <= DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX) {
122082
- return next;
122083
- }
122084
- return next.slice(-DEFAULT_BOOTSTRAP_PROMPT_WARNING_SIGNATURE_HISTORY_MAX);
122085
- }
122086
- function buildBootstrapInjectionStats(params) {
122087
- const injectedByPath = new Map;
122088
- const injectedByBaseName = new Map;
122089
- for (const file of params.injectedFiles) {
122090
- const pathValue = typeof file.path === "string" ? file.path.trim() : "";
122091
- if (!pathValue) {
122092
- continue;
122093
- }
122094
- if (!injectedByPath.has(pathValue)) {
122095
- injectedByPath.set(pathValue, file.content);
122096
- }
122097
- const normalizedPath = pathValue.replace(/\\/g, "/");
122098
- const baseName = path7.posix.basename(normalizedPath);
122099
- if (!injectedByBaseName.has(baseName)) {
122100
- injectedByBaseName.set(baseName, file.content);
122101
- }
122102
- }
122103
- return params.bootstrapFiles.map((file) => {
122104
- const pathValue = typeof file.path === "string" ? file.path.trim() : "";
122105
- const rawChars = file.missing ? 0 : (file.content ?? "").trimEnd().length;
122106
- const injected = (pathValue ? injectedByPath.get(pathValue) : undefined) ?? injectedByPath.get(file.name) ?? injectedByBaseName.get(file.name);
122107
- const injectedChars = injected ? injected.length : 0;
122108
- const truncated = !file.missing && injectedChars < rawChars;
122109
- return {
122110
- name: file.name,
122111
- path: pathValue || file.name,
122112
- missing: file.missing,
122113
- rawChars,
122114
- injectedChars,
122115
- truncated
122116
- };
122117
- });
122118
- }
122119
- function analyzeBootstrapBudget(params) {
122120
- const bootstrapMaxChars = normalizePositiveLimit(params.bootstrapMaxChars);
122121
- const bootstrapTotalMaxChars = normalizePositiveLimit(params.bootstrapTotalMaxChars);
122122
- const nearLimitRatio = typeof params.nearLimitRatio === "number" && Number.isFinite(params.nearLimitRatio) && params.nearLimitRatio > 0 && params.nearLimitRatio < 1 ? params.nearLimitRatio : DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO;
122123
- const nonMissing = params.files.filter((file) => !file.missing);
122124
- const rawChars = nonMissing.reduce((sum, file) => sum + file.rawChars, 0);
122125
- const injectedChars = nonMissing.reduce((sum, file) => sum + file.injectedChars, 0);
122126
- const totalNearLimit = injectedChars >= Math.ceil(bootstrapTotalMaxChars * nearLimitRatio);
122127
- const totalOverLimit = injectedChars >= bootstrapTotalMaxChars;
122128
- const files = params.files.map((file) => {
122129
- if (file.missing) {
122130
- return { ...file, nearLimit: false, causes: [] };
122131
- }
122132
- const perFileOverLimit = file.rawChars > bootstrapMaxChars;
122133
- const nearLimit = file.rawChars >= Math.ceil(bootstrapMaxChars * nearLimitRatio);
122134
- const causes = [];
122135
- if (file.truncated) {
122136
- if (perFileOverLimit) {
122137
- causes.push("per-file-limit");
122138
- }
122139
- if (totalOverLimit) {
122140
- causes.push("total-limit");
122141
- }
122142
- }
122143
- return { ...file, nearLimit, causes };
122144
- });
122145
- const truncatedFiles = files.filter((file) => file.truncated);
122146
- const nearLimitFiles = files.filter((file) => file.nearLimit);
122147
- return {
122148
- files,
122149
- truncatedFiles,
122150
- nearLimitFiles,
122151
- totalNearLimit,
122152
- hasTruncation: truncatedFiles.length > 0,
122153
- totals: {
122154
- rawChars,
122155
- injectedChars,
122156
- truncatedChars: Math.max(0, rawChars - injectedChars),
122157
- bootstrapMaxChars,
122158
- bootstrapTotalMaxChars,
122159
- nearLimitRatio
122160
- }
122161
- };
122162
- }
122163
- function buildBootstrapTruncationSignature(analysis) {
122164
- if (!analysis.hasTruncation) {
122165
- return;
122166
- }
122167
- const files = analysis.truncatedFiles.map((file) => ({
122168
- path: file.path || file.name,
122169
- rawChars: file.rawChars,
122170
- injectedChars: file.injectedChars,
122171
- causes: [...file.causes].sort()
122172
- })).sort((a, b) => {
122173
- const pathCmp = a.path.localeCompare(b.path);
122174
- if (pathCmp !== 0) {
122175
- return pathCmp;
122176
- }
122177
- if (a.rawChars !== b.rawChars) {
122178
- return a.rawChars - b.rawChars;
122179
- }
122180
- if (a.injectedChars !== b.injectedChars) {
122181
- return a.injectedChars - b.injectedChars;
122182
- }
122183
- return a.causes.join("+").localeCompare(b.causes.join("+"));
122184
- });
122185
- return JSON.stringify({
122186
- bootstrapMaxChars: analysis.totals.bootstrapMaxChars,
122187
- bootstrapTotalMaxChars: analysis.totals.bootstrapTotalMaxChars,
122188
- files
122189
- });
122190
- }
122191
- function formatBootstrapTruncationWarningLines(params) {
122192
- if (!params.analysis.hasTruncation) {
122193
- return [];
122194
- }
122195
- const maxFiles = typeof params.maxFiles === "number" && Number.isFinite(params.maxFiles) && params.maxFiles > 0 ? Math.floor(params.maxFiles) : DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES;
122196
- const lines = [];
122197
- const duplicateNameCounts = params.analysis.truncatedFiles.reduce((acc, file) => {
122198
- acc.set(file.name, (acc.get(file.name) ?? 0) + 1);
122199
- return acc;
122200
- }, new Map);
122201
- const topFiles = params.analysis.truncatedFiles.slice(0, maxFiles);
122202
- for (const file of topFiles) {
122203
- const pct = file.rawChars > 0 ? Math.round((file.rawChars - file.injectedChars) / file.rawChars * 100) : 0;
122204
- const causeText = file.causes.length > 0 ? file.causes.map((cause) => formatWarningCause(cause)).join(", ") : "";
122205
- const nameLabel = (duplicateNameCounts.get(file.name) ?? 0) > 1 && file.path.trim().length > 0 ? `${file.name} (${file.path})` : file.name;
122206
- lines.push(`${nameLabel}: ${file.rawChars} raw -> ${file.injectedChars} injected (~${Math.max(0, pct)}% removed${causeText ? `; ${causeText}` : ""}).`);
122207
- }
122208
- if (params.analysis.truncatedFiles.length > topFiles.length) {
122209
- lines.push(`+${params.analysis.truncatedFiles.length - topFiles.length} more truncated file(s).`);
122210
- }
122211
- lines.push("If unintentional, raise switchroom workspace bootstrapMaxChars and/or bootstrapTotalMaxChars config.");
122212
- return lines;
122213
- }
122214
- function buildBootstrapPromptWarning(params) {
122215
- const signature = buildBootstrapTruncationSignature(params.analysis);
122216
- let seenSignatures = normalizeSeenSignatures(params.seenSignatures);
122217
- if (params.previousSignature && !seenSignatures.includes(params.previousSignature)) {
122218
- seenSignatures = appendSeenSignature(seenSignatures, params.previousSignature);
122219
- }
122220
- const hasSeenSignature = Boolean(signature && seenSignatures.includes(signature));
122221
- const warningShown = params.mode !== "off" && Boolean(signature) && (params.mode === "always" || !hasSeenSignature);
122222
- const warningSignaturesSeen = signature && params.mode !== "off" ? appendSeenSignature(seenSignatures, signature) : seenSignatures;
122223
- return {
122224
- signature,
122225
- warningShown,
122226
- lines: warningShown ? formatBootstrapTruncationWarningLines({
122227
- analysis: params.analysis,
122228
- maxFiles: params.maxFiles
122229
- }) : [],
122230
- warningSignaturesSeen
122231
- };
122232
- }
122233
-
122234
- // src/agents/bootstrap-types.ts
122235
- var DEFAULT_AGENTS_FILENAME = "AGENTS.md";
122236
- var DEFAULT_SOUL_DEFAULT_FILENAME = "SOUL.default.md";
122237
- var DEFAULT_SOUL_FILENAME = "SOUL.md";
122238
- var DEFAULT_TOOLS_FILENAME = "TOOLS.md";
122239
- var DEFAULT_IDENTITY_FILENAME = "IDENTITY.md";
122240
- var DEFAULT_USER_FILENAME = "USER.md";
122241
- var DEFAULT_BOOTSTRAP_FILENAME = "BOOTSTRAP.md";
122242
- var DEFAULT_MEMORY_FILENAME = "MEMORY.md";
122243
-
122244
- // src/agents/workspace.ts
122245
- var DEFAULT_WORKSPACE_DIR_NAME = "workspace";
122246
- var DEFAULT_MEMORY_SUBDIR = "memory";
122247
- var STABLE_BOOTSTRAP_FILENAMES = [
122248
- DEFAULT_AGENTS_FILENAME,
122249
- DEFAULT_SOUL_DEFAULT_FILENAME,
122250
- DEFAULT_SOUL_FILENAME,
122251
- DEFAULT_IDENTITY_FILENAME,
122252
- DEFAULT_USER_FILENAME,
122253
- DEFAULT_TOOLS_FILENAME,
122254
- DEFAULT_BOOTSTRAP_FILENAME
122255
- ];
122256
- var DYNAMIC_BOOTSTRAP_FILENAMES = [
122257
- DEFAULT_MEMORY_FILENAME
122258
- ];
122259
- var DEFAULT_BOOTSTRAP_MAX_CHARS = 12000;
122260
- var DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS = 64000;
122261
- var DEFAULT_DYNAMIC_TOTAL_MAX_CHARS = 24000;
122262
- function resolveAgentWorkspaceDir(agentDir) {
122263
- return path8.join(agentDir, DEFAULT_WORKSPACE_DIR_NAME);
122264
- }
122265
- async function readOptionalFile(filePath) {
122266
- try {
122267
- const content = await readFile2(filePath, "utf8");
122268
- return content;
122269
- } catch (err) {
122270
- if (isErrnoException(err) && (err.code === "ENOENT" || err.code === "EISDIR")) {
122271
- return;
122272
- }
122273
- throw err;
122274
- }
122275
- }
122276
- function isErrnoException(value) {
122277
- return value instanceof Error && typeof value.code === "string";
122278
- }
122279
- async function loadNamedFile(workspaceDir, name, relativePath) {
122280
- const filePath = path8.join(workspaceDir, relativePath ?? name);
122281
- const content = await readOptionalFile(filePath);
122282
- if (content === undefined) {
122283
- return { name, path: filePath, missing: true };
122284
- }
122285
- return { name, path: filePath, content, missing: false };
122286
- }
122287
- async function loadStableBootstrapFiles(workspaceDir, options) {
122288
- const defaultFiles = await Promise.all(STABLE_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
122289
- const extras = options?.extraStableFiles ?? [];
122290
- if (extras.length === 0) {
122291
- return defaultFiles;
122292
- }
122293
- const extraFiles = await Promise.all(extras.map((name) => loadNamedFile(workspaceDir, name)));
122294
- return [...defaultFiles, ...extraFiles];
122295
- }
122296
- async function loadDynamicBootstrapFiles(workspaceDir, options) {
122297
- const now2 = options?.now ?? new Date;
122298
- const includeYesterday = options?.includeYesterday ?? true;
122299
- const files = await Promise.all(DYNAMIC_BOOTSTRAP_FILENAMES.map((name) => loadNamedFile(workspaceDir, name)));
122300
- const todayRelative = dailyMemoryRelativePath(now2);
122301
- const todayName = DEFAULT_MEMORY_FILENAME;
122302
- const today = await loadNamedFile(workspaceDir, todayName, todayRelative);
122303
- today.path = path8.join(workspaceDir, todayRelative);
122304
- files.push(today);
122305
- if (includeYesterday) {
122306
- const yesterdayRelative = dailyMemoryRelativePath(addDays(now2, -1));
122307
- const yesterday = await loadNamedFile(workspaceDir, todayName, yesterdayRelative);
122308
- yesterday.path = path8.join(workspaceDir, yesterdayRelative);
122309
- files.push(yesterday);
122919
+ this.name = "NodeEnvError";
122920
+ this.stderr = stderr;
122310
122921
  }
122311
- return files;
122312
- }
122313
- function pad22(n) {
122314
- return n < 10 ? `0${n}` : String(n);
122315
- }
122316
- function dailyMemoryRelativePath(date) {
122317
- const y = date.getFullYear();
122318
- const m = pad22(date.getMonth() + 1);
122319
- const d = pad22(date.getDate());
122320
- return path8.join(DEFAULT_MEMORY_SUBDIR, `${y}-${m}-${d}.md`);
122321
122922
  }
122322
- function addDays(date, days) {
122323
- const out = new Date(date.getTime());
122324
- out.setDate(out.getDate() + days);
122325
- return out;
122923
+ var ALL_LOCKFILES = [
122924
+ "bun.lock",
122925
+ "bun.lockb",
122926
+ "package-lock.json",
122927
+ "pnpm-lock.yaml",
122928
+ "yarn.lock"
122929
+ ];
122930
+ var LOCKFILES_FOR = {
122931
+ bun: ["bun.lock", "bun.lockb"],
122932
+ npm: ["package-lock.json"]
122933
+ };
122934
+ function defaultNodeCacheRoot() {
122935
+ return join109(homedir55(), ".switchroom", "deps", "node");
122326
122936
  }
122327
- function truncateContent(name, content, maxChars) {
122328
- if (content.length <= maxChars) {
122329
- return content;
122330
- }
122331
- if (maxChars < 200) {
122332
- return content.slice(0, Math.max(0, maxChars));
122937
+ function hashDepInputs(packageJsonPath) {
122938
+ const sourceDir = dirname47(packageJsonPath);
122939
+ const hasher = createHash21("sha256");
122940
+ hasher.update(`package.json
122941
+ `);
122942
+ hasher.update(readFileSync95(packageJsonPath));
122943
+ for (const lockName of ALL_LOCKFILES) {
122944
+ const lockPath = join109(sourceDir, lockName);
122945
+ if (existsSync104(lockPath)) {
122946
+ hasher.update(`
122947
+ `);
122948
+ hasher.update(lockName);
122949
+ hasher.update(`
122950
+ `);
122951
+ hasher.update(readFileSync95(lockPath));
122952
+ }
122333
122953
  }
122334
- const headRoomInitial = Math.floor(maxChars * 0.7);
122335
- const sampleMarker = `
122336
- \u2026(truncated ${name}: kept ${headRoomInitial}+${Math.max(0, maxChars - headRoomInitial - 96)} chars of ${content.length})\u2026
122337
- `;
122338
- const markerReserve = Math.max(64, sampleMarker.length + 8);
122339
- const headRoom = Math.min(headRoomInitial, Math.max(0, maxChars - markerReserve - 8));
122340
- const tailRoom = Math.max(0, maxChars - headRoom - markerReserve);
122341
- const head = content.slice(0, Math.max(0, headRoom));
122342
- const tail = tailRoom > 0 ? content.slice(content.length - tailRoom) : "";
122343
- const result = `${head}
122344
- \u2026(truncated ${name}: kept ${headRoom}+${tailRoom} chars of ${content.length})\u2026
122345
- ${tail}`;
122346
- return result.length <= maxChars ? result : result.slice(0, maxChars);
122954
+ return hasher.digest("hex");
122347
122955
  }
122348
- function projectBootstrapFiles(params) {
122349
- const {
122350
- files,
122351
- heading,
122352
- budget,
122353
- seenSignatures,
122354
- warningMode = "error",
122355
- warningMaxFiles = DEFAULT_BOOTSTRAP_PROMPT_WARNING_MAX_FILES
122356
- } = params;
122357
- const perFileCap = budget.bootstrapMaxChars;
122358
- let remainingTotal = budget.bootstrapTotalMaxChars;
122359
- const injectedFiles = [];
122360
- const parts = [];
122361
- for (const file of files) {
122362
- if (file.missing || typeof file.content !== "string") {
122363
- continue;
122364
- }
122365
- const raw = file.content.trimEnd();
122366
- if (raw.length === 0) {
122367
- continue;
122368
- }
122369
- const perFileAllowed = Math.min(perFileCap, remainingTotal);
122370
- if (perFileAllowed <= 0) {
122371
- break;
122956
+ function ensureNodeEnv(opts) {
122957
+ const { skillName, packageJsonPath, force = false } = opts;
122958
+ const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
122959
+ const installer = opts.installer ?? "bun";
122960
+ if (!existsSync104(packageJsonPath)) {
122961
+ throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
122962
+ }
122963
+ const sourceDir = dirname47(packageJsonPath);
122964
+ const envDir = join109(cacheRoot, skillName);
122965
+ const stampPath = join109(envDir, ".package.sha256");
122966
+ const nodeModulesDir = join109(envDir, "node_modules");
122967
+ const binDir = join109(nodeModulesDir, ".bin");
122968
+ const targetHash = hashDepInputs(packageJsonPath);
122969
+ if (!force && existsSync104(stampPath) && existsSync104(nodeModulesDir)) {
122970
+ const existingHash = readFileSync95(stampPath, "utf8").trim();
122971
+ if (existingHash === targetHash) {
122972
+ return {
122973
+ skillName,
122974
+ dir: envDir,
122975
+ nodeModulesDir,
122976
+ binDir,
122977
+ rebuilt: false
122978
+ };
122372
122979
  }
122373
- const injected = truncateContent(file.name, raw, perFileAllowed);
122374
- const relativePath = file.path;
122375
- injectedFiles.push({ path: relativePath, content: injected });
122376
- parts.push(`## ${relativePath}`);
122377
- parts.push(injected);
122378
- remainingTotal -= injected.length;
122379
- if (remainingTotal <= 0) {
122380
- break;
122980
+ }
122981
+ if (existsSync104(envDir)) {
122982
+ rmSync20(envDir, { recursive: true, force: true });
122983
+ }
122984
+ mkdirSync60(envDir, { recursive: true });
122985
+ copyFileSync13(packageJsonPath, join109(envDir, "package.json"));
122986
+ let copiedLockfile = false;
122987
+ for (const lockName of LOCKFILES_FOR[installer]) {
122988
+ const lockPath = join109(sourceDir, lockName);
122989
+ if (existsSync104(lockPath)) {
122990
+ copyFileSync13(lockPath, join109(envDir, lockName));
122991
+ copiedLockfile = true;
122381
122992
  }
122382
122993
  }
122383
- const concatenated = parts.length > 0 ? [heading.trim().length > 0 ? `# ${heading}` : null, ...parts].filter((p) => p !== null).join(`
122384
-
122385
- `) : "";
122386
- const stats = buildBootstrapInjectionStats({
122387
- bootstrapFiles: files,
122388
- injectedFiles
122389
- });
122390
- const analysis = analyzeBootstrapBudget({
122391
- files: stats,
122392
- bootstrapMaxChars: budget.bootstrapMaxChars,
122393
- bootstrapTotalMaxChars: budget.bootstrapTotalMaxChars,
122394
- nearLimitRatio: budget.nearLimitRatio ?? DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO
122395
- });
122396
- const warning = buildBootstrapPromptWarning({
122397
- analysis,
122398
- mode: warningMode,
122399
- seenSignatures,
122400
- maxFiles: warningMaxFiles
122401
- });
122402
- if (warningMode === "error" && analysis.hasTruncation) {
122403
- const errorLines = [
122404
- "Bootstrap budget exceeded. The following files exceed limits:",
122405
- ""
122406
- ];
122407
- for (const file of analysis.truncatedFiles) {
122408
- const excessChars = file.rawChars - file.injectedChars;
122409
- const causes = file.causes.map((c) => c === "per-file-limit" ? `per-file limit (${analysis.totals.bootstrapMaxChars.toLocaleString()} chars)` : `total limit (${analysis.totals.bootstrapTotalMaxChars.toLocaleString()} chars)`);
122410
- errorLines.push(` ${file.name}: ${file.rawChars.toLocaleString()} bytes (exceeds ${causes.join(", ")})`);
122411
- errorLines.push(` Trim ${excessChars.toLocaleString()} bytes, or pass --warning-mode warn to proceed with truncation.`);
122994
+ try {
122995
+ if (installer === "bun") {
122996
+ const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
122997
+ execFileSync32("bun", args, { cwd: envDir, stdio: "pipe" });
122998
+ } else {
122999
+ const args = copiedLockfile ? ["ci"] : ["install"];
123000
+ execFileSync32("npm", args, { cwd: envDir, stdio: "pipe" });
122412
123001
  }
122413
- throw new BootstrapBudgetExceededError(analysis, errorLines.join(`
122414
- `));
123002
+ } catch (err) {
123003
+ const e = err;
123004
+ throw new NodeEnvError(`Failed to install node deps for skill "${skillName}" with ${installer}: ${e.message}`, e.stderr?.toString());
122415
123005
  }
122416
- return { files, injectedFiles, concatenated, analysis, warning };
122417
- }
122418
- async function buildStableBootstrapPrompt(params) {
122419
- const files = await loadStableBootstrapFiles(params.workspaceDir, {
122420
- extraStableFiles: params.extraStableFiles
122421
- });
122422
- const budget = {
122423
- bootstrapMaxChars: params.budget?.bootstrapMaxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS,
122424
- bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_BOOTSTRAP_TOTAL_MAX_CHARS,
122425
- nearLimitRatio: params.budget?.nearLimitRatio
123006
+ writeFileSync42(stampPath, targetHash + `
123007
+ `);
123008
+ return {
123009
+ skillName,
123010
+ dir: envDir,
123011
+ nodeModulesDir,
123012
+ binDir,
123013
+ rebuilt: true
122426
123014
  };
122427
- return projectBootstrapFiles({
122428
- files,
122429
- heading: "Project Context (stable workspace files)",
122430
- budget,
122431
- seenSignatures: params.seenSignatures,
122432
- warningMode: params.budget?.warningMode,
122433
- warningMaxFiles: params.budget?.warningMaxFiles
122434
- });
122435
123015
  }
122436
- async function buildDynamicBootstrapPrompt(params) {
122437
- const files = await loadDynamicBootstrapFiles(params.workspaceDir, {
122438
- now: params.now,
122439
- includeYesterday: params.includeYesterday
122440
- });
122441
- const budget = {
122442
- bootstrapMaxChars: params.budget?.bootstrapMaxChars ?? DEFAULT_BOOTSTRAP_MAX_CHARS,
122443
- bootstrapTotalMaxChars: params.budget?.bootstrapTotalMaxChars ?? DEFAULT_DYNAMIC_TOTAL_MAX_CHARS,
122444
- nearLimitRatio: params.budget?.nearLimitRatio
122445
- };
122446
- return projectBootstrapFiles({
122447
- files,
122448
- heading: "Project Context (dynamic workspace files)",
122449
- budget,
122450
- seenSignatures: params.seenSignatures,
122451
- warningMode: params.budget?.warningMode,
122452
- warningMaxFiles: params.budget?.warningMaxFiles
123016
+
123017
+ // src/cli/deps.ts
123018
+ function builtinSkillsRoot() {
123019
+ return resolve55(homedir56(), ".switchroom/skills/_bundled");
123020
+ }
123021
+ function registerDepsCommand(program3) {
123022
+ const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
123023
+ deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
123024
+ const skillsRoot = builtinSkillsRoot();
123025
+ if (!existsSync105(skillsRoot)) {
123026
+ console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
123027
+ process.exit(1);
123028
+ }
123029
+ const skillDir = join110(skillsRoot, skill);
123030
+ if (!existsSync105(skillDir)) {
123031
+ console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
123032
+ process.exit(1);
123033
+ }
123034
+ const requirementsPath = join110(skillDir, "requirements.txt");
123035
+ const packageJsonPath = join110(skillDir, "package.json");
123036
+ const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync105(requirementsPath));
123037
+ const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync105(packageJsonPath));
123038
+ let did = 0;
123039
+ if (wantPython) {
123040
+ if (!existsSync105(requirementsPath)) {
123041
+ console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
123042
+ process.exit(1);
123043
+ }
123044
+ try {
123045
+ console.log(source_default.gray(`Rebuilding Python env for ${skill}...`));
123046
+ const env2 = ensurePythonEnv({
123047
+ skillName: skill,
123048
+ requirementsPath,
123049
+ force: true
123050
+ });
123051
+ console.log(source_default.green(` \u2713 Python env at ${env2.venvDir} (python: ${env2.pythonBin})`));
123052
+ did++;
123053
+ } catch (err) {
123054
+ if (err instanceof PythonEnvError) {
123055
+ console.error(source_default.red(` Python env failed: ${err.message}`));
123056
+ if (err.stderr)
123057
+ console.error(source_default.dim(err.stderr));
123058
+ process.exit(1);
123059
+ }
123060
+ throw err;
123061
+ }
123062
+ }
123063
+ if (wantNode) {
123064
+ if (!existsSync105(packageJsonPath)) {
123065
+ console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
123066
+ process.exit(1);
123067
+ }
123068
+ try {
123069
+ console.log(source_default.gray(`Rebuilding Node env for ${skill}...`));
123070
+ const env2 = ensureNodeEnv({
123071
+ skillName: skill,
123072
+ packageJsonPath,
123073
+ force: true
123074
+ });
123075
+ console.log(source_default.green(` \u2713 Node env at ${env2.dir} (node_modules: ${env2.nodeModulesDir})`));
123076
+ did++;
123077
+ } catch (err) {
123078
+ if (err instanceof NodeEnvError) {
123079
+ console.error(source_default.red(` Node env failed: ${err.message}`));
123080
+ if (err.stderr)
123081
+ console.error(source_default.dim(err.stderr));
123082
+ process.exit(1);
123083
+ }
123084
+ throw err;
123085
+ }
123086
+ }
123087
+ if (did === 0) {
123088
+ console.error(source_default.yellow(`Skill "${skill}" has neither requirements.txt nor package.json \u2014 nothing to rebuild.`));
123089
+ process.exit(1);
123090
+ }
122453
123091
  });
122454
123092
  }
122455
123093
 
123094
+ // src/cli/workspace.ts
123095
+ init_helpers();
123096
+ init_loader();
123097
+ import { existsSync as existsSync106 } from "node:fs";
123098
+ import { resolve as resolve56, sep as sep7 } from "node:path";
123099
+ import { spawnSync as spawnSync23 } from "node:child_process";
123100
+
122456
123101
  // src/agents/memory-search.ts
122457
123102
  import { readFile as readFile3, readdir, realpath, stat as stat2 } from "node:fs/promises";
122458
123103
  import path9 from "node:path";
@@ -122730,8 +123375,8 @@ function registerWorkspaceCommand(program3) {
122730
123375
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
122731
123376
  if (!dir)
122732
123377
  return;
122733
- const resolvedWorkspace = resolve55(dir);
122734
- const target = resolve55(resolvedWorkspace, file ?? "AGENTS.md");
123378
+ const resolvedWorkspace = resolve56(dir);
123379
+ const target = resolve56(resolvedWorkspace, file ?? "AGENTS.md");
122735
123380
  if (!isInsideWorkspace(resolvedWorkspace, target)) {
122736
123381
  process.stderr.write(`workspace edit: refusing path traversal outside workspace dir (${target})
122737
123382
  `);
@@ -122799,8 +123444,8 @@ function registerWorkspaceCommand(program3) {
122799
123444
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
122800
123445
  if (!dir)
122801
123446
  return;
122802
- const gitDir = resolve55(dir, ".git");
122803
- if (!existsSync105(gitDir)) {
123447
+ const gitDir = resolve56(dir, ".git");
123448
+ if (!existsSync106(gitDir)) {
122804
123449
  process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
122805
123450
  `);
122806
123451
  return;
@@ -122853,8 +123498,8 @@ function registerWorkspaceCommand(program3) {
122853
123498
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
122854
123499
  if (!dir)
122855
123500
  return;
122856
- const gitDir = resolve55(dir, ".git");
122857
- if (!existsSync105(gitDir)) {
123501
+ const gitDir = resolve56(dir, ".git");
123502
+ if (!existsSync106(gitDir)) {
122858
123503
  process.stdout.write(`Workspace is not a git repository.
122859
123504
  `);
122860
123505
  return;
@@ -122877,9 +123522,9 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
122877
123522
  return;
122878
123523
  }
122879
123524
  const agentsDir = resolveAgentsDir(config);
122880
- const agentDir = resolve55(agentsDir, agentName);
123525
+ const agentDir = resolve56(agentsDir, agentName);
122881
123526
  const dir = resolveAgentWorkspaceDir(agentDir);
122882
- if (!existsSync105(dir)) {
123527
+ if (!existsSync106(dir)) {
122883
123528
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
122884
123529
  `);
122885
123530
  return;
@@ -122915,8 +123560,8 @@ function safeParseInt(value, fallback) {
122915
123560
  init_helpers();
122916
123561
  init_loader();
122917
123562
  init_merge();
122918
- import { copyFileSync as copyFileSync14, existsSync as existsSync106, readFileSync as readFileSync95, writeFileSync as writeFileSync43 } from "node:fs";
122919
- import { join as join110, resolve as resolve56 } from "node:path";
123563
+ import { copyFileSync as copyFileSync14, existsSync as existsSync107, readFileSync as readFileSync96, writeFileSync as writeFileSync43 } from "node:fs";
123564
+ import { join as join111, resolve as resolve57 } from "node:path";
122920
123565
  init_scaffold();
122921
123566
  init_profiles();
122922
123567
  init_schema();
@@ -122931,9 +123576,9 @@ function resolveSoulTargetOrExit(program3, agentName) {
122931
123576
  const profileName = merged.extends ?? DEFAULT_PROFILE;
122932
123577
  const profilePath = getProfilePath(profileName);
122933
123578
  const agentsDir = resolveAgentsDir(config);
122934
- const agentDir = resolve56(agentsDir, agentName);
123579
+ const agentDir = resolve57(agentsDir, agentName);
122935
123580
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
122936
- if (!existsSync106(workspaceDir)) {
123581
+ if (!existsSync107(workspaceDir)) {
122937
123582
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
122938
123583
  process.exit(1);
122939
123584
  }
@@ -122942,7 +123587,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
122942
123587
  profileName,
122943
123588
  profilePath,
122944
123589
  workspaceDir,
122945
- soulPath: join110(workspaceDir, "SOUL.md"),
123590
+ soulPath: join111(workspaceDir, "SOUL.md"),
122946
123591
  soul: merged.soul
122947
123592
  };
122948
123593
  }
@@ -122959,11 +123604,11 @@ function registerSoulCommand(program3) {
122959
123604
  const t = resolveSoulTargetOrExit(program3, agentName);
122960
123605
  if (!t)
122961
123606
  return;
122962
- if (!existsSync106(t.soulPath)) {
123607
+ if (!existsSync107(t.soulPath)) {
122963
123608
  console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
122964
123609
  process.exit(1);
122965
123610
  }
122966
- process.stdout.write(readFileSync95(t.soulPath, "utf-8"));
123611
+ process.stdout.write(readFileSync96(t.soulPath, "utf-8"));
122967
123612
  }));
122968
123613
  cmd.command("reset <agent>").description("Re-seed SOUL.md from the agent's current profile " + "(backs the existing file up to SOUL.md.bak first)").option("-y, --yes", "Skip the confirmation prompt").action(withConfigError(async (agentName, opts) => {
122969
123614
  const t = resolveSoulTargetOrExit(program3, agentName);
@@ -122974,7 +123619,7 @@ function registerSoulCommand(program3) {
122974
123619
  console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
122975
123620
  process.exit(1);
122976
123621
  }
122977
- const exists = existsSync106(t.soulPath);
123622
+ const exists = existsSync107(t.soulPath);
122978
123623
  if (exists && !opts.yes) {
122979
123624
  if (!isInteractive()) {
122980
123625
  console.error(`soul: ${t.soulPath} already exists. Re-run with --yes to ` + `replace it (the current file is backed up to SOUL.md.bak).`);
@@ -122989,7 +123634,7 @@ function registerSoulCommand(program3) {
122989
123634
  let backupPath;
122990
123635
  if (exists) {
122991
123636
  backupPath = `${t.soulPath}.bak`;
122992
- if (existsSync106(backupPath)) {
123637
+ if (existsSync107(backupPath)) {
122993
123638
  backupPath = `${t.soulPath}.bak.${Date.now()}`;
122994
123639
  }
122995
123640
  copyFileSync14(t.soulPath, backupPath);
@@ -123005,279 +123650,6 @@ function registerSoulCommand(program3) {
123005
123650
  }));
123006
123651
  }
123007
123652
 
123008
- // src/cli/debug.ts
123009
- init_helpers();
123010
- init_loader();
123011
- import { existsSync as existsSync107, readFileSync as readFileSync96, readdirSync as readdirSync36, statSync as statSync62 } from "node:fs";
123012
- import { resolve as resolve57, join as join111 } from "node:path";
123013
- import { createHash as createHash21 } from "node:crypto";
123014
- init_merge();
123015
- init_hindsight2();
123016
- function formatBytes(bytes) {
123017
- return `${bytes.toLocaleString()} bytes`;
123018
- }
123019
- function estimateTokens(bytes) {
123020
- return Math.round(bytes / 3.7);
123021
- }
123022
- function readMcpServerNames(agentDir) {
123023
- const mcpPath = join111(agentDir, ".mcp.json");
123024
- if (!existsSync107(mcpPath))
123025
- return [];
123026
- try {
123027
- const parsed = JSON.parse(readFileSync96(mcpPath, "utf-8"));
123028
- return Object.keys(parsed.mcpServers ?? {});
123029
- } catch {
123030
- return null;
123031
- }
123032
- }
123033
- function sha2562(content) {
123034
- return createHash21("sha256").update(content).digest("hex").slice(0, 16);
123035
- }
123036
- function findLatestTranscriptJsonl(claudeConfigDir) {
123037
- const projectsDir = join111(claudeConfigDir, "projects");
123038
- if (!existsSync107(projectsDir))
123039
- return;
123040
- try {
123041
- const entries = readdirSync36(projectsDir, { withFileTypes: true });
123042
- let latest;
123043
- for (const entry of entries) {
123044
- if (!entry.isDirectory())
123045
- continue;
123046
- const projectPath = join111(projectsDir, entry.name);
123047
- const transcriptPath = join111(projectPath, "transcript.jsonl");
123048
- if (!existsSync107(transcriptPath))
123049
- continue;
123050
- const stat3 = statSync62(transcriptPath);
123051
- if (!latest || stat3.mtimeMs > latest.mtime) {
123052
- latest = { path: transcriptPath, mtime: stat3.mtimeMs };
123053
- }
123054
- }
123055
- return latest?.path;
123056
- } catch {
123057
- return;
123058
- }
123059
- }
123060
- function extractLatestUserMessage(transcriptPath) {
123061
- try {
123062
- const content = readFileSync96(transcriptPath, "utf-8");
123063
- const lines = content.trim().split(`
123064
- `).filter(Boolean);
123065
- for (let i2 = lines.length - 1;i2 >= 0; i2--) {
123066
- const line = lines[i2];
123067
- try {
123068
- const event = JSON.parse(line);
123069
- if (event.type === "message" && event.role === "user" && typeof event.content === "string") {
123070
- const timestamp = event.timestamp ? new Date(event.timestamp).toLocaleString() : "unknown";
123071
- return { text: event.content, timestamp };
123072
- }
123073
- } catch {
123074
- continue;
123075
- }
123076
- }
123077
- } catch {
123078
- return;
123079
- }
123080
- }
123081
- function buildProgressUpdateGuidance() {
123082
- return `## Progress updates (human-style check-ins)
123083
-
123084
- You're talking to a human colleague on Telegram. Alongside the emoji status
123085
- ladder, send a short \`progress_update\` at inflection points, the moments a
123086
- senior colleague would ping the person who asked them to do something:
123087
-
123088
- - **Plan formed:** "Got it. Going to do X first, then Y, then Z."
123089
- - **Pivot or blocker:** "First approach didn't work because <reason>. Trying
123090
- <alternative> instead."
123091
- - **Chunk finished:** "Done with X. Starting Y now."
123092
-
123093
- Keep them short (one or two sentences). Don't narrate every step, the pinned
123094
- progress card shows that for free. Don't send an update on a trivial one-shot
123095
- task. Send them when a colleague would genuinely want to know what's happening.
123096
-
123097
- Final answers still go through \`reply\` as usual,
123098
- \`progress_update\` is only for mid-turn check-ins.`;
123099
- }
123100
- function registerDebugCommand(program3) {
123101
- const cmd = program3.command("debug", { hidden: true }).description("Observability tools for inspecting agent prompt layering [advanced]");
123102
- cmd.command("turn <agent>").description("Dump the exact prompt layering the model saw on the most recent turn").option("--last <n>", "Show N-th most recent turn instead of latest", "1").action(withConfigError(async (agentName, opts) => {
123103
- const config = getConfig(program3);
123104
- const agentConfig = config.agents[agentName];
123105
- if (!agentConfig) {
123106
- console.error(`Agent '${agentName}' not found in switchroom.yaml`);
123107
- process.exit(1);
123108
- }
123109
- const agentsDir = resolveAgentsDir(config);
123110
- const agentDir = resolve57(agentsDir, agentName);
123111
- if (!existsSync107(agentDir)) {
123112
- console.error(`Agent directory not found: ${agentDir}`);
123113
- process.exit(1);
123114
- }
123115
- const workspaceDir = resolveAgentWorkspaceDir(agentDir);
123116
- const claudeConfigDir = join111(agentDir, ".claude");
123117
- const claudeMdPath2 = join111(agentDir, "CLAUDE.md");
123118
- const soulMdPath = join111(agentDir, "SOUL.md");
123119
- const workspaceSoulMdPath = join111(workspaceDir, "SOUL.md");
123120
- const handoffPath = join111(agentDir, ".handoff.md");
123121
- const lastN = parseInt(opts.last, 10);
123122
- if (isNaN(lastN) || lastN < 1) {
123123
- console.error("--last must be a positive integer");
123124
- process.exit(1);
123125
- }
123126
- if (lastN > 1) {
123127
- console.error("Note: --last N where N > 1 not yet implemented (only latest turn supported)");
123128
- process.exit(1);
123129
- }
123130
- console.log(`=== Debug Turn Dump: ${agentName} ===
123131
- `);
123132
- console.log(`=== Append System Prompt (stable) ===
123133
- `);
123134
- const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
123135
- const useHotReloadStable = resolved.channels?.telegram?.hotReloadStable === true;
123136
- const stableResult = await buildStableBootstrapPrompt({
123137
- workspaceDir,
123138
- budget: { warningMode: "off" }
123139
- });
123140
- if (useHotReloadStable) {
123141
- console.log(`-- Workspace Stable Render (not used \u2014 stable content is in per-turn hook) --`);
123142
- console.log();
123143
- } else if (stableResult.concatenated.trim().length > 0) {
123144
- console.log(`-- Workspace Stable Render (${formatBytes(stableResult.concatenated.length)}) --`);
123145
- console.log(stableResult.concatenated);
123146
- console.log();
123147
- } else {
123148
- console.log("-- Workspace Stable Render (0 bytes, not set up) --");
123149
- console.log();
123150
- }
123151
- const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(resolved);
123152
- const progressGuidance = useSwitchroomPlugin ? buildProgressUpdateGuidance() : "";
123153
- if (progressGuidance.length > 0) {
123154
- console.log(`-- Progress Updates Guidance (${formatBytes(progressGuidance.length)}) --`);
123155
- console.log(progressGuidance);
123156
- console.log();
123157
- }
123158
- const baseSystemPromptAppend = resolved.system_prompt_append ?? "";
123159
- if (baseSystemPromptAppend.trim().length > 0) {
123160
- console.log(`-- User System Prompt Append (${formatBytes(baseSystemPromptAppend.length)}) --`);
123161
- console.log(baseSystemPromptAppend);
123162
- console.log();
123163
- }
123164
- console.log(`=== Append System Prompt (per-session) ===
123165
- `);
123166
- const handoffContent = existsSync107(handoffPath) ? readFileSync96(handoffPath, "utf-8") : "";
123167
- if (handoffContent.trim().length > 0) {
123168
- console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
123169
- console.log(handoffContent);
123170
- console.log();
123171
- } else {
123172
- console.log("-- Handoff Briefing (0 bytes, no prior session) --");
123173
- console.log();
123174
- }
123175
- console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
123176
- `);
123177
- const claudeMdContent = existsSync107(claudeMdPath2) ? readFileSync96(claudeMdPath2, "utf-8") : "";
123178
- if (claudeMdContent.trim().length > 0) {
123179
- console.log(`(${formatBytes(claudeMdContent.length)})`);
123180
- console.log(claudeMdContent);
123181
- console.log();
123182
- } else {
123183
- console.log("(0 bytes, not present)");
123184
- console.log();
123185
- }
123186
- console.log(`=== Persona (SOUL.md) ===
123187
- `);
123188
- const soulMdContent = existsSync107(soulMdPath) ? readFileSync96(soulMdPath, "utf-8") : existsSync107(workspaceSoulMdPath) ? readFileSync96(workspaceSoulMdPath, "utf-8") : "";
123189
- if (soulMdContent.trim().length > 0) {
123190
- console.log(`(${formatBytes(soulMdContent.length)})`);
123191
- console.log(soulMdContent);
123192
- console.log();
123193
- } else {
123194
- console.log("(0 bytes, stale placeholder \u2014 Phase 2 item: single source of truth for persona)");
123195
- console.log();
123196
- }
123197
- console.log(`=== Per-Turn Injections (UserPromptSubmit) ===
123198
- `);
123199
- if (useHotReloadStable) {
123200
- if (stableResult.concatenated.trim().length > 0) {
123201
- console.log(`-- Workspace Stable (hot-reload hook): ${formatBytes(stableResult.concatenated.length)} --`);
123202
- console.log(stableResult.concatenated);
123203
- console.log();
123204
- } else {
123205
- console.log("-- Workspace Stable (hot-reload hook): 0 bytes, not set up --");
123206
- console.log();
123207
- }
123208
- }
123209
- const dynamicResult = await buildDynamicBootstrapPrompt({
123210
- workspaceDir,
123211
- budget: { warningMode: "off" }
123212
- });
123213
- if (dynamicResult.concatenated.trim().length > 0) {
123214
- console.log(`-- Workspace Dynamic: fired, ${formatBytes(dynamicResult.concatenated.length)} --`);
123215
- console.log(dynamicResult.concatenated);
123216
- console.log();
123217
- } else {
123218
- console.log("-- Workspace Dynamic: no content (MEMORY.md and daily notes empty or missing) --");
123219
- console.log();
123220
- }
123221
- const hindsightEnabled = isHindsightEnabled(config) && agentConfig.memory?.auto_recall !== false;
123222
- if (hindsightEnabled) {
123223
- console.log("-- Hindsight Recall: enabled (exact content unavailable, check hindsight logs) --");
123224
- console.log();
123225
- } else {
123226
- console.log("-- Hindsight Recall: disabled --");
123227
- console.log();
123228
- }
123229
- console.log(`=== User Message (latest turn) ===
123230
- `);
123231
- const transcriptPath = findLatestTranscriptJsonl(claudeConfigDir);
123232
- const userMessage = transcriptPath ? extractLatestUserMessage(transcriptPath) : undefined;
123233
- if (userMessage) {
123234
- console.log(`(Turn timestamp: ${userMessage.timestamp})`);
123235
- console.log(userMessage.text);
123236
- console.log();
123237
- } else {
123238
- console.log("(unavailable: no transcript found or transcript empty)");
123239
- console.log();
123240
- }
123241
- console.log(`=== Totals ===
123242
- `);
123243
- const stableBytes = stableResult.concatenated.length + progressGuidance.length + baseSystemPromptAppend.length;
123244
- const perSessionBytes = handoffContent.length;
123245
- const claudeMdBytes = claudeMdContent.length;
123246
- const soulMdBytes = soulMdContent.length;
123247
- const perTurnBytes = dynamicResult.concatenated.length;
123248
- const userBytes = userMessage?.text.length ?? 0;
123249
- const fleetDir = join111(agentsDir, "..", "fleet");
123250
- const fleetInvPath = join111(fleetDir, "switchroom-invariants.md");
123251
- const fleetClaudePath = join111(fleetDir, "CLAUDE.md");
123252
- const fleetInvBytes = existsSync107(fleetInvPath) ? readFileSync96(fleetInvPath, "utf-8").length : 0;
123253
- const fleetClaudeBytes = existsSync107(fleetClaudePath) ? readFileSync96(fleetClaudePath, "utf-8").length : 0;
123254
- const fleetBytes = fleetInvBytes + fleetClaudeBytes;
123255
- const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
123256
- console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
123257
- console.log(`Per-session: ${formatBytes(perSessionBytes).padEnd(20)} (cache-warm until next session)`);
123258
- console.log(`CLAUDE.md (cwd): ${formatBytes(claudeMdBytes).padEnd(20)} (cache-hot)`);
123259
- console.log(`Fleet invariants: ${formatBytes(fleetBytes).padEnd(20)} (--add-dir, cache-hot)`);
123260
- console.log(`Per-turn: ${formatBytes(perTurnBytes).padEnd(20)} (never cached \u2014 the real per-turn $ cost)`);
123261
- console.log(`User message: ${formatBytes(userBytes).padEnd(20)}`);
123262
- console.log(`Authored text: ${formatBytes(totalBytes).padEnd(20)} (~${estimateTokens(totalBytes).toLocaleString()} tokens est.)`);
123263
- const mcpServers = readMcpServerNames(agentDir);
123264
- const mcpCount = mcpServers?.length ?? null;
123265
- const mcpEstK = mcpCount != null ? mcpCount * 3 : null;
123266
- const mcpLabel = mcpCount != null ? `${mcpCount} servers` : "(unreadable \u2014 agent-private .mcp.json)";
123267
- const mcpEstLabel = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k tok` : "~30k tok (audit est.)";
123268
- console.log(`MCP tool surface: ${mcpLabel.padEnd(20)} (NOT counted above; ~3k tok/server \u2248 ${mcpEstLabel} \u2014 deferred under tool search)`);
123269
- if (mcpServers && mcpServers.length > 0) {
123270
- console.log(` [${mcpServers.join(", ")}]`);
123271
- }
123272
- const floorMcp = mcpEstK != null ? `~${mcpEstK.toLocaleString()}k` : "~30k";
123273
- console.log(`Per-turn FLOOR: ${`~${estimateTokens(totalBytes).toLocaleString()} + ${floorMcp} MCP + ~13k CLI-base`.padEnd(20)} tokens est. (before the user msg, recall, or tool results)`);
123274
- const stableCacheInput = stableResult.concatenated + progressGuidance + baseSystemPromptAppend;
123275
- const stableHash = sha2562(stableCacheInput);
123276
- console.log(`Cache stable hash: sha256:${stableHash}`);
123277
- console.log();
123278
- }));
123279
- }
123280
-
123281
123653
  // src/cli/worktree.ts
123282
123654
  init_source();
123283
123655
  init_claim();