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,1409 @@
1
+ /**
2
+ * Progress card renderer โ€” event-driven alternative to PTY-snapshot streaming.
3
+ *
4
+ * Turns a stream of `SessionEvent`s (from session-tail.ts) into a stable,
5
+ * flicker-free Telegram HTML message that edits in place as the turn
6
+ * progresses. Solves the root cause of the PTY-stream flicker: Ink's
7
+ * differential re-renders mutate the last line multiple times per tool
8
+ * call, so snapshot-edit based on TUI text wobbles. Event reducer only
9
+ * mutates state on *transitions* (tool start, tool finish, stage change),
10
+ * so nothing above the active line ever rewrites.
11
+ *
12
+ * Pure functions โ€” no IO, no globals, no timers. The outer loop owns
13
+ * flush cadence (500ms hard floor between edits, coalesce 400ms bursts,
14
+ * fire immediately on stage change).
15
+ */
16
+
17
+ import type { SessionEvent } from './session-tail.js'
18
+ import { toolLabel, isHumanDescription } from './tool-labels.js'
19
+ import {
20
+ formatDuration as sharedFormatDuration,
21
+ escapeHtml as sharedEscapeHtml,
22
+ truncate as sharedTruncate,
23
+ } from './card-format.js'
24
+ import { isBenignToolError } from './tool-error-filter.js'
25
+ import { renderTwoZoneCard } from './two-zone-card.js'
26
+ import type { FleetMember } from './fleet-state.js'
27
+
28
+ // โ”€โ”€โ”€ Types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
29
+
30
+ export type ItemState = 'pending' | 'running' | 'done' | 'failed'
31
+
32
+ export interface ChecklistItem {
33
+ /** Index within the current turn โ€” sequential, stable. */
34
+ readonly id: number
35
+ /**
36
+ * Claude Code tool_use content-block id (e.g. "toolu_01ABCโ€ฆ"). Used
37
+ * to pair the tool_result back to its tool_use by id rather than by
38
+ * running-item order โ€” required for correct handling of parallel
39
+ * tool_use calls within a single assistant message. Null when the
40
+ * session JSONL line omitted it (older event shape or synthetic test
41
+ * events), in which case the reducer falls back to FIFO pairing.
42
+ */
43
+ readonly toolUseId: string | null
44
+ /** Claude Code tool name, e.g. "Read", "Bash", "Grep". */
45
+ readonly tool: string
46
+ /** Short human-readable label derived from the tool's input (file path,
47
+ * command, query, etc.). Empty string when the tool has no natural
48
+ * label (e.g. TodoWrite) or input was missing. */
49
+ readonly label: string
50
+ /**
51
+ * True when the label came from a human-authored `description` field
52
+ * (Bash/BashOutput/Task/Agent with a non-empty description). The
53
+ * renderer uses this to suppress the tool-name prefix so the card reads
54
+ * "Check commit state" instead of "Bash Check commit state".
55
+ */
56
+ readonly humanAuthored: boolean
57
+ /** Current state. */
58
+ readonly state: ItemState
59
+ /** Unix ms when tool_use fired. */
60
+ readonly startedAt: number
61
+ /** Unix ms when tool_result arrived. Only set on done/failed. */
62
+ readonly finishedAt?: number
63
+ /**
64
+ * Multi-agent: for `Agent`/`Task` tool_use items only, the `agentId`
65
+ * of the correlated sub-agent. Set when `sub_agent_started` lands and
66
+ * matches this item's prompt text. Renderer (later PR) uses it to
67
+ * keep the [Main] line in ๐Ÿค– (not โœ…) until the parent's tool_result
68
+ * arrives. Null until correlation succeeds.
69
+ */
70
+ readonly spawnedAgentId?: string | null
71
+ }
72
+
73
+ export type Stage = 'plan' | 'run' | 'done'
74
+
75
+ /**
76
+ * Task-list item, mirroring the Claude Code `TodoWrite` tool's atomic
77
+ * todo schema. Populated by `tool_use` (or `sub_agent_tool_use`) events
78
+ * with `toolName === 'TodoWrite'`. Used by the per-agent card render to
79
+ * draw the โ—ผ / โ—ป / โœ” block under the status row.
80
+ *
81
+ * `content` is the imperative subject ("Refactor pin manager"); the
82
+ * card renders `activeForm` ("Refactoring pin manager") for the
83
+ * in-progress task and `content` for everything else.
84
+ *
85
+ * Token-count, thinking-duration, and the per-task elapsed counter are
86
+ * intentionally not tracked here โ€” those signals require ingestion
87
+ * changes (token counts aren't in the JSONL today; thinking is a
88
+ * boolean) and are deferred to a follow-up.
89
+ */
90
+ export type TaskState = 'pending' | 'in_progress' | 'completed'
91
+
92
+ export interface TaskItem {
93
+ readonly content: string
94
+ readonly activeForm: string
95
+ readonly state: TaskState
96
+ }
97
+
98
+ /**
99
+ * Multi-agent foundation (gated by PROGRESS_CARD_MULTI_AGENT=1):
100
+ *
101
+ * Per-sub-agent state, populated by the new `sub_agent_*` events. Today
102
+ * (in this PR) the renderer doesn't read any of it โ€” it lives alongside
103
+ * the existing per-tool checklist purely as a structural foundation. The
104
+ * later renderer PR consumes `subAgents` and `pendingAgentSpawns` to draw
105
+ * the two-section [Main] / [Sub-agents] card.
106
+ *
107
+ * `parentToolUseId` links a sub-agent back to the parent's `Agent`/`Task`
108
+ * tool_use that spawned it, established by prompt-text correlation in the
109
+ * correlation PR. Null while we haven't yet seen the parent (rare reverse
110
+ * race) or when correlation fails entirely (orphan).
111
+ */
112
+ export interface SubAgentState {
113
+ readonly agentId: string
114
+ readonly description: string
115
+ readonly subagentType?: string
116
+ readonly parentToolUseId: string | null
117
+ readonly state: ItemState
118
+ readonly startedAt: number
119
+ readonly finishedAt?: number
120
+ readonly toolCount: number
121
+ /**
122
+ * Monotonically-increasing version counter bumped only on milestone
123
+ * transitions: sub-agent started, finished (done/failed). NOT bumped
124
+ * on per-tool ticks (sub_agent_tool_use, sub_agent_tool_result,
125
+ * sub_agent_text). The render layer uses this to avoid re-rendering
126
+ * the `<blockquote expandable>` section on every throttle tick, which
127
+ * would re-collapse the user's expanded view.
128
+ */
129
+ readonly milestoneVersion: number
130
+ /**
131
+ * The first user-message text from the sub-agent's JSONL โ€” kept so the
132
+ * reverse-race adoption path (orphan first, parent later) can match
133
+ * against incoming pendingAgentSpawns entries.
134
+ */
135
+ readonly firstPromptText?: string
136
+ readonly currentTool?: {
137
+ readonly tool: string
138
+ readonly label: string
139
+ readonly humanAuthored: boolean
140
+ readonly toolUseId: string
141
+ readonly startedAt: number
142
+ }
143
+ /**
144
+ * Per-sub-agent analogue of ProgressCardState.pendingPreamble: the most
145
+ * recent single-line `text` block THIS sub-agent emitted that hasn't
146
+ * yet been paired to a `sub_agent_tool_use`. Set on every
147
+ * `sub_agent_text` event; consumed and cleared by the NEXT
148
+ * `sub_agent_tool_use` for the same agent (sibling tool_uses in the
149
+ * same batch get the filename fallback). Cleared on
150
+ * `sub_agent_turn_end` / `turn_end`. Lives per-agent โ€” a preamble from
151
+ * sub-agent A must not leak onto sub-agent B's tool_use.
152
+ */
153
+ readonly pendingPreamble?: string | null
154
+ /**
155
+ * The sub-agent's first narrative/text line, captured on the first
156
+ * `sub_agent_text` event for this agent. Used as a description fallback
157
+ * when correlation fails (orphan sub-agents) so the user still sees
158
+ * something meaningful instead of "(uncorrelated)". Never cleared.
159
+ */
160
+ readonly firstNarrativeText?: string
161
+ /**
162
+ * Most-recent narrative line pushed via the gateway's `progress_update`
163
+ * MCP tool (issue #305 Option A). Distinct from:
164
+ * - `firstNarrativeText` โ€” one-shot, used as description fallback
165
+ * - `pendingPreamble` โ€” one-shot pre-tool narration from session-tail
166
+ * `currentNarrative` is replace-on-each-call (last write wins). Cleared
167
+ * naturally on terminal-state render via the existing branch.
168
+ */
169
+ readonly currentNarrative?: string | null
170
+ /**
171
+ * The tool most recently completed by this sub-agent. Captured on
172
+ * `sub_agent_tool_result` (before the toolUseId match clears
173
+ * `currentTool`). Used by the render fallback chain when the sub-agent
174
+ * is running-but-between-tools so we show the last thing it did rather
175
+ * than the bare "(idle)" string.
176
+ */
177
+ readonly lastCompletedTool?: {
178
+ readonly tool: string
179
+ readonly label: string
180
+ readonly humanAuthored: boolean
181
+ readonly finishedAt: number
182
+ }
183
+ /**
184
+ * Issue #352: ring buffer of the last 2 completed tools, ordered oldest
185
+ * first. Combined with `lastCompletedTool` and `currentTool` this lets
186
+ * the expandable section show up to 3 recent actions with strikethrough
187
+ * for completed items and a `โ†ณ` arrow for the active one.
188
+ *
189
+ * Only `lastCompletedTool` is used for the between-tool fallback chain
190
+ * outside the expandable; `recentCompletedTools` is purely for the
191
+ * expandable view (issue #352).
192
+ */
193
+ readonly recentCompletedTools: ReadonlyArray<{
194
+ readonly tool: string
195
+ readonly label: string
196
+ readonly humanAuthored: boolean
197
+ readonly finishedAt: number
198
+ }>
199
+ /** Sub-sub-agents observed (rendered as `(spawned N)` only, not as rows). */
200
+ readonly nestedSpawnCount: number
201
+ /**
202
+ * Gap 4 (cold-JSONL detection): wall-clock ms of the most recent sub-agent
203
+ * event (sub_agent_tool_use, sub_agent_tool_result, sub_agent_text, etc.).
204
+ * Set on every event that updates this sub-agent's state. When the driver
205
+ * observes that a running sub-agent's `lastEventAt` is more than
206
+ * `coldSubAgentThresholdMs` (default 30s) in the past, it synthesises a
207
+ * `sub_agent_turn_end` so the deferred-completion path can proceed.
208
+ */
209
+ readonly lastEventAt?: number
210
+ /**
211
+ * TodoWrite-driven task list for the per-agent card render. Atomic
212
+ * replacement: each `sub_agent_tool_use` with `toolName === 'TodoWrite'`
213
+ * overwrites the slice with the parsed `input.todos` array. Empty
214
+ * until the sub-agent calls TodoWrite at least once.
215
+ */
216
+ readonly tasks: ReadonlyArray<TaskItem>
217
+ }
218
+
219
+ /**
220
+ * A parent `Agent`/`Task` tool_use whose sub-agent JSONL hasn't appeared
221
+ * yet. Once `sub_agent_started` arrives with matching `firstPromptText`
222
+ * the entry is moved into `subAgents` and removed from this map.
223
+ */
224
+ export interface PendingAgentSpawn {
225
+ readonly parentToolUseId: string
226
+ readonly description: string
227
+ readonly subagentType?: string
228
+ readonly promptText: string
229
+ readonly startedAt: number
230
+ }
231
+
232
+ export interface NarrativeStep {
233
+ readonly id: number
234
+ readonly text: string
235
+ /**
236
+ * State machine:
237
+ * - `active`: the narrative step is currently the latest, actively narrating.
238
+ * - `done`: the step is complete (next text event or turn_end fired, and no
239
+ * background sub-agents are pending).
240
+ * - `awaiting-subagent`: the step dispatched one or more background
241
+ * sub-agents (Agent/Task tool_use) that haven't reached terminal state
242
+ * yet. Rendered identically to `active` (โ—‰) so the card never shows
243
+ * "done" while sub-agents are still running. Transitions to `done` once
244
+ * all entries in `awaitingSubAgentIds` have completed.
245
+ */
246
+ readonly state: 'done' | 'active' | 'awaiting-subagent'
247
+ readonly startedAt: number
248
+ readonly toolCount: number
249
+ /**
250
+ * Agent/Task `toolUseId`s from the parent turn that this narrative step
251
+ * triggered but whose sub-agents haven't yet been correlated (i.e. the
252
+ * `sub_agent_started` event hasn't landed yet). When correlation arrives,
253
+ * the entry migrates from here to `awaitingSubAgentIds`. Allows the step
254
+ * to know about in-flight spawns even before the sub-agent JSONL appears.
255
+ */
256
+ readonly pendingAgentToolUseIds: ReadonlyArray<string>
257
+ /**
258
+ * `agentId`s of sub-agents spawned during this narrative step that are
259
+ * still running. When this set becomes empty and the step is in
260
+ * `awaiting-subagent` state, it flips to `done`.
261
+ */
262
+ readonly awaitingSubAgentIds: ReadonlyArray<string>
263
+ }
264
+
265
+ export interface ProgressCardState {
266
+ /** Unix ms when the turn started (enqueue event). 0 when idle. */
267
+ readonly turnStartedAt: number
268
+ /** User's inbound message text, truncated. */
269
+ readonly userRequest?: string
270
+ /** Ordered checklist items โ€” never reorder, only append and transition. */
271
+ readonly items: ReadonlyArray<ChecklistItem>
272
+ /** Current high-level stage. */
273
+ readonly stage: Stage
274
+ /** Whether the model is currently in a thinking block. */
275
+ readonly thinking: boolean
276
+ /** Latest short `text` content from the assistant (for the thought line). */
277
+ readonly latestText?: string
278
+ /**
279
+ * The most recent single-line `text` block the model emitted that
280
+ * hasn't yet been paired to a `tool_use`. Used by the file/search
281
+ * tools (Read/Write/Edit/Grep/Glob) to show the model's natural
282
+ * preamble ("I'll check foo.ts") instead of the filename fallback
283
+ * in the checklist. Set on every `text` event; consumed and cleared
284
+ * by the NEXT `tool_use` (so sibling tool_uses in the same batch do
285
+ * NOT reuse it). Cleared unconditionally on `turn_end` / `enqueue`.
286
+ */
287
+ readonly pendingPreamble?: string | null
288
+ /** Narrative steps derived from assistant text blocks. */
289
+ readonly narratives: ReadonlyArray<NarrativeStep>
290
+ /**
291
+ * Multi-agent: per-sub-agent state, keyed by `agentId` (sub-agent JSONL
292
+ * filename stem). Empty in single-agent turns. Always present so the
293
+ * shape is stable across flag-on / flag-off renders.
294
+ */
295
+ readonly subAgents: ReadonlyMap<string, SubAgentState>
296
+ /**
297
+ * Multi-agent: parent Agent/Task tool_uses awaiting a sub-agent JSONL
298
+ * to correlate with. Keyed by the parent's `toolUseId`.
299
+ */
300
+ readonly pendingAgentSpawns: ReadonlyMap<string, PendingAgentSpawn>
301
+ /**
302
+ * Parent-agent TodoWrite-driven task list for the per-agent card
303
+ * render. Atomic replacement: each `tool_use` with `toolName ===
304
+ * 'TodoWrite'` overwrites the slice with the parsed `input.todos`
305
+ * array. Empty until the parent calls TodoWrite at least once.
306
+ */
307
+ readonly tasks: ReadonlyArray<TaskItem>
308
+ }
309
+
310
+ /**
311
+ * True when any sub-agent โ€” correlated or orphan โ€” is still running.
312
+ *
313
+ * Used as both the **display** gate (keep the card showing "Workingโ€ฆ" with
314
+ * sub-agent rows) and the **defer** gate (hold `pendingCompletion` past
315
+ * parent turn_end so the card stays pinned until the last sub-agent reports
316
+ * done). Orphans (parentToolUseId == null, e.g. from
317
+ * `Agent({run_in_background: true})`) gate both, so background dispatches
318
+ * stay visible past parent turn-end (#87).
319
+ *
320
+ * Historical context: an earlier design excluded orphans from the defer
321
+ * gate because their `sub_agent_turn_end` could go missing if the parent
322
+ * turn rolled over (ghost-pin risk, #31 / #43). That risk is now bounded
323
+ * by `closeZombie` on next enqueue + the `maxIdleMs` heartbeat ceiling, so
324
+ * orphans gate the defer like correlated sub-agents do.
325
+ */
326
+ export function hasAnyRunningSubAgent(state: ProgressCardState): boolean {
327
+ for (const sa of state.subAgents.values()) {
328
+ if (sa.state === 'running') return true
329
+ }
330
+ return false
331
+ }
332
+
333
+ export function initialState(): ProgressCardState {
334
+ return {
335
+ turnStartedAt: 0,
336
+ items: [],
337
+ narratives: [],
338
+ stage: 'plan',
339
+ thinking: false,
340
+ subAgents: new Map(),
341
+ pendingAgentSpawns: new Map(),
342
+ tasks: [],
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Parse a `TodoWrite` tool_use input into a `TaskItem[]`. Returns null
348
+ * when the input shape doesn't match (no array, malformed entries) so
349
+ * the caller can leave the existing tasks slice unchanged. Callers
350
+ * should treat null as "not a recognised TodoWrite payload" rather than
351
+ * "empty list" โ€” TodoWrite never legitimately fires with no todos
352
+ * (it's an atomic-replace tool).
353
+ */
354
+ export function parseTodoWriteInput(
355
+ input: Record<string, unknown> | undefined,
356
+ ): TaskItem[] | null {
357
+ if (input == null) return null
358
+ const raw = (input as { todos?: unknown }).todos
359
+ if (!Array.isArray(raw)) return null
360
+ const out: TaskItem[] = []
361
+ for (const item of raw) {
362
+ if (item == null || typeof item !== 'object') continue
363
+ const o = item as Record<string, unknown>
364
+ const content = typeof o.content === 'string' ? o.content : null
365
+ const activeForm = typeof o.activeForm === 'string' ? o.activeForm : null
366
+ const status = typeof o.status === 'string' ? o.status : null
367
+ if (content == null || activeForm == null) continue
368
+ const state: TaskState =
369
+ status === 'in_progress' ? 'in_progress'
370
+ : status === 'completed' ? 'completed'
371
+ : 'pending'
372
+ out.push({ content, activeForm, state })
373
+ }
374
+ return out
375
+ }
376
+
377
+ /**
378
+ * Multi-agent sub-section in progress cards. Always enabled โ€” the two-
379
+ * section [Main]/[Sub-agents] layout activates automatically when sub-
380
+ * agent events are present, and is invisible otherwise. Can be forced
381
+ * off with PROGRESS_CARD_MULTI_AGENT=0 for debugging.
382
+ */
383
+ export function isMultiAgentEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
384
+ return env.PROGRESS_CARD_MULTI_AGENT !== '0'
385
+ }
386
+
387
+ // โ”€โ”€โ”€ Reducer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
388
+
389
+ /**
390
+ * Decide what state an `active` NarrativeStep should transition to when it
391
+ * would normally flip to `done` (on a new `text` event or `turn_end`).
392
+ *
393
+ * If the narrative has dispatched background sub-agents that are still
394
+ * running (i.e. `awaitingSubAgentIds` overlap with sub-agents in `running`
395
+ * state, or `pendingAgentToolUseIds` haven't yet been correlated), we keep
396
+ * it in `awaiting-subagent` rather than immediately marking it `done`.
397
+ *
398
+ * Foreground Agent/Task calls complete before the tool_result returns, so
399
+ * they won't appear in `awaitingSubAgentIds` by the time we reach here โ€”
400
+ * those flip straight to `done` as before (#324 fix, no regression).
401
+ */
402
+ function narrativeTransitionFromActive(
403
+ n: NarrativeStep,
404
+ subAgents: ReadonlyMap<string, SubAgentState>,
405
+ ): NarrativeStep {
406
+ // Any still-running sub-agents this narrative is waiting for?
407
+ const hasRunningAwaited = n.awaitingSubAgentIds.some(
408
+ id => subAgents.get(id)?.state === 'running',
409
+ )
410
+ // Any agent tool_use that hasn't yet been correlated to a sub_agent_started?
411
+ // (Rare race: tool_use fired but sub_agent_started hasn't landed yet.)
412
+ const hasPendingCorrelation = n.pendingAgentToolUseIds.length > 0
413
+ if (hasRunningAwaited || hasPendingCorrelation) {
414
+ return { ...n, state: 'awaiting-subagent' }
415
+ }
416
+ return { ...n, state: 'done' }
417
+ }
418
+
419
+ function extractNarrativeLabel(text: string): string {
420
+ const trimmed = text.trim()
421
+ if (!trimmed) return ''
422
+ const line = trimmed.split('\n')[0]
423
+ return line.length > 200 ? line.slice(0, 199) + 'โ€ฆ' : line
424
+ }
425
+
426
+ /**
427
+ * Fold a single event into the state. Events outside the turn lifecycle
428
+ * (stale tool_result before enqueue, duplicate turn_end, etc.) are no-ops.
429
+ */
430
+ export function reduce(
431
+ state: ProgressCardState,
432
+ event: SessionEvent,
433
+ now: number,
434
+ ): ProgressCardState {
435
+ switch (event.kind) {
436
+ case 'enqueue': {
437
+ // New turn starts. Reset state entirely. Extract a short summary
438
+ // from the enqueue's raw content (strip the channel XML wrapper).
439
+ return {
440
+ ...initialState(),
441
+ turnStartedAt: now,
442
+ userRequest: extractUserText(event.rawContent),
443
+ stage: 'plan',
444
+ }
445
+ }
446
+
447
+ case 'thinking': {
448
+ if (state.turnStartedAt === 0) return state
449
+ return { ...state, thinking: true, stage: state.stage === 'plan' ? 'plan' : state.stage }
450
+ }
451
+
452
+ case 'text': {
453
+ if (state.turnStartedAt === 0) return state
454
+ // Stash the raw text as a candidate preamble for the next
455
+ // tool_use. toolLabel() applies its own single-line + length
456
+ // gate, so we pass the full text through here and let the label
457
+ // layer decide. Multi-line narrative text will be rejected there
458
+ // and the filename/pattern fallback wins โ€” which is what we
459
+ // want for "here's my plan: <long paragraph>" style narration.
460
+ const pendingPreamble = event.text
461
+ const label = extractNarrativeLabel(event.text)
462
+ if (!label) {
463
+ return { ...state, latestText: event.text, thinking: false, pendingPreamble }
464
+ }
465
+ const prevNarratives = state.narratives.map(n =>
466
+ n.state === 'active' ? narrativeTransitionFromActive(n, state.subAgents) : n,
467
+ )
468
+ const newNarrative: NarrativeStep = {
469
+ id: prevNarratives.length,
470
+ text: label,
471
+ state: 'active',
472
+ startedAt: now,
473
+ toolCount: 0,
474
+ pendingAgentToolUseIds: [],
475
+ awaitingSubAgentIds: [],
476
+ }
477
+ return {
478
+ ...state,
479
+ narratives: [...prevNarratives, newNarrative],
480
+ latestText: event.text,
481
+ thinking: false,
482
+ pendingPreamble,
483
+ }
484
+ }
485
+
486
+ case 'tool_use': {
487
+ if (state.turnStartedAt === 0) return state
488
+ // Append the new item as running. We do NOT defensively close out
489
+ // prior still-running items: Claude Code emits parallel tool_use
490
+ // blocks within a single assistant message (e.g. Bash + Read
491
+ // batched), and those arrive as separate SessionEvents. Prior
492
+ // logic that auto-closed running items on each new tool_use
493
+ // mis-paired the subsequent tool_results โ€” the first result would
494
+ // land on the WRONG item (by FIFO fallback) because its
495
+ // toolUseId-matched item had already been force-done. Pairing is
496
+ // now entirely up to tool_result (by toolUseId when available).
497
+ // Consume pendingPreamble exactly once โ€” the first tool_use after
498
+ // the text block pairs with it; any sibling tool_uses in the same
499
+ // assistant message fall back to the filename/pattern label. This
500
+ // is why we capture before building the item and clear it in the
501
+ // returned state below.
502
+ const preamble = state.pendingPreamble ?? undefined
503
+ const nextItem: ChecklistItem = {
504
+ id: state.items.length,
505
+ toolUseId: event.toolUseId ?? null,
506
+ tool: event.toolName,
507
+ label: toolLabel(event.toolName, event.input, preamble),
508
+ humanAuthored: isHumanDescription(event.toolName, event.input),
509
+ state: 'running',
510
+ startedAt: now,
511
+ }
512
+ // Multi-agent: if this is an Agent/Task tool_use, stage a pending
513
+ // spawn awaiting the matching sub-agent JSONL. Correlation key is
514
+ // the prompt text (the sub-agent's first user message contains
515
+ // exactly this string). Reverse-race: if a sub-agent already
516
+ // landed as orphan with this prompt text, adopt it now.
517
+ let pendingAgentSpawns = state.pendingAgentSpawns
518
+ let subAgents = state.subAgents
519
+ if (
520
+ (event.toolName === 'Agent' || event.toolName === 'Task') &&
521
+ event.toolUseId &&
522
+ event.input
523
+ ) {
524
+ const promptText = String(event.input.prompt ?? '')
525
+ const description = String(event.input.description ?? '') || promptText.slice(0, 50)
526
+ const subagentType =
527
+ typeof event.input.subagent_type === 'string'
528
+ ? (event.input.subagent_type as string)
529
+ : undefined
530
+ // Reverse-race adoption: scan orphan sub-agents (parentToolUseId
531
+ // null) for a prompt-text match. When multiple orphans match the
532
+ // same prompt (parallel Agent calls with identical `prompt`), we
533
+ // pair the oldest orphan first โ€” `startedAt` as tiebreaker rather
534
+ // than JS Map insertion order, which depends on JSONL file-watch
535
+ // delivery order and can scramble the pairing across concurrent
536
+ // sub-agent processes.
537
+ let adopted = false
538
+ let bestAgentId: string | null = null
539
+ let bestStartedAt = Number.POSITIVE_INFINITY
540
+ for (const [agentId, sa] of subAgents) {
541
+ if (sa.parentToolUseId == null && sa.firstPromptText === promptText) {
542
+ if (sa.startedAt < bestStartedAt) {
543
+ bestStartedAt = sa.startedAt
544
+ bestAgentId = agentId
545
+ }
546
+ }
547
+ }
548
+ if (bestAgentId != null) {
549
+ const sa = subAgents.get(bestAgentId)!
550
+ const next = new Map(subAgents)
551
+ next.set(bestAgentId, {
552
+ ...sa,
553
+ parentToolUseId: event.toolUseId,
554
+ description,
555
+ subagentType,
556
+ })
557
+ subAgents = next
558
+ adopted = true
559
+ process.stderr.write(`telegram gateway: progress-card: tool_use โ†’ agent correlation toolUseId=${event.toolUseId} agentId=${bestAgentId} (reverse-race adopt orphan)\n`)
560
+ }
561
+ if (!adopted) {
562
+ process.stderr.write(`telegram gateway: progress-card: tool_use โ†’ agent correlation toolUseId=${event.toolUseId} agentId=pending (awaiting sub_agent_started)\n`)
563
+ const nextPending = new Map(pendingAgentSpawns)
564
+ nextPending.set(event.toolUseId, {
565
+ parentToolUseId: event.toolUseId,
566
+ description,
567
+ subagentType,
568
+ promptText,
569
+ startedAt: now,
570
+ })
571
+ pendingAgentSpawns = nextPending
572
+ }
573
+ }
574
+ let narratives = state.narratives
575
+ if (narratives.length > 0) {
576
+ const last = narratives[narratives.length - 1]
577
+ if (last.state === 'active') {
578
+ const isAgentCall =
579
+ (event.toolName === 'Agent' || event.toolName === 'Task') &&
580
+ !!event.toolUseId
581
+ const updatedLast: NarrativeStep = {
582
+ ...last,
583
+ toolCount: last.toolCount + 1,
584
+ // When the active narrative just triggered a background Agent/Task
585
+ // call, record the toolUseId so that when sub_agent_started
586
+ // correlates it, we can link the sub-agent to this narrative step.
587
+ pendingAgentToolUseIds: isAgentCall
588
+ ? [...last.pendingAgentToolUseIds, event.toolUseId!]
589
+ : last.pendingAgentToolUseIds,
590
+ }
591
+ narratives = [...narratives.slice(0, -1), updatedLast]
592
+ }
593
+ }
594
+ // Cap the raw item history. Only the last MAX_VISIBLE_ITEMS are
595
+ // rendered (see renderChecklist), and pairing of tool_result to
596
+ // tool_use happens by toolUseId โ€” not by position โ€” so keeping
597
+ // thousands of historical entries around only slows rendering and
598
+ // leaks memory on long turns. We keep ~10ร— the visible window so
599
+ // pairings for results arriving after many intervening tool uses
600
+ // still find their running partner.
601
+ const ITEM_HISTORY_CAP = MAX_VISIBLE_ITEMS * 10
602
+ const appended = [...state.items, nextItem]
603
+ const boundedItems =
604
+ appended.length > ITEM_HISTORY_CAP
605
+ ? appended.slice(appended.length - ITEM_HISTORY_CAP)
606
+ : appended
607
+ // TodoWrite is the atomic-replace task-list tool โ€” its input.todos
608
+ // is the canonical task-list state at this point in the turn. Lift
609
+ // it into a state slice so the per-agent card can render the
610
+ // โ—ผ / โ—ป / โœ” block. When the input shape doesn't match (older
611
+ // event shapes, synthetic test events without input) we leave the
612
+ // existing tasks slice untouched.
613
+ let tasks = state.tasks
614
+ if (event.toolName === 'TodoWrite') {
615
+ const parsed = parseTodoWriteInput(event.input)
616
+ if (parsed != null) tasks = parsed
617
+ }
618
+ return {
619
+ ...state,
620
+ items: boundedItems,
621
+ narratives,
622
+ stage: 'run',
623
+ thinking: false,
624
+ pendingAgentSpawns,
625
+ subAgents,
626
+ pendingPreamble: null,
627
+ tasks,
628
+ }
629
+ }
630
+
631
+ case 'tool_result': {
632
+ if (state.turnStartedAt === 0) return state
633
+ // Pair by tool_use_id when present: the model can emit parallel
634
+ // tool_use calls in a single assistant message, so FIFO pairing
635
+ // by running-item order is not sufficient. Falls back to the
636
+ // oldest running item when the result has no toolUseId or no
637
+ // running item matches (older JSONL shape, synthetic test events).
638
+ // is_error=true on the tool_result JSONL line flips state to
639
+ // 'failed' (โŒ).
640
+ let idx = -1
641
+ if (event.toolUseId) {
642
+ idx = state.items.findIndex(
643
+ (it) => it.state === 'running' && it.toolUseId === event.toolUseId,
644
+ )
645
+ }
646
+ if (idx === -1) {
647
+ idx = state.items.findIndex((it) => it.state === 'running')
648
+ }
649
+ if (idx === -1) return state
650
+ const items = state.items.slice()
651
+ // tool_result with is_error=true โ†’ 'failed' (โŒ), unless the error
652
+ // text matches a benign pattern (file-not-found, no-match, etc) in
653
+ // which case render 'done' (โœ…) โ€” see tool-error-filter.ts.
654
+ //
655
+ // Fail-closed semantics: when isError=true but errorText is missing
656
+ // or empty (older JSONL shapes, malformed lines, tools that error
657
+ // without output), keep the 'failed' state. Suppression requires
658
+ // *evidence* the error is benign; absence of evidence stays loud.
659
+ const nextState: ItemState =
660
+ event.isError === true
661
+ ? (event.errorText && isBenignToolError(event.errorText) ? 'done' : 'failed')
662
+ : 'done'
663
+ items[idx] = { ...items[idx], state: nextState, finishedAt: now }
664
+ // Multi-agent: a parent Agent/Task tool_result is the authoritative
665
+ // close-out for its sub-agent. Find any sub-agent linked to this
666
+ // toolUseId (via parentToolUseId) and finalize it. Also clear any
667
+ // matching pendingAgentSpawn (sub-agent JSONL never appeared).
668
+ let subAgents = state.subAgents
669
+ let pendingAgentSpawns = state.pendingAgentSpawns
670
+ if (event.toolUseId) {
671
+ for (const [agentId, sa] of subAgents) {
672
+ if (sa.parentToolUseId === event.toolUseId) {
673
+ const next = new Map(subAgents)
674
+ // Bump milestoneVersion โ€” parent tool_result is a milestone transition.
675
+ next.set(agentId, {
676
+ ...sa,
677
+ state: nextState,
678
+ finishedAt: now,
679
+ milestoneVersion: (sa.milestoneVersion ?? 0) + 1,
680
+ })
681
+ subAgents = next
682
+ break
683
+ }
684
+ }
685
+ if (pendingAgentSpawns.has(event.toolUseId)) {
686
+ const next = new Map(pendingAgentSpawns)
687
+ next.delete(event.toolUseId)
688
+ pendingAgentSpawns = next
689
+ }
690
+ }
691
+ return { ...state, items, subAgents, pendingAgentSpawns }
692
+ }
693
+
694
+ case 'sub_agent_started': {
695
+ if (state.turnStartedAt === 0) return state
696
+ // Already known? (Defensive โ€” re-attach can re-emit.)
697
+ if (state.subAgents.has(event.agentId)) return state
698
+ // Try to correlate by prompt-text against pendingAgentSpawns. On
699
+ // hit: move pending โ†’ subAgents, link the matching [Main]
700
+ // ChecklistItem via spawnedAgentId, and consume the pending entry.
701
+ // On miss: register as orphan; the parent's tool_use may arrive
702
+ // later (reverse race) and adopt via the tool_use case.
703
+ let parentToolUseId: string | null = null
704
+ let description = '(uncorrelated)'
705
+ let subagentType: string | undefined
706
+ let pendingAgentSpawns = state.pendingAgentSpawns
707
+ let items = state.items
708
+ for (const [parentId, pending] of pendingAgentSpawns) {
709
+ if (pending.promptText === event.firstPromptText) {
710
+ parentToolUseId = parentId
711
+ description = pending.description
712
+ subagentType = pending.subagentType
713
+ const nextPending = new Map(pendingAgentSpawns)
714
+ nextPending.delete(parentId)
715
+ pendingAgentSpawns = nextPending
716
+ // Link the [Main] checklist item back so renderer can keep
717
+ // its ๐Ÿค– state consistent.
718
+ items = items.map((it) =>
719
+ it.toolUseId === parentId
720
+ ? { ...it, spawnedAgentId: event.agentId }
721
+ : it,
722
+ )
723
+ break
724
+ }
725
+ }
726
+ const sub: SubAgentState = {
727
+ agentId: event.agentId,
728
+ description,
729
+ subagentType: subagentType ?? event.subagentType,
730
+ parentToolUseId,
731
+ state: 'running',
732
+ startedAt: now,
733
+ toolCount: 0,
734
+ firstPromptText: event.firstPromptText,
735
+ nestedSpawnCount: 0,
736
+ milestoneVersion: 1,
737
+ lastEventAt: now,
738
+ recentCompletedTools: [],
739
+ tasks: [],
740
+ }
741
+ const subAgents = new Map(state.subAgents)
742
+ subAgents.set(event.agentId, sub)
743
+ // Log correlation result. For orphans: include the promptText prefix
744
+ // and the count of pending spawns so callers can diagnose WHY the
745
+ // match failed (empty pendingAgentSpawns = no parent tool_use arrived
746
+ // yet; promptText mismatch = race between spawn and text delivery).
747
+ if (parentToolUseId != null) {
748
+ process.stderr.write(`telegram gateway: progress-card: sub_agent_started agentId=${event.agentId} correlated=yes parentToolUseId=${parentToolUseId}\n`)
749
+ } else {
750
+ const promptSnip = (event.firstPromptText ?? '').slice(0, 80).replace(/\n/g, ' ')
751
+ const pendingCount = state.pendingAgentSpawns.size
752
+ process.stderr.write(`telegram gateway: progress-card: sub_agent_started agentId=${event.agentId} correlated=orphan pendingSpawns=${pendingCount} promptSnip="${promptSnip}" โ€” NOTE: orphan sub-agents no longer gate parent turn_end defer (#31 fix)\n`)
753
+ }
754
+ // Gate parent narrative steps: if a narrative has a pendingAgentToolUseId
755
+ // matching this new sub-agent's parentToolUseId, migrate it from
756
+ // pendingAgentToolUseIds โ†’ awaitingSubAgentIds so the narrative knows
757
+ // which agentId to watch for completion (fixes #324).
758
+ const narratives = parentToolUseId != null
759
+ ? state.narratives.map(n => {
760
+ if (n.pendingAgentToolUseIds.includes(parentToolUseId)) {
761
+ return {
762
+ ...n,
763
+ pendingAgentToolUseIds: n.pendingAgentToolUseIds.filter(id => id !== parentToolUseId),
764
+ awaitingSubAgentIds: [...n.awaitingSubAgentIds, event.agentId],
765
+ }
766
+ }
767
+ return n
768
+ })
769
+ : state.narratives
770
+ return { ...state, subAgents, pendingAgentSpawns, items, narratives }
771
+ }
772
+
773
+ case 'sub_agent_text': {
774
+ // Per-sub-agent analogue of the parent `text` case: stash the raw
775
+ // text as a candidate preamble for THIS sub-agent's next
776
+ // sub_agent_tool_use. toolLabel() applies the single-line + length
777
+ // gate so we pass the full text through unfiltered. No-op if the
778
+ // sub-agent isn't known yet (defensive: sub_agent_started should
779
+ // always precede sub_agent_text in the same JSONL).
780
+ const sa = state.subAgents.get(event.agentId)
781
+ if (!sa) return state
782
+ const next = new Map(state.subAgents)
783
+ next.set(event.agentId, {
784
+ ...sa,
785
+ pendingPreamble: event.text,
786
+ // Capture the first narrative line for the description-fallback
787
+ // chain. Once set, never overwrite โ€” we want the sub-agent's
788
+ // initial framing, not its latest chatter.
789
+ firstNarrativeText: sa.firstNarrativeText ?? event.text,
790
+ lastEventAt: now,
791
+ })
792
+ return { ...state, subAgents: next }
793
+ }
794
+
795
+ case 'sub_agent_narrative': {
796
+ // Issue #305 Option A: most-recent-wins narrative line pushed by the
797
+ // sub-agent via the gateway's `progress_update` MCP tool. Replace-only
798
+ // (last write wins); no milestoneVersion bump (per-tick update, not a
799
+ // structural transition). No-op if the sub-agent isn't known yet.
800
+ const sa = state.subAgents.get(event.agentId)
801
+ if (!sa) return state
802
+ const next = new Map(state.subAgents)
803
+ next.set(event.agentId, {
804
+ ...sa,
805
+ currentNarrative: event.text,
806
+ lastEventAt: now,
807
+ })
808
+ return { ...state, subAgents: next }
809
+ }
810
+
811
+ case 'sub_agent_tool_use': {
812
+ const sa = state.subAgents.get(event.agentId)
813
+ if (!sa) return state
814
+ // Consume pendingPreamble exactly once โ€” same one-shot semantic as
815
+ // the parent path (3ad8436): the first sub_agent_tool_use after a
816
+ // sub_agent_text pairs with it; sibling tool_uses in the same
817
+ // assistant message fall back to filename/pattern.
818
+ const preamble = sa.pendingPreamble ?? undefined
819
+ // Mirror the parent tool_use TodoWrite handling: a sub-agent's
820
+ // TodoWrite atomically replaces its tasks slice for the per-agent
821
+ // card render.
822
+ let tasks = sa.tasks
823
+ if (event.toolName === 'TodoWrite') {
824
+ const parsed = parseTodoWriteInput(event.input)
825
+ if (parsed != null) tasks = parsed
826
+ }
827
+ const next = new Map(state.subAgents)
828
+ next.set(event.agentId, {
829
+ ...sa,
830
+ // toolCount is incremented on sub_agent_tool_result (not here) so
831
+ // the count reflects completed tools โ€” matching the semantics the
832
+ // renderer surfaces as "N tools total" (Gap 5 fix, #316).
833
+ currentTool: event.toolUseId
834
+ ? {
835
+ tool: event.toolName,
836
+ label: toolLabel(event.toolName, event.input, preamble),
837
+ humanAuthored: isHumanDescription(event.toolName, event.input),
838
+ toolUseId: event.toolUseId,
839
+ startedAt: now,
840
+ }
841
+ : sa.currentTool,
842
+ pendingPreamble: null,
843
+ lastEventAt: now,
844
+ tasks,
845
+ })
846
+ return { ...state, subAgents: next }
847
+ }
848
+
849
+ case 'sub_agent_tool_result': {
850
+ const sa = state.subAgents.get(event.agentId)
851
+ if (!sa) return state
852
+ // Per design ยง3.3: per-tool errors don't fail the agent; only the
853
+ // parent's tool_result does. We clear currentTool if it matches,
854
+ // AND stash it as lastCompletedTool so the render fallback chain
855
+ // can surface "just finished X" instead of a bare "(idle)" line
856
+ // while the sub-agent thinks between tools (Gap 6 fix, #316).
857
+ // toolCount increments here (on result, not on use) so the count
858
+ // reflects completed tools โ€” consistent with render semantics and
859
+ // the spec in the issue (Gap 5 fix, #316).
860
+ if (sa.currentTool && sa.currentTool.toolUseId === event.toolUseId) {
861
+ const justFinished = {
862
+ tool: sa.currentTool.tool,
863
+ label: sa.currentTool.label,
864
+ humanAuthored: sa.currentTool.humanAuthored,
865
+ finishedAt: now,
866
+ }
867
+ // Maintain a ring buffer of the last 2 completed tools for the
868
+ // expandable section (issue #352). Slide the window: drop the oldest
869
+ // when we're already at capacity (2), then append the new entry.
870
+ const prevRecent = sa.recentCompletedTools ?? []
871
+ const nextRecent = [...prevRecent, justFinished].slice(-2)
872
+ const next = new Map(state.subAgents)
873
+ next.set(event.agentId, {
874
+ ...sa,
875
+ currentTool: undefined,
876
+ lastCompletedTool: justFinished,
877
+ recentCompletedTools: nextRecent,
878
+ toolCount: sa.toolCount + 1,
879
+ lastEventAt: now,
880
+ })
881
+ return { ...state, subAgents: next }
882
+ }
883
+ return state
884
+ }
885
+
886
+ case 'sub_agent_turn_end': {
887
+ const sa = state.subAgents.get(event.agentId)
888
+ if (!sa) return state
889
+ // Tentative close: parent's tool_result is still authoritative.
890
+ // If it later arrives with isError=true, the tool_result case
891
+ // overrides this 'done' with 'failed'. Clear any lingering
892
+ // pendingPreamble defensively โ€” mirrors the parent turn_end path.
893
+ // Bump milestoneVersion โ€” this is a milestone transition.
894
+ const next = new Map(state.subAgents)
895
+ next.set(event.agentId, {
896
+ ...sa,
897
+ state: 'done',
898
+ finishedAt: now,
899
+ pendingPreamble: null,
900
+ milestoneVersion: (sa.milestoneVersion ?? 0) + 1,
901
+ })
902
+ // Gate parent narrative steps (#324): remove this agentId from any
903
+ // narrative step's awaitingSubAgentIds. If a step's awaiting list
904
+ // becomes empty (all sub-agents done) and the step is in
905
+ // `awaiting-subagent` state, flip it to `done`.
906
+ const narratives = state.narratives.map(n => {
907
+ if (!n.awaitingSubAgentIds.includes(event.agentId)) return n
908
+ const remaining = n.awaitingSubAgentIds.filter(id => id !== event.agentId)
909
+ // Keep pendingAgentToolUseIds in mind: those migrate to awaitingSubAgentIds
910
+ // when their sub_agent_started fires. Only flip to done when BOTH
911
+ // lists are empty.
912
+ const allDone = remaining.length === 0 && n.pendingAgentToolUseIds.length === 0
913
+ return {
914
+ ...n,
915
+ awaitingSubAgentIds: remaining,
916
+ state: (n.state === 'awaiting-subagent' && allDone) ? ('done' as const) : n.state,
917
+ }
918
+ })
919
+ return { ...state, subAgents: next, narratives }
920
+ }
921
+
922
+ case 'sub_agent_nested_spawn': {
923
+ const sa = state.subAgents.get(event.agentId)
924
+ if (!sa) return state
925
+ const next = new Map(state.subAgents)
926
+ next.set(event.agentId, { ...sa, nestedSpawnCount: sa.nestedSpawnCount + 1 })
927
+ return { ...state, subAgents: next }
928
+ }
929
+
930
+ case 'turn_end': {
931
+ if (state.turnStartedAt === 0) return state
932
+ const items = state.items.map((it) =>
933
+ it.state === 'running' ? { ...it, state: 'done' as const, finishedAt: now } : it,
934
+ )
935
+ // Running sub-agents may outlive parent turn_end (common for background
936
+ // `Agent(run_in_background=true)` calls โ€” parent returns immediately
937
+ // but the sub-agent keeps working). Leave them in `state: 'running'`
938
+ // so their card surface stays informative, and let them close via
939
+ // their own `sub_agent_turn_end` event (or via the driver's
940
+ // abandonment path on maxIdle / enqueue-force-close). For sub-agents
941
+ // already done, clear pendingPreamble defensively.
942
+ const subAgents = new Map<string, SubAgentState>()
943
+ for (const [k, sa] of state.subAgents) {
944
+ if (sa.state === 'running') {
945
+ subAgents.set(k, sa)
946
+ } else {
947
+ subAgents.set(k, { ...sa, pendingPreamble: null })
948
+ }
949
+ }
950
+ // At turn_end, pass the up-to-date subAgents map (built above) so
951
+ // narrativeTransitionFromActive can see which sub-agents are still
952
+ // running. Active narratives that dispatched background sub-agents
953
+ // become `awaiting-subagent`; the rest become `done` (#324).
954
+ const narratives = state.narratives.map(n =>
955
+ n.state === 'active' ? narrativeTransitionFromActive(n, subAgents) : n,
956
+ )
957
+ return {
958
+ ...state,
959
+ items,
960
+ narratives,
961
+ subAgents,
962
+ pendingAgentSpawns: new Map(),
963
+ stage: 'done',
964
+ thinking: false,
965
+ pendingPreamble: null,
966
+ }
967
+ }
968
+
969
+ case 'dequeue':
970
+ // No-op โ€” we key off enqueue + turn_end for the turn boundary.
971
+ return state
972
+ }
973
+ // Defensive: tsc can't prove exhaustiveness across the SessionEvent
974
+ // discriminated union when new kinds are added incrementally (#623
975
+ // strict-tsc enforcement on plugin source). Fall back to current
976
+ // state on any future unknown kind.
977
+ return state
978
+ }
979
+
980
+ // โ”€โ”€โ”€ Renderer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
981
+
982
+ const STEP_DONE = 'โ—'
983
+ const STEP_ACTIVE = 'โ—‰'
984
+ const STEP_FAILED = 'โœ—'
985
+ const STEP_PENDING = 'โ—‹'
986
+
987
+ const TOOL_SYMBOL: Record<ItemState, string> = {
988
+ pending: STEP_PENDING,
989
+ running: STEP_ACTIVE,
990
+ done: STEP_DONE,
991
+ failed: STEP_FAILED,
992
+ }
993
+
994
+ /**
995
+ * Max checklist lines to render inline. Older completed items collapse
996
+ * into a synthetic "(+N more earlier steps)" rollup line so the card
997
+ * stays compact during long turns. Chosen to fit comfortably on a
998
+ * mobile Telegram screen without scroll.
999
+ */
1000
+ const MAX_VISIBLE_ITEMS = 5
1001
+
1002
+ // Re-export the shared formatters so existing callers (and the test
1003
+ // file `tests/progress-card.test.ts`) keep working. The implementation
1004
+ // lives in `./card-format.js` โ€” see issue #94.
1005
+ export const formatDuration = sharedFormatDuration
1006
+ const escapeHtml = sharedEscapeHtml
1007
+ const truncate = sharedTruncate
1008
+
1009
+ /**
1010
+ * Strip the `<channel โ€ฆ>` XML wrapper (if present) from the enqueue raw
1011
+ * content, returning the plain user message text.
1012
+ */
1013
+ function extractUserText(raw: string): string {
1014
+ // The enqueue raw content typically looks like:
1015
+ // <channel source="switchroom-telegram" chat_id="โ€ฆ" โ€ฆ>USER TEXT</channel>
1016
+ const m = raw.match(/<channel[^>]*>([\s\S]*?)<\/channel>/)
1017
+ const body = m ? m[1] : raw
1018
+ return body.trim()
1019
+ }
1020
+
1021
+ /**
1022
+ * Render a single checklist line body: tool name + a short label hint.
1023
+ *
1024
+ * Format: `<code>tool</code> <code>label</code>` โ€” both tool name and
1025
+ * target argument use fixed-width formatting for scanability on mobile
1026
+ * narrow screens. The sub-agent `Agent` tool uses a colon separator
1027
+ * ("Agent: <description>") because the description is a phrase, not a
1028
+ * filename.
1029
+ *
1030
+ * `running` items bold the tool name so the eye jumps to the line that's
1031
+ * currently in flight.
1032
+ */
1033
+ function renderItemCore(
1034
+ tool: string,
1035
+ label: string,
1036
+ bold = false,
1037
+ humanAuthored = false,
1038
+ ): string {
1039
+ // MCP tools: the label from toolLabel() already begins with a
1040
+ // prettified "Server: action" form (from mcpBaseLabel), so echoing
1041
+ // the raw `mcp__server__action` tool name as a prefix just duplicates
1042
+ // the friendly name. Render the label alone. If label is empty
1043
+ // (malformed mcp__ name, no input keys to preview), fall through so
1044
+ // the raw tool name still appears rather than rendering nothing.
1045
+ //
1046
+ // humanAuthored: Bash/BashOutput/Task/Agent tool_use items whose label
1047
+ // came from input.description (a human-written phrase) rather than a
1048
+ // raw command / fallback. Suppress the tool-name prefix for the same
1049
+ // reason as MCP tools โ€” the description is already self-explanatory.
1050
+ if ((tool.startsWith('mcp__') || humanAuthored) && label) {
1051
+ return bold ? `<b>${escapeHtml(label)}</b>` : escapeHtml(label)
1052
+ }
1053
+ const toolHtml = bold ? `<b><code>${escapeHtml(tool)}</code></b>` : `<code>${escapeHtml(tool)}</code>`
1054
+ if (!label) return toolHtml
1055
+ const separator = tool === 'Agent' || tool === 'Task' ? ': ' : ' '
1056
+ return `${toolHtml}${separator}<code>${escapeHtml(label)}</code>`
1057
+ }
1058
+
1059
+ /**
1060
+ * Cap the visible checklist at MAX_VISIBLE_ITEMS. When more items exist,
1061
+ * the OLDEST completed items collapse into a "(+N more earlier steps)"
1062
+ * synthetic line rendered by render(); any still-running item is always
1063
+ * kept visible, even if that means pushing the visible tail beyond the cap.
1064
+ *
1065
+ * Exported for tests.
1066
+ */
1067
+ export function applyVisibleCap(
1068
+ items: ReadonlyArray<RolledItem>,
1069
+ ): { items: RolledItem[]; overflowCount: number } {
1070
+ if (items.length <= MAX_VISIBLE_ITEMS) {
1071
+ return { items: items.slice(), overflowCount: 0 }
1072
+ }
1073
+ // Take the last N; anything before that is collapsed. Running items
1074
+ // tend to be at the tail (new tool_use appends), so this naturally
1075
+ // keeps them visible.
1076
+ const tail = items.slice(items.length - MAX_VISIBLE_ITEMS)
1077
+ const dropped = items.length - tail.length
1078
+ // Count the dropped items by their underlying `count` when rolled up,
1079
+ // so a collapsed "Read ร—6" contributes 6 to the overflow count rather
1080
+ // than 1. Gives the user a meaningful "+N" signal.
1081
+ let overflow = 0
1082
+ for (let i = 0; i < dropped; i++) {
1083
+ overflow += items[i].count ?? 1
1084
+ }
1085
+ return { items: tail, overflowCount: overflow }
1086
+ }
1087
+
1088
+ /**
1089
+ * Render the current state to Telegram HTML. `now` is the wall-clock time
1090
+ * used for elapsed-time calculations so the render is deterministic in tests.
1091
+ *
1092
+ * Multi-agent: when `PROGRESS_CARD_MULTI_AGENT=1` AND there is any sub-agent
1093
+ * activity (subAgents non-empty OR pendingAgentSpawns non-empty), the card
1094
+ * splits into [Main] / [Sub-agents] sections. Otherwise the layout is
1095
+ * byte-identical to the legacy single-section card.
1096
+ */
1097
+ /**
1098
+ * Optional task-counter hint passed to render() by the driver when multiple
1099
+ * concurrent tasks are active in the same chat (e.g. parallel forum topics).
1100
+ * When provided, the header shows "(N/M)" so users can see "task 1 of 2".
1101
+ */
1102
+ export interface TaskNum {
1103
+ /** 1-based position of this task among the active tasks. */
1104
+ index: number
1105
+ /** Total number of currently active tasks in the chat. */
1106
+ total: number
1107
+ }
1108
+
1109
+ /**
1110
+ * Extra render hints the driver computes per flush. `stuckMs` is the
1111
+ * gap between the caller's clock and the last real session event that
1112
+ * updated this card. When it crosses STUCK_THRESHOLD_MS (2 min) the
1113
+ * renderer inserts a โš ๏ธ stuck-warning line under the header. Zombie
1114
+ * closure (driver `maxIdleMs`) still fires at its configured ceiling โ€”
1115
+ * the warning is the earlier, softer signal users see first.
1116
+ */
1117
+ export interface RenderOptions {
1118
+ stuckMs?: number
1119
+ /**
1120
+ * Issue #132: when a turn ends without the agent ever calling
1121
+ * `reply` / `stream_reply`, the card should NOT render as "โœ… Done"
1122
+ * (which the user reads as "agent acknowledged and replied") because
1123
+ * no user-visible text was produced. The driver tracks per-chat
1124
+ * "did a reply tool fire" and forwards the answer here so the
1125
+ * renderer can distinguish the silent-end case.
1126
+ *
1127
+ * When true and the turn is terminal, the header swaps to
1128
+ * "๐Ÿ™Š Ended without reply" with a hint line suggesting `/restart` or
1129
+ * a rephrase. Has no effect while the turn is still running.
1130
+ */
1131
+ silentEnd?: boolean
1132
+ /**
1133
+ * Issue #137: the agent DID call `reply` / `stream_reply` this turn
1134
+ * but no outbound message ever actually landed in the chat
1135
+ * (recordOutboundDelivered was never called for the card). Distinct
1136
+ * from silentEnd because the agent tried โ€” the failure is in the
1137
+ * delivery layer (MCP bridge instability, dropped streams, etc.),
1138
+ * not the model going mute.
1139
+ *
1140
+ * Mutually exclusive with silentEnd at the driver layer (replyNot-
1141
+ * Delivered requires replyToolCalled=true; silentEnd requires it
1142
+ * false), but the renderer guards with `!silentEnd` to be safe.
1143
+ * When true and the turn is terminal, the header swaps to
1144
+ * "โš ๏ธ Reply attempted but not delivered".
1145
+ */
1146
+ replyNotDelivered?: boolean
1147
+ /**
1148
+ * Gap 8 (decoupled render and unpin): when true, the parent turn has
1149
+ * ended (turn_end received) but sub-agents are still running. The
1150
+ * renderer shows "โœ… Done" in the parent header immediately rather than
1151
+ * "โš™๏ธ Workingโ€ฆ", while sub-agent rows still show their running state.
1152
+ * Distinct from `silentEnd` / `replyNotDelivered` โ€” those apply only
1153
+ * on true terminal state. This flag applies during the deferred-unpin
1154
+ * window.
1155
+ */
1156
+ parentDone?: boolean
1157
+ /**
1158
+ * Gap 8 (stalled forced close): when true, the deferred-completion
1159
+ * timeout fired (sub-agents never reported done). Render a "stalled"
1160
+ * header rather than "โœ… Done" to signal forced closure.
1161
+ */
1162
+ stalledClose?: boolean
1163
+ }
1164
+
1165
+ /**
1166
+ * Below this age the renderer treats the card as "fresh" and hides the
1167
+ * stuck-warning entirely. The 120s cutoff matches the spec in
1168
+ * `docs/pinned-progress-card-reliability.md` ยง5 F10.
1169
+ */
1170
+ export const STUCK_THRESHOLD_MS = 2 * 60_000
1171
+
1172
+ /**
1173
+ * Cache entry for a sub-agent's `<blockquote expandable>` section. The
1174
+ * driver holds one of these per sub-agent and passes the whole map to
1175
+ * render() on each flush. When the sub-agent's `milestoneVersion` hasn't
1176
+ * changed, render() reuses the cached HTML instead of re-building it โ€”
1177
+ * this prevents the edit from touching the expandable section, so the
1178
+ * user's expanded view survives per-tool throttle ticks.
1179
+ */
1180
+ export interface ExpandableCacheEntry {
1181
+ milestoneVersion: number
1182
+ html: string
1183
+ }
1184
+
1185
+ /** Keyed by agentId. */
1186
+ export type ExpandableCache = Map<string, ExpandableCacheEntry>
1187
+
1188
+ export function render(
1189
+ state: ProgressCardState,
1190
+ now: number,
1191
+ taskNum?: TaskNum,
1192
+ opts?: RenderOptions,
1193
+ expandableCache?: ExpandableCache,
1194
+ fleet?: ReadonlyMap<string, FleetMember>,
1195
+ ): string {
1196
+ // P4 of #662 โ€” two-zone renderer is the ONLY renderer. The legacy
1197
+ // <blockquote expandable> path was deleted in the same PR (no env
1198
+ // flag remains). expandableCache is retained for caller compatibility
1199
+ // but the two-zone renderer does not consume it.
1200
+ void expandableCache
1201
+ return renderTwoZoneCard({ state, fleet: fleet ?? new Map(), now, taskNum, opts })
1202
+ }
1203
+
1204
+ function renderNarrativeChecklist(
1205
+ narratives: ReadonlyArray<NarrativeStep>,
1206
+ now: number,
1207
+ lines: string[],
1208
+ ): void {
1209
+ if (narratives.length > MAX_VISIBLE_ITEMS) {
1210
+ const overflow = narratives.length - MAX_VISIBLE_ITEMS
1211
+ lines.push(`<i>(+${overflow} earlier)</i>`)
1212
+ }
1213
+ const visible = narratives.slice(-MAX_VISIBLE_ITEMS)
1214
+ for (const step of visible) {
1215
+ if (step.state === 'active' || step.state === 'awaiting-subagent') {
1216
+ const age = now - step.startedAt
1217
+ const dur = formatDuration(age)
1218
+ // When an active (or awaiting-subagent) narrative is older than the
1219
+ // stuck threshold, the "No events for X" banner will already be rendered
1220
+ // above. A confidently-bolded narrative with a ticking age next to it
1221
+ // sends mixed signals ("stuck" vs "actively working on X"). De-emphasise
1222
+ // the narrative to italic with a `stale` marker so the signals agree:
1223
+ // the last announced step, not necessarily what's running right now.
1224
+ if (age > STUCK_THRESHOLD_MS) {
1225
+ lines.push(`${STEP_ACTIVE} <i>${escapeHtml(step.text)} ยท stale (${dur})</i>`)
1226
+ } else {
1227
+ lines.push(`${STEP_ACTIVE} <b>${escapeHtml(step.text)}</b> <i>(${dur})</i>`)
1228
+ }
1229
+ } else {
1230
+ // #320: drop the <s>...</s> wrap on done items. Telegram desktop
1231
+ // renders strikethrough with a salmon/red strike-line in both
1232
+ // light and dark themes โ€” users read it as "deleted/failed/error",
1233
+ // not "done". The leading STEP_DONE bullet (โ—) + the symbol
1234
+ // distinction (vs โ—‰ for active) + bold-vs-plain weight already
1235
+ // signal completion without the alarm. See #320 Option A.
1236
+ lines.push(`${STEP_DONE} ${escapeHtml(step.text)}`)
1237
+ }
1238
+ }
1239
+ }
1240
+
1241
+ /**
1242
+ * Render one [Main]-section line. Encapsulates the existing per-state
1243
+ * branches (running/rollup/done) so the main render() loop reads cleanly.
1244
+ *
1245
+ * Multi-agent twist (Ken locked-in #4): an `Agent`/`Task` item with a
1246
+ * correlated, still-running sub-agent stays in the ๐Ÿค– emoji even if its
1247
+ * own state field already happens to be 'running' โ€” and we DON'T flip it
1248
+ * to โœ… on a tentative `sub_agent_turn_end`. Only the parent's own
1249
+ * tool_result (which mutates `item.state` to 'done'/'failed') flips it.
1250
+ */
1251
+ function renderMainItem(
1252
+ item: RolledItem,
1253
+ now: number,
1254
+ multiAgentActive: boolean,
1255
+ subAgents: ReadonlyMap<string, SubAgentState>,
1256
+ ): string {
1257
+ const isAgent = item.tool === 'Agent' || item.tool === 'Task'
1258
+ const indent = multiAgentActive ? ' ' : ''
1259
+
1260
+ const humanAuthored = item.humanAuthored ?? false
1261
+
1262
+ // #378 sub-issue 1: when an Agent/Task item has a correlated, still-
1263
+ // alive sub-agent and the multi-agent renderer is active (i.e. the
1264
+ // sub-agent's expandable WILL be drawn below), the Main row would be
1265
+ // a duplicate (same ๐Ÿค– emoji, same description, same elapsed). Return
1266
+ // empty so the outer render loop skips this row. With multiAgentActive
1267
+ // off, the sub-agent expandable does NOT render โ€” fall through to the
1268
+ // normal Main-row render so the user still sees something.
1269
+ //
1270
+ // The "Main ยท N tools" header count is intentionally NOT decremented โ€”
1271
+ // it reflects "N tool calls happened this turn" as a lifetime count,
1272
+ // not visible rows.
1273
+ if (
1274
+ multiAgentActive
1275
+ && isAgent
1276
+ && item.kind !== 'rollup'
1277
+ && item.state === 'running'
1278
+ && item.spawnedAgentId
1279
+ && subAgents.has(item.spawnedAgentId)
1280
+ ) {
1281
+ return ''
1282
+ }
1283
+
1284
+ if (isAgent && item.state === 'running' && multiAgentActive) {
1285
+ // Pre-correlation (or sub-agent already terminal), hold the ๐Ÿค–
1286
+ // emoji on the Main row. Show elapsed since the parent's tool_use
1287
+ // fired.
1288
+ const dur = formatDuration(now - item.startedAt)
1289
+ return `${indent}๐Ÿค– ${renderItemCore(item.tool, item.label, /*bold*/ true, humanAuthored)} <i>(${dur})</i>`
1290
+ }
1291
+
1292
+ const symbol = TOOL_SYMBOL[item.state]
1293
+ if (item.state === 'running') {
1294
+ const dur = formatDuration(now - item.startedAt)
1295
+ return `${indent}${symbol} ${renderItemCore(item.tool, item.label, /*bold*/ true, humanAuthored)} <i>(${dur})</i>`
1296
+ }
1297
+ if ((item.state === 'done' || item.state === 'failed') && item.finishedAt != null) {
1298
+ if (item.kind === 'rollup') {
1299
+ const labelHtml = item.label ? ` ${escapeHtml(item.label)}` : ''
1300
+ return `${indent}${symbol} ${escapeHtml(item.tool)}${labelHtml} <i>ร—${item.count}</i>`
1301
+ }
1302
+ const dur = formatDuration(item.finishedAt - item.startedAt)
1303
+ const needsDuration = item.finishedAt - item.startedAt >= 1000
1304
+ // #320: no <s> wrap on done items here either. The symbol
1305
+ // distinction (โ— vs โ—‰) + the bold-vs-plain treatment already
1306
+ // differentiate done from active; strikethrough renders red in
1307
+ // Telegram desktop and reads as "deleted/failed". See #320
1308
+ // Option A โ€” this aligns the rolled-card path with the
1309
+ // narrative-checklist + sub-agent-expandable paths now that all
1310
+ // three drop strikethrough.
1311
+ return `${indent}${symbol} ${renderItemCore(item.tool, item.label, false, humanAuthored)}${needsDuration ? ` <i>(${dur})</i>` : ''}`
1312
+ }
1313
+ void subAgents
1314
+ return `${indent}${symbol} ${renderItemCore(item.tool, item.label, false, humanAuthored)}`
1315
+ }
1316
+
1317
+
1318
+ /**
1319
+ * Collapse runs of consecutive identical tools (e.g. a slurry of Reads)
1320
+ * into a single rollup item "<tool> [label] ร—N". Two thresholds apply:
1321
+ *
1322
+ * - ROLLUP_THRESHOLD (2): identical tool + identical label โ†’ collapses to
1323
+ * "<tool> <label> ร—N", preserving the shared label in the rollup so the
1324
+ * user can still see "Read foo.ts ร—3" instead of three identical lines.
1325
+ *
1326
+ * - MIXED_ROLLUP_THRESHOLD (3): identical tool, differing labels โ†’ collapses
1327
+ * to "<tool> ร—N" (no label) when there are 3+ items. The label is dropped
1328
+ * because there is no single representative value, and showing one
1329
+ * arbitrarily would be misleading. Users see "Read ร—4" (heuristic summary).
1330
+ *
1331
+ * Partial runs (any item still running) are never collapsed โ€” the running
1332
+ * item is always shown individually so the user can see live progress.
1333
+ *
1334
+ * Human-authored items are never collapsed into a bare "Tool ร—N" rollup (#41).
1335
+ * When any item in the run has `humanAuthored=true`, each is rendered
1336
+ * individually so the agent's natural-language descriptions remain visible.
1337
+ */
1338
+ interface RolledItem extends ChecklistItem {
1339
+ readonly kind?: 'single' | 'rollup'
1340
+ readonly count?: number
1341
+ }
1342
+
1343
+ /** Minimum run length to collapse same-tool + same-label items. */
1344
+ const ROLLUP_THRESHOLD = 2
1345
+ /** Minimum run length to collapse same-tool, mixed-label items (C1 heuristic). */
1346
+ const MIXED_ROLLUP_THRESHOLD = 3
1347
+
1348
+ // Exported for tests.
1349
+ export function compactItems(items: ReadonlyArray<ChecklistItem>): RolledItem[] {
1350
+ const out: RolledItem[] = []
1351
+ let run: ChecklistItem[] = []
1352
+
1353
+ const flush = (): void => {
1354
+ if (run.length === 0) return
1355
+ const first = run[0]
1356
+ const last = run[run.length - 1]
1357
+ const allDone = run.every((r) => r.state === 'done')
1358
+ const sameLabel = run.every((r) => r.label === first.label)
1359
+ // Never collapse a run that contains any human-authored item (#41 fix).
1360
+ // Descriptions written by the agent ("Check commit state", "Run tests")
1361
+ // are valuable context โ€” collapsing them into "Bash ร—N" discards that
1362
+ // signal. Each human-authored item must appear as its own line.
1363
+ const anyHumanAuthored = run.some((r) => r.humanAuthored)
1364
+
1365
+ if (allDone && !anyHumanAuthored && sameLabel && run.length >= ROLLUP_THRESHOLD) {
1366
+ // B3 + B1: identical tool + identical label โ†’ rollup keeping the label
1367
+ out.push({
1368
+ id: first.id,
1369
+ toolUseId: null,
1370
+ tool: first.tool,
1371
+ label: first.label,
1372
+ humanAuthored: first.humanAuthored,
1373
+ state: 'done',
1374
+ startedAt: first.startedAt,
1375
+ finishedAt: last.finishedAt,
1376
+ kind: 'rollup',
1377
+ count: run.length,
1378
+ })
1379
+ } else if (allDone && !anyHumanAuthored && !sameLabel && run.length >= MIXED_ROLLUP_THRESHOLD) {
1380
+ // C1: same tool, mixed labels, no human-authored โ†’ rollup without label
1381
+ out.push({
1382
+ id: first.id,
1383
+ toolUseId: null,
1384
+ tool: first.tool,
1385
+ label: '',
1386
+ humanAuthored: false,
1387
+ state: 'done',
1388
+ startedAt: first.startedAt,
1389
+ finishedAt: last.finishedAt,
1390
+ kind: 'rollup',
1391
+ count: run.length,
1392
+ })
1393
+ } else {
1394
+ for (const r of run) out.push({ ...r, kind: 'single' })
1395
+ }
1396
+ run = []
1397
+ }
1398
+
1399
+ for (const it of items) {
1400
+ if (run.length > 0 && run[run.length - 1].tool === it.tool) {
1401
+ run.push(it)
1402
+ } else {
1403
+ flush()
1404
+ run = [it]
1405
+ }
1406
+ }
1407
+ flush()
1408
+ return out
1409
+ }