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,191 @@
1
+ /**
2
+ * analytics-posthog.ts — gateway-side PostHog client.
3
+ *
4
+ * Mirrors `src/analytics/posthog.ts` (the CLI's client) but sized for a
5
+ * long-lived gateway process: default batching instead of immediate-flush.
6
+ * Honours the same env vars (SWITCHROOM_POSTHOG_KEY, SWITCHROOM_POSTHOG_HOST,
7
+ * SWITCHROOM_TELEMETRY_DISABLED) so an operator opt-out applies fleet-wide.
8
+ *
9
+ * Distinct ID lineage:
10
+ * 1. SWITCHROOM_ANALYTICS_ID env var — set by compose.ts from the host's
11
+ * ~/.switchroom/analytics-id so per-agent runtime events merge with
12
+ * the same user's CLI events in PostHog.
13
+ * 2. Per-agent fallback UUID at /state/agent/analytics-id when the env
14
+ * var is missing (e.g. legacy compose). Persists across restarts.
15
+ *
16
+ * Every event auto-stamps `agent` and `switchroom_version` so dashboards
17
+ * can slice by agent without each call-site repeating the property.
18
+ */
19
+
20
+ import { PostHog } from 'posthog-node'
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
22
+ import { dirname, join } from 'node:path'
23
+ import { randomUUID } from 'node:crypto'
24
+
25
+ const DEFAULT_KEY = 'phc_qKY87cKWZm6ZyCtk7LcRd2cU8Sg42u7Ywhui5stYCegd'
26
+ const DEFAULT_HOST = 'https://us.i.posthog.com'
27
+
28
+ let client: PostHog | null = null
29
+ let initialized = false
30
+ let cachedDistinctId: string | null = null
31
+ let globalHandlersInstalled = false
32
+
33
+ function telemetryDisabled(): boolean {
34
+ const v = process.env.SWITCHROOM_TELEMETRY_DISABLED
35
+ return v === '1' || v === 'true'
36
+ }
37
+
38
+ function agentName(): string {
39
+ return process.env.SWITCHROOM_AGENT_NAME ?? 'unknown'
40
+ }
41
+
42
+ function switchroomVersion(): string {
43
+ return process.env.SWITCHROOM_VERSION ?? 'unknown'
44
+ }
45
+
46
+ export function getDistinctId(): string {
47
+ if (cachedDistinctId) return cachedDistinctId
48
+ const envId = process.env.SWITCHROOM_ANALYTICS_ID
49
+ if (envId && envId.trim() !== '') {
50
+ cachedDistinctId = envId.trim()
51
+ return cachedDistinctId
52
+ }
53
+ const fallbackPath = join(
54
+ process.env.SWITCHROOM_RUNTIME_STATE_DIR ?? '/state/agent',
55
+ 'analytics-id',
56
+ )
57
+ try {
58
+ if (existsSync(fallbackPath)) {
59
+ const existing = readFileSync(fallbackPath, 'utf-8').trim()
60
+ if (existing) {
61
+ cachedDistinctId = existing
62
+ return existing
63
+ }
64
+ }
65
+ } catch {
66
+ // fall through to fresh uuid
67
+ }
68
+ const id = randomUUID()
69
+ cachedDistinctId = id
70
+ try {
71
+ mkdirSync(dirname(fallbackPath), { recursive: true })
72
+ writeFileSync(fallbackPath, id, 'utf-8')
73
+ } catch {
74
+ // non-fatal — fresh uuid next boot is acceptable
75
+ }
76
+ return id
77
+ }
78
+
79
+ export function getPostHog(): PostHog | null {
80
+ if (initialized) return client
81
+ initialized = true
82
+ if (telemetryDisabled()) return null
83
+ const apiKey = process.env.SWITCHROOM_POSTHOG_KEY ?? DEFAULT_KEY
84
+ const host = process.env.SWITCHROOM_POSTHOG_HOST ?? DEFAULT_HOST
85
+ if (!apiKey) return null
86
+ try {
87
+ client = new PostHog(apiKey, {
88
+ host,
89
+ // Long-lived gateway: rely on default batching instead of the
90
+ // immediate-flush the short-lived CLI uses.
91
+ enableExceptionAutocapture: false,
92
+ // IP is considered PII in our telemetry policy (see docs/posthog.md).
93
+ disableGeoip: true,
94
+ })
95
+ } catch {
96
+ client = null
97
+ }
98
+ return client
99
+ }
100
+
101
+ export async function captureEvent(
102
+ event: string,
103
+ properties: Record<string, unknown> = {},
104
+ ): Promise<void> {
105
+ const ph = getPostHog()
106
+ if (!ph) return
107
+ try {
108
+ ph.capture({
109
+ distinctId: getDistinctId(),
110
+ event,
111
+ properties: {
112
+ agent: agentName(),
113
+ switchroom_version: switchroomVersion(),
114
+ source: 'gateway',
115
+ ...properties,
116
+ },
117
+ })
118
+ } catch {
119
+ // Telemetry must never break the gateway.
120
+ }
121
+ }
122
+
123
+ export async function captureException(
124
+ error: unknown,
125
+ properties: Record<string, unknown> = {},
126
+ ): Promise<void> {
127
+ const ph = getPostHog()
128
+ if (!ph) return
129
+ try {
130
+ await ph.captureException(error, getDistinctId(), {
131
+ agent: agentName(),
132
+ switchroom_version: switchroomVersion(),
133
+ source: 'gateway',
134
+ ...properties,
135
+ })
136
+ } catch {
137
+ // Telemetry must never break the gateway.
138
+ }
139
+ }
140
+
141
+ export async function shutdownAnalytics(): Promise<void> {
142
+ if (!client) return
143
+ try {
144
+ await client.shutdown(2000)
145
+ } catch {
146
+ // ignore
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Install process-level handlers for uncaught exceptions and unhandled
152
+ * rejections so they're reported to PostHog before the process dies.
153
+ *
154
+ * Mirrors the CLI's `installGlobalErrorHandlers()` so runtime errors land
155
+ * in the same Switchroom Errors dashboard as CLI errors, tagged
156
+ * `source: 'gateway'`.
157
+ *
158
+ * The gateway already exits non-zero on fatal errors (see the polling
159
+ * IIFE at the bottom of gateway.ts). We DO NOT re-exit here for
160
+ * unhandledRejection — Node's default is to keep running and we want
161
+ * the gateway to keep polling. For uncaughtException we DO exit, because
162
+ * Node's default-since-v15 is to exit anyway after listeners return.
163
+ */
164
+ export function installGlobalErrorHandlers(): void {
165
+ if (globalHandlersInstalled) return
166
+ globalHandlersInstalled = true
167
+
168
+ const FLUSH_TIMEOUT_MS = 2000
169
+
170
+ const flushWithTimeout = async (
171
+ error: unknown,
172
+ kind: 'uncaughtException' | 'unhandledRejection',
173
+ ): Promise<void> => {
174
+ await Promise.race([
175
+ captureException(error, { kind }),
176
+ new Promise<void>((resolve) => setTimeout(resolve, FLUSH_TIMEOUT_MS)),
177
+ ])
178
+ }
179
+
180
+ process.on('uncaughtException', (err) => {
181
+ process.stderr.write(`telegram gateway: uncaughtException: ${err}\n`)
182
+ void flushWithTimeout(err, 'uncaughtException').finally(() => {
183
+ process.exit(1)
184
+ })
185
+ })
186
+
187
+ process.on('unhandledRejection', (reason) => {
188
+ process.stderr.write(`telegram gateway: unhandledRejection: ${reason}\n`)
189
+ void flushWithTimeout(reason, 'unhandledRejection')
190
+ })
191
+ }
@@ -28,6 +28,7 @@ import {
28
28
  } from '../pty-tail.js'
29
29
  import { createIpcClient, type IpcClientHandle } from './ipc-client.js'
30
30
  import type { InboundMessage, PermissionEvent, StatusEvent } from '../gateway/ipc-protocol.js'
31
+ import { matchesAllowRule } from '../permission-rule.js'
31
32
 
32
33
  installPluginLogger()
33
34
 
@@ -108,6 +109,7 @@ const TOOL_SCHEMAS = [
108
109
  disable_web_page_preview: { type: 'boolean', description: 'Disable link preview thumbnails. Default: true.' },
109
110
  protect_content: { type: 'boolean', description: 'When true, Telegram prevents the message from being forwarded or saved.' },
110
111
  quote_text: { type: 'string', description: 'Surgical quote: specific text to highlight from the reply_to message. Requires reply_to.' },
112
+ disable_notification: { type: 'boolean', description: 'When true, Telegram delivers the message silently — no device ping for this user. Default false (pings). Use true for mid-turn updates ("still working through X") so only the final answer pings. Always omit (or pass false) on the final answer of a turn.' },
111
113
  inline_keyboard: {
112
114
  type: 'array',
113
115
  description: 'Optional 2D array of tappable buttons rendered under the message. Outer array = rows; inner array = buttons in each row (max 8 per row, 8 rows). Each button needs a `text` (label, max 64 chars) plus EXACTLY ONE of: `url` (opens link in browser; must start with http(s):// or tg://) or `callback_data` (string, max 58 chars; tap is delivered to this agent as an inbound channel event with meta.button_callback_data=<the data> and the original button_text). Use buttons for single-tap approval/triage flows like [Approve] [Hold]; one tap on mobile beats asking the user to type YES/NO. By default a tap shows a brief "✓ received" toast and removes the entire keyboard so the user can\'t double-fire — override per-button via `ack_text` (custom toast text, max 200 chars) and `single_use: false` (preserve the keyboard so e.g. a [Refresh] button stays tappable).',
@@ -146,6 +148,7 @@ const TOOL_SCHEMAS = [
146
148
  quote: { type: 'boolean', description: 'Opt out of the default quote-reply behavior. Default: true. Ignored when reply_to is explicitly set.' },
147
149
  protect_content: { type: 'boolean', description: 'When true, Telegram prevents the message from being forwarded or saved.' },
148
150
  quote_text: { type: 'string', description: 'Surgical quote: specific text to highlight from the reply_to message. Requires reply_to.' },
151
+ disable_notification: { type: 'boolean', description: 'When true, the INITIAL message send is silent (no device ping). Has no effect on subsequent edits — Telegram never pings on editMessageText. Default false. Use for mid-turn stream starts you do not want to ping; omit on the final answer.' },
149
152
  inline_keyboard: {
150
153
  type: 'array',
151
154
  description: '2D array of tappable buttons under the final message. Same shape and constraints as `reply.inline_keyboard` — each button has `text` and EXACTLY ONE of `url` or `callback_data`, plus optional `ack_text` (custom tap-toast; default "✓ received") and `single_use` (default true; set false to keep the keyboard tappable after a tap). Tap on a callback_data button is delivered to this agent as an inbound channel event with meta.button_callback_data set.',
@@ -418,6 +421,23 @@ const TOOL_SCHEMAS = [
418
421
  required: ['chat_id', 'key', 'value'],
419
422
  },
420
423
  },
424
+ {
425
+ name: 'vault_request_access',
426
+ description:
427
+ 'Ask the operator (via Telegram approval card) to grant this agent read or write access to a vault key it does not yet have. Use this when you hit `VAULT-BROKER-DENIED` or when you know upfront that an upcoming task needs a key you lack. Renders a [Approve] [Deny] card; on approve, the broker mints a scoped grant token and writes it to the agent\'s `.vault-token` file. You CANNOT mint or self-elevate; only the operator can tap Approve. After firing this tool, END YOUR TURN cleanly — the gateway will inject a fresh inbound message (with `<channel source="vault_grant_approved">`) when the operator approves, kicking off a new turn where you can resume the original task. Do NOT call this for keys you already have access to (use the normal `vault:<key>` reference) and do NOT spam-request (the operator sees every card).',
428
+ inputSchema: {
429
+ type: 'object',
430
+ properties: {
431
+ chat_id: { type: 'string', description: 'Chat to render the approval card in (use the chat_id of the user message that triggered the workflow).' },
432
+ key: { type: 'string', description: 'Vault key the agent wants access to (matches the key shown in the VAULT-BROKER-DENIED error, e.g. `fatsecret/credentials`).' },
433
+ scope: { type: 'string', enum: ['read', 'write'], description: 'Access scope: "read" (default) for `vault:<key>` references; "write" if the agent needs to put new values.' },
434
+ reason: { type: 'string', description: 'Short human-readable rationale rendered on the card (e.g. "to look up today\'s food log entries"). Helps the operator decide.' },
435
+ duration: { type: 'string', description: 'Requested grant TTL, like "30d" or "12h". Default 30d, capped at 90d. Beyond 90d the operator should use the host CLI explicitly.' },
436
+ message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message if not specified.' },
437
+ },
438
+ required: ['chat_id', 'key'],
439
+ },
440
+ },
421
441
  ]
422
442
 
423
443
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
@@ -469,6 +489,22 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
469
489
  // approval. Forward them to the gateway which renders inline keyboard
470
490
  // buttons in the user's Telegram chat. The gateway sends the decision
471
491
  // back as a PermissionEvent which we relay to Claude Code (see onPermission).
492
+ //
493
+ // #1138: session-scoped always-allow cache. When the operator taps
494
+ // "🔁 Always allow" the gateway calls `switchroom agent grant` which
495
+ // updates settings.json on disk, but the running claude process won't
496
+ // re-read that file — so a sub-agent (Task tool) dispatched later in
497
+ // the same session still hits the popup. To close that gap the gateway
498
+ // also broadcasts the resolved `rule` on the `permission` event and we
499
+ // stash it here; subsequent `permission_request` notifications whose
500
+ // (tool_name, input_preview) match a cached rule are auto-allowed
501
+ // without a round-trip to Telegram. The cache lives for the bridge's
502
+ // lifetime — which is the claude session's lifetime — so on the next
503
+ // boot the now-persisted `tools.allow` entry takes over and this cache
504
+ // is rebuilt as the operator approves things again. Parent claude and
505
+ // every Task-tool sub-agent share the same bridge process, so a rule
506
+ // added by either is honoured by all.
507
+ const sessionAllowRules = new Set<string>()
472
508
 
473
509
  mcp.setNotificationHandler(
474
510
  z.object({
@@ -481,6 +517,28 @@ mcp.setNotificationHandler(
481
517
  }),
482
518
  }),
483
519
  async ({ params }) => {
520
+ // Cache hit? Auto-allow without bothering the gateway. We deliver
521
+ // the same `notifications/claude/channel/permission` shape claude
522
+ // would otherwise receive after a Telegram tap, so the call site
523
+ // is indistinguishable. We still notify the gateway out-of-band
524
+ // (via a permission_request that the gateway short-circuits on
525
+ // its side would be ideal, but for now skipping the forward is
526
+ // safe: pendingPermissions is a gateway-side bookkeeping map only,
527
+ // and nothing else depends on seeing this request_id).
528
+ for (const rule of sessionAllowRules) {
529
+ if (matchesAllowRule(rule, params.tool_name, params.input_preview)) {
530
+ process.stderr.write(
531
+ `telegram bridge: session-cached allow for ${params.tool_name} ` +
532
+ `(rule="${rule}", request_id=${params.request_id})\n`,
533
+ )
534
+ onPermission({
535
+ type: 'permission',
536
+ requestId: params.request_id,
537
+ behavior: 'allow',
538
+ })
539
+ return
540
+ }
541
+ }
484
542
  if (!ipc || !ipc.isConnected()) {
485
543
  process.stderr.write('telegram bridge: permission_request received but not connected to gateway\n')
486
544
  return
@@ -495,6 +553,7 @@ mcp.setNotificationHandler(
495
553
  },
496
554
  )
497
555
 
556
+
498
557
  // ─── IPC client ──────────────────────────────────────────────────────────
499
558
 
500
559
  let ipc: IpcClientHandle | null = null
@@ -513,6 +572,16 @@ function onInbound(msg: InboundMessage): void {
513
572
  }
514
573
 
515
574
  function onPermission(msg: PermissionEvent): void {
575
+ // #1138: stash the rule the gateway resolved on "Always allow" so we
576
+ // can short-circuit later matching permission_request notifications
577
+ // (from the parent claude or any Task-dispatched sub-agent in the
578
+ // same session). The gateway only sets `rule` when it has also
579
+ // persisted the rule to settings.json, so a process restart will
580
+ // pick up the same set of rules from disk — the cache is purely a
581
+ // mid-session bridge between the disk write and the next agent boot.
582
+ if (msg.rule) {
583
+ sessionAllowRules.add(msg.rule)
584
+ }
516
585
  mcp.notification({
517
586
  method: 'notifications/claude/channel/permission',
518
587
  params: {
@@ -23,7 +23,10 @@ export function validateGatewayMessage(msg: unknown): msg is GatewayToClient {
23
23
  return typeof m.chatId === "string" && typeof m.text === "string";
24
24
  case "permission":
25
25
  return typeof m.requestId === "string"
26
- && (m.behavior === "allow" || m.behavior === "deny");
26
+ && (m.behavior === "allow" || m.behavior === "deny")
27
+ // `rule` is optional (only sent on "🔁 Always allow"); when present
28
+ // it must be a non-empty string. #1138 wire extension.
29
+ && (m.rule === undefined || (typeof m.rule === "string" && m.rule.length > 0));
27
30
  case "status":
28
31
  return typeof m.status === "string";
29
32
  case "tool_call_result":