commit 4e18dc01219c571358ef06c4fdcc97290494e9fc
Author: softdaddy-o <softdaddy.official@gmail.com>
Date:   Thu Apr 23 12:57:41 2026 +0900

    feat: add runtime console and validation tooling

diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 87068e9..2a09090 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -2,6 +2,21 @@
 
 All notable changes to soft-ue-cli will be documented in this file.
 
+## [1.25.7] - 2026-04-23
+
+### Added
+- new `exec-console-command` command executes arbitrary UE console commands directly in editor, PIE, or game worlds without requiring a Python wrapper script
+- new `inspect-pawn-possession` command returns structured controller/pawn possession state, AI auto-possession settings, and visibility data for runtime debugging
+- new `validate-class-path` command verifies whether a soft class path exists, loads, resolves to a `UClass`, and reports its parent hierarchy
+- new `request-gameplay-tag` helper resolves a registered gameplay tag name and returns validity plus export text
+- new `reload-gameplay-tags` helper reloads GameplayTags settings and refreshes the in-memory tag tables
+- new `release-asset-lock` best-effort command closes asset editors and forces GC to reduce editor-side file-handle conflicts during VCS workflows
+
+### Changed
+- `get-logs` now supports server-side `--contains`, cursor/timestamp-based `--since`, and `--tail-follow` polling for targeted runtime log inspection
+- `run-python-script --world pie`, `exec-console-command`, and `inspect-pawn-possession` now fail fast with a structured `PIE_NOT_RUNNING` error unless PIE is already running or auto-start is requested
+- `trigger-live-coding` now warns before dispatch when git reports changed reflected header files that are likely to make Live Coding cancel
+
 ## [1.25.6] - 2026-04-22
 
 ### Fixed
diff --git a/cli/README.md b/cli/README.md
index 9e2399c..d9a0e64 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -236,6 +236,7 @@ Every command is available via `soft-ue-cli <command>`. Run `soft-ue-cli <comman
 | `query-struct` | Inspect a UserDefinedStruct asset -- authored member names, defaults, and metadata |
 | `create-asset` | Create new Blueprint, Material, DataTable, World (Level), or other asset types |
 | `delete-asset` | Delete an asset |
+| `release-asset-lock` | Best-effort close editors and release UE file handles for a specific asset |
 | `set-asset-property` | Set a property on a Blueprint CDO or component, including nested `InstancedStruct` members |
 | `get-asset-diff` | Get property-level diff of an asset vs. source control |
 | `get-asset-preview` | Get a thumbnail/preview image of an asset |
@@ -254,14 +255,17 @@ Every command is available via `soft-ue-cli <command>`. Run `soft-ue-cli <comman
 | Command | Description |
 |---------|-------------|
 | `class-hierarchy` | Inspect class inheritance chains -- ancestors, descendants, or both |
+| `validate-class-path` | Verify that a soft class path exists, loads, and resolves to a `UClass` |
 
 ### Play-In-Editor (PIE) Control
 
 | Command | Description |
 |---------|-------------|
+| `exec-console-command` | Execute arbitrary UE console commands directly in editor, PIE, or game worlds |
 | `pie-session` | Start, stop, pause, resume PIE -- also query actor state during play |
 | `pie-tick` | Start PIE if needed and advance the world deterministically by frame count |
 | `inspect-anim-instance` | Snapshot a target actor's live `UAnimInstance` state, montages, slot activity, and blend weights |
+| `inspect-pawn-possession` | Inspect controller/pawn links, AI auto-possession, and hidden state in a running world |
 | `trigger-input` | Send input events to a running game (PIE or packaged build) |
 
 ### Screenshot and Visual Capture
@@ -276,10 +280,17 @@ Every command is available via `soft-ue-cli <command>`. Run `soft-ue-cli <comman
 
 | Command | Description |
 |---------|-------------|
-| `get-logs` | Read the UE output log with optional category and text filters |
+| `get-logs` | Read the UE output log with substring filters, cursors, and follow mode |
 | `get-console-var` | Read the value of a console variable (CVar) |
 | `set-console-var` | Set a console variable value |
 
+### Gameplay Tags
+
+| Command | Description |
+|---------|-------------|
+| `request-gameplay-tag` | Resolve a registered GameplayTag by name and return validity/export text |
+| `reload-gameplay-tags` | Reload GameplayTags settings and refresh tag tables where supported |
+
 ### Python Scripting in UE
 
 | Command | Description |
@@ -340,7 +351,7 @@ Requires the **Animation Insights (GameplayInsights)** plugin enabled in Edit >
 | Command | Description |
 |---------|-------------|
 | `build-and-relaunch` | Trigger a full C++ rebuild and optionally relaunch the editor (`--wait` to monitor progress) |
-| `trigger-live-coding` | Trigger a Live Coding compile (hot reload); waits for result by default |
+| `trigger-live-coding` | Trigger a Live Coding compile (hot reload); warns on risky reflected header changes |
 
 ### Skills (LLM Workflow Prompts)
 
@@ -410,6 +421,12 @@ soft-ue-cli batch-call --calls '[
 soft-ue-cli call-function --class-path /Script/Engine.Actor --function-name K2_GetActorLocation --spawn-transient
 ```
 
+### Validate a class path before spawning
+
+```bash
+soft-ue-cli validate-class-path /Game/Characters/BP_Hero.BP_Hero_C
+```
+
 ### Tick PIE and inspect animation state
 
 ```bash
@@ -417,6 +434,20 @@ soft-ue-cli pie-tick --frames 30
 soft-ue-cli inspect-anim-instance --actor-tag TestCharacter --include state_machines,montages
 ```
 
+### Execute a console command directly in PIE
+
+```bash
+soft-ue-cli exec-console-command stat fps
+soft-ue-cli exec-console-command --player-index 0 MyGame.MyCommand arg1 arg2
+```
+
+### Inspect possession state during PIE
+
+```bash
+soft-ue-cli inspect-pawn-possession
+soft-ue-cli inspect-pawn-possession --class-filter Character
+```
+
 ### Inspect a Blueprint's components and variables
 
 ```bash
@@ -489,6 +520,13 @@ soft-ue-cli compile-blueprint /Game/ABP_Hero
 soft-ue-cli save-asset /Game/ABP_Hero
 ```
 
+### Refresh GameplayTags after editing config
+
+```bash
+soft-ue-cli reload-gameplay-tags
+soft-ue-cli request-gameplay-tag Status.Effect.Burning
+```
+
 ### Disconnect a specific wire (preserving others)
 
 ```bash
diff --git a/cli/pyproject.toml b/cli/pyproject.toml
index f7799cd..f060621 100644
--- a/cli/pyproject.toml
+++ b/cli/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "soft-ue-cli"
-version = "1.25.6"
+version = "1.25.7"
 description = "CLI tool for controlling Unreal Engine via soft-ue-bridge plugin"
 requires-python = ">=3.10"
 classifiers = [
diff --git a/cli/soft_ue_cli/__init__.py b/cli/soft_ue_cli/__init__.py
index e59528e..2eec4e6 100644
--- a/cli/soft_ue_cli/__init__.py
+++ b/cli/soft_ue_cli/__init__.py
@@ -1,3 +1,3 @@
 """soft-ue-cli — CLI interface to the SoftUEBridge UE plugin."""
 
-__version__ = "1.25.6"
+__version__ = "1.25.7"
diff --git a/cli/soft_ue_cli/__main__.py b/cli/soft_ue_cli/__main__.py
index 40ca09f..f1c49ba 100644
--- a/cli/soft_ue_cli/__main__.py
+++ b/cli/soft_ue_cli/__main__.py
@@ -96,6 +96,48 @@ def _parse_json_arg(value: str, flag: str) -> object:
         sys.exit(1)
 
 
+def _emit_structured_error(code: str, message: str, **extra: object) -> None:
+    payload = {"success": False, "code": code, "error": message}
+    payload.update(extra)
+    _print_json(payload)
+    sys.exit(1)
+
+
+def _query_pie_state() -> dict | None:
+    from .errors import BridgeError
+
+    try:
+        return call_tool("pie-session", {"action": "get-state"})
+    except BridgeError:
+        return None
+
+
+def _ensure_pie_running(
+    *,
+    auto_start: bool,
+    map_path: str | None = None,
+    timeout: float | None = None,
+    command_name: str,
+) -> None:
+    state = _query_pie_state()
+    if state and state.get("state") in {"running", "starting"}:
+        return
+
+    if not auto_start:
+        _emit_structured_error(
+            "PIE_NOT_RUNNING",
+            "PIE session is not running. Start PIE first or use --auto-start-pie.",
+            command=command_name,
+        )
+
+    start_args: dict[str, object] = {"action": "start"}
+    if map_path:
+        start_args["map"] = map_path
+    if timeout is not None:
+        start_args["timeout"] = timeout
+    call_tool("pie-session", start_args)
+
+
 # -- Command handlers ----------------------------------------------------------
 
 
@@ -261,16 +303,42 @@ def cmd_get_property(args: argparse.Namespace) -> None:
 
 def cmd_get_logs(args: argparse.Namespace) -> None:
     arguments: dict = {"lines": args.lines}
-    if args.filter:
-        arguments["filter"] = args.filter
+    filter_text = args.contains or args.filter
+    if filter_text:
+        arguments["filter"] = filter_text
     if args.category:
         arguments["category"] = args.category
-    result = _run_tool("get-logs", arguments)
-    if args.raw:
-        for line in result.get("lines", []):
-            print(line)
-    else:
-        _print_json(result)
+    if args.since:
+        arguments["since"] = args.since
+
+    if not args.tail_follow:
+        result = _run_tool("get-logs", arguments)
+        if args.raw:
+            for line in result.get("lines", []):
+                print(line)
+        else:
+            _print_json(result)
+        return
+
+    # tail-follow starts from "now" if no cursor/timestamp is provided explicitly
+    if not args.since:
+        snapshot = _run_tool("get-logs", {"lines": 0})
+        arguments["since"] = snapshot.get("next_cursor") or snapshot.get("last_timestamp") or ""
+
+    try:
+        while True:
+            result = _run_tool("get-logs", arguments)
+            if args.raw:
+                for line in result.get("lines", []):
+                    print(line)
+            else:
+                _print_json(result)
+            next_cursor = result.get("next_cursor")
+            if next_cursor:
+                arguments["since"] = str(next_cursor)
+            time.sleep(args.poll_interval)
+    except KeyboardInterrupt:
+        return
 
 
 def cmd_get_console_var(args: argparse.Namespace) -> None:
@@ -295,6 +363,13 @@ def cmd_class_hierarchy(args: argparse.Namespace) -> None:
     _print_json(_run_tool("get-class-hierarchy", arguments))
 
 
+def cmd_validate_class_path(args: argparse.Namespace) -> None:
+    arguments: dict = {"class_path": args.class_path}
+    if args.parent_depth is not None:
+        arguments["parent_depth"] = args.parent_depth
+    _print_json(_run_tool("validate-class-path", arguments))
+
+
 def cmd_query_asset(args: argparse.Namespace) -> None:
     arguments: dict = {}
     if args.query:
@@ -334,6 +409,10 @@ def cmd_delete_asset(args: argparse.Namespace) -> None:
     _print_json(_run_tool("delete-asset", {"asset_path": args.asset_path}))
 
 
+def cmd_release_asset_lock(args: argparse.Namespace) -> None:
+    _print_json(_run_tool("release-asset-lock", {"asset_path": args.asset_path}))
+
+
 def cmd_get_asset_diff(args: argparse.Namespace) -> None:
     arguments: dict = {"asset_path": args.asset_path}
     if args.scm_type:
@@ -654,6 +733,8 @@ def cmd_trigger_live_coding(args: argparse.Namespace) -> None:
     arguments: dict = {}
     if not args.no_wait:
         arguments["wait_for_completion"] = True
+    if args.allow_header_changes:
+        arguments["allow_header_changes"] = True
     _print_json(_run_tool("trigger-live-coding", arguments))
 
 
@@ -717,6 +798,24 @@ def cmd_query_mpc(args: argparse.Namespace) -> None:
     _print_json(_run_tool("query-mpc", arguments))
 
 
+def cmd_exec_console_command(args: argparse.Namespace) -> None:
+    if args.world == "pie":
+        _ensure_pie_running(
+            auto_start=args.auto_start_pie,
+            map_path=args.map,
+            timeout=args.pie_timeout,
+            command_name="exec-console-command",
+        )
+
+    arguments: dict = {
+        "command": " ".join(args.command_parts),
+        "world": args.world,
+    }
+    if args.player_index is not None:
+        arguments["player_index"] = args.player_index
+    _print_json(_run_tool("exec-console-command", arguments))
+
+
 def cmd_pie_session(args: argparse.Namespace) -> None:
     arguments: dict = {"action": args.action}
     if args.mode:
@@ -740,6 +839,25 @@ def cmd_pie_session(args: argparse.Namespace) -> None:
     _print_json(_run_tool("pie-session", arguments))
 
 
+def cmd_inspect_pawn_possession(args: argparse.Namespace) -> None:
+    if args.world == "pie":
+        _ensure_pie_running(
+            auto_start=args.auto_start_pie,
+            map_path=args.map,
+            timeout=args.pie_timeout,
+            command_name="inspect-pawn-possession",
+        )
+
+    arguments: dict = {"world": args.world}
+    if args.class_filter:
+        arguments["class_filter"] = args.class_filter
+    if args.actor_name:
+        arguments["actor_name"] = args.actor_name
+    if args.include_hidden:
+        arguments["include_hidden"] = True
+    _print_json(_run_tool("inspect-pawn-possession", arguments))
+
+
 def cmd_pie_tick(args: argparse.Namespace) -> None:
     arguments: dict = {"frames": args.frames}
     if args.delta is not None:
@@ -872,12 +990,27 @@ def cmd_run_python_script(args: argparse.Namespace) -> None:
     if args.python_paths:
         arguments["python_paths"] = args.python_paths
     if args.world:
+        if args.world == "pie":
+            _ensure_pie_running(
+                auto_start=args.auto_start_pie,
+                map_path=args.map,
+                timeout=args.pie_timeout,
+                command_name="run-python-script",
+            )
         arguments["world"] = args.world
     if args.arguments:
         arguments["arguments"] = _parse_json_arg(args.arguments, "--arguments")
     _print_json(_run_tool("run-python-script", arguments))
 
 
+def cmd_request_gameplay_tag(args: argparse.Namespace) -> None:
+    _print_json(_run_tool("request-gameplay-tag", {"tag_name": args.tag_name}))
+
+
+def cmd_reload_gameplay_tags(args: argparse.Namespace) -> None:
+    _print_json(_run_tool("reload-gameplay-tags", {}))
+
+
 def cmd_save_script(args: argparse.Namespace) -> None:
     if not args.script and not args.script_path:
         print("error: provide --script or --script-path", file=sys.stderr)
@@ -2288,7 +2421,17 @@ def build_parser() -> argparse.ArgumentParser:
         "--lines", type=int, default=100, metavar="N", help="Number of recent lines to return (default: 100)"
     )
     p_gl.add_argument("--filter", metavar="TEXT", help="Case-insensitive substring filter on log text")
+    p_gl.add_argument("--contains", metavar="TEXT", help="Alias for --filter; substring filter applied server-side")
     p_gl.add_argument("--category", metavar="CAT", help="Filter by log category (e.g. LogAI, LogTemp, LogEngine)")
+    p_gl.add_argument("--since", metavar="CURSOR", help="Return only entries after this timestamp/cursor")
+    p_gl.add_argument("--tail-follow", action="store_true", help="Poll for new entries until interrupted (like tail -f)")
+    p_gl.add_argument(
+        "--poll-interval",
+        type=float,
+        default=0.5,
+        metavar="SEC",
+        help="Polling interval for --tail-follow (default: 0.5)",
+    )
     p_gl.add_argument("--raw", action="store_true", help="Output plain text lines instead of JSON")
     p_gl.set_defaults(func=cmd_get_logs)
 
@@ -2349,6 +2492,22 @@ def build_parser() -> argparse.ArgumentParser:
     p_ch.add_argument("--no-blueprints", action="store_true", help="Exclude Blueprint subclasses from children")
     p_ch.set_defaults(func=cmd_class_hierarchy)
 
+    p_vcp = sub.add_parser(
+        "validate-class-path",
+        help="Validate that a soft class path resolves to a loadable UClass.",
+        description=(
+            "Checks whether a class path exists, whether it loads, whether it resolves to a UClass,\n"
+            "and returns the resolved class plus its parent chain.\n\n"
+            "EXAMPLES:\n"
+            "  soft-ue-cli validate-class-path /Game/Characters/BP_Hero.BP_Hero_C\n"
+            "  soft-ue-cli validate-class-path /Game/Characters/BP_Hero --parent-depth 5"
+        ),
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    p_vcp.add_argument("class_path", help="Soft class path or asset path to validate")
+    p_vcp.add_argument("--parent-depth", type=int, metavar="N", help="Limit returned parent classes (default: 10)")
+    p_vcp.set_defaults(func=cmd_validate_class_path)
+
     # -------------------------------------------------------------------------
     # Editor tools — Asset
     # -------------------------------------------------------------------------
@@ -2429,6 +2588,20 @@ def build_parser() -> argparse.ArgumentParser:
     p_da.add_argument("asset_path", help="Asset path to delete (e.g. /Game/MyBlueprint)")
     p_da.set_defaults(func=cmd_delete_asset)
 
+    p_ral = sub.add_parser(
+        "release-asset-lock",
+        help="Best-effort release of UE editor file handles for an asset.",
+        description=(
+            "Closes open asset editors for the target asset and forces garbage collection.\n"
+            "This is intended to reduce file-in-use conflicts during version-control operations.\n\n"
+            "EXAMPLES:\n"
+            "  soft-ue-cli release-asset-lock /Game/Blueprints/BP_Player"
+        ),
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    p_ral.add_argument("asset_path", help="Asset path to unlock")
+    p_ral.set_defaults(func=cmd_release_asset_lock)
+
     p_gad = sub.add_parser(
         "get-asset-diff",
         help="Show SCM diff for an asset (git or Perforce).",
@@ -2583,6 +2756,11 @@ def build_parser() -> argparse.ArgumentParser:
         formatter_class=argparse.RawDescriptionHelpFormatter,
     )
     p_tlc.add_argument("--no-wait", action="store_true", help="Return immediately without waiting for compilation result")
+    p_tlc.add_argument(
+        "--allow-header-changes",
+        action="store_true",
+        help="Bypass reflected-header safety check and trigger Live Coding anyway",
+    )
     p_tlc.set_defaults(func=cmd_trigger_live_coding)
 
     # -------------------------------------------------------------------------
@@ -2683,6 +2861,27 @@ def build_parser() -> argparse.ArgumentParser:
     p_qmpc.add_argument("--world", choices=["editor", "pie", "game"], help="World context")
     p_qmpc.set_defaults(func=cmd_query_mpc)
 
+    p_ecc = sub.add_parser(
+        "exec-console-command",
+        help="Execute an arbitrary UE console command in a target world.",
+        description=(
+            "Runs an arbitrary console command directly in the editor, PIE, or game world.\n"
+            "PIE is the default target because this is most commonly used for iterative gameplay testing.\n\n"
+            "EXAMPLES:\n"
+            "  soft-ue-cli exec-console-command stat fps\n"
+            "  soft-ue-cli exec-console-command --world editor r.Streaming.PoolSize 4000\n"
+            "  soft-ue-cli exec-console-command --player-index 0 MyGame.MyCommand arg1 arg2"
+        ),
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    p_ecc.add_argument("--world", choices=["pie", "editor", "game"], default="pie", help="World context (default: pie)")
+    p_ecc.add_argument("--player-index", type=int, metavar="N", help="Player controller index when executing in PIE/game")
+    p_ecc.add_argument("--auto-start-pie", action="store_true", help="Start PIE automatically when --world pie is requested")
+    p_ecc.add_argument("--map", metavar="PATH", help="Map to load if --auto-start-pie starts a PIE session")
+    p_ecc.add_argument("--pie-timeout", type=float, default=30.0, metavar="SEC", help="Timeout for PIE auto-start (default: 30)")
+    p_ecc.add_argument("command_parts", nargs="+", help="Console command to execute")
+    p_ecc.set_defaults(func=cmd_exec_console_command)
+
     # -------------------------------------------------------------------------
     # Editor tools — PIE
     # -------------------------------------------------------------------------
@@ -2759,6 +2958,28 @@ def build_parser() -> argparse.ArgumentParser:
     p_iai.add_argument("--blend-weights", metavar="LIST", help="Comma-separated UAnimInstance float property names to read")
     p_iai.set_defaults(func=cmd_inspect_anim_instance)
 
+    p_ipp = sub.add_parser(
+        "inspect-pawn-possession",
+        help="Inspect controller/pawn possession state in a running world.",
+        description=(
+            "Returns structured JSON describing controllers, possessed pawns, pawn/controller links,\n"
+            "AI auto-possession settings, and hidden state.\n\n"
+            "EXAMPLES:\n"
+            "  soft-ue-cli inspect-pawn-possession\n"
+            "  soft-ue-cli inspect-pawn-possession --class-filter Character\n"
+            "  soft-ue-cli inspect-pawn-possession --world editor --actor-name BP_Hero_C_0"
+        ),
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    p_ipp.add_argument("--world", choices=["pie", "editor", "game"], default="pie", help="World context (default: pie)")
+    p_ipp.add_argument("--class-filter", metavar="CLASS", help="Only include pawns matching this class name")
+    p_ipp.add_argument("--actor-name", metavar="NAME", help="Only include a specific pawn/controller name")
+    p_ipp.add_argument("--include-hidden", action="store_true", help="Include hidden actors even when filtering")
+    p_ipp.add_argument("--auto-start-pie", action="store_true", help="Start PIE automatically when --world pie is requested")
+    p_ipp.add_argument("--map", metavar="PATH", help="Map to load if --auto-start-pie starts a PIE session")
+    p_ipp.add_argument("--pie-timeout", type=float, default=30.0, metavar="SEC", help="Timeout for PIE auto-start (default: 30)")
+    p_ipp.set_defaults(func=cmd_inspect_pawn_possession)
+
     p_ti = sub.add_parser(
         "trigger-input",
         help="Send input events to a running game (PIE or packaged build).",
@@ -2918,11 +3139,34 @@ def build_parser() -> argparse.ArgumentParser:
         choices=["editor", "pie", "game"],
         help="World helper to expose during execution (default: editor)",
     )
+    p_rps.add_argument("--auto-start-pie", action="store_true", help="Start PIE automatically when --world pie is requested")
+    p_rps.add_argument("--map", metavar="PATH", help="Map to load if --auto-start-pie starts a PIE session")
+    p_rps.add_argument("--pie-timeout", type=float, default=30.0, metavar="SEC", help="Timeout for PIE auto-start (default: 30)")
     p_rps.add_argument(
         "--arguments", metavar="JSON", help="Arguments as JSON object (accessible via unreal.get_mcp_args())"
     )
     p_rps.set_defaults(func=cmd_run_python_script)
 
+    p_rgt = sub.add_parser(
+        "request-gameplay-tag",
+        help="Resolve a registered GameplayTag by name.",
+        description=(
+            "Looks up a registered GameplayTag by name and returns validity plus the tag's export text.\n\n"
+            "EXAMPLES:\n"
+            "  soft-ue-cli request-gameplay-tag Status.Effect.Burning"
+        ),
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    p_rgt.add_argument("tag_name", help="GameplayTag name, e.g. Status.Effect.Burning")
+    p_rgt.set_defaults(func=cmd_request_gameplay_tag)
+
+    p_rlgt = sub.add_parser(
+        "reload-gameplay-tags",
+        help="Reload GameplayTags settings and rebuild the in-memory tag data where supported.",
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    p_rlgt.set_defaults(func=cmd_reload_gameplay_tags)
+
     p_ss = sub.add_parser(
         "save-script",
         help="Save a Python script locally for later reuse.",
diff --git a/cli/soft_ue_cli/skills/test-tools.md b/cli/soft_ue_cli/skills/test-tools.md
index 3c473c0..437feb4 100644
--- a/cli/soft_ue_cli/skills/test-tools.md
+++ b/cli/soft_ue_cli/skills/test-tools.md
@@ -375,6 +375,14 @@ def _run_single_mode(mode_name: str, caller) -> list[dict]:
 
     run_test("project-info", "get-project-info", {}, has("project_name"))
     run_test("get-logs", "get-logs", {"limit": 10}, has("lines"))
+    try:
+        _logs_cursor = caller("get-logs", {"lines": 0}, None).get("next_cursor")
+    except Exception:
+        _logs_cursor = None
+    if _logs_cursor:
+        run_test("get-logs since cursor", "get-logs", {"since": _logs_cursor}, has("entries"))
+    else:
+        _record("get-logs since cursor", "get-logs", {}, True, 0, "skipped: no cursor available")
 
     # ══════════════════════════════════════════════════════════════════════════
     # Suite 2: Console Variables
@@ -506,6 +514,7 @@ def _run_single_mode(mode_name: str, caller) -> list[dict]:
              {"asset_path": test_level_path}, lambda r: "world_settings" in r and "default_game_mode" in r)
     run_test("get-asset-preview", "get-asset-preview", {"asset_path": bp_path}, has("file_path"))
     run_test("open-asset", "open-asset", {"asset_path": bp_path}, has("success"))
+    run_test("release-asset-lock", "release-asset-lock", {"asset_path": bp_path}, has("success"))
 
     # ══════════════════════════════════════════════════════════════════════════
     # Suite 7: Blueprint Inspect
@@ -652,6 +661,8 @@ def _run_single_mode(mode_name: str, caller) -> list[dict]:
              {"class_name": "Actor", "direction": "children", "depth": 2}, has("children"))
     run_test("class-hierarchy StaticMeshActor", "get-class-hierarchy",
              {"class_name": "StaticMeshActor"}, has("class"))
+    run_test("validate-class-path Actor", "validate-class-path",
+             {"class_path": "/Script/Engine.Actor"}, has("class_exists"))
 
     # ══════════════════════════════════════════════════════════════════════════
     # Suite 10: Find References
@@ -711,6 +722,10 @@ def _run_single_mode(mode_name: str, caller) -> list[dict]:
              {"action": "start", "timeout": PIE_TIMEOUT}, has("success"), timeout=PIE_TIMEOUT)
     time.sleep(4)
     run_test("pie-session status", "pie-session", {"action": "status"}, has("state"), timeout=PIE_TIMEOUT)
+    run_test("exec-console-command stat fps", "exec-console-command",
+             {"command": "stat fps", "world": "pie"}, has("success"), timeout=PIE_TIMEOUT)
+    run_test("inspect-pawn-possession", "inspect-pawn-possession",
+             {"world": "pie"}, has("pawns"), timeout=PIE_TIMEOUT)
     run_test("get-logs during PIE", "get-logs", {"limit": 5}, has("lines"), timeout=PIE_TIMEOUT)
     run_test("pie-session stop", "pie-session", {"action": "stop", "timeout": PIE_TIMEOUT}, has("success"), timeout=PIE_TIMEOUT)
 
@@ -773,6 +788,7 @@ def _run_single_mode(mode_name: str, caller) -> list[dict]:
     run_test("run-python-script inline", "run-python-script", {
         "script": "import unreal; print(unreal.SystemLibrary.get_engine_version())"
     }, has("output"))
+    run_test("reload-gameplay-tags", "reload-gameplay-tags", {}, has("success"))
 
     run_cli("save-script", "save-script", script_name,
             "--script", "print('soft-ue-cli test script')")
@@ -793,6 +809,23 @@ def _run_single_mode(mode_name: str, caller) -> list[dict]:
         )
     run_cli("run-python-script helper import", "run-python-script", "--script-path", helper_script,
             check_stdout=lambda s: "HELPER_ACTORS" in s and "HELPER_FILE" in s)
+    try:
+        _project_tags = caller("get-project-info", {"section": "tags"}, None).get("settings", {}).get("tags", {})
+        _tag_sources = _project_tags.get("sources", [])
+        _first_tag = None
+        for _source in _tag_sources:
+            _tags = _source.get("tags") or []
+            if _tags:
+                _first_tag = _tags[0]
+                break
+    except Exception:
+        _first_tag = None
+    if _first_tag:
+        run_test("request-gameplay-tag", "request-gameplay-tag",
+                 {"tag_name": _first_tag}, has("valid"))
+    else:
+        _record("request-gameplay-tag", "request-gameplay-tag", {},
+                True, 0, "skipped: no gameplay tags found in project-info")
 
     begin_suite("advanced-automation")
 
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.cpp
index 4c65b82..e45aa9b 100644
--- a/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.cpp
+++ b/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.cpp
@@ -4,6 +4,9 @@
 #include "Tools/BridgeToolRegistry.h"
 #include "SoftUEBridgeModule.h"
 #include "Misc/OutputDeviceRedirector.h"
+#include "Misc/DateTime.h"
+#include "Misc/LexicalConversion.h"
+#include "Algo/AllOf.h"
 #include "Algo/Reverse.h"
 
 // ── FBridgeLogCapture ─────────────────────────────────────────────────────────
@@ -46,43 +49,71 @@ void FBridgeLogCapture::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity,
 	default:                     VerbStr = TEXT("Log");     break;
 	}
 
-	FString Line = FString::Printf(TEXT("[%s][%s] %s"), *Category.ToString(), *VerbStr, V);
+	FBridgeCapturedLogEntry Entry;
+	Entry.Timestamp = FDateTime::UtcNow().ToIso8601();
+	Entry.Category = Category.ToString();
+	Entry.Verbosity = VerbStr;
+	Entry.Message = V;
+	Entry.Line = FString::Printf(TEXT("[%s][%s] %s"), *Entry.Category, *Entry.Verbosity, V);
 
 	FScopeLock ScopeLock(&Lock);
-	Lines.Add(MoveTemp(Line));
-	if (Lines.Num() > MaxLines)
+	Entry.Sequence = NextSequence++;
+	Entries.Add(MoveTemp(Entry));
+	if (Entries.Num() > MaxLines)
 	{
-		Lines.RemoveAt(0, Lines.Num() - MaxLines);
+		Entries.RemoveAt(0, Entries.Num() - MaxLines);
 	}
 }
 
-TArray<FString> FBridgeLogCapture::GetLines(int32 N, const FString& Filter, const FString& Category) const
+TArray<FBridgeCapturedLogEntry> FBridgeLogCapture::GetEntries(
+	int32 N,
+	const FString& Filter,
+	const FString& Category,
+	const FString& Since) const
 {
 	FScopeLock ScopeLock(&Lock);
 
-	TArray<FString> Result;
-	if (N <= 0) return Result;
+	TArray<FBridgeCapturedLogEntry> Result;
+	const int32 RequestedCount = FMath::Max(N, 0);
+	const bool bHasSince = !Since.IsEmpty();
+	const bool bSinceIsCursor = bHasSince && Algo::AllOf(Since, [](TCHAR Ch) { return FChar::IsDigit(Ch); });
+	const uint64 SinceCursor = bSinceIsCursor ? FCString::Strtoui64(*Since, nullptr, 10) : 0;
 
 	const bool bHasFilter = !Filter.IsEmpty() || !Category.IsEmpty();
-	const FString CategoryBracket = Category.IsEmpty() ? FString() : (TEXT("[") + Category + TEXT("]"));
-
-	// When filtering, scan all lines (newest first) and stop after N matches.
-	// When not filtering, return the last N lines directly.
-	const int32 Start = bHasFilter ? 0 : FMath::Max(0, Lines.Num() - N);
-	Result.Reserve(FMath::Min(N, Lines.Num()));
-	for (int32 i = Lines.Num() - 1; i >= Start; --i)
+	const int32 Start = (bHasFilter || bHasSince || RequestedCount <= 0) ? 0 : FMath::Max(0, Entries.Num() - RequestedCount);
+	Result.Reserve(FMath::Min(RequestedCount > 0 ? RequestedCount : Entries.Num(), Entries.Num()));
+	for (int32 i = Entries.Num() - 1; i >= Start; --i)
 	{
-		const FString& Line = Lines[i];
-		if (!Filter.IsEmpty() && !Line.Contains(Filter, ESearchCase::IgnoreCase)) continue;
-		if (!CategoryBracket.IsEmpty() && !Line.Contains(CategoryBracket, ESearchCase::IgnoreCase)) continue;
-		Result.Add(Line);
-		if (Result.Num() >= N) break;
+		const FBridgeCapturedLogEntry& Entry = Entries[i];
+		if (!Filter.IsEmpty() && !Entry.Message.Contains(Filter, ESearchCase::IgnoreCase)) continue;
+		if (!Category.IsEmpty() && !Entry.Category.Equals(Category, ESearchCase::IgnoreCase)) continue;
+		if (bHasSince)
+		{
+			const bool bIsNewer = bSinceIsCursor ? Entry.Sequence > SinceCursor : Entry.Timestamp > Since;
+			if (!bIsNewer)
+			{
+				continue;
+			}
+		}
+		Result.Add(Entry);
+		if (RequestedCount > 0 && Result.Num() >= RequestedCount) break;
 	}
-	// Reverse so results are in chronological order (oldest first)
 	Algo::Reverse(Result);
 	return Result;
 }
 
+uint64 FBridgeLogCapture::GetLatestCursor() const
+{
+	FScopeLock ScopeLock(&Lock);
+	return Entries.Num() > 0 ? Entries.Last().Sequence : 0;
+}
+
+FString FBridgeLogCapture::GetLatestTimestamp() const
+{
+	FScopeLock ScopeLock(&Lock);
+	return Entries.Num() > 0 ? Entries.Last().Timestamp : FString();
+}
+
 // ── UGetLogsTool ──────────────────────────────────────────────────────────────
 
 #if !WITH_EDITOR
@@ -105,27 +136,42 @@ TMap<FString, FBridgeSchemaProperty> UGetLogsTool::GetInputSchema() const
 	S.Add(TEXT("lines"),    Prop(TEXT("integer"), TEXT("Number of recent lines to return (default: 100)")));
 	S.Add(TEXT("filter"),   Prop(TEXT("string"),  TEXT("Filter lines containing this text (case-insensitive)")));
 	S.Add(TEXT("category"), Prop(TEXT("string"),  TEXT("Filter by log category (e.g. 'LogBlueprintUserMessages')")));
+	S.Add(TEXT("since"),    Prop(TEXT("string"),  TEXT("Only return entries after this cursor/timestamp")));
 
 	return S;
 }
 
 FBridgeToolResult UGetLogsTool::Execute(const TSharedPtr<FJsonObject>& Args, const FBridgeToolContext& Ctx)
 {
-	const int32 N       = GetIntArgOrDefault(Args, TEXT("lines"), 100);
-	const FString Filter    = GetStringArgOrDefault(Args, TEXT("filter"));
-	const FString Category  = GetStringArgOrDefault(Args, TEXT("category"));
+	const int32 N = GetIntArgOrDefault(Args, TEXT("lines"), 100);
+	const FString Filter = GetStringArgOrDefault(Args, TEXT("filter"));
+	const FString Category = GetStringArgOrDefault(Args, TEXT("category"));
+	const FString Since = GetStringArgOrDefault(Args, TEXT("since"));
 
-	TArray<FString> LogLines = FBridgeLogCapture::Get().GetLines(N, Filter, Category);
+	TArray<FBridgeCapturedLogEntry> LogEntries = FBridgeLogCapture::Get().GetEntries(N, Filter, Category, Since);
 
 	TArray<TSharedPtr<FJsonValue>> LinesArr;
-	for (const FString& Line : LogLines)
+	TArray<TSharedPtr<FJsonValue>> EntriesArr;
+	for (const FBridgeCapturedLogEntry& Entry : LogEntries)
 	{
-		LinesArr.Add(MakeShareable(new FJsonValueString(Line)));
+		LinesArr.Add(MakeShareable(new FJsonValueString(Entry.Line)));
+
+		TSharedPtr<FJsonObject> EntryJson = MakeShareable(new FJsonObject);
+		EntryJson->SetStringField(TEXT("timestamp"), Entry.Timestamp);
+		EntryJson->SetNumberField(TEXT("cursor"), static_cast<double>(Entry.Sequence));
+		EntryJson->SetStringField(TEXT("category"), Entry.Category);
+		EntryJson->SetStringField(TEXT("verbosity"), Entry.Verbosity);
+		EntryJson->SetStringField(TEXT("message"), Entry.Message);
+		EntryJson->SetStringField(TEXT("line"), Entry.Line);
+		EntriesArr.Add(MakeShareable(new FJsonValueObject(EntryJson)));
 	}
 
 	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
 	Result->SetArrayField(TEXT("lines"), LinesArr);
+	Result->SetArrayField(TEXT("entries"), EntriesArr);
 	Result->SetNumberField(TEXT("count"), LinesArr.Num());
+	Result->SetStringField(TEXT("last_timestamp"), FBridgeLogCapture::Get().GetLatestTimestamp());
+	Result->SetStringField(TEXT("next_cursor"), LexToString(FBridgeLogCapture::Get().GetLatestCursor()));
 
 	return FBridgeToolResult::Json(Result);
 }
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.h b/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.h
index a88fd06..da80132 100644
--- a/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.h
+++ b/plugin/SoftUEBridge/Source/SoftUEBridge/Private/Tools/GetLogsTool.h
@@ -5,6 +5,16 @@
 #include "Misc/OutputDevice.h"
 #include "GetLogsTool.generated.h"
 
+struct FBridgeCapturedLogEntry
+{
+	uint64 Sequence = 0;
+	FString Timestamp;
+	FString Category;
+	FString Verbosity;
+	FString Message;
+	FString Line;
+};
+
 /** Thread-safe ring buffer capturing UE log output */
 class FBridgeLogCapture : public FOutputDevice
 {
@@ -17,16 +27,23 @@ public:
 	void Stop();
 
 	/** Retrieve up to N recent lines, optionally filtered by category or text */
-	TArray<FString> GetLines(int32 N, const FString& Filter = TEXT(""), const FString& Category = TEXT("")) const;
+	TArray<FBridgeCapturedLogEntry> GetEntries(
+		int32 N,
+		const FString& Filter = TEXT(""),
+		const FString& Category = TEXT(""),
+		const FString& Since = TEXT("")) const;
+	uint64 GetLatestCursor() const;
+	FString GetLatestTimestamp() const;
 
 protected:
 	virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override;
 	virtual bool CanBeUsedOnMultipleThreads() const override { return true; }
 
 private:
-	TArray<FString> Lines;
+	TArray<FBridgeCapturedLogEntry> Entries;
 	mutable FCriticalSection Lock;
 	bool bStarted = false;
+	uint64 NextSequence = 1;
 };
 
 UCLASS()
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/SoftUEBridgeEditorModule.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/SoftUEBridgeEditorModule.cpp
index a78c528..6518905 100644
--- a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/SoftUEBridgeEditorModule.cpp
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/SoftUEBridgeEditorModule.cpp
@@ -6,6 +6,7 @@
 
 // Analysis
 #include "Tools/Analysis/ClassHierarchyTool.h"
+#include "Tools/Analysis/ValidateClassPathTool.h"
 
 // Asset
 #include "Tools/Asset/QueryAssetTool.h"
@@ -15,6 +16,7 @@
 #include "Tools/Asset/GetAssetDiffTool.h"
 #include "Tools/Asset/GetAssetPreviewTool.h"
 #include "Tools/Asset/OpenAssetTool.h"
+#include "Tools/Asset/ReleaseAssetLockTool.h"
 
 // Blueprint
 #include "Tools/Blueprint/QueryBlueprintTool.h"
@@ -33,8 +35,10 @@
 #include "Tools/Material/QueryMPCTool.h"
 
 // PIE
+#include "Tools/PIE/ExecConsoleCommandTool.h"
 #include "Tools/PIE/PieSessionTool.h"
 #include "Tools/PIE/PieTickTool.h"
+#include "Tools/PIE/InspectPawnPossessionTool.h"
 
 // Performance
 #include "Tools/Performance/InsightsCaptureTool.h"
@@ -52,6 +56,8 @@
 
 // Project
 #include "Tools/Project/ProjectInfoTool.h"
+#include "Tools/Project/ReloadGameplayTagsTool.h"
+#include "Tools/Project/RequestGameplayTagTool.h"
 
 // References
 #include "Tools/References/FindReferencesTool.h"
@@ -102,6 +108,7 @@ void FSoftUEBridgeEditorModule::StartupModule()
 
 	// Analysis
 	Registry.RegisterToolClass<UClassHierarchyTool>();
+	Registry.RegisterToolClass<UValidateClassPathTool>();
 
 	// Asset
 	Registry.RegisterToolClass<UQueryAssetTool>();
@@ -111,6 +118,7 @@ void FSoftUEBridgeEditorModule::StartupModule()
 	Registry.RegisterToolClass<UGetAssetDiffTool>();
 	Registry.RegisterToolClass<UGetAssetPreviewTool>();
 	Registry.RegisterToolClass<UOpenAssetTool>();
+	Registry.RegisterToolClass<UReleaseAssetLockTool>();
 
 	// Blueprint
 	Registry.RegisterToolClass<UQueryBlueprintTool>();
@@ -129,8 +137,10 @@ void FSoftUEBridgeEditorModule::StartupModule()
 	Registry.RegisterToolClass<UQueryMPCTool>();
 
 	// PIE
+	Registry.RegisterToolClass<UExecConsoleCommandTool>();
 	Registry.RegisterToolClass<UPieSessionTool>();
 	Registry.RegisterToolClass<UPieTickTool>();
+	Registry.RegisterToolClass<UInspectPawnPossessionTool>();
 
 	// Performance
 	Registry.RegisterToolClass<UInsightsCaptureTool>();
@@ -148,6 +158,8 @@ void FSoftUEBridgeEditorModule::StartupModule()
 
 	// Project
 	Registry.RegisterToolClass<UProjectInfoTool>();
+	Registry.RegisterToolClass<UReloadGameplayTagsTool>();
+	Registry.RegisterToolClass<URequestGameplayTagTool>();
 
 	// References
 	Registry.RegisterToolClass<UFindReferencesTool>();
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Analysis/ValidateClassPathTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Analysis/ValidateClassPathTool.cpp
new file mode 100644
index 0000000..d892003
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Analysis/ValidateClassPathTool.cpp
@@ -0,0 +1,99 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#include "Tools/Analysis/ValidateClassPathTool.h"
+#include "SoftUEBridgeEditorModule.h"
+#include "Utils/BridgePropertySerializer.h"
+#include "Misc/PackageName.h"
+
+namespace
+{
+	FString BuildAssetObjectPath(const FString& InPath)
+	{
+		if (!InPath.StartsWith(TEXT("/")))
+		{
+			return InPath;
+		}
+
+		FString Path = InPath;
+		if (Path.EndsWith(TEXT("_C")))
+		{
+			Path.LeftChopInline(2);
+		}
+
+		if (!Path.Contains(TEXT(".")))
+		{
+			const FString AssetName = FPackageName::GetLongPackageAssetName(Path);
+			Path += TEXT(".") + AssetName;
+		}
+
+		return Path;
+	}
+}
+
+FString UValidateClassPathTool::GetToolDescription() const
+{
+	return TEXT("Validate that a soft class path resolves to a loadable UClass and return its parent hierarchy.");
+}
+
+TMap<FString, FBridgeSchemaProperty> UValidateClassPathTool::GetInputSchema() const
+{
+	TMap<FString, FBridgeSchemaProperty> Schema;
+
+	auto Prop = [](const FString& Type, const FString& Desc) {
+		FBridgeSchemaProperty P; P.Type = Type; P.Description = Desc; return P;
+	};
+
+	Schema.Add(TEXT("class_path"), Prop(TEXT("string"), TEXT("Soft class path or asset path to validate")));
+	Schema.Add(TEXT("parent_depth"), Prop(TEXT("integer"), TEXT("Optional maximum parent depth")));
+	return Schema;
+}
+
+FBridgeToolResult UValidateClassPathTool::Execute(
+	const TSharedPtr<FJsonObject>& Arguments,
+	const FBridgeToolContext& Context)
+{
+	const FString ClassPath = GetStringArgOrDefault(Arguments, TEXT("class_path"));
+	const int32 ParentDepth = GetIntArgOrDefault(Arguments, TEXT("parent_depth"), 10);
+	if (ClassPath.IsEmpty())
+	{
+		return FBridgeToolResult::Error(TEXT("validate-class-path: class_path is required"));
+	}
+
+	const FString AssetObjectPath = BuildAssetObjectPath(ClassPath);
+	UObject* AssetObject = nullptr;
+	if (AssetObjectPath.StartsWith(TEXT("/")))
+	{
+		AssetObject = StaticLoadObject(UObject::StaticClass(), nullptr, *AssetObjectPath);
+	}
+
+	FString ResolveError;
+	UClass* ResolvedClass = FBridgePropertySerializer::ResolveClass(ClassPath, ResolveError);
+
+	TArray<TSharedPtr<FJsonValue>> ParentClasses;
+	for (UClass* Current = ResolvedClass ? ResolvedClass->GetSuperClass() : nullptr;
+		Current && ParentClasses.Num() < ParentDepth;
+		Current = Current->GetSuperClass())
+	{
+		ParentClasses.Add(MakeShareable(new FJsonValueString(Current->GetPathName())));
+	}
+
+	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
+	Result->SetBoolField(TEXT("success"), ResolvedClass != nullptr);
+	Result->SetStringField(TEXT("input_path"), ClassPath);
+	Result->SetStringField(TEXT("asset_path"), AssetObjectPath);
+	Result->SetBoolField(TEXT("asset_exists"), AssetObject != nullptr);
+	Result->SetBoolField(TEXT("class_exists"), ResolvedClass != nullptr);
+	Result->SetBoolField(TEXT("load_success"), ResolvedClass != nullptr);
+	Result->SetArrayField(TEXT("parent_classes"), ParentClasses);
+	if (ResolvedClass)
+	{
+		Result->SetStringField(TEXT("resolved_class_path"), ResolvedClass->GetPathName());
+		Result->SetStringField(TEXT("class_name"), ResolvedClass->GetName());
+	}
+	else
+	{
+		Result->SetStringField(TEXT("error"), ResolveError);
+	}
+
+	return FBridgeToolResult::Json(Result);
+}
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Asset/ReleaseAssetLockTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Asset/ReleaseAssetLockTool.cpp
new file mode 100644
index 0000000..b589823
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Asset/ReleaseAssetLockTool.cpp
@@ -0,0 +1,61 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#include "Tools/Asset/ReleaseAssetLockTool.h"
+#include "SoftUEBridgeEditorModule.h"
+#include "Utils/BridgeAssetModifier.h"
+#include "Subsystems/AssetEditorSubsystem.h"
+#include "Editor.h"
+#include "Misc/PackageName.h"
+#include "UObject/UObjectGlobals.h"
+
+FString UReleaseAssetLockTool::GetToolDescription() const
+{
+	return TEXT("Best-effort release of editor-side asset handles by closing editors and forcing GC.");
+}
+
+TMap<FString, FBridgeSchemaProperty> UReleaseAssetLockTool::GetInputSchema() const
+{
+	TMap<FString, FBridgeSchemaProperty> Schema;
+	FBridgeSchemaProperty AssetPath;
+	AssetPath.Type = TEXT("string");
+	AssetPath.Description = TEXT("Asset path to unlock");
+	AssetPath.bRequired = true;
+	Schema.Add(TEXT("asset_path"), AssetPath);
+	return Schema;
+}
+
+FBridgeToolResult UReleaseAssetLockTool::Execute(
+	const TSharedPtr<FJsonObject>& Arguments,
+	const FBridgeToolContext& Context)
+{
+	const FString AssetPath = GetStringArgOrDefault(Arguments, TEXT("asset_path"));
+	if (AssetPath.IsEmpty())
+	{
+		return FBridgeToolResult::Error(TEXT("release-asset-lock: asset_path is required"));
+	}
+
+	FString LoadError;
+	UObject* Asset = FBridgeAssetModifier::LoadAssetByPath(AssetPath, LoadError);
+	if (!Asset)
+	{
+		return FBridgeToolResult::Error(FString::Printf(
+			TEXT("release-asset-lock: failed to load asset '%s': %s"), *AssetPath, *LoadError));
+	}
+
+	UAssetEditorSubsystem* AssetEditorSubsystem = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
+	if (AssetEditorSubsystem)
+	{
+		AssetEditorSubsystem->CloseAllEditorsForAsset(Asset);
+	}
+
+	FlushAsyncLoading();
+	CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
+	CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
+
+	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
+	Result->SetBoolField(TEXT("success"), true);
+	Result->SetStringField(TEXT("asset_path"), AssetPath);
+	Result->SetStringField(TEXT("package_file"), FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
+	Result->SetStringField(TEXT("message"), TEXT("Closed asset editors and forced garbage collection."));
+	return FBridgeToolResult::Json(Result);
+}
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Build/TriggerLiveCodingTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Build/TriggerLiveCodingTool.cpp
index 91f41f6..743219e 100644
--- a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Build/TriggerLiveCodingTool.cpp
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Build/TriggerLiveCodingTool.cpp
@@ -4,6 +4,9 @@
 #include "SoftUEBridgeEditorModule.h"
 #include "Engine/Engine.h"
 #include "Modules/ModuleManager.h"
+#include "HAL/PlatformProcess.h"
+#include "Misc/FileHelper.h"
+#include "Misc/Paths.h"
 
 // Live Coding module (Windows only)
 #if PLATFORM_WINDOWS
@@ -25,6 +28,12 @@ TMap<FString, FBridgeSchemaProperty> UTriggerLiveCodingTool::GetInputSchema() co
 	WaitForCompletion.bRequired = false;
 	Schema.Add(TEXT("wait_for_completion"), WaitForCompletion);
 
+	FBridgeSchemaProperty AllowHeaderChanges;
+	AllowHeaderChanges.Type = TEXT("boolean");
+	AllowHeaderChanges.Description = TEXT("If true, bypass reflected-header preflight warnings and trigger Live Coding anyway");
+	AllowHeaderChanges.bRequired = false;
+	Schema.Add(TEXT("allow_header_changes"), AllowHeaderChanges);
+
 	return Schema;
 }
 
@@ -40,6 +49,17 @@ FBridgeToolResult UTriggerLiveCodingTool::Execute(
 #if !PLATFORM_WINDOWS
 	return FBridgeToolResult::Error(TEXT("Live Coding is only supported on Windows"));
 #else
+	const bool bAllowHeaderChanges = GetBoolArgOrDefault(Arguments, TEXT("allow_header_changes"), false);
+	if (!bAllowHeaderChanges)
+	{
+		TArray<FString> RiskyHeaders;
+		if (DetectReflectedHeaderChanges(RiskyHeaders))
+		{
+			return FBridgeToolResult::Error(FString::Printf(
+				TEXT("trigger-live-coding: reflected header changes detected (%s). Live Coding will likely cancel; run build-and-relaunch instead or pass allow_header_changes=true."),
+				*FString::Join(RiskyHeaders, TEXT(", "))));
+		}
+	}
 
 	// Try to use ILiveCodingModule for better control
 	ILiveCodingModule* LiveCodingModule = FModuleManager::GetModulePtr<ILiveCodingModule>("LiveCoding");
@@ -81,6 +101,67 @@ FBridgeToolResult UTriggerLiveCodingTool::Execute(
 }
 
 #if PLATFORM_WINDOWS
+bool UTriggerLiveCodingTool::DetectReflectedHeaderChanges(TArray<FString>& OutFiles) const
+{
+	OutFiles.Reset();
+
+	const FString ProjectDir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir());
+	int32 ReturnCode = -1;
+	FString StdOut;
+	FString StdErr;
+	const FString GitArgs = FString::Printf(TEXT("-C \"%s\" status --porcelain --untracked-files=all -- \"*.h\""), *ProjectDir);
+	FPlatformProcess::ExecProcess(TEXT("git"), *GitArgs, &ReturnCode, &StdOut, &StdErr);
+	if (ReturnCode != 0 || StdOut.IsEmpty())
+	{
+		return false;
+	}
+
+	TArray<FString> Lines;
+	StdOut.ParseIntoArrayLines(Lines);
+	for (const FString& Line : Lines)
+	{
+		if (Line.Len() < 4)
+		{
+			continue;
+		}
+
+		FString RelativePath = Line.Mid(3).TrimStartAndEnd();
+		int32 RenameArrowIndex = INDEX_NONE;
+		if (RelativePath.FindLastChar(TEXT('>'), RenameArrowIndex))
+		{
+			const int32 ArrowStart = RelativePath.Find(TEXT("->"));
+			if (ArrowStart != INDEX_NONE)
+			{
+				RelativePath = RelativePath.Mid(ArrowStart + 2).TrimStartAndEnd();
+			}
+		}
+
+		FString AbsolutePath = RelativePath;
+		if (FPaths::IsRelative(AbsolutePath))
+		{
+			AbsolutePath = FPaths::Combine(ProjectDir, RelativePath);
+		}
+
+		FString HeaderText;
+		if (!FFileHelper::LoadFileToString(HeaderText, *AbsolutePath))
+		{
+			continue;
+		}
+
+		if (HeaderText.Contains(TEXT("UCLASS("))
+			|| HeaderText.Contains(TEXT("USTRUCT("))
+			|| HeaderText.Contains(TEXT("UINTERFACE("))
+			|| HeaderText.Contains(TEXT("UENUM("))
+			|| HeaderText.Contains(TEXT("GENERATED_BODY("))
+			|| HeaderText.Contains(TEXT("GENERATED_UCLASS_BODY(")))
+		{
+			OutFiles.Add(RelativePath);
+		}
+	}
+
+	return OutFiles.Num() > 0;
+}
+
 FBridgeToolResult UTriggerLiveCodingTool::ExecuteSynchronous(ILiveCodingModule* LiveCodingModule)
 {
 	UE_LOG(LogSoftUEBridgeEditor, Log, TEXT("trigger-live-coding: Starting synchronous compilation..."));
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/PIE/ExecConsoleCommandTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/PIE/ExecConsoleCommandTool.cpp
new file mode 100644
index 0000000..6e0aea6
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/PIE/ExecConsoleCommandTool.cpp
@@ -0,0 +1,82 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#include "Tools/PIE/ExecConsoleCommandTool.h"
+#include "SoftUEBridgeEditorModule.h"
+#include "Engine/Engine.h"
+#include "GameFramework/PlayerController.h"
+#include "Kismet/GameplayStatics.h"
+
+FString UExecConsoleCommandTool::GetToolDescription() const
+{
+	return TEXT("Execute an arbitrary UE console command in the editor, PIE, or game world.");
+}
+
+TMap<FString, FBridgeSchemaProperty> UExecConsoleCommandTool::GetInputSchema() const
+{
+	TMap<FString, FBridgeSchemaProperty> Schema;
+
+	auto Prop = [](const FString& Type, const FString& Desc) {
+		FBridgeSchemaProperty P; P.Type = Type; P.Description = Desc; return P;
+	};
+
+	Schema.Add(TEXT("command"), Prop(TEXT("string"), TEXT("Console command to execute")));
+	Schema.Add(TEXT("world"), Prop(TEXT("string"), TEXT("World context: pie, editor, or game")));
+	Schema.Add(TEXT("player_index"), Prop(TEXT("integer"), TEXT("Optional player controller index for PIE/game")));
+	return Schema;
+}
+
+FBridgeToolResult UExecConsoleCommandTool::Execute(
+	const TSharedPtr<FJsonObject>& Arguments,
+	const FBridgeToolContext& Context)
+{
+	const FString Command = GetStringArgOrDefault(Arguments, TEXT("command"));
+	const FString WorldType = GetStringArgOrDefault(Arguments, TEXT("world"), TEXT("pie"));
+	const int32 PlayerIndex = GetIntArgOrDefault(Arguments, TEXT("player_index"), 0);
+
+	if (Command.IsEmpty())
+	{
+		return FBridgeToolResult::Error(TEXT("exec-console-command: command is required"));
+	}
+
+	UWorld* World = FindWorldByType(WorldType);
+	if (!World)
+	{
+		return FBridgeToolResult::Error(FString::Printf(
+			TEXT("exec-console-command: no %s world available"), *WorldType));
+	}
+
+	bool bSuccess = false;
+	bool bUsedPlayerController = false;
+	FString Output;
+
+	if (WorldType != TEXT("editor"))
+	{
+		if (APlayerController* PC = UGameplayStatics::GetPlayerController(World, PlayerIndex))
+		{
+			Output = PC->ConsoleCommand(Command, true);
+			bSuccess = true;
+			bUsedPlayerController = true;
+		}
+	}
+
+	if (!bSuccess && GEngine)
+	{
+		bSuccess = GEngine->Exec(World, *Command);
+	}
+
+	if (!bSuccess)
+	{
+		return FBridgeToolResult::Error(FString::Printf(
+			TEXT("exec-console-command: failed to execute '%s'"), *Command));
+	}
+
+	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
+	Result->SetBoolField(TEXT("success"), true);
+	Result->SetStringField(TEXT("command"), Command);
+	Result->SetStringField(TEXT("world"), WorldType);
+	Result->SetBoolField(TEXT("used_player_controller"), bUsedPlayerController);
+	Result->SetNumberField(TEXT("player_index"), PlayerIndex);
+	Result->SetStringField(TEXT("output"), Output);
+
+	return FBridgeToolResult::Json(Result);
+}
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/PIE/InspectPawnPossessionTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/PIE/InspectPawnPossessionTool.cpp
new file mode 100644
index 0000000..a5ab8f1
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/PIE/InspectPawnPossessionTool.cpp
@@ -0,0 +1,144 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#include "Tools/PIE/InspectPawnPossessionTool.h"
+#include "SoftUEBridgeEditorModule.h"
+#include "EngineUtils.h"
+#include "GameFramework/Controller.h"
+#include "GameFramework/Pawn.h"
+#include "GameFramework/PlayerController.h"
+#include "AIController.h"
+#include "Components/SceneComponent.h"
+
+namespace
+{
+	bool IsActorVisible(const AActor* Actor)
+	{
+		if (const USceneComponent* Root = Actor ? Actor->GetRootComponent() : nullptr)
+		{
+			return Root->IsVisible();
+		}
+		return Actor ? !Actor->IsHiddenEd() : false;
+	}
+
+	bool MatchesActorFilter(const AActor* Actor, const FString& ActorName, const FString& ClassFilter)
+	{
+		if (!Actor)
+		{
+			return false;
+		}
+
+		const bool bMatchesName = ActorName.IsEmpty()
+			|| Actor->GetName().Equals(ActorName, ESearchCase::IgnoreCase)
+			|| Actor->GetActorNameOrLabel().Equals(ActorName, ESearchCase::IgnoreCase);
+		const bool bMatchesClass = ClassFilter.IsEmpty()
+			|| Actor->GetClass()->GetName().Equals(ClassFilter, ESearchCase::IgnoreCase);
+		return bMatchesName && bMatchesClass;
+	}
+
+	TArray<TSharedPtr<FJsonValue>> VectorToJson(const FVector& Value)
+	{
+		TArray<TSharedPtr<FJsonValue>> Array;
+		Array.Add(MakeShareable(new FJsonValueNumber(Value.X)));
+		Array.Add(MakeShareable(new FJsonValueNumber(Value.Y)));
+		Array.Add(MakeShareable(new FJsonValueNumber(Value.Z)));
+		return Array;
+	}
+}
+
+FString UInspectPawnPossessionTool::GetToolDescription() const
+{
+	return TEXT("Inspect controller/pawn possession links in a running world, including AI auto-possession settings and hidden state.");
+}
+
+TMap<FString, FBridgeSchemaProperty> UInspectPawnPossessionTool::GetInputSchema() const
+{
+	TMap<FString, FBridgeSchemaProperty> Schema;
+
+	auto Prop = [](const FString& Type, const FString& Desc) {
+		FBridgeSchemaProperty P; P.Type = Type; P.Description = Desc; return P;
+	};
+
+	Schema.Add(TEXT("world"), Prop(TEXT("string"), TEXT("World context: pie, editor, or game")));
+	Schema.Add(TEXT("class_filter"), Prop(TEXT("string"), TEXT("Optional pawn class filter")));
+	Schema.Add(TEXT("actor_name"), Prop(TEXT("string"), TEXT("Optional actor/controller name filter")));
+	Schema.Add(TEXT("include_hidden"), Prop(TEXT("boolean"), TEXT("Reserved for future filtering behavior")));
+	return Schema;
+}
+
+FBridgeToolResult UInspectPawnPossessionTool::Execute(
+	const TSharedPtr<FJsonObject>& Arguments,
+	const FBridgeToolContext& Context)
+{
+	const FString WorldType = GetStringArgOrDefault(Arguments, TEXT("world"), TEXT("pie"));
+	const FString ClassFilter = GetStringArgOrDefault(Arguments, TEXT("class_filter"));
+	const FString ActorName = GetStringArgOrDefault(Arguments, TEXT("actor_name"));
+
+	UWorld* World = FindWorldByType(WorldType);
+	if (!World)
+	{
+		return FBridgeToolResult::Error(FString::Printf(
+			TEXT("inspect-pawn-possession: no %s world available"), *WorldType));
+	}
+
+	TArray<TSharedPtr<FJsonValue>> ControllersArray;
+	for (TActorIterator<AController> It(World); It; ++It)
+	{
+		AController* Controller = *It;
+		if (!MatchesActorFilter(Controller, ActorName, TEXT("")))
+		{
+			continue;
+		}
+
+		TSharedPtr<FJsonObject> ControllerJson = MakeShareable(new FJsonObject);
+		ControllerJson->SetStringField(TEXT("name"), Controller->GetName());
+		ControllerJson->SetStringField(TEXT("label"), Controller->GetActorNameOrLabel());
+		ControllerJson->SetStringField(TEXT("class"), Controller->GetClass()->GetPathName());
+		ControllerJson->SetBoolField(TEXT("is_player_controller"), Controller->IsA<APlayerController>());
+		ControllerJson->SetBoolField(TEXT("visible"), IsActorVisible(Controller));
+		ControllerJson->SetBoolField(TEXT("hidden"), !IsActorVisible(Controller));
+		if (APawn* Pawn = Controller->GetPawn())
+		{
+			ControllerJson->SetStringField(TEXT("pawn_name"), Pawn->GetName());
+			ControllerJson->SetStringField(TEXT("pawn_class"), Pawn->GetClass()->GetPathName());
+		}
+		ControllersArray.Add(MakeShareable(new FJsonValueObject(ControllerJson)));
+	}
+
+	TArray<TSharedPtr<FJsonValue>> PawnsArray;
+	for (TActorIterator<APawn> It(World); It; ++It)
+	{
+		APawn* Pawn = *It;
+		if (!MatchesActorFilter(Pawn, ActorName, ClassFilter))
+		{
+			continue;
+		}
+
+		TSharedPtr<FJsonObject> PawnJson = MakeShareable(new FJsonObject);
+		PawnJson->SetStringField(TEXT("name"), Pawn->GetName());
+		PawnJson->SetStringField(TEXT("label"), Pawn->GetActorNameOrLabel());
+		PawnJson->SetStringField(TEXT("class"), Pawn->GetClass()->GetPathName());
+		PawnJson->SetBoolField(TEXT("visible"), IsActorVisible(Pawn));
+		PawnJson->SetBoolField(TEXT("hidden"), !IsActorVisible(Pawn));
+		PawnJson->SetArrayField(TEXT("location"), VectorToJson(Pawn->GetActorLocation()));
+		PawnJson->SetStringField(TEXT("auto_possess_ai"), StaticEnum<EAutoPossessAI>()->GetNameStringByValue(static_cast<int64>(Pawn->AutoPossessAI)));
+		if (Pawn->AIControllerClass)
+		{
+			PawnJson->SetStringField(TEXT("ai_controller_class"), Pawn->AIControllerClass->GetPathName());
+		}
+		if (AController* Controller = Pawn->GetController())
+		{
+			PawnJson->SetStringField(TEXT("controller_name"), Controller->GetName());
+			PawnJson->SetStringField(TEXT("controller_class"), Controller->GetClass()->GetPathName());
+		}
+		PawnsArray.Add(MakeShareable(new FJsonValueObject(PawnJson)));
+	}
+
+	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
+	Result->SetBoolField(TEXT("success"), true);
+	Result->SetStringField(TEXT("world"), WorldType);
+	Result->SetArrayField(TEXT("controllers"), ControllersArray);
+	Result->SetArrayField(TEXT("pawns"), PawnsArray);
+	Result->SetNumberField(TEXT("controller_count"), ControllersArray.Num());
+	Result->SetNumberField(TEXT("pawn_count"), PawnsArray.Num());
+	return FBridgeToolResult::Json(Result);
+}
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Project/ReloadGameplayTagsTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Project/ReloadGameplayTagsTool.cpp
new file mode 100644
index 0000000..7a3cd92
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Project/ReloadGameplayTagsTool.cpp
@@ -0,0 +1,38 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#include "Tools/Project/ReloadGameplayTagsTool.h"
+#include "GameplayTagContainer.h"
+#include "GameplayTagsManager.h"
+#include "GameplayTagsSettings.h"
+
+FString UReloadGameplayTagsTool::GetToolDescription() const
+{
+	return TEXT("Reload GameplayTags settings and refresh in-memory tag tables.");
+}
+
+TMap<FString, FBridgeSchemaProperty> UReloadGameplayTagsTool::GetInputSchema() const
+{
+	return {};
+}
+
+FBridgeToolResult UReloadGameplayTagsTool::Execute(
+	const TSharedPtr<FJsonObject>& Arguments,
+	const FBridgeToolContext& Context)
+{
+	if (UGameplayTagsSettings* Settings = GetMutableDefault<UGameplayTagsSettings>())
+	{
+		Settings->ReloadConfig();
+	}
+
+	UGameplayTagsManager& Manager = UGameplayTagsManager::Get();
+	Manager.LoadGameplayTagTables(false);
+
+	FGameplayTagContainer AllTags;
+	Manager.RequestAllGameplayTags(AllTags, false);
+
+	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
+	Result->SetBoolField(TEXT("success"), true);
+	Result->SetNumberField(TEXT("tag_count"), AllTags.Num());
+	Result->SetStringField(TEXT("message"), TEXT("GameplayTags settings reloaded and tag tables refreshed."));
+	return FBridgeToolResult::Json(Result);
+}
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Project/RequestGameplayTagTool.cpp b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Project/RequestGameplayTagTool.cpp
new file mode 100644
index 0000000..e48c628
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Private/Tools/Project/RequestGameplayTagTool.cpp
@@ -0,0 +1,41 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#include "Tools/Project/RequestGameplayTagTool.h"
+#include "GameplayTagContainer.h"
+
+FString URequestGameplayTagTool::GetToolDescription() const
+{
+	return TEXT("Resolve a registered GameplayTag by name and return its validity.");
+}
+
+TMap<FString, FBridgeSchemaProperty> URequestGameplayTagTool::GetInputSchema() const
+{
+	TMap<FString, FBridgeSchemaProperty> Schema;
+	FBridgeSchemaProperty TagName;
+	TagName.Type = TEXT("string");
+	TagName.Description = TEXT("GameplayTag name to resolve");
+	TagName.bRequired = true;
+	Schema.Add(TEXT("tag_name"), TagName);
+	return Schema;
+}
+
+FBridgeToolResult URequestGameplayTagTool::Execute(
+	const TSharedPtr<FJsonObject>& Arguments,
+	const FBridgeToolContext& Context)
+{
+	const FString TagName = GetStringArgOrDefault(Arguments, TEXT("tag_name"));
+	if (TagName.IsEmpty())
+	{
+		return FBridgeToolResult::Error(TEXT("request-gameplay-tag: tag_name is required"));
+	}
+
+	const FGameplayTag Tag = FGameplayTag::RequestGameplayTag(FName(*TagName), false);
+
+	TSharedPtr<FJsonObject> Result = MakeShareable(new FJsonObject);
+	Result->SetBoolField(TEXT("success"), Tag.IsValid());
+	Result->SetBoolField(TEXT("valid"), Tag.IsValid());
+	Result->SetStringField(TEXT("requested_name"), TagName);
+	Result->SetStringField(TEXT("tag_name"), Tag.IsValid() ? Tag.ToString() : FString());
+	Result->SetStringField(TEXT("export_text"), Tag.IsValid() ? FString::Printf(TEXT("(TagName=\"%s\")"), *Tag.ToString()) : FString());
+	return FBridgeToolResult::Json(Result);
+}
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Analysis/ValidateClassPathTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Analysis/ValidateClassPathTool.h
new file mode 100644
index 0000000..00312c0
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Analysis/ValidateClassPathTool.h
@@ -0,0 +1,22 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Tools/BridgeToolBase.h"
+#include "ValidateClassPathTool.generated.h"
+
+UCLASS()
+class SOFTUEBRIDGEEDITOR_API UValidateClassPathTool : public UBridgeToolBase
+{
+	GENERATED_BODY()
+
+public:
+	virtual FString GetToolName() const override { return TEXT("validate-class-path"); }
+	virtual FString GetToolDescription() const override;
+	virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
+	virtual TArray<FString> GetRequiredParams() const override { return { TEXT("class_path") }; }
+	virtual FBridgeToolResult Execute(
+		const TSharedPtr<FJsonObject>& Arguments,
+		const FBridgeToolContext& Context) override;
+};
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Asset/ReleaseAssetLockTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Asset/ReleaseAssetLockTool.h
new file mode 100644
index 0000000..567af87
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Asset/ReleaseAssetLockTool.h
@@ -0,0 +1,22 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Tools/BridgeToolBase.h"
+#include "ReleaseAssetLockTool.generated.h"
+
+UCLASS()
+class SOFTUEBRIDGEEDITOR_API UReleaseAssetLockTool : public UBridgeToolBase
+{
+	GENERATED_BODY()
+
+public:
+	virtual FString GetToolName() const override { return TEXT("release-asset-lock"); }
+	virtual FString GetToolDescription() const override;
+	virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
+	virtual TArray<FString> GetRequiredParams() const override { return { TEXT("asset_path") }; }
+	virtual FBridgeToolResult Execute(
+		const TSharedPtr<FJsonObject>& Arguments,
+		const FBridgeToolContext& Context) override;
+};
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Build/TriggerLiveCodingTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Build/TriggerLiveCodingTool.h
index aa4c0fc..21a4e25 100644
--- a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Build/TriggerLiveCodingTool.h
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Build/TriggerLiveCodingTool.h
@@ -33,6 +33,8 @@ public:
 
 private:
 #if PLATFORM_WINDOWS
+	bool DetectReflectedHeaderChanges(TArray<FString>& OutFiles) const;
+
 	// Execute synchronous compilation (blocks until complete)
 	FBridgeToolResult ExecuteSynchronous(ILiveCodingModule* LiveCodingModule);
 
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/PIE/ExecConsoleCommandTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/PIE/ExecConsoleCommandTool.h
new file mode 100644
index 0000000..a190262
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/PIE/ExecConsoleCommandTool.h
@@ -0,0 +1,22 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Tools/BridgeToolBase.h"
+#include "ExecConsoleCommandTool.generated.h"
+
+UCLASS()
+class SOFTUEBRIDGEEDITOR_API UExecConsoleCommandTool : public UBridgeToolBase
+{
+	GENERATED_BODY()
+
+public:
+	virtual FString GetToolName() const override { return TEXT("exec-console-command"); }
+	virtual FString GetToolDescription() const override;
+	virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
+	virtual TArray<FString> GetRequiredParams() const override { return { TEXT("command") }; }
+	virtual FBridgeToolResult Execute(
+		const TSharedPtr<FJsonObject>& Arguments,
+		const FBridgeToolContext& Context) override;
+};
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/PIE/InspectPawnPossessionTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/PIE/InspectPawnPossessionTool.h
new file mode 100644
index 0000000..e2fde68
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/PIE/InspectPawnPossessionTool.h
@@ -0,0 +1,21 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Tools/BridgeToolBase.h"
+#include "InspectPawnPossessionTool.generated.h"
+
+UCLASS()
+class SOFTUEBRIDGEEDITOR_API UInspectPawnPossessionTool : public UBridgeToolBase
+{
+	GENERATED_BODY()
+
+public:
+	virtual FString GetToolName() const override { return TEXT("inspect-pawn-possession"); }
+	virtual FString GetToolDescription() const override;
+	virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
+	virtual FBridgeToolResult Execute(
+		const TSharedPtr<FJsonObject>& Arguments,
+		const FBridgeToolContext& Context) override;
+};
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Project/ReloadGameplayTagsTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Project/ReloadGameplayTagsTool.h
new file mode 100644
index 0000000..b024136
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Project/ReloadGameplayTagsTool.h
@@ -0,0 +1,21 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Tools/BridgeToolBase.h"
+#include "ReloadGameplayTagsTool.generated.h"
+
+UCLASS()
+class SOFTUEBRIDGEEDITOR_API UReloadGameplayTagsTool : public UBridgeToolBase
+{
+	GENERATED_BODY()
+
+public:
+	virtual FString GetToolName() const override { return TEXT("reload-gameplay-tags"); }
+	virtual FString GetToolDescription() const override;
+	virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
+	virtual FBridgeToolResult Execute(
+		const TSharedPtr<FJsonObject>& Arguments,
+		const FBridgeToolContext& Context) override;
+};
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Project/RequestGameplayTagTool.h b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Project/RequestGameplayTagTool.h
new file mode 100644
index 0000000..71eeb85
--- /dev/null
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/Public/Tools/Project/RequestGameplayTagTool.h
@@ -0,0 +1,22 @@
+// Copyright soft-ue-expert. All Rights Reserved.
+
+#pragma once
+
+#include "CoreMinimal.h"
+#include "Tools/BridgeToolBase.h"
+#include "RequestGameplayTagTool.generated.h"
+
+UCLASS()
+class SOFTUEBRIDGEEDITOR_API URequestGameplayTagTool : public UBridgeToolBase
+{
+	GENERATED_BODY()
+
+public:
+	virtual FString GetToolName() const override { return TEXT("request-gameplay-tag"); }
+	virtual FString GetToolDescription() const override;
+	virtual TMap<FString, FBridgeSchemaProperty> GetInputSchema() const override;
+	virtual TArray<FString> GetRequiredParams() const override { return { TEXT("tag_name") }; }
+	virtual FBridgeToolResult Execute(
+		const TSharedPtr<FJsonObject>& Arguments,
+		const FBridgeToolContext& Context) override;
+};
diff --git a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/SoftUEBridgeEditor.Build.cs b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/SoftUEBridgeEditor.Build.cs
index 5a686bf..29b8718 100644
--- a/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/SoftUEBridgeEditor.Build.cs
+++ b/plugin/SoftUEBridge/Source/SoftUEBridgeEditor/SoftUEBridgeEditor.Build.cs
@@ -65,6 +65,7 @@ public class SoftUEBridgeEditor : ModuleRules
 			// Input
 			"InputCore",
 			"EnhancedInput",
+			"AIModule",
 
 			// Project Settings
 			"GameplayTags",
diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py
index 0ff916b..1683bc4 100644
--- a/tests/cli/test_main.py
+++ b/tests/cli/test_main.py
@@ -26,15 +26,19 @@ from soft_ue_cli.__main__ import (
     cmd_capture_screenshot,
     cmd_capture_viewport,
     cmd_delete_script,
+    cmd_exec_console_command,
     cmd_inspect_anim_instance,
+    cmd_inspect_pawn_possession,
     cmd_list_scripts,
     cmd_pie_tick,
     cmd_query_enum,
     cmd_query_mpc,
     cmd_query_struct,
+    cmd_release_asset_lock,
     cmd_run_python_script,
     cmd_save_script,
     cmd_setup,
+    cmd_validate_class_path,
 )
 
 
@@ -142,6 +146,9 @@ def test_parser_get_logs_defaults():
     args = parser.parse_args(["get-logs"])
     assert args.lines == 100
     assert args.raw is False
+    assert args.contains is None
+    assert args.since is None
+    assert args.tail_follow is False
 
 
 def test_parser_set_console_var():
@@ -165,6 +172,14 @@ def test_parser_build_and_relaunch_flags():
     assert args.wait is True
 
 
+def test_parser_get_logs_follow_args():
+    parser = build_parser()
+    args = parser.parse_args(["get-logs", "--contains", "warning", "--since", "42", "--tail-follow"])
+    assert args.contains == "warning"
+    assert args.since == "42"
+    assert args.tail_follow is True
+
+
 def test_parser_inspect_uasset():
     parser = build_parser()
     args = parser.parse_args(["inspect-uasset", "BP_Player.uasset", "--sections", "summary,properties", "--format", "json"])
@@ -409,9 +424,12 @@ def test_run_python_script_path_reads_file(tmp_path):
     parser = build_parser()
     args = parser.parse_args(["run-python-script", "--script-path", str(script_path), "--world", "pie"])
 
-    with patch("soft_ue_cli.__main__.call_tool", return_value={"output": "ok"}) as mock_call:
+    with patch("soft_ue_cli.__main__._ensure_pie_running") as mock_ensure, patch(
+        "soft_ue_cli.__main__.call_tool", return_value={"output": "ok"}
+    ) as mock_call:
         cmd_run_python_script(args)
 
+    mock_ensure.assert_called_once()
     mock_call.assert_called_once_with(
         "run-python-script",
         {
@@ -421,6 +439,18 @@ def test_run_python_script_path_reads_file(tmp_path):
     )
 
 
+def test_run_python_script_world_pie_auto_start():
+    parser = build_parser()
+    args = parser.parse_args(["run-python-script", "--script", "print('ok')", "--world", "pie", "--auto-start-pie"])
+
+    with patch("soft_ue_cli.__main__._ensure_pie_running") as mock_ensure, patch(
+        "soft_ue_cli.__main__.call_tool", return_value={"output": "ok"}
+    ):
+        cmd_run_python_script(args)
+
+    mock_ensure.assert_called_once()
+
+
 def test_run_python_script_path_missing_exits(tmp_path):
     parser = build_parser()
     args = parser.parse_args(["run-python-script", "--script-path", str(tmp_path / "missing.py")])
@@ -510,6 +540,89 @@ def test_cmd_build_and_relaunch_forwards_args(capsys):
     )
 
 
+def test_parser_exec_console_command():
+    parser = build_parser()
+    args = parser.parse_args(["exec-console-command", "--world", "editor", "stat", "fps"])
+    assert args.world == "editor"
+    assert args.command_parts == ["stat", "fps"]
+
+
+def test_cmd_exec_console_command_forwards_args():
+    parser = build_parser()
+    args = parser.parse_args(["exec-console-command", "--world", "editor", "--player-index", "1", "stat", "fps"])
+
+    with patch("soft_ue_cli.__main__._run_tool", return_value={"success": True}) as mock_run:
+        cmd_exec_console_command(args)
+
+    mock_run.assert_called_once_with(
+        "exec-console-command",
+        {"command": "stat fps", "world": "editor", "player_index": 1},
+    )
+
+
+def test_cmd_exec_console_command_auto_starts_pie():
+    parser = build_parser()
+    args = parser.parse_args(["exec-console-command", "--auto-start-pie", "stat", "fps"])
+
+    with patch("soft_ue_cli.__main__._ensure_pie_running") as mock_ensure, patch(
+        "soft_ue_cli.__main__._run_tool", return_value={"success": True}
+    ):
+        cmd_exec_console_command(args)
+
+    mock_ensure.assert_called_once()
+
+
+def test_parser_validate_class_path():
+    parser = build_parser()
+    args = parser.parse_args(["validate-class-path", "/Game/BP_Hero.BP_Hero_C", "--parent-depth", "5"])
+    assert args.class_path == "/Game/BP_Hero.BP_Hero_C"
+    assert args.parent_depth == 5
+
+
+def test_cmd_validate_class_path_forwards_args():
+    parser = build_parser()
+    args = parser.parse_args(["validate-class-path", "/Game/BP_Hero"])
+
+    with patch("soft_ue_cli.__main__._run_tool", return_value={"success": True}) as mock_run:
+        cmd_validate_class_path(args)
+
+    mock_run.assert_called_once_with("validate-class-path", {"class_path": "/Game/BP_Hero"})
+
+
+def test_parser_inspect_pawn_possession():
+    parser = build_parser()
+    args = parser.parse_args(["inspect-pawn-possession", "--class-filter", "Character", "--actor-name", "Hero"])
+    assert args.class_filter == "Character"
+    assert args.actor_name == "Hero"
+    assert args.world == "pie"
+
+
+def test_cmd_inspect_pawn_possession_forwards_args():
+    parser = build_parser()
+    args = parser.parse_args(["inspect-pawn-possession", "--world", "editor", "--class-filter", "Character"])
+
+    with patch("soft_ue_cli.__main__._run_tool", return_value={"success": True}) as mock_run:
+        cmd_inspect_pawn_possession(args)
+
+    mock_run.assert_called_once_with("inspect-pawn-possession", {"world": "editor", "class_filter": "Character"})
+
+
+def test_parser_release_asset_lock():
+    parser = build_parser()
+    args = parser.parse_args(["release-asset-lock", "/Game/Blueprints/BP_Player"])
+    assert args.asset_path == "/Game/Blueprints/BP_Player"
+
+
+def test_cmd_release_asset_lock_forwards_args():
+    parser = build_parser()
+    args = parser.parse_args(["release-asset-lock", "/Game/Blueprints/BP_Player"])
+
+    with patch("soft_ue_cli.__main__._run_tool", return_value={"success": True}) as mock_run:
+        cmd_release_asset_lock(args)
+
+    mock_run.assert_called_once_with("release-asset-lock", {"asset_path": "/Game/Blueprints/BP_Player"})
+
+
 # -- capture-screenshot parser -------------------------------------------------
 
 
