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
@@ -40,7 +40,7 @@ import {
40
40
  } from 'fs'
41
41
  import { basename, join } from 'path'
42
42
  import { homedir } from 'os'
43
- import { projectSubagentLine } from './session-tail.js'
43
+ import { projectSubagentLine, sanitizeCwdToProjectName } from './session-tail.js'
44
44
  import { sanitiseToolArg } from './fleet-state.js'
45
45
  import { escapeHtml, truncate } from './card-format.js'
46
46
  import { bumpSubagentActivity, recordSubagentStall, recordSubagentResume, recordSubagentEnd, reapStuckRunningRows } from './registry/subagents-schema.js'
@@ -83,8 +83,26 @@ export interface WorkerEntry {
83
83
  toolCount: number
84
84
  /** True once a stall notification has been sent (suppresses repeat). */
85
85
  stallNotified: boolean
86
+ /**
87
+ * Wall-clock ms when `stallNotified` flipped true. Null until then.
88
+ * Used by the post-stall terminal-synthesis path (RFC §Bug 6) to
89
+ * measure the post-stall window: when `now - stalledAt >=
90
+ * silentStallTerminalMs` the watcher synthesises a terminal
91
+ * transition for the entry. Workers whose JSONL never writes an
92
+ * explicit `sub_agent_turn_end` (e.g. background `Agent` dispatches
93
+ * in some Claude Code versions) would otherwise sit forever in
94
+ * `running` despite their real worker process having exited.
95
+ */
96
+ stalledAt: number | null
86
97
  /** True once a completion notification has been sent. */
87
98
  completionNotified: boolean
99
+ /**
100
+ * True once the post-stall terminal synthesis has fired so we don't
101
+ * re-synthesise on every poll tick after the silentStallTerminalMs
102
+ * window elapses. Paired with `stalledAt` — when synthesis runs it
103
+ * sets both `state='done'` and this flag.
104
+ */
105
+ stallTerminalSynthesised: boolean
88
106
  /** Short summary from last completed tool / narrative, for completion message. */
89
107
  lastSummaryLine: string
90
108
  /**
@@ -109,6 +127,16 @@ export interface SubagentWatcherConfig {
109
127
  * Used to derive `.claude/projects/<cwd>/` dirs to watch.
110
128
  */
111
129
  agentDir: string
130
+ /**
131
+ * Agent's working directory — used to compute the project-dir slug the
132
+ * watcher should restrict its enumeration to (Claude Code keys project
133
+ * dirs off the cwd at first launch via `sanitizeCwdToProjectName`).
134
+ * When omitted, the watcher walks every subdir of
135
+ * `<agentDir>/.claude/projects/` (legacy behaviour; see issue #1116
136
+ * for why this is unsafe — a foreign agent's stale project dir under
137
+ * an agent's home pollutes the watcher with phantom registrations).
138
+ */
139
+ agentCwd?: string
112
140
  /**
113
141
  * Send a fresh (non-edit) Telegram message. For stall / completion
114
142
  * state-transition notifications.
@@ -137,6 +165,16 @@ export interface SubagentWatcherConfig {
137
165
  * Both can be overridden for tests.
138
166
  */
139
167
  silentSynthesisStallThresholdMs?: number
168
+ /**
169
+ * RFC §Bug 6: how long after `stallNotified` fires the watcher waits
170
+ * before synthesising a terminal `sub_agent_turn_end` for the entry
171
+ * (ms). Default 300_000 (5 min) — sympathetic to legitimately-paused
172
+ * workers but tight enough that the progress card releases its
173
+ * deferred-completion gate well before the 30-min `maxIdleMs`
174
+ * ceiling. Set to a very large number (e.g. `Infinity`) to disable
175
+ * synthesis; tests use a tiny value to exercise the path.
176
+ */
177
+ silentStallTerminalMs?: number
140
178
  /**
141
179
  * Reaper TTL (ms): background rows in `status='running'` whose
142
180
  * `last_activity_at` (or `started_at` if liveness never wrote) is older
@@ -197,6 +235,21 @@ export interface SubagentWatcherConfig {
197
235
  * later in the same lifetime is detected (and reported) again.
198
236
  */
199
237
  onUnstall?: (agentId: string, description: string) => void
238
+ /**
239
+ * RFC §Bug 6: fires when the watcher synthesises a terminal transition
240
+ * for a stalled sub-agent (no explicit `sub_agent_turn_end` line in
241
+ * the JSONL after `silentStallTerminalMs` past the stall notification).
242
+ * Wired in gateway.ts to push a synthetic
243
+ * `{kind:'sub_agent_turn_end', agentId}` event into the progress
244
+ * driver so the pinned card can release its deferred-completion gate
245
+ * for the background dispatch.
246
+ *
247
+ * Idempotent: each sub-agent triggers this at most once per lifetime
248
+ * (guarded by `entry.stallTerminalSynthesised`). Fires *before* the
249
+ * existing `onFinish` callback so the driver-side state mutation
250
+ * lands first; the audit-log surface then sees a consistent fleet.
251
+ */
252
+ onStallTerminal?: (agentId: string, description: string) => void
200
253
  /**
201
254
  * Called exactly once per sub-agent when its watcher observes a terminal
202
255
  * transition (`done` or `failed`). Mirrors the existing `sub_agent_started`
@@ -257,6 +310,33 @@ const DEFAULT_STALL_THRESHOLD_MS = 60_000
257
310
  * before emitting their first event — the 60s active-loop threshold
258
311
  * misfires on those and freezes the card at ⚠. */
259
312
  const DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300_000
313
+ /**
314
+ * RFC §Bug 6 — post-stall terminal-synthesis window. 5min past the
315
+ * stall notification before the watcher synthesises a
316
+ * `sub_agent_turn_end` for the entry. Generous enough that a worker
317
+ * paused on an external dependency (operator unblocking, slow API)
318
+ * isn't reported done prematurely; tight enough that the pinned card's
319
+ * deferred-completion gate releases well before the 30-min `maxIdleMs`
320
+ * ceiling that closed-out cards used to wait on.
321
+ */
322
+ const DEFAULT_SILENT_STALL_TERMINAL_MS = 300_000
323
+
324
+ /**
325
+ * Resolve a threshold-knob env var (e.g.
326
+ * `SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS`) to a positive integer ms
327
+ * value. Returns null when unset, empty, or unparseable so the caller
328
+ * falls through to the compile-time default. Negative/zero/NaN values
329
+ * are treated as "invalid" rather than "disable" — a real "disable"
330
+ * needs an explicit config-arg, not an env override (don't let a
331
+ * stray `=0` silently kill the watcher's stall-detection in prod).
332
+ */
333
+ function parseEnvMs(varName: string): number | null {
334
+ const raw = process.env[varName]
335
+ if (raw == null || raw === '') return null
336
+ const n = Number(raw)
337
+ if (!Number.isFinite(n) || n <= 0) return null
338
+ return n
339
+ }
260
340
  const DEFAULT_REAPER_TTL_MS = 60 * 60_000 // 1 hour
261
341
  const DEFAULT_REAPER_INTERVAL_MS = 15 * 60_000 // 15 minutes
262
342
  /**
@@ -458,6 +538,11 @@ function readSubTail(
458
538
  // driver to clear its render-time badge.
459
539
  if (entry.stallNotified) {
460
540
  entry.stallNotified = false
541
+ // Clear the stall timestamp so a subsequent re-stall starts
542
+ // the post-stall terminal-synthesis clock from scratch
543
+ // (RFC §Bug 6). Without this, a stall→resume→stall sequence
544
+ // could prematurely synthesise terminal on the second stall.
545
+ entry.stalledAt = null
461
546
  if (db != null) {
462
547
  try {
463
548
  const rowRef = db
@@ -533,9 +618,36 @@ function readSubTail(
533
618
 
534
619
  export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWatcherHandle {
535
620
  const agentDir = config.agentDir
536
- const stallThresholdMs = config.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS
621
+ // Issue #1116: when agentCwd is supplied, restrict project-dir
622
+ // enumeration to the slug Claude Code would mint for that cwd.
623
+ // Foreign-slug shadow dirs (a sibling agent's stale project tree
624
+ // left over from a wayward CLAUDE_PROJECT_DIR or a past boot) are
625
+ // skipped — pre-#1116 they caused ENOENT log spam and false stalls.
626
+ // When agentCwd is null/undefined, fall back to the legacy walk-
627
+ // every-subdir behaviour (preserves tests that don't care about
628
+ // multi-slug isolation).
629
+ const expectedProjectSlug = config.agentCwd != null
630
+ ? sanitizeCwdToProjectName(config.agentCwd)
631
+ : null
632
+ // One-shot logging: warn the first time a foreign slug is observed
633
+ // so silent regressions are visible without re-running with debug.
634
+ const warnedForeignSlugs = new Set<string>()
635
+ // Threshold knobs resolve in this order: explicit config arg →
636
+ // env-var override → compile-time default. Env-vars exist so the
637
+ // UAT scenario (which times out at 120s) can compress the watcher's
638
+ // 60s-stall + 300s-synth window down to a few seconds without
639
+ // having to plumb config through every spinUp() caller. Production
640
+ // gateways don't set these — the defaults are tuned for live use.
641
+ const stallThresholdMs =
642
+ config.stallThresholdMs ?? parseEnvMs('SWITCHROOM_SUBAGENT_STALL_MS') ?? DEFAULT_STALL_THRESHOLD_MS
537
643
  const silentSynthesisStallThresholdMs =
538
- config.silentSynthesisStallThresholdMs ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS
644
+ config.silentSynthesisStallThresholdMs
645
+ ?? parseEnvMs('SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS')
646
+ ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS
647
+ const silentStallTerminalMs =
648
+ config.silentStallTerminalMs
649
+ ?? parseEnvMs('SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS')
650
+ ?? DEFAULT_SILENT_STALL_TERMINAL_MS
539
651
  const reaperTtlMs = config.reaperTtlMs ?? DEFAULT_REAPER_TTL_MS
540
652
  const reaperIntervalMs = config.reaperIntervalMs ?? DEFAULT_REAPER_INTERVAL_MS
541
653
  const rescanMs = config.rescanMs ?? DEFAULT_RESCAN_MS
@@ -595,6 +707,20 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
595
707
  * when they eventually report done — that transition is meaningful.
596
708
  */
597
709
  const historicalFiles = new Set<string>()
710
+ /**
711
+ * AgentIds that have transitioned to a terminal state and been swept
712
+ * out of `registry` by `cleanupTerminalAgent`. Issue #1116 (Bug B):
713
+ * the JSONL file outlives the registry entry — Claude Code leaves
714
+ * the file on disk after the sub-agent finishes. Without this guard,
715
+ * the next `rescanSubagentDirs` poll re-discovered the file, called
716
+ * `registerAgent`, the fresh entry read the terminal `turn_duration`
717
+ * line, and `maybySendStateTransition` fired a duplicate "Worker done"
718
+ * notification — looping forever every grace-window.
719
+ *
720
+ * `scanSubagentsDir` consults this set and treats re-discovered
721
+ * terminal JSONLs as a no-op.
722
+ */
723
+ const terminatedAgentIds = new Set<string>()
598
724
  /**
599
725
  * True while the initial boot scan is running. During this window every
600
726
  * newly discovered file is added to historicalFiles.
@@ -620,7 +746,9 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
620
746
  lastActivityAt: n,
621
747
  toolCount: 0,
622
748
  stallNotified: false,
749
+ stalledAt: null,
623
750
  completionNotified: false,
751
+ stallTerminalSynthesised: false,
624
752
  lastSummaryLine: '',
625
753
  lastTool: null,
626
754
  historical: isHistorical,
@@ -788,6 +916,10 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
788
916
  knownFiles.delete(entry.filePath)
789
917
  }
790
918
  registry.delete(agentId)
919
+ // Issue #1116 (Bug B): record that this agent has been fully
920
+ // processed so a rescan that rediscovers the still-present JSONL
921
+ // doesn't re-register and re-notify.
922
+ terminatedAgentIds.add(agentId)
791
923
  log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`)
792
924
  }
793
925
 
@@ -795,6 +927,9 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
795
927
 
796
928
  function checkStalls(): void {
797
929
  const n = nowFn()
930
+ // Pass 1: stall detection (existing behaviour). A running sub-agent
931
+ // with no JSONL growth for `threshold` ms transitions to "stalled"
932
+ // and notifies subscribers (badge on card, DB row update).
798
933
  for (const entry of registry.values()) {
799
934
  if (entry.state !== 'running') continue
800
935
  if (entry.historical) continue
@@ -812,6 +947,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
812
947
  : stallThresholdMs
813
948
  if (idleMs >= threshold) {
814
949
  entry.stallNotified = true
950
+ entry.stalledAt = n
815
951
  const desc = escapeHtml(truncate(entry.description, 80))
816
952
  const idleSec = Math.floor(idleMs / 1000)
817
953
  log?.(`subagent-watcher: stall detected for ${entry.agentId} (idle ${idleSec}s): ${desc}`)
@@ -842,6 +978,63 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
842
978
  }
843
979
  }
844
980
  }
981
+
982
+ // Pass 2 (RFC §Bug 6): post-stall terminal synthesis. Background
983
+ // `Agent` dispatches in some Claude Code versions write a JSONL
984
+ // that ends with the worker's last `sub_agent_tool_result` and
985
+ // never emits an explicit `system + turn_duration` line — so the
986
+ // canonical `sub_agent_turn_end` event never fires. Without
987
+ // synthesis the entry stays `running` until the 30-min
988
+ // `maxIdleMs` ceiling, and the pinned card's deferred-completion
989
+ // gate never releases.
990
+ //
991
+ // Wait `silentStallTerminalMs` past the stall notification before
992
+ // synthesising: a genuinely-paused worker (e.g. waiting on an
993
+ // external API the operator has to unblock) shouldn't be reported
994
+ // done immediately at the stall threshold.
995
+ for (const entry of registry.values()) {
996
+ if (entry.state !== 'running') continue
997
+ if (!entry.stallNotified) continue
998
+ if (entry.stallTerminalSynthesised) continue
999
+ if (entry.stalledAt == null) continue
1000
+ if (n - entry.stalledAt < silentStallTerminalMs) continue
1001
+ entry.stallTerminalSynthesised = true
1002
+ entry.state = 'done'
1003
+ const postStallSec = Math.floor((n - entry.stalledAt) / 1000)
1004
+ const totalIdleSec = Math.floor((n - entry.lastActivityAt) / 1000)
1005
+ log?.(`subagent-watcher: silent-stall terminal synthesis for ${entry.agentId} (stalled ${postStallSec}s post-notify, ${totalIdleSec}s total idle) — bg worker JSONL lacks turn_end; synthesising sub_agent_turn_end so deferred-completion gate releases`)
1006
+ // Persist completion to the registry DB so reaper / audit paths
1007
+ // see the same terminal state as the JSONL-driven path.
1008
+ if (db != null) {
1009
+ try {
1010
+ const rowRef = db
1011
+ .prepare('SELECT id FROM subagents WHERE jsonl_agent_id = ?')
1012
+ .get(entry.agentId) as { id: string } | null
1013
+ if (rowRef != null) {
1014
+ recordSubagentEnd(db, {
1015
+ id: rowRef.id,
1016
+ endedAt: n,
1017
+ status: 'completed',
1018
+ })
1019
+ }
1020
+ } catch (dbErr) {
1021
+ log?.(`subagent-watcher: stall-synth DB write error ${entry.agentId}: ${(dbErr as Error).message}`)
1022
+ }
1023
+ }
1024
+ // Push a synthetic sub_agent_turn_end into the progress driver
1025
+ // BEFORE the audit-log surface so the card mutation lands first.
1026
+ if (config.onStallTerminal != null) {
1027
+ try {
1028
+ config.onStallTerminal(entry.agentId, entry.description)
1029
+ } catch (cbErr) {
1030
+ log?.(`subagent-watcher: onStallTerminal callback error ${entry.agentId}: ${(cbErr as Error).message}`)
1031
+ }
1032
+ }
1033
+ // Fire the existing terminal-transition path (onFinish +
1034
+ // deferred cleanup). state==='done' was set above so
1035
+ // maybySendStateTransition flows through its happy path.
1036
+ maybySendStateTransition(entry.agentId)
1037
+ }
845
1038
  }
846
1039
 
847
1040
  // ─── Subagents dir scanner ───────────────────────────────────────────────
@@ -865,6 +1058,16 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
865
1058
  } catch { return }
866
1059
 
867
1060
  for (const pDir of projectDirs) {
1061
+ // Issue #1116: filter to the agent's own slug. Skip foreign
1062
+ // project dirs so their stale subagent JSONLs (which Claude
1063
+ // Code reaps mid-session) don't pollute the watcher's registry.
1064
+ if (expectedProjectSlug != null && pDir !== expectedProjectSlug) {
1065
+ if (!warnedForeignSlugs.has(pDir)) {
1066
+ warnedForeignSlugs.add(pDir)
1067
+ log?.(`subagent-watcher: skipping foreign project dir ${pDir} (expected ${expectedProjectSlug})`)
1068
+ }
1069
+ continue
1070
+ }
868
1071
  const projectPath = join(projectsRoot, pDir)
869
1072
  let sessionDirs: string[]
870
1073
  try {
@@ -910,6 +1113,12 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
910
1113
  if (!e.startsWith('agent-') || !e.endsWith('.jsonl')) continue
911
1114
  const filePath = join(subagentsPath, e)
912
1115
  if (knownFiles.has(filePath)) continue
1116
+ const agentId = e.slice('agent-'.length, -'.jsonl'.length)
1117
+ // Issue #1116 (Bug B): skip JSONLs whose agent already completed
1118
+ // and was swept by cleanupTerminalAgent. Re-adding to knownFiles
1119
+ // here would let a subsequent rescan re-register, fire a duplicate
1120
+ // "Worker done", and loop forever every grace-window.
1121
+ if (terminatedAgentIds.has(agentId)) continue
913
1122
  knownFiles.add(filePath)
914
1123
  // During the initial boot scan, mark every discovered file as
915
1124
  // historical so stall-detection and completion notifications are
@@ -918,7 +1127,6 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
918
1127
  if (bootScanInProgress) {
919
1128
  historicalFiles.add(filePath)
920
1129
  }
921
- const agentId = e.slice('agent-'.length, -'.jsonl'.length)
922
1130
  registerAgent(filePath, agentId)
923
1131
  }
924
1132
  }
@@ -1003,6 +1211,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
1003
1211
  tails.clear()
1004
1212
  registry.clear()
1005
1213
  knownFiles.clear()
1214
+ terminatedAgentIds.clear()
1006
1215
  },
1007
1216
 
1008
1217
  getRegistry(): ReadonlyMap<string, WorkerEntry> {