switchroom 0.15.36 → 0.15.38

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 (78) hide show
  1. package/dist/agent-scheduler/index.js +10 -9
  2. package/dist/auth-broker/index.js +9 -9
  3. package/dist/cli/autoaccept-poll.js +13 -7
  4. package/dist/cli/notion-write-pretool.mjs +9 -9
  5. package/dist/cli/switchroom.js +480 -217
  6. package/dist/cli/ui/index.html +87 -17
  7. package/dist/host-control/main.js +10 -10
  8. package/dist/vault/approvals/kernel-server.js +9 -9
  9. package/dist/vault/broker/server.js +9 -9
  10. package/package.json +1 -1
  11. package/profiles/_base/cron-session.sh.hbs +1 -1
  12. package/profiles/_base/start.sh.hbs +1 -1
  13. package/profiles/_shared/agent-self-service.md.hbs +25 -0
  14. package/skills/switchroom-manage/SKILL.md +1 -1
  15. package/skills/switchroom-runtime/SKILL.md +1 -1
  16. package/telegram-plugin/answer-stream.ts +1 -1
  17. package/telegram-plugin/bridge/bridge.ts +50 -1
  18. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  19. package/telegram-plugin/bridge/tool-filter.ts +77 -0
  20. package/telegram-plugin/chat-lock.ts +1 -1
  21. package/telegram-plugin/credits-watch.ts +1 -1
  22. package/telegram-plugin/dist/bridge/bridge.js +60 -3
  23. package/telegram-plugin/dist/gateway/gateway.js +753 -207
  24. package/telegram-plugin/dist/server.js +64 -4
  25. package/telegram-plugin/gateway/auto-classify-mid-turn.ts +1 -1
  26. package/telegram-plugin/gateway/boot-card.ts +5 -1
  27. package/telegram-plugin/gateway/boot-probes.ts +62 -0
  28. package/telegram-plugin/gateway/cron-session.ts +1 -1
  29. package/telegram-plugin/gateway/gateway.ts +254 -15
  30. package/telegram-plugin/gateway/grant-restart.ts +1 -1
  31. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +1 -1
  32. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +1 -1
  33. package/telegram-plugin/gateway/inbound-delivery-machine.ts +1 -1
  34. package/telegram-plugin/gateway/interrupt-defer.ts +1 -1
  35. package/telegram-plugin/gateway/ipc-protocol.ts +12 -0
  36. package/telegram-plugin/gateway/linear-activity.ts +56 -0
  37. package/telegram-plugin/gateway/linear-auth-watch.ts +102 -0
  38. package/telegram-plugin/gateway/linear-setup.ts +196 -0
  39. package/telegram-plugin/gateway/permission-card-origin.ts +62 -0
  40. package/telegram-plugin/gateway/permission-timeout.ts +70 -0
  41. package/telegram-plugin/gateway/prefix-warmup.ts +1 -1
  42. package/telegram-plugin/gateway/webhook-ingest-server.test.ts +1 -1
  43. package/telegram-plugin/gateway/webhook-ingest-server.ts +1 -1
  44. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +1 -1
  45. package/telegram-plugin/interrupt-marker.ts +1 -1
  46. package/telegram-plugin/over-ping-safety-net.ts +1 -1
  47. package/telegram-plugin/scoped-approval.ts +1 -1
  48. package/telegram-plugin/secret-detect/vault-error.ts +1 -1
  49. package/telegram-plugin/silence-poke.ts +2 -2
  50. package/telegram-plugin/silent-reply-anchor.ts +1 -1
  51. package/telegram-plugin/slot-banner-driver.ts +1 -1
  52. package/telegram-plugin/startup-reset.ts +1 -1
  53. package/telegram-plugin/tests/boot-probes-connections.test.ts +66 -0
  54. package/telegram-plugin/tests/gateway-startup-reset.test.ts +1 -1
  55. package/telegram-plugin/tests/inbound-delivery-machine.test.ts +1 -1
  56. package/telegram-plugin/tests/linear-agent-activity.test.ts +77 -0
  57. package/telegram-plugin/tests/linear-agent-setup.test.ts +132 -0
  58. package/telegram-plugin/tests/linear-auth-watch.test.ts +79 -0
  59. package/telegram-plugin/tests/linear-create-issue.test.ts +3 -1
  60. package/telegram-plugin/tests/permission-card-origin.test.ts +97 -0
  61. package/telegram-plugin/tests/permission-card-routing.test.ts +23 -0
  62. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +76 -0
  63. package/telegram-plugin/tests/permission-timeout.test.ts +87 -0
  64. package/telegram-plugin/tests/scoped-approval.test.ts +1 -1
  65. package/telegram-plugin/tests/silence-poke.test.ts +1 -1
  66. package/telegram-plugin/tests/tool-filter.test.ts +87 -0
  67. package/telegram-plugin/tests/turn-flush-safety.test.ts +1 -1
  68. package/telegram-plugin/turn-flush-safety.ts +1 -1
  69. package/telegram-plugin/uat/assertions.ts +1 -1
  70. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +1 -1
  71. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +1 -1
  72. package/telegram-plugin/uat/scenarios/jtbd-fast-ack-dm.test.ts +1 -1
  73. package/telegram-plugin/uat/scenarios/jtbd-fast-trivial-dm.test.ts +2 -2
  74. package/telegram-plugin/uat/scenarios/jtbd-forwarded-burst-dm.test.ts +1 -1
  75. package/telegram-plugin/uat/scenarios/jtbd-memory-survives-restart-dm.test.ts +1 -1
  76. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +1 -1
  77. package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +1 -1
  78. package/telegram-plugin/uat/scenarios/jtbd-wake-audit-content-dm.test.ts +1 -1
@@ -13565,7 +13565,7 @@ var init_schema = __esm(() => {
13565
13565
  ScheduleEntrySchema = exports_external.object({
13566
13566
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
13567
13567
  prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
13568
- kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
13568
+ kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (reference/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt tiering is on by default " + "(SWITCHROOM_CHEAP_CRON=0 is the kill-switch); an action is model-free " + "regardless (the kill-switch governs model tiering, not deterministic " + "actions)."),
13569
13569
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
13570
13570
  action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
13571
13571
  model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
@@ -13574,7 +13574,7 @@ var init_schema = __esm(() => {
13574
13574
  topic: exports_external.union([
13575
13575
  exports_external.string().min(1, "topic alias must be non-empty"),
13576
13576
  exports_external.number().int().positive("topic ID must be a positive integer")
13577
- ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See docs/rfcs/supergroup-mode.md.")
13577
+ ]).optional().describe("Forum topic this cron fires into when the owning agent is in " + "supergroup-owned mode (channels.telegram.chat_id set). Either a " + 'string alias resolved against `topic_aliases` (e.g. "planning") ' + "or a numeric topic ID. Falls back to the agent's `default_topic_id` " + "when unset. Ignored for agents in fleet-shared or dm_only mode. " + "Alias-resolution happens at config-load \u2014 typos surface immediately. " + "See reference/rfcs/supergroup-mode.md.")
13578
13578
  }).superRefine((entry, ctx) => {
13579
13579
  const kind = entry.kind ?? "prompt";
13580
13580
  if (kind === "poll" && !entry.poll) {
@@ -13747,15 +13747,15 @@ var init_schema = __esm(() => {
13747
13747
  webhook_rate_limit: exports_external.object({
13748
13748
  rpm: exports_external.number().int().positive()
13749
13749
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
13750
- webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
13751
- webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
13750
+ webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See reference/rfcs/webhook-via-gateway-socket.md."),
13751
+ webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See reference/rfcs/webhook-cloudflare-edge-lock.md."),
13752
13752
  linear_agent: exports_external.object({
13753
13753
  enabled: exports_external.boolean(),
13754
13754
  token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
13755
13755
  workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace."),
13756
13756
  default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
13757
13757
  }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
13758
- chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
13758
+ chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See reference/rfcs/supergroup-mode.md."),
13759
13759
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
13760
13760
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
13761
13761
  }).optional().superRefine((tg, ctx) => {
@@ -13981,7 +13981,7 @@ var init_schema = __esm(() => {
13981
13981
  drive: AgentGoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 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 \u2014 they live at the top level."),
13982
13982
  google_workspace: AgentGoogleWorkspaceConfigSchema.describe("RFC G canonical key. Per-agent Google Workspace overrides \u2014 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 \u2014 they live at the top level. " + "Mutually exclusive with `drive:` on the same agent (loader fails fast " + "if both are set)."),
13983
13983
  microsoft_workspace: AgentMicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Per-agent Microsoft Workspace " + "override \u2014 pins the Microsoft account this agent reads via the " + "auth-broker (must be a key in top-level `microsoft_accounts:` with " + "this agent in its `enabled_for[]`) and optionally overrides org_mode. " + "microsoft_client_id/secret are not per-agent."),
13984
- notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
13984
+ notion_workspace: AgentNotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Per-agent Notion access. " + "Presence opts the agent IN (launcher scaffolded, MCP entry emitted, " + "broker grants the integration token). Optional `databases:` filter " + "narrows which DBs this agent may read/write \u2014 names must resolve in " + "top-level notion_workspace.databases. Absence opts the agent OUT."),
13985
13985
  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({
13986
13986
  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."),
13987
13987
  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.")
@@ -14116,9 +14116,9 @@ var init_schema = __esm(() => {
14116
14116
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 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."),
14117
14117
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "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)."),
14118
14118
  microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration \u2014 OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
14119
- notion_workspace: NotionWorkspaceConfigSchema.describe("RFC docs/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
14119
+ notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config \u2014 vault key for the integration token, friendly-name \u2192 " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
14120
14120
  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."),
14121
- host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/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)."),
14121
+ 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)."),
14122
14122
  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)."),
14123
14123
  web_service: WebServiceConfigSchema.default({}).describe("Web-service container (dashboard + GitHub-webhook receiver) config. " + "Defaults to managed=false so existing systemd-mode installs are " + "untouched. Set managed: true after cutting over to the " + "`switchroom-web` container \u2014 then `switchroom update` keeps it " + "refreshed. See `switchroom webd install`."),
14124
14124
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
@@ -14140,7 +14140,7 @@ var init_schema = __esm(() => {
14140
14140
  agents: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
14141
14141
  message: "Agent name must start with a letter/digit, contain only lowercase letters/digits/hyphens/underscores, and be at most 51 characters (Telegram callback_data byte limit)"
14142
14142
  }), AgentSchema).describe("Map of agent name to agent configuration"),
14143
- cron: CronConfigSchema.optional().describe("Cheap-cron settings (docs/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
14143
+ cron: CronConfigSchema.optional().describe("Cheap-cron settings (reference/rfcs/cheap-cron-sessions.md). Operator-owned " + "egress allowlist + host-pinned secret bindings for Tier-0 http-diff " + "polls (\u00a76.1). Required to enable any http-diff poll; not agent-writable.")
14144
14144
  });
14145
14145
  });
14146
14146
 
@@ -31570,6 +31570,106 @@ var init_doctor_notion = __esm(() => {
31570
31570
  HEARTBEAT_STALE_MS = 60 * 1000;
31571
31571
  });
31572
31572
 
31573
+ // src/cli/doctor-mcp-secrets.ts
31574
+ function computeMcpSecretRequirements(config) {
31575
+ const cfg = config;
31576
+ const reqs = [];
31577
+ for (const [agent, agentConfig] of Object.entries(cfg.agents ?? {})) {
31578
+ if (!agentConfig)
31579
+ continue;
31580
+ const profileName = agentConfig.extends;
31581
+ const profileMcp = profileName != null && profileName.length > 0 ? cfg.profiles?.[profileName]?.mcp_servers ?? {} : {};
31582
+ const effectiveMcp = {
31583
+ ...cfg.defaults?.mcp_servers ?? {},
31584
+ ...profileMcp,
31585
+ ...agentConfig.mcp_servers ?? {}
31586
+ };
31587
+ for (const [server, entry] of Object.entries(effectiveMcp)) {
31588
+ if (!entry || typeof entry !== "object")
31589
+ continue;
31590
+ const declared = entry.secrets;
31591
+ if (!Array.isArray(declared))
31592
+ continue;
31593
+ const keys = declared.filter((k) => typeof k === "string" && k.length > 0);
31594
+ if (keys.length === 0)
31595
+ continue;
31596
+ reqs.push({ agent, server, keys });
31597
+ }
31598
+ }
31599
+ return reqs;
31600
+ }
31601
+ async function runMcpSecretChecks(config, deps = {}) {
31602
+ const reqs = computeMcpSecretRequirements(config);
31603
+ if (reqs.length === 0)
31604
+ return [];
31605
+ const read = deps.vaultAclReader ?? (async () => ({
31606
+ kind: "unreachable",
31607
+ msg: "no vault reader wired"
31608
+ }));
31609
+ const aclCache = new Map;
31610
+ const readKey = async (key) => {
31611
+ const cached = aclCache.get(key);
31612
+ if (cached)
31613
+ return cached;
31614
+ const r = await read(key);
31615
+ aclCache.set(key, r);
31616
+ return r;
31617
+ };
31618
+ const agentsNeedingKey = new Map;
31619
+ for (const r of reqs) {
31620
+ for (const key of r.keys) {
31621
+ let set = agentsNeedingKey.get(key);
31622
+ if (!set) {
31623
+ set = new Set;
31624
+ agentsNeedingKey.set(key, set);
31625
+ }
31626
+ set.add(r.agent);
31627
+ }
31628
+ }
31629
+ const results = [];
31630
+ for (const r of reqs) {
31631
+ for (const key of r.keys) {
31632
+ const acl = await readKey(key);
31633
+ const name = `mcp-conn:${r.agent}:${r.server}:${key}`;
31634
+ const want = [...agentsNeedingKey.get(key) ?? new Set].sort();
31635
+ if (acl.kind === "unreachable") {
31636
+ results.push({
31637
+ name,
31638
+ status: "warn",
31639
+ detail: `vault-broker unreachable, can't verify '${key}' for MCP '${r.server}': ${acl.msg}`,
31640
+ fix: "Ensure the vault-broker is running and the operator socket is reachable, then re-run doctor."
31641
+ });
31642
+ continue;
31643
+ }
31644
+ if (acl.kind === "not_found") {
31645
+ results.push({
31646
+ name,
31647
+ status: "fail",
31648
+ detail: `agent '${r.agent}' loads MCP '${r.server}' which needs vault key '${key}', but the key is missing \u2014 '${r.server}' is configured but NOT authed and will fail at runtime`,
31649
+ fix: `switchroom vault set ${key} --allow ${want.join(",")} (then provide the value), or remove '${r.server}' from this agent's mcp_servers.`
31650
+ });
31651
+ continue;
31652
+ }
31653
+ if (!acl.allow.includes(r.agent)) {
31654
+ const updated = [...new Set([...acl.allow, r.agent])].sort().join(",");
31655
+ results.push({
31656
+ name,
31657
+ status: "fail",
31658
+ detail: `agent '${r.agent}' loads MCP '${r.server}' but is NOT on the vault ACL for '${key}' \u2014 the broker will deny it at runtime`,
31659
+ fix: `switchroom vault set ${key} --allow ${updated} (vault set overwrites the scope \u2014 re-state the full list including '${r.agent}').`
31660
+ });
31661
+ continue;
31662
+ }
31663
+ results.push({
31664
+ name,
31665
+ status: "ok",
31666
+ detail: `MCP '${r.server}' \u2192 '${key}' present + ACL-allowed`
31667
+ });
31668
+ }
31669
+ }
31670
+ return results;
31671
+ }
31672
+
31573
31673
  // src/cli/doctor-credentials-migration.ts
31574
31674
  import {
31575
31675
  existsSync as realExistsSync5,
@@ -33909,6 +34009,20 @@ function registerDoctorCommand(program3) {
33909
34009
  console.error(`Unknown skill: ${opts.skill}. Supported: mff`);
33910
34010
  process.exit(1);
33911
34011
  }
34012
+ const vaultAclReader = async (key) => {
34013
+ try {
34014
+ const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
34015
+ const result = await getViaBrokerStructured2(key);
34016
+ if (result.kind === "ok") {
34017
+ return { kind: "ok", allow: result.entry.scope?.allow ?? [] };
34018
+ }
34019
+ if (result.kind === "not_found")
34020
+ return { kind: "not_found" };
34021
+ return { kind: "unreachable", msg: result.msg };
34022
+ } catch (err) {
34023
+ return { kind: "unreachable", msg: err.message };
34024
+ }
34025
+ };
33912
34026
  const sections = [
33913
34027
  { title: "Dependencies", results: checkDependencies() },
33914
34028
  { title: "Skills Prerequisites", results: checkSkillsPrerequisites() },
@@ -33950,25 +34064,11 @@ function registerDoctorCommand(program3) {
33950
34064
  },
33951
34065
  {
33952
34066
  title: "Notion (RFC notion-integration)",
33953
- results: await runNotionChecks(config, {
33954
- vaultAclReader: async (key) => {
33955
- try {
33956
- const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
33957
- const result = await getViaBrokerStructured2(key);
33958
- if (result.kind === "ok") {
33959
- return {
33960
- kind: "ok",
33961
- allow: result.entry.scope?.allow ?? []
33962
- };
33963
- }
33964
- if (result.kind === "not_found")
33965
- return { kind: "not_found" };
33966
- return { kind: "unreachable", msg: result.msg };
33967
- } catch (err) {
33968
- return { kind: "unreachable", msg: err.message };
33969
- }
33970
- }
33971
- })
34067
+ results: await runNotionChecks(config, { vaultAclReader })
34068
+ },
34069
+ {
34070
+ title: "MCP Connections (auth)",
34071
+ results: await runMcpSecretChecks(config, { vaultAclReader })
33972
34072
  },
33973
34073
  { title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) },
33974
34074
  { title: "Webkite", results: runWebkiteChecks(config) },
@@ -50592,8 +50692,8 @@ var {
50592
50692
  } = import__.default;
50593
50693
 
50594
50694
  // src/build-info.ts
50595
- var VERSION = "0.15.36";
50596
- var COMMIT_SHA = "18736aec";
50695
+ var VERSION = "0.15.38";
50696
+ var COMMIT_SHA = "d28a331f";
50597
50697
 
50598
50698
  // src/cli/agent.ts
50599
50699
  init_source();
@@ -53134,6 +53234,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
53134
53234
  const switchroomCliPath = "/usr/local/bin/switchroom";
53135
53235
  const resolvedConfigPath = DOCKER_CONFIG_PATH;
53136
53236
  const telegramStateDir = `${DOCKER_AGENT_HOME}/.switchroom/agents/${name}/telegram`;
53237
+ const linearAgentEnabled = agentConfig.channels?.telegram?.linear_agent?.enabled === true;
53137
53238
  const mcpServers = {
53138
53239
  "switchroom-telegram": {
53139
53240
  command: "bun",
@@ -53141,9 +53242,10 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
53141
53242
  env: {
53142
53243
  TELEGRAM_STATE_DIR: telegramStateDir,
53143
53244
  SWITCHROOM_CONFIG: resolvedConfigPath,
53144
- SWITCHROOM_CLI_PATH: switchroomCliPath
53245
+ SWITCHROOM_CLI_PATH: switchroomCliPath,
53246
+ ...linearAgentEnabled ? { SWITCHROOM_TELEGRAM_LINEAR: "1" } : {}
53145
53247
  },
53146
- alwaysLoad: true
53248
+ alwaysLoad: false
53147
53249
  },
53148
53250
  "agent-config": {
53149
53251
  command: switchroomCliPath,
@@ -54203,6 +54305,7 @@ ${body}
54203
54305
  const switchroomCliPath = "/usr/local/bin/switchroom";
54204
54306
  const resolvedConfigPath = DOCKER_CONFIG_PATH;
54205
54307
  const telegramStateDir = `${DOCKER_AGENT_HOME}/.switchroom/agents/${name}/telegram`;
54308
+ const linearAgentEnabled = agentConfig.channels?.telegram?.linear_agent?.enabled === true;
54206
54309
  const mcpServers = {
54207
54310
  "switchroom-telegram": {
54208
54311
  command: "bun",
@@ -54210,9 +54313,10 @@ ${body}
54210
54313
  env: {
54211
54314
  TELEGRAM_STATE_DIR: telegramStateDir,
54212
54315
  SWITCHROOM_CONFIG: resolvedConfigPath,
54213
- SWITCHROOM_CLI_PATH: switchroomCliPath
54316
+ SWITCHROOM_CLI_PATH: switchroomCliPath,
54317
+ ...linearAgentEnabled ? { SWITCHROOM_TELEGRAM_LINEAR: "1" } : {}
54214
54318
  },
54215
- alwaysLoad: true
54319
+ alwaysLoad: false
54216
54320
  },
54217
54321
  "agent-config": {
54218
54322
  command: switchroomCliPath,
@@ -58912,7 +59016,7 @@ function hasGoogleAccountEntry(doc, account) {
58912
59016
  // src/cli/auth-google.ts
58913
59017
  init_helpers();
58914
59018
  function registerAuthGoogleSubcommands(program3, authParent) {
58915
- const google = authParent.command("google").description("Manage Google Workspace accounts shared across agents (RFC G \u2014 see docs/rfcs/google-workspace-generalization.md)");
59019
+ const google = authParent.command("google").description("Manage Google Workspace accounts shared across agents (RFC G \u2014 see reference/rfcs/google-workspace-generalization.md)");
58916
59020
  registerConnect(google, program3);
58917
59021
  registerEnable(google, program3);
58918
59022
  registerDisable(google, program3);
@@ -59696,7 +59800,7 @@ function buildMicrosoftCredentials(opts) {
59696
59800
 
59697
59801
  // src/cli/auth-microsoft.ts
59698
59802
  function registerAuthMicrosoftSubcommands(program3, authParent) {
59699
- const microsoft = authParent.command("microsoft").description("Manage Microsoft 365 accounts shared across agents (RFC #1873 \u2014 see docs/rfcs/microsoft-workspace.md)");
59803
+ const microsoft = authParent.command("microsoft").description("Manage Microsoft 365 accounts shared across agents (RFC #1873 \u2014 see reference/rfcs/microsoft-workspace.md)");
59700
59804
  registerEnable2(microsoft, program3);
59701
59805
  registerDisable2(microsoft, program3);
59702
59806
  registerList2(microsoft, program3);
@@ -68263,9 +68367,22 @@ async function performLinearRefresh(io) {
68263
68367
  function bundleKeyFor(agent) {
68264
68368
  return `linear/${agent}/oauth`;
68265
68369
  }
68370
+ function linearSandboxRefusal(verb, runtime = process.env.SWITCHROOM_RUNTIME) {
68371
+ if (runtime !== "docker")
68372
+ return null;
68373
+ return `'linear-agent ${verb}' is a HOST command \u2014 it writes the vault file directly, which ` + `doesn't work inside an agent container (no mounted vault, no passphrase) and would ` + `silently no-op.
68374
+ \u2022 In-container: use the 'linear_agent_setup' MCP tool (operator-approved).
68375
+ ` + ` \u2022 On the host shell: run this same command there.`;
68376
+ }
68377
+ function refuseInSandbox(verb) {
68378
+ const msg = linearSandboxRefusal(verb);
68379
+ if (msg)
68380
+ fail2(msg);
68381
+ }
68266
68382
  function registerLinearAgentCommand(program3) {
68267
68383
  const linear = program3.command("linear-agent").description("Install an agent into a Linear workspace as a first-class app actor (#2298) \u2014 @-mentionable, delegate-assignable, agent sessions wake it instantly.");
68268
68384
  linear.command("setup").description("Provision <agent> as a Linear agent. Vault-stores the Linear OAuth app token (actor=app) under 'linear/<agent>/token' and enables the linear_agent block in switchroom.yaml. The OAuth browser authorize step is printed as instructions (it can't run headless); pass the already-obtained --token.").requiredOption("--agent <name>", "Agent name (must exist in switchroom.yaml)").requiredOption("--token <token>", "The Linear OAuth app token (actor=app), obtained out-of-band via the browser authorize step. Stored in the vault, never in switchroom.yaml.").option("--client-id <id>", "Linear OAuth app client id. Stored (with --client-secret + --refresh-token) to enable unattended token refresh.").option("--client-secret <secret>", "Linear OAuth app client secret. Stored in the vault (with --client-id + --refresh-token) so the token can be refreshed without a browser re-auth.").option("--refresh-token <token>", "The refresh_token from the OAuth exchange. Stored so an expired access token self-heals (see 'linear-agent refresh').").option("--token-expires-in <seconds>", "expires_in from the OAuth token response (seconds). Records when the access token expires so refresh runs proactively. Defaults to 86400.").option("--redirect-uri <uri>", "OAuth redirect URI registered on the Linear app (for the authorize-URL hint).").option("--workspace-id <id>", "Optional Linear workspace (organization) id to record in config.").option("--webhook-base <url>", "Base URL of the switchroom web server (e.g. https://hooks.switchroom.ai). Used to print the webhook URL to register in Linear. Defaults to a placeholder.").option("--dry-run", "Print the YAML diff + instructions without writing or vaulting anything").action(withConfigError(async (opts) => {
68385
+ refuseInSandbox("setup");
68269
68386
  if (!/^[a-z][a-z0-9_-]{0,63}$/.test(opts.agent)) {
68270
68387
  fail2(`--agent must be a lowercase agent slug (got '${opts.agent}').`);
68271
68388
  }
@@ -68328,6 +68445,7 @@ function registerLinearAgentCommand(program3) {
68328
68445
  printLinearInstructions(opts, vaultKey);
68329
68446
  }));
68330
68447
  linear.command("refresh").description("Refresh <agent>'s Linear app token using the stored refresh bundle (linear/<agent>/oauth). Exchanges the refresh_token for a fresh access token, writes it to linear/<agent>/token, and rotates the stored refresh_token + expiry. Use to recover an expired token or seed automation. Host-side write \u2014 the running agent picks it up on its next broker re-mirror / restart.").requiredOption("--agent <name>", "Agent name (must have a linear_agent block)").action(withConfigError(async (opts) => {
68448
+ refuseInSandbox("refresh");
68331
68449
  if (!/^[a-z][a-z0-9_-]{0,63}$/.test(opts.agent)) {
68332
68450
  fail2(`--agent must be a lowercase agent slug (got '${opts.agent}').`);
68333
68451
  }
@@ -74792,6 +74910,28 @@ function handleGetNotionWorkspace(config) {
74792
74910
  fullAccessAgents
74793
74911
  };
74794
74912
  }
74913
+ function handleGetLinearAgents(config) {
74914
+ const agentNames = Object.keys(config.agents ?? {});
74915
+ const agents = [];
74916
+ for (const name of agentNames) {
74917
+ const rawAgent = config.agents?.[name];
74918
+ if (!rawAgent)
74919
+ continue;
74920
+ const linear = resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.linear_agent;
74921
+ if (!linear?.enabled)
74922
+ continue;
74923
+ const tok = linear.token;
74924
+ const tokenVaultKey = typeof tok === "string" && tok.startsWith("vault:") ? tok : null;
74925
+ agents.push({
74926
+ agent: name,
74927
+ workspaceId: linear.workspace_id ?? null,
74928
+ defaultTeamId: linear.default_team_id ?? null,
74929
+ tokenVaultKey
74930
+ });
74931
+ }
74932
+ agents.sort((a, b) => a.agent.localeCompare(b.agent));
74933
+ return { configured: agents.length > 0, agents };
74934
+ }
74795
74935
  var connectionAccessStatuses = new Map;
74796
74936
  function reapConnectionAccessStatuses(now = Date.now()) {
74797
74937
  for (const [id, s] of connectionAccessStatuses) {
@@ -75863,6 +76003,23 @@ var MIME_TYPES = {
75863
76003
  ".svg": "image/svg+xml",
75864
76004
  ".ico": "image/x-icon"
75865
76005
  };
76006
+ var SPA_TAB_ROUTES = new Set([
76007
+ "summary",
76008
+ "agents",
76009
+ "accounts",
76010
+ "system",
76011
+ "memory",
76012
+ "connections",
76013
+ "schedule",
76014
+ "approvals"
76015
+ ]);
76016
+ function resolveDashboardFilePath(pathname) {
76017
+ const routeName = pathname.replace(/^\/+/, "").replace(/\/+$/, "");
76018
+ return pathname === "/" || SPA_TAB_ROUTES.has(routeName) ? "/index.html" : pathname;
76019
+ }
76020
+ function dashboardCacheControl(ext) {
76021
+ return ext === ".html" ? "no-cache, must-revalidate" : undefined;
76022
+ }
75866
76023
  function jsonResponse(data, status = 200) {
75867
76024
  return new Response(JSON.stringify(data), {
75868
76025
  status,
@@ -76100,6 +76257,9 @@ function parseRoute(pathname, method) {
76100
76257
  if (method === "GET" && pathname === "/api/notion-workspace") {
76101
76258
  return { handler: "getNotionWorkspace", params: {} };
76102
76259
  }
76260
+ if (method === "GET" && pathname === "/api/linear-agents") {
76261
+ return { handler: "getLinearAgents", params: {} };
76262
+ }
76103
76263
  if (method === "GET" && pathname === "/api/schedule") {
76104
76264
  return { handler: "getSchedule", params: {} };
76105
76265
  }
@@ -76191,8 +76351,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
76191
76351
  return new Response("Unauthorized", { status: 401 });
76192
76352
  }
76193
76353
  const wsProto = req.headers.get("Sec-WebSocket-Protocol");
76194
- const headers = wsProto && wsProto.split(",").map((s) => s.trim()).includes("bearer") ? { "Sec-WebSocket-Protocol": "bearer" } : undefined;
76195
- const upgraded = server2.upgrade(req, headers ? { headers } : undefined);
76354
+ const headers2 = wsProto && wsProto.split(",").map((s) => s.trim()).includes("bearer") ? { "Sec-WebSocket-Protocol": "bearer" } : undefined;
76355
+ const upgraded = server2.upgrade(req, headers2 ? { headers: headers2 } : undefined);
76196
76356
  if (!upgraded) {
76197
76357
  return new Response("WebSocket upgrade failed", { status: 400 });
76198
76358
  }
@@ -76280,6 +76440,8 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
76280
76440
  return (async () => jsonResponse(await handleGetMicrosoftAccounts(freshConfig())))();
76281
76441
  case "getNotionWorkspace":
76282
76442
  return jsonResponse(handleGetNotionWorkspace(freshConfig()));
76443
+ case "getLinearAgents":
76444
+ return jsonResponse(handleGetLinearAgents(freshConfig()));
76283
76445
  case "getSchedule":
76284
76446
  return (async () => jsonResponse(withStamp(await cachedSchedule())))();
76285
76447
  case "getApprovals":
@@ -76414,7 +76576,7 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
76414
76576
  }
76415
76577
  }
76416
76578
  }
76417
- let filePath = pathname === "/" ? "/index.html" : pathname;
76579
+ const filePath = resolveDashboardFilePath(pathname);
76418
76580
  const fullPath = join47(uiDir, filePath);
76419
76581
  if (!existsSync50(fullPath)) {
76420
76582
  return new Response("Not Found", { status: 404 });
@@ -76432,9 +76594,11 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
76432
76594
  const ext = extname(realFullPath);
76433
76595
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
76434
76596
  const content = readFileSync46(realFullPath);
76435
- return new Response(content, {
76436
- headers: { "Content-Type": contentType }
76437
- });
76597
+ const headers = { "Content-Type": contentType };
76598
+ const cacheControl = dashboardCacheControl(ext);
76599
+ if (cacheControl)
76600
+ headers["Cache-Control"] = cacheControl;
76601
+ return new Response(content, { headers });
76438
76602
  },
76439
76603
  websocket: {
76440
76604
  open(_ws) {},
@@ -82005,7 +82169,7 @@ async function runNotionMcpLauncher(opts, runtime) {
82005
82169
  return exitCode;
82006
82170
  }
82007
82171
  function registerNotionMcpLauncherCommand(program3) {
82008
- program3.command("notion-mcp-launcher", { hidden: true }).option("--vault-key <key>", "Override the vault key holding the Notion integration token. " + "Defaults to `notion/integration-token`.", DEFAULT_VAULT_KEY).option("--mcp-version <semver>", "Override the @notionhq/notion-mcp-server version to spawn.").option("--heartbeat-path <path>", "Override the heartbeat file path. Default: /state/agent/notion-launcher.heartbeat.json").description("Internal \u2014 Notion MCP launcher. Fetches the integration token from the vault-broker and execs @notionhq/notion-mcp-server in stdio mode. RFC docs/rfcs/notion-integration.md PR 2.").action(async (opts) => {
82172
+ program3.command("notion-mcp-launcher", { hidden: true }).option("--vault-key <key>", "Override the vault key holding the Notion integration token. " + "Defaults to `notion/integration-token`.", DEFAULT_VAULT_KEY).option("--mcp-version <semver>", "Override the @notionhq/notion-mcp-server version to spawn.").option("--heartbeat-path <path>", "Override the heartbeat file path. Default: /state/agent/notion-launcher.heartbeat.json").description("Internal \u2014 Notion MCP launcher. Fetches the integration token from the vault-broker and execs @notionhq/notion-mcp-server in stdio mode. RFC reference/rfcs/notion-integration.md PR 2.").action(async (opts) => {
82009
82173
  const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
82010
82174
  const code = await runNotionMcpLauncher(opts, {
82011
82175
  fetchToken: async () => {
@@ -82478,7 +82642,7 @@ function createNotionApiClient(opts) {
82478
82642
  // src/cli/notion.ts
82479
82643
  init_client();
82480
82644
  function registerNotionCommand(program3) {
82481
- const cmd = program3.command("notion").description("Notion integration operator helpers (list-dbs, test). See docs/rfcs/notion-integration.md.");
82645
+ const cmd = program3.command("notion").description("Notion integration operator helpers (list-dbs, test). See reference/rfcs/notion-integration.md.");
82482
82646
  cmd.command("list-dbs").description("List the databases the Notion integration can access. Output is a ready-to-paste YAML block for `notion_workspace.databases`.").option("--vault-key <key>", "Override the vault key holding the integration token.", "notion/integration-token").action(async (opts) => {
82483
82647
  const code = await runListDbs(opts);
82484
82648
  process.exit(code);
@@ -82668,7 +82832,7 @@ async function fetchToken(vaultKey) {
82668
82832
 
82669
82833
  // src/cli/apply.ts
82670
82834
  init_source();
82671
- import { accessSync as accessSync3, chownSync as chownSync7, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync74, mkdirSync as mkdirSync42, readFileSync as readFileSync63, readdirSync as readdirSync26, renameSync as renameSync14, writeFileSync as writeFileSync37 } from "node:fs";
82835
+ import { accessSync as accessSync3, chownSync as chownSync7, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync74, mkdirSync as mkdirSync43, readFileSync as readFileSync63, readdirSync as readdirSync26, renameSync as renameSync14, writeFileSync as writeFileSync38 } from "node:fs";
82672
82836
  import { mkdir as mkdir2 } from "node:fs/promises";
82673
82837
  import { spawnSync as childSpawnSync } from "node:child_process";
82674
82838
  import readline from "node:readline";
@@ -83057,16 +83221,97 @@ agents:
83057
83221
 
83058
83222
  // src/cli/apply.ts
83059
83223
  init_resolver();
83060
- import { dirname as dirname23, join as join75, resolve as resolve46 } from "node:path";
83224
+ import { dirname as dirname23, join as join76, resolve as resolve46 } from "node:path";
83061
83225
  import { homedir as homedir44 } from "node:os";
83062
83226
  import { execFileSync as execFileSync24 } from "node:child_process";
83063
83227
  init_vault();
83064
83228
  init_loader();
83065
83229
  init_loader();
83066
83230
 
83067
- // src/cli/update-prompt-hook.ts
83068
- import { existsSync as existsSync72, readFileSync as readFileSync62, writeFileSync as writeFileSync36, chmodSync as chmodSync10, mkdirSync as mkdirSync41 } from "node:fs";
83231
+ // src/agents/connection-health.ts
83232
+ import { mkdirSync as mkdirSync41, writeFileSync as writeFileSync36 } from "node:fs";
83069
83233
  import { join as join73 } from "node:path";
83234
+ var CONNECTION_HEALTH_FILENAME = "connection-health.json";
83235
+ async function computeAgentConnectionIssues(config, agentName, vaultAclReader) {
83236
+ const reqs = computeMcpSecretRequirements(config).filter((r) => r.agent === agentName);
83237
+ if (reqs.length === 0)
83238
+ return [];
83239
+ const agentsNeedingKey = new Map;
83240
+ for (const r of computeMcpSecretRequirements(config)) {
83241
+ for (const key of r.keys) {
83242
+ let set = agentsNeedingKey.get(key);
83243
+ if (!set) {
83244
+ set = new Set;
83245
+ agentsNeedingKey.set(key, set);
83246
+ }
83247
+ set.add(r.agent);
83248
+ }
83249
+ }
83250
+ const aclCache = new Map;
83251
+ const readKey = async (key) => {
83252
+ const cached = aclCache.get(key);
83253
+ if (cached)
83254
+ return cached;
83255
+ const r = await vaultAclReader(key);
83256
+ aclCache.set(key, r);
83257
+ return r;
83258
+ };
83259
+ const issues = [];
83260
+ for (const r of reqs) {
83261
+ for (const key of r.keys) {
83262
+ const acl = await readKey(key);
83263
+ const want = [...agentsNeedingKey.get(key) ?? new Set].sort();
83264
+ if (acl.kind === "unreachable")
83265
+ continue;
83266
+ if (acl.kind === "not_found") {
83267
+ issues.push({
83268
+ server: r.server,
83269
+ key,
83270
+ kind: "missing",
83271
+ detail: `MCP '${r.server}' needs vault key '${key}', but it is missing \u2014 configured but not authed`,
83272
+ fix: `switchroom vault set ${key} --allow ${want.join(",")} (then provide the value)`
83273
+ });
83274
+ continue;
83275
+ }
83276
+ if (!acl.allow.includes(agentName)) {
83277
+ const updated = [...new Set([...acl.allow, agentName])].sort().join(",");
83278
+ issues.push({
83279
+ server: r.server,
83280
+ key,
83281
+ kind: "acl",
83282
+ detail: `MCP '${r.server}' not on the vault ACL for '${key}' \u2014 broker will deny at runtime`,
83283
+ fix: `switchroom vault set ${key} --allow ${updated} (re-state the full list)`
83284
+ });
83285
+ }
83286
+ }
83287
+ }
83288
+ return issues;
83289
+ }
83290
+ function writeConnectionHealthFile(agentDir, health, deps) {
83291
+ const dir = join73(agentDir, ".claude");
83292
+ const path7 = join73(dir, CONNECTION_HEALTH_FILENAME);
83293
+ (deps?.mkdir ?? ((p, o) => mkdirSync41(p, o)))(dir, { recursive: true });
83294
+ (deps?.writeFile ?? ((p, d) => writeFileSync36(p, d)))(path7, JSON.stringify(health, null, 2) + `
83295
+ `);
83296
+ }
83297
+ async function refreshAgentConnectionHealth(config, agentName, agentDir, deps) {
83298
+ const now = deps.now ?? Date.now;
83299
+ let issues = [];
83300
+ try {
83301
+ issues = await computeAgentConnectionIssues(config, agentName, deps.vaultAclReader);
83302
+ } catch {
83303
+ issues = [];
83304
+ }
83305
+ const health = { computedAt: now(), issues };
83306
+ try {
83307
+ writeConnectionHealthFile(agentDir, health, deps);
83308
+ } catch {}
83309
+ return health;
83310
+ }
83311
+
83312
+ // src/cli/update-prompt-hook.ts
83313
+ import { existsSync as existsSync72, readFileSync as readFileSync62, writeFileSync as writeFileSync37, chmodSync as chmodSync10, mkdirSync as mkdirSync42 } from "node:fs";
83314
+ import { join as join74 } from "node:path";
83070
83315
  var HOOK_FILENAME = "update-card-on-prompt.sh";
83071
83316
  function updatePromptHookScript() {
83072
83317
  return `#!/bin/bash
@@ -83132,14 +83377,14 @@ exit 0
83132
83377
  `;
83133
83378
  }
83134
83379
  function installUpdatePromptHook(agentDir) {
83135
- const hooksDir = join73(agentDir, ".claude", "hooks");
83136
- mkdirSync41(hooksDir, { recursive: true });
83137
- const scriptPath = join73(hooksDir, HOOK_FILENAME);
83380
+ const hooksDir = join74(agentDir, ".claude", "hooks");
83381
+ mkdirSync42(hooksDir, { recursive: true });
83382
+ const scriptPath = join74(hooksDir, HOOK_FILENAME);
83138
83383
  const desired = updatePromptHookScript();
83139
83384
  let installed = false;
83140
83385
  const existing = existsSync72(scriptPath) ? readFileSync62(scriptPath, "utf-8") : "";
83141
83386
  if (existing !== desired) {
83142
- writeFileSync36(scriptPath, desired, { mode: 493 });
83387
+ writeFileSync37(scriptPath, desired, { mode: 493 });
83143
83388
  chmodSync10(scriptPath, 493);
83144
83389
  installed = true;
83145
83390
  } else {
@@ -83147,7 +83392,7 @@ function installUpdatePromptHook(agentDir) {
83147
83392
  chmodSync10(scriptPath, 493);
83148
83393
  } catch {}
83149
83394
  }
83150
- const settingsPath = join73(agentDir, ".claude", "settings.json");
83395
+ const settingsPath = join74(agentDir, ".claude", "settings.json");
83151
83396
  if (!existsSync72(settingsPath)) {
83152
83397
  return { scriptPath, settingsPath, installed };
83153
83398
  }
@@ -83184,7 +83429,7 @@ function installUpdatePromptHook(agentDir) {
83184
83429
  });
83185
83430
  hooks.UserPromptSubmit = list2;
83186
83431
  parsed.hooks = hooks;
83187
- writeFileSync36(settingsPath, JSON.stringify(parsed, null, 2) + `
83432
+ writeFileSync37(settingsPath, JSON.stringify(parsed, null, 2) + `
83188
83433
  `, { mode: 384 });
83189
83434
  installed = true;
83190
83435
  }
@@ -83249,14 +83494,14 @@ var EMBEDDED_EXAMPLES = {
83249
83494
  switchroom: switchroom_default,
83250
83495
  minimal: minimal_default
83251
83496
  };
83252
- var DEFAULT_COMPOSE_PATH2 = join75(homedir44(), ".switchroom", "compose", "docker-compose.yml");
83497
+ var DEFAULT_COMPOSE_PATH2 = join76(homedir44(), ".switchroom", "compose", "docker-compose.yml");
83253
83498
  var COMPOSE_PROJECT2 = "switchroom";
83254
83499
  function resolveVaultBindMountDir(homeDir, ctx) {
83255
83500
  const isCustomPath = ctx.migrationKind === "custom-path-skipped";
83256
83501
  if (isCustomPath && ctx.customVaultPath) {
83257
83502
  return dirname23(ctx.customVaultPath);
83258
83503
  }
83259
- return join75(homeDir, ".switchroom", "vault");
83504
+ return join76(homeDir, ".switchroom", "vault");
83260
83505
  }
83261
83506
  function inspectVaultBindMountDir(vaultDir) {
83262
83507
  if (!existsSync74(vaultDir))
@@ -83287,61 +83532,61 @@ function hasVaultRefs(value) {
83287
83532
  async function ensureHostMountSources(config) {
83288
83533
  const home2 = homedir44();
83289
83534
  const dirs = [
83290
- join75(home2, ".switchroom", "approvals"),
83291
- join75(home2, ".switchroom", "scheduler"),
83292
- join75(home2, ".switchroom", "logs"),
83293
- join75(home2, ".switchroom", "compose"),
83294
- join75(home2, ".switchroom", "broker-operator")
83535
+ join76(home2, ".switchroom", "approvals"),
83536
+ join76(home2, ".switchroom", "scheduler"),
83537
+ join76(home2, ".switchroom", "logs"),
83538
+ join76(home2, ".switchroom", "compose"),
83539
+ join76(home2, ".switchroom", "broker-operator")
83295
83540
  ];
83296
83541
  for (const name of Object.keys(config.agents)) {
83297
- dirs.push(join75(home2, ".switchroom", "agents", name));
83298
- dirs.push(join75(home2, ".switchroom", "logs", name));
83299
- dirs.push(join75(home2, ".claude", "projects", name));
83300
- dirs.push(join75(home2, ".switchroom", "audit", name));
83301
- if (existsSync74(join75(home2, ".switchroom-config"))) {
83302
- dirs.push(join75(home2, ".switchroom-config", "agents", name, "personal-skills"));
83542
+ dirs.push(join76(home2, ".switchroom", "agents", name));
83543
+ dirs.push(join76(home2, ".switchroom", "logs", name));
83544
+ dirs.push(join76(home2, ".claude", "projects", name));
83545
+ dirs.push(join76(home2, ".switchroom", "audit", name));
83546
+ if (existsSync74(join76(home2, ".switchroom-config"))) {
83547
+ dirs.push(join76(home2, ".switchroom-config", "agents", name, "personal-skills"));
83303
83548
  }
83304
83549
  }
83305
83550
  for (const dir of dirs) {
83306
83551
  await mkdir2(dir, { recursive: true });
83307
83552
  }
83308
- const autoUnlockPath = join75(home2, ".switchroom", "vault-auto-unlock");
83553
+ const autoUnlockPath = join76(home2, ".switchroom", "vault-auto-unlock");
83309
83554
  if (!existsSync74(autoUnlockPath)) {
83310
- writeFileSync37(autoUnlockPath, "", { mode: 384 });
83555
+ writeFileSync38(autoUnlockPath, "", { mode: 384 });
83311
83556
  }
83312
- const auditLogPath = join75(home2, ".switchroom", "vault-audit.log");
83557
+ const auditLogPath = join76(home2, ".switchroom", "vault-audit.log");
83313
83558
  if (!existsSync74(auditLogPath)) {
83314
- writeFileSync37(auditLogPath, "", { mode: 420 });
83559
+ writeFileSync38(auditLogPath, "", { mode: 420 });
83315
83560
  }
83316
- const grantsDbPath = join75(home2, ".switchroom", "vault-grants.db");
83561
+ const grantsDbPath = join76(home2, ".switchroom", "vault-grants.db");
83317
83562
  if (!existsSync74(grantsDbPath)) {
83318
- writeFileSync37(grantsDbPath, "", { mode: 384 });
83563
+ writeFileSync38(grantsDbPath, "", { mode: 384 });
83319
83564
  }
83320
- const hostdAuditLogPath = join75(home2, ".switchroom", "host-control-audit.log");
83565
+ const hostdAuditLogPath = join76(home2, ".switchroom", "host-control-audit.log");
83321
83566
  if (!existsSync74(hostdAuditLogPath)) {
83322
- writeFileSync37(hostdAuditLogPath, "", { mode: 420 });
83567
+ writeFileSync38(hostdAuditLogPath, "", { mode: 420 });
83323
83568
  }
83324
83569
  for (const name of Object.keys(config.agents)) {
83325
- const tokenPath = join75(home2, ".switchroom", "agents", name, ".vault-token");
83570
+ const tokenPath = join76(home2, ".switchroom", "agents", name, ".vault-token");
83326
83571
  if (!existsSync74(tokenPath)) {
83327
- writeFileSync37(tokenPath, "", { mode: 384 });
83572
+ writeFileSync38(tokenPath, "", { mode: 384 });
83328
83573
  }
83329
83574
  try {
83330
83575
  const uid = allocateAgentUid(name);
83331
83576
  chownSync7(tokenPath, uid, uid);
83332
83577
  } catch {}
83333
83578
  }
83334
- const fleetDir = join75(home2, ".switchroom", "fleet");
83579
+ const fleetDir = join76(home2, ".switchroom", "fleet");
83335
83580
  await mkdir2(fleetDir, { recursive: true });
83336
- const invariantsPath = join75(fleetDir, "switchroom-invariants.md");
83581
+ const invariantsPath = join76(fleetDir, "switchroom-invariants.md");
83337
83582
  const invariantsCanonical = renderFleetInvariants();
83338
83583
  const invariantsCurrent = existsSync74(invariantsPath) ? readFileSync63(invariantsPath, "utf-8") : null;
83339
83584
  if (invariantsCurrent !== invariantsCanonical) {
83340
- writeFileSync37(invariantsPath, invariantsCanonical, { mode: 420 });
83585
+ writeFileSync38(invariantsPath, invariantsCanonical, { mode: 420 });
83341
83586
  }
83342
- const fleetClaudePath = join75(fleetDir, "CLAUDE.md");
83587
+ const fleetClaudePath = join76(fleetDir, "CLAUDE.md");
83343
83588
  if (!existsSync74(fleetClaudePath)) {
83344
- writeFileSync37(fleetClaudePath, [
83589
+ writeFileSync38(fleetClaudePath, [
83345
83590
  "# Switchroom fleet defaults",
83346
83591
  "",
83347
83592
  "Operator-owned fleet brain. Every agent reads this via",
@@ -83424,16 +83669,16 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
83424
83669
  }
83425
83670
  function writeInstallTypeCache(homeDir = homedir44()) {
83426
83671
  const ctx = detectInstallType();
83427
- const dir = join75(homeDir, ".switchroom");
83428
- const out = join75(dir, "install-type.json");
83672
+ const dir = join76(homeDir, ".switchroom");
83673
+ const out = join76(dir, "install-type.json");
83429
83674
  const tmp = `${out}.tmp`;
83430
- mkdirSync42(dir, { recursive: true });
83675
+ mkdirSync43(dir, { recursive: true });
83431
83676
  const payload = {
83432
83677
  install_type: ctx.install_type,
83433
83678
  detected_at: new Date().toISOString(),
83434
83679
  source_paths: ctx.source_paths
83435
83680
  };
83436
- writeFileSync37(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
83681
+ writeFileSync38(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
83437
83682
  renameSync14(tmp, out);
83438
83683
  return out;
83439
83684
  }
@@ -83460,6 +83705,20 @@ Applying switchroom config...
83460
83705
  writeOut(source_default.gray(` (--only=${options.only}: scaffolding/aligning this agent only; ` + `compose still covers all ${allAgentNames.length})
83461
83706
  `));
83462
83707
  }
83708
+ const connHealthVaultAclReader = async (key) => {
83709
+ try {
83710
+ const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
83711
+ const result = await getViaBrokerStructured2(key);
83712
+ if (result.kind === "ok") {
83713
+ return { kind: "ok", allow: result.entry.scope?.allow ?? [] };
83714
+ }
83715
+ if (result.kind === "not_found")
83716
+ return { kind: "not_found" };
83717
+ return { kind: "unreachable", msg: result.msg };
83718
+ } catch (err) {
83719
+ return { kind: "unreachable", msg: err.message };
83720
+ }
83721
+ };
83463
83722
  let scaffolded = 0;
83464
83723
  const failures = [];
83465
83724
 
@@ -83474,14 +83733,17 @@ Applying switchroom config...
83474
83733
  writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
83475
83734
  `));
83476
83735
  try {
83477
- installUpdatePromptHook(join75(agentsDir, name));
83736
+ installUpdatePromptHook(join76(agentsDir, name));
83478
83737
  } catch (hookErr) {
83479
83738
  writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
83480
83739
  `));
83481
83740
  }
83741
+ await refreshAgentConnectionHealth(config, name, join76(agentsDir, name), {
83742
+ vaultAclReader: connHealthVaultAclReader
83743
+ });
83482
83744
  try {
83483
83745
  const uid = allocateAgentUid(name);
83484
- alignAgentUid(name, join75(agentsDir, name), uid, {
83746
+ alignAgentUid(name, join76(agentsDir, name), uid, {
83485
83747
  confirm: !options.nonInteractive,
83486
83748
  writeOut
83487
83749
  });
@@ -83489,12 +83751,12 @@ Applying switchroom config...
83489
83751
  const msg = alignErr.message;
83490
83752
  if (options.allowUnaligned) {
83491
83753
  writeOut(source_default.yellow(` ! could not chown ${name} state dir: ${msg}
83492
- ` + ` continuing because --allow-unaligned was passed; agent may fail on first write.
83754
+ continuing because --allow-unaligned was passed; agent may fail on first write.
83493
83755
  `));
83494
83756
  } else {
83495
83757
  writeOut(source_default.red(` x could not chown ${name} state dir: ${msg}
83496
- ` + ` The bind-mounted state dir must be owned by the container's UID or the agent will fail on first write.
83497
- ` + ` Fix: run \`switchroom apply\` from a TTY so it can prompt for sudo, OR run the suggested chown manually, OR re-run with --allow-unaligned to skip this check.
83758
+ The bind-mounted state dir must be owned by the container's UID or the agent will fail on first write.
83759
+ Fix: run \`switchroom apply\` from a TTY so it can prompt for sudo, OR run the suggested chown manually, OR re-run with --allow-unaligned to skip this check.
83498
83760
  `));
83499
83761
  throw new UidAlignmentAbort(`UID alignment failed for agent ${name}; aborting apply (pass --allow-unaligned to override).`);
83500
83762
  }
@@ -83518,14 +83780,14 @@ Applying switchroom config...
83518
83780
  for (const name of agentNames) {
83519
83781
  try {
83520
83782
  const uid = allocateAgentUid(name);
83521
- alignAgentUid(name, join75(agentsDir, name), uid, {
83783
+ alignAgentUid(name, join76(agentsDir, name), uid, {
83522
83784
  confirm: !options.nonInteractive,
83523
83785
  writeOut
83524
83786
  });
83525
83787
  } catch (alignErr) {
83526
83788
  const msg = alignErr.message;
83527
83789
  writeOut(source_default.yellow(` ! post-mount-source UID re-align failed for ${name}: ${msg}
83528
- ` + ` Agent may fail to write supervisor logs on first boot.
83790
+ Agent may fail to write supervisor logs on first boot.
83529
83791
  `));
83530
83792
  }
83531
83793
  }
@@ -83568,7 +83830,7 @@ Applying switchroom config...
83568
83830
  ];
83569
83831
  if (!acceptable.includes(postMigrationInspect.kind)) {
83570
83832
  writeErr(source_default.red(`Post-migration verification failed: state is ${postMigrationInspect.kind}
83571
- ` + `Expected one of: ${acceptable.join(", ")}
83833
+ Expected one of: ${acceptable.join(", ")}
83572
83834
  ` + `This is a switchroom bug \u2014 please file an issue with the apply log.
83573
83835
  `));
83574
83836
  process.exit(5);
@@ -83583,11 +83845,11 @@ Applying switchroom config...
83583
83845
  writeErr(source_default.red(`Vault directory ${vaultDir} contains unexpected files:
83584
83846
  ` + unknown.map((n) => ` - ${n}
83585
83847
  `).join("") + `Refusing to bind-mount: a docker bind-mount source is the
83586
- ` + `entire directory, so unexpected files would be visible inside
83587
- ` + `the broker container. Move them out, then re-run apply.
83588
- ` + `Known artifacts: vault.enc, vault.enc.bak, vault.enc.tmp,
83589
- ` + `vault.enc.lock (PID-file flock from saveVault), and
83590
- ` + `.vault.enc.<pid>.<ms>.tmp (atomicWriteFileSync sibling-tmp).
83848
+ entire directory, so unexpected files would be visible inside
83849
+ the broker container. Move them out, then re-run apply.
83850
+ Known artifacts: vault.enc, vault.enc.bak, vault.enc.tmp,
83851
+ vault.enc.lock (PID-file flock from saveVault), and
83852
+ .vault.enc.<pid>.<ms>.tmp (atomicWriteFileSync sibling-tmp).
83591
83853
  `));
83592
83854
  process.exit(6);
83593
83855
  }
@@ -83606,8 +83868,8 @@ Applying switchroom config...
83606
83868
  Wrote `) + displayComposePath + source_default.gray(` (${composeBytes} bytes)
83607
83869
  `));
83608
83870
  writeOut(`Bring the fleet up with:
83609
- ` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} pull && \\
83610
- ` + ` docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} up -d --remove-orphans
83871
+ docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} pull && \\
83872
+ docker compose -p ${COMPOSE_PROJECT2} -f ${displayComposePath} up -d --remove-orphans
83611
83873
  `);
83612
83874
  writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
83613
83875
  `));
@@ -83666,7 +83928,7 @@ function copyExampleConfig2(name) {
83666
83928
  }
83667
83929
  const embedded = EMBEDDED_EXAMPLES[name];
83668
83930
  if (embedded !== undefined) {
83669
- writeFileSync37(dest, embedded, { encoding: "utf8" });
83931
+ writeFileSync38(dest, embedded, { encoding: "utf8" });
83670
83932
  console.log(source_default.green(`Copied ${name}.yaml -> switchroom.yaml`));
83671
83933
  return;
83672
83934
  }
@@ -83682,7 +83944,7 @@ function findUnwritableAgentDirs(config, opts) {
83682
83944
  const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
83683
83945
  const unwritable = [];
83684
83946
  for (const name of targets) {
83685
- const startSh = join75(agentsDir, name, "start.sh");
83947
+ const startSh = join76(agentsDir, name, "start.sh");
83686
83948
  if (!existsSync74(startSh))
83687
83949
  continue;
83688
83950
  try {
@@ -83729,7 +83991,8 @@ function reexecUnderSudo() {
83729
83991
  if (errCode === "ENOENT") {
83730
83992
  process.stderr.write(source_default.red(`
83731
83993
  ERROR: sudo not found on PATH. Re-run as root, or use
83732
- ` + " `switchroom apply --compose-only` to skip the per-agent\n" + ` scaffold refresh entirely (compose file still regenerates).
83994
+ \`switchroom apply --compose-only\` to skip the per-agent
83995
+ scaffold refresh entirely (compose file still regenerates).
83733
83996
  `));
83734
83997
  process.exit(1);
83735
83998
  }
@@ -83742,7 +84005,7 @@ ERROR: failed to spawn sudo: ${result.error.message}
83742
84005
  process.exit(result.status ?? 1);
83743
84006
  }
83744
84007
  function registerApplyCommand(program3) {
83745
- program3.command("apply").description("Apply switchroom.yaml: scaffold every agent and (re)generate the compose file. Run `docker compose -f <path> up -d` afterwards to bring the fleet up.").option("--build-local [context]", "Dev-only: emit `build:` blocks instead of GHCR `image:` refs so `docker compose up --build` rebuilds from in-tree Dockerfiles. Optional context path (defaults to cwd).").option("-o, --out <path>", `Override compose output path (default: ${DEFAULT_COMPOSE_PATH2}).`).option("--example <name>", "Copy an example config into cwd before applying (e.g., 'switchroom' or 'minimal').").option("--non-interactive", "Skip prompts (e.g. sudo-chown explainer for UID alignment). Use in CI / scripts.").option("--allow-unaligned", "Treat UID-alignment chown failures as warnings instead of hard errors. Unsafe: an unaligned state dir will break the agent on first write. Use only if you know you'll fix ownership out-of-band.").option("--only <agent>", "Restrict scaffold + UID-alignment to a single agent (compose still covers the full fleet). Use during a v0.6 \u2192 v0.7 cutover to migrate agents one at a time without breaking the systemd-managed siblings.").option("--compose-only", "Skip the per-agent scaffold loop entirely; only (re)generate the compose file. Use in CI / scripts that can't chown into per-agent state dirs (mode 0700, owned by per-agent UIDs in v0.7+ docker mode). The full apply still runs preflight + emits compose; only the start.sh / .mcp.json / settings.json refresh is skipped.").option("--no-doctor", "Skip the post-apply doctor sweep that surfaces stale start.sh / unhealthy agents (#929). Default: doctor runs after a successful scaffold so the operator sees whether the v0.7+ post-Phase-4 supervisor block is now in place. `switchroom update` passes this internally to avoid running doctor twice (it has its own doctor step).").addOption(new Option("--channel <c>", "Override the resolved `release` block for this apply run: " + "follow the named channel pointer (dev|rc|latest). Mutually " + "exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved `release` block for this apply run: " + "pin to a specific build (sha-<7-40 hex> or v<semver>). " + "Mutually exclusive with --channel.").conflicts("channel")).option("--print-sudo-cmd", "Print the sudo invocation that `apply` would re-exec itself with when escalation is needed, then exit. Operators who want to script the escalation themselves (CI, custom orchestration) can capture this. Note: tokens are space-separated and not shell-quoted; re-quote arguments if pasting into a shell.").addOption(new Option("--skip-self-elevate").default(false).hideHelp()).action(async (opts) => {
84008
+ program3.command("apply").description("Apply switchroom.yaml: scaffold every agent and (re)generate the compose file. Run `docker compose -f <path> up -d` afterwards to bring the fleet up.").option("--build-local [context]", "Dev-only: emit `build:` blocks instead of GHCR `image:` refs so `docker compose up --build` rebuilds from in-tree Dockerfiles. Optional context path (defaults to cwd).").option("-o, --out <path>", `Override compose output path (default: ${DEFAULT_COMPOSE_PATH2}).`).option("--example <name>", "Copy an example config into cwd before applying (e.g., 'switchroom' or 'minimal').").option("--non-interactive", "Skip prompts (e.g. sudo-chown explainer for UID alignment). Use in CI / scripts.").option("--allow-unaligned", "Treat UID-alignment chown failures as warnings instead of hard errors. Unsafe: an unaligned state dir will break the agent on first write. Use only if you know you'll fix ownership out-of-band.").option("--only <agent>", "Restrict scaffold + UID-alignment to a single agent (compose still covers the full fleet). Use during a v0.6 \u2192 v0.7 cutover to migrate agents one at a time without breaking the systemd-managed siblings.").option("--compose-only", "Skip the per-agent scaffold loop entirely; only (re)generate the compose file. Use in CI / scripts that can't chown into per-agent state dirs (mode 0700, owned by per-agent UIDs in v0.7+ docker mode). The full apply still runs preflight + emits compose; only the start.sh / .mcp.json / settings.json refresh is skipped.").option("--no-doctor", "Skip the post-apply doctor sweep that surfaces stale start.sh / unhealthy agents (#929). Default: doctor runs after a successful scaffold so the operator sees whether the v0.7+ post-Phase-4 supervisor block is now in place. `switchroom update` passes this internally to avoid running doctor twice (it has its own doctor step).").addOption(new Option("--channel <c>", "Override the resolved `release` block for this apply run: follow the named channel pointer (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved `release` block for this apply run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--print-sudo-cmd", "Print the sudo invocation that `apply` would re-exec itself with when escalation is needed, then exit. Operators who want to script the escalation themselves (CI, custom orchestration) can capture this. Note: tokens are space-separated and not shell-quoted; re-quote arguments if pasting into a shell.").addOption(new Option("--skip-self-elevate").default(false).hideHelp()).action(async (opts) => {
83746
84009
  try {
83747
84010
  if (opts.example) {
83748
84011
  copyExampleConfig2(opts.example);
@@ -83768,7 +84031,7 @@ function registerApplyCommand(program3) {
83768
84031
  const canPrompt = !opts.nonInteractive && process.stdin.isTTY === true;
83769
84032
  const proceed = canPrompt ? await confirmYesNo(`Re-exec under sudo to refresh them? [Y/n] `) : true;
83770
84033
  if (!proceed) {
83771
- process.stderr.write(source_default.gray("Skipping. Re-run with --compose-only to regenerate compose " + `without touching per-agent files.
84034
+ process.stderr.write(source_default.gray(`Skipping. Re-run with --compose-only to regenerate compose without touching per-agent files.
83772
84035
  `));
83773
84036
  process.exit(0);
83774
84037
  }
@@ -83862,7 +84125,7 @@ function runRedactStdin() {
83862
84125
 
83863
84126
  // src/cli/status-ask.ts
83864
84127
  import { readFileSync as readFileSync64, existsSync as existsSync75, readdirSync as readdirSync27 } from "node:fs";
83865
- import { join as join76 } from "node:path";
84128
+ import { join as join77 } from "node:path";
83866
84129
  import { homedir as homedir45 } from "node:os";
83867
84130
 
83868
84131
  // src/status-ask/report.ts
@@ -84198,7 +84461,7 @@ function resolveSources(explicitPath) {
84198
84461
  const config = loadConfig();
84199
84462
  agentsDir = resolveAgentsDir(config);
84200
84463
  } catch {
84201
- agentsDir = join76(homedir45(), ".switchroom", "agents");
84464
+ agentsDir = join77(homedir45(), ".switchroom", "agents");
84202
84465
  }
84203
84466
  if (!existsSync75(agentsDir))
84204
84467
  return [];
@@ -84210,7 +84473,7 @@ function resolveSources(explicitPath) {
84210
84473
  return [];
84211
84474
  }
84212
84475
  for (const name of entries) {
84213
- const path8 = join76(agentsDir, name, "runtime-metrics.jsonl");
84476
+ const path8 = join77(agentsDir, name, "runtime-metrics.jsonl");
84214
84477
  if (existsSync75(path8)) {
84215
84478
  sources.push({ path: path8, agent: name });
84216
84479
  }
@@ -84239,7 +84502,7 @@ import {
84239
84502
  closeSync as closeSync13,
84240
84503
  existsSync as existsSync76,
84241
84504
  fsyncSync as fsyncSync6,
84242
- mkdirSync as mkdirSync43,
84505
+ mkdirSync as mkdirSync44,
84243
84506
  openSync as openSync13,
84244
84507
  readdirSync as readdirSync28,
84245
84508
  readFileSync as readFileSync65,
@@ -84248,34 +84511,34 @@ import {
84248
84511
  unlinkSync as unlinkSync14,
84249
84512
  writeSync as writeSync8
84250
84513
  } from "node:fs";
84251
- import { join as join77, resolve as resolve47 } from "node:path";
84514
+ import { join as join78, resolve as resolve47 } from "node:path";
84252
84515
  var STAGING_SUBDIR = ".staging";
84253
84516
  function overlayPathsFor(agent, opts = {}) {
84254
84517
  const base = opts.root ? resolve47(opts.root, agent) : resolve47(resolveDualPath(`~/.switchroom/agents/${agent}`));
84255
- const scheduleDir = join77(base, "schedule.d");
84256
- const scheduleStagingDir = join77(scheduleDir, STAGING_SUBDIR);
84257
- const skillsDir = join77(base, "skills.d");
84258
- const skillsStagingDir = join77(skillsDir, STAGING_SUBDIR);
84518
+ const scheduleDir = join78(base, "schedule.d");
84519
+ const scheduleStagingDir = join78(scheduleDir, STAGING_SUBDIR);
84520
+ const skillsDir = join78(base, "skills.d");
84521
+ const skillsStagingDir = join78(skillsDir, STAGING_SUBDIR);
84259
84522
  return {
84260
84523
  agentRoot: base,
84261
84524
  scheduleDir,
84262
84525
  scheduleStagingDir,
84263
84526
  skillsDir,
84264
84527
  skillsStagingDir,
84265
- lockPath: join77(base, ".lock"),
84528
+ lockPath: join78(base, ".lock"),
84266
84529
  stagingDir: scheduleStagingDir
84267
84530
  };
84268
84531
  }
84269
84532
  function ensureDirs(paths) {
84270
- mkdirSync43(paths.scheduleDir, { recursive: true });
84271
- mkdirSync43(paths.scheduleStagingDir, { recursive: true });
84533
+ mkdirSync44(paths.scheduleDir, { recursive: true });
84534
+ mkdirSync44(paths.scheduleStagingDir, { recursive: true });
84272
84535
  }
84273
84536
  function ensureSkillsDirs(paths) {
84274
- mkdirSync43(paths.skillsDir, { recursive: true });
84275
- mkdirSync43(paths.skillsStagingDir, { recursive: true });
84537
+ mkdirSync44(paths.skillsDir, { recursive: true });
84538
+ mkdirSync44(paths.skillsStagingDir, { recursive: true });
84276
84539
  }
84277
84540
  function withAgentLock(paths, fn) {
84278
- mkdirSync43(paths.agentRoot, { recursive: true });
84541
+ mkdirSync44(paths.agentRoot, { recursive: true });
84279
84542
  const start = Date.now();
84280
84543
  const TIMEOUT_MS = 5000;
84281
84544
  let fd = null;
@@ -84316,8 +84579,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
84316
84579
  const paths = overlayPathsFor(agent, opts);
84317
84580
  return withAgentLock(paths, () => {
84318
84581
  ensureDirs(paths);
84319
- const stagingPath = join77(paths.scheduleStagingDir, `${slug}.yaml`);
84320
- const finalPath = join77(paths.scheduleDir, `${slug}.yaml`);
84582
+ const stagingPath = join78(paths.scheduleStagingDir, `${slug}.yaml`);
84583
+ const finalPath = join78(paths.scheduleDir, `${slug}.yaml`);
84321
84584
  const fd = openSync13(stagingPath, "w", 384);
84322
84585
  try {
84323
84586
  writeSync8(fd, yamlText);
@@ -84333,8 +84596,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
84333
84596
  const paths = overlayPathsFor(agent, opts);
84334
84597
  return withAgentLock(paths, () => {
84335
84598
  ensureSkillsDirs(paths);
84336
- const stagingPath = join77(paths.skillsStagingDir, `${slug}.yaml`);
84337
- const finalPath = join77(paths.skillsDir, `${slug}.yaml`);
84599
+ const stagingPath = join78(paths.skillsStagingDir, `${slug}.yaml`);
84600
+ const finalPath = join78(paths.skillsDir, `${slug}.yaml`);
84338
84601
  const fd = openSync13(stagingPath, "w", 384);
84339
84602
  try {
84340
84603
  writeSync8(fd, yamlText);
@@ -84349,7 +84612,7 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
84349
84612
  function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
84350
84613
  const paths = overlayPathsFor(agent, opts);
84351
84614
  return withAgentLock(paths, () => {
84352
- const finalPath = join77(paths.skillsDir, `${slug}.yaml`);
84615
+ const finalPath = join78(paths.skillsDir, `${slug}.yaml`);
84353
84616
  if (!existsSync76(finalPath))
84354
84617
  return false;
84355
84618
  unlinkSync14(finalPath);
@@ -84364,7 +84627,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
84364
84627
  for (const name of readdirSync28(paths.skillsDir)) {
84365
84628
  if (!/\.ya?ml$/i.test(name))
84366
84629
  continue;
84367
- const full = join77(paths.skillsDir, name);
84630
+ const full = join78(paths.skillsDir, name);
84368
84631
  try {
84369
84632
  const raw = readFileSync65(full, "utf-8");
84370
84633
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -84376,7 +84639,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
84376
84639
  function deleteOverlayEntry(agent, slug, opts = {}) {
84377
84640
  const paths = overlayPathsFor(agent, opts);
84378
84641
  return withAgentLock(paths, () => {
84379
- const finalPath = join77(paths.scheduleDir, `${slug}.yaml`);
84642
+ const finalPath = join78(paths.scheduleDir, `${slug}.yaml`);
84380
84643
  if (!existsSync76(finalPath))
84381
84644
  return false;
84382
84645
  unlinkSync14(finalPath);
@@ -84391,7 +84654,7 @@ function listOverlayEntries(agent, opts = {}) {
84391
84654
  for (const name of readdirSync28(paths.scheduleDir)) {
84392
84655
  if (!/\.ya?ml$/i.test(name))
84393
84656
  continue;
84394
- const full = join77(paths.scheduleDir, name);
84657
+ const full = join78(paths.scheduleDir, name);
84395
84658
  try {
84396
84659
  const raw = readFileSync65(full, "utf-8");
84397
84660
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -84538,25 +84801,25 @@ import {
84538
84801
  closeSync as closeSync14,
84539
84802
  existsSync as existsSync77,
84540
84803
  fsyncSync as fsyncSync7,
84541
- mkdirSync as mkdirSync44,
84804
+ mkdirSync as mkdirSync45,
84542
84805
  openSync as openSync14,
84543
84806
  readdirSync as readdirSync29,
84544
84807
  readFileSync as readFileSync66,
84545
84808
  renameSync as renameSync16,
84546
84809
  unlinkSync as unlinkSync15,
84547
- writeFileSync as writeFileSync38,
84810
+ writeFileSync as writeFileSync39,
84548
84811
  writeSync as writeSync9
84549
84812
  } from "node:fs";
84550
- import { join as join78 } from "node:path";
84813
+ import { join as join79 } from "node:path";
84551
84814
  import { randomBytes as randomBytes14 } from "node:crypto";
84552
84815
  var STAGE_ID_PREFIX = "cap_";
84553
84816
  function pendingDir(agent, opts = {}) {
84554
84817
  const paths = overlayPathsFor(agent, opts);
84555
- return join78(paths.scheduleDir, ".pending");
84818
+ return join79(paths.scheduleDir, ".pending");
84556
84819
  }
84557
84820
  function ensurePendingDir(agent, opts = {}) {
84558
84821
  const dir = pendingDir(agent, opts);
84559
- mkdirSync44(dir, { recursive: true });
84822
+ mkdirSync45(dir, { recursive: true });
84560
84823
  return dir;
84561
84824
  }
84562
84825
  function newStageId() {
@@ -84565,8 +84828,8 @@ function newStageId() {
84565
84828
  function stagePendingScheduleEntry(opts) {
84566
84829
  const dir = ensurePendingDir(opts.agent, { root: opts.root });
84567
84830
  const stageId = opts.stageId ?? newStageId();
84568
- const yamlPath = join78(dir, `${stageId}.yaml`);
84569
- const metaPath = join78(dir, `${stageId}.meta.json`);
84831
+ const yamlPath = join79(dir, `${stageId}.yaml`);
84832
+ const metaPath = join79(dir, `${stageId}.meta.json`);
84570
84833
  const meta = {
84571
84834
  v: 1,
84572
84835
  stage_id: stageId,
@@ -84587,7 +84850,7 @@ function stagePendingScheduleEntry(opts) {
84587
84850
  }
84588
84851
  renameSync16(yamlTmp, yamlPath);
84589
84852
  }
84590
- writeFileSync38(metaPath, JSON.stringify(meta, null, 2) + `
84853
+ writeFileSync39(metaPath, JSON.stringify(meta, null, 2) + `
84591
84854
  `, { mode: 384 });
84592
84855
  return { stageId, yamlPath, metaPath };
84593
84856
  }
@@ -84600,8 +84863,8 @@ function listPendingScheduleEntries(agent, opts = {}) {
84600
84863
  if (!name.endsWith(".meta.json"))
84601
84864
  continue;
84602
84865
  const stageId = name.slice(0, -".meta.json".length);
84603
- const metaPath = join78(dir, name);
84604
- const yamlPath = join78(dir, `${stageId}.yaml`);
84866
+ const metaPath = join79(dir, name);
84867
+ const yamlPath = join79(dir, `${stageId}.yaml`);
84605
84868
  if (!existsSync77(yamlPath))
84606
84869
  continue;
84607
84870
  try {
@@ -84620,7 +84883,7 @@ function commitPendingScheduleEntry(opts) {
84620
84883
  return { committed: false, reason: "not_found" };
84621
84884
  const slug = match.meta.entry.name ?? match.stageId;
84622
84885
  const paths = overlayPathsFor(opts.agent, { root: opts.root });
84623
- const finalPath = join78(paths.scheduleDir, `${slug}.yaml`);
84886
+ const finalPath = join79(paths.scheduleDir, `${slug}.yaml`);
84624
84887
  if (existsSync77(finalPath)) {
84625
84888
  return { committed: false, reason: "slug_collision" };
84626
84889
  }
@@ -85253,7 +85516,7 @@ var import_yaml21 = __toESM(require_dist(), 1);
85253
85516
  import { existsSync as existsSync79 } from "node:fs";
85254
85517
  init_reconcile_default_skills();
85255
85518
  var import_yaml22 = __toESM(require_dist(), 1);
85256
- import { join as join79 } from "node:path";
85519
+ import { join as join80 } from "node:path";
85257
85520
  var MAX_SKILLS_PER_AGENT = 20;
85258
85521
  var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
85259
85522
  function exitCodeFor2(code) {
@@ -85328,7 +85591,7 @@ function skillInstall(opts) {
85328
85591
  return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
85329
85592
  }
85330
85593
  const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
85331
- const skillPath = join79(poolDir, skillName);
85594
+ const skillPath = join80(poolDir, skillName);
85332
85595
  if (!existsSync79(skillPath)) {
85333
85596
  return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
85334
85597
  }
@@ -85495,7 +85758,7 @@ import {
85495
85758
  closeSync as closeSync15,
85496
85759
  existsSync as existsSync80,
85497
85760
  lstatSync as lstatSync9,
85498
- mkdirSync as mkdirSync45,
85761
+ mkdirSync as mkdirSync46,
85499
85762
  mkdtempSync as mkdtempSync5,
85500
85763
  openSync as openSync15,
85501
85764
  readFileSync as readFileSync68,
@@ -85504,10 +85767,10 @@ import {
85504
85767
  renameSync as renameSync17,
85505
85768
  rmSync as rmSync16,
85506
85769
  statSync as statSync32,
85507
- writeFileSync as writeFileSync39
85770
+ writeFileSync as writeFileSync40
85508
85771
  } from "node:fs";
85509
85772
  import { tmpdir as tmpdir5, homedir as homedir46 } from "node:os";
85510
- import { dirname as dirname24, join as join80, relative as relative2, resolve as resolve48 } from "node:path";
85773
+ import { dirname as dirname24, join as join81, relative as relative2, resolve as resolve48 } from "node:path";
85511
85774
  import { spawnSync as spawnSync12 } from "node:child_process";
85512
85775
 
85513
85776
  // src/cli/skill-common.ts
@@ -85701,7 +85964,7 @@ function scanForClaudeP2(content) {
85701
85964
  function resolveSkillsPoolDir2(override) {
85702
85965
  const raw = override ?? "~/.switchroom/skills";
85703
85966
  if (raw.startsWith("~/")) {
85704
- return join80(homedir46(), raw.slice(2));
85967
+ return join81(homedir46(), raw.slice(2));
85705
85968
  }
85706
85969
  if (raw === "~")
85707
85970
  return homedir46();
@@ -85740,7 +86003,7 @@ function loadFromDir(dir) {
85740
86003
  const walk2 = (sub) => {
85741
86004
  const entries = readdirSync30(sub, { withFileTypes: true });
85742
86005
  for (const ent of entries) {
85743
- const full = join80(sub, ent.name);
86006
+ const full = join81(sub, ent.name);
85744
86007
  const rel = relative2(abs, full);
85745
86008
  if (ent.isSymbolicLink()) {
85746
86009
  fail3(`refusing to read symlink inside --from dir: ${rel}`);
@@ -85775,7 +86038,7 @@ function loadFromTarball(tarPath) {
85775
86038
  fail3(`tarball contains disallowed path: ${JSON.stringify(entry)} \u2014 ` + `refusing to extract before any file is written`);
85776
86039
  }
85777
86040
  }
85778
- const staging = mkdtempSync5(join80(tmpdir5(), "skill-apply-extract-"));
86041
+ const staging = mkdtempSync5(join81(tmpdir5(), "skill-apply-extract-"));
85779
86042
  try {
85780
86043
  const flags = isGz ? ["-xzf"] : ["-xf"];
85781
86044
  const r = spawnSync12("tar", [
@@ -85861,10 +86124,10 @@ function validatePayload(name, files) {
85861
86124
  errors2.push(`${path8} fails \`bash -n\` syntax check: ${(r.stderr ?? "").trim()}`);
85862
86125
  }
85863
86126
  } else if (PY_SCRIPT_RE2.test(path8)) {
85864
- const tmp = mkdtempSync5(join80(tmpdir5(), "skill-apply-py-"));
85865
- const tmpPy = join80(tmp, "check.py");
86127
+ const tmp = mkdtempSync5(join81(tmpdir5(), "skill-apply-py-"));
86128
+ const tmpPy = join81(tmp, "check.py");
85866
86129
  try {
85867
- writeFileSync39(tmpPy, content);
86130
+ writeFileSync40(tmpPy, content);
85868
86131
  const r = spawnSync12("python3", ["-m", "py_compile", tmpPy], {
85869
86132
  encoding: "utf-8"
85870
86133
  });
@@ -85885,7 +86148,7 @@ function diffSummary(currentDir, files) {
85885
86148
  if (existsSync80(currentDir)) {
85886
86149
  const walk2 = (sub) => {
85887
86150
  for (const ent of readdirSync30(sub, { withFileTypes: true })) {
85888
- const full = join80(sub, ent.name);
86151
+ const full = join81(sub, ent.name);
85889
86152
  const rel = relative2(currentDir, full);
85890
86153
  if (ent.isDirectory()) {
85891
86154
  walk2(full);
@@ -85919,9 +86182,9 @@ function diffSummary(currentDir, files) {
85919
86182
  }
85920
86183
  function writePayload(poolDir, name, files) {
85921
86184
  if (!existsSync80(poolDir)) {
85922
- mkdirSync45(poolDir, { recursive: true, mode: 493 });
86185
+ mkdirSync46(poolDir, { recursive: true, mode: 493 });
85923
86186
  }
85924
- const target = join80(poolDir, name);
86187
+ const target = join81(poolDir, name);
85925
86188
  let targetIsSymlink = false;
85926
86189
  try {
85927
86190
  const st = lstatSync9(target);
@@ -85932,15 +86195,15 @@ function writePayload(poolDir, name, files) {
85932
86195
  if (targetIsSymlink) {
85933
86196
  fail3(`refusing to overwrite symlink at ${target}; investigate manually`);
85934
86197
  }
85935
- const staging = mkdtempSync5(join80(poolDir, `.skill-apply-stage-${name}-`));
86198
+ const staging = mkdtempSync5(join81(poolDir, `.skill-apply-stage-${name}-`));
85936
86199
  let oldRename = null;
85937
86200
  try {
85938
86201
  for (const [path8, content] of Object.entries(files)) {
85939
- const full = join80(staging, path8);
85940
- mkdirSync45(dirname24(full), { recursive: true, mode: 493 });
86202
+ const full = join81(staging, path8);
86203
+ mkdirSync46(dirname24(full), { recursive: true, mode: 493 });
85941
86204
  const fd = openSync15(full, "wx");
85942
86205
  try {
85943
- writeFileSync39(fd, content);
86206
+ writeFileSync40(fd, content);
85944
86207
  } finally {
85945
86208
  closeSync15(fd);
85946
86209
  }
@@ -86014,7 +86277,7 @@ function registerSkillCommand(program3) {
86014
86277
  }
86015
86278
  const config = loadConfig();
86016
86279
  const poolDir = resolveSkillsPoolDir2(config.switchroom?.skills_dir);
86017
- const currentDir = join80(poolDir, name);
86280
+ const currentDir = join81(poolDir, name);
86018
86281
  console.log(source_default.bold(`Skill: ${name}`) + source_default.gray(` (${Object.keys(files).length} files, ${sumBytes(files)} bytes)`));
86019
86282
  console.log(source_default.bold("Diff vs current pool content:"));
86020
86283
  console.log(diffSummary(currentDir, files));
@@ -86047,7 +86310,7 @@ import {
86047
86310
  closeSync as closeSync16,
86048
86311
  existsSync as existsSync81,
86049
86312
  lstatSync as lstatSync10,
86050
- mkdirSync as mkdirSync46,
86313
+ mkdirSync as mkdirSync47,
86051
86314
  mkdtempSync as mkdtempSync6,
86052
86315
  openSync as openSync16,
86053
86316
  readFileSync as readFileSync69,
@@ -86056,9 +86319,9 @@ import {
86056
86319
  rmSync as rmSync17,
86057
86320
  statSync as statSync33,
86058
86321
  utimesSync,
86059
- writeFileSync as writeFileSync40
86322
+ writeFileSync as writeFileSync41
86060
86323
  } from "node:fs";
86061
- import { dirname as dirname25, join as join81, relative as relative3, resolve as resolve49 } from "node:path";
86324
+ import { dirname as dirname25, join as join82, relative as relative3, resolve as resolve49 } from "node:path";
86062
86325
  import { homedir as homedir47, tmpdir as tmpdir6 } from "node:os";
86063
86326
  import { spawnSync as spawnSync13 } from "node:child_process";
86064
86327
  init_helpers();
@@ -86069,10 +86332,10 @@ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
86069
86332
  var PERSONAL_SKILLS_SUBPATH = "personal-skills";
86070
86333
  function resolveConfigSkillsDir(agent) {
86071
86334
  const override = process.env.SWITCHROOM_CONFIG_DIR;
86072
- const candidate = override ? resolve49(override) : join81(homedir47(), ".switchroom-config");
86335
+ const candidate = override ? resolve49(override) : join82(homedir47(), ".switchroom-config");
86073
86336
  if (!existsSync81(candidate))
86074
86337
  return null;
86075
- return join81(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
86338
+ return join82(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
86076
86339
  }
86077
86340
  var MIRROR_PRIOR_TTL_MS = 24 * 60 * 60 * 1000;
86078
86341
  function sweepMirrorPriors(configSkillsRoot) {
@@ -86090,7 +86353,7 @@ function sweepMirrorPriors(configSkillsRoot) {
86090
86353
  if (now - ts < MIRROR_PRIOR_TTL_MS)
86091
86354
  continue;
86092
86355
  try {
86093
- rmSync17(join81(configSkillsRoot, ent), { recursive: true, force: true });
86356
+ rmSync17(join82(configSkillsRoot, ent), { recursive: true, force: true });
86094
86357
  } catch {}
86095
86358
  }
86096
86359
  } catch {}
@@ -86099,7 +86362,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
86099
86362
  const configSkillsRoot = resolveConfigSkillsDir(agent);
86100
86363
  if (!configSkillsRoot)
86101
86364
  return;
86102
- const dest = join81(configSkillsRoot, name);
86365
+ const dest = join82(configSkillsRoot, name);
86103
86366
  try {
86104
86367
  if (liveSkillDir !== null) {
86105
86368
  try {
@@ -86114,31 +86377,31 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
86114
86377
  if (liveSkillDir === null) {
86115
86378
  sweepMirrorPriors(configSkillsRoot);
86116
86379
  if (existsSync81(dest)) {
86117
- const trash = join81(configSkillsRoot, `.${name}-trash-${Date.now()}`);
86380
+ const trash = join82(configSkillsRoot, `.${name}-trash-${Date.now()}`);
86118
86381
  renameSync18(dest, trash);
86119
86382
  }
86120
86383
  return;
86121
86384
  }
86122
- mkdirSync46(configSkillsRoot, { recursive: true, mode: 493 });
86385
+ mkdirSync47(configSkillsRoot, { recursive: true, mode: 493 });
86123
86386
  sweepMirrorPriors(configSkillsRoot);
86124
- const staging = mkdtempSync6(join81(configSkillsRoot, `.${name}-staging-`));
86387
+ const staging = mkdtempSync6(join82(configSkillsRoot, `.${name}-staging-`));
86125
86388
  const walk2 = (src, dst) => {
86126
- mkdirSync46(dst, { recursive: true, mode: 493 });
86389
+ mkdirSync47(dst, { recursive: true, mode: 493 });
86127
86390
  for (const ent of readdirSync31(src, { withFileTypes: true })) {
86128
- const s = join81(src, ent.name);
86129
- const d = join81(dst, ent.name);
86391
+ const s = join82(src, ent.name);
86392
+ const d = join82(dst, ent.name);
86130
86393
  if (ent.isSymbolicLink())
86131
86394
  continue;
86132
86395
  if (ent.isDirectory())
86133
86396
  walk2(s, d);
86134
86397
  else if (ent.isFile()) {
86135
- writeFileSync40(d, readFileSync69(s));
86398
+ writeFileSync41(d, readFileSync69(s));
86136
86399
  }
86137
86400
  }
86138
86401
  };
86139
86402
  walk2(liveSkillDir, staging);
86140
86403
  if (existsSync81(dest)) {
86141
- const prior = join81(configSkillsRoot, `.${name}-prior-${Date.now()}`);
86404
+ const prior = join82(configSkillsRoot, `.${name}-prior-${Date.now()}`);
86142
86405
  renameSync18(dest, prior);
86143
86406
  }
86144
86407
  renameSync18(staging, dest);
@@ -86165,13 +86428,13 @@ function resolveAgent(opts) {
86165
86428
  function resolveAgentsRoot(opts) {
86166
86429
  if (opts.root)
86167
86430
  return resolve49(opts.root);
86168
- return join81(homedir47(), ".switchroom", "agents");
86431
+ return join82(homedir47(), ".switchroom", "agents");
86169
86432
  }
86170
86433
  function personalSkillDir(agentsRoot, agent, name) {
86171
- return join81(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
86434
+ return join82(agentsRoot, agent, ".claude", "skills", PERSONAL_PREFIX + name);
86172
86435
  }
86173
86436
  function trashDir(agentsRoot, agent) {
86174
- return join81(agentsRoot, agent, ".claude", TRASH_DIRNAME);
86437
+ return join82(agentsRoot, agent, ".claude", TRASH_DIRNAME);
86175
86438
  }
86176
86439
  function readStdinSync2() {
86177
86440
  const chunks = [];
@@ -86201,7 +86464,7 @@ function loadFromDir2(dir) {
86201
86464
  const files = {};
86202
86465
  const walk2 = (sub) => {
86203
86466
  for (const ent of readdirSync31(sub, { withFileTypes: true })) {
86204
- const full = join81(sub, ent.name);
86467
+ const full = join82(sub, ent.name);
86205
86468
  if (ent.isSymbolicLink()) {
86206
86469
  fail4(`refusing to read symlink in --from dir: ${relative3(abs, full)}`);
86207
86470
  }
@@ -86254,10 +86517,10 @@ function behavioralValidate(files) {
86254
86517
  errors2.push(`${path8} fails \`bash -n\`: ${(r.stderr ?? "").trim()}`);
86255
86518
  }
86256
86519
  } else if (PY_SCRIPT_RE.test(path8)) {
86257
- const tmp = mkdtempSync6(join81(tmpdir6(), "skill-personal-py-"));
86258
- const tmpPy = join81(tmp, "check.py");
86520
+ const tmp = mkdtempSync6(join82(tmpdir6(), "skill-personal-py-"));
86521
+ const tmpPy = join82(tmp, "check.py");
86259
86522
  try {
86260
- writeFileSync40(tmpPy, content);
86523
+ writeFileSync41(tmpPy, content);
86261
86524
  const r = spawnSync13("python3", ["-m", "py_compile", tmpPy], {
86262
86525
  encoding: "utf-8"
86263
86526
  });
@@ -86279,7 +86542,7 @@ function sweepTrash(agentsRoot, agent) {
86279
86542
  for (const ent of readdirSync31(trash, { withFileTypes: true })) {
86280
86543
  if (!ent.isDirectory())
86281
86544
  continue;
86282
- const entPath = join81(trash, ent.name);
86545
+ const entPath = join82(trash, ent.name);
86283
86546
  try {
86284
86547
  const st = statSync33(entPath);
86285
86548
  if (now - st.mtimeMs > TRASH_TTL_MS) {
@@ -86299,16 +86562,16 @@ function writePersonalSkill(targetDir, files) {
86299
86562
  if (targetIsSymlink) {
86300
86563
  fail4(`refusing to overwrite symlink at ${targetDir}; investigate manually`);
86301
86564
  }
86302
- mkdirSync46(dirname25(targetDir), { recursive: true, mode: 493 });
86303
- const staging = mkdtempSync6(join81(dirname25(targetDir), `.skill-personal-stage-`));
86565
+ mkdirSync47(dirname25(targetDir), { recursive: true, mode: 493 });
86566
+ const staging = mkdtempSync6(join82(dirname25(targetDir), `.skill-personal-stage-`));
86304
86567
  let oldRename = null;
86305
86568
  try {
86306
86569
  for (const [path8, content] of Object.entries(files)) {
86307
- const full = join81(staging, path8);
86308
- mkdirSync46(dirname25(full), { recursive: true, mode: 493 });
86570
+ const full = join82(staging, path8);
86571
+ mkdirSync47(dirname25(full), { recursive: true, mode: 493 });
86309
86572
  const fd = openSync16(full, "wx");
86310
86573
  try {
86311
- writeFileSync40(fd, content);
86574
+ writeFileSync41(fd, content);
86312
86575
  } finally {
86313
86576
  closeSync16(fd);
86314
86577
  }
@@ -86437,10 +86700,10 @@ function editPersonalAction(name, opts) {
86437
86700
  }
86438
86701
  var CLONE_SOURCE_RE = /^(shared|bundled):([a-z0-9][a-z0-9_-]{0,62})$/;
86439
86702
  function defaultSharedRoot() {
86440
- return join81(homedir47(), ".switchroom", "skills");
86703
+ return join82(homedir47(), ".switchroom", "skills");
86441
86704
  }
86442
86705
  function defaultBundledRoot() {
86443
- return join81(homedir47(), ".switchroom", "skills", "_bundled");
86706
+ return join82(homedir47(), ".switchroom", "skills", "_bundled");
86444
86707
  }
86445
86708
  function resolveCloneSource(source, opts) {
86446
86709
  const m = CLONE_SOURCE_RE.exec(source);
@@ -86450,7 +86713,7 @@ function resolveCloneSource(source, opts) {
86450
86713
  const tier = m[1];
86451
86714
  const slug = m[2];
86452
86715
  const root = tier === "bundled" ? opts.bundledRoot ?? defaultBundledRoot() : opts.sharedRoot ?? defaultSharedRoot();
86453
- const dir = join81(root, slug);
86716
+ const dir = join82(root, slug);
86454
86717
  if (!existsSync81(dir)) {
86455
86718
  fail4(`clone source ${JSON.stringify(source)} not found at ${dir}; ` + `check \`switchroom skill search --tier ${tier}\``, 1);
86456
86719
  }
@@ -86466,7 +86729,7 @@ function readSourceFiles(dir) {
86466
86729
  const skipped = [];
86467
86730
  const walk2 = (sub) => {
86468
86731
  for (const ent of readdirSync31(sub, { withFileTypes: true })) {
86469
- const full = join81(sub, ent.name);
86732
+ const full = join82(sub, ent.name);
86470
86733
  if (ent.isSymbolicLink()) {
86471
86734
  continue;
86472
86735
  }
@@ -86575,9 +86838,9 @@ function removePersonalAction(name, opts) {
86575
86838
  throw err2;
86576
86839
  }
86577
86840
  const trashRoot = trashDir(agentsRoot, agent);
86578
- mkdirSync46(trashRoot, { recursive: true, mode: 493 });
86841
+ mkdirSync47(trashRoot, { recursive: true, mode: 493 });
86579
86842
  const ts = Date.now();
86580
- const trashTarget = join81(trashRoot, `${name}-${ts}`);
86843
+ const trashTarget = join82(trashRoot, `${name}-${ts}`);
86581
86844
  renameSync18(target, trashTarget);
86582
86845
  const now = new Date(ts);
86583
86846
  utimesSync(trashTarget, now, now);
@@ -86596,7 +86859,7 @@ function listPersonalAction(opts) {
86596
86859
  const agent = resolveAgent(opts);
86597
86860
  const agentsRoot = resolveAgentsRoot(opts);
86598
86861
  sweepTrash(agentsRoot, agent);
86599
- const skillsDir = join81(agentsRoot, agent, ".claude", "skills");
86862
+ const skillsDir = join82(agentsRoot, agent, ".claude", "skills");
86600
86863
  const personal = [];
86601
86864
  if (existsSync81(skillsDir)) {
86602
86865
  for (const ent of readdirSync31(skillsDir, { withFileTypes: true })) {
@@ -86605,7 +86868,7 @@ function listPersonalAction(opts) {
86605
86868
  if (!ent.name.startsWith(PERSONAL_PREFIX))
86606
86869
  continue;
86607
86870
  const skillName = ent.name.slice(PERSONAL_PREFIX.length);
86608
- const skillPath = join81(skillsDir, ent.name);
86871
+ const skillPath = join82(skillsDir, ent.name);
86609
86872
  let fileCount = 0;
86610
86873
  let totalBytes = 0;
86611
86874
  const walk2 = (sub) => {
@@ -86613,10 +86876,10 @@ function listPersonalAction(opts) {
86613
86876
  if (e.isFile()) {
86614
86877
  fileCount += 1;
86615
86878
  try {
86616
- totalBytes += statSync33(join81(sub, e.name)).size;
86879
+ totalBytes += statSync33(join82(sub, e.name)).size;
86617
86880
  } catch {}
86618
86881
  } else if (e.isDirectory()) {
86619
- walk2(join81(sub, e.name));
86882
+ walk2(join82(sub, e.name));
86620
86883
  }
86621
86884
  }
86622
86885
  };
@@ -86657,7 +86920,7 @@ init_helpers();
86657
86920
  var import_yaml24 = __toESM(require_dist(), 1);
86658
86921
  import { existsSync as existsSync82, readdirSync as readdirSync32, readFileSync as readFileSync70, statSync as statSync34 } from "node:fs";
86659
86922
  import { homedir as homedir48 } from "node:os";
86660
- import { join as join82, resolve as resolve50 } from "node:path";
86923
+ import { join as join83, resolve as resolve50 } from "node:path";
86661
86924
  var PERSONAL_PREFIX2 = "personal-";
86662
86925
  var BUNDLED_SUBDIR = "_bundled";
86663
86926
  var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
@@ -86671,7 +86934,7 @@ function defaultBundledRoot2() {
86671
86934
  return resolve50(homedir48(), ".switchroom/skills/_bundled");
86672
86935
  }
86673
86936
  function readSkillFrontmatter(skillDir) {
86674
- const mdPath = join82(skillDir, "SKILL.md");
86937
+ const mdPath = join83(skillDir, "SKILL.md");
86675
86938
  if (!existsSync82(mdPath))
86676
86939
  return null;
86677
86940
  let content;
@@ -86704,7 +86967,7 @@ function readSkillFrontmatter(skillDir) {
86704
86967
  return { fm: parsed };
86705
86968
  }
86706
86969
  function statSkillMd(skillDir) {
86707
- const mdPath = join82(skillDir, "SKILL.md");
86970
+ const mdPath = join83(skillDir, "SKILL.md");
86708
86971
  try {
86709
86972
  const st = statSync34(mdPath);
86710
86973
  return { size: st.size, mtime: st.mtime.toISOString() };
@@ -86715,7 +86978,7 @@ function statSkillMd(skillDir) {
86715
86978
  function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
86716
86979
  if (!AGENT_NAME_RE3.test(agent))
86717
86980
  return [];
86718
- const skillsDir = join82(agentsRoot, agent, ".claude/skills");
86981
+ const skillsDir = join83(agentsRoot, agent, ".claude/skills");
86719
86982
  if (!existsSync82(skillsDir))
86720
86983
  return [];
86721
86984
  const out = [];
@@ -86728,7 +86991,7 @@ function listPersonalSkills(agent, agentsRoot = defaultAgentsRoot()) {
86728
86991
  for (const ent of entries) {
86729
86992
  if (!ent.startsWith(PERSONAL_PREFIX2))
86730
86993
  continue;
86731
- const dirPath = join82(skillsDir, ent);
86994
+ const dirPath = join83(skillsDir, ent);
86732
86995
  try {
86733
86996
  if (!statSync34(dirPath).isDirectory())
86734
86997
  continue;
@@ -86768,7 +87031,7 @@ function listSharedSkills(sharedRoot = defaultSharedRoot2()) {
86768
87031
  continue;
86769
87032
  if (ent.startsWith("."))
86770
87033
  continue;
86771
- const dirPath = join82(sharedRoot, ent);
87034
+ const dirPath = join83(sharedRoot, ent);
86772
87035
  try {
86773
87036
  if (!statSync34(dirPath).isDirectory())
86774
87037
  continue;
@@ -86804,7 +87067,7 @@ function listBundledSkills(bundledRoot = defaultBundledRoot2()) {
86804
87067
  for (const ent of entries) {
86805
87068
  if (ent.startsWith("."))
86806
87069
  continue;
86807
- const dirPath = join82(bundledRoot, ent);
87070
+ const dirPath = join83(bundledRoot, ent);
86808
87071
  try {
86809
87072
  if (!statSync34(dirPath).isDirectory())
86810
87073
  continue;
@@ -86948,9 +87211,9 @@ function registerHostdMcpCommand(program3) {
86948
87211
  // src/cli/hostd.ts
86949
87212
  init_source();
86950
87213
  init_helpers();
86951
- import { existsSync as existsSync84, mkdirSync as mkdirSync47, readdirSync as readdirSync33, readFileSync as readFileSync72, writeFileSync as writeFileSync41, statSync as statSync35, copyFileSync as copyFileSync12 } from "node:fs";
87214
+ import { existsSync as existsSync84, mkdirSync as mkdirSync48, readdirSync as readdirSync33, readFileSync as readFileSync72, writeFileSync as writeFileSync42, statSync as statSync35, copyFileSync as copyFileSync12 } from "node:fs";
86952
87215
  import { homedir as homedir49 } from "node:os";
86953
- import { join as join83 } from "node:path";
87216
+ import { join as join84 } from "node:path";
86954
87217
  import { spawnSync as spawnSync16 } from "node:child_process";
86955
87218
 
86956
87219
  // src/cli/deploy-version-guard.ts
@@ -87113,10 +87376,10 @@ function resolveHostdHostHome(env2 = process.env, home2 = homedir49()) {
87113
87376
  return resolved;
87114
87377
  }
87115
87378
  function hostdDir() {
87116
- return join83(homedir49(), ".switchroom", "hostd");
87379
+ return join84(homedir49(), ".switchroom", "hostd");
87117
87380
  }
87118
87381
  function hostdComposePath() {
87119
- return join83(hostdDir(), "docker-compose.yml");
87382
+ return join84(hostdDir(), "docker-compose.yml");
87120
87383
  }
87121
87384
  function backupExistingCompose() {
87122
87385
  const p = hostdComposePath();
@@ -87153,7 +87416,7 @@ async function doInstall(opts, program3) {
87153
87416
  }
87154
87417
  const dir = hostdDir();
87155
87418
  const composePath = hostdComposePath();
87156
- mkdirSync47(dir, { recursive: true });
87419
+ mkdirSync48(dir, { recursive: true });
87157
87420
  const imageTag = resolveHostdImageTag(opts.tag, cfg.release);
87158
87421
  const guard = checkDowngrade({
87159
87422
  container: "switchroom-hostd",
@@ -87178,7 +87441,7 @@ async function doInstall(opts, program3) {
87178
87441
  const bak = backupExistingCompose();
87179
87442
  if (bak)
87180
87443
  console.log(source_default.dim(` Backed up existing compose to ${bak}`));
87181
- writeFileSync41(composePath, yaml, "utf8");
87444
+ writeFileSync42(composePath, yaml, "utf8");
87182
87445
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
87183
87446
  const adminAgents = Object.entries(cfg.agents ?? {}).filter(([, a]) => a?.admin === true).map(([name]) => name);
87184
87447
  console.log(source_default.dim(` agents served (one socket each): ${allAgents.length === 0 ? "(none)" : allAgents.join(", ")}`));
@@ -87236,7 +87499,7 @@ function doStatus() {
87236
87499
  for (const name of readdirSync33(dir)) {
87237
87500
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
87238
87501
  continue;
87239
- const sockPath = join83(dir, name, "sock");
87502
+ const sockPath = join84(dir, name, "sock");
87240
87503
  if (existsSync84(sockPath)) {
87241
87504
  const st = statSync35(sockPath);
87242
87505
  if ((st.mode & 61440) === 49152) {
@@ -87327,9 +87590,9 @@ The log is created when hostd handles its first privileged-verb request.`));
87327
87590
  // src/cli/webd.ts
87328
87591
  init_source();
87329
87592
  init_helpers();
87330
- import { existsSync as existsSync85, mkdirSync as mkdirSync48, writeFileSync as writeFileSync42, copyFileSync as copyFileSync13 } from "node:fs";
87593
+ import { existsSync as existsSync85, mkdirSync as mkdirSync49, writeFileSync as writeFileSync43, copyFileSync as copyFileSync13 } from "node:fs";
87331
87594
  import { homedir as homedir50 } from "node:os";
87332
- import { join as join84 } from "node:path";
87595
+ import { join as join85 } from "node:path";
87333
87596
  import { spawnSync as spawnSync17 } from "node:child_process";
87334
87597
  function resolveWebImageTag(explicitTag, release) {
87335
87598
  if (explicitTag)
@@ -87414,10 +87677,10 @@ services:
87414
87677
  `;
87415
87678
  }
87416
87679
  function webdDir() {
87417
- return join84(homedir50(), ".switchroom", "web");
87680
+ return join85(homedir50(), ".switchroom", "web");
87418
87681
  }
87419
87682
  function webdComposePath() {
87420
- return join84(webdDir(), "docker-compose.yml");
87683
+ return join85(webdDir(), "docker-compose.yml");
87421
87684
  }
87422
87685
  function backupExistingCompose2() {
87423
87686
  const p = webdComposePath();
@@ -87446,7 +87709,7 @@ async function doInstall2(opts, program3) {
87446
87709
  }
87447
87710
  const dir = webdDir();
87448
87711
  const composePath = webdComposePath();
87449
- mkdirSync48(dir, { recursive: true });
87712
+ mkdirSync49(dir, { recursive: true });
87450
87713
  const cfg = getConfig(program3);
87451
87714
  const imageTag = resolveWebImageTag(opts.tag, cfg.release);
87452
87715
  const guard = checkDowngrade({
@@ -87472,7 +87735,7 @@ async function doInstall2(opts, program3) {
87472
87735
  const bak = backupExistingCompose2();
87473
87736
  if (bak)
87474
87737
  console.log(source_default.dim(` Backed up existing compose to ${bak}`));
87475
- writeFileSync42(composePath, yaml, "utf8");
87738
+ writeFileSync43(composePath, yaml, "utf8");
87476
87739
  console.log(source_default.green(` \u2713 Wrote ${composePath}`));
87477
87740
  console.log(source_default.dim(` running as uid ${operatorUid} (operator), network_mode: host`));
87478
87741
  console.log(source_default.dim(` Pulling ghcr.io/switchroom/switchroom-web:${imageTag}\u2026`));