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,345 +0,0 @@
1
- /**
2
- * Pure /setup wizard state machine.
3
- *
4
- * Walks the user through creating a new agent entirely from Telegram:
5
- * asked-slug → ask for the agent slug (e.g. "gymbro")
6
- * asked-persona → ask for the persona display name (e.g. "Gym Bro")
7
- * asked-model → ask which Claude model to use (or skip for default)
8
- * asked-emoji → ask for a topic emoji (or skip)
9
- * asked-bot-token → user creates a bot via BotFather and pastes the token
10
- * confirming-allowlist → confirm the calling user_id is the allowed user
11
- * reconciling → foreman provisions + starts the agent (orchestrator step)
12
- * done
13
- *
14
- * This module is pure: no grammY, no SQLite, no network calls.
15
- * foreman.ts interprets the returned actions and executes side-effects.
16
- *
17
- * Deferral notes in foreman.ts:
18
- * // TODO(#<issue>): BotFather auto-flow — currently user creates bot manually
19
- * // TODO(#<issue>): OAuth code paste step — currently manual terminal instruction
20
- * // TODO(#<issue>): Skills selector — currently shows placeholder message
21
- */
22
-
23
- import type { SetupFlowState, SetupFlowStep } from './setup-state.js'
24
-
25
- // ─── Action types ────────────────────────────────────────────────────────
26
-
27
- export type SetupFlowAction =
28
- | { kind: 'ask-slug' }
29
- | { kind: 'ask-persona'; slug: string }
30
- | { kind: 'ask-model'; slug: string; persona: string }
31
- | { kind: 'ask-emoji'; slug: string; persona: string; model: string | null }
32
- | { kind: 'ask-profile'; slug: string; persona: string; model: string | null; emoji: string | null; profiles: string[] }
33
- | { kind: 'ask-bot-token'; slug: string; persona: string; model: string | null; emoji: string | null; profile: string }
34
- | { kind: 'confirm-allowlist'; slug: string; callerId: string }
35
- // Pre-fix this was call-reconcile (single-step that did createAgent inline +
36
- // told the user to run `switchroom auth code` from a terminal). Split per
37
- // #189/#190 into call-create-agent (returns loginUrl) → asked-oauth-code
38
- // (collects the code via Telegram) → call-complete-creation (runs the
39
- // submit + starts the agent).
40
- | { kind: 'call-create-agent'; slug: string; persona: string; model: string | null; emoji: string | null; profile: string; botToken: string; allowedUserId: string }
41
- | { kind: 'ask-oauth-code'; slug: string; loginUrl: string | null }
42
- | { kind: 'call-complete-creation'; slug: string; persona: string; code: string }
43
- | { kind: 'done'; slug: string; botUsername: string | null }
44
- | { kind: 'error'; message: string; stayInStep: boolean }
45
- | { kind: 'cancel'; reason: string }
46
-
47
- // ─── Validation helpers ───────────────────────────────────────────────────
48
-
49
- /** Agent slug: same rules as assertSafeAgentName */
50
- export function isValidSlug(slug: string): boolean {
51
- return /^[a-z0-9][a-z0-9_-]{0,50}$/.test(slug)
52
- }
53
-
54
- /** Persona name: 1-80 printable chars, no control characters */
55
- export function isValidPersonaName(name: string): boolean {
56
- return name.length >= 1 && name.length <= 80 && !/[\x00-\x1f\x7f]/.test(name)
57
- }
58
-
59
- /** Known short model aliases and full IDs we accept */
60
- const KNOWN_MODEL_ALIASES = new Set(['sonnet', 'opus', 'haiku', 'inherit'])
61
-
62
- /** Model: alphanumeric with . _ - / [ ] : only, or short alias */
63
- export function isValidModel(model: string): boolean {
64
- return KNOWN_MODEL_ALIASES.has(model.toLowerCase()) ||
65
- /^[a-zA-Z0-9][a-zA-Z0-9._\-/[\]:]*$/.test(model)
66
- }
67
-
68
- /** Emoji: one or two Unicode grapheme clusters (rough check) */
69
- export function isValidEmoji(emoji: string): boolean {
70
- const trimmed = emoji.trim()
71
- return trimmed.length >= 1 && trimmed.length <= 16
72
- }
73
-
74
- /** Skip keywords */
75
- export function isSkip(text: string): boolean {
76
- const t = text.trim().toLowerCase()
77
- return t === 'skip' || t === 's' || t === '-'
78
- }
79
-
80
- /** Cancel keywords */
81
- export function isCancel(text: string): boolean {
82
- const t = text.trim().toLowerCase()
83
- return t === '/cancel' || t === 'cancel' || t === 'abort'
84
- }
85
-
86
- // ─── Flow entry point ────────────────────────────────────────────────────
87
-
88
- /**
89
- * Start a new /setup wizard. Optionally pre-fills slug from inline arg.
90
- */
91
- export function startSetupFlow(
92
- inlineSlug: string | null,
93
- ): SetupFlowAction {
94
- if (!inlineSlug) {
95
- return { kind: 'ask-slug' }
96
- }
97
-
98
- if (!isValidSlug(inlineSlug)) {
99
- return {
100
- kind: 'error',
101
- message: `"${inlineSlug}" is not a valid agent slug. Use lowercase letters, numbers, hyphens or underscores (max 51 chars).`,
102
- stayInStep: false,
103
- }
104
- }
105
-
106
- return { kind: 'ask-persona', slug: inlineSlug }
107
- }
108
-
109
- // ─── Step transition ──────────────────────────────────────────────────────
110
-
111
- export interface SetupStepInput {
112
- state: SetupFlowState | null
113
- text: string
114
- /** The Telegram user_id of the foreman caller (used for allowlist confirmation). */
115
- callerId: string
116
- /** Available profile names — passed through to the asked-profile step.
117
- * Foreman calls listAvailableProfiles() and forwards the list. */
118
- profiles: string[]
119
- }
120
-
121
- /**
122
- * Given the current state and user text, compute the next action.
123
- * Returns 'cancel' with reason='user-cancelled' when the user types /cancel.
124
- */
125
- export function handleSetupText(input: SetupStepInput): SetupFlowAction {
126
- const { state, text, callerId, profiles } = input
127
- const trimmed = text.trim()
128
-
129
- if (!state) {
130
- return { kind: 'cancel', reason: 'no-active-flow' }
131
- }
132
-
133
- // Global cancel at any step
134
- if (isCancel(trimmed)) {
135
- return { kind: 'cancel', reason: 'user-cancelled' }
136
- }
137
-
138
- switch (state.step) {
139
- case 'asked-slug': {
140
- if (!isValidSlug(trimmed)) {
141
- return {
142
- kind: 'error',
143
- message: `"${trimmed}" is not a valid agent slug. Use lowercase letters, numbers, hyphens or underscores (max 51 chars). Try again:`,
144
- stayInStep: true,
145
- }
146
- }
147
- return { kind: 'ask-persona', slug: trimmed }
148
- }
149
-
150
- case 'asked-persona': {
151
- const slug = state.slug ?? trimmed
152
- if (!isValidPersonaName(trimmed)) {
153
- return {
154
- kind: 'error',
155
- message: 'Persona name must be 1-80 printable characters. Try again:',
156
- stayInStep: true,
157
- }
158
- }
159
- return { kind: 'ask-model', slug, persona: trimmed }
160
- }
161
-
162
- case 'asked-model': {
163
- const slug = state.slug ?? ''
164
- const persona = state.persona ?? ''
165
- if (isSkip(trimmed)) {
166
- // Use default model
167
- return { kind: 'ask-emoji', slug, persona, model: null }
168
- }
169
- if (!isValidModel(trimmed)) {
170
- return {
171
- kind: 'error',
172
- message: `Unknown model "${trimmed}". Use <code>sonnet</code>, <code>opus</code>, <code>haiku</code>, a full model ID, or <code>skip</code> for the default:`,
173
- stayInStep: true,
174
- }
175
- }
176
- return { kind: 'ask-emoji', slug, persona, model: trimmed }
177
- }
178
-
179
- case 'asked-emoji': {
180
- const slug = state.slug ?? ''
181
- const persona = state.persona ?? ''
182
- const model = state.model ?? null
183
- const emoji = isSkip(trimmed) ? null : trimmed
184
- if (!isSkip(trimmed) && !isValidEmoji(trimmed)) {
185
- return {
186
- kind: 'error',
187
- message: 'Emoji must be 1-16 characters. Try again, or type <code>skip</code>:',
188
- stayInStep: true,
189
- }
190
- }
191
- // #190: gate on the profile selector before bot-token. The previous
192
- // wizard skipped straight to bot-token and hard-coded profile='default'.
193
- return { kind: 'ask-profile', slug, persona, model, emoji, profiles }
194
- }
195
-
196
- case 'asked-profile': {
197
- // #190: validate against the live list passed in by the foreman.
198
- if (!profiles.includes(trimmed)) {
199
- return {
200
- kind: 'error',
201
- message: `Unknown profile "${escapeForMessage(trimmed)}". Choose one of: ${profiles.join(', ')}`,
202
- stayInStep: true,
203
- }
204
- }
205
- const slug = state.slug ?? ''
206
- const persona = state.persona ?? ''
207
- const model = state.model ?? null
208
- const emoji = state.emoji ?? null
209
- if (!slug || !persona) {
210
- return { kind: 'cancel', reason: 'missing-slug-or-persona' }
211
- }
212
- return { kind: 'ask-bot-token', slug, persona, model, emoji, profile: trimmed }
213
- }
214
-
215
- case 'asked-bot-token': {
216
- const slug = state.slug ?? ''
217
- const persona = state.persona ?? ''
218
- // Basic bot token shape check
219
- if (!trimmed.includes(':') || trimmed.length < 20) {
220
- return {
221
- kind: 'error',
222
- message: "That doesn't look like a BotFather token (expected <code>1234567890:AAH...</code>). Try again:",
223
- stayInStep: true,
224
- }
225
- }
226
- if (!slug || !persona) {
227
- return { kind: 'cancel', reason: 'missing-slug-or-persona' }
228
- }
229
- return { kind: 'confirm-allowlist', slug, callerId }
230
- }
231
-
232
- case 'confirming-allowlist': {
233
- const slug = state.slug ?? ''
234
- const persona = state.persona ?? ''
235
- const model = state.model ?? null
236
- const emoji = state.emoji ?? null
237
- // #190: profile is captured in asked-profile. Default to 'default' for
238
- // legacy in-flight flows that started before the selector was added —
239
- // they pre-date the schema migration and have profile=null.
240
- const profile = state.profile ?? 'default'
241
- const botToken = state.botToken ?? ''
242
- const allowedUserId = trimmed.toLowerCase() === 'yes' || trimmed.toLowerCase() === 'y'
243
- ? callerId
244
- : trimmed // let the user override with a different user_id
245
-
246
- if (!allowedUserId) {
247
- return {
248
- kind: 'error',
249
- message: 'Please reply <b>yes</b> to use your own user_id, or paste a different user_id:',
250
- stayInStep: true,
251
- }
252
- }
253
- // #189: was 'call-reconcile' (createAgent + send-to-terminal); now
254
- // 'call-create-agent' which returns loginUrl so we can ask for the
255
- // OAuth code in-wizard. completeCreation runs in the next state.
256
- return { kind: 'call-create-agent', slug, persona, model, emoji, profile, botToken, allowedUserId }
257
- }
258
-
259
- case 'asked-oauth-code': {
260
- // #189: collect the OAuth code pasted by the user and pass it to
261
- // call-complete-creation. Same shape as the create-flow's equivalent.
262
- const slug = state.slug ?? ''
263
- const persona = state.persona ?? ''
264
- if (!slug) return { kind: 'cancel', reason: 'missing-slug' }
265
- if (trimmed.length < 4) {
266
- return {
267
- kind: 'error',
268
- message: 'That code looks too short. Paste the full code from the browser:',
269
- stayInStep: true,
270
- }
271
- }
272
- return { kind: 'call-complete-creation', slug, persona, code: trimmed }
273
- }
274
-
275
- case 'reconciling':
276
- // Should not receive text during reconciliation — foreman handles this step programmatically
277
- return { kind: 'cancel', reason: 'unexpected-text-in-reconciling' }
278
-
279
- case 'done':
280
- return { kind: 'cancel', reason: 'flow-already-done' }
281
-
282
- default: {
283
- const _exhaustive: never = state.step
284
- return { kind: 'cancel', reason: `unknown-step:${String(_exhaustive)}` }
285
- }
286
- }
287
- }
288
-
289
- /** Tiny HTML-escape for inline error messages. The foreman renders these
290
- * via switchroomReply with html: true, so user-supplied text needs the
291
- * same escape pass the rest of the wizard does. */
292
- function escapeForMessage(s: string): string {
293
- return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
294
- }
295
-
296
- // ─── State factory helpers ────────────────────────────────────────────────
297
-
298
- export function makeSetupInitialState(
299
- chatId: string,
300
- slug: string | null,
301
- ): SetupFlowState {
302
- const now = Date.now()
303
- return {
304
- chatId,
305
- step: slug ? 'asked-persona' : 'asked-slug',
306
- slug,
307
- persona: null,
308
- model: null,
309
- emoji: null,
310
- profile: null,
311
- botToken: null,
312
- allowedUserId: null,
313
- authSessionName: null,
314
- loginUrl: null,
315
- startedAt: now,
316
- updatedAt: now,
317
- }
318
- }
319
-
320
- export function advanceSetupState(
321
- state: SetupFlowState,
322
- updates: Partial<Omit<SetupFlowState, 'chatId' | 'startedAt'>>,
323
- ): SetupFlowState {
324
- return {
325
- ...state,
326
- ...updates,
327
- updatedAt: Date.now(),
328
- }
329
- }
330
-
331
- /** Human-readable step label for resume messages. */
332
- export function setupStepLabel(step: SetupFlowStep): string {
333
- switch (step) {
334
- case 'asked-slug': return 'waiting for agent slug'
335
- case 'asked-persona': return 'waiting for persona name'
336
- case 'asked-model': return 'waiting for model choice'
337
- case 'asked-emoji': return 'waiting for emoji'
338
- case 'asked-profile': return 'waiting for profile selection'
339
- case 'asked-bot-token': return 'waiting for BotFather token'
340
- case 'confirming-allowlist': return 'waiting for allowlist confirmation'
341
- case 'reconciling': return 'provisioning agent'
342
- case 'asked-oauth-code': return 'waiting for OAuth code'
343
- case 'done': return 'done'
344
- }
345
- }
@@ -1,239 +0,0 @@
1
- /**
2
- * /setup wizard conversation state — SQLite-backed per-chat state.
3
- *
4
- * Survives foreman restarts so a wizard started before a restart can resume.
5
- *
6
- * Location: ~/.switchroom/foreman/state.sqlite (same DB as create_flow)
7
- * Override via SWITCHROOM_FOREMAN_DIR env var.
8
- *
9
- * Schema:
10
- * CREATE TABLE IF NOT EXISTS setup_flow (
11
- * chat_id TEXT PRIMARY KEY,
12
- * step TEXT NOT NULL,
13
- * slug TEXT,
14
- * persona TEXT,
15
- * model TEXT,
16
- * emoji TEXT,
17
- * bot_token TEXT,
18
- * allowed_user_id TEXT,
19
- * started_at INTEGER NOT NULL,
20
- * updated_at INTEGER NOT NULL
21
- * );
22
- */
23
-
24
- import { Database } from 'bun:sqlite'
25
- import { chmodSync, mkdirSync } from 'fs'
26
- import { homedir } from 'os'
27
- import { join } from 'path'
28
-
29
- // ─── Types ────────────────────────────────────────────────────────────────
30
-
31
- export type SetupFlowStep =
32
- | 'asked-slug'
33
- | 'asked-persona'
34
- | 'asked-model'
35
- | 'asked-emoji'
36
- | 'asked-profile' // #190: skills/profile selector before bot-token
37
- | 'asked-bot-token'
38
- | 'confirming-allowlist'
39
- | 'reconciling'
40
- | 'asked-oauth-code' // #189: in-wizard OAuth code paste (replaces "go to terminal")
41
- | 'done'
42
-
43
- export interface SetupFlowState {
44
- chatId: string
45
- step: SetupFlowStep
46
- slug: string | null
47
- persona: string | null
48
- model: string | null
49
- emoji: string | null
50
- /** #190: which profile to use when scaffolding. Defaults to 'default'
51
- * for flows that started before the profile-selector step landed. */
52
- profile: string | null
53
- botToken: string | null
54
- allowedUserId: string | null
55
- /** #189: foreman captures these from createAgent's return value so the
56
- * asked-oauth-code step can render the URL and the call-complete-creation
57
- * action can find the right session. Null until call-create-agent runs. */
58
- authSessionName: string | null
59
- loginUrl: string | null
60
- startedAt: number
61
- updatedAt: number
62
- }
63
-
64
- // ─── DB singleton ─────────────────────────────────────────────────────────
65
-
66
- let _setupDb: Database | null = null
67
-
68
- function getSetupDb(): Database {
69
- if (_setupDb) return _setupDb
70
-
71
- const foremanDir =
72
- process.env.SWITCHROOM_FOREMAN_DIR ?? join(homedir(), '.switchroom', 'foreman')
73
-
74
- mkdirSync(foremanDir, { recursive: true, mode: 0o700 })
75
-
76
- const dbPath = join(foremanDir, 'state.sqlite')
77
- _setupDb = new Database(dbPath)
78
- try {
79
- chmodSync(dbPath, 0o600)
80
- } catch {
81
- // best-effort
82
- }
83
-
84
- _setupDb.exec(`
85
- CREATE TABLE IF NOT EXISTS setup_flow (
86
- chat_id TEXT PRIMARY KEY,
87
- step TEXT NOT NULL,
88
- slug TEXT,
89
- persona TEXT,
90
- model TEXT,
91
- emoji TEXT,
92
- bot_token TEXT,
93
- allowed_user_id TEXT,
94
- started_at INTEGER NOT NULL,
95
- updated_at INTEGER NOT NULL
96
- );
97
- `)
98
-
99
- // #189/#190: idempotent column additions for installs that pre-date the
100
- // profile + OAuth-code steps. ALTER TABLE ADD COLUMN is a no-op when the
101
- // column already exists in SQLite ≥ 3.35; older versions throw — wrap in
102
- // try/catch so the migration is forward-compatible.
103
- for (const migration of [
104
- `ALTER TABLE setup_flow ADD COLUMN profile TEXT`,
105
- `ALTER TABLE setup_flow ADD COLUMN auth_session_name TEXT`,
106
- `ALTER TABLE setup_flow ADD COLUMN login_url TEXT`,
107
- ]) {
108
- try {
109
- _setupDb.exec(migration)
110
- } catch {
111
- // Column already exists. Ignore.
112
- }
113
- }
114
-
115
- return _setupDb
116
- }
117
-
118
- // ─── Row type ─────────────────────────────────────────────────────────────
119
-
120
- interface SetupFlowRow {
121
- chat_id: string
122
- step: string
123
- slug: string | null
124
- persona: string | null
125
- model: string | null
126
- emoji: string | null
127
- bot_token: string | null
128
- allowed_user_id: string | null
129
- started_at: number
130
- updated_at: number
131
- // #189/#190: nullable in legacy rows that pre-date the migration.
132
- profile: string | null
133
- auth_session_name: string | null
134
- login_url: string | null
135
- }
136
-
137
- function rowToState(row: SetupFlowRow): SetupFlowState {
138
- return {
139
- chatId: row.chat_id,
140
- step: row.step as SetupFlowStep,
141
- slug: row.slug,
142
- persona: row.persona,
143
- model: row.model,
144
- emoji: row.emoji,
145
- profile: row.profile,
146
- botToken: row.bot_token,
147
- allowedUserId: row.allowed_user_id,
148
- authSessionName: row.auth_session_name,
149
- loginUrl: row.login_url,
150
- startedAt: row.started_at,
151
- updatedAt: row.updated_at,
152
- }
153
- }
154
-
155
- // ─── Public API ───────────────────────────────────────────────────────────
156
-
157
- /** Upsert the setup wizard state for a given chat. */
158
- export function setSetupState(state: SetupFlowState): void {
159
- const db = getSetupDb()
160
- db.prepare(`
161
- INSERT INTO setup_flow
162
- (chat_id, step, slug, persona, model, emoji, profile, bot_token,
163
- allowed_user_id, auth_session_name, login_url, started_at, updated_at)
164
- VALUES
165
- ($chatId, $step, $slug, $persona, $model, $emoji, $profile, $botToken,
166
- $allowedUserId, $authSessionName, $loginUrl, $startedAt, $updatedAt)
167
- ON CONFLICT(chat_id) DO UPDATE SET
168
- step = excluded.step,
169
- slug = excluded.slug,
170
- persona = excluded.persona,
171
- model = excluded.model,
172
- emoji = excluded.emoji,
173
- profile = excluded.profile,
174
- bot_token = excluded.bot_token,
175
- allowed_user_id = excluded.allowed_user_id,
176
- auth_session_name = excluded.auth_session_name,
177
- login_url = excluded.login_url,
178
- updated_at = excluded.updated_at
179
- `).run({
180
- $chatId: state.chatId,
181
- $step: state.step,
182
- $slug: state.slug,
183
- $persona: state.persona,
184
- $model: state.model,
185
- $emoji: state.emoji,
186
- $profile: state.profile,
187
- $botToken: state.botToken,
188
- $allowedUserId: state.allowedUserId,
189
- $authSessionName: state.authSessionName,
190
- $loginUrl: state.loginUrl,
191
- $startedAt: state.startedAt,
192
- $updatedAt: state.updatedAt,
193
- })
194
- }
195
-
196
- /** Retrieve the setup wizard state for a given chat, or null if none. */
197
- export function getSetupState(chatId: string): SetupFlowState | null {
198
- const db = getSetupDb()
199
- const row = db.prepare<SetupFlowRow, [string]>(`
200
- SELECT chat_id, step, slug, persona, model, emoji, profile, bot_token,
201
- allowed_user_id, auth_session_name, login_url, started_at, updated_at
202
- FROM setup_flow
203
- WHERE chat_id = ?
204
- `).get(chatId)
205
-
206
- return row ? rowToState(row) : null
207
- }
208
-
209
- /** Remove the setup wizard state for a given chat. */
210
- export function clearSetupState(chatId: string): void {
211
- const db = getSetupDb()
212
- db.prepare('DELETE FROM setup_flow WHERE chat_id = ?').run(chatId)
213
- }
214
-
215
- /**
216
- * List all in-progress setup flows updated within the last `maxAgeMs` ms.
217
- * Used at foreman startup to resume flows that survived a restart.
218
- */
219
- export function listActiveSetupFlows(maxAgeMs = 60 * 60 * 1000): SetupFlowState[] {
220
- const db = getSetupDb()
221
- const cutoff = Date.now() - maxAgeMs
222
- const rows = db.prepare<SetupFlowRow, [number]>(`
223
- SELECT chat_id, step, slug, persona, model, emoji, profile, bot_token,
224
- allowed_user_id, auth_session_name, login_url, started_at, updated_at
225
- FROM setup_flow
226
- WHERE step != 'done' AND updated_at > ?
227
- ORDER BY updated_at DESC
228
- `).all(cutoff)
229
-
230
- return rows.map(rowToState)
231
- }
232
-
233
- /** Reset the DB singleton (useful in tests to avoid sharing state). */
234
- export function _resetSetupDbForTest(): void {
235
- if (_setupDb) {
236
- _setupDb.close()
237
- _setupDb = null
238
- }
239
- }