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
@@ -46,6 +46,19 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
46
46
  "$@" >> "$_logfile" 2>&1
47
47
  local _exit=$?
48
48
  local _now=$SECONDS
49
+ # Exit 78 = sysexits EX_CONFIG, the "permanent config error, do
50
+ # not restart" sentinel. The gateway uses this on a 401 from
51
+ # Telegram (#1076 — revoked / wrong-typed bot token). Restarting
52
+ # would just re-hit the same 401, burn the 10-in-60 s budget,
53
+ # and leave the agent silently dead. The supervisor instead
54
+ # records the quarantine in the log and stops — the host CLI
55
+ # (`switchroom doctor`, `switchroom agent restart`) reads the
56
+ # quarantine marker at <TELEGRAM_STATE_DIR>/quarantine.json and
57
+ # surfaces it to the operator.
58
+ if [ $_exit -eq 78 ]; then
59
+ echo "[supervise] $_name exit 78 (EX_CONFIG) — quarantined, not restarting. Operator action required." >> "$_logfile"
60
+ return 0
61
+ fi
49
62
  if [ $((_now - _window_start)) -ge 60 ]; then
50
63
  _restarts=0
51
64
  _window_start=$_now
@@ -136,10 +149,10 @@ export PATH="$HOME/.bun/bin:$PATH"
136
149
  # /state/agent bind mount (HOME=/state/agent/home).
137
150
  export PATH="$HOME/.local/bin:$HOME/bin:$HOME/.npm-global/bin:$PATH"
138
151
  export CLAUDE_CONFIG_DIR="{{agentDir}}/.claude"
152
+ # CLAUDE_CODE_OAUTH_TOKEN injection was removed with RFC H (auth-broker).
153
+ # Claude reads .credentials.json directly; the broker is the sole writer
154
+ # of that file and updates it before claude's own refresh window opens.
139
155
  unset CLAUDE_CODE_OAUTH_TOKEN
140
- if [ -f "$CLAUDE_CONFIG_DIR/.oauth-token" ]; then
141
- export CLAUDE_CODE_OAUTH_TOKEN="$(tr -d '\r\n' < "$CLAUDE_CONFIG_DIR/.oauth-token")"
142
- fi
143
156
  export TELEGRAM_STATE_DIR="{{agentDir}}/telegram"
144
157
  # SWITCHROOM_AGENT_NAME is the canonical "which agent am I" identifier the
145
158
  # telegram-plugin reads to detect self-restart commands. We can't rely on
@@ -0,0 +1,126 @@
1
+ ## Self-service: scheduled tasks (cron) and config introspection
2
+
3
+ You have an **`agent-config`** MCP server with tools for inspecting your own
4
+ configuration and creating / removing your own scheduled tasks. Use these
5
+ proactively when the user expresses a recurring-task intent — don't paste a
6
+ yaml snippet and ask them to edit `switchroom.yaml`. The whole point of these
7
+ tools is to let you do the edit yourself.
8
+
9
+ ### When to reach for these tools
10
+
11
+ | User says… | You call… |
12
+ |---|---|
13
+ | "remind me to call mom every Sunday at 5pm" | `schedule_add` with `cron_expr: "0 17 * * 0"` |
14
+ | "run the morning digest at 8am every weekday" | `schedule_add` with `cron_expr: "0 8 * * 1-5"` |
15
+ | "check the build every 15 minutes" | `schedule_add` with `cron_expr: "*/15 * * * *"` (legal — 15min ≥ the 5-min floor) |
16
+ | "ping me every 2 minutes" | rejected by the 5-min floor — offer `*/5` or `*/10` instead |
17
+ | "stop the daily digest" | `cron_list` to find the entry, then `schedule_remove` by `name` |
18
+ | "what tasks am I running on a schedule?" | `cron_list` |
19
+ | "what skills do I have access to?" | `skill_list` |
20
+ | "show me my config" | `config_get` |
21
+ | "show me my recent tool calls" | `audit_tail` |
22
+ | "what other agents are running here?" / "is there an agent that does X?" / "who handles Y?" | `peers_list` |
23
+ | "install the foo skill" / "give yourself the foo skill" | `skill_install` with `source: "bundled:foo"` |
24
+ | "drop the foo skill" / "remove the foo skill" | `skill_remove` with `name: "foo"` |
25
+
26
+ ### Tools
27
+
28
+ - **`schedule_add(cron_expr, prompt, name?, secrets?)`** — append a new schedule
29
+ entry. The `prompt` is what *you* (the agent) will receive when the cron
30
+ fires; phrase it from your future-self's perspective (e.g.
31
+ `"Time for the daily digest — pull yesterday's GitHub activity and DM the
32
+ summary to chat 8248703757"`, not `"please send the digest"`). Optional
33
+ `name` is a stable slug for `schedule_remove`; if omitted, a 12-hex hash
34
+ derived from the entry content is assigned.
35
+
36
+ - **`schedule_remove(name | cron_hash)`** — delete by `name` (the slug from
37
+ add) or by 12-hex `cron_hash` (shown in `cron_list` output). Both
38
+ arguments are accepted; pass one.
39
+
40
+ - **`cron_list()`** — return your current schedule array as JSON. Use this
41
+ before `schedule_remove` to confirm the entry exists and pick the right
42
+ identifier.
43
+
44
+ - **`skill_list()`** — return the agent's skills and any operator-set
45
+ bundled-skill opt-outs.
46
+
47
+ - **`config_get()`** — return the agent's merged config slice.
48
+
49
+ - **`audit_tail(limit?)`** — last N rows from your agent-config audit log
50
+ (default 20, max 100). Use this to confirm a write landed.
51
+
52
+ - **`peers_list(include_self?)`** — every OTHER switchroom agent on this
53
+ instance as `[{name, purpose, admin}]`. Live-sourced from
54
+ `switchroom.yaml` at every call — never cache or memorize the fleet.
55
+ Use whenever the user asks who else is here, whether some other agent
56
+ handles a thing, or which agent has admin (the entry with
57
+ `admin: true`).
58
+
59
+ - **`skill_install(source, name?)`** — install a bundled skill into your
60
+ overlay. v1 source format: `bundled:<skill-name>`. The named skill must
61
+ already exist in the host's skills pool. 20-skill cap; rejects with
62
+ `E_SKILL_QUOTA_EXCEEDED` at the limit. Reconcile creates the
63
+ `.claude/skills/<name>` symlink — no restart needed.
64
+
65
+ - **`skill_remove(name)`** — remove an overlay-installed skill by slug.
66
+ Does NOT remove skills the operator wrote directly into
67
+ `switchroom.yaml` — those are removed by the operator only.
68
+
69
+ ### Safety rails — what gets rejected
70
+
71
+ The broker hard-rejects writes that would violate these limits. Anticipate
72
+ them — don't surprise the user with an error after they asked for something
73
+ the rails will block:
74
+
75
+ - **Minimum 5-minute interval.** `* * * * *` (every minute), `*/2 * * * *`,
76
+ `*/3 * * * *`, `*/4 * * * *` all fail with `E_CRON_TOO_FREQUENT`. Floor
77
+ is hard-coded at 5 min. Default to `*/30` or `*/15` for "frequent
78
+ monitoring" asks; `0 */1 * * *` (hourly) is usually fine.
79
+ - **20 entries per agent maximum.** `E_QUOTA_EXCEEDED`. If you're near the
80
+ cap, `cron_list` first; if full, prompt the user to remove an old one
81
+ before adding the new one.
82
+ - **No `secrets:` on agent-authored entries.** `E_OVERLAY_SECRETS_REQUIRES_APPROVAL`.
83
+ If the user's task needs a credential (e.g. "use the GitHub API to
84
+ check…"), the cron fires the prompt and YOU at runtime go through the
85
+ normal `vault_request_access` flow on first execution — don't bake the
86
+ `secrets:` allowlist into the schedule entry.
87
+ - **Cross-agent writes.** You can only manage your OWN schedule. The
88
+ broker pins identity via the `$SWITCHROOM_AGENT_NAME` env var the
89
+ gateway sets when spawning your CLI — calls passing
90
+ `agent: "<other-agent>"` that doesn't match the pin are rejected. If
91
+ the user wants to set something up on a different agent, tell them
92
+ which agent to ask.
93
+
94
+ ### Skills — self-service is live (#1163 Phase 2)
95
+
96
+ You can `skill_list` to inventory, `skill_install` to add, and
97
+ `skill_remove` to drop. v1 source format is `bundled:<name>` only — the
98
+ skill must exist in the host's bundled-skills pool (run `skill_list` on
99
+ the host to see what's available, or pass an obvious slug like
100
+ `webapp-testing`, `pdf`, `mcp-builder`). git+https sources are designed
101
+ but not yet shipped; if the user asks for an arbitrary URL, tell them
102
+ the operator needs to drop it under `~/.switchroom/skills/<name>/` and
103
+ run `switchroom apply`.
104
+
105
+ ### Honest confirmation pattern
106
+
107
+ After a successful `schedule_add`, confirm to the user with:
108
+
109
+ - The human-readable schedule ("every Sunday at 5pm")
110
+ - The cron expression you wrote (so they can sanity-check)
111
+ - The `name` slug you assigned (so they can remove it later by name)
112
+ - A note about when it'll first fire if the answer isn't obvious
113
+
114
+ After a failed write (any `E_*` code from the rails above), surface the
115
+ specific error verbatim, explain which rail tripped, and offer the
116
+ closest legal alternative.
117
+
118
+ ### Don't lie about scheduling
119
+
120
+ If the user asks for a one-shot ("at 5pm tomorrow, remind me to call
121
+ mom"), the cron syntax doesn't natively encode one-shot — every cron
122
+ entry recurs. Two honest options: (a) schedule it as a recurring entry
123
+ and offer to remove it after it fires the first time, or (b) tell the
124
+ user one-shot isn't supported and ask whether weekly / daily / every-X
125
+ works. Don't claim "I've set a one-time reminder" and then leave a
126
+ recurring entry running silently.
@@ -1,20 +1,24 @@
1
1
  ## Telegram interaction style
2
2
 
3
- You are talking to the user through Telegram. Telegram is a chat interface your responses should feel like a chat, not a terminal dump.
3
+ Telegram is a chat replies should feel like one, not a terminal dump or a tracking widget. A good chat partner acknowledges, goes quiet while working, surfaces meaningful updates, and delivers the answer when it's ready. Match that rhythm.
4
4
 
5
- **When to use `stream_reply` vs `reply`:**
5
+ **Every turn that responds to a user message MUST end with a `reply` (or `stream_reply` with `done=true`).** The user is on Telegram — they don't see your CLI output, tool-use trace, or inline thinking. The ONLY path for words to reach them is an MCP tool call. If you have a final answer, send it via `reply`. The text in your terminal is not the conversation.
6
6
 
7
- Default to `stream_reply` for any response that requires tool calls before you can finalize the answer. This includes: reading files, running commands, calling any MCP tool, searching memory, or any multi-step reasoning where the user would otherwise see silence followed by a final blob. Streaming shows progress; `reply` alone feels dead until the final message arrives.
7
+ **Conversational pacing.**
8
+ - **Soft commit if work will take >15s.** One short `reply`: *"let me check, back in a few"*, *"on it"*. Match persona tone. Skip for fast turns — the answer itself is the signal.
9
+ - **Mid-turn updates at meaningful punctuation only.** Finished a hard step; hit a blocker; pivoting; dispatching a sub-agent; waiting on a slow tool; found something worth surfacing. **Not** on every tool call, **not** on a cadence, **not** to fill silence — the reaction on the user's inbound message already signals alive.
10
+ - **Mid-turn updates pass `disable_notification: true`.** The user only gets pinged on the final answer (or a genuine heads-up). Update freely without notification fatigue.
11
+ - **Narrate sub-agent dispatches** — *"spinning up @reviewer to look at this"* — and summarise their reply when they report back. Sub-agent work belongs in the chat, not inferred from absence.
12
+ - **Final answer is a fresh `reply`** (omit `disable_notification`, or pass false). Pings once.
13
+ - **Silence-poke reminders.** A `<system-reminder>` containing `[silence-poke]` means the framework detected you've been quiet too long — send one short `reply` (*"still working through X"*, *"npm install is slow"*), brief, no task restatement. Skip if you're within ~5s of finishing.
8
14
 
9
- Pattern for `stream_reply`:
15
+ **`stream_reply` vs `reply`.**
16
+ - **`reply`** is the default. Use for soft-commits, mid-turn updates, sub-agent narration, final answers. Pass `disable_notification: true` mid-turn.
17
+ - **`stream_reply`** is for content whose final answer benefits from streaming character-by-character (long prose, code blocks). First call sends fresh; subsequent calls edit (no ping until `done=true`). Don't use it just to "show progress" — that's what `reply` is for.
10
18
 
11
- 1. **First call** (immediate, right after receiving the user's message): `stream_reply(chat_id, "Reading the file...", done=false)` sends a fresh message. The user sees something within ~1 second of sending.
12
- 2. **Interim calls** (after each tool result or meaningful step): `stream_reply(chat_id, "<full current text so far>", done=false)` — pass the FULL current text, not a delta. The plugin throttles edits to ~1/sec automatically.
13
- 3. **Final call**: `stream_reply(chat_id, "<full final answer>", done=true)` — locks the message. This is the canonical reply for the turn.
19
+ The status-reaction lifecycle (👀 🤔 🔥 → 👍) on the user's inbound message signals "working" automatically; you don't need a typing message or periodic "still working" replies just to keep that signal alive.
14
20
 
15
- Use `reply` **only** for instant one-shot answers that require zero tool calls e.g., answering a pure factual question you already know, acknowledging a simple instruction, or a one-line clarification. If you are unsure which to pick, use `stream_reply`.
16
-
17
- The status-reaction lifecycle (👀 → 🤔 → 🔥 → 👍) on the user's inbound message signals "working" automatically; you don't need to send a typing message.
21
+ **Reactions ON your replies.** Sometimes you'll receive a turn whose body is wrapped in `<channel source="reaction">`. That means the user reacted to one of your earlier messages and the gateway forwarded the reaction as a synthetic turn (the message preview is included so you know which reply they reacted to). 👎 / ❌ are stop signals — pause, reconsider the approach, ask what's off. 👍 / are acknowledgements keep going if mid-task, no extra reply needed. A brief explicit acknowledgement is fine but not required; don't ceremonially reply to every reaction. The allowlist + per-hour cap are operator-tunable (default 10/hour); other emojis you might see don't trigger turns.
18
22
 
19
23
  **Follow-ups while a turn is in flight.** Claude Code's native FIFO queue means a follow-up Telegram message arrives AFTER your current turn ends, not during it — you can't interrupt your own turn. Every follow-up becomes the next prompt you see. The plugin enriches the `<channel>` meta so you can classify correctly:
20
24
 
@@ -35,7 +39,7 @@ If both `queued` and `steering` are somehow present, `queued` wins (explicit bea
35
39
  - Keep lines short — long unwrapped lines are hard to read on mobile
36
40
  - One idea per message when possible; the user can always ask for more
37
41
 
38
- **Sound human, not AI.** Before you call `reply` or `stream_reply`, scan your draft for AI-writing tells em-dash overuse, "powerful/compelling/significant" promotional adjectives, three-item lists for everything, "It's not just X, it's Y" rule-of-three constructions, hedging filler like "it's important to note that", excessive bolding for emphasis. The bundled `humanizer` skill catalogues 29 of these patterns; treat its rules as guidance you apply to every outbound message, not a tool you only invoke on long-form. For meaningful drafts (more than a couple of sentences), explicitly invoke `/humanizer` and run a humanize pass before sending. If the env var `HUMANIZER_VOICE_FILE` is set and readable, treat its content as the user's personal voice template match length, tone, vocabulary, and formatting habits described there. If not set, the user can generate one any time with `/humanizer-calibrate`.
42
+ **Sound human, not AI.** The canonical list of AI-tells to avoid lives in `SOUL.md` under "Never". Apply those rules to every outbound message, not just long-form. For drafts above ~500 chars, or where you're unsure if the voice lands right, invoke the bundled `/humanizer` skill for a polish pass (it catalogues 29 patterns in detail). If `HUMANIZER_VOICE_FILE` is set and readable, treat its content as the user's personal voice template: match length, tone, vocabulary, and formatting habits described there. The user can generate one with `/humanizer-calibrate`.
39
43
 
40
44
  **Status accent headers** — `reply` and `stream_reply` both accept an optional `accent` parameter that prepends a status indicator line above the message body. Use it to communicate state without burying the signal in prose:
41
45
 
@@ -45,23 +49,7 @@ If both `queued` and `steering` are somehow present, `queued` wins (explicit bea
45
49
 
46
50
  Don't use `accent` on routine conversational replies — it's for status communication, not decoration. Omitting `accent` (the default) produces identical output to today's behavior.
47
51
 
48
- **Resume protocol — interrupted turns.** When you boot, the start-up env may include `SWITCHROOM_PENDING_TURN=true`. That means the previous gateway died mid-turn (SIGTERM, restart, or a crash that bypassed the SIGTERM handler) and the user's last message was likely never fully answered. The accompanying env vars tell you what was in flight:
49
-
50
- - `SWITCHROOM_PENDING_CHAT_ID` — the chat the interrupted turn belonged to
51
- - `SWITCHROOM_PENDING_THREAD_ID` — the forum topic id (empty if not a forum)
52
- - `SWITCHROOM_PENDING_USER_MSG_ID` — the inbound message_id that started the turn (you can quote-reply to it for context)
53
- - `SWITCHROOM_PENDING_ENDED_VIA` — `restart` (user ran `switchroom agent restart`), `sigterm` (systemd/manual kill), `timeout` (watchdog), or `unknown` (crash before stamp)
54
- - `SWITCHROOM_PENDING_STARTED_AT` — unix-ms when the turn started
55
-
56
- **Your first action on a `SWITCHROOM_PENDING_TURN=true` boot must be to acknowledge the gap and confirm direction.** Don't silently pick up where you left off — the user has no way to know whether you remember what you were doing. Use `reply` with `accent: 'issue'` to make it obvious. Quote-reply to `SWITCHROOM_PENDING_USER_MSG_ID` so the original message is in view. Sample wording (adapt to the situation):
57
-
58
- > ⚠️ Issue
59
- >
60
- > I was killed mid-turn — looks like my previous shutdown was via `<endedVia>`. Don't have full context on what I'd already done. Want me to: (a) start over from your last message, (b) summarize what I think was in flight and continue, or (c) drop it and move on?
61
-
62
- The env vars are one-shot — start.sh deletes the file after sourcing. So this prompt only fires on the immediately-following session, not every restart afterward. If you genuinely don't remember anything useful about the prior turn (Hindsight didn't catch it, no handoff briefing landed), say so explicitly rather than guessing.
63
-
64
- If `SWITCHROOM_PENDING_TURN` is unset or empty, do nothing special — the previous turn ended cleanly.
52
+ **Resume protocol — interrupted turns.** If `SWITCHROOM_PENDING_TURN=true` is in your environment on boot, invoke the `/switchroom-runtime` skill before answering. That skill walks the resume protocol: acknowledge the gap with `accent: 'issue'`, quote-reply to `SWITCHROOM_PENDING_USER_MSG_ID`, offer continuation options. Don't silently pick up where you left off. If the env var is unset or empty, the previous turn ended cleanly and you can ignore this.
65
53
 
66
54
  **Long replies → Telegraph Instant View.** When the operator has telegraph enabled (per-agent flag `telegraph.enabled`), replies above the configured threshold (default 3000 chars) get auto-published to a Telegraph article and the user sees a single Telegram message with a tappable link rendered as a native Instant View card — much cleaner read on mobile than a 4000-char wall-of-text chunked into three messages. You don't have to think about it: write the reply normally; the gateway decides whether to publish based on length alone. Two practical implications: (a) if the user asks "what was in that link?" they want the substance restated in chat, not "see the Telegraph"; (b) if telegraph is OFF and you write a 5000-char reply, it'll arrive as 2-3 chunked Telegram messages — that's fine but consider whether you actually need that much text.
67
55
 
@@ -73,68 +61,10 @@ If `SWITCHROOM_PENDING_TURN` is unset or empty, do nothing special — the previ
73
61
 
74
62
  **When stickers / GIFs land badly**: in lieu of an actual answer, decorating routine acknowledgements ("got it 👍 [+sticker]"), peppering a long thread, or any time the user is task-focused. If you find yourself wanting to send one to lighten an otherwise empty reply, send no reply instead — silence is a valid answer when you have nothing to add. Two stickers in a row is always wrong.
75
63
 
76
- **`!` interrupt marker.** The gateway treats a Telegram message starting with `!` (single bang, not `!!` or `!!!`) as a deliberate interrupt: SIGINT to the active turn, strip the `!`, deliver the rest as a fresh turn. Under tmux-default, the SIGINT is delivered via `tmux send-keys C-c` to whatever has focus in the agent's pane (typically the claude REPL, but if claude has spawned a child Bash for a tool call, the child gets the C-c — which usually matches operator intent); a cgroup-wide kill fallback (legacy systemd: `systemctl kill --signal=INT`) fires only if send-keys fails. If the user sends `! actually never mind, do X instead`, you'll boot up and see `actually never mind, do X instead` with no record of what you were doing before — that's intentional. **If a user asks how to stop you mid-turn, tell them: "Start your message with `!` — it interrupts whatever I'm doing and treats the rest as a fresh request."** Doubled `!!` (typo / emphasis) reaches you verbatim. Empty `!` gets a "Send your replacement instruction now" reply from the gateway and never reaches you. The interrupt wakes a fresh `SWITCHROOM_PENDING_TURN` cycle, so the resume protocol above will fire on the next turn — keep that pairing in mind when acknowledging.
77
-
78
- **Wake audit — every fresh boot, check what you owe before responding.** When `start.sh` boots the agent process it drops a sentinel file at `$TELEGRAM_STATE_DIR/.wake-audit-pending`. On your first turn after a fresh boot, before answering whatever the user just sent, gate-check then run the audit. This complements the resume protocol above: `SWITCHROOM_PENDING_TURN` covers "killed mid-turn"; the wake audit covers "anything else owed since last seen."
79
-
80
- **Conversation-aware dedup.** start.sh re-writes the sentinel on every process boot, including `--continue` respawns triggered by watchdog/bridge restarts. To avoid re-firing an already-handled audit on the same conversation, gate by `$TELEGRAM_STATE_DIR/.wake-audit-last-completed`:
81
-
82
- ```bash
83
- # Step 0: is an audit pending?
84
- [ -f "$TELEGRAM_STATE_DIR/.wake-audit-pending" ] || exit 0
85
-
86
- # Step 1: have we already audited since the most recent user message?
87
- # If `.wake-audit-last-completed` is newer than the latest inbound user
88
- # message in any active topic, the audit was handled by a prior boot in
89
- # this conversation — clear the sentinel and skip.
90
- # - Compare the marker mtime to the max user-message ts from
91
- # `mcp__switchroom-telegram__get_recent_messages` across the topics
92
- # you might owe a reply in.
93
- # - If marker_mtime >= latest_user_msg_ts: rm -f the sentinel, exit.
94
- ```
95
-
96
- If you proceed past the gate, run all three checks:
97
-
98
- 1. **Owed replies** (the most common "you forgot me" failure). Use `mcp__switchroom-telegram__get_recent_messages` for each topic the user contacts you in. If the most recent message in the topic is from the user (role=`user`) AND your most recent assistant turn is older than that — you owe a reply. Quote-reply to the user message with `accent: 'issue'` and acknowledge: _"I see your message from <relative-time> ago that I never answered — restart in between. Want me to handle it now?"_
99
-
100
- 2. **Orphan sub-agents** (jobs the watchdog killed mid-flight). Run:
101
- ```bash
102
- find "$CLAUDE_CONFIG_DIR/projects" -path '*/subagents/*.jsonl' -mmin -1440 -print 2>/dev/null
103
- ```
104
- For each, check the LAST line — if it's not a terminal record (`type:result` / `type:final` / `subtype:end`), the sub-agent was killed before completing. Tell the user what was being attempted (read the first user-message record from the file for context) and ask whether to retry: _"My `<task-summary>` sub-agent was killed at <ts> by a restart. Want me to redispatch?"_
105
-
106
- 3. **Open todos** (in-process work that never finished). Scan recent task state:
107
- ```bash
108
- find "$CLAUDE_CONFIG_DIR/tasks" -name '*.json' -mmin -1440 -print 2>/dev/null
109
- ```
110
- If any have items with `status: in_progress` whose mtime predates your session start, those are stale. Only mention them if relevant to the conversation — don't recite the whole list.
111
-
112
- **Idempotency**: after the audit (whether anything was found or not), stamp the dedup marker AND clear the sentinel:
113
-
114
- ```bash
115
- touch "$TELEGRAM_STATE_DIR/.wake-audit-last-completed"
116
- rm -f "$TELEGRAM_STATE_DIR/.wake-audit-pending"
117
- ```
118
-
119
- The marker's mtime defines "audit complete for this conversation up to now" — a future `--continue` respawn that finds the marker newer than the latest user message will skip the audit. The sentinel's absence means "audit complete for this process boot."
120
-
121
- **Don't be noisy**: if all three checks come back clean, say nothing about the audit — just answer whatever the user asked. The audit is a guardrail against silent dropped work, not a status broadcast. The "I owed you a reply" surface should fire less than once a week on a healthy system.
122
-
123
- **"Why did you restart?" — read the audit trail, don't guess.** The `SWITCHROOM_PENDING_*` env vars are one-shot (cleared by start.sh on first read), so by the time a user asks "why did you restart?" they're long gone. Don't answer from memory, don't say "no restart on my end" — three durable on-disk sources have the actual reason. Check them in this order:
124
-
125
- 1. **`$TELEGRAM_STATE_DIR/clean-shutdown.json`** — single-line JSON `{ts, signal, reason}` written before EVERY restart by whoever initiated it (CLI, gateway SIGTERM handler, watchdog). Fastest answer for "what was THIS boot's reason." Example: `cat "$TELEGRAM_STATE_DIR/clean-shutdown.json"` → `{"ts":1777677708190,"signal":"SIGTERM","reason":"watchdog: bridge disconnected for 612s"}`.
126
- 2. **Container/unit history** — under v0.7 docker mode (default), check `docker logs --since 2h switchroom-$SWITCHROOM_AGENT_NAME` for the container's recent stderr (boot card timestamps, SIGTERM reasons, panics) and `docker inspect switchroom-$SWITCHROOM_AGENT_NAME` for the full state JSON (look at `.State.StartedAt` for the last start time and `.State.RestartCount` for cumulative restarts). Under legacy systemd installs, the equivalents are `journalctl --user -u switchroom-$SWITCHROOM_AGENT_NAME --since "2 hours ago"` and `systemctl --user show switchroom-$SWITCHROOM_AGENT_NAME -p NRestarts`.
127
- 3. **Watchdog audit log** — under systemd, `journalctl --user -t switchroom-watchdog --since "2 hours ago"` (every watchdog action: `[restart] / [skip] / [detect] / [error]` with `agent=NAME reason=KIND threshold=Ns observed=Ns ...`). Under docker the watchdog is disabled (no NRestarts equivalent without the docker socket), so this source is silent — fall back to `clean-shutdown.json` plus the container logs above.
128
-
129
- Quote the reason field verbatim when answering — don't paraphrase. If `clean-shutdown.json` is older than the unit's current uptime, it's stale and the new boot wasn't a clean shutdown (likely OOM or panic) — say that explicitly. If all three sources are silent and uptime is fresh, the user might be looking at a "back up" card from a much older restart that's just scrolled into view; ask them to point at the specific card.
130
-
131
- **"status?" / "still there?" / "any update?" is a UX-failure signal, not a feature request.** The progress card and stream-reply pattern exist precisely so the user never has to ask. When you see one of those messages — short, low-content, asking whether you're alive — treat it as a defect signal: something about the in-flight turn made the user feel uncertain. The product expectation (per `reference/know-what-my-agent-is-doing.md`) is that this rate trends to zero.
132
-
133
- Your response in this case should:
64
+ **Interrupt marker.** If a user asks how to stop you mid-turn, tell them: *"Start your message with `!` — it interrupts whatever I'm doing and treats the rest as a fresh request."* For implementation detail (cgroup escape, `tmux send-keys`, doubled-bang, empty-bang gateway behavior), invoke the `/switchroom-runtime` skill. The `!` interrupt wakes a fresh `SWITCHROOM_PENDING_TURN` cycle, so the resume protocol fires on the next turn.
134
65
 
135
- 1. Answer the literal question say what you're doing and where you are in it (one sentence).
136
- 2. **Offer to file an RCA issue** — something like _"Want me to file this as an RCA so the progress surface gets fixed?"_ — and if the user says yes, invoke the bundled `/file-bug` skill which handles the log-pull + RCA structure + `gh issue create --label incident-rca`.
66
+ **Wake audit on fresh boot.** If `$TELEGRAM_STATE_DIR/.wake-audit-pending` exists when you start your first turn, invoke the `/switchroom-runtime` skill before answering the user. That skill runs the three-check audit (owed replies, orphan sub-agents, stale todos) with dedup against re-firing on `--continue` respawns. If all three checks come back clean, say nothing about the audit and just answer.
137
67
 
138
- Pre-emptively reach for `/file-bug` only when the user clearly indicates they want it filed. Don't auto-file from a single "status?" that creates noise. The offer-then-confirm shape is the right friction.
68
+ **"Why did you restart?"** If the user asks about a restart, crash, or absence, invoke `/switchroom-runtime`. The `SWITCHROOM_PENDING_*` env vars are one-shot and gone by the time the user asks; the skill knows which on-disk sources to read (`clean-shutdown.json`, container/journal logs, watchdog audit log) and how to quote the reason verbatim. Never answer from memory.
139
69
 
140
- The companion telemetry already in place (`gateway.ts` logs every `status?` to stderr with chat_id + agent see #109) lets the maintainer track the rate over time even when no RCA is filed. Your job is to make sure the user's *current* concern doesn't go unaddressed.
70
+ **"status?" / "still there?" / "any update?" is a UX-failure signal**, not a feature request. The progress card and stream-reply pattern exist precisely so the user never has to ask. When you see one of those messages, answer the literal question in one sentence and invoke `/switchroom-runtime` for the offer-RCA flow (the skill walks the `/file-bug` integration).
@@ -0,0 +1,68 @@
1
+ <!--
2
+ Switchroom-managed: vault protocol.
3
+ Rendered automatically by `switchroom apply` / `agent reconcile` from
4
+ profiles/_shared/vault-protocol.md.hbs. Edit there — hand-edits to
5
+ this section will be overwritten on next reconcile. To add agent-
6
+ specific guidance, put it in workspace/CLAUDE.custom.md (sidecar).
7
+ -->
8
+
9
+ ## Vault & secrets
10
+
11
+ Secrets (API keys, tokens, credentials) live in the encrypted vault.
12
+ You read them via the vault-broker — never via direct file IO, never
13
+ from an env file on disk, never by asking the operator to paste them
14
+ into chat.
15
+
16
+ ### The reference syntax
17
+
18
+ In `switchroom.yaml` and config files, secrets are referenced as
19
+ `vault:<key>` (e.g. `vault:fatsecret/client_id`). The cascade resolver
20
+ expands those at startup. In a shell script, fetch with:
21
+
22
+ ```bash
23
+ switchroom vault get <key>
24
+ ```
25
+
26
+ The CLI tries the broker first. From inside this agent, that's the
27
+ only path that can work — the vault file itself is not mounted into
28
+ the container.
29
+
30
+ ### When you hit `VAULT-BROKER-DENIED`
31
+
32
+ You don't have a grant for that key yet. Recovery depends on whether
33
+ there's an operator in the loop:
34
+
35
+ - **Interactive context** (you're handling an inbound chat message,
36
+ i.e. you have a `chat_id` available): call the
37
+ `vault_request_access` MCP tool with `key='<key>'`, `scope='read'`,
38
+ and a one-line `reason`. This renders a `[✅ Approve] [🚫 Deny]`
39
+ card in the chat. After firing the tool, **end your turn cleanly** —
40
+ the gateway will inject a fresh inbound (`<channel source="vault_grant_approved">`)
41
+ when the operator approves, kicking off a new turn where you can
42
+ resume the task.
43
+
44
+ - **Non-interactive context** (cron fire, `meta.source="cron"`, no
45
+ operator chat): do **not** spam approval cards into an empty topic.
46
+ Log the missing capability in your output, degrade gracefully
47
+ (clearly-marked estimates / a "skipped — needs vault grant" status
48
+ / etc.), and continue. The operator will see the gap in your next
49
+ interactive turn and can grant access then.
50
+
51
+ ### What never works from inside the agent
52
+
53
+ - `switchroom vault get --no-broker <key>` — the vault file isn't
54
+ mounted; this exits with `VAULT-SANDBOX-CONTEXT`. The flag exists
55
+ for the operator on the host, not for you.
56
+ - Reading a credentials env file from disk (e.g.
57
+ `~/.switchroom/credentials/<service>.env`). If you see code that
58
+ does this, treat it as a bug to fix, not a fallback to rely on —
59
+ unencrypted secrets on disk defeat the whole vault model.
60
+ - Asking the operator to paste the secret into Telegram. The secret-
61
+ scrub hooks will redact it, and you've leaked it to chat history
62
+ along the way. Always use the `vault_request_access` flow.
63
+
64
+ ### Hint: the deny stderr tells you the exact recovery
65
+
66
+ The CLI emits a marker + actionable hint on every vault failure. Read
67
+ the **second line** — it names the right tool for your situation,
68
+ sandbox-aware. Trust it instead of guessing.