package services

import (
	"archive/zip"
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	posixpath "path"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	"clawreef/internal/models"
	"clawreef/internal/repository"
	"clawreef/internal/services/k8s"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/tools/remotecommand"
)

const (
	teamSharedMountPath      = "/team"
	teamConfigFileName       = "team.json"
	teamIntroductionFileName = "team-introduction.md"
	teamAgentsFileName       = "AGENTS.md"
	teamSoulFileName         = "SOUL.md"
	teamManagedOverlayStart  = "<!-- CLAWMANAGER TEAM OVERLAY: START -->"
	teamManagedOverlayEnd    = "<!-- CLAWMANAGER TEAM OVERLAY: END -->"
	teamConfigMountDirPath   = "/etc/clawmanager/team"
	teamConfigMountPath      = teamConfigMountDirPath + "/" + teamConfigFileName
	teamHermesSoulMountPath  = "/config/.hermes/SOUL.md"
	teamSharedUID            = 1000
	teamSharedGID            = 1000
	teamSharedUmask          = "0002"
	teamRedisURLSecretKey    = "CLAWMANAGER_TEAM_REDIS_URL"
	teamTokenSecretKey       = "CLAWMANAGER_TEAM_TOKEN"
)

const (
	defaultTeamTaskStaleTimeout    = 30 * time.Minute
	teamTaskStaleSweepInterval     = 30 * time.Second
	teamConsumerScanInterval       = 10 * time.Second
	teamAssignmentMonitorEvery     = 3 * time.Minute
	teamAssignmentActivityFreshFor = 2 * time.Minute
	teamEventOutboxBatchSize       = 100

	initialLeaderTaskIntent = "team_bootstrap_introduction"
	teamTaskCompletionTool  = "team_complete_task"
	teamTaskReplyTarget     = "clawmanager"
)

const (
	teamCommunicationModeLeaderMediated = "leader_mediated"
	teamCommunicationModePeerAssisted   = "peer_assisted"
	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"
	teamPhaseCompletionPolicyExplicitV1   = "explicit-disposition-v1"

	teamCompletionDecisionAccepted          = "accepted"
	teamCompletionDecisionDeferred          = "deferred"
	teamCompletionDecisionNeedsConfirmation = "needs_confirmation"
	teamCompletionDecisionRejected          = "rejected"
	teamCompletionEvaluationVersion         = 2
)

var (
	teamMemberKeyPattern                = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
	teamMemberInstanceNameInvalidChars  = regexp.MustCompile(`[^a-z0-9-]+`)
	teamMemberInstanceNameRepeatedDashs = regexp.MustCompile(`-+`)
	teamTrailingSilentReplyPattern      = regexp.MustCompile(`(?i)(?:^|\r?\n[\t ]*|\*+)NO_REPLY[\t ]*$`)
	teamRuntimeControlReplyPattern      = regexp.MustCompile(`(?i)^Redis Team task completed[.!。！]?$`)
)

type TeamService interface {
	StartBackground(ctx context.Context)
	StopBackground()
	CreateTeam(userID int, req CreateTeamRequest) (*TeamDetailsPayload, error)
	ListTeams(userID, offset, limit int) (*TeamListPayload, error)
	GetTeam(userID, teamID int) (*TeamDetailsPayload, error)
	ListTeamTasks(userID, teamID, beforeID, limit int) (*TeamTasksHistoryPayload, error)
	ListTeamEvents(userID, teamID, beforeID, limit int) (*TeamEventsHistoryPayload, error)
	ListWorkspaceFiles(ctx context.Context, userID, teamID int, relPath string) (*TeamWorkspaceListPayload, error)
	PreviewWorkspaceFile(ctx context.Context, userID, teamID int, relPath string) (*TeamWorkspacePreviewPayload, error)
	DownloadWorkspaceFile(ctx context.Context, userID, teamID int, relPath string) (*TeamWorkspaceDownloadPayload, error)
	CreateWorkspaceFolder(ctx context.Context, userID, teamID int, req TeamWorkspaceFolderRequest) error
	RenameWorkspaceEntry(ctx context.Context, userID, teamID int, req TeamWorkspaceRenameRequest) error
	DeleteWorkspaceEntry(ctx context.Context, userID, teamID int, relPath string) error
	UploadWorkspaceFiles(ctx context.Context, userID, teamID int, targetPath string, files []*multipart.FileHeader, relativePaths []string) error
	DispatchTask(userID, teamID int, req DispatchTeamTaskRequest) (*TeamTaskPayload, error)
	DeleteTeam(userID, teamID int) error
	DeleteMember(userID, teamID int, memberID string) error
}

type CreateTeamRequest struct {
	Name              string                    `json:"name"`
	Description       *string                   `json:"description,omitempty"`
	CommunicationMode string                    `json:"communication_mode,omitempty"`
	RedisURL          string                    `json:"redis_url,omitempty"`
	SharedStorageGB   int                       `json:"shared_storage_gb,omitempty"`
	StorageClass      string                    `json:"storage_class,omitempty"`
	Members           []CreateTeamMemberRequest `json:"members"`
}

type CreateTeamMemberRequest struct {
	MemberID             string                        `json:"member_id,omitempty"`
	Name                 string                        `json:"name,omitempty"`
	Role                 string                        `json:"role"`
	Mode                 string                        `json:"mode,omitempty"`
	InstanceMode         string                        `json:"instance_mode,omitempty"`
	RuntimeType          string                        `json:"runtime_type,omitempty"`
	Description          *string                       `json:"description,omitempty"`
	CPUCores             float64                       `json:"cpu_cores,omitempty"`
	MemoryGB             int                           `json:"memory_gb,omitempty"`
	DiskGB               int                           `json:"disk_gb,omitempty"`
	GPUEnabled           bool                          `json:"gpu_enabled,omitempty"`
	GPUCount             int                           `json:"gpu_count,omitempty"`
	ImageRegistry        *string                       `json:"image_registry,omitempty"`
	ImageTag             *string                       `json:"image_tag,omitempty"`
	EnvironmentOverrides map[string]string             `json:"environment_overrides,omitempty"`
	RoleProfile          *TeamMemberRoleProfileRequest `json:"role_profile,omitempty"`
	OpenClawConfigPlan   *OpenClawConfigPlan           `json:"openclaw_config_plan,omitempty"`
	IsLeader             bool                          `json:"is_leader,omitempty"`
}

// TeamMemberRoleProfileRequest is the runtime-neutral semantic layer used by
// generated Team templates. Platform-owned AGENTS.md and Team protocol rules
// are deliberately not configurable here.
type TeamMemberRoleProfileRequest struct {
	SchemaVersion      int      `json:"schema_version,omitempty"`
	ProfileKey         string   `json:"profile_key,omitempty"`
	DisplayName        string   `json:"display_name,omitempty"`
	RoleHint           string   `json:"role_hint,omitempty"`
	Summary            string   `json:"summary,omitempty"`
	Mission            string   `json:"mission,omitempty"`
	Responsibilities   []string `json:"responsibilities,omitempty"`
	Boundaries         []string `json:"boundaries,omitempty"`
	ExpectedInputs     []string `json:"expected_inputs,omitempty"`
	Deliverables       []string `json:"deliverables,omitempty"`
	AcceptanceCriteria []string `json:"acceptance_criteria,omitempty"`
	CollaborationNotes []string `json:"collaboration_notes,omitempty"`
	CapabilityTags     []string `json:"capability_tags,omitempty"`
}

type DispatchTeamTaskRequest struct {
	TargetMemberID string                 `json:"target_member_id"`
	MessageID      string                 `json:"message_id,omitempty"`
	Payload        map[string]interface{} `json:"payload"`
}

type TeamListPayload struct {
	Teams []models.Team `json:"teams"`
	Total int           `json:"total"`
}

type TeamDetailsPayload struct {
	Team           *models.Team          `json:"team"`
	LeaderMemberID string                `json:"leader_member_id,omitempty"`
	Leader         *models.TeamMember    `json:"leader,omitempty"`
	Members        []models.TeamMember   `json:"members"`
	Tasks          []TeamTaskPayload     `json:"tasks,omitempty"`
	Events         []TeamEventPayload    `json:"events,omitempty"`
	WorkItems      []TeamWorkItemPayload `json:"work_items,omitempty"`
}

type TeamTasksHistoryPayload struct {
	Tasks        []TeamTaskPayload `json:"tasks"`
	HasMore      bool              `json:"has_more"`
	NextBeforeID *int              `json:"next_before_id,omitempty"`
}

type TeamEventsHistoryPayload struct {
	Events       []TeamEventPayload `json:"events"`
	HasMore      bool               `json:"has_more"`
	NextBeforeID *int               `json:"next_before_id,omitempty"`
}

type TeamWorkspaceFileEntry struct {
	Name        string `json:"name"`
	Path        string `json:"path"`
	Type        string `json:"type"`
	Size        int64  `json:"size"`
	ModifiedAt  string `json:"modified_at,omitempty"`
	Previewable bool   `json:"previewable"`
}

type TeamWorkspaceListPayload struct {
	Path    string                   `json:"path"`
	Root    string                   `json:"root"`
	Entries []TeamWorkspaceFileEntry `json:"entries"`
}

type TeamWorkspacePreviewPayload struct {
	Path    string `json:"path"`
	Name    string `json:"name"`
	Content string `json:"content"`
}

type TeamWorkspaceDownloadPayload struct {
	Path        string
	Name        string
	ContentType string
	Data        []byte
}

type TeamWorkspaceFolderRequest struct {
	Path string `json:"path"`
	Name string `json:"name"`
}

type TeamWorkspaceRenameRequest struct {
	Path    string `json:"path"`
	NewName string `json:"new_name"`
}

type TeamTaskPayload struct {
	models.TeamTask
	Payload map[string]interface{} `json:"payload,omitempty"`
	Result  map[string]interface{} `json:"result,omitempty"`
}

type TeamEventPayload struct {
	models.TeamEvent
	Payload map[string]interface{} `json:"payload,omitempty"`
}

type TeamWorkItemPayload struct {
	models.TeamWorkItem
	DependsOn    []string               `json:"depends_on,omitempty"`
	Result       map[string]interface{} `json:"result,omitempty"`
	ArtifactRefs []string               `json:"artifact_refs,omitempty"`
}

type teamService struct {
	repo                  repository.TeamRepository
	instanceService       InstanceService
	openClawConfigPlanner teamOpenClawConfigPlanner
	pvcService            *k8s.PVCService
	secretService         *k8s.SecretService
	configMapService      *k8s.ConfigMapService
	podService            *k8s.PodService

	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 {
	PlanWithoutTeamMemberLeaderOnlyChannels(userID int, plan *OpenClawConfigPlan) (*OpenClawConfigPlan, error)
}

type plannedTeamMember struct {
	Request       CreateTeamMemberRequest
	MemberKey     string
	DisplayName   string
	Role          string
	ProfileKey    string
	ProfileName   string
	EffectiveRole string
	RuntimeType   string
	InstanceMode  string
	IsLeader      bool
}

type teamVerificationRole string

const (
	teamVerificationRoleNone       teamVerificationRole = ""
	teamVerificationRoleEvidence   teamVerificationRole = "evidence"
	teamVerificationRoleCodeReview teamVerificationRole = "code-review"
	teamVerificationRoleAPITest    teamVerificationRole = "api-test"
)

type teamRuntimeSecrets struct {
	RedisURL string
	Token    string
}

type TeamServiceOption func(*teamService)

func WithTeamRuntimeWorkspaceRoot(root string) TeamServiceOption {
	return func(s *teamService) {
		if strings.TrimSpace(root) != "" {
			s.runtimeWorkspaceRoot = strings.TrimSpace(root)
		}
	}
}

func WithTeamOpenClawConfigService(service OpenClawConfigService) TeamServiceOption {
	return func(s *teamService) {
		s.openClawConfigPlanner = service
	}
}

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{}{},
		assignmentMonitorLast: map[string]time.Time{},
		runtimeWorkspaceRoot:  "/workspaces",
	}
	for _, opt := range opts {
		if opt != nil {
			opt(service)
		}
	}
	return service
}

// StartBackground starts the leader-only background workers: a periodic scan
// that ensures a Redis event consumer is running for every active team, and
// the stale-task monitor. It is safe to call repeatedly (a second call while
// running is a no-op) and can be called again after StopBackground, which is
// required for leader-election re-acquisition. HTTP request handling does not
// depend on these workers, so followers can still serve the API and the in-pod
// nginx data plane while only the leader runs them.
func (s *teamService) StartBackground(parent context.Context) {
	s.mu.Lock()
	if s.running {
		s.mu.Unlock()
		return
	}
	if parent == nil {
		parent = context.Background()
	}
	ctx, cancel := context.WithCancel(parent)
	s.ctx = ctx
	s.cancel = cancel
	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()

	fmt.Println("[TeamService] Starting leader-only background workers...")
	s.ensureStaleTaskMonitor(ctx)
}

// StopBackground stops all background workers and blocks until they have fully
// exited, so a subsequent StartBackground starts from a clean state with no
// goroutines from the previous generation still touching shared maps. It is
// idempotent.
func (s *teamService) StopBackground() {
	s.mu.Lock()
	if !s.running {
		s.mu.Unlock()
		return
	}
	s.running = false
	cancel := s.cancel
	s.mu.Unlock()

	fmt.Println("[TeamService] Stopping leader-only background workers...")
	if cancel != nil {
		cancel()
	}
	s.wg.Wait()

	s.mu.Lock()
	s.consumers = map[int]struct{}{}
	s.staleMonitorStarted = false
	s.mu.Unlock()
}

// consumerScanLoop periodically ensures a consumer goroutine exists for every
// active team. Team creation no longer starts consumers inline (that would run
// on whichever replica served the request); the leader picks up newly active
// teams here within teamConsumerScanInterval.
func (s *teamService) consumerScanLoop(ctx context.Context) {
	defer s.wg.Done()

	s.ensureConsumersForActiveTeams(ctx)

	ticker := time.NewTicker(teamConsumerScanInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			s.ensureConsumersForActiveTeams(ctx)
		}
	}
}

func (s *teamService) ensureConsumersForActiveTeams(ctx context.Context) {
	teams, err := s.repo.ListActiveTeams()
	if err != nil {
		fmt.Printf("Warning: failed to list active teams for consumer scan: %v\n", err)
		return
	}
	for i := range teams {
		s.ensureConsumer(ctx, teams[i].ID)
	}
}

func (s *teamService) CreateTeam(userID int, req CreateTeamRequest) (*TeamDetailsPayload, error) {
	req.Name = strings.TrimSpace(req.Name)
	if req.Name == "" {
		return nil, fmt.Errorf("team name is required")
	}
	if len(req.Members) == 0 {
		return nil, fmt.Errorf("team must include at least one member")
	}
	memberPlans, err := planTeamMembers(req.Name, req.Members)
	if err != nil {
		return nil, err
	}
	existingTeam, err := s.repo.GetTeamByUserIDAndName(userID, req.Name)
	if err != nil {
		return nil, err
	}
	if existingTeam != nil {
		if existingTeam.Status == models.TeamStatusFailed {
			if err := s.DeleteTeam(userID, existingTeam.ID); err != nil {
				return nil, err
			}
		} else {
			return nil, fmt.Errorf("team name already exists")
		}
	}

	communicationMode, err := normalizeTeamCommunicationMode(req.CommunicationMode)
	if err != nil {
		return nil, err
	}
	redisURL := strings.TrimSpace(req.RedisURL)
	if redisURL == "" {
		redisURL = defaultTeamRedisURL()
	}
	if redisURL == "" {
		return nil, fmt.Errorf("team redis url is required")
	}
	if _, err := newRedisBus(redisURL); err != nil {
		return nil, err
	}

	sharedStorageGB := req.SharedStorageGB
	if sharedStorageGB <= 0 {
		sharedStorageGB = 10
	}
	preflightTeam := &models.Team{
		ID:              0,
		Name:            req.Name,
		StorageClass:    optionalString(strings.TrimSpace(req.StorageClass)),
		SharedMountPath: teamSharedMountPath,
	}
	if err := s.instanceService.ValidateCreateRequests(userID, s.buildTeamMemberInstanceRequests(preflightTeam, memberPlans)); err != nil {
		return nil, err
	}

	now := time.Now().UTC()
	storageClass := optionalString(strings.TrimSpace(req.StorageClass))
	team := &models.Team{
		UserID:            userID,
		Name:              req.Name,
		Description:       req.Description,
		Status:            models.TeamStatusCreating,
		CommunicationMode: communicationMode,
		RedisEventsLastID: "0-0",
		SharedMountPath:   teamSharedMountPath,
		StorageClass:      storageClass,
		CreatedAt:         now,
		UpdatedAt:         now,
	}
	if err := s.repo.CreateTeam(team); err != nil {
		return nil, err
	}
	if err := s.instanceService.ValidateCreateRequests(userID, s.buildTeamMemberInstanceRequests(team, memberPlans)); err != nil {
		return nil, s.rollbackTeamCreation(userID, team, err)
	}

	runtimeSecrets, err := s.provisionTeamK8s(userID, team, redisURL, sharedStorageGB, strings.TrimSpace(req.StorageClass))
	if err != nil {
		return nil, s.rollbackTeamCreation(userID, team, err)
	}
	rosterJSON, err := s.upsertTeamRosterConfig(userID, team, memberPlans)
	if err != nil {
		return nil, s.rollbackTeamCreation(userID, team, err)
	}

	for _, memberPlan := range memberPlans {
		member, err := s.createTeamMemberInstance(userID, team, memberPlan, runtimeSecrets, rosterJSON)
		if err != nil {
			return nil, s.rollbackTeamCreation(userID, team, err)
		}
		member.Status = models.TeamMemberStatusIdle
		member.UpdatedAt = time.Now().UTC()
		if err := s.repo.UpdateMember(member); err != nil {
			return nil, s.rollbackTeamCreation(userID, team, err)
		}
	}

	team.Status = models.TeamStatusRunning
	team.UpdatedAt = time.Now().UTC()
	if err := s.repo.UpdateTeam(team); err != nil {
		return nil, err
	}
	// Background consumers / stale-task monitor are leader-only and started by
	// the leader's periodic scan (consumerScanLoop). Starting them here would
	// run them on whichever replica served the create request, bypassing
	// leader election, so we intentionally do not call ensureConsumer here.
	if err := s.dispatchInitialLeaderTask(userID, team); err != nil {
		fmt.Printf("Warning: failed to dispatch initial Team %d leader task: %v\n", team.ID, err)
		if recordErr := s.recordInitialLeaderTaskDispatchFailure(team.ID, err); recordErr != nil {
			fmt.Printf("Warning: failed to record Team %d initial leader task dispatch failure: %v\n", team.ID, recordErr)
		}
	}
	return s.GetTeam(userID, team.ID)
}

func (s *teamService) dispatchInitialLeaderTask(userID int, team *models.Team) error {
	if team == nil {
		return fmt.Errorf("team is required")
	}
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		return err
	}
	leader := findTeamLeader(activeTeamMembers(members))
	if leader == nil {
		return fmt.Errorf("team leader not found")
	}
	_, err = s.DispatchTask(userID, team.ID, DispatchTeamTaskRequest{
		TargetMemberID: leader.MemberKey,
		MessageID:      initialLeaderTaskMessageID(team.ID),
		Payload:        buildInitialLeaderTaskPayload(team.Name),
	})
	return err
}

func initialLeaderTaskMessageID(teamID int) string {
	return fmt.Sprintf("team-%d-bootstrap-introduction", teamID)
}

func (s *teamService) recordInitialLeaderTaskDispatchFailure(teamID int, cause error) error {
	now := time.Now().UTC()
	payload := map[string]interface{}{
		"v":         1,
		"event":     "bootstrap_dispatch_failed",
		"teamId":    strconv.Itoa(teamID),
		"intent":    initialLeaderTaskIntent,
		"messageId": initialLeaderTaskMessageID(teamID),
		"source":    "clawmanager",
	}
	if cause != nil {
		payload["diagnostic"] = cause.Error()
	}
	payloadJSON, err := marshalOptionalJSON(payload)
	if err != nil {
		return err
	}
	messageID := initialLeaderTaskMessageID(teamID)
	return s.repo.CreateEvent(&models.TeamEvent{
		TeamID:      teamID,
		MessageID:   &messageID,
		EventType:   "bootstrap_dispatch_failed",
		PayloadJSON: payloadJSON,
		OccurredAt:  &now,
		CreatedAt:   now,
	})
}

func buildTeamTaskEnvelope(teamID int, memberKey string, task *models.TeamTask, messageID string, taskPayload map[string]interface{}, memberContext map[string]string, now time.Time) map[string]interface{} {
	if taskPayload == nil {
		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")
	if prompt == "" {
		rawPayload, _ := marshalJSON(taskPayload)
		prompt = rawPayload
	}
	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",
		"to":                 memberKey,
		"memberId":           memberKey,
		"role":               memberContext["role"],
		"displayName":        memberContext["displayName"],
		"replyTo":            teamTaskReplyTarget,
		"requiresCompletion": true,
		"completionTool":     teamTaskCompletionTool,
		"resultSink": map[string]interface{}{
			"type":           "redis_stream",
			"eventsKey":      teamEventsKey(teamID),
			"successEvent":   "completion_proposed",
			"failureEvent":   "task_failed",
			"replyEvent":     "reply",
			"resultField":    "resultMarkdown",
			"summaryField":   "summary",
			"artifactField":  "artifactRefs",
			"completionTool": teamTaskCompletionTool,
		},
		"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)))
			}
			taskWorkPhysicalRoot := ""
			taskContextPhysicalRoot := ""
			if physicalRoot != "" && taskRef != "" {
				taskWorkPhysicalRoot = filepath.ToSlash(filepath.Join(physicalRoot, "work", taskRef))
				taskContextPhysicalRoot = filepath.ToSlash(filepath.Join(physicalRoot, "results", taskRef, "context"))
			}
			envelope["sharedWorkspace"] = map[string]interface{}{
				"physicalPath":                physicalRoot,
				"canonicalPrefix":             "/team",
				"memberArtifactPhysicalRoot":  memberArtifactPhysicalRoot,
				"memberArtifactCanonicalRoot": "/team/artifacts/" + taskRef + "/members/" + normalizeTeamMemberRouteKey(memberKey),
				"taskWorkPhysicalRoot":        taskWorkPhysicalRoot,
				"taskWorkCanonicalRoot":       "/team/work/" + taskRef,
				"taskContextPhysicalRoot":     taskContextPhysicalRoot,
				"taskContextCanonicalRoot":    "/team/results/" + taskRef + "/context",
			}
		}
	}
	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"
	}
	if envelope["title"] == "" {
		envelope["title"] = fmt.Sprintf("Team task %d", taskID)
	}
	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
	}
	taskWorkPhysicalRoot := ""
	taskWorkCanonicalRoot := ""
	taskContextPhysicalRoot := ""
	taskContextCanonicalRoot := ""
	if physicalRoot != "" && taskRef != "" {
		taskWorkPhysicalRoot = filepath.ToSlash(filepath.Join(physicalRoot, "work", taskRef))
		taskWorkCanonicalRoot = "/team/work/" + taskRef
		taskContextPhysicalRoot = filepath.ToSlash(filepath.Join(physicalRoot, "results", taskRef, "context"))
		taskContextCanonicalRoot = "/team/results/" + taskRef + "/context"
	}
	envelope["sharedWorkspace"] = map[string]interface{}{
		"physicalPath":                physicalRoot,
		"canonicalPrefix":             "/team",
		"memberArtifactPhysicalRoot":  memberPhysicalRoot,
		"memberArtifactCanonicalRoot": memberCanonicalRoot,
		"taskWorkPhysicalRoot":        taskWorkPhysicalRoot,
		"taskWorkCanonicalRoot":       taskWorkCanonicalRoot,
		"taskContextPhysicalRoot":     taskContextPhysicalRoot,
		"taskContextCanonicalRoot":    taskContextCanonicalRoot,
	}
}

func shouldRequestRootCoordinationRecovery(task *models.TeamTask, member *models.TeamMember, eventKind string, payload map[string]interface{}) bool {
	if task == nil || member == nil || isTerminalTeamTaskStatus(task.Status) {
		return false
	}
	if !strings.EqualFold(strings.TrimSpace(eventKind), "turn_finished_without_completion") ||
		!eventBool(payload, "activeTurnFinished", "active_turn_finished") ||
		eventBool(payload, "explicitCompletion", "explicit_completion") ||
		eventBool(payload, "rootTaskTerminal", "root_task_terminal") {
		return false
	}
	// Worker recovery is scoped to an authenticated assignment identity.  A
	// root owner may use its root identity, but no other member receives an
	// immediate reminder from an unbound observation.
	if member.ID != task.TargetMemberID && eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id") == "" {
		return false
	}
	// New Runtime observers publish an explicit, state-neutral classification.
	// Only a high-confidence actionable gap may request an immediate reminder.
	// Conflicting or unavailable evidence is left to the independent Monitor;
	// it must never cascade into another control-plane decision.
	if outcome := strings.ToLower(strings.TrimSpace(eventString(payload, "turnObservationOutcome", "turn_observation_outcome"))); outcome != "" {
		if eventBool(payload, "observationConflict", "observation_conflict") ||
			!eventBool(payload, "immediateRecoveryEligible", "immediate_recovery_eligible") {
			return false
		}
		if outcome != "retryable_tool_gap" && outcome != "completion_receipt_gap" {
			return false
		}
		if eventBool(payload, "downstreamAssignmentStarted", "downstream_assignment_started") {
			return false
		}
	} else if eventBool(payload, "hadOutboundAssignment", "had_outbound_assignment") {
		// Old observers did not distinguish a real downstream assignment from
		// an ordinary Team message. Preserve their conservative behavior.
		return false
	}
	// A recovery reminder receives one immediate model turn. If that turn still
	// does not act, the normal Monitor remains the fallback; recursively creating
	// reminders here would starve real inbox work and can form a self-loop.
	if eventInt(payload, "completionRecoveryAttempt", "completion_recovery_attempt") > 0 {
		return false
	}
	return true
}

func (s *teamService) createRootCoordinationRecovery(
	team *models.Team,
	bus *redisBus,
	task *models.TeamTask,
	leader *models.TeamMember,
	sourcePayload map[string]interface{},
	sourceEvent *models.TeamEvent,
) error {
	if s == nil || s.repo == nil || team == nil || task == nil || leader == nil || sourceEvent == nil {
		return nil
	}
	rootTaskRef := fmt.Sprintf("team-%d-task-%d", team.ID, task.ID)
	isLeaderOwner := isLeaderTeamMember(leader)
	recoveryKind := "root_assignment_recovery_requested"
	intent := "root_assignment_recovery"
	workID := "root-assignment-recovery"
	title := "Continue root task assignment"
	if isLeaderOwner {
		recoveryKind = "root_coordination_recovery_requested"
		intent = "root_coordination_recovery"
		workID = "leader-root-coordination"
		title = "Continue root task coordination"
	} else if activeWorkID := eventString(sourcePayload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id"); activeWorkID != "" {
		workID = activeWorkID
	} else {
		workID = rootTaskRef
	}
	sourceID := strings.TrimSpace(derefTeamString(sourceEvent.EventID))
	if sourceID == "" {
		sourceID = strings.TrimSpace(derefTeamString(sourceEvent.RedisStreamID))
	}
	if sourceID == "" {
		sourceID = fmt.Sprintf("event-%d", sourceEvent.ID)
	}
	eventID := fmt.Sprintf(
		"root-turn-recovery:%d:%d:%s",
		team.ID,
		task.ID,
		normalizeTeamRedisKeyPart(sourceID),
	)
	now := time.Now().UTC()
	summary := "The root-task owner turn ended without publishing a Team action; ClawManager requested a non-terminal recovery check."
	recoveryAttempt := teamMaxInt(eventInt(sourcePayload, "completionRecoveryAttempt", "completion_recovery_attempt")+1, 1)
	payload := map[string]interface{}{
		"event":                     recoveryKind,
		"type":                      recoveryKind,
		"eventKind":                 recoveryKind,
		"protocolVersion":           3,
		"source":                    "clawmanager_monitor",
		"sourceEventId":             sourceID,
		"nonAuthoritative":          true,
		"stateEffect":               "none",
		"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":                    workID,
		"assignmentId":              workID,
		"status":                    models.TeamTaskStatusRunning,
		"runtimeStatus":             models.TeamTaskStatusRunning,
		"availability":              models.TeamMemberAvailabilityBusy,
		"summary":                   summary,
		"workflowState":             task.WorkflowState,
		"planVersion":               task.PlanVersion,
		"ledgerVersion":             task.LedgerVersion,
		"visibleToChat":             false,
		"chatDigestEligible":        false,
		"monitor":                   true,
		"monitorType":               intent,
		"completionRecoveryAttempt": recoveryAttempt,
		"sourceTurnObservation":     eventString(sourcePayload, "turnObservationOutcome", "turn_observation_outcome"),
	}
	if eventBool(sourcePayload, "lastToolFailed", "last_tool_failed") {
		payload["lastToolFailed"] = true
		payload["lastToolName"] = eventString(sourcePayload, "lastToolName", "last_tool_name")
	}
	if value := eventString(sourcePayload, "lastToolError", "last_tool_error"); value != "" {
		payload["lastToolError"] = value
	}
	if value := eventString(sourcePayload, "lastToolCode", "last_tool_code"); value != "" {
		payload["lastToolCode"] = value
	}
	if candidates := normalizeContextRefs(firstTeamValue(sourcePayload, "targetCandidates", "target_candidates")); len(candidates) > 0 {
		payload["targetCandidates"] = candidates
	}
	payloadJSON, err := marshalOptionalJSON(payload)
	if err != nil {
		return err
	}
	recoveryEvent := &models.TeamEvent{
		TeamID:      team.ID,
		TaskID:      &task.ID,
		MemberID:    &leader.ID,
		MessageID:   &eventID,
		EventID:     &eventID,
		EventType:   recoveryKind,
		PayloadJSON: payloadJSON,
		OccurredAt:  &now,
		CreatedAt:   now,
	}
	promptLines := []string{
		"[ROOT_TASK_RECOVERY] The previous root-task owner turn ended without publishing a Team action. The root task remains open; this reminder is not a completion decision.",
		fmt.Sprintf("rootTaskId=%s rootMessageId=%s planVersion=%d ledgerVersion=%d", rootTaskRef, task.MessageID, task.PlanVersion, task.LedgerVersion),
	}
	if isLeaderOwner {
		promptLines = append(promptLines, "Inspect the current root-task ledger and continue the appropriate next step. If work must be delegated, publish/update the collaboration plan and call team_send. If assignments are already active, record only a concise progress update and wait for their results. If all required results are confirmed, synthesize the user-facing answer and call team_complete_task. If a real blocker exists, report it precisely so the Leader/control plane can recover. Do not claim completion before the task is actually complete.")
	} else {
		promptLines = append(promptLines, "Inspect the active root assignment and continue from its current evidence. If the deliverable is ready, call team_complete_task with the actual result and artifact links. If work is still in progress, call team_update_progress with a concise running update and continue. If a real blocker prevents progress, report it precisely for recovery. Do not claim completion before the deliverable is ready.")
	}
	if toolName := eventString(sourcePayload, "lastToolName", "last_tool_name"); toolName != "" {
		toolDiagnostic := fmt.Sprintf("The last Team tool was %s", toolName)
		if code := eventString(sourcePayload, "lastToolCode", "last_tool_code"); code != "" {
			toolDiagnostic += " (code=" + code + ")"
		}
		if detail := eventString(sourcePayload, "lastToolError", "last_tool_error"); detail != "" {
			toolDiagnostic += ": " + detail
		}
		if candidates := normalizeContextRefs(firstTeamValue(sourcePayload, "targetCandidates", "target_candidates")); len(candidates) > 0 {
			toolDiagnostic += ". Unambiguous candidates reported by Runtime: " + strings.Join(candidates, ", ")
		}
		promptLines = append(promptLines, toolDiagnostic+". Re-check the live task facts and correct the call only if that action is still required.")
	}
	prompt := strings.Join(promptLines, "\n")
	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":             workID,
		"assignmentId":       workID,
		"title":              title,
		"prompt":             prompt,
		"rawPrompt":          prompt,
		"monitorPolicy":      defaultTeamMonitorPolicy(),
		"turnOutcomePolicy": map[string]interface{}{
			"actionExpected":           true,
			"immediateRecoveryAllowed": false,
			"reason":                   "recovery_turn",
		},
		"metadata":  payload,
		"createdAt": now.Format(time.RFC3339Nano),
	}
	applyTeamTaskEnvelopeContext(envelope, task, leader.MemberKey)
	if refs := s.durableTeamTaskContextRefs(team, task); len(refs) > 0 {
		envelope["artifactRefs"] = refs
		envelope["contextRefs"] = refs
	}
	envelopeJSON, err := marshalJSON(envelope)
	if err != nil {
		return err
	}
	outbox := &models.TeamEventOutbox{
		TeamID:        team.ID,
		SourceEventID: eventID,
		Destination:   teamInboxKey(team.ID, leader.MemberKey),
		MessageID:     eventID,
		PayloadJSON:   envelopeJSON,
		Status:        "pending",
		AvailableAt:   now,
		CreatedAt:     now,
		UpdatedAt:     now,
	}
	if err := s.repo.CreateEventWithOutbox(recoveryEvent, outbox); err != nil {
		return err
	}
	if bus == nil || outbox.ID <= 0 {
		return nil
	}
	s.publishTeamRootWorkflowState(bus, task)
	if err := s.deliverTeamEventOutbox(team, bus, outbox); err != nil {
		_ = s.repo.MarkEventOutboxFailed(outbox.ID, now.Add(teamOutboxRetryDelay(outbox.Attempts)), err.Error())
		return nil
	}
	return s.repo.MarkEventOutboxDelivered(outbox.ID, time.Now().UTC())
}

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, 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.",
			"- If you write a detailed report, write it under \"$CLAWMANAGER_TEAM_SHARED_DIR/results/<taskId>/\" and report it only as /team/results/<taskId>/<file>.",
			"- Complete this bootstrap in the current turn by calling team_complete_task with status=\"succeeded\", summary, and resultMarkdown.",
			"- Successful bootstrap completion is recorded as the task_completed event.",
			"- Do not finish with tool calls only and do not wait for QA/review evidence for this bootstrap snapshot.",
		}, "\n")
		if base == "" {
			return instruction
		}
		return base + "\n\n" + instruction
	}
	if strings.Contains(base, teamTaskCompletionTool) && strings.Contains(base, "task_completed") {
		return base
	}
	mode := normalizedTeamCommunicationMode(communicationMode)
	modeInstructions := []string{
		"- Leader-mediated mode is a strict hub-and-spoke workflow: user root task -> Leader -> assigned workers -> Leader -> final user-facing result.",
		"- If you are the Leader and the user names a non-Leader member or role, you MUST delegate the work to that exact member with team_send, wait for the assigned workers' actual results, then provide a final synthesis before completing the root task.",
		"- If you are the Leader and the user gives a broad task without naming one member, first create a compact plan, decompose the work into owner/member_id assignments, send those assignments, wait for the assigned workers' actual results, verify them, then complete the root task.",
		"- If you are the Leader, answer self-contained control-plane or simple tasks directly only when the request clearly stays within the Leader/control-plane scope and does not require a named worker or multi-member evidence.",
		"- If you are a Worker, execute only the assignment addressed to you and report the result, evidence, artifact paths, or blocker back to the Leader. Do not hand off directly to another Worker.",
		"- A Leader dispatch, plan, or handoff is not a final result and must not call team_complete_task. Worker completion never closes the user root task. Only the Leader may finalize the root task after reconciling all required member outputs.",
	}
	switch mode {
	case teamCommunicationModePeerAssisted:
		modeInstructions = []string{
			"- Worker-direct mode: if the root task or collaboration plan names a downstream member, you MUST hand off to that exact member with team_send before completing your own step. This handoff is required, not optional.",
			"- In worker-direct mode, do not send a completed step only to the Leader when a downstream owner is specified. The Leader is the fallback only when no downstream owner is specified, when you are blocked, or when final synthesis is explicitly requested.",
			"- A worker-to-worker handoff must include rootTaskId/rootMessageId when available, artifact paths, the requested next action, acceptance criteria, and whether a reply is required.",
		}
	case teamCommunicationModeFullMesh:
		modeInstructions = []string{
			"- Full-mesh mode: coordinate directly with the named downstream owners. If a member is specified as the next owner, hand off to that exact member before completing your own step.",
			"- Preserve rootTaskId/rootMessageId, artifact paths, requested next action, acceptance criteria, and reply requirements in every peer handoff.",
		}
	}
	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. The Leader must use team_artifact_write with scope=\"team\", kind=\"plan\", path=\"collaboration-plan.md\" and then publish only the canonical /team path returned by that tool.",
		"- 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.",
		"- The Runtime restores the canonical root task and assignment from the active Team envelope. Reuse IDs supplied by ClawManager when present; if an optional taskId, assignmentId, or workId is uncertain, omit it instead of inventing a new identifier.",
		"- Prefer the Team artifact tools exposed by the Runtime for shared artifacts. When team_artifact_preview is available, use its managed URL before opening a Team file in Browser; never use file:// or start a temporary file server. Older Runtimes may not expose preview, so continue with file/static inspection instead of treating the missing tool as task failure. Worker output must use the assignment-specific member artifact root injected by the Runtime, even when an assignment body mentions a Team-root filename. Team-scoped writes must declare kind=plan, kind=context, kind=review, or kind=final and always use the canonical path returned by the tool; never invent /team links.",
		"- Before delegating research that used an external article, issue, API response, or repository, the Leader should persist the fetched evidence with team_artifact_write scope=\"team\", kind=\"context\" and pass the returned canonical reference to workers. Workers should reuse available contextRefs instead of repeatedly fetching the same source.",
		"- 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 owns validation assignment scope. For a production-only implementation assignment that will be checked downstream, set reviewRequired=true and state that the current member produces the artifact without running tests or acceptance checks. For every test, review, evidence, or verification assignment, set validationAssignment=true, validationTargetAssignmentId, validationTargetRevision, and dependsOn. Run assignments in parallel only when the published plan has no dependency path between them and every required input is independently ready; several members may receive different validation assignments in parallel only under that condition. If B depends on A, follow A -> B: registering B early does not mean B may execute before A succeeds. A waiting_dependencies receipt means B is registered but has not started and will release automatically; do not resend it. Set allowEarlyStart=true only when the published plan explicitly permits useful preflight work before the dependency is ready, never to claim final validation of a missing artifact. Before creating a newer revision, inspect the current attempt; a running/busy equivalent attempt with a recent heartbeat must be allowed to finish. These rules are role-agnostic: independent validators may run in parallel, while dependent validators must follow the plan. If the current member must both produce and validate, say so explicitly and mark that assignment as validation work rather than relying on the member's role name.",
		"- 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 required phase declared in leader_plan cannot disappear implicitly. If a planned phase is intentionally not started, the Leader must include phaseDispositions with phaseId, decision (cancelled, skipped, or superseded), and a concrete reason in team_complete_task. Omit phases whose assigned work already succeeded; the control plane closes those automatically. Never use a disposition for running or unfinished assigned work.",
		"- 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.",
		"- Do not use a pooled Runtime global /tmp directory for cross-member collaboration. Durable shared inputs belong under the current root task's Team context, and member scratch/output belongs under the injected assignment-specific member artifact directory.",
		"- When using shell commands for member work, write only below the injected member artifact physical root. Leaders may use Team-scoped artifact tools for plan, context, and final results; Reviewers may use kind=review for review reports.",
		"- 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.",
		"- Report shared artifact links using the canonical UI path /team/<relative-path>, even when a Lite runtime uses a different physical shared directory.",
		"- Members must report produced artifact paths and concrete outcomes through the Team channel before completing their assigned task.",
		"- When the final result is ready, call team_complete_task with status=\"succeeded\", summary, and resultMarkdown.",
		"- If the task fails, call team_complete_task with status=\"failed\" and an error message.",
		"- Do not send the final answer as a normal message to clawmanager; ClawManager consumes task_completed/task_failed events from the Team Redis event stream.",
	}, "\n")
	if base == "" {
		return instruction
	}
	return base + "\n\n" + instruction
}

func (s *teamService) provisionTeamK8s(userID int, team *models.Team, redisURL string, sharedStorageGB int, storageClass string) (*teamRuntimeSecrets, error) {
	ctx := context.Background()
	pvc, err := s.pvcService.CreateTeamSharedPVC(ctx, userID, team.ID, sharedStorageGB, storageClass)
	if err != nil {
		return nil, err
	}
	secretName := s.pvcService.GetClient().GetTeamSecretName(team.ID)
	teamToken, err := generatePrefixedToken("team")
	if err != nil {
		return nil, fmt.Errorf("failed to generate Team token: %w", err)
	}
	if err := s.secretService.UpsertSecret(ctx, userID, secretName, map[string]string{
		teamRedisURLSecretKey: redisURL,
		teamTokenSecretKey:    teamToken,
	}, map[string]string{
		"app":        "clawreef",
		"managed-by": "clawreef",
		"team-id":    strconv.Itoa(team.ID),
	}); err != nil {
		return nil, err
	}

	team.RedisURLSecretName = &secretName
	team.RedisURLSecretKey = optionalString(teamRedisURLSecretKey)
	team.TeamTokenSecretName = &secretName
	team.TeamTokenSecretKey = optionalString(teamTokenSecretKey)
	team.SharedPVCName = &pvc.Name
	team.SharedPVCNamespace = &pvc.Namespace
	team.UpdatedAt = time.Now().UTC()
	if err := s.repo.UpdateTeam(team); err != nil {
		return nil, err
	}
	return &teamRuntimeSecrets{RedisURL: redisURL, Token: teamToken}, nil
}

func (s *teamService) upsertTeamRosterConfig(userID int, team *models.Team, members []plannedTeamMember) (string, error) {
	rosterJSON, err := buildTeamRosterConfig(team, members)
	if err != nil {
		return "", err
	}
	data := buildTeamRosterConfigData(rosterJSON, team, members)
	if err := s.configMapService.UpsertConfigMap(context.Background(), userID, s.teamConfigMapName(team.ID), data, map[string]string{
		"app":        "clawreef",
		"managed-by": "clawreef",
		"team-id":    strconv.Itoa(team.ID),
	}); 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 := writeManagedTeamContextFile(path, []byte(rosterJSON), 0o664); err != nil {
		return fmt.Errorf("failed to write shared Team roster %s: %w", path, err)
	}
	return nil
}

func writeManagedTeamContextFile(target string, content []byte, mode os.FileMode) error {
	target = filepath.Clean(target)
	parent := filepath.Dir(target)
	if target == "." || target == string(filepath.Separator) || parent == "." || parent == string(filepath.Separator) {
		return fmt.Errorf("invalid managed Team context path %q", target)
	}
	if err := os.MkdirAll(parent, 0o2775); err != nil {
		return err
	}
	temp, err := os.CreateTemp(parent, "."+filepath.Base(target)+".tmp-*")
	if err != nil {
		return err
	}
	tempPath := temp.Name()
	cleanup := true
	defer func() {
		if cleanup {
			_ = os.Remove(tempPath)
		}
	}()
	if err := temp.Chmod(mode); err != nil {
		_ = temp.Close()
		return err
	}
	if _, err := temp.Write(content); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Sync(); err != nil {
		_ = temp.Close()
		return err
	}
	if err := temp.Close(); err != nil {
		return err
	}
	chownTeamWorkspacePath(tempPath)
	if err := os.Rename(tempPath, target); err != nil {
		return err
	}
	cleanup = false
	_ = os.Chmod(target, mode)
	chownTeamWorkspacePath(target)
	return nil
}

func buildTeamRosterConfigData(rosterJSON string, team *models.Team, members []plannedTeamMember) map[string]string {
	data := map[string]string{
		teamConfigFileName: rosterJSON,
	}
	for _, member := range members {
		if member.RuntimeType != "hermes" {
			continue
		}
		data[teamMemberSoulConfigKey(member.MemberKey)] = buildTeamMemberSoulMarkdown(member, normalizedTeamCommunicationMode(team.CommunicationMode))
	}
	return data
}

func (s *teamService) teamConfigMapName(teamID int) string {
	client := k8s.GetClient()
	if client == nil {
		return fmt.Sprintf("clawreef-team-%d-config", teamID)
	}
	return client.GetTeamConfigMapName(teamID)
}

func (s *teamService) createTeamMemberInstance(userID int, team *models.Team, memberPlan plannedTeamMember, runtimeSecrets *teamRuntimeSecrets, rosterJSON string) (*models.TeamMember, error) {
	now := time.Now().UTC()
	member := &models.TeamMember{
		TeamID:       team.ID,
		UserID:       userID,
		MemberKey:    memberPlan.MemberKey,
		DisplayName:  memberPlan.DisplayName,
		Role:         memberPlan.Role,
		RuntimeType:  memberPlan.RuntimeType,
		InstanceMode: memberPlan.InstanceMode,
		Description:  optionalString(plannedTeamMemberDescription(memberPlan)),
		Status:       models.TeamMemberStatusCreating,
		Availability: models.TeamMemberAvailabilityUnknown,
		CreatedAt:    now,
		UpdatedAt:    now,
	}
	if err := s.repo.CreateMember(member); err != nil {
		return nil, err
	}

	createReq := s.buildTeamMemberInstanceRequestWithSecrets(team, memberPlan, runtimeSecrets, rosterJSON)
	memberOpenClawPlan, err := s.openClawConfigPlanForTeamMember(userID, memberPlan)
	if err != nil {
		member.Status = models.TeamMemberStatusFailed
		member.UpdatedAt = time.Now().UTC()
		_ = s.repo.UpdateMember(member)
		return nil, err
	}
	createReq.OpenClawConfigPlan = memberOpenClawPlan
	instance, err := s.instanceService.CreatePrevalidated(userID, createReq)
	if err != nil {
		member.Status = models.TeamMemberStatusFailed
		member.UpdatedAt = time.Now().UTC()
		_ = s.repo.UpdateMember(member)
		return nil, err
	}
	if err := s.writeLiteTeamMemberIdentityFiles(instance, team, memberPlan, rosterJSON); err != nil {
		member.Status = models.TeamMemberStatusFailed
		member.UpdatedAt = time.Now().UTC()
		_ = s.repo.UpdateMember(member)
		return nil, err
	}
	member.InstanceID = &instance.ID
	member.UpdatedAt = time.Now().UTC()
	if err := s.repo.UpdateMember(member); err != nil {
		return nil, err
	}
	return member, nil
}

func (s *teamService) openClawConfigPlanForTeamMember(userID int, memberPlan plannedTeamMember) (*OpenClawConfigPlan, error) {
	plan := memberPlan.Request.OpenClawConfigPlan
	if plan == nil || memberPlan.IsLeader || s.openClawConfigPlanner == nil {
		return plan, nil
	}
	return s.openClawConfigPlanner.PlanWithoutTeamMemberLeaderOnlyChannels(userID, plan)
}

func (s *teamService) buildTeamMemberInstanceRequests(team *models.Team, memberPlans []plannedTeamMember) []CreateInstanceRequest {
	requests := make([]CreateInstanceRequest, 0, len(memberPlans))
	for _, memberPlan := range memberPlans {
		requests = append(requests, s.buildTeamMemberInstanceRequest(team, memberPlan))
	}
	return requests
}

func (s *teamService) buildTeamMemberInstanceRequest(team *models.Team, memberPlan plannedTeamMember) CreateInstanceRequest {
	return s.buildTeamMemberInstanceRequestWithSecrets(team, memberPlan, nil, "")
}

func (s *teamService) buildTeamMemberInstanceRequestWithSecrets(team *models.Team, memberPlan plannedTeamMember, runtimeSecrets *teamRuntimeSecrets, rosterJSON string) CreateInstanceRequest {
	req := memberPlan.Request
	instanceMode := memberPlan.InstanceMode
	if instanceMode == "" {
		instanceMode = InstanceModeLite
	}
	runtimeBackendType, _ := RuntimeTypeForInstanceMode(instanceMode)
	memberEnv := s.teamMemberEnv(team, memberPlan)
	if instanceMode == InstanceModeLite {
		memberEnv["CLAWMANAGER_TEAM_SHARED_DIR"] = s.teamRuntimeSharedPath(team)
	}
	environmentOverrides := mergeEnvMaps(req.EnvironmentOverrides, memberEnv)
	if instanceMode == InstanceModeLite && runtimeSecrets != nil {
		environmentOverrides = mergeEnvMaps(environmentOverrides, map[string]string{
			teamRedisURLSecretKey: runtimeSecrets.RedisURL,
			teamTokenSecretKey:    runtimeSecrets.Token,
		})
		if strings.TrimSpace(rosterJSON) != "" {
			environmentOverrides["CLAWMANAGER_TEAM_CONFIG_JSON"] = rosterJSON
		}
	}
	return CreateInstanceRequest{
		Name:                 teamMemberInstanceName(team.Name, team.ID, memberPlan.MemberKey),
		Type:                 memberPlan.RuntimeType,
		Mode:                 instanceMode,
		InstanceMode:         instanceMode,
		RuntimeType:          runtimeBackendType,
		CPUCores:             defaultFloat(req.CPUCores, 2),
		MemoryGB:             defaultInt(req.MemoryGB, 4),
		DiskGB:               defaultInt(req.DiskGB, 20),
		GPUEnabled:           req.GPUEnabled,
		GPUCount:             req.GPUCount,
		OSType:               memberPlan.RuntimeType,
		OSVersion:            "latest",
		ImageRegistry:        req.ImageRegistry,
		ImageTag:             req.ImageTag,
		EnvironmentOverrides: environmentOverrides,
		StorageClass:         derefTeamString(team.StorageClass),
		OpenClawConfigPlan:   req.OpenClawConfigPlan,
		Team: &TeamInstanceConfig{
			Environment:      memberEnv,
			SecretName:       derefTeamString(team.TeamTokenSecretName),
			SharedPVCName:    derefTeamString(team.SharedPVCName),
			SharedMountPath:  team.SharedMountPath,
			ConfigMapName:    s.teamConfigMapName(team.ID),
			ConfigMountPath:  teamConfigMountDirPath,
			PersonaConfigKey: teamMemberPersonaConfigKey(memberPlan),
			SharedUID:        teamSharedUID,
			SharedGID:        teamSharedGID,
			SharedUmask:      teamSharedUmask,
		},
	}
}

func (s *teamService) teamRuntimeSharedPath(team *models.Team) string {
	if team == nil {
		return k8s.TeamSharedWorkspacePath(s.runtimeWorkspaceRoot, 0, 0)
	}
	return k8s.TeamSharedWorkspacePath(s.runtimeWorkspaceRoot, team.UserID, team.ID)
}

func (s *teamService) teamRuntimeSharedPathFor(userID, teamID int) string {
	return k8s.TeamSharedWorkspacePath(s.runtimeWorkspaceRoot, userID, teamID)
}

func (s *teamService) teamMemberEnv(team *models.Team, member plannedTeamMember) map[string]string {
	managerBaseURL, _ := defaultTeamManagerBaseURL()
	memberContext := buildPlannedTeamMemberTaskContext(member)
	communicationMode := normalizedTeamCommunicationMode(team.CommunicationMode)
	collaborationPolicy := buildTeamCollaborationPolicy(communicationMode)
	collaborationPolicyJSON, _ := json.Marshal(collaborationPolicy)
	env := map[string]string{
		"CLAWMANAGER_TEAM_ENABLED":            "true",
		"CLAWMANAGER_TEAM_ID":                 strconv.Itoa(team.ID),
		"CLAWMANAGER_TEAM_MEMBER_ID":          member.MemberKey,
		"CLAWMANAGER_TEAM_ROLE":               effectiveTeamMemberRole(member),
		"CLAWMANAGER_TEAM_EFFECTIVE_ROLE":     effectiveTeamMemberRole(member),
		"CLAWMANAGER_TEAM_RUNTIME_TYPE":       member.RuntimeType,
		"CLAWMANAGER_TEAM_PROTOCOL_VERSION":   "4",
		"CLAWMANAGER_TEAM_COMMUNICATION_MODE": communicationMode,
		"CLAWMANAGER_TEAM_SHARED_DIR":         team.SharedMountPath,
		"CLAWMANAGER_TEAM_SHARED_UID":         strconv.Itoa(teamSharedUID),
		"CLAWMANAGER_TEAM_SHARED_GID":         strconv.Itoa(teamSharedGID),
		"CLAWMANAGER_TEAM_UMASK":              teamSharedUmask,
		"PUID":                                strconv.Itoa(teamSharedUID),
		"PGID":                                strconv.Itoa(teamSharedGID),
		"UMASK":                               teamSharedUmask,
		"CLAWMANAGER_TEAM_CONFIG_PATH":        teamConfigMountPath,
		"CLAWMANAGER_TEAM_AUTORUN":            "true",
		"CLAWMANAGER_TEAM_CONSUMER_GROUP":     "team-members",
		"CLAWMANAGER_TEAM_INBOX_KEY":          teamInboxKey(team.ID, member.MemberKey),
		"CLAWMANAGER_TEAM_EVENTS_KEY":         teamEventsKey(team.ID),
		"CLAWMANAGER_TEAM_PRESENCE_KEY":       teamPresenceKey(team.ID),
		"CLAWMANAGER_TEAM_DLQ_KEY":            teamDLQKey(team.ID),
		"CLAWMANAGER_TEAM_MANAGER_URL":        managerBaseURL,
		"GATEWAY_ALLOW_ALL_USERS":             "true",
	}
	if len(collaborationPolicyJSON) > 0 {
		env["CLAWMANAGER_TEAM_COLLABORATION_POLICY_JSON"] = string(collaborationPolicyJSON)
	}
	if proxyURL, ok := defaultEgressProxyURL(); ok {
		env["CLAWMANAGER_BROWSER_PROXY_URL"] = proxyURL
		if previewOrigin, previewOK := defaultTeamPreviewOrigin(); previewOK {
			env["CLAWMANAGER_TEAM_PREVIEW_ORIGIN"] = previewOrigin
		}
	}
	if profileKey := strings.TrimSpace(member.ProfileKey); profileKey != "" {
		env["CLAWMANAGER_TEAM_PROFILE_KEY"] = profileKey
	}
	if profileName := strings.TrimSpace(member.ProfileName); profileName != "" {
		env["CLAWMANAGER_TEAM_PROFILE_NAME"] = profileName
	}
	if description := strings.TrimSpace(memberContext["description"]); description != "" {
		env["CLAWMANAGER_TEAM_MEMBER_DESCRIPTION"] = description
	}
	if systemPrompt := strings.TrimSpace(memberContext["systemPrompt"]); systemPrompt != "" {
		systemPrompt = appendTeamCollaborationGuidance(systemPrompt, communicationMode)
		systemPrompt = appendTeamWorkspaceGuidance(systemPrompt)
		env["CLAWMANAGER_TEAM_SYSTEM_PROMPT"] = systemPrompt
		env["HERMES_AGENT_HELP_GUIDANCE"] = systemPrompt
	}
	return env
}

func teamMemberPersonaConfigKey(member plannedTeamMember) string {
	if member.RuntimeType != "hermes" {
		return ""
	}
	return teamMemberSoulConfigKey(member.MemberKey)
}

func teamMemberSoulConfigKey(memberKey string) string {
	key := normalizeTeamMemberKeyForInstanceName(memberKey)
	if key == "" {
		key = "member"
	}
	return fmt.Sprintf("hermes-soul-%s.md", key)
}

func (s *teamService) ListTeams(userID, offset, limit int) (*TeamListPayload, error) {
	teams, err := s.repo.ListTeamsByUserID(userID, offset, limit)
	if err != nil {
		return nil, err
	}
	teams = activeTeams(teams)
	total, err := s.repo.CountTeamsByUserID(userID)
	if err != nil {
		return nil, err
	}
	return &TeamListPayload{Teams: teams, Total: total}, nil
}

func (s *teamService) GetTeam(userID, teamID int) (*TeamDetailsPayload, error) {
	team, err := s.requireOwnedTeam(userID, teamID)
	if err != nil {
		return nil, err
	}
	members, err := s.repo.ListMembersByTeamID(teamID)
	if err != nil {
		return nil, err
	}
	members = activeTeamMembers(members)
	tasks, err := s.repo.ListTasksByTeamID(teamID, 20)
	if err != nil {
		return nil, err
	}
	events, err := s.repo.ListEventsByTeamID(teamID, 200)
	if err != nil {
		return nil, err
	}
	workItems, err := s.repo.ListWorkItemsByTeamID(teamID, 200)
	if err != nil {
		return nil, err
	}
	leader := findTeamLeader(members)
	return &TeamDetailsPayload{
		Team:           team,
		LeaderMemberID: leaderMemberKey(leader),
		Leader:         leader,
		Members:        members,
		Tasks:          teamTaskPayloads(tasks),
		Events:         teamEventPayloads(events),
		WorkItems:      teamWorkItemPayloads(workItems),
	}, nil
}

func (s *teamService) ListTeamTasks(userID, teamID, beforeID, limit int) (*TeamTasksHistoryPayload, error) {
	if _, err := s.requireOwnedTeam(userID, teamID); err != nil {
		return nil, err
	}
	limit = normalizeTeamHistoryLimit(limit, 20, 100)
	tasks, err := s.repo.ListTasksBeforeID(teamID, beforeID, limit+1)
	if err != nil {
		return nil, err
	}
	hasMore := len(tasks) > limit
	if hasMore {
		tasks = tasks[:limit]
	}
	payload := teamTaskPayloads(tasks)
	return &TeamTasksHistoryPayload{
		Tasks:        payload,
		HasMore:      hasMore,
		NextBeforeID: nextTeamTaskBeforeID(payload),
	}, nil
}

func (s *teamService) ListTeamEvents(userID, teamID, beforeID, limit int) (*TeamEventsHistoryPayload, error) {
	if _, err := s.requireOwnedTeam(userID, teamID); err != nil {
		return nil, err
	}
	limit = normalizeTeamHistoryLimit(limit, 50, 200)
	events, err := s.repo.ListEventsBeforeID(teamID, beforeID, limit+1)
	if err != nil {
		return nil, err
	}
	hasMore := len(events) > limit
	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: nextBeforeID,
	}, nil
}

func (s *teamService) ListWorkspaceFiles(ctx context.Context, userID, teamID int, relPath string) (*TeamWorkspaceListPayload, error) {
	cleanPath, err := cleanTeamWorkspacePath(relPath)
	if err != nil {
		return nil, err
	}
	team, root, target, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, cleanPath)
	if err != nil {
		return nil, err
	}
	info, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("Team workspace path not found")
		}
		return nil, fmt.Errorf("failed to inspect Team workspace: %w", err)
	}
	if !info.IsDir() {
		return nil, fmt.Errorf("Team workspace path is not a folder")
	}
	dirEntries, err := os.ReadDir(target)
	if err != nil {
		return nil, fmt.Errorf("failed to list Team workspace: %w", err)
	}
	entries := make([]TeamWorkspaceFileEntry, 0, len(dirEntries))
	for _, dirEntry := range dirEntries {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		info, err := dirEntry.Info()
		if err != nil {
			return nil, fmt.Errorf("failed to inspect Team workspace entry %q: %w", dirEntry.Name(), err)
		}
		entries = append(entries, teamWorkspaceFileEntryFromInfo(cleanPath, info))
	}
	sortTeamWorkspaceEntries(entries)
	return &TeamWorkspaceListPayload{
		Path:    cleanPath,
		Root:    teamWorkspaceDisplayRoot(team, root),
		Entries: entries,
	}, nil
}

func (s *teamService) PreviewWorkspaceFile(ctx context.Context, userID, teamID int, relPath string) (*TeamWorkspacePreviewPayload, error) {
	cleanPath, err := cleanTeamWorkspacePath(relPath)
	if err != nil {
		return nil, err
	}
	if cleanPath == "" || !isPreviewableWorkspaceFile(cleanPath) {
		return nil, fmt.Errorf("only md, txt, and json files can be previewed")
	}
	_, _, target, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, cleanPath)
	if err != nil {
		return nil, err
	}
	info, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("Team workspace file not found")
		}
		return nil, fmt.Errorf("failed to inspect Team workspace file: %w", err)
	}
	if info.IsDir() {
		return nil, fmt.Errorf("Team workspace entry is a folder")
	}
	if info.Size() > 1048576 {
		return nil, fmt.Errorf("Team workspace file is too large to preview")
	}
	raw, err := os.ReadFile(target)
	if err != nil {
		return nil, fmt.Errorf("failed to preview Team workspace file: %w", err)
	}
	return &TeamWorkspacePreviewPayload{
		Path:    cleanPath,
		Name:    posixpath.Base(cleanPath),
		Content: string(raw),
	}, nil
}

func (s *teamService) DownloadWorkspaceFile(ctx context.Context, userID, teamID int, relPath string) (*TeamWorkspaceDownloadPayload, error) {
	cleanPath, err := cleanTeamWorkspacePath(relPath)
	if err != nil {
		return nil, err
	}
	if cleanPath == "" {
		return nil, fmt.Errorf("file path is required")
	}
	_, _, target, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, cleanPath)
	if err != nil {
		return nil, err
	}
	info, err := os.Stat(target)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("Team workspace entry not found")
		}
		return nil, fmt.Errorf("failed to inspect Team workspace entry: %w", err)
	}
	if info.IsDir() {
		data, err := zipTeamWorkspaceDirectory(ctx, target)
		if err != nil {
			return nil, err
		}
		return &TeamWorkspaceDownloadPayload{
			Path:        cleanPath,
			Name:        posixpath.Base(cleanPath) + ".zip",
			ContentType: "application/zip",
			Data:        data,
		}, nil
	}
	data, err := os.ReadFile(target)
	if err != nil {
		return nil, fmt.Errorf("failed to download Team workspace file: %w", err)
	}
	return &TeamWorkspaceDownloadPayload{
		Path:        cleanPath,
		Name:        posixpath.Base(cleanPath),
		ContentType: "application/octet-stream",
		Data:        data,
	}, nil
}

func (s *teamService) CreateWorkspaceFolder(ctx context.Context, userID, teamID int, req TeamWorkspaceFolderRequest) error {
	parent, err := cleanTeamWorkspacePath(req.Path)
	if err != nil {
		return err
	}
	name, err := cleanWorkspaceEntryName(req.Name)
	if err != nil {
		return err
	}
	_, _, target, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, joinTeamWorkspacePath(parent, name))
	if err != nil {
		return err
	}
	if err := ensureTeamWorkspaceDirectory(target); err != nil {
		return fmt.Errorf("failed to create Team workspace folder: %w", err)
	}
	return nil
}

func (s *teamService) RenameWorkspaceEntry(ctx context.Context, userID, teamID int, req TeamWorkspaceRenameRequest) error {
	cleanPath, err := cleanTeamWorkspacePath(req.Path)
	if err != nil {
		return err
	}
	if cleanPath == "" {
		return fmt.Errorf("path is required")
	}
	newName, err := cleanWorkspaceEntryName(req.NewName)
	if err != nil {
		return err
	}
	parent := posixpath.Dir(cleanPath)
	if parent == "." {
		parent = ""
	}
	_, _, source, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, cleanPath)
	if err != nil {
		return err
	}
	_, _, target, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, joinTeamWorkspacePath(parent, newName))
	if err != nil {
		return err
	}
	if _, err := os.Stat(source); err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("Team workspace entry not found")
		}
		return fmt.Errorf("failed to inspect Team workspace entry: %w", err)
	}
	if _, err := os.Stat(target); err == nil {
		return fmt.Errorf("target Team workspace entry already exists")
	} else if !os.IsNotExist(err) {
		return fmt.Errorf("failed to inspect target Team workspace entry: %w", err)
	}
	if err := os.Rename(source, target); err != nil {
		return fmt.Errorf("failed to rename Team workspace entry: %w", err)
	}
	return nil
}

func (s *teamService) DeleteWorkspaceEntry(ctx context.Context, userID, teamID int, relPath string) error {
	cleanPath, err := cleanTeamWorkspacePath(relPath)
	if err != nil {
		return err
	}
	if cleanPath == "" {
		return fmt.Errorf("cannot delete workspace root")
	}
	_, _, target, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, cleanPath)
	if err != nil {
		return err
	}
	if _, err := os.Stat(target); err != nil {
		if os.IsNotExist(err) {
			return fmt.Errorf("Team workspace entry not found")
		}
		return fmt.Errorf("failed to inspect Team workspace entry: %w", err)
	}
	if err := os.RemoveAll(target); err != nil {
		return fmt.Errorf("failed to delete Team workspace entry: %w", err)
	}
	return nil
}

func (s *teamService) UploadWorkspaceFiles(ctx context.Context, userID, teamID int, targetPath string, files []*multipart.FileHeader, relativePaths []string) error {
	if len(files) == 0 {
		return fmt.Errorf("no files uploaded")
	}
	basePath, err := cleanTeamWorkspacePath(targetPath)
	if err != nil {
		return err
	}
	for index, fileHeader := range files {
		if fileHeader == nil {
			continue
		}
		uploadName := fileHeader.Filename
		if index < len(relativePaths) && strings.TrimSpace(relativePaths[index]) != "" {
			uploadName = relativePaths[index]
		}
		uploadPath, err := cleanTeamWorkspacePath(uploadName)
		if err != nil {
			return err
		}
		if uploadPath == "" {
			return fmt.Errorf("uploaded file name is required")
		}
		_, _, destination, err := s.resolveTeamWorkspacePath(ctx, userID, teamID, joinTeamWorkspacePath(basePath, uploadPath))
		if err != nil {
			return err
		}
		file, err := fileHeader.Open()
		if err != nil {
			return fmt.Errorf("failed to open uploaded file: %w", err)
		}
		if err := ensureTeamWorkspaceDirectory(filepath.Dir(destination)); err != nil {
			_ = file.Close()
			return fmt.Errorf("failed to create Team workspace upload folder: %w", err)
		}
		out, err := os.OpenFile(destination, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0664)
		if err != nil {
			_ = file.Close()
			return fmt.Errorf("failed to create Team workspace upload file: %w", err)
		}
		_, copyErr := io.Copy(out, file)
		closeErr := out.Close()
		_ = file.Close()
		if copyErr != nil {
			return fmt.Errorf("failed to upload Team workspace file: %w", copyErr)
		}
		if closeErr != nil {
			return fmt.Errorf("failed to close Team workspace upload file: %w", closeErr)
		}
		chownTeamWorkspacePath(destination)
	}
	return nil
}

func (s *teamService) DispatchTask(userID, teamID int, req DispatchTeamTaskRequest) (*TeamTaskPayload, error) {
	team, err := s.requireOwnedTeam(userID, teamID)
	if err != nil {
		return nil, err
	}
	memberKey := strings.TrimSpace(req.TargetMemberID)
	if memberKey == "" {
		members, err := s.repo.ListMembersByTeamID(teamID)
		if err != nil {
			return nil, err
		}
		memberKey = leaderMemberKey(findTeamLeader(activeTeamMembers(members)))
	}
	if memberKey == "" {
		return nil, fmt.Errorf("target member id is required")
	}
	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"
	}
	if _, exists := req.Payload["anchorEligible"]; !exists {
		req.Payload["anchorEligible"] = true
	}
	if strings.TrimSpace(eventString(req.Payload, "intent")) == initialLeaderTaskIntent {
		if err := s.enrichBootstrapTaskPayload(userID, team, req.Payload); err != nil {
			return nil, err
		}
	}
	member, err := s.repo.GetMemberByTeamKey(teamID, memberKey)
	if err != nil {
		return nil, err
	}
	if member == nil {
		return nil, fmt.Errorf("team member not found")
	}
	// Preserve the legacy fail-open behavior when optional instance metadata is
	// temporarily unavailable; task dispatch must not acquire a new dependency
	// on that lookup.
	memberInstance, _ := s.teamMemberInstance(member)
	if err := repairLitePromptWorkspaceOwnership(memberInstance); err != nil {
		return nil, err
	}

	messageID := strings.TrimSpace(req.MessageID)
	if messageID == "" {
		messageID = fmt.Sprintf("team-%d-task-%d", teamID, time.Now().UTC().UnixNano())
	}
	existing, err := s.repo.GetTaskByMessageID(teamID, messageID)
	if err != nil {
		return nil, err
	}
	if existing != nil {
		if existing.TargetMemberID != member.ID {
			return nil, fmt.Errorf("team task message id already exists")
		}
		if existing.Status != models.TeamTaskStatusPending || existing.RedisStreamID != nil {
			return teamTaskPayload(*existing)
		}
	} else {
		payloadJSON, err := marshalJSON(req.Payload)
		if err != nil {
			return nil, fmt.Errorf("failed to encode task payload: %w", err)
		}
		now := time.Now().UTC()
		existing = &models.TeamTask{
			TeamID:         teamID,
			TargetMemberID: member.ID,
			CreatedBy:      &userID,
			MessageID:      messageID,
			Status:         models.TeamTaskStatusPending,
			WorkflowState:  teamWorkflowStatePlanning,
			PlanVersion:    0,
			LedgerVersion:  0,
			PayloadJSON:    payloadJSON,
			CreatedAt:      now,
			UpdatedAt:      now,
		}
		if err := s.repo.CreateTask(existing); err != nil {
			return nil, err
		}
	}
	task := existing
	if err := s.prepareTeamTaskWorkspace(userID, team.ID, task.ID); 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()
	memberContext := buildTeamMemberTaskContext(member, memberInstance)
	communicationMode := normalizedTeamCommunicationMode(team.CommunicationMode)
	memberContext["communicationMode"] = communicationMode
	memberContext["systemPrompt"] = appendTeamCollaborationGuidance(memberContext["systemPrompt"], communicationMode)
	memberContext["systemPrompt"] = appendTeamWorkspaceGuidance(memberContext["systemPrompt"])
	envelope := buildTeamTaskEnvelope(teamID, member.MemberKey, task, messageID, taskPayload, memberContext, now)
	envelopeJSON, err := marshalJSON(envelope)
	if err != nil {
		return nil, fmt.Errorf("failed to encode task envelope: %w", err)
	}
	streamID, err := bus.XAdd(context.Background(), teamInboxKey(team.ID, member.MemberKey), map[string]string{
		"payload":    envelopeJSON,
		"team_id":    strconv.Itoa(team.ID),
		"task_id":    strconv.Itoa(task.ID),
		"message_id": messageID,
		"member_id":  member.MemberKey,
	})
	if err != nil {
		return nil, err
	}
	task.Status = models.TeamTaskStatusDispatched
	task.RedisStreamID = &streamID
	task.DispatchedAt = &now
	task.UpdatedAt = time.Now().UTC()
	if err := s.repo.UpdateTask(task); err != nil {
		return nil, err
	}
	return teamTaskPayload(*task)
}

func (s *teamService) enrichBootstrapTaskPayload(userID int, team *models.Team, payload map[string]interface{}) error {
	if s == nil || team == nil || payload == nil {
		return nil
	}
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		return err
	}
	activeMembers := activeTeamMembers(members)
	rosterJSON, err := buildTeamRosterConfigFromMembersWithSharedDir(team, activeMembers, team.SharedMountPath)
	if err != nil {
		return err
	}
	roster := map[string]interface{}{}
	_ = json.Unmarshal([]byte(rosterJSON), &roster)
	physicalSharedDir := s.teamRuntimeSharedPathFor(userID, team.ID)
	workspaceContract := 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/<relative-path>",
		"invalidConfigPaths":      []string{"/team/members"},
		"invalidArtifactPrefixes": []string{"team/"},
	}
	memberSnapshots := make([]map[string]interface{}, 0, len(activeMembers))
	for _, member := range activeMembers {
		memberSnapshots = append(memberSnapshots, map[string]interface{}{
			"memberId":      member.MemberKey,
			"displayName":   member.DisplayName,
			"role":          member.Role,
			"runtimeType":   member.RuntimeType,
			"instanceMode":  member.InstanceMode,
			"description":   derefTeamString(member.Description),
			"status":        member.Status,
			"availability":  member.Availability,
			"progress":      member.Progress,
			"runtimeStatus": derefTeamString(member.RuntimeStatus),
			"runtimeTaskId": derefTeamString(member.RuntimeTaskID),
			"lastSummary":   derefTeamString(member.LastSummary),
			"isLeader":      isTeamLeaderRole(member.Role),
		})
	}
	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
	}
	reportMembers := append([]models.TeamMember(nil), activeMembers...)
	for idx := range reportMembers {
		if reportMembers[idx].ID != leader.ID {
			continue
		}
		reportMembers[idx].Status = models.TeamMemberStatusIdle
		reportMembers[idx].Availability = models.TeamMemberAvailabilityIdle
		reportMembers[idx].Progress = 100
		reportMembers[idx].CurrentTaskID = nil
	}
	resultMarkdown := buildBackendBootstrapReport(team, reportMembers, taskRef, taskPayload)
	if err := writeManagedTeamContextFile(reportPath, []byte(resultMarkdown), 0o664); err != nil {
		return nil, fmt.Errorf("failed to write bootstrap report: %w", err)
	}
	rosterPath := filepath.Join(filepath.Clean(s.teamRuntimeSharedPathFor(userID, team.ID)), teamConfigFileName)
	rosterBytes, rosterReadErr := os.ReadFile(rosterPath)
	if rosterReadErr != nil || strings.TrimSpace(string(rosterBytes)) == "" {
		rosterJSON, rosterBuildErr := buildTeamRosterConfigFromMembers(team, activeMembers)
		if rosterBuildErr != nil {
			return nil, fmt.Errorf("failed to rebuild bootstrap Team roster: %w", rosterBuildErr)
		}
		rosterBytes = []byte(rosterJSON)
	}
	if err := s.syncLeaderTeamContextFiles(userID, team, activeMembers, string(rosterBytes), resultMarkdown); err != nil {
		return nil, err
	}
	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/<rootTaskId>/members/<memberId>/`。\n")
	b.WriteString("- 根任务最终报告建议写入 `/team/results/<rootTaskId>/`。\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 共享目录内安全读写产物；新版 Runtime 还会提供 `team_artifact_preview`，返回与 Team 身份绑定、可在 Browser 中打开的签名只读地址。\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)
	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("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))
		}
		if err := ensureTeamWorkspaceDirectory(target); err != nil {
			return err
		}
	}
	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/<relative-path>",
		"invalidConfigPaths":         []string{"/team/members"},
		"invalidArtifactPrefixes":    []string{"team/"},
		"taskRef":                    taskRef,
		"artifactRoot":               "/team/artifacts/" + taskRef,
		"memberArtifactRoot":         "/team/artifacts/" + taskRef + "/members/${memberId}/${assignmentId}",
		"memberArtifactPhysicalRoot": filepath.ToSlash(filepath.Join(physicalSharedDir, "artifacts", taskRef, "members", "${memberId}", "${assignmentId}")),
		"leaderPlanRoot":             "/team/results/" + taskRef + "/plan",
		"reviewResultRoot":           "/team/results/" + taskRef + "/reviews/${assignmentId}",
		"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/<rootTaskId>/members/<memberId>/<assignmentId>/ for member deliverables.",
			"Use /team/results/<rootTaskId>/members/<memberId>/ for member result summaries.",
			"The Leader publishes plans through the artifact tool under /team/results/<rootTaskId>/plan/. Reviewers publish review reports under /team/results/<rootTaskId>/reviews/<assignmentId>/.",
			"Only the Leader writes the root final synthesis under /team/results/<rootTaskId>/.",
			"Shared status JSON files are compatibility snapshots; do not treat them as the task truth source.",
		},
	}
	// Keep extension fields supplied by older/newer clients, but always replace
	// the control-plane-owned workspace identity. Reusing an earlier root task's
	// taskRef or derived directories poisons the next task's prompt and artifact
	// routing while making the conversation history look authoritative. The
	// current task and Team are the only source of truth for these fields.
	if existing, ok := payload["workspaceContract"].(map[string]interface{}); ok {
		for key, value := range existing {
			if _, controlled := contract[key]; !controlled {
				contract[key] = value
			}
		}
	}
	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
	}
	return s.instanceService.GetByID(*member.InstanceID)
}

func buildTeamMemberTaskContext(member *models.TeamMember, instance *models.Instance) map[string]string {
	if member == nil {
		return map[string]string{}
	}
	displayName := strings.TrimSpace(member.DisplayName)
	if displayName == "" {
		displayName = member.MemberKey
	}
	role := strings.TrimSpace(member.Role)
	if role == "" {
		role = "member"
	}
	description := derefTeamString(member.Description)
	personaSystemPrompt, personaDescription := teamMemberPersonaFromInstance(instance)
	if description == "" {
		description = personaDescription
	}
	systemPrompt := buildTeamMemberSystemPrompt(displayName, member.MemberKey, role, description, personaSystemPrompt)
	return map[string]string{
		"memberId":     member.MemberKey,
		"displayName":  displayName,
		"role":         role,
		"description":  description,
		"systemPrompt": systemPrompt,
	}
}

func buildPlannedTeamMemberTaskContext(member plannedTeamMember) map[string]string {
	displayName := strings.TrimSpace(member.DisplayName)
	if displayName == "" {
		displayName = member.MemberKey
	}
	role := strings.TrimSpace(member.Role)
	if role == "" {
		role = "member"
	}
	description := strings.TrimSpace(derefTeamString(member.Request.Description))
	personaSystemPrompt, personaDescription := teamMemberPersonaFromEnv(member.Request.EnvironmentOverrides)
	if description == "" {
		description = personaDescription
	}
	systemPrompt := buildTeamMemberSystemPrompt(displayName, member.MemberKey, role, description, personaSystemPrompt)
	return map[string]string{
		"memberId":     member.MemberKey,
		"displayName":  displayName,
		"role":         role,
		"description":  description,
		"systemPrompt": systemPrompt,
	}
}

func buildTeamMemberSystemPrompt(displayName, memberID, role, description, personaSystemPrompt string) string {
	systemPrompt := strings.TrimSpace(fmt.Sprintf(
		"You are Team member %q (member_id=%s, role=%s). Follow this role for this task. Role responsibilities: %s",
		displayName,
		memberID,
		role,
		description,
	))
	if strings.TrimSpace(description) == "" {
		systemPrompt = fmt.Sprintf(
			"You are Team member %q (member_id=%s, role=%s). Follow this role for this task.",
			displayName,
			memberID,
			role,
		)
	}
	if personaSystemPrompt != "" {
		systemPrompt = personaSystemPrompt + "\n\n" + systemPrompt
	}
	return systemPrompt
}

func buildTeamMemberSoulMarkdown(member plannedTeamMember, communicationMode string) string {
	context := buildPlannedTeamMemberTaskContext(member)
	lines := []string{
		fmt.Sprintf("# %s", strings.TrimSpace(context["displayName"])),
		"",
		"You are running as a ClawManager Team member. Treat this file as persistent identity and role guidance.",
		"",
		"## Team Identity",
		fmt.Sprintf("- Member ID: %s", context["memberId"]),
		fmt.Sprintf("- Display name: %s", context["displayName"]),
		fmt.Sprintf("- Role: %s", context["role"]),
		fmt.Sprintf("- Effective role: %s", effectiveTeamMemberRole(member)),
	}
	if profileKey := strings.TrimSpace(member.ProfileKey); profileKey != "" {
		lines = append(lines, fmt.Sprintf("- Profile key: %s", profileKey))
	}
	if profileName := strings.TrimSpace(member.ProfileName); profileName != "" {
		lines = append(lines, fmt.Sprintf("- Profile name: %s", profileName))
	}
	if description := strings.TrimSpace(context["description"]); description != "" {
		lines = append(lines, fmt.Sprintf("- Responsibilities: %s", description))
	}
	lines = append(lines,
		"",
		"## Role Instructions",
		strings.TrimSpace(context["systemPrompt"]),
	)
	if verificationGuidance := teamMemberVerificationGuidance(member); len(verificationGuidance) > 0 {
		lines = append(lines, "", "## Verification Policy")
		lines = append(lines, verificationGuidance...)
	} else if executionGuidance := teamMemberExecutionSelfCheckGuidance(member); len(executionGuidance) > 0 {
		lines = append(lines, "", "## Assignment Validation Ownership")
		lines = append(lines, executionGuidance...)
	}
	lines = append(lines,
		"",
		"## Collaboration Rules",
		teamCollaborationGuidance(communicationMode),
		"- Only handle tasks addressed to your Team member inbox.",
		"- If team.json contains effectiveRole/profileName, use those fields when describing your role instead of falling back to a generic roster role.",
		"- Use the exact CLAWMANAGER_TEAM_SHARED_DIR value for shared context, durable notes, and handoff artifacts. When using shell commands, always create files under \"$CLAWMANAGER_TEAM_SHARED_DIR/<relative-path>\".",
		"- 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.",
		"- Report shared artifact links as /team/<relative-path>. /team is the canonical ClawManager UI path even when a Lite runtime uses a different physical directory.",
		"- Report progress, blockers, verification evidence, and final results through the Team channel.",
		"- If asked about your role, answer from this Team Identity and Role Instructions section.",
		"",
	)
	return strings.Join(lines, "\n")
}

func classifyTeamVerificationRole(member plannedTeamMember) teamVerificationRole {
	switch strings.ToLower(strings.TrimSpace(member.ProfileKey)) {
	case "agency.evidence-collector":
		return teamVerificationRoleEvidence
	case "agency.code-reviewer":
		return teamVerificationRoleCodeReview
	case "agency.api-tester":
		return teamVerificationRoleAPITest
	}

	for _, candidate := range []string{member.EffectiveRole, member.Role} {
		switch strings.ToLower(strings.TrimSpace(candidate)) {
		case "reviewer", "qa", "qa-engineer", "evidence-collector":
			return teamVerificationRoleEvidence
		case "code-reviewer":
			return teamVerificationRoleCodeReview
		case "api-tester":
			return teamVerificationRoleAPITest
		}
	}
	return teamVerificationRoleNone
}

func teamMemberVerificationGuidance(member plannedTeamMember) []string {
	switch classifyTeamVerificationRole(member) {
	case teamVerificationRoleEvidence:
		return []string{
			"- Use proportionate, static-first validation with the source, artifacts, and tools already available in the runtime.",
			"- Browser is available. For Team files, use the signed HTTP URL from team_artifact_preview when that tool is exposed; on an older Runtime without it, continue with static file review. Use Browser only when interaction or visual evidence materially affects the verdict; for non-code or non-interactive work, proceed directly with static review.",
			"- After any Browser/environment error or when the brief Browser budget is exhausted, immediately continue with static review.",
			"- Do not create a separate Browser automation stack, start a temporary server, bypass navigation policy, or repeatedly retry Browser setup. Dependencies genuinely required by the assigned validation target remain allowed. Environment limitations are not product defects.",
			"- Say Browser verification passed only when it actually ran; otherwise report static-review scope and only concrete findings.",
		}
	case teamVerificationRoleCodeReview:
		return []string{
			"- Review the source, diff, architecture boundaries, and existing test evidence first; keep validation proportional to the assigned change.",
			"- Browser is available. For Team files, use the signed HTTP URL from team_artifact_preview when that tool is exposed; on an older Runtime without it, continue with static file review. Use Browser only when interaction or rendering materially affects the verdict.",
			"- On any Browser/environment error or when the brief Browser budget is exhausted, immediately continue with source review.",
			"- Do not create a separate Browser automation stack, start a temporary server, bypass navigation policy, or repeatedly retry Browser setup. Dependencies genuinely required by the assigned code validation remain allowed.",
			"- Report only concrete findings and residual risks; do not invent or target a fixed issue count.",
		}
	case teamVerificationRoleAPITest:
		return []string{
			"- Validate with existing HTTP tools, available service endpoints, artifacts, and static API-contract review.",
			"- Browser verification is not required for API testing. Prefer existing HTTP tools and do not download a separate GUI or Browser harness merely to duplicate them; dependencies explicitly required by the assigned API validation remain allowed.",
			"- If a runnable service or network target is unavailable, record that verification limit and continue with static contract checks; do not classify environment unavailability as a product defect.",
			"- Report reproducible endpoint failures only when directly observed, and keep the verdict proportional to the available evidence.",
		}
	default:
		return nil
	}
}

func teamMemberExecutionSelfCheckGuidance(member plannedTeamMember) []string {
	role := strings.ToLower(strings.TrimSpace(effectiveTeamMemberRole(member)))
	memberKey := strings.ToLower(strings.TrimSpace(member.MemberKey))
	if member.IsLeader || role == "leader" || strings.Contains(role, "leader") || memberKey == "leader" || classifyTeamVerificationRole(member) != teamVerificationRoleNone {
		return nil
	}
	return []string{
		"- Follow the validation ownership declared by the Leader for each assignment; role names alone do not decide who tests.",
		"- For a production-only implementation or content-production assignment, create the requested artifact and hand it off without running tests, Browser acceptance, or a second validation pass.",
		"- When the assignment itself is explicitly marked as test, review, evidence, or validation work, perform that validation normally. Multiple members may own different validation assignments in parallel.",
		"- Product dependencies required to produce the artifact remain allowed. Validation guidance is not a completion gate and must never prevent a usable result or blocker from being handed off.",
	}
}

func buildTeamMemberAgentsMarkdown(team *models.Team, member plannedTeamMember) string {
	communicationMode := teamCommunicationModeLeaderMediated
	teamID := ""
	if team != nil {
		communicationMode = normalizedTeamCommunicationMode(team.CommunicationMode)
		teamID = strconv.Itoa(team.ID)
	}
	lines := []string{
		"# ClawManager Team Runtime",
		"",
		"This file defines the stable Team runtime contract. Treat SOUL.md as the member-specific identity and role file.",
		"",
		"## Runtime Capabilities",
		"- You are running inside a ClawManager managed OpenClaw/Hermes runtime.",
		"- Use the available runtime tools normally, but coordinate Team work through the ClawManager Team channel.",
		"- Use team_send for assignments, handoffs, clarifying questions, blockers, and final delivery messages.",
		"- Use team_status / progress updates to report work state when available.",
		"- Browser is available to every supported Team worker. Open Team files with team_artifact_preview when that tool is exposed; on an older Runtime without it, use static file inspection. Do not use file:// or start a temporary file server.",
		"- When your assigned work is ready, call team_complete_task once with the result. A normal assistant reply remains non-terminal; if the receipt is missed, ClawManager Monitor sends a separate reminder without extending the current turn.",
		"- In an active Team turn, task and assignment identity is inherited by the Runtime. Reuse IDs supplied by ClawManager when present, but omit optional IDs rather than inventing replacements.",
		"",
		"## Workspace Contract",
		"- CLAWMANAGER_TEAM_SHARED_DIR is the only writable shared artifact directory.",
		"- Create shared artifacts under \"$CLAWMANAGER_TEAM_SHARED_DIR/<relative-path>\".",
		"- Report shared artifact links as /team/<relative-path>.",
		"- Do not report bare filenames or relative team/... paths as final artifact links.",
		"",
		"## Team Identity Source Order",
		"- Prefer SOUL.md for your member identity, role, profile, and collaboration rules.",
		"- Then use team.json for the authoritative roster and communication mode.",
		"- Environment variables are compatibility fallbacks, not a reason to ignore SOUL.md.",
		"",
		"## Collaboration Contract",
		teamCollaborationGuidance(communicationMode),
		"- Never invent another member's reply or treat your own assumptions as a peer response.",
		"- Keep role answers consistent with SOUL.md and the effectiveRole/profileName fields in team.json.",
		"",
		"## Current Member",
		fmt.Sprintf("- Team ID: %s", teamID),
		fmt.Sprintf("- Member ID: %s", member.MemberKey),
		fmt.Sprintf("- Display name: %s", member.DisplayName),
		fmt.Sprintf("- Runtime: %s", member.RuntimeType),
		fmt.Sprintf("- Effective role: %s", effectiveTeamMemberRole(member)),
	}
	if member.IsLeader {
		lines = append(lines,
			"",
			"## Leader Team Context Preflight",
			"- Before handling the first user root task and whenever team.json changes, read ./team.json and ./team-introduction.md from this prompt workspace after SOUL.md.",
			"- The same managed files are available at $CLAWMANAGER_TEAM_SHARED_DIR/team.json and $CLAWMANAGER_TEAM_SHARED_DIR/team-introduction.md.",
			"- Treat team.json as the authoritative current roster. Treat team-introduction.md as the operating overview; if the two differ, team.json wins for current membership.",
			"- Do not claim that no roster is available before checking these exact files.",
		)
	}
	if member.ProfileKey != "" {
		lines = append(lines, fmt.Sprintf("- Profile key: %s", member.ProfileKey))
	}
	if member.ProfileName != "" {
		lines = append(lines, fmt.Sprintf("- Profile name: %s", member.ProfileName))
	}
	return strings.Join(lines, "\n") + "\n"
}

func plannedTeamMemberDescription(member plannedTeamMember) string {
	if description := strings.TrimSpace(derefTeamString(member.Request.Description)); description != "" {
		return description
	}
	_, description := teamMemberPersonaFromEnv(member.Request.EnvironmentOverrides)
	return strings.TrimSpace(description)
}

func effectiveTeamMemberRole(member plannedTeamMember) string {
	if role := strings.TrimSpace(member.EffectiveRole); role != "" {
		return role
	}
	if role := strings.TrimSpace(member.Role); role != "" {
		return role
	}
	return "member"
}

func (s *teamService) writeLiteTeamMemberIdentityFiles(instance *models.Instance, team *models.Team, member plannedTeamMember, rosterJSON string) error {
	if instance == nil || modeForExistingInstance(instance) != InstanceModeLite {
		return nil
	}
	workspacePath := ""
	if instance.WorkspacePath != nil {
		workspacePath = strings.TrimSpace(*instance.WorkspacePath)
	}
	if workspacePath == "" {
		return fmt.Errorf("failed to write Lite Team identity files: workspace path is empty")
	}
	if err := os.MkdirAll(workspacePath, 0755); err != nil {
		return fmt.Errorf("failed to prepare Lite Team identity workspace: %w", err)
	}
	identityFiles := map[string]string{
		teamAgentsFileName: buildTeamMemberAgentsMarkdown(team, member),
		teamSoulFileName:   buildTeamMemberSoulMarkdown(member, normalizedTeamCommunicationMode(team.CommunicationMode)),
	}
	if strings.EqualFold(instance.Type, "openclaw") {
		// OpenClaw injects AGENTS.md and SOUL.md from home/.openclaw/workspace,
		// not from the instance workspace root. Merge Team guidance into the
		// real prompt workspace without replacing user/default instructions.
		openClawWorkspace := filepath.Join(workspacePath, "home", ".openclaw", "workspace")
		if err := os.MkdirAll(openClawWorkspace, 0755); err != nil {
			return fmt.Errorf("failed to prepare Lite Team OpenClaw workspace: %w", err)
		}
		chownTeamWorkspacePath(openClawWorkspace)
		for name, content := range identityFiles {
			if err := writeManagedTeamWorkspaceOverlay(filepath.Join(openClawWorkspace, name), content); err != nil {
				return fmt.Errorf("failed to write Lite Team OpenClaw identity file %s: %w", name, err)
			}
			chownTeamWorkspacePath(filepath.Join(openClawWorkspace, name))
		}
		if strings.TrimSpace(rosterJSON) != "" {
			target := filepath.Join(openClawWorkspace, teamConfigFileName)
			if err := writeManagedTeamContextFile(target, []byte(rosterJSON), 0o644); err != nil {
				return fmt.Errorf("failed to write Lite Team OpenClaw roster file: %w", err)
			}
		}
		if err := repairLitePromptWorkspaceOwnership(instance); err != nil {
			return fmt.Errorf("failed to repair Lite Team OpenClaw prompt workspace ownership: %w", err)
		}
	}
	files := map[string]string{}
	if !strings.EqualFold(instance.Type, "openclaw") {
		for name, content := range identityFiles {
			files[name] = content
		}
	}
	if strings.TrimSpace(rosterJSON) != "" {
		files[teamConfigFileName] = rosterJSON
	}
	for name, content := range files {
		target := filepath.Join(workspacePath, name)
		if err := writeManagedTeamContextFile(target, []byte(content), 0o644); err != nil {
			return fmt.Errorf("failed to write Lite Team identity file %s: %w", name, err)
		}
	}
	if strings.EqualFold(member.RuntimeType, "hermes") {
		hermesPromptRoots, err := prepareHermesLitePromptRoots(instance)
		if err != nil {
			return err
		}
		for _, promptRoot := range hermesPromptRoots {
			for name, content := range identityFiles {
				target := filepath.Join(promptRoot, name)
				if err := validateHermesLiteManagedFileTarget(target); err != nil {
					return fmt.Errorf("invalid Hermes Lite identity file target %s: %w", target, err)
				}
				if err := writeManagedTeamWorkspaceOverlay(target, content); err != nil {
					return fmt.Errorf("failed to write Hermes Lite identity file %s: %w", target, err)
				}
				if err := repairHermesLiteManagedPathOwnership(instance, target); err != nil {
					return fmt.Errorf("failed to set Hermes Lite identity file ownership %s: %w", target, err)
				}
			}
			if strings.TrimSpace(rosterJSON) != "" {
				target := filepath.Join(promptRoot, teamConfigFileName)
				if err := validateHermesLiteManagedFileTarget(target); err != nil {
					return fmt.Errorf("invalid Hermes Lite roster file target %s: %w", target, err)
				}
				if err := writeManagedTeamContextFile(target, []byte(rosterJSON), 0o644); err != nil {
					return fmt.Errorf("failed to write Hermes Lite roster file %s: %w", target, err)
				}
				if err := repairHermesLiteManagedPathOwnership(instance, target); err != nil {
					return fmt.Errorf("failed to set Hermes Lite roster file ownership %s: %w", target, err)
				}
			}
		}
	}
	return nil
}

func prepareHermesLitePromptRoots(instance *models.Instance) ([]string, error) {
	if instance == nil || instance.ID <= 0 || instance.WorkspacePath == nil {
		return nil, fmt.Errorf("Hermes Lite Team member requires a persisted instance workspace")
	}
	workspaceRoot := filepath.Clean(strings.TrimSpace(*instance.WorkspacePath))
	if workspaceRoot == "" || workspaceRoot == "." || workspaceRoot == string(filepath.Separator) {
		return nil, fmt.Errorf("Hermes Lite Team member has an invalid instance workspace")
	}
	homeRoot := filepath.Join(workspaceRoot, "home")
	workerRoot := filepath.Join(homeRoot, ".clawmanager-team-worker")
	promptRoots := []string{
		filepath.Join(homeRoot, ".hermes"),
		filepath.Join(workerRoot, ".hermes"),
	}
	for _, directory := range append([]string{workerRoot}, promptRoots...) {
		if err := prepareHermesLiteManagedDirectory(instance, workspaceRoot, directory); err != nil {
			return nil, fmt.Errorf("failed to prepare Hermes Lite managed directory %s: %w", directory, err)
		}
	}
	return promptRoots, nil
}

func prepareHermesLiteManagedDirectory(instance *models.Instance, workspaceRoot, directory string) error {
	relative, err := filepath.Rel(workspaceRoot, directory)
	if err != nil || relative == "." || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) {
		return fmt.Errorf("managed directory escaped the instance workspace")
	}
	if err := os.MkdirAll(directory, 0o750); err != nil {
		return err
	}
	info, err := os.Lstat(directory)
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
		return fmt.Errorf("managed path must be a real directory")
	}
	realWorkspace, err := filepath.EvalSymlinks(workspaceRoot)
	if err != nil {
		return fmt.Errorf("resolve instance workspace: %w", err)
	}
	realDirectory, err := filepath.EvalSymlinks(directory)
	if err != nil {
		return fmt.Errorf("resolve managed directory: %w", err)
	}
	realRelative, err := filepath.Rel(realWorkspace, realDirectory)
	if err != nil || realRelative == "." || realRelative == ".." ||
		strings.HasPrefix(realRelative, ".."+string(filepath.Separator)) || filepath.IsAbs(realRelative) {
		return fmt.Errorf("managed directory real path escaped the instance workspace")
	}
	if err := chownLitePromptWorkspacePath(directory, RuntimeLinuxID(instance.ID), teamSharedGID); err != nil {
		return fmt.Errorf("set runtime owner: %w", err)
	}
	if err := os.Chmod(directory, 0o750); err != nil {
		return fmt.Errorf("set managed directory mode: %w", err)
	}
	return nil
}

func validateHermesLiteManagedFileTarget(path string) error {
	info, err := os.Lstat(path)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 || info.IsDir() {
		return fmt.Errorf("managed identity target must be a regular file")
	}
	return nil
}

func repairHermesLiteManagedPathOwnership(instance *models.Instance, path string) error {
	if instance == nil || instance.ID <= 0 {
		return fmt.Errorf("persisted instance identity is required")
	}
	info, err := os.Lstat(path)
	if err != nil {
		return err
	}
	if info.Mode()&os.ModeSymlink != 0 || info.IsDir() {
		return fmt.Errorf("managed identity path must be a regular file")
	}
	if err := chownLitePromptWorkspacePath(path, RuntimeLinuxID(instance.ID), teamSharedGID); err != nil {
		return err
	}
	return nil
}

var chownLitePromptWorkspacePath = func(path string, uid, gid int) error {
	if runtime.GOOS == "windows" {
		return nil
	}
	return os.Chown(path, uid, gid)
}

var liteManagedPromptFileNames = map[string]struct{}{
	teamAgentsFileName:       {},
	teamSoulFileName:         {},
	teamConfigFileName:       {},
	teamIntroductionFileName: {},
	"TOOLS.md":               {},
}

func repairLitePromptPathAccess(path string, info os.FileInfo, uid, gid int) error {
	if err := chownLitePromptWorkspacePath(path, uid, gid); err == nil {
		return nil
	} else {
		required := os.FileMode(0o004)
		if info.IsDir() {
			required = 0o005
		}
		if info.Mode().Perm()&required == required {
			return nil
		}
		if chmodErr := os.Chmod(path, info.Mode().Perm()|required); chmodErr != nil {
			return fmt.Errorf("chown failed: %v; readable fallback failed: %w", err, chmodErr)
		}
		return nil
	}
}

// repairLitePromptWorkspaceOwnership repairs only the OpenClaw prompt root and
// its immediate entries. It intentionally does not recurse into user-managed
// skills or other workspace trees.
func repairLitePromptWorkspaceOwnership(instance *models.Instance) error {
	if instance == nil || instance.ID <= 0 || modeForExistingInstance(instance) != InstanceModeLite ||
		!strings.EqualFold(instance.Type, "openclaw") || instance.WorkspacePath == nil {
		return nil
	}
	workspaceRoot := strings.TrimSpace(*instance.WorkspacePath)
	if workspaceRoot == "" {
		return nil
	}
	promptRoot := filepath.Join(workspaceRoot, "home", ".openclaw", "workspace")
	info, err := os.Lstat(promptRoot)
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	if err != nil {
		return fmt.Errorf("failed to inspect Lite OpenClaw prompt workspace: %w", err)
	}
	if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
		return fmt.Errorf("Lite OpenClaw prompt workspace is not a real directory")
	}
	uid := RuntimeLinuxID(instance.ID)
	if err := repairLitePromptPathAccess(promptRoot, info, uid, teamSharedGID); err != nil {
		return fmt.Errorf("failed to repair Lite OpenClaw prompt workspace owner: %w", err)
	}
	entries, err := os.ReadDir(promptRoot)
	if err != nil {
		return fmt.Errorf("failed to list Lite OpenClaw prompt workspace: %w", err)
	}
	for _, entry := range entries {
		if _, managed := liteManagedPromptFileNames[entry.Name()]; !managed {
			continue
		}
		target := filepath.Join(promptRoot, entry.Name())
		entryInfo, err := os.Lstat(target)
		if err != nil {
			return fmt.Errorf("failed to inspect Lite OpenClaw prompt entry %s: %w", entry.Name(), err)
		}
		if entryInfo.Mode()&os.ModeSymlink != 0 {
			continue
		}
		if entryInfo.IsDir() {
			continue
		}
		if err := repairLitePromptPathAccess(target, entryInfo, uid, teamSharedGID); err != nil {
			return fmt.Errorf("failed to repair Lite OpenClaw prompt entry %s owner: %w", entry.Name(), err)
		}
	}
	return nil
}

func teamMemberPromptWorkspace(instance *models.Instance) string {
	if instance == nil || instance.WorkspacePath == nil {
		return ""
	}
	root := strings.TrimSpace(*instance.WorkspacePath)
	if root == "" {
		return ""
	}
	if strings.EqualFold(instance.Type, "openclaw") {
		return filepath.Join(root, "home", ".openclaw", "workspace")
	}
	return root
}

func (s *teamService) syncLeaderTeamContextFiles(userID int, team *models.Team, members []models.TeamMember, rosterJSON, introduction string) error {
	if s == nil || team == nil {
		return nil
	}
	sharedRoot := filepath.Clean(s.teamRuntimeSharedPathFor(userID, team.ID))
	if sharedRoot == "." || sharedRoot == string(filepath.Separator) {
		return fmt.Errorf("invalid Team shared workspace root for Team %d: %q", team.ID, sharedRoot)
	}
	contextFiles := []struct {
		name    string
		content string
	}{
		{name: teamConfigFileName, content: rosterJSON},
		{name: teamIntroductionFileName, content: introduction},
	}
	for _, file := range contextFiles {
		if strings.TrimSpace(file.content) == "" {
			continue
		}
		if err := writeManagedTeamContextFile(filepath.Join(sharedRoot, file.name), []byte(file.content), 0o664); err != nil {
			return fmt.Errorf("failed to write shared Leader context file %s: %w", file.name, err)
		}
	}
	leader := findTeamLeader(members)
	if leader == nil {
		return fmt.Errorf("Team %d has no active Leader", team.ID)
	}
	instance, err := s.teamMemberInstance(leader)
	if err != nil {
		return fmt.Errorf("failed to resolve Team %d Leader instance: %w", team.ID, err)
	}
	promptWorkspace := teamMemberPromptWorkspace(instance)
	if promptWorkspace == "" {
		// During compatibility imports and isolated unit tests an instance may
		// not be attached yet. The shared copies remain authoritative and the
		// instance creation path writes team.json when it becomes available.
		return nil
	}
	for _, file := range contextFiles {
		if strings.TrimSpace(file.content) == "" {
			continue
		}
		if err := writeManagedTeamContextFile(filepath.Join(promptWorkspace, file.name), []byte(file.content), 0o644); err != nil {
			return fmt.Errorf("failed to write Leader prompt workspace context file %s: %w", file.name, err)
		}
	}
	return nil
}

func writeManagedTeamWorkspaceOverlay(path, content string) error {
	existing, err := os.ReadFile(path)
	if err != nil && !os.IsNotExist(err) {
		return err
	}
	base := string(existing)
	if start := strings.Index(base, teamManagedOverlayStart); start >= 0 {
		if endOffset := strings.Index(base[start:], teamManagedOverlayEnd); endOffset >= 0 {
			end := start + endOffset + len(teamManagedOverlayEnd)
			base = strings.TrimRight(base[:start]+base[end:], " \t\r\n")
		}
	}
	overlay := strings.Join([]string{
		teamManagedOverlayStart,
		strings.TrimSpace(content),
		teamManagedOverlayEnd,
	}, "\n")
	if strings.TrimSpace(base) != "" {
		base = strings.TrimRight(base, " \t\r\n") + "\n\n"
	}
	return os.WriteFile(path, []byte(base+overlay+"\n"), 0644)
}

func appendTeamCollaborationGuidance(systemPrompt, communicationMode string) string {
	guidance := teamCollaborationGuidance(communicationMode)
	if strings.Contains(systemPrompt, guidance) {
		return systemPrompt
	}
	return strings.TrimSpace(systemPrompt) + "\n\n" + guidance
}

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/<relative-path>\". 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/<relative-path>. 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
	}
	return strings.TrimSpace(systemPrompt) + "\n\n" + guidance
}

func teamCollaborationGuidance(communicationMode string) string {
	switch normalizedTeamCommunicationMode(communicationMode) {
	case teamCommunicationModePeerAssisted:
		return "Collaboration mode: peer_assisted / worker-direct. This mode is isolated from leader_mediated flow: the Leader still owns final user-facing synthesis, but members must hand off directly to the named downstream owner when the root task, collaboration plan, or current instruction specifies one. Direct handoff is mandatory, not optional; sending only to the Leader is allowed only when there is no named downstream owner, when blocked, or when final synthesis is explicitly required. Preserve rootTaskId/rootMessageId, artifact paths, requested next action, acceptance criteria, and reply-required status in every peer message. Ask peer questions through the Team channel, then wait for the addressed member's real reply before continuing dependent work; never simulate, invent, or reinterpret another member's answer as if the user said it. Write durable artifacts under CLAWMANAGER_TEAM_SHARED_DIR, report peer outcomes through the Team channel, and let the Leader close the root task only after final synthesis. When receiving a peer request, respond with explicit evidence, artifact paths, blockers, or review findings so the requester can finish its own task."
	case teamCommunicationModeFullMesh:
		return "Collaboration mode: full_mesh. Team members coordinate directly with each other while preserving rootTaskId/rootMessageId context, shared artifacts under CLAWMANAGER_TEAM_SHARED_DIR, and final user-facing synthesis. If a downstream owner is named, hand off to that exact member before completing your own step. Use direct member-to-member messages for parallel research, design, implementation, review, and verification. Wait for real addressed-member replies before continuing dependent work; do not simulate peer answers or label peer messages as user replies. Keep each peer exchange bounded, evidence based, and visible in the Team channel."
	default:
		return "Collaboration mode: leader_mediated. This is a strict hub-and-spoke workflow isolated from worker-direct flow. User root tasks enter through the Leader. If the user names a non-Leader member or role, the Leader must delegate to that exact member with team_send, wait for that member's real result, then synthesize the final answer. If the user gives a broad task without naming one member, the Leader must create a compact plan, decompose work by owner/member_id, send assignments, wait for required member results, verify them, and produce final synthesis. Every delegated assignment must carry a stable workId and assignmentId; reuse that workId for all progress, result, and review messages belonging to the assignment. The Leader may answer directly only for self-contained control-plane or simple tasks that do not require a named worker or multi-member evidence. A dispatch, plan, or handoff is not a final result and must not close the root task. Workers execute only assignments addressed to them, preserve rootTaskId/rootMessageId/workId and artifact paths, and report results or blockers back to the Leader. Workers must not hand off directly to other workers. A worker completion never closes the user root task; only the Leader may finalize it after all required outputs are reconciled."
	}
}

func teamMemberPersonaFromInstance(instance *models.Instance) (string, string) {
	if instance == nil {
		return "", ""
	}
	overrides, err := parseEnvironmentOverridesJSON(instance.EnvironmentOverridesJSON)
	if err != nil || len(overrides) == 0 {
		return "", ""
	}
	return teamMemberPersonaFromEnv(overrides)
}

func teamMemberPersonaFromEnv(overrides map[string]string) (string, string) {
	if len(overrides) == 0 {
		return "", ""
	}
	systemPrompt := strings.TrimSpace(firstNonEmptyEnv(overrides,
		"CLAWMANAGER_AGENT_SYSTEM_PROMPT",
		"CLAWMANAGER_HERMES_SYSTEM_PROMPT",
		"CLAWMANAGER_RUNTIME_SYSTEM_PROMPT",
		"HERMES_SYSTEM_PROMPT",
	))
	description := ""
	for _, key := range []string{
		"CLAWMANAGER_AGENT_PERSONA_JSON",
		"CLAWMANAGER_HERMES_PERSONA_JSON",
		"CLAWMANAGER_RUNTIME_PERSONA_JSON",
	} {
		persona := parseTeamPersonaEnv(overrides[key])
		if persona == nil {
			continue
		}
		if systemPrompt == "" {
			systemPrompt = strings.TrimSpace(persona.SystemPrompt)
		}
		if description == "" {
			description = strings.TrimSpace(persona.Summary)
		}
	}
	for _, key := range []string{
		"CLAWMANAGER_HERMES_AGENTS_JSON",
		"CLAWMANAGER_RUNTIME_AGENTS_JSON",
		"CLAWMANAGER_OPENCLAW_AGENTS_JSON",
	} {
		agents := parseTeamAgentsEnv(overrides[key])
		if agents == nil {
			continue
		}
		if systemPrompt == "" {
			systemPrompt = strings.TrimSpace(agents.SystemPrompt)
		}
		if description == "" {
			description = strings.TrimSpace(agents.Summary)
		}
	}
	return systemPrompt, description
}

type teamPersonaEnv struct {
	ProfileKey   string `json:"profileKey"`
	Name         string `json:"name"`
	DisplayName  string `json:"displayName"`
	RoleHint     string `json:"roleHint"`
	SystemPrompt string `json:"systemPrompt"`
	Summary      string `json:"summary"`
}

type teamProfileEnv struct {
	ProfileKey  string
	ProfileName string
	RoleHint    string
	Summary     string
}

func teamMemberProfileFromEnv(overrides map[string]string) teamProfileEnv {
	if len(overrides) == 0 {
		return teamProfileEnv{}
	}
	for _, key := range []string{
		"CLAWMANAGER_AGENT_PERSONA_JSON",
		"CLAWMANAGER_HERMES_PERSONA_JSON",
		"CLAWMANAGER_RUNTIME_PERSONA_JSON",
	} {
		persona := parseTeamPersonaEnv(overrides[key])
		if persona == nil {
			continue
		}
		profile := teamProfileEnv{
			ProfileKey:  strings.TrimSpace(persona.ProfileKey),
			ProfileName: strings.TrimSpace(firstNonEmptyString(persona.DisplayName, persona.Name)),
			RoleHint:    strings.TrimSpace(persona.RoleHint),
			Summary:     strings.TrimSpace(persona.Summary),
		}
		if profile.ProfileKey != "" || profile.ProfileName != "" || profile.RoleHint != "" || profile.Summary != "" {
			return profile
		}
	}
	for _, key := range []string{
		"CLAWMANAGER_HERMES_AGENTS_JSON",
		"CLAWMANAGER_RUNTIME_AGENTS_JSON",
		"CLAWMANAGER_OPENCLAW_AGENTS_JSON",
	} {
		agents := parseTeamAgentsEnv(overrides[key])
		if agents == nil {
			continue
		}
		profile := teamProfileEnv{
			ProfileKey:  strings.TrimSpace(agents.ProfileKey),
			ProfileName: strings.TrimSpace(agents.Name),
			RoleHint:    strings.TrimSpace(agents.RoleHint),
			Summary:     strings.TrimSpace(agents.Summary),
		}
		if profile.ProfileKey != "" || profile.ProfileName != "" || profile.RoleHint != "" || profile.Summary != "" {
			return profile
		}
	}
	return teamProfileEnv{}
}

func parseTeamPersonaEnv(raw string) *teamPersonaEnv {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	var persona teamPersonaEnv
	if err := json.Unmarshal([]byte(raw), &persona); err != nil {
		return nil
	}
	return &persona
}

type teamAgentsEnv struct {
	Items []struct {
		Content struct {
			Config struct {
				ProfileKey   string `json:"profileKey"`
				Name         string `json:"name"`
				DisplayName  string `json:"displayName"`
				RoleHint     string `json:"roleHint"`
				SystemPrompt string `json:"systemPrompt"`
				Summary      string `json:"summary"`
			} `json:"config"`
		} `json:"content"`
	} `json:"items"`
}

func parseTeamAgentsEnv(raw string) *teamPersonaEnv {
	if strings.TrimSpace(raw) == "" {
		return nil
	}
	var payload teamAgentsEnv
	if err := json.Unmarshal([]byte(raw), &payload); err != nil {
		return nil
	}
	for _, item := range payload.Items {
		systemPrompt := strings.TrimSpace(item.Content.Config.SystemPrompt)
		summary := strings.TrimSpace(item.Content.Config.Summary)
		profileKey := strings.TrimSpace(item.Content.Config.ProfileKey)
		name := strings.TrimSpace(firstNonEmptyString(item.Content.Config.DisplayName, item.Content.Config.Name))
		roleHint := strings.TrimSpace(item.Content.Config.RoleHint)
		if systemPrompt != "" || summary != "" || profileKey != "" || name != "" || roleHint != "" {
			return &teamPersonaEnv{
				ProfileKey:   profileKey,
				Name:         name,
				RoleHint:     roleHint,
				SystemPrompt: systemPrompt,
				Summary:      summary,
			}
		}
	}
	return nil
}

func firstNonEmptyString(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func firstNonEmptyEnv(values map[string]string, keys ...string) string {
	for _, key := range keys {
		if value := strings.TrimSpace(values[key]); value != "" {
			return value
		}
	}
	return ""
}

func buildTeamRuntimePrompt(rawPrompt string, memberContext map[string]string) string {
	prompt := strings.TrimSpace(rawPrompt)
	if len(memberContext) == 0 {
		return prompt
	}
	systemPrompt := strings.TrimSpace(memberContext["systemPrompt"])
	if systemPrompt == "" {
		return prompt
	}
	if prompt == "" {
		return systemPrompt
	}
	return fmt.Sprintf("%s\n\nUser task:\n%s", systemPrompt, prompt)
}

func (s *teamService) DeleteTeam(userID, teamID int) error {
	team, err := s.requireOwnedTeam(userID, teamID)
	if err != nil {
		return err
	}
	if team.Status == models.TeamStatusDeleted {
		return nil
	}

	now := time.Now().UTC()
	team.Status = models.TeamStatusDeleting
	team.UpdatedAt = now
	if err := s.repo.UpdateTeam(team); err != nil {
		return err
	}

	members, err := s.repo.ListMembersByTeamID(teamID)
	if err != nil {
		return err
	}
	for idx := range members {
		member := members[idx]
		if member.Status == models.TeamMemberStatusDeleted {
			continue
		}
		member.Status = models.TeamMemberStatusDeleting
		member.UpdatedAt = time.Now().UTC()
		_ = s.repo.UpdateMember(&member)
		if member.InstanceID != nil && *member.InstanceID > 0 {
			if err := s.instanceService.Delete(*member.InstanceID); err != nil {
				fmt.Printf("Warning: failed to delete Team %d member %s instance %d: %v\n", teamID, member.MemberKey, *member.InstanceID, err)
			}
		}
		member.Status = models.TeamMemberStatusDeleted
		member.CurrentTaskID = nil
		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 {
			fmt.Printf("Warning: failed to delete Team %d secret: %v\n", teamID, err)
		}
	}
	if err := s.configMapService.DeleteConfigMap(ctx, userID, s.teamConfigMapName(teamID)); err != nil {
		fmt.Printf("Warning: failed to delete Team %d configmap: %v\n", teamID, err)
	}
	if err := s.pvcService.DeleteTeamSharedPVC(ctx, userID, teamID); err != nil {
		fmt.Printf("Warning: failed to delete Team %d shared PVC: %v\n", teamID, err)
	}

	team.Name = deletedTeamName(team.Name, team.ID)
	team.Status = models.TeamStatusDeleted
	team.UpdatedAt = time.Now().UTC()
	return s.repo.UpdateTeam(team)
}

func (s *teamService) DeleteMember(userID, teamID int, memberID string) error {
	team, err := s.requireOwnedTeam(userID, teamID)
	if err != nil {
		return err
	}
	member, err := s.findTeamMemberForDelete(teamID, memberID)
	if err != nil {
		return err
	}
	if member == nil {
		return fmt.Errorf("team member not found")
	}
	if member.UserID != userID || member.TeamID != teamID {
		return fmt.Errorf("access denied")
	}
	if member.Status == models.TeamMemberStatusDeleted {
		return nil
	}
	if isTeamLeaderRole(member.Role) {
		return fmt.Errorf("team leader cannot be deleted before assigning a new leader")
	}

	now := time.Now().UTC()
	member.Status = models.TeamMemberStatusDeleting
	member.UpdatedAt = now
	if err := s.repo.UpdateMember(member); err != nil {
		return err
	}
	if member.InstanceID != nil && *member.InstanceID > 0 {
		if err := s.instanceService.Delete(*member.InstanceID); err != nil {
			return err
		}
	}
	member.Status = models.TeamMemberStatusDeleted
	member.CurrentTaskID = nil
	member.Progress = 0
	member.UpdatedAt = time.Now().UTC()
	if err := s.repo.UpdateMember(member); err != nil {
		return err
	}
	return s.refreshTeamRosterConfig(userID, team)
}

func (s *teamService) findTeamMemberForDelete(teamID int, memberID string) (*models.TeamMember, error) {
	value := strings.TrimSpace(memberID)
	if value == "" {
		return nil, fmt.Errorf("team member id is required")
	}
	if numericID, err := strconv.Atoi(value); err == nil && numericID > 0 {
		member, err := s.repo.GetMemberByID(numericID)
		if err != nil || member == nil || member.TeamID != teamID {
			return member, err
		}
		return member, nil
	}
	return s.repo.GetMemberByTeamKey(teamID, value)
}

func (s *teamService) refreshTeamRosterConfig(userID int, team *models.Team) error {
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		return err
	}
	activeMembers := activeTeamMembers(members)
	rosterJSON, err := buildTeamRosterConfigFromMembers(team, activeMembers)
	if err != nil {
		return err
	}
	configData := map[string]string{
		teamConfigFileName: rosterJSON,
	}
	for _, member := range activeMembers {
		if member.RuntimeType != "hermes" {
			continue
		}
		configData[teamMemberSoulConfigKey(member.MemberKey)] = buildTeamMemberSoulMarkdown(plannedTeamMember{
			Request: CreateTeamMemberRequest{
				Description: member.Description,
			},
			MemberKey:    member.MemberKey,
			DisplayName:  member.DisplayName,
			Role:         member.Role,
			RuntimeType:  member.RuntimeType,
			InstanceMode: member.InstanceMode,
			IsLeader:     isTeamLeaderRole(member.Role),
		}, normalizedTeamCommunicationMode(team.CommunicationMode))
	}
	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
	}
	introduction := buildBackendBootstrapReport(
		team,
		activeMembers,
		fmt.Sprintf("team-%d-context", team.ID),
		map[string]interface{}{
			"workspaceContract": map[string]interface{}{
				"sharedDir":         team.SharedMountPath,
				"physicalSharedDir": s.teamRuntimeSharedPathFor(userID, team.ID),
			},
		},
	)
	return s.syncLeaderTeamContextFiles(userID, team, activeMembers, rosterJSON, introduction)
}

func (s *teamService) requireOwnedTeam(userID, teamID int) (*models.Team, error) {
	team, err := s.repo.GetTeamByID(teamID)
	if err != nil {
		return nil, err
	}
	if team == nil {
		return nil, fmt.Errorf("team not found")
	}
	if team.UserID != userID {
		return nil, fmt.Errorf("access denied")
	}
	return team, nil
}

func (s *teamService) ensureConsumer(ctx context.Context, teamID int) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if !s.running {
		return
	}
	if _, exists := s.consumers[teamID]; exists {
		return
	}
	s.consumers[teamID] = struct{}{}
	s.wg.Add(1)
	go s.consumeTeamEvents(ctx, teamID)
}

func (s *teamService) consumeTeamEvents(ctx context.Context, teamID int) {
	defer s.wg.Done()
	defer func() {
		s.mu.Lock()
		delete(s.consumers, teamID)
		s.mu.Unlock()
	}()

	for {
		select {
		case <-ctx.Done():
			return
		default:
		}

		team, err := s.repo.GetTeamByID(teamID)
		if err != nil || team == nil {
			time.Sleep(5 * time.Second)
			continue
		}
		bus, err := s.redisBusForTeam(ctx, team)
		if err != nil {
			time.Sleep(5 * time.Second)
			continue
		}
		lastID := strings.TrimSpace(team.RedisEventsLastID)
		if lastID == "" {
			lastID = "0-0"
		}
		messages, err := bus.XRead(ctx, teamEventsKey(teamID), lastID, 5*time.Second)
		if err != nil {
			time.Sleep(2 * time.Second)
			continue
		}
		for _, message := range messages {
			if err := s.projectTeamEvent(team, bus, message); err != nil {
				fmt.Printf("Warning: failed to project Team %d event %s: %v\n", teamID, message.ID, err)
			}
			team.RedisEventsLastID = message.ID
			team.UpdatedAt = time.Now().UTC()
			_ = s.repo.UpdateTeam(team)
		}
	}
}

func (s *teamService) ensureStaleTaskMonitor(ctx context.Context) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if !s.running {
		return
	}
	if s.staleMonitorStarted {
		return
	}
	s.staleMonitorStarted = true
	s.wg.Add(1)
	go s.monitorStaleTasks(ctx)
}

func (s *teamService) monitorStaleTasks(ctx context.Context) {
	defer s.wg.Done()
	ticker := time.NewTicker(teamTaskStaleSweepInterval)
	defer ticker.Stop()

	for {
		select {
		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.sweepTeamControlPlaneConsistency(); err != nil {
				fmt.Printf("Warning: failed to reconcile Team control-plane projections: %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<<attempts) * 5 * time.Second
}

func (s *teamService) deliverTeamEventOutbox(team *models.Team, bus *redisBus, outbox *models.TeamEventOutbox) error {
	if team == nil || bus == nil || outbox == nil || strings.TrimSpace(outbox.Destination) == "" {
		return fmt.Errorf("team, redis bus and outbox destination are required")
	}
	payload := map[string]interface{}{}
	if err := json.Unmarshal([]byte(outbox.PayloadJSON), &payload); err != nil {
		return fmt.Errorf("decode Team outbox payload %d: %w", outbox.ID, err)
	}
	messageID := strings.TrimSpace(outbox.MessageID)
	if messageID == "" {
		messageID = eventString(payload, "messageId", "message_id")
	}
	if strings.EqualFold(eventString(payload, "event", "type"), "completion_ack") {
		completionID := eventString(payload, "completionId", "completion_id")
		attemptID := eventString(payload, "attemptId", "attempt_id")
		if completionID == "" || attemptID == "" {
			return fmt.Errorf("completion acknowledgement outbox %d is missing completionId or attemptId", outbox.ID)
		}
		if err := bus.Set(context.Background(), teamCompletionAckKey(team.ID, completionID, attemptID), outbox.PayloadJSON, 24*time.Hour); err != nil {
			return err
		}
		return bus.Set(context.Background(), teamCompletionStateKey(team.ID, completionID), outbox.PayloadJSON, 7*24*time.Hour)
	}
	fields := map[string]string{
		"payload":    outbox.PayloadJSON,
		"team_id":    strconv.Itoa(team.ID),
		"message_id": messageID,
		"member_id":  eventString(payload, "to", "target", "memberId", "member_id"),
	}
	if taskID := eventString(payload, "taskId", "task_id", "rootTaskId", "root_task_id"); taskID != "" {
		fields["task_id"] = taskID
	}
	_, err := bus.XAdd(context.Background(), outbox.Destination, fields)
	return err
}

func (s *teamService) sweepStaleTasks() error {
	timeout := teamTaskStaleTimeout()
	if timeout <= 0 {
		return nil
	}
	cutoff := time.Now().UTC().Add(-timeout)
	tasks, err := s.repo.ListStaleCandidateTasks(cutoff, 100)
	if err != nil {
		return err
	}
	for idx := range tasks {
		if err := s.observeTaskStall(&tasks[idx], timeout); err != nil {
			fmt.Printf("Warning: failed to observe Team task %d stall: %v\n", tasks[idx].ID, err)
		}
	}
	return nil
}

// sweepTeamControlPlaneConsistency repairs projections from durable business
// facts. It is deliberately separate from Monitor: it may reconcile member
// availability and restore an outbox receipt for an already accepted result,
// but it never invents a result, changes an attempt outcome, or closes a root.
func (s *teamService) sweepTeamControlPlaneConsistency() error {
	if s == nil || s.repo == nil {
		return nil
	}
	now := time.Now().UTC()
	teams, err := s.repo.ListActiveTeams()
	if err != nil {
		return err
	}
	var errs []error
	for idx := range teams {
		team := teams[idx]
		if normalizedTeamCommunicationMode(team.CommunicationMode) != teamCommunicationModeLeaderMediated {
			continue
		}
		items, listErr := s.repo.ListWorkItemsByTeamID(team.ID, 500)
		if listErr != nil {
			errs = append(errs, listErr)
			continue
		}
		members, memberErr := s.repo.ListMembersByTeamID(team.ID)
		if memberErr != nil {
			errs = append(errs, memberErr)
			continue
		}
		for memberIdx := range members {
			member := members[memberIdx]
			if isLeaderTeamMember(&member) || !isActiveTeamMember(&member) {
				continue
			}
			if reconcileTeamMemberOperationalState(&member, items) {
				member.UpdatedAt = now
				if updateErr := s.repo.UpdateMember(&member); updateErr != nil {
					errs = append(errs, updateErr)
				}
			}
		}

		var bus *redisBus
		for itemIdx := range items {
			item := items[itemIdx]
			if item.Status != models.TeamTaskStatusSucceeded || item.OwnerMemberID == nil || !workItemHasAcceptedResultReceipt(item) {
				continue
			}
			task, taskErr := s.repo.GetTaskByID(item.RootTaskID)
			if taskErr != nil {
				errs = append(errs, taskErr)
				continue
			}
			owner, ownerErr := s.repo.GetMemberByID(*item.OwnerMemberID)
			if ownerErr != nil {
				errs = append(errs, ownerErr)
				continue
			}
			if task == nil || owner == nil || owner.TeamID != team.ID || isLeaderTeamMember(owner) {
				continue
			}
			assignmentID := derefTeamString(item.AssignmentID)
			if assignmentID == "" {
				assignmentID = item.WorkID
			}
			resultPayload := workItemResultPayload(item)
			contentHash := eventString(resultPayload, "contentHash", "content_hash")
			if contentHash == "" {
				contentHash = teamResultContentHash(resultPayload)
			}
			confirmed, confirmErr := s.hasLeaderMediatedResultConfirmationForAttempt(team.ID, task.ID, owner.MemberKey, assignmentID, teamMaxInt(item.Revision, 1), contentHash)
			if confirmErr != nil {
				errs = append(errs, confirmErr)
				continue
			}
			if confirmed {
				continue
			}
			if bus == nil {
				bus, err = s.redisBusForTeam(context.Background(), &team)
				if err != nil {
					errs = append(errs, err)
					continue
				}
			}
			resultPayload["recoveredByControlPlaneConsistency"] = true
			resultPayload["sourceWorkId"] = item.WorkID
			sourceEvent := &models.TeamEvent{
				TeamID: team.ID, TaskID: &task.ID, MemberID: &owner.ID,
				EventType:  "control_plane_result_receipt_recovery",
				OccurredAt: item.FinishedAt, CreatedAt: item.UpdatedAt,
			}
			if createErr := s.createLeaderMediatedResultNotification(&team, bus, task, owner, resultPayload, sourceEvent); createErr != nil {
				errs = append(errs, createErr)
			}
		}
	}
	return errors.Join(errs...)
}

func workItemHasAcceptedResultReceipt(item models.TeamWorkItem) bool {
	if item.Status != models.TeamTaskStatusSucceeded || item.FinishedAt == nil || item.ResultJSON == nil || strings.TrimSpace(*item.ResultJSON) == "" {
		return false
	}
	payload := workItemResultPayload(item)
	if eventString(payload, "completionId", "completion_id", "sourceCompletionId", "source_completion_id", "sourceMessageId", "source_message_id") != "" {
		return true
	}
	return eventBool(payload,
		"explicitCompletion", "explicit_completion",
		"automaticTurnResult", "automatic_turn_result",
		"assignmentResultOnly", "assignment_result_only",
		"memberTerminalOnly", "member_terminal_only",
	)
}

func workItemHasAcceptedFailureReceipt(item models.TeamWorkItem) bool {
	if item.Status != models.TeamTaskStatusFailed || item.ResultJSON == nil || strings.TrimSpace(*item.ResultJSON) == "" {
		return false
	}
	payload := workItemResultPayload(item)
	if isStateNeutralTeamObservation(payload) || !eventBool(payload, "resultFailed", "result_failed") {
		return false
	}
	eventType := strings.ToLower(strings.TrimSpace(eventString(payload, "originalEvent", "original_event", "event", "type")))
	return isAuthoritativeAssignmentFailure(eventType, payload)
}

func workItemIsMisprojectedWaitingObservation(item models.TeamWorkItem) bool {
	if item.Status != models.TeamTaskStatusFailed || item.ResultJSON == nil || strings.TrimSpace(*item.ResultJSON) == "" {
		return false
	}
	payload := workItemResultPayload(item)
	if workItemHasAcceptedFailureReceipt(item) {
		return false
	}
	status := normalizedTeamTaskEventStatus(payload)
	return isWaitingTeamTaskEventStatus(status) ||
		isStateNeutralTeamObservation(payload) ||
		eventBool(payload, "dependencyBlocked", "dependency_blocked", "provisionalAssignmentResult", "provisional_assignment_result")
}

func (s *teamService) sweepAssignmentStatusChecks() error {
	if s == nil || s.repo == nil {
		return nil
	}
	now := time.Now().UTC()
	cutoff := now.Add(-teamAssignmentMonitorEvery)
	teams, err := s.repo.ListActiveTeams()
	if err != nil {
		return err
	}
	var errs []error
	for idx := range teams {
		team := teams[idx]
		if normalizedTeamCommunicationMode(team.CommunicationMode) != teamCommunicationModeLeaderMediated {
			continue
		}
		items, err := s.repo.ListWorkItemsByTeamID(team.ID, 500)
		if err != nil {
			errs = append(errs, err)
			continue
		}
		var bus *redisBus
		for itemIdx := range items {
			item := items[itemIdx]
			if item.OwnerMemberID == nil || strings.TrimSpace(item.WorkID) == "" {
				continue
			}
			task, err := s.repo.GetTaskByID(item.RootTaskID)
			if err != nil {
				errs = append(errs, err)
				continue
			}
			if task == nil || task.TeamID != team.ID || isTerminalTeamTaskStatus(task.Status) {
				continue
			}
			owner, err := s.repo.GetMemberByID(*item.OwnerMemberID)
			if err != nil {
				errs = append(errs, err)
				continue
			}
			if owner == nil || owner.TeamID != team.ID || isLeaderTeamMember(owner) || !isActiveTeamMember(owner) {
				continue
			}
			if workItemIsMisprojectedWaitingObservation(item) {
				item.Status = models.TeamTaskStatusWaiting
				item.FinishedAt = nil
				item.UpdatedAt = now
				if repairErr := s.repo.UpsertWorkItem(&item); repairErr != nil {
					errs = append(errs, repairErr)
					continue
				}
				if _, ledgerErr := s.reconcileTeamWorkflowLedger(task, false, now); ledgerErr != nil {
					errs = append(errs, ledgerErr)
				}
			}
			if bus == nil {
				bus, err = s.redisBusForTeam(context.Background(), &team)
				if err != nil {
					errs = append(errs, err)
					continue
				}
			}
			activity, activitySupported, activityErr := s.readAssignmentActivitySnapshot(bus, team.ID, task, &item)
			if activityErr != nil {
				// Monitoring is advisory. A transient snapshot read must not
				// mutate the assignment or enqueue a second control message.
				errs = append(errs, activityErr)
				continue
			}
			if isTerminalTeamTaskStatus(item.Status) {
				if item.Status != models.TeamTaskStatusSucceeded && !item.UpdatedAt.After(cutoff) {
					monitorKey := fmt.Sprintf("terminal:%d:%d:%s:%d", team.ID, item.RootTaskID, item.WorkID, *item.OwnerMemberID)
					if s.claimAssignmentMonitorSlot(monitorKey, now) {
						if concernErr := s.createTerminalAssignmentSupervisorReview(&team, bus, task, &item, owner, activity, now); concernErr != nil {
							errs = append(errs, concernErr)
						}
					}
				}
				continue
			}
			if !isActiveTeamWorkItemStatus(item.Status) {
				continue
			}
			if activitySupported && activity != nil && activity.activeTurn() && activity.fresh(now) {
				if projectWorkItemStartedFromActivity(&item, activity, now) {
					if err := s.repo.UpsertWorkItem(&item); err != nil {
						errs = append(errs, err)
					}
				}
				if activity.ExecutionAlive && activity.StallCandidate {
					if reviewErr := s.createAssignmentSupervisorReview(&team, bus, task, &item, owner, activity, now); reviewErr != nil {
						errs = append(errs, reviewErr)
					}
					// The model/tool turn is still alive. Ask the Leader to judge the
					// evidence, but never queue a competing member turn or interrupt it.
					continue
				}
				// A healthy active turn is already being observed out of band. A
				// suspected stall still falls through so the independent Monitor can
				// remind the member/Leader; it never mutates the assignment itself.
				if !activity.suspectedStalled() {
					continue
				}
			}
			if !shouldMonitorTeamWorkItem(item, cutoff) {
				continue
			}
			attempts, _, attemptErr := s.assignmentMonitorAttemptState(&team, task, &item)
			if attemptErr != nil {
				errs = append(errs, attemptErr)
				continue
			}
			monitorKey := fmt.Sprintf("%d:%d:%s:%d", team.ID, item.RootTaskID, item.WorkID, *item.OwnerMemberID)
			outstanding, monitorErr := s.hasRecentUnansweredAssignmentMonitor(&team, task, &item, now)
			if monitorErr != nil {
				errs = append(errs, monitorErr)
				continue
			}
			if outstanding {
				continue
			}
			if !s.claimAssignmentMonitorSlot(monitorKey, now) {
				continue
			}
			if err := s.dispatchAssignmentStatusCheck(&team, bus, task, &item, owner, activity, attempts, now); err != nil {
				errs = append(errs, err)
			}
		}
		if err := s.sweepLeaderSynthesisReminders(&team, bus, items, cutoff, now); err != nil {
			errs = append(errs, err)
		}
	}
	return errors.Join(errs...)
}

func (s *teamService) createTerminalAssignmentSupervisorReview(team *models.Team, bus *redisBus, task *models.TeamTask, item *models.TeamWorkItem, owner *models.TeamMember, activity *teamAssignmentActivitySnapshot, now time.Time) error {
	if s == nil || team == nil || bus == nil || task == nil || item == nil || owner == nil || isTerminalTeamTaskStatus(task.Status) {
		return nil
	}
	assignmentID := workItemBusinessID(*item)
	if assignmentID == "" {
		assignmentID = item.WorkID
	}
	runtimeStillActive := activity != nil && activity.activeTurn() && activity.fresh(now)
	summary := fmt.Sprintf(
		"Root task is still open while assignment %s revision %d is recorded as %s. Runtime still reports an active exact turn: %t. Review the authenticated completion/failure receipt, latest member conversation, artifacts, and dependency facts. If this is a transport/projection conflict, remind the same member to continue the existing attempt; if it is a genuine business failure, coordinate rework or reassignment. Do not infer success and do not close the root from elapsed time alone.",
		assignmentID,
		teamMaxInt(item.Revision, 1),
		item.Status,
		runtimeStillActive,
	)
	payload := map[string]interface{}{
		"summary":                   summary,
		"eventKind":                 "terminal_assignment_supervisor_review",
		"supervisorReviewRequested": true,
		"nonAuthoritative":          true,
		"stateEffect":               "none",
		"rootTaskTerminal":          false,
		"assignmentId":              assignmentID,
		"workId":                    item.WorkID,
		"revision":                  teamMaxInt(item.Revision, 1),
		"workItemId":                item.ID,
		"attemptStatus":             item.Status,
		"runtimeAttemptStillActive": runtimeStillActive,
		"dependencies":              teamWorkItemDependencies(*item),
		"artifactRefs":              workItemArtifactRefs(*item),
		"acceptedResultReceipt":     workItemHasAcceptedResultReceipt(*item),
		"workflowState":             task.WorkflowState,
		"planVersion":               task.PlanVersion,
		"ledgerVersion":             task.LedgerVersion,
		"activity":                  activity,
	}
	// Repeat at a low cadence while the root remains open. Each generation is
	// independently auditable; there is no permanent retry ceiling.
	generation := now.Unix() / int64((15 * time.Minute).Seconds())
	streamRef := fmt.Sprintf("terminal-supervisor:%d:%s:%d", item.ID, item.Status, generation)
	sourceEvent := &models.TeamEvent{
		TeamID: team.ID, TaskID: &task.ID, MemberID: &owner.ID,
		EventType: "terminal_assignment_supervisor_review", RedisStreamID: &streamRef,
		OccurredAt: &now, CreatedAt: now,
	}
	return s.createLeaderMediatedRecoveryRequest(team, bus, task, owner, payload, sourceEvent)
}

func (s *teamService) sweepLeaderSynthesisReminders(team *models.Team, bus *redisBus, items []models.TeamWorkItem, cutoff, now time.Time) error {
	if s == nil || s.repo == nil || team == nil || !isLeaderMediatedTeam(team) {
		return nil
	}
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		return err
	}
	membersByID := make(map[int]*models.TeamMember, len(members))
	for idx := range members {
		membersByID[members[idx].ID] = &members[idx]
	}
	taskIDs := map[int]struct{}{}
	for idx := range items {
		if items[idx].RootTaskID > 0 {
			taskIDs[items[idx].RootTaskID] = struct{}{}
		}
	}
	teamTasks, taskListErr := s.repo.ListTasksByTeamID(team.ID, 500)
	if taskListErr != nil {
		return taskListErr
	}
	for idx := range teamTasks {
		if !isTerminalTeamTaskStatus(teamTasks[idx].Status) {
			taskIDs[teamTasks[idx].ID] = 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.
		identityRepaired, identityErr := s.reconcileUnissuedRunningWorkItems(team, task, now)
		if identityErr != nil {
			errs = append(errs, identityErr)
			continue
		}
		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 && !identityRepaired && !task.UpdatedAt.IsZero() && task.UpdatedAt.After(cutoff) {
			continue
		}
		// Re-read after identity/ledger repair.  The caller's Team-wide snapshot
		// can still contain the stale card that just became superseded.
		currentItems, currentItemsErr := s.repo.ListWorkItemsByRootTaskID(task.ID)
		if currentItemsErr != nil {
			errs = append(errs, currentItemsErr)
			continue
		}
		ready, resultItems := leaderMediatedRootNeedsSynthesisReminder(task, currentItems, membersByID)
		if !ready {
			continue
		}
		monitorKey := fmt.Sprintf(
			"%d:%d:%s:%s",
			team.ID,
			task.ID,
			task.WorkflowState,
			leaderSynthesisResultFingerprint(task, resultItems),
		)
		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
	}
	validationRepaired, err := s.reconcileCompletedAssignmentValidations(task, time.Now().UTC())
	if err != nil {
		return false, err
	}
	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
		}
		if eventBool(payload, "automaticTurnResult", "automatic_turn_result") ||
			eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
			// A natural report written before later work completed cannot
			// summarize that later work. Request a fresh Leader synthesis instead
			// of silently accepting the old draft when the ledger advances.
			continue
		}
		deferredLedgerVersion := int64(eventInt(payload, "ledgerVersion", "ledger_version"))
		// Deferred events intentionally keep their final report out of the
		// visible result fields.  Restore that private draft only for this new,
		// internally generated proposal; otherwise the strict envelope check
		// would reject a perfectly valid report forever after the ledger heals.
		if eventString(payload, "resultMarkdown", "result_markdown") == "" {
			if draft := eventString(payload, "completionDraftMarkdown", "completion_draft_markdown"); draft != "" {
				payload["resultMarkdown"] = draft
			}
		}
		// A deferred proposal retained its original summary separately.  Always
		// restore that authenticated draft when it is available: legacy Runtime
		// diagnostics are not a reliable signal for deciding whether the report
		// is final, and must not replace the Leader's actual summary on retry.
		if summary := eventString(payload, "completionDraftSummary", "completion_draft_summary"); summary != "" {
			payload["summary"] = summary
		}
		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. New plans also require a structured disposition;
		// legacy plans keep their historical sealing behavior.
		workflowFinalForReconcile := true
		if eventBool(payload, "automaticTurnResult", "automatic_turn_result") ||
			eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
			workflowFinalForReconcile = false
		}
		ledgerRepaired, err := s.reconcileTeamWorkflowLedgerWithDispositions(
			task,
			workflowFinalForReconcile,
			structuredTeamPhaseDispositions(payload),
			time.Now().UTC(),
		)
		if err != nil {
			return false, err
		}
		evaluationRulesChanged := eventInt(
			payload,
			"completionEvaluationVersion",
			"completion_evaluation_version",
		) < teamCompletionEvaluationVersion
		if !validationRepaired && !ledgerRepaired && !evaluationRulesChanged &&
			deferredLedgerVersion > 0 && task.LedgerVersion <= deferredLedgerVersion {
			// Re-evaluation is driven by a real business-ledger change, never by
			// a timer or an unchanged evaluator. A newer evaluator may retry one
			// older deferred draft exactly once, allowing a control-plane upgrade
			// to heal prior overly strict decisions without another Agent turn.
			return false, nil
		}
		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)
		payload["completionReconcile"] = true
		payload["completionEvaluationVersion"] = teamCompletionEvaluationVersion
		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
}

// reconcileUnissuedRunningWorkItems repairs an old projection bug without
// guessing at legitimate sequential work.  It only retires a non-terminal
// card when (1) this root has an explicit Leader-issued assignment for the
// same member and (2) the running card has no corresponding dispatch event at
// all. Phase names and the current status of the canonical card are derived
// projections and are intentionally not trusted here: Team 72 demonstrated
// that both can already be stale. Two real reviewer assignments remain
// independent because each has its own dispatch event.
func (s *teamService) reconcileUnissuedRunningWorkItems(team *models.Team, task *models.TeamTask, now time.Time) (bool, error) {
	if s == nil || s.repo == nil || team == nil || task == nil || isTerminalTeamTaskStatus(task.Status) {
		return false, nil
	}
	events, err := s.repo.ListEventsByTeamID(team.ID, 500)
	if err != nil {
		return false, err
	}
	items, err := s.repo.ListWorkItemsByRootTaskID(task.ID)
	if err != nil {
		return false, err
	}
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		return false, err
	}
	membersByID := make(map[int]models.TeamMember, len(members))
	for _, member := range members {
		membersByID[member.ID] = member
	}
	issued := map[int]map[string]struct{}{}
	for _, event := range events {
		payload := teamEventPayloadMap(event)
		if !teamEventMatchesRootTask(event, payload, task) {
			continue
		}
		isDispatch := isAuthoritativeTeamAssignmentEvent(event.EventType, payload)
		if !isDispatch {
			continue
		}
		assignmentID := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
		target := leaderMediatedRouteTarget(payload)
		if assignmentID == "" || target == "" {
			continue
		}
		for _, member := range members {
			if isLeaderTeamMember(&member) || !teamMemberRouteEquivalent(member.MemberKey, target) {
				continue
			}
			if issued[member.ID] == nil {
				issued[member.ID] = map[string]struct{}{}
			}
			issued[member.ID][assignmentID] = struct{}{}
		}
	}
	changed := false
	for idx := range items {
		orphan := items[idx]
		// Never infer identity for legacy member-lane cards that lack an
		// assignment.  Only an explicitly identified but unissued projection can
		// be safely retired; real sequential work remains untouched.
		if orphan.OwnerMemberID == nil || orphan.AssignmentID == nil || orphan.SupersededBy != nil || isTerminalTeamTaskStatus(orphan.Status) {
			continue
		}
		owner, ok := membersByID[*orphan.OwnerMemberID]
		if !ok || isLeaderTeamMember(&owner) || len(issued[owner.ID]) == 0 {
			continue
		}
		orphanAssignmentID := derefTeamString(orphan.AssignmentID)
		if orphanAssignmentID == "" {
			orphanAssignmentID = orphan.WorkID
		}
		if _, wasIssued := issued[owner.ID][orphanAssignmentID]; wasIssued {
			continue
		}
		var canonical *models.TeamWorkItem
		for candidateIndex := range items {
			candidate := items[candidateIndex]
			if candidate.OwnerMemberID == nil || *candidate.OwnerMemberID != owner.ID || candidate.SupersededBy != nil {
				continue
			}
			candidateAssignmentID := derefTeamString(candidate.AssignmentID)
			if candidateAssignmentID == "" {
				candidateAssignmentID = candidate.WorkID
			}
			if _, wasIssued := issued[owner.ID][candidateAssignmentID]; !wasIssued {
				continue
			}
			clone := candidate
			canonical = &clone
			break
		}
		if canonical == nil {
			continue
		}
		supersededBy := "identity-recovery:" + canonical.WorkID
		orphan.SupersededBy = &supersededBy
		orphan.RequiredForRoot = false
		orphan.UpdatedAt = now
		if err := s.repo.UpsertWorkItem(&orphan); err != nil {
			return changed, err
		}
		changed = true
	}
	if changed {
		task.LedgerVersion++
		task.UpdatedAt = now
		if err := s.repo.UpdateTask(task); err != nil {
			return false, err
		}
	}
	return changed, 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 leaderSynthesisResultFingerprint(task *models.TeamTask, resultItems []models.TeamWorkItem) string {
	parts := make([]string, 0, len(resultItems)+1)
	if task != nil {
		parts = append(parts, fmt.Sprintf(
			"task=%d|plan=%d|workflow=%s|phase=%s",
			task.ID,
			task.PlanVersion,
			strings.TrimSpace(task.WorkflowState),
			strings.TrimSpace(derefTeamString(task.CurrentPhaseID)),
		))
	}
	for idx := range resultItems {
		item := resultItems[idx]
		validatedRevision := 0
		if item.ValidatedRevision != nil {
			validatedRevision = *item.ValidatedRevision
		}
		parts = append(parts, fmt.Sprintf(
			"work=%s|revision=%d|status=%s|required=%t|review=%t|validated=%d|result=%s|artifacts=%s",
			workItemBusinessID(item),
			teamMaxInt(item.Revision, 1),
			strings.TrimSpace(item.Status),
			item.RequiredForRoot || item.AssignmentID == nil,
			item.ReviewRequired,
			validatedRevision,
			strings.TrimSpace(derefTeamString(item.ResultJSON)),
			strings.TrimSpace(derefTeamString(item.ArtifactRefsJSON)),
		))
	}
	sort.Strings(parts)
	digest := sha256.Sum256([]byte(strings.Join(parts, "\n")))
	return hex.EncodeToString(digest[:])
}

func leaderSynthesisReminderDelay(previousGenerations int) time.Duration {
	switch {
	case previousGenerations <= 0:
		return 0
	case previousGenerations <= 2:
		return teamAssignmentMonitorEvery
	case previousGenerations == 3:
		return 5 * time.Minute
	default:
		return 10 * time.Minute
	}
}

func (s *teamService) nextLeaderSynthesisReminderGeneration(teamID, taskID int, resultFingerprint string, now time.Time) (int, bool, error) {
	if s == nil || s.repo == nil {
		return 0, false, nil
	}
	events, err := s.repo.ListEventsByTeamID(teamID, 1000)
	if err != nil {
		return 0, false, err
	}
	generation := 0
	var latest time.Time
	for idx := range events {
		event := events[idx]
		if event.TaskID == nil || *event.TaskID != taskID ||
			(event.EventType != "leader_synthesis_reminder" && event.EventType != "leader_decision_reminder") {
			continue
		}
		payload := teamEventPayloadMap(event)
		if eventString(payload, "resultFingerprint", "result_fingerprint") != resultFingerprint {
			continue
		}
		candidateGeneration := eventInt(payload, "reminderGeneration", "reminder_generation")
		if candidateGeneration <= 0 {
			candidateGeneration = 1
		}
		if candidateGeneration > generation {
			generation = candidateGeneration
		}
		occurredAt := event.CreatedAt
		if event.OccurredAt != nil {
			occurredAt = *event.OccurredAt
		}
		if occurredAt.After(latest) {
			latest = occurredAt
		}
	}
	if !latest.IsZero() && now.Sub(latest) < leaderSynthesisReminderDelay(generation) {
		return generation, false, nil
	}
	return generation + 1, true, nil
}

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
	}
	resultFingerprint := leaderSynthesisResultFingerprint(task, resultItems)
	generation, due, err := s.nextLeaderSynthesisReminderGeneration(team.ID, task.ID, resultFingerprint, now)
	if err != nil || !due {
		return err
	}
	eventID := fmt.Sprintf(
		"leader-workflow-reminder:%d:%d:%s:%s:%d",
		team.ID,
		task.ID,
		normalizeTeamRedisKeyPart(task.WorkflowState),
		resultFingerprint,
		generation,
	)
	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,
		"resultFingerprint":  resultFingerprint,
		"reminderGeneration": generation,
		"expiresAt":          now.Add(2 * time.Minute).Format(time.RFC3339Nano),
		"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 {
		// A phase decision is deliberately not a synthesis request.  Reusing the
		// synthesis prompt here previously told the Leader that every assignment
		// was complete and to call team_complete_task, even while a newly-created
		// review/implementation phase was still running.
		prompt = buildLeaderDecisionReminderPrompt(task, resultItems, rootTaskRef)
	}
	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(),
		"turnOutcomePolicy": map[string]interface{}{
			"actionExpected":           true,
			"immediateRecoveryAllowed": true,
			"reason":                   "leader_workflow_notification",
		},
		"metadata":  payload,
		"createdAt": now.Format(time.RFC3339Nano),
		"expiresAt": now.Add(2 * time.Minute).Format(time.RFC3339Nano),
	}
	applyTeamTaskEnvelopeContext(envelope, task, leader.MemberKey)
	resultRefs := make([]string, 0)
	for idx := range resultItems {
		resultRefs = appendUniqueTeamArtifactRefs(resultRefs, workItemArtifactRefs(resultItems[idx])...)
	}
	contextRefs := s.durableTeamTaskContextRefs(team, task, resultRefs...)
	if len(contextRefs) > 0 {
		envelope["artifactRefs"] = contextRefs
		envelope["contextRefs"] = contextRefs
	}
	envelopeJSON, err := marshalJSON(envelope)
	if err != nil {
		return err
	}
	s.publishTeamRootWorkflowState(bus, task)
	_, 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 buildLeaderDecisionReminderPrompt(task *models.TeamTask, resultItems []models.TeamWorkItem, rootTaskRef string) string {
	lines := []string{
		"[LEADER_WORKFLOW_DECISION_REQUIRED] The current phase has terminal results, but the root workflow is still open.",
		fmt.Sprintf("rootTaskId=%s rootMessageId=%s planVersion=%d ledgerVersion=%d", rootTaskRef, task.MessageID, task.PlanVersion, task.LedgerVersion),
		"Review the confirmed results below and decide the next business step.",
		"If more implementation, verification, review, or refinement is required, publish the next planVersion and dispatch those assignments.",
		"If no required action remains, explicitly record that the workflow is sealed with a leader_synthesis progress update. Only after that explicit decision may you submit a final user-facing result with team_complete_task.",
		"Do not call team_complete_task merely because the current phase's workers have finished.",
		"",
		"Confirmed current-phase 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 teamArtifactMetadataHashes(value interface{}) map[string]string {
	result := map[string]string{}
	appendEntry := func(entry map[string]interface{}) {
		path := strings.TrimSpace(eventString(entry, "path", "artifactRef", "artifact_ref"))
		hash := strings.TrimSpace(eventString(entry, "contentHash", "content_hash", "sha256"))
		if path != "" && hash != "" {
			result[path] = strings.TrimPrefix(strings.ToLower(hash), "sha256:")
		}
	}
	switch typed := value.(type) {
	case []interface{}:
		for _, raw := range typed {
			if entry, ok := raw.(map[string]interface{}); ok {
				appendEntry(entry)
			}
		}
	case []map[string]interface{}:
		for _, entry := range typed {
			appendEntry(entry)
		}
	}
	return result
}

func workItemArtifactMetadataHashes(item models.TeamWorkItem) map[string]string {
	payload := workItemResultPayload(item)
	return teamArtifactMetadataHashes(firstTeamValue(payload, "artifactMetadata", "artifact_metadata"))
}

func appendUniqueTeamArtifactRefs(target []string, refs ...string) []string {
	seen := make(map[string]struct{}, len(target)+len(refs))
	for _, ref := range target {
		seen[ref] = struct{}{}
	}
	for _, raw := range refs {
		ref := trimTeamArtifactReferenceToken(raw)
		if !strings.HasPrefix(ref, teamSharedMountPath+"/") || strings.HasSuffix(ref, "/") {
			continue
		}
		safe := true
		for _, segment := range strings.Split(strings.TrimPrefix(ref, teamSharedMountPath+"/"), "/") {
			if segment == "" || segment == "." || segment == ".." {
				safe = false
				break
			}
		}
		if !safe {
			continue
		}
		if _, exists := seen[ref]; exists {
			continue
		}
		seen[ref] = struct{}{}
		target = append(target, ref)
		if len(target) >= 64 {
			break
		}
	}
	return target
}

// durableTeamTaskContextRefs rebuilds the collaboration context from durable
// Team facts. Runtime-local turn state is an optimization only: a later
// assignment must still receive the Leader plan and already-confirmed upstream
// deliveries after a Runtime restart or a context-only notification turn.
func (s *teamService) durableTeamTaskContextRefs(team *models.Team, task *models.TeamTask, extraRefs ...string) []string {
	refs := appendUniqueTeamArtifactRefs(nil, extraRefs...)
	if s == nil || s.repo == nil || team == nil || task == nil || len(refs) >= 64 {
		return refs
	}
	if events, err := s.repo.ListEventsByTeamID(team.ID, 1000); err == nil {
		legacyPlanCaptured := false
		for idx := range events {
			event := events[idx]
			payload := teamEventPayloadMap(event)
			if !teamEventMatchesRootTask(event, payload, task) {
				continue
			}
			eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "semanticEventKind", "semantic_event_kind", "eventKind", "event_kind", "kind")))
			artifactKind := strings.ToLower(strings.TrimSpace(eventString(payload, "artifactKind", "artifact_kind")))
			if eventKind != "leader_plan" && artifactKind != "plan" {
				continue
			}
			eventPlanVersion := eventInt(payload, "planVersion", "plan_version")
			if task.PlanVersion > 0 && eventPlanVersion > 0 && int64(eventPlanVersion) != task.PlanVersion {
				continue
			}
			if eventPlanVersion == 0 {
				if legacyPlanCaptured {
					continue
				}
				legacyPlanCaptured = true
			}
			eventRefs := explicitTeamArtifactReferences(payload)
			if len(eventRefs) == 0 {
				eventRefs = collectTeamArtifactReferences(payload)
			}
			refs = appendUniqueTeamArtifactRefs(refs, eventRefs...)
			if len(refs) >= 64 {
				return refs
			}
		}
	}
	if items, err := s.repo.ListWorkItemsByRootTaskID(task.ID); err == nil {
		for idx := range items {
			item := items[idx]
			if item.Status != models.TeamTaskStatusSucceeded || item.SupersededBy != nil {
				continue
			}
			refs = appendUniqueTeamArtifactRefs(refs, workItemArtifactRefs(item)...)
			if len(refs) >= 64 {
				break
			}
		}
	}
	return refs
}

func shouldMonitorTeamWorkItem(item models.TeamWorkItem, cutoff time.Time) bool {
	if item.OwnerMemberID == nil || strings.TrimSpace(item.WorkID) == "" {
		return false
	}
	if !isActiveTeamWorkItemStatus(item.Status) {
		return false
	}
	return item.UpdatedAt.IsZero() || item.UpdatedAt.Before(cutoff)
}

type teamAssignmentActivitySnapshot struct {
	SchemaVersion      int    `json:"schemaVersion"`
	RootTaskID         string `json:"rootTaskId"`
	AssignmentID       string `json:"assignmentId"`
	TurnID             string `json:"turnId"`
	TurnState          string `json:"turnState"`
	StartedAt          string `json:"startedAt"`
	ObservedAt         string `json:"observedAt"`
	LastSessionEventAt string `json:"lastSessionEventAt"`
	SessionCursor      string `json:"sessionCursor"`
	LastActivityKind   string `json:"lastActivityKind"`
	PendingToolName    string `json:"pendingToolName"`
	LastAssistantText  string `json:"lastAssistantText"`
	LastAssistantAt    string `json:"lastAssistantAt"`
	LastToolName       string `json:"lastToolName"`
	LastToolFailed     bool   `json:"lastToolFailed"`
	LastToolAt         string `json:"lastToolAt"`
	ExecutionAlive     bool   `json:"executionAlive"`
	QuietForSeconds    int    `json:"quietForSeconds"`
	StallCandidate     bool   `json:"stallCandidate"`
	ActivityClass      string `json:"activityClassification"`
	Terminal           bool   `json:"terminal"`
}

func (snapshot teamAssignmentActivitySnapshot) activeTurn() bool {
	if snapshot.Terminal {
		return false
	}
	switch strings.ToLower(strings.TrimSpace(snapshot.TurnState)) {
	case "starting", "running", "waiting_tool", "quiet_healthy", "suspected_stalled":
		return true
	default:
		return false
	}
}

func (snapshot teamAssignmentActivitySnapshot) fresh(now time.Time) bool {
	observedAt, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(snapshot.ObservedAt))
	if err != nil {
		return false
	}
	age := now.Sub(observedAt)
	return age >= -teamAssignmentActivityFreshFor && age <= teamAssignmentActivityFreshFor
}

func (snapshot teamAssignmentActivitySnapshot) suspectedStalled() bool {
	return strings.EqualFold(strings.TrimSpace(snapshot.TurnState), "suspected_stalled")
}

func (s *teamService) readAssignmentActivitySnapshot(
	bus *redisBus,
	teamID int,
	task *models.TeamTask,
	item *models.TeamWorkItem,
) (*teamAssignmentActivitySnapshot, bool, error) {
	if bus == nil || task == nil || item == nil {
		return nil, false, nil
	}
	rootTaskID := fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID)
	assignmentID := strings.TrimSpace(derefTeamString(item.AssignmentID))
	if assignmentID == "" {
		assignmentID = strings.TrimSpace(item.WorkID)
	}
	raw, ok, err := bus.Get(
		context.Background(),
		teamAssignmentActivityKey(teamID, rootTaskID, assignmentID),
	)
	if err != nil || !ok {
		return nil, ok, err
	}
	var snapshot teamAssignmentActivitySnapshot
	if err := json.Unmarshal([]byte(raw), &snapshot); err != nil {
		return nil, true, fmt.Errorf("decode Team assignment activity snapshot: %w", err)
	}
	if snapshot.SchemaVersion < 1 ||
		strings.TrimSpace(snapshot.RootTaskID) != rootTaskID ||
		(strings.TrimSpace(snapshot.AssignmentID) != "" &&
			strings.TrimSpace(snapshot.AssignmentID) != assignmentID) {
		return nil, false, nil
	}
	return &snapshot, true, nil
}

func projectWorkItemStartedFromActivity(item *models.TeamWorkItem, snapshot *teamAssignmentActivitySnapshot, now time.Time) bool {
	if item == nil || snapshot == nil || !snapshot.activeTurn() || !snapshot.fresh(now) || isTerminalTeamTaskStatus(item.Status) {
		return false
	}
	changed := false
	if item.Status == models.TeamTaskStatusDispatched {
		item.Status = models.TeamTaskStatusRunning
		changed = true
	}
	if item.StartedAt == nil {
		startedAt := now
		if parsed, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(snapshot.StartedAt)); err == nil {
			startedAt = parsed.UTC()
		}
		item.StartedAt = &startedAt
		changed = true
	}
	if changed {
		item.UpdatedAt = now
	}
	return changed
}

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) hasRecentUnansweredAssignmentMonitor(team *models.Team, task *models.TeamTask, item *models.TeamWorkItem, now time.Time) (bool, error) {
	if s == nil || s.repo == nil || team == nil || task == nil || item == nil {
		return false, nil
	}
	events, err := s.repo.ListEventsByTeamID(team.ID, 500)
	if err != nil {
		return false, err
	}
	answered := make(map[string]struct{})
	for idx := range events {
		event := events[idx]
		payload := teamEventPayloadMap(event)
		if !teamEventMatchesRootTask(event, payload, task) ||
			!strings.EqualFold(eventString(payload, "eventKind", "event_kind"), "assignment_check_result") {
			continue
		}
		if checkID := eventString(payload, "checkId", "check_id"); checkID != "" {
			answered[checkID] = struct{}{}
		}
	}
	for idx := range events {
		event := events[idx]
		payload := teamEventPayloadMap(event)
		identity := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
		matchesItem := identity != "" && (identity == item.WorkID ||
			(derefTeamString(item.AssignmentID) != "" && identity == derefTeamString(item.AssignmentID)) ||
			(derefTeamString(item.CanonicalWorkID) != "" && identity == derefTeamString(item.CanonicalWorkID)))
		if !teamEventMatchesRootTask(event, payload, task) ||
			!strings.EqualFold(eventString(payload, "eventKind", "event_kind"), "assignment_check_requested") || !matchesItem {
			continue
		}
		checkID := eventString(payload, "checkId", "check_id", "messageId", "message_id")
		if checkID == "" {
			continue
		}
		if _, ok := answered[checkID]; ok {
			continue
		}
		requestedAt := event.CreatedAt
		if event.OccurredAt != nil {
			requestedAt = *event.OccurredAt
		}
		// Do not stack Monitor messages behind one serial Agent turn. If an old
		// Runtime loses the check entirely, the bounded lease expires and a new
		// attempt can still recover the assignment.
		if requestedAt.IsZero() || now.Sub(requestedAt) < teamAssignmentMonitorEvery {
			return true, nil
		}
	}
	return false, nil
}

func (s *teamService) assignmentMonitorAttemptState(team *models.Team, task *models.TeamTask, item *models.TeamWorkItem) (int, *models.TeamEvent, error) {
	if s == nil || s.repo == nil || team == nil || task == nil || item == nil {
		return 0, nil, nil
	}
	events, err := s.repo.ListEventsByTeamID(team.ID, 500)
	if err != nil {
		return 0, nil, err
	}
	seen := map[string]struct{}{}
	var latest *models.TeamEvent
	for idx := range events {
		event := events[idx]
		payload := teamEventPayloadMap(event)
		identity := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
		if !teamEventMatchesRootTask(event, payload, task) ||
			!strings.EqualFold(eventString(payload, "eventKind", "event_kind"), "assignment_check_requested") ||
			(identity != item.WorkID && identity != derefTeamString(item.AssignmentID) && identity != derefTeamString(item.CanonicalWorkID)) {
			continue
		}
		occurredAt := event.CreatedAt
		if event.OccurredAt != nil {
			occurredAt = *event.OccurredAt
		}
		if !item.UpdatedAt.IsZero() && occurredAt.Before(item.UpdatedAt) {
			continue
		}
		checkID := eventString(payload, "checkId", "check_id", "messageId", "message_id")
		if checkID == "" {
			checkID = strconv.Itoa(event.ID)
		}
		if _, exists := seen[checkID]; exists {
			continue
		}
		seen[checkID] = struct{}{}
		if latest == nil || occurredAt.After(latest.CreatedAt) {
			clone := event
			clone.CreatedAt = occurredAt
			latest = &clone
		}
	}
	return len(seen), latest, nil
}

func (s *teamService) dispatchAssignmentStatusCheck(team *models.Team, bus *redisBus, task *models.TeamTask, item *models.TeamWorkItem, owner *models.TeamMember, activity *teamAssignmentActivitySnapshot, priorChecks int, now time.Time) error {
	if team == nil || bus == nil || task == nil || item == nil || owner == nil {
		return nil
	}
	envelope, messageID := buildAssignmentStatusCheckEnvelopeWithEvidence(team, task, item, owner, activity, priorChecks, now)
	if envelope == nil || strings.TrimSpace(messageID) == "" {
		return nil
	}
	assignmentID := workItemBusinessID(*item)
	if assignmentID == "" {
		assignmentID = item.WorkID
	}
	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,
		"checkId":           messageID,
		"checkSequence":     eventInt(envelope, "checkSequence", "check_sequence"),
		"memberId":          owner.MemberKey,
		"from":              "clawmanager-monitor",
		"to":                owner.MemberKey,
		"workId":            item.WorkID,
		"assignmentId":      assignmentID,
		"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
	}
	if priorChecks >= 2 {
		streamRef := fmt.Sprintf("monitor-escalation:%d:%d", item.ID, item.UpdatedAt.UTC().UnixNano())
		sourceEvent := *event
		sourceEvent.RedisStreamID = &streamRef
		sourcePayload := map[string]interface{}{
			"summary": fmt.Sprintf(
				"Assignment %s revision %d remains %s after %d member checks. Review the exact attempt, dependencies, artifacts, Runtime activity, and completion receipt before deciding whom to remind; do not infer failure from elapsed time alone.",
				assignmentID, teamMaxInt(item.Revision, 1), item.Status, priorChecks,
			),
			"supervisorReviewRequested": true,
			"nonAuthoritative":          true,
			"rootTaskTerminal":          false,
			"assignmentId":              assignmentID,
			"workId":                    item.WorkID,
			"revision":                  teamMaxInt(item.Revision, 1),
			"workItemId":                item.ID,
			"attemptStatus":             item.Status,
			"dependencies":              teamWorkItemDependencies(*item),
			"artifactRefs":              workItemArtifactRefs(*item),
			"acceptedResultReceipt":     workItemHasAcceptedResultReceipt(*item),
			"workflowState":             task.WorkflowState,
			"planVersion":               task.PlanVersion,
			"ledgerVersion":             task.LedgerVersion,
			"activity":                  activity,
		}
		if err := s.createLeaderMediatedRecoveryRequest(team, bus, task, owner, sourcePayload, &sourceEvent); err != nil {
			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) {
	return buildAssignmentStatusCheckEnvelopeWithEvidence(team, task, item, owner, nil, 0, now)
}

func buildAssignmentStatusCheckEnvelopeWithEvidence(team *models.Team, task *models.TeamTask, item *models.TeamWorkItem, owner *models.TeamMember, activity *teamAssignmentActivitySnapshot, priorChecks int, 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)
	// Monitor attempts are throttled by claimAssignmentMonitorSlot, but each
	// accepted attempt needs a fresh durable identity. Reusing UpdatedAt forever
	// made the first response permanently deduplicate every later recovery nudge
	// when a Worker had actually stopped without updating the work item.
	// Milliseconds remain exact in JavaScript Number while still giving every
	// throttled attempt a distinct identity across OpenClaw and Hermes.
	checkSequence := now.UTC().UnixMilli()
	if checkSequence <= 0 {
		checkSequence = time.Now().UTC().UnixMilli()
	}
	messageID := fmt.Sprintf("monitor:%s:%s:%d", taskRef, item.WorkID, checkSequence)
	assignmentID := workItemBusinessID(*item)
	if assignmentID == "" {
		assignmentID = item.WorkID
	}
	promptLines := []string{
		"[STATUS_CHECK] This is an automatic ClawManager assignment monitor.",
		fmt.Sprintf("rootTaskId=%s rootMessageId=%s workId=%s assignmentId=%s revision=%d workItemId=%d", taskRef, task.MessageID, item.WorkID, assignmentID, teamMaxInt(item.Revision, 1), item.ID),
		fmt.Sprintf(
			"Control-plane facts: attemptStatus=%s requiredForRoot=%t reviewRequired=%t workflowState=%s currentPhase=%s planVersion=%d ledgerVersion=%d updatedAt=%s acceptedResultReceipt=%t.",
			item.Status,
			item.RequiredForRoot,
			item.ReviewRequired,
			task.WorkflowState,
			derefTeamString(task.CurrentPhaseID),
			task.PlanVersion,
			task.LedgerVersion,
			item.UpdatedAt.UTC().Format(time.RFC3339Nano),
			workItemHasAcceptedResultReceipt(*item),
		),
	}
	if dependencies := teamWorkItemDependencies(*item); len(dependencies) > 0 {
		promptLines = append(promptLines, "Declared prerequisite assignments: "+strings.Join(dependencies, ", "))
	}
	if activity != nil {
		promptLines = append(promptLines, fmt.Sprintf(
			"Observed Runtime evidence: turnId=%s turnState=%s executionAlive=%t quietForSeconds=%d stallCandidate=%t activityClass=%s sessionCursor=%s lastActivityKind=%s pendingTool=%s lastTool=%s lastToolFailed=%t lastSessionEventAt=%s.",
			activity.TurnID,
			activity.TurnState,
			activity.ExecutionAlive,
			activity.QuietForSeconds,
			activity.StallCandidate,
			activity.ActivityClass,
			activity.SessionCursor,
			activity.LastActivityKind,
			activity.PendingToolName,
			activity.LastToolName,
			activity.LastToolFailed,
			activity.LastSessionEventAt,
		))
		if excerpt := strings.TrimSpace(activity.LastAssistantText); excerpt != "" {
			promptLines = append(promptLines, "Latest internal assistant message for this exact assignment (audit context, not completion proof): "+truncateForSummary(excerpt, 1200))
		}
	}
	if priorChecks > 0 {
		promptLines = append(promptLines, fmt.Sprintf("This assignment has received %d earlier Monitor reminder(s). Re-check current facts; do not repeat a stale status.", priorChecks))
	}
	artifactRefs := workItemArtifactRefs(*item)
	if len(artifactRefs) > 0 {
		promptLines = append(promptLines, "Durable artifact references already attached to this exact attempt: "+strings.Join(artifactRefs, ", "))
	}
	promptLines = append(promptLines, "Re-check these machine facts against your current assignment. If the same turn is genuinely still progressing and there is material new evidence, call team_update_progress with eventKind=\"assignment_check_result\" and continue it. If the turn ended and the deliverable is ready but no accepted receipt exists, call team_complete_task for this exact assignment. If work is blocked, report the concrete dependency, tool, provider, or artifact blocker to the Leader. Do not repeat a stale status and do not claim completion from prose or file existence alone.")
	prompt := strings.Join(promptLines, "\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":       assignmentID,
		"canonicalWorkId":    assignmentID,
		"revision":           teamMaxInt(item.Revision, 1),
		"workItemId":         item.ID,
		"checkId":            messageID,
		"checkSequence":      checkSequence,
		"requestedAt":        now.Format(time.RFC3339Nano),
		"title":              "Assignment status check",
		"prompt":             prompt,
		"rawPrompt":          prompt,
		"artifactRefs":       artifactRefs,
		"monitorPolicy":      defaultTeamMonitorPolicy(),
		"metadata": map[string]interface{}{
			"monitor":       true,
			"monitorType":   "assignment_status_check",
			"eventKind":     "assignment_check_requested",
			"visibleToChat": false,
			"workItemId":    item.ID,
			"assignmentId":  assignmentID,
			"workId":        item.WorkID,
			"revision":      teamMaxInt(item.Revision, 1),
			"workItemTitle": item.Title,
			"lastUpdatedAt": item.UpdatedAt.Format(time.RFC3339Nano),
			"checkId":       messageID,
			"checkSequence": checkSequence,
			"requestedAt":   now.Format(time.RFC3339Nano),
			"priorChecks":   priorChecks,
			"activity":      activity,
			"businessFacts": map[string]interface{}{
				"attemptStatus":         item.Status,
				"requiredForRoot":       item.RequiredForRoot,
				"reviewRequired":        item.ReviewRequired,
				"acceptedResultReceipt": workItemHasAcceptedResultReceipt(*item),
				"dependencies":          teamWorkItemDependencies(*item),
				"artifactRefs":          artifactRefs,
				"workflowState":         task.WorkflowState,
				"currentPhaseId":        derefTeamString(task.CurrentPhaseID),
				"planVersion":           task.PlanVersion,
				"ledgerVersion":         task.LedgerVersion,
			},
		},
		"createdAt": now.Format(time.RFC3339Nano),
	}
	applyTeamTaskEnvelopeContext(envelope, task, owner.MemberKey)
	return envelope, messageID
}

func (s *teamService) createAssignmentSupervisorReview(
	team *models.Team,
	bus *redisBus,
	task *models.TeamTask,
	item *models.TeamWorkItem,
	owner *models.TeamMember,
	activity *teamAssignmentActivitySnapshot,
	now time.Time,
) error {
	if s == nil || team == nil || bus == nil || task == nil || item == nil || owner == nil || activity == nil {
		return nil
	}
	assignmentID := workItemBusinessID(*item)
	if assignmentID == "" {
		assignmentID = item.WorkID
	}
	fingerprintSource := strings.Join([]string{
		strconv.Itoa(team.ID), strconv.Itoa(task.ID), strconv.Itoa(item.ID),
		activity.TurnID, activity.SessionCursor, activity.LastSessionEventAt,
	}, ":")
	fingerprint := teamResultContentHash(map[string]interface{}{"value": fingerprintSource})
	if len(fingerprint) > 24 {
		fingerprint = fingerprint[:24]
	}
	streamRef := "supervisor-" + fingerprint
	summary := fmt.Sprintf(
		"Assignment %s revision %d has a live Runtime turn but no observed session change for %d seconds. Judge whether this is a healthy long model/tool operation, an external wait, a provider stall, or a Runtime identity problem. Do not interrupt or reissue while evidence still shows a live turn.",
		assignmentID,
		teamMaxInt(item.Revision, 1),
		activity.QuietForSeconds,
	)
	payload := map[string]interface{}{
		"summary":                   summary,
		"supervisorReviewRequested": true,
		"nonAuthoritative":          true,
		"rootTaskTerminal":          false,
		"assignmentId":              assignmentID,
		"workId":                    item.WorkID,
		"revision":                  teamMaxInt(item.Revision, 1),
		"workItemId":                item.ID,
		"attemptStatus":             item.Status,
		"requiredForRoot":           item.RequiredForRoot,
		"dependencies":              teamWorkItemDependencies(*item),
		"artifactRefs":              workItemArtifactRefs(*item),
		"acceptedResultReceipt":     workItemHasAcceptedResultReceipt(*item),
		"workflowState":             task.WorkflowState,
		"planVersion":               task.PlanVersion,
		"ledgerVersion":             task.LedgerVersion,
		"activity":                  activity,
	}
	sourceEvent := &models.TeamEvent{
		TeamID:        team.ID,
		TaskID:        &task.ID,
		MemberID:      &owner.ID,
		EventType:     "assignment_supervisor_review_requested",
		RedisStreamID: &streamRef,
		OccurredAt:    &now,
		CreatedAt:     now,
	}
	return s.createLeaderMediatedRecoveryRequest(team, bus, task, owner, payload, sourceEvent)
}

func (s *teamService) observeTaskStall(task *models.TeamTask, timeout time.Duration) error {
	if task == nil {
		return nil
	}
	if task.Status != models.TeamTaskStatusDispatched && task.Status != models.TeamTaskStatusRunning {
		return nil
	}
	lastUpdatedAt := task.UpdatedAt
	team, err := s.repo.GetTeamByID(task.TeamID)
	if err != nil {
		return err
	}
	if team == nil || team.Status == models.TeamStatusDeleted || team.Status == models.TeamStatusDeleting {
		return nil
	}
	cutoff := time.Now().UTC().Add(-timeout)
	active, err := s.taskHasRecentActivity(team, task, cutoff)
	if err != nil {
		return err
	}
	if active {
		return nil
	}

	now := time.Now().UTC()
	previousStatus := task.Status
	message := fmt.Sprintf("No accepted Runtime activity was observed for %s since %s; the task remains open for member/Leader review.", timeout.String(), task.UpdatedAt.Format(time.RFC3339))
	member, err := s.repo.GetMemberByID(task.TargetMemberID)
	if err != nil {
		return err
	}
	payload := map[string]interface{}{
		"v":                 1,
		"event":             "task_stall_observed",
		"eventKind":         "task_stall_observed",
		"teamId":            strconv.Itoa(task.TeamID),
		"taskId":            fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID),
		"messageId":         task.MessageID,
		"previousStatus":    previousStatus,
		"staleAfterSeconds": int(timeout.Seconds()),
		"lastTaskUpdatedAt": lastUpdatedAt.Format(time.RFC3339Nano),
		"diagnostic":        message,
		"source":            "clawmanager_monitor",
		"nonAuthoritative":  true,
		"stateEffect":       "none",
		"rootTaskTerminal":  false,
		"visibleToChat":     false,
	}
	payloadJSON, err := marshalOptionalJSON(payload)
	if err != nil {
		return err
	}
	eventID := fmt.Sprintf("task-stall-observed:%d:%d:%d", task.TeamID, task.ID, now.Unix())
	event := &models.TeamEvent{
		TeamID:      task.TeamID,
		TaskID:      &task.ID,
		EventID:     &eventID,
		EventType:   "task_stall_observed",
		MessageID:   &task.MessageID,
		PayloadJSON: payloadJSON,
		OccurredAt:  &now,
		CreatedAt:   now,
	}
	if member != nil && member.TeamID == task.TeamID {
		event.MemberID = &member.ID
	}
	if err := s.repo.CreateEvent(event); err != nil && !errors.Is(err, repository.ErrDuplicateTeamEvent) {
		return err
	}
	if member == nil || member.TeamID != task.TeamID {
		return nil
	}
	bus, busErr := s.redisBusForTeam(context.Background(), team)
	if busErr != nil {
		// The durable observation remains available to the next reconciliation
		// sweep; an unavailable notification transport must not mutate the task.
		return nil
	}
	return s.createRootCoordinationRecovery(team, bus, task, member, payload, event)
}

func (s *teamService) taskHasRecentActivity(team *models.Team, task *models.TeamTask, cutoff time.Time) (bool, error) {
	if s == nil || team == nil || task == nil {
		return false, nil
	}
	events, err := s.repo.ListEventsByTeamID(team.ID, 500)
	if err != nil {
		return false, err
	}
	for idx := range events {
		event := events[idx]
		// CreatedAt is assigned by ClawManager when the event is accepted. Runtime
		// clocks can drift and must not keep a dead task alive or mark an active
		// task stale merely because occurredAt arrived out of order.
		eventTime := event.CreatedAt
		if !eventTime.After(cutoff) {
			continue
		}
		payload := teamEventPayloadMap(event)
		if teamEventMatchesRootTask(event, payload, task) {
			return true, nil
		}
	}
	workItems, err := s.repo.ListWorkItemsByRootTaskID(task.ID)
	if err != nil {
		return false, err
	}
	for idx := range workItems {
		item := workItems[idx]
		if !isActiveTeamWorkItemStatus(item.Status) {
			continue
		}
		if item.UpdatedAt.After(cutoff) {
			return true, nil
		}
	}
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		return false, err
	}
	for idx := range members {
		member := members[idx]
		ownsRootTask := member.CurrentTaskID != nil && *member.CurrentTaskID == task.ID
		runtimeTracksRoot := member.RuntimeTaskID != nil && strings.TrimSpace(*member.RuntimeTaskID) == task.MessageID
		if !ownsRootTask && !runtimeTracksRoot {
			continue
		}
		if member.UpdatedAt.After(cutoff) {
			return true, nil
		}
		if member.LastSeenAt != nil && member.LastSeenAt.After(cutoff) {
			return true, nil
		}
	}
	return false, nil
}

func (s *teamService) redisBusForTeam(ctx context.Context, team *models.Team) (*redisBus, error) {
	redisURL := ""
	if team.RedisURLSecretName != nil && team.RedisURLSecretKey != nil {
		client := k8s.GetClient()
		if client == nil {
			return nil, fmt.Errorf("k8s client not initialized")
		}
		value, err := s.secretService.GetSecretValue(ctx, client.GetNamespace(team.UserID), *team.RedisURLSecretName, *team.RedisURLSecretKey)
		if err != nil {
			return nil, err
		}
		redisURL = strings.TrimSpace(value)
	}
	if redisURL == "" {
		redisURL = defaultTeamRedisURL()
	}
	if redisURL == "" {
		return nil, fmt.Errorf("team redis url is required")
	}
	return newRedisBus(redisURL)
}

type teamTaskProjectionResult struct {
	status  string
	changed bool
}

func projectTeamTaskRuntimeState(task *models.TeamTask, payload map[string]interface{}, eventType string, payloadJSON *string, now time.Time) teamTaskProjectionResult {
	if task == nil {
		return teamTaskProjectionResult{}
	}
	status := normalizedTeamTaskEventStatus(payload)
	completed := isTeamTaskCompletionSignal(eventType, status, payload)
	failed := isTeamTaskFailureSignal(eventType, status, payload)
	running := isTeamTaskRunningSignal(eventType, status, payload)

	result := teamTaskProjectionResult{}
	setStatus := func(next string) {
		result.status = next
		if task.Status != next {
			task.Status = next
			result.changed = true
		}
	}
	setStarted := func() {
		if task.StartedAt == nil {
			task.StartedAt = &now
			result.changed = true
		}
	}
	setFinished := func() {
		if task.FinishedAt == nil || !task.FinishedAt.Equal(now) {
			task.FinishedAt = &now
			result.changed = true
		}
	}

	if eventBool(payload, "artifactValidationFailed", "artifact_validation_failed") {
		setStatus(models.TeamTaskStatusRunning)
		setStarted()
		if task.FinishedAt != nil {
			task.FinishedAt = nil
			result.changed = true
		}
		// Keep the member's final body available while the artifact contract is
		// unresolved. A later valid completion replaces this payload and closes
		// the task normally.
		if payloadJSON != nil && (task.ResultJSON == nil || *task.ResultJSON != *payloadJSON) {
			task.ResultJSON = payloadJSON
			result.changed = true
		}
		return result
	}

	if completed {
		setStatus(models.TeamTaskStatusSucceeded)
		setFinished()
		if payloadJSON != nil && (task.ResultJSON == nil || *task.ResultJSON != *payloadJSON) {
			task.ResultJSON = payloadJSON
			result.changed = true
		}
		if task.ErrorMessage != nil {
			task.ErrorMessage = nil
			result.changed = true
		}
		return result
	}

	if failed && task.Status != models.TeamTaskStatusSucceeded {
		setStatus(models.TeamTaskStatusFailed)
		setFinished()
		if errText := eventString(payload, "error_message", "error", "reason", "diagnostic", "lastSummary", "last_summary", "summary"); errText != "" {
			if task.ErrorMessage == nil || *task.ErrorMessage != errText {
				task.ErrorMessage = &errText
				result.changed = true
			}
		}
		return result
	}

	if isTerminalTeamTaskStatus(task.Status) {
		return result
	}

	switch eventType {
	case "task_received":
		if task.Status == models.TeamTaskStatusPending {
			setStatus(models.TeamTaskStatusDispatched)
		}
	case "task_started":
		setStatus(models.TeamTaskStatusRunning)
		setStarted()
	case "outbound", "task_assigned", "team_send", "peer_request", "peer_handoff", "peer_review_request":
		if task.Status == models.TeamTaskStatusPending {
			setStatus(models.TeamTaskStatusDispatched)
		}
		setStarted()
	default:
		if running {
			setStatus(models.TeamTaskStatusRunning)
			setStarted()
		}
	}
	return result
}

func normalizedTeamTaskEventStatus(payload map[string]interface{}) string {
	raw := eventString(payload, "task_status", "taskStatus", "result_status", "resultStatus", "status", "state")
	raw = strings.ToLower(strings.TrimSpace(raw))
	raw = strings.ReplaceAll(raw, "-", "_")
	raw = strings.ReplaceAll(raw, " ", "_")
	return raw
}

func isTeamTaskCompletionSignal(eventType, status string, payload map[string]interface{}) bool {
	if isFailedTeamTaskEventStatus(status) || isDispatchOnlyCompletionPayload(payload) {
		return false
	}
	if eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
		return false
	}
	if teamRedisProtocolVersion(payload) >= 2 {
		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
	}
	switch eventType {
	case "task_completed", "completion", "task_failed", "message_failed":
		return true
	}
	return hasTeamTaskCompletionToolCall(payload) && isSuccessfulTeamTaskEventStatus(status)
}

func isTrustedNaturalTurnCompletion(eventType string, payload map[string]interface{}) bool {
	// Compatibility probe only. Natural assistant prose is never an
	// authoritative Team completion, regardless of Runtime version.
	return false
}

func hasAuthoritativeTeamCompletionPayload(eventType, status string, payload map[string]interface{}) bool {
	if payload == nil {
		return false
	}
	if isDispatchOnlyCompletionPayload(payload) {
		return false
	}
	source := strings.ToLower(strings.TrimSpace(eventString(payload, "completionSource", "completion_source")))
	explicitCompletion := hasTeamTaskCompletionToolCall(payload) ||
		source == teamTaskCompletionTool ||
		eventBool(payload, "explicitCompletion", "explicit_completion", "rootTaskTerminal")
	if !explicitCompletion {
		return false
	}
	if step, ok := payload["collaborationStep"].(map[string]interface{}); ok {
		switch strings.ToLower(strings.TrimSpace(eventString(step, "type"))) {
		case "assignment", "progress", "ack", "peer_request":
			return false
		}
	}
	if eventType != "task_completed" && eventType != "completion" && eventType != "task_failed" && eventType != "message_failed" {
		return false
	}
	if !hasTeamCompletionResultBody(payload) {
		return false
	}
	return status == "" || isSuccessfulTeamTaskEventStatus(status)
}

func hasTeamCompletionResultBody(payload map[string]interface{}) bool {
	if payload == nil {
		return false
	}
	if body := eventString(payload, "resultMarkdown", "result_markdown", "result", "answer"); body != "" {
		return true
	}
	if summary := eventString(payload, "summary"); summary != "" {
		return true
	}
	if step, ok := payload["collaborationStep"].(map[string]interface{}); ok {
		if body := eventString(step, "resultMarkdown", "result_markdown", "result", "answer"); body != "" {
			return true
		}
		if summary := eventString(step, "summary"); summary != "" {
			return true
		}
	}
	for _, record := range eventRecordCandidates(payload) {
		if body := eventString(record, "resultMarkdown", "result_markdown", "result", "answer"); body != "" {
			return true
		}
		if summary := eventString(record, "summary"); summary != "" {
			return true
		}
	}
	return false
}

func isTeamTaskFailureSignal(eventType, status string, payload map[string]interface{}) bool {
	if isSuccessfulTeamTaskEventStatus(status) || isNonAuthoritativeDispatchFailure(eventType, payload) {
		return false
	}
	if teamRedisProtocolVersion(payload) >= 2 {
		if eventType != "task_failed" && eventType != "completion_proposed" {
			return false
		}
		source := strings.ToLower(strings.TrimSpace(eventString(payload, "completionSource", "completion_source")))
		if source != teamTaskCompletionTool && source != "runtime_error" && source != "runtime_processing" {
			return false
		}
		return hasStrictTeamFailureEnvelope(payload)
	}
	switch eventType {
	case "task_failed", "message_failed":
		return true
	}
	if !hasTeamTaskCompletionToolCall(payload) {
		return false
	}
	switch status {
	case "failed", "failure", "error", "errored", "blocked":
		return true
	default:
		return false
	}
}

// isAuthoritativeAssignmentFailure is the single assignment-level terminal
// failure gate. It relies on the authenticated event kind and active assignment
// identity, never on a free-form progress status such as "blocked". Legacy
// runtimes remain compatible because an authenticated task_failed event bound
// to an assignment is sufficient even when newer receipt fields are absent.
func isAuthoritativeAssignmentFailure(eventType string, payload map[string]interface{}) bool {
	if payload == nil || isStateNeutralTeamObservation(payload) || isNonAuthoritativeDispatchFailure(eventType, payload) {
		return false
	}
	status := normalizedTeamTaskEventStatus(payload)
	if isTeamTaskFailureSignal(eventType, status, payload) {
		return true
	}
	if !strings.EqualFold(strings.TrimSpace(eventType), "task_failed") {
		return false
	}
	return eventString(payload,
		"assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id",
	) != ""
}

func hasStrictTeamFailureEnvelope(payload map[string]interface{}) bool {
	if payload == nil {
		return false
	}
	for _, keys := range [][]string{
		{"eventId", "event_id"},
		{"completionId", "completion_id"},
		{"taskId", "task_id"},
		{"rootTaskId", "root_task_id"},
		{"memberId", "member_id"},
		{"summary"},
	} {
		if eventString(payload, keys...) == "" {
			return false
		}
	}
	return len(explicitTeamArtifactReferences(payload)) > 0
}

func teamRedisProtocolVersion(payload map[string]interface{}) int {
	if payload == nil {
		return 1
	}
	if version := eventInt(payload, "protocolVersion"); version > 0 {
		return version
	}
	if version := eventInt(payload, "protocol_version"); version > 0 {
		return version
	}
	return 1
}

func isExplicitTeamTaskCompletion(payload map[string]interface{}) bool {
	if payload == nil || eventString(payload, "completionId", "completion_id") == "" {
		return false
	}
	if !eventBool(payload, "explicitCompletion", "explicit_completion") {
		return false
	}
	return strings.EqualFold(
		strings.TrimSpace(eventString(payload, "completionSource", "completion_source")),
		teamTaskCompletionTool,
	)
}

func hasStrictTeamCompletionEnvelope(payload map[string]interface{}) bool {
	if payload == nil || !hasTeamCompletionResultBody(payload) {
		return false
	}
	for _, keys := range [][]string{
		{"eventId", "event_id"},
		{"completionId", "completion_id"},
		{"taskId", "task_id"},
		{"rootTaskId", "root_task_id"},
		{"memberId", "member_id"},
		{"summary"},
		{"resultMarkdown", "result_markdown"},
	} {
		if eventString(payload, keys...) == "" {
			return false
		}
	}
	// 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
}

// hydrateExplicitCompletionEnvelope fills correlation fields that can be
// derived from the authenticated stream context.  Older Lite images sometimes
// omit these mirrors after a deferred completion retry.  They are transport
// metadata, not a business decision: this helper never invents a completion
// source, explicit flag, owner, terminal flag, or result body.
func hydrateExplicitCompletionEnvelope(payload map[string]interface{}, team *models.Team, task *models.TeamTask, member *models.TeamMember, streamID string) {
	if payload == nil || team == nil || task == nil || member == nil {
		return
	}
	if teamRedisProtocolVersion(payload) < 2 || !isExplicitTeamTaskCompletion(payload) {
		return
	}
	rootTaskID := fmt.Sprintf("team-%d-task-%d", team.ID, task.ID)
	if reported := eventString(payload, "taskId", "task_id"); reported != "" && reported != rootTaskID {
		payload["reportedTaskId"] = reported
	}
	payload["taskId"] = rootTaskID
	if reported := eventString(payload, "rootTaskId", "root_task_id"); reported != "" && reported != rootTaskID {
		payload["reportedRootTaskId"] = reported
	}
	payload["rootTaskId"] = rootTaskID
	if reported := eventString(payload, "memberId", "member_id"); reported != "" && !teamMemberRouteEquivalent(reported, member.MemberKey) {
		payload["reportedMemberId"] = reported
	}
	payload["memberId"] = member.MemberKey
	if eventString(payload, "eventId", "event_id") == "" && strings.TrimSpace(streamID) != "" {
		payload["eventId"] = "stream:" + streamID
	}
	if eventString(payload, "resultMarkdown", "result_markdown") == "" {
		if draft := eventString(payload, "completionDraftMarkdown", "completion_draft_markdown"); draft != "" {
			payload["resultMarkdown"] = draft
		} else if result := eventString(payload, "result", "answer"); result != "" {
			payload["resultMarkdown"] = result
		} else if summary := eventString(payload, "summary", "completionDraftSummary", "completion_draft_summary"); summary != "" {
			// A short legacy final answer is still an answer.  Preserve flow while
			// keeping the hard ownership/work-ledger checks in the evaluator.
			payload["resultMarkdown"] = summary
		}
	}
	if eventString(payload, "summary") == "" {
		if summary := eventString(payload, "completionDraftSummary", "completion_draft_summary", "resultMarkdown", "result_markdown"); summary != "" {
			payload["summary"] = compactTeamEventSummary(summary, 240)
		}
	}
}

func (s *teamService) hasAcceptedTeamCompletionID(teamID int, completionID string) (bool, error) {
	completionID = strings.TrimSpace(completionID)
	if s == nil || completionID == "" {
		return false, nil
	}
	return s.repo.EventExistsByCompletionID(teamID, completionID)
}

func isUnauthoritativeCompletionEvent(eventType string, completion, failure bool) bool {
	if completion || failure {
		return false
	}
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "task_completed", "completion":
		return true
	default:
		return false
	}
}

func isSuccessfulTeamTaskEventStatus(status string) bool {
	switch status {
	case "succeeded", "success", "completed", "complete", "done", "finished", "ok":
		return true
	default:
		return false
	}
}

func isFailedTeamTaskEventStatus(status string) bool {
	switch status {
	case "failed", "failure", "error", "errored":
		return true
	default:
		return false
	}
}

// isWaitingTeamTaskEventStatus classifies execution states that keep the same
// assignment open. A Worker may use "blocked" for a dependency/input wait; the
// status alone is therefore never authority for a failed business result.
func isWaitingTeamTaskEventStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case "blocked", "waiting", "waiting_dependency", "waiting_dependencies",
		"waiting_input", "waiting_review", "waiting_completion", "paused":
		return true
	default:
		return false
	}
}

func isStateNeutralTeamObservation(payload map[string]interface{}) bool {
	if payload == nil {
		return false
	}
	if eventBool(payload, "nonAuthoritative", "non_authoritative") {
		return true
	}
	switch strings.ToLower(strings.TrimSpace(eventString(payload, "stateEffect", "state_effect"))) {
	case "none", "observe", "observation", "advisory":
		return true
	}
	switch strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))) {
	case "worker_progress", "assignment_heartbeat", "assignment_check_requested",
		"assignment_check_result", "dependency_wait", "dependency_ready_reminder":
		return true
	default:
		return false
	}
}

func isDispatchOnlyCompletionPayload(payload map[string]interface{}) bool {
	for _, key := range []string{"resultMarkdown", "result_markdown", "result", "answer"} {
		if body := eventString(payload, key); looksLikeSubstantiveResultDocument(body) {
			return false
		}
	}
	text := strings.ToLower(strings.Join(strings.Fields(strings.Join([]string{
		eventString(payload, "summary", "lastSummary", "last_summary", "message", "text", "diagnostic"),
		eventString(payload, "title", "intent"),
	}, " ")), " "))
	if text == "" {
		return false
	}
	if text == "redis team task completed" || text == "redis team task processing completed" {
		return true
	}
	if strings.Contains(text, "result already delivered") || strings.Contains(text, "\u7ed3\u679c\u5df2\u53cd\u9988") {
		return true
	}
	compact := strings.ReplaceAll(text, " ", "")
	if len([]rune(compact)) > 120 {
		return false
	}
	if strings.Contains(text, "dispatch") && (strings.Contains(text, "worker") || strings.Contains(text, "member")) {
		return true
	}
	return strings.Contains(compact, "\u5728\u7ebf\u7a7a\u95f2") ||
		strings.Contains(compact, "\u6d3e\u5355") ||
		strings.Contains(compact, "\u5df2\u6d3e\u53d1") ||
		strings.Contains(compact, "\u7b49\u5f85\u5176") ||
		strings.Contains(compact, "\u4efb\u52a1\u5206\u6d3e")
}

func looksLikeSubstantiveResultDocument(value string) bool {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return false
	}
	compact := strings.ReplaceAll(strings.ToLower(strings.Join(strings.Fields(trimmed), "")), " ", "")
	if compact == "redisteamtaskcompleted" || compact == "redisteamtaskprocessingcompleted" {
		return false
	}
	if len([]rune(compact)) >= 180 {
		return true
	}
	return strings.Contains(trimmed, "\n## ") ||
		strings.Contains(trimmed, "\n|") ||
		strings.Contains(trimmed, "\n- ") ||
		strings.Contains(trimmed, "\n1.") ||
		strings.Contains(trimmed, "\n### ")
}
func isTeamTaskRunningSignal(eventType, status string, payload map[string]interface{}) bool {
	switch eventType {
	case "task_started", "task_progress", "progress":
		return true
	}
	switch status {
	case "running", "in_progress", "processing", "busy", "working":
		return true
	}
	if isWaitingTeamTaskEventStatus(status) {
		return true
	}
	progress := eventInt(payload, "progress")
	return progress > 0 && progress < 100
}

func isTerminalTeamTaskStatus(status string) bool {
	return status == models.TeamTaskStatusSucceeded ||
		status == models.TeamTaskStatusFailed ||
		status == models.TeamTaskStatusStale
}

func isActiveTeamWorkItemStatus(status string) bool {
	switch strings.ToLower(strings.TrimSpace(status)) {
	case models.TeamTaskStatusDispatched, models.TeamTaskStatusRunning, models.TeamTaskStatusWaiting:
		return true
	default:
		return false
	}
}

func isImmutableRootTaskStatus(status string) bool {
	return status == models.TeamTaskStatusSucceeded || status == models.TeamTaskStatusFailed
}

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 isPostTerminalMutableTeamEvent(eventType string, payload map[string]interface{}) bool {
	eventType = strings.ToLower(strings.TrimSpace(eventType))
	eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind")))
	switch eventType {
	case "task_received", "task_started", "task_progress", "progress", "assignment_heartbeat",
		"assignment_check_requested", "completion_proposed", "completion_deferred",
		"completion_rejected", "completion_needs_confirmation", "leader_decision_reminder",
		"leader_synthesis_reminder", "outbound", "team_send", "task_assigned",
		"peer_request", "peer_handoff", "peer_review_request":
		return true
	}
	switch eventKind {
	case "leader_plan", "leader_progress", "worker_plan", "worker_progress", "leader_synthesis",
		"agent_narrative", "agent_plan", "agent_progress", "agent_synthesis",
		"assignment_heartbeat", "assignment_check_requested", "assignment_check_result",
		"leader_decision_reminder", "leader_synthesis_reminder", "completion_deferred",
		"agent_assignment", "agent_handoff":
		return true
	}
	return false
}

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
	}
	// Prefer the assignment contract over a worker's descriptive workId.  Old
	// monitor replies can contain both, and choosing workId first would make a
	// stale check miss the already-completed canonical card.
	workID := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
	canonicalWorkID := "member-" + normalizeTeamMemberRouteKey(member.MemberKey)
	found := false
	for idx := range items {
		item := items[idx]
		if item.OwnerMemberID == nil || *item.OwnerMemberID != member.ID {
			continue
		}
		itemAssignmentID := derefTeamString(item.AssignmentID)
		itemCanonicalID := derefTeamString(item.CanonicalWorkID)
		if workID != "" && item.WorkID != workID && itemAssignmentID != workID && itemCanonicalID != workID && item.WorkID != canonicalWorkID {
			continue
		}
		found = true
		if !isTerminalTeamTaskStatus(item.Status) {
			return false, nil
		}
	}
	return found, nil
}

// reconcileTerminalMonitorWorkItem intentionally never writes business state.
// Monitor observations can wake the real member/Leader recovery path, but only
// the member's actual completion/failure event may close an assignment.
func (s *teamService) reconcileTerminalMonitorWorkItem(team *models.Team, task *models.TeamTask, member *models.TeamMember, payload map[string]interface{}, now time.Time) (bool, error) {
	return false, 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":
		return true
	case "outbound", "task_assigned", "team_send", "peer_request", "peer_handoff", "peer_review_request", "peer_reply":
		return true
	}
	if eventString(payload, "taskId", "task_id", "runtimeTaskId", "runtime_task_id", "messageId", "message_id") != "" {
		return true
	}
	return teamEventHasBody(payload)
}

func isNonAuthoritativeDispatchFailure(eventType string, payload map[string]interface{}) bool {
	if eventType != "task_failed" && eventType != "message_failed" {
		return false
	}
	// message_failed describes an outbound transport attempt, not the business
	// outcome of the authenticated member assignment. Converting it into a
	// failed Work Item made a recoverable bad recipient or delivery race close
	// the attempt even while the Runtime session was still working.
	if eventType == "message_failed" {
		return true
	}
	if eventBool(payload, "nonAuthoritative", "non_authoritative") ||
		strings.EqualFold(eventString(payload, "stateEffect", "state_effect"), "none") ||
		strings.EqualFold(eventString(payload, "failureDomain", "failure_domain"), "transport") {
		return true
	}
	text := strings.ToLower(strings.Join(strings.Fields(strings.Join([]string{
		eventString(payload, "error_message", "error", "reason", "diagnostic", "lastSummary", "last_summary", "summary", "text", "message"),
	}, " ")), " "))
	if text == "redis team task failed" {
		return true
	}
	return strings.Contains(text, "dispatch finished without reply/completion") ||
		strings.Contains(text, "without reply/completion")
}

func isNonAuthoritativeDispatchWarning(eventType string, payload map[string]interface{}) bool {
	if eventType != "message_warning" {
		return false
	}
	return isNonAuthoritativeDispatchFailure(eventString(payload, "originalEvent"), payload) ||
		eventBool(payload, "nonAuthoritative", "non_authoritative") &&
			strings.EqualFold(eventString(payload, "stateEffect", "state_effect"), "none")
}

func isTargetResolutionWarning(eventType string, payload map[string]interface{}) bool {
	if eventType != "message_warning" && eventType != "message_failed" {
		return false
	}
	return strings.EqualFold(eventString(payload, "eventKind", "event_kind"), "target_resolution_warning") ||
		(strings.EqualFold(eventString(payload, "failureDomain", "failure_domain"), "transport") &&
			strings.EqualFold(eventString(payload, "failureKind", "failure_kind"), "target_resolution"))
}

func isLeaderMediatedLeaderDispatchOnlyCompletion(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask, completion bool) bool {
	return completion && isLeaderMediatedLeaderDispatchOnlyMessage(team, eventType, payload, member, task)
}

func isLeaderMediatedLeaderDispatchOnlyMessage(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask) bool {
	if team == nil || payload == nil || member == nil || task == nil {
		return false
	}
	if normalizedTeamCommunicationMode(team.CommunicationMode) != teamCommunicationModeLeaderMediated {
		return false
	}
	if member.ID != task.TargetMemberID || !isLeaderTeamMember(member) {
		return false
	}
	if strings.TrimSpace(eventString(payload, "intent")) == initialLeaderTaskIntent {
		return false
	}
	if eventBool(payload, "rootTaskTerminal", "root_task_terminal", "finalSynthesis", "final_synthesis") {
		return false
	}
	return looksLikeLeaderDispatchOnlyText(eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary"))
}

func (s *teamService) leaderMediatedRootCompletionReady(team *models.Team, task *models.TeamTask, member *models.TeamMember) (bool, error) {
	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
	}
	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
		}
		ownerID := *item.OwnerMemberID
		requiredOwners[ownerID] = struct{}{}
		if item.Status == models.TeamTaskStatusSucceeded {
			deliveredOwners[ownerID] = struct{}{}
		}
	}
	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)
	if err != nil {
		return false, err
	}
	required := map[string]struct{}{}
	delivered := map[string]struct{}{}
	for idx := range events {
		event := events[idx]
		payload := teamEventPayloadMap(event)
		if !teamEventMatchesRootTask(event, payload, task) {
			continue
		}
		step, _ := payload["collaborationStep"].(map[string]interface{})
		stepType := eventString(step, "type")
		actor := normalizeTeamMemberRouteKey(eventString(step, "actor"))
		target := normalizeTeamMemberRouteKey(eventString(step, "target"))
		if actor == "" && event.MemberID != nil {
			// The event table keeps member_id numeric; fall back to payload fields only here.
			actor = normalizeTeamMemberRouteKey(eventString(payload, "memberId", "member_id", "from", "sourceMemberId", "senderMemberId"))
		}
		if target == "" {
			target = normalizeTeamMemberRouteKey(leaderMediatedRouteTarget(payload))
		}
		if stepType == "assignment" && isLeaderRouteTarget(actor) && target != "" && !isLeaderRouteTarget(target) {
			required[target] = struct{}{}
			continue
		}
		if eventBool(payload, "assignmentResultOnly", "assignment_result_only") && actor != "" && !isLeaderRouteTarget(actor) {
			delivered[actor] = struct{}{}
			continue
		}
		if stepType == "result" && actor != "" && !isLeaderRouteTarget(actor) && (target == "" || isLeaderRouteTarget(target)) {
			delivered[actor] = struct{}{}
		}
	}
	if len(required) == 0 {
		return true, nil
	}
	for memberKey := range required {
		if !leaderMediatedDeliveredForRequiredMember(memberKey, delivered) {
			return false, nil
		}
	}
	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
}

type teamPhaseDisposition struct {
	PhaseID  string
	Decision string
	Reason   string
}

func structuredTeamPhaseDispositions(payload map[string]interface{}) map[string]teamPhaseDisposition {
	result := map[string]teamPhaseDisposition{}
	raw := firstTeamValue(payload, "phaseDispositions", "phase_dispositions")
	values, ok := raw.([]interface{})
	if !ok {
		return result
	}
	for _, value := range values {
		entry, ok := value.(map[string]interface{})
		if !ok {
			continue
		}
		disposition := teamPhaseDisposition{
			PhaseID:  eventString(entry, "phaseId", "phase_id", "id"),
			Decision: strings.ToLower(eventString(entry, "decision", "status")),
			Reason:   eventString(entry, "reason"),
		}
		if disposition.PhaseID == "" || disposition.Reason == "" {
			continue
		}
		switch disposition.Decision {
		case "cancelled", "skipped", "superseded":
			result[disposition.PhaseID] = disposition
		}
	}
	return result
}

func teamPhaseDispositionPayloads(dispositions map[string]teamPhaseDisposition) []interface{} {
	phaseIDs := make([]string, 0, len(dispositions))
	for phaseID := range dispositions {
		phaseIDs = append(phaseIDs, phaseID)
	}
	sort.Strings(phaseIDs)
	result := make([]interface{}, 0, len(phaseIDs))
	for _, phaseID := range phaseIDs {
		disposition := dispositions[phaseID]
		result = append(result, map[string]interface{}{
			"phaseId":  disposition.PhaseID,
			"decision": disposition.Decision,
			"reason":   disposition.Reason,
		})
	}
	return result
}

func phaseUsesExplicitDisposition(phase models.TeamWorkflowPhase) bool {
	return strings.EqualFold(strings.TrimSpace(derefTeamString(phase.CompletionPolicy)), teamPhaseCompletionPolicyExplicitV1)
}

func isLeaderFinalWorkflowPhase(phase models.TeamWorkflowPhase) bool {
	phaseID := strings.ToLower(strings.TrimSpace(phase.PhaseID))
	return strings.Contains(phaseID, "final") || strings.Contains(phaseID, "synthesis")
}

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)
	// Keep the Runtime proposal for audit, but make the locked database ledger
	// explicit as the effective source of truth. This prevents an accepted
	// completion from looking as though it closed workflowState=planning with
	// ledgerVersion=0 while preserving compatibility with older runtimes.
	payload["reportedWorkflowState"] = eventString(payload, "workflowState", "workflow_state")
	payload["reportedPlanVersion"] = eventInt(payload, "planVersion", "plan_version")
	payload["reportedLedgerVersion"] = eventInt(payload, "ledgerVersion", "ledger_version")
	payload["effectiveWorkflowState"] = task.WorkflowState
	payload["effectivePlanVersion"] = task.PlanVersion
	payload["effectiveLedgerVersion"] = task.LedgerVersion
	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")) {
		// Tool envelopes are model/runtime output, not workflow authority. The
		// authenticated Leader, resolved root task, and current ledger determine
		// scope; missing mirrored flags are retained only as diagnostics.
		payload["completionEnvelopeNormalized"] = true
		payload["reportedRootTaskTerminal"] = eventBool(payload, "rootTaskTerminal", "root_task_terminal")
		payload["rootTaskTerminal"] = true
	}
	proposalPlanVersion := int64(eventInt(payload, "planVersion", "plan_version"))
	proposalLedgerVersion := int64(eventInt(payload, "ledgerVersion", "ledger_version"))
	if proposalPlanVersion > 0 && task.PlanVersion > 0 && proposalPlanVersion != task.PlanVersion {
		payload["stalePlanDiagnostic"] = true
		payload["planVersion"] = task.PlanVersion
	}
	if proposalLedgerVersion > 0 && task.LedgerVersion > 0 && proposalLedgerVersion != task.LedgerVersion {
		payload["staleLedgerDiagnostic"] = true
		payload["ledgerVersion"] = task.LedgerVersion
	}

	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
			}
		}
	}
	ignoredWaivers := make([]string, 0)
	for key := range waivers {
		item, exists := byBusinessID[key]
		if !exists || item.Status == models.TeamTaskStatusSucceeded || (item.Status != models.TeamTaskStatusFailed && item.Status != models.TeamTaskStatusStale) {
			ignoredWaivers = append(ignoredWaivers, key)
		}
	}
	if len(ignoredWaivers) > 0 {
		sort.Strings(ignoredWaivers)
		payload["ignoredWaivers"] = uniqueTeamStrings(ignoredWaivers)
		payload["waiverDiagnostic"] = "waivers apply only to current failed or stale assignments"
	}
	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
		}
		// dependsOn is planning metadata authored by models and older runtimes.
		// It may contain an assignment ID, a phase ID, or a natural label. Once
		// this downstream contract has actually succeeded, that advisory text
		// must not recreate a blocker. Any real required predecessor is already
		// evaluated independently above, and incomplete phase work is evaluated
		// below. ReviewRequired is likewise contract metadata: the issued
		// validator/reviewer is its own required work item and is checked above,
		// so the reviewed assignment never needs a second Agent-authored closure.
		// This keeps completion safe without requiring Agents to reproduce an
		// exact control-plane identifier.
	}
	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"))
	phaseDispositions := structuredTeamPhaseDispositions(payload)
	ignoredPhaseDispositions := make([]interface{}, 0)
	for idx := range phases {
		phase := phases[idx]
		if latestPlanVersion > 0 && phase.PlanVersion != latestPlanVersion {
			continue
		}
		disposition, exists := phaseDispositions[phase.PhaseID]
		if !exists {
			continue
		}
		hasRequiredWork := phaseHasRequiredWork(phase.PhaseID, byBusinessID, member.ID)
		phaseSatisfied := phase.Status == teamPhaseStatusCompleted ||
			(hasRequiredWork && !phaseHasIncompleteRequiredWork(phase.PhaseID, byBusinessID, member.ID, waivers))
		if !phaseSatisfied {
			continue
		}
		ignoredPhaseDispositions = append(ignoredPhaseDispositions, map[string]interface{}{
			"phaseId":          disposition.PhaseID,
			"reportedDecision": disposition.Decision,
			"reason":           disposition.Reason,
			"ignoredReason":    "phase_already_satisfied",
		})
		delete(phaseDispositions, phase.PhaseID)
	}
	if len(ignoredPhaseDispositions) > 0 {
		payload["reportedPhaseDispositions"] = firstTeamValue(payload, "phaseDispositions", "phase_dispositions")
		payload["ignoredPhaseDispositions"] = ignoredPhaseDispositions
		payload["phaseDispositionDiagnostic"] = "dispositions apply only to unexecuted planned phases"
		payload["phaseDispositions"] = teamPhaseDispositionPayloads(phaseDispositions)
	}
	if protocolVersion >= 3 && (!workflowFinal || !finalAnswerReady || len(remainingActions) > 0) {
		payload["workflowSealDiagnostic"] = map[string]interface{}{
			"reportedWorkflowFinal":    workflowFinal,
			"reportedFinalAnswerReady": finalAnswerReady,
			"reportedRemainingActions": remainingActions,
		}
	}
	awaitingPersistedLeaderDecision := false
	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
		}
		if phaseHasIncompleteRequiredWork(phase.PhaseID, byBusinessID, member.ID, waivers) {
			result.PendingPhases = append(result.PendingPhases, phase.PhaseID)
			continue
		}
		hasRequiredWork := phaseHasRequiredWork(phase.PhaseID, byBusinessID, member.ID)
		finalSynthesisExecuted := !hasRequiredWork &&
			isLeaderFinalWorkflowPhase(phase) &&
			completionExecutesFinalSynthesis(payload)
		if phaseUsesExplicitDisposition(phase) && !hasRequiredWork && !finalSynthesisExecuted {
			// A required phase recorded in the current plan but never materialized is
			// a real workflow ambiguity, not proof of completion. Keep the completed
			// attempts terminal and ask the Leader to confirm whether this phase is
			// still needed. A structured disposition is accepted when supplied, but
			// Monitor/Supervisor must never manufacture it or silently cancel work.
			if disposition, ok := phaseDispositions[phase.PhaseID]; !ok ||
				(disposition.Decision != "cancelled" && disposition.Decision != "skipped" && disposition.Decision != "superseded") {
				result.PendingPhases = append(result.PendingPhases, phase.PhaseID+":leader_review")
			}
			continue
		}
		if finalSynthesisExecuted {
			continue
		}
		// Legacy plans did not declare an explicit phase-disposition policy.
		// Preserve their historical workflow sealing semantics so an upgraded
		// control plane cannot strand an already-running Team.
		// A persisted awaiting-decision state represents an unresolved workflow
		// choice. It cannot be cleared by Monitor or inferred from a missing model
		// field; the Leader receives a recoverable confirmation request instead.
		if phase.DecisionRequired && phase.Status == teamPhaseStatusAwaitingLeaderDecision && !workflowFinal {
			result.PendingPhases = append(result.PendingPhases, phase.PhaseID+":leader_review")
			awaitingPersistedLeaderDecision = true
		}
	}
	sort.Strings(result.PendingPhases)
	result.PendingPhases = uniqueTeamStrings(result.PendingPhases)
	if len(result.PendingPhases) > 0 {
		result.Decision = teamCompletionDecisionDeferred
		if awaitingPersistedLeaderDecision {
			result.Reason = "workflow_not_sealed"
		} else {
			result.Reason = "open_workflow_phases"
		}
		return result, nil
	}

	result.StrongContradictions = analyzeCompletionNarrativeContradictions(payload)
	if len(result.StrongContradictions) > 0 {
		payload["narrativeReviewSuggested"] = true
		payload["narrativeContradictions"] = result.StrongContradictions
		result.Decision = teamCompletionDecisionNeedsConfirmation
		result.Reason = "narrative_indicates_remaining_work"
	}
	return result, nil
}

func completionExecutesFinalSynthesis(payload map[string]interface{}) bool {
	if payload == nil ||
		!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 {
		return false
	}
	return strings.TrimSpace(eventString(
		payload,
		"resultMarkdown",
		"result_markdown",
		"result",
		"answer",
		"completionDraftMarkdown",
		"completion_draft_markdown",
	)) != ""
}

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)
}

type teamDependencyFacts struct {
	Ready   []string
	Waiting []string
	Unknown []string
}

// resolveTeamWorkItemDependencyFacts keeps model-authored dependency text
// advisory unless it can be losslessly bound to assignment identities in the
// current root ledger.  Unknown labels must never become a workflow lock.
func resolveTeamWorkItemDependencyFacts(items []models.TeamWorkItem, assignmentID string) teamDependencyFacts {
	facts := teamDependencyFacts{}
	assignmentID = strings.TrimSpace(assignmentID)
	if assignmentID == "" {
		return facts
	}
	latest := make(map[string]models.TeamWorkItem, len(items))
	for idx := range items {
		item := items[idx]
		businessID := workItemBusinessID(item)
		if businessID == "" {
			continue
		}
		if current, ok := latest[businessID]; !ok || teamMaxInt(item.Revision, 1) >= teamMaxInt(current.Revision, 1) {
			latest[businessID] = item
		}
	}
	target, ok := latest[assignmentID]
	if !ok {
		return facts
	}
	for _, dependency := range teamWorkItemDependencies(target) {
		dependency = strings.TrimSpace(dependency)
		if dependency == "" {
			continue
		}
		item, known := latest[dependency]
		if !known {
			facts.Unknown = append(facts.Unknown, dependency)
			continue
		}
		if item.SupersededBy == nil && item.Status == models.TeamTaskStatusSucceeded {
			facts.Ready = append(facts.Ready, dependency)
		} else {
			facts.Waiting = append(facts.Waiting, dependency)
		}
	}
	sort.Strings(facts.Ready)
	sort.Strings(facts.Waiting)
	sort.Strings(facts.Unknown)
	facts.Ready = uniqueTeamStrings(facts.Ready)
	facts.Waiting = uniqueTeamStrings(facts.Waiting)
	facts.Unknown = uniqueTeamStrings(facts.Unknown)
	return facts
}

func unresolvedTeamWorkItemDependencies(items []models.TeamWorkItem, assignmentID string) []string {
	return resolveTeamWorkItemDependencyFacts(items, assignmentID).Waiting
}

func dependencyBlockedAssignmentsReadyAfter(items []models.TeamWorkItem, completedAssignmentID string) []string {
	completedAssignmentID = strings.TrimSpace(completedAssignmentID)
	latest := make(map[string]models.TeamWorkItem, len(items))
	for idx := range items {
		item := items[idx]
		businessID := workItemBusinessID(item)
		if businessID == "" {
			continue
		}
		if current, ok := latest[businessID]; !ok || teamMaxInt(item.Revision, 1) >= teamMaxInt(current.Revision, 1) {
			latest[businessID] = item
		}
	}
	if completed, ok := latest[completedAssignmentID]; ok {
		completed.Status = models.TeamTaskStatusSucceeded
		completed.SupersededBy = nil
		latest[completedAssignmentID] = completed
	}
	ready := make([]string, 0)
	for businessID, item := range latest {
		if item.ResultJSON == nil || strings.TrimSpace(*item.ResultJSON) == "" {
			continue
		}
		result := map[string]interface{}{}
		if json.Unmarshal([]byte(*item.ResultJSON), &result) != nil ||
			(!eventBool(result, "dependencyBlocked", "dependency_blocked") && !eventBool(result, "provisionalAssignmentResult", "provisional_assignment_result")) {
			continue
		}
		dependencies := teamWorkItemDependencies(item)
		if len(dependencies) == 0 {
			continue
		}
		allSucceeded := true
		for _, dependency := range dependencies {
			prerequisite, ok := latest[dependency]
			if !ok || prerequisite.SupersededBy != nil || prerequisite.Status != models.TeamTaskStatusSucceeded {
				allSucceeded = false
				break
			}
		}
		if allSucceeded {
			ready = append(ready, businessID)
		}
	}
	sort.Strings(ready)
	return uniqueTeamStrings(ready)
}

func (s *teamService) dispatchDependencyReadyReminder(team *models.Team, bus *redisBus, task *models.TeamTask, item *models.TeamWorkItem, owner *models.TeamMember, now time.Time) error {
	if s == nil || s.repo == nil || team == nil || task == nil || item == nil || owner == nil {
		return nil
	}
	dependencies := teamWorkItemDependencies(*item)
	if len(dependencies) == 0 {
		return nil
	}
	items, err := s.repo.ListWorkItemsByRootTaskID(task.ID)
	if err != nil {
		return err
	}
	latest := make(map[string]models.TeamWorkItem, len(items))
	for idx := range items {
		businessID := workItemBusinessID(items[idx])
		if businessID == "" {
			continue
		}
		if current, ok := latest[businessID]; !ok || teamMaxInt(items[idx].Revision, 1) >= teamMaxInt(current.Revision, 1) {
			latest[businessID] = items[idx]
		}
	}
	dependencyFacts := make([]string, 0, len(dependencies))
	for _, dependency := range dependencies {
		candidate, known := latest[dependency]
		if !known || candidate.SupersededBy != nil || candidate.Status != models.TeamTaskStatusSucceeded {
			// Ambiguous or unfinished dependency facts remain advisory. Never
			// manufacture a readiness signal that could restart the wrong work.
			return nil
		}
		dependencyFacts = append(dependencyFacts, fmt.Sprintf("%s:r%d", dependency, teamMaxInt(candidate.Revision, 1)))
	}
	sort.Strings(dependencyFacts)
	assignmentID := workItemBusinessID(*item)
	if assignmentID == "" {
		assignmentID = item.WorkID
	}
	seed := strings.Join([]string{
		strconv.Itoa(team.ID), strconv.Itoa(task.ID), assignmentID,
		strconv.Itoa(teamMaxInt(item.Revision, 1)), strings.Join(dependencyFacts, ","),
	}, "|")
	digest := sha256.Sum256([]byte(seed))
	eventID := fmt.Sprintf("dependency-ready:%d:%d:%x", team.ID, task.ID, digest[: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)
	prompt := fmt.Sprintf(
		"[DEPENDENCY_READY] ClawManager observed that the exact prerequisite assignments for your current work are now succeeded: %s. Re-check the current assignment %s revision %d and continue the same attempt from its existing evidence. This is a context reminder only: do not create a new revision or repeat work that is already progressing. If the deliverable is ready, submit its normal completion; if another real blocker remains, report that blocker to the Leader.",
		strings.Join(dependencyFacts, ", "), assignmentID, teamMaxInt(item.Revision, 1),
	)
	payload := map[string]interface{}{
		"event":            "dependency_ready_reminder",
		"type":             "dependency_ready_reminder",
		"eventKind":        "dependency_ready_reminder",
		"source":           "clawmanager_monitor",
		"nonAuthoritative": true,
		"stateEffect":      "none",
		"rootTaskTerminal": false,
		"visibleToChat":    false,
		"rootTaskId":       rootTaskRef,
		"rootMessageId":    task.MessageID,
		"assignmentId":     assignmentID,
		"workId":           item.WorkID,
		"workItemId":       item.ID,
		"revision":         teamMaxInt(item.Revision, 1),
		"memberId":         owner.MemberKey,
		"dependencies":     dependencies,
		"dependencyFacts":  dependencyFacts,
		"messageId":        eventID,
		"summary":          "Declared prerequisite assignments are ready; continue the same assignment attempt.",
	}
	payloadJSON, err := marshalOptionalJSON(payload)
	if err != nil {
		return err
	}
	envelope := map[string]interface{}{
		"v":                  1,
		"protocolVersion":    3,
		"messageId":          eventID,
		"teamId":             strconv.Itoa(team.ID),
		"from":               "clawmanager-monitor",
		"to":                 owner.MemberKey,
		"replyTo":            teamTaskReplyTarget,
		"requiresCompletion": false,
		"completionTool":     teamTaskCompletionTool,
		"intent":             "dependency_ready",
		"taskId":             rootTaskRef,
		"rootTaskId":         rootTaskRef,
		"rootMessageId":      task.MessageID,
		"workId":             item.WorkID,
		"assignmentId":       assignmentID,
		"revision":           teamMaxInt(item.Revision, 1),
		"workItemId":         item.ID,
		"title":              "Assignment dependencies ready",
		"prompt":             prompt,
		"rawPrompt":          prompt,
		"metadata":           payload,
		"createdAt":          now.Format(time.RFC3339Nano),
	}
	applyTeamTaskEnvelopeContext(envelope, task, owner.MemberKey)
	envelopeJSON, err := marshalJSON(envelope)
	if err != nil {
		return err
	}
	event := &models.TeamEvent{
		TeamID: team.ID, TaskID: &task.ID, MemberID: &owner.ID, MessageID: &eventID,
		EventID: &eventID, EventType: "dependency_ready_reminder", PayloadJSON: payloadJSON,
		OccurredAt: &now, CreatedAt: now,
	}
	outbox := &models.TeamEventOutbox{
		TeamID: team.ID, SourceEventID: eventID, Destination: teamInboxKey(team.ID, owner.MemberKey),
		MessageID: eventID, PayloadJSON: envelopeJSON, Status: "pending", AvailableAt: now,
		CreatedAt: now, UpdatedAt: now,
	}
	if err := s.repo.CreateEventWithOutbox(event, outbox); err != nil {
		if errors.Is(err, repository.ErrDuplicateTeamEvent) {
			return nil
		}
		return err
	}
	if bus == nil || outbox.ID <= 0 {
		return nil
	}
	if err := s.deliverTeamEventOutbox(team, bus, outbox); err != nil {
		_ = s.repo.MarkEventOutboxFailed(outbox.ID, now.Add(teamOutboxRetryDelay(outbox.Attempts)), err.Error())
		return nil
	}
	return s.repo.MarkEventOutboxDelivered(outbox.ID, time.Now().UTC())
}

// dependencyReadyAssignmentsAfter returns only current, non-terminal attempts
// whose exact dependency identities all become succeeded after the supplied
// assignment completes. It does not infer dependencies from titles or prose,
// and it never changes the downstream attempt's state or revision.
func dependencyReadyAssignmentsAfter(items []models.TeamWorkItem, completedAssignmentID string) []string {
	completedAssignmentID = strings.TrimSpace(completedAssignmentID)
	if completedAssignmentID == "" {
		return nil
	}
	latest := make(map[string]models.TeamWorkItem, len(items))
	for idx := range items {
		item := items[idx]
		businessID := workItemBusinessID(item)
		if businessID == "" {
			continue
		}
		if current, ok := latest[businessID]; !ok || teamMaxInt(item.Revision, 1) >= teamMaxInt(current.Revision, 1) {
			latest[businessID] = item
		}
	}
	completed, ok := latest[completedAssignmentID]
	if !ok {
		return nil
	}
	completed.Status = models.TeamTaskStatusSucceeded
	completed.SupersededBy = nil
	latest[completedAssignmentID] = completed
	ready := make([]string, 0)
	for businessID, item := range latest {
		if item.SupersededBy != nil || !isActiveTeamWorkItemStatus(item.Status) {
			continue
		}
		dependencies := teamWorkItemDependencies(item)
		if len(dependencies) == 0 {
			continue
		}
		containsCompleted := false
		allSucceeded := true
		for _, dependency := range dependencies {
			if dependency == completedAssignmentID {
				containsCompleted = true
			}
			prerequisite, known := latest[dependency]
			if !known || prerequisite.SupersededBy != nil || prerequisite.Status != models.TeamTaskStatusSucceeded {
				allSucceeded = false
				break
			}
		}
		if containsCompleted && allSucceeded {
			ready = append(ready, businessID)
		}
	}
	sort.Strings(ready)
	return uniqueTeamStrings(ready)
}

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
}

func phaseHasRequiredWork(phaseID string, items map[string]models.TeamWorkItem, leaderID int) bool {
	for _, 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 {
			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. Legacy plans may
// retire it when the Leader seals the workflow; explicit-v1 plans also require
// a structured phase disposition with a reason.
func (s *teamService) reconcileTeamWorkflowLedger(task *models.TeamTask, workflowFinal bool, now time.Time) (bool, error) {
	return s.reconcileTeamWorkflowLedgerWithDispositions(task, workflowFinal, nil, now)
}

func (s *teamService) reconcileTeamWorkflowLedgerWithDispositions(
	task *models.TeamTask,
	workflowFinal bool,
	dispositions map[string]teamPhaseDisposition,
	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 && phaseUsesExplicitDisposition(phase):
			disposition := dispositions[phase.PhaseID]
			switch disposition.Decision {
			case "cancelled", "skipped":
				phase.Status = teamPhaseStatusCancelled
			case "superseded":
				phase.Status = teamPhaseStatusSuperseded
			default:
				anyDecision = true
				if currentPhase == "" {
					currentPhase = phase.PhaseID
				}
			}
		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 = &currentPhase
		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)(((完成|结束|汇总|done|complete).{0,12}(第一阶段|阶段一|phase\s*1)|(第一阶段|阶段一|phase\s*1).{0,18}(完成|结束|done|complete)).{0,30}((接下来|下一步|随后|然后|仍需|还需|将|即将|准备|继续|进入|开始|next|then|will|need\s+to|continue\s+to|proceed\s+to).{0,24}(第二阶段|阶段二|phase\s*2|评审|验证|review|verify)|(第二阶段|阶段二|phase\s*2).{0,18}(尚未|未开始|待|仍需|还需|将|即将|准备|will|need\s+to)))`)},
	}
	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
	}
	for candidate := range delivered {
		if teamMemberRouteEquivalent(required, candidate) {
			return true
		}
	}
	return false
}

func teamMemberRouteEquivalent(a, b string) bool {
	left := normalizeTeamMemberRouteKey(a)
	right := normalizeTeamMemberRouteKey(b)
	if left == "" || right == "" {
		return false
	}
	if left == right {
		return true
	}
	aliasGroups := [][]string{
		{"pm", "product-manager", "product"},
		{"designer", "ui-designer", "ui-ux-designer", "ux-designer"},
		{"architect", "solution-architect", "software-architect"},
		{"developer", "worker", "senior-developer", "frontend-developer", "backend-developer"},
	}
	for _, group := range aliasGroups {
		leftMatch := false
		rightMatch := false
		for _, alias := range group {
			if left == alias || strings.Contains(left, "-"+alias) || strings.Contains(left, alias+"-") {
				leftMatch = true
			}
			if right == alias || strings.Contains(right, "-"+alias) || strings.Contains(right, alias+"-") {
				rightMatch = true
			}
		}
		if leftMatch && rightMatch {
			return true
		}
	}
	return false
}

func markLeaderMediatedPrematureCompletion(eventType string, payload map[string]interface{}) string {
	if eventString(payload, "originalEvent") == "" {
		payload["originalEvent"] = eventType
	}
	payload["event"] = "reply"
	payload["type"] = "reply"
	payload["status"] = models.TeamTaskStatusRunning
	payload["runtimeStatus"] = models.TeamTaskStatusRunning
	payload["availability"] = models.TeamMemberAvailabilityBusy
	payload["rootTaskTerminal"] = false
	payload["leaderPrematureCompletion"] = true
	if eventString(payload, "summary") == "" {
		payload["summary"] = "Leader is waiting for assigned member results before final synthesis."
	}
	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
	payload["completionEvaluationVersion"] = teamCompletionEvaluationVersion
	// A deferred completion is business information, but its delivery draft is
	// not a final result.  Showing that full draft in the group chat makes a
	// still-running task look complete. Preserve an explicit diagnostic only;
	// the complete result remains visible when a completion is accepted.
	if draft := eventString(payload, "resultMarkdown", "result_markdown", "result", "answer"); draft != "" {
		// Keep the full report privately on the deferred proposal so the
		// reconciler can accept the same current report after the ledger catches
		// up.  Do not leave it in the normal result fields: the chat must not
		// render a deferred draft as a final delivery.
		payload["completionDraftMarkdown"] = draft
		if summary := eventString(payload, "summary"); summary != "" {
			payload["completionDraftSummary"] = summary
		}
		payload["completionDraftStored"] = true
		payload["completionDraftLength"] = len(draft)
		delete(payload, "resultMarkdown")
		delete(payload, "result_markdown")
		delete(payload, "result")
		delete(payload, "answer")
		if step, ok := payload["collaborationStep"].(map[string]interface{}); ok {
			// Some Runtime versions repeated the complete delivery inside the
			// nested collaboration step. It is the same private draft, not a
			// second red chat message.
			for _, key := range []string{"content", "detail", "resultMarkdown", "result_markdown", "result", "answer"} {
				if strings.TrimSpace(eventString(step, key)) == strings.TrimSpace(draft) {
					delete(step, key)
				}
			}
		}
	}
	if reason := completionDecisionMessage(evaluation); reason != "" {
		payload["summary"] = reason
	}
	if eventBool(payload, "automaticTurnResult", "automatic_turn_result") ||
		eventBool(payload, "completionReconcile", "completion_reconcile") {
		// Runtime-generated final-turn proposals are control-plane evidence.
		// A rejected/deferred proposal must not impersonate the Leader in chat;
		// the underlying assistant narrative remains governed by the existing
		// narrative policy and is intentionally not changed here.
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
		payload["chatPolicy"] = "hidden"
		payload["chatKind"] = "runtime_completion_" + evaluation.Decision
		payload["nonAuthoritative"] = true
	} else {
		payload["visibleToChat"] = true
		payload["visible_to_chat"] = true
		payload["chatPolicy"] = "warning"
		payload["chatKind"] = "completion_" + evaluation.Decision
	}
	if completionID := eventString(payload, "completionId", "completion_id"); completionID != "" {
		// One proposal remains one chat item while its blockers evolve.
		payload["displayKey"] = fmt.Sprintf("completion:%s", completionID)
	}
	return decisionType
}

func completionDecisionMessage(evaluation teamCompletionEvaluation) string {
	if len(evaluation.PendingAssignments) > 0 {
		return "最终交付暂未接受：仍等待 " + strings.Join(evaluation.PendingAssignments, "、") + "。"
	}
	if len(evaluation.PendingPhases) > 0 {
		return "最终交付暂未接受：仍有未完成阶段 " + strings.Join(evaluation.PendingPhases, "、") + "。"
	}
	switch evaluation.Reason {
	case "stale_ledger":
		return "最终交付暂未接受：任务账本已更新，系统会按最新状态重新核对。"
	case "missing_artifacts":
		return "最终交付暂未接受：引用的团队产物尚不可读取。"
	case "workflow_not_sealed":
		return "最终交付暂未接受：Leader 仍可继续派发或封闭工作流。"
	case "strong_narrative_contradiction":
		return "最终交付需要 Leader 确认：报告包含明确的后续动作。"
	case "invalid_completion_envelope":
		return "最终交付未被接受：完成请求缺少必要的结构化上下文。"
	}
	return "最终交付暂未接受，ClawManager 正在等待可验证的工作流状态。"
}

func normalizeTeamRoleEventPayload(payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask, rootCompletion bool) {
	if payload == nil || !isLeaderTeamMember(member) {
		return
	}
	reportedKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind")))
	semanticKind := strings.ToLower(strings.TrimSpace(eventString(payload, "semanticEventKind", "semantic_event_kind")))
	if semanticKind == "" {
		semanticKind = reportedKind
	}
	switch semanticKind {
	case "worker_plan":
		semanticKind = "leader_plan"
	case "worker_progress":
		semanticKind = "leader_progress"
	}
	if semanticKind != reportedKind {
		payload["reportedEventKind"] = reportedKind
		payload["semanticEventKind"] = semanticKind
		payload["eventKind"] = semanticKind
	}
	finalArtifact := semanticKind == "artifact_changed" &&
		strings.EqualFold(eventString(payload, "artifactKind", "artifact_kind"), "final") &&
		strings.EqualFold(eventString(payload, "artifactScope", "artifact_scope"), "team")
	if semanticKind == "leader_synthesis" || rootCompletion || finalArtifact {
		const finalWorkID = "leader-final-synthesis"
		inheritedWorkID := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
		if inheritedWorkID != "" && inheritedWorkID != finalWorkID && eventString(payload, "sourceWorkId", "source_work_id") == "" {
			payload["sourceWorkId"] = inheritedWorkID
		}
		payload["assignmentId"] = finalWorkID
		payload["canonicalWorkId"] = finalWorkID
		payload["workId"] = finalWorkID
		if rootCompletion || finalArtifact || eventString(payload, "phaseId", "phase_id") == "" || inheritedWorkID != "" && inheritedWorkID != finalWorkID {
			payload["phaseId"] = "phase-final-synthesis"
			payload["currentPhaseId"] = "phase-final-synthesis"
		}
	}
	if task != nil {
		payload["actorRole"] = "leader"
	}
}

func normalizeTrustedTeamChatOrder(payload map[string]interface{}, task *models.TeamTask, now time.Time) {
	if payload == nil || task == nil {
		return
	}
	eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind")))
	if eventKind != "agent_narrative" {
		return
	}
	raw := eventString(payload, "sourceOccurredAt", "source_occurred_at")
	if raw == "" {
		return
	}
	sourceTime, err := time.Parse(time.RFC3339Nano, raw)
	if err != nil {
		return
	}
	sourceTime = sourceTime.UTC()
	lowerBound := task.CreatedAt.UTC().Add(-5 * time.Minute)
	if task.CreatedAt.IsZero() {
		lowerBound = now.UTC().Add(-24 * time.Hour)
	}
	upperBound := now.UTC().Add(2 * time.Minute)
	if sourceTime.Before(lowerBound) || sourceTime.After(upperBound) {
		return
	}
	for _, key := range []string{"rootTaskId", "root_task_id", "taskId", "task_id"} {
		ref := eventString(payload, key)
		if ref == "" {
			continue
		}
		if parsed := parseClawManagerTeamTaskRef(task.TeamID, ref); parsed != 0 && parsed != task.ID {
			return
		}
	}
	payload["chatOrderAt"] = sourceTime.Format(time.RFC3339Nano)
	payload["chatOrderTrusted"] = true
}

func isTrustedLateTeamNarrative(payload map[string]interface{}) bool {
	return strings.EqualFold(eventString(payload, "eventKind", "event_kind", "kind"), "agent_narrative") &&
		eventBool(payload, "lateProjection", "late_projection") &&
		eventBool(payload, "chatOrderTrusted", "chat_order_trusted") &&
		eventBool(payload, "nonAuthoritative", "non_authoritative") &&
		strings.EqualFold(eventString(payload, "stateEffect", "state_effect"), "none")
}

func isStructurallySuppressedTeamNarrative(payload map[string]interface{}) bool {
	if payload == nil {
		return false
	}
	eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind")))
	return eventKind == "agent_narrative" &&
		eventBool(payload, "suppressedAfterTerminal", "suppressed_after_terminal") &&
		eventBool(payload, "lateProjection", "late_projection") &&
		!eventBool(payload, "terminalDelivery", "terminal_delivery") &&
		eventBool(payload, "nonAuthoritative", "non_authoritative") &&
		strings.EqualFold(eventString(payload, "stateEffect", "state_effect"), "none")
}

func applyTeamChatPolicy(eventType string, payload map[string]interface{}, task *models.TeamTask, member *models.TeamMember) {
	if payload == nil {
		return
	}
	automaticTurnResult := eventBool(payload, "automaticTurnResult", "automatic_turn_result")
	eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind")))
	// Agent-session prose is internal execution telemetry. Plans, explicit
	// progress, handoffs, reviews, blockers, and completions use their own
	// business event kinds and remain visible; raw narratives never enter chat.
	if eventKind == "agent_narrative" {
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
		return
	}
	if isStructurallySuppressedTeamNarrative(payload) {
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
		return
	}
	if automaticTurnResult {
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
		return
	}
	if eventKind == "turn_finished_without_completion" || eventKind == "assignment_attempt_failed" {
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
		return
	}
	if eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
		return
	}
	normalizedEvent := strings.ToLower(strings.TrimSpace(eventType))
	// 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 markAutomaticCompletionDiagnosticStateNeutral(payload map[string]interface{}) bool {
	if payload == nil || !eventBool(payload, "automaticTurnResult", "automatic_turn_result") {
		return false
	}
	payload["stateEffect"] = "none"
	payload["nonAuthoritative"] = true
	payload["rootTaskTerminal"] = false
	payload["chatPolicy"] = "hidden"
	payload["visibleToChat"] = false
	payload["visible_to_chat"] = false
	return true
}

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 || isStructurallySuppressedTeamNarrative(payload) || teamChatEventIsTransportNoise(eventType, eventKind, payload) {
		return false
	}
	switch eventKind {
	case "leader_plan", "leader_progress", "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",
		"member_result_updated", "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", "member_result_updated", "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) publishTeamRootWorkflowState(bus *redisBus, task *models.TeamTask) {
	if bus == nil || task == nil || task.ID <= 0 {
		return
	}
	rootTaskID := fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID)
	state := map[string]interface{}{
		"teamId":                   task.TeamID,
		"rootTaskId":               rootTaskID,
		"status":                   task.Status,
		"workflowState":            task.WorkflowState,
		"planVersion":              task.PlanVersion,
		"ledgerVersion":            task.LedgerVersion,
		"currentPhaseId":           task.CurrentPhaseID,
		"terminal":                 isTerminalTeamTaskStatus(task.Status),
		"assignmentLedgerComplete": false,
		"snapshotSchemaVersion":    2,
		"updatedAt":                time.Now().UTC().Format(time.RFC3339Nano),
	}
	// Runtime uses this compact, advisory snapshot only for dependency-aware
	// dispatch and in-flight deduplication. Database Work Items remain the source
	// of truth; a missing snapshot must stay fail-open for mixed-version pairs.
	if s != nil && s.repo != nil {
		if items, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID); listErr == nil {
			state["assignmentLedgerComplete"] = true
			memberKeys := map[int]string{}
			if members, memberErr := s.repo.ListMembersByTeamID(task.TeamID); memberErr == nil {
				for idx := range members {
					memberKeys[members[idx].ID] = members[idx].MemberKey
				}
			}
			latest := map[string]models.TeamWorkItem{}
			for idx := range items {
				item := items[idx]
				businessID := workItemBusinessID(item)
				if businessID == "" || item.SupersededBy != nil {
					continue
				}
				if current, ok := latest[businessID]; !ok || teamMaxInt(item.Revision, 1) >= teamMaxInt(current.Revision, 1) {
					latest[businessID] = item
				}
			}
			assignments := make(map[string]interface{}, len(latest))
			for businessID, item := range latest {
				currentRevision := teamMaxInt(item.Revision, 1)
				nextRevisionAllowed := teamAssignmentRevisionRecoveryAllowed(items, businessID)
				assignments[businessID] = map[string]interface{}{
					"workItemId":                   item.ID,
					"workId":                       item.WorkID,
					"assignmentId":                 businessID,
					"revision":                     currentRevision,
					"nextRevisionAllowed":          nextRevisionAllowed,
					"nextRevision":                 currentRevision + 1,
					"status":                       item.Status,
					"ownerMemberId":                item.OwnerMemberID,
					"ownerMemberKey":               memberKeys[derefTeamInt(item.OwnerMemberID)],
					"phaseId":                      derefTeamString(item.PhaseID),
					"requiredForRoot":              item.RequiredForRoot,
					"reviewRequired":               item.ReviewRequired,
					"hasAcceptedResult":            workItemHasAcceptedResultReceipt(item),
					"dependsOn":                    teamWorkItemDependencies(item),
					"validationTargetAssignmentId": derefTeamString(item.ReviewTargetAssignmentID),
					"validationTargetRevision":     derefTeamInt(item.ReviewTargetRevision),
					"updatedAt":                    item.UpdatedAt.Format(time.RFC3339Nano),
				}
			}
			state["assignments"] = assignments
		}
	}
	encoded, err := json.Marshal(state)
	if err != nil {
		return
	}
	// This key is an optimization used only to suppress stale reminders. Redis
	// or old-runtime incompatibility must never block the business workflow.
	_ = bus.Set(context.Background(), teamRootWorkflowStateKey(task.TeamID, rootTaskID), string(encoded), 7*24*time.Hour)
}

// teamAssignmentRevisionRecoveryAllowed derives revision authority exclusively
// from persisted business facts. Runtime hints and Agent-authored revision
// numbers are intentionally insufficient on their own.
func teamAssignmentRevisionRecoveryAllowed(items []models.TeamWorkItem, businessID string) bool {
	businessID = strings.TrimSpace(businessID)
	if businessID == "" {
		return false
	}
	var currentTarget *models.TeamWorkItem
	for idx := range items {
		item := items[idx]
		if item.SupersededBy != nil || workItemBusinessID(item) != businessID {
			continue
		}
		if currentTarget == nil || teamMaxInt(item.Revision, 1) > teamMaxInt(currentTarget.Revision, 1) ||
			(teamMaxInt(item.Revision, 1) == teamMaxInt(currentTarget.Revision, 1) && item.UpdatedAt.After(currentTarget.UpdatedAt)) {
			clone := item
			currentTarget = &clone
		}
	}
	if currentTarget == nil {
		return false
	}
	currentRevision := teamMaxInt(currentTarget.Revision, 1)
	if currentTarget.Status == models.TeamTaskStatusStale {
		return true
	}
	if currentTarget.Status == models.TeamTaskStatusFailed && workItemHasAcceptedFailureReceipt(*currentTarget) {
		return true
	}
	if currentTarget.ValidatedRevision != nil && *currentTarget.ValidatedRevision >= currentRevision {
		return false
	}

	var latestValidation *models.TeamWorkItem
	for idx := range items {
		item := items[idx]
		if item.SupersededBy != nil || strings.TrimSpace(derefTeamString(item.ReviewTargetAssignmentID)) != businessID {
			continue
		}
		if item.ReviewTargetRevision != nil && *item.ReviewTargetRevision != currentRevision {
			continue
		}
		if latestValidation == nil || item.UpdatedAt.After(latestValidation.UpdatedAt) ||
			(item.UpdatedAt.Equal(latestValidation.UpdatedAt) && item.ID > latestValidation.ID) {
			clone := item
			latestValidation = &clone
		}
	}
	if latestValidation == nil {
		return false
	}
	resultPayload := workItemResultPayload(*latestValidation)
	verdict := strings.ToLower(strings.TrimSpace(eventString(
		resultPayload,
		"reviewVerdict", "review_verdict", "validationVerdict", "validation_verdict", "verdict",
	)))
	switch verdict {
	case "fail", "failed", "reject", "rejected":
		return true
	default:
		return false
	}
}

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) != "" {
		_ = json.Unmarshal([]byte(*event.PayloadJSON), &payload)
	}
	return payload
}

func teamEventMatchesRootTask(event models.TeamEvent, payload map[string]interface{}, task *models.TeamTask) bool {
	if task == nil {
		return false
	}
	if event.TaskID != nil && *event.TaskID == task.ID {
		return true
	}
	for _, key := range []string{"taskId", "task_id", "rootTaskId", "root_task_id", "parentTaskId", "parent_task_id", "currentTaskId", "current_task_id", "runtimeTaskId", "runtime_task_id"} {
		if parseClawManagerTeamTaskRef(task.TeamID, eventString(payload, key)) == task.ID {
			return true
		}
	}
	for _, key := range []string{"messageId", "message_id", "rootMessageId", "root_message_id", "parentMessageId", "parent_message_id", "inReplyTo", "in_reply_to", "replyTo", "reply_to"} {
		if task.MessageID != "" && eventString(payload, key) == task.MessageID {
			return true
		}
	}
	if step, ok := payload["collaborationStep"].(map[string]interface{}); ok {
		return teamEventMatchesRootTask(event, step, task)
	}
	return false
}

func normalizeTeamMemberRouteKey(value string) string {
	return strings.ToLower(strings.TrimSpace(value))
}

func isLeaderTeamMember(member *models.TeamMember) bool {
	if member == nil {
		return false
	}
	normalizedKey := strings.ToLower(strings.TrimSpace(member.MemberKey))
	normalizedRole := strings.ToLower(strings.TrimSpace(member.Role))
	return normalizedKey == "leader" || strings.Contains(normalizedKey, "leader") ||
		normalizedRole == "leader" || strings.Contains(normalizedRole, "leader")
}

func isLeaderMediatedTeam(team *models.Team) bool {
	return team != nil && normalizedTeamCommunicationMode(team.CommunicationMode) == teamCommunicationModeLeaderMediated
}

func leaderMediatedRouteTarget(payload map[string]interface{}) string {
	target := eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id")
	if target != "" {
		return target
	}
	return eventString(payload, "assignee", "owner", "targetMember")
}

func isLeaderRouteTarget(target string) bool {
	normalized := strings.ToLower(strings.TrimSpace(target))
	if normalized == "" {
		return false
	}
	return normalized == "leader" ||
		normalized == teamTaskReplyTarget ||
		strings.Contains(normalized, "leader") ||
		strings.Contains(normalized, "clawmanager")
}

func isLeaderMediatedOutboundLikeEvent(eventType string) bool {
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "outbound", "task_assigned", "team_send", "reply", "task_completed", "completion", "completion_proposed":
		return true
	default:
		return false
	}
}

func isStructuredAgentNarrativeEvent(eventType string, payload map[string]interface{}) bool {
	eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind")))
	messageKind := strings.ToLower(strings.TrimSpace(eventString(payload, "messageKind", "message_kind")))
	if eventKind == "agent_narrative" || strings.HasPrefix(eventKind, "agent_") {
		return true
	}
	return strings.EqualFold(strings.TrimSpace(eventType), "reply") &&
		(messageKind == "narrative" || eventBool(payload, "nonAuthoritative", "non_authoritative") && strings.EqualFold(eventString(payload, "stateEffect", "state_effect"), "none"))
}

func isControlPlaneTurnMessage(payload map[string]interface{}) bool {
	messageID := strings.ToLower(strings.TrimSpace(eventString(
		payload,
		"sourceMessageId", "source_message_id", "messageId", "message_id",
	)))
	for _, prefix := range []string{
		"monitor:",
		"leader-workflow-reminder:",
		"leader-completion-recovery:",
		"leader-recovery:",
	} {
		if strings.HasPrefix(messageID, prefix) {
			return true
		}
	}
	intent := strings.ToLower(strings.TrimSpace(eventString(payload, "intent", "monitorType", "monitor_type")))
	return strings.Contains(intent, "monitor") ||
		strings.Contains(intent, "status_check") ||
		strings.Contains(intent, "recovery") ||
		strings.Contains(intent, "reminder")
}

func isTrustedRuntimeTurnResultSignal(eventType string, payload map[string]interface{}) bool {
	// Runtime turn-end records are Monitor evidence only. They are deliberately
	// never promoted into assignment or root completion.
	return false
}

func runtimeTurnResultText(payload map[string]interface{}) string {
	return strings.TrimSpace(eventString(
		payload,
		"resultMarkdown", "result_markdown", "result", "answer", "finalText", "final_text", "text", "content",
	))
}

// promoteRuntimeTurnResultCandidate converts a Runtime-authored end-of-turn
// signal into completion evidence. Protocol v4 carries the result directly.
// Protocol v3 is supported by binding it to the exact narrative emitted for
// the same dispatched message. Arbitrary historical replies are never scanned.
func (s *teamService) promoteRuntimeTurnResultCandidate(
	team *models.Team,
	task *models.TeamTask,
	member *models.TeamMember,
	eventType string,
	payload map[string]interface{},
) (string, error) {
	if s == nil || s.repo == nil || team == nil || task == nil || member == nil ||
		!isTrustedRuntimeTurnResultSignal(eventType, payload) {
		return eventType, nil
	}
	resultText := runtimeTurnResultText(payload)
	contentHash := strings.TrimSpace(eventString(payload, "contentHash", "content_hash"))
	sourceMessageID := strings.TrimSpace(eventString(payload, "sourceMessageId", "source_message_id", "messageId", "message_id"))
	if resultText == "" {
		events, err := s.repo.ListEventsByTeamID(team.ID, 200)
		if err != nil {
			return eventType, err
		}
		for idx := range events {
			candidate := events[idx]
			if candidate.TaskID == nil || *candidate.TaskID != task.ID ||
				candidate.MemberID == nil || *candidate.MemberID != member.ID {
				continue
			}
			if candidate.PayloadJSON == nil {
				continue
			}
			candidatePayload := map[string]interface{}{}
			if err := json.Unmarshal([]byte(*candidate.PayloadJSON), &candidatePayload); err != nil ||
				!isStructuredAgentNarrativeEvent(candidate.EventType, candidatePayload) {
				continue
			}
			candidateSourceID := strings.TrimSpace(eventString(
				candidatePayload,
				"sourceMessageId", "source_message_id", "inReplyTo", "in_reply_to",
			))
			if sourceMessageID == "" || candidateSourceID != sourceMessageID {
				continue
			}
			resultText = runtimeTurnResultText(candidatePayload)
			if resultText == "" {
				continue
			}
			contentHash = strings.TrimSpace(eventString(candidatePayload, "contentHash", "content_hash"))
			payload["resultCandidateNarrativeEventId"] = derefTeamString(candidate.EventID)
			break
		}
	}
	if resultText == "" {
		return eventType, nil
	}
	if contentHash == "" {
		sum := sha256.Sum256([]byte(resultText))
		contentHash = hex.EncodeToString(sum[:])
	}
	completionHash := contentHash
	if len(completionHash) < 24 {
		sum := sha256.Sum256([]byte(resultText))
		completionHash = hex.EncodeToString(sum[:])
	}
	summary := strings.TrimSpace(eventString(payload, "resultSummary", "result_summary"))
	if summary == "" {
		for _, line := range strings.Split(resultText, "\n") {
			if line = strings.TrimSpace(line); line != "" {
				summary = line
				break
			}
		}
	}
	payload["originalEvent"] = eventType
	payload["event"] = "task_completed"
	payload["type"] = "task_completed"
	payload["eventKind"] = "turn_result_candidate"
	payload["runtimeTurnResultCandidate"] = true
	payload["normalizedResultSource"] = "runtime_turn_result_candidate"
	payload["completionSource"] = "assistant_turn_result"
	payload["explicitCompletion"] = false
	payload["automaticTurnResult"] = true
	payload["resultMarkdown"] = resultText
	payload["summary"] = summary
	payload["contentHash"] = contentHash
	payload["status"] = models.TeamTaskStatusSucceeded
	payload["runtimeStatus"] = models.TeamTaskStatusSucceeded
	payload["availability"] = models.TeamMemberAvailabilityIdle
	assignmentResultOnly := isLeaderMediatedTeam(team) && member.ID != task.TargetMemberID
	payload["assignmentResultOnly"] = assignmentResultOnly
	payload["rootTaskTerminal"] = !assignmentResultOnly && member.ID == task.TargetMemberID
	payload["completionId"] = fmt.Sprintf(
		"turn-result:%d:%s:%s",
		task.ID,
		normalizeTeamRedisKeyPart(member.MemberKey),
		completionHash[:24],
	)
	return "task_completed", nil
}

// isAuthoritativeTeamAssignmentEvent identifies the control-plane instruction
// that creates a business contract. Assistant narrative and result events may
// describe the same identifiers, but must never rewrite that contract.
func isAuthoritativeTeamAssignmentEvent(eventType string, payload map[string]interface{}) bool {
	if payload == nil ||
		eventBool(payload, "assignmentResultOnly", "assignment_result_only") ||
		isStructuredAgentNarrativeEvent(eventType, payload) ||
		isPassiveAssignmentMonitorEvent(eventType, payload) {
		return false
	}
	if eventBool(payload, "nonAuthoritative", "non_authoritative") {
		return false
	}
	if requiresCompletion, defined := teamEventBoolValue(payload, "requiresCompletion", "requires_completion"); defined && !requiresCompletion {
		return false
	}
	if eventInt(payload, "deliverySemanticsVersion", "delivery_semantics_version") > 0 {
		return strings.EqualFold(eventString(payload, "businessDeliveryKind", "business_delivery_kind"), "assignment") &&
			eventBool(payload, "businessMutation", "business_mutation")
	}
	if eventBool(payload, "leaderDispatchOnly", "leader_dispatch_only") {
		return true
	}
	intent := strings.ToLower(strings.TrimSpace(eventString(payload, "intent", "agentIntent", "agent_intent")))
	switch intent {
	case "context", "context_update", "peer_request", "question", "reminder", "follow_up", "status_check", "assignment_status_check", "notification", "ack":
		return false
	}
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "task_assigned", "outbound", "team_send", "peer_handoff":
		return true
	default:
		return false
	}
}

func isStateNeutralLateAssignmentEvent(eventType string, payload map[string]interface{}) bool {
	if isStructuredAgentNarrativeEvent(eventType, payload) {
		return true
	}
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "task_received", "task_started", "task_progress", "progress", "assignment_heartbeat",
		"assignment_check_requested", "assignment_check_result", "task_failed", "message_failed":
		return !isExplicitTeamTaskCompletion(payload)
	default:
		return false
	}
}

func isLeaderMediatedInvalidWorkerRoute(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember) bool {
	if !isLeaderMediatedTeam(team) || payload == nil || member == nil || isLeaderTeamMember(member) {
		return false
	}
	if !isLeaderMediatedOutboundLikeEvent(eventType) {
		return false
	}
	target := leaderMediatedRouteTarget(payload)
	if target == "" {
		return false
	}
	return !isLeaderRouteTarget(target)
}

func markLeaderMediatedRouteViolation(eventType string, payload map[string]interface{}, member *models.TeamMember) string {
	if eventString(payload, "originalEvent") == "" {
		payload["originalEvent"] = eventType
	}
	target := leaderMediatedRouteTarget(payload)
	payload["event"] = "message_warning"
	payload["type"] = "message_warning"
	payload["status"] = "warning"
	payload["runtimeStatus"] = "warning"
	payload["availability"] = models.TeamMemberAvailabilityIdle
	payload["leaderMediatedRouteViolation"] = true
	payload["nonAuthoritative"] = true
	payload["rootTaskTerminal"] = false
	payload["summary"] = "Leader-mediated mode ignores worker-to-worker/self routing; workers must return assignment results to the Leader."
	if target != "" {
		payload["target"] = target
		payload["to"] = target
	}
	if member != nil {
		payload["from"] = member.MemberKey
	}
	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
		}
	}
	// Monitor/reminder turns are identified by their authenticated envelope
	// identity, never by matching phrases in model prose. This keeps a
	// non-authoritative control-plane reply from closing the root task while
	// preserving genuine explicit Leader failure completion.
	return isControlPlaneTurnMessage(payload) || isPassiveAssignmentMonitorEvent(eventType, payload)
}

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 normalizeWaitingAssignmentObservation(payload map[string]interface{}) {
	if payload == nil || !isWaitingTeamTaskEventStatus(normalizedTeamTaskEventStatus(payload)) {
		return
	}
	payload["status"] = models.TeamTaskStatusWaiting
	payload["runtimeStatus"] = models.TeamTaskStatusWaiting
	payload["availability"] = models.TeamMemberAvailabilityBusy
	payload["rootTaskTerminal"] = false
	payload["assignmentWaiting"] = true
}

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
	}
	if member.ID == task.TargetMemberID {
		return false
	}
	if isNonAuthoritativeDispatchFailure(eventType, payload) || isNonAuthoritativeDispatchWarning(eventType, payload) {
		return false
	}
	if isAuthoritativeAssignmentFailure(eventType, payload) {
		// A non-Leader failure belongs to its authenticated assignment, not to
		// the root task. New runtimes say this explicitly; older runtimes are
		// accepted when the current envelope still carries the assignment id.
		return eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id") != ""
	}
	// New runtimes project assistant prose as an explicit narrative event.
	// Its wording may look final, but only the structured completion tool owns
	// assignment terminal state. Text heuristics remain solely for old runtimes.
	if isStructuredAgentNarrativeEvent(eventType, payload) {
		return false
	}
	if !isLeaderMediatedOutboundLikeEvent(eventType) || !teamEventHasBody(payload) {
		return false
	}
	if isNonAuthoritativeDispatchFailure(eventType, payload) ||
		isDispatchOnlyCompletionPayload(payload) ||
		eventBool(payload, "leaderMediatedRouteViolation") {
		return false
	}
	target := leaderMediatedRouteTarget(payload)
	if target == "" {
		target = eventString(payload, "replyTarget", "reply_target")
	}
	if target != "" && !isLeaderRouteTarget(target) {
		return false
	}
	if teamRedisProtocolVersion(payload) >= 2 && isExplicitTeamTaskCompletion(payload) {
		return true
	}
	if teamRedisProtocolVersion(payload) >= 4 &&
		eventBool(payload, "assignmentResultOnly", "assignment_result_only") &&
		isTrustedNaturalTurnCompletion(eventType, payload) {
		// A structural Runtime turn result is a Worker assignment result. It is
		// never allowed to close the Leader-owned root task, but it must not be
		// discarded merely because the Agent missed the preferred completion tool.
		return true
	}
	if teamRedisProtocolVersion(payload) >= 4 {
		// Protocol v4+ always follows a Worker delivery turn with a structured
		// automatic or explicit completion proposal. Treating the preceding
		// team_send/outbound prose as a second terminal result caused duplicate
		// cards, notifications, and content-hash "updates" for one turn.
		return false
	}
	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) {
	resultFailed := isAuthoritativeAssignmentFailure(eventType, payload)
	payload["assignmentResultOnly"] = true
	payload["memberResultConfirmed"] = true
	payload["rootTaskTerminal"] = false
	if resultFailed {
		payload["status"] = models.TeamTaskStatusFailed
		payload["runtimeStatus"] = models.TeamTaskStatusFailed
		payload["availability"] = models.TeamMemberAvailabilityBlocked
		payload["resultFailed"] = true
	} else {
		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
	}
	if leaderMediatedRouteTarget(payload) == "" {
		payload["to"] = "leader"
		payload["target"] = "leader"
	}
}

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, _ map[string]interface{}, now time.Time) error {
	if s == nil || task == nil || !isLeaderMediatedTeam(team) || !isTerminalTeamTaskStatus(task.Status) {
		return nil
	}
	if task.Status == models.TeamTaskStatusSucceeded || task.WorkflowState == teamWorkflowStateCompleted || task.AcceptedCompletionID != nil {
		return nil
	}
	if task.ResultJSON != nil && strings.TrimSpace(*task.ResultJSON) != "" {
		previous := map[string]interface{}{}
		if json.Unmarshal([]byte(*task.ResultJSON), &previous) == nil &&
			isExplicitTeamTaskCompletion(previous) &&
			eventBool(previous, "rootTaskTerminal", "root_task_terminal") {
			return nil
		}
	}
	// A genuine member result is authoritative evidence that a failed/stale
	// non-final Leader workflow can continue. Reopen from structured task state;
	// never depend on particular Monitor wording or language.
	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)
	if strings.Contains(normalized, "$CLAWMANAGER_TEAM_SHARED_DIR") ||
		(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 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 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+"\u4f60\u597d") || strings.Contains(compact, "@"+target) {
			return true
		}
	}
	if strings.Contains(lower, "please ") &&
		(strings.Contains(lower, "write") || strings.Contains(lower, "complete") || strings.Contains(lower, "return") || strings.Contains(lower, "report back")) &&
		(strings.Contains(lower, "designer") || strings.Contains(lower, "pm") || strings.Contains(lower, "architect") || strings.Contains(lower, "worker")) {
		return true
	}
	return false
}

func looksLikeFinalResultText(text string) bool {
	normalized := strings.TrimSpace(text)
	if normalized == "" {
		return false
	}
	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 ""
	}
	if target := eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id", "assignee", "owner"); target != "" {
		return target
	}
	text := strings.ToLower(eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary"))
	for _, target := range []string{"ui-designer", "designer", "product-manager", "pm", "solution-architect", "architect", "worker"} {
		if strings.Contains(text, target) {
			return target
		}
	}
	return ""
}

func (s *teamService) projectTeamEvent(team *models.Team, bus *redisBus, message redisStreamMessage) error {
	if exists, err := s.repo.EventExistsByStreamID(team.ID, message.ID); err != nil || exists {
		return err
	}
	payload := mergeRedisEventPayload(message.Fields)
	eventID := eventString(payload, "eventId", "event_id")
	if eventID != "" {
		if exists, err := s.repo.EventExistsByEventID(team.ID, eventID); err != nil || exists {
			return err
		}
	}
	eventType := eventString(payload, "event_type", "event", "type")
	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 {
		enriched, err := s.enrichOutboundEventFromInbox(team.ID, bus, payload, messageID)
		if err != nil {
			fmt.Printf("Warning: failed to enrich Team %d outbound event %s from inbox: %v\n", team.ID, messageID, err)
		} else {
			payload = enriched
		}
	}

	var member *models.TeamMember
	if memberKey != "" {
		found, err := s.repo.GetMemberByTeamKey(team.ID, memberKey)
		if err != nil {
			return err
		}
		member = found
	}
	var err error
	eventType, member, err = s.normalizePeerAssistedTeamEvent(team, eventType, payload, member)
	if err != nil {
		return err
	}

	var task *models.TeamTask
	if taskID := eventInt(payload, "task_id", "taskId"); taskID > 0 {
		found, err := s.repo.GetTaskByID(taskID)
		if err != nil {
			return err
		}
		if found != nil && found.TeamID == team.ID {
			task = found
		}
	}
	if task == nil && messageID != "" {
		found, err := s.repo.GetTaskByMessageID(team.ID, messageID)
		if err != nil {
			return err
		}
		task = found
	}
	if task == nil {
		found, err := s.resolveTeamTaskFromEventReferences(team.ID, payload)
		if err != nil {
			return err
		}
		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 {
			return err
		}
		task = found
	}
	if task == nil && shouldAssociateEventWithCurrentMemberTask(eventType, payload) {
		found, err := s.activeTaskFromPeerContext(team.ID, payload, member)
		if err != nil {
			return err
		}
		task = found
	}
	// Completion retries from a compatible Lite runtime may omit only mirrored
	// transport fields (for example after receiving a deferred ACK). Restore
	// those from the resolved Team context before the strict protocol check.
	// This deliberately runs after task/member resolution and before any
	// terminal decision, so it cannot make an unrelated reply terminal.
	hydrateExplicitCompletionEnvelope(payload, team, task, member, message.ID)
	normalizeTrustedTeamChatOrder(payload, task, time.Now().UTC())
	// Root terminal state is an immutable business boundary. Runtime retries can
	// arrive after the accepted event (Team75 emitted a late leader_synthesis and
	// stale completion), but they may not create a new warning, reopen a member,
	// or replace the accepted summary. A completion proposal still receives an
	// ACK so a compatible Runtime can stop retrying.
	if task != nil &&
		isImmutableRootTaskStatus(task.Status) &&
		isPostTerminalMutableTeamEvent(eventType, payload) &&
		!isTrustedLateTeamNarrative(payload) {
		if isCompletionProposal {
			decision := teamCompletionDecisionAccepted
			reason := "root_already_completed"
			if task.Status != models.TeamTaskStatusSucceeded {
				decision = teamCompletionDecisionRejected
				reason = "root_already_terminal"
			}
			if err := s.emitCompletionAcknowledgement(team, bus, task, member, payload, decision, reason); err != nil {
				return err
			}
		}
		return nil
	}
	if task != nil && member != nil {
		eventType, err = s.promoteRuntimeTurnResultCandidate(team, task, member, eventType, payload)
		if err != nil {
			return err
		}
	}
	normalizeUnauthorizedAssignmentCheckResult(payload)
	passiveMonitorEvent := isPassiveAssignmentMonitorEvent(eventType, payload)
	stateNeutralAssignmentEvent := false
	if eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind"))); eventKind == "turn_finished_without_completion" || eventKind == "turn_result_candidate" || eventKind == "assignment_attempt_failed" || eventKind == "runtime_reconciliation_needed" || eventKind == "target_resolution_warning" || (eventBool(payload, "nonAuthoritative", "non_authoritative") && strings.EqualFold(eventString(payload, "stateEffect", "state_effect"), "none")) || eventBool(payload, "automaticTurnResult", "automatic_turn_result") || eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
		stateNeutralAssignmentEvent = true
		payload["stateEffect"] = "none"
		payload["nonAuthoritative"] = true
		payload["rootTaskTerminal"] = false
		if eventKind == "assignment_attempt_failed" {
			payload["retryable"] = true
			payload["status"] = models.TeamTaskStatusRunning
			payload["runtimeStatus"] = "retrying"
			payload["availability"] = models.TeamMemberAvailabilityBusy
		}
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
	}
	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 task != nil && member != nil && isStateNeutralLateAssignmentEvent(eventType, payload) {
		targetsTerminalWorkItem, terminalErr := s.leaderMediatedMonitorTargetsTerminalWorkItem(team, task, member, payload)
		if terminalErr != nil {
			return terminalErr
		}
		if targetsTerminalWorkItem {
			stateNeutralAssignmentEvent = true
			payload["lateAfterAssignmentTerminal"] = true
			payload["stateEffect"] = "none"
			payload["nonAuthoritative"] = true
			payload["rootTaskTerminal"] = false
			if strings.EqualFold(eventType, "task_failed") || strings.EqualFold(eventType, "message_failed") {
				payload["originalEvent"] = eventType
				payload["event"] = "message_warning"
				payload["type"] = "message_warning"
				payload["status"] = "warning"
				payload["runtimeStatus"] = models.TeamTaskStatusSucceeded
				payload["availability"] = models.TeamMemberAvailabilityIdle
				payload["chatPolicy"] = "hidden"
				payload["visibleToChat"] = false
				payload["visible_to_chat"] = false
				eventType = "message_warning"
			}
		}
	}
	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) || isNonAuthoritativeDispatchWarning(eventType, payload) {
		if eventString(payload, "originalEvent") == "" {
			payload["originalEvent"] = eventType
		}
		payload["event"] = "message_warning"
		payload["type"] = "message_warning"
		payload["status"] = "attention_required"
		if eventString(payload, "runtimeStatus", "runtime_status") == "" {
			payload["runtimeStatus"] = models.TeamTaskStatusRunning
		}
		if eventString(payload, "availability") == "" {
			payload["availability"] = models.TeamMemberAvailabilityBusy
		}
		payload["nonAuthoritative"] = true
		payload["stateEffect"] = "none"
		payload["rootTaskTerminal"] = false
		eventType = "message_warning"
	}
	leaderMediatedRouteViolation := isLeaderMediatedInvalidWorkerRoute(team, eventType, payload, member)
	if leaderMediatedRouteViolation {
		eventType = markLeaderMediatedRouteViolation(eventType, payload, member)
	}
	if !isAuthoritativeAssignmentFailure(eventType, payload) {
		normalizeWaitingAssignmentObservation(payload)
	}
	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.hasLeaderMediatedResultConfirmationForAttempt(team.ID, task.ID, member.MemberKey, assignmentID, teamMaxInt(eventInt(payload, "revision"), 1), contentHash)
			if confirmErr != nil {
				return confirmErr
			}
			sameSourceTurn, sourceErr := s.hasLeaderMediatedResultConfirmationForSource(
				team.ID,
				task.ID,
				member.MemberKey,
				assignmentID,
				teamMaxInt(eventInt(payload, "revision"), 1),
				eventString(payload, "sourceMessageId", "source_message_id"),
			)
			if sourceErr != nil {
				return sourceErr
			}
			if alreadyProjected || sameSourceTurn {
				payload["visibleToChat"] = false
				payload["visible_to_chat"] = false
				payload["duplicateResultProjection"] = true
				payload["duplicateResultSourceTurn"] = sameSourceTurn
			}
		}
	}
	if !assignmentResultOnly && !stateNeutralAssignmentEvent && 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
		}
		payload["event"] = "reply"
		payload["type"] = "reply"
		payload["status"] = models.TeamTaskStatusRunning
		payload["runtimeStatus"] = models.TeamTaskStatusRunning
		payload["availability"] = models.TeamMemberAvailabilityBusy
		payload["rootTaskTerminal"] = false
		payload["nonAuthoritativeCompletion"] = true
		eventType = "reply"
		eventStatus = normalizedTeamTaskEventStatus(payload)
		eventSignalsCompletion = false
		eventSignalsFailure = false
	}
	leaderDispatchOnly := false
	if !eventSignalsCompletion && !eventSignalsFailure {
		leaderDispatchOnly = isLeaderMediatedLeaderDispatchOnlyMessage(team, eventType, payload, member, task)
	}
	if leaderDispatchOnly {
		if eventString(payload, "originalEvent") == "" {
			payload["originalEvent"] = eventType
		}
		payload["event"] = "reply"
		payload["type"] = "reply"
		payload["status"] = models.TeamTaskStatusDispatched
		payload["runtimeStatus"] = models.TeamTaskStatusRunning
		payload["availability"] = models.TeamMemberAvailabilityBusy
		payload["rootTaskTerminal"] = false
		payload["leaderDispatchOnly"] = true
		if target := inferLeaderDispatchTarget(payload); target != "" {
			payload["target"] = target
			payload["to"] = target
		}
		eventType = "reply"
		eventStatus = normalizedTeamTaskEventStatus(payload)
		eventSignalsCompletion = false
		eventSignalsFailure = false
	}
	if eventSignalsCompletion && !leaderDispatchOnly && !assignmentResultOnly && !leaderMediatedRouteViolation {
		// 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 {
			validationRepaired, validationErr := s.reconcileCompletedAssignmentValidations(task, time.Now().UTC())
			if validationErr != nil {
				return validationErr
			}
			identityRepaired, identityErr := s.reconcileUnissuedRunningWorkItems(team, task, time.Now().UTC())
			if identityErr != nil {
				return identityErr
			}
			workflowFinal := eventBool(payload, "workflowFinal", "workflow_final", "sealWorkflow", "seal_workflow")
			if eventBool(payload, "automaticTurnResult", "automatic_turn_result") ||
				eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
				workflowFinal = false
			}
			reconciled, reconcileErr := s.reconcileTeamWorkflowLedgerWithDispositions(
				task,
				workflowFinal,
				structuredTeamPhaseDispositions(payload),
				time.Now().UTC(),
			)
			if reconcileErr != nil {
				return reconcileErr
			}
			if reconciled || identityRepaired || validationRepaired {
				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
		}
		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["completionEvaluationVersion"] = teamCompletionEvaluationVersion
			delete(payload, "completionReconcile")
			delete(payload, "completion_reconcile")
			payload["chatKind"] = "final_delivery"
			if eventBool(payload, "runtimeTurnResultCandidate", "runtime_turn_result_candidate") {
				// The exact assistant narrative was already projected for this
				// turn. Keep the accepted completion record private to avoid a
				// duplicate final bubble while retaining it as the root result.
				payload["chatPolicy"] = "hidden"
				payload["visibleToChat"] = false
				payload["visible_to_chat"] = false
				payload["finalDeliveredByNarrative"] = true
			} else {
				payload["chatPolicy"] = "visible"
				payload["visibleToChat"] = true
				payload["visible_to_chat"] = true
			}
			payload["displayKey"] = fmt.Sprintf("root-final:%d", task.ID)
		}
	}
	if markAutomaticCompletionDiagnosticStateNeutral(payload) {
		stateNeutralAssignmentEvent = true
	}
	s.normalizeTeamArtifactReferences(team, payload)
	if eventSignalsCompletion {
		if missing := s.missingTeamArtifactReferences(team, payload); len(missing) > 0 {
			if eventString(payload, "originalEvent") == "" {
				payload["originalEvent"] = eventType
			}
			payload["event"] = "message_warning"
			payload["type"] = "message_warning"
			payload["status"] = "blocked"
			payload["runtimeStatus"] = models.TeamTaskStatusRunning
			payload["availability"] = models.TeamMemberAvailabilityBusy
			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."
			eventType = "message_warning"
			eventStatus = normalizedTeamTaskEventStatus(payload)
			eventSignalsCompletion = false
			eventSignalsFailure = false
		}
	}
	if (eventSignalsCompletion || eventSignalsFailure) && teamRedisProtocolVersion(payload) >= 2 {
		duplicate, err := s.hasAcceptedTeamCompletionID(
			team.ID,
			eventString(payload, "completionId", "completion_id"),
		)
		if err != nil {
			return err
		}
		if duplicate {
			if assignmentResultOnly && task != nil && member != nil && !isTerminalTeamTaskStatus(task.Status) {
				assignmentID := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
				contentHash := eventString(payload, "contentHash", "content_hash")
				sameResult, confirmErr := s.hasLeaderMediatedResultConfirmationForAttempt(team.ID, task.ID, member.MemberKey, assignmentID, teamMaxInt(eventInt(payload, "revision"), 1), contentHash)
				if confirmErr != nil {
					return confirmErr
				}
				// A model may retry one canonical completion with corrected prose.
				// Preserve idempotency for identical content, but update the same
				// card and notify the Leader once when the explicit result changed.
				if !sameResult && contentHash != "" {
					payload["event"] = "member_result_updated"
					payload["type"] = "member_result_updated"
					payload["resultUpdated"] = true
					payload["normalizedResultSource"] = "explicit_completion_update"
					payload["chatPolicy"] = "visible"
					payload["visibleToChat"] = true
					payload["visible_to_chat"] = true
					payload["displayKey"] = fmt.Sprintf("worker-result:%d:%s:%d", task.ID, assignmentID, teamMaxInt(eventInt(payload, "revision"), 1))
					enrichTeamCollaborationStep(team, "member_result_updated", payload, member, task)
					updateEventID := fmt.Sprintf(
						"member-result-updated:%d:%d:%s:%s:%s",
						team.ID,
						task.ID,
						normalizeTeamRedisKeyPart(member.MemberKey),
						normalizeTeamRedisKeyPart(assignmentID),
						normalizeTeamRedisKeyPart(contentHash),
					)
					updatePayloadJSON, marshalErr := marshalOptionalJSON(payload)
					if marshalErr != nil {
						return marshalErr
					}
					updateEvent := &models.TeamEvent{
						TeamID: team.ID, TaskID: &task.ID, MemberID: &member.ID,
						EventID: &updateEventID, EventType: "member_result_updated",
						PayloadJSON: updatePayloadJSON, OccurredAt: eventTime(payload), CreatedAt: time.Now().UTC(),
					}
					if createErr := s.repo.CreateEvent(updateEvent); createErr != nil && !errors.Is(createErr, repository.ErrDuplicateTeamEvent) {
						return createErr
					}
					if notifyErr := s.createLeaderMediatedResultNotification(team, bus, task, member, payload, updateEvent); notifyErr != nil {
						return notifyErr
					}
					task.LedgerVersion++
					task.UpdatedAt = time.Now().UTC()
					if updateErr := s.repo.UpdateTask(task); updateErr != nil {
						return updateErr
					}
					s.publishTeamRootWorkflowState(bus, task)
					if isCompletionProposal {
						return s.emitCompletionAcknowledgement(team, bus, task, member, payload, teamCompletionDecisionAccepted, "assignment_result_updated")
					}
					return nil
				}
			}
			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
		}
	}
	memberTerminalOnly := task != nil &&
		member != nil &&
		member.ID != task.TargetMemberID &&
		(eventSignalsCompletion || eventSignalsFailure)
	if memberTerminalOnly {
		payload["memberTerminalOnly"] = true
		payload["rootTaskTerminal"] = false
	}
	rootCompletion := eventSignalsCompletion &&
		task != nil &&
		member != nil &&
		member.ID == task.TargetMemberID &&
		isLeaderTeamMember(member)
	normalizeTeamRoleEventPayload(payload, member, task, rootCompletion)
	applyTeamChatPolicy(eventType, payload, task, member)
	if stateNeutralAssignmentEvent &&
		(strings.EqualFold(eventString(payload, "originalEvent"), "task_failed") ||
			strings.EqualFold(eventString(payload, "originalEvent"), "message_failed")) {
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
	}
	enrichTeamCollaborationStep(team, eventType, payload, member, task)

	payloadJSON, err := marshalOptionalJSON(payload)
	if err != nil {
		return err
	}
	streamID := message.ID
	event := &models.TeamEvent{
		TeamID:        team.ID,
		EventType:     eventType,
		PayloadJSON:   payloadJSON,
		RedisStreamID: &streamID,
		OccurredAt:    eventTime(payload),
	}
	if eventID != "" {
		event.EventID = &eventID
	}
	if eventSignalsCompletion || eventSignalsFailure {
		completionID := eventString(payload, "completionId", "completion_id")
		if completionID != "" {
			event.CompletionID = &completionID
		}
	}
	if member != nil {
		event.MemberID = &member.ID
	}
	if task != nil {
		event.TaskID = &task.ID
	}
	if messageID != "" {
		event.MessageID = &messageID
	}
	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
		// Persist the terminal workflow snapshot together with the final report.
		// Otherwise a valid final delivery can carry the pre-completion state
		// (for example planning) even though the task itself is succeeded.
		payload["workflowState"] = teamWorkflowStateCompleted
		payload["ledgerVersion"] = task.LedgerVersion
		payload["effectiveWorkflowState"] = teamWorkflowStateCompleted
		payload["effectivePlanVersion"] = task.PlanVersion
		payload["effectiveLedgerVersion"] = task.LedgerVersion
		payload["currentPhaseId"] = nil
		payloadJSON, err = marshalOptionalJSON(payload)
		if err != nil {
			return err
		}
		event.PayloadJSON = payloadJSON
		task.ResultJSON = payloadJSON
		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
		}
	}
	if stateNeutralAssignmentEvent &&
		shouldRequestRootCoordinationRecovery(
			task,
			member,
			eventString(payload, "eventKind", "event_kind", "kind"),
			payload,
		) {
		if err := s.createRootCoordinationRecovery(team, bus, task, member, payload, event); err != nil {
			return err
		}
	}
	reviewInvalidated := false
	if !stateNeutralAssignmentEvent {
		reviewInvalidated, err = s.invalidateModifiedTeamArtifactReviews(task, payload, time.Now().UTC())
		if err != nil {
			return err
		}
	}
	workflowChanged := reviewInvalidated
	waitingAssignmentObservation := isWaitingTeamTaskEventStatus(normalizedTeamTaskEventStatus(payload)) &&
		!isAuthoritativeAssignmentFailure(eventType, payload)
	if !stateNeutralAssignmentEvent || waitingAssignmentObservation {
		if err := s.projectTeamWorkItem(team, task, member, eventType, payload, event); err != nil {
			return err
		}
	}
	if assignmentResultOnly {
		// ConfirmWorkItemResult is the sole terminal writer for member results.
		// Project the source event first as running evidence, then atomically
		// persist the real succeeded/failed outcome and Leader notification.
		if err := s.createLeaderMediatedResultNotification(team, bus, task, member, payload, event); err != nil {
			return err
		}
	}
	reviewValidated := false
	if !stateNeutralAssignmentEvent {
		reviewValidated, err = s.applyStructuredAssignmentValidation(task, member, payload, time.Now().UTC())
		if err != nil {
			return err
		}
	}
	workflowChanged = workflowChanged || reviewValidated
	// Monitor replies are observations and reminders only. They remain in the
	// audit stream but never write assignment/root terminal state; the member's
	// real completion/failure event is the only business-state input.
	if !atomicRootCompletionAccepted && !stateNeutralAssignmentEvent {
		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.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 atomicRootCompletionAccepted {
		taskProjection = teamTaskProjectionResult{changed: true, status: models.TeamTaskStatusSucceeded}
	} else if task != nil && !passiveMonitorEvent && !stateNeutralAssignmentEvent && !memberTerminalOnly && !assignmentResultOnly && !leaderMediatedRouteViolation {
		taskProjection = projectTeamTaskRuntimeState(task, payload, eventType, payloadJSON, now)
		if taskProjection.changed || workflowChanged {
			task.UpdatedAt = now
			if err := s.repo.UpdateTask(task); err != nil {
				return err
			}
		}
	}
	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
		}
	}
	if member != nil {
		member.LastSeenAt = &now
		if !passiveMonitorEvent && !stateNeutralAssignmentEvent {
			resetTeamMemberAssignmentProjection(member, eventType)
			applyTeamMemberRuntimeProjection(member, payload, eventType)
		}
		taskIsActive := task != nil && !isTerminalTeamTaskStatus(task.Status)
		if !passiveMonitorEvent && !stateNeutralAssignmentEvent && 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
			}
			member.CurrentTaskID = &task.ID
			member.Progress = eventInt(payload, "progress")
		}
		taskProjectedTerminal := taskProjection.status == models.TeamTaskStatusSucceeded || taskProjection.status == models.TeamTaskStatusFailed
		terminalEventWithoutTask := (task == nil || memberTerminalOnly || assignmentResultOnly) && (eventSignalsCompletion || eventSignalsFailure || assignmentResultOnly)
		if taskProjectedTerminal || terminalEventWithoutTask {
			member.Status = models.TeamMemberStatusIdle
			member.CurrentTaskID = nil
			if taskProjection.status == models.TeamTaskStatusFailed || (terminalEventWithoutTask && eventSignalsFailure) {
				member.Progress = 0
				if member.Availability == "" || member.Availability == models.TeamMemberAvailabilityUnknown {
					member.Availability = models.TeamMemberAvailabilityBlocked
				}
				if member.BlockedReason == nil {
					if errText := eventString(payload, "error_message", "error", "reason", "diagnostic", "lastSummary", "last_summary", "summary"); errText != "" {
						member.BlockedReason = &errText
					}
				}
			} else if taskProjection.status == models.TeamTaskStatusSucceeded || assignmentResultOnly || (terminalEventWithoutTask && eventSignalsCompletion) {
				member.Progress = 100
				if member.Availability != models.TeamMemberAvailabilityBlocked {
					member.Availability = models.TeamMemberAvailabilityIdle
					member.BlockedReason = nil
				}
			}
		}
		if task != nil && task.Status == models.TeamTaskStatusSucceeded {
			member.Status = models.TeamMemberStatusIdle
			member.CurrentTaskID = nil
			member.Progress = 100
			member.Availability = models.TeamMemberAvailabilityIdle
			member.BlockedReason = nil
		}
		// Runtime availability is an instantaneous transport observation. The
		// persisted assignment ledger is the business truth: a progress callback
		// cannot make a member available while another assignment is still active,
		// and a post-completion message cannot reopen a terminal member state.
		memberItems, memberItemsErr := s.repo.ListWorkItemsByTeamID(team.ID, 500)
		if memberItemsErr != nil {
			return memberItemsErr
		}
		reconcileTeamMemberOperationalState(member, memberItems)
		member.UpdatedAt = now
		if err := s.repo.UpdateMember(member); err != nil {
			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
			}
		}
	}
	s.publishTeamRootWorkflowState(bus, task)
	return nil
}

func reconcileTeamMemberOperationalState(member *models.TeamMember, items []models.TeamWorkItem) bool {
	if member == nil {
		return false
	}
	owned := make([]models.TeamWorkItem, 0)
	active := make([]models.TeamWorkItem, 0)
	for idx := range items {
		item := items[idx]
		if item.OwnerMemberID == nil || *item.OwnerMemberID != member.ID || item.SupersededBy != nil {
			continue
		}
		owned = append(owned, item)
		switch strings.ToLower(strings.TrimSpace(item.Status)) {
		case models.TeamTaskStatusDispatched, models.TeamTaskStatusRunning, models.TeamTaskStatusWaiting:
			active = append(active, item)
		}
	}
	if len(owned) == 0 {
		return false
	}
	changed := false
	setString := func(target *string, value string) {
		if *target != value {
			*target = value
			changed = true
		}
	}
	setOptionalString := func(target **string, value string) {
		if strings.TrimSpace(derefTeamString(*target)) == value {
			return
		}
		copyValue := value
		*target = &copyValue
		changed = true
	}
	if len(active) > 0 {
		latest := active[0]
		for idx := 1; idx < len(active); idx++ {
			if active[idx].UpdatedAt.After(latest.UpdatedAt) ||
				(active[idx].UpdatedAt.Equal(latest.UpdatedAt) && active[idx].ID > latest.ID) {
				latest = active[idx]
			}
		}
		setString(&member.Status, models.TeamMemberStatusBusy)
		setString(&member.Availability, models.TeamMemberAvailabilityBusy)
		if member.CurrentTaskID == nil || *member.CurrentTaskID != latest.RootTaskID {
			rootTaskID := latest.RootTaskID
			member.CurrentTaskID = &rootTaskID
			changed = true
		}
		runtimeStatus := strings.ToLower(strings.TrimSpace(derefTeamString(member.RuntimeStatus)))
		switch runtimeStatus {
		case "", models.TeamTaskStatusSucceeded, models.TeamTaskStatusFailed, models.TeamTaskStatusStale, "idle", "completed":
			setOptionalString(&member.RuntimeStatus, models.TeamTaskStatusRunning)
		}
		if member.BlockedReason != nil {
			member.BlockedReason = nil
			changed = true
		}
		return changed
	}

	latest := owned[0]
	for idx := 1; idx < len(owned); idx++ {
		if owned[idx].UpdatedAt.After(latest.UpdatedAt) ||
			(owned[idx].UpdatedAt.Equal(latest.UpdatedAt) && owned[idx].ID > latest.ID) {
			latest = owned[idx]
		}
	}
	switch strings.ToLower(strings.TrimSpace(latest.Status)) {
	case models.TeamTaskStatusSucceeded:
		setString(&member.Status, models.TeamMemberStatusIdle)
		setString(&member.Availability, models.TeamMemberAvailabilityIdle)
		setOptionalString(&member.RuntimeStatus, models.TeamTaskStatusSucceeded)
		if member.CurrentTaskID != nil {
			member.CurrentTaskID = nil
			changed = true
		}
		if member.Progress != 100 {
			member.Progress = 100
			changed = true
		}
		if member.BlockedReason != nil {
			member.BlockedReason = nil
			changed = true
		}
	case models.TeamTaskStatusFailed, models.TeamTaskStatusStale:
		setString(&member.Status, models.TeamMemberStatusIdle)
		setString(&member.Availability, models.TeamMemberAvailabilityBlocked)
		setOptionalString(&member.RuntimeStatus, latest.Status)
		if member.CurrentTaskID != nil {
			member.CurrentTaskID = nil
			changed = true
		}
	}
	return changed
}

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 isTargetResolutionWarning(eventType, payload) {
		return true
	}
	if isNonAuthoritativeDispatchWarning(eventType, payload) ||
		isNonAuthoritativeDispatchFailure(eventType, payload) {
		// Transport diagnostics never fail the business attempt, but an open root
		// must still surface them to the recovery observer. The source event gives
		// each reminder an idempotent identity, so this does not create a retry
		// storm and older Runtime wrappers retain a safe fallback.
		return true
	}
	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 eventKind == "runtime_reconciliation_needed" &&
		strings.EqualFold(eventString(payload, "failureDomain", "failure_domain"), "runtime_adapter") &&
		eventBool(payload, "retryable") {
		return true
	}
	if eventBool(payload, "artifactValidationFailed", "artifact_validation_failed") {
		return true
	}
	if eventBool(payload, "nonAuthoritativeCompletion", "non_authoritative_completion") {
		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,
		"supervisorReviewRequested":   eventBool(sourcePayload, "supervisorReviewRequested", "supervisor_review_requested"),
		"provisionalAssignmentResult": eventBool(sourcePayload, "provisionalAssignmentResult", "provisional_assignment_result"),
		"blockedDependencies":         normalizeContextRefs(firstTeamValue(sourcePayload, "blockedDependencies", "blocked_dependencies")),
		"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)
	if isTargetResolutionWarning("message_warning", sourcePayload) {
		s.dispatchMemberTargetResolutionReviewToInbox(team, bus, task, member, sourcePayload, eventID)
	}
	return nil
}

func (s *teamService) dispatchMemberTargetResolutionReviewToInbox(team *models.Team, bus *redisBus, task *models.TeamTask, member *models.TeamMember, sourcePayload map[string]interface{}, eventID string) {
	if team == nil || bus == nil || task == nil || member == nil || sourcePayload == nil {
		return
	}
	originalTarget := eventString(sourcePayload, "to", "target", "recipient", "targetMemberId", "target_member_id")
	candidates := normalizeContextRefs(firstTeamValue(sourcePayload, "targetCandidates", "target_candidates"))
	suggestions := normalizeContextRefs(firstTeamValue(sourcePayload, "targetSuggestions", "target_suggestions"))
	prompt := fmt.Sprintf(
		"ClawManager kept your current assignment open because the last collaboration message target could not be uniquely resolved. Original target: %q. Exact roster candidates: %s. Suggestions requiring confirmation: %s. Re-check the current Team roster and retry the intended message to one unambiguous member, or tell the Leader what remains ambiguous. Continue the same assignment; do not create a new revision merely for this routing correction. When the deliverable itself is ready, submit the normal completion for this exact assignment.",
		originalTarget,
		strings.Join(candidates, ", "),
		strings.Join(suggestions, ", "),
	)
	assignmentID := eventString(sourcePayload, "assignmentId", "assignment_id", "workId", "work_id")
	revision := teamMaxInt(eventInt(sourcePayload, "revision"), 1)
	messageID := eventID + ":member-review"
	envelope := map[string]interface{}{
		"v":                  1,
		"protocolVersion":    2,
		"messageId":          messageID,
		"teamId":             strconv.Itoa(team.ID),
		"from":               "clawmanager-monitor",
		"to":                 member.MemberKey,
		"replyTo":            teamTaskReplyTarget,
		"requiresCompletion": false,
		"completionTool":     teamTaskCompletionTool,
		"intent":             "assignment_recovery_reminder",
		"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":             assignmentID,
		"assignmentId":       assignmentID,
		"revision":           revision,
		"title":              "Confirm Team message recipient",
		"prompt":             prompt,
		"rawPrompt":          prompt,
		"metadata": map[string]interface{}{
			"monitor":               true,
			"monitorType":           "target_resolution_review",
			"eventKind":             "target_resolution_warning",
			"nonAuthoritative":      true,
			"stateEffect":           "none",
			"rootTaskTerminal":      false,
			"clarificationRequired": true,
			"sourcePayload":         sourcePayload,
		},
		"createdAt": time.Now().UTC().Format(time.RFC3339Nano),
	}
	applyTeamTaskEnvelopeContext(envelope, task, member.MemberKey)
	envelopeJSON, err := marshalJSON(envelope)
	if err != nil {
		fmt.Printf("Warning: failed to encode member target-resolution reminder for Team %d task %d: %v\n", team.ID, task.ID, err)
		return
	}
	if _, err := bus.XAdd(context.Background(), teamInboxKey(team.ID, member.MemberKey), map[string]string{
		"payload":    envelopeJSON,
		"team_id":    strconv.Itoa(team.ID),
		"task_id":    strconv.Itoa(task.ID),
		"message_id": messageID,
		"member_id":  member.MemberKey,
	}); err != nil {
		fmt.Printf("Warning: failed to dispatch member target-resolution reminder for Team %d task %d: %v\n", team.ID, task.ID, err)
	}
}

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 := ""
	if eventBool(notificationPayload, "supervisorReviewRequested", "supervisor_review_requested") {
		prompt = fmt.Sprintf(
			"ClawManager needs a business-aware stall review for member %s on root task %s.\n\nEvidence:\n%s\n\nClassify the situation as healthy long-running work, waiting for an external tool, provider/model stall, Runtime unavailable, dependency blocker, identity conflict, or ambiguous. Use the supplied activity, dependency, artifact, and receipt facts; do not infer failure merely from elapsed time. If healthy, keep waiting. If blocked, send a concise factual reminder to the member or coordinate a correction. Do not complete, cancel, interrupt, or replace a live attempt solely because it is quiet.",
			member.MemberKey,
			task.MessageID,
			summary,
		)
	} else if eventBool(notificationPayload, "provisionalAssignmentResult", "provisional_assignment_result") {
		prompt = fmt.Sprintf(
			"ClawManager recorded a provisional result from member %s for root task %s because these declared prerequisite assignments are not yet successful: %s.\n\nAttempt:\n%s\n\nKeep the root task open. Do not treat this attempt as PASS, FAIL, or completed work. The control-plane Monitor will ask the same member to re-check after the prerequisites become ready; intervene only if the dependency graph itself is wrong or a prerequisite has genuinely failed.",
			member.MemberKey,
			task.MessageID,
			strings.Join(normalizeContextRefs(firstTeamValue(notificationPayload, "blockedDependencies", "blocked_dependencies")), ", "),
			summary,
		)
	} else {
		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
	}
	items, itemsErr := s.repo.ListWorkItemsByRootTaskID(task.ID)
	if itemsErr != nil {
		return itemsErr
	}
	dependencyFacts := resolveTeamWorkItemDependencyFacts(items, workID)
	blockedDependencies := dependencyFacts.Waiting
	// Dependency metadata describes plan intent; it does not own an attempt's
	// terminal state.  Preserve a successful member result and surface any
	// uncertain ordering to the Leader/Monitor as diagnostics instead of
	// rewriting succeeded back to running.
	if len(dependencyFacts.Unknown) > 0 {
		sourcePayload["dependencyState"] = "unknown_advisory"
		sourcePayload["unknownDependencies"] = dependencyFacts.Unknown
		sourcePayload["dependencyReviewSuggested"] = true
	} else if len(dependencyFacts.Waiting) > 0 {
		sourcePayload["dependencyState"] = "known_waiting"
		sourcePayload["waitingDependencies"] = dependencyFacts.Waiting
		sourcePayload["dependencyReviewSuggested"] = true
	} else if len(dependencyFacts.Ready) > 0 {
		sourcePayload["dependencyState"] = "known_ready"
	}
	delete(sourcePayload, "provisionalAssignmentResult")
	delete(sourcePayload, "dependencyBlocked")
	resultFailed := isAuthoritativeAssignmentFailure(sourceEvent.EventType, sourcePayload)
	recoveryReadyAssignments := []string(nil)
	if resultFailed {
		// A failed result is not automatically a dependency blocker merely
		// because the assignment has unfinished prerequisites. Only preserve
		// dependency-specific recovery when the Runtime explicitly reported
		// that structured condition; otherwise let the Leader handle the real
		// failure without an unsafe automatic reissue.
		blockedDependencies = nil
	} else {
		recoveryReadyAssignments = uniqueTeamStrings(append(
			dependencyBlockedAssignmentsReadyAfter(items, workID),
			dependencyReadyAssignmentsAfter(items, workID)...,
		))
	}
	resultStatus := models.TeamTaskStatusSucceeded
	confirmationLabel := member.MemberKey + " assignment result confirmed"
	if resultFailed {
		resultStatus = models.TeamTaskStatusFailed
		confirmationLabel = member.MemberKey + " assignment failure confirmed"
	}
	contentHash := eventString(sourcePayload, "contentHash", "content_hash")
	if contentHash == "" {
		contentHash = teamResultContentHash(sourcePayload)
	}
	sourceMessageID := eventString(sourcePayload, "sourceMessageId", "source_message_id")
	sameSourceTurn, err := s.hasLeaderMediatedResultConfirmationForSource(
		team.ID,
		task.ID,
		member.MemberKey,
		workID,
		revision,
		sourceMessageID,
	)
	if err != nil || sameSourceTurn {
		return err
	}
	alreadyConfirmed, err := s.hasLeaderMediatedResultConfirmationForAttempt(team.ID, task.ID, member.MemberKey, workID, revision, contentHash)
	if err != nil || alreadyConfirmed {
		return err
	}
	resultIdentity := "content:" + contentHash
	if sourceMessageID != "" {
		resultIdentity = "source:" + sourceMessageID
	}
	confirmationSeed := fmt.Sprintf("%d:%d:%s:%s:%d:%s", team.ID, task.ID, normalizeTeamMemberRouteKey(member.MemberKey), workID, revision, resultIdentity)
	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,
		"sourceMessageId":          sourceMessageID,
		"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":                   resultStatus,
		"runtimeStatus":            resultStatus,
		"resultFailed":             resultFailed,
		"dependencyBlocked":        resultFailed && len(blockedDependencies) > 0,
		"blockedDependencies":      blockedDependencies,
		"recoveryReadyAssignments": recoveryReadyAssignments,
		"summary":                  confirmationLabel,
		"collaborationStep": map[string]interface{}{
			"type":          "ack",
			"status":        resultStatus,
			"actor":         member.MemberKey,
			"target":        "leader",
			"rootTaskId":    rootTaskRef,
			"rootMessageId": task.MessageID,
			"workId":        workID,
			"title":         confirmationLabel,
			"summary":       confirmationLabel,
			"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:          resultStatus,
		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 = resultStatus
				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
		}
	}
	if bus != nil && len(recoveryReadyAssignments) > 0 {
		currentItems, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID)
		if listErr != nil {
			return listErr
		}
		for _, readyAssignmentID := range recoveryReadyAssignments {
			var readyItem *models.TeamWorkItem
			for idx := range currentItems {
				candidate := currentItems[idx]
				if workItemBusinessID(candidate) != readyAssignmentID || candidate.SupersededBy != nil ||
					!isActiveTeamWorkItemStatus(candidate.Status) {
					continue
				}
				if readyItem == nil || teamMaxInt(candidate.Revision, 1) > teamMaxInt(readyItem.Revision, 1) {
					clone := candidate
					readyItem = &clone
				}
			}
			if readyItem == nil || readyItem.OwnerMemberID == nil {
				continue
			}
			owner, ownerErr := s.repo.GetMemberByID(*readyItem.OwnerMemberID)
			if ownerErr != nil {
				return ownerErr
			}
			if owner == nil || owner.TeamID != team.ID || !isActiveTeamMember(owner) {
				continue
			}
			if dispatchErr := s.dispatchDependencyReadyReminder(team, bus, task, readyItem, owner, now); dispatchErr != nil {
				return dispatchErr
			}
		}
	}
	return nil
}

func (s *teamService) hasLeaderMediatedResultConfirmation(teamID, taskID int, memberKey string, assignmentID string, contentHashes ...string) (bool, error) {
	return s.hasLeaderMediatedResultConfirmationForAttempt(teamID, taskID, memberKey, assignmentID, 0, contentHashes...)
}

func (s *teamService) hasLeaderMediatedResultConfirmationForAttempt(teamID, taskID int, memberKey string, assignmentID string, expectedRevision int, 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
		}
		if expectedRevision > 0 && teamMaxInt(eventInt(payload, "revision", "workRevision", "work_revision"), 1) != expectedRevision {
			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) hasLeaderMediatedResultConfirmationForSource(teamID, taskID int, memberKey string, assignmentID string, expectedRevision int, sourceMessageID string) (bool, error) {
	if s == nil || s.repo == nil || strings.TrimSpace(sourceMessageID) == "" {
		return false, nil
	}
	events, err := s.repo.ListEventsByTeamID(teamID, 500)
	if err != nil {
		return false, err
	}
	normalizedMember := normalizeTeamMemberRouteKey(memberKey)
	expectedAssignment := strings.TrimSpace(assignmentID)
	expectedSource := strings.TrimSpace(sourceMessageID)
	for idx := range events {
		event := events[idx]
		if event.EventType != "member_result_confirmed" || 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")
		if expectedAssignment != "" && confirmedAssignmentID != "" && confirmedAssignmentID != expectedAssignment {
			continue
		}
		if expectedRevision > 0 && teamMaxInt(eventInt(payload, "revision", "workRevision", "work_revision"), 1) != expectedRevision {
			continue
		}
		if eventString(payload, "sourceMessageId", "source_message_id") == expectedSource {
			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")
	resultFailed := eventBool(notificationPayload, "resultFailed", "result_failed") &&
		!isStateNeutralTeamObservation(notificationPayload)
	prompt := ""
	if resultFailed {
		blockedDependencies := normalizeContextRefs(firstTeamValue(notificationPayload, "blockedDependencies", "blocked_dependencies"))
		if len(blockedDependencies) > 0 {
			prompt = fmt.Sprintf(
				"Member %s returned a confirmed blocked assignment result for root task %s because these prerequisite assignments were not yet successful: %s.\n\nResult:\n%s\n\nKeep the root task open. Do not immediately retry while those prerequisites are still pending. When their confirmed results arrive, reissue this same assignment once with its existing contract and current artifact references. If a prerequisite fails, decide whether to repair, reassign, or explicitly waive that failed work. Do not treat this receipt as successful work.",
				member.MemberKey,
				task.MessageID,
				strings.Join(blockedDependencies, ", "),
				resultMarkdown,
			)
		} else {
			prompt = fmt.Sprintf(
				"Member %s returned a confirmed failed or blocked assignment result for root task %s.\n\nResult:\n%s\n\nKeep the root task open. Use the structured assignment state and available artifacts to recover: retry or reissue only when the reported failure/blocker is still applicable, or record an explicit risk waiver if the user goal can safely proceed. Do not treat this receipt as successful work and do not retry an assignment that has already recovered.",
				member.MemberKey,
				task.MessageID,
				resultMarkdown,
			)
		}
	} else {
		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,
		)
		recoveryReady := normalizeContextRefs(firstTeamValue(notificationPayload, "recoveryReadyAssignments", "recovery_ready_assignments"))
		if len(recoveryReady) > 0 {
			prompt += fmt.Sprintf(
				"\n\nThese assignments previously returned a confirmed dependency blocker and all of their declared prerequisites are now successful: %s. Reissue each affected assignment once using its existing contract and current artifact references, unless a newer revision is already running or completed.",
				strings.Join(recoveryReady, ", "),
			)
		}
	}
	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(),
		"turnOutcomePolicy": map[string]interface{}{
			"actionExpected":           true,
			"immediateRecoveryAllowed": true,
			"reason":                   "member_result_notification",
		},
		"metadata":  notificationPayload,
		"createdAt": time.Now().UTC().Format(time.RFC3339Nano),
	}
	applyTeamTaskEnvelopeContext(envelope, task, leaderKey)
	contextRefs := s.durableTeamTaskContextRefs(team, task, explicitTeamArtifactReferences(notificationPayload)...)
	if len(contextRefs) > 0 {
		envelope["artifactRefs"] = contextRefs
		envelope["contextRefs"] = contextRefs
	}
	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",
		"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
		}
		if found, err := s.repo.GetTaskByMessageID(teamID, messageID); err != nil {
			return nil, err
		} else if found != nil && found.TeamID == teamID {
			return found, nil
		}
	}
	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 {
				return nil, err
			}
			if found != nil && found.TeamID == teamID {
				return found, nil
			}
		}
	}
	if step, ok := payload["collaborationStep"].(map[string]interface{}); ok {
		return s.resolveTeamTaskFromEventReferences(teamID, step)
	}
	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 == "" {
		return 0
	}
	if matches := regexp.MustCompile(`^team-(\d+)-task-(\d+)$`).FindStringSubmatch(strings.ToLower(value)); len(matches) == 3 {
		refTeamID, _ := strconv.Atoi(matches[1])
		taskID, _ := strconv.Atoi(matches[2])
		if refTeamID == teamID {
			return taskID
		}
		return 0
	}
	if matches := regexp.MustCompile(`^clawmanager-task-(\d+)$`).FindStringSubmatch(strings.ToLower(value)); len(matches) == 2 {
		taskID, _ := strconv.Atoi(matches[1])
		return taskID
	}
	return 0
}

func (s *teamService) activeTaskForMember(teamID int, member *models.TeamMember, requireTarget bool) (*models.TeamTask, error) {
	if member == nil || member.CurrentTaskID == nil {
		return nil, nil
	}
	found, err := s.repo.GetTaskByID(*member.CurrentTaskID)
	if err != nil {
		return nil, err
	}
	if found == nil || found.TeamID != teamID || isTerminalTeamTaskStatus(found.Status) {
		return nil, nil
	}
	if requireTarget && found.TargetMemberID != member.ID {
		return nil, nil
	}
	return found, nil
}

func (s *teamService) activeTaskFromPeerContext(teamID int, payload map[string]interface{}, member *models.TeamMember) (*models.TeamTask, error) {
	candidates := []*models.TeamMember{member}
	for _, key := range []string{"from", "source", "sourceMemberId", "source_member_id", "sender", "senderMemberId", "sender_member_id", "to", "recipient", "target", "targetMemberId", "target_member_id", "memberId", "member_id"} {
		memberKey := eventString(payload, key)
		if memberKey == "" || memberKey == teamTaskReplyTarget {
			continue
		}
		found, err := s.repo.GetMemberByTeamKey(teamID, memberKey)
		if err != nil {
			return nil, err
		}
		if found != nil {
			candidates = append(candidates, found)
		}
	}
	seen := map[int]struct{}{}
	for _, candidate := range candidates {
		if candidate == nil {
			continue
		}
		if _, ok := seen[candidate.ID]; ok {
			continue
		}
		seen[candidate.ID] = struct{}{}
		if task, err := s.activeTaskForMember(teamID, candidate, false); err != nil {
			return nil, err
		} else if task != nil {
			return task, nil
		}
	}
	return nil, nil
}

func (s *teamService) normalizePeerAssistedTeamEvent(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember) (string, *models.TeamMember, error) {
	if team == nil || payload == nil || !isPeerCapableTeamEvent(eventType) {
		return eventType, member, nil
	}
	communicationMode := normalizedTeamCommunicationMode(team.CommunicationMode)
	if communicationMode != teamCommunicationModePeerAssisted && communicationMode != teamCommunicationModeFullMesh {
		return eventType, member, nil
	}
	sourceKey := eventString(payload, "from", "source", "sourceMemberId", "source_member_id", "sender", "senderMemberId", "sender_member_id")
	if sourceKey == "" && member != nil {
		sourceKey = member.MemberKey
	}
	targetKey := eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id")
	if sourceKey == "" || targetKey == "" || sourceKey == targetKey || sourceKey == teamTaskReplyTarget || targetKey == teamTaskReplyTarget {
		return eventType, member, nil
	}

	sourceMember, err := s.repo.GetMemberByTeamKey(team.ID, sourceKey)
	if err != nil {
		return eventType, member, err
	}
	targetMember, err := s.repo.GetMemberByTeamKey(team.ID, targetKey)
	if err != nil {
		return eventType, member, err
	}
	if !isActiveTeamMember(sourceMember) || !isActiveTeamMember(targetMember) {
		return eventType, member, nil
	}

	if member == nil || member.MemberKey != sourceMember.MemberKey {
		member = sourceMember
	}
	payload["peer"] = true
	payload["communicationMode"] = communicationMode
	payload["sourceMemberId"] = sourceMember.MemberKey
	payload["targetMemberId"] = targetMember.MemberKey
	payload["from"] = sourceMember.MemberKey
	payload["to"] = targetMember.MemberKey

	rawAction := eventString(payload, "peerAction", "peer_action", "action", "intent", "kind")
	action := normalizeTeamPeerAction(rawAction)
	if strings.TrimSpace(rawAction) == "" && (eventType == "outbound" || eventType == "task_assigned" || eventType == "team_send") && isTeamLeaderRole(sourceMember.Role) && !isTeamLeaderRole(targetMember.Role) {
		action = "handoff"
	}
	payload["peerAction"] = action
	if strings.HasPrefix(eventType, "peer_") {
		return eventType, member, nil
	}
	switch action {
	case "review_request", "peer_review":
		return "peer_review_request", member, nil
	case "handoff":
		return "peer_handoff", member, nil
	default:
		return "peer_request", member, nil
	}
}

func isPeerCapableTeamEvent(eventType string) bool {
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "outbound", "task_assigned", "team_send", "peer_request", "peer_handoff", "peer_review_request", "peer_reply":
		return true
	default:
		return false
	}
}

func normalizeTeamPeerAction(raw string) string {
	action := strings.ToLower(strings.TrimSpace(raw))
	action = strings.ReplaceAll(action, "-", "_")
	action = strings.ReplaceAll(action, " ", "_")
	switch action {
	case "handoff", "assign", "assignment":
		return "handoff"
	case "review", "review_request", "peer_review", "code_review":
		return "review_request"
	case "artifact", "artifact_request", "file_request":
		return "artifact_request"
	case "blocker", "blocker_help", "help":
		return "blocker_help"
	default:
		return "ask"
	}
}

func isActiveTeamMember(member *models.TeamMember) bool {
	return member != nil &&
		member.Status != models.TeamMemberStatusDeleted &&
		member.Status != models.TeamMemberStatusDeleting
}

func enrichTeamCollaborationStep(team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask) {
	if payload == nil {
		return
	}
	if existing, ok := payload["collaborationStep"].(map[string]interface{}); ok && len(existing) > 0 {
		normalizeExistingCollaborationStep(existing, team, eventType, payload, member, task)
		return
	}

	stepType := collaborationStepTypeForEvent(eventType, payload)
	if stepType == "" {
		return
	}
	actor := collaborationActorKey(payload, member)
	target := eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id", "memberId")
	if stepType == "assignment" && target == "" {
		target = eventString(payload, "assignee", "owner", "targetMember")
	}
	status := collaborationStepStatusForEvent(eventType, payload)
	title := collaborationStepTitle(stepType, actor, target, payload)
	summary := eventString(payload, "summary", "resultMarkdown", "result_markdown", "result", "text", "message", "prompt", "instruction", "instructions", "diagnostic", "error", "reason")
	fullContent := collaborationStepContentForEvent(stepType, payload, summary)
	messageID := eventString(payload, "messageId", "message_id")
	rootTaskID := ""
	rootMessageID := ""
	if task != nil {
		rootTaskID = fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID)
		rootMessageID = task.MessageID
		if messageID == "" {
			messageID = task.MessageID
		}
	}
	if rootTaskID == "" {
		rootTaskID = eventString(payload, "rootTaskId", "root_task_id", "parentTaskId", "parent_task_id")
	}
	if rootMessageID == "" {
		rootMessageID = eventString(payload, "rootMessageId", "root_message_id", "parentMessageId", "parent_message_id", "inReplyTo", "in_reply_to")
	}
	workID := collaborationWorkID(stepType, actor, target, messageID, payload)
	step := map[string]interface{}{
		"id":            workID,
		"workId":        workID,
		"type":          stepType,
		"status":        status,
		"title":         title,
		"summary":       summary,
		"content":       fullContent,
		"actor":         actor,
		"target":        target,
		"messageId":     messageID,
		"rootTaskId":    rootTaskID,
		"rootMessageId": rootMessageID,
		"eventType":     eventType,
		"source":        "clawmanager",
	}
	if progress := eventInt(payload, "progress"); progress > 0 {
		step["progress"] = progress
	}
	if action := eventString(payload, "peerAction", "peer_action", "action", "intent", "kind"); action != "" {
		step["action"] = normalizeTeamPeerAction(action)
	}
	if phase := inferCollaborationPhase(stepType, title, summary, payload); phase != "" {
		step["phase"] = phase
	}
	payload["collaborationStep"] = step
	if rootTaskID != "" && eventString(payload, "rootTaskId", "root_task_id") == "" {
		payload["rootTaskId"] = rootTaskID
	}
	if rootMessageID != "" && eventString(payload, "rootMessageId", "root_message_id") == "" {
		payload["rootMessageId"] = rootMessageID
	}
}

func (s *teamService) projectTeamWorkItem(
	team *models.Team,
	task *models.TeamTask,
	member *models.TeamMember,
	eventType string,
	payload map[string]interface{},
	event *models.TeamEvent,
) error {
	if s == nil || team == nil || task == nil || payload == nil || event == nil {
		return nil
	}
	if isLeaderControlPlaneSnapshotTask(task, payload) {
		return nil
	}
	// Runtime narrative is evidence for chat and (when paired with a trusted
	// turn-finished signal) completion evaluation. It is not an assignment
	// contract and cannot create a Kanban card or workflow phase.
	if isStructuredAgentNarrativeEvent(eventType, payload) &&
		!eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		return nil
	}
	step, _ := payload["collaborationStep"].(map[string]interface{})
	stepType := eventString(step, "type")
	if stepType == "" {
		stepType = collaborationStepTypeForEvent(eventType, payload)
	}
	if stepType == "" || stepType == "warning" {
		return nil
	}
	// Peer/context traffic remains visible and may wake a model, but it is not a
	// business contract. Materializing it as a Work Item is what turned ordinary
	// follow-ups into false revisions and root-task blockers.
	if (stepType == "peer_request" || stepType == "peer_reply") &&
		!eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		return nil
	}
	if isPassiveAssignmentMonitorEvent(eventType, payload) {
		return nil
	}
	authoritativeAssignment := stepType == "assignment" && isAuthoritativeTeamAssignmentEvent(eventType, payload)
	explicitAssignmentID := eventString(payload, "assignmentId", "assignment_id")
	explicitSourceWorkID := eventString(payload, "workId", "work_id", "subtaskId", "subtask_id")
	explicitWorkID := explicitAssignmentID
	if explicitWorkID == "" {
		explicitWorkID = explicitSourceWorkID
	}
	// Transport acknowledgements are useful in the event log, but are not
	// business work. Progress without an id is resolved against the unique
	// active issued assignment below; it can never create a card on its own.
	if stepType == "ack" {
		return nil
	}
	actor := eventString(step, "actor")
	target := eventString(step, "target")
	if actor == "" {
		actor = collaborationActorKey(payload, member)
	}
	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
	}
	rootCompletion := isLeaderTeamMember(member) && isTeamTaskCompletionSignal(eventType, normalizedTeamTaskEventStatus(payload), payload)
	if rootCompletion {
		ownerKey = member.MemberKey
		stepType = "final_synthesis"
	}
	ownerKey = normalizeTeamMemberRouteKey(ownerKey)
	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)
		if err != nil {
			return err
		}
		owner = found
	}
	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)
		requiredMatches := 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 candidate.RequiredForRoot {
				requiredMatches = append(requiredMatches, candidate)
			}
		}
		if len(matching) != 1 && len(requiredMatches) == 1 {
			// A delayed old-Runtime progress projection may coexist briefly with
			// the real Leader-issued card. Results belong to the sole required
			// contract; the compatibility display card never wins identity.
			matching = requiredMatches
		}
		if len(matching) != 1 {
			// Keep the delivery in the event log, but never invent or select an
			// assignment contract from an ambiguous execution result.
			return nil
		}
		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
			}
		}
	}
	legacyProgressProjection := false
	if stepType == "progress" && owner != nil {
		// Progress is an observation of a dispatched assignment, never an
		// instruction to create one.  A hallucinated/descriptive workId used to
		// create a second Kanban card (Team 69's `review-kanban`) and then kept
		// the root task open forever.  Keep the event in history, but materialize
		// it only when it identifies one existing assignment owned by this member.
		items, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID)
		if listErr != nil {
			return listErr
		}
		var matched *models.TeamWorkItem
		activeCandidates := make([]models.TeamWorkItem, 0, 2)
		for idx := range items {
			candidate := items[idx]
			if candidate.OwnerMemberID == nil || *candidate.OwnerMemberID != owner.ID || candidate.SupersededBy != nil {
				continue
			}
			if !isTerminalTeamTaskStatus(candidate.Status) {
				activeCandidates = append(activeCandidates, candidate)
			}
			candidateAssignmentID := derefTeamString(candidate.AssignmentID)
			candidateCanonicalID := derefTeamString(candidate.CanonicalWorkID)
			if explicitWorkID == "" || (candidate.WorkID != explicitWorkID && candidateAssignmentID != explicitWorkID && candidateCanonicalID != explicitWorkID) {
				continue
			}
			if matched != nil {
				// Ambiguous old-runtime progress is useful in the event log, but it
				// must not pick a card heuristically.
				return nil
			}
			clone := candidate
			matched = &clone
		}
		if matched == nil {
			// Forward compatibility for older Runtime images: if this member has one
			// and only one active Leader-issued card, bind descriptive/raw progress
			// to it.  With zero or multiple candidates the event stays in history
			// but must not create or block a Kanban card.
			if len(activeCandidates) != 1 {
				// Very old Runtime versions emitted only workId and relied on the
				// progress event for a display lane. Preserve that display behaviour
				// as a non-blocking compatibility projection. Protocol v2+ or any
				// explicit assignment mismatch is never allowed to create work.
				if len(activeCandidates) == 0 && explicitAssignmentID == "" && explicitSourceWorkID != "" && teamRedisProtocolVersion(payload) < 2 {
					legacyProgressProjection = true
					payload["legacyProgressProjection"] = true
				} else {
					return nil
				}
			} else {
				clone := activeCandidates[0]
				matched = &clone
			}
		} else if isTerminalTeamTaskStatus(matched.Status) {
			// A late heartbeat/progress update may not reopen a completed card.
			return nil
		}
		if matched != nil {
			canonical := derefTeamString(matched.AssignmentID)
			if canonical == "" {
				canonical = matched.WorkID
			}
			if explicitSourceWorkID != "" && explicitSourceWorkID != canonical {
				payload["sourceWorkId"] = explicitSourceWorkID
			}
			explicitAssignmentID = canonical
			explicitWorkID = canonical
			payload["assignmentId"] = canonical
			payload["canonicalWorkId"] = canonical
			if matched.PhaseID != nil && eventString(payload, "phaseId", "phase_id") == "" {
				payload["phaseId"] = *matched.PhaseID
			}
			if matched.Revision > 0 && eventInt(payload, "revision") <= 0 {
				payload["revision"] = matched.Revision
			}
		}
	}
	if stepType == "progress" && owner == nil {
		// Progress is observational.  Without an existing member lane it remains
		// a chat/event diagnostic and never becomes business work.
		return nil
	}
	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 rootCompletion {
		// Final synthesis is its own Leader-owned lane. Never inherit the last
		// Worker/Reviewer canonical id carried by a stale envelope.
		canonicalWorkID = workID
	} else if canonicalWorkID == "" {
		canonicalWorkID = assignmentID
	}
	if revision > 1 && !strings.HasSuffix(workID, fmt.Sprintf(":r%d", revision)) {
		workID = fmt.Sprintf("%s:r%d", workID, revision)
	}
	var existingAssignmentItems []models.TeamWorkItem
	if assignmentID != "" {
		existingItems, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID)
		if listErr != nil {
			return listErr
		}
		existingAssignmentItems = existingItems
	}
	if authoritativeAssignment && eventInt(payload, "deliverySemanticsVersion", "delivery_semantics_version") > 0 && assignmentID != "" {
		var latest *models.TeamWorkItem
		for idx := range existingAssignmentItems {
			candidate := existingAssignmentItems[idx]
			if workItemBusinessID(candidate) != assignmentID {
				continue
			}
			if latest == nil || teamMaxInt(candidate.Revision, 1) > teamMaxInt(latest.Revision, 1) {
				clone := candidate
				latest = &clone
			}
		}
		if latest == nil {
			// A different canonical assignment is a new workflow stage, not a
			// revision of whichever member happened to finish previously.
			revision = 1
			payload["revision"] = revision
			workID = assignmentID
		} else {
			currentRevision := teamMaxInt(latest.Revision, 1)
			if latest.OwnerMemberID != nil && owner != nil && *latest.OwnerMemberID != owner.ID {
				payload["workflowConcern"] = "assignment_owner_conflict"
				payload["projectionSuppressed"] = true
				return nil
			}
			if revision > currentRevision {
				authorized := revision == currentRevision+1 &&
					isTerminalTeamTaskStatus(latest.Status) &&
					teamAssignmentRevisionRecoveryAllowed(existingAssignmentItems, assignmentID)
				if !authorized {
					payload["workflowConcern"] = "revision_authority_missing"
					payload["projectionSuppressed"] = true
					return nil
				}
			} else if revision < currentRevision {
				payload["workflowConcern"] = "stale_revision_dispatch"
				payload["projectionSuppressed"] = true
				return nil
			}
			workID = assignmentID
			if revision > 1 {
				workID = fmt.Sprintf("%s:r%d", assignmentID, revision)
			}
		}
	}
	var immutableContract *models.TeamWorkItem
	inheritsPriorRevisionContract := authoritativeAssignment && revision > 1
	if (!authoritativeAssignment || inheritsPriorRevisionContract) && assignmentID != "" {
		for idx := range existingAssignmentItems {
			candidate := existingAssignmentItems[idx]
			if candidate.SupersededBy != nil || workItemBusinessID(candidate) != assignmentID {
				continue
			}
			if inheritsPriorRevisionContract && teamMaxInt(candidate.Revision, 1) >= revision {
				continue
			}
			if owner != nil && candidate.OwnerMemberID != nil && *candidate.OwnerMemberID != owner.ID {
				continue
			}
			if immutableContract == nil || teamMaxInt(candidate.Revision, 1) > teamMaxInt(immutableContract.Revision, 1) {
				clone := candidate
				immutableContract = &clone
			}
		}
		if immutableContract != nil {
			// Execution events update outcome fields only. All routing and gate
			// fields come from the issued assignment contract for every role. A
			// real successor revision keeps its new identity but inherits the same
			// durable business contract; omitted Agent fields cannot erase it.
			if !inheritsPriorRevisionContract {
				workID = immutableContract.WorkID
				assignmentID = workItemBusinessID(*immutableContract)
			}
			canonicalWorkID = derefTeamString(immutableContract.CanonicalWorkID)
			if canonicalWorkID == "" {
				canonicalWorkID = assignmentID
			}
			if !inheritsPriorRevisionContract {
				revision = teamMaxInt(immutableContract.Revision, 1)
			}
		}
	}
	reviewRequired := eventBool(payload, "reviewRequired", "review_required", "validationRequired", "validation_required")
	// Review is a property of the business assignment, not a mutable hint on
	// one event. Results and later revisions inherit an established review
	// gate so an Agent cannot clear it by emitting reviewRequired=false or by
	// superseding the reviewed revision.
	for idx := range existingAssignmentItems {
		existing := existingAssignmentItems[idx]
		existingAssignmentID := derefTeamString(existing.AssignmentID)
		if existingAssignmentID == "" {
			existingAssignmentID = existing.WorkID
		}
		if existingAssignmentID == assignmentID && existing.ReviewRequired {
			reviewRequired = true
			break
		}
	}
	status := models.TeamTaskStatusRunning
	if isWaitingTeamTaskEventStatus(normalizedTeamTaskEventStatus(payload)) &&
		!isAuthoritativeAssignmentFailure(eventType, payload) {
		status = models.TeamTaskStatusWaiting
	}
	switch stepType {
	case "assignment":
		status = models.TeamTaskStatusDispatched
	case "ack", "progress", "peer_request", "peer_reply":
		if status != models.TeamTaskStatusWaiting {
			status = models.TeamTaskStatusRunning
		}
	case "result", "final_synthesis":
		if eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
			status = models.TeamTaskStatusRunning
		} else {
			status = models.TeamTaskStatusSucceeded
		}
	case "blocker":
		if isAuthoritativeAssignmentFailure(eventType, payload) {
			status = models.TeamTaskStatusFailed
		} else {
			status = models.TeamTaskStatusWaiting
		}
	}
	now := event.CreatedAt
	if now.IsZero() {
		now = time.Now().UTC()
	}
	title := eventString(step, "title")
	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
	}
	if legacyProgressProjection {
		requiredForRoot = false
	}
	if immutableContract != nil {
		requiredForRoot = immutableContract.RequiredForRoot
		reviewRequired = immutableContract.ReviewRequired
		title = immutableContract.Title
	}
	item := &models.TeamWorkItem{
		TeamID:          team.ID,
		RootTaskID:      task.ID,
		WorkID:          workID,
		Title:           title,
		Status:          status,
		Revision:        revision,
		RequiredForRoot: requiredForRoot,
		ReviewRequired:  reviewRequired,
		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 immutableContract != nil {
		phaseID = strings.TrimSpace(derefTeamString(immutableContract.PhaseID))
	}
	if phaseID != "" {
		item.PhaseID = &phaseID
	}
	if authoritativeAssignment {
		if supersededBy := eventString(payload, "supersededBy", "superseded_by"); supersededBy != "" {
			item.SupersededBy = &supersededBy
		}
	} else if immutableContract != nil && !inheritsPriorRevisionContract {
		item.SupersededBy = immutableContract.SupersededBy
	}
	if immutableContract != nil {
		item.DependsOnJSON = immutableContract.DependsOnJSON
		item.ReviewTargetAssignmentID = immutableContract.ReviewTargetAssignmentID
		item.ReviewTargetRevision = immutableContract.ReviewTargetRevision
		item.ValidatedRevision = immutableContract.ValidatedRevision
	}
	if owner != nil {
		item.OwnerMemberID = &owner.ID
	}
	if immutableContract != nil {
		item.OwnerMemberID = immutableContract.OwnerMemberID
	}
	if status == models.TeamTaskStatusRunning || status == models.TeamTaskStatusWaiting {
		item.StartedAt = &now
	}
	if status == models.TeamTaskStatusSucceeded || status == models.TeamTaskStatusFailed {
		item.FinishedAt = &now
	}
	if status == models.TeamTaskStatusSucceeded || status == models.TeamTaskStatusFailed || eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		if encoded, err := marshalOptionalJSON(payload); err == nil {
			item.ResultJSON = encoded
		}
		if refs := explicitTeamArtifactReferences(payload); len(refs) > 0 {
			if encoded, err := json.Marshal(refs); err == nil {
				value := string(encoded)
				item.ArtifactRefsJSON = &value
			}
		}
	}
	dependencies := normalizeContextRefs(step["dependsOn"])
	if len(dependencies) == 0 {
		dependencies = normalizeContextRefs(firstTeamValue(payload, "dependsOn", "depends_on"))
	}
	if authoritativeAssignment && len(dependencies) > 0 {
		if encoded, err := json.Marshal(dependencies); err == nil {
			value := string(encoded)
			item.DependsOnJSON = &value
		}
	}
	if authoritativeAssignment && isTeamValidationAssignment(payload, owner, dependencies) {
		reviewTargetID := strings.TrimSpace(eventString(
			payload,
			"reviewedAssignmentId", "reviewed_assignment_id",
			"reviewTargetAssignmentId", "review_target_assignment_id",
			"validatedAssignmentId", "validated_assignment_id",
			"validationTargetAssignmentId", "validation_target_assignment_id",
		))
		// Explicit targets are accepted only when they agree with the issued
		// dependency contract. Older runtimes do not send a target; a sole
		// dependency is then the only safe compatibility inference.
		if reviewTargetID != "" && len(dependencies) > 0 && !slices.Contains(dependencies, reviewTargetID) {
			reviewTargetID = ""
		}
		if reviewTargetID == "" && len(dependencies) == 1 {
			reviewTargetID = dependencies[0]
		}
		if reviewTargetID != "" {
			var reviewTarget *models.TeamWorkItem
			for idx := range existingAssignmentItems {
				candidate := existingAssignmentItems[idx]
				if candidate.SupersededBy != nil || workItemBusinessID(candidate) != reviewTargetID {
					continue
				}
				if reviewTarget == nil || teamMaxInt(candidate.Revision, 1) > teamMaxInt(reviewTarget.Revision, 1) {
					clone := candidate
					reviewTarget = &clone
				}
			}
			if reviewTarget != nil {
				reviewTargetRevision := teamMaxInt(reviewTarget.Revision, 1)
				item.ReviewTargetAssignmentID = &reviewTargetID
				item.ReviewTargetRevision = &reviewTargetRevision
				if !reviewTarget.ReviewRequired {
					reviewTarget.ReviewRequired = true
					reviewTarget.UpdatedAt = now
					if err := s.repo.UpsertWorkItem(reviewTarget); err != nil {
						return err
					}
				}
			}
		}
	}
	if assignmentID != "" && revision > 1 {
		for idx := range existingAssignmentItems {
			existing := existingAssignmentItems[idx]
			existingAssignmentID := derefTeamString(existing.AssignmentID)
			if existingAssignmentID == "" {
				existingAssignmentID = existing.WorkID
			}
			if existingAssignmentID != assignmentID || teamMaxInt(existing.Revision, 1) >= revision || existing.SupersededBy != nil {
				continue
			}
			// A newer request must not invalidate a healthy in-flight attempt.
			// Failed validation remains auditable until its exact successor succeeds;
			// non-validation retries preserve the established phase recovery behavior.
			isDeferredValidationFailure := item.ReviewTargetAssignmentID != nil &&
				(existing.Status == models.TeamTaskStatusFailed || existing.Status == models.TeamTaskStatusStale)
			if isDeferredValidationFailure ||
				(existing.Status != models.TeamTaskStatusSucceeded && existing.Status != models.TeamTaskStatusFailed && existing.Status != models.TeamTaskStatusStale) {
				continue
			}
			supersededBy := workID
			existing.SupersededBy = &supersededBy
			existing.UpdatedAt = now
			if err := s.repo.UpsertWorkItem(&existing); err != nil {
				return err
			}
		}
	}
	return s.repo.UpsertWorkItem(item)
}

func isTeamValidationAssignment(payload map[string]interface{}, _ *models.TeamMember, _ []string) bool {
	if payload == nil {
		return false
	}
	if eventBool(payload,
		"validationAssignment", "validation_assignment",
		"isValidation", "is_validation",
	) {
		return true
	}
	if eventString(
		payload,
		"reviewedAssignmentId", "reviewed_assignment_id",
		"reviewTargetAssignmentId", "review_target_assignment_id",
		"validatedAssignmentId", "validated_assignment_id",
		"validationTargetAssignmentId", "validation_target_assignment_id",
		"validationKind", "validation_kind",
	) != "" {
		return true
	}
	// A member role and a dependency describe who should do work and in what
	// order; they do not create a second completion gate. Only an explicitly
	// issued validation contract may bind one assignment as another's validator.
	return false
}

func workItemBusinessID(item models.TeamWorkItem) string {
	if value := strings.TrimSpace(derefTeamString(item.AssignmentID)); value != "" {
		return value
	}
	return strings.TrimSpace(item.WorkID)
}

// applyStructuredAssignmentValidation closes a validation gate from the
// control-plane ledger: the bound validator assignment itself must have
// succeeded against the current target revision. Agent-authored verdict,
// target, revision, and hash fields are optional diagnostics and are never
// required to finish an otherwise completed workflow.
func (s *teamService) applyStructuredAssignmentValidation(
	task *models.TeamTask,
	member *models.TeamMember,
	payload map[string]interface{},
	now time.Time,
) (bool, error) {
	if s == nil || s.repo == nil || task == nil || member == nil || payload == nil ||
		!eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		return false, nil
	}
	items, err := s.repo.ListWorkItemsByRootTaskID(task.ID)
	if err != nil {
		return false, err
	}
	validatorAssignmentID := strings.TrimSpace(eventString(payload, "assignmentId", "assignment_id", "workId", "work_id"))
	for idx := range items {
		validator := items[idx]
		if validator.SupersededBy != nil || validator.OwnerMemberID == nil || *validator.OwnerMemberID != member.ID ||
			workItemBusinessID(validator) != validatorAssignmentID {
			continue
		}
		validated, validationErr := s.applyCompletedValidationWorkItem(task, items, validator, now)
		if validationErr != nil {
			return false, validationErr
		}
		superseded, supersedeErr := s.supersedeRecoveredValidationAttempts(task, items, validator, now)
		return validated || superseded, supersedeErr
	}
	return false, nil
}

func (s *teamService) applyCompletedValidationWorkItem(
	task *models.TeamTask,
	items []models.TeamWorkItem,
	validator models.TeamWorkItem,
	now time.Time,
) (bool, error) {
	if s == nil || s.repo == nil || task == nil || validator.SupersededBy != nil ||
		validator.Status != models.TeamTaskStatusSucceeded {
		return false, nil
	}
	authorizedRevision := 0
	if targetID := strings.TrimSpace(derefTeamString(validator.ReviewTargetAssignmentID)); targetID != "" {
		var target *models.TeamWorkItem
		for idx := range items {
			item := items[idx]
			if item.SupersededBy != nil || !item.ReviewRequired || workItemBusinessID(item) != targetID {
				continue
			}
			if target == nil || teamMaxInt(item.Revision, 1) > teamMaxInt(target.Revision, 1) {
				clone := item
				target = &clone
			}
		}
		if target == nil || target.Status != models.TeamTaskStatusSucceeded {
			return false, nil
		}
		targetRevision := teamMaxInt(target.Revision, 1)
		if validator.ReviewTargetRevision != nil {
			authorizedRevision = teamMaxInt(*validator.ReviewTargetRevision, 1)
		}
		if authorizedRevision > 0 && authorizedRevision != targetRevision {
			return false, nil
		}
		validatorFinishedAt := validator.UpdatedAt
		if validator.FinishedAt != nil {
			validatorFinishedAt = *validator.FinishedAt
		}
		if !target.UpdatedAt.IsZero() && !validatorFinishedAt.IsZero() && validatorFinishedAt.Before(target.UpdatedAt) {
			// A completed validator cannot validate target bytes changed afterwards.
			return false, nil
		}
		if target.ValidatedRevision != nil && *target.ValidatedRevision >= targetRevision {
			return false, nil
		}
		target.ValidatedRevision = &targetRevision
		target.UpdatedAt = now
		if err := s.repo.UpsertWorkItem(target); err != nil {
			return false, err
		}
		task.LedgerVersion++
		task.UpdatedAt = now
		return true, nil
	}
	return false, nil
}

// reconcileCompletedAssignmentValidations repairs workflows projected by older
// control-plane code that opened a validation gate but then waited for
// Agent-authored verdict fields. The persisted successful validator work item
// is the completion fact; no Agent retry is required.
func (s *teamService) reconcileCompletedAssignmentValidations(task *models.TeamTask, now time.Time) (bool, error) {
	if s == nil || s.repo == nil || task == nil || isTerminalTeamTaskStatus(task.Status) {
		return false, nil
	}
	items, err := s.repo.ListWorkItemsByRootTaskID(task.ID)
	if err != nil {
		return false, err
	}
	changed := false
	repairedTargets := map[string]struct{}{}
	for idx := range items {
		validator := items[idx]
		if validator.SupersededBy != nil || validator.Status != models.TeamTaskStatusSucceeded ||
			validator.ReviewTargetAssignmentID == nil {
			continue
		}
		targetID := strings.TrimSpace(derefTeamString(validator.ReviewTargetAssignmentID))
		if _, alreadyRepaired := repairedTargets[targetID]; alreadyRepaired {
			continue
		}
		applied, applyErr := s.applyCompletedValidationWorkItem(task, items, validator, now)
		if applyErr != nil {
			return changed, applyErr
		}
		if applied {
			repairedTargets[targetID] = struct{}{}
		}
		changed = changed || applied
		superseded, supersedeErr := s.supersedeRecoveredValidationAttempts(task, items, validator, now)
		if supersedeErr != nil {
			return changed, supersedeErr
		}
		changed = changed || superseded
	}
	if changed {
		if err := s.repo.UpdateTask(task); err != nil {
			return false, err
		}
	}
	return changed, nil
}

// supersedeRecoveredValidationAttempts retires only an older failed attempt in
// the same persisted validation lane after a newer attempt has succeeded. It
// never infers retry intent from prose or role names. Independent validations
// remain separate when their owner, phase, target, or dependency lineage differs.
func (s *teamService) supersedeRecoveredValidationAttempts(
	task *models.TeamTask,
	items []models.TeamWorkItem,
	successor models.TeamWorkItem,
	now time.Time,
) (bool, error) {
	if s == nil || s.repo == nil || task == nil || successor.SupersededBy != nil ||
		successor.Status != models.TeamTaskStatusSucceeded || successor.OwnerMemberID == nil ||
		successor.ReviewTargetAssignmentID == nil {
		return false, nil
	}
	targetID := strings.TrimSpace(derefTeamString(successor.ReviewTargetAssignmentID))
	phaseID := strings.TrimSpace(derefTeamString(successor.PhaseID))
	if targetID == "" || phaseID == "" {
		return false, nil
	}
	targetRevision := teamMaxInt(derefTeamInt(successor.ReviewTargetRevision), 1)
	changed := false
	for idx := range items {
		prior := items[idx]
		if prior.ID == successor.ID || prior.SupersededBy != nil || prior.OwnerMemberID == nil ||
			*prior.OwnerMemberID != *successor.OwnerMemberID ||
			strings.TrimSpace(derefTeamString(prior.PhaseID)) != phaseID ||
			strings.TrimSpace(derefTeamString(prior.ReviewTargetAssignmentID)) != targetID ||
			teamMaxInt(derefTeamInt(prior.ReviewTargetRevision), 1) != targetRevision ||
			(prior.Status != models.TeamTaskStatusFailed && prior.Status != models.TeamTaskStatusStale) ||
			!validationAttemptPrecedes(prior, successor) ||
			!validationRetryLineageProven(items, prior, successor, targetID) {
			continue
		}
		supersededBy := successor.WorkID
		prior.SupersededBy = &supersededBy
		prior.RequiredForRoot = false
		prior.UpdatedAt = now
		if err := s.repo.UpsertWorkItem(&prior); err != nil {
			return changed, err
		}
		items[idx] = prior
		changed = true
	}
	if changed {
		task.LedgerVersion++
		task.UpdatedAt = now
	}
	return changed, nil
}

func validationAttemptPrecedes(prior, successor models.TeamWorkItem) bool {
	priorFinished := prior.UpdatedAt
	if prior.FinishedAt != nil {
		priorFinished = *prior.FinishedAt
	}
	successorStarted := successor.CreatedAt
	if successor.StartedAt != nil {
		successorStarted = *successor.StartedAt
	}
	return prior.ID < successor.ID && (priorFinished.IsZero() || successorStarted.IsZero() || !successorStarted.Before(priorFinished))
}

func validationRetryLineageProven(items []models.TeamWorkItem, prior, successor models.TeamWorkItem, targetID string) bool {
	priorCanonical := strings.TrimSpace(derefTeamString(prior.CanonicalWorkID))
	successorCanonical := strings.TrimSpace(derefTeamString(successor.CanonicalWorkID))
	if priorCanonical != "" && priorCanonical == successorCanonical {
		return true
	}
	dependencies := teamWorkItemDependencies(successor)
	if slices.Contains(dependencies, workItemBusinessID(prior)) || slices.Contains(dependencies, prior.WorkID) {
		return true
	}
	priorFinished := prior.UpdatedAt
	if prior.FinishedAt != nil {
		priorFinished = *prior.FinishedAt
	}
	byBusinessID := make(map[string]models.TeamWorkItem, len(items))
	for idx := range items {
		item := items[idx]
		businessID := workItemBusinessID(item)
		if businessID != "" {
			byBusinessID[businessID] = item
		}
	}
	for _, dependencyID := range dependencies {
		dependency, ok := byBusinessID[dependencyID]
		if !ok || dependency.ID == prior.ID || dependency.ReviewTargetAssignmentID != nil ||
			(!priorFinished.IsZero() && dependency.CreatedAt.Before(priorFinished)) {
			continue
		}
		if dependencyID == targetID || slices.Contains(teamWorkItemDependencies(dependency), targetID) {
			return true
		}
	}
	return false
}

func derefTeamInt(value *int) int {
	if value == nil {
		return 0
	}
	return *value
}

func normalizeExistingCollaborationStep(step map[string]interface{}, team *models.Team, eventType string, payload map[string]interface{}, member *models.TeamMember, task *models.TeamTask) {
	if eventBool(payload, "leaderMediatedRouteViolation", "leader_mediated_route_violation") {
		step["type"] = "warning"
		step["status"] = "warning"
	}
	if eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		step["type"] = "result"
		if isAuthoritativeAssignmentFailure(eventType, payload) {
			step["status"] = models.TeamTaskStatusFailed
		} else {
			step["status"] = models.TeamTaskStatusSucceeded
		}
	} else if isWaitingTeamTaskEventStatus(normalizedTeamTaskEventStatus(payload)) {
		step["type"] = "progress"
		step["status"] = models.TeamTaskStatusWaiting
	}
	if eventString(step, "type") == "" {
		step["type"] = collaborationStepTypeForEvent(eventType, payload)
	}
	if eventString(step, "status") == "" {
		step["status"] = collaborationStepStatusForEvent(eventType, payload)
	}
	if eventString(step, "actor") == "" {
		step["actor"] = collaborationActorKey(payload, member)
	}
	if eventString(step, "target") == "" {
		if target := eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id", "memberId"); target != "" {
			step["target"] = target
		}
	}
	if task != nil {
		if eventString(step, "rootTaskId") == "" {
			step["rootTaskId"] = fmt.Sprintf("team-%d-task-%d", task.TeamID, task.ID)
		}
		if eventString(step, "rootMessageId") == "" {
			step["rootMessageId"] = task.MessageID
		}
	}
	if eventString(step, "eventType") == "" {
		step["eventType"] = eventType
	}
	if eventString(step, "content", "detail") == "" {
		stepType := eventString(step, "type")
		summary := eventString(step, "summary")
		if content := collaborationStepContentForEvent(stepType, payload, summary); content != "" {
			step["content"] = content
		}
	}
	if eventString(step, "source") == "" {
		step["source"] = "clawmanager"
	}
	if eventString(step, "id", "workId") == "" {
		stepType := eventString(step, "type")
		actor := eventString(step, "actor")
		target := eventString(step, "target")
		messageID := eventString(step, "messageId", "message_id")
		if messageID == "" {
			messageID = eventString(payload, "messageId", "message_id")
		}
		workID := collaborationWorkID(stepType, actor, target, messageID, payload)
		step["id"] = workID
		step["workId"] = workID
	}
	if team != nil && eventString(step, "teamId") == "" {
		step["teamId"] = strconv.Itoa(team.ID)
	}
}

func collaborationStepContentForEvent(stepType string, payload map[string]interface{}, fallback string) string {
	if stepType == "result" || stepType == "blocker" {
		if full := eventString(payload, "resultMarkdown", "result_markdown", "result", "answer", "diagnostic", "error_message", "error", "message", "text"); full != "" {
			return full
		}
	}
	if content := eventString(payload, "content", "detail", "text", "message", "resultMarkdown", "result_markdown", "result", "answer"); content != "" {
		return content
	}
	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)
	isLeaderSynthesis := eventKind == "leader_synthesis" && member != nil && isLeaderTeamMember(member)
	isAssignment := stepType == "assignment" && isAuthoritativeTeamAssignmentEvent(eventType, payload)
	isAssignmentResult := eventBool(payload, "assignmentResultOnly", "assignment_result_only")
	if isAssignment {
		assignmentID := eventString(payload, "assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
		revision := teamMaxInt(eventInt(payload, "revision"), 1)
		if assignmentID != "" {
			items, listErr := s.repo.ListWorkItemsByRootTaskID(task.ID)
			if listErr != nil {
				return false, listErr
			}
			for idx := range items {
				item := items[idx]
				if item.SupersededBy != nil || !isTerminalTeamTaskStatus(item.Status) || teamMaxInt(item.Revision, 1) != revision {
					continue
				}
				if item.WorkID == assignmentID || derefTeamString(item.AssignmentID) == assignmentID || derefTeamString(item.CanonicalWorkID) == assignmentID {
					// A duplicate delivery of the same assignment contract is an
					// idempotent transport retry, not a new workflow phase.
					isAssignment = false
					break
				}
			}
		}
	}
	if !isPlan && !isLeaderSynthesis && !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")
		phaseDispositionPolicy := eventString(payload, "phaseDispositionPolicy", "phase_disposition_policy")
		if plan, ok := payload["workflowPlan"].(map[string]interface{}); ok {
			if nested := firstTeamValue(plan, "phases", "workflowPhases", "workflow_phases"); nested != nil {
				phaseValues = nested
			}
			if phaseDispositionPolicy == "" {
				phaseDispositionPolicy = eventString(plan, "phaseDispositionPolicy", "phase_disposition_policy")
			}
		}
		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
				} else if strings.EqualFold(strings.TrimSpace(phaseDispositionPolicy), teamPhaseCompletionPolicyExplicitV1) {
					policy := teamPhaseCompletionPolicyExplicitV1
					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
		}
	}
	if isLeaderSynthesis {
		// A Leader may explicitly seal an otherwise completed phase before
		// proposing root completion. This is a stateful workflow decision, not
		// merely chat prose, so the completion evaluator can distinguish it from
		// an accidental early summary.
		if workflowState := eventString(payload, "workflowState", "workflow_state"); workflowState == teamWorkflowStateSynthesizing {
			if task.WorkflowState != teamWorkflowStateSynthesizing {
				task.WorkflowState = teamWorkflowStateSynthesizing
				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 isLeaderSynthesis && phaseID != "" {
		// A Leader synthesis event is execution evidence for a Leader-owned
		// phase. Complete that phase only after every required worker contract
		// in it has completed; prose alone can never skip active work.
		items, err := s.repo.ListWorkItemsByRootTaskID(task.ID)
		if err != nil {
			return changed, err
		}
		phaseIncomplete := false
		for idx := range items {
			item := items[idx]
			if item.SupersededBy != nil || strings.TrimSpace(derefTeamString(item.PhaseID)) != phaseID ||
				!item.RequiredForRoot || (item.OwnerMemberID != nil && *item.OwnerMemberID == task.TargetMemberID) {
				continue
			}
			if item.Status != models.TeamTaskStatusSucceeded {
				phaseIncomplete = true
				break
			}
		}
		if !phaseIncomplete {
			phases, err := s.repo.ListWorkflowPhasesByRootTaskID(task.ID)
			if err != nil {
				return changed, err
			}
			for idx := range phases {
				phase := phases[idx]
				if phase.PhaseID != phaseID || phase.PlanVersion != planVersion ||
					phase.Status == teamPhaseStatusCompleted || 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 changed, err
				}
				changed = true
				break
			}
		}
	}
	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
			} else if phase.DecisionRequired {
				phase.Status = teamPhaseStatusAwaitingLeaderDecision
			} else {
				phase.Status = teamPhaseStatusCompleted
				phase.CompletedAt = &now
			}
			phase.UpdatedAt = now
			if err := s.repo.UpsertWorkflowPhase(&phase); err != nil {
				return changed, err
			}
			changed = true
			break
		}
		// A result closes only its own phase. Derive the root state from every
		// latest required worker item so finishing phase 1 cannot temporarily
		// announce a Leader decision while phase 2 is already running.
		nextWorkflowState := teamWorkflowStateAwaitingLeaderDecision
		nextCurrentPhaseID := ""
		for idx := range items {
			item := items[idx]
			if item.SupersededBy != nil || item.OwnerMemberID == nil ||
				*item.OwnerMemberID == task.TargetMemberID ||
				!(item.RequiredForRoot || item.AssignmentID == nil) ||
				item.Status == models.TeamTaskStatusSucceeded {
				continue
			}
			nextWorkflowState = teamWorkflowStateAwaitingPhaseResults
			nextCurrentPhaseID = strings.TrimSpace(derefTeamString(item.PhaseID))
			break
		}
		if task.WorkflowState != nextWorkflowState {
			task.WorkflowState = nextWorkflowState
			changed = true
		}
		if nextCurrentPhaseID != "" &&
			(task.CurrentPhaseID == nil || strings.TrimSpace(*task.CurrentPhaseID) != nextCurrentPhaseID) {
			task.CurrentPhaseID = &nextCurrentPhaseID
			changed = true
		} else if nextCurrentPhaseID == "" && task.CurrentPhaseID != nil {
			task.CurrentPhaseID = nil
			changed = true
		}
	}
	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")))
	if eventInt(payload, "deliverySemanticsVersion", "delivery_semantics_version") > 0 {
		switch strings.ToLower(strings.TrimSpace(eventString(payload, "businessDeliveryKind", "business_delivery_kind"))) {
		case "context", "peer_request":
			return "peer_request"
		case "notification", "monitor", "ambiguous":
			return "progress"
		}
	}
	switch eventKind {
	case "leader_plan", "leader_progress", "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"
	}
	if eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		return "result"
	}
	if eventBool(payload, "leaderDispatchOnly", "leader_dispatch_only") {
		return "assignment"
	}
	if isTeamTaskCompletionSignal(eventType, status, payload) {
		return "result"
	}
	if isNonAuthoritativeDispatchFailure(eventType, payload) {
		return "warning"
	}
	switch eventType {
	case "leader_plan", "leader_progress", "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":
		return "assignment"
	case "peer_request", "peer_review_request":
		return "peer_request"
	case "peer_reply":
		return "peer_reply"
	case "task_received":
		return "ack"
	case "task_started", "task_progress", "progress":
		return "progress"
	case "task_completed", "completion":
		return "progress"
	case "task_failed", "message_failed", "task_stale":
		if !isTeamTaskFailureSignal(eventType, status, payload) && eventType != "task_stale" {
			return "warning"
		}
		return "blocker"
	case "message_warning":
		return "warning"
	case "reply":
		if eventBool(payload, "final", "isFinal", "complete", "completed", "taskCompleted") || isFinalCompletionPayload(payload) {
			return "result"
		}
		return "progress"
	default:
		if eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id") != "" {
			return "progress"
		}
		return ""
	}
}

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"
	}
	if eventBool(payload, "assignmentResultOnly", "assignment_result_only") {
		if isAuthoritativeAssignmentFailure(eventType, payload) {
			return models.TeamTaskStatusFailed
		}
		return models.TeamTaskStatusSucceeded
	}
	if eventBool(payload, "leaderDispatchOnly", "leader_dispatch_only") {
		return models.TeamTaskStatusDispatched
	}
	if isTeamTaskCompletionSignal(eventType, status, payload) {
		return models.TeamTaskStatusSucceeded
	}
	if isSuccessfulTeamTaskEventStatus(status) {
		switch eventType {
		case "task_completed", "completion", "task_failed", "message_failed":
			return models.TeamTaskStatusRunning
		default:
			return models.TeamTaskStatusSucceeded
		}
	}
	if isAuthoritativeAssignmentFailure(eventType, payload) {
		if isNonAuthoritativeDispatchFailure(eventType, payload) {
			return "warning"
		}
		return models.TeamTaskStatusFailed
	}
	if isWaitingTeamTaskEventStatus(status) {
		return models.TeamTaskStatusWaiting
	}
	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":
		return models.TeamTaskStatusDispatched
	case "task_received":
		return "acknowledged"
	case "task_started", "task_progress", "progress", "reply", "peer_reply":
		return models.TeamTaskStatusRunning
	case "message_warning":
		return "warning"
	default:
		if status != "" {
			return status
		}
		return "observed"
	}
}

func collaborationActorKey(payload map[string]interface{}, member *models.TeamMember) string {
	if actor := eventString(payload, "from", "sourceMemberId", "source_member_id", "sender", "senderMemberId", "sender_member_id", "memberId", "member_id"); actor != "" {
		return actor
	}
	if member != nil {
		return member.MemberKey
	}
	return "system"
}

func collaborationStepTitle(stepType, actor, target string, payload map[string]interface{}) string {
	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 "leader_progress":
		return "Leader updates progress"
	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 != "" {
			return "Assign to " + target
		}
		return "Assign subtask"
	case "peer_request":
		if target != "" {
			return actor + " asks " + target
		}
		return "Peer request"
	case "peer_reply":
		return actor + " replies"
	case "ack":
		return actor + " accepted task"
	case "progress":
		return actor + " updates progress"
	case "result":
		return actor + " delivers result"
	case "blocker":
		return actor + " reports blocker"
	case "warning":
		return actor + " reports warning"
	default:
		return stepType
	}
}

func collaborationWorkID(stepType, actor, target, messageID string, payload map[string]interface{}) string {
	if id := eventString(payload, "workId", "work_id", "stepId", "step_id", "subtaskId", "subtask_id"); id != "" {
		return id
	}
	parts := []string{stepType, actor, target}
	if messageID != "" {
		parts = append(parts, messageID)
	}
	if len(parts) == 0 {
		return "team-step"
	}
	return strings.ToLower(strings.Trim(strings.Map(func(r rune) rune {
		if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' {
			return r
		}
		if r == '-' || r == '_' {
			return '-'
		}
		return '-'
	}, strings.Join(parts, "-")), "-"))
}

func inferCollaborationPhase(stepType, title, summary string, payload map[string]interface{}) string {
	if phase := eventString(payload, "phase", "stage"); phase != "" {
		return phase
	}
	text := strings.ToLower(title + " " + summary)
	switch {
	case strings.Contains(text, "review") || strings.Contains(text, "verify"):
		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, "璋冪爺"):
		return "research"
	case stepType == "assignment":
		return "decomposition"
	case stepType == "result":
		return "delivery"
	default:
		return "execution"
	}
}

func isFinalCompletionPayload(payload map[string]interface{}) bool {
	status := normalizedTeamTaskEventStatus(payload)
	return hasAuthoritativeTeamCompletionPayload("reply", status, payload)
}

func looksLikeOpaqueRuntimeTaskID(value string) bool {
	normalized := strings.TrimSpace(value)
	return regexp.MustCompile(`^(task[-_][a-z0-9-]+|team-\d+-task-\d+)$`).MatchString(strings.ToLower(normalized))
}

func normalizeFinalReplyTaskEvent(eventType string, payload map[string]interface{}, task *models.TeamTask, _ *models.TeamMember) string {
	if task == nil || !strings.EqualFold(strings.TrimSpace(eventType), "reply") {
		return eventType
	}
	hasCompletionTool := hasTeamTaskCompletionToolCall(payload)
	if !hasCompletionTool {
		return eventType
	}
	if !eventBool(payload, "final", "isFinal", "complete", "completed", "taskCompleted") {
		return eventType
	}
	if !teamEventHasBody(payload) {
		return eventType
	}
	payload["originalEvent"] = eventType
	payload["event"] = "task_completed"
	payload["type"] = "task_completed"
	payload["status"] = "succeeded"
	payload["availability"] = models.TeamMemberAvailabilityIdle
	payload["runtimeStatus"] = "succeeded"
	if eventString(payload, "resultMarkdown") == "" {
		if text := eventString(payload, "text", "result", "summary"); text != "" {
			payload["resultMarkdown"] = text
		}
	}
	if eventString(payload, "summary") == "" {
		if text := eventString(payload, "text", "resultMarkdown", "result"); text != "" {
			payload["summary"] = text
		}
	}
	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 {
		// Old Runtime prose follows the same terminal ownership rule as new
		// Runtime prose. Keep it visible for compatibility, but never infer
		// completion from wording.
		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
	}
	if member.ID != task.TargetMemberID {
		return false
	}
	status := normalizedTeamTaskEventStatus(payload)
	if isTeamTaskFailureSignal("reply", status, payload) || isTeamTaskRunningSignal("reply", status, payload) {
		return false
	}
	resultText := directTaskCompletionReplyText(payload)
	if resultText == "" || isInterimOrDelegationReplyText(resultText) {
		return false
	}
	if eventBool(payload, "final", "isFinal", "complete", "completed", "taskCompleted") {
		return true
	}
	if eventString(payload, "resultMarkdown", "result_markdown", "result", "answer") != "" {
		return true
	}
	switch status {
	case "succeeded", "success", "completed", "complete", "done", "finished", "ok":
		return true
	}
	compact := strings.TrimSpace(resultText)
	compact = strings.Join(strings.Fields(compact), "")
	return len([]rune(compact)) >= 36 && looksLikeSubstantialFinalReply(resultText)
}

func directTaskCompletionReplyText(payload map[string]interface{}) string {
	for _, record := range eventRecordCandidates(payload) {
		if text := eventString(record, "resultMarkdown", "result_markdown", "result", "answer", "text", "message", "summary"); text != "" {
			return text
		}
	}
	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 == "" {
		return true
	}
	if looksLikeLeaderDispatchOnlyText(normalized) {
		return true
	}
	if looksLikeFinalResultText(normalized) {
		return false
	}
	lower := strings.ToLower(normalized)
	compact := strings.ToLower(strings.Join(strings.Fields(normalized), ""))
	if len([]rune(compact)) <= 12 {
		return true
	}
	for _, prefix := range interimReplyPrefixes() {
		if strings.HasPrefix(lower, strings.ToLower(prefix)) || strings.HasPrefix(normalized, prefix) {
			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 == "" {
		return false
	}
	if strings.ContainsAny(normalized, "#*>|`") {
		return true
	}
	if strings.ContainsAny(normalized, ";\n") || strings.Contains(normalized, "\u3002") || strings.Contains(normalized, "\uff1b") || strings.Contains(normalized, "\uff0c") {
		return true
	}
	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
	}
	if eventString(payload, "toolCallName", "tool_call_name", "calledTool", "called_tool") == teamTaskCompletionTool {
		return true
	}
	for _, key := range []string{"toolCall", "tool_call", "function_call"} {
		record, ok := payload[key].(map[string]interface{})
		if !ok || record == nil {
			continue
		}
		if eventString(record, "name", "function", "functionName", "function_name", "tool", "toolName", "tool_name") == teamTaskCompletionTool {
			return true
		}
	}
	return false
}

func eventRecordCandidates(payload map[string]interface{}) []map[string]interface{} {
	if payload == nil {
		return nil
	}
	records := []map[string]interface{}{payload}
	for _, key := range []string{"sent", "metadata", "data", "envelope", "task", "toolCall", "tool_call", "function_call"} {
		if record, ok := payload[key].(map[string]interface{}); ok && record != nil {
			records = append(records, record)
		}
	}
	return records
}

func (s *teamService) enrichOutboundEventFromInbox(teamID int, bus *redisBus, payload map[string]interface{}, messageID string) (map[string]interface{}, error) {
	targetMember := eventString(payload, "to", "recipient", "target", "targetMemberId", "target_member_id")
	if targetMember == "" {
		return payload, nil
	}
	var lastErr error
	for attempt := 0; attempt < 5; attempt++ {
		if attempt > 0 {
			time.Sleep(100 * time.Millisecond)
		}
		messages, err := bus.XRevRange(context.Background(), teamInboxKey(teamID, targetMember), 100)
		if err != nil {
			lastErr = err
			continue
		}
		for _, inboxMessage := range messages {
			if !redisStreamMessageMatches(inboxMessage, messageID) {
				continue
			}
			envelope := mergeRedisEventPayload(inboxMessage.Fields)
			return mergeMissingEventFields(payload, envelope), nil
		}
	}
	return payload, lastErr
}

func redisStreamMessageMatches(message redisStreamMessage, messageID string) bool {
	if strings.TrimSpace(message.Fields["message_id"]) == messageID {
		return true
	}
	payload := mergeRedisEventPayload(message.Fields)
	return eventString(payload, "message_id", "messageId") == messageID
}

func mergeMissingEventFields(base map[string]interface{}, extra map[string]interface{}) map[string]interface{} {
	merged := map[string]interface{}{}
	for key, value := range base {
		merged[key] = value
	}
	for key, value := range extra {
		if existing, ok := merged[key]; !ok || isEmptyEventValue(existing) {
			merged[key] = value
		}
	}
	if metadata, ok := extra["metadata"].(map[string]interface{}); ok {
		for key, value := range metadata {
			if existing, ok := merged[key]; !ok || isEmptyEventValue(existing) {
				merged[key] = value
			}
		}
	}
	return merged
}

func isEmptyEventValue(value interface{}) bool {
	if value == nil {
		return true
	}
	if text, ok := value.(string); ok {
		return strings.TrimSpace(text) == ""
	}
	return false
}

func isOutboundTeamEvent(eventType string) bool {
	switch eventType {
	case "outbound", "task_assigned":
		return true
	default:
		return false
	}
}

func teamEventHasBody(payload map[string]interface{}) bool {
	if eventString(payload, "text", "title", "prompt", "instruction", "instructions", "summary", "resultMarkdown") != "" {
		return true
	}
	for idx, record := range eventRecordCandidates(payload) {
		if idx == 0 {
			continue
		}
		if eventString(record, "text", "title", "prompt", "instruction", "instructions", "summary", "resultMarkdown") != "" {
			return true
		}
	}
	return false
}

func (s *teamService) normalizeTeamArtifactReferences(team *models.Team, payload map[string]interface{}) {
	if s == nil || team == nil || payload == nil {
		return
	}
	physicalRoot := filepath.ToSlash(filepath.Clean(s.teamRuntimeSharedPathFor(team.UserID, team.ID)))
	logicalRoot := strings.TrimRight(strings.TrimSpace(team.SharedMountPath), "/")
	if logicalRoot == "" {
		logicalRoot = teamSharedMountPath
	}
	normalizePath := func(raw string) string {
		value := strings.TrimSpace(raw)
		if value == "" {
			return raw
		}
		value = strings.TrimPrefix(value, "file://")
		value = strings.ReplaceAll(value, "\\", "/")
		switch {
		case strings.HasPrefix(value, logicalRoot+"/"):
			return teamSharedMountPath + strings.TrimPrefix(value, logicalRoot)
		case strings.HasPrefix(value, teamSharedMountPath+"/"):
			return value
		case strings.HasPrefix(value, physicalRoot+"/"):
			return teamSharedMountPath + strings.TrimPrefix(value, physicalRoot)
		case strings.HasPrefix(value, "$CLAWMANAGER_TEAM_SHARED_DIR/"):
			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/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:
			return raw
		}
	}
	normalizeText := func(raw string) string {
		if strings.TrimSpace(raw) == "" {
			return raw
		}
		// Free-form fields may contain serialized JSON and Markdown. Replacing
		// every backslash corrupts escapes such as `\n` into `/n`, after which
		// artifact extraction sees JSON syntax as part of the filename.
		text := raw
		if physicalRoot != "" {
			text = strings.ReplaceAll(text, "file://"+physicalRoot+"/", teamSharedMountPath+"/")
			text = strings.ReplaceAll(text, physicalRoot+"/", teamSharedMountPath+"/")
		}
		text = strings.ReplaceAll(text, "$CLAWMANAGER_TEAM_SHARED_DIR/", teamSharedMountPath+"/")
		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{}
	normalizeValue = func(value interface{}) interface{} {
		switch typed := value.(type) {
		case string:
			normalized := normalizePath(typed)
			if normalized != typed {
				return normalized
			}
			return normalizeText(typed)
		case []interface{}:
			for idx := range typed {
				typed[idx] = normalizeValue(typed[idx])
			}
			return typed
		case map[string]interface{}:
			for key, nested := range typed {
				typed[key] = normalizeValue(nested)
			}
			return typed
		default:
			return value
		}
	}
	for key, value := range payload {
		payload[key] = normalizeValue(value)
	}
}

func (s *teamService) missingTeamArtifactReferences(team *models.Team, payload map[string]interface{}) []string {
	if s == nil || team == nil || payload == nil {
		return nil
	}
	// Completion state is validated only against the structured artifactRefs
	// contract. Paths mentioned in prose are useful for rendering and legacy
	// discovery, but punctuation or explanatory text must never invent a hard
	// completion dependency.
	refs := explicitTeamArtifactReferences(payload)
	if len(refs) == 0 {
		return nil
	}
	root := filepath.Clean(s.teamRuntimeSharedPathFor(team.UserID, team.ID))
	missing := make([]string, 0)
	seen := map[string]struct{}{}
	for _, ref := range refs {
		rel := strings.TrimPrefix(strings.TrimSpace(ref), teamSharedMountPath+"/")
		rel = strings.TrimPrefix(rel, "/")
		if rel == "" || strings.HasPrefix(rel, "..") {
			continue
		}
		target := filepath.Clean(filepath.Join(root, filepath.FromSlash(rel)))
		if target != root && !strings.HasPrefix(target, root+string(filepath.Separator)) {
			continue
		}
		if _, ok := seen[ref]; ok {
			continue
		}
		seen[ref] = struct{}{}
		if info, err := os.Stat(target); err != nil {
			if os.IsNotExist(err) {
				missing = append(missing, ref)
			}
		} else if !info.Mode().IsRegular() {
			missing = append(missing, ref)
		}
	}
	return missing
}

func explicitTeamArtifactReferences(payload map[string]interface{}) []string {
	if payload == nil {
		return nil
	}
	value, ok := payload["artifactRefs"]
	if !ok {
		value = payload["artifact_refs"]
	}
	refs := make([]string, 0)
	seen := map[string]struct{}{}
	appendRef := func(raw interface{}) {
		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
		}
		seen[ref] = struct{}{}
		refs = append(refs, ref)
	}
	switch typed := value.(type) {
	case []interface{}:
		for _, item := range typed {
			appendRef(item)
		}
	case []string:
		for _, item := range typed {
			appendRef(item)
		}
	}
	return refs
}

func collectTeamArtifactReferences(value interface{}) []string {
	refs := make([]string, 0)
	// Stop at Markdown delimiters and JSON syntax. Runtime adapters can embed
	// 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/(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 := trimTeamArtifactReferenceToken(match)
				if ref == "" || !looksLikeTeamArtifactFileReference(ref) {
					continue
				}
				if _, exists := seen[ref]; exists {
					continue
				}
				seen[ref] = struct{}{}
				refs = append(refs, ref)
			}
		case []interface{}:
			for _, item := range typed {
				walk(item)
			}
		case map[string]interface{}:
			for _, item := range typed {
				walk(item)
			}
		}
	}
	walk(value)
	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"']+)`)
	seen := map[string]struct{}{}
	var walk func(interface{})
	walk = func(current interface{}) {
		switch typed := current.(type) {
		case string:
			for _, match := range pattern.FindAllStringSubmatch(typed, -1) {
				if len(match) < 3 {
					continue
				}
				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
				}
				if _, exists := seen[ref]; exists {
					continue
				}
				seen[ref] = struct{}{}
				refs = append(refs, ref)
			}
		case []interface{}:
			for _, item := range typed {
				walk(item)
			}
		case map[string]interface{}:
			for _, item := range typed {
				walk(item)
			}
		}
	}
	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
	team.UpdatedAt = time.Now().UTC()
	_ = s.repo.UpdateTeam(team)
	return cause
}

func (s *teamService) rollbackTeamCreation(userID int, team *models.Team, cause error) error {
	members, err := s.repo.ListMembersByTeamID(team.ID)
	if err != nil {
		fmt.Printf("Warning: failed to list Team %d members during create rollback: %v\n", team.ID, err)
	}
	for idx := range members {
		member := members[idx]
		if member.InstanceID != nil && *member.InstanceID > 0 {
			if err := s.instanceService.Delete(*member.InstanceID); err != nil {
				fmt.Printf("Warning: failed to delete Team %d member %s instance %d during create rollback: %v\n", team.ID, member.MemberKey, *member.InstanceID, err)
			}
		}
		member.Status = models.TeamMemberStatusDeleted
		member.CurrentTaskID = nil
		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 {
			fmt.Printf("Warning: failed to delete Team %d secret during create rollback: %v\n", team.ID, err)
		}
	}
	if err := s.configMapService.DeleteConfigMap(ctx, userID, s.teamConfigMapName(team.ID)); err != nil {
		fmt.Printf("Warning: failed to delete Team %d configmap during create rollback: %v\n", team.ID, err)
	}
	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()
	if err := s.repo.UpdateTeam(team); err != nil {
		fmt.Printf("Warning: failed to mark Team %d deleted during create rollback: %v\n", team.ID, err)
	}
	return cause
}

func teamTaskPayloads(tasks []models.TeamTask) []TeamTaskPayload {
	result := make([]TeamTaskPayload, 0, len(tasks))
	for _, task := range tasks {
		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
	}
	if limit > maxLimit {
		return maxLimit
	}
	return limit
}

func nextTeamTaskBeforeID(tasks []TeamTaskPayload) *int {
	if len(tasks) == 0 {
		return nil
	}
	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
}

func nextTeamEventBeforeID(events []TeamEventPayload) *int {
	if len(events) == 0 {
		return nil
	}
	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}
	if strings.TrimSpace(task.PayloadJSON) != "" {
		if err := json.Unmarshal([]byte(task.PayloadJSON), &payload.Payload); err != nil {
			return nil, err
		}
	}
	if task.ResultJSON != nil && strings.TrimSpace(*task.ResultJSON) != "" {
		if err := json.Unmarshal([]byte(*task.ResultJSON), &payload.Result); err != nil {
			return nil, err
		}
	}
	return &payload, nil
}

func teamEventPayloads(events []models.TeamEvent) []TeamEventPayload {
	parsed := make([]TeamEventPayload, 0, len(events))
	for _, event := range events {
		payload := TeamEventPayload{TeamEvent: event}
		if event.PayloadJSON != nil && strings.TrimSpace(*event.PayloadJSON) != "" {
			_ = json.Unmarshal([]byte(*event.PayloadJSON), &payload.Payload)
		}
		sanitizePublicTeamEventPayload(event.EventType, payload.Payload)
		normalizeTeamChatNarrativeDisplay(payload.Payload)
		parsed = append(parsed, payload)
	}

	// Runtime final turns carry two durable facts: a visible assistant narrative
	// and a hidden completion proposal that owns workflow state plus discovered
	// artifacts. Preserve both in storage, but project one chat bubble by merging
	// the completion artifacts into its exact same-turn narrative. If the paired
	// narrative is absent, retain the completion as a safe compatibility fallback.
	narrativeByTurn := make(map[string]int)
	duplicateNarratives := make(map[int]struct{})
	for idx := range parsed {
		payload := parsed[idx]
		if strings.EqualFold(eventString(payload.Payload, "eventKind", "event_kind", "kind"), "agent_narrative") {
			continue
		}
		if !isStructuredAgentNarrativeEvent(payload.EventType, payload.Payload) ||
			isStructurallySuppressedTeamNarrative(payload.Payload) ||
			eventBool(payload.Payload, "silentControlReply", "silent_control_reply") {
			continue
		}
		if key := teamChatPresentationTurnKey(payload.TeamEvent, payload.Payload); key != "" {
			if existingIndex, exists := narrativeByTurn[key]; exists {
				mergeTeamChatPresentationArtifacts(parsed[existingIndex].Payload, payload.Payload)
				duplicateNarratives[idx] = struct{}{}
				continue
			}
			narrativeByTurn[key] = idx
		}
	}

	result := make([]TeamEventPayload, 0, len(parsed))
	for idx := range parsed {
		payload := parsed[idx]
		if strings.EqualFold(eventString(payload.Payload, "eventKind", "event_kind", "kind"), "agent_narrative") {
			continue
		}
		if _, duplicate := duplicateNarratives[idx]; duplicate ||
			eventBool(payload.Payload, "silentControlReply", "silent_control_reply") {
			continue
		}
		chatPolicy := strings.ToLower(strings.TrimSpace(eventString(payload.Payload, "chatPolicy", "chat_policy")))
		if eventBool(payload.Payload, "finalDeliveredByNarrative", "final_delivered_by_narrative") &&
			runtimeTurnResultText(payload.Payload) != "" {
			// Older v4/v5 rows hid the structured result in favour of a paired
			// narrative. Narratives are no longer public, so expose the durable
			// completion as the historical compatibility copy.
			payload.Payload["chatPolicy"] = "visible"
			payload.Payload["visibleToChat"] = true
			payload.Payload["visible_to_chat"] = true
			payload.Payload["chatKind"] = "final_delivery"
			delete(payload.Payload, "finalDeliveredByNarrative")
			delete(payload.Payload, "final_delivered_by_narrative")
			chatPolicy = "visible"
		}
		if eventBool(payload.Payload, "finalDeliveredByNarrative", "final_delivered_by_narrative") && chatPolicy == "hidden" {
			if narrativeIndex, ok := narrativeByTurn[teamChatPresentationTurnKey(payload.TeamEvent, payload.Payload)]; ok && narrativeIndex != idx {
				mergeTeamChatPresentationArtifacts(parsed[narrativeIndex].Payload, payload.Payload)
				continue
			}
		}
		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(payload.EventType)), eventKind, payload.Payload)
		if payload.EventType == "member_result_confirmed" || (!business && (chatPolicy == "hidden" || (hiddenDefined && !hidden && chatPolicy == ""))) {
			continue
		}
		result = append(result, payload)
	}
	return result
}

func normalizeTeamChatNarrativeDisplay(payload map[string]interface{}) {
	if payload == nil {
		return
	}
	original := runtimeTurnResultText(payload)
	if original == "" {
		return
	}
	trimmed := strings.TrimSpace(original)
	normalized := trimmed
	if teamRuntimeControlReplyPattern.MatchString(trimmed) {
		normalized = ""
	} else if strings.EqualFold(eventString(payload, "eventKind", "event_kind", "kind"), "agent_narrative") {
		normalized = strings.TrimSpace(teamTrailingSilentReplyPattern.ReplaceAllString(trimmed, ""))
	}
	if normalized == original {
		return
	}
	for _, key := range []string{"text", "content"} {
		if value, ok := payload[key].(string); ok && strings.TrimSpace(value) == strings.TrimSpace(original) {
			payload[key] = normalized
		}
	}
	payload["displayControlTokenStripped"] = true
	if normalized == "" {
		payload["silentControlReply"] = true
		payload["chatPolicy"] = "hidden"
		payload["visibleToChat"] = false
		payload["visible_to_chat"] = false
	}
}

func teamChatPresentationTurnKey(event models.TeamEvent, payload map[string]interface{}) string {
	if payload == nil {
		return ""
	}
	sourceMessageID := eventString(payload,
		"sourceMessageId", "source_message_id", "inReplyTo", "in_reply_to")
	assignmentID := eventString(payload,
		"assignmentId", "assignment_id", "canonicalWorkId", "canonical_work_id", "workId", "work_id")
	body := strings.TrimSpace(strings.ReplaceAll(runtimeTurnResultText(payload), "\r\n", "\n"))
	if sourceMessageID == "" || body == "" {
		return ""
	}
	memberID := 0
	if event.MemberID != nil {
		memberID = *event.MemberID
	}
	taskID := 0
	if event.TaskID != nil {
		taskID = *event.TaskID
	}
	sum := sha256.Sum256([]byte(body))
	return fmt.Sprintf("%d:%d:%s:%s:%x", taskID, memberID, sourceMessageID, assignmentID, sum[:])
}

func mergeTeamChatPresentationArtifacts(target, source map[string]interface{}) {
	if target == nil || source == nil {
		return
	}
	refs := uniqueTeamStrings(append(
		normalizeContextRefs(firstTeamValue(target, "artifactRefs", "artifact_refs")),
		normalizeContextRefs(firstTeamValue(source, "artifactRefs", "artifact_refs"))...,
	))
	if len(refs) > 0 {
		target["artifactRefs"] = refs
		target["presentationArtifactsMerged"] = true
	}
	if firstTeamValue(target, "artifactMetadata", "artifact_metadata") == nil {
		if metadata := firstTeamValue(source, "artifactMetadata", "artifact_metadata"); metadata != nil {
			target["artifactMetadata"] = metadata
		}
	}
}

// Deferred completion reports must remain available to the internal ledger
// reconciler, but they are not accepted deliveries and must never cross the
// public Team-event boundary. Older Runtime events also retain their original
// wire payload as a JSON string; sanitize that copy as well so clients cannot
// accidentally restore a result field that the completion evaluator removed.
func sanitizePublicTeamEventPayload(eventType string, payload map[string]interface{}) {
	if payload == nil {
		return
	}
	eventKind := strings.ToLower(strings.TrimSpace(eventString(payload, "eventKind", "event_kind", "kind", "chatKind", "chat_kind")))
	if strings.ToLower(strings.TrimSpace(eventType)) != "completion_deferred" && eventKind != "completion_deferred" {
		return
	}
	removeDeferredCompletionResultFields(payload)
	raw, ok := payload["payload"].(string)
	if !ok || strings.TrimSpace(raw) == "" {
		return
	}
	var embedded map[string]interface{}
	if json.Unmarshal([]byte(raw), &embedded) != nil {
		return
	}
	removeDeferredCompletionResultFields(embedded)
	if encoded, err := json.Marshal(embedded); err == nil {
		payload["payload"] = string(encoded)
	}
}

func removeDeferredCompletionResultFields(payload map[string]interface{}) {
	if payload == nil {
		return
	}
	for _, key := range []string{
		"resultMarkdown", "result_markdown", "result", "answer",
		"completionDraftMarkdown", "completion_draft_markdown",
		"completionDraftSummary", "completion_draft_summary",
	} {
		delete(payload, key)
	}
	for key, value := range payload {
		if nested, ok := value.(map[string]interface{}); ok {
			if strings.EqualFold(key, "collaborationStep") || strings.EqualFold(key, "collaboration_step") {
				for _, resultKey := range []string{"content", "result", "resultMarkdown", "result_markdown", "answer"} {
					delete(nested, resultKey)
				}
			}
			removeDeferredCompletionResultFields(nested)
		}
	}
}

func teamWorkItemPayloads(items []models.TeamWorkItem) []TeamWorkItemPayload {
	result := make([]TeamWorkItemPayload, 0, len(items))
	for _, item := range items {
		payload := TeamWorkItemPayload{TeamWorkItem: item}
		if item.DependsOnJSON != nil && strings.TrimSpace(*item.DependsOnJSON) != "" {
			_ = json.Unmarshal([]byte(*item.DependsOnJSON), &payload.DependsOn)
		}
		if item.ResultJSON != nil && strings.TrimSpace(*item.ResultJSON) != "" {
			_ = json.Unmarshal([]byte(*item.ResultJSON), &payload.Result)
		}
		if item.ArtifactRefsJSON != nil && strings.TrimSpace(*item.ArtifactRefsJSON) != "" {
			_ = json.Unmarshal([]byte(*item.ArtifactRefsJSON), &payload.ArtifactRefs)
		}
		result = append(result, payload)
	}
	return result
}

func mergeRedisEventPayload(fields map[string]string) map[string]interface{} {
	payload := map[string]interface{}{}
	if raw := strings.TrimSpace(fields["payload"]); raw != "" {
		_ = json.Unmarshal([]byte(raw), &payload)
	}
	for key, value := range fields {
		if _, exists := payload[key]; !exists {
			payload[key] = value
		}
	}
	return payload
}

func eventString(payload map[string]interface{}, keys ...string) string {
	for _, key := range keys {
		value, ok := payload[key]
		if !ok || value == nil {
			continue
		}
		switch typed := value.(type) {
		case string:
			return strings.TrimSpace(typed)
		case float64:
			return strconv.Itoa(int(typed))
		case int:
			return strconv.Itoa(typed)
		default:
			return strings.TrimSpace(fmt.Sprintf("%v", typed))
		}
	}
	return ""
}

func eventBool(payload map[string]interface{}, keys ...string) bool {
	for _, key := range keys {
		value, ok := payload[key]
		if !ok || value == nil {
			continue
		}
		switch typed := value.(type) {
		case bool:
			return typed
		case string:
			switch strings.ToLower(strings.TrimSpace(typed)) {
			case "1", "true", "yes", "y", "on":
				return true
			case "0", "false", "no", "n", "off":
				return false
			}
		case float64:
			return typed != 0
		case int:
			return typed != 0
		default:
			text := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", typed)))
			if text == "true" || text == "yes" || text == "1" {
				return true
			}
		}
	}
	return false
}

func resetTeamMemberAssignmentProjection(member *models.TeamMember, eventType string) {
	if member == nil {
		return
	}
	switch strings.ToLower(strings.TrimSpace(eventType)) {
	case "task_received", "task_started":
		// RuntimeStatus and the new task identity are projected immediately
		// afterwards from this event. Clear only assignment-scoped display
		// fields so a new turn never shows the previous result/blocker.
		member.Progress = 0
		member.RuntimeTaskID = nil
		member.RuntimeIntent = nil
		member.BlockedReason = nil
		member.LastSummary = nil
	}
}

func applyTeamMemberRuntimeProjection(member *models.TeamMember, payload map[string]interface{}, eventType string) {
	if member == nil {
		return
	}
	nonAuthoritativeDispatchWarning := isNonAuthoritativeDispatchWarning(eventType, payload)
	status := normalizedTeamTaskEventStatus(payload)
	availability := normalizeTeamAvailability(eventString(payload, "availability", "memberAvailability"))
	if nonAuthoritativeDispatchWarning && availability == models.TeamMemberAvailabilityBlocked {
		availability = ""
	}
	explicitlyBlocked := availability == models.TeamMemberAvailabilityBlocked
	if availability != "" {
		member.Availability = availability
	}
	if member.Availability == "" {
		member.Availability = models.TeamMemberAvailabilityUnknown
	}
	if nonAuthoritativeDispatchWarning {
		if member.Availability == models.TeamMemberAvailabilityBlocked || member.Availability == models.TeamMemberAvailabilityUnknown {
			member.Availability = models.TeamMemberAvailabilityIdle
		}
		member.BlockedReason = nil
		return
	}
	if runtimeStatus := eventString(payload, "runtime_status", "runtimeStatus", "runtime", "liveness"); runtimeStatus != "" {
		member.RuntimeStatus = &runtimeStatus
	}
	if runtimeTaskID := eventString(payload, "runtime_task_id", "runtimeTaskId", "current_task_id", "currentTaskId", "taskId"); runtimeTaskID != "" {
		member.RuntimeTaskID = &runtimeTaskID
	}
	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 != "" && eventType != "assignment_heartbeat" {
		member.LastSummary = &summary
	}
	if reason := eventString(payload, "blocked_reason", "blockedReason", "error_message", "error", "reason"); reason != "" {
		if !nonAuthoritativeDispatchWarning {
			member.BlockedReason = &reason
		}
	}
	switch eventType {
	case "presence", "member_presence", "status", "member_status":
		return
	case "task_completed":
		if !explicitlyBlocked {
			member.Availability = models.TeamMemberAvailabilityIdle
			member.BlockedReason = nil
		}
	case "task_failed", "message_failed":
		if !isTeamTaskFailureSignal(eventType, status, payload) {
			return
		}
		if member.Availability == "" || member.Availability == models.TeamMemberAvailabilityUnknown || member.Availability == models.TeamMemberAvailabilityBusy {
			member.Availability = models.TeamMemberAvailabilityBlocked
		}
	}
}

func normalizeTeamAvailability(value string) string {
	switch strings.ToLower(strings.TrimSpace(value)) {
	case "idle", "available", "ready":
		return models.TeamMemberAvailabilityIdle
	case "busy", "running", "working":
		return models.TeamMemberAvailabilityBusy
	case "blocked", "error", "failed":
		return models.TeamMemberAvailabilityBlocked
	case "offline", "unavailable":
		return models.TeamMemberAvailabilityOffline
	case "unknown":
		return models.TeamMemberAvailabilityUnknown
	default:
		return ""
	}
}

func eventInt(payload map[string]interface{}, keys ...string) int {
	for _, key := range keys {
		value, ok := payload[key]
		if !ok || value == nil {
			continue
		}
		switch typed := value.(type) {
		case float64:
			return int(typed)
		case int:
			return typed
		case string:
			parsed, _ := strconv.Atoi(strings.TrimSpace(typed))
			return parsed
		}
	}
	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)
		if raw == "" {
			continue
		}
		if parsed, err := time.Parse(time.RFC3339Nano, raw); err == nil {
			return &parsed
		}
	}
	now := time.Now().UTC()
	return &now
}

func normalizeContextRefs(value interface{}) []string {
	rawItems, ok := value.([]interface{})
	if !ok {
		if typed, ok := value.([]string); ok {
			return typed
		}
		return nil
	}
	refs := make([]string, 0, len(rawItems))
	for _, item := range rawItems {
		ref := strings.TrimSpace(fmt.Sprintf("%v", item))
		if ref != "" {
			refs = append(refs, ref)
		}
	}
	return refs
}

func (s *teamService) workspaceProxyInstance(userID, teamID int) (*models.Team, int, error) {
	team, err := s.requireOwnedTeam(userID, teamID)
	if err != nil {
		return nil, 0, err
	}
	members, err := s.repo.ListMembersByTeamID(teamID)
	if err != nil {
		return nil, 0, err
	}
	for _, member := range activeTeamMembers(members) {
		if member.InstanceID == nil {
			continue
		}
		instance, err := s.instanceService.GetByID(*member.InstanceID)
		if err != nil || instance == nil || instance.UserID != userID {
			continue
		}
		if instance.Status == "running" || member.Status == models.TeamMemberStatusIdle || member.Status == models.TeamMemberStatusBusy {
			return team, instance.ID, nil
		}
	}
	for _, member := range activeTeamMembers(members) {
		if member.InstanceID != nil {
			instance, err := s.instanceService.GetByID(*member.InstanceID)
			if err == nil && instance != nil && instance.UserID == userID {
				return team, instance.ID, nil
			}
		}
	}
	return nil, 0, fmt.Errorf("no available Team member instance for workspace access")
}

func (s *teamService) execTeamWorkspace(ctx context.Context, userID, instanceID int, command []string, stdin io.Reader, stdout, stderr io.Writer) error {
	if s.podService == nil || s.podService.GetClient() == nil || s.podService.GetClient().Clientset == nil {
		return fmt.Errorf("k8s client not initialized")
	}
	pod, err := s.podService.GetPod(ctx, userID, instanceID)
	if err != nil {
		return fmt.Errorf("failed to get pod: %w", err)
	}
	req := s.podService.GetClient().Clientset.CoreV1().RESTClient().Post().
		Resource("pods").
		Name(pod.Name).
		Namespace(pod.Namespace).
		SubResource("exec")
	req.VersionedParams(&corev1.PodExecOptions{
		Container: "desktop",
		Command:   command,
		Stdin:     stdin != nil,
		Stdout:    stdout != nil,
		Stderr:    stderr != nil,
		TTY:       false,
	}, scheme.ParameterCodec)
	exec, err := remotecommand.NewSPDYExecutor(s.podService.GetClient().Config, "POST", req.URL())
	if err != nil {
		return fmt.Errorf("failed to initialize exec stream: %w", err)
	}
	return exec.StreamWithContext(ctx, remotecommand.StreamOptions{
		Stdin:  stdin,
		Stdout: stdout,
		Stderr: stderr,
		Tty:    false,
	})
}

func cleanTeamWorkspacePath(raw string) (string, error) {
	value := strings.TrimSpace(strings.ReplaceAll(raw, "\\", "/"))
	value = strings.TrimPrefix(value, "./")
	value = strings.TrimPrefix(value, "/")
	if value == "team" {
		value = ""
	} else if strings.HasPrefix(value, "team/") {
		value = strings.TrimPrefix(value, "team/")
	}
	if value == "" || value == "." {
		return "", nil
	}
	cleaned := posixpath.Clean(value)
	if cleaned == "." {
		return "", nil
	}
	for _, segment := range strings.Split(cleaned, "/") {
		if segment == "" || segment == "." || segment == ".." {
			return "", fmt.Errorf("invalid workspace path")
		}
	}
	return cleaned, nil
}

func cleanWorkspaceEntryName(raw string) (string, error) {
	name := strings.TrimSpace(strings.ReplaceAll(raw, "\\", "/"))
	if name == "" || strings.Contains(name, "/") || name == "." || name == ".." {
		return "", fmt.Errorf("invalid file or folder name")
	}
	return name, nil
}

func joinTeamWorkspacePath(base, child string) string {
	if strings.TrimSpace(base) == "" {
		return child
	}
	if strings.TrimSpace(child) == "" {
		return base
	}
	return posixpath.Clean(base + "/" + child)
}

func teamWorkspaceFullPath(team *models.Team, relPath string) string {
	root := strings.TrimRight(strings.TrimSpace(team.SharedMountPath), "/")
	if root == "" {
		root = teamSharedMountPath
	}
	if relPath == "" {
		return root
	}
	return root + "/" + relPath
}

func (s *teamService) resolveTeamWorkspacePath(ctx context.Context, userID, teamID int, cleanPath string) (*models.Team, string, string, error) {
	if err := ctx.Err(); err != nil {
		return nil, "", "", err
	}
	team, err := s.requireOwnedTeam(userID, teamID)
	if err != nil {
		return nil, "", "", err
	}
	root := filepath.Clean(s.teamRuntimeSharedPathFor(userID, team.ID))
	if root == "." || root == string(filepath.Separator) || strings.TrimSpace(root) == "" {
		return nil, "", "", fmt.Errorf("invalid Team workspace root")
	}
	if err := ensureTeamWorkspaceDirectory(root); err != nil {
		return nil, "", "", fmt.Errorf("failed to prepare Team workspace: %w", err)
	}
	target := root
	if cleanPath != "" {
		target = filepath.Join(root, filepath.FromSlash(cleanPath))
	}
	target = filepath.Clean(target)
	if target != root && !strings.HasPrefix(target, root+string(filepath.Separator)) {
		return nil, "", "", fmt.Errorf("invalid Team workspace path")
	}
	return team, root, target, nil
}

func teamWorkspaceDisplayRoot(team *models.Team, runtimeRoot string) string {
	if team != nil {
		if value := strings.TrimSpace(team.SharedMountPath); value != "" {
			return value
		}
	}
	return filepath.ToSlash(runtimeRoot)
}

func teamWorkspaceFileEntryFromInfo(parentPath string, info os.FileInfo) TeamWorkspaceFileEntry {
	entryType := "file"
	size := info.Size()
	if info.IsDir() {
		entryType = "directory"
		size = 0
	}
	name := info.Name()
	entryPath := joinTeamWorkspacePath(parentPath, name)
	modifiedAt := ""
	if !info.ModTime().IsZero() {
		modifiedAt = info.ModTime().UTC().Format(time.RFC3339)
	}
	return TeamWorkspaceFileEntry{
		Name:        name,
		Path:        entryPath,
		Type:        entryType,
		Size:        size,
		ModifiedAt:  modifiedAt,
		Previewable: entryType == "file" && isPreviewableWorkspaceFile(name),
	}
}

func sortTeamWorkspaceEntries(entries []TeamWorkspaceFileEntry) {
	sort.SliceStable(entries, func(i, j int) bool {
		if entries[i].Type != entries[j].Type {
			return entries[i].Type == "directory"
		}
		return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name)
	})
}

func chownTeamWorkspacePath(path string) {
	_ = os.Chown(path, teamSharedUID, teamSharedGID)
}

func ensureTeamWorkspaceDirectory(path string) error {
	clean := filepath.Clean(strings.TrimSpace(path))
	if clean == "" || clean == "." || clean == string(filepath.Separator) {
		return fmt.Errorf("invalid Team workspace directory")
	}
	if err := os.MkdirAll(clean, 0o2775); err != nil {
		return err
	}
	_ = os.Chmod(clean, 0o2775)
	chownTeamWorkspacePath(clean)
	return nil
}

func zipTeamWorkspaceDirectory(ctx context.Context, root string) ([]byte, error) {
	var buf bytes.Buffer
	archive := zip.NewWriter(&buf)
	base := filepath.Base(root)
	if err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if err := ctx.Err(); err != nil {
			return err
		}
		info, err := entry.Info()
		if err != nil {
			return err
		}
		rel, err := filepath.Rel(filepath.Dir(root), path)
		if err != nil {
			return err
		}
		zipName := filepath.ToSlash(rel)
		if entry.IsDir() {
			if path == root {
				return nil
			}
			_, err := archive.CreateHeader(&zip.FileHeader{
				Name:     strings.TrimSuffix(zipName, "/") + "/",
				Method:   zip.Store,
				Modified: info.ModTime(),
			})
			return err
		}
		header, err := zip.FileInfoHeader(info)
		if err != nil {
			return err
		}
		if zipName == "." {
			zipName = base
		}
		header.Name = zipName
		header.Method = zip.Deflate
		writer, err := archive.CreateHeader(header)
		if err != nil {
			return err
		}
		file, err := os.Open(path)
		if err != nil {
			return err
		}
		_, copyErr := io.Copy(writer, file)
		closeErr := file.Close()
		if copyErr != nil {
			return copyErr
		}
		return closeErr
	}); err != nil {
		_ = archive.Close()
		return nil, fmt.Errorf("failed to download Team workspace folder: %w", err)
	}
	if err := archive.Close(); err != nil {
		return nil, fmt.Errorf("failed to finalize Team workspace folder download: %w", err)
	}
	return buf.Bytes(), nil
}

func parseTeamWorkspaceList(parentPath, raw string) []TeamWorkspaceFileEntry {
	lines := strings.Split(strings.TrimSpace(raw), "\n")
	entries := make([]TeamWorkspaceFileEntry, 0, len(lines))
	for _, line := range lines {
		if strings.TrimSpace(line) == "" {
			continue
		}
		parts := strings.SplitN(line, "\t", 4)
		if len(parts) != 4 {
			continue
		}
		size, _ := strconv.ParseInt(strings.TrimSpace(parts[2]), 10, 64)
		mtimeUnix, _ := strconv.ParseInt(strings.TrimSpace(parts[3]), 10, 64)
		name := parts[1]
		entryPath := joinTeamWorkspacePath(parentPath, name)
		modifiedAt := ""
		if mtimeUnix > 0 {
			modifiedAt = time.Unix(mtimeUnix, 0).UTC().Format(time.RFC3339)
		}
		entries = append(entries, TeamWorkspaceFileEntry{
			Name:        name,
			Path:        entryPath,
			Type:        parts[0],
			Size:        size,
			ModifiedAt:  modifiedAt,
			Previewable: parts[0] == "file" && isPreviewableWorkspaceFile(name),
		})
	}
	sortTeamWorkspaceEntries(entries)
	return entries
}

func isPreviewableWorkspaceFile(path string) bool {
	name := strings.ToLower(strings.TrimSpace(path))
	return strings.HasSuffix(name, ".md") || strings.HasSuffix(name, ".txt") || strings.HasSuffix(name, ".json")
}

func planTeamMembers(teamName string, members []CreateTeamMemberRequest) ([]plannedTeamMember, error) {
	plans := make([]plannedTeamMember, 0, len(members))
	memberKeys := map[string]struct{}{}
	leaderCount := 0
	for idx, memberReq := range members {
		var err error
		memberReq, err = compileTeamMemberRoleProfile(memberReq)
		if err != nil {
			return nil, err
		}
		role := normalizeTeamMemberRole(memberReq.Role, memberReq.IsLeader)
		memberKey, err := normalizeTeamMemberKey(memberReq.MemberID, role, idx)
		if err != nil {
			return nil, err
		}
		if _, exists := memberKeys[memberKey]; exists {
			return nil, fmt.Errorf("duplicate team member id: %s", memberKey)
		}
		memberKeys[memberKey] = struct{}{}
		runtimeType, err := normalizeTeamMemberRuntimeType(memberReq.RuntimeType)
		if err != nil {
			return nil, err
		}
		instanceMode, err := normalizeTeamMemberInstanceMode(memberReq.Mode, memberReq.InstanceMode)
		if err != nil {
			return nil, err
		}

		isLeader := memberReq.IsLeader || isTeamLeaderRole(role)
		if isLeader {
			leaderCount++
			role = "leader"
		}
		if isLeader && (runtimeType != "openclaw" || instanceMode != InstanceModeLite) {
			return nil, fmt.Errorf("team leader must use OpenClaw Lite")
		}
		if runtimeType == "hermes" && instanceMode != InstanceModeLite {
			return nil, fmt.Errorf("Hermes team workers must use Lite mode")
		}
		profile := teamMemberProfileFromEnv(memberReq.EnvironmentOverrides)
		effectiveRole := role
		if !isLeader && strings.TrimSpace(profile.RoleHint) != "" && !isTeamLeaderRole(profile.RoleHint) {
			effectiveRole = strings.TrimSpace(profile.RoleHint)
			role = effectiveRole
		}
		displayName := strings.TrimSpace(memberReq.Name)
		if displayName == "" {
			displayName = fmt.Sprintf("%s-%s", teamName, memberKey)
		}
		plans = append(plans, plannedTeamMember{
			Request:       memberReq,
			MemberKey:     memberKey,
			DisplayName:   displayName,
			Role:          role,
			ProfileKey:    profile.ProfileKey,
			ProfileName:   profile.ProfileName,
			EffectiveRole: effectiveRole,
			RuntimeType:   runtimeType,
			InstanceMode:  instanceMode,
			IsLeader:      isLeader,
		})
	}
	if leaderCount != 1 {
		return nil, fmt.Errorf("team must include exactly one leader")
	}
	return plans, nil
}

func compileTeamMemberRoleProfile(member CreateTeamMemberRequest) (CreateTeamMemberRequest, error) {
	profile := member.RoleProfile
	if profile == nil {
		return member, nil
	}
	profileName := strings.TrimSpace(profile.DisplayName)
	if profileName == "" {
		profileName = strings.TrimSpace(member.Name)
	}
	if profileName == "" {
		profileName = strings.TrimSpace(member.MemberID)
	}
	roleHint := strings.TrimSpace(profile.RoleHint)
	if roleHint == "" {
		roleHint = strings.TrimSpace(member.Role)
	}
	profileKey := strings.TrimSpace(profile.ProfileKey)
	if profileKey == "" {
		profileKey = "custom.team-template." + normalizeTeamMemberKeyForInstanceName(member.MemberID)
	}
	if strings.TrimSuffix(profileKey, ".") == "custom.team-template" {
		profileKey = "custom.team-template.member"
	}
	summary := strings.TrimSpace(profile.Summary)
	if summary == "" {
		summary = strings.TrimSpace(profile.Mission)
	}
	isLeader := member.IsLeader || isTeamLeaderRole(member.Role)
	domainProfile := *profile
	if isLeader {
		// Leader collaboration is rendered once from the composed rule set below.
		// Keep the domain role prompt focused on the editable responsibility layer.
		domainProfile.CollaborationNotes = nil
	}
	systemPrompt := buildGeneratedTeamRolePrompt(profileName, domainProfile)
	collaborationRules := cleanUniqueTeamRoleValues(profile.CollaborationNotes)
	outputContract := cleanUniqueTeamRoleValues(append(append([]string{}, profile.Deliverables...), profile.AcceptanceCriteria...))
	baseProfileKey := ""
	tags := []string{"custom-team-template"}
	if isLeader {
		baseProfileKey = customTeamLeaderBaseProfileKey
		collaborationRules = cleanUniqueTeamRoleValues(append(append([]string{}, customTeamLeaderBaseCollaborationRules...), collaborationRules...))
		outputContract = cleanUniqueTeamRoleValues(append(append([]string{}, customTeamLeaderBaseOutputContract...), outputContract...))
		systemPrompt = buildGeneratedTeamLeaderSystemPrompt(member, profileName, summary, systemPrompt, collaborationRules, outputContract)
		tags = append(tags, "agency-agents", "leader-base-inherited")
	}
	config := map[string]interface{}{
		"profileKey":         profileKey,
		"baseProfileKey":     baseProfileKey,
		"sourceFile":         "custom-team-template",
		"memberId":           strings.TrimSpace(member.MemberID),
		"displayName":        profileName,
		"role":               strings.TrimSpace(member.Role),
		"runtimeType":        strings.TrimSpace(member.RuntimeType),
		"isLeader":           isLeader,
		"roleHint":           roleHint,
		"summary":            summary,
		"systemPrompt":       systemPrompt,
		"collaborationRules": collaborationRules,
		"outputContract":     outputContract,
		"capabilityTags":     profile.CapabilityTags,
	}
	agentsPayload := map[string]interface{}{
		"schemaVersion": 1,
		"items": []interface{}{map[string]interface{}{
			"id": 0, "type": "agent", "key": profileKey, "name": profileName, "version": 1,
			"tags": tags,
			"content": map[string]interface{}{
				"schemaVersion": 1, "kind": "agent", "format": "agent/clawmanager-profile@v1",
				"dependsOn": []interface{}{}, "config": config,
			},
		}},
	}
	personaPayload := map[string]interface{}{
		"schemaVersion": 1, "profileKey": profileKey, "name": profileName,
		"baseProfileKey": baseProfileKey,
		"displayName":    profileName, "roleHint": roleHint, "summary": summary,
		"memberId": strings.TrimSpace(member.MemberID), "role": strings.TrimSpace(member.Role),
		"runtimeType": strings.TrimSpace(member.RuntimeType), "isLeader": isLeader,
		"systemPrompt": systemPrompt,
	}
	agentsJSON, err := json.Marshal(agentsPayload)
	if err != nil {
		return member, fmt.Errorf("failed to compile custom team role profile: %w", err)
	}
	personaJSON, err := json.Marshal(personaPayload)
	if err != nil {
		return member, fmt.Errorf("failed to compile custom team role persona: %w", err)
	}
	compiled := map[string]string{
		"CLAWMANAGER_RUNTIME_AGENTS_JSON":   string(agentsJSON),
		"CLAWMANAGER_OPENCLAW_AGENTS_JSON":  string(agentsJSON),
		"CLAWMANAGER_HERMES_AGENTS_JSON":    string(agentsJSON),
		"CLAWMANAGER_RUNTIME_SYSTEM_PROMPT": systemPrompt,
		"CLAWMANAGER_HERMES_SYSTEM_PROMPT":  systemPrompt,
		"CLAWMANAGER_AGENT_SYSTEM_PROMPT":   systemPrompt,
		"HERMES_SYSTEM_PROMPT":              systemPrompt,
		"CLAWMANAGER_RUNTIME_PERSONA_JSON":  string(personaJSON),
		"CLAWMANAGER_HERMES_PERSONA_JSON":   string(personaJSON),
		"CLAWMANAGER_AGENT_PERSONA_JSON":    string(personaJSON),
	}
	merged := map[string]string{}
	for key, value := range member.EnvironmentOverrides {
		merged[key] = value
	}
	for key, value := range compiled {
		merged[key] = value
	}
	member.EnvironmentOverrides = merged
	leaderBriefing := buildGeneratedTeamLeaderBriefing(profileName, *profile)
	if leaderBriefing != "" {
		// Team member descriptions are persisted and used by team.json plus the
		// backend-generated team-introduction.md. Keep the complete generated
		// responsibility contract here so the Leader receives more than a short
		// card summary during the existing bootstrap flow.
		member.Description = optionalString(leaderBriefing)
	} else if strings.TrimSpace(derefTeamString(member.Description)) == "" && summary != "" {
		member.Description = optionalString(summary)
	}
	return member, nil
}

const (
	customTeamLeaderBaseProfileKey = "agency.agents-orchestrator"
	customTeamLeaderBaseSummary    = "Coordinates the Team, decomposes goals, assigns work, enforces handoffs, and returns the final integrated answer."
	customTeamLeaderBasePrompt     = "You are the Team Leader and orchestration controller. Break user goals into explicit subtasks, assign them to the right members via team_send, preserve context in /team, enforce evidence-based completion, and summarize the final outcome with decisions, outputs, risks, and next steps. Prefer coordination over doing all work yourself."
)

var customTeamLeaderBaseCollaborationRules = []string{
	"Only handle tasks addressed to this team member inbox.",
	"Use /team for shared context, durable notes, and handoff artifacts.",
	"Browser is available to OpenClaw Team workers. When team_artifact_preview is exposed, use its managed URL for Team files; older Runtimes may require static file inspection. Never use file:// or a temporary server.",
	"Report progress, blockers, verification evidence, and final results through the team event channel.",
	"Ask the Leader to coordinate cross-member dependencies instead of silently taking over another role.",
	"Default to leader-mediated collaboration: user tasks enter through the Leader, then fan out to members.",
	"Do not mark work complete until member outputs have been reconciled and any QA/review role has reported its verdict.",
}

var customTeamLeaderBaseOutputContract = []string{
	"task_breakdown",
	"assignments",
	"member_results",
	"verification",
	"final_answer",
	"open_risks",
}

func buildGeneratedTeamLeaderSystemPrompt(
	member CreateTeamMemberRequest,
	profileName string,
	summary string,
	domainPrompt string,
	collaborationRules []string,
	outputContract []string,
) string {
	role := strings.TrimSpace(member.Role)
	if role == "" {
		role = "leader"
	}
	runtimeType := strings.TrimSpace(member.RuntimeType)
	if runtimeType == "" {
		runtimeType = "openclaw"
	}
	if summary == "" {
		summary = customTeamLeaderBaseSummary
	}
	lines := []string{
		customTeamLeaderBasePrompt,
		"",
		"Domain-specific Leader role overlay:",
		strings.TrimSpace(domainPrompt),
		"",
		fmt.Sprintf(
			"Team member context: member_id=%s; display_name=%s; role=%s; runtime=%s; is_leader=true.",
			strings.TrimSpace(member.MemberID),
			strings.TrimSpace(profileName),
			role,
			runtimeType,
		),
		"Role summary: " + summary,
		"Collaboration rules:",
	}
	for _, rule := range collaborationRules {
		lines = append(lines, "- "+rule)
	}
	lines = append(lines, "Expected output contract: "+strings.Join(outputContract, ", ")+".")
	return strings.Join(lines, "\n")
}

func cleanUniqueTeamRoleValues(values []string) []string {
	result := make([]string, 0, len(values))
	seen := make(map[string]struct{}, len(values))
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" {
			continue
		}
		if _, exists := seen[value]; exists {
			continue
		}
		seen[value] = struct{}{}
		result = append(result, value)
	}
	return result
}

func buildGeneratedTeamLeaderBriefing(displayName string, profile TeamMemberRoleProfileRequest) string {
	parts := make([]string, 0, 9)
	appendText := func(label, value string) {
		if value = strings.TrimSpace(value); value != "" {
			parts = append(parts, label+": "+value)
		}
	}
	appendList := func(label string, values []string) {
		clean := make([]string, 0, len(values))
		for _, value := range values {
			if value = strings.TrimSpace(value); value != "" {
				clean = append(clean, value)
			}
		}
		if len(clean) > 0 {
			parts = append(parts, label+": "+strings.Join(clean, "、"))
		}
	}
	appendText("Role", displayName)
	appendText("Summary", profile.Summary)
	appendText("Mission", profile.Mission)
	appendList("Responsibilities", profile.Responsibilities)
	appendList("Boundaries", profile.Boundaries)
	appendList("Expected inputs", profile.ExpectedInputs)
	appendList("Deliverables", profile.Deliverables)
	appendList("Acceptance criteria", profile.AcceptanceCriteria)
	appendList("Collaboration", profile.CollaborationNotes)
	return strings.Join(parts, "; ")
}

func buildGeneratedTeamRolePrompt(displayName string, profile TeamMemberRoleProfileRequest) string {
	lines := []string{fmt.Sprintf("You are the %s for this ClawManager Team.", strings.TrimSpace(displayName))}
	appendSection := func(title string, values []string) {
		clean := make([]string, 0, len(values))
		for _, value := range values {
			if value = strings.TrimSpace(value); value != "" {
				clean = append(clean, value)
			}
		}
		if len(clean) == 0 {
			return
		}
		lines = append(lines, "", title+":")
		for _, value := range clean {
			lines = append(lines, "- "+value)
		}
	}
	appendSection("Mission", []string{profile.Mission})
	appendSection("Core responsibilities", profile.Responsibilities)
	appendSection("Role boundaries", profile.Boundaries)
	appendSection("Expected inputs", profile.ExpectedInputs)
	appendSection("Required deliverables", profile.Deliverables)
	appendSection("Acceptance criteria", profile.AcceptanceCriteria)
	appendSection("Collaboration notes", profile.CollaborationNotes)
	return strings.Join(lines, "\n")
}

func teamMemberInstanceName(teamName string, teamID int, memberKey string) string {
	teamPart := normalizeTeamMemberKeyForInstanceName(teamName)
	if teamPart == "" {
		teamPart = "team"
	}
	memberPart := normalizeTeamMemberKeyForInstanceName(memberKey)
	if memberPart == "" {
		memberPart = "member"
	}
	const maxInstanceNameLength = 50
	idPart := fmt.Sprintf("%d", teamID)
	maxMemberLength := maxInstanceNameLength - len(idPart) - len("--t")
	if maxMemberLength < 1 {
		maxMemberLength = 1
	}
	if len(memberPart) > maxMemberLength {
		memberPart = strings.Trim(memberPart[:maxMemberLength], "-")
		if memberPart == "" {
			memberPart = "member"
		}
	}
	suffix := fmt.Sprintf("-%s-%s", idPart, memberPart)
	if len(teamPart)+len(suffix) <= maxInstanceNameLength {
		return teamPart + suffix
	}
	maxTeamLength := maxInstanceNameLength - len(suffix)
	if maxTeamLength < 1 {
		maxTeamLength = 1
	}
	return strings.Trim(teamPart[:maxTeamLength], "-") + suffix
}

func normalizeTeamMemberKeyForInstanceName(value string) string {
	normalized := strings.ToLower(strings.TrimSpace(value))
	normalized = strings.ReplaceAll(normalized, "_", "-")
	normalized = strings.ReplaceAll(normalized, " ", "-")
	normalized = teamMemberInstanceNameInvalidChars.ReplaceAllString(normalized, "")
	normalized = teamMemberInstanceNameRepeatedDashs.ReplaceAllString(normalized, "-")
	return strings.Trim(normalized, "-")
}

func normalizeTeamMemberRuntimeType(raw string) (string, error) {
	runtimeType := strings.ToLower(strings.TrimSpace(raw))
	if runtimeType == "" {
		return "openclaw", nil
	}
	switch runtimeType {
	case "openclaw", "hermes":
		return runtimeType, nil
	default:
		return "", fmt.Errorf("unsupported team member runtime type: %s", raw)
	}
}

func normalizeTeamMemberInstanceMode(rawMode, rawInstanceMode string) (string, error) {
	if mode, ok := NormalizeInstanceMode(rawMode); ok {
		return mode, nil
	}
	if strings.TrimSpace(rawMode) != "" {
		return "", fmt.Errorf("unsupported team member instance mode: %s", rawMode)
	}
	if mode, ok := NormalizeInstanceMode(rawInstanceMode); ok {
		return mode, nil
	}
	if strings.TrimSpace(rawInstanceMode) != "" {
		return "", fmt.Errorf("unsupported team member instance mode: %s", rawInstanceMode)
	}
	return InstanceModeLite, nil
}

func normalizeTeamCommunicationMode(raw string) (string, error) {
	mode := strings.ToLower(strings.TrimSpace(raw))
	mode = strings.ReplaceAll(mode, "-", "_")
	mode = strings.ReplaceAll(mode, " ", "_")
	switch mode {
	case "", teamCommunicationModeLeaderMediated, "leader", "leader_only":
		return teamCommunicationModeLeaderMediated, nil
	case teamCommunicationModePeerAssisted, "peer", "peer_to_peer", "peer_assist":
		return teamCommunicationModePeerAssisted, nil
	case teamCommunicationModeFullMesh, "mesh":
		return teamCommunicationModeFullMesh, nil
	default:
		return "", fmt.Errorf("unsupported team communication mode: %s", raw)
	}
}

func normalizedTeamCommunicationMode(raw string) string {
	mode, err := normalizeTeamCommunicationMode(raw)
	if err != nil {
		return teamCommunicationModeLeaderMediated
	}
	return mode
}

func normalizeTeamMemberRole(raw string, isLeader bool) string {
	role := strings.TrimSpace(raw)
	if isLeader || isTeamLeaderRole(role) {
		return "leader"
	}
	if role == "" {
		return "member"
	}
	return role
}

func isTeamLeaderRole(role string) bool {
	normalized := strings.ToLower(strings.TrimSpace(role))
	normalized = strings.ReplaceAll(normalized, "_", "-")
	normalized = strings.ReplaceAll(normalized, " ", "-")
	return normalized == "leader" || normalized == "team-leader"
}

func findTeamLeader(members []models.TeamMember) *models.TeamMember {
	for idx := range members {
		if isTeamLeaderRole(members[idx].Role) {
			member := members[idx]
			return &member
		}
	}
	return nil
}

func leaderMemberKey(member *models.TeamMember) string {
	if member == nil {
		return ""
	}
	return member.MemberKey
}

type teamRosterConfig struct {
	Version             int                           `json:"version"`
	TeamID              string                        `json:"teamId"`
	RosterHash          string                        `json:"rosterHash"`
	LeaderMemberID      string                        `json:"leaderMemberId"`
	CommunicationMode   string                        `json:"communicationMode"`
	CollaborationPolicy teamRosterCollaborationPolicy `json:"collaborationPolicy"`
	SharedDir           string                        `json:"sharedDir"`
	Members             []teamRosterMember            `json:"members"`
	Redis               teamRosterRedis               `json:"redis"`
}

type teamRosterMember struct {
	MemberID      string `json:"memberId"`
	Role          string `json:"role"`
	EffectiveRole string `json:"effectiveRole,omitempty"`
	ProfileKey    string `json:"profileKey,omitempty"`
	ProfileName   string `json:"profileName,omitempty"`
	RuntimeType   string `json:"runtimeType"`
	InstanceMode  string `json:"instanceMode"`
	DisplayName   string `json:"displayName"`
	Description   string `json:"description,omitempty"`
	IsLeader      bool   `json:"isLeader"`
}

type teamRosterRedis struct {
	EventsKey   string `json:"eventsKey"`
	PresenceKey string `json:"presenceKey"`
	DLQKey      string `json:"dlqKey"`
}

type teamRosterCollaborationPolicy struct {
	Mode               string   `json:"mode"`
	AllowPeerToPeer    bool     `json:"allowPeerToPeer"`
	LeaderFinalizes    bool     `json:"leaderFinalizes"`
	PeerReplyRequired  bool     `json:"peerReplyRequired"`
	AllowedPeerActions []string `json:"allowedPeerActions,omitempty"`
}

func buildTeamCollaborationPolicy(communicationMode string) teamRosterCollaborationPolicy {
	mode := normalizedTeamCommunicationMode(communicationMode)
	policy := teamRosterCollaborationPolicy{
		Mode:            mode,
		LeaderFinalizes: true,
	}
	switch mode {
	case teamCommunicationModePeerAssisted:
		policy.AllowPeerToPeer = true
		policy.PeerReplyRequired = true
		policy.AllowedPeerActions = []string{"ask", "handoff", "review_request", "artifact_request", "blocker_help", "peer_review"}
	case teamCommunicationModeFullMesh:
		policy.AllowPeerToPeer = true
		policy.PeerReplyRequired = true
		policy.AllowedPeerActions = []string{"ask", "handoff", "review_request", "artifact_request", "blocker_help", "peer_review", "delegate"}
	}
	return policy
}

func buildTeamRosterConfig(team *models.Team, members []plannedTeamMember) (string, error) {
	return buildTeamRosterConfigWithSharedDir(team, members, team.SharedMountPath)
}

func buildTeamRosterConfigWithSharedDir(team *models.Team, members []plannedTeamMember, sharedDir string) (string, error) {
	communicationMode := normalizedTeamCommunicationMode(team.CommunicationMode)
	sharedDir = strings.TrimSpace(sharedDir)
	if sharedDir == "" {
		sharedDir = team.SharedMountPath
	}
	config := teamRosterConfig{
		Version:             1,
		TeamID:              strconv.Itoa(team.ID),
		CommunicationMode:   communicationMode,
		CollaborationPolicy: buildTeamCollaborationPolicy(communicationMode),
		SharedDir:           sharedDir,
		Members:             make([]teamRosterMember, 0, len(members)),
		Redis: teamRosterRedis{
			EventsKey:   teamEventsKey(team.ID),
			PresenceKey: teamPresenceKey(team.ID),
			DLQKey:      teamDLQKey(team.ID),
		},
	}
	for _, member := range members {
		if member.IsLeader {
			config.LeaderMemberID = member.MemberKey
		}
		config.Members = append(config.Members, teamRosterMember{
			MemberID:      member.MemberKey,
			Role:          member.Role,
			EffectiveRole: effectiveTeamMemberRole(member),
			ProfileKey:    member.ProfileKey,
			ProfileName:   member.ProfileName,
			RuntimeType:   member.RuntimeType,
			InstanceMode:  member.InstanceMode,
			DisplayName:   member.DisplayName,
			Description:   plannedTeamMemberDescription(member),
			IsLeader:      member.IsLeader,
		})
	}
	if config.LeaderMemberID == "" {
		return "", fmt.Errorf("team must include exactly one leader")
	}
	return marshalTeamRosterConfig(config)
}

func buildTeamRosterConfigFromMembers(team *models.Team, members []models.TeamMember) (string, error) {
	return buildTeamRosterConfigFromMembersWithSharedDir(team, members, team.SharedMountPath)
}

func buildTeamRosterConfigFromMembersWithSharedDir(team *models.Team, members []models.TeamMember, sharedDir string) (string, error) {
	communicationMode := normalizedTeamCommunicationMode(team.CommunicationMode)
	sharedDir = strings.TrimSpace(sharedDir)
	if sharedDir == "" {
		sharedDir = team.SharedMountPath
	}
	config := teamRosterConfig{
		Version:             1,
		TeamID:              strconv.Itoa(team.ID),
		CommunicationMode:   communicationMode,
		CollaborationPolicy: buildTeamCollaborationPolicy(communicationMode),
		SharedDir:           sharedDir,
		Members:             make([]teamRosterMember, 0, len(members)),
		Redis: teamRosterRedis{
			EventsKey:   teamEventsKey(team.ID),
			PresenceKey: teamPresenceKey(team.ID),
			DLQKey:      teamDLQKey(team.ID),
		},
	}
	for _, member := range members {
		isLeader := isTeamLeaderRole(member.Role)
		runtimeType := strings.TrimSpace(member.RuntimeType)
		if runtimeType == "" {
			runtimeType = "openclaw"
		}
		instanceMode := strings.TrimSpace(member.InstanceMode)
		if instanceMode == "" {
			instanceMode = InstanceModeLite
		}
		if isLeader {
			config.LeaderMemberID = member.MemberKey
		}
		config.Members = append(config.Members, teamRosterMember{
			MemberID:      member.MemberKey,
			Role:          member.Role,
			EffectiveRole: member.Role,
			RuntimeType:   runtimeType,
			InstanceMode:  instanceMode,
			DisplayName:   member.DisplayName,
			Description:   derefTeamString(member.Description),
			IsLeader:      isLeader,
		})
	}
	if config.LeaderMemberID == "" {
		return "", fmt.Errorf("team must include exactly one leader")
	}
	return marshalTeamRosterConfig(config)
}

func marshalTeamRosterConfig(config teamRosterConfig) (string, error) {
	config.RosterHash = ""
	canonical, err := marshalJSON(config)
	if err != nil {
		return "", err
	}
	sum := sha256.Sum256([]byte(canonical))
	config.RosterHash = "sha256:" + fmt.Sprintf("%x", sum[:])
	return marshalJSON(config)
}

func activeTeamMembers(members []models.TeamMember) []models.TeamMember {
	active := make([]models.TeamMember, 0, len(members))
	for _, member := range members {
		if member.Status == models.TeamMemberStatusDeleted || member.Status == models.TeamMemberStatusDeleting {
			continue
		}
		active = append(active, member)
	}
	return active
}

func activeTeams(teams []models.Team) []models.Team {
	active := make([]models.Team, 0, len(teams))
	for _, team := range teams {
		if team.Status == models.TeamStatusDeleted {
			continue
		}
		active = append(active, team)
	}
	return active
}

func deletedTeamName(name string, teamID int) string {
	const maxTeamNameLength = 255
	suffix := fmt.Sprintf("__deleted_%d", teamID)
	trimmed := strings.TrimSpace(name)
	if trimmed == "" {
		trimmed = "team"
	}
	if strings.HasSuffix(trimmed, suffix) {
		return trimmed
	}
	if len(trimmed)+len(suffix) <= maxTeamNameLength {
		return trimmed + suffix
	}
	runes := []rune(trimmed)
	maxPrefixLength := maxTeamNameLength - len(suffix)
	if len(runes) > maxPrefixLength {
		runes = runes[:maxPrefixLength]
	}
	return string(runes) + suffix
}

func normalizeTeamMemberKey(raw, role string, index int) (string, error) {
	value := strings.ToLower(strings.TrimSpace(raw))
	if value == "" {
		value = strings.ToLower(strings.TrimSpace(role))
	}
	if value == "" {
		value = fmt.Sprintf("member-%d", index+1)
	}
	value = strings.ReplaceAll(value, "_", "-")
	value = strings.ReplaceAll(value, " ", "-")
	if !teamMemberKeyPattern.MatchString(value) {
		return "", fmt.Errorf("team member id is invalid")
	}
	return value, nil
}

func defaultTeamRedisURL() string {
	for _, key := range []string{"CLAWMANAGER_TEAM_REDIS_URL", "TEAM_REDIS_URL", "REDIS_URL"} {
		if value := strings.TrimSpace(os.Getenv(key)); value != "" {
			return value
		}
	}
	if value, ok := defaultTeamRedisServiceURL(); ok {
		return value
	}
	return ""
}

func defaultTeamRedisServiceURL() (string, bool) {
	systemNamespace := strings.TrimSpace(os.Getenv("CLAWMANAGER_SYSTEM_NAMESPACE"))
	if systemNamespace == "" {
		if client := k8s.GetClient(); client != nil {
			systemNamespace = client.GetSystemNamespace()
		} else if baseNamespace := strings.TrimSpace(os.Getenv("K8S_NAMESPACE")); baseNamespace != "" {
			systemNamespace = fmt.Sprintf("%s-system", baseNamespace)
		}
	}
	if systemNamespace == "" {
		return "", false
	}

	serviceName := strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_REDIS_SERVICE_NAME"))
	if serviceName == "" {
		serviceName = strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_REDIS_SERVICE"))
	}
	if serviceName == "" {
		serviceName = "clawmanager-team-redis"
	}

	port := normalizePortValue(
		strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_REDIS_SERVICE_PORT")),
		strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_REDIS_PORT")),
	)
	if port == "" {
		port = "6379"
	}

	db := strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_REDIS_DB"))
	if db == "" {
		db = strings.TrimSpace(os.Getenv("TEAM_REDIS_DB"))
	}
	if db == "" {
		db = "0"
	}

	return fmt.Sprintf("redis://%s.%s.svc.cluster.local:%s/%s", serviceName, systemNamespace, port, db), true
}

func teamTaskStaleTimeout() time.Duration {
	raw := strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_TASK_STALE_SECONDS"))
	if raw == "" {
		return defaultTeamTaskStaleTimeout
	}
	seconds, err := strconv.Atoi(raw)
	if err != nil {
		return defaultTeamTaskStaleTimeout
	}
	if seconds <= 0 {
		return 0
	}
	return time.Duration(seconds) * time.Second
}

func defaultTeamManagerBaseURL() (string, bool) {
	if override := strings.TrimSpace(os.Getenv("CLAWMANAGER_TEAM_MANAGER_BASE_URL")); override != "" {
		return override, true
	}
	return defaultAgentControlBaseURL()
}

func teamInboxKey(teamID int, memberID string) string {
	return fmt.Sprintf("claw:team:%d:inbox:%s", teamID, memberID)
}

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 teamRootWorkflowStateKey(teamID int, rootTaskID string) string {
	return fmt.Sprintf("claw:team:%d:root:%s:state", teamID, normalizeTeamRedisKeyPart(rootTaskID))
}

func teamAssignmentActivityKey(teamID int, rootTaskID, assignmentID string) string {
	return fmt.Sprintf(
		"claw:team:%d:assignment-activity:%s:%s",
		teamID,
		normalizeTeamRedisKeyPart(rootTaskID),
		normalizeTeamRedisKeyPart(assignmentID),
	)
}

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)
}

func teamDLQKey(teamID int) string {
	return fmt.Sprintf("claw:team:%d:dlq", teamID)
}

func defaultInt(value, fallback int) int {
	if value > 0 {
		return value
	}
	return fallback
}

func defaultFloat(value, fallback float64) float64 {
	if value > 0 {
		return value
	}
	return fallback
}

func derefTeamString(value *string) string {
	if value == nil {
		return ""
	}
	return strings.TrimSpace(*value)
}
