switchroom 0.7.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/README.md +51 -59
  2. package/bin/run-hook.sh +27 -11
  3. package/bin/timezone-hook.sh +9 -7
  4. package/dist/agent-scheduler/index.js +410 -133
  5. package/dist/auth-broker/index.js +13932 -0
  6. package/dist/cli/switchroom.js +26937 -5601
  7. package/dist/host-control/main.js +12702 -0
  8. package/dist/vault/approvals/kernel-server.js +467 -184
  9. package/dist/vault/broker/server.js +1430 -724
  10. package/examples/minimal.yaml +63 -0
  11. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  12. package/examples/personal-google-workspace-mcp/README.md +194 -0
  13. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  14. package/examples/switchroom.yaml +220 -0
  15. package/package.json +7 -4
  16. package/profiles/_base/settings.json.hbs +20 -5
  17. package/profiles/_base/start.sh.hbs +16 -3
  18. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  19. package/profiles/_shared/telegram-style.md.hbs +20 -90
  20. package/profiles/_shared/vault-protocol.md.hbs +68 -0
  21. package/profiles/default/CLAUDE.md +50 -96
  22. package/profiles/default/CLAUDE.md.hbs +36 -6
  23. package/profiles/default/workspace/SOUL.md.hbs +12 -5
  24. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  25. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  26. package/skills/buildkite-api/SKILL.md +31 -8
  27. package/skills/buildkite-cli/SKILL.md +27 -9
  28. package/skills/buildkite-migration/SKILL.md +22 -9
  29. package/skills/buildkite-pipelines/SKILL.md +26 -9
  30. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  31. package/skills/buildkite-test-engine/SKILL.md +25 -8
  32. package/skills/docx/SKILL.md +1 -1
  33. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
  34. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
  35. package/skills/file-bug/SKILL.md +34 -6
  36. package/skills/humanizer/SKILL.md +15 -0
  37. package/skills/humanizer-calibrate/SKILL.md +7 -1
  38. package/skills/mcp-builder/SKILL.md +1 -1
  39. package/skills/pdf/SKILL.md +1 -1
  40. package/skills/pptx/SKILL.md +1 -1
  41. package/skills/skill-creator/SKILL.md +21 -1
  42. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  44. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  45. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  46. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  47. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  48. package/skills/switchroom-cli/SKILL.md +63 -64
  49. package/skills/switchroom-health/SKILL.md +23 -10
  50. package/skills/switchroom-install/SKILL.md +3 -3
  51. package/skills/switchroom-manage/SKILL.md +26 -19
  52. package/skills/switchroom-runtime/SKILL.md +191 -0
  53. package/skills/switchroom-status/SKILL.md +27 -2
  54. package/skills/telegram-test-harness/SKILL.md +3 -0
  55. package/skills/token-helpers/SKILL.md +24 -1
  56. package/skills/webapp-testing/SKILL.md +31 -1
  57. package/skills/xlsx/SKILL.md +1 -1
  58. package/telegram-plugin/admin-commands/index.ts +7 -5
  59. package/telegram-plugin/analytics-posthog.ts +191 -0
  60. package/telegram-plugin/bridge/bridge.ts +69 -0
  61. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  62. package/telegram-plugin/dist/bridge/bridge.js +194 -119
  63. package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
  64. package/telegram-plugin/dist/server.js +245 -189
  65. package/telegram-plugin/first-paint.ts +3 -24
  66. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  67. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  68. package/telegram-plugin/gateway/auth-command.ts +794 -0
  69. package/telegram-plugin/gateway/auth-line.ts +123 -0
  70. package/telegram-plugin/gateway/boot-card.ts +169 -40
  71. package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
  72. package/telegram-plugin/gateway/boot-probes.ts +166 -123
  73. package/telegram-plugin/gateway/boot-reason.ts +41 -7
  74. package/telegram-plugin/gateway/boot-version.ts +66 -0
  75. package/telegram-plugin/gateway/gateway.ts +3499 -1885
  76. package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
  77. package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
  78. package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
  79. package/telegram-plugin/gateway/quarantine.ts +69 -0
  80. package/telegram-plugin/gateway/quota-cache.ts +9 -4
  81. package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
  82. package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
  83. package/telegram-plugin/gateway/recent-denials.ts +77 -0
  84. package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
  85. package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
  86. package/telegram-plugin/history.ts +91 -0
  87. package/telegram-plugin/hooks/hooks.json +10 -0
  88. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
  89. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
  90. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
  91. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  92. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  93. package/telegram-plugin/inbound-classifier.ts +50 -0
  94. package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
  95. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  96. package/telegram-plugin/package.json +4 -2
  97. package/telegram-plugin/permission-rule.ts +51 -0
  98. package/telegram-plugin/permission-title.ts +56 -0
  99. package/telegram-plugin/quota-check.ts +19 -41
  100. package/telegram-plugin/registry/reaper.ts +223 -0
  101. package/telegram-plugin/retry-api-call.ts +80 -0
  102. package/telegram-plugin/runtime-metrics.ts +177 -0
  103. package/telegram-plugin/scripts/build.mjs +0 -1
  104. package/telegram-plugin/secret-detect/index.ts +24 -0
  105. package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
  106. package/telegram-plugin/secret-detect/vault-error.ts +78 -11
  107. package/telegram-plugin/secret-detect/vault-write.ts +14 -2
  108. package/telegram-plugin/server.js +41795 -0
  109. package/telegram-plugin/session-tail.ts +6 -1
  110. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  111. package/telegram-plugin/silence-poke.ts +420 -0
  112. package/telegram-plugin/silent-end.ts +174 -0
  113. package/telegram-plugin/stream-controller.ts +13 -0
  114. package/telegram-plugin/stream-reply-handler.ts +7 -0
  115. package/telegram-plugin/subagent-watcher.ts +213 -4
  116. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  117. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  118. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  119. package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
  120. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
  121. package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
  122. package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
  123. package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
  124. package/telegram-plugin/tests/boot-probes.test.ts +216 -10
  125. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  126. package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
  127. package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
  128. package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
  129. package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
  130. package/telegram-plugin/tests/history-reaper.test.ts +378 -0
  131. package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
  132. package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
  133. package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
  134. package/telegram-plugin/tests/issues-card.test.ts +49 -0
  135. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
  136. package/telegram-plugin/tests/permission-rule.test.ts +80 -1
  137. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  138. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  139. package/telegram-plugin/tests/races.test.ts +179 -0
  140. package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
  141. package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
  142. package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
  143. package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
  144. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
  145. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
  146. package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
  147. package/telegram-plugin/tests/silence-poke.test.ts +493 -0
  148. package/telegram-plugin/tests/silent-end.test.ts +206 -0
  149. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
  150. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
  151. package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
  152. package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
  153. package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
  154. package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
  155. package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
  156. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
  157. package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
  158. package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
  159. package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
  160. package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
  161. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
  162. package/telegram-plugin/turn-signal-tracker.ts +100 -24
  163. package/telegram-plugin/uat/SETUP.md +210 -35
  164. package/telegram-plugin/uat/assertions.ts +264 -37
  165. package/telegram-plugin/uat/driver-info.ts +57 -0
  166. package/telegram-plugin/uat/driver.ts +590 -51
  167. package/telegram-plugin/uat/harness.ts +140 -94
  168. package/telegram-plugin/uat/load-env.test.ts +72 -0
  169. package/telegram-plugin/uat/load-env.ts +48 -0
  170. package/telegram-plugin/uat/login.ts +96 -53
  171. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  172. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  173. package/telegram-plugin/uat/runners/report.ts +150 -0
  174. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  175. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  176. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  177. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  178. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  179. package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
  180. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
  181. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
  182. package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
  183. package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
  184. package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
  185. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
  186. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
  187. package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
  188. package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
  189. package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
  190. package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
  191. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
  192. package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
  193. package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
  194. package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
  195. package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
  196. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
  197. package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
  198. package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
  199. package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
  200. package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
  201. package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
  202. package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
  203. package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
  204. package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
  205. package/telegram-plugin/vault-approval-posture.ts +42 -0
  206. package/telegram-plugin/welcome-text.ts +1 -0
  207. package/telegram-plugin/active-pins-sweep.ts +0 -204
  208. package/telegram-plugin/active-pins.ts +0 -146
  209. package/telegram-plugin/auth-dashboard.ts +0 -1104
  210. package/telegram-plugin/auth-slot-parser.ts +0 -497
  211. package/telegram-plugin/card-event-log.ts +0 -138
  212. package/telegram-plugin/dist/foreman/foreman.js +0 -31106
  213. package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
  214. package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
  215. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  216. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  217. package/telegram-plugin/foreman/foreman.ts +0 -1165
  218. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  219. package/telegram-plugin/foreman/setup-state.ts +0 -239
  220. package/telegram-plugin/foreman/state.ts +0 -203
  221. package/telegram-plugin/pin-event-log.ts +0 -76
  222. package/telegram-plugin/progress-card-driver.ts +0 -2886
  223. package/telegram-plugin/progress-card-pin-manager.ts +0 -589
  224. package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
  225. package/telegram-plugin/progress-card.ts +0 -1409
  226. package/telegram-plugin/tests/HARNESS.md +0 -340
  227. package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
  228. package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
  229. package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
  230. package/telegram-plugin/tests/active-pins.test.ts +0 -187
  231. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  232. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  233. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  234. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  235. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  236. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  237. package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
  238. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  239. package/telegram-plugin/tests/card-event-log.test.ts +0 -145
  240. package/telegram-plugin/tests/first-paint.test.ts +0 -257
  241. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  242. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  243. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  244. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  245. package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
  246. package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
  247. package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
  248. package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
  249. package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
  250. package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
  251. package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
  252. package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
  253. package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
  254. package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
  255. package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
  256. package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
  257. package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
  258. package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
  259. package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
  260. package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
  261. package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
  262. package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
  263. package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
  264. package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
  265. package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
  266. package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
  267. package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
  268. package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
  269. package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
  270. package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
  271. package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
  272. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  273. package/telegram-plugin/tests/setup-state.test.ts +0 -146
  274. package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
  275. package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
  276. package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
  277. package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
  278. package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
  279. package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
  280. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
  281. package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
  282. package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
  283. package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
  284. package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
  285. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
  286. package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
  287. package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
  288. package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
  289. package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
  290. package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
  291. package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
  292. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
  293. package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
  294. package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
  295. package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
  296. package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
  297. package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
  298. package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
  299. package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
  300. package/telegram-plugin/two-zone-card.ts +0 -269
  301. package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Hostd dispatch helpers for the gateway's self-restart slash-commands
3
+ * (#1175 RFC C, Phase 2). When the operator has opted into
4
+ * `host_control.enabled: true`, /restart, /new, /reset, and
5
+ * /update apply route through the per-agent hostd UDS instead of the
6
+ * in-container `spawnSwitchroomDetached` shellout.
7
+ *
8
+ * Rationale: in docker-mode (the v0.7+ default) the agent container
9
+ * has no docker binary and no `/var/run/docker.sock` — so the
10
+ * spawn-path verbs fail with exit-127 the moment they touch compose.
11
+ * Hostd runs on the host with the docker socket mounted, so the verbs
12
+ * actually work.
13
+ *
14
+ * Extracted from gateway.ts for unit-testability — gateway.ts itself
15
+ * has too many boot-time side-effects to import directly in a test.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import { randomBytes } from "node:crypto";
19
+ import { hostdRequest } from "../../src/host-control/client.js";
20
+ import type {
21
+ HostdRequest,
22
+ HostdResponse,
23
+ } from "../../src/host-control/protocol.js";
24
+ import { loadConfig as loadSwitchroomConfig } from "../../src/config/loader.js";
25
+
26
+ let _hostdEnabled: boolean | undefined;
27
+
28
+ /**
29
+ * Reads `host_control.enabled` from the resolved switchroom config.
30
+ * Cached for the gateway's lifetime — config doesn't change without a
31
+ * restart, and the file-read isn't free.
32
+ *
33
+ * Best-effort: if the config can't be loaded (gateway running in a
34
+ * dir where loadConfig fails), returns false so the dispatch helper
35
+ * falls through to the legacy spawn path.
36
+ */
37
+ export function isHostdEnabled(): boolean {
38
+ if (_hostdEnabled !== undefined) return _hostdEnabled;
39
+ try {
40
+ const cfg = loadSwitchroomConfig();
41
+ _hostdEnabled = cfg.host_control?.enabled === true;
42
+ } catch {
43
+ _hostdEnabled = false;
44
+ }
45
+ return _hostdEnabled;
46
+ }
47
+
48
+ /** @internal Reset the cache so tests can swap config and re-probe. */
49
+ export function _resetHostdEnabledCache(): void {
50
+ _hostdEnabled = undefined;
51
+ }
52
+
53
+ export function hostdSocketPath(agentName: string): string {
54
+ return `/run/switchroom/hostd/${agentName}/sock`;
55
+ }
56
+
57
+ /**
58
+ * True only when (a) host_control is enabled in config AND (b) the
59
+ * per-agent socket is bound on disk. Distinct from "will the wire call
60
+ * succeed" — that's only knowable after attempting it.
61
+ *
62
+ * Callers use this to decide *whether to skip docker-availability
63
+ * preflight guards* (since hostd doesn't need in-container docker).
64
+ */
65
+ export function hostdWillBeUsed(agentName: string): boolean {
66
+ if (!isHostdEnabled()) return false;
67
+ return existsSync(hostdSocketPath(agentName));
68
+ }
69
+
70
+ /**
71
+ * Send one request to the per-agent hostd socket.
72
+ *
73
+ * Returns:
74
+ * - `"not-configured"` — hostd is disabled in config OR the per-agent
75
+ * socket isn't bound. Callers should fall back to the legacy
76
+ * `spawnSwitchroomDetached` path.
77
+ * - `HostdResponse` — hostd was contacted. Callers branch on
78
+ * `resp.result`. Wire errors (ECONNREFUSED, timeout, bad frame)
79
+ * are synthesized into a `result: "error"` response so callers
80
+ * don't need a separate try/catch around the failure.
81
+ *
82
+ * Deliberately no silent fallback to spawn when hostd is configured-on
83
+ * but returns error/denied: the operator opted in, so masking failures
84
+ * would just confuse them about why the verb didn't actually run.
85
+ */
86
+ export async function tryHostdDispatch(
87
+ agentName: string,
88
+ req: HostdRequest,
89
+ ): Promise<HostdResponse | "not-configured"> {
90
+ if (!isHostdEnabled()) return "not-configured";
91
+ const sockPath = hostdSocketPath(agentName);
92
+ if (!existsSync(sockPath)) return "not-configured";
93
+ try {
94
+ return await hostdRequest(
95
+ { socketPath: sockPath, timeoutMs: 5000 },
96
+ req,
97
+ );
98
+ } catch (err) {
99
+ process.stderr.write(
100
+ `telegram gateway: hostd dispatch failed ` +
101
+ `(request_id=${req.request_id} op=${req.op}): ` +
102
+ `${(err as Error).message}\n`,
103
+ );
104
+ return {
105
+ v: 1,
106
+ request_id: req.request_id,
107
+ result: "error",
108
+ exit_code: null,
109
+ duration_ms: 0,
110
+ error: `hostd wire error: ${(err as Error).message}`,
111
+ };
112
+ }
113
+ }
114
+
115
+ export function hostdRequestId(prefix: string): string {
116
+ return `${prefix}-${Date.now()}-${randomBytes(4).toString("hex")}`;
117
+ }
@@ -18,6 +18,24 @@ export interface PermissionEvent {
18
18
  type: "permission";
19
19
  requestId: string;
20
20
  behavior: "allow" | "deny";
21
+ /**
22
+ * Session-scoped always-allow rule. Only set when the operator taps
23
+ * "🔁 Always allow" — the gateway already persists the rule to
24
+ * switchroom.yaml + settings.json via `switchroom agent grant`, but
25
+ * those writes only kick in on the NEXT agent boot. This field carries
26
+ * the rule to the running bridge so it can short-circuit future
27
+ * `permission_request` notifications (from the parent claude AND any
28
+ * sub-agents dispatched via the Task tool, which share the same MCP
29
+ * server / bridge process) within the current session.
30
+ *
31
+ * Issue #1138: without this, a sub-agent dispatched after the operator
32
+ * tapped "Always allow" still hit the popup, because Claude Code reads
33
+ * `.claude/settings.json` once at boot.
34
+ *
35
+ * Format matches `resolveAlwaysAllowRule`'s output: bare tool name
36
+ * (`Edit`), `Skill(<name>)`, or `mcp__<server>__<tool>`.
37
+ */
38
+ rule?: string;
21
39
  }
22
40
 
23
41
  export interface StatusEvent {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Per-agent buffer for synthetic inbounds the gateway couldn't deliver
3
+ * because no live IPC client was registered for the agent at send-time.
4
+ *
5
+ * Background: `ipcServer.sendToAgent(agent, msg)` returns `false` when
6
+ * the agent's bridge isn't connected. Before this buffer existed, the
7
+ * gateway logged the failure and dropped the message — root cause of
8
+ * issue #1150 (operator taps Approve on a vault_request_access card,
9
+ * grant lands, but the `vault_grant_approved` inbound that wakes the
10
+ * agent never arrives if the bridge happens to be reconnecting in
11
+ * that exact 100ms window).
12
+ *
13
+ * Contract:
14
+ * - `push(agent, msg)` is best-effort and synchronous. Bounded:
15
+ * a slow / dead bridge can't fill memory.
16
+ * - `drain(agent)` returns ALL pending messages for `agent` in
17
+ * insertion order and removes them from the buffer. Called from
18
+ * `onClientRegistered` so a fresh bridge picks up the missed
19
+ * wake-ups before doing anything else.
20
+ * - In-memory only. Survives across IPC disconnect/reconnect within
21
+ * a single gateway-process lifetime, but NOT a gateway restart.
22
+ * A gateway crash mid-buffer means lost wake-ups; the silence-
23
+ * poke ladder catches this downstream so the worst-case is a
24
+ * 5-minute delay, not a permanent stall.
25
+ *
26
+ * Per-agent cap prevents a never-reconnecting bridge from leaking
27
+ * unbounded memory. When the cap is hit, the OLDEST entry is dropped
28
+ * — the assumption is the freshest wake-up is the most relevant. A
29
+ * dropped entry is logged via the provided logger.
30
+ */
31
+
32
+ import type { InboundMessage } from './ipc-protocol.js'
33
+
34
+ /** Default cap per agent. Tuned for `should fit a reasonable backlog of
35
+ * approval cards stacked while bridge is offline` but no more. */
36
+ export const DEFAULT_PENDING_INBOUND_CAP = 32
37
+
38
+ export interface PendingInboundBuffer {
39
+ /** Append `msg` to `agent`'s queue. Returns true if accepted, false if
40
+ * the cap forced an eviction (the message is STILL accepted; `false`
41
+ * signals "tail dropped to make room"). */
42
+ push: (agent: string, msg: InboundMessage) => boolean
43
+ /** Pop and return all pending messages for `agent`. Empty array when
44
+ * none. Idempotent. */
45
+ drain: (agent: string) => InboundMessage[]
46
+ /** Test-only: current depth for `agent`. */
47
+ depth: (agent: string) => number
48
+ /** Test-only: total depth across all agents. */
49
+ totalDepth: () => number
50
+ }
51
+
52
+ export interface PendingInboundBufferOptions {
53
+ capPerAgent?: number
54
+ log?: (line: string) => void
55
+ }
56
+
57
+ export function createPendingInboundBuffer(
58
+ opts: PendingInboundBufferOptions = {},
59
+ ): PendingInboundBuffer {
60
+ const cap = opts.capPerAgent ?? DEFAULT_PENDING_INBOUND_CAP
61
+ const log = opts.log ?? ((line: string) => process.stderr.write(line))
62
+ const queues = new Map<string, InboundMessage[]>()
63
+
64
+ return {
65
+ push(agent, msg) {
66
+ let q = queues.get(agent)
67
+ if (q == null) {
68
+ q = []
69
+ queues.set(agent, q)
70
+ }
71
+ let evicted = false
72
+ if (q.length >= cap) {
73
+ const dropped = q.shift()
74
+ evicted = true
75
+ log(
76
+ `pending-inbound-buffer: agent=${agent} cap=${cap} reached — ` +
77
+ `dropped oldest entry source=${dropped?.meta?.source ?? '-'} ts=${dropped?.ts ?? '-'}\n`,
78
+ )
79
+ }
80
+ q.push(msg)
81
+ log(
82
+ `pending-inbound-buffer: agent=${agent} buffered source=${msg.meta?.source ?? '-'} ` +
83
+ `depth_after=${q.length} evicted=${evicted}\n`,
84
+ )
85
+ return !evicted
86
+ },
87
+ drain(agent) {
88
+ const q = queues.get(agent)
89
+ if (q == null || q.length === 0) return []
90
+ queues.delete(agent)
91
+ log(
92
+ `pending-inbound-buffer: drained agent=${agent} count=${q.length} ` +
93
+ `sources=[${q.map((m) => m.meta?.source ?? '-').join(',')}]\n`,
94
+ )
95
+ return q
96
+ },
97
+ depth(agent) {
98
+ return queues.get(agent)?.length ?? 0
99
+ },
100
+ totalDepth() {
101
+ let n = 0
102
+ for (const q of queues.values()) n += q.length
103
+ return n
104
+ },
105
+ }
106
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Gateway-side writer for the agent quarantine marker (#1076).
3
+ *
4
+ * Mirrors the on-disk contract owned by `src/agents/quarantine.ts` —
5
+ * the host-side host CLI reads the marker, the gateway writes it. We
6
+ * keep a tiny copy here (writer only) because the gateway is bundled
7
+ * separately and can't import from `src/`.
8
+ *
9
+ * The schema MUST stay in sync with `src/agents/quarantine.ts`:
10
+ *
11
+ * { v: 1, reason: "startup.unauthorized", ts: <ms>, detail?: <string> }
12
+ *
13
+ * Any change to the shape should land in both files in the same PR
14
+ * (or be guarded by `v`). See the src module's docstring for the full
15
+ * threat-model and operator remediation.
16
+ *
17
+ * SECURITY: never write the bot token (or any secret material) into
18
+ * the marker. The detail field is for the API description ("Unauthorized")
19
+ * and similar non-secret context.
20
+ */
21
+
22
+ import { mkdirSync, writeFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+
25
+ export const QUARANTINE_FILENAME = 'quarantine.json'
26
+
27
+ export type QuarantineReason =
28
+ | 'startup.unauthorized'
29
+ // Config-class refusal-to-boot. Added 2026-05-13 after the
30
+ // vault-posture init started throwing as an unhandled rejection
31
+ // when the operator declared telegram-id posture but the auto-
32
+ // unlock blob couldn't be read. Pre-fix that path produced a tight
33
+ // restart loop ($n/60s -> hit supervisor cap -> stop) and posted
34
+ // an "agent-crashed" event per restart. The right outcome is a
35
+ // single EX_CONFIG exit at the first failure → supervisor
36
+ // quarantines → operator sees one clean error.
37
+ | 'startup.config_error'
38
+
39
+ export interface QuarantineMarker {
40
+ v: 1
41
+ reason: QuarantineReason
42
+ ts: number
43
+ detail?: string
44
+ }
45
+
46
+ /**
47
+ * Write the quarantine marker into a Telegram state dir (typically
48
+ * `process.env.TELEGRAM_STATE_DIR`). Idempotent — overwrites any
49
+ * existing marker. Creates the parent dir if missing.
50
+ */
51
+ export function writeQuarantineMarker(
52
+ telegramStateDir: string,
53
+ reason: QuarantineReason,
54
+ detail?: string,
55
+ nowFn: () => number = Date.now,
56
+ ): void {
57
+ mkdirSync(telegramStateDir, { recursive: true, mode: 0o700 })
58
+ const marker: QuarantineMarker = {
59
+ v: 1,
60
+ reason,
61
+ ts: nowFn(),
62
+ detail,
63
+ }
64
+ writeFileSync(
65
+ join(telegramStateDir, QUARANTINE_FILENAME),
66
+ JSON.stringify(marker) + '\n',
67
+ 'utf-8',
68
+ )
69
+ }
@@ -37,9 +37,14 @@ export const DEFAULT_TTL_MS = 5 * 60 * 1000 // 5 min
37
37
  * next scheduled boot gets a live result.
38
38
  */
39
39
  export const RATE_LIMIT_TTL_MS = 30 * 1000 // 30 s
40
- export const DEFAULT_CACHE_PATH =
41
- process.env.SWITCHROOM_QUOTA_CACHE_PATH
40
+
41
+ // Resolved lazily so tests can set SWITCHROOM_QUOTA_CACHE_PATH per-test
42
+ // (the env var was previously read at module-import time, which made the
43
+ // override in test setUp a no-op once the module was cached).
44
+ export function defaultCachePath(): string {
45
+ return process.env.SWITCHROOM_QUOTA_CACHE_PATH
42
46
  ?? join(process.env.HOME ?? '/tmp', '.switchroom', 'quota-cache.json')
47
+ }
43
48
 
44
49
  /**
45
50
  * Read a cached probe result if one exists and is still within TTL.
@@ -54,7 +59,7 @@ export function readQuotaCache(opts: {
54
59
  path?: string
55
60
  now?: number
56
61
  } = {}): ProbeResult | null {
57
- const path = opts.path ?? DEFAULT_CACHE_PATH
62
+ const path = opts.path ?? defaultCachePath()
58
63
  const now = opts.now ?? Date.now()
59
64
 
60
65
  if (!existsSync(path)) return null
@@ -102,7 +107,7 @@ export function writeQuotaCache(
102
107
  // Don't cache hard failures — let the next boot retry clean.
103
108
  if (result.status === 'fail') return
104
109
 
105
- const path = opts.path ?? DEFAULT_CACHE_PATH
110
+ const path = opts.path ?? defaultCachePath()
106
111
  // Rate-limit results use a shorter TTL: long enough to absorb a fleet
107
112
  // restart burst, short enough that subsequent boots get a live probe.
108
113
  // Use the structured `rateLimited` field rather than string-matching on