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,308 @@
1
+ /**
2
+ * Boot-card issue dedup cache.
3
+ *
4
+ * Every gateway boot runs a full probe sweep and surfaces every
5
+ * degraded/fail probe as a row on the boot card. That's correct on
6
+ * day one but noisy in steady state: a long-standing "broker socket
7
+ * missing" or "5 dangling skills" row reappears identically on every
8
+ * restart, training the user to ignore the boot card.
9
+ *
10
+ * This module persists a per-probe fingerprint of the last few probe
11
+ * outcomes per chat+topic so the renderer can:
12
+ *
13
+ * - hide ⚠ rows the user has already seen N consecutive boots
14
+ * ("snooze" semantics — the user knows; we won't keep yelling)
15
+ * - render ✅ "resolved" rows for probes that were degraded/fail on
16
+ * the previous boot and are now ok (the positive-feedback signal
17
+ * that's missing from a silent-when-healthy card)
18
+ *
19
+ * Fingerprint policy is per-probe and chosen to fold across the
20
+ * incidental variance in `detail` strings:
21
+ *
22
+ * - skills: folds across dangling-count ("3 dangling: a, b, c"
23
+ * and "4 dangling: a, b, c, d" share one fingerprint)
24
+ * - account: folds by status_kind ("signed-out" vs "token-expired"
25
+ * vs "token-expiring" — the kind of trouble, not the day-count)
26
+ * - agent: folds by raw systemd state string ("service failed",
27
+ * "service activating")
28
+ * - others: literal detail string (broker/kernel/hindsight/quota/
29
+ * scheduler have low-cardinality details that read well as-is)
30
+ *
31
+ * Snooze defaults: hide a row after the user has seen the SAME
32
+ * fingerprint on `snoozeBoots` consecutive boots (default 10) OR for
33
+ * `snoozeMs` (default 3 days), whichever fires first. A change in
34
+ * fingerprint (new failure mode) resets the counter — the user always
35
+ * sees novel failures.
36
+ *
37
+ * Storage: `~/.switchroom/<agent>/boot-issue-cache.json` (mode 0600).
38
+ * On corrupt cache: rename to `<path>.corrupt-<ts>` and start fresh.
39
+ * Entries older than 30 days are GC'd on every load.
40
+ */
41
+
42
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs'
43
+ import { dirname } from 'path'
44
+ import type { ProbeResult } from './boot-probes.js'
45
+ import type { ProbeKey, ProbeMap } from './boot-card.js'
46
+
47
+ // ─── Types ──────────────────────────────────────────────────────────────────
48
+
49
+ export interface BootIssueCacheEntry {
50
+ /** The fingerprint we computed for this probe on this boot. */
51
+ fingerprint: string
52
+ /** Number of CONSECUTIVE boots the same fingerprint has been observed. */
53
+ consecutiveBoots: number
54
+ /** Wall-clock ms at which this fingerprint was first observed in the
55
+ * current run of consecutive boots. */
56
+ firstSeenMs: number
57
+ /** Wall-clock ms at which this fingerprint was most recently observed. */
58
+ lastSeenMs: number
59
+ }
60
+
61
+ export interface BootIssueCacheFile {
62
+ /** Schema version — bump on incompatible changes. */
63
+ schema: 1
64
+ /** Map keyed by ProbeKey. */
65
+ probes: Partial<Record<ProbeKey, BootIssueCacheEntry>>
66
+ }
67
+
68
+ /** Outcome of a single probe after diffing against the cache. */
69
+ export interface ProbeDiffResult {
70
+ /** The fingerprint we'd persist for this outcome. */
71
+ fingerprint: string
72
+ /** True when the probe was degraded/fail on a prior boot and is now ok. */
73
+ resolved: boolean
74
+ /** True when the probe is degraded/fail AND should be hidden ("snoozed")
75
+ * because the user has seen this exact fingerprint enough times. */
76
+ snoozed: boolean
77
+ /** True when this is the FIRST boot we see this fingerprint (counter==1). */
78
+ firstSighting: boolean
79
+ /** The cache entry that would be written for this probe if we apply the diff.
80
+ * null when the probe is `ok` and the cache had no prior entry — nothing to
81
+ * persist. */
82
+ nextEntry: BootIssueCacheEntry | null
83
+ }
84
+
85
+ export type ProbeDiffMap = Partial<Record<ProbeKey, ProbeDiffResult>>
86
+
87
+ export interface DiffOpts {
88
+ /** Hide rows after this many consecutive boots with the same fingerprint. */
89
+ snoozeBoots?: number
90
+ /** Hide rows that have been seen for at least this many ms. */
91
+ snoozeMs?: number
92
+ /** Clock injection for tests. */
93
+ now?: () => number
94
+ }
95
+
96
+ export const DEFAULT_SNOOZE_BOOTS = 10
97
+ export const DEFAULT_SNOOZE_MS = 3 * 24 * 60 * 60 * 1000 // 3 days
98
+ export const GC_AGE_MS = 30 * 24 * 60 * 60 * 1000 // 30 days
99
+
100
+ // ─── Fingerprinting ──────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Compute a stable fingerprint for a probe result, applying the per-probe
104
+ * fold policy described in the module docstring.
105
+ *
106
+ * The fingerprint is what we compare across boots to decide
107
+ * "same issue" vs "new issue". `detail` strings sometimes vary in
108
+ * incidental ways (dangling-count, expiry day-count) that we want to fold,
109
+ * so each probe has its own normalizer.
110
+ */
111
+ export function fingerprintProbe(key: ProbeKey, r: ProbeResult): string {
112
+ // ok results always have a single fingerprint per probe — we don't track
113
+ // healthy variance.
114
+ if (r.status === 'ok') return `${key}:ok`
115
+
116
+ switch (key) {
117
+ case 'skills': {
118
+ // Fold across the dangling count and the listed names. The fact
119
+ // that "some skills dangle" is what matters; ten more or fewer
120
+ // doesn't reset the snooze.
121
+ if (/dangling/.test(r.detail)) return `${key}:${r.status}:dangling`
122
+ return `${key}:${r.status}:${normalizeDetail(r.detail)}`
123
+ }
124
+ case 'account': {
125
+ // Fold by status_kind: signed-in-but-expired vs not-signed-in vs
126
+ // token-expiring-soon. The literal detail includes the email and
127
+ // day-countdown — both vary incidentally.
128
+ const d = r.detail
129
+ if (/not signed in/i.test(d)) return `${key}:${r.status}:signed-out`
130
+ if (/expired/i.test(d)) return `${key}:${r.status}:token-expired`
131
+ if (/token \d+d/i.test(d)) return `${key}:${r.status}:token-expiring`
132
+ return `${key}:${r.status}:${normalizeDetail(d)}`
133
+ }
134
+ case 'agent': {
135
+ // Systemd state string is the right granularity: "service failed"
136
+ // vs "service activating" are different issues; the PID/uptime
137
+ // suffix that ok rows carry has already been excluded above.
138
+ const m = r.detail.match(/^service\s+([a-z-]+)/)
139
+ if (m) return `${key}:${r.status}:state=${m[1]}`
140
+ // Docker-mode "claude process not found" is its own bucket.
141
+ return `${key}:${r.status}:${normalizeDetail(r.detail)}`
142
+ }
143
+ default:
144
+ // broker / kernel / hindsight / quota / scheduler / gateway:
145
+ // literal detail is the right granularity — they're already low-
146
+ // cardinality strings the user can recognize.
147
+ return `${key}:${r.status}:${normalizeDetail(r.detail)}`
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Normalize a detail string for fingerprinting: lowercase, collapse
153
+ * whitespace, strip absolute paths down to their basename so a moving
154
+ * socket directory doesn't fragment the fingerprint.
155
+ */
156
+ function normalizeDetail(d: string): string {
157
+ return d.toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 120)
158
+ }
159
+
160
+ // ─── Diff (cache vs current probe results) ──────────────────────────────────
161
+
162
+ /**
163
+ * Compare current probe results against the cached entry for each probe
164
+ * and produce a per-probe verdict. Pure function — does not touch disk.
165
+ */
166
+ export function diffProbes(
167
+ probes: ProbeMap,
168
+ cache: BootIssueCacheFile,
169
+ opts: DiffOpts = {},
170
+ ): ProbeDiffMap {
171
+ const snoozeBoots = opts.snoozeBoots ?? DEFAULT_SNOOZE_BOOTS
172
+ const snoozeMs = opts.snoozeMs ?? DEFAULT_SNOOZE_MS
173
+ const now = opts.now ?? Date.now
174
+ const nowMs = now()
175
+
176
+ const out: ProbeDiffMap = {}
177
+ for (const [key, r] of Object.entries(probes) as [ProbeKey, ProbeResult | null | undefined][]) {
178
+ if (!r) continue
179
+ const prev = cache.probes[key]
180
+ const fp = fingerprintProbe(key, r)
181
+
182
+ if (r.status === 'ok') {
183
+ // Resolved iff the cache had a non-ok entry for this probe.
184
+ const resolved = prev != null && !prev.fingerprint.endsWith(':ok')
185
+ out[key] = {
186
+ fingerprint: fp,
187
+ resolved,
188
+ snoozed: false,
189
+ firstSighting: prev == null,
190
+ // No need to persist a freshly-ok probe — keeps the cache small.
191
+ nextEntry: null,
192
+ }
193
+ continue
194
+ }
195
+
196
+ // Degraded / fail path
197
+ let consecutiveBoots = 1
198
+ let firstSeenMs = nowMs
199
+ if (prev != null && prev.fingerprint === fp) {
200
+ consecutiveBoots = prev.consecutiveBoots + 1
201
+ firstSeenMs = prev.firstSeenMs
202
+ }
203
+ const ageMs = nowMs - firstSeenMs
204
+ const snoozed =
205
+ consecutiveBoots > snoozeBoots ||
206
+ ageMs >= snoozeMs
207
+
208
+ out[key] = {
209
+ fingerprint: fp,
210
+ resolved: false,
211
+ snoozed,
212
+ firstSighting: consecutiveBoots === 1,
213
+ nextEntry: {
214
+ fingerprint: fp,
215
+ consecutiveBoots,
216
+ firstSeenMs,
217
+ lastSeenMs: nowMs,
218
+ },
219
+ }
220
+ }
221
+ return out
222
+ }
223
+
224
+ // ─── Persistence ────────────────────────────────────────────────────────────
225
+
226
+ export const EMPTY_CACHE: BootIssueCacheFile = { schema: 1, probes: {} }
227
+
228
+ /**
229
+ * Load the cache from `path`. Returns an empty cache on:
230
+ * - file missing
231
+ * - JSON parse error (file is renamed aside as `<path>.corrupt-<ts>`)
232
+ * - schema mismatch
233
+ *
234
+ * Entries older than GC_AGE_MS are dropped on load — keeps the file
235
+ * from growing unbounded across years of restarts.
236
+ */
237
+ export function loadCache(path: string, now: () => number = Date.now): BootIssueCacheFile {
238
+ if (!existsSync(path)) return { ...EMPTY_CACHE, probes: {} }
239
+ let raw: string
240
+ try {
241
+ raw = readFileSync(path, 'utf-8')
242
+ } catch {
243
+ return { ...EMPTY_CACHE, probes: {} }
244
+ }
245
+ let parsed: unknown
246
+ try {
247
+ parsed = JSON.parse(raw)
248
+ } catch {
249
+ // Corrupt — preserve for forensics, return empty.
250
+ try {
251
+ renameSync(path, `${path}.corrupt-${now()}`)
252
+ } catch {
253
+ // best-effort
254
+ }
255
+ return { ...EMPTY_CACHE, probes: {} }
256
+ }
257
+ const obj = parsed as Partial<BootIssueCacheFile>
258
+ if (!obj || obj.schema !== 1 || typeof obj.probes !== 'object' || obj.probes == null) {
259
+ return { ...EMPTY_CACHE, probes: {} }
260
+ }
261
+ // GC ancient entries.
262
+ const cutoff = now() - GC_AGE_MS
263
+ const probes: Partial<Record<ProbeKey, BootIssueCacheEntry>> = {}
264
+ for (const [k, v] of Object.entries(obj.probes) as [ProbeKey, BootIssueCacheEntry | undefined][]) {
265
+ if (!v) continue
266
+ if (typeof v.lastSeenMs !== 'number') continue
267
+ if (v.lastSeenMs < cutoff) continue
268
+ probes[k] = v
269
+ }
270
+ return { schema: 1, probes }
271
+ }
272
+
273
+ /**
274
+ * Apply a diff back to the cache and persist atomically. Entries with
275
+ * `nextEntry: null` are removed from the cache (probe is now ok). Other
276
+ * entries are upserted.
277
+ *
278
+ * Writes go via `<path>.tmp` + rename so a crash mid-write can't leave
279
+ * partial JSON on disk.
280
+ */
281
+ export function applyAndSave(
282
+ path: string,
283
+ cache: BootIssueCacheFile,
284
+ diff: ProbeDiffMap,
285
+ ): BootIssueCacheFile {
286
+ const next: BootIssueCacheFile = {
287
+ schema: 1,
288
+ probes: { ...cache.probes },
289
+ }
290
+ for (const [k, d] of Object.entries(diff) as [ProbeKey, ProbeDiffResult][]) {
291
+ if (d.nextEntry == null) {
292
+ delete next.probes[k]
293
+ } else {
294
+ next.probes[k] = d.nextEntry
295
+ }
296
+ }
297
+ try {
298
+ mkdirSync(dirname(path), { recursive: true })
299
+ const tmp = `${path}.tmp`
300
+ writeFileSync(tmp, JSON.stringify(next), { mode: 0o600 })
301
+ renameSync(tmp, path)
302
+ } catch {
303
+ // Non-fatal: the cache is best-effort. Suppression on this boot
304
+ // still applied from the in-memory diff; persistence will retry
305
+ // on the next boot.
306
+ }
307
+ return next
308
+ }