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
@@ -6961,6 +6961,55 @@ var require_public_api = __commonJS((exports) => {
6961
6961
  exports.stringify = stringify;
6962
6962
  });
6963
6963
 
6964
+ // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/index.js
6965
+ var require_dist = __commonJS((exports) => {
6966
+ var composer = require_composer();
6967
+ var Document = require_Document();
6968
+ var Schema = require_Schema();
6969
+ var errors = require_errors();
6970
+ var Alias = require_Alias();
6971
+ var identity = require_identity();
6972
+ var Pair = require_Pair();
6973
+ var Scalar = require_Scalar();
6974
+ var YAMLMap = require_YAMLMap();
6975
+ var YAMLSeq = require_YAMLSeq();
6976
+ var cst = require_cst();
6977
+ var lexer = require_lexer();
6978
+ var lineCounter = require_line_counter();
6979
+ var parser = require_parser();
6980
+ var publicApi = require_public_api();
6981
+ var visit = require_visit();
6982
+ exports.Composer = composer.Composer;
6983
+ exports.Document = Document.Document;
6984
+ exports.Schema = Schema.Schema;
6985
+ exports.YAMLError = errors.YAMLError;
6986
+ exports.YAMLParseError = errors.YAMLParseError;
6987
+ exports.YAMLWarning = errors.YAMLWarning;
6988
+ exports.Alias = Alias.Alias;
6989
+ exports.isAlias = identity.isAlias;
6990
+ exports.isCollection = identity.isCollection;
6991
+ exports.isDocument = identity.isDocument;
6992
+ exports.isMap = identity.isMap;
6993
+ exports.isNode = identity.isNode;
6994
+ exports.isPair = identity.isPair;
6995
+ exports.isScalar = identity.isScalar;
6996
+ exports.isSeq = identity.isSeq;
6997
+ exports.Pair = Pair.Pair;
6998
+ exports.Scalar = Scalar.Scalar;
6999
+ exports.YAMLMap = YAMLMap.YAMLMap;
7000
+ exports.YAMLSeq = YAMLSeq.YAMLSeq;
7001
+ exports.CST = cst;
7002
+ exports.Lexer = lexer.Lexer;
7003
+ exports.LineCounter = lineCounter.LineCounter;
7004
+ exports.Parser = parser.Parser;
7005
+ exports.parse = publicApi.parse;
7006
+ exports.parseAllDocuments = publicApi.parseAllDocuments;
7007
+ exports.parseDocument = publicApi.parseDocument;
7008
+ exports.stringify = publicApi.stringify;
7009
+ exports.visit = visit.visit;
7010
+ exports.visitAsync = visit.visitAsync;
7011
+ });
7012
+
6964
7013
  // telegram-plugin/secret-detect/suppressor.ts
6965
7014
  function isSuppressed(text, start, end) {
6966
7015
  const left = Math.max(0, start - WINDOW);
@@ -9037,7 +9086,7 @@ function requireLodash_sortby() {
9037
9086
  return value;
9038
9087
  }
9039
9088
  if (value == null) {
9040
- return identity2;
9089
+ return identity;
9041
9090
  }
9042
9091
  if (typeof value == "object") {
9043
9092
  return isArray2(value) ? baseMatchesProperty(value[0], value[1]) : baseMatches(value);
@@ -9083,7 +9132,7 @@ function requireLodash_sortby() {
9083
9132
  }
9084
9133
  function baseOrderBy(collection, iteratees, orders) {
9085
9134
  var index = -1;
9086
- iteratees = arrayMap(iteratees.length ? iteratees : [identity2], baseUnary(baseIteratee));
9135
+ iteratees = arrayMap(iteratees.length ? iteratees : [identity], baseUnary(baseIteratee));
9087
9136
  var result = baseMap(collection, function(value, key, collection2) {
9088
9137
  var criteria = arrayMap(iteratees, function(iteratee) {
9089
9138
  return iteratee(value);
@@ -9518,7 +9567,7 @@ function requireLodash_sortby() {
9518
9567
  function keys(object) {
9519
9568
  return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
9520
9569
  }
9521
- function identity2(value) {
9570
+ function identity(value) {
9522
9571
  return value;
9523
9572
  }
9524
9573
  function property(path) {
@@ -9664,6 +9713,13 @@ var init_secretlint_source = __esm(() => {
9664
9713
  init_suppressor();
9665
9714
  });
9666
9715
 
9716
+ // src/util/atomic.ts
9717
+ import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
9718
+ var TMP_OPEN_FLAGS;
9719
+ var init_atomic = __esm(() => {
9720
+ TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
9721
+ });
9722
+
9667
9723
  // node_modules/.bun/commander@13.1.0/node_modules/commander/lib/error.js
9668
9724
  var require_error = __commonJS((exports2) => {
9669
9725
  class CommanderError extends Error {
@@ -11704,56 +11760,11 @@ import { existsSync as existsSync9 } from "node:fs";
11704
11760
  import { join as join6, resolve as resolve7 } from "node:path";
11705
11761
 
11706
11762
  // src/config/loader.ts
11763
+ var import_yaml2 = __toESM(require_dist(), 1);
11707
11764
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
11708
11765
  import { homedir } from "node:os";
11709
11766
  import { resolve as resolve3 } from "node:path";
11710
11767
 
11711
- // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/index.js
11712
- var composer = require_composer();
11713
- var Document = require_Document();
11714
- var Schema = require_Schema();
11715
- var errors = require_errors();
11716
- var Alias = require_Alias();
11717
- var identity = require_identity();
11718
- var Pair = require_Pair();
11719
- var Scalar = require_Scalar();
11720
- var YAMLMap = require_YAMLMap();
11721
- var YAMLSeq = require_YAMLSeq();
11722
- var cst = require_cst();
11723
- var lexer = require_lexer();
11724
- var lineCounter = require_line_counter();
11725
- var parser = require_parser();
11726
- var publicApi = require_public_api();
11727
- var visit = require_visit();
11728
- var $Composer = composer.Composer;
11729
- var $Document = Document.Document;
11730
- var $Schema = Schema.Schema;
11731
- var $YAMLError = errors.YAMLError;
11732
- var $YAMLParseError = errors.YAMLParseError;
11733
- var $YAMLWarning = errors.YAMLWarning;
11734
- var $Alias = Alias.Alias;
11735
- var $isAlias = identity.isAlias;
11736
- var $isCollection = identity.isCollection;
11737
- var $isDocument = identity.isDocument;
11738
- var $isMap = identity.isMap;
11739
- var $isNode = identity.isNode;
11740
- var $isPair = identity.isPair;
11741
- var $isScalar = identity.isScalar;
11742
- var $isSeq = identity.isSeq;
11743
- var $Pair = Pair.Pair;
11744
- var $Scalar = Scalar.Scalar;
11745
- var $YAMLMap = YAMLMap.YAMLMap;
11746
- var $YAMLSeq = YAMLSeq.YAMLSeq;
11747
- var $Lexer = lexer.Lexer;
11748
- var $LineCounter = lineCounter.LineCounter;
11749
- var $Parser = parser.Parser;
11750
- var $parse = publicApi.parse;
11751
- var $parseAllDocuments = publicApi.parseAllDocuments;
11752
- var $parseDocument = publicApi.parseDocument;
11753
- var $stringify = publicApi.stringify;
11754
- var $visit = visit.visit;
11755
- var $visitAsync = visit.visitAsync;
11756
-
11757
11768
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
11758
11769
  var exports_external = {};
11759
11770
  __export(exports_external, {
@@ -16188,7 +16199,7 @@ var AgentSchema = exports_external.object({
16188
16199
  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(),
16189
16200
  reactions: ReactionsSchema,
16190
16201
  reaction_dispatch: ReactionDispatchSchema,
16191
- 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')"),
16202
+ 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')"),
16192
16203
  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."),
16193
16204
  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 — use only in trusted sandboxes."),
16194
16205
  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."),
@@ -16319,6 +16330,11 @@ var HostControlConfigSchema = exports_external.object({
16319
16330
  var WebServiceConfigSchema = exports_external.object({
16320
16331
  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 — 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.")
16321
16332
  });
16333
+ var FleetHealthConfigSchema = exports_external.object({
16334
+ owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
16335
+ message: "owner_agent must match the standard agent-name pattern"
16336
+ }).optional().describe("The admin agent that runs the Fleet Health sensor + deep-dive crons " + "(RFC fleet-health.md). Default unset → 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 — never a self-authored loop (on-leash, " + "no-self-escalation).")
16337
+ });
16322
16338
  var HostdConfigSchema = exports_external.object({
16323
16339
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — 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."),
16324
16340
  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 §5). 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 — so a looping agent is throttled rather than spamming the chat.")
@@ -16367,6 +16383,7 @@ var SwitchroomConfigSchema = exports_external.object({
16367
16383
  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."),
16368
16384
  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)."),
16369
16385
  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)."),
16386
+ fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health — 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 → inert; the admin page renders an empty state."),
16370
16387
  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 — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
16371
16388
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
16372
16389
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
@@ -16458,6 +16475,7 @@ function resolveDualPath(pathStr) {
16458
16475
  }
16459
16476
 
16460
16477
  // src/config/overlay-loader.ts
16478
+ var import_yaml = __toESM(require_dist(), 1);
16461
16479
  import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
16462
16480
  import { basename, resolve as resolve2 } from "node:path";
16463
16481
 
@@ -16533,7 +16551,7 @@ function applyAgentOverlays(config) {
16533
16551
  for (const file of files) {
16534
16552
  try {
16535
16553
  const raw = readFileSync(file, "utf-8");
16536
- const parsed = $parse(raw);
16554
+ const parsed = import_yaml.parse(raw);
16537
16555
  const doc = OverlayDocSchema.parse(parsed);
16538
16556
  const title = deriveOverlayTitle(raw, basename(file));
16539
16557
  for (const entry of doc.schedule ?? []) {
@@ -16574,7 +16592,7 @@ function applyAgentOverlays(config) {
16574
16592
  for (const file of skillFiles) {
16575
16593
  try {
16576
16594
  const raw = readFileSync(file, "utf-8");
16577
- const parsed = $parse(raw);
16595
+ const parsed = import_yaml.parse(raw);
16578
16596
  const doc = OverlayDocSchema.parse(parsed);
16579
16597
  for (const skillName of doc.skills ?? []) {
16580
16598
  if (seen.has(skillName))
@@ -17091,7 +17109,7 @@ function loadConfig(configPath) {
17091
17109
  }
17092
17110
  let parsed;
17093
17111
  try {
17094
- parsed = $parse(raw);
17112
+ parsed = import_yaml2.parse(raw);
17095
17113
  } catch (err) {
17096
17114
  throw new ConfigError(`Invalid YAML in ${filePath}`, [
17097
17115
  ` ${err.message}`
@@ -19472,8 +19490,8 @@ function createStackParser(platform, ...parsers) {
19472
19490
  continue;
19473
19491
  const cleanedLine = WEBPACK_ERROR_REGEXP.test(line) ? line.replace(WEBPACK_ERROR_REGEXP, "$1") : line;
19474
19492
  if (!cleanedLine.match(/\S*Error: /)) {
19475
- for (const parser2 of parsers) {
19476
- const frame = parser2(cleanedLine, platform);
19493
+ for (const parser of parsers) {
19494
+ const frame = parser(cleanedLine, platform);
19477
19495
  if (frame) {
19478
19496
  frames.push(frame);
19479
19497
  break;
@@ -21282,14 +21300,14 @@ class PostHogBackendClient extends PostHogCoreStateless {
21282
21300
  else {
21283
21301
  requestId = flagsResponse.requestId;
21284
21302
  evaluatedAt = flagsResponse.evaluatedAt;
21285
- const errors3 = [];
21303
+ const errors2 = [];
21286
21304
  if (flagsResponse.errorsWhileComputingFlags)
21287
- errors3.push(FeatureFlagError2.ERRORS_WHILE_COMPUTING);
21305
+ errors2.push(FeatureFlagError2.ERRORS_WHILE_COMPUTING);
21288
21306
  if (flagsResponse.quotaLimited?.includes("feature_flags"))
21289
- errors3.push(FeatureFlagError2.QUOTA_LIMITED);
21307
+ errors2.push(FeatureFlagError2.QUOTA_LIMITED);
21290
21308
  const flagDetail = flagsResponse.flags[key];
21291
21309
  if (flagDetail === undefined)
21292
- errors3.push(FeatureFlagError2.FLAG_MISSING);
21310
+ errors2.push(FeatureFlagError2.FLAG_MISSING);
21293
21311
  else {
21294
21312
  flagId = flagDetail.metadata?.id;
21295
21313
  flagVersion = flagDetail.metadata?.version;
@@ -21308,8 +21326,8 @@ class PostHogBackendClient extends PostHogCoreStateless {
21308
21326
  payload: parsedPayload
21309
21327
  };
21310
21328
  }
21311
- if (errors3.length > 0)
21312
- featureFlagError = errors3.join(",");
21329
+ if (errors2.length > 0)
21330
+ featureFlagError = errors2.join(",");
21313
21331
  }
21314
21332
  }
21315
21333
  if (sendFeatureFlagEvents) {
@@ -22595,16 +22613,18 @@ function detectInstallType() {
22595
22613
  }
22596
22614
  }
22597
22615
 
22598
- // src/util/atomic.ts
22599
- import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
22600
- var TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
22616
+ // src/cli/release-yaml.ts
22617
+ var import_yaml3 = __toESM(require_dist(), 1);
22618
+
22619
+ // src/cli/rollout.ts
22620
+ init_atomic();
22601
22621
 
22602
22622
  // src/cli/resolve-version.ts
22603
22623
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:fs";
22604
22624
  import { dirname as dirname4, join as join2 } from "node:path";
22605
22625
 
22606
22626
  // src/build-info.ts
22607
- var VERSION = "0.16.38";
22627
+ var VERSION = "0.15.48";
22608
22628
 
22609
22629
  // src/cli/resolve-version.ts
22610
22630
  function readPackageVersion() {
@@ -22734,6 +22754,14 @@ function latestRolloutRowForRequest(raw, requestId) {
22734
22754
  return latest;
22735
22755
  }
22736
22756
 
22757
+ // src/setup/hindsight.ts
22758
+ var HINDSIGHT_CONSUMER_NAME = "hindsight";
22759
+ var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
22760
+ var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
22761
+ var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
22762
+ var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
22763
+ var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
22764
+
22737
22765
  // src/cli/rollout.ts
22738
22766
  function isVersionAssertable(target) {
22739
22767
  return /^v?\d+\.\d+\.\d+$/.test(target.trim());
@@ -22809,6 +22837,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process";
22809
22837
  var MAX_BYTES = 10 * 1024 * 1024;
22810
22838
 
22811
22839
  // src/agents/singleton-reconcile.ts
22840
+ var import_yaml4 = __toESM(require_dist(), 1);
22812
22841
  var CORE_SINGLETON_SERVICES = [
22813
22842
  "vault-broker",
22814
22843
  "approval-kernel",
@@ -22894,6 +22923,7 @@ function getAllAgentStatuses(config) {
22894
22923
  }
22895
22924
 
22896
22925
  // src/cli/update.ts
22926
+ init_atomic();
22897
22927
  var DEFAULT_COMPOSE_PATH = join3(homedir3(), ".switchroom", "compose", "docker-compose.yml");
22898
22928
  var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
22899
22929
  function parseUpdateResultLine(stdout) {
@@ -22916,6 +22946,7 @@ function parseUpdateResultLine(stdout) {
22916
22946
  }
22917
22947
 
22918
22948
  // src/host-control/config-edit-validator.ts
22949
+ var import_yaml5 = __toESM(require_dist(), 1);
22919
22950
  import { mkdtempSync, writeFileSync as writeFileSync2, rmSync as rmSync2, existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
22920
22951
  import { tmpdir } from "node:os";
22921
22952
  import { join as join4, isAbsolute as isAbsolute2, normalize, basename as basename2 } from "node:path";
@@ -23085,7 +23116,7 @@ function applyPatch(unifiedDiff, configPath, gitBin) {
23085
23116
  function failsafeParse(source) {
23086
23117
  let doc;
23087
23118
  try {
23088
- doc = $parseDocument(source, { merge: false, strict: true });
23119
+ doc = import_yaml5.parseDocument(source, { merge: false, strict: true });
23089
23120
  } catch (err2) {
23090
23121
  return {
23091
23122
  ok: false,
@@ -23101,25 +23132,25 @@ function failsafeParse(source) {
23101
23132
  };
23102
23133
  }
23103
23134
  let rejection = null;
23104
- $visit(doc, (_key, node, _path) => {
23135
+ import_yaml5.visit(doc, (_key, node, _path) => {
23105
23136
  if (rejection)
23106
- return $visit.BREAK;
23107
- if ($isAlias(node)) {
23137
+ return import_yaml5.visit.BREAK;
23138
+ if (import_yaml5.isAlias(node)) {
23108
23139
  rejection = {
23109
23140
  ok: false,
23110
23141
  code: "E_YAML_UNSAFE_CONSTRUCT",
23111
23142
  detail: "YAML aliases (`*name`) are not allowed"
23112
23143
  };
23113
- return $visit.BREAK;
23144
+ return import_yaml5.visit.BREAK;
23114
23145
  }
23115
- if ($isNode(node)) {
23146
+ if (import_yaml5.isNode(node)) {
23116
23147
  if (node.anchor) {
23117
23148
  rejection = {
23118
23149
  ok: false,
23119
23150
  code: "E_YAML_UNSAFE_CONSTRUCT",
23120
23151
  detail: `YAML anchors (\`&${node.anchor}\`) are not allowed`
23121
23152
  };
23122
- return $visit.BREAK;
23153
+ return import_yaml5.visit.BREAK;
23123
23154
  }
23124
23155
  const tag = node.tag;
23125
23156
  if (typeof tag === "string" && tag.startsWith("!")) {
@@ -23128,18 +23159,18 @@ function failsafeParse(source) {
23128
23159
  code: "E_YAML_UNSAFE_CONSTRUCT",
23129
23160
  detail: `YAML explicit tags (\`${tag}\`) are not allowed`
23130
23161
  };
23131
- return $visit.BREAK;
23162
+ return import_yaml5.visit.BREAK;
23132
23163
  }
23133
23164
  }
23134
- if ($isPair(node)) {
23165
+ if (import_yaml5.isPair(node)) {
23135
23166
  const k = node.key;
23136
- if ($isScalar(k) && k.value === "<<") {
23167
+ if (import_yaml5.isScalar(k) && k.value === "<<") {
23137
23168
  rejection = {
23138
23169
  ok: false,
23139
23170
  code: "E_YAML_UNSAFE_CONSTRUCT",
23140
23171
  detail: "YAML merge keys (`<<:`) are not allowed"
23141
23172
  };
23142
- return $visit.BREAK;
23173
+ return import_yaml5.visit.BREAK;
23143
23174
  }
23144
23175
  }
23145
23176
  return;
@@ -23275,7 +23306,7 @@ function validateConfigEdit(opts) {
23275
23306
  let beforeData = {};
23276
23307
  try {
23277
23308
  const beforeRaw = existsSync7(opts.configPath) ? readFileSync5(opts.configPath, "utf8") : "";
23278
- const beforeDoc = $parseDocument(beforeRaw, { merge: false, strict: false });
23309
+ const beforeDoc = import_yaml5.parseDocument(beforeRaw, { merge: false, strict: false });
23279
23310
  beforeData = beforeDoc.toJS();
23280
23311
  } catch {
23281
23312
  beforeData = {};
@@ -23289,8 +23320,8 @@ function assertSelfScopedAllowEdit(beforeContent, afterContent, caller) {
23289
23320
  let before;
23290
23321
  let after;
23291
23322
  try {
23292
- before = toObject($parseDocument(beforeContent, { merge: false, strict: false }).toJS());
23293
- after = toObject($parseDocument(afterContent, { merge: false, strict: false }).toJS());
23323
+ before = toObject(import_yaml5.parseDocument(beforeContent, { merge: false, strict: false }).toJS());
23324
+ after = toObject(import_yaml5.parseDocument(afterContent, { merge: false, strict: false }).toJS());
23294
23325
  } catch (e) {
23295
23326
  return { ok: false, detail: `self-scope: config did not parse (${e.message})` };
23296
23327
  }
@@ -23349,6 +23380,7 @@ function stripCallerAllow(cfg, caller) {
23349
23380
  }
23350
23381
 
23351
23382
  // src/host-control/config-blast-radius.ts
23383
+ var import_yaml6 = __toESM(require_dist(), 1);
23352
23384
  function toObject2(v) {
23353
23385
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
23354
23386
  }
@@ -23397,8 +23429,8 @@ function classifyBlastRadius(beforeYaml, afterYaml) {
23397
23429
  let before;
23398
23430
  let after;
23399
23431
  try {
23400
- before = toObject2($parse(beforeYaml));
23401
- after = toObject2($parse(afterYaml));
23432
+ before = toObject2(import_yaml6.parse(beforeYaml));
23433
+ after = toObject2(import_yaml6.parse(afterYaml));
23402
23434
  } catch {
23403
23435
  return { agents: [], fleetWide: true, changedPaths: ["<unparseable>"] };
23404
23436
  }
@@ -23789,8 +23821,8 @@ class HostdServer {
23789
23821
  this.idempotencyKeys.clear();
23790
23822
  }
23791
23823
  onConnection(socket, bindPath) {
23792
- const identity2 = socketPathToIdentity(bindPath);
23793
- if (!identity2) {
23824
+ const identity = socketPathToIdentity(bindPath);
23825
+ if (!identity) {
23794
23826
  socket.end();
23795
23827
  return;
23796
23828
  }
@@ -23809,7 +23841,7 @@ class HostdServer {
23809
23841
  return;
23810
23842
  const line = buf.slice(0, nl);
23811
23843
  buf = buf.slice(nl + 1);
23812
- this.handleLine(line, identity2, socket).catch((err2) => {
23844
+ this.handleLine(line, identity, socket).catch((err2) => {
23813
23845
  process.stderr.write(`hostd: handler error: ${err2.message}
23814
23846
  `);
23815
23847
  socket.end();
@@ -3984,7 +3984,7 @@ var init_zod = __esm(() => {
3984
3984
  });
3985
3985
 
3986
3986
  // src/config/schema.ts
3987
- 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
3987
+ 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
3988
3988
  var init_schema = __esm(() => {
3989
3989
  init_zod();
3990
3990
  CodeRepoEntrySchema = exports_external.object({
@@ -4447,7 +4447,7 @@ var init_schema = __esm(() => {
4447
4447
  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(),
4448
4448
  reactions: ReactionsSchema,
4449
4449
  reaction_dispatch: ReactionDispatchSchema,
4450
- 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')"),
4450
+ 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')"),
4451
4451
  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."),
4452
4452
  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 — use only in trusted sandboxes."),
4453
4453
  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."),
@@ -4578,6 +4578,11 @@ var init_schema = __esm(() => {
4578
4578
  WebServiceConfigSchema = exports_external.object({
4579
4579
  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 — 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.")
4580
4580
  });
4581
+ FleetHealthConfigSchema = exports_external.object({
4582
+ owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
4583
+ message: "owner_agent must match the standard agent-name pattern"
4584
+ }).optional().describe("The admin agent that runs the Fleet Health sensor + deep-dive crons " + "(RFC fleet-health.md). Default unset → 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 — never a self-authored loop (on-leash, " + "no-self-escalation).")
4585
+ });
4581
4586
  HostdConfigSchema = exports_external.object({
4582
4587
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — 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."),
4583
4588
  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 §5). 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 — so a looping agent is throttled rather than spamming the chat.")
@@ -4626,6 +4631,7 @@ var init_schema = __esm(() => {
4626
4631
  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."),
4627
4632
  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)."),
4628
4633
  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)."),
4634
+ fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health — 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 → inert; the admin page renders an empty state."),
4629
4635
  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 — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
4630
4636
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4631
4637
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
@@ -3984,7 +3984,7 @@ var init_zod = __esm(() => {
3984
3984
  });
3985
3985
 
3986
3986
  // src/config/schema.ts
3987
- 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
3987
+ 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, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
3988
3988
  var init_schema = __esm(() => {
3989
3989
  init_zod();
3990
3990
  CodeRepoEntrySchema = exports_external.object({
@@ -4447,7 +4447,7 @@ var init_schema = __esm(() => {
4447
4447
  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(),
4448
4448
  reactions: ReactionsSchema,
4449
4449
  reaction_dispatch: ReactionDispatchSchema,
4450
- 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')"),
4450
+ 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')"),
4451
4451
  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."),
4452
4452
  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 — use only in trusted sandboxes."),
4453
4453
  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."),
@@ -4578,6 +4578,11 @@ var init_schema = __esm(() => {
4578
4578
  WebServiceConfigSchema = exports_external.object({
4579
4579
  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 — 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.")
4580
4580
  });
4581
+ FleetHealthConfigSchema = exports_external.object({
4582
+ owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
4583
+ message: "owner_agent must match the standard agent-name pattern"
4584
+ }).optional().describe("The admin agent that runs the Fleet Health sensor + deep-dive crons " + "(RFC fleet-health.md). Default unset → 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 — never a self-authored loop (on-leash, " + "no-self-escalation).")
4585
+ });
4581
4586
  HostdConfigSchema = exports_external.object({
4582
4587
  config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — 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."),
4583
4588
  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 §5). 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 — so a looping agent is throttled rather than spamming the chat.")
@@ -4626,6 +4631,7 @@ var init_schema = __esm(() => {
4626
4631
  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."),
4627
4632
  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)."),
4628
4633
  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)."),
4634
+ fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health — 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 → inert; the admin page renders an empty state."),
4629
4635
  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 — then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
4630
4636
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4631
4637
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
@@ -23,7 +23,7 @@ memory:
23
23
  shared_collection: shared
24
24
 
25
25
  defaults:
26
- model: claude-sonnet-4-6
26
+ model: claude-sonnet-5
27
27
  tools:
28
28
  allow: [all]
29
29
  subagents:
@@ -58,7 +58,7 @@ vault:
58
58
  # Every agent inherits this. Per-agent fields win on conflict.
59
59
  # tools/skills/schedule are unioned; scalars are overridden.
60
60
  defaults:
61
- model: claude-sonnet-4-6
61
+ model: claude-sonnet-5
62
62
  # The switchroom-telegram fork is the default. Uncomment to fall back to
63
63
  # the upstream marketplace plugin: channels.telegram.plugin: official
64
64
  channels:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.16.38",
4
+ "version": "0.16.47",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "pretest": "npm run build",
27
27
  "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
28
28
  "test:vitest": "vitest run",
29
- "test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
29
+ "test:bun": "bun test src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
31
  "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
32
32
  "lint:tsc": "tsc --noEmit",
@@ -0,0 +1,9 @@
1
+ ## ⛔ SENDING YOUR ANSWER (read first, applies every turn)
2
+
3
+ **Plain text you write is INVISIBLE to the user.** The only thing that reaches Telegram is text passed through the `mcp__switchroom-telegram__reply` tool. Writing the answer in your response is NOT sending it — the user sees nothing until you call the reply tool.
4
+
5
+ - Every turn that answers a user **MUST end with a `reply` tool call** carrying the full answer. No exceptions, no "I'll just write it out."
6
+ - If you did work first (tool calls, lookups), the reply call is still required at the end — don't let the turn end on a tool result or plain prose.
7
+ - If you genuinely have nothing to add, emit exactly `NO_REPLY` and nothing else.
8
+
9
+ If you skip this, the gateway's safety net eventually forces your answer out 2-4 minutes late and burns extra turns. Don't rely on the net. Send it yourself.
@@ -82,7 +82,7 @@ End with a one-line summary: "X of Y agents running."
82
82
 
83
83
  ```
84
84
  assistant — running (2h 14m)
85
- model: claude-sonnet-4-6 collection: general
85
+ model: claude-sonnet-5 collection: general
86
86
 
87
87
  dev — running (45m)
88
88
  model: claude-opus-4-8 collection: coding
@@ -434,7 +434,8 @@ const TOOL_SCHEMAS = [
434
434
  chat_id: { type: 'string', description: 'Chat to render the approval card in (use the chat_id of the user message that triggered the workflow).' },
435
435
  key: { type: 'string', description: 'Vault key the agent wants access to (matches the key shown in the VAULT-BROKER-DENIED error, e.g. `fatsecret/credentials`).' },
436
436
  scope: { type: 'string', enum: ['read', 'write'], description: 'Access scope: "read" (default) for `vault:<key>` references; "write" if the agent needs to put new values.' },
437
- reason: { type: 'string', description: 'REQUIRED in practice — short human-readable rationale rendered on the card (e.g. "to look up today\'s food log entries"). The approval card now renders "why: not provided" when this is omitted, which signals to the operator that the agent skipped its explanation — they will usually Deny. Always supply a one-line rationale.' },
437
+ reason: { type: 'string', description: 'REQUIRED in practice — short human-readable rationale rendered on the card (e.g. "to look up today\'s food log entries"). The approval card now renders "why: not provided" when this is omitted, which signals to the operator that the agent skipped its explanation — they will usually Deny. Always supply a one-line rationale. `why` is accepted as an alias (matching `vault_request_save`).' },
438
+ why: { type: 'string', description: 'Alias for `reason` (matches the sibling tool `vault_request_save`). If both are supplied, `reason` wins.' },
438
439
  duration: { type: 'string', description: 'Requested grant TTL, like "30d" or "12h". Default 30d, capped at 90d. Beyond 90d the operator should use the host CLI explicitly.' },
439
440
  message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message if not specified.' },
440
441
  },