From 7f0d6d759da2c2d7895fe103a4cf50f76bda6223 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 18:06:36 +0900 Subject: [PATCH 1/4] Document reflection-based live state recovery and pause-point capture edge cases Second-round PlayMode/E2E verification surfaced several pause-point and dynamic-code techniques that were only discovered ad hoc during testing, not written down anywhere: - execute-dynamic-code: writing private fields via reflection to recover or steer a stuck PlayMode session without stopping Play, editing code, or recompiling. - pause-point: UnityEngine.Object captures only ever carry identifying info, never custom fields; nested-field previews off `this` bottom out at MaxCollectionPreviewDepth (2) per captured variable; setting up a self-progressing game's board via TryGetCapturedValue("this") while paused, reserving real input for only the verified move; using a temporary if-guarded Debug.Assert as an observation line when every reachable line runs unconditionally every frame; and that await-pause-point now extends a marker's own timeout before waiting, so enable's --timeout-seconds no longer needs to be inflated to survive a slow multi-step CLI sequence. Regenerated .claude/.agents skill copies with `uloop skills install` and confirmed byte-identical match against source. This also picks up one already-merged source change (#1729's zero-hit hint) whose generated copies had drifted out of sync before this PR. --- .agents/skills/uloop-execute-dynamic-code/SKILL.md | 2 ++ .agents/skills/uloop-find-game-objects/SKILL.md | 6 +++++- .agents/skills/uloop-pause-point/SKILL.md | 7 ++++++- .claude/skills/uloop-execute-dynamic-code/SKILL.md | 2 ++ .claude/skills/uloop-find-game-objects/SKILL.md | 6 +++++- .claude/skills/uloop-pause-point/SKILL.md | 7 ++++++- .../src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 7 ++++++- .../FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md | 2 ++ 8 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index 9831d85fa2..cf03e8c568 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,6 +15,8 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Reflection is otherwise disallowed without explicit user permission project-wide; `execute-dynamic-code` is the one place it is allowed, which is exactly what this technique relies on. + ## Parameters - `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. diff --git a/.agents/skills/uloop-find-game-objects/SKILL.md b/.agents/skills/uloop-find-game-objects/SKILL.md index d522998399..76e8b2a88b 100644 --- a/.agents/skills/uloop-find-game-objects/SKILL.md +++ b/.agents/skills/uloop-find-game-objects/SKILL.md @@ -33,7 +33,7 @@ uloop find-game-objects [options] | Mode | Description | |------|-------------| -| `Exact` | Exact name match (default) | +| `Exact` | Exact name match (default). **Trap**: `--name-pattern "Camera"` will not match a GameObject named `"Main Camera"` — use `Contains` or `Regex` for partial matching. A zero-hit `Exact` search returns a `Message` hint pointing this out (see Output). | | `Path` | Hierarchy path search (e.g., `Canvas/Button`) | | `Regex` | Regular expression pattern | | `Contains` | Partial name match | @@ -62,3 +62,7 @@ For `Selected` mode with **multiple** successfully serialized GameObjects, inlin - `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. + +### Zero-hit Exact-mode hint + +If an `Exact` search with a non-empty `--name-pattern` finds nothing (`TotalFound: 0`), `Message` carries a hint suggesting `Contains` or `Regex` instead of leaving the zero hit unexplained. This does not change the default mode or matching logic — it only surfaces the same trap described above at the point it actually bites. diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 51285a5adb..e5e2aaf3fe 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. You do not need to inflate this value to cover a slow multi-step CLI sequence between `enable-pause-point` and `await-pause-point` (for example seeding state with `execute-dynamic-code` first): `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so earlier setup steps cannot eat into the window it observes. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -58,7 +58,9 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. - `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. `InstanceField` entries come from a reflection walk of the paused instance's declared type, not from the method's IL usage, so a field the method never reads can still appear — and `MaxCapturedVariableCount` still caps the total entry count across all scopes, so a field-heavy type can push some instance fields out of the snapshot. If a specific field you want is missing, read it directly from the live instance instead of waiting on the capped snapshot: while still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live `this` reference, so `execute-dynamic-code` can read any field or property off it regardless of the cap. - The snapshot also includes a synthetic `this` entry (Scope `This`) for the paused instance itself, so you can tell which instance or GameObject was hit via its `UnityObjectPath` and `UnityObjectInstanceId`. For an async or coroutine method it resolves to the original outer instance, not the compiler-generated state machine, and static methods emit no `this` entry. While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live instance reference (for example so a watch expression can read `transform.position`). +- Reaching a value through nested fields off `this` (or off any captured object) bottoms out at type-name-only text past `MaxCollectionPreviewDepth` (2 levels), and this depth budget is independent per captured variable — going deep through `this` does not borrow depth from other variables in the same snapshot, nor vice versa. If the value you actually need is more than two levels deep, pick a pause point line where that value is itself a direct local or parameter instead of a nested field, so it gets its own top-level captured entry with its own full depth budget. - `UnityEngine.Object` values additionally carry `UnityObjectKind` (`SceneObject`, `PrefabAsset`, `Asset`, `RuntimeInstance`, or `Destroyed`), `UnityObjectPath`, and `UnityObjectInstanceId`. These three fields appear only for Unity object values; a non-Unity-object variable (an `int`, a `string`, a plain class) omits all three from the JSON entirely instead of sending them as empty/zero. Check whether `UnityObjectKind` is present to tell the two cases apart. Use the fields as handles for the next dig: a `SceneObject` path feeds `get-hierarchy`/`find-game-objects`, an asset path locates the asset, and the InstanceID works with `execute-dynamic-code`. +- A captured `UnityEngine.Object` value's `Value` string only ever carries identifying information (its `name` and similar identifiers), never its custom fields — a `MonoBehaviour`'s own `[SerializeField]` values do not appear in that entry. When you need a custom field's value, either expose it through the component's own `.name`/`ToString()` so it appears in the identifier, or read it directly while still paused with `execute-dynamic-code` off the live instance (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). - `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap. - A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value. - async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. @@ -99,6 +101,8 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity. +For a self-progressing game (a board that keeps advancing, an opponent that keeps moving), arranging a specific scenario through real input alone is a race against the game clock. While paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the board/state you need, reserving actual simulated key/mouse input for only the one move you are actually verifying. This keeps the setup deterministic while still exercising the real input path for the observed action itself. + ## Watch Expressions Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: @@ -219,6 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs; the `Debug.Assert` is a natural, low-friction choice to put inside it because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe, and it compiles away without changing behavior once removed. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index 9831d85fa2..cf03e8c568 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,6 +15,8 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Reflection is otherwise disallowed without explicit user permission project-wide; `execute-dynamic-code` is the one place it is allowed, which is exactly what this technique relies on. + ## Parameters - `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. diff --git a/.claude/skills/uloop-find-game-objects/SKILL.md b/.claude/skills/uloop-find-game-objects/SKILL.md index d522998399..76e8b2a88b 100644 --- a/.claude/skills/uloop-find-game-objects/SKILL.md +++ b/.claude/skills/uloop-find-game-objects/SKILL.md @@ -33,7 +33,7 @@ uloop find-game-objects [options] | Mode | Description | |------|-------------| -| `Exact` | Exact name match (default) | +| `Exact` | Exact name match (default). **Trap**: `--name-pattern "Camera"` will not match a GameObject named `"Main Camera"` — use `Contains` or `Regex` for partial matching. A zero-hit `Exact` search returns a `Message` hint pointing this out (see Output). | | `Path` | Hierarchy path search (e.g., `Canvas/Button`) | | `Regex` | Regular expression pattern | | `Contains` | Partial name match | @@ -62,3 +62,7 @@ For `Selected` mode with **multiple** successfully serialized GameObjects, inlin - `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. + +### Zero-hit Exact-mode hint + +If an `Exact` search with a non-empty `--name-pattern` finds nothing (`TotalFound: 0`), `Message` carries a hint suggesting `Contains` or `Regex` instead of leaving the zero hit unexplained. This does not change the default mode or matching logic — it only surfaces the same trap described above at the point it actually bites. diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 51285a5adb..e5e2aaf3fe 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. You do not need to inflate this value to cover a slow multi-step CLI sequence between `enable-pause-point` and `await-pause-point` (for example seeding state with `execute-dynamic-code` first): `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so earlier setup steps cannot eat into the window it observes. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -58,7 +58,9 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. - `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. `InstanceField` entries come from a reflection walk of the paused instance's declared type, not from the method's IL usage, so a field the method never reads can still appear — and `MaxCapturedVariableCount` still caps the total entry count across all scopes, so a field-heavy type can push some instance fields out of the snapshot. If a specific field you want is missing, read it directly from the live instance instead of waiting on the capped snapshot: while still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live `this` reference, so `execute-dynamic-code` can read any field or property off it regardless of the cap. - The snapshot also includes a synthetic `this` entry (Scope `This`) for the paused instance itself, so you can tell which instance or GameObject was hit via its `UnityObjectPath` and `UnityObjectInstanceId`. For an async or coroutine method it resolves to the original outer instance, not the compiler-generated state machine, and static methods emit no `this` entry. While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live instance reference (for example so a watch expression can read `transform.position`). +- Reaching a value through nested fields off `this` (or off any captured object) bottoms out at type-name-only text past `MaxCollectionPreviewDepth` (2 levels), and this depth budget is independent per captured variable — going deep through `this` does not borrow depth from other variables in the same snapshot, nor vice versa. If the value you actually need is more than two levels deep, pick a pause point line where that value is itself a direct local or parameter instead of a nested field, so it gets its own top-level captured entry with its own full depth budget. - `UnityEngine.Object` values additionally carry `UnityObjectKind` (`SceneObject`, `PrefabAsset`, `Asset`, `RuntimeInstance`, or `Destroyed`), `UnityObjectPath`, and `UnityObjectInstanceId`. These three fields appear only for Unity object values; a non-Unity-object variable (an `int`, a `string`, a plain class) omits all three from the JSON entirely instead of sending them as empty/zero. Check whether `UnityObjectKind` is present to tell the two cases apart. Use the fields as handles for the next dig: a `SceneObject` path feeds `get-hierarchy`/`find-game-objects`, an asset path locates the asset, and the InstanceID works with `execute-dynamic-code`. +- A captured `UnityEngine.Object` value's `Value` string only ever carries identifying information (its `name` and similar identifiers), never its custom fields — a `MonoBehaviour`'s own `[SerializeField]` values do not appear in that entry. When you need a custom field's value, either expose it through the component's own `.name`/`ToString()` so it appears in the identifier, or read it directly while still paused with `execute-dynamic-code` off the live instance (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). - `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap. - A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value. - async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. @@ -99,6 +101,8 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity. +For a self-progressing game (a board that keeps advancing, an opponent that keeps moving), arranging a specific scenario through real input alone is a race against the game clock. While paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the board/state you need, reserving actual simulated key/mouse input for only the one move you are actually verifying. This keeps the setup deterministic while still exercising the real input path for the observed action itself. + ## Watch Expressions Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: @@ -219,6 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs; the `Debug.Assert` is a natural, low-friction choice to put inside it because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe, and it compiles away without changing behavior once removed. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 51285a5adb..e5e2aaf3fe 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. You do not need to inflate this value to cover a slow multi-step CLI sequence between `enable-pause-point` and `await-pause-point` (for example seeding state with `execute-dynamic-code` first): `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so earlier setup steps cannot eat into the window it observes. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -58,7 +58,9 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. - `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. `InstanceField` entries come from a reflection walk of the paused instance's declared type, not from the method's IL usage, so a field the method never reads can still appear — and `MaxCapturedVariableCount` still caps the total entry count across all scopes, so a field-heavy type can push some instance fields out of the snapshot. If a specific field you want is missing, read it directly from the live instance instead of waiting on the capped snapshot: while still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live `this` reference, so `execute-dynamic-code` can read any field or property off it regardless of the cap. - The snapshot also includes a synthetic `this` entry (Scope `This`) for the paused instance itself, so you can tell which instance or GameObject was hit via its `UnityObjectPath` and `UnityObjectInstanceId`. For an async or coroutine method it resolves to the original outer instance, not the compiler-generated state machine, and static methods emit no `this` entry. While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live instance reference (for example so a watch expression can read `transform.position`). +- Reaching a value through nested fields off `this` (or off any captured object) bottoms out at type-name-only text past `MaxCollectionPreviewDepth` (2 levels), and this depth budget is independent per captured variable — going deep through `this` does not borrow depth from other variables in the same snapshot, nor vice versa. If the value you actually need is more than two levels deep, pick a pause point line where that value is itself a direct local or parameter instead of a nested field, so it gets its own top-level captured entry with its own full depth budget. - `UnityEngine.Object` values additionally carry `UnityObjectKind` (`SceneObject`, `PrefabAsset`, `Asset`, `RuntimeInstance`, or `Destroyed`), `UnityObjectPath`, and `UnityObjectInstanceId`. These three fields appear only for Unity object values; a non-Unity-object variable (an `int`, a `string`, a plain class) omits all three from the JSON entirely instead of sending them as empty/zero. Check whether `UnityObjectKind` is present to tell the two cases apart. Use the fields as handles for the next dig: a `SceneObject` path feeds `get-hierarchy`/`find-game-objects`, an asset path locates the asset, and the InstanceID works with `execute-dynamic-code`. +- A captured `UnityEngine.Object` value's `Value` string only ever carries identifying information (its `name` and similar identifiers), never its custom fields — a `MonoBehaviour`'s own `[SerializeField]` values do not appear in that entry. When you need a custom field's value, either expose it through the component's own `.name`/`ToString()` so it appears in the identifier, or read it directly while still paused with `execute-dynamic-code` off the live instance (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). - `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap. - A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value. - async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. @@ -99,6 +101,8 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity. +For a self-progressing game (a board that keeps advancing, an opponent that keeps moving), arranging a specific scenario through real input alone is a race against the game clock. While paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the board/state you need, reserving actual simulated key/mouse input for only the one move you are actually verifying. This keeps the setup deterministic while still exercising the real input path for the observed action itself. + ## Watch Expressions Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: @@ -219,6 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs; the `Debug.Assert` is a natural, low-friction choice to put inside it because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe, and it compiles away without changing behavior once removed. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index 9831d85fa2..cf03e8c568 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -15,6 +15,8 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Reflection is otherwise disallowed without explicit user permission project-wide; `execute-dynamic-code` is the one place it is allowed, which is exactly what this technique relies on. + ## Parameters - `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. From a3b100bebc5bf36aa32dd13612d9bb2e5c66cca4 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 18:17:54 +0900 Subject: [PATCH 2/4] Fix inaccurate claims in pause-point and reflection skill docs Advisor review of PR-I found three documentation defects: the timeout note conflated the setup window with the wait window even though a marker already expired before await-pause-point starts cannot be resurrected; the if+Debug.Assert observation pattern guarded on the same postcondition it asserted, making the assert a tautology instead of checking the actual mutation; and the reflection-is-otherwise-disallowed line stated the author's personal global policy as if it were a project-wide skill contract, which this skill package ships to every user. --- .agents/skills/uloop-execute-dynamic-code/SKILL.md | 2 +- .agents/skills/uloop-pause-point/SKILL.md | 4 ++-- .claude/skills/uloop-execute-dynamic-code/SKILL.md | 2 +- .claude/skills/uloop-pause-point/SKILL.md | 4 ++-- Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 4 ++-- .../Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index cf03e8c568..08ed081d93 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Reflection is otherwise disallowed without explicit user permission project-wide; `execute-dynamic-code` is the one place it is allowed, which is exactly what this technique relies on. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. This snippet-only reflection use is a one-off diagnostic action against a running session, not a change to production code, so it does not carry the maintainability and encapsulation costs that make reflection unsuitable in shipped code. ## Parameters diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index e5e2aaf3fe..5c9efdfbb0 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. You do not need to inflate this value to cover a slow multi-step CLI sequence between `enable-pause-point` and `await-pause-point` (for example seeding state with `execute-dynamic-code` first): `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so earlier setup steps cannot eat into the window it observes. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. This value only needs to cover the setup you do between `enable-pause-point` and calling `await-pause-point` (for example seeding state with `execute-dynamic-code` first) — the marker still expires if setup runs longer than that, since a marker cannot be resurrected once expired. It no longer needs to also cover the wait itself: `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so you do not need to add the wait's duration on top when sizing `enable-pause-point --timeout-seconds`. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -223,7 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. -- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs; the `Debug.Assert` is a natural, low-friction choice to put inside it because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe, and it compiles away without changing behavior once removed. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { ; Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs, guarding on the event you actually care about rather than repeating the postcondition it produces; the `Debug.Assert` line right after the mutation is a natural, low-friction place to put the pause point because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index cf03e8c568..08ed081d93 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Reflection is otherwise disallowed without explicit user permission project-wide; `execute-dynamic-code` is the one place it is allowed, which is exactly what this technique relies on. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. This snippet-only reflection use is a one-off diagnostic action against a running session, not a change to production code, so it does not carry the maintainability and encapsulation costs that make reflection unsuitable in shipped code. ## Parameters diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index e5e2aaf3fe..5c9efdfbb0 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. You do not need to inflate this value to cover a slow multi-step CLI sequence between `enable-pause-point` and `await-pause-point` (for example seeding state with `execute-dynamic-code` first): `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so earlier setup steps cannot eat into the window it observes. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. This value only needs to cover the setup you do between `enable-pause-point` and calling `await-pause-point` (for example seeding state with `execute-dynamic-code` first) — the marker still expires if setup runs longer than that, since a marker cannot be resurrected once expired. It no longer needs to also cover the wait itself: `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so you do not need to add the wait's duration on top when sizing `enable-pause-point --timeout-seconds`. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -223,7 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. -- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs; the `Debug.Assert` is a natural, low-friction choice to put inside it because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe, and it compiles away without changing behavior once removed. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { ; Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs, guarding on the event you actually care about rather than repeating the postcondition it produces; the `Debug.Assert` line right after the mutation is a natural, low-friction place to put the pause point because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index e5e2aaf3fe..5c9efdfbb0 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. You do not need to inflate this value to cover a slow multi-step CLI sequence between `enable-pause-point` and `await-pause-point` (for example seeding state with `execute-dynamic-code` first): `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so earlier setup steps cannot eat into the window it observes. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. This value only needs to cover the setup you do between `enable-pause-point` and calling `await-pause-point` (for example seeding state with `execute-dynamic-code` first) — the marker still expires if setup runs longer than that, since a marker cannot be resurrected once expired. It no longer needs to also cover the wait itself: `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so you do not need to add the wait's duration on top when sizing `enable-pause-point --timeout-seconds`. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -223,7 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. -- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs; the `Debug.Assert` is a natural, low-friction choice to put inside it because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe, and it compiles away without changing behavior once removed. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { ; Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs, guarding on the event you actually care about rather than repeating the postcondition it produces; the `Debug.Assert` line right after the mutation is a natural, low-friction place to put the pause point because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index cf03e8c568..08ed081d93 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Reflection is otherwise disallowed without explicit user permission project-wide; `execute-dynamic-code` is the one place it is allowed, which is exactly what this technique relies on. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. This snippet-only reflection use is a one-off diagnostic action against a running session, not a change to production code, so it does not carry the maintainability and encapsulation costs that make reflection unsuitable in shipped code. ## Parameters From c6e4a4042d82153fcf68428f5c67a3e9a47064e8 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 18:21:53 +0900 Subject: [PATCH 3/4] Apply advisor-authored wording for the three flagged skill doc issues The previous fix commit reworded these sections in the implementer's own words. The user asked that the skill wording itself come from the advisor, so this replaces that wording verbatim with the text the advisor supplied for the timeout note, the if+Debug.Assert observation pattern, and the reflection rationale. --- .agents/skills/uloop-execute-dynamic-code/SKILL.md | 2 +- .agents/skills/uloop-pause-point/SKILL.md | 4 ++-- .claude/skills/uloop-execute-dynamic-code/SKILL.md | 2 +- .claude/skills/uloop-pause-point/SKILL.md | 4 ++-- Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 4 ++-- .../Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index 08ed081d93..475754938f 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. This snippet-only reflection use is a one-off diagnostic action against a running session, not a change to production code, so it does not carry the maintainability and encapsulation costs that make reflection unsuitable in shipped code. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. ## Parameters diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 5c9efdfbb0..a32ebb6cb8 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. This value only needs to cover the setup you do between `enable-pause-point` and calling `await-pause-point` (for example seeding state with `execute-dynamic-code` first) — the marker still expires if setup runs longer than that, since a marker cannot be resurrected once expired. It no longer needs to also cover the wait itself: `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so you do not need to add the wait's duration on top when sizing `enable-pause-point --timeout-seconds`. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. Size it to cover only the setup steps you run between `enable-pause-point` and the `await-pause-point` call (for example seeding state with `execute-dynamic-code`): once `await-pause-point` starts waiting, it extends the marker's remaining lifetime to at least its own `--timeout-seconds`, so you no longer need to also budget for the wait itself. The extension cannot revive a marker that has already expired, so the enable timeout still has to outlive the setup steps themselves. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -223,7 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. -- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { ; Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs, guarding on the event you actually care about rather than repeating the postcondition it produces; the `Debug.Assert` line right after the mutation is a natural, low-friction place to put the pause point because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, move the moment you want to observe into a conditional block: `if () { ; Debug.Assert(); }`, then target the pause point at the `Debug.Assert` line. The `if` creates a line that executes only when the event actually happens, so the pause point no longer fires on the very next frame; the `Debug.Assert` states the mutation's postcondition, so the line you pause on is meaningful production code ("this must hold here") rather than an arbitrary probe, and it can stay in the codebase after the investigation ends. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index 08ed081d93..475754938f 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. This snippet-only reflection use is a one-off diagnostic action against a running session, not a change to production code, so it does not carry the maintainability and encapsulation costs that make reflection unsuitable in shipped code. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. ## Parameters diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 5c9efdfbb0..a32ebb6cb8 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. This value only needs to cover the setup you do between `enable-pause-point` and calling `await-pause-point` (for example seeding state with `execute-dynamic-code` first) — the marker still expires if setup runs longer than that, since a marker cannot be resurrected once expired. It no longer needs to also cover the wait itself: `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so you do not need to add the wait's duration on top when sizing `enable-pause-point --timeout-seconds`. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. Size it to cover only the setup steps you run between `enable-pause-point` and the `await-pause-point` call (for example seeding state with `execute-dynamic-code`): once `await-pause-point` starts waiting, it extends the marker's remaining lifetime to at least its own `--timeout-seconds`, so you no longer need to also budget for the wait itself. The extension cannot revive a marker that has already expired, so the enable timeout still has to outlive the setup steps themselves. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -223,7 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. -- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { ; Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs, guarding on the event you actually care about rather than repeating the postcondition it produces; the `Debug.Assert` line right after the mutation is a natural, low-friction place to put the pause point because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, move the moment you want to observe into a conditional block: `if () { ; Debug.Assert(); }`, then target the pause point at the `Debug.Assert` line. The `if` creates a line that executes only when the event actually happens, so the pause point no longer fires on the very next frame; the `Debug.Assert` states the mutation's postcondition, so the line you pause on is meaningful production code ("this must hold here") rather than an arbitrary probe, and it can stay in the codebase after the investigation ends. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 5c9efdfbb0..a32ebb6cb8 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -15,7 +15,7 @@ Use this small loop for one representative frame you care about. No source edit uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 ``` -`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. This value only needs to cover the setup you do between `enable-pause-point` and calling `await-pause-point` (for example seeding state with `execute-dynamic-code` first) — the marker still expires if setup runs longer than that, since a marker cannot be resurrected once expired. It no longer needs to also cover the wait itself: `await-pause-point` extends the marker's remaining lifetime to at least its own `--timeout-seconds` right before it starts waiting, so you do not need to add the wait's duration on top when sizing `enable-pause-point --timeout-seconds`. +`--timeout-seconds` on enable starts the marker lifetime clock at enable time, not when you later run `await-pause-point`. Size it to cover only the setup steps you run between `enable-pause-point` and the `await-pause-point` call (for example seeding state with `execute-dynamic-code`): once `await-pause-point` starts waiting, it extends the marker's remaining lifetime to at least its own `--timeout-seconds`, so you no longer need to also budget for the wait itself. The extension cannot revive a marker that has already expired, so the enable timeout still has to outlive the setup steps themselves. The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit: a rewritten file shifts line numbers, so do not assume a previously-derived line number still points at the same statement. Use the returned `Id` for every follow-up command. @@ -223,7 +223,7 @@ A pause point hit leaves Unity in this same paused state, so `Step` also works r - Prefer natural runtime points after input has been consumed, such as after a command is accepted, a state value changes, an evaluation step resolves, or a dependent component is updated. - For frame-specific bugs, target the suspicious state branch or the line right after the mutation you need to freeze (the snapshot is taken before the target line runs). - A line that runs unconditionally every frame hits on the very next frame, before the input or event you actually wanted to observe arrives. If you need to catch a specific moment, choose a line that only executes conditionally (inside an `if` guarding the event you care about) so the pause point does not fire prematurely. The opposite applies to `continuous` mode paired with a watch expression: the watch only re-evaluates on a paused frame where the marker's line executes, so a conditional line that stops being reached leaves the watch value frozen (see Watch Expressions) — pick a line reached every frame when you need continuous per-Step updates. -- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, wrap a temporary `if () { ; Debug.Assert(); }` around the moment you want to observe and target the pause point at the `Debug.Assert` line. The `if` is what creates the conditional line the pause point needs, guarding on the event you actually care about rather than repeating the postcondition it produces; the `Debug.Assert` line right after the mutation is a natural, low-friction place to put the pause point because this codebase already uses `Debug.Assert` to state postconditions, so the temporary line reads as intent ("this must hold here") rather than as an arbitrary probe. +- When every reachable line around the state change you want runs unconditionally every frame, with no existing `if` to hang the pause point on, move the moment you want to observe into a conditional block: `if () { ; Debug.Assert(); }`, then target the pause point at the `Debug.Assert` line. The `if` creates a line that executes only when the event actually happens, so the pause point no longer fires on the very next frame; the `Debug.Assert` states the mutation's postcondition, so the line you pause on is meaningful production code ("this must hold here") rather than an arbitrary probe, and it can stay in the codebase after the investigation ends. - An empty-body loop such as `while (TryMove(0, 1)) { }` has no statement inside the braces, so a pause point on the line right after the loop hits at the loop's condition re-check, not once the loop has actually finished advancing. If you need the state after the loop completes, target a line that is guaranteed to run exactly once after the loop exits, not the loop line itself. - Enable pause points after PlayMode is running: entering PlayMode with Domain Reload enabled reloads the domain and silently removes every source pause point (see Requirements & Safety). - Targeting the line that directly handles simulated input is safe: when the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` instead of running to completion, and `simulate-mouse-ui` additionally states in `Message` whether the pointer event was already dispatched before the pause. Prefer a line after the input is consumed when you want the settled result state rather than the input-handling moment. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index 08ed081d93..475754938f 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. This snippet-only reflection use is a one-off diagnostic action against a running session, not a change to production code, so it does not carry the maintainability and encapsulation costs that make reflection unsuitable in shipped code. +Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. ## Parameters From 7a56e4aaab1962865f8c46658c639fd26a10c34a Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 22:13:36 +0900 Subject: [PATCH 4/4] Rewrite four second-round skill passages for agent-facing clarity Advisor-authored pass over the remaining implementer-written additions: - Depth-cap note now names the symptom (type name where contents were expected), shows the this-vs-local contrast with a concrete example, and states that primitives and ToString-overriding types always render - UnityEngine.Object note drops the incorrect ToString() suggestion (the formatter only reads .name) and shows the rename workaround - Self-progressing-game note explains why the race is lost (CLI round trips outlast the game tick) before giving the pattern - Live-state-injection intro replaces the self-contradictory "unrecoverable state" framing with concrete trigger cases Generated copies regenerated via uloop skills install --claude --agents. --- .agents/skills/uloop-execute-dynamic-code/SKILL.md | 2 +- .agents/skills/uloop-pause-point/SKILL.md | 6 +++--- .claude/skills/uloop-execute-dynamic-code/SKILL.md | 2 +- .claude/skills/uloop-pause-point/SKILL.md | 6 +++--- Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 6 +++--- .../FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index 475754938f..cd4005d1a1 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. +Live state injection: when a running PlayMode session is merely in the wrong state — a stuck end-to-end scenario, a camera angle from which a raycast can never hit, a private flag blocking the path you want to test — fix the state instead of the code: write the field directly from this tool (reflection reaches private fields) and steer the session onward. Nothing recompiles and no domain reload happens, so the session's in-memory state (component references, in-progress fixtures, accumulated counters) survives intact, where stopping Play mode to edit code would throw it all away. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. ## Parameters diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index a32ebb6cb8..49fbfd4423 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -58,9 +58,9 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. - `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. `InstanceField` entries come from a reflection walk of the paused instance's declared type, not from the method's IL usage, so a field the method never reads can still appear — and `MaxCapturedVariableCount` still caps the total entry count across all scopes, so a field-heavy type can push some instance fields out of the snapshot. If a specific field you want is missing, read it directly from the live instance instead of waiting on the capped snapshot: while still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live `this` reference, so `execute-dynamic-code` can read any field or property off it regardless of the cap. - The snapshot also includes a synthetic `this` entry (Scope `This`) for the paused instance itself, so you can tell which instance or GameObject was hit via its `UnityObjectPath` and `UnityObjectInstanceId`. For an async or coroutine method it resolves to the original outer instance, not the compiler-generated state machine, and static methods emit no `this` entry. While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live instance reference (for example so a watch expression can read `transform.position`). -- Reaching a value through nested fields off `this` (or off any captured object) bottoms out at type-name-only text past `MaxCollectionPreviewDepth` (2 levels), and this depth budget is independent per captured variable — going deep through `this` does not borrow depth from other variables in the same snapshot, nor vice versa. If the value you actually need is more than two levels deep, pick a pause point line where that value is itself a direct local or parameter instead of a nested field, so it gets its own top-level captured entry with its own full depth budget. +- Nested previews stop at `MaxCollectionPreviewDepth` (2 levels) below each captured variable: past that, an object or collection renders as type-name-only text instead of expanding — a type name where you expected contents means you hit this cap, not a bug. The budget is counted per captured variable, so reaching a value through `this` costs one extra level compared to reading it as a direct local: `this.CurrentPiece.Origin` bottoms out as a type name, while a `dropped` local holding the same piece expands to `{Kind, RotationState, Origin: {X, Y}}`. When the value you need sits too deep, pick a pause point line where it is a direct local or parameter — as its own top-level entry it starts with a fresh full budget. Primitive leaves (numbers, strings, booleans, and any type that overrides `ToString()`) always render regardless of depth; only nested objects and collections get cut off. - `UnityEngine.Object` values additionally carry `UnityObjectKind` (`SceneObject`, `PrefabAsset`, `Asset`, `RuntimeInstance`, or `Destroyed`), `UnityObjectPath`, and `UnityObjectInstanceId`. These three fields appear only for Unity object values; a non-Unity-object variable (an `int`, a `string`, a plain class) omits all three from the JSON entirely instead of sending them as empty/zero. Check whether `UnityObjectKind` is present to tell the two cases apart. Use the fields as handles for the next dig: a `SceneObject` path feeds `get-hierarchy`/`find-game-objects`, an asset path locates the asset, and the InstanceID works with `execute-dynamic-code`. -- A captured `UnityEngine.Object` value's `Value` string only ever carries identifying information (its `name` and similar identifiers), never its custom fields — a `MonoBehaviour`'s own `[SerializeField]` values do not appear in that entry. When you need a custom field's value, either expose it through the component's own `.name`/`ToString()` so it appears in the identifier, or read it directly while still paused with `execute-dynamic-code` off the live instance (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). +- A captured `UnityEngine.Object` value's `Value` string is only the object's `name` — its fields never appear there, and its `ToString()` is not consulted either. A `MonoBehaviour` parameter therefore reads as something like `Block(Clone)`, indistinguishable from every other clone, with none of its `[SerializeField]` values visible. To tell instances apart in snapshots, assign distinguishing names when you create them (for example `gameObject.name = $"Block_{blockId}"`). To read a specific field, stay paused and read it off the live instance with `execute-dynamic-code` (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). - `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap. - A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value. - async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. @@ -101,7 +101,7 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity. -For a self-progressing game (a board that keeps advancing, an opponent that keeps moving), arranging a specific scenario through real input alone is a race against the game clock. While paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the board/state you need, reserving actual simulated key/mouse input for only the one move you are actually verifying. This keeps the setup deterministic while still exercising the real input path for the observed action itself. +For a self-progressing game (a board that advances on a timer, an opponent that keeps moving), arranging a specific scenario through real input alone is a race you will usually lose: each `simulate-*` call is a separate CLI round trip, and the gap between two calls is often longer than the game's own tick. Instead, while paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the exact state you need, then send real simulated input for only the one action you are verifying. The setup becomes deterministic while the observed action still exercises the real input path. ## Watch Expressions diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index 475754938f..cd4005d1a1 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. +Live state injection: when a running PlayMode session is merely in the wrong state — a stuck end-to-end scenario, a camera angle from which a raycast can never hit, a private flag blocking the path you want to test — fix the state instead of the code: write the field directly from this tool (reflection reaches private fields) and steer the session onward. Nothing recompiles and no domain reload happens, so the session's in-memory state (component references, in-progress fixtures, accumulated counters) survives intact, where stopping Play mode to edit code would throw it all away. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. ## Parameters diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index a32ebb6cb8..49fbfd4423 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -58,9 +58,9 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. - `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. `InstanceField` entries come from a reflection walk of the paused instance's declared type, not from the method's IL usage, so a field the method never reads can still appear — and `MaxCapturedVariableCount` still caps the total entry count across all scopes, so a field-heavy type can push some instance fields out of the snapshot. If a specific field you want is missing, read it directly from the live instance instead of waiting on the capped snapshot: while still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live `this` reference, so `execute-dynamic-code` can read any field or property off it regardless of the cap. - The snapshot also includes a synthetic `this` entry (Scope `This`) for the paused instance itself, so you can tell which instance or GameObject was hit via its `UnityObjectPath` and `UnityObjectInstanceId`. For an async or coroutine method it resolves to the original outer instance, not the compiler-generated state machine, and static methods emit no `this` entry. While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live instance reference (for example so a watch expression can read `transform.position`). -- Reaching a value through nested fields off `this` (or off any captured object) bottoms out at type-name-only text past `MaxCollectionPreviewDepth` (2 levels), and this depth budget is independent per captured variable — going deep through `this` does not borrow depth from other variables in the same snapshot, nor vice versa. If the value you actually need is more than two levels deep, pick a pause point line where that value is itself a direct local or parameter instead of a nested field, so it gets its own top-level captured entry with its own full depth budget. +- Nested previews stop at `MaxCollectionPreviewDepth` (2 levels) below each captured variable: past that, an object or collection renders as type-name-only text instead of expanding — a type name where you expected contents means you hit this cap, not a bug. The budget is counted per captured variable, so reaching a value through `this` costs one extra level compared to reading it as a direct local: `this.CurrentPiece.Origin` bottoms out as a type name, while a `dropped` local holding the same piece expands to `{Kind, RotationState, Origin: {X, Y}}`. When the value you need sits too deep, pick a pause point line where it is a direct local or parameter — as its own top-level entry it starts with a fresh full budget. Primitive leaves (numbers, strings, booleans, and any type that overrides `ToString()`) always render regardless of depth; only nested objects and collections get cut off. - `UnityEngine.Object` values additionally carry `UnityObjectKind` (`SceneObject`, `PrefabAsset`, `Asset`, `RuntimeInstance`, or `Destroyed`), `UnityObjectPath`, and `UnityObjectInstanceId`. These three fields appear only for Unity object values; a non-Unity-object variable (an `int`, a `string`, a plain class) omits all three from the JSON entirely instead of sending them as empty/zero. Check whether `UnityObjectKind` is present to tell the two cases apart. Use the fields as handles for the next dig: a `SceneObject` path feeds `get-hierarchy`/`find-game-objects`, an asset path locates the asset, and the InstanceID works with `execute-dynamic-code`. -- A captured `UnityEngine.Object` value's `Value` string only ever carries identifying information (its `name` and similar identifiers), never its custom fields — a `MonoBehaviour`'s own `[SerializeField]` values do not appear in that entry. When you need a custom field's value, either expose it through the component's own `.name`/`ToString()` so it appears in the identifier, or read it directly while still paused with `execute-dynamic-code` off the live instance (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). +- A captured `UnityEngine.Object` value's `Value` string is only the object's `name` — its fields never appear there, and its `ToString()` is not consulted either. A `MonoBehaviour` parameter therefore reads as something like `Block(Clone)`, indistinguishable from every other clone, with none of its `[SerializeField]` values visible. To tell instances apart in snapshots, assign distinguishing names when you create them (for example `gameObject.name = $"Block_{blockId}"`). To read a specific field, stay paused and read it off the live instance with `execute-dynamic-code` (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). - `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap. - A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value. - async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. @@ -101,7 +101,7 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity. -For a self-progressing game (a board that keeps advancing, an opponent that keeps moving), arranging a specific scenario through real input alone is a race against the game clock. While paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the board/state you need, reserving actual simulated key/mouse input for only the one move you are actually verifying. This keeps the setup deterministic while still exercising the real input path for the observed action itself. +For a self-progressing game (a board that advances on a timer, an opponent that keeps moving), arranging a specific scenario through real input alone is a race you will usually lose: each `simulate-*` call is a separate CLI round trip, and the gap between two calls is often longer than the game's own tick. Instead, while paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the exact state you need, then send real simulated input for only the one action you are verifying. The setup becomes deterministic while the observed action still exercises the real input path. ## Watch Expressions diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index a32ebb6cb8..49fbfd4423 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -58,9 +58,9 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. - `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. `InstanceField` entries come from a reflection walk of the paused instance's declared type, not from the method's IL usage, so a field the method never reads can still appear — and `MaxCapturedVariableCount` still caps the total entry count across all scopes, so a field-heavy type can push some instance fields out of the snapshot. If a specific field you want is missing, read it directly from the live instance instead of waiting on the capped snapshot: while still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live `this` reference, so `execute-dynamic-code` can read any field or property off it regardless of the cap. - The snapshot also includes a synthetic `this` entry (Scope `This`) for the paused instance itself, so you can tell which instance or GameObject was hit via its `UnityObjectPath` and `UnityObjectInstanceId`. For an async or coroutine method it resolves to the original outer instance, not the compiler-generated state machine, and static methods emit no `this` entry. While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("this")` returns the live instance reference (for example so a watch expression can read `transform.position`). -- Reaching a value through nested fields off `this` (or off any captured object) bottoms out at type-name-only text past `MaxCollectionPreviewDepth` (2 levels), and this depth budget is independent per captured variable — going deep through `this` does not borrow depth from other variables in the same snapshot, nor vice versa. If the value you actually need is more than two levels deep, pick a pause point line where that value is itself a direct local or parameter instead of a nested field, so it gets its own top-level captured entry with its own full depth budget. +- Nested previews stop at `MaxCollectionPreviewDepth` (2 levels) below each captured variable: past that, an object or collection renders as type-name-only text instead of expanding — a type name where you expected contents means you hit this cap, not a bug. The budget is counted per captured variable, so reaching a value through `this` costs one extra level compared to reading it as a direct local: `this.CurrentPiece.Origin` bottoms out as a type name, while a `dropped` local holding the same piece expands to `{Kind, RotationState, Origin: {X, Y}}`. When the value you need sits too deep, pick a pause point line where it is a direct local or parameter — as its own top-level entry it starts with a fresh full budget. Primitive leaves (numbers, strings, booleans, and any type that overrides `ToString()`) always render regardless of depth; only nested objects and collections get cut off. - `UnityEngine.Object` values additionally carry `UnityObjectKind` (`SceneObject`, `PrefabAsset`, `Asset`, `RuntimeInstance`, or `Destroyed`), `UnityObjectPath`, and `UnityObjectInstanceId`. These three fields appear only for Unity object values; a non-Unity-object variable (an `int`, a `string`, a plain class) omits all three from the JSON entirely instead of sending them as empty/zero. Check whether `UnityObjectKind` is present to tell the two cases apart. Use the fields as handles for the next dig: a `SceneObject` path feeds `get-hierarchy`/`find-game-objects`, an asset path locates the asset, and the InstanceID works with `execute-dynamic-code`. -- A captured `UnityEngine.Object` value's `Value` string only ever carries identifying information (its `name` and similar identifiers), never its custom fields — a `MonoBehaviour`'s own `[SerializeField]` values do not appear in that entry. When you need a custom field's value, either expose it through the component's own `.name`/`ToString()` so it appears in the identifier, or read it directly while still paused with `execute-dynamic-code` off the live instance (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). +- A captured `UnityEngine.Object` value's `Value` string is only the object's `name` — its fields never appear there, and its `ToString()` is not consulted either. A `MonoBehaviour` parameter therefore reads as something like `Block(Clone)`, indistinguishable from every other clone, with none of its `[SerializeField]` values visible. To tell instances apart in snapshots, assign distinguishing names when you create them (for example `gameObject.name = $"Block_{blockId}"`). To read a specific field, stay paused and read it off the live instance with `execute-dynamic-code` (via `UnityObjectPath`/`UnityObjectInstanceId`, or `UloopPausePoint.TryGetCapturedValue("this")` for the paused instance itself). - `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap. - A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value. - async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. @@ -101,7 +101,7 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity. -For a self-progressing game (a board that keeps advancing, an opponent that keeps moving), arranging a specific scenario through real input alone is a race against the game clock. While paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the board/state you need, reserving actual simulated key/mouse input for only the one move you are actually verifying. This keeps the setup deterministic while still exercising the real input path for the observed action itself. +For a self-progressing game (a board that advances on a timer, an opponent that keeps moving), arranging a specific scenario through real input alone is a race you will usually lose: each `simulate-*` call is a separate CLI round trip, and the gap between two calls is often longer than the game's own tick. Instead, while paused on a hit, use `TryGetCapturedValue("this")` to get the live instance and call its production methods directly to build up the exact state you need, then send real simulated input for only the one action you are verifying. The setup becomes deterministic while the observed action still exercises the real input path. ## Watch Expressions diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index 475754938f..cd4005d1a1 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -15,7 +15,7 @@ This tool can inspect reachable Unity state, such as GameObjects, components, pu The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill. -Live state injection: when a running PlayMode session ends up in an unrecoverable state (for example, a stuck end-to-end test scenario), use this tool to write directly to private fields via reflection and recover or steer the running state, instead of stopping Play mode, editing code, and recompiling. This needs no code change, no recompile, and no re-entering Play mode — the session's in-memory state (component references, in-progress test fixtures, accumulated counters) survives. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. +Live state injection: when a running PlayMode session is merely in the wrong state — a stuck end-to-end scenario, a camera angle from which a raycast can never hit, a private flag blocking the path you want to test — fix the state instead of the code: write the field directly from this tool (reflection reaches private fields) and steer the session onward. Nothing recompiles and no domain reload happens, so the session's in-memory state (component references, in-progress fixtures, accumulated counters) survives intact, where stopping Play mode to edit code would throw it all away. Because the snippet is a one-off diagnostic that never lands in the project's source files, using reflection here does not spread reflection through production code — a useful property even in projects whose coding rules restrict reflection. ## Parameters