dsh-plugin-t-expert 0.1.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 +215 -0
- package/THIRD-PARTY-NOTICES +33 -0
- package/cordis.patch.yml +7 -0
- package/data/experts/academic/academic-anthropologist.md +125 -0
- package/data/experts/academic/academic-geographer.md +127 -0
- package/data/experts/academic/academic-historian.md +123 -0
- package/data/experts/academic/academic-narratologist.md +118 -0
- package/data/experts/academic/academic-psychologist.md +118 -0
- package/data/experts/academic/academic-statistician.md +144 -0
- package/data/experts/design/design-brand-guardian.md +322 -0
- package/data/experts/design/design-image-prompt-engineer.md +236 -0
- package/data/experts/design/design-inclusive-visuals-specialist.md +71 -0
- package/data/experts/design/design-persona-walkthrough.md +272 -0
- package/data/experts/design/design-ui-designer.md +383 -0
- package/data/experts/design/design-ui-finish-gate-reviewer.md +217 -0
- package/data/experts/design/design-ux-architect.md +469 -0
- package/data/experts/design/design-ux-researcher.md +329 -0
- package/data/experts/design/design-visual-storyteller.md +149 -0
- package/data/experts/design/design-whimsy-injector.md +438 -0
- package/data/experts/engineering/engineering-ai-data-remediation-engineer.md +211 -0
- package/data/experts/engineering/engineering-ai-engineer.md +146 -0
- package/data/experts/engineering/engineering-api-platform-engineer.md +162 -0
- package/data/experts/engineering/engineering-ats-validator-architect.md +383 -0
- package/data/experts/engineering/engineering-autonomous-optimization-architect.md +107 -0
- package/data/experts/engineering/engineering-backend-architect.md +236 -0
- package/data/experts/engineering/engineering-china-network-engineer.md +252 -0
- package/data/experts/engineering/engineering-cms-developer.md +536 -0
- package/data/experts/engineering/engineering-code-reviewer.md +76 -0
- package/data/experts/engineering/engineering-codebase-onboarding-engineer.md +173 -0
- package/data/experts/engineering/engineering-data-engineer.md +306 -0
- package/data/experts/engineering/engineering-data-visualization-engineer.md +151 -0
- package/data/experts/engineering/engineering-database-optimizer.md +176 -0
- package/data/experts/engineering/engineering-database-reliability-engineer.md +162 -0
- package/data/experts/engineering/engineering-desktop-app-engineer.md +204 -0
- package/data/experts/engineering/engineering-developer-tooling-engineer.md +153 -0
- package/data/experts/engineering/engineering-devops-automator.md +376 -0
- package/data/experts/engineering/engineering-drupal-performance.md +347 -0
- package/data/experts/engineering/engineering-drupal-shopping-cart.md +360 -0
- package/data/experts/engineering/engineering-email-intelligence-engineer.md +353 -0
- package/data/experts/engineering/engineering-embedded-firmware-engineer.md +173 -0
- package/data/experts/engineering/engineering-feishu-integration-developer.md +598 -0
- package/data/experts/engineering/engineering-filament-optimization-specialist.md +283 -0
- package/data/experts/engineering/engineering-finops-engineer.md +153 -0
- package/data/experts/engineering/engineering-frontend-developer.md +225 -0
- package/data/experts/engineering/engineering-gaussdb-expert.md +334 -0
- package/data/experts/engineering/engineering-git-workflow-master.md +84 -0
- package/data/experts/engineering/engineering-i18n-engineer.md +184 -0
- package/data/experts/engineering/engineering-identity-access-engineer.md +196 -0
- package/data/experts/engineering/engineering-incident-response-commander.md +444 -0
- package/data/experts/engineering/engineering-iot-fleet-engineer.md +148 -0
- package/data/experts/engineering/engineering-it-service-manager.md +561 -0
- package/data/experts/engineering/engineering-knowledge-graph-engineer.md +368 -0
- package/data/experts/engineering/engineering-llm-post-training-engineer.md +166 -0
- package/data/experts/engineering/engineering-minimal-change-engineer.md +207 -0
- package/data/experts/engineering/engineering-mobile-app-builder.md +493 -0
- package/data/experts/engineering/engineering-mobile-release-engineer.md +163 -0
- package/data/experts/engineering/engineering-multi-agent-systems-architect.md +600 -0
- package/data/experts/engineering/engineering-network-engineer.md +239 -0
- package/data/experts/engineering/engineering-orgscript-engineer.md +113 -0
- package/data/experts/engineering/engineering-payments-billing-engineer.md +194 -0
- package/data/experts/engineering/engineering-pdf-engine-architect.md +666 -0
- package/data/experts/engineering/engineering-platform-engineer.md +270 -0
- package/data/experts/engineering/engineering-privacy-engineer.md +152 -0
- package/data/experts/engineering/engineering-prompt-engineer.md +202 -0
- package/data/experts/engineering/engineering-rag-pipeline-engineer.md +437 -0
- package/data/experts/engineering/engineering-rapid-prototyper.md +462 -0
- package/data/experts/engineering/engineering-realtime-collaboration-engineer.md +187 -0
- package/data/experts/engineering/engineering-rust-refactoring-specialist.md +313 -0
- package/data/experts/engineering/engineering-search-relevance-engineer.md +237 -0
- package/data/experts/engineering/engineering-section-508-specialist.md +339 -0
- package/data/experts/engineering/engineering-senior-developer.md +176 -0
- package/data/experts/engineering/engineering-software-architect.md +112 -0
- package/data/experts/engineering/engineering-solidity-smart-contract-engineer.md +522 -0
- package/data/experts/engineering/engineering-sre.md +90 -0
- package/data/experts/engineering/engineering-technical-writer.md +393 -0
- package/data/experts/engineering/engineering-universal-document-compiler.md +344 -0
- package/data/experts/engineering/engineering-uswds-developer.md +340 -0
- package/data/experts/engineering/engineering-video-streaming-engineer.md +150 -0
- package/data/experts/engineering/engineering-voice-ai-integration-engineer.md +561 -0
- package/data/experts/engineering/engineering-webassembly-engineer.md +156 -0
- package/data/experts/engineering/engineering-wechat-mini-program-developer.md +350 -0
- package/data/experts/engineering/engineering-wordpress-performance.md +346 -0
- package/data/experts/engineering/engineering-wordpress-shopping-cart.md +346 -0
- package/data/experts/finance/finance-bookkeeper-controller.md +260 -0
- package/data/experts/finance/finance-financial-analyst.md +234 -0
- package/data/experts/finance/finance-fpa-analyst.md +263 -0
- package/data/experts/finance/finance-investment-researcher.md +272 -0
- package/data/experts/finance/finance-tax-strategist.md +239 -0
- package/data/experts/game-development/blender/blender-addon-engineer.md +234 -0
- package/data/experts/game-development/economy-designer.md +156 -0
- package/data/experts/game-development/game-audio-engineer.md +264 -0
- package/data/experts/game-development/game-designer.md +167 -0
- package/data/experts/game-development/godot/godot-gameplay-scripter.md +334 -0
- package/data/experts/game-development/godot/godot-multiplayer-engineer.md +297 -0
- package/data/experts/game-development/godot/godot-shader-developer.md +266 -0
- package/data/experts/game-development/level-designer.md +208 -0
- package/data/experts/game-development/narrative-designer.md +243 -0
- package/data/experts/game-development/roblox-studio/roblox-avatar-creator.md +297 -0
- package/data/experts/game-development/roblox-studio/roblox-experience-designer.md +305 -0
- package/data/experts/game-development/roblox-studio/roblox-systems-scripter.md +325 -0
- package/data/experts/game-development/technical-artist.md +229 -0
- package/data/experts/game-development/unity/unity-architect.md +271 -0
- package/data/experts/game-development/unity/unity-editor-tool-developer.md +310 -0
- package/data/experts/game-development/unity/unity-multiplayer-engineer.md +321 -0
- package/data/experts/game-development/unity/unity-shader-graph-artist.md +269 -0
- package/data/experts/game-development/unreal-engine/unreal-multiplayer-architect.md +313 -0
- package/data/experts/game-development/unreal-engine/unreal-systems-engineer.md +310 -0
- package/data/experts/game-development/unreal-engine/unreal-technical-artist.md +256 -0
- package/data/experts/game-development/unreal-engine/unreal-world-builder.md +273 -0
- package/data/experts/gis/gis-3d-scene-developer.md +111 -0
- package/data/experts/gis/gis-analyst.md +91 -0
- package/data/experts/gis/gis-bim-specialist.md +108 -0
- package/data/experts/gis/gis-cartography-designer.md +150 -0
- package/data/experts/gis/gis-drone-reality-mapping.md +120 -0
- package/data/experts/gis/gis-geoai-ml-engineer.md +105 -0
- package/data/experts/gis/gis-geoprocessing-specialist.md +97 -0
- package/data/experts/gis/gis-qa-engineer.md +133 -0
- package/data/experts/gis/gis-solution-engineer.md +101 -0
- package/data/experts/gis/gis-spatial-data-engineer.md +97 -0
- package/data/experts/gis/gis-spatial-data-scientist.md +111 -0
- package/data/experts/gis/gis-technical-consultant.md +86 -0
- package/data/experts/gis/gis-web-gis-developer.md +108 -0
- package/data/experts/healthcare/healthcare-clinical-evidence-agent.md +231 -0
- package/data/experts/healthcare/healthcare-innovation-strategist.md +433 -0
- package/data/experts/healthcare/healthcare-sovereign-health-systems-agent.md +312 -0
- package/data/experts/marketing/marketing-aeo-foundations.md +264 -0
- package/data/experts/marketing/marketing-agentic-search-optimizer.md +313 -0
- package/data/experts/marketing/marketing-ai-citation-strategist.md +172 -0
- package/data/experts/marketing/marketing-app-store-optimizer.md +321 -0
- package/data/experts/marketing/marketing-baidu-seo-specialist.md +226 -0
- package/data/experts/marketing/marketing-bilibili-content-strategist.md +199 -0
- package/data/experts/marketing/marketing-book-co-author.md +110 -0
- package/data/experts/marketing/marketing-carousel-growth-engine.md +199 -0
- package/data/experts/marketing/marketing-china-ecommerce-operator.md +283 -0
- package/data/experts/marketing/marketing-china-market-localization-strategist.md +283 -0
- package/data/experts/marketing/marketing-content-creator.md +54 -0
- package/data/experts/marketing/marketing-cross-border-ecommerce.md +259 -0
- package/data/experts/marketing/marketing-douyin-strategist.md +149 -0
- package/data/experts/marketing/marketing-email-strategist.md +249 -0
- package/data/experts/marketing/marketing-global-podcast-strategist.md +206 -0
- package/data/experts/marketing/marketing-growth-hacker.md +54 -0
- package/data/experts/marketing/marketing-instagram-curator.md +113 -0
- package/data/experts/marketing/marketing-kuaishou-strategist.md +223 -0
- package/data/experts/marketing/marketing-linkedin-content-creator.md +214 -0
- package/data/experts/marketing/marketing-livestream-commerce-coach.md +305 -0
- package/data/experts/marketing/marketing-multi-platform-publisher.md +217 -0
- package/data/experts/marketing/marketing-podcast-strategist.md +277 -0
- package/data/experts/marketing/marketing-pr-communications-manager.md +473 -0
- package/data/experts/marketing/marketing-private-domain-operator.md +308 -0
- package/data/experts/marketing/marketing-reddit-community-builder.md +123 -0
- package/data/experts/marketing/marketing-seo-specialist.md +370 -0
- package/data/experts/marketing/marketing-short-video-editing-coach.md +412 -0
- package/data/experts/marketing/marketing-social-media-strategist.md +125 -0
- package/data/experts/marketing/marketing-tiktok-strategist.md +125 -0
- package/data/experts/marketing/marketing-twitter-engager.md +126 -0
- package/data/experts/marketing/marketing-video-optimization-specialist.md +119 -0
- package/data/experts/marketing/marketing-wechat-official-account.md +145 -0
- package/data/experts/marketing/marketing-weibo-strategist.md +240 -0
- package/data/experts/marketing/marketing-x-twitter-intelligence-analyst.md +161 -0
- package/data/experts/marketing/marketing-xiaohongshu-specialist.md +138 -0
- package/data/experts/marketing/marketing-zhihu-strategist.md +162 -0
- package/data/experts/paid-media/paid-media-auditor.md +71 -0
- package/data/experts/paid-media/paid-media-creative-strategist.md +71 -0
- package/data/experts/paid-media/paid-media-paid-social-strategist.md +71 -0
- package/data/experts/paid-media/paid-media-ppc-strategist.md +71 -0
- package/data/experts/paid-media/paid-media-programmatic-buyer.md +71 -0
- package/data/experts/paid-media/paid-media-search-query-analyst.md +71 -0
- package/data/experts/paid-media/paid-media-tracking-specialist.md +71 -0
- package/data/experts/product/product-behavioral-nudge-engine.md +80 -0
- package/data/experts/product/product-feedback-synthesizer.md +119 -0
- package/data/experts/product/product-manager.md +469 -0
- package/data/experts/product/product-sprint-prioritizer.md +154 -0
- package/data/experts/product/product-trend-researcher.md +159 -0
- package/data/experts/project-management/project-management-experiment-tracker.md +198 -0
- package/data/experts/project-management/project-management-jira-workflow-steward.md +230 -0
- package/data/experts/project-management/project-management-meeting-notes-specialist.md +95 -0
- package/data/experts/project-management/project-management-project-shepherd.md +194 -0
- package/data/experts/project-management/project-management-studio-operations.md +200 -0
- package/data/experts/project-management/project-management-studio-producer.md +203 -0
- package/data/experts/project-management/project-manager-senior.md +135 -0
- package/data/experts/research/research-synthesist.md +136 -0
- package/data/experts/sales/sales-account-strategist.md +227 -0
- package/data/experts/sales/sales-coach.md +271 -0
- package/data/experts/sales/sales-deal-strategist.md +180 -0
- package/data/experts/sales/sales-discovery-coach.md +225 -0
- package/data/experts/sales/sales-engineer.md +182 -0
- package/data/experts/sales/sales-offer-lead-gen-strategist.md +257 -0
- package/data/experts/sales/sales-outbound-strategist.md +201 -0
- package/data/experts/sales/sales-pipeline-analyst.md +267 -0
- package/data/experts/sales/sales-proposal-strategist.md +217 -0
- package/data/experts/security/security-ai-generated-code-auditor.md +207 -0
- package/data/experts/security/security-appsec-engineer.md +491 -0
- package/data/experts/security/security-architect.md +304 -0
- package/data/experts/security/security-blockchain-security-auditor.md +463 -0
- package/data/experts/security/security-cloud-security-architect.md +523 -0
- package/data/experts/security/security-compliance-auditor.md +158 -0
- package/data/experts/security/security-incident-responder.md +437 -0
- package/data/experts/security/security-penetration-tester.md +399 -0
- package/data/experts/security/security-secrets-credential-engineer.md +176 -0
- package/data/experts/security/security-senior-secops.md +750 -0
- package/data/experts/security/security-threat-detection-engineer.md +534 -0
- package/data/experts/security/security-threat-intelligence-analyst.md +644 -0
- package/data/experts/spatial-computing/macos-spatial-metal-engineer.md +337 -0
- package/data/experts/spatial-computing/terminal-integration-specialist.md +70 -0
- package/data/experts/spatial-computing/visionos-spatial-engineer.md +54 -0
- package/data/experts/spatial-computing/xr-cockpit-interaction-specialist.md +32 -0
- package/data/experts/spatial-computing/xr-immersive-developer.md +32 -0
- package/data/experts/spatial-computing/xr-interface-architect.md +32 -0
- package/data/experts/specialized/accounts-payable-agent.md +185 -0
- package/data/experts/specialized/agentic-identity-trust.md +387 -0
- package/data/experts/specialized/agents-orchestrator.md +367 -0
- package/data/experts/specialized/automation-governance-architect.md +216 -0
- package/data/experts/specialized/business-strategist.md +488 -0
- package/data/experts/specialized/change-management-consultant.md +497 -0
- package/data/experts/specialized/chief-financial-officer.md +388 -0
- package/data/experts/specialized/corporate-training-designer.md +192 -0
- package/data/experts/specialized/customer-service.md +398 -0
- package/data/experts/specialized/customer-success-manager.md +460 -0
- package/data/experts/specialized/data-consolidation-agent.md +60 -0
- package/data/experts/specialized/data-privacy-officer.md +412 -0
- package/data/experts/specialized/esg-sustainability-officer.md +396 -0
- package/data/experts/specialized/government-digital-presales-consultant.md +363 -0
- package/data/experts/specialized/grant-writer.md +511 -0
- package/data/experts/specialized/healthcare-aging-parent-care-companion.md +414 -0
- package/data/experts/specialized/healthcare-customer-service.md +389 -0
- package/data/experts/specialized/healthcare-marketing-compliance.md +395 -0
- package/data/experts/specialized/hospitality-guest-services.md +603 -0
- package/data/experts/specialized/hr-onboarding.md +451 -0
- package/data/experts/specialized/identity-graph-operator.md +260 -0
- package/data/experts/specialized/language-translator.md +264 -0
- package/data/experts/specialized/legal-billing-time-tracking.md +569 -0
- package/data/experts/specialized/legal-client-intake.md +492 -0
- package/data/experts/specialized/legal-document-review.md +454 -0
- package/data/experts/specialized/loan-officer-assistant.md +555 -0
- package/data/experts/specialized/lsp-index-engineer.md +314 -0
- package/data/experts/specialized/ma-integration-manager.md +427 -0
- package/data/experts/specialized/medical-billing-coding-specialist.md +491 -0
- package/data/experts/specialized/operations-manager.md +399 -0
- package/data/experts/specialized/organizational-psychologist.md +391 -0
- package/data/experts/specialized/personal-growth-mentor.md +159 -0
- package/data/experts/specialized/real-estate-buyer-seller.md +596 -0
- package/data/experts/specialized/recruitment-specialist.md +509 -0
- package/data/experts/specialized/report-distribution-agent.md +65 -0
- package/data/experts/specialized/resume-tailor.md +230 -0
- package/data/experts/specialized/retail-customer-returns.md +566 -0
- package/data/experts/specialized/sales-data-extraction-agent.md +67 -0
- package/data/experts/specialized/sales-outreach.md +425 -0
- package/data/experts/specialized/specialized-chief-of-staff.md +279 -0
- package/data/experts/specialized/specialized-civil-engineer.md +356 -0
- package/data/experts/specialized/specialized-codebase-archaeologist.md +341 -0
- package/data/experts/specialized/specialized-cultural-intelligence-strategist.md +88 -0
- package/data/experts/specialized/specialized-developer-advocate.md +317 -0
- package/data/experts/specialized/specialized-document-generator.md +55 -0
- package/data/experts/specialized/specialized-fedramp-rmf-compliance.md +378 -0
- package/data/experts/specialized/specialized-focus-music-architect.md +172 -0
- package/data/experts/specialized/specialized-french-consulting-market.md +194 -0
- package/data/experts/specialized/specialized-korean-business-navigator.md +216 -0
- package/data/experts/specialized/specialized-master-plan-architect.md +157 -0
- package/data/experts/specialized/specialized-mcp-builder.md +248 -0
- package/data/experts/specialized/specialized-model-qa.md +488 -0
- package/data/experts/specialized/specialized-pricing-analyst.md +243 -0
- package/data/experts/specialized/specialized-salesforce-architect.md +182 -0
- package/data/experts/specialized/specialized-strategy-duel-agent.md +130 -0
- package/data/experts/specialized/specialized-workflow-architect.md +597 -0
- package/data/experts/specialized/study-abroad-advisor.md +282 -0
- package/data/experts/specialized/supply-chain-strategist.md +582 -0
- package/data/experts/specialized/zk-steward.md +211 -0
- package/data/experts/support/support-analytics-reporter.md +365 -0
- package/data/experts/support/support-executive-summary-generator.md +212 -0
- package/data/experts/support/support-finance-tracker.md +442 -0
- package/data/experts/support/support-infrastructure-maintainer.md +618 -0
- package/data/experts/support/support-legal-compliance-checker.md +588 -0
- package/data/experts/support/support-support-responder.md +585 -0
- package/data/experts/testing/testing-accessibility-auditor.md +316 -0
- package/data/experts/testing/testing-api-tester.md +306 -0
- package/data/experts/testing/testing-evidence-collector.md +210 -0
- package/data/experts/testing/testing-performance-benchmarker.md +268 -0
- package/data/experts/testing/testing-reality-checker.md +249 -0
- package/data/experts/testing/testing-test-automation-engineer.md +179 -0
- package/data/experts/testing/testing-test-results-analyzer.md +305 -0
- package/data/experts/testing/testing-tool-evaluator.md +394 -0
- package/data/experts/testing/testing-workflow-optimizer.md +450 -0
- package/data/source.json +27 -0
- package/data/t-team.config.json +998 -0
- package/data/team-profiles.py +365 -0
- package/data/teams.json +581 -0
- package/data/teams.resolved.json +1121 -0
- package/data/zh/COVERAGE.json +20 -0
- package/data/zh/academic/academic-anthropologist.md +124 -0
- package/data/zh/academic/academic-geographer.md +126 -0
- package/data/zh/academic/academic-historian.md +122 -0
- package/data/zh/academic/academic-narratologist.md +117 -0
- package/data/zh/academic/academic-psychologist.md +117 -0
- package/data/zh/academic/academic-statistician.md +40 -0
- package/data/zh/descriptions.json +281 -0
- package/data/zh/design/design-brand-guardian.md +321 -0
- package/data/zh/design/design-image-prompt-engineer.md +255 -0
- package/data/zh/design/design-inclusive-visuals-specialist.md +177 -0
- package/data/zh/design/design-persona-walkthrough.md +271 -0
- package/data/zh/design/design-ui-designer.md +382 -0
- package/data/zh/design/design-ui-finish-gate-reviewer.md +40 -0
- package/data/zh/design/design-ux-architect.md +482 -0
- package/data/zh/design/design-ux-researcher.md +328 -0
- package/data/zh/design/design-visual-storyteller.md +159 -0
- package/data/zh/design/design-whimsy-injector.md +453 -0
- package/data/zh/divisions.json +74 -0
- package/data/zh/engineering/engineering-ai-data-remediation-engineer.md +209 -0
- package/data/zh/engineering/engineering-ai-engineer.md +161 -0
- package/data/zh/engineering/engineering-api-platform-engineer.md +40 -0
- package/data/zh/engineering/engineering-ats-validator-architect.md +383 -0
- package/data/zh/engineering/engineering-autonomous-optimization-architect.md +115 -0
- package/data/zh/engineering/engineering-backend-architect.md +234 -0
- package/data/zh/engineering/engineering-china-network-engineer.md +252 -0
- package/data/zh/engineering/engineering-cms-developer.md +534 -0
- package/data/zh/engineering/engineering-code-reviewer.md +172 -0
- package/data/zh/engineering/engineering-codebase-onboarding-engineer.md +172 -0
- package/data/zh/engineering/engineering-data-engineer.md +324 -0
- package/data/zh/engineering/engineering-data-visualization-engineer.md +40 -0
- package/data/zh/engineering/engineering-database-optimizer.md +175 -0
- package/data/zh/engineering/engineering-database-reliability-engineer.md +40 -0
- package/data/zh/engineering/engineering-desktop-app-engineer.md +40 -0
- package/data/zh/engineering/engineering-developer-tooling-engineer.md +40 -0
- package/data/zh/engineering/engineering-devops-automator.md +375 -0
- package/data/zh/engineering/engineering-drupal-performance.md +40 -0
- package/data/zh/engineering/engineering-drupal-shopping-cart.md +359 -0
- package/data/zh/engineering/engineering-email-intelligence-engineer.md +349 -0
- package/data/zh/engineering/engineering-embedded-firmware-engineer.md +168 -0
- package/data/zh/engineering/engineering-feishu-integration-developer.md +597 -0
- package/data/zh/engineering/engineering-filament-optimization-specialist.md +283 -0
- package/data/zh/engineering/engineering-finops-engineer.md +40 -0
- package/data/zh/engineering/engineering-frontend-developer.md +224 -0
- package/data/zh/engineering/engineering-gaussdb-expert.md +40 -0
- package/data/zh/engineering/engineering-git-workflow-master.md +220 -0
- package/data/zh/engineering/engineering-i18n-engineer.md +40 -0
- package/data/zh/engineering/engineering-identity-access-engineer.md +40 -0
- package/data/zh/engineering/engineering-incident-response-commander.md +465 -0
- package/data/zh/engineering/engineering-iot-fleet-engineer.md +40 -0
- package/data/zh/engineering/engineering-it-service-manager.md +560 -0
- package/data/zh/engineering/engineering-knowledge-graph-engineer.md +40 -0
- package/data/zh/engineering/engineering-llm-post-training-engineer.md +40 -0
- package/data/zh/engineering/engineering-minimal-change-engineer.md +206 -0
- package/data/zh/engineering/engineering-mobile-app-builder.md +434 -0
- package/data/zh/engineering/engineering-mobile-release-engineer.md +40 -0
- package/data/zh/engineering/engineering-multi-agent-systems-architect.md +599 -0
- package/data/zh/engineering/engineering-network-engineer.md +40 -0
- package/data/zh/engineering/engineering-orgscript-engineer.md +112 -0
- package/data/zh/engineering/engineering-payments-billing-engineer.md +40 -0
- package/data/zh/engineering/engineering-pdf-engine-architect.md +666 -0
- package/data/zh/engineering/engineering-platform-engineer.md +271 -0
- package/data/zh/engineering/engineering-privacy-engineer.md +40 -0
- package/data/zh/engineering/engineering-prompt-engineer.md +203 -0
- package/data/zh/engineering/engineering-rag-pipeline-engineer.md +40 -0
- package/data/zh/engineering/engineering-rapid-prototyper.md +461 -0
- package/data/zh/engineering/engineering-realtime-collaboration-engineer.md +40 -0
- package/data/zh/engineering/engineering-rust-refactoring-specialist.md +40 -0
- package/data/zh/engineering/engineering-search-relevance-engineer.md +40 -0
- package/data/zh/engineering/engineering-section-508-specialist.md +40 -0
- package/data/zh/engineering/engineering-senior-developer.md +177 -0
- package/data/zh/engineering/engineering-software-architect.md +200 -0
- package/data/zh/engineering/engineering-solidity-smart-contract-engineer.md +541 -0
- package/data/zh/engineering/engineering-sre.md +233 -0
- package/data/zh/engineering/engineering-technical-writer.md +409 -0
- package/data/zh/engineering/engineering-universal-document-compiler.md +344 -0
- package/data/zh/engineering/engineering-uswds-developer.md +40 -0
- package/data/zh/engineering/engineering-video-streaming-engineer.md +40 -0
- package/data/zh/engineering/engineering-voice-ai-integration-engineer.md +560 -0
- package/data/zh/engineering/engineering-webassembly-engineer.md +40 -0
- package/data/zh/engineering/engineering-wechat-mini-program-developer.md +288 -0
- package/data/zh/engineering/engineering-wordpress-performance.md +40 -0
- package/data/zh/engineering/engineering-wordpress-shopping-cart.md +345 -0
- package/data/zh/finance/finance-bookkeeper-controller.md +271 -0
- package/data/zh/finance/finance-financial-analyst.md +244 -0
- package/data/zh/finance/finance-fpa-analyst.md +272 -0
- package/data/zh/finance/finance-investment-researcher.md +283 -0
- package/data/zh/finance/finance-tax-strategist.md +250 -0
- package/data/zh/game-development/blender/blender-addon-engineer.md +233 -0
- package/data/zh/game-development/economy-designer.md +40 -0
- package/data/zh/game-development/game-audio-engineer.md +265 -0
- package/data/zh/game-development/game-designer.md +168 -0
- package/data/zh/game-development/godot/godot-gameplay-scripter.md +335 -0
- package/data/zh/game-development/godot/godot-multiplayer-engineer.md +296 -0
- package/data/zh/game-development/godot/godot-shader-developer.md +267 -0
- package/data/zh/game-development/level-designer.md +209 -0
- package/data/zh/game-development/narrative-designer.md +244 -0
- package/data/zh/game-development/roblox-studio/roblox-avatar-creator.md +298 -0
- package/data/zh/game-development/roblox-studio/roblox-experience-designer.md +306 -0
- package/data/zh/game-development/roblox-studio/roblox-systems-scripter.md +325 -0
- package/data/zh/game-development/technical-artist.md +230 -0
- package/data/zh/game-development/unity/unity-architect.md +272 -0
- package/data/zh/game-development/unity/unity-editor-tool-developer.md +300 -0
- package/data/zh/game-development/unity/unity-multiplayer-engineer.md +238 -0
- package/data/zh/game-development/unity/unity-shader-graph-artist.md +270 -0
- package/data/zh/game-development/unreal-engine/unreal-multiplayer-architect.md +314 -0
- package/data/zh/game-development/unreal-engine/unreal-systems-engineer.md +311 -0
- package/data/zh/game-development/unreal-engine/unreal-technical-artist.md +256 -0
- package/data/zh/game-development/unreal-engine/unreal-world-builder.md +274 -0
- package/data/zh/gis/gis-3d-scene-developer.md +110 -0
- package/data/zh/gis/gis-analyst.md +90 -0
- package/data/zh/gis/gis-bim-specialist.md +107 -0
- package/data/zh/gis/gis-cartography-designer.md +149 -0
- package/data/zh/gis/gis-drone-reality-mapping.md +119 -0
- package/data/zh/gis/gis-geoai-ml-engineer.md +104 -0
- package/data/zh/gis/gis-geoprocessing-specialist.md +96 -0
- package/data/zh/gis/gis-qa-engineer.md +132 -0
- package/data/zh/gis/gis-solution-engineer.md +100 -0
- package/data/zh/gis/gis-spatial-data-engineer.md +96 -0
- package/data/zh/gis/gis-spatial-data-scientist.md +110 -0
- package/data/zh/gis/gis-technical-consultant.md +85 -0
- package/data/zh/gis/gis-web-gis-developer.md +107 -0
- package/data/zh/healthcare/healthcare-clinical-evidence-agent.md +40 -0
- package/data/zh/healthcare/healthcare-innovation-strategist.md +40 -0
- package/data/zh/healthcare/healthcare-sovereign-health-systems-agent.md +40 -0
- package/data/zh/manual-bodies/engineering/engineering-ats-validator-architect.md +383 -0
- package/data/zh/manual-bodies/engineering/engineering-china-network-engineer.md +252 -0
- package/data/zh/manual-bodies/engineering/engineering-pdf-engine-architect.md +666 -0
- package/data/zh/manual-bodies/engineering/engineering-platform-engineer.md +271 -0
- package/data/zh/manual-bodies/engineering/engineering-universal-document-compiler.md +344 -0
- package/data/zh/manual-bodies/specialized/specialized-focus-music-architect.md +172 -0
- package/data/zh/manual.json +22 -0
- package/data/zh/marketing/marketing-aeo-foundations.md +263 -0
- package/data/zh/marketing/marketing-agentic-search-optimizer.md +312 -0
- package/data/zh/marketing/marketing-ai-citation-strategist.md +169 -0
- package/data/zh/marketing/marketing-app-store-optimizer.md +319 -0
- package/data/zh/marketing/marketing-baidu-seo-specialist.md +220 -0
- package/data/zh/marketing/marketing-bilibili-content-strategist.md +40 -0
- package/data/zh/marketing/marketing-book-co-author.md +109 -0
- package/data/zh/marketing/marketing-carousel-growth-engine.md +215 -0
- package/data/zh/marketing/marketing-china-ecommerce-operator.md +277 -0
- package/data/zh/marketing/marketing-china-market-localization-strategist.md +282 -0
- package/data/zh/marketing/marketing-content-creator.md +145 -0
- package/data/zh/marketing/marketing-cross-border-ecommerce.md +260 -0
- package/data/zh/marketing/marketing-douyin-strategist.md +150 -0
- package/data/zh/marketing/marketing-email-strategist.md +248 -0
- package/data/zh/marketing/marketing-global-podcast-strategist.md +204 -0
- package/data/zh/marketing/marketing-growth-hacker.md +121 -0
- package/data/zh/marketing/marketing-instagram-curator.md +179 -0
- package/data/zh/marketing/marketing-kuaishou-strategist.md +182 -0
- package/data/zh/marketing/marketing-linkedin-content-creator.md +232 -0
- package/data/zh/marketing/marketing-livestream-commerce-coach.md +303 -0
- package/data/zh/marketing/marketing-multi-platform-publisher.md +216 -0
- package/data/zh/marketing/marketing-podcast-strategist.md +278 -0
- package/data/zh/marketing/marketing-pr-communications-manager.md +472 -0
- package/data/zh/marketing/marketing-private-domain-operator.md +309 -0
- package/data/zh/marketing/marketing-reddit-community-builder.md +127 -0
- package/data/zh/marketing/marketing-seo-specialist.md +298 -0
- package/data/zh/marketing/marketing-short-video-editing-coach.md +413 -0
- package/data/zh/marketing/marketing-social-media-strategist.md +118 -0
- package/data/zh/marketing/marketing-tiktok-strategist.md +124 -0
- package/data/zh/marketing/marketing-twitter-engager.md +132 -0
- package/data/zh/marketing/marketing-video-optimization-specialist.md +128 -0
- package/data/zh/marketing/marketing-wechat-official-account.md +158 -0
- package/data/zh/marketing/marketing-weibo-strategist.md +241 -0
- package/data/zh/marketing/marketing-x-twitter-intelligence-analyst.md +160 -0
- package/data/zh/marketing/marketing-xiaohongshu-specialist.md +151 -0
- package/data/zh/marketing/marketing-zhihu-strategist.md +175 -0
- package/data/zh/names.json +281 -0
- package/data/zh/paid-media/paid-media-auditor.md +170 -0
- package/data/zh/paid-media/paid-media-creative-strategist.md +173 -0
- package/data/zh/paid-media/paid-media-paid-social-strategist.md +180 -0
- package/data/zh/paid-media/paid-media-ppc-strategist.md +180 -0
- package/data/zh/paid-media/paid-media-programmatic-buyer.md +177 -0
- package/data/zh/paid-media/paid-media-search-query-analyst.md +182 -0
- package/data/zh/paid-media/paid-media-tracking-specialist.md +199 -0
- package/data/zh/product/product-behavioral-nudge-engine.md +246 -0
- package/data/zh/product/product-feedback-synthesizer.md +175 -0
- package/data/zh/product/product-manager.md +474 -0
- package/data/zh/product/product-sprint-prioritizer.md +133 -0
- package/data/zh/product/product-trend-researcher.md +143 -0
- package/data/zh/project-management/project-management-experiment-tracker.md +206 -0
- package/data/zh/project-management/project-management-jira-workflow-steward.md +249 -0
- package/data/zh/project-management/project-management-meeting-notes-specialist.md +94 -0
- package/data/zh/project-management/project-management-project-shepherd.md +202 -0
- package/data/zh/project-management/project-management-studio-operations.md +208 -0
- package/data/zh/project-management/project-management-studio-producer.md +211 -0
- package/data/zh/project-management/project-manager-senior.md +135 -0
- package/data/zh/research/research-synthesist.md +40 -0
- package/data/zh/sales/sales-account-strategist.md +243 -0
- package/data/zh/sales/sales-coach.md +291 -0
- package/data/zh/sales/sales-deal-strategist.md +204 -0
- package/data/zh/sales/sales-discovery-coach.md +230 -0
- package/data/zh/sales/sales-engineer.md +200 -0
- package/data/zh/sales/sales-offer-lead-gen-strategist.md +256 -0
- package/data/zh/sales/sales-outbound-strategist.md +208 -0
- package/data/zh/sales/sales-pipeline-analyst.md +284 -0
- package/data/zh/sales/sales-proposal-strategist.md +233 -0
- package/data/zh/security/security-ai-generated-code-auditor.md +40 -0
- package/data/zh/security/security-appsec-engineer.md +490 -0
- package/data/zh/security/security-architect.md +303 -0
- package/data/zh/security/security-blockchain-security-auditor.md +462 -0
- package/data/zh/security/security-cloud-security-architect.md +522 -0
- package/data/zh/security/security-compliance-auditor.md +157 -0
- package/data/zh/security/security-incident-responder.md +436 -0
- package/data/zh/security/security-penetration-tester.md +398 -0
- package/data/zh/security/security-secrets-credential-engineer.md +40 -0
- package/data/zh/security/security-senior-secops.md +751 -0
- package/data/zh/security/security-threat-detection-engineer.md +533 -0
- package/data/zh/security/security-threat-intelligence-analyst.md +643 -0
- package/data/zh/spatial-computing/macos-spatial-metal-engineer.md +337 -0
- package/data/zh/spatial-computing/terminal-integration-specialist.md +236 -0
- package/data/zh/spatial-computing/visionos-spatial-engineer.md +282 -0
- package/data/zh/spatial-computing/xr-cockpit-interaction-specialist.md +220 -0
- package/data/zh/spatial-computing/xr-immersive-developer.md +229 -0
- package/data/zh/spatial-computing/xr-interface-architect.md +253 -0
- package/data/zh/specialized/accounts-payable-agent.md +212 -0
- package/data/zh/specialized/agentic-identity-trust.md +388 -0
- package/data/zh/specialized/agents-orchestrator.md +366 -0
- package/data/zh/specialized/automation-governance-architect.md +215 -0
- package/data/zh/specialized/business-strategist.md +487 -0
- package/data/zh/specialized/change-management-consultant.md +495 -0
- package/data/zh/specialized/chief-financial-officer.md +390 -0
- package/data/zh/specialized/corporate-training-designer.md +191 -0
- package/data/zh/specialized/customer-service.md +40 -0
- package/data/zh/specialized/customer-success-manager.md +458 -0
- package/data/zh/specialized/data-consolidation-agent.md +327 -0
- package/data/zh/specialized/data-privacy-officer.md +411 -0
- package/data/zh/specialized/esg-sustainability-officer.md +395 -0
- package/data/zh/specialized/government-digital-presales-consultant.md +362 -0
- package/data/zh/specialized/grant-writer.md +510 -0
- package/data/zh/specialized/healthcare-aging-parent-care-companion.md +40 -0
- package/data/zh/specialized/healthcare-customer-service.md +388 -0
- package/data/zh/specialized/healthcare-marketing-compliance.md +394 -0
- package/data/zh/specialized/hospitality-guest-services.md +597 -0
- package/data/zh/specialized/hr-onboarding.md +450 -0
- package/data/zh/specialized/identity-graph-operator.md +270 -0
- package/data/zh/specialized/language-translator.md +275 -0
- package/data/zh/specialized/legal-billing-time-tracking.md +566 -0
- package/data/zh/specialized/legal-client-intake.md +487 -0
- package/data/zh/specialized/legal-document-review.md +452 -0
- package/data/zh/specialized/loan-officer-assistant.md +549 -0
- package/data/zh/specialized/lsp-index-engineer.md +334 -0
- package/data/zh/specialized/ma-integration-manager.md +426 -0
- package/data/zh/specialized/medical-billing-coding-specialist.md +489 -0
- package/data/zh/specialized/operations-manager.md +398 -0
- package/data/zh/specialized/organizational-psychologist.md +390 -0
- package/data/zh/specialized/personal-growth-mentor.md +158 -0
- package/data/zh/specialized/real-estate-buyer-seller.md +594 -0
- package/data/zh/specialized/recruitment-specialist.md +49 -0
- package/data/zh/specialized/report-distribution-agent.md +354 -0
- package/data/zh/specialized/resume-tailor.md +40 -0
- package/data/zh/specialized/retail-customer-returns.md +564 -0
- package/data/zh/specialized/sales-data-extraction-agent.md +159 -0
- package/data/zh/specialized/sales-outreach.md +40 -0
- package/data/zh/specialized/specialized-chief-of-staff.md +40 -0
- package/data/zh/specialized/specialized-civil-engineer.md +355 -0
- package/data/zh/specialized/specialized-codebase-archaeologist.md +40 -0
- package/data/zh/specialized/specialized-cultural-intelligence-strategist.md +168 -0
- package/data/zh/specialized/specialized-developer-advocate.md +334 -0
- package/data/zh/specialized/specialized-document-generator.md +346 -0
- package/data/zh/specialized/specialized-fedramp-rmf-compliance.md +40 -0
- package/data/zh/specialized/specialized-focus-music-architect.md +172 -0
- package/data/zh/specialized/specialized-french-consulting-market.md +46 -0
- package/data/zh/specialized/specialized-korean-business-navigator.md +46 -0
- package/data/zh/specialized/specialized-master-plan-architect.md +40 -0
- package/data/zh/specialized/specialized-mcp-builder.md +351 -0
- package/data/zh/specialized/specialized-model-qa.md +507 -0
- package/data/zh/specialized/specialized-pricing-analyst.md +242 -0
- package/data/zh/specialized/specialized-salesforce-architect.md +179 -0
- package/data/zh/specialized/specialized-strategy-duel-agent.md +129 -0
- package/data/zh/specialized/specialized-workflow-architect.md +596 -0
- package/data/zh/specialized/study-abroad-advisor.md +281 -0
- package/data/zh/specialized/supply-chain-strategist.md +581 -0
- package/data/zh/specialized/zk-steward.md +228 -0
- package/data/zh/support/support-analytics-reporter.md +364 -0
- package/data/zh/support/support-executive-summary-generator.md +217 -0
- package/data/zh/support/support-finance-tracker.md +447 -0
- package/data/zh/support/support-infrastructure-maintainer.md +623 -0
- package/data/zh/support/support-legal-compliance-checker.md +587 -0
- package/data/zh/support/support-support-responder.md +584 -0
- package/data/zh/testing/testing-accessibility-auditor.md +329 -0
- package/data/zh/testing/testing-api-tester.md +305 -0
- package/data/zh/testing/testing-evidence-collector.md +153 -0
- package/data/zh/testing/testing-performance-benchmarker.md +196 -0
- package/data/zh/testing/testing-reality-checker.md +235 -0
- package/data/zh/testing/testing-test-automation-engineer.md +40 -0
- package/data/zh/testing/testing-test-results-analyzer.md +313 -0
- package/data/zh/testing/testing-tool-evaluator.md +402 -0
- package/data/zh/testing/testing-workflow-optimizer.md +458 -0
- package/lib/assets/t-team/action-celebrating-v2.png +0 -0
- package/lib/assets/t-team/action-reporting-v2.png +0 -0
- package/lib/assets/t-team/action-sending-v2.png +0 -0
- package/lib/assets/t-team/action-sleeping-v2.png +0 -0
- package/lib/assets/t-team/action-thinking-v2.png +0 -0
- package/lib/assets/t-team/action-working-v2.png +0 -0
- package/lib/assets/t-team/member-data-v2.png +0 -0
- package/lib/assets/t-team/member-designer-v2.png +0 -0
- package/lib/assets/t-team/member-docs-v2.png +0 -0
- package/lib/assets/t-team/member-engineer-v2.png +0 -0
- package/lib/assets/t-team/member-operator-v2.png +0 -0
- package/lib/assets/t-team/member-qa-v2.png +0 -0
- package/lib/assets/t-team/member-researcher-v2.png +0 -0
- package/lib/assets/t-team/member-security-v2.png +0 -0
- package/lib/assets/t-team/team-lead-v2.png +0 -0
- package/lib/bootstrap.js +85 -0
- package/lib/catalog.js +376 -0
- package/lib/client.js +21034 -0
- package/lib/command.js +228 -0
- package/lib/i18n.js +136 -0
- package/lib/index.js +686 -0
- package/lib/plan-check.js +171 -0
- package/lib/remote.js +353 -0
- package/lib/squads.js +231 -0
- package/lib/teams/capabilities.js +106 -0
- package/lib/teams/command.js +143 -0
- package/lib/teams/event-types.js +12 -0
- package/lib/teams/events.js +60 -0
- package/lib/teams/harness-compat.js +179 -0
- package/lib/teams/index.js +441 -0
- package/lib/teams/members.js +602 -0
- package/lib/teams/profiles.js +572 -0
- package/lib/teams/quality-gates.js +822 -0
- package/lib/teams/scheduler.js +434 -0
- package/lib/teams/snapshot.js +181 -0
- package/lib/teams/state.js +925 -0
- package/lib/teams/tool-names.js +11 -0
- package/lib/teams/tools.js +2279 -0
- package/lib/teams/types.js +22 -0
- package/lib/teams/web-routes.js +86 -0
- package/package.json +191 -0
|
@@ -0,0 +1,2279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `t_team_*` model-facing tools.
|
|
3
|
+
*
|
|
4
|
+
* The captain (the agent that created the team) orchestrates: members are
|
|
5
|
+
* continuable subagents it spawns and wakes. Members share the same tools and
|
|
6
|
+
* drive their own task state, mirroring the Claude Code T Team flow:
|
|
7
|
+
* create team → add members → create tasks with dependencies → claim/assign →
|
|
8
|
+
* work → report → status → delete.
|
|
9
|
+
* @module t-team/tools
|
|
10
|
+
*/
|
|
11
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { appendTeamEvent, captainSessionOf } from "./events.js";
|
|
15
|
+
import { acknowledgeMailbox, appendMailbox, archiveTeamDir, beginTaskAttempt, CAPTAIN_KEY, createMessage, createTeamDir, findTeamByCaptain, findTeamByParticipant, cancelUnfinishedTask, invalidateTaskAttempt, readUnreadMailbox, recordRetiredMemberIds, releaseMailboxDelivery, readTeam, sanitizeKey, transitionError, unsatisfiedDependencies, withTeamLock, writeTeam, removeTeamDir, validateCreateTask, evaluateQualityCompletion, planQualityFollowUp, resumeTeamState, buildCoverageMatrix, canDeclareDelivery, describeQualityLoop, sanitizeReviewAcceptance, sanitizeReviewObjective, normalizeBlankOptionalTaskFields, taskKindOf, } from "./state.js";
|
|
16
|
+
import { deliverToMember, installRetiredMemberGuard, installMemberSelectionRuntime, interruptMember, memberActivity, resolveMemberLlmSelection, spawnMember, steerCaptainReport, validateMemberLlmSelections, } from "./members.js";
|
|
17
|
+
import { TERMINAL_TASK_STATUSES } from "./types.js";
|
|
18
|
+
import { installTeamScheduler } from "./scheduler.js";
|
|
19
|
+
import { resolveTeamProfile } from "./profiles.js";
|
|
20
|
+
export { steerCaptainReport } from "./members.js";
|
|
21
|
+
/** The caller agent, or a loud failure for non-agent callers. */
|
|
22
|
+
function requireCaptain(exec) {
|
|
23
|
+
if (!exec.agent) {
|
|
24
|
+
throw new Error('t_team tools require a calling agent (exec.agent was undefined)');
|
|
25
|
+
}
|
|
26
|
+
return exec.agent;
|
|
27
|
+
}
|
|
28
|
+
/** The captain's workspace directory (team state root parent). */
|
|
29
|
+
function workspaceOf(agent) {
|
|
30
|
+
return agent.session.header.cwd ?? process.cwd();
|
|
31
|
+
}
|
|
32
|
+
/** Resolved absolute state root. */
|
|
33
|
+
function stateRootOf(workspace, config) {
|
|
34
|
+
return join(workspace, config.stateDir);
|
|
35
|
+
}
|
|
36
|
+
/** Process-local lock key scoped by workspace state root and team id. */
|
|
37
|
+
function teamLockKey(stateRoot, teamId) {
|
|
38
|
+
return `team:${stateRoot}:${teamId}`;
|
|
39
|
+
}
|
|
40
|
+
/** Process-local lock key enforcing one active team per captain session. */
|
|
41
|
+
function captainLockKey(stateRoot, captainId) {
|
|
42
|
+
return `captain:${stateRoot}:${captainId}`;
|
|
43
|
+
}
|
|
44
|
+
/** The team this captain currently leads, or a loud failure. */
|
|
45
|
+
async function requireCaptainTeam(workspace, config, captain) {
|
|
46
|
+
const team = await findTeamByCaptain(stateRootOf(workspace, config), captain.id);
|
|
47
|
+
if (team === undefined) {
|
|
48
|
+
throw new Error('you are not leading any team yet — call t_team_create first');
|
|
49
|
+
}
|
|
50
|
+
return team;
|
|
51
|
+
}
|
|
52
|
+
/** The team this captain or active member currently participates in. */
|
|
53
|
+
async function requireParticipantTeam(workspace, config, caller) {
|
|
54
|
+
const team = await findTeamByParticipant(stateRootOf(workspace, config), caller.id);
|
|
55
|
+
if (team === undefined) {
|
|
56
|
+
throw new Error('you do not lead or belong to any active team yet');
|
|
57
|
+
}
|
|
58
|
+
return team;
|
|
59
|
+
}
|
|
60
|
+
/** Re-derive a caller's role from fresh state while holding the team lock. */
|
|
61
|
+
function participantIdentityOf(team, agentId) {
|
|
62
|
+
if (team.captainSessionId === agentId)
|
|
63
|
+
return { kind: 'captain', name: CAPTAIN_KEY };
|
|
64
|
+
const member = team.members.find((candidate) => candidate.id === agentId && candidate.status !== 'removed');
|
|
65
|
+
return member === undefined ? undefined : { kind: 'member', name: member.name };
|
|
66
|
+
}
|
|
67
|
+
/** Fresh state for a team that still exists; never falls back to stale lookup data. */
|
|
68
|
+
async function requireFreshTeam(stateRoot, teamId) {
|
|
69
|
+
const fresh = await readTeam(stateRoot, teamId);
|
|
70
|
+
if (fresh === undefined)
|
|
71
|
+
throw new Error(`team "${teamId}" is no longer active`);
|
|
72
|
+
return fresh;
|
|
73
|
+
}
|
|
74
|
+
/** Fresh state with captain authorization rechecked inside the lock. */
|
|
75
|
+
async function requireFreshCaptainTeam(stateRoot, teamId, captainId) {
|
|
76
|
+
const fresh = await requireFreshTeam(stateRoot, teamId);
|
|
77
|
+
if (fresh.captainSessionId !== captainId) {
|
|
78
|
+
throw new Error(`only the captain of team "${fresh.name}" may perform this operation`);
|
|
79
|
+
}
|
|
80
|
+
return fresh;
|
|
81
|
+
}
|
|
82
|
+
/** Fresh state and caller identity rechecked inside the lock. */
|
|
83
|
+
async function requireFreshParticipant(stateRoot, teamId, callerId) {
|
|
84
|
+
const fresh = await requireFreshTeam(stateRoot, teamId);
|
|
85
|
+
const identity = participantIdentityOf(fresh, callerId);
|
|
86
|
+
if (identity === undefined)
|
|
87
|
+
throw new Error(`you are no longer an active participant in team "${fresh.name}"`);
|
|
88
|
+
return { team: fresh, identity };
|
|
89
|
+
}
|
|
90
|
+
/** Look up one live (non-removed) member by display name. */
|
|
91
|
+
function requireMember(team, name) {
|
|
92
|
+
const member = team.members.find((candidate) => candidate.name === name && candidate.status !== 'removed');
|
|
93
|
+
if (member === undefined) {
|
|
94
|
+
throw new Error(`no active member named "${name}" in team "${team.name}"`);
|
|
95
|
+
}
|
|
96
|
+
return member;
|
|
97
|
+
}
|
|
98
|
+
/** Look up one task by id. */
|
|
99
|
+
function requireTask(team, taskId) {
|
|
100
|
+
const task = team.tasks.find((candidate) => candidate.id === taskId);
|
|
101
|
+
if (task === undefined) {
|
|
102
|
+
throw new Error(`no task "${taskId}" in team "${team.name}" — use t_team_status to list tasks`);
|
|
103
|
+
}
|
|
104
|
+
return task;
|
|
105
|
+
}
|
|
106
|
+
function requireStagedTeam(team) {
|
|
107
|
+
if (team.phase !== 'staged') {
|
|
108
|
+
throw new Error(`team "${team.name}" is already running; its plan can no longer be edited`);
|
|
109
|
+
}
|
|
110
|
+
if (team.halted === true)
|
|
111
|
+
throw new Error(`team "${team.name}" is halted, not awaiting plan approval`);
|
|
112
|
+
}
|
|
113
|
+
function trimmedOptional(value) {
|
|
114
|
+
const trimmed = value?.trim();
|
|
115
|
+
return trimmed === undefined || trimmed === '' ? undefined : trimmed;
|
|
116
|
+
}
|
|
117
|
+
/** Validate references and cycles before a staged graph can be saved or run. */
|
|
118
|
+
function validateStagedGraph(team, requireRunnable) {
|
|
119
|
+
const members = team.members.filter((member) => member.status !== 'removed');
|
|
120
|
+
if (requireRunnable && members.length === 0)
|
|
121
|
+
throw new Error('add at least one member before approving the plan');
|
|
122
|
+
if (requireRunnable && team.tasks.length === 0)
|
|
123
|
+
throw new Error('add at least one task before approving the plan');
|
|
124
|
+
const memberNames = new Set(members.map((member) => member.name));
|
|
125
|
+
const taskIds = new Set(team.tasks.map((task) => task.id));
|
|
126
|
+
for (const task of team.tasks) {
|
|
127
|
+
if (task.subject.trim() === '')
|
|
128
|
+
throw new Error(`task "${task.id}" must have a subject`);
|
|
129
|
+
if (task.assignee !== undefined && task.assignee !== CAPTAIN_KEY && !memberNames.has(task.assignee)) {
|
|
130
|
+
throw new Error(`task "${task.id}" assignee "${task.assignee}" is not an active member`);
|
|
131
|
+
}
|
|
132
|
+
for (const dependency of task.dependencies) {
|
|
133
|
+
if (dependency === task.id)
|
|
134
|
+
throw new Error(`task "${task.id}" cannot depend on itself`);
|
|
135
|
+
if (!taskIds.has(dependency))
|
|
136
|
+
throw new Error(`task "${task.id}" depends on unknown task "${dependency}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const visiting = new Set();
|
|
140
|
+
const visited = new Set();
|
|
141
|
+
const byId = new Map(team.tasks.map((task) => [task.id, task]));
|
|
142
|
+
const visit = (taskId) => {
|
|
143
|
+
if (visiting.has(taskId))
|
|
144
|
+
throw new Error(`task dependency graph contains a cycle at "${taskId}"`);
|
|
145
|
+
if (visited.has(taskId))
|
|
146
|
+
return;
|
|
147
|
+
visiting.add(taskId);
|
|
148
|
+
for (const dependency of byId.get(taskId)?.dependencies ?? [])
|
|
149
|
+
visit(dependency);
|
|
150
|
+
visiting.delete(taskId);
|
|
151
|
+
visited.add(taskId);
|
|
152
|
+
};
|
|
153
|
+
for (const task of team.tasks)
|
|
154
|
+
visit(task.id);
|
|
155
|
+
}
|
|
156
|
+
function memberOpenTask(team, memberName, exceptTaskId) {
|
|
157
|
+
return team.tasks.find(task => task.id !== exceptTaskId
|
|
158
|
+
&& task.assignee === memberName
|
|
159
|
+
&& (task.status === 'claimed' || task.status === 'in_progress'));
|
|
160
|
+
}
|
|
161
|
+
/** Captain work is immediate, not a durable scheduler lane: allow one unfinished takeover at a time. */
|
|
162
|
+
function captainOpenTask(team, exceptTaskId) {
|
|
163
|
+
return team.tasks.find(task => task.id !== exceptTaskId
|
|
164
|
+
&& task.assignee === CAPTAIN_KEY
|
|
165
|
+
&& !TERMINAL_TASK_STATUSES.includes(task.status));
|
|
166
|
+
}
|
|
167
|
+
async function waitForMemberIdle(ctx, member, signal) {
|
|
168
|
+
if (member.id === '')
|
|
169
|
+
return;
|
|
170
|
+
const live = ctx.agents.get(member.id);
|
|
171
|
+
if (live === undefined)
|
|
172
|
+
return;
|
|
173
|
+
if (signal.aborted)
|
|
174
|
+
throw signal.reason;
|
|
175
|
+
let onAbort;
|
|
176
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
177
|
+
onAbort = () => reject(signal.reason ?? new Error('task reassignment was cancelled'));
|
|
178
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
await Promise.race([live.whenIdle(), aborted]);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
signal.removeEventListener('abort', onAbort);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Stop every currently-resident member activation for one halted team.
|
|
188
|
+
*
|
|
189
|
+
* Interrupt requests only cancel the member's current model turn and retain its
|
|
190
|
+
* activation. Draining the selected direct children is the stronger lifecycle
|
|
191
|
+
* boundary: it waits for the activation handles to release, so a child cannot
|
|
192
|
+
* keep executing after the captain-chat Stop control has reported success.
|
|
193
|
+
*/
|
|
194
|
+
async function stopTeamMemberActivations(ctx, captain, members, signal) {
|
|
195
|
+
const activeMembers = members.filter((member) => member.id !== '' && member.status !== 'removed');
|
|
196
|
+
const memberIds = activeMembers.map((member) => member.id);
|
|
197
|
+
if (memberIds.length === 0)
|
|
198
|
+
return;
|
|
199
|
+
for (const memberId of memberIds)
|
|
200
|
+
interruptMember(ctx, captain, memberId);
|
|
201
|
+
// `drainContinuableChildren` is available in the current runtime and releases
|
|
202
|
+
// the selected activation handles. Keep the quiescence fallback for pre-rc.8
|
|
203
|
+
// hosts, where interrupt is the strongest available lifecycle operation.
|
|
204
|
+
const runtime = ctx.subagents;
|
|
205
|
+
if (runtime.drainContinuableChildren !== undefined) {
|
|
206
|
+
try {
|
|
207
|
+
await runtime.drainContinuableChildren(captain, memberIds);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
// Do not claim the browser action stopped work when the runtime could not
|
|
212
|
+
// release all selected child activations. The HTTP route surfaces this
|
|
213
|
+
// failure instead of returning a false successful stop.
|
|
214
|
+
ctx.logger.warn(`t-team: failed to drain halted members: ${String(error)}`);
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const fallbackSignal = signal ?? new AbortController().signal;
|
|
219
|
+
const results = await Promise.allSettled(activeMembers.map((member) => waitForMemberIdle(ctx, member, fallbackSignal)));
|
|
220
|
+
const failed = results.find((result) => result.status === 'rejected');
|
|
221
|
+
if (failed?.status === 'rejected')
|
|
222
|
+
throw failed.reason;
|
|
223
|
+
}
|
|
224
|
+
export async function haltTeamWork(input) {
|
|
225
|
+
const halted = await withTeamLock(teamLockKey(input.stateRoot, input.teamId), async () => {
|
|
226
|
+
const fresh = await requireFreshCaptainTeam(input.stateRoot, input.teamId, input.captain.id);
|
|
227
|
+
if (fresh.halted === true) {
|
|
228
|
+
return {
|
|
229
|
+
teamName: fresh.name,
|
|
230
|
+
cancelledTasks: fresh.tasks.filter((task) => task.status === 'cancelled').length,
|
|
231
|
+
alreadyHalted: true,
|
|
232
|
+
members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const now = Date.now();
|
|
236
|
+
let cancelledTasks = 0;
|
|
237
|
+
for (const task of fresh.tasks) {
|
|
238
|
+
if (TERMINAL_TASK_STATUSES.includes(task.status))
|
|
239
|
+
continue;
|
|
240
|
+
cancelUnfinishedTask(task, 'Stopped from the captain chat.');
|
|
241
|
+
cancelledTasks += 1;
|
|
242
|
+
}
|
|
243
|
+
for (const member of fresh.members) {
|
|
244
|
+
if (member.status === 'removed')
|
|
245
|
+
continue;
|
|
246
|
+
member.status = 'idle';
|
|
247
|
+
}
|
|
248
|
+
fresh.halted = true;
|
|
249
|
+
fresh.haltedAt = now;
|
|
250
|
+
await writeTeam(input.stateRoot, fresh);
|
|
251
|
+
appendTeamEvent(input.ctx, captainSessionOf(input.ctx, fresh.captainSessionId, input.captain.session), 't-team/team-halted', {
|
|
252
|
+
teamId: fresh.id,
|
|
253
|
+
cancelledTasks,
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
teamName: fresh.name,
|
|
257
|
+
cancelledTasks,
|
|
258
|
+
alreadyHalted: false,
|
|
259
|
+
members: fresh.members.filter((member) => member.id !== '' && member.status !== 'removed').map((member) => ({ ...member })),
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
// Persist the stop boundary first, then abort the Captain before draining
|
|
263
|
+
// children. Otherwise its current model turn can observe `halted`, call
|
|
264
|
+
// resume, and race the still-running HTTP stop request.
|
|
265
|
+
input.captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
266
|
+
await stopTeamMemberActivations(input.ctx, input.captain, halted.members, input.signal);
|
|
267
|
+
// Interrupting a child emits a trailing subagent-settled notification. That
|
|
268
|
+
// notification can start a fresh Captain turn after the first cancellation,
|
|
269
|
+
// so close the stop boundary again once every child activation has drained.
|
|
270
|
+
// Queued user input is preserved both times; only runtime-generated work is
|
|
271
|
+
// prevented from silently resuming the halted team.
|
|
272
|
+
input.captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
273
|
+
return {
|
|
274
|
+
teamName: halted.teamName,
|
|
275
|
+
cancelledTasks: halted.cancelledTasks,
|
|
276
|
+
alreadyHalted: halted.alreadyHalted,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Web approval has no tool result in the captain's conversation. */
|
|
280
|
+
export function stagedPlanApprovedContext(teamName) {
|
|
281
|
+
return [
|
|
282
|
+
`The user approved the staged T Team plan "${teamName}" from the pre-run review UI.`,
|
|
283
|
+
'Approval has committed; the scheduler owns dispatch of the approved team. Do not approve again, recreate the roster, or send messages merely to start assigned tasks.',
|
|
284
|
+
'Acknowledge the approval and handle any reports or user work already pending. Yield only when waiting for members is the remaining action. Their reports will wake you automatically; do not busy-poll status or keep a turn running just to wait.',
|
|
285
|
+
'On a report, inspect the result and coordinate the next necessary action. If work has since been halted, respect that state and resume only on an explicit user request.',
|
|
286
|
+
].join('\n');
|
|
287
|
+
}
|
|
288
|
+
/** Context queued after the human rejects a staged plan. */
|
|
289
|
+
export function stagedPlanDiscardContext(teamName) {
|
|
290
|
+
return [
|
|
291
|
+
`The user discarded the staged T Team plan "${teamName}" from the pre-run review UI.`,
|
|
292
|
+
'That decision is final for this draft: it has been archived, no members were created, and no tasks may run.',
|
|
293
|
+
'Do not call t_team_create, t_team_approve, or recreate a replacement team merely because the old team is no longer active.',
|
|
294
|
+
'Wait for a later explicit user request. If the next user message is unrelated to T Team, answer it normally and do not start a team.',
|
|
295
|
+
].join('\n');
|
|
296
|
+
}
|
|
297
|
+
/** Model-facing continuation that turns the review UI back into a conversation. */
|
|
298
|
+
export function stagedPlanFeedbackContext(teamName) {
|
|
299
|
+
return [
|
|
300
|
+
`The user selected "Return to chat and revise" for the staged T Team plan "${teamName}".`,
|
|
301
|
+
'The existing staged plan is still the only draft. Do not create a replacement team, approve it, spawn members, edit the plan, or start work in this turn.',
|
|
302
|
+
'Ask the user one concise, concrete question about what they want changed, then stop and wait for their answer.',
|
|
303
|
+
'After the user answers, revise this same staged roster and DAG with one atomic t_team_edit_plan call, summarize the changes, and ask the user to review the updated plan again.',
|
|
304
|
+
].join('\n');
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Register every `t_team_*` tool into the shared tools registry.
|
|
308
|
+
* @param ctx - the plugin context (injects `tools`).
|
|
309
|
+
* @param config - resolved tool config.
|
|
310
|
+
*/
|
|
311
|
+
export function registerTTeamTools(ctx, config) {
|
|
312
|
+
installRetiredMemberGuard(ctx, config.stateDir);
|
|
313
|
+
const scheduler = installTeamScheduler(ctx, { stateDir: config.stateDir, executionPrompt: config.executionPrompt });
|
|
314
|
+
const memberSelections = installMemberSelectionRuntime(ctx, config.stateDir, (workspace, teamId, memberName) => (scheduler.kickMember(workspace, teamId, memberName)));
|
|
315
|
+
const updateStagedPlanBatch = async (captain, teamId, mutations, signal) => {
|
|
316
|
+
if (mutations.length === 0)
|
|
317
|
+
throw new Error('at least one staged plan operation is required');
|
|
318
|
+
const workspace = workspaceOf(captain);
|
|
319
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
320
|
+
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
321
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
322
|
+
requireStagedTeam(fresh);
|
|
323
|
+
for (const mutation of mutations) {
|
|
324
|
+
if (mutation.action === 'update_member') {
|
|
325
|
+
const member = requireMember(fresh, mutation.memberName);
|
|
326
|
+
if (member.id !== '')
|
|
327
|
+
throw new Error(`staged member "${member.name}" was already spawned`);
|
|
328
|
+
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
329
|
+
provider: mutation.provider,
|
|
330
|
+
model: mutation.model,
|
|
331
|
+
reasoningEffort: trimmedOptional(mutation.reasoningEffort),
|
|
332
|
+
fallback: member.fallback,
|
|
333
|
+
}, signal);
|
|
334
|
+
member.role = trimmedOptional(mutation.role);
|
|
335
|
+
member.provider = selection.provider;
|
|
336
|
+
member.model = selection.model;
|
|
337
|
+
member.reasoningEffort = selection.reasoningEffort;
|
|
338
|
+
member.executionPrompt = trimmedOptional(mutation.executionPrompt);
|
|
339
|
+
}
|
|
340
|
+
else if (mutation.action === 'update_task') {
|
|
341
|
+
const task = requireTask(fresh, mutation.taskId);
|
|
342
|
+
if (task.status !== 'pending' || (task.attempt ?? 0) !== 0) {
|
|
343
|
+
throw new Error(`task "${task.id}" has already started and cannot be edited`);
|
|
344
|
+
}
|
|
345
|
+
const subject = mutation.subject.trim();
|
|
346
|
+
if (subject === '')
|
|
347
|
+
throw new Error('task subject must not be empty');
|
|
348
|
+
task.subject = subject;
|
|
349
|
+
task.description = trimmedOptional(mutation.description);
|
|
350
|
+
task.assignee = trimmedOptional(mutation.assignee);
|
|
351
|
+
task.dependencies = [...new Set(mutation.dependencies.map((item) => item.trim()).filter(Boolean))];
|
|
352
|
+
task.updatedAt = Date.now();
|
|
353
|
+
}
|
|
354
|
+
else if (mutation.action === 'add_task') {
|
|
355
|
+
const subject = mutation.subject.trim();
|
|
356
|
+
if (subject === '')
|
|
357
|
+
throw new Error('task subject must not be empty');
|
|
358
|
+
fresh.taskSeq += 1;
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
fresh.tasks.push({
|
|
361
|
+
id: `t${fresh.taskSeq}`,
|
|
362
|
+
subject,
|
|
363
|
+
description: trimmedOptional(mutation.description),
|
|
364
|
+
status: 'pending',
|
|
365
|
+
assignee: trimmedOptional(mutation.assignee),
|
|
366
|
+
dependencies: [...new Set(mutation.dependencies.map((item) => item.trim()).filter(Boolean))],
|
|
367
|
+
attempt: 0,
|
|
368
|
+
kind: 'work',
|
|
369
|
+
createdAt: now,
|
|
370
|
+
updatedAt: now,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
else if (mutation.action === 'remove_task') {
|
|
374
|
+
const task = requireTask(fresh, mutation.taskId);
|
|
375
|
+
const dependent = fresh.tasks.find((candidate) => candidate.dependencies.includes(task.id));
|
|
376
|
+
if (dependent !== undefined) {
|
|
377
|
+
throw new Error(`task "${task.id}" is still required by "${dependent.id}"; update that dependency before removing the task`);
|
|
378
|
+
}
|
|
379
|
+
fresh.tasks = fresh.tasks.filter((candidate) => candidate.id !== task.id);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
const member = requireMember(fresh, mutation.memberName);
|
|
383
|
+
if (member.id !== '')
|
|
384
|
+
throw new Error(`staged member "${member.name}" was already spawned`);
|
|
385
|
+
const owned = fresh.tasks.filter((task) => task.assignee === member.name);
|
|
386
|
+
if (owned.length > 0) {
|
|
387
|
+
throw new Error(`member "${member.name}" still owns planned tasks: ${owned.map((task) => task.id).join(', ')}; update or remove those tasks first`);
|
|
388
|
+
}
|
|
389
|
+
fresh.members = fresh.members.filter((candidate) => candidate !== member);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
validateStagedGraph(fresh, false);
|
|
393
|
+
fresh.planReviewState = 'awaiting_review';
|
|
394
|
+
await writeTeam(stateRoot, fresh);
|
|
395
|
+
return fresh;
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
const updateStagedPlan = async (captain, teamId, mutation, signal) => (updateStagedPlanBatch(captain, teamId, [mutation], signal));
|
|
399
|
+
const approveStagedTeam = async (captain, teamId, signal) => {
|
|
400
|
+
const workspace = workspaceOf(captain);
|
|
401
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
402
|
+
const runSignal = signal ?? new AbortController().signal;
|
|
403
|
+
const approved = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
404
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
405
|
+
requireStagedTeam(fresh);
|
|
406
|
+
// A staged removal has no child session to retain in history. Drop those
|
|
407
|
+
// placeholders before transitioning to the stricter running shape.
|
|
408
|
+
fresh.members = fresh.members.filter((member) => member.status !== 'removed');
|
|
409
|
+
validateStagedGraph(fresh, true);
|
|
410
|
+
const spawned = [];
|
|
411
|
+
try {
|
|
412
|
+
const selections = new Map();
|
|
413
|
+
for (const member of fresh.members) {
|
|
414
|
+
if (member.id !== '')
|
|
415
|
+
continue;
|
|
416
|
+
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
417
|
+
provider: member.provider,
|
|
418
|
+
model: member.model,
|
|
419
|
+
reasoningEffort: member.reasoningEffort,
|
|
420
|
+
fallback: member.fallback,
|
|
421
|
+
}, runSignal);
|
|
422
|
+
selections.set(member, selection);
|
|
423
|
+
member.provider = selection.provider;
|
|
424
|
+
member.model = selection.model;
|
|
425
|
+
member.reasoningEffort = selection.reasoningEffort;
|
|
426
|
+
}
|
|
427
|
+
// This is the approval commit barrier: resolve and validate the whole
|
|
428
|
+
// final roster before spawning even the first durable child.
|
|
429
|
+
await validateMemberLlmSelections(ctx, [...selections.values()], runSignal);
|
|
430
|
+
for (const [member, selection] of selections) {
|
|
431
|
+
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, runSignal);
|
|
432
|
+
spawned.push(member);
|
|
433
|
+
}
|
|
434
|
+
if (fresh.members.some((member) => member.id === '')) {
|
|
435
|
+
throw new Error('one or more staged members could not be spawned');
|
|
436
|
+
}
|
|
437
|
+
fresh.phase = 'running';
|
|
438
|
+
delete fresh.planReviewState;
|
|
439
|
+
fresh.approvedAt = Date.now();
|
|
440
|
+
await writeTeam(stateRoot, fresh);
|
|
441
|
+
return { teamId: fresh.id, members: fresh.members.length, tasks: fresh.tasks.length };
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
await recordRetiredMemberIds(stateRoot, spawned.map((member) => member.id)).catch(() => undefined);
|
|
445
|
+
for (const member of spawned) {
|
|
446
|
+
if (member.id !== '')
|
|
447
|
+
interruptMember(ctx, captain, member.id);
|
|
448
|
+
}
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
try {
|
|
453
|
+
await scheduler.kickTeam(workspace, teamId, captain);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
// Approval is already durably committed. A transient wake-up failure is
|
|
457
|
+
// recoverable by the next status/member lifecycle kick and must not make
|
|
458
|
+
// the UI report that an already-running team failed to approve.
|
|
459
|
+
ctx.logger.warn(`t-team: post-approval kick failed for "${teamId}": ${String(error)}`);
|
|
460
|
+
}
|
|
461
|
+
return approved;
|
|
462
|
+
};
|
|
463
|
+
const continueStagedPlanning = async (captain, teamId) => {
|
|
464
|
+
const workspace = workspaceOf(captain);
|
|
465
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
466
|
+
const prepared = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
467
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
468
|
+
requireStagedTeam(fresh);
|
|
469
|
+
if (fresh.planReviewState === 'awaiting_feedback') {
|
|
470
|
+
return { teamName: fresh.name, alreadyWaiting: true };
|
|
471
|
+
}
|
|
472
|
+
fresh.planReviewState = 'awaiting_feedback';
|
|
473
|
+
await writeTeam(stateRoot, fresh);
|
|
474
|
+
return { teamName: fresh.name, alreadyWaiting: false };
|
|
475
|
+
});
|
|
476
|
+
if (prepared.alreadyWaiting)
|
|
477
|
+
return { teamId, alreadyWaiting: true };
|
|
478
|
+
// End any planning turn that is still producing tool calls. A plugin
|
|
479
|
+
// follow-up submitted after cancellation is queued as the next turn by the
|
|
480
|
+
// Harness Agent contract, so it cannot race ahead and recreate the team.
|
|
481
|
+
captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
482
|
+
try {
|
|
483
|
+
captain.followup(createUserMessage({
|
|
484
|
+
content: [{ type: 'text', text: stagedPlanFeedbackContext(prepared.teamName) }],
|
|
485
|
+
source: { kind: 'plugin', plugin: 't-team' },
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
// Do not leave the durable UI in a false waiting state when the live
|
|
490
|
+
// Captain disappeared between lookup and delivery.
|
|
491
|
+
await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
492
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
493
|
+
requireStagedTeam(fresh);
|
|
494
|
+
if (fresh.planReviewState === 'awaiting_feedback') {
|
|
495
|
+
fresh.planReviewState = 'awaiting_review';
|
|
496
|
+
await writeTeam(stateRoot, fresh);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
throw error;
|
|
500
|
+
}
|
|
501
|
+
return { teamId, alreadyWaiting: false };
|
|
502
|
+
};
|
|
503
|
+
const discardStagedTeam = async (captain, teamId) => {
|
|
504
|
+
const workspace = workspaceOf(captain);
|
|
505
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
506
|
+
const discarded = await withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
507
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, teamId, captain.id);
|
|
508
|
+
requireStagedTeam(fresh);
|
|
509
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/plan-discarded', {
|
|
510
|
+
teamId: fresh.id,
|
|
511
|
+
});
|
|
512
|
+
// A staged plan owns no child sessions. Archiving releases the captain
|
|
513
|
+
// immediately while retaining the rejected graph for later inspection.
|
|
514
|
+
await archiveTeamDir(stateRoot, fresh.id);
|
|
515
|
+
return { teamId: fresh.id, teamName: fresh.name };
|
|
516
|
+
});
|
|
517
|
+
// Preserve this control fact for the next genuine user turn, then abort the
|
|
518
|
+
// still-running Captain turn. Without both operations a late model step can
|
|
519
|
+
// observe the missing active team and incorrectly create it again.
|
|
520
|
+
try {
|
|
521
|
+
captain.inject(createUserMessage({
|
|
522
|
+
content: [{ type: 'text', text: stagedPlanDiscardContext(discarded.teamName) }],
|
|
523
|
+
source: { kind: 'plugin', plugin: 't-team' },
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
// The archive is already authoritative. Cancellation still prevents a
|
|
528
|
+
// late step from recreating work; failure to park extra context is only a
|
|
529
|
+
// live-delivery warning and must not turn a successful discard into 409.
|
|
530
|
+
ctx.logger.warn(`t-team: failed to inject discard context for "${discarded.teamId}": ${String(error)}`);
|
|
531
|
+
}
|
|
532
|
+
captain.cancel({ kind: 'user' }, { keepInbox: true });
|
|
533
|
+
return { teamId: discarded.teamId };
|
|
534
|
+
};
|
|
535
|
+
const runtime = {
|
|
536
|
+
isPendingMember: memberSelections.isPendingMember,
|
|
537
|
+
updateStagedPlan,
|
|
538
|
+
updateStagedPlanBatch,
|
|
539
|
+
approveStagedTeam,
|
|
540
|
+
continueStagedPlanning,
|
|
541
|
+
discardStagedTeam,
|
|
542
|
+
};
|
|
543
|
+
ctx.tools.register(defineTool({
|
|
544
|
+
name: 't_team_create',
|
|
545
|
+
description: 'Create a team. Use approval=required for a two-phase plan: members and tasks remain unspawned/unclaimed until the user reviews the Web plan and explicitly approves it. Optional profiles expand their configured roster; seed profiles also expand template tasks, while captain profiles leave the graph for the Captain to design. approval=automatic preserves the legacy immediate-execution path.',
|
|
546
|
+
parameters: {
|
|
547
|
+
name: { type: 'string', required: true, description: 'Name for the new team (used as its stable id).' },
|
|
548
|
+
description: { type: 'string', description: 'Team purpose / the goal the team will work on.' },
|
|
549
|
+
profile: { type: 'string', description: 'Optional configured profile name.' },
|
|
550
|
+
approval: {
|
|
551
|
+
type: 'string',
|
|
552
|
+
enum: ['required', 'automatic'],
|
|
553
|
+
description: 'required stages the plan for explicit user review; automatic starts immediately. Defaults to automatic for API compatibility.',
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
output: {
|
|
557
|
+
schema: {
|
|
558
|
+
type: 'object',
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
properties: {
|
|
561
|
+
team_id: { type: 'string', required: true },
|
|
562
|
+
team_name: { type: 'string', required: true },
|
|
563
|
+
state_dir: { type: 'string', required: true },
|
|
564
|
+
phase: { type: 'string', required: true },
|
|
565
|
+
profile: { type: 'string' },
|
|
566
|
+
task_planning: { type: 'string' },
|
|
567
|
+
members: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { member_name: { type: 'string', required: true }, member_id: { type: 'string', required: true }, provider: { type: 'string', required: true }, model: { type: 'string', required: true }, reasoning_effort: { type: 'string' }, status: { type: 'string', required: true } } } },
|
|
568
|
+
tasks: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { task_id: { type: 'string', required: true }, seed_id: { type: 'string', required: true }, subject: { type: 'string', required: true }, status: { type: 'string', required: true }, kind: { type: 'string' }, assignee: { type: 'string' }, dependencies: { type: 'array', items: { type: 'string' }, required: true } } } },
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
render: (args, value) => [{
|
|
572
|
+
type: 'text',
|
|
573
|
+
text: value.phase === 'staged'
|
|
574
|
+
? `Team "${value.team_name}" plan created under ${value.state_dir}. It is staged: finish the roster and DAG, then wait for the user to edit and approve it. Do not start or approve it yourself.`
|
|
575
|
+
: `Team "${value.team_name}" created (id ${value.team_id}) under ${value.state_dir}. You are the captain.`,
|
|
576
|
+
}],
|
|
577
|
+
},
|
|
578
|
+
async execute(args, exec) {
|
|
579
|
+
const captain = requireCaptain(exec);
|
|
580
|
+
const workspace = workspaceOf(captain);
|
|
581
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
582
|
+
const teamName = args.name.trim();
|
|
583
|
+
if (teamName === '')
|
|
584
|
+
throw new Error('team name must not be empty');
|
|
585
|
+
const teamId = sanitizeKey(teamName);
|
|
586
|
+
const staged = args.approval === 'required';
|
|
587
|
+
// Some models materialize optional parameters as "" instead of omitting
|
|
588
|
+
// them (issue #99). The profile is optional, so treat a blank value
|
|
589
|
+
// exactly like an omitted one instead of failing every create call.
|
|
590
|
+
const profileName = args.profile !== undefined && args.profile.trim() !== ''
|
|
591
|
+
? args.profile.trim()
|
|
592
|
+
: undefined;
|
|
593
|
+
const created = await withTeamLock(captainLockKey(stateRoot, captain.id), async () => {
|
|
594
|
+
const current = await findTeamByParticipant(stateRoot, captain.id);
|
|
595
|
+
if (current !== undefined) {
|
|
596
|
+
const relationship = current.captainSessionId === captain.id ? 'lead' : 'belong to';
|
|
597
|
+
const guidance = current.captainSessionId === captain.id
|
|
598
|
+
? 'Use t_team_status and continue the existing team. Do not delete and recreate it merely to continue work. End it only when the user explicitly wants a separate new team.'
|
|
599
|
+
: 'Continue your assigned member work and report to your captain; do not create a separate team.';
|
|
600
|
+
throw new Error(`you already ${relationship} team "${current.name}" (id ${current.id}). ${guidance}`);
|
|
601
|
+
}
|
|
602
|
+
return withTeamLock(teamLockKey(stateRoot, teamId), async () => {
|
|
603
|
+
const existing = await readTeam(stateRoot, teamId);
|
|
604
|
+
if (existing !== undefined) {
|
|
605
|
+
throw new Error(`team id "${teamId}" is taken by another captain — pick a different team name`);
|
|
606
|
+
}
|
|
607
|
+
if (profileName === undefined) {
|
|
608
|
+
const state = {
|
|
609
|
+
name: teamName,
|
|
610
|
+
id: teamId,
|
|
611
|
+
description: args.description,
|
|
612
|
+
captainSessionId: captain.id,
|
|
613
|
+
createdAt: Date.now(),
|
|
614
|
+
members: [],
|
|
615
|
+
tasks: [],
|
|
616
|
+
taskSeq: 0,
|
|
617
|
+
...staged ? { phase: 'staged', planReviewState: 'awaiting_review' } : {},
|
|
618
|
+
};
|
|
619
|
+
await createTeamDir(stateRoot, state);
|
|
620
|
+
return { committed: true, state };
|
|
621
|
+
}
|
|
622
|
+
return initializeProfileTeam({
|
|
623
|
+
ctx,
|
|
624
|
+
config,
|
|
625
|
+
memberSelections,
|
|
626
|
+
captain,
|
|
627
|
+
exec,
|
|
628
|
+
stateRoot,
|
|
629
|
+
teamName,
|
|
630
|
+
teamId,
|
|
631
|
+
profileName,
|
|
632
|
+
description: args.description,
|
|
633
|
+
staged,
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
if (created.committed) {
|
|
638
|
+
try {
|
|
639
|
+
await scheduler.kickTeam(workspace, created.state.id, captain);
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
ctx.logger.warn(`t-team: post-create kick failed for "${created.state.id}": ${String(error)}`);
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
appendTeamEvent(ctx, captain.session, 't-team/team-created', {
|
|
646
|
+
teamId: created.state.id,
|
|
647
|
+
captainSessionId: captain.id,
|
|
648
|
+
name: created.state.name,
|
|
649
|
+
...created.state.description !== undefined ? { description: created.state.description } : {},
|
|
650
|
+
...created.state.profile?.name === undefined ? {} : { profile: created.state.profile.name },
|
|
651
|
+
});
|
|
652
|
+
for (const member of created.state.members) {
|
|
653
|
+
appendTeamEvent(ctx, captain.session, 't-team/member-added', {
|
|
654
|
+
teamId: created.state.id,
|
|
655
|
+
memberId: member.id,
|
|
656
|
+
name: member.name,
|
|
657
|
+
...member.role === undefined ? {} : { role: member.role },
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
for (const task of created.state.tasks) {
|
|
661
|
+
appendTeamEvent(ctx, captain.session, 't-team/task-created', {
|
|
662
|
+
teamId: created.state.id,
|
|
663
|
+
taskId: task.id,
|
|
664
|
+
subject: task.subject,
|
|
665
|
+
dependencies: task.dependencies,
|
|
666
|
+
...task.assignee === undefined ? {} : { assignee: task.assignee },
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
ctx.logger.warn(`t-team: post-create events failed for "${created.state.id}": ${String(error)}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const persisted = await readTeam(stateRoot, created.state.id).catch(() => undefined);
|
|
675
|
+
const snapshot = persisted ?? created.state;
|
|
676
|
+
if (snapshot.profile === undefined) {
|
|
677
|
+
return {
|
|
678
|
+
team_id: snapshot.id,
|
|
679
|
+
team_name: snapshot.name,
|
|
680
|
+
state_dir: join(stateRoot, snapshot.id),
|
|
681
|
+
phase: snapshot.phase ?? 'running',
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
team_id: snapshot.id,
|
|
686
|
+
team_name: snapshot.name,
|
|
687
|
+
state_dir: join(stateRoot, snapshot.id),
|
|
688
|
+
phase: snapshot.phase ?? 'running',
|
|
689
|
+
profile: snapshot.profile.name,
|
|
690
|
+
task_planning: snapshot.profile.taskPlanning ?? 'seed',
|
|
691
|
+
members: snapshot.members.map((member) => ({
|
|
692
|
+
member_name: member.name,
|
|
693
|
+
member_id: member.id,
|
|
694
|
+
provider: member.provider ?? '',
|
|
695
|
+
model: member.model ?? '',
|
|
696
|
+
...member.reasoningEffort === undefined ? {} : { reasoning_effort: member.reasoningEffort },
|
|
697
|
+
status: member.status,
|
|
698
|
+
})),
|
|
699
|
+
tasks: snapshot.tasks.map((task) => ({
|
|
700
|
+
task_id: task.id,
|
|
701
|
+
seed_id: task.profileSeedId ?? '',
|
|
702
|
+
subject: task.subject,
|
|
703
|
+
status: task.status,
|
|
704
|
+
...task.kind === undefined ? {} : { kind: task.kind },
|
|
705
|
+
...task.assignee === undefined ? {} : { assignee: task.assignee },
|
|
706
|
+
dependencies: task.dependencies,
|
|
707
|
+
})),
|
|
708
|
+
};
|
|
709
|
+
},
|
|
710
|
+
}));
|
|
711
|
+
ctx.tools.register(defineTool({
|
|
712
|
+
name: 't_team_edit_plan',
|
|
713
|
+
description: 'Atomically revise the current staged T Team plan without spawning members or scheduling tasks. Use this when the user continues chatting to change a plan that is waiting for approval. Submit dependent edits in order (update downstream dependencies or assignees, then remove tasks, then remove unused members). Never inspect or edit .t-team state files or plugin source code to revise a plan.',
|
|
714
|
+
parameters: {
|
|
715
|
+
operations: {
|
|
716
|
+
type: 'array',
|
|
717
|
+
required: true,
|
|
718
|
+
description: 'One atomic, ordered batch of staged-plan edits. If any operation is invalid, none of the edits are saved.',
|
|
719
|
+
items: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
additionalProperties: false,
|
|
722
|
+
properties: {
|
|
723
|
+
action: {
|
|
724
|
+
type: 'string',
|
|
725
|
+
required: true,
|
|
726
|
+
enum: ['update_member', 'update_task', 'add_task', 'remove_task', 'remove_member'],
|
|
727
|
+
},
|
|
728
|
+
member_name: { type: 'string', description: 'Member name for update_member or remove_member.' },
|
|
729
|
+
task_id: { type: 'string', description: 'Task id for update_task or remove_task.' },
|
|
730
|
+
subject: { type: 'string', description: 'Required for add_task; optional replacement for update_task.' },
|
|
731
|
+
description: { type: 'string', description: 'Optional task description.' },
|
|
732
|
+
assignee: { type: 'string', description: 'Optional task assignee; an empty string moves it to the shared pool.' },
|
|
733
|
+
dependencies: { type: 'array', items: { type: 'string' }, description: 'Complete replacement dependency list for a task.' },
|
|
734
|
+
role: { type: 'string', description: 'Optional member role.' },
|
|
735
|
+
provider: { type: 'string', description: 'Optional member provider; defaults to the current staged route.' },
|
|
736
|
+
model: { type: 'string', description: 'Optional member model; defaults to the current staged route.' },
|
|
737
|
+
reasoning_effort: { type: 'string', description: 'Optional member reasoning effort.' },
|
|
738
|
+
execution_prompt: { type: 'string', description: 'Optional member-specific execution prompt.' },
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
output: {
|
|
744
|
+
schema: {
|
|
745
|
+
type: 'object',
|
|
746
|
+
additionalProperties: false,
|
|
747
|
+
properties: {
|
|
748
|
+
status: { type: 'string', required: true },
|
|
749
|
+
team_id: { type: 'string', required: true },
|
|
750
|
+
members: { type: 'number', required: true },
|
|
751
|
+
tasks: { type: 'number', required: true },
|
|
752
|
+
dependencies: { type: 'number', required: true },
|
|
753
|
+
roster: { type: 'array', items: { type: 'string' }, required: true },
|
|
754
|
+
graph: { type: 'array', items: { type: 'string' }, required: true },
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
render: (_args, value) => [{
|
|
758
|
+
type: 'text',
|
|
759
|
+
text: `Staged plan updated atomically (${value.members} members, ${value.tasks} tasks, ${value.dependencies} dependencies). No members were spawned and no tasks were scheduled.\n${value.graph.join('\n')}`,
|
|
760
|
+
}],
|
|
761
|
+
},
|
|
762
|
+
async execute(args, exec) {
|
|
763
|
+
const captain = requireCaptain(exec);
|
|
764
|
+
const workspace = workspaceOf(captain);
|
|
765
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
766
|
+
requireStagedTeam(team);
|
|
767
|
+
if (args.operations.length === 0)
|
|
768
|
+
throw new Error('at least one staged plan operation is required');
|
|
769
|
+
const mutations = args.operations.map((operation, index) => {
|
|
770
|
+
const label = `operation ${index + 1} (${operation.action})`;
|
|
771
|
+
if (operation.action === 'update_member') {
|
|
772
|
+
const memberName = operation.member_name?.trim() ?? '';
|
|
773
|
+
if (memberName === '')
|
|
774
|
+
throw new Error(`${label} requires member_name`);
|
|
775
|
+
const member = requireMember(team, memberName);
|
|
776
|
+
return {
|
|
777
|
+
action: 'update_member',
|
|
778
|
+
memberName,
|
|
779
|
+
role: operation.role ?? member.role,
|
|
780
|
+
provider: operation.provider?.trim() || member.provider || '',
|
|
781
|
+
model: operation.model?.trim() || member.model || '',
|
|
782
|
+
reasoningEffort: operation.reasoning_effort ?? member.reasoningEffort,
|
|
783
|
+
executionPrompt: operation.execution_prompt ?? member.executionPrompt,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
if (operation.action === 'update_task') {
|
|
787
|
+
const taskId = operation.task_id?.trim() ?? '';
|
|
788
|
+
if (taskId === '')
|
|
789
|
+
throw new Error(`${label} requires task_id`);
|
|
790
|
+
const task = requireTask(team, taskId);
|
|
791
|
+
return {
|
|
792
|
+
action: 'update_task',
|
|
793
|
+
taskId,
|
|
794
|
+
subject: operation.subject ?? task.subject,
|
|
795
|
+
description: operation.description ?? task.description,
|
|
796
|
+
assignee: operation.assignee ?? task.assignee,
|
|
797
|
+
dependencies: operation.dependencies ?? task.dependencies,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (operation.action === 'add_task') {
|
|
801
|
+
const subject = operation.subject?.trim() ?? '';
|
|
802
|
+
if (subject === '')
|
|
803
|
+
throw new Error(`${label} requires a non-empty subject`);
|
|
804
|
+
return {
|
|
805
|
+
action: 'add_task',
|
|
806
|
+
subject,
|
|
807
|
+
description: operation.description,
|
|
808
|
+
assignee: operation.assignee,
|
|
809
|
+
dependencies: operation.dependencies ?? [],
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
if (operation.action === 'remove_task') {
|
|
813
|
+
const taskId = operation.task_id?.trim() ?? '';
|
|
814
|
+
if (taskId === '')
|
|
815
|
+
throw new Error(`${label} requires task_id`);
|
|
816
|
+
return { action: 'remove_task', taskId };
|
|
817
|
+
}
|
|
818
|
+
const memberName = operation.member_name?.trim() ?? '';
|
|
819
|
+
if (memberName === '')
|
|
820
|
+
throw new Error(`${label} requires member_name`);
|
|
821
|
+
return { action: 'remove_member', memberName };
|
|
822
|
+
});
|
|
823
|
+
const updated = await updateStagedPlanBatch(captain, team.id, mutations, exec.signal);
|
|
824
|
+
return {
|
|
825
|
+
status: 'staged',
|
|
826
|
+
team_id: updated.id,
|
|
827
|
+
members: updated.members.length,
|
|
828
|
+
tasks: updated.tasks.length,
|
|
829
|
+
dependencies: updated.tasks.reduce((sum, task) => sum + task.dependencies.length, 0),
|
|
830
|
+
roster: updated.members.map((member) => `${member.name} (${member.role || 'member'}; ${member.provider ?? ''}/${member.model ?? ''})`),
|
|
831
|
+
graph: updated.tasks.map((task) => `${task.id}: ${task.subject} -> ${task.assignee || 'shared'}${task.dependencies.length === 0 ? '' : `; depends on ${task.dependencies.join(', ')}`}`),
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
}));
|
|
835
|
+
ctx.tools.register(defineTool({
|
|
836
|
+
name: 't_team_approve',
|
|
837
|
+
description: 'Approve and start a staged team plan. Call this only in response to an explicit user approval in a new user turn; never call it during the turn that created or edited the plan. The Web Approve & Run button uses the same runtime directly.',
|
|
838
|
+
parameters: {
|
|
839
|
+
confirmation: { type: 'string', required: true, description: 'The user\'s explicit approval statement.' },
|
|
840
|
+
},
|
|
841
|
+
output: {
|
|
842
|
+
schema: {
|
|
843
|
+
type: 'object',
|
|
844
|
+
additionalProperties: false,
|
|
845
|
+
properties: {
|
|
846
|
+
status: { type: 'string', required: true },
|
|
847
|
+
team_id: { type: 'string', required: true },
|
|
848
|
+
members: { type: 'number', required: true },
|
|
849
|
+
tasks: { type: 'number', required: true },
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
render: (_args, value) => [{
|
|
853
|
+
type: 'text',
|
|
854
|
+
text: `Team ${value.team_id} approved and running (${value.members} members, ${value.tasks} tasks).`,
|
|
855
|
+
}],
|
|
856
|
+
},
|
|
857
|
+
async execute(args, exec) {
|
|
858
|
+
if (args.confirmation.trim() === '')
|
|
859
|
+
throw new Error('explicit user approval text is required');
|
|
860
|
+
const captain = requireCaptain(exec);
|
|
861
|
+
const workspace = workspaceOf(captain);
|
|
862
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
863
|
+
const approved = await approveStagedTeam(captain, team.id, exec.signal);
|
|
864
|
+
return { status: 'running', team_id: approved.teamId, members: approved.members, tasks: approved.tasks };
|
|
865
|
+
},
|
|
866
|
+
}));
|
|
867
|
+
ctx.tools.register(defineTool({
|
|
868
|
+
name: 't_team_add_member',
|
|
869
|
+
description: 'Add a member to the team roster. In a staged team this only adds an editable plan row and does not spawn a child; approval spawns the final configuration. In a running team it creates the durable continuable member immediately.',
|
|
870
|
+
parameters: {
|
|
871
|
+
name: { type: 'string', required: true, description: 'Unique member name inside the team.' },
|
|
872
|
+
role: { type: 'string', description: 'Role of the member (e.g. researcher, engineer, reviewer).' },
|
|
873
|
+
provider: { type: 'string', description: 'Optional LLM provider route. Use only when the user explicitly requests a different provider; requires model.' },
|
|
874
|
+
model: { type: 'string', description: 'Optional model override. Omit for the captain\'s current model (or the configured memberModel default).' },
|
|
875
|
+
reasoning_effort: { type: 'string', description: 'Optional reasoning effort override: one of the target model\'s supported effort ids, or "default" to force its default. When omitted, the captain\'s effort is inherited only for the same provider/model; a changed route uses the target default.' },
|
|
876
|
+
executionPrompt: { type: 'string', description: 'Optional member-specific execution prompt. It remains editable while staged.' },
|
|
877
|
+
},
|
|
878
|
+
output: {
|
|
879
|
+
schema: {
|
|
880
|
+
type: 'object',
|
|
881
|
+
additionalProperties: false,
|
|
882
|
+
properties: {
|
|
883
|
+
member_name: { type: 'string', required: true },
|
|
884
|
+
member_id: { type: 'string', required: true },
|
|
885
|
+
provider: { type: 'string', required: true },
|
|
886
|
+
model: { type: 'string', required: true },
|
|
887
|
+
reasoning_effort: { type: 'string' },
|
|
888
|
+
status: { type: 'string', required: true },
|
|
889
|
+
phase: { type: 'string', required: true },
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
render: (args, value) => [{
|
|
893
|
+
type: 'text',
|
|
894
|
+
text: value.phase === 'staged'
|
|
895
|
+
? `Member "${value.member_name}" added to the staged roster (${value.provider}/${value.model}); no child was spawned.`
|
|
896
|
+
: `Member "${value.member_name}" added (subagent id ${value.member_id}, ${value.provider}/${value.model}${value.reasoning_effort === undefined ? '' : `, reasoning ${value.reasoning_effort}`}, status ${value.status}).`,
|
|
897
|
+
}],
|
|
898
|
+
},
|
|
899
|
+
async execute(args, exec) {
|
|
900
|
+
const captain = requireCaptain(exec);
|
|
901
|
+
const workspace = workspaceOf(captain);
|
|
902
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
903
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
904
|
+
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
905
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
906
|
+
const memberName = args.name.trim();
|
|
907
|
+
if (memberName === '')
|
|
908
|
+
throw new Error('member name must not be empty');
|
|
909
|
+
const memberKey = sanitizeKey(memberName);
|
|
910
|
+
if (memberKey === CAPTAIN_KEY) {
|
|
911
|
+
throw new Error(`member name "${args.name}" is reserved for the captain`);
|
|
912
|
+
}
|
|
913
|
+
if (fresh.members.some((candidate) => sanitizeKey(candidate.name) === memberKey)) {
|
|
914
|
+
throw new Error(`member name "${args.name}" has already been used in team "${fresh.name}"`);
|
|
915
|
+
}
|
|
916
|
+
if (fresh.members.filter((candidate) => candidate.status !== 'removed').length >= config.maxMembers) {
|
|
917
|
+
throw new Error(`team "${fresh.name}" is at its member cap (${config.maxMembers})`);
|
|
918
|
+
}
|
|
919
|
+
const selection = await resolveMemberLlmSelection(ctx, captain, {
|
|
920
|
+
provider: args.provider,
|
|
921
|
+
model: args.model,
|
|
922
|
+
defaultModel: config.memberModel,
|
|
923
|
+
reasoningEffort: args.reasoning_effort,
|
|
924
|
+
fallback: config.fallback,
|
|
925
|
+
}, exec.signal);
|
|
926
|
+
const member = {
|
|
927
|
+
id: '',
|
|
928
|
+
name: memberName,
|
|
929
|
+
role: args.role,
|
|
930
|
+
provider: selection.provider,
|
|
931
|
+
model: selection.model,
|
|
932
|
+
reasoningEffort: selection.reasoningEffort,
|
|
933
|
+
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
934
|
+
executionPrompt: trimmedOptional(args.executionPrompt),
|
|
935
|
+
joinedAt: Date.now(),
|
|
936
|
+
status: 'idle',
|
|
937
|
+
};
|
|
938
|
+
if (fresh.phase !== 'staged') {
|
|
939
|
+
await spawnMember(ctx, memberRuntime(config), memberSelections, selection, captain, fresh, member, config.stateDir, exec.signal);
|
|
940
|
+
}
|
|
941
|
+
fresh.members.push(member);
|
|
942
|
+
try {
|
|
943
|
+
await writeTeam(stateRoot, fresh);
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
// The continuable child is already live, but the durable team record
|
|
947
|
+
// never saw it. Retire the orphan so it disappears from subagent
|
|
948
|
+
// listings and cannot be resumed, then surface the write failure.
|
|
949
|
+
if (member.id !== '') {
|
|
950
|
+
await recordRetiredMemberIds(stateRoot, [member.id]).catch(() => undefined);
|
|
951
|
+
interruptMember(ctx, captain, member.id);
|
|
952
|
+
}
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/member-added', {
|
|
956
|
+
teamId: fresh.id,
|
|
957
|
+
memberId: member.id,
|
|
958
|
+
name: member.name,
|
|
959
|
+
...member.role !== undefined ? { role: member.role } : {},
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
member_name: member.name,
|
|
963
|
+
member_id: member.id,
|
|
964
|
+
provider: selection.provider,
|
|
965
|
+
model: selection.model,
|
|
966
|
+
...selection.reasoningEffort === undefined
|
|
967
|
+
? {}
|
|
968
|
+
: { reasoning_effort: selection.reasoningEffort },
|
|
969
|
+
status: member.status,
|
|
970
|
+
phase: fresh.phase ?? 'running',
|
|
971
|
+
};
|
|
972
|
+
});
|
|
973
|
+
await scheduler.kickMember(workspace, team.id, created.member_name, captain);
|
|
974
|
+
return created;
|
|
975
|
+
},
|
|
976
|
+
}));
|
|
977
|
+
ctx.tools.register(defineTool({
|
|
978
|
+
name: 't_team_remove_member',
|
|
979
|
+
description: 'Remove a member safely: revoke its current attempts, return all unfinished owned tasks to the shared pending pool, interrupt its live turn, and mark it removed.',
|
|
980
|
+
parameters: {
|
|
981
|
+
name: { type: 'string', required: true, description: 'Name of the member to remove.' },
|
|
982
|
+
},
|
|
983
|
+
output: {
|
|
984
|
+
schema: {
|
|
985
|
+
type: 'object',
|
|
986
|
+
additionalProperties: false,
|
|
987
|
+
properties: {
|
|
988
|
+
member_name: { type: 'string', required: true },
|
|
989
|
+
status: { type: 'string', required: true },
|
|
990
|
+
requeued_tasks: { type: 'array', items: { type: 'string' }, required: true },
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
render: (args, value) => [{
|
|
994
|
+
type: 'text',
|
|
995
|
+
text: `Member "${value.member_name}" removed (status ${value.status}); requeued tasks: ${value.requeued_tasks.join(', ') || 'none'}.`,
|
|
996
|
+
}],
|
|
997
|
+
},
|
|
998
|
+
async execute(args, exec) {
|
|
999
|
+
const captain = requireCaptain(exec);
|
|
1000
|
+
const workspace = workspaceOf(captain);
|
|
1001
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1002
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1003
|
+
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1004
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1005
|
+
const member = requireMember(fresh, args.name);
|
|
1006
|
+
const requeued = [];
|
|
1007
|
+
for (const task of fresh.tasks) {
|
|
1008
|
+
if (task.assignee !== member.name || task.status === 'completed')
|
|
1009
|
+
continue;
|
|
1010
|
+
invalidateTaskAttempt(task);
|
|
1011
|
+
task.reassigning = false;
|
|
1012
|
+
requeued.push(task.id);
|
|
1013
|
+
}
|
|
1014
|
+
member.status = 'removed';
|
|
1015
|
+
await writeTeam(stateRoot, fresh);
|
|
1016
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/member-removed', {
|
|
1017
|
+
teamId: fresh.id,
|
|
1018
|
+
memberId: member.id,
|
|
1019
|
+
});
|
|
1020
|
+
return { member: { ...member }, requeued };
|
|
1021
|
+
});
|
|
1022
|
+
if (revoked.member.id !== '') {
|
|
1023
|
+
await recordRetiredMemberIds(stateRoot, [revoked.member.id]);
|
|
1024
|
+
interruptMember(ctx, captain, revoked.member.id);
|
|
1025
|
+
await waitForMemberIdle(ctx, revoked.member, exec.signal);
|
|
1026
|
+
}
|
|
1027
|
+
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1028
|
+
return {
|
|
1029
|
+
member_name: revoked.member.name,
|
|
1030
|
+
status: revoked.member.status,
|
|
1031
|
+
requeued_tasks: revoked.requeued,
|
|
1032
|
+
};
|
|
1033
|
+
},
|
|
1034
|
+
}));
|
|
1035
|
+
ctx.tools.register(defineTool({
|
|
1036
|
+
name: 't_team_create_task',
|
|
1037
|
+
description: 'Create a task in your team\'s task list. Every call must include a non-empty subject, including verification and review tasks. Tasks can depend on other tasks (dependencies): a task is only claimable once every dependency is completed. Optionally assign it to a member, who still claims it before working.',
|
|
1038
|
+
parameters: {
|
|
1039
|
+
subject: { type: 'string', required: true, description: 'Required non-empty title for this task. Never omit it, including for verification or review tasks.' },
|
|
1040
|
+
description: { type: 'string', description: 'What needs to be done, in detail.' },
|
|
1041
|
+
dependencies: {
|
|
1042
|
+
type: 'array',
|
|
1043
|
+
items: { type: 'string' },
|
|
1044
|
+
description: 'Task ids this task depends on (must be completed before this task can be claimed).',
|
|
1045
|
+
},
|
|
1046
|
+
assignee: { type: 'string', description: 'Optional member name this task is intended for.' },
|
|
1047
|
+
kind: {
|
|
1048
|
+
type: 'string',
|
|
1049
|
+
enum: ['work', 'requirements', 'implementation', 'verification', 'review', 'repair', 'integration'],
|
|
1050
|
+
description: 'Task kind. Defaults to work (legacy, no quality gates). Quality kinds require a contract.',
|
|
1051
|
+
},
|
|
1052
|
+
round: { type: 'number', description: '1-based review / requirements / repair round.' },
|
|
1053
|
+
objective: { type: 'string', description: 'Required non-empty objective for quality kinds.' },
|
|
1054
|
+
inScope: { type: 'array', items: { type: 'string' }, description: 'Workspace-relative POSIX paths this task may change.' },
|
|
1055
|
+
outOfScope: { type: 'array', items: { type: 'string' }, description: 'Workspace-relative POSIX paths this task must not change.' },
|
|
1056
|
+
acceptance: { type: 'array', items: { type: 'string' }, description: 'Acceptance criteria. Required for quality kinds.' },
|
|
1057
|
+
verify: { type: 'array', items: { type: 'string' }, description: 'Verification commands. Required for implementation/repair.' },
|
|
1058
|
+
deliverables: { type: 'array', items: { type: 'string' }, description: 'Expected deliverable paths or names.' },
|
|
1059
|
+
nonGoals: { type: 'array', items: { type: 'string' }, description: 'Explicit non-goals.' },
|
|
1060
|
+
reviewedTaskId: { type: 'string', description: 'Task being reviewed. Required for kind=review.' },
|
|
1061
|
+
sourceTaskId: { type: 'string', description: 'Source implementation/artifact. Required for kind=repair.' },
|
|
1062
|
+
sourceFindingIds: { type: 'array', items: { type: 'string' }, description: 'Finding ids this repair must close.' },
|
|
1063
|
+
coverageOf: { type: 'array', items: { type: 'string' }, description: 'User-constraint / goal items this task covers.' },
|
|
1064
|
+
resume: { type: 'boolean', description: 'If true, clear halted in the same lock before creating the task.' },
|
|
1065
|
+
resumeReason: { type: 'string', description: 'Required non-empty reason when resume=true.' },
|
|
1066
|
+
},
|
|
1067
|
+
output: {
|
|
1068
|
+
schema: {
|
|
1069
|
+
type: 'object',
|
|
1070
|
+
additionalProperties: false,
|
|
1071
|
+
properties: {
|
|
1072
|
+
task_id: { type: 'string', required: true },
|
|
1073
|
+
subject: { type: 'string', required: true },
|
|
1074
|
+
status: { type: 'string', required: true },
|
|
1075
|
+
assignee: { type: 'string' },
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
render: (args, value) => [{
|
|
1079
|
+
type: 'text',
|
|
1080
|
+
text: `Task "${value.subject}" created as ${value.task_id} (status ${value.status}${value.assignee ? `, assigned to ${value.assignee}` : ''}).`,
|
|
1081
|
+
}],
|
|
1082
|
+
},
|
|
1083
|
+
async execute(args, exec) {
|
|
1084
|
+
const captain = requireCaptain(exec);
|
|
1085
|
+
const workspace = workspaceOf(captain);
|
|
1086
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1087
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1088
|
+
// Some models materialize optional parameters as "" instead of omitting
|
|
1089
|
+
// them (issue #105). Normalize blank optional fields to omitted before
|
|
1090
|
+
// validation so a blank value can neither be rejected spuriously nor be
|
|
1091
|
+
// persisted into team.json, where it would brick the team on reload.
|
|
1092
|
+
const input = normalizeBlankOptionalTaskFields(args);
|
|
1093
|
+
const created = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1094
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1095
|
+
const gate = validateCreateTask(fresh, {
|
|
1096
|
+
subject: input.subject,
|
|
1097
|
+
description: input.description,
|
|
1098
|
+
dependencies: input.dependencies,
|
|
1099
|
+
assignee: input.assignee,
|
|
1100
|
+
kind: input.kind,
|
|
1101
|
+
round: input.round,
|
|
1102
|
+
objective: input.objective,
|
|
1103
|
+
inScope: input.inScope,
|
|
1104
|
+
outOfScope: input.outOfScope,
|
|
1105
|
+
acceptance: input.acceptance,
|
|
1106
|
+
verify: input.verify,
|
|
1107
|
+
deliverables: input.deliverables,
|
|
1108
|
+
nonGoals: input.nonGoals,
|
|
1109
|
+
reviewedTaskId: input.reviewedTaskId,
|
|
1110
|
+
sourceTaskId: input.sourceTaskId,
|
|
1111
|
+
sourceFindingIds: input.sourceFindingIds,
|
|
1112
|
+
coverageOf: input.coverageOf,
|
|
1113
|
+
resume: input.resume,
|
|
1114
|
+
resumeReason: input.resumeReason,
|
|
1115
|
+
});
|
|
1116
|
+
if (!gate.ok)
|
|
1117
|
+
throw new Error(gate.error ?? 'create_task rejected by quality gates');
|
|
1118
|
+
if (fresh.halted === true) {
|
|
1119
|
+
const resumed = resumeTeamState(fresh, args.resumeReason ?? '');
|
|
1120
|
+
if (resumed.status !== 'resumed' || resumed.team === undefined) {
|
|
1121
|
+
throw new Error(resumed.error ?? 'team is halted; call t_team_resume or pass resume=true with resumeReason');
|
|
1122
|
+
}
|
|
1123
|
+
fresh.halted = false;
|
|
1124
|
+
fresh.haltedAt = undefined;
|
|
1125
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/team-resumed', {
|
|
1126
|
+
teamId: fresh.id,
|
|
1127
|
+
reason: args.resumeReason ?? '',
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
const dependencies = args.dependencies ?? [];
|
|
1131
|
+
for (const dependency of dependencies) {
|
|
1132
|
+
if (!fresh.tasks.some((task) => task.id === dependency)) {
|
|
1133
|
+
throw new Error(`dependency "${dependency}" does not exist in team "${fresh.name}"`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (args.assignee !== undefined)
|
|
1137
|
+
requireMember(fresh, args.assignee);
|
|
1138
|
+
const kind = gate.kind ?? 'work';
|
|
1139
|
+
const objective = kind === 'review' || kind === 'requirements'
|
|
1140
|
+
? sanitizeReviewObjective(input.objective)
|
|
1141
|
+
: input.objective;
|
|
1142
|
+
const acceptance = kind === 'review' || kind === 'requirements'
|
|
1143
|
+
? sanitizeReviewAcceptance(input.acceptance)
|
|
1144
|
+
: input.acceptance;
|
|
1145
|
+
const task = {
|
|
1146
|
+
id: `t${fresh.taskSeq + 1}`,
|
|
1147
|
+
subject: args.subject,
|
|
1148
|
+
description: args.description,
|
|
1149
|
+
status: 'pending',
|
|
1150
|
+
assignee: args.assignee,
|
|
1151
|
+
dependencies,
|
|
1152
|
+
attempt: 0,
|
|
1153
|
+
createdAt: Date.now(),
|
|
1154
|
+
updatedAt: Date.now(),
|
|
1155
|
+
kind,
|
|
1156
|
+
...args.round === undefined ? {} : { round: args.round },
|
|
1157
|
+
...objective === undefined ? {} : { objective },
|
|
1158
|
+
...input.inScope === undefined ? {} : { inScope: input.inScope },
|
|
1159
|
+
...input.outOfScope === undefined ? {} : { outOfScope: input.outOfScope },
|
|
1160
|
+
...acceptance === undefined ? {} : { acceptance },
|
|
1161
|
+
...input.verify === undefined ? {} : { verify: input.verify },
|
|
1162
|
+
...input.deliverables === undefined ? {} : { deliverables: input.deliverables },
|
|
1163
|
+
...input.nonGoals === undefined ? {} : { nonGoals: input.nonGoals },
|
|
1164
|
+
...input.reviewedTaskId === undefined ? {} : { reviewedTaskId: input.reviewedTaskId },
|
|
1165
|
+
...input.sourceTaskId === undefined ? {} : { sourceTaskId: input.sourceTaskId },
|
|
1166
|
+
...input.sourceFindingIds === undefined ? {} : { sourceFindingIds: input.sourceFindingIds },
|
|
1167
|
+
...input.coverageOf === undefined ? {} : { coverageOf: input.coverageOf },
|
|
1168
|
+
};
|
|
1169
|
+
fresh.taskSeq += 1;
|
|
1170
|
+
fresh.tasks.push(task);
|
|
1171
|
+
await writeTeam(stateRoot, fresh);
|
|
1172
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/task-created', {
|
|
1173
|
+
teamId: fresh.id,
|
|
1174
|
+
taskId: task.id,
|
|
1175
|
+
subject: task.subject,
|
|
1176
|
+
dependencies: task.dependencies,
|
|
1177
|
+
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
1178
|
+
...task.kind === undefined ? {} : { kind: task.kind },
|
|
1179
|
+
...task.round === undefined ? {} : { round: task.round },
|
|
1180
|
+
});
|
|
1181
|
+
return {
|
|
1182
|
+
task_id: task.id,
|
|
1183
|
+
subject: task.subject,
|
|
1184
|
+
status: task.status,
|
|
1185
|
+
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
1186
|
+
};
|
|
1187
|
+
});
|
|
1188
|
+
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1189
|
+
return created;
|
|
1190
|
+
},
|
|
1191
|
+
}));
|
|
1192
|
+
ctx.tools.register(defineTool({
|
|
1193
|
+
name: 't_team_reassign_task',
|
|
1194
|
+
description: 'Atomically retry, reassign, or let the captain take over one ready unfinished/failed task. The old attempt is revoked before its member is interrupted, so late updates cannot overwrite the new owner. Use assignee="captain" only when you will finish that task in this turn; a captain can own only one unfinished takeover at a time, and an unfinished takeover returns to the member pool when the captain becomes idle.',
|
|
1195
|
+
parameters: {
|
|
1196
|
+
task_id: { type: 'string', required: true, description: 'Task to retry/reassign.' },
|
|
1197
|
+
assignee: { type: 'string', required: true, description: 'Active member name, or "captain" for captain takeover.' },
|
|
1198
|
+
reason: { type: 'string', description: 'Why the task is being retried or reassigned.' },
|
|
1199
|
+
},
|
|
1200
|
+
output: {
|
|
1201
|
+
schema: {
|
|
1202
|
+
type: 'object',
|
|
1203
|
+
additionalProperties: false,
|
|
1204
|
+
properties: {
|
|
1205
|
+
task_id: { type: 'string', required: true },
|
|
1206
|
+
previous_assignee: { type: 'string', required: true },
|
|
1207
|
+
assignee: { type: 'string', required: true },
|
|
1208
|
+
status: { type: 'string', required: true },
|
|
1209
|
+
attempt: { type: 'number', required: true },
|
|
1210
|
+
attempt_id: { type: 'string' },
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
render: (_args, value) => [{
|
|
1214
|
+
type: 'text',
|
|
1215
|
+
text: `Task ${value.task_id} reassigned ${value.previous_assignee || 'unassigned'} → ${value.assignee} (attempt ${value.attempt}, status ${value.status}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}).`,
|
|
1216
|
+
}],
|
|
1217
|
+
},
|
|
1218
|
+
async execute(args, exec) {
|
|
1219
|
+
const captain = requireCaptain(exec);
|
|
1220
|
+
const workspace = workspaceOf(captain);
|
|
1221
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1222
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1223
|
+
const target = args.assignee.trim();
|
|
1224
|
+
if (target === '')
|
|
1225
|
+
throw new Error('reassignment assignee must not be empty');
|
|
1226
|
+
const revoked = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1227
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1228
|
+
const task = requireTask(fresh, args.task_id);
|
|
1229
|
+
if (task.status === 'completed')
|
|
1230
|
+
throw new Error(`completed task ${task.id} is immutable and cannot be reassigned`);
|
|
1231
|
+
if (task.reassigning === true)
|
|
1232
|
+
throw new Error(`task ${task.id} is already being reassigned`);
|
|
1233
|
+
const targetMember = target === CAPTAIN_KEY ? undefined : requireMember(fresh, target);
|
|
1234
|
+
if (target === CAPTAIN_KEY) {
|
|
1235
|
+
const busy = captainOpenTask(fresh, task.id);
|
|
1236
|
+
if (busy !== undefined) {
|
|
1237
|
+
throw new Error(`captain is busy with ${busy.id}; complete or reassign it before taking over ${task.id}`);
|
|
1238
|
+
}
|
|
1239
|
+
const pending = unsatisfiedDependencies(fresh.tasks, task.dependencies);
|
|
1240
|
+
if (pending.length > 0) {
|
|
1241
|
+
throw new Error(`task ${task.id} is blocked by unfinished dependencies: ${pending.join(', ')} — complete them before captain takeover`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
else if (targetMember !== undefined) {
|
|
1245
|
+
const busy = memberOpenTask(fresh, targetMember.name, task.id);
|
|
1246
|
+
if (busy !== undefined) {
|
|
1247
|
+
throw new Error(`member "${targetMember.name}" is busy with ${busy.id}; finish or reassign it first`);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
const previousAssignee = task.assignee ?? '';
|
|
1251
|
+
const previousMember = (task.status !== 'claimed' && task.status !== 'in_progress')
|
|
1252
|
+
|| task.assignee === undefined || task.assignee === CAPTAIN_KEY
|
|
1253
|
+
? undefined
|
|
1254
|
+
: fresh.members.find(member => member.name === task.assignee && member.status !== 'removed');
|
|
1255
|
+
invalidateTaskAttempt(task, target, true);
|
|
1256
|
+
await writeTeam(stateRoot, fresh);
|
|
1257
|
+
return {
|
|
1258
|
+
previousAssignee,
|
|
1259
|
+
previousMember: previousMember === undefined ? undefined : { ...previousMember },
|
|
1260
|
+
handoffId: task.handoffId,
|
|
1261
|
+
};
|
|
1262
|
+
});
|
|
1263
|
+
let quiescenceError;
|
|
1264
|
+
if (revoked.previousMember !== undefined) {
|
|
1265
|
+
interruptMember(ctx, captain, revoked.previousMember.id);
|
|
1266
|
+
try {
|
|
1267
|
+
await waitForMemberIdle(ctx, revoked.previousMember, exec.signal);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
quiescenceError = error;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1274
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1275
|
+
const task = requireTask(fresh, args.task_id);
|
|
1276
|
+
if (task.handoffId !== revoked.handoffId || task.assignee !== target || task.reassigning !== true) {
|
|
1277
|
+
throw new Error(`task ${task.id} changed during reassignment; refusing to overwrite the newer state`);
|
|
1278
|
+
}
|
|
1279
|
+
task.reassigning = false;
|
|
1280
|
+
if (quiescenceError === undefined && target === CAPTAIN_KEY) {
|
|
1281
|
+
beginTaskAttempt(task, CAPTAIN_KEY);
|
|
1282
|
+
// The captain is already in the turn that requested takeover; there
|
|
1283
|
+
// is no later member claim handshake to move claimed -> in_progress.
|
|
1284
|
+
task.status = 'in_progress';
|
|
1285
|
+
task.updatedAt = Date.now();
|
|
1286
|
+
}
|
|
1287
|
+
await writeTeam(stateRoot, fresh);
|
|
1288
|
+
appendTeamEvent(ctx, captain.session, 't-team/task-updated', {
|
|
1289
|
+
teamId: fresh.id,
|
|
1290
|
+
taskId: task.id,
|
|
1291
|
+
status: task.status,
|
|
1292
|
+
assignee: task.assignee,
|
|
1293
|
+
...args.reason === undefined ? {} : { output: `Reassigned: ${args.reason}` },
|
|
1294
|
+
});
|
|
1295
|
+
});
|
|
1296
|
+
if (quiescenceError !== undefined)
|
|
1297
|
+
throw quiescenceError;
|
|
1298
|
+
if (target !== CAPTAIN_KEY)
|
|
1299
|
+
await scheduler.kickMember(workspace, team.id, target, captain);
|
|
1300
|
+
const current = await readTeam(stateRoot, team.id);
|
|
1301
|
+
const task = current === undefined ? undefined : requireTask(current, args.task_id);
|
|
1302
|
+
if (task === undefined)
|
|
1303
|
+
throw new Error(`team "${team.name}" ended during reassignment`);
|
|
1304
|
+
return {
|
|
1305
|
+
task_id: task.id,
|
|
1306
|
+
previous_assignee: revoked.previousAssignee,
|
|
1307
|
+
assignee: task.assignee ?? '',
|
|
1308
|
+
status: task.status,
|
|
1309
|
+
attempt: task.attempt ?? 0,
|
|
1310
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
1311
|
+
};
|
|
1312
|
+
},
|
|
1313
|
+
}));
|
|
1314
|
+
ctx.tools.register(defineTool({
|
|
1315
|
+
name: 't_team_claim_task',
|
|
1316
|
+
description: 'Members claim their own ready task or read their existing attempt_id. Captains must use reassign_task to assign and wake a member; claim_task does not dispatch work. A member cannot own a second unfinished task. The returned attempt_id is required for updates and becomes stale after retry/reassignment.',
|
|
1317
|
+
parameters: {
|
|
1318
|
+
task_id: { type: 'string', required: true, description: 'The task id to claim.' },
|
|
1319
|
+
assignee: { type: 'string', description: 'Deprecated: claim_task only supports a member claiming its own task. Captains must use reassign_task.' },
|
|
1320
|
+
},
|
|
1321
|
+
output: {
|
|
1322
|
+
schema: {
|
|
1323
|
+
type: 'object',
|
|
1324
|
+
additionalProperties: false,
|
|
1325
|
+
properties: {
|
|
1326
|
+
task_id: { type: 'string', required: true },
|
|
1327
|
+
status: { type: 'string', required: true },
|
|
1328
|
+
assignee: { type: 'string', required: true },
|
|
1329
|
+
attempt: { type: 'number', required: true },
|
|
1330
|
+
attempt_id: { type: 'string' },
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
render: (args, value) => [{
|
|
1334
|
+
type: 'text',
|
|
1335
|
+
text: `Task ${value.task_id} claimed by ${value.assignee} (attempt ${value.attempt}${value.attempt_id ? `, attempt_id ${value.attempt_id}` : ''}, status ${value.status}).`,
|
|
1336
|
+
}],
|
|
1337
|
+
},
|
|
1338
|
+
async execute(args, exec) {
|
|
1339
|
+
const caller = requireCaptain(exec);
|
|
1340
|
+
const workspace = workspaceOf(caller);
|
|
1341
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1342
|
+
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1343
|
+
return withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1344
|
+
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1345
|
+
const task = requireTask(fresh, args.task_id);
|
|
1346
|
+
if (task.reassigning === true) {
|
|
1347
|
+
throw new Error(`task ${task.id} is being reassigned; wait for the handoff to finish`);
|
|
1348
|
+
}
|
|
1349
|
+
let assignee = task.assignee;
|
|
1350
|
+
if (identity.kind === 'captain') {
|
|
1351
|
+
// A captain may read the capability of its already-started takeover,
|
|
1352
|
+
// but must never create a member claim without dispatching it (#125).
|
|
1353
|
+
if (args.assignee !== undefined || task.assignee !== CAPTAIN_KEY
|
|
1354
|
+
|| (task.status !== 'claimed' && task.status !== 'in_progress')) {
|
|
1355
|
+
throw new Error('claim_task is for members claiming their own task; captains must use t_team_reassign_task to assign and wake a member');
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
else {
|
|
1359
|
+
if (args.assignee !== undefined) {
|
|
1360
|
+
throw new Error('members cannot set assignee when claiming a task');
|
|
1361
|
+
}
|
|
1362
|
+
if (assignee !== undefined && assignee !== identity.name) {
|
|
1363
|
+
throw new Error(`task ${task.id} is assigned to "${assignee}", not you`);
|
|
1364
|
+
}
|
|
1365
|
+
assignee = identity.name;
|
|
1366
|
+
}
|
|
1367
|
+
// Authorization must happen before the idempotent return: another
|
|
1368
|
+
// member must not receive a false success for somebody else's task.
|
|
1369
|
+
if (task.status === 'claimed' || task.status === 'in_progress') {
|
|
1370
|
+
if (assignee === undefined || task.assignee !== assignee) {
|
|
1371
|
+
throw new Error(`task ${task.id} is already claimed by "${task.assignee ?? 'nobody'}"`);
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
task_id: task.id,
|
|
1375
|
+
status: task.status,
|
|
1376
|
+
assignee,
|
|
1377
|
+
attempt: task.attempt ?? 0,
|
|
1378
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
1379
|
+
};
|
|
1380
|
+
}
|
|
1381
|
+
const pending = unsatisfiedDependencies(fresh.tasks, task.dependencies);
|
|
1382
|
+
if (pending.length > 0) {
|
|
1383
|
+
throw new Error(`task ${task.id} is blocked by unfinished dependencies: ${pending.join(', ')} — complete them first`);
|
|
1384
|
+
}
|
|
1385
|
+
const transition = transitionError(task.status, 'claimed');
|
|
1386
|
+
if (transition !== undefined)
|
|
1387
|
+
throw new Error(transition);
|
|
1388
|
+
if (assignee === undefined) {
|
|
1389
|
+
throw new Error('claiming an unassigned task needs an assignee (claim on behalf of a member)');
|
|
1390
|
+
}
|
|
1391
|
+
const busy = memberOpenTask(fresh, assignee, task.id);
|
|
1392
|
+
if (busy !== undefined) {
|
|
1393
|
+
throw new Error(`member "${assignee}" is busy with ${busy.id}; finish or reassign it first`);
|
|
1394
|
+
}
|
|
1395
|
+
const attemptId = beginTaskAttempt(task, assignee);
|
|
1396
|
+
await writeTeam(stateRoot, fresh);
|
|
1397
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 't-team/task-updated', {
|
|
1398
|
+
teamId: fresh.id,
|
|
1399
|
+
taskId: task.id,
|
|
1400
|
+
status: task.status,
|
|
1401
|
+
assignee: task.assignee,
|
|
1402
|
+
});
|
|
1403
|
+
return {
|
|
1404
|
+
task_id: task.id,
|
|
1405
|
+
status: task.status,
|
|
1406
|
+
assignee: task.assignee ?? '',
|
|
1407
|
+
attempt: task.attempt ?? 0,
|
|
1408
|
+
attempt_id: attemptId,
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1411
|
+
},
|
|
1412
|
+
}));
|
|
1413
|
+
ctx.tools.register(defineTool({
|
|
1414
|
+
name: 't_team_update_task',
|
|
1415
|
+
description: 'Update a task status/output. Members must supply the current attempt_id returned by claim_task; stale attempts are rejected after takeover/reassignment. Terminal results are immutable. A captain must use reassign_task(assignee="captain") before updating member-owned work.',
|
|
1416
|
+
parameters: {
|
|
1417
|
+
task_id: { type: 'string', required: true, description: 'The task id to update.' },
|
|
1418
|
+
status: {
|
|
1419
|
+
type: 'string',
|
|
1420
|
+
enum: ['in_progress', 'completed', 'failed', 'cancelled'],
|
|
1421
|
+
description: 'New status (in_progress, completed, failed, cancelled).',
|
|
1422
|
+
},
|
|
1423
|
+
output: { type: 'string', description: 'Result summary; set when completing or failing.' },
|
|
1424
|
+
attempt_id: { type: 'string', description: 'Current execution capability returned by claim_task (required for members when present on the task).' },
|
|
1425
|
+
verdict: {
|
|
1426
|
+
type: 'string',
|
|
1427
|
+
enum: ['pass', 'needs_revision', 'reject'],
|
|
1428
|
+
description: 'Required for completing requirements/review. needs_revision and reject must fail the task.',
|
|
1429
|
+
},
|
|
1430
|
+
findings: {
|
|
1431
|
+
type: 'array',
|
|
1432
|
+
items: {
|
|
1433
|
+
type: 'object',
|
|
1434
|
+
additionalProperties: false,
|
|
1435
|
+
properties: {
|
|
1436
|
+
id: { type: 'string', required: true },
|
|
1437
|
+
severity: { type: 'string', enum: ['low', 'medium', 'high', 'blocker'], required: true },
|
|
1438
|
+
problem: { type: 'string', required: true },
|
|
1439
|
+
requiredFix: { type: 'string', required: true },
|
|
1440
|
+
file: { type: 'string' },
|
|
1441
|
+
line: { type: 'number' },
|
|
1442
|
+
resolved: { type: 'boolean' },
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
description: 'Structured review findings. Required when verdict is needs_revision or reject; each item needs id, severity, problem, and requiredFix.',
|
|
1446
|
+
},
|
|
1447
|
+
changedPaths: {
|
|
1448
|
+
type: 'array',
|
|
1449
|
+
items: { type: 'string' },
|
|
1450
|
+
description: 'Workspace-relative POSIX paths changed by this implementation/repair.',
|
|
1451
|
+
},
|
|
1452
|
+
acceptanceResults: {
|
|
1453
|
+
type: 'array',
|
|
1454
|
+
items: {
|
|
1455
|
+
type: 'object',
|
|
1456
|
+
additionalProperties: false,
|
|
1457
|
+
properties: {
|
|
1458
|
+
criterion: { type: 'string', required: true },
|
|
1459
|
+
status: { type: 'string', enum: ['passed', 'failed'], required: true },
|
|
1460
|
+
evidence: { type: 'string' },
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
description: 'Acceptance evidence in contract order: {criterion, status:"passed"|"failed", evidence?}. Supply one item per acceptance criterion.',
|
|
1464
|
+
},
|
|
1465
|
+
commandsRun: {
|
|
1466
|
+
type: 'array',
|
|
1467
|
+
items: {
|
|
1468
|
+
type: 'object',
|
|
1469
|
+
additionalProperties: false,
|
|
1470
|
+
properties: {
|
|
1471
|
+
command: { type: 'string', required: true },
|
|
1472
|
+
status: { type: 'string', enum: ['passed', 'failed'], required: true },
|
|
1473
|
+
exitCode: { type: 'number' },
|
|
1474
|
+
evidence: { type: 'string' },
|
|
1475
|
+
},
|
|
1476
|
+
},
|
|
1477
|
+
description: 'Verification evidence in contract order: {command, status:"passed"|"failed", exitCode?, evidence?}. Supply one item per verify command.',
|
|
1478
|
+
},
|
|
1479
|
+
},
|
|
1480
|
+
output: {
|
|
1481
|
+
schema: {
|
|
1482
|
+
type: 'object',
|
|
1483
|
+
additionalProperties: false,
|
|
1484
|
+
properties: {
|
|
1485
|
+
task_id: { type: 'string', required: true },
|
|
1486
|
+
status: { type: 'string', required: true },
|
|
1487
|
+
output: { type: 'string' },
|
|
1488
|
+
attempt: { type: 'number', required: true },
|
|
1489
|
+
attempt_id: { type: 'string' },
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
render: (args, value) => [{
|
|
1493
|
+
type: 'text',
|
|
1494
|
+
text: `Task ${value.task_id} attempt ${value.attempt} → ${value.status}${value.output !== undefined ? `\nOutput: ${value.output}` : ''}`,
|
|
1495
|
+
}],
|
|
1496
|
+
},
|
|
1497
|
+
async execute(args, exec) {
|
|
1498
|
+
const caller = requireCaptain(exec);
|
|
1499
|
+
const workspace = workspaceOf(caller);
|
|
1500
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1501
|
+
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1502
|
+
const updated = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1503
|
+
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1504
|
+
const task = requireTask(fresh, args.task_id);
|
|
1505
|
+
if (identity.kind === 'captain'
|
|
1506
|
+
&& task.assignee !== undefined
|
|
1507
|
+
&& task.assignee !== CAPTAIN_KEY) {
|
|
1508
|
+
throw new Error(`task ${task.id} is owned by member "${task.assignee}"; call t_team_reassign_task with assignee="captain" before takeover`);
|
|
1509
|
+
}
|
|
1510
|
+
if (identity.kind === 'member') {
|
|
1511
|
+
if (task.assignee !== identity.name) {
|
|
1512
|
+
throw new Error(`task ${task.id} is assigned to "${task.assignee ?? 'nobody'}", not you`);
|
|
1513
|
+
}
|
|
1514
|
+
if (task.attemptId !== undefined && args.attempt_id !== task.attemptId) {
|
|
1515
|
+
throw new Error(`stale attempt for task ${task.id}: expected the current attempt_id; stop work and request fresh assignment`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
if (TERMINAL_TASK_STATUSES.includes(task.status)) {
|
|
1519
|
+
const sameStatus = args.status === undefined || args.status === task.status;
|
|
1520
|
+
const sameOutput = args.output === undefined || args.output === task.output;
|
|
1521
|
+
if (!sameStatus || !sameOutput) {
|
|
1522
|
+
throw new Error(`terminal task ${task.id} is immutable; use t_team_reassign_task to retry failed/cancelled work`);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
task_id: task.id,
|
|
1526
|
+
status: task.status,
|
|
1527
|
+
attempt: task.attempt ?? 0,
|
|
1528
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
1529
|
+
...task.output !== undefined ? { output: task.output } : {},
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
// Blank optional list entries (e.g. changedPaths:[""]) must not be
|
|
1533
|
+
// persisted: hasValidQualityTaskFields rejects them on reload and
|
|
1534
|
+
// would brick the whole team state (issue #105 class).
|
|
1535
|
+
const input = normalizeBlankOptionalTaskFields(args);
|
|
1536
|
+
const findings = parseFindings(args.findings);
|
|
1537
|
+
const acceptanceResults = parseAcceptanceResults(args.acceptanceResults);
|
|
1538
|
+
const commandsRun = parseCommandResults(args.commandsRun);
|
|
1539
|
+
const gate = evaluateQualityCompletion(task, {
|
|
1540
|
+
status: args.status,
|
|
1541
|
+
output: args.output,
|
|
1542
|
+
verdict: args.verdict,
|
|
1543
|
+
findings,
|
|
1544
|
+
changedPaths: input.changedPaths,
|
|
1545
|
+
acceptanceResults,
|
|
1546
|
+
commandsRun,
|
|
1547
|
+
});
|
|
1548
|
+
if (!gate.ok)
|
|
1549
|
+
throw new Error(gate.error ?? 'update_task rejected by quality gates');
|
|
1550
|
+
if (args.status !== undefined) {
|
|
1551
|
+
const transition = transitionError(task.status, args.status);
|
|
1552
|
+
if (transition !== undefined)
|
|
1553
|
+
throw new Error(transition);
|
|
1554
|
+
task.status = args.status;
|
|
1555
|
+
}
|
|
1556
|
+
if (args.output !== undefined)
|
|
1557
|
+
task.output = args.output;
|
|
1558
|
+
if (args.verdict !== undefined)
|
|
1559
|
+
task.verdict = args.verdict;
|
|
1560
|
+
if (findings !== undefined)
|
|
1561
|
+
task.findings = findings;
|
|
1562
|
+
if (input.changedPaths !== undefined)
|
|
1563
|
+
task.changedPaths = input.changedPaths;
|
|
1564
|
+
if (acceptanceResults !== undefined)
|
|
1565
|
+
task.acceptanceResults = acceptanceResults;
|
|
1566
|
+
if (commandsRun !== undefined)
|
|
1567
|
+
task.commandsRun = commandsRun;
|
|
1568
|
+
task.updatedAt = Date.now();
|
|
1569
|
+
const followUp = (task.status === 'failed' && (task.verdict === 'needs_revision' || task.verdict === 'reject'))
|
|
1570
|
+
? applyQualityFollowUp(fresh, task)
|
|
1571
|
+
: undefined;
|
|
1572
|
+
if (followUp?.escalated === true) {
|
|
1573
|
+
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, createMessage(CAPTAIN_KEY, CAPTAIN_KEY, `Quality-gate loop escalated after ${task.id} (${task.kind ?? 'review'} verdict=${task.verdict}). Automatic repair/review stopped.`));
|
|
1574
|
+
}
|
|
1575
|
+
await writeTeam(stateRoot, fresh);
|
|
1576
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 't-team/task-updated', {
|
|
1577
|
+
teamId: fresh.id,
|
|
1578
|
+
taskId: task.id,
|
|
1579
|
+
status: task.status,
|
|
1580
|
+
...task.assignee !== undefined ? { assignee: task.assignee } : {},
|
|
1581
|
+
...task.output !== undefined ? { output: task.output } : {},
|
|
1582
|
+
...task.verdict === undefined ? {} : { verdict: task.verdict },
|
|
1583
|
+
...task.round === undefined ? {} : { round: task.round },
|
|
1584
|
+
});
|
|
1585
|
+
for (const created of followUp?.created ?? []) {
|
|
1586
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 't-team/task-created', {
|
|
1587
|
+
teamId: fresh.id,
|
|
1588
|
+
taskId: created.id,
|
|
1589
|
+
subject: created.subject,
|
|
1590
|
+
dependencies: created.dependencies,
|
|
1591
|
+
...created.assignee === undefined ? {} : { assignee: created.assignee },
|
|
1592
|
+
...created.kind === undefined ? {} : { kind: created.kind },
|
|
1593
|
+
...created.round === undefined ? {} : { round: created.round },
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
task_id: task.id,
|
|
1598
|
+
status: task.status,
|
|
1599
|
+
attempt: task.attempt ?? 0,
|
|
1600
|
+
...task.attemptId === undefined ? {} : { attempt_id: task.attemptId },
|
|
1601
|
+
...task.output !== undefined ? { output: task.output } : {},
|
|
1602
|
+
};
|
|
1603
|
+
});
|
|
1604
|
+
await scheduler.kickTeam(workspace, team.id, team.captainSessionId === caller.id ? caller : undefined);
|
|
1605
|
+
return updated;
|
|
1606
|
+
},
|
|
1607
|
+
}));
|
|
1608
|
+
ctx.tools.register(defineTool({
|
|
1609
|
+
name: 't_team_send_message',
|
|
1610
|
+
description: 'Send a message to the captain or to a teammate. Messages go straight into the recipient\'s mailbox; when the captain agent is online the plugin also schedules live delivery (member recipients get the message as their next turn; a running captain sees it at the nearest model step). No relay is involved: teammates talk to each other directly, exactly like the Claude Code T Team mailbox model.',
|
|
1611
|
+
parameters: {
|
|
1612
|
+
to: { type: 'string', required: true, description: 'Recipient: "captain" or a member name.' },
|
|
1613
|
+
content: { type: 'string', required: true, description: 'The message text.' },
|
|
1614
|
+
from: { type: 'string', description: 'Sender (defaults to the caller: the captain, or the calling member).' },
|
|
1615
|
+
},
|
|
1616
|
+
output: {
|
|
1617
|
+
schema: {
|
|
1618
|
+
type: 'object',
|
|
1619
|
+
additionalProperties: false,
|
|
1620
|
+
properties: {
|
|
1621
|
+
message_id: { type: 'string', required: true },
|
|
1622
|
+
from: { type: 'string', required: true },
|
|
1623
|
+
to: { type: 'string', required: true },
|
|
1624
|
+
delivered: { type: 'string', required: true, description: 'live (accepted by the live captain), wake (member recipient woken), or mailbox (durable inbox only).' },
|
|
1625
|
+
},
|
|
1626
|
+
},
|
|
1627
|
+
render: (args, value) => [{
|
|
1628
|
+
type: 'text',
|
|
1629
|
+
text: `Message ${value.message_id} ${value.from} → ${value.to} delivered via ${value.delivered}.`,
|
|
1630
|
+
}],
|
|
1631
|
+
},
|
|
1632
|
+
async execute(args, exec) {
|
|
1633
|
+
const caller = requireCaptain(exec);
|
|
1634
|
+
const workspace = workspaceOf(caller);
|
|
1635
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1636
|
+
const team = await requireParticipantTeam(workspace, config, caller);
|
|
1637
|
+
const to = args.to.trim();
|
|
1638
|
+
const prepared = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1639
|
+
const { team: fresh, identity } = await requireFreshParticipant(stateRoot, team.id, caller.id);
|
|
1640
|
+
const from = identity.name;
|
|
1641
|
+
// `from` may only be the caller's own identity: impersonating another
|
|
1642
|
+
// member (or the captain) would poison the mailbox and event records.
|
|
1643
|
+
if (args.from !== undefined && args.from !== from) {
|
|
1644
|
+
throw new Error(`t_team_send_message: "from" must be your own identity ("${from}"), not "${args.from}"`);
|
|
1645
|
+
}
|
|
1646
|
+
if (to === CAPTAIN_KEY) {
|
|
1647
|
+
const message = { ...createMessage(from, CAPTAIN_KEY, args.content), deliveryClaimedAt: Date.now() };
|
|
1648
|
+
await appendMailbox(stateRoot, fresh.id, CAPTAIN_KEY, message);
|
|
1649
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 't-team/message-sent', {
|
|
1650
|
+
teamId: fresh.id,
|
|
1651
|
+
messageId: message.id,
|
|
1652
|
+
from,
|
|
1653
|
+
to: CAPTAIN_KEY,
|
|
1654
|
+
content: args.content,
|
|
1655
|
+
ts: message.ts,
|
|
1656
|
+
});
|
|
1657
|
+
return { kind: 'captain', fresh, identity, message, from };
|
|
1658
|
+
}
|
|
1659
|
+
if (fresh.halted === true) {
|
|
1660
|
+
throw new Error(`team "${fresh.name}" is halted; call t_team_resume before waking a member`);
|
|
1661
|
+
}
|
|
1662
|
+
const recipient = requireMember(fresh, to);
|
|
1663
|
+
const message = { ...createMessage(from, recipient.name, args.content), deliveryClaimedAt: Date.now() };
|
|
1664
|
+
await appendMailbox(stateRoot, fresh.id, recipient.name, message);
|
|
1665
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, caller.session), 't-team/message-sent', {
|
|
1666
|
+
teamId: fresh.id,
|
|
1667
|
+
messageId: message.id,
|
|
1668
|
+
from,
|
|
1669
|
+
to: recipient.name,
|
|
1670
|
+
content: args.content,
|
|
1671
|
+
ts: message.ts,
|
|
1672
|
+
});
|
|
1673
|
+
return { kind: 'member', fresh, identity, message, from, recipient };
|
|
1674
|
+
});
|
|
1675
|
+
// Resolve the exact live captain only after releasing the state lock.
|
|
1676
|
+
// The plugin mailbox is already durable if live delivery cannot proceed.
|
|
1677
|
+
const captain = ctx.agents.get(prepared.fresh.captainSessionId);
|
|
1678
|
+
if (prepared.kind === 'captain') {
|
|
1679
|
+
let delivered = 'mailbox';
|
|
1680
|
+
if (captain !== undefined && prepared.identity.kind === 'member') {
|
|
1681
|
+
delivered = steerCaptainReport(captain, prepared.from, args.content) ? 'live' : 'mailbox';
|
|
1682
|
+
}
|
|
1683
|
+
if (delivered === 'live') {
|
|
1684
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (acknowledgeMailbox(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (releaseMailboxDelivery(stateRoot, prepared.fresh.id, CAPTAIN_KEY, [prepared.message.id])));
|
|
1688
|
+
}
|
|
1689
|
+
return { message_id: prepared.message.id, from: prepared.from, to: CAPTAIN_KEY, delivered };
|
|
1690
|
+
}
|
|
1691
|
+
let delivered = 'mailbox';
|
|
1692
|
+
if (captain !== undefined && prepared.recipient.id !== '') {
|
|
1693
|
+
const senderText = prepared.from === CAPTAIN_KEY
|
|
1694
|
+
? args.content
|
|
1695
|
+
: `Message from team member ${prepared.from}:\n\n${args.content}`;
|
|
1696
|
+
const text = `T Team state policy: inspect ${config.stateDir}/${prepared.fresh.id}/ read-only; never edit team.json or inbox files directly. Use t_team_* tools for team state.\n\n${senderText}`;
|
|
1697
|
+
const accepted = await deliverToMember(ctx, captain, prepared.recipient.id, text, exec.signal);
|
|
1698
|
+
delivered = accepted ? 'wake' : 'mailbox';
|
|
1699
|
+
if (accepted) {
|
|
1700
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (acknowledgeMailbox(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (delivered === 'mailbox') {
|
|
1704
|
+
await withTeamLock(teamLockKey(stateRoot, prepared.fresh.id), () => (releaseMailboxDelivery(stateRoot, prepared.fresh.id, prepared.recipient.name, [prepared.message.id])));
|
|
1705
|
+
}
|
|
1706
|
+
return {
|
|
1707
|
+
message_id: prepared.message.id,
|
|
1708
|
+
from: prepared.from,
|
|
1709
|
+
to: prepared.recipient.name,
|
|
1710
|
+
delivered,
|
|
1711
|
+
};
|
|
1712
|
+
},
|
|
1713
|
+
}));
|
|
1714
|
+
ctx.tools.register(defineTool({
|
|
1715
|
+
name: 't_team_status',
|
|
1716
|
+
description: 'Team snapshot: members with live activity and tasks with status/assignee/dependencies/output. Captains also see every team mailbox; members see only their own inbox. Use after mailbox progress deliveries or for an explicit status request. After dispatch, end your turn while members work; do not repeatedly poll.',
|
|
1717
|
+
parameters: {},
|
|
1718
|
+
output: {
|
|
1719
|
+
schema: { type: 'object', additionalProperties: true, properties: {} },
|
|
1720
|
+
render: (_args, value) => [{ type: 'text', text: renderStatus(value) }],
|
|
1721
|
+
},
|
|
1722
|
+
async execute(_args, exec) {
|
|
1723
|
+
const caller = requireCaptain(exec);
|
|
1724
|
+
const workspace = workspaceOf(caller);
|
|
1725
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1726
|
+
// [local patch] a fresh workspace has no team: report that state instead of throwing
|
|
1727
|
+
const located = await requireParticipantTeam(workspace, config, caller).catch((error) => {
|
|
1728
|
+
if (error instanceof Error && error.message === 'you do not lead or belong to any active team yet')
|
|
1729
|
+
return undefined;
|
|
1730
|
+
throw error;
|
|
1731
|
+
});
|
|
1732
|
+
if (located === undefined) {
|
|
1733
|
+
return {
|
|
1734
|
+
active: false,
|
|
1735
|
+
team_id: '',
|
|
1736
|
+
team_name: '(none)',
|
|
1737
|
+
description: 'no active team in this workspace yet — call t_team_create to stage one',
|
|
1738
|
+
phase: 'none',
|
|
1739
|
+
halted: false,
|
|
1740
|
+
escalated: false,
|
|
1741
|
+
loop_summary: '',
|
|
1742
|
+
viewer: 'none',
|
|
1743
|
+
members: [],
|
|
1744
|
+
tasks: [],
|
|
1745
|
+
captain_inbox: [],
|
|
1746
|
+
member_inboxes: {},
|
|
1747
|
+
mailbox_warning_count: 0,
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
if (located.captainSessionId === caller.id) {
|
|
1751
|
+
await scheduler.kickTeam(workspace, located.id, caller);
|
|
1752
|
+
}
|
|
1753
|
+
const { team, identity } = await withTeamLock(teamLockKey(stateRoot, located.id), () => requireFreshParticipant(stateRoot, located.id, caller.id));
|
|
1754
|
+
const activity = memberActivity(ctx, team.members.map((member) => member.id));
|
|
1755
|
+
const members = team.members
|
|
1756
|
+
.filter((member) => member.status !== 'removed')
|
|
1757
|
+
.map((member) => ({
|
|
1758
|
+
name: member.name,
|
|
1759
|
+
role: member.role ?? '',
|
|
1760
|
+
provider: member.provider ?? '',
|
|
1761
|
+
model: member.model ?? '',
|
|
1762
|
+
reasoning_effort: member.reasoningEffort ?? '',
|
|
1763
|
+
status: member.status,
|
|
1764
|
+
activity: member.id !== '' ? (activity.get(member.id) ?? 'unknown') : 'unspawned',
|
|
1765
|
+
}));
|
|
1766
|
+
const tasks = team.tasks.map((task) => ({
|
|
1767
|
+
id: task.id,
|
|
1768
|
+
subject: task.subject,
|
|
1769
|
+
status: task.status,
|
|
1770
|
+
assignee: task.assignee ?? '',
|
|
1771
|
+
dependencies: task.dependencies,
|
|
1772
|
+
attempt: task.attempt ?? 0,
|
|
1773
|
+
attempt_id: task.attemptId ?? '',
|
|
1774
|
+
reassigning: task.reassigning === true,
|
|
1775
|
+
kind: taskKindOf(task),
|
|
1776
|
+
...task.round === undefined ? {} : { round: task.round },
|
|
1777
|
+
...task.verdict === undefined ? {} : { verdict: task.verdict },
|
|
1778
|
+
findings_open: (task.findings ?? []).filter((finding) => finding.resolved !== true).length,
|
|
1779
|
+
...task.profileSeedId === undefined ? {} : { seed_id: task.profileSeedId },
|
|
1780
|
+
...task.output !== undefined ? { output: task.output } : {},
|
|
1781
|
+
}));
|
|
1782
|
+
const mailboxWarnings = [];
|
|
1783
|
+
let mailboxWarningCount = 0;
|
|
1784
|
+
const reportMalformed = (agentKey) => (lineNumber) => {
|
|
1785
|
+
mailboxWarningCount += 1;
|
|
1786
|
+
if (mailboxWarnings.length < 10) {
|
|
1787
|
+
mailboxWarnings.push(`${agentKey} mailbox line ${lineNumber}`);
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
const captainInbox = identity.kind === 'captain'
|
|
1791
|
+
? await readUnreadMailbox(stateRoot, team.id, CAPTAIN_KEY, reportMalformed(CAPTAIN_KEY))
|
|
1792
|
+
: [];
|
|
1793
|
+
const memberInboxes = {};
|
|
1794
|
+
const visibleMembers = identity.kind === 'captain'
|
|
1795
|
+
? members
|
|
1796
|
+
: members.filter((member) => member.name === identity.name);
|
|
1797
|
+
for (const member of visibleMembers) {
|
|
1798
|
+
const messages = await readUnreadMailbox(stateRoot, team.id, member.name, reportMalformed(member.name));
|
|
1799
|
+
if (messages.length > 0) {
|
|
1800
|
+
memberInboxes[member.name] = {
|
|
1801
|
+
count: messages.length,
|
|
1802
|
+
latest: messages[messages.length - 1]?.content.slice(0, 200) ?? '',
|
|
1803
|
+
};
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
const coverage = buildCoverageMatrix([...new Set(team.tasks.flatMap((item) => item.coverageOf ?? []))], team.tasks).map((row) => ({
|
|
1807
|
+
goal_item: row.goal_item,
|
|
1808
|
+
task_ids: [...row.task_ids],
|
|
1809
|
+
status: row.status,
|
|
1810
|
+
...row.evidence === undefined ? {} : { evidence: row.evidence },
|
|
1811
|
+
}));
|
|
1812
|
+
const deliveryCheck = canDeclareDelivery(team);
|
|
1813
|
+
const delivery = { ok: deliveryCheck.ok, blockers: [...deliveryCheck.blockers] };
|
|
1814
|
+
const loop = describeQualityLoop(team);
|
|
1815
|
+
const result = {
|
|
1816
|
+
team_id: team.id,
|
|
1817
|
+
team_name: team.name,
|
|
1818
|
+
description: team.description ?? '',
|
|
1819
|
+
phase: team.phase ?? 'running',
|
|
1820
|
+
halted: loop.halted,
|
|
1821
|
+
escalated: loop.escalated,
|
|
1822
|
+
loop_state: loop.state,
|
|
1823
|
+
loop_summary: loop.summary,
|
|
1824
|
+
deliverable: loop.deliverable,
|
|
1825
|
+
coverage,
|
|
1826
|
+
delivery,
|
|
1827
|
+
...team.profile === undefined ? {} : {
|
|
1828
|
+
profile: {
|
|
1829
|
+
name: team.profile.name,
|
|
1830
|
+
...team.profile.protocol === undefined
|
|
1831
|
+
? {}
|
|
1832
|
+
: { protocol: team.profile.protocol.slice(0, 240) },
|
|
1833
|
+
...team.profile.taskPlanning === undefined ? {} : { task_planning: team.profile.taskPlanning },
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
viewer: identity.name,
|
|
1837
|
+
members,
|
|
1838
|
+
tasks,
|
|
1839
|
+
captain_inbox: captainInbox.slice(-10).map((message) => ({
|
|
1840
|
+
from: message.from,
|
|
1841
|
+
content: message.content,
|
|
1842
|
+
ts: message.ts,
|
|
1843
|
+
})),
|
|
1844
|
+
member_inboxes: memberInboxes,
|
|
1845
|
+
mailbox_warnings: mailboxWarnings,
|
|
1846
|
+
mailbox_warning_count: mailboxWarningCount,
|
|
1847
|
+
};
|
|
1848
|
+
const acknowledged = identity.kind === 'captain'
|
|
1849
|
+
? captainInbox.map(message => message.id)
|
|
1850
|
+
: await readUnreadMailbox(stateRoot, team.id, identity.name).then(messages => messages.map(message => message.id));
|
|
1851
|
+
if (acknowledged.length > 0) {
|
|
1852
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), () => (acknowledgeMailbox(stateRoot, team.id, identity.kind === 'captain' ? CAPTAIN_KEY : identity.name, acknowledged)));
|
|
1853
|
+
}
|
|
1854
|
+
return result;
|
|
1855
|
+
},
|
|
1856
|
+
}));
|
|
1857
|
+
ctx.tools.register(defineTool({
|
|
1858
|
+
name: 't_team_resume',
|
|
1859
|
+
description: 'Explicitly resume a halted team. Requires a non-empty reason. Does not recreate cancelled tasks; only still-pending work is scheduled.',
|
|
1860
|
+
parameters: {
|
|
1861
|
+
reason: { type: 'string', required: true, description: 'Why the team is being resumed.' },
|
|
1862
|
+
},
|
|
1863
|
+
output: {
|
|
1864
|
+
schema: {
|
|
1865
|
+
type: 'object',
|
|
1866
|
+
additionalProperties: false,
|
|
1867
|
+
properties: {
|
|
1868
|
+
status: { type: 'string', required: true },
|
|
1869
|
+
team_id: { type: 'string', required: true },
|
|
1870
|
+
reason: { type: 'string', required: true },
|
|
1871
|
+
},
|
|
1872
|
+
},
|
|
1873
|
+
render: (_args, value) => [{
|
|
1874
|
+
type: 'text',
|
|
1875
|
+
text: value.status === 'already_running'
|
|
1876
|
+
? `Team ${value.team_id} is already running.`
|
|
1877
|
+
: `Team ${value.team_id} resumed (${value.reason}).`,
|
|
1878
|
+
}],
|
|
1879
|
+
},
|
|
1880
|
+
async execute(args, exec) {
|
|
1881
|
+
const captain = requireCaptain(exec);
|
|
1882
|
+
const workspace = workspaceOf(captain);
|
|
1883
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1884
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1885
|
+
const result = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1886
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1887
|
+
const resumed = resumeTeamState(fresh, args.reason);
|
|
1888
|
+
if (resumed.status === 'rejected')
|
|
1889
|
+
throw new Error(resumed.error ?? 'resume rejected');
|
|
1890
|
+
if (resumed.status === 'resumed') {
|
|
1891
|
+
fresh.halted = false;
|
|
1892
|
+
fresh.haltedAt = undefined;
|
|
1893
|
+
await writeTeam(stateRoot, fresh);
|
|
1894
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/team-resumed', {
|
|
1895
|
+
teamId: fresh.id,
|
|
1896
|
+
reason: args.reason,
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
return {
|
|
1900
|
+
status: resumed.status,
|
|
1901
|
+
team_id: fresh.id,
|
|
1902
|
+
reason: args.reason,
|
|
1903
|
+
};
|
|
1904
|
+
});
|
|
1905
|
+
if (result.status === 'resumed')
|
|
1906
|
+
await scheduler.kickTeam(workspace, team.id, captain);
|
|
1907
|
+
return result;
|
|
1908
|
+
},
|
|
1909
|
+
}));
|
|
1910
|
+
ctx.tools.register(defineTool({
|
|
1911
|
+
name: 't_team_delete',
|
|
1912
|
+
description: 'End and archive your team: interrupts members and moves the current tasks and mailboxes out of active state for later inspection. Use when the work is done or explicitly abandoned. A same-name archive replaces its previous generation.',
|
|
1913
|
+
parameters: {},
|
|
1914
|
+
output: {
|
|
1915
|
+
schema: {
|
|
1916
|
+
type: 'object',
|
|
1917
|
+
additionalProperties: false,
|
|
1918
|
+
properties: {
|
|
1919
|
+
deleted: { type: 'boolean', required: true },
|
|
1920
|
+
team_name: { type: 'string', required: true },
|
|
1921
|
+
},
|
|
1922
|
+
},
|
|
1923
|
+
render: (args, value) => [{
|
|
1924
|
+
type: 'text',
|
|
1925
|
+
text: `Team "${value.team_name}" ended and archived.`,
|
|
1926
|
+
}],
|
|
1927
|
+
},
|
|
1928
|
+
async execute(_args, exec) {
|
|
1929
|
+
const captain = requireCaptain(exec);
|
|
1930
|
+
const workspace = workspaceOf(captain);
|
|
1931
|
+
const stateRoot = stateRootOf(workspace, config);
|
|
1932
|
+
const team = await requireCaptainTeam(workspace, config, captain);
|
|
1933
|
+
const members = await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1934
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1935
|
+
// Include previously removed members so deleting a pre-fix team also
|
|
1936
|
+
// retires durable catalog entries left behind by remove_member.
|
|
1937
|
+
const roster = fresh.members.map(member => ({ ...member }));
|
|
1938
|
+
for (const member of fresh.members) {
|
|
1939
|
+
if (member.status === 'removed')
|
|
1940
|
+
continue;
|
|
1941
|
+
member.status = 'removed';
|
|
1942
|
+
for (const task of fresh.tasks) {
|
|
1943
|
+
if (task.assignee === member.name && !TERMINAL_TASK_STATUSES.includes(task.status))
|
|
1944
|
+
invalidateTaskAttempt(task);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
await writeTeam(stateRoot, fresh);
|
|
1948
|
+
return roster;
|
|
1949
|
+
});
|
|
1950
|
+
await recordRetiredMemberIds(stateRoot, members.map(member => member.id));
|
|
1951
|
+
for (const member of members) {
|
|
1952
|
+
if (member.id === '')
|
|
1953
|
+
continue;
|
|
1954
|
+
interruptMember(ctx, captain, member.id);
|
|
1955
|
+
}
|
|
1956
|
+
const quiescence = await Promise.allSettled(members.map(member => waitForMemberIdle(ctx, member, exec.signal)));
|
|
1957
|
+
for (const result of quiescence) {
|
|
1958
|
+
if (result.status === 'rejected') {
|
|
1959
|
+
ctx.logger.warn(`t-team: member did not quiesce cleanly before team archive: ${String(result.reason)}`);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
await withTeamLock(teamLockKey(stateRoot, team.id), async () => {
|
|
1963
|
+
const fresh = await requireFreshCaptainTeam(stateRoot, team.id, captain.id);
|
|
1964
|
+
appendTeamEvent(ctx, captainSessionOf(ctx, fresh.captainSessionId, captain.session), 't-team/team-deleted', {
|
|
1965
|
+
teamId: fresh.id,
|
|
1966
|
+
});
|
|
1967
|
+
// Archive, not delete: tasks (with their dependency graph) and the
|
|
1968
|
+
// mailboxes stay on disk for later review and dependency rebuilds.
|
|
1969
|
+
await archiveTeamDir(stateRoot, fresh.id);
|
|
1970
|
+
});
|
|
1971
|
+
return { deleted: true, team_name: team.name };
|
|
1972
|
+
},
|
|
1973
|
+
}));
|
|
1974
|
+
return runtime;
|
|
1975
|
+
}
|
|
1976
|
+
async function initializeProfileTeam(input) {
|
|
1977
|
+
const profile = resolveTeamProfile(input.config.profiles, input.profileName, input.config.maxMembers);
|
|
1978
|
+
const selections = [];
|
|
1979
|
+
for (const template of profile.members) {
|
|
1980
|
+
selections.push(await resolveMemberLlmSelection(input.ctx, input.captain, {
|
|
1981
|
+
provider: template.provider,
|
|
1982
|
+
model: template.model,
|
|
1983
|
+
defaultModel: input.config.memberModel,
|
|
1984
|
+
reasoningEffort: template.reasoningEffort,
|
|
1985
|
+
fallback: template.fallback ?? profile.fallback ?? input.config.fallback,
|
|
1986
|
+
}, input.exec.signal));
|
|
1987
|
+
}
|
|
1988
|
+
await validateMemberLlmSelections(input.ctx, selections, input.exec.signal);
|
|
1989
|
+
const now = Date.now();
|
|
1990
|
+
const seedToActual = new Map(profile.tasks.map((template, index) => [template.id, `t${index + 1}`]));
|
|
1991
|
+
const draft = {
|
|
1992
|
+
name: input.teamName,
|
|
1993
|
+
id: input.teamId,
|
|
1994
|
+
description: input.description,
|
|
1995
|
+
profile: {
|
|
1996
|
+
name: profile.name,
|
|
1997
|
+
...profile.description === undefined ? {} : { description: profile.description },
|
|
1998
|
+
...profile.protocol === undefined ? {} : { protocol: profile.protocol },
|
|
1999
|
+
...profile.executionPrompt === undefined ? {} : { executionPrompt: profile.executionPrompt },
|
|
2000
|
+
...profile.fallback === undefined ? {} : { fallback: profile.fallback },
|
|
2001
|
+
taskPlanning: profile.taskPlanning,
|
|
2002
|
+
...profile.reviewPolicy === undefined ? {} : { reviewPolicy: profile.reviewPolicy },
|
|
2003
|
+
},
|
|
2004
|
+
...profile.reviewPolicy === undefined ? {} : { reviewPolicy: profile.reviewPolicy },
|
|
2005
|
+
captainSessionId: input.captain.id,
|
|
2006
|
+
createdAt: now,
|
|
2007
|
+
...input.staged ? { phase: 'staged', planReviewState: 'awaiting_review' } : {},
|
|
2008
|
+
members: profile.members.map((template, index) => {
|
|
2009
|
+
const selection = selections[index];
|
|
2010
|
+
return {
|
|
2011
|
+
id: '',
|
|
2012
|
+
name: template.name,
|
|
2013
|
+
role: template.role,
|
|
2014
|
+
provider: selection.provider,
|
|
2015
|
+
model: selection.model,
|
|
2016
|
+
reasoningEffort: selection.reasoningEffort,
|
|
2017
|
+
executionPrompt: template.executionPrompt ?? profile.executionPrompt ?? input.config.executionPrompt,
|
|
2018
|
+
...selection.fallback === undefined ? {} : { fallback: selection.fallback },
|
|
2019
|
+
joinedAt: now,
|
|
2020
|
+
status: 'idle',
|
|
2021
|
+
};
|
|
2022
|
+
}),
|
|
2023
|
+
tasks: profile.tasks.map((template, index) => ({
|
|
2024
|
+
id: `t${index + 1}`,
|
|
2025
|
+
profileSeedId: template.id,
|
|
2026
|
+
subject: template.subject,
|
|
2027
|
+
description: template.description,
|
|
2028
|
+
status: 'pending',
|
|
2029
|
+
assignee: template.assignee,
|
|
2030
|
+
dependencies: template.dependencies.map((dependency) => seedToActual.get(dependency) ?? dependency),
|
|
2031
|
+
attempt: 0,
|
|
2032
|
+
createdAt: now,
|
|
2033
|
+
updatedAt: now,
|
|
2034
|
+
})),
|
|
2035
|
+
taskSeq: profile.tasks.length,
|
|
2036
|
+
};
|
|
2037
|
+
if (input.staged) {
|
|
2038
|
+
await createTeamDir(input.stateRoot, draft);
|
|
2039
|
+
return { committed: true, state: draft };
|
|
2040
|
+
}
|
|
2041
|
+
const spawned = [];
|
|
2042
|
+
try {
|
|
2043
|
+
for (const member of draft.members) {
|
|
2044
|
+
const selection = selections[spawned.length];
|
|
2045
|
+
await spawnMember(input.ctx, memberRuntime(input.config), input.memberSelections, selection, input.captain, draft, member, input.config.stateDir, input.exec.signal);
|
|
2046
|
+
spawned.push(member);
|
|
2047
|
+
}
|
|
2048
|
+
if (draft.members.some((member) => member.id === '')) {
|
|
2049
|
+
throw new Error(`failed to initialize profile "${profile.name}": a spawned member is missing its child id`);
|
|
2050
|
+
}
|
|
2051
|
+
await createTeamDir(input.stateRoot, draft);
|
|
2052
|
+
return { committed: true, state: draft };
|
|
2053
|
+
}
|
|
2054
|
+
catch (error) {
|
|
2055
|
+
const cleanupErrors = [];
|
|
2056
|
+
try {
|
|
2057
|
+
await removeTeamDir(input.stateRoot, draft.id);
|
|
2058
|
+
}
|
|
2059
|
+
catch (cleanupError) {
|
|
2060
|
+
cleanupErrors.push(cleanupError);
|
|
2061
|
+
}
|
|
2062
|
+
try {
|
|
2063
|
+
await recordRetiredMemberIds(input.stateRoot, spawned.map((member) => member.id));
|
|
2064
|
+
}
|
|
2065
|
+
catch (cleanupError) {
|
|
2066
|
+
cleanupErrors.push(cleanupError);
|
|
2067
|
+
}
|
|
2068
|
+
for (const member of spawned) {
|
|
2069
|
+
try {
|
|
2070
|
+
interruptMember(input.ctx, input.captain, member.id);
|
|
2071
|
+
}
|
|
2072
|
+
catch (cleanupError) {
|
|
2073
|
+
cleanupErrors.push(cleanupError);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
if (cleanupErrors.length > 0) {
|
|
2077
|
+
throw new AggregateError([error, ...cleanupErrors], `failed to initialize profile "${profile.name}"`);
|
|
2078
|
+
}
|
|
2079
|
+
throw error;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function parseFindings(value) {
|
|
2083
|
+
if (value === undefined)
|
|
2084
|
+
return undefined;
|
|
2085
|
+
if (!Array.isArray(value))
|
|
2086
|
+
throw new Error('findings must be an array');
|
|
2087
|
+
return value.map((item, index) => {
|
|
2088
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
2089
|
+
throw new Error(`findings[${index}] must be an object`);
|
|
2090
|
+
}
|
|
2091
|
+
const raw = item;
|
|
2092
|
+
if (typeof raw['id'] !== 'string' || raw['id'].trim() === '')
|
|
2093
|
+
throw new Error(`findings[${index}].id is required`);
|
|
2094
|
+
if (raw['severity'] !== 'low' && raw['severity'] !== 'medium' && raw['severity'] !== 'high' && raw['severity'] !== 'blocker') {
|
|
2095
|
+
throw new Error(`findings[${index}].severity is invalid`);
|
|
2096
|
+
}
|
|
2097
|
+
if (typeof raw['problem'] !== 'string' || raw['problem'].trim() === '')
|
|
2098
|
+
throw new Error(`findings[${index}].problem is required`);
|
|
2099
|
+
if (typeof raw['requiredFix'] !== 'string' || raw['requiredFix'].trim() === '')
|
|
2100
|
+
throw new Error(`findings[${index}].requiredFix is required`);
|
|
2101
|
+
return {
|
|
2102
|
+
id: raw['id'].trim(),
|
|
2103
|
+
severity: raw['severity'],
|
|
2104
|
+
problem: raw['problem'],
|
|
2105
|
+
requiredFix: raw['requiredFix'],
|
|
2106
|
+
// A blank optional file must be omitted, not persisted: durable-state
|
|
2107
|
+
// validation requires non-empty optional strings (issue #105 class).
|
|
2108
|
+
...typeof raw['file'] === 'string' && raw['file'].trim() !== '' ? { file: raw['file'] } : {},
|
|
2109
|
+
...typeof raw['line'] === 'number' ? { line: raw['line'] } : {},
|
|
2110
|
+
...typeof raw['resolved'] === 'boolean' ? { resolved: raw['resolved'] } : {},
|
|
2111
|
+
};
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function parseAcceptanceResults(value) {
|
|
2115
|
+
if (value === undefined)
|
|
2116
|
+
return undefined;
|
|
2117
|
+
if (!Array.isArray(value))
|
|
2118
|
+
throw new Error('acceptanceResults must be an array');
|
|
2119
|
+
return value.map((item, index) => {
|
|
2120
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
2121
|
+
throw new Error(`acceptanceResults[${index}] must be an object`);
|
|
2122
|
+
}
|
|
2123
|
+
const raw = item;
|
|
2124
|
+
if (typeof raw['criterion'] !== 'string' || raw['criterion'].trim() === '') {
|
|
2125
|
+
throw new Error(`acceptanceResults[${index}].criterion is required`);
|
|
2126
|
+
}
|
|
2127
|
+
if (raw['status'] !== 'passed' && raw['status'] !== 'failed') {
|
|
2128
|
+
throw new Error(`acceptanceResults[${index}].status must be passed or failed`);
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
criterion: raw['criterion'],
|
|
2132
|
+
status: raw['status'],
|
|
2133
|
+
...typeof raw['evidence'] === 'string' ? { evidence: raw['evidence'] } : {},
|
|
2134
|
+
};
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
function parseCommandResults(value) {
|
|
2138
|
+
if (value === undefined)
|
|
2139
|
+
return undefined;
|
|
2140
|
+
if (!Array.isArray(value))
|
|
2141
|
+
throw new Error('commandsRun must be an array');
|
|
2142
|
+
return value.map((item, index) => {
|
|
2143
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
2144
|
+
throw new Error(`commandsRun[${index}] must be an object`);
|
|
2145
|
+
}
|
|
2146
|
+
const raw = item;
|
|
2147
|
+
if (typeof raw['command'] !== 'string' || raw['command'].trim() === '') {
|
|
2148
|
+
throw new Error(`commandsRun[${index}].command is required`);
|
|
2149
|
+
}
|
|
2150
|
+
if (raw['status'] !== 'passed' && raw['status'] !== 'failed') {
|
|
2151
|
+
throw new Error(`commandsRun[${index}].status must be passed or failed`);
|
|
2152
|
+
}
|
|
2153
|
+
return {
|
|
2154
|
+
command: raw['command'],
|
|
2155
|
+
status: raw['status'],
|
|
2156
|
+
...typeof raw['exitCode'] === 'number' ? { exitCode: raw['exitCode'] } : {},
|
|
2157
|
+
...typeof raw['evidence'] === 'string' ? { evidence: raw['evidence'] } : {},
|
|
2158
|
+
};
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
export function applyQualityFollowUp(team, closed) {
|
|
2162
|
+
const planned = planQualityFollowUp(team, closed);
|
|
2163
|
+
if (planned.escalated === true)
|
|
2164
|
+
team.escalated = true;
|
|
2165
|
+
const created = [];
|
|
2166
|
+
const existing = [...team.tasks];
|
|
2167
|
+
const now = Date.now();
|
|
2168
|
+
const idBySubject = new Map();
|
|
2169
|
+
for (const draft of planned.created) {
|
|
2170
|
+
team.taskSeq += 1;
|
|
2171
|
+
const id = `t${team.taskSeq}`;
|
|
2172
|
+
if (draft.id !== undefined)
|
|
2173
|
+
idBySubject.set(draft.id, id);
|
|
2174
|
+
if (draft.subject !== undefined)
|
|
2175
|
+
idBySubject.set(draft.subject, id);
|
|
2176
|
+
const dependencies = (draft.dependencies ?? []).map((dependency) => {
|
|
2177
|
+
if (team.tasks.some((item) => item.id === dependency))
|
|
2178
|
+
return dependency;
|
|
2179
|
+
return idBySubject.get(dependency) ?? dependency;
|
|
2180
|
+
});
|
|
2181
|
+
const next = {
|
|
2182
|
+
id,
|
|
2183
|
+
subject: draft.subject ?? `${draft.kind}-round-${draft.round ?? 1}`,
|
|
2184
|
+
status: 'pending',
|
|
2185
|
+
assignee: draft.assignee,
|
|
2186
|
+
dependencies,
|
|
2187
|
+
attempt: 0,
|
|
2188
|
+
createdAt: now,
|
|
2189
|
+
updatedAt: now,
|
|
2190
|
+
kind: draft.kind,
|
|
2191
|
+
...draft.round === undefined ? {} : { round: draft.round },
|
|
2192
|
+
...draft.objective === undefined ? {} : { objective: draft.objective },
|
|
2193
|
+
...draft.inScope === undefined ? {} : { inScope: draft.inScope },
|
|
2194
|
+
...draft.outOfScope === undefined ? {} : { outOfScope: draft.outOfScope },
|
|
2195
|
+
...draft.acceptance === undefined ? {} : { acceptance: draft.acceptance },
|
|
2196
|
+
...draft.verify === undefined ? {} : { verify: draft.verify },
|
|
2197
|
+
...draft.sourceTaskId === undefined ? {} : { sourceTaskId: draft.sourceTaskId },
|
|
2198
|
+
...draft.sourceFindingIds === undefined ? {} : { sourceFindingIds: draft.sourceFindingIds },
|
|
2199
|
+
...draft.reviewedTaskId === undefined ? {} : { reviewedTaskId: idBySubject.get(draft.reviewedTaskId) ?? draft.reviewedTaskId },
|
|
2200
|
+
};
|
|
2201
|
+
team.tasks.push(next);
|
|
2202
|
+
created.push(next);
|
|
2203
|
+
}
|
|
2204
|
+
// A staged full delivery plan may already contain downstream integration
|
|
2205
|
+
// work that points at the first requirements/review gate. When that gate
|
|
2206
|
+
// opens an automatic revision loop, move only still-pending downstream
|
|
2207
|
+
// edges to the new terminal gate so the approved plan can continue after
|
|
2208
|
+
// the repair instead of waiting forever on an intentionally failed task.
|
|
2209
|
+
const replacement = created.at(-1);
|
|
2210
|
+
if (replacement !== undefined) {
|
|
2211
|
+
for (const task of existing) {
|
|
2212
|
+
if (task.status !== 'pending' || !task.dependencies.includes(closed.id))
|
|
2213
|
+
continue;
|
|
2214
|
+
task.dependencies = task.dependencies.map((dependency) => (dependency === closed.id ? replacement.id : dependency));
|
|
2215
|
+
task.updatedAt = now;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
return { created, escalated: planned.escalated === true };
|
|
2219
|
+
}
|
|
2220
|
+
/** Build the `memberRuntime` config handed to member helpers. */
|
|
2221
|
+
function memberRuntime(config) {
|
|
2222
|
+
return {
|
|
2223
|
+
provider: config.memberProvider,
|
|
2224
|
+
maxDepth: config.memberMaxDepth,
|
|
2225
|
+
executionPrompt: config.executionPrompt,
|
|
2226
|
+
fallback: config.fallback,
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
/** Render the status snapshot as compact text for the model. */
|
|
2230
|
+
function renderStatus(value) {
|
|
2231
|
+
const team = value;
|
|
2232
|
+
const flags = [
|
|
2233
|
+
team.halted ? 'halted' : undefined,
|
|
2234
|
+
team.escalated ? 'escalated' : undefined,
|
|
2235
|
+
team.deliverable ? 'deliverable' : undefined,
|
|
2236
|
+
team.loop_state && team.loop_state !== 'running' && team.loop_state !== 'halted' && team.loop_state !== 'escalated'
|
|
2237
|
+
? team.loop_state
|
|
2238
|
+
: undefined,
|
|
2239
|
+
].filter((item) => item !== undefined);
|
|
2240
|
+
const lines = [
|
|
2241
|
+
`Team "${team.team_name}"${team.description ? ` — ${team.description}` : ''}${flags.length > 0 ? ` [${flags.join(', ')}]` : ''}`,
|
|
2242
|
+
...team.profile === undefined ? [] : [`Profile: ${team.profile.name}${team.profile.task_planning ? ` [${team.profile.task_planning}]` : ''}${team.profile.protocol ? ` — ${team.profile.protocol}` : ''}`],
|
|
2243
|
+
...team.loop_summary ? [`Loop: ${team.loop_state ?? ''} — ${team.loop_summary}`.replace(/^Loop: — /u, 'Loop: ')] : [],
|
|
2244
|
+
`Viewing as: ${team.viewer}`,
|
|
2245
|
+
`Members (${team.members.length}):`,
|
|
2246
|
+
...team.members.map((member) => {
|
|
2247
|
+
const route = member.provider && member.model ? ` · ${member.provider}/${member.model}` : '';
|
|
2248
|
+
const effort = member.reasoning_effort ? ` · reasoning ${member.reasoning_effort}` : '';
|
|
2249
|
+
return ` - ${member.name} [${member.role}] ${member.status}/${member.activity}${route}${effort}`;
|
|
2250
|
+
}),
|
|
2251
|
+
`Tasks (${team.tasks.length}):`,
|
|
2252
|
+
...team.tasks.map((task) => {
|
|
2253
|
+
const deps = task.dependencies.length > 0 ? ` (deps: ${task.dependencies.join(',')})` : '';
|
|
2254
|
+
const output = task.output !== undefined ? `\n output: ${task.output.slice(0, 300)}` : '';
|
|
2255
|
+
const handoff = task.reassigning ? ' (reassigning)' : '';
|
|
2256
|
+
const seed = task.seed_id === undefined || task.seed_id === '' ? '' : ` seed ${task.seed_id}`;
|
|
2257
|
+
const kind = task.kind ? ` ${task.kind}` : '';
|
|
2258
|
+
const round = task.round === undefined ? '' : ` r${task.round}`;
|
|
2259
|
+
const verdict = task.verdict === undefined ? '' : ` verdict ${task.verdict}`;
|
|
2260
|
+
return ` - ${task.id} [${task.status}]${kind}${round}${verdict} attempt ${task.attempt}${handoff}${seed} ${task.subject} → ${task.assignee || 'unassigned'}${deps}${output}`;
|
|
2261
|
+
}),
|
|
2262
|
+
...team.coverage === undefined || team.coverage.length === 0 ? [] : [
|
|
2263
|
+
'Coverage:',
|
|
2264
|
+
...team.coverage.map((row) => ` - ${row.goal_item}: ${row.status} (${row.task_ids.join(',') || 'none'})`),
|
|
2265
|
+
],
|
|
2266
|
+
...team.delivery === undefined ? [] : [
|
|
2267
|
+
`Delivery: ${team.delivery.ok ? 'ok' : `blocked (${team.delivery.blockers.join('; ')})`}`,
|
|
2268
|
+
],
|
|
2269
|
+
`Captain inbox (${team.captain_inbox.length}):`,
|
|
2270
|
+
...team.captain_inbox.map((message) => ` - [${message.from}] ${message.content.slice(0, 200)}`),
|
|
2271
|
+
];
|
|
2272
|
+
for (const [name, inbox] of Object.entries(team.member_inboxes)) {
|
|
2273
|
+
lines.push(`Member inbox ${name} (${inbox.count}): latest — ${inbox.latest.slice(0, 120)}`);
|
|
2274
|
+
}
|
|
2275
|
+
if (team.mailbox_warning_count > 0) {
|
|
2276
|
+
lines.push(`Mailbox warnings (${team.mailbox_warning_count}; malformed lines were skipped; showing up to 10):`, ...team.mailbox_warnings.map((warning) => ` - ${warning}`));
|
|
2277
|
+
}
|
|
2278
|
+
return lines.join('\n');
|
|
2279
|
+
}
|