switchroom 0.7.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/README.md +51 -59
  2. package/bin/run-hook.sh +27 -11
  3. package/bin/timezone-hook.sh +9 -7
  4. package/dist/agent-scheduler/index.js +410 -133
  5. package/dist/auth-broker/index.js +13932 -0
  6. package/dist/cli/switchroom.js +26937 -5601
  7. package/dist/host-control/main.js +12702 -0
  8. package/dist/vault/approvals/kernel-server.js +467 -184
  9. package/dist/vault/broker/server.js +1430 -724
  10. package/examples/minimal.yaml +63 -0
  11. package/examples/personal-google-workspace-mcp/.env.example +34 -0
  12. package/examples/personal-google-workspace-mcp/README.md +194 -0
  13. package/examples/personal-google-workspace-mcp/compose.yaml +66 -0
  14. package/examples/switchroom.yaml +220 -0
  15. package/package.json +7 -4
  16. package/profiles/_base/settings.json.hbs +20 -5
  17. package/profiles/_base/start.sh.hbs +16 -3
  18. package/profiles/_shared/agent-self-service.md.hbs +126 -0
  19. package/profiles/_shared/telegram-style.md.hbs +20 -90
  20. package/profiles/_shared/vault-protocol.md.hbs +68 -0
  21. package/profiles/default/CLAUDE.md +50 -96
  22. package/profiles/default/CLAUDE.md.hbs +36 -6
  23. package/profiles/default/workspace/SOUL.md.hbs +12 -5
  24. package/skills/buildkite-agent-infrastructure/SKILL.md +30 -11
  25. package/skills/buildkite-agent-runtime/SKILL.md +44 -11
  26. package/skills/buildkite-api/SKILL.md +31 -8
  27. package/skills/buildkite-cli/SKILL.md +27 -9
  28. package/skills/buildkite-migration/SKILL.md +22 -9
  29. package/skills/buildkite-pipelines/SKILL.md +26 -9
  30. package/skills/buildkite-secure-delivery/SKILL.md +23 -9
  31. package/skills/buildkite-test-engine/SKILL.md +25 -8
  32. package/skills/docx/SKILL.md +1 -1
  33. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
  34. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
  35. package/skills/file-bug/SKILL.md +34 -6
  36. package/skills/humanizer/SKILL.md +15 -0
  37. package/skills/humanizer-calibrate/SKILL.md +7 -1
  38. package/skills/mcp-builder/SKILL.md +1 -1
  39. package/skills/pdf/SKILL.md +1 -1
  40. package/skills/pptx/SKILL.md +1 -1
  41. package/skills/skill-creator/SKILL.md +21 -1
  42. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
  44. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
  45. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
  46. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
  47. package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
  48. package/skills/switchroom-cli/SKILL.md +63 -64
  49. package/skills/switchroom-health/SKILL.md +23 -10
  50. package/skills/switchroom-install/SKILL.md +3 -3
  51. package/skills/switchroom-manage/SKILL.md +26 -19
  52. package/skills/switchroom-runtime/SKILL.md +191 -0
  53. package/skills/switchroom-status/SKILL.md +27 -2
  54. package/skills/telegram-test-harness/SKILL.md +3 -0
  55. package/skills/token-helpers/SKILL.md +24 -1
  56. package/skills/webapp-testing/SKILL.md +31 -1
  57. package/skills/xlsx/SKILL.md +1 -1
  58. package/telegram-plugin/admin-commands/index.ts +7 -5
  59. package/telegram-plugin/analytics-posthog.ts +191 -0
  60. package/telegram-plugin/bridge/bridge.ts +69 -0
  61. package/telegram-plugin/bridge/ipc-client.ts +4 -1
  62. package/telegram-plugin/dist/bridge/bridge.js +194 -119
  63. package/telegram-plugin/dist/gateway/gateway.js +23611 -19671
  64. package/telegram-plugin/dist/server.js +245 -189
  65. package/telegram-plugin/first-paint.ts +3 -24
  66. package/telegram-plugin/gateway/auth-add-flow.ts +326 -0
  67. package/telegram-plugin/gateway/auth-broker-client.ts +75 -0
  68. package/telegram-plugin/gateway/auth-command.ts +794 -0
  69. package/telegram-plugin/gateway/auth-line.ts +123 -0
  70. package/telegram-plugin/gateway/boot-card.ts +169 -40
  71. package/telegram-plugin/gateway/boot-issue-cache.ts +308 -0
  72. package/telegram-plugin/gateway/boot-probes.ts +166 -123
  73. package/telegram-plugin/gateway/boot-reason.ts +41 -7
  74. package/telegram-plugin/gateway/boot-version.ts +66 -0
  75. package/telegram-plugin/gateway/gateway.ts +3499 -1885
  76. package/telegram-plugin/gateway/hostd-dispatch.ts +117 -0
  77. package/telegram-plugin/gateway/ipc-protocol.ts +18 -0
  78. package/telegram-plugin/gateway/pending-inbound-buffer.ts +106 -0
  79. package/telegram-plugin/gateway/quarantine.ts +69 -0
  80. package/telegram-plugin/gateway/quota-cache.ts +9 -4
  81. package/telegram-plugin/gateway/reaction-trigger.ts +401 -0
  82. package/telegram-plugin/gateway/recent-denials.test.ts +103 -0
  83. package/telegram-plugin/gateway/recent-denials.ts +77 -0
  84. package/telegram-plugin/gateway/startup-network-retry.ts +109 -31
  85. package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +125 -0
  86. package/telegram-plugin/history.ts +91 -0
  87. package/telegram-plugin/hooks/hooks.json +10 -0
  88. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +130 -0
  89. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -2
  90. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +22 -2
  91. package/telegram-plugin/hooks/tool-label-pretool.mjs +11 -0
  92. package/telegram-plugin/hooks/wedge-detect-posttool.mjs +303 -0
  93. package/telegram-plugin/inbound-classifier.ts +50 -0
  94. package/telegram-plugin/inline-keyboard-callbacks.ts +136 -0
  95. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  96. package/telegram-plugin/package.json +4 -2
  97. package/telegram-plugin/permission-rule.ts +51 -0
  98. package/telegram-plugin/permission-title.ts +56 -0
  99. package/telegram-plugin/quota-check.ts +19 -41
  100. package/telegram-plugin/registry/reaper.ts +223 -0
  101. package/telegram-plugin/retry-api-call.ts +80 -0
  102. package/telegram-plugin/runtime-metrics.ts +177 -0
  103. package/telegram-plugin/scripts/build.mjs +0 -1
  104. package/telegram-plugin/secret-detect/index.ts +24 -0
  105. package/telegram-plugin/secret-detect/vault-error.test.ts +64 -12
  106. package/telegram-plugin/secret-detect/vault-error.ts +78 -11
  107. package/telegram-plugin/secret-detect/vault-write.ts +14 -2
  108. package/telegram-plugin/server.js +41795 -0
  109. package/telegram-plugin/session-tail.ts +6 -1
  110. package/telegram-plugin/shared/bot-runtime.ts +5 -4
  111. package/telegram-plugin/silence-poke.ts +420 -0
  112. package/telegram-plugin/silent-end.ts +174 -0
  113. package/telegram-plugin/stream-controller.ts +13 -0
  114. package/telegram-plugin/stream-reply-handler.ts +7 -0
  115. package/telegram-plugin/subagent-watcher.ts +213 -4
  116. package/telegram-plugin/tests/auth-add-flow.test.ts +559 -0
  117. package/telegram-plugin/tests/auth-code-redact.test.ts +8 -4
  118. package/telegram-plugin/tests/auth-command-vernacular.test.ts +531 -0
  119. package/telegram-plugin/tests/boot-card-issue-dedup.test.ts +247 -0
  120. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +182 -0
  121. package/telegram-plugin/tests/boot-card-reason.test.ts +65 -2
  122. package/telegram-plugin/tests/boot-card-render.test.ts +146 -0
  123. package/telegram-plugin/tests/boot-card-silent-on-operator.test.ts +103 -0
  124. package/telegram-plugin/tests/boot-probes.test.ts +216 -10
  125. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  126. package/telegram-plugin/tests/finalize-callback.test.ts +190 -0
  127. package/telegram-plugin/tests/gateway-message-validator.test.ts +26 -0
  128. package/telegram-plugin/tests/gateway-secret-detect.test.ts +12 -3
  129. package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +104 -0
  130. package/telegram-plugin/tests/history-reaper.test.ts +378 -0
  131. package/telegram-plugin/tests/hostd-dispatch.test.ts +129 -0
  132. package/telegram-plugin/tests/inbound-classifier.test.ts +76 -0
  133. package/telegram-plugin/tests/inbound-message-types.test.ts +267 -0
  134. package/telegram-plugin/tests/issues-card.test.ts +49 -0
  135. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +132 -0
  136. package/telegram-plugin/tests/permission-rule.test.ts +80 -1
  137. package/telegram-plugin/tests/permission-title.test.ts +31 -0
  138. package/telegram-plugin/tests/quota-check.test.ts +5 -35
  139. package/telegram-plugin/tests/races.test.ts +179 -0
  140. package/telegram-plugin/tests/reaction-trigger-flow.test.ts +353 -0
  141. package/telegram-plugin/tests/reaction-trigger.test.ts +397 -0
  142. package/telegram-plugin/tests/retry-api-call.test.ts +152 -1
  143. package/telegram-plugin/tests/runtime-metrics.test.ts +145 -0
  144. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +155 -0
  145. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +133 -0
  146. package/telegram-plugin/tests/secret-detect-false-positives.test.ts +137 -0
  147. package/telegram-plugin/tests/silence-poke.test.ts +493 -0
  148. package/telegram-plugin/tests/silent-end.test.ts +206 -0
  149. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +107 -0
  150. package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +224 -0
  151. package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +316 -0
  152. package/telegram-plugin/tests/subagent-watcher.test.ts +263 -0
  153. package/telegram-plugin/tests/turn-signal-tracker.test.ts +81 -0
  154. package/telegram-plugin/tests/vault-approval-posture.test.ts +256 -0
  155. package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +73 -0
  156. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +226 -0
  157. package/telegram-plugin/tests/vault-grant-union.test.ts +130 -0
  158. package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +140 -0
  159. package/telegram-plugin/tests/vault-posture-quarantine.test.ts +104 -0
  160. package/telegram-plugin/tests/vault-request-access-tool.test.ts +114 -0
  161. package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +106 -0
  162. package/telegram-plugin/turn-signal-tracker.ts +100 -24
  163. package/telegram-plugin/uat/SETUP.md +210 -35
  164. package/telegram-plugin/uat/assertions.ts +264 -37
  165. package/telegram-plugin/uat/driver-info.ts +57 -0
  166. package/telegram-plugin/uat/driver.ts +590 -51
  167. package/telegram-plugin/uat/harness.ts +140 -94
  168. package/telegram-plugin/uat/load-env.test.ts +72 -0
  169. package/telegram-plugin/uat/load-env.ts +48 -0
  170. package/telegram-plugin/uat/login.ts +96 -53
  171. package/telegram-plugin/uat/runners/agent-self-sufficiency.ts +457 -0
  172. package/telegram-plugin/uat/runners/paraphrases.ts +231 -0
  173. package/telegram-plugin/uat/runners/report.ts +150 -0
  174. package/telegram-plugin/uat/runners/run-agent-self-sufficiency.sh +50 -0
  175. package/telegram-plugin/uat/runners/scorer.test.ts +196 -0
  176. package/telegram-plugin/uat/runners/scorer.ts +106 -0
  177. package/telegram-plugin/uat/runners/skill-coverage.test.ts +100 -0
  178. package/telegram-plugin/uat/runners/skill-coverage.ts +620 -0
  179. package/telegram-plugin/uat/scenarios/ask-user-button-tap-dm.test.ts +141 -0
  180. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +191 -0
  181. package/telegram-plugin/uat/scenarios/fuzz-extended-dm.test.ts +255 -0
  182. package/telegram-plugin/uat/scenarios/fuzz-human-style-dm.test.ts +275 -0
  183. package/telegram-plugin/uat/scenarios/fuzz-random-prompts-dm.test.ts +146 -0
  184. package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +486 -0
  185. package/telegram-plugin/uat/scenarios/jtbd-interrupt-marker-dm.test.ts +67 -0
  186. package/telegram-plugin/uat/scenarios/jtbd-rapid-followup-dm.test.ts +100 -0
  187. package/telegram-plugin/uat/scenarios/jtbd-soft-commit-dm.test.ts +67 -0
  188. package/telegram-plugin/uat/scenarios/jtbd-status-query-dm.test.ts +49 -0
  189. package/telegram-plugin/uat/scenarios/location-inbound-dm.test.ts +65 -0
  190. package/telegram-plugin/uat/scenarios/midturn-silent-dm.test.ts +175 -0
  191. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +142 -0
  192. package/telegram-plugin/uat/scenarios/reactions-trigger-turn-dm.test.ts +96 -0
  193. package/telegram-plugin/uat/scenarios/secret-redaction-deletes-original-dm.test.ts +123 -0
  194. package/telegram-plugin/uat/scenarios/secret-redaction-no-false-positive-dm.test.ts +87 -0
  195. package/telegram-plugin/uat/scenarios/silence-poke-soft-dm.test.ts +155 -0
  196. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +95 -0
  197. package/telegram-plugin/uat/scenarios/smoke-dm-reply.test.ts +57 -0
  198. package/telegram-plugin/uat/scenarios/subagent-watcher-no-rerun-dm.test.ts +135 -0
  199. package/telegram-plugin/uat/scenarios/vault-approval-posture-telegram-id-dm.test.ts +191 -0
  200. package/telegram-plugin/uat/scenarios/vault-audit-allow-dm.test.ts +108 -0
  201. package/telegram-plugin/uat/scenarios/vault-grant-auto-resume-dm.test.ts +121 -0
  202. package/telegram-plugin/uat/scenarios/vault-request-access-concurrent-dm.test.ts +161 -0
  203. package/telegram-plugin/uat/scenarios/vault-request-access-end-to-end-dm.test.ts +158 -0
  204. package/telegram-plugin/uat/scenarios/voice-inbound-dm.test.ts +65 -0
  205. package/telegram-plugin/vault-approval-posture.ts +42 -0
  206. package/telegram-plugin/welcome-text.ts +1 -0
  207. package/telegram-plugin/active-pins-sweep.ts +0 -204
  208. package/telegram-plugin/active-pins.ts +0 -146
  209. package/telegram-plugin/auth-dashboard.ts +0 -1104
  210. package/telegram-plugin/auth-slot-parser.ts +0 -497
  211. package/telegram-plugin/card-event-log.ts +0 -138
  212. package/telegram-plugin/dist/foreman/foreman.js +0 -31106
  213. package/telegram-plugin/docs/multi-agent-card-design.md +0 -847
  214. package/telegram-plugin/docs/pinned-progress-card-reliability.md +0 -144
  215. package/telegram-plugin/foreman/foreman-create-flow.ts +0 -202
  216. package/telegram-plugin/foreman/foreman-handlers.ts +0 -493
  217. package/telegram-plugin/foreman/foreman.ts +0 -1165
  218. package/telegram-plugin/foreman/setup-flow.ts +0 -345
  219. package/telegram-plugin/foreman/setup-state.ts +0 -239
  220. package/telegram-plugin/foreman/state.ts +0 -203
  221. package/telegram-plugin/pin-event-log.ts +0 -76
  222. package/telegram-plugin/progress-card-driver.ts +0 -2886
  223. package/telegram-plugin/progress-card-pin-manager.ts +0 -589
  224. package/telegram-plugin/progress-card-pin-watchdog.ts +0 -98
  225. package/telegram-plugin/progress-card.ts +0 -1409
  226. package/telegram-plugin/tests/HARNESS.md +0 -340
  227. package/telegram-plugin/tests/_progress-card-harness.ts +0 -109
  228. package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +0 -211
  229. package/telegram-plugin/tests/active-pins-sweep.test.ts +0 -309
  230. package/telegram-plugin/tests/active-pins.test.ts +0 -187
  231. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +0 -118
  232. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +0 -260
  233. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +0 -140
  234. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +0 -559
  235. package/telegram-plugin/tests/auth-dashboard.test.ts +0 -1045
  236. package/telegram-plugin/tests/auth-slot-commands.test.ts +0 -640
  237. package/telegram-plugin/tests/bg-agent-progress-card-757.test.ts +0 -201
  238. package/telegram-plugin/tests/boot-card-account-quota.test.ts +0 -137
  239. package/telegram-plugin/tests/card-event-log.test.ts +0 -145
  240. package/telegram-plugin/tests/first-paint.test.ts +0 -257
  241. package/telegram-plugin/tests/foreman-create-flow.test.ts +0 -359
  242. package/telegram-plugin/tests/foreman-handlers.test.ts +0 -347
  243. package/telegram-plugin/tests/foreman-state.test.ts +0 -164
  244. package/telegram-plugin/tests/foreman-write-ops.test.ts +0 -214
  245. package/telegram-plugin/tests/harness-ordering-invariants.test.ts +0 -243
  246. package/telegram-plugin/tests/pin-event-log.test.ts +0 -124
  247. package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +0 -73
  248. package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +0 -272
  249. package/telegram-plugin/tests/progress-card-cross-turn.test.ts +0 -258
  250. package/telegram-plugin/tests/progress-card-delay-842.test.ts +0 -160
  251. package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +0 -81
  252. package/telegram-plugin/tests/progress-card-draft-flag.test.ts +0 -80
  253. package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +0 -215
  254. package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +0 -123
  255. package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +0 -76
  256. package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +0 -62
  257. package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +0 -84
  258. package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +0 -139
  259. package/telegram-plugin/tests/progress-card-pin-manager.test.ts +0 -773
  260. package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +0 -66
  261. package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +0 -64
  262. package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +0 -190
  263. package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +0 -146
  264. package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +0 -123
  265. package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +0 -82
  266. package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +0 -114
  267. package/telegram-plugin/tests/real-gateway-harness.ts +0 -699
  268. package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +0 -313
  269. package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +0 -299
  270. package/telegram-plugin/tests/real-gateway-spec.test.ts +0 -487
  271. package/telegram-plugin/tests/real-gateway.smoke.test.ts +0 -101
  272. package/telegram-plugin/tests/setup-flow.test.ts +0 -510
  273. package/telegram-plugin/tests/setup-state.test.ts +0 -146
  274. package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +0 -116
  275. package/telegram-plugin/tests/turn-end-regressions.test.ts +0 -489
  276. package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +0 -218
  277. package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +0 -78
  278. package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +0 -131
  279. package/telegram-plugin/tests/two-zone-bg-detection.test.ts +0 -120
  280. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +0 -116
  281. package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +0 -87
  282. package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +0 -211
  283. package/telegram-plugin/tests/two-zone-card-cap.test.ts +0 -62
  284. package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +0 -101
  285. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +0 -78
  286. package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +0 -110
  287. package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +0 -128
  288. package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +0 -58
  289. package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +0 -133
  290. package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +0 -155
  291. package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +0 -117
  292. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +0 -187
  293. package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +0 -149
  294. package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +0 -101
  295. package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +0 -114
  296. package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +0 -105
  297. package/telegram-plugin/tests/waiting-ux-harness.ts +0 -381
  298. package/telegram-plugin/tests/waiting-ux.e2e.test.ts +0 -233
  299. package/telegram-plugin/turn-flush-prose-recovery.ts +0 -40
  300. package/telegram-plugin/two-zone-card.ts +0 -269
  301. package/telegram-plugin/uat/scenarios/smoke-clerk-reply.test.ts +0 -61
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tests for the sandbox-hint-posttool hook (Layer 2 of the sandbox UX work).
3
+ *
4
+ * Hook contract:
5
+ * stdin: PostToolUse JSON event { tool_name, tool_response, ... }
6
+ * stdout: optional JSON
7
+ * {"hookSpecificOutput":{"hookEventName":"PostToolUse",
8
+ * "additionalContext":"..."}}
9
+ * exit: 0 always.
10
+ *
11
+ * Tests spawn the hook as a subprocess (mirroring how Claude Code invokes
12
+ * it), feed a tool_response, and assert whether additionalContext was
13
+ * emitted and that it carries the load-bearing strings the agent needs.
14
+ */
15
+
16
+ import { describe, it, expect } from 'bun:test'
17
+ import { join } from 'path'
18
+ import { spawnSync } from 'child_process'
19
+
20
+ const HOOK_SCRIPT = join(import.meta.dir, '..', 'hooks', 'sandbox-hint-posttool.mjs')
21
+
22
+ function runHook(event: object) {
23
+ const result = spawnSync(process.execPath, [HOOK_SCRIPT], {
24
+ input: JSON.stringify(event),
25
+ encoding: 'utf8',
26
+ env: process.env,
27
+ timeout: 5_000,
28
+ })
29
+ return result
30
+ }
31
+
32
+ function parseContext(stdout: string): string | null {
33
+ if (!stdout.trim()) return null
34
+ const parsed = JSON.parse(stdout)
35
+ return parsed?.hookSpecificOutput?.additionalContext ?? null
36
+ }
37
+
38
+ describe('sandbox-hint-posttool', () => {
39
+ it('emits sandbox hint when tool_response contains EROFS', () => {
40
+ const result = runHook({
41
+ tool_name: 'Write',
42
+ tool_use_id: 'toolu_001',
43
+ tool_response: {
44
+ error: "EROFS: read-only file system, open '/opt/switchroom/skills/foo.md'",
45
+ },
46
+ })
47
+
48
+ expect(result.status).toBe(0)
49
+ const ctx = parseContext(result.stdout)
50
+ expect(ctx).not.toBeNull()
51
+ expect(ctx).toContain('Sandbox boundary hit')
52
+ expect(ctx).toContain('operator action')
53
+ expect(ctx).toContain('Writable paths')
54
+ })
55
+
56
+ it('emits sandbox hint when tool_response contains "Read-only file system"', () => {
57
+ const result = runHook({
58
+ tool_name: 'Edit',
59
+ tool_use_id: 'toolu_002',
60
+ tool_response: 'mkdir: cannot create directory: Read-only file system',
61
+ })
62
+
63
+ expect(result.status).toBe(0)
64
+ const ctx = parseContext(result.stdout)
65
+ expect(ctx).toContain('Sandbox boundary hit')
66
+ })
67
+
68
+ it('emits an apt-specific hint when tool_response shows dpkg permission denied', () => {
69
+ const result = runHook({
70
+ tool_name: 'Bash',
71
+ tool_use_id: 'toolu_003',
72
+ tool_response: {
73
+ stderr:
74
+ 'E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?',
75
+ },
76
+ })
77
+
78
+ expect(result.status).toBe(0)
79
+ const ctx = parseContext(result.stdout)
80
+ expect(ctx).toContain('docker/Dockerfile.agent')
81
+ expect(ctx).toContain('rebuild')
82
+ })
83
+
84
+ it('emits a hint for EACCES on a rootfs path', () => {
85
+ const result = runHook({
86
+ tool_name: 'Bash',
87
+ tool_use_id: 'toolu_004',
88
+ tool_response: 'npm ERR! EACCES: permission denied, mkdir "/usr/lib/node_modules/foo"',
89
+ })
90
+
91
+ expect(result.status).toBe(0)
92
+ const ctx = parseContext(result.stdout)
93
+ expect(ctx).toContain('Sandbox boundary hit')
94
+ })
95
+
96
+ it('emits nothing when tool_response is a normal success', () => {
97
+ const result = runHook({
98
+ tool_name: 'Bash',
99
+ tool_use_id: 'toolu_005',
100
+ tool_response: { stdout: 'hello world\n', exit_code: 0 },
101
+ })
102
+
103
+ expect(result.status).toBe(0)
104
+ expect(result.stdout.trim()).toBe('')
105
+ })
106
+
107
+ it('emits nothing when tool_response merely mentions /usr but is not a sandbox error', () => {
108
+ // Guard against false positives — the agent may legitimately discuss
109
+ // paths under /usr in normal output (e.g. `which node` returning
110
+ // /usr/local/bin/node). Only EACCES / EROFS patterns should trigger.
111
+ const result = runHook({
112
+ tool_name: 'Bash',
113
+ tool_use_id: 'toolu_006',
114
+ tool_response: { stdout: '/usr/local/bin/node\n' },
115
+ })
116
+
117
+ expect(result.status).toBe(0)
118
+ expect(result.stdout.trim()).toBe('')
119
+ })
120
+
121
+ it('exits 0 on malformed stdin without crashing', () => {
122
+ const result = spawnSync(process.execPath, [HOOK_SCRIPT], {
123
+ input: 'not json at all',
124
+ encoding: 'utf8',
125
+ timeout: 5_000,
126
+ })
127
+ expect(result.status).toBe(0)
128
+ expect(result.stdout.trim()).toBe('')
129
+ })
130
+
131
+ it('exits 0 on empty stdin', () => {
132
+ const result = spawnSync(process.execPath, [HOOK_SCRIPT], {
133
+ input: '',
134
+ encoding: 'utf8',
135
+ timeout: 5_000,
136
+ })
137
+ expect(result.status).toBe(0)
138
+ expect(result.stdout.trim()).toBe('')
139
+ })
140
+
141
+ it('caps the scan window for huge tool_response payloads', () => {
142
+ // 100 KiB of harmless output followed by an EROFS — we cap at 64 KiB
143
+ // so this should NOT match. Keeps a runaway tool_response from
144
+ // pinning the hook on a regex scan.
145
+ const huge = 'x'.repeat(100 * 1024) + ' EROFS happened'
146
+ const result = runHook({
147
+ tool_name: 'Bash',
148
+ tool_use_id: 'toolu_007',
149
+ tool_response: { stdout: huge },
150
+ })
151
+
152
+ expect(result.status).toBe(0)
153
+ expect(result.stdout.trim()).toBe('')
154
+ })
155
+ })
@@ -0,0 +1,133 @@
1
+ /**
2
+ * TDD-RED-first contract for the silent-delete-failure class the
3
+ * operator reported on 2026-05-12.
4
+ *
5
+ * Symptom: "🔒 captured a secret. we deleted it from chat" lands as
6
+ * a reply, but the raw secret-bearing message remains visible in
7
+ * chat history. The operator only finds out by scrolling, after
8
+ * the secret has already been screen-shot / cached / synced to
9
+ * other devices.
10
+ *
11
+ * Root cause: every secret-detect call site that invokes
12
+ * `bot.api.deleteMessage(chat_id, msgId)` does it via a raw
13
+ * `try { … } catch { … }` block that silently swallows the error
14
+ * (or, at best, logs to stderr — invisible to the operator). The
15
+ * gateway already has a `deleteSensitiveMessage(chat_id, msgId,
16
+ * reason)` helper that does the right thing — try delete, surface
17
+ * loudly on failure with an in-chat warning naming the message id
18
+ * the operator must delete manually. The four secret-detect sites
19
+ * weren't migrated when that helper was added (#44).
20
+ *
21
+ * The fix is mechanical: replace each `try { bot.api.deleteMessage
22
+ * } catch { }` in the secret-detect path with
23
+ * `deleteSensitiveMessage`. That's what this test pins.
24
+ *
25
+ * Failing means: at least one secret-detect path still has a raw
26
+ * `bot.api.deleteMessage` call wrapped in a swallow-catch — a
27
+ * regression of the silent-failure bug.
28
+ */
29
+
30
+ import { describe, it, expect } from "vitest";
31
+ import { readFileSync } from "node:fs";
32
+ import { resolve } from "node:path";
33
+
34
+ const gatewaySrc = readFileSync(
35
+ resolve(__dirname, "..", "gateway", "gateway.ts"),
36
+ "utf-8",
37
+ );
38
+
39
+ /** Slice the gateway source between two anchor strings. */
40
+ function sliceBetween(src: string, from: string, to: string): string {
41
+ const start = src.indexOf(from);
42
+ if (start < 0) return "";
43
+ const end = src.indexOf(to, start);
44
+ return src.slice(start, end > 0 ? end : src.length);
45
+ }
46
+
47
+ describe("secret-detect — delete failures must NOT be silently swallowed (2026-05-12)", () => {
48
+ // The secret-detect block lives inside `handleInbound`. It runs
49
+ // through several branches:
50
+ // - passphrase cached + high-confidence hit (stored path)
51
+ // - passphrase cached + Channel-B auth-flow fallback
52
+ // - no-passphrase deferred path
53
+ // - pipeline-error fail-closed path
54
+ //
55
+ // Each branch deletes the raw message. None of them may use the
56
+ // raw `bot.api.deleteMessage` pattern wrapped in a swallow-catch
57
+ // — they must all route through `deleteSensitiveMessage` which
58
+ // surfaces failures via stderr + in-chat warning.
59
+
60
+ const secretDetectBlock = sliceBetween(
61
+ gatewaySrc,
62
+ "FAIL-CLOSED: if the pipeline throws",
63
+ "Status reaction controller",
64
+ );
65
+
66
+ it("the secret-detect block exists and is non-trivial (anchor sanity)", () => {
67
+ // fails when: the anchors above are renamed/moved. If this fails,
68
+ // update the anchors AND audit the other assertions in this file
69
+ // to make sure they still target the secret-detect path.
70
+ expect(secretDetectBlock.length).toBeGreaterThan(500);
71
+ });
72
+
73
+ it("no raw `bot.api.deleteMessage` calls inside the secret-detect block", () => {
74
+ // fails when: a code site reverts to the raw API call (which
75
+ // swallows on failure). All deletes here MUST route through the
76
+ // shared helper.
77
+ expect(secretDetectBlock).not.toMatch(/bot\.api\.deleteMessage/);
78
+ });
79
+
80
+ it("every delete in the secret-detect block goes through deleteSensitiveMessage", () => {
81
+ // The block currently performs deletes for four distinct cases
82
+ // (stored / auth-flow-fallback / deferred / pipeline-error). All
83
+ // four MUST land here.
84
+ //
85
+ // fails when: a refactor extracts one of the branches into a
86
+ // helper that doesn't use deleteSensitiveMessage, OR when a new
87
+ // branch is added without the helper. Either way the contract
88
+ // breaks.
89
+ const callMatches = secretDetectBlock.match(/deleteSensitiveMessage\s*\(/g) ?? [];
90
+ expect(
91
+ callMatches.length,
92
+ `expected ≥3 deleteSensitiveMessage calls inside the secret-detect block; got ${callMatches.length}`,
93
+ ).toBeGreaterThanOrEqual(3);
94
+ });
95
+
96
+ it("no swallow-catch pattern around delete calls in the secret-detect block", () => {
97
+ // The legacy pattern: `try { await bot.api.deleteMessage(...) } catch {}`
98
+ // OR `try { ... } catch { /* swallow */ }`. The helper handles
99
+ // failure surfacing, so call sites should NOT wrap the helper
100
+ // in another silencing catch.
101
+ //
102
+ // fails when: a refactor wraps the helper call in `try { ... } catch {}`
103
+ // for "robustness" — which would re-introduce the silent-failure
104
+ // class this PR exists to close.
105
+ expect(secretDetectBlock).not.toMatch(/try\s*\{[^}]*deleteSensitiveMessage[^}]*\}\s*catch\s*\{\s*\}/s);
106
+ // Also forbid the raw `try { bot.api.deleteMessage ... } catch {}` shape.
107
+ expect(secretDetectBlock).not.toMatch(/try\s*\{[^}]*bot\.api\.deleteMessage[^}]*\}\s*catch/s);
108
+ });
109
+ });
110
+
111
+ describe("secret-detect — deleteSensitiveMessage helper retains its 'surface failures' contract", () => {
112
+ // The fix only works if the helper itself surfaces failures.
113
+ // Pin the helper's load-bearing behavior so a future refactor
114
+ // can't quietly turn it into a silent-catch.
115
+ const helperBody = sliceBetween(
116
+ gatewaySrc,
117
+ "async function deleteSensitiveMessage",
118
+ "function getCommandArgs",
119
+ );
120
+
121
+ it("helper logs to stderr on delete failure", () => {
122
+ expect(helperBody).toMatch(/process\.stderr\.write/);
123
+ expect(helperBody).toMatch(/SECURITY:.*FAILED/);
124
+ });
125
+
126
+ it("helper posts an in-chat warning naming the leaked message id", () => {
127
+ // The warning is the only signal a mobile-only operator gets —
128
+ // stderr is invisible to them. Pinning the in-chat surface as
129
+ // the load-bearing piece.
130
+ expect(helperBody).toMatch(/sendMessage/);
131
+ expect(helperBody).toMatch(/delete message.*manually|delete it manually|manually|delete message <code>/i);
132
+ });
133
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * TDD-RED-first contract tests for the false-positive class the
3
+ * operator reported on 2026-05-12.
4
+ *
5
+ * Symptom: casual chat that MENTIONS the words "secret", "token",
6
+ * "password", or an ALLCAPS *_KEY/_TOKEN/_SECRET identifier triggers
7
+ * the redaction pipeline as if the user just pasted a real credential
8
+ * — original message gets deleted, ambiguous-card lands, the operator
9
+ * has to dismiss it. Worst case: the operator is asking the agent
10
+ * *about* a secret ("delete the secret I sent yesterday") and gets
11
+ * stuck in a redaction-of-the-question loop.
12
+ *
13
+ * The pre-fix `env_key_value` pattern in patterns.ts:71 is the
14
+ * load-bearing culprit:
15
+ *
16
+ * /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\b\s*[=:]\s*
17
+ * (["']?)([^\s"'\\]+)\2/g
18
+ *
19
+ * It matches ANY value after `SECRET=` / `TOKEN=` with no entropy
20
+ * gate, no length floor, no shape check on the value. Trivially fires
21
+ * on `SECRET=foo`, `MY_KEY=bar`, even `FATSECRET=hello`.
22
+ *
23
+ * The pre-fix `kv_entropy` pattern in kv-scanner.ts:30 has a 4.0
24
+ * entropy gate which catches some — but `kv_entropy` is the
25
+ * lower-confidence layer and doesn't run when the higher-confidence
26
+ * `env_key_value` already fired. Tightening env_key_value is the
27
+ * right place to land the fix.
28
+ *
29
+ * Each test is the contract: detection MUST NOT fire on the listed
30
+ * input. Failing means the pipeline still flags the input as a hit.
31
+ * The fix adds an entropy + length floor to env_key_value to match
32
+ * the existing kv_entropy precedent.
33
+ */
34
+
35
+ import { describe, it, expect } from "vitest";
36
+ import { detectSecrets } from "../secret-detect/index.js";
37
+
38
+ describe("secret-detect — does NOT fire on casual mentions of 'secret' / 'token' / 'password'", () => {
39
+ // The "operator asking the agent about a secret" cases.
40
+ // Pre-fix: env_key_value matches "SECRET=" anywhere; these were
41
+ // tripping. Post-fix: entropy + length gate on the value.
42
+ it.each([
43
+ [
44
+ "operator asks for a secret by name",
45
+ "what's my fatsecret token?",
46
+ ],
47
+ [
48
+ "operator references a deleted prior message",
49
+ "please delete that secret you sent earlier",
50
+ ],
51
+ [
52
+ "agent name contains 'secret' as a substring (FatSecret API)",
53
+ "the FatSecret API needs an OAuth token — can you wire it up?",
54
+ ],
55
+ [
56
+ "human language sentence with 'password' as a noun",
57
+ "I keep forgetting my password again",
58
+ ],
59
+ [
60
+ "fragment that mentions an env var by name but no value",
61
+ "the FATSECRET_TOKEN env var is missing",
62
+ ],
63
+ [
64
+ "code-shaped placeholder, value is human-readable English",
65
+ "set FOO_SECRET=hello and try again",
66
+ ],
67
+ [
68
+ "shell example with placeholder value (test fixture style)",
69
+ "run: export OPENAI_API_KEY=sk-yourkey",
70
+ ],
71
+ ])("%s — %j", (_label, text) => {
72
+ const hits = detectSecrets(text);
73
+ expect(
74
+ hits,
75
+ `false positive on ${JSON.stringify(text)} — hits=${JSON.stringify(hits.map((h) => h.matched_text))}`,
76
+ ).toEqual([]);
77
+ });
78
+ });
79
+
80
+ describe("secret-detect — DOES still fire on actually-shaped secrets (regression guard)", () => {
81
+ // After tightening env_key_value, these MUST still be caught.
82
+ // Locking in so a future regex-tightening doesn't over-shoot.
83
+ // Values constructed at runtime so the source file doesn't trip
84
+ // GitHub Push Protection — same pattern as
85
+ // secret-detect-secretlint.test.ts:1.
86
+ const fakeApiKey = `sk-ant-${"a1b2c3d4".repeat(4)}XYZ987`; // sk-ant- + 32 chars
87
+ const fakeBearer = `${"abc123".repeat(8)}.${"def456".repeat(4)}`;
88
+ const fakeRandom = `${"x9zM4kP3qR7sT2vW".repeat(2)}`; // 32 chars, high entropy
89
+
90
+ it.each([
91
+ [
92
+ "real-shaped Anthropic API key (anchored prefix path)",
93
+ `export ANTHROPIC_API_KEY=${fakeApiKey}`,
94
+ ],
95
+ [
96
+ "real-shaped Bearer token (anchored Bearer path)",
97
+ `Authorization: Bearer ${fakeBearer}`,
98
+ ],
99
+ [
100
+ "uppercase env_key_value with high-entropy value (must still match)",
101
+ `MYAPP_API_KEY=${fakeRandom}`,
102
+ ],
103
+ ])("%s — %j", (_label, text) => {
104
+ const hits = detectSecrets(text);
105
+ expect(
106
+ hits.length,
107
+ `regression: expected a hit on ${JSON.stringify(text)} but pipeline returned 0`,
108
+ ).toBeGreaterThanOrEqual(1);
109
+ });
110
+ });
111
+
112
+ describe("secret-detect — boundary cases that pin the fix's shape", () => {
113
+ it("short low-entropy value after KEY= is NOT flagged", () => {
114
+ // pre-fix: matches; post-fix: filtered by entropy/length gate.
115
+ expect(detectSecrets("MY_API_KEY=foo")).toEqual([]);
116
+ });
117
+
118
+ it("English word after KEY= is NOT flagged", () => {
119
+ expect(detectSecrets("MY_TOKEN=hello")).toEqual([]);
120
+ });
121
+
122
+ it("long but low-entropy value after KEY= is NOT flagged (repeating chars)", () => {
123
+ // 32 chars but all 'a' — Shannon entropy ~0. Real secrets are
124
+ // dense; this is template/placeholder shape.
125
+ expect(detectSecrets(`MY_KEY=${"a".repeat(32)}`)).toEqual([]);
126
+ });
127
+
128
+ it("high-entropy value after KEY= IS flagged (the fix doesn't break detection)", () => {
129
+ // 32 chars of base64-ish noise. Should match.
130
+ const hits = detectSecrets(
131
+ // Build the value via concat to avoid Push-Protection trip on a
132
+ // contiguous secret-shaped literal.
133
+ `MY_API_KEY=${"k" + "9zMpQrT2vBxYuFnGwL8cHj"}`,
134
+ );
135
+ expect(hits.length).toBeGreaterThanOrEqual(1);
136
+ });
137
+ });