From 2dbe97d5b6172737abd57ca34e5769e7db263df3 Mon Sep 17 00:00:00 2001 From: Stark-Han Date: Sat, 11 Jul 2026 08:15:10 +0800 Subject: [PATCH] fix: stabilize OpenClaw Lite Team collaboration and documentation (#162) * chore: ignore local agency and team snapshots * feat: add team agent profile templates * KANBAN * template * prototype * Fix tenant runtime pool deployment * Fix local team profiles runtime override * Update local team profiles testing guide * Adapt local team profiles test deployment * Patch tenant NFS server to ClusterIP * Use NFS export root for workspace mounts * Tighten team task completion projection * Fix team event body map comparison * before modification * temp_peer1 * leadermodel * team_fix * fix(team): harden workflow completion and workspace handling * fix(team): harden redis workflow protocol * discord link updated * chore: checkpoint team workflow fixes * fix: stabilize leader kanban ledger * fix: coalesce leader mediated work items * fix: confirm leader-mediated member results * fix: harden leader mediated team state * fix: stabilize team shared runtime state * Fix team task reconciliation and create flow * fix: stabilize leader mediated team orchestration * fix: refine team workspace presentation * checkpoint: persist team workflow ledger fixes * fix: reconcile team workflow delivery state * fix: preserve meaningful team chat events * docs: add team workspace quick guides * chore: ignore local team deployment files * fix: preserve setgid on team shared directories --- .gitignore | 4 + README.de.md | 15 +- README.ja.md | 15 +- README.ko.md | 15 +- README.md | 15 +- README.zh-CN.md | 15 +- backend/cmd/server/main.go | 2 +- .../migrations/036_add_team_event_outbox.sql | 19 + .../037_add_team_workflow_ledger.sql | 48 + backend/internal/db/migrations_test.go | 43 + .../handlers/runtime_agent_handler.go | 59 +- .../handlers/runtime_agent_handler_test.go | 147 +- backend/internal/models/team.go | 99 +- .../internal/repository/team_repository.go | 309 +- backend/internal/services/k8s/pvc_service.go | 9 +- .../internal/services/k8s/pvc_service_test.go | 9 +- .../internal/services/runtime_scheduler.go | 76 +- .../services/runtime_scheduler_test.go | 50 + backend/internal/services/team_redis.go | 9 + backend/internal/services/team_service.go | 4365 ++++++++++++++++- .../internal/services/team_service_test.go | 2614 +++++++++- docs/admin-user-guide.md | 3 + docs/main/team-create.png | Bin 0 -> 100463 bytes docs/main/team-workflow.jpg | Bin 0 -> 314449 bytes docs/team-workspaces-guide.md | 50 + docs/team-workspaces-guide_de.md | 50 + docs/team-workspaces-guide_en.md | 50 + docs/team-workspaces-guide_ja.md | 50 + docs/team-workspaces-guide_ko.md | 50 + docs/use_guide_cn.md | 10 + docs/use_guide_de.md | 10 + docs/use_guide_en.md | 10 + docs/use_guide_ja.md | 10 + docs/use_guide_ko.md | 10 + frontend/src/index.css | 130 + frontend/src/lib/teamTemplates.ts | 37 + frontend/src/pages/teams/CreateTeamPage.tsx | 1289 ++--- frontend/src/pages/teams/TeamDetailPage.tsx | 949 +++- frontend/src/types/team.ts | 22 + 39 files changed, 9192 insertions(+), 1475 deletions(-) create mode 100644 backend/internal/db/migrations/036_add_team_event_outbox.sql create mode 100644 backend/internal/db/migrations/037_add_team_workflow_ledger.sql create mode 100644 docs/main/team-create.png create mode 100644 docs/main/team-workflow.jpg create mode 100644 docs/team-workspaces-guide.md create mode 100644 docs/team-workspaces-guide_de.md create mode 100644 docs/team-workspaces-guide_en.md create mode 100644 docs/team-workspaces-guide_ja.md create mode 100644 docs/team-workspaces-guide_ko.md diff --git a/.gitignore b/.gitignore index 4d5ed80..cf457db 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ Thumbs.db .codex-logs/ +.codex-temp/ .cache/ .VSCodeCounter/ .superpowers/ @@ -79,3 +80,6 @@ CLAUDE.md TEAM_IMAGE_DEPLOY_STEPS.md TEAM_PROFILES_LOCAL_TESTING.md /team-optimization-docs/ +/clawmanager-apply.sh +/clawmanager-team-profiles-test.yaml +/clawmanager-tenant.yaml diff --git a/README.de.md b/README.de.md index 30d21ab..a2f6fe7 100644 --- a/README.de.md +++ b/README.de.md @@ -110,16 +110,14 @@ Es eignet sich besonders fuer: ## Team Workspaces -Team Workspaces erweitern ClawManager von Einzelinstanz-Betrieb zu koordinierter Multi-Agent-Runtime-Verwaltung. Nutzer koennen ein Team erstellen, einen Leader und mehrere Member zuweisen und ClawManager die Bereitstellung der Member-Runtimes, die Injection der Kollaborationskonfiguration und die Sichtbarkeit von Tasks und Events in der Control Plane ueberlassen. +Team Workspaces bieten einen vereinfachten OpenClaw-Lite-Kollaborationsablauf: Rollenvorlage auswaehlen, Team erstellen und das Ziel im Team-Chat beschreiben. Der Leader plant, koordiniert die Mitglieder, sammelt Ergebnisse und liefert die finale Zusammenfassung. -Das aktuelle MVP konzentriert sich auf OpenClaw-Member-Orchestrierung und den Redis-Team-Bus-Loop: +- feste Leader-vermittelte Zusammenarbeit ohne Runtime- oder Ressourcenprofil je Mitglied +- integrierte Vorlagen fuer Auslieferung, Produkterkundung und Softwareentwicklung +- Team-Chat fuer Plan, Zuweisung, Fortschritt, Review, Lieferung und Zusammenfassung +- Execution Kanban fuer Gesamtaufgabe und aktuelle Mitgliederlieferungen -- One-Click-Team-Erstellung mit validiertem Leader/Member-Roster -- Member-Runtime-Pods mit Team-Rolle, Member-ID, Control-Plane-URL und Shared-Mount-Konfiguration -- Redis-basierte inbox-, events-, presence- und DLQ-Keys ueber kontrollierte Umgebungsvariablen und Secret-Referenzen -- Shared PVC unter `/team` fuer Kontext, Artefakte, Snapshots und Task-Ergebnisse -- Team-Detailansicht mit Leader-Desktop, Team-Chat, Member-Liste, Dispatch-Panel, Task-Fortschritt und Event-/Ergebnis-Historie -- DB-gestuetzte Team-, Member-, Task- und Event-Datensaetze, sodass Redis Message Bus bleibt und nicht zur Source of Truth wird +Siehe [Team Workspace Quick Guide](./docs/team-workspaces-guide_de.md) fuer Erstellung, Kollaborationsphasen und Ergebnisansicht. ## Runtime-Integrationen @@ -263,6 +261,7 @@ Siehe [Developer Guide (English)](./docs/developer-guide.md). ## Dokumentation - [Benutzerhandbuch](./docs/use_guide_de.md) +- [Team Workspace Quick Guide](./docs/team-workspaces-guide_de.md) - [Deployment Guide (English)](./docs/deployment.md) - [Admin and User Guide (English)](./docs/admin-user-guide.md) - [Agent Control Plane Guide (English)](./docs/agent-control-plane.md) diff --git a/README.ja.md b/README.ja.md index d10a7cd..5072c01 100644 --- a/README.ja.md +++ b/README.ja.md @@ -110,16 +110,14 @@ ClawManager は、AI エージェントインスタンスの運用を Kubernetes ## Team ワークスペース -Team ワークスペースは、ClawManager を単一インスタンス運用から複数 Agent の協調ランタイム管理へ拡張します。ユーザーは Team を作成し、1 人の Leader と複数のメンバーを割り当て、ClawManager にメンバー Runtime のプロビジョニング、協調設定の注入、タスクとイベント状態の可視化を任せることができます。 +Team ワークスペースは、簡素化された OpenClaw Lite の協働フローを提供します。ロールテンプレートを選び、Team を作成して、Team チャットで目標を説明するだけです。Leader が計画、メンバー調整、成果物収集、最終結果の提示を担当します。 -現在の MVP は、OpenClaw メンバーのオーケストレーションと Redis Team Bus のループに焦点を当てています。 +- メンバーごとの Runtime やリソースプリセットを設定せず、Leader 仲介型の協働に固定 +- デリバリー、製品探索、ソフトウェア開発向けの組み込みテンプレート +- 計画、割り当て、進捗、レビュー、成果物、最終統合を表示する Team チャット +- 総合タスク状態と現在の成果物を表示する Execution Kanban -- 検証済みの Leader / メンバー roster によるワンクリック Team 作成 -- Team ロール、メンバー ID、コントロールプレーン URL、共有マウント設定を持つメンバー Runtime Pod の作成 -- 管理された環境変数と Secret 参照による Redis inbox、events、presence、DLQ key の注入 -- コンテキスト、成果物、スナップショット、タスク結果のための共有 PVC を `/team` にマウント -- Leader デスクトップ、Team チャット、メンバー一覧、配布パネル、タスク進捗、イベント/結果履歴をまとめた Team 詳細ビュー -- Team、メンバー、タスク、イベントを DB の権威状態として保持し、Redis はメッセージバスとして扱う設計 +作成、協働段階、結果の確認方法は [Team Workspace Quick Guide](./docs/team-workspaces-guide_ja.md) を参照してください。 ## Runtime 連携 @@ -263,6 +261,7 @@ ClawManager は、React フロントエンド、Go バックエンド、状態 ## ドキュメント - [ユーザーガイド](./docs/use_guide_ja.md) +- [Team Workspace Quick Guide](./docs/team-workspaces-guide_ja.md) - [Deployment Guide (English)](./docs/deployment.md) - [Admin and User Guide (English)](./docs/admin-user-guide.md) - [Agent Control Plane Guide (English)](./docs/agent-control-plane.md) diff --git a/README.ko.md b/README.ko.md index 5005557..c1bac3f 100644 --- a/README.ko.md +++ b/README.ko.md @@ -110,16 +110,14 @@ ClawManager는 AI Agent 인스턴스 운영을 Kubernetes 위로 확장하고, ## Team 워크스페이스 -Team 워크스페이스는 ClawManager를 단일 인스턴스 운영에서 여러 Agent의 협업 런타임 관리로 확장합니다. 사용자는 Team을 생성하고 한 명의 Leader와 여러 멤버를 지정할 수 있으며, ClawManager는 멤버 Runtime 프로비저닝, 협업 설정 주입, 작업 및 이벤트 상태 가시화를 담당합니다. +Team 워크스페이스는 단순화된 OpenClaw Lite 협업 흐름을 제공합니다. 역할 템플릿을 고르고 Team을 만든 뒤 Team 채팅에 목표를 설명하면 됩니다. Leader가 계획 수립, 멤버 조율, 산출물 수집, 최종 결과 정리를 담당합니다. -현재 MVP는 OpenClaw 멤버 오케스트레이션과 Redis Team Bus 루프에 초점을 맞춥니다. +- 멤버별 Runtime 또는 리소스 프리셋 설정 없이 Leader 중개 협업으로 고정 +- 납품, 제품 탐색, 소프트웨어 엔지니어링을 위한 기본 템플릿 +- 계획, 배정, 진행, 검토, 산출물, 최종 종합을 보여 주는 Team 채팅 +- 전체 작업 상태와 현재 멤버 산출물을 보여 주는 Execution Kanban -- 검증된 Leader / 멤버 roster 기반 원클릭 Team 생성 -- Team 역할, 멤버 ID, 컨트롤 플레인 URL, 공유 마운트 설정이 포함된 멤버 Runtime Pod 생성 -- 관리되는 환경 변수와 Secret 참조를 통한 Redis inbox, events, presence, DLQ key 주입 -- 컨텍스트, 산출물, 스냅샷, 작업 결과를 위한 공유 PVC를 `/team`에 마운트 -- Leader 데스크톱, Team 채팅, 멤버 목록, 배포 패널, 작업 진행률, 이벤트/결과 이력을 모아 보여주는 Team 상세 화면 -- Team, 멤버, 작업, 이벤트를 DB의 권위 상태로 유지하고 Redis는 메시지 버스로 사용하는 설계 +생성 과정, 협업 단계, 결과 확인은 [Team Workspace Quick Guide](./docs/team-workspaces-guide_ko.md)를 참고하세요. ## Runtime 연동 @@ -263,6 +261,7 @@ ClawManager는 React 프런트엔드, Go 백엔드, 상태 저장용 MySQL, 그 ## 문서 - [사용자 가이드](./docs/use_guide_ko.md) +- [Team Workspace Quick Guide](./docs/team-workspaces-guide_ko.md) - [Deployment Guide (English)](./docs/deployment.md) - [Admin and User Guide (English)](./docs/admin-user-guide.md) - [Agent Control Plane Guide (English)](./docs/agent-control-plane.md) diff --git a/README.md b/README.md index 3296ff6..c07cb7c 100644 --- a/README.md +++ b/README.md @@ -110,16 +110,14 @@ It is designed for: ## Team Workspaces -Team workspaces extend ClawManager from single-instance operations into coordinated multi-agent runtime management. A user can create a Team, assign one leader and multiple members, and let ClawManager provision the member runtimes, inject collaboration configuration, and keep task and event state visible from the control plane. +Team Workspaces provide a simplified OpenClaw Lite collaboration flow: choose a role template, create the Team, and describe the goal in the Team chat. The Leader plans the work, coordinates members, collects deliveries, and publishes the final result. -The current MVP focuses on OpenClaw member orchestration and the Redis Team Bus loop: +- fixed Leader-mediated collaboration, without per-member runtime or resource-preset setup +- built-in templates for focused delivery, product discovery, and software engineering work +- Team chat for plans, assignments, progress, reviews, deliveries, and final synthesis +- Execution Kanban for the root-task state and current member deliveries -- one-click Team creation with a validated leader/member roster -- member runtime Pods created with Team role, member id, control-plane URL, and shared mount configuration -- Redis-backed inbox, events, presence, and DLQ keys injected through controlled environment and Secret references -- shared PVC mounted at `/team` for context, artifacts, snapshots, and task results -- Team detail view with leader desktop access, team chat, member roster, dispatch panel, task progress, and event/result history -- DB-backed Team, member, task, and event records so Redis remains the message bus, not the source of truth +See the [Team Workspace Quick Guide](./docs/team-workspaces-guide_en.md) for the creation flow, collaboration stages, and result viewing. ## Runtime Integrations @@ -263,6 +261,7 @@ See the [Developer Guide](./docs/developer-guide.md). ## Documentation - [User Guide](./docs/use_guide_en.md) +- [Team Workspace Quick Guide](./docs/team-workspaces-guide_en.md) - [Deployment Guide](./docs/deployment.md) - [Admin and User Guide](./docs/admin-user-guide.md) - [Agent Control Plane Guide](./docs/agent-control-plane.md) diff --git a/README.zh-CN.md b/README.zh-CN.md index 64e1821..6a02f65 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -110,16 +110,14 @@ ClawManager 将 AI Agent 实例的运行、治理与运维能力带到 Kubernete ## Team 工作空间 -Team 工作空间让 ClawManager 从单实例运维扩展到多 Agent 协作编排。用户可以创建一个 Team,指定一个 Leader 和多个成员,由 ClawManager 负责创建成员 Runtime、注入协作配置,并在控制面持续展示任务、事件和结果状态。 +Team 工作空间提供简化的 OpenClaw Lite 协作流程:选择角色模板、创建 Team,然后在团队群聊中描述目标即可。Leader 会负责制定计划、协调成员、收集交付并输出最终结果。 -当前 MVP 聚焦 OpenClaw 成员编排与 Redis Team Bus 闭环: +- 固定为 Leader 中介协作,无需逐个配置成员运行时或资源预设 +- 内置交付、产品探索和软件工程等成员模板 +- 团队群聊展示计划、派发、进度、验收、交付和最终汇总 +- Execution Kanban 展示总任务状态及当前成员交付 -- 一键创建 Team,并校验 Leader / 成员 roster -- 为成员 Runtime Pod 注入 Team 角色、成员 ID、控制面地址和共享目录配置 -- 通过受控环境变量和 Secret 引用注入 Redis inbox、events、presence 与 DLQ key -- 将共享 PVC 挂载到 `/team`,用于上下文、产物、快照和任务结果 -- Team 详情页集中展示 Leader 桌面、团队群聊、成员列表、调试派发、任务进度与事件结果 -- Team、成员、任务和事件以 DB 为权威状态,Redis 仅作为消息总线与短期 presence 通道 +参见 [Team 协作快速指南](./docs/team-workspaces-guide.md),了解创建、协作阶段和查看交付结果的流程。 ## Runtime 接入 @@ -267,6 +265,7 @@ ClawManager 是一个 Kubernetes 原生平台,包含 React 前端、Go 后端 ## 文档 - [用户指南](./docs/use_guide_cn.md) +- [Team 协作快速指南](./docs/team-workspaces-guide.md) - [Deployment Guide(英文)](./docs/deployment.md) - [Admin and User Guide(英文)](./docs/admin-user-guide.md) - [Agent Control Plane Guide(英文)](./docs/agent-control-plane.md) diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 619aef1..919f540 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -173,7 +173,7 @@ func main() { teamHandler := handlers.NewTeamHandler(teamService) workspaceFileHandler := handlers.NewWorkspaceFileHandler(instanceService, workspaceFileService, runtimeWorkspaceFileService) workspaceFileHandler.SetSkillRepository(skillRepo) - runtimeAgentHandler := handlers.NewRuntimeAgentHandler(cfg.Runtime, runtimePodRepo, bindingRepo, runtimeEvents) + runtimeAgentHandler := handlers.NewRuntimeAgentHandler(cfg.Runtime, runtimePodRepo, bindingRepo, instanceRepo, runtimeEvents) // Initialize WebSocket hub and handler wsHub := services.GetHub() diff --git a/backend/internal/db/migrations/036_add_team_event_outbox.sql b/backend/internal/db/migrations/036_add_team_event_outbox.sql new file mode 100644 index 0000000..d130317 --- /dev/null +++ b/backend/internal/db/migrations/036_add_team_event_outbox.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS team_event_outbox ( + id INT AUTO_INCREMENT PRIMARY KEY, + team_id INT NOT NULL, + source_event_id VARCHAR(255) NOT NULL, + destination VARCHAR(255) NOT NULL, + message_id VARCHAR(255) NOT NULL, + payload_json LONGTEXT NOT NULL, + status VARCHAR(30) NOT NULL DEFAULT 'pending', + attempts INT NOT NULL DEFAULT 0, + available_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_error TEXT NULL, + delivered_at TIMESTAMP NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, + UNIQUE KEY uk_team_event_outbox_message (team_id, destination, message_id), + INDEX idx_team_event_outbox_pending (status, available_at), + INDEX idx_team_event_outbox_team (team_id, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/internal/db/migrations/037_add_team_workflow_ledger.sql b/backend/internal/db/migrations/037_add_team_workflow_ledger.sql new file mode 100644 index 0000000..91546a7 --- /dev/null +++ b/backend/internal/db/migrations/037_add_team_workflow_ledger.sql @@ -0,0 +1,48 @@ +ALTER TABLE team_tasks + ADD COLUMN workflow_state VARCHAR(40) NOT NULL DEFAULT 'open' AFTER status, + ADD COLUMN plan_version BIGINT NOT NULL DEFAULT 0 AFTER workflow_state, + ADD COLUMN ledger_version BIGINT NOT NULL DEFAULT 0 AFTER plan_version, + ADD COLUMN current_phase_id VARCHAR(255) NULL AFTER ledger_version, + ADD COLUMN accepted_completion_id VARCHAR(255) NULL AFTER current_phase_id, + ADD UNIQUE KEY uk_team_tasks_accepted_completion (team_id, accepted_completion_id), + ADD INDEX idx_team_tasks_workflow (team_id, workflow_state, status); + +ALTER TABLE team_work_items + ADD COLUMN assignment_id VARCHAR(255) NULL AFTER work_id, + ADD COLUMN canonical_work_id VARCHAR(255) NULL AFTER assignment_id, + ADD COLUMN phase_id VARCHAR(255) NULL AFTER canonical_work_id, + ADD COLUMN revision INT NOT NULL DEFAULT 1 AFTER phase_id, + ADD COLUMN required_for_root BOOLEAN NOT NULL DEFAULT TRUE AFTER revision, + ADD COLUMN superseded_by VARCHAR(255) NULL AFTER required_for_root, + ADD COLUMN review_required BOOLEAN NOT NULL DEFAULT FALSE AFTER superseded_by, + ADD COLUMN validated_revision INT NULL AFTER review_required, + ADD INDEX idx_team_work_items_assignment (root_task_id, assignment_id, revision), + ADD INDEX idx_team_work_items_phase (root_task_id, phase_id, status); + +UPDATE team_work_items +SET assignment_id = work_id, + canonical_work_id = work_id, + phase_id = 'legacy' +WHERE assignment_id IS NULL OR canonical_work_id IS NULL OR phase_id IS NULL; + +CREATE TABLE IF NOT EXISTS team_workflow_phases ( + id INT AUTO_INCREMENT PRIMARY KEY, + team_id INT NOT NULL, + root_task_id INT NOT NULL, + phase_id VARCHAR(255) NOT NULL, + plan_version BIGINT NOT NULL DEFAULT 1, + sequence_no INT NOT NULL DEFAULT 0, + status VARCHAR(40) NOT NULL DEFAULT 'planned', + required_for_root BOOLEAN NOT NULL DEFAULT TRUE, + decision_required BOOLEAN NOT NULL DEFAULT FALSE, + depends_on_json LONGTEXT NULL, + next_phase_id VARCHAR(255) NULL, + completion_policy VARCHAR(80) NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (team_id) REFERENCES teams(id) ON DELETE CASCADE, + FOREIGN KEY (root_task_id) REFERENCES team_tasks(id) ON DELETE CASCADE, + UNIQUE KEY uk_team_workflow_phase (root_task_id, phase_id, plan_version), + INDEX idx_team_workflow_phase_state (root_task_id, plan_version, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/internal/db/migrations_test.go b/backend/internal/db/migrations_test.go index 642cda1..f12b39b 100644 --- a/backend/internal/db/migrations_test.go +++ b/backend/internal/db/migrations_test.go @@ -117,3 +117,46 @@ func TestMigration035HardensTeamEventProtocol(t *testing.T) { } } } + +func TestMigration036AddsReliableTeamEventOutbox(t *testing.T) { + raw, err := embeddedMigrations.ReadFile("migrations/036_add_team_event_outbox.sql") + if err != nil { + t.Fatalf("read migration 036: %v", err) + } + sql := string(raw) + for _, required := range []string{ + "CREATE TABLE IF NOT EXISTS team_event_outbox", + "uk_team_event_outbox_message", + "idx_team_event_outbox_pending", + "source_event_id", + "available_at", + } { + if !strings.Contains(sql, required) { + t.Fatalf("migration 036 must contain %s", required) + } + } +} + +func TestMigration037AddsTeamWorkflowLedger(t *testing.T) { + raw, err := embeddedMigrations.ReadFile("migrations/037_add_team_workflow_ledger.sql") + if err != nil { + t.Fatalf("read migration 037: %v", err) + } + sql := string(raw) + for _, required := range []string{ + "workflow_state", + "plan_version", + "ledger_version", + "accepted_completion_id", + "assignment_id", + "canonical_work_id", + "phase_id", + "required_for_root", + "CREATE TABLE IF NOT EXISTS team_workflow_phases", + "uk_team_workflow_phase", + } { + if !strings.Contains(sql, required) { + t.Fatalf("migration 037 must contain %s", required) + } + } +} diff --git a/backend/internal/handlers/runtime_agent_handler.go b/backend/internal/handlers/runtime_agent_handler.go index b29c66c..b671899 100644 --- a/backend/internal/handlers/runtime_agent_handler.go +++ b/backend/internal/handlers/runtime_agent_handler.go @@ -23,10 +23,11 @@ type runtimeEventPublisher interface { } type RuntimeAgentHandler struct { - cfg config.RuntimePoolConfig - podRepo repository.RuntimePodRepository - bindingRepo repository.InstanceRuntimeBindingRepository - events runtimeEventPublisher + cfg config.RuntimePoolConfig + podRepo repository.RuntimePodRepository + bindingRepo repository.InstanceRuntimeBindingRepository + instanceRepo repository.InstanceRepository + events runtimeEventPublisher } type runtimeAgentPodIdentity struct { @@ -88,12 +89,13 @@ type runtimeAgentGatewayReport struct { HealthAt *time.Time `json:"health_at,omitempty"` } -func NewRuntimeAgentHandler(cfg config.RuntimePoolConfig, podRepo repository.RuntimePodRepository, bindingRepo repository.InstanceRuntimeBindingRepository, events runtimeEventPublisher) *RuntimeAgentHandler { +func NewRuntimeAgentHandler(cfg config.RuntimePoolConfig, podRepo repository.RuntimePodRepository, bindingRepo repository.InstanceRuntimeBindingRepository, instanceRepo repository.InstanceRepository, events runtimeEventPublisher) *RuntimeAgentHandler { return &RuntimeAgentHandler{ - cfg: cfg, - podRepo: podRepo, - bindingRepo: bindingRepo, - events: events, + cfg: cfg, + podRepo: podRepo, + bindingRepo: bindingRepo, + instanceRepo: instanceRepo, + events: events, } } @@ -279,9 +281,9 @@ func (h *RuntimeAgentHandler) ReportGateways(c *gin.Context) { if binding == nil || binding.RuntimePodID != podID || binding.Generation != gateway.Generation { continue } - state := strings.TrimSpace(gateway.State) + state := strings.ToLower(strings.TrimSpace(gateway.State)) switch state { - case "running", "healthy": + case "running", "ready", "healthy": if err := h.bindingRepo.UpdateRunning(c.Request.Context(), gateway.InstanceID, gateway.Generation, strings.TrimSpace(gateway.GatewayID), gateway.GatewayPort, gateway.GatewayPID); err != nil { utils.HandleError(c, err) return @@ -292,6 +294,10 @@ func (h *RuntimeAgentHandler) ReportGateways(c *gin.Context) { return } } + if err := h.syncInstanceRuntimeState(c.Request.Context(), gateway, state); err != nil { + utils.HandleError(c, err) + return + } } h.publish(c.Request.Context(), "runtime_pod_gateways_reported", map[string]any{ "pod_id": podID, @@ -300,6 +306,37 @@ func (h *RuntimeAgentHandler) ReportGateways(c *gin.Context) { utils.Success(c, http.StatusOK, "Runtime gateway report accepted", nil) } +func (h *RuntimeAgentHandler) syncInstanceRuntimeState(ctx context.Context, gateway runtimeAgentGatewayReport, state string) error { + if h.instanceRepo == nil { + return nil + } + instanceState, message := instanceRuntimeStateFromGatewayReport(state, gateway.ErrorMessage) + return h.instanceRepo.UpdateRuntimeState(ctx, gateway.InstanceID, instanceState, gateway.Generation, message) +} + +func instanceRuntimeStateFromGatewayReport(state string, errorMessage *string) (string, *string) { + normalized := strings.ToLower(strings.TrimSpace(state)) + switch normalized { + case "running", "ready", "healthy": + return "running", nil + case "error", "failed", "failure", "errored": + if errorMessage != nil && strings.TrimSpace(*errorMessage) != "" { + msg := strings.TrimSpace(*errorMessage) + return "error", &msg + } + msg := "runtime gateway reported " + normalized + return "error", &msg + case "stopped", "deleted": + return "stopped", nil + default: + msg := "runtime gateway starting" + if errorMessage != nil && strings.TrimSpace(*errorMessage) != "" { + msg = strings.TrimSpace(*errorMessage) + } + return "creating", &msg + } +} + func (h *RuntimeAgentHandler) ReportSkills(c *gin.Context) { if !h.requireAgentToken(c) { return diff --git a/backend/internal/handlers/runtime_agent_handler_test.go b/backend/internal/handlers/runtime_agent_handler_test.go index ee825f9..718467c 100644 --- a/backend/internal/handlers/runtime_agent_handler_test.go +++ b/backend/internal/handlers/runtime_agent_handler_test.go @@ -18,7 +18,7 @@ import ( func TestRuntimeAgentHandlerRejectsInvalidToken(t *testing.T) { gin.SetMode(gin.TestMode) podRepo := &runtimeAgentHandlerPodRepo{} - handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, podRepo, &runtimeAgentHandlerBindingRepo{}, &runtimeAgentHandlerEvents{}) + handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, podRepo, &runtimeAgentHandlerBindingRepo{}, nil, &runtimeAgentHandlerEvents{}) router := gin.New() router.POST("/api/v1/runtime-agent/metrics/report", handler.ReportMetrics) @@ -43,7 +43,7 @@ func TestRuntimeAgentHandlerRegisterUsesConfiguredCapacity(t *testing.T) { handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{ AgentReportToken: "secret", MaxGatewaysPerPod: 33, - }, podRepo, &runtimeAgentHandlerBindingRepo{}, events) + }, podRepo, &runtimeAgentHandlerBindingRepo{}, nil, events) router := gin.New() router.POST("/api/v1/runtime-agent/register", handler.Register) @@ -94,7 +94,7 @@ func TestRuntimeAgentHandlerHeartbeatUsesConfiguredCapacity(t *testing.T) { handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{ AgentReportToken: "secret", MaxGatewaysPerPod: 44, - }, podRepo, &runtimeAgentHandlerBindingRepo{}, events) + }, podRepo, &runtimeAgentHandlerBindingRepo{}, nil, events) router := gin.New() router.POST("/api/v1/runtime-agent/heartbeat", handler.Heartbeat) @@ -133,7 +133,7 @@ func TestRuntimeAgentHandlerMetricsReportUpdatesPodAndPublishesEvent(t *testing. gin.SetMode(gin.TestMode) podRepo := &runtimeAgentHandlerPodRepo{} events := &runtimeAgentHandlerEvents{} - handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, podRepo, &runtimeAgentHandlerBindingRepo{}, events) + handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, podRepo, &runtimeAgentHandlerBindingRepo{}, nil, events) router := gin.New() router.POST("/api/v1/runtime-agent/metrics/report", handler.ReportMetrics) @@ -186,7 +186,7 @@ func TestRuntimeAgentHandlerGatewayReportOnlyUpdatesCurrentPodBinding(t *testing 12: {InstanceID: 12, RuntimePodID: 9, Generation: 3}, }, } - handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, &runtimeAgentHandlerPodRepo{}, bindingRepo, &runtimeAgentHandlerEvents{}) + handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, &runtimeAgentHandlerPodRepo{}, bindingRepo, nil, &runtimeAgentHandlerEvents{}) router := gin.New() router.POST("/api/v1/runtime-agent/gateways/report", handler.ReportGateways) @@ -213,6 +213,53 @@ func TestRuntimeAgentHandlerGatewayReportOnlyUpdatesCurrentPodBinding(t *testing } } +func TestRuntimeAgentHandlerGatewayReportSyncsInstanceRuntimeState(t *testing.T) { + gin.SetMode(gin.TestMode) + message := "gateway port 20000 is not listening" + bindingRepo := &runtimeAgentHandlerBindingRepo{ + bindings: map[int]*models.InstanceRuntimeBinding{ + 10: {InstanceID: 10, RuntimePodID: 9, Generation: 2}, + 11: {InstanceID: 11, RuntimePodID: 9, Generation: 2}, + 12: {InstanceID: 12, RuntimePodID: 9, Generation: 2}, + }, + } + instanceRepo := &runtimeAgentHandlerInstanceRepo{} + handler := NewRuntimeAgentHandler(config.RuntimePoolConfig{AgentReportToken: "secret"}, &runtimeAgentHandlerPodRepo{}, bindingRepo, instanceRepo, &runtimeAgentHandlerEvents{}) + + router := gin.New() + router.POST("/api/v1/runtime-agent/gateways/report", handler.ReportGateways) + + body := `{ + "pod_id": 9, + "gateways": [ + {"instance_id":10,"gateway_id":"gw-10","gateway_port":20010,"state":"healthy","generation":2}, + {"instance_id":11,"gateway_id":"gw-11","gateway_port":20011,"state":"error","generation":2,"error_message":"` + message + `"}, + {"instance_id":12,"gateway_id":"gw-12","gateway_port":20012,"state":"ready","generation":2} + ] + }` + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPost, "/api/v1/runtime-agent/gateways/report", bytes.NewBufferString(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-ClawManager-Agent-Token", "secret") + router.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s, want 200", rec.Code, rec.Body.String()) + } + if got := instanceRepo.statusByID[10]; got != "running" { + t.Fatalf("healthy gateway synced instance status = %q, want running", got) + } + if got := instanceRepo.statusByID[11]; got != "error" { + t.Fatalf("error gateway synced instance status = %q, want error", got) + } + if got := instanceRepo.statusByID[12]; got != "running" { + t.Fatalf("ready gateway synced instance status = %q, want running", got) + } + if instanceRepo.messageByID[11] == nil || *instanceRepo.messageByID[11] != message { + t.Fatalf("error message = %#v, want %q", instanceRepo.messageByID[11], message) + } +} + type runtimeAgentHandlerPodRepo struct { updatedPodID int64 lastMetrics repository.RuntimePodMetricsUpdate @@ -330,6 +377,96 @@ func (r *runtimeAgentHandlerBindingRepo) DeleteByInstanceIDAndReleaseSlot(ctx co return nil } +type runtimeAgentHandlerInstanceRepo struct { + statusByID map[int]string + generationByID map[int]int + messageByID map[int]*string +} + +func (r *runtimeAgentHandlerInstanceRepo) Create(instance *models.Instance) error { + return nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetByID(id int) (*models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetByAccessToken(accessToken string) (*models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetByAgentBootstrapToken(bootstrapToken string) (*models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetAll(offset, limit int) ([]models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) CountAll() (int, error) { + return 0, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetByUserID(userID int, offset, limit int) ([]models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) CountByUserID(userID int) (int, error) { + return 0, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) CountActiveByMode(ctx context.Context, mode string) (int, error) { + return 0, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) ExistsByUserIDAndName(userID int, name string) (bool, error) { + return false, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetAllRunning() ([]models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetV2DesiredRunning(ctx context.Context, limit int) ([]models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) GetV2Creating(ctx context.Context, limit int) ([]models.Instance, error) { + return nil, nil +} + +func (r *runtimeAgentHandlerInstanceRepo) UpdateRuntimeState(ctx context.Context, id int, status string, generation int, message *string) error { + if r.statusByID == nil { + r.statusByID = map[int]string{} + } + if r.generationByID == nil { + r.generationByID = map[int]int{} + } + if r.messageByID == nil { + r.messageByID = map[int]*string{} + } + r.statusByID[id] = status + r.generationByID[id] = generation + r.messageByID[id] = message + return nil +} + +func (r *runtimeAgentHandlerInstanceRepo) SetWorkspacePath(ctx context.Context, id int, workspacePath string) error { + return nil +} + +func (r *runtimeAgentHandlerInstanceRepo) UpdateWorkspaceUsage(ctx context.Context, id int, usageBytes int64) error { + return nil +} + +func (r *runtimeAgentHandlerInstanceRepo) Update(instance *models.Instance) error { + return nil +} + +func (r *runtimeAgentHandlerInstanceRepo) Delete(id int) error { + return nil +} + type runtimeAgentHandlerEvents struct { lastType string lastPayload any diff --git a/backend/internal/models/team.go b/backend/internal/models/team.go index c30de45..2051525 100644 --- a/backend/internal/models/team.go +++ b/backend/internal/models/team.go @@ -86,21 +86,26 @@ func (TeamMember) TableName() string { } type TeamTask struct { - ID int `db:"id,primarykey,autoincrement" json:"id"` - TeamID int `db:"team_id" json:"team_id"` - TargetMemberID int `db:"target_member_id" json:"target_member_id"` - CreatedBy *int `db:"created_by" json:"created_by,omitempty"` - MessageID string `db:"message_id" json:"message_id"` - Status string `db:"status" json:"status"` - PayloadJSON string `db:"payload_json" json:"-"` - ResultJSON *string `db:"result_json" json:"-"` - ErrorMessage *string `db:"error_message" json:"error_message,omitempty"` - RedisStreamID *string `db:"redis_stream_id" json:"redis_stream_id,omitempty"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - DispatchedAt *time.Time `db:"dispatched_at" json:"dispatched_at,omitempty"` - StartedAt *time.Time `db:"started_at" json:"started_at,omitempty"` - FinishedAt *time.Time `db:"finished_at" json:"finished_at,omitempty"` - UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + ID int `db:"id,primarykey,autoincrement" json:"id"` + TeamID int `db:"team_id" json:"team_id"` + TargetMemberID int `db:"target_member_id" json:"target_member_id"` + CreatedBy *int `db:"created_by" json:"created_by,omitempty"` + MessageID string `db:"message_id" json:"message_id"` + Status string `db:"status" json:"status"` + WorkflowState string `db:"workflow_state" json:"workflow_state"` + PlanVersion int64 `db:"plan_version" json:"plan_version"` + LedgerVersion int64 `db:"ledger_version" json:"ledger_version"` + CurrentPhaseID *string `db:"current_phase_id" json:"current_phase_id,omitempty"` + AcceptedCompletionID *string `db:"accepted_completion_id" json:"accepted_completion_id,omitempty"` + PayloadJSON string `db:"payload_json" json:"-"` + ResultJSON *string `db:"result_json" json:"-"` + ErrorMessage *string `db:"error_message" json:"error_message,omitempty"` + RedisStreamID *string `db:"redis_stream_id" json:"redis_stream_id,omitempty"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + DispatchedAt *time.Time `db:"dispatched_at" json:"dispatched_at,omitempty"` + StartedAt *time.Time `db:"started_at" json:"started_at,omitempty"` + FinishedAt *time.Time `db:"finished_at" json:"finished_at,omitempty"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } func (TeamTask) TableName() string { @@ -128,22 +133,72 @@ func (TeamEvent) TableName() string { } type TeamWorkItem struct { + ID int `db:"id,primarykey,autoincrement" json:"id"` + TeamID int `db:"team_id" json:"team_id"` + RootTaskID int `db:"root_task_id" json:"root_task_id"` + WorkID string `db:"work_id" json:"work_id"` + AssignmentID *string `db:"assignment_id" json:"assignment_id,omitempty"` + CanonicalWorkID *string `db:"canonical_work_id" json:"canonical_work_id,omitempty"` + PhaseID *string `db:"phase_id" json:"phase_id,omitempty"` + Revision int `db:"revision" json:"revision"` + RequiredForRoot bool `db:"required_for_root" json:"required_for_root"` + SupersededBy *string `db:"superseded_by" json:"superseded_by,omitempty"` + ReviewRequired bool `db:"review_required" json:"review_required"` + ValidatedRevision *int `db:"validated_revision" json:"validated_revision,omitempty"` + OwnerMemberID *int `db:"owner_member_id" json:"owner_member_id,omitempty"` + Title string `db:"title" json:"title"` + Status string `db:"status" json:"status"` + DependsOnJSON *string `db:"depends_on_json" json:"-"` + ResultJSON *string `db:"result_json" json:"-"` + ArtifactRefsJSON *string `db:"artifact_refs_json" json:"-"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + StartedAt *time.Time `db:"started_at" json:"started_at,omitempty"` + FinishedAt *time.Time `db:"finished_at" json:"finished_at,omitempty"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type TeamWorkflowPhase struct { ID int `db:"id,primarykey,autoincrement" json:"id"` TeamID int `db:"team_id" json:"team_id"` RootTaskID int `db:"root_task_id" json:"root_task_id"` - WorkID string `db:"work_id" json:"work_id"` - OwnerMemberID *int `db:"owner_member_id" json:"owner_member_id,omitempty"` - Title string `db:"title" json:"title"` + PhaseID string `db:"phase_id" json:"phase_id"` + PlanVersion int64 `db:"plan_version" json:"plan_version"` + SequenceNo int `db:"sequence_no" json:"sequence_no"` Status string `db:"status" json:"status"` + RequiredForRoot bool `db:"required_for_root" json:"required_for_root"` + DecisionRequired bool `db:"decision_required" json:"decision_required"` DependsOnJSON *string `db:"depends_on_json" json:"-"` - ResultJSON *string `db:"result_json" json:"-"` - ArtifactRefsJSON *string `db:"artifact_refs_json" json:"-"` + NextPhaseID *string `db:"next_phase_id" json:"next_phase_id,omitempty"` + CompletionPolicy *string `db:"completion_policy" json:"completion_policy,omitempty"` CreatedAt time.Time `db:"created_at" json:"created_at"` - StartedAt *time.Time `db:"started_at" json:"started_at,omitempty"` - FinishedAt *time.Time `db:"finished_at" json:"finished_at,omitempty"` + CompletedAt *time.Time `db:"completed_at" json:"completed_at,omitempty"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` } +func (TeamWorkflowPhase) TableName() string { + return "team_workflow_phases" +} + +type TeamEventOutbox struct { + ID int `db:"id,primarykey,autoincrement" json:"id"` + TeamID int `db:"team_id" json:"team_id"` + SourceEventID string `db:"source_event_id" json:"source_event_id"` + Destination string `db:"destination" json:"destination"` + MessageID string `db:"message_id" json:"message_id"` + PayloadJSON string `db:"payload_json" json:"-"` + Status string `db:"status" json:"status"` + Attempts int `db:"attempts" json:"attempts"` + AvailableAt time.Time `db:"available_at" json:"available_at"` + LastError *string `db:"last_error" json:"last_error,omitempty"` + DeliveredAt *time.Time `db:"delivered_at" json:"delivered_at,omitempty"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +func (TeamEventOutbox) TableName() string { + return "team_event_outbox" +} + func (TeamWorkItem) TableName() string { return "team_work_items" } diff --git a/backend/internal/repository/team_repository.go b/backend/internal/repository/team_repository.go index b694595..8cdf2b8 100644 --- a/backend/internal/repository/team_repository.go +++ b/backend/internal/repository/team_repository.go @@ -45,8 +45,17 @@ type TeamRepository interface { ListEventsBeforeID(teamID, beforeID, limit int) ([]models.TeamEvent, error) UpsertWorkItem(item *models.TeamWorkItem) error + InvalidateWorkItemReview(workItemID int, updatedAt time.Time) error ListWorkItemsByRootTaskID(rootTaskID int) ([]models.TeamWorkItem, error) ListWorkItemsByTeamID(teamID int, limit int) ([]models.TeamWorkItem, error) + UpsertWorkflowPhase(phase *models.TeamWorkflowPhase) error + ListWorkflowPhasesByRootTaskID(rootTaskID int) ([]models.TeamWorkflowPhase, error) + AcceptRootCompletion(task *models.TeamTask, expectedLedgerVersion int64, event *models.TeamEvent, outbox *models.TeamEventOutbox) (bool, error) + ConfirmWorkItemResult(item *models.TeamWorkItem, event *models.TeamEvent, outbox *models.TeamEventOutbox) error + CreateEventOutbox(outbox *models.TeamEventOutbox) error + ListPendingEventOutbox(now time.Time, limit int) ([]models.TeamEventOutbox, error) + MarkEventOutboxDelivered(id int, deliveredAt time.Time) error + MarkEventOutboxFailed(id int, availableAt time.Time, cause string) error } type teamRepository struct { @@ -304,6 +313,252 @@ func (r *teamRepository) CreateEvent(event *models.TeamEvent) error { return nil } +func (r *teamRepository) ConfirmWorkItemResult(item *models.TeamWorkItem, event *models.TeamEvent, outbox *models.TeamEventOutbox) error { + if item == nil || event == nil || outbox == nil { + return fmt.Errorf("work item, confirmation event and outbox are required") + } + return r.sess.Tx(func(sess db.Session) error { + txRepo := &teamRepository{sess: sess} + if err := txRepo.UpsertWorkItem(item); err != nil { + return err + } + if err := txRepo.CreateEvent(event); err != nil && !errors.Is(err, ErrDuplicateTeamEvent) { + return err + } + if outbox.Status == "" { + outbox.Status = "pending" + } + now := time.Now().UTC() + if outbox.AvailableAt.IsZero() { + outbox.AvailableAt = now + } + if outbox.CreatedAt.IsZero() { + outbox.CreatedAt = now + } + if outbox.UpdatedAt.IsZero() { + outbox.UpdatedAt = now + } + res, err := sess.Collection("team_event_outbox").Insert(outbox) + if err != nil { + if strings.Contains(err.Error(), "uk_team_event_outbox_message") { + return nil + } + return fmt.Errorf("failed to create team event outbox: %w", err) + } + if id, ok := res.ID().(int64); ok { + outbox.ID = int(id) + } + return nil + }) +} + +func (r *teamRepository) UpsertWorkflowPhase(phase *models.TeamWorkflowPhase) error { + if phase == nil || phase.RootTaskID <= 0 || strings.TrimSpace(phase.PhaseID) == "" { + return fmt.Errorf("team workflow phase is required") + } + if phase.PlanVersion <= 0 { + phase.PlanVersion = 1 + } + var existing models.TeamWorkflowPhase + err := r.sess.Collection("team_workflow_phases").Find(db.Cond{ + "root_task_id": phase.RootTaskID, + "phase_id": phase.PhaseID, + "plan_version": phase.PlanVersion, + }).One(&existing) + if err != nil && err != db.ErrNoMoreRows { + return fmt.Errorf("failed to get team workflow phase: %w", err) + } + if err == nil { + phase.ID = existing.ID + phase.CreatedAt = existing.CreatedAt + if phase.DependsOnJSON == nil { + phase.DependsOnJSON = existing.DependsOnJSON + } + if phase.NextPhaseID == nil { + phase.NextPhaseID = existing.NextPhaseID + } + if phase.CompletionPolicy == nil { + phase.CompletionPolicy = existing.CompletionPolicy + } + if phase.CompletedAt == nil { + phase.CompletedAt = existing.CompletedAt + } + if phase.UpdatedAt.IsZero() { + phase.UpdatedAt = time.Now().UTC() + } + if err := r.sess.Collection("team_workflow_phases").Find(db.Cond{"id": existing.ID}).Update(phase); err != nil { + return fmt.Errorf("failed to update team workflow phase: %w", err) + } + return nil + } + ensureTimestamps(&phase.CreatedAt, &phase.UpdatedAt) + res, err := r.sess.Collection("team_workflow_phases").Insert(phase) + if err != nil { + return fmt.Errorf("failed to create team workflow phase: %w", err) + } + if id, ok := res.ID().(int64); ok { + phase.ID = int(id) + } + return nil +} + +func (r *teamRepository) ListWorkflowPhasesByRootTaskID(rootTaskID int) ([]models.TeamWorkflowPhase, error) { + var phases []models.TeamWorkflowPhase + if err := r.sess.Collection("team_workflow_phases").Find(db.Cond{"root_task_id": rootTaskID}).OrderBy("plan_version", "sequence_no", "id").All(&phases); err != nil { + return nil, fmt.Errorf("failed to list team workflow phases: %w", err) + } + return phases, nil +} + +func (r *teamRepository) AcceptRootCompletion(task *models.TeamTask, expectedLedgerVersion int64, event *models.TeamEvent, outbox *models.TeamEventOutbox) (bool, error) { + if task == nil || event == nil || outbox == nil || task.ID <= 0 { + return false, fmt.Errorf("task, accepted completion event and outbox are required") + } + accepted := false + err := r.sess.Tx(func(sess db.Session) error { + result, err := sess.SQL().Exec(` +UPDATE team_tasks +SET status = ?, workflow_state = ?, plan_version = ?, ledger_version = ?, current_phase_id = ?, + accepted_completion_id = ?, result_json = ?, error_message = ?, finished_at = ?, updated_at = ? +WHERE id = ? AND ledger_version = ? AND accepted_completion_id IS NULL + AND status NOT IN (?, ?, ?) +`, + task.Status, + task.WorkflowState, + task.PlanVersion, + task.LedgerVersion, + task.CurrentPhaseID, + task.AcceptedCompletionID, + task.ResultJSON, + task.ErrorMessage, + task.FinishedAt, + task.UpdatedAt, + task.ID, + expectedLedgerVersion, + models.TeamTaskStatusSucceeded, + models.TeamTaskStatusFailed, + models.TeamTaskStatusStale, + ) + if err != nil { + return fmt.Errorf("failed to atomically accept team completion: %w", err) + } + affected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to inspect accepted team completion: %w", err) + } + if affected != 1 { + return nil + } + accepted = true + if _, err := sess.SQL().Exec(` +UPDATE team_workflow_phases +SET status = 'completed', completed_at = ?, updated_at = ? +WHERE root_task_id = ? AND (? = 0 OR plan_version = ?) + AND status NOT IN ('cancelled', 'superseded') +`, task.UpdatedAt, task.UpdatedAt, task.ID, task.PlanVersion, task.PlanVersion); err != nil { + return fmt.Errorf("failed to complete team workflow phases: %w", err) + } + txRepo := &teamRepository{sess: sess} + if err := txRepo.CreateEvent(event); err != nil && !errors.Is(err, ErrDuplicateTeamEvent) { + return err + } + if outbox.Status == "" { + outbox.Status = "pending" + } + now := time.Now().UTC() + if outbox.AvailableAt.IsZero() { + outbox.AvailableAt = now + } + if outbox.CreatedAt.IsZero() { + outbox.CreatedAt = now + } + if outbox.UpdatedAt.IsZero() { + outbox.UpdatedAt = now + } + insertResult, err := sess.Collection("team_event_outbox").Insert(outbox) + if err != nil { + if strings.Contains(err.Error(), "uk_team_event_outbox_message") { + return nil + } + return fmt.Errorf("failed to create completion acknowledgement outbox: %w", err) + } + if id, ok := insertResult.ID().(int64); ok { + outbox.ID = int(id) + } + return nil + }) + return accepted, err +} + +func (r *teamRepository) ListPendingEventOutbox(now time.Time, limit int) ([]models.TeamEventOutbox, error) { + if limit <= 0 { + limit = 100 + } + var rows []models.TeamEventOutbox + if err := r.sess.Collection("team_event_outbox").Find(db.Cond{ + "status": "pending", + "available_at <=": now, + }).OrderBy("available_at", "id").Limit(limit).All(&rows); err != nil { + return nil, fmt.Errorf("failed to list pending team event outbox: %w", err) + } + return rows, nil +} + +func (r *teamRepository) CreateEventOutbox(outbox *models.TeamEventOutbox) error { + if outbox == nil { + return fmt.Errorf("team event outbox is required") + } + if outbox.Status == "" { + outbox.Status = "pending" + } + now := time.Now().UTC() + if outbox.AvailableAt.IsZero() { + outbox.AvailableAt = now + } + if outbox.CreatedAt.IsZero() { + outbox.CreatedAt = now + } + if outbox.UpdatedAt.IsZero() { + outbox.UpdatedAt = now + } + res, err := r.sess.Collection("team_event_outbox").Insert(outbox) + if err != nil { + if strings.Contains(err.Error(), "uk_team_event_outbox_message") { + return nil + } + return fmt.Errorf("failed to create team event outbox: %w", err) + } + if id, ok := res.ID().(int64); ok { + outbox.ID = int(id) + } + return nil +} + +func (r *teamRepository) MarkEventOutboxDelivered(id int, deliveredAt time.Time) error { + if id <= 0 { + return nil + } + return r.sess.Collection("team_event_outbox").Find(db.Cond{"id": id}).Update(map[string]interface{}{ + "status": "delivered", + "delivered_at": deliveredAt, + "last_error": nil, + "updated_at": deliveredAt, + }) +} + +func (r *teamRepository) MarkEventOutboxFailed(id int, availableAt time.Time, cause string) error { + if id <= 0 { + return nil + } + return r.sess.Collection("team_event_outbox").Find(db.Cond{"id": id}).Update(map[string]interface{}{ + "status": "pending", + "attempts": db.Raw("attempts + 1"), + "available_at": availableAt, + "last_error": cause, + "updated_at": time.Now().UTC(), + }) +} + func (r *teamRepository) EventExistsByStreamID(teamID int, streamID string) (bool, error) { if streamID == "" { return false, nil @@ -375,8 +630,14 @@ func (r *teamRepository) UpsertWorkItem(item *models.TeamWorkItem) error { if err == nil { item.ID = existing.ID item.CreatedAt = existing.CreatedAt - if existing.Status == models.TeamTaskStatusSucceeded || existing.Status == models.TeamTaskStatusFailed { - if item.Status != models.TeamTaskStatusSucceeded && item.Status != models.TeamTaskStatusFailed { + newRevision := item.Revision > existing.Revision + existingTerminal := existing.Status == models.TeamTaskStatusSucceeded || existing.Status == models.TeamTaskStatusFailed || existing.Status == models.TeamTaskStatusStale + itemTerminal := item.Status == models.TeamTaskStatusSucceeded || item.Status == models.TeamTaskStatusFailed || item.Status == models.TeamTaskStatusStale + reopeningCurrent := !newRevision && existingTerminal && !itemTerminal && + !item.UpdatedAt.IsZero() && + (existing.UpdatedAt.IsZero() || item.UpdatedAt.After(existing.UpdatedAt)) + if !newRevision && existingTerminal && !reopeningCurrent { + if !itemTerminal { item.Status = existing.Status } } @@ -386,16 +647,37 @@ func (r *teamRepository) UpsertWorkItem(item *models.TeamWorkItem) error { if item.StartedAt == nil { item.StartedAt = existing.StartedAt } - if item.FinishedAt == nil { + if item.FinishedAt == nil && !newRevision && !reopeningCurrent { item.FinishedAt = existing.FinishedAt } if item.DependsOnJSON == nil { item.DependsOnJSON = existing.DependsOnJSON } - if item.ResultJSON == nil { + if item.AssignmentID == nil { + item.AssignmentID = existing.AssignmentID + } + if item.CanonicalWorkID == nil { + item.CanonicalWorkID = existing.CanonicalWorkID + } + if item.PhaseID == nil { + item.PhaseID = existing.PhaseID + } + if item.Revision <= 0 { + item.Revision = existing.Revision + } + if item.SupersededBy == nil && !reopeningCurrent { + item.SupersededBy = existing.SupersededBy + } + if item.ValidatedRevision == nil && !newRevision && !reopeningCurrent { + item.ValidatedRevision = existing.ValidatedRevision + } + if existing.ReviewRequired && !newRevision && !reopeningCurrent { + item.ReviewRequired = true + } + if item.ResultJSON == nil && !newRevision && !reopeningCurrent { item.ResultJSON = existing.ResultJSON } - if item.ArtifactRefsJSON == nil { + if item.ArtifactRefsJSON == nil && !newRevision && !reopeningCurrent { item.ArtifactRefsJSON = existing.ArtifactRefsJSON } if item.UpdatedAt.IsZero() { @@ -417,6 +699,23 @@ func (r *teamRepository) UpsertWorkItem(item *models.TeamWorkItem) error { return nil } +func (r *teamRepository) InvalidateWorkItemReview(workItemID int, updatedAt time.Time) error { + if workItemID <= 0 { + return fmt.Errorf("team work item id is required") + } + if updatedAt.IsZero() { + updatedAt = time.Now().UTC() + } + if _, err := r.sess.SQL().Exec(` +UPDATE team_work_items +SET validated_revision = NULL, updated_at = ? +WHERE id = ? AND review_required = TRUE AND validated_revision IS NOT NULL +`, updatedAt, workItemID); err != nil { + return fmt.Errorf("failed to invalidate team work item review: %w", err) + } + return nil +} + func (r *teamRepository) ListWorkItemsByRootTaskID(rootTaskID int) ([]models.TeamWorkItem, error) { var items []models.TeamWorkItem if err := r.sess.Collection("team_work_items").Find(db.Cond{"root_task_id": rootTaskID}).OrderBy("id").All(&items); err != nil { diff --git a/backend/internal/services/k8s/pvc_service.go b/backend/internal/services/k8s/pvc_service.go index 20affd3..7d8b6ef 100644 --- a/backend/internal/services/k8s/pvc_service.go +++ b/backend/internal/services/k8s/pvc_service.go @@ -30,6 +30,9 @@ const ( storageProfileSingle = "single-node" storageProfileLegacyNFS = "legacy-nfs" defaultPVCBindTimeout = 2 * time.Minute + // os.FileMode keeps setgid outside the Unix permission-bit range, so + // 0o2775 alone does not set it when passed to os.Chmod. + teamSharedDirectoryMode = os.FileMode(0o775) | os.ModeSetgid ) // NewPVCService creates a new PVC service @@ -521,11 +524,11 @@ func (s *PVCService) ensureTeamSharedWorkspaceDirectory(userID, teamID int) erro dir := filepath.Join(root, filepath.FromSlash(TeamSharedWorkspaceRelativePath(userID, teamID))) dirs := append([]string{dir}, teamSharedRuntimeSubdirectories(dir)...) for _, target := range dirs { - if err := os.MkdirAll(target, 0o2775); err != nil { + if err := os.MkdirAll(target, teamSharedDirectoryMode); err != nil { return fmt.Errorf("failed to create Team shared runtime workspace directory %s: %w", target, err) } _ = os.Chown(target, 1000, 1000) - if err := os.Chmod(target, 0o2775); err != nil { + if err := os.Chmod(target, teamSharedDirectoryMode); err != nil { return fmt.Errorf("failed to chmod Team shared runtime workspace directory %s: %w", target, err) } } @@ -538,6 +541,8 @@ func teamSharedRuntimeSubdirectories(root string) []string { filepath.Join(root, "inbox"), filepath.Join(root, "results"), filepath.Join(root, "tasks"), + filepath.Join(root, ".openclaw-redis-team"), + filepath.Join(root, ".openclaw-redis-team", "tasks"), } } diff --git a/backend/internal/services/k8s/pvc_service_test.go b/backend/internal/services/k8s/pvc_service_test.go index 8f044a4..9950478 100644 --- a/backend/internal/services/k8s/pvc_service_test.go +++ b/backend/internal/services/k8s/pvc_service_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "runtime" "strings" "testing" "time" @@ -251,7 +252,7 @@ func TestCreateTeamSharedPVCCreatesWritableRuntimeSubdirectories(t *testing.T) { t.Fatalf("CreateTeamSharedPVC returned error: %v", err) } - for _, name := range []string{"status", "inbox", "results", "tasks"} { + for _, name := range []string{"status", "inbox", "results", "tasks", ".openclaw-redis-team", filepath.Join(".openclaw-redis-team", "tasks")} { dir := filepath.Join(workspaceRoot, "teams", "user-1", "team-28-shared", name) info, err := os.Stat(dir) if err != nil { @@ -260,6 +261,12 @@ func TestCreateTeamSharedPVCCreatesWritableRuntimeSubdirectories(t *testing.T) { if !info.IsDir() { t.Fatalf("expected %s to be a directory", dir) } + if runtime.GOOS != "windows" { + mode := info.Mode() + if mode&os.ModeSetgid == 0 || mode.Perm()&0o020 == 0 { + t.Fatalf("expected %s to preserve shared group-write/setgid permissions, got mode %v", dir, mode) + } + } } } diff --git a/backend/internal/services/runtime_scheduler.go b/backend/internal/services/runtime_scheduler.go index a4dbf6c..60ea429 100644 --- a/backend/internal/services/runtime_scheduler.go +++ b/backend/internal/services/runtime_scheduler.go @@ -507,6 +507,9 @@ func (s *RuntimeScheduler) reconcile(ctx context.Context) error { continue } if binding != nil { + if err := s.syncInstanceStateFromBinding(ctx, instance, binding); err != nil { + errs = append(errs, fmt.Errorf("sync creating instance %d from binding: %w", instance.ID, err)) + } continue } if err := s.assignInstance(ctx, instance); err != nil { @@ -536,6 +539,9 @@ func (s *RuntimeScheduler) reconcile(ctx context.Context) error { continue } if binding != nil { + if err := s.syncInstanceStateFromBinding(ctx, instance, binding); err != nil { + errs = append(errs, fmt.Errorf("sync desired instance %d from binding: %w", instance.ID, err)) + } continue } binding, err = s.bindingRepo.GetByInstanceID(ctx, instance.ID) @@ -544,6 +550,9 @@ func (s *RuntimeScheduler) reconcile(ctx context.Context) error { continue } if binding != nil { + if err := s.syncInstanceStateFromBinding(ctx, instance, binding); err != nil { + errs = append(errs, fmt.Errorf("sync desired instance %d from binding: %w", instance.ID, err)) + } continue } if assignErr := s.assignInstance(ctx, instance); assignErr != nil { @@ -558,6 +567,33 @@ func (s *RuntimeScheduler) reconcile(ctx context.Context) error { return errors.Join(errs...) } +func (s *RuntimeScheduler) syncInstanceStateFromBinding(ctx context.Context, instance models.Instance, binding *models.InstanceRuntimeBinding) error { + if s == nil || s.instanceRepo == nil || binding == nil { + return nil + } + state := strings.ToLower(strings.TrimSpace(binding.State)) + switch state { + case "running", "ready", "healthy": + if strings.EqualFold(strings.TrimSpace(instance.Status), "running") && instance.RuntimeErrorMessage == nil { + return nil + } + return s.instanceRepo.UpdateRuntimeState(ctx, instance.ID, "running", maxInt(instance.RuntimeGeneration, binding.Generation), nil) + case "error", "failed": + message := "runtime gateway failed" + if binding.ErrorMessage != nil && strings.TrimSpace(*binding.ErrorMessage) != "" { + message = strings.TrimSpace(*binding.ErrorMessage) + } + if strings.EqualFold(strings.TrimSpace(instance.Status), "error") && + instance.RuntimeErrorMessage != nil && + strings.TrimSpace(*instance.RuntimeErrorMessage) == message { + return nil + } + return s.instanceRepo.UpdateRuntimeState(ctx, instance.ID, "error", maxInt(instance.RuntimeGeneration, binding.Generation), &message) + default: + return nil + } +} + func (s *RuntimeScheduler) failoverStalePods(ctx context.Context) error { if s.heartbeatTimeout <= 0 { return nil @@ -745,6 +781,7 @@ func (s *RuntimeScheduler) createGatewayOnPod(ctx context.Context, instance mode } now := time.Now().UTC() + gatewayState := normalizeRuntimeGatewayCreateState(resp.Status) binding := &models.InstanceRuntimeBinding{ InstanceID: instance.ID, RuntimePodID: pod.ID, @@ -753,9 +790,11 @@ func (s *RuntimeScheduler) createGatewayOnPod(ctx context.Context, instance mode GatewayPort: resp.Port, GatewayPID: resp.PID, WorkspacePath: workspacePath, - State: "running", + State: gatewayState, Generation: instance.RuntimeGeneration, - LastHealthAt: &now, + } + if gatewayState == "running" { + binding.LastHealthAt = &now } if err := s.bindingRepo.Create(ctx, binding); err != nil { return s.cleanupGatewayAfterAssignFailure(ctx, endpoint, instance.ID, resp.GatewayID, false, err) @@ -763,16 +802,29 @@ func (s *RuntimeScheduler) createGatewayOnPod(ctx context.Context, instance mode if err := s.instanceRepo.SetWorkspacePath(ctx, instance.ID, workspacePath); err != nil { return s.cleanupGatewayAfterAssignFailure(ctx, endpoint, instance.ID, resp.GatewayID, true, err) } - if err := s.instanceRepo.UpdateRuntimeState(ctx, instance.ID, "running", instance.RuntimeGeneration, nil); err != nil { + instanceState := "creating" + var stateMessage *string + if gatewayState == "running" { + instanceState = "running" + } else { + message := "runtime gateway starting" + stateMessage = &message + } + if err := s.instanceRepo.UpdateRuntimeState(ctx, instance.ID, instanceState, instance.RuntimeGeneration, stateMessage); err != nil { return s.cleanupGatewayAfterAssignFailure(ctx, endpoint, instance.ID, resp.GatewayID, true, err) } if s.events != nil { - if err := s.events.Publish(ctx, "runtime.instance.running", map[string]any{ + eventType := "runtime.instance.starting" + if gatewayState == "running" { + eventType = "runtime.instance.running" + } + if err := s.events.Publish(ctx, eventType, map[string]any{ "instance_id": instance.ID, "runtime_type": runtimeType, "runtime_pod_id": pod.ID, "gateway_id": resp.GatewayID, "gateway_port": resp.Port, + "gateway_state": gatewayState, "workspace_path": workspacePath, "generation": instance.RuntimeGeneration, }); err != nil { @@ -782,6 +834,15 @@ func (s *RuntimeScheduler) createGatewayOnPod(ctx context.Context, instance mode return nil } +func normalizeRuntimeGatewayCreateState(status string) string { + switch strings.ToLower(strings.TrimSpace(status)) { + case "running", "ready", "healthy": + return "running" + default: + return "starting" + } +} + func runtimeGatewayLinuxIDs(instanceID int, environment map[string]string) (int, int) { linuxID := RuntimeLinuxID(instanceID) if !strings.EqualFold(strings.TrimSpace(environment["CLAWMANAGER_TEAM_ENABLED"]), "true") { @@ -878,3 +939,10 @@ func minInt(a, b int) int { } return b } + +func maxInt(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/backend/internal/services/runtime_scheduler_test.go b/backend/internal/services/runtime_scheduler_test.go index 89c02ff..8ae017a 100644 --- a/backend/internal/services/runtime_scheduler_test.go +++ b/backend/internal/services/runtime_scheduler_test.go @@ -113,6 +113,9 @@ func TestRuntimeSchedulerAssignsCreatingInstanceToReadyPod(t *testing.T) { if got := len(events.published); got != 1 { t.Fatalf("published events = %d, want 1", got) } + if events.published[0].eventType != "runtime.instance.running" { + t.Fatalf("published event = %q, want runtime.instance.running", events.published[0].eventType) + } } func TestRuntimeSchedulerPassesGatewayEnvironmentToRuntimeAgent(t *testing.T) { @@ -197,6 +200,10 @@ func TestRuntimeSchedulerPassesGatewayEnvironmentToRuntimeAgent(t *testing.T) { if env["CLAWMANAGER_LLM_MODEL"] != `["auto","gpt-5.5"]` { t.Fatalf("CLAWMANAGER_LLM_MODEL = %q", env["CLAWMANAGER_LLM_MODEL"]) } + state := instanceRepo.runtimeStates[68] + if state.status != "creating" || state.generation != 4 || state.message == nil || !strings.Contains(*state.message, "gateway starting") { + t.Fatalf("runtime state after starting gateway = %+v, want creating with startup message", state) + } } func TestRuntimeSchedulerUsesTeamSharedGIDForTeamLiteGateway(t *testing.T) { @@ -739,6 +746,49 @@ func TestRuntimeSchedulerSkipsCreatingInstanceWithExistingBinding(t *testing.T) } } +func TestRuntimeSchedulerSyncsCreatingInstanceFromRunningBinding(t *testing.T) { + ctx := context.Background() + workspacePath := "/workspaces/openclaw/user-1/instance-25" + instanceRepo := newFakeRuntimeInstanceRepo() + instanceRepo.creating = []models.Instance{{ + ID: 25, + UserID: 1, + Type: RuntimeTypeOpenClaw, + RuntimeType: RuntimeBackendGateway, + InstanceMode: InstanceModeLite, + Status: "creating", + WorkspacePath: &workspacePath, + RuntimeGeneration: 2, + }} + bindingRepo := newFakeRuntimeBindingRepo() + bindingRepo.bindings[25] = &models.InstanceRuntimeBinding{ + InstanceID: 25, + RuntimeType: RuntimeTypeOpenClaw, + State: "running", + Generation: 2, + } + scheduler := NewRuntimeScheduler( + instanceRepo, + &fakeRuntimePodRepo{}, + bindingRepo, + &fakeRuntimeRolloutRepo{}, + &fakeRuntimeAgentClient{}, + NewRuntimeEventService(nil), + nil, + &fakeRuntimeDeploymentService{}, + time.Second, + ) + + if err := scheduler.reconcile(ctx); err != nil { + t.Fatalf("reconcile returned error: %v", err) + } + + state := instanceRepo.runtimeStates[25] + if state.status != "running" || state.generation != 2 || state.message != nil { + t.Fatalf("expected instance to sync to running from binding, got %+v", state) + } +} + func TestRuntimeSchedulerCleansUpGatewayAndReleasesSlotWhenBindingCreateFails(t *testing.T) { ctx := context.Background() endpoint := "http://agent.runtime" diff --git a/backend/internal/services/team_redis.go b/backend/internal/services/team_redis.go index 7856551..928de5b 100644 --- a/backend/internal/services/team_redis.go +++ b/backend/internal/services/team_redis.go @@ -84,6 +84,15 @@ func (b *redisBus) SetNX(ctx context.Context, key, value string, ttl time.Durati return reply == "OK", nil } +func (b *redisBus) Set(ctx context.Context, key, value string, ttl time.Duration) error { + args := []string{"SET", key, value} + if ttl > 0 { + args = append(args, "PX", fmt.Sprintf("%d", ttl.Milliseconds())) + } + _, err := b.do(ctx, args...) + return err +} + func (b *redisBus) Del(ctx context.Context, key string) error { _, err := b.do(ctx, "DEL", key) return err diff --git a/backend/internal/services/team_service.go b/backend/internal/services/team_service.go index 261c2ae..95f826e 100644 --- a/backend/internal/services/team_service.go +++ b/backend/internal/services/team_service.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "context" + "crypto/sha256" "encoding/json" "errors" "fmt" @@ -45,6 +46,8 @@ const ( defaultTeamTaskStaleTimeout = 30 * time.Minute teamTaskStaleSweepInterval = 30 * time.Second teamConsumerScanInterval = 10 * time.Second + teamAssignmentMonitorEvery = 3 * time.Minute + teamEventOutboxBatchSize = 100 initialLeaderTaskIntent = "team_bootstrap_introduction" teamTaskCompletionTool = "team_complete_task" @@ -57,6 +60,30 @@ const ( teamCommunicationModeFullMesh = "full_mesh" ) +const ( + teamWorkflowStatePlanning = "planning" + teamWorkflowStateExecuting = "executing" + teamWorkflowStateAwaitingPhaseResults = "awaiting_phase_results" + teamWorkflowStateAwaitingLeaderDecision = "awaiting_leader_decision" + teamWorkflowStateSynthesizing = "synthesizing" + teamWorkflowStateCompletionPending = "completion_pending" + teamWorkflowStateCompleted = "completed" + teamWorkflowStateFailed = "failed" + + teamPhaseStatusPlanned = "planned" + teamPhaseStatusActive = "active" + teamPhaseStatusAwaitingResults = "awaiting_results" + teamPhaseStatusAwaitingLeaderDecision = "awaiting_leader_decision" + teamPhaseStatusCompleted = "completed" + teamPhaseStatusCancelled = "cancelled" + teamPhaseStatusSuperseded = "superseded" + + teamCompletionDecisionAccepted = "accepted" + teamCompletionDecisionDeferred = "deferred" + teamCompletionDecisionNeedsConfirmation = "needs_confirmation" + teamCompletionDecisionRejected = "rejected" +) + var ( teamMemberKeyPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`) teamMemberInstanceNameInvalidChars = regexp.MustCompile(`[^a-z0-9-]+`) @@ -211,14 +238,15 @@ type teamService struct { configMapService *k8s.ConfigMapService podService *k8s.PodService - ctx context.Context - cancel context.CancelFunc - mu sync.Mutex - running bool - wg sync.WaitGroup - consumers map[int]struct{} - staleMonitorStarted bool - runtimeWorkspaceRoot string + ctx context.Context + cancel context.CancelFunc + mu sync.Mutex + running bool + wg sync.WaitGroup + consumers map[int]struct{} + assignmentMonitorLast map[string]time.Time + staleMonitorStarted bool + runtimeWorkspaceRoot string } type teamOpenClawConfigPlanner interface { @@ -262,16 +290,17 @@ func WithTeamOpenClawConfigService(service OpenClawConfigService) TeamServiceOpt func NewTeamService(repo repository.TeamRepository, instanceService InstanceService, opts ...TeamServiceOption) TeamService { ctx, cancel := context.WithCancel(context.Background()) service := &teamService{ - repo: repo, - instanceService: instanceService, - pvcService: k8s.NewPVCService(), - secretService: k8s.NewSecretService(), - configMapService: k8s.NewConfigMapService(), - podService: k8s.NewPodService(), - ctx: ctx, - cancel: cancel, - consumers: map[int]struct{}{}, - runtimeWorkspaceRoot: "/workspaces", + repo: repo, + instanceService: instanceService, + pvcService: k8s.NewPVCService(), + secretService: k8s.NewSecretService(), + configMapService: k8s.NewConfigMapService(), + podService: k8s.NewPodService(), + ctx: ctx, + cancel: cancel, + consumers: map[int]struct{}{}, + assignmentMonitorLast: map[string]time.Time{}, + runtimeWorkspaceRoot: "/workspaces", } for _, opt := range opts { if opt != nil { @@ -303,6 +332,7 @@ func (s *teamService) StartBackground(parent context.Context) { s.running = true s.staleMonitorStarted = false s.consumers = map[int]struct{}{} + s.assignmentMonitorLast = map[string]time.Time{} s.wg.Add(1) go s.consumerScanLoop(ctx) s.mu.Unlock() @@ -542,8 +572,17 @@ func buildTeamTaskEnvelope(teamID int, memberKey string, task *models.TeamTask, taskPayload = map[string]interface{}{} } taskID := 0 + workflowState := teamWorkflowStatePlanning + planVersion := int64(0) + ledgerVersion := int64(0) if task != nil { taskID = task.ID + workflowState = task.WorkflowState + if workflowState == "" { + workflowState = teamWorkflowStatePlanning + } + planVersion = task.PlanVersion + ledgerVersion = task.LedgerVersion } taskRef := fmt.Sprintf("team-%d-task-%d", teamID, taskID) prompt := eventString(taskPayload, "prompt", "goal", "instruction", "instructions") @@ -554,8 +593,13 @@ func buildTeamTaskEnvelope(teamID int, memberKey string, task *models.TeamTask, rawPrompt := prompt prompt = buildTeamRuntimePrompt(prompt, memberContext) intent := eventString(taskPayload, "intent") + responseLocale := eventString(taskPayload, "responseLocale", "response_locale") + if responseLocale == "" { + responseLocale = inferTeamResponseLocale(rawPrompt) + } envelope := map[string]interface{}{ "v": 1, + "protocolVersion": 3, "messageId": messageID, "teamId": strconv.Itoa(teamID), "from": "clawmanager", @@ -566,7 +610,7 @@ func buildTeamTaskEnvelope(teamID int, memberKey string, task *models.TeamTask, "resultSink": map[string]interface{}{ "type": "redis_stream", "eventsKey": teamEventsKey(teamID), - "successEvent": "task_completed", + "successEvent": "completion_proposed", "failureEvent": "task_failed", "replyEvent": "reply", "resultField": "resultMarkdown", @@ -574,23 +618,47 @@ func buildTeamTaskEnvelope(teamID int, memberKey string, task *models.TeamTask, "artifactField": "artifactRefs", "completionTool": teamTaskCompletionTool, }, - "intent": intent, - "taskId": taskRef, - "title": eventString(taskPayload, "title"), - "prompt": appendTeamTaskCompletionInstruction(prompt, memberContext["communicationMode"], intent), - "rawPrompt": rawPrompt, - "contextRefs": normalizeContextRefs(taskPayload["contextRefs"]), - "memberContext": memberContext, - "systemPrompt": memberContext["systemPrompt"], - "metadata": taskPayload, - "createdAt": now.Format(time.RFC3339Nano), + "intent": intent, + "taskId": taskRef, + "title": eventString(taskPayload, "title"), + "prompt": appendTeamResponseLocaleInstruction(appendTeamTaskCompletionInstruction(prompt, memberContext["communicationMode"], intent), responseLocale), + "rawPrompt": rawPrompt, + "responseLocale": responseLocale, + "workflowState": workflowState, + "planVersion": planVersion, + "ledgerVersion": ledgerVersion, + "contextRefs": normalizeContextRefs(taskPayload["contextRefs"]), + "memberContext": memberContext, + "systemPrompt": memberContext["systemPrompt"], + "monitorPolicy": defaultTeamMonitorPolicy(), + "metadata": taskPayload, + "createdAt": now.Format(time.RFC3339Nano), } if workspaceContract, ok := taskPayload["workspaceContract"]; ok { envelope["workspaceContract"] = workspaceContract + if contract, ok := workspaceContract.(map[string]interface{}); ok { + physicalRoot := eventString(contract, "physicalSharedDir") + taskRef := eventString(contract, "taskRef") + memberArtifactPhysicalRoot := "" + if physicalRoot != "" && taskRef != "" { + memberArtifactPhysicalRoot = filepath.ToSlash(filepath.Join(physicalRoot, "artifacts", taskRef, "members", normalizeTeamMemberRouteKey(memberKey))) + } + envelope["sharedWorkspace"] = map[string]interface{}{ + "physicalPath": physicalRoot, + "canonicalPrefix": "/team", + "memberArtifactPhysicalRoot": memberArtifactPhysicalRoot, + "memberArtifactCanonicalRoot": "/team/artifacts/" + taskRef + "/members/" + normalizeTeamMemberRouteKey(memberKey), + } + } } if bootstrapSnapshot, ok := taskPayload["bootstrapSnapshot"]; ok { envelope["bootstrapSnapshot"] = bootstrapSnapshot } + if teamConfigJSON, ok := taskPayload["teamConfigJson"]; ok { + envelope["teamConfigJson"] = teamConfigJSON + } else if teamConfigJSON, ok := taskPayload["teamConfigJSON"]; ok { + envelope["teamConfigJson"] = teamConfigJSON + } if envelope["intent"] == "" { envelope["intent"] = "run_task" } @@ -600,14 +668,87 @@ func buildTeamTaskEnvelope(teamID int, memberKey string, task *models.TeamTask, return envelope } +func inferTeamResponseLocale(text string) string { + for _, r := range text { + if r >= '\u3400' && r <= '\u9fff' { + return "zh-CN" + } + } + return "en-US" +} + +func appendTeamResponseLocaleInstruction(prompt, locale string) string { + locale = strings.TrimSpace(locale) + if locale == "" { + locale = "zh-CN" + } + instruction := fmt.Sprintf("Response locale contract: use %s for every user-visible plan, assignment, progress summary, status-check summary, resultMarkdown, and final synthesis. Keep source code, API names, file names, and necessary technical terms in their original form. Do not rely on frontend translation.", locale) + if strings.TrimSpace(prompt) == "" { + return instruction + } + return strings.TrimSpace(prompt) + "\n\n" + instruction +} + +func applyTeamTaskEnvelopeContext(envelope map[string]interface{}, task *models.TeamTask, targetMemberKey string) { + if envelope == nil || task == nil { + return + } + payload := map[string]interface{}{} + if strings.TrimSpace(task.PayloadJSON) != "" { + _ = json.Unmarshal([]byte(task.PayloadJSON), &payload) + } + locale := eventString(payload, "responseLocale", "response_locale") + if locale == "" { + locale = inferTeamResponseLocale(eventString(payload, "prompt", "goal", "instruction", "instructions")) + } + envelope["responseLocale"] = locale + envelope["workflowState"] = task.WorkflowState + envelope["planVersion"] = task.PlanVersion + envelope["ledgerVersion"] = task.LedgerVersion + if task.CurrentPhaseID != nil { + envelope["currentPhaseId"] = *task.CurrentPhaseID + } + contract, _ := payload["workspaceContract"].(map[string]interface{}) + if contract == nil { + return + } + envelope["workspaceContract"] = contract + physicalRoot := eventString(contract, "physicalSharedDir") + taskRef := eventString(contract, "taskRef") + memberKey := normalizeTeamMemberRouteKey(targetMemberKey) + memberPhysicalRoot := "" + memberCanonicalRoot := "" + if physicalRoot != "" && taskRef != "" && memberKey != "" { + memberPhysicalRoot = filepath.ToSlash(filepath.Join(physicalRoot, "artifacts", taskRef, "members", memberKey)) + memberCanonicalRoot = "/team/artifacts/" + taskRef + "/members/" + memberKey + } + envelope["sharedWorkspace"] = map[string]interface{}{ + "physicalPath": physicalRoot, + "canonicalPrefix": "/team", + "memberArtifactPhysicalRoot": memberPhysicalRoot, + "memberArtifactCanonicalRoot": memberCanonicalRoot, + } +} + +func defaultTeamMonitorPolicy() map[string]interface{} { + return map[string]interface{}{ + "enabled": true, + "heartbeatEverySec": 30, + "visibleHeartbeatEverySec": int(teamAssignmentMonitorEvery.Seconds()), + "checkEverySec": int(teamAssignmentMonitorEvery.Seconds()), + "softTimeoutSec": int((2 * teamAssignmentMonitorEvery).Seconds()), + "visibleToChat": true, + } +} + func appendTeamTaskCompletionInstruction(prompt string, communicationMode, intent string) string { base := strings.TrimSpace(prompt) if strings.TrimSpace(intent) == initialLeaderTaskIntent { instruction := strings.Join([]string{ "Bootstrap completion contract:", "- This is a control-plane Team snapshot assigned only to the Leader. Do not delegate it, create worker assignments, or wait for member replies.", - "- Use the injected metadata.bootstrapSnapshot and metadata.workspaceContract first. They are the authoritative roster, runtime, role, and workspace facts for this snapshot.", - "- If you need to read the roster file, read $CLAWMANAGER_TEAM_CONFIG_PATH or /etc/clawmanager/team/team.json. Never look for /team/team.json or /team/members; /team is the shared artifact workspace, not the config mount.", + "- Use the injected metadata.bootstrapSnapshot, metadata.teamConfigJson, and metadata.workspaceContract first. They are the authoritative roster, runtime, role, and workspace facts for this snapshot.", + "- If injected metadata is insufficient, read /team/team.json from the shared workspace. $CLAWMANAGER_TEAM_CONFIG_PATH and /etc/clawmanager/team/team.json are optional system fallbacks and may be blocked by tool sandboxes. Never look for /team/members.", "- Do not probe member HTTP health endpoints, Redis CLI, or worker desktops for this bootstrap. If a runtime status source is unavailable in the injected snapshot, report that field as unavailable instead of blocking.", "- Summarize every member's identity, role, runtime, responsibilities, capability boundaries, and the configured collaboration mode.", "- Explain task routing, Team Redis event synchronization, shared workspace usage, and the available Team methods without asking other members to restate their own roles.", @@ -649,10 +790,19 @@ func appendTeamTaskCompletionInstruction(prompt string, communicationMode, inten instruction := strings.Join([]string{ "Completion contract:", "- For multi-member Teams, first write a compact collaboration plan: subtasks, owner member_id, dependency, expected artifact, and verification rule.", + "- Publish that plan to ClawManager with team_update_progress status=\"running\" and eventKind=\"leader_plan\" before dispatching worker assignments. Plans are process visibility, not completion.", }, "\n") instruction += "\n" + strings.Join(modeInstructions, "\n") instruction += "\n" + strings.Join([]string{ + "- Publish meaningful process updates with team_update_progress. Use eventKind=\"worker_plan\" for worker execution plans, \"worker_progress\" for milestones, and \"leader_synthesis\" while reconciling member outputs. Use \"assignment_check_result\" only when replying to a ClawManager Monitor envelope carrying a monitor checkId; ordinary progress must remain worker_progress.", + "- Prefer team_artifact_write, team_artifact_read, team_artifact_list, and team_artifact_mkdir for shared artifacts. These tools enforce current-Team path isolation and cooperative permissions.", + "- If a worker is still executing a long step, report concise progress and continue. If context was lost or an artifact path is wrong, report a recoverable blocker to the Leader instead of treating the root task as failed.", "- Every Team message must preserve rootTaskId/messageId context when available and must clearly state whether it is an assignment, peer request, progress update, result, review, blocker, or final synthesis.", + "- For multi-stage work, publish a structured leader_plan with planVersion and phases. Every team_send must carry a stable phaseId, assignmentId, workId, revision, required flag, and dependencies. Completing one phase never completes the user root task.", + "- The Leader may call team_complete_task for the root only after the workflow is sealed, remainingActions is empty, every required latest assignment and review is complete, and finalAnswerReady is true. Worker completion closes only that assignment.", + "- A failed or stale required assignment blocks root success unless the Leader supplies a structured waiver containing assignmentId, reason, and accepted risk. Never waive running/pending work or omit the risk record.", + "- Optional work does not need to succeed, but every omitted optional assignment must be listed in skippedAssignments with assignmentId and a concrete reason.", + "- Report verification truthfully. If browser/DOM verification did not run or failed, label it as unverified; a hand-written simulator or static inspection is not a browser pass. Any artifact change after review invalidates that review and requires fresh validation.", "- The Leader must not mark the root task succeeded after merely dispatching work. Final success requires returned member evidence plus Leader synthesis, or a truly direct self-contained answer.", "- Write shared artifacts under the exact directory in CLAWMANAGER_TEAM_SHARED_DIR. When using shell commands, always create files under \"$CLAWMANAGER_TEAM_SHARED_DIR/\".", "- Never create or report a relative team/... folder. The path team/... is invalid because ClawManager file browsing only resolves shared artifacts through the Team shared directory.", @@ -716,9 +866,38 @@ func (s *teamService) upsertTeamRosterConfig(userID int, team *models.Team, memb }); err != nil { return "", err } + if err := s.writeSharedTeamRosterConfig(userID, team, rosterJSON); err != nil { + return "", err + } return rosterJSON, nil } +func (s *teamService) writeSharedTeamRosterConfig(userID int, team *models.Team, rosterJSON string) error { + if s == nil || team == nil || strings.TrimSpace(rosterJSON) == "" { + return nil + } + root := filepath.Clean(s.teamRuntimeSharedPathFor(userID, team.ID)) + if root == "." || root == string(filepath.Separator) { + return fmt.Errorf("invalid Team shared workspace root for Team %d: %q", team.ID, root) + } + for _, rel := range []string{"", "results", "tasks", "inbox", "status", "artifacts", "tmp"} { + target := root + if rel != "" { + target = filepath.Join(root, rel) + } + if err := os.MkdirAll(target, 0o2775); err != nil { + return fmt.Errorf("failed to prepare Team shared workspace %s: %w", target, err) + } + _ = os.Chmod(target, 0o2775) + } + path := filepath.Join(root, teamConfigFileName) + if err := os.WriteFile(path, []byte(rosterJSON), 0o664); err != nil { + return fmt.Errorf("failed to write shared Team roster %s: %w", path, err) + } + _ = os.Chmod(path, 0o664) + return nil +} + func buildTeamRosterConfigData(rosterJSON string, team *models.Team, members []plannedTeamMember) map[string]string { data := map[string]string{ teamConfigFileName: rosterJSON, @@ -968,7 +1147,7 @@ func (s *teamService) GetTeam(userID, teamID int) (*TeamDetailsPayload, error) { if err != nil { return nil, err } - events, err := s.repo.ListEventsByTeamID(teamID, 50) + events, err := s.repo.ListEventsByTeamID(teamID, 200) if err != nil { return nil, err } @@ -1022,11 +1201,16 @@ func (s *teamService) ListTeamEvents(userID, teamID, beforeID, limit int) (*Team if hasMore { events = events[:limit] } + var nextBeforeID *int + if len(events) > 0 { + value := events[len(events)-1].ID + nextBeforeID = &value + } payload := teamEventPayloads(events) return &TeamEventsHistoryPayload{ Events: payload, HasMore: hasMore, - NextBeforeID: nextTeamEventBeforeID(payload), + NextBeforeID: nextBeforeID, }, nil } @@ -1308,6 +1492,10 @@ func (s *teamService) DispatchTask(userID, teamID int, req DispatchTeamTaskReque if req.Payload == nil { return nil, fmt.Errorf("task payload is required") } + if strings.TrimSpace(eventString(req.Payload, "responseLocale", "response_locale")) == "" { + prompt := eventString(req.Payload, "prompt", "goal", "instruction", "instructions") + req.Payload["responseLocale"] = inferTeamResponseLocale(prompt) + } if _, exists := req.Payload["origin"]; !exists { req.Payload["origin"] = "user_query" } @@ -1354,6 +1542,9 @@ func (s *teamService) DispatchTask(userID, teamID int, req DispatchTeamTaskReque CreatedBy: &userID, MessageID: messageID, Status: models.TeamTaskStatusPending, + WorkflowState: teamWorkflowStatePlanning, + PlanVersion: 0, + LedgerVersion: 0, PayloadJSON: payloadJSON, CreatedAt: now, UpdatedAt: now, @@ -1367,16 +1558,32 @@ func (s *teamService) DispatchTask(userID, teamID int, req DispatchTeamTaskReque return nil, err } - bus, err := s.redisBusForTeam(context.Background(), team) - if err != nil { - return nil, err - } taskPayload := map[string]interface{}{} if strings.TrimSpace(task.PayloadJSON) != "" { if err := json.Unmarshal([]byte(task.PayloadJSON), &taskPayload); err != nil { return nil, fmt.Errorf("failed to decode task payload: %w", err) } } + s.enrichTaskWorkspaceContract(userID, team, task, taskPayload) + enrichedPayloadJSON, err := marshalJSON(taskPayload) + if err != nil { + return nil, fmt.Errorf("failed to encode enriched task payload: %w", err) + } + if enrichedPayloadJSON != task.PayloadJSON { + task.PayloadJSON = enrichedPayloadJSON + task.UpdatedAt = time.Now().UTC() + if err := s.repo.UpdateTask(task); err != nil { + return nil, err + } + } + if strings.TrimSpace(eventString(taskPayload, "intent")) == initialLeaderTaskIntent && backendGeneratedBootstrapEnabled() { + return s.completeInitialLeaderTaskFromSnapshot(userID, team, task, member, taskPayload) + } + + bus, err := s.redisBusForTeam(context.Background(), team) + if err != nil { + return nil, err + } now := time.Now().UTC() memberInstance, _ := s.teamMemberInstance(member) memberContext := buildTeamMemberTaskContext(member, memberInstance) @@ -1431,10 +1638,12 @@ func (s *teamService) enrichBootstrapTaskPayload(userID int, team *models.Team, "sharedDir": team.SharedMountPath, "sharedDirEnv": "CLAWMANAGER_TEAM_SHARED_DIR", "physicalSharedDir": physicalSharedDir, + "sharedConfigPath": "/team/" + teamConfigFileName, + "fallbackConfigPaths": []string{"/team/" + teamConfigFileName}, "writeRoot": "$CLAWMANAGER_TEAM_SHARED_DIR", "canonicalReportPrefix": "/team", "validReportPathPattern": "/team/", - "invalidConfigPaths": []string{"/team/team.json", "/team/members"}, + "invalidConfigPaths": []string{"/team/members"}, "invalidArtifactPrefixes": []string{"team/"}, } memberSnapshots := make([]map[string]interface{}, 0, len(activeMembers)) @@ -1456,34 +1665,241 @@ func (s *teamService) enrichBootstrapTaskPayload(userID int, team *models.Team, }) } payload["workspaceContract"] = workspaceContract + payload["teamConfigJson"] = rosterJSON payload["bootstrapSnapshot"] = map[string]interface{}{ "teamId": team.ID, "teamName": team.Name, "communicationMode": normalizedTeamCommunicationMode(team.CommunicationMode), "sharedDir": team.SharedMountPath, "configPath": teamConfigMountPath, + "sharedConfigPath": "/team/" + teamConfigFileName, + "teamConfigJson": rosterJSON, "roster": roster, "members": memberSnapshots, } return nil } +func backendGeneratedBootstrapEnabled() bool { + raw := strings.TrimSpace(os.Getenv("TEAM_BOOTSTRAP_BACKEND_GENERATED")) + if raw == "" { + raw = strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_BACKEND_BOOTSTRAP")) + } + if raw == "" { + return true + } + switch strings.ToLower(raw) { + case "0", "false", "no", "off", "disabled": + return false + default: + return true + } +} + +func (s *teamService) completeInitialLeaderTaskFromSnapshot(userID int, team *models.Team, task *models.TeamTask, leader *models.TeamMember, taskPayload map[string]interface{}) (*TeamTaskPayload, error) { + if s == nil || team == nil || task == nil || leader == nil { + return nil, fmt.Errorf("bootstrap completion requires team, task and leader") + } + members, err := s.repo.ListMembersByTeamID(team.ID) + if err != nil { + return nil, err + } + activeMembers := activeTeamMembers(members) + now := time.Now().UTC() + taskRef := fmt.Sprintf("team-%d-task-%d", team.ID, task.ID) + reportRel := filepath.ToSlash(filepath.Join("results", taskRef, "team-introduction.md")) + reportPath := filepath.Join(filepath.Clean(s.teamRuntimeSharedPathFor(userID, team.ID)), filepath.FromSlash(reportRel)) + if err := ensureTeamWorkspaceDirectory(filepath.Dir(reportPath)); err != nil { + return nil, err + } + resultMarkdown := buildBackendBootstrapReport(team, activeMembers, taskRef, taskPayload) + if err := os.WriteFile(reportPath, []byte(resultMarkdown), 0o664); err != nil { + return nil, fmt.Errorf("failed to write bootstrap report: %w", err) + } + _ = os.Chmod(reportPath, 0o664) + chownTeamWorkspacePath(reportPath) + artifactRef := "/team/" + reportRel + summary := fmt.Sprintf("Team %s 启动快照完成,已生成成员与协作机制介绍。", team.Name) + completionID := fmt.Sprintf("clawmanager-bootstrap:%s", taskRef) + eventID := fmt.Sprintf("%s:completed", completionID) + eventPayload := map[string]interface{}{ + "v": 2, + "protocolVersion": 2, + "event": "task_completed", + "type": "task_completed", + "eventKind": "leader_synthesis", + "intent": initialLeaderTaskIntent, + "origin": "system_bootstrap", + "source": "clawmanager", + "completionSource": "clawmanager_backend", + "completionId": completionID, + "explicitCompletion": true, + "rootTaskTerminal": true, + "teamId": strconv.Itoa(team.ID), + "taskId": taskRef, + "rootTaskId": taskRef, + "messageId": task.MessageID, + "rootMessageId": task.MessageID, + "from": leader.MemberKey, + "memberId": leader.MemberKey, + "status": models.TeamTaskStatusSucceeded, + "runtimeStatus": models.TeamTaskStatusSucceeded, + "availability": models.TeamMemberAvailabilityIdle, + "summary": summary, + "result": resultMarkdown, + "resultMarkdown": resultMarkdown, + "artifactRefs": []string{artifactRef}, + "visibleToChat": true, + "backendGenerated": true, + } + payloadJSON, err := marshalOptionalJSON(eventPayload) + if err != nil { + return nil, err + } + task.Status = models.TeamTaskStatusSucceeded + task.StartedAt = &now + task.FinishedAt = &now + task.ResultJSON = payloadJSON + task.ErrorMessage = nil + task.UpdatedAt = now + if err := s.repo.UpdateTask(task); err != nil { + return nil, err + } + leader.Status = models.TeamMemberStatusIdle + leader.CurrentTaskID = nil + leader.Progress = 100 + leader.Availability = models.TeamMemberAvailabilityIdle + runtimeStatus := models.TeamTaskStatusSucceeded + leader.RuntimeStatus = &runtimeStatus + leader.RuntimeTaskID = &task.MessageID + runtimeIntent := initialLeaderTaskIntent + leader.RuntimeIntent = &runtimeIntent + leader.BlockedReason = nil + leader.LastSummary = &summary + leader.LastSeenAt = &now + leader.UpdatedAt = now + if err := s.repo.UpdateMember(leader); err != nil { + return nil, err + } + messageID := task.MessageID + event := &models.TeamEvent{ + EventID: &eventID, + CompletionID: &completionID, + TeamID: team.ID, + MemberID: &leader.ID, + TaskID: &task.ID, + MessageID: &messageID, + EventType: "task_completed", + PayloadJSON: payloadJSON, + OccurredAt: &now, + CreatedAt: now, + } + if err := s.repo.CreateEvent(event); err != nil && !errors.Is(err, repository.ErrDuplicateTeamEvent) { + return nil, err + } + return teamTaskPayload(*task) +} + +func buildBackendBootstrapReport(team *models.Team, members []models.TeamMember, taskRef string, taskPayload map[string]interface{}) string { + var b strings.Builder + mode := normalizedTeamCommunicationMode(team.CommunicationMode) + b.WriteString("# Team 启动介绍\n\n") + b.WriteString(fmt.Sprintf("- Team:%s(ID %d)\n", team.Name, team.ID)) + b.WriteString(fmt.Sprintf("- 任务:%s\n", taskRef)) + b.WriteString(fmt.Sprintf("- 协作模式:%s\n", mode)) + b.WriteString(fmt.Sprintf("- 共享目录:%s\n\n", team.SharedMountPath)) + b.WriteString("## 成员\n\n") + b.WriteString("| 成员 ID | 显示名称 | 角色 | Runtime | Mode | 状态 | 职责 |\n") + b.WriteString("| --- | --- | --- | --- | --- | --- | --- |\n") + for _, member := range members { + b.WriteString(fmt.Sprintf( + "| %s | %s | %s | %s | %s | %s/%s | %s |\n", + markdownTableCell(member.MemberKey), + markdownTableCell(member.DisplayName), + markdownTableCell(member.Role), + markdownTableCell(member.RuntimeType), + markdownTableCell(member.InstanceMode), + markdownTableCell(member.Status), + markdownTableCell(member.Availability), + markdownTableCell(derefTeamString(member.Description)), + )) + } + b.WriteString("\n## 协作机制\n\n") + b.WriteString("- Leader 负责拆解任务、派发成员、整合结果,并最终关闭根任务。\n") + b.WriteString("- 成员结果应回到 Leader,由 Leader 进行最终汇总。\n") + b.WriteString("- Redis Streams 用于任务分发、成员回报、过程事件和完成事件同步。\n") + b.WriteString("- NFS 共享目录用于耐久化产物、任务快照和可恢复状态,不作为实时状态唯一事实源。\n\n") + b.WriteString("## 共享工作区\n\n") + b.WriteString("- 规范路径前缀:`/team/`\n") + if contract, ok := taskPayload["workspaceContract"].(map[string]interface{}); ok { + if sharedDir := eventString(contract, "sharedDir"); sharedDir != "" { + b.WriteString(fmt.Sprintf("- 容器共享目录:`%s`\n", sharedDir)) + } + if physicalDir := eventString(contract, "physicalSharedDir"); physicalDir != "" { + b.WriteString(fmt.Sprintf("- 物理共享目录:`%s`\n", physicalDir)) + } + } + b.WriteString("- 成员产物建议写入 `/team/artifacts//members//`。\n") + b.WriteString("- 根任务最终报告建议写入 `/team/results//`。\n\n") + b.WriteString("## 团队可用方法\n\n") + b.WriteString("- `team_send`:Leader 向成员派发任务。\n") + b.WriteString("- `team_update_progress`:记录业务计划、阶段进度、长任务状态和检查反馈。\n") + b.WriteString("- `team_status`:查询成员和任务状态。\n") + b.WriteString("- `team_complete_task`:仅用于成员提交分配结果或 Leader 关闭根任务。\n") + b.WriteString("- `team_artifact_write/read/list/mkdir`:在当前 Team 共享目录内安全读写产物,自动限制路径并使用协作权限。\n") + return b.String() +} + +func markdownTableCell(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "-" + } + value = strings.ReplaceAll(value, "\r\n", " ") + value = strings.ReplaceAll(value, "\n", " ") + value = strings.ReplaceAll(value, "|", "\\|") + return value +} + func (s *teamService) prepareTeamTaskWorkspace(userID, teamID, taskID int) error { if s == nil || taskID <= 0 { return nil } root := filepath.Clean(s.teamRuntimeSharedPathFor(userID, teamID)) taskRef := fmt.Sprintf("team-%d-task-%d", teamID, taskID) - for _, rel := range []string{ + standardDirs := []string{ "", + "artifacts", + filepath.ToSlash(filepath.Join("artifacts", taskRef)), + filepath.ToSlash(filepath.Join("artifacts", taskRef, "members")), "results", + filepath.ToSlash(filepath.Join("results", taskRef)), + filepath.ToSlash(filepath.Join("results", taskRef, "members")), "tasks", + filepath.ToSlash(filepath.Join("tasks", taskRef)), "inbox", "status", - filepath.ToSlash(filepath.Join("results", taskRef)), - filepath.ToSlash(filepath.Join("tasks", taskRef)), filepath.ToSlash(filepath.Join("status", taskRef)), - } { + "tmp", + } + if s.repo != nil { + members, err := s.repo.ListMembersByTeamID(teamID) + if err != nil { + return err + } + for _, member := range activeTeamMembers(members) { + memberKey := normalizeTeamMemberRouteKey(member.MemberKey) + if memberKey == "" { + continue + } + standardDirs = append(standardDirs, + filepath.ToSlash(filepath.Join("artifacts", taskRef, "members", memberKey)), + filepath.ToSlash(filepath.Join("results", taskRef, "members", memberKey)), + filepath.ToSlash(filepath.Join("tmp", memberKey)), + ) + } + } + for _, rel := range standardDirs { target := root if rel != "" { target = filepath.Join(root, filepath.FromSlash(rel)) @@ -1495,6 +1911,53 @@ func (s *teamService) prepareTeamTaskWorkspace(userID, teamID, taskID int) error return nil } +func (s *teamService) enrichTaskWorkspaceContract(userID int, team *models.Team, task *models.TeamTask, payload map[string]interface{}) { + if s == nil || team == nil || task == nil || payload == nil { + return + } + taskRef := fmt.Sprintf("team-%d-task-%d", team.ID, task.ID) + physicalSharedDir := s.teamRuntimeSharedPathFor(userID, team.ID) + contract := map[string]interface{}{ + "configPath": teamConfigMountPath, + "configEnv": "CLAWMANAGER_TEAM_CONFIG_PATH", + "sharedDir": team.SharedMountPath, + "sharedDirEnv": "CLAWMANAGER_TEAM_SHARED_DIR", + "physicalSharedDir": physicalSharedDir, + "sharedConfigPath": "/team/" + teamConfigFileName, + "fallbackConfigPaths": []string{"/team/" + teamConfigFileName}, + "writeRoot": "$CLAWMANAGER_TEAM_SHARED_DIR", + "canonicalReportPrefix": "/team", + "validReportPathPattern": "/team/", + "invalidConfigPaths": []string{"/team/members"}, + "invalidArtifactPrefixes": []string{"team/"}, + "taskRef": taskRef, + "artifactRoot": "/team/artifacts/" + taskRef, + "memberArtifactRoot": "/team/artifacts/" + taskRef + "/members/${memberId}", + "memberArtifactPhysicalRoot": filepath.ToSlash(filepath.Join(physicalSharedDir, "artifacts", taskRef, "members", "${memberId}")), + "memberResultRoot": "/team/results/" + taskRef + "/members/${memberId}", + "leaderResultRoot": "/team/results/" + taskRef, + "statusRoot": "/team/status/" + taskRef, + "tmpRoot": "/team/tmp/${memberId}", + "statusFilesAreAdvisory": true, + "stateAuthority": "clawmanager_event_ledger", + "rules": []string{ + "Use /team/artifacts//members/// for member deliverables.", + "Use /team/results//members// for member result summaries.", + "Only the Leader writes the root final synthesis under /team/results//.", + "Shared status JSON files are compatibility snapshots; do not treat them as the task truth source.", + }, + } + if existing, ok := payload["workspaceContract"].(map[string]interface{}); ok { + for key, value := range contract { + if _, exists := existing[key]; !exists { + existing[key] = value + } + } + return + } + payload["workspaceContract"] = contract +} + func (s *teamService) teamMemberInstance(member *models.TeamMember) (*models.Instance, error) { if s == nil || s.instanceService == nil || member == nil || member.InstanceID == nil || *member.InstanceID <= 0 { return nil, nil @@ -1737,7 +2200,7 @@ func appendTeamCollaborationGuidance(systemPrompt, communicationMode string) str } func appendTeamWorkspaceGuidance(systemPrompt string) string { - guidance := "Shared workspace contract: write every shared artifact under the exact CLAWMANAGER_TEAM_SHARED_DIR value. When using shell commands, always create files under \"$CLAWMANAGER_TEAM_SHARED_DIR/\". Never create or report a relative team/... directory; team/... is invalid and may not be visible in ClawManager. When reporting an artifact to ClawManager or another member, use the canonical link /team/. The /team prefix is a UI/logical alias and may map to a different physical directory in Lite runtimes." + guidance := "Shared workspace contract: write every shared artifact under the exact CLAWMANAGER_TEAM_SHARED_DIR value. When using shell commands, always create files under \"$CLAWMANAGER_TEAM_SHARED_DIR/\". Never list, search, resolve, or scan the parent of CLAWMANAGER_TEAM_SHARED_DIR, and never inspect sibling Team directories. Never create or report a relative team/... directory; team/... is invalid and may not be visible in ClawManager. When reporting an artifact to ClawManager or another member, use the canonical link /team/. The /team prefix is a UI/logical alias and may map to a different physical directory in Lite runtimes." if strings.Contains(systemPrompt, guidance) { return systemPrompt } @@ -2103,11 +2566,14 @@ func (s *teamService) refreshTeamRosterConfig(userID int, team *models.Team) err IsLeader: isTeamLeaderRole(member.Role), }, normalizedTeamCommunicationMode(team.CommunicationMode)) } - return s.configMapService.UpsertConfigMap(context.Background(), userID, s.teamConfigMapName(team.ID), configData, map[string]string{ + if err := s.configMapService.UpsertConfigMap(context.Background(), userID, s.teamConfigMapName(team.ID), configData, map[string]string{ "app": "clawreef", "managed-by": "clawreef", "team-id": strconv.Itoa(team.ID), - }) + }); err != nil { + return err + } + return s.writeSharedTeamRosterConfig(userID, team, rosterJSON) } func (s *teamService) requireOwnedTeam(userID, teamID int) (*models.Team, error) { @@ -2207,13 +2673,103 @@ func (s *teamService) monitorStaleTasks(ctx context.Context) { case <-ctx.Done(): return case <-ticker.C: + if err := s.sweepTeamEventOutbox(); err != nil { + fmt.Printf("Warning: failed to deliver Team event outbox: %v\n", err) + } if err := s.sweepStaleTasks(); err != nil { fmt.Printf("Warning: failed to sweep stale Team tasks: %v\n", err) } + if err := s.sweepAssignmentStatusChecks(); err != nil { + fmt.Printf("Warning: failed to sweep Team assignment status checks: %v\n", err) + } } } } +func (s *teamService) sweepTeamEventOutbox() error { + if s == nil || s.repo == nil { + return nil + } + now := time.Now().UTC() + rows, err := s.repo.ListPendingEventOutbox(now, teamEventOutboxBatchSize) + if err != nil { + return err + } + var errs []error + for idx := range rows { + row := rows[idx] + team, getErr := s.repo.GetTeamByID(row.TeamID) + if getErr != nil { + errs = append(errs, getErr) + continue + } + if team == nil || team.Status == models.TeamStatusDeleted || team.Status == models.TeamStatusDeleting { + continue + } + bus, busErr := s.redisBusForTeam(context.Background(), team) + if busErr != nil { + _ = s.repo.MarkEventOutboxFailed(row.ID, now.Add(teamOutboxRetryDelay(row.Attempts)), busErr.Error()) + errs = append(errs, busErr) + continue + } + if deliverErr := s.deliverTeamEventOutbox(team, bus, &row); deliverErr != nil { + _ = s.repo.MarkEventOutboxFailed(row.ID, now.Add(teamOutboxRetryDelay(row.Attempts)), deliverErr.Error()) + errs = append(errs, deliverErr) + continue + } + if markErr := s.repo.MarkEventOutboxDelivered(row.ID, time.Now().UTC()); markErr != nil { + errs = append(errs, markErr) + } + } + return errors.Join(errs...) +} + +func teamOutboxRetryDelay(attempts int) time.Duration { + if attempts < 0 { + attempts = 0 + } + if attempts > 6 { + attempts = 6 + } + return time.Duration(1< 0 { + taskIDs[items[idx].RootTaskID] = struct{}{} + } + } + var errs []error + for taskID := range taskIDs { + task, err := s.repo.GetTaskByID(taskID) + if err != nil { + errs = append(errs, err) + continue + } + if task == nil || task.TeamID != team.ID || isTerminalTeamTaskStatus(task.Status) { + continue + } + leader := membersByID[task.TargetMemberID] + if leader == nil || !isLeaderTeamMember(leader) || !isActiveTeamMember(leader) { + continue + } + // Phase state is derived data. Repair it before applying the normal + // reminder age gate so a stuck workflow can advance without another + // Leader model round. + ledgerRepaired, repairErr := s.reconcileTeamWorkflowLedger(task, false, now) + if repairErr != nil { + errs = append(errs, repairErr) + continue + } + reconciled, reconcileErr := s.reconcileDeferredTeamCompletion(team, bus, task, leader) + if reconcileErr != nil { + errs = append(errs, reconcileErr) + continue + } + if reconciled || isTerminalTeamTaskStatus(task.Status) { + continue + } + if !ledgerRepaired && !task.UpdatedAt.IsZero() && task.UpdatedAt.After(cutoff) { + continue + } + ready, resultItems := leaderMediatedRootNeedsSynthesisReminder(task, items, membersByID) + if !ready { + continue + } + monitorKey := fmt.Sprintf("%d:%d:%s:%d", team.ID, task.ID, task.WorkflowState, task.LedgerVersion) + if !s.claimAssignmentMonitorSlot(monitorKey, now) { + continue + } + teamBus := bus + if teamBus == nil { + teamBus, err = s.redisBusForTeam(context.Background(), team) + if err != nil { + errs = append(errs, err) + continue + } + } + if err := s.createLeaderSynthesisReminder(team, teamBus, task, leader, resultItems, now); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func (s *teamService) reconcileDeferredTeamCompletion(team *models.Team, bus *redisBus, task *models.TeamTask, leader *models.TeamMember) (bool, error) { + if s == nil || team == nil || task == nil || leader == nil || isTerminalTeamTaskStatus(task.Status) { + return false, nil + } + events, err := s.repo.ListEventsByTeamID(team.ID, 500) + if err != nil { + return false, err + } + for idx := range events { + event := events[idx] + if event.TaskID == nil || *event.TaskID != task.ID || event.EventType != "completion_deferred" { + continue + } + payload := teamEventPayloadMap(event) + if !eventBool(payload, "explicitCompletion", "explicit_completion") || eventString(payload, "completionId", "completion_id") == "" { + continue + } + proposalPlanVersion := int64(eventInt(payload, "planVersion", "plan_version")) + if proposalPlanVersion > 0 && task.PlanVersion > 0 && proposalPlanVersion != task.PlanVersion { + // A completion report produced for an older plan cannot summarize work + // introduced by a later plan. Ask the Leader to synthesize again. + continue + } + if teamRedisProtocolVersion(payload) >= 3 && (!eventBool(payload, "workflowFinal", "workflow_final", "sealWorkflow", "seal_workflow") || + !eventBool(payload, "finalAnswerReady", "final_answer_ready") || + len(normalizeContextRefs(firstTeamValue(payload, "remainingActions", "remaining_actions", "nextActions", "next_actions"))) > 0) { + continue + } + // Only a current, explicitly sealed completion proposal may retire an + // unused planned phase. An older report must not mutate a newer plan. + if _, err := s.reconcileTeamWorkflowLedger(task, true, time.Now().UTC()); err != nil { + return false, err + } + payload["event"] = "completion_proposed" + payload["type"] = "completion_proposed" + payload["rootTaskTerminal"] = true + payload["status"] = models.TeamTaskStatusSucceeded + payload["runtimeStatus"] = models.TeamTaskStatusSucceeded + payload["eventId"] = fmt.Sprintf("completion-reconcile:%d:%d:%s:%d", team.ID, task.ID, normalizeTeamRedisKeyPart(eventString(payload, "completionId", "completion_id")), task.LedgerVersion) + payload["attemptId"] = fmt.Sprintf("reconcile:%d", task.LedgerVersion) + payload["ledgerVersion"] = task.LedgerVersion + payload["planVersion"] = task.PlanVersion + payload["memberId"] = leader.MemberKey + payload["taskId"] = fmt.Sprintf("team-%d-task-%d", team.ID, task.ID) + payload["rootTaskId"] = fmt.Sprintf("team-%d-task-%d", team.ID, task.ID) + delete(payload, "completionDecision") + delete(payload, "completionDecisionReason") + delete(payload, "pendingAssignments") + delete(payload, "pendingPhases") + encoded, err := json.Marshal(payload) + if err != nil { + return false, err + } + streamID := fmt.Sprintf("reconcile-%d-%d", task.ID, task.LedgerVersion) + if err := s.projectTeamEvent(team, bus, redisStreamMessage{ID: streamID, Fields: map[string]string{"payload": string(encoded)}}); err != nil { + return false, err + } + updated, err := s.repo.GetTaskByID(task.ID) + if err != nil { + return false, err + } + if updated != nil && updated.Status == models.TeamTaskStatusSucceeded { + *task = *updated + return true, nil + } + return false, nil + } + return false, nil +} + +func leaderMediatedRootNeedsSynthesisReminder(task *models.TeamTask, items []models.TeamWorkItem, membersByID map[int]*models.TeamMember) (bool, []models.TeamWorkItem) { + if task == nil || isTerminalTeamTaskStatus(task.Status) { + return false, nil + } + latest := map[string]models.TeamWorkItem{} + for idx := range items { + item := items[idx] + if item.RootTaskID != task.ID || item.SupersededBy != nil { + continue + } + key := derefTeamString(item.AssignmentID) + if key == "" { + key = item.WorkID + } + if current, ok := latest[key]; !ok || teamMaxInt(item.Revision, 1) >= teamMaxInt(current.Revision, 1) { + latest[key] = item + } + } + var resultItems []models.TeamWorkItem + for _, item := range latest { + owner := memberForWorkItem(item, membersByID) + if owner == nil { + continue + } + if isLeaderTeamMember(owner) { + if item.Status == models.TeamTaskStatusSucceeded && isLeaderFinalSynthesisWorkItem(item) { + return false, nil + } + continue + } + if !isActiveTeamMember(owner) { + continue + } + if !(item.RequiredForRoot || item.AssignmentID == nil) { + continue + } + switch item.Status { + case models.TeamTaskStatusSucceeded: + resultItems = append(resultItems, item) + case models.TeamTaskStatusFailed, models.TeamTaskStatusStale: + return false, nil + default: + return false, nil + } + } + sort.Slice(resultItems, func(i, j int) bool { + return resultItems[i].WorkID < resultItems[j].WorkID + }) + return len(resultItems) > 0, resultItems +} + +func memberForWorkItem(item models.TeamWorkItem, membersByID map[int]*models.TeamMember) *models.TeamMember { + if item.OwnerMemberID == nil || membersByID == nil { + return nil + } + return membersByID[*item.OwnerMemberID] +} + +func isLeaderFinalSynthesisWorkItem(item models.TeamWorkItem) bool { + text := strings.ToLower(strings.TrimSpace(item.WorkID + " " + item.Title)) + return strings.Contains(text, "leader") && + (strings.Contains(text, "final") || + strings.Contains(text, "synthesis") || + strings.Contains(text, "result") || + strings.Contains(text, "complete")) +} + +func (s *teamService) createLeaderSynthesisReminder(team *models.Team, bus *redisBus, task *models.TeamTask, leader *models.TeamMember, resultItems []models.TeamWorkItem, now time.Time) error { + if s == nil || s.repo == nil || team == nil || task == nil || leader == nil || len(resultItems) == 0 { + return nil + } + eventID := fmt.Sprintf("leader-workflow-reminder:%d:%d:%s:%d", team.ID, task.ID, normalizeTeamRedisKeyPart(task.WorkflowState), task.LedgerVersion) + exists, err := s.repo.EventExistsByEventID(team.ID, eventID) + if err != nil || exists { + return err + } + rootTaskRef := fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + memberResults := make([]map[string]interface{}, 0, len(resultItems)) + for idx := range resultItems { + item := resultItems[idx] + memberResult := map[string]interface{}{ + "workId": item.WorkID, + "title": item.Title, + "status": item.Status, + "summary": workItemResultSummary(item), + } + if item.OwnerMemberID != nil { + memberResult["ownerMemberId"] = *item.OwnerMemberID + } + if refs := workItemArtifactRefs(item); len(refs) > 0 { + memberResult["artifactRefs"] = refs + } + memberResults = append(memberResults, memberResult) + } + decisionReminder := task.WorkflowState == teamWorkflowStateAwaitingLeaderDecision || task.WorkflowState == teamWorkflowStateExecuting + summary := "All tracked member assignments have delivered results; Leader should synthesize the final answer and close the root task." + eventKind := "leader_synthesis_reminder" + intent := "leader_synthesis_reminder" + title := "Leader final synthesis requested" + if decisionReminder { + summary = "The current phase has delivered its results. Leader must decide whether to create the next phase or explicitly seal the workflow before final synthesis." + eventKind = "leader_decision_reminder" + intent = "leader_workflow_decision" + title = "Leader workflow decision requested" + } + payload := map[string]interface{}{ + "event": eventKind, + "type": eventKind, + "eventKind": eventKind, + "protocolVersion": 3, + "source": "clawmanager_monitor", + "nonAuthoritative": true, + "rootTaskTerminal": false, + "teamId": strconv.Itoa(team.ID), + "taskId": rootTaskRef, + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "messageId": eventID, + "memberId": leader.MemberKey, + "from": "clawmanager-monitor", + "to": leader.MemberKey, + "target": leader.MemberKey, + "workId": "leader-final-synthesis", + "assignmentId": "leader-final-synthesis", + "status": models.TeamTaskStatusRunning, + "runtimeStatus": models.TeamTaskStatusRunning, + "availability": models.TeamMemberAvailabilityBusy, + "summary": summary, + "workflowState": task.WorkflowState, + "planVersion": task.PlanVersion, + "ledgerVersion": task.LedgerVersion, + "visibleToChat": true, + "chatDigestEligible": true, + "dedupeKey": fmt.Sprintf("leader-synthesis:%d:%d", team.ID, task.ID), + "monitor": true, + "monitorType": eventKind, + "memberResults": memberResults, + "collaborationStep": map[string]interface{}{ + "type": "progress", + "status": models.TeamTaskStatusRunning, + "actor": "clawmanager-monitor", + "target": leader.MemberKey, + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "workId": "leader-final-synthesis", + "title": title, + "summary": summary, + "content": summary, + "source": "clawmanager_monitor", + }, + } + payloadJSON, err := marshalOptionalJSON(payload) + if err != nil { + return err + } + event := &models.TeamEvent{ + TeamID: team.ID, + TaskID: &task.ID, + MemberID: &leader.ID, + MessageID: &eventID, + EventID: &eventID, + EventType: eventKind, + PayloadJSON: payloadJSON, + OccurredAt: &now, + CreatedAt: now, + } + if err := s.repo.CreateEvent(event); err != nil && !errors.Is(err, repository.ErrDuplicateTeamEvent) { + return err + } + if bus == nil { + return nil + } + prompt := buildLeaderSynthesisReminderPrompt(task, resultItems, rootTaskRef) + if decisionReminder { + prompt = "Review the confirmed results for the current phase. If the root task requires another implementation, verification, review, or refinement phase, publish a new planVersion and dispatch those assignments now. If no required action remains, explicitly seal the workflow and then submit the final user-facing result. Do not call team_complete_task merely because the current workers have finished.\n\n" + prompt + } + envelope := map[string]interface{}{ + "v": 1, + "protocolVersion": 3, + "messageId": eventID, + "teamId": strconv.Itoa(team.ID), + "from": "clawmanager-monitor", + "to": leader.MemberKey, + "replyTo": teamTaskReplyTarget, + "requiresCompletion": false, + "completionTool": teamTaskCompletionTool, + "intent": intent, + "taskId": rootTaskRef, + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "workId": "leader-final-synthesis", + "assignmentId": "leader-final-synthesis", + "title": title, + "prompt": prompt, + "rawPrompt": prompt, + "monitorPolicy": defaultTeamMonitorPolicy(), + "metadata": payload, + "createdAt": now.Format(time.RFC3339Nano), + } + applyTeamTaskEnvelopeContext(envelope, task, leader.MemberKey) + envelopeJSON, err := marshalJSON(envelope) + if err != nil { + return err + } + _, err = bus.XAdd(context.Background(), teamInboxKey(team.ID, leader.MemberKey), map[string]string{ + "payload": envelopeJSON, + "team_id": strconv.Itoa(team.ID), + "task_id": strconv.Itoa(task.ID), + "message_id": eventID, + "member_id": leader.MemberKey, + }) + return err +} + +func buildLeaderSynthesisReminderPrompt(task *models.TeamTask, resultItems []models.TeamWorkItem, rootTaskRef string) string { + lines := []string{ + "[LEADER_SYNTHESIS_REMINDER] All tracked member assignments for this root task have delivered terminal results in the ClawManager ledger.", + fmt.Sprintf("rootTaskId=%s rootMessageId=%s workId=leader-final-synthesis assignmentId=leader-final-synthesis", rootTaskRef, task.MessageID), + "Synthesize the final user-facing answer from the confirmed member results below, then call team_complete_task for the root task. Do not re-dispatch finished assignments unless the evidence below is actually insufficient.", + "", + "Confirmed member results:", + } + for idx := range resultItems { + item := resultItems[idx] + summary := workItemResultSummary(item) + if summary == "" { + summary = item.Title + } + lines = append(lines, fmt.Sprintf("- %s: %s", item.WorkID, summary)) + } + return strings.Join(lines, "\n") +} + +func workItemResultPayload(item models.TeamWorkItem) map[string]interface{} { + payload := map[string]interface{}{} + if item.ResultJSON != nil && strings.TrimSpace(*item.ResultJSON) != "" { + _ = json.Unmarshal([]byte(*item.ResultJSON), &payload) + } + return payload +} + +func workItemResultSummary(item models.TeamWorkItem) string { + payload := workItemResultPayload(item) + summary := eventString(payload, "summary", "title", "resultMarkdown", "result_markdown", "result", "answer", "text", "message") + if summary == "" { + summary = item.Title + } + return truncateForSummary(summary, 240) +} + +func workItemArtifactRefs(item models.TeamWorkItem) []string { + payload := workItemResultPayload(item) + refs := explicitTeamArtifactReferences(payload) + if len(refs) > 0 { + return refs + } + if item.ArtifactRefsJSON == nil || strings.TrimSpace(*item.ArtifactRefsJSON) == "" { + return nil + } + var parsed []string + if err := json.Unmarshal([]byte(*item.ArtifactRefsJSON), &parsed); err == nil { + return parsed + } + return nil +} + +func shouldMonitorTeamWorkItem(item models.TeamWorkItem, cutoff time.Time) bool { + if item.OwnerMemberID == nil || strings.TrimSpace(item.WorkID) == "" { + return false + } + switch item.Status { + case models.TeamTaskStatusDispatched, models.TeamTaskStatusRunning: + default: + return false + } + return item.UpdatedAt.IsZero() || item.UpdatedAt.Before(cutoff) +} + +func (s *teamService) claimAssignmentMonitorSlot(key string, now time.Time) bool { + if s == nil || strings.TrimSpace(key) == "" { + return false + } + s.mu.Lock() + defer s.mu.Unlock() + if s.assignmentMonitorLast == nil { + s.assignmentMonitorLast = map[string]time.Time{} + } + if last, ok := s.assignmentMonitorLast[key]; ok && now.Sub(last) < teamAssignmentMonitorEvery { + return false + } + s.assignmentMonitorLast[key] = now + return true +} + +func (s *teamService) dispatchAssignmentStatusCheck(team *models.Team, bus *redisBus, task *models.TeamTask, item *models.TeamWorkItem, owner *models.TeamMember, now time.Time) error { + if team == nil || bus == nil || task == nil || item == nil || owner == nil { + return nil + } + envelope, messageID := buildAssignmentStatusCheckEnvelope(team, task, item, owner, now) + if envelope == nil || strings.TrimSpace(messageID) == "" { + return nil + } + envelopeJSON, err := marshalJSON(envelope) + if err != nil { + return err + } + _, err = bus.XAdd(context.Background(), teamInboxKey(team.ID, owner.MemberKey), map[string]string{ + "payload": envelopeJSON, + "team_id": strconv.Itoa(team.ID), + "task_id": strconv.Itoa(task.ID), + "message_id": messageID, + "member_id": owner.MemberKey, + }) + if err != nil { + return err + } + payload := map[string]interface{}{ + "v": 1, + "protocolVersion": 2, + "event": "assignment_check_requested", + "type": "assignment_check_requested", + "eventKind": "assignment_check_requested", + "teamId": strconv.Itoa(team.ID), + "taskId": fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID), + "rootTaskId": fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID), + "rootMessageId": task.MessageID, + "messageId": messageID, + "memberId": owner.MemberKey, + "from": "clawmanager-monitor", + "to": owner.MemberKey, + "workId": item.WorkID, + "assignmentId": item.WorkID, + "status": models.TeamTaskStatusRunning, + "runtimeStatus": models.TeamTaskStatusRunning, + "availability": models.TeamMemberAvailabilityBusy, + "summary": "ClawManager requested an automatic assignment status check.", + "visibleToChat": false, + "nonAuthoritative": true, + "rootTaskTerminal": false, + "monitor": true, + "monitorType": "assignment_status_check", + "workItemId": item.ID, + "workItemTitle": item.Title, + "lastWorkUpdatedAt": item.UpdatedAt.Format(time.RFC3339Nano), + } + payloadJSON, err := marshalOptionalJSON(payload) + if err != nil { + return err + } + event := &models.TeamEvent{ + TeamID: team.ID, + TaskID: &task.ID, + MemberID: &owner.ID, + EventType: "assignment_check_requested", + MessageID: &messageID, + PayloadJSON: payloadJSON, + OccurredAt: &now, + CreatedAt: now, + } + if err := s.repo.CreateEvent(event); err != nil && !errors.Is(err, repository.ErrDuplicateTeamEvent) { + return err + } + return nil +} + +func buildAssignmentStatusCheckEnvelope(team *models.Team, task *models.TeamTask, item *models.TeamWorkItem, owner *models.TeamMember, now time.Time) (map[string]interface{}, string) { + if team == nil || task == nil || item == nil || owner == nil { + return nil, "" + } + taskRef := fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + checkSequence := now.Unix() + if seconds := int64(teamAssignmentMonitorEvery.Seconds()); seconds > 0 { + checkSequence = now.Unix() / seconds + } + messageID := fmt.Sprintf("monitor:%s:%s:%d", taskRef, item.WorkID, checkSequence) + prompt := strings.Join([]string{ + "[STATUS_CHECK] This is an automatic ClawManager assignment monitor.", + fmt.Sprintf("rootTaskId=%s rootMessageId=%s workId=%s assignmentId=%s", taskRef, task.MessageID, item.WorkID, item.WorkID), + "Check the current assignment state. If you are still working, call team_update_progress with status=\"running\", eventKind=\"assignment_check_result\", a concise progress summary, and continue the same assignment. If you stopped or lost context, resume from the assignment evidence and report the recoverable blocker or needed retry to the Leader. Do not mark the assignment complete unless the deliverable is actually ready.", + }, "\n") + envelope := map[string]interface{}{ + "v": 1, + "protocolVersion": 2, + "messageId": messageID, + "teamId": strconv.Itoa(team.ID), + "from": "clawmanager-monitor", + "to": owner.MemberKey, + "replyTo": teamTaskReplyTarget, + "requiresCompletion": false, + "completionTool": teamTaskCompletionTool, + "intent": "assignment_status_check", + "taskId": taskRef, + "rootTaskId": taskRef, + "rootMessageId": task.MessageID, + "workId": item.WorkID, + "assignmentId": item.WorkID, + "checkId": messageID, + "checkSequence": checkSequence, + "requestedAt": now.Format(time.RFC3339Nano), + "title": "Assignment status check", + "prompt": prompt, + "rawPrompt": prompt, + "monitorPolicy": defaultTeamMonitorPolicy(), + "metadata": map[string]interface{}{ + "monitor": true, + "monitorType": "assignment_status_check", + "eventKind": "assignment_check_requested", + "visibleToChat": false, + "workItemId": item.ID, + "workItemTitle": item.Title, + "lastUpdatedAt": item.UpdatedAt.Format(time.RFC3339Nano), + "checkId": messageID, + "checkSequence": checkSequence, + "requestedAt": now.Format(time.RFC3339Nano), + }, + "createdAt": now.Format(time.RFC3339Nano), + } + applyTeamTaskEnvelopeContext(envelope, task, owner.MemberKey) + return envelope, messageID +} + func (s *teamService) markTaskStale(task *models.TeamTask, timeout time.Duration) error { if task == nil { return nil @@ -2247,6 +3508,35 @@ func (s *teamService) markTaskStale(task *models.TeamTask, timeout time.Duration if team == nil || team.Status == models.TeamStatusDeleted || team.Status == models.TeamStatusDeleting { return nil } + if payloadJSON, terminal, err := s.taskHasTerminalCompletionEvidence(team, task); err != nil { + return err + } else if terminal { + now := time.Now().UTC() + task.Status = models.TeamTaskStatusSucceeded + task.FinishedAt = &now + task.UpdatedAt = now + task.ErrorMessage = nil + if payloadJSON != nil { + task.ResultJSON = payloadJSON + } + if err := s.repo.UpdateTask(task); err != nil { + return err + } + if member, err := s.repo.GetMemberByID(task.TargetMemberID); err != nil { + return err + } else if member != nil && member.TeamID == task.TeamID && member.CurrentTaskID != nil && *member.CurrentTaskID == task.ID { + member.Status = models.TeamMemberStatusIdle + member.CurrentTaskID = nil + member.Availability = models.TeamMemberAvailabilityIdle + member.BlockedReason = nil + member.Progress = 100 + member.UpdatedAt = now + if err := s.repo.UpdateMember(member); err != nil { + return err + } + } + return nil + } cutoff := time.Now().UTC().Add(-timeout) active, err := s.taskHasRecentActivity(team, task, cutoff) if err != nil { @@ -2373,6 +3663,48 @@ func (s *teamService) taskHasRecentActivity(team *models.Team, task *models.Team return false, nil } +func (s *teamService) taskHasTerminalCompletionEvidence(team *models.Team, task *models.TeamTask) (*string, bool, error) { + if s == nil || team == nil || task == nil { + return nil, false, nil + } + events, err := s.repo.ListEventsByTeamID(team.ID, 1000) + if err != nil { + return nil, false, err + } + for idx := range events { + event := events[idx] + payload := teamEventPayloadMap(event) + if !teamEventMatchesRootTask(event, payload, task) { + continue + } + if eventBool(payload, "artifactValidationFailed", "artifact_validation_failed") { + continue + } + member := (*models.TeamMember)(nil) + if event.MemberID != nil { + found, err := s.repo.GetMemberByID(*event.MemberID) + if err != nil { + return nil, false, err + } + if found != nil && found.TeamID == team.ID { + member = found + } + } + eventType := event.EventType + if markedType := markLegacyRuntimeCompletionCandidate(eventType, payload, task, member); markedType != eventType { + eventType = markedType + } + if isTeamTaskCompletionSignal(eventType, normalizedTeamTaskEventStatus(payload), payload) { + payloadJSON, err := marshalOptionalJSON(payload) + if err != nil { + return nil, false, err + } + return payloadJSON, true, nil + } + } + return nil, false, nil +} + func (s *teamService) redisBusForTeam(ctx context.Context, team *models.Team) (*redisBus, error) { redisURL := "" if team.RedisURLSecretName != nil && team.RedisURLSecretKey != nil { @@ -2512,11 +3844,14 @@ func isTeamTaskCompletionSignal(eventType, status string, payload map[string]int return false } if teamRedisProtocolVersion(payload) >= 2 { - return eventType == "task_completed" && + return (eventType == "task_completed" || eventType == "completion_proposed") && isSuccessfulTeamTaskEventStatus(status) && isExplicitTeamTaskCompletion(payload) && hasStrictTeamCompletionEnvelope(payload) } + if eventBool(payload, "legacyCompletionCandidate", "legacy_completion_candidate") { + return hasTeamCompletionResultBody(payload) && (status == "" || isSuccessfulTeamTaskEventStatus(status)) + } if !hasAuthoritativeTeamCompletionPayload(eventType, status, payload) { return false } @@ -2590,7 +3925,7 @@ func isTeamTaskFailureSignal(eventType, status string, payload map[string]interf return false } if teamRedisProtocolVersion(payload) >= 2 { - if eventType != "task_failed" { + if eventType != "task_failed" && eventType != "completion_proposed" { return false } source := strings.ToLower(strings.TrimSpace(eventString(payload, "completionSource", "completion_source"))) @@ -2676,8 +4011,13 @@ func hasStrictTeamCompletionEnvelope(payload map[string]interface{}) bool { return false } } - refs := explicitTeamArtifactReferences(payload) - return len(refs) > 0 + // Artifact references are validated separately when they are present. + // Some valid control-plane/bootstrap tasks only return resultMarkdown and + // rely on the completion tool/runtime to persist the standard result files. + // Requiring artifactRefs here leaves those completed root tasks stuck in a + // dispatched/running Kanban state even though the explicit completion + // envelope is otherwise authoritative. + return true } func (s *teamService) hasAcceptedTeamCompletionID(teamID int, completionID string) (bool, error) { @@ -2788,6 +4128,128 @@ func isTerminalTeamTaskStatus(status string) bool { status == models.TeamTaskStatusStale } +func isAssignmentHeartbeatEvent(eventType string, payload map[string]interface{}) bool { + eventType = strings.ToLower(strings.TrimSpace(eventType)) + if eventType == "assignment_heartbeat" { + return true + } + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + return eventKind == "assignment_heartbeat" +} + +func isPassiveAssignmentMonitorEvent(eventType string, payload map[string]interface{}) bool { + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + switch strings.ToLower(strings.TrimSpace(eventType)) { + case "assignment_heartbeat", "assignment_check_requested": + return true + } + switch eventKind { + case "assignment_heartbeat", "assignment_check_requested", "assignment_check_result": + return true + } + monitorType := strings.ToLower(strings.TrimSpace(eventString(payload, "monitorType", "monitor_type"))) + return monitorType == "assignment_status_check" && eventBool(payload, "nonAuthoritative", "non_authoritative") +} + +func isTeamPresenceEvent(eventType string) bool { + switch strings.ToLower(strings.TrimSpace(eventType)) { + case "presence", "member_presence", "status", "member_status": + return true + default: + return false + } +} + +func normalizePassiveAssignmentMonitorPayload(payload map[string]interface{}) { + if payload == nil { + return + } + payload["visibleToChat"] = false + payload["visible_to_chat"] = false + payload["chatDigestEligible"] = true + payload["chat_digest_eligible"] = true + payload["nonAuthoritative"] = true + payload["rootTaskTerminal"] = false +} + +func normalizeAssignmentHeartbeatPayload(payload map[string]interface{}) { + if payload == nil { + return + } + normalizePassiveAssignmentMonitorPayload(payload) + summary := strings.TrimSpace(eventString(payload, "summary", "message", "text", "diagnostic")) + if summary == "" || strings.EqualFold(summary, "Agent turn is still running") { + payload["summary"] = "\u4efb\u52a1\u4ecd\u5728\u6267\u884c\uff0cAgent \u6b63\u5728\u7ee7\u7eed\u5904\u7406\u5f53\u524d\u56de\u5408\u3002" + } +} + +func normalizeUnauthorizedAssignmentCheckResult(payload map[string]interface{}) { + if payload == nil { + return + } + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + if eventKind != "assignment_check_result" && eventKind != "assignment_check_requested" { + return + } + checkID := strings.TrimSpace(eventString(payload, "checkId", "check_id")) + if strings.HasPrefix(checkID, "monitor:") { + return + } + payload["originalEventKind"] = eventKind + payload["eventKind"] = "worker_progress" + payload["checkSemanticCorrected"] = true + delete(payload, "checkId") + delete(payload, "check_id") +} + +func (s *teamService) leaderMediatedMonitorTargetsTerminalWorkItem(team *models.Team, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}) (bool, error) { + if s == nil || s.repo == nil || team == nil || task == nil || member == nil || payload == nil { + return false, nil + } + if isLeaderTeamMember(member) { + return false, nil + } + items, err := s.repo.ListWorkItemsByRootTaskID(task.ID) + if err != nil { + return false, err + } + workID := eventString(payload, "workId", "work_id", "assignmentId", "assignment_id") + canonicalWorkID := "member-" + normalizeTeamMemberRouteKey(member.MemberKey) + found := false + for idx := range items { + item := items[idx] + if item.OwnerMemberID == nil || *item.OwnerMemberID != member.ID { + continue + } + if workID != "" && item.WorkID != workID && item.WorkID != canonicalWorkID { + continue + } + found = true + if !isTerminalTeamTaskStatus(item.Status) { + return false, nil + } + } + return found, nil +} + +func isLeaderControlPlaneSnapshotTask(task *models.TeamTask, payload map[string]interface{}) bool { + if payload != nil { + if strings.TrimSpace(eventString(payload, "intent")) == initialLeaderTaskIntent { + return true + } + if strings.EqualFold(strings.TrimSpace(eventString(payload, "origin")), "system_bootstrap") { + return true + } + if strings.EqualFold(strings.TrimSpace(eventString(payload, "executionMode", "execution_mode")), "leader_control_plane_snapshot") { + return true + } + } + if task == nil { + return false + } + return strings.Contains(strings.ToLower(strings.TrimSpace(task.MessageID)), "bootstrap-introduction") +} + func shouldAssociateEventWithCurrentMemberTask(eventType string, payload map[string]interface{}) bool { switch eventType { case "reply", "completion", "task_completed", "task_failed", "message_failed", "message_warning", "task_started", "task_progress", "progress": @@ -2846,22 +4308,32 @@ func (s *teamService) leaderMediatedRootCompletionReady(team *models.Team, task if !isLeaderMediatedTeam(team) || task == nil || member == nil || member.ID != task.TargetMemberID || !isLeaderTeamMember(member) { return true, nil } + if isLeaderControlPlaneSnapshotTask(task, nil) { + return true, nil + } workItems, err := s.repo.ListWorkItemsByRootTaskID(task.ID) if err != nil { return false, err } - hasAssignments := false + requiredOwners := map[int]struct{}{} + deliveredOwners := map[int]struct{}{} for idx := range workItems { item := workItems[idx] if item.OwnerMemberID == nil || *item.OwnerMemberID == member.ID || item.WorkID == "leader-final-synthesis" { continue } - hasAssignments = true - if item.Status != models.TeamTaskStatusSucceeded { - return false, nil + ownerID := *item.OwnerMemberID + requiredOwners[ownerID] = struct{}{} + if item.Status == models.TeamTaskStatusSucceeded { + deliveredOwners[ownerID] = struct{}{} } } - if hasAssignments { + if len(requiredOwners) > 0 { + for ownerID := range requiredOwners { + if _, ok := deliveredOwners[ownerID]; !ok { + return false, nil + } + } return true, nil } events, err := s.repo.ListEventsByTeamID(team.ID, 500) @@ -2910,6 +4382,514 @@ func (s *teamService) leaderMediatedRootCompletionReady(team *models.Team, task return true, nil } +type teamCompletionEvaluation struct { + Decision string + Reason string + PendingAssignments []string + PendingPhases []string + WaivedAssignments []string + SkippedAssignments []string + StrongContradictions []string + LedgerVersion int64 + PlanVersion int64 +} + +type teamCompletionWaiver struct { + AssignmentID string + Reason string + Risk string +} + +func structuredTeamCompletionWaivers(payload map[string]interface{}) map[string]teamCompletionWaiver { + result := map[string]teamCompletionWaiver{} + raw := firstTeamValue(payload, "waivers", "assignmentWaivers", "assignment_waivers") + values, ok := raw.([]interface{}) + if !ok { + return result + } + for _, value := range values { + entry, ok := value.(map[string]interface{}) + if !ok { + continue + } + waiver := teamCompletionWaiver{ + AssignmentID: eventString(entry, "assignmentId", "assignment_id", "workId", "work_id"), + Reason: eventString(entry, "reason", "waiverReason", "waiver_reason"), + Risk: eventString(entry, "risk", "riskAccepted", "risk_accepted"), + } + if waiver.AssignmentID != "" && waiver.Reason != "" && waiver.Risk != "" { + result[waiver.AssignmentID] = waiver + } + } + return result +} + +func structuredTeamSkippedAssignments(payload map[string]interface{}) map[string]string { + result := map[string]string{} + raw := firstTeamValue(payload, "skippedAssignments", "skipped_assignments") + values, ok := raw.([]interface{}) + if !ok { + return result + } + for _, value := range values { + entry, ok := value.(map[string]interface{}) + if !ok { + continue + } + assignmentID := eventString(entry, "assignmentId", "assignment_id", "workId", "work_id") + reason := eventString(entry, "reason", "skipReason", "skip_reason") + if assignmentID != "" && reason != "" { + result[assignmentID] = reason + } + } + return result +} + +func acceptedTeamCompletionEvaluation(task *models.TeamTask) teamCompletionEvaluation { + result := teamCompletionEvaluation{Decision: teamCompletionDecisionAccepted} + if task != nil { + result.LedgerVersion = task.LedgerVersion + result.PlanVersion = task.PlanVersion + } + return result +} + +func (s *teamService) evaluateLeaderRootCompletion(team *models.Team, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}) (teamCompletionEvaluation, error) { + if !isLeaderMediatedTeam(team) || task == nil || member == nil || payload == nil || isLeaderControlPlaneSnapshotTask(task, payload) { + return acceptedTeamCompletionEvaluation(task), nil + } + result := acceptedTeamCompletionEvaluation(task) + if member.ID != task.TargetMemberID || !isLeaderTeamMember(member) { + result.Decision = teamCompletionDecisionRejected + result.Reason = "invalid_root_completion_owner" + return result, nil + } + + protocolVersion := teamRedisProtocolVersion(payload) + if protocolVersion >= 2 && (!isExplicitTeamTaskCompletion(payload) || !eventBool(payload, "rootTaskTerminal", "root_task_terminal")) { + result.Decision = teamCompletionDecisionRejected + result.Reason = "invalid_completion_envelope" + return result, nil + } + proposalPlanVersion := int64(eventInt(payload, "planVersion", "plan_version")) + proposalLedgerVersion := int64(eventInt(payload, "ledgerVersion", "ledger_version")) + if proposalPlanVersion > 0 && task.PlanVersion > 0 && proposalPlanVersion != task.PlanVersion { + result.Decision = teamCompletionDecisionDeferred + result.Reason = "stale_plan" + return result, nil + } + if proposalLedgerVersion > 0 && task.LedgerVersion > 0 && proposalLedgerVersion != task.LedgerVersion { + result.Decision = teamCompletionDecisionDeferred + result.Reason = "stale_ledger" + return result, nil + } + + items, err := s.repo.ListWorkItemsByRootTaskID(task.ID) + if err != nil { + return result, err + } + if len(items) == 0 && protocolVersion < 3 { + legacyReady, legacyErr := s.leaderMediatedRootCompletionReady(team, task, member) + if legacyErr != nil { + return result, legacyErr + } + if !legacyReady { + result.Decision = teamCompletionDecisionDeferred + result.Reason = "pending_legacy_assignments" + return result, nil + } + } + byBusinessID := make(map[string]models.TeamWorkItem, len(items)) + waivers := structuredTeamCompletionWaivers(payload) + skippedAssignments := structuredTeamSkippedAssignments(payload) + for idx := range items { + item := items[idx] + key := strings.TrimSpace(derefTeamString(item.AssignmentID)) + if key == "" { + key = strings.TrimSpace(item.WorkID) + } + if key != "" { + if current, ok := byBusinessID[key]; !ok || item.Revision >= current.Revision { + byBusinessID[key] = item + } + } + } + for key, item := range byBusinessID { + if item.OwnerMemberID == nil || *item.OwnerMemberID == member.ID || item.WorkID == "leader-final-synthesis" || item.SupersededBy != nil { + continue + } + required := item.RequiredForRoot || item.AssignmentID == nil + if !required { + if item.Status != models.TeamTaskStatusSucceeded { + if skippedAssignments[key] == "" { + result.PendingAssignments = append(result.PendingAssignments, key+":skip_reason") + } else { + result.SkippedAssignments = append(result.SkippedAssignments, key) + } + } + continue + } + if item.Status != models.TeamTaskStatusSucceeded { + if (item.Status == models.TeamTaskStatusFailed || item.Status == models.TeamTaskStatusStale) && waivers[key].AssignmentID != "" { + result.WaivedAssignments = append(result.WaivedAssignments, key) + continue + } + result.PendingAssignments = append(result.PendingAssignments, key) + continue + } + if item.ReviewRequired && (item.ValidatedRevision == nil || *item.ValidatedRevision < teamMaxInt(item.Revision, 1)) { + result.PendingAssignments = append(result.PendingAssignments, key+":review") + continue + } + for _, dependency := range teamWorkItemDependencies(item) { + dependencyItem, ok := byBusinessID[dependency] + if !ok || dependencyItem.Status != models.TeamTaskStatusSucceeded { + result.PendingAssignments = append(result.PendingAssignments, key+":depends_on:"+dependency) + } + } + } + sort.Strings(result.PendingAssignments) + result.PendingAssignments = uniqueTeamStrings(result.PendingAssignments) + sort.Strings(result.WaivedAssignments) + result.WaivedAssignments = uniqueTeamStrings(result.WaivedAssignments) + sort.Strings(result.SkippedAssignments) + result.SkippedAssignments = uniqueTeamStrings(result.SkippedAssignments) + if len(result.PendingAssignments) > 0 { + result.Decision = teamCompletionDecisionDeferred + result.Reason = "pending_assignments" + return result, nil + } + + phases, err := s.repo.ListWorkflowPhasesByRootTaskID(task.ID) + if err != nil { + return result, err + } + latestPlanVersion := task.PlanVersion + if latestPlanVersion <= 0 { + for idx := range phases { + if phases[idx].PlanVersion > latestPlanVersion { + latestPlanVersion = phases[idx].PlanVersion + } + } + } + workflowFinal := eventBool(payload, "workflowFinal", "workflow_final", "sealWorkflow", "seal_workflow") + finalAnswerReady := eventBool(payload, "finalAnswerReady", "final_answer_ready") + remainingActions := normalizeContextRefs(firstTeamValue(payload, "remainingActions", "remaining_actions", "nextActions", "next_actions")) + if protocolVersion >= 3 && (!workflowFinal || !finalAnswerReady || len(remainingActions) > 0) { + result.Decision = teamCompletionDecisionDeferred + result.Reason = "workflow_not_sealed" + result.PendingAssignments = append(result.PendingAssignments, remainingActions...) + return result, nil + } + for idx := range phases { + phase := phases[idx] + if latestPlanVersion > 0 && phase.PlanVersion != latestPlanVersion { + continue + } + if !phase.RequiredForRoot || phase.Status == teamPhaseStatusCompleted || phase.Status == teamPhaseStatusCancelled || phase.Status == teamPhaseStatusSuperseded { + continue + } + // The phase ledger is a projection, not an additional source of work. A + // planned phase with no dispatched required work must never keep an + // explicitly sealed workflow open (Team 58 was stuck exactly this way). + // Actual current work items and their dependencies were checked above. + if phaseHasIncompleteRequiredWork(phase.PhaseID, byBusinessID, member.ID, waivers) { + result.PendingPhases = append(result.PendingPhases, phase.PhaseID) + continue + } + if phase.DecisionRequired && !workflowFinal { + result.PendingPhases = append(result.PendingPhases, phase.PhaseID+":leader_decision") + } + } + sort.Strings(result.PendingPhases) + result.PendingPhases = uniqueTeamStrings(result.PendingPhases) + if len(result.PendingPhases) > 0 { + result.Decision = teamCompletionDecisionDeferred + result.Reason = "open_workflow_phases" + return result, nil + } + + result.StrongContradictions = analyzeCompletionNarrativeContradictions(payload) + if len(result.StrongContradictions) > 0 && !eventBool(payload, "confirmFinal", "confirm_final") { + result.Decision = teamCompletionDecisionNeedsConfirmation + result.Reason = "narrative_indicates_remaining_work" + return result, nil + } + return result, nil +} + +func firstTeamValue(payload map[string]interface{}, keys ...string) interface{} { + for _, key := range keys { + if value, ok := payload[key]; ok && value != nil { + return value + } + } + return nil +} + +func teamWorkItemDependencies(item models.TeamWorkItem) []string { + if item.DependsOnJSON == nil || strings.TrimSpace(*item.DependsOnJSON) == "" { + return nil + } + var values []string + if err := json.Unmarshal([]byte(*item.DependsOnJSON), &values); err != nil { + return nil + } + return uniqueTeamStrings(values) +} + +func uniqueTeamStrings(values []string) []string { + seen := map[string]struct{}{} + result := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" { + continue + } + if _, ok := seen[value]; ok { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + return result +} + +func teamMaxInt(left, right int) int { + if left > right { + return left + } + return right +} + +func (s *teamService) invalidateModifiedTeamArtifactReviews(task *models.TeamTask, payload map[string]interface{}, now time.Time) (bool, error) { + if s == nil || s.repo == nil || task == nil || payload == nil || + !(eventBool(payload, "artifactChanged", "artifact_changed") || strings.EqualFold(eventString(payload, "eventKind", "event_kind"), "artifact_changed")) { + return false, nil + } + changedRefs := explicitTeamArtifactReferences(payload) + if len(changedRefs) == 0 { + return false, nil + } + changedSet := make(map[string]struct{}, len(changedRefs)) + for _, ref := range changedRefs { + changedSet[strings.TrimSpace(ref)] = struct{}{} + } + items, err := s.repo.ListWorkItemsByRootTaskID(task.ID) + if err != nil { + return false, err + } + invalidated := false + for idx := range items { + item := items[idx] + if item.ID <= 0 || item.SupersededBy != nil || !item.ReviewRequired || item.ValidatedRevision == nil { + continue + } + matches := false + for _, ref := range workItemArtifactRefs(item) { + if _, ok := changedSet[strings.TrimSpace(ref)]; ok { + matches = true + break + } + } + if !matches { + continue + } + if err := s.repo.InvalidateWorkItemReview(item.ID, now); err != nil { + return invalidated, err + } + invalidated = true + } + if invalidated { + task.LedgerVersion++ + task.UpdatedAt = now + } + return invalidated, nil +} + +func phaseHasIncompleteRequiredWork(phaseID string, items map[string]models.TeamWorkItem, leaderID int, waivers map[string]teamCompletionWaiver) bool { + for key, item := range items { + if strings.TrimSpace(derefTeamString(item.PhaseID)) != strings.TrimSpace(phaseID) || item.SupersededBy != nil { + continue + } + if item.OwnerMemberID == nil || *item.OwnerMemberID == leaderID { + continue + } + if !(item.RequiredForRoot || item.AssignmentID == nil) { + continue + } + if (item.Status == models.TeamTaskStatusFailed || item.Status == models.TeamTaskStatusStale) && waivers[key].AssignmentID != "" { + continue + } + if item.Status != models.TeamTaskStatusSucceeded { + return true + } + } + return false +} + +// reconcileTeamWorkflowLedger repairs the derived phase view from the current +// work-item ledger. It deliberately does not invent or complete work: a phase +// moves to completed only when all of its actual required assignments succeeded. +// A future planned phase without assignments remains planned until the Leader +// explicitly seals the workflow, at which point it is cancelled as unused. +func (s *teamService) reconcileTeamWorkflowLedger(task *models.TeamTask, workflowFinal bool, now time.Time) (bool, error) { + if s == nil || s.repo == nil || task == nil || task.ID <= 0 || isTerminalTeamTaskStatus(task.Status) { + return false, nil + } + phases, err := s.repo.ListWorkflowPhasesByRootTaskID(task.ID) + if err != nil { + return false, err + } + items, err := s.repo.ListWorkItemsByRootTaskID(task.ID) + if err != nil { + return false, err + } + latestPlanVersion := task.PlanVersion + if latestPlanVersion <= 0 { + for _, phase := range phases { + if phase.PlanVersion > latestPlanVersion { + latestPlanVersion = phase.PlanVersion + } + } + } + if latestPlanVersion <= 0 { + return false, nil + } + + latestItems := map[string]models.TeamWorkItem{} + for _, item := range items { + if item.SupersededBy != nil { + continue + } + key := strings.TrimSpace(derefTeamString(item.AssignmentID)) + if key == "" { + key = strings.TrimSpace(item.WorkID) + } + if key == "" { + continue + } + if current, ok := latestItems[key]; !ok || teamMaxInt(item.Revision, 1) >= teamMaxInt(current.Revision, 1) { + latestItems[key] = item + } + } + + changed := false + anyIncomplete := false + anyDecision := false + var currentPhase string + for idx := range phases { + phase := phases[idx] + if phase.PlanVersion != latestPlanVersion || !phase.RequiredForRoot || + phase.Status == teamPhaseStatusCancelled || phase.Status == teamPhaseStatusSuperseded || phase.Status == teamPhaseStatusCompleted { + continue + } + phaseHasWork := false + phaseComplete := true + for _, item := range latestItems { + if strings.TrimSpace(derefTeamString(item.PhaseID)) != strings.TrimSpace(phase.PhaseID) || + item.OwnerMemberID == nil || *item.OwnerMemberID == task.TargetMemberID || + !(item.RequiredForRoot || item.AssignmentID == nil) { + continue + } + phaseHasWork = true + if item.Status != models.TeamTaskStatusSucceeded { + phaseComplete = false + } + } + previousStatus := phase.Status + switch { + case !phaseHasWork && workflowFinal && phase.Status == teamPhaseStatusPlanned: + phase.Status = teamPhaseStatusCancelled + case phaseHasWork && phaseComplete && phase.DecisionRequired && !workflowFinal: + phase.Status = teamPhaseStatusAwaitingLeaderDecision + anyDecision = true + case phaseHasWork && phaseComplete: + phase.Status = teamPhaseStatusCompleted + phase.CompletedAt = &now + case phaseHasWork: + phase.Status = teamPhaseStatusAwaitingResults + anyIncomplete = true + currentPhase = phase.PhaseID + case phase.Status == teamPhaseStatusAwaitingLeaderDecision: + anyDecision = true + } + if phase.Status != previousStatus { + phase.UpdatedAt = now + if err := s.repo.UpsertWorkflowPhase(&phase); err != nil { + return changed, err + } + changed = true + } + } + newState := task.WorkflowState + switch { + case anyIncomplete: + newState = teamWorkflowStateAwaitingPhaseResults + case anyDecision: + newState = teamWorkflowStateAwaitingLeaderDecision + case workflowFinal: + newState = teamWorkflowStateSynthesizing + default: + newState = teamWorkflowStateAwaitingLeaderDecision + } + if task.WorkflowState != newState { + task.WorkflowState = newState + changed = true + } + if currentPhase == "" && !anyIncomplete { + if task.CurrentPhaseID != nil { + task.CurrentPhaseID = nil + changed = true + } + } else if currentPhase != "" && (task.CurrentPhaseID == nil || *task.CurrentPhaseID != currentPhase) { + task.CurrentPhaseID = ¤tPhase + changed = true + } + if changed { + task.LedgerVersion++ + task.UpdatedAt = now + if err := s.repo.UpdateTask(task); err != nil { + return false, err + } + } + return changed, nil +} + +func analyzeCompletionNarrativeContradictions(payload map[string]interface{}) []string { + if payload == nil { + return nil + } + texts := []string{ + eventString(payload, "summary"), + eventString(payload, "resultMarkdown", "result_markdown", "result", "answer"), + } + if step, ok := payload["collaborationStep"].(map[string]interface{}); ok { + texts = append(texts, eventString(step, "summary"), eventString(step, "content", "detail")) + } + patterns := []struct { + name string + pattern *regexp.Regexp + }{ + {"waiting_for_member", regexp.MustCompile(`(?i)(仍在等待|继续等待|等待\s*(pm|architect|designer|developer|reviewer|worker)|still waiting|waiting for\s+(pm|architect|designer|developer|reviewer|worker))`)}, + {"future_dispatch", regexp.MustCompile(`(?i)((下一步|接下来|随后|然后).{0,10}(将|需要|必须|准备|继续).{0,24}(派发|下发|交给|发送给|实现|评审|验证)|(next|then).{0,12}(will|must|need to|going to|continue to).{0,24}(dispatch|assign|send to|implement|review|verify))`)}, + {"phase_not_final", regexp.MustCompile(`(?i)(完成|结束|汇总).{0,12}(第一阶段|阶段一|phase\s*1).{0,30}(继续|下一阶段|phase\s*2)`)}, + } + result := []string{} + for _, text := range texts { + text = strings.TrimSpace(text) + if text == "" { + continue + } + for _, candidate := range patterns { + if candidate.pattern.MatchString(text) { + result = append(result, candidate.name) + } + } + } + return uniqueTeamStrings(result) +} + func leaderMediatedDeliveredForRequiredMember(required string, delivered map[string]struct{}) bool { if _, ok := delivered[required]; ok { return true @@ -2972,6 +4952,374 @@ func markLeaderMediatedPrematureCompletion(eventType string, payload map[string] return "reply" } +func markStructuredCompletionDecision(eventType string, payload map[string]interface{}, evaluation teamCompletionEvaluation) string { + if eventString(payload, "originalEvent") == "" { + payload["originalEvent"] = eventType + } + decisionType := "completion_" + evaluation.Decision + payload["event"] = decisionType + payload["type"] = decisionType + payload["completionDecision"] = evaluation.Decision + payload["completionDecisionReason"] = evaluation.Reason + payload["pendingAssignments"] = evaluation.PendingAssignments + payload["pendingPhases"] = evaluation.PendingPhases + payload["waivedAssignments"] = evaluation.WaivedAssignments + payload["skippedAssignmentsAccepted"] = evaluation.SkippedAssignments + payload["strongNarrativeContradictions"] = evaluation.StrongContradictions + payload["ledgerVersion"] = evaluation.LedgerVersion + payload["planVersion"] = evaluation.PlanVersion + payload["status"] = models.TeamTaskStatusRunning + payload["runtimeStatus"] = models.TeamTaskStatusRunning + payload["availability"] = models.TeamMemberAvailabilityBusy + payload["rootTaskTerminal"] = false + payload["completionProposalPreserved"] = true + // A deferred completion is business information: it contains the Leader's + // delivery proposal plus the exact reason the system did not accept it. + // Keep it in the group chat; only the transport ACK is hidden. + payload["visibleToChat"] = true + payload["visible_to_chat"] = true + payload["chatPolicy"] = "warning" + payload["chatKind"] = "completion_" + evaluation.Decision + if completionID := eventString(payload, "completionId", "completion_id"); completionID != "" { + payload["displayKey"] = fmt.Sprintf("completion:%s:%d", completionID, evaluation.LedgerVersion) + } + return decisionType +} + +func applyTeamChatPolicy(eventType string, payload map[string]interface{}, task *models.TeamTask, member *models.TeamMember) { + if payload == nil { + return + } + normalizedEvent := strings.ToLower(strings.TrimSpace(eventType)) + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + // Runtime-provided visibility is advisory. A real plan, assignment, + // narrative, delivery, review, or synthesis must not be hidden simply + // because an older adapter labelled it as transport traffic. + if teamChatEventIsBusinessContent(normalizedEvent, eventKind, payload) { + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + payload["visible_to_chat"] = true + if eventKind == "assignment_check_result" { + payload["chatBusinessKind"] = "worker_progress" + } + if eventString(payload, "chatKind", "chat_kind") != "final_delivery" && + eventString(payload, "completionDecision", "completion_decision") != teamCompletionDecisionAccepted { + // Legacy display keys can be shared by multiple workers when the + // assignment id was absent. Let content-based dedupe handle business + // messages instead of suppressing a different worker's narrative. + delete(payload, "displayKey") + delete(payload, "display_key") + } + return + } + if eventString(payload, "chatPolicy", "chat_policy") != "" { + return + } + rootKey := eventString(payload, "rootTaskId", "root_task_id") + if rootKey == "" && task != nil { + rootKey = fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + } + assignmentID := teamChatAssignmentIdentity(payload) + actor := eventString(payload, "from", "memberId", "member_id") + if actor == "" && member != nil { + actor = member.MemberKey + } + displayKey := "" + switch { + case normalizedEvent == "member_result_confirmed": + payload["chatPolicy"] = "hidden" + payload["visibleToChat"] = false + return + case eventKind == "assignment_heartbeat" || normalizedEvent == "assignment_heartbeat" || eventKind == "assignment_check_requested" || normalizedEvent == "assignment_check_requested": + payload["chatPolicy"] = "digest" + payload["visibleToChat"] = false + return + case eventKind == "assignment_check_result" || normalizedEvent == "assignment_check_result": + if teamMonitorEventHasBusinessChange(payload) { + payload["chatPolicy"] = "visible" + payload["chatBusinessKind"] = "worker_progress" + payload["visibleToChat"] = true + } else { + payload["chatPolicy"] = "digest" + payload["visibleToChat"] = false + return + } + case eventKind == "leader_plan" || normalizedEvent == "leader_plan": + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + displayKey = "leader-plan:" + rootKey + ":" + strconv.FormatInt(int64(eventInt(payload, "planVersion", "plan_version")), 10) + case eventKind == "worker_plan" || normalizedEvent == "worker_plan": + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + case eventKind == "worker_progress" || normalizedEvent == "worker_progress": + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + case eventKind == "artifact_changed" || normalizedEvent == "artifact_changed": + payload["chatPolicy"] = "visible" + payload["chatBusinessKind"] = "worker_progress" + payload["visibleToChat"] = true + case eventBool(payload, "leaderDispatchOnly", "leader_dispatch_only") || normalizedEvent == "team_send" || normalizedEvent == "task_assigned" || normalizedEvent == "outbound": + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + displayKey = "assignment:" + rootKey + ":" + assignmentID + case eventBool(payload, "assignmentResultOnly", "assignment_result_only"): + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + displayKey = "worker-result:" + rootKey + ":" + assignmentID + ":" + strconv.Itoa(teamMaxInt(eventInt(payload, "revision"), 1)) + case normalizedEvent == "task_completed" && eventString(payload, "completionDecision", "completion_decision") == teamCompletionDecisionAccepted: + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + displayKey = "root-final:" + rootKey + case normalizedEvent == "message_warning" || strings.Contains(normalizedEvent, "rejected") || strings.Contains(normalizedEvent, "needs_confirmation"): + payload["chatPolicy"] = "warning" + payload["visibleToChat"] = true + case normalizedEvent == "task_received" || normalizedEvent == "task_started" || normalizedEvent == "presence": + payload["chatPolicy"] = "hidden" + payload["visibleToChat"] = false + } + if displayKey != "" { + payload["displayKey"] = displayKey + } +} + +func teamChatAssignmentIdentity(payload map[string]interface{}) string { + if payload == nil { + return "" + } + if identity := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id"); identity != "" { + return identity + } + if step, ok := payload["collaborationStep"].(map[string]interface{}); ok { + if identity := eventString(step, "assignmentId", "assignment_id", "workId", "work_id", "id"); identity != "" { + return identity + } + } + actor := eventString(payload, "from", "memberId", "member_id") + phase := eventString(payload, "phaseId", "phase_id", "phase", "stage") + if actor != "" || phase != "" { + return "member:" + normalizeTeamMemberRouteKey(actor) + ":phase:" + normalizeTeamRedisKeyPart(phase) + } + return "" +} + +func teamChatEventIsBusinessContent(eventType, eventKind string, payload map[string]interface{}) bool { + if payload == nil || teamChatEventIsTransportNoise(eventType, eventKind, payload) { + return false + } + switch eventKind { + case "leader_plan", "worker_plan", "worker_progress", "leader_synthesis", "leader_synthesis_reminder", "leader_decision_reminder", + "agent_narrative", "agent_plan", "agent_assignment", "agent_handoff", "agent_progress", "agent_delivery", "agent_review", "agent_synthesis", + "completion_deferred", "completion_candidate", "completion_validation_warning", "assignment_recovery_started", "assignment_reissued", "assignment_recovery_exhausted": + return teamChatHasMeaningfulBody(payload) + } + if eventBool(payload, "leaderDispatchOnly", "leader_dispatch_only", "assignmentResultOnly", "assignment_result_only") { + return teamChatHasMeaningfulBody(payload) + } + switch eventType { + case "outbound", "team_send", "task_assigned", "peer_request", "peer_handoff", "peer_review_request", "peer_reply", "reply", "completion_proposed", "task_completed", "completion", "message_warning": + return teamChatHasMeaningfulBody(payload) + case "task_progress", "progress": + return teamChatHasMeaningfulBody(payload) + default: + return false + } +} + +func teamChatEventIsTransportNoise(eventType, eventKind string, payload map[string]interface{}) bool { + if eventType == "member_result_confirmed" || eventType == "inbound" { + return true + } + switch eventKind { + case "assignment_heartbeat", "assignment_check_requested": + return true + case "assignment_check_result": + return !teamMonitorEventHasBusinessChange(payload) + } + switch eventType { + case "assignment_heartbeat", "assignment_check_requested", "task_received", "task_started", "presence": + return true + } + return false +} + +func teamChatHasMeaningfulBody(payload map[string]interface{}) bool { + if payload == nil { + return false + } + values := []string{ + eventString(payload, "content", "detail", "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary"), + } + if step, ok := payload["collaborationStep"].(map[string]interface{}); ok { + values = append(values, eventString(step, "content", "detail", "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary")) + } + for _, value := range values { + normalized := strings.ToLower(strings.TrimSpace(strings.Join(strings.Fields(value), " "))) + if normalized == "" { + continue + } + switch normalized { + case "task_received", "task_started", "redis team task received", "redis team task started", "redis team task processing completed", "agent turn is still running", "still running", "status unchanged", "no change": + continue + } + return true + } + return false +} + +func teamMonitorEventHasBusinessChange(payload map[string]interface{}) bool { + if payload == nil { + return false + } + if eventBool(payload, "blocked", "recovered", "artifactChanged", "artifact_changed", "phaseChanged", "phase_changed") || + eventInt(payload, "progress") > 0 || len(explicitTeamArtifactReferences(payload)) > 0 { + return true + } + summary := strings.ToLower(strings.TrimSpace(eventString(payload, "summary", "detail", "message", "text"))) + if summary == "" { + return false + } + for _, generic := range []string{"agent turn is still running", "still running", "status unchanged", "no change", "仍在执行", "状态无变化"} { + if summary == generic { + return false + } + } + return len([]rune(strings.Join(strings.Fields(summary), ""))) >= 12 +} + +func buildCompletionAcknowledgement(team *models.Team, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}, decision, reason string) (map[string]interface{}, string, string) { + completionID := eventString(payload, "completionId", "completion_id") + attemptID := eventString(payload, "attemptId", "attempt_id") + if attemptID == "" { + attemptID = eventString(payload, "eventId", "event_id") + } + if attemptID == "" { + attemptID = completionID + } + memberKey := "leader" + if member != nil && strings.TrimSpace(member.MemberKey) != "" { + memberKey = member.MemberKey + } + rootTaskID := "" + ledgerVersion := int64(0) + planVersion := int64(0) + if task != nil { + rootTaskID = fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + ledgerVersion = task.LedgerVersion + planVersion = task.PlanVersion + } + ack := map[string]interface{}{ + "v": 1, + "protocolVersion": 3, + "event": "completion_ack", + "type": "completion_ack", + "intent": "completion_ack", + "teamId": strconv.Itoa(team.ID), + "memberId": memberKey, + "rootTaskId": rootTaskID, + "rootMessageId": eventString(payload, "rootMessageId", "root_message_id"), + "completionId": completionID, + "attemptId": attemptID, + "decision": decision, + "reason": reason, + "pendingAssignments": firstTeamValue(payload, "pendingAssignments", "pending_assignments"), + "pendingPhases": firstTeamValue(payload, "pendingPhases", "pending_phases"), + "ledgerVersion": ledgerVersion, + "planVersion": planVersion, + "requiresCompletion": false, + "visibleToChat": false, + "chatPolicy": "hidden", + "acknowledgedAt": time.Now().UTC().Format(time.RFC3339Nano), + } + return ack, completionID, attemptID +} + +func (s *teamService) emitCompletionAcknowledgement(team *models.Team, bus *redisBus, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}, decision, reason string) error { + if team == nil || payload == nil { + return nil + } + ack, completionID, attemptID := buildCompletionAcknowledgement(team, task, member, payload, decision, reason) + if completionID == "" || attemptID == "" || bus == nil { + return nil + } + encoded, err := json.Marshal(ack) + if err != nil { + return err + } + if err := bus.Set(context.Background(), teamCompletionAckKey(team.ID, completionID, attemptID), string(encoded), 24*time.Hour); err != nil { + return err + } + return bus.Set(context.Background(), teamCompletionStateKey(team.ID, completionID), string(encoded), 7*24*time.Hour) +} + +func (s *teamService) completeWorkflowPhases(task *models.TeamTask, now time.Time) error { + if s == nil || task == nil || task.ID <= 0 { + return nil + } + phases, err := s.repo.ListWorkflowPhasesByRootTaskID(task.ID) + if err != nil { + return err + } + for idx := range phases { + phase := phases[idx] + if task.PlanVersion > 0 && phase.PlanVersion != task.PlanVersion { + continue + } + if phase.Status == teamPhaseStatusCancelled || phase.Status == teamPhaseStatusSuperseded { + continue + } + phase.Status = teamPhaseStatusCompleted + phase.CompletedAt = &now + phase.UpdatedAt = now + if err := s.repo.UpsertWorkflowPhase(&phase); err != nil { + return err + } + } + return nil +} + +func completionAcknowledgementOutbox(team *models.Team, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}, decision, reason, sourceEventID string, now time.Time) (*models.TeamEventOutbox, error) { + ack, completionID, attemptID := buildCompletionAcknowledgement(team, task, member, payload, decision, reason) + if completionID == "" || attemptID == "" { + return nil, fmt.Errorf("completion acknowledgement requires completionId and attemptId") + } + encoded, err := json.Marshal(ack) + if err != nil { + return nil, err + } + memberKey := "leader" + if member != nil && strings.TrimSpace(member.MemberKey) != "" { + memberKey = member.MemberKey + } + return &models.TeamEventOutbox{ + TeamID: team.ID, + SourceEventID: sourceEventID, + Destination: teamCompletionAckStreamKey(team.ID, memberKey), + MessageID: fmt.Sprintf("completion-ack:%s:%s", completionID, attemptID), + PayloadJSON: string(encoded), + Status: "pending", + AvailableAt: now, + CreatedAt: now, + UpdatedAt: now, + }, nil +} + +func isLeaderMediatedInterimRootCompletion(team *models.Team, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}) bool { + if !isLeaderMediatedTeam(team) || task == nil || member == nil || payload == nil { + return false + } + if member.ID != task.TargetMemberID || !isLeaderTeamMember(member) { + return false + } + if isLeaderControlPlaneSnapshotTask(task, payload) { + return false + } + text := eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary") + if text == "" { + return false + } + return isInterimOrDelegationReplyText(text) +} + func teamEventPayloadMap(event models.TeamEvent) map[string]interface{} { payload := map[string]interface{}{} if event.PayloadJSON != nil && strings.TrimSpace(*event.PayloadJSON) != "" { @@ -3042,7 +5390,7 @@ func isLeaderRouteTarget(target string) bool { func isLeaderMediatedOutboundLikeEvent(eventType string) bool { switch strings.ToLower(strings.TrimSpace(eventType)) { - case "outbound", "task_assigned", "team_send", "reply", "task_completed", "completion": + case "outbound", "task_assigned", "team_send", "reply", "task_completed", "completion", "completion_proposed": return true default: return false @@ -3087,6 +5435,68 @@ func markLeaderMediatedRouteViolation(eventType string, payload map[string]inter return "message_warning" } +func isLeaderMediatedMonitorBlockerCandidate(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask) bool { + if !isLeaderMediatedTeam(team) || payload == nil || member == nil || task == nil || member.ID != task.TargetMemberID || !isLeaderTeamMember(member) { + return false + } + if hasTeamTaskCompletionToolCall(payload) || + strings.EqualFold(strings.TrimSpace(eventString(payload, "completionSource", "completion_source")), teamTaskCompletionTool) || + eventBool(payload, "explicitCompletion", "explicit_completion", "rootTaskTerminal") { + return false + } + if eventType != "task_failed" && eventType != "message_failed" { + status := normalizedTeamTaskEventStatus(payload) + if !isFailedTeamTaskEventStatus(status) { + return false + } + } + return looksLikeNonAuthoritativeMonitorBlockerText(eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary", "error_message", "error", "reason", "diagnostic", "lastSummary", "last_summary")) +} + +func markLeaderMediatedMonitorBlockerCandidate(eventType string, payload map[string]interface{}) string { + if eventString(payload, "originalEvent") == "" { + payload["originalEvent"] = eventType + } + payload["event"] = "message_warning" + payload["type"] = "message_warning" + payload["status"] = "attention_required" + payload["runtimeStatus"] = models.TeamTaskStatusRunning + payload["availability"] = models.TeamMemberAvailabilityBusy + payload["rootTaskTerminal"] = false + payload["nonAuthoritative"] = true + payload["blockerCandidate"] = true + payload["monitorBlockerCandidate"] = true + if eventString(payload, "summary") == "" { + payload["summary"] = "Leader monitor reported a possible blocker; waiting for member result or explicit Leader finalization." + } + return "message_warning" +} + +func looksLikeNonAuthoritativeMonitorBlockerText(text string) bool { + normalized := strings.TrimSpace(text) + if normalized == "" { + return false + } + lower := strings.ToLower(normalized) + compact := strings.ToLower(strings.Join(strings.Fields(normalized), "")) + if strings.Contains(lower, "monitoring agent") || + strings.Contains(lower, "status check") || + strings.Contains(lower, "status checks") || + strings.Contains(lower, "unresponsive") || + strings.Contains(lower, "no summary created") || + strings.Contains(lower, "zero paper files") || + strings.Contains(lower, "artifacts not found") || + strings.Contains(lower, "artifact not found") || + strings.Contains(lower, "blocker report sent") { + return true + } + return strings.Contains(compact, "监控") || + strings.Contains(compact, "状态检查") || + strings.Contains(compact, "无响应") || + strings.Contains(compact, "未找到产物") || + strings.Contains(compact, "找不到产物") +} + func isLeaderMediatedWorkerToLeaderResult(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask) bool { if !isLeaderMediatedTeam(team) || payload == nil || member == nil || task == nil || isLeaderTeamMember(member) { return false @@ -3094,10 +5504,15 @@ func isLeaderMediatedWorkerToLeaderResult(team *models.Team, eventType string, p if member.ID == task.TargetMemberID { return false } + if isFailedTeamTaskEventStatus(normalizedTeamTaskEventStatus(payload)) { + return false + } if !isLeaderMediatedOutboundLikeEvent(eventType) || !teamEventHasBody(payload) { return false } - if isNonAuthoritativeDispatchFailure(eventType, payload) || eventBool(payload, "leaderMediatedRouteViolation") { + if isNonAuthoritativeDispatchFailure(eventType, payload) || + isDispatchOnlyCompletionPayload(payload) || + eventBool(payload, "leaderMediatedRouteViolation") { return false } target := leaderMediatedRouteTarget(payload) @@ -3107,22 +5522,39 @@ func isLeaderMediatedWorkerToLeaderResult(team *models.Team, eventType string, p if target != "" && !isLeaderRouteTarget(target) { return false } + if teamRedisProtocolVersion(payload) >= 2 && isExplicitTeamTaskCompletion(payload) { + return true + } body := eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary") if looksLikeLeaderDispatchOnlyText(body) { return false } + if looksLikeNonTerminalProgressText(body) { + return false + } return true } func markLeaderMediatedAssignmentResult(eventType string, payload map[string]interface{}, member *models.TeamMember) { payload["assignmentResultOnly"] = true + payload["memberResultConfirmed"] = true payload["rootTaskTerminal"] = false payload["status"] = models.TeamTaskStatusSucceeded payload["runtimeStatus"] = models.TeamTaskStatusSucceeded payload["availability"] = models.TeamMemberAvailabilityIdle + if eventString(payload, "normalizedResultSource") == "" { + if teamRedisProtocolVersion(payload) >= 2 { + payload["normalizedResultSource"] = "explicit_completion" + } else { + payload["normalizedResultSource"] = "legacy_normalized_reply" + } + } if eventString(payload, "originalEvent") == "" { payload["originalEvent"] = eventType } + if eventString(payload, "contentHash", "content_hash") == "" { + payload["contentHash"] = teamResultContentHash(payload) + } if member != nil { payload["from"] = member.MemberKey payload["memberId"] = member.MemberKey @@ -3133,44 +5565,115 @@ func markLeaderMediatedAssignmentResult(eventType string, payload map[string]int } } +func teamResultContentHash(payload map[string]interface{}) string { + content := eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary") + normalized := strings.Join(strings.Fields(strings.TrimSpace(content)), " ") + refs := explicitTeamArtifactReferences(payload) + sort.Strings(refs) + if normalized == "" && len(refs) == 0 { + return "" + } + fingerprint := normalized + "\nrefs=" + strings.Join(refs, "|") + digest := sha256.Sum256([]byte(fingerprint)) + return fmt.Sprintf("sha256:%x", digest[:]) +} + +func (s *teamService) reopenLeaderMediatedRootAfterMemberResult(team *models.Team, task *models.TeamTask, payload map[string]interface{}, now time.Time) error { + if s == nil || task == nil || !isLeaderMediatedTeam(team) || !isTerminalTeamTaskStatus(task.Status) { + return nil + } + if task.Status == models.TeamTaskStatusSucceeded { + return nil + } + errText := derefTeamString(task.ErrorMessage) + if errText == "" { + errText = eventString(payload, "previousError", "previous_error") + } + if !looksLikeNonAuthoritativeMonitorBlockerText(errText) { + return nil + } + task.Status = models.TeamTaskStatusRunning + task.FinishedAt = nil + task.ErrorMessage = nil + task.UpdatedAt = now + return s.repo.UpdateTask(task) +} + +func looksLikeNonTerminalProgressText(text string) bool { + normalized := strings.TrimSpace(text) + if normalized == "" { + return false + } + if looksLikeFinalResultText(normalized) || + containsAnyTeamTextMarker(normalized, []string{ + "final delivery", + "final confirmation", + "files are at", + "qa verdict", + "pass with", + "pass:", + "needs revision", + "ready for review", + "delivered", + "交付", + "最终", + "已归档", + }) { + return false + } + lower := strings.ToLower(normalized) + compact := strings.ToLower(strings.Join(strings.Fields(normalized), "")) + if strings.Contains(lower, "progress update") || + strings.Contains(lower, "status update") || + strings.Contains(lower, "phase 1") || + strings.Contains(lower, "phase 2") || + strings.Contains(lower, "starting ") || + strings.Contains(lower, "now extracting") || + strings.Contains(lower, "now processing") || + strings.Contains(lower, "still working") || + strings.Contains(lower, "in progress") || + strings.Contains(lower, "currently ") || + strings.Contains(lower, "processing all") || + strings.Contains(lower, "downloaded successfully") || + strings.Contains(lower, "curating to") { + return true + } + return strings.Contains(compact, "进度更新") || + strings.Contains(compact, "状态更新") || + strings.Contains(compact, "正在") || + strings.Contains(compact, "处理中") || + strings.Contains(compact, "继续执行") || + strings.Contains(compact, "阶段一") || + strings.Contains(compact, "阶段二") +} + func looksLikeLeaderDispatchOnlyText(text string) bool { normalized := strings.TrimSpace(text) if normalized == "" { return false } + if containsAnyTeamTextMarker(normalized, finalResultTextMarkers()) { + return false + } + if containsAnyTeamTextMarker(normalized, leaderDispatchTextMarkers()) { + return true + } compact := strings.ToLower(strings.Join(strings.Fields(normalized), "")) lower := strings.ToLower(normalized) - for _, marker := range []string{ - "任务结果反馈", "任务输出", "最终回答", "最终方案", "最终总结", "汇总如下", "结果如下", - "已完成并", "已完成,", "已完成:", "已完成:", "产出摘要", - "final answer", "final synthesis", "result summary", "task result", "completed with evidence", - } { - if strings.Contains(compact, strings.ToLower(strings.Join(strings.Fields(marker), ""))) || strings.Contains(lower, strings.ToLower(marker)) { - return false - } - } - for _, marker := range []string{ - "任务分派", "任务下发", "分派给", "派发给", "交给", "请你", "请写", "请完成", - "assignment", "assigned to", "handoff", - } { - if strings.Contains(compact, strings.ToLower(strings.Join(strings.Fields(marker), ""))) || strings.Contains(lower, strings.ToLower(marker)) { - return true - } - } if strings.Contains(normalized, "$CLAWMANAGER_TEAM_SHARED_DIR") || - (strings.Contains(compact, "共享目录") && strings.Contains(compact, "规范路径")) || + (containsTeamTextMarker(normalized, "\u5171\u4eab\u76ee\u5f55") && containsTeamTextMarker(normalized, "\u89c4\u8303\u8def\u5f84")) || (strings.Contains(lower, "shared directory") && strings.Contains(lower, "canonical path")) { return true } - if strings.Contains(compact, "完成后") && - (strings.Contains(compact, "回传") || strings.Contains(compact, "返回给我") || strings.Contains(compact, "通知我") || strings.Contains(compact, "交付给我")) { + if containsTeamTextMarker(normalized, "\u5b8c\u6210\u540e") && + (containsTeamTextMarker(normalized, "\u56de\u4f20") || containsTeamTextMarker(normalized, "\u8fd4\u56de\u7ed9\u6211") || containsTeamTextMarker(normalized, "\u901a\u77e5\u6211") || containsTeamTextMarker(normalized, "\u4ea4\u4ed8\u7ed9\u6211")) { return true } - if strings.Contains(compact, "用户想让你") && strings.Contains(compact, "请") { + if containsTeamTextMarker(normalized, "\u7528\u6237\u60f3\u8ba9\u4f60") && containsTeamTextMarker(normalized, "\u8bf7") { return true } for _, target := range []string{"pm", "designer", "ui-designer", "architect", "worker", "product-manager", "solution-architect"} { - if strings.Contains(compact, target+"你好") || strings.Contains(compact, "@"+target) { + if strings.Contains(compact, target+"\u4f60\u597d") || strings.Contains(compact, "@"+target) { return true } } @@ -3187,20 +5690,66 @@ func looksLikeFinalResultText(text string) bool { if normalized == "" { return false } - compact := strings.ToLower(strings.Join(strings.Fields(normalized), "")) - lower := strings.ToLower(normalized) - for _, marker := range []string{ - "任务结果反馈", "任务输出", "最终回答", "最终方案", "最终总结", "汇总如下", "结果如下", - "已完成并", "已完成,", "已完成:", "已完成:", "产出摘要", - "final answer", "final synthesis", "result summary", "task result", "completed with evidence", - } { - if strings.Contains(compact, strings.ToLower(strings.Join(strings.Fields(marker), ""))) || strings.Contains(lower, strings.ToLower(marker)) { + return containsAnyTeamTextMarker(normalized, finalResultTextMarkers()) +} + +func finalResultTextMarkers() []string { + return []string{ + "\u4efb\u52a1\u7ed3\u679c\u53cd\u9988", + "\u4efb\u52a1\u8f93\u51fa", + "\u6700\u7ec8\u56de\u7b54", + "\u6700\u7ec8\u65b9\u6848", + "\u6700\u7ec8\u603b\u7ed3", + "\u6700\u7ec8\u4ea4\u4ed8", + "\u6700\u7ec8\u4ea4\u4ed8\u62a5\u544a", + "\u6c47\u603b\u5982\u4e0b", + "\u7ed3\u679c\u5982\u4e0b", + "\u5df2\u5b8c\u6210\u5e76", + "\u5df2\u5b8c\u6210\uff0c", + "\u5df2\u5b8c\u6210", + "\u5df2\u5b8c\u6210\uff1a", + "\u4ea7\u51fa\u6458\u8981", + "final answer", + "final synthesis", + "final delivery", + "result summary", + "task result", + "completed with evidence", + } +} + +func leaderDispatchTextMarkers() []string { + return []string{ + "\u4efb\u52a1\u5206\u6d3e", + "\u4efb\u52a1\u4e0b\u53d1", + "\u5206\u6d3e\u7ed9", + "\u6d3e\u53d1\u7ed9", + "\u4ea4\u7ed9", + "\u8bf7\u4f60", + "\u8bf7\u5199", + "\u8bf7\u5b8c\u6210", + "assignment", + "assigned to", + "handoff", + } +} + +func containsAnyTeamTextMarker(text string, markers []string) bool { + for _, marker := range markers { + if containsTeamTextMarker(text, marker) { return true } } return false } +func containsTeamTextMarker(text, marker string) bool { + lowerText := strings.ToLower(text) + lowerMarker := strings.ToLower(marker) + compactText := strings.ToLower(strings.Join(strings.Fields(text), "")) + compactMarker := strings.ToLower(strings.Join(strings.Fields(marker), "")) + return strings.Contains(lowerText, lowerMarker) || strings.Contains(compactText, compactMarker) +} func inferLeaderDispatchTarget(payload map[string]interface{}) string { if payload == nil { return "" @@ -3232,6 +5781,14 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message if eventType == "" { eventType = "message" } + incomingEventType := strings.ToLower(strings.TrimSpace(eventType)) + isCompletionProposal := incomingEventType == "completion_proposed" + if strings.EqualFold(eventType, "member_result_confirmed") { + payload["visibleToChat"] = false + payload["visible_to_chat"] = false + payload["assignmentResultOnly"] = true + payload["rootTaskTerminal"] = false + } messageID := eventString(payload, "message_id", "messageId") memberKey := eventString(payload, "member_id", "memberId", "member_key") if isOutboundTeamEvent(eventType) && messageID != "" && !teamEventHasBody(payload) && bus != nil { @@ -3281,6 +5838,13 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message } task = found } + if task == nil && isLeaderMediatedTeam(team) && member != nil { + found, err := s.resolveLeaderMediatedTaskFromWorkItem(team.ID, payload, member) + if err != nil { + return err + } + task = found + } if task == nil && member != nil && member.CurrentTaskID != nil && shouldAssociateEventWithCurrentMemberTask(eventType, payload) { found, err := s.activeTaskForMember(team.ID, member, true) if err != nil { @@ -3295,6 +5859,38 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message } task = found } + normalizeUnauthorizedAssignmentCheckResult(payload) + passiveMonitorEvent := isPassiveAssignmentMonitorEvent(eventType, payload) + if isAssignmentHeartbeatEvent(eventType, payload) { + normalizeAssignmentHeartbeatPayload(payload) + if task != nil && isTerminalTeamTaskStatus(task.Status) { + return nil + } + passiveMonitorEvent = true + } else if passiveMonitorEvent { + normalizePassiveAssignmentMonitorPayload(payload) + } + if passiveMonitorEvent && task != nil && member != nil { + targetsTerminalWorkItem, err := s.leaderMediatedMonitorTargetsTerminalWorkItem(team, task, member, payload) + if err != nil { + return err + } + if targetsTerminalWorkItem { + return nil + } + } + if isTeamPresenceEvent(eventType) && task != nil && member != nil { + terminalWorkItem, err := s.leaderMediatedMonitorTargetsTerminalWorkItem(team, task, member, payload) + if err != nil { + return err + } + if terminalWorkItem { + payload["availability"] = models.TeamMemberAvailabilityIdle + payload["runtimeStatus"] = models.TeamTaskStatusSucceeded + payload["status"] = models.TeamTaskStatusSucceeded + payload["staleRunningSuppressed"] = true + } + } if isNonAuthoritativeDispatchFailure(eventType, payload) { if eventString(payload, "originalEvent") == "" { payload["originalEvent"] = eventType @@ -3313,11 +5909,37 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message assignmentResultOnly := isLeaderMediatedWorkerToLeaderResult(team, eventType, payload, member, task) if assignmentResultOnly { markLeaderMediatedAssignmentResult(eventType, payload, member) + if task != nil && member != nil { + assignmentID := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id") + contentHash := eventString(payload, "contentHash", "content_hash") + alreadyProjected, confirmErr := s.hasLeaderMediatedResultConfirmation(team.ID, task.ID, member.MemberKey, assignmentID, contentHash) + if confirmErr != nil { + return confirmErr + } + if alreadyProjected { + payload["visibleToChat"] = false + payload["visible_to_chat"] = false + payload["duplicateResultProjection"] = true + } + } + } + if !assignmentResultOnly && isLeaderMediatedMonitorBlockerCandidate(team, eventType, payload, member, task) { + eventType = markLeaderMediatedMonitorBlockerCandidate(eventType, payload) } eventType = normalizeFinalReplyTaskEvent(eventType, payload, task, member) + eventType = markLegacyRuntimeCompletionCandidate(eventType, payload, task, member) eventStatus := normalizedTeamTaskEventStatus(payload) eventSignalsCompletion := isTeamTaskCompletionSignal(eventType, eventStatus, payload) eventSignalsFailure := isTeamTaskFailureSignal(eventType, eventStatus, payload) + if isCompletionProposal && !eventSignalsCompletion && !eventSignalsFailure { + evaluation := teamCompletionEvaluation{Decision: teamCompletionDecisionRejected, Reason: "invalid_completion_envelope"} + if task != nil { + evaluation.LedgerVersion = task.LedgerVersion + evaluation.PlanVersion = task.PlanVersion + } + eventType = markStructuredCompletionDecision(eventType, payload, evaluation) + eventStatus = normalizedTeamTaskEventStatus(payload) + } if isUnauthoritativeCompletionEvent(eventType, eventSignalsCompletion, eventSignalsFailure) { if eventString(payload, "originalEvent") == "" { payload["originalEvent"] = eventType @@ -3334,7 +5956,10 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message eventSignalsCompletion = false eventSignalsFailure = false } - leaderDispatchOnly := isLeaderMediatedLeaderDispatchOnlyMessage(team, eventType, payload, member, task) + leaderDispatchOnly := false + if !eventSignalsCompletion && !eventSignalsFailure { + leaderDispatchOnly = isLeaderMediatedLeaderDispatchOnlyMessage(team, eventType, payload, member, task) + } if leaderDispatchOnly { if eventString(payload, "originalEvent") == "" { payload["originalEvent"] = eventType @@ -3356,15 +5981,48 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message eventSignalsFailure = false } if eventSignalsCompletion && !leaderDispatchOnly && !assignmentResultOnly && !leaderMediatedRouteViolation { - ready, err := s.leaderMediatedRootCompletionReady(team, task, member) + // Repair a stale derived phase state before judging an explicit final + // delivery. This is local projection repair, not a concurrent-plan change, + // so carry the repaired ledger version into this evaluation. + if task != nil { + workflowFinal := eventBool(payload, "workflowFinal", "workflow_final", "sealWorkflow", "seal_workflow") + reconciled, reconcileErr := s.reconcileTeamWorkflowLedger(task, workflowFinal, time.Now().UTC()) + if reconcileErr != nil { + return reconcileErr + } + if reconciled { + payload["ledgerVersion"] = task.LedgerVersion + payload["planVersion"] = task.PlanVersion + payload["workflowLedgerReconciled"] = true + } + } + narrative := eventString(payload, "summary") + "\n" + eventString(payload, "resultMarkdown", "result_markdown", "result", "answer") + payload["interimNarrativeSignal"] = isInterimOrDelegationReplyText(narrative) + evaluation, err := s.evaluateLeaderRootCompletion(team, task, member, payload) if err != nil { return err } - if !ready { - eventType = markLeaderMediatedPrematureCompletion(eventType, payload) + payload["completionEvaluation"] = evaluation.Decision + if len(evaluation.WaivedAssignments) > 0 { + payload["waivedAssignments"] = evaluation.WaivedAssignments + } + if len(evaluation.SkippedAssignments) > 0 { + payload["skippedAssignmentsAccepted"] = evaluation.SkippedAssignments + } + if evaluation.Decision != teamCompletionDecisionAccepted { + eventType = markStructuredCompletionDecision(eventType, payload, evaluation) eventStatus = normalizedTeamTaskEventStatus(payload) eventSignalsCompletion = false eventSignalsFailure = false + } else { + payload["event"] = "task_completed" + payload["type"] = "task_completed" + payload["completionDecision"] = teamCompletionDecisionAccepted + payload["chatKind"] = "final_delivery" + payload["chatPolicy"] = "visible" + payload["visibleToChat"] = true + payload["visible_to_chat"] = true + payload["displayKey"] = fmt.Sprintf("root-final:%d", task.ID) } } s.normalizeTeamArtifactReferences(team, payload) @@ -3381,6 +6039,11 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message payload["rootTaskTerminal"] = false payload["artifactValidationFailed"] = true payload["missingArtifactRefs"] = missing + if isCompletionProposal || isExplicitTeamTaskCompletion(payload) { + payload["completionDecision"] = teamCompletionDecisionRejected + payload["completionDecisionReason"] = "missing_artifacts" + payload["completionEvaluation"] = teamCompletionDecisionRejected + } // Preserve the final answer. Artifact validation is workflow state, not a // replacement for the result the member already returned. payload["artifactValidationMessage"] = "Team artifact references are not readable from the shared workspace; task remains open." @@ -3399,6 +6062,16 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message return err } if duplicate { + if isCompletionProposal { + if task != nil && task.Status == models.TeamTaskStatusSucceeded { + if err := s.completeWorkflowPhases(task, time.Now().UTC()); err != nil { + return err + } + } + if err := s.emitCompletionAcknowledgement(team, bus, task, member, payload, teamCompletionDecisionAccepted, "already_accepted"); err != nil { + return err + } + } return nil } } @@ -3410,6 +6083,7 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message payload["memberTerminalOnly"] = true payload["rootTaskTerminal"] = false } + applyTeamChatPolicy(eventType, payload, task, member) enrichTeamCollaborationStep(team, eventType, payload, member, task) payloadJSON, err := marshalOptionalJSON(payload) @@ -3442,28 +6116,131 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message if messageID != "" { event.MessageID = &messageID } - if err := s.repo.CreateEvent(event); err != nil { - if errors.Is(err, repository.ErrDuplicateTeamEvent) { - return nil + atomicRootCompletionAccepted := false + if eventSignalsCompletion && task != nil && member != nil && member.ID == task.TargetMemberID && isLeaderTeamMember(member) { + now := time.Now().UTC() + expectedLedgerVersion := task.LedgerVersion + completionID := eventString(payload, "completionId", "completion_id") + if completionID == "" { + seed := eventID + if seed == "" { + seed = message.ID + } + completionID = fmt.Sprintf("legacy:%d:%s", task.ID, normalizeTeamRedisKeyPart(seed)) + payload["completionId"] = completionID + payloadJSON, err = marshalOptionalJSON(payload) + if err != nil { + return err + } + event.PayloadJSON = payloadJSON + event.CompletionID = &completionID } + task.Status = models.TeamTaskStatusSucceeded + task.WorkflowState = teamWorkflowStateCompleted + task.LedgerVersion = expectedLedgerVersion + 1 + task.CurrentPhaseID = nil + task.AcceptedCompletionID = &completionID + task.ResultJSON = payloadJSON + task.ErrorMessage = nil + task.FinishedAt = &now + task.UpdatedAt = now + sourceEventID := eventID + if sourceEventID == "" { + sourceEventID = message.ID + } + ackOutbox, ackErr := completionAcknowledgementOutbox(team, task, member, payload, teamCompletionDecisionAccepted, "workflow_closed", sourceEventID, now) + if ackErr != nil { + return ackErr + } + accepted, acceptErr := s.repo.AcceptRootCompletion(task, expectedLedgerVersion, event, ackOutbox) + if acceptErr != nil { + return acceptErr + } + if accepted { + atomicRootCompletionAccepted = true + if err := s.emitCompletionAcknowledgement(team, bus, task, member, payload, teamCompletionDecisionAccepted, "workflow_closed"); err != nil { + return err + } + if ackOutbox.ID > 0 && bus != nil { + if err := s.repo.MarkEventOutboxDelivered(ackOutbox.ID, time.Now().UTC()); err != nil { + return err + } + } + } else { + task.LedgerVersion = expectedLedgerVersion + task.Status = models.TeamTaskStatusRunning + task.WorkflowState = teamWorkflowStateCompletionPending + task.AcceptedCompletionID = nil + task.FinishedAt = nil + evaluation := teamCompletionEvaluation{ + Decision: teamCompletionDecisionDeferred, + Reason: "stale_ledger", + LedgerVersion: expectedLedgerVersion, + PlanVersion: task.PlanVersion, + } + eventType = markStructuredCompletionDecision(eventType, payload, evaluation) + eventSignalsCompletion = false + event.CompletionID = nil + event.EventType = eventType + payloadJSON, err = marshalOptionalJSON(payload) + if err != nil { + return err + } + event.PayloadJSON = payloadJSON + } + } + if !atomicRootCompletionAccepted { + if err := s.repo.CreateEvent(event); err != nil { + if errors.Is(err, repository.ErrDuplicateTeamEvent) { + return nil + } + return err + } + } + reviewInvalidated, err := s.invalidateModifiedTeamArtifactReviews(task, payload, time.Now().UTC()) + if err != nil { return err } + workflowChanged := reviewInvalidated if err := s.projectTeamWorkItem(team, task, member, eventType, payload, event); err != nil { return err } - + if !atomicRootCompletionAccepted { + ledgerChanged, ledgerErr := s.projectTeamWorkflowLedger(team, task, member, eventType, payload, time.Now().UTC()) + err = ledgerErr + if err != nil { + return err + } + workflowChanged = workflowChanged || ledgerChanged + } now := time.Now().UTC() + if assignmentResultOnly { + if err := s.createLeaderMediatedResultNotification(team, bus, task, member, payload, event); err != nil { + return err + } + if err := s.reopenLeaderMediatedRootAfterMemberResult(team, task, payload, now); err != nil { + return err + } + } + if !assignmentResultOnly && !passiveMonitorEvent && isLeaderMediatedRecoverableWarning(team, eventType, payload, member, task) { + if err := s.createLeaderMediatedRecoveryRequest(team, bus, task, member, payload, event); err != nil { + return err + } + } + taskProjection := teamTaskProjectionResult{} - if task != nil && !memberTerminalOnly && !assignmentResultOnly && !leaderMediatedRouteViolation { + if atomicRootCompletionAccepted { + taskProjection = teamTaskProjectionResult{changed: true, status: models.TeamTaskStatusSucceeded} + } else if task != nil && !passiveMonitorEvent && !memberTerminalOnly && !assignmentResultOnly && !leaderMediatedRouteViolation { taskProjection = projectTeamTaskRuntimeState(task, payload, eventType, payloadJSON, now) - if taskProjection.changed { + if taskProjection.changed || workflowChanged { task.UpdatedAt = now if err := s.repo.UpdateTask(task); err != nil { return err } } } - if task != nil && (memberTerminalOnly || leaderDispatchOnly || assignmentResultOnly || leaderMediatedRouteViolation) && !taskProjection.changed && !isTerminalTeamTaskStatus(task.Status) { + if task != nil && (memberTerminalOnly || leaderDispatchOnly || assignmentResultOnly || leaderMediatedRouteViolation || workflowChanged) && !taskProjection.changed && !isTerminalTeamTaskStatus(task.Status) { task.UpdatedAt = now if err := s.repo.UpdateTask(task); err != nil { return err @@ -3471,9 +6248,11 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message } if member != nil { member.LastSeenAt = &now - applyTeamMemberRuntimeProjection(member, payload, eventType) + if !passiveMonitorEvent { + applyTeamMemberRuntimeProjection(member, payload, eventType) + } taskIsActive := task != nil && !isTerminalTeamTaskStatus(task.Status) - if taskIsActive && (leaderDispatchOnly || eventType == "task_received" || eventType == "task_started" || taskProjection.status == models.TeamTaskStatusRunning || taskProjection.status == models.TeamTaskStatusDispatched) { + if !passiveMonitorEvent && taskIsActive && (leaderDispatchOnly || eventType == "task_received" || eventType == "task_started" || taskProjection.status == models.TeamTaskStatusRunning || taskProjection.status == models.TeamTaskStatusDispatched) { member.Status = models.TeamMemberStatusBusy if member.Availability == "" || member.Availability == models.TeamMemberAvailabilityUnknown { member.Availability = models.TeamMemberAvailabilityBusy @@ -3516,14 +6295,522 @@ func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message return err } } + if isCompletionProposal && !atomicRootCompletionAccepted { + decision := eventString(payload, "completionDecision", "completion_decision") + reason := eventString(payload, "completionDecisionReason", "completion_decision_reason") + if assignmentResultOnly || memberTerminalOnly || eventSignalsFailure { + decision = teamCompletionDecisionAccepted + if eventSignalsFailure { + reason = "failure_recorded" + } else { + reason = "assignment_result_recorded" + } + } + if decision == "" { + decision = teamCompletionDecisionDeferred + } + sourceEventID := eventID + if sourceEventID == "" { + sourceEventID = message.ID + } + var ackOutbox *models.TeamEventOutbox + if eventString(payload, "completionId", "completion_id") != "" { + var err error + ackOutbox, err = completionAcknowledgementOutbox(team, task, member, payload, decision, reason, sourceEventID, now) + if err != nil { + return err + } + if err := s.repo.CreateEventOutbox(ackOutbox); err != nil { + return err + } + } + if err := s.emitCompletionAcknowledgement(team, bus, task, member, payload, decision, reason); err != nil { + return err + } + if ackOutbox != nil && ackOutbox.ID > 0 && bus != nil { + if err := s.repo.MarkEventOutboxDelivered(ackOutbox.ID, time.Now().UTC()); err != nil { + return err + } + } + } return nil } +func isLeaderMediatedRecoverableWarning(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask) bool { + if team == nil || task == nil || member == nil || payload == nil { + return false + } + if !isLeaderMediatedTeam(team) || isLeaderTeamMember(member) || isTerminalTeamTaskStatus(task.Status) { + return false + } + if isPassiveAssignmentMonitorEvent(eventType, payload) { + return false + } + if eventBool(payload, "rootTaskTerminal", "root_task_terminal") { + return false + } + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + if eventKind == "assignment_recovery_exhausted" { + return false + } + if eventBool(payload, "artifactValidationFailed", "artifact_validation_failed") { + return true + } + if eventBool(payload, "nonAuthoritativeCompletion", "non_authoritative_completion") { + return true + } + if strings.EqualFold(eventType, "message_warning") { + return true + } + return eventKind == "completion_validation_warning" +} + +func (s *teamService) createLeaderMediatedRecoveryRequest(team *models.Team, bus *redisBus, task *models.TeamTask, member *models.TeamMember, sourcePayload map[string]interface{}, sourceEvent *models.TeamEvent) error { + if s == nil || team == nil || bus == nil || task == nil || member == nil || sourcePayload == nil || sourceEvent == nil { + return nil + } + leaderKey := "leader" + if leader, err := s.repo.GetMemberByID(task.TargetMemberID); err == nil && leader != nil && strings.TrimSpace(leader.MemberKey) != "" { + leaderKey = leader.MemberKey + } + streamRef := "" + if sourceEvent.RedisStreamID != nil { + streamRef = *sourceEvent.RedisStreamID + } + if streamRef == "" { + streamRef = strconv.FormatInt(sourceEvent.CreatedAt.UnixNano(), 10) + } + eventID := fmt.Sprintf("leader-recovery:%d:%d:%s:%s", team.ID, task.ID, member.MemberKey, streamRef) + exists, err := s.repo.EventExistsByEventID(team.ID, eventID) + if err != nil || exists { + return err + } + rootTaskRef := fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + summary := eventString(sourcePayload, "summary", "diagnostic", "error_message", "error", "message", "text") + if summary == "" { + summary = "Recoverable assignment issue detected; Leader should replan, retry, or reassign before surfacing an error to the user." + } + workID := eventString(sourcePayload, "workId", "work_id", "assignmentId", "assignment_id") + if workID == "" { + workID = "member-" + normalizeTeamMemberRouteKey(member.MemberKey) + } + notificationPayload := map[string]interface{}{ + "event": "assignment_recovery_started", + "type": "assignment_recovery_started", + "eventKind": "assignment_recovery_started", + "protocolVersion": 2, + "source": "clawmanager_recovery_controller", + "recoverable": true, + "rootTaskTerminal": false, + "nonAuthoritative": true, + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "messageId": task.MessageID, + "assignmentId": workID, + "workId": workID, + "from": "clawmanager", + "memberId": member.MemberKey, + "to": leaderKey, + "target": leaderKey, + "status": models.TeamTaskStatusRunning, + "runtimeStatus": models.TeamTaskStatusRunning, + "availability": models.TeamMemberAvailabilityBusy, + "summary": summary, + "sourceEventId": sourceEvent.ID, + "sourceEventType": sourceEvent.EventType, + "sourcePayload": sourcePayload, + "visibleToChat": true, + "recoveryAction": "leader_replan_or_reissue", + "collaborationStep": map[string]interface{}{ + "type": "progress", + "status": models.TeamTaskStatusRunning, + "actor": "clawmanager", + "target": member.MemberKey, + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "workId": workID, + "title": "Recovery started for " + member.MemberKey, + "summary": summary, + "content": summary, + "source": "clawmanager_recovery_controller", + }, + } + payloadJSON, err := marshalOptionalJSON(notificationPayload) + if err != nil { + return err + } + now := time.Now().UTC() + event := &models.TeamEvent{ + TeamID: team.ID, + TaskID: &task.ID, + MemberID: &member.ID, + MessageID: &task.MessageID, + EventID: &eventID, + EventType: "assignment_recovery_started", + PayloadJSON: payloadJSON, + OccurredAt: &now, + CreatedAt: now, + } + if err := s.repo.CreateEvent(event); err != nil { + return err + } + s.dispatchLeaderMediatedRecoveryRequestToInbox(team, bus, task, member, leaderKey, notificationPayload, eventID) + return nil +} + +func (s *teamService) dispatchLeaderMediatedRecoveryRequestToInbox(team *models.Team, bus *redisBus, task *models.TeamTask, member *models.TeamMember, leaderKey string, notificationPayload map[string]interface{}, eventID string) { + if team == nil || bus == nil || task == nil || member == nil || notificationPayload == nil { + return + } + if strings.TrimSpace(leaderKey) == "" { + leaderKey = "leader" + } + summary := eventString(notificationPayload, "summary") + prompt := fmt.Sprintf( + "ClawManager detected a recoverable issue for member %s on root task %s.\n\nIssue:\n%s\n\nFirst try to recover without exposing this as a user-facing failure: inspect existing progress/artifacts, ask the same member to continue if appropriate, reissue the assignment with corrected instructions, or reassign to another member. Only report failure to the user after recovery is exhausted.", + member.MemberKey, + task.MessageID, + summary, + ) + envelope := map[string]interface{}{ + "v": 1, + "messageId": eventID, + "teamId": strconv.Itoa(team.ID), + "from": "clawmanager", + "to": leaderKey, + "replyTo": teamTaskReplyTarget, + "requiresCompletion": false, + "completionTool": teamTaskCompletionTool, + "intent": "assignment_recovery_request", + "taskId": fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID), + "rootTaskId": fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID), + "rootMessageId": task.MessageID, + "workId": eventString(notificationPayload, "workId", "assignmentId"), + "assignmentId": eventString(notificationPayload, "assignmentId", "workId"), + "title": member.MemberKey + " assignment recovery requested", + "prompt": prompt, + "rawPrompt": prompt, + "monitorPolicy": defaultTeamMonitorPolicy(), + "metadata": notificationPayload, + "createdAt": time.Now().UTC().Format(time.RFC3339Nano), + } + applyTeamTaskEnvelopeContext(envelope, task, leaderKey) + envelopeJSON, err := marshalJSON(envelope) + if err != nil { + fmt.Printf("Warning: failed to encode Leader recovery notification for Team %d task %d: %v\n", team.ID, task.ID, err) + return + } + if _, err := bus.XAdd(context.Background(), teamInboxKey(team.ID, leaderKey), map[string]string{ + "payload": envelopeJSON, + "team_id": strconv.Itoa(team.ID), + "task_id": strconv.Itoa(task.ID), + "message_id": eventID, + "member_id": leaderKey, + }); err != nil { + fmt.Printf("Warning: failed to dispatch Leader recovery notification for Team %d task %d: %v\n", team.ID, task.ID, err) + } +} + +func (s *teamService) createLeaderMediatedResultNotification(team *models.Team, bus *redisBus, task *models.TeamTask, member *models.TeamMember, sourcePayload map[string]interface{}, sourceEvent *models.TeamEvent) error { + if s == nil || team == nil || task == nil || member == nil || sourcePayload == nil || sourceEvent == nil { + return nil + } + if !isLeaderMediatedTeam(team) || isLeaderTeamMember(member) { + return nil + } + workID := eventString(sourcePayload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id") + if workID == "" { + workID = "member-" + normalizeTeamMemberRouteKey(member.MemberKey) + } + revision := eventInt(sourcePayload, "revision", "workRevision", "work_revision") + if revision <= 0 { + revision = 1 + } + source := eventString(sourcePayload, "normalizedResultSource") + if source == "" { + source = "legacy_normalized_reply" + } + summary := eventString(sourcePayload, "summary", "title") + resultMarkdown := eventString(sourcePayload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary") + if summary == "" { + summary = truncateForSummary(resultMarkdown, 180) + } + if strings.TrimSpace(summary) == "" && strings.TrimSpace(resultMarkdown) == "" { + return nil + } + contentHash := eventString(sourcePayload, "contentHash", "content_hash") + if contentHash == "" { + contentHash = teamResultContentHash(sourcePayload) + } + alreadyConfirmed, err := s.hasLeaderMediatedResultConfirmation(team.ID, task.ID, member.MemberKey, workID, contentHash) + if err != nil || alreadyConfirmed { + return err + } + confirmationSeed := fmt.Sprintf("%d:%d:%s:%s:%d:%s", team.ID, task.ID, normalizeTeamMemberRouteKey(member.MemberKey), workID, revision, contentHash) + confirmationDigest := sha256.Sum256([]byte(confirmationSeed)) + eventID := fmt.Sprintf("member-result-confirmed:%d:%d:%x", team.ID, task.ID, confirmationDigest[:12]) + exists, err := s.repo.EventExistsByEventID(team.ID, eventID) + if err != nil || exists { + return err + } + rootTaskRef := fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + sourceEventID := "" + if sourceEvent.EventID != nil { + sourceEventID = strings.TrimSpace(*sourceEvent.EventID) + } + if sourceEventID == "" && sourceEvent.RedisStreamID != nil { + sourceEventID = strings.TrimSpace(*sourceEvent.RedisStreamID) + } + if sourceEventID == "" && sourceEvent.ID > 0 { + sourceEventID = strconv.Itoa(sourceEvent.ID) + } + sourceCompletionID := eventString(sourcePayload, "completionId", "completion_id") + sourceWorkID := eventString(sourcePayload, "workId", "work_id", "assignmentId", "assignment_id") + if sourceWorkID == "" { + sourceWorkID = workID + } + notificationPayload := map[string]interface{}{ + "event": "member_result_confirmed", + "type": "member_result_confirmed", + "protocolVersion": 2, + "source": "clawmanager_assignment_ledger", + "normalizedResultSource": source, + "memberResultConfirmed": true, + "assignmentResultOnly": true, + "rootTaskTerminal": false, + "visibleToChat": false, + "visible_to_chat": false, + "chatPolicy": "hidden", + "sourceEventId": sourceEventID, + "sourceCompletionId": sourceCompletionID, + "sourceWorkId": sourceWorkID, + "contentHash": contentHash, + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "messageId": task.MessageID, + "assignmentId": workID, + "workId": workID, + "revision": revision, + "from": member.MemberKey, + "memberId": member.MemberKey, + "to": "leader", + "target": "leader", + "status": models.TeamTaskStatusSucceeded, + "summary": member.MemberKey + " assignment result confirmed", + "collaborationStep": map[string]interface{}{ + "type": "ack", + "status": models.TeamTaskStatusSucceeded, + "actor": member.MemberKey, + "target": "leader", + "rootTaskId": rootTaskRef, + "rootMessageId": task.MessageID, + "workId": workID, + "title": member.MemberKey + " result confirmed", + "summary": member.MemberKey + " assignment result confirmed", + "source": "clawmanager_assignment_ledger", + }, + } + payloadJSON, err := marshalOptionalJSON(notificationPayload) + if err != nil { + return err + } + now := time.Now().UTC() + event := &models.TeamEvent{ + TeamID: team.ID, + TaskID: &task.ID, + MemberID: &member.ID, + MessageID: &task.MessageID, + EventID: &eventID, + EventType: "member_result_confirmed", + PayloadJSON: payloadJSON, + OccurredAt: &now, + CreatedAt: now, + } + resultJSON, err := marshalOptionalJSON(sourcePayload) + if err != nil { + return err + } + workItem := &models.TeamWorkItem{ + TeamID: team.ID, + RootTaskID: task.ID, + WorkID: workID, + AssignmentID: &workID, + CanonicalWorkID: &workID, + Revision: revision, + RequiredForRoot: true, + OwnerMemberID: &member.ID, + Title: member.MemberKey + " delivers result", + Status: models.TeamTaskStatusSucceeded, + ResultJSON: resultJSON, + FinishedAt: &now, + UpdatedAt: now, + } + if items, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID); listErr == nil { + for idx := range items { + candidateID := derefTeamString(items[idx].AssignmentID) + if candidateID == "" { + candidateID = items[idx].WorkID + } + if items[idx].TeamID == team.ID && candidateID == workID && teamMaxInt(items[idx].Revision, 1) == revision { + clone := items[idx] + clone.Status = models.TeamTaskStatusSucceeded + clone.ResultJSON = resultJSON + clone.FinishedAt = &now + clone.UpdatedAt = now + workItem = &clone + break + } + } + } + deliveryPayload := make(map[string]interface{}, len(notificationPayload)+3) + for key, value := range notificationPayload { + deliveryPayload[key] = value + } + deliveryPayload["summary"] = summary + deliveryPayload["resultMarkdown"] = resultMarkdown + deliveryPayload["artifactRefs"] = explicitTeamArtifactReferences(sourcePayload) + deliveryEnvelope, leaderKey := s.buildLeaderMediatedResultNotificationEnvelope(team, task, member, deliveryPayload, eventID) + deliveryJSON, err := marshalJSON(deliveryEnvelope) + if err != nil { + return err + } + outbox := &models.TeamEventOutbox{ + TeamID: team.ID, + SourceEventID: eventID, + Destination: teamInboxKey(team.ID, leaderKey), + MessageID: eventID, + PayloadJSON: deliveryJSON, + Status: "pending", + AvailableAt: now, + CreatedAt: now, + UpdatedAt: now, + } + if err := s.repo.ConfirmWorkItemResult(workItem, event, outbox); err != nil { + return err + } + if outbox.ID > 0 { + if err := s.deliverTeamEventOutbox(team, bus, outbox); err != nil { + _ = s.repo.MarkEventOutboxFailed(outbox.ID, now.Add(teamOutboxRetryDelay(outbox.Attempts)), err.Error()) + return nil + } + if err := s.repo.MarkEventOutboxDelivered(outbox.ID, time.Now().UTC()); err != nil { + return err + } + } + return nil +} + +func (s *teamService) hasLeaderMediatedResultConfirmation(teamID, taskID int, memberKey string, assignmentID string, contentHashes ...string) (bool, error) { + if s == nil || s.repo == nil { + return false, nil + } + events, err := s.repo.ListEventsByTeamID(teamID, 500) + if err != nil { + return false, err + } + normalizedMember := normalizeTeamMemberRouteKey(memberKey) + expectedAssignment := strings.TrimSpace(assignmentID) + expectedHash := "" + if len(contentHashes) > 0 { + expectedHash = strings.TrimSpace(contentHashes[0]) + } + for idx := range events { + event := events[idx] + if event.EventType != "member_result_confirmed" { + continue + } + if event.TaskID == nil || *event.TaskID != taskID { + continue + } + payload := teamEventPayloadMap(event) + from := normalizeTeamMemberRouteKey(eventString(payload, "from", "memberId", "member_id")) + if from == "" || !teamMemberRouteEquivalent(from, normalizedMember) { + continue + } + confirmedAssignmentID := eventString(payload, "assignmentId", "assignment_id", "workId", "work_id", "sourceWorkId", "source_work_id") + if expectedAssignment != "" && confirmedAssignmentID != "" && confirmedAssignmentID != expectedAssignment { + continue + } + confirmedHash := eventString(payload, "contentHash", "content_hash") + if confirmedHash == "" { + confirmedHash = teamResultContentHash(payload) + } + if expectedHash == "" || confirmedHash == "" { + continue + } + if confirmedHash == expectedHash { + return true, nil + } + } + return false, nil +} + +func (s *teamService) buildLeaderMediatedResultNotificationEnvelope(team *models.Team, task *models.TeamTask, member *models.TeamMember, notificationPayload map[string]interface{}, eventID string) (map[string]interface{}, string) { + leaderKey := "leader" + if s != nil && s.repo != nil { + if leader, err := s.repo.GetMemberByID(task.TargetMemberID); err == nil && leader != nil && strings.TrimSpace(leader.MemberKey) != "" { + leaderKey = leader.MemberKey + } + } + resultMarkdown := eventString(notificationPayload, "resultMarkdown", "summary") + prompt := fmt.Sprintf( + "Member %s delivered a confirmed assignment result for root task %s.\n\nResult:\n%s\n\nRecord this result in your synthesis context. If every required assignment has delivered, provide the final user-facing synthesis and call %s for the root task. Do not close the root task if any required assignment is still missing or blocked.", + member.MemberKey, + task.MessageID, + resultMarkdown, + teamTaskCompletionTool, + ) + envelope := map[string]interface{}{ + "v": 1, + "protocolVersion": 2, + "messageId": eventID, + "teamId": strconv.Itoa(team.ID), + "from": "clawmanager", + "to": leaderKey, + "replyTo": teamTaskReplyTarget, + "requiresCompletion": false, + "completionTool": teamTaskCompletionTool, + "intent": "member_result_confirmed", + "taskId": fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID), + "rootTaskId": fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID), + "rootMessageId": task.MessageID, + "workId": eventString(notificationPayload, "workId", "assignmentId"), + "assignmentId": eventString(notificationPayload, "assignmentId", "workId"), + "title": member.MemberKey + " assignment result confirmed", + "prompt": prompt, + "rawPrompt": prompt, + "monitorPolicy": defaultTeamMonitorPolicy(), + "metadata": notificationPayload, + "createdAt": time.Now().UTC().Format(time.RFC3339Nano), + } + applyTeamTaskEnvelopeContext(envelope, task, leaderKey) + return envelope, leaderKey +} + +func truncateForSummary(text string, max int) string { + value := strings.TrimSpace(text) + if value == "" || max <= 0 { + return value + } + runes := []rune(value) + if len(runes) <= max { + return value + } + return string(runes[:max]) + "..." +} + func (s *teamService) resolveTeamTaskFromEventReferences(teamID int, payload map[string]interface{}) (*models.TeamTask, error) { if payload == nil { return nil, nil } - for _, key := range []string{"rootMessageId", "root_message_id", "parentMessageId", "parent_message_id", "inReplyTo", "in_reply_to", "replyTo", "reply_to"} { + for _, key := range []string{ + "rootMessageId", "root_message_id", + "completionMessageId", "completion_message_id", + "messageId", "message_id", + "parentMessageId", "parent_message_id", + "inReplyTo", "in_reply_to", + "replyTo", "reply_to", + } { messageID := eventString(payload, key) if messageID == "" { continue @@ -3534,7 +6821,14 @@ func (s *teamService) resolveTeamTaskFromEventReferences(teamID int, payload map return found, nil } } - for _, key := range []string{"rootTaskId", "root_task_id", "parentTaskId", "parent_task_id", "currentTaskId", "current_task_id", "runtimeTaskId", "runtime_task_id"} { + for _, key := range []string{ + "rootTaskId", "root_task_id", + "completionTaskId", "completion_task_id", + "taskId", "task_id", + "parentTaskId", "parent_task_id", + "currentTaskId", "current_task_id", + "runtimeTaskId", "runtime_task_id", + } { if taskID := parseClawManagerTeamTaskRef(teamID, eventString(payload, key)); taskID > 0 { found, err := s.repo.GetTaskByID(taskID) if err != nil { @@ -3551,6 +6845,84 @@ func (s *teamService) resolveTeamTaskFromEventReferences(teamID int, payload map return nil, nil } +func (s *teamService) resolveLeaderMediatedTaskFromWorkItem(teamID int, payload map[string]interface{}, member *models.TeamMember) (*models.TeamTask, error) { + if s == nil || payload == nil || member == nil || isLeaderTeamMember(member) { + return nil, nil + } + status := normalizedTeamTaskEventStatus(payload) + eventType := strings.ToLower(strings.TrimSpace(eventString(payload, "event", "type", "event_type"))) + if !isLeaderMediatedWorkerResultLikeEvent(eventType, status, payload) && !isPassiveAssignmentMonitorEvent(eventType, payload) { + return nil, nil + } + ownerKey := normalizeTeamMemberRouteKey(member.MemberKey) + if ownerKey == "" { + return nil, nil + } + workIDs := map[string]struct{}{ + "member-" + ownerKey: {}, + } + for _, key := range []string{"workId", "work_id", "assignmentId", "assignment_id", "subtaskId", "subtask_id"} { + if value := normalizeTeamMemberRouteKey(eventString(payload, key)); value != "" { + workIDs[value] = struct{}{} + } + } + if step, ok := payload["collaborationStep"].(map[string]interface{}); ok { + for _, key := range []string{"workId", "work_id", "id", "assignmentId", "assignment_id"} { + if value := normalizeTeamMemberRouteKey(eventString(step, key)); value != "" { + workIDs[value] = struct{}{} + } + } + } + items, err := s.repo.ListWorkItemsByTeamID(teamID, 500) + if err != nil { + return nil, err + } + for idx := range items { + item := items[idx] + if item.TeamID != teamID || item.OwnerMemberID == nil || *item.OwnerMemberID != member.ID { + continue + } + normalizedWorkID := normalizeTeamMemberRouteKey(item.WorkID) + _, directWorkMatch := workIDs[normalizedWorkID] + canonicalOwnerMatch := normalizedWorkID == "member-"+ownerKey + if !directWorkMatch && !canonicalOwnerMatch { + continue + } + task, err := s.repo.GetTaskByID(item.RootTaskID) + if err != nil { + return nil, err + } + if task == nil || task.TeamID != teamID { + continue + } + if isPassiveAssignmentMonitorEvent(eventType, payload) && isTerminalTeamTaskStatus(task.Status) { + return nil, nil + } + payload["rootTaskId"] = fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID) + payload["rootMessageId"] = task.MessageID + if eventString(payload, "workId", "work_id") == "" { + payload["workId"] = item.WorkID + } + if eventString(payload, "assignmentId", "assignment_id") == "" { + payload["assignmentId"] = item.WorkID + } + return task, nil + } + return nil, nil +} + +func isLeaderMediatedWorkerResultLikeEvent(eventType, status string, payload map[string]interface{}) bool { + if isTeamTaskCompletionSignal(eventType, status, payload) || eventBool(payload, "assignmentResultOnly", "assignment_result_only") { + return true + } + switch strings.ToLower(strings.TrimSpace(eventType)) { + case "reply", "outbound", "completion", "task_completed": + return teamEventHasBody(payload) + default: + return false + } +} + func parseClawManagerTeamTaskRef(teamID int, raw string) int { value := strings.TrimSpace(raw) if value == "" { @@ -3796,6 +7168,9 @@ func (s *teamService) projectTeamWorkItem( if s == nil || team == nil || task == nil || payload == nil || event == nil { return nil } + if isLeaderControlPlaneSnapshotTask(task, payload) { + return nil + } step, _ := payload["collaborationStep"].(map[string]interface{}) stepType := eventString(step, "type") if stepType == "" { @@ -3804,7 +7179,15 @@ func (s *teamService) projectTeamWorkItem( if stepType == "" || stepType == "warning" { return nil } - explicitWorkID := eventString(payload, "workId", "work_id", "assignmentId", "assignment_id", "subtaskId", "subtask_id") + if isPassiveAssignmentMonitorEvent(eventType, payload) { + return nil + } + explicitAssignmentID := eventString(payload, "assignmentId", "assignment_id") + explicitSourceWorkID := eventString(payload, "workId", "work_id", "subtaskId", "subtask_id") + explicitWorkID := explicitAssignmentID + if explicitWorkID == "" { + explicitWorkID = explicitSourceWorkID + } // Transport acknowledgements and unscoped heartbeats are useful in the // event log, but they are not business work and must not become Kanban // cards. A progress event is materialized only when the orchestrator gave @@ -3820,7 +7203,20 @@ func (s *teamService) projectTeamWorkItem( if target == "" { target = leaderMediatedRouteTarget(payload) } + if isLeaderMediatedTeam(team) && stepType == "assignment" { + normalizedTarget := normalizeTeamMemberRouteKey(target) + if normalizedTarget == "" || isLeaderRouteTarget(normalizedTarget) { + return nil + } + } ownerKey := actor + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + if eventKind == "assignment_heartbeat" || strings.EqualFold(eventType, "assignment_heartbeat") { + return nil + } + if stepType == "progress" && eventKind == "assignment_check_requested" && target != "" { + ownerKey = target + } if stepType == "assignment" && target != "" { ownerKey = target } @@ -3833,6 +7229,9 @@ func (s *teamService) projectTeamWorkItem( if ownerKey == "" || ownerKey == "system" || ownerKey == "clawmanager" { return nil } + if isLeaderMediatedTeam(team) && stepType == "progress" && isLeaderRouteTarget(ownerKey) { + return nil + } owner := member if owner == nil || !teamMemberRouteEquivalent(owner.MemberKey, ownerKey) { found, err := s.repo.GetMemberByTeamKey(team.ID, ownerKey) @@ -3841,13 +7240,73 @@ func (s *teamService) projectTeamWorkItem( } owner = found } - workID := explicitWorkID - if workID == "" { - if rootCompletion { - workID = "leader-final-synthesis" - } else { - workID = "member-" + normalizeTeamMemberRouteKey(ownerKey) + if eventBool(payload, "assignmentResultOnly", "assignment_result_only") && owner != nil { + items, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID) + if listErr != nil { + return listErr } + matching := make([]models.TeamWorkItem, 0) + for idx := range items { + candidate := items[idx] + if candidate.OwnerMemberID == nil || *candidate.OwnerMemberID != owner.ID || candidate.SupersededBy != nil || candidate.WorkID == "leader-final-synthesis" { + continue + } + candidateAssignmentID := derefTeamString(candidate.AssignmentID) + if candidateAssignmentID == "" { + candidateAssignmentID = candidate.WorkID + } + if explicitWorkID != "" && (candidateAssignmentID == explicitWorkID || candidate.WorkID == explicitWorkID) { + matching = []models.TeamWorkItem{candidate} + break + } + matching = append(matching, candidate) + } + if len(matching) == 1 { + canonical := derefTeamString(matching[0].AssignmentID) + if canonical == "" { + canonical = matching[0].WorkID + } + if explicitSourceWorkID != "" && explicitSourceWorkID != canonical { + payload["sourceWorkId"] = explicitSourceWorkID + } + explicitAssignmentID = canonical + explicitWorkID = canonical + payload["assignmentId"] = canonical + payload["canonicalWorkId"] = canonical + if matching[0].PhaseID != nil && eventString(payload, "phaseId", "phase_id") == "" { + payload["phaseId"] = *matching[0].PhaseID + } + if matching[0].Revision > 0 && eventInt(payload, "revision") <= 0 { + payload["revision"] = matching[0].Revision + } + } + } + assignmentID := explicitAssignmentID + if assignmentID == "" { + assignmentID = explicitWorkID + } + workID := assignmentID + // The runtime may echo an assignmentId from a prompt or old plugin payload, + // but the backend ledger must be owned by the member that actually produced + // the result. Otherwise one worker can accidentally overwrite another + // worker's Kanban lane and the Leader will wait on the wrong member. + if rootCompletion { + workID = "leader-final-synthesis" + assignmentID = workID + } else if workID == "" { + workID = "member-" + normalizeTeamMemberRouteKey(ownerKey) + assignmentID = workID + } + revision := eventInt(payload, "revision", "workRevision", "work_revision", "artifactRevision", "artifact_revision") + if revision <= 0 { + revision = 1 + } + canonicalWorkID := eventString(payload, "canonicalWorkId", "canonical_work_id") + if canonicalWorkID == "" { + canonicalWorkID = assignmentID + } + if revision > 1 && !strings.HasSuffix(workID, fmt.Sprintf(":r%d", revision)) { + workID = fmt.Sprintf("%s:r%d", workID, revision) } status := models.TeamTaskStatusRunning switch stepType { @@ -3868,13 +7327,39 @@ func (s *teamService) projectTeamWorkItem( if title == "" { title = collaborationStepTitle(stepType, actor, target, payload) } + requiredForRoot := !eventBool(payload, "optional", "isOptional", "is_optional") + if required, ok := teamEventBoolValue(payload, "required", "requiredForRoot", "required_for_root"); ok { + requiredForRoot = required + } item := &models.TeamWorkItem{ - TeamID: team.ID, - RootTaskID: task.ID, - WorkID: workID, - Title: title, - Status: status, - UpdatedAt: now, + TeamID: team.ID, + RootTaskID: task.ID, + WorkID: workID, + Title: title, + Status: status, + Revision: revision, + RequiredForRoot: requiredForRoot, + ReviewRequired: eventBool(payload, "reviewRequired", "review_required"), + UpdatedAt: now, + } + if assignmentID != "" { + item.AssignmentID = &assignmentID + } + if canonicalWorkID != "" { + item.CanonicalWorkID = &canonicalWorkID + } + phaseID := eventString(payload, "phaseId", "phase_id", "phase", "stage") + if phaseID == "" { + phaseID = eventString(step, "phase") + } + if phaseID != "" { + item.PhaseID = &phaseID + } + if validatedRevision := eventInt(payload, "validatedRevision", "validated_revision", "reviewedRevision", "reviewed_revision"); validatedRevision > 0 { + item.ValidatedRevision = &validatedRevision + } + if supersededBy := eventString(payload, "supersededBy", "superseded_by"); supersededBy != "" { + item.SupersededBy = &supersededBy } if owner != nil { item.OwnerMemberID = &owner.ID @@ -3894,12 +7379,38 @@ func (s *teamService) projectTeamWorkItem( } } } - if dependencies := normalizeContextRefs(step["dependsOn"]); len(dependencies) > 0 { + dependencies := normalizeContextRefs(step["dependsOn"]) + if len(dependencies) == 0 { + dependencies = normalizeContextRefs(firstTeamValue(payload, "dependsOn", "depends_on")) + } + if len(dependencies) > 0 { if encoded, err := json.Marshal(dependencies); err == nil { value := string(encoded) item.DependsOnJSON = &value } } + if assignmentID != "" && revision > 1 { + existingItems, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID) + if listErr != nil { + return listErr + } + for idx := range existingItems { + existing := existingItems[idx] + existingAssignmentID := derefTeamString(existing.AssignmentID) + if existingAssignmentID == "" { + existingAssignmentID = existing.WorkID + } + if existingAssignmentID != assignmentID || teamMaxInt(existing.Revision, 1) >= revision || existing.SupersededBy != nil { + continue + } + supersededBy := workID + existing.SupersededBy = &supersededBy + existing.UpdatedAt = now + if err := s.repo.UpsertWorkItem(&existing); err != nil { + return err + } + } + } return s.repo.UpsertWorkItem(item) } @@ -3976,8 +7487,186 @@ func collaborationStepContentForEvent(stepType string, payload map[string]interf return fallback } +func (s *teamService) projectTeamWorkflowLedger(team *models.Team, task *models.TeamTask, member *models.TeamMember, eventType string, payload map[string]interface{}, now time.Time) (bool, error) { + if s == nil || team == nil || task == nil || payload == nil || isTerminalTeamTaskStatus(task.Status) { + return false, nil + } + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + step, _ := payload["collaborationStep"].(map[string]interface{}) + stepType := strings.ToLower(strings.TrimSpace(eventString(step, "type"))) + isPlan := eventKind == "leader_plan" && member != nil && isLeaderTeamMember(member) + isAssignment := stepType == "assignment" || eventBool(payload, "leaderDispatchOnly", "leader_dispatch_only") || eventType == "team_send" || eventType == "task_assigned" || eventType == "outbound" + isAssignmentResult := eventBool(payload, "assignmentResultOnly", "assignment_result_only") + if !isPlan && !isAssignment && !isAssignmentResult { + return false, nil + } + + changed := false + planVersion := int64(eventInt(payload, "planVersion", "plan_version")) + if planVersion <= 0 { + planVersion = task.PlanVersion + if planVersion <= 0 { + planVersion = 1 + } + } + if planVersion > task.PlanVersion { + task.PlanVersion = planVersion + changed = true + } + + if isPlan { + phaseValues := firstTeamValue(payload, "phases", "workflowPhases", "workflow_phases") + if plan, ok := payload["workflowPlan"].(map[string]interface{}); ok { + if nested := firstTeamValue(plan, "phases", "workflowPhases", "workflow_phases"); nested != nil { + phaseValues = nested + } + } + if rawPhases, ok := phaseValues.([]interface{}); ok { + for index, rawPhase := range rawPhases { + phaseMap, ok := rawPhase.(map[string]interface{}) + if !ok { + continue + } + phaseID := eventString(phaseMap, "phaseId", "phase_id", "id", "name") + if phaseID == "" { + continue + } + phase := &models.TeamWorkflowPhase{ + TeamID: team.ID, + RootTaskID: task.ID, + PhaseID: phaseID, + PlanVersion: planVersion, + SequenceNo: index, + Status: eventString(phaseMap, "status"), + RequiredForRoot: true, + DecisionRequired: eventBool(phaseMap, "decisionRequiredAfterCompletion", "decisionRequired", "decision_required"), + UpdatedAt: now, + } + if phase.Status == "" { + phase.Status = teamPhaseStatusPlanned + } + if required, exists := teamEventBoolValue(phaseMap, "required", "requiredForRoot", "required_for_root"); exists { + phase.RequiredForRoot = required + } + if dependencies := normalizeContextRefs(firstTeamValue(phaseMap, "dependsOn", "depends_on")); len(dependencies) > 0 { + if encoded, err := json.Marshal(dependencies); err == nil { + value := string(encoded) + phase.DependsOnJSON = &value + } + } + if nextPhaseID := eventString(phaseMap, "nextPhaseId", "next_phase_id"); nextPhaseID != "" { + phase.NextPhaseID = &nextPhaseID + } + if policy := eventString(phaseMap, "completionPolicy", "completion_policy"); policy != "" { + phase.CompletionPolicy = &policy + } + if err := s.repo.UpsertWorkflowPhase(phase); err != nil { + return changed, err + } + if task.CurrentPhaseID == nil && phase.Status != teamPhaseStatusPlanned { + task.CurrentPhaseID = &phaseID + } + changed = true + } + } + workflowState := eventString(payload, "workflowState", "workflow_state") + if workflowState == "" || workflowState == "open" { + workflowState = teamWorkflowStateExecuting + } + if task.WorkflowState != workflowState { + task.WorkflowState = workflowState + changed = true + } + } + + phaseID := eventString(payload, "phaseId", "phase_id", "phase", "stage") + if phaseID == "" { + phaseID = eventString(step, "phase") + } + if isAssignment { + if task.WorkflowState != teamWorkflowStateExecuting && task.WorkflowState != teamWorkflowStateAwaitingPhaseResults { + task.WorkflowState = teamWorkflowStateExecuting + changed = true + } + if phaseID != "" { + task.CurrentPhaseID = &phaseID + phase := &models.TeamWorkflowPhase{ + TeamID: team.ID, + RootTaskID: task.ID, + PhaseID: phaseID, + PlanVersion: planVersion, + Status: teamPhaseStatusAwaitingResults, + RequiredForRoot: true, + UpdatedAt: now, + } + if err := s.repo.UpsertWorkflowPhase(phase); err != nil { + return changed, err + } + changed = true + } + } + if isAssignmentResult && phaseID != "" { + phases, err := s.repo.ListWorkflowPhasesByRootTaskID(task.ID) + if err != nil { + return changed, err + } + items, err := s.repo.ListWorkItemsByRootTaskID(task.ID) + if err != nil { + return changed, err + } + phaseIncomplete := false + for idx := range items { + item := items[idx] + if derefTeamString(item.PhaseID) != phaseID || item.SupersededBy != nil || !(item.RequiredForRoot || item.AssignmentID == nil) { + continue + } + if item.Status != models.TeamTaskStatusSucceeded { + phaseIncomplete = true + break + } + } + for idx := range phases { + phase := phases[idx] + if phase.PhaseID != phaseID || phase.PlanVersion != planVersion { + continue + } + if phaseIncomplete { + phase.Status = teamPhaseStatusAwaitingResults + task.WorkflowState = teamWorkflowStateAwaitingPhaseResults + } else if phase.DecisionRequired { + phase.Status = teamPhaseStatusAwaitingLeaderDecision + task.WorkflowState = teamWorkflowStateAwaitingLeaderDecision + } else { + phase.Status = teamPhaseStatusCompleted + phase.CompletedAt = &now + task.WorkflowState = teamWorkflowStateAwaitingLeaderDecision + } + phase.UpdatedAt = now + if err := s.repo.UpsertWorkflowPhase(&phase); err != nil { + return changed, err + } + changed = true + break + } + } + if changed { + task.LedgerVersion++ + task.UpdatedAt = now + } + return changed, nil +} + func collaborationStepTypeForEvent(eventType string, payload map[string]interface{}) string { status := normalizedTeamTaskEventStatus(payload) + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + switch eventKind { + case "leader_plan", "worker_plan", "worker_progress", "artifact_changed", "assignment_check_requested", "assignment_check_result", "assignment_heartbeat", "leader_synthesis", "leader_synthesis_reminder", "leader_decision_reminder", "completion_deferred", "assignment_recovery_started", "assignment_reissued", "agent_narrative", "agent_plan", "agent_assignment", "agent_handoff", "agent_progress", "agent_delivery", "agent_review", "agent_synthesis": + return "progress" + case "completion_candidate": + return "progress" + case "completion_validation_warning", "completion_needs_confirmation", "completion_rejected", "assignment_recovery_exhausted": + return "warning" + } if eventBool(payload, "leaderMediatedRouteViolation", "leader_mediated_route_violation") { return "warning" } @@ -3994,6 +7683,10 @@ func collaborationStepTypeForEvent(eventType string, payload map[string]interfac return "warning" } switch eventType { + case "leader_plan", "worker_plan", "worker_progress", "artifact_changed", "assignment_check_requested", "assignment_check_result", "assignment_heartbeat", "leader_synthesis", "leader_synthesis_reminder", "leader_decision_reminder", "completion_candidate", "completion_deferred", "assignment_recovery_started", "assignment_reissued", "agent_narrative", "agent_plan", "agent_assignment", "agent_handoff", "agent_progress", "agent_delivery", "agent_review", "agent_synthesis": + return "progress" + case "completion_validation_warning", "completion_needs_confirmation", "completion_rejected", "assignment_recovery_exhausted": + return "warning" case "outbound", "task_assigned", "team_send": return "assignment" case "peer_handoff": @@ -4030,6 +7723,15 @@ func collaborationStepTypeForEvent(eventType string, payload map[string]interfac func collaborationStepStatusForEvent(eventType string, payload map[string]interface{}) string { status := normalizedTeamTaskEventStatus(payload) + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) + switch eventKind { + case "assignment_check_requested", "assignment_check_result", "assignment_heartbeat", "leader_synthesis_reminder", "assignment_recovery_started", "assignment_reissued": + return models.TeamTaskStatusRunning + case "completion_candidate": + return "result_pending_confirmation" + case "completion_validation_warning", "assignment_recovery_exhausted": + return "warning" + } if eventBool(payload, "leaderMediatedRouteViolation", "leader_mediated_route_violation") { return "warning" } @@ -4057,6 +7759,12 @@ func collaborationStepStatusForEvent(eventType string, payload map[string]interf return models.TeamTaskStatusFailed } switch eventType { + case "assignment_check_requested", "assignment_check_result", "assignment_heartbeat", "leader_synthesis_reminder", "assignment_recovery_started", "assignment_reissued": + return models.TeamTaskStatusRunning + case "completion_candidate": + return "result_pending_confirmation" + case "completion_validation_warning", "assignment_recovery_exhausted": + return "warning" case "task_stale": return models.TeamTaskStatusStale case "outbound", "task_assigned", "team_send", "peer_request", "peer_handoff", "peer_review_request": @@ -4089,6 +7797,39 @@ func collaborationStepTitle(stepType, actor, target string, payload map[string]i if title := eventString(payload, "stepTitle", "step_title", "title", "intent"); title != "" && !looksLikeOpaqueRuntimeTaskID(title) { return title } + switch strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) { + case "leader_plan": + return "Leader execution plan" + case "worker_plan": + return actor + " execution plan" + case "worker_progress": + return actor + " updates progress" + case "assignment_check_requested": + if target != "" { + return "ClawManager checks " + target + } + return "ClawManager checks assignment status" + case "assignment_check_result": + return actor + " reports status" + case "assignment_heartbeat": + return actor + " heartbeat" + case "leader_synthesis": + return "Leader synthesizes results" + case "agent_narrative", "agent_plan", "agent_assignment", "agent_handoff", "agent_progress", "agent_delivery", "agent_review", "agent_synthesis": + return actor + " collaboration update" + case "leader_synthesis_reminder": + return "Leader final synthesis requested" + case "completion_candidate": + return actor + " produced candidate result" + case "completion_validation_warning": + return "Completion needs artifact recovery" + case "assignment_recovery_started": + return "Recovery started for " + actor + case "assignment_reissued": + return "Assignment reissued to " + actor + case "assignment_recovery_exhausted": + return "Recovery needs attention" + } switch stepType { case "assignment": if target != "" { @@ -4149,7 +7890,7 @@ func inferCollaborationPhase(stepType, title, summary string, payload map[string return "verification" case strings.Contains(text, "design") || strings.Contains(text, "ui") || strings.Contains(text, "prototype"): return "design" - case strings.Contains(text, "research") || strings.Contains(text, "调研"): + case strings.Contains(text, "research") || strings.Contains(text, "璋冪爺"): return "research" case stepType == "assignment": return "decomposition" @@ -4203,6 +7944,87 @@ func normalizeFinalReplyTaskEvent(eventType string, payload map[string]interface return "task_completed" } +func markLegacyRuntimeCompletionCandidate(eventType string, payload map[string]interface{}, task *models.TeamTask, member *models.TeamMember) string { + if task == nil || member == nil || payload == nil || teamRedisProtocolVersion(payload) >= 2 { + return eventType + } + normalizedEvent := strings.ToLower(strings.TrimSpace(eventType)) + if normalizedEvent != "reply" && normalizedEvent != "message" { + return eventType + } + explicitControlPlaneCompletion := isLeaderControlPlaneSnapshotTask(task, payload) && + hasTeamCompletionResultBody(payload) && + (strings.EqualFold(strings.TrimSpace(eventString(payload, "completionSource", "completion_source")), teamTaskCompletionTool) || + hasTeamTaskCompletionToolCall(payload) || + eventBool(payload, "explicitCompletion", "explicit_completion")) + if member.ID != task.TargetMemberID || (isDispatchOnlyCompletionPayload(payload) && !explicitControlPlaneCompletion) { + return eventType + } + resultText := directTaskCompletionReplyText(payload) + if resultText == "" { + resultText = eventString(payload, "summary") + } + if resultText == "" { + return eventType + } + if !explicitControlPlaneCompletion { + if isInterimOrDelegationReplyText(resultText) || !looksLikeLegacyRuntimeCompletionReport(task, payload, resultText) { + return eventType + } + } + payload["legacyCompletionCandidate"] = true + payload["completionSource"] = "legacy_runtime_reply" + payload["rootTaskTerminal"] = true + payload["final"] = true + payload["status"] = models.TeamTaskStatusSucceeded + payload["availability"] = models.TeamMemberAvailabilityIdle + payload["runtimeStatus"] = models.TeamTaskStatusSucceeded + if eventString(payload, "resultMarkdown", "result_markdown") == "" { + payload["resultMarkdown"] = resultText + } + if eventString(payload, "summary") == "" { + payload["summary"] = compactTeamEventSummary(resultText, 240) + } + if eventString(payload, "originalEvent") == "" { + payload["originalEvent"] = eventType + } + payload["event"] = "task_completed" + payload["type"] = "task_completed" + return "task_completed" +} + +func looksLikeLegacyRuntimeCompletionReport(task *models.TeamTask, payload map[string]interface{}, resultText string) bool { + text := strings.TrimSpace(resultText) + if text == "" { + return false + } + lower := strings.ToLower(text) + compact := strings.ToLower(strings.Join(strings.Fields(text), "")) + hasCanonicalArtifact := regexp.MustCompile(`/team/(artifacts|results|tasks)/[^\s)\]<>{},\\\x60"']+\.[A-Za-z0-9]+`).FindString(text) != "" + hasCompletionWord := strings.Contains(lower, "completed") || + strings.Contains(lower, "complete") || + strings.Contains(lower, "succeeded") || + strings.Contains(lower, "delivered") || + strings.Contains(text, "\u5b8c\u6210") || + strings.Contains(text, "\u5df2\u5b8c\u6210") || + strings.Contains(text, "\u4ea4\u4ed8") + hasBootstrapWord := strings.Contains(lower, "bootstrap") || + strings.Contains(lower, "introduction") || + strings.Contains(text, "\u5f15\u5bfc") || + strings.Contains(text, "\u4ecb\u7ecd") || + strings.Contains(text, "\u56e2\u961f") + if hasCanonicalArtifact && (hasCompletionWord || looksLikeFinalResultText(text) || len([]rune(compact)) >= 80) { + return true + } + if strings.Contains(strings.ToLower(task.MessageID), "bootstrap-introduction") && hasBootstrapWord && hasCompletionWord && len([]rune(compact)) >= 80 { + return true + } + if eventBool(payload, "final", "isFinal", "complete", "completed", "taskCompleted") && looksLikeFinalResultText(text) && len([]rune(compact)) >= 120 { + return true + } + return false +} + func isImplicitDirectTaskCompletionReply(task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}) bool { if task == nil || member == nil || payload == nil { return false @@ -4242,6 +8064,18 @@ func directTaskCompletionReplyText(payload map[string]interface{}) string { return "" } +func compactTeamEventSummary(value string, limit int) string { + text := strings.TrimSpace(strings.Join(strings.Fields(value), " ")) + if text == "" || limit <= 0 { + return text + } + runes := []rune(text) + if len(runes) <= limit { + return text + } + return string(runes[:limit]) + "..." +} + func isInterimOrDelegationReplyText(text string) bool { normalized := strings.TrimSpace(text) if normalized == "" { @@ -4258,28 +8092,95 @@ func isInterimOrDelegationReplyText(text string) bool { if len([]rune(compact)) <= 12 { return true } - for _, prefix := range []string{ - "收到", "好的", "好,", "好,", "ok", "okay", "处理中", "正在", "准备", "我将", "让我", "先看", "稍等", - "let me", "now let me", "i will", "i'll", "checking", "working on", "good, i have", "i have the", "i have all", "i can see", - } { - if strings.HasPrefix(lower, prefix) || strings.HasPrefix(normalized, prefix) { + for _, prefix := range interimReplyPrefixes() { + if strings.HasPrefix(lower, strings.ToLower(prefix)) || strings.HasPrefix(normalized, prefix) { return true } } - for _, marker := range []string{ - "正在整理", "现在整理", "稍后", "等待其", "等待他", "等待她", "等待worker", "等待 worker", - "派单", "已派发", "派发给", "下发给", "转派给", "交给worker", "交给 worker", - "让worker", "让 worker", "请worker", "请 worker", "worker在线空闲", - "sent to worker", "assigned to worker", "waiting for worker", "handoff to worker", - "now let me write", "then finalize", "and then finalize", "write the comprehensive report", - } { - if strings.Contains(compact, strings.ReplaceAll(strings.ToLower(marker), " ", "")) || strings.Contains(lower, marker) { - return true - } + if containsAnyTeamTextMarker(normalized, interimReplyMarkers()) { + return true } return false } +func interimReplyPrefixes() []string { + return []string{ + "\u6536\u5230", + "\u597d\u7684", + "\u597d\uff0c", + "\u597d", + "ok", + "okay", + "\u5904\u7406\u4e2d", + "\u6b63\u5728", + "\u51c6\u5907", + "\u6211\u5c06", + "\u8ba9\u6211", + "\u5148\u770b", + "\u7a0d\u7b49", + "let me", + "now let me", + "i will", + "i'll", + "checking", + "working on", + "good, i have", + "i have the", + "i have all", + "i can see", + } +} + +func interimReplyMarkers() []string { + return []string{ + "\u6b63\u5728\u6574\u7406", + "\u73b0\u5728\u6574\u7406", + "\u7a0d\u540e", + "\u7ee7\u7eed\u7b49\u5f85", + "\u4ecd\u5728\u7b49\u5f85", + "\u5df2\u6536\u5230\u5e76\u5f52\u6863", + "\u5ef6\u8fdf\u9001\u8fbe", + "\u91cd\u590d\u901a\u77e5", + "\u91cd\u590d\u7ed3\u679c", + "\u91cd\u590d\u6295\u9012", + "\u7b49\u5f85\u5b8c\u6210", + "\u7b49\u5f85\u4ed6", + "\u7b49\u5f85\u5979", + "\u7b49\u5f85 designer", + "\u7b49\u5f85 architect", + "\u7b49\u5f85 pm", + "\u7b49\u5f85worker", + "\u7b49\u5f85 worker", + "\u6d3e\u5355", + "\u5df2\u6d3e\u53d1", + "\u6d3e\u53d1\u7ed9", + "\u4e0b\u53d1\u7ed9", + "\u8f6c\u6d3e\u7ed9", + "\u4ea4\u7ed9worker", + "\u4ea4\u7ed9 worker", + "\u8ba9worker", + "\u8ba9 worker", + "\u8bf7worker", + "\u8bf7 worker", + "worker\u5728\u7ebf\u7a7a\u95f2", + "sent to worker", + "assigned to worker", + "waiting for worker", + "still waiting", + "continuing to wait", + "waiting on ", + "duplicate notification", + "duplicate result", + "duplicate delivery", + "delayed notification", + "handoff to worker", + "now let me write", + "then finalize", + "and then finalize", + "write the comprehensive report", + } +} + func looksLikeSubstantialFinalReply(text string) bool { normalized := strings.TrimSpace(text) if normalized == "" { @@ -4288,17 +8189,18 @@ func looksLikeSubstantialFinalReply(text string) bool { if strings.ContainsAny(normalized, "#*>|`") { return true } - if strings.ContainsAny(normalized, "。;;::\n") { + if strings.ContainsAny(normalized, ";\n") || strings.Contains(normalized, "\u3002") || strings.Contains(normalized, "\uff1b") || strings.Contains(normalized, "\uff0c") { return true } - return strings.Contains(normalized, "完成") || - strings.Contains(normalized, "总结") || - strings.Contains(normalized, "报告") || - strings.Contains(normalized, "结果") || - strings.Contains(strings.ToLower(normalized), "completed") || - strings.Contains(strings.ToLower(normalized), "summary") + return containsAnyTeamTextMarker(normalized, []string{ + "\u5b8c\u6210", + "\u603b\u7ed3", + "\u62a5\u544a", + "\u7ed3\u679c", + "completed", + "summary", + }) } - func hasTeamTaskCompletionToolCall(payload map[string]interface{}) bool { if payload == nil { return false @@ -4446,9 +8348,11 @@ func (s *teamService) normalizeTeamArtifactReferences(team *models.Team, payload return teamSharedMountPath + strings.TrimPrefix(value, "$CLAWMANAGER_TEAM_SHARED_DIR") case strings.HasPrefix(value, "CLAWMANAGER_TEAM_SHARED_DIR/"): return teamSharedMountPath + strings.TrimPrefix(value, "CLAWMANAGER_TEAM_SHARED_DIR") - case strings.HasPrefix(value, "team/results/") || + case strings.HasPrefix(value, "team/artifacts/") || + strings.HasPrefix(value, "team/results/") || strings.HasPrefix(value, "team/tasks/") || strings.HasPrefix(value, "team/inbox/") || + strings.HasPrefix(value, "team/tmp/") || strings.HasPrefix(value, "team/status/"): return "/" + value default: @@ -4468,7 +8372,7 @@ func (s *teamService) normalizeTeamArtifactReferences(team *models.Team, payload text = strings.ReplaceAll(text, physicalRoot+"/", teamSharedMountPath+"/") } text = strings.ReplaceAll(text, "$CLAWMANAGER_TEAM_SHARED_DIR/", teamSharedMountPath+"/") - text = regexp.MustCompile(`(^|[\s(\[>])team/(results|tasks|inbox|status)/([^\s)\]<>{},\\\x60"']+)`).ReplaceAllString(text, `${1}/team/${2}/${3}`) + text = regexp.MustCompile(`(^|[\s(\[>])team/(artifacts|results|tasks|inbox|status|tmp)/([^\s)\]<>{},\\\x60"']+)`).ReplaceAllString(text, `${1}/team/${2}/${3}`) return text } var normalizeValue func(value interface{}) interface{} @@ -4503,9 +8407,10 @@ func (s *teamService) missingTeamArtifactReferences(team *models.Team, payload m if s == nil || team == nil || payload == nil { return nil } + explicitRefsOnly := teamRedisProtocolVersion(payload) >= 2 refs := collectTeamArtifactReferences(payload) invalidRelativeRefs := collectInvalidRelativeTeamArtifactReferences(payload) - if teamRedisProtocolVersion(payload) >= 2 { + if explicitRefsOnly { refs = explicitTeamArtifactReferences(payload) invalidRelativeRefs = nil } @@ -4536,7 +8441,7 @@ func (s *teamService) missingTeamArtifactReferences(team *models.Team, payload m if os.IsNotExist(err) { missing = append(missing, ref) } - } else if !info.Mode().IsRegular() { + } else if !info.Mode().IsRegular() && explicitRefsOnly { missing = append(missing, ref) } } @@ -4554,10 +8459,13 @@ func explicitTeamArtifactReferences(payload map[string]interface{}) []string { refs := make([]string, 0) seen := map[string]struct{}{} appendRef := func(raw interface{}) { - ref := strings.TrimSpace(fmt.Sprintf("%v", raw)) + ref := trimTeamArtifactReferenceToken(fmt.Sprintf("%v", raw)) if !strings.HasPrefix(ref, teamSharedMountPath+"/") || strings.ContainsAny(ref, "`\"'{}[]") { return } + if strings.HasSuffix(ref, "/") { + return + } if _, exists := seen[ref]; exists { return } @@ -4583,15 +8491,15 @@ func collectTeamArtifactReferences(value interface{}) []string { // the same completion payload as text, Markdown, or serialized JSON; a // greedy token here turns `/team/file.md` into a nonexistent filename such // as `/team/file.md`/n\",\"artifactRefs\"...`. - pattern := regexp.MustCompile(`/team/(results|tasks|inbox|status)/[^\s)\]<>{},\\\x60"']+`) + pattern := regexp.MustCompile(`/team/(artifacts|results|tasks|inbox|status|tmp)/[^\s)\]<>{},\\\x60"']+`) seen := map[string]struct{}{} var walk func(interface{}) walk = func(current interface{}) { switch typed := current.(type) { case string: for _, match := range pattern.FindAllString(typed, -1) { - ref := strings.TrimRight(match, ".,;:,。;、") - if ref == "" { + ref := trimTeamArtifactReferenceToken(match) + if ref == "" || !looksLikeTeamArtifactFileReference(ref) { continue } if _, exists := seen[ref]; exists { @@ -4614,6 +8522,28 @@ func collectTeamArtifactReferences(value interface{}) []string { return refs } +func trimTeamArtifactReferenceToken(value string) string { + ref := strings.TrimSpace(value) + if ref == "" { + return "" + } + ref = strings.TrimRight(ref, " \t\r\n.,;:!?。;:,、!?)") + ref = strings.TrimRight(ref, "]}>)】》”’") + return ref +} + +func looksLikeTeamArtifactFileReference(ref string) bool { + ref = strings.TrimSpace(ref) + if ref == "" || strings.HasSuffix(ref, "/") { + return false + } + lastSlash := strings.LastIndex(ref, "/") + name := ref + if lastSlash >= 0 { + name = ref[lastSlash+1:] + } + return strings.Contains(name, ".") +} func collectInvalidRelativeTeamArtifactReferences(value interface{}) []string { refs := make([]string, 0) pattern := regexp.MustCompile(`(^|[\s(\[>])(team/[^\s)\]<>{},\\\x60"']+)`) @@ -4626,10 +8556,12 @@ func collectInvalidRelativeTeamArtifactReferences(value interface{}) []string { if len(match) < 3 { continue } - ref := strings.TrimRight(match[2], ".,;:,。;、") - if strings.HasPrefix(ref, "team/results/") || + ref := strings.TrimRight(match[2], ".,;:") + if strings.HasPrefix(ref, "team/artifacts/") || + strings.HasPrefix(ref, "team/results/") || strings.HasPrefix(ref, "team/tasks/") || strings.HasPrefix(ref, "team/inbox/") || + strings.HasPrefix(ref, "team/tmp/") || strings.HasPrefix(ref, "team/status/") { continue } @@ -4652,6 +8584,22 @@ func collectInvalidRelativeTeamArtifactReferences(value interface{}) []string { walk(value) return refs } +func buildInitialLeaderTaskPayload(teamName string) map[string]interface{} { + normalizedTeamName := strings.TrimSpace(teamName) + if normalizedTeamName == "" { + normalizedTeamName = "current" + } + prompt := fmt.Sprintf("\u8bf7\u4ecb\u7ecdteam %s\u5f53\u524d Redis Team\u6210\u5458\u6784\u6210\uff0c\u5305\u62ec\u5404\u89d2\u8272\u7684\u804c\u8d23\u5206\u5de5\u3001\u8fd0\u884c\u72b6\u6001\u4e0e\u6280\u672f\u80fd\u529b\u8fb9\u754c\u3002\u540c\u65f6\u8bf4\u660e\u56e2\u961f\u5185\u90e8\u7684\u534f\u4f5c\u4e0e\u901a\u4fe1\u673a\u5236(team_send)\uff0c\u4f8b\u5982\u4efb\u52a1\u6d41\u8f6c\u65b9\u5f0f\u3001\u6d88\u606f\u540c\u6b65\u65b9\u5f0f\u3001\u4e0a\u4e0b\u6587\u5171\u4eab\u65b9\u5f0f\u4ee5\u53ca\u53ef\u8c03\u7528\u7684\u65b9\u6cd5\u3001\u5de5\u5177\u4e0e\u64cd\u4f5c\u80fd\u529b\uff0c\u4ee5\u4fbf\u540e\u7eed\u80fd\u591f\u66f4\u9ad8\u6548\u5730\u5f00\u5c55\u56e2\u961f\u5de5\u4f5c", normalizedTeamName) + return map[string]interface{}{ + "intent": initialLeaderTaskIntent, + "title": "\u4ecb\u7ecd\u5f53\u524d Redis Team \u6210\u5458\u4e0e\u534f\u4f5c\u673a\u5236", + "prompt": prompt, + "origin": "system_bootstrap", + "executionMode": "leader_control_plane_snapshot", + "requiresDelegation": false, + "anchorEligible": false, + } +} func (s *teamService) markTeamFailed(team *models.Team, cause error) error { team.Status = models.TeamStatusFailed @@ -4677,7 +8625,6 @@ func (s *teamService) rollbackTeamCreation(userID int, team *models.Team, cause member.UpdatedAt = time.Now().UTC() _ = s.repo.UpdateMember(&member) } - ctx := context.Background() if strings.TrimSpace(derefTeamString(team.TeamTokenSecretName)) != "" { if err := s.secretService.DeleteSecret(ctx, userID, derefTeamString(team.TeamTokenSecretName)); err != nil { @@ -4690,7 +8637,6 @@ func (s *teamService) rollbackTeamCreation(userID int, team *models.Team, cause if err := s.pvcService.DeleteTeamSharedPVC(ctx, userID, team.ID); err != nil { fmt.Printf("Warning: failed to delete Team %d shared PVC during create rollback: %v\n", team.ID, err) } - team.Name = deletedTeamName(team.Name, team.ID) team.Status = models.TeamStatusDeleted team.UpdatedAt = time.Now().UTC() @@ -4703,14 +8649,23 @@ func (s *teamService) rollbackTeamCreation(userID int, team *models.Team, cause func teamTaskPayloads(tasks []models.TeamTask) []TeamTaskPayload { result := make([]TeamTaskPayload, 0, len(tasks)) for _, task := range tasks { - if payload, err := teamTaskPayload(task); err == nil { - result = append(result, *payload) + payload, err := teamTaskPayload(task) + if err != nil { + result = append(result, TeamTaskPayload{TeamTask: task}) + continue } + result = append(result, *payload) } return result } func normalizeTeamHistoryLimit(limit, defaultLimit, maxLimit int) int { + if defaultLimit <= 0 { + defaultLimit = 50 + } + if maxLimit <= 0 { + maxLimit = defaultLimit + } if limit <= 0 { return defaultLimit } @@ -4724,7 +8679,16 @@ func nextTeamTaskBeforeID(tasks []TeamTaskPayload) *int { if len(tasks) == 0 { return nil } - next := tasks[len(tasks)-1].ID + minID := tasks[0].ID + for _, task := range tasks[1:] { + if task.ID < minID { + minID = task.ID + } + } + if minID <= 0 { + return nil + } + next := minID return &next } @@ -4732,12 +8696,21 @@ func nextTeamEventBeforeID(events []TeamEventPayload) *int { if len(events) == 0 { return nil } - next := events[len(events)-1].ID + minID := events[0].ID + for _, event := range events[1:] { + if event.ID < minID { + minID = event.ID + } + } + if minID <= 0 { + return nil + } + next := minID return &next } func teamTaskPayload(task models.TeamTask) (*TeamTaskPayload, error) { - payload := &TeamTaskPayload{TeamTask: task} + payload := TeamTaskPayload{TeamTask: task} if strings.TrimSpace(task.PayloadJSON) != "" { if err := json.Unmarshal([]byte(task.PayloadJSON), &payload.Payload); err != nil { return nil, err @@ -4748,24 +8721,7 @@ func teamTaskPayload(task models.TeamTask) (*TeamTaskPayload, error) { return nil, err } } - return payload, nil -} - -func buildInitialLeaderTaskPayload(teamName string) map[string]interface{} { - normalizedTeamName := strings.TrimSpace(teamName) - if normalizedTeamName == "" { - normalizedTeamName = "current" - } - prompt := fmt.Sprintf("请介绍`team %s`当前 Redis Team成员构成,包括各角色的职责分工、运行状态与技术能力边界。同时说明团队内部的协作与通信机制(team_send),例如任务流转方式、消息同步方式、上下文共享方式以及可调用的方法、工具与操作能力,以便后续能够更高效地开展团队工作", normalizedTeamName) - return map[string]interface{}{ - "intent": initialLeaderTaskIntent, - "title": "介绍当前 Redis Team 成员与协作机制", - "prompt": prompt, - "origin": "system_bootstrap", - "executionMode": "leader_control_plane_snapshot", - "requiresDelegation": false, - "anchorEligible": false, - } + return &payload, nil } func teamEventPayloads(events []models.TeamEvent) []TeamEventPayload { @@ -4775,6 +8731,13 @@ func teamEventPayloads(events []models.TeamEvent) []TeamEventPayload { if event.PayloadJSON != nil && strings.TrimSpace(*event.PayloadJSON) != "" { _ = json.Unmarshal([]byte(*event.PayloadJSON), &payload.Payload) } + chatPolicy := strings.ToLower(strings.TrimSpace(eventString(payload.Payload, "chatPolicy", "chat_policy"))) + hidden, hiddenDefined := teamEventBoolValue(payload.Payload, "visibleToChat", "visible_to_chat") + eventKind := strings.ToLower(strings.TrimSpace(eventString(payload.Payload, "eventKind", "event_kind", "kind"))) + business := teamChatEventIsBusinessContent(strings.ToLower(strings.TrimSpace(event.EventType)), eventKind, payload.Payload) + if event.EventType == "member_result_confirmed" || (!business && (chatPolicy == "hidden" || (hiddenDefined && !hidden && chatPolicy == ""))) { + continue + } result = append(result, payload) } return result @@ -4894,7 +8857,7 @@ func applyTeamMemberRuntimeProjection(member *models.TeamMember, payload map[str if runtimeIntent := eventString(payload, "runtime_intent", "runtimeIntent", "current_intent", "currentIntent", "intent"); runtimeIntent != "" { member.RuntimeIntent = &runtimeIntent } - if summary := eventString(payload, "last_summary", "lastSummary", "summary", "diagnostic"); summary != "" { + if summary := eventString(payload, "last_summary", "lastSummary", "summary", "diagnostic"); summary != "" && eventType != "assignment_heartbeat" { member.LastSummary = &summary } if reason := eventString(payload, "blocked_reason", "blockedReason", "error_message", "error", "reason"); reason != "" { @@ -4956,6 +8919,33 @@ func eventInt(payload map[string]interface{}, keys ...string) int { return 0 } +func teamEventBoolValue(payload map[string]interface{}, keys ...string) (bool, bool) { + if payload == nil { + return false, false + } + for _, key := range keys { + value, ok := payload[key] + if !ok || value == nil { + continue + } + switch typed := value.(type) { + case bool: + return typed, true + case float64: + return typed != 0, true + case int: + return typed != 0, true + case string: + normalized := strings.ToLower(strings.TrimSpace(typed)) + if normalized == "" { + continue + } + return normalized == "true" || normalized == "1" || normalized == "yes" || normalized == "on", true + } + } + return false, false +} + func eventTime(payload map[string]interface{}) *time.Time { for _, key := range []string{"occurred_at", "occurredAt", "timestamp"} { raw := eventString(payload, key) @@ -5774,6 +9764,31 @@ func teamEventsKey(teamID int) string { return fmt.Sprintf("claw:team:%d:events", teamID) } +func teamCompletionAckKey(teamID int, completionID, attemptID string) string { + return fmt.Sprintf("claw:team:%d:completion-ack:%s:%s", teamID, normalizeTeamRedisKeyPart(completionID), normalizeTeamRedisKeyPart(attemptID)) +} + +func teamCompletionStateKey(teamID int, completionID string) string { + return fmt.Sprintf("claw:team:%d:completion-state:%s", teamID, normalizeTeamRedisKeyPart(completionID)) +} + +func teamCompletionAckStreamKey(teamID int, memberKey string) string { + return fmt.Sprintf("claw:team:%d:completion-acks:%s", teamID, normalizeTeamRedisKeyPart(memberKey)) +} + +func normalizeTeamRedisKeyPart(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "unknown" + } + return strings.Map(func(r rune) rune { + if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '-' || r == '_' || r == ':' || r == '.' { + return r + } + return '-' + }, value) +} + func teamPresenceKey(teamID int) string { return fmt.Sprintf("claw:team:%d:presence", teamID) } diff --git a/backend/internal/services/team_service_test.go b/backend/internal/services/team_service_test.go index ceb64ec..d8297ff 100644 --- a/backend/internal/services/team_service_test.go +++ b/backend/internal/services/team_service_test.go @@ -2,9 +2,11 @@ package services import ( "encoding/json" + "fmt" "os" "path/filepath" "strings" + "sync" "testing" "time" @@ -357,7 +359,7 @@ func TestProjectTeamTaskRuntimeStateDoesNotTreatPlainReplyAsCompletion(t *testin task := &models.TeamTask{Status: models.TeamTaskStatusRunning} projection := projectTeamTaskRuntimeState(task, map[string]interface{}{ - "message": "worker 正在整理结果", + "message": "worker is preparing the result and will report back soon", }, "reply", nil, now) if projection.changed || task.Status != models.TeamTaskStatusRunning || task.FinishedAt != nil { @@ -635,12 +637,15 @@ func TestAppendTeamTaskCompletionInstructionUsesLeaderOnlyBootstrapContract(t *t "assigned only to the Leader", "Do not delegate it", "Complete this bootstrap in the current turn", + "metadata.teamConfigJson", + "/team/team.json from the shared workspace", + "optional system fallbacks", } { if !strings.Contains(bootstrap, expected) { t.Fatalf("bootstrap contract missing %q: %s", expected, bootstrap) } } - for _, forbidden := range []string{"For multi-member Teams", "workers report deliverables back to the Leader"} { + for _, forbidden := range []string{"For multi-member Teams", "workers report deliverables back to the Leader", "Never look for /team/team.json"} { if strings.Contains(bootstrap, forbidden) { t.Fatalf("bootstrap contract must not include normal collaboration rule %q: %s", forbidden, bootstrap) } @@ -765,14 +770,14 @@ func TestBuildInitialLeaderTaskPayloadDescribesRosterAndTeamSend(t *testing.T) { t.Fatalf("expected prompt string: %#v", payload) } for _, expected := range []string{ - "`team Software Engineering Team`", - "Redis Team成员构成", - "运行状态与技术能力边界", - "协作与通信机制(team_send)", - "任务流转方式", - "消息同步方式", - "上下文共享方式", - "可调用的方法、工具与操作能力", + "team Software Engineering Team", + "Redis Team\u6210\u5458\u6784\u6210", + "\u8fd0\u884c\u72b6\u6001\u4e0e\u6280\u672f\u80fd\u529b\u8fb9\u754c", + "\u534f\u4f5c\u4e0e\u901a\u4fe1\u673a\u5236(team_send)", + "\u4efb\u52a1\u6d41\u8f6c\u65b9\u5f0f", + "\u6d88\u606f\u540c\u6b65\u65b9\u5f0f", + "\u4e0a\u4e0b\u6587\u5171\u4eab\u65b9\u5f0f", + "\u53ef\u8c03\u7528\u7684\u65b9\u6cd5\u3001\u5de5\u5177\u4e0e\u64cd\u4f5c\u80fd\u529b", } { if !strings.Contains(prompt, expected) { t.Fatalf("bootstrap prompt missing %q: %s", expected, prompt) @@ -783,9 +788,10 @@ func TestBuildInitialLeaderTaskPayloadDescribesRosterAndTeamSend(t *testing.T) { func TestBuildTeamTaskEnvelopeIncludesCompletionContract(t *testing.T) { task := &models.TeamTask{ID: 67} payload := map[string]interface{}{ - "intent": "team_bootstrap_introduction", - "title": "Introduce the team", - "prompt": "Generate the team report.", + "intent": "team_bootstrap_introduction", + "title": "Introduce the team", + "prompt": "Generate the team report.", + "teamConfigJson": `{"sharedDir":"/team"}`, } envelope := buildTeamTaskEnvelope(31, "leader", task, "team-31-bootstrap-introduction", payload, nil, time.Unix(123, 0).UTC()) @@ -799,6 +805,13 @@ func TestBuildTeamTaskEnvelopeIncludesCompletionContract(t *testing.T) { if envelope["completionTool"] != "team_complete_task" { t.Fatalf("expected completion tool team_complete_task, got %#v", envelope["completionTool"]) } + monitorPolicy, ok := envelope["monitorPolicy"].(map[string]interface{}) + if !ok || monitorPolicy["enabled"] != true || monitorPolicy["visibleToChat"] != true { + t.Fatalf("expected visible monitor policy in envelope, got %#v", envelope["monitorPolicy"]) + } + if monitorPolicy["heartbeatEverySec"] != 30 || monitorPolicy["visibleHeartbeatEverySec"] != 180 { + t.Fatalf("expected 30s internal heartbeat and 180s chat digest policy, got %#v", monitorPolicy) + } resultSink, ok := envelope["resultSink"].(map[string]interface{}) if !ok { t.Fatalf("expected resultSink map, got %#v", envelope["resultSink"]) @@ -806,9 +819,12 @@ func TestBuildTeamTaskEnvelopeIncludesCompletionContract(t *testing.T) { if resultSink["type"] != "redis_stream" || resultSink["eventsKey"] != "claw:team:31:events" { t.Fatalf("unexpected resultSink: %#v", resultSink) } - if resultSink["successEvent"] != "task_completed" || resultSink["failureEvent"] != "task_failed" { + if resultSink["successEvent"] != "completion_proposed" || resultSink["failureEvent"] != "task_failed" { t.Fatalf("unexpected resultSink events: %#v", resultSink) } + if envelope["teamConfigJson"] != `{"sharedDir":"/team"}` { + t.Fatalf("expected teamConfigJson in envelope, got %#v", envelope["teamConfigJson"]) + } prompt, ok := envelope["prompt"].(string) if !ok { t.Fatalf("expected prompt string, got %#v", envelope["prompt"]) @@ -820,6 +836,96 @@ func TestBuildTeamTaskEnvelopeIncludesCompletionContract(t *testing.T) { } } +func TestBuildTeamTaskEnvelopeCarriesLocaleAndMemberWorkspace(t *testing.T) { + task := &models.TeamTask{ID: 88} + payload := map[string]interface{}{ + "prompt": "请实现一个计算器应用", + "responseLocale": "zh-CN", + "workspaceContract": map[string]interface{}{ + "physicalSharedDir": "/workspaces/teams/user-1/team-54-shared", + "taskRef": "team-54-task-88", + }, + } + envelope := buildTeamTaskEnvelope(54, "ui-designer", task, "root-message", payload, map[string]string{}, time.Unix(123, 0).UTC()) + if envelope["responseLocale"] != "zh-CN" { + t.Fatalf("response locale was not propagated: %#v", envelope) + } + shared, ok := envelope["sharedWorkspace"].(map[string]interface{}) + if !ok || shared["physicalPath"] != "/workspaces/teams/user-1/team-54-shared" || shared["memberArtifactPhysicalRoot"] != "/workspaces/teams/user-1/team-54-shared/artifacts/team-54-task-88/members/ui-designer" { + t.Fatalf("member shared workspace was not resolved: %#v", envelope["sharedWorkspace"]) + } + prompt, _ := envelope["prompt"].(string) + if !strings.Contains(prompt, "use zh-CN") || !strings.Contains(prompt, "team_artifact_write") { + t.Fatalf("runtime prompt is missing locale/artifact guidance: %s", prompt) + } +} + +func TestCompleteInitialLeaderTaskFromSnapshotWritesReportAndCompletion(t *testing.T) { + workspaceRoot := t.TempDir() + team := &models.Team{ + ID: 49, + UserID: 1, + Name: "delivery-team", + CommunicationMode: teamCommunicationModeLeaderMediated, + SharedMountPath: "/team", + } + task := &models.TeamTask{ + ID: 91, + TeamID: team.ID, + TargetMemberID: 700, + MessageID: "team-49-bootstrap-introduction", + Status: models.TeamTaskStatusPending, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 700, TeamID: team.ID, MemberKey: "delivery-lead", DisplayName: "Delivery Lead", Role: "leader", Status: models.TeamMemberStatusBusy, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: 701, TeamID: team.ID, MemberKey: "developer", DisplayName: "Developer", Role: "developer", RuntimeType: "openclaw", InstanceMode: "lite", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + repo := &teamRepositoryStub{ + membersByKey: map[string]*models.TeamMember{ + "delivery-lead": leader, + "developer": developer, + }, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: workspaceRoot} + payload := map[string]interface{}{ + "intent": initialLeaderTaskIntent, + "workspaceContract": map[string]interface{}{ + "sharedDir": "/team", + "physicalSharedDir": filepath.Join(workspaceRoot, "teams", "user-1", "team-49-shared"), + }, + } + + result, err := service.completeInitialLeaderTaskFromSnapshot(1, team, task, leader, payload) + if err != nil { + t.Fatalf("completeInitialLeaderTaskFromSnapshot returned error: %v", err) + } + if result.Status != models.TeamTaskStatusSucceeded || result.FinishedAt == nil { + t.Fatalf("expected backend bootstrap task to finish, got %#v", result.TeamTask) + } + reportPath := filepath.Join(workspaceRoot, "teams", "user-1", "team-49-shared", "results", "team-49-task-91", "team-introduction.md") + reportBytes, err := os.ReadFile(reportPath) + if err != nil { + t.Fatalf("expected bootstrap report to be written: %v", err) + } + report := string(reportBytes) + if !strings.Contains(report, "Developer") || !strings.Contains(report, "Redis Streams") { + t.Fatalf("bootstrap report missing member or mechanism detail: %s", report) + } + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusSucceeded || repo.updatedTask.ResultJSON == nil { + t.Fatalf("expected task result to be persisted, got %#v", repo.updatedTask) + } + if repo.updatedMember == nil || repo.updatedMember.MemberKey != "delivery-lead" || repo.updatedMember.Status != models.TeamMemberStatusIdle { + t.Fatalf("expected leader member to become idle, got %#v", repo.updatedMember) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "task_completed" { + t.Fatalf("expected one backend task_completed event, got %#v", repo.createdEvents) + } + stored := teamEventPayloadMap(repo.createdEvents[0]) + if eventString(stored, "completionSource") != "clawmanager_backend" || eventBool(stored, "backendGenerated") != true { + t.Fatalf("expected backend completion markers, got %#v", stored) + } +} + func TestProjectTeamEventDoesNotTreatPlainFinalReplyAsTaskCompleted(t *testing.T) { taskID := 67 messageID := "team-31-bootstrap-introduction" @@ -1105,15 +1211,15 @@ func TestProjectTeamEventLeaderDispatchCompletionDoesNotCloseRootTask(t *testing "status": "succeeded", "summary": "Dispatched to designer.", "resultMarkdown": strings.Join([]string{ - "【任务分派 · 自我介绍】", + "[ASSIGNMENT] Self introduction", "", - "designer 你好,我是 Leader。用户想让你做一段自我介绍。", + "designer, the user wants you to write a short self introduction.", "", - "请写一段自我介绍,内容包括你的角色身份、能力范围和工作风格。", - "完成后将内容写入共享目录,并回传结果给我。", + "Please include your role identity, capability boundary, and working style.", + "After finishing, write the content into the shared workspace and report back to me.", "", - "共享目录:$CLAWMANAGER_TEAM_SHARED_DIR/results/team-31-task-169/", - "规范路径:/team/results/team-31-task-169/intro-designer.md", + "Shared directory: $CLAWMANAGER_TEAM_SHARED_DIR/results/team-31-task-169/", + "Canonical path: /team/results/team-31-task-169/intro-designer.md", }, "\n"), } payloadJSON, err := json.Marshal(payload) @@ -1151,11 +1257,68 @@ func TestProjectTeamEventLeaderDispatchCompletionDoesNotCloseRootTask(t *testing if step["type"] != "assignment" || step["status"] != models.TeamTaskStatusDispatched { t.Fatalf("expected assignment collaboration step, got %#v", step) } - if got, _ := step["content"].(string); !strings.Contains(got, "designer 你好") { + if got, _ := step["content"].(string); !strings.Contains(got, "designer, the user wants you to write a short self introduction") { t.Fatalf("expected assignment content preserved, got %#v", got) } } +func TestProjectTeamEventLeaderPlanningDoesNotCreateLeaderAssignmentLane(t *testing.T) { + taskID := 169 + messageID := "team-31-task-169" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ + ID: 120, + TeamID: 31, + MemberKey: "leader", + Role: "leader", + Status: models.TeamMemberStatusBusy, + CurrentTaskID: &taskID, + Availability: models.TeamMemberAvailabilityBusy, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + } + service := &teamService{repo: repo} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "leader", + "messageId": messageID, + "rootTaskId": messageID, + "status": "dispatched", + "summary": "Planning: decomposing the task into assignments", + "leaderDispatchOnly": true, + "collaborationStep": map[string]interface{}{ + "type": "assignment", + "status": "dispatched", + "actor": "leader", + "target": "leader", + "rootTaskId": messageID, + "content": "Planning: decomposing the task into assignments", + }, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178657-1", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.workItems) != 0 { + t.Fatalf("leader planning/dispatch-to-self must not create Kanban work items, got %#v", repo.workItems) + } +} + func TestProjectTeamEventDoesNotTreatNonTargetReplyAsTaskCompleted(t *testing.T) { taskID := 70 messageID := "team-31-task-70" @@ -1204,7 +1367,7 @@ func TestProjectTeamEventDoesNotTreatNonTargetReplyAsTaskCompleted(t *testing.T) if repo.updatedTask != nil && (repo.updatedTask.Status == models.TeamTaskStatusSucceeded || repo.updatedTask.FinishedAt != nil) { t.Fatalf("non-target reply must not mark task succeeded, got %#v", repo.updatedTask) } - if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "reply" { + if len(repo.createdEvents) != 2 || repo.createdEvents[0].EventType != "reply" || repo.createdEvents[1].EventType != "member_result_confirmed" { t.Fatalf("expected stored event type reply, got %#v", repo.createdEvents) } } @@ -1533,7 +1696,7 @@ func TestProjectTeamEventLeaderMediatedWorkerCompletionDoesNotCloseRootTask(t *t if repo.updatedMember == nil || repo.updatedMember.MemberKey != "worker" || repo.updatedMember.Status != models.TeamMemberStatusIdle || repo.updatedMember.Progress != 100 { t.Fatalf("worker delivery should complete only the worker lane, got %#v", repo.updatedMember) } - if len(repo.createdEvents) != 1 || repo.createdEvents[0].TaskID == nil || *repo.createdEvents[0].TaskID != taskID { + if len(repo.createdEvents) != 2 || repo.createdEvents[0].TaskID == nil || *repo.createdEvents[0].TaskID != taskID || repo.createdEvents[1].EventType != "member_result_confirmed" { t.Fatalf("worker delivery must remain linked to the Leader root task, got %#v", repo.createdEvents) } } @@ -1595,10 +1758,83 @@ func TestProjectTeamEventLeaderMediatedWorkerReplyToLeaderIsAssignmentResult(t * if stored["assignmentResultOnly"] != true || stored["rootTaskTerminal"] != false { t.Fatalf("expected assignment-result marker, got %#v", stored) } + if stored["memberResultConfirmed"] != true || stored["normalizedResultSource"] != "legacy_normalized_reply" { + t.Fatalf("expected normalized member result marker, got %#v", stored) + } step := stored["collaborationStep"].(map[string]interface{}) if step["type"] != "result" || step["status"] != models.TeamTaskStatusSucceeded || step["actor"] != "designer" { t.Fatalf("expected worker result collaboration step, got %#v", step) } + if len(repo.createdEvents) != 2 || repo.createdEvents[1].EventType != "member_result_confirmed" { + t.Fatalf("expected leader ledger notification after worker result, got %#v", repo.createdEvents) + } + var notification map[string]interface{} + if err := json.Unmarshal([]byte(*repo.createdEvents[1].PayloadJSON), ¬ification); err != nil { + t.Fatalf("decode leader notification: %v", err) + } + if notification["workId"] != "member-designer" || notification["to"] != "leader" || notification["memberResultConfirmed"] != true { + t.Fatalf("expected structured leader notification, got %#v", notification) + } +} + +func TestProjectTeamEventLeaderMediatedIgnoresGenericWorkerCompletionSummary(t *testing.T) { + taskID := 276 + messageID := "team-31-task-276" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + worker := &models.TeamMember{ + ID: 121, + TeamID: 31, + MemberKey: "designer", + Role: "ui-ux-designer", + Status: models.TeamMemberStatusBusy, + CurrentTaskID: &taskID, + Availability: models.TeamMemberAvailabilityBusy, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"designer": worker}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "task_completed", + "memberId": "designer", + "from": "designer", + "to": "leader", + "taskId": messageID, + "messageId": "msg-worker-assignment", + "status": "succeeded", + "summary": "Redis Team task processing completed", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178678-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.createdEvents) != 1 { + t.Fatalf("generic runtime completion summary must not create ledger notification, got %#v", repo.createdEvents) + } + if repo.createdEvents[0].EventType != "reply" { + t.Fatalf("generic runtime completion should be downgraded to non-authoritative reply, got %#v", repo.createdEvents[0]) + } + var stored map[string]interface{} + if err := json.Unmarshal([]byte(*repo.createdEvents[0].PayloadJSON), &stored); err != nil { + t.Fatalf("decode stored payload: %v", err) + } + if stored["assignmentResultOnly"] == true || stored["memberResultConfirmed"] == true { + t.Fatalf("generic runtime completion must not be a member result, got %#v", stored) + } } func TestProjectTeamEventLeaderMediatedRejectsWorkerSelfRoute(t *testing.T) { @@ -1722,8 +1958,8 @@ func TestProjectTeamEventLeaderMediatedPrematureLeaderCompletionWaitsForAssignme if err := json.Unmarshal([]byte(*repo.createdEvents[len(repo.createdEvents)-1].PayloadJSON), &stored); err != nil { t.Fatalf("decode stored payload: %v", err) } - if stored["leaderPrematureCompletion"] != true || stored["rootTaskTerminal"] != false { - t.Fatalf("expected premature completion marker, got %#v", stored) + if stored["completionDecision"] != teamCompletionDecisionDeferred || stored["completionDecisionReason"] != "pending_legacy_assignments" || stored["rootTaskTerminal"] != false { + t.Fatalf("expected structured deferred completion, got %#v", stored) } } @@ -1784,6 +2020,1028 @@ func TestProjectTeamEventLeaderMediatedLeaderCompletionAfterAssignmentResultsClo if repo.updatedTask.ResultJSON == nil || !strings.Contains(*repo.updatedTask.ResultJSON, "designer=1") { t.Fatalf("expected final synthesis result stored, got %#v", repo.updatedTask.ResultJSON) } + if len(repo.outboxRows) != 1 || !strings.Contains(repo.outboxRows[0].Destination, "completion-acks") || !strings.Contains(repo.outboxRows[0].PayloadJSON, `"decision":"accepted"`) { + t.Fatalf("accepted root completion must atomically persist its acknowledgement: %#v", repo.outboxRows) + } + acceptedPayload := teamEventPayloadMap(repo.createdEvents[len(repo.createdEvents)-1]) + if eventString(acceptedPayload, "chatKind") != "final_delivery" || eventString(acceptedPayload, "displayKey") != "root-final:179" || eventString(acceptedPayload, "resultMarkdown") == "" { + t.Fatalf("accepted completion must expose one full final delivery event: %#v", acceptedPayload) + } +} + +func TestProjectTeamEventLeaderMediatedInterimLeaderCompletionDoesNotCloseAfterMemberResults(t *testing.T) { + taskID := 183 + messageID := "team-31-task-183" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + pm := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "pm", Role: "product-manager", Status: models.TeamMemberStatusIdle, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityIdle} + designer := &models.TeamMember{ID: 122, TeamID: 31, MemberKey: "designer", Role: "designer", Status: models.TeamMemberStatusIdle, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityIdle} + architect := &models.TeamMember{ID: 123, TeamID: 31, MemberKey: "architect", Role: "architect", Status: models.TeamMemberStatusIdle, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityIdle} + now := time.Now().UTC() + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{ + "leader": leader, + "pm": pm, + "designer": designer, + "architect": architect, + }, + workItems: []models.TeamWorkItem{ + {TeamID: 31, RootTaskID: taskID, WorkID: "member-pm", OwnerMemberID: &pm.ID, Status: models.TeamTaskStatusSucceeded, UpdatedAt: now}, + {TeamID: 31, RootTaskID: taskID, WorkID: "member-designer", OwnerMemberID: &designer.ID, Status: models.TeamTaskStatusSucceeded, UpdatedAt: now}, + {TeamID: 31, RootTaskID: taskID, WorkID: "member-architect", OwnerMemberID: &architect.ID, Status: models.TeamTaskStatusSucceeded, UpdatedAt: now}, + }, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "task_completed", + "memberId": "leader", + "messageId": messageID, + "taskId": messageID, + "status": "succeeded", + "summary": "PM result archived. Still waiting on Designer and Architect.", + "resultMarkdown": "PM result archived. Still waiting on Designer and Architect.", + "explicitCompletion": true, + "rootTaskTerminal": true, + "completionId": "leader-fallback-msg-1", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178682-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if repo.updatedTask == nil || repo.updatedTask.Status == models.TeamTaskStatusSucceeded || repo.updatedTask.FinishedAt != nil { + t.Fatalf("interim Leader completion must keep root task open even after member results, got %#v", repo.updatedTask) + } + stored := map[string]interface{}{} + if err := json.Unmarshal([]byte(*repo.createdEvents[len(repo.createdEvents)-1].PayloadJSON), &stored); err != nil { + t.Fatalf("decode stored payload: %v", err) + } + if stored["completionDecision"] != teamCompletionDecisionNeedsConfirmation || stored["completionDecisionReason"] != "narrative_indicates_remaining_work" || stored["rootTaskTerminal"] != false { + t.Fatalf("expected completion confirmation request, got %#v", stored) + } +} + +func TestEvaluateProtocolV3CompletionAllowsUnusedPlannedPhaseAfterWorkflowSeal(t *testing.T) { + leaderID := 120 + workerID := 121 + task := &models.TeamTask{ + ID: 190, TeamID: 31, TargetMemberID: leaderID, Status: models.TeamTaskStatusRunning, + WorkflowState: teamWorkflowStateAwaitingLeaderDecision, PlanVersion: 2, LedgerVersion: 7, + } + assignmentID := "research-pm" + phaseID := "research" + repo := &teamRepositoryStub{ + workItems: []models.TeamWorkItem{{ + TeamID: 31, RootTaskID: task.ID, WorkID: assignmentID, AssignmentID: &assignmentID, + PhaseID: &phaseID, Revision: 1, RequiredForRoot: true, OwnerMemberID: &workerID, + Status: models.TeamTaskStatusSucceeded, + }}, + workflowPhases: []models.TeamWorkflowPhase{ + {TeamID: 31, RootTaskID: task.ID, PhaseID: "research", PlanVersion: 2, Status: teamPhaseStatusCompleted, RequiredForRoot: true}, + {TeamID: 31, RootTaskID: task.ID, PhaseID: "implementation", PlanVersion: 2, Status: teamPhaseStatusPlanned, RequiredForRoot: true}, + }, + } + service := &teamService{repo: repo} + payload := map[string]interface{}{ + "protocolVersion": 3, "completionId": "completion-190", "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, "rootTaskTerminal": true, "workflowFinal": true, "finalAnswerReady": true, + "planVersion": 2, "ledgerVersion": 7, "status": "succeeded", "summary": "第一阶段完成", + "resultMarkdown": "第一阶段结果已经汇总。", + } + evaluation, err := service.evaluateLeaderRootCompletion( + &models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, + &models.TeamMember{ID: leaderID, TeamID: 31, MemberKey: "leader", Role: "leader"}, payload, + ) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionAccepted { + t.Fatalf("an explicitly sealed workflow must ignore a planned phase with no dispatched work: %#v", evaluation) + } +} + +func TestReconcileTeamWorkflowLedgerRepairsCompletedAndUnusedPhases(t *testing.T) { + now := time.Now().UTC() + leaderID := 120 + workerID := 121 + assignmentID := "review-calculator" + phaseID := "phase-2" + task := &models.TeamTask{ + ID: 201, TeamID: 31, TargetMemberID: leaderID, Status: models.TeamTaskStatusRunning, + WorkflowState: teamWorkflowStateAwaitingPhaseResults, PlanVersion: 1, LedgerVersion: 6, + CurrentPhaseID: &phaseID, + } + repo := &teamRepositoryStub{ + workItems: []models.TeamWorkItem{{ + TeamID: 31, RootTaskID: task.ID, WorkID: assignmentID, AssignmentID: &assignmentID, + PhaseID: &phaseID, OwnerMemberID: &workerID, RequiredForRoot: true, + Revision: 1, Status: models.TeamTaskStatusSucceeded, + }}, + workflowPhases: []models.TeamWorkflowPhase{ + {TeamID: 31, RootTaskID: task.ID, PhaseID: phaseID, PlanVersion: 1, Status: teamPhaseStatusAwaitingResults, RequiredForRoot: true}, + {TeamID: 31, RootTaskID: task.ID, PhaseID: "phase-3", PlanVersion: 1, Status: teamPhaseStatusPlanned, RequiredForRoot: true}, + }, + } + service := &teamService{repo: repo} + changed, err := service.reconcileTeamWorkflowLedger(task, true, now) + if err != nil || !changed { + t.Fatalf("expected workflow ledger repair, changed=%v err=%v", changed, err) + } + if repo.workflowPhases[0].Status != teamPhaseStatusCompleted || repo.workflowPhases[1].Status != teamPhaseStatusCancelled { + t.Fatalf("expected completed current phase and cancelled unused planned phase, got %#v", repo.workflowPhases) + } + if task.WorkflowState != teamWorkflowStateSynthesizing || task.LedgerVersion != 7 || task.CurrentPhaseID != nil { + t.Fatalf("unexpected reconciled task state: %#v", task) + } +} + +func TestMarkStructuredCompletionDeferredRemainsVisibleInChat(t *testing.T) { + payload := map[string]interface{}{ + "completionId": "completion-201", "resultMarkdown": "# Final delivery\n\nFull report.", + } + markStructuredCompletionDecision("completion_proposed", payload, teamCompletionEvaluation{ + Decision: teamCompletionDecisionDeferred, Reason: "open_workflow_phases", LedgerVersion: 9, + }) + if !eventBool(payload, "visibleToChat") || eventString(payload, "chatPolicy") != "warning" || eventString(payload, "chatKind") != "completion_deferred" { + t.Fatalf("deferred completion must retain visible report and diagnostic: %#v", payload) + } +} + +func TestReconcileDeferredCompletionAcceptsAfterLedgerRepair(t *testing.T) { + now := time.Now().UTC() + taskID := 202 + leaderID := 120 + workerID := 121 + messageID := "team-31-task-202" + assignmentID := "review-calculator" + phaseID := "phase-2" + task := &models.TeamTask{ + ID: taskID, TeamID: 31, TargetMemberID: leaderID, MessageID: messageID, + Status: models.TeamTaskStatusRunning, WorkflowState: teamWorkflowStateAwaitingPhaseResults, + PlanVersion: 1, LedgerVersion: 6, UpdatedAt: now, + } + leader := &models.TeamMember{ID: leaderID, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy} + deferredPayload, err := json.Marshal(map[string]interface{}{ + "protocolVersion": 3, "event": "completion_deferred", "completionId": "completion-202", + "attemptId": "attempt-202", "completionSource": teamTaskCompletionTool, "explicitCompletion": true, + "rootTaskTerminal": false, "workflowFinal": true, "finalAnswerReady": true, + "remainingActions": []string{}, "planVersion": 1, "ledgerVersion": 6, + "summary": "Calculator delivered and verified.", + "resultMarkdown": "# Final delivery\n\nCalculator delivered and verified.", + }) + if err != nil { + t.Fatal(err) + } + eventID := "deferred-202" + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + workItems: []models.TeamWorkItem{{ + TeamID: 31, RootTaskID: taskID, WorkID: assignmentID, AssignmentID: &assignmentID, + PhaseID: &phaseID, OwnerMemberID: &workerID, RequiredForRoot: true, + Revision: 1, Status: models.TeamTaskStatusSucceeded, + }}, + workflowPhases: []models.TeamWorkflowPhase{ + {TeamID: 31, RootTaskID: taskID, PhaseID: phaseID, PlanVersion: 1, Status: teamPhaseStatusAwaitingResults, RequiredForRoot: true}, + {TeamID: 31, RootTaskID: taskID, PhaseID: "phase-3", PlanVersion: 1, Status: teamPhaseStatusPlanned, RequiredForRoot: true}, + }, + createdEvents: []models.TeamEvent{{ + TeamID: 31, TaskID: &taskID, MemberID: &leaderID, EventID: &eventID, + EventType: "completion_deferred", PayloadJSON: stringPtr(string(deferredPayload)), + }}, + } + service := &teamService{repo: repo} + reconciled, err := service.reconcileDeferredTeamCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, task, leader) + if err != nil || !reconciled || task.Status != models.TeamTaskStatusSucceeded { + t.Fatalf("expected deferred completion to self-heal without another Leader turn: reconciled=%v task=%#v err=%v", reconciled, task, err) + } + if repo.workflowPhases[0].Status != teamPhaseStatusCompleted || repo.workflowPhases[1].Status != teamPhaseStatusCancelled { + t.Fatalf("expected repaired phase ledger before acceptance: %#v", repo.workflowPhases) + } +} + +func TestProjectProtocolV3DeferredCompletionPersistsAcknowledgementOutbox(t *testing.T) { + now := time.Now().UTC() + taskID := 193 + messageID := "team-31-task-193" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + WorkflowState: teamWorkflowStateExecuting, + PlanVersion: 1, + LedgerVersion: 2, + UpdatedAt: now, + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID} + workerID := 121 + assignmentID := "implementation" + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + workItems: []models.TeamWorkItem{{ + TeamID: 31, + RootTaskID: taskID, + WorkID: assignmentID, + AssignmentID: &assignmentID, + OwnerMemberID: &workerID, + Status: models.TeamTaskStatusRunning, + RequiredForRoot: true, + Revision: 1, + UpdatedAt: now, + }}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "protocolVersion": 3, + "event": "completion_proposed", + "eventId": "evt-deferred-193", + "completionId": "completion:31:team-31-task-193:leader", + "attemptId": "attempt-deferred-193", + "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, + "rootTaskTerminal": true, + "memberId": "leader", + "messageId": messageID, + "taskId": messageID, + "rootTaskId": messageID, + "status": "succeeded", + "summary": "最终报告已准备,但实现任务仍在执行。", + "resultMarkdown": "# 最终报告\n\n等待结构化账本允许后提交。", + "workflowFinal": true, + "finalAnswerReady": true, + "remainingActions": []string{}, + "planVersion": 1, + "ledgerVersion": 2, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178993-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "completion_deferred" { + t.Fatalf("pending required assignment must defer root completion, got %#v", repo.createdEvents) + } + if len(repo.outboxRows) != 1 || !strings.Contains(repo.outboxRows[0].PayloadJSON, `"decision":"deferred"`) || !strings.Contains(repo.outboxRows[0].PayloadJSON, assignmentID) { + t.Fatalf("deferred completion acknowledgement must be durable and diagnostic: %#v", repo.outboxRows) + } +} + +func TestEvaluateProtocolV3DynamicPhaseRequiresLeaderDecisionOrWorkflowSeal(t *testing.T) { + leaderID := 120 + workerID := 121 + assignmentID := "collect-worker-input" + phaseID := "collection" + task := &models.TeamTask{ID: 191, TeamID: 31, TargetMemberID: leaderID, Status: models.TeamTaskStatusRunning, WorkflowState: teamWorkflowStateAwaitingLeaderDecision, PlanVersion: 1, LedgerVersion: 4} + repo := &teamRepositoryStub{ + workItems: []models.TeamWorkItem{{TeamID: 31, RootTaskID: task.ID, WorkID: assignmentID, AssignmentID: &assignmentID, PhaseID: &phaseID, Revision: 1, RequiredForRoot: true, OwnerMemberID: &workerID, Status: models.TeamTaskStatusSucceeded}}, + workflowPhases: []models.TeamWorkflowPhase{{TeamID: 31, RootTaskID: task.ID, PhaseID: phaseID, PlanVersion: 1, Status: teamPhaseStatusAwaitingLeaderDecision, RequiredForRoot: true, DecisionRequired: true}}, + } + service := &teamService{repo: repo} + payload := map[string]interface{}{ + "protocolVersion": 3, "completionId": "completion-191", "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, "rootTaskTerminal": true, "finalAnswerReady": true, + "planVersion": 1, "ledgerVersion": 4, "status": "succeeded", "summary": "信息已收集", "resultMarkdown": "信息已收集。", + } + leader := &models.TeamMember{ID: leaderID, TeamID: 31, MemberKey: "leader", Role: "leader"} + evaluation, err := service.evaluateLeaderRootCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader, payload) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionDeferred || evaluation.Reason != "workflow_not_sealed" { + t.Fatalf("dynamic phase must wait for Leader decision: %#v", evaluation) + } + payload["workflowFinal"] = true + evaluation, err = service.evaluateLeaderRootCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader, payload) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionAccepted { + t.Fatalf("explicitly sealed dynamic workflow should be accepted: %#v", evaluation) + } +} + +func TestEvaluateProtocolV3CompletionDoesNotTreatFeatureWordAsInterim(t *testing.T) { + if isInterimOrDelegationReplyText("计算器最终交付报告:重复 = 操作,所有功能均已验证。") { + t.Fatal("legacy narrative helper must not treat an ordinary feature label as an interim reply") + } + task := &models.TeamTask{ID: 192, TeamID: 31, TargetMemberID: 120, Status: models.TeamTaskStatusRunning, WorkflowState: teamWorkflowStateSynthesizing, PlanVersion: 1, LedgerVersion: 1} + service := &teamService{repo: &teamRepositoryStub{}} + payload := map[string]interface{}{ + "protocolVersion": 3, "completionId": "completion-192", "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, "rootTaskTerminal": true, "workflowFinal": true, "finalAnswerReady": true, + "planVersion": 1, "ledgerVersion": 1, "status": "succeeded", "summary": "计算器最终交付报告", + "resultMarkdown": "功能清单:重复 = 操作。所有功能已验证。", + } + evaluation, err := service.evaluateLeaderRootCompletion( + &models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, + &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader"}, payload, + ) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionAccepted || len(evaluation.StrongContradictions) != 0 { + t.Fatalf("ordinary feature wording must not veto structured completion: %#v", evaluation) + } +} + +func TestEvaluateProtocolV3RequiresStructuredRiskWaiverForFailedRequiredWork(t *testing.T) { + leaderID := 120 + workerID := 121 + assignmentID := "browser-validation" + optionalAssignmentID := "nice-to-have-benchmark" + phaseID := "validation" + task := &models.TeamTask{ID: 195, TeamID: 31, TargetMemberID: leaderID, Status: models.TeamTaskStatusRunning, WorkflowState: teamWorkflowStateSynthesizing, PlanVersion: 1, LedgerVersion: 5} + repo := &teamRepositoryStub{ + workItems: []models.TeamWorkItem{ + {ID: 1, TeamID: 31, RootTaskID: task.ID, WorkID: assignmentID, AssignmentID: &assignmentID, PhaseID: &phaseID, Revision: 1, RequiredForRoot: true, OwnerMemberID: &workerID, Status: models.TeamTaskStatusFailed}, + {ID: 2, TeamID: 31, RootTaskID: task.ID, WorkID: optionalAssignmentID, AssignmentID: &optionalAssignmentID, PhaseID: &phaseID, Revision: 1, RequiredForRoot: false, OwnerMemberID: &workerID, Status: models.TeamTaskStatusRunning}, + }, + workflowPhases: []models.TeamWorkflowPhase{{TeamID: 31, RootTaskID: task.ID, PhaseID: phaseID, PlanVersion: 1, Status: teamPhaseStatusAwaitingResults, RequiredForRoot: true}}, + } + service := &teamService{repo: repo} + payload := map[string]interface{}{ + "protocolVersion": 3, "completionId": "completion-195", "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, "rootTaskTerminal": true, "workflowFinal": true, "finalAnswerReady": true, + "planVersion": 1, "ledgerVersion": 5, "status": "succeeded", "summary": "最终交付报告", + "resultMarkdown": "浏览器验证失败,交付报告明确记录该风险。", + } + leader := &models.TeamMember{ID: leaderID, TeamID: 31, MemberKey: "leader", Role: "leader"} + evaluation, err := service.evaluateLeaderRootCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader, payload) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionDeferred || !containsTeamString(evaluation.PendingAssignments, assignmentID) { + t.Fatalf("failed required work without a waiver must block completion: %#v", evaluation) + } + payload["waivers"] = []interface{}{map[string]interface{}{"assignmentId": assignmentID, "reason": "测试环境不可用"}} + evaluation, err = service.evaluateLeaderRootCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader, payload) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionDeferred { + t.Fatalf("waiver without an accepted risk must not close the root task: %#v", evaluation) + } + payload["waivers"] = []interface{}{map[string]interface{}{ + "assignmentId": assignmentID, + "reason": "测试环境不可用", + "risk": "浏览器交互仍未验证,发布前必须人工复核", + }} + evaluation, err = service.evaluateLeaderRootCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader, payload) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionDeferred || !containsTeamString(evaluation.PendingAssignments, optionalAssignmentID+":skip_reason") { + t.Fatalf("omitted optional work must have a structured skip reason: %#v", evaluation) + } + payload["skippedAssignments"] = []interface{}{map[string]interface{}{"assignmentId": optionalAssignmentID, "reason": "不影响核心交付,留待后续性能专项"}} + evaluation, err = service.evaluateLeaderRootCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader, payload) + if err != nil { + t.Fatal(err) + } + if evaluation.Decision != teamCompletionDecisionAccepted || !containsTeamString(evaluation.WaivedAssignments, assignmentID) || !containsTeamString(evaluation.SkippedAssignments, optionalAssignmentID) { + t.Fatalf("complete structured waiver and optional skip record should permit a recorded partial-risk delivery: %#v", evaluation) + } +} + +func TestProjectTeamWorkItemKeepsSequentialAssignmentsForSameMember(t *testing.T) { + team := &models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated} + task := &models.TeamTask{ID: 193, TeamID: 31, TargetMemberID: 120, Status: models.TeamTaskStatusRunning} + developer := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "developer", Role: "developer"} + repo := &teamRepositoryStub{membersByKey: map[string]*models.TeamMember{"developer": developer}} + service := &teamService{repo: repo} + for index, assignmentID := range []string{"developer-research", "developer-implementation"} { + phaseID := []string{"research", "implementation"}[index] + payload := map[string]interface{}{ + "assignmentId": assignmentID, "workId": assignmentID, "phaseId": phaseID, + "required": true, "revision": 1, + "collaborationStep": map[string]interface{}{ + "type": "assignment", "status": "dispatched", "actor": "leader", "target": "developer", + "workId": assignmentID, "phase": phaseID, "title": assignmentID, + }, + } + if err := service.projectTeamWorkItem(team, task, developer, "team_send", payload, &models.TeamEvent{CreatedAt: time.Now().UTC()}); err != nil { + t.Fatal(err) + } + } + if len(repo.workItems) != 2 || repo.workItems[0].WorkID == repo.workItems[1].WorkID { + t.Fatalf("sequential assignments for one member must remain distinct: %#v", repo.workItems) + } +} + +func TestArtifactChangeInvalidatesMatchingReviewedWorkItem(t *testing.T) { + taskID := 196 + leaderID := 120 + developerID := 121 + validatedRevision := 1 + assignmentID := "developer-implementation" + artifactRefs := `["/team/artifacts/team-31-task-196/members/developer/app.js"]` + task := &models.TeamTask{ID: taskID, TeamID: 31, TargetMemberID: leaderID, MessageID: "team-31-task-196", Status: models.TeamTaskStatusRunning, LedgerVersion: 3, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: leaderID, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{task.MessageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + workItems: []models.TeamWorkItem{{ + ID: 1, TeamID: 31, RootTaskID: taskID, WorkID: assignmentID, AssignmentID: &assignmentID, + OwnerMemberID: &developerID, Revision: 1, RequiredForRoot: true, ReviewRequired: true, + ValidatedRevision: &validatedRevision, Status: models.TeamTaskStatusSucceeded, ArtifactRefsJSON: &artifactRefs, + }}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "artifact_changed", "eventKind": "artifact_changed", "eventId": "artifact-change-196", + "memberId": "leader", "messageId": task.MessageID, "taskId": task.MessageID, "rootTaskId": task.MessageID, + "assignmentId": "leader-final-synthesis", "artifactChanged": true, + "artifactRefs": []string{"/team/artifacts/team-31-task-196/members/developer/app.js"}, + "status": "running", "summary": "Leader updated the reviewed artifact.", + }) + if err != nil { + t.Fatal(err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178996-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatal(err) + } + for idx := range repo.workItems { + if repo.workItems[idx].ID == 1 && repo.workItems[idx].ValidatedRevision != nil { + t.Fatalf("modifying a reviewed artifact must invalidate the old review: %#v", repo.workItems[idx]) + } + } + if repo.updatedTask == nil || repo.updatedTask.LedgerVersion != 4 { + t.Fatalf("review invalidation must advance the root ledger: %#v", repo.updatedTask) + } +} + +func TestTeamChatPolicyPreservesMeaningfulCheckProgress(t *testing.T) { + payload := map[string]interface{}{ + "eventKind": "assignment_check_result", "checkId": "monitor:team-31-task-1:work-1:2", + "assignmentId": "work-1", "rootTaskId": "team-31-task-1", "progress": 60, + "summary": "已完成核心模块,正在进行集成验证。", + } + applyTeamChatPolicy("task_progress", payload, nil, &models.TeamMember{MemberKey: "developer"}) + if payload["chatPolicy"] != "visible" || payload["visibleToChat"] != true || payload["chatBusinessKind"] != "worker_progress" { + t.Fatalf("meaningful check progress must remain visible: %#v", payload) + } + quiet := map[string]interface{}{ + "eventKind": "assignment_check_result", "checkId": "monitor:team-31-task-1:work-1:3", + "assignmentId": "work-1", "rootTaskId": "team-31-task-1", "summary": "still running", + } + applyTeamChatPolicy("task_progress", quiet, nil, &models.TeamMember{MemberKey: "developer"}) + if quiet["chatPolicy"] != "digest" || quiet["visibleToChat"] != false { + t.Fatalf("unchanged transport check should be digested: %#v", quiet) + } +} + +func TestTeamChatPolicyBusinessNarrativeOverridesLegacyHiddenPolicy(t *testing.T) { + payload := map[string]interface{}{ + "eventKind": "worker_plan", "chatPolicy": "hidden", "visibleToChat": false, + "memberId": "developer", "phaseId": "implementation", + "text": "I will implement the calculator UI and report the deliverable to the Leader.", + "displayKey": "worker-plan:team-31-task-1:", + } + applyTeamChatPolicy("task_progress", payload, nil, &models.TeamMember{MemberKey: "developer"}) + if payload["chatPolicy"] != "visible" || payload["visibleToChat"] != true || eventString(payload, "displayKey") != "" { + t.Fatalf("business narrative must override stale hidden policy and empty worker display key: %#v", payload) + } +} + +func TestTeamChatPolicyKeepsTransportAcknowledgementHidden(t *testing.T) { + payload := map[string]interface{}{ + "eventKind": "assignment_heartbeat", "summary": "still running", "visibleToChat": true, + } + applyTeamChatPolicy("assignment_heartbeat", payload, nil, &models.TeamMember{MemberKey: "developer"}) + if payload["chatPolicy"] != "digest" || payload["visibleToChat"] != false { + t.Fatalf("heartbeat without business content must remain digest-only: %#v", payload) + } +} + +func TestTeamEventPayloadsFilterOnlyHiddenTransportFacts(t *testing.T) { + hiddenJSON := `{"event":"member_result_confirmed","chatPolicy":"hidden","visibleToChat":false}` + progressJSON := `{"event":"task_progress","eventKind":"worker_progress","chatPolicy":"replaceable","visibleToChat":true,"summary":"正在实现核心模块"}` + digestJSON := `{"event":"assignment_heartbeat","chatPolicy":"digest","visibleToChat":false,"summary":"仍在执行"}` + events := []models.TeamEvent{ + {ID: 1, EventType: "member_result_confirmed", PayloadJSON: &hiddenJSON}, + {ID: 2, EventType: "task_progress", PayloadJSON: &progressJSON}, + {ID: 3, EventType: "assignment_heartbeat", PayloadJSON: &digestJSON}, + } + payloads := teamEventPayloads(events) + if len(payloads) != 2 || payloads[0].ID != 2 || payloads[1].ID != 3 { + t.Fatalf("chat projection must preserve business progress and digests while removing hidden confirmation facts: %#v", payloads) + } +} + +func containsTeamString(values []string, expected string) bool { + for _, value := range values { + if value == expected { + return true + } + } + return false +} + +func TestTeamEventPayloadsRestoreLegacyHiddenBusinessNarrative(t *testing.T) { + workerPlanJSON := `{"event":"task_progress","eventKind":"worker_plan","chatPolicy":"hidden","visibleToChat":false,"text":"Worker plan: implement the core module and report to Leader."}` + transportJSON := `{"event":"task_received","chatPolicy":"hidden","visibleToChat":false,"summary":"task_received"}` + events := []models.TeamEvent{ + {ID: 31, EventType: "task_progress", PayloadJSON: &workerPlanJSON}, + {ID: 32, EventType: "task_received", PayloadJSON: &transportJSON}, + } + payloads := teamEventPayloads(events) + if len(payloads) != 1 || payloads[0].ID != 31 { + t.Fatalf("legacy hidden business narrative must be returned while transport acknowledgement stays hidden: %#v", payloads) + } +} + +func TestProjectTeamEventAssignmentResultUsesActualMemberLane(t *testing.T) { + taskID := 180 + messageID := "team-31-task-180" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "developer", Role: "senior-developer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + reviewer := &models.TeamMember{ID: 122, TeamID: 31, MemberKey: "reviewer", Role: "qa-engineer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "developer": developer, "reviewer": reviewer}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "outbound", + "assignmentResultOnly": true, + "assignmentId": "assignment-developer-001", + "memberId": "reviewer", + "from": "reviewer", + "to": "leader", + "rootTaskId": messageID, + "status": "succeeded", + "text": "Reviewer delivered a valid result.", + "collaborationStep": map[string]interface{}{ + "type": "result", + "status": "succeeded", + "actor": "reviewer", + "target": "leader", + "rootTaskId": messageID, + "content": "Reviewer delivered a valid result.", + }, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178680-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.workItems) != 1 { + t.Fatalf("expected one work item, got %#v", repo.workItems) + } + if repo.workItems[0].WorkID != "assignment-developer-001" { + t.Fatalf("assignment result must preserve the business assignment ID, got workID %q", repo.workItems[0].WorkID) + } + if repo.workItems[0].OwnerMemberID == nil || *repo.workItems[0].OwnerMemberID != reviewer.ID { + t.Fatalf("expected reviewer owner, got %#v", repo.workItems[0].OwnerMemberID) + } +} + +func TestProjectTeamEventLeaderDispatchAndWorkerResultShareMemberLane(t *testing.T) { + taskID := 181 + messageID := "team-31-task-181" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "developer", Role: "senior-developer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "developer": developer}, + } + service := &teamService{repo: repo} + assignmentJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "leaderDispatchOnly": true, + "assignmentId": "assignment-developer-001", + "memberId": "leader", + "rootTaskId": messageID, + "status": "dispatched", + "collaborationStep": map[string]interface{}{ + "type": "assignment", + "status": "dispatched", + "actor": "leader", + "target": "developer", + "rootTaskId": messageID, + "content": "Please answer with one flower.", + }, + }) + if err != nil { + t.Fatalf("marshal assignment: %v", err) + } + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178681-0", + Fields: map[string]string{"payload": string(assignmentJSON)}, + }); err != nil { + t.Fatalf("project assignment: %v", err) + } + resultJSON, err := json.Marshal(map[string]interface{}{ + "event": "outbound", + "assignmentResultOnly": true, + "assignmentId": "runtime-random-assignment-id", + "memberId": "developer", + "from": "developer", + "to": "leader", + "rootTaskId": messageID, + "status": "succeeded", + "text": "洋牡丹:受欢迎、魅力四射。", + "collaborationStep": map[string]interface{}{ + "type": "result", + "status": "succeeded", + "actor": "developer", + "target": "leader", + "rootTaskId": messageID, + "content": "洋牡丹:受欢迎、魅力四射。", + }, + }) + if err != nil { + t.Fatalf("marshal result: %v", err) + } + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178681-1", + Fields: map[string]string{"payload": string(resultJSON)}, + }); err != nil { + t.Fatalf("project result: %v", err) + } + if len(repo.workItems) != 1 { + t.Fatalf("dispatch and result should share one member lane, got %#v", repo.workItems) + } + item := repo.workItems[0] + if item.WorkID != "assignment-developer-001" || item.Status != models.TeamTaskStatusSucceeded { + t.Fatalf("expected succeeded canonical developer assignment, got %#v", item) + } + if item.ResultJSON == nil || !strings.Contains(*item.ResultJSON, "洋牡丹") { + t.Fatalf("expected full worker result in lane payload, got %#v", item.ResultJSON) + } + if len(repo.createdEvents) < 3 || repo.createdEvents[len(repo.createdEvents)-1].EventType != "member_result_confirmed" { + t.Fatalf("expected a structured leader notification for worker result, got %#v", repo.createdEvents) + } +} + +func TestProjectTeamEventLeaderMediatedWorkerProgressAndResultShareMemberLane(t *testing.T) { + taskID := 184 + messageID := "team-31-task-184" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + pm := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "pm", Role: "product-manager", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "pm": pm}, + } + service := &teamService{repo: repo} + progressJSON, err := json.Marshal(map[string]interface{}{ + "event": "progress", + "memberId": "pm", + "from": "pm", + "to": "leader", + "rootTaskId": messageID, + "workId": "w1-pm-flower", + "status": "running", + "progress": 50, + "text": "Working on PM flower choice.", + "collaborationStep": map[string]interface{}{ + "type": "progress", + "status": "running", + "actor": "pm", + "target": "leader", + "rootTaskId": messageID, + "content": "Working on PM flower choice.", + }, + }) + if err != nil { + t.Fatalf("marshal progress: %v", err) + } + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178683-0", + Fields: map[string]string{"payload": string(progressJSON)}, + }); err != nil { + t.Fatalf("project progress: %v", err) + } + if len(repo.workItems) != 1 || repo.workItems[0].WorkID != "w1-pm-flower" || repo.workItems[0].Status != models.TeamTaskStatusRunning { + t.Fatalf("expected running PM assignment after progress, got %#v", repo.workItems) + } + resultJSON, err := json.Marshal(map[string]interface{}{ + "event": "outbound", + "assignmentResultOnly": true, + "assignmentId": "runtime-random-assignment-id", + "memberId": "pm", + "from": "pm", + "to": "leader", + "rootTaskId": messageID, + "status": "succeeded", + "text": "PM flower: Sunflower.", + "collaborationStep": map[string]interface{}{ + "type": "result", + "status": "succeeded", + "actor": "pm", + "target": "leader", + "rootTaskId": messageID, + "content": "PM flower: Sunflower.", + }, + }) + if err != nil { + t.Fatalf("marshal result: %v", err) + } + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178683-1", + Fields: map[string]string{"payload": string(resultJSON)}, + }); err != nil { + t.Fatalf("project result: %v", err) + } + if len(repo.workItems) != 1 { + t.Fatalf("progress and result should share one member lane, got %#v", repo.workItems) + } + item := repo.workItems[0] + if item.WorkID != "w1-pm-flower" || item.Status != models.TeamTaskStatusSucceeded { + t.Fatalf("expected succeeded PM assignment, got %#v", item) + } + if item.ResultJSON == nil || !strings.Contains(*item.ResultJSON, "Sunflower") { + t.Fatalf("expected worker result stored in pm lane, got %#v", item.ResultJSON) + } +} + +func TestProjectTeamEventLeaderMediatedDuplicateWorkerResultDoesNotNotifyTwice(t *testing.T) { + taskID := 185 + messageID := "team-31-task-185" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + pm := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "pm", Role: "product-manager", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + confirmedPayload := `{"event":"member_result_confirmed","from":"pm","memberId":"pm","rootTaskId":"team-31-task-185","text":"PM flower: Sunflower."}` + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "pm": pm}, + createdEvents: []models.TeamEvent{{ + TeamID: 31, + TaskID: &taskID, + EventType: "member_result_confirmed", + PayloadJSON: &confirmedPayload, + }}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "outbound", + "assignmentResultOnly": true, + "memberId": "pm", + "from": "pm", + "to": "leader", + "rootTaskId": messageID, + "status": "succeeded", + "text": "PM flower: Sunflower.", + "collaborationStep": map[string]interface{}{ + "type": "result", + "status": "succeeded", + "actor": "pm", + "target": "leader", + "rootTaskId": messageID, + "content": "PM flower: Sunflower.", + }, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178684-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("project duplicate result: %v", err) + } + confirmations := 0 + for _, event := range repo.createdEvents { + if event.EventType == "member_result_confirmed" { + confirmations++ + } + } + if confirmations != 1 { + t.Fatalf("duplicate worker result must not create another leader notification, got %d events: %#v", confirmations, repo.createdEvents) + } +} + +func TestProjectTeamEventLeaderMediatedChangedWorkerResultNotifiesAgain(t *testing.T) { + taskID := 186 + messageID := "team-31-task-186" + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + reviewer := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "reviewer", Role: "qa-engineer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + confirmedPayload := `{"event":"member_result_confirmed","from":"reviewer","memberId":"reviewer","rootTaskId":"team-31-task-186","assignmentId":"review-current","workId":"review-current","text":"Reviewer verdict: FAIL"}` + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "reviewer": reviewer}, + createdEvents: []models.TeamEvent{{ + TeamID: 31, + TaskID: &taskID, + EventType: "member_result_confirmed", + PayloadJSON: &confirmedPayload, + }}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "outbound", + "assignmentResultOnly": true, + "assignmentId": "review-current", + "memberId": "reviewer", + "from": "reviewer", + "to": "leader", + "rootTaskId": messageID, + "status": "succeeded", + "text": "Reviewer verdict: PASS", + "collaborationStep": map[string]interface{}{ + "type": "result", + "status": "succeeded", + "actor": "reviewer", + "target": "leader", + "rootTaskId": messageID, + "content": "Reviewer verdict: PASS", + }, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178685-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("project changed result: %v", err) + } + confirmations := 0 + for _, event := range repo.createdEvents { + if event.EventType == "member_result_confirmed" { + confirmations++ + } + } + if confirmations != 2 { + t.Fatalf("changed worker result must create a fresh leader notification, got %d events: %#v", confirmations, repo.createdEvents) + } + if len(repo.outboxRows) != 1 { + t.Fatalf("changed worker result must be delivered to leader once, got outbox %#v", repo.outboxRows) + } + if len(repo.workItems) != 1 || repo.workItems[0].ResultJSON == nil || !strings.Contains(*repo.workItems[0].ResultJSON, "PASS") { + t.Fatalf("changed worker result must refresh current work item, got %#v", repo.workItems) + } +} + +func TestProjectTeamEventLeaderDispatchRefreshesTerminalCurrentLane(t *testing.T) { + taskID := 187 + messageID := "team-31-task-187" + now := time.Now().UTC() + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: now.Add(-time.Minute), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + reviewer := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "reviewer", Role: "qa-engineer", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + oldResult := `{"summary":"Reviewer verdict: FAIL"}` + oldFinishedAt := now.Add(-2 * time.Minute) + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "reviewer": reviewer}, + workItems: []models.TeamWorkItem{{ + TeamID: 31, + RootTaskID: taskID, + WorkID: "review-current", + AssignmentID: stringPtr("review-current"), + OwnerMemberID: &reviewer.ID, + Title: "reviewer delivers result", + Status: models.TeamTaskStatusFailed, + ResultJSON: &oldResult, + FinishedAt: &oldFinishedAt, + UpdatedAt: now.Add(-2 * time.Minute), + }}, + } + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "leaderDispatchOnly": true, + "assignmentId": "review-current", + "memberId": "leader", + "from": "leader", + "to": "reviewer", + "rootTaskId": messageID, + "status": "dispatched", + "text": "Please re-check after the calculator fix.", + "collaborationStep": map[string]interface{}{ + "type": "assignment", + "status": "dispatched", + "actor": "leader", + "target": "reviewer", + "rootTaskId": messageID, + "content": "Please re-check after the calculator fix.", + }, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + service := &teamService{repo: repo} + if err := service.projectTeamEvent(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178686-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("project redispatch: %v", err) + } + if len(repo.workItems) != 1 { + t.Fatalf("redispatch should refresh one current lane, got %#v", repo.workItems) + } + item := repo.workItems[0] + if item.Status != models.TeamTaskStatusDispatched || item.ResultJSON != nil || item.FinishedAt != nil { + t.Fatalf("redispatch must reopen current lane and clear old terminal result, got %#v", item) + } +} + +func TestLeaderMediatedRootCompletionReadyCoalescesSameOwnerWorkItems(t *testing.T) { + taskID := 182 + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: "team-31-task-182", + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: 121, TeamID: 31, MemberKey: "developer", Role: "senior-developer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + workItems: []models.TeamWorkItem{ + {TeamID: 31, RootTaskID: taskID, WorkID: "assignment-developer-legacy", OwnerMemberID: &developer.ID, Status: models.TeamTaskStatusDispatched, UpdatedAt: time.Now().UTC()}, + {TeamID: 31, RootTaskID: taskID, WorkID: "member-developer", OwnerMemberID: &developer.ID, Status: models.TeamTaskStatusSucceeded, UpdatedAt: time.Now().UTC()}, + }, + } + service := &teamService{repo: repo} + ready, err := service.leaderMediatedRootCompletionReady(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, task, leader) + if err != nil { + t.Fatalf("leaderMediatedRootCompletionReady returned error: %v", err) + } + if !ready { + t.Fatalf("same-owner legacy dispatch card must not block root completion after member result") + } } func TestProjectTeamEventAssociatesCurrentTaskReplyWithoutCompletingIt(t *testing.T) { @@ -1992,6 +3250,104 @@ func TestProjectTeamEventCompletionReportMentioningDispatchStillClosesTask(t *te } } +func TestProjectTeamEventProtocolV2BootstrapCompletionMentioningTeamSendClosesTask(t *testing.T) { + teamID := 40 + taskID := 69 + messageID := "team-40-bootstrap-introduction" + workspaceRoot := t.TempDir() + resultDir := filepath.Join(workspaceRoot, "teams", "user-1", "team-40-shared", "results", "team-40-task-69") + if err := os.MkdirAll(resultDir, 0o775); err != nil { + t.Fatalf("create result dir: %v", err) + } + for _, name := range []string{"team-introduction.md", "result.md"} { + if err := os.WriteFile(filepath.Join(resultDir, name), []byte("team introduction"), 0o664); err != nil { + t.Fatalf("write %s: %v", name, err) + } + } + task := &models.TeamTask{ + ID: taskID, + TeamID: teamID, + TargetMemberID: 145, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ + ID: 145, + TeamID: teamID, + MemberKey: "leader", + Role: "leader", + Status: models.TeamMemberStatusBusy, + CurrentTaskID: &taskID, + Availability: models.TeamMemberAvailabilityBusy, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: workspaceRoot} + report := strings.Join([]string{ + "# Product Discovery Team 23 - Bootstrap complete", + "", + "## Collaboration", + "- Mode: Leader Mediated; all user work enters through the Leader.", + "- Communication layer: Redis Streams with team_send, events, inbox, presence, and DLQ.", + "- Worker pipeline: Worker completes, writes result, calls team_complete_task, then reports back.", + "", + "Detailed report: /team/results/team-40-task-69/team-introduction.md", + }, "\n") + payloadJSON, err := json.Marshal(map[string]interface{}{ + "v": 1, + "protocolVersion": 2, + "eventId": "evt_team40_completion", + "event": "task_completed", + "type": "task_completed", + "memberId": "leader", + "messageId": messageID, + "taskId": "team-40-task-69", + "rootTaskId": "team-40-task-69", + "rootMessageId": messageID, + "status": "succeeded", + "runtimeStatus": "succeeded", + "availability": "idle", + "completionId": "completion:40:team-40-task-69:leader", + "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, + "summary": "Bootstrap report completed.", + "result": report, + "resultMarkdown": report, + "artifactRefs": []interface{}{"/team/results/team-40-task-69/team-introduction.md", "/team/results/team-40-task-69/result.md"}, + "sourceMessageId": messageID, + "completionMessageId": "completion:40:team-40-task-69:leader", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783407850912-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusSucceeded || repo.updatedTask.FinishedAt == nil { + t.Fatalf("protocol v2 completion report should close the root task, got %#v", repo.updatedTask) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "task_completed" { + t.Fatalf("expected stored task_completed event, got %#v", repo.createdEvents) + } + stored := teamEventPayloadMap(repo.createdEvents[0]) + if eventBool(stored, "leaderDispatchOnly") { + t.Fatalf("explicit completion must not be downgraded to leader dispatch, got %#v", stored) + } + step, ok := stored["collaborationStep"].(map[string]interface{}) + if !ok || step["type"] != "result" || step["status"] != models.TeamTaskStatusSucceeded { + t.Fatalf("expected result collaboration step for explicit completion, got %#v", step) + } +} + func TestActiveTeamMembersFiltersDeletedMembers(t *testing.T) { members := activeTeamMembers([]models.TeamMember{ {MemberKey: "leader", Status: models.TeamMemberStatusIdle}, @@ -2104,6 +3460,7 @@ func TestCollectTeamArtifactReferencesStopsAtMarkdownAndJSONDelimiters(t *testin payload := map[string]interface{}{ "resultMarkdown": "Report: `/team/results/team-22-task-40/team-introduction-report.md`.", "raw": `{"resultMarkdown":"/team/results/team-22-task-40/team-introduction-report.md\\n","artifactRefs":["/team/results/team-22-task-40/result.md"]}`, + "directory": "Detailed report directory: `/team/results/team-22-task-40/`.", "artifactRefs": []interface{}{ "/team/results/team-22-task-40/team-introduction-report.md", "/team/results/team-22-task-40/result.md", @@ -2125,6 +3482,246 @@ func TestCollectTeamArtifactReferencesStopsAtMarkdownAndJSONDelimiters(t *testin } } +func TestProjectTeamEventTreatsLegacyBootstrapReportAsCompletion(t *testing.T) { + teamID := 31 + taskID := 67 + messageID := "team-31-bootstrap-introduction" + workspaceRoot := t.TempDir() + resultDir := filepath.Join(workspaceRoot, "teams", "user-1", "team-31-shared", "results", "team-31-task-67") + if err := os.MkdirAll(resultDir, 0o775); err != nil { + t.Fatalf("create result dir: %v", err) + } + if err := os.WriteFile(filepath.Join(resultDir, "team-introduction.md"), []byte("team introduction"), 0o664); err != nil { + t.Fatalf("write result: %v", err) + } + task := &models.TeamTask{ + ID: taskID, + TeamID: teamID, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ + ID: 120, + TeamID: teamID, + MemberKey: "leader", + Role: "leader", + Status: models.TeamMemberStatusBusy, + CurrentTaskID: &taskID, + Availability: models.TeamMemberAvailabilityBusy, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: workspaceRoot} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "leader", + "messageId": messageID, + "text": "Team 31 bootstrap introduction completed. The Team roster, runtime status, collaboration mode, " + + "capability boundaries, Redis Streams team_send mechanism, shared workspace rules, and available methods " + + "have been delivered. Detailed report: /team/results/team-31-task-67/team-introduction.md", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178692-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusSucceeded || repo.updatedTask.FinishedAt == nil { + t.Fatalf("legacy bootstrap report should close the root task, got %#v", repo.updatedTask) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "task_completed" { + t.Fatalf("expected one normalized task_completed event, got %#v", repo.createdEvents) + } + payload := teamEventPayloadMap(repo.createdEvents[0]) + if !eventBool(payload, "legacyCompletionCandidate") || eventString(payload, "completionSource") != "legacy_runtime_reply" { + t.Fatalf("expected legacy completion markers, got %#v", payload) + } +} + +func TestProjectTeamEventTreatsServerBootstrapToolReplyAsCompletion(t *testing.T) { + teamID := 30 + taskID := 55 + messageID := "team-30-bootstrap-introduction" + workspaceRoot := t.TempDir() + resultDir := filepath.Join(workspaceRoot, "teams", "user-1", "team-30-shared", "results", "team-30-task-55") + if err := os.MkdirAll(resultDir, 0o775); err != nil { + t.Fatalf("create result dir: %v", err) + } + if err := os.WriteFile(filepath.Join(resultDir, "team-introduction.md"), []byte("team introduction"), 0o664); err != nil { + t.Fatalf("write result: %v", err) + } + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 120, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 120, TeamID: teamID, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + designer := &models.TeamMember{ID: 121, TeamID: teamID, MemberKey: "ui-designer", Role: "ui-ux-designer", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "ui-designer": designer}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: workspaceRoot} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "leader", + "messageId": messageID, + "completionSource": teamTaskCompletionTool, + "status": "dispatched", + "summary": "Bootstrap 团队介绍任务完成。已编制 product-discovery-team (Team 30) 完整团队画像,包含 4 名成员的角色职责、运行时状态、" + + "技术能力边界,以及 leader_mediated 协作模式下的任务流转、消息同步、上下文共享与可调用方法说明。详细报告已落盘至 /team/results/team-30-task-55/team-introduction.md。", + "resultMarkdown": "# Product Discovery Team (team-30) — Bootstrap 团队介绍\n\n" + + "## 团队架构\n\n**协作模式**:`leader_mediated`。Leader 是唯一入口,Worker 之间禁止直连。\n\n" + + "| 成员 | 角色 | 运行时 |\n|------|------|--------|\n| discovery-lead | Leader | OpenClaw Pro |\n| ui-designer | UI Designer | Hermes Pro |\n\n" + + "详细报告:/team/results/team-30-task-55/team-introduction.md", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178693-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusSucceeded || repo.updatedTask.FinishedAt == nil { + t.Fatalf("server bootstrap tool reply should close the root task, got %#v", repo.updatedTask) + } + if len(repo.workItems) != 0 { + t.Fatalf("server bootstrap tool reply must not create synthetic assignments, got %#v", repo.workItems) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "task_completed" { + t.Fatalf("expected one normalized task_completed event, got %#v", repo.createdEvents) + } +} + +func TestProjectTeamEventDoesNotCreateBootstrapAssignmentWorkItem(t *testing.T) { + teamID := 30 + taskID := 55 + messageID := "team-30-bootstrap-introduction" + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 120, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 120, TeamID: teamID, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + designer := &models.TeamMember{ID: 121, TeamID: teamID, MemberKey: "ui-designer", Role: "ui-ux-designer", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "ui-designer": designer}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: t.TempDir()} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "leader", + "messageId": messageID, + "text": "任务分派:请 ui-designer 补充团队能力边界说明。", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178694-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.workItems) != 0 { + t.Fatalf("bootstrap/control-plane replies must not create work items, got %#v", repo.workItems) + } +} + +func TestProjectTeamEventBootstrapCompletionIgnoresStaleSyntheticWorkItem(t *testing.T) { + teamID := 30 + taskID := 55 + messageID := "team-30-bootstrap-introduction" + workspaceRoot := t.TempDir() + resultDir := filepath.Join(workspaceRoot, "teams", "user-1", "team-30-shared", "results", "team-30-task-55") + if err := os.MkdirAll(resultDir, 0o775); err != nil { + t.Fatalf("create result dir: %v", err) + } + if err := os.WriteFile(filepath.Join(resultDir, "team-introduction.md"), []byte("full report"), 0o664); err != nil { + t.Fatalf("write result: %v", err) + } + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 120, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 120, TeamID: teamID, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + designer := &models.TeamMember{ID: 121, TeamID: teamID, MemberKey: "ui-designer", Role: "ui-ux-designer", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader, "ui-designer": designer}, + workItems: []models.TeamWorkItem{{ + TeamID: teamID, + RootTaskID: taskID, + WorkID: "member-ui-designer", + OwnerMemberID: &designer.ID, + Title: "Assign to ui-designer", + Status: models.TeamTaskStatusDispatched, + UpdatedAt: time.Now().UTC(), + }}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: workspaceRoot} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "task_completed", + "memberId": "leader", + "messageId": messageID, + "status": "succeeded", + "summary": "Bootstrap complete.", + "resultMarkdown": "Bootstrap 完成。详见 /team/results/team-30-task-55/team-introduction.md", + "artifactRefs": []string{"/team/results/team-30-task-55/team-introduction.md"}, + "explicitCompletion": true, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178695-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusSucceeded { + t.Fatalf("bootstrap completion must ignore stale synthetic work items, got %#v", repo.updatedTask) + } +} + +func TestProjectTeamEventDoesNotTreatLegacyAckAsCompletion(t *testing.T) { + teamID := 31 + taskID := 68 + messageID := "team-31-bootstrap-introduction" + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 120, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 120, TeamID: teamID, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: t.TempDir()} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "leader", + "messageId": messageID, + "text": "Redis Team task completed", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178693-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if repo.updatedTask != nil { + t.Fatalf("ack-only legacy reply must not close the root task, got %#v", repo.updatedTask) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "reply" { + t.Fatalf("expected ack to stay as reply, got %#v", repo.createdEvents) + } +} + func TestProjectTeamEventAcceptsExistingMarkdownArtifactReference(t *testing.T) { teamID := 22 taskID := 40 @@ -2172,12 +3769,82 @@ func TestProjectTeamEventAcceptsExistingMarkdownArtifactReference(t *testing.T) } } +func TestProjectTeamEventResolvesProtocolV2CompletionByMessageID(t *testing.T) { + teamID := 22 + taskID := 52 + messageID := "team-22-bootstrap-introduction" + workspaceRoot := t.TempDir() + resultDir := filepath.Join(workspaceRoot, "teams", "user-1", "team-22-shared", "results", "team-22-task-52") + if err := os.MkdirAll(resultDir, 0o775); err != nil { + t.Fatalf("create result dir: %v", err) + } + if err := os.WriteFile(filepath.Join(resultDir, "result.md"), []byte("full report"), 0o664); err != nil { + t.Fatalf("write result: %v", err) + } + task := &models.TeamTask{ + ID: taskID, + TeamID: teamID, + TargetMemberID: 120, + MessageID: messageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC(), + } + leader := &models.TeamMember{ + ID: 120, + TeamID: teamID, + MemberKey: "leader", + Role: "leader", + Status: models.TeamMemberStatusBusy, + CurrentTaskID: &taskID, + Availability: models.TeamMemberAvailabilityBusy, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: workspaceRoot} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "protocolVersion": 2, + "event": "task_completed", + "eventId": "evt-team-22-task-52", + "completionId": "completion:22:team-22-task-52:leader", + "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, + "memberId": "leader", + "messageId": messageID, + "completionMessageId": messageID, + // Some runtimes preserve their own internal task identifier here. The + // root task must still resolve through the message identifiers above. + "taskId": "runtime-task-52", + "rootTaskId": "runtime-task-52", + "status": "succeeded", + "summary": "Team introduction complete.", + "resultMarkdown": "Full report: /team/results/team-22-task-52/result.md", + "artifactRefs": []string{"/team/results/team-22-task-52/result.md"}, + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171178690-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusSucceeded || repo.updatedTask.FinishedAt == nil { + t.Fatalf("protocol v2 completion should resolve and close the root task, got %#v", repo.updatedTask) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "task_completed" { + t.Fatalf("expected accepted task_completed event, got %#v", repo.createdEvents) + } +} + func TestProjectTeamEventMissingArtifactKeepsFinalAnswerInWarning(t *testing.T) { teamID := 22 taskID := 40 messageID := "team-22-bootstrap-introduction" - previouslyFinished := time.Now().UTC().Add(-time.Minute) - task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 120, MessageID: messageID, Status: models.TeamTaskStatusSucceeded, FinishedAt: &previouslyFinished, UpdatedAt: time.Now().UTC()} + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 120, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} leader := &models.TeamMember{ID: 120, TeamID: teamID, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} repo := &teamRepositoryStub{ tasksByID: map[int]*models.TeamTask{taskID: task}, @@ -2187,14 +3854,25 @@ func TestProjectTeamEventMissingArtifactKeepsFinalAnswerInWarning(t *testing.T) service := &teamService{repo: repo, runtimeWorkspaceRoot: t.TempDir()} finalBody := "# Delivery Team\n\nDetailed final answer." payloadJSON, err := json.Marshal(map[string]interface{}{ - "event": "task_completed", + "protocolVersion": 3, + "event": "completion_proposed", + "eventId": "evt-missing-artifact", + "completionId": "completion:22:team-22-task-40:leader", + "attemptId": "attempt-missing-artifact", + "completionSource": teamTaskCompletionTool, "memberId": "leader", "messageId": messageID, + "taskId": "team-22-task-40", + "rootTaskId": "team-22-task-40", "status": "succeeded", "summary": "Team introduction complete.", "resultMarkdown": finalBody, "artifactRefs": []string{"/team/results/team-22-task-40/missing.md"}, "explicitCompletion": true, + "rootTaskTerminal": true, + "workflowFinal": true, + "finalAnswerReady": true, + "remainingActions": []string{}, }) if err != nil { t.Fatalf("marshal payload: %v", err) @@ -2221,6 +3899,12 @@ func TestProjectTeamEventMissingArtifactKeepsFinalAnswerInWarning(t *testing.T) if stored["artifactValidationMessage"] == "" { t.Fatalf("expected separate artifact validation diagnostic, got %#v", stored) } + if stored["completionDecision"] != teamCompletionDecisionRejected || stored["completionDecisionReason"] != "missing_artifacts" { + t.Fatalf("missing artifact must never acknowledge completion as accepted, got %#v", stored) + } + if len(repo.outboxRows) != 1 || !strings.Contains(repo.outboxRows[0].PayloadJSON, `"decision":"rejected"`) { + t.Fatalf("missing artifact rejection must be delivered reliably, got %#v", repo.outboxRows) + } } func TestNormalizeTeamArtifactReferencesDoesNotCorruptSerializedJSON(t *testing.T) { @@ -2270,6 +3954,11 @@ func TestTeamTaskCompletionSignalsRemainBackwardCompatibleAcrossProtocolVersions if !isTeamTaskCompletionSignal("task_completed", "succeeded", v2) { t.Fatal("expected an explicit v2 task_completed event to be terminal") } + v2WithoutArtifacts := cloneStringInterfaceMap(v2) + delete(v2WithoutArtifacts, "artifactRefs") + if !isTeamTaskCompletionSignal("task_completed", "succeeded", v2WithoutArtifacts) { + t.Fatal("expected explicit v2 completion with resultMarkdown to be terminal even without artifactRefs") + } missingBody := cloneStringInterfaceMap(v2) delete(missingBody, "resultMarkdown") @@ -2405,7 +4094,689 @@ func TestTaskHasRecentActivityUsesBusinessWorkItems(t *testing.T) { } } +func TestLeaderMediatedWorkerProgressIsNotConfirmedAsResult(t *testing.T) { + teamID := 46 + taskID := 79 + messageID := "team-46-task-1783559040281180893" + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 169, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 169, TeamID: teamID, MemberKey: "delivery-lead", Role: "leader", Status: models.TeamMemberStatusBusy, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: 170, TeamID: teamID, MemberKey: "developer", Role: "developer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByID: map[int]*models.TeamMember{169: leader, 170: developer}, + membersByKey: map[string]*models.TeamMember{"delivery-lead": leader, "leader": leader, "developer": developer}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: t.TempDir()} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "developer", + "messageId": "msg-progress-1", + "rootTaskId": "team-46-task-79", + "rootMessageId": messageID, + "status": "running", + "text": "RAG Papers Task - Progress Update: Starting paper search and crawl. I found candidates and am now processing all PDFs.", + }) + if err != nil { + t.Fatal(err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783559160000-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + + if len(repo.workItems) != 0 { + t.Fatalf("progress-only worker reply must not create a completed member result, got %#v", repo.workItems) + } + for _, event := range repo.createdEvents { + if event.EventType == "member_result_confirmed" { + t.Fatalf("progress-only worker reply must not emit result confirmation: %#v", event) + } + } +} + +func TestLeaderMediatedMonitorBlockerDoesNotCloseRootTask(t *testing.T) { + teamID := 46 + taskID := 79 + messageID := "team-46-task-1783559040281180893" + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 169, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 169, TeamID: teamID, MemberKey: "delivery-lead", Role: "leader", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByID: map[int]*models.TeamMember{169: leader}, + membersByKey: map[string]*models.TeamMember{"delivery-lead": leader, "leader": leader}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: t.TempDir()} + monitorSummary := "Monitoring agent for dev-papers-001 completed. Developer unresponsive after 3 consecutive status checks. Zero paper files found, no summary created. Blocker report sent to leader for investigation." + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "task_failed", + "memberId": "delivery-lead", + "messageId": messageID, + "rootTaskId": "team-46-task-79", + "rootMessageId": messageID, + "status": "failed", + "summary": monitorSummary, + }) + if err != nil { + t.Fatal(err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783560082000-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + + if repo.updatedTask != nil && repo.updatedTask.Status == models.TeamTaskStatusFailed { + t.Fatalf("monitor blocker candidate must not close root task, got %#v", repo.updatedTask) + } + if len(repo.createdEvents) != 1 || repo.createdEvents[0].EventType != "message_warning" { + t.Fatalf("expected monitor blocker to be stored as warning, got %#v", repo.createdEvents) + } + stored := teamEventPayloadMap(repo.createdEvents[0]) + if !eventBool(stored, "monitorBlockerCandidate") || eventString(stored, "status") != "attention_required" { + t.Fatalf("expected monitor blocker candidate markers, got %#v", stored) + } +} + +func TestLeaderMediatedMemberResultReopensNonAuthoritativeMonitorFailure(t *testing.T) { + teamID := 46 + taskID := 79 + messageID := "team-46-task-1783559040281180893" + finishedAt := time.Now().UTC().Add(-5 * time.Minute) + monitorSummary := "Monitoring agent for dev-papers-001 completed. Developer unresponsive after 3 consecutive status checks. Zero paper files found, no summary created." + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 169, MessageID: messageID, Status: models.TeamTaskStatusFailed, FinishedAt: &finishedAt, ErrorMessage: &monitorSummary, UpdatedAt: time.Now().UTC()} + leader := &models.TeamMember{ID: 169, TeamID: teamID, MemberKey: "delivery-lead", Role: "leader", Status: models.TeamMemberStatusBusy, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: 170, TeamID: teamID, MemberKey: "developer", Role: "developer", Status: models.TeamMemberStatusBusy, CurrentTaskID: &taskID, Availability: models.TeamMemberAvailabilityBusy} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByID: map[int]*models.TeamMember{169: leader, 170: developer}, + membersByKey: map[string]*models.TeamMember{"delivery-lead": leader, "leader": leader, "developer": developer}, + } + service := &teamService{repo: repo, runtimeWorkspaceRoot: t.TempDir()} + resultText := "Final Delivery Confirmation - dev-papers-001 COMPLETE\n\nFiles are now at /team/artifacts/team-46-task-79/members/developer/dev-papers-001/report.md." + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "reply", + "memberId": "developer", + "messageId": "msg-final-1", + "rootTaskId": "team-46-task-79", + "rootMessageId": messageID, + "status": "succeeded", + "summary": "Developer delivered the paper report.", + "resultMarkdown": resultText, + }) + if err != nil { + t.Fatal(err) + } + + if err := service.projectTeamEvent(&models.Team{ID: teamID, UserID: 1, SharedMountPath: "/team", CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783560514000-0", Fields: map[string]string{"payload": string(payloadJSON)}, + }); err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + + if repo.updatedTask == nil || repo.updatedTask.Status != models.TeamTaskStatusRunning || repo.updatedTask.FinishedAt != nil || repo.updatedTask.ErrorMessage != nil { + t.Fatalf("member result should reopen non-authoritative monitor failure, got %#v", repo.updatedTask) + } + if len(repo.workItems) != 1 || repo.workItems[0].Status != models.TeamTaskStatusSucceeded || repo.workItems[0].WorkID != "member-developer" { + t.Fatalf("expected developer result work item, got %#v", repo.workItems) + } + if len(repo.createdEvents) < 2 || repo.createdEvents[len(repo.createdEvents)-1].EventType != "member_result_confirmed" { + t.Fatalf("expected member result confirmation event, got %#v", repo.createdEvents) + } +} + +func TestAssignmentMonitorEnvelopeIsNonTerminalAndAddressedToWorker(t *testing.T) { + now := time.Date(2026, 7, 9, 10, 30, 0, 0, time.UTC) + team := &models.Team{ID: 46} + task := &models.TeamTask{ID: 78, TeamID: 46, MessageID: "team-46-task-1783559040281180893"} + item := &models.TeamWorkItem{ID: 9001, WorkID: "dev-papers-001", Title: "Assign to developer", UpdatedAt: now.Add(-4 * time.Minute)} + owner := &models.TeamMember{ID: 301, TeamID: 46, MemberKey: "developer"} + + envelope, messageID := buildAssignmentStatusCheckEnvelope(team, task, item, owner, now) + if messageID != "monitor:team-46-task-78:dev-papers-001:9908850" { + t.Fatalf("unexpected monitor message id %q", messageID) + } + if envelope["intent"] != "assignment_status_check" || envelope["to"] != "developer" || envelope["from"] != "clawmanager-monitor" { + t.Fatalf("unexpected monitor routing: %#v", envelope) + } + if envelope["requiresCompletion"] != false || envelope["rootTaskId"] != "team-46-task-78" || envelope["workId"] != "dev-papers-001" { + t.Fatalf("monitor envelope should be non-terminal and assignment scoped: %#v", envelope) + } + if envelope["checkId"] != messageID || envelope["checkSequence"] != int64(9908850) || envelope["requestedAt"] == "" { + t.Fatalf("monitor envelope must carry stable check identity: %#v", envelope) + } + monitorPolicy, ok := envelope["monitorPolicy"].(map[string]interface{}) + if !ok || monitorPolicy["enabled"] != true || monitorPolicy["visibleToChat"] != true { + t.Fatalf("expected visible monitor policy on monitor envelope, got %#v", envelope["monitorPolicy"]) + } + if monitorPolicy["heartbeatEverySec"] != 30 || monitorPolicy["visibleHeartbeatEverySec"] != 180 { + t.Fatalf("expected monitor envelope to separate internal heartbeat and chat digest cadence, got %#v", monitorPolicy) + } + prompt, _ := envelope["prompt"].(string) + if !strings.Contains(prompt, "call team_update_progress") || !strings.Contains(prompt, "assignment_check_result") || strings.Contains(prompt, "call team_complete_task") { + t.Fatalf("monitor prompt should request progress without forcing completion: %s", prompt) + } + metadata, ok := envelope["metadata"].(map[string]interface{}) + if !ok || metadata["monitor"] != true || metadata["monitorType"] != "assignment_status_check" || metadata["eventKind"] != "assignment_check_requested" || metadata["visibleToChat"] != false { + t.Fatalf("unexpected monitor metadata: %#v", envelope["metadata"]) + } +} + +func TestAssignmentMonitorEligibilityAndThrottle(t *testing.T) { + cutoff := time.Date(2026, 7, 9, 10, 30, 0, 0, time.UTC) + ownerID := 301 + if !shouldMonitorTeamWorkItem(models.TeamWorkItem{OwnerMemberID: &ownerID, WorkID: "dev-papers-001", Status: models.TeamTaskStatusRunning, UpdatedAt: cutoff.Add(-time.Second)}, cutoff) { + t.Fatal("expected stale running worker item to be monitored") + } + if shouldMonitorTeamWorkItem(models.TeamWorkItem{OwnerMemberID: &ownerID, WorkID: "dev-papers-001", Status: models.TeamTaskStatusRunning, UpdatedAt: cutoff.Add(time.Second)}, cutoff) { + t.Fatal("fresh worker item should not be monitored") + } + if shouldMonitorTeamWorkItem(models.TeamWorkItem{OwnerMemberID: &ownerID, WorkID: "dev-papers-001", Status: models.TeamTaskStatusSucceeded, UpdatedAt: cutoff.Add(-time.Hour)}, cutoff) { + t.Fatal("terminal worker item should not be monitored") + } + if shouldMonitorTeamWorkItem(models.TeamWorkItem{WorkID: "dev-papers-001", Status: models.TeamTaskStatusRunning, UpdatedAt: cutoff.Add(-time.Hour)}, cutoff) { + t.Fatal("unowned worker item should not be monitored") + } + + service := &teamService{} + if !service.claimAssignmentMonitorSlot("46:78:dev-papers-001:301", cutoff) { + t.Fatal("expected first monitor slot claim to succeed") + } + if service.claimAssignmentMonitorSlot("46:78:dev-papers-001:301", cutoff.Add(teamAssignmentMonitorEvery-time.Second)) { + t.Fatal("expected monitor slot to be throttled inside interval") + } + if !service.claimAssignmentMonitorSlot("46:78:dev-papers-001:301", cutoff.Add(teamAssignmentMonitorEvery+time.Second)) { + t.Fatal("expected monitor slot to reopen after interval") + } +} + +func TestLeaderMediatedRecoverableWarningClassification(t *testing.T) { + team := &models.Team{ID: 47, CommunicationMode: teamCommunicationModeLeaderMediated} + task := &models.TeamTask{ID: 82, TeamID: 47, Status: models.TeamTaskStatusRunning} + member := &models.TeamMember{ID: 12, TeamID: 47, MemberKey: "developer", Role: "developer"} + if !isLeaderMediatedRecoverableWarning(team, "message_warning", map[string]interface{}{ + "artifactValidationFailed": true, + "rootTaskTerminal": false, + }, member, task) { + t.Fatal("artifact validation warnings should start recovery") + } + if !isLeaderMediatedRecoverableWarning(team, "message_warning", map[string]interface{}{ + "nonAuthoritative": true, + "rootTaskTerminal": false, + "eventKind": "completion_validation_warning", + "assignmentResult": "candidate", + "assignmentId": "member-developer", + "assignment_result": "candidate", + }, member, task) { + t.Fatal("non-authoritative worker warnings should start recovery") + } + if isLeaderMediatedRecoverableWarning(team, "message_warning", map[string]interface{}{ + "eventKind": "assignment_recovery_exhausted", + "rootTaskTerminal": false, + }, member, task) { + t.Fatal("recovery exhausted should not start another automatic recovery") + } + if isLeaderMediatedRecoverableWarning(team, "assignment_heartbeat", map[string]interface{}{ + "eventKind": "assignment_heartbeat", + "nonAuthoritative": true, + "rootTaskTerminal": false, + }, member, task) { + t.Fatal("heartbeat must not start recovery") + } + if isLeaderMediatedRecoverableWarning(team, "task_progress", map[string]interface{}{ + "eventKind": "assignment_check_result", + "nonAuthoritative": true, + "rootTaskTerminal": false, + }, member, task) { + t.Fatal("passive assignment check result must not start recovery") + } +} + +func TestProjectTeamEventDropsHeartbeatAfterTerminalTask(t *testing.T) { + taskID := 91 + messageID := "team-49-bootstrap-introduction" + finishedAt := time.Now().UTC().Add(-time.Minute) + task := &models.TeamTask{ + ID: taskID, + TeamID: 49, + TargetMemberID: 700, + MessageID: messageID, + Status: models.TeamTaskStatusSucceeded, + FinishedAt: &finishedAt, + UpdatedAt: finishedAt, + } + leader := &models.TeamMember{ + ID: 700, + TeamID: 49, + MemberKey: "delivery-lead", + Role: "leader", + Status: models.TeamMemberStatusIdle, + Availability: models.TeamMemberAvailabilityIdle, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByKey: map[string]*models.TeamMember{"delivery-lead": leader}, + } + service := &teamService{repo: repo} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "assignment_heartbeat", + "eventKind": "assignment_heartbeat", + "memberId": "delivery-lead", + "messageId": messageID, + "taskId": "team-49-task-91", + "heartbeatSeq": 12, + "visibleToChat": true, + "summary": "Agent turn is still running", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + err = service.projectTeamEvent(&models.Team{ID: 49, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1781171179000-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }) + if err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.createdEvents) != 0 { + t.Fatalf("terminal heartbeat must not be stored as a chat event, got %#v", repo.createdEvents) + } + if repo.updatedTask != nil || repo.updatedMember != nil { + t.Fatalf("terminal heartbeat must not mutate task/member state, task=%#v member=%#v", repo.updatedTask, repo.updatedMember) + } +} + +func TestProjectTeamEventDropsHeartbeatAfterTerminalWorkItem(t *testing.T) { + rootTaskID := 95 + memberID := 701 + task := &models.TeamTask{ + ID: rootTaskID, + TeamID: 52, + TargetMemberID: 700, + MessageID: "team-52-task-1783601111", + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC().Add(-time.Minute), + } + developer := &models.TeamMember{ + ID: memberID, + TeamID: 52, + MemberKey: "developer", + Role: "developer", + Status: models.TeamMemberStatusIdle, + Availability: models.TeamMemberAvailabilityIdle, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{rootTaskID: task}, + membersByID: map[int]*models.TeamMember{memberID: developer}, + membersByKey: map[string]*models.TeamMember{"developer": developer}, + workItems: []models.TeamWorkItem{{ + TeamID: 52, + RootTaskID: rootTaskID, + WorkID: "member-developer", + OwnerMemberID: &memberID, + Title: "developer delivers result", + Status: models.TeamTaskStatusSucceeded, + UpdatedAt: time.Now().UTC().Add(-time.Minute), + }}, + } + service := &teamService{repo: repo} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "assignment_heartbeat", + "eventKind": "assignment_heartbeat", + "memberId": "developer", + "from": "developer", + "taskId": "team-52-task-95", + "rootTaskId": "team-52-task-95", + "workId": "member-developer", + "assignmentId": "member-developer", + "status": models.TeamTaskStatusRunning, + "nonAuthoritative": true, + "summary": "Agent turn is still running", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + err = service.projectTeamEvent(&models.Team{ID: 52, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783601200000-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }) + if err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.createdEvents) != 0 { + t.Fatalf("post-terminal work item heartbeat must not be stored, got %#v", repo.createdEvents) + } + if repo.updatedTask != nil || repo.updatedMember != nil { + t.Fatalf("post-terminal work item heartbeat must not mutate task/member state, task=%#v member=%#v", repo.updatedTask, repo.updatedMember) + } +} + +func TestProjectTeamEventMapsGeneratedWorkerCompletionToExistingWorkItem(t *testing.T) { + rootTaskID := 87 + rootMessageID := "team-50-task-1783587467953008490" + ownerID := 183 + task := &models.TeamTask{ + ID: rootTaskID, + TeamID: 50, + TargetMemberID: 181, + MessageID: rootMessageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: time.Now().UTC().Add(-time.Minute), + } + reviewer := &models.TeamMember{ + ID: ownerID, + TeamID: 50, + MemberKey: "reviewer", + Role: "reviewer", + Status: models.TeamMemberStatusBusy, + Availability: models.TeamMemberAvailabilityBusy, + } + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{rootTaskID: task}, + membersByID: map[int]*models.TeamMember{ownerID: reviewer}, + membersByKey: map[string]*models.TeamMember{"reviewer": reviewer}, + workItems: []models.TeamWorkItem{{ + TeamID: 50, + RootTaskID: rootTaskID, + WorkID: "member-reviewer", + OwnerMemberID: &ownerID, + Title: "Assign to reviewer", + Status: models.TeamTaskStatusDispatched, + UpdatedAt: time.Now().UTC().Add(-2 * time.Minute), + }}, + } + service := &teamService{repo: repo} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "event": "task_completed", + "protocolVersion": 2, + "completionId": "completion:50:task_01d9:reviewer", + "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, + "assignmentResultOnly": true, + "memberId": "reviewer", + "from": "reviewer", + "to": "leader", + "taskId": "task_01d9ca72-9721-44ea-a389-d43bdf573521", + "rootTaskId": "task_01d9ca72-9721-44ea-a389-d43bdf573521", + "workId": "chat-app-review", + "status": models.TeamTaskStatusSucceeded, + "summary": "Review complete. Verdict: PASS.", + "resultMarkdown": "Review complete. Verdict: PASS.", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + + err = service.projectTeamEvent(&models.Team{ID: 50, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783589347769-0", + Fields: map[string]string{"payload": string(payloadJSON)}, + }) + if err != nil { + t.Fatalf("projectTeamEvent returned error: %v", err) + } + if len(repo.createdEvents) == 0 || repo.createdEvents[0].TaskID == nil || *repo.createdEvents[0].TaskID != rootTaskID { + t.Fatalf("expected generated runtime completion to attach to root task, got %#v", repo.createdEvents) + } + if len(repo.createdEvents) < 2 { + t.Fatalf("expected generated runtime completion to create a leader result notification, got %#v", repo.createdEvents) + } + notification := repo.createdEvents[len(repo.createdEvents)-1] + if notification.EventType != "member_result_confirmed" || notification.TaskID == nil || *notification.TaskID != rootTaskID { + t.Fatalf("expected member result confirmation on root task, got %#v", notification) + } + notificationPayload := teamEventPayloadMap(notification) + if eventString(notificationPayload, "rootTaskId") != "team-50-task-87" || + eventString(notificationPayload, "rootMessageId") != rootMessageID || + eventString(notificationPayload, "workId") != "member-reviewer" { + t.Fatalf("member result notification must preserve root context, got %#v", notificationPayload) + } + if eventBool(notificationPayload, "visibleToChat", "visible_to_chat") || + eventString(notificationPayload, "sourceCompletionId") != "completion:50:task_01d9:reviewer" || + eventString(notificationPayload, "sourceWorkId") != "chat-app-review" || + eventString(notificationPayload, "resultMarkdown", "result", "text") != "" { + t.Fatalf("member result confirmation must be hidden and linked to its source: %#v", notificationPayload) + } + if len(repo.outboxRows) != 1 || repo.outboxRows[0].MessageID == "" || repo.outboxRows[0].SourceEventID == "" { + t.Fatalf("member result confirmation must atomically create a delivery outbox row, got %#v", repo.outboxRows) + } + if len(repo.workItems) != 1 || repo.workItems[0].WorkID != "member-reviewer" || repo.workItems[0].Status != models.TeamTaskStatusSucceeded { + t.Fatalf("expected reviewer work item to close, got %#v", repo.workItems) + } + if repo.updatedTask == nil || repo.updatedTask.ID != rootTaskID || repo.updatedTask.Status != models.TeamTaskStatusRunning { + t.Fatalf("worker completion must touch but not close root task, got %#v", repo.updatedTask) + } + if repo.updatedMember == nil || repo.updatedMember.MemberKey != "reviewer" || repo.updatedMember.Status != models.TeamMemberStatusIdle { + t.Fatalf("expected reviewer member to become idle, got %#v", repo.updatedMember) + } +} + +func TestTerminalMemberPresenceCannotRestoreRunningState(t *testing.T) { + teamID := 51 + taskID := 91 + memberID := 220 + messageID := "team-51-task-91-root" + task := &models.TeamTask{ID: taskID, TeamID: teamID, TargetMemberID: 219, MessageID: messageID, Status: models.TeamTaskStatusRunning, UpdatedAt: time.Now().UTC()} + member := &models.TeamMember{ID: memberID, TeamID: teamID, MemberKey: "pm", Role: "product-manager", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle, Progress: 100} + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + tasksByMessageID: map[string]*models.TeamTask{messageID: task}, + membersByID: map[int]*models.TeamMember{memberID: member}, + membersByKey: map[string]*models.TeamMember{"pm": member}, + workItems: []models.TeamWorkItem{{ + TeamID: teamID, + RootTaskID: taskID, + WorkID: "member-pm", + OwnerMemberID: &memberID, + Status: models.TeamTaskStatusSucceeded, + }}, + } + service := &teamService{repo: repo} + payload, _ := json.Marshal(map[string]interface{}{ + "event": "presence", + "memberId": "pm", + "taskId": "team-51-task-91", + "rootTaskId": "team-51-task-91", + "availability": "busy", + "runtimeStatus": "running", + }) + if err := service.projectTeamEvent(&models.Team{ID: teamID, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, redisStreamMessage{ + ID: "1783593000000-0", Fields: map[string]string{"payload": string(payload)}, + }); err != nil { + t.Fatal(err) + } + if repo.updatedMember == nil || repo.updatedMember.Status != models.TeamMemberStatusIdle || repo.updatedMember.Availability != models.TeamMemberAvailabilityIdle || derefTeamString(repo.updatedMember.RuntimeStatus) != models.TeamTaskStatusSucceeded { + t.Fatalf("terminal worker presence restored running state: %#v", repo.updatedMember) + } +} + +func TestThreeWorkerResultsEachCreateOneConfirmationAndOutbox(t *testing.T) { + team := &models.Team{ID: 52, CommunicationMode: teamCommunicationModeLeaderMediated} + task := &models.TeamTask{ID: 93, TeamID: 52, TargetMemberID: 230, MessageID: "team-52-root", Status: models.TeamTaskStatusRunning} + leader := &models.TeamMember{ID: 230, TeamID: 52, MemberKey: "delivery-lead", Role: "leader"} + members := []*models.TeamMember{ + {ID: 231, TeamID: 52, MemberKey: "pm", Role: "product-manager"}, + {ID: 232, TeamID: 52, MemberKey: "designer", Role: "ui-designer"}, + {ID: 233, TeamID: 52, MemberKey: "architect", Role: "solution-architect"}, + } + repo := &teamRepositoryStub{ + membersByID: map[int]*models.TeamMember{230: leader, 231: members[0], 232: members[1], 233: members[2]}, + membersByKey: map[string]*models.TeamMember{"delivery-lead": leader, "pm": members[0], "designer": members[1], "architect": members[2]}, + } + for _, member := range members { + ownerID := member.ID + repo.workItems = append(repo.workItems, models.TeamWorkItem{ + TeamID: team.ID, + RootTaskID: task.ID, + WorkID: "member-" + member.MemberKey, + OwnerMemberID: &ownerID, + Status: models.TeamTaskStatusSucceeded, + }) + } + service := &teamService{repo: repo} + var wg sync.WaitGroup + errs := make(chan error, len(members)) + for index, member := range members { + index, member := index, member + wg.Add(1) + go func() { + defer wg.Done() + completionID := fmt.Sprintf("completion:52:93:%s", member.MemberKey) + payload := map[string]interface{}{ + "completionId": completionID, + "workId": "assignment-" + member.MemberKey, + "summary": fmt.Sprintf("%s 已完成任务", member.MemberKey), + "resultMarkdown": fmt.Sprintf("%s 已完成任务并提交产物。", member.MemberKey), + } + sourceEventID := fmt.Sprintf("evt-worker-%d", index) + errs <- service.createLeaderMediatedResultNotification(team, nil, task, member, payload, &models.TeamEvent{EventID: &sourceEventID, CreatedAt: time.Now().UTC()}) + }() + } + wg.Wait() + close(errs) + for err := range errs { + if err != nil { + t.Fatal(err) + } + } + confirmations := 0 + seenMembers := map[string]bool{} + for _, event := range repo.createdEvents { + if event.EventType != "member_result_confirmed" { + continue + } + confirmations++ + payload := teamEventPayloadMap(event) + seenMembers[eventString(payload, "memberId")] = true + } + if confirmations != 3 || len(seenMembers) != 3 || len(repo.outboxRows) != 3 || !seenMembers["pm"] { + t.Fatalf("three-worker confirmation reconciliation failed: events=%#v outbox=%#v", repo.createdEvents, repo.outboxRows) + } +} + +func TestLeaderSynthesisReminderCreatedWhenWorkersDone(t *testing.T) { + now := time.Now().UTC() + rootTaskID := 89 + leaderID := 201 + developerID := 202 + reviewerID := 203 + rootMessageID := "team-51-task-1783600000000" + task := &models.TeamTask{ + ID: rootTaskID, + TeamID: 51, + TargetMemberID: leaderID, + MessageID: rootMessageID, + Status: models.TeamTaskStatusRunning, + UpdatedAt: now.Add(-5 * time.Minute), + } + leader := &models.TeamMember{ID: leaderID, TeamID: 51, MemberKey: "delivery-lead", Role: "leader", Status: models.TeamMemberStatusBusy, Availability: models.TeamMemberAvailabilityBusy} + developer := &models.TeamMember{ID: developerID, TeamID: 51, MemberKey: "developer", Role: "developer", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + reviewer := &models.TeamMember{ID: reviewerID, TeamID: 51, MemberKey: "reviewer", Role: "reviewer", Status: models.TeamMemberStatusIdle, Availability: models.TeamMemberAvailabilityIdle} + developerResult := `{"summary":"Build complete","resultMarkdown":"Developer delivered the chat app."}` + reviewerResult := `{"summary":"PASS","resultMarkdown":"Reviewer verdict: PASS."}` + items := []models.TeamWorkItem{ + {TeamID: 51, RootTaskID: rootTaskID, WorkID: "member-developer", OwnerMemberID: &developerID, Title: "Assign to developer", Status: models.TeamTaskStatusSucceeded, ResultJSON: &developerResult, UpdatedAt: now.Add(-4 * time.Minute)}, + {TeamID: 51, RootTaskID: rootTaskID, WorkID: "member-reviewer", OwnerMemberID: &reviewerID, Title: "Assign to reviewer", Status: models.TeamTaskStatusSucceeded, ResultJSON: &reviewerResult, UpdatedAt: now.Add(-4 * time.Minute)}, + } + membersByID := map[int]*models.TeamMember{ + leaderID: leader, + developerID: developer, + reviewerID: reviewer, + } + ready, resultItems := leaderMediatedRootNeedsSynthesisReminder(task, items, membersByID) + if !ready || len(resultItems) != 2 { + t.Fatalf("expected root to need leader synthesis after worker results, ready=%v items=%#v", ready, resultItems) + } + repo := &teamRepositoryStub{ + teamsByID: map[int]*models.Team{51: &models.Team{ID: 51, CommunicationMode: teamCommunicationModeLeaderMediated}}, + tasksByID: map[int]*models.TeamTask{rootTaskID: task}, + membersByID: map[int]*models.TeamMember{leaderID: leader, developerID: developer, reviewerID: reviewer}, + membersByKey: map[string]*models.TeamMember{"delivery-lead": leader, "developer": developer, "reviewer": reviewer}, + workItems: items, + } + service := &teamService{repo: repo} + if err := service.createLeaderSynthesisReminder(&models.Team{ID: 51, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, task, leader, resultItems, now); err != nil { + t.Fatalf("createLeaderSynthesisReminder returned error: %v", err) + } + if len(repo.createdEvents) != 1 { + t.Fatalf("expected one leader synthesis reminder event, got %#v", repo.createdEvents) + } + event := repo.createdEvents[0] + if event.EventType != "leader_synthesis_reminder" || event.TaskID == nil || *event.TaskID != rootTaskID || event.MemberID == nil || *event.MemberID != leaderID { + t.Fatalf("unexpected leader synthesis reminder event: %#v", event) + } + payload := teamEventPayloadMap(event) + if eventString(payload, "rootTaskId") != "team-51-task-89" || + eventString(payload, "rootMessageId") != rootMessageID || + eventString(payload, "workId") != "leader-final-synthesis" { + t.Fatalf("leader synthesis reminder must preserve root context, got %#v", payload) + } + memberResults, _ := payload["memberResults"].([]interface{}) + if len(memberResults) != 2 { + t.Fatalf("expected reminder to carry member result summaries, got %#v", payload["memberResults"]) + } +} + +func TestReconcileDeferredCompletionNeverReusesReportFromOlderPlan(t *testing.T) { + now := time.Now().UTC() + taskID := 194 + task := &models.TeamTask{ + ID: taskID, + TeamID: 31, + TargetMemberID: 120, + MessageID: "team-31-task-194", + Status: models.TeamTaskStatusRunning, + WorkflowState: teamWorkflowStateAwaitingLeaderDecision, + PlanVersion: 2, + LedgerVersion: 7, + UpdatedAt: now, + } + leader := &models.TeamMember{ID: 120, TeamID: 31, MemberKey: "leader", Role: "leader", Status: models.TeamMemberStatusBusy} + payloadJSON, err := json.Marshal(map[string]interface{}{ + "protocolVersion": 3, + "event": "completion_deferred", + "completionId": "completion:31:team-31-task-194:leader", + "completionSource": teamTaskCompletionTool, + "explicitCompletion": true, + "rootTaskTerminal": false, + "workflowFinal": true, + "finalAnswerReady": true, + "remainingActions": []string{}, + "planVersion": 1, + "ledgerVersion": 3, + "resultMarkdown": "# 第一阶段报告\n\n这份报告不包含第二阶段产物。", + }) + if err != nil { + t.Fatalf("marshal payload: %v", err) + } + eventID := "old-plan-deferred" + repo := &teamRepositoryStub{ + tasksByID: map[int]*models.TeamTask{taskID: task}, + membersByKey: map[string]*models.TeamMember{"leader": leader}, + createdEvents: []models.TeamEvent{{ + TeamID: task.TeamID, TaskID: &taskID, MemberID: &leader.ID, EventID: &eventID, + EventType: "completion_deferred", PayloadJSON: stringPtr(string(payloadJSON)), + }}, + } + service := &teamService{repo: repo} + reconciled, err := service.reconcileDeferredTeamCompletion(&models.Team{ID: 31, CommunicationMode: teamCommunicationModeLeaderMediated}, nil, task, leader) + if err != nil { + t.Fatalf("reconcileDeferredTeamCompletion returned error: %v", err) + } + if reconciled || task.Status == models.TeamTaskStatusSucceeded || len(repo.createdEvents) != 1 { + t.Fatalf("an older plan report must not be auto-accepted after plan advancement: reconciled=%v task=%#v events=%#v", reconciled, task, repo.createdEvents) + } +} + type teamRepositoryStub struct { + mu sync.Mutex teamsByID map[int]*models.Team membersByID map[int]*models.TeamMember membersByKey map[string]*models.TeamMember @@ -2413,6 +4784,8 @@ type teamRepositoryStub struct { tasksByMessageID map[string]*models.TeamTask createdEvents []models.TeamEvent workItems []models.TeamWorkItem + workflowPhases []models.TeamWorkflowPhase + outboxRows []models.TeamEventOutbox updatedTask *models.TeamTask updatedMember *models.TeamMember updatedTeam *models.Team @@ -2481,7 +4854,21 @@ func (s *teamRepositoryStub) GetMemberByTeamKey(teamID int, memberKey string) (* return member, nil } func (s *teamRepositoryStub) ListMembersByTeamID(teamID int) ([]models.TeamMember, error) { - return nil, nil + result := make([]models.TeamMember, 0) + seen := map[int]bool{} + for _, member := range s.membersByKey { + if member != nil && member.TeamID == teamID && !seen[member.ID] { + result = append(result, *member) + seen[member.ID] = true + } + } + for _, member := range s.membersByID { + if member != nil && member.TeamID == teamID && !seen[member.ID] { + result = append(result, *member) + seen[member.ID] = true + } + } + return result, nil } func (s *teamRepositoryStub) CreateTask(task *models.TeamTask) error { return nil } func (s *teamRepositoryStub) UpdateTask(task *models.TeamTask) error { @@ -2515,6 +4902,8 @@ func (s *teamRepositoryStub) ListStaleCandidateTasks(cutoff time.Time, limit int return nil, nil } func (s *teamRepositoryStub) CreateEvent(event *models.TeamEvent) error { + s.mu.Lock() + defer s.mu.Unlock() clone := *event s.createdEvents = append(s.createdEvents, clone) return nil @@ -2523,6 +4912,8 @@ func (s *teamRepositoryStub) EventExistsByStreamID(teamID int, streamID string) return false, nil } func (s *teamRepositoryStub) EventExistsByEventID(teamID int, eventID string) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() for _, event := range s.createdEvents { if event.TeamID == teamID && event.EventID != nil && *event.EventID == eventID { return true, nil @@ -2544,6 +4935,8 @@ func (s *teamRepositoryStub) EventExistsByCompletionID(teamID int, completionID return false, nil } func (s *teamRepositoryStub) ListEventsByTeamID(teamID int, limit int) ([]models.TeamEvent, error) { + s.mu.Lock() + defer s.mu.Unlock() events := make([]models.TeamEvent, 0, len(s.createdEvents)) for _, event := range s.createdEvents { if event.TeamID == teamID { @@ -2559,8 +4952,25 @@ func (s *teamRepositoryStub) ListEventsBeforeID(teamID, beforeID, limit int) ([] return nil, nil } func (s *teamRepositoryStub) UpsertWorkItem(item *models.TeamWorkItem) error { + s.mu.Lock() + defer s.mu.Unlock() for idx := range s.workItems { if s.workItems[idx].TeamID == item.TeamID && s.workItems[idx].RootTaskID == item.RootTaskID && s.workItems[idx].WorkID == item.WorkID { + existing := s.workItems[idx] + newRevision := item.Revision > existing.Revision + existingTerminal := existing.Status == models.TeamTaskStatusSucceeded || existing.Status == models.TeamTaskStatusFailed || existing.Status == models.TeamTaskStatusStale + itemTerminal := item.Status == models.TeamTaskStatusSucceeded || item.Status == models.TeamTaskStatusFailed || item.Status == models.TeamTaskStatusStale + reopeningCurrent := !newRevision && existingTerminal && !itemTerminal && + !item.UpdatedAt.IsZero() && + (existing.UpdatedAt.IsZero() || item.UpdatedAt.After(existing.UpdatedAt)) + if !newRevision && !reopeningCurrent { + if item.ResultJSON == nil { + item.ResultJSON = existing.ResultJSON + } + if item.FinishedAt == nil { + item.FinishedAt = existing.FinishedAt + } + } s.workItems[idx] = *item return nil } @@ -2568,7 +4978,20 @@ func (s *teamRepositoryStub) UpsertWorkItem(item *models.TeamWorkItem) error { s.workItems = append(s.workItems, *item) return nil } +func (s *teamRepositoryStub) InvalidateWorkItemReview(workItemID int, updatedAt time.Time) error { + s.mu.Lock() + defer s.mu.Unlock() + for idx := range s.workItems { + if s.workItems[idx].ID == workItemID && s.workItems[idx].ReviewRequired && s.workItems[idx].ValidatedRevision != nil { + s.workItems[idx].ValidatedRevision = nil + s.workItems[idx].UpdatedAt = updatedAt + } + } + return nil +} func (s *teamRepositoryStub) ListWorkItemsByRootTaskID(rootTaskID int) ([]models.TeamWorkItem, error) { + s.mu.Lock() + defer s.mu.Unlock() result := make([]models.TeamWorkItem, 0) for _, item := range s.workItems { if item.RootTaskID == rootTaskID { @@ -2586,3 +5009,136 @@ func (s *teamRepositoryStub) ListWorkItemsByTeamID(teamID int, limit int) ([]mod } return result, nil } +func (s *teamRepositoryStub) UpsertWorkflowPhase(phase *models.TeamWorkflowPhase) error { + s.mu.Lock() + defer s.mu.Unlock() + for idx := range s.workflowPhases { + current := s.workflowPhases[idx] + if current.RootTaskID == phase.RootTaskID && current.PhaseID == phase.PhaseID && current.PlanVersion == phase.PlanVersion { + s.workflowPhases[idx] = *phase + return nil + } + } + clone := *phase + clone.ID = len(s.workflowPhases) + 1 + phase.ID = clone.ID + s.workflowPhases = append(s.workflowPhases, clone) + return nil +} +func (s *teamRepositoryStub) ListWorkflowPhasesByRootTaskID(rootTaskID int) ([]models.TeamWorkflowPhase, error) { + s.mu.Lock() + defer s.mu.Unlock() + result := make([]models.TeamWorkflowPhase, 0) + for _, phase := range s.workflowPhases { + if phase.RootTaskID == rootTaskID { + result = append(result, phase) + } + } + return result, nil +} +func (s *teamRepositoryStub) AcceptRootCompletion(task *models.TeamTask, expectedLedgerVersion int64, event *models.TeamEvent, outbox *models.TeamEventOutbox) (bool, error) { + s.mu.Lock() + defer s.mu.Unlock() + current := s.tasksByID[task.ID] + if current != nil && current != task && (current.LedgerVersion != expectedLedgerVersion || current.AcceptedCompletionID != nil || isTerminalTeamTaskStatus(current.Status)) { + return false, nil + } + cloneTask := *task + if s.tasksByID == nil { + s.tasksByID = map[int]*models.TeamTask{} + } + s.tasksByID[task.ID] = &cloneTask + s.updatedTask = &cloneTask + cloneEvent := *event + cloneEvent.ID = len(s.createdEvents) + 1 + event.ID = cloneEvent.ID + s.createdEvents = append(s.createdEvents, cloneEvent) + cloneOutbox := *outbox + cloneOutbox.ID = len(s.outboxRows) + 1 + outbox.ID = cloneOutbox.ID + s.outboxRows = append(s.outboxRows, cloneOutbox) + return true, nil +} +func (s *teamRepositoryStub) ConfirmWorkItemResult(item *models.TeamWorkItem, event *models.TeamEvent, outbox *models.TeamEventOutbox) error { + s.mu.Lock() + defer s.mu.Unlock() + updated := false + for idx := range s.workItems { + if s.workItems[idx].TeamID == item.TeamID && s.workItems[idx].RootTaskID == item.RootTaskID && s.workItems[idx].WorkID == item.WorkID { + s.workItems[idx] = *item + updated = true + break + } + } + if !updated { + s.workItems = append(s.workItems, *item) + } + eventExists := false + for idx := range s.createdEvents { + current := s.createdEvents[idx] + if current.TeamID == event.TeamID && current.EventID != nil && *current.EventID == derefTeamString(event.EventID) { + eventExists = true + break + } + } + if !eventExists { + s.createdEvents = append(s.createdEvents, *event) + } + for idx := range s.outboxRows { + if s.outboxRows[idx].TeamID == outbox.TeamID && s.outboxRows[idx].Destination == outbox.Destination && s.outboxRows[idx].MessageID == outbox.MessageID { + return nil + } + } + clone := *outbox + clone.ID = len(s.outboxRows) + 1 + outbox.ID = clone.ID + s.outboxRows = append(s.outboxRows, clone) + return nil +} +func (s *teamRepositoryStub) ListPendingEventOutbox(now time.Time, limit int) ([]models.TeamEventOutbox, error) { + result := make([]models.TeamEventOutbox, 0) + for _, row := range s.outboxRows { + if row.Status == "pending" && !row.AvailableAt.After(now) { + result = append(result, row) + } + } + if limit > 0 && len(result) > limit { + result = result[:limit] + } + return result, nil +} +func (s *teamRepositoryStub) CreateEventOutbox(outbox *models.TeamEventOutbox) error { + if outbox == nil { + return nil + } + for idx := range s.outboxRows { + if s.outboxRows[idx].TeamID == outbox.TeamID && s.outboxRows[idx].MessageID == outbox.MessageID { + return nil + } + } + clone := *outbox + clone.ID = len(s.outboxRows) + 1 + outbox.ID = clone.ID + s.outboxRows = append(s.outboxRows, clone) + return nil +} +func (s *teamRepositoryStub) MarkEventOutboxDelivered(id int, deliveredAt time.Time) error { + for idx := range s.outboxRows { + if s.outboxRows[idx].ID == id { + s.outboxRows[idx].Status = "delivered" + s.outboxRows[idx].DeliveredAt = &deliveredAt + } + } + return nil +} +func (s *teamRepositoryStub) MarkEventOutboxFailed(id int, availableAt time.Time, cause string) error { + for idx := range s.outboxRows { + if s.outboxRows[idx].ID == id { + s.outboxRows[idx].Status = "pending" + s.outboxRows[idx].Attempts++ + s.outboxRows[idx].AvailableAt = availableAt + s.outboxRows[idx].LastError = &cause + } + } + return nil +} diff --git a/docs/admin-user-guide.md b/docs/admin-user-guide.md index e986a4a..478e94c 100644 --- a/docs/admin-user-guide.md +++ b/docs/admin-user-guide.md @@ -17,6 +17,7 @@ Administrators use ClawManager to: End users use ClawManager to: - create or access OpenClaw workspaces +- create a template-based Team and describe a shared goal in the Team chat - open workspaces through the portal experience - inspect runtime status, agent signals, and recent command activity - attach or remove skills from an instance when permitted @@ -28,9 +29,11 @@ End users use ClawManager to: - [Agent Control Plane Guide](./agent-control-plane.md) - [Resource Management Guide](./resource-management.md) - [Security / Skill Scanner Guide](./security-skill-scanner.md) +- [Team Workspace Quick Guide](./team-workspaces-guide_en.md) ## Suggested Walkthrough 1. Start with the AI Gateway overview if your team cares most about model governance. 2. Review Agent Control Plane if your focus is runtime visibility and operations. 3. Review Resource Management and Security Center if you want reusable channels, skills, and scan-backed workflows. +4. Create a Team from a role template when a task benefits from coordinated planning, delivery, and review. diff --git a/docs/main/team-create.png b/docs/main/team-create.png new file mode 100644 index 0000000000000000000000000000000000000000..281ff48151ea23524c4fda36c8dce6bebb6bc1a6 GIT binary patch literal 100463 zcmb5W2UL?;&;}e=#kI1cf=II=AOsXdq+1!Q6aOwaE z1UjsJ=avx&v=0sf?T+}J3;2n!!{l$ke|EVWY25%7v?ojcttt??9w(m{{sZt%GADf{l1SCePdsoAPqmyPFp5evB>OER-Qis(LT~Y zG;)M^wQ<*{%gqmWU+Cu&h`Rjw!oI-$F>SmLUuf+46SSM>dJ`Iya9(52BezS}Pw%-w z4ZEkI7|h>bh?w)snV8M?%gL!6)$g`k-pE_Fc#@No8>lJYz)r$W6MlaVO|1TOv$wzk zX4soItJ69yKl9qG%+J}ox4Ff{G1Hxv_)QVe8~8lW>spWz+Sk2sNr=zg+GY$cr~Kjj zW8$~p&%BI!wi&wdZue;sarmyCX9w?5cy9dea9CF(`-_K+izN_Es z_L_%p=Z7B;i%J6$G=q5fnk00fJ3oATH4gXkj~k~>=s-_rF-$2gBUOq%Z9*xzHVa)R zWKm;A3D}4Gfp1;ViX`|6_Kp_`W0qIQLo58(e?OCd6sh^gjo-C(B{##KiNk@%u3U`6 zY3S23>xVx*vVQiWP;8xK>cLg$xv_Q_>{-kW@_kQ)sUDeGco0eQL-dZ93Ku4YryT`- zF<;@|1-jF%qkc|G2YTc8GvNnBrK5~}#o^2HldeyvXC8mr3o?%tPFuc~PjpmuQ8=i6 z4)k_;LGCwD)CVk5^ZdJd(U*U8eY{8cO);Lv13?`$mMJQEZ+#+A8uaDDl21O-Srw_k zz)r0K%ghD?D?Y!{w#IJ=tn!aTvh&cm83m_9$b-hPW1zPilM5D+<9;fI3P??mhX1(z z9?)sMb12QY|2XzysA$1vV`l27Ld!l|$S%;}U&6#?AFGbhj0xazo85qpAcs>6a_@Qm zBQ#@`B;m3U(q~SFQw~U-G zqi_UsZwsDYmV0mXU%~e&6{Ixhk*a}$-$138RSWw36t8fUzq;clVfAx}|Ab15QOHmy z{-TcyNP{`KU>UjCvn`u%TlUip{;@zF%pW)Iq~8cRNWE{W1KpTy@jZzCF|-JZGAt9E zaP*Y|HlW8XxsX-_dfwaDI|O{rMdF{KbsrR;|;`RF>-^e@+2tgj9+r!c(n}2O6HE0 zgK0-=enukLkTPAp-!$I}e2@?+F_ag=dg1RLbmPA9tnI znuvd1Ym!3Y)NN@|X~qlSaDUOyQ5=CRo&;&Iv%mYm+N?pK*(`pd|ISI$>DGaQtw()k zocVn^Gu;+4AuWe*7RHm>m5g68m94l^pS@I3n%iYo;^%ou(tEhV(`{-B6{E|QDml|z zP*(X=Tn9?sD=H0on{xmEG$9_RbkLiYW75^rV*>rqFvqYZ+18i!HS1}KhzIVoJf926 zNB89RHCacho;epWBT2$YMbL?z!~*j&C2_=zuSk-P4!Lh+^8ptWk-om!noFgVG+SOr znA8R{KjThhT{g;s!gMyqVu;hw+-+(wREu{oX$6?7l*5{ioWi@9Gwz6 zC1Y(j;H=Y0%u=Rnp``^7h!n zKv6OG8My0BHM<;ZD;M)5s6pc;v9%XB^tFRGxo2K|;zqZ7OT&}8d zl5mW7UYt5?O~IFfhcu{WZbA-{k78AiAIG}$Nkyx}l#Ch)46ma4>V@8Yfu0(z?=g^gGC0 zSDwgzDjfUqc0{KMW;ZAb{?o#S@ltd#^Nkf{MOXrEO*nsC2EJ^6ucJ>`9n!LK_TCS& z=w{kp(BQpu5iH)FlbzY83r)Gr9$|~L{H>mLMr>~J9RA_F5Outk5DVF4`15-*7u+@# z?VIZ&Riwu^7ycs4Gk5ti@q9K7uaDr7^>@sQ5!qGj%}Tc+A@Al2PfJe;H&Q&#yxvNV zVKs%?T$Y_%;0qM_QqbR%fFGM=7S!8vNt+)Qb+Pq$o{{xe{_O-FNYFkfy6bpSZSq@{ zPl4(U*D)Cr7Y}WIou~$BEbg4JY`=|@vvPM}YkZNQ(~dhke12>T_K}Kl=D`-JLtgOo zZ{+=j#?9Hc)|-vNc)`Z)Tol`9t$B*Fmse=_krAM#2tT2D-Je&=($kP!j46^Hl0}QE zi-=Me+OSacDpaLWVe9t`)O{fMiBBmulYdr{444BjB(9^-h82jERNq`S>|(|3lBIkg zkYWwgCC6cj;*Nd!aq3HK<9mAylCIwDErhwIHHJ#s)UgODgeTeS6F~|si^oO})QsTZ z#`jvRWa-R@tNtP#;;rogn>}Ar4~WA%bHw42#_k~&-?2Ze1I33+s_B1gKWc=qH1t1* zefs$F#otE`?b1lv$pSjl4b~oyFDIWa5(GUD%iYclF<*@+7hpZ_e#m{Wue`UKxU%>@ zLevHO&GRs_L7MA%`TfH20EwST$w~t5*+};nL5LHsQN7*1&2ODoU!gr>h$z!9jIK4; zFqTE;#njgu&M-o)_0b~BiZ^OL4eJ-r??**#Y(7gwSdYS*%l!pU8>lKEP|@%#tPtW>ghYeJ(q0(PHFY;wv22Mk-=G6B`-y^rVwrwR8Jul$0vLvea7%rYD>Z33`Z_xG#pzkuj5K~MJW9D@A2NJLiht9jSi1 zd|*thcv)XPUS@X1dSLv`C`p)@dVX|;c80MkLcdVC^-yfUC25w5ny<*Bd#w~P(np2P z!A2MPiZ@0GG2H5jXCu`YN~=^>NteR8=?%=zY64P{779EnvHS+T!2C~Z+>^lI26EqQ4o zH;`z#fi0@dOpw?PqI4bZ46&>SbLeDefz*U}E6RoTPi}HGpGm{TU*3|c%nJAHR#x4; z`Qh}PQ*X5;fLhCB=X7cNEuXYZ-bhOQAtMkk{Tt|X($5oChQS5Jl-rxJ`t}pq&-@Pb z>KueXxG<7IH@8s6-+|+){IPGL=J?Mm$yk?MP_XPaWpa}ywr|6jM8x3t_E4a%b*`J)WVN5-^@#|zh5Al}1}nw3Bik~Diu z^SjIgaTNZ$xmmee=K6P>dJ~Mec~dhIucelH)zYpt6&%RL9UU(T8OYlA$BhsRenJ9H zlKkdgsC*~yY&JwwDNEZoXlR0mQ5zKd$PlS|K= zQg|bmnK)scE*X#s0_n1qR)@6Iq+@i)52*XkEe^?mNHbnU7v7)7i$Saxqj8*J?cWDTG+|CJIOU%nk`N?JKSv>ZKcWX5M z-02uyvIo$2~iO4k=tu?KW+}#Ri;oKhY&C?vWpo1QtT|X zTh1%a%zo~CY~<+e_Q3%EDeTY@+3CqZ+Ddw49E}8NI*h6|Gxf}m4RIxiX)P=3wu`Jm z@C)c6DotS|hA`q6+?p&uR3ivF6D|#x_mzbEmxB-G)!S(X?epM4?jAWZc|MeTaL;G< z!43S7X-+|1dW_!L>^7Gm+k5y91O)!w#aWW{2aVWp|b z$+&$Ye)p|57PA?hDB-hdsh^UnP=g(`fzdfIYmSgkcK^m?bT%uuEUC|)mD|LHR-vY) z-R6WxqazIlE$veA1eKLO5_N-HE#@E)$x{dUnset-r~5`X-X6kF9a(+S4oy&Utx=I% zuwDI;9yv?;4G8n0b0XU8Z5F#KGZOmo#m}L;gSE|k4_uR{H zyCBWbR(f@H@ua0o!1XakO8h3{D9Gma&v;)iIlbwAMjD~gr@n6}iN7#@eJi9An{R0a zk3oKTX+NfPqU{LR^IntxTpq9DaE`rs(dwy)7b)x|(IKQlnRRXB0;(5KJD#gM?tEs@%p;)ZxtgZrY9Q_?}lkY4qu zkzE?>|0cX)pmYjiZ?Z+2Zth0Oq^&!+Ciu;DP|MSi7hQ$$>0}gpl*UvhHZ=#ZFxtVw zr$sjGyc1R{hi$({SGMF?dqro9t>1e^C!{1-bXYMa0+DBk^Cek`SrRN36S4WORq>I1 zf;ZW~X1W0th1_71juHjMWU3b5kvEq@8PueC5)LyObCjhbJ^>U)32NiU>|)T#I9z=8 zlfU1y6?%t3e(-1KPloYd45UjzvC>mU3DLDl8MWvP+x?<$IH|u*?AtFFxa)1hPHu4U zW1kd4ZFXH;Tipxd--(~|62h*>klJ$El!dAzqAs6c?9lYv&5`>h*Uvs396t#06JPcL zJuz8U8o}JyX|A&ULvmT~Hx{G!X>X;?o!ug%Aii1i(y|2|`fjrZ1Q~nolc%><*>$v* z4X?P}*wu1q#SDwKyxTiAA$U%@B%)u>ZUbqbfShiZWqc87{&5Z|p&%Uamh4|2N$Vc> zidNK`yWk$#udAjw^*eq8w?T^t^uipARMjpJkki-ST-@shv~(@~Y+`d_nPcEtehmkX zLp=XbIeAF?WR!$zlhGfkofdZnneVR0{}BCh*HBRCQWr5!#OAl>raLy@&zT}VDcptR z8rntOsk4oB9xEoDhAS6ZBizm?yP_+9yN9qGv`+rKl@zy%3Vb+fTfXZ#@BR7l0G^*$ z;jk{5n7Wp=zW7&K^P<12SfDrAPF<}pF;Z?XmlGQgg#-uT0i>#L`@F4CQbO8F~~zMD|ktr%1r|GDB$kzfVkv4I2GJ*2&9ybhzKi?;g-0|B93y|CB~LCMG0ipt=yJ!m2lx-AIqjw7wmjXVE8`bk(_7CezNbuON14 z{-osS(5BdR!`#)vEynHDW~+TI!6E@XPTh1KR!gpncn!LmsrbM&-`rVYzZA6eLCr?> z@Qc|R1_5GXs++~SVS^UUI|YqI@Df=k+O@U&itQvYrcI45yDKt3{L?Brn( zxN!UbwU5=@M=FLVhFFA`=5ob<-Ec2L=bqT5A=k6$llE!{LNQ|TK_0#yl4mwL?|s9^Lx)f;P+bqt=drTBy6?M7XQc}vZxj`*?yDT|X~qm}dgVlw(~ zjw6OGMCbZy7V5qccFUZ6tPmY8pwKcOv3~O&W0SSD>IvAB;~g?&Of{nX2Aba4uU(+h zw13S@gjX3{gwMW75He2ZAXrA)r%WkI###FD$JT_Uu&FRT9Jfq zscHzdp$+zQl@;kKnR>R!$y7^q^GTzdT?t@JYQXze#N@?<g8=1=a_YLb4m`;kEd z{-w;% z^LP`u$|U?3{~B{!L;5%UIXbL4r#Oe{nx|iPn@wSn)@?~P{f8A=sLh4ViSm-Rw45XO zXH77h{$cKx;f}jeq~Jr*{NB)uX<;iRh-aQCe$Vfhizl$y_yEEtfhJZtd*;~M=6z`b zUDj;0$2P6}f-YJrcHV_G9)r1fME4@x^TNCXO2wjljWvH<#-WO6z#S)%cK2Yf`g&7r zZSn3SDk`|N!mfc5f+GFp!__83>^*)5IEA2>jM2Gnq@-Q&X1mR@Y%g)RVS4 zHu+VhW;`XZ&mWK%jy2{{tlL`oiPQflz22SuH*4gNFL5)CmSr z#7T@W-3^IQUqtviFl>BTb=`2V&!SAF-y&2oNbzerw2J*~qr2F%AL}4{`IW#g> zflEjRBeZr<(WB3j+DHI^L1w>pI{CvHlR<5T`KRp5$;XhTAhdOp9`); z_^$qTF}uEpXBoCRDnoh{oF*=o;`VCYrf_%Y-pTh3x;ATJ35+0_0E3#fWg(lns?}O+ zJA{(t8~NNJAr@Pmm!wk-b;Eu16fIC2jWtjeh%57(vH|}Y-!3@f$%5@E+zRB*ss|wE zLbXEl`?Wz2b-!lY;fYbr5px80LH#fs6A(DN>^N+&5^%plJwOJ9_wy-=>M1r+Pujxw z^w!!t>q5anWqZ+7!8Mw_#j#2khP>t-eaS5|`T?Kli- zl2&b>Ss3Af-KW{xqX*;Jj^ANFfsa>c+~P!AtYPrH3}I`Gn;L>7#2oV?Yg?ff!K)km zyJ-p75sZLlM2tlDhvv0#%VLLa&4EwwKq+YPt%Az$r&hab32|~v;ObUgy`{KlVBa(l zEPUY@%*RlAeae8=0Ce-Zk?Pu-X>O%?u*zOFXFX_$g~ORQ_bD~(k>IWYeDTV1OMLE& zvyg+36tfw-;flg|bgB9J^dXa!{dsaPJkbU1y*+LaU8@t)aLY%zL7#Z*TH44xx;Gqa zpXx#b495J|bKu-+r|5Dja{T}<8`y7YWG*w5hjBeZnA2fS9C(^CoL6F(5~-GYacfg# zNK=PA;QoDYrqhaVyky?8dv`}iz+45LQVVGwEL-ou76QS_!Slrf2Hb&RNcaDRm_G?o zc5FPvzrGkirN`={?ISnhp+1!6T3b)om7z8_wz^o0K(~E!j7hGIXP?s4;3u`F#AVCk z$8D#~aK$&R-mK@Wdr|?!{ks(Z7DcyfU_b&@IW=D^^+h}O0;#`p9^9^x&j+DAoyws( zDr$=s)M$Yfmt$H+x5f-;gaBr4gW1P|ysx?!X8O!qE>=T}d5II;6)HZ+03km!JFvPX zJo~e#%Zg()wTL&{FT}PwxoCt>A-mpW&AytvP#Y|0EM+)**AzmDz|U+_#yz&Q1Os%a z0r!guVyTF$M$_oQw4tkDm$W%|g9@=r7h~gjAoH%WuC+XK_CP6YLW0+ziNAr&VPN8_ zx$r^IlYlmrcH^A^9nc>GSho%rcMa4E!Bpg5$b~gmr@J10UAcOYw<#nm4UAQjH*7>* zGqD51q)B}R=m<|o+trG)VxQ%U3FS5+f6)O)1F(LF`Et76IGn9 zQ03WDK?}-sz{BO@&lPv)yK^wRdtZ1a&2NeunN}1(2-O2$UuF>frl|X6^kTLax3=c5 zH_zDhyqb@81Y#z9&Ag2KmA}Y*t)lo!c71>+BdM0@=RirDV>{x1PzX%}6IMJDcj6`C zv(-;$89GdR^>ib)`_S}VGF!ywJbFRqZdfajB5)(4yM3bpuS{jam+-(~E1k$Pm;SR6 z80Iud19KWs*Vi?<&-;EN%I`!Ac=OHw_R4#3#y%BZaCgJz=_jaezl3oXsVHk*+XXd! z8$Z6<%=kw2wp|bIw)sqyqmK2B#wLj%&DT=k5qaj-AF@SLcwhQOnt^{W3+pi|KW=fa zg2yX3g&7S@eD;w_o4($VgPK<8o^hO7iwqXVbZiVM1a_Iqq~~Fbd)X8#uhmbKJSY*s zgO{hhlnz4rhUJ*G4^zPKVcTTSBn#KdESv#L7@G zV7L(2y|SWL>wqEnhh2LjSYh<4W_!ho8=hHSNkhXUo4*huqvd3T&$1%?AD{~-e-|Ed zinR(jCw|*fd$Q$0=KOV;%j=Yv+O>VIU~1T$4bM#h2Rb+*MLSkoZ;BWyG-ey*cUsdv(T=B2tl zOGN~^6tdotUw*`nsdi~Dno_=oS5e;-O<**aesg5w>tJXdXb;{kJa6(zyX%ABK#4aa zXZ#R_Cpaei@0eVeb~OIy`0h$(yN3 zWYp(-pvE3U)Cqye0FOVaoyySdV;fVBTcn=4KwB4)wFltRtu;XG;4V$LV;N^915`%O>EE^o!__Fw`!9AX*sferm)xXMq8p8oTFl_D2@J zhhik-pBK|o&q*Ww`WQ$2+N8AsQ93KnqcKnNav9E(+-d@b@0ifTDc_)tW-=4>^YJ)8 z{)zMmRj|0Rd=~dL*j$Z0G{eKBJkOKP&|zWymQSsA$VrRyj&u4F+07t~HQQK;ZO$4~ z6A0g2eI?*3ZS`1j$~`#EFAmpy+hsl;H|O@mvCnhB1oZssd*Xs0Y%`2g=pEiJ^c?Ct z{?h5^>5GUTD7vy;@WsF4;j3=6j8M$Z(;V-~tuMP>kQ6>7AX;EPstQUk+zZt4ODp+A zxN2&j0I>0It9RzG(dy?~LeRKQGo06>lMkWYRX4IKqy6$Si0#?bW@J zjxRQ+T-*rPt@el**=@V{z0FG=^mgymf_dbS-?|IcwS5to%nt61yc2f>hnKiOej&oBII9%?FBw>&#+C`_#YYmB55!r z;PTA#C6+o+Ed%Cq+4TMztH)X>V`GCA4uL|!Z@p*ylW^oRj(i|+Y(4Ds%c$Pp)z1Oc zkBrO{D9vj49AI4(akw2hMWQZa^=0w@vN=XP)=4LD+l9 z$RT(1O!7c({-d*0_kxV_1v_Hvwtn}Iam+nsNEtkf`G@ghW~U2PHnp=R4sxh7r5p+W z`?6)C1)-|DGJn!%G)C^5QwEFVftbyJ{(tgnatuXXa83Vy2Mvtql{I&CMQ`i+KfNLg z0YqLFP%hU5bcx&ca2XnR%3oIJAA3}?GwRE%p8{;0JEGg0YbH8}1~U_W5`CCi<5oKr z>NZ#BlqKcqckoo)VF87KEDk9O_zb2-_)o21gpQRI{a21K$GXd&W~GR-JZQIV+NqtV zpZF`yWsbwV?uIeYz?!NISZULlmsC0C^Lx(p{W-c5Ii9kv%{tQAhqj-8ez$W$T8ITu zlaw#)m?`||0uU!{;&A!3p}$B$Y>8D)^f&y#Ul{3eHn%I*z+dB|7F|2H4WMK_1}ItidFJ_BmSU2SeDDW=mi1W4 zv_RVK_C(HUlCmjpiPP;YDOiya8ayv$H&q<*7%gRaw3#AGvNkLAF|#!*jr+MEumR#L2`FpE-tQmm>OFzYY#Dm? zXOihiZ~EUo{YA2z_yy=;z_Izr5oHzo*x-f#Ycf?c0%NlJNEpNBq5^QXe3>9Ir(K*xeeUnLsjPA9G+Ioc64`&7Br@|UT8=?E_MQ6 zW9uRsQ~>tsK42xlRzLQSaex7g{vzI_{s(%Wee?$?y}3U{ByNi^(Se4mDgvIFZ7v79 z2j1vQ{Umn0g7$jYsA%gPKv4Tf%*99{$L#Z7cg4zZ@~kSoi>~Q=UHp749RAELvVIIa zbVqeN`N(m^W$T5iYDe(lk+v>_hlbr6T(bMO{yhDUq~FhM7FkQj*GJ4Ax3(j%#r^b( z;%Z3(AtouMaii&2KDHSupi z(o?r18H4-UQCvigg>U{_JjK#=P`WuMwWA)uP zexI7{AHUBFWLD~Z*EpOs^C6%?hLS|5tin{raN*bd3rv*kFLyEomA#DS`-?;@$fibs zln3{-`!5%$G$46_%^fNtW=n0`G9m>W$LAK>bZswl`hI`&B1h{j`{%#3xuWjXdO^{P zo>klhI3f9Bh|o}r4xN4eWmH~+Fl!LtA!z6?dxhi{?d#TgY*0aJNNr{FR{q+F46HTL zV?ES@Pr3KLHvNsMt37Snv)E_#X#zGw?&SD6KpEU*k!d8}k6uO`*9_*+9#vQRci(Vr zNIl7}PyLJCn7VT8->r%24G|(OgbV31LWJs`1|4r1iZ6E4!#MThw9B1}(vhI&ex25N z!j8*ta*0-|Ttnk-=e8Xt8PC}=x`6)+tNH&KDm}!A>aWUP^d8szJ}CGeW(0?Gx+7_;$zCZ_Z{gEOmc78qa1ueph-(V zDVN?bDCed#FRMRHOuf~o%*;sp<=W{ z!*(R6;0#)CNZ1!_1C(ig~L3GGqi}I$wI<0B; z!Kpkp^^^XAwxGnUVI8P|KSl398kp1jjg)T_7f!0}d*1sFSldHNPX-ROMx@o7uShc7 z^D;7ee!5mZtIOXy0t-$a&CBf>ESs_UdoP6VAjPfIdHHgCm#rj~+x`$bG7EVMkzcsL zaCF_Tdee99WF_ov&y{gdcT@kd1)aw-#GAj?9hC8E&5G$S%Jh02F}@2ROUWsrKcI2u zQK^XUnT(;)G;rG8G!tk6N^NPS&i{JBbW03@Q?X_ZDL#la5^<+95jglZOpr|6@T5=pMtWAdGJ-*yjbc%*3e}6 zvMDf{9nlYkD7z~-^A<`Xy6t9-sPDHmKTssb{N?lW%0<@K%?<&SywX&TpCsV+Z3!~_ zgjU5A=eq=StRDNc4SKpUMfWk3rW#CGN+c<`mghe9NZOo$ zo>3rsU@c8z-9!r{EiL(ThbqjNu!ns$cm4r^=anXy@5^vJqu+y7Yq zwam-Xn;t5<#4-;ADZ*D?X>A<{z4t^r|EdD-L?eooUz<4IV^Xi7u#7C|(eS(C;^%kQ zmpY6;wA~%95pIdcRp^KIsUJEi`6_4g7m$BzxISsz)o44`Wm25y4sHPd zEQ1mwT&J#KBD@DKf8wyNLFNWPfC0R!075}9;IA&(@^K$icK865b&P4_2TT2dS(#b*qd43X`L=6+PpaNn;*o3pdx!k3 zfd;zR2uX=(2JZFdyV(A3#r2l@3eb*ahgPh>Mfm5nOzSSba*qQIn$^SPqEi zWba7-8|^jJ!M7QzV@sh_Ru&8@+o1B5d`|a`-%AW35zaFY4_B)-~_JBloeZKd0!n|K?a4Gil{tG`f$s4hD zDnIm=6D+p?36G`&UL+jXzqC`ON7*h5T9@43UZwz`h^BH_x*O~rl@ zMFTv~IRU{ZD*gOb_QZZW^J}$xw*OiK-QynCh6Zt*R{?ZySMI7BTq6!=|zj&bY($Mc2-%|qpj|}Uuwj*o7C4Oy6js2X4y2vcDlZn#<6t-KwRx5Qv zRRjKF|Mr52aD=x^0I-cbW0=5ROAya>JhB`-)O%YoPgB!nN>?PGFZJH%qP zgNtvw=Q5$Z;?_@oxYHS)CILGxAGqxGNnH@U4=~`ZJe~&&jdqM=t8t39U#}zRrRzW; zhpv>=TUOa*uT%c6v&B{;MMV(T8Vb<(#qT5o)TY2HnXf$B5KDG|?5n7#uo?T@Ab*>+ z3zRPnrJygm&B8~;wkxmq^=13A>{5QJ0IIG<<7##@nMKmohLop&u2l;$<75z?%Dyux zz|>rGRqFen`$Xc?=mDP)Ub*?SEOq7cD@s*XSQI6D;>B{=EzT+tJbW5;Un>FO z7xYU0+2ZFTQTHjPvQJ#GScf1U)}Vl9!+~C-&e_p`t(&Xe{Cu~%CO(b7ScYverQHV8 z+V{Ul_1adnyA-lP&rKl?Ar?~|K^dH4?N=vy0pN)XSNB#Id?ThJxf~s|wu9D;G8h=GEBDK&7=QyM@CiHg z>X`K2L=#GC*A~6M)!g!fRqB%Yy}4=G>X*f9ljGS91Eqb33E2;-3aqL%kpmM={WTZE z)l50wYDeQ}uc2)Bw=w_IybqnCb2&YZNwSUTE5}$lkIqr~_6bXmj%f z3s*_JW|g*q9xhg5B!$BG%%r}kQ|W2x$ka9b^u$oov?XQG%(CqwJic12fIZlb)_d)L z7gry^+2xICRNY=@(IJpVE&vQQIkvM(&l5cEsV*e%`T6BY zWP7#AcQo+)Ve*hO)QSI#h3Fi$ZzRUxgFbgn@1#;Di`bKZn>bvd!aQj~L3+*z1v)Pi z*2>zy_n1|WB~*I}00(RqAnt3_U8$(=2{idPvdp#n(BVbzqpzr~;T4i1Q1Pcq4Fqu5 zp_jz3e(3;*kRGQcj54obwB1OUK41a?!0U#xXp(p(>7vLYTOBA|dR_ujD}mJX<wL)Lk%^>GNhlW0sU5=)gpSF=StUaBlL z#WKEI%xmgEt6PiPzZ@pEjr1TKD~GLToXMGqyWYkF=+TomfaY@#d%}Ju>gT3!SvF<1 zXQ!&K%n0KP>cR5Q(ysWp_6@@YaXh#&pPyML2otd8pTZkaDT1#_h9yLB0z_V>u>a&*m z0wR50Zv*n<1CEaD)1x?{578Q?g0~$?^YRSC>#sysSvy4)B z*^NJks(2vyuJZdKGY5JdT~D3}yF9?7Kp)dei|Gh>B(N%ywq<&$M8RqG^%2D%QR#sJ zf{4i?UAM6D5su^a=Hh~Xp&f_Zt`f_CZ5&{r1)o=Y*D(eF(U{!a+ zGDm&bpLmzh$;-B^L~p6qru<72kaeb?4mog6Dp}B=vzQsm*4-kei;+}C=gJxJFDK~D z5e*#)JLfhE;)r$n>%?CtY!^biTz%A6F^flHJCfZq5H9`C@Xn945S4yeHaH!Uz9 zio1MO-gWauF)-yhdlj)YSlOud<^e^?($Z3PvjIbU9vB%GIpADYM#l|x*#4m14D_FK z>HkvY&%awEf2lu@_#{pDqNCG<(e?{%wwd(-kM0@0d-9riy|6#H>}~3c9);S>=pJMH z`rg&^(9g^`oPASwL#DQ$l>%^w0-w``N(zOcC;qZ6SvoqfSXD=LKQ|XuRPvn^Trv`+ ztCe;>O>tfd@8R;9x!gjA7n4@abB(oe$-ci9aY^Bl`vp%#I66tnU%%?lwSgkl~rNx!`0* zc(*`P27S#L1i%D;Oerw9Nx3wo$1h8_;}qj3Zex3hpeMLP&Ra(nE_`jjwZ_h7<-x_Dn{6&rznvhmX+~X*n~%o|D&; zTXlWz&Q3)cDu`T2!`nG6`9hHA$N4_Q=a*&0Y}BmQPcahQU-g#N_Yg3eC893bwQQpq zdcS#zF4tFNLuo)(xoc$H4Sg|H` zVdPo2d5e@xcB{LL&geP)0|zYI7RSXjQ}=<)O)lt=4eZ-u(}et9LlHeq56qHyhfPJ& zBl8s7KeO&B{nN1=sQ`@zf);Dri#9-q-GNASO!-Pzdhk!0d)H%&&cFgNVD0qvTz;`PhR#rmbcMkYH`__iZu(%$ab0pLYwJ&Fg|lst zBx?tMe(P4QB9#_NZ6zIvbxwW5Yr*<%Y~O6p6&@!RN}ITYpC0Kxh*4A9@|a@P#!$*d zYOX@dMX1stDn^Z7lMDxdRv+AKuQ_@Kb3|SCK^A+3*dkx-(H6@naX%7UQ#jCPd?&-n za`ScaH9GF|c!#1<%_YWBUt8DFXe-C#N6puYjG{6- z;J*3z)G$}hVc0fY!FljJKny?p9;H7Bu&(u1Z4y=1>eX!rmlCcQS?nK9Yp2p;T0n!h zfsNpUA)dLY=-}bI z14edX=#)y*mwe`&*ha_62ZkyvN^0llGDoQTz%+Y<+GRg+=ZV4DTxHwN z_o7x)4}?Z`T7=a{p>+&jLtxO}Rp9UV1g`n?9x^q(VHS~qMb@m3Z+6qlGGV09Xm7O)l?NU{(^MXb?fV0GbL1>lY9GAZDy=> zha;(1zT0L!Pc(?X_8_rYdP4Am7NJB_O;G)wn)|>Leu8g!tN*I(>StRqPfSJ73x(`A z(h$mYpVx)CSeBqzTIF)X4fxPP@{*9p8^NWVt&JA~;Ou%mfxitbQm@qX(t!;AEtm)U zny0UmVr-9lrTf*W#A=uhu%~iLe9CY=vOT-yWFV4GGn3 zG6|`KCmz~LkRCPlQmX*$_W9-#^m#FgZDYu?GhlVQVI!AjrSGukMl8R)Dwp8R^k+t^ z)>L5yJzDtPq9on0(_;21)mVhrkTtblZmqd}`1YJVOAsvQZ>3j!FJvZ`vhmEMs9vvP z590*m1sYd7@u|#7{$8Y*YUZ3hH?CwtRr-_+MC^EDJT-!Kvv7M{@jUqlA(cFXskBB29a_(hs-in8#(+i|FF#aRr?a4i|y=sLQjLN6)_6JMH z&dMljt7WmicxEmdfjuf=b@{~Rka1$m)!NoE2z7E%KSnMKGvInTQwKT>S>F&nQJ>kB zN0Jz-WtZ++FQqkGI|gwO7RT4XwA9)sd%U`lDece<4av350_%}(i!8RYjnD2aObWO$h-BB)GX|Ae38*Z3U-j&dfnict zl}C`1h@_P0f#Juw>_AN+?QJuU z_dcHT7)`qBWk8<$yj-C0qA-x*PK{M%5mWrNO6&D-9z|fSbQtQqy%qA!I%$&6){hSF zEM9K*KggmB5%l{25&|ESRf2)V9g{hA(OO|T^PK@k7bXmbO-iTf&MM4$UI50|vTZ$@ zTUMv?ff_v^F9{ej0gwO*;4W-Red!+&1t&&1HKb|OV zg&~WHEEc*Dmr!c{xqWh^@kHj>0REku$IKa#muUUiTyKTQ!r*&mNl*$imQA*en0LSJ zJSOLB-!u|bh@Cy>k0@bpB(Qj-;aGJkwV|u%W9%kV?SDe=L8a|Y;A%`-9k*d~N}zDu z&Bpdb8Ig8B!}j$dZAQW(SIe^Q!J$30319o>)ox{*skK$NAt1)=r7x0Yyhm~|&Gzkz z`fD{x>f*i=Z2?B@2t)fEpgrkps`7r-GSRMhx8?_SZo+aUg3SE#gum>Pc{vwKc;Tp4cK}?!+~@$ zxLedEP?!7+o&>z3?(EDzbHK`ia?#t&E+8xb`leFEbwv^SZpMm~G>^O$68fsyBt!rz z>*K1p@I8w*7Ci2&HE(87MP>9B_Z0XkQXjc>*FlzAGs;9=2n!AN04(RpN(4XaNU@@L z=>>=aH7G48+cKI5yH)ZoMU;HZyEBF8AtP{7YG!p%^h$dF{&-xc5py>_y%JZwHXwS! zX}d($P;)|qi+(GLX|-5M7F4C<=16;is!QN1)Ww2aQgaWvd0YRh8w88LJMTWzD-$i2 zhPzwyB_Mkog-5(i-ijWPEB6S=P1x*wpg74mcdkR(3!iXV**lpodqZx6Scf=w+Yazq_)P9}3RKWx#nnGwTM78YEU6U9N! z**vW8%Eab42f4tF*3`px*RwdzWqJiPk-VS4R^RcOrS!N%O2m360 z;#s!2QthV5JU+wShoHLGxq~e;H#&@A%|0sje1aO{Ue(~OUMsEo^fA|7%OR7lRcFJV zqXiI6KB;bJ0mWiYk9;dnz8Q1oLj0{QwZwP8!9wrgfY2STG&+-JkHeLf*w)R|Wj+C5 z)ChELVuq&@F!;*l(gT})Z#qi_J>w!av(NckwE4A!WEE)?(|^sWz!42JUZm*_bBc*U_1(1 zjSJy{SSq-Mtyj%w7s<+jEArJo@Rry2{C1xe0OZ&}9F5u45FdxD$R9bT;E>W4@KPo+ zxlt{6PGNme5KAy(|Avq+ub<6lduGJpo(@>Zt^+USNSc$UufoWNUTV|>zCM};GuLSC z$l?u5C8?QHZ%wEV{~$?V%5RNaX4O~EH_E-nTW-gGlB?{Vea4jB@D&BBM1G8O-8s~_PlDOw7x`8$s~?a-352o7 z{2Z;Ht91Jp7YuftkUXWmk)GyWGWVw=fc0po+1AvnuD0q1el4lrdyij-EHxb$Ve6I9 zEfLB);Adjr9f8Rj1<0sY%4%JRwCbv< z2G7>y#K*KOWI?sbh!OycMHwVW!FQE%{RFQ-Z?6Kpk%QKb9Kd}#Mu+UCz7OiOFP4`B zZBLs}24g!u*ZvDy5AcEE>EkwDAa!R;52x+RZPzx0uNC5GpfbGw!cO-OATECcL@khd zex9BMXD?>|a+N=C`}}n*UOmeXY|P9UKgmv%-SJnbc{xtBjHGz$uHjhzMXz6-vi2U$=(tMBhD@1;c1$gde z4ycwb05N;A^^Y5UY>b3Df2_fD=I=iK?%^8IDEFfIz*UA={%r$cc0ahLVgxxVh1p#VQWR<1fsq3uNZ`-kSK zl@n$6bD@wHO%dT1KCVNd{kL=ODGL+tVe?*M83YQF=@httwlJUE7ONJc667{%yu)m|ve(sa}kQd#{FOw*TV|-rO zQ5TrYsr5NzGDY>mA4HGZFn7Y`UDh`D{2nZ`a_H38yi-Hdj%&WCS^7 z+b5~7I8Ej z5dNaxeLP$05iMArNV^@G6jGv_?~^1a1^H~-)MSDrh?Cd)j`9f~c6<`!)^s+{+hZV} zkEKi17K8WPtrxrHna1gZ7oW#}=u2E9&Rs?uVWaR(w{y(Vwn49*NGAhQqR>2s0dLW$8q`u!bO>mBmfl?uv}1V5j;ky=wTM;&)G_rjy~C&BI22w6fvuC*|oT`l&{ud-E;^jK;a6{p`A<4!C z-964;|FG4EACacaA@KB}wU8ik!uHQYkreF7+ypg2r0T8cS~fCg-@#90Jp?}@DMx*A zuU92~-$v*1YOQ`LhbtT7>Vv&TlOvCow&s~Qb!N3pxOgQq-eap_)A~7xzX+GiyXFjD z&m{S5IWPN3dab)-KR?tLlHE`l3p7Sf9K7f$m2cJUmu_)`%FkiHCV|XKuY12osYgHU zSKmj`NJH;lN2D$ALp&(6F|}A?4u~WRhtDnPCd5GywPXi{$cqPg2{^~`nYIN9H7`G% zGPjM!y?6EA@u@qv5@o6|HzI+i&6iI$X20KuZTbB)U4PX9k8it3FA2)tzXp!^x5JAL zX0p!!)s!tPV31IW8Zt~yE2!K@n_!dDw%d_^f!G(s84{3pgB+k#^_2Tnl`;zv^`udB zbjiMH^yE;!Na`3Hc?}+IQ1=2)K2&;Y#$No*p`j=9mw3VP*JnTOL7`>OlzyI)q5+9N z`$8buoSBiv$V(dUWr{`ZcVvmWC1*vWX_g-WBM`>LQotTRP#!#Svl(PvU*`-PW+Pt% zIUCdCt#MnMI8*j%rBt?w|310|V%C=eEOJT(xx*KlDonzpOZOsP*?hS-?wU|H-h^0e zuI|S>wVT)&gzHjm4ub5Ziyi-X$3=9|j_bmqpO1cx51-rE@VpdJnN@Y7sI@4)RRs{3 zSPZDnE4bXFDu#f)TYK@_Rgp5wUFlHGMg}IN3P~TV1EO3WQ2+$d@5)VonQ_1j;_7jW zFCk-1e_72pf;rUxY9M8AXyTJ4om#04vx3Z{x3uETo;JLg%@6@fUM2@lEjnxA4M}t5N{uSpmB(m9`f=| zHdwBgc)e!X=M*GYPH~;-!ulQc&N7%^=+2$kU3>YvK}-b}#`w1(18P&je0;)p<{OJu z$fIPEsr&IoPs-eAZZ+d7{r6jBxWCs_7wW#b=-0U6`Ie=_VdM_ol ze8s}UBi<*;xjE@3C8a88DEzg*Kg#fw0`Z-sxNYmJ56uk-)2SF2O~(U$e!i=;nU$jM zI%+j9tGMrjql2SZ?<>9XLx(zl^YhIqKg;KLq(lmy?J?D=WziA_$t5dZhcoZ6jnA3b z#$zyWfto*P{M7f>^*3v4Al|e+6_eiRJ-`EqL)mlQlf=FVx|*zNyIK^Rm+@J21U@!a ze4xwLe>m2sMfG)Z#JncSaM0Cf+4^zl+KSbv_k0`htIAgGN1vTZ!(***y@r;n6ApQU zwE`AzCW)l{zGX~_@5qbSkEjP4&Ye6RINax~uF+oRG5F-QnXbySU&aR#og1D6vvR#P z5jc)wUhkjJ6c?EPBGVW>3aj?x>3*FA1ceY1W+0K#y3oDtr7kNRo??;Nklxl=u<$8} z*YotVCmHJ~hUb7=IjiOPm}hKZM!@vUr1AyZB?ee#0uq7yf-rcoa*=4g%w5#D$j8Ni z?)dptcVyx(c!M=tv;XU|e+n|jyBqM6G9HY@0Sl*i*prbwZnCKu52whkqF5=*N14%* z(4qr<9pHNvWA%nmOM8I7JL!sjS{cLo>fq&OBFm2mBP${;+wtuO(3LDwt-8H`Lpc#zyvG@tvsU*?wwkpttV#%xc zaw4B8sho&8BY65!swql`s?z%I=uz>BI$?kNIg^6ocTZVGcZsUT6!;|PR?$8Vk%lTH z8uvaVcj9_Z*eWbt#>lUisPKrN!s|^iQsa3uTR;X`u-sCDf4V3V<)^jt!2X5&j{~7Y zvFz$em5^YjS^O{4+z-hn;>MQ=&So@Ymya2Ufk{>i!@jc-j(9!G(k zg8z7{38w)`gV=KwXW%$hnr6y=Q3;R*{%wU!v$?%{z*qOcZ_U|`5ZqwgrhFjPM|GFr z-=4%MxDz*Q?XL%6g|hL*>+Ju&7eT(rDb^pR2loM;8W-M(C`3*7_0F`*PLa6Eiertv zDqek{U5j5#-5>>Z&8bhMusp8We6#*=gc@mp^a2u@N#-e;?cM3BqgYgVbdB6J)0X8n z*Q-uUTbSMY%kaP6H>s7-S61i@NG2xNux>dAf-uo$cx?G|_RD?q6o23R_;=?b&IwI) z?7zL#yWgu0^x$|Rf}jOkkC+&AHto3DQ2IgKAm7qHCs>#Tiyzyu6zVX3EOkx+&~rLZ z98g$($_zRC-`3@G?omu7y}Y9Qkl{NzRGZf_S-z{C<|npJny+GW1M$om671TJCIw-m zoH6H;-}*T^QK8!&HX_L#JN<{rSoj-8&YXBHgZI5u$0Sc*=UMjy{lmK=qk&w-9+*GY z+qwFt*Xk50+=R8L1w{|_(q@u}Udyb;^vL*xlwFHCR04yiV)gpG^z`&{yd&d}98->j z9woVPdO2bGgqIVqA4|FlS@1@}Zv`y|r*a!6$}3q-yeD6Us$g|~Py&(^#mF!*B| z%S7Xs30;i9M@!aXq@B%`t*lBepSVA#HrF@MLs&4iHTX8__DmD(MI~iSp-g~lf0@u} zY6FjphemxsklO0d7^xpOB2%@u{#`wJSI6mRn3g9ly z%sWi68NbmZH9X++GAv{G%a}EXg}q%J_5Giu0W=qF%bJT??jg0XIS-VJ3&tDaQ>teoA?NOg zh-DC+{L~E}()u1;CDX*AkNVtak4n!?vk&!^&B3Eqx{MZvSnM|q_Og-K>@W#4L>jol zc09?AX~Q?-*0o80iw869%KebZ>DDk)P>kxhl)365^wZXipD?kufr`TCfq!9EZWtHgpSIextm@+E#A&eva2ec}!;9wf4?NjR0knp`sU+S2&kG z8)cm98~&J_Uzwdr-YH#v;D1Jc=zG!dhBD%(zU7Pg4iGM2Y)oiS$5fP{oDrO_WLmUX zuf-m;W&KN20!eWt8#8eFTheIoT%dwAl+v)G<4`t_&`21YO_qOP21fk#vWf8=iUxT) zzf>^dQn1+CnFTWM6D_h$cnz)6$5!vaUyRq0bC?fv9&rY#`HYozS0a~H`IwzfEXT?S z`}(62*V4TO4J>M&_DWVNwEEF4er3P8tM-#4=A=&(v6_vg-b)WZIFrkHpgAc1x!Tn= z6*Q~M*8_MBWSbHtPdoW@-}MyK16d5$XJ4-OV#LH&7)E%FqD;8$dVub?+w4_v<1t^^ zr}zy{Qx5Ig4!Yecla?uZ$&U5>+2tbQN&26q!)1Q|s~U0$UB09tZj+Wa^wGCmP6U5H zH+%iHDC4T-gRvp6!qaVDjbmjfBluL^1^ST(e9lUPad;(?7F_CfYt{w9^knRis72=X z@s`F%2l5!|2UMWQ`MfH_Gc*xxs4t2DG<~CWr#LdO6QNcyd;|f+VsH7f%p7Wr@!q0X zjX=_(ITK08gnf8dyC_on<-E>~=Lojr{<&mJK)#xXy)A}$D-NDnn0+p}+R{4gnq0_9 zSYG`w=>Y3Am5SapXRC z*%{vk;?3*GazhnU%c}qs`g;tNjalrONbJVv>+D<8*?nn;Xo`fcau5Y4B{A#;rlUSUs`fOuF!X(>(6;vtLGc0|EZ#Q*|ve z{h^m8b2F8mXtUBsc_u=K&(D6{hfJ{eyRQNhft6)?;G50C6lb?jkrw~B2y<$ka2|i? z!X8-Hz*H-yj{&t}bj??VV&YK{f7!>sfAXY1|K-Sc#M1I{xy6W)H;0xNm2lA0FIx zb0cZC6XYNjTX`hyJ11%6_9=Z^P?~?D^|pkRO4aB9n%~kz(50lPOba$x-|60&7uuS8 z+VZyN=$tFhAHK0YyE2)XBvDGT`sF*{0GN}~5ut2ug(Y5$o6>gcZ|+iI*Y;x`SmIH_ z=G6J&L*$8-{P3Z1WcFzkV>z|N)yJ>XiVxmrn(gINnCb2F^azJs3U*GKfn0LYYRu=@ zz8&HI+wXH-#Ux6SrsQc?GyjDC&Y~AlP-F35SXf#wjEP0Jt#Va?p0_&3P2$M|MLcIZ zII1V}YClzX0R0e_GeJLivS24W2_1k;rs57fj-|_JHu$;(io`OqSLQ2Wd9$w*2Pp~e zK9|-!KmHnZ4t1rm`1Yr+Qmju%_yxL;NWAa#;}{=DzRo4RYjtY^p zMXxFSxItH`XI)=NIOvYr+A@-Uk8mKl;nlv35vPDB7n-HTEgjbmw|Q3wASb}9Feq(3x3E7+7`PDUs#FR4Uj1dq&e_zBv-dL-y9Xu#+JeN)z z0|nSEeguf;UGLmd<%s6t9dz0D;t7LxRV=kfYm&K2o-IKNm9*pzjEN!#%J1qvLr1%o znFFYOfM_+9A`ec)^X&gJKZwWZOCe8V>Cy(GUUOzqIJfy_JyK@=~55R1gK2XYocG5CNjb#cSL(JO-wm4Fdm+cSLbg;Swd3WDGq!|SQ0 z{d(y-Fsi{+By^jiA7J_9fO2()y?Ydf9&JiE`P~vJ$$I3X{yc>E;WFZ zX1E2APOmSzT^g$O%C^%-+ghP3Z(FqlT2Ze)V;=SiN&(K%_h%m?CXb}_5Y&Lmy&Lz) zd$Hn9=kx*}(6TdOKWw4Vc{aDPM+Qzq^M1IXEobx>lzQ5=U3LEiH}f%Fs%vE3g|Hn8 z;9wFOlx~qc?ab~=5SaioR^nWKN$--H5?N2CB->|@16PchZk5Yp);;b z1c{X&3~F9q8-K5Ln>P1*+j|C_7d3X?!@Ak1%7V!mT?2HuoAASMDL@nr>NfVBfWD>) zaZr2#ceHA_wOZeevhHq2fL-O|>5YV~72j~HiaS|S)=Jyy{hf{a-FgB^fT)x#DCDeI z=`!X~@#-HPPO8v+XSibe_VniUuGMQM*WCIux(RvOL3(uedRJ+GNS0NPbVKiH7pjgU zExS@x_-=&-mXSYcq}(mlxB5zdd`ukI6H%z9iMEZehVf~Ledo@7haO-f@k&;M?a{(yeC8X_@#X&B1Z~Z+>9KbA9dS#6 z8|xVcc12+)d2xmz{FI7zXnr!HYg}JSLO1?WkiJ*c(5cC05@1?v|Mx~|DfKXTJq<;V zdTKpx*=fS@G{Kb+oShL!Je0Sm58KX>dJA|#go(maZ6x6bR-?M&?0P}^!BEc%WH*tG z98uS=xyYP8UM>@LbdcwF{?&J|FOY^vck}=qOtEEy8Wh?=rP!rPd={PRZ}l4#-m0&> z;_&ak1!7V@eT!yfA3R7lN^g!HbHdr1=<)}G-_B(^; zJFFNZh%8L<{xl>tPE7|AHj^_oAwX#;ARUv7h$SXDxyK*&itllf0Quo;_5IZL?0oEj zS2($+00=-lK6{j8UUjcC&t>6^ft>S0k+nH9W{&pb_PTn(*Mx_0`6Gm&xQaPU(rhXu zABHZs-1u+rpCJ?VD@hg&u}}ZGExo{=59N3+TCA1~#p-m)+uQG^S@zj@EQ7WGzx6iF zws*yaRnw@%$%n=}2t6vsvP5zOZUJ=;ERO<_NWAAvFTuS`B=5$Q*9q%+Rv#l6=Gikt~{T{$KI?zt@mA~n*<+sBso@fh**NFu`K z;k0{fIH`u%H^8mb=ztv(|3``V_RQ%`Mx-(cf?LuU(szn4JB1Mv?B zL(Uz?2zDUvvu3|MuE{y(YP}G0SlvhxWE_L%pD-+~M0Mg{AcxhgnV1lvGIr%H(W&ch z(7nCq+^#3x2XVRk^jz1CIH@+0l4bU`3NUYbvlOOY+TMFfp3vj-qWo#(k6J#fV$6-0@7m#qY z>nR{0hSi3c?Kj&vT|<27A`oE6d`u_$~FaBCY;!(8M^bX?$@x?hBKjrCZ?E0 zWf~UMhyt~lnj`YVP0sO#qp)^YQE;8IuxJ(d%p4FLh(0sBys<8Nr_wZB27l@A-}Bz;XxjC7i;nag_!l_R3c3;mpli1(r@! z;;@|Oq?RzH8Ur;NzYUh9_Jlqa(V-vjoa)3}OD`%5BPCt8XRb+=^h9KU5m~kZE$>w3 zS6CfaeloAvm|-dmFqN$OZ%{OF8zMkOqIyBsgTTzOY#ZoF=#B^5MdwEEjmtZFmWL5(1myJ20qpl}va3*Hc02T-W*ZurmXUtk^!;yq&+ zt!8{+dQHLifj~eB7?OXuGk~{sL~I{Sdw;o4HoQl>pZwd@LW{+N0lvo`t|wVo-gAy2 zC$(s>Iqws`z7l$q=hiejZ(%P|X<%K-W)xQX45f_t=$@@G70!%at@%xQBJY~H>sS%4 zPIxyX^HgCRUHi?yymVd&=|6*-K1IZznD^W_aZG)R15mE!&(YI-`NGX_Dk)|t_WF%iG^xO4 zqUY|oklf80D(EfRzom0}h;1O*C5`dvYOfZZ_9*hzB&`nSUwL8Zeja}M@W;BN{_kkJ zv8-j+dd%Qa-LrJ|QoB{owh5>+<0N2)`V({@Q){_p= z8u+)C!-2f)_4|V-S`NKhkiMON(UvHHxPpD174H};Xh5+QJXHKdyx^r5)sP3?&RN(p!QerTBZ^uja*otS^w{2j;;XJ}NFt9^D&G{C>3YLZ zgGmTuNS#6XFX=# zR=4x}i#!DotG-_3*;9skkza1|tjbu)RjtnTgp-5$M)uH+P{0S$gSxZV|L$TGTf=^UBlHA|}o3hn+TzF&$Y9 zK0_8m9_mRu5`SUERR|QZiUI|+ntU%b$qa>C&w$L;C(QUIX{38{qg38stk}nq0;QaG zsgNv8Zm7bN-LyXCqo4Ph(M+m@=wlE|zMPF;4onz9fvi|&GmFdDw(Y1ZM$t}S*;xz& zzD(h0;b-9_x2)FwLJ$l?^wsw5@0O5w3+$xn? zJh1M53rR^3zFLF7#u^EKbp>D3Hsb6nH{VjT4AQY9g+Uha056u|2@oInN+A&-(%UkpW8J*#-m?@FkM$(_p(Z!e)3tlBQgij#3YL+ z^2N#ycV2@QJYKEf_O}?#6FPiJQ z+K8MpPVMbkYQca)5kkPS{S)|3QiwY}1g*oLmwunel>djYYR{+v5U4+m51jyky1M&l zELdQ@vkM=A?6Gx=RUJ|w)+7dBXZ&G&Zc(Tc@Bs8gEWtD}N##2? z^(m*EB4}mmZExNwJLPwR{0J}q=bH3*{RMK(JaF$tGBD9sY+1XL*rd>iVawsI24RJ4 zdRe3ToH4H?P6AZX{EtoU>+P%Hnme7`rrfiSM{2u-BahGI(@Nle#3~ujHV$Id;(#ej=B!JO-fQb7e)tCXNQb8PR7=JJkZg@p5GS!TO^^ z`PK|w;Qe-eJ!AoLC~MJG|3==?)JtH|P}wNxrokdFUO@nVnMfLl#UCl#i70%A4$7?ZfE{WYHK`mqy0Ic@odTdj1Y zF_%F~auyg>{8qBG+55BNx93WqSO8UU#z!LGc6wGDwu1h)Ev%g)Aq7ORYJj9IxU|7% zBZ3cB%(rVX8EoH{#&Wj$Y*3b=p7lXpp&wC;Q`E%Jxcp{Qb3%O^syGZ&>c|fVhRam1oj<%T5!^VXex?dxhie_7o=*wdi~Rz zo1HaAQ>;aV<+t7UsuCt^$Ln-be&s{4m_mS6k0$*S=#S{@!wN2mC&|OAbR)A zUUqce&Eidx)DYK$Sbh?QyuR}Z;9s?OG5XV|lf%VIJ+-ey-Y9C~EqeY7Aiw-5M)JeUuqOK>xW+E36f;iG$?tw;$3&ay+xoFJP5RkI zj{M+=0tZT?MpVaYTjgt4pWXSB;feRirX3DT!=FWx&kKW~pu;d5JxmyJnls(-Z6IrB zfyFIF8r*}nMqA6AlXf^NA(|)8Qo#0|?}EV9mBX^bUVd8C;%d&KrunKUEJUg2JRS0sqSC!sM&u9+H{Sn+|8ZV+ zW`pZRZis8kbM0NZQWcd8(oyrKGrN+ildCAR?UZd~d6V@_2KfXt!(;bDWBQR|QEN^y zmO1`SlcFuD)Ps1^tiyT(YDw{9js9PxDy+;8NlYl9WccOS)r&w~EXoG&kiS#u(q#Yqw6Ahmr?I?S@OSo!n=R`VaxO$=UM z!e)T}N`7oX?_s1$+_{bj-0mUk+9-U0)8ea}3IWvz4v&LSlJ!T#9B=W)_=3{6UzWv- z!*QPgdkA{7@J=bJ9)A;)3B4T-)YiS)G5F%%hGvw6AIb<eA6oafJ{5TMnTJ4g5Ym(AQ5deaS!JGZga0ud+cUyB&YNl%Rs38#f zvlus#33m%oIg*+0b;%<4h9Y+~J{TG@LH^P^X=d+?sW#lUVn#>G9I~RNX-Z^f(vPv5 zgAt`yz86#E8D&mUizFBTtVf7OYceQzmHr_Rz^`mfFKl?Tk8%K~(as-{4SE;0-R6#; zV1PqfCmszluA(d@h*H`Yfs+9~3G+-e-3d(-jm;cN7gjI)(`U+G@U_#sAa+yMcI($R zVx^hJH+bT5GuBmwtRtW{lz}(-z7*JmiYd#8{pLkH4saduGlpT3IhFeajdeX5yb4Ywl zn-9M|me!2X8r=FlEN%y0Gn22MutUNr+^T}J)r7hu(tbZ2(TP4(SluTOnqc_g|ATxo*zkh$qs|k7R=z(VB3rjG&|A;hL zfWU(Ch)6UeEfbzE<0`&c&zOh#v*SnkZ1_LBa@YD?fglf%&;&}DpsEN<-?>T=Si$B@O~VizyAMxtYTEE#~(3L$3dF zRG|6OOC~SJ0nkE77^o9vI@+8b&wF~^dtv+18$BjF;k&a4WM8i74gnX(On~;aExV4N zDsKXhsee1a@gO==)en%4ReJ#kn<+L5o|5pt%{PUvQV)o4Cz;jW>p;tV>(j1U2bYrT z)xn3~fi6XWBp^h2@qCE*!rFs-Smq z!}x`z=Ijs80S=O)9=Z6xo&)HcM!cSbwYOb%Os+3eaYL0_jgNP;n??kKx)Tpr&S!5n zj`9G~7bgvI4O9DDWBbn#p0m%v4Imte=Zw_aakqK1$pOldE3BS#z4y=Wf9EuOi3yyH zg6W3z&<=aN78Q&9M6O}dCuge#GKD`vr2VkIPK>#cnJB+00a?=(%{L28C5}iw>6s~s zON?|XitU@uyEdqc!g4-w^9q?UpMVFMQ|R?^Y=Voc z9YV<{T4uu$?*>O?xhkZ%?*kNdS`(^@o)s#=K!xrrLYDw3;BT)v!({K=z4|rn_H#V4 zYip701w|e)J-Fi1n<`5_oDin{|mg6{dyGFVJq2nN3h zvAP{~SoA`og>~cdWc9PWbAlj9VgWEy(MX^O9e)GKm?W>>^Rgen3Sw}d8Y82aHAX(K zO=}aT4;;KXdd@U3viJc7uadOet?|r+0oU_ShlOtm6H@Kcs;`-eA&q4eEvz-sh6X^( z=2-LzeN&vMl!Z96nAw-7dIH}0KL!biR#{@wRPEYrXH?oo7WzT$`91YY$~}gb)D%SY zLL7y`TH2UIQtc*d=YbYDM|sWv=V}E8xN?uaqBS{ywYp~2l68A>BS0)CiOr`t^J^7F z{ty;ICUyt*na#kIBk;S2#J{!f`Zi{I)}2F}P61F1!29Ye;iG1eP*F_AM<)`OshLvk zh)TcWI5x z$cHvKU%wt|ia$SJ{}Cx?M(~+@qm=xUt}izoL^C``#hQ_cSATjxCRf5+a#dt7B7erd z+EhYfXeZJf~4=NA2eRn7YJVuMYY- zzcm@uqRVgsq=Y=zcViujvYQ51-NpNQHTwDQta?A?g=$mNQNpt3ojvNw8Mg58!A&D> zt{@|tG;m2qtySbF)?QjBK!R1P^FCaTi%Qz%hi9l$i0e^a6J0c8CEtvT=B#<%uwOJ^<=c%D_GIK-Xb61- z9~!bga!!LOEA!LNentu--y7uKEE0*8hb{)!SH=7O{hW(kXC+eq_r=O$YRCMn-@xko zY{;HJ30?v?L9+JCA_QQQ19gZ8Or?fLM|^OU%eO?*Dqy7z9BOZ{;@*;g2W)kV8fkI>S_;G{g}EXx@GJ z*Y|_E?#-DxMw$GdN@Mb%W{t#=!vx?vc9denZS_ash zo)IaNY;}kf`(`^hF19zEEFbS>3;`m&PI&F5y)11r-*PTDmkk2|9Z1O{V-gJ88=}ik z;{~!Ysji%MT?5T*WoMk+7qUgg&PGcaAAW#yjx=hYH=GNkJCICSUI~16(rrrmV4@fk zJ_yQQW4EcUBm=x6NyKa2h?2@#F${MpEIIe7Fu|_vX*8P0HwKY`B~#e-=w_yV&1)

2(HmL2h6VjJ6N5w6y@|H`sAPG!GXn%Wzh z?*!&O&2v_gd#)qqeHSk)FHu)5quTb4*^|cfSco^@+_RGh4m3^mPkR;ig)bMbLfeEC zK(USRmT|h5ehzHeL8+W8s4unOk7sB>XE()kycALe3-v|Flj?l2BetrGvhU5V4|d#` z_j*_sx0ONH_ayi>G!5uC&MXceRP91;)EHpWW><8@koKda00B@hJksXXJ~o7|$cO7_ zqBFW5&@GgW;Zgv1xlI~hZ!E^IJ_s_x`RT!k_w{PWc_p=JV3|g~-iZKZBUR1* z7vru?mq~HgFX(8WZl{t~J6H8Uc|S&nlQ#~low#76)_FVo`g)lVrClA~>&2v>U_OzL zicSE=n=xDavM+1$^q*4fb^Z5EkDOkWr_Hjm?^fdMQPaF|*ig3#M%|vpD-gDhpm-@Q zdMo+r_B%ZgxW=I<4CE4i+5&`Uuv^zK^1{ggJUN^ z0-%ob;_vz`ZWU>`&LNj$`pGt?shsbM`;w}+oiE*N73S$s`6G{82VB$m(CcJGRl&U} zcKpO91ivWpOSq<*%17U;#3tb(^fmG<<;ud2*G;e+f`9Bj2Q3M<=(_{4aY3G z70akMx#wEieEZZf2dk<(QEhvsBvf6Z3;f)4GG5(18<4f+t?6fd2`4-|B@?sAP>TDV zN9(ooocDpwmr`Z+zfae{DTk*tnu93yMVX zZEHBnW;ey_plnR6kEl#$^W6_>&aD<>?Lh^%`R@pK+URH}?$#u6K|?&DGmuiHeo;}Y z=UNU-n@vZvM>}mU^J)gMLq$Y%zy^Ax<%~`FjZYVUmt@x~=VDU`YKJ1Xx$-04(DTE! zuaC_V4HB#7(eal&nmSWIHI}66kFUv*R%R}o%JtDXHs^6FA|ikcpqmL%jf(Xmvpy+n zQV_#w&y+mD5qf-{k#}rGzSX(spY5Gk9b^{fgXJ)Jc(kRg-uOXx@Dsy?g z*|4&V{`%={!E|~|8_IBz|J_s6_H>w}oO1iBg7>0tVgJO0im}flSg1)^C+xw1k}@)WlzY0?{NqO$abq~cs zxT9(hFZcfWu0hRfs;QbVmHK9QP2aO0q1;1VOB9q-cSDZw4w(2g`(_+kDc#9%j`x`B zyGW&L99whKme8UGt_Lr8E2Ku^PtJJ_=1!L<&P@e$(6urvp(9bbrc-QEE7Dvh%9_a7 zxm62tXVQA?#%S4q{I9-rAO({^4#uAqXC*_)_s#%7h&$hKdTgOTtR1R6z)O4B3H0bJ zmjlTe(`#U;pdRo~cBZLHo~tYxVw-#rCU3IJNkuu1ZAkC&ae5uR|F-B0yNC5ouc$?Z)hLP}?GMK0Vej0sv^u%qoi&x1dWV$* zV4@0;p!qN*?=^^mK!j#b6#e<)(q&$wvC{705y8bh5F>ORcCnPFJjQ9|Y?Z=;n?7{< z4R40weQ=qrzQ{Y%&$U-60k{uS_(RT~CJyc%T zHy+YT_hRl777b>|JOcx6XTKl1hNZmRuH+BA+}8t|wXI68Dp>j0t>j~$2_nP$%m{2!bERxcQV}bw&!2bDvDeVii#`UA*QQci zOYR^iun0)K6g0;*ObQrVWA{^OWMKV9bfCXI>$>op;5` z25oeD@5w2w%8ev8Qe|s(6eyBzWonjrUrBJBjdxgJjcy#uCbi)4>iUH^ZJUKZG5FfP z18I!|_vt(y2eN-HlJHLK%MVaKOYY~|v-L$|-2fNoiFp5|@vtJSzK#UafwuPOq90$? z(5ET?d-tcF6hz^7$14x9#ic&2B>w_^l}KWRcukwwF7<3IUXCB*Z%p5E2v-TJ`D$s_ zJ-I0v6OI1$dBo_Rn97&Q7qegOi#ute#Lsclx=O#xNg(&GvS%-KW?JR#h()3otovfx z$U=91>=TWFqHk-Xl~pvM*EV!ELisl8Z!Elb}^x z58PM4*DpyAX?9+SUdpw``^cBCYK;;ID?bBD#RVvTpv}+`hK(Z9qh_Ii^G~@3|wl0>e`j9db@UFku`l5`Xdbf z$IEN1jYf-7hbh=q-qUNc?R2)d8+=|MgA{yBe`ZDlNZLXPrg!tHpyJDb7~wl%q^9e& z2G)Wc%@W~fOvK+hkp>qZm0qsJ7D7BY6l*Pf?nBRYpU=HqX5p0S?MzDyFjlu7DO*>< z;8{P`Yz>`4Pm45A7bxpj$7k_VwZJ9=H2~7M4Y{5^@Wl!cR&e9>t#KBHe^r+YTrA)F zME67b{i(*0VWW@F%uH~r0e>Xf-)1wpG{cxU$l(2RWQp;jFZtKLnU_aPeX> zN@~6%&^r0lyR+74s)TQ4$xC*e+e_sfx%W4rnV4I}*{z;*lW|zfOY9X>Ey3mD&~po| zH)Sa}-yN7TexgIPCYI+s?ceQs9Gjl?jkGJago|3N^_mvgY+)cqzg#ekgxEtY9L{Aw zDhzO0B%oq};JKB~s8>-pgrioG^rkLr=lkN#w)i(kj42gjM5xR%fBQ<+y(rxQB%e(+ z4#U|p<0XA8bM5wVWewFeIMHkck@LSVltwZ`qXe=JLjG?1zk6PFKtB)jP%)CdeMm8K zuWq4Zh2B=wbj|!*6^+<#>$FVLxbHZ5`Byp$kGH>ywl!tFYxh+N;ae5}b3rkA3jn<) zyJ2jzB(B4yGZ=Z>9SxCV4>e22otjIL6`_JUR8W1PDofal6oFsx=mGkF?~6rNqE5++ zLKjxI-SOFUH41*mH34c7{2Zyt_k;AgN>@Mu*2AmL%`$FF7IYLOz^Y4xT+oTTk_E;G&eHW&fa^1G`<)L6YBN9rDgs_XFI7aUt1WjmF+P}h zF6G=TkAx%b&Ee08eVNen@Z2M1SO>g_YVokcVsByJ>g%{C2cQAMDvJ-f`pRyW&gewN-Nu(a&gMWdZ!+&+lqCMKUA01k`iBy2xd1EN&egdzznZ-|BUV!mUfS{zu@CVc13e8>0!;YwSvS)()?D{5C*TOJVeAldy+tJ5%wE1o#(+U?Y&>H7AO zK^2|telu@Ob)XLm&brqbU^i2n~c<^BQu{rA}$ z5zFQl@<`hEmC9ad6d)9IdQ1zyju!yB4A&?yfdWYtzHUJGXjT9T>I;B}kvg=kEKV~%lPJzl{S;6H8dQhk(R~pxMExWN9@1`GE=apQR}Po=Tuq83wAu z3dusm7nEpZUbSkyYSk~a9)7IgFQ2{ng_yao&ob!eqKs8Q1pARhhc_ zO4J((^LIl*0~TW%+=-FWAsp!xMf_f8bFd^Kh-ptY6RZ1}nxUc%BZuX@{7)aJNnLoF z{)u#qI1j{H-TnNF*N=F4)9p;9T6Cv$c)G8Sy7q?^F^~~(H3DPxX_SF+%mj}eC|Som z@L1lf{LFX&?#gybq`0V-JEv9*+~^s1bV>FDOhG@Y3QJUOohIkrSL&&8Eyhkp;mzuk zBP)g+`t#})BON43U-tx4Eb<@ABchf~s^FBjdRI~>AX*@_9;!q2+4sQRI;n~Gwif;t z?)LtJk|lLPP7SxuB%Uac?UB7$-wR;0SBx~=jnNmS@d}83SjngARE2o6?2EJaZ(p2w z!A-)O`OZ1!m}5TUdBy~;KO?#%P(Q*I{7w{D2Ls%O^e2T*QI1(Ul80PlZsAWTU)pxh~#vpqFG!M?O4 zy^TZL_xCl6e8tf#D3lzfk@*Y9_If^G6MknsDeji z?1UtDFXN%m5o3I_*Lu}6kNs*wKz44@D?By;Z4PZ|q6D({q|mL{g`9%McT;8K)jKQ1 zNHJ)!pK~?;+SF77`o^_KER{W#{)kht`DEL=`+c<$7{Hfb$orMwKP>R{=UO)3<6!}T z?}ddZ0Oh#5r(&U(2Hgq(Fxf91RrmYt6@A(s5>|D1zxdD4%k244KZi2wezmNF-uamQ zg+^I&g`~`Vg(J{}ANiekUN<95o5F6^nl?diF6V2nVC$!%3eSLqboQvY1UD8bnGc`6 zOiHhsl+gtZINw_v3+Y(7=ynejjgU`z3`)Oa@L|uL26^ciVv@ zL5$}cN}=4J)5a_L7e_7MZ7^!!#B1+JQnQn`$F`S$UPFAm-*;rsEo~J?#&xJscV=wcDiFlx9kVx3ECYUE33PY6r2wR?rB~+ z28o*Zrpx&~E028EVWYs4)j5V%0CP@l?E*OKqO8xQEq^ZkW+^H0Ez!gacr98tAT?Z| zT^dcP!Ir3;u}IDbZc<|FdzE>Xzqcg#cLzOC<7dqs73hswm3KG<+^G60x$Q0_=c{~B z+MPc=XuSXTF6uhxjhQ@|1o{cst=JpgHd}xl2HY@{SWSE9v|ap#`TaK+-(dT=PY>Bc z0oCXh>e9RPT7mOhS6i1YpcU6YN3QY|qyP2r+r*v#St+&ajqb_N@8^%;m$U)Fy-DlH zLgv{LnF_!_bOT_!UHOgjHYvwuhoy)wgY}4QA&iNJC}acu%T@u_>Qr>=$*3Byj@Hi= zhxC^AD3^wnf&MI%eOrGBFdVfl9%Bf5FMNOUpTr9K2-XA}UwZ8$i3gr)Nf1O1pdJ zg~Z4?0I zFH<%yLvB4=7er5+;AMSFI}eDf{l>elGX0F|#{WD72W8i~gXL@l1~WvDtRAq z#Z+Rh5*!y&lP9a=Wq-Mfm29DQrR{nFaP{s=qeQL2CE1(Y{_b&p{sL1afX0-9an=t5 zluIAlL5H^FdlUWmOQRI;xH`7T=G_Go&URI--9g9_VCNTN{K;&5dXroiw^E#Fdi2U* z8b`W32FULTZ2x&9u(UmJc}(*{pHaYxU(PJ90>GM z{L)3VXP~J$21P#h*-wYsa384B$BuCsil1ix{u6s-OM8m|AC+HQm z(O~K9_*w&&a%C$9+a|;QmL27I-bKr2d~s{)d7*iKwkukwb$k^6WfZj;xFXEgaE}79ggZO8Z=rm>{c+p2_6=EI zbbqnNkb8#vK&*l{bVMEA0m7i;sbM1;HEMl*eZ%DyEQdZSZp0zRY0=(g0|GR87r>5g zdnNSnsn-OPwup#d$=esOJH5lwi2qPR9$iL^R@|!5>36<6(8(`cUURrT#lrq*E zd_(@-^`rq<)URZccs`;|4q-TNt@HgRE5H`MHgtj@?S)|YnHl~^{sPWQRhFOPPM>z# zx)SERGCs#gzK}Y5$js?>!d6k*J;kQEB)Ef`TIkDp^Z`5lX>Qt499}iu z^Qd*Nh#l0(Crm%TePFt8p!D`7s8V!@c^zedRFR4W6*0eCAmBTqX5M!il-DLddcV6$ za{Wr=+9R9K$9D-S?3Pn~Z+2qz(4_-hHjnL3gWmq z{pa^MloU~I{3JP@V1^!;KkMsU=bo*nS)PzL=6f)~xvav;+w1MpBH?499Y)}V z047${>Z@gbt0qq0Rf`x=xcJeCS5~dcA4|>g?025wN3;BOST~uOYR5k4e|xW5N5#gM zPW%?A+))l*4rR6)Ee4fMAf%}_0*6Hcm*7&gE7m7Qe|fx14q=u0>toHUQQpR-OwzaV zj+ckGc~BakdwxahsTg}-sw;mxH&l>u!U-A5wrsWn-|V!5HF_K;U~&Flz*VGww)Rv` z$h5M@1nLg9V{Fn9?Ulod!5!r@?VPXJf^H-%R^ULsBMWd|^^Vty*|VXVqE)gt?BL_w zqm{B3j1IFeFspE2Zyry2#8tIsxvW7eMkU}S3{yFJK8O>_P==vB33Zcke70n}v2@K@ z|1ARR<$s!}rE4wtxE6CKe~$u9rDO7`bxXR>FW}Crw8TGlt_#lQ&c?uN%IrX23MTS7 z;ZxNx2^M8v-#Pg2BMT1tjmx?SB8tKqS|J)h-IbYPSPk^O>}GfB3!TkL1;6ft!>8txABHTm;l%uzn8 zsexzR3Du*k<;}Ffne0h?4%@nnl|wtH65$l36ug9W=U<1h!Z3DE<(u?4MBy~W+=w@6 zmdm6Zu6Kj7>s6+3MH*OlJT%(1xnwV7I)ItsI&z6W-K`Kn(|J_O9c6(J#(KM^w#~RV z(oYBi)pOxpUrf^(#2Ew7E&=6>KMkGVsfBPNy4_#F?|}Vo;I8CdnkLC<4>V2)6bR_X&o!$3B z=hT^|rhMa_ixr#B?nJ9Ca*fofm4qKyk;R^0ninrv+1FtcxyxY}cnDBUnLMHqT3%8cyI2knKdByb*`u-b zF#)4P1Kq!oLd!#7mc7-K2bKBdn!dqBH~Tm>iMJRd54V0f)RZ*#>INVev@-HVpy!ud<{2_Ls5w+$ z>CyxSw@Bl=qajV~4r$&@ZaNYQb(MKy?1bUeNgruK^Ew)Q96C z@pXsl^MGyZw}X-Kl_bKodGtIyGnY0}yi!%_dwc&VVMbO$J8|ZPSEA-4%u{(p?WmnI zi$lP0W=E5;VhN~Ohbud4b2-c94H`R>>Fo9`(j07ti)2E*M$e^wFsA{ocKY>`J*Vy! ziaiT|h1_H6Mt-wZ^$;rV%NeIZXOBwfxstYMM&Ix)s&CHu{$k4r zw4wdVV&{k}Zy6h|x6*e#QM~VZ^RvGJ)4qSzv`gH@LYEPQ4)jHmOAhhauu zGiI#eCpTu=Bj>pTe$c|{ghyHQw%q}XcdFvV$^zqH!!!5KC)@34qdwZ8*JF$Bu^pf2 z!xw8~W*kk^Qh{A@lyk#4ul4Lu>fXhSQONZPvK12K$zzq}BH8{)m5JKf3)&6prs=D36ri^J_2vEJDKr$Zyga^)e%2`gWo@^!P1b+IBHC#Y;nY%` zi3XM525wB41)=Of%$K^e6nChQMjM+9U#w^kfQLm_&3Kaz8w~|k*e{0iFZ(bHj^?Ti z9UmI4pcoil#-eIzPac#vGh7(W!pu_DG(utiWnt4LO&xlq;G9K<5ue&H8!2Sin`Ok_ z$?Uy215}83TiZMZXssj7H$-hcZ2e$gKrU?hDzkZet~MkNww)C7=W!6QRe2+hy}Gm9 zn284Lh-Ki$nbb%uEmAB^K)_Y*XG_@~ZFdMW=HDTj%X_zKfKE3?xz%19LRyOf|C|7D z4L-`O{=aA}bm@y=jFk;qKck7q=9hh z@Sas3`{^HMPFMMLMjFC!pHBUV<;-?m2&4b6L$`pym0z1H;~xvR97}jC-f=hU#4_-8 zpfDG8)tk!&YzQ;x{3xDfxBN%vKiku&C-d(G@;-iH5EmV=PeM2`K<4{v*e9(0 z6oj#J@_tfSDMJeGJ-?UxJ9B~3H`($;vJJ1iKlty*rD(5AL+Ac60}cBAF(VBjxv>*v z;rB}|I`XhtDVlN$3@~K+w z##Uf9hoVjX;gz0D@p6kW{pWd}>Md(MYTHVL{b#$9F}`hdKX77tuzJ0%MErlX%fIPF zE&!8u`%C;!2Y|l^MnKGDtsz*M=0Dr*`)iFmaE$o-3ssaL^{uXN!e2Gfnv@qzV zJ;($3D82&7Aa8<=S| zaW&bS29g=aa9OppMQ%5Tk07wgkFa%=AvG#cDpTR7N1u*TLVgPSI#SbdF^;yKm$vE! zKR#J`FyPoa5&@qk1Eg)!T2T(~8{K@0rcvpYArO$WU0T9cfu7YdoJ0mT*~+0+no_Hk z{tt@mmX)yLo-!sL6M-j9$ORBi7v&5eBD5+yq;CeuaA~_xk1hwdMUd^K52r%IK{u2$ zFX^Z{2&@-4o3z?Hn2Fe-nWvr8KW_V>hKUa_ZG7mKb&R!f+2-v{c;tqrgsWd;uV6$Xb;F$Pcxn=bxB^SV^4b_K! z{zqMLFJ4yToZOgk*}jztKI0Ul1Twyt!$b>WJ^kJ{yBAAGL+`|I&BHnqA(ql-yRWuL9mM zNJ_2`NzVZldX7>w-eI4ptQSbzxO!VUFGshcW15z4k|OJ+DL)p4TkZFVGec{o_r{~G z?U$7-je8yJm9QSwD03$jS9#mdBNeXsiUFXd=79k%hdtEXbpi^jaS)ANQ8k_fF=iPz z?pOF~Nyp6hDCpV{;7HZPqrC6uo7smAmn4^!>MMWQPE%k4Rzt zg0+KnhxkmaNTco_BeLmWhto5xjBi!KM@j0Vn}t zjDZdAZQF8H@5SX*VS&S&R|f|5W<+WJzJ>Lx<}cobc&+uXK4L`iTP`J(DUkx4Z)z1C z-VAiieHgFe=yXAe;5;{DWm?g5IYCL|a;KocmG0HHVfMqocW8_Qk$}JzhySCW6A{bJ zMTQagb!N7Pho!OnL05&2z1{y1?1KvD6l*K)UASJh^l+Y(0L|Jnk2Bg5>s`jm1rJw` z=)MPDmUwsvX#4n|0A<-VrZIFsu7e%6*5={NWRLz|s?V|8BMKjjuG2eO~(T z)2=AOAqeIhYOjbkG|Qwrq|Mm~byg*B*tJcU!{CvNDq^C>2THS8KkRPn<~8s%-)_&w zYiTA|CeHv;jE}s<(KDmfrR6(|L8WEh+`Dp!UV~%olw+EKHI%;coF5&NS*v3+PwyVh zigHkntJSz%`VD{3;^{T+m$>%+ldpA=dpdW&5geLz5y5A_B*+B3AP*ZtE!ftU@6-;jqeh0EN`%1rFx#L>R~+mU0NQx6Y-P*jJXg zpRi`h|1%f%w}8{Lz+shD%LKsTT5IJIoVvHMJNh(^EC@MqgOb&pd~Eq;y%WBZ*p#IK zM&|YnuXndq1GnmwFqT>9n)z>b5LQYseH+1K)IlO5fLZNXv3F+_wapdd%+vX2w(cqG zuQ9NAD;Y({JTHCGluBDUJ^pqu!6_Pd<&~du_yfA5dm~cqM&#>o?-@$@#iNmx&J22H zd?os=@ee_rTlk65*i^y21MV$0}mY=ruc{3fZ(WVF6{!pQj+{c;$S@GFr4^&<+;+=q&fT zr|F|1pa}DaDrf%wGSU@SD(+RF7wcIu6H_3p=HI``PrnTv^T&g z%USN#e&M9c+#h~hOhnEP4rC+ySm!#h$*$h&*X3`JCJ3gdoQT;1mtk#x zjulhTRLZffdY6eOa4P-T3;jLqRNg33zrN}(9MXeFErXRegR_@I zR?yc5-E;{hO&3xH<>Mrr*ShukI*JB3${T0jW-G3D3cnkPcpZ$WuiqRGoRY0FvBM~?A#(-OIoavQ`y$2!U{h}%p$y*Pv{ZoGMO6b( z=gC6YgmR?Lq~+w| z!@8pQdawgudh%JM@KMGsd{qNa*4#LP*Ww+gDa@4{5n?7(mO72U-JM?|QC+G$AMUOK za+juDmcW&NC0PFRUFyEQNELIjkt=!&=fi@g zsU)XneA8YCkkH@VASmlf`goxQz3JO#f(i z548*+U9-@-)vJ9g_x zIV3J@X(}HA=*SpZ>6N|&lsQq!Fw{~7Ors|GN&v_X$|tGPL8}64rzW6m_|x{3`oqg^ z(=j9RE+^dj8pHn-G2dF)Zi9~~cobnpqmk;$Ze^F&o?NU6jle9#61VaeU4nWZI7a8f zd8zQW+9@nXJ(Rz&m)T{(dwI*CH2Dp~k@e-I8JgM7&fqV1W=~3!>RR_K7{SZLp3WLn zW0;9{830`OI}^x9MCTgSg=8<{aWM32JLxFN{qi2JSh=^Ke zitwcCjRgCZ(8)sK2zzdVMF*JI$7gw@|KlA%s7K?-k()s!enTR5{T7SrcZTdnTeZUb zm4U?J!cT7QUx8ecg*2e>yi+u_A27%F$xv%j9grJa_b*`L$8IWfN@$J9=Kq7wON+{; zk|xJ3wC=V2Wqf(x;fjCdBx$W;L98mz^cbzFiK|+mz!aS@chk%T8|>)UQi))#SUY#Ej!YXtaay2g^gDY@@69YL_#d2!Fl|UE#-d$HtcmMKSdaL7=Z2hz%)qB?JZ==r5}LUqVZ!}Q0dPA#bCmbE zKJb1{!y&aOj@(Smexic4-yz?oLJDxDWQFequt|1=XrZ^Brx}DCcEk~1YvlerpQiS!5|#5tm}ij?Ih_MON%7?)(2-dp zb$%XkhtRapG~;G=^F6xiVMe{zFxmzz+e8gUE=dbL+WZShaJVsB_GK(OG;cYrx$3-=eBQ72T{%mJ zTXSmO`n7Pd9_!c^c&^0L2{|4_uLw?J&CGUpvAp$CZ;nE%^7znclib{%ir^gZK>kg6 zSZB4+)ml=)Ir}e+s`)?wEIg~WMr2rFe*N}a1K*P;jkjx1#*#HB^YlZ%wMoD3jyzS( z^QHDo?M-tC>{^eyc!s>s-S4 zcCaqpAk|P%X`^GVilj=sLW&_?>9L}jBSBs3vy|hj#6;ygz^(IQyy7izple{B9H1`c;(?i{w$ya6&$Qlao0_A)}_mKNyZB zT7@{Les;<_jXSD0g?2I`aa|bY-$KumNjk;&A*-P5X%@9hQLm|!%Ge(axFQYK8V6M3 zt=2nfH+H=9eft`J{MzALFO4IAUl>Ba6gkxS*;>9l@VQ|3@>kt<-M-IG&p|J~k2qTZ z%9mqg_XoA?kxM=1&XPL6jUG3;cPoQ$*s@okpB<_kvO{a|96f|t7|MT&mn-sb3+fHp zG3c#rdJ;>|F2$H<6)GMkhA(n0RK$NtxWbMxb7tsXvwy1ndAJZ)^jzL-fTz0@z97Bp zK<<-o;rD63qtX$MN#DZ94TJ+`cA|ok`_Uii;yGid6LnFrjTZ^VqE+N@Fk>&FDbI;o zHkD;sl1)lksv_PrFDmAGllwaeMoZglIn;?e&n`_ZOUl?zpCAUl6^I?+)Dm~RX(nQu@FybF8iZ(zO&@ag5Eyvx?RAjphtL(y}>tX??PAP zy+dD)TjteXfva53x|4kzd+GbBvf({&@;VkTrl7T8w-+b$C@j+Y`nOJyr{LDi`uf24 zZM&tzZx5;z|Aq5EJZT3h4t83xeYxhJHDpmzm_2oNz=$s5T2eo|GN0_$-eqFB|a zJUcYD6uKDn&UYBbPAu;!5|=Uv&|%WibNsI?2unSS z1RZ3@Ftr~z`sFF>ph);Zs4I=s*FVLw_XU@~=sj8#Idm?0N?LIwRlERszsI)V5;u79 zRR(3b*I+0ZHf5+MR&kD^i_O{ICp1EeFOC8${kO@riQ4`vDv$sE{GpE97Y+~XmXIq< z6_30VweGNgZpxh_NFn;|#JvYtc>n6t+don3k9dNWvT6s#!*}=?MhQK$c(>_188ekc zsg2MXOZF1fyco=;oR7M9Eqe|+(OlJh+oz@)B^|BwiW8SMQ+>h6eg<CJl^bzuM&vJio5S|m*3)>+R zlBK|_a~$!UnAQTpl>PXhfe`>tK|1G^hG%liV^lBTBI%}g4J(L@_Aneao5Xw@zram{ zlLkU+t|ireks)d;RnvUtR-9frl!4_~MWMlz)2AIOo+iOLI9@v5=Nbkd13AzQzS*&_ zm;F2|H$u3K&UvF%Q(YD0MOUuV9{-Y86`nHg^bzG%lL8zsUr7KN27$9N zt6=O6QH3HBc~j?nzLoy|EK2B63N_=-%j=F-M7hM@E-AIX!N1&yv%U}+8GiVZ@Y(Wd z$!)K^W(wktl1@`D=aJuMT51kR`D=zA*5C76sNm=*hw4+^h%u>W1&gQ^k^+2&?q6KK z+)$1lPjc5AT4$3F^3~hL23#zmM`7nE+mzwjfZ$^_Fn91@kG}4d*R7n6en>0vM3~k> zMtm>wdFDp=sBi){r%u+F?OCZB&WpMhC290Pl3ZH>w(0bE%(7A$ZK9L+sttSivV$F; z9*NF^*me6}D8KXQ_@GwU@Z`?n5UnMNv%ca4G%NHOY`yoS75f#1xU=~9-!@FzQpi6lx(sbJSO_DCY_h=8) z{l=61O@W5Nn{k-|&CMb|b9V)L9NlzSp?l`I6CwMvG*9dyi}w-Lcg z$6I9i`&ma`Em+qRPB@>Fvi`qB5RAlqUuxvvUi=FK7MRrWG@RCCf zHG52fQ!>xFWRVJlf765B^A<*mS{)^>Gznxb>D-L(lH9GmH}$4+3oCJErEq19I0&-I zAhT9X#fzT%=1C#i&5$N$Ic|91IcK>ygg!3?wJlLTuz$qUVlHSxs_=j*t47KiQOl#; zV%2pk3!jcZWQtb|L3CC6UUF)>73My)6h-H}D@JZ7^!X-z(+`Fx9;yIWb5btnp=o&J z@*uzN6-?N3M9nyTN#}+L#>yuv(4qC((yxTL2e?STAa0OUuvHk#!T^(i?p6b{do1Ck za9&GLKilC}ab_NaaqA{Xlxv+w{G9%JX>cj>27TT*-A2k4e`m+Z=Cf;Ims~l)Du)+> zYEn8ow|%1C(c+eA9-Ca&-V@*X+o#7ukE*<+iF4fnh9@{1)(0jqv#qSx$JEad@2wB+ z5OAvOR1KPN5B#iNjmZrBqz;q1$c{c!VmK0Vq9#;1bS&HMDD&`d%^|T=A%}iGVxy|w ze*CKm{fn&a8_!(yItclaPO6jKbd0>%O}5E}A05avB*#`);%?x}3v$KzdsPLC_Y)wT zuiT-W^>ydinI*oh3$$GXBzTV77vf6<=I5=IRK7FFmyUA1feYEZMcd2#e81hWVhFS} zuHEcB<50DKzY(=}VgO&U#PvIq^DBAsw2|FNNwn)RPBliemLxqNK9pWjKnm`dU0`ft z!)Vp08RkK2(nS@*oH>EfoI+Bc@$B(U!dBVfl3?=+iIkBlvmtKcLYZT)UL|Y_lN*h1 z5oDk0fu#E@vUp{^-9c>`oQFiGa{Jd?S(DktM7|KYD%2sP|aTaeav4P{OgDA3+}O{zbX6K#NT< z1z-UCXl%$gMr&)QWx4+)g(FQ*Q0cLH`zSdNLy^bW=i6=v>8s%PCw{&C=At-MEsX7S z1I{+149RaeNjUl>xeIByRI8hG0hM{>?Qj?(7~Vq;PwrxuMr*#OS2(sof7dbMm%p{) z&XsSaJ3@_?3gV>Zwofg+T+%`E3`Q&b^R{Puk2OL~TGw&E)wYh4q`xdKux9Df{*G++ zxxR-RX(BpJ_ntPx_P%@fI!-*n;6z>4F1FPzq!=qICkdiVX8XK}i^kT(4Rb!#C=Sot zG%;hll#z_2XITqX;|7cb&ufO}&T#{*ZFo*o2;p=^YVOx66z`m6K5Szl^EF|39Ww?-|&PKs-xur98(97rEEFWqK zI+qO|9cew2=fi;%SLPFgDWOj95K+7%VSeY*yH=7a3TtgzR5@Aor3UFMDr4JJ^ZWZa zt+<4BY6fPL+t5QN+K&s_L;|Irgavg!hEsKqwcQOTu&xVOIJ2i$_OA_*wh69-^F8aG zeFNldr2G%}fyKoSPi)J1ee5;mh5PY+Hz=+qh}<(El-?!^>V7EShBh}b@a*QXh2P&n z-k_s?Wubx(@~B`njOB;woAM5LO87=r{3W)_&Fpgw&q{#y`W5nr|8x8^T{r`Fks5uB z9fn-T3FkCd%T6(4oy98*M184@R8=rCrouCV??G5tLI+TPmvX~H{dU{w-kZg~0=c{t zLDj&0>&5^D<)J8!Tyq4$hHLo9Am7~UH!w+Gv@>+k1XoWZ6P6Anqj-5s(BR-p`}BOL zSTP;5ONIwKr$i_3ek~)&)y^O4WHS2ks0(k1{ihi-6kQZ3znO8D$F^H`a&7DOq928h z+}ZVbK9fuNz1Iu95;#5`(SD5-CszsagqS@nz4|(K&J?{#qi5 zR$ul!^*&iF>r~U{5PiQomL?&g_p^80uidyXM7hr!WU6n)Qtj}(m3)XHbUuj+c79l@ z%AG$LN2V(_Z58~3>+MBNyAJn+Ge!cIdcvF^R;vbcOkzphG?6h1urq&nbVqEOH@voY5DNbGa8=1g34ga*GazAaxstF? zsC1-PH${AX-qt>5HH}O)nET`$)X6b)Dx#i5g#io+_E3Ky)t98szX}*>4Skf|cg$Q%TDWO0S zsjs9Y>r5`oO)e7w2-l8LPZe1Xi?PgF+Qe`3mjwipRwtY!qOrT@X+u}{5`y60npVFG z*e%PfF~;Kr@G3jx5ueZW?XpERHfI|s1;hn~ z5vPj!ya^z8{KSxmmqUC5_(mP4*dx9$6hGl}T?#5neBf3QynLtv{p8y+ z_m|6|IjlR!mxnHUc~>M=o4IAHoMOCSZxBe6TRp~~fTs_P2*w!orYBhZ7-2;7t@y`V zTNXYkuMnBuU}+pK%Hh?}Af|v(R9<#-*d9|_+}qJ_1q)$^g3GrF9A2H>dAdkbhjrl zGLoV0xvC2C-wh?&PDN8xT)<;BS?AvzN(<1aLs%gO(=XfR29i$KQv#}{tXCm?|3C9p zl(zr)Kl{U+t_;NgC8NNzPOZf0GaoNfEVVO9;a{ZF)SUUr;ftNw zH65AfmL-GU-%)CZ^B2L|3>ZVO1IW`&(5SO>Jyw}GFlz(z`Nz0J^9jpw;P9M90>+3i zO~R0-3GBBzbTun=G7g$|Y;wKuv<7DJ?tY4|d?^Eu2`E(kUEoT~&q>~&Y-2^pL8=?$ zOn${nfh{g61xQ2ZN?aD1!jSY8JyLrWBU6(9S12jrgT#kM9xxyyTNvLaAYhU36CU*s z4gc-o6;2>f`}$FBusr>~bls8o0$E4n>gb5zlDlj`RdY9NG)l2HSk+;Ok{e0`v>oH+ zP3cC^kKpo@^#X6xR%bqc=H&WoLQuZyH7Gx=8rbssAKOW(Q(17(56XGWil8R&Y2`{D z|M)BkW$H)f-0WbUl`=q1V(wx{~M09~o z#gk^_5I)`w(`B%ks!2ixDFG;~q!+-a1=)#{@*$tdW$NJRM%GoX1b!>laBj{|) z=eV%HUlurbkL%n2v#)|MKl- zNsJ-YuA>j{%qSL>h{0~SEwkFGZUdCOiYgX&3i-EJ@r7NZ0IG1HiQk_ySQElfSJPd7 z?J`N*&JRcm!w)jF`Lscdtp%t94HeGB?_s`r{y6E}qTY9Xq#>`xD)SV0`C+yQ`w5?n z>{yZN#dc7iuDepOn*XxRe6%$*r&$T8C9o=Zn14EUx@;--JG=NmzWg(k5! z_yiv8*3ZRIqwOmWuE_R+ctq3oddy4>i~S{f6Z-3@l^E4p1n=iv!C%t$>oZ<2pe*r| zNtqJ?F8DX8ALZ(0@*X`FZMoH@yb}I=PtGs>G&TV1HUALX2c!4jSiQie^)vnBZ=&wP zen{khBkKN_n3~XT1+O;xIi@~lGz|VTR$YVLE*rr1A&Br2R>k@z(h}O=3A<}`42Usx zexkST_^)3!8Rabq)=Pi=VfOc$E4$vUCC=Co^<(l*KG?GcME<|>dAxhBWomkoowJ8u z3fv#lSnEBpVUkJMPuRI){_z(!u0fWZH&Z_xkz!xxRk*ke&>BKPWqtOTt|I3!+Y-Zc z3#|w4A}1-t2=no(u7KPZCqJD=%~-c&VM}cNx8OGvuJ0xNA%(VC=EO9E^q4G{FMS~ARK8D*Kb65=3S}K_ zAWdZxfY?#Ocg?*2F%kBpMQeSY$@rznrVAGcD8yr<%P%)R=g%8VGK?zwBWP&FyWC9@ z)@Q;Je|Mw7VwYZB6tio(QBj5quX>ygy*Fp+gkHd9uv*UoooGu zHpbgf)_ol~e&agfuzC~+0p}nF%RJ)-gV@>~7#@?;-l9ZuHJll~0Y3R4@m$r=%%0#T zLxKavY+AFZ-zKuPlr^CP3G9J2esrzsO$ZqeBQo?iU7TyR!HhG*69#b%`kj1wLe4H#p#M6rXUf8 zf1SQac6K!+Ev-`r%)s@#4Cr5^mf$g=eL8LV3%~ch#gI#l_v+FI%+xZWqHsJ{;2|Q}2+l zWyuTQOt|f0)E=Cc%vfyrlLE!_uDDbftzhZfgAo#BYoq%lQw0-4Q(v~Pqw^%7+hz8j zTio{2*`_E5sGZlz<$e^t(Sl=Cd$?Zk(_z}8RN6q$CrjraJYg@nahy2rS~%#fbHJi* z1S$voM=Fcjm>cCNtlAgZH1R%BPvE6s-p%TMc0#4Y9?_H2?|oTLfP$Deyu=OdczE=# zr%LIQy@%&>?N6HNmk1l*Pp`l}>74ZjsF+AAofvlMZ0I|~jWSpZq_C>$`O&3`LA)Gw zqSR_UJtVe+aUGb=k~N2Ib5xtu-2~_60y-heszZrnebz zQ=*$07g}Dm>0hOuard7Gzk~0)X9kp&NjNR%han}E2nQa68#T!xczZz}D_rNPO>58x zf?Wh3y=d!@!0dVe&X2~iWXQoGdbNYo(8V973Dp}8Z^g6Z4Tq?c;)$|eYt8UiFi}R?2^qT8oZqF{~6!%%5 ztMh;OHQ{uQx4}Zz@(w|nyy%?m(g0Y%1o4FY7QZFI6ubeJ{mw-L5cUl5I&@|a752x( z!oA1G>Lm}q!}dw{nyFkYfai-5&g6c!a2kGiOM1ByJFIrWZ#->fVM(sFuuR6|^*+DY z_?nWA>bsJl$<#79{?hZ%j+4Z>a0Az~r4$B&7SSkMc5kSEYTek^hetQbTy=XjeRL9( zKyE>IS4Q|HAGVRsh;D>-7GsOq7bgZuUBL!K34Ukc5S3+XxIb`n*B}aHxcu2rvV&C7 z5Q??KU7L%vTGFxg8VT=dZ{D60%GDcmjKu;BpYFA&R6o;0|5M%YfA(`C&!>vIw`^95 z3`7Ju6BF5<(D(Xt=fjo@5!_OsbPBayhrzi~QfHiE;o=0qf9=H;P*XM4c&J;QNo$Jh zz*u8^u%AI*790Fcow*>&VZ9O%xVTzGeBy^Z;9k6pU=%?*;ScHRucasNMmz1EIm8#! z0d8@O^5Jxrbs{&s@lpKcsDc|o4pDP1R_4QH3pYuXYwkuhXDN9V;xe>o4*-f;9oFs( z_<>q|r*+3|vPyU^8|aRrc^ZX+Wb9o!BI8nlb)d5Z1RguirBdBXB>j7e^^Mog1C z#l-R+Uo!X?Y;|{K=Keki05sAHqi_ZKk|f&s(FIP1i$PCcG+xVT@Qrztt=+DgGGEa2 zEYf#iSFia)iK>6gH&$H>BoPSI=jhUO^(ERP9BLiRGZSBi)TNY%G!`4=EZhBBYKV%> zE>?1PsG>$Yt30`=83y#~#81f2&%FwTJyN#p$a&K7*s{f%V^C4TXx&W&P(Qr-R%>ZT zlf|WRCY76)<1k+x)#46}+Rq30e~p?I{;ZRd8;gzGauCq8+oZ(Y2xgbKS2ZOtK3mqI z>|Nv$K8h0#QA(AXcR^Q`2@MoL=)GFcwM*__Z_3=SC5rFNC>QB#NUlt5Gps{g_5)+4 zVYuS92#?V%Ax3~B3PBZTCo45od{{(F=>c*2;oYka5dZN+?#>HuJTcXGd$-uz)jAYm zch`GYq&92l#n^eYUQd4`@0Xii8E#bH>Nwcgi3Y(x;$I+P|B|q65&3y(5f`t%^gehR zr*`l4?owM+by)zbFolMrYlUkSUvZxfWe|N3)%3OojJvc_#Bh48dXzU%Z8mroT#!HQ z-=HZg{&OKB4Y}3Pxta0`I=@nTLCKOvO)S=bve|xTo@^+sFm5=#Bm}};a#MnrKTb}s zvE>cxKPN+dq*x&U9=^ITzz-nveSCUDE4z@7pq(F##{uQva^RPuCFBuM%}XX#cbC|8Y3`&;IIss4@-+Cv)nO8uCL-kuJB^yf{?_v>rs5Td65uisp91P_}bbtv)2aey=%R%EyC zz1iAW+~4+q>6NE zKLe29RPRWZRYn4+=XcGNx~pwJoGV$;&zUAx5%%yWD|RDKrY!fh_^X{Rxx(hRW1u6o z3vg~g3Fc`KDVDR_BShS)Is+~;J?lY=+XYK?MlJ&;`dc5edp@E6O zi1d;bv`(xI42V7^^+F@8#5V}EJzX36{Cj~}*pEg}FIPq8c%xY~##fukE>meu_{}-+ zt^|0@`(0l+i6p*g8Lv%SER1(mO;hbwmvkD0LAcQDu=7RfbY=xNf3bUsf8g}wiiAYw zj5|Cb1H+HTi&2fjTQIvuS#z2rQhSu{1A6otP2LQ&VViTham1*jl~ODJL#p5Fm&MsK zO5}r7=Y{SU?Ky(d8b%}49Ywb~V`3&U&?2FH9(VEqzp~&OCWxeMXU53o&9%5}EZgL^ zbIe+JXP;~!0Ls3IS%2Hz?Elq#jE_v7A+D&VSX#ERJkEBoimY`%NbRm|9%9qEI{yAR z{p-$)ig4$r7;ik?C#o<2?5m7)g#iMi^X;-J=_MLt^Zw@`b~0J+dIeN9{?OK~EpLF* zPaC+p?mAXFqzcUc!`^#FHPvllqu2{JqJRoEnuVqyAXP;v0#b$0LXh490)}e9h8U0z zp@UMTB|u05K|m#lQbkIDP&5dUgaCpeKm@*x`kwRl^NsuK-ZAd};TVpw$zFTSx#wDI z&SyT)SVqtzOq(CdFV24B%ej+(Xu$hfcY1lX+CNiqGNrqHFnFbjf>s$8lp}OOzxcFf zDAH3DvCn4a4pgqRz@5@<(5LBv_Z5qUOFX~&`*!9o`a5Kc!B`_MW;nF;q?+c7tnnqe zajz6^c7@_(%MmwPSb1Y9(135Us%#oUQ$2Bj+)(UJ!JE`i&tE}ToM~0fR$84GV#cf0 zCmvFRsyWvB<5R<@_M-%9z3$H&F-I80o%ExyTl8%Bp5dQSb$tcoD2?|&xxrC#O#S5M z{DHX9q#SbqwYJ{$Wk#g0K(4*;pJ@!cl{EIw+oT%Ao-E9k2SbAq=Q!+GT78~c7H{#~ zDDC^gn#_+X!+L;fBiGkg#J&+ikFJdDO#`^`ChX+PoKAqqtEH5ecpff^&_8S?(5+Du zSC@q8i2|$$WcliWrA^H9SR2fb;BLO>z(yg#FVN*gnI>MI(-l22YC?_~d0X_ax>)4c zl}IvS($u2)<%mmeNg?Pb9`Ap)Rxj{Y$a|(vE_lSw_pl+FuM!*&3g67GPWY<8A={ zYn3LQc6KXEbKei{&S^Nf{khY~5*$s^C~0%G-@tNx7E{hp*G(Rv;&s(q0#juLhW2i~ z@VYZuqnEYwbFvgh8BTF%RYAN^!~)`LS`KpL^0;bmVqfyNnv*Z^5$)$;s-4$(YBhP2 zu0DxW4DQU!Dk>OmbuYwRX=qy`8lYyE^fLS_vyjXq_w6@RFW}qij-^}F5KEdDy?61n9$Wid z$Zf<49Std10(kH&r^a-KxGx5|o``j_I!4JXA zR);V0QSP|4J4edL;sa(b0q-evM)SwNGxX(>1#*H9MM%h0@d>RCAxlAVs~Z^wP0Wk~t^kr6^yM;+c9lTQs(wjnMZ_h@unMGOmG zNjB9T(pE+jE64pGDA{~{vbnROJT{~z{oNA*Tmpcu=n+NFK4`yq#;GKtjhAP+q?1!; zhbrT(zR#O-?ECwsrl0}+t29+gngIO&T})iD)lxjjTKhUh7H>EzG((kR=)0JzkUrwl?_25uMf=+NaeqxnSwZLWe)eWH+v8Qb`~Xtb}RX?PCWa%X|Vv+f1D;Oj#H%LnxX zjk-XuU<`ViT%{JoT{7`}P`1r^?6&l5d$y~FPeHC|edw*u0A{aXLqGZyG&wiKkD~M5 z*j#sP`>Uz9%OP`^aR0pluF3by4mh$$M@Kj%_2s!iBh31Gl4Mr04f~Y`I#}K_y_Mze zdXhZ=#k~nMW9)7J!r*;F>C}~@Kt``jS6*6FxDR3LHdSQcU8jB7Y7&&Qr{kH58kok1 zgBM@@_N4-6?Y)+^8@ZnRvCPWI5t*(RXh&|toZa?rud<8; zFmot5)(ueZyyW>|1)j&-&lv1{_+VlDW2l~+;>BRl<+{0^ge^MT-D`YX-&B7wQkQrM zf%d*i*k;%t!nGPT%QX}+oN0Na8R9pige~@@1=MbvBPD1BWxqW) z@!({{m2)&uPetMLGgL@t@i0sN-R&K(Jl!@N8vUnR5U-LjF|)@z$KTRz^x zytsi%D*!3ROnestk>ijdwQZ}!&SW*Rv`*zr#6^?cy6>RXp7r|Fi~kZdZb34rxeg>@u%-+lFu(TY0j;lmb|gtUZLT)J1YMeV00jKi1B> zO_%q!xbqZ+O5Jk2;$f>M>Y{Tea9=_-Oej~#7*MdN=DB<@c?VD22TN2IOcoCtfnVu$RL z1F)e`(BisqjT2IpJ=OS1X&zK77&%=D;dt_95Op^fZ}>C&i@tVMG+dS`cj zIp^_>WiYb;>jKWB)~6%m1}@bCmNRYnRx@UQ?a^z<^nE$6rfO#o&cDJWOO6&j2(}jI&mX?641Xi3+%O#_QhMtl z3N^Bv_9a`_F9ioii_?W9JCA z8&>p@RwfY@i%&z-_Zc$oD^)QCiefzXN?y-dgzvqo_4xv%@*0MGk>C8D<@bF@?L!i2 zAi>tUg&nWJ-9o&qKvWxMnmaCV89@CGt!Z|gyJAL&T-j%kF_0QvM|<+!JsZgo~t>+h>|y-(i%I>y*$@eGbiY>BOZH}w`jHD z6D5CQv}~`%3VQfEs-K#TNcE5QI7PZtm9covQgm9ORG?7mdmir{udcHrYG*ITQm0*MBwpifN<*X9zs$gG) ze41%*n=wW$PKkZ<1Q}7Eh8t%13NZs!1hjXlo``x0_eQINxHy|K;(m1c6xH!n2iH8`MaR+dvSeP@DS|D zfImP`AY3xuFsnJA)%l_`73uyY(2Y>DT$S5?uX1%jD{V~bJHEymIp8p`o=P&G7`nYs zHt`nAdOvg&JF%OPCJg;TTa*jF5Bu);W(YeW@0I$!v~{Mo&7R}r`<;8{tU^&!Q%Kx6 zALQ8Ojv8xxdj{t%(FPLcL*W)zQa4lB2s3D?yo$sa772`_2w2ipFRg__HMD3D|9e{ z)MK*=22&|Kr_b+2T0x;~BGEB;u1sl~zl5mq(D>F$(&G+*<|>LiFxn;aM*TVn=f8a z!eokJ$8EgPyuEsNAID{K0(rWoL21E&CzJQf+ZWX;(Yk$GCB4O~UVeLM`FZK_j`ltG zPQmxk52%DrZ#yK!D%~*-QC>(}=6rGI&$=QkY;UX>qE?uWBeX9Q1+dJKBS|mYT=|nH z>i2f&KA%BO5Dzp*X32LFF!OUS3dx7 z6%RC9+YGC=`pAO@>N5@nm&m%CCPCU=>fU*h)7&1F31a!MH-}a7?hKfb=Xa;KA5Mi^ zuMBAq%?310eSRt>dGnE!gnRM5C9`V)7A;!8M!{xzJKTKmU-Fj{sl_l|pNXN?W0Zig&FoNx0Q++7|&&*^Hp(~pFF_e zM}1gW-HjZxyaF5Nd^em_Vlg4rw-ZG^J$kdF7jt95sVCyrZbsjmJ0hOfV#xA@JN(k? zo0GPff6mz6ZOTtXg+u!Lqm4g^iJ;@NtS}8-31v2l{4IV-2jGqcC(T%;qduJd7_Zgg z#7T4seVkd+dZx3f%;eJ5gN4y2DL^ONLv(FNLX$t#Z4bA|*_TtNwxDIZdy~*R)X$xf zsnT^py}sc9J8MF&w^^Ekg}sluX1b>AEj}rr-Fv2^tv?6t+^Po+-RkuX-ZB!%8D$B4 zY(KlMVs`i1xqnWvmp?yHY1Dax<>+$U`vDWT3;q~2(`sZ;MWi$@B#qsGcga3%bbQvc z`xax^TP4bBCPgB;cdsq|hOBiA6sx#mIj&US=8=x1wE2{savB}E={Z5K7sT1zG<6saWE=%Zgz947RtwPw}p2X_Cx6qRsSM|WG7OJH!oxCqx*0=22 zF6WM7nM_zbNI@tuVYBRGfRh=3xgoa>XidXP)Xlz_H@yP2hl^akye`F_dKAGOx#jb{e&YroJ#I91MiQCu( z8(g(hxJB*UW-J{wSD5p}Uf<USZS z>;u{^TaSO4l9zd1Sf>qd-l{uMl{WTAm;m&cs7$vvFugmEKnnMHalH%eB{5sHh&qZw zz9nNE%DDDW4>8`*e3Qg(V!n%YOMCe9CsT>l>cXQ`NAtW=&0P@}JRBTwQda0BYUMuv zrg{h*vOP({uS5ED|IF~szKDlq^lqBc8&at>n6erD9#NJm!?G$;v~gg+$Gakoh+HP3mO~}KB@}9TW0)ofa6M$xWf;KXYGnI&dc=F z*J;z2XtJ%XYCUmgG3fDSeu(Cc9>3E0WWrmV`d0ct4kSl5Ms2P)26{~VJiN9F^tk!z zefiW*J!@BKt@FC6Ne-+TMO8qgz6R!myBcYxXyZF{f7ucby@>ESfAJ&$xb0JFzDnKL z_T=A2v~R(*6H(6%Fgx3(KFx=ozAZlKAUgUmEFri}K5#M;4Ba&-^Ex#{s8by&kbnph zEA)&Ui8s+DhJljqtSVYmdF<+V7WKB|!KNIF$kl=eXf4@p^0a$rb5c=`wsc2H>8fzY<=!@NAs#xsg3K>1%n`6>(oH2I++VyQe{`S zl)a{CLK^B*t;U!qnKbTiPLE*yc-Idgf>r$+kg}gua8UDJ2T7nC!>RpO%HuCg-oW8p zMsh%@%;yKhzCc&6Ws9tG?{Zj+j=>X)cf4&5Dz7rCKJdFdBV|VRHCT_R?kDqy>cnd{ z7~oXT?YJPF6E7|1EuMjQPl(=dFF@~Vug0+}YZ+Is&2MzZ`!5aGFUz&P*2JDMF9-|L zr0eUryv#H*>et*}%c9}}l)5YtT{3t-*wdh`jJWF`VYmeJL z?{sHSX%W-a3nD}t9Cm6PcC*19z%-vS6>&cw+C7tnpOLxvv$Mk~(~nb$4W*A1>n z&G|Ewj&Dg1j+~Egi>u1I3KL0DrgTl8#O$+?1M3W3s^s-4#gK67u#VD*2u27j0rhTP zgY0@{ZWcdgD^| zig`snw_Bo{+Yr{M)%uSDhLW3*XFKA-8LYlwiG1xMuq7e8wm(5iN(}wTCqK z{ZO=$xD3K>ra0G6@I+M5S?@;t^Bz$jCEGEJ$=SEi* z{5sSF)#id+tkA)`)&k%wcI2`P4cfyhNXAg^W60et=YD++Kx;>@@$GTt4q1lRV$j!7 zTfLGN00&jr&wduZ!1wybDnn?aDe6aeRgcfF>zcOPIJxDTRXO^v=NB85pw~ETL^18$(OOJx*vBOfCqgn#V)m;{q)9^@u&#GI@ zCLZ>syUp5^EV=LLaGsjpM`Z8jg(2{;UDK_8n{$)J*&BkH#?9X}{HoRNs0J(&9T73( zRSI0B)V69P<#p0+pnQG)di(PzfCLi8T&OsNM~IY02LoWGjW6x~h&Y2>r7nja00_pH zlUnjUB%5G0QzjEaTCamBp;RU4o)YjGiv6$Dj5(=E=smEaV4Z%YGNi%4;#ST#U=wM^ zrkUHx^z*)2kZY?LYu8#8^4(zjN2b^RV10fAxgQ5tco*Y3QSEz6_7ztsG6 z>#Cr5V~Z8Lw+*Ex8H5?X3@E1M*pE#hyTV9zjdMg z2H*)*1o9q-9mQ`066@p|j5TTigK(nK$Cs7;VW|jb1|3lK=IZ`^gYn-?56~IyC+1FJ zL@?cxM5jFl0P;^Mv%!`&=xJ%WO@xv=nKu-8baN>u z$U)KtW)YOCIpgfS>NY^(T`67%ME()l_t#m`1srMik00myQ`WEF&^dpz%KpVZ|36ym zH+7l+8z|eM_}{}wMNqCl-SaQA+!Fg=h}qp=NHw4pxC60hzpnH5Ey4o;S~~p!%kURS znlv0)68NpFxecJsHk)TsX5N=@acLU-JC0>s%==h?ZSqoX?pn?wkS|&MeOKIH5q3~d z5ZLMC>PQ?0*qU)&{U1oLL{ZNU_NTcz;A;!~uO!dkOd7DPMtwz@7-;ou>>xHDk=C#nA% zSCzN|*y**(@)!FcQ&nX);2Awj?fil_j{+tHFmJZ!|A+Nj*83Ia{@rSS@y)-n*Z)@{ zfCLy12_&uqnu5-?9Yi;p_q&)K@g-UWV{Gcb{lvNcZI0$gNPOR>+cI3+k#W0ihf}X) zP}t8Lg>TQfl@B&q+WBZ$%UCbXx7GUAlm4%HyT1{0C;D(bhz^h0==9DGZr5sb zTk4CA;QZ;*ow-=+u`FLQo^M$HRUj{NC|mdLKb~HHKy&Y&=Xuv{|MX(I`YQZg$~(h| zaFo62ec&LdsbO$J;Bi^b%Fseo`%nT$M_L!A0G)h}sQ}gNu)bH=d40_~gCXLW!RKyd z2jh=;5khK7TY$e6IWq)XQ4&FuF^iNwa?P1h_j$XE!&~DR$MzK&=tb)<)_1Y>ZtZ0# z@jhn=mK2KNqtucQEsWRvOO9oIZu7e43gVvz9S?n`=kf8P*wZ^ZKUK)cwN}NMK<0e3 z^PGE{#&7`hF}!ouDn(`Lg?zDoQEJjT^$i6b(1)fsB>diyLtIYq=L|}jC6C;{g(u&< ze03)G@p&9+Fkd^+lcnlK(INM?ymL?t2^5VyyB|ze_GxJT?E7sbT1@O!>wF^TMzSjA z(05t8f5!wLzpmF;^*CV(#KSW$42-gr6czEsPJS%If;VH{CkJ%xP`tKow)|_DJzM*V zHT`qjXX|Q}+l&lQ-*k`x^$oD)7zlqi-*(dU;>Ay@Ha(Jl1_T)jqx`rv(kfA)ud{%q z^tRqtppjGEtT!=x*nP**NdfygMW3}ce({yyVFepbQSd1<1Ab9#opauZw9l&U<9{?i zyrwH7;Nwo;m!}!+6EWF#W-7s9iXUHIY5!DMrl6dChR`4MsS8+2V&rL|pLY5V&1c*1 zxo|7Y;wufD(#|2Iw8mVlTwJ;fy)gK_k<6Za{bTchI>BQxhD_B-v$5DcYjy6KGiJ-{ z*Vz?#dW&`{+V=5wKB#3|>Tb&^k%w1LtI9v3nR2TjIxKJ3#OD+kgR+QwPnLXoh5ASctwANS6Jcb_*K+VZTSmWc zXA~lF5O{XK`_t@(<9L{zXd+o`0c<%d0uRQy0?pD zb<&kMi+j zZyT`|{6A!i%cr(GUO%Dfwtds|1)K<0#Mr^U@Z z0ZsW%?vmbn<-FE{aLZlpZ55TB-sF^(-W--!!l4(1sVw*pI1^WE(&N&vpRb(SslN0~ zq!d-C?&0U9L*0sT-Nn2i`$hYFk=e{;Md=99XFPUA4g3+?9rc{nBCGh1X2>E!z z4$O@dc^R$stj2MQMe9}ck|v)HdfI>#6w`5Ofod8^fuklT?@XAro3az?wCJ4HEa=i` ze@*y!Gxk03mD$;y4S4k0y{Pk*S3QW{LUni ze57E5n&+(;$s=O2o;~`1ugRCJ^HvG#)^4JZPH1JsL4!5UDf9o#NsOtt3S0;5S&_3@ zPw#cmrnQ@?BWm|9cq6AuwvV@ESTJ^U8E&qjCCKh4z@V!0n3u5d$5lS5)ui2nxC7_! zUDd}Uu1OXT()m;g+E#RqG!d#`Nqt01+!sn{OCoos*L}Hj@Y!=O_1X6Qh|Y(nkXG$) zcd30bgcd%0&zr+ktOKn2!rhjlYS!sw@y${hL|9VHtJ%}*yRSC;fjm2=jMMI9AskG} z%2!)#1~k$L1_R{DF;~};(7qnn8QPJW&LL;lqTg~kf3t}p-cVn7$)9d7{bIml*srkW zI}CHCetw}Ep5LeOqw4jt`fYNxrBlC*+d0E`DFu3x=X(~+a(XKB2S?IXc?X{x8k(tp5EalZnNpGt>6YiVJS3Al!LDO(kB~EKeq-Yx9=5hw53#8CnaK9FY zPyt&mq}3>f5?kZsyO;+?{I4IPtMyRNxpYoK!UK)Iog7!q?>y~svSIq3b{Qr&M1Bj@ z2SvVV|DIgUy0){GdbA66u&8tMd$wPtR|Jx+t$GD%l{DWp_acG(GH&V<+~V-R!I_d` z5v_-Pn@(%-%`Mz@KIR(DSwsanNv&@eZI2G!4p*E5iqNV-HBLXE4_84C;DCTXAhAaJG%dv(I z(W2lpek%Ty{^gKw$eEvy4hbK)x$;#b;7pufIUC}=dpKB|;2#K?^8fsZ338{E zL++2UZBH;iy!QzeKYhrWEswOqh_}Y<98$Z108gnrSiT{V2k}T{$y#HK$k$lU@Yy}A z9SosylJo4~oQK8r2A+j?^QsNkWd0=={!pEd>rvyfiCIdc)DW6~)5|B-lT$~9Zisl6 zOkIC#X#n!lZSBYQ&el$Wr4v>>W!)n@UtjD{b@ybTmAPJuYJd zRC)QqzSX|Cz0&3d{zv#eNQs)2%QMHwUCC$`+@F$Td2GtL6^D86*Ychc<~>-TajS5; zF|110!U^DbBk}_janE^Pacr+8eGF2tBV5qT8yTKecD6io|lho;408O_1reFThgI<2NUG{g<088Q}~zZ z1acpb0LB5^zC`2miP_Unz)g2-3a1{Mr=BZurnKz(C=3kw9j? zHZCCF@+*C}4sraGG|UED?*UJG60uW21)Y|8J6SLDLbzFm7arI03^f^vpI-=?+ ztM8b1kp)wG$dEJ`bRlxOzDKa2g`a2YXg9^?W=z+V2>?;Dl5Xe-5eOJ9L#((qDVF+o zo;F_lQh$GHrb@#onh?r5fzqxsX6}8zH|`m_oS7n*4isg0F_gBN=$De?Wq`y>bkHN% zal7@v!^M?r`_G$CTkndwuDAzbD!r9`$>HNN;2S8=2c2HK6`tg@GC#QJIY5rabld4r zxX6ZzFi5<`FJ!;XFjtF8I7x6vTi(|CYOw^#n zqLNiQ_?Bol%A7w?+V(?5_TZM^TKJ@nNdelP6MdBWT>f-P|6%(dvY%J>!l4Q4oyfl_ zgF-PI?{4^0!pqhh$xZe{ro?9Bo;<rkB$x;+}7gNRJaGyy7y5@S7o1(y| zLWq>-a71V=2cEg@(#iSc>a0`n%_TsyLnddKQ2#M$Odjjq_&g{O&@pVl|Jcr;WPgqj z=%WLF{;b@&d{;4W+yZ`8Q);V_@o>=F&ig$^SBtmzx*q>vx_KH%tMaR|1r^2Nc-Gdt zH-ArX1GqRaP~A7HuX`_FbvCwM#vGhJW5R8md~5W=E59Qk@G-1AzO)t*1ZBC6N2miS zYC?v=t)pC-)v}fxU{fGI`i!z%JO+>>gUG>hz9MC#y+1LgzgiQ4Jdn8YjYwS{ptX#n z(|XNTqY{;!y)p!EBD(5YM`prnS07=5Wfb`!;7C|Qf2j?Z`N*I02Ikf}?+%89!sZ7* zrCgP8j-iA1m&|1EZqW-hD9wzrj2yLF*$n8Q5lFq%8nj?_H^# z6=`d#g!%aD%x|&PNqH=dvp}U2@TfxobmB5s`^&J-YzTbD(Xm_OZ9I*1X)1|A#f~t-3~J7$l+n=_r(O#*4%8+s z?rxRNcb-a-x?E=^idGxxnjo1hcamVkU_uk{{je&CiZr^-gO4g)O7CCeq`8;$hpL`j z*B)A@yWJ=vb=R?RXvf5J`?QSP<9nQ$d$4}vz<+C^Ix1sf;H+s6lW8oS4;L3cIDt~c z?6EOJ%f5@b4Zy&GJVD05@immpARfn(XenEGm)GAgb`X^VF|B=9nS+Q7%J%FVVU$Xs zG&(N!Z6&l7F%niWScUlscDKAw(N!X9-obF~;e5fH39^iZWt)Uz&1*vEuf zkj#$l<%6O(aqUX~d13{46-&Yj-$12sJ1#s51Z%=Nq!AHT?S+YTGv-CSJn z-)LfsWA9fCQ~<{gCuGq5p14S~M2~FnFkndLc$)_#^5#U%n zKmPT$1uU;dG* zuapsB0SfEzSU)3?#Yk{W{Z8%(=#5*c74ZbCZkBYzIl%La%Lr4#Ye`_y*019YGTaJc z&@gjVNd%x^%e-B;!8Y)Z#V_YZpg^4}iy7<9Uti72+xFF1yuBP31>h#ZDA`+NO=n_9 zs_8)OqpjW>ml>y&zus_4mTw4T10LVL1$f0kIx`Vyc$-(+l;js zN+NKlaEx)~0H%a_QTlg}eBWIVbR-IUE^if(*IQtjvjCG%}Y|6RxOLfK7?qZ&cnZ$E91pwD^ z7V~@u#0aTMh_~ms|E^jXno@XuVJqAvsoD*{zJcEQtH&M4aq~_+F#|ufETZe^T1d;< zF#Y@l#ILd3A42Tn&A3gh>O)xNE7Mls9=B(u<3$q5hX~FM1fJT;sP98!c9g3yiqeU` zXsJps$|_g6${&-s*k48C)}!K@?s4>nGzGOy=PO_EJ?ova0hjV|kh5@&TwlRp54Lik z*W(Tg~O;vcmj-_9BR<9DmvO#DQ;R2EC!Pa#WnPKMU>)06NT`M_9w_yT|qu ziYupxJ`I%;k`BF{Y^Za`?FC9t(33~TpixUNE5TO9n8QG9FI1w8<*=cd)SAG>0AHWo+Xn^Wz!qMmla@N-p!=U@=cXm8@yDa z{Fs|xr|DII+XL|)3k`oHo`id%uYs?bh+N}Ag!$~`c_mbAkMY=gH~)_X8F-O{qomHb zv$%OHJnFW@dMA;FzI=`ux3ND2P}^HtzRKi<$GZEPyS?>)vuszDn1VaI+lhus#9ZymA-v`(T`e`?q}>El z&sYfX67R=ns5(=1hVMM{AVlt<^jAJo0}(uuZy!RF7Fy%&-FYBQ!MIA$M*uK#dJ}N? z-{kx+84>S%r)%?v9qOjdeB=1yaMioWa^o&17~iKJ>mS3#$XfNa7_PzF;|A*V5kmvm z`bAS3|8~pJ>{;F}%h_TBVKewCCu$$|&iWp~cHpCq8mq1}|ITd%Pi@^H!(!A0A>#)5 zf&BzGW$>ki`05V-Zu&~@4~xW*;5!<^38_T_clM%(M>0iA(U*_E*y5#GiK;GE1;88H zHV`eYyN+-FCF_@?F##aCv%&^VsE>@#4~c0&kydZ}Ee|j(UV0_nc;0K*^CKF`_5Sc!Dt76X7<@6=WBTQMe=U%SzCn>*9F@Ul; zz`a3UW4LTf0G2u1yyM=G7Vu8a=oTR2EhoME1VnNFmO)rL^UN!^m`|$|dmoTO;MB+O zY1s*!=U}OA7?bF%AB^@~wt=4-{1gIqdx=;sCJFg;gvwzfdC|BVyPIwbEADr6Q6l+V zxdmM8MWy}&eHequ;E2kXcf3xe;ah+nTRTj>}Rq91)+A{`PDD9 z>#?|?E#=a7D;7kvjy?>xpK}%;s2j~mtQkxyohHt!IZEl&ol#s+8Y4~h)dOAB9*B^R z&Uc978A4fKX5cV9DIvDr)3B}^eO4dnsXZ(ACyJAY!1?9iN#NX> za`;d6QuW{c@E@zSCxCeRW?gZ+U>z0o>-qtISpV^{&A`J<*1x)6*AHri1XOkV1qxMr zXkAP>a9s=aDB|0>x-&KUs5O8O*XTRAZ)IIh8A$7EobOz2qVZ7xdRKot5SUFHECN>0 zr~H2!-{CY8?}!h5azX)m>tW!>dk~h4xbr&u!wZ;XW!Y4MrL^`Tp?iQUp56R+FMj?i zpdrr$XmQP?_g9E_J${h{gM4xaIRDoFLga7%LIf!uZ`QDSftQCM7W;f)8K!=V{{n;HCjtm zU%YswfgW&o527#X_Tj^aAfy?Mat+#5b&`E;{fQ8dsZM-B~}0k~W*1C82D z419EEVP0wQRUW{_eem?3=eClBXe}z_$HFw;7T6R`=~xfwJAG*hU2ksNwXO~VINKk6 zVz&`>kbEK%pbiGebl>7YU=nlY$dcO(u4VnT*J<(o;$UeHNl|5162SJnFY`LX&UuiC z^rm;e=?fHzH~oX3%QQvM zyjACrz>l#kiv-f!xw*;(zlwUk7&XvPPPqZhldz-q@7a@L%?sKM*WcvxJz!#r&WZfj zYo7uIRvy5;Z8O%lZ*IUpzufr;s%85>eo%?R-mCyiwzU~=9<<-Ybyp?NYr1=e;j;FD?`JglgXRBu6sN#c;+R{I#hn+sf$@|d@D}w1XG?dD z`46Uy(e=v^AVGuZ`w%zUt5KQNQ(dbb z5IMTx*O1GBAybqF37`LNma7br6!`_G=Fwl1_ARb@5ce4<-u%}dkuU2QZP&!fsj!97 z<_z0t9Y@dH5z5V63wYb46SEG;g9H5vGRAm)M#aP83_O$Aq1BAP(u5Z`Fi4c9DLKo3 zY1#v{D4bnyN?0x`WfWWWXi5PmetY;aVIA*|BS68LX`H3&>$!V!0VKrMGKvYzu-|*1 zsLi3z7gIAMi%pF5XE?7lItXKFTC+3hlV@k_^QI*Ud%a`Fs+m(b4GPX6F?n9^0Qva^ z&jzM5VAd!nQSaot}?r89EHoRsfEJ;l4=vY;T{l?R^e0L@7dJ5vgKFO+h zrS^08P9WX+%C30Z=zQkmEG&M!t9$F%Z616Vsf!IUYMv!7kd$;W3|Rx2 z>!eSRqI?~~goUn!cA=DbP}A7fkegYnv5<&o*2fsvIXjM2;xb);2MdRaJUChcRR zNCig)?;)=T8+v;a1R}Wa@+4o0Nee$_e|rZIA&(;}zqJz^^jR~%eHh9q>yZSw%<-hH zOJQNU*3TenlE-v*@VXcBS#KD_K`p7o0~^JKZW+UlBzSc7G}CM9bi6%iLlMiRDykW~ z(^Ia0iBQY#sWazEGSjy*o0GA@Uumurtn4cNKt6GfQ&r$A21~yUsPZ;w36Z@#Gvf!P z5)$1zaCC9+2g`-p!`l>D21z{eV8UiXTNo{^y7SvSBQ!wB=Xx$XDm0`{B=DP{2`tU! zqR6x!KAIqQwVeI7*&+tz>)>BMHG zS&hP!*3cX1fu8`15@PE)`q+;c|2h6(X?85oSduU%XZMPt^{#ZTKd2xdzH`^>?-YFDz<} zqpiSeL7Pw0-N#XA=U$ZWxGK-kgG?Dytzq$A)f%0SR>+9ZKv;mXl`)ekH6uQsIMgQ_ zq#Q>1KI`PV5L=C!PQ_LD6}@NMiWkZ6acwYLne8kowm(iwKhLzAS#dkYBD$^(xW4}& z|D7vLvJtk8rl{LiU-s1jw!CBw*PY6WkWAmbUqHd_CLahaBwK%Uh+VCw0yI$`oQ$M6 zeR}g>lk71-P+qLq#$mkVXKHRn4lHMbQ%s;Ybd1Hl**2kRieG@BGdu4oZ{_cKF8SD+ z9iT@6Pyx)Gb)uyy0L*hLJ%Y7Pkpnfpku}S(!qi^>RvUmtgkW_ui;Wj=V^Z)bteH?> zBd6oud+$^zF1$`}@0%HQ8Cl)Av=lWlr@m7FTeA-*E#Hh-{}HCs{85Y0S;dLI!qRm* zkANnvHm8yDA3Jz%AyH*95pQA8GoG!_s(i?>$Jd5ZKJR<>@tXBY#HU*bt0TsCQUY4k z9=i*?T0*XuyOS=xbVxv*4;dVf7q8q{lRMfVlQ(ezBFyTw*a)9C|M%g)0zr30M-v~8R0TAdoN zzKK#w$s@r8!V^@=X>saD&k6%uqXq3zaYXsICSvF`ok9aBaMw>kN<+6aPh5<#qNQ}M z0nB>OMW_9+mJX9f?t0_KQ3HWtfjI5qNJ22kA0tUUy#&z3nK=pu-5anz=Fm_>t$s{` z$IORyM-Vdu5H+heQW3$+?eHBFDBp-fwS^HC5-2#Tc_~LPcAQdOy{biZo z=USedNdd((>pky12<}1c5JorsWdi&w%p*{dT-tw9jQI^=ob43=t!6)~54qRY{RMx! z=3YoP*hS=&lcet=@q+HM3>QN#FdLBsFt(s}d0k)&99* zq?EI$b8@8dsezp{%HGUf1819-3;ohd43vp?R=(Z~PiyRa(KWSO#PhT*OFWvK4T`It zmL~&9hT;VD1Be&2a4Hk%uLhQ>7uq{iM7JrCE}b$uld%$ST+FvI`umMmomcyKMDIOB zX2b|f4l?#hhmKTlEXZB4o{;o5k^Nj%wSyj?Qgo0spmhv`dK(vf&CBZPxJmIIPt{>W z3zNBvG(s+Y?rfoX+!wA~Qw>AKTgw&sx+-IQYJ!ix$1XS725_W10{>ODaC9XO&k%(x0sSwQbFJB(x{Ocsb`pYl7z?L)V!PjsXk{W6 zX5^u&m^=C2iY4TvN~^XdX#k^ifohD!joc*Ee15(QX@&gwK)<1@YDwI%FAG9vPBSm8mBjkWSzT)T~u zjlbkMT3)`T5}9;Of2j+GxXFtToU*ifN3$7Ql#7h4(N2wyjhY!Vj3h?mCxr%&o)`w2 zk%~itU6A0a3R&Txnjo>C_%qDre(>cz$7UNUy7e9h(X>`V?kzf|~c?_?WynvXDXV1MS zp}mriL2Ev#97LN=9k z<>(BpIIU1V*9Jfx^;g<5!wgX~Gb`cU=u@P#KXWHy$Rz3Mr@?0#Wmk4Rw9ARVcF(-0 zvI)eq^ByUxr?phQj~+DLfmyW=PPJB|^zboi}f{LszxF~?^Zttj6oNsP9wk}{I7 zR^x)zG;hVUCC`qO-?yTRGtCJGhU5r(jifVAcxM<`lZJ~N-sG81in*BzMDVOIMeK>M zZQ*PS-IYOf>ikV^aocY1>6qol?R2Fkykz+?zQ>SG^I`j6QQuqwS<*Z~yy~elVfiX9 zzQ*#4Tam#2TG)Mnz}T*9MN6;W&DFgLmi_|1#ZzZQU;U%b?Tc#2pGPdR^NfPDT(j!h z|7q^aOgR#8Vq@Le>|L*5`{&?Si-mA~2Kg@NV*Lj}TaUSdUI1bf_Ga7%k__Xz^ zgx~`*%HMX91Aj=31vK2O`?%Z>aHJd6Z{qUR%QSPq8(d`y&9=)Blg#QD<#dnaEQHg$ zL24r{v2#KS2H`JL+@n#WI#jH+3=wZV&bIU)-QEHODSAwo-F9pPU@eNe^?#Ztska$+ zKvgnk7$dh5C%r`eAl2SMSLc5-7av)z{jsg0=h|M*DCxQFm%cmXdHk_I@q4lM zrBiOTj%MY>4fq+$A+t#R>=z;Q?C0a+)(&&DJk3gcO`Ym1%B=-NB1f!R zgNP_1@zacfnDfr{g!#6>$Z7Q*5=zjXrV5}$K#I-XU^!)@d}oxTH!}k5ZE_i7IMswl z#qcVIAI@fR3JrF;xsxQIb^RDt_jY0u2FIXJeHxtD2>Rufp>uyP^hyi8-vNMlrO6gD zJ1*+2yxz@F8n266=UuwV#Zwu)b*;mvQp${AAW9OwoYazph_OfOmRcAq zi-oSkVUa;!`^^SR@P?&(!kl$aY4Zx`zCG^^lE5K2k0vB)a5}|`Kd!i62)Y68#LZ-j zS#lSec$2PfKho=ml7b}zg16B2tWa_2bl~~sVq)bpd;5xK@3~s1vhcNYOG8Di7DPRQE32-N+WDFsRRzA~QfYthz(PP{=B8xdbk^+{?+@MD1KRxH?evjC3%dMsO$UmxUko{s3Q!<8Wy7$`q5T z)Iw_yvXd;+vAYvvi@D*&$uaNp17H?opHG5N&FtbnMdZZ9B*zS)4g)(l(&AM$rI#nxHg~>c)@5gI9(gPQlC}u4j zXQTjyA*w0yB(xgg*$1&{)p76Flz=&dmrqa?hMwQ%uvFWLmOBDCM$V+ca`MDri2OglcfI<@<5-O!=isb>Kl7Gh24ri`Zq%esyuNkcAj zwO>{`Y*Hg>i3vvFCyDsVMfj#rZQmAt<{S_`e;EO2u~}cc#KZ(^2np3dIh3|9Gu$4Q>GYWm6!XHu_!^gsaGAJG-876@^0%4eY^-5#HEK}Y9e5(M2!bT zDRv<#0FUxv%WXnxnsDec78CsBlc!@d@g^6V{0$O?R1A-s)n;JtNqburCTB|B= zmx4T{?l~&JfpWcrTL9xT`+*>H#c{yJ(I&?$E9Wrl488t*RBv$QYLwvRmbs`9l%&w0 zygRIz_1)B0tDkppv*YwHPiI)8cBqA?l83S`#hf)7ED8Mt)GbV0p0;KTN5su+7?E@F z2Rgn2$KjJh>aDSZ zNQtr>KMs^i!)NsaecXUuJH8trWUK3u(p}C=k-YqcZc;C}Yt*c;hc90np4d1kJSe}I zP{lncvJ^@8cLiMiY_n|p`)m`=?m3=M06M-}5yrJDjf=DL1h#_;Y=?kt*T<6<)7+O^*0Nb?61c3X zXY@U+t4s{~;BTkxSR}HR;|!>^|DDti(q3z6SA!LSgql)r3?$0Cb2O5QGu1 z4Kb>iZiz-phaWEgG7%!7g3BUA((U=Ae|yAJ4*if$2abOe;Kq9>ez`OePW4Q)oOywj zzM&!sp7kIct}e{GSBZ)%4YYN5<8|q3VpCp8y8qP^l9(I4c^XzRWjK(giRGSDE{-p3 ziIsN}23kE{-No(H{gaE;PlVj*?pn8k`ZnNFejl2PA|EY8#qVtcQuOdU3v)9gP9$Lc zw4@D4F5^~5PC&)Q&PixSIaZ5V#!vxV7ftN4vhvre*T-_L-oMH9d=^6TI#=vFxDbFn zHp8FX#0!MDfQ0o%Y?pBYR(aKuD;jwcSnap2^`6T)tggJnTQs*cx1Qgk_M9bZI`Sj=nEj}X^ zYyMrwKA$WFBkP4TNU>X$7xqabMyM+)X1Wd$WH9~?rZV*PYxlJ3+D;7XF__Yd68@TpyX z$7W?PfNg~1Wpf(2fYxB=a9OE3zM<7UTX8uu2w@v`z< z@~ym2qF7!U^S!C;wf!kj#Jo7{bRK%N$(!C@c?HR}UZ>*>%#^v;US z@dML7>)B6f@oH*y7^xXXp!Y=Uugpw z2Y|++A+_FF#bs&IKqL6W?8O{E*w0ODUN<6L^i=Dvup>&r#;1t<#O5A&Pj(+qdSHf{ z-84^UWBG)1j3NjnakvGd`0&%nB#!-YDA%3H1&@HtU$}1u!0O$))`z@*p$vp73NnC= z^vjTLgfzEV_2+}_KZ9B50ZiR>T?#LEe|%{~3LJz|d#usiGh2k{76Y8`0907_uHW5c zfI6HOh2CD`*G|Ox+Dl!)oE3u!eYk61NWMbpn}7q3CpU8*f_T}ZKDf-@yrMh|o@Z() zI=MGaGm{9IJxUGg9yN3W{0rgU$T`!FmXjw(n+YIwenf@s#900V0|RW z>G;#!l|%KJWGpiT{=vkwvMdENfg?b35tMJwzA?qOM+WkNg z_DsT_;(NWTh}X#r%!w&yq!qHsg-bFwkN1PslyX_9=3q%(IRsfEeS3W|gPT}bwqaxg z<{icuP;^jN5YTuy2Zy-BzjgRO8|6U^+SB?`b! z1Y~$8UnnII%S`$d!)id#)n79?F1fMa*gi$a$@8vD39|0&z|Oa_Xt)Hb$m4Q2vdqxW z{^(41;;k$TQHK%dypvGbr#a1T1vHA@5Ec)~Mb{tP&c^bFvr_{Uc_V2$S$jmo+fL)O zP6TfGzi1=%I?HW0f{U0yBOxdOq2(-Bt_aiMw|dsUyx-!3tRqlg$#%J}f=-K0n@bx( zHqUAoV*p~ODIHb$Ca*yFjfS|bdZrMTqwFFJ!)W{&SM5kRIN#2ID5lYCpTud$lB2wD zwAK|eh$}8cUCp~y!U4Jc-Yudogb#a;mM^Q0#!)g8gG`3Ye(O!mySaJUp8>d<8n=`_ zrsmpx(N;RnKQ>_b-TD~Nx3$Y`o8_@lyTl-4x?#00)vlN6HGBbRl@W2OhPBeE;_n`* z(;}uz*Q-{c3MU1b0V#q&`!}{$)*P`p;JP(BkbJ;K^Mdtk;vJcc{`^CS-sZ*ynbOS= zn@fggS;}Iad%<_(|N8Kg7}m2f^h?I_?||}g&MiGTiP+OBqZ>vIBNxvHRF>DiF?UhD z3)X2deoih%->+2oEl#&y{4=KR1~~0qlv`}1a7{v!2XD+I6zTN51g z9oJZW%VBV$+I0~@PP!O(q~9JPnGG{^FM2801^B@b^@4rZiL7UwcnC_>xSO}_=|hFlb?fDH8Xv;DDm&; z8Q>W}Eb<#HATCV)hX}OjH5MIG;^>g;0CFM44 zWMSG9I7PqLLMQ194z3-~+y+;cFhm9O4Hh0#&jWOu$ns>NbwvOiB2E$ThTETnZvMCT zm{XeUQUR=}z1gc6 z;FxRo_4?X>8y09ZY0F=XsiCF~OXZ?6QqxwUwgSw`wz%Dq>MPjcN5${T0T~0DV|?3h z?Lv{0`Hw_I&U=7F7xrUBRwt;7-gRZuISpDw2m5M4>ph~t9JNPFYI(tSv7Vvy7g+Ni zt7RkdXYKz;@iph%f-=f^-ui)o0$u=HmE6JY9~cQ#uJO^Q3?K%<7zp9ozjlk=@#}Xh zI-n_?L#pyMa^5A^YvO=P&kUrY0a%xFbsGvn zu^v8+jC`A6Rwj#hv75qD)34CXayg00@#8A41NTM>M?xK zGsE}ah6A!5Sbad(-3?MJP;k=;Jl4@+sAnS`f1_ zxP^ft$;IBCQ!K2s^+DB>hn>P@CI!8(-TU`EIR|`=tX?&J=8#sEy~v5%dSc8XGI~{$ z-r6S~d(PioPp5@DdTY3U&vF6L1P%Hw>O-sxT6nz;dD6SARil_Of|_1!(QO~>3=R2% zuowIc5T=%Fk5@sO$VKhFbqisdjWbYd)9*jzXCqKOd-AU>T4h@1)>7C@JRo#A- z66@?tztFli&vV6Av=lpg!QsLie2c0)i(keuK<}tR+OW`|pZ9YIFnoU<;>dWQ;oO?U zLm&Coar3HJ&EaLG(m41~wN>}H=VU#`bAa&qvWCHBgobteJSXXukG-nh)M`~}nkb-a zTxK^8O=Uwx0SKVFEcBeDF!rP1z1HYJ?awCGZBs-c_k)|?GXj4if8TRo*}Z7nb@Z|jsip9*P;8#- z@bS(TXJFlpjq_z6oNIl74cz@|LsjQcJfFd!W^_+?c6);72bs1hL8yD`>nS0I!5xk9 zv7RU5AMv9_pv16w(O}I8%D^6C>7k_^BE4`Q)MhT7a5BklKmEgA)c0whtS2He6R=^` zaWz&dwio?X`O!eMzwLq#Pyl{H5h7LAFl@Ju`YZ#KIX=QhgSJ%svbEj2L}B73TF&g+ zBVQ%N>?D7$q;DHbGyEfTO2PG#<%1)A>dCe9Hp{azg(FBY_sr%L2IBCbJPd`1NJ#kH zDr)bh{K7O!iiAA-A^kk&<-%$YoF>i>pTg}{JXL(3Tm1qCXX|74$l6od!K%Y62Mkmd z29s|2%f^mJXf#`DI6A1Pa;YF716|#Uo+|6xwuvVGISDSWt{5`-FmD+jyg^pe?oeVr zIu;+;{*R9X@Arb*`@B?L`#zqs;r$N49dZsSv57HK&zEqS^BNQ;^4TYG}w=J?GxC578h!0P!sU2XGIuoI}L zrlW@*gq7spa7tyK7?KvcbG+@DXN*Xue{M#_7qdCp02|qRykW>G&>ch?)oN+sTE$w6 zAACV0pGJ|1(kDOruJrkU6lZUXzU?dowJ1-sE#O^ zZ$1n#uvwoz_0Z2J!aiWbJAL8_@0-Z=i&y8)Zy>1k#;VImG_Mrtw&hB?N}>Ca{h5N9-bW3Z+}74<>@$c%)Ym zT8gjaHeap^V9A%9*|WC7(=lyn0CJDT_IQ>er|tcpiAQ`Q%F-)8DrX!x;{M6My!}af{r-XNTH;NxVhihwD z(TTak*JY*E_SPhAXlXuWyEy7FXkE;R8P>j~wi5J5G|O(^^##j2aA z?8b`hs_-E0k-3}BSftmMEv{2+%a+d?uU`)+dAc*EiW*#?IYF^yPOTJ#$W6VSTd+Dw zyvlF{!D!xT&h)h1s}ceBhN*?m1%UV*plHAOon6vGiVmB_3pb!}de z?CjTQ=+W@Zp~5xScV&MaD>}rPrHe6qxpe9#H)dM#C3af*<5c{D2F%Kc1LZi~<2hJCQ{ zM;alc>0G{uk(~;~uMopJ!N_yYiN4s0 z3yP`75X0%4mv-kHQXo!QN#01P540YDax{Lc=4xLdEOq(WoRl~V?b*fNmyOMt!9ykX zRNH1)PLpgu_j4D8yrVBB&qXIT;n8j?`~noQLv6+HSjukug948ZQI*50jC`J>et!%1ah41B2^Ok3Ihb{|+m zx2)j4B>fDC?%K9-<3;BX%NC5;IKn#bsz_RTYO<+KU&)lZm-QkDWhZJuPAj!XOyld> z8e{dhbEUg~d=itDR#;1Q#<@l9VI}N(D1GRmw|gD}lS4B1;uBWaGzyke>e*joY}1lU z*S)D`UZ3Wd!mYAt!>1W>Cnl{>E@LVe)&I0p#eu7^N29b~*~~jYQEp`Zj4^8-sdO6l zi76{k33@ZQS)gx&xnL%T+*O9u)G|A9a83Jh5yPu@!zYPo14l##X`Nnb80;B7+RKzn z81|__g$~Vz<~M4RhmS6~k{UYJ>T6zH2=JQAazq?D&7fYGVAO1=Eh6MMg4#o?ayCOo z8hjjP)joR&C)|22aNsgqcg`)nN!hNU9Z3tIZd+NKU2#l2q;;i!d6BTY4bKz5Ue=KLZg4#g{f$hq%H!p@DVj3zqwhYTS;?C|R%92)#XM5|9@ARtw6HlAlL zdS+!cso99Z#SuoIPU_9~LYfJD%oC6sl5s^13CNjdg4f>)hOR0HNW7~JwDzfe;#?U> z2vZ%JZ4T$`aR1adVY#%-r^2s4C{-NOe_CLAv#Gi8g2dY|-@3)p8}p+wN(4(*FPKe@ zQuR!i9+R3B$69BmJuAO=4uF}BQIy0p&*xeVjKn2o{p%c~{)YpQ9hmQI@ zcR>1ub3H| zWhiCs$ne>f@9LM9UYM+Hx8c5ymj)&0Xa?a{;Im=!272HKXuq1$C)9jq`41 zKiKfn-NZ~(1(j6Q_P$)R!4yWp#P!)mAS8mDZ$e7nhJ&m39*7>s?Mpcjuu;_`)E@NC zuzx1=8pnQQX3|~#_MQ^UhI^H!T{NH^5VgR*0EzZ~A`7%~Ay2H$U3FFf0e(*L5n;2E zWTp_)*$H=CC7Z1(beo@oJ;=<)?|OUl7NzeMzA*(v=)U@cA6c+cV#Pe=Xgwj(YHC(= z;)i$~p}!&t1a%lLHYc_nQyx!cXqi#^kPXqo?fU_)p$tC@z53VV#dVwP$?q}DDj8PS zkhTySN2Y~?-q=%`BA`{Jh&`3E=jzr4{U1$VgC;Z-s|CmfMB;{qEfQXs+CRcX2i#bK zskE3aAxLyYKb3{HZDqO~W<1 z^T1jCTec-uXp&PB*xV#g!hoy~;}qT7ffj)32bOmW*+(U#Ck3*Id`b8+#i`ZakTl=Nlk$__}*Aczjq|brJ2|G z#&nP?1@{7zeH)(VO3A1!EWy?F2j01^n#T9`ZXe{l2__f=EypjqY%^ge0AH_?Yl=R_ z_Fw!KZQHKLOvitq26--CCzsruhDxCNo7aF+o7*ckQdZLuk7Bf5;g#$yGCc-iBOV+l zxSoBht!Ov++FRIMo$8d-5b1XFkp5`iZ<)If#b%MpC3A1Yyv99$g)qvdnejI zKYgkFkEACdsRhF;CmJwHr>`!$eqgzbaQiO%Q~Ht$q;P^}#Kdo!>WrQm_dP+!wbxK3U}QxMfFGQ^99lVE!aG8WbstEP#*HTlW3>A2~Vf3q{lPUH&x%osBhwS z631-OhWCud*7gi@1wR z+-t1;s`YT*UBzDNXu5$7!=tVatXy#WdEZ$OeXi1BqiLTOfkB*0@`h4Qv{L$jFiO_v zXB`tm2T$EcZ^wlQA+(C=cYLFzs`kg06~`r0XW^#KU#fS!cl*cJ0Z!$scfq@--j{nB zaYxK9_-a>!-#}doI>W9@ho8=@&9 z7#x1F%wSV>$Q3bN^#*?cRs2{scu{G`MfHCiJK$Ak;(~(jIqV*vh#LIiO6kb+^^Jgl z?H)HTa`hK+`#ZukSFT$jpk}eKOoPZ`rmfy^!w;a0!W8*Lxt4$rnU_PS;D1RF@{s|b zWEhS(}O#FZgf*UvdIUZjXws_ z4=cLlj3`|*y|?2c*Z-_8|K;JOmR^WHp2h8^%avWQKltdmGe+p`t=Bv;|mI4Ep5wuAUV9oi^0CXLaEP5fiexC%X)71xSukq%i!jBk>u17XPE(_efELT!H`FRYk+% z)|T5D*k(k!b{%eKk}iXTR6kcv+Wk zeWAnCOf7hs2@}-dwd$HauX(7h;bV(EB$t`h4s!IFXX3sw32h%Www-Hd_bINCSzL~@x=!oUsH5u&jAX~%i~ z+PeWaznoYJ@+LTkd@kB|%ldqgE^pdQJ6uxVCUiISZN0zItak&pF9K3s5)hwx$|#D( z59GI`XT0Hzf_|4AU%>`$1%}2(H!mY7oiDB#!7G%0XZf0yo$DHc46G2hK@<^ zUKS3AH$#WkjqD~ZFTj9lg;fSc%7y zu|D!?^Nyf~297hK;sPo~cbXc=mdAfm;;qM9dR?s4y#z%kGk0sV>?vxr0eS8nq5j@u zl&%OnKgjKtd0T|`o%4XLeQ!hU(Y_WnSR}3WlF9N)sPIW!3bK`!5?_d$yO~rC<4k)s z6fN+y>W0?iZ}N6rb`NYc%lk_i!x&ReS|6Mh?K-}jRZ`P50LJN5A2M;ts&Rq*`E=^y z8O7?2pxqokXJ5c_w*5^28*Gfk4vHrxfS{T|$}d61gkBvGbVHL&!3wKOV?R8%2j=$* zbcb^yIU+K76SYU$#e0nUnx$45D*@CRiS$6_qrw*ov#pZe8oIpFiG|1L z+t_|HzhG4D?NSndy2odQ<+ixKuX9jP-fZIptV@tCy|(ga=Z7q6di(Z1)@=#7Fu!O3 z^mq7g+OWMj*(LT|y&kIYWu!o9T3J4!w+JYc7w%mZx_*s1XIW+GM>raL+o+?5jkl1{ z9`rQj;80U|3Cjum3w`|5w1D3&+x~|~{nCelzXB?PLm%Mz{`tA8E=O`L9AoPZ?28xu z4|V0|-`}SET@6;6;>O5$O94qqpc!1!JTL)0*r-uRoe%*gXl>X_VOaD>j_j_gP0dr4%!afZ9CicuI=nLJn*}#VupQ*O&s`{9t5VFqK_doZ^_~!*TP3c4V5Ica1!(-OEg~!7)wn!ZLDW z@7B{A4oy$m&fnrg-#cJSnR=(7fzpXeuctKD^Cz=c7Py1 z5Ly6K!}|H^SN3i9b6)CxOFy~pQUw^G$mDL(F`*pP}FA5<}}$=<+z^%~ou`!i+%wD%!(`?j`%_l)9@V-uonIhKFV)RYkR#1@@* z2Lh|_f#pNyWnoOlu>5$&CZ>F$ zMNlJhzfMS)a_X$S#dl-2BFOK23byBrqGMSdT?g#Fd}}U2%co#%ffRacgZ@yyHOnuzjx~QyjEWS z7JPfoXiq=u47pTacSpzXf}-`YcQiVdRNv6YgXObV0L6N(XRb8{NKcRB@JfxeQq203 z_RjT>tZ4<4KIlMgWBumR3{V(-INefU(voQFu<@cAkK(Fi`f zo_wxz%AP=B@XZfk+fie-i3D7?VegX;EaSxmY$g?bAuRyjY>d z3eR}wQM=Ghn0w%O^UeH=Y^`fqPZ3y`}s~oZ5y&w;td|L@UGOmrL%#! z(ccAVMF#fG^6m#PMz)bOFRzX3kx?svQ(=qH#-9APuZ6xNt-Ua@2ALb($} z+B4(c;Pp)LA$ix#^+^Y0Noqz7KyAhp2TCQFf)h*L0`&r2{J`dv$GhaVWT(@d0ojC} zIQ{xlS?f9G{~%g%mQu+-Tv&7#!5n-qM0b9c%FlpkX)V0XoAV-;Rnbf+4&7|4<>xj3>&RZe9~tzzB-H4taKo)P!hu(u zhQ$?lN)>IwU?~C1oGX1zvVpJgt|nuX+iFG2H#t{Y&yLXqsPEQY%;`iYu*2Y+ZB30yl_BwI zR((mzfUN!>>O1=tIKa|hyBr|^^<6WQz~bwSm1mB|OA<%(W;o}Pn%jl5kZF)YbqvSI zXmZtv%)JA@CvZDtOSee+EDM`&mdu~F405Rr`NT0Y)=Gl$Vw-FXZ1~m>-TZy~g5g)7 zGEM0bfQvj*cnw0O!C4PIaWh8Mr7hx1Le=H-4OF~an#(qc4;oA@H$|i0g8cp3%`F=; z0Zb8|ubf`TkQk=UGo0S@2`**wEvxfKNlJUet^@UA?f@~_ou6{RNi(&Dn~ASP z`;mGa6Ow`Y&#Znl)9yH;?<9u;1c^npf^;w}NtVcI!b+Aw&f zuFd1W@4A?EP71b|aspt8-6s>V;&UU!;W%kjomqW@Mw&LcM}@IkWdk3X2|W7+htelM o+OrtHr7DC)bUqLIWe;y2I>_@C)ZpL6Vt-3LZKJEG%eNo?A6>`P5dZ)H literal 0 HcmV?d00001 diff --git a/docs/main/team-workflow.jpg b/docs/main/team-workflow.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5b876a4348c7bc8f59eaa5edd88cf9df2a142b18 GIT binary patch literal 314449 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

e6+X0xDHi1v(@7 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ξ*pgTd$TCMs!PlmTP#=@7XUg6BqAn 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�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

<<^@sU;>w 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

dQKs1_C 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 Td*A(As=@oCf_!N{CvW}_I^H|~ literal 0 HcmV?d00001 diff --git a/docs/team-workspaces-guide.md b/docs/team-workspaces-guide.md new file mode 100644 index 0000000..a61e791 --- /dev/null +++ b/docs/team-workspaces-guide.md @@ -0,0 +1,50 @@ +# Team 协作快速指南 + +Team 让多个 OpenClaw Lite 成员围绕同一个目标协作。选择角色模板、创建 Team,然后在团队群聊中向 Leader 描述目标即可;Leader 负责计划、派发、协调、汇总和最终交付。 + +## 适用范围 + +- Team 当前使用 **OpenClaw Lite** 运行方式。 +- 协作模式固定为 **Leader 中介协作**:任务先进入 Leader,再由 Leader 协调成员完成。 +- 模板已经包含成员职责;通常无需分别设置成员 Runtime、资源预设或协作参数。 + +## 1. 创建 Team + +1. 在左侧导航打开 **Teams**,选择创建 Team。 +2. 填写 Team 名称;按需要调整共享存储容量。 +3. 选择成员模板,并在右侧摘要确认 Leader、成员数量和运行方式。 +4. 点击 **创建**。 + +![创建 Team:填写名称、选择模板并确认摘要](./main/team-create.png) + +创建页会固定显示 OpenClaw Lite 与 Leader 中介协作。模板决定成员职责,因此不需要手动逐项选择成员资源预设。 + +## 2. 发起协作 + +进入 Team 详情页,在团队群聊中向 Leader 描述业务目标。例如:“生成一个简易的计算器 App”。 + +Leader 会根据目标制定计划、安排成员、收集交付和验收信息,并在工作完成后给出最终汇总。用户不需要逐个向成员手动派发同一任务。 + +## 3. 查看过程与进度 + +Team 详情页包含两个主要区域: + +- **团队群聊**:展示计划、任务派发、有效进度、交付、Review 和 Leader 汇总等协作过程。 +- **Execution Kanban**:展示总任务状态、当前待办、进行中的工作和已完成交付。 + +![Team 详情:群聊展示协作过程,Kanban 展示任务状态](./main/team-workflow.jpg) + +当 Kanban 显示“Leader 正在规划”“等待成员交付”或“等待 Leader 汇总”时,表示协作正在对应阶段推进。无需重复发送相同任务;如需调整目标,可在群聊中补充要求。 + +## 4. 查看交付结果 + +成员会把产物和验收结论回传给 Leader。Leader 的最终汇总会显示在团队群聊和任务详情中;需要查看产物时,可使用详情页顶部的 **文件** 标签浏览 Team 共享文件。 + +## 5. 选择合适模板 + +- **标准双成员**:适合简单任务,由 Leader 协调一名执行成员。 +- **交付三成员**:适合“实现 + 验收”的小型交付。 +- **产品探索四成员**:适合需求、设计和技术可行性分析。 +- **软件工程八成员**:适合包含产品、设计、前后端、架构、测试与代码审查的完整研发协作。 + +模板是起点。优先选择最接近目标的模板,再在群聊中补充范围、约束和验收标准。 diff --git a/docs/team-workspaces-guide_de.md b/docs/team-workspaces-guide_de.md new file mode 100644 index 0000000..5dff51d --- /dev/null +++ b/docs/team-workspaces-guide_de.md @@ -0,0 +1,50 @@ +# Team Workspace Quick Guide + +Teams lassen mehrere OpenClaw-Lite-Mitglieder an einem gemeinsamen Ziel arbeiten. Waehle eine Rollenvorlage, erstelle das Team und beschreibe das Ziel im Team-Chat fuer den Leader. Der Leader plant, delegiert, koordiniert, sammelt Ergebnisse und liefert die finale Zusammenfassung. + +## Geltungsbereich + +- Teams laufen derzeit im Modus **OpenClaw Lite**. +- Die Zusammenarbeit ist fest als **Leader-vermittelte Zusammenarbeit** definiert: Arbeit erreicht zuerst den Leader, der die Mitglieder koordiniert. +- Vorlagen enthalten bereits die Rollen der Mitglieder. In der Regel musst du nicht fuer jedes Mitglied Runtime, Ressourcenprofil oder Kollaborationsoptionen konfigurieren. + +## 1. Team erstellen + +1. Oeffne **Teams** in der Navigation und waehle das Erstellen eines Teams. +2. Gib einen Teamnamen ein und passe bei Bedarf den gemeinsamen Speicher an. +3. Waehle eine Mitgliedervorlage und pruefe Leader, Mitgliederzahl und Runtime in der Zusammenfassung. +4. Waehle **Erstellen**. + +![Team erstellen: Name eingeben, Vorlage waehlen und Zusammenfassung pruefen](./main/team-create.png) + +Die Erstellungsseite verwendet fest OpenClaw Lite und Leader-vermittelte Zusammenarbeit. Die Vorlage definiert die Aufgaben der Mitglieder; einzelne Ressourcenprofile sind nicht erforderlich. + +## 2. Zusammenarbeit starten + +Oeffne die Team-Detailseite und beschreibe das fachliche Ziel im Team-Chat fuer den Leader, zum Beispiel: „Erstelle eine einfache Rechner-App.“ + +Der Leader erstellt den Plan, weist Mitglieder zu, sammelt Ergebnisse und Review-Nachweise und veroeffentlicht die finale Zusammenfassung. Du musst nicht dieselbe Aufgabe einzeln an alle Mitglieder senden. + +## 3. Fortschritt verfolgen + +Die Team-Detailseite hat zwei zentrale Bereiche: + +- **Team-Chat** zeigt Plaene, Zuweisungen, aussagekraeftigen Fortschritt, Lieferungen, Reviews und die Leader-Zusammenfassung. +- **Execution Kanban** zeigt Gesamtaufgabenstatus, ausstehende, laufende und abgeschlossene Arbeit. + +![Team-Details: Chat zeigt Zusammenarbeit, Kanban zeigt Aufgabenstatus](./main/team-workflow.jpg) + +Zustaende wie „Leader plant“, „Warten auf Mitgliederlieferung“ und „Warten auf Leader-Zusammenfassung“ zeigen die aktuelle Phase. Sende die gleiche Aufgabe nicht erneut; ergaenze Anforderungen im Chat, wenn sich Ziel oder Abnahmekriterien aendern. + +## 4. Ergebnisse ansehen + +Mitglieder geben Artefakte und Validierungsergebnisse an den Leader zurueck. Die finale Zusammenfassung erscheint im Team-Chat und in den Aufgabendetails. Ueber den Tab **Files** oben auf der Detailseite kannst du gemeinsame Team-Artefakte ansehen. + +## 5. Vorlage waehlen + +- **Standard Two-Member Team**: Ein Leader koordiniert ein Ausfuehrungsmitglied fuer eine fokussierte Aufgabe. +- **Delivery Three-Member Team**: Implementierung plus Review fuer eine kleine Lieferung. +- **Product Discovery Four-Member Team**: Produkt-, Design- und technische Machbarkeitsanalyse. +- **Software Engineering Eight-Member Team**: Produkt, Design, Frontend, Backend, Architektur, QA und Code Review. + +Vorlagen sind ein Ausgangspunkt. Waehle die passendste Vorlage und ergaenze Umfang, Einschraenkungen und Abnahmekriterien im Team-Chat. diff --git a/docs/team-workspaces-guide_en.md b/docs/team-workspaces-guide_en.md new file mode 100644 index 0000000..072ec16 --- /dev/null +++ b/docs/team-workspaces-guide_en.md @@ -0,0 +1,50 @@ +# Team Workspace Quick Guide + +Teams let multiple OpenClaw Lite members work toward one shared goal. Choose a role template, create the Team, then describe the goal to the Leader in Team chat. The Leader plans, delegates, coordinates, synthesizes, and delivers the final result. + +## Scope + +- Teams currently run in **OpenClaw Lite** mode. +- Collaboration is fixed to **Leader-mediated collaboration**: work reaches the Leader first, who coordinates the members. +- Templates already define member responsibilities. You normally do not configure a runtime, resource preset, or collaboration setting for every member. + +## 1. Create a Team + +1. Open **Teams** from the navigation and choose to create a Team. +2. Enter a Team name and adjust shared storage when needed. +3. Choose a member template and confirm the Leader, member count, and runtime in the summary. +4. Select **Create**. + +![Create a Team: enter a name, choose a template, and confirm the summary](./main/team-create.png) + +The creation page fixes Teams to OpenClaw Lite and Leader-mediated collaboration. The selected template defines member responsibilities, so individual resource presets are not required. + +## 2. Start Collaboration + +Open the Team detail page and describe the business goal to the Leader in Team chat. For example: “Build a simple calculator app.” + +The Leader creates the plan, assigns members, collects deliveries and review evidence, and publishes a final synthesis. You do not need to manually send the same task to every member. + +## 3. Follow Progress + +The Team detail page has two primary areas: + +- **Team chat** shows plans, assignments, meaningful progress, deliveries, reviews, and Leader synthesis. +- **Execution Kanban** shows root-task state, pending work, active work, and completed deliveries. + +![Team detail: chat shows collaboration while Kanban shows task state](./main/team-workflow.jpg) + +States such as “Leader planning”, “waiting for member delivery”, and “waiting for Leader synthesis” indicate the current collaboration stage. Do not resend the same task; add a follow-up in chat if the goal or acceptance criteria change. + +## 4. View Deliveries + +Members return artifacts and validation conclusions to the Leader. The final synthesis appears in Team chat and task details. Use the **Files** tab at the top of the detail page to browse shared Team artifacts. + +## 5. Choose a Template + +- **Standard two-member Team**: a Leader coordinates one execution member for a focused task. +- **Delivery three-member Team**: implementation plus review for a small delivery. +- **Product discovery four-member Team**: product, design, and technical-feasibility exploration. +- **Software engineering eight-member Team**: product, design, frontend, backend, architecture, QA, and code review. + +Templates are a starting point. Select the closest one, then add scope, constraints, and acceptance criteria in Team chat. diff --git a/docs/team-workspaces-guide_ja.md b/docs/team-workspaces-guide_ja.md new file mode 100644 index 0000000..966bd9e --- /dev/null +++ b/docs/team-workspaces-guide_ja.md @@ -0,0 +1,50 @@ +# Team Workspace Quick Guide + +Team では複数の OpenClaw Lite メンバーが一つの共通目標に取り組みます。ロールテンプレートを選び、Team を作成してから、Team チャットで Leader に目標を説明します。Leader が計画、割り当て、調整、成果物の収集、最終結果の提示を担当します。 + +## 対象範囲 + +- Team は現在 **OpenClaw Lite** モードで動作します。 +- 協働方式は **Leader 仲介型** に固定されています。作業はまず Leader に届き、Leader がメンバーを調整します。 +- テンプレートにはメンバーの役割が含まれます。通常、メンバーごとに Runtime、リソースプリセット、協働設定を指定する必要はありません。 + +## 1. Team を作成する + +1. ナビゲーションで **Teams** を開き、Team の作成を選択します。 +2. Team 名を入力し、必要に応じて共有ストレージを調整します。 +3. メンバーテンプレートを選び、概要で Leader、メンバー数、Runtime を確認します。 +4. **作成** を選択します。 + +![Team 作成: 名前を入力し、テンプレートと概要を確認する](./main/team-create.png) + +作成画面では OpenClaw Lite と Leader 仲介型協働が固定されています。テンプレートが役割を定義するため、個別のリソースプリセットは不要です。 + +## 2. 協働を開始する + +Team 詳細ページを開き、Team チャットで Leader に業務目標を説明します。例: 「シンプルな電卓アプリを作成してください。」 + +Leader は計画を作成し、メンバーを割り当て、成果物とレビュー証跡を集め、最終統合を公開します。同じ作業を各メンバーに手動で送る必要はありません。 + +## 3. 進捗を確認する + +Team 詳細ページには二つの主要領域があります。 + +- **Team チャット**: 計画、割り当て、有効な進捗、成果物、レビュー、Leader の統合結果を表示します。 +- **Execution Kanban**: ルートタスクの状態、未着手、進行中、完了済みの作業を表示します。 + +![Team 詳細: チャットは協働過程、Kanban はタスク状態を表示する](./main/team-workflow.jpg) + +「Leader が計画中」「メンバーの成果物を待機中」「Leader の統合を待機中」といった状態は現在の段階を示します。同じタスクを再送せず、目標や受入基準が変わった場合はチャットで追記してください。 + +## 4. 成果物を確認する + +メンバーは成果物と検証結果を Leader に返します。最終統合は Team チャットとタスク詳細に表示されます。詳細ページ上部の **Files** タブで共有 Team 成果物を参照できます。 + +## 5. テンプレートを選ぶ + +- **Standard Two-Member Team**: Leader が一人の実行メンバーを調整する、集中的な作業向けです。 +- **Delivery Three-Member Team**: 小規模な提供に対する実装とレビュー向けです。 +- **Product Discovery Four-Member Team**: プロダクト、デザイン、技術的実現性の探索向けです。 +- **Software Engineering Eight-Member Team**: プロダクト、デザイン、フロントエンド、バックエンド、アーキテクチャ、QA、コードレビューを含む開発向けです。 + +テンプレートは出発点です。最も近いテンプレートを選び、範囲、制約、受入基準を Team チャットで補足してください。 diff --git a/docs/team-workspaces-guide_ko.md b/docs/team-workspaces-guide_ko.md new file mode 100644 index 0000000..b454dbe --- /dev/null +++ b/docs/team-workspaces-guide_ko.md @@ -0,0 +1,50 @@ +# Team Workspace Quick Guide + +Team은 여러 OpenClaw Lite 멤버가 하나의 공통 목표를 위해 협업하게 합니다. 역할 템플릿을 선택하고 Team을 만든 다음 Team 채팅에서 Leader에게 목표를 설명하면 됩니다. Leader가 계획 수립, 배정, 조율, 산출물 수집, 최종 결과 정리를 담당합니다. + +## 적용 범위 + +- Team은 현재 **OpenClaw Lite** 모드로 실행됩니다. +- 협업 방식은 **Leader 중개 협업**으로 고정됩니다. 작업은 먼저 Leader에게 전달되고 Leader가 멤버를 조율합니다. +- 템플릿에는 멤버 역할이 이미 정의되어 있습니다. 일반적으로 멤버별 Runtime, 리소스 프리셋 또는 협업 설정을 따로 구성할 필요가 없습니다. + +## 1. Team 만들기 + +1. 탐색 메뉴에서 **Teams**를 열고 Team 만들기를 선택합니다. +2. Team 이름을 입력하고 필요하면 공유 스토리지 용량을 조정합니다. +3. 멤버 템플릿을 선택하고 요약에서 Leader, 멤버 수, Runtime을 확인합니다. +4. **만들기**를 선택합니다. + +![Team 만들기: 이름 입력, 템플릿 선택 및 요약 확인](./main/team-create.png) + +생성 화면은 OpenClaw Lite와 Leader 중개 협업을 고정으로 사용합니다. 템플릿이 멤버 역할을 정하므로 개별 리소스 프리셋은 필요하지 않습니다. + +## 2. 협업 시작하기 + +Team 상세 페이지를 열고 Team 채팅에서 Leader에게 업무 목표를 설명합니다. 예: “간단한 계산기 앱을 만들어 주세요.” + +Leader는 계획을 만들고, 멤버를 배정하며, 산출물과 검토 근거를 수집한 뒤 최종 종합 결과를 게시합니다. 같은 작업을 각 멤버에게 직접 보낼 필요가 없습니다. + +## 3. 진행 상황 확인하기 + +Team 상세 페이지에는 두 가지 주요 영역이 있습니다. + +- **Team 채팅**: 계획, 배정, 의미 있는 진행, 산출물, 검토, Leader 종합을 보여 줍니다. +- **Execution Kanban**: 루트 작업 상태, 대기 중인 작업, 진행 중인 작업, 완료된 산출물을 보여 줍니다. + +![Team 상세: 채팅은 협업 과정을, Kanban은 작업 상태를 표시](./main/team-workflow.jpg) + +“Leader 계획 중”, “멤버 산출물 대기”, “Leader 종합 대기” 같은 상태는 현재 협업 단계를 뜻합니다. 같은 작업을 다시 보내지 말고, 목표나 승인 기준이 바뀌면 채팅에 후속 요구 사항을 남기세요. + +## 4. 산출물 보기 + +멤버는 산출물과 검증 결론을 Leader에게 반환합니다. 최종 종합 결과는 Team 채팅과 작업 상세에 표시됩니다. 상세 페이지 상단의 **Files** 탭에서 공유 Team 산출물을 살펴볼 수 있습니다. + +## 5. 템플릿 선택하기 + +- **Standard Two-Member Team**: Leader가 한 명의 실행 멤버를 조율하는 집중 작업에 적합합니다. +- **Delivery Three-Member Team**: 소규모 납품을 위한 구현과 검토에 적합합니다. +- **Product Discovery Four-Member Team**: 제품, 디자인, 기술 실현 가능성 탐색에 적합합니다. +- **Software Engineering Eight-Member Team**: 제품, 디자인, 프론트엔드, 백엔드, 아키텍처, QA, 코드 리뷰를 포함한 개발 작업에 적합합니다. + +템플릿은 시작점입니다. 가장 가까운 템플릿을 선택한 뒤 범위, 제약 조건, 승인 기준을 Team 채팅에 추가하세요. diff --git a/docs/use_guide_cn.md b/docs/use_guide_cn.md index dd40527..c4b2bd0 100644 --- a/docs/use_guide_cn.md +++ b/docs/use_guide_cn.md @@ -13,6 +13,7 @@ - [八、快速启动指南(登录后初始化并创建 OpenClaw 实例)](#sec-09) - [九、控制台与 AI 网关其他功能说明](#sec-12) - [十、工作台模块说明](#sec-13) +- [10.4 Team 协作](#sec-13-team) - [十一、问题与对策速查](#sec-14) - [十二、建议的最终检查顺序(可按此自查)](#sec-15) @@ -685,6 +686,15 @@ AI 审计页面用于查看最近的受管模型调用记录,帮助管理员 > - 如果上传后列表未立即刷新,可手动点击页面右上角 **刷新** 重新加载。 --- + +### 10.4 Team 协作 + +Team 用于让多个 OpenClaw Lite 成员围绕同一目标协作。进入 **Teams** 后,选择成员模板、填写 Team 名称并创建即可;不需要逐个配置成员运行时或资源预设。 + +创建后,在团队群聊中向 Leader 描述目标。Leader 会安排成员完成计划、执行、验收和最终汇总;通过 **Execution Kanban** 查看当前阶段和已完成交付,通过 **文件** 标签查看共享产物。 + +详见 [Team 协作快速指南](./team-workspaces-guide.md)。 + ## 十一、问题与对策速查 diff --git a/docs/use_guide_de.md b/docs/use_guide_de.md index 636b260..8112426 100644 --- a/docs/use_guide_de.md +++ b/docs/use_guide_de.md @@ -13,6 +13,7 @@ - [VIII. Schnellstartanleitung (nach dem Login initialisieren und eine OpenClaw-Instanz erstellen)](#sec-09) - [IX. Konsole und weitere Funktionen des AI Gateway](#sec-12) - [X. Beschreibung des Workspace-Moduls](#sec-13) +- [10.4 Team-Zusammenarbeit](#sec-13-team) - [XI. Schnelle Referenz für Probleme und Gegenmaßnahmen](#sec-14) - [XII. Empfohlene abschließende Prüfreihenfolge (zur Selbstprüfung)](#sec-15) @@ -659,6 +660,15 @@ Gehe beim Hochladen von Skills wie folgt vor: > - Falls die Liste nach dem Upload nicht sofort aktualisiert wird, kannst du oben rechts auf der Seite manuell auf **Aktualisieren** klicken, um neu zu laden。 --- + +### 10.4 Team-Zusammenarbeit + +Teams lassen mehrere OpenClaw-Lite-Mitglieder an einem gemeinsamen Ziel arbeiten. Oeffne **Teams**, waehle eine Mitgliedervorlage, gib einen Teamnamen ein und erstelle das Team; Runtime- und Ressourcenprofile je Mitglied sind nicht erforderlich. + +Beschreibe nach der Erstellung das Ziel im Team-Chat fuer den Leader. Der Leader koordiniert Planung, Ausfuehrung, Review und finale Zusammenfassung. Mit **Execution Kanban** siehst du die aktuelle Phase und abgeschlossene Lieferungen; unter **Files** findest du gemeinsame Artefakte. + +Siehe [Team Workspace Quick Guide](./team-workspaces-guide_de.md). + ## XI. Schnelle Referenz für Probleme und Gegenmaßnahmen diff --git a/docs/use_guide_en.md b/docs/use_guide_en.md index ce1bc7d..da47ddc 100644 --- a/docs/use_guide_en.md +++ b/docs/use_guide_en.md @@ -13,6 +13,7 @@ - [VIII. Quick Start Guide (Initialize and Create an OpenClaw Instance After Login)](#sec-09) - [IX. Console and Other AI Gateway Features](#sec-12) - [X. Workspace Module Guide](#sec-13) +- [10.4 Team Collaboration](#sec-13-team) - [XI. Quick Troubleshooting Reference](#sec-14) - [XII. Recommended Final Check Sequence (Use as a Self-Check)](#sec-15) @@ -684,6 +685,15 @@ To upload skills, follow these steps: > - If the list is not refreshed immediately after upload, you can manually click **Refresh** in the upper-right corner of the page to reload it. --- + +### 10.4 Team Collaboration + +Teams let multiple OpenClaw Lite members work toward a shared goal. Open **Teams**, choose a member template, enter a Team name, and create it; individual member runtime and resource-preset setup is not required. + +After creation, describe the goal to the Leader in Team chat. The Leader coordinates planning, execution, review, and final synthesis. Use **Execution Kanban** to follow the current stage and completed deliveries, and use **Files** to browse shared artifacts. + +See the [Team Workspace Quick Guide](./team-workspaces-guide_en.md). + ## XI. Quick Troubleshooting Reference diff --git a/docs/use_guide_ja.md b/docs/use_guide_ja.md index 4646e37..8355acd 100644 --- a/docs/use_guide_ja.md +++ b/docs/use_guide_ja.md @@ -13,6 +13,7 @@ - [八、クイックスタートガイド(ログイン後に初期化して OpenClaw インスタンスを作成)](#sec-09) - [九、コンソールと AI Gateway のその他の機能説明](#sec-12) - [十、ワークスペースモジュールの説明](#sec-13) +- [10.4 Team 協働](#sec-13-team) - [十一、問題と対処のクイックリファレンス](#sec-14) - [十二、推奨される最終確認手順(セルフチェック用)](#sec-15) @@ -658,6 +659,15 @@ AI 監査ページは、最近の管理対象モデル呼び出し記録を確 > - アップロード後に一覧がすぐ更新されない場合は、ページ右上の **更新** を手動でクリックして再読み込みしてください。 --- + +### 10.4 Team 協働 + +Team では複数の OpenClaw Lite メンバーが共通目標に取り組みます。**Teams** を開き、メンバーテンプレートを選び、Team 名を入力して作成します。メンバーごとの Runtime やリソースプリセットの設定は不要です。 + +作成後、Team チャットで Leader に目標を説明します。Leader が計画、実行、レビュー、最終統合を調整します。現在の段階と完了した成果物は **Execution Kanban** で、共有成果物は **Files** で確認できます。 + +詳しくは [Team Workspace Quick Guide](./team-workspaces-guide_ja.md) を参照してください。 + ## 十一、問題と対処のクイックリファレンス diff --git a/docs/use_guide_ko.md b/docs/use_guide_ko.md index 8e8c07f..499c09f 100644 --- a/docs/use_guide_ko.md +++ b/docs/use_guide_ko.md @@ -13,6 +13,7 @@ - [8. 빠른 시작 가이드(로그인 후 초기화 및 OpenClaw 인스턴스 생성)](#sec-09) - [9. 콘솔 및 AI Gateway 기타 기능 설명](#sec-12) - [10. 워크스페이스 모듈 설명](#sec-13) +- [10.4 Team 협업](#sec-13-team) - [11. 문제와 대응 빠른 참조](#sec-14) - [12. 권장 최종 점검 순서(자가 점검용)](#sec-15) @@ -659,6 +660,15 @@ AI 감사 페이지는 최근의 관리형 모델 호출 기록을 확인하는 > - 업로드 후 목록이 바로 새로고침되지 않으면 페이지 오른쪽 상단의 **새로고침**을 수동으로 클릭하여 다시 불러올 수 있습니다。 --- + +### 10.4 Team 협업 + +Team은 여러 OpenClaw Lite 멤버가 공통 목표를 위해 협업하게 합니다. **Teams**를 열고 멤버 템플릿을 선택한 다음 Team 이름을 입력해 만드세요. 멤버별 Runtime이나 리소스 프리셋 설정은 필요하지 않습니다. + +생성 후 Team 채팅에서 Leader에게 목표를 설명합니다. Leader가 계획, 실행, 검토, 최종 종합을 조율합니다. **Execution Kanban**에서 현재 단계와 완료된 산출물을 보고, **Files**에서 공유 산출물을 확인할 수 있습니다. + +자세한 내용은 [Team Workspace Quick Guide](./team-workspaces-guide_ko.md)를 참고하세요. + ## 11. 문제와 대응 빠른 참조 diff --git a/frontend/src/index.css b/frontend/src/index.css index 172310f..7991541 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,6 +2,44 @@ @tailwind components; @tailwind utilities; +@keyframes cm-silver-sheen { + 0%, + 18% { + transform: translateX(-180%) skewX(-18deg); + opacity: 0; + } + 28% { + opacity: 0.55; + } + 58%, + 100% { + transform: translateX(420%) skewX(-18deg); + opacity: 0; + } +} + +@keyframes cm-tech-progress { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes cm-tech-breathe { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(56, 189, 248, 0.12); + } + 50% { + box-shadow: 0 0 0 5px rgba(56, 189, 248, 0); + } +} + :root { --text: #334155; --text-strong: #0f172a; @@ -93,4 +131,96 @@ body { .cm-icon-button:hover { @apply border-slate-400 bg-slate-50 text-slate-950; } + + .cm-tech-panel { + position: relative; + isolation: isolate; + border-color: #d8dee6; + background: #f4f7fa; + box-shadow: + 0 24px 60px -42px rgba(15, 23, 42, 0.46), + 0 1px 0 rgba(255, 255, 255, 0.95) inset; + } + + .cm-tech-header { + position: relative; + overflow: hidden; + border-bottom: 1px solid #d8dee6; + background: + radial-gradient(circle at 86% 0%, rgba(125, 211, 252, 0.16), transparent 34%), + linear-gradient(135deg, #fbfcfd 0%, #e9eef3 48%, #f8fafc 100%); + } + + .cm-tech-header::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(148, 163, 184, 0.07) 1px, transparent 1px), + linear-gradient(90deg, rgba(148, 163, 184, 0.07) 1px, transparent 1px); + background-size: 24px 24px; + mask-image: linear-gradient(90deg, black, transparent 82%); + } + + .cm-tech-header::after { + content: ""; + position: absolute; + top: -40%; + bottom: -40%; + left: 0; + width: 18%; + pointer-events: none; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.88), transparent); + filter: blur(1px); + animation: cm-silver-sheen 6s ease-in-out infinite; + } + + .cm-tech-workspace { + background: + radial-gradient(circle at 100% 0%, rgba(186, 230, 253, 0.2), transparent 32%), + linear-gradient(180deg, #f8fafc 0%, #edf2f6 52%, #f8fafc 100%); + } + + .cm-tech-surface { + border: 1px solid rgba(203, 213, 225, 0.92); + background: linear-gradient(145deg, rgba(255, 255, 255, 0.94), rgba(239, 243, 247, 0.9)); + box-shadow: + 0 14px 30px -25px rgba(15, 23, 42, 0.55), + 0 1px 0 rgba(255, 255, 255, 0.95) inset; + transition: + border-color 220ms ease, + box-shadow 220ms ease, + transform 220ms ease; + } + + .cm-tech-surface:hover { + border-color: #b9c5d2; + box-shadow: + 0 18px 36px -26px rgba(15, 23, 42, 0.58), + 0 1px 0 rgba(255, 255, 255, 0.98) inset; + } + + .cm-tech-subtle { + border: 1px solid rgba(211, 219, 228, 0.9); + background: linear-gradient(145deg, rgba(248, 250, 252, 0.94), rgba(231, 237, 243, 0.82)); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.9) inset; + } + + .cm-tech-progress { + background-size: 220% 220%; + animation: cm-tech-progress 3.6s ease-in-out infinite; + } + + .cm-tech-breathe { + animation: cm-tech-breathe 2.4s ease-in-out infinite; + } +} + +@media (prefers-reduced-motion: reduce) { + .cm-tech-header::after, + .cm-tech-progress, + .cm-tech-breathe { + animation: none; + } } diff --git a/frontend/src/lib/teamTemplates.ts b/frontend/src/lib/teamTemplates.ts index b9b8550..e11845c 100644 --- a/frontend/src/lib/teamTemplates.ts +++ b/frontend/src/lib/teamTemplates.ts @@ -33,6 +33,43 @@ export type TeamMemberTemplate = { members: TeamMemberTemplateMember[]; }; +const BUILTIN_MEMBER_DESCRIPTION_ZH: Record = { + "Team Leader / Agents Orchestrator: decomposes goals, coordinates members, maintains context, validates member outputs, and reports externally.": + "团队负责人 / 智能体编排官:拆解目标、协调成员、维护上下文、验证成员产出,并向用户汇报最终结果。", + "Senior Developer: executes implementation tasks assigned by the Leader, reports progress, lists changes, and escalates blockers.": + "资深开发工程师:执行 Leader 分派的实现任务,汇报进度和变更,并及时上报阻塞问题。", + "Agents Orchestrator: decomposes requirements, sets priorities, dispatches tasks, manages risks, and integrates results.": + "智能体编排官:拆解需求、设定优先级、分派任务、管理风险,并整合成员结果。", + "Senior Developer: implements code, integrates interfaces, adds necessary tests, and provides reproducible delivery notes.": + "资深开发工程师:负责代码实现和接口集成,补充必要测试,并提供可复现的交付说明。", + "Evidence Collector / Reviewer: verifies behavior, checks regressions, gathers evidence, reviews delivery items, and gives a PASS/FAIL verdict.": + "验收验证员 / 评审员:验证功能行为和回归风险,收集证据、审查交付内容,并给出通过或不通过的结论。", + "Agents Orchestrator: owns goals, definition of done, task breakdown, dependency coordination, risk management, acceptance, and final decisions.": + "智能体编排官:负责目标和完成标准,统筹任务拆解、依赖协调、风险管理、验收与最终决策。", + "Product Manager: owns requirements, product direction, PRD, user flows, feature boundaries, priorities, and acceptance criteria.": + "产品经理:负责需求、产品方向、PRD、用户流程、功能边界、优先级和验收标准。", + "UI Designer: owns visual direction, UX, interaction states, component guidance, and implementable design handoff.": + "UI 设计师:负责视觉方向、用户体验、交互状态和组件规范,并提供可落地的设计交付。", + "Frontend Developer: owns frontend UI implementation, API integration, state management, interaction behavior, responsiveness, and accessibility.": + "前端开发工程师:负责前端界面实现、API 对接、状态管理、交互行为、响应式适配和无障碍体验。", + "Backend Architect: owns APIs, databases, permissions, queues, business logic, and server-side system capabilities.": + "后端架构师:负责 API、数据库、权限、队列、业务逻辑和服务端系统能力。", + "Software Architect: owns technical choices, system boundaries, availability, extensibility, technical standards, and evolution plans.": + "软件架构师:负责技术选型、系统边界、可用性、可扩展性、技术标准和演进规划。", + "Evidence Collector: owns functional validation, regression checks, evidence gathering, reproduction notes, and acceptance verdicts.": + "验收验证员:负责功能验证、回归检查、证据收集、复现说明和验收结论。", + "Code Reviewer: owns code review, architecture consistency, maintainability, test coverage, risk findings, and pre-merge quality gates.": + "代码审查员:负责代码评审、架构一致性、可维护性、测试覆盖、风险识别和合并前质量把关。", +}; + +export const getTeamMemberDisplayDescription = (description?: string) => { + const normalized = description?.trim(); + if (!normalized) { + return description; + } + return BUILTIN_MEMBER_DESCRIPTION_ZH[normalized] || description; +}; + const baseMember = ( overrides: Partial, ): TeamMemberTemplateMember => ({ diff --git a/frontend/src/pages/teams/CreateTeamPage.tsx b/frontend/src/pages/teams/CreateTeamPage.tsx index 54ff6b0..edf7eba 100644 --- a/frontend/src/pages/teams/CreateTeamPage.tsx +++ b/frontend/src/pages/teams/CreateTeamPage.tsx @@ -10,13 +10,12 @@ import { } from "../../services/systemSettingsService"; import { teamService } from "../../services/teamService"; import { - AGENCY_AGENT_PROFILES, buildAgencyAgentEnvironment, getAgencyAgentProfile, - type AgencyAgentProfileKey, } from "../../lib/agencyAgentProfiles"; import { BUILTIN_MEMBER_TEMPLATES, + getTeamMemberDisplayDescription, type ResourcePresetKey, type RuntimeType, type TeamMemberTemplate, @@ -40,16 +39,6 @@ type TeamMemberDraft = TeamMemberTemplateMember & { instanceMode: InstanceMode; }; -const RUNTIME_OPTIONS: Array<{ value: RuntimeType; label: string }> = [ - { value: "openclaw", label: "OpenClaw" }, - { value: "hermes", label: "Hermes" }, -]; - -const INSTANCE_MODE_OPTIONS: Array<{ value: InstanceMode; label: string }> = [ - { value: "lite", label: "Lite" }, - { value: "pro", label: "Pro" }, -]; - const TEAM_COMMUNICATION_MODE_OPTIONS: Array<{ value: TeamCommunicationMode; label: string; @@ -72,8 +61,43 @@ const RESOURCE_PRESETS: Record< }; const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/; -const CUSTOM_MEMBER_TEMPLATES_STORAGE_KEY = "clawmanager.team.memberTemplates.v1"; -const AGENCY_AGENT_PROFILE_OPTIONS = Object.values(AGENCY_AGENT_PROFILES); +const FIXED_RUNTIME_TYPE: RuntimeType = "openclaw"; +const FIXED_INSTANCE_MODE: InstanceMode = "lite"; +const FIXED_COMMUNICATION_MODE: TeamCommunicationMode = "leader_mediated"; +const TEAM_TEMPLATE_DISPLAY_COPY: Record< + string, + { name: string; description: string } +> = { + "builtin-leader-worker": { + name: "标准双成员团队", + description: + "Leader 负责任务拆解、成员协调和结果整合,Worker 执行具体任务并回报进度。", + }, + "builtin-dev-qa-docs": { + name: "交付三成员团队", + description: + "Leader 负责拆解和协调,Developer 负责实现与集成,Reviewer 负责验证测试、回归风险和交付质量。", + }, + "builtin-software-engineering-team": { + name: "软件工程八成员团队", + description: + "Leader 统筹目标、协作和最终集成,PM、UI/UX、Frontend、Backend、Architect、QA、Code Reviewer 分别覆盖产品、设计、客户端、服务端、架构、验证和代码审查。", + }, +}; + +const getTemplateDisplayName = (template: TeamMemberTemplate) => + TEAM_TEMPLATE_DISPLAY_COPY[template.id]?.name || template.name; + +const getTemplateDisplayDescription = (template: TeamMemberTemplate) => + TEAM_TEMPLATE_DISPLAY_COPY[template.id]?.description || + template.description || + "模板未提供说明。"; + +const SORTED_BUILTIN_MEMBER_TEMPLATES = [...BUILTIN_MEMBER_TEMPLATES].sort( + (left, right) => + left.members.length - right.members.length || + getTemplateDisplayName(left).localeCompare(getTemplateDisplayName(right), "zh-Hans"), +); const newDraftId = () => `member-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`; @@ -99,10 +123,67 @@ const defaultMember = ( ...overrides, }); -const imageOptionKey = (item: SystemImageSetting) => - item.id != null - ? `image-${item.id}` - : `${item.instance_type}:${item.image}`; +const normalizedPreset = ( + preset?: ResourcePresetKey, +): Exclude => + preset && preset !== "custom" ? preset : "small"; + +const draftFromTemplateMember = ( + templateMember: TeamMemberTemplateMember, + usedIds: Set, + index: number, + isLeader: boolean, + image: string, +): TeamMemberDraft => { + const preset = normalizedPreset(templateMember.resourcePreset); + const config = RESOURCE_PRESETS[preset]; + const templateRole = + templateMember.role && templateMember.role !== "leader" + ? templateMember.role + : "member"; + + return defaultMember({ + ...templateMember, + memberId: uniqueMemberId(templateMember.memberId, usedIds, index), + role: isLeader ? "leader" : templateRole, + runtimeType: FIXED_RUNTIME_TYPE, + instanceMode: FIXED_INSTANCE_MODE, + image, + resourcePreset: preset, + isLeader, + cpuCores: config.cpuCores, + memoryGb: config.memoryGb, + diskGb: config.diskGb, + gpuEnabled: false, + gpuCount: 0, + }); +}; + +const buildTemplateMembers = (template: TeamMemberTemplate, image: string) => { + const usedIds = new Set(); + const importedMembers: TeamMemberDraft[] = []; + let leaderAssigned = false; + + template.members.forEach((templateMember, index) => { + const shouldBeLeader = Boolean(templateMember.isLeader) && !leaderAssigned; + if (shouldBeLeader) { + leaderAssigned = true; + } + importedMembers.push( + draftFromTemplateMember(templateMember, usedIds, index + 1, shouldBeLeader, image), + ); + }); + + if (!leaderAssigned && importedMembers.length > 0) { + importedMembers[0] = { + ...importedMembers[0], + isLeader: true, + role: "leader", + }; + } + + return importedMembers; +}; const imageRuntimeTypeForMode = ( mode: InstanceMode, @@ -122,51 +203,6 @@ const normalizeMemberId = (value: string) => .replace(/[^a-z0-9-]/g, "") .slice(0, 63); -const loadCustomMemberTemplates = (): TeamMemberTemplate[] => { - try { - if (typeof window === "undefined") { - return []; - } - const raw = window.localStorage.getItem(CUSTOM_MEMBER_TEMPLATES_STORAGE_KEY); - if (!raw) { - return []; - } - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) { - return []; - } - return parsed - .filter((item): item is TeamMemberTemplate => { - return ( - item && - typeof item.id === "string" && - typeof item.name === "string" && - Array.isArray(item.members) - ); - }) - .map((item) => ({ - ...item, - source: "custom" as const, - })); - } catch { - return []; - } -}; - -const saveCustomMemberTemplates = (templates: TeamMemberTemplate[]) => { - try { - if (typeof window === "undefined") { - return; - } - window.localStorage.setItem( - CUSTOM_MEMBER_TEMPLATES_STORAGE_KEY, - JSON.stringify(templates), - ); - } catch { - // localStorage can be unavailable in private browsing or strict webviews. - } -}; - const uniqueMemberId = (raw: string, usedIds: Set, fallbackIndex: number) => { const fallback = `member-${fallbackIndex}`; const base = normalizeMemberId(raw) || fallback; @@ -185,34 +221,16 @@ const uniqueMemberId = (raw: string, usedIds: Set, fallbackIndex: number const CreateTeamPage: React.FC = () => { const navigate = useNavigate(); - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - const [communicationMode] = useState("leader_mediated"); + const initialTemplate = SORTED_BUILTIN_MEMBER_TEMPLATES[0]; + const [name, setName] = useState(initialTemplate?.teamName || ""); const [sharedStorageGb, setSharedStorageGb] = useState(10); - const [storageClass, setStorageClass] = useState(""); const [images, setImages] = useState([]); - const [selectedImageKey, setSelectedImageKey] = useState(""); - const [customMemberTemplates, setCustomMemberTemplates] = useState< - TeamMemberTemplate[] - >(() => loadCustomMemberTemplates()); const [selectedTemplateId, setSelectedTemplateId] = useState( - BUILTIN_MEMBER_TEMPLATES[0]?.id || "", + initialTemplate?.id || "", + ); + const [members, setMembers] = useState(() => + initialTemplate ? buildTemplateMembers(initialTemplate, "") : [], ); - const [templatePackageName, setTemplatePackageName] = useState(""); - const [templateNotice, setTemplateNotice] = useState(null); - const [members, setMembers] = useState(() => [ - defaultMember({ - memberId: "leader", - role: "leader", - isLeader: true, - agentProfileKey: "agency.agents-orchestrator", - }), - defaultMember({ - memberId: "worker", - role: "developer", - agentProfileKey: "agency.senior-developer", - }), - ]); const [loadingImages, setLoadingImages] = useState(true); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -228,52 +246,25 @@ const CreateTeamPage: React.FC = () => { null, ); - const openClawImages = useMemo( - () => - images.filter( - (item) => item.instance_type === "openclaw" && item.is_enabled !== false, - ), - [images], - ); - const hermesImages = useMemo( - () => - images.filter( - (item) => item.instance_type === "hermes" && item.is_enabled !== false, - ), - [images], - ); - const memberTemplates = useMemo( - () => [...BUILTIN_MEMBER_TEMPLATES, ...customMemberTemplates], - [customMemberTemplates], - ); + const memberTemplates = SORTED_BUILTIN_MEMBER_TEMPLATES; const selectedTemplate = useMemo( () => memberTemplates.find((template) => template.id === selectedTemplateId) || memberTemplates[0], [memberTemplates, selectedTemplateId], ); - const imageOptionsForRuntime = useCallback( - (runtimeType: RuntimeType) => - runtimeType === "hermes" ? hermesImages : openClawImages, - [hermesImages, openClawImages], - ); - const imageOptionsForMember = useCallback( - (runtimeType: RuntimeType, instanceMode: InstanceMode) => - imageOptionsForRuntime(runtimeType).filter( - (item) => - normalizedImageRuntimeType(item) === - imageRuntimeTypeForMode(instanceMode), - ), - [imageOptionsForRuntime], - ); const defaultOpenClawMemberImages = useMemo( - () => imageOptionsForMember("openclaw", "lite"), - [imageOptionsForMember], + () => + images.filter( + (item) => + item.instance_type === "openclaw" && + item.is_enabled !== false && + normalizedImageRuntimeType(item) === imageRuntimeTypeForMode("lite"), + ), + [images], ); - const selectedImage = - defaultOpenClawMemberImages.find( - (item) => imageOptionKey(item) === selectedImageKey, - ) || defaultOpenClawMemberImages[0]; + const selectedImage = defaultOpenClawMemberImages[0]; + const openClawLiteImage = selectedImage?.image || ""; useEffect(() => { const loadImages = async () => { @@ -290,316 +281,18 @@ const CreateTeamPage: React.FC = () => { void loadImages(); }, []); - useEffect(() => { - if (defaultOpenClawMemberImages.length === 0) { - setSelectedImageKey(""); - return; - } - setSelectedImageKey((current) => - defaultOpenClawMemberImages.some((item) => imageOptionKey(item) === current) - ? current - : imageOptionKey(defaultOpenClawMemberImages[0]), - ); - }, [defaultOpenClawMemberImages]); - - useEffect(() => { - saveCustomMemberTemplates(customMemberTemplates); - }, [customMemberTemplates]); - - useEffect(() => { - if ( - memberTemplates.length > 0 && - !memberTemplates.some((template) => template.id === selectedTemplateId) - ) { - setSelectedTemplateId(memberTemplates[0].id); - } - }, [memberTemplates, selectedTemplateId]); - - useEffect(() => { - setMembers((current) => - current.map((member) => { - const options = imageOptionsForMember( - member.runtimeType, - member.instanceMode, - ); - const fallbackImage = options[0]?.image || ""; - const imageMatchesRuntimeAndMode = options.some( - (item) => item.image === member.image, - ); - if ((!member.image || !imageMatchesRuntimeAndMode) && fallbackImage) { - return { ...member, image: fallbackImage }; - } - return member; - }), - ); - }, [imageOptionsForMember]); - - const updateMember = ( - id: string, - patch: - | Partial - | ((current: TeamMemberDraft) => Partial), - ) => { - setMembers((current) => - current.map((member) => { - if (member.id !== id) { - return member; - } - const nextPatch = typeof patch === "function" ? patch(member) : patch; - return { ...member, ...nextPatch }; - }), - ); - }; - - const setMemberRuntimeType = (id: string, runtimeType: RuntimeType) => { - updateMember(id, (current) => ({ - runtimeType, - image: - imageOptionsForMember(runtimeType, current.instanceMode)[0]?.image || "", - })); - }; - - const setMemberInstanceMode = (id: string, instanceMode: InstanceMode) => { - updateMember(id, (current) => ({ - instanceMode, - image: - imageOptionsForMember(current.runtimeType, instanceMode)[0]?.image || "", - })); - }; - - const applyResourcePreset = (id: string, preset: ResourcePresetKey) => { - if (preset === "custom") { - updateMember(id, { resourcePreset: "custom" }); - return; - } - const config = RESOURCE_PRESETS[preset]; - updateMember(id, { - resourcePreset: preset, - cpuCores: config.cpuCores, - memoryGb: config.memoryGb, - diskGb: config.diskGb, - }); - }; - - const setLeader = (id: string) => { - setMembers((current) => - current.map((member) => ({ - ...member, - isLeader: member.id === id, - role: member.id === id ? "leader" : member.role === "leader" ? "developer" : member.role, - })), - ); - }; - - const addMember = () => { - const nextIndex = members.length + 1; - setMembers((current) => [ - ...current, - defaultMember({ - memberId: `worker-${nextIndex}`, - image: selectedImage?.image || "", - }), - ]); - }; - - const removeMember = (id: string) => { - setMembers((current) => { - const next = current.filter((member) => member.id !== id); - if (next.length > 0 && !next.some((member) => member.isLeader)) { - return next.map((member, index) => - index === 0 - ? { ...member, isLeader: true, role: "leader" } - : member, - ); - } - return next; - }); - }; - - const draftFromTemplateMember = ( - templateMember: TeamMemberTemplateMember, - usedIds: Set, - index: number, - isLeader: boolean, - ): TeamMemberDraft => { - const runtimeType = templateMember.runtimeType || "openclaw"; - const instanceMode = templateMember.instanceMode || "lite"; - const runtimeImages = imageOptionsForMember(runtimeType, instanceMode); - const templateImageAvailable = runtimeImages.some( - (item) => item.image === templateMember.image, - ); - const image = templateImageAvailable - ? templateMember.image - : runtimeImages[0]?.image || templateMember.image || ""; - const role = - isLeader || templateMember.role !== "leader" - ? templateMember.role - : "developer"; - - return defaultMember({ - ...templateMember, - memberId: uniqueMemberId(templateMember.memberId, usedIds, index), - role: isLeader ? "leader" : role || "member", - runtimeType, - instanceMode, - isLeader, - image, - }); - }; - - const buildTemplateMembers = ( - template: TeamMemberTemplate, - existingMembers: TeamMemberDraft[], - ) => { - const usedIds = new Set( - existingMembers - .map((member) => normalizeMemberId(member.memberId)) - .filter(Boolean), - ); - const importedMembers: TeamMemberDraft[] = []; - let leaderAssigned = existingMembers.some((member) => member.isLeader); - - template.members.forEach((templateMember, index) => { - const shouldBeLeader = Boolean(templateMember.isLeader) && !leaderAssigned; - if (shouldBeLeader) { - leaderAssigned = true; - } - importedMembers.push( - draftFromTemplateMember(templateMember, usedIds, index + 1, shouldBeLeader), - ); - }); - - if (!leaderAssigned && importedMembers.length > 0) { - importedMembers[0] = { - ...importedMembers[0], - isLeader: true, - role: "leader", - }; - } - - return importedMembers; - }; - - const importMemberTemplate = (mode: "replace" | "append") => { - if (!selectedTemplate) { - return; - } - if (selectedTemplate.teamName) { - setName(selectedTemplate.teamName); - } - if (selectedTemplate.description) { - setDescription(selectedTemplate.description); - } - setMembers((current) => { - const existingMembers = mode === "append" ? current : []; - const importedMembers = buildTemplateMembers(selectedTemplate, existingMembers); - return mode === "append" ? [...current, ...importedMembers] : importedMembers; - }); - setTemplateNotice( - mode === "append" - ? `已追加模板包:${selectedTemplate.name}` - : `已导入模板包:${selectedTemplate.name}`, - ); - setError(null); - }; - - const buildTemplateFromCurrentMembers = ( - packageName: string, - templateId?: string, - ): TeamMemberTemplate | null => { - const templateMembers = members.map((member, index) => ({ - memberId: normalizeMemberId(member.memberId) || `member-${index + 1}`, - name: member.name.trim(), - role: member.isLeader ? "leader" : member.role.trim() || "member", - runtimeType: member.runtimeType, - instanceMode: member.instanceMode, - description: member.description.trim(), - agentProfileKey: member.agentProfileKey, - resourcePreset: member.resourcePreset, - isLeader: member.isLeader, - cpuCores: member.cpuCores, - memoryGb: member.memoryGb, - diskGb: member.diskGb, - gpuEnabled: member.gpuEnabled, - gpuCount: member.gpuEnabled ? member.gpuCount : 0, - image: member.image.trim(), - })); - - return { - id: - templateId || - `custom-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`, - name: packageName, - teamName: name.trim() || undefined, - description: description.trim() || undefined, - communicationMode: "leader_mediated", - source: "custom", - members: templateMembers, - }; - }; - - const saveCurrentMembersAsTemplate = () => { - const packageName = templatePackageName.trim(); - if (!packageName) { - setError("模板包名称不能为空"); - return; - } - if (members.length === 0) { - setError("至少需要一个成员才能保存模板"); - return; - } - - const template = buildTemplateFromCurrentMembers(packageName); + const applyTemplate = (templateId: string) => { + const template = + memberTemplates.find((item) => item.id === templateId) || memberTemplates[0]; if (!template) { return; } - - setCustomMemberTemplates((current) => [...current, template]); setSelectedTemplateId(template.id); - setTemplatePackageName(""); - setTemplateNotice(`已保存模板包:${packageName}`); + setName(template.teamName || ""); + setMembers(buildTemplateMembers(template, openClawLiteImage)); setError(null); }; - const updateSelectedTemplate = () => { - if (!selectedTemplate || selectedTemplate.source !== "custom") { - setError("只能编辑自定义模板,请先把内置模板另存为自定义模板"); - return; - } - if (members.length === 0) { - setError("至少需要一个成员才能更新模板"); - return; - } - const packageName = templatePackageName.trim() || selectedTemplate.name; - const updatedTemplate = buildTemplateFromCurrentMembers( - packageName, - selectedTemplate.id, - ); - if (!updatedTemplate) { - return; - } - - setCustomMemberTemplates((current) => - current.map((template) => - template.id === selectedTemplate.id ? updatedTemplate : template, - ), - ); - setTemplatePackageName(""); - setTemplateNotice(`已更新模板包:${packageName}`); - setError(null); - }; - - const deleteSelectedTemplate = () => { - if (!selectedTemplate || selectedTemplate.source !== "custom") { - return; - } - setCustomMemberTemplates((current) => - current.filter((template) => template.id !== selectedTemplate.id), - ); - setSelectedTemplateId(BUILTIN_MEMBER_TEMPLATES[0]?.id || ""); - setTemplateNotice(`已删除模板包:${selectedTemplate.name}`); - }; - const addEnvironmentRow = () => { setEnvironmentRows((current) => [ ...current, @@ -686,6 +379,16 @@ const CreateTeamPage: React.FC = () => { return profileForMember(member)?.summary || ""; }; + const displayNameForMember = (member: TeamMemberDraft) => { + const normalizedMemberId = normalizeMemberId(member.memberId); + return member.name.trim() || `${name.trim() || "team"}-${normalizedMemberId || member.memberId}`; + }; + + const profileLabelForMember = (member: TeamMemberDraft) => { + const profile = profileForMember(member); + return profile?.displayName || profile?.name || member.agentProfileKey || "未指定"; + }; + const buildMemberEnvironmentOverrides = ( member: TeamMemberDraft, normalizedMemberId: string, @@ -695,7 +398,7 @@ const CreateTeamPage: React.FC = () => { memberId: normalizedMemberId, displayName: member.name.trim() || normalizedMemberId, role: effectiveMemberRole(member), - runtimeType: member.runtimeType, + runtimeType: FIXED_RUNTIME_TYPE, isLeader: member.isLeader, }); const merged = { @@ -721,6 +424,11 @@ const CreateTeamPage: React.FC = () => { if (!name.trim()) { return "Team 名称不能为空"; } + if (!openClawLiteImage) { + return loadingImages + ? "正在加载 OpenClaw Lite 镜像" + : "没有可用的 OpenClaw Lite 镜像"; + } if (members.length === 0) { return "至少需要一个成员"; } @@ -737,20 +445,14 @@ const CreateTeamPage: React.FC = () => { return `成员 ID 重复:${memberId}`; } memberIds.add(memberId); - if (!member.image.trim()) { - return `成员 ${memberId} 未选择镜像`; - } if (member.cpuCores <= 0 || member.memoryGb <= 0 || member.diskGb <= 0) { return `成员 ${memberId} 的资源规格无效`; } } return null; - }, [members, name]); + }, [loadingImages, members, name, openClawLiteImage]); - const environmentDraft = useMemo( - () => buildEnvironmentOverridesPayload(), - [environmentRows], - ); + const environmentDraft = buildEnvironmentOverridesPayload(); const openClawPlanInvalid = (openClawInjectionMode === "bundle" && (!openClawBundleId || Boolean(openClawPreviewError) || openClawPreviewLoading)) || @@ -763,6 +465,10 @@ const CreateTeamPage: React.FC = () => { const resolvedChannelNames = (openClawPreview?.resolved_resources || []) .filter((resource) => resource.resource_type === "channel") .map((resource) => resource.name); + const communicationModeOption = + TEAM_COMMUNICATION_MODE_OPTIONS.find( + (option) => option.value === FIXED_COMMUNICATION_MODE, + ) || TEAM_COMMUNICATION_MODE_OPTIONS[0]; const submitDisabled = submitting || @@ -791,28 +497,26 @@ const CreateTeamPage: React.FC = () => { const openClawConfigPlan = buildOpenClawConfigPlan(); const payload: CreateTeamRequest = { name: name.trim(), - description: description.trim() || undefined, - communication_mode: "leader_mediated", + communication_mode: FIXED_COMMUNICATION_MODE, shared_storage_gb: sharedStorageGb, - storage_class: storageClass.trim() || undefined, members: members.map((member) => { const normalizedMemberId = normalizeMemberId(member.memberId); const memberDescription = effectiveMemberDescription(member); return { member_id: normalizedMemberId, - name: member.name.trim() || undefined, + name: displayNameForMember(member), role: effectiveMemberRole(member), - mode: member.instanceMode, - instance_mode: member.instanceMode, - runtime_type: member.runtimeType, + mode: FIXED_INSTANCE_MODE, + instance_mode: FIXED_INSTANCE_MODE, + runtime_type: FIXED_RUNTIME_TYPE, description: memberDescription || undefined, is_leader: member.isLeader, cpu_cores: member.cpuCores, memory_gb: member.memoryGb, disk_gb: member.diskGb, - gpu_enabled: member.gpuEnabled, - gpu_count: member.gpuEnabled ? member.gpuCount : 0, - image_registry: member.image.trim(), + gpu_enabled: false, + gpu_count: 0, + image_registry: openClawLiteImage, environment_overrides: buildMemberEnvironmentOverrides( member, normalizedMemberId, @@ -827,8 +531,9 @@ const CreateTeamPage: React.FC = () => { setError(null); const created = await teamService.createTeam(payload); navigate(`/teams/${created.team.id}`); - } catch (err: any) { - setError(err.response?.data?.error || "创建 Team 失败"); + } catch (err: unknown) { + const apiError = err as { response?: { data?: { error?: string } } }; + setError(apiError.response?.data?.error || "创建 Team 失败"); } finally { setSubmitting(false); } @@ -845,86 +550,6 @@ const CreateTeamPage: React.FC = () => {

-
-
- - -