using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

using AIRuntime.Runtime.Providers;
using AIRuntime.Runtime.Providers.Fallback;
using AIRuntime.Runtime.Services.Agent;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using UnityEditor;
using UnityEngine;


namespace AIRuntime.Editor
{
    /// <summary>
    /// Lightweight task board for AIRuntime development and
    /// agent work planning inside the Unity Editor.
    /// </summary>
    public sealed partial class AIRuntimeKanbanSession :
        ScriptableObject
    {
        private const int CurrentDecompositionPolicyVersion = 13;

        private const int MaximumDecompositionFailureCount = 3;

        private const int MaximumAutoDecompositionDepth = 8;

        private const int MaximumAutoDecompositionDescendantsPerRoot = 128;

        private static readonly TimeSpan
            MaximumTrackedAgentRunInactivity =
                TimeSpan.FromMinutes(15);

        private static readonly TimeSpan
            DefaultProviderRateLimitCooldown =
                TimeSpan.FromMinutes(5);

        private static readonly TimeSpan
            MaximumProviderRateLimitBackoff =
                TimeSpan.FromHours(2);

        private static readonly TimeSpan
            ProviderRateLimitStreakResetWindow =
                TimeSpan.FromHours(6);

        private const string ProviderCooldownReviewStatus =
            "Provider Cooldown";

        private const string ArchitectAgentId =
            "ArchitectAgent";

        private const string ReviewAgentId =
            "ReviewAgent";

        private const string ExplicitArtifactRecoveryPrefix =
            "[ExplicitArtifactRecovery]";

        private const int CurrentCapabilityBudgetPolicyVersion = 2;

        private const int CurrentCapabilityRecoveryPolicyVersion = 8;

        private const int CurrentOrchestrationSettingsVersion = 1;

        private const string WorkflowPhasePlanning = "Planning";

        private const string WorkflowPhaseImplementation = "Implementation";

        private const string WorkflowPhaseFinalReview = "FinalReview";

        private const string WorkflowPhaseCompleted = "Completed";

        internal static string BoardPathOverrideForTests;

        private enum TaskStatus
        {
            Backlog = 0,
            Todo = 1,
            InProgress = 2,
            Review = 3,
            Done = 4,
            Crashed = 5,
            Failed = 6
        }


        private enum TaskPriority
        {
            Low,
            Normal,
            High,
            Critical
        }


        [Serializable]
        private sealed class KanbanTask
        {
            public string Id;

            public string Title;

            public string Description;

            public string AcceptanceCriteria;
            public bool HumanVisualReviewRequired;
            public string HumanVisualArtifacts;
            public string HumanVisualReviewFingerprint;
            public string HumanVisualApprovalFingerprint;
            public string HumanVisualReviewNotes;
            public string HumanVisualReviewedAtUtc;


            public string ParentTaskId;

            public List<string> DependsOnTaskIds =
                new List<string>();

            // Title declarations exist only to resolve forward sibling
            // references while Planning builds a batch. Runtime scheduling
            // uses DependsOnTaskIds exclusively.
            public List<string> DeclaredDependencyTitles =
                new List<string>();

            public List<string> AssociatedTaskIds =
                new List<string>();

            public List<string> ProducedArtifactPaths =
                new List<string>();

            public List<string> ConsumedArtifactPaths =
                new List<string>();

            public List<string> MutatedResourceKeys =
                new List<string>();

            public string CreationSource;

            public string CreatedStatus;

            public bool AgentAssignmentInherited;

            public bool IsDecompositionDraft;

            public string AssignedAgent;

            public string Tags;

            public string DueDate;

            public TaskStatus Status;

            public TaskPriority Priority;

            public int Estimate;

            public string CreatedAt;

            public string UpdatedAt;

            public string AgentResult;

            public string ExecutionMemory;
            public KanbanCardMemory Memory;
            public string NextAttemptAtUtc;
            public int AutomaticRetryCount;
            public string RetryReason;
            public bool RetryReview;
            public string ReviewQueuedAtUtc;
            public DispatchAttempt Dispatch;

            public string AgentError;

            public string AgentStartedAt;

            public string AgentFinishedAt;

            public string NextAgentMessage;

            public string ReviewStatus;

            public string ReviewResult;

            public string ReviewEvidence;

            public string ReviewFailureKind;

            public string ReviewStartedAt;

            public string ReviewFinishedAt;

            public int ReviewFailureCount;

            public int PreflightFailureCount;

            public string ReviewEvidenceFingerprint;

            public string LastRejectedReviewEvidenceFingerprint;

            public string LastRejectedReviewOutcomeFingerprint;

            public string ReviewDependencyStateFingerprint;

            public string BlockedByTaskId;

            public string BlockedReason;

            public int DecompositionFailureCount;

            public int DecompositionPolicyVersion;

            public bool SuppressAutoDecomposition;

            public int CapabilityBudgetPolicyVersion;

            public int CapabilityRecoveryPolicyVersion;

            public string RequestTitle;

            public string RequestDescription;

            public string RequestAcceptanceCriteria;

            public string WorkflowPhase;

            public int PlanRevision;

            public int PlannedRevision;

            // Planning proposals are snapshots, not additive mutation logs.
            // These identifiers let completion reconcile the desired graph
            // even when an assembly reload separates the tool calls from the
            // queued completion callback.
            public string PlanningAttemptId;

            public string ProposedInPlanningAttemptId;

            public string PlanningKickbackReason;

            public string PlanningKickbackFingerprint;
        }


        [Serializable]
        private sealed class KanbanBoard
        {
            public int Version =
                1;

            // Monotonic compare-and-swap token for durable board writes. A
            // window may save only the exact snapshot it most recently loaded
            // or wrote, preventing a stale Editor/domain from erasing newer
            // command-line or externally restored state.
            public bool SchedulerEnabled;

            public int Revision;

            public string LastWriterId;

            public string LastSavedAtUtc;

            public string Name =
                "AIRuntime Tasks";

            public List<KanbanTask> Tasks =
                new List<KanbanTask>();

            public List<ProviderCooldownState> ProviderCooldowns =
                new List<ProviderCooldownState>();

            public List<AgentFallbackChain> AgentFallbackChains = new List<AgentFallbackChain>();
        }


        [Serializable]
        private sealed class ProviderCooldownState
        {
            public string AgentId;

            public string ProviderName;

            public string UntilUtc;

            public string Reason;

            public int ConsecutiveFailureCount;

            public string LastFailureUtc;

            public bool Released;

            public string ReleasedUtc;
        }

        [Serializable]
        private sealed class SavedAgentReference
        {
            public int Version;

            public string AgentId;

            public string AgentDisplayName;

            public string AgentPreset;

            public bool UseAgent;
        }

        [Serializable]
        private sealed class KanbanAgentActivationState
        {
            public int Version =
                2;

            public List<string> ActiveAgentIds =
                new List<string>();
        }


        private sealed class AgentOption
        {
            public string Id;

            public string Label;

            public string ConfigurationPath;

            public string Role;
        }


        private const string DragDataKey =
            "AIRuntime.Kanban.TaskId";

        private const string ReloadPausedCountKey =
            "AIRuntime.Kanban.ReloadPausedCount";

        private const string ReloadResumeAgentsKey =
            "AIRuntime.Kanban.ResumeAgentsAfterReload";

        private const float MinimumColumnWidth =
            230;

        private const float MinimumBoardHeight =
            220;


        [SerializeField]
        private string _boardFilePath;

        [SerializeField]
        private bool _autoSave =
            true;

        [SerializeField]
        private bool _autoRunAgents =
            false;

        [SerializeField]
        private bool _autoDecompose =
            true;

        [SerializeField]
        private int _orchestrationSettingsVersion;

        [SerializeField]
        private string _search =
            string.Empty;

        private readonly Vector2[] _columnScroll =
            new Vector2[
                Enum.GetValues(
                        typeof(TaskStatus))
                    .Length];


        private KanbanBoard _board =
            new KanbanBoard();

        private string _selectedTaskId;

        private string _statusMessage =
            "Ready";

        private string _lastDependencyDeadlock =
            string.Empty;

        private string _lastSchedulerDiagnostic;
        private double _nextSchedulerDiagnosticAt;

        [SerializeField] private bool _showAgentStatus = true;
        private Vector2 _agentStatusScroll;
        private float _agentStatusWidth;
        private double _nextAgentStatusRepaint;

        private sealed class AgentStatusRow
        {
            public string AgentId;
            public string Status;
            public string Detail;
            public KanbanTask Task;
            public KanbanTask Dependency;
        }

        private Vector2 _boardScroll;

        private Vector2 _taskDescriptionScroll;

        private Vector2 _acceptanceCriteriaScroll;

        private GUIStyle _wrappedTaskTextAreaStyle;

        private Vector2 _taskEditorScroll;

        private Vector2 _agentOutputScroll;

        private Vector2 _executionMemoryScroll;

        private Vector2 _nextAgentMessageScroll;

        private bool _isDirty;

        private string _loadedBoardPath =
            string.Empty;

        private string _loadedBoardFingerprint =
            string.Empty;

        private bool _boardWriteConflict;

        private double _nextExternalBoardCheckEditorTime;

        private readonly string _boardWriterId =
            Guid.NewGuid().ToString("N");

        private bool _autoSaveScheduled;

        private bool _executionMemorySaveScheduled;

        private List<AgentOption> _agentOptions;

        private readonly HashSet<string> _activeKanbanAgentIds =
            new HashSet<string>(
                StringComparer.Ordinal);

        private readonly Dictionary<string, ActiveAgentRun>
            _activeAgentRuns =
                new Dictionary<string, ActiveAgentRun>(
                    StringComparer.Ordinal);

        private readonly ConcurrentQueue<PendingAgentCompletion>
            _pendingAgentCompletions =
                new ConcurrentQueue<PendingAgentCompletion>();

        private readonly ConcurrentQueue<PendingExecutionMemoryUpdate>
            _pendingExecutionMemoryUpdates =
                new ConcurrentQueue<PendingExecutionMemoryUpdate>();

        private readonly Dictionary<string, bool>
            _taskCardExpansionOverrides =
                new Dictionary<string, bool>(
                    StringComparer.Ordinal);

        private readonly HashSet<string> _discardedAgentRunTaskIds =
            new HashSet<string>(
                StringComparer.Ordinal);

        private bool _isAgentQueuePumping;

        private bool _isPreparingForAssemblyReload;

        private double _agentQueueNotBeforeEditorTime;

        // Negative means unlimited (the interactive Run agents behavior).
        // A non-negative value bounds how many new provider runs this window
        // may start, which gives command-line recovery a safe single-step mode.
        private int _agentStartsRemaining =
            -1;


        private sealed class ActiveAgentRun
        {
            public string AttemptId;
            public string TaskId;

            public CancellationTokenSource Cancellation;

            public bool CoordinationOnly;

            public string PlanningAttemptId;

            public bool PlanningCommitted;

            public string LastPlanningCommitError;

            public DateTime LastActivityAtUtc;

            public DateTime LastProgressAtUtc;
        }


        private sealed class PendingAgentCompletion
        {
            public string AttemptId;
            public string TaskId;
            public string AgentId;
            public string Result;
            public string Error;
            public string ReviewEvidence;
            public bool ReviewMode;
            public ProviderFailureKind? ProviderFailureKind;
        }


        private sealed class PendingExecutionMemoryUpdate
        {
            public string AttemptId;
            public string TaskId;
            public string Memory;
            public string HumanVisualArtifacts;
        }


        // Rendering stays shared with the inspector/graph; its lifetime is owned by
        // AIRuntimeKanbanScheduler, independently of any EditorWindow.
        internal event Action Changed;
        private Rect position = new Rect(0, 0, 1000, 700);
        private void Repaint() => Changed?.Invoke();
        public static void Open() => AIRuntimeKanbanWindow.Open();
        internal void DrawBoard(Rect bounds) { position = bounds; OnGUI(); }
        internal void RefreshView() { RefreshAgentOptions(); Repaint(); }

        /// <summary>
        /// Applies versioned board migrations without depending on a visible
        /// EditorWindow lifecycle. Intended for Unity's -executeMethod path
        /// during recovery and maintenance.
        /// </summary>
        public static void MigrateDefaultBoardForCommandLine()
        {
            string boardPath = GetDefaultBoardPath();

            if (!File.Exists(boardPath))
            {
                throw new FileNotFoundException(
                    "The default AIRuntime Kanban board does not exist.",
                    boardPath);
            }

            AIRuntimeKanbanSession window =
                CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                window._boardFilePath = boardPath;
                window.LoadBoard(showDialog: false);
                Debug.Log(
                    $"[Kanban] Applied durable migrations to '{boardPath}'.");
            }
            finally
            {
                DestroyImmediate(window);
            }
        }


        /// <summary>
        /// Requeues one explicitly named persisted card for an evidence-only
        /// review without rerunning its implementation. The task title is read
        /// from AIRUNTIME_KANBAN_TASK_TITLE so recovery scripts do not edit the
        /// board JSON directly.
        /// </summary>
        public static void RequeueNamedTaskReviewForCommandLine()
        {
            string requestedTitle =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_TASK_TITLE")?.Trim();

            if (string.IsNullOrWhiteSpace(requestedTitle))
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_TASK_TITLE must name exactly one persisted Kanban card.");
            }

            string boardPath = GetDefaultBoardPath();
            AIRuntimeKanbanSession window =
                CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                window._boardFilePath = boardPath;
                window.LoadBoard(showDialog: false);

                List<KanbanTask> matches =
                    window._board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                string.Equals(
                                    task.Title?.Trim(),
                                    requestedTitle,
                                    StringComparison.Ordinal))
                        .ToList();

                if (matches.Count != 1)
                {
                    throw new InvalidOperationException(
                        $"Expected exactly one Kanban card titled '{requestedTitle}', but found {matches.Count}.");
                }

                window.RequeueReview(matches[0]);

                if (matches[0].Status != TaskStatus.Review ||
                    !string.Equals(
                        matches[0].ReviewStatus,
                        "Pending",
                        StringComparison.Ordinal))
                {
                    throw new InvalidOperationException(
                        $"Kanban card '{requestedTitle}' could not be requeued for review.");
                }

                window.SaveBoard(
                    showDialog: false);

                Debug.Log(
                    $"[Kanban] Requeued '{requestedTitle}' for evidence-only review.");
            }
            finally
            {
                DestroyImmediate(window);
            }
        }


        /// <summary>
        /// Requeues one explicitly named card for implementation after an
        /// operator has verified a concrete defect in its owned artifacts.
        /// This preserves the implementation result, execution memory, and
        /// committed graph while avoiding a redundant provider review.
        /// </summary>
        public static void RequestNamedTaskReworkForCommandLine()
        {
            string requestedTitle =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_TASK_TITLE")?.Trim();

            string verifiedFinding =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_REWORK_MESSAGE")?.Trim();

            if (string.IsNullOrWhiteSpace(requestedTitle) ||
                string.IsNullOrWhiteSpace(verifiedFinding))
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_TASK_TITLE and AIRUNTIME_KANBAN_REWORK_MESSAGE are required.");
            }

            string boardPath = GetDefaultBoardPath();
            AIRuntimeKanbanSession window =
                CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                window._boardFilePath = boardPath;
                window.LoadBoard(showDialog: false);

                List<KanbanTask> matches =
                    window._board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                string.Equals(
                                    task.Title?.Trim(),
                                    requestedTitle,
                                    StringComparison.Ordinal))
                        .ToList();

                if (matches.Count != 1)
                {
                    throw new InvalidOperationException(
                        $"Expected exactly one Kanban card titled '{requestedTitle}', but found {matches.Count}.");
                }

                KanbanTask task = matches[0];

                if (task.Status == TaskStatus.InProgress)
                {
                    throw new InvalidOperationException(
                        $"Kanban card '{requestedTitle}' is currently running and cannot be requeued by an operator command.");
                }

                task.Status = TaskStatus.Todo;
                task.AgentError = string.Empty;
                task.ReviewStatus = "Rework Requested";
                task.ReviewFailureKind = "ImplementationDefect";
                // Operator-verified rework is a new, explicitly authorized
                // correction attempt. It must not spend the automated
                // reviewer's bounded retry budget before that work runs.
                task.ReviewFailureCount = 0;
                task.ReviewResult = verifiedFinding;
                task.ReviewEvidence = string.Empty;
                task.ReviewStartedAt = string.Empty;
                task.ReviewFinishedAt = string.Empty;
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = string.Empty;
                task.ReviewDependencyStateFingerprint = string.Empty;
                task.LastRejectedReviewEvidenceFingerprint = string.Empty;
                task.LastRejectedReviewOutcomeFingerprint = string.Empty;
                task.NextAgentMessage =
                    "[OperatorVerifiedRework] An operator verified the following defect against current persisted artifact evidence. Preserve valid artifacts and execution memory, correct only this card's owned outputs, and return them for fresh evidence-based review:\n\n" +
                    verifiedFinding;

                KanbanTask root = window.GetTaskRoot(task);
                if (root != null)
                {
                    window.RestoreImplementationWorkflowRoot(
                        root,
                        task);
                    TouchTask(root);
                }

                TouchTask(task);
                window.MarkDirty();
                window.SaveBoard(showDialog: false);

                Debug.Log(
                    $"[Kanban] Requeued '{requestedTitle}' for operator-verified rework.");
            }
            finally
            {
                DestroyImmediate(window);
            }
        }


        /// <summary>
        /// Explicitly resumes one crashed or failed persisted attempt without
        /// discarding its execution memory or valid artifacts. The task title
        /// is read from AIRUNTIME_KANBAN_TASK_TITLE.
        /// </summary>
        public static void ResumeNamedTaskForCommandLine()
        {
            string requestedTitle =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_TASK_TITLE")?.Trim();

            if (string.IsNullOrWhiteSpace(requestedTitle))
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_TASK_TITLE must name exactly one persisted Kanban card.");
            }

            string boardPath = GetDefaultBoardPath();
            AIRuntimeKanbanSession window =
                CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                window._boardFilePath = boardPath;
                window.LoadBoard(showDialog: false);

                List<KanbanTask> matches =
                    window._board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                string.Equals(
                                    task.Title?.Trim(),
                                    requestedTitle,
                                    StringComparison.Ordinal))
                        .ToList();

                if (matches.Count != 1)
                {
                    throw new InvalidOperationException(
                        $"Expected exactly one Kanban card titled '{requestedTitle}', but found {matches.Count}.");
                }

                if (!window.ResumeFailedAttempt(matches[0]))
                {
                    throw new InvalidOperationException(
                        $"Kanban card '{requestedTitle}' is not a resumable crashed or failed attempt.");
                }

                window.SaveBoard(showDialog: false);

                Debug.Log(
                    $"[Kanban] Resumed '{requestedTitle}' while preserving execution memory and artifacts.");
            }
            finally
            {
                DestroyImmediate(window);
            }
        }


        /// <summary>
        /// Applies an operator-approved correction to one committed card's
        /// produced-artifact contract without rebuilding or resetting the
        /// board. Paths are read from semicolon-delimited
        /// AIRUNTIME_KANBAN_ARTIFACT_PATHS.
        /// </summary>
        public static void ExpandNamedTaskArtifactContractForCommandLine()
        {
            string requestedTitle =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_TASK_TITLE")?.Trim();
            string requestedPaths =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_ARTIFACT_PATHS");

            if (string.IsNullOrWhiteSpace(requestedTitle) ||
                string.IsNullOrWhiteSpace(requestedPaths))
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_TASK_TITLE and AIRUNTIME_KANBAN_ARTIFACT_PATHS are required.");
            }

            List<string> additions =
                NormalizeArtifactPaths(
                    requestedPaths.Split(
                        new[] { ';' },
                        StringSplitOptions.RemoveEmptyEntries));

            if (additions.Count == 0)
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_ARTIFACT_PATHS did not contain any valid Assets/... paths.");
            }

            string boardPath = GetDefaultBoardPath();
            AIRuntimeKanbanSession window =
                CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                window._boardFilePath = boardPath;
                window.LoadBoard(showDialog: false);

                List<KanbanTask> matches =
                    window._board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                string.Equals(
                                    task.Title?.Trim(),
                                    requestedTitle,
                                    StringComparison.Ordinal))
                        .ToList();

                if (matches.Count != 1)
                {
                    throw new InvalidOperationException(
                        $"Expected exactly one Kanban card titled '{requestedTitle}', but found {matches.Count}.");
                }

                KanbanTask task = matches[0];
                task.ProducedArtifactPaths =
                    NormalizeArtifactPaths(
                        (task.ProducedArtifactPaths ?? new List<string>())
                            .Concat(additions));

                foreach (string path in additions)
                {
                    KanbanTask conflictingOwner =
                        window._board.Tasks.FirstOrDefault(
                            candidate =>
                                candidate != null &&
                                !ReferenceEquals(candidate, task) &&
                                (candidate.ProducedArtifactPaths ?? new List<string>())
                                    .Any(
                                        ownedPath =>
                                            string.Equals(
                                                ownedPath?.Trim().Replace('\\', '/'),
                                                path,
                                                StringComparison.OrdinalIgnoreCase)));

                    if (conflictingOwner != null)
                    {
                        throw new InvalidOperationException(
                            $"Artifact '{path}' is already owned by '{conflictingOwner.Title}'.");
                    }
                }

                window.ReconcileAllTaskRelationships();

                if (task.Status == TaskStatus.Crashed ||
                    task.Status == TaskStatus.Failed)
                {
                    window.ResumeFailedAttempt(task);
                }

                task.NextAgentMessage =
                    "[ArtifactContractCorrection] AIRuntime added verified missing outputs to this card's ProducedArtifactPaths: " +
                    string.Join(", ", additions) +
                    ". Preserve valid persisted work and execution memory. Inspect current state, create only unfinished owned artifacts, and complete with a changed strategy.";

                TouchTask(task);
                window.MarkDirty();
                window.SaveBoard(showDialog: false);

                Debug.Log(
                    $"[Kanban] Expanded '{requestedTitle}' artifact contract with: {string.Join(", ", additions)}.");
            }
            finally
            {
                DestroyImmediate(window);
            }
        }


        /// <summary>
        /// Applies an operator-approved correction to one committed card's
        /// shared mutation contract without transferring ownership or editing
        /// board JSON directly. Resource keys are read from semicolon-delimited
        /// AIRUNTIME_KANBAN_RESOURCE_KEYS. Scene mutations must use the exact
        /// scene:Assets/.../.unity form enforced by the runner middleware.
        /// </summary>
        public static void ExpandNamedTaskMutationContractForCommandLine()
        {
            string requestedTitle =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_TASK_TITLE")?.Trim();

            string requestedKeys =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_RESOURCE_KEYS");

            string recoveryMessage =
                Environment.GetEnvironmentVariable(
                    "AIRUNTIME_KANBAN_RECOVERY_MESSAGE")?.Trim();

            if (string.IsNullOrWhiteSpace(requestedTitle) ||
                string.IsNullOrWhiteSpace(requestedKeys))
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_TASK_TITLE and AIRUNTIME_KANBAN_RESOURCE_KEYS are required.");
            }

            List<string> additions =
                NormalizeResourceKeys(
                    requestedKeys.Split(
                        new[] { ';' },
                        StringSplitOptions.RemoveEmptyEntries))
                .Where(
                    key =>
                        key.StartsWith("scene:Assets/", StringComparison.OrdinalIgnoreCase) &&
                        key.EndsWith(".unity", StringComparison.OrdinalIgnoreCase))
                .ToList();

            if (additions.Count == 0)
            {
                throw new InvalidOperationException(
                    "AIRUNTIME_KANBAN_RESOURCE_KEYS did not contain any valid scene:Assets/.../.unity keys.");
            }

            string boardPath = GetDefaultBoardPath();
            AIRuntimeKanbanSession window =
                CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                window._boardFilePath = boardPath;
                window.LoadBoard(showDialog: false);

                List<KanbanTask> matches =
                    window._board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                string.Equals(
                                    task.Title?.Trim(),
                                    requestedTitle,
                                    StringComparison.Ordinal))
                        .ToList();

                if (matches.Count != 1)
                {
                    throw new InvalidOperationException(
                        $"Expected exactly one Kanban card titled '{requestedTitle}', but found {matches.Count}.");
                }

                KanbanTask task = matches[0];
                task.MutatedResourceKeys =
                    NormalizeResourceKeys(
                        (task.MutatedResourceKeys ?? new List<string>())
                            .Concat(additions));

                window.ReconcileAllTaskRelationships();

                if (task.Status == TaskStatus.Crashed ||
                    task.Status == TaskStatus.Failed)
                {
                    window.ResumeFailedAttempt(task);
                }

                task.NextAgentMessage =
                    "[MutationContractCorrection] AIRuntime granted this card exact shared mutation authority for: " +
                    string.Join(", ", additions) +
                    ". Consumed artifacts remain read-only unless named by one of these resource keys. Inspect current state, preserve valid work, mutate only the declared scene, save it through Unity, and verify the remaining acceptance criteria." +
                    (string.IsNullOrWhiteSpace(recoveryMessage)
                        ? string.Empty
                        : " Operator-supplied recovery evidence: " + recoveryMessage);

                TouchTask(task);
                window.MarkDirty();
                window.SaveBoard(showDialog: false);

                Debug.Log(
                    $"[Kanban] Expanded '{requestedTitle}' mutation contract with: {string.Join(", ", additions)}.");
            }
            finally
            {
                DestroyImmediate(window);
            }
        }


        /// <summary>
        /// Opens the Kanban window and starts its scheduler after an explicit
        /// external invocation. This is intentionally separate from Open so a
        /// normal window restore remains side-effect free.
        /// </summary>
        public static void OpenAndRunAgentsForCommandLine()
        {
            AIRuntimeKanbanSession window =
                AIRuntimeKanbanScheduler.Session;

            window._agentStartsRemaining = -1;
            window.SetAutomaticAgentScheduling(true);
            window._statusMessage = "Agent scheduler started by explicit command.";
            window._agentQueueNotBeforeEditorTime =
                EditorApplication.timeSinceStartup;
            AIRuntimeKanbanWindow.Open();
            window.PumpAgentQueue();
        }


        /// <summary>
        /// Starts at most one eligible implementation or review run. The
        /// scheduler disables itself immediately after that run starts, so a
        /// recovery operator can inspect the persisted board before explicitly
        /// allowing another step.
        /// </summary>
        public static void OpenAndRunSingleAgentStepForCommandLine()
        {
            AIRuntimeKanbanSession window =
                AIRuntimeKanbanScheduler.Session;

            window._board.SchedulerEnabled = false;
            if (!window.PersistSchedulerState()) return;
            window._agentStartsRemaining = 1;
            window._autoRunAgents = true;
            window._statusMessage =
                "Bounded agent step started by explicit command.";
            window._agentQueueNotBeforeEditorTime =
                EditorApplication.timeSinceStartup;
            AIRuntimeKanbanWindow.Open();
            window.PumpAgentQueue();
        }


        private void OnEnable()
        {
            // OnDisable can run during an AssetDatabase refresh even when no
            // managed-domain reload follows. Never carry the transient reload
            // guard into the re-enabled window or valid agent completions will
            // be silently discarded.
            _isPreparingForAssemblyReload =
                false;

            // Opening or restoring the Kanban window must never start
            // agents without an explicit user action.
            _autoRunAgents =
                false;

            if (_orchestrationSettingsVersion <
                CurrentOrchestrationSettingsVersion)
            {
                _autoDecompose =
                    true;

                _orchestrationSettingsVersion =
                    CurrentOrchestrationSettingsVersion;
            }

            if (!string.IsNullOrWhiteSpace(_initialServiceBoardPath))
                _boardFilePath = _initialServiceBoardPath;

            if (string.IsNullOrWhiteSpace(
                    _boardFilePath))
            {
                _boardFilePath =
                    GetDefaultBoardPath();
            }


            if (File.Exists(
                    _boardFilePath))
            {
                LoadBoard(
                    showDialog: false);
            }


            LoadAgentActivationState();
            RefreshAgentOptions();

            // A cold window open remains manual. A managed-domain reload,
            // however, must preserve an explicitly running scheduler so a
            // script-producing task can continue into Review afterwards.
            _autoRunAgents = ConsumeRunAgentsReloadState() || (IsServiceSession && _board.SchedulerEnabled);

            int reloadPausedCount =
                SessionState.GetInt(
                    ReloadPausedCountKey,
                    0);

            if (reloadPausedCount > 0)
            {
                _statusMessage =
                    _autoRunAgents
                        ? $"Unity recompiled scripts; safely returned {reloadPausedCount} interrupted run(s) to their queues and resumed the agents."
                        : $"Unity recompiled scripts; safely returned {reloadPausedCount} interrupted run(s) to their queues. Press Run agents to continue.";

                SessionState.SetInt(
                    ReloadPausedCountKey,
                    0);
            }


        }


        private void OnFocus()
        {
            RefreshAgentOptions();
            RecoverOrphanedAgentRuns();
        }


        private void OnDisable()
        {
            if (!_isPreparingForAssemblyReload &&
                EditorApplication.isCompiling)
            {
                PrepareForAssemblyReload();
            }

            EditorApplication.update -=
                PumpAgentQueue;

            AssemblyReloadEvents.beforeAssemblyReload -=
                PrepareForAssemblyReload;

            List<ActiveAgentRun> activeRuns =
                _activeAgentRuns.Values.ToList();

            _activeAgentRuns.Clear();


            foreach (
                ActiveAgentRun run
                in activeRuns)
            {
                run.Cancellation.Cancel();
                run.Cancellation.Dispose();
            }

            if (_autoSaveScheduled)
            {
                EditorApplication.delayCall -=
                    PerformAutoSave;

                _autoSaveScheduled =
                    false;
            }


            if (_isDirty &&
                _autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }
            _schedulerOwnerLock?.Dispose();
            _schedulerOwnerLock = null;
            EditorApplication.delayCall -= PerformExecutionMemorySave;
        }


        internal void PrepareForAssemblyReload()
        {
            if (_isPreparingForAssemblyReload)
            {
                return;
            }

            _isPreparingForAssemblyReload =
                true;

            // Tool events are emitted before script imports trigger the
            // reload. Commit their journal snapshots before pausing runs so
            // the next domain can continue with the latest durable memory.
            DrainPendingExecutionMemoryUpdates();

            // Scene mutations are otherwise only resident in Unity's loaded
            // scene. Persist dirty scenes that the active card explicitly
            // owns before the managed domain and agent run are torn down.
            // Pathless scenes are intentionally excluded so this can never
            // open Unity's interactive Save As dialog.
            CheckpointActiveRunScenesForAssemblyReload();

            SessionState.SetBool(
                ReloadResumeAgentsKey,
                _autoRunAgents);

            foreach (var run in _activeAgentRuns.Values)
            {
                var card = FindTask(run.TaskId);
                if (card?.Dispatch?.Id == run.AttemptId && card.Dispatch != null)
                {
                    card.Dispatch.Outcome = "Interrupted";
                    card.Dispatch.LeaseUntilUtc = string.Empty;
                    MarkDirty();
                }
            }
            int pausedCount =
                PauseActiveRunsForReload(
                    _board?.Tasks);

            // Stop provider transports and tool continuations while the
            // current domain is still valid. Waiting for OnDisable is too
            // late: UnityWebRequest download handlers may otherwise be
            // finalized after their GC handles belong to the old domain.
            foreach (ActiveAgentRun run in _activeAgentRuns.Values)
            {
                if (run == null)
                {
                    continue;
                }

                if (!string.IsNullOrWhiteSpace(
                        run.TaskId))
                {
                    _discardedAgentRunTaskIds.Add(
                        run.TaskId);
                }

                try
                {
                    run.Cancellation?.Cancel();
                }
                catch (ObjectDisposedException)
                {
                    // A completion may have won the race with compilation.
                }
            }

            // DownloadHandlerScript stores a native GC handle. Cancellation
            // aborts the logical run, while explicit disposal here guarantees
            // the handle is released before Unity unloads this managed domain.
            AIRuntime.Runtime.Providers.ProviderRequestLifetime
                .AbortAndDisposeAll();

            if (pausedCount > 0 || _isDirty)
            {
                SessionState.SetInt(
                    ReloadPausedCountKey,
                    pausedCount);

                _isDirty =
                    true;

                // A script compilation destroys the current managed domain.
                // Persist synchronously even when normal auto-save is off so
                // no task remains falsely recorded as running.
                SaveBoard(
                    showDialog: false);
            }
        }


        private static bool ConsumeRunAgentsReloadState()
        {
            bool resume =
                SessionState.GetBool(
                    ReloadResumeAgentsKey,
                    false);

            SessionState.SetBool(
                ReloadResumeAgentsKey,
                false);

            return resume;
        }


        private void OnGUI()
        {
            DrawToolbar();

            using (new EditorGUILayout.HorizontalScope(GUILayout.ExpandHeight(true)))
            {
                DrawAgentStatusWidget();
                DrawBoard();
            }

            DrawStatusBar();
        }


        private void DrawToolbar()
        {
            using (
                new EditorGUILayout.HorizontalScope(
                    EditorStyles.toolbar))
            {
                if (GUILayout.Button(
                        "New task",
                        EditorStyles.toolbarButton))
                {
                    CreateTask();
                }


                GUILayout.Space(
                    8);

                GUILayout.Label(
                    "Search",
                    EditorStyles.miniLabel);

                _search =
                    GUILayout.TextField(
                        _search,
                        EditorStyles.toolbarSearchField,
                        GUILayout.MinWidth(
                            120),
                        GUILayout.MaxWidth(
                            280));


                if (!string.IsNullOrEmpty(
                        _search) &&
                    GUILayout.Button(
                        "×",
                        EditorStyles.toolbarButton,
                        GUILayout.Width(
                            22)))
                {
                    _search =
                        string.Empty;

                    GUI.FocusControl(
                        null);
                }


                GUILayout.FlexibleSpace();


                _autoSave =
                    GUILayout.Toggle(
                        _autoSave,
                        "Auto-save",
                        EditorStyles.toolbarButton);

                bool runAgents = GUILayout.Toggle(
                        _autoRunAgents,
                        "Run agents",
                        EditorStyles.toolbarButton);
                if (runAgents != _autoRunAgents)
                    SetAutomaticAgentScheduling(runAgents);

                bool autoDecompose =
                    GUILayout.Toggle(
                        _autoDecompose,
                        "Auto-decompose",
                        EditorStyles.toolbarButton);

                if (autoDecompose !=
                    _autoDecompose)
                {
                    _autoDecompose =
                        autoDecompose;

                    EditorUtility.SetDirty(
                        this);
                }

                if (GUILayout.Button(
                        "Agents",
                        EditorStyles.toolbarButton))
                {
                    AIRuntimeKanbanAgentsWindow.Open(
                        this);
                }

                if (GUILayout.Button(
                        "Graph",
                        EditorStyles.toolbarButton))
                {
                    AIRuntimeKanbanGraphWindow.Open(
                        this);
                }

                if (GUILayout.Button(
                        "Activity",
                        EditorStyles.toolbarButton))
                {
                    AIRuntimeAgentActivityWindow.Open();
                }


                if (GUILayout.Button(
                        "Load",
                        EditorStyles.toolbarButton))
                {
                    LoadBoard(
                        showDialog: true);
                }


                if (GUILayout.Button(
                        "Save",
                        EditorStyles.toolbarButton))
                {
                    SaveBoard(
                        showDialog: true);
                }


                if (GUILayout.Button(
                        "File…",
                        EditorStyles.toolbarButton))
                {
                    SelectBoardFile();
                }
            }
        }


        private string DrawThreeLineWrappedTextArea(
            string label,
            string value,
            ref Vector2 scrollPosition)
        {
            if (_wrappedTaskTextAreaStyle == null)
            {
                _wrappedTaskTextAreaStyle =
                    new GUIStyle(
                        EditorStyles.textArea)
                    {
                        wordWrap = true
                    };

                Color editorTextColor =
                    EditorGUIUtility.isProSkin
                        ? Color.white
                        : Color.black;

                _wrappedTaskTextAreaStyle.normal.textColor = editorTextColor;
                _wrappedTaskTextAreaStyle.hover.textColor = editorTextColor;
                _wrappedTaskTextAreaStyle.active.textColor = editorTextColor;
                _wrappedTaskTextAreaStyle.focused.textColor = editorTextColor;
            }

            value = value ??
                    string.Empty;

            float visibleHeight =
                EditorGUIUtility.singleLineHeight *
                3f +
                _wrappedTaskTextAreaStyle.padding.vertical;

            float contentWidth =
                Mathf.Max(
                    100f,
                    EditorGUIUtility.currentViewWidth -
                    EditorGUIUtility.labelWidth -
                    42f);

            float contentHeight =
                Mathf.Max(
                    visibleHeight,
                    _wrappedTaskTextAreaStyle.CalcHeight(
                        new GUIContent(
                            value + "\n"),
                        contentWidth));

            using (
                new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField(
                    label,
                    GUILayout.Width(
                        EditorGUIUtility.labelWidth - 4));

                scrollPosition =
                    EditorGUILayout.BeginScrollView(
                        scrollPosition,
                        false,
                        true,
                        GUILayout.Height(
                            visibleHeight));

                value =
                    EditorGUILayout.TextArea(
                        value,
                        _wrappedTaskTextAreaStyle,
                        GUILayout.Height(
                            contentHeight),
                        GUILayout.ExpandWidth(
                            true));

                EditorGUILayout.EndScrollView();
            }

            return value;
        }


        private void DrawTaskEditor(
            KanbanTask task)
        {
            EditorGUILayout.BeginVertical(
                EditorStyles.helpBox,
                GUILayout.ExpandHeight(
                    true));


            using (
                new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField(
                    "Task details",
                    EditorStyles.boldLabel);

                GUILayout.FlexibleSpace();

                GUILayout.Label(
                    task.Id,
                    EditorStyles.miniLabel);
            }

            if (DrawTaskRelationshipNavigation(
                    task))
            {
                EditorGUILayout.EndVertical();
                return;
            }

            _taskEditorScroll =
                EditorGUILayout.BeginScrollView(
                    _taskEditorScroll,
                    GUILayout.ExpandHeight(
                        true));


            string previousTitle = task.Title;
            string previousDescription = task.Description;
            string previousAcceptanceCriteria = task.AcceptanceCriteria;
            DrawHumanVisualReview(task);


            EditorGUI.BeginChangeCheck();


            task.Title =
                EditorGUILayout.TextField(
                    "Title",
                    task.Title);

            TaskStatus requestedStatus =
                (TaskStatus)EditorGUILayout.EnumPopup(
                    "Status",
                    task.Status);

            if (requestedStatus != task.Status)
            {
                TrySetTaskStatus(
                    task,
                    requestedStatus,
                    out _statusMessage);
            }

            using (new EditorGUI.DisabledScope(true))
            {
                EditorGUILayout.TextField(
                    "Workflow phase",
                    GetWorkflowPhase(task));
                EditorGUILayout.IntField(
                    "Plan revision",
                    GetTaskRoot(task)?.PlanRevision ?? 0);
            }

            task.Priority =
                (TaskPriority)EditorGUILayout.EnumPopup(
                    "Priority",
                    task.Priority);

            task.AssignedAgent =
                DrawAssignedAgentPopup(
                    task.AssignedAgent);

            task.ParentTaskId =
                DrawParentTaskPopup(
                    task);

            int childCount =
                GetDirectChildren(
                        task)
                    .Count;

            using (
                new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField(
                    "Children",
                    childCount.ToString());

                if (GUILayout.Button(
                        "Add child task",
                        EditorStyles.miniButton,
                        GUILayout.Width(
                            110)))
                {
                    CreateTask(
                        TaskStatus.Backlog,
                        task);
                }
            }


            task.Tags =
                EditorGUILayout.TextField(
                    "Tags",
                    task.Tags);

            task.DueDate =
                EditorGUILayout.TextField(
                    "Due date",
                    task.DueDate);

            task.Estimate =
                Mathf.Max(
                    0,
                    EditorGUILayout.IntField(
                        "Estimate",
                        task.Estimate));


            task.Description =
                DrawThreeLineWrappedTextArea(
                    "Description",
                    task.Description,
                    ref _taskDescriptionScroll);

            GUILayout.Space(
                EditorGUIUtility.singleLineHeight *
                0.5f);


            task.AcceptanceCriteria =
                DrawThreeLineWrappedTextArea(
                    "Acceptance criteria",
                    task.AcceptanceCriteria,
                    ref _acceptanceCriteriaScroll);


            if (EditorGUI.EndChangeCheck())
            {
                InvalidateAtomicLeafSuppressionForContractChange(
                    task,
                    previousTitle,
                    previousDescription,
                    previousAcceptanceCriteria);

                TouchTask(
                    task);

                ReconcileAllTaskRelationships();

                MarkDirty();
            }


            DrawCardMemory(task);

            if (!string.IsNullOrWhiteSpace(
                    task.ExecutionMemory))
            {
                EditorGUILayout.LabelField(
                    "Execution memory",
                    EditorStyles.boldLabel);

                _executionMemoryScroll =
                    EditorGUILayout.BeginScrollView(
                        _executionMemoryScroll,
                        GUILayout.MinHeight(90),
                        GUILayout.MaxHeight(220));

                EditorGUILayout.SelectableLabel(
                    task.ExecutionMemory,
                    EditorStyles.textArea,
                    GUILayout.MinHeight(
                        GetAgentOutputHeight(
                            task.ExecutionMemory)));

                EditorGUILayout.EndScrollView();
            }


            if (!string.IsNullOrWhiteSpace(
                    task.AgentError))
            {
                EditorGUILayout.HelpBox(
                    task.AgentError,
                    MessageType.Error);
            }


            if (!string.IsNullOrWhiteSpace(
                    task.AgentResult))
            {
                EditorGUILayout.LabelField(
                    "Agent output",
                    EditorStyles.boldLabel);

                _agentOutputScroll =
                    EditorGUILayout.BeginScrollView(
                        _agentOutputScroll,
                        GUILayout.MinHeight(
                            90),
                        GUILayout.MaxHeight(
                            220));

                EditorGUILayout.SelectableLabel(
                    task.AgentResult,
                    EditorStyles.textArea,
                    GUILayout.MinHeight(
                        GetAgentOutputHeight(
                            task.AgentResult)));

                EditorGUILayout.EndScrollView();
            }


            if (!string.IsNullOrWhiteSpace(
                    task.ReviewResult))
            {
                EditorGUILayout.Space(
                    4);

                EditorGUILayout.LabelField(
                    $"Review result ({task.ReviewStatus})",
                    EditorStyles.boldLabel);

                EditorGUILayout.SelectableLabel(
                    task.ReviewResult,
                    EditorStyles.textArea,
                    GUILayout.MinHeight(
                        GetAgentOutputHeight(
                            task.ReviewResult)));
            }


            EditorGUILayout.Space(
                4);

            using (
                new EditorGUILayout.HorizontalScope())
            {
                EditorGUILayout.LabelField(
                    "Message for next agent",
                    EditorStyles.boldLabel);

                using (
                    new EditorGUI.DisabledScope(
                        string.IsNullOrWhiteSpace(
                            task.AgentResult)))
                {
                    if (GUILayout.Button(
                            "Use agent output",
                            EditorStyles.miniButton,
                            GUILayout.Width(
                                112)))
                    {
                        task.NextAgentMessage =
                            task.AgentResult;

                        TouchTask(
                            task);

                        MarkDirty();
                    }
                }
            }

            EditorGUI.BeginChangeCheck();

            _nextAgentMessageScroll =
                EditorGUILayout.BeginScrollView(
                    _nextAgentMessageScroll,
                    GUILayout.MinHeight(
                        64),
                    GUILayout.MaxHeight(
                        150));

            task.NextAgentMessage =
                EditorGUILayout.TextArea(
                    task.NextAgentMessage ??
                    string.Empty,
                    GUILayout.ExpandHeight(
                        true));

            EditorGUILayout.EndScrollView();


            if (EditorGUI.EndChangeCheck())
            {
                TouchTask(
                    task);

                MarkDirty();
            }

            EditorGUILayout.EndScrollView();

            EditorGUILayout.EndVertical();
        }


        private static bool InvalidateAtomicLeafSuppressionForContractChange(
            KanbanTask task,
            string previousTitle,
            string previousDescription,
            string previousAcceptanceCriteria)
        {
            if (task == null)
                return false;

            bool contractChanged =
                !string.Equals(
                    previousTitle ?? string.Empty,
                    task.Title ?? string.Empty,
                    StringComparison.Ordinal) ||
                !string.Equals(
                    previousDescription ?? string.Empty,
                    task.Description ?? string.Empty,
                    StringComparison.Ordinal) ||
                !string.Equals(
                    previousAcceptanceCriteria ?? string.Empty,
                    task.AcceptanceCriteria ?? string.Empty,
                    StringComparison.Ordinal);

            if (!contractChanged)
                return false;

            bool invalidated =
                task.SuppressAutoDecomposition ||
                (task.NextAgentMessage ?? string.Empty).StartsWith(
                    "[AssociationLeaf]",
                    StringComparison.Ordinal);

            task.SuppressAutoDecomposition = false;

            if ((task.NextAgentMessage ?? string.Empty).StartsWith(
                    "[AssociationLeaf]",
                    StringComparison.Ordinal))
            {
                task.NextAgentMessage = string.Empty;
            }

            return invalidated;
        }


        private bool DrawTaskRelationshipNavigation(
            KanbanTask task)
        {
            KanbanTask parent =
                FindTask(
                    task.ParentTaskId);

            if (parent != null)
            {
                if (GUILayout.Button(
                        $"Open parent: {parent.Title}",
                        EditorStyles.miniButton))
                {
                    SelectTask(parent);
                    return true;
                }
            }

            List<KanbanTask> children =
                GetDirectChildren(task)
                    .OrderBy(
                        child => child.Title)
                    .ThenBy(
                        child => child.CreatedAt)
                    .ToList();

            if (children.Count == 0)
            {
                return false;
            }

            string[] options =
                new[]
                {
                    $"Select child ({children.Count})"
                }
                .Concat(
                    children.Select(
                        child =>
                            $"{child.Title} [{GetStatusLabel(child.Status)}]"))
                .ToArray();

            int selected =
                EditorGUILayout.Popup(
                    "Open child",
                    0,
                    options);

            if (selected > 0)
            {
                SelectTask(
                    children[selected - 1]);

                return true;
            }

            return false;
        }


        internal void DrawTaskDetailsWindow(
            string taskId)
        {
            KanbanTask task =
                FindTask(
                    taskId);


            if (task == null)
            {
                EditorGUILayout.HelpBox(
                    "This task is no longer available. Select another card from the Kanban board.",
                    MessageType.Info);

                return;
            }


            DrawTaskEditor(
                task);
        }


        private List<AgentStatusRow> BuildAgentStatusRows()
        {
            var ids = (_agentOptions ?? new List<AgentOption>()).Select(option => option.Id)
                .Concat(_activeKanbanAgentIds).Concat(_activeAgentRuns.Keys)
                .Where(id => !string.IsNullOrWhiteSpace(id)).Distinct(StringComparer.Ordinal)
                .OrderByDescending(IsKanbanAgentActive).ThenBy(id => id, StringComparer.Ordinal);
            var rows = new List<AgentStatusRow>();
            foreach (string id in ids)
            {
                var row = new AgentStatusRow { AgentId = id, Status = "Idle", Detail = "No queued work for this agent." };
                _activeAgentRuns.TryGetValue(id, out ActiveAgentRun run);
                row.Task = FindTask(run?.TaskId);
                if (row.Task != null)
                {
                    row.Status = run.CoordinationOnly ? "Planning" : row.Task.Status == TaskStatus.Review ? "Reviewing" : "Running";
                    row.Detail = "Working on " + row.Task.Title;
                    rows.Add(row);
                    continue;
                }

                var candidates = (_board?.Tasks ?? new List<KanbanTask>()).Where(task => task != null &&
                    task.Status != TaskStatus.Done && !task.IsDecompositionDraft &&
                    ResolveWorkflowAgentId(task, task.Status == TaskStatus.Review) == id &&
                    (!string.IsNullOrWhiteSpace(task.ParentTaskId) || IsPlanningCoordinator(task) ||
                     task.Status == TaskStatus.Review || !HasIncompleteCommittedChildTasks(task)))
                    .OrderBy(task => IsEligibleForCurrentWorkflowPhase(task, task.Status == TaskStatus.Review) ? 0 : 1)
                    .ThenBy(task => {
                        string blocker = GetTaskSchedulingBlocker(task);
                        return blocker == "Eligible" || blocker == "Provider cooldown" || blocker == "Agent is inactive" ? 0 : 1;
                    })
                    .ThenBy(task => task.Status == TaskStatus.Todo ? 0 : 1)
                    .ThenByDescending(task => task.Priority).ThenBy(task => task.CreatedAt).ToList();
                row.Task = candidates.FirstOrDefault();
                if (!IsKanbanAgentActive(id))
                {
                    row.Status = "Inactive";
                    row.Detail = "Enable this agent in Agents to allow it to run.";
                }
                else if (IsAgentProviderCoolingDown(id, out ProviderCooldownState cooldown))
                {
                    DateTimeOffset until = DateTimeOffset.Parse(cooldown.UntilUtc, CultureInfo.InvariantCulture);
                    int seconds = Math.Max(0, (int)Math.Ceiling((until - DateTimeOffset.UtcNow).TotalSeconds));
                    row.Status = $"Cooldown {seconds / 60:00}:{seconds % 60:00}";
                    row.Detail = $"{cooldown.ProviderName} · retry at {until.ToLocalTime():HH:mm:ss}. {cooldown.Reason}";
                }
                else if (row.Task != null)
                {
                    string blocker = GetTaskSchedulingBlocker(row.Task);
                    row.Dependency = GetAuthoritativeDependencyTaskIds(row.Task).Select(FindTask)
                        .FirstOrDefault(dependency => dependency != null && dependency.Status != TaskStatus.Done);
                    if (row.Task.Status == TaskStatus.InProgress || row.Task.ReviewStatus == "Running")
                    {
                        row.Status = "Recovering";
                        row.Detail = "Waiting for the scheduler to reconcile this recorded run.";
                    }
                    else if (GetSchedulerGate() != "Ready")
                    {
                        row.Status = "Paused";
                        row.Detail = GetSchedulerGate();
                    }
                    else
                    {
                        row.Status = blocker == "Eligible" ? "Queued" : "Waiting";
                        row.Detail = row.Dependency != null ? "Waiting for " + row.Dependency.Title
                            : blocker == "Eligible" ? "Next card ready to start." : blocker;
                    }
                }
                rows.Add(row);
            }
            return rows;
        }

        private void DrawAgentStatusWidget()
        {
            var rows = BuildAgentStatusRows();
            _agentStatusWidth = _showAgentStatus ? Mathf.Clamp(position.width * .2f, 220f, 290f) : 100f;
            float height = Mathf.Max(MinimumBoardHeight, position.height - 70);
            using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.Width(_agentStatusWidth), GUILayout.Height(height)))
            {
                _showAgentStatus = EditorGUILayout.Foldout(_showAgentStatus, $"Agents ({rows.Count})", true);
                if (!_showAgentStatus) return;
                var wrap = new GUIStyle(EditorStyles.miniLabel) { wordWrap = true };
                GUILayout.Label(GetSchedulerGate() == "Ready" ? "Automatic queue enabled" : GetSchedulerGate(), wrap);
                _agentStatusScroll = GUILayout.BeginScrollView(_agentStatusScroll, false, false,
                    GUIStyle.none, GUI.skin.verticalScrollbar, GUILayout.Width(_agentStatusWidth - 8), GUILayout.ExpandHeight(true));
                if (rows.Count == 0) GUILayout.Label("No agents configured. Open Agents to get started.", wrap);
                foreach (var row in rows)
                {
                    using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox, GUILayout.Width(_agentStatusWidth - 30)))
                    {
                        bool running = row.Status == "Running" || row.Status == "Reviewing" || row.Status == "Planning";
                        Color color = running ? new Color(.30f, .75f, .45f) : row.Status.StartsWith("Cooldown", StringComparison.Ordinal) ||
                            row.Status == "Waiting" || row.Status == "Paused" ? new Color(.95f, .65f, .20f) : Color.gray;
                        using (new EditorGUILayout.HorizontalScope())
                        {
                            Rect dot = GUILayoutUtility.GetRect(9, EditorGUIUtility.singleLineHeight, GUILayout.Width(9));
                            EditorGUI.DrawRect(new Rect(dot.x, dot.y + 6, 6, 6), color);
                            GUILayout.Label(new GUIContent(row.AgentId, row.AgentId), EditorStyles.miniBoldLabel,
                                GUILayout.Width(_agentStatusWidth - 66));
                        }
                        GUILayout.Label(new GUIContent(row.Status, row.Detail), wrap);
                        var taskButton = new GUIStyle(EditorStyles.miniButton)
                        {
                            wordWrap = true, alignment = TextAnchor.MiddleLeft, fixedHeight = 0,
                            clipping = TextClipping.Clip, padding = new RectOffset(5, 5, 5, 5)
                        };
                        DrawAgentStatusTaskLink(row.Task, running ? "Working: " : "Next: ", row.Detail, taskButton);
                        DrawAgentStatusTaskLink(row.Dependency, "Waiting for: ", row.Detail, taskButton);
                        if (!string.IsNullOrWhiteSpace(row.Detail)) GUILayout.Label(row.Detail, wrap, GUILayout.Width(_agentStatusWidth - 44));
                    }
                }
                EditorGUILayout.EndScrollView();
            }
        }

        private void DrawAgentStatusTaskLink(KanbanTask task, string prefix, string detail, GUIStyle style)
        {
            if (task == null) return;
            float width = _agentStatusWidth - 44;
            var content = new GUIContent(prefix + task.Title, task.Title + "\n" + detail);
            float height = Mathf.Max(30, style.CalcHeight(content, width) + 2);
            if (GUILayout.Button(content, style, GUILayout.Width(width), GUILayout.Height(height))) SelectTask(task);
        }

        internal void OnInspectorUpdate()
        {
            if (!_showAgentStatus || EditorApplication.timeSinceStartup < _nextAgentStatusRepaint) return;
            _nextAgentStatusRepaint = EditorApplication.timeSinceStartup + 1;
            Repaint();
        }

        private void DrawBoard()
        {
            float boardHeight =
                Mathf.Max(
                    MinimumBoardHeight,
                    position.height -
                    70);


            _boardScroll =
                EditorGUILayout.BeginScrollView(
                    _boardScroll,
                    true,
                    false,
                    GUILayout.Height(
                        boardHeight),
                    GUILayout.ExpandWidth(
                        true));

            EditorGUILayout.BeginHorizontal(
                GUILayout.ExpandHeight(
                    true));


            TaskStatus[] statuses =
                (TaskStatus[])Enum.GetValues(
                    typeof(TaskStatus));

            float availableWidth =
                Mathf.Max(
                    position.width - _agentStatusWidth - 34,
                    MinimumColumnWidth *
                    statuses.Length);

            float columnWidth =
                Mathf.Max(
                    MinimumColumnWidth,
                    availableWidth /
                    statuses.Length);


            for (
                int index = 0;
                index < statuses.Length;
                index++)
            {
                DrawColumn(
                    statuses[index],
                    index,
                    columnWidth,
                    boardHeight - 24);
            }


            EditorGUILayout.EndHorizontal();

            EditorGUILayout.EndScrollView();
        }


        private void DrawColumn(
            TaskStatus status,
            int columnIndex,
            float width,
            float height)
        {
            EditorGUILayout.BeginVertical(
                EditorStyles.helpBox,
                GUILayout.Width(
                    width),
                GUILayout.Height(
                    height));


            List<KanbanTask> tasks =
                GetVisibleTasks(
                    status);


            using (
                new EditorGUILayout.HorizontalScope())
            {
                GUILayout.Label(
                    GetStatusLabel(
                        status),
                    EditorStyles.boldLabel);

                GUILayout.FlexibleSpace();

                GUILayout.Label(
                    tasks.Count.ToString(),
                    EditorStyles.miniLabel);


                if (GUILayout.Button(
                        "+",
                        EditorStyles.miniButton,
                        GUILayout.Width(
                            24)))
                {
                    CreateTask(
                        status);
                }
            }


            _columnScroll[columnIndex] =
                EditorGUILayout.BeginScrollView(
                    _columnScroll[columnIndex],
                    GUILayout.ExpandHeight(
                        true));


            foreach (
                KanbanTask task
                in tasks)
            {
                DrawTaskCard(
                    task);
            }


            GUILayout.FlexibleSpace();

            EditorGUILayout.EndScrollView();

            EditorGUILayout.EndVertical();


            Rect columnRect =
                GUILayoutUtility.GetLastRect();

            HandleColumnDrop(
                columnRect,
                status);
        }


        private void DrawTaskCard(
            KanbanTask task)
        {
            bool expanded =
                IsTaskCardExpanded(
                    task);

            Color previousColor =
                GUI.backgroundColor;

            GUI.backgroundColor =
                GetTaskCardColor(
                    task);


            EditorGUILayout.BeginVertical(
                EditorStyles.helpBox);

            GUI.backgroundColor =
                previousColor;


            using (
                new EditorGUILayout.HorizontalScope())
            {
                if (GUILayout.Button(
                        expanded ? "-" : "+",
                        EditorStyles.miniButton,
                        GUILayout.Width(
                            20)))
                {
                    expanded =
                        !expanded;

                    _taskCardExpansionOverrides[task.Id] =
                        expanded;
                }

                GUILayout.Label(
                    new string(
                        '>',
                        GetTaskDepth(
                            task)) +
                    (GetTaskDepth(task) > 0
                        ? " "
                        : string.Empty) +
                    task.Title,
                    EditorStyles.boldLabel,
                    GUILayout.ExpandWidth(
                        true));

                GUILayout.Label(
                    GetPriorityLabel(
                        task.Priority),
                    EditorStyles.miniLabel,
                    GUILayout.Width(
                        52));
            }


            if (expanded)
            {
            KanbanTask parentTask =
                FindTask(
                    task.ParentTaskId);

            if (parentTask != null)
            {
                GUILayout.Label(
                    $"Parent: {parentTask.Title}",
                    EditorStyles.miniLabel);
            }

            int directChildCount =
                GetDirectChildren(
                        task)
                    .Count;

            if (directChildCount > 0)
            {
                GUILayout.Label(
                    $"Children: {directChildCount}" +
                    (HasIncompleteChildTasks(task)
                        ? " (pending)"
                        : " (complete)"),
                    EditorStyles.miniLabel);
            }

            if (!string.IsNullOrWhiteSpace(
                    task.Description))
            {
                GUILayout.Label(
                    Truncate(
                        task.Description,
                        120),
                    EditorStyles.wordWrappedMiniLabel);
            }


            if (!string.IsNullOrWhiteSpace(
                    task.AssignedAgent))
            {
                GUILayout.Label(
                    $"Agent: {task.AssignedAgent}",
                    EditorStyles.miniLabel);
            }

            if (!string.IsNullOrWhiteSpace(
                    task.AgentResult))
            {
                GUILayout.Label(
                    "Agent output available",
                    EditorStyles.miniLabel);
            }


            if (!string.IsNullOrWhiteSpace(
                    task.Tags))
            {
                GUILayout.Label(
                    $"Tags: {task.Tags}",
                    EditorStyles.miniLabel);
            }


            using (
                new EditorGUILayout.HorizontalScope())
            {
                using (
                    new EditorGUI.DisabledScope(
                        task.Status ==
                        TaskStatus.Backlog))
                {
                    if (GUILayout.Button(
                            "◀",
                            EditorStyles.miniButton,
                            GUILayout.Width(
                                28)))
                    {
                        MoveTask(
                            task,
                            -1);
                    }
                }


                bool taskIsRunning =
                    IsTaskRunning(
                        task);

                bool providerRetryAvailable =
                    !taskIsRunning &&
                    string.Equals(
                        task.ReviewStatus,
                        "Retry Available",
                        StringComparison.Ordinal);

                bool failedAttemptResumeAvailable =
                    !taskIsRunning &&
                    (task.Status == TaskStatus.Crashed ||
                     task.Status == TaskStatus.Failed);

                if (GUILayout.Button(
                        taskIsRunning
                            ? "Stop & Reset"
                            : providerRetryAvailable ||
                              failedAttemptResumeAvailable
                                ? "Resume"
                            : "Reset",
                        EditorStyles.miniButton))
                {
                    if (providerRetryAvailable)
                    {
                        ResumeProviderRetry(
                            task);
                    }
                    else if (failedAttemptResumeAvailable)
                    {
                        ResumeFailedAttempt(
                            task);
                    }
                    else
                    {
                        ResetTask(
                            task);
                    }
                }

                using (
                    new EditorGUI.DisabledScope(
                        taskIsRunning ||
                        string.IsNullOrWhiteSpace(task.AgentResult)))
                {
                    if (GUILayout.Button(
                            "Re-review",
                            EditorStyles.miniButton))
                    {
                        RequeueReview(
                            task);
                    }
                }


                if (GUILayout.Button(
                        "Delete",
                        EditorStyles.miniButton))
                {
                    DeleteTask(
                        task);
                }


                using (
                    new EditorGUI.DisabledScope(
                        task.Status ==
                            TaskStatus.Done ||
                        task.Status ==
                            TaskStatus.Crashed ||
                        task.Status ==
                            TaskStatus.Failed))
                {
                    if (GUILayout.Button(
                            "▶",
                            EditorStyles.miniButton,
                            GUILayout.Width(
                                28)))
                    {
                        MoveTask(
                            task,
                            1);
                    }
                }
            }
            }


            EditorGUILayout.EndVertical();


            Rect cardRect =
                GUILayoutUtility.GetLastRect();

            HandleCardDrag(
                cardRect,
                task);

            HandleCardSelection(
                cardRect,
                task);

            GUILayout.Space(
                4);
        }


        private bool IsTaskCardExpanded(
            KanbanTask task)
        {
            if (task != null &&
                !string.IsNullOrWhiteSpace(
                    task.Id) &&
                _taskCardExpansionOverrides.TryGetValue(
                    task.Id,
                    out bool expanded))
            {
                return expanded;
            }

            return task == null ||
                   task.Status != TaskStatus.Done ||
                   !string.Equals(
                       task.ReviewStatus,
                       "Passed",
                       StringComparison.OrdinalIgnoreCase);
        }


        private int GetTaskDepth(
            KanbanTask task)
        {
            int depth = 0;

            HashSet<string> visited =
                new HashSet<string>(
                    StringComparer.Ordinal);

            KanbanTask current = task;

            while (current != null &&
                   !string.IsNullOrWhiteSpace(current.ParentTaskId) &&
                   visited.Add(current.Id))
            {
                current = FindTask(current.ParentTaskId);

                if (current != null)
                {
                    depth++;
                }
            }

            return depth;
        }


        private void HandleCardSelection(
            Rect cardRect,
            KanbanTask task)
        {
            Event currentEvent =
                Event.current;


            if (currentEvent.type !=
                    EventType.MouseUp ||
                currentEvent.button != 0 ||
                !cardRect.Contains(
                    currentEvent.mousePosition))
            {
                return;
            }


            SelectTask(
                task);

            currentEvent.Use();
        }


        private void DrawStatusBar()
        {
            using (
                new EditorGUILayout.HorizontalScope(
                    EditorStyles.helpBox))
            {
                GUILayout.Label(
                    _statusMessage,
                    EditorStyles.miniLabel);

                GUILayout.Label("Scheduler: " + GetSchedulerGate(), EditorStyles.miniLabel);

                GUILayout.FlexibleSpace();

                if (_activeAgentRuns.Count > 0)
                {
                    GUILayout.Label(
                        $"{_activeAgentRuns.Count} agent run(s)",
                        EditorStyles.miniLabel);

                    GUILayout.Space(
                        8);
                }

                GUILayout.Label(
                    new GUIContent(
                        $"{_board.Tasks.Count} tasks · {Path.GetFileName(_boardFilePath)}",
                        _boardFilePath),
                    EditorStyles.miniLabel);
            }
        }


        private void HandleCardDrag(
            Rect cardRect,
            KanbanTask task)
        {
            Event currentEvent =
                Event.current;


            if (currentEvent.type !=
                    EventType.MouseDrag ||
                currentEvent.button != 0 ||
                !cardRect.Contains(
                    currentEvent.mousePosition))
            {
                return;
            }


            DragAndDrop.PrepareStartDrag();

            DragAndDrop.SetGenericData(
                DragDataKey,
                task.Id);

            DragAndDrop.StartDrag(
                task.Title);

            currentEvent.Use();
        }


        private void HandleColumnDrop(
            Rect columnRect,
            TaskStatus targetStatus)
        {
            Event currentEvent =
                Event.current;


            if (!columnRect.Contains(
                    currentEvent.mousePosition))
            {
                return;
            }


            string taskId =
                DragAndDrop.GetGenericData(
                    DragDataKey) as string;


            if (string.IsNullOrWhiteSpace(
                    taskId))
            {
                return;
            }


            switch (currentEvent.type)
            {
                case EventType.DragUpdated:
                    DragAndDrop.visualMode =
                        DragAndDropVisualMode.Move;

                    currentEvent.Use();

                    break;


                case EventType.DragPerform:
                    DragAndDrop.AcceptDrag();


                    KanbanTask task =
                        FindTask(
                            taskId);


                    if (task != null &&
                        task.Status !=
                        targetStatus)
                    {
                        TrySetTaskStatus(
                            task,
                            targetStatus,
                            out _statusMessage);
                    }


                    DragAndDrop.SetGenericData(
                        DragDataKey,
                        null);

                    currentEvent.Use();

                    break;
            }
        }


        private void CreateTask(
            TaskStatus status = TaskStatus.Backlog,
            KanbanTask parentTask = null)
        {
            string now =
                DateTime.UtcNow.ToString(
                    "O");

            KanbanTask task =
                new KanbanTask
                {
                    Id =
                        Guid.NewGuid()
                            .ToString("N"),

                    Title =
                        "New task",

                    Description =
                        string.Empty,

                    AcceptanceCriteria =
                        string.Empty,

                    ParentTaskId =
                        parentTask?.Id ??
                        string.Empty,

                    CreationSource =
                        parentTask == null
                            ? "Manual"
                            : "ManualChild",

                    CreatedStatus =
                        GetStatusLabel(status),

                    AssignedAgent =
                        string.Empty,

                    Tags =
                        string.Empty,

                    DueDate =
                        string.Empty,

                    Status =
                        status,

                    Priority =
                        TaskPriority.Normal,

                    CreatedAt =
                        now,

                    UpdatedAt =
                        now
                };


            _board.Tasks.Add(
                task);

            ReconcileAllTaskRelationships();

            if (parentTask != null)
            {
                TrySetTaskStatus(
                    parentTask,
                    TaskStatus.Todo,
                    out _statusMessage);
            }

            SelectTask(
                task);

            MarkDirty();

            _statusMessage =
                "Task created";
        }


        private void DeleteTask(
            KanbanTask task)
        {
            if (!EditorUtility.DisplayDialog(
                    "Delete task",
                    $"Delete '{task.Title}'?",
                    "Delete",
                    "Cancel"))
            {
                return;
            }


            _board.Tasks.Remove(
                task);

            foreach (KanbanTask child in GetDirectChildren(task))
            {
                child.ParentTaskId =
                    string.Empty;

                TouchTask(
                    child);
            }

            ReconcileAllTaskRelationships();


            if (_selectedTaskId ==
                task.Id)
            {
                _selectedTaskId =
                    null;
            }


            MarkDirty();

            _statusMessage =
                "Task deleted";
        }


        private void MoveTask(
            KanbanTask task,
            int direction)
        {
            int next =
                Mathf.Clamp(
                    (int)task.Status +
                    direction,
                    (int)TaskStatus.Backlog,
                    (int)TaskStatus.Failed);


            TrySetTaskStatus(
                task,
                (TaskStatus)next,
                out _statusMessage);
        }


        private bool TrySetTaskStatus(
            KanbanTask task,
            TaskStatus requestedStatus,
            out string message)
        {
            if (task == null)
            {
                message =
                    "Task is unavailable";

                return false;
            }

            if (requestedStatus == TaskStatus.Done && task.HumanVisualReviewRequired &&
                !HasCurrentHumanVisualApproval(task))
            {
                message = "Human visual approval is required before this card can finish.";
                return false;
            }

            if (requestedStatus == TaskStatus.Todo &&
                string.IsNullOrWhiteSpace(task.ParentTaskId) &&
                string.IsNullOrWhiteSpace(task.WorkflowPhase))
            {
                task.WorkflowPhase =
                    _autoDecompose
                        ? WorkflowPhasePlanning
                        : WorkflowPhaseImplementation;
            }

            if (HasIncompleteChildTasks(
                    task) &&
                requestedStatus != TaskStatus.Todo &&
                requestedStatus != TaskStatus.Crashed &&
                requestedStatus != TaskStatus.Failed)
            {
                task.Status =
                    TaskStatus.Todo;

                TouchTask(
                    task);

                MarkDirty();

                message =
                    $"'{task.Title}' remains in To Do until all child tasks are Done";

                return false;
            }

            if (requestedStatus == TaskStatus.Done &&
                !string.Equals(
                    task.ReviewStatus,
                    "Passed",
                    StringComparison.OrdinalIgnoreCase))
            {
                message =
                    $"'{task.Title}' cannot move to Done without a successful review";

                return false;
            }

            task.Status =
                requestedStatus;

            TouchTask(
                task);

            MarkDirty();

            message =
                $"Moved '{task.Title}' to {GetStatusLabel(requestedStatus)}";

            return true;
        }


        private List<KanbanTask> GetDirectChildren(
            KanbanTask parent)
        {
            if (parent == null ||
                string.IsNullOrWhiteSpace(
                    parent.Id))
            {
                return new List<KanbanTask>();
            }

            return _board.Tasks
                .Where(
                    task =>
                        task != null &&
                        string.Equals(
                            task.ParentTaskId,
                            parent.Id,
                            StringComparison.Ordinal))
                .ToList();
        }


        private bool HasIncompleteChildTasks(
            KanbanTask task)
        {
            return HasIncompleteChildTasks(
                task,
                new HashSet<string>(
                    StringComparer.Ordinal));
        }


        private bool HasIncompleteChildTasks(
            KanbanTask task,
            HashSet<string> visited)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(
                    task.Id) ||
                !visited.Add(
                    task.Id))
            {
                return false;
            }

            foreach (KanbanTask child in GetDirectChildren(task))
            {
                if (child.Status != TaskStatus.Done ||
                    HasIncompleteChildTasks(
                        child,
                        visited))
                {
                    return true;
                }
            }

            return false;
        }


        private bool HasIncompleteCommittedChildTasks(
            KanbanTask task)
        {
            return HasIncompleteCommittedChildTasks(
                task,
                new HashSet<string>(
                    StringComparer.Ordinal));
        }


        private bool HasIncompleteCommittedChildTasks(
            KanbanTask task,
            HashSet<string> visited)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(task.Id) ||
                !visited.Add(task.Id))
            {
                return false;
            }

            foreach (KanbanTask child in GetDirectChildren(task).Where(
                         child => !child.IsDecompositionDraft))
            {
                if (child.Status != TaskStatus.Done ||
                    HasIncompleteCommittedChildTasks(child, visited))
                {
                    return true;
                }
            }

            return false;
        }


        private bool ShouldStartCoordinationOnlyRun(
            KanbanTask task)
        {
            // Planning is the sole graph-authoring phase. Broad or defective
            // Implementation cards must kick the project back to Planning;
            // they may never regain the legacy coordination capability.
            return _autoDecompose &&
                   task != null &&
                   IsPlanningCoordinator(task);
        }


        private static bool IsCohesiveReadOnlyVerificationLeaf(
            KanbanTask task)
        {
            if (task == null ||
                (task.ProducedArtifactPaths ?? new List<string>()).Any(
                    path => !string.IsNullOrWhiteSpace(path)) ||
                (task.MutatedResourceKeys ?? new List<string>()).Any(
                    key => !string.IsNullOrWhiteSpace(key)))
            {
                return false;
            }

            string[] consumedArtifacts =
                (task.ConsumedArtifactPaths ?? new List<string>())
                    .Where(path => !string.IsNullOrWhiteSpace(path))
                    .Select(path => path.Trim().Replace('\\', '/'))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToArray();

            if (consumedArtifacts.Length == 0)
                return false;

            string request =
                string.Join(
                    "\n",
                    task.Title ?? string.Empty,
                    task.Description ?? string.Empty,
                    task.AcceptanceCriteria ?? string.Empty);

            if (!Regex.IsMatch(
                    request,
                    @"\b(?:read[- ]only|verify|verification|validate|validation|inspect|inspection|confirm|check|review)\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return false;
            }

            string positiveScope =
                RemoveNegatedMutationClauses(
                    RemoveNegatedSceneMutationPhrases(request));

            return !Regex.IsMatch(
                positiveScope,
                @"\b(?:create|write|author|implement|modify|update|edit|fix|attach|configure|wire|assign|apply|remove|build)\b",
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);
        }


        private static string RemoveNegatedMutationClauses(
            string request)
        {
            if (string.IsNullOrWhiteSpace(request))
                return request;

            return Regex.Replace(
                request,
                @"\b(?:do\s+not|don't|must\s+not|never)\b[^.?!;\r\n]*(?:[.?!;]|$)",
                string.Empty,
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);
        }


        private bool TryApplyDecompositionCircuitBreaker(
            KanbanTask task)
        {
            if (task == null || task.SuppressAutoDecomposition)
                return false;

            int depth = GetTaskHierarchyDepth(task);
            KanbanTask root = GetTaskRoot(task);
            int rootDescendantCount =
                root == null
                    ? 0
                    : GetAllDescendants(root).Count;

            if (depth < MaximumAutoDecompositionDepth &&
                rootDescendantCount <
                MaximumAutoDecompositionDescendantsPerRoot)
            {
                return false;
            }

            task.SuppressAutoDecomposition = true;
            task.NextAgentMessage =
                "[DecompositionCircuitBreaker] AIRuntime stopped automatic delegation because this task graph reached " +
                $"depth {depth} or {rootDescendantCount} descendants. Execute this card's owned outcome directly. " +
                "Do not create child cards; report any genuinely indivisible blocker through the normal task result.";
            TouchTask(task);
            MarkDirty();

            return true;
        }


        private int GetTaskHierarchyDepth(
            KanbanTask task)
        {
            int depth = 0;
            KanbanTask current = task;
            HashSet<string> visited =
                new HashSet<string>(StringComparer.Ordinal);

            while (current != null &&
                   !string.IsNullOrWhiteSpace(current.Id) &&
                   visited.Add(current.Id) &&
                   !string.IsNullOrWhiteSpace(current.ParentTaskId))
            {
                current = FindTask(current.ParentTaskId);

                if (current != null)
                    depth++;
            }

            return depth;
        }


        private KanbanTask GetTaskRoot(
            KanbanTask task)
        {
            KanbanTask current = task;
            HashSet<string> visited =
                new HashSet<string>(StringComparer.Ordinal);

            while (current != null &&
                   !string.IsNullOrWhiteSpace(current.Id) &&
                   visited.Add(current.Id) &&
                   !string.IsNullOrWhiteSpace(current.ParentTaskId))
            {
                KanbanTask parent = FindTask(current.ParentTaskId);

                if (parent == null)
                    break;

                current = parent;
            }

            return current;
        }


        private string GetWorkflowPhase(
            KanbanTask task)
        {
            KanbanTask root = GetTaskRoot(task);

            return string.IsNullOrWhiteSpace(root?.WorkflowPhase)
                ? WorkflowPhaseImplementation
                : root.WorkflowPhase;
        }


        private bool IsPlanningCoordinator(
            KanbanTask task)
        {
            KanbanTask root = GetTaskRoot(task);

            return task != null &&
                   ReferenceEquals(task, root) &&
                   string.Equals(
                       GetWorkflowPhase(task),
                       WorkflowPhasePlanning,
                       StringComparison.Ordinal);
        }


        private bool IsEligibleForCurrentWorkflowPhase(
            KanbanTask task,
            bool reviewMode)
        {
            if (task == null)
                return false;

            string phase = GetWorkflowPhase(task);

            if (reviewMode)
            {
                KanbanTask workflowRoot = GetTaskRoot(task);

                if (string.Equals(
                        phase,
                        WorkflowPhaseImplementation,
                        StringComparison.Ordinal))
                {
                    return !ReferenceEquals(task, workflowRoot) &&
                           !HasUnmetFinalReviewDependency(task);
                }

                return string.Equals(
                           phase,
                           WorkflowPhaseFinalReview,
                           StringComparison.Ordinal) &&
                       ReferenceEquals(task, workflowRoot) &&
                       !HasUnmetFinalReviewDependency(task);
            }

            if (string.Equals(
                    phase,
                    WorkflowPhasePlanning,
                    StringComparison.Ordinal))
            {
                return IsPlanningCoordinator(task);
            }

            if (!string.Equals(
                    phase,
                    WorkflowPhaseImplementation,
                    StringComparison.Ordinal))
            {
                return false;
            }

            KanbanTask root = GetTaskRoot(task);
            return ReferenceEquals(task, root)
                ? GetDirectChildren(root).Count == 0
                : true;
        }


        private bool HasUnmetFinalReviewDependency(
            KanbanTask task)
        {
            return GetAuthoritativeDependencyTaskIds(task).Any(
                dependencyId =>
                {
                    KanbanTask dependency = FindTask(dependencyId);
                    return dependency == null ||
                           dependency.Status != TaskStatus.Done;
                });
        }


        private void AdvanceWorkflowPhases()
        {
            bool changed = false;

            foreach (KanbanTask root in _board.Tasks.Where(
                         candidate =>
                             candidate != null &&
                             string.IsNullOrWhiteSpace(candidate.ParentTaskId)))
            {
                string phase = GetWorkflowPhase(root);
                List<KanbanTask> descendants =
                    GetAllDescendants(root)
                        .Where(task => !task.IsDecompositionDraft)
                        .ToList();

                if (string.Equals(
                        phase,
                        WorkflowPhaseImplementation,
                        StringComparison.Ordinal) &&
                     ((descendants.Count == 0 &&
                       root.Status == TaskStatus.Review) ||
                      (descendants.Count > 0 &&
                       descendants.All(
                           task =>
                               task.Status == TaskStatus.Done &&
                               string.Equals(
                                   task.ReviewStatus,
                                   "Passed",
                                   StringComparison.Ordinal)))))
                {
                    root.WorkflowPhase = WorkflowPhaseFinalReview;
                    root.BlockedReason = string.Empty;
                    TouchTask(root);
                    changed = true;
                    Debug.Log(
                        $"[Kanban] Project '{root.Title}' entered Final Review after all implementation cards completed.");
                }
                else if (string.Equals(
                             phase,
                             WorkflowPhaseFinalReview,
                             StringComparison.Ordinal) &&
                         root.Status == TaskStatus.Todo &&
                         descendants.Count > 0 &&
                         descendants.All(
                             task =>
                                 task.Status == TaskStatus.Done &&
                                 string.Equals(
                                     task.ReviewStatus,
                                     "Passed",
                                     StringComparison.Ordinal)))
                {
                    root.Status = TaskStatus.Review;
                    root.ReviewStatus = "Pending";
                    root.ReviewResult = string.Empty;
                    root.ReviewEvidence = string.Empty;
                    TouchTask(root);
                    changed = true;
                }
                else if (string.Equals(
                             phase,
                             WorkflowPhaseFinalReview,
                             StringComparison.Ordinal) &&
                         root.Status == TaskStatus.Done)
                {
                    root.WorkflowPhase = WorkflowPhaseCompleted;
                    TouchTask(root);
                    changed = true;
                }
            }

            if (!changed)
                return;

            MarkDirty();
            if (_autoSave)
                SaveBoard(showDialog: false);
        }


        private bool TryPromoteCompletedCoordinationParentToReview(
            KanbanTask task)
        {
            if (task == null)
            {
                return false;
            }

            if (string.Equals(
                    task.ReviewStatus,
                    "Rework Requested",
                    StringComparison.Ordinal) ||
                !string.IsNullOrWhiteSpace(
                    task.NextAgentMessage))
            {
                return false;
            }

            List<KanbanTask> descendants =
                GetAllDescendants(task);

            if (descendants.Count == 0 ||
                descendants.Any(
                    descendant =>
                        descendant.IsDecompositionDraft ||
                        descendant.Status != TaskStatus.Done ||
                        !string.Equals(
                            descendant.ReviewStatus,
                            "Passed",
                            StringComparison.Ordinal)))
            {
                return false;
            }

            task.Status =
                TaskStatus.Review;

            task.ReviewStatus =
                "Pending";

            task.ReviewResult =
                string.Empty;

            task.ReviewEvidence =
                string.Empty;

            task.ReviewStartedAt =
                string.Empty;

            task.ReviewFinishedAt =
                string.Empty;

            TouchTask(task);
            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }

            _statusMessage =
                $"All descendants of '{task.Title}' passed; queued deterministic integration review";

            Repaint();

            return true;
        }


        private int CountIncompleteDescendants(
            KanbanTask task,
            HashSet<string> visited)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(task.Id) ||
                !visited.Add(task.Id))
            {
                return 0;
            }

            int count = 0;

            foreach (KanbanTask child in GetDirectChildren(task))
            {
                if (child.Status != TaskStatus.Done)
                {
                    count++;
                }

                count += CountIncompleteDescendants(
                    child,
                    visited);
            }

            return count;
        }


        private void ResetTask(
            KanbanTask task)
        {
            if (task == null)
            {
                return;
            }


            ActiveAgentRun running =
                _activeAgentRuns.Values.FirstOrDefault(
                    run =>
                        run != null &&
                        string.Equals(
                            run.TaskId,
                            task.Id,
                            StringComparison.Ordinal));

            if (running != null)
            {
                _discardedAgentRunTaskIds.Add(
                    task.Id);

                running.Cancellation.Cancel();
                foreach (var key in _activeAgentRuns.Where(pair => ReferenceEquals(pair.Value, running)).Select(pair => pair.Key).ToList())
                    _activeAgentRuns.Remove(key);
                running.Cancellation.Dispose();
            }

            ResetCardMemory(task);
            task.Dispatch = null;
            task.NextAttemptAtUtc = string.Empty;
            task.AutomaticRetryCount = 0;
            task.RetryReview = false;
            task.RetryReason = string.Empty;
            task.ReviewQueuedAtUtc = string.Empty;


            task.Status =
                TaskStatus.Todo;

            task.AgentResult =
                string.Empty;

            task.ExecutionMemory =
                string.Empty;

            task.AgentError =
                string.Empty;

            task.BlockedReason =
                string.Empty;

            task.AgentStartedAt =
                string.Empty;

            task.AgentFinishedAt =
                string.Empty;

            task.ReviewStatus =
                string.Empty;

            task.ReviewResult =
                string.Empty;

            task.ReviewEvidence =
                string.Empty;

            task.ReviewFailureKind =
                string.Empty;

            task.ReviewStartedAt =
                string.Empty;

            task.ReviewFinishedAt =
                string.Empty;

            task.ReviewFailureCount =
                0;

            task.PreflightFailureCount =
                0;

            task.ReviewEvidenceFingerprint =
                string.Empty;

            task.LastRejectedReviewEvidenceFingerprint =
                string.Empty;

            task.LastRejectedReviewOutcomeFingerprint =
                string.Empty;

            task.ReviewDependencyStateFingerprint =
                string.Empty;

            task.BlockedByTaskId =
                string.Empty;

            task.BlockedReason =
                string.Empty;

            task.DecompositionFailureCount =
                0;

            task.CapabilityRecoveryPolicyVersion =
                0;

            task.SuppressAutoDecomposition =
                false;

            task.NextAgentMessage =
                string.Empty;

            task.RequestTitle =
                string.Empty;

            task.RequestDescription =
                string.Empty;

            task.RequestAcceptanceCriteria =
                string.Empty;

            task.PlanningKickbackReason = string.Empty;
            task.PlanningKickbackFingerprint = string.Empty;

            KanbanTask workflowRoot = GetTaskRoot(task);
            if (ReferenceEquals(task, workflowRoot))
            {
                task.WorkflowPhase =
                    _autoDecompose
                        ? WorkflowPhasePlanning
                        : WorkflowPhaseImplementation;
            }
            else if (workflowRoot != null)
            {
                workflowRoot.WorkflowPhase = WorkflowPhaseImplementation;
                TouchTask(workflowRoot);
            }

            TouchTask(
                task);

            MarkDirty();

            _statusMessage =
                $"Reset '{task.Title}' to To Do";
        }


        private void ResumeProviderRetry(
            KanbanTask task)
        {
            if (task == null ||
                !string.Equals(
                    task.ReviewStatus,
                    "Retry Available",
                    StringComparison.Ordinal))
            {
                return;
            }

            task.Status = task.RetryReview ? TaskStatus.Review : TaskStatus.Todo;
            task.AutomaticRetryCount = 0;
            task.NextAttemptAtUtc = string.Empty;
            task.AgentError = string.Empty;
            task.ReviewStatus = task.RetryReview ? "Pending" : string.Empty;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = string.Empty;
            task.AgentStartedAt = string.Empty;
            task.AgentFinishedAt = string.Empty;

            if (string.IsNullOrWhiteSpace(
                    task.NextAgentMessage))
            {
                task.NextAgentMessage =
                    "[ProviderRetryAvailable] Resume the same durable attempt. Preserve accepted proposals, execution memory, and existing artifacts; inspect before recreating work.";
            }

            TouchTask(task);
            MarkDirty();

            if (_autoSave)
                SaveBoard(showDialog: false);

            _statusMessage =
                $"Resumed '{task.Title}' without discarding durable work";

            Repaint();
        }


        private bool ResumeFailedAttempt(
            KanbanTask task)
        {
            if (task == null ||
                IsTaskRunning(task) ||
                (task.Status != TaskStatus.Crashed &&
                 task.Status != TaskStatus.Failed))
            {
                return false;
            }

            string previousError =
                string.IsNullOrWhiteSpace(task.AgentError)
                    ? task.ReviewResult
                    : task.AgentError;

            bool resumeReview =
                !string.IsNullOrWhiteSpace(task.AgentResult) &&
                !string.IsNullOrWhiteSpace(task.ReviewStartedAt) &&
                !TryGetMissingProducedArtifact(task, out _);

            task.Status =
                resumeReview
                    ? TaskStatus.Review
                    : TaskStatus.Todo;
            task.AgentError = string.Empty;
            task.ReviewStatus =
                resumeReview
                    ? "Pending"
                    : string.Empty;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = string.Empty;
            task.AgentStartedAt = string.Empty;
            task.AgentFinishedAt = string.Empty;

            if (resumeReview)
            {
                task.ReviewResult = string.Empty;
                task.ReviewEvidence = string.Empty;
                task.ReviewFailureKind = string.Empty;
                task.ReviewStartedAt = string.Empty;
                task.ReviewFinishedAt = string.Empty;
            }

            task.NextAgentMessage =
                "[DurableAttemptResume] The previous " +
                (resumeReview ? "Review" : "implementation") +
                " attempt stopped before an accepted completion" +
                (string.IsNullOrWhiteSpace(previousError)
                    ? "."
                    : $": {previousError}") +
                " Preserve valid persisted work and execution memory. Inspect current state before mutating, do not recreate completed work, and continue with a changed strategy that addresses the prior failure.";

            TouchTask(task);
            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(showDialog: false);
            }

            _statusMessage =
                $"Resumed '{task.Title}' without discarding durable work";

            Repaint();
            return true;
        }


        private void RequeueReview(
            KanbanTask task)
        {
            if (task == null ||
                IsTaskRunning(task) ||
                (string.IsNullOrWhiteSpace(task.AgentResult) &&
                 TryGetMissingProducedArtifact(task, out _)))
            {
                return;
            }

            task.Status = TaskStatus.Review;
            task.AgentError = string.Empty;
            task.ReviewStatus = "Pending";
            task.ReviewResult = string.Empty;
            task.ReviewEvidence = string.Empty;
            task.ReviewFailureKind = string.Empty;
            task.ReviewStartedAt = string.Empty;
            task.ReviewFinishedAt = string.Empty;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = string.Empty;
            task.ReviewDependencyStateFingerprint = string.Empty;
            task.NextAgentMessage =
                "AIRuntime requested a fresh evidence-only review. Preserve the completed implementation and judge the original card against current authoritative evidence.";

            TouchTask(task);
            MarkDirty();

            if (_autoSave)
                SaveBoard(showDialog: false);

            _statusMessage =
                $"Queued a fresh review for '{task.Title}' without rerunning implementation";
        }


        private bool IsTaskRunning(
            KanbanTask task)
        {
            if (task == null)
                return false;


            return _activeAgentRuns.Values.Any(
                run =>
                    string.Equals(
                        run.TaskId,
                        task.Id,
                        StringComparison.Ordinal));
        }


        private void RefreshDeferredReviews()
        {
            bool changed = false;

            foreach (KanbanTask task in _board.Tasks)
            {
                if (task == null ||
                    task.Status != TaskStatus.Review ||
                    !(string.Equals(
                          task.ReviewStatus,
                          "Waiting for Evidence",
                          StringComparison.Ordinal) ||
                      string.Equals(
                          task.ReviewStatus,
                          "Blocked by Upstream",
                          StringComparison.Ordinal)))
                {
                    continue;
                }

                if (AIRuntimeKanbanAgentRunner.ContainsUnexecutedToolMarkup(task.AgentResult))
                {
                    task.Status = TaskStatus.Todo;
                    task.ReviewStatus = "Rework Requested";
                    task.ReviewFailureKind = "ImplementationDefect";
                    task.BlockedByTaskId = string.Empty;
                    task.BlockedReason = string.Empty;
                    task.ReviewDependencyStateFingerprint = string.Empty;
                    task.LastRejectedReviewEvidenceFingerprint = string.Empty;
                    task.LastRejectedReviewOutcomeFingerprint = string.Empty;
                    task.NextAgentMessage =
                        "[ReviewFailureKind: ImplementationDefect] AIRuntime detected an unexecuted textual tool request in the prior implementation result. Continue the implementation with the available capabilities, verify the actual workspace state, and finish every remaining acceptance criterion before returning to review.\n\n" +
                        (task.ReviewResult ?? string.Empty);
                    TouchTask(task);
                    changed = true;
                    continue;
                }

                string currentDependencyState =
                    ComputeReviewDependencyStateFingerprint(task);

                bool blockerReleased =
                    !string.IsNullOrWhiteSpace(task.BlockedByTaskId) &&
                    FindTask(task.BlockedByTaskId)?.Status == TaskStatus.Done;

                bool evidenceInputsChanged =
                    !string.IsNullOrWhiteSpace(
                        task.ReviewDependencyStateFingerprint) &&
                    !string.Equals(
                        currentDependencyState,
                        task.ReviewDependencyStateFingerprint,
                        StringComparison.Ordinal);

                if (!blockerReleased && !evidenceInputsChanged)
                    continue;

                task.ReviewStatus = "Pending";
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = string.Empty;
                task.ReviewDependencyStateFingerprint =
                    currentDependencyState;
                task.NextAgentMessage =
                    "AIRuntime detected changed upstream/evidence state. Perform a fresh evidence-only review and preserve the completed implementation.";
                TouchTask(task);
                changed = true;
            }

            if (!changed)
                return;

            MarkDirty();

            if (_autoSave)
                SaveBoard(showDialog: false);
        }


        private string ComputeReviewDependencyStateFingerprint(
            KanbanTask reviewedTask)
        {
            string state =
                string.Join(
                    "\n",
                    _board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                !ReferenceEquals(task, reviewedTask))
                        .OrderBy(task => task.Id, StringComparer.Ordinal)
                        .Select(
                            task =>
                                string.Join(
                                    "|",
                                    task.Id ?? string.Empty,
                                    (int)task.Status,
                                    task.ReviewStatus ?? string.Empty,
                                    task.UpdatedAt ?? string.Empty)));

            return ComputeStableFingerprint(state);
        }


        private void SetAutomaticAgentScheduling(bool enabled)
        {
            _schedulerPersistenceFailed = false;
            _autoRunAgents = enabled;
            _board.SchedulerEnabled = enabled;
            if (!PersistSchedulerState()) _autoRunAgents = false;
            AIRuntimeKanbanScheduler.Wake();
            // The interactive toggle always means continuous scheduling. A
            // previous command-line single step must not leave a zero budget.
            if (enabled) _agentStartsRemaining = -1;
            _lastSchedulerDiagnostic = null;
            PublishSchedulerDiagnostics();
        }

        private string GetSchedulerGate()
        {
            if (!string.IsNullOrEmpty(_cliTaskId)) return "CLI owns explicit dispatch";
            if (_boardWriteConflict) return "Board persistence conflict";
            if (_schedulerPersistenceFailed) return "Scheduler persistence failed; reload or explicitly resume";
            if (!_autoRunAgents) return "Run agents is disabled";
            if (!HasAgentStartBudget()) return "Single-step start budget exhausted";
            if (_isPreparingForAssemblyReload) return "Preparing for assembly reload";
            if (_isAgentQueuePumping) return "Queue pump already active";
            if (EditorApplication.isCompiling) return "Unity is compiling";
            if (EditorApplication.isUpdating) return "Unity is importing assets";
            if (EditorApplication.isPlayingOrWillChangePlaymode) return "Unity is in or entering Play Mode";
            if (EditorApplication.timeSinceStartup < _agentQueueNotBeforeEditorTime) return "Waiting for scheduled start delay";
            return "Ready";
        }

        private string GetTaskSchedulingBlocker(KanbanTask task)
        {
            if (task?.ReviewStatus == "Awaiting visual approval") return "Awaiting visual approval";
            bool review = task.Status == TaskStatus.Review;
            string agent = ResolveWorkflowAgentId(task, review);
            return HasLiveDispatchLease(task, DateTimeOffset.UtcNow) &&
                        !_activeAgentRuns.Values.Any(run => run.TaskId == task.Id) ? "Waiting for previous execution lease"
                    : task.ReviewStatus == "Retry Available" && TryUtc(task.NextAttemptAtUtc, out var retryAt)
                        ? "Retry scheduled: " + retryAt.ToLocalTime().ToString("HH:mm:ss")
                    : task.IsDecompositionDraft ? "Uncommitted draft"
                    : task.Status != TaskStatus.Todo && !review ? "Task status: " + task.Status
                    : !IsEligibleForCurrentWorkflowPhase(task, review) ? "Workflow phase: " + GetWorkflowPhase(task)
                    : review && !string.IsNullOrWhiteSpace(task.ReviewStatus) && task.ReviewStatus != "Pending" ? "Review: " + task.ReviewStatus
                    : !review && HasRunningAncestor(task) ? "Ancestor is running"
                    : !review && HasTerminalFailureAncestor(task) ? "Ancestor has a terminal failure"
                    : !review && HasUnmetDeclaredDependency(task) ? "Unfinished dependency"
                    : !review && !IsPlanningCoordinator(task) && HasIncompleteCommittedChildTasks(task) ? "Unfinished child tasks"
                    : task.ReviewStatus == "Retry Available" || task.ReviewStatus == "Decomposition Blocked" ? task.ReviewStatus
                    : string.IsNullOrWhiteSpace(task.AssignedAgent) || string.IsNullOrWhiteSpace(agent) ? "No assigned agent"
                    : !IsKanbanAgentActive(agent) ? "Agent is inactive"
                    : IsAgentProviderCoolingDown(agent, out _) ? "Provider cooldown"
                    : _activeAgentRuns.ContainsKey(agent) || _board.Tasks.Any(other => other != null &&
                        other.Status == TaskStatus.InProgress && other.AssignedAgent?.Trim() == agent) ? "Agent is busy"
                    : "Eligible";
        }

        private void PublishSchedulerDiagnostics(string forcedGate = null)
        {
            if (_lastSchedulerDiagnostic != null && EditorApplication.timeSinceStartup < _nextSchedulerDiagnosticAt)
                return;
            _nextSchedulerDiagnosticAt = EditorApplication.timeSinceStartup + 1;
            var tasks = new JArray();
            foreach (KanbanTask task in _board?.Tasks ?? new List<KanbanTask>())
            {
                if (task == null || task.Status == TaskStatus.Done) continue;
                bool review = task.Status == TaskStatus.Review;
                string agent = ResolveWorkflowAgentId(task, review);
                string blocker = GetTaskSchedulingBlocker(task);
                tasks.Add(new JObject {
                    ["TaskId"] = task.Id, ["Title"] = task.Title, ["Status"] = task.Status.ToString(),
                    ["ReviewStatus"] = task.ReviewStatus, ["WorkflowPhase"] = GetWorkflowPhase(task),
                    ["ResolvedAgent"] = agent, ["Blocker"] = blocker, ["BlockedReason"] = task.BlockedReason,
                    ["NextAttemptAtUtc"] = task.NextAttemptAtUtc, ["AutomaticRetryCount"] = task.AutomaticRetryCount,
                    ["AttemptId"] = task.Dispatch?.Id, ["LeaseUntilUtc"] = task.Dispatch?.LeaseUntilUtc,
                    ["ReviewQueuedAtUtc"] = task.ReviewQueuedAtUtc
                });
            }
            var snapshot = new JObject {
                ["ProjectPath"] = Directory.GetParent(Application.dataPath).FullName,
                ["BoardPath"] = _boardFilePath, ["WindowId"] = GetInstanceID(),
                ["Gate"] = forcedGate ?? GetSchedulerGate(), ["RunAgents"] = _autoRunAgents,
                ["StartsRemaining"] = _agentStartsRemaining, ["ActiveRunCount"] = _activeAgentRuns.Count,
                ["Tasks"] = tasks
            };
            string signature = snapshot.ToString(Newtonsoft.Json.Formatting.None);
            if (signature == _lastSchedulerDiagnostic) return;
            _lastSchedulerDiagnostic = signature;
            AIRuntime.Runtime.Providers.ProviderTransportDebug.WriteEvent("Harness", "SCHEDULER STATE", snapshot);
        }

        internal void PumpAgentQueue()
        {
            if (_boardWriteConflict || _schedulerPersistenceFailed || !TryAcquireSchedulerOwnership()) return;
            if (!_isPreparingForAssemblyReload &&
                EditorApplication.isCompiling)
            {
                PrepareForAssemblyReload();
            }

            if (TryObserveExternalBoardChange())
            {
                PublishSchedulerDiagnostics("External board change or persistence conflict");
                return;
            }

            RefreshDispatchLeases();
            if (_schedulerPersistenceFailed || _boardWriteConflict) return;
            DrainPendingExecutionMemoryUpdates();
            DrainPendingAgentCompletions();
            RecoverOwnershipCapabilityRequests();
            ReleaseDueProviderRetries();
            TrackReviewWaitTimes();
            TryRecoverFailedCompilationWithoutReload(EditorApplication.isCompiling,
                EditorApplication.isUpdating, EditorUtility.scriptCompilationFailed);
            KeepEditorLoopAliveForAgentRuns();
            RecoverInactiveTrackedAgentRuns();
            RecoverOrphanedAgentRuns();
            RefreshDeferredReviews();
            ReleaseExpiredProviderCooldowns();

            if (ResolvePendingScriptCompilationChecks())
            {
                PublishSchedulerDiagnostics("Awaiting script compilation verification");
                return;
            }

            AdvanceWorkflowPhases();

            PublishSchedulerDiagnostics();

            if (GetSchedulerGate() != "Ready")
            {
                return;
            }


            _isAgentQueuePumping =
                true;

            try
            {
                HashSet<string> busyAgents =
                    new HashSet<string>(
                        _board.Tasks
                            .Where(
                                task =>
                                    task != null &&
                                    task.Status ==
                                    TaskStatus.InProgress &&
                                    !string.IsNullOrWhiteSpace(
                                        task.AssignedAgent))
                            .Select(
                                task =>
                                    task.AssignedAgent.Trim()),
                        StringComparer.Ordinal);

                busyAgents.UnionWith(
                    _activeAgentRuns.Keys);

                foreach (var review in _board.Tasks.Where(task => IsAgedReview(task, DateTimeOffset.UtcNow))
                             .OrderBy(task => task.ReviewQueuedAtUtc).ToList())
                {
                    if (!HasAgentStartBudget()) break;
                    string reviewer = ResolveWorkflowAgentId(review, reviewMode: true);
                    if (GetTaskSchedulingBlocker(review) != "Eligible" || busyAgents.Contains(reviewer)) continue;
                    if (StartReviewTask(review, reviewer))
                    {
                        RecordAgentStart();
                        busyAgents.Add(reviewer);
                    }
                }

                StartCorrectiveDecompositionRetries();
                busyAgents.UnionWith(_activeAgentRuns.Keys);

                List<KanbanTask> candidates =
                    _board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                 task.Status ==
                                     TaskStatus.Todo &&
                                 !task.IsDecompositionDraft &&
                                 IsEligibleForCurrentWorkflowPhase(
                                     task,
                                     reviewMode: false) &&
                                 !HasRunningAncestor(
                                     task) &&
                                !HasTerminalFailureAncestor(
                                    task) &&
                                !HasUnmetDeclaredDependency(
                                    task) &&
                                 (IsPlanningCoordinator(task) ||
                                  !HasIncompleteCommittedChildTasks(
                                      task)) &&
                                 !string.Equals(
                                     task.ReviewStatus,
                                     "Decomposition Blocked",
                                     StringComparison.Ordinal) &&
                                 !string.Equals(
                                     task.ReviewStatus,
                                     "Retry Available",
                                     StringComparison.Ordinal) &&
                                !string.IsNullOrWhiteSpace(
                                    task.AssignedAgent))
                        .OrderByDescending(
                            task =>
                                task.Priority)
                        .ThenBy(
                            task =>
                                task.CreatedAt)
                        .ToList();


                foreach (
                    KanbanTask task
                    in candidates)
                {
                    if (!HasAgentStartBudget())
                    {
                        break;
                    }

                    if (TryPromoteCompletedCoordinationParentToReview(
                            task))
                    {
                        continue;
                    }

                    string agentId =
                        ResolveWorkflowAgentId(
                            task,
                            reviewMode: false);


                    if (!IsKanbanAgentActive(
                            agentId) ||
                        IsAgentProviderCoolingDown(
                            agentId,
                            out _) ||
                        busyAgents.Contains(
                            agentId))
                    {
                        continue;
                    }


                    if (StartAgentTask(task, agentId))
                    {
                        RecordAgentStart();
                        busyAgents.Add(agentId);
                    }
                }


                // Reviews younger than the promotion threshold run after each
                // agent. The implementation pass above marks only agents
                // with their own active/eligible work as busy, so unrelated
                // agents and their queues cannot block this review pass.
                List<KanbanTask> reviewCandidates =
                    _board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                 task.Status ==
                                     TaskStatus.Review &&
                                 IsEligibleForCurrentWorkflowPhase(
                                     task,
                                     reviewMode: true) &&
                                 !string.IsNullOrWhiteSpace(
                                    task.AssignedAgent) &&
                                (string.IsNullOrWhiteSpace(
                                     task.ReviewStatus) ||
                                 string.Equals(
                                     task.ReviewStatus,
                                     "Pending",
                                     StringComparison.Ordinal)))
                        .OrderBy(
                            task =>
                                task.CreatedAt)
                        .ToList();


                foreach (
                    KanbanTask task
                    in reviewCandidates)
                {
                    if (!HasAgentStartBudget())
                    {
                        break;
                    }

                    string agentId =
                        ResolveWorkflowAgentId(
                            task,
                            reviewMode: true);


                    if (!IsKanbanAgentActive(
                            agentId) ||
                        IsAgentProviderCoolingDown(
                            agentId,
                            out _) ||
                        busyAgents.Contains(
                            agentId))
                    {
                        continue;
                    }


                    if (StartReviewTask(task, agentId))
                    {
                        RecordAgentStart();
                        busyAgents.Add(agentId);
                    }
                }

                if (_activeAgentRuns.Count ==
                        0 &&
                    candidates.Count ==
                        0 &&
                    reviewCandidates.Count ==
                        0)
                {
                    string dependencyDeadlock =
                        FindDependencyDeadlock();

                    if (!string.IsNullOrWhiteSpace(
                            dependencyDeadlock))
                    {
                        _statusMessage =
                            dependencyDeadlock;

                        if (!string.Equals(
                                _lastDependencyDeadlock,
                                dependencyDeadlock,
                                StringComparison.Ordinal))
                        {
                            _lastDependencyDeadlock =
                                dependencyDeadlock;

                            Debug.LogError(
                                "[Kanban] " +
                                dependencyDeadlock);
                        }
                    }
                    else
                    {
                        _lastDependencyDeadlock =
                            string.Empty;
                    }
                }
            }
            finally
            {
                _isAgentQueuePumping =
                    false;
            }
        }


        private bool HasAgentStartBudget()
        {
            return _agentStartsRemaining != 0;
        }


        private void RecordAgentStart()
        {
            if (_agentStartsRemaining <= 0)
            {
                return;
            }

            _agentStartsRemaining--;

            if (_agentStartsRemaining == 0)
            {
                _autoRunAgents = false;
                _statusMessage =
                    "Bounded agent step is running; inspect the board before starting another step.";
            }
        }


        private void DrainPendingAgentCompletions()
        {
            DrainMemoryEvents();
            while (_pendingAgentCompletions.TryDequeue(
                       out PendingAgentCompletion completion))
            {
                if (!string.IsNullOrEmpty(completion.AttemptId))
                {
                    _nextExternalBoardCheckEditorTime = 0;
                    TryObserveExternalBoardChange();
                }
                if (!AcceptAttempt(completion.TaskId, completion.AttemptId))
                {
                    RetireRejectedAttempt(completion);
                    continue;
                }
                var completedTask = FindTask(completion.TaskId);
                if (completedTask?.Dispatch != null)
                {
                    completedTask.Dispatch.Outcome = string.IsNullOrWhiteSpace(completion.Error) ? "Completed" : "Failed";
                    completedTask.Dispatch.FinishedAtUtc = DateTimeOffset.UtcNow.ToString("O");
                    completedTask.Dispatch.LeaseUntilUtc = string.Empty;
                    MarkDirty();
                }
                Debug.Log(
                    $"[Kanban] Applying queued completion for task '{completion.TaskId}' (review={completion.ReviewMode}).");

                if (ControlPlayModeCapability.EndOwnedSession(
                        completion.TaskId))
                {
                    Debug.Log(
                        $"[Kanban] Exited the Play Mode session owned by completed task '{completion.TaskId}'.");
                }

                CompleteAgentTaskCore(
                    completion.TaskId,
                    completion.AgentId,
                    completion.Result,
                    completion.Error,
                    completion.ReviewEvidence,
                    completion.ReviewMode,
                    completion.ProviderFailureKind);

                FinishMemoryAttempt(completedTask, completion);

                // Run outcomes are durable workflow state, independent of the
                // Auto Save preference for manual edits. Include early recovery
                // and review transitions made by CompleteAgentTaskCore.
                if (_isDirty && !_boardWriteConflict)
                    SaveBoard(showDialog: false);
            }
        }


        private void DrainPendingExecutionMemoryUpdates()
        {
            DrainMemoryEvents();
            bool changed = false;

            while (_pendingExecutionMemoryUpdates.TryDequeue(
                       out PendingExecutionMemoryUpdate update))
            {
                if (!AcceptAttempt(update.TaskId, update.AttemptId) || _discardedAgentRunTaskIds.Contains(
                        update.TaskId))
                {
                    continue;
                }

                KanbanTask task = FindTask(update.TaskId);
                if (task != null && update.HumanVisualArtifacts != null)
                {
                    RecordHumanVisualEvidence(task, update.HumanVisualArtifacts);
                    changed = true;
                    continue;
                }
                if (task == null ||
                    string.Equals(
                        task.ExecutionMemory,
                        update.Memory,
                        StringComparison.Ordinal))
                {
                    continue;
                }

                if (task.Dispatch != null) task.Dispatch.LastCheckpointAtUtc = DateTimeOffset.UtcNow.ToString("O");
                task.ExecutionMemory =
                    update.Memory ??
                    string.Empty;

                ActiveAgentRun activeRun =
                    _activeAgentRuns.Values.FirstOrDefault(
                        run =>
                            run != null &&
                            string.Equals(
                                run.TaskId,
                                update.TaskId,
                                StringComparison.Ordinal));

                if (activeRun != null)
                {
                    activeRun.LastActivityAtUtc = DateTime.UtcNow;

                    if (ExecutionMemoryUpdateRepresentsProgress(
                            update.Memory))
                    {
                        activeRun.LastProgressAtUtc = DateTime.UtcNow;
                    }
                }

                TouchTask(task);
                changed = true;
            }

            if (!changed)
            {
                return;
            }

            MarkDirty();
            ScheduleExecutionMemorySave();

            Repaint();
        }


        private static bool ExecutionMemoryUpdateRepresentsProgress(
            string memory)
        {
            if (string.IsNullOrWhiteSpace(memory))
                return false;

            try
            {
                JToken latestEntry =
                    JObject.Parse(memory)["Entries"]?
                        .LastOrDefault();

                string eventName =
                    latestEntry?.Value<string>("Event") ??
                    string.Empty;

                return string.Equals(
                           eventName,
                           AgentEventType.ToolCompleted.ToString(),
                           StringComparison.Ordinal) ||
                       string.Equals(
                           eventName,
                           AgentEventType.RunCompleted.ToString(),
                           StringComparison.Ordinal);
            }
            catch (JsonException)
            {
                return false;
            }
        }


        private void KeepEditorLoopAliveForAgentRuns()
        {
            if (_activeAgentRuns.Count == 0 ||
                _isPreparingForAssemblyReload)
            {
                return;
            }

            // Provider, capability, and final Kanban continuations commonly
            // capture UnitySynchronizationContext. Unity may heavily throttle
            // editor-loop work when its window or this docked tab is not
            // focused. Keep scheduling player-loop work while an agent is
            // active so completed network/tool tasks can reach their final
            // continuation and release the card from In Progress.
            EditorApplication.QueuePlayerLoopUpdate();
        }


        private void StartCorrectiveDecompositionRetries()
        {
            if (!_autoRunAgents ||
                !HasAgentStartBudget() ||
                _board?.Tasks == null ||
                _isPreparingForAssemblyReload ||
                EditorApplication.isCompiling ||
                EditorApplication.isUpdating ||
                EditorApplication.isPlayingOrWillChangePlaymode)
            {
                return;
            }

            var busyAgents =
                new HashSet<string>(
                    _board.Tasks
                        .Where(
                            task =>
                                task != null &&
                                task.Status == TaskStatus.InProgress &&
                                !string.IsNullOrWhiteSpace(task.AssignedAgent))
                        .Select(task => task.AssignedAgent.Trim()),
                    StringComparer.Ordinal);

            busyAgents.UnionWith(_activeAgentRuns.Keys);

            foreach (KanbanTask task in _board.Tasks.Where(
                         IsCorrectiveDecompositionRetryCandidate))
            {
                if (!HasAgentStartBudget())
                {
                    break;
                }

                string agentId =
                    ResolveWorkflowAgentId(
                        task,
                        reviewMode: false);

                if (busyAgents.Contains(agentId) ||
                    !IsKanbanAgentActive(agentId) || IsAgentProviderCoolingDown(agentId, out _) ||
                    HasUnmetDeclaredDependency(task) || HasRunningAncestor(task) || HasTerminalFailureAncestor(task) ||
                    string.Equals(
                        task.ReviewStatus,
                        "Retry Available",
                        StringComparison.Ordinal))
                {
                    continue;
                }

                if (StartAgentTask(task, agentId))
                {
                    RecordAgentStart();
                    busyAgents.Add(agentId);
                }
            }
        }


        private bool IsCorrectiveDecompositionRetryCandidate(
            KanbanTask candidate)
        {
            if (candidate == null ||
                candidate.Status != TaskStatus.Todo ||
                candidate.DecompositionFailureCount <= 0 ||
                candidate.DecompositionFailureCount >=
                MaximumDecompositionFailureCount ||
                candidate.IsDecompositionDraft ||
                !HasDecompositionDrafts(candidate) ||
                string.IsNullOrWhiteSpace(candidate.AssignedAgent))
            {
                return false;
            }

            string message =
                candidate.NextAgentMessage ?? string.Empty;

            return message.StartsWith(
                       "[PlanningValidationFailure]",
                       StringComparison.Ordinal) ||
                   message.StartsWith(
                       "[DelegationValidationFailure]",
                       StringComparison.Ordinal) ||
                   message.StartsWith(
                       "[DecompositionValidationFailure]",
                       StringComparison.Ordinal) ||
                   message.StartsWith(
                       "[DecompositionRecovery]",
                       StringComparison.Ordinal) ||
                   message.StartsWith(
                       "[DecompositionPolicyMigration]",
                       StringComparison.Ordinal);
        }


        private int RecoverOrphanedAgentRuns()
        {
            if (_board?.Tasks == null ||
                _isPreparingForAssemblyReload)
            {
                return 0;
            }

            HashSet<string> liveTaskIds =
                new HashSet<string>(
                    _activeAgentRuns.Values
                        .Where(run => run != null)
                        .Select(run => run.TaskId),
                    StringComparer.Ordinal);

            List<KanbanTask> orphaned =
                _board.Tasks
                    .Where(
                        task =>
                            task != null &&
                            !liveTaskIds.Contains(task.Id) &&
                            (task.Status == TaskStatus.InProgress ||
                             (task.Status == TaskStatus.Review &&
                              string.Equals(
                                  task.ReviewStatus,
                                  "Running",
                                  StringComparison.OrdinalIgnoreCase))))
                    .ToList();

            int recovered =
                RecoverInterruptedTasks(
                    orphaned);

            if (recovered == 0)
            {
                return 0;
            }

            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }

            _statusMessage =
                $"Recovered {recovered} task(s) whose agent runner was lost; returned them to their queues";

            Repaint();

            return recovered;
        }


        private int RecoverInactiveTrackedAgentRuns()
        {
            if (_board?.Tasks == null ||
                _isPreparingForAssemblyReload)
            {
                return 0;
            }

            DateTime now = DateTime.UtcNow;
            List<KeyValuePair<string, ActiveAgentRun>> inactiveRuns =
                _activeAgentRuns
                    .Where(
                        pair =>
                        {
                            ActiveAgentRun run = pair.Value;
                            KanbanTask task =
                                run == null
                                    ? null
                                    : FindTask(run.TaskId);

                            return run != null &&
                                   task != null &&
                                   (task.Status == TaskStatus.Todo ||
                                    task.Status == TaskStatus.InProgress ||
                                    (task.Status == TaskStatus.Review &&
                                     string.Equals(
                                         task.ReviewStatus,
                                         "Running",
                                         StringComparison.OrdinalIgnoreCase))) &&
                                   now - run.LastActivityAtUtc >=
                                       MaximumTrackedAgentRunInactivity;
                        })
                    .ToList();

            foreach (KeyValuePair<string, ActiveAgentRun> pair in inactiveRuns)
            {
                ActiveAgentRun run = pair.Value;
                KanbanTask task = FindTask(run.TaskId);

                _activeAgentRuns.Remove(pair.Key);
                _discardedAgentRunTaskIds.Add(run.TaskId);

                try
                {
                    run.Cancellation?.Cancel();
                }
                catch (ObjectDisposedException)
                {
                }

                run.Cancellation?.Dispose();

                if (task == null)
                    continue;

                if (task.Dispatch != null)
                {
                    task.Dispatch.Outcome = "Interrupted";
                    task.Dispatch.LeaseUntilUtc = string.Empty;
                }
                task.AgentFinishedAt = now.ToString("O");

                if (run.CoordinationOnly)
                {
                    task.NextAgentMessage =
                        "[PlanningRunInactivityRecovery] AIRuntime released a tracked Planning run after fifteen minutes without provider, capability, or journal activity. " +
                        "Preserve its proposed graph and continue from durable Planning state; do not recreate completed graph work.";
                }
                else if (task.Status == TaskStatus.InProgress &&
                         (task.ProducedArtifactPaths ?? new List<string>())
                             .Any(path => !string.IsNullOrWhiteSpace(path)) &&
                         !TryGetMissingProducedArtifact(task, out _))
                {
                    task.Status = TaskStatus.Review;
                    task.AgentResult =
                        "[ImplementationInactivityRecovery] AIRuntime stopped an inactive implementation run after fifteen minutes without provider, capability, or journal activity. " +
                        "Every declared ProducedArtifactPath exists, so persisted work was preserved for fresh authoritative Review.";
                    task.AgentError = string.Empty;
                    task.ReviewStatus = "Pending";
                    task.ReviewResult = string.Empty;
                    task.NextAgentMessage = string.Empty;
                }
                else if (task.Status == TaskStatus.InProgress)
                {
                    task.Status = TaskStatus.Crashed;
                    task.AgentError =
                        "[ImplementationInactivityRecovery] AIRuntime stopped this run after fifteen minutes without provider, capability, or journal activity.";
                    task.ReviewStatus = "Crashed";
                    task.NextAgentMessage =
                        "Resume with a changed strategy or changed external state. Existing persisted work was preserved; do not recreate completed artifacts.";
                }
                else if (task.Status == TaskStatus.Review)
                {
                    task.ReviewStatus = "Crashed";
                    task.ReviewResult =
                        "[ReviewInactivityRecovery] AIRuntime stopped this Review run after fifteen minutes without provider, capability, or journal activity. Implementation was preserved.";
                    task.NextAgentMessage =
                        "Resume Review with fresh authoritative evidence; do not repeat implementation mutations.";
                }

                TouchTask(task);
            }

            if (inactiveRuns.Count == 0)
                return 0;

            MarkDirty();

            if (_autoSave)
                SaveBoard(showDialog: false);

            _statusMessage =
                $"Recovered {inactiveRuns.Count} inactive agent run(s); released their agent slots";
            Repaint();

            return inactiveRuns.Count;
        }


        private bool HasRunningAncestor(
            KanbanTask task)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(task.ParentTaskId))
            {
                return false;
            }

            HashSet<string> visited =
                new HashSet<string>(
                    StringComparer.Ordinal);

            string parentId =
                task.ParentTaskId;

            while (!string.IsNullOrWhiteSpace(parentId) &&
                   visited.Add(parentId))
            {
                KanbanTask parent =
                    FindTask(parentId);

                if (parent == null)
                    return false;

                if (IsTaskRunning(parent))
                    return true;

                parentId =
                    parent.ParentTaskId;
            }

            return false;
        }


        private bool HasTerminalFailureAncestor(
            KanbanTask task)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(task.ParentTaskId))
            {
                return false;
            }

            var visited =
                new HashSet<string>(
                    StringComparer.Ordinal);

            string parentId =
                task.ParentTaskId;

            while (!string.IsNullOrWhiteSpace(parentId) &&
                   visited.Add(parentId))
            {
                KanbanTask parent =
                    FindTask(parentId);

                if (parent == null)
                    return false;

                if (parent.Status == TaskStatus.Failed ||
                    parent.Status == TaskStatus.Crashed)
                {
                    return true;
                }

                parentId =
                    parent.ParentTaskId;
            }

            return false;
        }


        private bool HasUnmetDeclaredDependency(
            KanbanTask task)
        {
            if (task == null)
            {
                return false;
            }

            IReadOnlyList<string> dependencyIds =
                GetAuthoritativeDependencyTaskIds(task);

            if (dependencyIds.Count > 0)
            {
                if (dependencyIds.Any(
                    dependencyId =>
                    {
                        KanbanTask dependency = FindTask(dependencyId);

                        return dependency == null ||
                               !string.Equals(
                                   dependency.ParentTaskId ?? string.Empty,
                                task.ParentTaskId ?? string.Empty,
                                StringComparison.Ordinal) ||
                               dependency.Status != TaskStatus.Done;
                    }))
                {
                    return true;
                }
            }

            // A declaration that names no unique sibling is invalid and must
            // remain blocked rather than silently running out of order.
            return GetUnresolvedDependencyTitles(task).Count > 0;
        }


        private string FindDependencyDeadlock()
        {
            List<KanbanTask> todoTasks =
                _board.Tasks
                    .Where(
                        task =>
                            task != null &&
                            task.Status ==
                            TaskStatus.Todo)
                    .ToList();

            foreach (KanbanTask task in todoTasks)
            {
                if (task.DependsOnTaskIds != null &&
                    task.DependsOnTaskIds.Count > 0)
                {
                    foreach (string dependencyId in task.DependsOnTaskIds)
                    {
                        KanbanTask dependency = FindTask(dependencyId);

                        if (dependency == null ||
                            !string.Equals(
                                dependency.ParentTaskId ?? string.Empty,
                                task.ParentTaskId ?? string.Empty,
                                StringComparison.Ordinal))
                        {
                            return $"Dependency deadlock: '{task.Title}' references missing sibling task id '{dependencyId}'.";
                        }
                    }

                    continue;
                }

                foreach (string dependencyTitle in
                         task.DeclaredDependencyTitles ??
                         new List<string>())
                {
                    KanbanTask dependency =
                        _board.Tasks.FirstOrDefault(
                            candidate =>
                                candidate != null &&
                                !string.Equals(
                                    candidate.Id,
                                    task.Id,
                                    StringComparison.Ordinal) &&
                                string.Equals(
                                    candidate.ParentTaskId ?? string.Empty,
                                    task.ParentTaskId ?? string.Empty,
                                    StringComparison.Ordinal) &&
                                string.Equals(
                                    candidate.Title?.Trim(),
                                    dependencyTitle,
                                    StringComparison.OrdinalIgnoreCase));

                    if (dependency ==
                        null)
                    {
                        return
                            $"Dependency deadlock: '{task.Title}' references missing sibling '{dependencyTitle}'.";
                    }
                }
            }

            HashSet<string> completed =
                new HashSet<string>(
                    StringComparer.Ordinal);

            foreach (KanbanTask task in todoTasks)
            {
                List<string> path =
                    new List<string>();

                HashSet<string> visiting =
                    new HashSet<string>(
                        StringComparer.Ordinal);

                if (TryFindDependencyCycle(
                        task,
                        visiting,
                        completed,
                        path,
                        out string cycle))
                {
                    return
                        "Dependency deadlock: " +
                        cycle;
                }
            }

            return null;
        }


        private bool TryFindDependencyCycle(
            KanbanTask task,
            HashSet<string> visiting,
            HashSet<string> completed,
            List<string> path,
            out string cycle)
        {
            cycle =
                null;

            if (task ==
                    null ||
                task.Status !=
                    TaskStatus.Todo ||
                completed.Contains(
                    task.Id))
            {
                return false;
            }

            if (!visiting.Add(
                    task.Id))
            {
                int cycleStart =
                    path.FindIndex(
                        title =>
                            string.Equals(
                                title,
                                task.Title,
                                StringComparison.OrdinalIgnoreCase));

                IEnumerable<string> cycleTitles =
                    cycleStart >=
                    0
                        ? path.Skip(
                            cycleStart)
                        : path;

                cycle =
                    string.Join(
                        " -> ",
                        cycleTitles.Concat(
                            new[]
                            {
                                task.Title
                            }));

                return true;
            }

            path.Add(
                task.Title);

            IEnumerable<KanbanTask> dependencies =
                (task.DependsOnTaskIds ?? new List<string>())
                    .Select(FindTask);

            foreach (KanbanTask dependency in dependencies)
            {
                if (dependency?.Status ==
                        TaskStatus.Todo &&
                    TryFindDependencyCycle(
                        dependency,
                        visiting,
                        completed,
                        path,
                        out cycle))
                {
                    return true;
                }
            }

            path.RemoveAt(
                path.Count -
                1);

            visiting.Remove(
                task.Id);

            completed.Add(
                task.Id);

            return false;
        }


        private static IReadOnlyList<string> ExtractDeclaredDependencyTitles(
            string description)
        {
            return ExtractDependencyTitlesFromProse(description, true);
        }


        private static IReadOnlyList<string> ExtractDependencyTitlesFromProse(
            string description, bool normalizeQuoteEscapes)
        {
            if (string.IsNullOrWhiteSpace(description))
                return Array.Empty<string>();

            var titles =
                new List<string>();

            System.Text.RegularExpressions.MatchCollection clauses =
                System.Text.RegularExpressions.Regex.Matches(
                    description,
                    @"Depends\s+on\s+(?:the\s+)?(?:sibling\s+)?tasks?\s+(?<titles>.*?)\s+being\s+Done\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant |
                    System.Text.RegularExpressions.RegexOptions.Singleline);

            foreach (System.Text.RegularExpressions.Match clause in clauses)
            {
                // Providers sometimes preserve one or more JSON escape layers in
                // prose. Decode only quote delimiters, never arbitrary title text.
                string titleClause = clause.Groups["titles"].Value;
                if (normalizeQuoteEscapes)
                {
                    titleClause = System.Text.RegularExpressions.Regex.Replace(
                        titleClause, @"\\+(?=[""'])", string.Empty);
                }

                System.Text.RegularExpressions.MatchCollection quotedTitles =
                    System.Text.RegularExpressions.Regex.Matches(
                        titleClause,
                        @"[\""'](?<title>[^\""']+)[\""']",
                        System.Text.RegularExpressions.RegexOptions.CultureInvariant);

                foreach (System.Text.RegularExpressions.Match quotedTitle in quotedTitles)
                {
                    string title =
                        quotedTitle.Groups["title"].Value.Trim();

                    if (!string.IsNullOrWhiteSpace(title) &&
                        !titles.Contains(
                            title,
                            StringComparer.OrdinalIgnoreCase))
                    {
                        titles.Add(title);
                    }
                }
            }

            return titles;
        }


        private static int MigrateLegacyDependencyDeclarations(
            IEnumerable<KanbanTask> tasks)
        {
            int changes = 0;
            List<KanbanTask> allTasks = (tasks ?? Array.Empty<KanbanTask>())
                .Where(task => task != null).ToList();

            foreach (KanbanTask task in allTasks)
            {
                List<string> migrated = ExtractDeclaredDependencyTitles(task.Description).ToList();
                List<string> declared = task.DeclaredDependencyTitles ?? new List<string>();
                if (!declared.Any(title => !string.IsNullOrWhiteSpace(title)))
                {
                    if (migrated.Count > 0)
                    {
                        task.DeclaredDependencyTitles = migrated;
                        changes++;
                    }
                    continue;
                }

                // Repair only values demonstrably produced by the old parser.
                // Exact existing references win; ambiguous or missing siblings
                // remain blocked for explicit plan correction.
                IReadOnlyList<string> legacy = ExtractDependencyTitlesFromProse(task.Description, false);
                if (legacy.Count != migrated.Count)
                    continue;
                List<KanbanTask> siblings = allTasks.Where(candidate =>
                    candidate != task && string.Equals(candidate.ParentTaskId ?? string.Empty,
                        task.ParentTaskId ?? string.Empty, StringComparison.Ordinal)).ToList();
                bool repaired = false;
                for (int i = 0; i < declared.Count; i++)
                {
                    if (siblings.Any(sibling => string.Equals(sibling.Title?.Trim(),
                        declared[i], StringComparison.OrdinalIgnoreCase)))
                        continue;
                    for (int j = 0; j < legacy.Count; j++)
                    {
                        if (!string.Equals(declared[i], legacy[j], StringComparison.OrdinalIgnoreCase) ||
                            string.Equals(legacy[j], migrated[j], StringComparison.OrdinalIgnoreCase) ||
                            siblings.Count(sibling => string.Equals(sibling.Title?.Trim(),
                                migrated[j], StringComparison.OrdinalIgnoreCase)) != 1)
                            continue;
                        declared[i] = migrated[j];
                        repaired = true;
                        break;
                    }
                }
                if (repaired)
                {
                    task.DeclaredDependencyTitles = declared.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
                    changes++;
                }
            }

            return changes;
        }


        private IReadOnlyList<string> GetAuthoritativeDependencyTaskIds(
            KanbanTask task)
        {
            if (task == null)
                return Array.Empty<string>();

            var resolved =
                (task.DependsOnTaskIds ?? new List<string>())
                    .Where(id => !string.IsNullOrWhiteSpace(id))
                    .Distinct(StringComparer.Ordinal)
                    .ToList();

            return resolved
                .Distinct(StringComparer.Ordinal)
                .ToArray();
        }


        private IReadOnlyList<string> GetUnresolvedDependencyTitles(
            KanbanTask task)
        {
            if (task == null)
                return Array.Empty<string>();

            List<KanbanTask> siblings =
                _board.Tasks
                    .Where(
                        candidate =>
                            candidate != null &&
                            string.Equals(
                                candidate.ParentTaskId ?? string.Empty,
                                task.ParentTaskId ?? string.Empty,
                                StringComparison.Ordinal))
                    .ToList();

            return (task.DeclaredDependencyTitles ?? new List<string>())
                .Where(
                    title =>
                        siblings.Count(
                            sibling =>
                                string.Equals(
                                    sibling.Title?.Trim(),
                                    title,
                                    StringComparison.OrdinalIgnoreCase)) != 1)
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToArray();
        }


        private void RefreshDependencyTaskIds(
            KanbanTask parent)
        {
            ReconcileAllTaskRelationships();
        }


        private int ReconcileAllTaskRelationships()
        {
            if (_board?.Tasks == null ||
                _board.Tasks.Count == 0)
            {
                return 0;
            }

            Dictionary<string, string> previousDependencySignatures =
                _board.Tasks
                    .Where(
                        task =>
                            task != null &&
                            !string.IsNullOrWhiteSpace(task.Id))
                    .ToDictionary(
                        task => task.Id,
                        task => string.Join(
                            "\n",
                            (task.DependsOnTaskIds ?? new List<string>())
                                .Where(id => !string.IsNullOrWhiteSpace(id))
                                .Distinct(StringComparer.Ordinal)
                                .OrderBy(id => id, StringComparer.Ordinal)),
                        StringComparer.Ordinal);

            // Rebuild every sibling scope first. A new card can resolve a
            // forward title/artifact declaration on an older card, and a
            // rename, reparent, or deletion can invalidate either the old or
            // new scope. Relationship correctness must therefore be a board
            // mutation invariant, not a property of only the spawning parent.
            foreach (string relationshipParentId in
                     _board.Tasks
                         .Where(task => task != null)
                         .Select(
                             task =>
                                 task.ParentTaskId ?? string.Empty)
                         .Distinct(StringComparer.Ordinal)
                         .ToArray())
            {
                RefreshSiblingDependencyTaskIds(
                    relationshipParentId);
            }

            List<KanbanTask> tasks =
                _board.Tasks
                    .Where(
                        task =>
                            task != null &&
                            !string.IsNullOrWhiteSpace(task.Id))
                    .ToList();

            Dictionary<string, KanbanTask> byId =
                tasks.ToDictionary(
                    task => task.Id,
                    StringComparer.Ordinal);

            AIRuntimeKanbanAssociationReconciler reconciler =
                new AIRuntimeKanbanAssociationReconciler();

            IReadOnlyList<AIRuntimeKanbanAssociationTaskResult> results =
                reconciler.Reconcile(
                    tasks.Select(
                        task =>
                            new AIRuntimeKanbanAssociationTaskSnapshot
                            {
                                Id = task.Id,
                                ParentTaskId =
                                    task.ParentTaskId ?? string.Empty,
                                RootTaskId =
                                    GetRootTaskId(task, byId),
                                Title = task.Title ?? string.Empty,
                                Description =
                                    task.Description ?? string.Empty,
                                CreatedAt = task.CreatedAt ?? string.Empty,
                                ProducedArtifactPaths =
                                    task.ProducedArtifactPaths ??
                                    new List<string>(),
                                ConsumedArtifactPaths =
                                    task.ConsumedArtifactPaths ??
                                    new List<string>(),
                                MutatedResourceKeys =
                                    task.MutatedResourceKeys ??
                                    new List<string>(),
                                // Sibling dependencies have just been rebuilt
                                // from their authoritative declarations. The
                                // reconciler may reason over them, while the
                                // scheduler keeps blocking edges sibling-scoped
                                // and stores global ownership as associations.
                                DependencyTaskIds =
                                    task.DependsOnTaskIds ??
                                    new List<string>(),
                                IsFinalVerifier =
                                    IsFinalIntegrationVerificationTask(task)
                            }));

            int changes =
                tasks.Count(
                    task =>
                        !previousDependencySignatures.TryGetValue(
                            task.Id,
                            out string previousSignature) ||
                        !string.Equals(
                            previousSignature,
                            string.Join(
                                "\n",
                                (task.DependsOnTaskIds ?? new List<string>())
                                    .Where(id => !string.IsNullOrWhiteSpace(id))
                                    .Distinct(StringComparer.Ordinal)
                                    .OrderBy(id => id, StringComparer.Ordinal)),
                            StringComparison.Ordinal));

            foreach (AIRuntimeKanbanAssociationTaskResult result in results)
            {
                if (!byId.TryGetValue(result.Id, out KanbanTask task))
                    continue;

                List<string> associatedIds =
                    (result.AssociatedTaskIds ?? Array.Empty<string>())
                        .Where(
                            associatedId =>
                                !string.IsNullOrWhiteSpace(associatedId) &&
                                !string.Equals(
                                    associatedId,
                                    task.Id,
                                    StringComparison.Ordinal) &&
                                byId.ContainsKey(associatedId))
                        .Distinct(StringComparer.Ordinal)
                        .OrderBy(id => id, StringComparer.Ordinal)
                        .ToList();

                List<string> previous =
                    (task.AssociatedTaskIds ?? new List<string>())
                        .Where(id => !string.IsNullOrWhiteSpace(id))
                        .Distinct(StringComparer.Ordinal)
                        .OrderBy(id => id, StringComparer.Ordinal)
                        .ToList();

                if (previous.SequenceEqual(associatedIds))
                    continue;

                task.AssociatedTaskIds = associatedIds;
                changes++;
            }

            return changes;
        }


        private static string GetRootTaskId(
            KanbanTask task,
            IReadOnlyDictionary<string, KanbanTask> byId)
        {
            if (task == null)
                return string.Empty;

            KanbanTask current = task;
            HashSet<string> visited =
                new HashSet<string>(StringComparer.Ordinal);

            while (current != null &&
                   !string.IsNullOrWhiteSpace(current.Id) &&
                   visited.Add(current.Id) &&
                   !string.IsNullOrWhiteSpace(current.ParentTaskId) &&
                   byId.TryGetValue(current.ParentTaskId, out KanbanTask parent))
            {
                current = parent;
            }

            return current?.Id ?? task.Id ?? string.Empty;
        }


        private static DateTimeOffset GetTaskCreatedAtSortValue(
            KanbanTask task)
        {
            return DateTimeOffset.TryParse(
                    task?.CreatedAt,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AssumeUniversal |
                    DateTimeStyles.AdjustToUniversal,
                    out DateTimeOffset createdAt)
                ? createdAt
                : DateTimeOffset.MaxValue;
        }


        private void RefreshSiblingDependencyTaskIds(
            string parentTaskId)
        {
            List<KanbanTask> children =
                _board.Tasks
                    .Where(
                        task =>
                            task != null &&
                            string.Equals(
                                task.ParentTaskId ?? string.Empty,
                                parentTaskId ?? string.Empty,
                                StringComparison.Ordinal))
                    .OrderBy(GetTaskCreatedAtSortValue)
                    .ThenBy(
                        task => task.Id ?? string.Empty,
                        StringComparer.Ordinal)
                    .ToList();

            if (RepairIncompleteDraftOwnership(children) > 0)
            {
                MarkDirty();
            }

            ResolveSiblingDependencies(children, parentTaskId);
        }


        private static void ResolveSiblingDependencies(
            List<KanbanTask> children, string parentTaskId)
        {
            foreach (KanbanTask child in children)
            {
                IReadOnlyList<string> titles =
                    child.DeclaredDependencyTitles ??
                    new List<string>();

                var resolvedIds = new List<string>();
                bool fullyResolved = true;

                foreach (string title in titles)
                {
                    KanbanTask[] matches =
                        children
                            .Where(
                                candidate =>
                                    string.Equals(
                                        candidate.Title?.Trim(),
                                        title,
                                        StringComparison.OrdinalIgnoreCase))
                            .ToArray();

                    if (matches.Length != 1)
                    {
                        fullyResolved = false;
                        break;
                    }

                    resolvedIds.Add(matches[0].Id);
                }

                if (fullyResolved)
                {
                    // Structured artifact ownership is authoritative. A
                    // consumer cannot safely run before the sibling that
                    // produces its exact path, even when the model omitted
                    // that producer from DependsOnSiblingTitles. Recompute
                    // this closure after every insertion so forward-created
                    // producers are backfilled as soon as they exist.
                    foreach (string consumedPath in
                             child.ConsumedArtifactPaths ??
                             new List<string>())
                    {
                        KanbanTask canonicalOwner =
                            children.FirstOrDefault(
                                candidate =>
                                    candidate != null &&
                                    (candidate.ProducedArtifactPaths ??
                                     new List<string>())
                                        .Any(
                                            producedPath =>
                                                string.Equals(
                                                    producedPath,
                                                    consumedPath,
                                                    StringComparison.OrdinalIgnoreCase)));

                        // An exact persistent path has one canonical owner:
                        // the first sibling that declares it. Later cards may
                        // re-save the same mutable artifact, but treating each
                        // of them as a producer would create reverse edges in
                        // an otherwise valid resource chain.
                        if (canonicalOwner != null &&
                            !ReferenceEquals(canonicalOwner, child) &&
                            !string.IsNullOrWhiteSpace(canonicalOwner.Id))
                        {
                            resolvedIds.Add(canonicalOwner.Id);
                        }
                    }

                    foreach (string resourceKey in
                             (child.MutatedResourceKeys ??
                              new List<string>())
                                 .Where(
                                     key =>
                                         !string.IsNullOrWhiteSpace(key)))
                    {
                        List<KanbanTask> resourceWriters =
                            children
                                .Where(
                                    candidate =>
                                        candidate != null &&
                                        (candidate.MutatedResourceKeys ??
                                         new List<string>())
                                            .Contains(
                                                resourceKey,
                                                StringComparer.OrdinalIgnoreCase))
                                .ToList();

                        string ownedArtifactPath =
                            resourceKey.IndexOf(':') >= 0
                                ? resourceKey
                                    .Substring(resourceKey.IndexOf(':') + 1)
                                    .Trim()
                                : string.Empty;

                        KanbanTask canonicalResourceOwner =
                            string.IsNullOrWhiteSpace(ownedArtifactPath)
                                ? null
                                : resourceWriters.FirstOrDefault(
                                    candidate =>
                                        (candidate.ProducedArtifactPaths ??
                                         new List<string>())
                                            .Contains(
                                                ownedArtifactPath,
                                                StringComparer.OrdinalIgnoreCase));

                        List<KanbanTask> orderedWriters =
                            canonicalResourceOwner == null
                                ? resourceWriters
                                : new[] { canonicalResourceOwner }
                                    .Concat(
                                        resourceWriters.Where(
                                            writer =>
                                                !ReferenceEquals(
                                                    writer,
                                                    canonicalResourceOwner)))
                                    .ToList();

                        int writerIndex =
                            orderedWriters.IndexOf(child);

                        if (writerIndex > 0)
                        {
                            KanbanTask previousWriter =
                                orderedWriters[writerIndex - 1];

                            if (!string.IsNullOrWhiteSpace(previousWriter.Id))
                                resolvedIds.Add(previousWriter.Id);
                        }
                    }
                }

                child.DependsOnTaskIds =
                    fullyResolved
                        ? resolvedIds
                            .Distinct(StringComparer.Ordinal)
                            .ToList()
                        : new List<string>();
            }

            // A designated final integration verifier is a graph sink by
            // definition. Corrective runs may append narrower siblings after
            // it, so insertion order cannot be allowed to put later scene
            // mutations behind the verifier. Remove reverse edges to the
            // verifier and make it join every sibling outcome explicitly.
            // Root cards are independent work graphs rather than siblings of
            // one implicit board-wide parent. A root-level verifier must not
            // become a prerequisite join for every unrelated project on the
            // board. Nested verifier cards retain the normal sibling-sink
            // behavior within their explicit parent scope.
            KanbanTask finalVerifier =
                string.IsNullOrWhiteSpace(parentTaskId)
                    ? null
                    : children.FirstOrDefault(
                        IsFinalIntegrationVerificationTask);

            if (finalVerifier != null &&
                !string.IsNullOrWhiteSpace(finalVerifier.Id))
            {
                foreach (KanbanTask child in children.Where(
                             candidate =>
                                 candidate != null &&
                                 !ReferenceEquals(candidate, finalVerifier)))
                {
                    child.DependsOnTaskIds =
                        (child.DependsOnTaskIds ?? new List<string>())
                            .Where(
                                dependencyId =>
                                    !string.Equals(
                                        dependencyId,
                                        finalVerifier.Id,
                                        StringComparison.Ordinal))
                            .Distinct(StringComparer.Ordinal)
                            .ToList();
                }

                finalVerifier.DependsOnTaskIds =
                    children
                        .Where(
                            child =>
                                child != null &&
                                !ReferenceEquals(child, finalVerifier) &&
                                !string.IsNullOrWhiteSpace(child.Id))
                        .Select(child => child.Id)
                        .Distinct(StringComparer.Ordinal)
                        .ToList();
            }
        }


        private static int RepairIncompleteDraftOwnership(
            IReadOnlyList<KanbanTask> children)
        {
            if (children == null || children.Count == 0)
                return 0;

            var ownedArtifacts =
                children
                    .Where(child => child != null)
                    .SelectMany(
                        child =>
                            (child.ProducedArtifactPaths ?? new List<string>())
                                .Where(path => !string.IsNullOrWhiteSpace(path))
                                .Select(path => new { Owner = child, Path = path }))
                    .ToList();

            if (ownedArtifacts.Count == 0)
                return 0;

            int repaired = 0;

            foreach (KanbanTask child in children.Where(
                         candidate =>
                             candidate != null &&
                             candidate.IsDecompositionDraft &&
                             (candidate.ProducedArtifactPaths?.Count ?? 0) == 0 &&
                             (candidate.ConsumedArtifactPaths?.Count ?? 0) == 0 &&
                             (candidate.MutatedResourceKeys?.Count ?? 0) == 0))
            {
                string request =
                    string.Join(
                        "\n",
                        child.Title ?? string.Empty,
                        child.Description ?? string.Empty,
                        child.AcceptanceCriteria ?? string.Empty);

                List<string> referencedArtifacts =
                    ownedArtifacts
                        .Where(
                            artifact =>
                                !ReferenceEquals(artifact.Owner, child) &&
                                request.IndexOf(
                                    artifact.Path,
                                    StringComparison.OrdinalIgnoreCase) >= 0)
                        .Select(artifact => artifact.Path)
                        .Distinct(StringComparer.OrdinalIgnoreCase)
                        .ToList();

                if (referencedArtifacts.Count == 0)
                    continue;

                child.ConsumedArtifactPaths = referencedArtifacts;

                if (MutatesSharedUnityScene(request))
                {
                    child.MutatedResourceKeys =
                        referencedArtifacts
                            .Where(
                                path =>
                                    path.EndsWith(
                                        ".unity",
                                        StringComparison.OrdinalIgnoreCase))
                            .Select(path => "scene:" + path)
                            .Distinct(StringComparer.OrdinalIgnoreCase)
                            .ToList();
                }

                repaired++;
            }

            return repaired;
        }


        private bool ReopenStaleCompletedChildBatch(
            KanbanTask parent)
        {
            if (parent == null)
                return false;

            List<KanbanTask> completedChildren =
                GetDirectChildren(parent)
                    .Where(child =>
                        child != null &&
                        child.Status == TaskStatus.Done)
                    .ToList();

            if (completedChildren.Count == 0)
                return false;

            string workspaceRoot =
                Directory.GetParent(Application.dataPath)?.FullName;

            if (string.IsNullOrWhiteSpace(workspaceRoot))
                return false;

            bool hasMissingClaimedArtifact =
                completedChildren.Any(child =>
                    HasMissingClaimedAssetDeliverable(
                        child,
                        workspaceRoot));

            if (!hasMissingClaimedArtifact)
                return false;

            foreach (KanbanTask child in completedChildren)
            {
                child.Status =
                    TaskStatus.Todo;

                child.ReviewStatus =
                    string.Empty;

                child.ReviewResult =
                    string.Empty;

                child.AgentError =
                    "AIRuntime reopened this batch because at least one claimed asset deliverable was absent from the workspace.";

                TouchTask(child);
            }

            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(showDialog: false);
            }

            _statusMessage =
                $"Reopened {completedChildren.Count} completed child task(s): claimed asset deliverables were missing";

            Repaint();

            return true;
        }


        private static bool HasMissingClaimedAssetDeliverable(
            KanbanTask task,
            string workspaceRoot)
        {
            string request =
                string.Join(
                    "\n",
                    task.Title ?? string.Empty,
                    task.Description ?? string.Empty,
                    task.AcceptanceCriteria ?? string.Empty);

            bool claimsCreation =
                request.IndexOf("create", StringComparison.OrdinalIgnoreCase) >= 0 ||
                request.IndexOf("write", StringComparison.OrdinalIgnoreCase) >= 0 ||
                request.IndexOf("author", StringComparison.OrdinalIgnoreCase) >= 0 ||
                request.IndexOf("implement", StringComparison.OrdinalIgnoreCase) >= 0 ||
                request.IndexOf("build", StringComparison.OrdinalIgnoreCase) >= 0;

            if (!claimsCreation)
                return false;

            string root =
                Path.GetFullPath(workspaceRoot)
                    .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) +
                Path.DirectorySeparatorChar;

            System.Text.RegularExpressions.MatchCollection matches =
                System.Text.RegularExpressions.Regex.Matches(
                    request,
                    @"Assets(?:[\\/][A-Za-z0-9_.-]+)+",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            foreach (System.Text.RegularExpressions.Match match in matches)
            {
                string relativePath =
                    match.Value
                        .Replace('\\', '/')
                        .TrimEnd('.', ',', ';', ':');

                string candidate =
                    Path.GetFullPath(
                        Path.Combine(
                            root,
                            relativePath.Replace('/', Path.DirectorySeparatorChar)));

                if (!candidate.StartsWith(root, StringComparison.OrdinalIgnoreCase) ||
                    (!File.Exists(candidate) && !Directory.Exists(candidate)))
                {
                    return true;
                }
            }

            return false;
        }


        private bool StartAgentTask(
            KanbanTask task,
            string agentId)
        {
            bool coordinationOnly =
                ShouldStartCoordinationOnlyRun(task);

            if (ReopenStaleCompletedChildBatch(task))
            {
                TrySetTaskStatus(
                    task,
                    TaskStatus.Todo,
                    out _statusMessage);

                return false;
            }

            if (!coordinationOnly &&
                HasIncompleteCommittedChildTasks(task))
            {
                TrySetTaskStatus(
                    task,
                    TaskStatus.Todo,
                    out _statusMessage);

                return false;
            }

            if (!coordinationOnly && !string.IsNullOrWhiteSpace(task.ParentTaskId))
            {
                string contractError = GetMissingExplicitOutputContract(task);
                if (!string.IsNullOrWhiteSpace(contractError))
                {
                    RequestPlanningRevision(task, contractError, "Implementation");
                    return false;
                }
            }
            // Planning retries begin from the persisted proposal for context,
            // but the current run must restate the complete desired graph.
            // Completion removes proposal cards that were not restated.
            RefreshDependencyTaskIds(task);

            string previousAgentOutput =
                task.AgentResult;

            string nextAgentMessage =
                task.NextAgentMessage;

            if (!coordinationOnly &&
                task.SuppressAutoDecomposition &&
                (nextAgentMessage ?? string.Empty).StartsWith(
                    "[AssociationLeaf]",
                    StringComparison.Ordinal))
            {
                // Pass the one-time handoff to the direct implementation run,
                // then remove it from durable state so review does not inherit
                // an orchestration correction that has already been applied.
                task.NextAgentMessage = string.Empty;
            }

            if (!TryClaimDispatch(task)) return false;
            task.Memory.Attempts.Last().Agent = agentId;
            task.Dispatch.AgentId = agentId;
            if (!coordinationOnly && !string.Equals(agentId, task.AssignedAgent?.Trim(), StringComparison.Ordinal))
            {
                string handoff = $"[AgentFallback] {task.AssignedAgent} is rate-limited. Continue this same card as {agentId} using its saved checkpoint, execution history and existing artifacts. Inspect current state before changing anything; do not restart completed work.";
                nextAgentMessage = string.IsNullOrWhiteSpace(nextAgentMessage) ? handoff : nextAgentMessage + "\n" + handoff;
                task.NextAgentMessage = nextAgentMessage;
            }
            _discardedAgentRunTaskIds.Remove(task.Id);
            CancellationTokenSource cancellation =
                new CancellationTokenSource();

            bool resumesProviderPlanningAttempt =
                coordinationOnly &&
                !string.IsNullOrWhiteSpace(task.PlanningAttemptId) &&
                (string.Equals(
                     task.ReviewStatus,
                     "Retry Available",
                     StringComparison.Ordinal) ||
                 IsProviderLifecycleHandoff(task.NextAgentMessage));

            string planningAttemptId =
                coordinationOnly
                    ? resumesProviderPlanningAttempt
                        ? task.PlanningAttemptId
                        : Guid.NewGuid().ToString("N")
                    : string.Empty;

            task.PlanningAttemptId = planningAttemptId;

            _activeAgentRuns.Add(
                agentId,
                new ActiveAgentRun
                {
                    AttemptId = task.Dispatch.Id,
                    TaskId =
                        task.Id,

                    Cancellation =
                        cancellation,

                    CoordinationOnly =
                        coordinationOnly,

                    PlanningAttemptId =
                        planningAttemptId,

                    LastProgressAtUtc =
                        DateTime.UtcNow,

                    LastActivityAtUtc =
                        DateTime.UtcNow
                });

            task.Status =
                TaskStatus.InProgress;

            task.RequestTitle =
                task.Title;

            task.RequestDescription =
                task.Description;

            task.RequestAcceptanceCriteria =
                task.AcceptanceCriteria;

            task.AgentError =
                string.Empty;

            task.BlockedReason =
                string.Empty;

            task.AgentStartedAt =
                DateTime.UtcNow.ToString(
                    "O");

            task.AgentFinishedAt =
                string.Empty;

            task.ReviewStatus =
                string.Empty;

            task.ReviewResult =
                string.Empty;

            task.ReviewEvidence =
                string.Empty;

            task.ReviewStartedAt =
                string.Empty;

            task.ReviewFinishedAt =
                string.Empty;

            TouchTask(
                task);

            MarkDirty();

            if (!PersistSchedulerState())
            {
                _activeAgentRuns.Remove(agentId);
                cancellation.Cancel();
                cancellation.Dispose();
                return false;
            }

            _statusMessage =
                $"{agentId} started '{task.Title}'";

            if (!coordinationOnly && !string.Equals(agentId, task.AssignedAgent?.Trim(), StringComparison.Ordinal))
                AIRuntimeAgentActivityStore.Shared.Publish(
                    new AIRuntimeEditorRuntimePluginContext("Kanban", agentId, task.Title, "Implementation", task.Id),
                    AIRuntimeAgentActivityKind.Status,
                    $"{task.AssignedAgent} is rate-limited. Continuing with {agentId} using the saved checkpoint.",
                    "The preferred assignment, artifact permissions and both agents' cooldowns are preserved.");

            Repaint();

            _ =
                RunAgentTaskAsync(
                    task.Id,
                    agentId,
                    GetOriginalRequestValue(
                        task.RequestTitle,
                        task.Title),
                    GetOriginalRequestValue(
                        task.RequestDescription,
                        task.Description),
                    GetOriginalRequestValue(
                        task.RequestAcceptanceCriteria,
                        task.AcceptanceCriteria),
                    previousAgentOutput,
                    nextAgentMessage,
                    task.ExecutionMemory,
                    reviewMode: false,
                    coordinationOnly: coordinationOnly,
                    cancellationToken: cancellation.Token,
                    attemptId: task.Dispatch.Id);
            return true;
        }


        private bool StartReviewTask(
            KanbanTask task,
            string agentId)
        {
            if (task?.ReviewStatus == "Awaiting visual approval") return false;

            if (!string.Equals(
                    GetWorkflowPhase(task),
                    WorkflowPhaseFinalReview,
                    StringComparison.Ordinal) &&
                TryBuildCoordinationParentReviewPass(
                    task,
                    out string rollupResult,
                    out string rollupEvidence))
            {
                task.ReviewStartedAt =
                    DateTime.UtcNow.ToString("O");

                CompleteReviewTask(
                    task,
                    agentId,
                    rollupResult,
                    string.Empty,
                    rollupEvidence);

                return false;
            }

            if (!TryClaimDispatch(task)) return false;
            task.Memory.Attempts.Last().Agent = agentId;
            _discardedAgentRunTaskIds.Remove(task.Id);
            CancellationTokenSource cancellation =
                new CancellationTokenSource();

            _activeAgentRuns.Add(
                agentId,
                new ActiveAgentRun
                {
                    AttemptId = task.Dispatch.Id,
                    TaskId =
                        task.Id,

                    Cancellation =
                        cancellation,

                    CoordinationOnly =
                        false,

                    LastProgressAtUtc =
                        DateTime.UtcNow,

                    LastActivityAtUtc =
                        DateTime.UtcNow
                });

            task.ReviewStatus =
                "Running";

            task.ReviewResult =
                string.Empty;

            task.ReviewEvidence =
                string.Empty;

            task.ReviewStartedAt =
                DateTime.UtcNow.ToString(
                    "O");

            task.ReviewFinishedAt =
                string.Empty;

            TouchTask(
                task);

            MarkDirty();


            if (!PersistSchedulerState())
            {
                _activeAgentRuns.Remove(agentId);
                cancellation.Cancel();
                cancellation.Dispose();
                return false;
            }


            _statusMessage =
                $"{agentId} is reviewing '{task.Title}'";

            Repaint();

            _ =
                RunAgentTaskAsync(
                    task.Id,
                    agentId,
                    GetOriginalRequestValue(
                        task.RequestTitle,
                        task.Title),
                    GetOriginalRequestValue(
                        task.RequestDescription,
                        task.Description),
                    GetOriginalRequestValue(
                        task.RequestAcceptanceCriteria,
                        task.AcceptanceCriteria),
                    task.AgentResult,
                    task.NextAgentMessage,
                    task.ExecutionMemory,
                    reviewMode: true,
                    coordinationOnly: false,
                    cancellationToken: cancellation.Token,
                    attemptId: task.Dispatch.Id);
            return true;
        }


        private async Task RunAgentTaskAsync(
            string taskId,
            string agentId,
            string title,
            string description,
            string acceptanceCriteria,
            string previousAgentOutput,
            string nextAgentMessage,
            string executionMemory,
            bool reviewMode,
            bool coordinationOnly,
            CancellationToken cancellationToken,
            string attemptId)
        {
            string result =
                null;

            string reviewEvidence =
                null;

            string error =
                null;

            ProviderFailureKind? providerFailureKind =
                null;

            try
            {
                if (RunnerOverrideForTests != null)
                {
                    result = await RunnerOverrideForTests(taskId, cancellationToken).ConfigureAwait(false);
                    return;
                }
                DelegateKanbanTaskExecutionServices capabilityServices =
                    new DelegateKanbanTaskExecutionServices(
                        () => { RequireAttempt(taskId, attemptId); return GetAgentTaskContext(taskId); },
                        coordinationOnly
                            ? arguments => { RequireAttempt(taskId, attemptId); return CreateAgentChildTask(taskId, arguments); }
                            : null,
                        taskId,
                        coordinationOnly
                            ? () => { RequireAttempt(taskId, attemptId); return CommitAgentPlanningAttempt(taskId); }
                            : null,
                        args => SaveCardCheckpoint(taskId, attemptId, args),
                        args => ReadCardMemory(taskId, attemptId, args));

                result =
                    await AIRuntimeKanbanAgentRunner.RunAsync(
                        agentId,
                        title,
                        description,
                        acceptanceCriteria,
                        previousAgentOutput,
                        nextAgentMessage,
                        reviewMode,
                        coordinationOnly,
                        cancellationToken,
                        evidence => reviewEvidence = evidence,
                        executionMemory,
                        memory =>
                            _pendingExecutionMemoryUpdates.Enqueue(
                                new PendingExecutionMemoryUpdate
                                {
                                    TaskId = taskId,
                                    AttemptId = attemptId,
                                    Memory = memory
                                }),
                        correlationId: taskId,
                        capabilityServices: capabilityServices,
                        historySink: value => QueueMemoryEvent(taskId, attemptId, value),
                        cardMemoryContext: JsonConvert.SerializeObject(new { Card = MemoryContext(FindTask(taskId)), Dependencies = MemoryDependencyHandoffs(FindTask(taskId)) }),
                        humanVisualEvidenceSink: artifacts => _pendingExecutionMemoryUpdates.Enqueue(new PendingExecutionMemoryUpdate {
                            TaskId = taskId, AttemptId = attemptId, HumanVisualArtifacts = artifacts }))
                        .ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                error =
                    "Agent run was cancelled.";
            }
            catch (ProviderExecutionException exception)
            {
                error =
                    exception.Message;

                providerFailureKind =
                    exception.FailureKind;
            }
            catch (Exception exception)
            {
                error =
                    exception.Message;
            }
            finally
            {
                // Never depend on UnitySynchronizationContext to perform the
                // final board transition. Provider and verification tasks can
                // finish while the Editor is unfocused; queue their outcome
                // and let EditorApplication.update apply it on the main thread.
                _pendingAgentCompletions.Enqueue(
                    new PendingAgentCompletion
                    {
                        TaskId = taskId,
                        AttemptId = attemptId,
                        AgentId = agentId,
                        Result = result,
                        Error = error,
                        ReviewEvidence = reviewEvidence,
                        ReviewMode = reviewMode,
                        ProviderFailureKind = providerFailureKind
                    });

                AIRuntimeKanbanScheduler.Wake();
                Debug.Log(
                    $"[Kanban] Queued completed run for task '{title}' (review={reviewMode}, error={!string.IsNullOrWhiteSpace(error)})." );
            }
        }


        private static bool HasContradictoryProducedArtifactProhibition(
            CreateKanbanChildTaskArguments arguments)
        {
            if (arguments == null ||
                !(arguments.ProducesArtifactPaths ?? new List<string>())
                    .Any(path => !string.IsNullOrWhiteSpace(path)))
            {
                return false;
            }

            string contract = string.Join(
                "\n",
                arguments.Description ?? string.Empty,
                arguments.AcceptanceCriteria ?? string.Empty);

            return Regex.IsMatch(
                contract,
                @"\b(?:do\s+not|don't|must\s+not|never)\b[^.?!;\r\n]{0,120}\b(?:create|write|touch)\b[^.?!;\r\n]{0,80}\b(?:any\s+)?(?:asset|file)s?\b",
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);
        }


        private object CommitAgentPlanningAttempt(
            string parentTaskId)
        {
            KanbanTask parent =
                FindTask(
                    parentTaskId);

            ActiveAgentRun planningRun =
                _activeAgentRuns.Values.FirstOrDefault(
                    run =>
                        run != null &&
                        string.Equals(
                            run.TaskId,
                            parentTaskId,
                            StringComparison.Ordinal));

            CommitKanbanPlanResult response =
                new CommitKanbanPlanResult
                {
                    ParentTaskId = parentTaskId ?? string.Empty,
                    ParentTitle = parent?.Title ?? string.Empty,
                    PlanningAttemptId = planningRun?.PlanningAttemptId ?? string.Empty
                };

            if (parent == null ||
                planningRun == null ||
                !planningRun.CoordinationOnly ||
                !IsPlanningCoordinator(parent))
            {
                response.Error =
                    "Only the active project Planning / Orchestration run may commit a Kanban plan.";
                response.ResponseInstruction =
                    "Do not continue graph mutation outside the active Planning run.";
                return response;
            }

            if (planningRun.PlanningCommitted)
            {
                response.Succeeded = true;
                response.ProposedChildCount =
                    GetDirectChildren(parent).Count;
                response.ResponseInstruction =
                    "This Planning snapshot was already committed. Return without further graph mutation.";
                return response;
            }

            response.ProposedChildCount =
                GetDirectChildren(parent).Count;

            string validationError =
                ValidatePlanningProposal(parent, planningRun.PlanningAttemptId);

            planningRun.LastPlanningCommitError = validationError;

            if (!string.IsNullOrWhiteSpace(
                    validationError))
            {
                response.Error = validationError;
                response.ResponseInstruction =
                    "Correct only the reported graph defects, then call commit_kanban_plan again.";
                MarkDirty();
                if (_autoSave)
                    SaveBoard(showDialog: false);
                return response;
            }

            // Validation is read-only. Retire omitted branches only after the
            // complete candidate graph passes, on this same main-thread call.
            ReconcilePlanningProposalSnapshot(parent, planningRun.PlanningAttemptId);
            foreach (KanbanTask child in GetDirectChildren(parent))
            {
                // Keep the attempt membership until the completion handoff so
                // the accepted snapshot cannot be mistaken for omitted cards.
                child.IsDecompositionDraft = false;
            }
            response.ProposedChildCount = GetDirectChildren(parent).Count;
            planningRun.PlanningCommitted = true;
            response.Succeeded = true;
            response.ResponseInstruction =
                "The Planning snapshot is durably accepted. Do not create or revise more cards; a prose summary is optional.";

            MarkDirty();
            if (_autoSave)
                SaveBoard(showDialog: false);

            return response;
        }


        private object CreateAgentChildTask(
            string parentTaskId,
            CreateKanbanChildTaskArguments arguments)
        {
            KanbanTask parent =
                FindTask(
                    parentTaskId);

            if (parent == null)
            {
                throw new InvalidOperationException(
                    "The currently executing parent task no longer exists.");
            }

            ActiveAgentRun trackedPlanningRun =
                _activeAgentRuns.Values.FirstOrDefault(
                    run =>
                        run != null &&
                        string.Equals(
                            run.TaskId,
                            parent.Id,
                            StringComparison.Ordinal));

            bool hasTrackedRun =
                trackedPlanningRun != null;

            bool activePlanningRun =
                hasTrackedRun &&
                IsPlanningCoordinator(parent) &&
                trackedPlanningRun.CoordinationOnly;

            // Runtime calls always have a tracked run and therefore require
            // the exact Planning-root authority above. The no-run path keeps
            // deterministic editor/unit fixtures able to exercise the
            // transactional graph mutator directly without manufacturing an
            // asynchronous runner.
            if (hasTrackedRun &&
                !activePlanningRun)
            {
                throw new InvalidOperationException(
                    "Only the project Planning / Orchestration run may create or revise Kanban child cards.");
            }

            if (activePlanningRun &&
                trackedPlanningRun.PlanningCommitted)
            {
                throw new InvalidOperationException(
                    "The current Planning snapshot is already committed; no further graph mutation is allowed in this run.");
            }

            if (arguments == null ||
                string.IsNullOrWhiteSpace(arguments.Title))
            {
                throw new ArgumentException(
                    "Child task title is required.");
            }

            if (string.IsNullOrWhiteSpace(arguments.Description))
            {
                return BuildRejectedChildResult(
                    parent,
                    arguments,
                    "Child task description is required and cannot be truncated or empty.");
            }

            if (string.IsNullOrWhiteSpace(arguments.AcceptanceCriteria))
            {
                return BuildRejectedChildResult(
                    parent,
                    arguments,
                    "Child task acceptance criteria are required and cannot be truncated or empty.");
            }

            if (HasContradictoryProducedArtifactProhibition(arguments))
            {
                return BuildRejectedChildResult(
                    parent,
                    arguments,
                    "The child contract declares ProducedArtifactPaths but its prose prohibits creating or touching files/assets. Permit creation of the declared output while keeping unrelated product assets read-only.");
            }

            if (!Enum.TryParse(
                    arguments.Priority?.Trim() ??
                    "Normal",
                    true,
                    out TaskPriority priority))
            {
                return BuildRejectedChildResult(
                    parent,
                    arguments,
                    "Priority must be Low, Normal, High, or Critical.");
            }

            string requestedAgent =
                arguments.AssignedAgent?.Trim() ??
                string.Empty;

            bool requestedAgentIsActive =
                !string.IsNullOrWhiteSpace(requestedAgent) &&
                IsKanbanAgentActive(requestedAgent);

            string childAssignedAgent =
                requestedAgentIsActive
                    ? requestedAgent
                    : parent.AssignedAgent;

            bool childAssignmentInherited =
                !requestedAgentIsActive;

            string routingInstruction =
                !string.IsNullOrWhiteSpace(requestedAgent) &&
                !requestedAgentIsActive
                    ? $" Requested agent '{requestedAgent}' is not active, so AIRuntime safely routed this child to executing parent agent '{parent.AssignedAgent}'."
                    : string.Empty;

            string invalidArtifactPath =
                (arguments.ProducesArtifactPaths ?? new List<string>())
                    .Concat(
                        arguments.ConsumesArtifactPaths ?? new List<string>())
                    .FirstOrDefault(
                        path =>
                            !string.IsNullOrWhiteSpace(path) &&
                            !path.Trim()
                                .Replace('\\', '/')
                                .StartsWith(
                                    "Assets/",
                                    StringComparison.OrdinalIgnoreCase));

            if (!string.IsNullOrWhiteSpace(invalidArtifactPath))
            {
                return BuildRejectedChildResult(
                    parent,
                    arguments,
                    $"Artifact path '{invalidArtifactPath}' is not a canonical workspace-relative Assets/... path.");
            }

            string now =
            DateTime.UtcNow.ToString("O");

            KanbanTask conflictingReference =
                FindConflictingChildTaskParentReference(
                parent,
                arguments);


            if (conflictingReference != null)
            {
                return new CreateKanbanChildTaskResult
                {
                    Succeeded = false,
                    Error =
                        $"Child attachment criteria reference card '{conflictingReference.Id}', " +
                        $"but the executing parent is '{parent.Id}'. Replace the stale card ID " +
                        $"with the authoritative executing ParentTaskId '{parent.Id}' and retry.",
                    ParentTaskId = parent.Id,
                    ParentTitle = parent.Title,
                    ChildStatus = string.Empty,
                    AssignedAgent = childAssignedAgent,
                    DirectChildCount = GetDirectChildren(parent).Count,
                    ResponseInstruction =
                        "No child was created. Correct the attachment criteria using ParentTaskId and retry create_kanban_child_task." +
                        routingInstruction
                };
            }

            List<KanbanTask> existingChildren =
                GetDirectChildren(
                    parent);

            if (parent.SuppressAutoDecomposition &&
                (parent.NextAgentMessage ?? string.Empty).StartsWith(
                    "[AssociationLeaf]",
                    StringComparison.Ordinal))
            {
                return BuildAtomicLeafAssociationResult(
                    parent,
                    parent,
                    arguments,
                    childAssignedAgent,
                    routingInstruction);
            }

            List<string> proposedArtifactPaths =
                NormalizeArtifactPaths(
                    arguments.ProducesArtifactPaths);

            bool repeatsAncestorOwnership = false;

            KanbanTask equivalentArtifactOwner = null;

            if (IsSingleScriptArtifactSet(
                    proposedArtifactPaths))
            {
                equivalentArtifactOwner =
                    FindEquivalentArtifactOwner(
                        parent,
                        proposedArtifactPaths,
                        out repeatsAncestorOwnership);
            }

            bool repeatsAtomicParentOwnership =
                equivalentArtifactOwner != null &&
                repeatsAncestorOwnership &&
                IsStandaloneScriptAssociationLeaf(
                    parent,
                    proposedArtifactPaths[0]);

            if (equivalentArtifactOwner != null &&
                repeatsAtomicParentOwnership)
            {
                return BuildAtomicLeafAssociationResult(
                    parent,
                    equivalentArtifactOwner,
                    arguments,
                    childAssignedAgent,
                    routingInstruction);
            }

            if (repeatsAncestorOwnership)
            {
                // Aggregate/multi-outcome parents may intentionally delegate a
                // script or scene mutation they also list in their roll-up
                // ownership. Only a standalone single-script card is an
                // atomic recursion loop.
                equivalentArtifactOwner = null;
            }

            if (existingChildren.Any(
                    existing =>
                        string.Equals(
                            existing?.Title?.Trim(),
                            arguments.Title.Trim(),
                            StringComparison.OrdinalIgnoreCase)))
            {
                KanbanTask existingChild =
                    existingChildren.First(
                        existing =>
                            string.Equals(
                                existing?.Title?.Trim(),
                                arguments.Title.Trim(),
                                StringComparison.OrdinalIgnoreCase));

                if ((existingChild.IsDecompositionDraft &&
                     parent.DecompositionFailureCount > 0) ||
                    (IsPlanningCoordinator(parent) &&
                     !string.IsNullOrWhiteSpace(
                         parent.PlanningKickbackReason)))
                {
                    List<string> replacementDependencyTitles =
                        (arguments.DependsOnSiblingTitles ?? new List<string>())
                            .Concat(new[] { arguments.DependsOnSiblingTitle })
                            .Where(title => !string.IsNullOrWhiteSpace(title))
                            .Select(title => title.Trim())
                            .Where(
                                title =>
                                    !string.Equals(
                                        title,
                                        existingChild.Title?.Trim(),
                                        StringComparison.OrdinalIgnoreCase))
                            .Distinct(StringComparer.OrdinalIgnoreCase)
                            .ToList();

                    string replacementDescription =
                        arguments.Description.Trim();

                    IReadOnlyList<string> alreadyDeclared =
                        ExtractDeclaredDependencyTitles(
                            replacementDescription);

                    List<string> replacementMissingDependencyTitles =
                        replacementDependencyTitles
                            .Where(
                                title =>
                                    !alreadyDeclared.Contains(
                                        title,
                                        StringComparer.OrdinalIgnoreCase))
                            .ToList();

                    if (replacementMissingDependencyTitles.Count > 0)
                    {
                        string dependencyNoun =
                            replacementMissingDependencyTitles.Count == 1
                                ? "task"
                                : "tasks";

                        replacementDescription =
                            $"Depends on sibling {dependencyNoun} " +
                            string.Join(
                                ", ",
                                replacementMissingDependencyTitles.Select(
                                    title => $"\"{title}\"")) +
                            " being Done.\n" +
                            replacementDescription;
                    }

                    List<string> replacementProducedArtifactPaths =
                        NormalizeArtifactPaths(
                            arguments.ProducesArtifactPaths);
                    List<string> replacementConsumedArtifactPaths =
                        NormalizeArtifactPaths(
                            arguments.ConsumesArtifactPaths);
                    List<string> replacementMutatedResourceKeys =
                        NormalizeResourceKeysForContract(
                            string.Join(
                                "\n",
                                arguments.Title ?? string.Empty,
                                replacementDescription,
                                arguments.AcceptanceCriteria ?? string.Empty),
                            arguments.MutatesResourceKeys);

                    string replacementAcceptanceCriteria =
                        arguments.AcceptanceCriteria.Trim();

                    bool contractChanged =
                        !string.Equals(
                            existingChild.Description ?? string.Empty,
                            replacementDescription,
                            StringComparison.Ordinal) ||
                        !string.Equals(
                            existingChild.AcceptanceCriteria ?? string.Empty,
                            replacementAcceptanceCriteria,
                            StringComparison.Ordinal) ||
                        !HaveSameValues(
                            existingChild.DeclaredDependencyTitles,
                            replacementDependencyTitles) ||
                        !HaveSameValues(
                            existingChild.ProducedArtifactPaths,
                            replacementProducedArtifactPaths) ||
                        !HaveSameValues(
                            existingChild.ConsumedArtifactPaths,
                            replacementConsumedArtifactPaths) ||
                        !HaveSameValues(
                            existingChild.MutatedResourceKeys,
                            replacementMutatedResourceKeys) ||
                        !string.Equals(
                            existingChild.AssignedAgent ?? string.Empty,
                            childAssignedAgent ?? string.Empty,
                            StringComparison.Ordinal) ||
                        existingChild.Priority != priority;

                    existingChild.Description = replacementDescription;
                    existingChild.DeclaredDependencyTitles =
                        replacementDependencyTitles;
                    existingChild.AcceptanceCriteria =
                        replacementAcceptanceCriteria;
                    existingChild.ProducedArtifactPaths =
                        replacementProducedArtifactPaths;
                    existingChild.ConsumedArtifactPaths =
                        replacementConsumedArtifactPaths;
                    existingChild.MutatedResourceKeys =
                        replacementMutatedResourceKeys;
                    existingChild.AssignedAgent =
                        childAssignedAgent;
                    existingChild.AgentAssignmentInherited =
                        childAssignmentInherited;
                    existingChild.Priority = priority;
                    if (contractChanged || existingChild.IsDecompositionDraft)
                    {
                        existingChild.Status = TaskStatus.Todo;
                        existingChild.ReviewStatus = string.Empty;
                        existingChild.ReviewResult = string.Empty;
                        existingChild.ReviewEvidence = string.Empty;
                        existingChild.ReviewFailureKind = string.Empty;
                        existingChild.AgentError = string.Empty;
                        existingChild.BlockedReason = string.Empty;
                        existingChild.NextAgentMessage = string.Empty;
                    }
                    existingChild.PlannedRevision =
                        Math.Max(0, parent.PlanRevision) + 1;
                    existingChild.ProposedInPlanningAttemptId =
                        trackedPlanningRun?.PlanningAttemptId ?? string.Empty;
                    existingChild.UpdatedAt = now;

                    RefreshDependencyTaskIds(parent);
                    MarkDirty();

                    if (_autoSave)
                        SaveBoard(showDialog: false);

                    Repaint();

                    return new CreateKanbanChildTaskResult
                    {
                        Succeeded = true,
                        ParentTaskId = parent.Id,
                        ParentTitle = parent.Title,
                        ChildTaskId = existingChild.Id,
                        ChildTitle = existingChild.Title,
                        ChildStatus = GetStatusLabel(existingChild.Status),
                        AssignedAgent = existingChild.AssignedAgent,
                        DependsOnSiblingTitle =
                            replacementDependencyTitles.Count == 1
                                ? replacementDependencyTitles[0]
                                : string.Empty,
                        DependsOnSiblingTitles =
                            replacementDependencyTitles,
                        ProducesArtifactPaths =
                            existingChild.ProducedArtifactPaths,
                        ConsumesArtifactPaths =
                            existingChild.ConsumedArtifactPaths,
                        MutatesResourceKeys =
                            existingChild.MutatedResourceKeys,
                        DirectChildCount = existingChildren.Count,
                        ResponseInstruction =
                            "The proposed card was updated in place and recorded in the current desired graph. Restate every other card that belongs in the complete plan; omitted proposal cards are retired when this Planning attempt completes." +
                            routingInstruction
                    };
                }

                existingChild.ProposedInPlanningAttemptId =
                    trackedPlanningRun?.PlanningAttemptId ?? string.Empty;
                TouchTask(existingChild);

                return new CreateKanbanChildTaskResult
                {
                    Succeeded = true,
                    ParentTaskId = parent.Id,
                    ParentTitle = parent.Title,
                    ChildTaskId = existingChild.Id,
                    ChildTitle = existingChild.Title,
                    ChildStatus = GetStatusLabel(existingChild.Status),
                    AssignedAgent = existingChild.AssignedAgent,
                    ProducesArtifactPaths =
                        existingChild.ProducedArtifactPaths ??
                        new List<string>(),
                    ConsumesArtifactPaths =
                        existingChild.ConsumedArtifactPaths ??
                        new List<string>(),
                    MutatesResourceKeys =
                        existingChild.MutatedResourceKeys ??
                        new List<string>(),
                    DirectChildCount = existingChildren.Count,
                    ResponseInstruction =
                        "An equivalent sibling already exists, was reused, and is included in the current desired graph. Restate every other card that should remain in the complete plan."
                };
            }

            if (equivalentArtifactOwner != null)
            {
                parent.ConsumedArtifactPaths =
                    parent.ConsumedArtifactPaths ??
                    new List<string>();

                foreach (string artifactPath in proposedArtifactPaths)
                {
                    if (!(parent.ConsumedArtifactPaths ?? new List<string>())
                        .Contains(
                            artifactPath,
                            StringComparer.OrdinalIgnoreCase))
                    {
                        parent.ConsumedArtifactPaths.Add(
                            artifactPath);
                    }
                }

                TouchTask(parent);
                ReconcileAllTaskRelationships();
                MarkDirty();

                if (_autoSave)
                    SaveBoard(showDialog: false);

                Repaint();

                return new CreateKanbanChildTaskResult
                {
                    Succeeded = false,
                    Error =
                        $"Artifact ownership conflict: card '{equivalentArtifactOwner.Id}' ('{equivalentArtifactOwner.Title}') already owns {string.Join(", ", proposedArtifactPaths)}. A second writer cannot be created in another branch without an explicit ordered mutation contract.",
                    ParentTaskId = parent.Id,
                    ParentTitle = parent.Title,
                    ChildTaskId = equivalentArtifactOwner.Id,
                    ChildTitle = equivalentArtifactOwner.Title,
                    ChildStatus = GetStatusLabel(equivalentArtifactOwner.Status),
                    AssignedAgent = equivalentArtifactOwner.AssignedAgent,
                    ProducesArtifactPaths =
                        equivalentArtifactOwner.ProducedArtifactPaths ??
                        new List<string>(),
                    ConsumesArtifactPaths =
                        equivalentArtifactOwner.ConsumedArtifactPaths ??
                        new List<string>(),
                    MutatesResourceKeys =
                        equivalentArtifactOwner.MutatedResourceKeys ??
                        new List<string>(),
                    DirectChildCount = existingChildren.Count,
                    ResponseInstruction =
                        $"AIRuntime compared this proposal with every card and associated the executing card with existing owner '{equivalentArtifactOwner.Id}' ('{equivalentArtifactOwner.Title}'). No duplicate writer was created, and the association alone does not satisfy readiness. Remove the duplicate outcome or define a distinct consumer/ordered mutation card; do not claim this child was created." +
                        routingInstruction
                };
            }

            var dependencyTitles =
                (arguments.DependsOnSiblingTitles ?? new List<string>())
                    .Concat(
                        new[]
                        {
                            arguments.DependsOnSiblingTitle
                        })
                    .Where(title => !string.IsNullOrWhiteSpace(title))
                    .Select(title => title.Trim())
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

            if (dependencyTitles.Count == 0 &&
                !arguments.CanRunInParallel &&
                existingChildren.Count > 0 &&
                !string.IsNullOrWhiteSpace(parent.AgentStartedAt))
            {
                KanbanTask previousChildFromCurrentRun =
                    existingChildren
                        .Where(
                            existing =>
                                string.CompareOrdinal(
                                    existing?.CreatedAt ?? string.Empty,
                                    parent.AgentStartedAt) >= 0)
                        .LastOrDefault();

                string previousTitle =
                    previousChildFromCurrentRun?.Title?.Trim() ??
                    string.Empty;

                if (!string.IsNullOrWhiteSpace(previousTitle))
                    dependencyTitles.Add(previousTitle);
            }

            string childDescription =
                arguments.Description?.Trim() ??
                string.Empty;

            string childAcceptanceCriteria =
                arguments.AcceptanceCriteria?.Trim() ??
                string.Empty;

            string childRequest =
                arguments.Title.Trim() +
                "\n" +
                childDescription +
                "\n" +
                childAcceptanceCriteria;

            if (MutatesSharedUnityScene(
                    childRequest))
            {
                string scenePath =
                    ExtractExplicitUnityScenePath(
                        childRequest);

                if (!string.IsNullOrWhiteSpace(scenePath))
                {
                    string requiredAssetScope =
                        ExtractInheritedAssetScope(
                            parent);

                    if (!string.IsNullOrWhiteSpace(
                            requiredAssetScope) &&
                        !scenePath.StartsWith(
                            requiredAssetScope.TrimEnd('/') +
                            "/",
                            StringComparison.OrdinalIgnoreCase))
                    {
                        return BuildRejectedChildResult(
                            parent,
                            arguments,
                            $"Scene path '{scenePath}' is outside the inherited parent asset scope '{requiredAssetScope}'. Use a canonical scene inside that scope.");
                    }

                    KanbanTask previousSceneWriter =
                        existingChildren
                            .LastOrDefault(
                                existing =>
                                    existing != null &&
                                    MutatesSharedUnityScene(
                                        string.Join(
                                            "\n",
                                            existing.Title ?? string.Empty,
                                            existing.Description ?? string.Empty,
                                            existing.AcceptanceCriteria ?? string.Empty)) &&
                                    string.Equals(
                                        ExtractExplicitUnityScenePath(
                                            string.Join(
                                                "\n",
                                                existing.Description ?? string.Empty,
                                                existing.AcceptanceCriteria ?? string.Empty)),
                                        scenePath,
                                        StringComparison.OrdinalIgnoreCase));

                    if (previousSceneWriter != null &&
                        !dependencyTitles.Contains(
                            previousSceneWriter.Title,
                            StringComparer.OrdinalIgnoreCase))
                    {
                        dependencyTitles.Add(
                            previousSceneWriter.Title.Trim());
                    }
                }
            }

            IReadOnlyList<string> declaredDependencyTitles =
                ExtractDeclaredDependencyTitles(
                    childDescription);

            List<string> missingDependencyTitles =
                dependencyTitles
                    .Where(
                        title =>
                            !declaredDependencyTitles.Contains(
                                title,
                                StringComparer.OrdinalIgnoreCase))
                    .ToList();

            if (missingDependencyTitles.Count > 0)
            {
                string dependencyNoun =
                    missingDependencyTitles.Count == 1
                        ? "task"
                        : "tasks";

                childDescription =
                    $"Depends on sibling {dependencyNoun} " +
                    string.Join(
                        ", ",
                        missingDependencyTitles.Select(title => $"\"{title}\"")) +
                    " being Done.\n" +
                    childDescription;
            }

            KanbanTask child =
                new KanbanTask
                {
                    Id = Guid.NewGuid().ToString("N"),
                    Title = arguments.Title.Trim(),
                    Description = childDescription,
                    AcceptanceCriteria = childAcceptanceCriteria,
                    ParentTaskId = parent.Id,
                    DependsOnTaskIds = new List<string>(),
                    DeclaredDependencyTitles =
                        dependencyTitles
                            .Distinct(StringComparer.OrdinalIgnoreCase)
                            .ToList(),
                    ProducedArtifactPaths =
                        NormalizeArtifactPaths(
                            arguments.ProducesArtifactPaths),
                    ConsumedArtifactPaths =
                        NormalizeArtifactPaths(
                            arguments.ConsumesArtifactPaths),
                    MutatedResourceKeys =
                        NormalizeResourceKeysForContract(
                            childRequest,
                            arguments.MutatesResourceKeys),
                    CreationSource = "create_kanban_child_task",
                    CreatedStatus = "To Do",
                    AgentAssignmentInherited =
                        childAssignmentInherited,
                    IsDecompositionDraft = true,
                    PlannedRevision =
                        Math.Max(0, parent.PlanRevision) + 1,
                    ProposedInPlanningAttemptId =
                        trackedPlanningRun?.PlanningAttemptId ?? string.Empty,
                    AssignedAgent =
                        childAssignedAgent,
                    Tags = string.Empty,
                    DueDate = string.Empty,
                    Status = TaskStatus.Todo,
                    Priority = priority,
                    CreatedAt = now,
                    UpdatedAt = now
                };

            _board.Tasks.Add(child);

            RefreshDependencyTaskIds(parent);

            if (TryFindDependencyCycle(
                    child,
                    new HashSet<string>(StringComparer.Ordinal),
                    new HashSet<string>(StringComparer.Ordinal),
                    new List<string>(),
                    out string dependencyCycle))
            {
                _board.Tasks.Remove(child);
                RefreshDependencyTaskIds(parent);

                return BuildRejectedChildResult(
                    parent,
                    arguments,
                    "Dependency cycle rejected: " + dependencyCycle +
                    ". Reverse or remove one sibling dependency and retry without recreating valid children.");
            }

            TrySetTaskStatus(
                parent,
                TaskStatus.Todo,
                out _statusMessage);

            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }

            Repaint();

            return new CreateKanbanChildTaskResult
            {
                Succeeded = true,
                ParentTaskId = parent.Id,
                ParentTitle = parent.Title,
                ChildTaskId = child.Id,
                ChildTitle = child.Title,
                ChildStatus = GetStatusLabel(child.Status),
                AssignedAgent = child.AssignedAgent,
                DependsOnSiblingTitle =
                    dependencyTitles.Count == 1
                        ? dependencyTitles[0]
                        : string.Empty,
                DependsOnSiblingTitles = dependencyTitles,
                ProducesArtifactPaths =
                    child.ProducedArtifactPaths,
                ConsumesArtifactPaths =
                    child.ConsumedArtifactPaths,
                MutatesResourceKeys =
                    child.MutatedResourceKeys,
                DirectChildCount = GetDirectChildren(parent).Count,
                ResponseInstruction =
                    "The card is included in the current Planning snapshot and remains ineligible to start until AIRuntime reconciles and approves the complete graph. Restate every desired direct child before returning; omitted proposal cards are retired." +
                    routingInstruction
            };
        }


        private static List<string> NormalizeArtifactPaths(
            IEnumerable<string> paths)
        {
            return (paths ?? Enumerable.Empty<string>())
                .Where(path => !string.IsNullOrWhiteSpace(path))
                .Select(path => path.Trim().Replace('\\', '/'))
                .Where(path => path.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }


        private KanbanTask FindEquivalentArtifactOwner(
            KanbanTask parent,
            IReadOnlyCollection<string> proposedArtifactPaths,
            out bool repeatsAncestorOwnership)
        {
            repeatsAncestorOwnership = false;

            if (parent == null ||
                proposedArtifactPaths == null ||
                proposedArtifactPaths.Count == 0)
            {
                return null;
            }

            HashSet<string> proposed =
                new HashSet<string>(
                    proposedArtifactPaths,
                    StringComparer.OrdinalIgnoreCase);

            HashSet<string> visited =
                new HashSet<string>(StringComparer.Ordinal);

            KanbanTask ancestor = parent;

            while (ancestor != null &&
                   !string.IsNullOrWhiteSpace(ancestor.Id) &&
                   visited.Add(ancestor.Id))
            {
                if (HasEquivalentProducedArtifactSet(
                        ancestor,
                        proposed))
                {
                    repeatsAncestorOwnership = true;
                    return ancestor;
                }

                ancestor = FindTask(ancestor.ParentTaskId);
            }

            return (_board?.Tasks ?? new List<KanbanTask>())
                .Where(
                    candidate =>
                        candidate != null &&
                        !string.IsNullOrWhiteSpace(candidate.Id) &&
                        HasEquivalentProducedArtifactSet(
                            candidate,
                            proposed))
                .OrderBy(
                    candidate =>
                        candidate.CreatedAt ?? string.Empty,
                    StringComparer.Ordinal)
                .ThenBy(
                    candidate => candidate.Id,
                    StringComparer.Ordinal)
                .FirstOrDefault();
        }


        private static bool HasEquivalentProducedArtifactSet(
            KanbanTask task,
            ISet<string> proposedArtifactPaths)
        {
            List<string> owned =
                NormalizeArtifactPaths(
                    task?.ProducedArtifactPaths);

            return owned.Count > 0 &&
                   owned.Count == proposedArtifactPaths.Count &&
                   owned.All(proposedArtifactPaths.Contains);
        }


        private static bool IsSingleScriptArtifactSet(
            IReadOnlyCollection<string> artifactPaths)
        {
            return artifactPaths != null &&
                   artifactPaths.Count == 1 &&
                   artifactPaths.First().EndsWith(
                       ".cs",
                       StringComparison.OrdinalIgnoreCase);
        }


        private static bool IsStandaloneScriptAssociationLeaf(
            KanbanTask task,
            string artifactPath)
        {
            // Structured ownership is the authoritative coordination
            // contract. A card which owns exactly one script and mutates no
            // other resource is already an atomic producer, even when its C#
            // contract mentions Update, Transform, Rigidbody, scenes, or
            // prefabs. Re-running the prose classifier here previously let a
            // false scene-mutation match disable the same-artifact recursion
            // guard.
            return task != null &&
                   string.Equals(
                       GetSingleOwnedScriptPath(task),
                       artifactPath,
                       StringComparison.OrdinalIgnoreCase) &&
                   MutatesOnlyOwnedScriptArtifact(
                       task,
                       artifactPath);
        }


        private static bool MutatesOnlyOwnedScriptArtifact(
            KanbanTask task,
            string artifactPath)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(artifactPath))
            {
                return false;
            }

            string normalizedPath =
                artifactPath.Trim().Replace('\\', '/');

            string fileName =
                Path.GetFileName(normalizedPath) ?? string.Empty;

            return (task.MutatedResourceKeys ?? new List<string>())
                .Where(key => !string.IsNullOrWhiteSpace(key))
                .Select(key => key.Trim().Replace('\\', '/'))
                .All(
                    key =>
                        key.IndexOf(
                            normalizedPath,
                            StringComparison.OrdinalIgnoreCase) >= 0 ||
                        (!string.IsNullOrWhiteSpace(fileName) &&
                         key.IndexOf(
                             fileName,
                             StringComparison.OrdinalIgnoreCase) >= 0));
        }


        private object BuildAtomicLeafAssociationResult(
            KanbanTask parent,
            KanbanTask existingOwner,
            CreateKanbanChildTaskArguments arguments,
            string assignedAgent,
            string routingInstruction)
        {
            string[] artifactPaths =
                NormalizeArtifactPaths(
                        arguments?.ProducesArtifactPaths)
                    .DefaultIfEmpty(
                        NormalizeArtifactPaths(
                                parent?.ProducedArtifactPaths)
                            .FirstOrDefault() ??
                        "the same owned artifact")
                    .ToArray();

            int removedDrafts =
                ReconcileAtomicLeafDecompositionDrafts(
                    parent,
                    artifactPaths[0]);

            ActiveAgentRun trackedRun =
                _activeAgentRuns.Values.FirstOrDefault(
                    run =>
                        run != null &&
                        string.Equals(
                            run.TaskId,
                            parent.Id,
                            StringComparison.Ordinal));

            bool requiresDirectHandoff =
                trackedRun == null ||
                trackedRun.CoordinationOnly;

            parent.SuppressAutoDecomposition = true;
            parent.NextAgentMessage =
                requiresDirectHandoff
                    ? "[AssociationLeaf] AIRuntime compared this proposal with every card and found that it repeats atomic artifact ownership already represented by " +
                      $"card '{existingOwner?.Id}' ('{existingOwner?.Title}') for {string.Join(", ", artifactPaths)}. " +
                      "Resume the full original card contract and execute it directly; do not decompose the same artifact again."
                    : string.Empty;

            TouchTask(parent);
            ReconcileAllTaskRelationships();
            MarkDirty();

            if (_autoSave)
                SaveBoard(showDialog: false);

            Repaint();

            return new CreateKanbanChildTaskResult
            {
                Succeeded = true,
                ParentTaskId = parent.Id,
                ParentTitle = parent.Title,
                ChildTaskId = existingOwner?.Id ?? parent.Id,
                ChildTitle = existingOwner?.Title ?? parent.Title,
                ChildStatus = GetStatusLabel(
                    existingOwner?.Status ?? parent.Status),
                AssignedAgent =
                    existingOwner?.AssignedAgent ??
                    assignedAgent,
                ProducesArtifactPaths =
                    existingOwner?.ProducedArtifactPaths ??
                    parent.ProducedArtifactPaths ??
                    new List<string>(),
                DirectChildCount = GetDirectChildren(parent).Count,
                ResponseInstruction =
                    (requiresDirectHandoff
                        ? "No duplicate child was created. This coordination turn identified an atomic ownership leaf, so AIRuntime will hand the full original card off once for direct implementation. Do not create verification or producer subcards for the same artifact."
                        : "No duplicate child was created. This run already has implementation tools: continue now by completing and verifying the full original card directly. Do not stop at this association result and do not create verification or producer subcards for the same artifact.") +
                    (removedDrafts > 0
                        ? $" AIRuntime discarded {removedDrafts} draft card(s) from this incomplete recursive decomposition turn."
                        : string.Empty) +
                    routingInstruction
            };
        }


        private int ReconcileAtomicLeafDecompositionDrafts(
            KanbanTask parent,
            string artifactPath)
        {
            if (parent == null ||
                string.IsNullOrWhiteSpace(parent.Id))
            {
                return 0;
            }

            List<KanbanTask> drafts =
                GetDirectChildren(parent)
                    .Where(
                        child =>
                            child != null &&
                            child.IsDecompositionDraft &&
                            string.Equals(
                                child.CreationSource,
                                "create_kanban_child_task",
                                StringComparison.Ordinal))
                    .ToList();

            if (drafts.Count == 0)
                return 0;

            HashSet<string> removableIds =
                new HashSet<string>(
                    drafts
                        .SelectMany(
                            draft =>
                                new[] { draft }
                                    .Concat(GetAllDescendants(draft)))
                        .Where(
                            task =>
                                task != null &&
                                string.Equals(
                                    task.CreationSource,
                                    "create_kanban_child_task",
                                    StringComparison.Ordinal) &&
                                IsTaskWithinArtifactScope(
                                    task,
                                    artifactPath))
                        .Select(task => task.Id),
                    StringComparer.Ordinal);

            string retainedParentId =
                parent.ParentTaskId ?? string.Empty;

            foreach (KanbanTask retained in _board.Tasks.Where(
                         task =>
                             task != null &&
                             !removableIds.Contains(task.Id) &&
                             (drafts.Contains(task) ||
                              removableIds.Contains(
                                  task.ParentTaskId ?? string.Empty))))
            {
                retained.ParentTaskId = retainedParentId;
                retained.IsDecompositionDraft = false;
                TouchTask(retained);
            }

            int before = _board.Tasks.Count;

            _board.Tasks.RemoveAll(
                task =>
                    task != null &&
                    removableIds.Contains(task.Id));

            return before - _board.Tasks.Count;
        }


        private static bool HaveSameValues(
            IEnumerable<string> left,
            IEnumerable<string> right)
        {
            return new HashSet<string>(
                    (left ?? Enumerable.Empty<string>())
                        .Where(value => !string.IsNullOrWhiteSpace(value))
                        .Select(value => value.Trim()),
                    StringComparer.OrdinalIgnoreCase)
                .SetEquals(
                    (right ?? Enumerable.Empty<string>())
                        .Where(value => !string.IsNullOrWhiteSpace(value))
                        .Select(value => value.Trim()));
        }


        private static List<string> NormalizeResourceKeys(
            IEnumerable<string> keys)
        {
            return (keys ?? Enumerable.Empty<string>())
                .Where(key => !string.IsNullOrWhiteSpace(key))
                .Select(key => key.Trim().Replace('\\', '/'))
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList();
        }


        private static List<string> NormalizeResourceKeysForContract(
            string request,
            IEnumerable<string> keys)
        {
            List<string> normalized =
                NormalizeResourceKeys(keys);

            if (!ExplicitlyProhibitsSceneMutation(request))
                return normalized;

            // A structured mutation declaration cannot override an explicit
            // negative contract such as "Do NOT edit the scene file". Keeping
            // that stale scene ownership introduces false sibling conflicts
            // and can manufacture dependency cycles during reconciliation.
            return normalized
                .Where(
                    key =>
                        !key.StartsWith(
                            "scene:",
                            StringComparison.OrdinalIgnoreCase))
                .ToList();
        }


        private static bool ExplicitlyProhibitsSceneMutation(
            string request)
        {
            return Regex.IsMatch(
                request ?? string.Empty,
                @"\b(?:(?:must|should|shall|do|does|did|will|can)\s+not|never)\s+(?:edit|modify|mutate|touch|create|build|save|author|configure|change)\b[^.;\r\n]{0,140}\bscene\b|\bwithout\s+(?:editing|modifying|mutating|touching|creating|building|saving|authoring|configuring|changing)\b[^.;\r\n]{0,140}\bscene\b",
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);
        }


        private CreateKanbanChildTaskResult BuildRejectedChildResult(
            KanbanTask parent,
            CreateKanbanChildTaskArguments arguments,
            string error)
        {
            return new CreateKanbanChildTaskResult
            {
                Succeeded = false,
                Error = error,
                ParentTaskId = parent?.Id ?? string.Empty,
                ParentTitle = parent?.Title ?? string.Empty,
                ChildTitle = arguments?.Title?.Trim() ?? string.Empty,
                ChildStatus = string.Empty,
                AssignedAgent =
                    string.IsNullOrWhiteSpace(arguments?.AssignedAgent)
                        ? parent?.AssignedAgent ?? string.Empty
                        : arguments.AssignedAgent.Trim(),
                DirectChildCount =
                    parent == null
                        ? 0
                        : GetDirectChildren(parent).Count,
                ResponseInstruction =
                    "No child was created. Correct only the reported field and retry create_kanban_child_task once; previously created siblings remain valid."
            };
        }


        private static bool MutatesSharedUnityScene(
            string request)
        {
            if (string.IsNullOrWhiteSpace(
                    request))
            {
                return false;
            }

            string normalizedRequest =
                System.Text.RegularExpressions.Regex.Replace(
                    request,
                    @"Depends\s+on\s+(?:the\s+)?(?:sibling\s+)?tasks?\s+.*?\s+being\s+Done\.?",
                    string.Empty,
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            const string mutationVerb =
                @"\b(?:create|add|attach|configure|position|move|build|author|set\s+up|save|wire|apply|assign|place|modify|update|edit|remove)\b";

            const string sceneTarget =
                @"\b(?:scene|GameObject|Camera|Light|Collider|Rigidbody|Renderer|MeshFilter|Transform|Player|Ground|Platform|Goal|prefab|component)\b";

            return System.Text.RegularExpressions.Regex.Split(
                    normalizedRequest,
                    @"(?<=[.!?])\s+|\r?\n",
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant)
                .Any(
                    sentence =>
                        SentenceMutatesSharedUnityScene(
                            sentence,
                            mutationVerb,
                            sceneTarget));
        }


        private static bool SentenceMutatesSharedUnityScene(
            string sentence,
            string mutationVerb,
            string sceneTarget)
        {
            sentence =
                RemoveNegatedSceneMutationPhrases(
                    sentence);

            if (string.IsNullOrWhiteSpace(sentence))
                return false;

            bool authorsStandaloneFile =
                Regex.IsMatch(
                    sentence,
                    @"\b(?:create|write|author|implement)\b[\s\S]{0,160}Assets(?:[\/][A-Za-z0-9_.-]+)+\.(?:mat|cs|asmdef)\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant);

            bool explicitlyWiresFileToScene =
                Regex.IsMatch(
                    sentence,
                    @"\b(?:attach|add|wire|configure|assign|apply|set|place|remove)\b[\s\S]{0,160}" + sceneTarget,
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant) ||
                Regex.IsMatch(
                    sentence,
                    sceneTarget + @"[\s\S]{0,160}\b(?:attach|add|wire|configure|assign|apply|set|place|remove)\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant);

            if (authorsStandaloneFile &&
                !explicitlyWiresFileToScene)
            {
                return false;
            }

            return Regex.IsMatch(
                       sentence,
                       mutationVerb + @"[\s\S]{0,160}" + sceneTarget,
                       RegexOptions.IgnoreCase |
                       RegexOptions.CultureInvariant) ||
                   Regex.IsMatch(
                       sentence,
                       sceneTarget + @"[\s\S]{0,160}" + mutationVerb,
                       RegexOptions.IgnoreCase |
                       RegexOptions.CultureInvariant);
        }


        private static string RemoveNegatedSceneMutationPhrases(
            string request)
        {
            string normalized =
                request ??
                string.Empty;

            normalized = Regex.Replace(
                normalized,
                @"\b(?:must|should|shall|do|does|did|will|can)\s+not\s+(?:attach|add|wire|configure|assign|apply|set|place|remove|mutate|modify|edit|create|build)\b[^.;\r\n]*?(?=\bbut\b|\bhowever\b|[.;\r\n]|$)",
                string.Empty,
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);

            return Regex.Replace(
                normalized,
                @"\bnever\s+(?:attach|add|wire|configure|assign|apply|set|place|remove|mutate|modify|edit|create|build)\b[^.;\r\n]*?(?=\bbut\b|\bhowever\b|[.;\r\n]|$)|\bwithout\s+(?:attaching|adding|wiring|configuring|assigning|applying|setting|placing|removing|mutating|modifying|editing|creating|building)\b[^.;\r\n]*?(?=\bbut\b|\bhowever\b|[.;\r\n]|$)",
                string.Empty,
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);
        }


        private static int CountIndependentSceneMutationClauses(
            string request)
        {
            const string mutationVerb =
                @"\b(?:open|create|construct|build|add|attach|configure|wire|assign|place|save|modify|edit|set)\b";

            const string sceneTarget =
                @"\b(?:scene|GameObject|Camera|Light|Collider|Rigidbody|Renderer|MeshFilter|Transform|Player|Ground|Platform|Goal|prefab|component|object)\b";

            string normalized =
                RemoveNegatedSceneMutationPhrases(
                    request ?? string.Empty);

            return Regex.Split(
                    normalized,
                    @"(?<=[.!?;])\s+|\r?\n|(?=\b(?:Then|Next|After that)\b)",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant)
                .SelectMany(
                    clause =>
                        Regex.Split(
                            clause,
                            @"\s+(?:and\s+then|then)\s+",
                            RegexOptions.IgnoreCase |
                            RegexOptions.CultureInvariant))
                .Count(
                    clause =>
                        SentenceMutatesSharedUnityScene(
                            clause,
                            mutationVerb,
                            sceneTarget));
        }


        private static bool OwnsSingleTransactionalSceneResource(
            KanbanTask task)
        {
            string[] resourceKeys =
                (task?.MutatedResourceKeys ?? new List<string>())
                    .Where(key => !string.IsNullOrWhiteSpace(key))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToArray();

            string[] producedScenes =
                (task?.ProducedArtifactPaths ?? new List<string>())
                    .Where(
                        path =>
                            !string.IsNullOrWhiteSpace(path) &&
                            path.EndsWith(
                                ".unity",
                                StringComparison.OrdinalIgnoreCase))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToArray();

            if (resourceKeys.Length != 1 ||
                !resourceKeys[0].StartsWith(
                    "scene:",
                    StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            string resourceScenePath =
                resourceKeys[0]
                    .Substring("scene:".Length)
                    .Replace('\\', '/');

            return producedScenes.Length == 0 ||
                   (producedScenes.Length == 1 &&
                    string.Equals(
                        producedScenes[0].Replace('\\', '/'),
                        resourceScenePath,
                        StringComparison.OrdinalIgnoreCase));
        }


        private static string ExtractExplicitUnityScenePath(
            string request)
        {
            System.Text.RegularExpressions.Match match =
                System.Text.RegularExpressions.Regex.Match(
                    request ??
                    string.Empty,
                    @"Assets(?:[\\/][A-Za-z0-9_.-]+)+\.unity\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            return match.Success
                ? match.Value.Replace('\\', '/')
                : null;
        }


        private string ExtractInheritedAssetScope(
            KanbanTask task)
        {
            HashSet<string> visited =
                new HashSet<string>(
                    StringComparer.Ordinal);

            KanbanTask current =
                task;

            while (current !=
                       null &&
                   visited.Add(
                       current.Id ??
                       string.Empty))
            {
                string declaredScope =
                    ExtractDeclaredAssetScope(
                        current);

                if (!string.IsNullOrWhiteSpace(declaredScope))
                    return declaredScope;

                string request =
                    string.Join(
                        "\n",
                        current.Description ?? string.Empty,
                        current.AcceptanceCriteria ?? string.Empty);

                System.Text.RegularExpressions.Match match =
                    System.Text.RegularExpressions.Regex.Match(
                        request,
                        @"\b(?:under|inside|within)\s+(?<path>Assets(?:[\\/][A-Za-z0-9_.-]+)+)",
                        System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                        System.Text.RegularExpressions.RegexOptions.CultureInvariant);

                if (match.Success)
                {
                    return match.Groups["path"]
                        .Value
                        .Replace('\\', '/')
                        .TrimEnd('/', '.', ',', ';', ':');
                }

                current =
                    FindTask(
                        current.ParentTaskId);
            }

            return null;
        }


        private static string ExtractDeclaredAssetScope(
            KanbanTask task)
        {
            if (task == null)
                return null;

            List<string> ownedPaths =
                (task.ProducedArtifactPaths ?? new List<string>())
                    .Concat(
                        (task.MutatedResourceKeys ?? new List<string>())
                            .Select(ExtractAssetPathFromResourceKey))
                    .Where(path => !string.IsNullOrWhiteSpace(path))
                    .Select(path => path.Trim().Replace('\\', '/'))
                    .Where(
                        path =>
                            path.StartsWith(
                                "Assets/",
                                StringComparison.OrdinalIgnoreCase))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

            if (ownedPaths.Count == 0)
                return null;

            List<string[]> directorySegments =
                ownedPaths
                    .Select(GetArtifactDirectorySegments)
                    .Where(segments => segments.Length > 0)
                    .ToList();

            if (directorySegments.Count == 0)
                return null;

            int commonSegmentCount =
                directorySegments.Min(segments => segments.Length);

            for (int index = 0; index < commonSegmentCount; index++)
            {
                string expected = directorySegments[0][index];

                if (directorySegments.Any(
                        segments =>
                            !string.Equals(
                                segments[index],
                                expected,
                                StringComparison.OrdinalIgnoreCase)))
                {
                    commonSegmentCount = index;
                    break;
                }
            }

            return commonSegmentCount > 1
                ? string.Join(
                    "/",
                    directorySegments[0].Take(commonSegmentCount))
                : null;
        }


        private static string ExtractAssetPathFromResourceKey(
            string resourceKey)
        {
            if (string.IsNullOrWhiteSpace(resourceKey))
                return null;

            System.Text.RegularExpressions.Match match =
                System.Text.RegularExpressions.Regex.Match(
                    resourceKey,
                    @"Assets(?:[\\/][A-Za-z0-9_.-]+)+",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            return match.Success
                ? match.Value
                : null;
        }


        private static string[] GetArtifactDirectorySegments(
            string artifactPath)
        {
            string normalized =
                (artifactPath ?? string.Empty)
                    .Trim()
                    .Replace('\\', '/')
                    .TrimEnd('/');

            int lastSlash = normalized.LastIndexOf('/');

            if (lastSlash <= 0)
                return Array.Empty<string>();

            string finalSegment = normalized.Substring(lastSlash + 1);
            string directory =
                finalSegment.IndexOf('.') >= 0
                    ? normalized.Substring(0, lastSlash)
                    : normalized;

            return directory
                .Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
        }


        private KanbanTask FindConflictingChildTaskParentReference(
            KanbanTask parent,
            CreateKanbanChildTaskArguments arguments)
        {
            string verificationText =
                (arguments.Description ?? string.Empty) +
                "\n" +
                (arguments.AcceptanceCriteria ?? string.Empty);


            if (verificationText.IndexOf(
                    "attach",
                    StringComparison.OrdinalIgnoreCase) <
                0)
            {
                return null;
            }


            KanbanTask conflictingTask =
                _board.Tasks.FirstOrDefault(
                    task =>
                        task != null &&
                        !string.Equals(
                            task.Id,
                            parent.Id,
                            StringComparison.Ordinal) &&
                        !string.IsNullOrWhiteSpace(
                            task.Id) &&
                        verificationText.IndexOf(
                            task.Id,
                            StringComparison.Ordinal) >=
                        0);


            return conflictingTask;
        }


        private object GetAgentTaskContext(
            string taskId)
        {
            KanbanTask task =
                FindTask(
                    taskId);

            if (task == null)
            {
                throw new InvalidOperationException(
                    "The currently executing Kanban task no longer exists.");
            }

            KanbanTask parent =
                FindTask(
                    task.ParentTaskId);

            List<object> ancestorContracts =
                new List<object>();

            HashSet<string> visitedAncestors =
                new HashSet<string>(
                    StringComparer.Ordinal);

            KanbanTask ancestor =
                parent;

            while (ancestor !=
                       null &&
                   visitedAncestors.Add(
                       ancestor.Id ??
                       string.Empty))
            {
                ancestorContracts.Insert(
                    0,
                    new
                    {
                        TaskId = ancestor.Id,
                        ancestor.Title,
                        Description =
                            ancestor.Description ??
                            string.Empty,
                        AcceptanceCriteria =
                            ancestor.AcceptanceCriteria ??
                            string.Empty,
                        ParentTaskId =
                            ancestor.ParentTaskId ??
                            string.Empty
                    });

                ancestor =
                    FindTask(
                        ancestor.ParentTaskId);
            }

            int incompleteDescendantCount =
                CountIncompleteDescendants(
                    task,
                    new HashSet<string>(
                        StringComparer.Ordinal));

            int directChildCount =
                GetDirectChildren(task).Count;

            object[] directChildren =
                GetDirectChildren(task)
                    .Select(
                        child =>
                            (object)new
                            {
                                TaskId = child.Id,
                                child.Title,
                                Description =
                                    child.Description ??
                                    string.Empty,
                                AcceptanceCriteria =
                                    child.AcceptanceCriteria ??
                                    string.Empty,
                                child.ParentTaskId,
                                DependsOnTaskIds =
                                    GetAuthoritativeDependencyTaskIds(child),
                                ProducedArtifactPaths =
                                    child.ProducedArtifactPaths ??
                                    new List<string>(),
                                ConsumedArtifactPaths =
                                    child.ConsumedArtifactPaths ??
                                    new List<string>(),
                                MutatedResourceKeys =
                                    child.MutatedResourceKeys ??
                                    new List<string>(),
                                AssociatedTaskIds =
                                    child.AssociatedTaskIds ??
                                    new List<string>(),
                                DeclaredDependencyTitles =
                                    child.DeclaredDependencyTitles ??
                                    new List<string>(),
                                UnresolvedDependencyTitles =
                                    GetUnresolvedDependencyTitles(child),
                                CreationSource =
                                    child.CreationSource ??
                                    string.Empty,
                                child.IsDecompositionDraft,
                                CreatedStatus =
                                    child.CreatedStatus ??
                                    string.Empty,
                                Status = GetStatusLabel(child.Status),
                                ReviewStatus =
                                    child.ReviewStatus ??
                                    string.Empty,
                                ReviewFailureKind =
                                    child.ReviewFailureKind ??
                                    string.Empty,
                                HasReviewEvidence =
                                    !string.IsNullOrWhiteSpace(
                                        child.ReviewEvidence),
                                child.AssignedAgent,
                                child.AgentAssignmentInherited,
                                AssignmentMatchesParent =
                                    string.Equals(
                                        child.AssignedAgent,
                                        task.AssignedAgent,
                                        StringComparison.Ordinal)
                            })
                    .ToArray();

            object[] siblingContracts =
                parent == null
                    ? Array.Empty<object>()
                    : GetDirectChildren(parent)
                        .Where(
                            sibling =>
                                sibling != null &&
                                !string.Equals(
                                    sibling.Id,
                                    task.Id,
                                    StringComparison.Ordinal))
                        .Select(
                            sibling =>
                                (object)new
                                {
                                    TaskId = sibling.Id,
                                    sibling.Title,
                                    Status = GetStatusLabel(sibling.Status),
                                    ReviewStatus = sibling.ReviewStatus ?? string.Empty,
                                    Description = sibling.Description ?? string.Empty,
                                    AcceptanceCriteria = sibling.AcceptanceCriteria ?? string.Empty,
                                    ProducedArtifactPaths =
                                        sibling.ProducedArtifactPaths ??
                                        new List<string>(),
                                    ConsumedArtifactPaths =
                                        sibling.ConsumedArtifactPaths ??
                                        new List<string>()
                                })
                        .ToArray();

            string descendantGateState =
                incompleteDescendantCount >
                    0
                    ? "BlockedInToDo"
                    : directChildCount >
                        0
                        ? "ReleasedAllDescendantsDone"
                        : "NoDescendants";

            return new
            {
                CardMemory = MemoryContext(task),
                DependencyHandoffs = MemoryDependencyHandoffs(task),
                TaskId = task.Id,
                task.Title,
                Status = GetStatusLabel(task.Status),
                WorkflowPhase = GetWorkflowPhase(task),
                PlanRevision = GetTaskRoot(task)?.PlanRevision ?? 0,
                task.PlannedRevision,
                PlanningKickbackReason =
                    GetTaskRoot(task)?.PlanningKickbackReason ?? string.Empty,
                task.AssignedAgent,
                ActiveAgentIds =
                    _activeKanbanAgentIds
                        .OrderBy(
                            agentId =>
                                agentId,
                            StringComparer.Ordinal)
                        .ToArray(),
                task.ParentTaskId,
                ProducedArtifactPaths =
                    task.ProducedArtifactPaths ??
                    new List<string>(),
                ConsumedArtifactPaths =
                    task.ConsumedArtifactPaths ??
                    new List<string>(),
                EvidenceScope = AIRuntimeKanbanAgentRunner.IsSourceArtifactEvidenceScope(
                    task.ProducedArtifactPaths, task.ConsumedArtifactPaths, task.MutatedResourceKeys) ? "SourceArtifacts" : "DeclaredArtifactsAndResources",
                CompletedDependencyArtifactPaths = GetCompletedDependencyArtifactPaths(task),
                MutatedResourceKeys =
                    task.MutatedResourceKeys ??
                    new List<string>(),
                AssociatedTaskIds =
                    task.AssociatedTaskIds ??
                    new List<string>(),
                AssociationResolvedAsAtomicLeaf =
                    task.SuppressAutoDecomposition,
                ParentTitle = parent?.Title ?? string.Empty,
                ParentAssignedAgent = parent?.AssignedAgent ?? string.Empty,
                AncestorContracts = ancestorContracts.ToArray(),
                SiblingContracts = siblingContracts,
                IsChildTask = parent != null,
                CreationSource = task.CreationSource ?? string.Empty,
                CreatedStatus = task.CreatedStatus ?? string.Empty,
                task.AgentAssignmentInherited,
                AssignmentMatchesParent =
                    parent != null &&
                    string.Equals(
                        task.AssignedAgent,
                        parent.AssignedAgent,
                        StringComparison.Ordinal),
                DirectChildCount = directChildCount,
                DirectChildren = directChildren,
                ParentDirectChildCount =
                    parent == null
                        ? 0
                        : GetDirectChildren(parent).Count,
                IncompleteDescendantCount = incompleteDescendantCount,
                HasIncompleteDescendants =
                    incompleteDescendantCount >
                    0,
                AllDescendantsDone =
                    directChildCount >
                        0 &&
                    incompleteDescendantCount ==
                        0,
                CoordinationReviewRollup =
                    BuildCoordinationReviewRollup(task),
                DescendantGateState = descendantGateState,
                ResponseInstruction =
                    "This is authoritative scoped Kanban metadata. WorkflowPhase is the project-wide execution barrier: only Planning may mutate DirectChildren, Implementation performs approved card contracts, and FinalReview inspects completed implementation. PlanRevision identifies the approved graph version and PlannedRevision identifies the version governing this card. ActiveAgentIds is the complete routing roster for this run; omit AssignedAgent to inherit the current agent when no better active specialist exists. AncestorContracts contains inherited integration invariants, while SiblingContracts identifies which other approved cards own the remaining parent outcomes. Do not assign unfinished sibling-owned behavior to this card unless its own Description or AcceptanceCriteria explicitly requires that behavior. DirectChildren contains authoritative creation and attachment evidence for children spawned by this executing card. BlockedInToDo means unfinished descendants currently block the card. ReleasedAllDescendantsDone means every descendant reached Done and the card is correctly eligible to advance beyond To Do."
            };
        }


        private object BuildCoordinationReviewRollup(
            KanbanTask task)
        {
            List<KanbanTask> descendants =
                GetAllDescendants(task);

            List<KanbanTask> finalVerifiers =
                descendants
                    .Where(IsFinalIntegrationVerificationTask)
                    .ToList();

            return new
            {
                DescendantCount = descendants.Count,
                AllDescendantsDoneAndPassed =
                    descendants.Count > 0 &&
                    descendants.All(
                        descendant =>
                            descendant.Status == TaskStatus.Done &&
                            string.Equals(
                                descendant.ReviewStatus,
                                "Passed",
                                StringComparison.Ordinal)),
                FinalVerificationCount = finalVerifiers.Count,
                FinalVerifications =
                    finalVerifiers.Select(
                        verifier =>
                            (object)new
                            {
                                TaskId = verifier.Id,
                                verifier.Title,
                                Status = GetStatusLabel(verifier.Status),
                                ReviewStatus = verifier.ReviewStatus ?? string.Empty,
                                CoversParentAcceptanceCriteria =
                                    CoversParentAcceptanceCriteria(
                                        task,
                                        verifier),
                                ReviewEvidence = verifier.ReviewEvidence ?? string.Empty,
                                ReviewResult = verifier.ReviewResult ?? string.Empty
                            })
                        .ToArray(),
                ResponseInstruction =
                    "For a coordination parent, descendant Passed statuses and the designated final-verification raw ReviewEvidence form the authoritative evidence roll-up. Do not demand a second independent component inspection when every descendant passed and a final verifier explicitly covers the parent acceptance criteria."
            };
        }


        private bool TryBuildCoordinationParentReviewPass(
            KanbanTask task,
            out string result,
            out string evidence)
        {
            result = null;
            evidence = null;

            List<KanbanTask> descendants =
                GetAllDescendants(task);

            if (descendants.Count == 0 ||
                descendants.Any(
                    descendant =>
                        descendant.Status != TaskStatus.Done ||
                        !string.Equals(
                            descendant.ReviewStatus,
                            "Passed",
                            StringComparison.Ordinal)))
            {
                return false;
            }

            KanbanTask finalVerifier =
                descendants
                    .Where(IsFinalIntegrationVerificationTask)
                    .Where(
                        verifier =>
                            CoversParentAcceptanceCriteria(task, verifier))
                    .OrderByDescending(verifier => verifier.UpdatedAt)
                    .FirstOrDefault();

            if (finalVerifier == null ||
                !HasPersistedFinalVerificationEvidence(finalVerifier))
            {
                return false;
            }

            evidence =
                Newtonsoft.Json.JsonConvert.SerializeObject(
                    BuildCoordinationReviewRollup(task),
                    Newtonsoft.Json.Formatting.Indented);

            result =
                "KANBAN_REVIEW: PASS\n\n" +
                $"AIRuntime deterministically approved this coordination parent because all {descendants.Count} descendants are Done with Passed reviews and designated final verifier '{finalVerifier.Title}' explicitly covers the parent acceptance criteria.\n\n" +
                "Evidence checked:\n" +
                "- Authoritative descendant statuses and review verdicts.\n" +
                "- The designated final verifier's persisted raw ReviewEvidence bundle.\n" +
                "- Explicit final-verifier coverage of the parent acceptance criteria.\n\n" +
                "Missing or incorrect work:\n- None.";

            return true;
        }


        private static bool HasPersistedFinalVerificationEvidence(
            KanbanTask verifier)
        {
            if (verifier == null)
                return false;

            if (!string.IsNullOrWhiteSpace(verifier.ReviewEvidence))
                return true;

            // Legacy boards predate the structured ReviewEvidence field. A
            // passed final verifier may still carry its complete authoritative
            // inspection transcript in ReviewResult; accept that once so the
            // board can migrate instead of stranding an already verified run.
            string legacy =
                verifier.ReviewResult ??
                string.Empty;

            return HasReviewVerdict(legacy, "PASS") &&
                   legacy.IndexOf(
                       "Evidence checked",
                       StringComparison.OrdinalIgnoreCase) >= 0 &&
                   (legacy.IndexOf(
                        "inspect_editor_component",
                        StringComparison.OrdinalIgnoreCase) >= 0 ||
                    legacy.IndexOf(
                        "inspect_scene",
                        StringComparison.OrdinalIgnoreCase) >= 0);
        }


        private List<KanbanTask> GetAllDescendants(
            KanbanTask parent)
        {
            List<KanbanTask> descendants =
                new List<KanbanTask>();

            CollectDescendants(
                parent,
                descendants,
                new HashSet<string>(StringComparer.Ordinal));

            return descendants;
        }


        private void CollectDescendants(
            KanbanTask parent,
            ICollection<KanbanTask> descendants,
            ISet<string> visited)
        {
            if (parent == null ||
                !visited.Add(parent.Id ?? string.Empty))
            {
                return;
            }

            foreach (KanbanTask child in GetDirectChildren(parent))
            {
                if (child == null)
                    continue;

                descendants.Add(child);
                CollectDescendants(child, descendants, visited);
            }
        }


        private static bool IsFinalIntegrationVerificationTask(
            KanbanTask task)
        {
            // A dependency-producing child often refers to the downstream
            // final verifier in its description (for example, "the final
            // integration-verification child will aggregate this output").
            // Treating that reference as the child's own role makes the first
            // producer look like the graph sink and creates a producer <->
            // verifier cycle. Role identity must therefore come from the
            // card's title, while CoversParentAcceptanceCriteria separately
            // evaluates the full verification contract.
            string title =
                task?.Title?.Trim() ??
                string.Empty;

            return Regex.IsMatch(
                       title,
                       @"^\s*final\b.{0,80}\b(?:integration|integrated|verification|verify|validation|review|report|smoke\s+test)\b|^\s*(?:integration|integrated)\b.{0,80}\b(?:verification|verify|validation|review|report|smoke\s+test)\b",
                       RegexOptions.IgnoreCase |
                       RegexOptions.CultureInvariant) ||
                   Regex.IsMatch(
                       title,
                       @"\b(?:verify|verification|validate|validation|review)\b.{0,80}\b(?:all|every)\b.{0,40}\b(?:parent\s+)?acceptance\s+criteria\b",
                       RegexOptions.IgnoreCase |
                       RegexOptions.CultureInvariant);
        }


        private static bool CoversParentAcceptanceCriteria(
            KanbanTask parent,
            KanbanTask verifier)
        {
            if (parent == null || verifier == null)
                return false;

            string contract =
                string.Join(
                    "\n",
                    verifier.Description ?? string.Empty,
                    verifier.AcceptanceCriteria ?? string.Empty);

            if (contract.IndexOf(
                    "all acceptance criteria from the parent",
                    StringComparison.OrdinalIgnoreCase) >= 0 ||
                contract.IndexOf(
                    "every acceptance criterion from the parent",
                    StringComparison.OrdinalIgnoreCase) >= 0)
            {
                return true;
            }

            string[] parentCriteria =
                (parent.AcceptanceCriteria ?? string.Empty)
                    .Split(
                        new[] { "\r\n", "\n", "\r" },
                        StringSplitOptions.RemoveEmptyEntries)
                    .Select(value => value.Trim())
                    .Where(value => value.Length >= 12)
                    .ToArray();

            return parentCriteria.Length > 0 &&
                   parentCriteria.All(
                       criterion =>
                           contract.IndexOf(
                               criterion,
                               StringComparison.OrdinalIgnoreCase) >= 0);
        }


        private static bool TryExtractPlanningKickback(
            string result,
            out string reason)
        {
            reason = string.Empty;

            if (string.IsNullOrWhiteSpace(result))
                return false;

            Match match = Regex.Match(
                result,
                @"\[(?:PlanningKickback|RequiresReplanning)\]\s*(?<reason>[\s\S]+)",
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);

            if (!match.Success)
                return false;

            reason = match.Groups["reason"].Value.Trim();
            return !string.IsNullOrWhiteSpace(reason);
        }


        private static bool TryGetOwnedArtifactRemediation(
            KanbanTask task,
            string reason,
            out string artifactPath)
        {
            artifactPath = string.Empty;

            if (task == null ||
                string.IsNullOrWhiteSpace(reason))
            {
                return false;
            }

            foreach (
                string path
                in (task.ProducedArtifactPaths ?? new List<string>())
                    .Where(value => !string.IsNullOrWhiteSpace(value)))
            {
                string normalizedPath =
                    path.Trim().Replace('\\', '/');

                string fileName =
                    Path.GetFileName(normalizedPath);

                string artifactName =
                    Path.GetFileNameWithoutExtension(normalizedPath);

                if (string.IsNullOrWhiteSpace(artifactName))
                    continue;

                string artifactReference =
                    @"\b" +
                    Regex.Escape(artifactName) +
                    @"(?:" +
                    Regex.Escape(
                        Path.GetExtension(fileName)) +
                    @")?\b";

                bool forwardRemediation =
                    Regex.IsMatch(
                        reason,
                        artifactReference +
                        @"[\s\S]{0,160}\b(?:should|must|needs?\s+to)\b[\s\S]{0,100}\b(?:add|include|implement|provide|expose|modify|update|create|configure|set|use|call|remove|rename)\b",
                        RegexOptions.IgnoreCase |
                        RegexOptions.CultureInvariant);

                bool reverseRemediation =
                    Regex.IsMatch(
                        reason,
                        @"\b(?:add|include|implement|provide|expose|modify|update|create|configure|set|use|call|remove|rename)\b[\s\S]{0,120}" +
                        artifactReference,
                        RegexOptions.IgnoreCase |
                        RegexOptions.CultureInvariant);

                if (!forwardRemediation &&
                    !reverseRemediation)
                {
                    continue;
                }

                artifactPath = normalizedPath;
                return true;
            }

            return false;
        }

        // Contradictory transport success is not acceptance. It should reach
        // artifact checks and Review, rather than rewrite the approved graph.
        private static bool IsCompletionOnlyPlanningKickback(string reason)
        {
            return !string.IsNullOrWhiteSpace(reason) && Regex.IsMatch(reason,
                @"\bAll acceptance criteria (?:are|have been) met\b|\bNo (?:further work|PlanningKickback) is needed\b|\bNo further work is needed\b",
                RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
        }


        private static bool TryExtractCapabilityRequest(
            string result,
            out string request)
        {
            request = string.Empty;

            if (string.IsNullOrWhiteSpace(result))
                return false;

            Match match = Regex.Match(
                result,
                @"\[CapabilityRequest\]\s*(?<request>[\s\S]+)",
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);

            if (!match.Success)
                return false;

            request = match.Groups["request"].Value.Trim();
            return !string.IsNullOrWhiteSpace(request);
        }


        private static bool TryGetMissingProducedArtifact(
            KanbanTask task,
            out string missingArtifact)
        {
            missingArtifact = string.Empty;

            string workspaceRoot =
                Directory.GetParent(Application.dataPath)?.FullName;

            if (task == null || string.IsNullOrWhiteSpace(workspaceRoot))
                return false;

            string fullRoot =
                Path.GetFullPath(workspaceRoot)
                    .TrimEnd(
                        Path.DirectorySeparatorChar,
                        Path.AltDirectorySeparatorChar) +
                Path.DirectorySeparatorChar;

            foreach (string artifact in
                     (task.ProducedArtifactPaths ?? new List<string>())
                         .Where(path => !string.IsNullOrWhiteSpace(path)))
            {
                string normalized = artifact.Trim().Replace('\\', '/');
                string fullPath = Path.GetFullPath(
                    Path.Combine(
                        fullRoot,
                        normalized.Replace('/', Path.DirectorySeparatorChar)));

                if (!fullPath.StartsWith(
                        fullRoot,
                        StringComparison.OrdinalIgnoreCase) ||
                    (!File.Exists(fullPath) && !Directory.Exists(fullPath)))
                {
                    missingArtifact = normalized;
                    return true;
                }

                if (File.Exists(fullPath) &&
                    TryGetInvalidUnityArtifactReason(
                        task,
                        normalized,
                        out string invalidReason))
                {
                    missingArtifact =
                        normalized + " (" + invalidReason + ")";
                    return true;
                }
            }

            return false;
        }


        private static bool TryApplyExplicitArtifactRecovery(
            KanbanTask task,
            string result,
            string error,
            out string missingArtifact)
        {
            missingArtifact = string.Empty;

            if (task == null ||
                string.IsNullOrWhiteSpace(error) ||
                error.IndexOf(
                    "The agent claimed completion, but these explicit asset deliverables do not exist:",
                    StringComparison.OrdinalIgnoreCase) < 0 ||
                (task.NextAgentMessage ?? string.Empty).StartsWith(
                    ExplicitArtifactRecoveryPrefix,
                    StringComparison.Ordinal) ||
                !TryGetMissingProducedArtifact(
                    task,
                    out missingArtifact))
            {
                return false;
            }

            task.Status = TaskStatus.Todo;
            task.AgentResult =
                !string.IsNullOrWhiteSpace(result)
                    ? result
                    : task.AgentResult ?? string.Empty;
            task.AgentError = string.Empty;
            task.ReviewStatus = "Rework Requested";
            task.ReviewFailureKind = "ImplementationDefect";
            task.ReviewResult = error;
            task.ReviewEvidence = string.Empty;
            task.ReviewStartedAt = string.Empty;
            task.ReviewFinishedAt = string.Empty;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = missingArtifact;
            task.NextAgentMessage =
                ExplicitArtifactRecoveryPrefix +
                " The prior implementation run claimed completion without creating its exact declared output '" +
                missingArtifact +
                "'. Preserve existing valid artifacts and execution memory. Create and validate this exact path, then audit every declared ProducedArtifactPath before completing. A semantically similar asset at another path is not a substitute.";

            return true;
        }


        private static bool TryGetInvalidUnityArtifactReason(
            KanbanTask task,
            string assetPath,
            out string reason)
        {
            reason = string.Empty;

            if (string.IsNullOrWhiteSpace(assetPath) ||
                !assetPath.StartsWith(
                    "Assets/",
                    StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            string extension =
                Path.GetExtension(assetPath).ToLowerInvariant();

            if (extension != ".prefab" &&
                extension != ".mat" &&
                extension != ".unity")
            {
                return false;
            }

            AssetDatabase.ImportAsset(
                assetPath,
                ImportAssetOptions.ForceSynchronousImport);

            if (extension == ".prefab")
            {
                GameObject prefab =
                    AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);

                if (prefab == null ||
                    PrefabUtility.GetPrefabAssetType(prefab) ==
                    PrefabAssetType.NotAPrefab)
                {
                    reason = "not a valid Unity prefab asset";
                    return true;
                }

                return false;
            }

            if (extension == ".mat")
            {
                if (AssetDatabase.LoadAssetAtPath<Material>(assetPath) == null)
                {
                    reason = "not a valid Unity material asset";
                    return true;
                }

                return false;
            }

            if (AssetDatabase.LoadAssetAtPath<SceneAsset>(assetPath) == null)
            {
                reason = "not a valid Unity scene asset";
                return true;
            }

            UnityEngine.SceneManagement.Scene loadedScene =
                EditorSceneAuthoringUtility.FindLoadedScene(
                    assetPath);

            if (loadedScene.IsValid() &&
                loadedScene.isLoaded &&
                loadedScene.isDirty)
            {
                reason =
                    "loaded scene has unsaved changes; call save_editor_scene and verify DirtyAfterSave=false before completion";
                return true;
            }

            string taskContract = string.Join(
                "\n",
                task?.Title ?? string.Empty,
                task?.Description ?? string.Empty,
                task?.AcceptanceCriteria ?? string.Empty);

            if (Regex.IsMatch(
                    taskContract,
                    @"\b(?:empty|blank)\s+scene\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return false;
            }

            UnityEngine.SceneManagement.Scene scene =
                UnityEngine.SceneManagement.SceneManager.GetSceneByPath(
                    assetPath);
            bool closeAfterInspection = !scene.IsValid() || !scene.isLoaded;

            try
            {
                if (closeAfterInspection)
                {
                    scene =
                        UnityEditor.SceneManagement.EditorSceneManager.OpenScene(
                            assetPath,
                            UnityEditor.SceneManagement.OpenSceneMode.Additive);
                }

                if (!scene.IsValid() ||
                    scene.GetRootGameObjects().Length == 0)
                {
                    reason = "scene contains no persisted root GameObjects";
                    return true;
                }
            }
            catch (Exception exception)
            {
                reason =
                    "scene could not be inspected: " + exception.Message;
                return true;
            }
            finally
            {
                if (closeAfterInspection && scene.IsValid())
                {
                    UnityEditor.SceneManagement.EditorSceneManager.CloseScene(
                        scene,
                        removeScene: true);
                }
            }

            return false;
        }


        private IReadOnlyCollection<string> GetCompletedDependencyArtifactPaths(KanbanTask task)
        {
            var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var visited = new HashSet<string>(StringComparer.Ordinal);
            var pending = new Queue<string>(task?.DependsOnTaskIds ?? new List<string>());
            while (pending.Count > 0)
            {
                string id = pending.Dequeue();
                if (!visited.Add(id)) continue;
                KanbanTask dependency = FindTask(id);
                if (dependency == null || dependency.Status != TaskStatus.Done ||
                    !string.Equals(dependency.ParentTaskId, task.ParentTaskId, StringComparison.Ordinal))
                    continue;
                foreach (string path in dependency.ProducedArtifactPaths ?? new List<string>())
                {
                    // Directory scaffolding never grants recursive read authority.
                    if (!string.IsNullOrWhiteSpace(path) && Path.HasExtension(path))
                        paths.Add(path.Trim().Replace('\\', '/'));
                }
                foreach (string upstream in dependency.DependsOnTaskIds ?? new List<string>())
                    pending.Enqueue(upstream);
            }
            return paths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToArray();
        }


        private string ComputePlanFingerprint(
            KanbanTask root)
        {
            return ComputePlanningCardsFingerprint(GetDirectChildren(root)
                .Where(task => !task.IsDecompositionDraft));
        }


        private static string ComputePlanningCardsFingerprint(IEnumerable<KanbanTask> cards)
        {
            string canonical = string.Join(
                "\n---\n",
                cards
                    .OrderBy(task => task.Title, StringComparer.OrdinalIgnoreCase)
                    .ThenBy(task => task.Id, StringComparer.Ordinal)
                    .Select(
                        task => string.Join(
                            "\n",
                            task.Title ?? string.Empty,
                            task.Description ?? string.Empty,
                            task.AcceptanceCriteria ?? string.Empty,
                            string.Join("|", task.ProducedArtifactPaths ?? new List<string>()),
                            string.Join("|", task.ConsumedArtifactPaths ?? new List<string>()),
                            string.Join("|", task.MutatedResourceKeys ?? new List<string>()),
                            string.Join("|", task.DependsOnTaskIds ?? new List<string>()))));

            return ComputeStableFingerprint(canonical);
        }


        private string FinalizePlanningPhase(
            KanbanTask root)
        {
            if (!IsPlanningCoordinator(root))
                return string.Empty;

            string readinessError =
                ValidatePlanningReadiness(root);

            if (!string.IsNullOrWhiteSpace(readinessError))
                return readinessError;

            string fingerprint = ComputePlanFingerprint(root);

            if (!string.IsNullOrWhiteSpace(root.PlanningKickbackReason) &&
                string.Equals(
                    fingerprint,
                    root.PlanningKickbackFingerprint,
                    StringComparison.Ordinal))
            {
                return
                    "Planning validation made no structural progress: the approved task contracts, ownership, and dependencies are unchanged from the plan that was kicked back. Revise the affected card contract or graph before implementation can resume.";
            }

            root.PlanRevision = Math.Max(0, root.PlanRevision) + 1;
            root.WorkflowPhase = WorkflowPhaseImplementation;
            root.PlanningKickbackReason = string.Empty;
            root.PlanningKickbackFingerprint = string.Empty;
            root.BlockedReason = string.Empty;

            foreach (KanbanTask child in GetAllDescendants(root))
            {
                if (!child.IsDecompositionDraft)
                    child.PlannedRevision = root.PlanRevision;
            }

            Debug.Log(
                $"[Kanban] Project '{root.Title}' approved plan revision {root.PlanRevision} and entered Implementation.");
            return string.Empty;
        }


        private string ValidatePlanningReadiness(
            KanbanTask root)
        {
            return ValidatePlanningChildren(root, GetDirectChildren(root));
        }


        private string ValidatePlanningProposal(KanbanTask root, string attemptId)
        {
            if (string.IsNullOrWhiteSpace(attemptId))
                return "Planning validation requires an active planning attempt.";

            // Resolve against detached candidate cards: an omitted sibling must
            // neither satisfy a forward reference nor be deleted on rejection.
            List<KanbanTask> candidates = GetDirectChildren(root)
                .Where(child => child.CreationSource != "create_kanban_child_task" ||
                    string.Equals(child.ProposedInPlanningAttemptId, attemptId, StringComparison.Ordinal))
                .Select(child => JsonUtility.FromJson<KanbanTask>(JsonUtility.ToJson(child)))
                .OrderBy(GetTaskCreatedAtSortValue)
                .ThenBy(child => child.Id, StringComparer.Ordinal)
                .ToList();
            RepairIncompleteDraftOwnership(candidates);
            ResolveSiblingDependencies(candidates, root.Id);
            string error = ValidatePlanningChildren(root, candidates);
            if (!string.IsNullOrWhiteSpace(error))
            {
                var omitted = GetDirectChildren(root)
                    .Where(child => candidates.All(candidate => candidate.Id != child.Id))
                    .Select(child => child.Title).ToArray();
                return omitted.Length == 0 ? error : error +
                    " Existing cards omitted from this planning attempt: " + string.Join(", ", omitted) +
                    ". Restate every desired card with create_kanban_child_task before committing. " +
                    "Existing board cards do not automatically belong to the current authoritative snapshot; reading memory does not restate them.";
            }
            if (!string.IsNullOrWhiteSpace(root.PlanningKickbackReason) &&
                string.Equals(ComputePlanningCardsFingerprint(candidates),
                    root.PlanningKickbackFingerprint, StringComparison.Ordinal))
                return "Planning validation made no structural progress: revise the affected card contract or graph before implementation can resume.";
            return string.Empty;
        }


        private string ValidatePlanningChildren(KanbanTask root, List<KanbanTask> directChildren)
        {
            if (!IsPlanningCoordinator(root))
                return string.Empty;

            if (directChildren.Count == 0)
            {
                return
                    "Planning validation requires at least one execution-ready implementation or verification card before Implementation can begin.";
            }

            KanbanTask nested =
                directChildren
                    .SelectMany(GetAllDescendants)
                    .FirstOrDefault();

            if (nested != null)
            {
                return
                    $"Planning validation requires a flat execution graph, but '{nested.Title}' is nested below another implementation card. Move every executable leaf directly under the project root.";
            }

            foreach (KanbanTask child in directChildren)
            {
                if (string.IsNullOrWhiteSpace(child.Description) ||
                    string.IsNullOrWhiteSpace(child.AcceptanceCriteria))
                {
                    return
                        $"Planning validation found an incomplete contract on '{child.Title}'. Every implementation card requires a complete description and acceptance criteria.";
                }

                string outputContractError = GetMissingExplicitOutputContract(child);
                if (!string.IsNullOrWhiteSpace(outputContractError)) return outputContractError;

                var unresolvedTitles = (child.DeclaredDependencyTitles ?? new List<string>())
                    .Where(title => directChildren.Count(sibling => string.Equals(
                        sibling.Title?.Trim(), title, StringComparison.OrdinalIgnoreCase)) != 1)
                    .ToArray();
                if (unresolvedTitles.Length > 0)
                {
                    return
                        $"Planning validation found unresolved structured dependencies on '{child.Title}': " +
                        string.Join(
                            ", ",
                            unresolvedTitles) +
                        ". Correct the exact sibling titles before Implementation begins.";
                }

                // AIRuntime validates graph integrity and explicit contracts;
                // it does not infer strategy from keyword or artifact counts.
                // A cohesive outcome may legitimately own several files.
            }

            return ValidatePlanningGraphContracts(
                directChildren);
        }


        // Detect explicit output instructions, without granting authority from prose.
        // Read-only verification and coordination cards may legitimately have no outputs.
        private static string GetMissingExplicitOutputContract(KanbanTask task)
        {
            string request = (task.Description ?? "") + "\n" + (task.AcceptanceCriteria ?? "");
            foreach (Match match in Regex.Matches(request,
                @"(?im)^\s*(?:[-*]\s*)?(?:create|write|save|place|implement|update|modify)\b[^\r\n]*?(?<path>Assets/[^\s""'`<>]+\.[a-zA-Z0-9]+)"))
            {
                string path = match.Groups["path"].Value.TrimEnd('.', ',', ';', ')', ']');
                if (NormalizeArtifactPaths(task.ProducedArtifactPaths).Contains(path, StringComparer.OrdinalIgnoreCase))
                    continue;
                if (path.EndsWith(".unity", StringComparison.OrdinalIgnoreCase) &&
                    (task.MutatedResourceKeys ?? new List<string>()).Contains("scene:" + path, StringComparer.OrdinalIgnoreCase))
                    continue;
                return $"[MissingArtifactOwnership] '{task.Title}' explicitly requires creating or changing '{path}', " +
                    "but its output contract does not authorize that file. Assign the exact output to the responsible card " +
                    "and reconcile its dependencies before implementation. A consumed input or folder is not file write authority.";
            }
            return string.Empty;
        }
        private static string ValidatePlanningGraphContracts(
            IReadOnlyCollection<KanbanTask> children)
        {
            List<KanbanTask> executable =
                (children ?? Array.Empty<KanbanTask>())
                    .Where(child => child != null)
                    .ToList();

            if (executable.Count == 0)
            {
                return
                    "Planning validation requires at least one proposed executable child card.";
            }

            KanbanTask missingIdentity =
                executable.FirstOrDefault(
                    child => string.IsNullOrWhiteSpace(child.Id));

            if (missingIdentity != null)
            {
                return
                    $"Planning validation found a child without a durable ID: '{missingIdentity.Title}'.";
            }

            IGrouping<string, KanbanTask> duplicateIdentity =
                executable
                    .GroupBy(child => child.Id, StringComparer.Ordinal)
                    .FirstOrDefault(group => group.Count() > 1);

            if (duplicateIdentity != null)
            {
                return
                    $"Planning validation found duplicate durable child ID '{duplicateIdentity.Key}'.";
            }

            Dictionary<string, KanbanTask> byId =
                executable.ToDictionary(
                    child => child.Id,
                    child => child,
                    StringComparer.Ordinal);

            foreach (KanbanTask child in executable)
            {
                string unknownDependency =
                    (child.DependsOnTaskIds ?? new List<string>())
                        .FirstOrDefault(
                            dependencyId =>
                                !string.IsNullOrWhiteSpace(dependencyId) &&
                                !byId.ContainsKey(dependencyId));

                if (!string.IsNullOrWhiteSpace(unknownDependency))
                {
                    return
                        $"Planning validation found dependency '{unknownDependency}' on '{child.Title}' outside the committed sibling graph.";
                }

                if (TransitivelyDependsOn(
                        child,
                        child,
                        byId,
                        new HashSet<string>(StringComparer.Ordinal)))
                {
                    return
                        $"Planning validation found a dependency cycle containing '{child.Title}'.";
                }
            }

            var artifactOwners =
                executable
                    .SelectMany(
                        child =>
                            NormalizeArtifactPaths(
                                    child.ProducedArtifactPaths)
                                .Select(
                                    path => new
                                    {
                                        Path = path,
                                        Task = child
                                    }))
                    .GroupBy(
                        entry => entry.Path,
                        StringComparer.OrdinalIgnoreCase)
                    .ToList();

            var duplicateOwner =
                artifactOwners.FirstOrDefault(
                    group => group.Select(entry => entry.Task.Id).Distinct().Count() > 1);

            if (duplicateOwner != null)
            {
                return
                    $"Planning validation found artifact ownership conflict for '{duplicateOwner.Key}' between " +
                    string.Join(
                        ", ",
                        duplicateOwner
                            .Select(entry => $"'{entry.Task.Title}'")
                            .Distinct()) +
                    ". Every persisted artifact requires exactly one owning card.";
            }

            Dictionary<string, KanbanTask> ownerByArtifact =
                artifactOwners.ToDictionary(
                    group => group.Key,
                    group => group.First().Task,
                    StringComparer.OrdinalIgnoreCase);

            foreach (KanbanTask consumer in executable)
            {
                foreach (string consumedPath in NormalizeArtifactPaths(
                             consumer.ConsumedArtifactPaths))
                {
                    if (!ownerByArtifact.TryGetValue(
                            consumedPath,
                            out KanbanTask producer) ||
                        ReferenceEquals(producer, consumer))
                    {
                        continue;
                    }

                    if (!TransitivelyDependsOn(
                            consumer,
                            producer,
                            byId,
                            new HashSet<string>(StringComparer.Ordinal)))
                    {
                        return
                            $"Planning validation found unordered artifact consumption: '{consumer.Title}' consumes '{consumedPath}' from '{producer.Title}' but does not depend on that producer.";
                    }
                }
            }

            var resourceWriters =
                executable
                    .SelectMany(
                        child =>
                            NormalizeResourceKeys(
                                    child.MutatedResourceKeys)
                                .Select(
                                    resourceKey => new
                                    {
                                        ResourceKey = resourceKey,
                                        Task = child
                                    }))
                    .GroupBy(
                        entry => entry.ResourceKey,
                        StringComparer.OrdinalIgnoreCase);

            foreach (var writerGroup in resourceWriters)
            {
                List<KanbanTask> writers =
                    writerGroup
                        .Select(entry => entry.Task)
                        .Distinct()
                        .ToList();

                for (int left = 0; left < writers.Count; left++)
                {
                    for (int right = left + 1; right < writers.Count; right++)
                    {
                        bool ordered =
                            TransitivelyDependsOn(
                                writers[left],
                                writers[right],
                                byId,
                                new HashSet<string>(StringComparer.Ordinal)) ||
                            TransitivelyDependsOn(
                                writers[right],
                                writers[left],
                                byId,
                                new HashSet<string>(StringComparer.Ordinal));

                        if (!ordered)
                        {
                            return
                                $"Planning validation found unordered writers for shared mutable resource '{writerGroup.Key}': '{writers[left].Title}' and '{writers[right].Title}'. Put every writer in one explicit dependency chain.";
                        }
                    }
                }
            }

            return string.Empty;
        }


        private static bool TransitivelyDependsOn(
            KanbanTask source,
            KanbanTask target,
            IReadOnlyDictionary<string, KanbanTask> byId,
            ISet<string> visited)
        {
            if (source == null ||
                target == null ||
                string.IsNullOrWhiteSpace(source.Id) ||
                string.IsNullOrWhiteSpace(target.Id) ||
                !visited.Add(source.Id))
            {
                return false;
            }

            foreach (string dependencyId in
                     source.DependsOnTaskIds ?? new List<string>())
            {
                if (string.Equals(
                        dependencyId,
                        target.Id,
                        StringComparison.Ordinal))
                {
                    return true;
                }

                if (byId.TryGetValue(
                        dependencyId,
                        out KanbanTask dependency) &&
                    TransitivelyDependsOn(
                        dependency,
                        target,
                        byId,
                        visited))
                {
                    return true;
                }
            }

            return false;
        }


        private int ReconcilePlanningProposalSnapshot(
            KanbanTask root,
            string planningAttemptId)
        {
            if (!IsPlanningCoordinator(root) ||
                string.IsNullOrWhiteSpace(planningAttemptId))
            {
                return 0;
            }

            List<KanbanTask> obsoleteRoots =
                GetDirectChildren(root)
                    .Where(
                        child =>
                            child != null &&
                            string.Equals(
                                child.CreationSource,
                                "create_kanban_child_task",
                                StringComparison.Ordinal) &&
                            !string.Equals(
                                child.ProposedInPlanningAttemptId,
                                planningAttemptId,
                                StringComparison.Ordinal))
                    .ToList();

            if (obsoleteRoots.Count == 0)
                return 0;

            HashSet<string> obsoleteIds =
                new HashSet<string>(
                    obsoleteRoots
                        .SelectMany(
                            child =>
                                new[] { child }
                                    .Concat(GetAllDescendants(child)))
                        .Where(child => child != null)
                        .Select(child => child.Id),
                    StringComparer.Ordinal);

            int before = _board.Tasks.Count;
            _board.Tasks.RemoveAll(
                child =>
                    child != null &&
                    obsoleteIds.Contains(child.Id));

            ReconcileAllTaskRelationships();
            MarkDirty();
            return before - _board.Tasks.Count;
        }


        private bool TryGetUnresolvedSiblingOwnershipFailure(KanbanTask source, out string reason)
        {
            reason = string.Empty;
            if (source == null || string.IsNullOrWhiteSpace(source.ParentTaskId) ||
                string.IsNullOrWhiteSpace(source.ExecutionMemory))
                return false;

            // Use actual capability outcomes, not the model's closing prose or
            // directory existence. A later successful identical operation clears
            // the failure; observations of a different file do not.
            var failures = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            var readFailures = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            try
            {
                var entries = JObject.Parse(source.ExecutionMemory)["Entries"] as JArray;
                foreach (JObject entry in (entries ?? new JArray()).OfType<JObject>())
                {
                    string eventName = (string)entry["Event"];
                    if (eventName != "ToolFailed" && eventName != "ToolCompleted") continue;
                    string arguments = (string)entry["Arguments"];
                    if (string.IsNullOrWhiteSpace(arguments)) continue;
                    JObject args;
                    try { args = JObject.Parse(arguments); }
                    catch (Newtonsoft.Json.JsonException) { continue; }
                    string path = ((string)args["path"] ?? (string)args["Path"] ?? (string)args["assetPath"] ?? (string)args["scenePath"] ?? (string)args["destinationScenePath"] ?? string.Empty)
                        .Trim().Replace('\\', '/');
                    if (string.IsNullOrWhiteSpace(path)) continue;
                    string key = (string)entry["Tool"] + "\n" + path;
                    failures.Remove(key);
                    readFailures.Remove(key);
                    string failureText = (string)entry["Result"] ?? string.Empty;
                    bool readDenied = failureText.StartsWith("[ArtifactReadScope]", StringComparison.Ordinal);
                    if (eventName == "ToolFailed" && (readDenied ||
                        failureText.StartsWith("[ArtifactOwnership]", StringComparison.Ordinal)))
                    {
                        failures[key] = path;
                        if (readDenied) readFailures.Add(key);
                    }
                }
            }
            catch (Newtonsoft.Json.JsonException) { return false; }

            var owned = new HashSet<string>(NormalizeArtifactPaths(source.ProducedArtifactPaths),
                StringComparer.OrdinalIgnoreCase);
            foreach (var failure in failures.OrderBy(entry => readFailures.Contains(entry.Key)))
            {
                string path = failure.Value;
                bool readDenied = readFailures.Contains(failure.Key);

                if (owned.Contains(path)) continue;
                if (readDenied && NormalizeArtifactPaths(source.ConsumedArtifactPaths).Contains(path, StringComparer.OrdinalIgnoreCase)) continue;
                KanbanTask owner = _board.Tasks.FirstOrDefault(candidate => candidate != null &&
                    candidate.Id != source.Id && candidate.ParentTaskId == source.ParentTaskId &&
                    !candidate.IsDecompositionDraft && NormalizeArtifactPaths(candidate.ProducedArtifactPaths)
                        .Contains(path, StringComparer.OrdinalIgnoreCase));
                if (readDenied)
                {
                    reason = $"[ArtifactReadContractConflict] '{source.Title}' stalled after attempting to read '{path}' " +
                        "outside its declared inputs, outputs, and completed dependency artifacts. " +
                        (owner == null ? "No sibling declares this artifact as an output. " :
                            $"The artifact belongs to '{owner.Title}' ({owner.Id}). ") +
                        "Reconcile the card's requirements with the graph. If this input is genuinely required, declare its " +
                        "producer and a valid dependency; do not create a cycle on a downstream integration card. " +
                        "Otherwise move scene-placement or integration requirements to their owner and direct this card to finish " +
                        "its own missing outputs. Preserve existing work; do not broaden permissions merely to permit exploratory reads.";
                    return true;
                }
                if (owner == null)
                {
                    reason = $"[MissingArtifactOwnership] '{source.Title}' attempted to create or change '{path}', " +
                        "but this required artifact has no declared owner in the committed sibling graph. " +
                        "The capability exists; its execution was rejected by the artifact ownership boundary. " +
                        "Reconcile the acceptance criteria, assign the exact artifact path to one owner, and update dependencies. " +
                        "Preserve existing cards, execution memory, and artifacts. Do not grant recursive folder authority.";
                    return true;
                }
                reason = $"[ArtifactOwnershipConflict] '{source.Title}' repeatedly attempted a forbidden mutation of '{path}', " +
                    $"whose canonical owner is '{owner.Title}' ({owner.Id}). " +
                    "Reconcile the source card's acceptance criteria and implementation strategy with that ownership. " +
                    "Folder ownership grants no recursive file authority. Remove conflicting placeholder requirements, " +
                    "or assign the cohesive work to one owner and correct its dependency contracts. " +
                    "Preserve existing cards and artifacts; do not mark folder existence as completed script work.";
                return true;
            }
            return false;
        }

        // Recover unresolved implementation contract failures, including those masked by a later provider error.
        private void RecoverOwnershipCapabilityRequests()
        {
            if (_activeAgentRuns.Count > 0) return;
            foreach (KanbanTask task in _board.Tasks.ToList())
            {
                if (task == null || task.Status != TaskStatus.Crashed) continue;
                if (task.Memory?.Attempts?.LastOrDefault()?.Phase == "Review") continue;
                KanbanTask root = GetTaskRoot(task);
                if (root == null || root.WorkflowPhase == WorkflowPhasePlanning) continue;
                if (!TryGetUnresolvedSiblingOwnershipFailure(task, out string reason)) continue;
                task.AgentError = string.Empty;
                RequestPlanningRevision(task, reason, "Implementation");
                return;
            }
        }
        private void RequestPlanningRevision(
            KanbanTask source,
            string reason,
            string originatingPhase)
        {
            KanbanTask root = GetTaskRoot(source);
            if (root == null)
                return;

            string ownedArtifactPath = string.Empty;
            if (!TryGetUnresolvedSiblingOwnershipFailure(source, out _) &&
                (IsCompletionOnlyPlanningKickback(reason) || TryGetOwnedArtifactRemediation(
                    source,
                    reason,
                    out ownedArtifactPath)))
            {
                bool routeToEvidenceReview =
                    string.Equals(
                        originatingPhase,
                        "Implementation",
                        StringComparison.Ordinal) &&
                    (source.ProducedArtifactPaths ?? new List<string>())
                        .Any(path => !string.IsNullOrWhiteSpace(path)) &&
                    !TryGetMissingProducedArtifact(
                        source,
                        out _);

                source.Status =
                    routeToEvidenceReview
                        ? TaskStatus.Review
                        : TaskStatus.Todo;
                source.AgentError = string.Empty;
                source.AgentResult =
                    routeToEvidenceReview
                        ? "[PlanningKickbackRejected] AIRuntime rejected a local implementation planning escalation after confirming every declared ProducedArtifactPath exists. Persisted work was preserved for fresh authoritative Review."
                        : source.AgentResult;
                source.ReviewStatus =
                    routeToEvidenceReview
                        ? "Pending"
                        : "Rework Requested";
                source.ReviewFailureKind =
                    routeToEvidenceReview
                        ? string.Empty
                        : "ImplementationDefect";
                source.ReviewResult =
                    routeToEvidenceReview
                        ? string.Empty
                        : reason?.Trim() ?? string.Empty;
                source.ReviewEvidence =
                    routeToEvidenceReview
                        ? string.Empty
                        : source.ReviewEvidence;
                source.ReviewStartedAt =
                    routeToEvidenceReview
                        ? string.Empty
                        : source.ReviewStartedAt;
                source.ReviewFinishedAt =
                    routeToEvidenceReview
                        ? string.Empty
                        : source.ReviewFinishedAt;
                source.BlockedByTaskId = string.Empty;
                source.BlockedReason = string.Empty;
                source.NextAgentMessage =
                    routeToEvidenceReview
                        ? string.Empty
                        : "[PlanningKickbackRejected] Complete and verify the declared outputs before claiming success. AIRuntime rejected this planning escalation for the card-owned artifact '" +
                          ownedArtifactPath +
                          "'. Implement and verify that local correction on this card. Do not revise or replace the committed graph.\n\n" +
                          source.ReviewResult;

                RestoreImplementationWorkflowRoot(
                    root,
                    source);

                TouchTask(source);

                if (!ReferenceEquals(source, root))
                    TouchTask(root);

                MarkDirty();

                if (_autoSave)
                    SaveBoard(showDialog: false);

                _statusMessage =
                    routeToEvidenceReview
                        ? $"Rejected local implementation kickback from '{source.Title}'; persisted artifacts queued for Review"
                        : $"Rejected local implementation kickback from '{source.Title}'; committed graph preserved";
                Debug.LogWarning(
                    "[Kanban] " +
                    _statusMessage +
                    ": " +
                    reason);
                Repaint();
                return;
            }

            root.WorkflowPhase = WorkflowPhasePlanning;
            AIRuntime.Runtime.Providers.ProviderTransportDebug.WriteEvent("Harness", "PLANNING REVISION",
                new { RootTaskId = root.Id, SourceTaskId = source?.Id, Reason = reason, Phase = originatingPhase });
            root.PlanningKickbackReason = reason?.Trim() ?? string.Empty;
            root.PlanningKickbackFingerprint = ComputePlanFingerprint(root);
            root.Status = TaskStatus.Todo;
            root.ReviewStatus = "Planning Required";
            root.ReviewFailureKind = "PlanningDefect";
            root.BlockedReason =
                $"{originatingPhase} requested a planning revision from '{source?.Title}'.";
            root.NextAgentMessage =
                $"[PlanningKickback from {originatingPhase}: {source?.Title}]\n" +
                root.PlanningKickbackReason +
                "\n\nSubmit the complete desired direct-child graph as an authoritative snapshot. Restate unchanged cards to preserve them, revise affected contracts materially, and omit cards that the new plan supersedes. Do not perform implementation work during this run.";

            if (source != null && !ReferenceEquals(source, root))
            {
                source.Status = TaskStatus.Todo;
                source.ReviewStatus = "Planning Required";
                source.ReviewFailureKind = "PlanningDefect";
                source.BlockedReason = root.BlockedReason;
                source.NextAgentMessage = string.Empty;
                TouchTask(source);
            }

            TouchTask(root);
            MarkDirty();
            if (_autoSave)
                SaveBoard(showDialog: false);

            _statusMessage =
                $"'{source?.Title}' returned project '{root.Title}' to Planning";
            Debug.LogWarning("[Kanban] " + _statusMessage + ": " + reason);
            Repaint();
        }


        private void CompleteAgentTask(
            string taskId,
            string agentId,
            string result,
            string error,
            string reviewEvidence,
            bool reviewMode)
        {
            CompleteAgentTaskCore(
                taskId,
                agentId,
                result,
                error,
                reviewEvidence,
                reviewMode,
                providerFailureKind: null);
        }


        private void CompleteAgentTaskCore(
            string taskId,
            string agentId,
            string result,
            string error,
            string reviewEvidence,
            bool reviewMode,
            ProviderFailureKind? providerFailureKind)
        {
            bool completedCoordinationOnly =
                false;

            string completedPlanningAttemptId =
                string.Empty;

            bool completedPlanningCommitted =
                false;

            string lastPlanningCommitError = null;

            bool hadTrackedRun =
                false;

            if (_activeAgentRuns.TryGetValue(
                    agentId,
                    out ActiveAgentRun activeRun) &&
                string.Equals(
                    activeRun.TaskId,
                    taskId,
                    StringComparison.Ordinal))
            {
                hadTrackedRun = true;

                completedCoordinationOnly =
                    activeRun.CoordinationOnly;

                completedPlanningAttemptId =
                    activeRun.PlanningAttemptId ?? string.Empty;

                completedPlanningCommitted =
                    activeRun.PlanningCommitted;

                lastPlanningCommitError = activeRun.LastPlanningCommitError;

                _activeAgentRuns.Remove(
                    agentId);

                activeRun.Cancellation.Dispose();
            }


            if (_discardedAgentRunTaskIds.Remove(
                    taskId))
            {
                return;
            }


            bool completingDuringReloadPreparation =
                _isPreparingForAssemblyReload;


            if (this == null)
                return;


            KanbanTask task =
                FindTask(
                    taskId);


            if (task == null)
                return;

            if (!reviewMode &&
                !completedCoordinationOnly &&
                IsPlanningCoordinator(task))
            {
                completedCoordinationOnly = true;
                completedPlanningAttemptId =
                    task.PlanningAttemptId ?? string.Empty;
            }

            if (completedCoordinationOnly &&
                completedPlanningCommitted &&
                ((providerFailureKind.HasValue &&
                  providerFailureKind != ProviderFailureKind.Cancelled) ||
                 IsProviderRateLimitFailure(
                     error,
                     providerFailureKind) ||
                 IsTransientProviderAvailabilityFailure(
                     error,
                     providerFailureKind)))
            {
                result =
                    "[PlanningCommitAccepted] AIRuntime accepted and validated the complete Planning graph before the provider failed to produce optional closing prose.";
                error = string.Empty;
                providerFailureKind = null;
            }


            if (string.IsNullOrWhiteSpace(error))
            {
                task.AutomaticRetryCount = 0;
                task.NextAttemptAtUtc = string.Empty;
                task.RetryReason = string.Empty;
            }
            if (reviewMode && !IsProviderRateLimitFailure(error, providerFailureKind) &&
                (IsTransientProviderAvailabilityFailure(error, providerFailureKind) || providerFailureKind == ProviderFailureKind.Authentication))
            {
                task.RetryReview = true;
                task.Status = TaskStatus.Review;
                task.ReviewStatus = "Retry Available";
                task.ReviewResult = error;
                task.BlockedReason = BuildProviderResumeGuidance(error);
                ScheduleProviderRetry(task, error, providerFailureKind, review: true);
                TouchTask(task);
                PersistSchedulerState();
                return;
            }

            if (reviewMode && TryPauseUnsupportedProviderInput(task, error, providerFailureKind, true, reviewEvidence))
                return;

            if (reviewMode)
            {
                CompleteReviewTask(
                    task,
                    agentId,
                    result,
                    error,
                    reviewEvidence);

                return;
            }


            task.AgentFinishedAt =
                DateTime.UtcNow.ToString(
                    "O");

            int checkpointedSceneCount = 0;
            string sceneCheckpointError = string.Empty;

            if (!completedCoordinationOnly &&
                !TryCheckpointOwnedDirtyScenes(
                    task,
                    out checkpointedSceneCount,
                    out sceneCheckpointError))
            {
                string originalError = error ?? string.Empty;
                error =
                    "[SceneCheckpointFailure] AIRuntime could not persist the card's owned dirty scene before ending this run: " +
                    sceneCheckpointError;

                if (!string.IsNullOrWhiteSpace(originalError))
                {
                    error += "\n\nOriginal run failure:\n" + originalError;
                }
            }
            else if (checkpointedSceneCount > 0)
            {
                Debug.Log(
                    $"[Kanban] Checkpointed {checkpointedSceneCount} owned dirty scene(s) before completing '{task.Title}'.");
            }

            if (!completedCoordinationOnly && string.IsNullOrEmpty(sceneCheckpointError) &&
                TryPauseUnsupportedProviderInput(task, error, providerFailureKind, false, reviewEvidence))
                return;

            if (!reviewMode &&
                !completedCoordinationOnly &&
                TryApplyExplicitArtifactRecovery(
                    task,
                    result,
                    error,
                    out string recoverableMissingArtifact))
            {
                TouchTask(task);
                MarkDirty();

                if (_autoSave || completingDuringReloadPreparation)
                    SaveBoard(showDialog: false);

                _statusMessage =
                    $"'{task.Title}' was returned to Implementation to create '{recoverableMissingArtifact}'";

                Debug.LogWarning(
                    "[Kanban] " + _statusMessage);

                Repaint();
                return;
            }

            if (!reviewMode && !completedCoordinationOnly && !string.IsNullOrWhiteSpace(error) &&
                TryGetUnresolvedSiblingOwnershipFailure(task, out string ownershipConflict))
            {
                task.AgentResult = result ?? string.Empty;
                task.AgentError = string.Empty;
                RequestPlanningRevision(task, ownershipConflict, "Implementation");
                return;
            }

            if (!reviewMode &&
                !completedCoordinationOnly &&
                IsCapabilityNoProgressFailure(error) &&
                (task.ProducedArtifactPaths ?? new List<string>())
                    .Any(path => !string.IsNullOrWhiteSpace(path)) &&
                !TryGetMissingProducedArtifact(
                    task,
                    out _))
            {
                string stoppedError = error;
                error = string.Empty;
                result =
                    "[ImplementationNoProgressRecovery] AIRuntime stopped the implementation agent after repeated unchanged capability observations. " +
                    "Every declared ProducedArtifactPath exists, so the persisted implementation was preserved and sent to Review for fresh authoritative verification.\n\n" +
                    stoppedError;
                task.NextAgentMessage = string.Empty;
                task.ReviewFailureKind = string.Empty;
            }

            if (!reviewMode &&
                !completedCoordinationOnly &&
                IsCapabilityNoProgressFailure(error) &&
                task.CapabilityRecoveryPolicyVersion <
                    CurrentCapabilityRecoveryPolicyVersion)
            {
                TryGetMissingProducedArtifact(
                    task,
                    out string noProgressMissingArtifact);

                task.Status = TaskStatus.Todo;
                task.AgentResult = result ?? string.Empty;
                task.AgentError = string.Empty;
                task.ReviewStatus = "Rework Requested";
                task.ReviewFailureKind = "ImplementationDefect";
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = noProgressMissingArtifact ?? string.Empty;
                task.CapabilityRecoveryPolicyVersion =
                    CurrentCapabilityRecoveryPolicyVersion;
                task.NextAgentMessage =
                    BuildCapabilityNoProgressRecoveryMessage(
                        task,
                        error,
                        noProgressMissingArtifact);

                TouchTask(task);
                MarkDirty();

                if (_autoSave || completingDuringReloadPreparation)
                    SaveBoard(showDialog: false);

                _statusMessage =
                    $"'{task.Title}' was requeued once with a concrete capability no-progress correction";
                Debug.LogWarning(
                    "[Kanban] " + _statusMessage + ": " + error);
                Repaint();
                return;
            }

            if (!reviewMode &&
                !completedCoordinationOnly &&
                string.IsNullOrWhiteSpace(error) &&
                TryExtractCapabilityRequest(
                    result,
                    out string capabilityRequest))
            {
                if (TryGetUnresolvedSiblingOwnershipFailure(task, out string planningReason))
                {
                    task.AgentResult = result ?? string.Empty;
                    task.AgentError = string.Empty;
                    RequestPlanningRevision(task, planningReason, "Implementation");
                    return;
                }
                task.Status = TaskStatus.Crashed;
                task.AgentResult = result ?? string.Empty;
                task.AgentError = string.Empty;
                task.ReviewStatus = "Capability Required";
                task.ReviewFailureKind = "CapabilityGap";
                task.BlockedReason = capabilityRequest;
                task.NextAgentMessage =
                    "[CapabilityRequest] " + capabilityRequest;
                TouchTask(task);
                MarkDirty();
                if (_autoSave || completingDuringReloadPreparation)
                    SaveBoard(showDialog: false);
                _statusMessage =
                    $"'{task.Title}' paused because it requires an unavailable capability";
                Repaint();
                return;
            }

            if (!reviewMode &&
                !completedCoordinationOnly &&
                string.IsNullOrWhiteSpace(error) &&
                TryExtractPlanningKickback(
                    result,
                    out string planningKickback))
            {
                RequestPlanningRevision(
                    task,
                    planningKickback,
                    "Implementation");
                return;
            }

            if (!reviewMode &&
                !completedCoordinationOnly &&
                string.IsNullOrWhiteSpace(error) &&
                TryGetMissingProducedArtifact(
                    task,
                    out string missingArtifact))
            {
                task.Status = TaskStatus.Todo;
                task.AgentResult = result ?? string.Empty;
                task.AgentError = string.Empty;
                task.ReviewStatus = "Rework Requested";
                task.ReviewFailureKind = "ImplementationDefect";
                task.NextAgentMessage =
                    "[ReviewFailureKind: ImplementationDefect] The implementation run returned successfully, but its declared output is missing or invalid: " +
                    missingArtifact +
                    ". Create and validate every declared ProducedArtifactPath before completing the card.";
                TouchTask(task);
                MarkDirty();
                if (_autoSave || completingDuringReloadPreparation)
                    SaveBoard(showDialog: false);
                _statusMessage =
                    $"'{task.Title}' was returned to Implementation because '{missingArtifact}' is missing or invalid";
                Repaint();
                return;
            }

            if (task.SuppressAutoDecomposition &&
                !string.IsNullOrWhiteSpace(error))
            {
                // Suppression is a scoped handoff optimization, not a durable
                // excuse to bypass future decomposition after a failed run.
                task.SuppressAutoDecomposition = false;

                if ((task.NextAgentMessage ?? string.Empty).StartsWith(
                        "[AssociationLeaf]",
                        StringComparison.Ordinal))
                {
                    task.NextAgentMessage = string.Empty;
                }
            }

            if (completedCoordinationOnly &&
                task.SuppressAutoDecomposition &&
                string.IsNullOrWhiteSpace(error) &&
                !HasDecompositionDrafts(task))
            {
                // The board-wide ownership guard proved that the proposed
                // child was the same atomic artifact as this card or one of
                // its ancestors. The coordination turn intentionally created
                // nothing. Requeue once with implementation capabilities and
                // discard orchestration memory so the direct run is clean.
                task.Status = TaskStatus.Todo;
                task.AgentResult = string.Empty;
                task.ExecutionMemory = string.Empty;
                ResetCardMemory(task);
                task.AgentError = string.Empty;
                task.AgentStartedAt = string.Empty;
                task.AgentFinishedAt = string.Empty;
                task.ReviewStatus = string.Empty;
                task.ReviewResult = string.Empty;
                task.ReviewEvidence = string.Empty;
                task.ReviewFailureKind = string.Empty;
                task.ReviewStartedAt = string.Empty;
                task.ReviewFinishedAt = string.Empty;
                task.ReviewFailureCount = 0;
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = string.Empty;
                task.DecompositionFailureCount = 0;
                task.DecompositionPolicyVersion =
                    CurrentDecompositionPolicyVersion;
                task.RequestTitle = string.Empty;
                task.RequestDescription = string.Empty;
                task.RequestAcceptanceCriteria = string.Empty;

                TouchTask(task);
                MarkDirty();

                if (_autoSave || completingDuringReloadPreparation)
                {
                    SaveBoard(showDialog: false);
                }

                _statusMessage =
                    $"'{task.Title}' was recognized as an atomic associated leaf and requeued for direct execution";

                Repaint();
                return;
            }

            bool providerLifecycleFailure =
                IsProviderRateLimitFailure(
                    error,
                    providerFailureKind) ||
                IsTransientProviderAvailabilityFailure(
                    error,
                    providerFailureKind);

            if (!string.IsNullOrWhiteSpace(error) &&
                !providerLifecycleFailure &&
                (IsDecompositionValidationFailure(error) ||
                 HasDecompositionDrafts(task)))
            {
                CompleteFailedDecompositionAttempt(
                    task,
                    agentId,
                    result,
                    error,
                    completingDuringReloadPreparation);

                return;
            }


            if (string.IsNullOrWhiteSpace(error) &&
                completedCoordinationOnly)
            {
                if (hadTrackedRun &&
                    !completedPlanningCommitted)
                {
                    CompleteFailedDecompositionAttempt(
                        task,
                        agentId,
                        result,
                        !string.IsNullOrWhiteSpace(lastPlanningCommitError)
                            ? "Planning commit was attempted but rejected: " + lastPlanningCommitError +
                              " The draft proposal remains durable for correction."
                            : "Planning did not call commit_kanban_plan after constructing the graph. The draft proposal remains durable for a corrective Planning pass.",
                        completingDuringReloadPreparation);
                    return;
                }

                string planningAttemptId =
                    !string.IsNullOrWhiteSpace(completedPlanningAttemptId)
                        ? completedPlanningAttemptId
                        : task.PlanningAttemptId;

                error = ValidatePlanningProposal(task, planningAttemptId);

                if (!string.IsNullOrWhiteSpace(error))
                {
                    CompleteFailedDecompositionAttempt(
                        task,
                        agentId,
                        result,
                        error,
                        completingDuringReloadPreparation);
                    return;
                }

                ReconcilePlanningProposalSnapshot(task, planningAttemptId);
                CommitDecompositionDrafts(task);
                error = FinalizePlanningPhase(task);
            }

            if (string.IsNullOrWhiteSpace(
                    error))
            {
                CommitDecompositionDrafts(
                    task);

                task.DecompositionFailureCount =
                    0;

                task.DecompositionPolicyVersion =
                    CurrentDecompositionPolicyVersion;

                string nextMessage =
                    task.NextAgentMessage ??
                    string.Empty;

                if (nextMessage.StartsWith(
                        "[PlanningValidationFailure]",
                        StringComparison.Ordinal) ||
                    nextMessage.StartsWith(
                        "[DelegationValidationFailure]",
                        StringComparison.Ordinal) ||
                    nextMessage.StartsWith(
                        "[DecompositionValidationFailure]",
                        StringComparison.Ordinal) ||
                    nextMessage.StartsWith(
                        ExplicitArtifactRecoveryPrefix,
                        StringComparison.Ordinal))
                {
                    task.NextAgentMessage =
                        string.Empty;
                }

                task.AgentResult =
                    result ??
                    string.Empty;

                task.AgentError =
                    string.Empty;

                TrySetTaskStatus(
                    task,
                    GetAgentCompletionStatus(
                        task.Status,
                        error),
                    out _statusMessage);

                task.ReviewStatus =
                    TaskMayHaveWrittenUnityScript(task)
                        ? "Compilation Pending"
                        : "Pending";

                task.ReviewResult =
                    string.Empty;


            }
            else
            {
                bool providerRateLimited =
                    IsProviderRateLimitFailure(
                        error,
                        providerFailureKind);

                bool providerRetryAvailable =
                    IsTransientProviderAvailabilityFailure(
                        error,
                        providerFailureKind);

                task.AgentError =
                    error;

                if (providerRateLimited)
                {
                    ApplyProviderCooldown(
                        task,
                        agentId,
                        error,
                        reviewMode: false);

                    Debug.LogWarning(
                        $"[Kanban] Paused provider lane after rate limit while running '{task.Title}': {error}");
                }

                if (IsCapabilityRoundLimitFailure(error))
                {
                    task.CapabilityBudgetPolicyVersion =
                        CurrentCapabilityBudgetPolicyVersion;
                }

                if (!providerRateLimited)
                {
                    TrySetTaskStatus(
                        task,
                        providerRetryAvailable
                            ? TaskStatus.Todo
                            : GetAgentCompletionStatus(
                                task.Status,
                                error),
                        out _statusMessage);
                }

                task.ReviewStatus =
                    providerRateLimited
                        ? ProviderCooldownReviewStatus
                    : providerRetryAvailable
                        ? "Retry Available"
                        : string.IsNullOrWhiteSpace(
                        task.ReviewFailureKind)
                        ? IsDeterministicCompletionFailure(error)
                            ? "Failed"
                            : "Crashed"
                        : "Rework Blocked";

                if (string.IsNullOrWhiteSpace(
                        task.ReviewResult))
                {
                    task.ReviewResult =
                        error;
                }

                if (providerRetryAvailable &&
                    !providerRateLimited)
                {
                    task.BlockedReason =
                        BuildProviderResumeGuidance(error);
                    task.NextAgentMessage =
                        "[ProviderRetryAvailable] Resume this durable attempt after provider availability changes. Preserve accepted proposals, execution memory, and valid persisted artifacts; inspect before recreating anything.";
                    ScheduleProviderRetry(task, error, providerFailureKind, review: false);
                }
                else if (!providerRateLimited &&
                         IsProviderLifecycleHandoff(
                             task.NextAgentMessage))
                {
                    task.NextAgentMessage =
                        string.Empty;
                }

                if (!providerRateLimited)
                {
                    Debug.LogError(
                        $"[Kanban] {agentId} failed task '{task.Title}': {error}");
                }
            }


            TouchTask(
                task);

            MarkDirty();


            if (_autoSave ||
                completingDuringReloadPreparation)
            {
                SaveBoard(
                    showDialog: false);
            }

            _statusMessage =
                string.IsNullOrWhiteSpace(
                    error)
                    ? $"{agentId} completed '{task.Title}' — ready for review"
                    : IsProviderRateLimitFailure(
                          error,
                          providerFailureKind)
                        ? $"{agentId} provider lane paused after rate limiting; '{task.Title}' will resume after cooldown"
                    : $"{agentId} could not complete '{task.Title}'";

            Repaint();

            if (string.IsNullOrWhiteSpace(error) &&
                TaskMayHaveWrittenUnityScript(task))
            {
                ImportWrittenUnityScriptsBeforeContinuing(task);
            }
        }


        private void CompleteFailedDecompositionAttempt(
            KanbanTask task,
            string agentId,
            string result,
            string error,
            bool completingDuringReloadPreparation)
        {
            if (TryRecoverAtomicLeafAfterDecompositionFailure(
                    task,
                    completingDuringReloadPreparation))
            {
                return;
            }

            string previousError =
                task.ReviewResult ??
                string.Empty;

            int retainedDraftCount =
                GetDirectChildren(task)
                    .Count(
                        child =>
                            child != null &&
                            child.IsDecompositionDraft &&
                            string.Equals(
                                child.CreationSource,
                                "create_kanban_child_task",
                                StringComparison.Ordinal));

            task.DecompositionFailureCount =
                task.DecompositionFailureCount == int.MaxValue
                    ? int.MaxValue
                    : task.DecompositionFailureCount + 1;

            task.DecompositionPolicyVersion =
                CurrentDecompositionPolicyVersion;
            task.PlanningAttemptId = string.Empty;

            task.AgentResult =
                result ??
                string.Empty;

            task.ReviewResult =
                error;

            task.ReviewEvidence =
                string.Empty;

            task.ReviewFailureKind =
                string.Empty;

            bool noProgress =
                task.DecompositionFailureCount >=
                MaximumDecompositionFailureCount ||
                (task.DecompositionFailureCount > 1 &&
                 !string.IsNullOrWhiteSpace(error) &&
                 string.Equals(
                     previousError.Trim(),
                     error.Trim(),
                     StringComparison.Ordinal));

            task.Status =
                noProgress
                    ? TaskStatus.Failed
                    : TaskStatus.Todo;

            task.AgentError =
                string.Empty;

            if (!noProgress)
            {
                task.ReviewStatus =
                    "Planning Retry";

                task.BlockedReason =
                    string.Empty;

                task.NextAgentMessage =
                    "[PlanningValidationFailure] AIRuntime blocked the proposed graph before any child could run. Submit the complete desired direct-child graph again: restate every card that should remain, revise invalid cards, and omit obsolete cards so AIRuntime can retire them transactionally.\n\n" +
                    error;

                Debug.LogWarning(
                    $"[Kanban] {agentId} plan for '{task.Title}' was blocked; retained {retainedDraftCount} current proposal card(s) for context and queued a complete-graph correction: {error}");
            }
            else
            {
                task.ReviewStatus =
                    "Delegation Blocked by No Progress";

                task.NextAgentMessage =
                    "[PlanningNoProgress] AIRuntime paused because the authoritative Planning graph made no structural progress or reached the retry limit. Resume after changing the desired graph or relevant external state.\n\n" +
                    error;

                task.BlockedReason =
                    error;

                Debug.LogWarning(
                    $"[Kanban] {agentId} plan for '{task.Title}' repeated an unchanged validation failure with {retainedDraftCount} proposal card(s) and paused: {error}");
            }

            TouchTask(task);
            MarkDirty();

            if (_autoSave ||
                completingDuringReloadPreparation)
            {
                SaveBoard(showDialog: false);
            }

            _statusMessage =
                noProgress
                    ? $"Planning paused for '{task.Title}' after an unchanged structural failure"
                    : $"Blocked the invalid plan for '{task.Title}' and queued a complete-graph correction";

            Repaint();
        }


        private bool TryRecoverAtomicLeafAfterDecompositionFailure(
            KanbanTask task,
            bool completingDuringReloadPreparation)
        {
            string artifactPath =
                GetSingleOwnedScriptPath(task);

            if (string.IsNullOrWhiteSpace(artifactPath) ||
                !IsStandaloneScriptAssociationLeaf(
                    task,
                    artifactPath))
            {
                return false;
            }

            HashSet<string> removableIds =
                new HashSet<string>(
                    GetDirectChildren(task)
                        .Where(
                            child =>
                                child != null &&
                                child.IsDecompositionDraft &&
                                string.Equals(
                                    child.CreationSource,
                                    "create_kanban_child_task",
                                    StringComparison.Ordinal))
                        .SelectMany(
                            child =>
                                new[] { child }
                                    .Concat(GetAllDescendants(child)))
                        .Where(
                            child =>
                                child != null &&
                                string.Equals(
                                    child.CreationSource,
                                    "create_kanban_child_task",
                                    StringComparison.Ordinal))
                        .Select(child => child.Id),
                    StringComparer.Ordinal);

            _board.Tasks.RemoveAll(
                candidate =>
                    candidate != null &&
                    removableIds.Contains(candidate.Id));

            task.Status = TaskStatus.Todo;
            task.IsDecompositionDraft = false;
            task.SuppressAutoDecomposition = true;
            task.AgentResult = string.Empty;
            task.ExecutionMemory = string.Empty;
            ResetCardMemory(task);
            task.AgentError = string.Empty;
            task.AgentStartedAt = string.Empty;
            task.AgentFinishedAt = string.Empty;
            task.ReviewStatus = string.Empty;
            task.ReviewResult = string.Empty;
            task.ReviewEvidence = string.Empty;
            task.ReviewFailureKind = string.Empty;
            task.ReviewStartedAt = string.Empty;
            task.ReviewFinishedAt = string.Empty;
            task.ReviewFailureCount = 0;
            task.PreflightFailureCount = 0;
            task.ReviewEvidenceFingerprint = string.Empty;
            task.LastRejectedReviewEvidenceFingerprint = string.Empty;
            task.LastRejectedReviewOutcomeFingerprint = string.Empty;
            task.ReviewDependencyStateFingerprint = string.Empty;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = string.Empty;
            task.DecompositionFailureCount = 0;
            task.DecompositionPolicyVersion =
                CurrentDecompositionPolicyVersion;
            task.RequestTitle = string.Empty;
            task.RequestDescription = string.Empty;
            task.RequestAcceptanceCriteria = string.Empty;
            task.NextAgentMessage =
                "[AssociationLeaf] AIRuntime rejected recursive decomposition of the card's single owned script " +
                $"artifact ({artifactPath}), discarded {removableIds.Count} draft card(s), and requeued the full " +
                "original contract for direct implementation. Do not delegate this artifact again.";

            TouchTask(task);
            ReconcileAllTaskRelationships();
            MarkDirty();

            if (_autoSave ||
                completingDuringReloadPreparation)
            {
                SaveBoard(showDialog: false);
            }

            _statusMessage =
                $"'{task.Title}' was recovered as an atomic script leaf and requeued for direct execution";

            Repaint();
            return true;
        }


        private void CommitDecompositionDrafts(
            KanbanTask parent)
        {
            if (parent == null ||
                string.IsNullOrWhiteSpace(parent.Id))
            {
                return;
            }

            foreach (KanbanTask child in _board.Tasks.Where(
                         candidate =>
                             candidate != null &&
                             string.Equals(
                                 candidate.ParentTaskId,
                                 parent.Id,
                                 StringComparison.Ordinal)))
            {
                child.IsDecompositionDraft = false;
                child.ProposedInPlanningAttemptId = string.Empty;
                TouchTask(child);
            }

            parent.PlanningAttemptId = string.Empty;
        }


        private static bool IsDecompositionValidationFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   (error.TrimStart().StartsWith(
                        "Delegation validation",
                        StringComparison.OrdinalIgnoreCase) ||
                    error.TrimStart().StartsWith(
                         "Granular decomposition",
                         StringComparison.OrdinalIgnoreCase) ||
                    error.TrimStart().StartsWith(
                         "Planning validation",
                         StringComparison.OrdinalIgnoreCase));
        }


        private static bool IsCapabilityRoundLimitFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   Regex.IsMatch(
                       error,
                       @"Conversation exceeded the maximum of \d+ capability rounds\.",
                       RegexOptions.IgnoreCase |
                       RegexOptions.CultureInvariant);
        }


        private static bool IsRecoverableCapabilityArgumentFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   error.TrimStart().StartsWith(
                       "DirectoryName must be one directory name",
                       StringComparison.Ordinal);
        }


        private static int RepairSourceOwnershipReviews(IEnumerable<KanbanTask> tasks)
        {
            int changes = 0;
            foreach (KanbanTask task in tasks ?? Array.Empty<KanbanTask>())
            {
                if (task == null || task.Status != TaskStatus.Review ||
                    task.ReviewStatus != "Waiting for Evidence" ||
                    !AIRuntimeKanbanAgentRunner.IsSourceArtifactEvidenceScope(task.ProducedArtifactPaths, task.ConsumedArtifactPaths, task.MutatedResourceKeys) ||
                    (task.ReviewResult ?? string.Empty).IndexOf(
                        "The component inspection evidence is unavailable, so the review cannot verify the diagnosis or any claimed fix.",
                        StringComparison.Ordinal) < 0)
                    continue;
                task.ReviewStatus = "Pending";
                task.ReviewFailureKind = string.Empty;
                task.LastRejectedReviewEvidenceFingerprint = string.Empty;
                task.ReviewDependencyStateFingerprint = string.Empty;
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = string.Empty;
                task.NextAgentMessage = "[OwnershipScopeRecovery] Review the preserved source implementation and current compilation against its declared contract. " +
                    "The prior harness-generated demand for downstream live component evidence was invalid. Do not repeat implementation or require scene assembly.";
                changes++;
            }
            return changes;
        }

        private static bool IsRecoverableFalseComponentInspectionFailure(
            KanbanTask task)
        {
            if (task ==
                    null ||
                string.IsNullOrWhiteSpace(
                    task.AgentError) ||
                task.AgentError.IndexOf(
                    "did not complete the required inspect_editor_component tool call",
                    StringComparison.OrdinalIgnoreCase) <
                0)
            {
                return false;
            }

            string request =
                string.Join(
                    "\n",
                    string.IsNullOrWhiteSpace(
                        task.RequestTitle)
                        ? task.Title ??
                          string.Empty
                        : task.RequestTitle,
                    string.IsNullOrWhiteSpace(
                        task.RequestDescription)
                        ? task.Description ??
                          string.Empty
                        : task.RequestDescription,
                    string.IsNullOrWhiteSpace(
                        task.RequestAcceptanceCriteria)
                        ? task.AcceptanceCriteria ??
                          string.Empty
                        : task.RequestAcceptanceCriteria);

            return AIRuntimeKanbanAgentRunner
                .IsStandaloneScriptAssetTask(
                    request);
        }


        private static bool IsLegacyDuplicateToolLoopFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   error.IndexOf(
                       "repeatedly requested the identical capability call",
                       StringComparison.OrdinalIgnoreCase) >= 0 &&
                   error.IndexOf(
                       "run was stopped to prevent a tool-call loop",
                       StringComparison.OrdinalIgnoreCase) >= 0;
        }


        private static bool IsLegacyMalformedProviderToolCallFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   (error.IndexOf(
                        "provider repeated the same malformed tool call after AIRuntime requested a schema correction",
                        StringComparison.OrdinalIgnoreCase) >= 0 ||
                    error.IndexOf(
                        "provider returned another malformed tool call after",
                        StringComparison.OrdinalIgnoreCase) >= 0);
        }


        private static bool IsRecoverableComponentNamespaceMismatch(
            KanbanTask task)
        {
            return task != null &&
                   !string.IsNullOrWhiteSpace(
                       task.ExecutionMemory) &&
                   task.ExecutionMemory.IndexOf(
                       "Component type 'UnityEngine.UI.Canvas' was not found",
                       StringComparison.OrdinalIgnoreCase) >= 0;
        }


        private static bool IsCapabilityNoProgressFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   (error.IndexOf(
                        "[CapabilityNoProgress]",
                        StringComparison.OrdinalIgnoreCase) >= 0 ||
                    (error.IndexOf(
                         "repeated the identical capability call",
                         StringComparison.OrdinalIgnoreCase) >= 0 &&
                     error.IndexOf(
                         "no-progress tool-call loop",
                         StringComparison.OrdinalIgnoreCase) >= 0));
        }


        private static bool TryGetLegacyRepeatedTailCapabilityFailure(
            KanbanTask task,
            out string capabilityName,
            out string failure)
        {
            capabilityName = string.Empty;
            failure = string.Empty;

            if (task == null ||
                string.IsNullOrWhiteSpace(task.AgentError) ||
                !task.AgentError.TrimStart().StartsWith(
                    "[ImplementationInactivityRecovery]",
                    StringComparison.OrdinalIgnoreCase) ||
                string.IsNullOrWhiteSpace(task.ExecutionMemory))
            {
                return false;
            }

            try
            {
                JArray entries =
                    JObject.Parse(task.ExecutionMemory)["Entries"] as JArray;

                if (entries == null || entries.Count < 3)
                    return false;

                int matchingTailFailures = 0;

                for (int index = entries.Count - 1; index >= 0; index--)
                {
                    JToken entry = entries[index];
                    string eventName =
                        entry?["Event"]?.Value<string>() ??
                        string.Empty;

                    if (!string.Equals(
                            eventName,
                            AgentEventType.ToolFailed.ToString(),
                            StringComparison.OrdinalIgnoreCase))
                    {
                        break;
                    }

                    string entryCapability =
                        (entry["Tool"]?.Value<string>() ??
                         string.Empty).Trim();
                    string entryFailure =
                        (entry["Result"]?.Value<string>() ??
                         entry["Error"]?.Value<string>() ??
                         string.Empty).Trim();

                    if (matchingTailFailures == 0)
                    {
                        capabilityName = entryCapability;
                        failure = entryFailure;
                    }
                    else if (!string.Equals(
                                 capabilityName,
                                 entryCapability,
                                 StringComparison.Ordinal) ||
                             !string.Equals(
                                 failure,
                                 entryFailure,
                                 StringComparison.Ordinal))
                    {
                        break;
                    }

                    matchingTailFailures++;
                }

                return matchingTailFailures >= 3 &&
                       !string.IsNullOrWhiteSpace(capabilityName) &&
                       !string.IsNullOrWhiteSpace(failure);
            }
            catch (JsonException)
            {
                return false;
            }
        }


        private static string BuildCapabilityNoProgressRecoveryMessage(
            KanbanTask task,
            string error,
            string missingArtifact)
        {
            string outputCorrection =
                string.IsNullOrWhiteSpace(missingArtifact)
                    ? "Inspect persisted execution memory once, then make progress with a mutation, a materially different observation, a capability request, or a completion report."
                    : "The declared output is still missing or invalid: " +
                      missingArtifact +
                      ". Create or repair it before further broad inspection.";

            string sceneCorrection =
                (task?.ProducedArtifactPaths ?? new List<string>())
                    .FirstOrDefault(path =>
                        !string.IsNullOrWhiteSpace(path) &&
                        path.EndsWith(
                            ".unity",
                            StringComparison.OrdinalIgnoreCase));

            string sceneInstruction =
                string.IsNullOrWhiteSpace(sceneCorrection)
                    ? string.Empty
                    : " If the active hierarchy already contains this card's intended work but its ScenePath is empty, preserve it without a popup by calling save_editor_scene with destinationScenePath='" +
                      sceneCorrection +
                      "'. Do not call create_editor_scene first, because that would reject or risk the populated untitled scene.";

            return
                "[CapabilityNoProgressRecovery] AIRuntime requeued this implementation once because capability calls repeated without material progress. " +
                "Do not repeat a failed call shape or superficially vary its arguments; use the capability schema and the prior failure to make a materially different correction. " +
                outputCorrection +
                sceneInstruction +
                "\n\nPrevious stop:\n" +
                (error ?? string.Empty);
        }


        private bool HasDecompositionDrafts(
            KanbanTask parent)
        {
            return parent != null &&
                   !string.IsNullOrWhiteSpace(parent.Id) &&
                   GetDirectChildren(parent).Any(
                       child =>
                           child.IsDecompositionDraft &&
                           string.Equals(
                               child.CreationSource,
                               "create_kanban_child_task",
                               StringComparison.Ordinal));
        }


        private static bool TaskMayHaveWrittenUnityScript(
            KanbanTask task)
        {
            if (task == null)
                return false;

            string request =
                string.Join(
                    "\n",
                    task.Title ?? string.Empty,
                    task.RequestDescription ?? task.Description ?? string.Empty,
                    task.RequestAcceptanceCriteria ?? task.AcceptanceCriteria ?? string.Empty);

            bool namesScriptAsset =
                System.Text.RegularExpressions.Regex.IsMatch(
                    request,
                    @"Assets[/\\][^\r\n\""']+\.cs\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant) ||
                System.Text.RegularExpressions.Regex.IsMatch(
                    request,
                    @"\b[A-Za-z_][A-Za-z0-9_.-]*\.cs\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant) ||
                System.Text.RegularExpressions.Regex.IsMatch(
                    request,
                    @"\bC\s*#\s+scripts?\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            if (!namesScriptAsset)
                return false;

            bool titleCreatesScript =
                System.Text.RegularExpressions.Regex.IsMatch(
                    task.Title ?? string.Empty,
                    @"\b(?:create|write|author|implement|generate)\b.*\bscripts?\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            bool requestCreatesNamedScript =
                System.Text.RegularExpressions.Regex.IsMatch(
                    request,
                    @"\b(?:create|write|author|implement|generate)\b[^\r\n]{0,160}\b(?:C\s*#\s+scripts?|[A-Za-z_][A-Za-z0-9_.-]*\.cs)\b",
                    System.Text.RegularExpressions.RegexOptions.IgnoreCase |
                    System.Text.RegularExpressions.RegexOptions.CultureInvariant);

            return titleCreatesScript ||
                   requestCreatesNamedScript;
        }


        private void ImportWrittenUnityScriptsBeforeContinuing(
            KanbanTask task)
        {
            try
            {
                // Filesystem capabilities write directly to disk. Unity may not
                // notice those files until focus returns, which allowed a
                // dependent component-authoring card to run before MonoScript
                // import and compilation. Import now, after the producing run
                // and board save have completed, then hold the queue while the
                // compiler/domain reload catches up.
                AssetDatabase.Refresh(
                    ImportAssetOptions.ForceSynchronousImport);

                UnityEditor.Compilation.CompilationPipeline
                    .RequestScriptCompilation();

                _agentQueueNotBeforeEditorTime =
                    EditorApplication.timeSinceStartup +
                    1.0d;

                _statusMessage =
                    $"Imported scripts from '{task.Title}'; waiting for Unity compilation before review or dependent work";
            }
            catch (Exception exception)
            {
                Debug.LogError(
                    $"[Kanban] Could not import scripts after '{task.Title}': {exception.Message}");
            }
        }


        private bool TryRecoverFailedCompilationWithoutReload(bool isCompiling, bool isUpdating, bool compilationFailed)
        {
            // Compilation errors keep the current managed domain alive: OnEnable
            // will never run to release the reload guard. Wait until cancelled
            // attempts have drained, then let the owning agent repair its source.
            if (!_isPreparingForAssemblyReload || isCompiling || isUpdating || !compilationFailed ||
                _activeAgentRuns.Count != 0) return false;

            _isPreparingForAssemblyReload = false;
            SessionState.SetBool(ReloadResumeAgentsKey, false);
            SessionState.SetInt(ReloadPausedCountKey, 0);
            var affected = new List<string>();
            foreach (KanbanTask task in _board.Tasks.Where(task => task != null && task.Status == TaskStatus.Todo &&
                (task.NextAgentMessage ?? string.Empty).StartsWith("[AssemblyReloadResume]", StringComparison.Ordinal)))
            {
                task.ReviewStatus = "Rework Requested";
                task.ReviewFailureKind = "ImplementationDefect";
                task.NextAgentMessage = "[CompilationFailureResume] Unity compilation failed, so no assembly reload occurred. " +
                    "Your source changes remain on disk and require correction.\n\n" +
                    BuildCompilerReworkFeedback(task, EditorCompilationMessageBuffer.Snapshot()) +
                    "\nRead the exact compiler errors and referenced CompletedDependencyArtifactPaths before editing owned source. " +
                    "Correct the caller's namespace/API usage where required, then import the changed source and check compilation. " +
                    "Do not recreate completed cards or keep polling unchanged errors.";
                TouchTask(task);
                affected.Add(task.Id);
            }
            if (affected.Count > 0)
            {
                MarkDirty();
                if (_autoSave) SaveBoard(showDialog: false);
            }
            _statusMessage = "Compilation failed without an assembly reload; released the scheduler for source correction.";
            AIRuntime.Runtime.Providers.ProviderTransportDebug.WriteEvent("Harness", "COMPILATION FAILURE RECOVERY",
                new { TaskIds = affected, RunAgents = _autoRunAgents, Message = _statusMessage });
            _lastSchedulerDiagnostic = null;
            Repaint();
            return true;
        }

        private bool ResolvePendingScriptCompilationChecks()
        {
            List<KanbanTask> pending =
                _board.Tasks
                    .Where(
                        task =>
                            task != null &&
                            task.Status == TaskStatus.Review &&
                            string.Equals(
                                task.ReviewStatus,
                                "Compilation Pending",
                                StringComparison.Ordinal))
                    .ToList();

            if (pending.Count == 0)
                return false;

            if (EditorApplication.isCompiling ||
                EditorApplication.isUpdating)
            {
                return true;
            }

            bool compilationFailed =
                EditorUtility.scriptCompilationFailed;

            foreach (KanbanTask task in pending)
            {
                if (compilationFailed)
                {
                    task.Status = TaskStatus.Todo;
                    task.ReviewStatus = "Rework Requested";
                    task.ReviewFailureKind = "ImplementationDefect";
                    task.ReviewResult =
                        BuildCompilerReworkFeedback(task, EditorCompilationMessageBuffer.Snapshot());
                    task.NextAgentMessage =
                        "[ReviewFailureKind: ImplementationDefect] " + task.ReviewResult +
                        "\nInspect the failing owned source and the exact CompletedDependencyArtifactPaths exposed by get_kanban_task_context. " +
                        "Read the existing API before correcting the caller. These dependency paths grant no mutation authority. " +
                        "If correction requires a changed upstream contract or an unplanned dependency, return [PlanningKickback] with the exact contract defect. " +
                        "After changing owned source, import it and check compilation once; do not keep polling unchanged errors.";
                    task.BlockedReason = string.Empty;
                }
                else
                {
                    task.ReviewStatus = "Pending";
                    task.ReviewFailureKind = string.Empty;
                    task.ReviewResult = string.Empty;
                }

                TouchTask(task);
            }

            MarkDirty();

            if (_autoSave)
                SaveBoard(showDialog: false);

            _statusMessage =
                compilationFailed
                    ? $"Unity compilation failed; returned {pending.Count} script card(s) to Implementation"
                    : $"Unity compilation passed for {pending.Count} script card(s)";

            Repaint();
            return true;
        }


        private static string BuildCompilerReworkFeedback(KanbanTask task,
            IReadOnlyCollection<EditorCompilationMessageRecord> diagnostics)
        {
            var owned = new HashSet<string>(NormalizeArtifactPaths(task.ProducedArtifactPaths), StringComparer.OrdinalIgnoreCase);
            var errors = (diagnostics ?? Array.Empty<EditorCompilationMessageRecord>())
                .Where(diagnostic => diagnostic.Type == "Error" &&
                    owned.Contains((diagnostic.File ?? string.Empty).Replace('\\', '/')))
                .OrderBy(diagnostic => diagnostic.File, StringComparer.OrdinalIgnoreCase)
                .ThenBy(diagnostic => diagnostic.Line).Take(20)
                .Select(diagnostic => $"{diagnostic.File}({diagnostic.Line},{diagnostic.Column}): {diagnostic.Message}")
                .ToArray();
            return errors.Length > 0
                ? "Unity compiler errors in this card's owned source:\n" + string.Join("\n", errors)
                : "Unity reports compilation failure, but no captured errors map to this card's owned source. " +
                  "Inspect get_compilation_errors to identify the responsible artifact; missing diagnostics do not prove a clean compile.";
        }


        private static TaskStatus GetAgentCompletionStatus(
            TaskStatus currentStatus,
            string error)
        {
            if (!string.IsNullOrWhiteSpace(
                    error))
            {
                return IsDeterministicCompletionFailure(error)
                    ? TaskStatus.Failed
                    : TaskStatus.Crashed;
            }

            // Reaching this callback proves that an actual tracked run
            // finished. Reload preparation may already have restored its
            // persisted card to To Do, so requiring InProgress here loses a
            // valid result in that race. Normal transition validation still
            // applies in TrySetTaskStatus.
            return TaskStatus.Review;
        }


        private static bool IsDeterministicCompletionFailure(
            string error)
        {
            return !string.IsNullOrWhiteSpace(error) &&
                   (error.IndexOf(
                        "[CompilationNoProgress]",
                        StringComparison.Ordinal) >= 0 ||
                    error.IndexOf(
                        "[ActionContinuationExhausted]",
                        StringComparison.Ordinal) >= 0 ||
                    error.IndexOf(
                        "The agent claimed completion",
                        StringComparison.OrdinalIgnoreCase) >= 0 ||
                    error.IndexOf(
                        "Explicit asset deliverable",
                        StringComparison.OrdinalIgnoreCase) >= 0 ||
                    error.IndexOf(
                        "Granular decomposition",
                        StringComparison.OrdinalIgnoreCase) >= 0);
        }


        private static TaskStatus GetReviewCompletionStatus(
            TaskStatus currentStatus,
            bool passed,
            string error,
            int reviewFailureCount)
        {
            if (!string.IsNullOrWhiteSpace(
                    error))
            {
                return TaskStatus.Crashed;
            }

            if (!passed)
                return TaskStatus.Todo;

            // A successful completion is authoritative for the active review
            // run. Do not preserve a stale/transient Crashed value that may
            // have raced the queued completion between provider return and
            // main-thread application.
            return currentStatus == TaskStatus.Review ||
                   currentStatus == TaskStatus.Crashed
                ? TaskStatus.Done
                : currentStatus;
        }


        private bool TryPauseUnsupportedProviderInput(KanbanTask task, string error,
            ProviderFailureKind? failureKind, bool reviewMode, string reviewEvidence)
        {
            if (string.IsNullOrWhiteSpace(error) ||
                (failureKind != ProviderFailureKind.UnsupportedInput &&
                 !error.StartsWith("Visual verification blocked: no configured provider can accept these images.", StringComparison.Ordinal)))
                return false;

            task.Status = reviewMode ? TaskStatus.Review : TaskStatus.Todo;
            task.ReviewStatus = "Retry Available";
            task.RetryReview = reviewMode;
            task.NextAttemptAtUtc = string.Empty;
            task.RetryReason = error;
            task.AgentError = error;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = "Visual verification needs a provider configured for image input. Configure an image-capable model and endpoint, then use Resume. No automatic retry is scheduled; existing work and images are preserved.";
            task.NextAgentMessage = "[ProviderInputConfiguration] Resume the saved attempt after image input is configured. Preserve completed work and obtain the missing visual evidence; do not claim visual verification without inspecting the images.";
            if (reviewMode)
            {
                task.ReviewResult = error;
                task.ReviewEvidence = reviewEvidence ?? string.Empty;
                task.ReviewFinishedAt = DateTime.UtcNow.ToString("O");
            }
            TouchTask(task);
            MarkDirty();
            PersistSchedulerState();
            _statusMessage = $"'{task.Title}' is paused until an image-capable provider is configured";
            Repaint();
            return true;
        }


        private void CompleteReviewTask(
            KanbanTask task,
            string agentId,
            string result,
            string error,
            string reviewEvidence)
        {
            bool incompleteImplementation =
                AIRuntimeKanbanAgentRunner.ContainsUnexecutedToolMarkup(task.AgentResult);

            bool incompleteEvidence =
                HasIncompleteReviewEvidence(
                    reviewEvidence);

            bool missingArtifactEvidence =
                HasReviewEvidenceFailureKind(
                    reviewEvidence,
                    "MissingArtifact");

            bool passed =
                string.IsNullOrWhiteSpace(
                    error) &&
                !incompleteImplementation &&
                !incompleteEvidence &&
                !missingArtifactEvidence &&
                HasReviewVerdict(
                    result,
                    "PASS");

            task.ReviewFinishedAt =
                DateTime.UtcNow.ToString(
                    "O");

            task.ReviewResult =
                string.IsNullOrWhiteSpace(
                    error)
                    ? result ??
                      string.Empty
                    : error;

            task.ReviewEvidence =
                reviewEvidence ??
                string.Empty;

            string evidenceFingerprint =
                ComputeReviewEvidenceFingerprint(
                    reviewEvidence);

            task.ReviewEvidenceFingerprint =
                evidenceFingerprint;

            if (passed && TryAwaitHumanVisualReview(task)) return;

            if (IsProviderRateLimitFailure(error))
            {
                ApplyProviderCooldown(
                    task,
                    agentId,
                    error,
                    reviewMode: true);

                TouchTask(task);
                MarkDirty();

                if (_autoSave)
                {
                    SaveBoard(
                        showDialog: false);
                }

                _statusMessage =
                    $"{agentId} provider lane paused during review; '{task.Title}' will resume after cooldown";

                Debug.LogWarning(
                    "[Kanban] " +
                    _statusMessage);

                Repaint();
                return;
            }

            if (TryRecoverReviewContextLengthFailure(
                    task,
                    agentId,
                    error))
            {
                return;
            }

            bool crashed =
                !string.IsNullOrWhiteSpace(
                    error);

            string failureKind =
                passed
                    ? string.Empty
                    : !crashed &&
                      incompleteImplementation
                        ? "ImplementationDefect"
                    : !crashed &&
                      missingArtifactEvidence
                        ? "ImplementationDefect"
                    : !crashed &&
                      (task.ReviewResult ?? string.Empty).IndexOf(
                          "AIRuntime deterministic pre-review compilation preflight",
                          StringComparison.Ordinal) >= 0
                        ? "PreflightDefect"
                    : !crashed &&
                      incompleteEvidence
                        ? "EvidenceGap"
                        : ClassifyReviewFailure(
                            task.ReviewResult,
                            error);

            string upstreamTaskId = string.Empty;
            string upstreamReason = string.Empty;

            if (!passed &&
                !crashed &&
                TryResolveUpstreamCompilationBlocker(
                    task,
                    reviewEvidence,
                    out upstreamTaskId,
                    out upstreamReason))
            {
                failureKind = "UpstreamDefect";
            }

            if (!passed &&
                !crashed &&
                !string.Equals(
                    failureKind,
                    "UpstreamDefect",
                    StringComparison.Ordinal) &&
                TryRouteUpstreamArtifactDefect(
                    task,
                    task.ReviewResult,
                    out upstreamTaskId,
                    out upstreamReason))
            {
                failureKind = "UpstreamDefect";
            }

            if (!passed &&
                !crashed &&
                string.Equals(
                    failureKind,
                    "PlanningDefect",
                    StringComparison.Ordinal))
            {
                task.ReviewFailureKind = failureKind;
                RequestPlanningRevision(
                    task,
                    task.ReviewResult,
                    "Final Review");
                return;
            }

            task.ReviewFailureKind =
                failureKind;

            if (string.Equals(
                    failureKind,
                    "PreflightDefect",
                    StringComparison.Ordinal))
            {
                task.PreflightFailureCount =
                    task.PreflightFailureCount == int.MaxValue
                        ? int.MaxValue
                        : task.PreflightFailureCount + 1;
            }

            if (passed)
            {
                task.PreflightFailureCount = 0;
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = string.Empty;
                task.ReviewDependencyStateFingerprint = string.Empty;
                task.LastRejectedReviewEvidenceFingerprint = string.Empty;
                task.LastRejectedReviewOutcomeFingerprint = string.Empty;
            }

            task.ReviewFailureCount =
                GetReviewFailureCountAfterCompletion(
                    task.ReviewFailureCount,
                    passed ||
                    string.Equals(
                        failureKind,
                        "EvidenceGap",
                        StringComparison.Ordinal) ||
                    string.Equals(
                        failureKind,
                        "UpstreamDefect",
                        StringComparison.Ordinal) ||
                    string.Equals(
                        failureKind,
                        "PreflightDefect",
                        StringComparison.Ordinal),
                    crashed);

            bool evidenceRefreshRequested =
                !crashed &&
                !passed &&
                string.Equals(
                    failureKind,
                    "EvidenceGap",
                    StringComparison.Ordinal) &&
                !string.IsNullOrWhiteSpace(
                    evidenceFingerprint) &&
                !string.Equals(
                    evidenceFingerprint,
                    task.LastRejectedReviewEvidenceFingerprint,
                    StringComparison.Ordinal);

            bool deferredForEvidence =
                !crashed &&
                !passed &&
                ((string.Equals(
                      failureKind,
                      "EvidenceGap",
                      StringComparison.Ordinal) &&
                  !evidenceRefreshRequested) ||
                 string.Equals(
                     failureKind,
                     "UpstreamDefect",
                     StringComparison.Ordinal));

            string outcomeFingerprint =
                passed || crashed || deferredForEvidence
                    ? string.Empty
                    : ComputeStableFingerprint(
                        string.Join(
                            "\n---\n",
                            task.ReviewResult ?? string.Empty,
                            evidenceFingerprint,
                            failureKind ?? string.Empty));

            bool noProgress =
                !string.IsNullOrWhiteSpace(outcomeFingerprint) &&
                string.Equals(
                    outcomeFingerprint,
                    task.LastRejectedReviewOutcomeFingerprint,
                    StringComparison.Ordinal);

            bool reworkRequested =
                !crashed &&
                !passed &&
                !evidenceRefreshRequested &&
                !deferredForEvidence &&
                !noProgress;

            if (reworkRequested)
            {
                task.LastRejectedReviewOutcomeFingerprint =
                    outcomeFingerprint;
            }

            task.ReviewStatus =
                crashed
                    ? "Crashed"
                    : passed
                        ? "Passed"
                        : string.Equals(
                              failureKind,
                              "UpstreamDefect",
                              StringComparison.Ordinal)
                            ? "Blocked by Upstream"
                        : evidenceRefreshRequested
                            ? "Pending"
                        : deferredForEvidence
                            ? "Waiting for Evidence"
                        : noProgress
                            ? "Blocked by No Progress"
                        : reworkRequested
                            ? "Rework Requested"
                            : "Failed";


            if (evidenceRefreshRequested)
            {
                task.LastRejectedReviewEvidenceFingerprint =
                    evidenceFingerprint;
                task.ReviewDependencyStateFingerprint =
                    ComputeReviewDependencyStateFingerprint(task);
                task.BlockedByTaskId = string.Empty;
                task.BlockedReason = string.Empty;
                task.NextAgentMessage =
                    "[ReviewFailureKind: EvidenceGap] AIRuntime scheduled one fresh evidence-only review because the authoritative evidence snapshot changed. Preserve the completed implementation and recapture current evidence; an unchanged repeated gap will pause instead of looping.\n\n" +
                    task.ReviewResult;
            }
            else if (deferredForEvidence)
            {
                task.LastRejectedReviewEvidenceFingerprint =
                    evidenceFingerprint;
                task.ReviewDependencyStateFingerprint =
                    ComputeReviewDependencyStateFingerprint(task);
                task.BlockedByTaskId = upstreamTaskId;
                task.BlockedReason = upstreamReason;
                task.NextAgentMessage =
                    $"[ReviewFailureKind: {failureKind}] AIRuntime preserved the completed implementation and paused review until its authoritative evidence or upstream owner changes. Repeating identical evidence will not consume a retry.\n\n" +
                    task.ReviewResult;
            }
            else if (reworkRequested)
            {
                task.NextAgentMessage =
                    $"[ReviewFailureKind: {failureKind}] Review requested further work. Address every current finding before returning the card for another evidence-based review:\n\n" +
                    task.ReviewResult;
            }
            else if (noProgress)
            {
                task.BlockedReason =
                    "The implementation result, reviewer findings, and authoritative evidence were unchanged across consecutive review cycles.";
                task.NextAgentMessage =
                    "[ReviewNoProgress] AIRuntime paused this card because an identical implementation/review/evidence outcome repeated. Resume it after changing the implementation strategy or relevant external state.\n\n" +
                    task.ReviewResult;
            }

            if (reworkRequested)
            {
                KanbanTask workflowRoot = GetTaskRoot(task);
                if (workflowRoot != null)
                {
                    workflowRoot.WorkflowPhase = WorkflowPhaseImplementation;
                    TouchTask(workflowRoot);
                }
            }


            TrySetTaskStatus(
                task,
                noProgress
                    ? TaskStatus.Failed
                    : evidenceRefreshRequested
                    ? TaskStatus.Review
                    : deferredForEvidence
                    ? TaskStatus.Review
                    : GetReviewCompletionStatus(
                        task.Status,
                        passed,
                        error,
                        string.Equals(
                            failureKind,
                            "PreflightDefect",
                            StringComparison.Ordinal)
                            ? task.PreflightFailureCount
                            : task.ReviewFailureCount),
                out _statusMessage);


            TouchTask(
                task);

            MarkDirty();


            if (_autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }


            _statusMessage =
                crashed
                    ? $"{agentId} crashed while reviewing '{task.Title}'"
                    : passed
                    ? $"{agentId} approved '{task.Title}'"
                    : evidenceRefreshRequested
                        ? $"{agentId} queued one fresh evidence-only review for '{task.Title}'"
                    : deferredForEvidence
                        ? $"{agentId} deferred '{task.Title}' until evidence or upstream state changes"
                    : noProgress
                        ? $"{agentId} paused '{task.Title}' after an unchanged review outcome"
                    : reworkRequested
                        ? $"{agentId} requested further work for '{task.Title}'"
                        : $"{agentId} found unresolved issues in '{task.Title}'";


            if (!passed)
            {
                Debug.LogWarning(
                    $"[Kanban] Review failed for '{task.Title}': {task.ReviewResult}");
            }


            Repaint();
        }


        private bool TryRecoverReviewContextLengthFailure(
            KanbanTask task,
            string agentId,
            string error)
        {
            if (task == null ||
                task.PreflightFailureCount > 0 ||
                !IsProviderContextLengthFailure(error))
            {
                return false;
            }

            task.PreflightFailureCount = 1;
            task.ReviewFailureKind = "EvidenceGap";
            task.ReviewStatus = "Retry Available";
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason =
                "The provider rejected the reviewer request because it exceeded the context window. Use Re-review after checking provider availability; AIRuntime will not retry automatically.";
            task.NextAgentMessage =
                "[ReviewContextRecovery] The previous reviewer request exceeded the provider context window. " +
                "AIRuntime preserved the completed implementation and paused. Use Re-review to explicitly request a bounded authoritative evidence retry; do not repeat mutations.";

            TrySetTaskStatus(
                task,
                TaskStatus.Review,
                out _statusMessage);

            TouchTask(task);
            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }

            _statusMessage =
                $"{agentId} exceeded the review context window for '{task.Title}'; review paused with Retry Available";

            Debug.LogWarning(
                "[Kanban] " +
                _statusMessage);

            Repaint();
            return true;
        }


        private static bool IsProviderContextLengthFailure(
            string error)
        {
            if (string.IsNullOrWhiteSpace(error))
                return false;

            return error.IndexOf(
                       "maximum context length",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   error.IndexOf(
                       "context_length_exceeded",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   (error.IndexOf(
                        "context window",
                        StringComparison.OrdinalIgnoreCase) >= 0 &&
                    error.IndexOf(
                        "exceed",
                        StringComparison.OrdinalIgnoreCase) >= 0);
        }


        private static string BuildProviderResumeGuidance(string error)
        {
            bool authenticationFailed = !string.IsNullOrWhiteSpace(error) &&
                (error.IndexOf("[Authentication]", StringComparison.OrdinalIgnoreCase) >= 0 ||
                 error.IndexOf("HTTP 401", StringComparison.OrdinalIgnoreCase) >= 0 ||
                 error.IndexOf("HTTP 403", StringComparison.OrdinalIgnoreCase) >= 0);

            return authenticationFailed
                ? "Provider attempts were exhausted, and at least one provider rejected authentication or access. " +
                  "Check the named provider's credentials and access, or select a working provider, before resuming. " +
                  "AIRuntime preserved this attempt and its artifacts; waiting alone will not repair an authentication failure."
                : "The provider returned no usable completion, timed out, or reported temporary unavailability. " +
                  "AIRuntime preserved this attempt and paused it for an explicit resume.";
        }


        private static bool IsTransientProviderAvailabilityFailure(
            string error)
        {
            if (string.IsNullOrWhiteSpace(error))
                return false;

            bool exhaustedInvalidResponse =
                error.IndexOf(
                    "All eligible providers failed",
                    StringComparison.OrdinalIgnoreCase) >= 0 &&
                error.IndexOf(
                    "[InvalidResponse]",
                    StringComparison.OrdinalIgnoreCase) >= 0;

            bool interruptedSuccessfulStream =
                error.IndexOf(
                    "Failed to receive data",
                    StringComparison.OrdinalIgnoreCase) >= 0 ||
                error.IndexOf(
                    "OPENROUTER PROCESSING",
                    StringComparison.OrdinalIgnoreCase) >= 0;

            bool directSuccessfulHttpStreamFailure =
                interruptedSuccessfulStream &&
                error.IndexOf(
                    "HTTP 200",
                    StringComparison.OrdinalIgnoreCase) >= 0;

            return error.IndexOf(
                       "neither assistant text nor tool calls",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   error.IndexOf(
                       "HTTP 503",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   error.IndexOf(
                       "Service Unavailable",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   error.IndexOf(
                       "model is currently unavailable",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                    (error.IndexOf(
                         "All eligible providers failed",
                         StringComparison.OrdinalIgnoreCase) >= 0 &&
                     error.IndexOf(
                         "did not respond within",
                         StringComparison.OrdinalIgnoreCase) >= 0) ||
                    directSuccessfulHttpStreamFailure ||
                    (exhaustedInvalidResponse &&
                     interruptedSuccessfulStream);
        }


        private static bool IsTransientProviderAvailabilityFailure(
            string error,
            ProviderFailureKind? providerFailureKind)
        {
            if (providerFailureKind == ProviderFailureKind.EmptyResponse ||
                providerFailureKind == ProviderFailureKind.Timeout ||
                providerFailureKind == ProviderFailureKind.Unavailable ||
                providerFailureKind == ProviderFailureKind.ModelUnavailable)
            {
                return true;
            }

            return IsTransientProviderAvailabilityFailure(
                error);
        }


        private static bool IsProviderLifecycleHandoff(
            string nextAgentMessage)
        {
            return !string.IsNullOrWhiteSpace(nextAgentMessage) &&
                   (nextAgentMessage.StartsWith(
                        "[ProviderCooldown",
                        StringComparison.Ordinal) ||
                    nextAgentMessage.StartsWith(
                        "[ProviderRetryAvailable]",
                        StringComparison.Ordinal));
        }


        private static bool IsProviderRateLimitFailure(
            string error)
        {
            if (string.IsNullOrWhiteSpace(error))
                return false;


            return error.IndexOf(
                       "HTTP 429",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   error.IndexOf(
                       "[RateLimited]",
                       StringComparison.OrdinalIgnoreCase) >= 0 ||
                   error.IndexOf(
                       "exceeded the rate limit",
                       StringComparison.OrdinalIgnoreCase) >= 0;
        }


        private static bool IsProviderRateLimitFailure(
            string error,
            ProviderFailureKind? providerFailureKind)
        {
            return providerFailureKind ==
                       ProviderFailureKind.RateLimited ||
                   IsProviderRateLimitFailure(
                       error);
        }


        private static TimeSpan ResolveProviderCooldownDuration(
            string error)
        {
            Match retryAfter =
                Regex.Match(
                    error ?? string.Empty,
                    @"Retry after\s+(?<seconds>\d+(?:\.\d+)?)\s+seconds?\.?",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant);


            if (retryAfter.Success &&
                double.TryParse(
                    retryAfter.Groups["seconds"].Value,
                    NumberStyles.Float,
                    CultureInfo.InvariantCulture,
                    out double seconds) &&
                !double.IsNaN(seconds) &&
                !double.IsInfinity(seconds) &&
                seconds >= 0)
            {
                return TimeSpan.FromSeconds(
                    Math.Max(
                        1,
                        seconds));
            }


            return DefaultProviderRateLimitCooldown;
        }


        private void ApplyProviderCooldown(
            KanbanTask task,
            string agentId,
            string error,
            bool reviewMode)
        {
            if (task == null)
                return;


            string normalizedAgentId =
                string.IsNullOrWhiteSpace(agentId)
                    ? task.AssignedAgent?.Trim() ?? string.Empty
                    : agentId.Trim();

            _board.ProviderCooldowns =
                _board.ProviderCooldowns ??
                new List<ProviderCooldownState>();

            ProviderCooldownState state =
                _board.ProviderCooldowns.FirstOrDefault(
                    candidate =>
                        candidate != null &&
                        string.Equals(
                            candidate.AgentId,
                            normalizedAgentId,
                            StringComparison.Ordinal));


            if (state == null)
            {
                state =
                    new ProviderCooldownState
                    {
                        AgentId =
                            normalizedAgentId
                    };

                _board.ProviderCooldowns.Add(
                    state);
            }

            DateTimeOffset now =
                DateTimeOffset.UtcNow;

            bool continuesRecentStreak =
                DateTimeOffset.TryParse(
                    state.LastFailureUtc,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AssumeUniversal |
                    DateTimeStyles.AdjustToUniversal,
                    out DateTimeOffset previousFailure) &&
                now - previousFailure <=
                    ProviderRateLimitStreakResetWindow;

            state.ConsecutiveFailureCount =
                continuesRecentStreak
                    ? Math.Max(
                        1,
                        state.ConsecutiveFailureCount) + 1
                    : 1;

            TimeSpan duration =
                ApplyProviderCooldownBackoff(
                    ResolveProviderCooldownDuration(
                        error),
                    state.ConsecutiveFailureCount);

            DateTimeOffset until =
                now + duration;

            if (DateTimeOffset.TryParse(
                    state.UntilUtc,
                    CultureInfo.InvariantCulture,
                    DateTimeStyles.AssumeUniversal |
                    DateTimeStyles.AdjustToUniversal,
                    out DateTimeOffset existingUntil) &&
                existingUntil > until)
            {
                until =
                    existingUntil;
            }


            state.ProviderName =
                ExtractRateLimitedProviderName(
                    error);

            state.UntilUtc =
                until.UtcDateTime.ToString(
                    "O",
                    CultureInfo.InvariantCulture);

            state.Reason =
                error ??
                string.Empty;

            state.LastFailureUtc =
                now.UtcDateTime.ToString(
                    "O",
                    CultureInfo.InvariantCulture);

            state.Released =
                false;

            state.ReleasedUtc =
                string.Empty;


            task.Status =
                reviewMode
                    ? TaskStatus.Review
                    : TaskStatus.Todo;

            task.ReviewStatus =
                ProviderCooldownReviewStatus;

            task.BlockedByTaskId =
                string.Empty;

            task.BlockedReason =
                $"Provider rate limit attempt {state.ConsecutiveFailureCount} is paused until {until.ToLocalTime():yyyy-MM-dd HH:mm:ss zzz}. AIRuntime applied durable backoff without discarding card artifacts.";

            task.NextAgentMessage =
                $"[ProviderCooldown] Provider rate-limit attempt {state.ConsecutiveFailureCount} entered durable backoff. Resume from persisted execution memory and inspect existing artifacts before creating or mutating anything; do not restart completed work.";
        }


        private static TimeSpan ApplyProviderCooldownBackoff(
            TimeSpan requestedDuration,
            int consecutiveFailureCount)
        {
            int exponent =
                Math.Max(
                    0,
                    Math.Min(
                        10,
                        consecutiveFailureCount - 1));

            double multiplier =
                Math.Pow(
                    2,
                    exponent);

            TimeSpan backoffFloor =
                TimeSpan.FromTicks(
                    Math.Min(
                        MaximumProviderRateLimitBackoff.Ticks,
                        (long)Math.Min(
                            long.MaxValue,
                            DefaultProviderRateLimitCooldown.Ticks * multiplier)));

            return requestedDuration > backoffFloor
                ? requestedDuration
                : backoffFloor;
        }


        private bool IsAgentProviderCoolingDown(
            string agentId,
            out ProviderCooldownState state)
        {
            state =
                null;

            if (string.IsNullOrWhiteSpace(agentId) ||
                _board?.ProviderCooldowns == null)
            {
                return false;
            }


            state =
                _board.ProviderCooldowns.FirstOrDefault(
                    candidate =>
                        candidate != null &&
                        string.Equals(
                            candidate.AgentId,
                            agentId.Trim(),
                            StringComparison.Ordinal));


            return state != null &&
                   !state.Released &&
                   DateTimeOffset.TryParse(
                       state.UntilUtc,
                       CultureInfo.InvariantCulture,
                       DateTimeStyles.AssumeUniversal |
                       DateTimeStyles.AdjustToUniversal,
                       out DateTimeOffset until) &&
                   until > DateTimeOffset.UtcNow;
        }


        private void ReleaseExpiredProviderCooldowns()
        {
            if (_board?.ProviderCooldowns == null ||
                _board.ProviderCooldowns.Count == 0)
            {
                return;
            }


            DateTimeOffset now =
                DateTimeOffset.UtcNow;

            HashSet<string> releasedAgentIds =
                new HashSet<string>(
                    StringComparer.Ordinal);


            for (int index =
                     _board.ProviderCooldowns.Count - 1;
                 index >= 0;
                 index--)
            {
                ProviderCooldownState state =
                    _board.ProviderCooldowns[index];

                if (state == null ||
                    string.IsNullOrWhiteSpace(
                        state.AgentId))
                {
                    _board.ProviderCooldowns.RemoveAt(
                        index);
                    continue;
                }

                bool active =
                    !state.Released &&
                    DateTimeOffset.TryParse(
                        state.UntilUtc,
                        CultureInfo.InvariantCulture,
                        DateTimeStyles.AssumeUniversal |
                        DateTimeStyles.AdjustToUniversal,
                        out DateTimeOffset until) &&
                    until > now;

                if (active)
                    continue;

                if (state != null &&
                    state.Released)
                {
                    continue;
                }


                if (!string.IsNullOrWhiteSpace(
                        state?.AgentId))
                {
                    releasedAgentIds.Add(
                        state.AgentId.Trim());
                }

                if (state != null)
                {
                    state.Released =
                        true;

                    state.ReleasedUtc =
                        now.UtcDateTime.ToString(
                            "O",
                            CultureInfo.InvariantCulture);
                }
            }


            if (releasedAgentIds.Count == 0)
                return;


            int resumedTaskCount =
                0;

            foreach (KanbanTask task in _board.Tasks)
            {
                if (task == null ||
                    !releasedAgentIds.Contains(
                        task.AssignedAgent?.Trim() ?? string.Empty) ||
                    !string.Equals(
                        task.ReviewStatus,
                        ProviderCooldownReviewStatus,
                        StringComparison.Ordinal))
                {
                    continue;
                }


                task.ReviewStatus =
                    task.Status == TaskStatus.Review
                        ? "Pending"
                        : string.Empty;

                task.AgentError =
                    string.Empty;

                task.ReviewResult =
                    string.Empty;

                task.BlockedByTaskId =
                    string.Empty;

                task.BlockedReason =
                    string.Empty;

                task.NextAgentMessage =
                    "[ProviderCooldownElapsed] Provider cooldown elapsed. Resume from persisted execution memory, inspect existing artifacts first, and complete only the remaining work.";

                TouchTask(task);
                resumedTaskCount++;
            }


            MarkDirty();

            if (_autoSave)
            {
                SaveBoard(
                    showDialog: false);
            }

            _statusMessage =
                resumedTaskCount > 0
                    ? $"Provider cooldown elapsed; resumed {resumedTaskCount} paused card(s)"
                    : "Provider cooldown elapsed";
        }


        private static string ExtractRateLimitedProviderName(
            string error)
        {
            Match namedAttempt =
                Regex.Match(
                    error ?? string.Empty,
                    @"'(?<provider>[^']+)'\s*\[RateLimited\]",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant);


            return namedAttempt.Success
                ? namedAttempt.Groups["provider"].Value.Trim()
                : "Provider";
        }


        private static int GetReviewFailureCountAfterCompletion(
            int currentFailureCount,
            bool passed,
            bool crashed)
        {
            if (passed || crashed)
                return currentFailureCount;

            return currentFailureCount == int.MaxValue
                ? int.MaxValue
                : currentFailureCount + 1;
        }


        private static bool HasIncompleteReviewEvidence(
            string reviewEvidence)
        {
            return !string.IsNullOrWhiteSpace(reviewEvidence) &&
                   reviewEvidence.IndexOf(
                       "AIRUNTIME_REVIEW_EVIDENCE_INCOMPLETE",
                       StringComparison.Ordinal) >= 0;
        }


        private static bool HasReviewEvidenceFailureKind(
            string reviewEvidence,
            string failureKind)
        {
            if (string.IsNullOrWhiteSpace(reviewEvidence) ||
                string.IsNullOrWhiteSpace(failureKind))
            {
                return false;
            }

            try
            {
                JToken evidence =
                    JToken.Parse(reviewEvidence);

                return new[] { evidence }
                    .Concat(
                        (evidence as JContainer)?.Descendants() ??
                        Enumerable.Empty<JToken>())
                    .OfType<JProperty>()
                    .Any(
                        property =>
                            string.Equals(
                                property.Name,
                                "FailureKind",
                                StringComparison.OrdinalIgnoreCase) &&
                            string.Equals(
                                property.Value?.Value<string>(),
                                failureKind,
                                StringComparison.OrdinalIgnoreCase));
            }
            catch (JsonException)
            {
                return false;
            }
        }


        private static string ComputeReviewEvidenceFingerprint(
            string reviewEvidence)
        {
            if (string.IsNullOrWhiteSpace(reviewEvidence))
                return string.Empty;

            string stableEvidence = reviewEvidence.Trim();

            try
            {
                JToken token = JToken.Parse(stableEvidence);

                foreach (JProperty property in
                    ((token as JContainer)?.Descendants() ??
                     Enumerable.Empty<JToken>())
                        .OfType<JProperty>()
                        .Where(
                            property =>
                                string.Equals(
                                    property.Name,
                                    "CapturedAtUtc",
                                    StringComparison.OrdinalIgnoreCase) ||
                                string.Equals(
                                    property.Name,
                                    "CapturedSinceUtc",
                                    StringComparison.OrdinalIgnoreCase))
                        .ToList())
                {
                    property.Remove();
                }

                stableEvidence =
                    token.ToString(Formatting.None);
            }
            catch (JsonReaderException)
            {
                // Non-JSON evidence is still fingerprinted deterministically.
            }

            return ComputeStableFingerprint(stableEvidence);
        }


        private static string ComputeStableFingerprint(
            string value)
        {
            using (var sha256 =
                   System.Security.Cryptography.SHA256.Create())
            {
                byte[] bytes =
                    Encoding.UTF8.GetBytes(value ?? string.Empty);

                return string.Concat(
                    sha256.ComputeHash(bytes)
                        .Select(item => item.ToString("x2")));
            }
        }


        private bool TryResolveUpstreamCompilationBlocker(
            KanbanTask reviewedTask,
            string reviewEvidence,
            out string blockerTaskId,
            out string reason)
        {
            blockerTaskId = string.Empty;
            reason = string.Empty;

            if (string.IsNullOrWhiteSpace(reviewEvidence))
                return false;

            if (!Regex.IsMatch(
                    reviewedTask?.ReviewResult ?? string.Empty,
                    @"\b(?:compile|compilation|compiler|diagnostic|error)\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return false;
            }

            try
            {
                JObject bundle = JObject.Parse(reviewEvidence);
                JObject compilation =
                    bundle["Compilation"] as JObject;

                if (compilation == null ||
                    !string.Equals(
                        compilation["Scope"]?.Value<string>(),
                        "TargetArtifacts",
                        StringComparison.Ordinal) ||
                    (compilation["ErrorCount"]?.Value<int>() ?? 0) != 0 ||
                    (compilation["UnrelatedErrorCount"]?.Value<int>() ?? 0) == 0)
                {
                    return false;
                }

                string diagnosticFile =
                    (compilation["UnrelatedDiagnostics"] as JArray ?? new JArray())
                        .OfType<JObject>()
                        .Where(
                            diagnostic =>
                                string.Equals(
                                    diagnostic["Type"]?.Value<string>(),
                                    "Error",
                                    StringComparison.OrdinalIgnoreCase))
                        .Select(
                            diagnostic =>
                                diagnostic["File"]?.Value<string>()
                                    ?.Replace('\\', '/'))
                        .FirstOrDefault(
                            file => !string.IsNullOrWhiteSpace(file));

                KanbanTask owner =
                    _board.Tasks.FirstOrDefault(
                        task =>
                            task != null &&
                            !ReferenceEquals(task, reviewedTask) &&
                            ((task.ProducedArtifactPaths ?? new List<string>())
                                 .Any(
                                     path =>
                                         string.Equals(
                                             path?.Replace('\\', '/'),
                                             diagnosticFile,
                                             StringComparison.OrdinalIgnoreCase)) ||
                             (!string.IsNullOrWhiteSpace(diagnosticFile) &&
                              string.Join(
                                      "\n",
                                      task.Title ?? string.Empty,
                                      task.Description ?? string.Empty,
                                      task.AcceptanceCriteria ?? string.Empty)
                                  .IndexOf(
                                      diagnosticFile,
                                      StringComparison.OrdinalIgnoreCase) >= 0)));

                blockerTaskId = owner?.Id ?? string.Empty;
                reason =
                    string.IsNullOrWhiteSpace(diagnosticFile)
                        ? "Compiler errors exist only outside this card's owned artifacts."
                        : $"Compiler error is owned by '{diagnosticFile}', not this card.";

                return true;
            }
            catch (JsonReaderException)
            {
                return false;
            }
        }


        private bool TryRouteUpstreamArtifactDefect(
            KanbanTask reviewedTask,
            string reviewResult,
            out string blockerTaskId,
            out string reason)
        {
            blockerTaskId = string.Empty;
            reason = string.Empty;

            if (reviewedTask == null ||
                string.IsNullOrWhiteSpace(reviewResult))
            {
                return false;
            }

            Match marker =
                Regex.Match(
                    reviewResult,
                    @"\[UpstreamArtifactDefect:\s*(?<path>Assets/[^\]\r\n]+)\]",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant);

            string artifactPath;
            if (marker.Success)
            {
                artifactPath = marker.Groups["path"].Value.Trim().Replace('\\', '/');
            }
            else
            {
                // A failed review may identify its owner precisely while omitting
                // the routing marker. Accept only one declared input whose
                // canonical owner ID and unambiguous file name both appear.
                var inputs = new HashSet<string>((reviewedTask.ConsumedArtifactPaths ?? new List<string>())
                    .Select(path => (path ?? string.Empty).Trim().Replace('\\', '/')), StringComparer.OrdinalIgnoreCase);
                var matches = _board.Tasks.Where(candidate => candidate != null && candidate != reviewedTask &&
                        !string.IsNullOrWhiteSpace(candidate.Id) &&
                        Regex.IsMatch(reviewResult, @"(?<![A-Za-z0-9_-])" + Regex.Escape(candidate.Id) + @"(?![A-Za-z0-9_-])"))
                    .SelectMany(candidate => (candidate.ProducedArtifactPaths ?? new List<string>())
                        .Select(path => (path ?? string.Empty).Trim().Replace('\\', '/')))
                    .Where(path => inputs.Contains(path) && !string.IsNullOrWhiteSpace(Path.GetFileName(path)) &&
                        Regex.IsMatch(reviewResult, @"(?<![A-Za-z0-9_.-])" + Regex.Escape(Path.GetFileName(path)) + @"(?![A-Za-z0-9_-]|\.[A-Za-z0-9_-])", RegexOptions.IgnoreCase))
                    .Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
                if (matches.Length != 1) return false;
                artifactPath = matches[0];
                // An ambiguous basename must not pick a sibling by accident.
                if (inputs.Count(path => string.Equals(Path.GetFileName(path), Path.GetFileName(artifactPath), StringComparison.OrdinalIgnoreCase)) != 1)
                    return false;
            }

            bool isDeclaredInput =
                (reviewedTask.ConsumedArtifactPaths ?? new List<string>())
                    .Any(
                        path =>
                            string.Equals(
                                path?.Trim().Replace('\\', '/'),
                                artifactPath,
                                StringComparison.OrdinalIgnoreCase));

            bool isLocalOutput =
                (reviewedTask.ProducedArtifactPaths ?? new List<string>())
                    .Any(
                        path =>
                            string.Equals(
                                path?.Trim().Replace('\\', '/'),
                                artifactPath,
                                StringComparison.OrdinalIgnoreCase));

            bool isLocallyMutableScene =
                (reviewedTask.MutatedResourceKeys ?? new List<string>())
                    .Any(
                        key =>
                            string.Equals(
                                key?.Trim().Replace('\\', '/'),
                                "scene:" + artifactPath,
                                StringComparison.OrdinalIgnoreCase));

            if (!isDeclaredInput ||
                isLocalOutput ||
                isLocallyMutableScene)
            {
                return false;
            }

            KanbanTask[] owners =
                _board.Tasks.Where(
                    candidate =>
                        candidate != null &&
                        !ReferenceEquals(candidate, reviewedTask) &&
                        (candidate.ProducedArtifactPaths ?? new List<string>())
                            .Any(
                                path =>
                                    string.Equals(
                                        path?.Trim().Replace('\\', '/'),
                                        artifactPath,
                                        StringComparison.OrdinalIgnoreCase))).ToArray();

            if (owners.Length != 1) return false;
            KanbanTask owner = owners[0];

            if (owner == null ||
                owner.Status == TaskStatus.InProgress)
            {
                return false;
            }

            owner.Status = TaskStatus.Todo;
            owner.AgentError = string.Empty;
            owner.ReviewStatus = "Rework Requested";
            owner.ReviewFailureKind = "ImplementationDefect";
            owner.ReviewResult = reviewResult.Trim();
            owner.ReviewEvidence = string.Empty;
            owner.ReviewStartedAt = string.Empty;
            owner.ReviewFinishedAt = string.Empty;
            owner.BlockedByTaskId = string.Empty;
            owner.BlockedReason = string.Empty;
            owner.ReviewDependencyStateFingerprint = string.Empty;
            owner.LastRejectedReviewEvidenceFingerprint = string.Empty;
            owner.LastRejectedReviewOutcomeFingerprint = string.Empty;
            owner.NextAgentMessage =
                "[UpstreamArtifactDefect] Downstream card '" +
                reviewedTask.Title +
                "' found a verified integration defect in this card's owned artifact '" +
                artifactPath +
                "'. Preserve valid work, correct only this owned artifact, verify its original acceptance criteria plus the downstream incompatibility, and return it for fresh review.\n\n" +
                reviewResult.Trim();

            KanbanTask workflowRoot = GetTaskRoot(reviewedTask);
            if (workflowRoot != null)
            {
                RestoreImplementationWorkflowRoot(
                    workflowRoot,
                    owner);
                TouchTask(workflowRoot);
            }

            TouchTask(owner);
            blockerTaskId = owner.Id ?? string.Empty;
            reason =
                $"Review found a defect in consumed artifact '{artifactPath}', owned by card '{owner.Title}'. AIRuntime reopened that owner before retrying integration.";
            return true;
        }


        private void RestoreImplementationWorkflowRoot(
            KanbanTask root,
            KanbanTask activeReworkTask)
        {
            if (root == null)
                return;

            root.WorkflowPhase = WorkflowPhaseImplementation;
            root.PlanningKickbackReason = string.Empty;
            root.PlanningKickbackFingerprint = string.Empty;

            if (ReferenceEquals(root, activeReworkTask) ||
                !string.Equals(
                    root.ReviewFailureKind,
                    "PlanningDefect",
                    StringComparison.OrdinalIgnoreCase))
            {
                return;
            }

            root.Status = TaskStatus.Todo;
            root.AgentError = string.Empty;
            root.ReviewStatus = string.Empty;
            root.ReviewResult = string.Empty;
            root.ReviewEvidence = string.Empty;
            root.ReviewFailureKind = string.Empty;
            root.ReviewStartedAt = string.Empty;
            root.ReviewFinishedAt = string.Empty;
            root.BlockedByTaskId = string.Empty;
            root.BlockedReason = string.Empty;
            root.ReviewDependencyStateFingerprint = string.Empty;
            root.LastRejectedReviewEvidenceFingerprint = string.Empty;
            root.LastRejectedReviewOutcomeFingerprint = string.Empty;
            root.NextAgentMessage = string.Empty;
        }


        private static string ClassifyReviewFailure(
            string result,
            string error)
        {
            if (!string.IsNullOrWhiteSpace(error))
                return "ExecutionFailure";

            string normalized =
                result ??
                string.Empty;

            if (Regex.IsMatch(
                    normalized,
                    @"\[ReviewFailureKind:\s*(?:PlanningDefect|CoordinationDefect)\]",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return "PlanningDefect";
            }

            if (Regex.IsMatch(
                    normalized,
                    @"component\s+inspection\s+evidence\s+is\s+unavailable[\s\S]{0,220}(?:verdict\s+must\s+be\s+FAIL|mandates?\s+FAIL)|governing\s+review\s+rule\s+mandates?\s+FAIL",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return "EvidenceGap";
            }

            if (Regex.IsMatch(
                    normalized,
                    @"\b(?:direct\s+contradiction|does\s+not\s+match|is\s+wrong|not\s+public|private,?\s+not\s+public)\b|\bmissing\s+(?:required\s+)?(?:asset|component|file|folder|GameObject|object|property|scene|script)\b|\bexpected\b[^.\r\n]{0,160}\bbut\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return "ImplementationDefect";
            }

            if (Regex.IsMatch(
                    normalized,
                    @"\b(?:evidence|inspection|snapshot|read-back|persistence)\b[\s\S]{0,100}\b(?:unavailable|missing|unsupported|unverified|not\s+provided|cannot\s+verify)\b|\bno\s+authoritative\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return "EvidenceGap";
            }

            if (Regex.IsMatch(
                    normalized,
                    @"\b(?:decomposition|child\s+cards?|descendant\s+gate|parent\s+attachment|coordination)\b",
                    RegexOptions.IgnoreCase |
                    RegexOptions.CultureInvariant))
            {
                return "PlanningDefect";
            }

            return "ImplementationDefect";
        }


        private static bool HasReviewVerdict(
            string result,
            string verdict)
        {
            if (string.IsNullOrWhiteSpace(
                    result))
            {
                return false;
            }


            bool foundRequestedVerdict =
                false;

            bool foundConflictingVerdict =
                false;


            foreach (
                string line
                in result.Split(
                    new[]
                    {
                        "\r\n",
                        "\n",
                        "\r"
                    },
                    StringSplitOptions.RemoveEmptyEntries))
            {
                string[] parts =
                    line.Trim().Split(
                        new[]
                        {
                            ':'
                        },
                        2);


                if (parts.Length !=
                    2)
                {
                    continue;
                }


                string marker =
                    parts[0]
                        .Trim()
                        .ToUpperInvariant();


                if (GetEditDistance(
                        marker,
                        "KANBAN_REVIEW") >
                    2)
                {
                    continue;
                }


                string parsedVerdict =
                    parts[1].Trim();


                if (string.Equals(
                    parsedVerdict,
                    verdict,
                    StringComparison.OrdinalIgnoreCase))
                {
                    foundRequestedVerdict =
                        true;
                }
                else if (
                    string.Equals(
                        parsedVerdict,
                        "PASS",
                        StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(
                        parsedVerdict,
                        "FAIL",
                        StringComparison.OrdinalIgnoreCase))
                {
                    foundConflictingVerdict =
                        true;
                }
            }


            return foundRequestedVerdict &&
                   !foundConflictingVerdict;
        }


        private static int GetEditDistance(
            string left,
            string right)
        {
            if (string.IsNullOrEmpty(
                    left))
            {
                return right?.Length ??
                       0;
            }


            if (string.IsNullOrEmpty(
                    right))
            {
                return left.Length;
            }


            int[] previous =
                Enumerable
                    .Range(
                        0,
                        right.Length +
                        1)
                    .ToArray();

            int[] current =
                new int[
                    right.Length +
                    1];


            for (int leftIndex = 1;
                 leftIndex <=
                 left.Length;
                 leftIndex++)
            {
                current[0] =
                    leftIndex;


                for (int rightIndex = 1;
                     rightIndex <=
                     right.Length;
                     rightIndex++)
                {
                    int substitutionCost =
                        left[leftIndex -
                             1] ==
                        right[rightIndex -
                              1]
                            ? 0
                            : 1;

                    current[rightIndex] =
                        Math.Min(
                            Math.Min(
                                current[rightIndex -
                                        1] +
                                1,
                                previous[rightIndex] +
                                1),
                            previous[rightIndex -
                                     1] +
                            substitutionCost);
                }


                int[] swap =
                    previous;

                previous =
                    current;

                current =
                    swap;
            }


            return previous[
                right.Length];
        }


        private void SelectTask(
            KanbanTask task)
        {
            _selectedTaskId =
                task.Id;

            _taskDescriptionScroll =
                Vector2.zero;

            _acceptanceCriteriaScroll =
                Vector2.zero;

            _agentOutputScroll =
                Vector2.zero;

            _nextAgentMessageScroll =
                Vector2.zero;

            AIRuntimeKanbanTaskDetailsWindow.Open(
                this,
                task.Id,
                task.Title);
        }


        private KanbanTask GetSelectedTask()
        {
            return FindTask(
                _selectedTaskId);
        }


        private KanbanTask FindTask(
            string taskId)
        {
            if (string.IsNullOrWhiteSpace(
                    taskId))
            {
                return null;
            }


            return _board.Tasks.FirstOrDefault(
                task =>
                    string.Equals(
                        task.Id,
                        taskId,
                        StringComparison.Ordinal));
        }


        private List<KanbanTask> GetVisibleTasks(
            TaskStatus status)
        {
            IEnumerable<KanbanTask> tasks =
                _board.Tasks.Where(
                    task =>
                        task.Status ==
                        status);


            if (!string.IsNullOrWhiteSpace(
                    _search))
            {
                string search =
                    _search.Trim();


                tasks =
                    tasks.Where(
                        task =>
                            Contains(
                                task.Title,
                                search) ||
                            Contains(
                                task.Description,
                                search) ||
                            Contains(
                                task.AssignedAgent,
                                search) ||
                            Contains(
                                task.Tags,
                                search));
            }


            return tasks.ToList();
        }


        private string DrawAssignedAgentPopup(
            string currentAgentId)
        {
            if (_agentOptions == null)
            {
                RefreshAgentOptions();
            }


            List<AgentOption> options =
                new List<AgentOption>(
                    _agentOptions);

            string normalizedCurrent =
                currentAgentId?.Trim() ??
                string.Empty;


            if (!string.IsNullOrEmpty(
                    normalizedCurrent) &&
                options.All(
                    option =>
                        !string.Equals(
                            option.Id,
                            normalizedCurrent,
                            StringComparison.Ordinal)))
            {
                options.Add(
                    new AgentOption
                    {
                        Id =
                            normalizedCurrent,

                        Label =
                            $"{normalizedCurrent} (saved assignment)"
                    });
            }


            int selectedIndex =
                options.FindIndex(
                    option =>
                        string.Equals(
                            option.Id,
                            normalizedCurrent,
                            StringComparison.Ordinal));


            if (selectedIndex < 0)
            {
                selectedIndex =
                    0;
            }


            int nextIndex =
                EditorGUILayout.Popup(
                    "Assigned agent",
                    selectedIndex,
                    options
                        .Select(
                            option =>
                                option.Label)
                        .ToArray());


            return options[
                    Mathf.Clamp(
                        nextIndex,
                        0,
                        options.Count - 1)]
                .Id;
        }


        private string DrawParentTaskPopup(
            KanbanTask task)
        {
            List<KanbanTask> candidates =
                _board.Tasks
                    .Where(
                        candidate =>
                            candidate != null &&
                            candidate != task &&
                            !IsDescendantOf(
                                candidate,
                                task))
                    .OrderBy(
                        candidate =>
                            candidate.Title)
                    .ToList();

            List<string> ids =
                new List<string>
                {
                    string.Empty
                };

            List<string> labels =
                new List<string>
                {
                    "None"
                };

            foreach (KanbanTask candidate in candidates)
            {
                ids.Add(
                    candidate.Id);

                labels.Add(
                    candidate.Title);
            }

            int currentIndex =
                ids.FindIndex(
                    id =>
                        string.Equals(
                            id,
                            task.ParentTaskId,
                            StringComparison.Ordinal));

            if (currentIndex < 0)
            {
                currentIndex = 0;
            }

            int nextIndex =
                EditorGUILayout.Popup(
                    "Parent task",
                    currentIndex,
                    labels.ToArray());

            string parentId =
                ids[Mathf.Clamp(
                    nextIndex,
                    0,
                    ids.Count - 1)];

            if (!string.Equals(
                    parentId,
                    task.ParentTaskId,
                    StringComparison.Ordinal) &&
                !string.IsNullOrWhiteSpace(
                    parentId))
            {
                KanbanTask parent =
                    FindTask(
                        parentId);

                TrySetTaskStatus(
                    parent,
                    TaskStatus.Todo,
                    out _statusMessage);
            }

            return parentId;
        }


        private bool IsDescendantOf(
            KanbanTask candidate,
            KanbanTask potentialAncestor)
        {
            if (candidate == null ||
                potentialAncestor == null)
            {
                return false;
            }

            HashSet<string> visited =
                new HashSet<string>(
                    StringComparer.Ordinal);

            KanbanTask current =
                candidate;

            while (current != null &&
                   !string.IsNullOrWhiteSpace(
                       current.ParentTaskId) &&
                   visited.Add(
                       current.Id))
            {
                if (string.Equals(
                        current.ParentTaskId,
                        potentialAncestor.Id,
                        StringComparison.Ordinal))
                {
                    return true;
                }

                current =
                    FindTask(
                        current.ParentTaskId);
            }

            return false;
        }


        private void RefreshAgentOptions()
        {
            List<AgentOption> options =
                new List<AgentOption>
                {
                    new AgentOption
                    {
                        Id =
                            string.Empty,

                        Label =
                            "Unassigned"
                    },
                };


            string root =
                Path.Combine(
                    Environment.GetFolderPath(
                        Environment.SpecialFolder.MyDocuments),
                    "AIRuntime");


            if (Directory.Exists(
                    root))
            {
                try
                {
                    foreach (
                        string path
                        in Directory.GetFiles(
                            root,
                            "*.json",
                            SearchOption.AllDirectories))
                    {
                        TryAddSavedAgent(
                            options,
                            path);
                    }
                }
                catch (IOException)
                {
                    // Keep the built-in list when saved-agent
                    // discovery is temporarily unavailable.
                }
                catch (UnauthorizedAccessException)
                {
                    // Keep the built-in list when a child folder
                    // cannot be inspected.
                }
            }


            _agentOptions =
                options;
        }


        internal void RefreshKanbanAgents()
        {
            RefreshAgentOptions();
        }


        internal IReadOnlyList<AIRuntimeKanbanGraphNode>
            GetKanbanGraphNodes()
        {
            if (_board?.Tasks ==
                null)
            {
                return Array.Empty<AIRuntimeKanbanGraphNode>();
            }

            return _board.Tasks
                .Where(
                    task =>
                        task !=
                            null &&
                        !string.IsNullOrWhiteSpace(
                            task.Id))
                .Select(
                    task =>
                        new AIRuntimeKanbanGraphNode
                        {
                            Id =
                                task.Id,

                            Title =
                                task.Title ??
                                string.Empty,

                            ParentTaskId =
                                task.ParentTaskId ??
                                string.Empty,

                            DependsOnTaskIds =
                                (task.DependsOnTaskIds ??
                                 new List<string>())
                                .Where(
                                    dependencyId =>
                                        !string.IsNullOrWhiteSpace(
                                            dependencyId))
                                .Distinct(
                                    StringComparer.Ordinal)
                                .ToList(),

                            AssociatedTaskIds =
                                (task.AssociatedTaskIds ??
                                 new List<string>())
                                .Where(
                                    associatedTaskId =>
                                        !string.IsNullOrWhiteSpace(
                                            associatedTaskId) &&
                                        !string.Equals(
                                            associatedTaskId,
                                            task.Id,
                                            StringComparison.Ordinal))
                                .Distinct(
                                    StringComparer.Ordinal)
                                .ToList(),

                            BlockedByTaskId =
                                task.BlockedByTaskId ??
                                string.Empty,

                            IsRunning = IsTaskRunning(task),

                            Status =
                                task.Status.ToString(),

                            Priority =
                                task.Priority.ToString(),

                            AssignedAgent =
                                task.AssignedAgent ??
                                string.Empty,

                            ReviewStatus =
                                task.ReviewStatus ??
                                string.Empty,

                            IsDecompositionDraft =
                                task.IsDecompositionDraft
                        })
                .ToList();
        }


        internal string GetSelectedKanbanTaskId()
        {
            return _selectedTaskId ??
                   string.Empty;
        }


        internal void SelectKanbanTaskFromGraph(
            string taskId,
            bool openDetails)
        {
            KanbanTask task =
                FindTask(
                    taskId);

            if (task ==
                null)
            {
                return;
            }

            _selectedTaskId =
                task.Id;

            _taskCardExpansionOverrides[
                    task.Id] =
                true;

            Repaint();

            if (openDetails)
            {
                SelectTask(
                    task);
            }
        }


        internal IReadOnlyList<AIRuntimeKanbanAgentCard>
            GetKanbanAgentCards()
        {
            if (_agentOptions ==
                null)
            {
                RefreshAgentOptions();
            }

            return _agentOptions
                .Where(
                    option =>
                        option !=
                            null &&
                        !string.IsNullOrWhiteSpace(
                            option.Id))
                .Select(
                    option =>
                    {
                        string agentId =
                            option.Id.Trim();

                        bool isActive =
                            IsKanbanAgentActive(
                                agentId);

                        bool isRunning =
                            _activeAgentRuns.TryGetValue(
                                agentId,
                                out ActiveAgentRun activeRun);

                        KanbanTask activeTask =
                            isRunning
                                ? FindTask(
                                    activeRun.TaskId)
                                : null;

                        int assignedTaskCount =
                            _board.Tasks.Count(
                                task =>
                                    task !=
                                        null &&
                                    string.Equals(
                                        task.AssignedAgent
                                            ?.Trim(),
                                        agentId,
                                        StringComparison.Ordinal));

                        int pendingTaskCount =
                            _board.Tasks.Count(
                                task =>
                                    task !=
                                        null &&
                                    string.Equals(
                                        task.AssignedAgent
                                            ?.Trim(),
                                        agentId,
                                        StringComparison.Ordinal) &&
                                    (task.Status ==
                                         TaskStatus.Todo ||
                                     task.Status ==
                                         TaskStatus.InProgress ||
                                     task.Status ==
                                         TaskStatus.Review));

                        return new AIRuntimeKanbanAgentCard
                        {
                            Id =
                                agentId,

                            Label =
                                option.Label,

                            IsActive =
                                isActive,

                            IsRunning =
                                isRunning,

                            ActiveTaskTitle =
                                activeTask?.Title ??
                                string.Empty,

                            AssignedTaskCount =
                                assignedTaskCount,

                            PendingTaskCount =
                                pendingTaskCount,

                            ConfigurationPath =
                                option.ConfigurationPath ??
                                string.Empty
                        };
                    })
                .OrderByDescending(
                    agent =>
                        agent.IsActive)
                .ThenBy(
                    agent =>
                        agent.Label,
                    StringComparer.OrdinalIgnoreCase)
                .ToList();
        }

        internal void SetKanbanAgentActive(
            string agentId,
            bool isActive)
        {
            string normalized =
                agentId?.Trim();

            if (string.IsNullOrWhiteSpace(
                    normalized))
            {
                return;
            }

            bool changed =
                isActive
                    ? _activeKanbanAgentIds.Add(
                        normalized)
                    : _activeKanbanAgentIds.Remove(
                        normalized);

            if (!changed)
            {
                return;
            }

            SaveAgentActivationState();

            _statusMessage =
                isActive
                    ? $"Activated agent '{normalized}'"
                    : $"Deactivated agent '{normalized}'";

            Repaint();
        }


        private bool IsKanbanAgentActive(
            string agentId)
        {
            return !string.IsNullOrWhiteSpace(
                       agentId) &&
                   _activeKanbanAgentIds.Contains(
                       agentId.Trim());
        }


        private string ResolveWorkflowAgentId(
            KanbanTask task,
            bool reviewMode)
        {
            if (task == null)
                return string.Empty;

            if (reviewMode &&
                IsKanbanAgentActive(
                    ReviewAgentId))
            {
                return ReviewAgentId;
            }

            if (!reviewMode &&
                IsPlanningCoordinator(task) &&
                IsKanbanAgentActive(
                    ArchitectAgentId))
            {
                return ArchitectAgentId;
            }

            return reviewMode ? task.AssignedAgent?.Trim() ?? string.Empty : ResolveImplementationAgentId(task);
        }


        private static void TryAddSavedAgent(
            ICollection<AgentOption> options,
            string path)
        {
            try
            {
                SavedAgentReference saved =
                    JsonUtility.FromJson<SavedAgentReference>(
                        File.ReadAllText(
                            path,
                            Encoding.UTF8));


                if (saved == null ||
                    saved.Version != 1 ||
                    !saved.UseAgent ||
                    string.IsNullOrWhiteSpace(
                        saved.AgentId))
                {
                    return;
                }


                string id =
                    AIRuntimeEditorAgentConfigurationPaths.ResolveInstanceId(
                        saved.AgentId,
                        path);

                string displayName =
                    string.IsNullOrWhiteSpace(
                        saved.AgentDisplayName)
                        ? id
                        : saved.AgentDisplayName.Trim();

                AgentOption existing =
                    options.FirstOrDefault(
                        option =>
                            string.Equals(
                                option.Id,
                                id,
                                StringComparison.Ordinal));

                if (existing !=
                    null)
                {
                    if (AIRuntimeEditorAgentConfigurationPaths.ShouldPrefer(
                            id,
                            path,
                            existing.ConfigurationPath))
                    {
                        existing.Label =
                            $"{displayName} ({id})";

                        existing.ConfigurationPath =
                            path;
                        existing.Role = saved.AgentPreset;
                    }

                    return;
                }


                options.Add(
                    new AgentOption
                    {
                        Id =
                            id,

                        Label =
                            $"{displayName} ({id})",

                        ConfigurationPath =
                            path,
                        Role = saved.AgentPreset
                    });
            }
            catch (Exception exception)
                when (
                    exception is IOException ||
                    exception is UnauthorizedAccessException ||
                    exception is ArgumentException)
            {
                // Ignore unrelated or unreadable JSON files.
            }
        }


        private void LoadAgentActivationState()
        {
            _activeKanbanAgentIds.Clear();

            string path =
                GetAgentActivationStatePath();

            if (!File.Exists(
                    path))
            {
                return;
            }

            try
            {
                KanbanAgentActivationState state =
                    JsonUtility.FromJson<KanbanAgentActivationState>(
                        File.ReadAllText(
                            path,
                            Encoding.UTF8));

                if (state == null ||
                    state.Version !=
                    2)
                {
                    // Version 1 stored disabled agents and therefore
                    // implied that every other agent was active. Do not
                    // migrate that unsafe default.
                    return;
                }

                foreach (
                    string agentId
                    in (IEnumerable<string>)state.ActiveAgentIds ??
                       Enumerable.Empty<string>())
                {
                    if (!string.IsNullOrWhiteSpace(
                            agentId))
                    {
                        _activeKanbanAgentIds.Add(
                            agentId.Trim());
                    }
                }
            }
            catch (Exception exception)
                when (
                    exception is IOException ||
                    exception is UnauthorizedAccessException ||
                    exception is ArgumentException)
            {
                _statusMessage =
                    $"Could not load agent activation settings: {exception.Message}";
            }
        }


        private void SaveAgentActivationState()
        {
            try
            {
                string path =
                    GetAgentActivationStatePath();

                string directory =
                    Path.GetDirectoryName(
                        path);

                if (!string.IsNullOrWhiteSpace(
                        directory))
                {
                    Directory.CreateDirectory(
                        directory);
                }

                KanbanAgentActivationState state =
                    new KanbanAgentActivationState
                    {
                        ActiveAgentIds =
                            _activeKanbanAgentIds
                                .OrderBy(
                                    value =>
                                        value,
                                    StringComparer.Ordinal)
                                .ToList()
                    };

                File.WriteAllText(
                    path,
                    JsonUtility.ToJson(
                        state,
                        prettyPrint: true),
                    new UTF8Encoding(
                        encoderShouldEmitUTF8Identifier: false));
            }
            catch (Exception exception)
                when (
                    exception is IOException ||
                    exception is UnauthorizedAccessException ||
                    exception is ArgumentException)
            {
                _statusMessage =
                    $"Could not save agent activation settings: {exception.Message}";
            }
        }


        private void MarkDirty()
        {
            _isDirty = true;
            AIRuntimeKanbanScheduler.Wake();

            if (_autoSave)
            {
                ScheduleAutoSave();
            }
        }


        private void ScheduleAutoSave()
        {
            if (_autoSaveScheduled)
                return;


            _autoSaveScheduled =
                true;

            EditorApplication.delayCall +=
                PerformAutoSave;
        }


        private void PerformAutoSave()
        {
            _autoSaveScheduled =
                false;


            if (this == null ||
                !_autoSave ||
                !_isDirty)
            {
                return;
            }


            SaveBoard(
                showDialog: false);
        }


        private void ScheduleExecutionMemorySave()
        {
            if (_executionMemorySaveScheduled)
            {
                return;
            }

            _executionMemorySaveScheduled = true;
            EditorApplication.delayCall +=
                PerformExecutionMemorySave;
        }


        private void PerformExecutionMemorySave()
        {
            _executionMemorySaveScheduled = false;

            if (this == null ||
                !_isDirty)
            {
                return;
            }

            // A failed memory save must be visible and stop owned work; never
            // continue while durable checkpoints cannot be committed.
            PersistSchedulerState();
        }


        private bool TryObserveExternalBoardChange()
        {
            if (_isPreparingForAssemblyReload ||
                EditorApplication.timeSinceStartup <
                _nextExternalBoardCheckEditorTime)
            {
                return false;
            }

            _nextExternalBoardCheckEditorTime =
                EditorApplication.timeSinceStartup + 0.75d;

            if (string.IsNullOrWhiteSpace(_loadedBoardPath) ||
                string.IsNullOrWhiteSpace(_loadedBoardFingerprint))
            {
                return false;
            }

            string path;
            try
            {
                path = NormalizeBoardPath();
            }
            catch (Exception)
            {
                return false;
            }

            if (!string.Equals(
                    path,
                    _loadedBoardPath,
                    StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            string diskJson;
            try
            {
                if (!File.Exists(path))
                {
                    HandleBoardWriteConflict(
                        path,
                        "The persisted board was removed after this window loaded it.");
                    return true;
                }

                diskJson = File.ReadAllText(path, Encoding.UTF8);
            }
            catch (IOException)
            {
                // Another revisioned writer may be in its brief atomic replace
                // window. Retry on the next throttled editor update.
                return false;
            }
            catch (UnauthorizedAccessException)
            {
                return false;
            }

            if (string.Equals(
                    ComputeBoardContentFingerprint(diskJson),
                    _loadedBoardFingerprint,
                    StringComparison.Ordinal))
            {
                return false;
            }

            if (_isDirty ||
                _activeAgentRuns.Count > 0 ||
                !_pendingAgentCompletions.IsEmpty ||
                !_pendingExecutionMemoryUpdates.IsEmpty)
            {
                HandleBoardWriteConflict(
                    path,
                    "The persisted board changed while this window had uncommitted or active work.");
                return true;
            }

            LoadBoard(showDialog: false);
            _statusMessage =
                $"Reloaded {_board.Tasks.Count} tasks after detecting a newer persisted board revision.";
            return true;
        }


        private void HandleBoardWriteConflict(
            string path,
            string reason)
        {
            bool firstObservation =
                !_boardWriteConflict;

            _boardWriteConflict = true;
            _autoRunAgents = false;
            AbandonOwnedRuns();
            _statusMessage =
                "Board write conflict: AIRuntime stopped the scheduler and preserved the newer disk state. Reload the board before continuing.";

            if (firstObservation)
            {
                Debug.LogError(
                    $"[Kanban] Refused to overwrite externally changed board '{path}'. {reason} " +
                    "The scheduler was stopped; no persisted tasks were replaced.");
            }
        }


        private static string ComputeBoardContentFingerprint(
            string json)
        {
            byte[] bytes =
                Encoding.UTF8.GetBytes(json ?? string.Empty);

            using (SHA256 sha256 = SHA256.Create())
            {
                return Convert.ToBase64String(
                    sha256.ComputeHash(bytes));
            }
        }


        private static int ReadPersistedBoardRevision(
            string json)
        {
            if (string.IsNullOrWhiteSpace(json))
                return 0;

            try
            {
                return Math.Max(
                    0,
                    JObject.Parse(json)["Revision"]?.Value<int>() ?? 0);
            }
            catch (JsonException)
            {
                return 0;
            }
        }


        private void RecordLoadedBoardSnapshot(
            string path,
            string json)
        {
            _loadedBoardPath = path ?? string.Empty;
            _loadedBoardFingerprint =
                ComputeBoardContentFingerprint(json);
            _boardWriteConflict = false;
        }


        private static void WriteBoardAtomically(
            string path,
            string json)
        {
            string temporaryPath =
                path + "." + Guid.NewGuid().ToString("N") + ".tmp";

            try
            {
                File.WriteAllText(
                    temporaryPath,
                    json,
                    new UTF8Encoding(
                        encoderShouldEmitUTF8Identifier: false));

                if (File.Exists(path))
                    File.Replace(temporaryPath, path, null);
                else
                    File.Move(temporaryPath, path);
            }
            finally
            {
                if (File.Exists(temporaryPath))
                    File.Delete(temporaryPath);
            }
        }


        private void SaveBoard(
            bool showDialog)
        {
            int previousRevision =
                _board?.Revision ?? 0;
            string previousWriterId =
                _board?.LastWriterId;
            string previousSavedAtUtc =
                _board?.LastSavedAtUtc;

            try
            {
                string path =
                    NormalizeBoardPath();

                string directory =
                    Path.GetDirectoryName(
                        path);


                if (string.IsNullOrWhiteSpace(
                        directory))
                {
                    throw new InvalidOperationException(
                        "The board file must have a parent folder.");
                }


                Directory.CreateDirectory(
                    directory);

                string lockPath =
                    path + ".lock";

                using (new FileStream(
                           lockPath,
                           FileMode.OpenOrCreate,
                           FileAccess.ReadWrite,
                           FileShare.None))
                {
                    bool diskExists =
                        File.Exists(path);
                    string diskJson =
                        diskExists
                            ? File.ReadAllText(path, Encoding.UTF8)
                            : string.Empty;

                    bool tracksThisPath =
                        string.Equals(
                            path,
                            _loadedBoardPath,
                            StringComparison.OrdinalIgnoreCase);

                    bool exactLoadedSnapshot =
                        tracksThisPath &&
                        !string.IsNullOrWhiteSpace(
                            _loadedBoardFingerprint) &&
                        diskExists &&
                        string.Equals(
                            ComputeBoardContentFingerprint(diskJson),
                            _loadedBoardFingerprint,
                            StringComparison.Ordinal);

                    bool newUntrackedBoard =
                        !diskExists &&
                        string.IsNullOrWhiteSpace(
                            _loadedBoardFingerprint);

                    if (!exactLoadedSnapshot &&
                        !newUntrackedBoard)
                    {
                        HandleBoardWriteConflict(
                            path,
                            diskExists
                                ? "The destination no longer matches the exact snapshot loaded by this writer."
                                : "The destination was removed after this writer loaded it.");
                        return;
                    }

                    int diskRevision =
                        ReadPersistedBoardRevision(diskJson);

                    _board.Revision =
                        Math.Max(previousRevision, diskRevision) + 1;
                    _board.LastWriterId =
                        _boardWriterId;
                    _board.LastSavedAtUtc =
                        DateTime.UtcNow.ToString("O");

                    SynchronizeMemoryAttempts();
                    string json =
                        JsonUtility.ToJson(
                            _board,
                            prettyPrint: true);

                    WriteBoardAtomically(path, json);
                    RecordLoadedBoardSnapshot(path, json);
                }


                _boardFilePath =
                    path;

                _isDirty =
                    false;

                _statusMessage =
                    $"Saved {_board.Tasks.Count} tasks";


                if (showDialog)
                {
                    EditorUtility.DisplayDialog(
                        "Kanban saved",
                        $"Saved the board to:\n\n{path}",
                        "OK");
                }
            }
            catch (Exception exception)
            {
                if (_board != null)
                {
                    _board.Revision = previousRevision;
                    _board.LastWriterId = previousWriterId;
                    _board.LastSavedAtUtc = previousSavedAtUtc;
                }

                _statusMessage =
                    "Save failed";


                if (showDialog)
                {
                    EditorUtility.DisplayDialog(
                        "Could not save Kanban board",
                        exception.Message,
                        "OK");
                }
                else
                {
                    Debug.LogError(
                        $"AIRuntime Kanban could not save: {exception.Message}");
                }
            }
        }


        private void LoadBoard(
            bool showDialog)
        {
            if (_activeAgentRuns.Count > 0)
            {
                _statusMessage = "Wait for active runs to finish before loading another board snapshot.";
                return;
            }
            try
            {
                string path =
                    NormalizeBoardPath();


                if (!File.Exists(path))
                {
                    if (showDialog)
                    {
                        throw new FileNotFoundException(
                            "The Kanban board file does not exist.",
                            path);
                    }


                    return;
                }


                string json =
                    File.ReadAllText(
                        path,
                        Encoding.UTF8);

                KanbanBoard loaded =
                    JsonUtility.FromJson<KanbanBoard>(json);


                if (loaded == null ||
                    loaded.Version != 1)
                {
                    throw new InvalidOperationException(
                        "The Kanban board has an unsupported format or version.");
                }


                loaded.Tasks =
                    loaded.Tasks ??
                    new List<KanbanTask>();

                loaded.ProviderCooldowns =
                    loaded.ProviderCooldowns ??
                    new List<ProviderCooldownState>();

                loaded.AgentFallbackChains = loaded.AgentFallbackChains ?? new List<AgentFallbackChain>();

                ValidateLoadedTasks(
                    loaded.Tasks);

                int repairedOwnershipReviewCount = RepairSourceOwnershipReviews(loaded.Tasks);

                int migratedDependencyDeclarationCount =
                    MigrateLegacyDependencyDeclarations(
                        loaded.Tasks);

                int normalizedHierarchyCount =
                    NormalizeLoadedTaskHierarchy(
                        loaded.Tasks);

                int collapsedRecursiveDecompositionCount =
                    RepairRecursiveArtifactDecompositionLoops(
                        loaded.Tasks);

                int recoveredRateLimitedCount =
                    RecoverLegacyProviderRateLimitFailures(
                        loaded.Tasks);

                int recoveredTransientProviderCount =
                    RecoverLegacyTransientProviderAvailabilityFailures(
                        loaded.Tasks);

                int recoveredDraftDecompositionCount =
                    RecoverLegacyTerminalDecompositionDrafts(
                        loaded.Tasks);

                int recoveredInterruptedCount =
                    _activeAgentRuns.Count ==
                    0
                        ? RecoverInterruptedTasks(
                            loaded.Tasks)
                        : 0;


                _board =
                    loaded;

                _schedulerPersistenceFailed = false;

                int reconciledRelationshipCount =
                    ReconcileAllTaskRelationships();

                int migratedWorkflowCount =
                    MigrateWorkflowPhases();

                _selectedTaskId =
                    null;

                _boardFilePath =
                    path;

                RecordLoadedBoardSnapshot(
                    path,
                    json);

                _isDirty =
                    recoveredInterruptedCount > 0 ||
                    normalizedHierarchyCount > 0 ||
                    collapsedRecursiveDecompositionCount > 0 ||
                    recoveredDraftDecompositionCount > 0 ||
                    recoveredRateLimitedCount > 0 ||
                    recoveredTransientProviderCount > 0 ||
                    repairedOwnershipReviewCount > 0 ||
                    migratedDependencyDeclarationCount > 0 ||
                    reconciledRelationshipCount > 0 ||
                    migratedWorkflowCount > 0;

                _statusMessage =
                    collapsedRecursiveDecompositionCount > 0
                        ? $"Loaded {_board.Tasks.Count} tasks; collapsed {collapsedRecursiveDecompositionCount} recursively generated same-artifact card(s) and queued their atomic owners for direct execution"
                        : recoveredDraftDecompositionCount > 0
                        ? $"Loaded {_board.Tasks.Count} tasks; recovered interrupted Planning proposals for authoritative graph reconciliation"
                        : recoveredRateLimitedCount > 0
                        ? $"Loaded {_board.Tasks.Count} tasks; requeued {recoveredRateLimitedCount} legacy rate-limited card(s) with persisted artifacts preserved"
                        : recoveredTransientProviderCount > 0
                        ? $"Loaded {_board.Tasks.Count} tasks; requeued {recoveredTransientProviderCount} interrupted provider-stream card(s) with persisted artifacts preserved"
                        : recoveredInterruptedCount > 0
                        ? $"Loaded {_board.Tasks.Count} tasks; safely requeued {recoveredInterruptedCount} interrupted run(s)"
                        : $"Loaded {_board.Tasks.Count} tasks";

                // Versioned recovery and schema migrations are part of the
                // durable board contract, not an unsaved UI edit. Persist
                // them immediately even when the user has disabled normal
                // edit auto-save, otherwise the same crashed state returns
                // on the next domain/window reload.
                if (_isDirty)
                    SaveBoard(showDialog: false);


                if (showDialog)
                {
                    EditorUtility.DisplayDialog(
                        "Kanban loaded",
                        $"Loaded {_board.Tasks.Count} tasks.",
                        "OK");
                }


                Repaint();
            }
            catch (Exception exception)
            {
                _statusMessage =
                    "Load failed";

                EditorUtility.DisplayDialog(
                    "Could not load Kanban board",
                    exception.Message,
                    "OK");
            }
        }


        private int MigrateWorkflowPhases()
        {
            int changes = 0;

            foreach (KanbanTask root in _board.Tasks.Where(
                         task =>
                             task != null &&
                             string.IsNullOrWhiteSpace(task.ParentTaskId)))
            {
                if (!string.IsNullOrWhiteSpace(root.WorkflowPhase))
                    continue;

                List<KanbanTask> descendants = GetAllDescendants(root);
                if (root.Status == TaskStatus.Done)
                {
                    root.WorkflowPhase = WorkflowPhaseCompleted;
                }
                else if (descendants.Count > 0)
                {
                    root.PlanRevision = Math.Max(1, root.PlanRevision);
                    root.WorkflowPhase =
                        descendants.All(
                            task =>
                                task.Status == TaskStatus.Review ||
                                task.Status == TaskStatus.Done)
                            ? WorkflowPhaseFinalReview
                            : WorkflowPhaseImplementation;

                    foreach (KanbanTask descendant in descendants)
                    {
                        if (descendant.PlannedRevision <= 0)
                            descendant.PlannedRevision = root.PlanRevision;
                    }
                }
                else
                {
                    root.WorkflowPhase =
                        _autoDecompose &&
                        root.Status == TaskStatus.Todo
                            ? WorkflowPhasePlanning
                            : WorkflowPhaseImplementation;
                }

                changes++;
            }

            return changes;
        }


        private void SelectBoardFile()
        {
            if (_activeAgentRuns.Count > 0)
            {
                _statusMessage = "Wait for active runs to finish before changing board files.";
                return;
            }
            string directory =
                GetBoardDirectory();

            string selected =
                EditorUtility.SaveFilePanel(
                    "Select AIRuntime Kanban board",
                    directory,
                    Path.GetFileName(
                        _boardFilePath),
                    "json");


            if (string.IsNullOrWhiteSpace(
                    selected))
            {
                return;
            }


            if (_isDirty &&
                !EditorUtility.DisplayDialog(
                    "Unsaved changes",
                    "Change board files without saving the current board?",
                    "Change file",
                    "Cancel"))
            {
                return;
            }


            _boardFilePath =
                selected;


            if (File.Exists(
                    selected))
            {
                LoadBoard(
                    showDialog: false);
            }
            else
            {
                _board =
                    new KanbanBoard();

                _selectedTaskId =
                    null;

                _isDirty =
                    true;

                _statusMessage =
                    "New board file selected";
            }
        }


        private string NormalizeBoardPath()
        {
            if (string.IsNullOrWhiteSpace(
                    _boardFilePath))
            {
                throw new InvalidOperationException(
                    "Select a Kanban board file.");
            }


            string path =
                Path.GetFullPath(
                    _boardFilePath.Trim());


            if (!string.Equals(
                    Path.GetExtension(path),
                    ".json",
                    StringComparison.OrdinalIgnoreCase))
            {
                path +=
                    ".json";
            }


            return path;
        }


        private string GetBoardDirectory()
        {
            try
            {
                string directory =
                    Path.GetDirectoryName(
                        _boardFilePath);


                return string.IsNullOrWhiteSpace(
                        directory)
                    ? Environment.GetFolderPath(
                        Environment.SpecialFolder.MyDocuments)
                    : directory;
            }
            catch (ArgumentException)
            {
                return Environment.GetFolderPath(
                    Environment.SpecialFolder.MyDocuments);
            }
        }


        private static void ValidateLoadedTasks(
            IEnumerable<KanbanTask> tasks)
        {
            HashSet<string> identifiers =
                new HashSet<string>(
                    StringComparer.Ordinal);


            foreach (
                KanbanTask task
                in tasks)
            {
                if (task == null)
                {
                    throw new InvalidOperationException(
                        "The board contains a null task.");
                }


                if (string.IsNullOrWhiteSpace(
                        task.Id))
                {
                    task.Id =
                        Guid.NewGuid()
                            .ToString("N");
                }


                if (!identifiers.Add(
                        task.Id))
                {
                    throw new InvalidOperationException(
                        $"The board contains duplicate task id '{task.Id}'.");
                }


                task.Title =
                    string.IsNullOrWhiteSpace(
                        task.Title)
                        ? "Untitled task"
                        : task.Title;

                task.Description =
                    task.Description ??
                    string.Empty;

                task.AcceptanceCriteria =
                    task.AcceptanceCriteria ??
                    string.Empty;

                task.ParentTaskId =
                    task.ParentTaskId ??
                    string.Empty;

                task.DependsOnTaskIds =
                    task.DependsOnTaskIds ??
                    new List<string>();

                task.DeclaredDependencyTitles =
                    task.DeclaredDependencyTitles ??
                    new List<string>();

                task.AssociatedTaskIds =
                    task.AssociatedTaskIds ??
                    new List<string>();

                task.ProducedArtifactPaths =
                    task.ProducedArtifactPaths ??
                    new List<string>();

                task.ConsumedArtifactPaths =
                    task.ConsumedArtifactPaths ??
                    new List<string>();

                task.MutatedResourceKeys =
                    task.MutatedResourceKeys ??
                    new List<string>();

                task.CreationSource =
                    task.CreationSource ??
                    string.Empty;

                task.CreatedStatus =
                    task.CreatedStatus ??
                    string.Empty;

                task.AssignedAgent =
                    task.AssignedAgent ??
                    string.Empty;

                task.Tags =
                    task.Tags ??
                    string.Empty;

                task.DueDate =
                    task.DueDate ??
                    string.Empty;

                task.AgentResult =
                    task.AgentResult ??
                    string.Empty;

                task.ExecutionMemory =
                    task.ExecutionMemory ??
                    string.Empty;

                task.AgentError =
                    task.AgentError ??
                    string.Empty;

                task.AgentStartedAt =
                    task.AgentStartedAt ??
                    string.Empty;

                task.AgentFinishedAt =
                    task.AgentFinishedAt ??
                    string.Empty;

                task.NextAgentMessage =
                    task.NextAgentMessage ??
                    string.Empty;

                task.ReviewStatus =
                    task.ReviewStatus ??
                    string.Empty;

                task.ReviewResult =
                    task.ReviewResult ??
                    string.Empty;

                task.ReviewEvidence =
                    task.ReviewEvidence ??
                    string.Empty;

                task.ReviewFailureKind =
                    task.ReviewFailureKind ??
                    string.Empty;

                task.ReviewEvidenceFingerprint =
                    task.ReviewEvidenceFingerprint ??
                    string.Empty;

                task.LastRejectedReviewEvidenceFingerprint =
                    task.LastRejectedReviewEvidenceFingerprint ??
                    string.Empty;

                task.LastRejectedReviewOutcomeFingerprint =
                    task.LastRejectedReviewOutcomeFingerprint ??
                    string.Empty;

                task.ReviewDependencyStateFingerprint =
                    task.ReviewDependencyStateFingerprint ??
                    string.Empty;

                task.BlockedByTaskId =
                    task.BlockedByTaskId ??
                    string.Empty;

                task.BlockedReason =
                    task.BlockedReason ??
                    string.Empty;

                task.ReviewStartedAt =
                    task.ReviewStartedAt ??
                    string.Empty;

                task.ReviewFinishedAt =
                    task.ReviewFinishedAt ??
                    string.Empty;

                task.RequestTitle =
                    task.RequestTitle ??
                    string.Empty;

                task.RequestDescription =
                    task.RequestDescription ??
                    string.Empty;

                task.RequestAcceptanceCriteria =
                    task.RequestAcceptanceCriteria ??
                    string.Empty;

                task.WorkflowPhase =
                    task.WorkflowPhase ??
                    string.Empty;

                task.PlanningAttemptId =
                    task.PlanningAttemptId ??
                    string.Empty;

                task.ProposedInPlanningAttemptId =
                    task.ProposedInPlanningAttemptId ??
                    string.Empty;

                task.PlanningKickbackReason =
                    task.PlanningKickbackReason ??
                    string.Empty;

                task.PlanningKickbackFingerprint =
                    task.PlanningKickbackFingerprint ??
                    string.Empty;
            }
        }


        private static int RecoverLegacyProviderRateLimitFailures(
            IList<KanbanTask> tasks)
        {
            if (tasks == null)
                return 0;


            int recovered =
                0;

            foreach (KanbanTask task in tasks)
            {
                if (task == null ||
                    task.Status != TaskStatus.Crashed ||
                    !IsProviderRateLimitFailure(
                        task.AgentError))
                {
                    continue;
                }


                bool interruptedReview =
                    !string.IsNullOrWhiteSpace(
                        task.AgentResult) &&
                    !string.IsNullOrWhiteSpace(
                        task.ReviewStartedAt);

                task.Status =
                    interruptedReview
                        ? TaskStatus.Review
                        : TaskStatus.Todo;

                task.ReviewStatus =
                    interruptedReview
                        ? "Pending"
                        : string.Empty;

                task.BlockedByTaskId =
                    string.Empty;

                task.BlockedReason =
                    string.Empty;

                task.NextAgentMessage =
                    "[ProviderRateLimitRecovery] AIRuntime migrated this legacy rate-limit crash into resumable work. Inspect persisted execution memory and existing artifacts before continuing; do not recreate completed work.";

                recovered++;
            }


            return recovered;
        }


        private static int RecoverLegacyTransientProviderAvailabilityFailures(
            IList<KanbanTask> tasks)
        {
            if (tasks == null)
                return 0;

            int recovered = 0;

            foreach (KanbanTask task in tasks)
            {
                string providerError =
                    FirstNonEmpty(
                        task?.AgentError,
                        task?.ReviewResult,
                        task?.BlockedReason);

                // Refresh diagnostics on an already paused attempt without
                // resetting its status, review history, or continuation memory.
                if (task != null && task.ReviewStatus == "Retry Available" &&
                    IsTransientProviderAvailabilityFailure(providerError))
                {
                    string guidance = BuildProviderResumeGuidance(providerError);
                    if (!string.Equals(task.BlockedReason, guidance, StringComparison.Ordinal))
                    {
                        task.BlockedReason = guidance;
                        recovered++;
                    }
                    continue;
                }

                if (task == null ||
                    (task.Status != TaskStatus.Crashed &&
                     task.Status != TaskStatus.Failed) ||
                    !IsTransientProviderAvailabilityFailure(
                        providerError))
                {
                    continue;
                }

                bool interruptedReview =
                    !string.IsNullOrWhiteSpace(task.AgentResult) &&
                    !string.IsNullOrWhiteSpace(task.ReviewStartedAt);

                task.Status =
                    interruptedReview
                        ? TaskStatus.Review
                        : TaskStatus.Todo;

                task.ReviewStatus =
                    interruptedReview
                        ? "Pending"
                        : "Retry Available";

                task.AgentError =
                    string.Empty;

                if (!string.IsNullOrWhiteSpace(
                        providerError))
                {
                    task.ReviewResult =
                        providerError;
                }

                if (task.DecompositionFailureCount > 0)
                {
                    task.DecompositionFailureCount--;
                }

                task.BlockedByTaskId =
                    string.Empty;

                task.BlockedReason =
                    string.Empty;

                task.NextAgentMessage =
                    "[ProviderTransportRecovery] AIRuntime recovered an interrupted provider stream as resumable work. Preserve persisted execution memory and existing artifacts. Inspect each existing artifact once, do not recreate completed work, and continue with a mutation, a materially different observation, or completion verification.";

                recovered++;
            }

            return recovered;
        }


        private static string FirstNonEmpty(
            params string[] values)
        {
            return values?
                       .FirstOrDefault(
                           value =>
                               !string.IsNullOrWhiteSpace(
                                   value)) ??
                   string.Empty;
        }


        private static int RepairRecursiveArtifactDecompositionLoops(
            IList<KanbanTask> tasks)
        {
            if (tasks == null || tasks.Count == 0)
                return 0;

            List<KanbanTask> initialTasks =
                tasks
                    .Where(
                        task =>
                            task != null &&
                            !string.IsNullOrWhiteSpace(task.Id))
                    .ToList();

            Dictionary<string, KanbanTask> initialById =
                initialTasks.ToDictionary(
                    task => task.Id,
                    StringComparer.Ordinal);

            List<KanbanTask> candidates =
                initialTasks
                    .Where(
                        candidate =>
                        {
                            string artifactPath =
                                GetSingleOwnedScriptPath(candidate);

                            return !string.IsNullOrWhiteSpace(artifactPath) &&
                                   !IsPartOfCommittedWorkflowPlan(
                                       candidate,
                                       initialById) &&
                                   IsStandaloneScriptAssociationLeaf(
                                       candidate,
                                       artifactPath) &&
                                   GetDescendantTaskIds(
                                           candidate.Id,
                                           initialTasks)
                                       .Select(
                                           descendantId =>
                                               initialById.TryGetValue(
                                                   descendantId,
                                                   out KanbanTask descendant)
                                                   ? descendant
                                                   : null)
                                       .Any(
                                           descendant =>
                                               descendant != null &&
                                               string.Equals(
                                                   descendant.CreationSource,
                                                   "create_kanban_child_task",
                                                   StringComparison.Ordinal) &&
                                               string.Equals(
                                                   GetSingleOwnedScriptPath(
                                                       descendant),
                                                   artifactPath,
                                                   StringComparison.OrdinalIgnoreCase));
                        })
                    .OrderBy(
                        candidate =>
                            GetHierarchyDepth(
                                candidate,
                                initialById))
                    .ThenBy(
                        candidate =>
                            candidate.CreatedAt ?? string.Empty,
                        StringComparer.Ordinal)
                    .ThenBy(
                        candidate => candidate.Id,
                        StringComparer.Ordinal)
                    .ToList();

            int removedCount = 0;

            foreach (KanbanTask canonical in candidates)
            {
                if (!tasks.Contains(canonical))
                    continue;

                string artifactPath =
                    GetSingleOwnedScriptPath(canonical);

                if (string.IsNullOrWhiteSpace(artifactPath))
                    continue;

                Dictionary<string, KanbanTask> currentById =
                    tasks
                        .Where(
                            task =>
                                task != null &&
                                !string.IsNullOrWhiteSpace(task.Id))
                        .ToDictionary(
                            task => task.Id,
                            StringComparer.Ordinal);

                if (HasAncestorOwningArtifact(
                        canonical,
                        artifactPath,
                        currentById))
                {
                    continue;
                }

                HashSet<string> descendantIds =
                    new HashSet<string>(
                        GetDescendantTaskIds(
                            canonical.Id,
                            tasks),
                        StringComparer.Ordinal);

                HashSet<string> removableIds =
                    new HashSet<string>(
                        tasks
                            .Where(
                                task =>
                                    task != null &&
                                    descendantIds.Contains(task.Id) &&
                                    string.Equals(
                                        task.CreationSource,
                                        "create_kanban_child_task",
                                        StringComparison.Ordinal) &&
                                    IsTaskWithinArtifactScope(
                                        task,
                                        artifactPath))
                            .Select(task => task.Id),
                        StringComparer.Ordinal);

                bool removesRepeatedOwner =
                    tasks.Any(
                        task =>
                            task != null &&
                            removableIds.Contains(task.Id) &&
                            string.Equals(
                                GetSingleOwnedScriptPath(task),
                                artifactPath,
                                StringComparison.OrdinalIgnoreCase));

                if (!removesRepeatedOwner)
                    continue;

                foreach (KanbanTask retained in tasks.Where(
                             task =>
                                 task != null &&
                                 !removableIds.Contains(task.Id) &&
                                 removableIds.Contains(
                                     task.ParentTaskId ?? string.Empty)))
                {
                    retained.ParentTaskId =
                        canonical.ParentTaskId ?? string.Empty;
                    retained.UpdatedAt =
                        DateTime.UtcNow.ToString("O");
                }

                for (int index = tasks.Count - 1; index >= 0; index--)
                {
                    KanbanTask task = tasks[index];

                    if (task != null && removableIds.Contains(task.Id))
                    {
                        tasks.RemoveAt(index);
                        removedCount++;
                    }
                }

                ResetAtomicAssociationLeaf(
                    canonical,
                    artifactPath,
                    removableIds.Count);
            }

            return removedCount;
        }


        private static bool IsPartOfCommittedWorkflowPlan(
            KanbanTask task,
            IReadOnlyDictionary<string, KanbanTask> byId)
        {
            if (task == null || byId == null)
                return false;

            KanbanTask root = task;
            var visited = new HashSet<string>(StringComparer.Ordinal);

            while (!string.IsNullOrWhiteSpace(root.ParentTaskId) &&
                   visited.Add(root.Id ?? string.Empty) &&
                   byId.TryGetValue(root.ParentTaskId, out KanbanTask parent))
            {
                root = parent;
            }

            if (root.PlanRevision <= 0)
                return false;

            string phase = root.WorkflowPhase ?? string.Empty;

            return string.Equals(
                       phase,
                       WorkflowPhaseImplementation,
                       StringComparison.Ordinal) ||
                   string.Equals(
                       phase,
                       WorkflowPhaseFinalReview,
                       StringComparison.Ordinal) ||
                   string.Equals(
                       phase,
                       WorkflowPhaseCompleted,
                       StringComparison.Ordinal);
        }


        private static string GetSingleOwnedScriptPath(
            KanbanTask task)
        {
            List<string> paths =
                NormalizeArtifactPaths(
                    task?.ProducedArtifactPaths);

            return paths.Count == 1 &&
                   paths[0].EndsWith(
                       ".cs",
                       StringComparison.OrdinalIgnoreCase)
                ? paths[0]
                : string.Empty;
        }


        private static IReadOnlyCollection<string> GetDescendantTaskIds(
            string taskId,
            IEnumerable<KanbanTask> tasks)
        {
            List<KanbanTask> all =
                (tasks ?? Enumerable.Empty<KanbanTask>())
                    .Where(task => task != null)
                    .ToList();

            var descendants =
                new HashSet<string>(StringComparer.Ordinal);

            var pending =
                new Queue<string>();

            pending.Enqueue(taskId ?? string.Empty);

            while (pending.Count > 0)
            {
                string parentId = pending.Dequeue();

                foreach (KanbanTask child in all.Where(
                             candidate =>
                                 !string.IsNullOrWhiteSpace(candidate.Id) &&
                                 string.Equals(
                                     candidate.ParentTaskId,
                                     parentId,
                                     StringComparison.Ordinal)))
                {
                    if (descendants.Add(child.Id))
                        pending.Enqueue(child.Id);
                }
            }

            descendants.Remove(taskId ?? string.Empty);
            return descendants;
        }


        private static int GetHierarchyDepth(
            KanbanTask task,
            IReadOnlyDictionary<string, KanbanTask> byId)
        {
            int depth = 0;
            string parentId = task?.ParentTaskId ?? string.Empty;
            var visited = new HashSet<string>(StringComparer.Ordinal);

            while (!string.IsNullOrWhiteSpace(parentId) &&
                   visited.Add(parentId) &&
                   byId.TryGetValue(parentId, out KanbanTask parent))
            {
                depth++;
                parentId = parent.ParentTaskId ?? string.Empty;
            }

            return depth;
        }


        private static bool HasAncestorOwningArtifact(
            KanbanTask task,
            string artifactPath,
            IReadOnlyDictionary<string, KanbanTask> byId)
        {
            string parentId = task?.ParentTaskId ?? string.Empty;
            var visited = new HashSet<string>(StringComparer.Ordinal);

            while (!string.IsNullOrWhiteSpace(parentId) &&
                   visited.Add(parentId) &&
                   byId.TryGetValue(parentId, out KanbanTask parent))
            {
                if (string.Equals(
                        GetSingleOwnedScriptPath(parent),
                        artifactPath,
                        StringComparison.OrdinalIgnoreCase))
                {
                    return true;
                }

                parentId = parent.ParentTaskId ?? string.Empty;
            }

            return false;
        }


        private static bool IsTaskWithinArtifactScope(
            KanbanTask task,
            string artifactPath)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(artifactPath))
            {
                return false;
            }

            List<string> structuredPaths =
                NormalizeArtifactPaths(
                        task.ProducedArtifactPaths)
                    .Concat(
                        NormalizeArtifactPaths(
                            task.ConsumedArtifactPaths))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

            string fileName =
                Path.GetFileName(artifactPath) ?? string.Empty;

            List<string> mutationKeys =
                (task.MutatedResourceKeys ?? new List<string>())
                    .Where(key => !string.IsNullOrWhiteSpace(key))
                    .Select(key => key.Trim().Replace('\\', '/'))
                    .Distinct(StringComparer.OrdinalIgnoreCase)
                    .ToList();

            if (structuredPaths.Count > 0)
            {
                bool onlyUsesArtifact =
                    structuredPaths.All(
                        path =>
                            string.Equals(
                                path,
                                artifactPath,
                                StringComparison.OrdinalIgnoreCase));

                bool onlyMutatesArtifact =
                    mutationKeys.All(
                        key =>
                            key.IndexOf(
                                artifactPath,
                                StringComparison.OrdinalIgnoreCase) >= 0 ||
                            (!string.IsNullOrWhiteSpace(fileName) &&
                             key.IndexOf(
                                 fileName,
                                 StringComparison.OrdinalIgnoreCase) >= 0));

                return onlyUsesArtifact &&
                       onlyMutatesArtifact;
            }

            if (mutationKeys.Count > 0)
            {
                return mutationKeys.All(
                    key =>
                        key.IndexOf(
                            artifactPath,
                            StringComparison.OrdinalIgnoreCase) >= 0 ||
                        (!string.IsNullOrWhiteSpace(fileName) &&
                         key.IndexOf(
                             fileName,
                             StringComparison.OrdinalIgnoreCase) >= 0));
            }

            // Unstructured prose mentioning a filename is not enough evidence
            // to delete work during a load migration. Generated cards must
            // carry exact ownership/consumption/resource metadata to collapse.
            return false;
        }


        private static void ResetAtomicAssociationLeaf(
            KanbanTask task,
            string artifactPath,
            int removedCount)
        {
            task.Status = TaskStatus.Todo;
            task.IsDecompositionDraft = false;
            task.SuppressAutoDecomposition = true;
            task.AgentResult = string.Empty;
            task.ExecutionMemory = string.Empty;
            ResetCardMemory(task);
            task.AgentError = string.Empty;
            task.AgentStartedAt = string.Empty;
            task.AgentFinishedAt = string.Empty;
            task.ReviewStatus = string.Empty;
            task.ReviewResult = string.Empty;
            task.ReviewEvidence = string.Empty;
            task.ReviewFailureKind = string.Empty;
            task.ReviewStartedAt = string.Empty;
            task.ReviewFinishedAt = string.Empty;
            task.ReviewFailureCount = 0;
            task.PreflightFailureCount = 0;
            task.ReviewEvidenceFingerprint = string.Empty;
            task.LastRejectedReviewEvidenceFingerprint = string.Empty;
            task.LastRejectedReviewOutcomeFingerprint = string.Empty;
            task.ReviewDependencyStateFingerprint = string.Empty;
            task.BlockedByTaskId = string.Empty;
            task.BlockedReason = string.Empty;
            task.DecompositionFailureCount = 0;
            task.DecompositionPolicyVersion =
                CurrentDecompositionPolicyVersion;
            task.RequestTitle = string.Empty;
            task.RequestDescription = string.Empty;
            task.RequestAcceptanceCriteria = string.Empty;
            task.NextAgentMessage =
                "[AssociationLeaf] AIRuntime collapsed a recursive decomposition stack that repeatedly delegated " +
                $"the same script artifact ({artifactPath}) across {removedCount} generated card(s). " +
                "Execute this card directly and verify the artifact; do not decompose the same ownership scope again.";
            task.UpdatedAt =
                DateTime.UtcNow.ToString("O");
        }


        private static bool RemoveLegacySemanticDecompositionMigration(
            KanbanTask task)
        {
            if (task == null ||
                string.IsNullOrWhiteSpace(task.Description) ||
                !task.Description.StartsWith(
                    "[SemanticDecompositionMigration]",
                    StringComparison.Ordinal))
            {
                return false;
            }

            const string legacyPrefix =
                "[SemanticDecompositionMigration]\n" +
                "This is now a coordination card. Before implementing or mutating the shared scene, create the complete ordered child-card batch described below. Use separate cards for each scene-construction/configuration step. Create one smallest independently executable child per semantic scene outcome, declare exact artifact/resource ownership, and put every shared scene mutation in one dependency chain. Do not implement the bundled scene work in this parent run.\n\n";

            if (task.Description.StartsWith(
                    legacyPrefix,
                    StringComparison.Ordinal))
            {
                task.Description =
                    task.Description.Substring(legacyPrefix.Length);
            }
            else
            {
                int contractStart =
                    task.Description.IndexOf(
                        "\n\n",
                        StringComparison.Ordinal);

                task.Description =
                    contractStart >= 0
                        ? task.Description.Substring(contractStart + 2)
                        : string.Empty;
            }

            task.AcceptanceCriteria =
                Regex.Replace(
                        task.AcceptanceCriteria ?? string.Empty,
                        @"(?:\r?\n)?- Every generated child card must reach Done with a successful review before this coordination card completes\.",
                        string.Empty,
                        RegexOptions.IgnoreCase |
                        RegexOptions.CultureInvariant)
                    .TrimEnd();

            task.RequestTitle = string.Empty;
            task.RequestDescription = string.Empty;
            task.RequestAcceptanceCriteria = string.Empty;

            return true;
        }


        private static int NormalizeLoadedTaskHierarchy(
            IList<KanbanTask> tasks)
        {
            int changes = 0;

            Dictionary<string, KanbanTask> byId =
                tasks
                    .Where(task => task != null)
                    .ToDictionary(
                        task => task.Id,
                        StringComparer.Ordinal);

            foreach (KanbanTask task in tasks)
            {
                if (RemoveLegacySemanticDecompositionMigration(task))
                {
                    changes++;
                }

                bool carriesAtomicLeafState =
                    task.SuppressAutoDecomposition ||
                    (task.NextAgentMessage ?? string.Empty).StartsWith(
                        "[AssociationLeaf]",
                        StringComparison.Ordinal);

                if (carriesAtomicLeafState)
                {
                    string atomicArtifactPath =
                        GetSingleOwnedScriptPath(task);

                    if (string.IsNullOrWhiteSpace(atomicArtifactPath) ||
                        !IsStandaloneScriptAssociationLeaf(
                            task,
                            atomicArtifactPath))
                    {
                        task.SuppressAutoDecomposition = false;

                        if ((task.NextAgentMessage ?? string.Empty).StartsWith(
                                "[AssociationLeaf]",
                                StringComparison.Ordinal))
                        {
                            task.NextAgentMessage = string.Empty;
                        }

                        changes++;
                    }
                }

                if (!string.IsNullOrWhiteSpace(task.ParentTaskId) &&
                    (!byId.ContainsKey(task.ParentTaskId) ||
                     string.Equals(
                         task.ParentTaskId,
                         task.Id,
                         StringComparison.Ordinal)))
                {
                    task.ParentTaskId = string.Empty;
                    changes++;
                }

                bool failedDecomposition =
                    task.Status == TaskStatus.Failed &&
                    (IsDecompositionValidationFailure(task.AgentError) ||
                     IsDecompositionValidationFailure(task.BlockedReason) ||
                     IsDecompositionValidationFailure(task.ReviewResult));

                bool pausedDecomposition =
                    task.Status == TaskStatus.Todo &&
                    string.Equals(
                        task.ReviewStatus,
                        "Decomposition Blocked",
                        StringComparison.Ordinal) &&
                    IsDecompositionValidationFailure(task.BlockedReason);

                if ((failedDecomposition || pausedDecomposition) &&
                    task.DecompositionFailureCount >= 2 &&
                    task.DecompositionPolicyVersion <
                        CurrentDecompositionPolicyVersion)
                {
                    bool hasRetainedDrafts =
                        tasks.Any(
                            child =>
                                child != null &&
                                child.IsDecompositionDraft &&
                                string.Equals(
                                    child.ParentTaskId,
                                    task.Id,
                                    StringComparison.Ordinal));

                    task.Status = TaskStatus.Todo;
                    task.AgentError = string.Empty;
                    task.BlockedReason = string.Empty;
                    task.AgentStartedAt = string.Empty;
                    task.AgentFinishedAt = string.Empty;
                    task.ReviewStatus = string.Empty;
                    task.ReviewResult = string.Empty;
                    task.ReviewEvidence = string.Empty;
                    task.ReviewFailureKind = string.Empty;
                    task.ReviewFailureCount = 0;
                    task.DecompositionFailureCount =
                        hasRetainedDrafts ? 1 : 0;
                    task.DecompositionPolicyVersion =
                        CurrentDecompositionPolicyVersion;
                    task.NextAgentMessage =
                        hasRetainedDrafts
                            ? "[PlanningValidationFailure] AIRuntime upgraded this proposal to authoritative Planning reconciliation. Submit the complete desired direct-child graph: restate cards that should remain, revise invalid contracts, and omit obsolete proposals."
                            : string.Empty;
                    task.RequestTitle = string.Empty;
                    task.RequestDescription = string.Empty;
                    task.RequestAcceptanceCriteria = string.Empty;
                    changes++;
                }

                if (task.Status == TaskStatus.Crashed &&
                    task.CapabilityBudgetPolicyVersion <
                        CurrentCapabilityBudgetPolicyVersion &&
                    IsCapabilityRoundLimitFailure(
                        task.AgentError))
                {
                    task.Status = TaskStatus.Todo;
                    task.AgentResult = string.Empty;
                    task.AgentError = string.Empty;
                    task.AgentStartedAt = string.Empty;
                    task.AgentFinishedAt = string.Empty;
                    task.ReviewStatus = string.Empty;
                    task.ReviewResult = string.Empty;
                    task.ReviewEvidence = string.Empty;
                    task.ReviewFailureKind = string.Empty;
                    task.ReviewFailureCount = 0;
                    task.CapabilityBudgetPolicyVersion =
                        CurrentCapabilityBudgetPolicyVersion;
                    task.NextAgentMessage = string.Empty;
                    task.RequestTitle = string.Empty;
                    task.RequestDescription = string.Empty;
                    task.RequestAcceptanceCriteria = string.Empty;
                    changes++;
                }

                bool recoverableFalseComponentInspection =
                    IsRecoverableFalseComponentInspectionFailure(
                        task);

                bool recoverableCapabilityNoProgress =
                    IsCapabilityNoProgressFailure(
                        task.AgentError);

                bool recoverableLegacyInactivityLoop =
                    TryGetLegacyRepeatedTailCapabilityFailure(
                        task,
                        out string repeatedCapabilityName,
                        out string repeatedCapabilityFailure);

                recoverableCapabilityNoProgress =
                    recoverableCapabilityNoProgress ||
                    recoverableLegacyInactivityLoop;

                bool recoverableMalformedProviderToolCall =
                    IsLegacyMalformedProviderToolCallFailure(
                        task.AgentError);

                bool recoverableComponentNamespaceMismatch =
                    IsRecoverableComponentNamespaceMismatch(
                        task);

                string capabilityNoProgressError =
                    recoverableCapabilityNoProgress
                        ? task.AgentError +
                          (recoverableLegacyInactivityLoop
                              ? "\n\nRepeated tail failure from execution memory (" +
                                repeatedCapabilityName +
                                "): " +
                                repeatedCapabilityFailure
                              : string.Empty)
                        : string.Empty;

                string capabilityNoProgressMissingArtifact =
                    string.Empty;

                if (recoverableCapabilityNoProgress)
                {
                    TryGetMissingProducedArtifact(
                        task,
                        out capabilityNoProgressMissingArtifact);
                }

                if ((task.Status == TaskStatus.Crashed ||
                     (task.Status == TaskStatus.Failed &&
                      recoverableComponentNamespaceMismatch)) &&
                    task.CapabilityRecoveryPolicyVersion <
                        CurrentCapabilityRecoveryPolicyVersion &&
                    (IsRecoverableCapabilityArgumentFailure(
                         task.AgentError) ||
                     IsLegacyDuplicateToolLoopFailure(
                          task.AgentError) ||
                      recoverableMalformedProviderToolCall ||
                      recoverableCapabilityNoProgress ||
                      recoverableFalseComponentInspection ||
                      recoverableComponentNamespaceMismatch))
                {
                    bool resumeCompletedImplementation =
                        recoverableFalseComponentInspection &&
                        !string.IsNullOrWhiteSpace(
                            task.AgentResult);

                    task.Status =
                        resumeCompletedImplementation
                            ? TaskStatus.Review
                            : TaskStatus.Todo;

                    if (!resumeCompletedImplementation)
                    {
                        task.AgentResult = string.Empty;
                    }

                    if (recoverableFalseComponentInspection)
                    {
                        // The rejected run contains successful source reads
                        // which the next prompt would otherwise tell the agent
                        // not to repeat. Fresh current-run captures are still
                        // mandatory for verification, so discard this stale
                        // journal together with the synthetic prerequisite.
                        task.ExecutionMemory = string.Empty;
                        ResetCardMemory(task);
                    }

                    task.AgentError = string.Empty;
                    task.AgentStartedAt = string.Empty;
                    task.AgentFinishedAt = string.Empty;
                    task.ReviewStatus =
                        resumeCompletedImplementation
                            ? "Pending"
                            : string.Empty;
                    task.ReviewResult = string.Empty;
                    task.ReviewEvidence = string.Empty;
                    task.ReviewFailureKind = string.Empty;
                    task.ReviewFailureCount = 0;
                    task.ReviewStartedAt = string.Empty;
                    task.ReviewFinishedAt = string.Empty;
                    task.ReviewEvidenceFingerprint = string.Empty;
                    task.LastRejectedReviewEvidenceFingerprint = string.Empty;
                    task.LastRejectedReviewOutcomeFingerprint = string.Empty;
                    task.ReviewDependencyStateFingerprint = string.Empty;
                    task.BlockedByTaskId = string.Empty;
                    task.BlockedReason = string.Empty;
                    task.CapabilityRecoveryPolicyVersion =
                        CurrentCapabilityRecoveryPolicyVersion;
                    task.NextAgentMessage =
                        recoverableComponentNamespaceMismatch
                            ? "[ComponentTypeRecovery] AIRuntime corrected the prior Unity component namespace mismatch. Preserve the existing scene and execution memory, inspect the current hierarchy, add the built-in UnityEngine.Canvas component without installing packages or requesting Network permission, then complete only the remaining acceptance criteria."
                            : recoverableMalformedProviderToolCall
                            ? "[ModelProtocolRecovery] AIRuntime upgraded this provider batching shape into ordered capability calls. Preserve persisted execution memory and existing artifacts, inspect prior successful results, and continue only the unfinished work."
                            : recoverableCapabilityNoProgress
                            ? BuildCapabilityNoProgressRecoveryMessage(
                                task,
                                capabilityNoProgressError,
                                capabilityNoProgressMissingArtifact)
                            : string.Empty;
                    task.RequestTitle = string.Empty;
                    task.RequestDescription = string.Empty;
                    task.RequestAcceptanceCriteria = string.Empty;
                    changes++;
                }

                // Older authoritative review reads could recover a missing
                // declared path by filename and then classify the resulting
                // contradiction as an evidence gap. A typed MissingArtifact
                // record is implementation evidence and must return to the
                // owning worker, never wait for an unchanged snapshot.
                if ((task.Status == TaskStatus.Review ||
                     task.Status == TaskStatus.Failed) &&
                    string.Equals(
                        task.ReviewFailureKind,
                        "EvidenceGap",
                        StringComparison.OrdinalIgnoreCase) &&
                    HasReviewEvidenceFailureKind(
                        task.ReviewEvidence,
                        "MissingArtifact"))
                {
                    task.Status = TaskStatus.Todo;
                    task.ReviewStatus = "Rework Requested";
                    task.ReviewFailureKind = "ImplementationDefect";
                    task.ReviewFailureCount =
                        Math.Max(1, task.ReviewFailureCount);
                    task.BlockedByTaskId = string.Empty;
                    task.BlockedReason = string.Empty;
                    task.ReviewDependencyStateFingerprint = string.Empty;
                    task.LastRejectedReviewEvidenceFingerprint = string.Empty;
                    task.LastRejectedReviewOutcomeFingerprint = string.Empty;
                    task.NextAgentMessage =
                        "[ReviewFailureKind: ImplementationDefect] AIRuntime corrected a legacy review that contained authoritative MissingArtifact evidence. Create or restore every declared missing artifact, verify its exact path, and return the card for fresh review.\n\n" +
                        (task.ReviewResult ?? string.Empty);
                    changes++;
                }

                // Older builds only inspected the first non-empty review
                // line. Recover cards that were incorrectly filed as failed
                // even though their persisted review contains an
                // unambiguous PASS verdict later in the response.
                if ((task.Status == TaskStatus.Crashed ||
                     task.Status == TaskStatus.Failed) &&
                    string.Equals(
                        task.ReviewStatus,
                        "Failed",
                        StringComparison.OrdinalIgnoreCase) &&
                    string.IsNullOrWhiteSpace(task.AgentError) &&
                    HasReviewVerdict(
                        task.ReviewResult,
                        "PASS"))
                {
                    task.Status = TaskStatus.Done;
                    task.ReviewStatus = "Passed";
                    changes++;
                }

                if (task.Status == TaskStatus.Crashed &&
                    ((string.Equals(
                          task.ReviewStatus,
                          "Failed",
                          StringComparison.OrdinalIgnoreCase) &&
                      string.IsNullOrWhiteSpace(task.AgentError) &&
                      HasReviewVerdict(task.ReviewResult, "FAIL")) ||
                     IsDeterministicCompletionFailure(task.AgentError)))
                {
                    task.Status = TaskStatus.Failed;
                    changes++;
                }

                // Older review classification treated the standard
                // "Missing or incorrect work" heading as an implementation
                // defect even when every finding underneath it described
                // unavailable read-back evidence. Reopen those cards for a
                // deterministic evidence review; do not rerun their mutation.
                if (task.Status == TaskStatus.Failed &&
                    string.Equals(
                        task.ReviewFailureKind,
                        "EvidenceGap",
                        StringComparison.OrdinalIgnoreCase) &&
                    string.IsNullOrWhiteSpace(task.AgentError) &&
                    !string.IsNullOrWhiteSpace(task.AgentResult))
                {
                    task.Status = TaskStatus.Review;
                    task.ReviewStatus = "Pending";
                    task.ReviewFailureCount = 0;
                    task.BlockedByTaskId = string.Empty;
                    task.BlockedReason = string.Empty;
                    task.ReviewDependencyStateFingerprint = string.Empty;
                    task.NextAgentMessage =
                        "[ReviewFailureKind: EvidenceGap] AIRuntime reopened this legacy terminal evidence failure for a fresh evidence-only review. Preserve the completed implementation; unchanged evidence must not consume another retry.\n\n" +
                        task.ReviewResult;
                    changes++;
                }

                if (task.Status == TaskStatus.Failed &&
                    string.Equals(
                        task.ReviewStatus,
                        "Failed",
                        StringComparison.OrdinalIgnoreCase) &&
                    string.Equals(
                        task.ReviewFailureKind,
                        "ImplementationDefect",
                        StringComparison.OrdinalIgnoreCase) &&
                    string.IsNullOrWhiteSpace(task.AgentError) &&
                    string.Equals(
                        ClassifyReviewFailure(
                            task.ReviewResult,
                            string.Empty),
                        "EvidenceGap",
                        StringComparison.Ordinal))
                {
                    task.Status = TaskStatus.Review;
                    task.ReviewStatus = "Pending";
                    task.ReviewFailureKind = "EvidenceGap";
                    task.ReviewFailureCount = 0;
                    task.NextAgentMessage =
                        "[ReviewFailureKind: EvidenceGap] AIRuntime reopened this legacy evidence-only failure. Review the completed work using fresh authoritative evidence; do not repeat the implementation mutation.\n\n" +
                        task.ReviewResult;
                    changes++;
                }

                if (task.Status == TaskStatus.Done &&
                    !string.Equals(
                        task.ReviewStatus,
                        "Passed",
                        StringComparison.OrdinalIgnoreCase))
                {
                    task.Status = TaskStatus.Failed;
                    task.ReviewStatus =
                        string.IsNullOrWhiteSpace(task.ReviewStatus)
                            ? "Failed"
                            : task.ReviewStatus;
                    changes++;
                }

                if (task.Status == TaskStatus.Review &&
                    string.Equals(
                        task.ReviewStatus,
                        "Crashed",
                        StringComparison.OrdinalIgnoreCase) &&
                    (task.AgentError ?? string.Empty).IndexOf(
                        "Coordination rework did not create any corrective child cards",
                        StringComparison.OrdinalIgnoreCase) >= 0)
                {
                    List<KanbanTask> directChildren =
                        tasks
                            .Where(
                                child =>
                                    child != null &&
                                    string.Equals(
                                        child.ParentTaskId,
                                        task.Id,
                                        StringComparison.Ordinal))
                            .ToList();

                    bool verifiedBatch =
                        directChildren.Count > 0 &&
                        directChildren.All(
                            child =>
                                child.Status == TaskStatus.Done &&
                                string.Equals(
                                    child.ReviewStatus,
                                    "Passed",
                                    StringComparison.OrdinalIgnoreCase)) &&
                        directChildren.Any(
                            child =>
                                IsFinalIntegrationVerificationTask(child) &&
                                CoversParentAcceptanceCriteria(task, child) &&
                                HasPersistedFinalVerificationEvidence(child));

                    if (verifiedBatch)
                    {
                        task.AgentError = string.Empty;
                        task.ReviewStatus = "Pending";
                        task.ReviewFailureKind = "EvidenceGap";
                        changes++;
                    }
                }
            }

            foreach (KanbanTask task in tasks)
            {
                HashSet<string> chain =
                    new HashSet<string>(
                        StringComparer.Ordinal);

                KanbanTask current = task;

                while (current != null &&
                       !string.IsNullOrWhiteSpace(current.ParentTaskId))
                {
                    if (!chain.Add(current.Id))
                    {
                        task.ParentTaskId = string.Empty;
                        changes++;
                        break;
                    }

                    byId.TryGetValue(
                        current.ParentTaskId,
                        out current);
                }
            }

            bool changed;

            do
            {
                changed = false;

                foreach (KanbanTask parent in tasks)
                {
                    bool hasIncompleteChild =
                        tasks.Any(
                            child =>
                                child != null &&
                                string.Equals(
                                    child.ParentTaskId,
                                    parent.Id,
                                    StringComparison.Ordinal) &&
                                child.Status != TaskStatus.Done);

                    if (hasIncompleteChild &&
                        parent.Status != TaskStatus.Todo &&
                        parent.Status != TaskStatus.Crashed &&
                        parent.Status != TaskStatus.Failed)
                    {
                        parent.Status = TaskStatus.Todo;
                        changes++;
                        changed = true;
                    }
                }
            }
            while (changed);

            return changes;
        }


        private static int RecoverLegacyTerminalDecompositionDrafts(
            IList<KanbanTask> tasks)
        {
            if (tasks == null ||
                tasks.Count == 0)
            {
                return 0;
            }

            List<KanbanTask> parents =
                tasks
                    .Where(
                        parent =>
                            parent != null &&
                            (parent.Status == TaskStatus.Crashed ||
                             parent.Status == TaskStatus.Failed) &&
                            tasks.Any(
                                child =>
                                    child != null &&
                                    child.IsDecompositionDraft &&
                                    string.Equals(
                                        child.CreationSource,
                                        "create_kanban_child_task",
                                        StringComparison.Ordinal) &&
                                    string.Equals(
                                        child.ParentTaskId,
                                        parent.Id,
                                        StringComparison.Ordinal)))
                    .ToList();

            int recovered = 0;

            foreach (KanbanTask parent in parents)
            {
                string previousError =
                    parent.AgentError ??
                    string.Empty;

                parent.Status =
                    TaskStatus.Todo;

                parent.AgentError =
                    string.Empty;

                parent.ReviewStatus =
                    "Planning Retry";

                parent.DecompositionFailureCount =
                    Math.Max(
                        1,
                        parent.DecompositionFailureCount);

                parent.NextAgentMessage =
                    "[PlanningValidationFailure] AIRuntime recovered an interrupted Planning proposal. Submit the complete desired direct-child graph again; matching titles retain stable IDs and omitted proposal cards are retired.\n\n" +
                    previousError;

                TouchTask(parent);

                recovered++;
            }

            return recovered;
        }


        private static int RecoverInterruptedTasks(
            IEnumerable<KanbanTask> tasks)
        {
            int recovered =
                0;

            foreach (
                KanbanTask task
                in tasks ??
                   Enumerable.Empty<KanbanTask>())
            {
                if (task ==
                    null)
                {
                    continue;
                }

                if (HasLiveDispatchLease(task, DateTimeOffset.UtcNow)) continue;
                if (task.Dispatch?.Outcome == "Running")
                {
                    task.Dispatch.Outcome = "Interrupted";
                    task.Dispatch.LeaseUntilUtc = string.Empty;
                }

                bool interruptedImplementation =
                    task.Status ==
                    TaskStatus.InProgress;

                bool interruptedReview =
                    task.Status ==
                        TaskStatus.Review &&
                    string.Equals(
                        task.ReviewStatus,
                        "Running",
                        StringComparison.OrdinalIgnoreCase);

                if (!interruptedImplementation &&
                    !interruptedReview)
                {
                    if (task.Status == TaskStatus.Todo &&
                        ExecutionMemoryShowsInterruptedAssetRefresh(
                            task.ExecutionMemory) &&
                        StageAssemblyReloadContinuation(task))
                    {
                        recovered++;
                    }

                    continue;
                }

                if (interruptedReview)
                {
                    task.Status =
                        TaskStatus.Review;

                    task.ReviewStatus =
                        "Pending";

                    task.ReviewResult =
                        string.Empty;

                    task.ReviewFinishedAt =
                        string.Empty;
                }
                else
                {
                    task.Status =
                        TaskStatus.Todo;

                    task.AgentError =
                        string.Empty;

                    task.ReviewStatus =
                        string.Empty;

                    StageAssemblyReloadContinuation(task);
                }

                task.AgentFinishedAt =
                    string.Empty;

                recovered++;
            }

            return recovered;
        }


        private static bool ExecutionMemoryShowsInterruptedAssetRefresh(
            string executionMemory)
        {
            return !string.IsNullOrWhiteSpace(executionMemory) &&
                   executionMemory.IndexOf(
                       "refresh_unity_assets",
                       StringComparison.Ordinal) >= 0 &&
                   executionMemory.IndexOf(
                       "RunCancelled",
                       StringComparison.Ordinal) >= 0;
        }


        private static bool StageAssemblyReloadContinuation(
            KanbanTask task)
        {
            if (task == null ||
                (!string.IsNullOrWhiteSpace(task.NextAgentMessage) &&
                 task.NextAgentMessage.IndexOf(
                     "[AssemblyReloadResume]",
                     StringComparison.Ordinal) >= 0))
            {
                return false;
            }

            const string reloadContinuation =
                "[AssemblyReloadResume] Unity imported a source mutation from the previous attempt and interrupted that worker at the domain boundary. " +
                "Treat the imported artifacts as durable progress. Do not rewrite or force-refresh unchanged scripts; inspect compilation/current Unity state and continue with the next unsatisfied acceptance criterion.";

            task.NextAgentMessage =
                string.IsNullOrWhiteSpace(task.NextAgentMessage)
                    ? reloadContinuation
                    : reloadContinuation +
                      "\n\n" +
                      task.NextAgentMessage;

            return true;
        }


        private static int PauseActiveRunsForReload(
            IEnumerable<KanbanTask> tasks)
        {
            return RecoverInterruptedTasks(
                tasks);
        }


        private void CheckpointActiveRunScenesForAssemblyReload()
        {
            foreach (string taskId in
                     _activeAgentRuns.Values
                         .Where(run => run != null)
                         .Select(run => run.TaskId)
                         .Where(id => !string.IsNullOrWhiteSpace(id))
                         .Distinct(StringComparer.Ordinal))
            {
                KanbanTask task = FindTask(taskId);

                if (task == null)
                {
                    continue;
                }

                if (!TryCheckpointOwnedDirtyScenes(
                        task,
                        out int checkpointedSceneCount,
                        out string error))
                {
                    Debug.LogError(
                        $"[Kanban] Could not checkpoint the owned scene for '{task.Title}' before assembly reload: {error}");
                    continue;
                }

                if (checkpointedSceneCount > 0)
                {
                    Debug.Log(
                        $"[Kanban] Checkpointed {checkpointedSceneCount} owned dirty scene(s) for '{task.Title}' before assembly reload.");
                }
            }
        }


        private static bool TryCheckpointOwnedDirtyScenes(
            KanbanTask task,
            out int checkpointedSceneCount,
            out string error)
        {
            checkpointedSceneCount = 0;
            error = string.Empty;

            if (task == null)
            {
                return true;
            }

            foreach (string declaredPath in
                     (task.ProducedArtifactPaths ?? new List<string>())
                         .Where(path => !string.IsNullOrWhiteSpace(path))
                         .Select(path => path.Trim().Replace('\\', '/'))
                         .Where(path =>
                             path.StartsWith(
                                 "Assets/",
                                 StringComparison.Ordinal) &&
                             path.EndsWith(
                                 ".unity",
                                 StringComparison.OrdinalIgnoreCase))
                         .Distinct(StringComparer.OrdinalIgnoreCase))
            {
                try
                {
                    string scenePath =
                        EditorSceneAuthoringUtility.ValidateScenePath(
                            declaredPath);

                    UnityEngine.SceneManagement.Scene scene =
                        EditorSceneAuthoringUtility.FindLoadedScene(
                            scenePath);

                    if (!scene.IsValid() ||
                        !scene.isLoaded ||
                        !scene.isDirty)
                    {
                        continue;
                    }

                    if (!UnityEditor.SceneManagement.EditorSceneManager.SaveScene(
                            scene) ||
                        scene.isDirty)
                    {
                        error =
                            $"Unity did not durably save '{scenePath}'.";
                        return false;
                    }

                    checkpointedSceneCount++;
                }
                catch (Exception exception)
                {
                    error =
                        $"'{declaredPath}' could not be checkpointed: {exception.Message}";
                    return false;
                }
            }

            if (checkpointedSceneCount > 0)
            {
                AssetDatabase.SaveAssets();
            }

            return true;
        }


        private static string GetOriginalRequestValue(
            string snapshot,
            string currentValue)
        {
            return string.IsNullOrEmpty(
                snapshot)
                ? currentValue ??
                  string.Empty
                : snapshot;
        }


        private float GetAgentOutputHeight(
            string output)
        {
            float availableWidth =
                Mathf.Max(
                    200,
                    EditorGUIUtility.currentViewWidth -
                    64);

            return Mathf.Clamp(
                EditorStyles.textArea.CalcHeight(
                    new GUIContent(
                        output ??
                        string.Empty),
                    availableWidth),
                80,
                800);
        }


        private static void TouchTask(
            KanbanTask task)
        {
            task.UpdatedAt =
                DateTime.UtcNow.ToString(
                    "O");
        }


        private static bool Contains(
            string value,
            string search)
        {
            return !string.IsNullOrWhiteSpace(
                       value) &&
                   value.IndexOf(
                       search,
                       StringComparison.OrdinalIgnoreCase) >= 0;
        }


        private static string Truncate(
            string value,
            int maximumLength)
        {
            if (string.IsNullOrEmpty(
                    value) ||
                value.Length <=
                maximumLength)
            {
                return value;
            }


            return value.Substring(
                       0,
                       maximumLength - 1) +
                   "…";
        }


        private static string GetStatusLabel(
            TaskStatus status)
        {
            switch (status)
            {
                case TaskStatus.Backlog:
                    return "Backlog";

                case TaskStatus.Todo:
                    return "To Do";

                case TaskStatus.InProgress:
                    return "In Progress";

                case TaskStatus.Review:
                    return "Review";

                case TaskStatus.Done:
                    return "Done";

                case TaskStatus.Crashed:
                    return "Crashed";

                case TaskStatus.Failed:
                    return "Failed";

                default:
                    throw new ArgumentOutOfRangeException(
                        nameof(status),
                        status,
                        "Unknown task status.");
            }
        }


        private static string GetPriorityLabel(
            TaskPriority priority)
        {
            switch (priority)
            {
                case TaskPriority.Low:
                    return "LOW";

                case TaskPriority.Normal:
                    return "NORMAL";

                case TaskPriority.High:
                    return "HIGH";

                case TaskPriority.Critical:
                    return "CRITICAL";

                default:
                    throw new ArgumentOutOfRangeException(
                        nameof(priority),
                        priority,
                        "Unknown task priority.");
            }
        }


        private static Color GetPriorityColor(
            TaskPriority priority)
        {
            switch (priority)
            {
                case TaskPriority.Low:
                    return new Color(
                        0.75f,
                        0.85f,
                        1f);

                case TaskPriority.Normal:
                    return Color.white;

                case TaskPriority.High:
                    return new Color(
                        1f,
                        0.8f,
                        0.45f);

                case TaskPriority.Critical:
                    return new Color(
                        1f,
                        0.45f,
                        0.45f);

                default:
                    return Color.white;
            }
        }


        private static Color GetTaskCardColor(
            KanbanTask task)
        {
            if (task != null &&
                task.Status ==
                    TaskStatus.Crashed)
            {
                return new Color(
                    1f,
                    0.45f,
                    0.45f);
            }

            if (task != null &&
                task.Status ==
                    TaskStatus.Failed)
            {
                return new Color(
                    1f,
                    0.65f,
                    0.35f);
            }

            if (task != null &&
                task.Status ==
                TaskStatus.Done)
            {
                if (string.Equals(
                        task.ReviewStatus,
                        "Passed",
                        StringComparison.OrdinalIgnoreCase))
                {
                    return new Color(
                        0.55f,
                        0.9f,
                        0.6f);
                }


                if (string.Equals(
                        task.ReviewStatus,
                        "Failed",
                        StringComparison.OrdinalIgnoreCase))
                {
                    return new Color(
                        1f,
                        0.55f,
                        0.55f);
                }
            }


            return GetPriorityColor(
                task?.Priority ??
                TaskPriority.Normal);
        }


        private static string GetDefaultBoardPath()
        {
            if (!string.IsNullOrWhiteSpace(
                    BoardPathOverrideForTests))
            {
                return BoardPathOverrideForTests;
            }

            string explicitPath = Environment.GetEnvironmentVariable("AIRUNTIME_KANBAN_BOARD_PATH");
            if (!string.IsNullOrWhiteSpace(explicitPath)) return Path.GetFullPath(explicitPath);


            string documents =
                Environment.GetFolderPath(
                    Environment.SpecialFolder.MyDocuments);


            if (string.IsNullOrWhiteSpace(
                    documents))
            {
                documents =
                    Application.dataPath;
            }


            return Path.Combine(
                documents,
                "AIRuntime",
                "kanban",
                "board.json");
        }


        private static string GetAgentActivationStatePath()
        {
            string documents =
                Environment.GetFolderPath(
                    Environment.SpecialFolder.MyDocuments);

            if (string.IsNullOrWhiteSpace(
                    documents))
            {
                documents =
                    Application.dataPath;
            }

            return Path.Combine(
                documents,
                "AIRuntime",
                "kanban",
                "agents.json");
        }
    }
}
