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,863 @@
1
+ /**
2
+ * Boot-card probes — live evidential data gathered at gateway startup.
3
+ *
4
+ * Each probe returns a ProbeResult within its timeout budget. All probes
5
+ * are run concurrently via Promise.allSettled; callers supply a 2.5s wall
6
+ * clock budget and let this module own the per-probe 2s guard.
7
+ *
8
+ * Probes are defensive by design: every file read guards ENOENT, every
9
+ * network call is wrapped in a race timeout, every field access uses
10
+ * optional-chaining. A failure in one probe must never surface to the
11
+ * caller as a thrown error — only as ProbeResult{ status:'fail', ... }.
12
+ */
13
+
14
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs'
15
+ import { join } from 'path'
16
+ import { execFile as execFileCb } from 'child_process'
17
+ import { promisify } from 'util'
18
+
19
+ import { readQuotaCache, writeQuotaCache } from './quota-cache.js'
20
+
21
+ const execFile = promisify(execFileCb)
22
+
23
+ // ─── Types ──────────────────────────────────────────────────────────────────
24
+
25
+ export type ProbeStatus = 'ok' | 'degraded' | 'fail'
26
+
27
+ export interface ProbeResult {
28
+ status: ProbeStatus
29
+ label: string
30
+ detail: string
31
+ /** True when a 429 caused the probe to skip the live check. Used by
32
+ * writeQuotaCache to select the short RATE_LIMIT_TTL_MS instead of the
33
+ * default 5-min TTL. Keying off this boolean avoids matching on the
34
+ * user-facing detail string, which is a maintenance trap. */
35
+ rateLimited?: boolean
36
+ }
37
+
38
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
39
+
40
+ const PROBE_TIMEOUT_MS = 2000
41
+
42
+ /**
43
+ * Race a probe against a hard timeout. Returns a fail ProbeResult if the
44
+ * probe doesn't settle within timeoutMs.
45
+ */
46
+ async function withTimeout<T extends ProbeResult>(
47
+ label: string,
48
+ p: Promise<T>,
49
+ timeoutMs = PROBE_TIMEOUT_MS,
50
+ ): Promise<ProbeResult> {
51
+ let timer: ReturnType<typeof setTimeout>
52
+ const timeout = new Promise<ProbeResult>((resolve) => {
53
+ timer = setTimeout(() => resolve({ status: 'fail', label, detail: 'timed out' }), timeoutMs)
54
+ })
55
+ try {
56
+ return await Promise.race([p, timeout])
57
+ } finally {
58
+ clearTimeout(timer!)
59
+ }
60
+ }
61
+
62
+ function formatMs(ms: number): string {
63
+ if (ms < 1000) return `${ms}ms`
64
+ const s = ms / 1000
65
+ if (s < 60) return `${s.toFixed(1)}s`
66
+ const m = Math.floor(s / 60)
67
+ const r = Math.round(s % 60)
68
+ return r > 0 ? `${m}m ${r}s` : `${m}m`
69
+ }
70
+
71
+ function formatDaysFromNow(expiresAt: number): string {
72
+ const days = Math.round((expiresAt - Date.now()) / 86_400_000)
73
+ if (days < 0) return 'expired'
74
+ return `token ${days}d`
75
+ }
76
+
77
+ // ─── Probe: Account ──────────────────────────────────────────────────────────
78
+
79
+ interface ClaudeJson {
80
+ oauthAccount?: {
81
+ emailAddress?: string
82
+ displayName?: string
83
+ billingType?: string
84
+ hasExtraUsageEnabled?: boolean
85
+ }
86
+ }
87
+
88
+ interface OauthTokenMeta {
89
+ expiresAt?: number
90
+ createdAt?: number
91
+ }
92
+
93
+ function mapPlan(billingType?: string, hasExtra?: boolean): string {
94
+ if (!billingType) return 'unknown plan'
95
+ if (billingType === 'stripe_subscription') {
96
+ return hasExtra ? 'Pro+' : 'Pro'
97
+ }
98
+ if (billingType.toLowerCase().includes('max')) return 'Max'
99
+ return billingType
100
+ }
101
+
102
+ /**
103
+ * Threshold below which a still-valid OAuth token is treated as
104
+ * `degraded` so the boot card surfaces it before the user is locked
105
+ * out mid-turn. 7 days is the smallest window that still gives
106
+ * comfortable lead time for a manual reauth in normal use.
107
+ */
108
+ const TOKEN_EXPIRING_SOON_DAYS = 7
109
+
110
+ /**
111
+ * Read account info from the agent's .claude.json.
112
+ * agentDir: e.g. /home/user/.switchroom/agents/clerk
113
+ */
114
+ export async function probeAccount(agentDir: string): Promise<ProbeResult> {
115
+ return withTimeout('Account', (async (): Promise<ProbeResult> => {
116
+ const claudeDir = join(agentDir, '.claude')
117
+ const claudeJsonPath = join(claudeDir, '.claude.json')
118
+ let cfg: ClaudeJson = {}
119
+ try {
120
+ const raw = readFileSync(claudeJsonPath, 'utf8')
121
+ cfg = JSON.parse(raw) as ClaudeJson
122
+ } catch {
123
+ return { status: 'fail', label: 'Account', detail: 'no .claude.json' }
124
+ }
125
+
126
+ const acc = cfg.oauthAccount
127
+ if (!acc?.emailAddress) {
128
+ return { status: 'degraded', label: 'Account', detail: 'not signed in' }
129
+ }
130
+
131
+ const plan = mapPlan(acc.billingType, acc.hasExtraUsageEnabled)
132
+
133
+ // Read token expiry. Status is driven by the days-remaining bucket:
134
+ // < 0 days → fail (already expired — agent is locked out)
135
+ // < 7 days → degraded (surface so the user can reauth in time)
136
+ // ≥ 7 days → ok (no row in the boot card)
137
+ let tokenStr = ''
138
+ let status: ProbeStatus = 'ok'
139
+ for (const candidate of [
140
+ join(claudeDir, '.oauth-token.meta.json'),
141
+ join(claudeDir, 'accounts', 'default', '.oauth-token.meta.json'),
142
+ ]) {
143
+ if (existsSync(candidate)) {
144
+ try {
145
+ const meta = JSON.parse(readFileSync(candidate, 'utf8')) as OauthTokenMeta
146
+ if (meta.expiresAt) {
147
+ tokenStr = ' · ' + formatDaysFromNow(meta.expiresAt)
148
+ const daysLeft = Math.round((meta.expiresAt - Date.now()) / 86_400_000)
149
+ if (daysLeft < 0) status = 'fail'
150
+ else if (daysLeft < TOKEN_EXPIRING_SOON_DAYS) status = 'degraded'
151
+ }
152
+ } catch {}
153
+ break
154
+ }
155
+ }
156
+
157
+ return {
158
+ status,
159
+ label: 'Account',
160
+ detail: `${acc.emailAddress} · ${plan}${tokenStr}`,
161
+ }
162
+ })())
163
+ }
164
+
165
+ // ─── Probe: Agent process ────────────────────────────────────────────────────
166
+
167
+ function parseSystemctlKv(output: string): Record<string, string> {
168
+ const result: Record<string, string> = {}
169
+ for (const line of output.split('\n')) {
170
+ const eq = line.indexOf('=')
171
+ if (eq > 0) {
172
+ result[line.slice(0, eq).trim()] = line.slice(eq + 1).trim()
173
+ }
174
+ }
175
+ return result
176
+ }
177
+
178
+ function formatUptime(activeEnterTimestamp: string): string {
179
+ if (!activeEnterTimestamp || activeEnterTimestamp === '0') return ''
180
+ // systemctl outputs like "Thu 2026-04-26 10:15:30 UTC" or epoch microseconds
181
+ let ms: number
182
+ const epoch = Number(activeEnterTimestamp)
183
+ if (!isNaN(epoch) && epoch > 0) {
184
+ ms = Date.now() - Math.round(epoch / 1000)
185
+ } else {
186
+ const d = new Date(activeEnterTimestamp)
187
+ if (isNaN(d.getTime())) return ''
188
+ ms = Date.now() - d.getTime()
189
+ }
190
+ return ms > 0 ? `up ${formatMs(ms)}` : ''
191
+ }
192
+
193
+ function formatMemory(memoryCurrent: string): string {
194
+ const bytes = Number(memoryCurrent)
195
+ if (!isFinite(bytes) || bytes <= 0) return ''
196
+ const mb = Math.round(bytes / 1024 / 1024)
197
+ return `${mb} MB`
198
+ }
199
+
200
+ /**
201
+ * How often to retry after a non-active state during the re-probe loop.
202
+ * Exported for test injection.
203
+ */
204
+ export const AGENT_RETRY_INTERVAL_MS = 1500
205
+
206
+ /**
207
+ * Maximum additional wait beyond the settle window before committing to
208
+ * whatever the final state is. Exported for test injection.
209
+ */
210
+ export const AGENT_RETRY_MAX_MS = 12_000
211
+
212
+ /**
213
+ * How long the boot-card live-agent-status loop keeps polling and editing
214
+ * the card in-place after the initial probe run. The loop exits early as
215
+ * soon as the agent reaches `active`. If the window expires without the
216
+ * agent becoming active, the card commits to whatever state is current.
217
+ *
218
+ * 45 s covers the typical systemd restart cycle (deactivating → inactive →
219
+ * activating → active) even under load, while staying short enough that a
220
+ * genuinely stuck unit (still `inactive` at 45 s) is a real problem.
221
+ * Exported for test injection.
222
+ */
223
+ export const AGENT_LIVE_WINDOW_MS = 45_000
224
+
225
+ /**
226
+ * How often the live-watch loop re-polls systemd while waiting for the
227
+ * agent to become active. Exported for test injection.
228
+ */
229
+ export const AGENT_LIVE_POLL_INTERVAL_MS = 2_000
230
+
231
+ /**
232
+ * After the live window expires with the agent still not `active`, the
233
+ * generator schedules ONE follow-up re-poll this many ms later. If the
234
+ * agent has reached `active` by then, an updated ✅ ProbeResult is
235
+ * yielded and the boot card edits in place. Otherwise no further yield.
236
+ *
237
+ * Pre-#296 fix the generator returned immediately at window-expiry, so
238
+ * an agent that became active 1-30s after the window stayed visibly
239
+ * 🟡 "service inactive" forever (until the user noticed and asked).
240
+ *
241
+ * 30 s is the recommended-by-issue-author value: long enough to catch
242
+ * the common late-boot scenario (slow disk, claude-cli npm install
243
+ * ticking down), short enough that genuinely stuck units still surface
244
+ * as a real problem within ~75 s total.
245
+ */
246
+ export const AGENT_LIVE_FOLLOWUP_REPOLL_MS = 30_000
247
+
248
+ type ExecFileResult = { stdout: string; stderr: string }
249
+ type ExecFileFnType = (
250
+ cmd: string,
251
+ args: string[],
252
+ ) => Promise<ExecFileResult>
253
+
254
+ /**
255
+ * Resolve the "real" agent PID under tmux supervisor by walking the
256
+ * unit's cgroup and picking the heaviest-RSS claude/node process.
257
+ *
258
+ * Returns null on any failure — caller should fall back to MainPID.
259
+ *
260
+ * Mirrors `resolveAgentPid()` in `src/agents/lifecycle.ts` and
261
+ * `agent_main_pid()` in `bin/bridge-watchdog.sh`. Kept duplicated rather
262
+ * than imported because the gateway runs in a separate package and we
263
+ * don't want a cross-package import for a 30-line helper.
264
+ */
265
+ async function resolveTmuxSupervisorPid(
266
+ agentName: string,
267
+ execFileImpl: ExecFileFnType,
268
+ ): Promise<number | null> {
269
+ try {
270
+ const { stdout: cgOut } = await execFileImpl('systemctl', [
271
+ '--user', 'show', `switchroom-${agentName}.service`,
272
+ '-p', 'ControlGroup', '--value',
273
+ ])
274
+ const cgroup = cgOut.trim()
275
+ if (!cgroup) return null
276
+ const procsPath = `/sys/fs/cgroup${cgroup}/cgroup.procs`
277
+ if (!existsSync(procsPath)) return null
278
+ const pidsRaw = readFileSync(procsPath, 'utf-8')
279
+ const pids = pidsRaw.split('\n').map(s => s.trim()).filter(Boolean)
280
+ if (pids.length === 0) return null
281
+
282
+ type Candidate = { pid: number; rss: number; comm: string }
283
+ const candidates: Candidate[] = []
284
+ for (const pidStr of pids) {
285
+ const pid = parseInt(pidStr, 10)
286
+ if (!Number.isFinite(pid) || pid <= 0) continue
287
+ let rss = 0
288
+ let comm = ''
289
+ try {
290
+ const status = readFileSync(`/proc/${pid}/status`, 'utf-8')
291
+ const rssLine = status.split('\n').find(l => l.startsWith('VmRSS:'))
292
+ if (rssLine) {
293
+ const m = rssLine.match(/(\d+)/)
294
+ if (m) rss = parseInt(m[1], 10) || 0
295
+ }
296
+ } catch {
297
+ continue
298
+ }
299
+ try {
300
+ comm = readFileSync(`/proc/${pid}/comm`, 'utf-8').trim()
301
+ } catch { /* ignore */ }
302
+ candidates.push({ pid, rss, comm })
303
+ }
304
+ if (candidates.length === 0) return null
305
+
306
+ const isAgent = (c: Candidate): boolean => c.comm === 'claude' || c.comm === 'node'
307
+ const isWrapper = (c: Candidate): boolean =>
308
+ c.comm === 'tmux' || c.comm.startsWith('tmux:') ||
309
+ c.comm === 'expect' || c.comm === 'script' ||
310
+ c.comm === 'bash' || c.comm === 'sh'
311
+
312
+ const agentMatches = candidates.filter(isAgent)
313
+ if (agentMatches.length > 0) {
314
+ agentMatches.sort((a, b) => b.rss - a.rss)
315
+ return agentMatches[0].pid
316
+ }
317
+ const nonWrapper = candidates.filter(c => !isWrapper(c))
318
+ if (nonWrapper.length > 0) {
319
+ nonWrapper.sort((a, b) => b.rss - a.rss)
320
+ return nonWrapper[0].pid
321
+ }
322
+ // Candidates enumerated but every one was a wrapper (tmux/expect/
323
+ // script/bash/sh). Emit a breadcrumb mirroring the one in
324
+ // src/agents/lifecycle.ts:resolveAgentPid so journalctl shows the
325
+ // same state on both sides. The boot-window race (zero pids) returns
326
+ // earlier without logging, by design.
327
+ process.stderr.write(
328
+ `[switchroom] resolveTmuxSupervisorPid: cgroup walk found ${candidates.length} processes, no claude match — falling back to MainPID for unit=switchroom-${agentName}.service\n`,
329
+ )
330
+ return null
331
+ } catch {
332
+ return null
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Query systemctl for the agent service and return a snapshot of its state.
338
+ * Extracted so the re-probe loop can call it multiple times.
339
+ */
340
+ async function queryAgentState(
341
+ agentName: string,
342
+ execFileImpl: ExecFileFnType,
343
+ ): Promise<{
344
+ state: string
345
+ kv: Record<string, string>
346
+ } | { error: string }> {
347
+ let stdout: string
348
+ try {
349
+ const result = await execFileImpl('systemctl', [
350
+ '--user', 'show',
351
+ `switchroom-${agentName}.service`,
352
+ '-p', 'MainPID,ActiveState,MemoryCurrent,ActiveEnterTimestamp',
353
+ ])
354
+ stdout = result.stdout
355
+ } catch (err: unknown) {
356
+ return { error: `systemctl failed: ${(err as Error).message ?? String(err)}` }
357
+ }
358
+ const kv = parseSystemctlKv(stdout)
359
+ return { state: kv['ActiveState'] ?? 'unknown', kv }
360
+ }
361
+
362
+ export async function probeAgentProcess(
363
+ agentName: string,
364
+ opts: {
365
+ retryIntervalMs?: number
366
+ retryMaxMs?: number
367
+ /** Override for tests — replaces real delays */
368
+ sleepImpl?: (ms: number) => Promise<void>
369
+ /** Override for tests — replaces real execFile calls */
370
+ execFileImpl?: ExecFileFnType
371
+ /** When true, resolve PID via cgroup walk (heaviest claude/node) — under
372
+ * tmux supervisor MainPID is the tmux server (~2MB) which is misleading. */
373
+ tmuxSupervisor?: boolean
374
+ } = {},
375
+ ): Promise<ProbeResult> {
376
+ const retryIntervalMs = opts.retryIntervalMs ?? AGENT_RETRY_INTERVAL_MS
377
+ const retryMaxMs = opts.retryMaxMs ?? AGENT_RETRY_MAX_MS
378
+ const sleep = opts.sleepImpl ?? ((ms: number) => new Promise(resolve => setTimeout(resolve, ms)))
379
+ const execFileFn: ExecFileFnType = opts.execFileImpl ?? execFile
380
+
381
+ return withTimeout('Agent', (async (): Promise<ProbeResult> => {
382
+ const startMs = Date.now()
383
+
384
+ // Re-probe loop: if state is not yet `active`, retry every retryIntervalMs
385
+ // up to retryMaxMs total elapsed. Transients (deactivating, activating,
386
+ // auto-restart) typically resolve within one or two retries.
387
+ // eslint-disable-next-line no-constant-condition
388
+ while (true) {
389
+ const snapshot = await queryAgentState(agentName, execFileFn)
390
+
391
+ if ('error' in snapshot) {
392
+ return { status: 'fail', label: 'Agent', detail: snapshot.error }
393
+ }
394
+
395
+ const { state, kv } = snapshot
396
+
397
+ if (state === 'active') {
398
+ let pid: string = kv['MainPID'] ?? '?'
399
+ if (opts.tmuxSupervisor) {
400
+ const resolved = await resolveTmuxSupervisorPid(agentName, execFileFn)
401
+ if (resolved && resolved > 0) pid = String(resolved)
402
+ }
403
+ const uptime = formatUptime(kv['ActiveEnterTimestamp'] ?? '')
404
+ const mem = formatMemory(kv['MemoryCurrent'] ?? '')
405
+ const parts = [`PID ${pid}`, uptime, mem].filter(Boolean)
406
+ return { status: 'ok', label: 'Agent', detail: parts.join(' · ') }
407
+ }
408
+
409
+ const elapsedMs = Date.now() - startMs
410
+ if (elapsedMs >= retryMaxMs) {
411
+ // Committed to the current non-active state.
412
+ // `deactivating`, `activating`, and `auto-restart` are unambiguous
413
+ // transients — honest severity is degraded (🟡), not fail (🔴).
414
+ // Any other non-active state (inactive, failed, …) is a hard fail.
415
+ const isTransient =
416
+ state === 'deactivating' ||
417
+ state === 'activating' ||
418
+ state === 'auto-restart'
419
+ const status = isTransient ? 'degraded' : 'fail'
420
+ return { status, label: 'Agent', detail: `service ${state}` }
421
+ }
422
+
423
+ // Still within retry budget — wait and try again.
424
+ await sleep(retryIntervalMs)
425
+ }
426
+ })(), PROBE_TIMEOUT_MS + retryMaxMs) // extend outer timeout to cover full retry budget
427
+ }
428
+
429
+ /**
430
+ * Async generator that watches the agent systemd unit and yields a
431
+ * ProbeResult each time the meaningful state changes, for up to
432
+ * `liveWindowMs` total. Exits early as soon as the unit reaches `active`.
433
+ *
434
+ * Designed for the boot-card live-update loop in `boot-card.ts`: the
435
+ * caller iterates, edits the card on each yielded result, and breaks once
436
+ * it sees `status === 'ok'` or the generator exhausts.
437
+ *
438
+ * Key contract:
439
+ * - First yield is immediate (no initial delay) so the card can show
440
+ * the current state right away.
441
+ * - Subsequent yields happen every `pollIntervalMs`.
442
+ * - `inactive` and `activating` within the window → status `degraded`
443
+ * (🟡 "starting"), not `fail`. Only `failed` or window-expired-`inactive`
444
+ * commits to `fail`.
445
+ * - When the window expires without `active` the generator yields a
446
+ * final committed result and then ends.
447
+ */
448
+ export async function* watchAgentProcess(
449
+ agentName: string,
450
+ opts: {
451
+ liveWindowMs?: number
452
+ pollIntervalMs?: number
453
+ /**
454
+ * Wait this many ms after the live window expires before doing one
455
+ * follow-up state check. If the agent reached `active` in that
456
+ * window, yield an updated ✅ ProbeResult so the boot card flips
457
+ * from 🟡 "service inactive" to ✅. See #296. Set to 0 to disable.
458
+ */
459
+ followupRepollMs?: number
460
+ /** Override for tests — replaces real delays */
461
+ sleepImpl?: (ms: number) => Promise<void>
462
+ /** Override for tests — replaces real execFile calls */
463
+ execFileImpl?: ExecFileFnType
464
+ /**
465
+ * Override for tests. Defaults to Date.now. The within-window
466
+ * check uses this; injecting lets tests advance "time" without
467
+ * real sleeps.
468
+ */
469
+ nowImpl?: () => number
470
+ /** When true, resolve PID via cgroup walk (heaviest claude/node). */
471
+ tmuxSupervisor?: boolean
472
+ } = {},
473
+ ): AsyncGenerator<ProbeResult> {
474
+ const liveWindowMs = opts.liveWindowMs ?? AGENT_LIVE_WINDOW_MS
475
+ const pollIntervalMs = opts.pollIntervalMs ?? AGENT_LIVE_POLL_INTERVAL_MS
476
+ const followupRepollMs = opts.followupRepollMs ?? AGENT_LIVE_FOLLOWUP_REPOLL_MS
477
+ const sleep = opts.sleepImpl ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)))
478
+ const execFileFn: ExecFileFnType = opts.execFileImpl ?? execFile
479
+ const now = opts.nowImpl ?? (() => Date.now())
480
+
481
+ const startMs = now()
482
+ let lastYieldedDetail: string | null = null
483
+
484
+ /**
485
+ * Convert a raw systemd state into a ProbeResult suitable for the boot card.
486
+ * Within the live window: inactive, activating, auto-restart, and
487
+ * deactivating are all 🟡 "starting" — we don't know they're stuck yet.
488
+ * Only `failed` is immediately 🔴. Everything else (unknown) is also 🔴.
489
+ */
490
+ async function toProbeResult(
491
+ state: string,
492
+ kv: Record<string, string>,
493
+ withinWindow: boolean,
494
+ ): Promise<ProbeResult> {
495
+ if (state === 'active') {
496
+ let pid: string = kv['MainPID'] ?? '?'
497
+ if (opts.tmuxSupervisor) {
498
+ const resolved = await resolveTmuxSupervisorPid(agentName, execFileFn)
499
+ if (resolved && resolved > 0) pid = String(resolved)
500
+ }
501
+ const uptime = formatUptime(kv['ActiveEnterTimestamp'] ?? '')
502
+ const mem = formatMemory(kv['MemoryCurrent'] ?? '')
503
+ const parts = [`PID ${pid}`, uptime, mem].filter(Boolean)
504
+ return { status: 'ok', label: 'Agent', detail: parts.join(' · ') }
505
+ }
506
+ if (withinWindow) {
507
+ // Treat all non-active states as transient while still within the
508
+ // window. `failed` is the only exception — hard fail even in-window.
509
+ if (state === 'failed') {
510
+ return { status: 'fail', label: 'Agent', detail: 'service failed' }
511
+ }
512
+ return { status: 'degraded', label: 'Agent', detail: 'service starting' }
513
+ }
514
+ // Window expired — commit to the actual state.
515
+ const isTransient =
516
+ state === 'deactivating' ||
517
+ state === 'activating' ||
518
+ state === 'auto-restart' ||
519
+ state === 'inactive'
520
+ const status = isTransient ? 'degraded' : 'fail'
521
+ return { status, label: 'Agent', detail: `service ${state}` }
522
+ }
523
+
524
+ while (true) {
525
+ const elapsedMs = now() - startMs
526
+ const withinWindow = elapsedMs < liveWindowMs
527
+
528
+ const snapshot = await queryAgentState(agentName, execFileFn)
529
+
530
+ if ('error' in snapshot) {
531
+ yield { status: 'fail', label: 'Agent', detail: snapshot.error }
532
+ return
533
+ }
534
+
535
+ const result = await toProbeResult(snapshot.state, snapshot.kv, withinWindow)
536
+
537
+ // Only yield when the result detail actually changed — avoids
538
+ // redundant card edits ("service starting" → "service starting").
539
+ if (result.detail !== lastYieldedDetail) {
540
+ lastYieldedDetail = result.detail
541
+ yield result
542
+ }
543
+
544
+ // Terminal states: active (ok) or genuinely failed.
545
+ if (result.status === 'ok' || (result.status === 'fail' && snapshot.state === 'failed')) {
546
+ return
547
+ }
548
+
549
+ // If window expired, we already yielded the final committed result.
550
+ if (!withinWindow) {
551
+ // #296 follow-up: schedule ONE re-poll after the live window so a
552
+ // late-boot transition (active arriving 1-30s after the window) flips
553
+ // the card from 🟡 "service inactive" to ✅ instead of staying stale
554
+ // until the next user-driven event. Skipped when:
555
+ // - followupRepollMs <= 0 (test override / explicit disable)
556
+ // - the final result was already 'ok' (handled by the early-return above)
557
+ // - the final result was 'fail' due to systemd reporting `failed`
558
+ // (also handled above) — anything reaching here is degraded
559
+ if (followupRepollMs <= 0) return
560
+ await sleep(followupRepollMs)
561
+ const followup = await queryAgentState(agentName, execFileFn)
562
+ if ('error' in followup) return
563
+ // Only yield on a state we DIDN'T see before — silently no-op if the
564
+ // agent is still inactive/activating/etc., to avoid card flapping.
565
+ if (followup.state !== 'active') return
566
+ const okResult = await toProbeResult(followup.state, followup.kv, false)
567
+ if (okResult.detail !== lastYieldedDetail) {
568
+ yield okResult
569
+ }
570
+ return
571
+ }
572
+
573
+ await sleep(pollIntervalMs)
574
+ }
575
+ }
576
+
577
+ // ─── Probe: Gateway ──────────────────────────────────────────────────────────
578
+
579
+ export interface GatewayRuntimeInfo {
580
+ pid: number
581
+ startedAtMs: number
582
+ lastPollMs?: number
583
+ }
584
+
585
+ export async function probeGateway(info: GatewayRuntimeInfo): Promise<ProbeResult> {
586
+ return withTimeout('Gateway', (async (): Promise<ProbeResult> => {
587
+ const uptime = formatMs(Date.now() - info.startedAtMs)
588
+ const lastPoll = info.lastPollMs != null
589
+ ? `last poll ${formatMs(Date.now() - info.lastPollMs)} ago`
590
+ : ''
591
+ const parts = [`PID ${info.pid}`, `up ${uptime}`, lastPoll].filter(Boolean)
592
+ return { status: 'ok', label: 'Gateway', detail: parts.join(' · ') }
593
+ })())
594
+ }
595
+
596
+ // ─── Probe: Quota ─────────────────────────────────────────────────────────────
597
+
598
+ const QUOTA_DEBUG_FILE = 'quota-debug.json'
599
+
600
+ /**
601
+ * Attempt to read quota info via the /api/oauth/usage endpoint.
602
+ * The response schema is undocumented — we probe defensively and
603
+ * save the raw response to a debug file on first 2xx hit.
604
+ *
605
+ * Result is cached for 5 min in `~/.switchroom/quota-cache.json` and
606
+ * shared across all agents. Without the cache, every gateway boot +
607
+ * bridge-reconnect across 4 agents hits the endpoint, triggering 429s
608
+ * that surface as 🟡 "rate limited" in the boot card. See `quota-cache.ts`.
609
+ *
610
+ * Tests can override the cache path via SWITCHROOM_QUOTA_CACHE_PATH.
611
+ */
612
+ export async function probeQuota(
613
+ claudeConfigDir: string,
614
+ agentDir: string,
615
+ fetchImpl: typeof fetch = fetch,
616
+ ): Promise<ProbeResult> {
617
+ return withTimeout('Quota', (async (): Promise<ProbeResult> => {
618
+ // Cache hit → return early (avoids the rate-limit cascade)
619
+ const cached = readQuotaCache()
620
+ if (cached) {
621
+ return cached
622
+ }
623
+
624
+ // Read token
625
+ let token: string | null = null
626
+ for (const candidate of [
627
+ join(claudeConfigDir, '.oauth-token'),
628
+ join(claudeConfigDir, 'accounts', 'default', '.oauth-token'),
629
+ ]) {
630
+ if (existsSync(candidate)) {
631
+ try {
632
+ const raw = readFileSync(candidate, 'utf8').trim()
633
+ if (raw.length > 0) { token = raw; break }
634
+ } catch {}
635
+ }
636
+ }
637
+ if (!token) {
638
+ return { status: 'degraded', label: 'Quota', detail: 'no OAuth token' }
639
+ }
640
+
641
+ let resp: Response
642
+ try {
643
+ const controller = new AbortController()
644
+ const t = setTimeout(() => controller.abort(), 1800)
645
+ resp = await fetchImpl('https://api.anthropic.com/api/oauth/usage', {
646
+ method: 'GET',
647
+ headers: {
648
+ 'Authorization': `Bearer ${token}`,
649
+ 'Accept': 'application/json',
650
+ 'anthropic-version': '2023-06-01',
651
+ 'anthropic-beta': 'oauth-2025-04-20',
652
+ 'User-Agent': 'switchroom-boot/0.1',
653
+ },
654
+ signal: controller.signal,
655
+ })
656
+ clearTimeout(t)
657
+ } catch (err: unknown) {
658
+ return { status: 'fail', label: 'Quota', detail: `request failed: ${(err as Error).message ?? String(err)}` }
659
+ }
660
+
661
+ if (resp.status === 429) {
662
+ // A 429 from /api/oauth/usage means the endpoint is rate-limiting our
663
+ // probe calls — it does NOT mean the user is out of quota. Conflating
664
+ // the two is the root cause of the false 🟡 "rate limited" alarm
665
+ // reported in #210. Return ok-with-note and cache it for 30 s so
666
+ // simultaneous fleet restarts read the cached result instead of piling
667
+ // up on the same endpoint (see quota-cache.ts: RATE_LIMIT_TTL_MS).
668
+ //
669
+ // We assume 429 from /api/oauth/usage signals endpoint rate-limiting,
670
+ // not quota exhaustion. Anthropic uses 403 / 200-with-flag for the
671
+ // latter today; if that changes, revisit this 🟢 mapping.
672
+ const rateLimitResult: ProbeResult = {
673
+ status: 'ok',
674
+ label: 'Quota',
675
+ detail: 'quota check skipped: rate limited',
676
+ rateLimited: true,
677
+ }
678
+ writeQuotaCache(rateLimitResult)
679
+ return rateLimitResult
680
+ }
681
+ if (!resp.ok) {
682
+ return { status: 'degraded', label: 'Quota', detail: `HTTP ${resp.status}` }
683
+ }
684
+
685
+ let body: unknown
686
+ try {
687
+ body = await resp.json()
688
+ } catch {
689
+ return { status: 'degraded', label: 'Quota', detail: 'invalid JSON response' }
690
+ }
691
+
692
+ // Defensive schema discovery — save raw response for tightening
693
+ const debugPath = join(agentDir, 'telegram', QUOTA_DEBUG_FILE)
694
+ try {
695
+ // Redact token/UUID fields before saving
696
+ const redacted = JSON.parse(JSON.stringify(body, (k, v) => {
697
+ if (/token|uuid|id|key/i.test(k) && typeof v === 'string' && v.length > 10) return '[REDACTED]'
698
+ return v
699
+ }))
700
+ mkdirSync(join(agentDir, 'telegram'), { recursive: true })
701
+ writeFileSync(debugPath, JSON.stringify({ capturedAt: new Date().toISOString(), body: redacted }, null, 2))
702
+ } catch {}
703
+
704
+ // Try common field paths — schema not yet locked
705
+ const b = body as Record<string, unknown>
706
+ const sessionQuota =
707
+ (b?.['data'] as Record<string, unknown> | undefined)?.['session_quota'] ??
708
+ b?.['session_quota'] ??
709
+ (b?.['quota'] as Record<string, unknown> | undefined)?.['session'] ??
710
+ (b?.['usage'] as Record<string, unknown> | undefined)?.['session']
711
+
712
+ if (!sessionQuota) {
713
+ return {
714
+ status: 'degraded',
715
+ label: 'Quota',
716
+ detail: `schema unknown — first call captured (debug: ${debugPath})`,
717
+ }
718
+ }
719
+
720
+ const sq = sessionQuota as Record<string, unknown>
721
+ const parts: string[] = []
722
+ if (typeof sq['sonnet_used_pct'] === 'number') parts.push(`Sonnet ${Math.round(sq['sonnet_used_pct'] as number)}%`)
723
+ if (typeof sq['opus_used_pct'] === 'number') parts.push(`Opus ${Math.round(sq['opus_used_pct'] as number)}%`)
724
+ if (typeof sq['used_pct'] === 'number') parts.push(`${Math.round(sq['used_pct'] as number)}% used`)
725
+ if (typeof sq['resets_in_sec'] === 'number') {
726
+ const sec = sq['resets_in_sec'] as number
727
+ const h = Math.floor(sec / 3600)
728
+ const m = Math.round((sec % 3600) / 60)
729
+ parts.push(`resets in ${h}h ${m}m`)
730
+ }
731
+
732
+ if (parts.length === 0) {
733
+ return { status: 'degraded', label: 'Quota', detail: 'schema unknown — saving raw response' }
734
+ }
735
+ const result: ProbeResult = { status: 'ok', label: 'Quota', detail: parts.join(' · ') }
736
+ writeQuotaCache(result)
737
+ return result
738
+ })())
739
+ }
740
+
741
+ // ─── Probe: Hindsight ────────────────────────────────────────────────────────
742
+
743
+ export async function probeHindsight(
744
+ bankName?: string,
745
+ fetchImpl: typeof fetch = fetch,
746
+ ): Promise<ProbeResult> {
747
+ return withTimeout('Hindsight', (async (): Promise<ProbeResult> => {
748
+ const base = 'http://127.0.0.1:18888'
749
+ let resp: Response | null = null
750
+
751
+ for (const path of ['/health', '/']) {
752
+ try {
753
+ const controller = new AbortController()
754
+ const t = setTimeout(() => controller.abort(), 1800)
755
+ resp = await fetchImpl(`${base}${path}`, { signal: controller.signal })
756
+ clearTimeout(t)
757
+ if (resp.status !== 404) break
758
+ } catch {}
759
+ }
760
+
761
+ if (!resp || !resp.ok) {
762
+ return { status: 'fail', label: 'Hindsight', detail: 'unreachable' }
763
+ }
764
+
765
+ const bankSuffix = bankName ? ` · bank=${bankName}` : ''
766
+ return { status: 'ok', label: 'Hindsight', detail: `reachable${bankSuffix}` }
767
+ })())
768
+ }
769
+
770
+ // ─── Probe: Cron timers ──────────────────────────────────────────────────────
771
+
772
+ interface SystemctlTimerEntry {
773
+ next?: string
774
+ left?: string
775
+ last?: string
776
+ unit?: string
777
+ activates?: string
778
+ passed?: string
779
+ }
780
+
781
+ function parseTimerLeft(left: string | undefined): number | null {
782
+ if (!left) return null
783
+ // format: "1h 32min left" or "2min 5s left" or similar
784
+ let ms = 0
785
+ const h = left.match(/(\d+)h/)
786
+ const m = left.match(/(\d+)min/)
787
+ const s = left.match(/(\d+)s/)
788
+ if (h) ms += Number(h[1]) * 3600_000
789
+ if (m) ms += Number(m[1]) * 60_000
790
+ if (s) ms += Number(s[1]) * 1000
791
+ return ms > 0 ? ms : null
792
+ }
793
+
794
+ export async function probeCronTimers(
795
+ agentName: string,
796
+ opts: { execFileImpl?: ExecFileFnType } = {},
797
+ ): Promise<ProbeResult> {
798
+ const execFileFn: ExecFileFnType = opts.execFileImpl ?? execFile
799
+ return withTimeout('Crons', (async (): Promise<ProbeResult> => {
800
+ let stdout: string
801
+ try {
802
+ const result = await execFileFn('systemctl', [
803
+ '--user', 'list-timers',
804
+ `switchroom-${agentName}-cron-*`,
805
+ '--output=json',
806
+ '--all',
807
+ ])
808
+ stdout = result.stdout.trim()
809
+ } catch (err: unknown) {
810
+ // systemctl exits non-zero when no units match
811
+ const msg = (err as NodeJS.ErrnoException)?.message ?? String(err)
812
+ // child_process exec errors have `code` typed as string in
813
+ // NodeJS.ErrnoException, but at runtime it's numeric for shell
814
+ // exit codes. Stringify to avoid the type-system mismatch and
815
+ // the comparison "looks unintentional" warning.
816
+ if (msg.includes('No timers found') || String((err as NodeJS.ErrnoException)?.code) === '1') {
817
+ return { status: 'ok', label: 'Crons', detail: '0 timers' }
818
+ }
819
+ return { status: 'fail', label: 'Crons', detail: `systemctl failed: ${msg}` }
820
+ }
821
+
822
+ if (!stdout || stdout === '[]' || stdout.length === 0) {
823
+ return { status: 'ok', label: 'Crons', detail: '0 timers' }
824
+ }
825
+
826
+ let timers: SystemctlTimerEntry[] = []
827
+ try {
828
+ timers = JSON.parse(stdout) as SystemctlTimerEntry[]
829
+ } catch {
830
+ // Fall back to line-count if JSON failed
831
+ const count = stdout.split('\n').filter(l => l.includes('cron')).length
832
+ return { status: 'ok', label: 'Crons', detail: `${count} timers` }
833
+ }
834
+
835
+ if (!Array.isArray(timers) || timers.length === 0) {
836
+ return { status: 'ok', label: 'Crons', detail: '0 timers' }
837
+ }
838
+
839
+ // Find the timer that fires soonest
840
+ let earliest: { name: string; leftMs: number } | null = null
841
+ for (const t of timers) {
842
+ const ms = parseTimerLeft(t.left)
843
+ const name = (t.unit ?? t.activates ?? '').replace(/^switchroom-[^-]+-cron-/, '').replace(/\.timer$/, '')
844
+ if (ms != null && (earliest == null || ms < earliest.leftMs)) {
845
+ earliest = { name, leftMs: ms }
846
+ }
847
+ }
848
+
849
+ const count = timers.length
850
+ if (!earliest) {
851
+ return { status: 'ok', label: 'Crons', detail: `${count} timers` }
852
+ }
853
+
854
+ const h = Math.floor(earliest.leftMs / 3600_000)
855
+ const m = Math.round((earliest.leftMs % 3600_000) / 60_000)
856
+ const timeStr = h > 0 ? `${h}h ${m}m` : `${m}m`
857
+ return {
858
+ status: 'ok',
859
+ label: 'Crons',
860
+ detail: `${count} timers · next: ${earliest.name} in ${timeStr}`,
861
+ }
862
+ })())
863
+ }