switchroom 0.5.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 (718) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +447 -0
  3. package/bin/autoaccept.exp +81 -0
  4. package/bin/boot-self-test.sh +149 -0
  5. package/bin/bridge-watchdog.sh +967 -0
  6. package/bin/handoff-briefing.sh +206 -0
  7. package/bin/run-hook.sh +228 -0
  8. package/bin/switchroom.ts +4 -0
  9. package/bin/timezone-hook.sh +67 -0
  10. package/bin/user-profile-refresh-hook.sh +38 -0
  11. package/bin/workspace-dynamic-hook.sh +142 -0
  12. package/bin/workspace-stable-hook.sh +57 -0
  13. package/dist/cli/autoaccept-poll.js +118 -0
  14. package/dist/cli/switchroom.js +48557 -0
  15. package/package.json +95 -0
  16. package/profiles/_base/settings.json.hbs +15 -0
  17. package/profiles/_base/start.sh.hbs +383 -0
  18. package/profiles/_shared/telegram-style.md.hbs +140 -0
  19. package/profiles/coding/CLAUDE.md.hbs +57 -0
  20. package/profiles/coding/skills/architecture/SKILL.md +70 -0
  21. package/profiles/coding/skills/code-review/SKILL.md +58 -0
  22. package/profiles/coding/workspace/SOUL.md.hbs +25 -0
  23. package/profiles/default/CLAUDE.md +238 -0
  24. package/profiles/default/CLAUDE.md.hbs +113 -0
  25. package/profiles/default/workspace/CLAUDE.md.hbs +126 -0
  26. package/profiles/default/workspace/HEARTBEAT.md.hbs +40 -0
  27. package/profiles/default/workspace/IDENTITY.md.hbs +32 -0
  28. package/profiles/default/workspace/MEMORY.md.hbs +29 -0
  29. package/profiles/default/workspace/SOUL.md.hbs +61 -0
  30. package/profiles/default/workspace/TOOLS.md.hbs +29 -0
  31. package/profiles/default/workspace/USER.md.hbs +52 -0
  32. package/profiles/default/workspace/memory/.gitkeep +0 -0
  33. package/profiles/executive-assistant/CLAUDE.md.hbs +51 -0
  34. package/profiles/executive-assistant/skills/daily-briefing/SKILL.md +55 -0
  35. package/profiles/executive-assistant/skills/meeting-prep/SKILL.md +58 -0
  36. package/profiles/executive-assistant/workspace/SOUL.md.hbs +25 -0
  37. package/profiles/health-coach/CLAUDE.md.hbs +45 -0
  38. package/profiles/health-coach/skills/check-in/SKILL.md +41 -0
  39. package/profiles/health-coach/skills/weekly-review/SKILL.md +53 -0
  40. package/profiles/health-coach/workspace/SOUL.md.hbs +25 -0
  41. package/skills/buildkite-agent-infrastructure/SKILL.md +302 -0
  42. package/skills/buildkite-agent-infrastructure/agents/openai.yaml +6 -0
  43. package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-large.png +0 -0
  44. package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-small.png +0 -0
  45. package/skills/buildkite-agent-infrastructure/references/audit-logging.md +87 -0
  46. package/skills/buildkite-agent-infrastructure/references/graphql-mutations.md +690 -0
  47. package/skills/buildkite-agent-infrastructure/references/instance-shapes.md +38 -0
  48. package/skills/buildkite-agent-infrastructure/references/pipeline-templates.md +73 -0
  49. package/skills/buildkite-agent-infrastructure/references/self-hosted-agents.md +137 -0
  50. package/skills/buildkite-agent-infrastructure/references/sso-saml.md +92 -0
  51. package/skills/buildkite-agent-runtime/SKILL.md +476 -0
  52. package/skills/buildkite-agent-runtime/agents/openai.yaml +6 -0
  53. package/skills/buildkite-agent-runtime/assets/buildkite-icon-large.png +0 -0
  54. package/skills/buildkite-agent-runtime/assets/buildkite-icon-small.png +0 -0
  55. package/skills/buildkite-agent-runtime/references/flag-reference.md +417 -0
  56. package/skills/buildkite-agent-runtime/references/patterns-and-recipes.md +555 -0
  57. package/skills/buildkite-api/SKILL.md +285 -0
  58. package/skills/buildkite-api/agents/openai.yaml +6 -0
  59. package/skills/buildkite-api/assets/buildkite-icon-large.png +0 -0
  60. package/skills/buildkite-api/assets/buildkite-icon-small.png +0 -0
  61. package/skills/buildkite-api/references/graphql-reference.md +195 -0
  62. package/skills/buildkite-api/references/patterns.md +44 -0
  63. package/skills/buildkite-api/references/webhooks.md +161 -0
  64. package/skills/buildkite-cli/SKILL.md +379 -0
  65. package/skills/buildkite-cli/agents/openai.yaml +6 -0
  66. package/skills/buildkite-cli/assets/buildkite-icon-large.png +0 -0
  67. package/skills/buildkite-cli/assets/buildkite-icon-small.png +0 -0
  68. package/skills/buildkite-cli/references/command-reference.md +181 -0
  69. package/skills/buildkite-migration/SKILL.md +182 -0
  70. package/skills/buildkite-pipelines/SKILL.md +464 -0
  71. package/skills/buildkite-pipelines/agents/openai.yaml +6 -0
  72. package/skills/buildkite-pipelines/assets/buildkite-icon-large.png +0 -0
  73. package/skills/buildkite-pipelines/assets/buildkite-icon-small.png +0 -0
  74. package/skills/buildkite-pipelines/examples/basic-pipeline.yml +24 -0
  75. package/skills/buildkite-pipelines/examples/optimized-pipeline.yml +100 -0
  76. package/skills/buildkite-pipelines/references/advanced-patterns.md +286 -0
  77. package/skills/buildkite-pipelines/references/retry-and-error-codes.md +131 -0
  78. package/skills/buildkite-pipelines/references/step-types-reference.md +225 -0
  79. package/skills/buildkite-secure-delivery/SKILL.md +168 -0
  80. package/skills/buildkite-secure-delivery/agents/openai.yaml +6 -0
  81. package/skills/buildkite-secure-delivery/assets/buildkite-icon-large.png +0 -0
  82. package/skills/buildkite-secure-delivery/assets/buildkite-icon-small.png +0 -0
  83. package/skills/buildkite-secure-delivery/references/oidc-cloud-providers.md +83 -0
  84. package/skills/buildkite-secure-delivery/references/package-publishing.md +100 -0
  85. package/skills/buildkite-test-engine/SKILL.md +239 -0
  86. package/skills/buildkite-test-engine/agents/openai.yaml +6 -0
  87. package/skills/buildkite-test-engine/assets/buildkite-icon-large.png +0 -0
  88. package/skills/buildkite-test-engine/assets/buildkite-icon-small.png +0 -0
  89. package/skills/buildkite-test-engine/examples/bktec-splitting.yml +16 -0
  90. package/skills/buildkite-test-engine/examples/collector-pipeline.yml +11 -0
  91. package/skills/buildkite-test-engine/references/collectors.md +198 -0
  92. package/skills/buildkite-test-engine/references/splitting-examples.md +93 -0
  93. package/skills/docx/LICENSE.txt +30 -0
  94. package/skills/docx/SKILL.md +590 -0
  95. package/skills/docx/VENDORED.md +32 -0
  96. package/skills/docx/scripts/__init__.py +1 -0
  97. package/skills/docx/scripts/accept_changes.py +135 -0
  98. package/skills/docx/scripts/comment.py +318 -0
  99. package/skills/docx/scripts/office/helpers/__init__.py +0 -0
  100. package/skills/docx/scripts/office/helpers/merge_runs.py +199 -0
  101. package/skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
  102. package/skills/docx/scripts/office/pack.py +159 -0
  103. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  104. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  105. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  106. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  107. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  108. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  109. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  110. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  111. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  112. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  113. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  114. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  115. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  116. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  117. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  118. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  119. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  120. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  121. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  122. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  123. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  124. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  125. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  126. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  127. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  128. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  129. package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  130. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  131. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  132. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  133. package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  134. package/skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
  135. package/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  136. package/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  137. package/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  138. package/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  139. package/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  140. package/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  141. package/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  142. package/skills/docx/scripts/office/soffice.py +183 -0
  143. package/skills/docx/scripts/office/unpack.py +132 -0
  144. package/skills/docx/scripts/office/validate.py +111 -0
  145. package/skills/docx/scripts/office/validators/__init__.py +15 -0
  146. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
  147. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
  148. package/skills/docx/scripts/office/validators/base.py +847 -0
  149. package/skills/docx/scripts/office/validators/docx.py +446 -0
  150. package/skills/docx/scripts/office/validators/pptx.py +275 -0
  151. package/skills/docx/scripts/office/validators/redlining.py +247 -0
  152. package/skills/docx/scripts/templates/comments.xml +3 -0
  153. package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
  154. package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  155. package/skills/docx/scripts/templates/commentsIds.xml +3 -0
  156. package/skills/docx/scripts/templates/people.xml +3 -0
  157. package/skills/file-bug/SKILL.md +129 -0
  158. package/skills/humanizer/LICENSE +21 -0
  159. package/skills/humanizer/SKILL.md +559 -0
  160. package/skills/humanizer/VENDORED.md +38 -0
  161. package/skills/humanizer-calibrate/SKILL.md +144 -0
  162. package/skills/mcp-builder/LICENSE.txt +202 -0
  163. package/skills/mcp-builder/SKILL.md +236 -0
  164. package/skills/mcp-builder/VENDORED.md +32 -0
  165. package/skills/mcp-builder/reference/evaluation.md +602 -0
  166. package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  167. package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
  168. package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
  169. package/skills/mcp-builder/scripts/connections.py +151 -0
  170. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  171. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  172. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  173. package/skills/pdf/LICENSE.txt +30 -0
  174. package/skills/pdf/SKILL.md +314 -0
  175. package/skills/pdf/VENDORED.md +32 -0
  176. package/skills/pdf/forms.md +294 -0
  177. package/skills/pdf/reference.md +612 -0
  178. package/skills/pdf/scripts/check_bounding_boxes.py +65 -0
  179. package/skills/pdf/scripts/check_fillable_fields.py +11 -0
  180. package/skills/pdf/scripts/convert_pdf_to_images.py +33 -0
  181. package/skills/pdf/scripts/create_validation_image.py +37 -0
  182. package/skills/pdf/scripts/extract_form_field_info.py +122 -0
  183. package/skills/pdf/scripts/extract_form_structure.py +115 -0
  184. package/skills/pdf/scripts/fill_fillable_fields.py +98 -0
  185. package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  186. package/skills/pptx/LICENSE.txt +30 -0
  187. package/skills/pptx/SKILL.md +232 -0
  188. package/skills/pptx/VENDORED.md +32 -0
  189. package/skills/pptx/editing.md +205 -0
  190. package/skills/pptx/pptxgenjs.md +420 -0
  191. package/skills/pptx/scripts/__init__.py +0 -0
  192. package/skills/pptx/scripts/add_slide.py +195 -0
  193. package/skills/pptx/scripts/clean.py +286 -0
  194. package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
  195. package/skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
  196. package/skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
  197. package/skills/pptx/scripts/office/pack.py +159 -0
  198. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  199. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  200. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  201. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  202. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  203. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  204. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  205. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  206. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  207. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  208. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  209. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  210. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  211. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  212. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  213. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  214. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  215. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  216. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  217. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  218. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  219. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  220. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  221. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  222. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  223. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  224. package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  225. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  226. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  227. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  228. package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  229. package/skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
  230. package/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  231. package/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  232. package/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  233. package/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  234. package/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  235. package/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  236. package/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  237. package/skills/pptx/scripts/office/soffice.py +183 -0
  238. package/skills/pptx/scripts/office/unpack.py +132 -0
  239. package/skills/pptx/scripts/office/validate.py +111 -0
  240. package/skills/pptx/scripts/office/validators/__init__.py +15 -0
  241. package/skills/pptx/scripts/office/validators/base.py +847 -0
  242. package/skills/pptx/scripts/office/validators/docx.py +446 -0
  243. package/skills/pptx/scripts/office/validators/pptx.py +275 -0
  244. package/skills/pptx/scripts/office/validators/redlining.py +247 -0
  245. package/skills/pptx/scripts/thumbnail.py +289 -0
  246. package/skills/skill-creator/LICENSE.txt +202 -0
  247. package/skills/skill-creator/SKILL.md +485 -0
  248. package/skills/skill-creator/VENDORED.md +32 -0
  249. package/skills/skill-creator/agents/analyzer.md +274 -0
  250. package/skills/skill-creator/agents/comparator.md +202 -0
  251. package/skills/skill-creator/agents/grader.md +223 -0
  252. package/skills/skill-creator/assets/eval_review.html +146 -0
  253. package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  254. package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  255. package/skills/skill-creator/references/schemas.md +430 -0
  256. package/skills/skill-creator/scripts/__init__.py +0 -0
  257. package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  258. package/skills/skill-creator/scripts/generate_report.py +326 -0
  259. package/skills/skill-creator/scripts/improve_description.py +247 -0
  260. package/skills/skill-creator/scripts/package_skill.py +136 -0
  261. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  262. package/skills/skill-creator/scripts/run_eval.py +310 -0
  263. package/skills/skill-creator/scripts/run_loop.py +328 -0
  264. package/skills/skill-creator/scripts/utils.py +47 -0
  265. package/skills/switchroom-architecture/SKILL.md +60 -0
  266. package/skills/switchroom-architecture/cascade.md +112 -0
  267. package/skills/switchroom-architecture/sub-agents.md +87 -0
  268. package/skills/switchroom-architecture/telegram.md +94 -0
  269. package/skills/switchroom-cli/SKILL.md +274 -0
  270. package/skills/switchroom-health/SKILL.md +101 -0
  271. package/skills/switchroom-install/SKILL.md +116 -0
  272. package/skills/switchroom-manage/SKILL.md +90 -0
  273. package/skills/switchroom-status/SKILL.md +69 -0
  274. package/skills/switchroom-status/scripts/status.sh +69 -0
  275. package/skills/telegram-test-harness/SKILL.md +191 -0
  276. package/skills/token-helpers/SKILL.md +73 -0
  277. package/skills/token-helpers/scripts/google-cal-token.sh +62 -0
  278. package/skills/token-helpers/scripts/ms-graph-token.sh +70 -0
  279. package/skills/webapp-testing/LICENSE.txt +202 -0
  280. package/skills/webapp-testing/SKILL.md +96 -0
  281. package/skills/webapp-testing/VENDORED.md +32 -0
  282. package/skills/webapp-testing/examples/console_logging.py +35 -0
  283. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  284. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  285. package/skills/webapp-testing/scripts/with_server.py +106 -0
  286. package/skills/xlsx/LICENSE.txt +30 -0
  287. package/skills/xlsx/SKILL.md +292 -0
  288. package/skills/xlsx/VENDORED.md +32 -0
  289. package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
  290. package/skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
  291. package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  292. package/skills/xlsx/scripts/office/pack.py +159 -0
  293. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  294. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  295. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  296. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  297. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  298. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  299. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  300. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  301. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  302. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  303. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  304. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  305. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  306. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  307. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  308. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  309. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  310. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  311. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  312. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  313. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  314. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  315. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  316. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  317. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  318. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  319. package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  320. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  321. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  322. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  323. package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  324. package/skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  325. package/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  326. package/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  327. package/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  328. package/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  329. package/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  330. package/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  331. package/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  332. package/skills/xlsx/scripts/office/soffice.py +183 -0
  333. package/skills/xlsx/scripts/office/unpack.py +132 -0
  334. package/skills/xlsx/scripts/office/validate.py +111 -0
  335. package/skills/xlsx/scripts/office/validators/__init__.py +15 -0
  336. package/skills/xlsx/scripts/office/validators/base.py +847 -0
  337. package/skills/xlsx/scripts/office/validators/docx.py +446 -0
  338. package/skills/xlsx/scripts/office/validators/pptx.py +275 -0
  339. package/skills/xlsx/scripts/office/validators/redlining.py +247 -0
  340. package/skills/xlsx/scripts/recalc.py +184 -0
  341. package/telegram-plugin/.claude-plugin/plugin.json +20 -0
  342. package/telegram-plugin/.mcp.json +14 -0
  343. package/telegram-plugin/LICENSE +21 -0
  344. package/telegram-plugin/README.md +352 -0
  345. package/telegram-plugin/active-pins-sweep.ts +204 -0
  346. package/telegram-plugin/active-pins.ts +146 -0
  347. package/telegram-plugin/active-reactions-sweep.ts +79 -0
  348. package/telegram-plugin/active-reactions.ts +134 -0
  349. package/telegram-plugin/admin-commands/dispatch.test.ts +149 -0
  350. package/telegram-plugin/admin-commands/index.ts +106 -0
  351. package/telegram-plugin/answer-stream.ts +565 -0
  352. package/telegram-plugin/ask-user.ts +179 -0
  353. package/telegram-plugin/attachment-path.ts +80 -0
  354. package/telegram-plugin/auth-code-redact.ts +83 -0
  355. package/telegram-plugin/auth-dashboard.ts +1104 -0
  356. package/telegram-plugin/auth-slot-parser.ts +497 -0
  357. package/telegram-plugin/auto-fallback-dispatcher.ts +68 -0
  358. package/telegram-plugin/auto-fallback.ts +348 -0
  359. package/telegram-plugin/bridge/bridge.ts +687 -0
  360. package/telegram-plugin/bridge/ipc-client.ts +326 -0
  361. package/telegram-plugin/bun.lock +218 -0
  362. package/telegram-plugin/card-format.ts +62 -0
  363. package/telegram-plugin/channel-envelope-safety.test.ts +56 -0
  364. package/telegram-plugin/channel-envelope-safety.ts +56 -0
  365. package/telegram-plugin/chat-lock.ts +65 -0
  366. package/telegram-plugin/context-exhaustion.ts +38 -0
  367. package/telegram-plugin/credits-watch.ts +220 -0
  368. package/telegram-plugin/dist/bridge/bridge.js +24758 -0
  369. package/telegram-plugin/dist/foreman/foreman.js +30723 -0
  370. package/telegram-plugin/dist/gateway/gateway.js +46497 -0
  371. package/telegram-plugin/dist/server.js +24551 -0
  372. package/telegram-plugin/dm-command-gate.ts +56 -0
  373. package/telegram-plugin/docs/gateway-server-split.md +133 -0
  374. package/telegram-plugin/docs/multi-agent-card-design.md +847 -0
  375. package/telegram-plugin/docs/pinned-progress-card-reliability.md +144 -0
  376. package/telegram-plugin/docs/stream-json-daemon-mode.md +477 -0
  377. package/telegram-plugin/docs/waiting-ux-spec.md +233 -0
  378. package/telegram-plugin/draft-stream.ts +442 -0
  379. package/telegram-plugin/draft-transport.ts +72 -0
  380. package/telegram-plugin/first-paint.ts +246 -0
  381. package/telegram-plugin/fleet-state.ts +246 -0
  382. package/telegram-plugin/foreman/foreman-create-flow.ts +202 -0
  383. package/telegram-plugin/foreman/foreman-handlers.ts +493 -0
  384. package/telegram-plugin/foreman/foreman.ts +1130 -0
  385. package/telegram-plugin/foreman/setup-flow.ts +345 -0
  386. package/telegram-plugin/foreman/setup-state.ts +239 -0
  387. package/telegram-plugin/foreman/state.ts +203 -0
  388. package/telegram-plugin/format.ts +685 -0
  389. package/telegram-plugin/gateway/access-validator.test.ts +95 -0
  390. package/telegram-plugin/gateway/access-validator.ts +37 -0
  391. package/telegram-plugin/gateway/boot-card.ts +582 -0
  392. package/telegram-plugin/gateway/boot-probes.ts +863 -0
  393. package/telegram-plugin/gateway/boot-reason.ts +51 -0
  394. package/telegram-plugin/gateway/boot-sweep-filter.test.ts +54 -0
  395. package/telegram-plugin/gateway/boot-sweep-filter.ts +32 -0
  396. package/telegram-plugin/gateway/clean-shutdown-marker.ts +183 -0
  397. package/telegram-plugin/gateway/disconnect-flush.ts +109 -0
  398. package/telegram-plugin/gateway/gateway.ts +10202 -0
  399. package/telegram-plugin/gateway/inbound-coalesce.ts +147 -0
  400. package/telegram-plugin/gateway/inject-handler.test.ts +221 -0
  401. package/telegram-plugin/gateway/inject-handler.ts +190 -0
  402. package/telegram-plugin/gateway/ipc-protocol.ts +151 -0
  403. package/telegram-plugin/gateway/ipc-server.ts +494 -0
  404. package/telegram-plugin/gateway/pid-file.ts +103 -0
  405. package/telegram-plugin/gateway/poll-health.ts +156 -0
  406. package/telegram-plugin/gateway/preamble-suppressor.ts +154 -0
  407. package/telegram-plugin/gateway/quota-cache.ts +125 -0
  408. package/telegram-plugin/gateway/resolve-calling-subagent.ts +78 -0
  409. package/telegram-plugin/gateway/restart-watchdog.ts +200 -0
  410. package/telegram-plugin/gateway/session-marker.ts +83 -0
  411. package/telegram-plugin/gateway/shutdown-drain.ts +162 -0
  412. package/telegram-plugin/gateway/startup-mutex.ts +285 -0
  413. package/telegram-plugin/gateway/startup-network-retry.ts +142 -0
  414. package/telegram-plugin/gateway/turn-active-marker.ts +176 -0
  415. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +78 -0
  416. package/telegram-plugin/handoff-continuity.ts +200 -0
  417. package/telegram-plugin/history.ts +468 -0
  418. package/telegram-plugin/hooks/hooks.json +58 -0
  419. package/telegram-plugin/hooks/secret-guard-pretool.mjs +208 -0
  420. package/telegram-plugin/hooks/secret-scrub-stop.mjs +98 -0
  421. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +111 -0
  422. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +296 -0
  423. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +261 -0
  424. package/telegram-plugin/html-sanitize.ts +244 -0
  425. package/telegram-plugin/idle-footer.ts +65 -0
  426. package/telegram-plugin/inline-keyboard-callbacks.ts +166 -0
  427. package/telegram-plugin/interrupt-marker.ts +66 -0
  428. package/telegram-plugin/issues-card.ts +371 -0
  429. package/telegram-plugin/issues-watcher.ts +125 -0
  430. package/telegram-plugin/model-unavailable.ts +325 -0
  431. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  432. package/telegram-plugin/operator-events-history.ts +94 -0
  433. package/telegram-plugin/operator-events.fixtures.json +161 -0
  434. package/telegram-plugin/operator-events.ts +421 -0
  435. package/telegram-plugin/package.json +55 -0
  436. package/telegram-plugin/permission-rule.ts +133 -0
  437. package/telegram-plugin/permission-title.ts +117 -0
  438. package/telegram-plugin/pin-event-log.ts +76 -0
  439. package/telegram-plugin/plugin-logger.ts +136 -0
  440. package/telegram-plugin/progress-card-driver.ts +2697 -0
  441. package/telegram-plugin/progress-card-pin-manager.ts +589 -0
  442. package/telegram-plugin/progress-card-pin-watchdog.ts +98 -0
  443. package/telegram-plugin/progress-card.ts +1409 -0
  444. package/telegram-plugin/pty-partial-handler.ts +247 -0
  445. package/telegram-plugin/pty-tail.ts +730 -0
  446. package/telegram-plugin/quota-check.ts +474 -0
  447. package/telegram-plugin/recent-outbound-dedup.ts +169 -0
  448. package/telegram-plugin/registry/api-registry.test.ts +201 -0
  449. package/telegram-plugin/registry/subagents-bugs.test.ts +454 -0
  450. package/telegram-plugin/registry/subagents-schema.ts +509 -0
  451. package/telegram-plugin/registry/subagents.test.ts +476 -0
  452. package/telegram-plugin/registry/turns-schema.test.ts +101 -0
  453. package/telegram-plugin/registry/turns-schema.ts +417 -0
  454. package/telegram-plugin/retry-api-call.ts +172 -0
  455. package/telegram-plugin/scripts/build.mjs +78 -0
  456. package/telegram-plugin/secret-detect/audit.ts +66 -0
  457. package/telegram-plugin/secret-detect/chunker.ts +37 -0
  458. package/telegram-plugin/secret-detect/entropy.ts +20 -0
  459. package/telegram-plugin/secret-detect/gitleaks-loader.ts +74 -0
  460. package/telegram-plugin/secret-detect/gitleaks.toml +27 -0
  461. package/telegram-plugin/secret-detect/index.ts +218 -0
  462. package/telegram-plugin/secret-detect/kv-scanner.ts +60 -0
  463. package/telegram-plugin/secret-detect/mask.ts +13 -0
  464. package/telegram-plugin/secret-detect/patterns.ts +115 -0
  465. package/telegram-plugin/secret-detect/pipeline.ts +144 -0
  466. package/telegram-plugin/secret-detect/rewrite.ts +26 -0
  467. package/telegram-plugin/secret-detect/secretlint-source.ts +95 -0
  468. package/telegram-plugin/secret-detect/slug.ts +44 -0
  469. package/telegram-plugin/secret-detect/staging.ts +85 -0
  470. package/telegram-plugin/secret-detect/suppressor.ts +34 -0
  471. package/telegram-plugin/secret-detect/url-redact.ts +60 -0
  472. package/telegram-plugin/secret-detect/vault-write.ts +56 -0
  473. package/telegram-plugin/server.js +41795 -0
  474. package/telegram-plugin/server.ts +171 -0
  475. package/telegram-plugin/session-tail.ts +884 -0
  476. package/telegram-plugin/shared/bot-runtime.ts +324 -0
  477. package/telegram-plugin/silent-reply.ts +58 -0
  478. package/telegram-plugin/slot-banner-driver.ts +147 -0
  479. package/telegram-plugin/slot-banner.ts +86 -0
  480. package/telegram-plugin/start.js +26 -0
  481. package/telegram-plugin/startup-reset.ts +45 -0
  482. package/telegram-plugin/status-reactions.ts +332 -0
  483. package/telegram-plugin/steering.ts +155 -0
  484. package/telegram-plugin/sticker-aliases.ts +249 -0
  485. package/telegram-plugin/stream-controller.ts +311 -0
  486. package/telegram-plugin/stream-reply-handler.ts +664 -0
  487. package/telegram-plugin/streaming-metrics.ts +134 -0
  488. package/telegram-plugin/streaming-report.ts +204 -0
  489. package/telegram-plugin/subagent-watcher.ts +880 -0
  490. package/telegram-plugin/telegram-button-constraints.ts +191 -0
  491. package/telegram-plugin/telegraph.ts +381 -0
  492. package/telegram-plugin/tests/HARNESS.md +340 -0
  493. package/telegram-plugin/tests/_progress-card-harness.ts +105 -0
  494. package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +211 -0
  495. package/telegram-plugin/tests/active-pins-sweep.test.ts +309 -0
  496. package/telegram-plugin/tests/active-pins.test.ts +187 -0
  497. package/telegram-plugin/tests/active-reactions-sweep.test.ts +116 -0
  498. package/telegram-plugin/tests/active-reactions.test.ts +198 -0
  499. package/telegram-plugin/tests/answer-stream-dedup.test.ts +352 -0
  500. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +236 -0
  501. package/telegram-plugin/tests/answer-stream.test.ts +878 -0
  502. package/telegram-plugin/tests/ask-user.test.ts +203 -0
  503. package/telegram-plugin/tests/attachment-path.test.ts +199 -0
  504. package/telegram-plugin/tests/auth-account-identity-surface.test.ts +118 -0
  505. package/telegram-plugin/tests/auth-code-auto-capture.test.ts +144 -0
  506. package/telegram-plugin/tests/auth-code-redact.test.ts +248 -0
  507. package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +260 -0
  508. package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +140 -0
  509. package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +559 -0
  510. package/telegram-plugin/tests/auth-dashboard.test.ts +1045 -0
  511. package/telegram-plugin/tests/auth-login-url-button.test.ts +122 -0
  512. package/telegram-plugin/tests/auth-slot-commands.test.ts +640 -0
  513. package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +183 -0
  514. package/telegram-plugin/tests/auto-fallback.test.ts +381 -0
  515. package/telegram-plugin/tests/boot-card-account-quota.test.ts +137 -0
  516. package/telegram-plugin/tests/boot-card-dedupe.test.ts +154 -0
  517. package/telegram-plugin/tests/boot-card-probe-target.test.ts +194 -0
  518. package/telegram-plugin/tests/boot-card-reason.test.ts +103 -0
  519. package/telegram-plugin/tests/boot-card-render.test.ts +219 -0
  520. package/telegram-plugin/tests/boot-probes.test.ts +451 -0
  521. package/telegram-plugin/tests/bot-api.harness.ts +116 -0
  522. package/telegram-plugin/tests/bot-runtime.test.ts +190 -0
  523. package/telegram-plugin/tests/bridge-anonymous-refuse.test.ts +60 -0
  524. package/telegram-plugin/tests/context-exhaustion.test.ts +114 -0
  525. package/telegram-plugin/tests/credits-watch.test.ts +221 -0
  526. package/telegram-plugin/tests/dm-command-gate.test.ts +176 -0
  527. package/telegram-plugin/tests/draft-stream.test.ts +752 -0
  528. package/telegram-plugin/tests/draft-transport.test.ts +141 -0
  529. package/telegram-plugin/tests/e2e.test.ts +436 -0
  530. package/telegram-plugin/tests/fake-bot-api.test.ts +213 -0
  531. package/telegram-plugin/tests/fake-bot-api.ts +617 -0
  532. package/telegram-plugin/tests/false-restart-banner.test.ts +253 -0
  533. package/telegram-plugin/tests/first-paint.test.ts +257 -0
  534. package/telegram-plugin/tests/fixtures/pty-tail-tmux-fragment.bin +6 -0
  535. package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +3624 -0
  536. package/telegram-plugin/tests/fleet-state-watcher.test.ts +101 -0
  537. package/telegram-plugin/tests/fleet-state.test.ts +185 -0
  538. package/telegram-plugin/tests/foreman-create-flow.test.ts +359 -0
  539. package/telegram-plugin/tests/foreman-handlers.test.ts +347 -0
  540. package/telegram-plugin/tests/foreman-state.test.ts +164 -0
  541. package/telegram-plugin/tests/foreman-write-ops.test.ts +214 -0
  542. package/telegram-plugin/tests/gateway-409-retry-banner.test.ts +173 -0
  543. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +72 -0
  544. package/telegram-plugin/tests/gateway-bridge.test.ts +811 -0
  545. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +414 -0
  546. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +144 -0
  547. package/telegram-plugin/tests/gateway-message-validator.test.ts +133 -0
  548. package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +103 -0
  549. package/telegram-plugin/tests/gateway-secret-detect.test.ts +127 -0
  550. package/telegram-plugin/tests/gateway-startup-mutex.test.ts +284 -0
  551. package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +185 -0
  552. package/telegram-plugin/tests/gateway-startup-reset.test.ts +72 -0
  553. package/telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts +125 -0
  554. package/telegram-plugin/tests/handoff-continuity.test.ts +249 -0
  555. package/telegram-plugin/tests/harness-ordering-invariants.test.ts +243 -0
  556. package/telegram-plugin/tests/harness-parse-mode-validation.test.ts +114 -0
  557. package/telegram-plugin/tests/history.test.ts +364 -0
  558. package/telegram-plugin/tests/html-balanced.ts +63 -0
  559. package/telegram-plugin/tests/html-sanitize.test.ts +146 -0
  560. package/telegram-plugin/tests/idle-footer-wiring.test.ts +88 -0
  561. package/telegram-plugin/tests/idle-footer.test.ts +66 -0
  562. package/telegram-plugin/tests/inbound-coalesce.test.ts +127 -0
  563. package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +150 -0
  564. package/telegram-plugin/tests/interrupt-marker.test.ts +126 -0
  565. package/telegram-plugin/tests/ipc-protocol.test.ts +218 -0
  566. package/telegram-plugin/tests/ipc-server-anonymous-refuse.test.ts +82 -0
  567. package/telegram-plugin/tests/ipc-server-client.test.ts +323 -0
  568. package/telegram-plugin/tests/ipc-server-race.test.ts +183 -0
  569. package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +91 -0
  570. package/telegram-plugin/tests/ipc-server-validate-pty-partial.test.ts +64 -0
  571. package/telegram-plugin/tests/ipc-server-validate-update-placeholder.test.ts +77 -0
  572. package/telegram-plugin/tests/ipc-validator.test.ts +274 -0
  573. package/telegram-plugin/tests/issues-card.test.ts +495 -0
  574. package/telegram-plugin/tests/issues-watcher.test.ts +165 -0
  575. package/telegram-plugin/tests/model-unavailable.test.ts +303 -0
  576. package/telegram-plugin/tests/multi-turn-continuity.test.ts +159 -0
  577. package/telegram-plugin/tests/operator-events-history.test.ts +125 -0
  578. package/telegram-plugin/tests/operator-events-session-tail.test.ts +192 -0
  579. package/telegram-plugin/tests/operator-events.test.ts +331 -0
  580. package/telegram-plugin/tests/outbound-ordering.test.ts +293 -0
  581. package/telegram-plugin/tests/parse-mode-rotation.test.ts +164 -0
  582. package/telegram-plugin/tests/permission-rule.test.ts +121 -0
  583. package/telegram-plugin/tests/permission-title.test.ts +106 -0
  584. package/telegram-plugin/tests/pin-event-log.test.ts +124 -0
  585. package/telegram-plugin/tests/plugin-logger.test.ts +97 -0
  586. package/telegram-plugin/tests/poll-health.test.ts +86 -0
  587. package/telegram-plugin/tests/preamble-suppressor.test.ts +285 -0
  588. package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +73 -0
  589. package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +272 -0
  590. package/telegram-plugin/tests/progress-card-cross-turn.test.ts +258 -0
  591. package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +81 -0
  592. package/telegram-plugin/tests/progress-card-draft-flag.test.ts +80 -0
  593. package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +215 -0
  594. package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +123 -0
  595. package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +76 -0
  596. package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +62 -0
  597. package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +84 -0
  598. package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +139 -0
  599. package/telegram-plugin/tests/progress-card-pin-manager.test.ts +773 -0
  600. package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +66 -0
  601. package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +64 -0
  602. package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +190 -0
  603. package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +146 -0
  604. package/telegram-plugin/tests/progress-update.test.ts +236 -0
  605. package/telegram-plugin/tests/protocol-fixtures.test.ts +59 -0
  606. package/telegram-plugin/tests/protocol-fixtures.ts +198 -0
  607. package/telegram-plugin/tests/pty-partial-handler.test.ts +326 -0
  608. package/telegram-plugin/tests/pty-tail-real-fixture.test.ts +114 -0
  609. package/telegram-plugin/tests/pty-tail-tmux-fragment.test.ts +71 -0
  610. package/telegram-plugin/tests/pty-tail.test.ts +525 -0
  611. package/telegram-plugin/tests/quota-cache.test.ts +187 -0
  612. package/telegram-plugin/tests/quota-check.test.ts +622 -0
  613. package/telegram-plugin/tests/races.test.ts +842 -0
  614. package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +123 -0
  615. package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +82 -0
  616. package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +114 -0
  617. package/telegram-plugin/tests/real-gateway-harness.ts +699 -0
  618. package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +313 -0
  619. package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +299 -0
  620. package/telegram-plugin/tests/real-gateway-spec.test.ts +487 -0
  621. package/telegram-plugin/tests/real-gateway.smoke.test.ts +101 -0
  622. package/telegram-plugin/tests/recent-outbound-dedup.test.ts +192 -0
  623. package/telegram-plugin/tests/registry-turns.test.ts +432 -0
  624. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +149 -0
  625. package/telegram-plugin/tests/resolve-calling-subagent.test.ts +269 -0
  626. package/telegram-plugin/tests/restart-watchdog.test.ts +224 -0
  627. package/telegram-plugin/tests/retry-api-call.test.ts +287 -0
  628. package/telegram-plugin/tests/secret-detect-audit.test.ts +58 -0
  629. package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +83 -0
  630. package/telegram-plugin/tests/secret-detect-gitleaks.test.ts +32 -0
  631. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +308 -0
  632. package/telegram-plugin/tests/secret-detect-pipeline.test.ts +123 -0
  633. package/telegram-plugin/tests/secret-detect-secretlint.test.ts +101 -0
  634. package/telegram-plugin/tests/secret-detect-staging.test.ts +45 -0
  635. package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +67 -0
  636. package/telegram-plugin/tests/secret-detect.test.ts +223 -0
  637. package/telegram-plugin/tests/secret-guard-pretool.test.ts +194 -0
  638. package/telegram-plugin/tests/send-typing-action-validation.test.ts +61 -0
  639. package/telegram-plugin/tests/session-tail-capped.test.ts +285 -0
  640. package/telegram-plugin/tests/session-tail.test.ts +524 -0
  641. package/telegram-plugin/tests/setup-flow.test.ts +510 -0
  642. package/telegram-plugin/tests/setup-state.test.ts +146 -0
  643. package/telegram-plugin/tests/silent-reply-guard.test.ts +122 -0
  644. package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +350 -0
  645. package/telegram-plugin/tests/slot-banner.test.ts +74 -0
  646. package/telegram-plugin/tests/snapshot-serializer.ts +79 -0
  647. package/telegram-plugin/tests/spawn-detached-cgroup-escape.test.ts +51 -0
  648. package/telegram-plugin/tests/status-accent.test.ts +186 -0
  649. package/telegram-plugin/tests/status-reactions-allowed-filter.test.ts +132 -0
  650. package/telegram-plugin/tests/status-reactions.test.ts +314 -0
  651. package/telegram-plugin/tests/steering.test.ts +282 -0
  652. package/telegram-plugin/tests/sticker-aliases.test.ts +232 -0
  653. package/telegram-plugin/tests/stream-controller-html-fallback.test.ts +127 -0
  654. package/telegram-plugin/tests/stream-controller.test.ts +262 -0
  655. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +208 -0
  656. package/telegram-plugin/tests/stream-reply-handler.test.ts +1292 -0
  657. package/telegram-plugin/tests/streaming-e2e.test.ts +389 -0
  658. package/telegram-plugin/tests/streaming-metrics.test.ts +201 -0
  659. package/telegram-plugin/tests/streaming-orchestration.test.ts +756 -0
  660. package/telegram-plugin/tests/subagent-registry-bugs.test.ts +725 -0
  661. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +213 -0
  662. package/telegram-plugin/tests/subagent-watcher-parent-marker.test.ts +274 -0
  663. package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +243 -0
  664. package/telegram-plugin/tests/subagent-watcher.test.ts +877 -0
  665. package/telegram-plugin/tests/subagents-schema-init-order.test.ts +109 -0
  666. package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +116 -0
  667. package/telegram-plugin/tests/telegram-button-constraints.test.ts +194 -0
  668. package/telegram-plugin/tests/telegram-format.test.ts +1093 -0
  669. package/telegram-plugin/tests/telegraph.test.ts +246 -0
  670. package/telegram-plugin/tests/tool-labels.test.ts +383 -0
  671. package/telegram-plugin/tests/turn-active-marker.test.ts +195 -0
  672. package/telegram-plugin/tests/turn-end-regressions.test.ts +489 -0
  673. package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +218 -0
  674. package/telegram-plugin/tests/turn-flush-dedup-controller.test.ts +144 -0
  675. package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +78 -0
  676. package/telegram-plugin/tests/turn-flush-safety.test.ts +189 -0
  677. package/telegram-plugin/tests/turn-signal-tracker.test.ts +107 -0
  678. package/telegram-plugin/tests/turns-writer.test.ts +323 -0
  679. package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +131 -0
  680. package/telegram-plugin/tests/two-zone-bg-detection.test.ts +120 -0
  681. package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +114 -0
  682. package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +87 -0
  683. package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +211 -0
  684. package/telegram-plugin/tests/two-zone-card-cap.test.ts +62 -0
  685. package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +101 -0
  686. package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +68 -0
  687. package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +110 -0
  688. package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +128 -0
  689. package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +58 -0
  690. package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +133 -0
  691. package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +155 -0
  692. package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +117 -0
  693. package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +143 -0
  694. package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +149 -0
  695. package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +101 -0
  696. package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +114 -0
  697. package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +105 -0
  698. package/telegram-plugin/tests/typing-wrap.test.ts +141 -0
  699. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +147 -0
  700. package/telegram-plugin/tests/update-factory-edited-and-reactions.test.ts +108 -0
  701. package/telegram-plugin/tests/update-factory.ts +305 -0
  702. package/telegram-plugin/tests/vault-grant-wizard.test.ts +84 -0
  703. package/telegram-plugin/tests/vault-grants-revoke.test.ts +265 -0
  704. package/telegram-plugin/tests/vault-subcommands.test.ts +234 -0
  705. package/telegram-plugin/tests/voice-transcribe.test.ts +196 -0
  706. package/telegram-plugin/tests/waiting-ux-harness.ts +381 -0
  707. package/telegram-plugin/tests/waiting-ux.e2e.test.ts +233 -0
  708. package/telegram-plugin/tests/welcome-text.test.ts +407 -0
  709. package/telegram-plugin/tool-error-filter.ts +89 -0
  710. package/telegram-plugin/tool-labels.ts +330 -0
  711. package/telegram-plugin/tool-names.ts +53 -0
  712. package/telegram-plugin/turn-flush-prose-recovery.ts +40 -0
  713. package/telegram-plugin/turn-flush-safety.ts +109 -0
  714. package/telegram-plugin/turn-signal-tracker.ts +79 -0
  715. package/telegram-plugin/two-zone-card.ts +249 -0
  716. package/telegram-plugin/typing-wrap.ts +92 -0
  717. package/telegram-plugin/voice-transcribe.ts +166 -0
  718. package/telegram-plugin/welcome-text.ts +359 -0
@@ -0,0 +1,1292 @@
1
+ /**
2
+ * Integration tests for the `stream_reply` MCP tool handler.
3
+ *
4
+ * Exercises the extracted `handleStreamReply` against the mock bot harness
5
+ * with realistic deps (format rendering, access check, thread resolution,
6
+ * handoff prefix, history record).
7
+ */
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
9
+ import {
10
+ handleStreamReply,
11
+ type StreamReplyDeps,
12
+ type StreamReplyState,
13
+ } from '../stream-reply-handler.js'
14
+ import type { DraftStreamHandle } from '../draft-stream.js'
15
+ import { markdownToHtml as realMarkdownToHtml } from '../format.js'
16
+ import { createMockBot, installBotResetHook, microtaskFlush } from './bot-api.harness.js'
17
+ import {
18
+ handlePtyPartialPure,
19
+ type PtyHandlerState,
20
+ } from '../pty-partial-handler.js'
21
+
22
+ function makeState(): StreamReplyState {
23
+ return {
24
+ activeDraftStreams: new Map<string, DraftStreamHandle>(),
25
+ activeDraftParseModes: new Map<string, 'HTML' | 'MarkdownV2' | undefined>(),
26
+ }
27
+ }
28
+
29
+ function makeDeps(
30
+ bot: ReturnType<typeof createMockBot>,
31
+ overrides?: Partial<StreamReplyDeps>,
32
+ ): StreamReplyDeps {
33
+ return {
34
+ bot,
35
+ markdownToHtml: (t) => `<b>${t}</b>`,
36
+ escapeMarkdownV2: (t) => `\\${t}\\`,
37
+ repairEscapedWhitespace: (t) => t,
38
+ takeHandoffPrefix: () => '',
39
+ assertAllowedChat: () => {},
40
+ resolveThreadId: (_, explicit) => (explicit != null ? Number(explicit) : undefined),
41
+ disableLinkPreview: true,
42
+ defaultFormat: 'html',
43
+ logStreamingEvent: () => {},
44
+ endStatusReaction: () => {},
45
+ historyEnabled: false,
46
+ recordOutbound: () => {},
47
+ writeError: () => {},
48
+ throttleMs: 600,
49
+ ...overrides,
50
+ }
51
+ }
52
+
53
+ describe('handleStreamReply', () => {
54
+ const bot = createMockBot()
55
+ installBotResetHook(bot)
56
+
57
+ beforeEach(() => vi.useFakeTimers())
58
+ afterEach(() => vi.useRealTimers())
59
+
60
+ it('first call creates stream + sends with rendered HTML text', async () => {
61
+ const state = makeState()
62
+ const deps = makeDeps(bot)
63
+
64
+ const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
65
+ await microtaskFlush()
66
+ const result = await pending
67
+
68
+ expect(result.status).toBe('updated')
69
+ expect(result.messageId).toBe(500)
70
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
71
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>hi</b>')
72
+ expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBe('HTML')
73
+ expect(state.activeDraftStreams.size).toBe(1)
74
+ })
75
+
76
+ it('respects format=markdownv2 — uses MDv2 escaper and parse_mode', async () => {
77
+ const state = makeState()
78
+ const deps = makeDeps(bot)
79
+
80
+ const pending = handleStreamReply(
81
+ { chat_id: '1', text: 'hi', format: 'markdownv2' },
82
+ state,
83
+ deps,
84
+ )
85
+ await microtaskFlush()
86
+ await pending
87
+
88
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('\\hi\\')
89
+ expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBe('MarkdownV2')
90
+ })
91
+
92
+ it('respects format=text — no parse_mode, raw text', async () => {
93
+ const state = makeState()
94
+ const deps = makeDeps(bot)
95
+
96
+ const pending = handleStreamReply(
97
+ { chat_id: '1', text: 'plain', format: 'text' },
98
+ state,
99
+ deps,
100
+ )
101
+ await microtaskFlush()
102
+ await pending
103
+
104
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('plain')
105
+ expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBeUndefined()
106
+ })
107
+
108
+ it('prepends handoff prefix on first chunk only', async () => {
109
+ const state = makeState()
110
+ const deps = makeDeps(bot, {
111
+ takeHandoffPrefix: vi.fn<(fmt: string) => string>(() => '↩️ '),
112
+ })
113
+
114
+ // First call: prefix applied
115
+ const p1 = handleStreamReply({ chat_id: '1', text: 'first' }, state, deps)
116
+ await microtaskFlush()
117
+ await p1
118
+ // Prefix is prepended AFTER format rendering (it's already format-safe
119
+ // because takeHandoffPrefix takes the format tag).
120
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('↩️ <b>first</b>')
121
+
122
+ // Second call: handoff not consumed again
123
+ vi.advanceTimersByTime(1000)
124
+ const p2 = handleStreamReply({ chat_id: '1', text: 'second' }, state, deps)
125
+ await microtaskFlush()
126
+ await p2
127
+ expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>second</b>')
128
+ expect(deps.takeHandoffPrefix).toHaveBeenCalledTimes(1)
129
+ })
130
+
131
+ it('throws when text exceeds 4096 (no silent id:pending)', async () => {
132
+ // Pins the bug found in prod: a >4096-char text would hit draft-
133
+ // stream's length guard, silently stop, and the handler would return
134
+ // status:finalized, messageId:null — the MCP response read
135
+ // "finalized (id: pending)" looking like success. Fixed upstream by
136
+ // an over-limit pre-check that throws BEFORE touching stream state,
137
+ // so both first-send-over-limit AND mid-stream-over-limit fail loudly
138
+ // instead of corrupting the stream. done=true not required.
139
+ const state = makeState()
140
+ const deps = makeDeps(bot)
141
+ const tooLong = 'x'.repeat(5000)
142
+
143
+ await expect(
144
+ handleStreamReply({ chat_id: '1', text: tooLong, done: true }, state, deps),
145
+ ).rejects.toThrow(/exceeds Telegram's 4096-char limit/)
146
+
147
+ // Mock bot should NOT have received any sendMessage call.
148
+ expect(bot.api.sendMessage).not.toHaveBeenCalled()
149
+ })
150
+
151
+ it('mid-stream over-limit throws without corrupting stream state', async () => {
152
+ // A stream that starts small but a later update() goes over 4096.
153
+ // Before the upfront length check, the draft-stream would set its
154
+ // internal stopped=true flag and silently drop all further text —
155
+ // including the done=true final answer. The pre-check now throws
156
+ // on the over-limit call, leaving the stream intact so the caller
157
+ // can fall back to `reply`. The previously-sent short text stays
158
+ // visible in Telegram; the throw is the signal to the caller.
159
+ const state = makeState()
160
+ const deps = makeDeps(bot)
161
+
162
+ await handleStreamReply(
163
+ { chat_id: '1', text: 'short' },
164
+ state,
165
+ deps,
166
+ )
167
+ await microtaskFlush()
168
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
169
+
170
+ // Second call: now over limit.
171
+ await expect(
172
+ handleStreamReply(
173
+ { chat_id: '1', text: 'y'.repeat(5000), done: true },
174
+ state,
175
+ deps,
176
+ ),
177
+ ).rejects.toThrow(/exceeds Telegram's 4096-char limit/)
178
+
179
+ // No additional API calls from the rejected update.
180
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
181
+ })
182
+
183
+ it('done=true finalizes and fires terminal 👍 on default lane after finalize resolves', async () => {
184
+ // Bug Z fix: stream_reply(done=true) on the default (unnamed) lane
185
+ // now fires endStatusReaction('done') AFTER stream.finalize()
186
+ // resolves. This ties the 👍 emoji to actual Telegram delivery
187
+ // (the final draft edit landing) rather than to JSONL turn_end
188
+ // (which races the disconnect-flush and the dedup-suppress paths).
189
+ //
190
+ // Previously this test asserted endStatusReaction was NOT called,
191
+ // and the gateway turn_end handler was the sole 👍 emitter. That
192
+ // design left 👍 firing off either (a) a 500ms-lagged read of
193
+ // local history (turn-flush dedup branch), or (b) a disconnect
194
+ // event that may have fired before any verification of delivery.
195
+ const state = makeState()
196
+ const endStatusReaction = vi.fn()
197
+ const deps = makeDeps(bot, { endStatusReaction })
198
+
199
+ const pending = handleStreamReply(
200
+ { chat_id: '1', text: 'final', done: true },
201
+ state,
202
+ deps,
203
+ )
204
+ await microtaskFlush()
205
+ const result = await pending
206
+
207
+ expect(result.status).toBe('finalized')
208
+ expect(state.activeDraftStreams.size).toBe(0)
209
+ expect(endStatusReaction).toHaveBeenCalledTimes(1)
210
+ expect(endStatusReaction).toHaveBeenCalledWith('1', undefined, 'done')
211
+ })
212
+
213
+ it('done=true on a named lane does NOT fire terminal 👍', async () => {
214
+ // Named lanes (lane:'progress', lane:'thinking', lane:'activity'
215
+ // etc.) are internal driver emits, not user-visible answers. They
216
+ // must not be allowed to claim turn-completion: a progress-lane
217
+ // emit firing setDone would race the actual answer message.
218
+ const state = makeState()
219
+ const endStatusReaction = vi.fn()
220
+ const deps = makeDeps(bot, { endStatusReaction })
221
+
222
+ const pending = handleStreamReply(
223
+ { chat_id: '1', text: 'progress snapshot', done: true, lane: 'progress' },
224
+ state,
225
+ deps,
226
+ )
227
+ await microtaskFlush()
228
+ await pending
229
+
230
+ expect(endStatusReaction).not.toHaveBeenCalled()
231
+ })
232
+
233
+ it('done=true does NOT fire 👍 if finalize never produced a messageId', async () => {
234
+ // The over-limit branch throws before getMessageId() is non-null.
235
+ // Even if it didn't throw, a null messageId means the initial send
236
+ // never landed, so 👍 must not fire. Pinning that the gating on
237
+ // `getMessageId() != null` holds.
238
+ const state = makeState()
239
+ const endStatusReaction = vi.fn()
240
+ const deps = makeDeps(bot, { endStatusReaction })
241
+
242
+ await expect(
243
+ handleStreamReply(
244
+ { chat_id: '1', text: 'x'.repeat(5000), done: true },
245
+ state,
246
+ deps,
247
+ ),
248
+ ).rejects.toThrow(/exceeds Telegram's 4096-char limit/)
249
+
250
+ expect(endStatusReaction).not.toHaveBeenCalled()
251
+ })
252
+
253
+ it('done=true with historyEnabled records the final message row', async () => {
254
+ const state = makeState()
255
+ const recordOutbound = vi.fn()
256
+ const deps = makeDeps(bot, {
257
+ historyEnabled: true,
258
+ recordOutbound,
259
+ resolveThreadId: () => 42,
260
+ })
261
+
262
+ const pending = handleStreamReply(
263
+ { chat_id: '1', text: 'final text', done: true, message_thread_id: '42' },
264
+ state,
265
+ deps,
266
+ )
267
+ await microtaskFlush()
268
+ await pending
269
+
270
+ expect(recordOutbound).toHaveBeenCalledWith({
271
+ chat_id: '1',
272
+ thread_id: 42,
273
+ message_ids: [500],
274
+ texts: ['final text'], // raw text, NOT HTML-rendered
275
+ })
276
+ })
277
+
278
+ it('historyEnabled=false skips recordOutbound', async () => {
279
+ const state = makeState()
280
+ const recordOutbound = vi.fn()
281
+ const deps = makeDeps(bot, { historyEnabled: false, recordOutbound })
282
+
283
+ const pending = handleStreamReply(
284
+ { chat_id: '1', text: 'f', done: true },
285
+ state,
286
+ deps,
287
+ )
288
+ await microtaskFlush()
289
+ await pending
290
+
291
+ expect(recordOutbound).not.toHaveBeenCalled()
292
+ })
293
+
294
+ it('recordOutbound throws → error logged, handler still resolves finalized', async () => {
295
+ const state = makeState()
296
+ const writeError = vi.fn()
297
+ const recordOutbound = vi.fn(() => {
298
+ throw new Error('db locked')
299
+ })
300
+ const deps = makeDeps(bot, { historyEnabled: true, recordOutbound, writeError })
301
+
302
+ const pending = handleStreamReply(
303
+ { chat_id: '1', text: 'f', done: true },
304
+ state,
305
+ deps,
306
+ )
307
+ await microtaskFlush()
308
+ const result = await pending
309
+
310
+ expect(result.status).toBe('finalized')
311
+ expect(writeError).toHaveBeenCalledTimes(1)
312
+ expect(writeError.mock.calls[0][0]).toMatch(/db locked/)
313
+ })
314
+
315
+ it('rejects when assertAllowedChat throws', async () => {
316
+ const state = makeState()
317
+ const deps = makeDeps(bot, {
318
+ assertAllowedChat: () => { throw new Error('chat not allowed') },
319
+ })
320
+
321
+ await expect(
322
+ handleStreamReply({ chat_id: 'evil', text: 'x' }, state, deps),
323
+ ).rejects.toThrow('chat not allowed')
324
+
325
+ expect(bot.api.sendMessage).not.toHaveBeenCalled()
326
+ })
327
+
328
+ it('subsequent calls reuse the same stream + edit in place', async () => {
329
+ const state = makeState()
330
+ const deps = makeDeps(bot)
331
+
332
+ const p1 = handleStreamReply({ chat_id: '1', text: 'step 1' }, state, deps)
333
+ await microtaskFlush()
334
+ await p1
335
+ vi.advanceTimersByTime(1000)
336
+
337
+ const p2 = handleStreamReply({ chat_id: '1', text: 'step 2' }, state, deps)
338
+ await microtaskFlush()
339
+ await p2
340
+
341
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
342
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
343
+ expect(bot.api.editMessageText.mock.calls[0][1]).toBe(500) // same id
344
+ expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>step 2</b>')
345
+ })
346
+
347
+ it('passes repairEscapedWhitespace through before rendering', async () => {
348
+ const state = makeState()
349
+ const deps = makeDeps(bot, {
350
+ repairEscapedWhitespace: (t) => t.replace(/\\n/g, '\n'),
351
+ })
352
+
353
+ const pending = handleStreamReply({ chat_id: '1', text: 'a\\nb' }, state, deps)
354
+ await microtaskFlush()
355
+ await pending
356
+
357
+ // repair happens first; then markdownToHtml wraps the repaired text
358
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>a\nb</b>')
359
+ })
360
+
361
+ it('different lanes for same chat produce independent Telegram messages', async () => {
362
+ const state = makeState()
363
+ const deps = makeDeps(bot)
364
+
365
+ const p1 = handleStreamReply(
366
+ { chat_id: '1', text: 'thinking aloud', lane: 'thinking' },
367
+ state,
368
+ deps,
369
+ )
370
+ await microtaskFlush()
371
+ const r1 = await p1
372
+
373
+ const p2 = handleStreamReply(
374
+ { chat_id: '1', text: 'final answer' }, // no lane = answer
375
+ state,
376
+ deps,
377
+ )
378
+ await microtaskFlush()
379
+ const r2 = await p2
380
+
381
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
382
+ expect(r1.messageId).not.toBe(r2.messageId) // separate messages
383
+ expect(state.activeDraftStreams.size).toBe(2)
384
+ expect(state.activeDraftStreams.has('1:_')).toBe(true)
385
+ expect(state.activeDraftStreams.has('1:_:thinking')).toBe(true)
386
+ })
387
+
388
+ it('same lane updates the same message (no duplicate send per lane)', async () => {
389
+ const state = makeState()
390
+ const deps = makeDeps(bot)
391
+
392
+ const p1 = handleStreamReply(
393
+ { chat_id: '1', text: 'step 1', lane: 'thinking' },
394
+ state,
395
+ deps,
396
+ )
397
+ await microtaskFlush()
398
+ await p1
399
+
400
+ vi.advanceTimersByTime(1000)
401
+ const p2 = handleStreamReply(
402
+ { chat_id: '1', text: 'step 1 — step 2', lane: 'thinking' },
403
+ state,
404
+ deps,
405
+ )
406
+ await microtaskFlush()
407
+ await p2
408
+
409
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
410
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
411
+ expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>step 1 — step 2</b>')
412
+ })
413
+
414
+ it('done=true on one lane does not affect other lanes', async () => {
415
+ const state = makeState()
416
+ const deps = makeDeps(bot)
417
+
418
+ const pThink = handleStreamReply(
419
+ { chat_id: '1', text: 'thinking', lane: 'thinking', done: true },
420
+ state,
421
+ deps,
422
+ )
423
+ await microtaskFlush()
424
+ await pThink
425
+
426
+ const pAnswer = handleStreamReply(
427
+ { chat_id: '1', text: 'answering' }, // still in progress
428
+ state,
429
+ deps,
430
+ )
431
+ await microtaskFlush()
432
+ await pAnswer
433
+
434
+ expect(state.activeDraftStreams.has('1:_:thinking')).toBe(false)
435
+ expect(state.activeDraftStreams.has('1:_')).toBe(true)
436
+ })
437
+
438
+ // ─── Regression: concurrent turns on the same chat+thread+lane ───────
439
+ // Before the fix, two simultaneously active turns emitting on
440
+ // lane:'progress' (the progress-card driver's lane) computed the same
441
+ // streamKey and collapsed into one draft stream. Telegram saw a single
442
+ // message flapping between the two turns' narratives instead of two
443
+ // separate pinned cards. The fix threads a per-turn `turnKey` through
444
+ // `StreamReplyArgs` → `streamKey()` so each active turn gets its own
445
+ // slot in `activeDraftStreams` (and therefore its own Telegram message
446
+ // and its own pin via `progressPinnedMsgIds`).
447
+ it('concurrent turns with different turnKeys produce separate draft streams and messages', async () => {
448
+ const state = makeState()
449
+ const deps = makeDeps(bot)
450
+
451
+ // Turn A: progress lane, turnKey "1:_:1"
452
+ const pA = handleStreamReply(
453
+ { chat_id: '1', text: 'turn A step 1', lane: 'progress', turnKey: '1:_:1' },
454
+ state,
455
+ deps,
456
+ )
457
+ await microtaskFlush()
458
+ const rA = await pA
459
+
460
+ // Turn B: progress lane, same chat+thread+lane but DIFFERENT turnKey
461
+ const pB = handleStreamReply(
462
+ { chat_id: '1', text: 'turn B step 1', lane: 'progress', turnKey: '1:_:2' },
463
+ state,
464
+ deps,
465
+ )
466
+ await microtaskFlush()
467
+ const rB = await pB
468
+
469
+ // Two independent Telegram messages (not one edited twice).
470
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
471
+ expect(bot.api.editMessageText).not.toHaveBeenCalled()
472
+ expect(rA.messageId).not.toBe(rB.messageId)
473
+
474
+ // Two independent draft streams in state, each keyed by turnKey.
475
+ expect(state.activeDraftStreams.size).toBe(2)
476
+ expect(state.activeDraftStreams.has('1:_:progress:1:_:1')).toBe(true)
477
+ expect(state.activeDraftStreams.has('1:_:progress:1:_:2')).toBe(true)
478
+
479
+ // Each message carried its own turn's text.
480
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>turn A step 1</b>')
481
+ expect(bot.api.sendMessage.mock.calls[1][1]).toBe('<b>turn B step 1</b>')
482
+ })
483
+
484
+ it('subsequent updates with same turnKey reuse the stream (edit in place)', async () => {
485
+ const state = makeState()
486
+ const deps = makeDeps(bot)
487
+
488
+ const p1 = handleStreamReply(
489
+ { chat_id: '1', text: 'A first', lane: 'progress', turnKey: '1:_:1' },
490
+ state,
491
+ deps,
492
+ )
493
+ await microtaskFlush()
494
+ await p1
495
+
496
+ vi.advanceTimersByTime(1000)
497
+
498
+ const p2 = handleStreamReply(
499
+ { chat_id: '1', text: 'A first + second', lane: 'progress', turnKey: '1:_:1' },
500
+ state,
501
+ deps,
502
+ )
503
+ await microtaskFlush()
504
+ await p2
505
+
506
+ // One send (first call) + one edit (second call) on the same message.
507
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
508
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
509
+ expect(bot.api.editMessageText.mock.calls[0][1]).toBe(500)
510
+ expect(bot.api.editMessageText.mock.calls[0][2]).toBe('<b>A first + second</b>')
511
+ expect(state.activeDraftStreams.size).toBe(1)
512
+ expect(state.activeDraftStreams.has('1:_:progress:1:_:1')).toBe(true)
513
+ })
514
+
515
+ it('interleaved concurrent turns each update their own message independently', async () => {
516
+ const state = makeState()
517
+ const deps = makeDeps(bot)
518
+
519
+ // Turn A opens
520
+ const pa1 = handleStreamReply(
521
+ { chat_id: '1', text: 'A step 1', lane: 'progress', turnKey: '1:_:1' },
522
+ state, deps,
523
+ )
524
+ await microtaskFlush()
525
+ await pa1
526
+
527
+ // Turn B opens
528
+ const pb1 = handleStreamReply(
529
+ { chat_id: '1', text: 'B step 1', lane: 'progress', turnKey: '1:_:2' },
530
+ state, deps,
531
+ )
532
+ await microtaskFlush()
533
+ await pb1
534
+
535
+ vi.advanceTimersByTime(1000)
536
+
537
+ // Turn A updates
538
+ const pa2 = handleStreamReply(
539
+ { chat_id: '1', text: 'A step 1 + 2', lane: 'progress', turnKey: '1:_:1' },
540
+ state, deps,
541
+ )
542
+ await microtaskFlush()
543
+ await pa2
544
+
545
+ vi.advanceTimersByTime(1000)
546
+
547
+ // Turn B updates
548
+ const pb2 = handleStreamReply(
549
+ { chat_id: '1', text: 'B step 1 + 2', lane: 'progress', turnKey: '1:_:2' },
550
+ state, deps,
551
+ )
552
+ await microtaskFlush()
553
+ await pb2
554
+
555
+ // Two sends (one per turn), two edits (one per turn's update).
556
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
557
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(2)
558
+
559
+ // The edits must target distinct message ids — one per turn's
560
+ // original message — not both collapse onto the same id.
561
+ const editTargets = bot.api.editMessageText.mock.calls.map((c) => c[1])
562
+ expect(new Set(editTargets).size).toBe(2)
563
+
564
+ // And each edit carries its own turn's text — no cross-contamination.
565
+ const editTexts = bot.api.editMessageText.mock.calls.map((c) => c[2])
566
+ expect(editTexts).toContain('<b>A step 1 + 2</b>')
567
+ expect(editTexts).toContain('<b>B step 1 + 2</b>')
568
+ })
569
+
570
+ it('done=true on one turnKey does not close the other concurrent turn', async () => {
571
+ const state = makeState()
572
+ const deps = makeDeps(bot)
573
+
574
+ const pA = handleStreamReply(
575
+ { chat_id: '1', text: 'A', lane: 'progress', turnKey: '1:_:1' },
576
+ state, deps,
577
+ )
578
+ await microtaskFlush()
579
+ await pA
580
+
581
+ const pB = handleStreamReply(
582
+ { chat_id: '1', text: 'B', lane: 'progress', turnKey: '1:_:2' },
583
+ state, deps,
584
+ )
585
+ await microtaskFlush()
586
+ await pB
587
+
588
+ expect(state.activeDraftStreams.size).toBe(2)
589
+
590
+ // Advance past the throttle window so the finalize edit can flush
591
+ // instead of sitting on the debounce timer (fake timers).
592
+ vi.advanceTimersByTime(1000)
593
+
594
+ // Finalize turn A
595
+ const pAFinal = handleStreamReply(
596
+ { chat_id: '1', text: 'A final', lane: 'progress', turnKey: '1:_:1', done: true },
597
+ state, deps,
598
+ )
599
+ await microtaskFlush()
600
+ await pAFinal
601
+
602
+ // Turn A's slot is gone; turn B's is still live.
603
+ expect(state.activeDraftStreams.has('1:_:progress:1:_:1')).toBe(false)
604
+ expect(state.activeDraftStreams.has('1:_:progress:1:_:2')).toBe(true)
605
+ expect(state.activeDraftStreams.size).toBe(1)
606
+ })
607
+
608
+ it('turnKey omitted falls back to legacy chat+thread+lane key (no regression for non-progress callers)', async () => {
609
+ // Other lanes (default, thinking, activity) don't pass turnKey. They
610
+ // must still multiplex the legacy way: one stream per chat+thread+lane.
611
+ // This pins the backwards-compatible behavior of streamKey() when
612
+ // turnKey is undefined — a non-progress caller shouldn't suddenly
613
+ // create a new stream on every call.
614
+ const state = makeState()
615
+ const deps = makeDeps(bot)
616
+
617
+ const p1 = handleStreamReply({ chat_id: '1', text: 'a1' }, state, deps)
618
+ await microtaskFlush()
619
+ await p1
620
+ vi.advanceTimersByTime(1000)
621
+ const p2 = handleStreamReply({ chat_id: '1', text: 'a2' }, state, deps)
622
+ await microtaskFlush()
623
+ await p2
624
+
625
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
626
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
627
+ expect(state.activeDraftStreams.size).toBe(1)
628
+ expect(state.activeDraftStreams.has('1:_')).toBe(true)
629
+ })
630
+
631
+ it('bug 1: parseMode mismatch with existing stream rotates to fresh stream with new parseMode + rendered text', async () => {
632
+ // Reproduces the reported bug: PTY-tail auto-stream seeds a stream
633
+ // with format:'text' (parseMode undefined). A later explicit
634
+ // stream_reply on the same key with format:'html' + markdown text
635
+ // must NOT inherit the stale parseMode — it must finalize the old
636
+ // stream and create a fresh one with parse_mode=HTML so the markdown
637
+ // converts to HTML tags instead of sending literal asterisks.
638
+ const state = makeState()
639
+ const deps = makeDeps(bot, {
640
+ markdownToHtml: realMarkdownToHtml,
641
+ defaultFormat: 'text',
642
+ })
643
+
644
+ // First call: PTY-tail-style, format:'text'
645
+ const p1 = handleStreamReply(
646
+ { chat_id: '1', text: 'Running Bash: ls', format: 'text' },
647
+ state, deps,
648
+ )
649
+ await microtaskFlush()
650
+ await p1
651
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
652
+ expect(bot.api.sendMessage.mock.calls[0][2]?.parse_mode).toBeUndefined()
653
+
654
+ vi.advanceTimersByTime(1000)
655
+
656
+ // Second call on the same stream key: model explicitly uses html +
657
+ // markdown. Must produce a new send (stream rotated), parse_mode HTML,
658
+ // and literal markdown converted to Telegram HTML tags.
659
+ const p2 = handleStreamReply(
660
+ { chat_id: '1', text: '**bold** and `code`', format: 'html' },
661
+ state, deps,
662
+ )
663
+ await microtaskFlush()
664
+ await p2
665
+
666
+ // A fresh stream means a second sendMessage, not an edit of the old
667
+ // one (the old stream was finalized + discarded).
668
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(2)
669
+ const secondSend = bot.api.sendMessage.mock.calls[1]
670
+ expect(secondSend[2]?.parse_mode).toBe('HTML')
671
+ // markdownToHtml renders `**bold**` → `<b>bold</b>` and
672
+ // `` `code` `` → `<code>code</code>`.
673
+ expect(secondSend[1]).toContain('<b>bold</b>')
674
+ expect(secondSend[1]).toContain('<code>code</code>')
675
+ expect(secondSend[1]).not.toContain('**')
676
+ expect(secondSend[1]).not.toMatch(/`code`/)
677
+ })
678
+
679
+ // ─── Regression: PTY-tail duplicate message. Before the fix,
680
+ // stream_reply did not add itself to suppressPtyPreview, so a PTY
681
+ // partial firing after a finalized stream (TUI capture of the same
682
+ // assistant text) created a duplicate message with the raw TUI text
683
+ // and visibly escaped HTML tags. See log sequence: msg 559 finalized,
684
+ // then msg 560 draft_send path=pty_preview with the same content.
685
+ // Now stream_reply claims the suppress slot on the first call.
686
+
687
+ it('adds sKey (without lane) to suppressPtyPreview on first call', async () => {
688
+ const state: StreamReplyState = {
689
+ ...makeState(),
690
+ suppressPtyPreview: new Set<string>(),
691
+ }
692
+ const deps = makeDeps(bot)
693
+
694
+ const pending = handleStreamReply({ chat_id: '42', text: 'hi' }, state, deps)
695
+ await microtaskFlush()
696
+ await pending
697
+
698
+ expect(state.suppressPtyPreview!.has('42:_')).toBe(true)
699
+ })
700
+
701
+ it('suppression key ignores lane — claims default PTY lane', async () => {
702
+ const state: StreamReplyState = {
703
+ ...makeState(),
704
+ suppressPtyPreview: new Set<string>(),
705
+ }
706
+ const deps = makeDeps(bot)
707
+
708
+ const pending = handleStreamReply(
709
+ { chat_id: '42', text: 'hi', lane: 'thinking' },
710
+ state,
711
+ deps,
712
+ )
713
+ await microtaskFlush()
714
+ await pending
715
+
716
+ // The stream itself is keyed with the lane...
717
+ expect(state.activeDraftStreams.has('42:_:thinking')).toBe(true)
718
+ // ...but the PTY-suppression key is lane-less so the PTY handler
719
+ // (which has no concept of lanes) actually sees it as suppressed.
720
+ expect(state.suppressPtyPreview!.has('42:_')).toBe(true)
721
+ expect(state.suppressPtyPreview!.has('42:_:thinking')).toBe(false)
722
+ })
723
+
724
+ it('suppression survives done=true so late PTY partials are still dropped', async () => {
725
+ // This covers the exact production sequence from telegram-plugin.log:
726
+ // stream_reply done=true → draft_edit final → PTY partial arrives
727
+ // 500ms later with the TUI capture → must NOT create a new message.
728
+ const state: StreamReplyState = {
729
+ ...makeState(),
730
+ suppressPtyPreview: new Set<string>(),
731
+ }
732
+ const deps = makeDeps(bot)
733
+
734
+ const pending = handleStreamReply(
735
+ { chat_id: '42', text: 'final', done: true },
736
+ state,
737
+ deps,
738
+ )
739
+ await microtaskFlush()
740
+ await pending
741
+
742
+ // After done=true the stream is gone from activeDraftStreams...
743
+ expect(state.activeDraftStreams.has('42:_')).toBe(false)
744
+ // ...but the suppress slot must remain so a PTY partial landing
745
+ // AFTER finalize is dropped. server.ts clears this on turn_end.
746
+ expect(state.suppressPtyPreview!.has('42:_')).toBe(true)
747
+ })
748
+
749
+ it('end-to-end: PTY partial after stream_reply finalize is suppressed (no dup message)', async () => {
750
+ // Reproduces the production sequence:
751
+ // 1. stream_reply done=true for chat 42
752
+ // 2. PTY-tail fires with the TUI capture of the same assistant text
753
+ // 3. PTY handler sees suppress flag and drops the partial
754
+ // Before the fix, step 2 created a duplicate Telegram message with
755
+ // raw TUI text and visibly-escaped HTML tags (see log msg 559 → 560).
756
+ const activeDraftStreams = new Map<string, DraftStreamHandle>()
757
+ const suppressPtyPreview = new Set<string>()
758
+ const streamState: StreamReplyState = {
759
+ activeDraftStreams,
760
+ activeDraftParseModes: new Map(),
761
+ suppressPtyPreview,
762
+ }
763
+ const streamDeps = makeDeps(bot)
764
+
765
+ // Step 1: stream_reply finalizes.
766
+ const pending = handleStreamReply(
767
+ { chat_id: '42', text: 'final answer', done: true },
768
+ streamState,
769
+ streamDeps,
770
+ )
771
+ await microtaskFlush()
772
+ await pending
773
+ const sendsAfterStream = bot.api.sendMessage.mock.calls.length
774
+
775
+ // Step 2: PTY partial fires into the SHARED state — same Sets/Maps.
776
+ const ptyState: PtyHandlerState = {
777
+ currentSessionChatId: '42',
778
+ currentSessionThreadId: undefined,
779
+ pendingPtyPartial: null,
780
+ activeDraftStreams,
781
+ suppressPtyPreview,
782
+ lastPtyPreviewByChat: new Map(),
783
+ }
784
+ const action = handlePtyPartialPure(
785
+ 'TUI capture: <b>final answer</b>',
786
+ ptyState,
787
+ { bot, renderText: (t) => t },
788
+ )
789
+ await microtaskFlush()
790
+
791
+ // Step 3: partial was dropped — no extra sendMessage call.
792
+ expect(action).toBe('suppressed')
793
+ expect(bot.api.sendMessage.mock.calls.length).toBe(sendsAfterStream)
794
+ })
795
+
796
+ it('works without suppressPtyPreview (backwards compat)', async () => {
797
+ // Callers that don't thread the set through must still function.
798
+ const state = makeState() // no suppressPtyPreview
799
+ const deps = makeDeps(bot)
800
+
801
+ const pending = handleStreamReply({ chat_id: '42', text: 'hi' }, state, deps)
802
+ await microtaskFlush()
803
+ const result = await pending
804
+ expect(result.messageId).toBe(500)
805
+ })
806
+
807
+ it('streamExisted flag in logStreamingEvent reflects map state', async () => {
808
+ const state = makeState()
809
+ const logStreamingEvent = vi.fn()
810
+ const deps = makeDeps(bot, { logStreamingEvent })
811
+
812
+ const p1 = handleStreamReply({ chat_id: '1', text: 'a' }, state, deps)
813
+ await microtaskFlush()
814
+ await p1
815
+ vi.advanceTimersByTime(1000)
816
+ const p2 = handleStreamReply({ chat_id: '1', text: 'b' }, state, deps)
817
+ await microtaskFlush()
818
+ await p2
819
+
820
+ const calledEvents = logStreamingEvent.mock.calls.map(c => c[0])
821
+ const streamReplyCalledEvents = calledEvents.filter(
822
+ (e: { kind: string }) => e.kind === 'stream_reply_called',
823
+ )
824
+ expect(streamReplyCalledEvents[0].streamExisted).toBe(false)
825
+ expect(streamReplyCalledEvents[1].streamExisted).toBe(true)
826
+ })
827
+
828
+ describe('progressCardActive coexistence', () => {
829
+ // The progress card and the answer message live on different lanes
830
+ // (progress vs default) and render different content (tool structure
831
+ // vs model prose), so default-lane stream_reply(done=false) is
832
+ // accepted in checklist mode. The card is no longer treated as the
833
+ // sole mid-turn surface — it shows tool structure on its own lane
834
+ // while the model's progressive replies stream into the answer
835
+ // message. See #481.
836
+ it('accepts default-lane done=false when progress card is active (streams into answer)', async () => {
837
+ const state: StreamReplyState = {
838
+ ...makeState(),
839
+ suppressPtyPreview: new Set<string>(),
840
+ }
841
+ const deps = makeDeps(bot, { progressCardActive: true })
842
+
843
+ const pending = handleStreamReply(
844
+ { chat_id: '1', text: 'working...' },
845
+ state,
846
+ deps,
847
+ )
848
+ await microtaskFlush()
849
+ const result = await pending
850
+
851
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
852
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>working...</b>')
853
+ expect(result.status).toBe('updated')
854
+ // PTY-preview slot is claimed because the model is now the
855
+ // answer-lane surface owner — late PTY partials should defer to
856
+ // the model's stream rather than racing it with a parallel edit.
857
+ // (Same suppression pattern as before — no longer for cleanup
858
+ // after a rejection, but for ownership during normal streaming.)
859
+ expect(state.suppressPtyPreview?.has('1:_')).toBe(true)
860
+ })
861
+
862
+ it('still posts final done=true call when progress card is active', async () => {
863
+ const state = makeState()
864
+ const deps = makeDeps(bot, { progressCardActive: true })
865
+
866
+ const pending = handleStreamReply(
867
+ { chat_id: '1', text: 'final answer', done: true },
868
+ state,
869
+ deps,
870
+ )
871
+ await microtaskFlush()
872
+ const result = await pending
873
+
874
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
875
+ expect(bot.api.sendMessage.mock.calls[0][1]).toBe('<b>final answer</b>')
876
+ expect(result.status).toBe('finalized')
877
+ expect(result.messageId).toBe(500)
878
+ })
879
+
880
+ it('does NOT reject named-lane calls (internal progress-card driver uses lane=progress)', async () => {
881
+ const state = makeState()
882
+ const deps = makeDeps(bot, { progressCardActive: true })
883
+
884
+ const pending = handleStreamReply(
885
+ { chat_id: '1', text: 'card snapshot', lane: 'progress' },
886
+ state,
887
+ deps,
888
+ )
889
+ await microtaskFlush()
890
+ await pending
891
+
892
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
893
+ })
894
+
895
+ it('legacy behavior preserved when progressCardActive is false', async () => {
896
+ const state = makeState()
897
+ const deps = makeDeps(bot, { progressCardActive: false })
898
+
899
+ const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
900
+ await microtaskFlush()
901
+ await pending
902
+
903
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
904
+ })
905
+ })
906
+
907
+ describe('quote-reply default', () => {
908
+ it('auto-quotes the latest inbound message when reply_to is omitted', async () => {
909
+ const state = makeState()
910
+ const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
911
+ () => 4242,
912
+ )
913
+ const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
914
+
915
+ const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
916
+ await microtaskFlush()
917
+ await pending
918
+
919
+ expect(lookup).toHaveBeenCalledWith('1', null)
920
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toEqual({
921
+ message_id: 4242,
922
+ })
923
+ })
924
+
925
+ it('explicit reply_to overrides the auto-quote lookup', async () => {
926
+ const state = makeState()
927
+ const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
928
+ () => 4242,
929
+ )
930
+ const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
931
+
932
+ const pending = handleStreamReply(
933
+ { chat_id: '1', text: 'hi', reply_to: '777' },
934
+ state,
935
+ deps,
936
+ )
937
+ await microtaskFlush()
938
+ await pending
939
+
940
+ // Lookup is skipped entirely when reply_to is explicit.
941
+ expect(lookup).not.toHaveBeenCalled()
942
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toEqual({
943
+ message_id: 777,
944
+ })
945
+ })
946
+
947
+ it('quote:false opts out — no reply_parameters sent', async () => {
948
+ const state = makeState()
949
+ const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
950
+ () => 4242,
951
+ )
952
+ const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
953
+
954
+ const pending = handleStreamReply(
955
+ { chat_id: '1', text: 'hi', quote: false },
956
+ state,
957
+ deps,
958
+ )
959
+ await microtaskFlush()
960
+ await pending
961
+
962
+ expect(lookup).not.toHaveBeenCalled()
963
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toBeUndefined()
964
+ })
965
+
966
+ it('no reply_parameters when history lookup returns null (empty history)', async () => {
967
+ const state = makeState()
968
+ const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
969
+ () => null,
970
+ )
971
+ const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
972
+
973
+ const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
974
+ await microtaskFlush()
975
+ await pending
976
+
977
+ expect(lookup).toHaveBeenCalledTimes(1)
978
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toBeUndefined()
979
+ })
980
+
981
+ it('no auto-quote when getLatestInboundMessageId dep is omitted (legacy callers)', async () => {
982
+ const state = makeState()
983
+ const deps = makeDeps(bot) // no lookup dep
984
+
985
+ const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
986
+ await microtaskFlush()
987
+ await pending
988
+
989
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toBeUndefined()
990
+ })
991
+
992
+ it('passes thread id to the lookup', async () => {
993
+ const state = makeState()
994
+ const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
995
+ () => 55,
996
+ )
997
+ const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
998
+
999
+ const pending = handleStreamReply(
1000
+ { chat_id: '1', text: 'hi', message_thread_id: '7' },
1001
+ state,
1002
+ deps,
1003
+ )
1004
+ await microtaskFlush()
1005
+ await pending
1006
+
1007
+ expect(lookup).toHaveBeenCalledWith('1', 7)
1008
+ })
1009
+
1010
+ it('edit-path does not include reply_parameters (only initial send)', async () => {
1011
+ const state = makeState()
1012
+ const lookup = vi.fn<(chatId: string, threadId: number | null) => number | null>(
1013
+ () => 4242,
1014
+ )
1015
+ const deps = makeDeps(bot, { getLatestInboundMessageId: lookup })
1016
+
1017
+ // First call → send with reply_parameters.
1018
+ await handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
1019
+ await microtaskFlush()
1020
+
1021
+ // Second call on the same stream → edit. editMessageText must NOT
1022
+ // receive reply_parameters (Telegram rejects it on edit).
1023
+ vi.advanceTimersByTime(1000)
1024
+ await handleStreamReply({ chat_id: '1', text: 'hi there' }, state, deps)
1025
+ await microtaskFlush()
1026
+
1027
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_parameters).toEqual({
1028
+ message_id: 4242,
1029
+ })
1030
+ expect(bot.api.editMessageText).toHaveBeenCalled()
1031
+ const editOpts = bot.api.editMessageText.mock.calls[0][3]
1032
+ expect((editOpts as { reply_parameters?: unknown })?.reply_parameters).toBeUndefined()
1033
+ })
1034
+ })
1035
+
1036
+ describe('reply_markup persistence', () => {
1037
+ it('reply_markup in args is included in sendMessage opts on stream creation', async () => {
1038
+ const state = makeState()
1039
+ const deps = makeDeps(bot)
1040
+ const keyboard = { inline_keyboard: [[{ text: 'Steer', callback_data: 'steer:1' }]] }
1041
+
1042
+ const pending = handleStreamReply(
1043
+ { chat_id: '1', text: 'hi', reply_markup: keyboard },
1044
+ state,
1045
+ deps,
1046
+ )
1047
+ await microtaskFlush()
1048
+ await pending
1049
+
1050
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_markup).toBe(keyboard)
1051
+ })
1052
+
1053
+ it('reply_markup persists through editMessageText on subsequent updates', async () => {
1054
+ const state = makeState()
1055
+ const deps = makeDeps(bot)
1056
+ const keyboard = { inline_keyboard: [[{ text: 'Steer', callback_data: 'steer:1' }]] }
1057
+
1058
+ const p1 = handleStreamReply(
1059
+ { chat_id: '1', text: 'step 1', reply_markup: keyboard },
1060
+ state,
1061
+ deps,
1062
+ )
1063
+ await microtaskFlush()
1064
+ await p1
1065
+
1066
+ vi.advanceTimersByTime(1000)
1067
+ const p2 = handleStreamReply(
1068
+ { chat_id: '1', text: 'step 2', reply_markup: keyboard },
1069
+ state,
1070
+ deps,
1071
+ )
1072
+ await microtaskFlush()
1073
+ await p2
1074
+
1075
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
1076
+ expect(bot.api.editMessageText.mock.calls[0][3]?.reply_markup).toBe(keyboard)
1077
+ })
1078
+
1079
+ it('reply_markup persists through finalize flush on done=true', async () => {
1080
+ const state = makeState()
1081
+ const deps = makeDeps(bot)
1082
+ const keyboard = { inline_keyboard: [[{ text: 'Steer', callback_data: 'steer:1' }]] }
1083
+
1084
+ const p1 = handleStreamReply(
1085
+ { chat_id: '1', text: 'draft', reply_markup: keyboard },
1086
+ state,
1087
+ deps,
1088
+ )
1089
+ await microtaskFlush()
1090
+ await p1
1091
+
1092
+ vi.advanceTimersByTime(1000)
1093
+ const p2 = handleStreamReply(
1094
+ { chat_id: '1', text: 'final', done: true, reply_markup: keyboard },
1095
+ state,
1096
+ deps,
1097
+ )
1098
+ await microtaskFlush()
1099
+ await p2
1100
+
1101
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
1102
+ expect(bot.api.editMessageText.mock.calls[0][3]?.reply_markup).toBe(keyboard)
1103
+ })
1104
+
1105
+ it('omits reply_markup when not provided in args', async () => {
1106
+ const state = makeState()
1107
+ const deps = makeDeps(bot)
1108
+
1109
+ const pending = handleStreamReply({ chat_id: '1', text: 'hi' }, state, deps)
1110
+ await microtaskFlush()
1111
+ await pending
1112
+
1113
+ expect(bot.api.sendMessage.mock.calls[0][2]?.reply_markup).toBeUndefined()
1114
+ })
1115
+ })
1116
+
1117
+ describe('lookupExistingMessageId hook (#626 — multiple status messages regression)', () => {
1118
+ it('reuses an externally-known messageId on stream creation — first emit edits, no sendMessage', async () => {
1119
+ // The pin manager already knows the anchor message id for this
1120
+ // turnKey from a previous emit cycle (e.g. before done=true wiped
1121
+ // activeDraftStreams[sKey]). The hook hands that id back; the
1122
+ // new stream initializes with it, so the FIRST update edits in
1123
+ // place. No fresh sendMessage = no extra "status message" lands.
1124
+ const state = makeState()
1125
+ const deps = makeDeps(bot, {
1126
+ lookupExistingMessageId: ({ turnKey, lane }) => {
1127
+ if (turnKey === 'turn-A' && lane === 'progress') return 4242
1128
+ return null
1129
+ },
1130
+ })
1131
+
1132
+ const pending = handleStreamReply(
1133
+ { chat_id: '1', text: 'second emit', lane: 'progress', turnKey: 'turn-A' },
1134
+ state,
1135
+ deps,
1136
+ )
1137
+ await microtaskFlush()
1138
+ const result = await pending
1139
+
1140
+ expect(bot.api.sendMessage).not.toHaveBeenCalled()
1141
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
1142
+ const [, id] = bot.api.editMessageText.mock.calls[0]
1143
+ expect(id).toBe(4242)
1144
+ expect(result.messageId).toBe(4242)
1145
+ })
1146
+
1147
+ it('hook returns null → falls through to legacy sendMessage path', async () => {
1148
+ // Back-compat sanity: a hook that returns null on every call
1149
+ // produces identical behavior to omitting the hook entirely.
1150
+ const state = makeState()
1151
+ const deps = makeDeps(bot, {
1152
+ lookupExistingMessageId: () => null,
1153
+ })
1154
+
1155
+ const pending = handleStreamReply(
1156
+ { chat_id: '1', text: 'fresh send', lane: 'progress', turnKey: 'turn-X' },
1157
+ state,
1158
+ deps,
1159
+ )
1160
+ await microtaskFlush()
1161
+ await pending
1162
+
1163
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
1164
+ expect(bot.api.editMessageText).not.toHaveBeenCalled()
1165
+ })
1166
+
1167
+ it('hook NOT consulted when an active draft stream already exists for the lane+turn', async () => {
1168
+ // Lifecycle invariant: the hook only fires on stream creation.
1169
+ // If activeDraftStreams[sKey] is already populated (turn in
1170
+ // progress, no done=true yet), the existing stream handles
1171
+ // edits — the hook is never consulted, so it can't disturb the
1172
+ // running stream's state.
1173
+ const state = makeState()
1174
+ let lookupCalls = 0
1175
+ const deps = makeDeps(bot, {
1176
+ lookupExistingMessageId: () => {
1177
+ lookupCalls++
1178
+ return 9999
1179
+ },
1180
+ })
1181
+
1182
+ // First emit creates the stream (lookup IS called, returns
1183
+ // 9999 → first edit goes to 9999).
1184
+ await handleStreamReply(
1185
+ { chat_id: '1', text: 'first', lane: 'progress', turnKey: 'turn-B' },
1186
+ state,
1187
+ deps,
1188
+ )
1189
+ await microtaskFlush()
1190
+ vi.advanceTimersByTime(1000)
1191
+ expect(lookupCalls).toBe(1)
1192
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(1)
1193
+
1194
+ // Second emit on the same lane+turn reuses the existing stream
1195
+ // — the lookup is NOT called again. Edits still target 9999.
1196
+ await handleStreamReply(
1197
+ { chat_id: '1', text: 'second', lane: 'progress', turnKey: 'turn-B' },
1198
+ state,
1199
+ deps,
1200
+ )
1201
+ await microtaskFlush()
1202
+ expect(lookupCalls).toBe(1)
1203
+ expect(bot.api.editMessageText).toHaveBeenCalledTimes(2)
1204
+ })
1205
+
1206
+ it('hook throws → error logged, handler falls through to fresh sendMessage', async () => {
1207
+ // Defensive contract: a buggy lookup must never break the
1208
+ // outbound path. Caller's writeError gets the diagnostic; the
1209
+ // emit lands as a fresh send.
1210
+ const state = makeState()
1211
+ const writeError = vi.fn()
1212
+ const deps = makeDeps(bot, {
1213
+ writeError,
1214
+ lookupExistingMessageId: () => {
1215
+ throw new Error('lookup blew up')
1216
+ },
1217
+ })
1218
+
1219
+ await handleStreamReply(
1220
+ { chat_id: '1', text: 'after-fault', lane: 'progress', turnKey: 'turn-C' },
1221
+ state,
1222
+ deps,
1223
+ )
1224
+ await microtaskFlush()
1225
+
1226
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
1227
+ expect(writeError).toHaveBeenCalled()
1228
+ const errLine = (writeError.mock.calls[0]?.[0] as string) ?? ''
1229
+ expect(errLine).toContain('lookupExistingMessageId failed')
1230
+ })
1231
+
1232
+ it('full #626 lifecycle scenario — done=true → activeDraftStreams cleared → next emit edits via hook (one anchor message total)', async () => {
1233
+ // The end-to-end repro of #626. Sequence:
1234
+ // 1. First progress-card emit (isFirstEmit=true) → fresh
1235
+ // sendMessage on the 'progress' lane for turn-A. Pin
1236
+ // manager records messageId=500.
1237
+ // 2. done=true emit (e.g. the parent turn_end fires before
1238
+ // sub-agents finish) → handler finalizes + DELETES
1239
+ // activeDraftStreams[sKey].
1240
+ // 3. A subsequent sub-agent event triggers a fresh progress-
1241
+ // card emit on the SAME turn-A. Without the hook, the
1242
+ // handler would create a new stream → fresh sendMessage →
1243
+ // a SECOND status message lands in the chat.
1244
+ // 4. With the hook returning the pin-manager's messageId 500,
1245
+ // the new stream initializes with 500. The next update
1246
+ // hits editMessageText against 500. Total Telegram surface
1247
+ // = ONE message.
1248
+ const state = makeState()
1249
+ const knownMessageId = { value: null as number | null }
1250
+ const deps = makeDeps(bot, {
1251
+ lookupExistingMessageId: () => knownMessageId.value,
1252
+ })
1253
+
1254
+ // 1. First emit, no known messageId yet → fresh sendMessage
1255
+ await handleStreamReply(
1256
+ { chat_id: '1', text: 'tool 1...', lane: 'progress', turnKey: 'turn-A' },
1257
+ state,
1258
+ deps,
1259
+ )
1260
+ await microtaskFlush()
1261
+ // The pin manager records id=500 (mock bot's first id).
1262
+ knownMessageId.value = 500
1263
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
1264
+ expect(bot.api.editMessageText).not.toHaveBeenCalled()
1265
+
1266
+ // 2. done=true → finalize + clear sKey
1267
+ vi.advanceTimersByTime(1000)
1268
+ await handleStreamReply(
1269
+ { chat_id: '1', text: 'tool 1, tool 2 ✓', lane: 'progress', turnKey: 'turn-A', done: true },
1270
+ state,
1271
+ deps,
1272
+ )
1273
+ await microtaskFlush()
1274
+ expect(state.activeDraftStreams.size).toBe(0)
1275
+
1276
+ // 3. Subsequent sub-agent emit on the SAME turn-A — without
1277
+ // the hook this would land as sendMessage #2 (the bug).
1278
+ await handleStreamReply(
1279
+ { chat_id: '1', text: 'tool 1, tool 2 ✓, sub-agent...', lane: 'progress', turnKey: 'turn-A' },
1280
+ state,
1281
+ deps,
1282
+ )
1283
+ await microtaskFlush()
1284
+
1285
+ // Invariant: total fresh sendMessages on this chat = 1.
1286
+ // Anything > 1 is the #626 bug class.
1287
+ expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
1288
+ // The post-done emit was an edit against id 500.
1289
+ expect(bot.api.editMessageText.mock.calls.some((c) => c[1] === 500)).toBe(true)
1290
+ })
1291
+ })
1292
+ })