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
@@ -1,497 +0,0 @@
1
- /**
2
- * Pure logic for the `/auth` slot-management sub-verbs (add/use/list/rm).
3
- *
4
- * Lives outside gateway.ts + server.ts so it's unit-testable without
5
- * spinning up a grammy bot. The gateway/server command handlers call
6
- * `parseAuthSubCommand` to turn a raw /auth argv into a dispatch plan
7
- * (switchroom CLI args + label + optional post-action hook), then
8
- * handle that plan via their existing runSwitchroomCommand pipeline.
9
- */
10
-
11
- /** Pattern used by slot names throughout switchroom. Matches the shape
12
- * used by `addAccountStart` and slot-dir naming in src/auth/accounts.ts. */
13
- const SLOT_NAME_RE = /^[a-zA-Z0-9_-]{1,32}$/;
14
-
15
- export function assertSafeSlotName(slot: string): void {
16
- if (!SLOT_NAME_RE.test(slot)) {
17
- throw new Error(`invalid slot name: ${slot}`);
18
- }
19
- }
20
-
21
- /** Pattern used by global account labels — matches validateAccountLabel
22
- * in src/auth/account-store.ts. Allows email-shaped labels
23
- * (`pixsoul@gmail.com`) and gmail-tag forms (`ken+work@example.com`).
24
- * Excludes `:` (callback_data separator), path separators, shell
25
- * metas, and whitespace. Max 64 chars. Keep in sync with `LABEL_RE`
26
- * in account-store.ts and `isSafeAccountLabel` in auth-dashboard.ts. */
27
- const ACCOUNT_LABEL_RE = /^[A-Za-z0-9._@+-]{1,64}$/;
28
-
29
- export function assertSafeAccountLabel(label: string): void {
30
- if (label === '.' || label === '..') {
31
- throw new Error(`invalid account label: ${label}`);
32
- }
33
- if (!ACCOUNT_LABEL_RE.test(label)) {
34
- throw new Error(`invalid account label: ${label}`);
35
- }
36
- }
37
-
38
- /** Agent-name check mirrored from gateway.ts so the parser doesn't
39
- * need to import gateway.ts (which has top-level side effects). */
40
- const AGENT_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
41
- export function assertSafeAgentNameForParser(name: string): void {
42
- if (name !== 'all' && !AGENT_NAME_RE.test(name)) {
43
- throw new Error(`invalid agent name: ${name}`);
44
- }
45
- }
46
-
47
- export type AuthIntent =
48
- | { kind: 'login' | 'reauth' | 'link'; agent: string; label: string; cliArgs: string[]; registerReauth: boolean }
49
- | { kind: 'code'; agent: string; code: string; label: string; cliArgs: string[] }
50
- | { kind: 'cancel'; agent: string; label: string; cliArgs: string[] }
51
- | { kind: 'status'; label: string; cliArgs: string[] }
52
- | { kind: 'add'; agent: string; slot?: string; label: string; cliArgs: string[] }
53
- | { kind: 'use'; agent: string; slot: string; force: boolean; label: string; cliArgs: string[]; restartAgentAfter: true }
54
- | { kind: 'list'; agent: string; label: string; cliArgs: string[] }
55
- | { kind: 'rm'; agent: string; slot: string; force: boolean; label: string; cliArgs: string[] }
56
- // ── New account-shaped verbs (see reference/share-auth-across-the-fleet.md) ──
57
- | { kind: 'account-add'; account: string; fromAgent: string; label: string; cliArgs: string[] }
58
- | { kind: 'account-list'; label: string; cliArgs: string[] }
59
- | { kind: 'account-rm'; account: string; label: string; cliArgs: string[] }
60
- | { kind: 'account-rename'; oldAccount: string; newAccount: string; label: string; cliArgs: string[] }
61
- | { kind: 'enable'; account: string; agents: string[]; label: string; cliArgs: string[]; restartAgentsAfter: true }
62
- | { kind: 'disable'; account: string; agents: string[]; label: string; cliArgs: string[] }
63
- | { kind: 'share'; account: string; fromAgent: string; label: string; cliArgs: string[]; restartAgentsAfter: true }
64
- | { kind: 'usage'; message: string }
65
- | { kind: 'error'; message: string };
66
-
67
- export const AUTH_VERBS = [
68
- 'login', 'reauth', 'link',
69
- 'code', 'cancel', 'status',
70
- 'add', 'use', 'list', 'rm',
71
- // New account-shaped verbs
72
- 'account', 'enable', 'disable', 'share',
73
- ] as const;
74
-
75
- /** Help/usage string shown for unknown subcommands. Keep wording close
76
- * to the previous inline usage so the help-text asserting tests
77
- * naturally catch drift. */
78
- export function usageText(): string {
79
- return [
80
- 'Usage:',
81
- '/auth — status dashboard',
82
- '',
83
- 'Per-agent (legacy slot model):',
84
- '/auth login [agent] — start OAuth for agent',
85
- '/auth reauth [agent] — re-auth from scratch',
86
- '/auth code [agent] <browser-code> — finish OAuth flow',
87
- '/auth cancel [agent] — cancel pending flow',
88
- '/auth add [agent] [--slot <name>] — add another slot',
89
- '/auth use [agent] <slot> [--force] — switch active slot',
90
- '/auth list [agent] — list slots',
91
- '/auth rm [agent] <slot> [--force] — remove a slot',
92
- '',
93
- 'Anthropic accounts (shared across agents):',
94
- '/auth account add <label> [--from-agent <name>] — promote slot to global account',
95
- '/auth account list — accounts + agents using each',
96
- '/auth account rm <label> — remove (refused if enabled)',
97
- '/auth account rename <old> <new> — rename account + rewrite agents.<name>.auth.accounts lists',
98
- '/auth enable <label> [agents...|all] — wire account to agent(s); "all" = every agent',
99
- '/auth disable <label> [agents...|all] — unwire account from agent(s); "all" = every agent',
100
- '/auth share <label> [--from-agent <name>] — account add + enable on every agent in one step',
101
- ].join('\n');
102
- }
103
-
104
- /**
105
- * Turn raw /auth argv into a dispatch intent.
106
- *
107
- * `parts` is the whitespace-split tail of the /auth command (no leading
108
- * "/auth"). `currentAgent` is the agent this gateway process represents.
109
- * Missing agent arg defaults to `currentAgent` so single-agent setups
110
- * Just Work without typing the name.
111
- */
112
- export function parseAuthSubCommand(
113
- parts: string[],
114
- currentAgent: string,
115
- ): AuthIntent {
116
- const sub = (parts[0] ?? 'status').toLowerCase();
117
-
118
- // Existing verbs — kept here so both gateway.ts and server.ts can
119
- // route them through a single source of truth once they migrate.
120
- if (sub === 'login' || sub === 'reauth' || sub === 'link') {
121
- const agent = parts[1] ?? currentAgent;
122
- try { assertSafeAgentNameForParser(agent); }
123
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
124
- return {
125
- kind: sub,
126
- agent,
127
- label: `auth ${sub} ${agent}`,
128
- cliArgs: ['auth', sub, agent],
129
- registerReauth: sub === 'reauth' || sub === 'login',
130
- };
131
- }
132
-
133
- if (sub === 'code') {
134
- let agent = currentAgent; let code = '';
135
- if (parts.length >= 3) { agent = parts[1]; code = parts.slice(2).join(' '); }
136
- else if (parts.length === 2) { code = parts[1]; }
137
- if (!code) return { kind: 'usage', message: 'Usage: /auth code [agent] <browser-code>' };
138
- try { assertSafeAgentNameForParser(agent); }
139
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
140
- return { kind: 'code', agent, code, label: `auth code ${agent}`, cliArgs: ['auth', 'code', agent, code] };
141
- }
142
-
143
- if (sub === 'cancel') {
144
- const agent = parts[1] ?? currentAgent;
145
- try { assertSafeAgentNameForParser(agent); }
146
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
147
- return { kind: 'cancel', agent, label: `auth cancel ${agent}`, cliArgs: ['auth', 'cancel', agent] };
148
- }
149
-
150
- if (sub === 'status') {
151
- return { kind: 'status', label: 'auth status', cliArgs: ['auth', 'status'] };
152
- }
153
-
154
- // --- New slot-management verbs ---
155
-
156
- if (sub === 'add') {
157
- // /auth add [agent] [--slot <name>]
158
- const rest = parts.slice(1);
159
- const { flags, positional } = splitFlags(rest, ['--slot']);
160
- const agent = positional[0] ?? currentAgent;
161
- // splitFlags returns `string | true | undefined` for value flags
162
- // (true when the flag is present without a value). For `--slot` we
163
- // expect a string value; reject the bare-flag form.
164
- const rawSlot = flags['--slot'];
165
- const slot = typeof rawSlot === 'string' ? rawSlot : undefined;
166
- try { assertSafeAgentNameForParser(agent); }
167
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
168
- if (slot !== undefined) {
169
- try { assertSafeSlotName(slot); }
170
- catch { return { kind: 'error', message: 'Invalid slot name. Use [A-Za-z0-9_-], 1-32 chars.' }; }
171
- }
172
- const cliArgs = ['auth', 'add', agent];
173
- if (slot) cliArgs.push('--slot', slot);
174
- return { kind: 'add', agent, slot, label: `auth add ${agent}`, cliArgs };
175
- }
176
-
177
- if (sub === 'use') {
178
- // /auth use [agent] <slot> [--force]
179
- const rest = parts.slice(1);
180
- const { flags, positional } = splitFlags(rest, []);
181
- if (positional.length === 0) {
182
- return { kind: 'usage', message: 'Usage: /auth use [agent] <slot> [--force]' };
183
- }
184
- const [agent, slot] = positional.length === 1
185
- ? [currentAgent, positional[0]]
186
- : [positional[0], positional[1]];
187
- try { assertSafeAgentNameForParser(agent); }
188
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
189
- try { assertSafeSlotName(slot); }
190
- catch { return { kind: 'error', message: 'Invalid slot name. Use [A-Za-z0-9_-], 1-32 chars.' }; }
191
- return {
192
- kind: 'use', agent, slot,
193
- force: flags['--force'] === true,
194
- label: `auth use ${agent} ${slot}`,
195
- cliArgs: ['auth', 'use', agent, slot],
196
- restartAgentAfter: true,
197
- };
198
- }
199
-
200
- if (sub === 'list') {
201
- const agent = parts[1] ?? currentAgent;
202
- try { assertSafeAgentNameForParser(agent); }
203
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
204
- return {
205
- kind: 'list', agent,
206
- label: `auth list ${agent}`,
207
- cliArgs: ['auth', 'list', agent, '--json'],
208
- };
209
- }
210
-
211
- if (sub === 'rm') {
212
- // /auth rm [agent] <slot> [--force]
213
- const rest = parts.slice(1);
214
- const { flags, positional } = splitFlags(rest, ['--force']);
215
- if (positional.length === 0) {
216
- return { kind: 'usage', message: 'Usage: /auth rm [agent] <slot> [--force]' };
217
- }
218
- const [agent, slot] = positional.length === 1
219
- ? [currentAgent, positional[0]]
220
- : [positional[0], positional[1]];
221
- try { assertSafeAgentNameForParser(agent); }
222
- catch { return { kind: 'error', message: 'Invalid agent name.' }; }
223
- try { assertSafeSlotName(slot); }
224
- catch { return { kind: 'error', message: 'Invalid slot name. Use [A-Za-z0-9_-], 1-32 chars.' }; }
225
- const force = flags['--force'] === true;
226
- return {
227
- kind: 'rm', agent, slot, force,
228
- label: `auth rm ${agent} ${slot}`,
229
- cliArgs: ['auth', 'rm', agent, slot],
230
- };
231
- }
232
-
233
- // --- Account-shaped verbs (see reference/share-auth-across-the-fleet.md) ---
234
-
235
- if (sub === 'account') {
236
- const accountSub = (parts[1] ?? 'list').toLowerCase();
237
-
238
- if (accountSub === 'add') {
239
- // /auth account add <label> [--from-agent <name>]
240
- // Default --from-agent to the current agent — that's the common case
241
- // for a Telegram-only operator who just /auth login'd this agent.
242
- const rest = parts.slice(2);
243
- const { flags, positional } = splitFlags(rest, ['--from-agent']);
244
- const account = positional[0];
245
- if (!account) {
246
- return {
247
- kind: 'usage',
248
- message: 'Usage: /auth account add <label> [--from-agent <name>]',
249
- };
250
- }
251
- try { assertSafeAccountLabel(account); }
252
- catch { return { kind: 'error', message: 'Invalid account label. Use [A-Za-z0-9._@+-], 1-64 chars (email shape OK).' }; }
253
- const fromAgentRaw = flags['--from-agent'];
254
- const fromAgent = typeof fromAgentRaw === 'string' ? fromAgentRaw : currentAgent;
255
- try { assertSafeAgentNameForParser(fromAgent); }
256
- catch { return { kind: 'error', message: 'Invalid --from-agent value.' }; }
257
- return {
258
- kind: 'account-add',
259
- account,
260
- fromAgent,
261
- label: `auth account add ${account}`,
262
- cliArgs: ['auth', 'account', 'add', account, '--from-agent', fromAgent],
263
- };
264
- }
265
-
266
- if (accountSub === 'list') {
267
- return {
268
- kind: 'account-list',
269
- label: 'auth account list',
270
- cliArgs: ['auth', 'account', 'list'],
271
- };
272
- }
273
-
274
- if (accountSub === 'rm') {
275
- // /auth account rm <label>
276
- const account = parts[2];
277
- if (!account) {
278
- return { kind: 'usage', message: 'Usage: /auth account rm <label>' };
279
- }
280
- try { assertSafeAccountLabel(account); }
281
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
282
- return {
283
- kind: 'account-rm',
284
- account,
285
- label: `auth account rm ${account}`,
286
- cliArgs: ['auth', 'account', 'rm', account],
287
- };
288
- }
289
-
290
- if (accountSub === 'rename') {
291
- // /auth account rename <oldLabel> <newLabel>
292
- const oldAccount = parts[2];
293
- const newAccount = parts[3];
294
- if (!oldAccount || !newAccount) {
295
- return {
296
- kind: 'usage',
297
- message: 'Usage: /auth account rename <oldLabel> <newLabel>',
298
- };
299
- }
300
- try { assertSafeAccountLabel(oldAccount); assertSafeAccountLabel(newAccount); }
301
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
302
- if (oldAccount === newAccount) {
303
- return { kind: 'error', message: `Account "${oldAccount}" already has that name — nothing to do.` };
304
- }
305
- return {
306
- kind: 'account-rename',
307
- oldAccount,
308
- newAccount,
309
- label: `auth account rename ${oldAccount} ${newAccount}`,
310
- cliArgs: ['auth', 'account', 'rename', oldAccount, newAccount],
311
- };
312
- }
313
-
314
- return {
315
- kind: 'usage',
316
- message: 'Usage: /auth account add | list | rm | rename (see /auth)',
317
- };
318
- }
319
-
320
- if (sub === 'enable') {
321
- // /auth enable <label> [agents...] — defaults to the current agent.
322
- const rest = parts.slice(1);
323
- const account = rest[0];
324
- if (!account) {
325
- return { kind: 'usage', message: 'Usage: /auth enable <label> [agents...]' };
326
- }
327
- try { assertSafeAccountLabel(account); }
328
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
329
- const agents = rest.slice(1);
330
- if (agents.length === 0) agents.push(currentAgent);
331
- for (const a of agents) {
332
- try { assertSafeAgentNameForParser(a); }
333
- catch { return { kind: 'error', message: `Invalid agent name: ${a}` }; }
334
- }
335
- return {
336
- kind: 'enable',
337
- account,
338
- agents,
339
- label: `auth enable ${account} ${agents.join(' ')}`,
340
- cliArgs: ['auth', 'enable', account, ...agents],
341
- restartAgentsAfter: true,
342
- };
343
- }
344
-
345
- if (sub === 'disable') {
346
- // /auth disable <label> [agents...] — defaults to the current agent.
347
- const rest = parts.slice(1);
348
- const account = rest[0];
349
- if (!account) {
350
- return { kind: 'usage', message: 'Usage: /auth disable <label> [agents...]' };
351
- }
352
- try { assertSafeAccountLabel(account); }
353
- catch { return { kind: 'error', message: 'Invalid account label.' }; }
354
- const agents = rest.slice(1);
355
- if (agents.length === 0) agents.push(currentAgent);
356
- for (const a of agents) {
357
- try { assertSafeAgentNameForParser(a); }
358
- catch { return { kind: 'error', message: `Invalid agent name: ${a}` }; }
359
- }
360
- return {
361
- kind: 'disable',
362
- account,
363
- agents,
364
- label: `auth disable ${account} ${agents.join(' ')}`,
365
- cliArgs: ['auth', 'disable', account, ...agents],
366
- };
367
- }
368
-
369
- if (sub === 'share') {
370
- // /auth share <label> [--from-agent <name>] — one-shot: account add + enable
371
- // on every agent. Defaults --from-agent to the current agent (same shape as
372
- // /auth account add).
373
- const rest = parts.slice(1);
374
- const { flags, positional } = splitFlags(rest, ['--from-agent']);
375
- const account = positional[0];
376
- if (!account) {
377
- return { kind: 'usage', message: 'Usage: /auth share <label> [--from-agent <name>]' };
378
- }
379
- try { assertSafeAccountLabel(account); }
380
- catch { return { kind: 'error', message: 'Invalid account label. Use [A-Za-z0-9._@+-], 1-64 chars (email shape OK).' }; }
381
- const fromAgentRaw = flags['--from-agent'];
382
- const fromAgent = typeof fromAgentRaw === 'string' ? fromAgentRaw : currentAgent;
383
- try { assertSafeAgentNameForParser(fromAgent); }
384
- catch { return { kind: 'error', message: 'Invalid --from-agent value.' }; }
385
- return {
386
- kind: 'share',
387
- account,
388
- fromAgent,
389
- label: `auth share ${account}`,
390
- cliArgs: ['auth', 'share', account, '--from-agent', fromAgent],
391
- restartAgentsAfter: true,
392
- };
393
- }
394
-
395
- return { kind: 'usage', message: usageText() };
396
- }
397
-
398
- /** Helper to split --flag [value]? from positional args.
399
- * Value-taking flags are passed in `valueFlags`; bare flags (like
400
- * --force) show up in `flags` as boolean true.*/
401
- export function splitFlags(
402
- parts: string[],
403
- valueFlags: string[],
404
- ): { flags: Record<string, string | true>; positional: string[] } {
405
- const flags: Record<string, string | true> = {};
406
- const positional: string[] = [];
407
- const valueSet = new Set(valueFlags);
408
- for (let i = 0; i < parts.length; i++) {
409
- const p = parts[i];
410
- if (p.startsWith('--')) {
411
- if (valueSet.has(p)) {
412
- const next = parts[i + 1];
413
- if (next !== undefined && !next.startsWith('--')) { flags[p] = next; i++; }
414
- else flags[p] = true;
415
- } else {
416
- flags[p] = true;
417
- }
418
- } else {
419
- positional.push(p);
420
- }
421
- }
422
- return { flags, positional };
423
- }
424
-
425
- /** Active + total slot accounting for the rm safety check.
426
- * Returned from the CLI's --json shape (see src/cli/auth.ts `list`). */
427
- export type SlotListingFromCli = {
428
- agent: string;
429
- slots: Array<{
430
- slot: string;
431
- active: boolean;
432
- health: string;
433
- expires_at: number | null;
434
- quota_exhausted_until: number | null;
435
- }>;
436
- };
437
-
438
- /** Check whether a /auth rm is safe. Returns `null` if safe, or an error
439
- * message if the slot is the only/active slot without --force. */
440
- export function checkRemoveSafety(
441
- listing: SlotListingFromCli,
442
- targetSlot: string,
443
- force: boolean,
444
- ): string | null {
445
- if (force) return null;
446
- if (listing.slots.length <= 1) {
447
- return `Refusing to remove the only account slot. Add another with /auth add ${listing.agent}, or pass --force to proceed.`;
448
- }
449
- const target = listing.slots.find(s => s.slot === targetSlot);
450
- if (!target) return null; // CLI will error with its own message
451
- if (target.active) {
452
- return `Refusing to remove the active slot "${targetSlot}". Switch first with /auth use ${listing.agent} <other-slot>, or pass --force.`;
453
- }
454
- return null;
455
- }
456
-
457
- /** Format the /auth list CLI --json output as a Telegram HTML block. */
458
- export function formatSlotList(listing: SlotListingFromCli): string {
459
- if (!listing.slots || listing.slots.length === 0) {
460
- return `<i>No slots for <b>${escapeMini(listing.agent)}</b>. Add one with /auth add ${escapeMini(listing.agent)}.</i>`;
461
- }
462
- const lines = [`<b>Slots for ${escapeMini(listing.agent)}</b>`];
463
- for (const s of listing.slots) {
464
- const active = s.active ? '● ' : ' ';
465
- const name = `<code>${escapeMini(s.slot)}</code>`;
466
- const health = healthIcon(s.health) + ' ' + s.health;
467
- let tail = '';
468
- if (s.health === 'quota-exhausted' && s.quota_exhausted_until) {
469
- const mins = Math.max(0, Math.round((s.quota_exhausted_until - Date.now()) / 60_000));
470
- tail = ` · resets in ~${mins}m`;
471
- } else if (s.health === 'expired') {
472
- tail = ' · run /auth reauth';
473
- }
474
- lines.push(`${active}${name} ${health}${tail}`);
475
- }
476
- return lines.join('\n');
477
- }
478
-
479
- function healthIcon(health: string): string {
480
- switch (health) {
481
- case 'healthy': return '✓';
482
- case 'quota-exhausted': return '⚠️';
483
- case 'expired': return '⌛';
484
- case 'missing': return '✗';
485
- default: return '·';
486
- }
487
- }
488
-
489
- /** Tiny HTML escaper — mirrored from welcome-text.ts so this module
490
- * stays dependency-free and testable in isolation. */
491
- function escapeMini(text: string): string {
492
- return text
493
- .replace(/&/g, '&amp;')
494
- .replace(/</g, '&lt;')
495
- .replace(/>/g, '&gt;')
496
- .replace(/"/g, '&quot;');
497
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Structured logger for the pinned progress-card lifecycle.
3
- *
4
- * Mirrors `pin-event-log.ts` in shape: an append-only JSON-line writer with
5
- * a stable schema. Every meaningful card-driver state transition emits one
6
- * line so operators can grep / replay days-old sessions and answer "did the
7
- * card render? when did it finalize? was a sub-agent row ever attached?"
8
- * without parsing free-form `progress-card:` traces.
9
- *
10
- * Output target:
11
- * - If `$STATE_DIR` is set, `<STATE_DIR>/card-events.jsonl` (append-only).
12
- * - Otherwise the line is forwarded to stderr (which the plugin-logger
13
- * captures into `~/.switchroom/logs/telegram-plugin.log`).
14
- *
15
- * No rotation in this PR — the file is the durable audit trail and a
16
- * follow-up can add retention once the size envelope is understood.
17
- *
18
- * Pure helper. No globals. The write target is injectable for tests.
19
- */
20
-
21
- import { appendFileSync, mkdirSync } from 'fs'
22
- import { dirname, join } from 'path'
23
-
24
- export type CardEventName =
25
- | 'rendered'
26
- | 'edited'
27
- | 'finalized'
28
- | 'suppressed'
29
- | 'deferred'
30
- | 'force-completed'
31
- | 'deleted'
32
-
33
- export interface CardEvent {
34
- /** Unix-ms wall clock. */
35
- ts: number
36
- /** Agent slug (e.g. SWITCHROOM_AGENT_NAME). Empty string if unknown. */
37
- agent: string
38
- /** Telegram chat id as string (matches the rest of the plugin). */
39
- chatId: string
40
- /** Driver-assigned per-turn key (chatId:threadId:seq). */
41
- turnKey: string
42
- /** The pinned card message_id once known. Optional pre-render. */
43
- cardMessageId?: number
44
- event: CardEventName
45
- /**
46
- * Free-text qualifier — e.g. the reason a turn was deferred
47
- * ("in-flight-sub-agents"), the API class for a 4xx abandon, the
48
- * synthetic kind for a force-complete. Single-line, ≤200 chars.
49
- */
50
- reason?: string
51
- /** sha1-12 of the rendered HTML, when relevant. Lets us spot edit storms. */
52
- htmlHash?: string
53
- /** Sub-agent ids attached to the card at the time of the event. */
54
- subagents?: string[]
55
- /** Elapsed ms since turn start, when the call site has it cheaply. */
56
- durationMs?: number
57
- }
58
-
59
- export type CardEventWriter = (line: string) => void
60
-
61
- let resolvedPath: string | null | undefined
62
-
63
- /**
64
- * Compute the target path once and memoize. `$STATE_DIR` set → write to
65
- * `<STATE_DIR>/card-events.jsonl`; otherwise return null (the default
66
- * writer falls back to stderr in that case).
67
- *
68
- * Exposed so tests can assert resolution without actually writing.
69
- */
70
- export function resolveCardEventPath(env: NodeJS.ProcessEnv = process.env): string | null {
71
- const dir = env.STATE_DIR
72
- if (!dir || dir.length === 0) return null
73
- return join(dir, 'card-events.jsonl')
74
- }
75
-
76
- /**
77
- * Reset the memoized path. Tests only.
78
- */
79
- export function _resetForTests(): void {
80
- resolvedPath = undefined
81
- }
82
-
83
- const defaultWriter: CardEventWriter = (line) => {
84
- if (resolvedPath === undefined) {
85
- resolvedPath = resolveCardEventPath()
86
- }
87
- const target = resolvedPath
88
- if (target == null) {
89
- // Fall back to stderr (the plugin-logger captures stderr into the
90
- // freeform log). Prefix lets operators grep just like pin-event:.
91
- try {
92
- process.stderr.write(`card-event: ${line}`)
93
- } catch {
94
- // Never throw from a logger.
95
- }
96
- return
97
- }
98
- try {
99
- mkdirSync(dirname(target), { recursive: true })
100
- appendFileSync(target, line)
101
- } catch {
102
- // Best-effort: if the structured sink fails, surface to stderr so the
103
- // event is at least in the freeform log.
104
- try {
105
- process.stderr.write(`card-event: ${line}`)
106
- } catch {
107
- // ignore
108
- }
109
- }
110
- }
111
-
112
- export function logCardEvent(event: CardEvent, write: CardEventWriter = defaultWriter): void {
113
- // Drop undefined fields so the JSON output stays compact and grep-friendly.
114
- const cleaned: Record<string, unknown> = {}
115
- for (const [k, v] of Object.entries(event)) {
116
- if (v !== undefined) cleaned[k] = v
117
- }
118
- const payload = JSON.stringify(cleaned)
119
- write(`${payload}\n`)
120
- }
121
-
122
- /**
123
- * Convenience constructor — fills `ts` automatically. Most call sites only
124
- * have agent / chatId / turnKey / event / a few qualifiers; this keeps the
125
- * boilerplate low.
126
- */
127
- export function emitCardEvent(
128
- partial: Omit<CardEvent, 'ts'> & { ts?: number },
129
- write: CardEventWriter = defaultWriter,
130
- ): void {
131
- logCardEvent(
132
- {
133
- ts: partial.ts ?? Date.now(),
134
- ...partial,
135
- } as CardEvent,
136
- write,
137
- )
138
- }