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,493 +0,0 @@
1
- /**
2
- * Pure handler logic extracted from foreman.ts for testability.
3
- *
4
- * foreman.ts has process-level side effects (reads .env, connects Bot,
5
- * starts polling) that prevent direct import in tests. This module
6
- * exports the command handler implementations and their helpers so that
7
- * tests can exercise real code with mocked bot + ctx, rather than
8
- * re-implementing the logic locally.
9
- */
10
-
11
- import { execFileSync } from 'child_process'
12
- import { renameSync, existsSync } from 'fs'
13
- import { homedir } from 'os'
14
- import { join, resolve } from 'path'
15
- import {
16
- escapeHtmlForTg,
17
- preBlock,
18
- stripAnsi,
19
- formatSwitchroomOutput,
20
- } from '../shared/bot-runtime.js'
21
-
22
- // ─── Types ────────────────────────────────────────────────────────────────
23
-
24
- export type SwitchroomExecFn = (args: string[]) => string
25
- export type SwitchroomExecJsonFn = <T = unknown>(args: string[]) => T | null
26
-
27
- // ─── Agent name validation ────────────────────────────────────────────────
28
-
29
- /**
30
- * Throw if the agent name is not safe for use in journalctl unit names.
31
- * Mirrors AGENT_NAME_RE in src/agents/create-orchestrator.ts and the yaml
32
- * schema in src/config/schema.ts — all three MUST stay in sync. Max 51
33
- * chars (see operator-events.ts callback_data contract).
34
- */
35
- export function assertSafeAgentName(name: string): void {
36
- if (!/^[a-z0-9][a-z0-9_-]{0,50}$/.test(name)) {
37
- throw new Error(`invalid agent name: ${name}`)
38
- }
39
- }
40
-
41
- // ─── Tail-N parsing ───────────────────────────────────────────────────────
42
-
43
- export function parseTailN(args: string[]): number {
44
- let tailN = 50
45
- const tailIdx = args.indexOf('--tail')
46
- if (tailIdx !== -1 && args[tailIdx + 1]) {
47
- const parsed = parseInt(args[tailIdx + 1], 10)
48
- if (!isNaN(parsed) && parsed > 0) tailN = Math.min(parsed, 500)
49
- }
50
- return tailN
51
- }
52
-
53
- // ─── Text chunking ────────────────────────────────────────────────────────
54
-
55
- export function chunkText(text: string, maxLen = 4096): string[] {
56
- if (text.length <= maxLen) return [text]
57
- const chunks: string[] = []
58
- let pos = 0
59
- while (pos < text.length) {
60
- chunks.push(text.slice(pos, pos + maxLen))
61
- pos += maxLen
62
- }
63
- return chunks
64
- }
65
-
66
- // ─── /status handler impl ─────────────────────────────────────────────────
67
-
68
- export type AgentListEntry = {
69
- name: string
70
- status: string
71
- uptime: string
72
- template?: string | null
73
- topic_name?: string | null
74
- }
75
-
76
- export function statusIcon(status: string): string {
77
- if (status === 'active' || status === 'running') return '🟢'
78
- if (status === 'inactive' || status === 'stopped' || status === 'dead') return '🔴'
79
- if (status === 'failed') return '⚠️'
80
- return '⚪'
81
- }
82
-
83
- export function buildFleetSummary(
84
- switchroomExecJson: SwitchroomExecJsonFn,
85
- ): string {
86
- try {
87
- const data = switchroomExecJson<{ agents: AgentListEntry[] }>(['agent', 'list'])
88
- if (!data || data.agents.length === 0) return '<i>No agents defined</i>'
89
- const lines = ['<b>Fleet status</b>']
90
- for (const a of data.agents) {
91
- lines.push(
92
- `${statusIcon(a.status)} <b>${escapeHtmlForTg(a.name)}</b> · ${escapeHtmlForTg(a.status)} · ${escapeHtmlForTg(a.uptime)}`,
93
- )
94
- if (a.template || a.topic_name) {
95
- const meta = [a.template, a.topic_name]
96
- .filter(Boolean)
97
- .map((s) => escapeHtmlForTg(s!))
98
- .join(' → ')
99
- lines.push(` <i>${meta}</i>`)
100
- }
101
- }
102
- return lines.join('\n')
103
- } catch (err) {
104
- return `<b>agent list failed:</b>\n${preBlock(formatSwitchroomOutput((err as Error).message))}`
105
- }
106
- }
107
-
108
- // ─── /logs handler impl ───────────────────────────────────────────────────
109
-
110
- export const LOG_PAGE_BYTES = 3 * 1024 // 3 KB
111
-
112
- export interface LogsResult {
113
- /** One or more reply strings. Send them in order. */
114
- replies: Array<{ text: string; html: boolean }>
115
- }
116
-
117
- /**
118
- * Core /logs implementation — returns the reply payloads rather than
119
- * sending them directly, so the caller (foreman.ts) can use its own
120
- * switchroomReply and tests can inspect the output.
121
- *
122
- * @param match The text after "/logs " from ctx.match
123
- * @param execFile Injected execFileSync for testability
124
- */
125
- export function handleLogsCommand(
126
- match: string,
127
- execFile: typeof execFileSync = execFileSync,
128
- ): LogsResult {
129
- const args = match.trim().split(/\s+/).filter(Boolean)
130
-
131
- if (args.length === 0) {
132
- return { replies: [{ text: 'Usage: /logs &lt;agent&gt; [--tail N]', html: true }] }
133
- }
134
-
135
- const agentName = args[0]
136
- try {
137
- assertSafeAgentName(agentName)
138
- } catch {
139
- return { replies: [{ text: 'Invalid agent name.', html: true }] }
140
- }
141
-
142
- const tailN = parseTailN(args)
143
-
144
- let output: string
145
- try {
146
- output = stripAnsi(
147
- execFile(
148
- 'journalctl',
149
- [
150
- '--user',
151
- '-u',
152
- `switchroom-${agentName}`,
153
- '-n',
154
- String(tailN),
155
- '--no-pager',
156
- '--output=short-monotonic',
157
- ],
158
- {
159
- encoding: 'utf-8',
160
- timeout: 10000,
161
- env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
162
- stdio: ['ignore', 'pipe', 'pipe'],
163
- },
164
- ) as string,
165
- )
166
- } catch (err) {
167
- const msg = err as { stdout?: string; stderr?: string; message?: string }
168
- const detail = msg.stdout || msg.stderr || msg.message || 'unknown error'
169
- return {
170
- replies: [
171
- {
172
- text: `<b>logs failed for ${escapeHtmlForTg(agentName)}:</b>\n${preBlock(formatSwitchroomOutput(stripAnsi(detail)))}`,
173
- html: true,
174
- },
175
- ],
176
- }
177
- }
178
-
179
- const trimmed = output.trim()
180
- if (!trimmed) {
181
- return {
182
- replies: [
183
- {
184
- text: `No logs found for <code>${escapeHtmlForTg(agentName)}</code>.`,
185
- html: true,
186
- },
187
- ],
188
- }
189
- }
190
-
191
- if (Buffer.byteLength(trimmed, 'utf8') > LOG_PAGE_BYTES) {
192
- const chunks = chunkText(trimmed, 3800)
193
- return {
194
- replies: chunks.map((chunk, i) => {
195
- const label = chunks.length > 1 ? ` (${i + 1}/${chunks.length})` : ''
196
- return {
197
- text: preBlock(chunk) + (label ? `\n<i>${label}</i>` : ''),
198
- html: true,
199
- }
200
- }),
201
- }
202
- }
203
-
204
- return { replies: [{ text: preBlock(trimmed), html: true }] }
205
- }
206
-
207
- // ─── /restart handler impl ────────────────────────────────────────────────
208
-
209
- export interface RestartResult {
210
- ok: boolean
211
- text: string
212
- html: boolean
213
- }
214
-
215
- /**
216
- * Core /restart implementation.
217
- *
218
- * Shells out to `systemctl --user restart switchroom-<agent>` via execFileSync
219
- * (no shell, so agent name is safely passed as an arg — no injection risk).
220
- *
221
- * @param match Text after "/restart " from ctx.match
222
- * @param execFile Injected execFileSync for testability
223
- */
224
- export function handleRestartCommand(
225
- match: string,
226
- execFile: typeof execFileSync = execFileSync,
227
- ): RestartResult {
228
- const agentName = match.trim().split(/\s+/)[0] ?? ''
229
-
230
- if (!agentName) {
231
- return {
232
- ok: false,
233
- text: 'Usage: /restart &lt;agent&gt;',
234
- html: true,
235
- }
236
- }
237
-
238
- try {
239
- assertSafeAgentName(agentName)
240
- } catch {
241
- return { ok: false, text: 'Invalid agent name.', html: true }
242
- }
243
-
244
- try {
245
- execFile(
246
- 'systemctl',
247
- ['--user', 'restart', `switchroom-${agentName}`],
248
- {
249
- encoding: 'utf-8',
250
- timeout: 15000,
251
- stdio: ['ignore', 'pipe', 'pipe'],
252
- },
253
- )
254
- return {
255
- ok: true,
256
- text: `Restarted <code>switchroom-${escapeHtmlForTg(agentName)}</code>.`,
257
- html: true,
258
- }
259
- } catch (err) {
260
- const msg = err as { stderr?: string; stdout?: string; message?: string }
261
- const detail = stripAnsi(msg.stderr || msg.stdout || msg.message || 'unknown error').trim()
262
- return {
263
- ok: false,
264
- text: `<b>restart failed for ${escapeHtmlForTg(agentName)}:</b>\n${preBlock(formatSwitchroomOutput(detail))}`,
265
- html: true,
266
- }
267
- }
268
- }
269
-
270
- // ─── /delete (destroy) handler impl ──────────────────────────────────────
271
-
272
- export interface DeleteResult {
273
- replies: Array<{ text: string; html: boolean }>
274
- /** When true, foreman.ts should also send an inline keyboard for confirmation. */
275
- needsConfirm?: boolean
276
- /** Agent name (for the confirmation prompt). */
277
- agentForConfirm?: string
278
- }
279
-
280
- /**
281
- * Resolve the agents directory from environment or default location.
282
- * Exposed for testability.
283
- */
284
- export function resolveAgentsDirForDelete(): string {
285
- const switchroomDir = process.env.SWITCHROOM_AGENTS_DIR
286
- ?? join(homedir(), '.switchroom', 'agents')
287
- return switchroomDir
288
- }
289
-
290
- /**
291
- * Core /delete first-step implementation — returns a confirmation prompt.
292
- *
293
- * The actual deletion is performed by executeDeleteAgent() once the user
294
- * confirms via callback_query or "YES" text.
295
- */
296
- export function handleDeleteCommand(match: string): DeleteResult {
297
- const agentName = match.trim().split(/\s+/)[0] ?? ''
298
-
299
- if (!agentName) {
300
- return {
301
- replies: [{ text: 'Usage: /delete &lt;agent&gt;', html: true }],
302
- }
303
- }
304
-
305
- try {
306
- assertSafeAgentName(agentName)
307
- } catch {
308
- return { replies: [{ text: 'Invalid agent name.', html: true }] }
309
- }
310
-
311
- return {
312
- replies: [
313
- {
314
- text: `Are you sure you want to delete agent <b>${escapeHtmlForTg(agentName)}</b>?\n\nThis will stop and remove the systemd unit and archive the agent directory. Reply <b>YES</b> to confirm.`,
315
- html: true,
316
- },
317
- ],
318
- needsConfirm: true,
319
- agentForConfirm: agentName,
320
- }
321
- }
322
-
323
- /**
324
- * Execute agent deletion after confirmation.
325
- *
326
- * Archives the agent dir to `agents/_archived_<name>_<timestamp>/` before
327
- * running `switchroom agent destroy --yes <name>` so data is recoverable.
328
- *
329
- * @param agentName Validated agent name
330
- * @param switchroomExec Injected CLI exec for testability
331
- * @param execFile Injected execFileSync for testability (systemctl)
332
- * @param agentsDir Override agents dir (for tests)
333
- */
334
- export function executeDeleteAgent(
335
- agentName: string,
336
- switchroomExec: SwitchroomExecFn,
337
- execFile: typeof execFileSync = execFileSync,
338
- agentsDir: string = resolveAgentsDirForDelete(),
339
- ): DeleteResult {
340
- try {
341
- assertSafeAgentName(agentName)
342
- } catch {
343
- return { replies: [{ text: 'Invalid agent name.', html: true }] }
344
- }
345
-
346
- const agentDir = resolve(agentsDir, agentName)
347
- let archivePath: string | null = null
348
-
349
- // Step 1: Archive the dir if it exists
350
- if (existsSync(agentDir)) {
351
- const timestamp = Date.now()
352
- archivePath = resolve(agentsDir, `_archived_${agentName}_${timestamp}`)
353
- try {
354
- renameSync(agentDir, archivePath)
355
- } catch (err) {
356
- return {
357
- replies: [
358
- {
359
- text: `<b>Archive failed for ${escapeHtmlForTg(agentName)}:</b>\n${preBlock(formatSwitchroomOutput((err as Error).message))}`,
360
- html: true,
361
- },
362
- ],
363
- }
364
- }
365
- }
366
-
367
- // Step 2: Stop + remove systemd unit via CLI (--yes skips the interactive prompt)
368
- let cliOutput = ''
369
- let cliOk = true
370
- try {
371
- cliOutput = switchroomExec(['agent', 'destroy', '--yes', agentName])
372
- } catch (err) {
373
- cliOk = false
374
- const msg = err as { stderr?: string; stdout?: string; message?: string }
375
- cliOutput = stripAnsi(msg.stderr || msg.stdout || msg.message || 'unknown error').trim()
376
- }
377
-
378
- const lines: string[] = []
379
-
380
- if (archivePath) {
381
- lines.push(`Archived <code>${escapeHtmlForTg(agentName)}</code> to:`)
382
- lines.push(`<code>${escapeHtmlForTg(archivePath)}</code>`)
383
- lines.push('')
384
- }
385
-
386
- if (cliOk) {
387
- lines.push(`Agent <b>${escapeHtmlForTg(agentName)}</b> deleted.`)
388
- if (cliOutput.trim()) {
389
- lines.push(preBlock(formatSwitchroomOutput(stripAnsi(cliOutput))))
390
- }
391
- } else {
392
- lines.push(`<b>CLI destroy failed</b> (agent dir was archived; systemd unit may still exist):`)
393
- lines.push(preBlock(formatSwitchroomOutput(cliOutput)))
394
- }
395
-
396
- return { replies: [{ text: lines.join('\n'), html: true }] }
397
- }
398
-
399
- // ─── /version handler impl ────────────────────────────────────────────────
400
-
401
- export interface VersionResult {
402
- replies: Array<{ text: string; html: boolean }>
403
- }
404
-
405
- /**
406
- * Core /version implementation.
407
- *
408
- * Shells out to `switchroom version` and posts the output. Mirrors the
409
- * /version handler in server.ts and gateway.ts — kept as a pure function
410
- * here so tests can exercise it without a live bot.
411
- *
412
- * @param switchroomExec Injected CLI exec (combined stdout+stderr) for testability
413
- */
414
- export function handleVersionCommand(
415
- switchroomExec: SwitchroomExecFn,
416
- ): VersionResult {
417
- let output: string
418
- try {
419
- output = switchroomExec(['version'])
420
- } catch (err) {
421
- const msg = err as { stderr?: string; stdout?: string; message?: string }
422
- const detail = stripAnsi(msg.stderr || msg.stdout || msg.message || 'unknown error').trim()
423
- return {
424
- replies: [
425
- {
426
- text: `<b>version failed:</b>\n${preBlock(formatSwitchroomOutput(detail))}`,
427
- html: true,
428
- },
429
- ],
430
- }
431
- }
432
-
433
- const trimmed = stripAnsi(output).trim()
434
- if (!trimmed) {
435
- return { replies: [{ text: 'version: no output.', html: false }] }
436
- }
437
-
438
- return { replies: [{ text: preBlock(trimmed), html: true }] }
439
- }
440
-
441
- // ─── /update handler impl ─────────────────────────────────────────────────
442
-
443
- export interface UpdateResult {
444
- replies: Array<{ text: string; html: boolean }>
445
- }
446
-
447
- /**
448
- * Core /update implementation.
449
- *
450
- * Shells out to `switchroom update` via the CLI exec helper. Output is
451
- * paginated when > 3 KB.
452
- *
453
- * @param switchroomExec Injected CLI exec (combined stdout+stderr) for testability
454
- */
455
- export function handleUpdateCommand(
456
- switchroomExec: SwitchroomExecFn,
457
- ): UpdateResult {
458
- let output: string
459
- try {
460
- output = switchroomExec(['update'])
461
- } catch (err) {
462
- const msg = err as { stderr?: string; stdout?: string; message?: string }
463
- const detail = stripAnsi(msg.stderr || msg.stdout || msg.message || 'unknown error').trim()
464
- return {
465
- replies: [
466
- {
467
- text: `<b>update failed:</b>\n${preBlock(formatSwitchroomOutput(detail))}`,
468
- html: true,
469
- },
470
- ],
471
- }
472
- }
473
-
474
- const trimmed = stripAnsi(output).trim()
475
- if (!trimmed) {
476
- return { replies: [{ text: 'Update complete (no output).', html: false }] }
477
- }
478
-
479
- if (Buffer.byteLength(trimmed, 'utf8') > LOG_PAGE_BYTES) {
480
- const chunks = chunkText(trimmed, 3800)
481
- return {
482
- replies: chunks.map((chunk, i) => {
483
- const label = chunks.length > 1 ? ` (${i + 1}/${chunks.length})` : ''
484
- return {
485
- text: preBlock(chunk) + (label ? `\n<i>${label}</i>` : ''),
486
- html: true,
487
- }
488
- }),
489
- }
490
- }
491
-
492
- return { replies: [{ text: preBlock(trimmed), html: true }] }
493
- }