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
@@ -68,71 +68,298 @@ export async function expectEventually(
68
68
  });
69
69
  }
70
70
 
71
- // ---------- Phase 2 stubs ----------
71
+ // ---------- Phase 2a (DM smoke) ----------
72
+
73
+ export interface ExpectMessageOptions extends PollOptions {
74
+ threadId?: number;
75
+ /**
76
+ * Filter the observed stream by sender. `userId` matches exact
77
+ * senders; `notUserId` excludes a specific sender (used by the
78
+ * harness to translate `from: "bot"` into "anyone but the driver").
79
+ */
80
+ senderFilter?: { userId: number } | { notUserId: number };
81
+ }
72
82
 
73
83
  /**
74
- * TODO(#866): wait for the bot to send a message in `chatId`/topic
75
- * matching `match` (substring, regex, or predicate over the raw
76
- * `ObservedMessage`). Returns the matched message.
84
+ * Wait for the next message in `chatId` (optionally a forum topic)
85
+ * matching `match` — a substring, regex, or predicate over the raw
86
+ * `ObservedMessage`. Returns the matched message.
87
+ *
88
+ * The implementation iterates the live `driver.observeMessages`
89
+ * stream, so messages sent *before* the call started are not
90
+ * considered; backfill is a Phase 2b helper.
77
91
  */
78
92
  export async function expectMessage(
79
- _driver: Driver,
80
- _chatId: number,
81
- _match: string | RegExp | ((m: ObservedMessage) => boolean),
82
- _opts: PollOptions & { threadId?: number; from?: "bot" | "user" },
93
+ driver: Driver,
94
+ chatId: number,
95
+ match: string | RegExp | ((m: ObservedMessage) => boolean),
96
+ opts: ExpectMessageOptions,
83
97
  ): Promise<ObservedMessage> {
84
- throw new Error("expectMessage not implemented (Phase 2)");
98
+ const predicate = compileMatcher(match);
99
+ const senderOk = compileSenderFilter(opts.senderFilter);
100
+ const iter = driver.observeMessages(chatId, opts.threadId !== undefined ? { threadId: opts.threadId } : undefined)[Symbol.asyncIterator]();
101
+ const deadline = Date.now() + opts.timeout;
102
+
103
+ try {
104
+ while (Date.now() < deadline) {
105
+ // Race the next observation against the remaining timeout so
106
+ // we don't hang forever if no messages arrive.
107
+ const remaining = deadline - Date.now();
108
+ const next = await raceTimeout(iter.next(), remaining);
109
+ if (next === "timeout") break;
110
+ if (next.done === true) break;
111
+ const msg = next.value;
112
+ if (!senderOk(msg)) continue;
113
+ if (predicate(msg)) return msg;
114
+ }
115
+ } finally {
116
+ await iter.return?.();
117
+ }
118
+ throw new Error(
119
+ `expectMessage: no matching message in chat=${chatId} within ${opts.timeout}ms`,
120
+ );
121
+ }
122
+
123
+ function compileMatcher(
124
+ match: string | RegExp | ((m: ObservedMessage) => boolean),
125
+ ): (m: ObservedMessage) => boolean {
126
+ if (typeof match === "string") return (m) => m.text.includes(match);
127
+ if (match instanceof RegExp) return (m) => match.test(m.text);
128
+ return match;
129
+ }
130
+
131
+ function compileSenderFilter(
132
+ f: ExpectMessageOptions["senderFilter"],
133
+ ): (m: ObservedMessage) => boolean {
134
+ if (!f) return () => true;
135
+ if ("userId" in f) return (m) => m.senderUserId === f.userId;
136
+ return (m) => m.senderUserId !== f.notUserId;
85
137
  }
86
138
 
139
+ function raceTimeout<T>(p: Promise<T>, ms: number): Promise<T | "timeout"> {
140
+ if (ms <= 0) return Promise.resolve("timeout");
141
+ return new Promise<T | "timeout">((resolve) => {
142
+ const t = setTimeout(() => resolve("timeout"), ms);
143
+ p.then((v) => {
144
+ clearTimeout(t);
145
+ resolve(v);
146
+ }).catch(() => {
147
+ clearTimeout(t);
148
+ resolve("timeout");
149
+ });
150
+ });
151
+ }
152
+
153
+ // ---------- Phase 2b stubs (deferred to follow-up PR) ----------
154
+
87
155
  /**
88
- * TODO(#866): wait for a reaction sequence on `messageId`. Each
89
- * emoji in `sequence` must appear (add op) in order; intermediate
90
- * other reactions are tolerated. Returns the full observed reaction
91
- * trail.
156
+ * Wait for a reaction `sequence` on `messageId` in `chatId`. Each
157
+ * emoji must appear (as an add `+` op) in the order given;
158
+ * intermediate add/remove ops for other emojis are tolerated.
159
+ *
160
+ * Returns the full observed trail (every add/remove seen up to and
161
+ * including the final match) so scenarios can do follow-up
162
+ * assertions on the order or count.
163
+ *
164
+ * Production note: the gateway calls `setMessageReaction` which
165
+ * REPLACES the prior emoji (not adds-in-addition-to). So an
166
+ * 👀 → 🤔 transition emits `-👀` and `+🤔` in the same observation
167
+ * window. This helper only watches the `+` ops, so the
168
+ * gateway-replace pattern reads as a clean sequence.
169
+ *
170
+ * Fast-turn note: turns shorter than the gateway's
171
+ * `progress-card initialDelayMs` may collapse intermediate
172
+ * reactions — you might only see 👀 and 👍. The sequence-match
173
+ * tolerates this: every emoji in `sequence` must appear in order,
174
+ * but we don't require it to be the ONLY emojis added.
92
175
  */
93
176
  export async function expectReaction(
94
- _driver: Driver,
95
- _chatId: number,
96
- _messageId: number,
97
- _sequence: string[],
98
- _opts: PollOptions,
177
+ driver: Driver,
178
+ chatId: number,
179
+ messageId: number,
180
+ sequence: string[],
181
+ opts: PollOptions,
99
182
  ): Promise<ObservedReaction[]> {
100
- throw new Error("expectReaction not implemented (Phase 2)");
183
+ if (sequence.length === 0) {
184
+ throw new Error("expectReaction: sequence must be non-empty");
185
+ }
186
+ const trail: ObservedReaction[] = [];
187
+ const iter = driver.observeReactions(chatId, { messageId })[Symbol.asyncIterator]();
188
+ const deadline = Date.now() + opts.timeout;
189
+ let cursor = 0;
190
+ try {
191
+ while (Date.now() < deadline && cursor < sequence.length) {
192
+ const remaining = deadline - Date.now();
193
+ const next = await raceTimeout(iter.next(), remaining);
194
+ if (next === "timeout") break;
195
+ if (next.done === true) break;
196
+ const r = next.value;
197
+ trail.push(r);
198
+ if (r.op === "+" && r.emoji === sequence[cursor]) {
199
+ cursor++;
200
+ }
201
+ }
202
+ } finally {
203
+ await iter.return?.();
204
+ }
205
+ if (cursor < sequence.length) {
206
+ throw new Error(
207
+ `expectReaction: saw ${cursor}/${sequence.length} expected emoji ` +
208
+ `(missing ${sequence.slice(cursor).map((e) => JSON.stringify(e)).join(", ")}) ` +
209
+ `on chat=${chatId} msg=${messageId} within ${opts.timeout}ms ` +
210
+ `(observed ops: ${trail.map((t) => `${t.op}${t.emoji}`).join(" ")})`,
211
+ );
212
+ }
213
+ return trail;
101
214
  }
102
215
 
216
+ export type CardPhase = "boot" | "working" | "background" | "done" | "error";
217
+
103
218
  export interface PinnedCardSnapshot {
219
+ chatId: number;
104
220
  messageId: number;
105
221
  text: string;
106
- html?: string;
107
- /** Production phase markers: `boot` | `working` | `done` | `error`. */
108
- phase: string;
222
+ /** Detected phase, or `"unknown"` when the text doesn't match any marker. */
223
+ phase: CardPhase | "unknown";
109
224
  }
110
225
 
111
226
  /**
112
- * TODO(#866): wait for a pinned message to appear in
113
- * `chatId`/topic (the progress card). Resolves with a snapshot of
114
- * its current text/phase.
227
+ * Wait for a pinned message to appear in `chatId` (the progress
228
+ * card). Resolves with a snapshot of its current text + phase.
229
+ *
230
+ * Implementation:
231
+ * 1. Subscribe to `driver.observePins(chatId)`.
232
+ * 2. On the first pin event, fetch the message text via
233
+ * `driver.getMessage` (the pin update carries only ids).
234
+ * 3. Return a snapshot with the parsed phase.
235
+ *
236
+ * Fast-turn note: the gateway's `progress_card.delay_ms` (default
237
+ * 45s) suppresses the card entirely for short turns. UAT runs
238
+ * against the standard-runtime test-harness agent will time out
239
+ * here unless the operator sets `delay_ms` to something small in
240
+ * `switchroom.yaml` under the agent's `channels.telegram` block.
115
241
  */
116
242
  export async function expectPinnedCard(
117
- _driver: Driver,
118
- _chatId: number,
119
- _opts: PollOptions & { threadId?: number },
243
+ driver: Driver,
244
+ chatId: number,
245
+ opts: PollOptions & { threadId?: number },
120
246
  ): Promise<PinnedCardSnapshot> {
121
- throw new Error("expectPinnedCard not implemented (Phase 2)");
247
+ void opts.threadId; // forum/topic routing rolls in with Phase 2d
248
+ const iter = driver.observePins(chatId)[Symbol.asyncIterator]();
249
+ const deadline = Date.now() + opts.timeout;
250
+ try {
251
+ while (Date.now() < deadline) {
252
+ const remaining = deadline - Date.now();
253
+ const next = await raceTimeout(iter.next(), remaining);
254
+ if (next === "timeout") break;
255
+ if (next.done === true) break;
256
+ const pin = next.value;
257
+ if (!pin.pinned) continue; // skip unpin events; we want pins
258
+ // Fetch the message body to compose the snapshot. If the
259
+ // message has been deleted in the gap between pin event and
260
+ // lookup, treat as a miss and keep waiting.
261
+ const msg = await driver.getMessage(chatId, pin.messageId).catch(() => null);
262
+ if (!msg) continue;
263
+ return {
264
+ chatId,
265
+ messageId: pin.messageId,
266
+ text: msg.text,
267
+ phase: detectPhase(msg.text),
268
+ };
269
+ }
270
+ } finally {
271
+ await iter.return?.();
272
+ }
273
+ throw new Error(
274
+ `expectPinnedCard: no pinned message in chat=${chatId} within ${opts.timeout}ms`,
275
+ );
122
276
  }
123
277
 
124
278
  /**
125
- * TODO(#866): wait for the pinned progress card to transition to
126
- * `phase`. The harness must read live edits, not just the snapshot
127
- * captured by `expectPinnedCard`.
279
+ * Wait for the pinned progress card to transition to `phase`.
280
+ *
281
+ * The gateway updates the card via `editMessage`, NOT by sending a
282
+ * new message. We observe edits via `driver.observeMessages` on
283
+ * the card's chat, filter to `msg.messageId === card.messageId`,
284
+ * and detect the target phase via the same regex set as
285
+ * `detectPhase`. If the snapshot we were handed is already at the
286
+ * target phase (e.g. a fast-turn scenario where the card was
287
+ * first-rendered straight at `done`), resolve immediately.
288
+ *
289
+ * The iterator yields a fresh `PinnedCardSnapshot` for each
290
+ * matching edit, so callers can chain phase transitions
291
+ * (`boot → working → done`) without re-subscribing.
128
292
  */
129
293
  export async function waitForCardPhase(
130
- _driver: Driver,
131
- _card: PinnedCardSnapshot,
132
- _phase: "boot" | "working" | "done" | "error",
133
- _opts: PollOptions,
294
+ driver: Driver,
295
+ card: PinnedCardSnapshot,
296
+ phase: CardPhase,
297
+ opts: PollOptions,
134
298
  ): Promise<PinnedCardSnapshot> {
135
- throw new Error("waitForCardPhase not implemented (Phase 2)");
299
+ if (detectPhase(card.text) === phase) {
300
+ // Refresh the phase field — the snapshot we were handed may
301
+ // have a stale `phase` from when the snapshot was captured
302
+ // (e.g. pin-time at "boot") even though the text has since
303
+ // been edited to a later phase. Returning the input as-is
304
+ // would surface that stale phase to the caller.
305
+ return { ...card, phase };
306
+ }
307
+ const iter = driver
308
+ .observeMessages(card.chatId)
309
+ [Symbol.asyncIterator]();
310
+ const deadline = Date.now() + opts.timeout;
311
+ try {
312
+ while (Date.now() < deadline) {
313
+ const remaining = deadline - Date.now();
314
+ const next = await raceTimeout(iter.next(), remaining);
315
+ if (next === "timeout") break;
316
+ if (next.done === true) break;
317
+ const msg = next.value;
318
+ if (msg.messageId !== card.messageId) continue;
319
+ const detected = detectPhase(msg.text);
320
+ if (detected === phase) {
321
+ return {
322
+ chatId: card.chatId,
323
+ messageId: card.messageId,
324
+ text: msg.text,
325
+ phase,
326
+ };
327
+ }
328
+ }
329
+ } finally {
330
+ await iter.return?.();
331
+ }
332
+ throw new Error(
333
+ `waitForCardPhase: card ${card.messageId} did not reach phase="${phase}" within ${opts.timeout}ms`,
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Detect the progress card's phase from its rendered text.
339
+ *
340
+ * The actual card render (telegram-plugin/progress-card.ts) uses
341
+ * emoji markers in the header: `✅` for done, `❌` for errors, `⚙️`
342
+ * while working (foreground), `🌀` for Background (parent done but
343
+ * fleet still running, see #862 / status-card-design.md §Header),
344
+ * and `⏳` during the boot-card window. These markers are stable
345
+ * enough to key on for UAT — finer parsing (checklist items,
346
+ * sub-agent row content) is out of scope.
347
+ *
348
+ * Phase precedence (highest first): error → done → background →
349
+ * working → boot. "background" sits above "working" because the
350
+ * Background phase implies parent reached terminal state — the
351
+ * card is no longer in foreground "working" mode even though the
352
+ * fleet is still alive. Tests asserting "is still working" should
353
+ * use either "working" or "background" depending on whether the
354
+ * parent has replied.
355
+ */
356
+ function detectPhase(text: string): CardPhase | "unknown" {
357
+ if (/❌|\bFailed\b|\bError\b/i.test(text)) return "error";
358
+ if (/✅|\bDone\b/i.test(text)) return "done";
359
+ if (/🌀|\bBackground\b/i.test(text)) return "background";
360
+ if (/⚙️|🤖|\bWorking\b/i.test(text)) return "working";
361
+ if (/⏳|\bStarting\b/i.test(text)) return "boot";
362
+ return "unknown";
136
363
  }
137
364
 
138
365
  function sleep(ms: number): Promise<void> {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * `bun uat/driver-info.ts` — prints the driver user account's
3
+ * numeric `user_id` to stdout. Used during one-shot `test-harness`
4
+ * agent creation (see uat/SETUP.md §5) to populate `--allow-from`.
5
+ *
6
+ * Issue: https://github.com/switchroom/switchroom/issues/866
7
+ *
8
+ * Reads vault credentials via `switchroom vault get --no-broker`
9
+ * (the operator already has `SWITCHROOM_VAULT_PASSPHRASE` set per
10
+ * the runbook). The session string lives in-process only; never
11
+ * printed.
12
+ */
13
+
14
+ import { execFileSync } from "node:child_process";
15
+ import { Driver } from "./driver.js";
16
+
17
+ async function main(): Promise<void> {
18
+ const apiIdRaw = vaultGet("telegram-uat-api-id");
19
+ const apiHash = vaultGet("telegram-uat-api-hash");
20
+ const session = vaultGet("telegram-uat-driver-session");
21
+
22
+ const apiId = Number.parseInt(apiIdRaw, 10);
23
+ if (!Number.isFinite(apiId)) {
24
+ process.stderr.write(
25
+ `uat/driver-info: invalid TELEGRAM_API_ID in vault (got '${apiIdRaw}')\n`,
26
+ );
27
+ process.exit(1);
28
+ }
29
+
30
+ const driver = new Driver({ apiId, apiHash, session });
31
+ try {
32
+ await driver.connect();
33
+ const uid = await driver.getMyUserId();
34
+ process.stdout.write(`${uid}\n`);
35
+ } finally {
36
+ await driver.disconnect().catch(() => undefined);
37
+ }
38
+ }
39
+
40
+ function vaultGet(key: string): string {
41
+ const out = execFileSync(
42
+ "switchroom",
43
+ ["vault", "get", "--no-broker", key],
44
+ { encoding: "utf8", stdio: ["inherit", "pipe", "inherit"] },
45
+ );
46
+ return out.trim();
47
+ }
48
+
49
+ main().catch((err) => {
50
+ // Strip any long base64 blob defensively (same posture as login.ts).
51
+ const sanitized = String(err?.message ?? err).replace(
52
+ /[A-Za-z0-9+/=_-]{64,}/g,
53
+ "<redacted>",
54
+ );
55
+ process.stderr.write(`uat/driver-info failed: ${sanitized}\n`);
56
+ process.exit(1);
57
+ });