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.
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/bin/autoaccept.exp +81 -0
- package/bin/boot-self-test.sh +149 -0
- package/bin/bridge-watchdog.sh +967 -0
- package/bin/handoff-briefing.sh +206 -0
- package/bin/run-hook.sh +228 -0
- package/bin/switchroom.ts +4 -0
- package/bin/timezone-hook.sh +67 -0
- package/bin/user-profile-refresh-hook.sh +38 -0
- package/bin/workspace-dynamic-hook.sh +142 -0
- package/bin/workspace-stable-hook.sh +57 -0
- package/dist/cli/autoaccept-poll.js +118 -0
- package/dist/cli/switchroom.js +48557 -0
- package/package.json +95 -0
- package/profiles/_base/settings.json.hbs +15 -0
- package/profiles/_base/start.sh.hbs +383 -0
- package/profiles/_shared/telegram-style.md.hbs +140 -0
- package/profiles/coding/CLAUDE.md.hbs +57 -0
- package/profiles/coding/skills/architecture/SKILL.md +70 -0
- package/profiles/coding/skills/code-review/SKILL.md +58 -0
- package/profiles/coding/workspace/SOUL.md.hbs +25 -0
- package/profiles/default/CLAUDE.md +238 -0
- package/profiles/default/CLAUDE.md.hbs +113 -0
- package/profiles/default/workspace/CLAUDE.md.hbs +126 -0
- package/profiles/default/workspace/HEARTBEAT.md.hbs +40 -0
- package/profiles/default/workspace/IDENTITY.md.hbs +32 -0
- package/profiles/default/workspace/MEMORY.md.hbs +29 -0
- package/profiles/default/workspace/SOUL.md.hbs +61 -0
- package/profiles/default/workspace/TOOLS.md.hbs +29 -0
- package/profiles/default/workspace/USER.md.hbs +52 -0
- package/profiles/default/workspace/memory/.gitkeep +0 -0
- package/profiles/executive-assistant/CLAUDE.md.hbs +51 -0
- package/profiles/executive-assistant/skills/daily-briefing/SKILL.md +55 -0
- package/profiles/executive-assistant/skills/meeting-prep/SKILL.md +58 -0
- package/profiles/executive-assistant/workspace/SOUL.md.hbs +25 -0
- package/profiles/health-coach/CLAUDE.md.hbs +45 -0
- package/profiles/health-coach/skills/check-in/SKILL.md +41 -0
- package/profiles/health-coach/skills/weekly-review/SKILL.md +53 -0
- package/profiles/health-coach/workspace/SOUL.md.hbs +25 -0
- package/skills/buildkite-agent-infrastructure/SKILL.md +302 -0
- package/skills/buildkite-agent-infrastructure/agents/openai.yaml +6 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-infrastructure/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-infrastructure/references/audit-logging.md +87 -0
- package/skills/buildkite-agent-infrastructure/references/graphql-mutations.md +690 -0
- package/skills/buildkite-agent-infrastructure/references/instance-shapes.md +38 -0
- package/skills/buildkite-agent-infrastructure/references/pipeline-templates.md +73 -0
- package/skills/buildkite-agent-infrastructure/references/self-hosted-agents.md +137 -0
- package/skills/buildkite-agent-infrastructure/references/sso-saml.md +92 -0
- package/skills/buildkite-agent-runtime/SKILL.md +476 -0
- package/skills/buildkite-agent-runtime/agents/openai.yaml +6 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-agent-runtime/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-agent-runtime/references/flag-reference.md +417 -0
- package/skills/buildkite-agent-runtime/references/patterns-and-recipes.md +555 -0
- package/skills/buildkite-api/SKILL.md +285 -0
- package/skills/buildkite-api/agents/openai.yaml +6 -0
- package/skills/buildkite-api/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-api/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-api/references/graphql-reference.md +195 -0
- package/skills/buildkite-api/references/patterns.md +44 -0
- package/skills/buildkite-api/references/webhooks.md +161 -0
- package/skills/buildkite-cli/SKILL.md +379 -0
- package/skills/buildkite-cli/agents/openai.yaml +6 -0
- package/skills/buildkite-cli/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-cli/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-cli/references/command-reference.md +181 -0
- package/skills/buildkite-migration/SKILL.md +182 -0
- package/skills/buildkite-pipelines/SKILL.md +464 -0
- package/skills/buildkite-pipelines/agents/openai.yaml +6 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-pipelines/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-pipelines/examples/basic-pipeline.yml +24 -0
- package/skills/buildkite-pipelines/examples/optimized-pipeline.yml +100 -0
- package/skills/buildkite-pipelines/references/advanced-patterns.md +286 -0
- package/skills/buildkite-pipelines/references/retry-and-error-codes.md +131 -0
- package/skills/buildkite-pipelines/references/step-types-reference.md +225 -0
- package/skills/buildkite-secure-delivery/SKILL.md +168 -0
- package/skills/buildkite-secure-delivery/agents/openai.yaml +6 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-secure-delivery/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-secure-delivery/references/oidc-cloud-providers.md +83 -0
- package/skills/buildkite-secure-delivery/references/package-publishing.md +100 -0
- package/skills/buildkite-test-engine/SKILL.md +239 -0
- package/skills/buildkite-test-engine/agents/openai.yaml +6 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-large.png +0 -0
- package/skills/buildkite-test-engine/assets/buildkite-icon-small.png +0 -0
- package/skills/buildkite-test-engine/examples/bktec-splitting.yml +16 -0
- package/skills/buildkite-test-engine/examples/collector-pipeline.yml +11 -0
- package/skills/buildkite-test-engine/references/collectors.md +198 -0
- package/skills/buildkite-test-engine/references/splitting-examples.md +93 -0
- package/skills/docx/LICENSE.txt +30 -0
- package/skills/docx/SKILL.md +590 -0
- package/skills/docx/VENDORED.md +32 -0
- package/skills/docx/scripts/__init__.py +1 -0
- package/skills/docx/scripts/accept_changes.py +135 -0
- package/skills/docx/scripts/comment.py +318 -0
- package/skills/docx/scripts/office/helpers/__init__.py +0 -0
- package/skills/docx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/docx/scripts/office/pack.py +159 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/docx/scripts/office/soffice.py +183 -0
- package/skills/docx/scripts/office/unpack.py +132 -0
- package/skills/docx/scripts/office/validate.py +111 -0
- package/skills/docx/scripts/office/validators/__init__.py +15 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/base.py +847 -0
- package/skills/docx/scripts/office/validators/docx.py +446 -0
- package/skills/docx/scripts/office/validators/pptx.py +275 -0
- package/skills/docx/scripts/office/validators/redlining.py +247 -0
- package/skills/docx/scripts/templates/comments.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtended.xml +3 -0
- package/skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- package/skills/docx/scripts/templates/commentsIds.xml +3 -0
- package/skills/docx/scripts/templates/people.xml +3 -0
- package/skills/file-bug/SKILL.md +129 -0
- package/skills/humanizer/LICENSE +21 -0
- package/skills/humanizer/SKILL.md +559 -0
- package/skills/humanizer/VENDORED.md +38 -0
- package/skills/humanizer-calibrate/SKILL.md +144 -0
- package/skills/mcp-builder/LICENSE.txt +202 -0
- package/skills/mcp-builder/SKILL.md +236 -0
- package/skills/mcp-builder/VENDORED.md +32 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/pdf/LICENSE.txt +30 -0
- package/skills/pdf/SKILL.md +314 -0
- package/skills/pdf/VENDORED.md +32 -0
- package/skills/pdf/forms.md +294 -0
- package/skills/pdf/reference.md +612 -0
- package/skills/pdf/scripts/check_bounding_boxes.py +65 -0
- package/skills/pdf/scripts/check_fillable_fields.py +11 -0
- package/skills/pdf/scripts/convert_pdf_to_images.py +33 -0
- package/skills/pdf/scripts/create_validation_image.py +37 -0
- package/skills/pdf/scripts/extract_form_field_info.py +122 -0
- package/skills/pdf/scripts/extract_form_structure.py +115 -0
- package/skills/pdf/scripts/fill_fillable_fields.py +98 -0
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/skills/pptx/LICENSE.txt +30 -0
- package/skills/pptx/SKILL.md +232 -0
- package/skills/pptx/VENDORED.md +32 -0
- package/skills/pptx/editing.md +205 -0
- package/skills/pptx/pptxgenjs.md +420 -0
- package/skills/pptx/scripts/__init__.py +0 -0
- package/skills/pptx/scripts/add_slide.py +195 -0
- package/skills/pptx/scripts/clean.py +286 -0
- package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- package/skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/pptx/scripts/office/pack.py +159 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/pptx/scripts/office/soffice.py +183 -0
- package/skills/pptx/scripts/office/unpack.py +132 -0
- package/skills/pptx/scripts/office/validate.py +111 -0
- package/skills/pptx/scripts/office/validators/__init__.py +15 -0
- package/skills/pptx/scripts/office/validators/base.py +847 -0
- package/skills/pptx/scripts/office/validators/docx.py +446 -0
- package/skills/pptx/scripts/office/validators/pptx.py +275 -0
- package/skills/pptx/scripts/office/validators/redlining.py +247 -0
- package/skills/pptx/scripts/thumbnail.py +289 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/SKILL.md +485 -0
- package/skills/skill-creator/VENDORED.md +32 -0
- package/skills/skill-creator/agents/analyzer.md +274 -0
- package/skills/skill-creator/agents/comparator.md +202 -0
- package/skills/skill-creator/agents/grader.md +223 -0
- package/skills/skill-creator/assets/eval_review.html +146 -0
- package/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/skill-creator/references/schemas.md +430 -0
- package/skills/skill-creator/scripts/__init__.py +0 -0
- package/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/skill-creator/scripts/generate_report.py +326 -0
- package/skills/skill-creator/scripts/improve_description.py +247 -0
- package/skills/skill-creator/scripts/package_skill.py +136 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/run_eval.py +310 -0
- package/skills/skill-creator/scripts/run_loop.py +328 -0
- package/skills/skill-creator/scripts/utils.py +47 -0
- package/skills/switchroom-architecture/SKILL.md +60 -0
- package/skills/switchroom-architecture/cascade.md +112 -0
- package/skills/switchroom-architecture/sub-agents.md +87 -0
- package/skills/switchroom-architecture/telegram.md +94 -0
- package/skills/switchroom-cli/SKILL.md +274 -0
- package/skills/switchroom-health/SKILL.md +101 -0
- package/skills/switchroom-install/SKILL.md +116 -0
- package/skills/switchroom-manage/SKILL.md +90 -0
- package/skills/switchroom-status/SKILL.md +69 -0
- package/skills/switchroom-status/scripts/status.sh +69 -0
- package/skills/telegram-test-harness/SKILL.md +191 -0
- package/skills/token-helpers/SKILL.md +73 -0
- package/skills/token-helpers/scripts/google-cal-token.sh +62 -0
- package/skills/token-helpers/scripts/ms-graph-token.sh +70 -0
- package/skills/webapp-testing/LICENSE.txt +202 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/VENDORED.md +32 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/skills/xlsx/LICENSE.txt +30 -0
- package/skills/xlsx/SKILL.md +292 -0
- package/skills/xlsx/VENDORED.md +32 -0
- package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- package/skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
- package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- package/skills/xlsx/scripts/office/pack.py +159 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- package/skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- package/skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- package/skills/xlsx/scripts/office/soffice.py +183 -0
- package/skills/xlsx/scripts/office/unpack.py +132 -0
- package/skills/xlsx/scripts/office/validate.py +111 -0
- package/skills/xlsx/scripts/office/validators/__init__.py +15 -0
- package/skills/xlsx/scripts/office/validators/base.py +847 -0
- package/skills/xlsx/scripts/office/validators/docx.py +446 -0
- package/skills/xlsx/scripts/office/validators/pptx.py +275 -0
- package/skills/xlsx/scripts/office/validators/redlining.py +247 -0
- package/skills/xlsx/scripts/recalc.py +184 -0
- package/telegram-plugin/.claude-plugin/plugin.json +20 -0
- package/telegram-plugin/.mcp.json +14 -0
- package/telegram-plugin/LICENSE +21 -0
- package/telegram-plugin/README.md +352 -0
- package/telegram-plugin/active-pins-sweep.ts +204 -0
- package/telegram-plugin/active-pins.ts +146 -0
- package/telegram-plugin/active-reactions-sweep.ts +79 -0
- package/telegram-plugin/active-reactions.ts +134 -0
- package/telegram-plugin/admin-commands/dispatch.test.ts +149 -0
- package/telegram-plugin/admin-commands/index.ts +106 -0
- package/telegram-plugin/answer-stream.ts +565 -0
- package/telegram-plugin/ask-user.ts +179 -0
- package/telegram-plugin/attachment-path.ts +80 -0
- package/telegram-plugin/auth-code-redact.ts +83 -0
- package/telegram-plugin/auth-dashboard.ts +1104 -0
- package/telegram-plugin/auth-slot-parser.ts +497 -0
- package/telegram-plugin/auto-fallback-dispatcher.ts +68 -0
- package/telegram-plugin/auto-fallback.ts +348 -0
- package/telegram-plugin/bridge/bridge.ts +687 -0
- package/telegram-plugin/bridge/ipc-client.ts +326 -0
- package/telegram-plugin/bun.lock +218 -0
- package/telegram-plugin/card-format.ts +62 -0
- package/telegram-plugin/channel-envelope-safety.test.ts +56 -0
- package/telegram-plugin/channel-envelope-safety.ts +56 -0
- package/telegram-plugin/chat-lock.ts +65 -0
- package/telegram-plugin/context-exhaustion.ts +38 -0
- package/telegram-plugin/credits-watch.ts +220 -0
- package/telegram-plugin/dist/bridge/bridge.js +24758 -0
- package/telegram-plugin/dist/foreman/foreman.js +30723 -0
- package/telegram-plugin/dist/gateway/gateway.js +46497 -0
- package/telegram-plugin/dist/server.js +24551 -0
- package/telegram-plugin/dm-command-gate.ts +56 -0
- package/telegram-plugin/docs/gateway-server-split.md +133 -0
- package/telegram-plugin/docs/multi-agent-card-design.md +847 -0
- package/telegram-plugin/docs/pinned-progress-card-reliability.md +144 -0
- package/telegram-plugin/docs/stream-json-daemon-mode.md +477 -0
- package/telegram-plugin/docs/waiting-ux-spec.md +233 -0
- package/telegram-plugin/draft-stream.ts +442 -0
- package/telegram-plugin/draft-transport.ts +72 -0
- package/telegram-plugin/first-paint.ts +246 -0
- package/telegram-plugin/fleet-state.ts +246 -0
- package/telegram-plugin/foreman/foreman-create-flow.ts +202 -0
- package/telegram-plugin/foreman/foreman-handlers.ts +493 -0
- package/telegram-plugin/foreman/foreman.ts +1130 -0
- package/telegram-plugin/foreman/setup-flow.ts +345 -0
- package/telegram-plugin/foreman/setup-state.ts +239 -0
- package/telegram-plugin/foreman/state.ts +203 -0
- package/telegram-plugin/format.ts +685 -0
- package/telegram-plugin/gateway/access-validator.test.ts +95 -0
- package/telegram-plugin/gateway/access-validator.ts +37 -0
- package/telegram-plugin/gateway/boot-card.ts +582 -0
- package/telegram-plugin/gateway/boot-probes.ts +863 -0
- package/telegram-plugin/gateway/boot-reason.ts +51 -0
- package/telegram-plugin/gateway/boot-sweep-filter.test.ts +54 -0
- package/telegram-plugin/gateway/boot-sweep-filter.ts +32 -0
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +183 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +109 -0
- package/telegram-plugin/gateway/gateway.ts +10202 -0
- package/telegram-plugin/gateway/inbound-coalesce.ts +147 -0
- package/telegram-plugin/gateway/inject-handler.test.ts +221 -0
- package/telegram-plugin/gateway/inject-handler.ts +190 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +151 -0
- package/telegram-plugin/gateway/ipc-server.ts +494 -0
- package/telegram-plugin/gateway/pid-file.ts +103 -0
- package/telegram-plugin/gateway/poll-health.ts +156 -0
- package/telegram-plugin/gateway/preamble-suppressor.ts +154 -0
- package/telegram-plugin/gateway/quota-cache.ts +125 -0
- package/telegram-plugin/gateway/resolve-calling-subagent.ts +78 -0
- package/telegram-plugin/gateway/restart-watchdog.ts +200 -0
- package/telegram-plugin/gateway/session-marker.ts +83 -0
- package/telegram-plugin/gateway/shutdown-drain.ts +162 -0
- package/telegram-plugin/gateway/startup-mutex.ts +285 -0
- package/telegram-plugin/gateway/startup-network-retry.ts +142 -0
- package/telegram-plugin/gateway/turn-active-marker.ts +176 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +78 -0
- package/telegram-plugin/handoff-continuity.ts +200 -0
- package/telegram-plugin/history.ts +468 -0
- package/telegram-plugin/hooks/hooks.json +58 -0
- package/telegram-plugin/hooks/secret-guard-pretool.mjs +208 -0
- package/telegram-plugin/hooks/secret-scrub-stop.mjs +98 -0
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +111 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +296 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +261 -0
- package/telegram-plugin/html-sanitize.ts +244 -0
- package/telegram-plugin/idle-footer.ts +65 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +166 -0
- package/telegram-plugin/interrupt-marker.ts +66 -0
- package/telegram-plugin/issues-card.ts +371 -0
- package/telegram-plugin/issues-watcher.ts +125 -0
- package/telegram-plugin/model-unavailable.ts +325 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/operator-events-history.ts +94 -0
- package/telegram-plugin/operator-events.fixtures.json +161 -0
- package/telegram-plugin/operator-events.ts +421 -0
- package/telegram-plugin/package.json +55 -0
- package/telegram-plugin/permission-rule.ts +133 -0
- package/telegram-plugin/permission-title.ts +117 -0
- package/telegram-plugin/pin-event-log.ts +76 -0
- package/telegram-plugin/plugin-logger.ts +136 -0
- package/telegram-plugin/progress-card-driver.ts +2697 -0
- package/telegram-plugin/progress-card-pin-manager.ts +589 -0
- package/telegram-plugin/progress-card-pin-watchdog.ts +98 -0
- package/telegram-plugin/progress-card.ts +1409 -0
- package/telegram-plugin/pty-partial-handler.ts +247 -0
- package/telegram-plugin/pty-tail.ts +730 -0
- package/telegram-plugin/quota-check.ts +474 -0
- package/telegram-plugin/recent-outbound-dedup.ts +169 -0
- package/telegram-plugin/registry/api-registry.test.ts +201 -0
- package/telegram-plugin/registry/subagents-bugs.test.ts +454 -0
- package/telegram-plugin/registry/subagents-schema.ts +509 -0
- package/telegram-plugin/registry/subagents.test.ts +476 -0
- package/telegram-plugin/registry/turns-schema.test.ts +101 -0
- package/telegram-plugin/registry/turns-schema.ts +417 -0
- package/telegram-plugin/retry-api-call.ts +172 -0
- package/telegram-plugin/scripts/build.mjs +78 -0
- package/telegram-plugin/secret-detect/audit.ts +66 -0
- package/telegram-plugin/secret-detect/chunker.ts +37 -0
- package/telegram-plugin/secret-detect/entropy.ts +20 -0
- package/telegram-plugin/secret-detect/gitleaks-loader.ts +74 -0
- package/telegram-plugin/secret-detect/gitleaks.toml +27 -0
- package/telegram-plugin/secret-detect/index.ts +218 -0
- package/telegram-plugin/secret-detect/kv-scanner.ts +60 -0
- package/telegram-plugin/secret-detect/mask.ts +13 -0
- package/telegram-plugin/secret-detect/patterns.ts +115 -0
- package/telegram-plugin/secret-detect/pipeline.ts +144 -0
- package/telegram-plugin/secret-detect/rewrite.ts +26 -0
- package/telegram-plugin/secret-detect/secretlint-source.ts +95 -0
- package/telegram-plugin/secret-detect/slug.ts +44 -0
- package/telegram-plugin/secret-detect/staging.ts +85 -0
- package/telegram-plugin/secret-detect/suppressor.ts +34 -0
- package/telegram-plugin/secret-detect/url-redact.ts +60 -0
- package/telegram-plugin/secret-detect/vault-write.ts +56 -0
- package/telegram-plugin/server.js +41795 -0
- package/telegram-plugin/server.ts +171 -0
- package/telegram-plugin/session-tail.ts +884 -0
- package/telegram-plugin/shared/bot-runtime.ts +324 -0
- package/telegram-plugin/silent-reply.ts +58 -0
- package/telegram-plugin/slot-banner-driver.ts +147 -0
- package/telegram-plugin/slot-banner.ts +86 -0
- package/telegram-plugin/start.js +26 -0
- package/telegram-plugin/startup-reset.ts +45 -0
- package/telegram-plugin/status-reactions.ts +332 -0
- package/telegram-plugin/steering.ts +155 -0
- package/telegram-plugin/sticker-aliases.ts +249 -0
- package/telegram-plugin/stream-controller.ts +311 -0
- package/telegram-plugin/stream-reply-handler.ts +664 -0
- package/telegram-plugin/streaming-metrics.ts +134 -0
- package/telegram-plugin/streaming-report.ts +204 -0
- package/telegram-plugin/subagent-watcher.ts +880 -0
- package/telegram-plugin/telegram-button-constraints.ts +191 -0
- package/telegram-plugin/telegraph.ts +381 -0
- package/telegram-plugin/tests/HARNESS.md +340 -0
- package/telegram-plugin/tests/_progress-card-harness.ts +105 -0
- package/telegram-plugin/tests/active-pins-boot-reaper.test.ts +211 -0
- package/telegram-plugin/tests/active-pins-sweep.test.ts +309 -0
- package/telegram-plugin/tests/active-pins.test.ts +187 -0
- package/telegram-plugin/tests/active-reactions-sweep.test.ts +116 -0
- package/telegram-plugin/tests/active-reactions.test.ts +198 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +352 -0
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +236 -0
- package/telegram-plugin/tests/answer-stream.test.ts +878 -0
- package/telegram-plugin/tests/ask-user.test.ts +203 -0
- package/telegram-plugin/tests/attachment-path.test.ts +199 -0
- package/telegram-plugin/tests/auth-account-identity-surface.test.ts +118 -0
- package/telegram-plugin/tests/auth-code-auto-capture.test.ts +144 -0
- package/telegram-plugin/tests/auth-code-redact.test.ts +248 -0
- package/telegram-plugin/tests/auth-dashboard-edge-cases.test.ts +260 -0
- package/telegram-plugin/tests/auth-dashboard-restart-flow.test.ts +140 -0
- package/telegram-plugin/tests/auth-dashboard-v3b.test.ts +559 -0
- package/telegram-plugin/tests/auth-dashboard.test.ts +1045 -0
- package/telegram-plugin/tests/auth-login-url-button.test.ts +122 -0
- package/telegram-plugin/tests/auth-slot-commands.test.ts +640 -0
- package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +183 -0
- package/telegram-plugin/tests/auto-fallback.test.ts +381 -0
- package/telegram-plugin/tests/boot-card-account-quota.test.ts +137 -0
- package/telegram-plugin/tests/boot-card-dedupe.test.ts +154 -0
- package/telegram-plugin/tests/boot-card-probe-target.test.ts +194 -0
- package/telegram-plugin/tests/boot-card-reason.test.ts +103 -0
- package/telegram-plugin/tests/boot-card-render.test.ts +219 -0
- package/telegram-plugin/tests/boot-probes.test.ts +451 -0
- package/telegram-plugin/tests/bot-api.harness.ts +116 -0
- package/telegram-plugin/tests/bot-runtime.test.ts +190 -0
- package/telegram-plugin/tests/bridge-anonymous-refuse.test.ts +60 -0
- package/telegram-plugin/tests/context-exhaustion.test.ts +114 -0
- package/telegram-plugin/tests/credits-watch.test.ts +221 -0
- package/telegram-plugin/tests/dm-command-gate.test.ts +176 -0
- package/telegram-plugin/tests/draft-stream.test.ts +752 -0
- package/telegram-plugin/tests/draft-transport.test.ts +141 -0
- package/telegram-plugin/tests/e2e.test.ts +436 -0
- package/telegram-plugin/tests/fake-bot-api.test.ts +213 -0
- package/telegram-plugin/tests/fake-bot-api.ts +617 -0
- package/telegram-plugin/tests/false-restart-banner.test.ts +253 -0
- package/telegram-plugin/tests/first-paint.test.ts +257 -0
- package/telegram-plugin/tests/fixtures/pty-tail-tmux-fragment.bin +6 -0
- package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +3624 -0
- package/telegram-plugin/tests/fleet-state-watcher.test.ts +101 -0
- package/telegram-plugin/tests/fleet-state.test.ts +185 -0
- package/telegram-plugin/tests/foreman-create-flow.test.ts +359 -0
- package/telegram-plugin/tests/foreman-handlers.test.ts +347 -0
- package/telegram-plugin/tests/foreman-state.test.ts +164 -0
- package/telegram-plugin/tests/foreman-write-ops.test.ts +214 -0
- package/telegram-plugin/tests/gateway-409-retry-banner.test.ts +173 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +72 -0
- package/telegram-plugin/tests/gateway-bridge.test.ts +811 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +414 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +144 -0
- package/telegram-plugin/tests/gateway-message-validator.test.ts +133 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +103 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +127 -0
- package/telegram-plugin/tests/gateway-startup-mutex.test.ts +284 -0
- package/telegram-plugin/tests/gateway-startup-network-retry.test.ts +185 -0
- package/telegram-plugin/tests/gateway-startup-reset.test.ts +72 -0
- package/telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts +125 -0
- package/telegram-plugin/tests/handoff-continuity.test.ts +249 -0
- package/telegram-plugin/tests/harness-ordering-invariants.test.ts +243 -0
- package/telegram-plugin/tests/harness-parse-mode-validation.test.ts +114 -0
- package/telegram-plugin/tests/history.test.ts +364 -0
- package/telegram-plugin/tests/html-balanced.ts +63 -0
- package/telegram-plugin/tests/html-sanitize.test.ts +146 -0
- package/telegram-plugin/tests/idle-footer-wiring.test.ts +88 -0
- package/telegram-plugin/tests/idle-footer.test.ts +66 -0
- package/telegram-plugin/tests/inbound-coalesce.test.ts +127 -0
- package/telegram-plugin/tests/inline-keyboard-callbacks.test.ts +150 -0
- package/telegram-plugin/tests/interrupt-marker.test.ts +126 -0
- package/telegram-plugin/tests/ipc-protocol.test.ts +218 -0
- package/telegram-plugin/tests/ipc-server-anonymous-refuse.test.ts +82 -0
- package/telegram-plugin/tests/ipc-server-client.test.ts +323 -0
- package/telegram-plugin/tests/ipc-server-race.test.ts +183 -0
- package/telegram-plugin/tests/ipc-server-validate-operator.test.ts +91 -0
- package/telegram-plugin/tests/ipc-server-validate-pty-partial.test.ts +64 -0
- package/telegram-plugin/tests/ipc-server-validate-update-placeholder.test.ts +77 -0
- package/telegram-plugin/tests/ipc-validator.test.ts +274 -0
- package/telegram-plugin/tests/issues-card.test.ts +495 -0
- package/telegram-plugin/tests/issues-watcher.test.ts +165 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +303 -0
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +159 -0
- package/telegram-plugin/tests/operator-events-history.test.ts +125 -0
- package/telegram-plugin/tests/operator-events-session-tail.test.ts +192 -0
- package/telegram-plugin/tests/operator-events.test.ts +331 -0
- package/telegram-plugin/tests/outbound-ordering.test.ts +293 -0
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +164 -0
- package/telegram-plugin/tests/permission-rule.test.ts +121 -0
- package/telegram-plugin/tests/permission-title.test.ts +106 -0
- package/telegram-plugin/tests/pin-event-log.test.ts +124 -0
- package/telegram-plugin/tests/plugin-logger.test.ts +97 -0
- package/telegram-plugin/tests/poll-health.test.ts +86 -0
- package/telegram-plugin/tests/preamble-suppressor.test.ts +285 -0
- package/telegram-plugin/tests/progress-card-api-failure-during-deferred.test.ts +73 -0
- package/telegram-plugin/tests/progress-card-close-paths-converge.test.ts +272 -0
- package/telegram-plugin/tests/progress-card-cross-turn.test.ts +258 -0
- package/telegram-plugin/tests/progress-card-dispose-preservepending.test.ts +81 -0
- package/telegram-plugin/tests/progress-card-draft-flag.test.ts +80 -0
- package/telegram-plugin/tests/progress-card-driver-eviction.test.ts +215 -0
- package/telegram-plugin/tests/progress-card-driver-fleet-shadow.test.ts +123 -0
- package/telegram-plugin/tests/progress-card-driver-force-complete-parent-done.test.ts +76 -0
- package/telegram-plugin/tests/progress-card-edit-timestamps-budget.test.ts +62 -0
- package/telegram-plugin/tests/progress-card-memory-bounds.test.ts +84 -0
- package/telegram-plugin/tests/progress-card-pin-failure-paths.test.ts +139 -0
- package/telegram-plugin/tests/progress-card-pin-manager.test.ts +773 -0
- package/telegram-plugin/tests/progress-card-pin-race-fast-turn.test.ts +66 -0
- package/telegram-plugin/tests/progress-card-pin-sidecar-partial-write.test.ts +64 -0
- package/telegram-plugin/tests/progress-card-pin-watchdog.test.ts +190 -0
- package/telegram-plugin/tests/progress-card-sigterm-pin-flush.test.ts +146 -0
- package/telegram-plugin/tests/progress-update.test.ts +236 -0
- package/telegram-plugin/tests/protocol-fixtures.test.ts +59 -0
- package/telegram-plugin/tests/protocol-fixtures.ts +198 -0
- package/telegram-plugin/tests/pty-partial-handler.test.ts +326 -0
- package/telegram-plugin/tests/pty-tail-real-fixture.test.ts +114 -0
- package/telegram-plugin/tests/pty-tail-tmux-fragment.test.ts +71 -0
- package/telegram-plugin/tests/pty-tail.test.ts +525 -0
- package/telegram-plugin/tests/quota-cache.test.ts +187 -0
- package/telegram-plugin/tests/quota-check.test.ts +622 -0
- package/telegram-plugin/tests/races.test.ts +842 -0
- package/telegram-plugin/tests/real-gateway-f1-ladder-integrity.test.ts +123 -0
- package/telegram-plugin/tests/real-gateway-f2-instant-draft.test.ts +82 -0
- package/telegram-plugin/tests/real-gateway-f3-late-card.test.ts +114 -0
- package/telegram-plugin/tests/real-gateway-harness.ts +699 -0
- package/telegram-plugin/tests/real-gateway-i6-turn-flush-replay-dedup.test.ts +313 -0
- package/telegram-plugin/tests/real-gateway-ipc-lifecycle.test.ts +299 -0
- package/telegram-plugin/tests/real-gateway-spec.test.ts +487 -0
- package/telegram-plugin/tests/real-gateway.smoke.test.ts +101 -0
- package/telegram-plugin/tests/recent-outbound-dedup.test.ts +192 -0
- package/telegram-plugin/tests/registry-turns.test.ts +432 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +149 -0
- package/telegram-plugin/tests/resolve-calling-subagent.test.ts +269 -0
- package/telegram-plugin/tests/restart-watchdog.test.ts +224 -0
- package/telegram-plugin/tests/retry-api-call.test.ts +287 -0
- package/telegram-plugin/tests/secret-detect-audit.test.ts +58 -0
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +83 -0
- package/telegram-plugin/tests/secret-detect-gitleaks.test.ts +32 -0
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +308 -0
- package/telegram-plugin/tests/secret-detect-pipeline.test.ts +123 -0
- package/telegram-plugin/tests/secret-detect-secretlint.test.ts +101 -0
- package/telegram-plugin/tests/secret-detect-staging.test.ts +45 -0
- package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +67 -0
- package/telegram-plugin/tests/secret-detect.test.ts +223 -0
- package/telegram-plugin/tests/secret-guard-pretool.test.ts +194 -0
- package/telegram-plugin/tests/send-typing-action-validation.test.ts +61 -0
- package/telegram-plugin/tests/session-tail-capped.test.ts +285 -0
- package/telegram-plugin/tests/session-tail.test.ts +524 -0
- package/telegram-plugin/tests/setup-flow.test.ts +510 -0
- package/telegram-plugin/tests/setup-state.test.ts +146 -0
- package/telegram-plugin/tests/silent-reply-guard.test.ts +122 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +350 -0
- package/telegram-plugin/tests/slot-banner.test.ts +74 -0
- package/telegram-plugin/tests/snapshot-serializer.ts +79 -0
- package/telegram-plugin/tests/spawn-detached-cgroup-escape.test.ts +51 -0
- package/telegram-plugin/tests/status-accent.test.ts +186 -0
- package/telegram-plugin/tests/status-reactions-allowed-filter.test.ts +132 -0
- package/telegram-plugin/tests/status-reactions.test.ts +314 -0
- package/telegram-plugin/tests/steering.test.ts +282 -0
- package/telegram-plugin/tests/sticker-aliases.test.ts +232 -0
- package/telegram-plugin/tests/stream-controller-html-fallback.test.ts +127 -0
- package/telegram-plugin/tests/stream-controller.test.ts +262 -0
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +208 -0
- package/telegram-plugin/tests/stream-reply-handler.test.ts +1292 -0
- package/telegram-plugin/tests/streaming-e2e.test.ts +389 -0
- package/telegram-plugin/tests/streaming-metrics.test.ts +201 -0
- package/telegram-plugin/tests/streaming-orchestration.test.ts +756 -0
- package/telegram-plugin/tests/subagent-registry-bugs.test.ts +725 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +213 -0
- package/telegram-plugin/tests/subagent-watcher-parent-marker.test.ts +274 -0
- package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +243 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +877 -0
- package/telegram-plugin/tests/subagents-schema-init-order.test.ts +109 -0
- package/telegram-plugin/tests/sync-chat-running-subagents.test.ts +116 -0
- package/telegram-plugin/tests/telegram-button-constraints.test.ts +194 -0
- package/telegram-plugin/tests/telegram-format.test.ts +1093 -0
- package/telegram-plugin/tests/telegraph.test.ts +246 -0
- package/telegram-plugin/tests/tool-labels.test.ts +383 -0
- package/telegram-plugin/tests/turn-active-marker.test.ts +195 -0
- package/telegram-plugin/tests/turn-end-regressions.test.ts +489 -0
- package/telegram-plugin/tests/turn-flush-card-takeover.test.ts +218 -0
- package/telegram-plugin/tests/turn-flush-dedup-controller.test.ts +144 -0
- package/telegram-plugin/tests/turn-flush-prose-recovery.test.ts +78 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +189 -0
- package/telegram-plugin/tests/turn-signal-tracker.test.ts +107 -0
- package/telegram-plugin/tests/turns-writer.test.ts +323 -0
- package/telegram-plugin/tests/two-zone-bg-carry-full-lifecycle.test.ts +131 -0
- package/telegram-plugin/tests/two-zone-bg-detection.test.ts +120 -0
- package/telegram-plugin/tests/two-zone-bg-done-when-all-terminal.test.ts +114 -0
- package/telegram-plugin/tests/two-zone-bg-early-turn-end.test.ts +87 -0
- package/telegram-plugin/tests/two-zone-bg-survives-next-turn.test.ts +211 -0
- package/telegram-plugin/tests/two-zone-card-cap.test.ts +62 -0
- package/telegram-plugin/tests/two-zone-card-fleet-row.test.ts +101 -0
- package/telegram-plugin/tests/two-zone-card-header-phases.test.ts +68 -0
- package/telegram-plugin/tests/two-zone-card-html-balance.test.ts +110 -0
- package/telegram-plugin/tests/two-zone-card-lifecycle.test.ts +128 -0
- package/telegram-plugin/tests/two-zone-card-sanitise.test.ts +58 -0
- package/telegram-plugin/tests/two-zone-card-snapshot.test.ts +133 -0
- package/telegram-plugin/tests/two-zone-concurrent-turns-isolation.test.ts +155 -0
- package/telegram-plugin/tests/two-zone-phasefor-precedence.test.ts +117 -0
- package/telegram-plugin/tests/two-zone-snapshot-extras.test.ts +143 -0
- package/telegram-plugin/tests/two-zone-stuck-edit-throttle.test.ts +149 -0
- package/telegram-plugin/tests/two-zone-stuck-header-escalation.test.ts +101 -0
- package/telegram-plugin/tests/two-zone-stuck-per-member.test.ts +114 -0
- package/telegram-plugin/tests/two-zone-stuck-recovery.test.ts +105 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +141 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +147 -0
- package/telegram-plugin/tests/update-factory-edited-and-reactions.test.ts +108 -0
- package/telegram-plugin/tests/update-factory.ts +305 -0
- package/telegram-plugin/tests/vault-grant-wizard.test.ts +84 -0
- package/telegram-plugin/tests/vault-grants-revoke.test.ts +265 -0
- package/telegram-plugin/tests/vault-subcommands.test.ts +234 -0
- package/telegram-plugin/tests/voice-transcribe.test.ts +196 -0
- package/telegram-plugin/tests/waiting-ux-harness.ts +381 -0
- package/telegram-plugin/tests/waiting-ux.e2e.test.ts +233 -0
- package/telegram-plugin/tests/welcome-text.test.ts +407 -0
- package/telegram-plugin/tool-error-filter.ts +89 -0
- package/telegram-plugin/tool-labels.ts +330 -0
- package/telegram-plugin/tool-names.ts +53 -0
- package/telegram-plugin/turn-flush-prose-recovery.ts +40 -0
- package/telegram-plugin/turn-flush-safety.ts +109 -0
- package/telegram-plugin/turn-signal-tracker.ts +79 -0
- package/telegram-plugin/two-zone-card.ts +249 -0
- package/telegram-plugin/typing-wrap.ts +92 -0
- package/telegram-plugin/voice-transcribe.ts +166 -0
- package/telegram-plugin/welcome-text.ts +359 -0
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the subagent-watcher module.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Registry transitions (register, tool_use, turn_end)
|
|
6
|
+
* - JSONL tail parsing (description from sub_agent_text, toolCount from sub_agent_tool_use)
|
|
7
|
+
* - Stall detection (stall notification after stallThresholdMs idle)
|
|
8
|
+
* - Completion notification (sent once on state=done)
|
|
9
|
+
* - Historical-vs-active filter (pre-existing files do not fire stalls/completions)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
13
|
+
import * as fs from 'fs'
|
|
14
|
+
import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, rmSync } from 'fs'
|
|
15
|
+
import { tmpdir } from 'os'
|
|
16
|
+
import { join } from 'path'
|
|
17
|
+
import { startSubagentWatcher, type WorkerEntry } from '../subagent-watcher.js'
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function makeEntry(overrides: Partial<WorkerEntry> = {}): WorkerEntry {
|
|
22
|
+
return {
|
|
23
|
+
agentId: 'test-agent-01',
|
|
24
|
+
filePath: '/tmp/agent-test-agent-01.jsonl',
|
|
25
|
+
description: 'Build the feature',
|
|
26
|
+
state: 'running',
|
|
27
|
+
dispatchedAt: 1000,
|
|
28
|
+
lastActivityAt: 1000,
|
|
29
|
+
toolCount: 0,
|
|
30
|
+
stallNotified: false,
|
|
31
|
+
completionNotified: false,
|
|
32
|
+
lastSummaryLine: '',
|
|
33
|
+
lastTool: null,
|
|
34
|
+
historical: false,
|
|
35
|
+
...overrides,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── startSubagentWatcher harness ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Minimal harness to drive the watcher without real filesystem or timers.
|
|
43
|
+
*
|
|
44
|
+
* We mock:
|
|
45
|
+
* - fs.existsSync, fs.readdirSync → control which dirs/files are "on disk"
|
|
46
|
+
* - fs.statSync → control file sizes (drives JSONL read)
|
|
47
|
+
* - fs.openSync, fs.readSync, fs.closeSync → feed JSONL content
|
|
48
|
+
* - fs.watch → stub (returns a fake watcher)
|
|
49
|
+
* - Date.now → injected via config.now
|
|
50
|
+
* - setInterval / clearInterval → injected via config
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
function buildJSONL(...lines: object[]): string {
|
|
54
|
+
return lines.map((l) => JSON.stringify(l)).join('\n') + '\n'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function subAgentUserMsg(promptText: string) {
|
|
58
|
+
return { type: 'user', message: { content: [{ type: 'text', text: promptText }] } }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function subAgentAssistantText(text: string) {
|
|
62
|
+
return {
|
|
63
|
+
type: 'assistant',
|
|
64
|
+
message: { content: [{ type: 'text', text }] },
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function subAgentToolUse(name: string, id: string) {
|
|
69
|
+
return {
|
|
70
|
+
type: 'assistant',
|
|
71
|
+
message: { content: [{ type: 'tool_use', name, id, input: {} }] },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function subAgentTurnDuration() {
|
|
76
|
+
return { type: 'system', subtype: 'turn_duration', durationMs: 5000 }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface WatcherHarness {
|
|
80
|
+
notifications: string[]
|
|
81
|
+
logs: string[]
|
|
82
|
+
advance: (ms: number) => void
|
|
83
|
+
// Trigger the poll timer manually
|
|
84
|
+
poll: () => void
|
|
85
|
+
// Expose the watcher
|
|
86
|
+
watcher: ReturnType<typeof startSubagentWatcher>
|
|
87
|
+
// Current mocked time
|
|
88
|
+
now: () => number
|
|
89
|
+
// Mutable fs object — tests can override .readSync, .statSync etc.
|
|
90
|
+
// for per-test customization (the watcher reads each method on every call,
|
|
91
|
+
// so reassigning is picked up immediately).
|
|
92
|
+
mockFs: {
|
|
93
|
+
existsSync: typeof fs.existsSync
|
|
94
|
+
readdirSync: typeof fs.readdirSync
|
|
95
|
+
statSync: typeof fs.statSync
|
|
96
|
+
openSync: typeof fs.openSync
|
|
97
|
+
closeSync: typeof fs.closeSync
|
|
98
|
+
readSync: typeof fs.readSync
|
|
99
|
+
watch: typeof fs.watch
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function makeHarness(opts: {
|
|
104
|
+
agentDir?: string
|
|
105
|
+
files?: Record<string, string> // filePath → JSONL content
|
|
106
|
+
dirs?: Record<string, string[]> // dirPath → list of filenames
|
|
107
|
+
existingDirs?: string[]
|
|
108
|
+
stallThresholdMs?: number
|
|
109
|
+
rescanMs?: number
|
|
110
|
+
}): WatcherHarness {
|
|
111
|
+
const {
|
|
112
|
+
agentDir = '/home/user/.switchroom/agents/myagent',
|
|
113
|
+
files = {},
|
|
114
|
+
dirs = {},
|
|
115
|
+
existingDirs = [],
|
|
116
|
+
stallThresholdMs = 60_000,
|
|
117
|
+
rescanMs = 500,
|
|
118
|
+
} = opts
|
|
119
|
+
|
|
120
|
+
let currentTime = 1000
|
|
121
|
+
const notifications: string[] = []
|
|
122
|
+
const logs: string[] = []
|
|
123
|
+
|
|
124
|
+
// Track all JSONL content per path for statSync + read simulation
|
|
125
|
+
const fileContents: Map<string, Buffer> = new Map()
|
|
126
|
+
for (const [path, content] of Object.entries(files)) {
|
|
127
|
+
fileContents.set(path, Buffer.from(content, 'utf-8'))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Build a mock fs object — injected via watcher config (ESM namespace
|
|
131
|
+
// exports are not configurable so vi.spyOn(fs, ...) doesn't work).
|
|
132
|
+
const fakeWatchers: Array<{ close: () => void }> = []
|
|
133
|
+
// Track which path was last opened so readSync can serve the right content.
|
|
134
|
+
// The mock fd is always 42; we only ever have one open file at a time.
|
|
135
|
+
let lastOpenedPath: string | null = null
|
|
136
|
+
const mockFs = {
|
|
137
|
+
existsSync: ((p: fs.PathLike) => {
|
|
138
|
+
const ps = String(p)
|
|
139
|
+
if (existingDirs.includes(ps)) return true
|
|
140
|
+
if (dirs[ps] !== undefined) return true
|
|
141
|
+
if (fileContents.has(ps)) return true
|
|
142
|
+
for (const fp of fileContents.keys()) {
|
|
143
|
+
if (fp.startsWith(ps + '/')) return true
|
|
144
|
+
}
|
|
145
|
+
return false
|
|
146
|
+
}) as typeof fs.existsSync,
|
|
147
|
+
readdirSync: ((p: fs.PathLike) => {
|
|
148
|
+
const ps = String(p)
|
|
149
|
+
if (dirs[ps]) return dirs[ps]
|
|
150
|
+
const children = new Set<string>()
|
|
151
|
+
for (const fp of fileContents.keys()) {
|
|
152
|
+
if (fp.startsWith(ps + '/')) {
|
|
153
|
+
const rest = fp.slice(ps.length + 1)
|
|
154
|
+
const part = rest.split('/')[0]
|
|
155
|
+
if (part) children.add(part)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return Array.from(children)
|
|
159
|
+
}) as unknown as typeof fs.readdirSync,
|
|
160
|
+
statSync: ((p: fs.PathLike) => {
|
|
161
|
+
const ps = String(p)
|
|
162
|
+
const content = fileContents.get(ps)
|
|
163
|
+
return { size: content?.length ?? 0 } as fs.Stats
|
|
164
|
+
}) as typeof fs.statSync,
|
|
165
|
+
openSync: ((p: fs.PathLike) => {
|
|
166
|
+
lastOpenedPath = String(p)
|
|
167
|
+
return 42
|
|
168
|
+
}) as unknown as typeof fs.openSync,
|
|
169
|
+
closeSync: (() => {
|
|
170
|
+
lastOpenedPath = null
|
|
171
|
+
}) as typeof fs.closeSync,
|
|
172
|
+
readSync: ((
|
|
173
|
+
_fd: number,
|
|
174
|
+
buf: NodeJS.ArrayBufferView,
|
|
175
|
+
offset: number,
|
|
176
|
+
length: number,
|
|
177
|
+
position: number | null,
|
|
178
|
+
): number => {
|
|
179
|
+
// Serve content from fileContents for the currently open file.
|
|
180
|
+
const content = lastOpenedPath != null ? fileContents.get(lastOpenedPath) : undefined
|
|
181
|
+
if (!content) return 0
|
|
182
|
+
const pos = position ?? 0
|
|
183
|
+
const src = content.slice(pos, pos + length)
|
|
184
|
+
;(src as Buffer).copy(buf as Buffer, offset)
|
|
185
|
+
return src.length
|
|
186
|
+
}) as unknown as typeof fs.readSync,
|
|
187
|
+
watch: (() => {
|
|
188
|
+
const w = { close: vi.fn() }
|
|
189
|
+
fakeWatchers.push(w)
|
|
190
|
+
return w as unknown as fs.FSWatcher
|
|
191
|
+
}) as unknown as typeof fs.watch,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Injected timers
|
|
195
|
+
const intervals: Array<{ fn: () => void; ms: number; ref: number; fireAt: number }> = []
|
|
196
|
+
const timeouts: Array<{ fn: () => void; ref: number; fireAt: number }> = []
|
|
197
|
+
let nextRef = 1
|
|
198
|
+
|
|
199
|
+
const watcher = startSubagentWatcher({
|
|
200
|
+
agentDir,
|
|
201
|
+
sendNotification: (text) => notifications.push(text),
|
|
202
|
+
stallThresholdMs,
|
|
203
|
+
rescanMs,
|
|
204
|
+
now: () => currentTime,
|
|
205
|
+
setInterval: (fn, ms) => {
|
|
206
|
+
const ref = nextRef++
|
|
207
|
+
intervals.push({ fn, ms, ref, fireAt: currentTime + ms })
|
|
208
|
+
return { ref }
|
|
209
|
+
},
|
|
210
|
+
clearInterval: (handle) => {
|
|
211
|
+
const { ref } = handle as { ref: number }
|
|
212
|
+
const idx = intervals.findIndex((i) => i.ref === ref)
|
|
213
|
+
if (idx !== -1) intervals.splice(idx, 1)
|
|
214
|
+
},
|
|
215
|
+
setTimeout: (fn, ms) => {
|
|
216
|
+
const ref = nextRef++
|
|
217
|
+
timeouts.push({ fn, ref, fireAt: currentTime + ms })
|
|
218
|
+
return { ref }
|
|
219
|
+
},
|
|
220
|
+
clearTimeout: (handle) => {
|
|
221
|
+
const { ref } = handle as { ref: number }
|
|
222
|
+
const idx = timeouts.findIndex((t) => t.ref === ref)
|
|
223
|
+
if (idx !== -1) timeouts.splice(idx, 1)
|
|
224
|
+
},
|
|
225
|
+
fs: mockFs,
|
|
226
|
+
log: (msg: string) => { logs.push(msg) },
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const advance = (ms: number): void => {
|
|
230
|
+
currentTime += ms
|
|
231
|
+
// Fire any intervals whose fireAt <= currentTime
|
|
232
|
+
for (;;) {
|
|
233
|
+
intervals.sort((a, b) => a.fireAt - b.fireAt)
|
|
234
|
+
const next = intervals[0]
|
|
235
|
+
if (!next || next.fireAt > currentTime) break
|
|
236
|
+
next.fireAt += next.ms
|
|
237
|
+
next.fn()
|
|
238
|
+
}
|
|
239
|
+
// Fire any one-shot timeouts whose fireAt <= currentTime — drain
|
|
240
|
+
// the queue (oneshots, so remove on fire).
|
|
241
|
+
for (;;) {
|
|
242
|
+
timeouts.sort((a, b) => a.fireAt - b.fireAt)
|
|
243
|
+
const next = timeouts[0]
|
|
244
|
+
if (!next || next.fireAt > currentTime) break
|
|
245
|
+
timeouts.shift()
|
|
246
|
+
next.fn()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const poll = (): void => {
|
|
251
|
+
const pollInterval = intervals[0]
|
|
252
|
+
if (pollInterval) pollInterval.fn()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
notifications,
|
|
257
|
+
logs,
|
|
258
|
+
advance,
|
|
259
|
+
poll,
|
|
260
|
+
watcher,
|
|
261
|
+
now: () => currentTime,
|
|
262
|
+
mockFs,
|
|
263
|
+
fakeWatchers,
|
|
264
|
+
pendingTimeouts: () => timeouts.length,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Tests ───────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
describe('startSubagentWatcher', () => {
|
|
271
|
+
beforeEach(() => {
|
|
272
|
+
vi.restoreAllMocks()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('does nothing when the agent dir has no .claude/projects', () => {
|
|
276
|
+
const h = makeHarness({ agentDir: '/nonexistent', existingDirs: [] })
|
|
277
|
+
h.poll()
|
|
278
|
+
expect(h.notifications).toHaveLength(0)
|
|
279
|
+
h.watcher.stop()
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('detects a new subagent JSONL created after startup', () => {
|
|
283
|
+
// Watcher starts with an empty subagents dir, then a new file appears.
|
|
284
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
285
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
286
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
287
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
288
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
289
|
+
const jsonlPath = `${subagentsDir}/agent-deadbeef.jsonl`
|
|
290
|
+
const content = buildJSONL(subAgentUserMsg('Fix the tests please'))
|
|
291
|
+
|
|
292
|
+
const h = makeHarness({
|
|
293
|
+
agentDir,
|
|
294
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
295
|
+
dirs: {
|
|
296
|
+
[projectsRoot]: ['myproject'],
|
|
297
|
+
[projectDir]: ['session-abc123'],
|
|
298
|
+
// subagentsDir is empty at startup
|
|
299
|
+
[subagentsDir]: [],
|
|
300
|
+
},
|
|
301
|
+
files: {},
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// No notifications during boot
|
|
305
|
+
expect(h.notifications).toHaveLength(0)
|
|
306
|
+
|
|
307
|
+
// Simulate the new file appearing after startup
|
|
308
|
+
h.mockFs.readdirSync = ((p: unknown) => {
|
|
309
|
+
const ps = String(p)
|
|
310
|
+
if (ps === subagentsDir) return ['agent-deadbeef.jsonl']
|
|
311
|
+
if (ps === projectsRoot) return ['myproject']
|
|
312
|
+
if (ps === projectDir) return ['session-abc123']
|
|
313
|
+
return []
|
|
314
|
+
}) as unknown as typeof fs.readdirSync
|
|
315
|
+
h.mockFs.existsSync = ((p: unknown) => {
|
|
316
|
+
const ps = String(p)
|
|
317
|
+
return [projectsRoot, projectDir, sessionDir, subagentsDir, jsonlPath].includes(ps)
|
|
318
|
+
}) as typeof fs.existsSync
|
|
319
|
+
const contentBuf = Buffer.from(content, 'utf-8')
|
|
320
|
+
h.mockFs.statSync = ((p: unknown) => {
|
|
321
|
+
if (String(p) === jsonlPath) return { size: contentBuf.length } as import('fs').Stats
|
|
322
|
+
return { size: 0 } as import('fs').Stats
|
|
323
|
+
}) as typeof fs.statSync
|
|
324
|
+
|
|
325
|
+
h.poll()
|
|
326
|
+
|
|
327
|
+
const entry = h.watcher.getRegistry().get('deadbeef')
|
|
328
|
+
expect(entry).toBeDefined()
|
|
329
|
+
expect(entry?.historical).toBe(false)
|
|
330
|
+
expect(entry?.state).toBe('running')
|
|
331
|
+
|
|
332
|
+
h.watcher.stop()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// The next three tests use a real tmp dir + real files + real fs (no
|
|
336
|
+
// injection). The over-mocked harness can't reproduce the read-sequence
|
|
337
|
+
// statefully — real fs is simpler and more accurate.
|
|
338
|
+
describe('with real tmp filesystem', () => {
|
|
339
|
+
let tmpRoot = ''
|
|
340
|
+
const startedWatchers: Array<{ stop(): void }> = []
|
|
341
|
+
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'switchroom-watcher-test-'))
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
afterEach(() => {
|
|
347
|
+
while (startedWatchers.length) {
|
|
348
|
+
try { startedWatchers.pop()?.stop() } catch { /* ignore */ }
|
|
349
|
+
}
|
|
350
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }) } catch { /* ignore */ }
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
function setupRealFs(jsonlContent: string, agentId: string): {
|
|
354
|
+
agentDir: string
|
|
355
|
+
jsonlPath: string
|
|
356
|
+
} {
|
|
357
|
+
const agentDir = join(tmpRoot, 'agent')
|
|
358
|
+
const subagentsDir = join(agentDir, '.claude', 'projects', 'p1', 'session-abc', 'subagents')
|
|
359
|
+
mkdirSync(subagentsDir, { recursive: true })
|
|
360
|
+
const jsonlPath = join(subagentsDir, `agent-${agentId}.jsonl`)
|
|
361
|
+
writeFileSync(jsonlPath, jsonlContent)
|
|
362
|
+
return { agentDir, jsonlPath }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function startWatcherSync(opts: { agentDir: string }): {
|
|
366
|
+
notifications: string[]
|
|
367
|
+
poll: () => void
|
|
368
|
+
watcher: ReturnType<typeof startSubagentWatcher>
|
|
369
|
+
fireScheduledCleanups: () => number
|
|
370
|
+
} {
|
|
371
|
+
const notifications: string[] = []
|
|
372
|
+
const intervals: Array<{ fn: () => void; ref: number }> = []
|
|
373
|
+
const timeouts: Array<{ fn: () => void; ref: number }> = []
|
|
374
|
+
let nextRef = 1
|
|
375
|
+
const watcher = startSubagentWatcher({
|
|
376
|
+
agentDir: opts.agentDir,
|
|
377
|
+
sendNotification: (text) => notifications.push(text),
|
|
378
|
+
stallThresholdMs: 60_000,
|
|
379
|
+
rescanMs: 500,
|
|
380
|
+
now: () => Date.now(),
|
|
381
|
+
setInterval: (fn) => {
|
|
382
|
+
const ref = nextRef++
|
|
383
|
+
intervals.push({ fn, ref })
|
|
384
|
+
return { ref }
|
|
385
|
+
},
|
|
386
|
+
clearInterval: (handle) => {
|
|
387
|
+
const { ref } = handle as { ref: number }
|
|
388
|
+
const idx = intervals.findIndex((i) => i.ref === ref)
|
|
389
|
+
if (idx !== -1) intervals.splice(idx, 1)
|
|
390
|
+
},
|
|
391
|
+
setTimeout: (fn) => {
|
|
392
|
+
const ref = nextRef++
|
|
393
|
+
timeouts.push({ fn, ref })
|
|
394
|
+
return { ref }
|
|
395
|
+
},
|
|
396
|
+
clearTimeout: (handle) => {
|
|
397
|
+
const { ref } = handle as { ref: number }
|
|
398
|
+
const idx = timeouts.findIndex((t) => t.ref === ref)
|
|
399
|
+
if (idx !== -1) timeouts.splice(idx, 1)
|
|
400
|
+
},
|
|
401
|
+
log: () => {},
|
|
402
|
+
})
|
|
403
|
+
startedWatchers.push(watcher)
|
|
404
|
+
return {
|
|
405
|
+
notifications,
|
|
406
|
+
poll: () => intervals[0]?.fn(),
|
|
407
|
+
watcher,
|
|
408
|
+
// Drain any scheduled deferred-cleanups regardless of fireAt time
|
|
409
|
+
// (tests use this to advance past the 30s grace deterministically).
|
|
410
|
+
fireScheduledCleanups: () => {
|
|
411
|
+
let fired = 0
|
|
412
|
+
while (timeouts.length) {
|
|
413
|
+
const next = timeouts.shift()!
|
|
414
|
+
next.fn()
|
|
415
|
+
fired++
|
|
416
|
+
}
|
|
417
|
+
return fired
|
|
418
|
+
},
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
it('does NOT overwrite description with sub_agent_text narrative (#352)', () => {
|
|
423
|
+
// Pre-#352 the watcher would overwrite a sub-agent's dispatch
|
|
424
|
+
// description with the first narrative line it saw. That made identical
|
|
425
|
+
// dispatches render differently depending on which event reached the
|
|
426
|
+
// watcher first — a race-condition-dependent UX bug.
|
|
427
|
+
//
|
|
428
|
+
// Post-#352 the description must remain whatever the watcher started
|
|
429
|
+
// with (the dispatch description set by the parent Agent tool_use
|
|
430
|
+
// input, or the placeholder when the watcher is bootstrapped without
|
|
431
|
+
// one). Narrative text is recorded in `lastSummaryLine` instead.
|
|
432
|
+
const content = buildJSONL(
|
|
433
|
+
subAgentUserMsg('Do the thing'),
|
|
434
|
+
subAgentAssistantText('I will implement the feature now'),
|
|
435
|
+
)
|
|
436
|
+
const { agentDir } = setupRealFs(content, 'deadbeef')
|
|
437
|
+
const h = startWatcherSync({ agentDir })
|
|
438
|
+
h.poll()
|
|
439
|
+
const entry = h.watcher.getRegistry().get('deadbeef')
|
|
440
|
+
expect(entry).toBeDefined()
|
|
441
|
+
// Description stays as the bootstrap value — the watcher must NOT
|
|
442
|
+
// promote the narrative line into the description field.
|
|
443
|
+
expect(entry?.description).not.toMatch(/I will implement/)
|
|
444
|
+
// Narrative text still flows into lastSummaryLine for telemetry.
|
|
445
|
+
expect(entry?.lastSummaryLine).toMatch(/I will implement/)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('counts tools from sub_agent_tool_use events', () => {
|
|
449
|
+
const content = buildJSONL(
|
|
450
|
+
subAgentUserMsg('Fix things'),
|
|
451
|
+
subAgentToolUse('Read', 'id1'),
|
|
452
|
+
subAgentToolUse('Bash', 'id2'),
|
|
453
|
+
subAgentToolUse('Edit', 'id3'),
|
|
454
|
+
)
|
|
455
|
+
const { agentDir } = setupRealFs(content, 'deadbeef')
|
|
456
|
+
const h = startWatcherSync({ agentDir })
|
|
457
|
+
h.poll()
|
|
458
|
+
const entry = h.watcher.getRegistry().get('deadbeef')
|
|
459
|
+
expect(entry).toBeDefined()
|
|
460
|
+
expect(entry?.toolCount).toBe(3)
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('does NOT emit completion notification for a file already done at startup', () => {
|
|
464
|
+
// File pre-exists with turn_end already written — agent was done before
|
|
465
|
+
// the watcher started. No completion notification should fire.
|
|
466
|
+
const content = buildJSONL(
|
|
467
|
+
subAgentUserMsg('Do the task'),
|
|
468
|
+
subAgentTurnDuration(),
|
|
469
|
+
)
|
|
470
|
+
const { agentDir } = setupRealFs(content, 'deadbeef')
|
|
471
|
+
const h = startWatcherSync({ agentDir })
|
|
472
|
+
h.poll()
|
|
473
|
+
const entry = h.watcher.getRegistry().get('deadbeef')
|
|
474
|
+
expect(entry).toBeDefined()
|
|
475
|
+
expect(entry?.state).toBe('done')
|
|
476
|
+
// Already done at boot → historical → no completion notification
|
|
477
|
+
const completionNotifs = h.notifications.filter((n) => n.includes('Worker done'))
|
|
478
|
+
expect(completionNotifs).toHaveLength(0)
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('emits completion notification when a NEW subagent finishes', () => {
|
|
482
|
+
// File does NOT exist at startup. Watcher starts, then file appears
|
|
483
|
+
// with an in-flight status. Then turn_end is appended — we should
|
|
484
|
+
// get a completion notification.
|
|
485
|
+
const agentDir = join(tmpRoot, 'agent')
|
|
486
|
+
const subagentsDir = join(agentDir, '.claude', 'projects', 'p1', 'session-abc', 'subagents')
|
|
487
|
+
mkdirSync(subagentsDir, { recursive: true })
|
|
488
|
+
const jsonlPath = join(subagentsDir, 'agent-newagent.jsonl')
|
|
489
|
+
|
|
490
|
+
// Write just the initial user message (in-flight state)
|
|
491
|
+
const initialContent = buildJSONL(subAgentUserMsg('Do the task'))
|
|
492
|
+
|
|
493
|
+
const h = startWatcherSync({ agentDir })
|
|
494
|
+
|
|
495
|
+
// Write file AFTER watcher starts (post-startup, so not historical)
|
|
496
|
+
writeFileSync(jsonlPath, initialContent)
|
|
497
|
+
h.poll()
|
|
498
|
+
|
|
499
|
+
const entry = h.watcher.getRegistry().get('newagent')
|
|
500
|
+
expect(entry).toBeDefined()
|
|
501
|
+
expect(entry?.state).toBe('running')
|
|
502
|
+
expect(entry?.historical).toBe(false)
|
|
503
|
+
|
|
504
|
+
// Now append turn_end to simulate agent finishing
|
|
505
|
+
appendFileSync(jsonlPath, buildJSONL(subAgentTurnDuration()))
|
|
506
|
+
h.poll()
|
|
507
|
+
|
|
508
|
+
const completionNotifs = h.notifications.filter((n) => n.includes('Worker done'))
|
|
509
|
+
expect(completionNotifs).toHaveLength(1)
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
it('drops the FSWatcher + Map entries after terminal-state grace fires (MEM1)', () => {
|
|
513
|
+
// Pre-MEM1 fix: per-subagent FSWatcher entries lived for the
|
|
514
|
+
// entire process lifetime. With sustained sub-agent load a
|
|
515
|
+
// long-running gateway hit ulimit -n. This test pins the deferred
|
|
516
|
+
// cleanup contract: completion → fire grace timer → tails/registry
|
|
517
|
+
// entries removed → underlying FSWatcher closed.
|
|
518
|
+
const agentDir = join(tmpRoot, 'agent')
|
|
519
|
+
const subagentsDir = join(agentDir, '.claude', 'projects', 'p1', 'session-abc', 'subagents')
|
|
520
|
+
mkdirSync(subagentsDir, { recursive: true })
|
|
521
|
+
const jsonlPath = join(subagentsDir, 'agent-cleanme.jsonl')
|
|
522
|
+
writeFileSync(jsonlPath, buildJSONL(subAgentUserMsg('Do the task')))
|
|
523
|
+
|
|
524
|
+
const h = startWatcherSync({ agentDir })
|
|
525
|
+
|
|
526
|
+
// Discover + register the agent (running state).
|
|
527
|
+
h.poll()
|
|
528
|
+
expect(h.watcher.getRegistry().has('cleanme')).toBe(true)
|
|
529
|
+
|
|
530
|
+
// Append turn_end → done state → completion notification + scheduled cleanup.
|
|
531
|
+
appendFileSync(jsonlPath, buildJSONL(subAgentTurnDuration()))
|
|
532
|
+
h.poll()
|
|
533
|
+
expect(h.notifications.some((n) => n.includes('Worker done'))).toBe(true)
|
|
534
|
+
// Registry still has it during the 30s grace window.
|
|
535
|
+
expect(h.watcher.getRegistry().has('cleanme')).toBe(true)
|
|
536
|
+
|
|
537
|
+
// Drain pending timeouts (simulates 30s elapsing).
|
|
538
|
+
const fired = h.fireScheduledCleanups()
|
|
539
|
+
expect(fired).toBeGreaterThan(0)
|
|
540
|
+
|
|
541
|
+
// Post-grace: registry entry gone, downstream consumers see no
|
|
542
|
+
// dangling FSWatcher.
|
|
543
|
+
expect(h.watcher.getRegistry().has('cleanme')).toBe(false)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it('cleans up historical-and-already-done agents after grace (MEM1)', () => {
|
|
547
|
+
// Historical files (pre-existing at boot, already done) used to
|
|
548
|
+
// keep their FSWatcher open forever — they bypass the
|
|
549
|
+
// maybySendStateTransition done branch because completionNotified
|
|
550
|
+
// is set to true in the registerAgent path. Cleanup must still
|
|
551
|
+
// schedule there.
|
|
552
|
+
const agentDir = join(tmpRoot, 'agent')
|
|
553
|
+
const subagentsDir = join(agentDir, '.claude', 'projects', 'p1', 'session-abc', 'subagents')
|
|
554
|
+
mkdirSync(subagentsDir, { recursive: true })
|
|
555
|
+
const jsonlPath = join(subagentsDir, 'agent-historical.jsonl')
|
|
556
|
+
// Already-done at boot: contains turn_end already.
|
|
557
|
+
writeFileSync(jsonlPath, buildJSONL(
|
|
558
|
+
subAgentUserMsg('From a prior session'),
|
|
559
|
+
subAgentTurnDuration(),
|
|
560
|
+
))
|
|
561
|
+
|
|
562
|
+
const h = startWatcherSync({ agentDir })
|
|
563
|
+
|
|
564
|
+
// Boot scan picks it up as historical-and-done; no completion
|
|
565
|
+
// notification fires (would be a spurious replay).
|
|
566
|
+
expect(h.notifications.filter((n) => n.includes('Worker done'))).toHaveLength(0)
|
|
567
|
+
expect(h.watcher.getRegistry().has('historical')).toBe(true)
|
|
568
|
+
|
|
569
|
+
// Cleanup is still scheduled (the FSWatcher would otherwise leak).
|
|
570
|
+
const fired = h.fireScheduledCleanups()
|
|
571
|
+
expect(fired).toBeGreaterThan(0)
|
|
572
|
+
expect(h.watcher.getRegistry().has('historical')).toBe(false)
|
|
573
|
+
})
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
it('emits stall notification after stallThresholdMs idle', () => {
|
|
577
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
578
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
579
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
580
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
581
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
582
|
+
const jsonlPath = `${subagentsDir}/agent-deadbeef.jsonl`
|
|
583
|
+
|
|
584
|
+
// Only the initial user message — no tool_use or turn_end
|
|
585
|
+
const content = buildJSONL(subAgentUserMsg('Run a long task'))
|
|
586
|
+
|
|
587
|
+
const h = makeHarness({
|
|
588
|
+
agentDir,
|
|
589
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
590
|
+
dirs: {
|
|
591
|
+
[projectsRoot]: ['myproject'],
|
|
592
|
+
[projectDir]: ['session-abc123'],
|
|
593
|
+
[subagentsDir]: ['agent-deadbeef.jsonl'],
|
|
594
|
+
},
|
|
595
|
+
files: { [jsonlPath]: content },
|
|
596
|
+
stallThresholdMs: 60_000,
|
|
597
|
+
rescanMs: 500,
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// Initial poll — registers the agent (as historical, since the file
|
|
601
|
+
// already exists at boot). Flip historical=false to simulate an entry
|
|
602
|
+
// that was discovered post-boot, which is the only case stalls fire.
|
|
603
|
+
h.poll()
|
|
604
|
+
const entry = h.watcher.getRegistry().get('deadbeef')
|
|
605
|
+
if (entry) entry.historical = false
|
|
606
|
+
|
|
607
|
+
// Advance past stall threshold without any new JSONL activity
|
|
608
|
+
h.advance(65_000)
|
|
609
|
+
|
|
610
|
+
const stallLogs = h.logs.filter((n) => n.includes('stall detected'))
|
|
611
|
+
expect(stallLogs.length).toBeGreaterThanOrEqual(1)
|
|
612
|
+
expect(stallLogs[0]).toContain('stall detected')
|
|
613
|
+
|
|
614
|
+
h.watcher.stop()
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
it('suppresses stall notifications for historical entries', () => {
|
|
618
|
+
// Historical entries (file existed at watcher boot) must NOT fire
|
|
619
|
+
// stall notifications. The sub-agent process is long dead; the file
|
|
620
|
+
// is just left over from a prior session. With many historicals
|
|
621
|
+
// present at restart, firing stalls for each would flood the chat.
|
|
622
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
623
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
624
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
625
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
626
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
627
|
+
const jsonlPath = `${subagentsDir}/agent-deadbeef.jsonl`
|
|
628
|
+
const content = buildJSONL(subAgentUserMsg('Old task'))
|
|
629
|
+
|
|
630
|
+
const h = makeHarness({
|
|
631
|
+
agentDir,
|
|
632
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
633
|
+
dirs: {
|
|
634
|
+
[projectsRoot]: ['myproject'],
|
|
635
|
+
[projectDir]: ['session-abc123'],
|
|
636
|
+
[subagentsDir]: ['agent-deadbeef.jsonl'],
|
|
637
|
+
},
|
|
638
|
+
files: { [jsonlPath]: content },
|
|
639
|
+
stallThresholdMs: 60_000,
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
h.poll()
|
|
643
|
+
h.advance(65_000) // past stall threshold
|
|
644
|
+
|
|
645
|
+
const stallLogs = h.logs.filter((n) => n.includes('stall detected'))
|
|
646
|
+
expect(stallLogs).toHaveLength(0)
|
|
647
|
+
|
|
648
|
+
h.watcher.stop()
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
it('does not emit stall notification twice', () => {
|
|
652
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
653
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
654
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
655
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
656
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
657
|
+
const jsonlPath = `${subagentsDir}/agent-deadbeef.jsonl`
|
|
658
|
+
|
|
659
|
+
const content = buildJSONL(subAgentUserMsg('Long task'))
|
|
660
|
+
|
|
661
|
+
const h = makeHarness({
|
|
662
|
+
agentDir,
|
|
663
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
664
|
+
dirs: {
|
|
665
|
+
[projectsRoot]: ['myproject'],
|
|
666
|
+
[projectDir]: ['session-abc123'],
|
|
667
|
+
[subagentsDir]: ['agent-deadbeef.jsonl'],
|
|
668
|
+
},
|
|
669
|
+
files: { [jsonlPath]: content },
|
|
670
|
+
stallThresholdMs: 60_000,
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
h.poll()
|
|
674
|
+
const entry = h.watcher.getRegistry().get('deadbeef')
|
|
675
|
+
if (entry) entry.historical = false
|
|
676
|
+
|
|
677
|
+
h.advance(65_000)
|
|
678
|
+
h.advance(65_000) // advance past threshold AGAIN
|
|
679
|
+
|
|
680
|
+
const stallLogs = h.logs.filter((n) => n.includes('stall detected'))
|
|
681
|
+
expect(stallLogs.length).toBe(1)
|
|
682
|
+
|
|
683
|
+
h.watcher.stop()
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
it('does not duplicate workers registered from same file', () => {
|
|
687
|
+
// File exists at startup → historical. Repeated polls should not
|
|
688
|
+
// re-register the agent or emit extra notifications.
|
|
689
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
690
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
691
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
692
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
693
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
694
|
+
const jsonlPath = `${subagentsDir}/agent-deadbeef.jsonl`
|
|
695
|
+
|
|
696
|
+
const content = buildJSONL(subAgentUserMsg('Do it'))
|
|
697
|
+
|
|
698
|
+
const h = makeHarness({
|
|
699
|
+
agentDir,
|
|
700
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
701
|
+
dirs: {
|
|
702
|
+
[projectsRoot]: ['myproject'],
|
|
703
|
+
[projectDir]: ['session-abc123'],
|
|
704
|
+
[subagentsDir]: ['agent-deadbeef.jsonl'],
|
|
705
|
+
},
|
|
706
|
+
files: { [jsonlPath]: content },
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
h.poll()
|
|
710
|
+
h.poll() // second poll — should not re-register
|
|
711
|
+
h.poll()
|
|
712
|
+
|
|
713
|
+
const registry = h.watcher.getRegistry()
|
|
714
|
+
expect(registry.size).toBe(1)
|
|
715
|
+
|
|
716
|
+
h.watcher.stop()
|
|
717
|
+
})
|
|
718
|
+
|
|
719
|
+
it('stop() cleans up and stops poll timers', () => {
|
|
720
|
+
const h = makeHarness({})
|
|
721
|
+
h.watcher.stop()
|
|
722
|
+
|
|
723
|
+
// After stop, advancing should not trigger anything new
|
|
724
|
+
const notifsBefore = h.notifications.length
|
|
725
|
+
h.advance(100_000)
|
|
726
|
+
expect(h.notifications.length).toBe(notifsBefore)
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
// ─── Historical-vs-active filter regression tests ────────────────────────
|
|
730
|
+
|
|
731
|
+
describe('historical-vs-active filter', () => {
|
|
732
|
+
/**
|
|
733
|
+
* Pre-existing JSONL files at watcher boot are tagged historical=true.
|
|
734
|
+
* Stalls and completion notifications are gated on !historical so a
|
|
735
|
+
* restart with months of session history doesn't flood the chat.
|
|
736
|
+
*/
|
|
737
|
+
|
|
738
|
+
it('pre-existing JSONL files at startup are tagged historical', () => {
|
|
739
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
740
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
741
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
742
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
743
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
744
|
+
const jsonlA = `${subagentsDir}/agent-hist-aaaa.jsonl`
|
|
745
|
+
const jsonlB = `${subagentsDir}/agent-hist-bbbb.jsonl`
|
|
746
|
+
|
|
747
|
+
const content = buildJSONL(subAgentUserMsg('Old task'))
|
|
748
|
+
|
|
749
|
+
const h = makeHarness({
|
|
750
|
+
agentDir,
|
|
751
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
752
|
+
dirs: {
|
|
753
|
+
[projectsRoot]: ['myproject'],
|
|
754
|
+
[projectDir]: ['session-abc123'],
|
|
755
|
+
[subagentsDir]: ['agent-hist-aaaa.jsonl', 'agent-hist-bbbb.jsonl'],
|
|
756
|
+
},
|
|
757
|
+
files: {
|
|
758
|
+
[jsonlA]: content,
|
|
759
|
+
[jsonlB]: content,
|
|
760
|
+
},
|
|
761
|
+
})
|
|
762
|
+
|
|
763
|
+
const registry = h.watcher.getRegistry()
|
|
764
|
+
expect(registry.size).toBe(2)
|
|
765
|
+
for (const entry of registry.values()) {
|
|
766
|
+
expect(entry.historical).toBe(true)
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
h.watcher.stop()
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('JSONL file created after startup is tagged non-historical', () => {
|
|
773
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
774
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
775
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
776
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
777
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
778
|
+
const newJsonl = `${subagentsDir}/agent-new-cccc.jsonl`
|
|
779
|
+
|
|
780
|
+
const content = buildJSONL(subAgentUserMsg('Fresh task'))
|
|
781
|
+
|
|
782
|
+
const h = makeHarness({
|
|
783
|
+
agentDir,
|
|
784
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
785
|
+
dirs: {
|
|
786
|
+
[projectsRoot]: ['myproject'],
|
|
787
|
+
[projectDir]: ['session-abc123'],
|
|
788
|
+
[subagentsDir]: [],
|
|
789
|
+
},
|
|
790
|
+
files: {},
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
h.mockFs.readdirSync = ((p: unknown) => {
|
|
794
|
+
if (String(p) === subagentsDir) return ['agent-new-cccc.jsonl']
|
|
795
|
+
if (String(p) === projectsRoot) return ['myproject']
|
|
796
|
+
if (String(p) === projectDir) return ['session-abc123']
|
|
797
|
+
return []
|
|
798
|
+
}) as unknown as typeof import('fs').readdirSync
|
|
799
|
+
h.mockFs.existsSync = ((p: unknown) => {
|
|
800
|
+
const ps = String(p)
|
|
801
|
+
return [projectsRoot, projectDir, sessionDir, subagentsDir, newJsonl].includes(ps)
|
|
802
|
+
}) as typeof import('fs').existsSync
|
|
803
|
+
h.mockFs.statSync = ((p: unknown) => {
|
|
804
|
+
const ps = String(p)
|
|
805
|
+
if (ps === newJsonl) return { size: Buffer.from(content, 'utf-8').length } as import('fs').Stats
|
|
806
|
+
return { size: 0 } as import('fs').Stats
|
|
807
|
+
}) as typeof import('fs').statSync
|
|
808
|
+
|
|
809
|
+
h.poll()
|
|
810
|
+
|
|
811
|
+
const entry = h.watcher.getRegistry().get('new-cccc')
|
|
812
|
+
expect(entry).toBeDefined()
|
|
813
|
+
expect(entry?.historical).toBe(false)
|
|
814
|
+
|
|
815
|
+
h.watcher.stop()
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
it('pre-existing in-flight agent that finishes after restart fires completion', () => {
|
|
819
|
+
// Historical at boot. Then writes turn_end. Completion notification
|
|
820
|
+
// still fires for the state transition (the file was in-flight at
|
|
821
|
+
// boot, so the transition is meaningful even if the entry is tagged
|
|
822
|
+
// historical for stall-suppression purposes).
|
|
823
|
+
const agentDir = '/home/user/.switchroom/agents/myagent'
|
|
824
|
+
const projectsRoot = `${agentDir}/.claude/projects`
|
|
825
|
+
const projectDir = `${projectsRoot}/myproject`
|
|
826
|
+
const sessionDir = `${projectDir}/session-abc123`
|
|
827
|
+
const subagentsDir = `${sessionDir}/subagents`
|
|
828
|
+
const jsonlPath = `${subagentsDir}/agent-inflight-dddd.jsonl`
|
|
829
|
+
|
|
830
|
+
const initialContent = buildJSONL(subAgentUserMsg('Important in-flight task'))
|
|
831
|
+
const initialBuf = Buffer.from(initialContent, 'utf-8')
|
|
832
|
+
|
|
833
|
+
let currentContent = initialBuf
|
|
834
|
+
|
|
835
|
+
const h = makeHarness({
|
|
836
|
+
agentDir,
|
|
837
|
+
existingDirs: [projectsRoot, projectDir, sessionDir, subagentsDir],
|
|
838
|
+
dirs: {
|
|
839
|
+
[projectsRoot]: ['myproject'],
|
|
840
|
+
[projectDir]: ['session-abc123'],
|
|
841
|
+
[subagentsDir]: ['agent-inflight-dddd.jsonl'],
|
|
842
|
+
},
|
|
843
|
+
files: { [jsonlPath]: initialContent },
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
const entry = h.watcher.getRegistry().get('inflight-dddd')
|
|
847
|
+
expect(entry).toBeDefined()
|
|
848
|
+
expect(entry?.state).toBe('running')
|
|
849
|
+
|
|
850
|
+
const finishedContent = initialContent + buildJSONL(subAgentTurnDuration())
|
|
851
|
+
currentContent = Buffer.from(finishedContent, 'utf-8')
|
|
852
|
+
h.mockFs.statSync = ((p: unknown) => {
|
|
853
|
+
if (String(p) === jsonlPath) return { size: currentContent.length } as import('fs').Stats
|
|
854
|
+
return { size: 0 } as import('fs').Stats
|
|
855
|
+
}) as typeof import('fs').statSync
|
|
856
|
+
h.mockFs.readSync = ((
|
|
857
|
+
_fd: number,
|
|
858
|
+
buf: NodeJS.ArrayBufferView,
|
|
859
|
+
offset: number,
|
|
860
|
+
length: number,
|
|
861
|
+
position: number | null,
|
|
862
|
+
): number => {
|
|
863
|
+
const pos = position ?? 0
|
|
864
|
+
const src = currentContent.slice(pos, pos + length)
|
|
865
|
+
Buffer.from(src).copy(buf as Buffer, offset)
|
|
866
|
+
return src.length
|
|
867
|
+
}) as unknown as typeof import('fs').readSync
|
|
868
|
+
|
|
869
|
+
h.poll()
|
|
870
|
+
|
|
871
|
+
const completionNotifs = h.notifications.filter((n) => n.includes('Worker done'))
|
|
872
|
+
expect(completionNotifs).toHaveLength(1)
|
|
873
|
+
|
|
874
|
+
h.watcher.stop()
|
|
875
|
+
})
|
|
876
|
+
})
|
|
877
|
+
})
|