switchroom 0.16.38 → 0.16.47

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 (59) hide show
  1. package/dist/agent-scheduler/index.js +8 -2
  2. package/dist/auth-broker/index.js +7 -1
  3. package/dist/cli/notion-write-pretool.mjs +7 -1
  4. package/dist/cli/switchroom.js +1259 -375
  5. package/dist/cli/ui/index.html +877 -214
  6. package/dist/host-control/main.js +116 -84
  7. package/dist/vault/approvals/kernel-server.js +8 -2
  8. package/dist/vault/broker/server.js +8 -2
  9. package/examples/minimal.yaml +1 -1
  10. package/examples/switchroom.yaml +1 -1
  11. package/package.json +2 -2
  12. package/profiles/_shared/reply-discipline.md.hbs +9 -0
  13. package/skills/switchroom-status/SKILL.md +1 -1
  14. package/telegram-plugin/bridge/bridge.ts +2 -1
  15. package/telegram-plugin/card-format.ts +7 -1
  16. package/telegram-plugin/dist/bridge/bridge.js +20 -2
  17. package/telegram-plugin/dist/gateway/gateway.js +2197 -964
  18. package/telegram-plugin/dist/server.js +20 -2
  19. package/telegram-plugin/format.ts +305 -31
  20. package/telegram-plugin/gateway/gateway.ts +310 -70
  21. package/telegram-plugin/gateway/model-command.ts +173 -19
  22. package/telegram-plugin/hooks/tool-label-pretool.d.mts +12 -0
  23. package/telegram-plugin/hooks/tool-label-pretool.mjs +54 -16
  24. package/telegram-plugin/package.json +1 -1
  25. package/telegram-plugin/session-tail.ts +47 -1
  26. package/telegram-plugin/stream-reply-handler.ts +19 -1
  27. package/telegram-plugin/tests/always-allow-grant.test.ts +34 -2
  28. package/telegram-plugin/tests/card-format.test.ts +28 -0
  29. package/telegram-plugin/tests/claude-code-event-contract.test.ts +151 -0
  30. package/telegram-plugin/tests/format-consistency.test.ts +223 -0
  31. package/telegram-plugin/tests/formatting-parse-regression.test.ts +272 -0
  32. package/telegram-plugin/tests/formatting-torture-set.ts +218 -0
  33. package/telegram-plugin/tests/model-command.test.ts +213 -47
  34. package/telegram-plugin/tests/paragraph-normalizer.test.ts +203 -21
  35. package/telegram-plugin/tests/rich-markdown-oracle.ts +469 -0
  36. package/telegram-plugin/tests/session-tail.test.ts +91 -0
  37. package/telegram-plugin/tests/status-vocabulary-unification.test.ts +125 -0
  38. package/telegram-plugin/tests/telegram-format.test.ts +33 -8
  39. package/telegram-plugin/tests/text-voice-scrub.test.ts +142 -22
  40. package/telegram-plugin/tests/tool-activity-summary.test.ts +6 -1
  41. package/telegram-plugin/tests/tts-normalize.test.ts +242 -0
  42. package/telegram-plugin/tests/vault-request-access-tool.test.ts +24 -0
  43. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +46 -0
  44. package/telegram-plugin/tests/voice-ondemand.test.ts +99 -2
  45. package/telegram-plugin/tests/voice-presynth.test.ts +437 -0
  46. package/telegram-plugin/tests/worker-activity-feed.test.ts +49 -0
  47. package/telegram-plugin/text-voice-scrub.ts +68 -18
  48. package/telegram-plugin/tool-activity-summary.ts +20 -108
  49. package/telegram-plugin/tts-normalize.ts +377 -0
  50. package/telegram-plugin/uat/driver.ts +472 -22
  51. package/telegram-plugin/uat/scenarios/jtbd-model-litellm-sr-dm.test.ts +34 -14
  52. package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +169 -0
  53. package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +134 -0
  54. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +254 -0
  55. package/telegram-plugin/uat/scenarios/jtbd-status-phase-transitions-dm.test.ts +109 -0
  56. package/telegram-plugin/uat/uat-driver.test.ts +297 -0
  57. package/telegram-plugin/voice-ondemand.ts +161 -10
  58. package/telegram-plugin/voice-presynth.ts +242 -0
  59. package/telegram-plugin/worker-activity-feed.ts +9 -1
@@ -13542,7 +13542,7 @@ var init_zod = __esm(() => {
13542
13542
  function isValidTimezone(value) {
13543
13543
  return TIMEZONE_REGEX.test(value);
13544
13544
  }
13545
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13545
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
13546
13546
  var init_schema = __esm(() => {
13547
13547
  init_zod();
13548
13548
  CodeRepoEntrySchema = exports_external.object({
@@ -14005,7 +14005,7 @@ var init_schema = __esm(() => {
14005
14005
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).optional(),
14006
14006
  reactions: ReactionsSchema,
14007
14007
  reaction_dispatch: ReactionDispatchSchema,
14008
- model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-4-6')"),
14008
+ model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Model name must be alphanumeric with ._-/[]: only (no spaces or shell specials)").optional().describe("Claude model override (e.g., 'claude-sonnet-5')"),
14009
14009
  thinking_effort: exports_external.enum(["low", "medium", "high", "xhigh", "max"]).optional().describe("Adaptive-thinking effort level passed as --effort to the claude CLI. " + "Per-agent override wins over defaults.thinking_effort. " + "lower = faster/cheaper, higher = more reasoning. Omit to use Claude's default."),
14010
14010
  permission_mode: exports_external.enum(["acceptEdits", "auto", "bypassPermissions", "default", "dontAsk", "plan"]).optional().describe("Permission mode passed as --permission-mode to the claude CLI. " + "Per-agent override wins over defaults.permission_mode. " + "Warning: bypassPermissions and dontAsk skip all safety checks \u2014 use only in trusted sandboxes."),
14011
14011
  fallback_model: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._\-/\[\]:]*$/, "Fallback model name must be alphanumeric with ._-/[]: only").optional().describe("Fallback model passed as --fallback-model to the claude CLI. " + "Per-agent override wins over defaults.fallback_model. " + "Used when the primary model is overloaded. Note: only functional in --print (non-interactive) mode per Claude CLI docs; silently no-ops in interactive sessions."),
@@ -14136,6 +14136,11 @@ var init_schema = __esm(() => {
14136
14136
  WebServiceConfigSchema = exports_external.object({
14137
14137
  managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false \u2014 existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
14138
14138
  });
14139
+ FleetHealthConfigSchema = exports_external.object({
14140
+ owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
14141
+ message: "owner_agent must match the standard agent-name pattern"
14142
+ }).optional().describe("The admin agent that runs the Fleet Health sensor + deep-dive crons " + "(RFC fleet-health.md). Default unset \u2192 the feature is inert: no crons " + "are scheduled and the admin page renders its empty state. The named " + "agent must be admin: true. A dedicated owner (not any admin) keeps " + "the fleet-health work attributable, its memory scoped, and its token " + "spend accountable. The detection runs ONLY as operator-set schedules " + "on this agent \u2014 never a self-authored loop (on-leash, " + "no-self-escalation).")
14143
+ });
14139
14144
  HostdConfigSchema = exports_external.object({
14140
14145
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true, admin agents can propose unified-diff " + "patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
14141
14146
  config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. ENFORCED server-side: a caller exceeding this in a sliding " + "1-hour window is rejected with `E_RATE_LIMITED` (carrying a " + "`retry_after` fix) instead of posting another operator approval " + "card \u2014 so a looping agent is throttled rather than spamming the chat.")
@@ -14184,6 +14189,7 @@ var init_schema = __esm(() => {
14184
14189
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
14185
14190
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
14186
14191
  hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
14192
+ fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health \u2014 job-spec-anchored, operator-facing issue tracker (RFC " + "fleet-health.md, serves fleet-stays-healthy). Assigns the owner agent " + "that runs the nightly model-free sensor + weekly deep-dive. Default " + "unset owner_agent \u2192 inert; the admin page renders an empty state."),
14187
14193
  web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
14188
14194
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
14189
14195
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
@@ -15682,7 +15688,7 @@ function scheduleNeedsCronSession(entries, opts) {
15682
15688
  return r.session === "cron";
15683
15689
  });
15684
15690
  }
15685
- var DEFAULT_CRON_MODEL = "claude-sonnet-4-6", CHEAP_MODEL_RE, OPUS_MODEL_RE;
15691
+ var DEFAULT_CRON_MODEL = "claude-sonnet-5", CHEAP_MODEL_RE, OPUS_MODEL_RE;
15686
15692
  var init_cron_routing = __esm(() => {
15687
15693
  CHEAP_MODEL_RE = /(^|[^a-z])(sonnet|haiku)([^a-z]|$)/i;
15688
15694
  OPUS_MODEL_RE = /opus/i;
@@ -22263,6 +22269,14 @@ function renderExecutionDisciplineFragment(context = {}, profilesRoot = PROFILES
22263
22269
  const template = import_handlebars.default.compile(source, { noEscape: true });
22264
22270
  return template(context).trimEnd();
22265
22271
  }
22272
+ function renderReplyDisciplineFragment(context = {}, profilesRoot = PROFILES_ROOT) {
22273
+ const fragPath = join4(resolve4(profilesRoot, "_shared"), "reply-discipline.md.hbs");
22274
+ if (!existsSync6(fragPath))
22275
+ return "";
22276
+ const source = readFileSync6(fragPath, "utf-8");
22277
+ const template = import_handlebars.default.compile(source, { noEscape: true });
22278
+ return template(context).trimEnd();
22279
+ }
22266
22280
  function renderProfileClaudeTemplate(profileName, profilesRoot = PROFILES_ROOT) {
22267
22281
  const profileDir = resolve4(profilesRoot, profileName);
22268
22282
  const hbsPath = join4(profileDir, "CLAUDE.md.hbs");
@@ -22296,7 +22310,7 @@ var init_profiles = __esm(() => {
22296
22310
  return typeof value === "number" && Number.isFinite(value);
22297
22311
  });
22298
22312
  SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
22299
- SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline"];
22313
+ SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
22300
22314
  for (const name of SHARED_FRAGMENTS) {
22301
22315
  const fragPath = join4(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
22302
22316
  if (existsSync6(fragPath)) {
@@ -24883,6 +24897,26 @@ import { homedir as homedir5 } from "node:os";
24883
24897
  import { execSync, execFileSync as execFileSync4 } from "node:child_process";
24884
24898
  import { join as join10, resolve as resolve11 } from "node:path";
24885
24899
  import { createHash as createHash2 } from "node:crypto";
24900
+ function prependReplyDiscipline(rendered, context) {
24901
+ const replyDiscipline = renderReplyDisciplineFragment(context);
24902
+ if (!replyDiscipline)
24903
+ return rendered;
24904
+ const newlineIdx = rendered.indexOf(`
24905
+ `);
24906
+ if (newlineIdx === -1) {
24907
+ return rendered.trimEnd() + `
24908
+
24909
+ ` + replyDiscipline + `
24910
+ `;
24911
+ }
24912
+ const title = rendered.slice(0, newlineIdx);
24913
+ const body = rendered.slice(newlineIdx + 1).replace(/^\n+/, "");
24914
+ return title + `
24915
+
24916
+ ` + replyDiscipline + `
24917
+
24918
+ ` + body;
24919
+ }
24886
24920
  function renderFleetInvariants() {
24887
24921
  return [
24888
24922
  "<!--",
@@ -25452,6 +25486,67 @@ function copyDirRecursive2(src, dest) {
25452
25486
  }
25453
25487
  }
25454
25488
  }
25489
+ function writeFileSyncIfChanged(filePath, content, mode) {
25490
+ if (existsSync15(filePath)) {
25491
+ try {
25492
+ if (readFileSync13(filePath, "utf-8") === content)
25493
+ return false;
25494
+ } catch {}
25495
+ }
25496
+ writeFileSync5(filePath, content, mode !== undefined ? { encoding: "utf-8", mode } : "utf-8");
25497
+ return true;
25498
+ }
25499
+ function dirContentEquals(src, dest, topLevelOverrides) {
25500
+ if (!existsSync15(src) || !existsSync15(dest))
25501
+ return false;
25502
+ let srcEntries;
25503
+ let destEntries;
25504
+ try {
25505
+ srcEntries = readdirSync6(src).sort();
25506
+ destEntries = readdirSync6(dest).sort();
25507
+ } catch {
25508
+ return false;
25509
+ }
25510
+ if (srcEntries.length !== destEntries.length)
25511
+ return false;
25512
+ for (let i = 0;i < srcEntries.length; i++) {
25513
+ if (srcEntries[i] !== destEntries[i])
25514
+ return false;
25515
+ }
25516
+ for (const entry of srcEntries) {
25517
+ const s = join10(src, entry);
25518
+ const d = join10(dest, entry);
25519
+ let sStat;
25520
+ let dStat;
25521
+ try {
25522
+ sStat = statSync7(s);
25523
+ dStat = statSync7(d);
25524
+ } catch {
25525
+ return false;
25526
+ }
25527
+ if (sStat.isDirectory() !== dStat.isDirectory())
25528
+ return false;
25529
+ if (sStat.isDirectory()) {
25530
+ if (!dirContentEquals(s, d))
25531
+ return false;
25532
+ } else {
25533
+ if ((sStat.mode & 64) !== (dStat.mode & 64))
25534
+ return false;
25535
+ try {
25536
+ const expected = topLevelOverrides?.[entry];
25537
+ if (expected !== undefined) {
25538
+ if (readFileSync13(d, "utf-8") !== expected)
25539
+ return false;
25540
+ } else if (!readFileSync13(s).equals(readFileSync13(d))) {
25541
+ return false;
25542
+ }
25543
+ } catch {
25544
+ return false;
25545
+ }
25546
+ }
25547
+ }
25548
+ return true;
25549
+ }
25455
25550
  function seedWorkspaceBootstrapFiles(params) {
25456
25551
  const profileWorkspaceDir = join10(params.profilePath, "workspace");
25457
25552
  if (!existsSync15(profileWorkspaceDir)) {
@@ -25624,11 +25719,21 @@ function installHindsightPlugin(agentName, agentDir, switchroomConfig) {
25624
25719
  return null;
25625
25720
  }
25626
25721
  const destPath = join10(agentDir, ".claude", "plugins", "hindsight-memory");
25627
- if (existsSync15(destPath)) {
25628
- rmSync3(destPath, { recursive: true, force: true });
25629
- }
25630
- copyDirRecursive2(sourcePath, destPath);
25631
25722
  const additionalBanks = resolveUsers(switchroomConfig, agentName).additionalBanks;
25723
+ let expectedSettings = null;
25724
+ try {
25725
+ const vendorSettingsPath = join10(sourcePath, "settings.json");
25726
+ if (existsSync15(vendorSettingsPath)) {
25727
+ expectedSettings = renderHindsightSettingsOverrides(readFileSync13(vendorSettingsPath, "utf-8"), additionalBanks);
25728
+ }
25729
+ } catch {}
25730
+ const upToDate = dirContentEquals(sourcePath, destPath, expectedSettings != null ? { "settings.json": expectedSettings } : undefined);
25731
+ if (!upToDate) {
25732
+ if (existsSync15(destPath)) {
25733
+ rmSync3(destPath, { recursive: true, force: true });
25734
+ }
25735
+ copyDirRecursive2(sourcePath, destPath);
25736
+ }
25632
25737
  applyHindsightSettingsOverrides(destPath, additionalBanks);
25633
25738
  const bankId = agentMemory?.collection ?? agentName;
25634
25739
  const mcpUrl = memory.config?.url ?? "http://127.0.0.1:8888/mcp/";
@@ -25645,11 +25750,17 @@ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
25645
25750
  } catch {
25646
25751
  return;
25647
25752
  }
25753
+ const next = renderHindsightSettingsOverrides(raw, additionalBanks);
25754
+ if (next == null)
25755
+ return;
25756
+ writeFileSyncIfChanged(settingsPath, next);
25757
+ }
25758
+ function renderHindsightSettingsOverrides(raw, additionalBanks) {
25648
25759
  let settings;
25649
25760
  try {
25650
25761
  settings = JSON.parse(raw);
25651
25762
  } catch {
25652
- return;
25763
+ return null;
25653
25764
  }
25654
25765
  settings.retainEveryNTurns = 1;
25655
25766
  settings.recallMaxMemories = 8;
@@ -25659,8 +25770,8 @@ function applyHindsightSettingsOverrides(pluginDestPath, additionalBanks) {
25659
25770
  if (additionalBanks.length > 0) {
25660
25771
  settings.recallAdditionalBanks = [...additionalBanks];
25661
25772
  }
25662
- writeFileSync5(settingsPath, JSON.stringify(settings, null, 2) + `
25663
- `, "utf-8");
25773
+ return JSON.stringify(settings, null, 2) + `
25774
+ `;
25664
25775
  }
25665
25776
  function buildWorkspaceContext(args) {
25666
25777
  const {
@@ -26011,8 +26122,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
26011
26122
  if (agentConfig.settings_raw && Object.keys(agentConfig.settings_raw).length > 0) {
26012
26123
  mergedSettings._switchroomManagedRawKeys = Object.keys(agentConfig.settings_raw);
26013
26124
  }
26014
- writeFileSync5(settingsPath, JSON.stringify(mergedSettings, null, 2) + `
26015
- `, "utf-8");
26125
+ writeFileSyncIfChanged(settingsPath, JSON.stringify(mergedSettings, null, 2) + `
26126
+ `);
26016
26127
  }
26017
26128
  }
26018
26129
  let mcpServerKeysToTrust = null;
@@ -26092,6 +26203,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
26092
26203
  rerenderWithFingerprint(join10(agentDir, dest), () => {
26093
26204
  let rendered = renderTemplate(srcPath, context);
26094
26205
  if (dest === "CLAUDE.md") {
26206
+ rendered = prependReplyDiscipline(rendered, context);
26095
26207
  const vaultProtocol = renderVaultProtocolFragment(context);
26096
26208
  if (vaultProtocol) {
26097
26209
  rendered = rendered.trimEnd() + `
@@ -26261,7 +26373,7 @@ ${fmLines}
26261
26373
 
26262
26374
  ${body}
26263
26375
  `;
26264
- writeFileSync5(mdPath, content, "utf-8");
26376
+ writeFileSyncIfChanged(mdPath, content);
26265
26377
  }
26266
26378
  }
26267
26379
  {
@@ -26576,6 +26688,8 @@ function buildSettingsHooksBlock(p) {
26576
26688
  ` + ` - Question with a short answer \u2192 just reply with the answer.
26577
26689
  ` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' + `"still working".
26578
26690
 
26691
+ ` + "NARRATE YOUR INTENT (your reasoning is NOT shown to the user). The " + "user cannot see your thinking \u2014 only your tool activity and your " + "replies. So before you fire a burst of tools or work silently for a " + "while, first write ONE short plain-language line of intent as " + "ordinary working text (NOT a reply tool call): name what you are " + 'about to do and why \u2014 "Now checking the gateway logs to find why ' + 'the turn stalled". The framework mirrors that line into the live ' + "compose-area preview the user watches; because it is plain text and " + "not a reply, it is never a chat message and never pings. One intent " + "line before a silent stretch, in plain words, never raw tool names " + '("calling Bash", "Read(x)") and never a debug dump. And when you run ' + "a Bash command, always give it a plain-English `description` naming " + "the goal \u2014 that description, not the raw command, is what the user " + "sees. This intent narration is the ONE thing to keep saying; it is " + "NOT the placeholder ack banned above (that is a chat reply, which " + `you still skip).
26692
+
26579
26693
  ` + 'Do NOT send a trailing confirmation after your answer \u2014 no "Done.", ' + '"Sent.", "Hope that helps." as a separate message once you have ' + "already replied. Your answer is the last thing the user should " + `see; a follow-up "Done." is dead-air clutter (and the user's ` + `device already pinged on the answer). Stop after the answer.
26580
26694
 
26581
26695
  ` + "GROUND BEFORE YOU ASSERT. Any fact in your reply that can change " + "(a number, a status, a price, a date, who-uses-what, anything " + '"current" or "latest") must come from a source you actually checked ' + "THIS turn: your data tool, a file, the web. Memory and what you " + '"already know" are leads to verify, not sources. If you have not ' + "checked it this turn, do not state it as fact: go get it now, or tell " + "the user you will confirm and then do it. A confident wrong number is " + `worse than "let me check".
@@ -26906,6 +27020,7 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
26906
27020
  linearAgentEnabled: agentConfig.channels?.telegram?.linear_agent?.enabled === true
26907
27021
  };
26908
27022
  let rendered = renderTemplate(claudeMdSrc, claudeContext);
27023
+ rendered = prependReplyDiscipline(rendered, claudeContext);
26909
27024
  const vaultProtocol = renderVaultProtocolFragment(claudeContext);
26910
27025
  if (vaultProtocol) {
26911
27026
  rendered = rendered.trimEnd() + `
@@ -27530,9 +27645,22 @@ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
27530
27645
  compose the full answer. This holds even for a pure-thinking
27531
27646
  answer: if it will run to a paragraph, ack first. It is the line
27532
27647
  between a colleague and a black box.
27533
- 2. **Then go quiet and work.** Heads-down is correct \u2014 do NOT narrate
27534
- every tool call. A typing indicator runs automatically while you
27535
- work; you do not maintain it.
27648
+ 2. **Then go quiet and work \u2014 but leave a trail of intent.** Heads-down
27649
+ is correct: do NOT narrate every tool call, and a typing indicator
27650
+ runs automatically (you do not maintain it). One thing you SHOULD do,
27651
+ because your private reasoning is never shown to the user: before a
27652
+ burst of tool calls or a long silent stretch, drop ONE short
27653
+ plain-language line of intent in your own working text \u2014 *what* you
27654
+ are about to do and *why* ("Now checking the gateway logs to find why
27655
+ the turn stalled"). Write it as ordinary text, NOT a \`reply\` call, so
27656
+ it never becomes a chat message and never pings \u2014 the framework
27657
+ mirrors that line into the live preview the user watches while you
27658
+ work. One line per stretch, plain words, never raw tool names
27659
+ ("calling Bash") or debug dumps. It is the difference between the user
27660
+ seeing "checking the logs to find the stall" and seeing a black box.
27661
+ When a step is a \`Bash\` command, always give it a plain-English
27662
+ \`description\` naming the goal \u2014 that description, not the raw command,
27663
+ is what the user sees.
27536
27664
  3. **Surface meaningful progress** at genuine inflection points \u2014 a
27537
27665
  hard step finished, a blocker, a pivot, dispatching a sub-agent, a
27538
27666
  notably slow wait, a finding worth knowing now. One short \`reply\`,
@@ -27703,53 +27831,34 @@ operator and stop \u2014 don't theorise about auth or tokens; it just means
27703
27831
  a stored value reached a tool argument.`, TELEGRAM_FORMATTING_FLOOR_CARD = `## Formatting for Telegram
27704
27832
 
27705
27833
  You're writing for a phone screen in Telegram. Every reply renders as rich Markdown
27706
- (Bot API 10.1). Format to make the message **scannable and easy to read**, not
27707
- decorated. These are communication tools \u2014 use them with judgment. Most short replies
27708
- need none of them.
27709
-
27710
- ### Mechanical rules (always)
27711
- - Separate paragraphs with a blank line (\`\\n\\n\`). A single newline collapses onto the
27712
- same line and reads as a wall of text.
27713
- - Keep paragraphs short \u2014 1 to 4 lines. Break before the reader has to work for it.
27714
- - Hard cap is 32768 characters. Long before that, ask whether a wall of text is the
27715
- right answer at all.
27716
-
27717
- ### The toolkit, and when to reach for each
27718
- - **Bold** \u2014 the one key fact or the answer. Not every noun. If everything is bold,
27719
- nothing is.
27720
- - *Italic* \u2014 light emphasis, asides, labels.
27721
- - \`code spans\` \u2014 every identifier: filenames, commands, config keys, agent / account /
27722
- slot names, error codes. Tap-to-copy, and visually distinct from prose.
27723
- - Code fences \u2014 multi-line output: diffs, logs, command blocks, JSON. Add a language
27724
- hint (\` \`\`\`diff \`, \` \`\`\`json \`) when it sharpens the render.
27725
- - Bulleted list \u2014 3+ parallel items the reader will scan or compare. Never for a single
27726
- thought or a flowing narrative.
27727
- - Numbered list \u2014 ordered steps or ranked items.
27728
- - Task lists (\`- [x]\` / \`- [ ]\`) \u2014 progress on a checklist of steps the reader is
27729
- tracking. Renders as real checkboxes; reserve for genuine done/not-done state.
27730
- - Tables \u2014 2-D data only (rows \u00d7 columns): per-account usage, status fields. Render as
27731
- real tables (up to 20 columns). Overkill for a flat list.
27732
- - Headings (\`#\`) \u2014 only in a long, multi-section answer. Clutter on a short reply.
27733
- - Blockquotes (\`>\`) \u2014 quoted text or indented continuation. Telegram drops leading
27734
- spaces, so use \`>\`, never literal indentation.
27735
- - \`==highlight==\` \u2014 a single word or value the reader's eye must land on. Stronger
27736
- than bold; use at most once per message.
27737
- - Dividers (\`---\`) \u2014 between genuinely separate sections. Heavy; use sparingly.
27738
-
27739
- Tables, headings, dividers, task lists, lists, blockquotes and \`==highlight==\` all
27740
- render as rich blocks (Bot API 10.1) \u2014 reach for whichever cuts the reader's scanning
27741
- effort. The ONE exception: sub/superscript (\`H~2~O\`, \`x^2^\`) falls back to literal
27742
- text, so don't use it.
27743
-
27744
- ### The why
27745
- Structure exists for the reader, not the writer. A two-item bullet list is worse than a
27746
- sentence. A heading on a three-line reply is noise. Reach for structure only when it
27747
- **reduces the reader's effort** \u2014 parallel options, scannable data, ordered steps \u2014 and
27748
- stay in prose when the thought is connected. When in doubt: shorter and plainer wins.
27834
+ (Bot API 10.1). One shared spec, three tiers:
27835
+
27836
+ - **Short answers (a line or two): plain prose, no formatting.** "on it, pulling the
27837
+ logs now" is already perfect. No bold, no bullets, no headings.
27838
+ - **Default: light structure.** Bold ONLY the one key fact or answer, never more. Use
27839
+ a list only for 3+ genuinely parallel items the reader will scan or compare; two
27840
+ items or a flowing thought stay prose. \`code spans\` for identifiers: filenames,
27841
+ commands, config keys, error codes (tap-to-copy).
27842
+ - **Long answers may add tables / headings / blockquotes, but only when they genuinely
27843
+ aid scanning**: a table for real 2-D data (rows x columns), headings only in a
27844
+ multi-section answer, \`>\` for quoted text. If the structure doesn't cut the
27845
+ reader's effort, drop it.
27846
+
27847
+ The framework normalizes mechanics in code on every outbound message: block spacing
27848
+ (one blank line between distinct blocks), em/en dashes, and \`\u2022\` bullet markers are
27849
+ rewritten deterministically at send time. Don't fight it or hand-tune spacing; write
27850
+ the content, the gateway makes the typography consistent. Over-bolded messages (most
27851
+ of the text bold, or whole paragraphs/lists bolded) have their bold stripped at send
27852
+ time, so bold sparingly.
27853
+
27854
+ Hard cap is 32768 characters. Long before that, ask whether a wall of text is the
27855
+ right answer at all. Structure exists for the reader, not the writer: a two-item
27856
+ bullet list is worse than a sentence, a heading on a three-line reply is noise. When
27857
+ in doubt, shorter and plainer wins.
27749
27858
 
27750
27859
  Every turn that answers a user message ends with a user-visible \`reply\` (or
27751
27860
  \`stream_reply\` done=true) \u2014 Telegram is all the user sees; your terminal output
27752
- never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-4-6", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
27861
+ never reaches them.`, SWITCHROOM_TELEGRAM_MCP_TOOLS, HINDSIGHT_MCP_TOOLS, AGENT_CONFIG_MCP_TOOLS, HOSTD_MCP_TOOLS, WEBKITE_MCP_TOOLS, LEGACY_SWITCHROOM_MCP_TOKENS, LEGACY_HOSTD_BLANKET_TOKENS, DEFAULT_READ_ONLY_PREAPPROVED_TOOLS, WEBKITE_FLEET_DENY_TOOLS, INTERACTIVE_TUI_FLEET_DENY_TOOLS, WEBKITE_BINARY_CONTAINER_PATH = "/usr/local/bin/webkite", SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-5", SWITCHROOM_DEFAULT_THINKING_EFFORT = "low", SWITCHROOM_OWNED_SETTINGS_KEYS, ALL_BUILTIN_TOOLS, DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin", DOCKER_HOOKS_PATH, DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks", DOCKER_BIN_PATH = "/opt/switchroom/bin", DOCKER_SECURITY_PLUGIN_PATH = "/opt/switchroom/security-plugin", DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml", DOCKER_SWITCHROOM_CLI_PATH = "/usr/local/bin/switchroom", DOCKER_AUTH_BROKER_SOCKET = "/run/switchroom/auth-broker/sock", DOCKER_VAULT_BROKER_SOCKET = "/run/switchroom/broker/sock", DOCKER_AGENT_HOME = "/state/agent/home", INTEGRATION_MCP_RESOLVERS;
27753
27862
  var init_scaffold = __esm(() => {
27754
27863
  init_source();
27755
27864
  init_cron_unit_name();
@@ -39474,6 +39583,50 @@ var init_client5 = __esm(() => {
39474
39583
  init_protocol3();
39475
39584
  });
39476
39585
 
39586
+ // src/web/fleet-health-read.ts
39587
+ import { readFileSync as readFileSync46 } from "node:fs";
39588
+ import { resolve as resolve29 } from "node:path";
39589
+ import { homedir as homedir24 } from "node:os";
39590
+ function fleetHealthLedgerPath(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir24()) {
39591
+ return resolve29(home2, ".switchroom", "fleet-health", "ledger.json");
39592
+ }
39593
+ function readFleetHealth(path4) {
39594
+ let ledger;
39595
+ try {
39596
+ ledger = JSON.parse(readFileSync46(path4, "utf-8"));
39597
+ } catch {
39598
+ return {
39599
+ owner_agent: null,
39600
+ generated_at: null,
39601
+ summary: null,
39602
+ records: [],
39603
+ empty: true,
39604
+ reason: EMPTY_REASON
39605
+ };
39606
+ }
39607
+ const rawRecords = Array.isArray(ledger.records) ? ledger.records : [];
39608
+ const records = rawRecords.map((r) => {
39609
+ const n = Number(r?.priority_score);
39610
+ return { ...r, priority_score: Number.isFinite(n) ? n : 0 };
39611
+ });
39612
+ const ranked = [...records].sort((a, b) => b.priority_score - a.priority_score);
39613
+ return {
39614
+ owner_agent: ledger.owner_agent ?? null,
39615
+ generated_at: ledger.generated_at ?? null,
39616
+ summary: ledger.summary ?? null,
39617
+ records: ranked,
39618
+ empty: ranked.length === 0,
39619
+ ...ranked.length === 0 ? { reason: EMPTY_REASON } : {}
39620
+ };
39621
+ }
39622
+ function handleGetFleetHealth(path4 = fleetHealthLedgerPath()) {
39623
+ return readFleetHealth(path4);
39624
+ }
39625
+ var EMPTY_REASON;
39626
+ var init_fleet_health_read = __esm(() => {
39627
+ EMPTY_REASON = "Fleet Health is not wired up yet \u2014 no ledger at " + "~/.switchroom/fleet-health/ledger.json. Assign an owner agent " + "(`fleet_health.owner_agent` in switchroom.yaml) and schedule its " + "nightly sensor cron. See reference/rfcs/fleet-health.md.";
39628
+ });
39629
+
39477
39630
  // src/cli/doctor-status.ts
39478
39631
  function statusGlyph(status) {
39479
39632
  switch (status) {
@@ -39518,7 +39671,7 @@ var init_thinking_effort_risk = __esm(() => {
39518
39671
  // src/manifest.ts
39519
39672
  import {
39520
39673
  existsSync as existsSync56,
39521
- readFileSync as readFileSync51,
39674
+ readFileSync as readFileSync52,
39522
39675
  readdirSync as readdirSync19
39523
39676
  } from "node:fs";
39524
39677
  import { dirname as dirname16, join as join50 } from "node:path";
@@ -39540,7 +39693,7 @@ function loadManifest(manifestPath) {
39540
39693
  }
39541
39694
  let raw;
39542
39695
  try {
39543
- raw = readFileSync51(path4, "utf-8");
39696
+ raw = readFileSync52(path4, "utf-8");
39544
39697
  } catch (err) {
39545
39698
  throw new Error(`Failed to read manifest at ${path4}: ${err.message}`);
39546
39699
  }
@@ -39605,7 +39758,7 @@ function probePlaywrightMcpVersion() {
39605
39758
  const pkgPath = join50(npxCache, entry, "node_modules/@playwright/mcp/package.json");
39606
39759
  if (existsSync56(pkgPath)) {
39607
39760
  try {
39608
- const pkg = JSON.parse(readFileSync51(pkgPath, "utf-8"));
39761
+ const pkg = JSON.parse(readFileSync52(pkgPath, "utf-8"));
39609
39762
  if (pkg.version)
39610
39763
  return pkg.version;
39611
39764
  } catch {}
@@ -39840,7 +39993,7 @@ var init_doctor_memory = __esm(() => {
39840
39993
  });
39841
39994
 
39842
39995
  // src/cli/doctor-docker.ts
39843
- import { readFileSync as readFileSync52 } from "node:fs";
39996
+ import { readFileSync as readFileSync53 } from "node:fs";
39844
39997
  import { spawnSync as spawnSync8 } from "node:child_process";
39845
39998
  function imageTagOf(ref) {
39846
39999
  if (!ref)
@@ -39856,7 +40009,7 @@ function isDockerMode(opts) {
39856
40009
  return true;
39857
40010
  if (opts?.composePath) {
39858
40011
  try {
39859
- readFileSync52(opts.composePath, "utf8");
40012
+ readFileSync53(opts.composePath, "utf8");
39860
40013
  return true;
39861
40014
  } catch {}
39862
40015
  }
@@ -40138,10 +40291,10 @@ var init_doctor_docker = __esm(() => {
40138
40291
  });
40139
40292
 
40140
40293
  // src/cli/doctor-auth-broker.ts
40141
- import { existsSync as existsSync57, readFileSync as readFileSync53 } from "node:fs";
40294
+ import { existsSync as existsSync57, readFileSync as readFileSync54 } from "node:fs";
40142
40295
  import { createHash as createHash11 } from "node:crypto";
40143
40296
  import { spawnSync as spawnSync9 } from "node:child_process";
40144
- import { homedir as homedir27 } from "node:os";
40297
+ import { homedir as homedir28 } from "node:os";
40145
40298
  import { join as join51 } from "node:path";
40146
40299
  function defaultDockerInspect(container, format) {
40147
40300
  try {
@@ -40252,7 +40405,7 @@ function checkAuthBrokerDrift(deps = {}) {
40252
40405
  }
40253
40406
  let index;
40254
40407
  try {
40255
- index = JSON.parse(readFileSync53(indexPath, "utf-8"));
40408
+ index = JSON.parse(readFileSync54(indexPath, "utf-8"));
40256
40409
  } catch (err) {
40257
40410
  return {
40258
40411
  name: "auth-broker: drift",
@@ -40261,7 +40414,7 @@ function checkAuthBrokerDrift(deps = {}) {
40261
40414
  fix: "Inspect `~/.switchroom/state/auth-broker/sha-index.json` for corruption."
40262
40415
  };
40263
40416
  }
40264
- const home2 = deps.home ?? homedir27();
40417
+ const home2 = deps.home ?? homedir28();
40265
40418
  const divergent = [];
40266
40419
  const missingOnDisk = [];
40267
40420
  for (const [label, expected] of Object.entries(index)) {
@@ -40272,7 +40425,7 @@ function checkAuthBrokerDrift(deps = {}) {
40272
40425
  }
40273
40426
  let got;
40274
40427
  try {
40275
- got = sha256Hex(readFileSync53(credsPath, "utf-8"));
40428
+ got = sha256Hex(readFileSync54(credsPath, "utf-8"));
40276
40429
  } catch (err) {
40277
40430
  divergent.push(`${label} (read failed: ${err.message})`);
40278
40431
  continue;
@@ -40313,7 +40466,7 @@ function checkAuthBrokerThresholdViolations(deps = {}) {
40313
40466
  }
40314
40467
  let violations;
40315
40468
  try {
40316
- violations = JSON.parse(readFileSync53(path4, "utf-8"));
40469
+ violations = JSON.parse(readFileSync54(path4, "utf-8"));
40317
40470
  } catch (err) {
40318
40471
  return {
40319
40472
  name: "auth-broker: threshold violations",
@@ -40347,7 +40500,7 @@ function checkAuthBrokerActiveAccount(config, deps = {}) {
40347
40500
  fix: "Run `switchroom auth use <label>` to pin a fleet-wide account, then `switchroom apply`. Without an active account every agent boot fails."
40348
40501
  };
40349
40502
  }
40350
- const home2 = deps.home ?? homedir27();
40503
+ const home2 = deps.home ?? homedir28();
40351
40504
  const dir = accountDir(active, home2);
40352
40505
  if (!existsSync57(dir)) {
40353
40506
  return {
@@ -40499,7 +40652,7 @@ import {
40499
40652
  realpathSync as realpathSync5,
40500
40653
  statSync as statSync23
40501
40654
  } from "node:fs";
40502
- import { userInfo, homedir as homedir28 } from "node:os";
40655
+ import { userInfo, homedir as homedir29 } from "node:os";
40503
40656
  import { join as join52 } from "node:path";
40504
40657
  function resolveVaultPath2(config) {
40505
40658
  return config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
@@ -40638,7 +40791,7 @@ async function runSecretAccessChecks(config, deps = {}) {
40638
40791
  };
40639
40792
  const passphrase = deps.passphrase ?? process.env.SWITCHROOM_VAULT_PASSPHRASE;
40640
40793
  if (!passphrase) {
40641
- const sock = deps.brokerOperatorSocket ?? join52(homedir28(), ".switchroom", "broker-operator", "sock");
40794
+ const sock = deps.brokerOperatorSocket ?? join52(homedir29(), ".switchroom", "broker-operator", "sock");
40642
40795
  const preflight = deps.preflight ?? ((a, k) => defaultPreflight(sock, a, k));
40643
40796
  for (const name of Object.keys(config.agents ?? {})) {
40644
40797
  const resolved = resolveAgentConfig(config.defaults, config.profiles, config.agents[name]);
@@ -40723,8 +40876,8 @@ import {
40723
40876
  existsSync as realExistsSync,
40724
40877
  readFileSync as realReadFileSync
40725
40878
  } from "node:fs";
40726
- import { join as join53, resolve as resolve32 } from "node:path";
40727
- import { homedir as homedir29 } from "node:os";
40879
+ import { join as join53, resolve as resolve33 } from "node:path";
40880
+ import { homedir as homedir30 } from "node:os";
40728
40881
  function resolveDeps(config, deps) {
40729
40882
  let agentsDir = deps.agentsDir;
40730
40883
  if (agentsDir === undefined) {
@@ -40838,7 +40991,7 @@ function checkScaffoldWiring(config, driveAgents, d) {
40838
40991
  ];
40839
40992
  }
40840
40993
  for (const name of driveAgents) {
40841
- const agentDir = resolve32(d.agentsDir, name);
40994
+ const agentDir = resolve33(d.agentsDir, name);
40842
40995
  if (!d.existsSync(agentDir)) {
40843
40996
  results.push({
40844
40997
  name: `drive: ${name} scaffold`,
@@ -40876,7 +41029,7 @@ function checkScaffoldWiring(config, driveAgents, d) {
40876
41029
  let trustOk = false;
40877
41030
  let trustDetail = "no .claude/.claude.json";
40878
41031
  if (trustRead.kind === "ok") {
40879
- const proj = trustRead.data?.projects?.[resolve32(agentDir)];
41032
+ const proj = trustRead.data?.projects?.[resolve33(agentDir)];
40880
41033
  const enabled = proj?.enabledMcpjsonServers;
40881
41034
  if (Array.isArray(enabled) && enabled.includes("gdrive")) {
40882
41035
  trustOk = true;
@@ -40961,7 +41114,7 @@ async function runDriveBrokerReachabilityChecks(config, deps = {}) {
40961
41114
  }
40962
41115
  ];
40963
41116
  }
40964
- const sock = deps.brokerOperatorSocket ?? join53(homedir29(), ".switchroom", "broker-operator", "sock");
41117
+ const sock = deps.brokerOperatorSocket ?? join53(homedir30(), ".switchroom", "broker-operator", "sock");
40965
41118
  const preflight = deps.preflight ?? ((a, k) => defaultPreflight(sock, a, k));
40966
41119
  const results = [];
40967
41120
  for (const agent of driveAgents) {
@@ -41016,7 +41169,7 @@ import {
41016
41169
  closeSync as realCloseSync
41017
41170
  } from "node:fs";
41018
41171
  import { join as join54 } from "node:path";
41019
- import { homedir as homedir30 } from "node:os";
41172
+ import { homedir as homedir31 } from "node:os";
41020
41173
  function defaultReadHead(p, n) {
41021
41174
  let fd;
41022
41175
  try {
@@ -41044,7 +41197,7 @@ function resolveDeps2(config, deps) {
41044
41197
  }
41045
41198
  }
41046
41199
  return {
41047
- homeDir: deps.homeDir ?? homedir30(),
41200
+ homeDir: deps.homeDir ?? homedir31(),
41048
41201
  agentsDir,
41049
41202
  existsSync: deps.existsSync ?? ((p) => realExistsSync2(p)),
41050
41203
  readFileSync: deps.readFileSync ?? ((p) => realReadFileSync2(p, "utf-8")),
@@ -41201,7 +41354,7 @@ var init_doctor_webkite = __esm(() => {
41201
41354
 
41202
41355
  // src/cli/doctor-cron-session.ts
41203
41356
  import { statSync as realStatSync } from "node:fs";
41204
- import { resolve as resolve33 } from "node:path";
41357
+ import { resolve as resolve34 } from "node:path";
41205
41358
  function agentRunsCronSession(config, agent) {
41206
41359
  const raw = config.agents[agent];
41207
41360
  if (!raw)
@@ -41217,7 +41370,7 @@ function runCronSessionChecks(config, deps = defaultDeps2) {
41217
41370
  if (!agentRunsCronSession(config, agent))
41218
41371
  continue;
41219
41372
  const name = `cron-session: ${agent}`;
41220
- const alivePath = resolve33(agentsDir, agent, "telegram", ".bridge-alive-cron");
41373
+ const alivePath = resolve34(agentsDir, agent, "telegram", ".bridge-alive-cron");
41221
41374
  let mtimeMs;
41222
41375
  try {
41223
41376
  mtimeMs = deps.statMtimeMs(alivePath);
@@ -41266,7 +41419,7 @@ var init_doctor_cron_session = __esm(() => {
41266
41419
  });
41267
41420
 
41268
41421
  // src/cli/doctor-scaffold-wiring.ts
41269
- import { join as join55, resolve as resolve34 } from "node:path";
41422
+ import { join as join55, resolve as resolve35 } from "node:path";
41270
41423
  function readJson2(d, path4) {
41271
41424
  if (!d.existsSync(path4))
41272
41425
  return { kind: "absent" };
@@ -41300,7 +41453,7 @@ function checkIntegrationScaffoldWiring(args) {
41300
41453
  ];
41301
41454
  }
41302
41455
  for (const name of agents) {
41303
- const agentDir = resolve34(agentsDir, name);
41456
+ const agentDir = resolve35(agentsDir, name);
41304
41457
  if (!deps.existsSync(agentDir)) {
41305
41458
  results.push({
41306
41459
  name: `${label}: ${name} scaffold`,
@@ -41340,7 +41493,7 @@ function checkIntegrationScaffoldWiring(args) {
41340
41493
  let trustDetail = "no .claude/.claude.json";
41341
41494
  if (trustRead.kind === "ok") {
41342
41495
  const projects = trustRead.data?.projects ?? {};
41343
- const proj = projects[resolve34(agentDir)];
41496
+ const proj = projects[resolve35(agentDir)];
41344
41497
  const enabled = proj?.enabledMcpjsonServers;
41345
41498
  if (Array.isArray(enabled) && enabled.includes(mcpKey)) {
41346
41499
  trustOk = true;
@@ -41375,9 +41528,9 @@ import {
41375
41528
  readFileSync as realReadFileSync3
41376
41529
  } from "node:fs";
41377
41530
  import { join as join56 } from "node:path";
41378
- import { homedir as homedir31 } from "node:os";
41531
+ import { homedir as homedir32 } from "node:os";
41379
41532
  function resolveDeps3(deps) {
41380
- const home2 = deps.homeDir?.() ?? homedir31();
41533
+ const home2 = deps.homeDir?.() ?? homedir32();
41381
41534
  return {
41382
41535
  existsSync: deps.existsSync ?? realExistsSync3,
41383
41536
  readFileSync: deps.readFileSync ?? realReadFileSync3,
@@ -41562,9 +41715,9 @@ import {
41562
41715
  statSync as realStatSync2
41563
41716
  } from "node:fs";
41564
41717
  import { join as join57 } from "node:path";
41565
- import { homedir as homedir32 } from "node:os";
41718
+ import { homedir as homedir33 } from "node:os";
41566
41719
  function resolveDeps4(deps) {
41567
- const home2 = deps.homeDir?.() ?? homedir32();
41720
+ const home2 = deps.homeDir?.() ?? homedir33();
41568
41721
  return {
41569
41722
  existsSync: deps.existsSync ?? realExistsSync4,
41570
41723
  readFileSync: deps.readFileSync ?? realReadFileSync4,
@@ -41915,10 +42068,10 @@ import {
41915
42068
  readdirSync as realReaddirSync2,
41916
42069
  statSync as realStatSync3
41917
42070
  } from "node:fs";
41918
- import { homedir as homedir33 } from "node:os";
42071
+ import { homedir as homedir34 } from "node:os";
41919
42072
  import { join as join58 } from "node:path";
41920
42073
  function runCredentialsMigrationChecks(config, deps = {}) {
41921
- const credDir = deps.credentialsDir ?? join58(homedir33(), ".switchroom", "credentials");
42074
+ const credDir = deps.credentialsDir ?? join58(homedir34(), ".switchroom", "credentials");
41922
42075
  const existsSync59 = deps.existsSync ?? ((p) => realExistsSync5(p));
41923
42076
  const readdirSync21 = deps.readdirSync ?? ((p) => realReaddirSync2(p));
41924
42077
  const isDirectory = deps.isDirectory ?? ((p) => {
@@ -42069,7 +42222,7 @@ var init_doctor_inlined_secrets = __esm(() => {
42069
42222
 
42070
42223
  // src/cli/doctor-audit-integrity.ts
42071
42224
  import { readFileSync as fsReadFileSync2 } from "node:fs";
42072
- import { homedir as homedir34 } from "node:os";
42225
+ import { homedir as homedir35 } from "node:os";
42073
42226
  import { join as join59 } from "node:path";
42074
42227
  function rootWrittenLogs(home2) {
42075
42228
  return [
@@ -42081,7 +42234,7 @@ function rootWrittenLogs(home2) {
42081
42234
  ];
42082
42235
  }
42083
42236
  function runAuditIntegrityChecks(deps = {}) {
42084
- const home2 = deps.homeDir ?? homedir34();
42237
+ const home2 = deps.homeDir ?? homedir35();
42085
42238
  const read = deps.readFileSync ?? ((p) => fsReadFileSync2(p, "utf8"));
42086
42239
  const results = [];
42087
42240
  for (const { label, path: path4 } of rootWrittenLogs(home2)) {
@@ -42139,13 +42292,13 @@ var init_doctor_audit_integrity = __esm(() => {
42139
42292
 
42140
42293
  // src/cli/doctor-agent-smoke.ts
42141
42294
  import { existsSync as existsSync59 } from "node:fs";
42142
- import { homedir as homedir35 } from "node:os";
42295
+ import { homedir as homedir36 } from "node:os";
42143
42296
  import { join as join60 } from "node:path";
42144
42297
  import { randomUUID as randomUUID5 } from "node:crypto";
42145
42298
  async function runAgentSmokeChecks(config, deps = {}) {
42146
42299
  if (deps.fast)
42147
42300
  return [];
42148
- const home2 = deps.homeDir ?? homedir35();
42301
+ const home2 = deps.homeDir ?? homedir36();
42149
42302
  const sock = deps.operatorSockPath ?? join60(home2, ".switchroom", "hostd", "operator", "sock");
42150
42303
  if (!deps.hostdRequestImpl && !existsSync59(sock)) {
42151
42304
  return [
@@ -42226,7 +42379,7 @@ var init_doctor_agent_smoke = __esm(() => {
42226
42379
  // src/cli/doctor-vault-broker-durability.ts
42227
42380
  import { execFileSync as execFileSync18 } from "node:child_process";
42228
42381
  import { existsSync as existsSync60, statSync as statSync24 } from "node:fs";
42229
- import { homedir as homedir36 } from "node:os";
42382
+ import { homedir as homedir37 } from "node:os";
42230
42383
  import { join as join61 } from "node:path";
42231
42384
  function probeBindMountInode(hostPath, brokerContainerPath, opts) {
42232
42385
  const statHost = opts?.statHost ?? defaultStatHost;
@@ -42370,7 +42523,7 @@ function defaultBrokerStatusProbe() {
42370
42523
  }
42371
42524
  }
42372
42525
  function runVaultBrokerDurabilityChecks(_config, opts) {
42373
- const home2 = homedir36();
42526
+ const home2 = homedir37();
42374
42527
  const probe2 = opts?.inodeProbe ?? probeBindMountInode;
42375
42528
  return [
42376
42529
  probeBrokerUnlocked(opts?.statusProbe),
@@ -42629,11 +42782,11 @@ import {
42629
42782
  existsSync as existsSync61,
42630
42783
  lstatSync as lstatSync7,
42631
42784
  mkdirSync as mkdirSync32,
42632
- readFileSync as readFileSync54,
42785
+ readFileSync as readFileSync55,
42633
42786
  readdirSync as readdirSync21,
42634
42787
  statSync as statSync25
42635
42788
  } from "node:fs";
42636
- import { dirname as dirname17, join as join62, resolve as resolve35 } from "node:path";
42789
+ import { dirname as dirname17, join as join62, resolve as resolve36 } from "node:path";
42637
42790
  import { createPublicKey, createPrivateKey } from "node:crypto";
42638
42791
  function findInNvm(bin) {
42639
42792
  const nvmRoot = join62(process.env.HOME ?? "", ".nvm", "versions", "node");
@@ -43441,7 +43594,7 @@ function classifyReadError(err) {
43441
43594
  }
43442
43595
  function tryReadHostFile(path4) {
43443
43596
  try {
43444
- return { kind: "ok", content: readFileSync54(path4, "utf-8") };
43597
+ return { kind: "ok", content: readFileSync55(path4, "utf-8") };
43445
43598
  } catch (err) {
43446
43599
  const kind = classifyReadError(err);
43447
43600
  const error = err?.message ?? String(err);
@@ -43457,7 +43610,7 @@ function parseEnvFile(path4) {
43457
43610
  return {};
43458
43611
  let content;
43459
43612
  try {
43460
- content = readFileSync54(path4, "utf-8");
43613
+ content = readFileSync55(path4, "utf-8");
43461
43614
  } catch {
43462
43615
  return {};
43463
43616
  }
@@ -43574,7 +43727,7 @@ function checkStartShStale(agentName, startShPath) {
43574
43727
  }
43575
43728
  let content;
43576
43729
  try {
43577
- content = readFileSync54(startShPath, "utf-8");
43730
+ content = readFileSync55(startShPath, "utf-8");
43578
43731
  } catch (err) {
43579
43732
  return {
43580
43733
  name: label,
@@ -43687,7 +43840,7 @@ function isSwitchroomCheckout(dir) {
43687
43840
  const pkgPath = join62(dir, "package.json");
43688
43841
  if (!existsSync61(pkgPath))
43689
43842
  return false;
43690
- const pkg = JSON.parse(readFileSync54(pkgPath, "utf-8"));
43843
+ const pkg = JSON.parse(readFileSync55(pkgPath, "utf-8"));
43691
43844
  return pkg.name === "switchroom";
43692
43845
  } catch {
43693
43846
  return false;
@@ -43699,7 +43852,7 @@ function checkAgents(config, configPath) {
43699
43852
  const statuses = getAllAgentStatuses(config);
43700
43853
  const authStatuses = getAllAuthStatuses(config);
43701
43854
  for (const [name, agentConfig] of Object.entries(config.agents)) {
43702
- const agentDir = resolve35(agentsDir, name);
43855
+ const agentDir = resolve36(agentsDir, name);
43703
43856
  if (!existsSync61(agentDir)) {
43704
43857
  results.push({
43705
43858
  name: `${name}: scaffold`,
@@ -43808,7 +43961,7 @@ function checkAgents(config, configPath) {
43808
43961
  });
43809
43962
  } else {
43810
43963
  try {
43811
- const mcp = JSON.parse(readFileSync54(mcpJsonPath, "utf-8"));
43964
+ const mcp = JSON.parse(readFileSync55(mcpJsonPath, "utf-8"));
43812
43965
  const hasSwitchroomTelegram = !!mcp.mcpServers?.["switchroom-telegram"];
43813
43966
  const memoryEnabled = isHindsightEnabled(config);
43814
43967
  const hasHindsight = !!mcp.mcpServers?.hindsight;
@@ -43882,7 +44035,7 @@ function mffAgentName(config) {
43882
44035
  function mffEnvPath(config) {
43883
44036
  const home2 = process.env.HOME ?? "/root";
43884
44037
  const agent = mffAgentName(config);
43885
- return agent ? resolve35(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve35(home2, ".switchroom/credentials/my-family-finance/.env");
44038
+ return agent ? resolve36(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve36(home2, ".switchroom/credentials/my-family-finance/.env");
43886
44039
  }
43887
44040
  function mffEnvState(envPath) {
43888
44041
  if (!existsSync61(envPath))
@@ -44298,16 +44451,16 @@ async function checkManifestDrift(probers) {
44298
44451
  return results;
44299
44452
  }
44300
44453
  function runDockerSection(config) {
44301
- const composePath = resolve35(process.env.HOME ?? "", ".switchroom", "compose", "docker-compose.yml");
44454
+ const composePath = resolve36(process.env.HOME ?? "", ".switchroom", "compose", "docker-compose.yml");
44302
44455
  const active = isDockerMode({ composePath });
44303
44456
  let composeYaml;
44304
44457
  let dockerfileAgent;
44305
44458
  try {
44306
- composeYaml = readFileSync54(composePath, "utf8");
44459
+ composeYaml = readFileSync55(composePath, "utf8");
44307
44460
  } catch {}
44308
- const dockerfilePath = resolve35(process.env.HOME ?? "", ".switchroom", "docker", "Dockerfile.agent");
44461
+ const dockerfilePath = resolve36(process.env.HOME ?? "", ".switchroom", "docker", "Dockerfile.agent");
44309
44462
  try {
44310
- dockerfileAgent = readFileSync54(dockerfilePath, "utf8");
44463
+ dockerfileAgent = readFileSync55(dockerfilePath, "utf8");
44311
44464
  } catch {}
44312
44465
  return runDockerChecks({
44313
44466
  config,
@@ -47300,7 +47453,7 @@ vault:
47300
47453
  # Every agent inherits this. Per-agent fields win on conflict.
47301
47454
  # tools/skills/schedule are unioned; scalars are overridden.
47302
47455
  defaults:
47303
- model: claude-sonnet-4-6
47456
+ model: claude-sonnet-5
47304
47457
  # The switchroom-telegram fork is the default. Uncomment to fall back to
47305
47458
  # the upstream marketplace plugin: channels.telegram.plugin: official
47306
47459
  channels:
@@ -47583,7 +47736,7 @@ memory:
47583
47736
  shared_collection: shared
47584
47737
 
47585
47738
  defaults:
47586
- model: claude-sonnet-4-6
47739
+ model: claude-sonnet-5
47587
47740
  tools:
47588
47741
  allow: [all]
47589
47742
  subagents:
@@ -47708,7 +47861,7 @@ var CONNECTION_HEALTH_FILENAME = "connection-health.json";
47708
47861
  var init_connection_health = () => {};
47709
47862
 
47710
47863
  // src/cli/update-prompt-hook.ts
47711
- import { existsSync as existsSync77, readFileSync as readFileSync66, writeFileSync as writeFileSync38, chmodSync as chmodSync10, mkdirSync as mkdirSync44 } from "node:fs";
47864
+ import { existsSync as existsSync77, readFileSync as readFileSync67, writeFileSync as writeFileSync38, chmodSync as chmodSync10, mkdirSync as mkdirSync44 } from "node:fs";
47712
47865
  import { join as join77 } from "node:path";
47713
47866
  function containerHookCommand() {
47714
47867
  return join77(CONTAINER_AGENT_DIR, ".claude", "hooks", HOOK_FILENAME);
@@ -47782,7 +47935,7 @@ function installUpdatePromptHook(agentDir) {
47782
47935
  const scriptPath = join77(hooksDir, HOOK_FILENAME);
47783
47936
  const desired = updatePromptHookScript();
47784
47937
  let installed = false;
47785
- const existing = existsSync77(scriptPath) ? readFileSync66(scriptPath, "utf-8") : "";
47938
+ const existing = existsSync77(scriptPath) ? readFileSync67(scriptPath, "utf-8") : "";
47786
47939
  if (existing !== desired) {
47787
47940
  writeFileSync38(scriptPath, desired, { mode: 493 });
47788
47941
  chmodSync10(scriptPath, 493);
@@ -47796,7 +47949,7 @@ function installUpdatePromptHook(agentDir) {
47796
47949
  if (!existsSync77(settingsPath)) {
47797
47950
  return { scriptPath, settingsPath, installed };
47798
47951
  }
47799
- const raw = readFileSync66(settingsPath, "utf-8");
47952
+ const raw = readFileSync67(settingsPath, "utf-8");
47800
47953
  let parsed;
47801
47954
  try {
47802
47955
  parsed = JSON.parse(raw);
@@ -48020,7 +48173,7 @@ __export(exports_voice_sidecar_token, {
48020
48173
  VOICE_SIDECAR_TOKEN_ENV: () => VOICE_SIDECAR_TOKEN_ENV
48021
48174
  });
48022
48175
  import { randomBytes as randomBytes14 } from "node:crypto";
48023
- import { chmodSync as chmodSync11, chownSync as chownSync7, existsSync as existsSync79, mkdirSync as mkdirSync45, readFileSync as readFileSync67, rmSync as rmSync17, writeFileSync as writeFileSync39 } from "node:fs";
48176
+ import { chmodSync as chmodSync11, chownSync as chownSync7, existsSync as existsSync79, mkdirSync as mkdirSync45, readFileSync as readFileSync68, rmSync as rmSync17, writeFileSync as writeFileSync39 } from "node:fs";
48024
48177
  import { dirname as dirname26 } from "node:path";
48025
48178
  async function defaultResolveOrSeedToken(home2, writeErr) {
48026
48179
  const [{ getViaBrokerStructured: getViaBrokerStructured2, putViaBroker: putViaBroker2 }, { resolveOperatorVaultPassphrase }] = await Promise.all([
@@ -48056,7 +48209,7 @@ async function provisionVoiceSidecarToken(composePath, home2, ctx) {
48056
48209
  if (engine !== "local") {
48057
48210
  try {
48058
48211
  if (existsSync79(envPath)) {
48059
- const body = readFileSync67(envPath, "utf-8");
48212
+ const body = readFileSync68(envPath, "utf-8");
48060
48213
  if (body.includes(`${VOICE_SIDECAR_TOKEN_ENV}=`))
48061
48214
  rmSync17(envPath);
48062
48215
  }
@@ -48079,7 +48232,7 @@ async function provisionVoiceSidecarToken(composePath, home2, ctx) {
48079
48232
  let body = "";
48080
48233
  try {
48081
48234
  if (existsSync79(envPath))
48082
- body = readFileSync67(envPath, "utf-8");
48235
+ body = readFileSync68(envPath, "utf-8");
48083
48236
  } catch {}
48084
48237
  const line = `${VOICE_SIDECAR_TOKEN_ENV}=${token}`;
48085
48238
  const keyRe = new RegExp(`^${VOICE_SIDECAR_TOKEN_ENV}=.*$`, "m");
@@ -48132,12 +48285,12 @@ __export(exports_apply, {
48132
48285
  DEFAULT_COMPOSE_PATH: () => DEFAULT_COMPOSE_PATH2,
48133
48286
  COMPOSE_PROJECT: () => COMPOSE_PROJECT2
48134
48287
  });
48135
- import { accessSync as accessSync3, chownSync as chownSync8, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync80, mkdirSync as mkdirSync46, readFileSync as readFileSync68, readdirSync as readdirSync27, renameSync as renameSync15, writeFileSync as writeFileSync40 } from "node:fs";
48288
+ import { accessSync as accessSync3, chownSync as chownSync8, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync80, mkdirSync as mkdirSync46, readFileSync as readFileSync69, readdirSync as readdirSync27, renameSync as renameSync15, writeFileSync as writeFileSync40 } from "node:fs";
48136
48289
  import { mkdir as mkdir2 } from "node:fs/promises";
48137
48290
  import { spawnSync as childSpawnSync } from "node:child_process";
48138
48291
  import readline from "node:readline";
48139
- import { dirname as dirname27, join as join79, resolve as resolve48 } from "node:path";
48140
- import { homedir as homedir46 } from "node:os";
48292
+ import { dirname as dirname27, join as join79, resolve as resolve49 } from "node:path";
48293
+ import { homedir as homedir47 } from "node:os";
48141
48294
  import { execFileSync as execFileSync25 } from "node:child_process";
48142
48295
  function effectiveLiteLLMEnabled(config, agentResolvedLitellm) {
48143
48296
  return agentResolvedLitellm?.enabled ?? config.litellm?.enabled ?? false;
@@ -48175,7 +48328,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
48175
48328
  Promise.resolve().then(() => (init_provision(), exports_provision)),
48176
48329
  Promise.resolve().then(() => (init_telegram_yaml(), exports_telegram_yaml))
48177
48330
  ]);
48178
- const passphrase = await resolveOperatorVaultPassphrase(ctx.home ?? homedir46());
48331
+ const passphrase = await resolveOperatorVaultPassphrase(ctx.home ?? homedir47());
48179
48332
  if (passphrase === null) {
48180
48333
  const { getViaBrokerStructured: getViaBrokerStructured3 } = await Promise.resolve().then(() => (init_client(), exports_client));
48181
48334
  const alreadyProvisioned = [];
@@ -48217,7 +48370,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
48217
48370
  let configText = null;
48218
48371
  if (switchroomConfigPath && existsSync80(switchroomConfigPath)) {
48219
48372
  try {
48220
- configText = readFileSync68(switchroomConfigPath, "utf-8");
48373
+ configText = readFileSync69(switchroomConfigPath, "utf-8");
48221
48374
  } catch (err) {
48222
48375
  ctx.writeErr(source_default.yellow(` ! litellm: could not read config for ACL grants (${err.message}); keys will be provisioned but agents may lack read-ACL.
48223
48376
  `));
@@ -48459,7 +48612,7 @@ async function ensureHostMountSources(config) {
48459
48612
  await mkdir2(fleetDir, { recursive: true });
48460
48613
  const invariantsPath = join79(fleetDir, "switchroom-invariants.md");
48461
48614
  const invariantsCanonical = renderFleetInvariants();
48462
- const invariantsCurrent = existsSync80(invariantsPath) ? readFileSync68(invariantsPath, "utf-8") : null;
48615
+ const invariantsCurrent = existsSync80(invariantsPath) ? readFileSync69(invariantsPath, "utf-8") : null;
48463
48616
  if (invariantsCurrent !== invariantsCanonical) {
48464
48617
  writeFileSync40(invariantsPath, invariantsCanonical, { mode: 420 });
48465
48618
  }
@@ -48546,7 +48699,7 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
48546
48699
  `));
48547
48700
  }
48548
48701
  }
48549
- function writeInstallTypeCache(homeDir = homedir46()) {
48702
+ function writeInstallTypeCache(homeDir = homedir47()) {
48550
48703
  const ctx = detectInstallType();
48551
48704
  const dir = join79(homeDir, ".switchroom");
48552
48705
  const out = join79(dir, "install-type.json");
@@ -48663,7 +48816,7 @@ Applying switchroom config...
48663
48816
  writeOut,
48664
48817
  writeErr,
48665
48818
  failures,
48666
- home: homedir46()
48819
+ home: homedir47()
48667
48820
  });
48668
48821
  }
48669
48822
  await ensureHostMountSources(config);
@@ -48685,7 +48838,7 @@ Applying switchroom config...
48685
48838
  }
48686
48839
  const vaultPathConfigured = config.vault?.path;
48687
48840
  const customVaultPath = vaultPathConfigured ? resolvePath(vaultPathConfigured) : undefined;
48688
- const migrationResult = migrateVaultLayout(homedir46(), {
48841
+ const migrationResult = migrateVaultLayout(homedir47(), {
48689
48842
  customVaultPath
48690
48843
  });
48691
48844
  switch (migrationResult.kind) {
@@ -48711,7 +48864,7 @@ Applying switchroom config...
48711
48864
  writeErr(formatDivergentRecoveryMessage(migrationResult.details));
48712
48865
  process.exit(4);
48713
48866
  }
48714
- const postMigrationInspect = inspectVaultLayout(homedir46());
48867
+ const postMigrationInspect = inspectVaultLayout(homedir47());
48715
48868
  const acceptable = [
48716
48869
  "no-vault",
48717
48870
  "already-migrated",
@@ -48726,7 +48879,7 @@ Expected one of: ${acceptable.join(", ")}
48726
48879
  `));
48727
48880
  process.exit(5);
48728
48881
  }
48729
- const vaultDir = resolveVaultBindMountDir(homedir46(), {
48882
+ const vaultDir = resolveVaultBindMountDir(homedir47(), {
48730
48883
  migrationKind: migrationResult.kind,
48731
48884
  customVaultPath
48732
48885
  });
@@ -48757,7 +48910,7 @@ vault.enc.lock (PID-file flock from saveVault), and
48757
48910
  });
48758
48911
  if (!skipScaffold) {
48759
48912
  const { provisionVoiceSidecarToken: provisionVoiceSidecarToken2 } = await Promise.resolve().then(() => (init_voice_sidecar_token(), exports_voice_sidecar_token));
48760
- await provisionVoiceSidecarToken2(composePath, homedir46(), {
48913
+ await provisionVoiceSidecarToken2(composePath, homedir47(), {
48761
48914
  writeOut,
48762
48915
  writeErr,
48763
48916
  operatorUid
@@ -48773,7 +48926,7 @@ Wrote `) + displayComposePath + source_default.gray(` (${composeBytes} bytes)
48773
48926
  writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
48774
48927
  `));
48775
48928
  if (process.geteuid?.() === 0 && operatorUid !== undefined) {
48776
- const restored = restoreOperatorOwnership(homedir46(), operatorUid);
48929
+ const restored = restoreOperatorOwnership(homedir47(), operatorUid);
48777
48930
  if (restored.length > 0) {
48778
48931
  writeOut(source_default.gray(` Restored operator ownership of ${restored.length} ~/.switchroom path(s)
48779
48932
  `));
@@ -48820,7 +48973,7 @@ function copyExampleConfig2(name) {
48820
48973
  if (!/^[a-z0-9_-]+$/.test(name)) {
48821
48974
  throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
48822
48975
  }
48823
- const dest = resolve48(process.cwd(), "switchroom.yaml");
48976
+ const dest = resolve49(process.cwd(), "switchroom.yaml");
48824
48977
  if (existsSync80(dest)) {
48825
48978
  console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
48826
48979
  return;
@@ -48831,7 +48984,7 @@ function copyExampleConfig2(name) {
48831
48984
  console.log(source_default.green(`Copied ${name}.yaml -> switchroom.yaml`));
48832
48985
  return;
48833
48986
  }
48834
- const exampleFile = resolve48(import.meta.dirname, `../../examples/${name}.yaml`);
48987
+ const exampleFile = resolve49(import.meta.dirname, `../../examples/${name}.yaml`);
48835
48988
  if (!existsSync80(exampleFile)) {
48836
48989
  throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
48837
48990
  }
@@ -49027,7 +49180,7 @@ var init_apply = __esm(() => {
49027
49180
  switchroom: switchroom_default,
49028
49181
  minimal: minimal_default
49029
49182
  };
49030
- DEFAULT_COMPOSE_PATH2 = join79(homedir46(), ".switchroom", "compose", "docker-compose.yml");
49183
+ DEFAULT_COMPOSE_PATH2 = join79(homedir47(), ".switchroom", "compose", "docker-compose.yml");
49031
49184
  SELF_ELEVATE_PRESERVED_ENV = [
49032
49185
  "HOME",
49033
49186
  "SWITCHROOM_CONFIG",
@@ -54502,7 +54655,7 @@ class Protocol {
54502
54655
  return;
54503
54656
  }
54504
54657
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
54505
- await new Promise((resolve53) => setTimeout(resolve53, pollInterval));
54658
+ await new Promise((resolve54) => setTimeout(resolve54, pollInterval));
54506
54659
  options?.signal?.throwIfAborted();
54507
54660
  }
54508
54661
  } catch (error2) {
@@ -54514,7 +54667,7 @@ class Protocol {
54514
54667
  }
54515
54668
  request(request, resultSchema, options) {
54516
54669
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
54517
- return new Promise((resolve53, reject) => {
54670
+ return new Promise((resolve54, reject) => {
54518
54671
  const earlyReject = (error2) => {
54519
54672
  reject(error2);
54520
54673
  };
@@ -54592,7 +54745,7 @@ class Protocol {
54592
54745
  if (!parseResult.success) {
54593
54746
  reject(parseResult.error);
54594
54747
  } else {
54595
- resolve53(parseResult.data);
54748
+ resolve54(parseResult.data);
54596
54749
  }
54597
54750
  } catch (error2) {
54598
54751
  reject(error2);
@@ -54783,12 +54936,12 @@ class Protocol {
54783
54936
  interval = task.pollInterval;
54784
54937
  }
54785
54938
  } catch {}
54786
- return new Promise((resolve53, reject) => {
54939
+ return new Promise((resolve54, reject) => {
54787
54940
  if (signal.aborted) {
54788
54941
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
54789
54942
  return;
54790
54943
  }
54791
- const timeoutId = setTimeout(resolve53, interval);
54944
+ const timeoutId = setTimeout(resolve54, interval);
54792
54945
  signal.addEventListener("abort", () => {
54793
54946
  clearTimeout(timeoutId);
54794
54947
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
@@ -57773,7 +57926,7 @@ var require_compile = __commonJS((exports2) => {
57773
57926
  const schOrFunc = root.refs[ref];
57774
57927
  if (schOrFunc)
57775
57928
  return schOrFunc;
57776
- let _sch = resolve53.call(this, root, ref);
57929
+ let _sch = resolve54.call(this, root, ref);
57777
57930
  if (_sch === undefined) {
57778
57931
  const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref];
57779
57932
  const { schemaId } = this.opts;
@@ -57800,7 +57953,7 @@ var require_compile = __commonJS((exports2) => {
57800
57953
  function sameSchemaEnv(s1, s2) {
57801
57954
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
57802
57955
  }
57803
- function resolve53(root, ref) {
57956
+ function resolve54(root, ref) {
57804
57957
  let sch;
57805
57958
  while (typeof (sch = this.refs[ref]) == "string")
57806
57959
  ref = sch;
@@ -58330,7 +58483,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
58330
58483
  }
58331
58484
  return uri;
58332
58485
  }
58333
- function resolve53(baseURI, relativeURI, options) {
58486
+ function resolve54(baseURI, relativeURI, options) {
58334
58487
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
58335
58488
  const resolved = resolveComponent(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
58336
58489
  schemelessOptions.skipEscape = true;
@@ -58558,7 +58711,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
58558
58711
  var fastUri = {
58559
58712
  SCHEMES,
58560
58713
  normalize,
58561
- resolve: resolve53,
58714
+ resolve: resolve54,
58562
58715
  resolveComponent,
58563
58716
  equal,
58564
58717
  serialize,
@@ -61941,12 +62094,12 @@ class StdioServerTransport {
61941
62094
  this.onclose?.();
61942
62095
  }
61943
62096
  send(message) {
61944
- return new Promise((resolve53) => {
62097
+ return new Promise((resolve54) => {
61945
62098
  const json = serializeMessage(message);
61946
62099
  if (this._stdout.write(json)) {
61947
- resolve53();
62100
+ resolve54();
61948
62101
  } else {
61949
- this._stdout.once("drain", resolve53);
62102
+ this._stdout.once("drain", resolve54);
61950
62103
  }
61951
62104
  });
61952
62105
  }
@@ -62472,7 +62625,7 @@ __export(exports_server2, {
62472
62625
  TOOLS: () => TOOLS2
62473
62626
  });
62474
62627
  import { randomBytes as randomBytes16 } from "node:crypto";
62475
- import { existsSync as existsSync89, readFileSync as readFileSync77 } from "node:fs";
62628
+ import { existsSync as existsSync89, readFileSync as readFileSync78 } from "node:fs";
62476
62629
  function selfSocketPath() {
62477
62630
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
62478
62631
  }
@@ -62689,7 +62842,7 @@ function getLastUpdateApplyStatus() {
62689
62842
  }
62690
62843
  let raw;
62691
62844
  try {
62692
- raw = readFileSync77(path8, "utf-8");
62845
+ raw = readFileSync78(path8, "utf-8");
62693
62846
  } catch (err2) {
62694
62847
  return errorText2(`get_status: failed to read audit log at ${path8}: ${err2.message}`);
62695
62848
  }
@@ -62968,6 +63121,475 @@ var init_server4 = __esm(() => {
62968
63121
  ];
62969
63122
  });
62970
63123
 
63124
+ // src/fleet-health/detect.ts
63125
+ function parseTurns(text) {
63126
+ const out = [];
63127
+ for (const raw of text.split(`
63128
+ `)) {
63129
+ const line = raw.trim();
63130
+ if (!line)
63131
+ continue;
63132
+ try {
63133
+ out.push(JSON.parse(line));
63134
+ } catch {}
63135
+ }
63136
+ return out;
63137
+ }
63138
+ function isoFromTs(ts) {
63139
+ if (typeof ts !== "number" || !Number.isFinite(ts))
63140
+ return null;
63141
+ return new Date(ts * 1000).toISOString();
63142
+ }
63143
+ function detectTurnFindings(agent, turns) {
63144
+ const findings = [];
63145
+ for (const t of turns) {
63146
+ const tid = t.turn_id ?? "?";
63147
+ const st = t.status;
63148
+ const tl = typeof t.tools === "number" ? t.tools : 0;
63149
+ const dur = typeof t.duration_ms === "number" ? t.duration_ms : 0;
63150
+ const synthetic = tid.includes("synthetic-");
63151
+ const ts = isoFromTs(t.ts);
63152
+ if (st !== "complete" && st !== "no_reply") {
63153
+ findings.push({
63154
+ signal: "killed-incomplete-turn",
63155
+ agent,
63156
+ turn_id: tid,
63157
+ log_pointer: `turns.jsonl:${tid} status=${st}`,
63158
+ ts
63159
+ });
63160
+ }
63161
+ if (dur > HANG_MS && tl <= HANG_MAXTOOLS) {
63162
+ findings.push({
63163
+ signal: "hang-long-stalled",
63164
+ agent,
63165
+ turn_id: tid,
63166
+ log_pointer: `turns.jsonl:${tid} ${Math.floor(dur / 1000)}s tools=${tl}`,
63167
+ ts
63168
+ });
63169
+ }
63170
+ if (st === "complete" && tl === 0 && !synthetic) {
63171
+ findings.push({
63172
+ signal: "silent-no-op-candidate",
63173
+ agent,
63174
+ turn_id: tid,
63175
+ log_pointer: `turns.jsonl:${tid} tools=0`,
63176
+ ts
63177
+ });
63178
+ }
63179
+ }
63180
+ return findings;
63181
+ }
63182
+ function detectGatewayFindings(agent, logText, logName = `logs/${agent}/gateway-supervisor.log`) {
63183
+ const findings = [];
63184
+ const gw_hits = {
63185
+ "duplicate-delivery-represent": 0,
63186
+ "represent-escalation": 0,
63187
+ "reply-delivery-failure": 0
63188
+ };
63189
+ const lines = logText.split(`
63190
+ `);
63191
+ for (const [name, re] of Object.entries(GATEWAY_SIGNATURES)) {
63192
+ for (let i = 0;i < lines.length; i++) {
63193
+ const line = lines[i];
63194
+ if (!line)
63195
+ continue;
63196
+ if (re.test(line)) {
63197
+ gw_hits[name] += 1;
63198
+ findings.push({
63199
+ signal: name,
63200
+ agent,
63201
+ turn_id: extractTurnId(line) ?? `${agent}:gw#${i + 1}`,
63202
+ log_pointer: `${logName}:${i + 1}`,
63203
+ ts: extractTs(line)
63204
+ });
63205
+ }
63206
+ }
63207
+ }
63208
+ return { findings, gw_hits };
63209
+ }
63210
+ function extractTurnId(line) {
63211
+ const m = line.match(/\d+:_#\d+/);
63212
+ return m ? m[0] : null;
63213
+ }
63214
+ function extractTs(line) {
63215
+ const m = line.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?/);
63216
+ return m ? m[0] : null;
63217
+ }
63218
+ function scanAgent(agent, turnsText, gatewayText) {
63219
+ const turns = parseTurns(turnsText);
63220
+ const status_mix = {};
63221
+ for (const t of turns) {
63222
+ const st = t.status ?? "unknown";
63223
+ status_mix[st] = (status_mix[st] ?? 0) + 1;
63224
+ }
63225
+ const turnFindings = detectTurnFindings(agent, turns);
63226
+ const { findings: gwFindings, gw_hits } = detectGatewayFindings(agent, gatewayText);
63227
+ const findings = [...turnFindings, ...gwFindings];
63228
+ const escalate = turnFindings.some((f) => f.signal === "killed-incomplete-turn" || f.signal === "hang-long-stalled" || f.signal === "silent-no-op-candidate") || gw_hits["duplicate-delivery-represent"] > 0 || gw_hits["reply-delivery-failure"] > 0;
63229
+ return { agent, turns: turns.length, status_mix, findings, gw_hits, escalate };
63230
+ }
63231
+ var HANG_MS = 360000, HANG_MAXTOOLS = 2, GATEWAY_SIGNATURES;
63232
+ var init_detect = __esm(() => {
63233
+ GATEWAY_SIGNATURES = {
63234
+ "duplicate-delivery-represent": /represent duplicate-send/,
63235
+ "represent-escalation": /obligation escalation/,
63236
+ "reply-delivery-failure": /tg-post method=sendRichMessage[^\n]*status=err/
63237
+ };
63238
+ });
63239
+
63240
+ // src/fleet-health/mapping.ts
63241
+ function mapSignal(signal) {
63242
+ return SIGNAL_MAP[signal];
63243
+ }
63244
+ function dedupKeyFor(finding) {
63245
+ const m = mapSignal(finding.signal);
63246
+ return `${m.job_spec}:${m.signature}`;
63247
+ }
63248
+ function frequencyFactor(count) {
63249
+ return Math.log10(1 + Math.max(0, count));
63250
+ }
63251
+ function recencyFactor(newestIso, now = new Date, windowDays = 30) {
63252
+ if (!newestIso)
63253
+ return 0;
63254
+ const newest = Date.parse(newestIso);
63255
+ if (!Number.isFinite(newest))
63256
+ return 0;
63257
+ const ageMs = now.getTime() - newest;
63258
+ const dayMs = 86400000;
63259
+ if (ageMs <= dayMs)
63260
+ return 1;
63261
+ const windowMs = windowDays * dayMs;
63262
+ if (ageMs >= windowMs)
63263
+ return 0.1;
63264
+ const frac = (ageMs - dayMs) / (windowMs - dayMs);
63265
+ return 1 - frac * 0.9;
63266
+ }
63267
+ function reachFactor(reachCount) {
63268
+ return Math.max(1, reachCount);
63269
+ }
63270
+ function issuePriority(severity, frequency, reachCount, newestIso, now = new Date, windowDays = 30) {
63271
+ return severity * frequencyFactor(frequency) * reachFactor(reachCount) * recencyFactor(newestIso, now, windowDays);
63272
+ }
63273
+ var SIGNAL_MAP, ALL_JOB_SPECS;
63274
+ var init_mapping = __esm(() => {
63275
+ SIGNAL_MAP = {
63276
+ "silent-no-op-candidate": {
63277
+ failure_mode: "silent-no-op",
63278
+ severity: 3,
63279
+ job_spec: "know-what-my-agent-is-doing",
63280
+ signature: "silent-no-op:completed-zero-tools"
63281
+ },
63282
+ "duplicate-delivery-represent": {
63283
+ failure_mode: "duplicate",
63284
+ severity: 2,
63285
+ job_spec: "talk-to-agents-from-anywhere",
63286
+ signature: "represent-duplicate-send:reply-tool-not-called"
63287
+ },
63288
+ "reply-delivery-failure": {
63289
+ failure_mode: "success-theater",
63290
+ severity: 3,
63291
+ job_spec: "talk-to-agents-from-anywhere",
63292
+ signature: "reply-delivery-failure:sendRichMessage-err"
63293
+ },
63294
+ "hang-long-stalled": {
63295
+ failure_mode: "partial",
63296
+ severity: 2,
63297
+ job_spec: "steer-or-queue-mid-flight",
63298
+ signature: "hang:long-stalled-turn"
63299
+ },
63300
+ "killed-incomplete-turn": {
63301
+ failure_mode: "missed-trigger",
63302
+ severity: 3,
63303
+ job_spec: "steer-or-queue-mid-flight",
63304
+ signature: "killed:incomplete-turn"
63305
+ },
63306
+ "represent-escalation": {
63307
+ failure_mode: "drift",
63308
+ severity: 1,
63309
+ job_spec: "feel-like-a-colleague",
63310
+ signature: "represent:obligation-escalation"
63311
+ }
63312
+ };
63313
+ ALL_JOB_SPECS = [
63314
+ "act-in-my-tools-with-an-identity",
63315
+ "approve-what-my-agent-can-touch",
63316
+ "crons-use-the-model-only-when-it-earns-it",
63317
+ "deliver-files-i-can-open",
63318
+ "extend-without-forking",
63319
+ "feel-like-a-colleague",
63320
+ "fleet-stays-healthy",
63321
+ "get-better-the-longer-they-run",
63322
+ "get-from-zero-to-a-working-fleet",
63323
+ "give-each-agent-its-own-workspace",
63324
+ "idempotent-update-and-restart",
63325
+ "keep-my-subscription-honest",
63326
+ "know-what-my-agent-is-doing",
63327
+ "operate-the-fleet-from-telegram",
63328
+ "remember-across-sessions",
63329
+ "restart-and-know-what-im-running",
63330
+ "run-a-fleet-of-specialists",
63331
+ "see-my-whole-fleet-from-one-screen",
63332
+ "share-auth-across-the-fleet",
63333
+ "steer-or-queue-mid-flight",
63334
+ "survive-reboots-and-real-life",
63335
+ "talk-to-agents-from-anywhere",
63336
+ "track-plan-quota-live"
63337
+ ];
63338
+ });
63339
+
63340
+ // src/fleet-health/ledger.ts
63341
+ function indexPriorIssues(prior) {
63342
+ const idx = new Map;
63343
+ if (!prior?.records)
63344
+ return idx;
63345
+ for (const rec of prior.records) {
63346
+ for (const iss of rec.issues ?? []) {
63347
+ idx.set(iss.dedup_key, {
63348
+ gh_issue: iss.gh_issue,
63349
+ frequency: iss.frequency,
63350
+ status: iss.status,
63351
+ job_spec: rec.job_spec,
63352
+ failure_mode: iss.failure_mode,
63353
+ severity: iss.severity
63354
+ });
63355
+ }
63356
+ }
63357
+ return idx;
63358
+ }
63359
+ function newer(a, b) {
63360
+ if (!a)
63361
+ return b;
63362
+ if (!b)
63363
+ return a;
63364
+ return Date.parse(a) >= Date.parse(b) ? a : b;
63365
+ }
63366
+ function buildLedger(findings, opts = {}) {
63367
+ const now = opts.now ?? new Date;
63368
+ const windowDays = opts.windowDays ?? 30;
63369
+ const priorIdx = indexPriorIssues(opts.prior ?? null);
63370
+ const nowIso = now.toISOString();
63371
+ const aggs = new Map;
63372
+ for (const f of findings) {
63373
+ const m = mapSignal(f.signal);
63374
+ const key = dedupKeyFor(f);
63375
+ let agg = aggs.get(key);
63376
+ if (!agg) {
63377
+ agg = {
63378
+ dedup_key: key,
63379
+ job_spec: m.job_spec,
63380
+ failure_mode: m.failure_mode,
63381
+ severity: m.severity,
63382
+ occurrences: [],
63383
+ reach: new Set,
63384
+ newest: null,
63385
+ count: 0
63386
+ };
63387
+ aggs.set(key, agg);
63388
+ }
63389
+ agg.count += 1;
63390
+ agg.reach.add(f.agent);
63391
+ agg.newest = newer(agg.newest, f.ts);
63392
+ if (agg.occurrences.length < 50) {
63393
+ agg.occurrences.push({
63394
+ agent: f.agent,
63395
+ turn_id: f.turn_id,
63396
+ log_pointer: f.log_pointer
63397
+ });
63398
+ }
63399
+ }
63400
+ const byJob = new Map;
63401
+ for (const agg of aggs.values()) {
63402
+ const count = agg.count;
63403
+ const prior = priorIdx.get(agg.dedup_key);
63404
+ let status = "open";
63405
+ if (count <= RESOLVED_THRESHOLD && prior && prior.frequency > RESOLVED_THRESHOLD) {
63406
+ status = "resolved-pending-verify";
63407
+ } else if (prior?.status === "resolved-pending-verify" && count <= RESOLVED_THRESHOLD) {
63408
+ status = "closed";
63409
+ }
63410
+ const issue2 = {
63411
+ dedup_key: agg.dedup_key,
63412
+ failure_mode: agg.failure_mode,
63413
+ severity: agg.severity,
63414
+ frequency: count,
63415
+ reach: [...agg.reach].sort(),
63416
+ recency: agg.newest,
63417
+ occurrences: agg.occurrences,
63418
+ ...prior?.gh_issue !== undefined ? { gh_issue: prior.gh_issue } : {},
63419
+ status
63420
+ };
63421
+ const list2 = byJob.get(agg.job_spec) ?? [];
63422
+ list2.push(issue2);
63423
+ byJob.set(agg.job_spec, list2);
63424
+ }
63425
+ for (const [dedup_key, prior] of priorIdx) {
63426
+ if (aggs.has(dedup_key))
63427
+ continue;
63428
+ if (prior.status !== "open" && prior.status !== "resolved-pending-verify") {
63429
+ continue;
63430
+ }
63431
+ const issue2 = {
63432
+ dedup_key,
63433
+ failure_mode: prior.failure_mode,
63434
+ severity: prior.severity,
63435
+ frequency: 0,
63436
+ reach: [],
63437
+ recency: null,
63438
+ occurrences: [],
63439
+ ...prior.gh_issue !== undefined ? { gh_issue: prior.gh_issue } : {},
63440
+ status: "closed"
63441
+ };
63442
+ const list2 = byJob.get(prior.job_spec) ?? [];
63443
+ list2.push(issue2);
63444
+ byJob.set(prior.job_spec, list2);
63445
+ }
63446
+ const records = ALL_JOB_SPECS.map((job_spec) => {
63447
+ const issues = byJob.get(job_spec) ?? [];
63448
+ const openIssues = issues.filter((i) => i.status === "open");
63449
+ const priority_score = issues.reduce((max, i) => {
63450
+ const s = issuePriority(i.severity, i.frequency, i.reach.length, i.recency, now, windowDays);
63451
+ return Math.max(max, s);
63452
+ }, 0);
63453
+ const gh_issues = issues.filter((i) => i.status !== "closed").map((i) => i.gh_issue).filter((n) => typeof n === "number");
63454
+ return {
63455
+ job_spec,
63456
+ open_issue_count: openIssues.length,
63457
+ last_scanned: nowIso,
63458
+ priority_score: Number(priority_score.toFixed(4)),
63459
+ gh_issues,
63460
+ last_deep_dive: findLastDeepDive(opts.prior ?? null, job_spec),
63461
+ issues
63462
+ };
63463
+ });
63464
+ return {
63465
+ ...opts.ownerAgent ? { owner_agent: opts.ownerAgent } : {},
63466
+ generated_at: nowIso,
63467
+ ...opts.summary ? { summary: opts.summary } : {},
63468
+ records
63469
+ };
63470
+ }
63471
+ function findLastDeepDive(prior, job_spec) {
63472
+ if (!prior?.records)
63473
+ return null;
63474
+ const rec = prior.records.find((r) => r.job_spec === job_spec);
63475
+ return rec?.last_deep_dive ?? null;
63476
+ }
63477
+ var RESOLVED_THRESHOLD = 3;
63478
+ var init_ledger = __esm(() => {
63479
+ init_mapping();
63480
+ });
63481
+
63482
+ // src/fleet-health/scan.ts
63483
+ var exports_scan = {};
63484
+ __export(exports_scan, {
63485
+ writeLedger: () => writeLedger,
63486
+ runScan: () => runScan,
63487
+ resolveSwitchroomBase: () => resolveSwitchroomBase,
63488
+ readLedgerIfPresent: () => readLedgerIfPresent,
63489
+ listAgents: () => listAgents,
63490
+ ledgerPathForBase: () => ledgerPathForBase
63491
+ });
63492
+ import {
63493
+ readFileSync as readFileSync80,
63494
+ readdirSync as readdirSync35,
63495
+ existsSync as existsSync92,
63496
+ mkdirSync as mkdirSync53,
63497
+ writeFileSync as writeFileSync46
63498
+ } from "node:fs";
63499
+ import { resolve as resolve54, dirname as dirname30 } from "node:path";
63500
+ import { homedir as homedir56 } from "node:os";
63501
+ function resolveSwitchroomBase(home2 = process.env.SWITCHROOM_HOME ?? process.env.HOME ?? homedir56()) {
63502
+ return resolve54(home2, ".switchroom");
63503
+ }
63504
+ function listAgents(base) {
63505
+ const dir = resolve54(base, "agents");
63506
+ if (!existsSync92(dir))
63507
+ return [];
63508
+ try {
63509
+ return readdirSync35(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
63510
+ } catch {
63511
+ return [];
63512
+ }
63513
+ }
63514
+ function runScan(opts = {}) {
63515
+ const base = opts.base ?? resolveSwitchroomBase();
63516
+ const log = opts.log ?? (() => {});
63517
+ const agents = listAgents(base);
63518
+ const findings = [];
63519
+ const perAgent = [];
63520
+ const skipped = [];
63521
+ let scanned = 0;
63522
+ for (const agent of agents) {
63523
+ const turnsPath = resolve54(base, "agents", agent, "turns.jsonl");
63524
+ const gwPath = resolve54(base, "logs", agent, "gateway-supervisor.log");
63525
+ let turnsText = "";
63526
+ let gwText = "";
63527
+ let sawArtifact = false;
63528
+ try {
63529
+ if (existsSync92(turnsPath)) {
63530
+ turnsText = readFileSync80(turnsPath, "utf-8");
63531
+ sawArtifact = true;
63532
+ }
63533
+ } catch (e) {
63534
+ log(`fleet-health: WARN skipping ${agent} turns.jsonl unreadable: ${String(e)}`);
63535
+ }
63536
+ try {
63537
+ if (existsSync92(gwPath)) {
63538
+ gwText = readFileSync80(gwPath, "utf-8");
63539
+ sawArtifact = true;
63540
+ }
63541
+ } catch (e) {
63542
+ log(`fleet-health: WARN ${agent} gateway log unreadable: ${String(e)}`);
63543
+ }
63544
+ if (!sawArtifact) {
63545
+ skipped.push(agent);
63546
+ continue;
63547
+ }
63548
+ try {
63549
+ const res = scanAgent(agent, turnsText, gwText);
63550
+ perAgent.push(res);
63551
+ findings.push(...res.findings);
63552
+ scanned++;
63553
+ } catch (e) {
63554
+ log(`fleet-health: WARN scan failed for ${agent}: ${String(e)}`);
63555
+ skipped.push(agent);
63556
+ }
63557
+ }
63558
+ const prior = readLedgerIfPresent(base);
63559
+ const ledger = buildLedger(findings, {
63560
+ ownerAgent: opts.ownerAgent,
63561
+ now: opts.now,
63562
+ windowDays: opts.windowDays,
63563
+ prior
63564
+ });
63565
+ return { ledger, perAgent, agentsScanned: scanned, agentsSkipped: skipped };
63566
+ }
63567
+ function readLedgerIfPresent(base) {
63568
+ const path8 = ledgerPathForBase(base);
63569
+ try {
63570
+ if (!existsSync92(path8))
63571
+ return null;
63572
+ return JSON.parse(readFileSync80(path8, "utf-8"));
63573
+ } catch {
63574
+ return null;
63575
+ }
63576
+ }
63577
+ function ledgerPathForBase(base) {
63578
+ return fleetHealthLedgerPath(dirname30(base));
63579
+ }
63580
+ function writeLedger(base, ledger) {
63581
+ const path8 = ledgerPathForBase(base);
63582
+ mkdirSync53(dirname30(path8), { recursive: true });
63583
+ writeFileSync46(path8, JSON.stringify(ledger, null, 2) + `
63584
+ `, "utf-8");
63585
+ return path8;
63586
+ }
63587
+ var init_scan = __esm(() => {
63588
+ init_fleet_health_read();
63589
+ init_detect();
63590
+ init_ledger();
63591
+ });
63592
+
62971
63593
  // src/cli/index.ts
62972
63594
  init_esm();
62973
63595
 
@@ -62976,8 +63598,8 @@ import { existsSync, readFileSync } from "node:fs";
62976
63598
  import { dirname, join } from "node:path";
62977
63599
 
62978
63600
  // src/build-info.ts
62979
- var VERSION = "0.16.38";
62980
- var COMMIT_SHA = "6b75d6d7";
63601
+ var VERSION = "0.15.48";
63602
+ var COMMIT_SHA = "eee77212";
62981
63603
 
62982
63604
  // src/cli/resolve-version.ts
62983
63605
  function readPackageVersion() {
@@ -76615,8 +77237,16 @@ var HINDSIGHT_DEFAULT_UI_PORT = 9999;
76615
77237
  var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
76616
77238
  var HINDSIGHT_CONSUMER_NAME = "hindsight";
76617
77239
  var HINDSIGHT_DEFAULT_UID = 11000;
76618
- var HINDSIGHT_IMAGE = "ghcr.io/switchroom/switchroom-hindsight:latest";
76619
- var HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-4-6";
77240
+ var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
77241
+ var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
77242
+ function hindsightImageRef(tag) {
77243
+ const t = tag?.trim();
77244
+ if (!t)
77245
+ return HINDSIGHT_IMAGE;
77246
+ const v = t.replace(/^v/, "");
77247
+ return `${HINDSIGHT_IMAGE_REPO}:v${v}`;
77248
+ }
77249
+ var HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-5";
76620
77250
  var HINDSIGHT_DEFAULT_MCP_STATELESS = true;
76621
77251
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
76622
77252
  var HINDSIGHT_DEFAULT_RERANKER_BUCKET_BATCHING = "true";
@@ -76693,7 +77323,7 @@ function isHindsightContainerExists() {
76693
77323
  return false;
76694
77324
  }
76695
77325
  }
76696
- function startHindsight(ports, litellm) {
77326
+ function startHindsight(ports, litellm, imageTag) {
76697
77327
  const apiPort = ports?.apiPort ?? HINDSIGHT_DEFAULT_API_PORT;
76698
77328
  const uiPort = ports?.uiPort ?? HINDSIGHT_DEFAULT_UI_PORT;
76699
77329
  const envArgs = [
@@ -76756,7 +77386,7 @@ x-litellm-tags: service:hindsight`);
76756
77386
  } else {
76757
77387
  args.push("-p", `127.0.0.1:${apiPort}:8888`, "-p", `127.0.0.1:${uiPort}:9999`);
76758
77388
  }
76759
- args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, HINDSIGHT_IMAGE);
77389
+ args.push("-v", "switchroom-hindsight-data:/home/hindsight/.pg0", "-v", "switchroom-hindsight-backups:/backups", "-v", `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`, "--tmpfs", `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`, ...envArgs, hindsightImageRef(imageTag));
76760
77390
  execFileSync16("docker", args, { stdio: "pipe" });
76761
77391
  }
76762
77392
  function stopHindsight() {
@@ -76767,8 +77397,8 @@ function stopHindsight() {
76767
77397
  execFileSync16("docker", ["rm", "switchroom-hindsight"], { stdio: "pipe" });
76768
77398
  } catch {}
76769
77399
  }
76770
- function pullHindsightImage() {
76771
- execFileSync16("docker", ["pull", HINDSIGHT_IMAGE], { stdio: "inherit" });
77400
+ function pullHindsightImage(imageTag) {
77401
+ execFileSync16("docker", ["pull", hindsightImageRef(imageTag)], { stdio: "inherit" });
76772
77402
  }
76773
77403
  function getRunningHindsightPorts() {
76774
77404
  const readPort = (containerPort) => {
@@ -77055,7 +77685,7 @@ Cross-agent reflection plan
77055
77685
  }
77056
77686
  console.log();
77057
77687
  }));
77058
- 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.").option("--provider <provider>", "LLM provider (ollama, openai, anthropic)").action(async (opts) => {
77688
+ 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.").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)").action(async (opts) => {
77059
77689
  if (opts.status) {
77060
77690
  if (!isDockerAvailable()) {
77061
77691
  console.log(source_default.red(" Docker is not available."));
@@ -77105,9 +77735,9 @@ Cross-agent reflection plan
77105
77735
  return;
77106
77736
  }
77107
77737
  if (recreate) {
77108
- console.log(source_default.gray(" Pulling latest Hindsight image..."));
77738
+ console.log(source_default.gray(` Pulling ${opts.tag ? `Hindsight image ${opts.tag}` : "latest Hindsight image"}...`));
77109
77739
  try {
77110
- pullHindsightImage();
77740
+ pullHindsightImage(opts.tag);
77111
77741
  } catch (err) {
77112
77742
  console.error(source_default.red(`
77113
77743
  Failed to pull Hindsight image: ${err.message}
@@ -77142,7 +77772,7 @@ Cross-agent reflection plan
77142
77772
  console.log(source_default.gray(" Starting Hindsight Docker container..."));
77143
77773
  try {
77144
77774
  const litellmCfg = await resolveLiteLLMForHindsight(getConfig(program3));
77145
- startHindsight(ports, litellmCfg);
77775
+ startHindsight(ports, litellmCfg, opts.tag);
77146
77776
  if (litellmCfg) {
77147
77777
  console.log(source_default.gray(" LiteLLM routing enabled for hindsight (--network host)."));
77148
77778
  }
@@ -77349,7 +77979,7 @@ init_merge();
77349
77979
  init_loader();
77350
77980
  init_client();
77351
77981
  import {
77352
- readFileSync as readFileSync49,
77982
+ readFileSync as readFileSync50,
77353
77983
  existsSync as existsSync54,
77354
77984
  realpathSync as realpathSync4,
77355
77985
  mkdirSync as mkdirSync30,
@@ -77358,8 +77988,8 @@ import {
77358
77988
  writeSync as writeSync6,
77359
77989
  constants as fsConstants3
77360
77990
  } from "node:fs";
77361
- import { resolve as resolve30, extname, join as join49, relative, dirname as dirname14 } from "node:path";
77362
- import { homedir as homedir26 } from "node:os";
77991
+ import { resolve as resolve31, extname, join as join49, relative, dirname as dirname14 } from "node:path";
77992
+ import { homedir as homedir27 } from "node:os";
77363
77993
  import { timingSafeEqual as timingSafeEqual3, randomBytes as randomBytes11 } from "node:crypto";
77364
77994
 
77365
77995
  // src/web/api.ts
@@ -79192,10 +79822,13 @@ async function handleGetSummary(deps, now = Date.now()) {
79192
79822
  };
79193
79823
  }
79194
79824
 
79825
+ // src/web/server.ts
79826
+ init_fleet_health_read();
79827
+
79195
79828
  // src/web/webhook-handler.ts
79196
- import { appendFileSync as appendFileSync4, existsSync as existsSync52, mkdirSync as mkdirSync29, readFileSync as readFileSync47, writeFileSync as writeFileSync27 } from "fs";
79829
+ import { appendFileSync as appendFileSync4, existsSync as existsSync52, mkdirSync as mkdirSync29, readFileSync as readFileSync48, writeFileSync as writeFileSync27 } from "fs";
79197
79830
  import { join as join47 } from "path";
79198
- import { homedir as homedir25 } from "os";
79831
+ import { homedir as homedir26 } from "os";
79199
79832
 
79200
79833
  // src/web/webhook-verify.ts
79201
79834
  import { createHmac as createHmac2, timingSafeEqual } from "crypto";
@@ -79359,7 +79992,7 @@ function escapeHtml3(s) {
79359
79992
  import net4 from "node:net";
79360
79993
  function forwardToGateway(socketPath, req, opts = {}) {
79361
79994
  const timeoutMs = opts.timeoutMs ?? 5000;
79362
- return new Promise((resolve29) => {
79995
+ return new Promise((resolve30) => {
79363
79996
  let settled = false;
79364
79997
  let buf = "";
79365
79998
  const done = (value) => {
@@ -79369,7 +80002,7 @@ function forwardToGateway(socketPath, req, opts = {}) {
79369
80002
  try {
79370
80003
  conn.destroy();
79371
80004
  } catch {}
79372
- resolve29(value);
80005
+ resolve30(value);
79373
80006
  };
79374
80007
  const conn = net4.createConnection(socketPath);
79375
80008
  conn.setEncoding("utf8");
@@ -79400,20 +80033,20 @@ function forwardToGateway(socketPath, req, opts = {}) {
79400
80033
  }
79401
80034
 
79402
80035
  // src/web/webhook-edge.ts
79403
- import { existsSync as existsSync51, readFileSync as readFileSync46 } from "fs";
80036
+ import { existsSync as existsSync51, readFileSync as readFileSync47 } from "fs";
79404
80037
  import { join as join46 } from "path";
79405
- import { homedir as homedir24 } from "os";
80038
+ import { homedir as homedir25 } from "os";
79406
80039
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
79407
80040
  var EDGE_HEADER = "x-switchroom-edge";
79408
80041
  function edgeSecretPath() {
79409
- return join46(homedir24(), ".switchroom", "webhook-edge-secret");
80042
+ return join46(homedir25(), ".switchroom", "webhook-edge-secret");
79410
80043
  }
79411
80044
  function loadEdgeSecret(path4) {
79412
80045
  const p = path4 ?? edgeSecretPath();
79413
80046
  try {
79414
80047
  if (!existsSync51(p))
79415
80048
  return null;
79416
- const raw = readFileSync46(p, "utf-8").trim();
80049
+ const raw = readFileSync47(p, "utf-8").trim();
79417
80050
  return raw.length > 0 ? raw : null;
79418
80051
  } catch {
79419
80052
  return null;
@@ -79447,7 +80080,7 @@ function loadDedupFile(path4) {
79447
80080
  try {
79448
80081
  if (!existsSync52(path4))
79449
80082
  return {};
79450
- const raw = JSON.parse(readFileSync47(path4, "utf-8"));
80083
+ const raw = JSON.parse(readFileSync48(path4, "utf-8"));
79451
80084
  return typeof raw.deliveries === "object" && raw.deliveries !== null ? raw.deliveries : {};
79452
80085
  } catch {
79453
80086
  return {};
@@ -79547,7 +80180,7 @@ function writeThrottleIssue(agent, source, now, telegramDir, log) {
79547
80180
  async function handleWebhookIngest(args, deps = {}) {
79548
80181
  const log = deps.log ?? ((s) => process.stderr.write(s));
79549
80182
  const now = (deps.now ?? Date.now)();
79550
- const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join47(homedir25(), ".switchroom", "agents", a));
80183
+ const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join47(homedir26(), ".switchroom", "agents", a));
79551
80184
  const rateLimiter = deps.rateLimiter ?? defaultRateLimiter;
79552
80185
  const dedupStore = deps.dedupStore ?? createFileDedupStore(resolveAgentDir);
79553
80186
  if (!args.agentExists) {
@@ -79700,8 +80333,8 @@ async function handleWebhookIngest(args, deps = {}) {
79700
80333
  init_loader();
79701
80334
  import { createConnection as createConnection3 } from "node:net";
79702
80335
  import { createHash as createHash10 } from "node:crypto";
79703
- import { join as join48, resolve as resolve29 } from "node:path";
79704
- import { existsSync as existsSync53, readFileSync as readFileSync48 } from "node:fs";
80336
+ import { join as join48, resolve as resolve30 } from "node:path";
80337
+ import { existsSync as existsSync53, readFileSync as readFileSync49 } from "node:fs";
79705
80338
 
79706
80339
  // src/agent-scheduler/channel-target.ts
79707
80340
  init_merge();
@@ -79818,11 +80451,11 @@ function resolveAgentChat(config, sessionId, agentsDir) {
79818
80451
  const threadId = topicId !== undefined ? topicId === null ? undefined : parseInt(topicId, 10) || undefined : channel.threadId;
79819
80452
  return { chatId: channel.chatId, threadId };
79820
80453
  }
79821
- const accessPath = resolve29(agentsDir, agentName, "telegram", "access.json");
80454
+ const accessPath = resolve30(agentsDir, agentName, "telegram", "access.json");
79822
80455
  if (!existsSync53(accessPath))
79823
80456
  return null;
79824
80457
  try {
79825
- const raw = readFileSync48(accessPath, "utf-8");
80458
+ const raw = readFileSync49(accessPath, "utf-8");
79826
80459
  const access = JSON.parse(raw);
79827
80460
  const chatId = access.allowFrom?.[0];
79828
80461
  if (!chatId)
@@ -79833,7 +80466,7 @@ function resolveAgentChat(config, sessionId, agentsDir) {
79833
80466
  }
79834
80467
  }
79835
80468
  async function injectInbound(agentsDir, agentName, chatId, threadId, text, promptKey) {
79836
- const socketPath = resolve29(agentsDir, agentName, "telegram", "gateway.sock");
80469
+ const socketPath = resolve30(agentsDir, agentName, "telegram", "gateway.sock");
79837
80470
  if (!existsSync53(socketPath)) {
79838
80471
  return { ok: false, error: `agent ${agentName} gateway socket not found \u2014 is it running?` };
79839
80472
  }
@@ -79881,7 +80514,7 @@ async function injectInbound(agentsDir, agentName, chatId, threadId, text, promp
79881
80514
  });
79882
80515
  }
79883
80516
  function readHistoryDb(agentsDir, agentName, limit) {
79884
- const dbPath = resolve29(agentsDir, agentName, "telegram", "history.db");
80517
+ const dbPath = resolve30(agentsDir, agentName, "telegram", "history.db");
79885
80518
  if (!existsSync53(dbPath))
79886
80519
  return null;
79887
80520
  try {
@@ -79910,7 +80543,7 @@ function readHistoryDb(agentsDir, agentName, limit) {
79910
80543
  }
79911
80544
  }
79912
80545
  function startHistoryPoll(ctx, agentsDir, agentName, sessionId, pollIntervalMs = 2000) {
79913
- const dbPath = resolve29(agentsDir, agentName, "telegram", "history.db");
80546
+ const dbPath = resolve30(agentsDir, agentName, "telegram", "history.db");
79914
80547
  let SqliteDatabase = null;
79915
80548
  try {
79916
80549
  const meta = import.meta;
@@ -80028,15 +80661,14 @@ function turnsToMessages(turns) {
80028
80661
  }
80029
80662
  return messages;
80030
80663
  }
80031
- var SWITCHROOM_MODELS = [
80032
- "claude-sonnet-4-6",
80033
- "claude-opus-4-8",
80034
- "claude-haiku-4-5-20251001",
80035
- "claude-fable-5"
80036
- ];
80037
- function switchroomModelOptions() {
80664
+ var SWITCHROOM_MODELS = ["fable", "opus", "sonnet", "haiku"];
80665
+ function configuredModel(config, agentName) {
80666
+ const agentModel = agentName ? config.agents?.[agentName]?.model : undefined;
80667
+ return agentModel ?? config.defaults?.model ?? "sonnet";
80668
+ }
80669
+ function switchroomModelOptions(config, agentName) {
80038
80670
  return {
80039
- model: "claude-sonnet-4-6",
80671
+ model: configuredModel(config, agentName),
80040
80672
  provider: "switchroom",
80041
80673
  providers: [
80042
80674
  {
@@ -80164,7 +80796,7 @@ async function handleHermesRest(method, pathname, config, search = "") {
80164
80796
  return {
80165
80797
  status: 200,
80166
80798
  body: {
80167
- model: "claude-sonnet-4-6",
80799
+ model: configuredModel(config),
80168
80800
  provider: "switchroom",
80169
80801
  capabilities: {}
80170
80802
  }
@@ -80235,7 +80867,7 @@ async function handleHermesRest(method, pathname, config, search = "") {
80235
80867
  return { status: 200, body: { providers: [] } };
80236
80868
  }
80237
80869
  if (method === "GET" && pathname.startsWith("/api/model/options")) {
80238
- return { status: 200, body: switchroomModelOptions() };
80870
+ return { status: 200, body: switchroomModelOptions(config) };
80239
80871
  }
80240
80872
  if (method === "POST" && pathname === "/api/model/set") {
80241
80873
  return { status: 200, body: { ok: true } };
@@ -80467,11 +81099,15 @@ async function onHermesMessage(ctx, raw) {
80467
81099
  break;
80468
81100
  }
80469
81101
  case "model.options": {
80470
- sendResponse(ctx, rpcOk(id, switchroomModelOptions()));
81102
+ const sessionId = String(params.session_id ?? ctx.activeSessionId ?? "");
81103
+ const agentName = sessionId ? parseHermesSessionId(sessionId).agentName : undefined;
81104
+ sendResponse(ctx, rpcOk(id, switchroomModelOptions(config, agentName)));
80471
81105
  break;
80472
81106
  }
80473
81107
  case "model.info": {
80474
- sendResponse(ctx, rpcOk(id, { model: "claude-sonnet-4-6", provider: "switchroom" }));
81108
+ const sessionId = String(params.session_id ?? ctx.activeSessionId ?? "");
81109
+ const agentName = sessionId ? parseHermesSessionId(sessionId).agentName : undefined;
81110
+ sendResponse(ctx, rpcOk(id, { model: configuredModel(config, agentName), provider: "switchroom" }));
80475
81111
  break;
80476
81112
  }
80477
81113
  case "config.set": {
@@ -80577,7 +81213,8 @@ var SPA_TAB_ROUTES = new Set([
80577
81213
  "memory",
80578
81214
  "connections",
80579
81215
  "schedule",
80580
- "approvals"
81216
+ "approvals",
81217
+ "fleet-health"
80581
81218
  ]);
80582
81219
  function resolveDashboardFilePath(pathname) {
80583
81220
  const routeName = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
@@ -80607,10 +81244,10 @@ function resolveWebToken() {
80607
81244
  const fromEnv = process.env.SWITCHROOM_WEB_TOKEN;
80608
81245
  if (fromEnv && fromEnv.length > 0)
80609
81246
  return fromEnv;
80610
- const home2 = process.env.HOME ?? homedir26();
81247
+ const home2 = process.env.HOME ?? homedir27();
80611
81248
  const tokenPath = join49(home2, ".switchroom", "web-token");
80612
81249
  if (existsSync54(tokenPath)) {
80613
- const existing = readFileSync49(tokenPath, "utf8").trim();
81250
+ const existing = readFileSync50(tokenPath, "utf8").trim();
80614
81251
  if (existing.length > 0)
80615
81252
  return existing;
80616
81253
  }
@@ -80627,7 +81264,7 @@ function resolveWebToken() {
80627
81264
  return token;
80628
81265
  } catch (err) {
80629
81266
  if (err.code === "EEXIST") {
80630
- const existing = readFileSync49(tokenPath, "utf8").trim();
81267
+ const existing = readFileSync50(tokenPath, "utf8").trim();
80631
81268
  if (existing.length > 0)
80632
81269
  return existing;
80633
81270
  }
@@ -80700,11 +81337,11 @@ function checkWsAuth(req, token, server) {
80700
81337
  return presented !== null && constantTimeEqual(presented, token);
80701
81338
  }
80702
81339
  function loadWebhookSecrets() {
80703
- const path4 = join49(homedir26(), ".switchroom", "webhook-secrets.json");
81340
+ const path4 = join49(homedir27(), ".switchroom", "webhook-secrets.json");
80704
81341
  if (!existsSync54(path4))
80705
81342
  return {};
80706
81343
  try {
80707
- const parsed = JSON.parse(readFileSync49(path4, "utf-8"));
81344
+ const parsed = JSON.parse(readFileSync50(path4, "utf-8"));
80708
81345
  return parsed && typeof parsed === "object" ? parsed : {};
80709
81346
  } catch (err) {
80710
81347
  process.stderr.write(`webhook-ingest: failed to parse ${path4}: ${err.message} \u2014 webhooks will return 401 until fixed
@@ -80839,6 +81476,9 @@ function parseRoute(pathname, method) {
80839
81476
  if (method === "GET" && pathname === "/api/approvals") {
80840
81477
  return { handler: "getApprovals", params: {} };
80841
81478
  }
81479
+ if (method === "GET" && pathname === "/api/fleet-health") {
81480
+ return { handler: "getFleetHealth", params: {} };
81481
+ }
80842
81482
  if (method === "GET" && pathname === "/api/grants") {
80843
81483
  return { handler: "getGrants", params: {} };
80844
81484
  }
@@ -80882,7 +81522,7 @@ function parseRoute(pathname, method) {
80882
81522
  return null;
80883
81523
  }
80884
81524
  function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
80885
- const uiDirRaw = resolve30(import.meta.dirname, "ui");
81525
+ const uiDirRaw = resolve31(import.meta.dirname, "ui");
80886
81526
  const uiDir = existsSync54(uiDirRaw) ? realpathSync4(uiDirRaw) : uiDirRaw;
80887
81527
  const token = resolveWebToken();
80888
81528
  const freshConfig = () => {
@@ -81034,6 +81674,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
81034
81674
  return (async () => jsonResponse(withStamp(await cachedSchedule())))();
81035
81675
  case "getApprovals":
81036
81676
  return (async () => jsonResponse(withStamp(await cachedApprovals())))();
81677
+ case "getFleetHealth":
81678
+ return jsonResponse(handleGetFleetHealth());
81037
81679
  case "getGrants":
81038
81680
  return (async () => jsonResponse(withStamp(await cachedGrants())))();
81039
81681
  case "getAccounts":
@@ -81188,12 +81830,12 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
81188
81830
  return new Response("Not Found", { status: 404 });
81189
81831
  }
81190
81832
  const rel = relative(uiDir, realFullPath);
81191
- if (rel.startsWith("..") || resolve30(uiDir, rel) !== realFullPath) {
81833
+ if (rel.startsWith("..") || resolve31(uiDir, rel) !== realFullPath) {
81192
81834
  return new Response("Forbidden", { status: 403 });
81193
81835
  }
81194
81836
  const ext = extname(realFullPath);
81195
81837
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
81196
- const content = readFileSync49(realFullPath);
81838
+ const content = readFileSync50(realFullPath);
81197
81839
  const headers = { "Content-Type": contentType };
81198
81840
  const cacheControl = dashboardCacheControl(ext);
81199
81841
  if (cacheControl)
@@ -81368,8 +82010,8 @@ Starting Switchroom dashboard...
81368
82010
  init_source();
81369
82011
  init_loader();
81370
82012
  init_scaffold();
81371
- import { existsSync as existsSync55, copyFileSync as copyFileSync8, readFileSync as readFileSync50, writeFileSync as writeFileSync28, mkdirSync as mkdirSync31 } from "node:fs";
81372
- import { resolve as resolve31, dirname as dirname15 } from "node:path";
82013
+ import { existsSync as existsSync55, copyFileSync as copyFileSync8, readFileSync as readFileSync51, writeFileSync as writeFileSync28, mkdirSync as mkdirSync31 } from "node:fs";
82014
+ import { resolve as resolve32, dirname as dirname15 } from "node:path";
81373
82015
  init_state();
81374
82016
  init_vault();
81375
82017
  init_manager();
@@ -81650,14 +82292,14 @@ async function stepConfigFile(configPath, nonInteractive) {
81650
82292
  console.log(source_default.green(` ${STEP_DONE} Config loaded`) + source_default.gray(` (${Object.keys(config.agents).length} agents)`));
81651
82293
  const hasExplicitTimezone = config.switchroom?.timezone !== undefined || config.defaults?.timezone !== undefined;
81652
82294
  if (!hasExplicitTimezone) {
81653
- await writeDetectedTimezone(resolve31(existingConfig), nonInteractive);
82295
+ await writeDetectedTimezone(resolve32(existingConfig), nonInteractive);
81654
82296
  }
81655
- return { config, configPath: resolve31(existingConfig) };
82297
+ return { config, configPath: resolve32(existingConfig) };
81656
82298
  }
81657
82299
  return await copyExampleConfig(nonInteractive);
81658
82300
  }
81659
82301
  async function copyExampleConfig(nonInteractive) {
81660
- const examplesDir = resolve31(import.meta.dirname, "../../examples");
82302
+ const examplesDir = resolve32(import.meta.dirname, "../../examples");
81661
82303
  let choice;
81662
82304
  if (nonInteractive) {
81663
82305
  choice = "switchroom";
@@ -81668,7 +82310,7 @@ async function copyExampleConfig(nonInteractive) {
81668
82310
  ]);
81669
82311
  choice = choice.split(" ")[0];
81670
82312
  }
81671
- const srcFile = resolve31(examplesDir, `${choice}.yaml`);
82313
+ const srcFile = resolve32(examplesDir, `${choice}.yaml`);
81672
82314
  const destFile = resolvePath("~/.switchroom/switchroom.yaml");
81673
82315
  if (!existsSync55(srcFile)) {
81674
82316
  throw new ConfigError(`Example config not found: ${choice}.yaml`);
@@ -81680,12 +82322,12 @@ async function copyExampleConfig(nonInteractive) {
81680
82322
  await writeDetectedTimezone(destFile, nonInteractive);
81681
82323
  const config = loadConfig(destFile);
81682
82324
  console.log(source_default.green(` ${STEP_DONE} Config loaded`) + source_default.gray(` (${Object.keys(config.agents).length} agents)`));
81683
- return { config, configPath: resolve31(destFile) };
82325
+ return { config, configPath: resolve32(destFile) };
81684
82326
  }
81685
82327
  async function writeDetectedTimezone(destFile, nonInteractive, detect = detectServerTimezone, prompt = ask) {
81686
82328
  const detected = detect();
81687
82329
  if (detected !== undefined && detected !== "UTC" && isValidTimezone(detected)) {
81688
- const before2 = readFileSync50(destFile, "utf-8");
82330
+ const before2 = readFileSync51(destFile, "utf-8");
81689
82331
  const after2 = setSwitchroomTimezone(before2, detected);
81690
82332
  if (after2 !== before2)
81691
82333
  writeFileSync28(destFile, after2);
@@ -81706,7 +82348,7 @@ async function writeDetectedTimezone(destFile, nonInteractive, detect = detectSe
81706
82348
  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."));
81707
82349
  return;
81708
82350
  }
81709
- const before = readFileSync50(destFile, "utf-8");
82351
+ const before = readFileSync51(destFile, "utf-8");
81710
82352
  const after = setSwitchroomTimezone(before, zone);
81711
82353
  if (after !== before)
81712
82354
  writeFileSync28(destFile, after);
@@ -82258,9 +82900,9 @@ async function stepAutoUnlock(config, switchroomConfigPath, nonInteractive) {
82258
82900
  const choice = await askChoice(" Approval posture", [PASSPHRASE_CHOICE, TELEGRAM_ID_CHOICE]);
82259
82901
  if (choice === TELEGRAM_ID_CHOICE) {
82260
82902
  try {
82261
- const yamlPath = existsSync55(resolve31(process.cwd(), "switchroom.yaml")) ? resolve31(process.cwd(), "switchroom.yaml") : resolve31(process.cwd(), "switchroom.yml");
82903
+ const yamlPath = existsSync55(resolve32(process.cwd(), "switchroom.yaml")) ? resolve32(process.cwd(), "switchroom.yaml") : resolve32(process.cwd(), "switchroom.yml");
82262
82904
  if (existsSync55(yamlPath)) {
82263
- const content = readFileSync50(yamlPath, "utf-8");
82905
+ const content = readFileSync51(yamlPath, "utf-8");
82264
82906
  const result = insertVaultBrokerApprovalAuth(content, "telegram-id");
82265
82907
  if (result.kind === "rewritten") {
82266
82908
  writeFileSync28(yamlPath, result.content, "utf-8");
@@ -82290,12 +82932,12 @@ async function stepDangerousMode(config, nonInteractive) {
82290
82932
  }
82291
82933
  if (enableDangerous) {
82292
82934
  const configPaths = [
82293
- resolve31(process.cwd(), "switchroom.yaml"),
82294
- resolve31(process.cwd(), "switchroom.yml")
82935
+ resolve32(process.cwd(), "switchroom.yaml"),
82936
+ resolve32(process.cwd(), "switchroom.yml")
82295
82937
  ];
82296
82938
  for (const configPath of configPaths) {
82297
82939
  if (existsSync55(configPath)) {
82298
- let content = readFileSync50(configPath, "utf-8");
82940
+ let content = readFileSync51(configPath, "utf-8");
82299
82941
  const agentNames = Object.keys(config.agents);
82300
82942
  for (const name of agentNames) {
82301
82943
  const agentPattern = new RegExp(`(^ ${name}:\\s*\\n)`, "m");
@@ -82325,7 +82967,7 @@ async function stepOnboardingGuidance(config, nonInteractive) {
82325
82967
  const agentNames = Object.keys(config.agents);
82326
82968
  let allAuthenticated = true;
82327
82969
  for (const name of agentNames) {
82328
- const agentDir = resolve31(agentsDir, name);
82970
+ const agentDir = resolve32(agentsDir, name);
82329
82971
  const status = getAuthStatus(name, agentDir);
82330
82972
  if (status.authenticated) {
82331
82973
  console.log(` ${source_default.green("OK")} ${source_default.bold(name)}` + source_default.gray(` - authenticated (expires: ${status.timeUntilExpiry ?? "unknown"})`));
@@ -82425,10 +83067,10 @@ init_source();
82425
83067
  init_loader();
82426
83068
  init_lifecycle();
82427
83069
  init_compose_env();
82428
- import { cpSync as cpSync2, existsSync as existsSync62, mkdirSync as mkdirSync33, readFileSync as readFileSync55, realpathSync as realpathSync6, rmSync as rmSync12, statSync as statSync27, chownSync as chownSync5 } from "node:fs";
83070
+ import { cpSync as cpSync2, existsSync as existsSync62, mkdirSync as mkdirSync33, readFileSync as readFileSync56, realpathSync as realpathSync6, rmSync as rmSync12, statSync as statSync27, chownSync as chownSync5 } from "node:fs";
82429
83071
  import { spawnSync as spawnSync12 } from "node:child_process";
82430
- import { join as join63, dirname as dirname18, resolve as resolve36 } from "node:path";
82431
- import { homedir as homedir37 } from "node:os";
83072
+ import { join as join63, dirname as dirname18, resolve as resolve37 } from "node:path";
83073
+ import { homedir as homedir38 } from "node:os";
82432
83074
 
82433
83075
  // src/cli/release-yaml.ts
82434
83076
  var import_yaml19 = __toESM(require_dist(), 1);
@@ -82520,7 +83162,7 @@ ${lines.join(`
82520
83162
  function defaultPersistPin(configPath) {
82521
83163
  return (pin) => {
82522
83164
  const path4 = configPath ?? findConfigFile();
82523
- const before = readFileSync55(path4, "utf8");
83165
+ const before = readFileSync56(path4, "utf8");
82524
83166
  const after = setReleasePinInConfig(before, pin);
82525
83167
  if (after === before)
82526
83168
  return;
@@ -82534,13 +83176,13 @@ function defaultPersistPin(configPath) {
82534
83176
  } catch {}
82535
83177
  };
82536
83178
  }
82537
- var DEFAULT_COMPOSE_PATH = join63(homedir37(), ".switchroom", "compose", "docker-compose.yml");
83179
+ var DEFAULT_COMPOSE_PATH = join63(homedir38(), ".switchroom", "compose", "docker-compose.yml");
82538
83180
  function runningFromSwitchroomCheckout(scriptPath) {
82539
83181
  let dir = dirname18(scriptPath);
82540
83182
  for (let i = 0;i < 12; i++) {
82541
83183
  if (existsSync62(join63(dir, ".git"))) {
82542
83184
  try {
82543
- const pkg = JSON.parse(readFileSync55(join63(dir, "package.json"), "utf-8"));
83185
+ const pkg = JSON.parse(readFileSync56(join63(dir, "package.json"), "utf-8"));
82544
83186
  if (pkg.name === "switchroom")
82545
83187
  return true;
82546
83188
  } catch {}
@@ -82730,8 +83372,8 @@ function planUpdate(opts) {
82730
83372
  opts.syncBundledSkillsFn();
82731
83373
  return;
82732
83374
  }
82733
- const source = resolve36(import.meta.dirname, "../../skills");
82734
- const dest = join63(homedir37(), ".switchroom", "skills", "_bundled");
83375
+ const source = resolve37(import.meta.dirname, "../../skills");
83376
+ const dest = join63(homedir38(), ".switchroom", "skills", "_bundled");
82735
83377
  if (!existsSync62(source)) {
82736
83378
  process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
82737
83379
  `);
@@ -82777,7 +83419,7 @@ function planUpdate(opts) {
82777
83419
  description: "docker compose up -d --remove-orphans (recreates services with new images / compose)",
82778
83420
  run: () => {
82779
83421
  try {
82780
- const composeText = readFileSync55(composePath, "utf8");
83422
+ const composeText = readFileSync56(composePath, "utf8");
82781
83423
  const pf = validateBindSources(composeText);
82782
83424
  if (!pf.ok)
82783
83425
  throw new Error(formatPreflightError(pf));
@@ -82859,7 +83501,7 @@ function defaultStatusProbe(composePath) {
82859
83501
  const pkgPath = join63(dir, "package.json");
82860
83502
  if (existsSync62(pkgPath)) {
82861
83503
  try {
82862
- const pkg = JSON.parse(readFileSync55(pkgPath, "utf-8"));
83504
+ const pkg = JSON.parse(readFileSync56(pkgPath, "utf-8"));
82863
83505
  if (typeof pkg.version === "string")
82864
83506
  cliVersion = pkg.version;
82865
83507
  } catch (err) {
@@ -83076,8 +83718,8 @@ function registerUpdateCommand(program3) {
83076
83718
  // src/cli/rollout.ts
83077
83719
  init_helpers();
83078
83720
  import { spawnSync as spawnSync13 } from "node:child_process";
83079
- import { readFileSync as readFileSync56, chownSync as chownSync6, statSync as statSync28 } from "node:fs";
83080
- import { homedir as homedir38 } from "node:os";
83721
+ import { readFileSync as readFileSync57, chownSync as chownSync6, statSync as statSync28 } from "node:fs";
83722
+ import { homedir as homedir39 } from "node:os";
83081
83723
  init_operator_uid();
83082
83724
  init_atomic();
83083
83725
  init_audit_reader();
@@ -83121,6 +83763,7 @@ function planRollout(agents, opts = {}) {
83121
83763
  if (!opts.skipWeb) {
83122
83764
  steps.push({ kind: "refresh-web" });
83123
83765
  steps.push({ kind: "refresh-hostd" });
83766
+ steps.push({ kind: "refresh-hindsight" });
83124
83767
  }
83125
83768
  steps.push({ kind: "sweep" });
83126
83769
  return steps;
@@ -83146,6 +83789,9 @@ function formatRolloutPlan(steps, target) {
83146
83789
  case "refresh-hostd":
83147
83790
  lines.push(` ${n}. hostd install --tag ${target} (separate compose project)`);
83148
83791
  break;
83792
+ case "refresh-hindsight":
83793
+ lines.push(` ${n}. refresh hindsight \u2014 pull ${target} + recreate (standalone docker run, not compose)`);
83794
+ break;
83149
83795
  case "sweep":
83150
83796
  lines.push(` ${n}. sweep \u2014 print per-agent version table`);
83151
83797
  break;
@@ -83219,11 +83865,14 @@ function executeRollout(steps, target, deps, execOpts = {}) {
83219
83865
  case "apply": {
83220
83866
  deps.log(`ROLL_STEP apply \u2014 regenerating compose for ${target}`);
83221
83867
  emit({ phase: "apply", target });
83222
- const applyArgs = execOpts.hostdContext ? ["apply", "--pin", target] : ["apply"];
83868
+ const applyArgs = execOpts.hostdContext ? ["apply", "--pin", target, "--compose-only", "--non-interactive"] : ["apply"];
83223
83869
  const r = deps.run(applyArgs);
83224
83870
  if (r.status !== 0) {
83225
83871
  return { ok: false, rolled, failedStep: "apply", warnings };
83226
83872
  }
83873
+ if (execOpts.hostdContext) {
83874
+ warnings.push(`apply ran --compose-only (hostd is unprivileged and cannot ` + `write per-agent state dirs). Compose + compose-level env/` + `healthcheck are up to date, but per-agent template changes ` + `(start.sh / .mcp.json / settings.json) are NOT applied \u2014 run ` + `a host-side \`sudo switchroom apply\` if this release changed them.`);
83875
+ }
83227
83876
  break;
83228
83877
  }
83229
83878
  case "restart-agent": {
@@ -83265,6 +83914,25 @@ function executeRollout(steps, target, deps, execOpts = {}) {
83265
83914
  warnings.push(`hostd refresh failed (non-fatal); agents already rolled`);
83266
83915
  break;
83267
83916
  }
83917
+ case "refresh-hindsight": {
83918
+ const hindsightExists = deps.hindsightExists ?? isHindsightContainerExists;
83919
+ if (!hindsightExists()) {
83920
+ deps.log(`ROLL_STEP refresh-hindsight \u2014 no hindsight container on this host; skipping`);
83921
+ break;
83922
+ }
83923
+ deps.log(`ROLL_STEP refresh-hindsight \u2014 memory setup --recreate --tag ${target} (pull ${target} + recreate)`);
83924
+ const r = deps.run(["memory", "setup", "--recreate", "--tag", target]);
83925
+ if (r.status !== 0) {
83926
+ deps.log(` \u2717 hindsight recreate FAILED \u2014 the memory backend is DOWN (the ` + `recreate stopped + removed the old container before the failed ` + `start). Run \`switchroom memory setup\` to bring it back. \u2014 STOPPING`);
83927
+ return {
83928
+ ok: false,
83929
+ rolled,
83930
+ failedStep: "refresh-hindsight",
83931
+ warnings
83932
+ };
83933
+ }
83934
+ break;
83935
+ }
83268
83936
  case "sweep": {
83269
83937
  deps.log(`ROLL_STEP sweep`);
83270
83938
  for (const a of rolled) {
@@ -83278,10 +83946,10 @@ function executeRollout(steps, target, deps, execOpts = {}) {
83278
83946
  return { ok: true, rolled, warnings };
83279
83947
  }
83280
83948
  function resolveRollbackTarget(auditLogPath) {
83281
- const logPath = auditLogPath ?? defaultAuditLogPath2(homedir38());
83949
+ const logPath = auditLogPath ?? defaultAuditLogPath2(homedir39());
83282
83950
  let raw;
83283
83951
  try {
83284
- raw = readFileSync56(logPath, "utf8");
83952
+ raw = readFileSync57(logPath, "utf8");
83285
83953
  } catch {
83286
83954
  return null;
83287
83955
  }
@@ -83296,7 +83964,7 @@ function resolveRollbackTarget(auditLogPath) {
83296
83964
  }
83297
83965
  function registerRolloutCommand(program3) {
83298
83966
  const hostdCtx = isHostdContext2();
83299
- program3.command("rollout").description("Deploy a pinned version to the fleet, safely (staggered restart + " + "per-agent version assert + web/hostd refresh)." + (hostdCtx ? "" : " Run with sudo.")).option("--pin <version>", "Version to roll (e.g. v0.15.18). Defaults to release.pin from config.").option("--agents <list>", "Comma-separated subset of agents to roll (default: all configured).").option("--skip-web", "Skip the web + hostd refresh step.").option("--allow-downgrade", "Permit rolling to an older semver tag (operator-approved rollback path). " + "When set, the downgrade guard is relaxed so --pin may be older than the " + "current release.pin. All other safety rails apply unchanged.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
83967
+ program3.command("rollout").description("Deploy a pinned version to the fleet, safely (staggered restart + " + "per-agent version assert + web/hostd refresh)." + (hostdCtx ? "" : " Run with sudo.")).option("--pin <version>", "Version to roll (e.g. v0.15.18). Defaults to release.pin from config.").option("--agents <list>", "Comma-separated subset of agents to roll (default: all configured).").option("--skip-web", "Skip the web + hostd + hindsight refresh step.").option("--allow-downgrade", "Permit rolling to an older semver tag (operator-approved rollback path). " + "When set, the downgrade guard is relaxed so --pin may be older than the " + "current release.pin. All other safety rails apply unchanged.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
83300
83968
  const config = getConfig(program3);
83301
83969
  let resolvedPin = opts.pin;
83302
83970
  if (opts.allowDowngrade && !resolvedPin) {
@@ -83395,7 +84063,7 @@ function registerRolloutCommand(program3) {
83395
84063
  emitPhase: (phase) => process.stdout.write(encodeRolloutPhaseLine(phase) + `
83396
84064
  `),
83397
84065
  persistPin: (pin) => {
83398
- const before = readFileSync56(configPath, "utf8");
84066
+ const before = readFileSync57(configPath, "utf8");
83399
84067
  const after = setReleasePinInConfig(before, pin);
83400
84068
  if (after === before)
83401
84069
  return;
@@ -83443,7 +84111,7 @@ function registerRolloutCommand(program3) {
83443
84111
  });
83444
84112
  }
83445
84113
  function registerRollbackCommand(program3) {
83446
- program3.command("rollback").description("Roll the fleet back to the previous version (operator-approved downgrade). " + "Defaults to the version captured in the last completed rollout's prior_pin. " + "Equivalent to `rollout --allow-downgrade [--pin <version>]`.").option("--to <version>", "Explicit version to roll back to (e.g. v0.15.17). " + "Omit to use the last completed rollout's prior_pin.").option("--agents <list>", "Comma-separated subset of agents to roll back (default: all configured).").option("--skip-web", "Skip the web + hostd refresh step.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
84114
+ program3.command("rollback").description("Roll the fleet back to the previous version (operator-approved downgrade). " + "Defaults to the version captured in the last completed rollout's prior_pin. " + "Equivalent to `rollout --allow-downgrade [--pin <version>]`.").option("--to <version>", "Explicit version to roll back to (e.g. v0.15.17). " + "Omit to use the last completed rollout's prior_pin.").option("--agents <list>", "Comma-separated subset of agents to roll back (default: all configured).").option("--skip-web", "Skip the web + hostd + hindsight refresh step.").option("--dry-run", "Print the plan and exit without changing anything.").action(async (opts) => {
83447
84115
  const rolloutArgs = ["--allow-downgrade"];
83448
84116
  if (opts.to)
83449
84117
  rolloutArgs.push("--pin", opts.to);
@@ -83462,14 +84130,14 @@ init_source();
83462
84130
  init_helpers();
83463
84131
  init_loader();
83464
84132
  init_lifecycle();
83465
- import { resolve as resolve37 } from "node:path";
84133
+ import { resolve as resolve38 } from "node:path";
83466
84134
 
83467
84135
  // src/cli/version.ts
83468
84136
  init_source();
83469
84137
  init_helpers();
83470
84138
  init_lifecycle();
83471
84139
  import { execSync as execSync4 } from "node:child_process";
83472
- import { existsSync as existsSync63, readFileSync as readFileSync57 } from "node:fs";
84140
+ import { existsSync as existsSync63, readFileSync as readFileSync58 } from "node:fs";
83473
84141
  import { dirname as dirname19, join as join64 } from "node:path";
83474
84142
  function getClaudeCodeVersion() {
83475
84143
  try {
@@ -83523,7 +84191,7 @@ function locateSwitchroomInstallDir() {
83523
84191
  const pkgPath = join64(dir, "package.json");
83524
84192
  if (existsSync63(pkgPath)) {
83525
84193
  try {
83526
- const pkg = JSON.parse(readFileSync57(pkgPath, "utf-8"));
84194
+ const pkg = JSON.parse(readFileSync58(pkgPath, "utf-8"));
83527
84195
  if (pkg.name === "switchroom" && existsSync63(join64(dir, ".git"))) {
83528
84196
  return dir;
83529
84197
  }
@@ -83617,7 +84285,7 @@ function registerRestartCommand(program3) {
83617
84285
  }
83618
84286
  const didRestart = res.restarted || !graceful;
83619
84287
  if (didRestart) {
83620
- const agentDir = resolve37(agentsDir, name);
84288
+ const agentDir = resolve38(agentsDir, name);
83621
84289
  const converged = waitForAuthConverge(name, agentDir);
83622
84290
  if (!converged) {
83623
84291
  console.log(source_default.yellow(` ${name}: agent is up but auth status didn't converge in 30s \u2014 check logs`));
@@ -83698,7 +84366,7 @@ Dependency manifest`));
83698
84366
  // src/cli/handoff.ts
83699
84367
  init_helpers();
83700
84368
  init_loader();
83701
- import { resolve as resolve38 } from "node:path";
84369
+ import { resolve as resolve39 } from "node:path";
83702
84370
  function registerHandoffCommand(program3) {
83703
84371
  program3.command("handoff <agent>", { hidden: true }).description("Build the agent's session handoff sidecars \u2014 a transcript-tail " + "briefing (.handoff.md) and topic line (.handoff-topic). " + "[internal \u2014 used by the Stop hook]").option("--max-turns <n>", "Max turns kept in the handoff transcript tail", String(DEFAULT_MAX_TURNS)).action(withConfigError(async (agentName, opts) => {
83704
84372
  let agentConfig;
@@ -83712,7 +84380,7 @@ function registerHandoffCommand(program3) {
83712
84380
  return;
83713
84381
  }
83714
84382
  const agentsDir = resolveAgentsDir(config);
83715
- agentDir = resolve38(agentsDir, agentName);
84383
+ agentDir = resolve39(agentsDir, agentName);
83716
84384
  } catch (err) {
83717
84385
  if (!(err instanceof ConfigError))
83718
84386
  throw err;
@@ -83727,7 +84395,7 @@ function registerHandoffCommand(program3) {
83727
84395
  `);
83728
84396
  return;
83729
84397
  }
83730
- const claudeConfigDir = resolve38(agentDir, ".claude");
84398
+ const claudeConfigDir = resolve39(agentDir, ".claude");
83731
84399
  const jsonl = findLatestSessionJsonl(claudeConfigDir);
83732
84400
  if (!jsonl) {
83733
84401
  process.stderr.write(`handoff: no session JSONL under ${claudeConfigDir}/projects; skipping
@@ -83754,7 +84422,7 @@ import {
83754
84422
  mkdirSync as mkdirSync34,
83755
84423
  openSync as openSync11,
83756
84424
  readdirSync as readdirSync22,
83757
- readFileSync as readFileSync58,
84425
+ readFileSync as readFileSync59,
83758
84426
  renameSync as renameSync12,
83759
84427
  statSync as statSync29,
83760
84428
  unlinkSync as unlinkSync11,
@@ -84164,7 +84832,7 @@ function readAll(stateDir) {
84164
84832
  return [];
84165
84833
  let raw;
84166
84834
  try {
84167
- raw = readFileSync58(path4, "utf-8");
84835
+ raw = readFileSync59(path4, "utf-8");
84168
84836
  } catch {
84169
84837
  return [];
84170
84838
  }
@@ -84236,7 +84904,7 @@ function record(stateDir, input, nowFn = Date.now) {
84236
84904
  return result;
84237
84905
  });
84238
84906
  }
84239
- function resolve39(stateDir, fingerprint, nowFn = Date.now) {
84907
+ function resolve40(stateDir, fingerprint, nowFn = Date.now) {
84240
84908
  if (!existsSync64(join65(stateDir, ISSUES_FILE)))
84241
84909
  return 0;
84242
84910
  return withLock(stateDir, () => {
@@ -84375,7 +85043,7 @@ function withLock(stateDir, fn) {
84375
85043
  function tryStealStaleLock(lockPath) {
84376
85044
  let pidStr;
84377
85045
  try {
84378
- pidStr = readFileSync58(lockPath, "utf-8").trim();
85046
+ pidStr = readFileSync59(lockPath, "utf-8").trim();
84379
85047
  } catch {
84380
85048
  return true;
84381
85049
  }
@@ -84524,11 +85192,11 @@ function registerIssuesCommand(program3) {
84524
85192
  const stateDir = resolveStateDir2(opts);
84525
85193
  let flipped;
84526
85194
  if (opts.source && opts.code) {
84527
- flipped = resolve39(stateDir, computeFingerprint(opts.source, opts.code));
85195
+ flipped = resolve40(stateDir, computeFingerprint(opts.source, opts.code));
84528
85196
  } else if (opts.source && !fingerprint) {
84529
85197
  flipped = resolveAllBySource(stateDir, opts.source);
84530
85198
  } else if (fingerprint) {
84531
- flipped = resolve39(stateDir, fingerprint);
85199
+ flipped = resolve40(stateDir, fingerprint);
84532
85200
  } else {
84533
85201
  process.stderr.write(`issues resolve: need either <fingerprint>, --source, or --source + --code
84534
85202
  `);
@@ -84624,20 +85292,20 @@ function relTime(deltaMs) {
84624
85292
  // src/cli/deps.ts
84625
85293
  init_source();
84626
85294
  import { existsSync as existsSync67 } from "node:fs";
84627
- import { homedir as homedir41 } from "node:os";
84628
- import { join as join68, resolve as resolve40 } from "node:path";
85295
+ import { homedir as homedir42 } from "node:os";
85296
+ import { join as join68, resolve as resolve41 } from "node:path";
84629
85297
 
84630
85298
  // src/deps/python.ts
84631
85299
  import { createHash as createHash12 } from "node:crypto";
84632
85300
  import {
84633
85301
  existsSync as existsSync65,
84634
85302
  mkdirSync as mkdirSync35,
84635
- readFileSync as readFileSync59,
85303
+ readFileSync as readFileSync60,
84636
85304
  rmSync as rmSync13,
84637
85305
  writeFileSync as writeFileSync30
84638
85306
  } from "node:fs";
84639
85307
  import { dirname as dirname20, join as join66 } from "node:path";
84640
- import { homedir as homedir39 } from "node:os";
85308
+ import { homedir as homedir40 } from "node:os";
84641
85309
  import { execFileSync as execFileSync19 } from "node:child_process";
84642
85310
 
84643
85311
  class PythonEnvError extends Error {
@@ -84649,10 +85317,10 @@ class PythonEnvError extends Error {
84649
85317
  }
84650
85318
  }
84651
85319
  function defaultPythonCacheRoot() {
84652
- return join66(homedir39(), ".switchroom", "deps", "python");
85320
+ return join66(homedir40(), ".switchroom", "deps", "python");
84653
85321
  }
84654
85322
  function hashFile(path4) {
84655
- return createHash12("sha256").update(readFileSync59(path4)).digest("hex");
85323
+ return createHash12("sha256").update(readFileSync60(path4)).digest("hex");
84656
85324
  }
84657
85325
  function ensurePythonEnv(opts) {
84658
85326
  const { skillName, requirementsPath, force = false } = opts;
@@ -84668,7 +85336,7 @@ function ensurePythonEnv(opts) {
84668
85336
  const pipBin = join66(binDir, "pip");
84669
85337
  const targetHash = hashFile(requirementsPath);
84670
85338
  if (!force && existsSync65(stampPath) && existsSync65(pythonBin)) {
84671
- const existingHash = readFileSync59(stampPath, "utf8").trim();
85339
+ const existingHash = readFileSync60(stampPath, "utf8").trim();
84672
85340
  if (existingHash === targetHash) {
84673
85341
  return {
84674
85342
  skillName,
@@ -84720,12 +85388,12 @@ import {
84720
85388
  copyFileSync as copyFileSync9,
84721
85389
  existsSync as existsSync66,
84722
85390
  mkdirSync as mkdirSync36,
84723
- readFileSync as readFileSync60,
85391
+ readFileSync as readFileSync61,
84724
85392
  rmSync as rmSync14,
84725
85393
  writeFileSync as writeFileSync31
84726
85394
  } from "node:fs";
84727
85395
  import { dirname as dirname21, join as join67 } from "node:path";
84728
- import { homedir as homedir40 } from "node:os";
85396
+ import { homedir as homedir41 } from "node:os";
84729
85397
  import { execFileSync as execFileSync20 } from "node:child_process";
84730
85398
 
84731
85399
  class NodeEnvError extends Error {
@@ -84748,14 +85416,14 @@ var LOCKFILES_FOR = {
84748
85416
  npm: ["package-lock.json"]
84749
85417
  };
84750
85418
  function defaultNodeCacheRoot() {
84751
- return join67(homedir40(), ".switchroom", "deps", "node");
85419
+ return join67(homedir41(), ".switchroom", "deps", "node");
84752
85420
  }
84753
85421
  function hashDepInputs(packageJsonPath) {
84754
85422
  const sourceDir = dirname21(packageJsonPath);
84755
85423
  const hasher = createHash13("sha256");
84756
85424
  hasher.update(`package.json
84757
85425
  `);
84758
- hasher.update(readFileSync60(packageJsonPath));
85426
+ hasher.update(readFileSync61(packageJsonPath));
84759
85427
  for (const lockName of ALL_LOCKFILES) {
84760
85428
  const lockPath = join67(sourceDir, lockName);
84761
85429
  if (existsSync66(lockPath)) {
@@ -84764,7 +85432,7 @@ function hashDepInputs(packageJsonPath) {
84764
85432
  hasher.update(lockName);
84765
85433
  hasher.update(`
84766
85434
  `);
84767
- hasher.update(readFileSync60(lockPath));
85435
+ hasher.update(readFileSync61(lockPath));
84768
85436
  }
84769
85437
  }
84770
85438
  return hasher.digest("hex");
@@ -84783,7 +85451,7 @@ function ensureNodeEnv(opts) {
84783
85451
  const binDir = join67(nodeModulesDir, ".bin");
84784
85452
  const targetHash = hashDepInputs(packageJsonPath);
84785
85453
  if (!force && existsSync66(stampPath) && existsSync66(nodeModulesDir)) {
84786
- const existingHash = readFileSync60(stampPath, "utf8").trim();
85454
+ const existingHash = readFileSync61(stampPath, "utf8").trim();
84787
85455
  if (existingHash === targetHash) {
84788
85456
  return {
84789
85457
  skillName,
@@ -84832,7 +85500,7 @@ function ensureNodeEnv(opts) {
84832
85500
 
84833
85501
  // src/cli/deps.ts
84834
85502
  function builtinSkillsRoot() {
84835
- return resolve40(homedir41(), ".switchroom/skills/_bundled");
85503
+ return resolve41(homedir42(), ".switchroom/skills/_bundled");
84836
85504
  }
84837
85505
  function registerDepsCommand(program3) {
84838
85506
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
@@ -84911,7 +85579,7 @@ function registerDepsCommand(program3) {
84911
85579
  init_helpers();
84912
85580
  init_loader();
84913
85581
  import { existsSync as existsSync68 } from "node:fs";
84914
- import { resolve as resolve41, sep as sep3 } from "node:path";
85582
+ import { resolve as resolve42, sep as sep3 } from "node:path";
84915
85583
  import { spawnSync as spawnSync14 } from "node:child_process";
84916
85584
 
84917
85585
  // src/agents/workspace.ts
@@ -85617,8 +86285,8 @@ function registerWorkspaceCommand(program3) {
85617
86285
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
85618
86286
  if (!dir)
85619
86287
  return;
85620
- const resolvedWorkspace = resolve41(dir);
85621
- const target = resolve41(resolvedWorkspace, file ?? "AGENTS.md");
86288
+ const resolvedWorkspace = resolve42(dir);
86289
+ const target = resolve42(resolvedWorkspace, file ?? "AGENTS.md");
85622
86290
  if (!isInsideWorkspace(resolvedWorkspace, target)) {
85623
86291
  process.stderr.write(`workspace edit: refusing path traversal outside workspace dir (${target})
85624
86292
  `);
@@ -85686,7 +86354,7 @@ function registerWorkspaceCommand(program3) {
85686
86354
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
85687
86355
  if (!dir)
85688
86356
  return;
85689
- const gitDir = resolve41(dir, ".git");
86357
+ const gitDir = resolve42(dir, ".git");
85690
86358
  if (!existsSync68(gitDir)) {
85691
86359
  process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
85692
86360
  `);
@@ -85740,7 +86408,7 @@ function registerWorkspaceCommand(program3) {
85740
86408
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
85741
86409
  if (!dir)
85742
86410
  return;
85743
- const gitDir = resolve41(dir, ".git");
86411
+ const gitDir = resolve42(dir, ".git");
85744
86412
  if (!existsSync68(gitDir)) {
85745
86413
  process.stdout.write(`Workspace is not a git repository.
85746
86414
  `);
@@ -85764,7 +86432,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
85764
86432
  return;
85765
86433
  }
85766
86434
  const agentsDir = resolveAgentsDir(config);
85767
- const agentDir = resolve41(agentsDir, agentName);
86435
+ const agentDir = resolve42(agentsDir, agentName);
85768
86436
  const dir = resolveAgentWorkspaceDir(agentDir);
85769
86437
  if (!existsSync68(dir)) {
85770
86438
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
@@ -85802,8 +86470,8 @@ function safeParseInt(value, fallback) {
85802
86470
  init_helpers();
85803
86471
  init_loader();
85804
86472
  init_merge();
85805
- import { copyFileSync as copyFileSync10, existsSync as existsSync69, readFileSync as readFileSync61, writeFileSync as writeFileSync32 } from "node:fs";
85806
- import { join as join69, resolve as resolve42 } from "node:path";
86473
+ import { copyFileSync as copyFileSync10, existsSync as existsSync69, readFileSync as readFileSync62, writeFileSync as writeFileSync32 } from "node:fs";
86474
+ import { join as join69, resolve as resolve43 } from "node:path";
85807
86475
  init_scaffold();
85808
86476
  init_profiles();
85809
86477
  init_schema();
@@ -85818,7 +86486,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
85818
86486
  const profileName = merged.extends ?? DEFAULT_PROFILE;
85819
86487
  const profilePath = getProfilePath(profileName);
85820
86488
  const agentsDir = resolveAgentsDir(config);
85821
- const agentDir = resolve42(agentsDir, agentName);
86489
+ const agentDir = resolve43(agentsDir, agentName);
85822
86490
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
85823
86491
  if (!existsSync69(workspaceDir)) {
85824
86492
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
@@ -85850,7 +86518,7 @@ function registerSoulCommand(program3) {
85850
86518
  console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
85851
86519
  process.exit(1);
85852
86520
  }
85853
- process.stdout.write(readFileSync61(t.soulPath, "utf-8"));
86521
+ process.stdout.write(readFileSync62(t.soulPath, "utf-8"));
85854
86522
  }));
85855
86523
  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) => {
85856
86524
  const t = resolveSoulTargetOrExit(program3, agentName);
@@ -85895,8 +86563,8 @@ function registerSoulCommand(program3) {
85895
86563
  // src/cli/debug.ts
85896
86564
  init_helpers();
85897
86565
  init_loader();
85898
- import { existsSync as existsSync70, readFileSync as readFileSync62, readdirSync as readdirSync23, statSync as statSync30 } from "node:fs";
85899
- import { resolve as resolve43, join as join70 } from "node:path";
86566
+ import { existsSync as existsSync70, readFileSync as readFileSync63, readdirSync as readdirSync23, statSync as statSync30 } from "node:fs";
86567
+ import { resolve as resolve44, join as join70 } from "node:path";
85900
86568
  import { createHash as createHash14 } from "node:crypto";
85901
86569
  init_merge();
85902
86570
  init_hindsight();
@@ -85911,7 +86579,7 @@ function readMcpServerNames(agentDir) {
85911
86579
  if (!existsSync70(mcpPath))
85912
86580
  return [];
85913
86581
  try {
85914
- const parsed = JSON.parse(readFileSync62(mcpPath, "utf-8"));
86582
+ const parsed = JSON.parse(readFileSync63(mcpPath, "utf-8"));
85915
86583
  return Object.keys(parsed.mcpServers ?? {});
85916
86584
  } catch {
85917
86585
  return null;
@@ -85946,7 +86614,7 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
85946
86614
  }
85947
86615
  function extractLatestUserMessage(transcriptPath) {
85948
86616
  try {
85949
- const content = readFileSync62(transcriptPath, "utf-8");
86617
+ const content = readFileSync63(transcriptPath, "utf-8");
85950
86618
  const lines = content.trim().split(`
85951
86619
  `).filter(Boolean);
85952
86620
  for (let i = lines.length - 1;i >= 0; i--) {
@@ -85994,7 +86662,7 @@ function registerDebugCommand(program3) {
85994
86662
  process.exit(1);
85995
86663
  }
85996
86664
  const agentsDir = resolveAgentsDir(config);
85997
- const agentDir = resolve43(agentsDir, agentName);
86665
+ const agentDir = resolve44(agentsDir, agentName);
85998
86666
  if (!existsSync70(agentDir)) {
85999
86667
  console.error(`Agent directory not found: ${agentDir}`);
86000
86668
  process.exit(1);
@@ -86050,7 +86718,7 @@ function registerDebugCommand(program3) {
86050
86718
  }
86051
86719
  console.log(`=== Append System Prompt (per-session) ===
86052
86720
  `);
86053
- const handoffContent = existsSync70(handoffPath) ? readFileSync62(handoffPath, "utf-8") : "";
86721
+ const handoffContent = existsSync70(handoffPath) ? readFileSync63(handoffPath, "utf-8") : "";
86054
86722
  if (handoffContent.trim().length > 0) {
86055
86723
  console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
86056
86724
  console.log(handoffContent);
@@ -86061,7 +86729,7 @@ function registerDebugCommand(program3) {
86061
86729
  }
86062
86730
  console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
86063
86731
  `);
86064
- const claudeMdContent = existsSync70(claudeMdPath) ? readFileSync62(claudeMdPath, "utf-8") : "";
86732
+ const claudeMdContent = existsSync70(claudeMdPath) ? readFileSync63(claudeMdPath, "utf-8") : "";
86065
86733
  if (claudeMdContent.trim().length > 0) {
86066
86734
  console.log(`(${formatBytes(claudeMdContent.length)})`);
86067
86735
  console.log(claudeMdContent);
@@ -86072,7 +86740,7 @@ function registerDebugCommand(program3) {
86072
86740
  }
86073
86741
  console.log(`=== Persona (SOUL.md) ===
86074
86742
  `);
86075
- const soulMdContent = existsSync70(soulMdPath) ? readFileSync62(soulMdPath, "utf-8") : existsSync70(workspaceSoulMdPath) ? readFileSync62(workspaceSoulMdPath, "utf-8") : "";
86743
+ const soulMdContent = existsSync70(soulMdPath) ? readFileSync63(soulMdPath, "utf-8") : existsSync70(workspaceSoulMdPath) ? readFileSync63(workspaceSoulMdPath, "utf-8") : "";
86076
86744
  if (soulMdContent.trim().length > 0) {
86077
86745
  console.log(`(${formatBytes(soulMdContent.length)})`);
86078
86746
  console.log(soulMdContent);
@@ -86136,8 +86804,8 @@ function registerDebugCommand(program3) {
86136
86804
  const fleetDir = join70(agentsDir, "..", "fleet");
86137
86805
  const fleetInvPath = join70(fleetDir, "switchroom-invariants.md");
86138
86806
  const fleetClaudePath = join70(fleetDir, "CLAUDE.md");
86139
- const fleetInvBytes = existsSync70(fleetInvPath) ? readFileSync62(fleetInvPath, "utf-8").length : 0;
86140
- const fleetClaudeBytes = existsSync70(fleetClaudePath) ? readFileSync62(fleetClaudePath, "utf-8").length : 0;
86807
+ const fleetInvBytes = existsSync70(fleetInvPath) ? readFileSync63(fleetInvPath, "utf-8").length : 0;
86808
+ const fleetClaudeBytes = existsSync70(fleetClaudePath) ? readFileSync63(fleetClaudePath, "utf-8").length : 0;
86141
86809
  const fleetBytes = fleetInvBytes + fleetClaudeBytes;
86142
86810
  const totalBytes = stableBytes + perSessionBytes + claudeMdBytes + fleetBytes + perTurnBytes + userBytes;
86143
86811
  console.log(`Stable prefix: ${formatBytes(stableBytes).padEnd(20)} (cache-hot; includes SOUL.md ${soulMdBytes.toLocaleString()}B)`);
@@ -86171,24 +86839,24 @@ init_source();
86171
86839
  // src/worktree/claim.ts
86172
86840
  import { execFileSync as execFileSync21 } from "node:child_process";
86173
86841
  import { closeSync as closeSync12, mkdirSync as mkdirSync38, openSync as openSync12, existsSync as existsSync72, unlinkSync as unlinkSync13 } from "node:fs";
86174
- import { join as join72, resolve as resolve45 } from "node:path";
86175
- import { homedir as homedir43 } from "node:os";
86842
+ import { join as join72, resolve as resolve46 } from "node:path";
86843
+ import { homedir as homedir44 } from "node:os";
86176
86844
  import { randomBytes as randomBytes13 } from "node:crypto";
86177
86845
 
86178
86846
  // src/worktree/registry.ts
86179
86847
  import {
86180
86848
  mkdirSync as mkdirSync37,
86181
86849
  writeFileSync as writeFileSync33,
86182
- readFileSync as readFileSync63,
86850
+ readFileSync as readFileSync64,
86183
86851
  readdirSync as readdirSync24,
86184
86852
  unlinkSync as unlinkSync12,
86185
86853
  existsSync as existsSync71,
86186
86854
  renameSync as renameSync13
86187
86855
  } from "node:fs";
86188
- import { join as join71, resolve as resolve44 } from "node:path";
86189
- import { homedir as homedir42 } from "node:os";
86856
+ import { join as join71, resolve as resolve45 } from "node:path";
86857
+ import { homedir as homedir43 } from "node:os";
86190
86858
  function registryDir() {
86191
- return resolve44(process.env.SWITCHROOM_WORKTREE_DIR ?? join71(homedir42(), ".switchroom", "worktrees"));
86859
+ return resolve45(process.env.SWITCHROOM_WORKTREE_DIR ?? join71(homedir43(), ".switchroom", "worktrees"));
86192
86860
  }
86193
86861
  function recordPath(id) {
86194
86862
  return join71(registryDir(), `${id}.json`);
@@ -86207,7 +86875,7 @@ function writeRecord(record2) {
86207
86875
  function readRecord(id) {
86208
86876
  const path7 = recordPath(id);
86209
86877
  try {
86210
- const raw = readFileSync63(path7, "utf8");
86878
+ const raw = readFileSync64(path7, "utf8");
86211
86879
  return JSON.parse(raw);
86212
86880
  } catch {
86213
86881
  return null;
@@ -86269,7 +86937,7 @@ function acquireRepoLock(repoPath) {
86269
86937
  }
86270
86938
  var DEFAULT_CONCURRENCY = 5;
86271
86939
  function worktreesBaseDir() {
86272
- return resolve45(process.env.SWITCHROOM_WORKTREE_BASE ?? join72(homedir43(), ".switchroom", "worktree-checkouts"));
86940
+ return resolve46(process.env.SWITCHROOM_WORKTREE_BASE ?? join72(homedir44(), ".switchroom", "worktree-checkouts"));
86273
86941
  }
86274
86942
  function shortId() {
86275
86943
  return randomBytes13(4).toString("hex");
@@ -86291,7 +86959,7 @@ function resolveRepoPath(repo, codeRepos) {
86291
86959
  }
86292
86960
  function expandHome(p) {
86293
86961
  if (p.startsWith("~/"))
86294
- return join72(homedir43(), p.slice(2));
86962
+ return join72(homedir44(), p.slice(2));
86295
86963
  return p;
86296
86964
  }
86297
86965
  async function claimWorktree(input, codeRepos) {
@@ -86466,15 +87134,15 @@ function runReaper(nowMs) {
86466
87134
  import { execFileSync as execFileSync24 } from "node:child_process";
86467
87135
  import {
86468
87136
  existsSync as existsSync75,
86469
- readFileSync as readFileSync64,
87137
+ readFileSync as readFileSync65,
86470
87138
  readdirSync as readdirSync25,
86471
87139
  statSync as statSync31,
86472
87140
  renameSync as renameSync14,
86473
87141
  mkdirSync as mkdirSync39,
86474
87142
  rmSync as rmSync15
86475
87143
  } from "node:fs";
86476
- import { homedir as homedir44 } from "node:os";
86477
- import { join as join73, resolve as resolve46 } from "node:path";
87144
+ import { homedir as homedir45 } from "node:os";
87145
+ import { join as join73, resolve as resolve47 } from "node:path";
86478
87146
  function parseGitdirPointer(dotGitFileContents) {
86479
87147
  const m = /^gitdir:\s*(.+?)\s*$/m.exec(dotGitFileContents);
86480
87148
  return m ? m[1] : null;
@@ -86549,7 +87217,7 @@ function looksLikeAgentWorktree(path7, branch) {
86549
87217
  return false;
86550
87218
  }
86551
87219
  function isEphemeralPath(path7) {
86552
- const p = resolve46(path7);
87220
+ const p = resolve47(path7);
86553
87221
  for (const root of ["/tmp", "/host", "/state"]) {
86554
87222
  if (p === root || p.startsWith(root + "/"))
86555
87223
  return true;
@@ -86589,12 +87257,12 @@ function defaultPrSignal(repo, branch, exec) {
86589
87257
  }
86590
87258
  }
86591
87259
  function trashRoot() {
86592
- return resolve46(process.env.SWITCHROOM_WORKTREE_TRASH ?? join73(homedir44(), ".switchroom", "worktree-gc-trash"));
87260
+ return resolve47(process.env.SWITCHROOM_WORKTREE_TRASH ?? join73(homedir45(), ".switchroom", "worktree-gc-trash"));
86593
87261
  }
86594
87262
  function planGc(roots, deps = {}) {
86595
87263
  const exists = deps.existsSync ?? existsSync75;
86596
87264
  const readDir = deps.readDir ?? ((p) => readdirSync25(p));
86597
- const readFile4 = deps.readFile ?? ((p) => readFileSync64(p, "utf8"));
87265
+ const readFile4 = deps.readFile ?? ((p) => readFileSync65(p, "utf8"));
86598
87266
  const stat3 = deps.stat ?? ((p) => statSync31(p));
86599
87267
  const exec = deps.exec ?? defaultExec;
86600
87268
  const prSignal = deps.prSignal ?? ((repo, branch) => defaultPrSignal(repo, branch, exec));
@@ -86602,7 +87270,7 @@ function planGc(roots, deps = {}) {
86602
87270
  const trash = join73(trashRoot(), stamp);
86603
87271
  let claimed;
86604
87272
  try {
86605
- claimed = new Set(listRecords().map((r) => resolve46(r.path)));
87273
+ claimed = new Set(listRecords().map((r) => resolve47(r.path)));
86606
87274
  } catch {
86607
87275
  claimed = new Set;
86608
87276
  }
@@ -86680,8 +87348,8 @@ function planGc(roots, deps = {}) {
86680
87348
  continue;
86681
87349
  }
86682
87350
  for (const wt of parseWorktreeList(porcelain)) {
86683
- const wtPath = resolve46(wt.path);
86684
- const isMain = wtPath === resolve46(repo);
87351
+ const wtPath = resolve47(wt.path);
87352
+ const isMain = wtPath === resolve47(repo);
86685
87353
  const claimedWt = claimed.has(wtPath);
86686
87354
  const agentWt = looksLikeAgentWorktree(wt.path, wt.branch);
86687
87355
  const ephemeral = isEphemeralPath(wt.path);
@@ -86810,7 +87478,7 @@ function purgeTrash(paths) {
86810
87478
  return { deleted, errors: errors2 };
86811
87479
  }
86812
87480
  function defaultRoots() {
86813
- return [join73(homedir44(), "code")];
87481
+ return [join73(homedir45(), "code")];
86814
87482
  }
86815
87483
 
86816
87484
  // src/cli/worktree.ts
@@ -87419,20 +88087,20 @@ function wireStdio(child) {
87419
88087
  async function killChild(child, gracefulMs = 3000) {
87420
88088
  if (child.exitCode !== null || child.signalCode !== null)
87421
88089
  return;
87422
- return new Promise((resolve47) => {
88090
+ return new Promise((resolve48) => {
87423
88091
  let killTimer = null;
87424
88092
  const onExit = () => {
87425
88093
  if (killTimer) {
87426
88094
  clearTimeout(killTimer);
87427
88095
  killTimer = null;
87428
88096
  }
87429
- resolve47();
88097
+ resolve48();
87430
88098
  };
87431
88099
  child.once("exit", onExit);
87432
88100
  try {
87433
88101
  child.kill("SIGTERM");
87434
88102
  } catch {
87435
- resolve47();
88103
+ resolve48();
87436
88104
  return;
87437
88105
  }
87438
88106
  killTimer = setTimeout(() => {
@@ -87546,8 +88214,8 @@ async function runMs365McpLauncher(opts, rt) {
87546
88214
  };
87547
88215
  process.on("SIGINT", onSignal);
87548
88216
  process.on("SIGTERM", onSignal);
87549
- return new Promise((resolve47) => {
87550
- resolveLauncher = resolve47;
88217
+ return new Promise((resolve48) => {
88218
+ resolveLauncher = resolve48;
87551
88219
  });
87552
88220
  }
87553
88221
  function registerM365McpLauncherCommand(program3) {
@@ -87649,23 +88317,23 @@ async function runNotionMcpLauncher(opts, runtime) {
87649
88317
  };
87650
88318
  process.on("SIGTERM", () => forward("SIGTERM"));
87651
88319
  process.on("SIGINT", () => forward("SIGINT"));
87652
- const exitCode = await new Promise((resolve47) => {
88320
+ const exitCode = await new Promise((resolve48) => {
87653
88321
  child.once("exit", (code, signal) => {
87654
88322
  clearTimer(heartbeatHandle);
87655
88323
  if (typeof code === "number") {
87656
- resolve47(code);
88324
+ resolve48(code);
87657
88325
  } else if (signal) {
87658
88326
  const sigCode = { SIGTERM: 15, SIGINT: 2, SIGKILL: 9 }[signal] ?? 1;
87659
- resolve47(128 + sigCode);
88327
+ resolve48(128 + sigCode);
87660
88328
  } else {
87661
- resolve47(1);
88329
+ resolve48(1);
87662
88330
  }
87663
88331
  });
87664
88332
  child.once("error", (err) => {
87665
88333
  clearTimer(heartbeatHandle);
87666
88334
  process.stderr.write(`notion-mcp-launcher: child spawn error: ${err.message}
87667
88335
  `);
87668
- resolve47(1);
88336
+ resolve48(1);
87669
88337
  });
87670
88338
  });
87671
88339
  return exitCode;
@@ -87704,7 +88372,7 @@ function registerNotionMcpLauncherCommand(program3) {
87704
88372
 
87705
88373
  // src/cli/deliver-file.ts
87706
88374
  init_client2();
87707
- import { readFileSync as readFileSync65, statSync as statSync32 } from "node:fs";
88375
+ import { readFileSync as readFileSync66, statSync as statSync32 } from "node:fs";
87708
88376
  import { basename as basename10 } from "node:path";
87709
88377
 
87710
88378
  // src/delivery/onedrive.ts
@@ -88044,7 +88712,7 @@ async function defaultResolveProvider() {
88044
88712
  async function runDeliverFile(localPath, deps = {}) {
88045
88713
  const agentName = safeAgentName(deps.agentName ?? process.env.SWITCHROOM_AGENT_NAME);
88046
88714
  const sizeOf = deps.fileSize ?? ((p) => statSync32(p).size);
88047
- const read = deps.readFile ?? ((p) => new Uint8Array(readFileSync65(p)));
88715
+ const read = deps.readFile ?? ((p) => new Uint8Array(readFileSync66(p)));
88048
88716
  const resolveProvider = deps.resolveProvider ?? defaultResolveProvider;
88049
88717
  let size;
88050
88718
  try {
@@ -88365,9 +89033,9 @@ function runRedactStdin() {
88365
89033
  }
88366
89034
 
88367
89035
  // src/cli/status-ask.ts
88368
- import { readFileSync as readFileSync69, existsSync as existsSync81, readdirSync as readdirSync28 } from "node:fs";
89036
+ import { readFileSync as readFileSync70, existsSync as existsSync81, readdirSync as readdirSync28 } from "node:fs";
88369
89037
  import { join as join80 } from "node:path";
88370
- import { homedir as homedir47 } from "node:os";
89038
+ import { homedir as homedir48 } from "node:os";
88371
89039
 
88372
89040
  // src/status-ask/report.ts
88373
89041
  function parseJsonl(content) {
@@ -88641,7 +89309,7 @@ function runReport(opts) {
88641
89309
  for (const src of sources) {
88642
89310
  let content;
88643
89311
  try {
88644
- content = readFileSync69(src.path, "utf-8");
89312
+ content = readFileSync70(src.path, "utf-8");
88645
89313
  } catch (err) {
88646
89314
  process.stderr.write(`status-ask report: cannot read ${src.path}: ${err instanceof Error ? err.message : String(err)}
88647
89315
  `);
@@ -88702,7 +89370,7 @@ function resolveSources(explicitPath) {
88702
89370
  const config = loadConfig();
88703
89371
  agentsDir = resolveAgentsDir(config);
88704
89372
  } catch {
88705
- agentsDir = join80(homedir47(), ".switchroom", "agents");
89373
+ agentsDir = join80(homedir48(), ".switchroom", "agents");
88706
89374
  }
88707
89375
  if (!existsSync81(agentsDir))
88708
89376
  return [];
@@ -88750,16 +89418,16 @@ import {
88750
89418
  mkdirSync as mkdirSync47,
88751
89419
  openSync as openSync13,
88752
89420
  readdirSync as readdirSync29,
88753
- readFileSync as readFileSync70,
89421
+ readFileSync as readFileSync71,
88754
89422
  renameSync as renameSync16,
88755
89423
  statSync as statSync33,
88756
89424
  unlinkSync as unlinkSync14,
88757
89425
  writeSync as writeSync8
88758
89426
  } from "node:fs";
88759
- import { join as join81, resolve as resolve49 } from "node:path";
89427
+ import { join as join81, resolve as resolve50 } from "node:path";
88760
89428
  var STAGING_SUBDIR = ".staging";
88761
89429
  function overlayPathsFor(agent, opts = {}) {
88762
- const base = opts.root ? resolve49(opts.root, agent) : resolve49(resolveDualPath(`~/.switchroom/agents/${agent}`));
89430
+ const base = opts.root ? resolve50(opts.root, agent) : resolve50(resolveDualPath(`~/.switchroom/agents/${agent}`));
88763
89431
  const scheduleDir = join81(base, "schedule.d");
88764
89432
  const scheduleStagingDir = join81(scheduleDir, STAGING_SUBDIR);
88765
89433
  const skillsDir = join81(base, "skills.d");
@@ -88874,7 +89542,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
88874
89542
  continue;
88875
89543
  const full = join81(paths.skillsDir, name);
88876
89544
  try {
88877
- const raw = readFileSync70(full, "utf-8");
89545
+ const raw = readFileSync71(full, "utf-8");
88878
89546
  const slug = name.replace(/\.ya?ml$/i, "");
88879
89547
  out.push({ slug, path: full, raw });
88880
89548
  } catch {}
@@ -88901,7 +89569,7 @@ function listOverlayEntries(agent, opts = {}) {
88901
89569
  continue;
88902
89570
  const full = join81(paths.scheduleDir, name);
88903
89571
  try {
88904
- const raw = readFileSync70(full, "utf-8");
89572
+ const raw = readFileSync71(full, "utf-8");
88905
89573
  const slug = name.replace(/\.ya?ml$/i, "");
88906
89574
  out.push({ slug, path: full, raw });
88907
89575
  } catch {}
@@ -89057,7 +89725,7 @@ import {
89057
89725
  mkdirSync as mkdirSync48,
89058
89726
  openSync as openSync14,
89059
89727
  readdirSync as readdirSync30,
89060
- readFileSync as readFileSync71,
89728
+ readFileSync as readFileSync72,
89061
89729
  renameSync as renameSync17,
89062
89730
  unlinkSync as unlinkSync15,
89063
89731
  writeFileSync as writeFileSync41,
@@ -89121,7 +89789,7 @@ function listPendingScheduleEntries(agent, opts = {}) {
89121
89789
  if (!existsSync83(yamlPath))
89122
89790
  continue;
89123
89791
  try {
89124
- const meta = JSON.parse(readFileSync71(metaPath, "utf-8"));
89792
+ const meta = JSON.parse(readFileSync72(metaPath, "utf-8"));
89125
89793
  if (meta?.v !== 1 || typeof meta.stage_id !== "string")
89126
89794
  continue;
89127
89795
  out.push({ stageId: meta.stage_id, agent: meta.agent, yamlPath, metaPath, meta });
@@ -89159,7 +89827,7 @@ function denyPendingScheduleEntry(opts) {
89159
89827
  }
89160
89828
 
89161
89829
  // src/cli/agent-config-write.ts
89162
- import { existsSync as existsSync84, readFileSync as readFileSync72 } from "node:fs";
89830
+ import { existsSync as existsSync84, readFileSync as readFileSync73 } from "node:fs";
89163
89831
  import { execFileSync as execFileSync26 } from "node:child_process";
89164
89832
 
89165
89833
  // src/scheduler/schedule-report.ts
@@ -89572,7 +90240,7 @@ function scheduleRemove(opts) {
89572
90240
  let priorContent = null;
89573
90241
  try {
89574
90242
  if (existsSync84(match.path))
89575
- priorContent = readFileSync72(match.path, "utf-8");
90243
+ priorContent = readFileSync73(match.path, "utf-8");
89576
90244
  } catch {}
89577
90245
  deleteOverlayEntry(agent, match.slug, { root: opts.root });
89578
90246
  const reconcileFn = opts.reconcile === undefined ? opts.root ? null : reconcileAgentCronOnly : opts.reconcile;
@@ -89775,7 +90443,7 @@ function registerAgentConfigWriteCommands(program3) {
89775
90443
  }
89776
90444
  let blob;
89777
90445
  if (opts.jsonl) {
89778
- blob = existsSync84(opts.jsonl) ? readFileSync72(opts.jsonl, "utf-8") : "";
90446
+ blob = existsSync84(opts.jsonl) ? readFileSync73(opts.jsonl, "utf-8") : "";
89779
90447
  } else {
89780
90448
  try {
89781
90449
  blob = execFileSync26("docker", ["exec", `switchroom-${agent}`, "cat", "/state/agent/scheduler.jsonl"], {
@@ -90056,7 +90724,7 @@ import {
90056
90724
  mkdirSync as mkdirSync49,
90057
90725
  mkdtempSync as mkdtempSync5,
90058
90726
  openSync as openSync15,
90059
- readFileSync as readFileSync73,
90727
+ readFileSync as readFileSync74,
90060
90728
  readdirSync as readdirSync31,
90061
90729
  realpathSync as realpathSync7,
90062
90730
  renameSync as renameSync18,
@@ -90064,8 +90732,8 @@ import {
90064
90732
  statSync as statSync34,
90065
90733
  writeFileSync as writeFileSync42
90066
90734
  } from "node:fs";
90067
- import { tmpdir as tmpdir5, homedir as homedir48 } from "node:os";
90068
- import { dirname as dirname28, join as join84, relative as relative2, resolve as resolve50 } from "node:path";
90735
+ import { tmpdir as tmpdir5, homedir as homedir49 } from "node:os";
90736
+ import { dirname as dirname28, join as join84, relative as relative2, resolve as resolve51 } from "node:path";
90069
90737
  import { spawnSync as spawnSync15 } from "node:child_process";
90070
90738
 
90071
90739
  // src/cli/skill-common.ts
@@ -90299,11 +90967,11 @@ function scanForClaudeP2(content) {
90299
90967
  function resolveSkillsPoolDir2(override) {
90300
90968
  const raw = override ?? "~/.switchroom/skills";
90301
90969
  if (raw.startsWith("~/")) {
90302
- return join84(homedir48(), raw.slice(2));
90970
+ return join84(homedir49(), raw.slice(2));
90303
90971
  }
90304
90972
  if (raw === "~")
90305
- return homedir48();
90306
- return resolve50(raw);
90973
+ return homedir49();
90974
+ return resolve51(raw);
90307
90975
  }
90308
90976
  function readStdinSync() {
90309
90977
  const chunks = [];
@@ -90348,7 +91016,7 @@ function loadFromDir(dir) {
90348
91016
  continue;
90349
91017
  }
90350
91018
  if (ent.isFile()) {
90351
- const buf = readFileSync73(full);
91019
+ const buf = readFileSync74(full);
90352
91020
  files[rel.replace(/\\/g, "/")] = buf.toString("utf-8");
90353
91021
  }
90354
91022
  }
@@ -90397,7 +91065,7 @@ function loadFromTarball(tarPath) {
90397
91065
  }
90398
91066
  }
90399
91067
  function loadSingleFile(filePath) {
90400
- const content = readFileSync73(filePath, "utf-8");
91068
+ const content = readFileSync74(filePath, "utf-8");
90401
91069
  return { "SKILL.md": content };
90402
91070
  }
90403
91071
  function loadFromStdin() {
@@ -90488,7 +91156,7 @@ function diffSummary(currentDir, files) {
90488
91156
  if (ent.isDirectory()) {
90489
91157
  walk2(full);
90490
91158
  } else if (ent.isFile()) {
90491
- currentFiles[rel.replace(/\\/g, "/")] = readFileSync73(full, "utf-8");
91159
+ currentFiles[rel.replace(/\\/g, "/")] = readFileSync74(full, "utf-8");
90492
91160
  }
90493
91161
  }
90494
91162
  };
@@ -90587,7 +91255,7 @@ function registerSkillCommand(program3) {
90587
91255
  if (opts.from === undefined) {
90588
91256
  files = loadFromStdin();
90589
91257
  } else {
90590
- const fromPath = resolve50(opts.from);
91258
+ const fromPath = resolve51(opts.from);
90591
91259
  if (!existsSync86(fromPath)) {
90592
91260
  fail3(`--from path does not exist: ${opts.from}`);
90593
91261
  }
@@ -90649,7 +91317,7 @@ import {
90649
91317
  mkdirSync as mkdirSync50,
90650
91318
  mkdtempSync as mkdtempSync6,
90651
91319
  openSync as openSync16,
90652
- readFileSync as readFileSync74,
91320
+ readFileSync as readFileSync75,
90653
91321
  readdirSync as readdirSync32,
90654
91322
  renameSync as renameSync19,
90655
91323
  rmSync as rmSync19,
@@ -90657,8 +91325,8 @@ import {
90657
91325
  utimesSync,
90658
91326
  writeFileSync as writeFileSync43
90659
91327
  } from "node:fs";
90660
- import { dirname as dirname29, join as join85, relative as relative3, resolve as resolve51 } from "node:path";
90661
- import { homedir as homedir49, tmpdir as tmpdir6 } from "node:os";
91328
+ import { dirname as dirname29, join as join85, relative as relative3, resolve as resolve52 } from "node:path";
91329
+ import { homedir as homedir50, tmpdir as tmpdir6 } from "node:os";
90662
91330
  import { spawnSync as spawnSync16 } from "node:child_process";
90663
91331
  init_helpers();
90664
91332
  init_agent_config();
@@ -90669,7 +91337,7 @@ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
90669
91337
  var PERSONAL_SKILLS_SUBPATH = "personal-skills";
90670
91338
  function resolveConfigSkillsDir(agent) {
90671
91339
  const override = process.env.SWITCHROOM_CONFIG_DIR;
90672
- const candidate = override ? resolve51(override) : join85(homedir49(), ".switchroom-config");
91340
+ const candidate = override ? resolve52(override) : join85(homedir50(), ".switchroom-config");
90673
91341
  if (!existsSync87(candidate))
90674
91342
  return null;
90675
91343
  return join85(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
@@ -90732,7 +91400,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
90732
91400
  if (ent.isDirectory())
90733
91401
  walk2(s, d);
90734
91402
  else if (ent.isFile()) {
90735
- writeFileSync43(d, readFileSync74(s));
91403
+ writeFileSync43(d, readFileSync75(s));
90736
91404
  }
90737
91405
  }
90738
91406
  };
@@ -90764,8 +91432,8 @@ function resolveAgent(opts) {
90764
91432
  }
90765
91433
  function resolveAgentsRoot(opts) {
90766
91434
  if (opts.root)
90767
- return resolve51(opts.root);
90768
- return join85(homedir49(), ".switchroom", "agents");
91435
+ return resolve52(opts.root);
91436
+ return join85(homedir50(), ".switchroom", "agents");
90769
91437
  }
90770
91438
  function personalSkillDir(agentsRoot, agent, name) {
90771
91439
  return join85(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
@@ -90805,7 +91473,7 @@ function readStdinSync2() {
90805
91473
  return Buffer.concat(chunks).toString("utf-8");
90806
91474
  }
90807
91475
  function loadFromDir2(dir) {
90808
- const abs = resolve51(dir);
91476
+ const abs = resolve52(dir);
90809
91477
  if (!statSync35(abs).isDirectory()) {
90810
91478
  fail4(`--from path is not a directory: ${dir}`);
90811
91479
  }
@@ -90822,7 +91490,7 @@ function loadFromDir2(dir) {
90822
91490
  }
90823
91491
  if (ent.isFile()) {
90824
91492
  const rel = relative3(abs, full).replace(/\\/g, "/");
90825
- files[rel] = readFileSync74(full, "utf-8");
91493
+ files[rel] = readFileSync75(full, "utf-8");
90826
91494
  }
90827
91495
  }
90828
91496
  };
@@ -91013,7 +91681,7 @@ function loadFiles(opts) {
91013
91681
  if (opts.from === undefined) {
91014
91682
  return loadFromStdin2();
91015
91683
  }
91016
- const p = resolve51(opts.from);
91684
+ const p = resolve52(opts.from);
91017
91685
  if (!existsSync87(p)) {
91018
91686
  fail4(`--from path does not exist: ${opts.from}`);
91019
91687
  }
@@ -91022,7 +91690,7 @@ function loadFiles(opts) {
91022
91690
  return loadFromDir2(p);
91023
91691
  }
91024
91692
  if (p.endsWith(".md")) {
91025
- return { "SKILL.md": readFileSync74(p, "utf-8") };
91693
+ return { "SKILL.md": readFileSync75(p, "utf-8") };
91026
91694
  }
91027
91695
  fail4(`--from must be a directory or a .md file. Got: ${opts.from}`);
91028
91696
  }
@@ -91062,10 +91730,10 @@ function editPersonalAction(name, opts) {
91062
91730
  }
91063
91731
  var CLONE_SOURCE_RE = /^(shared|bundled):([a-z0-9][a-z0-9_-]{0,62})$/;
91064
91732
  function defaultSharedRoot() {
91065
- return join85(homedir49(), ".switchroom", "skills");
91733
+ return join85(homedir50(), ".switchroom", "skills");
91066
91734
  }
91067
91735
  function defaultBundledRoot() {
91068
- return join85(homedir49(), ".switchroom", "skills", "_bundled");
91736
+ return join85(homedir50(), ".switchroom", "skills", "_bundled");
91069
91737
  }
91070
91738
  function resolveCloneSource(source, opts) {
91071
91739
  const m = CLONE_SOURCE_RE.exec(source);
@@ -91111,7 +91779,7 @@ function readSourceFiles(dir) {
91111
91779
  fail4(`clone source has oversized file ${rel} (${st.size} bytes > ${CLONE_MAX_FILE_BYTES}); ` + `refuse to read`, 3);
91112
91780
  }
91113
91781
  } catch {}
91114
- files[rel] = readFileSync74(full, "utf-8");
91782
+ files[rel] = readFileSync75(full, "utf-8");
91115
91783
  }
91116
91784
  }
91117
91785
  };
@@ -91279,19 +91947,19 @@ function registerSkillPersonalCommands(program3) {
91279
91947
 
91280
91948
  // src/cli/self-improve-propose-skill.ts
91281
91949
  import { createConnection as createConnection4 } from "node:net";
91282
- import { homedir as homedir50 } from "node:os";
91950
+ import { homedir as homedir51 } from "node:os";
91283
91951
  import { join as join86 } from "node:path";
91284
- import { readFileSync as readFileSync75 } from "node:fs";
91952
+ import { readFileSync as readFileSync76 } from "node:fs";
91285
91953
  var IPC_CONNECT_TIMEOUT_MS = 5000;
91286
91954
  function gatewaySocketPath() {
91287
- return process.env.SWITCHROOM_GATEWAY_SOCKET ?? (process.env.TELEGRAM_STATE_DIR ? join86(process.env.TELEGRAM_STATE_DIR, "gateway.sock") : join86(homedir50(), ".claude", "channels", "telegram", "gateway.sock"));
91955
+ return process.env.SWITCHROOM_GATEWAY_SOCKET ?? (process.env.TELEGRAM_STATE_DIR ? join86(process.env.TELEGRAM_STATE_DIR, "gateway.sock") : join86(homedir51(), ".claude", "channels", "telegram", "gateway.sock"));
91288
91956
  }
91289
91957
  function fail5(msg, code = 1) {
91290
91958
  console.error(msg);
91291
91959
  process.exit(code);
91292
91960
  }
91293
91961
  function sendProposal(payload) {
91294
- return new Promise((resolve52, reject) => {
91962
+ return new Promise((resolve53, reject) => {
91295
91963
  const sock = createConnection4({ path: gatewaySocketPath() });
91296
91964
  const t = setTimeout(() => {
91297
91965
  sock.destroy();
@@ -91302,7 +91970,7 @@ function sendProposal(payload) {
91302
91970
  sock.write(JSON.stringify(payload) + `
91303
91971
  `, () => {
91304
91972
  sock.end();
91305
- resolve52();
91973
+ resolve53();
91306
91974
  });
91307
91975
  });
91308
91976
  sock.on("error", (err2) => {
@@ -91319,7 +91987,7 @@ function registerSelfImproveProposeSkillCommand(program3) {
91319
91987
  fail5("agent name required (--agent or $SWITCHROOM_AGENT_NAME)");
91320
91988
  let draft;
91321
91989
  try {
91322
- draft = JSON.parse(readFileSync75(opts.draft, "utf-8"));
91990
+ draft = JSON.parse(readFileSync76(opts.draft, "utf-8"));
91323
91991
  } catch (e) {
91324
91992
  fail5(`failed to read/parse --draft: ${e.message}`);
91325
91993
  }
@@ -91356,20 +92024,20 @@ function registerSelfImproveProposeSkillCommand(program3) {
91356
92024
  init_esm();
91357
92025
  init_helpers();
91358
92026
  var import_yaml25 = __toESM(require_dist(), 1);
91359
- import { existsSync as existsSync88, readdirSync as readdirSync33, readFileSync as readFileSync76, statSync as statSync36 } from "node:fs";
91360
- import { homedir as homedir51 } from "node:os";
91361
- import { join as join87, resolve as resolve52 } from "node:path";
92027
+ import { existsSync as existsSync88, readdirSync as readdirSync33, readFileSync as readFileSync77, statSync as statSync36 } from "node:fs";
92028
+ import { homedir as homedir52 } from "node:os";
92029
+ import { join as join87, resolve as resolve53 } from "node:path";
91362
92030
  var PERSONAL_PREFIX2 = "personal-";
91363
92031
  var BUNDLED_SUBDIR = "_bundled";
91364
92032
  var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
91365
92033
  function defaultAgentsRoot() {
91366
- return resolve52(homedir51(), ".switchroom/agents");
92034
+ return resolve53(homedir52(), ".switchroom/agents");
91367
92035
  }
91368
92036
  function defaultSharedRoot2() {
91369
- return resolve52(homedir51(), ".switchroom/skills");
92037
+ return resolve53(homedir52(), ".switchroom/skills");
91370
92038
  }
91371
92039
  function defaultBundledRoot2() {
91372
- return resolve52(homedir51(), ".switchroom/skills/_bundled");
92040
+ return resolve53(homedir52(), ".switchroom/skills/_bundled");
91373
92041
  }
91374
92042
  function readSkillFrontmatter(skillDir) {
91375
92043
  const mdPath = join87(skillDir, "SKILL.md");
@@ -91377,7 +92045,7 @@ function readSkillFrontmatter(skillDir) {
91377
92045
  return null;
91378
92046
  let content;
91379
92047
  try {
91380
- content = readFileSync76(mdPath, "utf-8");
92048
+ content = readFileSync77(mdPath, "utf-8");
91381
92049
  } catch {
91382
92050
  return null;
91383
92051
  }
@@ -91654,14 +92322,14 @@ import {
91654
92322
  existsSync as existsSync90,
91655
92323
  mkdirSync as mkdirSync51,
91656
92324
  readdirSync as readdirSync34,
91657
- readFileSync as readFileSync78,
92325
+ readFileSync as readFileSync79,
91658
92326
  writeFileSync as writeFileSync44,
91659
92327
  statSync as statSync37,
91660
92328
  lstatSync as lstatSync11,
91661
92329
  realpathSync as realpathSync8,
91662
92330
  copyFileSync as copyFileSync12
91663
92331
  } from "node:fs";
91664
- import { homedir as homedir52 } from "node:os";
92332
+ import { homedir as homedir53 } from "node:os";
91665
92333
  import { join as join88 } from "node:path";
91666
92334
  import { spawnSync as spawnSync20 } from "node:child_process";
91667
92335
 
@@ -91884,7 +92552,7 @@ networks:
91884
92552
  # operator surface; the daemon's stderr lands in \`docker logs switchroom-hostd\`.
91885
92553
  `;
91886
92554
  }
91887
- function resolveHostdHostHome(env2 = process.env, home2 = homedir52()) {
92555
+ function resolveHostdHostHome(env2 = process.env, home2 = homedir53()) {
91888
92556
  const fromEnv = env2.SWITCHROOM_HOST_HOME?.trim();
91889
92557
  const resolved = fromEnv && fromEnv.length > 0 ? fromEnv : home2;
91890
92558
  if (resolved === "/host-home" || resolved.startsWith("/host-home/")) {
@@ -91919,7 +92587,7 @@ function resolveHostdSkillsTarget(hostHome) {
91919
92587
  return target;
91920
92588
  }
91921
92589
  function hostdDir() {
91922
- return join88(homedir52(), ".switchroom", "hostd");
92590
+ return join88(homedir53(), ".switchroom", "hostd");
91923
92591
  }
91924
92592
  function hostdComposePath() {
91925
92593
  return join88(hostdDir(), "docker-compose.yml");
@@ -92100,7 +92768,7 @@ function registerHostdCommand(program3) {
92100
92768
  The log is created when hostd handles its first privileged-verb request.`));
92101
92769
  return;
92102
92770
  }
92103
- const raw = readFileSync78(logPath, "utf-8");
92771
+ const raw = readFileSync79(logPath, "utf-8");
92104
92772
  const limit = Math.max(1, parseInt(opts.tail ?? "50", 10) || 50);
92105
92773
  const filters = {
92106
92774
  agent: opts.agent,
@@ -92144,7 +92812,7 @@ init_source();
92144
92812
  init_helpers();
92145
92813
  init_operator_uid();
92146
92814
  import { existsSync as existsSync91, mkdirSync as mkdirSync52, writeFileSync as writeFileSync45, copyFileSync as copyFileSync13 } from "node:fs";
92147
- import { homedir as homedir53 } from "node:os";
92815
+ import { homedir as homedir54 } from "node:os";
92148
92816
  import { join as join89 } from "node:path";
92149
92817
  import { spawnSync as spawnSync21 } from "node:child_process";
92150
92818
  function resolveWebImageTag(explicitTag, release) {
@@ -92230,7 +92898,7 @@ services:
92230
92898
  `;
92231
92899
  }
92232
92900
  function webdDir() {
92233
- return join89(homedir53(), ".switchroom", "web");
92901
+ return join89(homedir54(), ".switchroom", "web");
92234
92902
  }
92235
92903
  function webdComposePath() {
92236
92904
  return join89(webdDir(), "docker-compose.yml");
@@ -92275,7 +92943,7 @@ async function doInstall2(opts, program3) {
92275
92943
  return;
92276
92944
  }
92277
92945
  const yaml = renderWebComposeFile({
92278
- hostHome: homedir53(),
92946
+ hostHome: homedir54(),
92279
92947
  imageTag,
92280
92948
  operatorUid
92281
92949
  });
@@ -92374,7 +93042,7 @@ function registerWebdCommand(program3) {
92374
93042
 
92375
93043
  // src/cli/host-repair.ts
92376
93044
  init_source();
92377
- import { homedir as homedir54 } from "node:os";
93045
+ import { homedir as homedir55 } from "node:os";
92378
93046
  import { join as join90 } from "node:path";
92379
93047
  var ARTIFACT_ALLOWLIST = {
92380
93048
  dockerComposePluginDir: (home2) => join90(home2, ".docker", "cli-plugins", "docker-compose"),
@@ -92426,7 +93094,7 @@ function isStateBogusAutoDir(probe2) {
92426
93094
  return true;
92427
93095
  }
92428
93096
  function planMountRepairs(probe2, opts = {}) {
92429
- const home2 = opts.hostHome ?? homedir54();
93097
+ const home2 = opts.hostHome ?? homedir55();
92430
93098
  const items = [];
92431
93099
  const dockerComposePath = ARTIFACT_ALLOWLIST.dockerComposePluginDir(home2);
92432
93100
  const dockerComposeSt = probe2.lstat(dockerComposePath);
@@ -92614,6 +93282,221 @@ Some artifacts were skipped (see above). Remediate them manually.`));
92614
93282
  });
92615
93283
  }
92616
93284
 
93285
+ // src/cli/fleet-health.ts
93286
+ init_source();
93287
+ init_helpers();
93288
+ init_scan();
93289
+
93290
+ // src/fleet-health/gh-sync.ts
93291
+ import { spawnSync as spawnSync22 } from "node:child_process";
93292
+ var LABEL = "fleet-health";
93293
+ function defaultGhDeps(log) {
93294
+ return {
93295
+ log,
93296
+ run: (args) => {
93297
+ const r = spawnSync22("gh", args, { encoding: "utf-8" });
93298
+ return {
93299
+ ok: r.status === 0,
93300
+ stdout: (r.stdout ?? "").trim(),
93301
+ stderr: (r.stderr ?? "").trim()
93302
+ };
93303
+ }
93304
+ };
93305
+ }
93306
+ function ghAvailable(deps) {
93307
+ const r = deps.run(["auth", "status"]);
93308
+ return r.ok;
93309
+ }
93310
+ function severityLabel(sev) {
93311
+ const s = Math.max(1, Math.min(3, Math.round(sev)));
93312
+ return `severity:${s}`;
93313
+ }
93314
+ function issueTitle(iss, job_spec) {
93315
+ return `[fleet-health] ${job_spec}: ${iss.failure_mode} (${iss.dedup_key})`;
93316
+ }
93317
+ function issueBody(iss, job_spec) {
93318
+ const lines = [];
93319
+ lines.push(`**Job spec:** \`${job_spec}\` (\`reference/jobs/${job_spec}.md\`)`);
93320
+ lines.push(`**Failure mode:** ${iss.failure_mode} \u2014 severity ${iss.severity}`);
93321
+ lines.push(`**Dedup key:** \`${iss.dedup_key}\``);
93322
+ lines.push(`**Frequency (scan window):** ${iss.frequency}`);
93323
+ lines.push(`**Reach:** ${iss.reach.join(", ") || "\u2014"}`);
93324
+ lines.push(`**Newest occurrence:** ${iss.recency ?? "\u2014"}`);
93325
+ lines.push("");
93326
+ lines.push("**Occurrences (hard-artifact evidence):**");
93327
+ for (const o of iss.occurrences.slice(0, 25)) {
93328
+ lines.push(`- \`${o.agent}\` turn \`${o.turn_id}\` \u2014 ${o.log_pointer ?? ""}`);
93329
+ }
93330
+ lines.push("");
93331
+ lines.push("_Opened + maintained by the Fleet Health sensor " + "(`switchroom fleet-health scan --sync-issues`). Closes automatically on " + "a verified count-drop. See `reference/rfcs/fleet-health.md`._");
93332
+ return lines.join(`
93333
+ `);
93334
+ }
93335
+ function syncIssue(deps, repo, job_spec, iss) {
93336
+ const labels = [LABEL, severityLabel(iss.severity), `job:${job_spec}`];
93337
+ const title = issueTitle(iss, job_spec);
93338
+ const body = issueBody(iss, job_spec);
93339
+ const shouldClose = iss.status === "closed" || iss.status === "resolved-pending-verify";
93340
+ if (iss.gh_issue === undefined) {
93341
+ if (shouldClose)
93342
+ return;
93343
+ const r = deps.run([
93344
+ "issue",
93345
+ "create",
93346
+ "-R",
93347
+ repo,
93348
+ "--title",
93349
+ title,
93350
+ "--body",
93351
+ body,
93352
+ "--label",
93353
+ labels.join(",")
93354
+ ]);
93355
+ if (r.ok) {
93356
+ const m = r.stdout.match(/\/issues\/(\d+)/);
93357
+ if (m) {
93358
+ iss.gh_issue = Number(m[1]);
93359
+ deps.log(`fleet-health: opened GH #${iss.gh_issue} for ${iss.dedup_key}`);
93360
+ }
93361
+ } else {
93362
+ deps.log(`fleet-health: gh issue create failed for ${iss.dedup_key}: ${r.stderr}`);
93363
+ }
93364
+ return;
93365
+ }
93366
+ const num = String(iss.gh_issue);
93367
+ const upd = deps.run(["issue", "edit", num, "-R", repo, "--body", body]);
93368
+ if (!upd.ok) {
93369
+ deps.log(`fleet-health: gh issue edit #${num} failed: ${upd.stderr}`);
93370
+ }
93371
+ if (iss.status === "closed") {
93372
+ const c = deps.run([
93373
+ "issue",
93374
+ "close",
93375
+ num,
93376
+ "-R",
93377
+ repo,
93378
+ "--comment",
93379
+ `Verified count-drop (frequency ${iss.frequency} \u2264 resolved threshold). Closed by the Fleet Health sensor.`
93380
+ ]);
93381
+ if (c.ok)
93382
+ deps.log(`fleet-health: closed GH #${num} (count-drop) for ${iss.dedup_key}`);
93383
+ else
93384
+ deps.log(`fleet-health: gh issue close #${num} failed: ${c.stderr}`);
93385
+ }
93386
+ }
93387
+ function syncLedgerIssues(ledger, repo, deps) {
93388
+ if (!ghAvailable(deps)) {
93389
+ deps.log("fleet-health: gh unavailable or not authenticated \u2014 skipping GitHub issue sync (ledger still written).");
93390
+ return { synced: 0, skipped: true };
93391
+ }
93392
+ let synced = 0;
93393
+ for (const rec of ledger.records) {
93394
+ for (const iss of rec.issues) {
93395
+ syncIssue(deps, repo, rec.job_spec, iss);
93396
+ synced++;
93397
+ }
93398
+ rec.gh_issues = rec.issues.map((i) => i.gh_issue).filter((n) => typeof n === "number");
93399
+ }
93400
+ return { synced, skipped: false };
93401
+ }
93402
+
93403
+ // src/cli/fleet-health.ts
93404
+ init_mapping();
93405
+ function registerFleetHealthCommand(program3) {
93406
+ const cmd = program3.command("fleet-health").description("Fleet Health live detection pipeline: model-free sensor, priority " + "ledger, and GitHub-issue lifecycle (RFC fleet-health.md).");
93407
+ cmd.command("scan").description("Nightly model-free sensor: scan every agent's turns.jsonl + gateway " + "log, rank recurring failures by priority, write the ledger.").option("--dry-run", "print the ledger, do not write it", false).option("--json", "emit machine-readable JSON", false).option("--sync-issues", "open/update/close GitHub issues (needs an authed gh; off by default)", false).option("--window-days <n>", "scan window in days", "30").option("--repo <owner/name>", "GitHub repo for issue sync (default switchroom/switchroom)", "switchroom/switchroom").action(withConfigError(async (opts) => {
93408
+ const cfg = getConfig(program3);
93409
+ const ownerAgent = cfg.fleet_health?.owner_agent;
93410
+ const windowDays = Number(opts.windowDays) || 30;
93411
+ const log = (m) => console.error(source_default.gray(m));
93412
+ const result = runScan({ ownerAgent, windowDays, log });
93413
+ const ledger = result.ledger;
93414
+ log(`fleet-health: scanned ${result.agentsScanned} agents` + (result.agentsSkipped.length ? `, skipped ${result.agentsSkipped.length} (${result.agentsSkipped.join(", ")})` : ""));
93415
+ if (opts.syncIssues && !opts.dryRun) {
93416
+ const deps = defaultGhDeps(log);
93417
+ const sync = syncLedgerIssues(ledger, String(opts.repo), deps);
93418
+ if (!sync.skipped) {
93419
+ log(`fleet-health: synced ${sync.synced} issues to ${String(opts.repo)}`);
93420
+ }
93421
+ } else if (opts.syncIssues && opts.dryRun) {
93422
+ log("fleet-health: --dry-run set, skipping GitHub issue sync.");
93423
+ }
93424
+ if (opts.json) {
93425
+ process.stdout.write(JSON.stringify(ledger, null, 2) + `
93426
+ `);
93427
+ } else {
93428
+ printLedgerTable(ledger);
93429
+ }
93430
+ if (opts.dryRun) {
93431
+ log("fleet-health: --dry-run \u2014 ledger NOT written.");
93432
+ return;
93433
+ }
93434
+ const base = resolveSwitchroomBase();
93435
+ const path8 = writeLedger(base, ledger);
93436
+ log(source_default.green(`fleet-health: wrote ledger \u2192 ${path8}`));
93437
+ }));
93438
+ cmd.command("deep-dive-targets").description("Print the top-N ledger records (by priority) as a structured brief " + "for the owner agent's weekly Opus deep-dive cron.").option("--top <n>", "number of targets", "2").option("--json", "emit machine-readable JSON", false).action(withConfigError(async (opts) => {
93439
+ const base = resolveSwitchroomBase();
93440
+ const { readLedgerIfPresent: readLedgerIfPresent2 } = await Promise.resolve().then(() => (init_scan(), exports_scan));
93441
+ const ledger = readLedgerIfPresent2(base);
93442
+ const top = Number(opts.top) || 2;
93443
+ if (!ledger) {
93444
+ console.error(source_default.yellow("fleet-health: no ledger yet \u2014 run `fleet-health scan` first."));
93445
+ return;
93446
+ }
93447
+ const targets = [...ledger.records].filter((r) => r.open_issue_count > 0).sort((a, b) => b.priority_score - a.priority_score).slice(0, top);
93448
+ if (opts.json) {
93449
+ process.stdout.write(JSON.stringify({ targets }, null, 2) + `
93450
+ `);
93451
+ return;
93452
+ }
93453
+ printDeepDiveBrief(targets);
93454
+ }));
93455
+ cmd.command("mapping").description("Print the signal\u2192job-spec mapping + severity table.").action(() => {
93456
+ console.log(source_default.bold(`Signal \u2192 job-spec mapping (model-free):
93457
+ `));
93458
+ for (const [signal, m] of Object.entries(SIGNAL_MAP)) {
93459
+ console.log(` ${source_default.cyan(signal.padEnd(28))} \u2192 ${m.job_spec.padEnd(30)} ` + `[${m.failure_mode}, sev ${m.severity}]`);
93460
+ }
93461
+ });
93462
+ }
93463
+ function printLedgerTable(ledger) {
93464
+ const ranked = [...ledger.records].sort((a, b) => b.priority_score - a.priority_score);
93465
+ console.log(source_default.bold(`
93466
+ Fleet Health ledger (worst-first):`));
93467
+ console.log(source_default.gray(`owner=${ledger.owner_agent ?? "(unset)"} generated_at=${ledger.generated_at}`));
93468
+ for (const r of ranked) {
93469
+ if (r.open_issue_count === 0 && r.priority_score === 0)
93470
+ continue;
93471
+ console.log(` ${source_default.yellow(r.priority_score.toFixed(2).padStart(8))} ` + `${r.job_spec.padEnd(34)} open=${r.open_issue_count} ` + `issues=${r.issues.length}`);
93472
+ for (const iss of r.issues) {
93473
+ console.log(source_default.gray(` - ${iss.dedup_key} freq=${iss.frequency} ` + `reach=[${iss.reach.join(",")}] sev=${iss.severity} ${iss.status}`));
93474
+ }
93475
+ }
93476
+ }
93477
+ function printDeepDiveBrief(targets) {
93478
+ console.log(source_default.bold(`Fleet Health \u2014 weekly deep-dive targets
93479
+ `));
93480
+ if (targets.length === 0) {
93481
+ console.log(" (no open issues \u2014 nothing to deep-dive this week)");
93482
+ return;
93483
+ }
93484
+ for (const r of targets) {
93485
+ console.log(source_default.cyan(`## ${r.job_spec} (score ${r.priority_score.toFixed(2)})`));
93486
+ console.log(` spec: reference/jobs/${r.job_spec}.md`);
93487
+ console.log(` open issues: ${r.open_issue_count}`);
93488
+ for (const iss of r.issues) {
93489
+ console.log(` - ${iss.dedup_key} [${iss.failure_mode}, sev ${iss.severity}]`);
93490
+ console.log(` freq=${iss.frequency} reach=[${iss.reach.join(",")}] ` + `newest=${iss.recency ?? "\u2014"} gh=${iss.gh_issue ?? "none"}`);
93491
+ for (const o of iss.occurrences.slice(0, 5)) {
93492
+ console.log(` \u2022 ${o.agent} ${o.turn_id} \u2014 ${o.log_pointer ?? ""}`);
93493
+ }
93494
+ }
93495
+ console.log("");
93496
+ }
93497
+ console.log(source_default.gray("Deep-dive: read the flagged turns' transcripts (join by turn_id), " + "root-cause, and write the finding into the linked GitHub issue."));
93498
+ }
93499
+
92617
93500
  // src/cli/index.ts
92618
93501
  init_posthog();
92619
93502
  installGlobalErrorHandlers();
@@ -92670,6 +93553,7 @@ registerHostdMcpCommand(program3);
92670
93553
  registerHostdCommand(program3);
92671
93554
  registerWebdCommand(program3);
92672
93555
  registerHostCommand(program3);
93556
+ registerFleetHealthCommand(program3);
92673
93557
 
92674
93558
  // bin/switchroom.ts
92675
93559
  program3.parse();