using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

using AIRuntime.Editor;
using AIRuntime.Runtime.Services.Agent;

using Newtonsoft.Json.Linq;

using NUnit.Framework;

using UnityEngine;


namespace AIRuntime.Tests
{
    public sealed class KanbanAgentRunnerPromptTests
    {
        private Type _runnerType;

        private string _testBoardDirectory;


        [SetUp]
        public void SetUp()
        {
            _testBoardDirectory =
                Path.Combine(
                    Path.GetTempPath(),
                    "AIRuntimeKanbanTests",
                    Guid.NewGuid().ToString("N"));

            Directory.CreateDirectory(
                _testBoardDirectory);

            SetBoardPathOverride(
                Path.Combine(
                    _testBoardDirectory,
                    "board.json"));

            _runnerType =
                typeof(AIRuntimeKanbanSession)
                    .Assembly
                    .GetType(
                        "AIRuntime.Editor.AIRuntimeKanbanAgentRunner",
                        throwOnError: true);
        }


        [TearDown]
        public void TearDown()
        {
            SetBoardPathOverride(
                null);

            if (!string.IsNullOrWhiteSpace(
                    _testBoardDirectory) &&
                Directory.Exists(
                    _testBoardDirectory))
            {
                Directory.Delete(
                    _testBoardDirectory,
                    recursive: true);
            }
        }


        private static void SetBoardPathOverride(
            string path)
        {
            FieldInfo field =
                typeof(AIRuntimeKanbanSession).GetField(
                    "BoardPathOverrideForTests",
                    BindingFlags.Static |
                    BindingFlags.NonPublic);

            Assert.NotNull(
                field);

            field.SetValue(
                null,
                path);
        }


        [Test]
        public void FollowUp_ReplacesOriginalCardAsEffectiveRequest()
        {
            string effectiveRequest =
                Invoke<string>(
                    "GetEffectiveRequest",
                    "User selection",
                    "Please tell me about the selected objects",
                    string.Empty,
                    "Please summarise the contents of DEPENDENCY_RULES.md");

            Assert.AreEqual(
                "Please summarise the contents of DEPENDENCY_RULES.md",
                effectiveRequest);
        }


        [Test]
        public void InitialSelectionPrompt_DoesNotRequestFolderContents()
        {
            const string request =
                "User selection\nPlease tell me about the selected objects\n";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "User selection",
                    "Please tell me about the selected objects",
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.Contains(
                "Call get_editor_selection",
                prompt);

            StringAssert.Contains(
                "do not inspect or list its contents",
                prompt);

            StringAssert.DoesNotContain(
                "MANDATORY FILE CONTENT READ",
                prompt);
        }


        [Test]
        public void FileSummaryFollowUp_IsActiveAndRequiresReadFile()
        {
            const string followUp =
                "Please summarise the contents of DEPENDENCY_RULES.md";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "User selection",
                    "Please tell me about the selected objects",
                    string.Empty,
                    "The selected folder is Assets/Runtime/Docs.",
                    followUp,
                    followUp,
                    null);

            StringAssert.Contains(
                "CURRENT ACTIVE REQUEST",
                prompt);

            StringAssert.Contains(
                followUp,
                prompt);

            StringAssert.Contains(
                "MANDATORY FILE CONTENT READ",
                prompt);

            StringAssert.Contains(
                "Call read_file before answering",
                prompt);

            StringAssert.Contains(
                "Original card description (context only",
                prompt);

            StringAssert.DoesNotContain(
                "MANDATORY: Call get_editor_selection",
                prompt);
        }


        [Test]
        public void InspectionTask_DoesNotInheritReadRequirementFromDependencyTitle()
        {
            const string description =
                "Select the Player GameObject and call get_editor_selection then inspect_editor_component on it. " +
                "Depends on sibling task \"Read PlayerBallController.cs and FallResetController.cs file contents\" being Done.";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Inspect Player ball GameObject components in saved scene",
                    description,
                    "Confirm the components are present.",
                    string.Empty,
                    string.Empty,
                    description,
                    null);

            StringAssert.DoesNotContain(
                "MANDATORY FILE CONTENT READ",
                prompt);

            StringAssert.Contains(
                "inspect_editor_component",
                prompt);
        }


        [Test]
        public void SceneAuthoringRework_DoesNotInheritFileReadFromReviewerMetaCommentary()
        {
            const string originalRequest =
                "Build Ground GameObject\n" +
                "Create the Ground GameObject with a visible MeshRenderer and enabled non-trigger BoxCollider.\n" +
                "No files outside Assets/JumpingBallGame may be changed.";

            const string reworkMessage =
                "[ReviewFailureKind: ImplementationDefect] Review requested further work. Address every current finding before returning the card for another evidence-based review:\n\n" +
                "KANBAN_REVIEW: FAIL\nThe reviewer did not use read tools to confirm files. " +
                "Use inspect_scene and inspect_game_object to verify Ground.";

            string classificationRequest =
                Invoke<string>(
                    "GetClassificationRequest",
                    originalRequest,
                    reworkMessage,
                    reworkMessage);

            Assert.AreEqual(
                originalRequest,
                classificationRequest);

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresImplementationFileContentRead",
                    classificationRequest));

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Build Ground GameObject",
                    "Create the Ground GameObject with a visible MeshRenderer and enabled non-trigger BoxCollider.",
                    "No files outside Assets/JumpingBallGame may be changed.",
                    string.Empty,
                    reworkMessage,
                    classificationRequest,
                    null);

            StringAssert.Contains(
                reworkMessage,
                prompt);

            StringAssert.DoesNotContain(
                "MANDATORY FILE CONTENT READ",
                prompt);
        }


        [Test]
        public void ScriptRework_DoesNotBecomeComponentTroubleshootingFromReviewerMetaCommentary()
        {
            const string originalRequest =
                "Create BallController script\n" +
                "Create Assets/JumpingBallGame/Scripts/BallController.cs. " +
                "It uses a Rigidbody to move on X and freezes Z position.\n" +
                "BallController.cs exists and handles movement and grounded jumping.";

            const string reworkMessage =
                "[ReviewFailureKind: ImplementationDefect] The first review requested rework. Address every item below before returning the card for another review:\n\n" +
                "KANBAN_REVIEW: FAIL\nThe component inspection evidence is unavailable, " +
                "so the review cannot verify the diagnosis or any claimed fix.";

            string classificationRequest =
                Invoke<string>(
                    "GetClassificationRequest",
                    originalRequest,
                    reworkMessage,
                    reworkMessage);

            Assert.AreEqual(
                originalRequest,
                classificationRequest);

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresRunComponentInspection",
                    false,
                    classificationRequest,
                    reworkMessage,
                    false));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    reworkMessage));
        }


        [Test]
        public void SceneAuthoringWithoutRemovalRequest_HidesDestructiveRemovalCapability()
        {
            List<string> capabilities =
                new List<string>
                {
                    "create_game_object",
                    "remove_editor_component",
                    "inspect_game_object"
                };

            Invoke<object>(
                "ConfigureComponentRemovalCapability",
                capabilities,
                false);

            CollectionAssert.AreEqual(
                new[]
                {
                    "create_game_object",
                    "inspect_game_object"
                },
                capabilities);

            Assert.IsFalse(
                Invoke<bool>(
                    "RequestsAllSelectedMissingScriptRemoval",
                    "Create the player ball GameObject with a SphereCollider and scripts."));
        }


        [Test]
        public void ExplicitMissingScriptCleanup_KeepsRemovalCapability()
        {
            List<string> capabilities =
                new List<string>
                {
                    "remove_editor_component"
                };

            Invoke<object>(
                "ConfigureComponentRemovalCapability",
                capabilities,
                true);

            CollectionAssert.AreEqual(
                new[]
                {
                    "remove_editor_component"
                },
                capabilities);

            Assert.IsTrue(
                Invoke<bool>(
                    "RequestsAllSelectedMissingScriptRemoval",
                    "Remove missing scripts from all selected objects."));
        }


        [Test]
        public void FileSummaryReview_JudgesFollowUpAndRequiresFreshRead()
        {
            const string followUp =
                "Please summarise the contents of DEPENDENCY_RULES.md";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "User selection",
                    "Please tell me about the selected objects",
                    string.Empty,
                    "The summary was requested.",
                    followUp,
                    null,
                    null,
                    null,
                    null,
                    null,
                    followUp);

            StringAssert.Contains(
                "CURRENT ACTIVE REQUEST",
                prompt);

            StringAssert.Contains(
                "this replaces the original card request for this review",
                prompt);

            StringAssert.Contains(
                "must call read_file",
                prompt);

            StringAssert.Contains(
                "call get_editor_selection to obtain its exact FolderPath",
                prompt);

            StringAssert.Contains(
                "Merely acknowledging or repeating it is a FAIL",
                prompt);
        }


        [Test]
        public void ReviewPrompt_PreservesObjectIdentityAndCurrentStateSemantics()
        {
            const string request =
                "Verify Player and Main Camera in Assets/Game/Scenes/GameScene.unity.";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Scene review",
                    request,
                    "Main Camera is orthographic and the scene is saved.",
                    "Implementation complete.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    "authoritative component evidence",
                    request);

            StringAssert.Contains(
                "OBJECT IDENTITY RULE",
                prompt);
            StringAssert.Contains(
                "Never attribute a component or property from Player",
                prompt);
            StringAssert.Contains(
                "Dirty=false",
                prompt);
            StringAssert.Contains(
                "UNITY ENUM RULE",
                prompt);
        }


        [Test]
        public void FileSummaryReview_CannotPassWithoutReadFileEvidence()
        {
            string result =
                Invoke<string>(
                    "EnsureReviewVerdict",
                    "KANBAN_REVIEW: PASS\n\nThe request was acknowledged.",
                    null,
                    null,
                    "The summary was requested.",
                    true,
                    null,
                    null,
                    false,
                    null,
                    null);

            StringAssert.StartsWith(
                "KANBAN_REVIEW: FAIL",
                result);

            StringAssert.Contains(
                "did not complete the required read_file call",
                result);
        }

        [Test]
        public void FileSummaryReview_UsesAuthoritativeRuntimeRead()
        {
            const string followUp =
                "Please summarise the contents of DEPENDENCY_RULES.md";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "User selection",
                    "Please tell me about the selected objects",
                    string.Empty,
                    "Providers must never depend on Modules.",
                    followUp,
                    null,
                    null,
                    "Assets/Runtime/Docs/DEPENDENCY_RULES.md",
                    "Providers must never depend on Modules.",
                    null,
                    followUp);

            StringAssert.Contains(
                "Treat the following as the authoritative file contents",
                prompt);

            StringAssert.Contains(
                "Do not invent contradictions",
                prompt);

            string result =
                Invoke<string>(
                    "EnsureReviewVerdict",
                    "KANBAN_REVIEW: PASS",
                    null,
                    null,
                    "Providers must never depend on Modules.",
                    true,
                    "Providers must never depend on Modules.",
                    null,
                    false,
                    null,
                    null);

            StringAssert.StartsWith(
                "KANBAN_REVIEW: PASS",
                result);
        }


        [Test]
        public void FileSummaryReview_ResolvesNamedFileInsideSelectedFolder()
        {
            string workspaceRoot =
                Directory.GetParent(
                        Application.dataPath)
                    .FullName;

            string selectionEvidence =
                "{\"Objects\":[{\"IsFolder\":true,\"FolderPath\":\"" + Path.GetDirectoryName(TestAssetPaths.Resolve("Assets/Runtime/Docs/DEPENDENCY_RULES.md")).Replace('\\', '/') + "\"}]}";

            string resolvedPath =
                Invoke<string>(
                    "ResolveRequestedFilePath",
                    "Please summarise the contents of DEPENDENCY_RULES.md",
                    selectionEvidence,
                    workspaceRoot);

            Assert.AreEqual(
                TestAssetPaths.Resolve("Assets/Runtime/Docs/DEPENDENCY_RULES.md"),
                resolvedPath);
        }

        [Test]
        public void ColliderTroubleshooting_RequiresConcreteComponentInspection()
        {
            const string request =
                "Inspect the components of the selected object. The object's collider is not working; troubleshoot and fix it if possible.";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Collider troubleshooting",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.Contains(
                "MANDATORY: Call get_editor_selection",
                prompt);

            StringAssert.Contains(
                "MANDATORY COMPONENT INSPECTION",
                prompt);

            StringAssert.Contains(
                "call inspect_editor_component",
                prompt);

            StringAssert.Contains(
                "do not claim a fix occurred",
                prompt);
        }


        [Test]
        public void GameBuildPrompt_DoesNotRequireAPreselectedComponent()
        {
            const string request =
                "Create a simple 2.5D jumping-ball game. Use 3D objects, colliders, and Rigidbody physics. " +
                "Inspect the resulting scene and important components before reporting completion.";

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    request));

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresEditorSelection",
                    request));
        }


        [Test]
        public void SceneBuildPrompt_CanNameInspectionToolsWithoutPreselection()
        {
            const string request =
                "Create the JumpingBall scene, then use inspect_game_object and " +
                "inspect_editor_component to verify the Ball and platforms.";

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    request));

            const string capabilityVerification =
                "Verify capability: inspect_editor_component. Inspect a component.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    capabilityVerification));
        }


        [Test]
        public void ExistingSceneReference_NormalizesUniqueWrongFolder()
        {
            const string request =
                "Inspect Assets/IncorrectFolder/EmptyScene.unity and add Ground there.";

            string normalized =
                Invoke<string>(
                    "NormalizeExistingSceneReferences",
                    request);

            StringAssert.Contains(
                TestAssetPaths.EmptyScene,
                normalized);

            StringAssert.DoesNotContain(
                "Assets/IncorrectFolder/EmptyScene.unity",
                normalized);
        }


        [TestCase(
            "Create PlayerBall GameObject with mesh, collider and Rigidbody",
            "PlayerBall")]
        [TestCase(
            "Create Platform_1 GameObject in scene",
            "Platform_1")]
        [TestCase(
            "Author Goal GameObject",
            "Goal")]
        [TestCase(
            "Create a Checkpoint GameObject",
            "Checkpoint")]
        [TestCase(
            "Create an EnemyDrone GameObject",
            "EnemyDrone")]
        [TestCase(
            "Author the SpawnPoint GameObject",
            "SpawnPoint")]
        [TestCase(
            "A Cube named Platform_2 exists under Level in the saved scene.",
            "Platform_2")]
        public void CreatedGameObjectVerification_PreservesExplicitIdentifierName(
            string request,
            string expected)
        {
            Assert.AreEqual(
                expected,
                Invoke<string>(
                    "ExtractExpectedCreatedGameObjectName",
                    request));
        }


        [Test]
        public void CreatedGameObjectVerification_ResolvesPlayerBallDescription()
        {
            const string request =
                "Create a PlayerBall GameObject in the scene: a Sphere " +
                "with Rigidbody and BallMovement. Save it as a prefab.";

            Assert.AreEqual(
                "PlayerBall",
                Invoke<string>(
                    "ExtractExpectedCreatedGameObjectName",
                    request));
        }


        [Test]
        public void CreatedGameObjectVerification_ResolvesContextualCreationClause()
        {
            Assert.AreEqual(
                "SensorRig",
                Invoke<string>(
                    "ExtractExpectedCreatedGameObjectName",
                    "In the Diagnostics scene, create the sensor rig GameObject with a visible mesh."));

            Assert.AreEqual(
                "SensorRig",
                Invoke<string>(
                    "ExtractExpectedCreatedGameObjectName",
                    "Depends on sibling task \"Create Decoy GameObject\" being Done. In the scene, create the sensor rig GameObject."));
        }


        [Test]
        public void ComponentAcceptanceCriteria_EnableTargetedReviewInspection()
        {
            const string request =
                "Attach FallResetter to PlayerBall. Acceptance: inspect_editor_component confirms the component is attached and enabled.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                    request));

            Assert.AreEqual(
                "PlayerBall",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));

            const string componentEvidence =
                "{\"GameObjectName\":\"PlayerBall\",\"Components\":[{\"Type\":\"JumpingBallGame.FallResetter\",\"Enabled\":true}]}";

            string reviewPrompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Attach fall reset",
                    request,
                    "FallResetter is enabled.",
                    "Completed.",
                    null,
                    "{}",
                    null,
                    null,
                    null,
                    componentEvidence,
                    request);

            StringAssert.Contains(
                "authoritative inspect_editor_component result",
                reviewPrompt);

            StringAssert.Contains(
                componentEvidence,
                reviewPrompt);
        }


        [Test]
        public void ComponentReview_ExplicitTargetOverridesComponentNameHeuristic()
        {
            const string request =
                "Attach and configure PlayerBallController on Player. " +
                "Inspect the attached component and verify its settings.";

            Assert.AreEqual(
                "Player",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void GoalAcceptanceCriteria_ResolveGoalRatherThanExistsKeyword()
        {
            const string request =
                "Create Goal GameObject. Goal GameObject exists with visible mesh, enabled trigger collider, Goal material, and GoalTrigger component attached.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                    request));

            Assert.AreEqual(
                "Goal",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void PlayerBallInspection_ResolvesCompoundNaturalLanguageName()
        {
            const string request =
                "Inspect player ball GameObject components and confirm the " +
                "Rigidbody constraints, collider state, BallMovement, and FallReset.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                    request));

            Assert.AreEqual(
                "PlayerBall",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void PlayerBallCreationReview_RequiresFreshComponentInspection()
        {
            const string request =
                "In the JumpingBallGame scene, create the player ball GameObject: " +
                "a SphereCollider and Rigidbody with constraints freezing Z position. " +
                "Acceptance: Player ball GameObject exists with Rigidbody (Z frozen).";

            Assert.AreEqual(
                "PlayerBall",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                    request));
        }


        [Test]
        public void ArbitrarySceneNames_AreDerivedWithoutProjectSpecificAliases()
        {
            Assert.AreEqual(
                "HoverDrone",
                Invoke<string>(
                    "ExtractExpectedCreatedGameObjectName",
                    "Create the hover drone GameObject with a visible mesh."));

            Assert.AreEqual(
                "HoverDrone",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    "Inspect the hover drone GameObject and verify its settings."));

            Assert.AreEqual(
                "NavigationBrain",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "HoverDrone",
                    "Confirm HoverDrone has NavigationBrain configured."));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractExpectedCreatedGameObjectName",
                    "Create four checkpoint gates GameObject for the course."));
        }


        [Test]
        public void NamedSceneWording_ExtractsSceneNameForReviewPreflight()
        {
            Assert.AreEqual(
                "JumpingBallGame",
                Invoke<string>(
                    "ExtractNamedSceneName",
                    "In the JumpingBallGame scene, create a goal GameObject."));
        }


        [Test]
        public void ComponentReview_SelectTheGameObjectWordingResolvesNamedTarget()
        {
            const string request =
                "Verify Player components in saved scene. " +
                "Select the Player GameObject in the saved Assets/JumpingBallGame/Scenes/JumpingBallGame.unity and inspect its components. " +
                "Confirm PlayerBallController and FallResetController are present and enabled.";

            Assert.AreEqual(
                "Player",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void CameraComponentReview_TargetsMainCameraInsteadOfCurrentSelection()
        {
            const string request =
                "Inspect Main Camera and confirm SideViewCameraFollow targets PlayerBall. The Goal task is already complete.";

            Assert.AreEqual(
                "Main Camera",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void GoalComponentReview_IgnoresSiblingDependencyClause()
        {
            const string request =
                "Depends on sibling task \"Create GoalTrigger.cs\" being Done. " +
                "Using the Goal inspection and GoalTrigger.cs, confirm the Player carries a Rigidbody so trigger callbacks fire.";

            Assert.AreEqual(
                "Goal",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void GoalComponentReview_NeverTreatsWorkflowSiblingAsGameObject()
        {
            const string request =
                "This depends on sibling card Create GoalTrigger before verification. " +
                "Using the Goal inspection and GoalTrigger.cs, confirm the Player carries a Rigidbody.";

            Assert.AreEqual(
                "Goal",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void JumpReview_UsesPlayerControllerAsAuthoritativeTargetComponent()
        {
            const string request =
                "Add the jump behaviour to the existing player controller. " +
                "PlayerBall has PlayerBallController attached and enabled.";

            Assert.AreEqual(
                "PlayerBall",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));

            Assert.AreEqual(
                "PlayerBallController",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "PlayerBall",
                    request));
        }


        [Test]
        public void ReviewEffectiveRequest_PreservesCardCriteriaWhenHandoffTargetsAnotherObject()
        {
            string request =
                Invoke<string>(
                    "GetReviewEffectiveRequest",
                    "Configure Main Camera",
                    "Attach SideViewCameraFollow to Main Camera.",
                    "Inspect Main Camera and verify its target is PlayerBall.",
                    "The current selection is PlayerBall.");

            StringAssert.Contains(
                "Configure Main Camera",
                request);

            StringAssert.Contains(
                "current selection is PlayerBall",
                request);

            Assert.AreEqual(
                "Main Camera",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void ScriptDeliverableReview_RequiresFileAndCompilationEvidence()
        {
            const string request =
                "Assets/JumpingBallGame/Scripts/FallResetController.cs exists, records the start position, and compiles without errors.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewFileEvidence",
                    request));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewCompilationEvidence",
                    request));

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Create fall reset controller",
                    request,
                    request,
                    "Complete.",
                    "File created.",
                    null,
                    null,
                    "Assets/JumpingBallGame/Scripts/FallResetController.cs",
                    "authoritative source contents",
                    null,
                    request);

            StringAssert.Contains(
                "authoritative file contents",
                prompt);

            StringAssert.Contains(
                "original card and acceptance criteria remain authoritative",
                prompt);
        }


        [Test]
        public void CompilationEvidence_IsScopedToOwnedScriptArtifacts()
        {
            string raw =
                new JObject
                {
                    ["IsCompiling"] = false,
                    ["ErrorCount"] = 1,
                    ["WarningCount"] = 0,
                    ["Messages"] =
                        new JArray
                        {
                            new JObject
                            {
                                ["Type"] = "Error",
                                ["File"] = "Assets/Game/Scripts/OtherController.cs",
                                ["Line"] = 12,
                                ["Column"] = 4,
                                ["Message"] = "CS0000 sibling error"
                            }
                        }
                }.ToString();

            JObject scoped =
                JObject.Parse(
                    Invoke<string>(
                        "ScopeCompilationEvidence",
                        raw,
                        "Assets/Game/Scripts/PlayerController.cs exists and compiles."));

            Assert.AreEqual("TargetArtifacts", scoped["Scope"]?.Value<string>());
            Assert.AreEqual(0, scoped["ErrorCount"]?.Value<int>());
            Assert.AreEqual(1, scoped["UnrelatedErrorCount"]?.Value<int>());
            Assert.IsTrue(scoped["TargetArtifactsCompile"]?.Value<bool>() == true);
            Assert.IsFalse(
                Invoke<bool>(
                    "HasTargetArtifactCompilationErrors",
                    scoped.ToString()));
        }


        [Test]
        public void CompilationEvidence_ProjectGateRetainsGlobalErrors()
        {
            string raw =
                new JObject
                {
                    ["IsCompiling"] = false,
                    ["ErrorCount"] = 1,
                    ["WarningCount"] = 0,
                    ["Messages"] =
                        new JArray
                        {
                            new JObject
                            {
                                ["Type"] = "Error",
                                ["File"] = "Assets/Game/Scripts/OtherController.cs"
                            }
                        }
                }.ToString();

            JObject scoped =
                JObject.Parse(
                    Invoke<string>(
                        "ScopeCompilationEvidence",
                        raw,
                        "Verify the entire project compiles with zero errors across all scripts."));

            Assert.AreEqual("Project", scoped["Scope"]?.Value<string>());
            Assert.AreEqual(1, scoped["ErrorCount"]?.Value<int>());
        }


        [Test]
        public void DecompositionGraph_AcceptsFocusedGroundAndLightingChild()
        {
            JObject focused =
                GraphChild(
                    "foundation",
                    "Create scene with ground and lighting",
                    "Create Assets/JumpingBallGame/Scenes/JumpingBall.unity with a visible ground GameObject using a BoxCollider and add a Directional Light. Save the scene.");
            focused["AcceptanceCriteria"] =
                "The JumpingBall scene contains the visible ground with its BoxCollider and one Directional Light, and the scene is saved.";
            focused["ProducedArtifactPaths"] =
                new JArray("Assets/JumpingBallGame/Scenes/JumpingBall.unity");
            focused["ConsumedArtifactPaths"] = new JArray();
            focused["MutatedResourceKeys"] =
                new JArray("scene:Assets/JumpingBallGame/Scenes/JumpingBall.unity");

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    new JObject
                    {
                        ["DirectChildren"] = new JArray(focused)
                    },
                    "Create a feature through child cards."));
        }


        [Test]
        public void ScriptReworkReview_UsesOriginalFileContractAndNeverInfersMethodAsGameObject()
        {
            const string originalRequest =
                "Write FallReset.cs under Assets/ExampleProject/Scripts. " +
                "Store the starting position in Awake; clear Rigidbody velocity when below a threshold. " +
                "Assets/ExampleProject/Scripts/FallReset.cs exists and compiles.";

            const string reworkMessage =
                "[ReviewFailureKind: ImplementationDefect] The first review requested rework. Address every item below before returning the card for another review:\n" +
                "The component inspection evidence is unavailable. No read_file result was supplied.";

            string classificationRequest =
                Invoke<string>(
                    "GetClassificationRequest",
                    originalRequest,
                    reworkMessage,
                    reworkMessage);

            Assert.AreEqual(
                originalRequest,
                classificationRequest);

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresRunComponentInspection",
                    true,
                    classificationRequest,
                    reworkMessage,
                    false));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    classificationRequest));

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ResolveRequestedFilePaths",
                    classificationRequest,
                    null,
                    Directory.GetParent(Application.dataPath).FullName,
                    null);

            CollectionAssert.AreEqual(
                new[]
                {
                    "Assets/ExampleProject/Scripts/FallReset.cs"
                },
                paths);

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Write reset script",
                    originalRequest,
                    "The script exists and compiles.",
                    "Completed.",
                    reworkMessage,
                    null,
                    null,
                    paths[0],
                    "authoritative source contents",
                    null,
                    classificationRequest);

            StringAssert.Contains(
                "authoritative source contents",
                prompt);
            StringAssert.DoesNotContain(
                "component inspection evidence is unavailable",
                prompt);
        }


        [Test]
        public void FolderStructureTask_RequiresRootFirstScaffolding()
        {
            const string request =
                "Create JumpingBallGame scripts and folder structure with " +
                "Scripts, Scenes, and Materials subfolders.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresDirectoryScaffolding",
                    request));

            FieldInfo promptField =
                _runnerType.GetField(
                    "DirectoryScaffoldingSystemPrompt",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.IsNotNull(promptField);

            string prompt =
                (string)promptField.GetRawConstantValue();

            StringAssert.Contains(
                "parentPath=\"Assets\"",
                prompt);

            StringAssert.Contains(
                "createParents=true",
                prompt);

            StringAssert.Contains(
                "Never begin by using a not-yet-created root as parentPath",
                prompt);
        }


        [Test]
        public void FolderStructureReview_DoesNotDemandComponentInspection()
        {
            const string description =
                "Create the required folder structure under Assets/JumpingBallGame. " +
                "No scene or script content yet.";

            const string acceptance =
                "Folders Assets/JumpingBallGame, Assets/JumpingBallGame/Scripts, " +
                "Assets/JumpingBallGame/Materials, and Assets/JumpingBallGame/Scenes exist in the project.";

            string request =
                "Create project folder structure\n" +
                description + "\n" +
                acceptance;

            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneDirectoryScaffoldingTask",
                    request));

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Create project folder structure",
                    description,
                    acceptance,
                    "Created the required folders.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    request);

            StringAssert.DoesNotContain(
                "component inspection evidence is unavailable",
                prompt);
        }


        [Test]
        public void AddChildToNamedParent_IsNotTreatedAsScaffolding()
        {
            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresDirectoryScaffolding",
                    "Please add NewFolder to Runtime/Docs/FolderTest"));
        }


        [Test]
        public void FolderStructureTask_ExtractsExplicitAssetsRootForPreflight()
        {
            const string request =
                "Create Assets/JumpingBallGame with subfolders Scripts, Scenes, and Materials.";

            Assert.AreEqual(
                "JumpingBallGame",
                Invoke<string>(
                    "ExtractScaffoldingRoot",
                    request));

            Assert.AreEqual(
                "JumpingBallGame",
                Invoke<string>(
                    "ExtractScaffoldingRoot",
                    "Create Assets\\JumpingBallGame\\Scripts and the folder structure."));
        }


        [Test]
        public void FolderStructurePreflight_RequiresExplicitAssetsRoot()
        {
            Assert.IsNull(
                Invoke<string>(
                    "ExtractScaffoldingRoot",
                    "Create a new folder structure somewhere suitable."));
        }


        [Test]
        public void FolderStructureAcceptance_DoesNotTreatUnrelatedAsReadInstruction()
        {
            const string acceptanceCriteria =
                "All five folders exist and are visible in the Unity Project window with .meta files generated. " +
                "No unrelated project files changed. ParentTaskId of this card equals the executing parent task's authoritative TaskId.";

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresFileContentRead",
                    acceptanceCriteria));
        }


        [Test]
        public void ImplementationPrompt_RejectsSelfDecompositionAndRequestsPlanningKickback()
        {
            const string request =
                "Build a simple 2.5D jumping-ball game. " +
                "Every child task must reach Done with a successful review before this parent task can pass.";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Build Ball Game",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.Contains("IMPLEMENTATION PHASE BOUNDARY", prompt);
            StringAssert.Contains("create_kanban_child_task is unavailable", prompt);
            StringAssert.Contains("[PlanningKickback]", prompt);
            StringAssert.Contains(
                "missing method, property, object, component, configuration, or behavior",
                prompt);
            StringAssert.Contains(
                "never a planning defect",
                prompt);
            StringAssert.DoesNotContain("COORDINATION RUN", prompt);
            StringAssert.DoesNotContain("TOP-DOWN DECOMPOSITION", prompt);
            StringAssert.DoesNotContain("Do not implement project files in this parent", prompt);
        }


        [Test]
        public void CompilerFeedback_CompletedTransitiveDependenciesAreReadableButNotWritable()
        {
            using (var f = new PlanningCommitFixture())
            {
                object worker = f.Add("hazard");
                object platform = f.Add("platform");
                object manager = f.Add("manager");
                object unfinished = f.Add("unfinished");
                Type status = typeof(AIRuntimeKanbanSession).GetNestedType("TaskStatus", BindingFlags.NonPublic);
                foreach (object task in new[] { platform, manager })
                    SetTaskField(f.TaskType, task, "Status", Enum.Parse(status, "Done"));
                SetTaskField(f.TaskType, worker, "DependsOnTaskIds", new List<string> { "platform", "unfinished" });
                SetTaskField(f.TaskType, platform, "DependsOnTaskIds", new List<string> { "manager" });
                SetTaskField(f.TaskType, manager, "ProducedArtifactPaths", new List<string> { "Assets/Game/GameManager.cs", "Assets/Game/Scripts" });
                SetTaskField(f.TaskType, unfinished, "ProducedArtifactPaths", new List<string> { "Assets/Game/Secret.cs" });
                var paths = (IReadOnlyCollection<string>)f.Call("GetCompletedDependencyArtifactPaths", worker);
                CollectionAssert.AreEqual(new[] { "Assets/Game/GameManager.cs" }, paths);
                string context = JObject.FromObject(f.Call("GetAgentTaskContext", "hazard")).ToString();
                var exposed = (IReadOnlyCollection<string>)_runnerType.GetMethod("ExtractCompletedDependencyArtifactPaths",
                    BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { context });
                CollectionAssert.AreEqual(paths, exposed);
                object middleware = CreateCompilerFeedbackMiddleware(paths);
                Type middlewareType = middleware.GetType();
                MethodInfo validate = middlewareType.GetMethod("ValidateMutationTarget", BindingFlags.NonPublic | BindingFlags.Instance);
                JObject read = JObject.Parse("{\"path\":\"Assets/Game/GameManager.cs\"}");
                Assert.DoesNotThrow(() => validate.Invoke(middleware, new object[] { "read_file", read }));
                Assert.Throws<TargetInvocationException>(() => validate.Invoke(middleware, new object[] { "write_file", read }));
                Assert.Throws<TargetInvocationException>(() => validate.Invoke(middleware, new object[] { "read_file", JObject.Parse("{\"path\":\"Assets/Game/Secret.cs\"}") }));
            }
        }

        private object CreateCompilerFeedbackMiddleware(IEnumerable<string> readable = null)
        {
            Type type = _runnerType.GetNestedType("KanbanArtifactOwnershipMiddleware", BindingFlags.NonPublic);
            object middleware = Activator.CreateInstance(type, true);
            type.GetMethod("Activate").Invoke(middleware, new object[] {
                new[] { "Assets/Game/Hazard.cs" }, readable ?? Array.Empty<string>(), Array.Empty<string>(), true });
            return middleware;
        }

        private static void RecordCompilerFeedback(object middleware, string capability, string arguments, string result)
        {
            middleware.GetType().GetMethod("RecordSuccessfulCapability", BindingFlags.NonPublic | BindingFlags.Instance)
                .Invoke(middleware, new object[] { capability, JObject.Parse(arguments), result });
        }

        [Test]
        public void CompilerFeedback_UnchangedErrorsWithDifferentFiltersStopPolling()
        {
            object middleware = CreateCompilerFeedbackMiddleware();
            const string result = "{\"IsCompiling\":false,\"ErrorCount\":1,\"ErrorFingerprint\":\"same\",\"Messages\":[{\"Type\":\"Error\",\"File\":\"Assets/Game/Hazard.cs\",\"Line\":87,\"Message\":\"CS1061 missing API\"}]}";
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{\"contains\":\"GameManager\"}", result);
            TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
                RecordCompilerFeedback(middleware, "get_compilation_errors", "{\"maxMessages\":200}", result));
            Assert.AreEqual("CompilationNoProgressException", error.InnerException.GetType().Name);
            Assert.IsFalse((bool)typeof(AIRuntime.Runtime.Core.ConversationEngine).GetMethod(
                "IsRecoverableCapabilityFailure", BindingFlags.NonPublic | BindingFlags.Static)
                .Invoke(null, new object[] { error.InnerException }));
            StringAssert.Contains("[CompilationNoProgress]", error.InnerException.Message);
            StringAssert.Contains("CS1061", error.InnerException.Message);
        }

        [Test]
        public void CompilerFeedback_RealSourceChangeResetsPollingBudgetButIdenticalWriteDoesNot()
        {
            object middleware = CreateCompilerFeedbackMiddleware();
            const string result = "{\"IsCompiling\":false,\"ErrorCount\":1,\"ErrorFingerprint\":\"same\"}";
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            RecordCompilerFeedback(middleware, "write_file", "{\"path\":\"Assets/Game/Hazard.cs\"}", "{\"PreviousSha256\":\"old\",\"CurrentSha256\":\"new\"}");
            Assert.DoesNotThrow(() => RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result));
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            RecordCompilerFeedback(middleware, "write_file", "{\"path\":\"Assets/Game/Hazard.cs\"}", "{\"PreviousSha256\":\"new\",\"CurrentSha256\":\"new\"}");
            Assert.Throws<TargetInvocationException>(() => RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result));
        }

        [Test]
        public void CompilerFeedback_NewFilteredDiagnosticsAreProgress_UnrelatedWritesAreNot()
        {
            object middleware = CreateCompilerFeedbackMiddleware();
            string Diagnostic(string file) => new JObject {
                ["IsCompiling"] = false, ["ErrorCount"] = 3, ["ErrorFingerprint"] = "all-three",
                ["Messages"] = new JArray(new JObject { ["Type"] = "Error", ["File"] = file, ["Line"] = 1, ["Message"] = "CS1061" })
            }.ToString();
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", Diagnostic("A.cs"));
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", Diagnostic("B.cs"));
            Assert.DoesNotThrow(() => RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", Diagnostic("C.cs")));
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", Diagnostic("A.cs"));
            RecordCompilerFeedback(middleware, "write_file", "{\"path\":\"Assets/Game/Notes.txt\"}", "{}");
            Assert.Throws<TargetInvocationException>(() => RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", Diagnostic("B.cs")));
        }

        [Test]
        public void CompilerFeedback_MissingCapturedErrorsDoesNotBecomeAFalseCleanCompile()
        {
            object middleware = CreateCompilerFeedbackMiddleware();
            const string result = "{\"IsCompiling\":false,\"CompilationFailed\":true,\"DiagnosticsUnavailable\":true,\"ErrorCount\":0,\"ErrorFingerprint\":\"empty\"}";
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            Assert.Throws<TargetInvocationException>(() => RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result));
        }

        [TestCase("{\"IsCompiling\":true,\"ErrorCount\":1,\"ErrorFingerprint\":\"same\"}")]
        [TestCase("{\"IsCompiling\":false,\"ErrorCount\":0,\"ErrorFingerprint\":\"clean\"}")]
        [TestCase("{\"IsCompiling\":false,\"ErrorCount\":1,\"ErrorFingerprint\":\"changed\"}")]
        public void CompilerFeedback_NewCompilationOrChangedEvidenceDoesNotStop(string next)
        {
            object middleware = CreateCompilerFeedbackMiddleware();
            const string result = "{\"IsCompiling\":false,\"ErrorCount\":1,\"ErrorFingerprint\":\"same\"}";
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", result);
            Assert.DoesNotThrow(() => RecordCompilerFeedback(middleware, "get_compilation_errors", "{}", next));
        }

        [Test]
        public void ArtifactOwnership_RejectsSiblingFileMutation()
        {
            Type middlewareType =
                _runnerType.GetNestedType(
                    "KanbanArtifactOwnershipMiddleware",
                    BindingFlags.NonPublic);

            Assert.NotNull(middlewareType);

            object middleware =
                Activator.CreateInstance(
                    middlewareType,
                    nonPublic: true);

            MethodInfo activate =
                middlewareType.GetMethod(
                    "Activate",
                    BindingFlags.Public |
                    BindingFlags.Instance);

            MethodInfo validate =
                middlewareType.GetMethod(
                    "ValidateMutationTarget",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            MethodInfo recordMutation =
                middlewareType.GetMethod(
                    "RecordSuccessfulMutation",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            MethodInfo recordCapability =
                middlewareType.GetMethod(
                    "RecordSuccessfulCapability",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            Assert.NotNull(activate);
            Assert.NotNull(validate);
            Assert.NotNull(recordMutation);
            Assert.NotNull(recordCapability);

            activate.Invoke(
                middleware,
                new object[]
                {
                    new[]
                    {
                        "Assets/BallJumpGame/Scripts/GoalDetector.cs"
                    },
                    new[]
                    {
                        "Assets/BallJumpGame/Scripts/GoalDetector.cs",
                        "Assets/BallJumpGame/ARCHITECTURE.md"
                    },
                    Array.Empty<string>(),
                    true
                });

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "read_file",
                        JObject.Parse(
                            "{\"path\":\"Assets/BallJumpGame/ARCHITECTURE.md\"}")
                    }));

            TargetInvocationException siblingRead =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "read_file",
                            JObject.Parse(
                                "{\"path\":\"Assets/BallJumpGame/Scripts/UIManager.cs\"}")
                        }));

            StringAssert.Contains(
                "[ArtifactReadScope]",
                siblingRead.InnerException?.Message);

            TargetInvocationException broadSearch =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "search_workspace",
                            JObject.Parse(
                                "{\"path\":\"Assets/BallJumpGame/Scripts\",\"pattern\":\"UIManager\"}")
                        }));

            StringAssert.Contains(
                "Broad workspace content search is unavailable",
                broadSearch.InnerException?.Message);

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "write_file",
                        JObject.Parse(
                            "{\"path\":\"Assets/BallJumpGame/Scripts/GoalDetector.cs\"}")
                    }));

            TargetInvocationException rejected =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "write_file",
                            JObject.Parse(
                                "{\"path\":\"Assets/BallJumpGame/Scripts/UIManager.cs\"}")
                        }));

            StringAssert.Contains(
                "[ArtifactOwnership]",
                rejected.InnerException?.Message);
            StringAssert.Contains(
                "GoalDetector.cs",
                rejected.InnerException?.Message);
            StringAssert.Contains(
                "sibling card's output",
                rejected.InnerException?.Message);

            TargetInvocationException unchangedRefresh =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "refresh_unity_assets",
                            JObject.Parse(
                                "{\"assetPaths\":[\"Assets/BallJumpGame/Scripts/GoalDetector.cs\"]}")
                        }));

            StringAssert.Contains(
                "rejected unchanged artifacts",
                unchangedRefresh.InnerException?.Message);

            recordMutation.Invoke(
                middleware,
                new object[]
                {
                    "write_file",
                    JObject.Parse(
                        "{\"path\":\"Assets/BallJumpGame/Scripts/GoalDetector.cs\"}")
                });

            TargetInvocationException importRequired =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "get_compilation_errors",
                            new JObject()
                        }));

            StringAssert.Contains(
                "[ScriptImportRequired]",
                importRequired.InnerException?.Message);

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "refresh_unity_assets",
                        JObject.Parse(
                            "{\"assetPaths\":[\"Assets/BallJumpGame/Scripts/GoalDetector.cs\"]}")
                    }));

            recordCapability.Invoke(
                middleware,
                new object[]
                {
                    "refresh_unity_assets",
                    JObject.Parse(
                        "{\"assetPaths\":[\"Assets/BallJumpGame/Scripts/GoalDetector.cs\"]}"),
                    "{\"OperationId\":\"op1\",\"State\":\"Compiling\"}"
                });

            Assert.Throws<TargetInvocationException>(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "get_compilation_errors",
                        new JObject()
                    }));

            recordCapability.Invoke(
                middleware,
                new object[]
                {
                    "get_unity_operation",
                    JObject.Parse("{\"operationId\":\"op1\"}"),
                    "{\"OperationId\":\"op1\",\"State\":\"Succeeded\"}"
                });

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "get_compilation_errors",
                        new JObject()
                    }));
        }


        [Test]
        public void ArtifactOwnership_ZeroOutputChildIsReadOnly()
        {
            Type middlewareType =
                _runnerType.GetNestedType(
                    "KanbanArtifactOwnershipMiddleware",
                    BindingFlags.NonPublic);

            Assert.NotNull(middlewareType);

            object middleware =
                Activator.CreateInstance(
                    middlewareType,
                    nonPublic: true);

            MethodInfo activate =
                middlewareType.GetMethod(
                    "Activate",
                    BindingFlags.Public |
                    BindingFlags.Instance);

            MethodInfo validate =
                middlewareType.GetMethod(
                    "ValidateMutationTarget",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            Assert.NotNull(activate);
            Assert.NotNull(validate);

            activate.Invoke(
                middleware,
                new object[]
                {
                    Array.Empty<string>(),
                    new[]
                    {
                        "Assets/BallJumpGame/Scenes/GameScene.unity"
                    },
                    Array.Empty<string>(),
                    true
                });

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "read_file",
                        JObject.Parse(
                            "{\"path\":\"Assets/BallJumpGame/Scenes/GameScene.unity\"}")
                    }));

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "run_play_mode_tests",
                        new JObject()
                    }));

            TargetInvocationException sceneMutation =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "create_game_object",
                            JObject.Parse(
                                "{\"name\":\"Unexpected\",\"scenePath\":\"Assets/BallJumpGame/Scenes/GameScene.unity\"}")
                        }));

            StringAssert.Contains(
                "read-only verification worker",
                sceneMutation.InnerException?.Message);

            TargetInvocationException fileMutation =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "write_file",
                            JObject.Parse(
                                "{\"path\":\"Assets/BallJumpGame/Scenes/GameScene.unity\"}")
                        }));

            StringAssert.Contains(
                "[ArtifactOwnership]",
                fileMutation.InnerException?.Message);
        }


        [Test]
        public void ArtifactOwnership_SceneResourceKeyAllowsOnlyExactUnityScene()
        {
            Type middlewareType =
                _runnerType.GetNestedType(
                    "KanbanArtifactOwnershipMiddleware",
                    BindingFlags.NonPublic);

            Assert.NotNull(middlewareType);

            object middleware = Activator.CreateInstance(
                middlewareType,
                nonPublic: true);

            MethodInfo activate = middlewareType.GetMethod(
                "Activate",
                BindingFlags.Public | BindingFlags.Instance);

            MethodInfo validate = middlewareType.GetMethod(
                "ValidateMutationTarget",
                BindingFlags.NonPublic | BindingFlags.Instance);

            Assert.NotNull(activate);
            Assert.NotNull(validate);

            activate.Invoke(
                middleware,
                new object[]
                {
                    Array.Empty<string>(),
                    new[] { "Assets/Game/Scenes/Game.unity" },
                    new[] { "scene:Assets/Game/Scenes/Game.unity" },
                    true
                });

            Assert.DoesNotThrow(
                () => validate.Invoke(
                    middleware,
                    new object[]
                    {
                        "create_editor_scene",
                        JObject.Parse(
                            "{\"scenePath\":\"Assets/Game/Scenes/Game.unity\"}")
                    }));

            TargetInvocationException otherScene =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "create_editor_scene",
                            JObject.Parse(
                                "{\"scenePath\":\"Assets/Game/Scenes/Other.unity\"}")
                        }));

            StringAssert.Contains(
                "ConsumedArtifactPaths are readable inputs",
                otherScene.InnerException?.Message);

            TargetInvocationException rawYamlWrite =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "write_file",
                            JObject.Parse(
                                "{\"path\":\"Assets/Game/Scenes/Game.unity\"}")
                        }));

            StringAssert.Contains(
                "[ArtifactOwnership]",
                rawYamlWrite.InnerException?.Message);

            TargetInvocationException unscopedObjectMutation =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "update_game_object",
                            new JObject()
                        }));

            StringAssert.Contains(
                "requires an exact GameObject",
                unscopedObjectMutation.InnerException?.Message);

            TargetInvocationException projectMutation =
                Assert.Throws<TargetInvocationException>(
                    () => validate.Invoke(
                        middleware,
                        new object[]
                        {
                            "configure_project_settings",
                            new JObject()
                        }));

            StringAssert.Contains(
                "require their own committed resource contract",
                projectMutation.InnerException?.Message);
        }


        [Test]
        public void StructuredChildDetection_RequiresAuthoritativeChildFlag()
        {
            Assert.IsTrue(
                Invoke<bool>(
                    "IsStructuredChildTask",
                    "{\"IsChildTask\":true}"));

            Assert.IsFalse(
                Invoke<bool>(
                    "IsStructuredChildTask",
                    "{\"IsChildTask\":false}"));

            Assert.IsFalse(
                Invoke<bool>(
                    "IsStructuredChildTask",
                    "not-json"));
        }


        [Test]
        public void ResumeFailedAttempt_PreservesExecutionMemoryAndArtifacts()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType(
                "TaskStatus",
                BindingFlags.NonPublic);
            MethodInfo resume = windowType.GetMethod(
                "ResumeFailedAttempt",
                BindingFlags.NonPublic |
                BindingFlags.Instance);

            Assert.NotNull(taskType);
            Assert.NotNull(statusType);
            Assert.NotNull(resume);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            object task = Activator.CreateInstance(
                taskType,
                nonPublic: true);

            try
            {
                const string memory =
                    "{\"Entries\":[{\"Event\":\"ToolCompleted\"}]}";
                var artifacts = new List<string>
                {
                    "Assets/BallJumpGame/Prefabs/PlayerBall.prefab"
                };

                taskType.GetField("Id").SetValue(task, "task-1");
                taskType.GetField("Title").SetValue(task, "Player Prefab");
                taskType.GetField("Status").SetValue(
                    task,
                    Enum.Parse(statusType, "Crashed"));
                taskType.GetField("ExecutionMemory").SetValue(task, memory);
                taskType.GetField("AgentError").SetValue(
                    task,
                    "Malformed tool call was rejected.");
                taskType.GetField("AgentResult").SetValue(task, string.Empty);
                taskType.GetField("ReviewResult").SetValue(task, string.Empty);
                taskType.GetField("ReviewStartedAt").SetValue(task, string.Empty);
                taskType.GetField("ProducedArtifactPaths").SetValue(
                    task,
                    artifacts);

                bool resumed = (bool)resume.Invoke(
                    window,
                    new[] { task });

                Assert.IsTrue(resumed);
                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual(
                    memory,
                    taskType.GetField("ExecutionMemory").GetValue(task));
                Assert.AreSame(
                    artifacts,
                    taskType.GetField("ProducedArtifactPaths").GetValue(task));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("AgentError").GetValue(task));
                StringAssert.Contains(
                    "[DurableAttemptResume]",
                    (string)taskType.GetField("NextAgentMessage").GetValue(task));
                StringAssert.Contains(
                    "changed strategy",
                    (string)taskType.GetField("NextAgentMessage").GetValue(task));
            }
            finally
            {
                ScriptableObject.DestroyImmediate(window);
            }
        }


        [Test]
        public void InterruptedRefreshMemory_IsRecognizedAsReloadContinuation()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "ExecutionMemoryShowsInterruptedAssetRefresh",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            Assert.IsTrue(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "{\"Entries\":[{\"Event\":\"ToolCompleted\",\"Tool\":\"refresh_unity_assets\"},{\"Event\":\"RunCancelled\"}]}"
                    }));

            Assert.IsFalse(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "{\"Entries\":[{\"Event\":\"RunCancelled\"}]}"
                    }));
        }


        [Test]
        public void PlanningPrompt_LeavesGranularityJudgmentToThePlanner()
        {
            string prompt =
                Invoke<string>(
                    "BuildCoordinationTaskPrompt",
                    "Build ball game",
                    "Create a cohesive runtime subsystem with several related scripts, then assemble and verify the scene.",
                    "The complete game compiles and plays.",
                    string.Empty);

            StringAssert.Contains("Keep a cohesive outcome atomic", prompt);
            StringAssert.Contains("split only independent", prompt);
            StringAssert.Contains("authoritative graph snapshot", prompt);
            StringAssert.Contains("commit_kanban_plan", prompt);
            StringAssert.DoesNotContain("three or more", prompt);
            StringAssert.DoesNotContain("one card per file", prompt);
        }


        [Test]
        public void AssociationLeafHandoff_KeepsOriginalCardAsActiveRequest()
        {
            const string title = "Author controller";
            const string description =
                "Create Assets/Game/Scripts/Controller.cs with the required runtime API.";
            const string acceptance =
                "Controller.cs exists and Unity compiles with zero errors.";
            const string handoff =
                "[AssociationLeaf] Resume the full original card and execute it directly.";

            string original =
                string.Join("\n", title, description, acceptance);

            string effective =
                Invoke<string>(
                    "GetEffectiveRequest",
                    title,
                    description,
                    acceptance,
                    handoff);

            Assert.AreEqual(original, effective);
            Assert.AreEqual(
                original,
                Invoke<string>(
                    "GetClassificationRequest",
                    original,
                    effective,
                    handoff));

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    title,
                    description,
                    acceptance,
                    string.Empty,
                    handoff,
                    original,
                    null);

            StringAssert.Contains("Title: Author controller", prompt);
            StringAssert.Contains(description, prompt);
            StringAssert.Contains(acceptance, prompt);
            StringAssert.Contains("full original card remains the active request", prompt);
            StringAssert.DoesNotContain("context only; it is not the current request", prompt);
        }


        [Test]
        public void CoordinationPrompt_RestrictsParentToDurableGraphOrchestration()
        {
            string prompt =
                Invoke<string>(
                    "BuildCoordinationTaskPrompt",
                    "Build ball game",
                    "Create runtime scripts, a scene, UI, and tests.",
                    "Every parent acceptance criterion is verified.",
                    string.Empty);

            StringAssert.Contains("PLANNING / ORCHESTRATION-ONLY KANBAN RUN", prompt);
            StringAssert.Contains("authoritative graph snapshot", prompt);
            StringAssert.Contains("Proposal cards are durable within the current PlanningAttemptId", prompt);
            StringAssert.Contains("call commit_kanban_plan with CommitSequence=1", prompt);
            StringAssert.Contains("increment CommitSequence", prompt);
            StringAssert.Contains("prose after it is optional", prompt);
            StringAssert.Contains("must not inspect, create, edit, compile, test, or verify Unity project assets", prompt);
            StringAssert.Contains("Keep a cohesive outcome atomic", prompt);
            StringAssert.Contains("direct child of the project root", prompt);
            StringAssert.Contains("ActiveAgentIds", prompt);
            StringAssert.Contains("final integration-verification child", prompt);
            StringAssert.DoesNotContain("OPTIONAL DELEGATION", prompt);
        }


        [Test]
        public void ReviewPrompt_RequiresRelationalEvidenceAcrossValidChildren()
        {
            const string request =
                "Build a warehouse route whose stations are reachable by the configured vehicle.";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Verify warehouse route",
                    request,
                    "All stations are reachable.",
                    "Completed.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    request);

            StringAssert.Contains(
                "RELATIONAL ACCEPTANCE RULE",
                prompt);

            StringAssert.Contains(
                "A contradiction between individually valid child outputs is a FAIL",
                prompt);
        }


        [Test]
        public void ScriptAuthoring_DoesNotRequireImplementationReadback()
        {
            const string request =
                "Write BallController.cs, then read its contents and report completion.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresFileContentRead",
                    request));

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresImplementationFileContentRead",
                    request));

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Write BallController script",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.DoesNotContain(
                "MANDATORY FILE CONTENT READ",
                prompt);
        }


        [Test]
        public void SceneAuthoring_ContentsHeadingDoesNotRequireReadingNewScene()
        {
            const string request =
                "Create and save Assets/JumpingBallGame/Scenes/JumpingBallGame.unity.\n" +
                "Contents of this card: add a Directional Light and Main Camera.\n" +
                "No files outside Assets/JumpingBallGame may change.";

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresImplementationFileContentRead",
                    request));

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Create and save JumpingBallGame scene",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.DoesNotContain(
                "MANDATORY FILE CONTENT READ",
                prompt);
        }


        [Test]
        public void DelegationPrompt_RequiresDeterministicArtifactContracts()
        {
            const string request =
                "Create a side-scrolling game under Assets/JumpingBallGame. " +
                "The player moves on X/Y and remains on Z=0. Create child cards for every deliverable.";

            string prompt =
                Invoke<string>(
                    "BuildCoordinationTaskPrompt",
                    "Build game",
                    request,
                    string.Empty,
                    string.Empty);

            StringAssert.Contains("PLANNING PHASE BOUNDARY", prompt);
            StringAssert.Contains("ProducesArtifactPaths", prompt);
            StringAssert.Contains("ConsumesArtifactPaths", prompt);
            StringAssert.Contains("MutatesResourceKeys", prompt);
            StringAssert.Contains("DependsOnSiblingTitles", prompt);
            StringAssert.Contains(
                "execution-ready leaves rather than umbrella cards",
                prompt);
            StringAssert.Contains("complete big-picture implementation graph", prompt);
            StringAssert.DoesNotContain("SHARED MUTABLE ARTIFACTS", prompt);
            StringAssert.DoesNotContain("TRANSACTIONAL SCENE BOUNDARY", prompt);
        }


        [Test]
        public void ScriptOnlyReview_DoesNotDemandLiveSceneComponentEvidence()
        {
            const string request =
                "Create Assets/JumpingBallGame/Scripts/CameraFollow.cs. " +
                "This MonoBehaviour follows X/Y with a constant negative-Z offset. " +
                "CameraFollow.cs exists and compiles with zero errors.";

            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneScriptAssetReview",
                    true,
                    request));

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Write CameraFollow script",
                    request,
                    "CameraFollow.cs exists and compiles.",
                    "Completed.",
                    null,
                    null,
                    null,
                    "Assets/JumpingBallGame/Scripts/CameraFollow.cs",
                    "public sealed class CameraFollow : MonoBehaviour {}",
                    null,
                    request);

            StringAssert.DoesNotContain(
                "component inspection evidence is unavailable",
                prompt);
        }


        [Test]
        public void ScriptSourceVerification_DoesNotRequireLiveComponentInspection()
        {
            const string request =
                "Verify PlayerBall integration criteria\n" +
                "Consume Assets/BallJumpGame/Scripts/PlayerBall.cs and explicitly verify every parent acceptance criterion.\n" +
                "Use the Unity Editor / compilation tooling to confirm the script compiles with zero errors. " +
                "Inspect the script source to confirm serialized fields moveSpeed, jumpForce, groundCheckDistance, groundLayer; " +
                "horizontal input handling; grounded-only jump; Rigidbody constraint enforcement in Awake; Player tag set at runtime; " +
                "no Z-axis movement; and no external package references. Do NOT modify the script; verification only.";

            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneScriptAssetTask",
                    request));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresRunComponentInspection",
                    false,
                    request,
                    request,
                    false));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresImplementationFileContentRead",
                    request));
        }


        [Test]
        public void ScriptAuthoringWithSelectedComponentInspection_RemainsLiveInspectionTask()
        {
            const string request =
                "Create Assets/Game/Scripts/PlayerBall.cs, then inspect the selected Player Rigidbody component and confirm Freeze Position Z.";

            Assert.IsFalse(
                Invoke<bool>(
                    "IsStandaloneScriptAssetTask",
                    request));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresRunComponentInspection",
                    false,
                    request,
                    request,
                    false));
        }


        [Test]
        public void ScriptOnlyReview_DoesNotArtificiallyRemoveEnabledCapabilities()
        {
            const string request =
                "Create a new C# script at Assets/JumpingBallGame/Scripts/FallReset.cs. " +
                "It stores the starting position and clears Rigidbody velocity below fallThreshold. " +
                "The script only defines the MonoBehaviour; it must NOT attach components or mutate any scene. " +
                "No scene/GameObject mutation, no component attachment in this card.";

            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneScriptAssetReview",
                    true,
                    request));

            var capabilities =
                new List<string>
                {
                    "write_file",
                    "get_compilation_errors",
                    "create_directory",
                    "create_editor_scene",
                    "create_game_object",
                    "save_editor_scene",
                    "add_editor_component",
                    "set_serialized_property"
                };

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "write_file",
                    "get_compilation_errors",
                    "create_directory",
                    "create_editor_scene",
                    "create_game_object",
                    "save_editor_scene",
                    "add_editor_component",
                    "set_serialized_property"
                },
                capabilities);
        }


        [Test]
        public void KanbanContextPrompt_MakesAncestorIntegrationContractBinding()
        {
            const string evidence =
                "{\"AncestorContracts\":[{\"Title\":\"Make Game\",\"Description\":\"Move on X/Y and keep Z=0.\",\"AcceptanceCriteria\":\"All assets stay under Assets/JumpingBallGame.\"}],\"SiblingContracts\":[{\"Title\":\"Victory system\",\"Status\":\"To Do\",\"ProducedArtifactPaths\":[\"Assets/JumpingBallGame/Scripts/GoalDetector.cs\"]}]}";

            string implementationPrompt =
                Invoke<string>(
                    "AppendKanbanTaskContextEvidence",
                    "Implement the platform.",
                    evidence,
                    false);

            StringAssert.Contains(
                "INHERITED INTEGRATION CONTRACT",
                implementationPrompt);
            StringAssert.Contains(
                "coordinate axes or planes",
                implementationPrompt);
            StringAssert.Contains(
                "Never substitute the currently active scene",
                implementationPrompt);

            string reviewPrompt =
                Invoke<string>(
                    "AppendKanbanTaskContextEvidence",
                    "Review the platform.",
                    evidence,
                    true);

            StringAssert.Contains(
                "conflicting axis, scene path",
                reviewPrompt);
            StringAssert.Contains(
                "must receive FAIL",
                reviewPrompt);
            StringAssert.Contains(
                "Parent acceptance criteria are not duplicated onto every child",
                reviewPrompt);
            StringAssert.Contains(
                "SiblingContracts",
                reviewPrompt);
            StringAssert.Contains(
                "UNFINISHED SIBLING OWNERSHIP",
                reviewPrompt);
            StringAssert.Contains(
                "Victory system owns Assets/JumpingBallGame/Scripts/GoalDetector.cs",
                reviewPrompt);
        }


        [Test]
        public void JumpingBallBuild_PrefersOutcomeDecompositionWithoutNumericMinimum()
        {
            const string request =
                "Create a jumping ball game with a player, camera, platforms, goal, scripts, scene, compilation, and verification. " +
                "Every child task must complete successfully.";

            string prompt =
                Invoke<string>(
                    "BuildCoordinationTaskPrompt",
                    "Build jumping ball game",
                    request,
                    string.Empty,
                    string.Empty);

            StringAssert.Contains("PLANNING PHASE BOUNDARY", prompt);
            StringAssert.Contains("smallest useful dependency graph", prompt);
            StringAssert.DoesNotContain("minimum child", prompt.ToLowerInvariant());
            StringAssert.DoesNotContain("MANDATORY GRANULAR DECOMPOSITION", prompt);
        }


        [Test]
        public void KanbanRun_DisablesFixedTotalTimeout()
        {
            Assert.AreEqual(
                System.Threading.Timeout.InfiniteTimeSpan,
                Invoke<TimeSpan>(
                    "ResolveKanbanRunTimeout"));
        }


        [Test]
        public void ExplicitAssetDeliverables_MustExistBeforeSuccess()
        {
            string workspaceRoot =
                Directory.GetParent(Application.dataPath).FullName;

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "VerifyExplicitAssetDeliverables",
                    "Implement the requested update in " + TestAssetPaths.Resolve("Assets/Editor/AIRuntimeKanbanAgentRunner.cs") + ".",
                    workspaceRoot));

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "VerifyExplicitAssetDeliverables",
                        "Create Assets/__AIRuntimeDefinitelyMissing__/Missing.cs",
                        workspaceRoot));

            StringAssert.Contains(
                "do not exist",
                exception.Message);
        }


        [Test]
        public void ExplicitAssetDeliverables_NegatedGenericAssetsPhraseIsNotAPath()
        {
            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ExtractExplicitAssetDeliverablePaths",
                    "Create primitive materials and prefabs using only built-in Unity features (no external assets/packages).");

            CollectionAssert.IsEmpty(paths);
        }


        [Test]
        public void StructuredProducedArtifacts_AreTheOnlyCompletionManifest()
        {
            string workspaceRoot =
                Directory.GetParent(Application.dataPath).FullName;

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "VerifyAssetDeliverables",
                    "Create the requested output with no external assets/packages. " +
                    "Do not edit Assets/__AIRuntimeDefinitelyMissing__/Unowned.unity.",
                    workspaceRoot,
                    new[]
                    {
                        TestAssetPaths.Resolve("Assets/Editor/AIRuntimeKanbanAgentRunner.cs")
                    }));
        }


        [Test]
        public void StructuredProducedArtifacts_StillRejectMissingDeclaredOutput()
        {
            string workspaceRoot =
                Directory.GetParent(Application.dataPath).FullName;

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "VerifyAssetDeliverables",
                        "No path-like prose is required.",
                        workspaceRoot,
                        new[]
                        {
                            "Assets/__AIRuntimeDefinitelyMissing__/Owned.prefab"
                        }));

            StringAssert.Contains(
                "Owned.prefab",
                exception.Message);
        }


        [Test]
        public void StructuredMutationKeys_AreExtractedFromTheCommittedContract()
        {
            IReadOnlyCollection<string> keys =
                Invoke<IReadOnlyCollection<string>>(
                    "ExtractStructuredMutatedResourceKeys",
                    "{\"MutatedResourceKeys\":[\"scene:Assets/Game/Scenes/Game.unity\",\"scene:Assets\\\\Game\\\\Scenes\\\\Game.unity\",\"asset:Assets/Game/Shared.asset\"]}");

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "scene:Assets/Game/Scenes/Game.unity",
                    "asset:Assets/Game/Shared.asset"
                },
                keys);
        }


        [Test]
        public void NegatedSceneContract_DropsContradictorySceneMutationKey()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "NormalizeResourceKeysForContract",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            var keys =
                (IReadOnlyList<string>)method.Invoke(
                    null,
                    new object[]
                    {
                        "Create prefabs. Do NOT edit the scene file; scene assembly is a separate card.",
                        new[]
                        {
                            "scene:Assets/Game/Scenes/Game.unity",
                            "asset:Assets/Game/Prefabs/Player.prefab"
                        }
                    });

            CollectionAssert.AreEqual(
                new[]
                {
                    "asset:Assets/Game/Prefabs/Player.prefab"
                },
                keys);
        }


        [Test]
        public void MultiFileReview_ResolvesEveryExplicitAssetPath()
        {
            const string request =
                "Read Assets/JumpingBallGame/Scripts/BallController.cs, " +
                "Assets/JumpingBallGame/Scripts/BallRespawner.cs, " +
                "Assets/JumpingBallGame/Scripts/GoalTrigger.cs, " +
                "Assets/JumpingBallGame/Scripts/SideScrollCameraFollow.cs, and " +
                "Assets/JumpingBallGame/Scripts/JumpingBallGame.asmdef. " +
                "Ensure Assets/JumpingBallGame/Materials exists.";

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ResolveRequestedFilePaths",
                    request,
                    null,
                    Directory.GetParent(
                            Application.dataPath)
                        .FullName,
                    null);

            Assert.AreEqual(
                5,
                paths.Count);

            CollectionAssert.Contains(
                paths,
                "Assets/JumpingBallGame/Scripts/JumpingBallGame.asmdef");

            Assert.AreEqual(
                "Assets/JumpingBallGame/Materials",
                Invoke<string>(
                    "ExtractRequiredExistingDirectory",
                    request));
        }


        [Test]
        public void StructuredChildReviewScope_ExcludesNegatedProsePath()
        {
            const string allowedPath =
                "Assets/BallJumpGame/Scripts/PlayerHitReporter.cs";

            const string prohibitedPath =
                "Assets/BallJumpGame/Scripts/PlayerCollisionRelay.cs";

            string request =
                $"Review {allowedPath}. {prohibitedPath} is NOT allowed and must not be created.";

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ResolveRequestedFilePaths",
                    request,
                    null,
                    Directory.GetParent(
                            Application.dataPath)
                        .FullName,
                    new[]
                    {
                        allowedPath
                    });

            CollectionAssert.AreEqual(
                new[]
                {
                    allowedPath
                },
                paths);
        }


        [Test]
        public void StructuredReviewScope_UsesChildProducedAndConsumedArtifacts()
        {
            string context =
                new JObject
                {
                    ["IsChildTask"] = true,
                    ["ProducedArtifactPaths"] =
                        new JArray(
                            "Assets/Game/Scripts/Player.cs"),
                    ["ConsumedArtifactPaths"] =
                        new JArray(
                            "Assets/Game/Scripts/Input.cs")
                }.ToString();

            IReadOnlyCollection<string> paths =
                Invoke<IReadOnlyCollection<string>>(
                    "ExtractStructuredReviewArtifactPaths",
                    context);

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "Assets/Game/Scripts/Player.cs",
                    "Assets/Game/Scripts/Input.cs"
                },
                paths);
        }


        [Test]
        public void ProducedPrefabEvidence_IsAppendedAsAuthoritativeReviewEvidence()
        {
            string existing =
                "AIRuntime authoritative inspect_editor_component result for 'PlayerBall'.";

            string prefabs =
                new JObject
                {
                    ["RequestedPrefabCount"] = 3,
                    ["Prefabs"] =
                        new JArray(
                            new JObject
                            {
                                ["AssetPath"] = "Assets/Game/Platform.prefab"
                            },
                            new JObject
                            {
                                ["AssetPath"] = "Assets/Game/Goal.prefab"
                            },
                            new JObject
                            {
                                ["AssetPath"] = "Assets/Game/Hazard.prefab"
                            })
                }.ToString();

            string evidence =
                Invoke<string>(
                    "AppendProducedPrefabReviewEvidence",
                    existing,
                    prefabs,
                    true);

            StringAssert.Contains(existing, evidence);
            StringAssert.Contains(
                "authoritative prefab evidence for every declared produced prefab asset",
                evidence);
            StringAssert.Contains(
                "Do not call inspect_prefab",
                evidence);
            StringAssert.Contains("Assets/Game/Platform.prefab", evidence);
            StringAssert.Contains("Assets/Game/Goal.prefab", evidence);
            StringAssert.Contains("Assets/Game/Hazard.prefab", evidence);
        }


        [Test]
        public void MissingReviewArtifact_IsEvidenceInsteadOfRunnerFailure()
        {
            JObject evidence =
                Invoke<JObject>(
                    "BuildMissingReviewArtifactEvidence",
                    "Assets/Game/Scripts/Missing.cs",
                    "File not found");

            Assert.IsFalse(
                evidence["Succeeded"].Value<bool>());

            Assert.AreEqual(
                "MissingArtifact",
                evidence["FailureKind"].Value<string>());

            StringAssert.Contains(
                "ImplementationDefect",
                evidence["ReviewInstruction"].Value<string>());
        }

        [TestCase(true, "KANBAN_REVIEW: PASS")]
        [TestCase(false, "KANBAN_REVIEW: FAIL")]
        public void PrefabReview_ExplicitConstraintClaimsMustMatchCapturedValues(bool actual, string verdict)
        {
            var evidence = new JObject { ["Prefabs"] = new JArray(new JObject {
                ["ComponentInspection"] = new JObject { ["Components"] = new JArray(new JObject {
                    ["Rigidbody"] = new JObject { ["FreezePositionZ"] = actual }
                }) }
            }) };
            string response = Invoke<string>("RejectContradictoryPrefabConstraintClaims",
                "KANBAN_REVIEW: PASS\nFreezePositionZ=true", evidence.ToString());
            StringAssert.StartsWith(verdict, response);
        }


        [Test]
        public void DirectoryReview_ExtractsEveryRequiredFolderFromPluralExistenceClause()
        {
            const string request =
                "Folders Assets/JumpingBallGame, Assets/JumpingBallGame/Scripts, " +
                "Assets/JumpingBallGame/Materials, and Assets/JumpingBallGame/Scenes exist in the project. " +
                "Assets/JumpingBallGame/Prefabs may be created if needed.";

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ExtractRequiredExistingDirectories",
                    request);

            CollectionAssert.AreEqual(
                new[]
                {
                    "Assets/JumpingBallGame",
                    "Assets/JumpingBallGame/Scripts",
                    "Assets/JumpingBallGame/Materials",
                    "Assets/JumpingBallGame/Scenes"
                },
                paths);
        }


        [Test]
        public void ScriptReview_IncludesNearestOwningAssemblyDefinition()
        {
            string workspaceRoot =
                Path.Combine(
                    Path.GetTempPath(),
                    "AIRuntimeTests",
                    Guid.NewGuid().ToString("N"));

            string scriptsDirectory =
                Path.Combine(
                    workspaceRoot,
                    "Assets",
                    "JumpingBallGame",
                    "Scripts");

            Directory.CreateDirectory(scriptsDirectory);
            File.WriteAllText(
                Path.Combine(scriptsDirectory, "JumpingBallGame.Runtime.asmdef"),
                "{\"name\":\"JumpingBallGame.Runtime\"}");

            try
            {
                IReadOnlyList<string> paths =
                    Invoke<IReadOnlyList<string>>(
                        "ResolveRequestedFilePaths",
                        "Review Assets/JumpingBallGame/Scripts/CameraFollow.cs in the JumpingBallGame.Runtime assembly.",
                        null,
                        workspaceRoot,
                        null);

                CollectionAssert.Contains(
                    paths,
                    "Assets/JumpingBallGame/Scripts/CameraFollow.cs");
                CollectionAssert.Contains(
                    paths,
                    "Assets/JumpingBallGame/Scripts/JumpingBallGame.Runtime.asmdef");
            }
            finally
            {
                Directory.Delete(workspaceRoot, recursive: true);
            }
        }


        [Test]
        public void ComponentInspectionTask_UsesAuthoritativePreflightEvidence()
        {
            const string request =
                "Inspect the selected object's Collider component.";

            const string inspection =
                "{\"GameObjectInstanceId\":42,\"GameObjectName\":\"TestObject\",\"Diagnostics\":[]}";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Verify capability: inspect_editor_component",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    inspection);

            StringAssert.Contains(
                "already completed the mandatory fresh selection",
                prompt);

            StringAssert.Contains(
                inspection,
                prompt);

            StringAssert.Contains(
                "do not substitute an example or placeholder instance ID",
                prompt);

            StringAssert.DoesNotContain(
                "MANDATORY: Call get_editor_selection",
                prompt);
        }


        [Test]
        public void RigidbodyConstraintTask_RequiresAuthoritativeDecodedFlags()
        {
            const string request =
                "Inspect the Player Rigidbody constraints and verify Freeze Position Z.";

            const string inspection =
                "{\"Properties\":[{\"Path\":\"m_Constraints\",\"Value\":\"28\"}]," +
                "\"Rigidbody\":{\"FreezePositionZ\":true}}";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Verify Rigidbody Z-freeze constraint on Player",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    inspection);

            StringAssert.Contains(
                "decoded FreezePositionX/Y/Z and FreezeRotationX/Y/Z Boolean fields exactly",
                prompt);

            StringAssert.Contains(
                "Do not manually reinterpret the integer",
                prompt);
        }


        [Test]
        public void ComponentInspectionPreflight_UsesExactSelectedGameObjectId()
        {
            const string selection =
                "{\"Objects\":[{\"Name\":\"TestObject\",\"IsGameObject\":true,\"GameObjectInstanceId\":31415}]}";

            int instanceId =
                Invoke<int>(
                    "ResolveSingleSelectedGameObjectId",
                    selection);

            Assert.AreEqual(
                31415,
                instanceId);
        }


        [Test]
        public void ReworkTargetResolution_PreservesOriginalNamedObject()
        {
            const string request =
                "Verify camera follow wiring against Player target\n" +
                "Confirm the Main Camera SideScrollCameraFollow target.\n" +
                "The first review requested rework because no GameObject was selected.";

            Assert.AreEqual(
                "Main Camera",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    request));
        }


        [Test]
        public void CameraTargetResolution_UsesRequiredFollowComponent()
        {
            Assert.AreEqual(
                "SideScrollCameraFollow",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "Main Camera",
                    "Confirm Main Camera has SideScrollCameraFollow configured."));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "Main Camera",
                    "Inspect the ordinary Main Camera."));
        }


        [Test]
        public void ComponentTypeExtraction_SkipsConfigureTargetName()
        {
            Assert.AreEqual(
                "CameraFollow",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "Main Camera",
                    "Create and configure Main Camera. Main Camera has CameraFollow with its target assigned."));

            Assert.AreEqual(
                "NavigationBrain",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "HoverDrone",
                    "Configure HoverDrone. HoverDrone has NavigationBrain attached."));
        }


        [Test]
        public void ComponentTypeExtraction_DoesNotTreatMaterialOrAxisAsComponent()
        {
            Assert.AreEqual(
                "GoalTrigger",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "Goal",
                    "Create Goal with GoalMaterial applied and attach GoalTrigger."));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "Ground",
                    "Create Ground at Y=0 spanning the level width along X."));
        }


        [Test]
        public void MissingComponentType_DoesNotInventScenePath()
        {
            Assert.IsNull(
                Invoke<string>(
                    "ResolveUniqueScenePathForComponent",
                    "AIRuntimeComponentThatDoesNotExist"));
        }


        [Test]
        public void CrossObjectCriteria_DoNotInventASecondInspectionTarget()
        {
            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                    "Verify GoalTrigger fires once and confirm the Player carries a Rigidbody."));

            Assert.AreEqual(
                "Goal",
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    "Using the Goal inspection, confirm the trigger behavior. The Player carries a Rigidbody."));
        }


        [Test]
        public void NamedCameraVerification_RequiresAuthoritativeComponentInspection()
        {
            const string request =
                "Using the Player inspection, confirm the Main Camera's SideScrollCameraFollow.target references the Player Transform and preserves a side view.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    request));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                request));
        }


        [Test]
        public void SceneSaveCompilationGate_DoesNotBecomeComponentTroubleshooting()
        {
            const string request =
                "Depends on sibling task \"Configure side-view follow camera in scene\" being Done.\n" +
                "Final persistence and compile gate. Ensure Assets/JumpingBallGame/Scenes/JumpingBallGame.unity is saved with all edits from previous cards. " +
                "Trigger a full script compile and confirm zero compilation errors for BallController, FallReset, CameraFollow, and GoalTrigger. " +
                "Fix any compile error found in Assets/JumpingBallGame scripts only.";

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    request));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractReviewGameObjectName",
                    "Save the scene; changes are required to fix a compile error."));

            Assert.IsNull(
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "fix",
                    "The scene is saved with all edits."));
        }


        [Test]
        public void CompilationGate_StillHonorsExplicitComponentInspection()
        {
            const string request =
                "Save the scene and compile scripts, then inspect the selected Player Rigidbody component and confirm its constraints.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    request));
        }


        [Test]
        public void SerializedMemberNotation_IdentifiesComponentWithoutAssetLookup()
        {
            Assert.AreEqual(
                "TrackingBehaviour",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "SensorRig",
                    "Confirm SensorRig's TrackingBehaviour.subject references the tracked Transform."));

            Assert.AreEqual(
                "TrackingBehaviour",
                Invoke<string>(
                    "ExtractTargetComponentType",
                    "SensorRig",
                    "Depends on Create TriggerHandler.cs. Confirm SensorRig's TrackingBehaviour.subject is assigned."));
        }


        [Test]
        public void NamedGoalVerification_RequiresAuthoritativeComponentInspection()
        {
            const string request =
                "Using the Goal inspection and GoalTrigger.cs, confirm the Player carries a Rigidbody so trigger callbacks fire.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresComponentInspection",
                    request));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresReviewComponentInspection",
                    request));
        }


        [Test]
        public void RemoveComponentVerification_UsesExactInspectedBoxColliderId()
        {
            const string request =
                "Verify capability: remove_editor_component. Remove only the intended BoxCollider.";

            const string inspection =
                "{\"GameObjectInstanceId\":42,\"Components\":[{\"Type\":\"UnityEngine.Transform\",\"InstanceId\":100},{\"Type\":\"UnityEngine.BoxCollider\",\"InstanceId\":200}]}";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Verify capability: remove_editor_component",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    inspection);

            StringAssert.Contains(
                "{\"componentInstanceId\":200}",
                prompt);

            StringAssert.Contains(
                "Do not omit ComponentInstanceId",
                prompt);
        }


        [Test]
        public void SerializedBooleanVerification_BuildsGuardedToggleAndRestoreArguments()
        {
            const string properties =
                "{\"ObjectInstanceId\":200,\"Properties\":[" +
                "{\"Path\":\"m_IsTrigger\",\"Type\":\"Boolean\",\"Editable\":true,\"Value\":false}]}";

            object verification =
                Invoke<object>(
                    "ResolveSerializedBooleanVerification",
                    200,
                    properties);

            Type type = verification.GetType();

            string toggle =
                (string) type.GetField(
                    "ToggleArguments")
                    .GetValue(verification);

            string restore =
                (string) type.GetField(
                    "RestoreArguments")
                    .GetValue(verification);

            StringAssert.Contains(
                "\"valueJson\":\"true\"",
                toggle);
            StringAssert.Contains(
                "\"expectedValueJson\":\"false\"",
                toggle);
            StringAssert.Contains(
                "\"useExpectedValue\":true",
                toggle);
            StringAssert.Contains(
                "\"valueJson\":\"false\"",
                restore);
            StringAssert.Contains(
                "\"expectedValueJson\":\"true\"",
                restore);
        }


        [Test]
        public void ComponentInspectionPreflight_RejectsMultipleGameObjects()
        {
            const string selection =
                "{\"Objects\":[{\"Name\":\"One\",\"IsGameObject\":true,\"GameObjectInstanceId\":1},{\"Name\":\"Two\",\"IsGameObject\":true,\"GameObjectInstanceId\":2}]}";

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () =>
                        Invoke<int>(
                            "ResolveSingleSelectedGameObjectId",
                            selection));

            StringAssert.Contains(
                "exactly one selected GameObject",
                exception.Message);
        }


        [Test]
        public void HierarchyTreePreflight_PreventsFilesystemPathCalls()
        {
            const string evidence =
                "{\"ObjectCount\":2,\"Roots\":[{\"Name\":\"Root\",\"HierarchyPath\":\"Root\",\"Children\":[{\"Name\":\"Child\",\"HierarchyPath\":\"Root/Child\"}]}]}";

            string prompt =
                Invoke<string>(
                    "AppendHierarchyTreeEvidence",
                    "Complete the hierarchy verification.",
                    evidence,
                    false);

            StringAssert.Contains(
                "already completed get_editor_selection and get_hierarchy_tree",
                prompt);

            StringAssert.Contains(
                "is not a filesystem path",
                prompt);

            StringAssert.Contains(
                "do not pass it to list_directory",
                prompt);

            StringAssert.Contains(
                evidence,
                prompt);
        }


        [Test]
        public void HierarchyTreeVerification_IsDetectedByCapabilityName()
        {
            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresHierarchyTree",
                    "Verify capability: get_hierarchy_tree"));
        }


        [Test]
        public void SceneWideHierarchyVerification_DoesNotRequireSelection()
        {
            const string request =
                "Verify the integrated JumpingBallGame. Call inspect_scene, " +
                "get_hierarchy_tree, inspect_game_object, and " +
                "inspect_editor_component for the important scene objects.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresHierarchyTree",
                    request));

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresSelectedHierarchyTree",
                    request));
        }


        [Test]
        public void AgentConfiguration_ExactFileNameWinsDuplicateAgentId()
        {
            Type pathsType =
                typeof(AIRuntimeKanbanSession)
                    .Assembly
                    .GetType(
                        "AIRuntime.Editor.AIRuntimeEditorAgentConfigurationPaths");

            Assert.NotNull(
                pathsType);

            MethodInfo shouldPrefer =
                pathsType.GetMethod(
                    "ShouldPrefer",
                    BindingFlags.Public |
                    BindingFlags.Static);

            Assert.NotNull(
                shouldPrefer);

            Assert.IsTrue(
                (bool)shouldPrefer.Invoke(
                    null,
                    new object[]
                    {
                        "DeveloperAgent",
                        @"C:\Agents\DeveloperAgent.json",
                        @"C:\Agents\DeveloperAgent2.json"
                    }));

            Assert.IsFalse(
                (bool)shouldPrefer.Invoke(
                    null,
                    new object[]
                    {
                        "DeveloperAgent",
                        @"C:\Agents\DeveloperAgent2.json",
                        @"C:\Agents\DeveloperAgent.json"
                    }));
        }


        [Test]
        public void SavedAgentFiles_CreateIndependentKanbanInstances()
        {
            Type pathsType =
                typeof(AIRuntimeKanbanSession)
                    .Assembly
                    .GetType(
                        "AIRuntime.Editor.AIRuntimeEditorAgentConfigurationPaths");

            Assert.NotNull(
                pathsType);

            MethodInfo resolveInstanceId =
                pathsType.GetMethod(
                    "ResolveInstanceId",
                    BindingFlags.Public |
                    BindingFlags.Static);

            Assert.NotNull(
                resolveInstanceId);

            Assert.AreEqual(
                "DeveloperAgent",
                resolveInstanceId.Invoke(
                    null,
                    new object[]
                    {
                        "DeveloperAgent",
                        @"C:\Users\User\Documents\AIRuntime\DeveloperAgent.json"
                    }));

            Assert.AreEqual(
                "DeveloperAgent2",
                resolveInstanceId.Invoke(
                    null,
                    new object[]
                    {
                        "DeveloperAgent",
                        @"C:\Users\User\Documents\AIRuntime\DeveloperAgent2.json"
                    }));

            Assert.AreEqual(
                "EditorAgent",
                resolveInstanceId.Invoke(
                    null,
                    new object[]
                    {
                        "EditorAgent",
                        @"C:\Users\User\Documents\AIRuntime\DefaultAgent.json"
                    }),
                "The established default editor-agent assignment must remain compatible.");
        }


        [TestCase("Provider crashed.", "Crashed")]
        [TestCase("[CompilationNoProgress] Same compiler errors repeated.", "Failed")]
        [TestCase("[ActionContinuationExhausted] The model repeatedly announced an action without executing it.", "Failed")]
        public void AgentExecutionException_RoutesTaskToExpectedStatus(string error, string expected)
        {
            Type statusType =
                typeof(AIRuntimeKanbanSession)
                    .GetNestedType(
                        "TaskStatus",
                        BindingFlags.NonPublic);

            Assert.NotNull(
                statusType);

            object inProgress =
                Enum.Parse(
                    statusType,
                    "InProgress");

            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "GetAgentCompletionStatus",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.NotNull(
                method);

            object crashed =
                method.Invoke(
                    null,
                    new[]
                    {
                        inProgress,
                        error
                    });

            Assert.AreEqual(
                expected,
                crashed.ToString());
        }


        [Test]
        public void MissingClaimedDeliverable_RoutesTaskToFailedInsteadOfCrashed()
        {
            Type statusType =
                typeof(AIRuntimeKanbanSession)
                    .GetNestedType(
                        "TaskStatus",
                        BindingFlags.NonPublic);

            object inProgress =
                Enum.Parse(
                    statusType,
                    "InProgress");

            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "GetAgentCompletionStatus",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            object failed =
                method.Invoke(
                    null,
                    new[]
                    {
                        inProgress,
                        "The agent claimed completion, but these explicit asset deliverables do not exist: Assets/Game/Game.unity"
                    });

            Assert.AreEqual(
                "Failed",
                failed.ToString());
        }


        [Test]
        public void MissingClaimedDeliverable_GetsOneConcreteImplementationRecoveryBeforeFailedGuard()
        {
            Type windowType =
                typeof(AIRuntimeKanbanSession);

            Type taskType =
                windowType.GetNestedType(
                    "KanbanTask",
                    BindingFlags.NonPublic);

            Type statusType =
                windowType.GetNestedType(
                    "TaskStatus",
                    BindingFlags.NonPublic);

            MethodInfo method =
                windowType.GetMethod(
                    "TryApplyExplicitArtifactRecovery",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(taskType);
            Assert.NotNull(statusType);
            Assert.NotNull(method);

            object task =
                Activator.CreateInstance(
                    taskType);

            string missingPath =
                "Assets/__AIRuntimeMissingArtifact_" +
                Guid.NewGuid().ToString("N") +
                ".asset";

            const string errorPrefix =
                "The agent claimed completion, but these explicit asset deliverables do not exist: ";

            string error =
                errorPrefix +
                missingPath;

            SetTaskField(
                taskType,
                task,
                "Status",
                Enum.Parse(
                    statusType,
                    "InProgress"));

            SetTaskField(
                taskType,
                task,
                "ProducedArtifactPaths",
                new List<string>
                {
                    missingPath
                });

            SetTaskField(
                taskType,
                task,
                "ExecutionMemory",
                "persisted execution memory");

            SetTaskField(
                taskType,
                task,
                "NextAgentMessage",
                "[ProviderCooldownElapsed] stale provider handoff");

            object[] firstArguments =
            {
                task,
                "partial implementation result",
                error,
                null
            };

            bool recovered =
                (bool)method.Invoke(
                    null,
                    firstArguments);

            Assert.IsTrue(recovered);
            Assert.AreEqual(
                missingPath,
                firstArguments[3]);
            Assert.AreEqual(
                "Todo",
                GetTaskField(taskType, task, "Status").ToString());
            Assert.AreEqual(
                "Rework Requested",
                GetTaskField(taskType, task, "ReviewStatus"));
            Assert.AreEqual(
                "ImplementationDefect",
                GetTaskField(taskType, task, "ReviewFailureKind"));
            Assert.AreEqual(
                missingPath,
                GetTaskField(taskType, task, "BlockedReason"));
            Assert.AreEqual(
                "persisted execution memory",
                GetTaskField(taskType, task, "ExecutionMemory"));
            Assert.AreEqual(
                string.Empty,
                GetTaskField(taskType, task, "AgentError"));

            string handoff =
                (string)GetTaskField(
                    taskType,
                    task,
                    "NextAgentMessage");

            StringAssert.StartsWith(
                "[ExplicitArtifactRecovery]",
                handoff);
            StringAssert.Contains(
                missingPath,
                handoff);
            StringAssert.Contains(
                "not a substitute",
                handoff);

            SetTaskField(
                taskType,
                task,
                "Status",
                Enum.Parse(
                    statusType,
                    "InProgress"));

            object[] repeatedArguments =
            {
                task,
                string.Empty,
                error,
                null
            };

            Assert.IsFalse(
                (bool)method.Invoke(
                    null,
                    repeatedArguments),
                "An unchanged second failure must fall through to the existing terminal Failed guard instead of looping forever.");
        }


        [Test]
        public void SuccessfulCompletion_RoutesReloadPausedTodoTaskToReview()
        {
            Type statusType =
                typeof(AIRuntimeKanbanSession)
                    .GetNestedType(
                        "TaskStatus",
                        BindingFlags.NonPublic);

            object todo =
                Enum.Parse(
                    statusType,
                    "Todo");

            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "GetAgentCompletionStatus",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            object review =
                method.Invoke(
                    null,
                    new[]
                    {
                        todo,
                        string.Empty
                    });

            Assert.AreEqual(
                "Review",
                review.ToString());
        }


        [Test]
        public void DomainReload_RequeuesImplementationAndReviewWithoutFailure()
        {
            Type windowType =
                typeof(AIRuntimeKanbanSession);

            Type taskType =
                windowType.GetNestedType(
                    "KanbanTask",
                    BindingFlags.NonPublic);

            Type statusType =
                windowType.GetNestedType(
                    "TaskStatus",
                    BindingFlags.NonPublic);

            object implementation =
                Activator.CreateInstance(
                    taskType);

            object review =
                Activator.CreateInstance(
                    taskType);

            SetTaskField(
                taskType,
                implementation,
                "Status",
                Enum.Parse(
                    statusType,
                    "InProgress"));

            SetTaskField(
                taskType,
                implementation,
                "AgentError",
                "stale error");

            SetTaskField(
                taskType,
                review,
                "Status",
                Enum.Parse(
                    statusType,
                    "Review"));

            SetTaskField(
                taskType,
                review,
                "ReviewStatus",
                "Running");

            Type listType =
                typeof(List<>).MakeGenericType(
                    taskType);

            IList tasks =
                (IList)Activator.CreateInstance(
                    listType);

            tasks.Add(
                implementation);

            tasks.Add(
                review);

            MethodInfo recover =
                windowType.GetMethod(
                    "RecoverInterruptedTasks",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                recover);

            Assert.AreEqual(
                2,
                recover.Invoke(
                    null,
                    new object[] { tasks }));

            Assert.AreEqual(
                "Todo",
                GetTaskField(
                        taskType,
                        implementation,
                        "Status")
                    .ToString());

            Assert.AreEqual(
                string.Empty,
                GetTaskField(
                    taskType,
                    implementation,
                    "AgentError"));

            Assert.AreEqual(
                "Review",
                GetTaskField(
                        taskType,
                        review,
                        "Status")
                    .ToString());

            Assert.AreEqual(
                "Pending",
                GetTaskField(
                    taskType,
                    review,
                    "ReviewStatus"));
        }


        [Test]
        public void OrphanedInProgressTask_IsRecoveredWhenNoLiveRunnerExists()
        {
            Type windowType =
                typeof(AIRuntimeKanbanSession);

            Type taskType =
                windowType.GetNestedType(
                    "KanbanTask",
                    BindingFlags.NonPublic);

            Type boardType =
                windowType.GetNestedType(
                    "KanbanBoard",
                    BindingFlags.NonPublic);

            Type statusType =
                windowType.GetNestedType(
                    "TaskStatus",
                    BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(
                        window,
                        false);

                object board =
                    Activator.CreateInstance(
                        boardType);

                IList tasks =
                    (IList)boardType.GetField(
                            "Tasks")
                        .GetValue(
                            board);

                object task =
                    Activator.CreateInstance(
                        taskType);

                SetTaskField(
                    taskType,
                    task,
                    "Id",
                    "orphaned-task");

                SetTaskField(
                    taskType,
                    task,
                    "Status",
                    Enum.Parse(
                        statusType,
                        "InProgress"));

                tasks.Add(
                    task);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(
                        window,
                        board);

                MethodInfo recover =
                    windowType.GetMethod(
                        "RecoverOrphanedAgentRuns",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance);

                Assert.NotNull(
                    recover);

                Assert.AreEqual(
                    1,
                    recover.Invoke(
                        window,
                        null));

                Assert.AreEqual(
                    "Todo",
                    GetTaskField(
                            taskType,
                            task,
                            "Status")
                        .ToString());
            }
            finally
            {
                ScriptableObject.DestroyImmediate(
                    window);
            }
        }


        [Test]
        public void InactiveTrackedCoordinationRun_ReleasesAgentSlot()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type activeRunType = windowType.GetNestedType("ActiveAgentRun", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "inactive-coordinator");
                SetTaskField(taskType, task, "Title", "Read one script and quote evidence");
                SetTaskField(taskType, task, "AssignedAgent", "DeveloperAgent");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, task, "AgentStartedAt", DateTime.UtcNow.AddMinutes(-10).ToString("O"));
                tasks.Add(task);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                object activeRun = Activator.CreateInstance(activeRunType);
                activeRunType.GetField("TaskId").SetValue(activeRun, "inactive-coordinator");
                activeRunType.GetField("Cancellation").SetValue(
                    activeRun,
                    new System.Threading.CancellationTokenSource());
                activeRunType.GetField("CoordinationOnly").SetValue(activeRun, true);
                activeRunType.GetField("LastActivityAtUtc").SetValue(
                    activeRun,
                    DateTime.UtcNow.AddMinutes(-16));

                IDictionary activeRuns =
                    (IDictionary)windowType.GetField(
                            "_activeAgentRuns",
                            BindingFlags.NonPublic | BindingFlags.Instance)
                        .GetValue(window);
                activeRuns.Add("DeveloperAgent", activeRun);

                MethodInfo recover = windowType.GetMethod(
                    "RecoverInactiveTrackedAgentRuns",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.AreEqual(1, recover.Invoke(window, null));
                Assert.AreEqual(0, activeRuns.Count);
                Assert.IsNotEmpty((string)GetTaskField(taskType, task, "AgentFinishedAt"));
                StringAssert.StartsWith(
                    "[PlanningRunInactivityRecovery]",
                    (string)GetTaskField(taskType, task, "NextAgentMessage"));
                Assert.AreEqual(
                    "Todo",
                    GetTaskField(taskType, task, "Status").ToString());
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ExecutionMemoryProgress_ExcludesStartedAndFailedTools()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "ExecutionMemoryUpdateRepresentsProgress",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                method);

            Func<string, bool> representsProgress =
                eventName =>
                    (bool)method.Invoke(
                        null,
                        new object[]
                        {
                            "{\"Entries\":[{\"Event\":\"" +
                            eventName +
                            "\"}]}"
                        });

            Assert.IsFalse(
                representsProgress("RunStarted"));

            Assert.IsFalse(
                representsProgress("ToolStarted"));

            Assert.IsFalse(
                representsProgress("ToolFailed"));

            Assert.IsTrue(
                representsProgress("ToolCompleted"));

            Assert.IsTrue(
                representsProgress("RunCompleted"));
        }


        [Test]
        public void ImplementationCapabilityNoProgress_WithDeclaredOutputs_AdvancesToReview()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            string relativeOutput =
                "Assets/AIRuntimeCapabilityVerification/no-progress-output-" +
                Guid.NewGuid().ToString("N") +
                ".txt";
            string absoluteOutput =
                Path.Combine(
                    Directory.GetParent(Application.dataPath).FullName,
                    relativeOutput.Replace('/', Path.DirectorySeparatorChar));

            try
            {
                Directory.CreateDirectory(Path.GetDirectoryName(absoluteOutput));
                File.WriteAllText(absoluteOutput, "persisted implementation");

                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "implementation-no-progress");
                SetTaskField(taskType, task, "Title", "Create persisted output");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, task, "ProducedArtifactPaths", new List<string> { relativeOutput });
                tasks.Add(task);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                windowType.GetMethod(
                        "CompleteAgentTask",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(
                        window,
                        new object[]
                        {
                            "implementation-no-progress",
                            "DeveloperAgent",
                            string.Empty,
                            "[CapabilityNoProgress] The provider repeated the identical capability call 'get_serialized_properties' after AIRuntime had already replayed unchanged authoritative evidence. The run was stopped to prevent a no-progress tool-call loop.",
                            null,
                            false
                        });

                Assert.AreEqual("Review", GetTaskField(taskType, task, "Status").ToString());
                Assert.AreEqual("Pending", GetTaskField(taskType, task, "ReviewStatus"));
                Assert.AreEqual(string.Empty, GetTaskField(taskType, task, "AgentError"));
                StringAssert.StartsWith(
                    "[ImplementationNoProgressRecovery]",
                    (string)GetTaskField(taskType, task, "AgentResult"));
            }
            finally
            {
                if (File.Exists(absoluteOutput))
                    File.Delete(absoluteOutput);

                UnityEngine.Object.DestroyImmediate(window);
            }
        }

        [TestCase(false, false, false, false, false, false)]
        [TestCase(true, false, false, false, false, false)]
        [TestCase(false, true, false, false, false, false)]
        [TestCase(false, true, true, false, false, false)]
        [TestCase(false, false, true, false, false, false)]
        [TestCase(false, true, true, true, false, false)]
        [TestCase(false, false, false, false, true, false)]
        [TestCase(false, false, false, true, true, false)]
        [TestCase(true, false, false, false, true, false)]
        [TestCase(false, true, false, false, false, true)]
        [TestCase(false, true, false, true, false, true)]
        [TestCase(true, true, false, true, false, true)]
        public void ImplementationOwnershipConflict_UsesAuthoritativeFailureAndPreservesGraph(bool laterSucceeded, bool unowned, bool capabilityRequest, bool recoverPersisted, bool readDenied, bool providerError)
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            string folder = "Assets/AIRuntimeOwnershipVerification-" + Guid.NewGuid().ToString("N");
            string absolute = Path.Combine(Directory.GetParent(Application.dataPath).FullName, folder);
            string file = folder + "/PlayerController.cs";
            try
            {
                Directory.CreateDirectory(absolute);
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object setup = Activator.CreateInstance(taskType);
                object owner = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "ownership-root");
                SetTaskField(taskType, root, "Title", "Game");
                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, setup, "Id", "ownership-setup");
                SetTaskField(taskType, setup, "Title", "Project Structure and Core Architecture");
                SetTaskField(taskType, setup, "ParentTaskId", "ownership-root");
                SetTaskField(taskType, setup, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, setup, "ProducedArtifactPaths", new List<string> { folder });
                SetTaskField(taskType, owner, "Id", "ownership-player");
                SetTaskField(taskType, owner, "Title", "Player Controller");
                SetTaskField(taskType, owner, "ParentTaskId", "ownership-root");
                SetTaskField(taskType, owner, "ProducedArtifactPaths", unowned ? new List<string>() : new List<string> { file });
                SetTaskField(taskType, owner, "Status", Enum.Parse(statusType, "Todo"));
                var entries = new JArray(new JObject {
                    ["Event"] = "ToolFailed", ["Tool"] = readDenied ? "read_file" : "write_file",
                    ["Arguments"] = new JObject { [unowned ? "assetPath" : "path"] = file, ["content"] = "placeholder" }.ToString(),
                    ["Result"] = (readDenied ? "[ArtifactReadScope] Cannot read sibling output: " : "[ArtifactOwnership] Cannot create sibling output: ") + file
                });
                if (providerError && !laterSucceeded)
                    entries.Insert(0, new JObject {
                        ["Event"] = "ToolFailed", ["Tool"] = "read_file",
                        ["Arguments"] = new JObject { ["path"] = folder + "/Architecture.md" }.ToString(),
                        ["Result"] = "[ArtifactReadScope] Cannot read undeclared input."
                    });
                if (laterSucceeded)
                    entries.Add(new JObject {
                        ["Event"] = "ToolCompleted", ["Tool"] = readDenied ? "read_file" : "write_file",
                        ["Arguments"] = new JObject { [unowned ? "assetPath" : "path"] = file }.ToString(), ["Result"] = "Succeeded"
                    });
                SetTaskField(taskType, setup, "ExecutionMemory", new JObject { ["Entries"] = entries }.ToString());
                tasks.Add(root); tasks.Add(setup); tasks.Add(owner);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(window, board);
                if (recoverPersisted)
                {
                    SetTaskField(taskType, setup, "Status", Enum.Parse(statusType, "Crashed"));
                    SetTaskField(taskType, setup, "ReviewStatus", readDenied ? "Crashed" : "Capability Required");
                    SetTaskField(taskType, setup, "ReviewFailureKind", readDenied ? "" : "CapabilityGap");
                    if (providerError) SetTaskField(taskType, setup, "AgentError", "Nous Portal request failed. HTTP 400. Provider returned error");
                    if (readDenied) SetTaskField(taskType, setup, "AgentError", "[CapabilityNoProgress] repeated read_file" );
                    windowType.GetMethod("RecoverOwnershipCapabilityRequests", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null);
                }
                else
                    windowType.GetMethod("CompleteAgentTask", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window,
                        new object[] { "ownership-setup", "DeveloperAgent",
                            capabilityRequest ? "[CapabilityRequest] name=create_material; gap=missing ownership" : "",
                            capabilityRequest ? "" : providerError ? "Nous Portal request failed. HTTP 400. Provider returned error" : "[CapabilityNoProgress] Repeated failed write_file", null, false });

                Assert.AreEqual(laterSucceeded ? (recoverPersisted ? "Crashed" : "Review") : "Todo", GetTaskField(taskType, setup, "Status").ToString());
                Assert.AreEqual(laterSucceeded ? "Implementation" : "Planning", GetTaskField(taskType, root, "WorkflowPhase"));
                if (!laterSucceeded)
                {
                    Assert.AreEqual("Planning Required", GetTaskField(taskType, setup, "ReviewStatus"));
                    StringAssert.Contains(file, (string)GetTaskField(taskType, root, "PlanningKickbackReason"));
                    StringAssert.Contains(unowned ? "[MissingArtifactOwnership]" : "ownership-player", (string)GetTaskField(taskType, root, "PlanningKickbackReason"));
                }
                Assert.AreEqual(3, tasks.Count, "Recovery must preserve the committed graph.");
                Assert.AreSame(owner, tasks[2]);
                Assert.IsTrue(Directory.Exists(absolute));
                Assert.IsFalse(File.Exists(Path.Combine(absolute, "PlayerController.cs")), "Recovery must not bypass ownership by writing the file.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
                if (Directory.Exists(absolute)) Directory.Delete(absolute, true);
            }
        }


        [Test]
        public void ImplementationCapabilityNoProgress_WithMissingScene_RequeuesOnceWithPersistenceCorrection()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            const string scenePath =
                "Assets/AIRuntimeCapabilityVerification/MissingRecoveredScene.unity";
            const string noProgressError =
                "[CapabilityNoProgress] The provider repeated the identical capability call 'get_serialized_properties' after AIRuntime had already replayed unchanged authoritative evidence. The run was stopped to prevent a no-progress tool-call loop.";

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "missing-scene-no-progress");
                SetTaskField(taskType, task, "Title", "Persist assembled scene");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(
                    taskType,
                    task,
                    "ProducedArtifactPaths",
                    new List<string> { scenePath });
                SetTaskField(taskType, task, "ExecutionMemory", "authoritative hierarchy inspection");
                tasks.Add(task);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo complete =
                    windowType.GetMethod(
                        "CompleteAgentTask",
                        BindingFlags.NonPublic | BindingFlags.Instance);

                complete.Invoke(
                    window,
                    new object[]
                    {
                        "missing-scene-no-progress",
                        "DeveloperAgent",
                        string.Empty,
                        noProgressError,
                        null,
                        false
                    });

                Assert.AreEqual(
                    "Todo",
                    GetTaskField(taskType, task, "Status").ToString());
                Assert.AreEqual(
                    string.Empty,
                    GetTaskField(taskType, task, "AgentError"));
                Assert.AreEqual(
                    "authoritative hierarchy inspection",
                    GetTaskField(taskType, task, "ExecutionMemory"));
                string nextMessage =
                    (string)GetTaskField(taskType, task, "NextAgentMessage");
                StringAssert.StartsWith(
                    "[CapabilityNoProgressRecovery]",
                    nextMessage);
                StringAssert.Contains(
                    "destinationScenePath='" + scenePath + "'",
                    nextMessage);
                StringAssert.Contains(
                    "Do not repeat a failed call shape",
                    nextMessage);

                SetTaskField(
                    taskType,
                    task,
                    "Status",
                    Enum.Parse(statusType, "InProgress"));

                UnityEngine.TestTools.LogAssert.Expect(
                    LogType.Error,
                    "[Kanban] DeveloperAgent failed task 'Persist assembled scene': " +
                    noProgressError);

                complete.Invoke(
                    window,
                    new object[]
                    {
                        "missing-scene-no-progress",
                        "DeveloperAgent",
                        string.Empty,
                        noProgressError,
                        null,
                        false
                    });

                Assert.AreEqual(
                    "Crashed",
                    GetTaskField(taskType, task, "Status").ToString(),
                    "The corrective retry must remain bounded if the provider repeats the same no-progress loop.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void KanbanScheduler_ContainsBackgroundEditorLoopKeepalive()
        {
            MethodInfo keepalive =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "KeepEditorLoopAliveForAgentRuns",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            Assert.NotNull(
                keepalive,
                "Active agent runs need an editor-loop keepalive so losing focus cannot strand their UnitySynchronizationContext continuations.");
        }


        [Test]
        public void KanbanScheduler_QueuesFinishedRunsForMainThreadHandoff()
        {
            MethodInfo drain =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "DrainPendingAgentCompletions",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            Assert.NotNull(
                drain,
                "Finished background runs must be handed back to the Kanban board without relying on a captured UnitySynchronizationContext continuation.");
        }


        [Test]
        public void KanbanScheduler_HasOwnedPlayModeCompletionCleanup()
        {
            MethodInfo cleanup =
                typeof(ControlPlayModeCapability).GetMethod(
                    "EndOwnedSession",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                cleanup,
                "A completed, failed, or cancelled Kanban run must be able to release only the Play Mode session it owns.");
        }


        [Test]
        public void KanbanScheduler_PreservesExplicitRunStateAcrossScriptReload()
        {
            MethodInfo consumeReloadState =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "ConsumeRunAgentsReloadState",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                consumeReloadState,
                "A script compilation must not leave active agents green but idle by silently disabling the scheduler.");
        }

        [TestCase(true)]
        [TestCase(false)]
        public void KanbanScheduler_ReloadPreservesRunIntentWithOnlyReviewWork(bool resume)
        {
            Type type = typeof(AIRuntimeKanbanSession);
            Type taskType = type.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = type.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = type.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            string reloadKey = (string)type.GetField("ReloadResumeAgentsKey", BindingFlags.Static | BindingFlags.NonPublic).GetRawConstantValue();
            bool previous = UnityEditor.SessionState.GetBool(reloadKey, false);
            var window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            try
            {
                type.GetField("_autoSave", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "review-only-root");
                SetTaskField(taskType, task, "Title", "Final review");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));
                SetTaskField(taskType, task, "WorkflowPhase", "FinalReview");
                SetTaskField(taskType, task, "ReviewStatus", "Pending");
                SetTaskField(taskType, task, "AssignedAgent", "ReviewAgent");
                ((IList)boardType.GetField("Tasks").GetValue(board)).Add(task);
                type.GetField("_board", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, board);
                UnityEditor.SessionState.SetBool(reloadKey, resume);
                type.GetMethod("OnEnable", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(window, null);
                Assert.AreEqual(resume, type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window),
                    "Reload must preserve explicit intent independently of whether a Todo card exists; cold opens remain stopped.");
                Assert.IsFalse(UnityEditor.SessionState.GetBool(reloadKey, false), "Resume token is consumed once.");
            }
            finally
            {
                type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                UnityEngine.Object.DestroyImmediate(window);
                UnityEditor.SessionState.SetBool(reloadKey, previous);
            }
        }

        [Test]
        public void KanbanScheduler_InteractiveRunClearsSpentStepBudgetAndLogsStateChangesOnly()
        {
            Type type = typeof(AIRuntimeKanbanSession);
            var window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            var records = new List<AIRuntime.Runtime.Providers.ProviderTransportRecord>();
            Action<AIRuntime.Runtime.Providers.ProviderTransportRecord> collect = record => {
                if (record.Direction == "SCHEDULER STATE") records.Add(record);
            };
            AIRuntime.Runtime.Providers.ProviderTransportDebug.RecordPublished += collect;
            try
            {
                type.GetField("_autoSave", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                type.GetField("_board", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window,
                    Activator.CreateInstance(type.GetNestedType("KanbanBoard", BindingFlags.NonPublic)));
                type.GetField("_agentStartsRemaining", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, 0);
                type.GetMethod("SetAutomaticAgentScheduling", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(window, new object[] { true });
                Assert.AreEqual(-1, type.GetField("_agentStartsRemaining", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window));
                Assert.IsTrue((bool)type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window));
                Assert.AreEqual(1, records.Count);
                var state = JObject.Parse(records[0].Payload);
                Assert.IsTrue((bool)state["RunAgents"]);
                Assert.AreEqual(-1, (int)state["StartsRemaining"]);
                type.GetField("_nextSchedulerDiagnosticAt", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, 0d);
                type.GetMethod("PublishSchedulerDiagnostics", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(window, new object[] { null });
                Assert.AreEqual(1, records.Count, "Unchanged scheduler state must not flood the journal.");
                type.GetMethod("SetAutomaticAgentScheduling", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(window, new object[] { false });
                Assert.AreEqual(2, records.Count);
                Assert.AreEqual("Run agents is disabled", (string)JObject.Parse(records[1].Payload)["Gate"]);
            }
            finally
            {
                AIRuntime.Runtime.Providers.ProviderTransportDebug.RecordPublished -= collect;
                type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void KanbanWindow_ExposesCompletionApplicationDuringReloadPreparation()
        {
            MethodInfo completion =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "CompleteAgentTask",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            Assert.NotNull(
                completion,
                "A completed run must still be applied and persisted when it races with Editor reload preparation.");
        }

        [TestCase(false, false, true, true)]
        [TestCase(true, false, true, false)]
        [TestCase(false, true, true, false)]
        [TestCase(false, false, false, false)]
        public void FailedCompilationRecovery_ReleasesGuardOnlyAfterFailureSettles(bool compiling, bool updating, bool failed, bool recover)
        {
            Type type = typeof(AIRuntimeKanbanSession);
            Type taskType = type.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = type.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = type.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            var window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            try
            {
                type.GetField("_autoSave", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                type.GetField("_isPreparingForAssemblyReload", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, true);
                object board = Activator.CreateInstance(boardType);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "failed-compile-owner");
                SetTaskField(taskType, task, "Title", "Respawn Manager");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, task, "NextAgentMessage", "[AssemblyReloadResume] Previous import was interrupted.");
                SetTaskField(taskType, task, "ProducedArtifactPaths", new List<string> { "Assets/Game/RespawnManager.cs" });
                ((IList)boardType.GetField("Tasks").GetValue(board)).Add(task);
                type.GetField("_board", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, board);
                MethodInfo recovery = type.GetMethod("TryRecoverFailedCompilationWithoutReload", BindingFlags.Instance | BindingFlags.NonPublic);
                Assert.AreEqual(recover, recovery.Invoke(window, new object[] { compiling, updating, failed }));
                Assert.AreEqual(!recover, type.GetField("_isPreparingForAssemblyReload", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window));
                Assert.IsFalse((bool)type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window),
                    "Recovery must not turn a manually stopped scheduler on.");
                Assert.AreEqual(1, ((IList)boardType.GetField("Tasks").GetValue(board)).Count);
                if (recover)
                {
                    Assert.AreEqual("Rework Requested", GetTaskField(taskType, task, "ReviewStatus"));
                    StringAssert.StartsWith("[CompilationFailureResume]", (string)GetTaskField(taskType, task, "NextAgentMessage"));
                    Assert.IsFalse((bool)recovery.Invoke(window, new object[] { false, false, true }), "Recovery is applied once.");
                }
            }
            finally { UnityEngine.Object.DestroyImmediate(window); }
        }

        [TestCase("cooldown")]
        [TestCase("running")]
        [TestCase("dependency")]
        public void AgentStatusWidget_IdentifiesTheActualActiveOrWaitingCard(string scenario)
        {
            Type type = typeof(AIRuntimeKanbanSession);
            Type taskType = type.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = type.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = type.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            var window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            try
            {
                type.GetField("_autoSave", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, true);
                type.GetField("_agentOptions", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, null);
                var active = (HashSet<string>)type.GetField("_activeKanbanAgentIds", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window);
                active.Clear(); active.Add("DeveloperAgent");
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object NewTask(string id, string title, string parent, string agent)
                {
                    object task = Activator.CreateInstance(taskType);
                    SetTaskField(taskType, task, "Id", id);
                    SetTaskField(taskType, task, "Title", title);
                    SetTaskField(taskType, task, "ParentTaskId", parent);
                    SetTaskField(taskType, task, "AssignedAgent", agent);
                    SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Todo"));
                    tasks.Add(task);
                    return task;
                }
                object root = NewTask("widget-root", "Game", "", "DeveloperAgent");
                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                // Put the blocked card first to catch a widget that merely
                // displays the first unfinished card rather than the next one.
                object player = NewTask("widget-player", "Player Controller", "widget-root", "DeveloperAgent");
                SetTaskField(taskType, player, "DependsOnTaskIds", new List<string> { "widget-architecture" });
                object architecture = NewTask("widget-architecture", "Architecture", "widget-root",
                    scenario == "dependency" ? "ArchitectAgent" : "DeveloperAgent");
                type.GetField("_board", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, board);

                if (scenario != "dependency")
                {
                    Type cooldownType = type.GetNestedType("ProviderCooldownState", BindingFlags.NonPublic);
                    object cooldown = Activator.CreateInstance(cooldownType);
                    cooldownType.GetField("AgentId").SetValue(cooldown, "DeveloperAgent");
                    cooldownType.GetField("ProviderName").SetValue(cooldown, "Nous Portal");
                    cooldownType.GetField("UntilUtc").SetValue(cooldown, DateTimeOffset.UtcNow.AddMinutes(5).ToString("O"));
                    ((IList)boardType.GetField("ProviderCooldowns").GetValue(board)).Add(cooldown);
                }
                if (scenario == "running")
                {
                    Type runType = type.GetNestedType("ActiveAgentRun", BindingFlags.NonPublic);
                    object run = Activator.CreateInstance(runType);
                    runType.GetField("TaskId").SetValue(run, "widget-player");
                    SetTaskField(taskType, player, "Status", Enum.Parse(statusType, "InProgress"));
                    ((IDictionary)type.GetField("_activeAgentRuns", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window))
                        .Add("DeveloperAgent", run);
                }
                IList rows = (IList)type.GetMethod("BuildAgentStatusRows", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(window, null);
                Assert.AreEqual(1, rows.Count);
                object row = rows[0]; Type rowType = row.GetType();
                Assert.AreSame(scenario == "cooldown" ? architecture : player, rowType.GetField("Task").GetValue(row));
                string state = (string)rowType.GetField("Status").GetValue(row);
                if (scenario == "cooldown")
                {
                    StringAssert.StartsWith("Cooldown ", state);
                    StringAssert.Contains("Nous Portal", (string)rowType.GetField("Detail").GetValue(row));
                }
                else if (scenario == "running") Assert.AreEqual("Running", state);
                else
                {
                    Assert.AreEqual("Waiting", state);
                    Assert.AreSame(architecture, rowType.GetField("Dependency").GetValue(row));
                }
            }
            finally
            {
                ((IDictionary)type.GetField("_activeAgentRuns", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(window)).Clear();
                type.GetField("_autoRunAgents", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(window, false);
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void InvalidDecomposition_PreservesDraftStackForCorrectiveRework()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Make Game");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, parent, "AgentStartedAt", DateTime.UtcNow.AddMinutes(-1).ToString("O"));
                tasks.Add(parent);

                object draft = Activator.CreateInstance(taskType);
                SetTaskField(taskType, draft, "Id", "draft-one");
                SetTaskField(taskType, draft, "Title", "Combined Scene");
                SetTaskField(taskType, draft, "ParentTaskId", "parent");
                SetTaskField(taskType, draft, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, draft, "IsDecompositionDraft", true);
                SetTaskField(taskType, draft, "CreatedAt", DateTime.UtcNow.ToString("O"));
                SetTaskField(taskType, draft, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(draft);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo complete = windowType.GetMethod(
                    "CompleteAgentTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                const string validationError =
                    "Scene consumer(s) for MonoBehaviour 'PlayerBall' do not depend on its producer.";

                complete.Invoke(
                    window,
                    new object[]
                    {
                        "parent",
                        "DeveloperAgent",
                        string.Empty,
                        validationError,
                        null,
                        false
                    });

                Assert.AreEqual(2, tasks.Count, "The blocked draft stack must remain available for branch rework.");
                Assert.IsTrue((bool)taskType.GetField("IsDecompositionDraft").GetValue(draft));
                Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(parent).ToString());
                Assert.AreEqual(1, taskType.GetField("DecompositionFailureCount").GetValue(parent));
                StringAssert.Contains(
                    "complete desired direct-child graph",
                    (string)taskType.GetField("NextAgentMessage").GetValue(parent));

                SetTaskField(taskType, parent, "AgentStartedAt", DateTime.UtcNow.AddSeconds(-1).ToString("O"));
                object secondDraft = Activator.CreateInstance(taskType);
                SetTaskField(taskType, secondDraft, "Id", "draft-two");
                SetTaskField(taskType, secondDraft, "ParentTaskId", "parent");
                SetTaskField(taskType, secondDraft, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, secondDraft, "IsDecompositionDraft", true);
                SetTaskField(taskType, secondDraft, "CreatedAt", DateTime.UtcNow.ToString("O"));
                tasks.Add(secondDraft);

                complete.Invoke(
                    window,
                    new object[]
                    {
                        "parent",
                        "DeveloperAgent",
                        string.Empty,
                        validationError,
                        null,
                        false
                    });

                Assert.AreEqual(3, tasks.Count);
                Assert.AreEqual("Failed", taskType.GetField("Status").GetValue(parent).ToString());
                Assert.AreEqual(2, taskType.GetField("DecompositionFailureCount").GetValue(parent));
                Assert.AreEqual(
                    "Delegation Blocked by No Progress",
                    taskType.GetField("ReviewStatus").GetValue(parent));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("AgentError").GetValue(parent));
                StringAssert.StartsWith(
                    "[PlanningNoProgress]",
                    (string)taskType.GetField("NextAgentMessage").GetValue(parent));
                StringAssert.Contains(
                    "Scene consumer(s)",
                    (string)taskType.GetField("BlockedReason").GetValue(parent));
                Assert.IsTrue((bool)taskType.GetField("IsDecompositionDraft").GetValue(draft));
                Assert.IsTrue((bool)taskType.GetField("IsDecompositionDraft").GetValue(secondDraft));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void InvalidDecomposition_VaryingErrorsStopAtRetryLimit()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Build broad outcome");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(parent);

                object draft = Activator.CreateInstance(taskType);
                SetTaskField(taskType, draft, "Id", "draft");
                SetTaskField(taskType, draft, "ParentTaskId", "parent");
                SetTaskField(taskType, draft, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, draft, "IsDecompositionDraft", true);
                tasks.Add(draft);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo complete = windowType.GetMethod(
                    "CompleteAgentTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                for (int attempt = 1; attempt <= 3; attempt++)
                {
                    complete.Invoke(
                        window,
                        new object[]
                        {
                            "parent",
                            "DeveloperAgent",
                            string.Empty,
                            $"Granular decomposition contains dependency cycle variant {attempt}.",
                            null,
                            false
                        });
                }

                Assert.AreEqual(
                    3,
                    GetTaskField(taskType, parent, "DecompositionFailureCount"));
                Assert.AreEqual(
                    "Failed",
                    GetTaskField(taskType, parent, "Status").ToString());
                Assert.AreEqual(
                    "Delegation Blocked by No Progress",
                    GetTaskField(taskType, parent, "ReviewStatus"));
                StringAssert.StartsWith(
                    "[PlanningNoProgress]",
                    (string)GetTaskField(taskType, parent, "NextAgentMessage"));

                MethodInfo retryCandidate = windowType.GetMethod(
                    "IsCorrectiveDecompositionRetryCandidate",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsFalse(
                    (bool)retryCandidate.Invoke(window, new[] { parent }),
                    "The scheduler must never restart a card which reached the decomposition retry ceiling.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void InvalidDecomposition_AtomicScriptOwnerRequeuesForDirectExecution()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "game-manager");
                SetTaskField(taskType, parent, "Title", "Author GameManager.cs");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(
                    taskType,
                    parent,
                    "Description",
                    "In Update, compare player.transform.y with fallThreshold. Do not modify a scene.");
                SetTaskField(
                    taskType,
                    parent,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/BallJumpGame/Scripts/GameManager.cs" });
                tasks.Add(parent);

                object draft = Activator.CreateInstance(taskType);
                SetTaskField(taskType, draft, "Id", "draft-verifier");
                SetTaskField(taskType, draft, "ParentTaskId", "game-manager");
                SetTaskField(taskType, draft, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, draft, "IsDecompositionDraft", true);
                SetTaskField(
                    taskType,
                    draft,
                    "ConsumedArtifactPaths",
                    new List<string> { "Assets/BallJumpGame/Scripts/GameManager.cs" });
                tasks.Add(draft);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo complete = windowType.GetMethod(
                    "CompleteAgentTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                complete.Invoke(
                    window,
                    new object[]
                    {
                        "game-manager",
                        "DeveloperAgent",
                        string.Empty,
                        "Granular decomposition contains a dependency cycle.",
                        null,
                        false
                    });

                Assert.AreEqual(1, tasks.Count);
                Assert.AreEqual(
                    "Todo",
                    GetTaskField(taskType, parent, "Status").ToString());
                Assert.IsTrue(
                    (bool)GetTaskField(taskType, parent, "SuppressAutoDecomposition"));
                Assert.AreEqual(
                    0,
                    GetTaskField(taskType, parent, "DecompositionFailureCount"));
                StringAssert.StartsWith(
                    "[AssociationLeaf]",
                    (string)GetTaskField(taskType, parent, "NextAgentMessage"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void RetainedDraftStack_IsEligibleForImmediateCorrectiveContinuation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                SetTaskField(taskType, parent, "DecompositionFailureCount", 1);
                SetTaskField(
                    taskType,
                    parent,
                    "NextAgentMessage",
                    "[DecompositionValidationFailure] repair the retained branch");
                tasks.Add(parent);

                object draft = Activator.CreateInstance(taskType);
                SetTaskField(taskType, draft, "Id", "draft");
                SetTaskField(taskType, draft, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    draft,
                    "CreationSource",
                    "create_kanban_child_task");
                SetTaskField(taskType, draft, "IsDecompositionDraft", true);
                tasks.Add(draft);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo isCandidate = windowType.GetMethod(
                    "IsCorrectiveDecompositionRetryCandidate",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.NotNull(isCandidate);
                Assert.IsTrue((bool)isCandidate.Invoke(window, new[] { parent }));

                SetTaskField(
                    taskType,
                    parent,
                    "NextAgentMessage",
                    "[DecompositionRecovery] resume the retained branch after reload");

                Assert.IsTrue(
                    (bool)isCandidate.Invoke(window, new[] { parent }),
                    "A recovered retained branch must continue without requiring the global auto-run toggle.");

                SetTaskField(
                    taskType,
                    parent,
                    "NextAgentMessage",
                    "[DecompositionPolicyMigration] resume the retained branch under the upgraded policy");

                Assert.IsTrue(
                    (bool)isCandidate.Invoke(window, new[] { parent }),
                    "A policy-migrated retained branch must continue without requiring the global auto-run toggle.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ValidDecomposition_CommitsDraftsBeforeChildrenCanRun()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Make Game");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, parent, "DecompositionFailureCount", 1);
                SetTaskField(
                    taskType,
                    parent,
                    "NextAgentMessage",
                    "[DecompositionValidationFailure] retry guidance");
                tasks.Add(parent);

                object draft = Activator.CreateInstance(taskType);
                SetTaskField(taskType, draft, "Id", "draft");
                SetTaskField(taskType, draft, "Title", "Player controller");
                SetTaskField(taskType, draft, "ParentTaskId", "parent");
                SetTaskField(taskType, draft, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, draft, "IsDecompositionDraft", true);
                SetTaskField(taskType, draft, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(draft);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo complete = windowType.GetMethod(
                    "CompleteAgentTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                complete.Invoke(
                    window,
                    new object[]
                    {
                        "parent",
                        "DeveloperAgent",
                        "Created a valid child batch.",
                        string.Empty,
                        null,
                        false
                    });

                Assert.IsFalse(
                    (bool)taskType.GetField("IsDecompositionDraft").GetValue(draft),
                    "A validated decomposition must publish its child batch atomically.");
                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(parent).ToString(),
                    "The parent must wait while its newly committed child is unfinished.");
                Assert.AreEqual(
                    0,
                    taskType.GetField("DecompositionFailureCount").GetValue(parent));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("NextAgentMessage").GetValue(parent));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void AllRunnerPaths_PreserveEditorCompletionContinuation()
        {
            MethodInfo selector =
                _runnerType.GetMethod(
                    "ShouldAvoidCapturedEditorContinuation",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                selector);

            Assert.IsFalse(
                (bool)selector.Invoke(
                    null,
                    new object[] { false, "Platform_2" }));

            Assert.IsFalse(
                (bool)selector.Invoke(
                    null,
                    new object[] { true, string.Empty }));

            Assert.IsFalse(
                (bool)selector.Invoke(
                    null,
                    new object[] { false, string.Empty }));
        }


        [Test]
        public void ReviewFailure_RemainsReworkableWhileProgressCanContinue()
        {
            Type statusType =
                typeof(AIRuntimeKanbanSession)
                    .GetNestedType(
                        "TaskStatus",
                        BindingFlags.NonPublic);

            Assert.NotNull(
                statusType);

            object review =
                Enum.Parse(
                    statusType,
                    "Review");

            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "GetReviewCompletionStatus",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.NotNull(
                method);

            object completedReview =
                method.Invoke(
                    null,
                    new[]
                    {
                        review,
                        true,
                        string.Empty,
                        0
                    });

            object firstFailedReview =
                method.Invoke(
                    null,
                    new[]
                    {
                        review,
                        false,
                        string.Empty,
                        1
                    });

            object secondFailedReview =
                method.Invoke(
                    null,
                    new[]
                    {
                        review,
                        false,
                        string.Empty,
                        2
                    });

            object crashedReview =
                method.Invoke(
                    null,
                    new[]
                    {
                        review,
                        false,
                        "Provider crashed.",
                        0
                    });

            object staleCrashWithPassingReview =
                method.Invoke(
                    null,
                    new[]
                    {
                        Enum.Parse(statusType, "Crashed"),
                        true,
                        string.Empty,
                        0
                    });

            Assert.AreEqual(
                "Done",
                completedReview.ToString(),
                "Only a successful review belongs in Done.");

            Assert.AreEqual(
                "Todo",
                firstFailedReview.ToString(),
                "A completed FAIL verdict remains eligible for corrective work.");

            Assert.AreEqual(
                "Todo",
                secondFailedReview.ToString(),
                "Attempt count alone must not terminate a productive review loop.");

            Assert.AreEqual(
                "Crashed",
                crashedReview.ToString());

            Assert.AreEqual(
                "Done",
                staleCrashWithPassingReview.ToString(),
                "An authoritative active PASS must not preserve a stale Crashed status.");
        }


        [Test]
        public void RepeatedReviewFailures_IncrementPastFirstRework()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "GetReviewFailureCountAfterCompletion",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.NotNull(method);

            int firstFailure =
                (int)method.Invoke(
                    null,
                    new object[] { 0, false, false });

            int secondFailure =
                (int)method.Invoke(
                    null,
                    new object[] { firstFailure, false, false });

            Assert.AreEqual(
                1,
                firstFailure,
                "Failure count remains useful diagnostic history.");

            Assert.AreEqual(
                2,
                secondFailure,
                "Failure count is informational and does not impose a review ceiling.");

            Assert.AreEqual(
                2,
                method.Invoke(
                    null,
                    new object[] { secondFailure, true, false }),
                "Passing review must not add a failure.");

            Assert.AreEqual(
                2,
                method.Invoke(
                    null,
                    new object[] { secondFailure, false, true }),
                "An execution crash is tracked separately from completed failed reviews.");
        }


        [Test]
        public void IdenticalReviewOutcome_PausesOnlyAfterNoProgressIsDemonstrated()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "reviewed-task");
                SetTaskField(taskType, task, "Title", "Reviewed task");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));
                SetTaskField(taskType, task, "AgentResult", "Implementation state A");

                MethodInfo completeReview = windowType.GetMethod(
                    "CompleteReviewTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                object[] completionArguments =
                {
                    task,
                    "ReviewerAgent",
                    "KANBAN_REVIEW: FAIL\n\nThe same defect remains.",
                    string.Empty,
                    "{\"State\":\"A\"}"
                };

                completeReview.Invoke(window, completionArguments);

                Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual("Rework Requested", taskType.GetField("ReviewStatus").GetValue(task));

                SetTaskField(
                    taskType,
                    task,
                    "AgentResult",
                    "Implementation state B narrative, but authoritative evidence is unchanged");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));
                completeReview.Invoke(window, completionArguments);

                Assert.AreEqual("Failed", taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual("Blocked by No Progress", taskType.GetField("ReviewStatus").GetValue(task));
                StringAssert.StartsWith(
                    "[ReviewNoProgress]",
                    (string)taskType.GetField("NextAgentMessage").GetValue(task));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void FailedAndCrashedColumns_UseDistinctLabels()
        {
            Type statusType =
                typeof(AIRuntimeKanbanSession)
                    .GetNestedType(
                        "TaskStatus",
                        BindingFlags.NonPublic);

            object crashed =
                Enum.Parse(
                    statusType,
                    "Crashed");

            object failed =
                Enum.Parse(
                    statusType,
                    "Failed");

            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "GetStatusLabel",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.AreEqual(
                "Crashed",
                method.Invoke(
                    null,
                    new[] { crashed }));

            Assert.AreEqual(
                "Failed",
                method.Invoke(
                    null,
                    new[] { failed }));
        }


        [Test]
        public void LoadedRecursiveHierarchy_BlocksEveryIncompleteAncestorInTodo()
        {
            Type windowType =
                typeof(AIRuntimeKanbanSession);

            Type taskType =
                windowType.GetNestedType(
                    "KanbanTask",
                    BindingFlags.NonPublic);

            Type statusType =
                windowType.GetNestedType(
                    "TaskStatus",
                    BindingFlags.NonPublic);

            object parent = Activator.CreateInstance(taskType);
            object child = Activator.CreateInstance(taskType);
            object grandchild = Activator.CreateInstance(taskType);

            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, child, "Id", "child");
            SetTaskField(taskType, child, "ParentTaskId", "parent");
            SetTaskField(taskType, grandchild, "Id", "grandchild");
            SetTaskField(taskType, grandchild, "ParentTaskId", "child");

            object inProgress = Enum.Parse(statusType, "InProgress");
            object done = Enum.Parse(statusType, "Done");
            object todo = Enum.Parse(statusType, "Todo");

            SetTaskField(taskType, parent, "Status", inProgress);
            SetTaskField(taskType, child, "Status", done);
            SetTaskField(taskType, child, "ReviewStatus", "Passed");
            SetTaskField(taskType, grandchild, "Status", todo);

            Type listType =
                typeof(List<>).MakeGenericType(taskType);

            IList tasks =
                (IList)Activator.CreateInstance(listType);

            tasks.Add(parent);
            tasks.Add(child);
            tasks.Add(grandchild);

            MethodInfo normalize =
                windowType.GetMethod(
                    "NormalizeLoadedTaskHierarchy",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            normalize.Invoke(
                null,
                new object[] { tasks });

            Assert.AreEqual(
                "Todo",
                taskType.GetField("Status").GetValue(child).ToString());

            Assert.AreEqual(
                "Todo",
                taskType.GetField("Status").GetValue(parent).ToString());
        }


        [Test]
        public void LoadedTerminalParent_RecoversProposalForAuthoritativePlanningSnapshot()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object parent = Activator.CreateInstance(taskType);
            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, parent, "AgentError", "A legacy untyped decomposition validation error.");
            SetTaskField(taskType, parent, "DecompositionFailureCount", 1);
            tasks.Add(parent);

            object draft = Activator.CreateInstance(taskType);
            SetTaskField(taskType, draft, "Id", "draft");
            SetTaskField(taskType, draft, "ParentTaskId", "parent");
            SetTaskField(taskType, draft, "CreationSource", "create_kanban_child_task");
            SetTaskField(taskType, draft, "IsDecompositionDraft", true);
            tasks.Add(draft);

            MethodInfo recover = windowType.GetMethod(
                "RecoverLegacyTerminalDecompositionDrafts",
                BindingFlags.NonPublic | BindingFlags.Static);

            Assert.AreEqual(1, recover.Invoke(null, new object[] { tasks }));
            Assert.AreEqual(2, tasks.Count);
            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(parent).ToString());
            Assert.AreEqual(string.Empty, taskType.GetField("AgentError").GetValue(parent));
            Assert.AreEqual(1, taskType.GetField("DecompositionFailureCount").GetValue(parent));
            Assert.IsTrue((bool)taskType.GetField("IsDecompositionDraft").GetValue(draft));
            StringAssert.StartsWith(
                "[PlanningValidationFailure]",
                (string)taskType.GetField("NextAgentMessage").GetValue(parent));
            StringAssert.Contains(
                "omitted proposal cards are retired",
                (string)taskType.GetField("NextAgentMessage").GetValue(parent));
        }


        [Test]
        public void LoadedEvidenceOnlyFailure_ReopensReviewWithoutRepeatingMutation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Id", "folders");
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Failed"));
            SetTaskField(taskType, task, "ReviewStatus", "Failed");
            SetTaskField(taskType, task, "ReviewFailureKind", "ImplementationDefect");
            SetTaskField(taskType, task, "ReviewFailureCount", 2);
            SetTaskField(
                taskType,
                task,
                "ReviewResult",
                "KANBAN_REVIEW: FAIL\nMissing or incorrect work:\nThe authoritative directory evidence is missing and the workspace cannot be independently verified.");
            tasks.Add(task);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Review", taskType.GetField("Status").GetValue(task).ToString());
            Assert.AreEqual("Pending", taskType.GetField("ReviewStatus").GetValue(task));
            Assert.AreEqual("EvidenceGap", taskType.GetField("ReviewFailureKind").GetValue(task));
            Assert.AreEqual(0, taskType.GetField("ReviewFailureCount").GetValue(task));
        }


        [Test]
        public void LoadedTerminalEvidenceGap_ReopensCompletedImplementationForReview()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Id", "script");
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Failed"));
            SetTaskField(taskType, task, "ReviewStatus", "Failed");
            SetTaskField(taskType, task, "ReviewFailureKind", "EvidenceGap");
            SetTaskField(taskType, task, "ReviewFailureCount", 2);
            SetTaskField(taskType, task, "AgentResult", "The owned script was created.");
            SetTaskField(taskType, task, "ReviewResult", "KANBAN_REVIEW: FAIL\nUnrelated compiler evidence blocked review.");
            tasks.Add(task);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Review", taskType.GetField("Status").GetValue(task).ToString());
            Assert.AreEqual("Pending", taskType.GetField("ReviewStatus").GetValue(task));
            Assert.AreEqual(0, taskType.GetField("ReviewFailureCount").GetValue(task));
            Assert.AreEqual("The owned script was created.", taskType.GetField("AgentResult").GetValue(task));
        }


        [Test]
        public void LoadedMissingArtifactEvidenceGap_RequeuesOwningImplementation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Id", "missing-script");
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));
            SetTaskField(taskType, task, "ReviewStatus", "Waiting for Evidence");
            SetTaskField(taskType, task, "ReviewFailureKind", "EvidenceGap");
            SetTaskField(taskType, task, "ReviewFailureCount", 0);
            SetTaskField(taskType, task, "AgentResult", "[TaskCompleted]");
            SetTaskField(
                taskType,
                task,
                "ReviewResult",
                "KANBAN_REVIEW: FAIL\nAssets/Game/Scripts/Game.cs does not exist.");
            SetTaskField(
                taskType,
                task,
                "ReviewEvidence",
                "{\"FileContents\":{\"Assets/Game/Scripts/Game.cs\":{\"FailureKind\":\"MissingArtifact\"}}}");
            tasks.Add(task);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            Assert.GreaterOrEqual(
                (int)normalize.Invoke(null, new object[] { tasks }),
                1);
            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(task).ToString());
            Assert.AreEqual("Rework Requested", taskType.GetField("ReviewStatus").GetValue(task));
            Assert.AreEqual("ImplementationDefect", taskType.GetField("ReviewFailureKind").GetValue(task));
            Assert.AreEqual(1, taskType.GetField("ReviewFailureCount").GetValue(task));
            StringAssert.StartsWith(
                "[ReviewFailureKind: ImplementationDefect]",
                (string)taskType.GetField("NextAgentMessage").GetValue(task));
        }


        [Test]
        public void LoadedComponentNamespaceMismatch_RequeuesFailedImplementation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Id", "main-scene");
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Failed"));
            SetTaskField(taskType, task, "ReviewStatus", "Rework Blocked");
            SetTaskField(taskType, task, "ReviewFailureKind", "ImplementationDefect");
            SetTaskField(taskType, task, "CapabilityRecoveryPolicyVersion", 7);
            SetTaskField(
                taskType,
                task,
                "AgentError",
                "Capability 'add_unity_package' was denied because permission 'Network' is not allowed.");
            SetTaskField(
                taskType,
                task,
                "ExecutionMemory",
                "ToolFailed: Component type 'UnityEngine.UI.Canvas' was not found, and no matching 'Canvas.cs' script asset exists under Assets.");
            tasks.Add(task);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            Assert.GreaterOrEqual(
                (int)normalize.Invoke(null, new object[] { tasks }),
                1);
            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(task).ToString());
            Assert.AreEqual(string.Empty, taskType.GetField("AgentError").GetValue(task));
            Assert.AreEqual(
                8,
                taskType.GetField("CapabilityRecoveryPolicyVersion").GetValue(task));
            StringAssert.StartsWith(
                "[ComponentTypeRecovery]",
                (string)taskType.GetField("NextAgentMessage").GetValue(task));
        }


        [Test]
        public void LegacySemanticMigration_IsRemovedWithoutDeletingPassedSiblings()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object parent = Activator.CreateInstance(taskType);
            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
            SetTaskField(
                taskType,
                parent,
                "Description",
                "Use separate cards for each scene-construction/configuration step.");
            tasks.Add(parent);

            object passedScript = Activator.CreateInstance(taskType);
            SetTaskField(taskType, passedScript, "Id", "script");
            SetTaskField(taskType, passedScript, "ParentTaskId", "parent");
            SetTaskField(taskType, passedScript, "Status", Enum.Parse(statusType, "Done"));
            SetTaskField(taskType, passedScript, "ReviewStatus", "Passed");
            tasks.Add(passedScript);

            object bundledScene = Activator.CreateInstance(taskType);
            SetTaskField(taskType, bundledScene, "Id", "scene");
            SetTaskField(taskType, bundledScene, "ParentTaskId", "parent");
            SetTaskField(taskType, bundledScene, "Title", "Build complete scene");
            SetTaskField(taskType, bundledScene, "Status", Enum.Parse(statusType, "Todo"));
            const string originalDescription =
                "Create Assets/Game/Scenes/Game.unity. Create the player GameObject. Create the goal GameObject. Configure the Camera. Save the scene.";
            SetTaskField(
                taskType,
                bundledScene,
                "Description",
                "[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" +
                originalDescription);
            SetTaskField(
                taskType,
                bundledScene,
                "AcceptanceCriteria",
                "The scene is complete.\n- Every generated child card must reach Done with a successful review before this coordination card completes.");
            tasks.Add(bundledScene);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual(3, tasks.Count, "Passed sibling history must be preserved.");
            Assert.AreEqual("Done", taskType.GetField("Status").GetValue(passedScript).ToString());
            Assert.AreEqual(
                originalDescription,
                taskType.GetField("Description").GetValue(bundledScene));
            StringAssert.DoesNotContain(
                "Every generated child card",
                (string)taskType.GetField("AcceptanceCriteria").GetValue(bundledScene));
        }


        [Test]
        public void LoadedTransactionalSceneCard_RemainsOneExecutableLeaf()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object parent = Activator.CreateInstance(taskType);
            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
            SetTaskField(
                taskType,
                parent,
                "Description",
                "Use separate cards for each scene-construction/configuration step.");
            tasks.Add(parent);

            const string description =
                "Create Assets/Game/Scenes/Game.unity. Create the player. Create the goal. Configure the camera. Save the scene.";

            object scene = Activator.CreateInstance(taskType);
            SetTaskField(taskType, scene, "Id", "scene");
            SetTaskField(taskType, scene, "ParentTaskId", "parent");
            SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Todo"));
            SetTaskField(taskType, scene, "Description", description);
            SetTaskField(
                taskType,
                scene,
                "ProducedArtifactPaths",
                new List<string> { "Assets/Game/Scenes/Game.unity" });
            SetTaskField(
                taskType,
                scene,
                "MutatedResourceKeys",
                new List<string> { "scene:Assets/Game/Scenes/Game.unity" });
            tasks.Add(scene);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual(description, taskType.GetField("Description").GetValue(scene));
        }


        [Test]
        public void LoadedFailureFromSupersededSceneSentencePolicy_RequeuesDecomposition()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object parent = Activator.CreateInstance(taskType);
            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Failed"));
            SetTaskField(taskType, parent, "ReviewStatus", "Failed");
            SetTaskField(taskType, parent, "DecompositionFailureCount", 2);
            SetTaskField(
                taskType,
                parent,
                "AgentError",
                "Granular decomposition combines multiple semantic scene-construction/configuration steps in 'Construct scene'.");
            tasks.Add(parent);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(parent).ToString());
            Assert.AreEqual(string.Empty, taskType.GetField("AgentError").GetValue(parent));
            Assert.AreEqual(0, taskType.GetField("DecompositionFailureCount").GetValue(parent));
            int currentPolicyVersion =
                (int)windowType.GetField(
                        "CurrentDecompositionPolicyVersion",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .GetRawConstantValue();
            Assert.AreEqual(
                currentPolicyVersion,
                taskType.GetField("DecompositionPolicyVersion").GetValue(parent));
            Assert.AreEqual(
                string.Empty,
                (string)taskType.GetField("NextAgentMessage").GetValue(parent),
                "A legacy failure without retained drafts may now execute directly.");
        }


        [Test]
        public void LoadedPausedPlanningFromSupersededPolicy_RequeuesAuthoritativeSnapshot()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object parent = Activator.CreateInstance(taskType);
            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
            SetTaskField(taskType, parent, "ReviewStatus", "Decomposition Blocked");
            SetTaskField(taskType, parent, "DecompositionFailureCount", 2);
            SetTaskField(taskType, parent, "DecompositionPolicyVersion", 9);
            SetTaskField(
                taskType,
                parent,
                "BlockedReason",
                "Granular decomposition validation failed: a semantic verification join was missing.");
            tasks.Add(parent);

            object draft = Activator.CreateInstance(taskType);
            SetTaskField(taskType, draft, "Id", "draft");
            SetTaskField(taskType, draft, "ParentTaskId", "parent");
            SetTaskField(taskType, draft, "IsDecompositionDraft", true);
            tasks.Add(draft);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(parent).ToString());
            Assert.AreEqual(1, taskType.GetField("DecompositionFailureCount").GetValue(parent));
            Assert.AreEqual(string.Empty, taskType.GetField("BlockedReason").GetValue(parent));
            StringAssert.StartsWith(
                "[PlanningValidationFailure]",
                (string)taskType.GetField("NextAgentMessage").GetValue(parent));
            Assert.IsTrue((bool)taskType.GetField("IsDecompositionDraft").GetValue(draft));
        }


        [Test]
        public void LoadedCapabilityRoundCrash_RequeuesUnderUnlimitedDefaultOnce()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object scene = Activator.CreateInstance(taskType);
            SetTaskField(taskType, scene, "Id", "scene");
            SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, scene, "ReviewStatus", "Crashed");
            SetTaskField(
                taskType,
                scene,
                "AgentError",
                "Conversation exceeded the maximum of 32 capability rounds.");
            tasks.Add(scene);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(scene).ToString());
            Assert.AreEqual(string.Empty, taskType.GetField("AgentError").GetValue(scene));
            int currentBudgetPolicyVersion =
                (int)windowType.GetField(
                        "CurrentCapabilityBudgetPolicyVersion",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .GetRawConstantValue();
            Assert.AreEqual(
                currentBudgetPolicyVersion,
                taskType.GetField("CapabilityBudgetPolicyVersion").GetValue(scene));

            // The version stamp prevents an unchanged current-policy crash
            // from being reopened indefinitely on every load.
            SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                scene,
                "AgentError",
                "Conversation exceeded the maximum of 32 capability rounds.");
            normalize.Invoke(null, new object[] { tasks });
            Assert.AreEqual("Crashed", taskType.GetField("Status").GetValue(scene).ToString());
        }


        [Test]
        public void LoadedDirectoryArgumentCrash_RequeuesOnceUnderRecoveryPolicy()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object script = Activator.CreateInstance(taskType);
            SetTaskField(taskType, script, "Id", "camera-follow");
            SetTaskField(taskType, script, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, script, "ReviewStatus", "Crashed");
            SetTaskField(
                taskType,
                script,
                "AgentError",
                "DirectoryName must be one directory name without path separators, leading or trailing whitespace, or a trailing period.");
            tasks.Add(script);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(script).ToString());
            Assert.AreEqual(string.Empty, taskType.GetField("AgentError").GetValue(script));
            int currentRecoveryPolicyVersion =
                (int)windowType.GetField(
                        "CurrentCapabilityRecoveryPolicyVersion",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .GetRawConstantValue();
            Assert.AreEqual(
                currentRecoveryPolicyVersion,
                taskType.GetField("CapabilityRecoveryPolicyVersion").GetValue(script));

            SetTaskField(taskType, script, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                script,
                "AgentError",
                "DirectoryName must be one directory name without path separators, leading or trailing whitespace, or a trailing period.");
            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Crashed", taskType.GetField("Status").GetValue(script).ToString());
        }


        [Test]
        public void LoadedMalformedProviderBatchCrash_RequeuesOnceWithMemoryPreserved()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            object script = Activator.CreateInstance(taskType);
            SetTaskField(taskType, script, "Id", "provider-batch");
            SetTaskField(taskType, script, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, script, "ReviewStatus", "Crashed");
            SetTaskField(taskType, script, "ExecutionMemory", "durable tool journal");
            SetTaskField(
                taskType,
                script,
                "AgentError",
                "The provider repeated the same malformed tool call after AIRuntime requested a schema correction. The rejected call was not executed.");
            tasks.Add(script);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(script).ToString());
            Assert.AreEqual(string.Empty, taskType.GetField("AgentError").GetValue(script));
            Assert.AreEqual("durable tool journal", taskType.GetField("ExecutionMemory").GetValue(script));
            StringAssert.StartsWith(
                "[ModelProtocolRecovery]",
                (string)taskType.GetField("NextAgentMessage").GetValue(script));

            int currentRecoveryPolicyVersion =
                (int)windowType.GetField(
                        "CurrentCapabilityRecoveryPolicyVersion",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .GetRawConstantValue();
            Assert.AreEqual(
                currentRecoveryPolicyVersion,
                taskType.GetField("CapabilityRecoveryPolicyVersion").GetValue(script));

            SetTaskField(taskType, script, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                script,
                "AgentError",
                "The provider returned another malformed tool call after 1 schema-correction attempt. The rejected call was not executed.");
            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual("Crashed", taskType.GetField("Status").GetValue(script).ToString());
        }


        [Test]
        public void LoadedLegacyInactivityLoop_RequeuesOnceWithFailureJournalPreserved()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            JObject memory =
                new JObject
                {
                    ["Entries"] =
                        new JArray(
                            Enumerable.Range(0, 3).Select(
                                index =>
                                    new JObject
                                    {
                                        ["Event"] = "ToolFailed",
                                        ["Tool"] = "set_serialized_property",
                                        ["Arguments"] =
                                            "{\"propertyPath\":\"m_UiScaleMode\",\"valueJson\":\"{\\\"index\\\":" +
                                            index +
                                            "}\"}",
                                        ["Result"] =
                                            "Enum value '' is invalid for 'm_UiScaleMode'."
                                    }))
                };

            object scene = Activator.CreateInstance(taskType);
            SetTaskField(taskType, scene, "Id", "legacy-enum-loop");
            SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                scene,
                "AgentError",
                "[ImplementationInactivityRecovery] AIRuntime stopped this run after five minutes without execution-memory progress.");
            SetTaskField(taskType, scene, "ExecutionMemory", memory.ToString());
            tasks.Add(scene);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual(
                "Todo",
                taskType.GetField("Status").GetValue(scene).ToString());
            Assert.AreEqual(
                string.Empty,
                taskType.GetField("AgentError").GetValue(scene));
            Assert.AreEqual(
                memory.ToString(),
                taskType.GetField("ExecutionMemory").GetValue(scene));
            StringAssert.StartsWith(
                "[CapabilityNoProgressRecovery]",
                (string)taskType.GetField("NextAgentMessage").GetValue(scene));
            StringAssert.Contains(
                "set_serialized_property",
                (string)taskType.GetField("NextAgentMessage").GetValue(scene));
            StringAssert.Contains(
                "m_UiScaleMode",
                (string)taskType.GetField("NextAgentMessage").GetValue(scene));

            int currentRecoveryPolicyVersion =
                (int)windowType.GetField(
                        "CurrentCapabilityRecoveryPolicyVersion",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .GetRawConstantValue();
            Assert.AreEqual(
                currentRecoveryPolicyVersion,
                taskType.GetField("CapabilityRecoveryPolicyVersion").GetValue(scene));

            SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                scene,
                "AgentError",
                "[ImplementationInactivityRecovery] AIRuntime stopped this run after five minutes without execution-memory progress.");
            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual(
                "Crashed",
                taskType.GetField("Status").GetValue(scene).ToString());
        }


        [Test]
        public void LoadBoard_PersistsRecoveryMigrationWhenNormalAutoSaveIsDisabled()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            string boardPath = Path.Combine(_testBoardDirectory, "board.json");
            JObject memory =
                new JObject
                {
                    ["Entries"] =
                        new JArray(
                            Enumerable.Range(0, 3).Select(
                                _ =>
                                    new JObject
                                    {
                                        ["Event"] = "ToolFailed",
                                        ["Tool"] = "set_serialized_property",
                                        ["Result"] = "Same enum conversion failure."
                                    }))
                };

            object board = Activator.CreateInstance(boardType);
            IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
            object scene = Activator.CreateInstance(taskType);
            SetTaskField(taskType, scene, "Id", "durable-migration");
            SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                scene,
                "AgentError",
                "[ImplementationInactivityRecovery] legacy inactivity stop");
            SetTaskField(taskType, scene, "ExecutionMemory", memory.ToString());
            tasks.Add(scene);
            File.WriteAllText(boardPath, JsonUtility.ToJson(board, prettyPrint: true));

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                windowType.GetField(
                        "_boardFilePath",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, boardPath);

                windowType.GetMethod(
                        "LoadBoard",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, new object[] { false });

                JObject persisted = JObject.Parse(File.ReadAllText(boardPath));
                JToken persistedTask = persisted["Tasks"]?[0];
                Assert.NotNull(persistedTask);
                Assert.AreEqual(
                    (int)Enum.Parse(statusType, "Todo"),
                    persistedTask["Status"]?.Value<int>());
                Assert.AreEqual(
                    string.Empty,
                    persistedTask["AgentError"]?.Value<string>());
                Assert.AreEqual(
                    memory.ToString(),
                    persistedTask["ExecutionMemory"]?.Value<string>());
                Assert.AreEqual(
                    (int)windowType.GetField("CurrentCapabilityRecoveryPolicyVersion", BindingFlags.NonPublic | BindingFlags.Static).GetRawConstantValue(),
                    persistedTask["CapabilityRecoveryPolicyVersion"]?.Value<int>());
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void LoadedStandaloneScriptInspectionCrash_RequeuesOnceUnderRecoveryPolicy()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            const string description =
                "Consume Assets/BallJumpGame/Scripts/PlayerBall.cs. " +
                "Inspect the script source to confirm Rigidbody constraints are enforced in Awake. " +
                "Do not modify the script; verification only.";

            const string inspectionError =
                "The model did not complete the required inspect_editor_component tool call before answering the component troubleshooting request.";

            object verification = Activator.CreateInstance(taskType);
            SetTaskField(taskType, verification, "Id", "player-ball-verification");
            SetTaskField(taskType, verification, "Title", "Verify PlayerBall integration criteria");
            SetTaskField(taskType, verification, "Description", description);
            SetTaskField(taskType, verification, "AcceptanceCriteria", "Confirm the script compiles and implements the required behavior.");
            SetTaskField(taskType, verification, "RequestTitle", "Verify PlayerBall integration criteria");
            SetTaskField(taskType, verification, "RequestDescription", description);
            SetTaskField(taskType, verification, "RequestAcceptanceCriteria", "Confirm the script compiles and implements the required behavior.");
            SetTaskField(taskType, verification, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, verification, "ReviewStatus", "Crashed");
            SetTaskField(taskType, verification, "ReviewResult", inspectionError);
            SetTaskField(taskType, verification, "AgentError", inspectionError);
            SetTaskField(taskType, verification, "ExecutionMemory", "stale successful read_file evidence");
            tasks.Add(verification);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic | BindingFlags.Static);

            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual(
                "Todo",
                taskType.GetField("Status").GetValue(verification).ToString());
            Assert.AreEqual(
                string.Empty,
                taskType.GetField("AgentError").GetValue(verification));
            Assert.AreEqual(
                string.Empty,
                taskType.GetField("ReviewStatus").GetValue(verification));
            Assert.AreEqual(
                string.Empty,
                taskType.GetField("ExecutionMemory").GetValue(verification));

            int currentRecoveryPolicyVersion =
                (int)windowType.GetField(
                        "CurrentCapabilityRecoveryPolicyVersion",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .GetRawConstantValue();

            Assert.AreEqual(
                currentRecoveryPolicyVersion,
                taskType.GetField("CapabilityRecoveryPolicyVersion").GetValue(verification));

            SetTaskField(taskType, verification, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, verification, "AgentError", inspectionError);
            normalize.Invoke(null, new object[] { tasks });

            Assert.AreEqual(
                "Crashed",
                taskType.GetField("Status").GetValue(verification).ToString());
        }


        [Test]
        public void ChildTaskCapability_UsesScopedWriteToolContract()
        {
            CreateKanbanChildTaskCapability capability =
                new CreateKanbanChildTaskCapability();

            Assert.AreEqual(
                "create_kanban_child_task",
                capability.Name);

            Assert.AreEqual(
                Runtime.Capabilities.Permissions.CapabilityPermission.Write,
                capability.Permission);

            StringAssert.Contains(
                "currently executing Kanban task",
                capability.Description);
        }


        [Test]
        public void ExplicitSiblingDependency_BlocksUntilNamedTaskIsDone()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);

                object scripts = Activator.CreateInstance(taskType);
                SetTaskField(taskType, scripts, "Id", "scripts");
                SetTaskField(taskType, scripts, "ParentTaskId", "root");
                SetTaskField(taskType, scripts, "Title", "Create JumpingBallGame scripts and asset folder");
                SetTaskField(taskType, scripts, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(scripts);

                object scene = Activator.CreateInstance(taskType);
                SetTaskField(taskType, scene, "Id", "scene");
                SetTaskField(taskType, scene, "ParentTaskId", "root");
                SetTaskField(taskType, scene, "Title", "Build playable level");
                SetTaskField(
                    taskType,
                    scene,
                    "Description",
                    "Depends on the sibling task \"Create JumpingBallGame scripts and asset folder\" being Done.");
                SetTaskField(
                    taskType,
                    scene,
                    "DeclaredDependencyTitles",
                    new List<string> { "Create JumpingBallGame scripts and asset folder" });
                SetTaskField(taskType, scene, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(scene);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);
                windowType.GetMethod(
                        "ReconcileAllTaskRelationships",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, null);

                MethodInfo isBlocked = windowType.GetMethod(
                    "HasUnmetDeclaredDependency",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsTrue((bool)isBlocked.Invoke(window, new[] { scene }));

                SetTaskField(taskType, scripts, "Status", Enum.Parse(statusType, "Review"));

                Assert.IsTrue(
                    (bool)isBlocked.Invoke(window, new[] { scene }),
                    "A dependency still awaiting review must not release downstream implementation.");

                SetTaskField(taskType, scripts, "Status", Enum.Parse(statusType, "Done"));

                Assert.IsFalse((bool)isBlocked.Invoke(window, new[] { scene }));
            }
            finally
            {
                ScriptableObject.DestroyImmediate(window);
            }
        }


        [Test]
        public void MultipleSiblingDependencies_BlockUntilEveryNamedTaskIsDone()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);

                string[] prerequisiteTitles =
                {
                    "Create BallMovement script",
                    "Create FallReset script",
                    "Create GoalTrigger script",
                    "Create CameraFollow script"
                };

                var prerequisites =
                    new List<object>();

                for (int index = 0; index < prerequisiteTitles.Length; index++)
                {
                    object prerequisite = Activator.CreateInstance(taskType);
                    SetTaskField(taskType, prerequisite, "Id", $"script-{index}");
                    SetTaskField(taskType, prerequisite, "ParentTaskId", "root");
                    SetTaskField(taskType, prerequisite, "Title", prerequisiteTitles[index]);
                    SetTaskField(taskType, prerequisite, "Status", Enum.Parse(statusType, "Done"));
                    tasks.Add(prerequisite);
                    prerequisites.Add(prerequisite);
                }

                SetTaskField(
                    taskType,
                    prerequisites[2],
                    "Status",
                    Enum.Parse(statusType, "Todo"));

                object compilation = Activator.CreateInstance(taskType);
                SetTaskField(taskType, compilation, "Id", "compile");
                SetTaskField(taskType, compilation, "ParentTaskId", "root");
                SetTaskField(taskType, compilation, "Title", "Compile the Unity project");
                SetTaskField(
                    taskType,
                    compilation,
                    "Description",
                    "Compile the Unity project and ensure zero errors. Depends on sibling tasks \"Create BallMovement script\", \"Create FallReset script\", \"Create GoalTrigger script\", and \"Create CameraFollow script\" being Done.");
                SetTaskField(
                    taskType,
                    compilation,
                    "DeclaredDependencyTitles",
                    prerequisiteTitles.ToList());
                SetTaskField(taskType, compilation, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(compilation);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);
                windowType.GetMethod(
                        "ReconcileAllTaskRelationships",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, null);

                MethodInfo isBlocked = windowType.GetMethod(
                    "HasUnmetDeclaredDependency",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsTrue((bool)isBlocked.Invoke(window, new[] { compilation }));

                SetTaskField(
                    taskType,
                    prerequisites[2],
                    "Status",
                    Enum.Parse(statusType, "Done"));

                Assert.IsFalse((bool)isBlocked.Invoke(window, new[] { compilation }));
            }
            finally
            {
                ScriptableObject.DestroyImmediate(window);
            }
        }


        [Test]
        public void DependencyCycle_IsReportedAsDeadlockInsteadOfSilentStall()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);

                object save = Activator.CreateInstance(taskType);
                SetTaskField(taskType, save, "Id", "save");
                SetTaskField(taskType, save, "ParentTaskId", "parent");
                SetTaskField(taskType, save, "Title", "Save scene");
                SetTaskField(taskType, save, "Description",
                    "Depends on sibling task \"Build player\" being Done.");
                SetTaskField(taskType, save, "DeclaredDependencyTitles", new List<string> { "Build player" });
                SetTaskField(taskType, save, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(save);

                object compile = Activator.CreateInstance(taskType);
                SetTaskField(taskType, compile, "Id", "compile");
                SetTaskField(taskType, compile, "ParentTaskId", "parent");
                SetTaskField(taskType, compile, "Title", "Compile");
                SetTaskField(taskType, compile, "Description",
                    "Depends on sibling task \"Save scene\" being Done.");
                SetTaskField(taskType, compile, "DeclaredDependencyTitles", new List<string> { "Save scene" });
                SetTaskField(taskType, compile, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(compile);

                object player = Activator.CreateInstance(taskType);
                SetTaskField(taskType, player, "Id", "player");
                SetTaskField(taskType, player, "ParentTaskId", "parent");
                SetTaskField(taskType, player, "Title", "Build player");
                SetTaskField(taskType, player, "Description",
                    "Depends on sibling task \"Compile\" being Done.");
                SetTaskField(taskType, player, "DeclaredDependencyTitles", new List<string> { "Compile" });
                SetTaskField(taskType, player, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(player);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);
                windowType.GetMethod(
                        "ReconcileAllTaskRelationships",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, null);

                MethodInfo findDeadlock = windowType.GetMethod(
                    "FindDependencyDeadlock",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                string deadlock = (string)findDeadlock.Invoke(window, null);

                StringAssert.Contains("Dependency deadlock", deadlock);
                StringAssert.Contains("Save scene", deadlock);
                StringAssert.Contains("Build player", deadlock);
                StringAssert.Contains("Compile", deadlock);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ScriptAuthoringTask_RequiresUnityAssetImportBeforeDependentsRun()
        {
            Type windowType =
                typeof(AIRuntimeKanbanSession);

            Type taskType =
                windowType.GetNestedType(
                    "KanbanTask",
                    BindingFlags.NonPublic);

            object scriptTask =
                Activator.CreateInstance(taskType);

            SetTaskField(
                taskType,
                scriptTask,
                "Title",
                "Write BallMovement script");

            SetTaskField(
                taskType,
                scriptTask,
                "Description",
                "Create a C# script at Assets/JumpingBallGame/Scripts/BallMovement.cs.");

            MethodInfo requiresImport =
                windowType.GetMethod(
                    "TaskMayHaveWrittenUnityScript",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.IsTrue(
                (bool)requiresImport.Invoke(
                    null,
                    new[] { scriptTask }));

            object splitPathScriptTask =
                Activator.CreateInstance(taskType);

            SetTaskField(
                taskType,
                splitPathScriptTask,
                "Title",
                "Create NavigationBrain script");

            SetTaskField(
                taskType,
                splitPathScriptTask,
                "Description",
                "Create the NavigationBrain C# script under Assets/ExampleProject/Scripts.");

            SetTaskField(
                taskType,
                splitPathScriptTask,
                "AcceptanceCriteria",
                "NavigationBrain.cs exists under Assets/ExampleProject/Scripts and compiles.");

            Assert.IsTrue(
                (bool)requiresImport.Invoke(
                    null,
                    new[] { splitPathScriptTask }));

            object readOnlyTask =
                Activator.CreateInstance(taskType);

            SetTaskField(
                taskType,
                readOnlyTask,
                "Title",
                "Review BallMovement script");

            SetTaskField(
                taskType,
                readOnlyTask,
                "Description",
                "Read Assets/JumpingBallGame/Scripts/BallMovement.cs and report its behavior.");

            Assert.IsFalse(
                (bool)requiresImport.Invoke(
                    null,
                    new[] { readOnlyTask }));
        }


        [Test]
        public void CompletedChildArtifactAudit_DetectsMissingClaimedFile()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);

            object missingTask = Activator.CreateInstance(taskType);
            SetTaskField(
                taskType,
                missingTask,
                "Description",
                "Write Assets/__AIRuntimeDefinitelyMissing__/BallController.cs.");

            MethodInfo audit = windowType.GetMethod(
                "HasMissingClaimedAssetDeliverable",
                BindingFlags.NonPublic | BindingFlags.Static);

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

            Assert.IsTrue(
                (bool)audit.Invoke(
                    null,
                    new[] { missingTask, workspaceRoot }));

            object existingTask = Activator.CreateInstance(taskType);
            SetTaskField(
                taskType,
                existingTask,
                "Description",
                "Implement " + TestAssetPaths.Resolve("Assets/Editor/AIRuntimeKanbanAgentRunner.cs") + ".");

            Assert.IsFalse(
                (bool)audit.Invoke(
                    null,
                    new[] { existingTask, workspaceRoot }));
        }


        [Test]
        public void AgentSpawnedChild_BlocksParentUntilChildIsDone()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType(
                "TaskStatus",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Parent");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                SetTaskField(
                    taskType,
                    parent,
                    "Status",
                    Enum.Parse(statusType, "InProgress"));
                tasks.Add(parent);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                object result = createChild.Invoke(
                    window,
                    new object[]
                    {
                        "parent",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = "Child",
                            Description = "Complete the child work",
                            AcceptanceCriteria = "Child reaches Done"
                        }
                    });

                Assert.NotNull(result);
                Assert.AreEqual(2, tasks.Count);
                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(parent).ToString());

                object child = tasks[1];
                Assert.AreEqual(
                    "parent",
                    taskType.GetField("ParentTaskId").GetValue(child));
                Assert.AreEqual(
                    "DeveloperAgent",
                    taskType.GetField("AssignedAgent").GetValue(child));
                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(child).ToString());
                Assert.AreEqual(
                    "create_kanban_child_task",
                    taskType.GetField("CreationSource").GetValue(child));
                Assert.AreEqual(
                    "To Do",
                    taskType.GetField("CreatedStatus").GetValue(child));
                Assert.IsTrue(
                    (bool)taskType.GetField("AgentAssignmentInherited").GetValue(child));
                Assert.IsTrue(
                    (bool)taskType.GetField("IsDecompositionDraft").GetValue(child),
                    "A child must remain a non-runnable draft until its parent decomposition validates.");

                object sibling = Activator.CreateInstance(taskType);
                SetTaskField(taskType, sibling, "Id", "victory-sibling");
                SetTaskField(taskType, sibling, "Title", "Victory system");
                SetTaskField(taskType, sibling, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    sibling,
                    "Status",
                    Enum.Parse(statusType, "Todo"));
                SetTaskField(
                    taskType,
                    sibling,
                    "Description",
                    "Implement the goal and victory behavior.");
                SetTaskField(
                    taskType,
                    sibling,
                    "AcceptanceCriteria",
                    "Reaching the goal shows a victory state.");
                SetTaskField(
                    taskType,
                    sibling,
                    "ProducedArtifactPaths",
                    new List<string>
                    {
                        "Assets/Game/GoalDetector.cs"
                    });
                tasks.Add(sibling);

                Type activeRunType =
                    windowType.GetNestedType(
                        "ActiveAgentRun",
                        BindingFlags.NonPublic);

                object activeRun =
                    Activator.CreateInstance(activeRunType);

                activeRunType.GetField("TaskId")
                    .SetValue(activeRun, "parent");

                IDictionary activeRuns =
                    (IDictionary)windowType.GetField(
                            "_activeAgentRuns",
                            BindingFlags.NonPublic |
                            BindingFlags.Instance)
                        .GetValue(window);

                activeRuns.Add(
                    "DeveloperAgent",
                    activeRun);

                MethodInfo hasRunningAncestor =
                    windowType.GetMethod(
                        "HasRunningAncestor",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance);

                Assert.IsTrue(
                    (bool)hasRunningAncestor.Invoke(
                        window,
                        new[] { child }),
                    "A newly spawned child must not start while its parent is still producing the decomposition batch.");

                activeRuns.Clear();

                Assert.IsFalse(
                    (bool)hasRunningAncestor.Invoke(
                        window,
                        new[] { child }));

                MethodInfo hasTerminalAncestor =
                    windowType.GetMethod(
                        "HasTerminalFailureAncestor",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance);

                SetTaskField(
                    taskType,
                    parent,
                    "Status",
                    Enum.Parse(statusType, "Failed"));

                Assert.IsTrue(
                    (bool)hasTerminalAncestor.Invoke(window, new[] { child }),
                    "A draft or committed child must never run beneath a terminally failed parent.");

                MethodInfo getContext = windowType.GetMethod(
                    "GetAgentTaskContext",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                object childContext = getContext.Invoke(
                    window,
                    new[]
                    {
                        taskType.GetField("Id").GetValue(child)
                    });

                Assert.AreEqual(
                    0,
                    childContext.GetType()
                        .GetProperty("DirectChildCount")
                        .GetValue(childContext));
                Assert.AreEqual(
                    2,
                    childContext.GetType()
                        .GetProperty("ParentDirectChildCount")
                        .GetValue(childContext));

                Array ancestorContracts =
                    (Array)childContext.GetType()
                        .GetProperty("AncestorContracts")
                        .GetValue(childContext);

                Assert.AreEqual(1, ancestorContracts.Length);
                Assert.AreEqual(
                    "Parent",
                    ancestorContracts.GetValue(0)
                        .GetType()
                        .GetProperty("Title")
                        .GetValue(ancestorContracts.GetValue(0)));

                Array siblingContracts =
                    (Array)childContext.GetType()
                        .GetProperty("SiblingContracts")
                        .GetValue(childContext);

                Assert.AreEqual(1, siblingContracts.Length);
                Assert.AreEqual(
                    "Victory system",
                    siblingContracts.GetValue(0)
                        .GetType()
                        .GetProperty("Title")
                        .GetValue(siblingContracts.GetValue(0)));

                tasks.Remove(sibling);

                object rejectedResult =
                    createChild.Invoke(
                        window,
                        new object[]
                        {
                            taskType.GetField("Id").GetValue(child),
                            new CreateKanbanChildTaskArguments
                            {
                                Title = "Malformed probe",
                                Description =
                                    "Verify attachment to card parent.",
                                AcceptanceCriteria =
                                    "The child is attached to parent."
                            }
                        });

                Assert.IsFalse(
                    (bool)rejectedResult.GetType()
                        .GetField("Succeeded")
                        .GetValue(rejectedResult));
                StringAssert.Contains(
                    "but the executing parent is",
                    (string)rejectedResult.GetType()
                        .GetField("Error")
                        .GetValue(rejectedResult));
                Assert.AreEqual(
                    2,
                    tasks.Count,
                    "A recoverable validation result must not create a malformed card.");

                MethodInfo setStatus = windowType.GetMethod(
                    "TrySetTaskStatus",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                object inProgress = Enum.Parse(statusType, "InProgress");
                object[] blockedArguments =
                    { parent, inProgress, null };

                Assert.IsFalse(
                    (bool)setStatus.Invoke(window, blockedArguments));
                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(parent).ToString());

                object blockedParentContext = getContext.Invoke(
                    window,
                    new object[] { "parent" });

                Array parentChildren =
                    (Array)blockedParentContext.GetType()
                        .GetProperty("DirectChildren")
                        .GetValue(blockedParentContext);

                Assert.AreEqual(
                    1,
                    parentChildren.Length);

                object childEvidence =
                    parentChildren.GetValue(0);

                Assert.AreEqual(
                    "create_kanban_child_task",
                    childEvidence.GetType()
                        .GetProperty("CreationSource")
                        .GetValue(childEvidence));
                Assert.IsTrue(
                    (bool)childEvidence.GetType()
                        .GetProperty("AgentAssignmentInherited")
                        .GetValue(childEvidence));

                Assert.AreEqual(
                    "BlockedInToDo",
                    blockedParentContext.GetType()
                        .GetProperty("DescendantGateState")
                        .GetValue(blockedParentContext));

                SetTaskField(
                    taskType,
                    child,
                    "Status",
                    Enum.Parse(statusType, "Done"));
                SetTaskField(taskType, child, "ReviewStatus", "Passed");

                object releasedParentContext = getContext.Invoke(
                    window,
                    new object[] { "parent" });

                Assert.AreEqual(
                    "ReleasedAllDescendantsDone",
                    releasedParentContext.GetType()
                        .GetProperty("DescendantGateState")
                        .GetValue(releasedParentContext));
                Assert.IsTrue(
                    (bool)releasedParentContext.GetType()
                        .GetProperty("AllDescendantsDone")
                        .GetValue(releasedParentContext));

                object[] releasedArguments =
                    { parent, inProgress, null };

                Assert.IsTrue(
                    (bool)setStatus.Invoke(window, releasedArguments));
                Assert.AreEqual(
                    "InProgress",
                    taskType.GetField("Status").GetValue(parent).ToString());
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void SameArtifactChildProposal_StopsRecursiveDecompositionAndReusesParent()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType(
                "TaskStatus",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "game-manager");
                SetTaskField(taskType, parent, "Title", "Author GameManager.cs");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                SetTaskField(
                    taskType,
                    parent,
                    "Status",
                    Enum.Parse(statusType, "InProgress"));
                SetTaskField(
                    taskType,
                    parent,
                    "AgentStartedAt",
                    "2026-08-22T00:00:00.0000000Z");
                SetTaskField(
                    taskType,
                    parent,
                    "ProducedArtifactPaths",
                    new List<string>
                    {
                        "Assets/BallJumpGame/Scripts/GameManager.cs"
                    });
                tasks.Add(parent);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                createChild.Invoke(
                    window,
                    new object[]
                    {
                        "game-manager",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = "Verify GameManager.cs",
                            Description =
                                "Inspect Assets/BallJumpGame/Scripts/GameManager.cs after implementation.",
                            AcceptanceCriteria =
                                "GameManager.cs compiles and satisfies its contract.",
                            ConsumesArtifactPaths = new List<string>
                            {
                                "Assets/BallJumpGame/Scripts/GameManager.cs"
                            }
                        }
                    });

                Assert.AreEqual(2, tasks.Count);

                object result = createChild.Invoke(
                    window,
                    new object[]
                    {
                        "game-manager",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = "Author GameManager.cs script file",
                            Description =
                                "Create Assets/BallJumpGame/Scripts/GameManager.cs.",
                            AcceptanceCriteria =
                                "The script exists and compiles.",
                            ProducesArtifactPaths = new List<string>
                            {
                                "Assets/BallJumpGame/Scripts/GameManager.cs"
                            }
                        }
                    });

                Assert.IsTrue(
                    (bool)result.GetType()
                        .GetField("Succeeded")
                        .GetValue(result));
                Assert.AreEqual(
                    "game-manager",
                    result.GetType()
                        .GetField("ChildTaskId")
                        .GetValue(result));
                Assert.AreEqual(
                    1,
                    tasks.Count,
                    "Draft verifier/producer cards from the recursive turn must be discarded before direct handoff.");
                Assert.IsTrue(
                    (bool)GetTaskField(
                        taskType,
                        parent,
                        "SuppressAutoDecomposition"));
                StringAssert.StartsWith(
                    "[AssociationLeaf]",
                    (string)GetTaskField(
                        taskType,
                        parent,
                        "NextAgentMessage"));

                object context = windowType.GetMethod(
                        "GetAgentTaskContext",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .Invoke(window, new object[] { "game-manager" });

                Assert.IsTrue(
                    (bool)context.GetType()
                        .GetProperty("AssociationResolvedAsAtomicLeaf")
                        .GetValue(context));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void SameScriptProposal_FromSceneOwningParent_RemainsValidDecomposition()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "aggregate-parent");
                SetTaskField(
                    taskType,
                    parent,
                    "Title",
                    "Implement GameManager.cs runtime outcome");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                SetTaskField(
                    taskType,
                    parent,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/GameManager.cs" });
                SetTaskField(
                    taskType,
                    parent,
                    "MutatedResourceKeys",
                    new List<string> { "scene:Assets/Game/Game.unity" });
                tasks.Add(parent);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                object result = windowType.GetMethod(
                        "CreateAgentChildTask",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .Invoke(
                        window,
                        new object[]
                        {
                            "aggregate-parent",
                            new CreateKanbanChildTaskArguments
                            {
                                Title = "Author GameManager.cs",
                                Description = "Create Assets/Game/GameManager.cs.",
                                AcceptanceCriteria = "GameManager.cs compiles.",
                                ProducesArtifactPaths = new List<string>
                                {
                                    "Assets/Game/GameManager.cs"
                                }
                            }
                        });

                Assert.IsTrue(
                    (bool)result.GetType()
                        .GetField("Succeeded")
                        .GetValue(result));
                Assert.AreEqual(2, tasks.Count);
                Assert.AreNotEqual(
                    "aggregate-parent",
                    result.GetType()
                        .GetField("ChildTaskId")
                        .GetValue(result));
                Assert.IsFalse(
                    (bool)GetTaskField(
                        taskType,
                        parent,
                        "SuppressAutoDecomposition"),
                    "A scene-owning aggregate parent must not be collapsed into an atomic script leaf.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void CrossBranchArtifactOwner_IsAssociatedButDuplicateWriterIsRejected()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object owner = Activator.CreateInstance(taskType);
                SetTaskField(taskType, owner, "Id", "existing-owner");
                SetTaskField(taskType, owner, "Title", "Implement shared script");
                SetTaskField(
                    taskType,
                    owner,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Scripts/Shared.cs" });
                SetTaskField(
                    taskType,
                    owner,
                    "CreatedAt",
                    "2026-08-22T00:00:00.0000000Z");
                tasks.Add(owner);

                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "other-branch");
                SetTaskField(taskType, parent, "Title", "Coordinate other branch");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                tasks.Add(parent);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                object result = windowType.GetMethod(
                        "CreateAgentChildTask",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .Invoke(
                        window,
                        new object[]
                        {
                            "other-branch",
                            new CreateKanbanChildTaskArguments
                            {
                                Title = "Author Shared.cs again",
                                Description = "Create Assets/Game/Scripts/Shared.cs.",
                                AcceptanceCriteria = "Shared.cs exists.",
                                ProducesArtifactPaths = new List<string>
                                {
                                    "Assets/Game/Scripts/Shared.cs"
                                }
                            }
                        });

                Assert.IsFalse(
                    (bool)result.GetType()
                        .GetField("Succeeded")
                        .GetValue(result));
                StringAssert.Contains(
                    "ownership conflict",
                    (string)result.GetType()
                        .GetField("Error")
                        .GetValue(result));
                Assert.AreEqual(
                    "existing-owner",
                    result.GetType()
                        .GetField("ChildTaskId")
                        .GetValue(result));
                Assert.AreEqual(
                    2,
                    tasks.Count,
                    "A second writer card must not be created for exact global ownership.");
                CollectionAssert.Contains(
                    (ICollection)GetTaskField(
                        taskType,
                        parent,
                        "AssociatedTaskIds"),
                    "existing-owner");
                CollectionAssert.Contains(
                    (ICollection)GetTaskField(
                        taskType,
                        owner,
                        "AssociatedTaskIds"),
                    "other-branch");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void RootRelationshipReconciliation_BackfillsAndClearsSiblingDependency()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object consumer = Activator.CreateInstance(taskType);
                SetTaskField(taskType, consumer, "Id", "root-consumer");
                SetTaskField(taskType, consumer, "Title", "Root consumer");
                SetTaskField(
                    taskType,
                    consumer,
                    "Description",
                    "Depends on sibling task \"Root producer\" being Done. Verify integration.");
                SetTaskField(
                    taskType,
                    consumer,
                    "DeclaredDependencyTitles",
                    new List<string> { "Root producer" });
                tasks.Add(consumer);

                object producer = Activator.CreateInstance(taskType);
                SetTaskField(taskType, producer, "Id", "root-producer");
                SetTaskField(taskType, producer, "Title", "Root producer");
                tasks.Add(producer);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo reconcile = windowType.GetMethod(
                    "ReconcileAllTaskRelationships",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                reconcile.Invoke(window, null);

                CollectionAssert.AreEqual(
                    new[] { "root-producer" },
                    (ICollection)GetTaskField(
                        taskType,
                        consumer,
                        "DependsOnTaskIds"));

                tasks.Remove(producer);
                reconcile.Invoke(window, null);

                Assert.IsEmpty(
                    (ICollection)GetTaskField(
                        taskType,
                        consumer,
                        "DependsOnTaskIds"),
                    "Deleting a root sibling must clear its stale edge immediately.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void RootRelationshipReconciliation_DoesNotJoinIndependentFinalVerifier()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object unrelatedRoot = Activator.CreateInstance(taskType);
                SetTaskField(taskType, unrelatedRoot, "Id", "root-project");
                SetTaskField(taskType, unrelatedRoot, "Title", "Independent project");
                tasks.Add(unrelatedRoot);

                object rootVerifier = Activator.CreateInstance(taskType);
                SetTaskField(taskType, rootVerifier, "Id", "root-verifier");
                SetTaskField(
                    taskType,
                    rootVerifier,
                    "Title",
                    "Final integration verification");
                tasks.Add(rootVerifier);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                windowType.GetMethod(
                        "ReconcileAllTaskRelationships",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .Invoke(window, null);

                Assert.IsEmpty(
                    (ICollection)GetTaskField(
                        taskType,
                        rootVerifier,
                        "DependsOnTaskIds"),
                    "A root verifier must not join unrelated root work graphs.");
                Assert.IsEmpty(
                    (ICollection)GetTaskField(
                        taskType,
                        unrelatedRoot,
                        "DependsOnTaskIds"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void RelationshipReconciliation_UsesStableCreatedAtForCanonicalProducer()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object laterProducer = Activator.CreateInstance(taskType);
                SetTaskField(taskType, laterProducer, "Id", "later-producer");
                SetTaskField(taskType, laterProducer, "ParentTaskId", "scope");
                SetTaskField(
                    taskType,
                    laterProducer,
                    "CreatedAt",
                    "2026-08-22T02:00:00.0000000Z");
                SetTaskField(
                    taskType,
                    laterProducer,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Shared.cs" });
                tasks.Add(laterProducer);

                object earlierProducer = Activator.CreateInstance(taskType);
                SetTaskField(taskType, earlierProducer, "Id", "earlier-producer");
                SetTaskField(taskType, earlierProducer, "ParentTaskId", "scope");
                SetTaskField(
                    taskType,
                    earlierProducer,
                    "CreatedAt",
                    "2026-08-22T01:00:00.0000000Z");
                SetTaskField(
                    taskType,
                    earlierProducer,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Shared.cs" });
                tasks.Add(earlierProducer);

                object consumer = Activator.CreateInstance(taskType);
                SetTaskField(taskType, consumer, "Id", "consumer");
                SetTaskField(taskType, consumer, "ParentTaskId", "scope");
                SetTaskField(
                    taskType,
                    consumer,
                    "ConsumedArtifactPaths",
                    new List<string> { "Assets/Game/Shared.cs" });
                tasks.Add(consumer);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                windowType.GetMethod(
                        "ReconcileAllTaskRelationships",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .Invoke(window, null);

                CollectionAssert.AreEqual(
                    new[] { "earlier-producer" },
                    (ICollection)GetTaskField(
                        taskType,
                        consumer,
                        "DependsOnTaskIds"),
                    "Canonical ownership must not depend on serialized task-list order.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void SemanticEdit_InvalidatesAtomicSuppressionAndRestoresDecomposition()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Title", "Author Player.cs");
            SetTaskField(
                taskType,
                task,
                "Description",
                "Create Assets/Game/Player.cs directly.");
            SetTaskField(
                taskType,
                task,
                "AcceptanceCriteria",
                "Player.cs compiles.");
            SetTaskField(taskType, task, "SuppressAutoDecomposition", true);
            SetTaskField(taskType, task, "WorkflowPhase", "Planning");
            SetTaskField(
                taskType,
                task,
                "NextAgentMessage",
                "[AssociationLeaf] Resume the original script card.");

            const string previousDescription =
                "Create Assets/Game/Player.cs directly.";

            SetTaskField(
                taskType,
                task,
                "Description",
                "Create Assets/Game/Player.cs, a playable scene and prefab, UI, PlayMode tests, and final integration verification.");

            MethodInfo invalidate = windowType.GetMethod(
                "InvalidateAtomicLeafSuppressionForContractChange",
                BindingFlags.NonPublic |
                BindingFlags.Static);

            Assert.IsTrue(
                (bool)invalidate.Invoke(
                    null,
                    new[]
                    {
                        task,
                        "Author Player.cs",
                        previousDescription,
                        "Player.cs compiles."
                    }));
            Assert.IsFalse(
                (bool)GetTaskField(
                    taskType,
                    task,
                    "SuppressAutoDecomposition"));
            Assert.AreEqual(
                string.Empty,
                GetTaskField(
                    taskType,
                    task,
                    "NextAgentMessage"));

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoDecompose",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, true);

                Assert.IsTrue(
                    (bool)windowType.GetMethod(
                            "ShouldStartCoordinationOnlyRun",
                            BindingFlags.NonPublic |
                            BindingFlags.Instance)
                        .Invoke(window, new[] { task }));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void LoadedSameArtifactDecompositionStack_CollapsesToAtomicOwner()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType(
                "TaskStatus",
                BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            const string artifact =
                "Assets/BallJumpGame/Scripts/GameManager.cs";

            object canonical = Activator.CreateInstance(taskType);
            SetTaskField(taskType, canonical, "Id", "canonical");
            SetTaskField(taskType, canonical, "Title", "Implement GameManager.cs");
            SetTaskField(
                taskType,
                canonical,
                "Status",
                Enum.Parse(statusType, "InProgress"));
            SetTaskField(
                taskType,
                canonical,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(canonical);

            object duplicate = Activator.CreateInstance(taskType);
            SetTaskField(taskType, duplicate, "Id", "duplicate");
            SetTaskField(taskType, duplicate, "ParentTaskId", "canonical");
            SetTaskField(taskType, duplicate, "Title", "Author GameManager.cs");
            SetTaskField(
                taskType,
                duplicate,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                duplicate,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(duplicate);

            object deepDuplicate = Activator.CreateInstance(taskType);
            SetTaskField(taskType, deepDuplicate, "Id", "deep-duplicate");
            SetTaskField(taskType, deepDuplicate, "ParentTaskId", "duplicate");
            SetTaskField(taskType, deepDuplicate, "Title", "Author GameManager.cs file");
            SetTaskField(
                taskType,
                deepDuplicate,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                deepDuplicate,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(deepDuplicate);

            object verifier = Activator.CreateInstance(taskType);
            SetTaskField(taskType, verifier, "Id", "verifier");
            SetTaskField(taskType, verifier, "ParentTaskId", "canonical");
            SetTaskField(taskType, verifier, "Title", "Verify GameManager.cs");
            SetTaskField(
                taskType,
                verifier,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                verifier,
                "ConsumedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(verifier);

            object unrelated = Activator.CreateInstance(taskType);
            SetTaskField(taskType, unrelated, "Id", "unrelated");
            SetTaskField(taskType, unrelated, "ParentTaskId", "duplicate");
            SetTaskField(taskType, unrelated, "Title", "Author Other.cs");
            SetTaskField(
                taskType,
                unrelated,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                unrelated,
                "ProducedArtifactPaths",
                new List<string> { "Assets/BallJumpGame/Scripts/Other.cs" });
            tasks.Add(unrelated);

            object sceneConsumer = Activator.CreateInstance(taskType);
            SetTaskField(taskType, sceneConsumer, "Id", "scene-consumer");
            SetTaskField(taskType, sceneConsumer, "ParentTaskId", "duplicate");
            SetTaskField(taskType, sceneConsumer, "Title", "Wire GameManager into scene");
            SetTaskField(
                taskType,
                sceneConsumer,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                sceneConsumer,
                "ConsumedArtifactPaths",
                new List<string> { artifact });
            SetTaskField(
                taskType,
                sceneConsumer,
                "MutatedResourceKeys",
                new List<string> { "scene:Assets/BallJumpGame/Scenes/Game.unity" });
            tasks.Add(sceneConsumer);

            MethodInfo repair = windowType.GetMethod(
                "RepairRecursiveArtifactDecompositionLoops",
                BindingFlags.NonPublic |
                BindingFlags.Static);

            Assert.AreEqual(
                3,
                repair.Invoke(null, new object[] { tasks }));
            Assert.AreEqual(3, tasks.Count);
            Assert.AreEqual(
                "Todo",
                GetTaskField(taskType, canonical, "Status").ToString());
            Assert.IsTrue(
                (bool)GetTaskField(
                    taskType,
                    canonical,
                    "SuppressAutoDecomposition"));
            StringAssert.StartsWith(
                "[AssociationLeaf]",
                (string)GetTaskField(
                    taskType,
                    canonical,
                    "NextAgentMessage"));
            Assert.AreEqual(
                string.Empty,
                GetTaskField(
                    taskType,
                    unrelated,
                    "ParentTaskId"));
            Assert.AreEqual(
                string.Empty,
                GetTaskField(
                    taskType,
                    sceneConsumer,
                    "ParentTaskId"),
                "A distinct scene mutation must be preserved and moved outside the atomic script leaf.");
            Assert.AreEqual(
                0,
                repair.Invoke(null, new object[] { tasks }),
                "The load repair must be idempotent.");
        }


        [Test]
        public void CommittedImplementationPlan_IsNeverRewrittenByLegacyArtifactRepair()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);
            const string artifact = "Assets/Game/Scripts/PlayerController.cs";

            object root = Activator.CreateInstance(taskType);
            SetTaskField(taskType, root, "Id", "committed-root");
            SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
            SetTaskField(taskType, root, "PlanRevision", 7);
            tasks.Add(root);

            object approvedCard = Activator.CreateInstance(taskType);
            SetTaskField(taskType, approvedCard, "Id", "approved-card");
            SetTaskField(taskType, approvedCard, "ParentTaskId", "committed-root");
            SetTaskField(
                taskType,
                approvedCard,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(approvedCard);

            object repeatedOwner = Activator.CreateInstance(taskType);
            SetTaskField(taskType, repeatedOwner, "Id", "legacy-repeated-owner");
            SetTaskField(taskType, repeatedOwner, "ParentTaskId", "approved-card");
            SetTaskField(
                taskType,
                repeatedOwner,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                repeatedOwner,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(repeatedOwner);

            MethodInfo repair = windowType.GetMethod(
                "RepairRecursiveArtifactDecompositionLoops",
                BindingFlags.NonPublic | BindingFlags.Static);

            Assert.AreEqual(0, repair.Invoke(null, new object[] { tasks }));
            Assert.AreEqual(
                3,
                tasks.Count,
                "A legacy cleanup must not delete cards from an approved implementation plan.");
            Assert.AreEqual(
                "approved-card",
                GetTaskField(taskType, repeatedOwner, "ParentTaskId"));
        }


        [Test]
        public void LoadedAggregateScriptAndSceneStack_RemainsDecomposable()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);

            const string artifact = "Assets/Game/GameManager.cs";

            object aggregate = Activator.CreateInstance(taskType);
            SetTaskField(taskType, aggregate, "Id", "aggregate");
            SetTaskField(
                taskType,
                aggregate,
                "Title",
                "Implement GameManager.cs runtime outcome");
            SetTaskField(
                taskType,
                aggregate,
                "Description",
                "Implement the owned runtime outcome.");
            SetTaskField(
                taskType,
                aggregate,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            SetTaskField(
                taskType,
                aggregate,
                "MutatedResourceKeys",
                new List<string> { "scene:Assets/Game/Game.unity" });
            SetTaskField(
                taskType,
                aggregate,
                "SuppressAutoDecomposition",
                true);
            SetTaskField(
                taskType,
                aggregate,
                "NextAgentMessage",
                "[AssociationLeaf] Legacy false atomic classification.");
            tasks.Add(aggregate);

            object repeatedScript = Activator.CreateInstance(taskType);
            SetTaskField(taskType, repeatedScript, "Id", "repeated-script");
            SetTaskField(taskType, repeatedScript, "ParentTaskId", "aggregate");
            SetTaskField(
                taskType,
                repeatedScript,
                "CreationSource",
                "create_kanban_child_task");
            SetTaskField(
                taskType,
                repeatedScript,
                "ProducedArtifactPaths",
                new List<string> { artifact });
            tasks.Add(repeatedScript);

            MethodInfo normalize = windowType.GetMethod(
                "NormalizeLoadedTaskHierarchy",
                BindingFlags.NonPublic |
                BindingFlags.Static);

            Assert.GreaterOrEqual(
                (int)normalize.Invoke(null, new object[] { tasks }),
                1,
                "Load normalization must clear stale atomic state from structured scene work.");
            Assert.AreEqual(
                string.Empty,
                GetTaskField(
                    taskType,
                    aggregate,
                    "NextAgentMessage"));

            MethodInfo repair = windowType.GetMethod(
                "RepairRecursiveArtifactDecompositionLoops",
                BindingFlags.NonPublic |
                BindingFlags.Static);

            Assert.AreEqual(
                0,
                repair.Invoke(null, new object[] { tasks }));
            Assert.AreEqual(2, tasks.Count);
            Assert.IsFalse(
                (bool)GetTaskField(
                    taskType,
                    aggregate,
                    "SuppressAutoDecomposition"),
                "Scene-owning aggregate work must not be migrated into an atomic script leaf.");
        }


        [Test]
        public void ReadOnlyVerificationAndFinalVerifier_DoNotStartCoordinationRuns()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoDecompose", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, true);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);

                object readOnly = Activator.CreateInstance(taskType);
                SetTaskField(taskType, readOnly, "Id", "read-only");
                SetTaskField(taskType, readOnly, "Title", "Verify PlayerBall.cs namespace declaration");
                SetTaskField(
                    taskType,
                    readOnly,
                    "Description",
                    "READ-ONLY verification. Inspect Assets/Game/Scripts/PlayerBall.cs. Do NOT modify any asset, scene, or prefab.");
                SetTaskField(
                    taskType,
                    readOnly,
                    "ConsumedArtifactPaths",
                    new List<string> { "Assets/Game/Scripts/PlayerBall.cs" });
                tasks.Add(readOnly);

                object quotedEvidence = Activator.CreateInstance(taskType);
                SetTaskField(taskType, quotedEvidence, "Id", "quoted-evidence");
                SetTaskField(
                    taskType,
                    quotedEvidence,
                    "Title",
                    "Read PlayerController.cs and quote criterion-2 evidence");
                SetTaskField(
                    taskType,
                    quotedEvidence,
                    "Description",
                    "READ-ONLY SOURCE AUDIT. Read and quote the constraints assignment. " +
                    "Do NOT create, edit, move, delete, or recompile any script, asset, or scene. " +
                    "Do NOT write any artifact file.");
                SetTaskField(
                    taskType,
                    quotedEvidence,
                    "AcceptanceCriteria",
                    "Verify the exact source line with path and line-number evidence.");
                SetTaskField(
                    taskType,
                    quotedEvidence,
                    "ConsumedArtifactPaths",
                    new List<string> { "Assets/BallJumpGame/Scripts/PlayerController.cs" });
                tasks.Add(quotedEvidence);

                object final = Activator.CreateInstance(taskType);
                SetTaskField(taskType, final, "Id", "final");
                SetTaskField(taskType, final, "Title", "Final integration verification and report");
                SetTaskField(
                    taskType,
                    final,
                    "Description",
                    "Verify scripts, a scene, UI, compilation, tests, and every acceptance criterion.");
                SetTaskField(
                    taskType,
                    final,
                    "ConsumedArtifactPaths",
                    new List<string>
                    {
                        "Assets/Game/Scripts/PlayerBall.cs",
                        "Assets/Game/Scenes/Game.unity"
                    });
                tasks.Add(final);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo shouldCoordinate = windowType.GetMethod(
                    "ShouldStartCoordinationOnlyRun",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsFalse(
                    (bool)shouldCoordinate.Invoke(window, new[] { readOnly }),
                    "A one-artifact read-only verifier is a cohesive execution leaf.");
                Assert.IsFalse(
                    (bool)shouldCoordinate.Invoke(window, new[] { quotedEvidence }),
                    "Negated create/edit/write clauses must not recursively decompose a one-script read-only evidence card.");
                Assert.IsFalse(
                    (bool)shouldCoordinate.Invoke(window, new[] { final }),
                    "A designated final verifier is the terminal sink of its existing graph.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ImplementationPhase_NeverStartsLegacyCoordinationAtAnyHierarchyDepth()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoDecompose", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, true);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                string parentId = string.Empty;
                object leaf = null;

                for (int depth = 0; depth <= 8; depth++)
                {
                    object task = Activator.CreateInstance(taskType);
                    string id = $"depth-{depth}";
                    SetTaskField(taskType, task, "Id", id);
                    SetTaskField(taskType, task, "ParentTaskId", parentId);
                    SetTaskField(taskType, task, "Title", $"Outcome at depth {depth}");
                    tasks.Add(task);
                    parentId = id;
                    leaf = task;
                }

                SetTaskField(taskType, leaf, "Title", "Build complete game outcome");
                SetTaskField(
                    taskType,
                    leaf,
                    "Description",
                    "Build runtime scripts, a playable scene with UI, PlayMode tests, compilation, and final acceptance.");

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo shouldCoordinate = windowType.GetMethod(
                    "ShouldStartCoordinationOnlyRun",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsFalse(
                    (bool)shouldCoordinate.Invoke(window, new[] { leaf }),
                    "Implementation cannot mutate the graph, regardless of legacy hierarchy depth or prose breadth.");
                Assert.IsFalse(
                    (bool)GetTaskField(taskType, leaf, "SuppressAutoDecomposition"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void BroadRoot_AutoDecomposesThenPromotesAfterChildrenPass()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_autoSave",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, false);

                windowType.GetField(
                        "_autoDecompose",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, true);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);

                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "broad-parent");
                SetTaskField(taskType, parent, "Title", "Build complete ball game");
                SetTaskField(
                    taskType,
                    parent,
                    "Description",
                    "Create runtime movement scripts, a playable Unity scene with camera and UI, PlayMode tests, and final verification.");
                SetTaskField(taskType, parent, "AcceptanceCriteria", "The game compiles, plays, and passes its tests.");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, parent, "WorkflowPhase", "Planning");
                tasks.Add(parent);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo shouldCoordinate = windowType.GetMethod(
                    "ShouldStartCoordinationOnlyRun",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                Assert.IsTrue(
                    (bool)shouldCoordinate.Invoke(
                        window,
                        new[] { parent }));

                object child = Activator.CreateInstance(taskType);
                SetTaskField(taskType, child, "Id", "completed-child");
                SetTaskField(taskType, child, "Title", "Final integration verification");
                SetTaskField(taskType, child, "ParentTaskId", "broad-parent");
                SetTaskField(taskType, child, "Status", Enum.Parse(statusType, "Done"));
                SetTaskField(taskType, child, "ReviewStatus", "Passed");
                SetTaskField(taskType, child, "IsDecompositionDraft", false);
                tasks.Add(child);

                SetTaskField(taskType, parent, "WorkflowPhase", "Implementation");

                Assert.IsFalse(
                    (bool)shouldCoordinate.Invoke(
                        window,
                        new[] { parent }),
                    "Implementation cannot reopen graph authoring after plan approval.");

                MethodInfo promote = windowType.GetMethod(
                    "TryPromoteCompletedCoordinationParentToReview",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

                Assert.IsTrue(
                    (bool)promote.Invoke(
                        window,
                        new[] { parent }));

                Assert.AreEqual(
                    "Review",
                    taskType.GetField("Status").GetValue(parent).ToString());
                Assert.AreEqual(
                    "Pending",
                    taskType.GetField("ReviewStatus").GetValue(parent));

                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, parent, "ReviewStatus", "Rework Requested");
                SetTaskField(taskType, parent, "NextAgentMessage", "Add coverage for a missing parent criterion.");

                Assert.IsFalse(
                    (bool)shouldCoordinate.Invoke(
                        window,
                        new[] { parent }),
                    "Implementation rework must kick the project back to Planning before graph changes are allowed.");

                Assert.IsFalse(
                    (bool)promote.Invoke(
                        window,
                        new[] { parent }),
                    "A rework request must be handled before the parent can return to review.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void SceneWritingChildren_RequireCanonicalInScopePathAndSerializeWriters()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);

                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Make Game");
                SetTaskField(taskType, parent, "Description",
                    "Place every new asset under Assets/JumpingBallGame. The player moves on X/Y and remains on Z=0.");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                MethodInfo mutatesSharedScene = windowType.GetMethod(
                    "MutatesSharedUnityScene",
                    BindingFlags.NonPublic | BindingFlags.Static);

                Assert.IsFalse(
                    (bool)mutatesSharedScene.Invoke(
                        null,
                        new object[]
                        {
                            "Compile the project. Depends on sibling task \"Save JumpingBallGame scene\" being Done."
                        }),
                    "A dependency title mentioning a scene must not classify an otherwise file-only card as a scene writer.");

                Assert.IsFalse(
                    (bool)mutatesSharedScene.Invoke(
                        null,
                        new object[]
                        {
                            "Create Assets/JumpingBallGame/Materials/BallMaterial.mat. " +
                            "This material is applied to PlayerBall in Assets/JumpingBallGame/Scenes/JumpingBallGame.unity by a sibling card."
                        }),
                    "A future consumer scene reference must not make a material producer card a scene writer.");

                Assert.IsTrue(
                    (bool)mutatesSharedScene.Invoke(
                        null,
                        new object[]
                        {
                            "In Assets/JumpingBallGame/Scenes/JumpingBallGame.unity, create a PlayerBall GameObject."
                        }));

                object deferredScenePath =
                    createChild.Invoke(window, new object[]
                    {
                        "parent",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = "Create Ground",
                            Description = "Create the Ground GameObject in the game scene.",
                            AcceptanceCriteria = "Ground exists."
                        }
                    });

                Assert.IsTrue(
                    (bool)deferredScenePath.GetType()
                        .GetField("Succeeded")
                        .GetValue(deferredScenePath),
                    "A scene writer may defer its canonical path until another card in the same batch establishes it.");

                object outsideScope =
                    createChild.Invoke(window, new object[]
                    {
                        "parent",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = "Create Outside Ground",
                            Description = "Create the Ground GameObject in Assets/Scenes/AIRuntime.unity.",
                            AcceptanceCriteria = "Ground exists."
                        }
                    });

                Assert.IsFalse(
                    (bool)outsideScope.GetType()
                        .GetField("Succeeded")
                        .GetValue(outsideScope));
                StringAssert.Contains(
                    "outside the inherited parent asset scope",
                    (string)outsideScope.GetType()
                        .GetField("Error")
                        .GetValue(outsideScope));

                object forwardDependency =
                    createChild.Invoke(window, new object[]
                    {
                        "parent",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = "Save JumpingBallGame scene",
                            Description =
                                "Save Assets/JumpingBallGame/Scenes/JumpingBallGame.unity. " +
                                "Depends on sibling task \"Build player ball GameObject\" being Done.",
                            AcceptanceCriteria = "The scene is saved."
                        }
                    });

                Assert.IsTrue(
                    (bool)forwardDependency.GetType()
                        .GetField("Succeeded")
                        .GetValue(forwardDependency),
                    "Forward sibling dependencies must be accepted while the batch is still being constructed.");

                object deferredGround = tasks[1];
                StringAssert.DoesNotContain(
                    "Assets/JumpingBallGame/Scenes/JumpingBallGame.unity",
                    (string)taskType.GetField("Description").GetValue(deferredGround),
                    "AIRuntime must not rewrite existing child descriptions because injected paths can impose false review preconditions.");

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Create Ground",
                        Description = "Create the Ground GameObject in Assets/JumpingBallGame/Scenes/JumpingBallGame.unity.",
                        AcceptanceCriteria = "Ground exists."
                    }
                });

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Write helper script",
                        Description = "Create Assets/JumpingBallGame/Scripts/Helper.cs.",
                        AcceptanceCriteria = "Helper.cs exists."
                    }
                });

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                        {
                            Title = "Create Platform",
                            Description = "Create the Platform GameObject in Assets/JumpingBallGame/Scenes/JumpingBallGame.unity.",
                            AcceptanceCriteria = "Platform exists.",
                            DependsOnSiblingTitles =
                                new List<string>
                                {
                                    "Write helper script",
                                    "Save JumpingBallGame scene"
                                }
                        }
                    });

                object platform = tasks[tasks.Count - 1];
                string platformDescription = (string)taskType.GetField("Description").GetValue(platform);

                StringAssert.Contains(
                    "Depends on sibling tasks",
                    platformDescription);
                StringAssert.Contains(
                    "\"Save JumpingBallGame scene\"",
                    platformDescription);
                StringAssert.Contains(
                    "\"Write helper script\"",
                    platformDescription);

                IList platformDependencyIds =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(platform);

                Assert.AreEqual(
                    2,
                    platformDependencyIds.Count,
                    "Every structured sibling prerequisite must resolve to a stable task ID.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void SceneWritingChild_UsesDeclaredOwnershipInsteadOfIncidentalScriptScope()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);

                SetTaskField(taskType, parent, "Id", "scene-parent");
                SetTaskField(taskType, parent, "Title", "Build scene and level layout");
                SetTaskField(
                    taskType,
                    parent,
                    "Description",
                    "Build Assets/BallJumpGame/Scenes/BallJumpGame.unity. " +
                    "A small GameUI.cs script under Assets/BallJumpGame/Scripts is acceptable if needed.");
                SetTaskField(
                    taskType,
                    parent,
                    "ProducedArtifactPaths",
                    new List<string>
                    {
                        "Assets/BallJumpGame/Scenes/BallJumpGame.unity",
                        "Assets/BallJumpGame/Scripts/GameUI.cs"
                    });
                SetTaskField(
                    taskType,
                    parent,
                    "MutatedResourceKeys",
                    new List<string>
                    {
                        "scene:Assets/BallJumpGame/Scenes/BallJumpGame.unity"
                    });
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo extractScope = windowType.GetMethod(
                    "ExtractInheritedAssetScope",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.AreEqual(
                    "Assets/BallJumpGame",
                    (string)extractScope.Invoke(window, new[] { parent }),
                    "Declared scene and script ownership must establish their common project scope; incidental prose must not narrow the entire card to Scripts.");

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);
                object result = createChild.Invoke(window, new object[]
                {
                    "scene-parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Create player ball and platform level",
                        Description =
                            "In Assets/BallJumpGame/Scenes/BallJumpGame.unity, create the player ball and platform level.",
                        AcceptanceCriteria = "The player and platforms exist in the saved scene.",
                        ProducesArtifactPaths =
                            new List<string>
                            {
                                "Assets/BallJumpGame/Scenes/BallJumpGame.unity"
                            },
                        MutatesResourceKeys =
                            new List<string>
                            {
                                "scene:Assets/BallJumpGame/Scenes/BallJumpGame.unity"
                            }
                    }
                });

                Assert.IsTrue(
                    (bool)result.GetType().GetField("Succeeded").GetValue(result),
                    (string)result.GetType().GetField("Error").GetValue(result));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ChildCreation_RejectsAndRollsBackResolvedDependencyCycle()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "audit-parent");
                SetTaskField(taskType, parent, "Title", "Audit criterion 2");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);
                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                object verifier = createChild.Invoke(window, new object[]
                {
                    "audit-parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Verify parent acceptance criteria",
                        Description = "Verify the quoted source evidence after the reader is Done.",
                        AcceptanceCriteria = "Every criterion has pass/fail evidence.",
                        DependsOnSiblingTitles =
                            new List<string> { "Read PlayerController.cs and quote evidence" },
                        ConsumesArtifactPaths =
                            new List<string> { "Assets/Game/Scripts/PlayerController.cs" }
                    }
                });

                Assert.IsTrue(
                    (bool)verifier.GetType().GetField("Succeeded").GetValue(verifier),
                    "A forward dependency remains valid while its sibling has not yet been created.");

                object reader = createChild.Invoke(window, new object[]
                {
                    "audit-parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Read PlayerController.cs and quote evidence",
                        Description = "Read the source after the verifier is Done and return quoted evidence.",
                        AcceptanceCriteria = "Exact source evidence is returned.",
                        DependsOnSiblingTitles =
                            new List<string> { "Verify parent acceptance criteria" },
                        ConsumesArtifactPaths =
                            new List<string> { "Assets/Game/Scripts/PlayerController.cs" }
                    }
                });

                Assert.IsFalse(
                    (bool)reader.GetType().GetField("Succeeded").GetValue(reader));
                StringAssert.Contains(
                    "Dependency cycle rejected",
                    (string)reader.GetType().GetField("Error").GetValue(reader));
                Assert.AreEqual(
                    2,
                    tasks.Count,
                    "The cycle-closing child must be removed transactionally while the valid forward-dependency sibling remains.");
                Assert.IsEmpty(
                    (ICollection)GetTaskField(
                        taskType,
                        tasks[1],
                        "DependsOnTaskIds"),
                    "Rolling back the missing sibling must also clear its temporarily resolved task ID.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ChildCreation_ReturnsRecoverableValidationAndReusesDuplicates()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Parent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                object emptyAcceptance =
                    createChild.Invoke(window, new object[]
                        {
                            "parent",
                            new CreateKanbanChildTaskArguments
                            {
                                Title = "Final verification",
                                Description = "Verify the integrated result.",
                                AcceptanceCriteria = string.Empty
                            }
                        });

                Assert.IsFalse(
                    (bool)emptyAcceptance.GetType()
                        .GetField("Succeeded")
                        .GetValue(emptyAcceptance));
                StringAssert.Contains(
                    "acceptance criteria are required",
                    (string)emptyAcceptance.GetType()
                        .GetField("Error")
                        .GetValue(emptyAcceptance));
                Assert.AreEqual(1, tasks.Count);

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Final verification",
                        Description = "Verify the integrated result.",
                        AcceptanceCriteria = "All criteria pass."
                    }
                });

                object duplicate =
                    createChild.Invoke(window, new object[]
                        {
                            "parent",
                            new CreateKanbanChildTaskArguments
                            {
                                Title = " final VERIFICATION ",
                                Description = "Duplicate work.",
                                AcceptanceCriteria = "Duplicate passes."
                            }
                        });

                Assert.IsTrue(
                    (bool)duplicate.GetType()
                        .GetField("Succeeded")
                        .GetValue(duplicate));
                StringAssert.Contains(
                    "already exists",
                    (string)duplicate.GetType()
                        .GetField("ResponseInstruction")
                        .GetValue(duplicate));
                Assert.AreEqual(2, tasks.Count);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void PlanningSnapshot_UpdatesStableCardAndCanClearOwnershipInPlace()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Parent");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                SetTaskField(taskType, parent, "DecompositionFailureCount", 1);
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                object firstResult = createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Build scene branch",
                        Description = "Build an oversized scene branch.",
                        AcceptanceCriteria = "The whole branch is complete.",
                        ProducesArtifactPaths = new List<string>
                        {
                            "Assets/Game/Scenes/Game.unity"
                        }
                    }
                });

                object retainedDraft = tasks[1];
                object retainedId = taskType.GetField("Id").GetValue(retainedDraft);

                object updateResult = createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Build scene branch",
                        Description = "Coordinate only the scene foundation branch and decompose further if needed.",
                        AcceptanceCriteria = "The foundation branch is delegated and verified.",
                        ProducesArtifactPaths = new List<string>
                        {
                            "Assets/Game/Scenes/Foundation.unity"
                        }
                    }
                });

                Assert.AreEqual(2, tasks.Count);
                Assert.AreEqual(
                    retainedId,
                    taskType.GetField("Id").GetValue(tasks[1]));
                Assert.AreEqual(
                    "Coordinate only the scene foundation branch and decompose further if needed.",
                    taskType.GetField("Description").GetValue(tasks[1]));
                CollectionAssert.AreEqual(
                    new[] { "Assets/Game/Scenes/Foundation.unity" },
                    (ICollection)taskType.GetField("ProducedArtifactPaths").GetValue(tasks[1]));
                StringAssert.Contains(
                    "updated in place",
                    (string)updateResult.GetType()
                        .GetField("ResponseInstruction")
                        .GetValue(updateResult));
                Assert.AreEqual(
                    firstResult.GetType().GetField("ChildTaskId").GetValue(firstResult),
                    updateResult.GetType().GetField("ChildTaskId").GetValue(updateResult));

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Build scene branch",
                        Description = "Refine the same proposed scene branch with no durable artifact ownership.",
                        AcceptanceCriteria = "The revised branch remains deterministic."
                    }
                });

                Assert.IsEmpty(
                    (ICollection)taskType.GetField("ProducedArtifactPaths").GetValue(tasks[1]),
                    "An authoritative Planning snapshot must be able to remove obsolete ownership declarations, not inherit them additively.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void DraftOwnershipRepair_RecoversExactSiblingArtifactsFromCardContract()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Make Game");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Func<CreateKanbanChildTaskArguments, object> create =
                    arguments =>
                        createChild.Invoke(
                            window,
                            new object[] { "parent", arguments });

                create(
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Create canonical scene",
                        Description = "Create and save Assets/Game/Scenes/Game.unity.",
                        AcceptanceCriteria = "The scene exists.",
                        ProducesArtifactPaths =
                            new List<string> { "Assets/Game/Scenes/Game.unity" },
                        MutatesResourceKeys =
                            new List<string> { "scene:Assets/Game/Scenes/Game.unity" }
                    });

                create(
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Author goal trigger",
                        Description = "Create Assets/Game/Scripts/GoalTrigger.cs.",
                        AcceptanceCriteria = "The script exists.",
                        ProducesArtifactPaths =
                            new List<string> { "Assets/Game/Scripts/GoalTrigger.cs" },
                        CanRunInParallel = true
                    });

                create(
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Create goal material",
                        Description = "Create Assets/Game/Materials/Goal.mat.",
                        AcceptanceCriteria = "The material exists.",
                        ProducesArtifactPaths =
                            new List<string> { "Assets/Game/Materials/Goal.mat" },
                        CanRunInParallel = true
                    });

                create(
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Add goal to scene",
                        Description =
                            "Add the Goal GameObject to Assets/Game/Scenes/Game.unity, attach " +
                            "Assets/Game/Scripts/GoalTrigger.cs, assign Assets/Game/Materials/Goal.mat, and save the scene.",
                        AcceptanceCriteria = "The saved scene contains the configured goal."
                    });

                object goal = tasks[4];
                CollectionAssert.AreEquivalent(
                    new[]
                    {
                        "Assets/Game/Scenes/Game.unity",
                        "Assets/Game/Scripts/GoalTrigger.cs",
                        "Assets/Game/Materials/Goal.mat"
                    },
                    (ICollection)taskType.GetField("ConsumedArtifactPaths").GetValue(goal));
                CollectionAssert.AreEqual(
                    new[] { "scene:Assets/Game/Scenes/Game.unity" },
                    (ICollection)taskType.GetField("MutatedResourceKeys").GetValue(goal));
                Assert.AreEqual(
                    3,
                    ((ICollection)taskType.GetField("DependsOnTaskIds").GetValue(goal)).Count,
                    "Recovered ownership must immediately rebuild producer dependencies.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void DependencyNormalization_KeepsFinalVerifierAsGraphSink()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                tasks.Add(parent);

                object scene = Activator.CreateInstance(taskType);
                SetTaskField(taskType, scene, "Id", "scene");
                SetTaskField(taskType, scene, "Title", "Create scene");
                SetTaskField(taskType, scene, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    scene,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Scenes/Game.unity" });
                SetTaskField(
                    taskType,
                    scene,
                    "MutatedResourceKeys",
                    new List<string> { "scene:Assets/Game/Scenes/Game.unity" });
                tasks.Add(scene);

                object final = Activator.CreateInstance(taskType);
                SetTaskField(taskType, final, "Id", "final");
                SetTaskField(taskType, final, "Title", "Final integrated verification");
                SetTaskField(taskType, final, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    final,
                    "ConsumedArtifactPaths",
                    new List<string> { "Assets/Game/Scenes/Game.unity" });
                SetTaskField(
                    taskType,
                    final,
                    "MutatedResourceKeys",
                    new List<string> { "scene:Assets/Game/Scenes/Game.unity" });
                tasks.Add(final);

                object laterWriter = Activator.CreateInstance(taskType);
                SetTaskField(taskType, laterWriter, "Id", "later");
                SetTaskField(taskType, laterWriter, "Title", "Add later scene outcome");
                SetTaskField(taskType, laterWriter, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    laterWriter,
                    "ConsumedArtifactPaths",
                    new List<string> { "Assets/Game/Scenes/Game.unity" });
                SetTaskField(
                    taskType,
                    laterWriter,
                    "MutatedResourceKeys",
                    new List<string> { "scene:Assets/Game/Scenes/Game.unity" });
                tasks.Add(laterWriter);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo refresh = windowType.GetMethod(
                    "RefreshDependencyTaskIds",
                    BindingFlags.NonPublic | BindingFlags.Instance);
                refresh.Invoke(window, new[] { parent });

                ICollection laterDependencies =
                    (ICollection)taskType.GetField("DependsOnTaskIds").GetValue(laterWriter);
                ICollection finalDependencies =
                    (ICollection)taskType.GetField("DependsOnTaskIds").GetValue(final);

                CollectionAssert.DoesNotContain(laterDependencies, "final");
                CollectionAssert.AreEquivalent(
                    new[] { "scene", "later" },
                    finalDependencies,
                    "The final verifier must join every sibling, including outcomes appended by corrective rework.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void DependencyNormalization_DoesNotTreatReferencesToFinalVerifierAsRole()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                tasks.Add(parent);

                object compile = Activator.CreateInstance(taskType);
                SetTaskField(taskType, compile, "Id", "compile");
                SetTaskField(taskType, compile, "Title", "Compile check of four scripts");
                SetTaskField(taskType, compile, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    compile,
                    "Description",
                    "This child is independent. Its evidence will be consumed by the final integration-verification child.");
                SetTaskField(
                    taskType,
                    compile,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Docs/CompileCheck.md" });
                tasks.Add(compile);

                object scan = Activator.CreateInstance(taskType);
                SetTaskField(taskType, scan, "Id", "scan");
                SetTaskField(taskType, scan, "Title", "Asset-reference grep scan");
                SetTaskField(taskType, scan, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    scan,
                    "Description",
                    "Do not write the final report artifact owned by the final integration-verification child.");
                SetTaskField(
                    taskType,
                    scan,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Docs/AssetScan.md" });
                tasks.Add(scan);

                object final = Activator.CreateInstance(taskType);
                SetTaskField(taskType, final, "Id", "final");
                SetTaskField(taskType, final, "Title", "Final integration verification and report");
                SetTaskField(taskType, final, "ParentTaskId", "parent");
                SetTaskField(
                    taskType,
                    final,
                    "Description",
                    "Depends on sibling tasks \"Compile check of four scripts\", \"Asset-reference grep scan\" being Done.");
                SetTaskField(
                    taskType,
                    final,
                    "ConsumedArtifactPaths",
                    new List<string>
                    {
                        "Assets/Game/Docs/CompileCheck.md",
                        "Assets/Game/Docs/AssetScan.md"
                    });
                tasks.Add(final);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo refresh = windowType.GetMethod(
                    "RefreshDependencyTaskIds",
                    BindingFlags.NonPublic | BindingFlags.Instance);
                refresh.Invoke(window, new[] { parent });

                ICollection compileDependencies =
                    (ICollection)taskType.GetField("DependsOnTaskIds").GetValue(compile);
                ICollection scanDependencies =
                    (ICollection)taskType.GetField("DependsOnTaskIds").GetValue(scan);
                ICollection finalDependencies =
                    (ICollection)taskType.GetField("DependsOnTaskIds").GetValue(final);

                CollectionAssert.IsEmpty(
                    compileDependencies,
                    "A producer must not become the graph sink merely because it refers to the final verifier.");
                CollectionAssert.IsEmpty(
                    scanDependencies,
                    "A scan child must remain independent when it only names the downstream final verifier.");
                CollectionAssert.AreEquivalent(
                    new[] { "compile", "scan" },
                    finalDependencies,
                    "The actual final verifier must remain the unique graph sink.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ForwardDependencies_BackfillStableTaskIdsAfterPrerequisiteCreation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Parent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Dependent",
                        Description = "Depends on sibling task \"Prerequisite\" being Done. Complete dependent work.",
                        AcceptanceCriteria = "Dependent work passes.",
                        DependsOnSiblingTitle = "Prerequisite",
                        CanRunInParallel = true
                    }
                });

                object dependent = tasks[1];
                IList unresolvedIds =
                    (IList)taskType.GetField("DependsOnTaskIds")
                        .GetValue(dependent);
                Assert.AreEqual(0, unresolvedIds.Count);

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Prerequisite",
                        Description = "Complete prerequisite work.",
                        AcceptanceCriteria = "Prerequisite passes.",
                        CanRunInParallel = true
                    }
                });

                IList resolvedIds =
                    (IList)taskType.GetField("DependsOnTaskIds")
                        .GetValue(dependent);
                Assert.AreEqual(1, resolvedIds.Count);
                Assert.AreEqual(
                    taskType.GetField("Id").GetValue(tasks[2]),
                    resolvedIds[0]);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ArtifactConsumers_BackfillProducerDependenciesFromStructuredPaths()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Make Game");
                SetTaskField(taskType, parent, "AssignedAgent", "DeveloperAgent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Build ground and three platforms",
                        Description = "Create the level route in Assets/JumpingBallGame/Scenes/JumpingBall.unity.",
                        AcceptanceCriteria = "The route is saved.",
                        ProducesArtifactPaths = new List<string>
                        {
                            "Assets/JumpingBallGame/Scenes/JumpingBall.unity"
                        },
                        MutatesResourceKeys = new List<string>
                        {
                            "scene:Assets/JumpingBallGame/Scenes/JumpingBall.unity"
                        }
                    }
                });

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Build goal trigger and follow camera",
                        Description = "Attach the goal and camera scripts in Assets/JumpingBallGame/Scenes/JumpingBall.unity.",
                        AcceptanceCriteria = "The goal and camera are saved.",
                        DependsOnSiblingTitles = new List<string>
                        {
                            "Build ground and three platforms"
                        },
                        ConsumesArtifactPaths = new List<string>
                        {
                            "Assets/JumpingBallGame/Scripts/GoalTrigger.cs",
                            "Assets/JumpingBallGame/Scenes/JumpingBall.unity"
                        },
                        MutatesResourceKeys = new List<string>
                        {
                            "scene:Assets/JumpingBallGame/Scenes/JumpingBall.unity"
                        }
                    }
                });

                object consumer = tasks[2];
                IList dependenciesBeforeProducer =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(consumer);
                Assert.AreEqual(1, dependenciesBeforeProducer.Count);

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    new CreateKanbanChildTaskArguments
                    {
                        Title = "Create GoalTrigger script",
                        Description = "Create Assets/JumpingBallGame/Scripts/GoalTrigger.cs.",
                        AcceptanceCriteria = "GoalTrigger.cs exists.",
                        CanRunInParallel = true,
                        ProducesArtifactPaths = new List<string>
                        {
                            "Assets/JumpingBallGame/Scripts/GoalTrigger.cs"
                        }
                    }
                });

                IList dependenciesAfterProducer =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(consumer);
                Assert.AreEqual(2, dependenciesAfterProducer.Count);
                CollectionAssert.Contains(
                    dependenciesAfterProducer,
                    taskType.GetField("Id").GetValue(tasks[3]));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void DuplicateSceneProducerDeclarations_DoNotCreateReverseDependency()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                SetTaskField(taskType, parent, "Title", "Make Game");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Func<string, string, CreateKanbanChildTaskArguments> sceneCard =
                    (title, description) =>
                        new CreateKanbanChildTaskArguments
                        {
                            Title = title,
                            Description = description,
                            AcceptanceCriteria = "The scene change is saved.",
                            ProducesArtifactPaths = new List<string>
                            {
                                "Assets/Game/Scenes/Game.unity"
                            },
                            ConsumesArtifactPaths = new List<string>
                            {
                                "Assets/Game/Scenes/Game.unity"
                            },
                            MutatesResourceKeys = new List<string>
                            {
                                "scene:Assets/Game/Scenes/Game.unity"
                            }
                        };

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    sceneCard(
                        "Build ground surface",
                        "Create the ground surface in Assets/Game/Scenes/Game.unity.")
                });

                CreateKanbanChildTaskArguments platforms =
                    sceneCard(
                        "Build three platforms in ascending route",
                        "Create three platforms in Assets/Game/Scenes/Game.unity.");
                platforms.DependsOnSiblingTitles =
                    new List<string> { "Build ground surface" };

                createChild.Invoke(window, new object[]
                {
                    "parent",
                    platforms
                });

                object ground = tasks[1];
                object platform = tasks[2];
                IList groundDependencies =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(ground);
                IList platformDependencies =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(platform);

                Assert.AreEqual(0, groundDependencies.Count);
                Assert.AreEqual(1, platformDependencies.Count);
                Assert.AreEqual(
                    taskType.GetField("Id").GetValue(ground),
                    platformDependencies[0]);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void SharedMutableResources_AutoChainInStableOrder()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                SetTaskField(taskType, parent, "Id", "parent");
                tasks.Add(parent);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo createChild = windowType.GetMethod(
                    "CreateAgentChildTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                foreach (string title in new[] { "Configure player", "Configure level" })
                {
                    createChild.Invoke(window, new object[]
                    {
                        "parent",
                        new CreateKanbanChildTaskArguments
                        {
                            Title = title,
                            Description = "Update the shared feature configuration.",
                            AcceptanceCriteria = "The focused configuration is persisted.",
                            CanRunInParallel = true,
                            MutatesResourceKeys = new List<string>
                            {
                                "config:JumpingBallGame"
                            }
                        }
                    });
                }

                object first = tasks[1];
                object second = tasks[2];
                IList firstDependencies =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(first);
                IList secondDependencies =
                    (IList)taskType.GetField("DependsOnTaskIds").GetValue(second);

                Assert.AreEqual(0, firstDependencies.Count);
                Assert.AreEqual(1, secondDependencies.Count);
                Assert.AreEqual(
                    taskType.GetField("Id").GetValue(first),
                    secondDependencies[0]);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        private static void SetTaskField(
            Type taskType,
            object task,
            string name,
            object value)
        {
            taskType.GetField(name).SetValue(task, value);
        }


        private static object GetTaskField(
            Type taskType,
            object task,
            string name)
        {
            return taskType.GetField(name).GetValue(task);
        }


        [Test]
        public void ActiveKanbanAgentCard_IsGreen()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanAgentsWindow)
                    .GetMethod(
                        "GetAgentCardColor",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.NotNull(
                method);

            Color active =
                (Color)method.Invoke(
                    null,
                    new object[]
                    {
                        true
                    });

            Color idle =
                (Color)method.Invoke(
                    null,
                    new object[]
                    {
                        false
                    });

            Assert.Greater(
                active.g,
                active.r,
                "The active agent card should use a green tint.");

            Assert.AreEqual(
                Color.white,
                idle);
        }


        [Test]
        public void ComponentInspectionRetry_RequiresTheMissingToolSequence()
        {
            string prompt =
                Invoke<string>(
                    "BuildComponentInspectionRetryPrompt",
                    "Verify capability: inspect_editor_component",
                    "Inspect the selected GameObject's Collider component.");

            StringAssert.Contains(
                "previous response cannot complete",
                prompt);

            StringAssert.Contains(
                "Call get_editor_selection with includeComponents=true",
                prompt);

            StringAssert.Contains(
                "Call inspect_editor_component",
                prompt);

            StringAssert.Contains(
                "Do not provide a final answer until inspect_editor_component completes",
                prompt);
        }


        [Test]
        public void ColliderTroubleshootingReview_ReceivesAuthoritativeInspection()
        {
            const string request =
                "The selected object's collider is not working. Please troubleshoot it.";

            const string inspection =
                "{\"Diagnostics\":[{\"Severity\":\"Error\",\"Code\":\"MeshColliderMissingMesh\",\"Message\":\"MeshCollider has no Shared Mesh assigned.\"}]}";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Collider troubleshooting",
                    request,
                    string.Empty,
                    "The MeshCollider has no mesh assigned.",
                    string.Empty,
                    null,
                    null,
                    null,
                    null,
                    inspection,
                    request);

            StringAssert.Contains(
                "authoritative inspect_editor_component result",
                prompt);

            StringAssert.Contains(
                "MeshColliderMissingMesh",
                prompt);

            StringAssert.Contains(
                "Generic troubleshooting without the concrete inspection evidence is a FAIL",
                prompt);

            StringAssert.Contains(
                "provides no authoritative source mesh",
                prompt);

            StringAssert.Contains(
                "Do not fail solely because no mutation occurred",
                prompt);
        }


        [Test]
        public void ColliderTroubleshootingPrompt_DoesNotInventMissingMesh()
        {
            const string request =
                "Inspect the selected collider and fix it if possible, otherwise give me options.";

            const string inspection =
                "{\"Components\":[{\"Type\":\"UnityEngine.MeshCollider\"}]," +
                "\"Diagnostics\":[{\"Severity\":\"Error\",\"Code\":\"MeshColliderMissingMesh\"}]}";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Collider troubleshooting",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    inspection);

            StringAssert.Contains(
                "do not invent or arbitrarily select one",
                prompt);

            StringAssert.Contains(
                "automatic fix is unsafe",
                prompt);

            StringAssert.Contains(
                "replacing the MeshCollider with an appropriate primitive collider",
                prompt);
        }


        [Test]
        public void ComponentFixRequest_EnablesComponentMutationPath()
        {
            bool requestsFix =
                Invoke<bool>(
                    "RequestsComponentFix",
                    "Inspect the collider and fix it if possible.");

            bool inspectionOnly =
                Invoke<bool>(
                    "RequestsComponentFix",
                    "Inspect the collider and report its settings.");

            Assert.IsTrue(
                requestsFix);

            Assert.IsFalse(
                inspectionOnly);
        }


        [Test]
        public void MissingReviewSelection_ProducesClearFailVerdict()
        {
            string review =
                Invoke<string>(
                    "BuildUnverifiableSelectionReview",
                    "The current selection contains no GameObject target.");

            StringAssert.StartsWith(
                "KANBAN_REVIEW: FAIL",
                review);

            StringAssert.Contains(
                "has not been treated as fresh review evidence",
                review);

            StringAssert.Contains(
                "Select exactly one target GameObject",
                review);
        }


        [Test]
        public void RemovedComponentVerification_UsesFreshGameObjectInspection()
        {
            const string mutation =
                "{\"Succeeded\":true,\"GameObjectInstanceId\":42," +
                "\"GameObjectName\":\"Disposable\",\"ComponentInstanceId\":84," +
                "\"ComponentType\":\"UnityEngine.BoxCollider\",\"AffectedCount\":1," +
                "\"AffectedGameObjects\":[\"Disposable\"],\"VerificationSucceeded\":true}";

            const string inspection =
                "{\"GameObjectInstanceId\":42,\"Components\":[" +
                "{\"InstanceId\":43,\"Type\":\"UnityEngine.Transform\"}]}";

            string verification =
                Invoke<string>(
                    "ValidateRemovedComponentInspection",
                    mutation,
                    inspection);

            StringAssert.Contains(
                "AffectedCount=1",
                verification);

            StringAssert.Contains(
                "instance ID 84 is absent",
                verification);

            StringAssert.Contains(
                "UnityEngine.Transform remains",
                verification);
        }


        [Test]
        public void RemovedComponentVerification_RejectsStaleRemovedInstance()
        {
            const string mutation =
                "{\"Succeeded\":true,\"GameObjectInstanceId\":42," +
                "\"GameObjectName\":\"Disposable\",\"ComponentInstanceId\":84," +
                "\"ComponentType\":\"UnityEngine.BoxCollider\",\"AffectedCount\":1," +
                "\"AffectedGameObjects\":[\"Disposable\"],\"VerificationSucceeded\":true}";

            const string staleInspection =
                "{\"GameObjectInstanceId\":42,\"Components\":[" +
                "{\"InstanceId\":43,\"Type\":\"UnityEngine.Transform\"}," +
                "{\"InstanceId\":84,\"Type\":\"UnityEngine.BoxCollider\"}]}";

            Assert.Throws<InvalidOperationException>(
                () =>
                    Invoke<string>(
                        "ValidateRemovedComponentInspection",
                        mutation,
                        staleInspection));
        }


        [Test]
        public void DirectoryTreeContract_UsesOnlyAuthoritativePaths()
        {
            const string toolResult =
                "{\"Entries\":[" +
                "{\"Path\":\"Assets/Runtime/Docs/ARCHITECTURE.md\"}," +
                "{\"Path\":\"Assets/Runtime/Docs/FolderTest\"}]}";

            string output =
                Invoke<string>(
                    "EnforceDirectoryTreeOutputContract",
                    toolResult,
                    "Call list_directory_tree and exclude .meta files.");

            Assert.AreEqual(
                "Assets/Runtime/Docs/ARCHITECTURE.md\n" +
                "Assets/Runtime/Docs/FolderTest",
                output);
        }


        [Test]
        public void DirectoryTreeContract_RejectsForbiddenMetaEvidence()
        {
            const string toolResult =
                "{\"Entries\":[" +
                "{\"Path\":\"Assets/Runtime/Docs/ARCHITECTURE.md.meta\"}]}";

            Assert.Throws<InvalidOperationException>(
                () =>
                    Invoke<string>(
                        "EnforceDirectoryTreeOutputContract",
                        toolResult,
                        "Call list_directory_tree with includeMetaFiles=false."));
        }


        [Test]
        public void CapabilityVerification_ExtractsRequiredToolName()
        {
            string capability =
                Invoke<string>(
                    "ExtractVerifiedCapabilityName",
                    "Verify capability: get_console_logs\nCall it with a bounded result count.");

            Assert.AreEqual(
                "get_console_logs",
                capability);
        }


        [Test]
        public void CapabilityVerificationPrompt_SaysToolIsAvailable()
        {
            const string request =
                "Verify capability: get_console_logs";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Verify capability: get_console_logs",
                    "Call get_console_logs.",
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.Contains(
                "Call get_console_logs",
                prompt);

            StringAssert.Contains(
                "available for this run",
                prompt);
        }


        [Test]
        public void AppendVerification_UsesRequiredContentField()
        {
            const string request =
                "Verify capability: append_file\n" +
                "Call append_file on \"Assets/Test/capability_test.txt\" with exactly \"line two\". Then read the file.\n" +
                "The original text (\"line one\") is preserved and read_file confirms \"line one\" followed by \"line two\".";

            string content =
                Invoke<string>(
                    "ExtractExactAppendContent",
                    request);

            Assert.AreEqual(
                "line two",
                content);

            Assert.AreEqual(
                "line one",
                Invoke<string>(
                    "ExtractAppendBaselineContent",
                    request));

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Verify capability: append_file",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.Contains(
                "\"content\":\"line two\"",
                prompt);

            StringAssert.Contains(
                "never text, value, data, or an omitted argument",
                prompt);

            StringAssert.Contains(
                "reset the target",
                prompt);

            StringAssert.Contains(
                "Do not call write_file or read_file yourself",
                prompt);

            Assert.AreEqual(
                1,
                Invoke<int>(
                    "CountExactOccurrences",
                    "line one\nline two",
                    "line two"));

            Assert.IsTrue(
                Invoke<bool>(
                    "IsRepeatedCapabilityLoopFailure",
                    "The provider repeated the identical capability call 'append_file' after its result was already returned."));
        }


        [Test]
        public void CopyVerification_ResolvesMalformedSourceAndOwnsContentReads()
        {
            const string request =
                "Verify capability: copy_file\n" +
                "Copy \"Assets/  /capability_test.txt\" to " +
                "\"Assets/AIRuntimeCapabilityVerification/capability_test_copy.txt\" using copy_file, then read both files.";

            string workspaceRoot = Path.Combine(Path.GetTempPath(), "AIRuntimeCopyTest", Guid.NewGuid().ToString("N"));
            string fixtureFolder = Path.Combine(workspaceRoot, "Assets/AIRuntimeCapabilityVerification");
            Directory.CreateDirectory(fixtureFolder);
            File.WriteAllText(Path.Combine(fixtureFolder, "capability_test.txt"), "copy fixture");
            try
            {

            MethodInfo resolve =
                _runnerType.GetMethod(
                    "ResolveCopyVerificationPaths",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                resolve);

            object paths =
                resolve.Invoke(
                    null,
                    new object[]
                    {
                        request,
                        workspaceRoot
                    });

            Type pathsType =
                paths.GetType();

            Assert.AreEqual(
                "Assets/AIRuntimeCapabilityVerification/capability_test.txt",
                pathsType.GetField("SourcePath").GetValue(paths));

            Assert.AreEqual(
                "Assets/AIRuntimeCapabilityVerification/capability_test_copy.txt",
                pathsType.GetField("DestinationPath").GetValue(paths));

            MethodInfo appendEvidence =
                _runnerType.GetMethod(
                    "AppendCopyVerificationEvidence",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                appendEvidence);

            string prompt =
                (string)appendEvidence.Invoke(
                    null,
                    new[]
                    {
                        "Complete the copy verification.",
                        paths,
                        null,
                        (object)false
                    });

            StringAssert.Contains(
                "\"sourcePath\":\"Assets/AIRuntimeCapabilityVerification/capability_test.txt\"",
                prompt);

            StringAssert.Contains(
                "\"destinationPath\":\"Assets/AIRuntimeCapabilityVerification/capability_test_copy.txt\"",
                prompt);

            StringAssert.Contains(
                "Do not call read_file yourself",
                prompt);
            }
            finally { Directory.Delete(workspaceRoot, true); }
        }


        [Test]
        public void AppendVerification_RetriesOnlyMissingContentValidation()
        {
            bool safeToRetry =
                Invoke<bool>(
                    "IsMissingAppendContentFailure",
                    "Content is required.");

            bool unsafeToRetry =
                Invoke<bool>(
                    "IsMissingAppendContentFailure",
                    "Provider timed out after mutation.");

            Assert.IsTrue(
                safeToRetry);

            Assert.IsFalse(
                unsafeToRetry);
        }


        [TestCase("NVIDIA")]
        [TestCase("Nvidia")]
        [TestCase("NVIDIA Developer")]
        [TestCase("NvidiaDeveloper")]
        [TestCase("  NVIDIA  ")]
        public void SavedNvidiaProviderAliases_NormalizeToCanonicalName(
            string savedProvider)
        {
            Assert.AreEqual(
                "NvidiaDeveloper",
                AIRuntimeEditorPluginCatalog.NormalizeProviderId(
                    savedProvider));
        }


        [Test]
        public void UnknownSavedProvider_IsNotSilentlyChanged()
        {
            Assert.IsFalse(
                AIRuntimeEditorPluginCatalog.TryGetProviderPlugin(
                    "  UnknownProvider  ",
                    out _));
        }


        [Test]
        public void ReviewVerdict_IsRecognizedAfterReviewHeading()
        {
            Assert.IsTrue(
                InvokeWindowVerdict(
                    "## Review\n\nEvidence checked.\n\nKANBAN_REVIEW: PASS\n\nNo missing work.",
                    "PASS"));
        }


        [Test]
        public void ReviewNormalization_HonorsFailVerdictAfterIntroductoryProse()
        {
            const string response =
                "Now let me verify the numeric claims.\n\n" +
                "KANBAN_REVIEW: FAIL\n\n" +
                "The Platform_1 to Platform_2 gap is 3.0 units, not 2.0 units.";

            string result =
                Invoke<string>(
                    "EnsureReviewVerdict",
                    response,
                    "{\"Objects\":[{\"GameObjectInstanceId\":1,\"HasMissingComponents\":false,\"MissingComponentCount\":0}]}",
                    null,
                    string.Empty,
                    false,
                    null,
                    null,
                    false,
                    string.Empty,
                    null);

            StringAssert.StartsWith(
                "KANBAN_REVIEW: FAIL",
                result);

            StringAssert.DoesNotContain(
                "KANBAN_REVIEW: PASS",
                result);

            StringAssert.Contains(
                "gap is 3.0 units",
                result);
        }


        [Test]
        public void ScopedKanbanEvidence_DoesNotRequestASecondContextCall()
        {
            string prompt =
                Invoke<string>(
                    "AppendKanbanTaskContextEvidence",
                    "Complete the task.",
                    "{\"TaskId\":\"task-1\"}",
                    false);

            StringAssert.Contains(
                "already called get_kanban_task_context",
                prompt);
            StringAssert.Contains(
                "Do not call it again",
                prompt);
        }


        [Test]
        public void ContextCapabilityVerification_UsesRunnerOwnedSnapshot()
        {
            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Verify capability: get_kanban_task_context",
                    "Verify the scoped task context.",
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    "Verify capability: get_kanban_task_context\nVerify the scoped task context.",
                    null);

            StringAssert.Contains(
                "AIRuntime calls get_kanban_task_context",
                prompt);
            StringAssert.Contains(
                "do not call the capability again",
                prompt);
        }


        [Test]
        public void ReviewVerdict_RejectsConflictingMarkers()
        {
            Assert.IsFalse(
                InvokeWindowVerdict(
                    "KANBAN_REVIEW: PASS\n\nKANBAN_REVIEW: FAIL",
                    "PASS"));
        }


        [Test]
        public void StandaloneMaterialReview_UsesAssetEvidenceNotComponentFailure()
        {
            const string request =
                "Create a material at Assets/ExampleProject/Materials/Accent.mat with a non-default blue color.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresMaterialAssetReviewEvidence",
                    request));

            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneMaterialAssetReview",
                    true,
                    request));

            string reviewPrompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Create accent material",
                    request,
                    "The material exists and has a non-default color.",
                    "Completed.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    request);

            StringAssert.DoesNotContain(
                "component inspection evidence is unavailable",
                reviewPrompt);

            string evidencePrompt =
                Invoke<string>(
                    "AppendMaterialAssetReviewEvidence",
                    reviewPrompt,
                    "{\"Materials\":[{\"AssetPath\":\"Assets/ExampleProject/Materials/Accent.mat\",\"Exists\":true,\"ObjectType\":\"UnityEngine.Material\",\"HasNonDefaultColor\":true}]}",
                    true);

            StringAssert.Contains(
                "authoritative persistent-asset evidence",
                evidencePrompt);
            StringAssert.Contains(
                "HasNonDefaultColor",
                evidencePrompt);
        }


        [Test]
        public void MaterialReview_InfersNamedAssetsInsideExplicitMaterialsDirectory()
        {
            const string request =
                "Create visible materials under Assets/JumpingBallGame/Materials: " +
                "BallMaterial, GroundMaterial, PlatformMaterial, GoalMaterial. " +
                "These are asset-only deliverables; do not attach them to scene GameObjects.";

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ExtractRequestedMaterialAssetPaths",
                    request);

            CollectionAssert.AreEquivalent(
                new[]
                {
                    "Assets/JumpingBallGame/Materials/BallMaterial.mat",
                    "Assets/JumpingBallGame/Materials/GroundMaterial.mat",
                    "Assets/JumpingBallGame/Materials/PlatformMaterial.mat",
                    "Assets/JumpingBallGame/Materials/GoalMaterial.mat"
                },
                paths);

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresMaterialAssetReviewEvidence",
                    request));
            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneMaterialAssetReview",
                    true,
                    request));
        }


        [Test]
        public void MaterialAppliedToGameObject_IsNotStandaloneAssetReview()
        {
            const string request =
                "Create a Goal GameObject with a MeshRenderer and apply Assets/ExampleProject/Materials/Goal.mat.";

            Assert.IsFalse(
                Invoke<bool>(
                    "IsStandaloneMaterialAssetReview",
                    true,
                    request));
        }


        [Test]
        public void MaterialProducer_FutureConsumerSceneDoesNotTriggerSceneInspection()
        {
            const string request =
                "Create Ball Material Asset\n" +
                "Create a ball material asset at Assets/JumpingBallGame/Materials/BallMaterial.mat. " +
                "Give it a distinct visible color using a Standard shader. " +
                "This material is applied to the player ball sphere in the scene " +
                "Assets/JumpingBallGame/Scenes/JumpingBallGameScene.unity. " +
                "Shared identities: Assets/JumpingBallGame/Materials/BallMaterial.mat and " +
                "Assets/JumpingBallGame/Scenes/JumpingBallGameScene.unity.\n" +
                "Assets/JumpingBallGame/Materials/BallMaterial.mat exists with a visible Standard-shader color.";

            Assert.IsTrue(
                Invoke<bool>(
                    "IsStandaloneMaterialAssetReview",
                    true,
                    request));

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresSceneInspectionEvidence",
                    true,
                    request),
                "A future consumer scene path must not become a material-card review deliverable.");
        }


        [Test]
        public void SceneMutationContract_StillRequiresSceneInspection()
        {
            const string request =
                "In Assets/JumpingBallGame/Scenes/JumpingBallGameScene.unity, " +
                "create a PlayerBall GameObject and assign BallMaterial to its MeshRenderer. " +
                "The saved scene contains PlayerBall.";

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresSceneInspectionEvidence",
                    true,
                    request));
        }


        [Test]
        public void PersistedSceneSave_IsAuthoritativeWithoutLaterInspection()
        {
            var saveResult =
                Newtonsoft.Json.Linq.JObject.Parse(
                    "{\"Succeeded\":true,\"Saved\":true,\"DirtyAfterSave\":false," +
                    "\"PersistenceVerified\":true,\"PersistedGameObjectPaths\":[\"World/HoverDrone\"]}");

            Assert.IsTrue(
                Invoke<bool>(
                    "SaveResultVerifiesPersistedGameObject",
                    saveResult,
                    "HoverDrone"));

            saveResult["DirtyAfterSave"] = true;

            Assert.IsFalse(
                Invoke<bool>(
                    "SaveResultVerifiesPersistedGameObject",
                    saveResult,
                    "HoverDrone"));
        }


        [Test]
        public void CoordinationParent_RollsUpPassedFinalVerificationEvidence()
        {
            Type windowType =
                typeof(AIRuntimeKanbanSession);
            Type taskType =
                windowType.GetNestedType(
                    "KanbanTask",
                    BindingFlags.NonPublic);
            Type boardType =
                windowType.GetNestedType(
                    "KanbanBoard",
                    BindingFlags.NonPublic);
            Type statusType =
                windowType.GetNestedType(
                    "TaskStatus",
                    BindingFlags.NonPublic);

            object parent = Activator.CreateInstance(taskType);
            object verifier = Activator.CreateInstance(taskType);

            SetTaskField(taskType, parent, "Id", "parent");
            SetTaskField(taskType, parent, "Title", "Make Game");
            SetTaskField(
                taskType,
                parent,
                "AcceptanceCriteria",
                "The completed game is playable.");

            SetTaskField(taskType, verifier, "Id", "verify");
            SetTaskField(taskType, verifier, "ParentTaskId", "parent");
            SetTaskField(
                taskType,
                verifier,
                "Title",
                "Final integrated verification");
            SetTaskField(
                taskType,
                verifier,
                "AcceptanceCriteria",
                "Verify all acceptance criteria from the parent.");
            SetTaskField(
                taskType,
                verifier,
                "Status",
                Enum.Parse(statusType, "Done"));
            SetTaskField(taskType, verifier, "ReviewStatus", "Passed");
            SetTaskField(taskType, verifier, "ReviewEvidence", "{\"Scene\":{\"Dirty\":false}}");
            SetTaskField(taskType, verifier, "ReviewResult", "KANBAN_REVIEW: PASS");
            SetTaskField(taskType, verifier, "UpdatedAt", "2026-08-14T00:00:00Z");

            object board = Activator.CreateInstance(boardType);
            IList tasks =
                (IList)boardType.GetField("Tasks").GetValue(board);
            tasks.Add(parent);
            tasks.Add(verifier);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo method =
                    windowType.GetMethod(
                        "TryBuildCoordinationParentReviewPass",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance);

                object[] arguments = { parent, null, null };

                Assert.IsTrue(
                    (bool)method.Invoke(window, arguments));
                StringAssert.StartsWith(
                    "KANBAN_REVIEW: PASS",
                    (string)arguments[1]);
                StringAssert.Contains(
                    "ReviewEvidence",
                    (string)arguments[2]);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ReviewFailureClassification_RoutesEvidenceWithoutCorrectiveChildren()
        {
            MethodInfo classify =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "ClassifyReviewFailure",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.AreEqual(
                "EvidenceGap",
                classify.Invoke(
                    null,
                    new object[]
                    {
                        "KANBAN_REVIEW: FAIL\nComponent inspection evidence is unavailable.",
                        string.Empty
                    }));

            Assert.AreEqual(
                "EvidenceGap",
                classify.Invoke(
                    null,
                    new object[]
                    {
                        "KANBAN_REVIEW: FAIL\nThe component inspection evidence is unavailable, so the verdict must be FAIL regardless of file evidence. The governing review rule mandates FAIL.",
                        string.Empty
                    }),
                "AIRuntime's own missing-evidence directive must never be mistaken for an implementation defect merely because it contains 'must be FAIL'.");

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresNewCorrectiveChildren",
                    "[ReviewFailureKind: EvidenceGap] The first review requested rework.\nMissing inspection."));

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresNewCorrectiveChildren",
                    "[ReviewFailureKind: ImplementationDefect] The first review requested rework.\nCollider is wrong."));

            Assert.AreEqual(
                "ImplementationDefect",
                classify.Invoke(
                    null,
                    new object[]
                    {
                        "KANBAN_REVIEW: FAIL\nInspection evidence is unavailable. Missing/incorrect work: offset is private, not public.",
                        string.Empty
                    }),
                "A concrete implementation contradiction must outrank a secondary evidence gap.");

            Assert.AreEqual(
                "EvidenceGap",
                classify.Invoke(
                    null,
                    new object[]
                    {
                        "KANBAN_REVIEW: FAIL\nMissing or incorrect work:\n" +
                        "The required folder-existence evidence is missing and the workspace cannot be independently verified. " +
                        "This must be supported by authoritative evidence, but no evidence was captured.",
                        string.Empty
                    }),
                "A generic review heading must not turn an evidence-only failure into an implementation defect.");
        }


        [Test]
        public void ReviewReworkHandoff_CannotReplaceOriginalAcceptanceContract()
        {
            const string handoff =
                "[ReviewFailureKind: ImplementationDefect] The first review requested rework.\n" +
                "The component inspection evidence is unavailable, so the verdict must be FAIL.";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Create FallReset script",
                    "Create Assets/Game/FallReset.cs; do not mutate any scene.",
                    "The script exists and compiles.",
                    "Completed.",
                    handoff,
                    null,
                    null,
                    "Assets/Game/FallReset.cs",
                    "public sealed class FallReset : MonoBehaviour {}",
                    null,
                    "Create Assets/Game/FallReset.cs.\n[AIRUNTIME_COMPONENT_EVIDENCE_POLICY:SKIP]");

            StringAssert.Contains(
                "PRIOR REVIEW FINDINGS",
                prompt);
            StringAssert.Contains(
                "sole authority",
                prompt);
            StringAssert.DoesNotContain(
                "CURRENT ACTIVE REQUEST (this replaces",
                prompt);
        }


        [Test]
        public void GeneralDecomposition_RejectsMissingVerificationJoin()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("scene", "Build Scene", "Build Assets/Game/Scenes/Game.unity."),
                            GraphChild("persist", "Verify scene persistence", "Verify the saved scene.", "scene"),
                            GraphChild("compile", "Verify compilation", "Compile the scripts.", "scene"),
                            GraphChild("final", "Final integrated verification", "Perform final integrated verification.", "persist")
                        }
                };

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "ValidateDecompositionGraph",
                        context,
                        "Create a game through child cards."));

            StringAssert.Contains(
                "does not depend on every verification gate",
                exception.Message);
            StringAssert.Contains(
                "Verify compilation",
                exception.Message);
        }


        [Test]
        public void ExecutionSafetyGraph_AllowsReviewToJudgeSemanticCoverage()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("scene", "Build Scene", "Build Assets/Game/Scenes/Game.unity."),
                            GraphChild("gate", "Verify Controller", "Verify the controller.", "scene"),
                            GraphChild("final", "Final integrated verification", "Perform final integrated verification.", "scene")
                        }
                };

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionSafetyGraph",
                    context),
                "Semantic completeness belongs to descendant review and must not block a structurally safe batch from starting.");
        }


        [Test]
        public void ExecutionSafetyGraph_StillRejectsRealDependencyCycle()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("one", "First", "First outcome.", "two"),
                            GraphChild("two", "Second", "Second outcome.", "one")
                        }
                };

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "ValidateDecompositionSafetyGraph",
                        context));

            StringAssert.Contains("dependency cycle", exception.Message);
        }


        [Test]
        public void GeneralDecomposition_RejectsInformalDependencies()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("script", "Create Controller", "Create Assets/Game/Controller.cs."),
                            GraphChild("scene", "Build Scene", "Build Assets/Game/Scenes/Game.unity. This card depends on all scripts being Done.")
                        }
                };

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "ValidateDecompositionGraph",
                        context,
                        "Create a game through child cards."));

            StringAssert.Contains(
                "informal dependency",
                exception.Message);
        }


        [Test]
        public void GeneralDecomposition_RejectsSemanticSceneBundleWithoutNumbering()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild(
                                "scene",
                                "Build complete scene",
                                "Create the persisted scene at Assets/Game/Scenes/Game.unity. " +
                                "Create and configure the player object. " +
                                "Create and configure the goal object. " +
                                "Configure the camera. Save the scene.")
                        }
                };

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "ValidateDecompositionGraph",
                        context,
                        "Use separate cards for each scene-construction/configuration step."));

            StringAssert.Contains("semantic scene-construction", exception.Message);
        }


        [Test]
        public void GeneralDecomposition_AllowsOneTransactionalSceneOutcome()
        {
            JObject scene =
                GraphChild(
                    "scene",
                    "Construct feature scene",
                    "Create Assets/Game/Scenes/Game.unity. Create the player object. " +
                    "Create the goal object. Configure the camera. Save the scene.");

            scene["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            scene["ConsumedArtifactPaths"] = new JArray();
            scene["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            JObject context =
                new JObject
                {
                    ["DirectChildren"] = new JArray(scene)
                };

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    context,
                    "Use separate cards for each scene-construction/configuration step."));
        }


        [Test]
        public void StructuredArtifactGraph_RejectsUnorderedSharedMutation()
        {
            JObject first =
                GraphChild(
                    "one",
                    "Create scene object",
                    "Create an object in Assets/Game/Scenes/Game.unity.");
            JObject second =
                GraphChild(
                    "two",
                    "Configure scene object",
                    "Configure another object in Assets/Game/Scenes/Game.unity.");

            first["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");
            second["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            JObject context =
                new JObject
                {
                    ["DirectChildren"] = new JArray(first, second)
                };

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "ValidateDecompositionGraph",
                        context,
                        "Create a feature through child cards."));

            StringAssert.Contains("Shared mutable resource", exception.Message);
        }


        [Test]
        public void StructuredArtifactGraph_UsesFirstSceneProducerAsCanonicalOwner()
        {
            JObject ground =
                GraphChild(
                    "ground",
                    "Build ground surface",
                    "Create the ground in Assets/Game/Scenes/Game.unity.");
            ground["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            ground["ConsumedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            ground["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            JObject platforms =
                GraphChild(
                    "platforms",
                    "Build three platforms in ascending route",
                    "Create three platforms in Assets/Game/Scenes/Game.unity.",
                    "ground");
            platforms["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            platforms["ConsumedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            platforms["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    new JObject
                    {
                        ["DirectChildren"] =
                            new JArray(ground, platforms)
                    },
                    "Create a feature through child cards."));
        }


        [Test]
        public void StructuredMaterialOwnership_DoesNotCreateWordBasedCrossDependencies()
        {
            JObject groundMaterial =
                GraphChild(
                    "ground",
                    "Create Ground Material Asset",
                    "Create Assets/Game/Materials/GroundMaterial.mat.");
            groundMaterial["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Materials/GroundMaterial.mat");
            groundMaterial["ConsumedArtifactPaths"] = new JArray();
            groundMaterial["MutatedResourceKeys"] = new JArray();

            JObject ballMaterial =
                GraphChild(
                    "ball",
                    "Create Ball Material Asset",
                    "Create Assets/Game/Materials/BallMaterial.mat.");
            ballMaterial["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Materials/BallMaterial.mat");
            ballMaterial["ConsumedArtifactPaths"] = new JArray();
            ballMaterial["MutatedResourceKeys"] = new JArray();

            JObject player =
                GraphChild(
                    "player",
                    "Build Player Ball GameObject",
                    "Create the player GameObject and assign BallMaterial in Assets/Game/Scenes/Game.unity.",
                    "ball");
            player["ProducedArtifactPaths"] = new JArray();
            player["ConsumedArtifactPaths"] =
                new JArray("Assets/Game/Materials/BallMaterial.mat");
            player["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray(groundMaterial, ballMaterial, player)
                };

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    context,
                    "Create a feature through child cards."));
        }


        [Test]
        public void StructuredMaterialConsumer_IsNotReclassifiedAsMaterialProducer()
        {
            JObject materials =
                GraphChild(
                    "materials",
                    "Create gameplay materials",
                    "Create Assets/Game/Materials/GroundMaterial.mat.");
            materials["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Materials/GroundMaterial.mat");
            materials["ConsumedArtifactPaths"] = new JArray();
            materials["MutatedResourceKeys"] = new JArray();

            JObject sceneOwner =
                GraphChild(
                    "scene",
                    "Create canonical scene",
                    "Create Assets/Game/Scenes/Game.unity after the gameplay materials exist.",
                    "materials");
            sceneOwner["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            sceneOwner["ConsumedArtifactPaths"] = new JArray();
            sceneOwner["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            JObject ground =
                GraphChild(
                    "ground",
                    "Build ground in scene",
                    "Create the ground and assign Assets/Game/Materials/GroundMaterial.mat in Assets/Game/Scenes/Game.unity.",
                    "scene",
                    "materials");
            ground["ProducedArtifactPaths"] = new JArray();
            ground["ConsumedArtifactPaths"] =
                new JArray(
                    "Assets/Game/Scenes/Game.unity",
                    "Assets/Game/Materials/GroundMaterial.mat");
            ground["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    new JObject
                    {
                        ["DirectChildren"] =
                            new JArray(materials, sceneOwner, ground)
                    },
                    "Create a feature through child cards."));
        }


        [Test]
        public void GeneralDecomposition_AllowsRedundantInformalDependencyWhenGraphIsStructured()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("script", "Create Controller", "Create Assets/Game/Controller.cs."),
                            GraphChild("scene", "Build Scene", "Build Assets/Game/Scenes/Game.unity. This card depends on all scripts being Done.", "script")
                        }
                };

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    context,
                    "Create a game through child cards."));
        }


        [Test]
        public void GeneralDecomposition_DoesNotTreatNegatedAssetScopeAsSceneAuthoring()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild(
                                "script",
                                "Write PlayerBall script",
                                "Create Assets/Game/Scripts/PlayerBall.cs. Implement a MonoBehaviour. Script/file-only: do NOT attach components or mutate any scene."),
                            GraphChild(
                                "material",
                                "Create ball material",
                                "Create Assets/Game/Materials/BallMaterial.mat. Asset-only: do NOT attach it to GameObjects or mutate the scene."),
                            GraphChild(
                                "scene",
                                "Build scene player",
                                "Build Assets/Game/Scenes/Game.unity and add the PlayerBall script and ball material to the player GameObject.",
                                "script",
                                "material")
                        }
                };

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    context,
                    "Create a game through child cards."));
        }


        [Test]
        public void GeneralDecomposition_DoesNotTreatStructuredRuntimeScriptAsSceneAuthoring()
        {
            JObject script =
                GraphChild(
                    "movement",
                    "Author player ball movement and jump script",
                    "Create Assets/JumpingBallGame/Scripts/BallController.cs as a MonoBehaviour. " +
                    "In Awake configure Rigidbody constraints with FreezePositionZ. " +
                    "Read horizontal input, apply movement force, and implement grounded jumping. " +
                    "This is file-only work and performs no scene mutation.");

            script["ProducedArtifactPaths"] =
                new JArray("Assets/JumpingBallGame/Scripts/BallController.cs");
            script["ConsumedArtifactPaths"] = new JArray();
            script["MutatedResourceKeys"] = new JArray();

            Assert.DoesNotThrow(
                () => Invoke<object>(
                    "ValidateDecompositionGraph",
                    new JObject
                    {
                        ["DirectChildren"] = new JArray(script)
                    },
                    "Use separate cards for each independently executable script or asset, " +
                    "and each scene-construction/configuration step."));
        }


        [Test]
        public void GeneralDecomposition_StillRequiresConsumptionWhenSceneAuthorExists()
        {
            JObject script =
                GraphChild(
                    "movement",
                    "Author movement script",
                    "Create Assets/Game/Scripts/BallController.cs as a MonoBehaviour. File-only work.");
            script["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Scripts/BallController.cs");
            script["ConsumedArtifactPaths"] = new JArray();
            script["MutatedResourceKeys"] = new JArray();

            JObject scene =
                GraphChild(
                    "scene",
                    "Build unrelated scene object",
                    "Create Ground in Assets/Game/Scenes/Game.unity.");
            scene["ProducedArtifactPaths"] =
                new JArray("Assets/Game/Scenes/Game.unity");
            scene["ConsumedArtifactPaths"] = new JArray();
            scene["MutatedResourceKeys"] =
                new JArray("scene:Assets/Game/Scenes/Game.unity");

            InvalidOperationException exception =
                Assert.Throws<InvalidOperationException>(
                    () => Invoke<object>(
                        "ValidateDecompositionGraph",
                        new JObject
                        {
                            ["DirectChildren"] = new JArray(script, scene)
                        },
                        "Create a game through child cards."));

            StringAssert.Contains(
                "no scene-authoring child consumes it",
                exception.Message);
        }


        [Test]
        public void CoordinationReviewPolicy_DoesNotDemandDisabledComponentEvidence()
        {
            const string request =
                "Make Game. PlayerBall has Rigidbody and Goal has trigger collider.\n" +
                "[AIRUNTIME_COMPONENT_EVIDENCE_POLICY:SKIP]";

            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Make Game",
                    request,
                    "All descendants pass.",
                    "Delegation complete.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    request);

            StringAssert.DoesNotContain(
                "component inspection evidence is unavailable",
                prompt);
        }


        [Test]
        public void PlayableGameDelegation_DoesNotInventPlayModeSmokeTest()
        {
            const string request =
                "Create a game with a player, camera, jumping movement, and a goal.";

            Assert.IsFalse(
                Invoke<bool>(
                    "RequiresPlayableGameSmokeTest",
                    request));

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Make Game",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.DoesNotContain(
                "MANDATORY PLAY MODE ASSEMBLY AND SMOKE-TEST CHAIN",
                prompt);
            StringAssert.Contains("IMPLEMENTATION PHASE BOUNDARY", prompt);
            StringAssert.DoesNotContain(
                "PLAY MODE TEST AUTHORING PREFLIGHT",
                prompt);

            Assert.IsTrue(
                Invoke<bool>(
                    "RequiresPlayableGameSmokeTest",
                "Create and run a PlayMode smoke test for the game."));
        }


        [Test]
        public void UnexecutedTaggedToolRequest_IsRecognizedAsIncompleteImplementation()
        {
            Assert.IsTrue(
                Invoke<bool>(
                    "ContainsUnexecutedToolMarkup",
                    "Need one more check.<tool_calls:x><tool_call:x>list_directory<tool_sep:x><arg_key:x>path</arg_key:x><arg_value:x>Assets/Game</arg_value:x></tool_call:x></tool_calls:x>"));

            Assert.IsFalse(
                Invoke<bool>(
                    "ContainsUnexecutedToolMarkup",
                    "All requested work is complete and verified."));
        }


        [Test]
        public void DirectParentReview_DoesNotTreatEmptyArtifactClaimsAsMissingDelivery()
        {
            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Build game",
                    "Create a playable game under Assets/Game.",
                    "The scene exists and plays.",
                    "Created and verified the scene.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    "Build game\nCreate a playable game under Assets/Game.\nThe scene exists and plays.");

            StringAssert.Contains(
                "an empty ProducedArtifactPaths list is not evidence that no files or scenes were created",
                prompt);
            StringAssert.Contains("CARD OWNERSHIP RULE", prompt);
            StringAssert.Contains("CanvasScaler mode", prompt);
            StringAssert.Contains("FAIL SCOPE CHECK", prompt);
        }


        [Test]
        public void ExecutionMemory_IsInjectedAsDurableContinuationContext()
        {
            const string memory =
                "{\"Version\":1,\"Entries\":[{\"Event\":\"ToolCompleted\",\"Tool\":\"inspect_scene\",\"Result\":\"Dirty=false\"}]}";

            string prompt =
                Invoke<string>(
                    "AppendExecutionMemory",
                    "Complete the card.",
                    memory);

            StringAssert.Contains("PERSISTED CARD EXECUTION MEMORY", prompt);
            StringAssert.Contains("inspect_scene", prompt);
            StringAssert.Contains("Continue from successful work already recorded", prompt);
            StringAssert.Contains("Do not repeat an identical successful read", prompt);
        }


        [Test]
        public void ReviewPrompt_DoesNotReplayImplementationExecutionMemory()
        {
            const string memory =
                "{\"Version\":1,\"Entries\":[{\"Event\":\"ToolCompleted\",\"Tool\":\"get_serialized_properties\",\"Result\":\"very large journal\"}]}";

            string implementationPrompt =
                Invoke<string>(
                    "AppendExecutionMemoryForRun",
                    "Complete the card.",
                    memory,
                    false,
                    false);

            string reviewPrompt =
                Invoke<string>(
                    "AppendExecutionMemoryForRun",
                    "Review the card.",
                    memory,
                    true,
                    false);

            StringAssert.Contains(
                "PERSISTED CARD EXECUTION MEMORY",
                implementationPrompt);

            Assert.AreEqual(
                "Review the card.",
                reviewPrompt,
                "Review must use the implementation report and fresh bounded evidence, not replay the mutation journal.");
        }


        [Test]
        public void ResetTask_ClearsPersistedExecutionMemory()
        {
            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                Type taskType =
                    typeof(AIRuntimeKanbanSession).GetNestedType(
                        "KanbanTask",
                        BindingFlags.NonPublic);

                Assert.NotNull(taskType);

                object task =
                    Activator.CreateInstance(
                        taskType);

                taskType.GetField("Id").SetValue(task, "reset-memory");
                taskType.GetField("Title").SetValue(task, "Reset memory");
                taskType.GetField("ExecutionMemory").SetValue(
                    task,
                    "{\"Version\":1,\"Entries\":[{\"Event\":\"RunStarted\"}]}");

                MethodInfo resetTask =
                    typeof(AIRuntimeKanbanSession).GetMethod(
                        "ResetTask",
                        BindingFlags.Instance |
                        BindingFlags.NonPublic);

                Assert.NotNull(resetTask);

                resetTask.Invoke(
                    window,
                    new[] { task });

                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("ExecutionMemory").GetValue(task));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void DiscardedRun_CannotRestoreExecutionMemoryAfterReset()
        {
            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                Type windowType =
                    typeof(AIRuntimeKanbanSession);

                Type taskType =
                    windowType.GetNestedType(
                        "KanbanTask",
                        BindingFlags.NonPublic);

                object task =
                    Activator.CreateInstance(
                        taskType);

                taskType.GetField("Id").SetValue(task, "discarded-memory");
                taskType.GetField("ExecutionMemory").SetValue(task, string.Empty);

                object board =
                    windowType.GetField(
                            "_board",
                            BindingFlags.Instance |
                            BindingFlags.NonPublic)
                        .GetValue(window);

                IList tasks =
                    (IList)board.GetType().GetField("Tasks").GetValue(board);

                tasks.Add(task);

                HashSet<string> discardedTaskIds =
                    (HashSet<string>)windowType.GetField(
                            "_discardedAgentRunTaskIds",
                            BindingFlags.Instance |
                            BindingFlags.NonPublic)
                        .GetValue(window);

                discardedTaskIds.Add("discarded-memory");

                Type updateType =
                    windowType.GetNestedType(
                        "PendingExecutionMemoryUpdate",
                        BindingFlags.NonPublic);

                object update =
                    Activator.CreateInstance(
                        updateType);

                updateType.GetField("TaskId").SetValue(update, "discarded-memory");
                updateType.GetField("Memory").SetValue(update, "stale memory");

                object updates =
                    windowType.GetField(
                            "_pendingExecutionMemoryUpdates",
                            BindingFlags.Instance |
                            BindingFlags.NonPublic)
                        .GetValue(window);

                updates.GetType().GetMethod("Enqueue").Invoke(
                    updates,
                    new[] { update });

                windowType.GetMethod(
                        "DrainPendingExecutionMemoryUpdates",
                        BindingFlags.Instance |
                        BindingFlags.NonPublic)
                    .Invoke(window, null);

                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("ExecutionMemory").GetValue(task));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ExecutionMemory_CollapsesIdenticalConsecutiveToolResults()
        {
            Type observerType =
                _runnerType.GetNestedType(
                    "ExecutionMemoryObserver",
                    BindingFlags.NonPublic);

            Assert.NotNull(observerType);

            string captured = string.Empty;
            object observer =
                Activator.CreateInstance(
                    observerType,
                    BindingFlags.Instance |
                    BindingFlags.Public |
                    BindingFlags.NonPublic,
                    binder: null,
                    args: new object[]
                    {
                        string.Empty,
                        new Action<string>(value => captured = value)
                    },
                    culture: null);

            MethodInfo onAgentEvent =
                observerType.GetMethod(
                    "OnAgentEvent",
                    BindingFlags.Instance | BindingFlags.Public);

            for (int index = 0; index < 2; index++)
            {
                onAgentEvent.Invoke(
                    observer,
                    new object[]
                    {
                        new AgentEvent(
                            AgentEventType.ToolStarted,
                            "session",
                            "get_hierarchy_tree",
                            "{\"rootObjectInstanceId\":24618}")
                    });

                onAgentEvent.Invoke(
                    observer,
                    new object[]
                    {
                        new AgentEvent(
                            AgentEventType.ToolCompleted,
                            "session",
                            "get_hierarchy_tree",
                            "{\"ObjectCount\":7,\"Truncated\":false}")
                    });
            }

            JObject document = JObject.Parse(captured);
            JArray entries = (JArray)document["Entries"];

            Assert.AreEqual(1, entries.Count);
            Assert.AreEqual("get_hierarchy_tree", entries[0]["Tool"]?.Value<string>());
            Assert.AreEqual(2, entries[0]["RepeatCount"]?.Value<int>());
            StringAssert.Contains("24618", entries[0]["Arguments"]?.Value<string>());
            StringAssert.Contains("ObjectCount", entries[0]["Result"]?.Value<string>());
        }


        [Test]
        public void PlayableGameTestChain_RejectsStaleCompileAndMissingAssemblies()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            Child("Write Player Script", "Create Assets/Game/Ball.cs."),
                            Child("Save Scene", "Save Assets/Game/Scenes/Game.unity."),
                            Child("Compile Project", "Compile runtime scripts."),
                            Child("Author PlayMode Smoke Test", "Create Assets/Game/Tests/Smoke.cs."),
                            Child("Run PlayMode Smoke Test", "Call run_play_mode_tests."),
                            Child("Final Integrated Verification", "Final verification.")
                        }
                };

            MethodInfo method =
                _runnerType.GetMethod(
                    "ValidatePlayableGameTestChain",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            TargetInvocationException exception =
                Assert.Throws<TargetInvocationException>(
                    () => method.Invoke(null, new object[] { context }));

            StringAssert.Contains(
                "runtime asmdef",
                exception.InnerException.Message);
        }


        [Test]
        public void PlayableGameTestChain_AcceptsCompileAfterTestSource()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            Child("Create Runtime Assembly Definition", "Create Assets/Game/Game.Runtime.asmdef."),
                            Child("Write Player Script", "Create Assets/Game/Scripts/Ball.cs with an injected input source test seam."),
                            Child("Save Scene", "Save Assets/Game/Scenes/Game.unity."),
                            Child("Create PlayMode Test Assembly Definition", "Create Assets/Game/Tests/Game.PlayModeTests.asmdef."),
                            Child("Author PlayMode Smoke Test", "Author PlayMode smoke test source Assets/Game/Tests/Smoke.cs."),
                            Child("Compile Runtime and Tests", "Compile all assemblies after the test source."),
                            Child("Run PlayMode Smoke Test", "Call run_play_mode_tests."),
                            Child("Final Integrated Verification", "Perform final integrated verification.")
                        }
                };

            MethodInfo method =
                _runnerType.GetMethod(
                    "ValidatePlayableGameTestChain",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.DoesNotThrow(
                () => method.Invoke(null, new object[] { context }));
        }


        [Test]
        public void PlayableGameTestChain_UsesDependencyGraphInsteadOfArrayOrder()
        {
            JObject context =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("final", "Final Integrated Verification", "Perform final integrated verification.", "run"),
                            GraphChild("compile", "Compile Runtime and Tests", "Compile all assemblies after the test source.", "author"),
                            GraphChild("script", "Write Player Script", "Create Assets/Game/Scripts/Ball.cs with a public command method test seam.", "runtime"),
                            GraphChild("run", "Run PlayMode Smoke Test", "Call run_play_mode_tests.", "compile"),
                            GraphChild("runtime", "Create Runtime Assembly Definition", "Create Assets/Game/Game.Runtime.asmdef."),
                            GraphChild("author", "Author PlayMode Smoke Test", "Author PlayMode test source Assets/Game/Tests/Smoke.cs.", "testasm"),
                            GraphChild("testasm", "Create PlayMode Test Assembly Definition", "Create Assets/Game/Tests/Game.PlayModeTests.asmdef.", "scene", "runtime"),
                            GraphChild("scene", "Save Scene", "Save Assets/Game/Scenes/Game.unity.", "script")
                        }
                };

            MethodInfo method =
                _runnerType.GetMethod(
                    "ValidatePlayableGameTestChain",
                    BindingFlags.NonPublic | BindingFlags.Static);

            Assert.DoesNotThrow(
                () => method.Invoke(null, new object[] { context }));
        }


        [Test]
        public void PlayableGameTestChain_RejectsDependencyCycleAndDuplicateTitles()
        {
            MethodInfo method =
                _runnerType.GetMethod(
                    "ValidatePlayableGameTestChain",
                    BindingFlags.NonPublic | BindingFlags.Static);

            JObject duplicateContext =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("one", "Final Integrated Verification", "Final verification."),
                            GraphChild("two", "final integrated verification", "Final verification again.")
                        }
                };

            TargetInvocationException duplicate =
                Assert.Throws<TargetInvocationException>(
                    () => method.Invoke(null, new object[] { duplicateContext }));
            StringAssert.Contains("duplicate sibling titles", duplicate.InnerException?.Message);

            JObject cycleContext =
                new JObject
                {
                    ["DirectChildren"] =
                        new JArray
                        {
                            GraphChild("runtime", "Create Runtime Assembly Definition", "Create Assets/Game/Game.Runtime.asmdef.", "final"),
                            GraphChild("script", "Write Player Script", "Create Assets/Game/Scripts/Ball.cs with a public command method test seam.", "runtime"),
                            GraphChild("testasm", "Create PlayMode Test Assembly Definition", "Create Assets/Game/Tests/Game.PlayModeTests.asmdef.", "runtime"),
                            GraphChild("author", "Author PlayMode Smoke Test", "Author PlayMode test source Assets/Game/Tests/Smoke.cs.", "testasm"),
                            GraphChild("compile", "Compile Runtime and Tests", "Compile all assemblies.", "author"),
                            GraphChild("run", "Run PlayMode Smoke Test", "Call run_play_mode_tests.", "compile"),
                            GraphChild("final", "Final Integrated Verification", "Perform final integrated verification.", "run")
                        }
                };

            TargetInvocationException cycle =
                Assert.Throws<TargetInvocationException>(
                    () => method.Invoke(null, new object[] { cycleContext }));
            StringAssert.Contains("dependency cycle", cycle.InnerException?.Message);
        }


        [Test]
        public void PlayModeTestAuthoringPrompt_ForbidsUnavailableInputHelpersAndEmptyWrites()
        {
            const string request =
                "Author a PlayMode smoke test at Assets/Game/Tests/Smoke.cs.";

            string prompt =
                Invoke<string>(
                    "BuildTaskPrompt",
                    "Author PlayMode Smoke Test",
                    request,
                    string.Empty,
                    string.Empty,
                    string.Empty,
                    request,
                    null);

            StringAssert.Contains(
                "PLAY MODE TEST AUTHORING PREFLIGHT",
                prompt);
            StringAssert.Contains(
                "Do not use UnityEngine.TestTools.Utils.InputHelpers",
                prompt);
            StringAssert.Contains(
                "never issue an empty-content write",
                prompt);
        }


        [Test]
        public void ExplicitAssetDeliverables_ExcludeFutureConsumerScenePath()
        {
            const string request =
                "Create a visible material asset at Assets/JumpingBallGame/Materials/BallMaterial.mat. " +
                "This material is consumed by a later child which targets the scene " +
                "Assets/JumpingBallGame/Scenes/JumpingBallGame.unity.\n" +
                "Assets/JumpingBallGame/Materials/BallMaterial.mat exists and is valid.";

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ExtractExplicitAssetDeliverablePaths",
                    request);

            CollectionAssert.AreEqual(
                new[]
                {
                    "Assets/JumpingBallGame/Materials/BallMaterial.mat"
                },
                paths);
        }


        [Test]
        public void ExplicitAssetDeliverables_IncludeSceneMutatedByThisLeaf()
        {
            const string request =
                "In Assets/JumpingBallGame/Scenes/JumpingBallGame.unity, create the PlayerBall GameObject.";

            IReadOnlyList<string> paths =
                Invoke<IReadOnlyList<string>>(
                    "ExtractExplicitAssetDeliverablePaths",
                    request);

            CollectionAssert.Contains(
                paths,
                "Assets/JumpingBallGame/Scenes/JumpingBallGame.unity");
        }


        [Test]
        public void ReviewHierarchyTargets_MatchNestedContractNamesAndExcludeUnmentionedObjects()
        {
            JObject hierarchy =
                Hierarchy(
                    Node(
                        1,
                        "Environment",
                        "Environment",
                        Node(2, "SpawnPoint", "Environment/SpawnPoint"),
                        Node(3, "CheckpointA", "Environment/CheckpointA"),
                        Node(4, "Decoration", "Environment/Decoration")));

            object[] arguments =
            {
                hierarchy,
                "Verify SpawnPoint and CheckpointA beneath Environment. Each must have its configured components.",
                false,
                24,
                false
            };

            IList selected =
                InvokeHierarchyTargetSelection(
                    arguments);

            CollectionAssert.AreEqual(
                new[]
                {
                    "Environment",
                    "SpawnPoint",
                    "CheckpointA"
                },
                selected
                    .Cast<JObject>()
                    .Select(node => node["Name"]?.Value<string>())
                    .ToArray());

            Assert.IsFalse((bool)arguments[4]);
        }


        [Test]
        public void ReviewHierarchyTargets_MatchSpacedContractNameToCamelCaseObject()
        {
            object[] arguments =
            {
                Hierarchy(Node(1, "MainCamera", "MainCamera")),
                "Inspect Main Camera and verify the attached component.",
                false,
                24,
                false
            };

            IList selected =
                InvokeHierarchyTargetSelection(
                    arguments);

            Assert.AreEqual(1, selected.Count);
            Assert.AreEqual(
                "MainCamera",
                ((JObject)selected[0])["Name"]?.Value<string>());
            Assert.IsFalse((bool)arguments[4]);
        }


        [Test]
        public void ReviewHierarchyTargets_AmbiguousNamesRequireQualifiedPath()
        {
            JObject hierarchy =
                Hierarchy(
                    Node(
                        1,
                        "Left",
                        "Left",
                        Node(2, "Target", "Left/Target")),
                    Node(
                        3,
                        "Right",
                        "Right",
                        Node(4, "Target", "Right/Target")));

            object[] ambiguousArguments =
            {
                hierarchy,
                "Inspect Target and verify its components.",
                false,
                24,
                false
            };

            IList ambiguous =
                InvokeHierarchyTargetSelection(
                    ambiguousArguments);

            Assert.AreEqual(2, ambiguous.Count);
            Assert.IsTrue(
                (bool)ambiguousArguments[4],
                "Duplicate names without a hierarchy path must produce incomplete evidence.");

            object[] qualifiedArguments =
            {
                hierarchy,
                "Inspect Left/Target and verify its components.",
                false,
                24,
                false
            };

            InvokeHierarchyTargetSelection(
                qualifiedArguments);

            Assert.IsFalse(
                (bool)qualifiedArguments[4],
                "An authoritative hierarchy path resolves the otherwise ambiguous name.");
        }


        [Test]
        public void ReviewHierarchyTargets_BoundedSelectionReportsIncompleteEvidence()
        {
            object[] arguments =
            {
                Hierarchy(
                    Node(1, "One", "One"),
                    Node(2, "Two", "Two"),
                    Node(3, "Three", "Three")),
                "Verify all required GameObjects.",
                true,
                2,
                false
            };

            IList selected =
                InvokeHierarchyTargetSelection(
                    arguments);

            Assert.AreEqual(2, selected.Count);
            Assert.IsTrue((bool)arguments[4]);
        }


        [Test]
        public void ReviewEvidencePlanner_AlwaysEnablesReadOnlyHierarchyCapability()
        {
            IEnumerable<string> capabilities =
                Invoke<IEnumerable<string>>(
                    "NormalizeCapabilities",
                    (object)Array.Empty<string>());

            CollectionAssert.Contains(
                capabilities.ToArray(),
                "get_hierarchy_tree");
        }


        [Test]
        public void IncompleteReviewEvidenceMarker_IsDetectedDeterministically()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession)
                    .GetMethod(
                        "HasIncompleteReviewEvidence",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.NotNull(method);
            Assert.IsTrue(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "{\"ComponentInspection\":\"[AIRUNTIME_REVIEW_EVIDENCE_INCOMPLETE]\"}"
                    }));
            Assert.IsFalse(
                (bool)method.Invoke(
                    null,
                    new object[] { "{\"ComponentInspection\":\"complete\"}" }));
        }


        [Test]
        public void WorkflowPhases_EnforcePlanningImplementationAndReviewBarriers()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object child = Activator.CreateInstance(taskType);

                SetTaskField(taskType, root, "Id", "phase-root");
                SetTaskField(taskType, root, "Title", "Build project");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, child, "Id", "phase-child");
                SetTaskField(taskType, child, "Title", "Implement feature");
                SetTaskField(taskType, child, "ParentTaskId", "phase-root");
                SetTaskField(taskType, child, "Status", Enum.Parse(statusType, "Todo"));
                tasks.Add(root);
                tasks.Add(child);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo eligible = windowType.GetMethod(
                    "IsEligibleForCurrentWorkflowPhase",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsTrue((bool)eligible.Invoke(window, new[] { root, (object)false }));
                Assert.IsFalse((bool)eligible.Invoke(window, new[] { child, (object)false }));

                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                Assert.IsFalse((bool)eligible.Invoke(window, new[] { root, (object)false }));
                Assert.IsTrue((bool)eligible.Invoke(window, new[] { child, (object)false }));
                Assert.IsTrue((bool)eligible.Invoke(window, new[] { child, (object)true }));

                SetTaskField(taskType, root, "WorkflowPhase", "FinalReview");
                Assert.IsFalse((bool)eligible.Invoke(window, new[] { child, (object)false }));
                Assert.IsFalse((bool)eligible.Invoke(window, new[] { child, (object)true }));
                Assert.IsTrue((bool)eligible.Invoke(window, new[] { root, (object)true }));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void WorkflowPhases_OnlyPlanningRootCanStartCoordination()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoDecompose", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, true);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object child = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "phase-authority-root");
                SetTaskField(taskType, root, "Title", "Build complete product");
                SetTaskField(taskType, root, "Description", "Build scripts, scenes, UI, tests, and final verification.");
                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                SetTaskField(taskType, child, "Id", "phase-authority-child");
                SetTaskField(taskType, child, "ParentTaskId", "phase-authority-root");
                SetTaskField(taskType, child, "Title", "Build scripts and scene");
                SetTaskField(taskType, child, "Description", "Create scripts, a playable scene, UI, and verification.");
                tasks.Add(root);
                tasks.Add(child);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo coordinate = windowType.GetMethod(
                    "ShouldStartCoordinationOnlyRun",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.IsFalse((bool)coordinate.Invoke(window, new[] { root }));
                Assert.IsFalse((bool)coordinate.Invoke(window, new[] { child }));

                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                Assert.IsTrue((bool)coordinate.Invoke(window, new[] { root }));
                Assert.IsFalse(
                    (bool)coordinate.Invoke(window, new[] { child }),
                    "Planning authority belongs to the project root, never an implementation descendant.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void PlanningReadiness_AcceptsCohesiveMultiArtifactOutcome()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object umbrella = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "readiness-root");
                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, umbrella, "Id", "readiness-umbrella");
                SetTaskField(taskType, umbrella, "ParentTaskId", "readiness-root");
                SetTaskField(taskType, umbrella, "Title", "Create all gameplay systems");
                SetTaskField(
                    taskType,
                    umbrella,
                    "Description",
                    "Create movement scripts, a playable scene with prefabs, UI, PlayMode tests, and integration verification.");
                SetTaskField(taskType, umbrella, "AcceptanceCriteria", "The complete game compiles and plays.");
                SetTaskField(
                    taskType,
                    umbrella,
                    "ProducedArtifactPaths",
                    new List<string>
                    {
                        "Assets/Game/Player.cs",
                        "Assets/Game/Manager.cs",
                        "Assets/Game/Hazard.cs",
                        "Assets/Game/Goal.cs",
                        "Assets/Game/Hud.cs"
                    });
                tasks.Add(root);
                tasks.Add(umbrella);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                string error = (string)windowType.GetMethod(
                        "ValidatePlanningReadiness",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, new[] { root });

                Assert.AreEqual(
                    string.Empty,
                    error,
                    "Planning validates explicit graph integrity; it must not override the planner's cohesive ownership judgment using file or keyword counts.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void PlanningReadiness_AcceptsPrefabOutputOwnedByAChild()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object child = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "prefab-plan-root");
                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, child, "Id", "prefab-plan-child");
                SetTaskField(taskType, child, "ParentTaskId", "prefab-plan-root");
                SetTaskField(taskType, child, "Title", "Create player prefab");
                SetTaskField(taskType, child, "Description", "Create a reusable player prefab.");
                SetTaskField(taskType, child, "AcceptanceCriteria", "The prefab is valid and inspectable.");
                SetTaskField(
                    taskType,
                    child,
                    "ProducedArtifactPaths",
                    new List<string> { "Assets/Game/Prefabs/Player.prefab" });
                tasks.Add(root);
                tasks.Add(child);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                string error = (string)windowType.GetMethod(
                        "ValidatePlanningReadiness",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, new[] { root });

                Assert.AreEqual(
                    string.Empty,
                    error,
                    "Planning owns strategy and may assign any valid artifact type; installed execution capabilities must not constrain the graph contract.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void CapabilityRequest_IsRecognizedAsAnExplicitExecutionRequirement()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            MethodInfo method = windowType.GetMethod(
                "TryExtractCapabilityRequest",
                BindingFlags.NonPublic | BindingFlags.Static);
            object[] arguments =
            {
                "[CapabilityRequest] Create a Unity Timeline asset from structured tracks; permission=Write; output=Assets/Cutscene.playable",
                null
            };

            Assert.IsTrue((bool)method.Invoke(null, arguments));
            StringAssert.StartsWith(
                "Create a Unity Timeline asset",
                (string)arguments[1]);
        }


        [Test]
        public void KanbanImplementation_ReceivesEveryInstalledKanbanCapability()
        {
            MethodInfo method = typeof(AIRuntimeKanbanAgentRunner).GetMethod(
                "ResolveKanbanCapabilitiesForRun",
                BindingFlags.NonPublic | BindingFlags.Static);

            string[] implementation =
                ((IEnumerable<string>)method.Invoke(null, new object[] { false }))
                .ToArray();
            string[] installed =
                AIRuntimeEditorPluginCatalog
                    .GetCapabilityPlugins(AIRuntimeEditorPluginHost.Kanban)
                    .Where(plugin =>
                        plugin.Name != "create_kanban_child_task" &&
                        plugin.Name != "commit_kanban_plan")
                    .Select(plugin => plugin.Name)
                    .OrderBy(name => name, StringComparer.Ordinal)
                    .ToArray();

            CollectionAssert.AreEqual(installed, implementation);
            CollectionAssert.Contains(implementation, "create_prefab");
            CollectionAssert.DoesNotContain(
                implementation,
                "create_kanban_child_task");
            CollectionAssert.DoesNotContain(
                implementation,
                "commit_kanban_plan");
        }


        [Test]
        public void PlanningFallbacks_MoveFromArchitectToIndependentProviderLanes()
        {
            IReadOnlyList<string> fallbacks =
                AIRuntimeKanbanAgentRunner.ResolvePhaseFallbackAgentIds(
                    "ArchitectAgent",
                    coordinationOnly: true,
                    reviewMode: false);

            CollectionAssert.AreEqual(
                new[] { "ReviewAgent", "DeveloperAgent" },
                fallbacks);
        }


        private static UnityEngine.SceneManagement.Scene CreateSavedArtifactTestScene(string scenePath)
        {
            // Opening a saved fixture avoids NewScene(Additive)'s rejection of
            // an existing untitled scene, without saving or replacing user work.
            File.Copy(
                Path.Combine(Directory.GetParent(Application.dataPath).FullName,
                    UnityEditor.AssetDatabase.GUIDToAssetPath("edb4629b297e4a26bb4d3a4275537131")),
                Path.Combine(Directory.GetParent(Application.dataPath).FullName, scenePath));
            UnityEditor.AssetDatabase.ImportAsset(
                scenePath, UnityEditor.ImportAssetOptions.ForceSynchronousImport);
            return UnityEditor.SceneManagement.EditorSceneManager.OpenScene(
                scenePath, UnityEditor.SceneManagement.OpenSceneMode.Additive);
        }

        [Test]
        public void ProducedArtifactValidation_RejectsSceneYamlRenamedAsPrefab()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            string folder =
                "Assets/AIRuntimeCapabilityVerification/KanbanArtifactValidation-" +
                Guid.NewGuid().ToString("N");
            string scenePath = folder + "/Source.unity";
            string fakePrefabPath = folder + "/Player.prefab";
            UnityEngine.SceneManagement.Scene scene = default;

            try
            {
                Directory.CreateDirectory(
                    Path.Combine(
                        Directory.GetParent(Application.dataPath).FullName,
                        folder.Replace('/', Path.DirectorySeparatorChar)));
                scene = CreateSavedArtifactTestScene(scenePath);
                UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                File.Copy(
                    Path.Combine(
                        Directory.GetParent(Application.dataPath).FullName,
                        scenePath.Replace('/', Path.DirectorySeparatorChar)),
                    Path.Combine(
                        Directory.GetParent(Application.dataPath).FullName,
                        fakePrefabPath.Replace('/', Path.DirectorySeparatorChar)));
                UnityEngine.TestTools.LogAssert.Expect(LogType.Error,
                    "Prefab 'Player.prefab' has unexpected file IDs and is likely to be corrupt");
                UnityEditor.AssetDatabase.ImportAsset(
                    fakePrefabPath,
                    UnityEditor.ImportAssetOptions.ForceSynchronousImport);
                SetTaskField(taskType, task, "Title", "Create player prefab");
                SetTaskField(
                    taskType,
                    task,
                    "ProducedArtifactPaths",
                    new List<string> { fakePrefabPath });

                UnityEngine.TestTools.LogAssert.Expect(LogType.Error,
                    "Prefab 'Player.prefab' has unexpected file IDs and is likely to be corrupt");
                object[] arguments = { task, null };
                bool invalid = (bool)windowType.GetMethod(
                        "TryGetMissingProducedArtifact",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .Invoke(null, arguments);

                Assert.IsTrue(invalid);
                StringAssert.Contains("not a valid Unity prefab asset", (string)arguments[1]);
            }
            finally
            {
                if (scene.IsValid() && scene.isLoaded)
                {
                    UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                }

                UnityEditor.AssetDatabase.DeleteAsset(folder);
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ProducedArtifactValidation_RejectsEmptyGameplayScene()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            string folder =
                "Assets/AIRuntimeCapabilityVerification/KanbanSceneValidation-" +
                Guid.NewGuid().ToString("N");
            string scenePath = folder + "/Gameplay.unity";
            UnityEngine.SceneManagement.Scene scene = default;

            try
            {
                Directory.CreateDirectory(
                    Path.Combine(
                        Directory.GetParent(Application.dataPath).FullName,
                        folder.Replace('/', Path.DirectorySeparatorChar)));
                scene = CreateSavedArtifactTestScene(scenePath);
                UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                SetTaskField(taskType, task, "Title", "Build playable gameplay scene");
                SetTaskField(
                    taskType,
                    task,
                    "ProducedArtifactPaths",
                    new List<string> { scenePath });

                object[] arguments = { task, null };
                bool invalid = (bool)windowType.GetMethod(
                        "TryGetMissingProducedArtifact",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .Invoke(null, arguments);

                Assert.IsTrue(invalid);
                StringAssert.Contains("no persisted root GameObjects", (string)arguments[1]);
            }
            finally
            {
                if (scene.IsValid() && scene.isLoaded)
                {
                    UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                }

                UnityEditor.AssetDatabase.DeleteAsset(folder);
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ProducedArtifactValidation_RejectsDirtyLoadedGameplayScene()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            string folder =
                "Assets/AIRuntimeCapabilityVerification/KanbanDirtySceneValidation-" +
                Guid.NewGuid().ToString("N");
            string scenePath = folder + "/Gameplay.unity";
            UnityEngine.SceneManagement.Scene scene = default;

            try
            {
                Directory.CreateDirectory(
                    Path.Combine(
                        Directory.GetParent(Application.dataPath).FullName,
                        folder.Replace('/', Path.DirectorySeparatorChar)));
                scene = CreateSavedArtifactTestScene(scenePath);
                GameObject persistedRoot = new GameObject("PersistedRoot");
                UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(
                    persistedRoot,
                    scene);
                UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, scenePath);
                GameObject unsavedRoot = new GameObject("UnsavedRoot");
                UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(
                    unsavedRoot,
                    scene);
                UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene);
                Assert.IsTrue(scene.isDirty);
                SetTaskField(taskType, task, "Title", "Build playable gameplay scene");
                SetTaskField(
                    taskType,
                    task,
                    "ProducedArtifactPaths",
                    new List<string> { scenePath });

                object[] arguments = { task, null };
                bool invalid = (bool)windowType.GetMethod(
                        "TryGetMissingProducedArtifact",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .Invoke(null, arguments);

                Assert.IsTrue(invalid);
                StringAssert.Contains("unsaved changes", (string)arguments[1]);
            }
            finally
            {
                if (scene.IsValid() && scene.isLoaded)
                {
                    UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                }

                UnityEditor.AssetDatabase.DeleteAsset(folder);
            }
        }


        [Test]
        public void OwnedDirtySceneCheckpoint_PersistsSceneBeforeRunEnds()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            string folder =
                "Assets/AIRuntimeCapabilityVerification/KanbanSceneCheckpoint-" +
                Guid.NewGuid().ToString("N");
            string scenePath = folder + "/Gameplay.unity";
            UnityEngine.SceneManagement.Scene scene = default;

            try
            {
                Directory.CreateDirectory(
                    Path.Combine(
                        Directory.GetParent(Application.dataPath).FullName,
                        folder.Replace('/', Path.DirectorySeparatorChar)));
                scene = CreateSavedArtifactTestScene(scenePath);
                UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene, scenePath);
                GameObject checkpointedRoot = new GameObject("CheckpointedRoot");
                UnityEditor.SceneManagement.EditorSceneManager.MoveGameObjectToScene(
                    checkpointedRoot,
                    scene);
                UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene);
                Assert.IsTrue(scene.isDirty);
                SetTaskField(
                    taskType,
                    task,
                    "ProducedArtifactPaths",
                    new List<string> { scenePath });

                object[] arguments = { task, 0, null };
                bool saved = (bool)windowType.GetMethod(
                        "TryCheckpointOwnedDirtyScenes",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .Invoke(null, arguments);

                Assert.IsTrue(saved, (string)arguments[2]);
                Assert.AreEqual(1, (int)arguments[1]);
                Assert.IsFalse(scene.isDirty);
                UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene(
                    scenePath,
                    UnityEditor.SceneManagement.OpenSceneMode.Additive);
                Assert.IsNotNull(
                    scene.GetRootGameObjects()
                        .FirstOrDefault(root => root.name == "CheckpointedRoot"));
            }
            finally
            {
                if (scene.IsValid() && scene.isLoaded)
                {
                    UnityEditor.SceneManagement.EditorSceneManager.CloseScene(scene, true);
                }

                UnityEditor.AssetDatabase.DeleteAsset(folder);
            }
        }


        [Test]
        public void ProviderAvailabilityFailure_IsPausedForExplicitRetry()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            MethodInfo method = windowType.GetMethod(
                "IsTransientProviderAvailabilityFailure",
                BindingFlags.NonPublic | BindingFlags.Static,
                null, new[] { typeof(string) }, null);

            Assert.IsTrue(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "All eligible providers failed: request did not respond within 60 seconds; HTTP 503 model is currently unavailable"
                    }));
            Assert.IsTrue(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "All eligible providers failed. Attempts: 'Nous Portal' [InvalidResponse]: Nous Portal returned an invalid response containing neither assistant text nor tool calls.; 'Nous Portal' [InvalidResponse]: Nous Portal request failed. HTTP 200. Failed to receive data\n: OPENROUTER PROCESSING"
                    }));
            Assert.IsTrue(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "Nous Portal request failed. HTTP 200. Unknown Error\n: OPENROUTER PROCESSING\n\ndata: {\"choices\":[{\"delta\":{\"content\":\"partial\"},\"finish_reason\":null}]}"
                    }));
            Assert.IsFalse(
                (bool)method.Invoke(
                    null,
                    new object[]
                    {
                        "All eligible providers failed. Attempts: 'Provider' [InvalidResponse]: malformed response with an invalid tool call."
                    }));
            Assert.IsFalse(
                (bool)method.Invoke(
                    null,
                    new object[] { "Compilation failed with CS1002" }));
        }

        [TestCase("All eligible providers failed. NVIDIA [EmptyResponse]; OpenRouter [Authentication]: HTTP 401", true)]
        [TestCase("Provider rejected request: HTTP 403", true)]
        [TestCase("Provider returned neither assistant text nor tool calls", false)]
        public void ProviderResumeGuidance_ExplainsConfigurationFailures(string error, bool requiresCredentials)
        {
            MethodInfo method = typeof(AIRuntimeKanbanSession).GetMethod(
                "BuildProviderResumeGuidance", BindingFlags.NonPublic | BindingFlags.Static);
            string guidance = (string)method.Invoke(null, new object[] { error });
            Assert.AreEqual(requiresCredentials, guidance.Contains("credentials and access"));
            StringAssert.Contains("preserved this attempt", guidance);
        }


        [Test]
        public void InterruptedPlanningStream_WithRetainedDrafts_DoesNotConsumeStructuralRetry()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object parent = Activator.CreateInstance(taskType);
                object draft = Activator.CreateInstance(taskType);

                SetTaskField(taskType, parent, "Id", "interrupted-planning-root");
                SetTaskField(taskType, parent, "Title", "Plan a game");
                SetTaskField(taskType, parent, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, parent, "DecompositionFailureCount", 2);
                SetTaskField(taskType, parent, "ExecutionMemory", "durable planning journal");
                SetTaskField(taskType, draft, "Id", "retained-draft");
                SetTaskField(taskType, draft, "ParentTaskId", "interrupted-planning-root");
                SetTaskField(taskType, draft, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, draft, "IsDecompositionDraft", true);
                tasks.Add(parent);
                tasks.Add(draft);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                const string error =
                    "Nous Portal request failed. HTTP 200. Unknown Error\n: OPENROUTER PROCESSING\n\ndata: {\"choices\":[{\"delta\":{\"content\":\"partial\"},\"finish_reason\":null}]}";

                UnityEngine.TestTools.LogAssert.Expect(LogType.Error,
                    "[Kanban] DeveloperAgent failed task 'Plan a game': " + error);

                windowType.GetMethod(
                        "CompleteAgentTask",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(
                        window,
                        new object[]
                        {
                            "interrupted-planning-root",
                            "DeveloperAgent",
                            string.Empty,
                            error,
                            null,
                            false
                        });

                Assert.AreEqual(
                    2,
                    GetTaskField(taskType, parent, "DecompositionFailureCount"));
                Assert.AreEqual(
                    "Todo",
                    GetTaskField(taskType, parent, "Status").ToString());
                Assert.AreEqual(
                    "Retry Available",
                    GetTaskField(taskType, parent, "ReviewStatus"));
                Assert.AreEqual(
                    "durable planning journal",
                    GetTaskField(taskType, parent, "ExecutionMemory"));
                StringAssert.StartsWith(
                    "[ProviderRetryAvailable]",
                    (string)GetTaskField(taskType, parent, "NextAgentMessage"));
                Assert.IsTrue(
                    (bool)GetTaskField(taskType, draft, "IsDecompositionDraft"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void InterruptedSuccessfulProviderStream_PreservesMemoryAndReplacesStaleCooldownHandoff()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "provider-stream-recovery");
                SetTaskField(taskType, task, "Title", "Assemble persisted scene");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, task, "ExecutionMemory", "durable scene journal");
                SetTaskField(
                    taskType,
                    task,
                    "NextAgentMessage",
                    "[ProviderCooldownElapsed] stale rate-limit recovery");
                tasks.Add(task);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                const string error =
                    "All eligible providers failed. Attempts: 'Nous Portal' [InvalidResponse]: Nous Portal request failed. HTTP 200. Failed to receive data\n: OPENROUTER PROCESSING";
                UnityEngine.TestTools.LogAssert.Expect(LogType.Error,
                    "[Kanban] DeveloperAgent failed task 'Assemble persisted scene': " + error);


                windowType.GetMethod(
                        "CompleteAgentTask",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(
                        window,
                        new object[]
                        {
                            "provider-stream-recovery",
                            "DeveloperAgent",
                            string.Empty,
                            error,
                            null,
                            false
                        });

                Assert.AreEqual(
                    "Retry Available",
                    GetTaskField(taskType, task, "ReviewStatus"));
                Assert.AreEqual(
                    "durable scene journal",
                    GetTaskField(taskType, task, "ExecutionMemory"));
                StringAssert.StartsWith(
                    "[ProviderRetryAvailable]",
                    (string)GetTaskField(taskType, task, "NextAgentMessage"));
                StringAssert.Contains(
                    "Failed to receive data",
                    (string)GetTaskField(taskType, task, "AgentError"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void LegacyFailedPlanningStream_IsRequeuedWithoutStructuralRetryPenalty()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Failed"));
            SetTaskField(taskType, task, "DecompositionFailureCount", 3);
            SetTaskField(
                taskType,
                task,
                "ReviewResult",
                "Nous Portal request failed. HTTP 200. Unknown Error\n: OPENROUTER PROCESSING");
            SetTaskField(
                taskType,
                task,
                "BlockedReason",
                "Nous Portal request failed. HTTP 200. Unknown Error\n: OPENROUTER PROCESSING");

            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);
            tasks.Add(task);

            int recovered = (int)windowType.GetMethod(
                    "RecoverLegacyTransientProviderAvailabilityFailures",
                    BindingFlags.NonPublic | BindingFlags.Static)
                .Invoke(null, new object[] { tasks });

            Assert.AreEqual(1, recovered);
            Assert.AreEqual(
                "Todo",
                GetTaskField(taskType, task, "Status").ToString());
            Assert.AreEqual(
                "Retry Available",
                GetTaskField(taskType, task, "ReviewStatus"));
            Assert.AreEqual(
                2,
                GetTaskField(taskType, task, "DecompositionFailureCount"));
            Assert.AreEqual(
                string.Empty,
                GetTaskField(taskType, task, "BlockedReason"));
            StringAssert.StartsWith(
                "[ProviderTransportRecovery]",
                (string)GetTaskField(taskType, task, "NextAgentMessage"));
        }


        [Test]
        public void PausedProviderAttempt_RefreshesGuidanceWithoutResettingProgress()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Todo"));
            SetTaskField(taskType, task, "ReviewStatus", "Retry Available");
            SetTaskField(taskType, task, "ExecutionMemory", "saved scene checkpoint");
            SetTaskField(taskType, task, "NextAgentMessage", "continue from checkpoint");
            SetTaskField(taskType, task, "ReviewFailureKind", "ImplementationDefect");
            SetTaskField(taskType, task, "DecompositionFailureCount", 2);
            string error = "All eligible providers failed: neither assistant text nor tool calls; OpenRouter [Authentication]: HTTP 401";
            SetTaskField(taskType, task, "AgentError", error);
            IList tasks = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(taskType));
            tasks.Add(task);
            MethodInfo migrate = windowType.GetMethod("RecoverLegacyTransientProviderAvailabilityFailures",
                BindingFlags.NonPublic | BindingFlags.Static);

            Assert.AreEqual(1, migrate.Invoke(null, new object[] { tasks }));
            Assert.AreEqual(0, migrate.Invoke(null, new object[] { tasks }));
            StringAssert.Contains("credentials and access", (string)GetTaskField(taskType, task, "BlockedReason"));
            Assert.AreEqual("Todo", GetTaskField(taskType, task, "Status").ToString());
            Assert.AreEqual("Retry Available", GetTaskField(taskType, task, "ReviewStatus"));
            Assert.AreEqual("saved scene checkpoint", GetTaskField(taskType, task, "ExecutionMemory"));
            Assert.AreEqual("continue from checkpoint", GetTaskField(taskType, task, "NextAgentMessage"));
            Assert.AreEqual("ImplementationDefect", GetTaskField(taskType, task, "ReviewFailureKind"));
            Assert.AreEqual(2, GetTaskField(taskType, task, "DecompositionFailureCount"));
            Assert.AreEqual(error, GetTaskField(taskType, task, "AgentError"));
        }


        [Test]
        public void LegacyInterruptedProviderStream_IsRequeuedWithArtifactsAndMemoryPreserved()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(taskType, task, "ExecutionMemory", "durable hierarchy journal");
            SetTaskField(
                taskType,
                task,
                "ProducedArtifactPaths",
                new List<string> { "Assets/Game/Scenes/Gameplay.unity" });
            SetTaskField(
                taskType,
                task,
                "AgentError",
                "All eligible providers failed. Attempts: 'Nous Portal' [InvalidResponse]: Nous Portal request failed. HTTP 200. Failed to receive data\n: OPENROUTER PROCESSING");
            SetTaskField(
                taskType,
                task,
                "NextAgentMessage",
                "[ProviderCooldownElapsed] stale provider handoff");

            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);
            tasks.Add(task);

            int recovered = (int)windowType.GetMethod(
                    "RecoverLegacyTransientProviderAvailabilityFailures",
                    BindingFlags.NonPublic | BindingFlags.Static)
                .Invoke(null, new object[] { tasks });

            Assert.AreEqual(1, recovered);
            Assert.AreEqual(
                "Todo",
                GetTaskField(taskType, task, "Status").ToString());
            Assert.AreEqual(
                "durable hierarchy journal",
                GetTaskField(taskType, task, "ExecutionMemory"));
            CollectionAssert.AreEqual(
                new[] { "Assets/Game/Scenes/Gameplay.unity" },
                (ICollection)GetTaskField(taskType, task, "ProducedArtifactPaths"));
            StringAssert.StartsWith(
                "[ProviderTransportRecovery]",
                (string)GetTaskField(taskType, task, "NextAgentMessage"));
            StringAssert.Contains(
                "inspect each existing artifact once",
                ((string)GetTaskField(taskType, task, "NextAgentMessage")).ToLowerInvariant());
        }


        [Test]
        public void ReviewContextOverflow_PausesAtReviewForExplicitRetry()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                SetTaskField(taskType, task, "Title", "Verify implementation");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));

                bool recovered = (bool)windowType.GetMethod(
                        "TryRecoverReviewContextLengthFailure",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(
                        window,
                        new[]
                        {
                            task,
                            (object)"Reviewer",
                            "Request exceeded the maximum context length"
                        });

                Assert.IsTrue(recovered);
                Assert.AreEqual(
                    "Review",
                    GetTaskField(taskType, task, "Status").ToString());
                Assert.AreEqual(
                    "Retry Available",
                    GetTaskField(taskType, task, "ReviewStatus"));
                Assert.AreEqual(
                    1,
                    GetTaskField(taskType, task, "PreflightFailureCount"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        private sealed class PlanningCommitFixture : IDisposable
        {
            public readonly Type TaskType = typeof(AIRuntimeKanbanSession).GetNestedType("KanbanTask", BindingFlags.NonPublic);
            public readonly AIRuntimeKanbanSession Window;
            public readonly object Root;
            public readonly IList Tasks;
            public readonly object Run;
            private readonly Type _windowType = typeof(AIRuntimeKanbanSession);

            public PlanningCommitFixture()
            {
                Window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
                _windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(Window, false);
                Type boardType = _windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
                object board = Activator.CreateInstance(boardType);
                Tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                _windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(Window, board);
                Root = Activator.CreateInstance(TaskType);
                SetTaskField(TaskType, Root, "Id", "transaction-root");
                SetTaskField(TaskType, Root, "Title", "Build project");
                SetTaskField(TaskType, Root, "AssignedAgent", "DeveloperAgent");
                SetTaskField(TaskType, Root, "WorkflowPhase", "Planning");
                SetTaskField(TaskType, Root, "PlanningAttemptId", "attempt-current");
                Type statusType = _windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
                SetTaskField(TaskType, Root, "Status", Enum.Parse(statusType, "InProgress"));
                Tasks.Add(Root);
                Type runType = _windowType.GetNestedType("ActiveAgentRun", BindingFlags.NonPublic);
                Run = Activator.CreateInstance(runType);
                runType.GetField("TaskId").SetValue(Run, "transaction-root");
                runType.GetField("PlanningAttemptId").SetValue(Run, "attempt-current");
                runType.GetField("CoordinationOnly").SetValue(Run, true);
                runType.GetField("Cancellation").SetValue(Run, new System.Threading.CancellationTokenSource());
                ((IDictionary)_windowType.GetField("_activeAgentRuns", BindingFlags.NonPublic | BindingFlags.Instance)
                    .GetValue(Window)).Add("DeveloperAgent", Run);
            }

            public object Add(string id, string attempt = "attempt-current", string parent = "transaction-root")
            {
                object task = Activator.CreateInstance(TaskType);
                SetTaskField(TaskType, task, "Id", id);
                SetTaskField(TaskType, task, "Title", id);
                SetTaskField(TaskType, task, "Description", "Implement " + id);
                SetTaskField(TaskType, task, "AcceptanceCriteria", "Verify " + id);
                SetTaskField(TaskType, task, "ParentTaskId", parent);
                SetTaskField(TaskType, task, "CreationSource", "create_kanban_child_task");
                SetTaskField(TaskType, task, "IsDecompositionDraft", true);
                SetTaskField(TaskType, task, "ProposedInPlanningAttemptId", attempt);
                SetTaskField(TaskType, task, "ExecutionMemory", "checkpoint " + id);
                Tasks.Add(task);
                return task;
            }

            public JObject Commit() => JObject.FromObject(Call("CommitAgentPlanningAttempt", "transaction-root"));
            public object Call(string method, params object[] args) => _windowType.GetMethod(method,
                BindingFlags.NonPublic | BindingFlags.Instance).Invoke(Window, args);
            public void Complete(string error = "") => Call("CompleteAgentTask", "transaction-root",
                "DeveloperAgent", "Plan ready", error, null, false);
            public void Dispose() => UnityEngine.Object.DestroyImmediate(Window);
        }

        [Test]
        public void CompletionKickbackRecovery_PreservesCardsAndMemoryAndPromotesInterruptedDraft()
        {
            using (var f = new PlanningCommitFixture())
            {
                object source = f.Add("completed", "old-attempt");
                object draft = f.Add("verification");
                SetTaskField(f.TaskType, source, "ProducedArtifactPaths", new List<string> { TestAssetPaths.Resolve("Assets/Editor/AIRuntimeKanbanSession.cs") });
                SetTaskField(f.TaskType, source, "ReviewStatus", "Planning Required");
                SetTaskField(f.TaskType, f.Root, "PlanningKickbackReason", "All acceptance criteria are met. No further work is needed.");
                SetTaskField(f.TaskType, draft, "DeclaredDependencyTitles", new List<string> { "completed" });
                f.Call("RecoverCompletionKickback", "completed");
                Assert.AreEqual(3, f.Tasks.Count);
                Assert.AreEqual("Implementation", GetTaskField(f.TaskType, f.Root, "WorkflowPhase"));
                Assert.AreEqual("Review", GetTaskField(f.TaskType, source, "Status").ToString());
                Assert.AreEqual("Pending", GetTaskField(f.TaskType, source, "ReviewStatus"));
                Assert.AreEqual("checkpoint completed", GetTaskField(f.TaskType, source, "ExecutionMemory"));
                Assert.IsFalse((bool)GetTaskField(f.TaskType, draft, "IsDecompositionDraft"));
                CollectionAssert.Contains((ICollection)GetTaskField(f.TaskType, draft, "DependsOnTaskIds"), "completed");
            }
        }

        [Test]
        public void PlanningCommit_OmittedExistingDependencies_ExplainsSnapshotCorrection()
        {
            using (var f = new PlanningCommitFixture())
            {
                f.Add("producer", "previous-attempt");
                object consumer = f.Add("consumer");
                SetTaskField(f.TaskType, consumer, "DeclaredDependencyTitles", new List<string> { "producer" });
                var result = f.Commit();
                Assert.IsFalse((bool)result["Succeeded"]);
                StringAssert.Contains("Existing cards omitted from this planning attempt: producer", (string)result["Error"]);
                StringAssert.Contains("Restate every desired card", (string)result["Error"]);
                Assert.AreEqual(3, f.Tasks.Count);
            }
        }

        [Test]
        public void PlanningCommit_EighteenCreatedDrafts_ReachImplementationInOneAttempt()
        {
            using (var f = new PlanningCommitFixture())
            {
                for (int i = 0; i < 18; i++)
                {
                    JObject created = JObject.FromObject(f.Call("CreateAgentChildTask", "transaction-root",
                        new CreateKanbanChildTaskArguments {
                            Title = "Implement module " + i,
                            Description = "Create the independent module " + i + " report.",
                            AcceptanceCriteria = "The report for module " + i + " exists and is verified.",
                            ProducesArtifactPaths = new List<string> { "Assets/CommitTest/Module" + i + ".txt" }
                        }));
                    Assert.IsTrue((bool)created["Succeeded"], created.ToString());
                }
                Assert.AreEqual(19, f.Tasks.Count);
                JObject committed = f.Commit();
                Assert.IsTrue((bool)committed["Succeeded"], committed.ToString());
                Assert.AreEqual(18, (int)committed["ProposedChildCount"]);
                Assert.IsTrue(f.Tasks.Cast<object>().Skip(1).All(t => !(bool)GetTaskField(f.TaskType, t, "IsDecompositionDraft")),
                    "Acceptance must promote drafts in the same transition that retires omitted cards.");
                Assert.IsTrue((bool)f.Commit()["Succeeded"], "Repeated commit must be idempotent.");
                f.Complete();
                Assert.AreEqual("Implementation", GetTaskField(f.TaskType, f.Root, "WorkflowPhase"));
                Assert.AreEqual(1, GetTaskField(f.TaskType, f.Root, "PlanRevision"));
                Assert.AreEqual(19, f.Tasks.Count);
                Assert.IsTrue(f.Tasks.Cast<object>().Skip(1).All(t => !(bool)GetTaskField(f.TaskType, t, "IsDecompositionDraft")));
            }
        }

        [Test]
        public void PlanningCommit_EmptyReplacement_PreservesEveryOldCardAndDescendant()
        {
            using (var f = new PlanningCommitFixture())
            {
                for (int i = 0; i < 18; i++) f.Add("old-" + i, "attempt-old");
                f.Add("nested", "attempt-old", "old-0");
                string before = JsonUtility.ToJson(f.Window.GetType().GetField("_board",
                    BindingFlags.NonPublic | BindingFlags.Instance).GetValue(f.Window));
                Assert.IsFalse((bool)f.Commit()["Succeeded"]);
                Assert.AreEqual(before, JsonUtility.ToJson(f.Window.GetType().GetField("_board",
                    BindingFlags.NonPublic | BindingFlags.Instance).GetValue(f.Window)));
                Assert.AreEqual(20, f.Tasks.Count);
            }
        }

        [Test]
        public void PlanningCommit_RejectedReplacement_CanBeCorrectedWithoutRecreatingCards()
        {
            using (var f = new PlanningCommitFixture())
            {
                object obsolete = f.Add("obsolete", "attempt-old");
                object descendant = f.Add("obsolete-leaf", "attempt-old", "obsolete");
                object desired = f.Add("desired");
                SetTaskField(f.TaskType, desired, "DeclaredDependencyTitles", new List<string> { "obsolete" });
                JObject rejected = f.Commit();
                Assert.IsFalse((bool)rejected["Succeeded"]);
                StringAssert.Contains("unresolved", (string)rejected["Error"]);
                Assert.AreEqual(4, f.Tasks.Count);
                Assert.IsTrue(f.Tasks.Contains(obsolete) && f.Tasks.Contains(descendant));
                Assert.AreEqual("checkpoint desired", GetTaskField(f.TaskType, desired, "ExecutionMemory"));
                SetTaskField(f.TaskType, desired, "DeclaredDependencyTitles", new List<string>());
                Assert.IsTrue((bool)f.Commit()["Succeeded"]);
                Assert.AreEqual(2, f.Tasks.Count);
                Assert.IsTrue(f.Tasks.Contains(desired), "Correction must preserve the original card identity.");
                f.Complete();
                Assert.AreEqual("Implementation", GetTaskField(f.TaskType, f.Root, "WorkflowPhase"));
                Assert.AreEqual("checkpoint desired", GetTaskField(f.TaskType, desired, "ExecutionMemory"));
            }
        }

        [Test]
        public void PlanningCommit_RejectionAtCompletion_ReportsActualValidationError()
        {
            using (var f = new PlanningCommitFixture())
            {
                object desired = f.Add("incomplete");
                SetTaskField(f.TaskType, desired, "AcceptanceCriteria", "");
                Assert.IsFalse((bool)f.Commit()["Succeeded"]);
                f.Complete();
                string error = (string)GetTaskField(f.TaskType, f.Root, "ReviewResult");
                StringAssert.Contains("commit was attempted but rejected", error);
                StringAssert.Contains("incomplete contract", error);
                StringAssert.DoesNotContain("did not call", error);
                Assert.IsTrue(f.Tasks.Contains(desired));
            }
        }

        [TestCase(false)]
        [TestCase(true)]
        public void PlanningCommit_InvalidDraftGraph_DoesNotRetirePreviousPlan(bool dependencyCycle)
        {
            using (var f = new PlanningCommitFixture())
            {
                object old = f.Add("old", "attempt-old");
                object left = f.Add("left");
                object right = f.Add("right");
                if (dependencyCycle)
                {
                    SetTaskField(f.TaskType, left, "DeclaredDependencyTitles", new List<string> { "right" });
                    SetTaskField(f.TaskType, right, "DeclaredDependencyTitles", new List<string> { "left" });
                }
                else
                {
                    SetTaskField(f.TaskType, left, "ProducedArtifactPaths", new List<string> { "Assets/Game/Shared.cs" });
                    SetTaskField(f.TaskType, right, "ProducedArtifactPaths", new List<string> { "Assets/Game/Shared.cs" });
                }
                JObject result = f.Commit();
                Assert.IsFalse((bool)result["Succeeded"]);
                StringAssert.Contains(dependencyCycle ? "cycle" : "ownership conflict", (string)result["Error"]);
                Assert.AreEqual(4, f.Tasks.Count);
                Assert.IsTrue(f.Tasks.Contains(old));
                Assert.IsTrue((bool)GetTaskField(f.TaskType, left, "IsDecompositionDraft"));
                Assert.IsTrue((bool)GetTaskField(f.TaskType, right, "IsDecompositionDraft"));
            }
        }

        [Test]
        public void PlanningCommit_ProviderFailsAfterAcceptance_StillEntersImplementation()
        {
            using (var f = new PlanningCommitFixture())
            {
                object desired = f.Add("desired");
                Assert.IsTrue((bool)f.Commit()["Succeeded"]);
                f.Complete("Provider request failed. HTTP 503 Service Unavailable");
                Assert.AreEqual("Implementation", GetTaskField(f.TaskType, f.Root, "WorkflowPhase"));
                Assert.AreEqual(2, f.Tasks.Count);
                Assert.IsFalse((bool)GetTaskField(f.TaskType, desired, "IsDecompositionDraft"));
                Assert.AreEqual("checkpoint desired", GetTaskField(f.TaskType, desired, "ExecutionMemory"));
            }
        }

        [Test]
        public void PlanningCommit_UnchangedKickback_DoesNotRetireOmittedCards()
        {
            using (var f = new PlanningCommitFixture())
            {
                object desired = f.Add("desired");
                SetTaskField(f.TaskType, desired, "IsDecompositionDraft", false);
                string fingerprint = (string)f.Call("ComputePlanFingerprint", f.Root);
                object old = f.Add("old", "attempt-old");
                SetTaskField(f.TaskType, f.Root, "PlanningKickbackReason", "Revise the graph");
                SetTaskField(f.TaskType, f.Root, "PlanningKickbackFingerprint", fingerprint);
                JObject result = f.Commit();
                Assert.IsFalse((bool)result["Succeeded"]);
                StringAssert.Contains("no structural progress", (string)result["Error"]);
                Assert.IsTrue(f.Tasks.Contains(old));
                Assert.AreEqual(3, f.Tasks.Count);
            }
        }

        [Test]
        public void PlanningSnapshot_RetiresOmittedProposalBranchesAndKeepsRestatedCards()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object obsolete = Activator.CreateInstance(taskType);
                object obsoleteDescendant = Activator.CreateInstance(taskType);
                object desired = Activator.CreateInstance(taskType);

                SetTaskField(taskType, root, "Id", "snapshot-root");
                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, obsolete, "Id", "obsolete-umbrella");
                SetTaskField(taskType, obsolete, "ParentTaskId", "snapshot-root");
                SetTaskField(taskType, obsolete, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, obsolete, "IsDecompositionDraft", true);
                SetTaskField(taskType, obsolete, "ProposedInPlanningAttemptId", "previous-attempt");
                SetTaskField(taskType, obsoleteDescendant, "Id", "obsolete-leaf");
                SetTaskField(taskType, obsoleteDescendant, "ParentTaskId", "obsolete-umbrella");
                SetTaskField(taskType, obsoleteDescendant, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, obsoleteDescendant, "IsDecompositionDraft", true);
                SetTaskField(taskType, desired, "Id", "desired-leaf");
                SetTaskField(taskType, desired, "ParentTaskId", "snapshot-root");
                SetTaskField(taskType, desired, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, desired, "IsDecompositionDraft", true);
                SetTaskField(taskType, desired, "ProposedInPlanningAttemptId", "current-attempt");

                tasks.Add(root);
                tasks.Add(obsolete);
                tasks.Add(obsoleteDescendant);
                tasks.Add(desired);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                int retired = (int)windowType.GetMethod(
                        "ReconcilePlanningProposalSnapshot",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, new[] { root, (object)"current-attempt" });

                Assert.AreEqual(2, retired);
                CollectionAssert.AreEquivalent(
                    new[] { "snapshot-root", "desired-leaf" },
                    tasks.Cast<object>()
                        .Select(task => (string)GetTaskField(taskType, task, "Id"))
                        .ToArray());
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void PlanningCompletion_ReconcilesAuthoritativeSnapshotBeforeImplementation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object obsolete = Activator.CreateInstance(taskType);
                object desired = Activator.CreateInstance(taskType);

                SetTaskField(taskType, root, "Id", "completion-root");
                SetTaskField(taskType, root, "Title", "Build project");
                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, root, "PlanningAttemptId", "completion-attempt");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, obsolete, "Id", "completion-obsolete");
                SetTaskField(taskType, obsolete, "Title", "Obsolete umbrella");
                SetTaskField(taskType, obsolete, "ParentTaskId", "completion-root");
                SetTaskField(taskType, obsolete, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, obsolete, "IsDecompositionDraft", true);
                SetTaskField(taskType, obsolete, "ProposedInPlanningAttemptId", "old-attempt");
                SetTaskField(taskType, desired, "Id", "completion-desired");
                SetTaskField(taskType, desired, "Title", "Implement cohesive subsystem");
                SetTaskField(taskType, desired, "Description", "Implement the approved cohesive subsystem and its related files.");
                SetTaskField(taskType, desired, "AcceptanceCriteria", "The subsystem compiles and its owned outcome is verified.");
                SetTaskField(taskType, desired, "ParentTaskId", "completion-root");
                SetTaskField(taskType, desired, "CreationSource", "create_kanban_child_task");
                SetTaskField(taskType, desired, "IsDecompositionDraft", true);
                SetTaskField(taskType, desired, "ProposedInPlanningAttemptId", "completion-attempt");
                SetTaskField(taskType, desired, "Status", Enum.Parse(statusType, "Todo"));

                tasks.Add(root);
                tasks.Add(obsolete);
                tasks.Add(desired);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                windowType.GetMethod(
                        "CompleteAgentTask",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(
                        window,
                        new object[]
                        {
                            "completion-root",
                            "DeveloperAgent",
                            "Submitted the authoritative graph.",
                            string.Empty,
                            null,
                            false
                        });

                CollectionAssert.AreEquivalent(
                    new[] { "completion-root", "completion-desired" },
                    tasks.Cast<object>()
                        .Select(task => (string)GetTaskField(taskType, task, "Id"))
                        .ToArray());
                Assert.AreEqual("Implementation", GetTaskField(taskType, root, "WorkflowPhase"));
                Assert.AreEqual(string.Empty, GetTaskField(taskType, root, "PlanningAttemptId"));
                Assert.IsFalse((bool)GetTaskField(taskType, desired, "IsDecompositionDraft"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void AutoOrchestratedRoot_AlwaysEntersPlanningBeforeImplementation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoDecompose", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, true);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "narrow-root");
                SetTaskField(taskType, root, "Title", "Create one cohesive report");
                SetTaskField(taskType, root, "Description", "Create the requested report.");
                SetTaskField(taskType, root, "AcceptanceCriteria", "The report exists.");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "Backlog"));
                tasks.Add(root);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                object[] arguments =
                {
                    root,
                    Enum.Parse(statusType, "Todo"),
                    null
                };
                Assert.IsTrue(
                    (bool)windowType.GetMethod(
                            "TrySetTaskStatus",
                            BindingFlags.NonPublic | BindingFlags.Instance)
                        .Invoke(window, arguments));
                Assert.AreEqual("Planning", GetTaskField(taskType, root, "WorkflowPhase"));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void MultiArtifactReadOnlyVerification_IsAtomicExecutionLeaf()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            object verifier = Activator.CreateInstance(taskType);
            SetTaskField(taskType, verifier, "Title", "Verify core script compilation");
            SetTaskField(
                taskType,
                verifier,
                "Description",
                "Read-only verification: inspect and compile the four related gameplay scripts. Do not create or modify product files.");
            SetTaskField(taskType, verifier, "AcceptanceCriteria", "All four scripts compile together with zero errors.");
            SetTaskField(
                taskType,
                verifier,
                "ConsumedArtifactPaths",
                new List<string>
                {
                    "Assets/Game/Player.cs",
                    "Assets/Game/Manager.cs",
                    "Assets/Game/Goal.cs",
                    "Assets/Game/Hazard.cs"
                });

            Assert.IsTrue(
                (bool)windowType.GetMethod(
                        "IsCohesiveReadOnlyVerificationLeaf",
                        BindingFlags.NonPublic | BindingFlags.Static)
                    .Invoke(null, new[] { verifier }));
        }


        [Test]
        public void StructuredDependencies_DoNotReinterpretContradictoryProseAtRuntime()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "structured-only");
                SetTaskField(taskType, task, "ParentTaskId", "root");
                SetTaskField(
                    taskType,
                    task,
                    "Description",
                    "Depends on sibling task \"Missing producer\" being Done. This card does not depend on any sibling in this graph.");
                SetTaskField(taskType, task, "DeclaredDependencyTitles", new List<string>());
                SetTaskField(taskType, task, "DependsOnTaskIds", new List<string>());
                tasks.Add(task);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                ICollection unresolved =
                    (ICollection)windowType.GetMethod(
                            "GetUnresolvedDependencyTitles",
                            BindingFlags.NonPublic | BindingFlags.Instance)
                        .Invoke(window, new[] { task });

                Assert.IsEmpty(unresolved, "Description prose is not runtime graph state after migration.");
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [TestCase("Assets/Game/Player.cs", "", true)]
        [TestCase("Assets/Game/Player.cs", "scene:Assets/Game/Game.unity", false)]
        [TestCase("Assets/Game/Game.unity", "", false)]
        [TestCase("", "", false)]
        public void OwnershipScope_UsesStructuredAuthority(string output, string resource, bool expected)
        {
            var context = new JObject { ["IsChildTask"] = true,
                ["ProducedArtifactPaths"] = new JArray(output), ["MutatedResourceKeys"] = new JArray(resource) };
            Assert.AreEqual(expected, _runnerType.GetMethod("IsSourceArtifactContext",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { context.ToString() }));
        }

        [Test]
        public void OwnershipScope_PreservesExplicitSceneInspectionInputs()
        {
            var context = new JObject { ["IsChildTask"] = true,
                ["ProducedArtifactPaths"] = new JArray("Assets/Game/Player.cs"),
                ["ConsumedArtifactPaths"] = new JArray("Assets/Game/Game.unity"), ["MutatedResourceKeys"] = new JArray() };
            Assert.AreEqual(false, _runnerType.GetMethod("IsSourceArtifactContext",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { context.ToString() }));
        }

        [TestCase("create_game_object")]
        [TestCase("add_editor_component")]
        [TestCase("configure_project_settings")]
        [TestCase("create_editor_scene")]
        public void OwnershipScope_SourceCannotMutateLiveUnityState(string capability)
        {
            Type middlewareType = _runnerType.GetNestedType("KanbanArtifactOwnershipMiddleware", BindingFlags.NonPublic);
            object middleware = Activator.CreateInstance(middlewareType);
            middlewareType.GetMethod("Activate").Invoke(middleware, new object[] {
                new[] { "Assets/Game/Player.cs" }, Array.Empty<string>(), Array.Empty<string>(), true });
            middlewareType.GetMethod("SetSiblingOwnershipContext").Invoke(middleware, new object[] {
                new JObject { ["SiblingContracts"] = new JArray(new JObject { ["Title"] = "Scene Setup",
                    ["TaskId"] = "scene-owner", ["ProducedArtifactPaths"] = new JArray("Assets/Game/Game.unity") }) }.ToString() });
            var error = Assert.Throws<TargetInvocationException>(() => middlewareType.GetMethod("ValidateMutationTarget",
                BindingFlags.NonPublic | BindingFlags.Instance).Invoke(middleware,
                    new object[] { capability, new JObject { ["scenePath"] = "Assets/Game/Game.unity" } }));
            StringAssert.Contains("[ArtifactOwnership]", error.InnerException.Message);
            StringAssert.Contains("scene-owner", error.InnerException.Message);
        }

        [TestCase("[OwnershipScopeRecovery] Review preserved source.")]
        [TestCase("[ReviewFailureKind: EvidenceGap] AIRuntime preserved the completed implementation.")]
        [TestCase("[AssemblyReloadResume] Continue after compilation.")]
        public void OwnershipScope_RecoveryCannotReplaceCardContract(string handoff)
        {
            Assert.AreEqual("Original source acceptance criteria", Invoke<string>("GetClassificationRequest",
                "Original source acceptance criteria", handoff, handoff));
        }

        [Test]
        public void OwnershipScope_WorkerPromptHonorsEvidencePolicy()
        {
            string request = "Inspect the Player Rigidbody component and implement the controller script.";
            string prompt = Invoke<string>("BuildTaskPrompt", "Player Controller", request,
                "Source implements movement.", null, null,
                request + "\n[AIRUNTIME_COMPONENT_EVIDENCE_POLICY:SKIP]", null);
            StringAssert.DoesNotContain("MANDATORY COMPONENT INSPECTION", prompt);
        }

        [Test]
        public void OwnershipScope_ReadsAllOwnedSourceAndExplainsIntegrationBoundary()
        {
            string[] outputs = { "Assets/Game/Player.cs", "Assets/Game/Ground.cs" };
            var paths = (IEnumerable<string>)_runnerType.GetMethod("ResolveOwnedSourceReviewPaths",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { outputs, _testBoardDirectory });
            CollectionAssert.IsSubsetOf(outputs, paths.ToArray());
            var context = new JObject { ["IsChildTask"] = true, ["ProducedArtifactPaths"] = new JArray(outputs),
                ["MutatedResourceKeys"] = new JArray() };
            string prompt = (string)_runnerType.GetMethod("BuildOwnershipScopePrompt",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { context.ToString() });
            StringAssert.Contains("Do not demand live component evidence", prompt);
            StringAssert.Contains("actual source defects", prompt);
        }

        [Test]
        public void OwnershipScope_RepairsOnlySyntheticReviewPauseAndPreservesImplementation()
        {
            using (var f = new PlanningCommitFixture())
            {
                object task = f.Add("script");
                var statusType = f.TaskType.GetField("Status").FieldType;
                SetTaskField(f.TaskType, task, "Status", Enum.Parse(statusType, "Review"));
                SetTaskField(f.TaskType, task, "ReviewStatus", "Waiting for Evidence");
                SetTaskField(f.TaskType, task, "ProducedArtifactPaths", new List<string> { "Assets/Game/Player.cs" });
                SetTaskField(f.TaskType, task, "AgentResult", "preserved implementation");
                SetTaskField(f.TaskType, task, "ReviewResult", "The component inspection evidence is unavailable, so the review cannot verify the diagnosis or any claimed fix.");
                var method = typeof(AIRuntimeKanbanSession).GetMethod("RepairSourceOwnershipReviews", BindingFlags.NonPublic | BindingFlags.Static);
                Assert.AreEqual(1, method.Invoke(null, new object[] { f.Tasks }));
                Assert.AreEqual("Pending", GetTaskField(f.TaskType, task, "ReviewStatus"));
                Assert.AreEqual("preserved implementation", GetTaskField(f.TaskType, task, "AgentResult"));
                Assert.AreEqual(0, method.Invoke(null, new object[] { f.Tasks }));
            }
        }

        [Test]
        public void OwnershipScope_CompletionPersistsWithAutoSaveDisabled()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            var window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(window, false);
                object board = windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(window);
                Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "durable-completion");
                SetTaskField(taskType, task, "Title", "Summarize architecture");
                SetTaskField(taskType, task, "Status", Enum.Parse(taskType.GetField("Status").FieldType, "InProgress"));
                ((IList)board.GetType().GetField("Tasks").GetValue(board)).Add(task);
                Type completionType = windowType.GetNestedType("PendingAgentCompletion", BindingFlags.NonPublic);
                object completion = Activator.CreateInstance(completionType);
                completionType.GetField("TaskId").SetValue(completion, "durable-completion");
                completionType.GetField("AgentId").SetValue(completion, "DeveloperAgent");
                completionType.GetField("Result").SetValue(completion, "Architecture summary complete.");
                object queue = windowType.GetField("_pendingAgentCompletions", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(window);
                queue.GetType().GetMethod("Enqueue").Invoke(queue, new[] { completion });
                windowType.GetMethod("DrainPendingAgentCompletions", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(window, null);
                string saved = File.ReadAllText(Path.Combine(_testBoardDirectory, "board.json"));
                StringAssert.Contains("Architecture summary complete.", saved);
                Assert.AreEqual("Review", GetTaskField(taskType, task, "Status").ToString());
            }
            finally { UnityEngine.Object.DestroyImmediate(window); }
        }

        [TestCase(0)]
        [TestCase(1)]
        [TestCase(3)]
        public void DependencyProse_ParsesEscapedQuoteDelimiters(int escapeCount)
        {
            string quote = new string('\\', escapeCount) + "\"";
            var extract = typeof(AIRuntimeKanbanSession).GetMethod("ExtractDeclaredDependencyTitles",
                BindingFlags.NonPublic | BindingFlags.Static);
            var titles = (IEnumerable<string>)extract.Invoke(null, new object[] {
                "Depends on sibling tasks " + quote + "Producer A" + quote + ", " +
                quote + @"Folder\Producer B" + quote + " being Done." });
            CollectionAssert.AreEqual(new[] { "Producer A", @"Folder\Producer B" }, titles);
        }

        [TestCase(false, false, 1)]
        [TestCase(true, false, 0)]
        [TestCase(false, true, 0)]
        public void LegacyDependencyMigration_RepairsOnlyConfirmedEscapedReferences(
            bool ambiguous, bool exactExists, int expectedChanges)
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            IList tasks = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(taskType));
            string quote = new string('\\', 3) + "\"";
            string description = "Depends on sibling task " + quote + "Producer" + quote + " being Done.";
            var legacy = (IReadOnlyList<string>)windowType.GetMethod("ExtractDependencyTitlesFromProse",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { description, false });
            Assert.AreNotEqual("Producer", legacy[0], "Reproduce persisted parser corruption.");
            object consumer = Activator.CreateInstance(taskType);
            SetTaskField(taskType, consumer, "Description", description);
            SetTaskField(taskType, consumer, "DeclaredDependencyTitles", new List<string>(legacy));
            tasks.Add(consumer);
            foreach (string title in new[] { "Producer", ambiguous ? "Producer" : exactExists ? legacy[0] : "Unrelated" })
            {
                object producer = Activator.CreateInstance(taskType);
                SetTaskField(taskType, producer, "Title", title);
                tasks.Add(producer);
            }
            var migrate = windowType.GetMethod("MigrateLegacyDependencyDeclarations",
                BindingFlags.NonPublic | BindingFlags.Static);
            Assert.AreEqual(expectedChanges, migrate.Invoke(null, new object[] { tasks }));
            CollectionAssert.AreEqual(new[] { expectedChanges == 1 ? "Producer" : legacy[0] },
                (ICollection)GetTaskField(taskType, consumer, "DeclaredDependencyTitles"));
            Assert.AreEqual(0, migrate.Invoke(null, new object[] { tasks }));
        }


        [Test]
        public void LegacyDependencyMigration_PersistsStructuredTitleDeclarations()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type listType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(listType);
            object task = Activator.CreateInstance(taskType);
            SetTaskField(
                taskType,
                task,
                "Description",
                "Depends on sibling tasks \"Producer A\", \"Producer B\" being Done. Verify both outputs.");
            tasks.Add(task);

            MethodInfo migrate = windowType.GetMethod(
                "MigrateLegacyDependencyDeclarations",
                BindingFlags.NonPublic | BindingFlags.Static);

            Assert.AreEqual(1, migrate.Invoke(null, new object[] { tasks }));
            CollectionAssert.AreEquivalent(
                new[] { "Producer A", "Producer B" },
                (ICollection)GetTaskField(taskType, task, "DeclaredDependencyTitles"));
            Assert.AreEqual(0, migrate.Invoke(null, new object[] { tasks }));
        }


        [Test]
        public void PlanningCompletion_VersionsPlanAndRejectsNoOpKickbackRevision()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                object child = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "plan-root");
                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, child, "Id", "plan-child");
                SetTaskField(taskType, child, "Title", "Implement owned output");
                SetTaskField(taskType, child, "Description", "Create the owned output.");
                SetTaskField(taskType, child, "AcceptanceCriteria", "The output exists.");
                SetTaskField(taskType, child, "ParentTaskId", "plan-root");
                tasks.Add(root);
                tasks.Add(child);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo finalize = windowType.GetMethod(
                    "FinalizePlanningPhase",
                    BindingFlags.NonPublic | BindingFlags.Instance);
                MethodInfo fingerprint = windowType.GetMethod(
                    "ComputePlanFingerprint",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                Assert.AreEqual(string.Empty, finalize.Invoke(window, new[] { root }));
                Assert.AreEqual("Implementation", taskType.GetField("WorkflowPhase").GetValue(root));
                Assert.AreEqual(1, taskType.GetField("PlanRevision").GetValue(root));
                Assert.AreEqual(1, taskType.GetField("PlannedRevision").GetValue(child));

                SetTaskField(taskType, root, "WorkflowPhase", "Planning");
                SetTaskField(taskType, root, "PlanningKickbackReason", "Split this card.");
                SetTaskField(
                    taskType,
                    root,
                    "PlanningKickbackFingerprint",
                    fingerprint.Invoke(window, new[] { root }));

                string error = (string)finalize.Invoke(window, new[] { root });
                StringAssert.StartsWith("Planning validation made no structural progress", error);
                Assert.AreEqual("Planning", taskType.GetField("WorkflowPhase").GetValue(root));
                Assert.AreEqual(1, taskType.GetField("PlanRevision").GetValue(root));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void AtomicRoot_AdvancesFromImplementationToFinalReview()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);
                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "atomic-root");
                SetTaskField(taskType, root, "Title", "Implement one script");
                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "Review"));
                tasks.Add(root);
                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                windowType.GetMethod(
                        "AdvanceWorkflowPhases",
                        BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(window, null);

                Assert.AreEqual(
                    "FinalReview",
                    taskType.GetField("WorkflowPhase").GetValue(root));
                Assert.IsTrue(
                    (bool)windowType.GetMethod(
                            "IsEligibleForCurrentWorkflowPhase",
                            BindingFlags.NonPublic | BindingFlags.Instance)
                        .Invoke(window, new[] { root, (object)true }));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void PlanningKickbackAndPlanningReviewDefect_AreExplicitlyClassified()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            MethodInfo extract = windowType.GetMethod(
                "TryExtractPlanningKickback",
                BindingFlags.NonPublic | BindingFlags.Static);
            object[] extractionArguments =
            {
                "[PlanningKickback] This card owns two independent outcomes.",
                null
            };

            Assert.IsTrue((bool)extract.Invoke(null, extractionArguments));
            StringAssert.Contains("two independent outcomes", (string)extractionArguments[1]);

            MethodInfo classify = windowType.GetMethod(
                "ClassifyReviewFailure",
                BindingFlags.NonPublic | BindingFlags.Static);
            Assert.AreEqual(
                "PlanningDefect",
                classify.Invoke(
                    null,
                    new object[]
                    {
                        "[ReviewFailureKind: PlanningDefect] The graph omits integration ownership.",
                        string.Empty
                    }));
        }


        [Test]
        public void OwnedArtifactRemediation_CannotRewriteCommittedPlan()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(
                taskType,
                task,
                "ProducedArtifactPaths",
                new List<string>
                {
                    "Assets/BallJumpGame/Scripts/PlayerController.cs"
                });

            MethodInfo classify = windowType.GetMethod(
                "TryGetOwnedArtifactRemediation",
                BindingFlags.NonPublic | BindingFlags.Static);

            object[] localArguments =
            {
                task,
                "PlayerController should provide a public SetControlEnabled(bool) method.",
                null
            };

            Assert.IsTrue(
                (bool)classify.Invoke(null, localArguments));
            Assert.AreEqual(
                "Assets/BallJumpGame/Scripts/PlayerController.cs",
                localArguments[2]);

            object[] structuralArguments =
            {
                task,
                "The graph has an actual dependency cycle between two independently owned outcomes.",
                null
            };

            Assert.IsFalse(
                (bool)classify.Invoke(null, structuralArguments));
        }


        [TestCase("AIRuntimeKanbanSession.cs should provide a local correction method.")]
        [TestCase("The task requires creating the artifact. I have successfully created it. All acceptance criteria are met. No further work is needed. The task is complete.")]
        [TestCase("Structural defect: starting position unspecified. However the implementation satisfies the requirements. No PlanningKickback is needed.")]
        public void OwnedArtifactKickback_WithPersistedOutput_RoutesToReview(string reason)
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType(
                "KanbanTask",
                BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType(
                "KanbanBoard",
                BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType(
                "TaskStatus",
                BindingFlags.NonPublic);

            AIRuntimeKanbanSession window =
                ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                object board = Activator.CreateInstance(boardType);
                IList tasks =
                    (IList)boardType.GetField("Tasks").GetValue(board);

                object root = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "root");
                SetTaskField(taskType, root, "Title", "Build game");
                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "Todo"));

                object child = Activator.CreateInstance(taskType);
                SetTaskField(taskType, child, "Id", "child");
                SetTaskField(taskType, child, "ParentTaskId", "root");
                SetTaskField(taskType, child, "Title", "Implement window");
                SetTaskField(taskType, child, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(
                    taskType,
                    child,
                    "ProducedArtifactPaths",
                    new List<string>
                    {
                        TestAssetPaths.Resolve("Assets/Editor/AIRuntimeKanbanSession.cs")
                    });

                tasks.Add(root);
                tasks.Add(child);

                windowType.GetField(
                        "_board",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .SetValue(window, board);

                windowType.GetMethod(
                        "RequestPlanningRevision",
                        BindingFlags.NonPublic |
                        BindingFlags.Instance)
                    .Invoke(
                        window,
                        new[]
                        {
                            child,
                            reason,
                            "Implementation"
                        });

                Assert.AreEqual(
                    "Review",
                    taskType.GetField("Status").GetValue(child).ToString());
                Assert.AreEqual(
                    "Pending",
                    taskType.GetField("ReviewStatus").GetValue(child));
                StringAssert.Contains(
                    "Persisted work was preserved",
                    (string)taskType.GetField("AgentResult").GetValue(child));
                Assert.AreEqual(
                    "Implementation",
                    taskType.GetField("WorkflowPhase").GetValue(root));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [TestCase("Create a script.\n- Place script under Assets/Game/Player.cs", false, false, true)]
        [TestCase("Create a script.\n- Place script under Assets/Game/Player.cs", true, false, false)]
        [TestCase("Create a script.\n- Place script under Assets/Game/Player.cs", false, true, true)]
        [TestCase("Inspect Assets/Game/Player.cs and report findings.", false, false, false)]
        [TestCase("Do not modify Assets/Game/Player.cs.", false, false, false)]
        public void ExplicitOutputContract_RequiresExactWriteAuthority(string description, bool owns, bool consumes, bool rejected)
        {
            Type taskType = typeof(AIRuntimeKanbanSession).GetNestedType("KanbanTask", BindingFlags.NonPublic);
            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Title", "Player");
            SetTaskField(taskType, task, "Description", description);
            SetTaskField(taskType, task, "ProducedArtifactPaths", new List<string> { owns ? "Assets/Game/Player.cs" : "Assets/Game" });
            SetTaskField(taskType, task, "ConsumedArtifactPaths", consumes ? new List<string> { "Assets/Game/Player.cs" } : new List<string>());
            string error = (string)typeof(AIRuntimeKanbanSession).GetMethod("GetMissingExplicitOutputContract",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new[] { task });
            Assert.AreEqual(rejected, !string.IsNullOrWhiteSpace(error));
            if (rejected) StringAssert.Contains("Assets/Game/Player.cs", error);
        }

        [Test]
        public void Dispatch_MissingExplicitOutput_ReturnsToPlanningBeforeCallingProvider()
        {
            Type type = typeof(AIRuntimeKanbanSession);
            Type taskType = type.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = type.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            var session = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();
            try
            {
                type.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(session, false);
                object board = Activator.CreateInstance(boardType);
                var tasks = (IList)boardType.GetField("Tasks").GetValue(board);
                object root = Activator.CreateInstance(taskType), child = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "root");
                SetTaskField(taskType, root, "Title", "Game");
                SetTaskField(taskType, root, "WorkflowPhase", "Implementation");
                SetTaskField(taskType, child, "Id", "child");
                SetTaskField(taskType, child, "ParentTaskId", "root");
                SetTaskField(taskType, child, "Title", "Player");
                SetTaskField(taskType, child, "Description", "Create a script.\n- Place script under Assets/Game/Player.cs");
                tasks.Add(root); tasks.Add(child);
                type.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(session, board);
                bool started = (bool)type.GetMethod("StartAgentTask", BindingFlags.NonPublic | BindingFlags.Instance)
                    .Invoke(session, new[] { child, (object)"MissingProviderMustNotBeCalled" });
                Assert.IsFalse(started);
                Assert.AreEqual("Planning", GetTaskField(taskType, root, "WorkflowPhase"));
                Assert.AreEqual("Planning Required", GetTaskField(taskType, child, "ReviewStatus"));
                Assert.AreEqual(2, tasks.Count);
                SetTaskField(taskType, child, "AcceptanceCriteria", "Script compiles.");
                var validate = type.GetMethod("ValidatePlanningReadiness", BindingFlags.NonPublic | BindingFlags.Instance);
                StringAssert.Contains("[MissingArtifactOwnership]", (string)validate.Invoke(session, new[] { root }));
                SetTaskField(taskType, child, "ProducedArtifactPaths", new List<string> { "Assets/Game/Player.cs" });
                Assert.IsEmpty((string)validate.Invoke(session, new[] { root }), "A corrected exact output contract can proceed.");
            }
            finally { UnityEngine.Object.DestroyImmediate(session); }
        }
        [Test]
        public void PlanningContract_RejectsProducedArtifactProhibitedByItsOwnProse()
        {
            MethodInfo contradictory = typeof(AIRuntimeKanbanSession).GetMethod(
                "HasContradictoryProducedArtifactProhibition",
                BindingFlags.NonPublic | BindingFlags.Static);
            CreateKanbanChildTaskArguments arguments =
                new CreateKanbanChildTaskArguments
                {
                    Description = "Inspect the source. Do not create or touch any asset.",
                    AcceptanceCriteria = "Write the report.",
                    ProducesArtifactPaths = new List<string>
                    {
                        "Assets/Reports/Inspection.report.md"
                    }
                };

            Assert.IsTrue((bool)contradictory.Invoke(null, new object[] { arguments }));

            arguments.Description =
                "Inspect the source without modifying gameplay assets; create only the declared report.";
            Assert.IsFalse((bool)contradictory.Invoke(null, new object[] { arguments }));
        }


        [Test]
        public void IncompleteReviewEvidence_RefreshesOnceThenWaitsWithoutLooping()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "incomplete-review-evidence");
                SetTaskField(taskType, task, "Title", "Incomplete review evidence");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));
                SetTaskField(taskType, task, "AgentResult", "Implementation completed.");

                MethodInfo completeReview = windowType.GetMethod(
                    "CompleteReviewTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                completeReview.Invoke(
                    window,
                    new object[]
                    {
                        task,
                        "ReviewerAgent",
                        "KANBAN_REVIEW: PASS\nAll criteria appear satisfied.",
                        string.Empty,
                        "{\"ComponentInspection\":\"[AIRUNTIME_REVIEW_EVIDENCE_INCOMPLETE]\"}"
                    });

                Assert.AreEqual(
                    "Review",
                    taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual(
                    "Pending",
                    taskType.GetField("ReviewStatus").GetValue(task));
                Assert.AreEqual(
                    "EvidenceGap",
                    taskType.GetField("ReviewFailureKind").GetValue(task));

                completeReview.Invoke(
                    window,
                    new object[]
                    {
                        task,
                        "ReviewerAgent",
                        "KANBAN_REVIEW: PASS\nAll criteria appear satisfied.",
                        string.Empty,
                        "{\"ComponentInspection\":\"[AIRUNTIME_REVIEW_EVIDENCE_INCOMPLETE]\"}"
                    });

                Assert.AreEqual(
                    "Review",
                    taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual(
                    "Waiting for Evidence",
                    taskType.GetField("ReviewStatus").GetValue(task));
                Assert.AreEqual(
                    0,
                    taskType.GetField("ReviewFailureCount").GetValue(task));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void MissingArtifactEvidence_OverridesIncompleteEvidenceAndRequestsRework()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "missing-artifact-review-evidence");
                SetTaskField(taskType, task, "Title", "Missing artifact review evidence");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Review"));
                SetTaskField(taskType, task, "AgentResult", "[TaskCompleted]");

                MethodInfo completeReview = windowType.GetMethod(
                    "CompleteReviewTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                completeReview.Invoke(
                    window,
                    new object[]
                    {
                        task,
                        "ReviewerAgent",
                        "KANBAN_REVIEW: FAIL\nThe required file does not exist.",
                        string.Empty,
                        "{\"FileContents\":{\"Assets/Game.cs\":{\"FailureKind\":\"MissingArtifact\"}},\"ComponentInspection\":\"[AIRUNTIME_REVIEW_EVIDENCE_INCOMPLETE]\"}"
                    });

                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual(
                    "Rework Requested",
                    taskType.GetField("ReviewStatus").GetValue(task));
                Assert.AreEqual(
                    "ImplementationDefect",
                    taskType.GetField("ReviewFailureKind").GetValue(task));
                Assert.AreEqual(
                    1,
                    taskType.GetField("ReviewFailureCount").GetValue(task));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [TestCase(true)]
        [TestCase(false)]
        public void DownstreamReviewDefect_ReopensExactConsumedArtifactOwner(bool includeMarker)
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type boardType = windowType.GetNestedType("KanbanBoard", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object board = Activator.CreateInstance(boardType);
                IList tasks = (IList)boardType.GetField("Tasks").GetValue(board);

                object root = Activator.CreateInstance(taskType);
                SetTaskField(taskType, root, "Id", "root");
                SetTaskField(taskType, root, "Title", "Build game");
                SetTaskField(taskType, root, "Status", Enum.Parse(statusType, "Todo"));
                SetTaskField(taskType, root, "WorkflowPhase", "FinalReview");
                SetTaskField(taskType, root, "ReviewStatus", "Pending");
                SetTaskField(taskType, root, "ReviewFailureKind", "PlanningDefect");
                SetTaskField(taskType, root, "PlanningKickbackReason", "stale planning state");
                SetTaskField(taskType, root, "NextAgentMessage", "stale planning instructions");

                object owner = Activator.CreateInstance(taskType);
                SetTaskField(taskType, owner, "Id", "camera-owner");
                SetTaskField(taskType, owner, "ParentTaskId", "root");
                SetTaskField(taskType, owner, "Title", "Camera Controller");
                SetTaskField(taskType, owner, "Status", Enum.Parse(statusType, "Done"));
                SetTaskField(
                    taskType,
                    owner,
                    "ProducedArtifactPaths",
                    new List<string>
                    {
                        "Assets/Game/Scripts/CameraController.cs"
                    });

                object integration = Activator.CreateInstance(taskType);
                SetTaskField(taskType, integration, "Id", "integration");
                SetTaskField(taskType, integration, "ParentTaskId", "root");
                SetTaskField(taskType, integration, "Title", "Final Integration");
                SetTaskField(taskType, integration, "Status", Enum.Parse(statusType, "Review"));
                SetTaskField(taskType, integration, "AgentResult", "Integration verification completed.");
                SetTaskField(
                    taskType,
                    integration,
                    "ConsumedArtifactPaths",
                    new List<string>
                    {
                        "Assets/Game/Scripts/CameraController.cs",
                        "Assets/Game/Scenes/Game.unity"
                    });
                SetTaskField(
                    taskType,
                    integration,
                    "MutatedResourceKeys",
                    new List<string>
                    {
                        "scene:Assets/Game/Scenes/Game.unity"
                    });

                tasks.Add(root);
                tasks.Add(owner);
                tasks.Add(integration);

                windowType.GetField("_board", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, board);

                MethodInfo completeReview = windowType.GetMethod(
                    "CompleteReviewTask",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                completeReview.Invoke(
                    window,
                    new object[]
                    {
                        integration,
                        "ReviewerAgent",
                        "KANBAN_REVIEW: FAIL\n" +
                        "[ReviewFailureKind: ImplementationDefect]\n" +
                        (includeMarker ? "[UpstreamArtifactDefect: Assets/Game/Scripts/CameraController.cs]\n" : "The owning card (camera-owner) must repair CameraController.cs.\n") +
                        "CameraController moves the wrong transform during play.",
                        string.Empty,
                        "{}"
                    });

                Assert.AreEqual(
                    "Todo",
                    taskType.GetField("Status").GetValue(owner).ToString());
                Assert.AreEqual(
                    "Rework Requested",
                    taskType.GetField("ReviewStatus").GetValue(owner));
                StringAssert.Contains(
                    "Downstream card 'Final Integration'",
                    (string)taskType.GetField("NextAgentMessage").GetValue(owner));
                Assert.AreEqual(
                    "Review",
                    taskType.GetField("Status").GetValue(integration).ToString());
                Assert.AreEqual(
                    "Blocked by Upstream",
                    taskType.GetField("ReviewStatus").GetValue(integration));
                Assert.AreEqual(
                    "camera-owner",
                    taskType.GetField("BlockedByTaskId").GetValue(integration));
                Assert.AreEqual(
                    "Implementation",
                    taskType.GetField("WorkflowPhase").GetValue(root));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("ReviewStatus").GetValue(root));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("ReviewFailureKind").GetValue(root));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("PlanningKickbackReason").GetValue(root));
                Assert.AreEqual(
                    string.Empty,
                    taskType.GetField("NextAgentMessage").GetValue(root));
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void ReviewPrompt_ExplainsExactUpstreamArtifactRouting()
        {
            string prompt =
                Invoke<string>(
                    "BuildReviewPrompt",
                    "Final Integration",
                    "Verify the completed game.",
                    "The game is playable.",
                    "Verification complete.",
                    null,
                    null,
                    null,
                    null,
                    null,
                    null,
                    "Verify the completed game.");

            StringAssert.Contains(
                "[UpstreamArtifactDefect: Assets/exact/path.ext]",
                prompt);
            StringAssert.Contains(
                "reopens only its canonical owner",
                prompt);
        }


        [Test]
        public void LegacyRateLimitCrash_IsMigratedToResumableImplementation()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            Type taskListType = typeof(List<>).MakeGenericType(taskType);
            IList tasks = (IList)Activator.CreateInstance(taskListType);

            object task = Activator.CreateInstance(taskType);
            SetTaskField(taskType, task, "Id", "rate-limited-card");
            SetTaskField(taskType, task, "Title", "Resume provider work");
            SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "Crashed"));
            SetTaskField(
                taskType,
                task,
                "AgentError",
                "Nous Portal request failed. HTTP 429. exceeded the rate limit");
            SetTaskField(taskType, task, "ExecutionMemory", "created PlayerController.cs");
            tasks.Add(task);

            MethodInfo recover = windowType.GetMethod(
                "RecoverLegacyProviderRateLimitFailures",
                BindingFlags.NonPublic | BindingFlags.Static);

            int count = (int)recover.Invoke(null, new object[] { tasks });

            Assert.AreEqual(1, count);
            Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(task).ToString());
            StringAssert.StartsWith(
                "[ProviderRateLimitRecovery]",
                (string)taskType.GetField("NextAgentMessage").GetValue(task));
            Assert.AreEqual(
                "created PlayerController.cs",
                taskType.GetField("ExecutionMemory").GetValue(task));
        }


        [Test]
        public void RateLimitFailure_PausesProviderLaneAndKeepsCardResumable()
        {
            Type windowType = typeof(AIRuntimeKanbanSession);
            Type taskType = windowType.GetNestedType("KanbanTask", BindingFlags.NonPublic);
            Type statusType = windowType.GetNestedType("TaskStatus", BindingFlags.NonPublic);
            AIRuntimeKanbanSession window = ScriptableObject.CreateInstance<AIRuntimeKanbanSession>();

            try
            {
                windowType.GetField("_autoSave", BindingFlags.NonPublic | BindingFlags.Instance)
                    .SetValue(window, false);

                object task = Activator.CreateInstance(taskType);
                SetTaskField(taskType, task, "Id", "active-rate-limit");
                SetTaskField(taskType, task, "Title", "Interrupted implementation");
                SetTaskField(taskType, task, "AssignedAgent", "DeveloperAgent");
                SetTaskField(taskType, task, "Status", Enum.Parse(statusType, "InProgress"));
                SetTaskField(taskType, task, "ExecutionMemory", "existing artifacts remain");

                MethodInfo apply = windowType.GetMethod(
                    "ApplyProviderCooldown",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                apply.Invoke(
                    window,
                    new object[]
                    {
                        task,
                        "DeveloperAgent",
                        "'Nous Portal' [RateLimited]: HTTP 429. Retry after 17 seconds.",
                        false
                    });

                Assert.AreEqual("Todo", taskType.GetField("Status").GetValue(task).ToString());
                Assert.AreEqual(
                    "Provider Cooldown",
                    taskType.GetField("ReviewStatus").GetValue(task));
                StringAssert.StartsWith(
                    "[ProviderCooldown]",
                    (string)taskType.GetField("NextAgentMessage").GetValue(task));
                Assert.AreEqual(
                    "existing artifacts remain",
                    taskType.GetField("ExecutionMemory").GetValue(task));

                MethodInfo isCoolingDown = windowType.GetMethod(
                    "IsAgentProviderCoolingDown",
                    BindingFlags.NonPublic | BindingFlags.Instance);

                object[] arguments = { "DeveloperAgent", null };
                Assert.IsTrue((bool)isCoolingDown.Invoke(window, arguments));
                Assert.IsNotNull(arguments[1]);
            }
            finally
            {
                UnityEngine.Object.DestroyImmediate(window);
            }
        }


        [Test]
        public void RepeatedRateLimits_UsePersistedExponentialBackoffFloor()
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "ApplyProviderCooldownBackoff",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            TimeSpan first =
                (TimeSpan)method.Invoke(
                    null,
                    new object[]
                    {
                        TimeSpan.FromSeconds(17),
                        1
                    });

            TimeSpan second =
                (TimeSpan)method.Invoke(
                    null,
                    new object[]
                    {
                        TimeSpan.FromSeconds(17),
                        2
                    });

            TimeSpan sixth =
                (TimeSpan)method.Invoke(
                    null,
                    new object[]
                    {
                        TimeSpan.FromSeconds(17),
                        6
                    });

            Assert.AreEqual(
                TimeSpan.FromMinutes(5),
                first);

            Assert.AreEqual(
                TimeSpan.FromMinutes(10),
                second);

            Assert.AreEqual(
                TimeSpan.FromHours(2),
                sixth,
                "The automatic backoff floor must be capped without shortening a longer provider Retry-After value.");
        }


        private static JObject Child(
            string title,
            string description)
        {
            return new JObject
            {
                ["Title"] = title,
                ["Description"] = description,
                ["AcceptanceCriteria"] = string.Empty
            };
        }


        private static JObject Hierarchy(
            params JObject[] roots)
        {
            return new JObject
            {
                ["Truncated"] = false,
                ["Roots"] = new JArray(roots ?? Array.Empty<JObject>())
            };
        }


        private static JObject Node(
            int instanceId,
            string name,
            string hierarchyPath,
            params JObject[] children)
        {
            return new JObject
            {
                ["InstanceId"] = instanceId,
                ["Name"] = name,
                ["HierarchyPath"] = hierarchyPath,
                ["DepthLimited"] = false,
                ["Children"] = new JArray(children ?? Array.Empty<JObject>())
            };
        }


        private IList InvokeHierarchyTargetSelection(
            object[] arguments)
        {
            MethodInfo method =
                _runnerType.GetMethod(
                    "SelectContractHierarchyTargets",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(method);

            return (IList)method.Invoke(
                null,
                arguments);
        }


        private static JObject GraphChild(
            string id,
            string title,
            string description,
            params string[] dependencyIds)
        {
            return new JObject
            {
                ["TaskId"] = id,
                ["Title"] = title,
                ["Description"] = description,
                ["AcceptanceCriteria"] = "The focused deliverable is verified.",
                ["DependsOnTaskIds"] = new JArray(dependencyIds ?? Array.Empty<string>())
            };
        }


        private static bool InvokeWindowVerdict(
            string result,
            string verdict)
        {
            MethodInfo method =
                typeof(AIRuntimeKanbanSession).GetMethod(
                    "HasReviewVerdict",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                method);

            return (bool)method.Invoke(
                null,
                new object[]
                {
                    result,
                    verdict
                });
        }


        private T Invoke<T>(
            string methodName,
            params object[] arguments)
        {
            MethodInfo method =
                _runnerType.GetMethod(
                    methodName,
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(
                method,
                $"Expected private runner method '{methodName}'.");

            try
            {
                return (T)method.Invoke(
                    null,
                    arguments);
            }
            catch (TargetInvocationException exception)
            {
                throw exception.InnerException ??
                      exception;
            }
        }
    }
}
