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
@@ -36,15 +36,6 @@ export interface FirstPaintBotApi {
36
36
  ): Promise<unknown>
37
37
  }
38
38
 
39
- export interface FirstPaintProgressDriver {
40
- startTurn(args: {
41
- chatId: string
42
- threadId?: string
43
- userText: string
44
- replyToMessageId?: number
45
- }): void
46
- }
47
-
48
39
  export interface FirstPaintAccess {
49
40
  /** When false, all status-reaction posting is suppressed. */
50
41
  statusReactions?: boolean
@@ -65,7 +56,6 @@ export interface FirstPaintCtx {
65
56
 
66
57
  export interface FirstPaintDeps {
67
58
  bot: { api: FirstPaintBotApi }
68
- progressDriver: FirstPaintProgressDriver | undefined
69
59
  activeStatusReactions: Map<string, StatusReactionController>
70
60
  activeReactionMsgIds: Map<string, { chatId: string; messageId: number }>
71
61
  activeTurnStartedAt: Map<string, number>
@@ -227,20 +217,9 @@ export async function firstPaintTurn(
227
217
  }
228
218
  }
229
219
 
230
- // Start a new progress card only for fresh turns (no prior turn in flight).
231
- if (!isSteering && priorTurnStartedAt == null) {
232
- try {
233
- deps.progressDriver?.startTurn({
234
- chatId,
235
- threadId: messageThreadId != null ? String(messageThreadId) : undefined,
236
- userText: effectiveText,
237
- replyToMessageId: msgId != null ? msgId : undefined,
238
- })
239
- } catch (err) {
240
- const log = deps.logError ?? ((m: string) => process.stderr.write(m))
241
- log(`telegram gateway: progress-card startTurn failed: ${(err as Error).message}\n`)
242
- }
243
- }
220
+ // #1122 PR3: progress-card startTurn removed with the card.
221
+ // The status reaction (👀 above) is now the sole first-paint signal;
222
+ // conversational pacing + silence-poke own the rest of the turn.
244
223
 
245
224
  return { isSteering, priorTurnStartedAt }
246
225
  }
@@ -0,0 +1,326 @@
1
+ /**
2
+ * `/auth add <label>` Telegram chat flow (RFC H §4.3 add-account, §7.3).
3
+ *
4
+ * The headline use case: every account on the fleet is rate-limited,
5
+ * the LLM is unreachable, and the operator is on their phone. They
6
+ * need a deterministic — LLM-free — chat path to add a fresh Anthropic
7
+ * OAuth account. This module owns that flow end-to-end:
8
+ *
9
+ * 1. Operator sends `/auth add <label>`.
10
+ * 2. Gateway calls {@link startAccountAuthSession} → spawns
11
+ * `claude setup-token` against a scratch directory under
12
+ * `~/.switchroom/accounts/.in-progress/<label>-<rand>/`, captures
13
+ * the OAuth authorize URL, and tucks pending state into
14
+ * {@link pendingAuthAddFlows}.
15
+ * 3. Gateway replies to chat with the URL + paste instructions.
16
+ * 4. Operator opens URL, logs in, copies the browser code, pastes
17
+ * into chat. Gateway's `pendingReauthFlows`-style intercept
18
+ * catches the paste and calls {@link submitAccountAuthCode}.
19
+ * 5. Helper reads `<scratch>/.credentials.json` (the dotfile that
20
+ * `claude setup-token` writes on success — pinned in
21
+ * `src/auth/broker/server-add-account.test.ts`), builds the
22
+ * {@link AddAccountCredentials} payload, and the gateway calls
23
+ * broker `addAccount(label, credentials, replace=false)`.
24
+ * 6. Scratch dir is wiped on every code path — success, cancel,
25
+ * paste-failure, TTL timeout, gateway shutdown.
26
+ *
27
+ * Why a separate module (vs reusing `src/auth/manager.ts`):
28
+ *
29
+ * - `startAuthSession` writes `<agentDir>/.claude/.setup-token.session.json`
30
+ * and is built around the per-agent OAuth flow. The `/auth add`
31
+ * flow has no agent — the resulting credentials become a
32
+ * broker-managed account that any agent can be set to. Threading
33
+ * `agentDir` through it would corrupt the agent's own auth state
34
+ * if the operator's add-flow collides with a normal reauth.
35
+ * - The chat-flow surface is deterministic and stateless beyond
36
+ * `pendingAuthAddFlows`. Reusing the full manager would inherit
37
+ * legacy slot logic, tmp-dir cleanup heuristics, and stale-session
38
+ * detection that doesn't apply when each `/auth add` creates a
39
+ * fresh, unguessable scratch dir of its own.
40
+ *
41
+ * What we DO reuse: the pure parsing helpers — `parseSetupTokenUrl`
42
+ * (handles both claude.ai/oauth and claude.com/cai/oauth shapes),
43
+ * `extractCodeChallenge` (PKCE stale-session detection), and
44
+ * `readTokenFromCredentialsFile` (validates the `sk-ant-oat...` token
45
+ * shape). Those are label-agnostic.
46
+ *
47
+ * **Hard rule: NEVER touch the agent's claude process.** This flow runs
48
+ * as a deterministic chat handler in the gateway. The URL goes straight
49
+ * to chat via `bot.api.sendMessage`. The code paste is intercepted by
50
+ * the gateway, never forwarded to the agent's bridge. If every account
51
+ * on the fleet is rate-limited the LLM is unreachable — that's the
52
+ * whole point of the flow existing.
53
+ */
54
+
55
+ import { spawn, type ChildProcess } from 'node:child_process'
56
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
57
+ import { homedir } from 'node:os'
58
+ import { join } from 'node:path'
59
+ import { randomBytes } from 'node:crypto'
60
+
61
+ import {
62
+ parseSetupTokenUrl,
63
+ readTokenFromCredentialsFile,
64
+ } from '../../src/auth/manager.js'
65
+ import type {
66
+ AddAccountCredentials,
67
+ AnthropicAddAccountCredentials,
68
+ } from '../../src/auth/broker/client.js'
69
+
70
+ /* ── Pending-state map ────────────────────────────────────────────────── */
71
+
72
+ /**
73
+ * In-flight `/auth add` flow keyed by Telegram chat id. The gateway's
74
+ * generic message intercept (sibling to `pendingReauthFlows`) reads
75
+ * this map to decide whether a sk-ant-…-shaped paste belongs to an
76
+ * add flow or to a reauth flow.
77
+ *
78
+ * TTL matches `REAUTH_INTERCEPT_TTL_MS` (10 minutes); the reaper sweep
79
+ * in gateway.ts walks both maps each minute.
80
+ */
81
+ export interface PendingAuthAddFlow {
82
+ label: string
83
+ scratchDir: string
84
+ /** PID of the spawned `claude setup-token` process, for cancel-kill. */
85
+ child: ChildProcess
86
+ startedAt: number
87
+ }
88
+ export const pendingAuthAddFlows = new Map<string, PendingAuthAddFlow>()
89
+
90
+ /* ── Scratch dir lifecycle ────────────────────────────────────────────── */
91
+
92
+ /**
93
+ * Pick a fresh scratch path under
94
+ * `~/.switchroom/accounts/.in-progress/<label>-<rand>/`.
95
+ *
96
+ * The leading dot keeps the dir hidden from `listAccounts(home)` in
97
+ * `src/auth/account-store.ts`, which enumerates accounts by scanning
98
+ * `~/.switchroom/accounts/`. That listing is the source of truth for
99
+ * broker `list-state` — a half-written add-in-progress must NOT
100
+ * appear there. `.in-progress/` is also outside the broker's
101
+ * managed-artifact whitelist, so a stray dir won't blow up on the
102
+ * next apply.
103
+ *
104
+ * Random suffix is 8 bytes of crypto-grade randomness so:
105
+ * - two concurrent operators adding the same label can't collide
106
+ * on the scratch path
107
+ * - an attacker watching `~/.switchroom/accounts/.in-progress/`
108
+ * can't predict the next dir name and squat a symlink
109
+ */
110
+ export function pickScratchDir(label: string, home: string = homedir()): string {
111
+ const suffix = randomBytes(8).toString('hex')
112
+ return join(home, '.switchroom', 'accounts', '.in-progress', `${label}-${suffix}`)
113
+ }
114
+
115
+ /**
116
+ * Best-effort scratch-dir wipe. Used on every exit path — success,
117
+ * cancel, timeout, error. Synchronous because the caller has already
118
+ * settled the user-facing reply by the time we get here; an extra
119
+ * tick of latency is not worth event-loop juggling.
120
+ */
121
+ export function cleanScratchDir(scratchDir: string): void {
122
+ try {
123
+ rmSync(scratchDir, { recursive: true, force: true })
124
+ } catch {
125
+ // best-effort
126
+ }
127
+ }
128
+
129
+ /* ── Subprocess lifecycle ─────────────────────────────────────────────── */
130
+
131
+ export interface StartAccountAuthSessionResult {
132
+ loginUrl: string
133
+ scratchDir: string
134
+ child: ChildProcess
135
+ }
136
+
137
+ /**
138
+ * Spawn `claude setup-token` against a fresh scratch directory and
139
+ * resolve once the authorize URL has been parsed from its stdout/stderr.
140
+ *
141
+ * Why we *don't* use tmux: the `submitAuthCode` path in
142
+ * `src/auth/manager.ts` uses tmux because that flow is interactive —
143
+ * an operator on a host can `tmux attach` to inspect the auth prompt
144
+ * if anything goes wrong. The chat flow has no equivalent escape
145
+ * hatch (the operator is on their phone) and a pipe-based subprocess
146
+ * is far easier to lifecycle-manage from a long-running gateway. We
147
+ * write the code to the child's stdin in {@link submitAccountAuthCode}.
148
+ *
149
+ * The child is left running between {@link startAccountAuthSession}
150
+ * and {@link submitAccountAuthCode} — closing stdin before the code
151
+ * is pasted would tear down the OAuth session.
152
+ *
153
+ * Timeout default: 12 seconds to see the URL. claude setup-token
154
+ * typically prints the URL within ~3–5s; 12s covers an unloaded VM
155
+ * with slow startup. Caller passes the timeout via opts so tests can
156
+ * shorten it.
157
+ */
158
+ export async function startAccountAuthSession(
159
+ label: string,
160
+ opts: {
161
+ home?: string
162
+ urlTimeoutMs?: number
163
+ /** Override the binary name (tests). */
164
+ claudeBinary?: string
165
+ } = {},
166
+ ): Promise<StartAccountAuthSessionResult> {
167
+ const home = opts.home ?? homedir()
168
+ const urlTimeoutMs = opts.urlTimeoutMs ?? 12_000
169
+ const binary = opts.claudeBinary ?? 'claude'
170
+
171
+ const scratchDir = pickScratchDir(label, home)
172
+ mkdirSync(scratchDir, { recursive: true, mode: 0o700 })
173
+
174
+ // BROWSER=/bin/true: same rationale as src/auth/manager.ts's
175
+ // startAuthSession — suppress claude setup-token's host-side browser
176
+ // auto-launch (would land on Claude's login page with no cookies on
177
+ // a headless box). The chat flow is paste-only.
178
+ const child = spawn(binary, ['setup-token'], {
179
+ env: {
180
+ ...process.env,
181
+ CLAUDE_CONFIG_DIR: scratchDir,
182
+ BROWSER: '/bin/true',
183
+ },
184
+ stdio: ['pipe', 'pipe', 'pipe'],
185
+ })
186
+
187
+ // Aggregate stdout+stderr; the URL can land on either channel
188
+ // depending on claude CLI version.
189
+ let buffer = ''
190
+ const collect = (chunk: Buffer): void => {
191
+ buffer += chunk.toString('utf8')
192
+ }
193
+ child.stdout?.on('data', collect)
194
+ child.stderr?.on('data', collect)
195
+
196
+ // Race: URL detection vs timeout vs child exit before URL appeared.
197
+ const loginUrl = await new Promise<string>((resolve, reject) => {
198
+ const deadline = setTimeout(() => {
199
+ cleanup()
200
+ reject(new Error(`claude setup-token did not print an OAuth URL within ${urlTimeoutMs}ms`))
201
+ }, urlTimeoutMs)
202
+
203
+ const tick = setInterval(() => {
204
+ const url = parseSetupTokenUrl(buffer)
205
+ if (url) {
206
+ cleanup()
207
+ resolve(url)
208
+ }
209
+ }, 200)
210
+
211
+ const onExit = (code: number | null): void => {
212
+ cleanup()
213
+ reject(new Error(`claude setup-token exited (code ${code}) before printing OAuth URL`))
214
+ }
215
+ child.once('exit', onExit)
216
+
217
+ function cleanup(): void {
218
+ clearTimeout(deadline)
219
+ clearInterval(tick)
220
+ child.removeListener('exit', onExit)
221
+ }
222
+ }).catch((err) => {
223
+ // Kill the child and wipe the scratch dir before re-raising so
224
+ // failed-to-start sessions don't leak.
225
+ try { child.kill('SIGTERM') } catch { /* best-effort */ }
226
+ cleanScratchDir(scratchDir)
227
+ throw err
228
+ })
229
+
230
+ return { loginUrl, scratchDir, child }
231
+ }
232
+
233
+ /**
234
+ * Paste the operator's browser code into the live `claude setup-token`
235
+ * child's stdin and wait for the success-written credentials.json.
236
+ *
237
+ * Returns the `AddAccountCredentials` shape the broker's add-account
238
+ * verb expects — same `claudeAiOauth: { accessToken, refreshToken,
239
+ * expiresAt, scopes, subscriptionType, rateLimitTier }` envelope.
240
+ *
241
+ * On success: the caller is responsible for invoking
242
+ * `cleanScratchDir(scratchDir)` after `addAccount` returns; we
243
+ * deliberately don't wipe here because the broker call might race the
244
+ * filesystem cleanup. On failure (invalid code, expired code, timeout)
245
+ * the helper throws and cleans the scratch dir itself.
246
+ *
247
+ * Poll interval default: 250ms — same as `submitAuthCode`'s 500ms
248
+ * halved because there's no tmux capture-pane overhead per tick.
249
+ * Timeout default: 120s, matching the env var in `submitAuthCode`.
250
+ */
251
+ export async function submitAccountAuthCode(
252
+ flow: PendingAuthAddFlow,
253
+ code: string,
254
+ opts: { pollIntervalMs?: number; pollTimeoutMs?: number } = {},
255
+ ): Promise<AddAccountCredentials> {
256
+ const pollIntervalMs = opts.pollIntervalMs ?? 250
257
+ const pollTimeoutMs = opts.pollTimeoutMs ?? 120_000
258
+
259
+ const credentialsPath = join(flow.scratchDir, '.credentials.json')
260
+
261
+ // Write the code + newline to stdin. claude setup-token's prompt
262
+ // expects line-buffered input — see the manual-paste paste at the
263
+ // bottom of `submitAuthCode`. We use a single write here (vs the
264
+ // two send-keys calls of the tmux path) because there's no
265
+ // terminfo-flake concern over a pipe.
266
+ if (!flow.child.stdin || flow.child.stdin.destroyed) {
267
+ cleanScratchDir(flow.scratchDir)
268
+ throw new Error('claude setup-token process stdin is not writable (child may have exited)')
269
+ }
270
+ flow.child.stdin.write(code.trim() + '\n')
271
+
272
+ // Poll for the credentials file. Same two-channel design as
273
+ // submitAuthCode but tmux-pane-scrape and log-scrape are out (the
274
+ // pane scrape was a fallback for older claude CLI versions; the
275
+ // chat flow targets the current CLI by definition).
276
+ const deadline = Date.now() + pollTimeoutMs
277
+ while (Date.now() < deadline) {
278
+ await new Promise((r) => setTimeout(r, pollIntervalMs))
279
+ if (existsSync(credentialsPath)) {
280
+ const token = readTokenFromCredentialsFile(credentialsPath)
281
+ if (token) {
282
+ // Parse the full credentials envelope to forward to the
283
+ // broker. readTokenFromCredentialsFile already validated the
284
+ // accessToken regex, so the JSON is well-formed.
285
+ try {
286
+ const raw = readFileSync(credentialsPath, 'utf-8')
287
+ const parsed = JSON.parse(raw) as { claudeAiOauth?: AnthropicAddAccountCredentials['claudeAiOauth'] }
288
+ if (parsed.claudeAiOauth?.accessToken) {
289
+ // Drain the child so it exits cleanly after success.
290
+ try { flow.child.stdin?.end() } catch { /* best-effort */ }
291
+ return { claudeAiOauth: parsed.claudeAiOauth }
292
+ }
293
+ } catch {
294
+ // fall through — file may be mid-write; next tick retries.
295
+ }
296
+ }
297
+ }
298
+ // Detect child early exit (invalid code → claude prints + exits).
299
+ if (flow.child.exitCode != null) {
300
+ cleanScratchDir(flow.scratchDir)
301
+ throw new Error(
302
+ `claude setup-token exited (code ${flow.child.exitCode}) — code may have been invalid or expired`,
303
+ )
304
+ }
305
+ }
306
+
307
+ // Timeout — kill the child + wipe scratch.
308
+ try { flow.child.kill('SIGTERM') } catch { /* best-effort */ }
309
+ cleanScratchDir(flow.scratchDir)
310
+ throw new Error(`No credentials file appeared at ${credentialsPath} within ${Math.round(pollTimeoutMs / 1000)}s`)
311
+ }
312
+
313
+ /**
314
+ * Cancel an in-flight `/auth add` flow: kill the `claude setup-token`
315
+ * child, wipe the scratch dir, and let the caller delete the
316
+ * `pendingAuthAddFlows` entry. Idempotent — safe to call when the
317
+ * child has already exited.
318
+ */
319
+ export function cancelAccountAuthSession(flow: PendingAuthAddFlow): void {
320
+ try {
321
+ if (flow.child.exitCode == null) flow.child.kill('SIGTERM')
322
+ } catch {
323
+ // best-effort
324
+ }
325
+ cleanScratchDir(flow.scratchDir)
326
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Thin adapter between the gateway and `src/auth/broker/client.ts`.
3
+ *
4
+ * The broker client is a stateful class (holds a persistent UDS
5
+ * connection). The gateway constructs one per `/auth` command —
6
+ * cheap, and avoids dangling sockets on idle. The handler needs the
7
+ * five methods on the `AuthBrokerClient` interface in
8
+ * `./auth-command.ts` (listState / setActive / rmAccount /
9
+ * refreshAccount / setOverride); we narrow `BrokerClient` down to
10
+ * that surface so a test mock only has to stub those five.
11
+ */
12
+
13
+ import { AuthBrokerClient as BrokerClient, type AddAccountCredentials } from '../../src/auth/broker/client.js'
14
+ import type { AuthBrokerClient } from './auth-command.js'
15
+
16
+ /**
17
+ * Construct an {@link AuthBrokerClient} for one `/auth` command. The
18
+ * caller is responsible for closing the underlying socket when done
19
+ * (do `await client.close()` after the reply lands).
20
+ */
21
+ export function createAuthBrokerClient(): {
22
+ client: AuthBrokerClient
23
+ close: () => Promise<void>
24
+ } {
25
+ const broker = new BrokerClient()
26
+ const client: AuthBrokerClient = {
27
+ listState: () => broker.listState(),
28
+ setActive: (label: string) => broker.setActive(label),
29
+ rmAccount: (label: string) => broker.rmAccount(label),
30
+ refreshAccount: (label: string) => broker.refreshAccount(label),
31
+ setOverride: (agent: string, account: string | null) =>
32
+ broker.setOverride(agent, account),
33
+ }
34
+ return { client, close: () => broker.close() }
35
+ }
36
+
37
+ /**
38
+ * Legacy `getAuthBrokerClient` entry — kept so the gateway's existing
39
+ * call site doesn't need rewiring. Returns the client object only;
40
+ * the underlying socket leaks unless the caller imports
41
+ * `createAuthBrokerClient` directly. Acceptable because:
42
+ * - The gateway is long-lived (one process per agent).
43
+ * - The broker tolerates many connections per peer.
44
+ * - `/auth` is a low-frequency human-driven verb.
45
+ *
46
+ * If allocations become a concern, swap callers over to the structured
47
+ * variant above.
48
+ */
49
+ export async function getAuthBrokerClient(
50
+ _agentName: string,
51
+ ): Promise<AuthBrokerClient | null> {
52
+ const { client } = createAuthBrokerClient()
53
+ return client
54
+ }
55
+
56
+ /**
57
+ * Add an account via the broker. Used exclusively by the `/auth add`
58
+ * chat flow — the narrow {@link AuthBrokerClient} surface in
59
+ * `auth-command.ts` deliberately omits `addAccount` because the verb
60
+ * is gateway-routed (not handler-routed). Constructs and closes a
61
+ * one-shot {@link BrokerClient} so the gateway doesn't need a
62
+ * long-lived handle just for this verb.
63
+ */
64
+ export async function addAccountViaBroker(
65
+ label: string,
66
+ credentials: AddAccountCredentials,
67
+ opts: { replace?: boolean } = {},
68
+ ): Promise<{ label: string; expiresAt?: number }> {
69
+ const broker = new BrokerClient()
70
+ try {
71
+ return await broker.addAccount(label, credentials, opts.replace)
72
+ } finally {
73
+ await broker.close()
74
+ }
75
+ }