switchroom 0.8.1 → 0.11.0

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 (137) hide show
  1. package/README.md +54 -61
  2. package/bin/timezone-hook.sh +9 -7
  3. package/dist/agent-scheduler/index.js +285 -45
  4. package/dist/auth-broker/index.js +13932 -0
  5. package/dist/cli/drive-write-pretool.mjs +5418 -0
  6. package/dist/cli/switchroom.js +8890 -5560
  7. package/dist/host-control/main.js +582 -43
  8. package/dist/vault/approvals/kernel-server.js +276 -47
  9. package/dist/vault/broker/server.js +333 -69
  10. package/examples/minimal.yaml +63 -0
  11. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  12. package/examples/personal-google-workspace-mcp/README.md +194 -0
  13. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  14. package/examples/switchroom.yaml +220 -0
  15. package/package.json +6 -4
  16. package/profiles/_base/start.sh.hbs +3 -3
  17. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  18. package/profiles/default/CLAUDE.md +10 -0
  19. package/profiles/default/CLAUDE.md.hbs +16 -0
  20. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  21. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  22. package/skills/buildkite-api/SKILL.md +31 -8
  23. package/skills/buildkite-cli/SKILL.md +27 -9
  24. package/skills/buildkite-migration/SKILL.md +22 -9
  25. package/skills/buildkite-pipelines/SKILL.md +26 -9
  26. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  27. package/skills/buildkite-test-engine/SKILL.md +25 -8
  28. package/skills/docx/SKILL.md +1 -1
  29. package/skills/file-bug/SKILL.md +34 -6
  30. package/skills/humanizer/SKILL.md +15 -0
  31. package/skills/humanizer-calibrate/SKILL.md +7 -1
  32. package/skills/mcp-builder/SKILL.md +1 -1
  33. package/skills/pdf/SKILL.md +1 -1
  34. package/skills/pptx/SKILL.md +1 -1
  35. package/skills/skill-creator/SKILL.md +21 -1
  36. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  37. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  38. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  39. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  40. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  41. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  42. package/skills/switchroom-cli/SKILL.md +63 -64
  43. package/skills/switchroom-health/SKILL.md +23 -10
  44. package/skills/switchroom-install/SKILL.md +3 -3
  45. package/skills/switchroom-manage/SKILL.md +26 -19
  46. package/skills/switchroom-runtime/SKILL.md +67 -15
  47. package/skills/switchroom-status/SKILL.md +26 -1
  48. package/skills/telegram-test-harness/SKILL.md +3 -0
  49. package/skills/webapp-testing/SKILL.md +31 -1
  50. package/skills/xlsx/SKILL.md +1 -1
  51. package/telegram-plugin/admin-commands/dispatch.test.ts +1 -1
  52. package/telegram-plugin/admin-commands/index.ts +9 -5
  53. package/telegram-plugin/auth-snapshot-format.ts +612 -0
  54. package/telegram-plugin/auto-fallback-fleet.ts +215 -0
  55. package/telegram-plugin/auto-fallback.ts +28 -301
  56. package/telegram-plugin/dist/gateway/gateway.js +17453 -15100
  57. package/telegram-plugin/fleet-fallback-gate.ts +105 -0
  58. package/telegram-plugin/gateway/approval-callback.test.ts +104 -0
  59. package/telegram-plugin/gateway/approval-callback.ts +31 -3
  60. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  61. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  62. package/telegram-plugin/gateway/auth-command.ts +905 -0
  63. package/telegram-plugin/gateway/auth-line.ts +123 -0
  64. package/telegram-plugin/gateway/auth-status-adapter.ts +101 -0
  65. package/telegram-plugin/gateway/boot-card.ts +23 -37
  66. package/telegram-plugin/gateway/boot-probes.ts +9 -12
  67. package/telegram-plugin/gateway/diff-preview-card.test.ts +192 -0
  68. package/telegram-plugin/gateway/diff-preview-card.ts +170 -0
  69. package/telegram-plugin/gateway/drive-write-approval.test.ts +312 -0
  70. package/telegram-plugin/gateway/drive-write-approval.ts +243 -0
  71. package/telegram-plugin/gateway/folder-picker-handler.test.ts +314 -0
  72. package/telegram-plugin/gateway/folder-picker-handler.ts +348 -0
  73. package/telegram-plugin/gateway/gateway.ts +1156 -938
  74. package/telegram-plugin/gateway/hostd-dispatch.ts +244 -0
  75. package/telegram-plugin/gateway/ipc-protocol.ts +83 -2
  76. package/telegram-plugin/gateway/ipc-server.ts +69 -0
  77. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +103 -12
  78. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  79. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  80. package/telegram-plugin/model-unavailable.ts +28 -12
  81. package/telegram-plugin/permission-title.ts +56 -0
  82. package/telegram-plugin/quota-check.ts +19 -41
  83. package/telegram-plugin/scripts/build.mjs +0 -1
  84. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  85. package/telegram-plugin/silence-poke.ts +153 -1
  86. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  87. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  88. package/telegram-plugin/tests/auth-command-format2.test.ts +156 -0
  89. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  90. package/telegram-plugin/tests/auth-snapshot-format.test.ts +429 -0
  91. package/telegram-plugin/tests/auth-status-adapter.test.ts +129 -0
  92. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +211 -0
  93. package/telegram-plugin/tests/auto-fallback.test.ts +60 -358
  94. package/telegram-plugin/tests/boot-probes.test.ts +27 -22
  95. package/telegram-plugin/tests/fleet-fallback-gate.test.ts +197 -0
  96. package/telegram-plugin/tests/model-unavailable.test.ts +30 -5
  97. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  98. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  99. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +212 -2
  100. package/telegram-plugin/tests/silence-poke.test.ts +237 -0
  101. package/telegram-plugin/tests/turn-flush-safety.test.ts +112 -0
  102. package/telegram-plugin/turn-flush-safety.ts +55 -1
  103. package/telegram-plugin/uat/SETUP.md +35 -1
  104. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  105. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  106. package/telegram-plugin/uat/runners/report.ts +150 -0
  107. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  108. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  109. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  110. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  111. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  112. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +7 -1
  113. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +7 -1
  114. package/telegram-plugin/auth-dashboard.ts +0 -1104
  115. package/telegram-plugin/auth-slot-parser.ts +0 -497
  116. package/telegram-plugin/auto-fallback-dispatcher.ts +0 -68
  117. package/telegram-plugin/dist/foreman/foreman.js +0 -31358
  118. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  119. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  120. package/telegram-plugin/foreman/foreman.ts +0 -1165
  121. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  122. package/telegram-plugin/foreman/setup-state.ts +0 -239
  123. package/telegram-plugin/foreman/state.ts +0 -203
  124. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  125. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  126. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  127. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  128. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  129. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  130. package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +0 -183
  131. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  132. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  133. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  134. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  135. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  136. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  137. package/telegram-plugin/tests/setup-state.test.ts +0 -146
@@ -6932,12 +6932,12 @@ var require_public_api = __commonJS((exports) => {
6932
6932
  });
6933
6933
 
6934
6934
  // src/agent-scheduler/index.ts
6935
- import { resolve as resolve2, join } from "node:path";
6935
+ import { resolve as resolve4, join } from "node:path";
6936
6936
 
6937
6937
  // src/config/loader.ts
6938
- import { readFileSync, existsSync } from "node:fs";
6938
+ import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
6939
6939
  import { homedir } from "node:os";
6940
- import { resolve } from "node:path";
6940
+ import { resolve as resolve3 } from "node:path";
6941
6941
 
6942
6942
  // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/index.js
6943
6943
  var composer = require_composer();
@@ -11012,7 +11012,7 @@ var AgentHooksSchema = exports_external.object({
11012
11012
  SessionEnd: exports_external.array(HookEntrySchema).optional()
11013
11013
  }).catchall(exports_external.array(HookEntrySchema)).optional();
11014
11014
  var SubagentSchema = exports_external.object({
11015
- description: exports_external.string().describe("When the main agent should delegate to this sub-agent"),
11015
+ description: exports_external.string().optional().describe("When the main agent should delegate to this sub-agent"),
11016
11016
  model: exports_external.string().optional().describe("Model: 'sonnet', 'opus', 'haiku', full ID, or 'inherit' (default)"),
11017
11017
  background: exports_external.boolean().optional().describe("Run in background by default (non-blocking). Default false"),
11018
11018
  isolation: exports_external.enum(["worktree"]).optional().describe("'worktree' gives the sub-agent its own git branch"),
@@ -11091,13 +11091,20 @@ var ChannelsSchema = exports_external.object({
11091
11091
  }).optional();
11092
11092
  var TIMEZONE_REGEX = /^UTC$|^[A-Z][A-Za-z0-9_+-]+(\/[A-Z][A-Za-z0-9_+-]+){1,2}$/;
11093
11093
  var ApproverIdSchema = exports_external.union([exports_external.number(), exports_external.string().regex(/^\d+$/)]);
11094
- var DriveConfigSchema = exports_external.object({
11094
+ var GoogleWorkspaceTierSchema = exports_external.enum([
11095
+ "core",
11096
+ "extended",
11097
+ "complete"
11098
+ ]);
11099
+ var GoogleWorkspaceConfigSchema = exports_external.object({
11095
11100
  google_client_id: exports_external.string().min(1).describe("Google OAuth client ID (literal string or vault reference e.g. 'vault:google-oauth-client-id')"),
11096
11101
  google_client_secret: exports_external.string().min(1).describe("Google OAuth client secret (literal string or vault reference e.g. 'vault:google-oauth-client-secret')"),
11097
- approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified.")
11102
+ approvers: exports_external.array(ApproverIdSchema).min(1).describe("Array of numeric Telegram user IDs authorized to approve drive onboarding. " + "At least one must be specified."),
11103
+ tier: GoogleWorkspaceTierSchema.optional().describe("RFC G Phase 1: which upstream MCP tier to expose. " + "core (default) = ~16 tools (Drive+Docs+Sheets+Calendar). " + "extended = ~40 tools (+Slides, Forms, Tasks, Chat). " + "complete = ~60+ tools (+Gmail; not recommended yet — see RFC G §5).")
11098
11104
  }).optional();
11099
- var AgentDriveConfigSchema = exports_external.object({
11100
- approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card.")
11105
+ var AgentGoogleWorkspaceConfigSchema = exports_external.object({
11106
+ approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
11107
+ tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
11101
11108
  }).optional();
11102
11109
  var ReactionsSchema = exports_external.object({
11103
11110
  enabled: exports_external.boolean().optional().describe("Master switch for the reaction-trigger path. When false, " + "reactions are still persisted via recordReaction but never " + "dispatched to the agent as synthetic inbound turns. Default true."),
@@ -11151,6 +11158,12 @@ var profileFields = {
11151
11158
  claude_md_raw: exports_external.string().optional(),
11152
11159
  cli_args: exports_external.array(exports_external.string()).optional(),
11153
11160
  extra_stable_files: exports_external.array(exports_external.string()).optional().describe("Extra filenames (relative to the agent's workspace directory) to append " + "to the stable bootstrap render. Loaded once at session start via " + "`--append-system-prompt`. Missing files are silently skipped. " + "Example: ['BRIEF.md', 'CONTEXT.md']."),
11161
+ resources: exports_external.object({
11162
+ memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory must be a Docker size string like '6g', '512m', '1.5g'").optional().describe("Hard memory cap (Docker `mem_limit` → cgroup memory.max). When the " + "container exceeds this, the kernel OOM-kills processes in the cgroup. " + "Format: '6g', '1.5g', '512m'. When unset at every cascade layer the " + "compose generator falls back to the hard-coded per-profile defaults " + "in src/agents/compose.ts (klanker 6g, coding 2g, conversational 1.5g, " + "lightweight 1g, default 1.5g)."),
11163
+ memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/, "memory_reservation must be a Docker size string like '4g', '256m'").optional().describe("Soft memory floor (Docker `mem_reservation` → cgroup memory.low). " + "Under host-wide memory pressure, the kernel protects at least this " + "much from being reclaimed from the cgroup. Must be ≤ memory. Use to " + "keep an agent RAM-resident when the host has other tenants that " + "might push the box (Coolify apps, build jobs). Default: unset."),
11164
+ pids_limit: exports_external.number().int().positive().optional().describe("Max processes the cgroup can spawn (cgroup pids.max). Prevents " + "fork bombs and runaway test runners. Counts every process in the " + "cgroup including bash subprocesses, claude itself, sidecars, and " + "any test/build worker. A typical agent at idle uses ~30 PIDs; " + "`npm test`-style workloads can spike to 200+. Set generously " + "(2000 is a comfortable cap for test-running agents). Default: " + "unset (no cgroup pid cap)."),
11165
+ cpus: exports_external.number().positive().optional().describe("CPU quota (Docker `cpus`). Fractional values OK (e.g. 0.5, 2.0). " + "When unset at every cascade layer the compose generator falls " + "back to the per-profile default (klanker/coding 2.0, default 1.0, " + "lightweight 0.5).")
11166
+ }).optional().describe("Per-agent resource limits. Cascades through defaults → profile → " + "per-agent with per-field merge (agent wins on each field independently). " + "Any field left unset at every layer falls back to the hard-coded " + "per-profile defaults in src/agents/compose.ts."),
11154
11167
  experimental: exports_external.object({
11155
11168
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent under " + "the legacy PTY supervisor instead. Default: false (tmux is the default)."),
11156
11169
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script behaviour " + "instead of the tmux send-keys path. Default: false.")
@@ -11164,13 +11177,13 @@ var AgentSchema = exports_external.object({
11164
11177
  bot_token: exports_external.string().optional().describe("Per-agent Telegram bot token or vault reference (overrides global telegram.bot_token)"),
11165
11178
  bot_username: exports_external.string().optional().describe("Per-agent Telegram bot username (without leading @) when it doesn't " + "contain the agent slug. Replaces the default 'username includes slug' " + "preflight check with an exact (case-insensitive) match. Use when an " + "agent and its bot have intentionally divergent names (e.g. agent " + "'lawgpt' paired with bot '@meken_law_bot')."),
11166
11179
  timezone: exports_external.string().regex(TIMEZONE_REGEX, "timezone must be an IANA zone name like 'Australia/Melbourne' or 'UTC' " + "(three-letter aliases like EST/PST and bare offsets like UTC+10 are not accepted)").optional().describe("Per-agent IANA timezone override. Wins over any profile/defaults " + "value and over the top-level switchroom.timezone global. Controls " + "the UserPromptSubmit timezone hook's emitted local time and the " + "systemd unit's TZ= env."),
11167
- auth_label: exports_external.string().optional().describe("Human-readable identity for the session-start greeting (e.g. 'user@example.com'). " + "Anthropic does not expose a public user-profile endpoint for OAuth tokens, so the " + "email/account cannot be read locally; the user declares it here. Appears in the Auth " + "row as '✓ max · <label> · expires ...'."),
11168
11180
  auth: exports_external.object({
11169
- accounts: exports_external.array(exports_external.string()).optional().describe("Ordered list of Anthropic account labels (from `~/.switchroom/accounts/`) " + "this agent can use. The first non-quota-exhausted account is the active one; " + "subsequent entries are auto-fallback targets. switchroom-auth-broker keeps " + "`<agentDir>/.claude/credentials.json` in sync with the active account on " + "every refresh and on every quota event. When unset, the agent falls back to " + "a single 'default' account; if no `default` account exists, the boot self-test " + "surfaces a one-line nudge to run `switchroom auth account add`.")
11170
- }).optional().describe("Account routing for switchroom-auth-broker. See " + "reference/share-auth-across-the-fleet.md for the unit-of-authentication model."),
11181
+ override: exports_external.string().min(1).optional().describe("Per-agent override of the fleet-wide `auth.active`. Edge-case use only " + "this agent talks to the named account regardless of fleet active. See RFC H §4.5.")
11182
+ }).optional().describe("Account routing for switchroom-auth-broker. RFC H schema uses " + "fleet-wide `auth.active` plus per-agent `override:` for edge cases. " + "Pre-RFC-H `auth.accounts: [..]` and `auth_label:` are migrated in-place " + "on first apply (see src/auth/migrate-schema.ts)."),
11171
11183
  dm_only: exports_external.boolean().optional().describe("Mark this agent as a DM-only bot — has its own bot_token and lives " + "exclusively in a private chat with the operator. Suppresses " + "scaffolding's default behavior of inheriting the global " + "telegram.forum_chat_id into the agent's access.json `groups` entry " + "(the forum chat the bot isn't a member of, which would otherwise " + "trigger a 'boot-probe-failed: 400 chat not found' warning every " + "restart). topic_name is still schema-required but unused — set it " + "to a display label like 'DM' for /switchroom status output."),
11172
11184
  topic_name: exports_external.string().describe("Telegram forum topic display name"),
11173
11185
  topic_emoji: exports_external.string().optional().describe("Emoji for the topic (e.g., '\uD83C\uDFCB️')"),
11186
+ purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
11174
11187
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
11175
11188
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
11176
11189
  webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
@@ -11217,7 +11230,8 @@ var AgentSchema = exports_external.object({
11217
11230
  disallowed_tools: exports_external.array(exports_external.string()).optional().describe("Granular tool denylist passed verbatim to Claude Code's --disallowedTools " + "flag. Same pattern syntax as allowed_tools (e.g. 'Bash(rm *)'). See #199."),
11218
11231
  extra_stable_files: exports_external.array(exports_external.string()).optional().describe("Extra filenames (relative to the agent's workspace directory) to append " + "to the stable bootstrap render. Loaded once at session start via " + "`--append-system-prompt`. Missing files are silently skipped. " + "Example: ['BRIEF.md', 'CONTEXT.md']."),
11219
11232
  code_repos: exports_external.array(CodeRepoEntrySchema).optional().describe("Git repositories this agent is allowed to claim worktrees from. " + "Each entry provides a short name alias, a source path, and an " + "optional concurrency cap (default 5). When code_repos is set, " + "claim_worktree accepts the alias as the repo argument. " + "Absolute paths may always be passed regardless of this list."),
11220
- drive: AgentDriveConfigSchema.describe("Per-agent drive onboarding overrides (currently just approvers). " + "When set, replaces the top-level drive.approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
11233
+ drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Per-agent " + "google_workspace overrides (currently approvers + tier). When set, " + "replaces the top-level approvers list for this agent. " + "google_client_id/secret are not per-agent — they live at the top level."),
11234
+ google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides — currently " + "approvers (replaces, does not extend the top-level list) and tier " + "(`core` | `extended` | `complete`, replaces top-level default). " + "google_client_id/secret are not per-agent — they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
11221
11235
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
11222
11236
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
11223
11237
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
@@ -11225,7 +11239,13 @@ var AgentSchema = exports_external.object({
11225
11239
  experimental: exports_external.object({
11226
11240
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
11227
11241
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
11228
- }).optional().describe("Opt-in flags for experimental / legacy behaviours. Cascades through " + "defaults → profile → per-agent.")
11242
+ }).optional().describe("Opt-in flags for experimental / legacy behaviours. Cascades through " + "defaults → profile → per-agent."),
11243
+ resources: exports_external.object({
11244
+ memory: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
11245
+ memory_reservation: exports_external.string().regex(/^\d+(\.\d+)?[kmgKMG]?$/).optional(),
11246
+ pids_limit: exports_external.number().int().positive().optional(),
11247
+ cpus: exports_external.number().positive().optional()
11248
+ }).optional()
11229
11249
  });
11230
11250
  var TelegramConfigSchema = exports_external.object({
11231
11251
  bot_token: exports_external.string().describe("Telegram bot token or vault reference (e.g., 'vault:telegram-bot-token')"),
@@ -11266,7 +11286,7 @@ var QuotaConfigSchema = exports_external.object({
11266
11286
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11267
11287
  });
11268
11288
  var HostControlConfigSchema = exports_external.object({
11269
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false (Phase 1). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. The daemon itself is installed by `switchroom setup` " + "as a systemd user unit. systemd hosts only compose-mode " + "support is deferred to a v2 RFC (see RFC C §5.1 for why). " + "Gateway integration (swap of spawnSwitchroomDetached callsites) " + "lands in a Phase 2 follow-up PR; setting enabled: true in " + "Phase 1 ships the daemon but does not change gateway behavior.")
11289
+ enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
11270
11290
  });
11271
11291
  var SwitchroomConfigSchema = exports_external.object({
11272
11292
  switchroom: exports_external.object({
@@ -11278,9 +11298,28 @@ var SwitchroomConfigSchema = exports_external.object({
11278
11298
  telegram: TelegramConfigSchema,
11279
11299
  memory: MemoryBackendConfigSchema.optional(),
11280
11300
  vault: VaultConfigSchema.optional(),
11281
- drive: DriveConfigSchema.describe("Optional drive onboarding configuration. When set, supplies Google " + "OAuth client credentials and the approver allowlist for `switchroom " + "drive connect`. Env vars (SWITCHROOM_GOOGLE_CLIENT_ID, " + "SWITCHROOM_GOOGLE_CLIENT_SECRET, SWITCHROOM_APPROVER_USER_ID) take " + "precedence over this block when set, preserving back-compat with " + "the env-only flow shipped in #766."),
11301
+ auth: exports_external.object({
11302
+ active: exports_external.string().min(1).optional().describe("Fleet-wide active Anthropic account label. Every agent without " + "an explicit `agent.auth.override` uses this account. See " + "docs/auth.md for the full model. Set by `switchroom auth use <label>`."),
11303
+ fallback_order: exports_external.array(exports_external.string().min(1)).optional().describe("Ordered list of account labels for `switchroom auth rotate` to cycle " + "through when the active account hits a quota event. First entry is " + "normally the same as `auth.active`. When unset, `rotate` is a no-op."),
11304
+ consumers: exports_external.array(exports_external.object({
11305
+ name: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, {
11306
+ message: "Consumer name must be a path-safe slug (letters, digits, underscore, hyphen)"
11307
+ }).describe("Socket-path identity; binds at /run/switchroom/auth-broker/<name>/sock"),
11308
+ account: exports_external.string().min(1).describe("Pinned account label for this consumer. `get-credentials` returns " + "this account's credentials; `mark-exhausted` from this consumer " + "only affects this account."),
11309
+ uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root).")
11310
+ })).optional().describe("Non-agent peers that hold a broker socket (RFC H §4.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation.")
11311
+ }).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
11312
+ drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11313
+ google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11282
11314
  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."),
11283
11315
  host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
11316
+ google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11317
+ message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11318
+ }).transform((v) => v.trim().toLowerCase()), exports_external.object({
11319
+ enabled_for: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
11320
+ message: "Agent name must match the standard agent-name pattern"
11321
+ })).describe("Agent slugs that may read this account's vault slots " + "(`google:<account>:refresh_token` etc). Per-agent ACL is " + "enforced at the broker, not at the agent identity layer — " + "the agent still authenticates via socket-path-as-identity " + "per RFC D §4.1, broker just gates the cross-agent token share.")
11322
+ })).optional().describe("RFC G Phase 2: per-Google-account ACL for vault slots holding " + "OAuth refresh tokens. Maps account email → list of agents " + "permitted to read that account's slots. Written by `switchroom " + "auth google enable|disable` (Phase 3); read by the broker on " + "every Google slot access. Replaces RFC D's per-agent vault slot " + "scope (which can't express 'two agents share one Google account')."),
11284
11323
  defaults: AgentDefaultsSchema.describe("Implicit bottom-of-cascade profile applied to every agent before " + "per-agent config and `extends:` resolution. Tools, mcp_servers, and " + "schedule are unioned/concatenated; scalars and nested objects are " + "shallow-merged with per-agent values winning."),
11285
11324
  profiles: exports_external.record(exports_external.string(), ProfileSchema).optional().describe("Named profile definitions. Agents reference via `extends: <name>`. " + "Inline profiles declared here take priority over filesystem " + "profiles/<name>/ directories when both exist."),
11286
11325
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
@@ -11288,6 +11327,158 @@ var SwitchroomConfigSchema = exports_external.object({
11288
11327
  }), AgentSchema).describe("Map of agent name to agent configuration")
11289
11328
  });
11290
11329
 
11330
+ // src/config/paths.ts
11331
+ import { existsSync } from "node:fs";
11332
+ import { resolve } from "node:path";
11333
+ var DEFAULT_STATE_DIR = ".switchroom";
11334
+ var LEGACY_STATE_DIR = ".clerk";
11335
+ function home() {
11336
+ return process.env.HOME ?? "/root";
11337
+ }
11338
+ function resolveDualPath(pathStr) {
11339
+ const h = home();
11340
+ if (pathStr.startsWith("~/")) {
11341
+ const rest = pathStr.slice(2);
11342
+ const absolute = resolve(h, rest);
11343
+ if (rest.startsWith(`${DEFAULT_STATE_DIR}/`)) {
11344
+ const frag = rest.slice(DEFAULT_STATE_DIR.length + 1);
11345
+ if (!existsSync(absolute)) {
11346
+ const legacy = resolve(h, LEGACY_STATE_DIR, frag);
11347
+ if (existsSync(legacy))
11348
+ return legacy;
11349
+ }
11350
+ }
11351
+ return absolute;
11352
+ }
11353
+ return resolve(pathStr);
11354
+ }
11355
+
11356
+ // src/config/overlay-loader.ts
11357
+ import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
11358
+ import { resolve as resolve2 } from "node:path";
11359
+
11360
+ // src/config/overlay-schema.ts
11361
+ var OverlayDocSchema = exports_external.object({
11362
+ schedule: exports_external.array(ScheduleEntrySchema).optional(),
11363
+ skills: exports_external.array(exports_external.string()).optional()
11364
+ }).strict();
11365
+
11366
+ // src/config/overlay-loader.ts
11367
+ var OVERLAY_SOURCE = Symbol.for("switchroom.config.overlay-source");
11368
+ function overlayDirFor(agentName, subdir) {
11369
+ const base = resolveDualPath(`~/.switchroom/agents/${agentName}/${subdir}`);
11370
+ return resolve2(base);
11371
+ }
11372
+ function listYamlFiles(dir) {
11373
+ if (!existsSync2(dir))
11374
+ return [];
11375
+ let entries;
11376
+ try {
11377
+ entries = readdirSync(dir);
11378
+ } catch {
11379
+ return [];
11380
+ }
11381
+ const out = [];
11382
+ for (const name of entries) {
11383
+ if (!/\.ya?ml$/i.test(name))
11384
+ continue;
11385
+ const full = resolve2(dir, name);
11386
+ try {
11387
+ if (statSync(full).isFile())
11388
+ out.push(full);
11389
+ } catch {}
11390
+ }
11391
+ return out.sort();
11392
+ }
11393
+ function stampOverlay(entry) {
11394
+ Object.defineProperty(entry, OVERLAY_SOURCE, {
11395
+ value: true,
11396
+ enumerable: false,
11397
+ configurable: false,
11398
+ writable: false
11399
+ });
11400
+ return entry;
11401
+ }
11402
+ function applyAgentOverlays(config) {
11403
+ const warnings = [];
11404
+ const agents = config.agents ?? {};
11405
+ for (const [agentName, agentCfg] of Object.entries(agents)) {
11406
+ try {
11407
+ const scheduleDir = overlayDirFor(agentName, "schedule.d");
11408
+ const files = listYamlFiles(scheduleDir);
11409
+ if (files.length > 0) {
11410
+ const merged = [...agentCfg.schedule ?? []];
11411
+ for (const file of files) {
11412
+ try {
11413
+ const raw = readFileSync(file, "utf-8");
11414
+ const parsed = $parse(raw);
11415
+ const doc = OverlayDocSchema.parse(parsed);
11416
+ for (const entry of doc.schedule ?? []) {
11417
+ if (entry.secrets && entry.secrets.length > 0) {
11418
+ const w = {
11419
+ agent: agentName,
11420
+ file,
11421
+ reason: "Overlay schedule entry declares secrets — dropped pending Phase E operator approval"
11422
+ };
11423
+ warnings.push(w);
11424
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${w.reason}`);
11425
+ continue;
11426
+ }
11427
+ merged.push(stampOverlay(entry));
11428
+ }
11429
+ } catch (err) {
11430
+ const reason = err instanceof ZodError ? `schema rejection: ${err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}` : `parse error: ${err.message}`;
11431
+ warnings.push({ agent: agentName, file, reason });
11432
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${reason}`);
11433
+ }
11434
+ }
11435
+ agentCfg.schedule = merged;
11436
+ }
11437
+ } catch (err) {
11438
+ warnings.push({
11439
+ agent: agentName,
11440
+ file: "(agent schedule overlay scan)",
11441
+ reason: `unexpected error: ${err.message}`
11442
+ });
11443
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' schedule.d: unexpected error: ${err.message}`);
11444
+ }
11445
+ try {
11446
+ const skillsDir = overlayDirFor(agentName, "skills.d");
11447
+ const skillFiles = listYamlFiles(skillsDir);
11448
+ if (skillFiles.length === 0) {} else {
11449
+ const merged = [...agentCfg.skills ?? []];
11450
+ const seen = new Set(merged);
11451
+ for (const file of skillFiles) {
11452
+ try {
11453
+ const raw = readFileSync(file, "utf-8");
11454
+ const parsed = $parse(raw);
11455
+ const doc = OverlayDocSchema.parse(parsed);
11456
+ for (const skillName of doc.skills ?? []) {
11457
+ if (seen.has(skillName))
11458
+ continue;
11459
+ seen.add(skillName);
11460
+ merged.push(skillName);
11461
+ }
11462
+ } catch (err) {
11463
+ const reason = err instanceof ZodError ? `schema rejection: ${err.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ")}` : `parse error: ${err.message}`;
11464
+ warnings.push({ agent: agentName, file, reason });
11465
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' file='${file}': ${reason}`);
11466
+ }
11467
+ }
11468
+ agentCfg.skills = merged;
11469
+ }
11470
+ } catch (err) {
11471
+ warnings.push({
11472
+ agent: agentName,
11473
+ file: "(agent skills overlay scan)",
11474
+ reason: `unexpected error: ${err.message}`
11475
+ });
11476
+ console.warn(`[switchroom] overlay-loader: agent='${agentName}' skills.d: unexpected error: ${err.message}`);
11477
+ }
11478
+ }
11479
+ return { config, warnings };
11480
+ }
11481
+
11291
11482
  // src/config/loader.ts
11292
11483
  class ConfigError extends Error {
11293
11484
  details;
@@ -11303,27 +11494,65 @@ function formatZodErrors(error) {
11303
11494
  return ` ${path}: ${e.message}`;
11304
11495
  });
11305
11496
  }
11497
+ function coerceLegacyGoogleWorkspaceKeys(parsed, filePath) {
11498
+ const stableStringify = (v) => {
11499
+ if (v === null || typeof v !== "object")
11500
+ return JSON.stringify(v);
11501
+ if (Array.isArray(v))
11502
+ return `[${v.map(stableStringify).join(",")}]`;
11503
+ const obj = v;
11504
+ const keys = Object.keys(obj).sort();
11505
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
11506
+ };
11507
+ const aliasInPlace = (obj, where) => {
11508
+ const a = obj.drive;
11509
+ const b = obj.google_workspace;
11510
+ if (a !== undefined && b !== undefined) {
11511
+ if (stableStringify(a) !== stableStringify(b)) {
11512
+ throw new ConfigError(`Both \`drive:\` and \`google_workspace:\` are set on ${where} in ${filePath} with different values.`, [
11513
+ " These are aliases — pick one and remove the other.",
11514
+ " `google_workspace:` is the RFC G canonical key; `drive:` is the legacy alias.",
11515
+ " Allowed during transition: setting both with identical values."
11516
+ ]);
11517
+ }
11518
+ return;
11519
+ }
11520
+ if (a !== undefined && b === undefined)
11521
+ obj.google_workspace = a;
11522
+ if (b !== undefined && a === undefined)
11523
+ obj.drive = b;
11524
+ };
11525
+ aliasInPlace(parsed, "the top level");
11526
+ const agents = parsed.agents;
11527
+ if (agents && typeof agents === "object" && !Array.isArray(agents)) {
11528
+ for (const [name, agent] of Object.entries(agents)) {
11529
+ if (agent && typeof agent === "object" && !Array.isArray(agent)) {
11530
+ aliasInPlace(agent, `agent \`${name}\``);
11531
+ }
11532
+ }
11533
+ }
11534
+ }
11306
11535
  function findConfigFile(startDir) {
11307
11536
  const envPath = process.env.SWITCHROOM_CONFIG;
11308
- const home = homedir();
11309
- const userDir = resolve(home, ".switchroom");
11537
+ const home2 = homedir();
11538
+ const userDir = resolve3(home2, ".switchroom");
11310
11539
  const searchPaths = [
11311
- envPath ? resolve(envPath) : null,
11312
- startDir ? resolve(startDir, "switchroom.yaml") : null,
11313
- startDir ? resolve(startDir, "switchroom.yml") : null,
11314
- startDir ? resolve(startDir, "clerk.yaml") : null,
11315
- startDir ? resolve(startDir, "clerk.yml") : null,
11316
- resolve(process.cwd(), "switchroom.yaml"),
11317
- resolve(process.cwd(), "switchroom.yml"),
11318
- resolve(process.cwd(), "clerk.yaml"),
11319
- resolve(process.cwd(), "clerk.yml"),
11320
- resolve(userDir, "switchroom.yaml"),
11321
- resolve(userDir, "switchroom.yml"),
11322
- resolve(userDir, "clerk.yaml"),
11323
- resolve(userDir, "clerk.yml")
11540
+ envPath ? resolve3(envPath) : null,
11541
+ startDir ? resolve3(startDir, "switchroom.yaml") : null,
11542
+ startDir ? resolve3(startDir, "switchroom.yml") : null,
11543
+ startDir ? resolve3(startDir, "clerk.yaml") : null,
11544
+ startDir ? resolve3(startDir, "clerk.yml") : null,
11545
+ resolve3(process.cwd(), "switchroom.yaml"),
11546
+ resolve3(process.cwd(), "switchroom.yml"),
11547
+ resolve3(process.cwd(), "clerk.yaml"),
11548
+ resolve3(process.cwd(), "clerk.yml"),
11549
+ resolve3(userDir, "switchroom.yaml"),
11550
+ resolve3(userDir, "switchroom.yml"),
11551
+ resolve3(userDir, "clerk.yaml"),
11552
+ resolve3(userDir, "clerk.yml")
11324
11553
  ].filter(Boolean);
11325
11554
  for (const path of searchPaths) {
11326
- if (existsSync(path)) {
11555
+ if (existsSync3(path)) {
11327
11556
  return path;
11328
11557
  }
11329
11558
  }
@@ -11331,12 +11560,12 @@ function findConfigFile(startDir) {
11331
11560
  }
11332
11561
  function loadConfig(configPath) {
11333
11562
  const filePath = configPath ?? findConfigFile();
11334
- if (!existsSync(filePath)) {
11563
+ if (!existsSync3(filePath)) {
11335
11564
  throw new ConfigError(`Config file not found: ${filePath}`);
11336
11565
  }
11337
11566
  let raw;
11338
11567
  try {
11339
- raw = readFileSync(filePath, "utf-8");
11568
+ raw = readFileSync2(filePath, "utf-8");
11340
11569
  } catch (err) {
11341
11570
  throw new ConfigError(`Failed to read config file: ${filePath}`, [
11342
11571
  ` ${err.message}`
@@ -11355,14 +11584,20 @@ function loadConfig(configPath) {
11355
11584
  obj.switchroom = obj.clerk;
11356
11585
  delete obj.clerk;
11357
11586
  }
11587
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
11588
+ coerceLegacyGoogleWorkspaceKeys(parsed, filePath);
11589
+ }
11590
+ let config;
11358
11591
  try {
11359
- return SwitchroomConfigSchema.parse(parsed);
11592
+ config = SwitchroomConfigSchema.parse(parsed);
11360
11593
  } catch (err) {
11361
11594
  if (err instanceof ZodError) {
11362
11595
  throw new ConfigError("Invalid switchroom.yaml configuration", formatZodErrors(err));
11363
11596
  }
11364
11597
  throw err;
11365
11598
  }
11599
+ applyAgentOverlays(config);
11600
+ return config;
11366
11601
  }
11367
11602
 
11368
11603
  // src/scheduler/dispatch.ts
@@ -11657,6 +11892,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
11657
11892
  }
11658
11893
  merged.reactions = combined;
11659
11894
  }
11895
+ if (defaults.resources || merged.resources) {
11896
+ const d = defaults.resources ?? {};
11897
+ const a = merged.resources ?? {};
11898
+ merged.resources = { ...d, ...a };
11899
+ }
11660
11900
  if (defaults.experimental || merged.experimental) {
11661
11901
  const d = defaults.experimental ?? {};
11662
11902
  const a = merged.experimental ?? {};
@@ -11830,17 +12070,17 @@ function createInjectIpcClient(options) {
11830
12070
  return Promise.resolve(true);
11831
12071
  if (closed)
11832
12072
  return Promise.resolve(false);
11833
- return new Promise((resolve2) => {
12073
+ return new Promise((resolve4) => {
11834
12074
  const start = Date.now();
11835
12075
  const timer = setInterval(() => {
11836
12076
  if (connected) {
11837
12077
  clearInterval(timer);
11838
- resolve2(true);
12078
+ resolve4(true);
11839
12079
  return;
11840
12080
  }
11841
12081
  if (closed || Date.now() - start >= timeoutMs) {
11842
12082
  clearInterval(timer);
11843
- resolve2(connected);
12083
+ resolve4(connected);
11844
12084
  }
11845
12085
  }, 50);
11846
12086
  if (typeof timer.unref === "function") {
@@ -11867,8 +12107,8 @@ function createInjectIpcClient(options) {
11867
12107
  import {
11868
12108
  closeSync,
11869
12109
  openSync,
11870
- readFileSync as readFileSync2,
11871
- statSync,
12110
+ readFileSync as readFileSync3,
12111
+ statSync as statSync2,
11872
12112
  unlinkSync,
11873
12113
  writeSync,
11874
12114
  mkdirSync as mkdirSync2
@@ -11876,7 +12116,7 @@ import {
11876
12116
  import { dirname as dirname2 } from "node:path";
11877
12117
  function readContainerBootTimeMs() {
11878
12118
  try {
11879
- const stat1 = readFileSync2("/proc/1/stat", "utf8");
12119
+ const stat1 = readFileSync3("/proc/1/stat", "utf8");
11880
12120
  const lastParen = stat1.lastIndexOf(")");
11881
12121
  if (lastParen < 0)
11882
12122
  return null;
@@ -11884,7 +12124,7 @@ function readContainerBootTimeMs() {
11884
12124
  const starttimeTicks = Number(after[19]);
11885
12125
  if (!Number.isFinite(starttimeTicks))
11886
12126
  return null;
11887
- const procStat = readFileSync2("/proc/stat", "utf8");
12127
+ const procStat = readFileSync3("/proc/stat", "utf8");
11888
12128
  const btimeLine = procStat.split(`
11889
12129
  `).find((l) => l.startsWith("btime "));
11890
12130
  if (!btimeLine)
@@ -11919,7 +12159,7 @@ function acquireLock(path, pid = process.pid, options = {}) {
11919
12159
  }
11920
12160
  if (bootTimeMs != null) {
11921
12161
  try {
11922
- const lockMtime = statSync(path).mtimeMs;
12162
+ const lockMtime = statSync2(path).mtimeMs;
11923
12163
  if (lockMtime < bootTimeMs - FRESHNESS_MARGIN_MS) {
11924
12164
  try {
11925
12165
  unlinkSync(path);
@@ -11930,7 +12170,7 @@ function acquireLock(path, pid = process.pid, options = {}) {
11930
12170
  }
11931
12171
  let holderPid;
11932
12172
  try {
11933
- const raw = readFileSync2(path, "utf8").trim();
12173
+ const raw = readFileSync3(path, "utf8").trim();
11934
12174
  const parsed = Number.parseInt(raw, 10);
11935
12175
  if (Number.isInteger(parsed) && parsed > 0)
11936
12176
  holderPid = parsed;
@@ -12224,7 +12464,7 @@ async function main() {
12224
12464
  process.once("SIGINT", cleanup);
12225
12465
  return;
12226
12466
  }
12227
- const sink = new JsonlAuditSink(resolve2(jsonlPath));
12467
+ const sink = new JsonlAuditSink(resolve4(jsonlPath));
12228
12468
  const ipcClient = createInjectIpcClient({
12229
12469
  socketPath,
12230
12470
  log: (m) => process.stderr.write(`agent-scheduler: ${m}
@@ -12232,7 +12472,7 @@ async function main() {
12232
12472
  });
12233
12473
  const dispatcher = ipcDispatcher(ipcClient);
12234
12474
  const replayWindowMin = Number.parseInt(process.env.SWITCHROOM_AGENT_SCHEDULER_REPLAY_MIN ?? "30", 10);
12235
- const recentFires = readRecentFires(resolve2(jsonlPath));
12475
+ const recentFires = readRecentFires(resolve4(jsonlPath));
12236
12476
  const missed = findMissedFires({
12237
12477
  entries,
12238
12478
  recentFires,