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,303 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PostToolUse hook — detect a wedged persistent-bash session.
4
+ *
5
+ * Claude Code's Bash tool uses a persistent `bash` subprocess for state
6
+ * continuity (so `cd /foo` in one call survives to the next). When that
7
+ * subprocess's IO state desyncs — typically after a long-running or
8
+ * interrupted command leaves stdin in mid-heredoc, or after sentinel
9
+ * parsing breaks — every subsequent Bash call returns exit-1 with empty
10
+ * stdout and empty stderr. Even `true` returns exit 1. The wedge is
11
+ * sticky for the session; `switchroom agent restart <self>` is the only
12
+ * reliable recovery (it spawns a fresh `claude` → fresh persistent bash).
13
+ *
14
+ * This hook watches PostToolUse events for the wedge signature and,
15
+ * after N consecutive matches, writes a sentinel + logs to stderr so
16
+ * the operator (via `docker logs`) or the gateway (via a future card)
17
+ * can prompt for restart. The hook itself can NEVER fix the wedge —
18
+ * PostToolUse fires after the tool already ran. It's a detection +
19
+ * surfacing surface, not a recovery surface.
20
+ *
21
+ * Claude Code PostToolUse protocol:
22
+ * stdin: JSON { tool_name, tool_use_id, tool_input, tool_response, ... }
23
+ * stdout: optional JSON (hookSpecificOutput.additionalContext for next
24
+ * turn). We use this to nudge the model toward KillBash +
25
+ * self-restart guidance once the wedge is detected.
26
+ * exit: 0 always. Hook failures must never block the tool flow.
27
+ *
28
+ * State:
29
+ * $TELEGRAM_STATE_DIR/wedge-counter.txt — integer, consecutive empty Bash
30
+ * results. Reset to 0 on any non-Bash event or any non-empty Bash
31
+ * result. Incremented on each empty Bash result.
32
+ * $TELEGRAM_STATE_DIR/wedge-detected.json — JSON sentinel written when
33
+ * counter reaches THRESHOLD. Contains { ts, session_id, agent,
34
+ * consecutive }. Gateway can poll for this and surface a card; for
35
+ * now its presence is informational only.
36
+ *
37
+ * Threshold: 3. Picked to balance false positives (some real commands
38
+ * legitimately produce no output and exit non-zero, e.g. `test -f
39
+ * /nonexistent`) against latency-to-detect. Three in a row is rare
40
+ * outside genuine wedge.
41
+ *
42
+ * Detection is shape-based not exit-code-based because the tool_response
43
+ * shape varies by Claude Code version. We match on:
44
+ * - tool_name === "Bash"
45
+ * - stringified response contains BOTH empty stdout marker AND empty
46
+ * stderr marker. Marker patterns covered: <bash-stdout></bash-stdout>,
47
+ * "stdout":"" + "stderr":"", and the bare "(no output)" string some
48
+ * versions emit.
49
+ *
50
+ * If detection markers change in a future Claude Code release, this hook
51
+ * silently misses the wedge — that's the right failure mode (better than
52
+ * false-firing).
53
+ */
54
+
55
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
56
+ import { join, dirname } from 'node:path'
57
+
58
+ // Higher than the original 3 to avoid false-firing on legitimate
59
+ // empty-output command sequences (a sed, then two greps with no matches,
60
+ // is a normal refactor pattern and shouldn't trigger). PR #1188 review
61
+ // found 3 was guaranteed-FP. 5 + the noOutputExpected /
62
+ // returnCodeInterpretation skip below should keep real wedges detectable
63
+ // while staying quiet during normal grep/find/sed chains.
64
+ const THRESHOLD = 5
65
+
66
+ // node:fs operations on the counter / sentinel files are read-modify-write
67
+ // without explicit locking. Safe because Claude Code serializes tool calls
68
+ // per session — there is at most one PostToolUse fire in flight per agent
69
+ // at any time. Documented so a future caller doesn't introduce parallelism
70
+ // and silently lose counts.
71
+
72
+ function readStdin() {
73
+ try {
74
+ return readFileSync(0, 'utf8')
75
+ } catch {
76
+ return ''
77
+ }
78
+ }
79
+
80
+ function stateDir() {
81
+ return process.env.TELEGRAM_STATE_DIR || null
82
+ }
83
+
84
+ function counterPath() {
85
+ const dir = stateDir()
86
+ return dir ? join(dir, 'wedge-counter.txt') : null
87
+ }
88
+
89
+ function sentinelPath() {
90
+ const dir = stateDir()
91
+ return dir ? join(dir, 'wedge-detected.json') : null
92
+ }
93
+
94
+ function readCounter() {
95
+ const p = counterPath()
96
+ if (!p || !existsSync(p)) return 0
97
+ try {
98
+ const raw = readFileSync(p, 'utf8').trim()
99
+ const n = Number.parseInt(raw, 10)
100
+ return Number.isFinite(n) && n >= 0 ? n : 0
101
+ } catch {
102
+ return 0
103
+ }
104
+ }
105
+
106
+ function writeCounter(n) {
107
+ const p = counterPath()
108
+ if (!p) return
109
+ try {
110
+ mkdirSync(dirname(p), { recursive: true })
111
+ writeFileSync(p, String(n), 'utf8')
112
+ } catch {
113
+ // fail-silent; counter loss just delays detection by a couple of cycles
114
+ }
115
+ }
116
+
117
+ function writeSentinel(payload) {
118
+ const p = sentinelPath()
119
+ if (!p) return
120
+ try {
121
+ mkdirSync(dirname(p), { recursive: true })
122
+ writeFileSync(p, JSON.stringify(payload, null, 2), 'utf8')
123
+ } catch {
124
+ // fail-silent
125
+ }
126
+ }
127
+
128
+ function clearSentinel() {
129
+ const p = sentinelPath()
130
+ if (!p) return
131
+ try {
132
+ rmSync(p, { force: true })
133
+ } catch {
134
+ // fail-silent
135
+ }
136
+ }
137
+
138
+ function resetCounter() {
139
+ // Counter reset means we're back in healthy territory — clear the
140
+ // sentinel too so a future operator-side surface that polls for
141
+ // `wedge-detected.json` doesn't see stale state from a long-cleared
142
+ // wedge. Per PR #1188 review B2.
143
+ writeCounter(0)
144
+ clearSentinel()
145
+ }
146
+
147
+ /**
148
+ * Test whether a Bash tool_response matches the wedge signature.
149
+ *
150
+ * The wedge produces: empty stdout AND empty stderr AND no
151
+ * Claude-Code-supplied "no output is expected here" annotation AND not
152
+ * interrupted by the user.
153
+ *
154
+ * The benign empty-output cases that PR #1188 review B1 called out
155
+ * (grep/find/sed/test with no matches or in-place mutation) are
156
+ * disambiguated by:
157
+ * - `noOutputExpected: true` — Claude Code annotates Bash calls whose
158
+ * command pattern legitimately produces no output.
159
+ * - `returnCodeInterpretation: "..."` — present when Claude Code has
160
+ * a human-readable explanation for the exit code (e.g. "No matches
161
+ * found" for grep). Its presence means "this empty result is
162
+ * understood, not a desync."
163
+ * - `interrupted: true` — user pressed `!` mid-command. Not a wedge.
164
+ *
165
+ * Defensive: response shape varies across Claude Code versions and
166
+ * across plain-string vs structured-object representations. We check
167
+ * each known marker and fail-no-match on anything else.
168
+ */
169
+ function isEmptyBashResponse(toolResponse) {
170
+ if (toolResponse == null) return false
171
+
172
+ // Structured-object path. Most reliable — read the fields directly
173
+ // and consult the annotations.
174
+ if (typeof toolResponse === 'object') {
175
+ const r = toolResponse
176
+ // Interruption is user-initiated, not a desync. Don't count.
177
+ if (r.interrupted === true) return false
178
+ // Claude Code already knows this command's empty output is expected.
179
+ if (r.noOutputExpected === true) return false
180
+ // Claude Code has a human-readable explanation — the empty result is
181
+ // accounted for, not a parse failure.
182
+ if (typeof r.returnCodeInterpretation === 'string' && r.returnCodeInterpretation.length > 0) {
183
+ return false
184
+ }
185
+ // Real empty-result check. Both streams empty (or missing).
186
+ const stdout = typeof r.stdout === 'string' ? r.stdout : ''
187
+ const stderr = typeof r.stderr === 'string' ? r.stderr : ''
188
+ if (stdout === '' && stderr === '') return true
189
+ return false
190
+ }
191
+
192
+ // String path — older Claude Code versions, or when the response was
193
+ // wrapped before reaching the hook. We can't read structured fields,
194
+ // so we rely on substring shape and accept slightly higher FP risk on
195
+ // this path (covered by THRESHOLD raise + skill-side recovery being
196
+ // cheap).
197
+ let body
198
+ try {
199
+ body = String(toolResponse)
200
+ } catch {
201
+ return false
202
+ }
203
+ if (body.length > 4096) return false
204
+
205
+ // If the string form contains noOutputExpected:true or a
206
+ // returnCodeInterpretation, treat as accounted-for.
207
+ if (/"noOutputExpected"\s*:\s*true/.test(body)) return false
208
+ if (/"interrupted"\s*:\s*true/.test(body)) return false
209
+ if (/"returnCodeInterpretation"\s*:\s*"[^"]+"/.test(body)) return false
210
+
211
+ // XML-style tags: <bash-stdout></bash-stdout><bash-stderr></bash-stderr>
212
+ const hasEmptyStdoutTag = /<bash-stdout>\s*<\/bash-stdout>/i.test(body)
213
+ const hasEmptyStderrTag = /<bash-stderr>\s*<\/bash-stderr>/i.test(body)
214
+ if (hasEmptyStdoutTag && hasEmptyStderrTag) return true
215
+
216
+ // JSON-stringified shape from older serializers.
217
+ const hasEmptyStdoutJson = /"stdout"\s*:\s*""/.test(body)
218
+ const hasEmptyStderrJson = /"stderr"\s*:\s*""/.test(body)
219
+ if (hasEmptyStdoutJson && hasEmptyStderrJson) return true
220
+
221
+ // Literal zero-info bodies.
222
+ if (body === '{}' || body === '""' || body === '') return true
223
+
224
+ return false
225
+ }
226
+
227
+ function emitWedgeContext(consecutive) {
228
+ // PostToolUse can prepend additionalContext to the model's next turn.
229
+ // Use it to surface a single-line nudge once the wedge is suspected
230
+ // so the agent knows to try recovery rather than retrying the same
231
+ // command in a loop.
232
+ const text =
233
+ `[wedge-detect] ${consecutive} consecutive empty-result Bash calls — ` +
234
+ `your persistent shell is likely wedged. Try \`KillBash\` to drop ` +
235
+ `the wedged session, OR ask the user for \`switchroom agent restart ${process.env.SWITCHROOM_AGENT_NAME || '<self>'}\` ` +
236
+ `if KillBash doesn't recover. Don't retry the same command.`
237
+ const payload = {
238
+ hookSpecificOutput: {
239
+ hookEventName: 'PostToolUse',
240
+ additionalContext: text,
241
+ },
242
+ }
243
+ try {
244
+ process.stdout.write(JSON.stringify(payload) + '\n')
245
+ } catch {
246
+ // fail-silent
247
+ }
248
+ }
249
+
250
+ function main() {
251
+ const raw = readStdin()
252
+ if (!raw) return
253
+ let evt
254
+ try {
255
+ evt = JSON.parse(raw)
256
+ } catch {
257
+ return
258
+ }
259
+
260
+ // Non-Bash events reset the counter (the wedge is specific to the
261
+ // persistent shell; other tools succeeding doesn't tell us anything
262
+ // about Bash, but a different tool firing means we're at least not in
263
+ // a tight loop of Bash retries — safe to reset).
264
+ if (evt.tool_name !== 'Bash') {
265
+ resetCounter()
266
+ return
267
+ }
268
+
269
+ if (!isEmptyBashResponse(evt.tool_response)) {
270
+ // Bash call returned real output → not wedged → reset.
271
+ resetCounter()
272
+ return
273
+ }
274
+
275
+ // Empty Bash result. Increment.
276
+ const next = readCounter() + 1
277
+ writeCounter(next)
278
+
279
+ if (next >= THRESHOLD) {
280
+ const sentinel = {
281
+ ts: new Date().toISOString(),
282
+ session_id: evt.session_id || null,
283
+ agent: process.env.SWITCHROOM_AGENT_NAME || null,
284
+ consecutive: next,
285
+ // Capture the last tool_use_id so an operator-side investigator
286
+ // can pin which tool calls triggered the threshold.
287
+ last_tool_use_id: evt.tool_use_id || null,
288
+ }
289
+ writeSentinel(sentinel)
290
+ process.stderr.write(
291
+ `wedge-detect: ${next} consecutive empty-result Bash calls; ` +
292
+ `sentinel at ${sentinelPath()}; recommend KillBash or ` +
293
+ `switchroom agent restart\n`,
294
+ )
295
+ emitWedgeContext(next)
296
+ }
297
+ }
298
+
299
+ try {
300
+ main()
301
+ } catch {
302
+ // PostToolUse must never block the tool flow.
303
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * inbound-classifier.ts — cheap regex classifier for inbound user text.
3
+ *
4
+ * Today the only signal we emit is `status_query` — short user messages
5
+ * asking "are you still working / are you there / status?" — because that's
6
+ * the primary KPI for the conversational turn UX redesign (see issue #1122):
7
+ * if the user has to ask, the design has failed.
8
+ *
9
+ * Strictly read-only: the classifier never alters routing. Inbound text
10
+ * still reaches the agent unchanged.
11
+ */
12
+
13
+ /**
14
+ * Patterns that mean "are you still working / do you remember me." Kept
15
+ * conservative on purpose — false positives are worse than misses, because
16
+ * a wrong-positive would noise up the very KPI we're trying to measure.
17
+ *
18
+ * All patterns match the entire trimmed message body (anchored), so longer
19
+ * messages that happen to contain "status" don't trip them.
20
+ */
21
+ const STATUS_QUERY_PATTERNS: readonly RegExp[] = [
22
+ /^\?+$/,
23
+ /^status\s*\??$/i,
24
+ /^update\s*\??$/i,
25
+ /^any\s+update\s*\??$/i,
26
+ /^still\s+there\s*\??$/i,
27
+ /^still\s+working\s*\??$/i,
28
+ /^are\s+you\s+there\s*\??$/i,
29
+ /^you\s+there\s*\??$/i,
30
+ /^hello\s*\?+$/i,
31
+ /^hey\s*\?+$/i,
32
+ ]
33
+
34
+ export interface InboundClassification {
35
+ isStatusQuery: boolean
36
+ }
37
+
38
+ export function classifyInbound(text: string | null | undefined): InboundClassification {
39
+ if (text == null) return { isStatusQuery: false }
40
+ const trimmed = text.trim()
41
+ if (trimmed === '') return { isStatusQuery: false }
42
+ // Cap length — a long message that starts with "status?" isn't a status
43
+ // query; the user's appending real content. Keep the classifier focused
44
+ // on standalone "ping" messages.
45
+ if (trimmed.length > 40) return { isStatusQuery: false }
46
+ for (const pat of STATUS_QUERY_PATTERNS) {
47
+ if (pat.test(trimmed)) return { isStatusQuery: true }
48
+ }
49
+ return { isStatusQuery: false }
50
+ }
@@ -164,3 +164,139 @@ export function validateAndWrapAgentKeyboard(
164
164
  const wrapped = wrapAgentCallbacks(keyboard)
165
165
  return { ok: true, wrapped }
166
166
  }
167
+
168
+ // ─── finalizeCallback (#1150 + audit follow-up) ──────────────────────────
169
+ //
170
+ // Centralized "the user tapped a terminal button" helper. Every callback
171
+ // handler that resolves a decision (Approve / Deny / Pick option / Confirm
172
+ // revoke / Always-allow / Dismiss / etc.) MUST route through this helper
173
+ // so the three button-UX invariants are uniformly enforced:
174
+ //
175
+ // 1. Visible press feedback — `answerCallbackQuery` with `text:` so
176
+ // Telegram shows a toast. Operators who tap and see nothing within
177
+ // ~200ms double-tap; the toast IS the press-feedback affordance.
178
+ // 2. Keyboard collapses with clarity — the message is edited in place
179
+ // to strip `reply_markup` AND append a status line that describes
180
+ // what the operator selected. One atomic edit, not two: the user
181
+ // must be able to scroll back later and see the resolved decision
182
+ // next to the original prompt.
183
+ // 3. Side effect (typically: synthesize an inbound back to the model
184
+ // so the agent's turn continues) — runs AFTER the message edit
185
+ // lands so the model never sees "I'm being woken up" before the
186
+ // operator sees the visual confirmation.
187
+ //
188
+ // Multi-step wizards (vault grant wizard, etc.) should NOT use this
189
+ // helper for intermediate-step transitions — those swap one keyboard
190
+ // for the next via `editMessageText` + new `reply_markup`. Use this
191
+ // helper for the WIZARD-FINAL step (Generate, Cancel) so the success
192
+ // card collapses correctly and isn't re-tappable.
193
+
194
+ /**
195
+ * Minimal callback-context shape the helper needs. Real grammy
196
+ * `Context` satisfies this; tests can implement a lightweight fake
197
+ * without dragging the grammy types in.
198
+ */
199
+ export interface FinalizeCallbackContext {
200
+ answerCallbackQuery: (
201
+ opts?: { text?: string; show_alert?: boolean },
202
+ ) => Promise<unknown>
203
+ editMessageText: (text: string, opts?: Record<string, unknown>) => Promise<unknown>
204
+ }
205
+
206
+ export interface FinalizeCallbackOptions {
207
+ /**
208
+ * Toast text shown to the operator via `answerCallbackQuery`. Telegram
209
+ * caps this at 200 chars; pass a short verb-phrase ("Approved",
210
+ * "Saved", "Switching to slot 2"). Required — the toast IS invariant 1.
211
+ */
212
+ ackText: string
213
+ /**
214
+ * When true, the toast renders as a full modal alert instead of the
215
+ * bottom-bar toast. Default false. Use for destructive or one-way
216
+ * decisions (e.g. "Vault grant revoked") where the operator needs
217
+ * stronger acknowledgement.
218
+ */
219
+ alert?: boolean
220
+ /**
221
+ * The new body text for the message AFTER the keyboard is stripped.
222
+ * Build this yourself from `<original prompt>\n\n<status line>` so the
223
+ * scrollback preserves the question alongside the answer. The keyboard
224
+ * is stripped unconditionally regardless of `newText`.
225
+ */
226
+ newText: string
227
+ /**
228
+ * Parse mode for `newText`. Match the original message's parse mode —
229
+ * mixing modes mid-edit silently breaks formatting. Optional; omitted
230
+ * means plain text.
231
+ */
232
+ parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'
233
+ /**
234
+ * Side effect invoked AFTER `editMessageText` resolves. Use for
235
+ * synthesizing the `<channel source="...">` inbound that wakes the
236
+ * agent's session. Errors are caught + logged via the `log` seam;
237
+ * they do NOT propagate (a failed inbound synthesis must not regress
238
+ * to "operator's tap visually un-applied"). The model-visible flow
239
+ * is allowed to fail loudly via separate mechanisms (the supervisor
240
+ * watchdog, the silence-poke ladder); this helper's job is to keep
241
+ * invariants 1+2 strict and best-effort the rest.
242
+ *
243
+ * Skip for surfaces with no model in the loop (auth dashboard
244
+ * actions that shell to the host CLI, operator-event dismiss, etc).
245
+ */
246
+ synthInbound?: () => void | Promise<void>
247
+ /** Logger seam for tests. Defaults to stderr. */
248
+ log?: (line: string) => void
249
+ }
250
+
251
+ /**
252
+ * Apply the three-invariant finalize pattern. See module docstring
253
+ * above for design rationale.
254
+ *
255
+ * Order: ack → edit → synth. The ack is fired-and-forgotten (so a slow
256
+ * Telegram API doesn't delay the visible state change), but the edit
257
+ * is awaited so `synthInbound` doesn't race ahead of the operator's
258
+ * visual confirmation. Each step's error is logged + swallowed —
259
+ * partial success is preferred to "tap looked dead AND the model
260
+ * stayed stuck" full failure.
261
+ */
262
+ export async function finalizeCallback(
263
+ ctx: FinalizeCallbackContext,
264
+ opts: FinalizeCallbackOptions,
265
+ ): Promise<void> {
266
+ const log = opts.log ?? ((line: string) => process.stderr.write(line))
267
+ // Invariant 1 — toast. Fire-and-forget; we don't want a slow
268
+ // answerCallbackQuery round-trip to delay the message edit.
269
+ void ctx.answerCallbackQuery({
270
+ text: opts.ackText,
271
+ ...(opts.alert ? { show_alert: true } : {}),
272
+ }).catch((err: unknown) => {
273
+ log(`finalizeCallback: answerCallbackQuery failed: ${(err as Error).message}\n`)
274
+ })
275
+ // Invariant 2 — strip keyboard + append status line, atomic edit.
276
+ try {
277
+ await ctx.editMessageText(opts.newText, {
278
+ reply_markup: { inline_keyboard: [] },
279
+ ...(opts.parseMode ? { parse_mode: opts.parseMode } : {}),
280
+ // Default link_preview_options off — most finalized cards don't
281
+ // benefit from preview cards, and a stale preview survives the
282
+ // edit otherwise.
283
+ link_preview_options: { is_disabled: true },
284
+ })
285
+ } catch (err) {
286
+ // MESSAGE_NOT_MODIFIED (text didn't change) and MESSAGE_TO_EDIT_NOT_FOUND
287
+ // (operator already deleted the card) are both benign. Other failures
288
+ // log + continue — we still want synthInbound to run.
289
+ log(`finalizeCallback: editMessageText failed: ${(err as Error).message}\n`)
290
+ }
291
+ // Invariant 3 — model wake-up (when applicable).
292
+ if (opts.synthInbound != null) {
293
+ try {
294
+ const r = opts.synthInbound()
295
+ if (r != null && typeof (r as Promise<unknown>).then === 'function') {
296
+ await (r as Promise<unknown>)
297
+ }
298
+ } catch (err) {
299
+ log(`finalizeCallback: synthInbound threw: ${(err as Error).message}\n`)
300
+ }
301
+ }
302
+ }
@@ -0,0 +1 @@
1
+ {"version":"3.2.4","results":[[":tests/progress-card-driver.test.ts",{"duration":82.55005700000004,"failed":false}],[":tests/progress-card.test.ts",{"duration":50.04072000000002,"failed":false}],[":tests/telegram-format.test.ts",{"duration":0,"failed":false}],[":tests/stream-reply-handler.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-harness.test.ts",{"duration":4955.245276000001,"failed":false}],[":tests/streaming-orchestration.test.ts",{"duration":0,"failed":false}],[":tests/races.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher.test.ts",{"duration":0,"failed":false}],[":tests/gateway-bridge.test.ts",{"duration":0,"failed":true}],[":tests/answer-stream.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-pin-manager.test.ts",{"duration":25.365711999999974,"failed":false}],[":tests/pty-tail.test.ts",{"duration":0,"failed":false}],[":tests/turn-end-regressions.test.ts",{"duration":0,"failed":false}],[":tests/session-tail.test.ts",{"duration":0,"failed":false}],[":tests/gateway-clean-shutdown-marker.test.ts",{"duration":0,"failed":true}],[":tests/e2e.test.ts",{"duration":0,"failed":false}],[":tests/setup-flow.test.ts",{"duration":0,"failed":false}],[":tests/tool-labels.test.ts",{"duration":0,"failed":false}],[":tests/registry-turns.test.ts",{"duration":0,"failed":true}],[":tests/welcome-text.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-oauth-code.test.ts",{"duration":0,"failed":false}],[":tests/draft-stream.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-stuck-warning.test.ts",{"duration":21.23773799999998,"failed":false}],[":tests/history.test.ts",{"duration":0,"failed":false}],[":tests/streaming-e2e.test.ts",{"duration":0,"failed":false}],[":tests/foreman-handlers.test.ts",{"duration":0,"failed":false}],[":tests/foreman-create-flow.test.ts",{"duration":0,"failed":false}],[":tests/operator-events.test.ts",{"duration":0,"failed":false}],[":tests/vault-grants-revoke.test.ts",{"duration":0,"failed":false}],[":tests/boot-probes.test.ts",{"duration":0,"failed":true}],[":tests/pty-partial-handler.test.ts",{"duration":0,"failed":false}],[":tests/auth-slot-commands.test.ts",{"duration":0,"failed":false}],[":tests/vault-subcommands.test.ts",{"duration":0,"failed":false}],[":tests/auth-dashboard.test.ts",{"duration":0,"failed":false}],[":tests/active-pins-sweep.test.ts",{"duration":0,"failed":false}],[":tests/turns-writer.test.ts",{"duration":0,"failed":true}],[":tests/retry-api-call.test.ts",{"duration":0,"failed":false}],[":tests/steering.test.ts",{"duration":0,"failed":false}],[":tests/outbound-ordering.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-client.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-mutex.test.ts",{"duration":0,"failed":true}],[":tests/auth-dashboard-edge-cases.test.ts",{"duration":0,"failed":false}],[":tests/status-reactions.test.ts",{"duration":0,"failed":false}],[":tests/auto-fallback.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect.test.ts",{"duration":0,"failed":false}],[":tests/foreman-write-ops.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-render.test.ts",{"duration":0,"failed":false}],[":tests/handoff-continuity.test.ts",{"duration":0,"failed":false}],[":tests/false-restart-banner.test.ts",{"duration":0,"failed":false}],[":tests/answer-stream-silent-markers.test.ts",{"duration":0,"failed":false}],[":tests/ipc-validator.test.ts",{"duration":0,"failed":false}],[":tests/stream-controller.test.ts",{"duration":0,"failed":false}],[":tests/gateway-409-retry-banner.test.ts",{"duration":0,"failed":false}],[":tests/stream-reply-error-paths.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-race.test.ts",{"duration":0,"failed":false}],[":tests/progress-update.test.ts",{"duration":0,"failed":true}],[":tests/restart-watchdog.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-cross-turn.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-probe-target.test.ts",{"duration":0,"failed":false}],[":tests/active-reactions.test.ts",{"duration":0,"failed":false}],[":tests/quota-cache.test.ts",{"duration":0,"failed":true}],[":tests/streaming-metrics.test.ts",{"duration":0,"failed":false}],[":tests/operator-events-session-tail.test.ts",{"duration":0,"failed":false}],[":tests/foreman-state.test.ts",{"duration":0,"failed":true}],[":tests/ipc-protocol.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-pin-watchdog.test.ts",{"duration":0,"failed":false}],[":tests/telegram-button-constraints.test.ts",{"duration":0,"failed":false}],[":tests/bot-runtime.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-network-retry.test.ts",{"duration":0,"failed":false}],[":tests/attachment-path.test.ts",{"duration":0,"failed":false}],[":tests/active-pins.test.ts",{"duration":0,"failed":false}],[":tests/subagent-tracker-hooks.test.ts",{"duration":0,"failed":true}],[":tests/fake-bot-api.test.ts",{"duration":0,"failed":false}],[":tests/quota-check.test.ts",{"duration":0,"failed":false}],[":tests/parse-mode-rotation.test.ts",{"duration":0,"failed":false}],[":tests/gateway-secret-detect.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-safety.test.ts",{"duration":0,"failed":false}],[":tests/multi-turn-continuity.test.ts",{"duration":0,"failed":false}],[":tests/status-accent.test.ts",{"duration":0,"failed":false}],[":tests/auth-code-auto-capture.test.ts",{"duration":0,"failed":false}],[":tests/auth-login-url-button.test.ts",{"duration":0,"failed":false}],[":tests/auth-dashboard-restart-flow.test.ts",{"duration":0,"failed":false}],[":tests/setup-state.test.ts",{"duration":0,"failed":true}],[":tests/progress-card-golden.test.ts",{"duration":6.658348999999987,"failed":false}],[":tests/unhandled-rejection-policy.test.ts",{"duration":0,"failed":true}],[":tests/typing-wrap.test.ts",{"duration":0,"failed":false}],[":tests/auth-account-identity-surface.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-secretlint.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-pipeline.test.ts",{"duration":0,"failed":false}],[":tests/operator-events-history.test.ts",{"duration":0,"failed":false}],[":tests/gateway-message-validator.test.ts",{"duration":0,"failed":false}],[":tests/silent-reply-guard.test.ts",{"duration":0,"failed":false}],[":tests/active-reactions-sweep.test.ts",{"duration":0,"failed":false}],[":tests/pin-event-log.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-fail-closed.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-operator.test.ts",{"duration":0,"failed":false}],[":tests/idle-footer-wiring.test.ts",{"duration":0,"failed":true}],[":tests/boot-card-reason.test.ts",{"duration":0,"failed":true}],[":tests/plugin-logger.test.ts",{"duration":0,"failed":false}],[":tests/vault-grant-wizard.test.ts",{"duration":0,"failed":false}],[":tests/turn-signal-tracker.test.ts",{"duration":0,"failed":false}],[":tests/context-exhaustion.test.ts",{"duration":0,"failed":false}],[":tests/gateway-startup-reset.test.ts",{"duration":0,"failed":false}],[":tests/idle-footer.test.ts",{"duration":0,"failed":false}],[":tests/poll-health.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-prose-recovery.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-suppressor-no-silent-allow.test.ts",{"duration":0,"failed":false}],[":tests/boot-card-dedupe.test.ts",{"duration":0,"failed":true}],[":tests/protocol-fixtures.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-staging.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-audit.test.ts",{"duration":0,"failed":false}],[":tests/secret-detect-gitleaks.test.ts",{"duration":0,"failed":false}],[":tests/subagent-registry-bugs.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-driver-eviction.test.ts",{"duration":22.822707999999977,"failed":false}],[":tests/progress-card-driver-fleet-shadow.test.ts",{"duration":7.463677000000018,"failed":false}],[":tests/two-zone-card-lifecycle.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-concurrent-turns-isolation.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-survives-next-turn.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-snapshot.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-edit-throttle.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-done-when-all-terminal.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-html-balance.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-bg-detection.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-draft-flag.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-per-member.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-header-phases.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-recovery.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-stuck-header-escalation.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-fleet-row.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-cap.test.ts",{"duration":0,"failed":false}],[":tests/two-zone-card-sanitise.test.ts",{"duration":0,"failed":false}],[":tests/progress-card-close-paths-converge.test.ts",{"duration":10.167681000000016,"failed":false}],[":registry/subagents.test.ts",{"duration":0,"failed":true}],[":tests/issues-card.test.ts",{"duration":0,"failed":false}],[":registry/subagents-bugs.test.ts",{"duration":0,"failed":true}],[":tests/answer-stream-dedup.test.ts",{"duration":0,"failed":false}],[":tests/model-unavailable.test.ts",{"duration":0,"failed":false}],[":tests/preamble-suppressor.test.ts",{"duration":0,"failed":false}],[":tests/harness-ordering-invariants.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-spec.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-ipc-lifecycle.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-i6-turn-flush-replay-dedup.test.ts",{"duration":0,"failed":false}],[":tests/slot-banner-driver.e2e.test.ts",{"duration":0,"failed":false}],[":tests/waiting-ux.e2e.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher-parent-marker.test.ts",{"duration":0,"failed":false}],[":tests/auth-code-redact.test.ts",{"duration":0,"failed":false}],[":tests/subagent-watcher-stall-notification.test.ts",{"duration":0,"failed":false}],[":tests/telegraph.test.ts",{"duration":0,"failed":true}],[":tests/recent-outbound-dedup.test.ts",{"duration":0,"failed":true}],[":tests/turn-flush-card-takeover.test.ts",{"duration":0,"failed":false}],[":tests/resolve-calling-subagent.test.ts",{"duration":0,"failed":true}],[":tests/secret-guard-pretool.test.ts",{"duration":0,"failed":true}],[":tests/first-paint.test.ts",{"duration":0,"failed":false}],[":tests/ask-user.test.ts",{"duration":0,"failed":true}],[":registry/api-registry.test.ts",{"duration":0,"failed":true}],[":tests/credits-watch.test.ts",{"duration":0,"failed":false}],[":admin-commands/dispatch.test.ts",{"duration":0,"failed":false}],[":tests/fleet-state.test.ts",{"duration":0,"failed":false}],[":tests/voice-transcribe.test.ts",{"duration":0,"failed":true}],[":tests/turn-active-marker.test.ts",{"duration":0,"failed":false}],[":tests/inline-keyboard-callbacks.test.ts",{"duration":0,"failed":false}],[":tests/issues-watcher.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f1-ladder-integrity.test.ts",{"duration":0,"failed":false}],[":tests/gateway-disconnect-flush.test.ts",{"duration":0,"failed":false}],[":tests/reply-terminal-reaction.test.ts",{"duration":0,"failed":false}],[":tests/turn-flush-dedup-controller.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f3-late-card.test.ts",{"duration":0,"failed":false}],[":tests/status-reactions-allowed-filter.test.ts",{"duration":0,"failed":false}],[":tests/pty-tail-real-fixture.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway.smoke.test.ts",{"duration":0,"failed":false}],[":tests/harness-parse-mode-validation.test.ts",{"duration":0,"failed":false}],[":tests/inbound-coalesce.test.ts",{"duration":0,"failed":false}],[":tests/gateway-update-placeholder-dispatch.test.ts",{"duration":0,"failed":true}],[":tests/interrupt-marker.test.ts",{"duration":0,"failed":true}],[":tests/dm-command-gate.test.ts",{"duration":0,"failed":false}],[":tests/auto-fallback-dispatcher.e2e.test.ts",{"duration":0,"failed":false}],[":tests/sync-chat-running-subagents.test.ts",{"duration":0,"failed":false}],[":tests/subagents-schema-init-order.test.ts",{"duration":0,"failed":true}],[":tests/draft-transport.test.ts",{"duration":0,"failed":false}],[":gateway/access-validator.test.ts",{"duration":0,"failed":false}],[":registry/turns-schema.test.ts",{"duration":0,"failed":true}],[":tests/update-factory-edited-and-reactions.test.ts",{"duration":0,"failed":false}],[":tests/gateway-no-reply-single-emit.test.ts",{"duration":0,"failed":false}],[":tests/real-gateway-f2-instant-draft.test.ts",{"duration":0,"failed":false}],[":tests/gateway-boot-marker-clear.test.ts",{"duration":0,"failed":false}],[":tests/fleet-state-watcher.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-update-placeholder.test.ts",{"duration":0,"failed":false}],[":tests/permission-title.test.ts",{"duration":0,"failed":false}],[":tests/slot-banner.test.ts",{"duration":0,"failed":false}],[":tests/sticker-aliases.test.ts",{"duration":0,"failed":true}],[":tests/ipc-server-anonymous-refuse.test.ts",{"duration":0,"failed":false}],[":tests/ipc-server-validate-pty-partial.test.ts",{"duration":0,"failed":false}],[":channel-envelope-safety.test.ts",{"duration":0,"failed":false}],[":tests/bridge-anonymous-refuse.test.ts",{"duration":0,"failed":false}],[":tests/send-typing-action-validation.test.ts",{"duration":0,"failed":false}],[":tests/spawn-detached-cgroup-escape.test.ts",{"duration":0,"failed":false}],[":gateway/boot-sweep-filter.test.ts",{"duration":0,"failed":false}],[":tests/finalize-callback.test.ts",{"duration":17.317307,"failed":false}]]}
@@ -21,7 +21,8 @@
21
21
  "build": "node scripts/build.mjs",
22
22
  "prepublishOnly": "npm run build",
23
23
  "test:uat": "vitest run --config ../vitest.uat.config.ts",
24
- "uat:login": "bun uat/login.ts"
24
+ "uat:login": "bun uat/login.ts",
25
+ "uat:driver-info": "bun uat/driver-info.ts"
25
26
  },
26
27
  "dependencies": {
27
28
  "@grammyjs/runner": "^2.0.3",
@@ -31,7 +32,8 @@
31
32
  "@secretlint/secretlint-rule-preset-recommend": "^12.2.0",
32
33
  "@secretlint/types": "^12.2.0",
33
34
  "@xterm/headless": "^6.0.0",
34
- "grammy": "^1.21.0"
35
+ "grammy": "^1.21.0",
36
+ "posthog-node": "^5.29.2"
35
37
  },
36
38
  "engines": {
37
39
  "node": ">=20.11.0"
@@ -131,3 +131,54 @@ function skillBasenameFromPath(input: Record<string, unknown>): string | null {
131
131
  const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
132
132
  return basename(trimmed) || null;
133
133
  }
134
+
135
+ /**
136
+ * Inverse of `resolveAlwaysAllowRule` — does a stored allow-rule cover a
137
+ * fresh `permission_request`? Used by the bridge's session-scoped
138
+ * always-allow cache (issue #1138) to short-circuit prompts when the
139
+ * operator has already tapped "🔁 Always allow" for an equivalent tool
140
+ * call earlier in the same session.
141
+ *
142
+ * Matching rules (mirrors what `resolveAlwaysAllowRule` produces):
143
+ *
144
+ * - Bare tool name (`Edit`, `Bash`, `Write`, …) ⇒ matches any
145
+ * invocation of that tool. This is consistent with how
146
+ * `tools.allow: [Edit]` works in `.claude/settings.json` — there's
147
+ * no arg matching at this layer.
148
+ * - `Skill(<name>)` ⇒ matches only `Skill` invocations whose resolved
149
+ * skill name (via the same field-fallback chain as the resolver)
150
+ * equals `<name>`.
151
+ * - `mcp__<server>__<tool>` ⇒ matches the exact namespaced MCP tool
152
+ * name.
153
+ *
154
+ * Returns `false` for any malformed rule rather than throwing — the
155
+ * caller (bridge) is on the hot permission path and should fall through
156
+ * to the gateway prompt on bad input.
157
+ */
158
+ export function matchesAllowRule(
159
+ rule: string,
160
+ toolName: string,
161
+ inputPreview: string | undefined,
162
+ ): boolean {
163
+ if (!rule || !toolName) return false;
164
+
165
+ // Skill(name) — extract the parenthesized argument and compare against
166
+ // the resolved skill identifier from the request.
167
+ const skillMatch = /^Skill\(([^)]+)\)$/.exec(rule);
168
+ if (skillMatch) {
169
+ if (toolName !== "Skill") return false;
170
+ const ruleSkill = skillMatch[1];
171
+ const input = parseInput(inputPreview);
172
+ if (!input) return false;
173
+ const reqSkill =
174
+ readString(input, "skill") ??
175
+ readString(input, "skill_name") ??
176
+ readString(input, "skillName") ??
177
+ readString(input, "name") ??
178
+ skillBasenameFromPath(input);
179
+ return reqSkill === ruleSkill;
180
+ }
181
+
182
+ // Bare tool name or namespaced MCP tool — exact string compare.
183
+ return rule === toolName;
184
+ }