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
@@ -1,559 +0,0 @@
1
- /**
2
- * Dashboard v3b — active/fallback marking + promote verb tests.
3
- *
4
- * Three surfaces under test:
5
- * 1. encodeCallbackData / parseCallbackData round-trip for the new
6
- * `account-promote` + `confirm-account-promote` verbs (`apr`/`cpr`).
7
- * 2. formatQuotaBar — the mini-bar renderer used on the active row.
8
- * 3. buildDashboardText / buildDashboardKeyboard — verifies the `▶`
9
- * glyph floats the active account, the "Fallback ↓:" subhead
10
- * appears when there's a distinguished active row, and that the
11
- * v3a unmarked layout is preserved when no account claims active
12
- * (older CLI without primaryForAgents).
13
- *
14
- * Pure module — no gateway/Telegram side effects.
15
- */
16
-
17
- import { describe, expect, it } from "vitest";
18
- import {
19
- encodeCallbackData,
20
- parseCallbackData,
21
- formatQuotaBar,
22
- buildDashboardText,
23
- buildDashboardKeyboard,
24
- buildAccountPromoteConfirmKeyboard,
25
- CALLBACK_BUDGET_BYTES,
26
- type AccountSummary,
27
- type DashboardState,
28
- } from "../auth-dashboard.js";
29
-
30
- const baseState: Omit<DashboardState, "accounts"> = {
31
- agent: "clerk",
32
- bankId: "clerk",
33
- plan: "max",
34
- rateLimitTier: "default_claude_max_20x",
35
- slots: [],
36
- quotaHot: false,
37
- };
38
-
39
- const acc = (
40
- label: string,
41
- overrides: Partial<AccountSummary> = {},
42
- ): AccountSummary => ({
43
- label,
44
- health: "healthy",
45
- enabledHere: true,
46
- ...overrides,
47
- });
48
-
49
- describe("v3b: account-promote callback round-trip", () => {
50
- it("encodes and decodes account-promote (verb apr)", () => {
51
- const encoded = encodeCallbackData({
52
- kind: "account-promote",
53
- agent: "clerk",
54
- label: "pixsoul@gmail.com",
55
- });
56
- expect(encoded).toBe("auth:apr:clerk:pixsoul@gmail.com");
57
- expect(parseCallbackData(encoded)).toEqual({
58
- kind: "account-promote",
59
- agent: "clerk",
60
- label: "pixsoul@gmail.com",
61
- });
62
- });
63
-
64
- it("encodes and decodes confirm-account-promote (verb cpr)", () => {
65
- const encoded = encodeCallbackData({
66
- kind: "confirm-account-promote",
67
- agent: "clerk",
68
- label: "me@kenthompson.com.au",
69
- });
70
- expect(encoded).toBe("auth:cpr:clerk:me@kenthompson.com.au");
71
- expect(parseCallbackData(encoded)).toEqual({
72
- kind: "confirm-account-promote",
73
- agent: "clerk",
74
- label: "me@kenthompson.com.au",
75
- });
76
- });
77
-
78
- it("rejects labels with disallowed characters", () => {
79
- // `/` is rejected by isSafeAccountLabel — would create on-disk
80
- // ambiguity under ~/.switchroom/accounts/.
81
- expect(parseCallbackData("auth:apr:clerk:bad/label")).toEqual({
82
- kind: "noop",
83
- });
84
- // Whitespace, quotes, etc.
85
- expect(parseCallbackData("auth:apr:clerk:bad label")).toEqual({
86
- kind: "noop",
87
- });
88
- });
89
-
90
- it("rejects payloads beyond the 64-byte cap", () => {
91
- const longLabel = "a".repeat(60);
92
- const overlong = `auth:cpr:agent:${longLabel}`;
93
- // sanity — payload exceeds the cap
94
- expect(Buffer.byteLength(overlong, "utf8")).toBeGreaterThan(
95
- CALLBACK_BUDGET_BYTES,
96
- );
97
- expect(parseCallbackData(overlong)).toEqual({ kind: "noop" });
98
- });
99
-
100
- it("rejects empty label segment", () => {
101
- expect(parseCallbackData("auth:apr:clerk:")).toEqual({ kind: "noop" });
102
- });
103
- });
104
-
105
- describe("v3b: formatQuotaBar", () => {
106
- it("renders all-empty for 0%", () => {
107
- expect(formatQuotaBar(0)).toBe("░░░░░░");
108
- });
109
-
110
- it("renders all-full for 100%", () => {
111
- expect(formatQuotaBar(100)).toBe("██████");
112
- });
113
-
114
- it("clamps below full for 99% so the bar reads visibly under the cap", () => {
115
- // Critical UX point: a 99% account is one bad turn from exhaustion.
116
- // The bar must NOT show as full. The cell math floors, so 99/100*6
117
- // = 5.94 → 5 filled cells.
118
- expect(formatQuotaBar(99)).toBe("█████░");
119
- });
120
-
121
- it("scales linearly across the range", () => {
122
- expect(formatQuotaBar(50)).toBe("███░░░");
123
- expect(formatQuotaBar(33)).toBe("█░░░░░"); // 33/100*6=1.98 → 1
124
- expect(formatQuotaBar(17)).toBe("█░░░░░"); // 17/100*6=1.02 → 1
125
- expect(formatQuotaBar(83)).toBe("████░░"); // 83/100*6=4.98 → 4
126
- });
127
-
128
- it("clamps negative or >100 inputs to the legal range", () => {
129
- expect(formatQuotaBar(-5)).toBe("░░░░░░");
130
- expect(formatQuotaBar(150)).toBe("██████");
131
- });
132
-
133
- it("supports a custom cell count", () => {
134
- expect(formatQuotaBar(50, 10)).toBe("█████░░░░░");
135
- expect(formatQuotaBar(0, 0)).toBe("");
136
- });
137
- });
138
-
139
- describe("v3b: buildDashboardText — active-row marking", () => {
140
- it("floats the activeForThisAgent row to the top with a ▶ glyph", () => {
141
- const state: DashboardState = {
142
- ...baseState,
143
- accounts: [
144
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
145
- acc("me@kenthompson.com.au"),
146
- acc("ken.thompson@outlook.com.au"),
147
- ],
148
- };
149
- const text = buildDashboardText(state);
150
- const pixIdx = text.indexOf("pixsoul@gmail.com");
151
- const meIdx = text.indexOf("me@kenthompson.com.au");
152
- expect(pixIdx).toBeGreaterThan(-1);
153
- expect(meIdx).toBeGreaterThan(-1);
154
- // Active row precedes fallbacks in the rendered text.
155
- expect(pixIdx).toBeLessThan(meIdx);
156
- // ▶ glyph appears on the active row, before the label.
157
- const arrowIdx = text.indexOf("▶");
158
- expect(arrowIdx).toBeGreaterThan(-1);
159
- expect(arrowIdx).toBeLessThan(pixIdx);
160
- });
161
-
162
- it("emits a 'Fallback ↓:' subhead when there's a distinguished active row", () => {
163
- const state: DashboardState = {
164
- ...baseState,
165
- accounts: [
166
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
167
- acc("me@kenthompson.com.au"),
168
- ],
169
- };
170
- expect(buildDashboardText(state)).toContain("Fallback");
171
- });
172
-
173
- it("falls back to the v3a unmarked layout when no account claims active", () => {
174
- // Older CLI without primaryForAgents → activeForThisAgent is unset
175
- // on every account → no ▶ glyph, no Fallback subhead. The v3a
176
- // bullet-list rendering still works.
177
- const state: DashboardState = {
178
- ...baseState,
179
- accounts: [acc("pixsoul@gmail.com"), acc("me@kenthompson.com.au")],
180
- };
181
- const text = buildDashboardText(state);
182
- expect(text).not.toContain("▶");
183
- expect(text).not.toContain("Fallback");
184
- // Both labels still appear.
185
- expect(text).toContain("pixsoul@gmail.com");
186
- expect(text).toContain("me@kenthompson.com.au");
187
- });
188
-
189
- it("renders inline mini-bars on the active row when both percentages are known", () => {
190
- const state: DashboardState = {
191
- ...baseState,
192
- accounts: [
193
- acc("pixsoul@gmail.com", {
194
- activeForThisAgent: true,
195
- fiveHourPct: 47,
196
- sevenDayPct: 12,
197
- }),
198
- ],
199
- };
200
- const text = buildDashboardText(state);
201
- // Both bars present (the "█"/"░" cells appear in the active-row's
202
- // inline summary). Spot-check the 47% → "██░░░░░" (47/100*6=2.82
203
- // → 2 filled cells) and 12% → "░░░░░░" (12/100*6=0.72 → 0 filled).
204
- expect(text).toContain(formatQuotaBar(47));
205
- expect(text).toContain(formatQuotaBar(12));
206
- expect(text).toContain("47%");
207
- expect(text).toContain("12%");
208
- });
209
-
210
- it("falls back to the legacy quota-line on the active row when only one percentage is known", () => {
211
- const state: DashboardState = {
212
- ...baseState,
213
- accounts: [
214
- acc("pixsoul@gmail.com", {
215
- activeForThisAgent: true,
216
- fiveHourPct: 47,
217
- // sevenDayPct intentionally absent
218
- }),
219
- ],
220
- };
221
- const text = buildDashboardText(state);
222
- // No mini-bar (would require both); the legacy line shows just 5h.
223
- expect(text).toContain("47%");
224
- expect(text).not.toContain("12%");
225
- });
226
-
227
- it("uses the existing 'exhausted · resets in …' line when active is exhausted", () => {
228
- const state: DashboardState = {
229
- ...baseState,
230
- accounts: [
231
- acc("pixsoul@gmail.com", {
232
- activeForThisAgent: true,
233
- quotaExhaustedUntil: Date.now() + 90 * 60_000,
234
- fiveHourPct: 100,
235
- sevenDayPct: 50,
236
- }),
237
- ],
238
- };
239
- const text = buildDashboardText(state);
240
- expect(text).toContain("exhausted");
241
- expect(text).toContain("resets in");
242
- });
243
- });
244
-
245
- describe("v3c: buildDashboardKeyboard — single Switch primary button", () => {
246
- // v3c replaces the v3b per-fallback `⤴ Promote` flood with a single
247
- // `🔀 Switch primary →` entry that opens a picker sub-keyboard.
248
- // Pin the visibility rules + the picker behaviour so a refactor can't
249
- // silently re-surface the v3b button explosion.
250
- const renderRows = (
251
- accounts: AccountSummary[],
252
- ): Array<Array<{ text: string; data: string }>> => {
253
- const kb = buildDashboardKeyboard({ ...baseState, accounts });
254
- const raw = (kb as unknown as { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> })
255
- .inline_keyboard;
256
- return raw.map((row) =>
257
- row.map((b) => ({ text: b.text, data: b.callback_data })),
258
- );
259
- };
260
-
261
- it("emits exactly ONE `🔀 Switch primary →` button when fallbacks exist", () => {
262
- const rows = renderRows([
263
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
264
- acc("me@kenthompson.com.au"),
265
- acc("ken.thompson@outlook.com.au"),
266
- ]);
267
- const switchButtons = rows
268
- .flat()
269
- .filter((b) => b.text.includes("Switch primary"));
270
- expect(switchButtons.length).toBe(1);
271
- expect(switchButtons[0].data).toBe("auth:spv:clerk");
272
- });
273
-
274
- it("hides the Switch primary button when no fallback exists", () => {
275
- // Only one account, and it's already active → nothing to switch to.
276
- const rows = renderRows([
277
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
278
- ]);
279
- const switchButtons = rows
280
- .flat()
281
- .filter((b) => b.text.includes("Switch primary"));
282
- expect(switchButtons.length).toBe(0);
283
- });
284
-
285
- it("hides the Switch primary button when no account claims active", () => {
286
- // Older CLI without primaryForAgents → activeForThisAgent unset
287
- // everywhere → can't tell which account to keep, so no picker.
288
- const rows = renderRows([
289
- acc("pixsoul@gmail.com"),
290
- acc("me@kenthompson.com.au"),
291
- ]);
292
- const switchButtons = rows
293
- .flat()
294
- .filter((b) => b.text.includes("Switch primary"));
295
- expect(switchButtons.length).toBe(0);
296
- });
297
-
298
- it("does NOT emit per-fallback ⤴ Promote buttons on the main board", () => {
299
- // The whole point of v3c — kill the button flood.
300
- const rows = renderRows([
301
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
302
- acc("me@kenthompson.com.au"),
303
- acc("ken.thompson@outlook.com.au"),
304
- ]);
305
- const promoteRows = rows.flat().filter((b) => b.text.includes("⤴ Promote"));
306
- expect(promoteRows.length).toBe(0);
307
- });
308
-
309
- it("does NOT emit per-account drilldown buttons on the main board", () => {
310
- // v3c also drops the per-account `account-view` drilldown buttons
311
- // (av verb) — the text already names every account, the sub-views
312
- // are reachable via Switch primary / Reauth / Add buttons.
313
- const rows = renderRows([
314
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
315
- acc("me@kenthompson.com.au"),
316
- ]);
317
- const drilldownRows = rows
318
- .flat()
319
- .filter((b) => b.data.startsWith("auth:av:"));
320
- expect(drilldownRows.length).toBe(0);
321
- });
322
- });
323
-
324
- describe("v3c: buildSwitchPrimaryKeyboard — picker", () => {
325
- it("emits one row per candidate, each fires confirm-account-promote", async () => {
326
- const { buildSwitchPrimaryKeyboard } = await import("../auth-dashboard.js");
327
- const kb = buildSwitchPrimaryKeyboard("clerk", [
328
- { label: "me@kenthompson.com.au", health: "healthy" },
329
- { label: "ken.thompson@outlook.com.au", health: "healthy" },
330
- ]);
331
- const raw = (kb as unknown as {
332
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
333
- }).inline_keyboard;
334
- // 2 candidate rows + 1 cancel row.
335
- expect(raw.length).toBe(3);
336
- expect(raw[0][0].callback_data).toBe(
337
- "auth:cpr:clerk:me@kenthompson.com.au",
338
- );
339
- expect(raw[1][0].callback_data).toBe(
340
- "auth:cpr:clerk:ken.thompson@outlook.com.au",
341
- );
342
- // Cancel returns to the main board via refresh.
343
- expect(raw[2][0].text).toContain("Cancel");
344
- expect(raw[2][0].callback_data).toBe("auth:refresh:clerk");
345
- });
346
-
347
- it("renders a noop fallback when a candidate's payload exceeds 64 bytes", async () => {
348
- const { buildSwitchPrimaryKeyboard } = await import("../auth-dashboard.js");
349
- const kb = buildSwitchPrimaryKeyboard("a".repeat(50), [
350
- { label: "b".repeat(50), health: "healthy" },
351
- ]);
352
- const raw = (kb as unknown as {
353
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
354
- }).inline_keyboard;
355
- const guarded = raw[0][0];
356
- expect(guarded.text).toContain("(use CLI)");
357
- expect(guarded.callback_data).toBe("auth:noop");
358
- });
359
-
360
- it("appends health suffix to each candidate row", async () => {
361
- const { buildSwitchPrimaryKeyboard } = await import("../auth-dashboard.js");
362
- const kb = buildSwitchPrimaryKeyboard("clerk", [
363
- { label: "expired@x.com", health: "expired" },
364
- { label: "good@x.com", health: "healthy" },
365
- ]);
366
- const raw = (kb as unknown as {
367
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
368
- }).inline_keyboard;
369
- expect(raw[0][0].text).toContain("⌛");
370
- expect(raw[1][0].text).not.toContain("⌛");
371
- expect(raw[1][0].text).not.toContain("⚠");
372
- });
373
- });
374
-
375
- describe("v3c: switch-primary-view callback round-trip", () => {
376
- it("encodes and decodes (verb spv)", () => {
377
- const encoded = encodeCallbackData({
378
- kind: "switch-primary-view",
379
- agent: "clerk",
380
- });
381
- expect(encoded).toBe("auth:spv:clerk");
382
- expect(parseCallbackData(encoded)).toEqual({
383
- kind: "switch-primary-view",
384
- agent: "clerk",
385
- });
386
- });
387
-
388
- it("rejects unsafe agent names", () => {
389
- expect(parseCallbackData("auth:spv:bad/agent")).toEqual({ kind: "noop" });
390
- });
391
- });
392
-
393
- describe("v3b: Slots + Pool sections hide when active-account signal is present", () => {
394
- // The slot row was rendering `● pixsoul@gmail.com (active) ✓ healthy`
395
- // when the active label was known — a 1:1 duplicate of the
396
- // `▶ pixsoul@gmail.com ✓` active-account row above. Same for the
397
- // `Pool: pixsoul@gmail.com is active` line. So we hide both sections
398
- // entirely under the new account model. Pin the visibility rules so
399
- // a refactor can't silently re-surface the duplication.
400
- const slotRowState = (
401
- activeAccountLabel: string | null,
402
- ): DashboardState => ({
403
- ...baseState,
404
- slots: [
405
- {
406
- slot: "default",
407
- active: true,
408
- health: "active",
409
- },
410
- ],
411
- accounts:
412
- activeAccountLabel != null
413
- ? [
414
- acc(activeAccountLabel, { activeForThisAgent: true }),
415
- acc("ken.thompson@outlook.com.au"),
416
- ]
417
- : [acc("ken.thompson@outlook.com.au")],
418
- });
419
-
420
- it("hides the Slots section entirely when an active-account signal is present", () => {
421
- const text = buildDashboardText(slotRowState("pixsoul@gmail.com"));
422
- // No "Slots (N)" header, no "default" leaking out, no Pool line.
423
- expect(text).not.toContain("Slots (");
424
- expect(text).not.toContain("default");
425
- expect(text).not.toMatch(/Pool:/);
426
- // The ▶ active row is the single source of truth for what's active.
427
- expect(text).toContain("▶");
428
- expect(text).toContain("pixsoul@gmail.com");
429
- });
430
-
431
- it("keeps the legacy Slots + Pool layout when accounts have no active signal", () => {
432
- // Older CLIs don't emit primaryForAgents → no activeForThisAgent
433
- // is set on any account → slots section is the only signal of
434
- // "what's active." Preserve it for graceful degradation.
435
- const text = buildDashboardText(slotRowState(null));
436
- expect(text).toContain("Slots (");
437
- expect(text).toContain("<code>default</code> (active)");
438
- expect(text).toContain("Pool:");
439
- });
440
-
441
- it("keeps the Slots section visible when no accounts exist (fresh-fleet bootstrap)", () => {
442
- // Bootstrap path: no accounts yet, the operator's only handle is
443
- // the slot — they need [➕ Add slot] / [🔄 Reauth] to work.
444
- const text = buildDashboardText({
445
- ...baseState,
446
- slots: [{ slot: "default", active: true, health: "active" }],
447
- accounts: [],
448
- });
449
- expect(text).toContain("Slots (");
450
- expect(text).toContain("default");
451
- });
452
- });
453
-
454
- describe("v3b: buildAccountPromoteConfirmKeyboard", () => {
455
- it("emits a confirm row whose callback dispatches confirm-account-promote", () => {
456
- const kb = buildAccountPromoteConfirmKeyboard("clerk", "pixsoul@gmail.com");
457
- const raw = (kb as unknown as { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> }).inline_keyboard;
458
- const confirm = raw.flat().find((b) => b.text.includes("Confirm promote"));
459
- expect(confirm?.callback_data).toBe("auth:cpr:clerk:pixsoul@gmail.com");
460
- const cancel = raw.flat().find((b) => b.text.includes("Cancel"));
461
- expect(cancel?.callback_data).toBe("auth:refresh:clerk");
462
- });
463
- });
464
-
465
- describe("regression: button count cap on the main board", () => {
466
- // Real-world wedge: a screenshot from /auth showed 8 buttons stacked
467
- // vertically on a three-account fleet (the v3b explosion). v3c
468
- // collapsed everything into a Switch primary picker. Pin the cap so
469
- // a future "let's add one more affordance" PR can't bring it back.
470
- const renderRows = (accounts: AccountSummary[]): number => {
471
- const kb = buildDashboardKeyboard({ ...baseState, accounts });
472
- return (
473
- kb as unknown as {
474
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
475
- }
476
- ).inline_keyboard.length;
477
- };
478
-
479
- it("renders <=6 keyboard rows with three accounts (down from 8 in v3b)", () => {
480
- // pixsoul (active) + 2 fallbacks. Expected layout:
481
- // row 1: 🔀 Switch primary →
482
- // row 2: 🔄 Reauth + ➕ Add slot (2 buttons, 1 row)
483
- // row 3: 📊 Full quota
484
- // row 4: 🔁 Refresh
485
- // = 4 rows. Cap at 6 leaves room for a future row without letting
486
- // the v3b explosion return.
487
- expect(
488
- renderRows([
489
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
490
- acc("me@kenthompson.com.au"),
491
- acc("ken.thompson@outlook.com.au"),
492
- ]),
493
- ).toBeLessThanOrEqual(6);
494
- });
495
-
496
- it("never emits a Promote button targeting the active account", () => {
497
- // The original screenshot bug: ⤴ Promote pixsoul@gmail.com
498
- // appeared even when pixsoul was the active row. Pin that no
499
- // promote callback (apr/cpr verbs) targets the active label.
500
- const kb = buildDashboardKeyboard({
501
- ...baseState,
502
- accounts: [
503
- acc("pixsoul@gmail.com", { activeForThisAgent: true }),
504
- acc("me@kenthompson.com.au"),
505
- ],
506
- });
507
- const allButtons = (
508
- kb as unknown as {
509
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
510
- }
511
- ).inline_keyboard.flat();
512
- for (const btn of allButtons) {
513
- const m = btn.callback_data.match(/^auth:(?:apr|cpr):[^:]+:(.+)$/);
514
- if (m) {
515
- expect(m[1], "active label found in promote callback").not.toBe(
516
- "pixsoul@gmail.com",
517
- );
518
- }
519
- }
520
- });
521
- });
522
-
523
- describe("regression: [⚠️ Fall back now] button stays gone (v0.6.11)", () => {
524
- // Removed when the Switch primary picker became the operator-facing
525
- // surface for the same outcome. Two paths to the same action
526
- // confused operators. If quotaHot ever re-surfaces the button, this
527
- // test catches it.
528
- it("absent regardless of quotaHot, slot health, or accounts shape", () => {
529
- const cases: Array<Parameters<typeof buildDashboardKeyboard>[0]> = [
530
- { ...baseState, quotaHot: false },
531
- { ...baseState, quotaHot: true },
532
- {
533
- ...baseState,
534
- quotaHot: true,
535
- slots: [{ slot: "default", active: true, health: "quota-exhausted" }],
536
- },
537
- {
538
- ...baseState,
539
- accounts: [
540
- acc("pixsoul", { activeForThisAgent: true, fiveHourPct: 99 }),
541
- ],
542
- },
543
- ];
544
- for (const state of cases) {
545
- const kb = buildDashboardKeyboard(state);
546
- const labels = (
547
- kb as unknown as {
548
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
549
- }
550
- ).inline_keyboard
551
- .flat()
552
- .map((b) => b.text);
553
- expect(
554
- labels.some((t) => /fall.?back/i.test(t)),
555
- `Fall back surfaced under quotaHot=${state.quotaHot}, slots=${state.slots?.length}`,
556
- ).toBe(false);
557
- }
558
- });
559
- });