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,531 @@
1
+ /**
2
+ * `/auth` CLI-vernacular alignment coverage (RFC H Decision 11 —
3
+ * "same shape on the CLI and in Telegram").
4
+ *
5
+ * Pins the post-/auth-add verb tree that mirrors `switchroom auth`:
6
+ *
7
+ * list / show [<agent>] / rm <label> [confirm] / refresh [<label>]
8
+ * / agent override <agent> <label|clear> / help
9
+ *
10
+ * The headline guarantees:
11
+ *
12
+ * 1. Every verb resolves through the pure parser to the right
13
+ * ParsedAuthCommand kind (no I/O in `parseAuthCommand`).
14
+ * 2. Read verbs (`show`, `list`, `show <agent>`, `help`) are open
15
+ * to any agent; mutating verbs are admin-gated.
16
+ * 3. The `rm` two-step confirm is paired by chat id + label and
17
+ * respects the 60s TTL.
18
+ * 4. `rm` refuses to even prompt when the label is the fleet active
19
+ * (broker enforces too, but the chat surface short-circuits for
20
+ * a cleaner error).
21
+ * 5. `refresh` (no label) iterates every known account, once each.
22
+ * 6. `override` set vs clear translates the chat-ergonomic `clear`
23
+ * keyword to a `null` broker argument.
24
+ * 7. Help text lists every verb (string-contains).
25
+ *
26
+ * Sibling to `auth-add-flow.test.ts` — keeps the new surface's tests
27
+ * scoped to a dedicated file rather than ballooning that one further.
28
+ */
29
+
30
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
31
+
32
+ import {
33
+ parseAuthCommand,
34
+ handleAuthCommand,
35
+ pendingAuthRmFlows,
36
+ AUTH_RM_CONFIRM_TTL_MS,
37
+ type AuthBrokerClient,
38
+ type ListStateData,
39
+ } from '../gateway/auth-command.js'
40
+
41
+ /* ── Fixture builders ─────────────────────────────────────────────────── */
42
+
43
+ function fakeState(over: Partial<ListStateData> = {}): ListStateData {
44
+ return {
45
+ active: 'primary',
46
+ fallback_order: ['primary', 'spare'],
47
+ accounts: [
48
+ {
49
+ label: 'primary',
50
+ expiresAt: Date.now() + 6 * 3600_000,
51
+ exhausted: false,
52
+ last_refreshed_at: Date.now() - 600_000,
53
+ },
54
+ {
55
+ label: 'spare',
56
+ expiresAt: Date.now() + 4 * 3600_000,
57
+ exhausted: false,
58
+ },
59
+ ],
60
+ agents: [
61
+ { name: 'clerk', account: 'primary', override: null },
62
+ { name: 'researcher', account: 'spare', override: 'spare' },
63
+ ],
64
+ consumers: [],
65
+ ...over,
66
+ }
67
+ }
68
+
69
+ interface MockClient extends AuthBrokerClient {
70
+ listState: ReturnType<typeof vi.fn>
71
+ setActive: ReturnType<typeof vi.fn>
72
+ rmAccount: ReturnType<typeof vi.fn>
73
+ refreshAccount: ReturnType<typeof vi.fn>
74
+ setOverride: ReturnType<typeof vi.fn>
75
+ }
76
+
77
+ function mockClient(state: ListStateData = fakeState()): MockClient {
78
+ return {
79
+ listState: vi.fn().mockResolvedValue(state),
80
+ setActive: vi.fn().mockResolvedValue({ active: 'spare', fanned: ['clerk'] }),
81
+ rmAccount: vi.fn().mockImplementation(async (label: string) => ({ label })),
82
+ refreshAccount: vi.fn().mockImplementation(async (label: string) => ({
83
+ account: label,
84
+ expiresAt: Date.now() + 8 * 3600_000,
85
+ })),
86
+ setOverride: vi
87
+ .fn()
88
+ .mockImplementation(async (agent: string, account: string | null) => ({
89
+ agent,
90
+ account,
91
+ })),
92
+ }
93
+ }
94
+
95
+ beforeEach(() => {
96
+ pendingAuthRmFlows.clear()
97
+ })
98
+
99
+ /* ── 1. Parser ────────────────────────────────────────────────────────── */
100
+
101
+ describe('parseAuthCommand — new verbs', () => {
102
+ it('parses /auth list as { kind: "list" }', () => {
103
+ expect(parseAuthCommand('/auth list')).toEqual({ kind: 'list' })
104
+ })
105
+
106
+ it('parses /auth show <agent> as { kind: "show", agent }', () => {
107
+ expect(parseAuthCommand('/auth show clerk')).toEqual({
108
+ kind: 'show',
109
+ agent: 'clerk',
110
+ })
111
+ })
112
+
113
+ it('bare /auth show stays kindshow with no agent field set', () => {
114
+ const p = parseAuthCommand('/auth show')
115
+ expect(p?.kind).toBe('show')
116
+ expect((p as { agent?: string }).agent).toBeUndefined()
117
+ })
118
+
119
+ it('parses /auth rm <label> as rm-prompt', () => {
120
+ expect(parseAuthCommand('/auth rm spare')).toEqual({
121
+ kind: 'rm-prompt',
122
+ label: 'spare',
123
+ })
124
+ })
125
+
126
+ it('parses /auth rm <label> confirm as rm-confirmed (case-insensitive)', () => {
127
+ expect(parseAuthCommand('/auth rm spare confirm')).toEqual({
128
+ kind: 'rm-confirmed',
129
+ label: 'spare',
130
+ })
131
+ expect(parseAuthCommand('/auth rm spare CONFIRM')).toEqual({
132
+ kind: 'rm-confirmed',
133
+ label: 'spare',
134
+ })
135
+ })
136
+
137
+ it('rejects /auth rm <label> <bogus> with a help reason', () => {
138
+ const p = parseAuthCommand('/auth rm spare yesplease')
139
+ expect(p?.kind).toBe('help')
140
+ expect((p as { reason?: string }).reason).toMatch(/confirm/i)
141
+ })
142
+
143
+ it('rejects /auth rm with no label', () => {
144
+ const p = parseAuthCommand('/auth rm')
145
+ expect(p?.kind).toBe('help')
146
+ expect((p as { reason?: string }).reason).toMatch(/usage/i)
147
+ })
148
+
149
+ it('parses /auth refresh (no label)', () => {
150
+ expect(parseAuthCommand('/auth refresh')).toEqual({ kind: 'refresh' })
151
+ })
152
+
153
+ it('parses /auth refresh <label>', () => {
154
+ expect(parseAuthCommand('/auth refresh primary')).toEqual({
155
+ kind: 'refresh',
156
+ label: 'primary',
157
+ })
158
+ })
159
+
160
+ it('parses /auth agent override <agent> <label>', () => {
161
+ expect(parseAuthCommand('/auth agent override clerk primary')).toEqual({
162
+ kind: 'override-set',
163
+ agent: 'clerk',
164
+ label: 'primary',
165
+ })
166
+ })
167
+
168
+ it('parses /auth agent override <agent> clear as override-clear', () => {
169
+ expect(parseAuthCommand('/auth agent override clerk clear')).toEqual({
170
+ kind: 'override-clear',
171
+ agent: 'clerk',
172
+ })
173
+ // case-insensitive
174
+ expect(parseAuthCommand('/auth agent override clerk CLEAR')).toEqual({
175
+ kind: 'override-clear',
176
+ agent: 'clerk',
177
+ })
178
+ })
179
+
180
+ it('rejects /auth agent override with missing args', () => {
181
+ const a = parseAuthCommand('/auth agent override')
182
+ const b = parseAuthCommand('/auth agent override clerk')
183
+ expect(a?.kind).toBe('help')
184
+ expect(b?.kind).toBe('help')
185
+ })
186
+
187
+ it('rejects /auth agent <unknown-sub>', () => {
188
+ const p = parseAuthCommand('/auth agent pin clerk primary')
189
+ expect(p?.kind).toBe('help')
190
+ expect((p as { reason?: string }).reason).toMatch(/override/i)
191
+ })
192
+
193
+ it('parses /auth help explicitly', () => {
194
+ expect(parseAuthCommand('/auth help')).toEqual({ kind: 'help' })
195
+ })
196
+
197
+ it('routes unknown verbs to help with a reason', () => {
198
+ const p = parseAuthCommand('/auth nonsense')
199
+ expect(p?.kind).toBe('help')
200
+ expect((p as { reason?: string }).reason).toMatch(/unknown/i)
201
+ })
202
+
203
+ it('tolerates extra whitespace and bot-suffix', () => {
204
+ expect(parseAuthCommand(' /auth list ')).toEqual({ kind: 'list' })
205
+ expect(parseAuthCommand('/auth@switchroombot list')).toEqual({ kind: 'list' })
206
+ expect(parseAuthCommand('/auth\tshow\tclerk')).toEqual({
207
+ kind: 'show',
208
+ agent: 'clerk',
209
+ })
210
+ })
211
+
212
+ it('is case-insensitive on the verb', () => {
213
+ expect(parseAuthCommand('/auth LIST')?.kind).toBe('list')
214
+ expect(parseAuthCommand('/auth REFRESH')?.kind).toBe('refresh')
215
+ expect(parseAuthCommand('/auth Agent OVERRIDE clerk clear')).toEqual({
216
+ kind: 'override-clear',
217
+ agent: 'clerk',
218
+ })
219
+ })
220
+ })
221
+
222
+ /* ── 2. Read-verb open access ─────────────────────────────────────────── */
223
+
224
+ describe('handleAuthCommand — read verbs are open to any agent', () => {
225
+ it('/auth list renders the fleet snapshot without an admin gate', async () => {
226
+ const client = mockClient()
227
+ const reply = await handleAuthCommand(
228
+ { kind: 'list' },
229
+ { agentName: 'random-agent', isAdmin: false, client },
230
+ )
231
+ expect(reply.html).toBe(true)
232
+ expect(reply.text).toMatch(/Auth — fleet snapshot/)
233
+ expect(reply.text).not.toMatch(/Not authorized/i)
234
+ expect(client.listState).toHaveBeenCalledTimes(1)
235
+ })
236
+
237
+ it('/auth show <agent> renders per-agent detail for any agent', async () => {
238
+ const client = mockClient()
239
+ const reply = await handleAuthCommand(
240
+ { kind: 'show', agent: 'researcher' },
241
+ { agentName: 'random', isAdmin: false, client },
242
+ )
243
+ expect(reply.text).toMatch(/researcher/)
244
+ expect(reply.text).toMatch(/override/)
245
+ expect(reply.text).toMatch(/spare/)
246
+ })
247
+
248
+ it('/auth show <unknown-agent> returns a friendly error', async () => {
249
+ const client = mockClient()
250
+ const reply = await handleAuthCommand(
251
+ { kind: 'show', agent: 'ghost' },
252
+ { agentName: 'random', isAdmin: false, client },
253
+ )
254
+ expect(reply.text).toMatch(/no agent named/i)
255
+ expect(reply.text).toMatch(/ghost/)
256
+ })
257
+ })
258
+
259
+ /* ── 3. Admin gating ──────────────────────────────────────────────────── */
260
+
261
+ describe('handleAuthCommand — admin gating', () => {
262
+ const nonAdmin = { agentName: 'snooper', isAdmin: false }
263
+
264
+ it('refuses /auth rm <label> for non-admin', async () => {
265
+ const client = mockClient()
266
+ const reply = await handleAuthCommand(
267
+ { kind: 'rm-prompt', label: 'spare' },
268
+ { ...nonAdmin, client },
269
+ )
270
+ expect(reply.text).toMatch(/Not authorized/i)
271
+ expect(client.listState).not.toHaveBeenCalled()
272
+ })
273
+
274
+ it('refuses /auth rm <label> confirm for non-admin', async () => {
275
+ const client = mockClient()
276
+ const reply = await handleAuthCommand(
277
+ { kind: 'rm-confirmed', label: 'spare' },
278
+ { ...nonAdmin, client },
279
+ )
280
+ expect(reply.text).toMatch(/Not authorized/i)
281
+ expect(client.rmAccount).not.toHaveBeenCalled()
282
+ })
283
+
284
+ it('refuses /auth refresh for non-admin', async () => {
285
+ const client = mockClient()
286
+ const reply = await handleAuthCommand(
287
+ { kind: 'refresh' },
288
+ { ...nonAdmin, client },
289
+ )
290
+ expect(reply.text).toMatch(/Not authorized/i)
291
+ expect(client.refreshAccount).not.toHaveBeenCalled()
292
+ })
293
+
294
+ it('refuses /auth agent override <set> for non-admin', async () => {
295
+ const client = mockClient()
296
+ const reply = await handleAuthCommand(
297
+ { kind: 'override-set', agent: 'clerk', label: 'spare' },
298
+ { ...nonAdmin, client },
299
+ )
300
+ expect(reply.text).toMatch(/Not authorized/i)
301
+ expect(client.setOverride).not.toHaveBeenCalled()
302
+ })
303
+
304
+ it('refuses /auth agent override <clear> for non-admin', async () => {
305
+ const client = mockClient()
306
+ const reply = await handleAuthCommand(
307
+ { kind: 'override-clear', agent: 'clerk' },
308
+ { ...nonAdmin, client },
309
+ )
310
+ expect(reply.text).toMatch(/Not authorized/i)
311
+ expect(client.setOverride).not.toHaveBeenCalled()
312
+ })
313
+ })
314
+
315
+ /* ── 4. rm two-step confirm flow ──────────────────────────────────────── */
316
+
317
+ describe('handleAuthCommand — /auth rm two-step confirm', () => {
318
+ const admin = { agentName: 'clerk', isAdmin: true }
319
+
320
+ it('prompt phase succeeds for a valid non-active label and stashes a pending entry', async () => {
321
+ const client = mockClient()
322
+ const reply = await handleAuthCommand(
323
+ { kind: 'rm-prompt', label: 'spare' },
324
+ { ...admin, client, chatId: '999' },
325
+ )
326
+ expect(reply.text).toMatch(/about to remove/i)
327
+ expect(reply.text).toMatch(/spare/)
328
+ expect(reply.text).toMatch(/confirm/i)
329
+ expect(pendingAuthRmFlows.get('999')?.label).toBe('spare')
330
+ })
331
+
332
+ it('refuses to prompt when the label is unknown', async () => {
333
+ const client = mockClient()
334
+ const reply = await handleAuthCommand(
335
+ { kind: 'rm-prompt', label: 'doesnotexist' },
336
+ { ...admin, client, chatId: '999' },
337
+ )
338
+ expect(reply.text).toMatch(/no account named/i)
339
+ expect(client.rmAccount).not.toHaveBeenCalled()
340
+ expect(pendingAuthRmFlows.size).toBe(0)
341
+ })
342
+
343
+ it('refuses to prompt when the label is the fleet active', async () => {
344
+ const client = mockClient()
345
+ const reply = await handleAuthCommand(
346
+ { kind: 'rm-prompt', label: 'primary' },
347
+ { ...admin, client, chatId: '999' },
348
+ )
349
+ expect(reply.text).toMatch(/fleet active/i)
350
+ expect(reply.text).toMatch(/use/)
351
+ expect(client.rmAccount).not.toHaveBeenCalled()
352
+ expect(pendingAuthRmFlows.size).toBe(0)
353
+ })
354
+
355
+ it('confirm phase only fires when a matching pending entry exists', async () => {
356
+ const client = mockClient()
357
+ // Phase 1
358
+ await handleAuthCommand(
359
+ { kind: 'rm-prompt', label: 'spare' },
360
+ { ...admin, client, chatId: 'C' },
361
+ )
362
+ // Phase 2
363
+ const reply = await handleAuthCommand(
364
+ { kind: 'rm-confirmed', label: 'spare' },
365
+ { ...admin, client, chatId: 'C' },
366
+ )
367
+ expect(reply.text).toMatch(/Removed/i)
368
+ expect(client.rmAccount).toHaveBeenCalledTimes(1)
369
+ expect(client.rmAccount).toHaveBeenCalledWith('spare')
370
+ expect(pendingAuthRmFlows.has('C')).toBe(false)
371
+ })
372
+
373
+ it('confirm refuses when no prompt was issued', async () => {
374
+ const client = mockClient()
375
+ const reply = await handleAuthCommand(
376
+ { kind: 'rm-confirmed', label: 'spare' },
377
+ { ...admin, client, chatId: 'C' },
378
+ )
379
+ expect(reply.text).toMatch(/no pending confirm/i)
380
+ expect(client.rmAccount).not.toHaveBeenCalled()
381
+ })
382
+
383
+ it('confirm refuses when the pending label does not match', async () => {
384
+ const client = mockClient()
385
+ pendingAuthRmFlows.set('C', {
386
+ label: 'other-label',
387
+ expiresAt: Date.now() + AUTH_RM_CONFIRM_TTL_MS,
388
+ })
389
+ const reply = await handleAuthCommand(
390
+ { kind: 'rm-confirmed', label: 'spare' },
391
+ { ...admin, client, chatId: 'C' },
392
+ )
393
+ expect(reply.text).toMatch(/no pending confirm/i)
394
+ expect(client.rmAccount).not.toHaveBeenCalled()
395
+ })
396
+
397
+ it('confirm refuses when the pending entry has expired', async () => {
398
+ const client = mockClient()
399
+ pendingAuthRmFlows.set('C', {
400
+ label: 'spare',
401
+ expiresAt: Date.now() - 1, // expired
402
+ })
403
+ const reply = await handleAuthCommand(
404
+ { kind: 'rm-confirmed', label: 'spare' },
405
+ { ...admin, client, chatId: 'C' },
406
+ )
407
+ expect(reply.text).toMatch(/expired|no pending confirm/i)
408
+ expect(client.rmAccount).not.toHaveBeenCalled()
409
+ // Stale entry should be reaped.
410
+ expect(pendingAuthRmFlows.has('C')).toBe(false)
411
+ })
412
+
413
+ it('TTL is the documented 60 seconds', () => {
414
+ expect(AUTH_RM_CONFIRM_TTL_MS).toBe(60_000)
415
+ })
416
+ })
417
+
418
+ /* ── 5. refresh ───────────────────────────────────────────────────────── */
419
+
420
+ describe('handleAuthCommand — /auth refresh', () => {
421
+ const admin = { agentName: 'clerk', isAdmin: true }
422
+
423
+ it('without a label refreshes every account, once each', async () => {
424
+ const client = mockClient()
425
+ const reply = await handleAuthCommand(
426
+ { kind: 'refresh' },
427
+ { ...admin, client },
428
+ )
429
+ expect(reply.text).toMatch(/Refreshed/)
430
+ expect(client.refreshAccount).toHaveBeenCalledTimes(2)
431
+ expect(client.refreshAccount).toHaveBeenCalledWith('primary')
432
+ expect(client.refreshAccount).toHaveBeenCalledWith('spare')
433
+ })
434
+
435
+ it('with a label refreshes that account once', async () => {
436
+ const client = mockClient()
437
+ const reply = await handleAuthCommand(
438
+ { kind: 'refresh', label: 'spare' },
439
+ { ...admin, client },
440
+ )
441
+ expect(reply.text).toMatch(/Refreshed/)
442
+ expect(reply.text).toMatch(/spare/)
443
+ expect(client.refreshAccount).toHaveBeenCalledTimes(1)
444
+ expect(client.refreshAccount).toHaveBeenCalledWith('spare')
445
+ })
446
+
447
+ it('with an unknown label returns a friendly error and does not call the broker', async () => {
448
+ const client = mockClient()
449
+ const reply = await handleAuthCommand(
450
+ { kind: 'refresh', label: 'ghost' },
451
+ { ...admin, client },
452
+ )
453
+ expect(reply.text).toMatch(/no account named/i)
454
+ expect(client.refreshAccount).not.toHaveBeenCalled()
455
+ })
456
+
457
+ it('reports per-account failures without aborting the whole sweep', async () => {
458
+ const client = mockClient()
459
+ client.refreshAccount.mockImplementation(async (label: string) => {
460
+ if (label === 'primary') throw new Error('rate-limited')
461
+ return { account: label, expiresAt: Date.now() + 1000 }
462
+ })
463
+ const reply = await handleAuthCommand(
464
+ { kind: 'refresh' },
465
+ { ...admin, client },
466
+ )
467
+ expect(client.refreshAccount).toHaveBeenCalledTimes(2)
468
+ expect(reply.text).toMatch(/Failures/i)
469
+ expect(reply.text).toMatch(/rate-limited/)
470
+ })
471
+ })
472
+
473
+ /* ── 6. override set + clear ──────────────────────────────────────────── */
474
+
475
+ describe('handleAuthCommand — /auth agent override', () => {
476
+ const admin = { agentName: 'clerk', isAdmin: true }
477
+
478
+ it('set calls setOverride(agent, label)', async () => {
479
+ const client = mockClient()
480
+ const reply = await handleAuthCommand(
481
+ { kind: 'override-set', agent: 'researcher', label: 'primary' },
482
+ { ...admin, client },
483
+ )
484
+ expect(client.setOverride).toHaveBeenCalledTimes(1)
485
+ expect(client.setOverride).toHaveBeenCalledWith('researcher', 'primary')
486
+ expect(reply.text).toMatch(/Override set/i)
487
+ expect(reply.text).toMatch(/researcher/)
488
+ expect(reply.text).toMatch(/primary/)
489
+ })
490
+
491
+ it('clear calls setOverride(agent, null) — chat "clear" → null arg', async () => {
492
+ const client = mockClient()
493
+ const reply = await handleAuthCommand(
494
+ { kind: 'override-clear', agent: 'researcher' },
495
+ { ...admin, client },
496
+ )
497
+ expect(client.setOverride).toHaveBeenCalledTimes(1)
498
+ expect(client.setOverride).toHaveBeenCalledWith('researcher', null)
499
+ expect(reply.text).toMatch(/Override cleared/i)
500
+ expect(reply.text).toMatch(/researcher/)
501
+ })
502
+ })
503
+
504
+ /* ── 7. help text contents ────────────────────────────────────────────── */
505
+
506
+ describe('handleAuthCommand — help text lists every verb', () => {
507
+ it('help reply mentions all the load-bearing verbs', async () => {
508
+ const client = mockClient()
509
+ const reply = await handleAuthCommand(
510
+ { kind: 'help' },
511
+ { agentName: 'x', isAdmin: true, client },
512
+ )
513
+ const text = reply.text
514
+ // Verbs (all variants). The help is HTML; <code> wraps each verb.
515
+ for (const fragment of [
516
+ '/auth show',
517
+ '/auth show &lt;agent&gt;',
518
+ '/auth list',
519
+ '/auth use',
520
+ '/auth rotate',
521
+ '/auth add',
522
+ '/auth cancel',
523
+ '/auth rm',
524
+ '/auth refresh',
525
+ '/auth agent override',
526
+ '/auth help',
527
+ ]) {
528
+ expect(text).toContain(fragment)
529
+ }
530
+ })
531
+ })