From e53ec9b093ac29aeb8b6367b26e6105bc5642cf4 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:01:46 +0900 Subject: [PATCH 1/7] docs: Document annotate-raycast-grid and raycast-layer-mask in screenshot skill The flags exist in the tool schema and the annotated-elements reference assumes them, but the skill's Parameters table never mentioned them, so an agent reading only SKILL.md could not discover the 3D collider annotation flow. Also align the elements-only requirement text with the schema (annotate-elements OR annotate-raycast-grid). --- .agents/skills/uloop-screenshot/SKILL.md | 11 +++++++---- .claude/skills/uloop-screenshot/SKILL.md | 11 +++++++---- .../Editor/FirstPartyTools/Screenshot/Skill/SKILL.md | 11 +++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.agents/skills/uloop-screenshot/SKILL.md b/.agents/skills/uloop-screenshot/SKILL.md index 81b2ffe464..d7f92513e2 100644 --- a/.agents/skills/uloop-screenshot/SKILL.md +++ b/.agents/skills/uloop-screenshot/SKILL.md @@ -11,7 +11,7 @@ Take a screenshot of any Unity EditorWindow by name and save as PNG. ## Usage ```bash -uloop screenshot [--window-name ] [--resolution-scale ] [--match-mode ] [--capture-mode ] [--annotate-elements] [--elements-only] [--output-directory ] +uloop screenshot [--window-name ] [--resolution-scale ] [--match-mode ] [--capture-mode ] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask ] [--elements-only] [--output-directory ] ``` ## Parameters @@ -24,7 +24,9 @@ uloop screenshot [--window-name ] [--resolution-scale ] [--match-mo | `--capture-mode` | enum | `window` | `window`=capture EditorWindow including toolbar, `rendering`=capture game rendering only (PlayMode required, coordinates match simulate-mouse) | | `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. | | `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). Only works with `--capture-mode rendering` in PlayMode. | -| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` and `--capture-mode rendering` in PlayMode. | +| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works with `--capture-mode rendering` in PlayMode. | +| `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. | +| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and `--capture-mode rendering` in PlayMode. | ## Match Modes @@ -58,9 +60,10 @@ Returns JSON with: - `ResolutionScale`: Resolution scale used for capture - `ImageToInputOffsetY`: Y offset used for top-left-game-view coordinate conversion - `ScreenshotToInputFormula`: Formula converting raw image pixels to simulate-mouse input coordinates - - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` or `--annotate-raycast-grid` is used. + - `RaycastLayerSummaries` / `RaycastLayerNamesChecked`: Physics-layer diagnostics populated when `--annotate-raycast-grid` is used. -For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. +For `AnnotatedElements` / `RaycastLayerSummaries` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/.claude/skills/uloop-screenshot/SKILL.md b/.claude/skills/uloop-screenshot/SKILL.md index 81b2ffe464..d7f92513e2 100644 --- a/.claude/skills/uloop-screenshot/SKILL.md +++ b/.claude/skills/uloop-screenshot/SKILL.md @@ -11,7 +11,7 @@ Take a screenshot of any Unity EditorWindow by name and save as PNG. ## Usage ```bash -uloop screenshot [--window-name ] [--resolution-scale ] [--match-mode ] [--capture-mode ] [--annotate-elements] [--elements-only] [--output-directory ] +uloop screenshot [--window-name ] [--resolution-scale ] [--match-mode ] [--capture-mode ] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask ] [--elements-only] [--output-directory ] ``` ## Parameters @@ -24,7 +24,9 @@ uloop screenshot [--window-name ] [--resolution-scale ] [--match-mo | `--capture-mode` | enum | `window` | `window`=capture EditorWindow including toolbar, `rendering`=capture game rendering only (PlayMode required, coordinates match simulate-mouse) | | `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. | | `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). Only works with `--capture-mode rendering` in PlayMode. | -| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` and `--capture-mode rendering` in PlayMode. | +| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works with `--capture-mode rendering` in PlayMode. | +| `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. | +| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and `--capture-mode rendering` in PlayMode. | ## Match Modes @@ -58,9 +60,10 @@ Returns JSON with: - `ResolutionScale`: Resolution scale used for capture - `ImageToInputOffsetY`: Y offset used for top-left-game-view coordinate conversion - `ScreenshotToInputFormula`: Formula converting raw image pixels to simulate-mouse input coordinates - - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` or `--annotate-raycast-grid` is used. + - `RaycastLayerSummaries` / `RaycastLayerNamesChecked`: Physics-layer diagnostics populated when `--annotate-raycast-grid` is used. -For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. +For `AnnotatedElements` / `RaycastLayerSummaries` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md index 81b2ffe464..d7f92513e2 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md @@ -11,7 +11,7 @@ Take a screenshot of any Unity EditorWindow by name and save as PNG. ## Usage ```bash -uloop screenshot [--window-name ] [--resolution-scale ] [--match-mode ] [--capture-mode ] [--annotate-elements] [--elements-only] [--output-directory ] +uloop screenshot [--window-name ] [--resolution-scale ] [--match-mode ] [--capture-mode ] [--annotate-elements] [--annotate-raycast-grid] [--raycast-layer-mask ] [--elements-only] [--output-directory ] ``` ## Parameters @@ -24,7 +24,9 @@ uloop screenshot [--window-name ] [--resolution-scale ] [--match-mo | `--capture-mode` | enum | `window` | `window`=capture EditorWindow including toolbar, `rendering`=capture game rendering only (PlayMode required, coordinates match simulate-mouse) | | `--output-directory` | string | `""` | Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths. | | `--annotate-elements` | flag | - | Annotate interactive UI elements with index labels and interaction hints (A / CLICK, B / DRAG, ...). Only works with `--capture-mode rendering` in PlayMode. | -| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` and `--capture-mode rendering` in PlayMode. | +| `--annotate-raycast-grid` | flag | - | Annotate clustered 3D physics collider candidates as `PhysicsCollider` entries in `AnnotatedElements`. Uses `Camera.main` visibility and the same top-left Game View coordinates as `simulate-mouse-input`. Only works with `--capture-mode rendering` in PlayMode. | +| `--raycast-layer-mask` | string | `""` | Comma-separated physics layer names to narrow which layers `--annotate-raycast-grid` clusters. Hits are limited to layers also visible to `Camera.main.cullingMask`. When omitted, clusters against `Physics.DefaultRaycastLayers`. | +| `--elements-only` | flag | - | Return only annotated element JSON without capturing a screenshot image. Requires `--annotate-elements` or `--annotate-raycast-grid`, and `--capture-mode rendering` in PlayMode. | ## Match Modes @@ -58,9 +60,10 @@ Returns JSON with: - `ResolutionScale`: Resolution scale used for capture - `ImageToInputOffsetY`: Y offset used for top-left-game-view coordinate conversion - `ScreenshotToInputFormula`: Formula converting raw image pixels to simulate-mouse input coordinates - - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` is used. + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` or `--annotate-raycast-grid` is used. + - `RaycastLayerSummaries` / `RaycastLayerNamesChecked`: Physics-layer diagnostics populated when `--annotate-raycast-grid` is used. -For `AnnotatedElements` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. +For `AnnotatedElements` / `RaycastLayerSummaries` fields and gameView coordinate conversion, read [references/annotated-elements.md](references/annotated-elements.md) before using screenshot coordinates with mouse simulation tools. When multiple windows match (e.g., multiple Inspector windows or when using `contains` mode), all matching windows are captured with numbered filenames (e.g., `Inspector_1_*.png`, `Inspector_2_*.png`). From 869b2987039af866e98ec67ba61928760c07de38 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:02:02 +0900 Subject: [PATCH 2/7] docs: Split pause-point skill into a lean core plus reference files SKILL.md had grown to ~33KB of dense prose, all loaded on every skill invocation. Move the deep-dive material to on-demand references while keeping the core workflow, line placement, and safety rules inline: - references/captured-variables.md: snapshot timing, scopes, preview caps, Unity object values, evidence-source trade-offs, raw capture API - references/watch-expressions.md: evaluation rules and lifetime - references/condition-triggered-pause.md: dynamic-code watcher pattern Core SKILL.md keeps compact summaries with explicit read-this-when pointers, shrinking it to ~19KB with no information removed. --- .agents/skills/uloop-pause-point/SKILL.md | 102 ++---------------- .../references/captured-variables.md | 66 ++++++++++++ .../references/condition-triggered-pause.md | 47 ++++++++ .../references/watch-expressions.md | 20 ++++ .claude/skills/uloop-pause-point/SKILL.md | 102 ++---------------- .../references/captured-variables.md | 66 ++++++++++++ .../references/condition-triggered-pause.md | 47 ++++++++ .../references/watch-expressions.md | 20 ++++ .../CliOnlyTools~/PausePoint/Skill/SKILL.md | 102 ++---------------- .../Skill/references/captured-variables.md | 66 ++++++++++++ .../references/condition-triggered-pause.md | 47 ++++++++ .../Skill/references/watch-expressions.md | 20 ++++ 12 files changed, 423 insertions(+), 282 deletions(-) create mode 100644 .agents/skills/uloop-pause-point/references/captured-variables.md create mode 100644 .agents/skills/uloop-pause-point/references/condition-triggered-pause.md create mode 100644 .agents/skills/uloop-pause-point/references/watch-expressions.md create mode 100644 .claude/skills/uloop-pause-point/references/captured-variables.md create mode 100644 .claude/skills/uloop-pause-point/references/condition-triggered-pause.md create mode 100644 .claude/skills/uloop-pause-point/references/watch-expressions.md create mode 100644 Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/captured-variables.md create mode 100644 Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/condition-triggered-pause.md create mode 100644 Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/watch-expressions.md diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 49fbfd4423..92f43c60db 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -54,71 +54,22 @@ Repeat the Step/status pair to inspect the history tail. A new frame is captured Every hit response embeds `CapturedVariables`: the method's in-scope locals, its parameters, and the `this` instance fields, captured at the exact moment execution reached the patched line. Values are point-in-time strings, not live references, so they stay valid as evidence even after Unity resumes. - The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line. -- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame. -- `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`). -- 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 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. -- If the patched method ran off the main thread, values degrade to type names with a `(captured off main thread)` note; the hit itself is still recorded. +- `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. The synthetic `this` entry identifies which instance or GameObject was hit via `UnityObjectPath` and `UnityObjectInstanceId`; `UnityEngine.Object` values carry the same handle fields for follow-up digs with `get-hierarchy`, `find-game-objects`, or `execute-dynamic-code`. +- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops every `Value` and keeps `Name`/`Scope`/`TypeName` — use it first on field-heavy classes, then fetch full values with a plain `pause-point-status` call. +- While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("name")` (and `"this"`) returns live captured references for `execute-dynamic-code`; the holder clears on resume. -`await-pause-point`'s hit response also carries a top-level `Warning` (omitted when empty): it flags multiple hits, multiple matching logs, or truncated matching logs, so you can tell a single clean hit apart from evidence that needs closer inspection. `MatchingLogs` (log entries whose text contains the marker id) is still embedded, but source-derived ids rarely appear in log text, so treat `CapturedVariables` as the primary variable evidence. - -Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. - -### Pulling More Than the Default Response Carries - -The hit and status responses are push-first and kept lean by default: no field is ever a re-summary of another field, and a variable's `Value` is the only per-entry cost. For a class with dozens of `[SerializeField]` fields, a `continuous` marker's history still multiplies entry count by `MaxHistory` (default 20), which can be a lot of `Value` strings to carry around when you only need to know which names were captured. - -Pull only what you need instead of paying for it all up front: - -- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops `Value` from every captured variable (including every history frame) and keeps `Name`/`Scope`/`TypeName`. Use it first on a field-heavy class, then fetch specific values afterward. -- `uloop pause-point-status --id ` returns the full response again, including every `Value`, whenever you need it — call it plain (no `--captured-variables`) for the complete picture after a lightweight `names` scan. - -### Choosing the Right Evidence Source - -Three different sources answer three different questions about a captured variable; pick by what you actually need: - -| Need | Source | Notes | -|---|---|---| -| A value type's value at capture time | `UloopPausePoint.TryGetCapturedValue("name")` | Faithful: value types are a boxed copy taken at capture time, so this never drifts. | -| A reference type's *live* current state | `UloopPausePoint.TryGetCapturedValue("name")` | The reference itself is live, so the object it points to may have changed since capture (or been destroyed/resumed away). Only available while Unity is still paused. | -| A reference type's state *as it was at capture time* | `uloop pause-point-status --id ` | The only faithful source for this: the response is a formatted string snapshot taken at capture time and stored in the registry, so it never drifts and stays retrievable after resume until the next clear or domain reload. | - -Capturing a deep copy at hit time was deliberately not adopted: it would cost hot-path performance and risk getter side effects, so the formatted-string snapshot (`pause-point-status`) remains the only way to get capture-time-faithful evidence for reference types. - -## Raw Capture While Paused - -While Unity is paused on a hit, `execute-dynamic-code` can read live captured references through `UloopPausePoint`: - -- `TryGetCapturedValue(string name)` returns `(bool Found, object Value)` for the latest hit only. When multiple captured variables share the same name, the last one wins. -- `GetCapturedNames()` lists captured variable names from that snapshot. -- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot. - -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 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. +Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). ## Watch Expressions -Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: +Use watch expressions when a value should be re-evaluated automatically after each paused Play Mode Step: ```bash uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20 uloop get-watch-values --id "speed" ``` -`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id ` before re-registering a changed expression. `clear-watch --id ` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch. - -Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same. - -The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit. - -Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`. +A watch evaluates only on a changed, paused frame, and a domain reload clears all watches. For the full evaluation rules (baseline, ordering, duplicate ids, `ValueFrozenHint`, error handling), read [references/watch-expressions.md](references/watch-expressions.md). ## Marker Types @@ -129,46 +80,9 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re ## Catching a Runtime Condition with a Dynamic-Code Trigger -A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file. - -1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). -2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`. -3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. -4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. -5. Resume with `uloop control-play-mode --action Play`. - -Example watcher (freeze when the Hit animation passes 30% of the motion): - -```csharp -using UnityEngine; -using UnityEditor; -using io.github.hatayama.UnityCliLoop.Runtime; -Animator animator = GameObject.Find("Zombie").GetComponent(); -// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate -double deadline = EditorApplication.timeSinceStartup + 120d; -EditorApplication.CallbackFunction watcher = null; -watcher = () => -{ - if (EditorApplication.timeSinceStartup > deadline) - { - EditorApplication.update -= watcher; - return; - } - AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); - if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; - EditorApplication.update -= watcher; - UloopPausePoint.Pause("hit-peak"); -}; -EditorApplication.update += watcher; -return "watcher registered"; -``` - -Rules for this pattern: +A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — enable an id-only marker (`uloop enable-pause-point --id `, no `--file`/`--line`), then use `execute-dynamic-code` to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause("")` on the first frame the condition holds, and wait with `uloop await-pause-point --id ` on the CLI side. This freezes the first frame where the condition holds, without writing any .cs file. -- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`. -- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. -- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly. -- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume. +Before using this pattern, read [references/condition-triggered-pause.md](references/condition-triggered-pause.md) for the full workflow, a complete watcher example, and the safety rules (never sleep in the snippet, watcher self-unsubscription, deadline handling). ## Pausing Right After Simulated Input, Plus N Frames diff --git a/.agents/skills/uloop-pause-point/references/captured-variables.md b/.agents/skills/uloop-pause-point/references/captured-variables.md new file mode 100644 index 0000000000..2c5ee21b1f --- /dev/null +++ b/.agents/skills/uloop-pause-point/references/captured-variables.md @@ -0,0 +1,66 @@ +# CapturedVariables Semantics + +Read this before interpreting unexpected, missing, or truncated captured values, nested previews, `continuous`-mode history, or when you need live references while Unity is still paused. + +## Snapshot Timing + +- The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line. +- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame. +- `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. + +## Scopes and the `this` Entry + +- `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`). +- async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. +- If the patched method ran off the main thread, values degrade to type names with a `(captured off main thread)` note; the hit itself is still recorded. + +## Value Rendering, Previews, and Caps + +- 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. +- 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. +- `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. + +## Unity Object Values + +- `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 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). + +## Pulling More Than the Default Response Carries + +The hit and status responses are push-first and kept lean by default: no field is ever a re-summary of another field, and a variable's `Value` is the only per-entry cost. For a class with dozens of `[SerializeField]` fields, a `continuous` marker's history still multiplies entry count by `MaxHistory` (default 20), which can be a lot of `Value` strings to carry around when you only need to know which names were captured. + +Pull only what you need instead of paying for it all up front: + +- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops `Value` from every captured variable (including every history frame) and keeps `Name`/`Scope`/`TypeName`. Use it first on a field-heavy class, then fetch specific values afterward. +- `uloop pause-point-status --id ` returns the full response again, including every `Value`, whenever you need it — call it plain (no `--captured-variables`) for the complete picture after a lightweight `names` scan. + +## Choosing the Right Evidence Source + +Three different sources answer three different questions about a captured variable; pick by what you actually need: + +| Need | Source | Notes | +|---|---|---| +| A value type's value at capture time | `UloopPausePoint.TryGetCapturedValue("name")` | Faithful: value types are a boxed copy taken at capture time, so this never drifts. | +| A reference type's *live* current state | `UloopPausePoint.TryGetCapturedValue("name")` | The reference itself is live, so the object it points to may have changed since capture (or been destroyed/resumed away). Only available while Unity is still paused. | +| A reference type's state *as it was at capture time* | `uloop pause-point-status --id ` | The only faithful source for this: the response is a formatted string snapshot taken at capture time and stored in the registry, so it never drifts and stays retrievable after resume until the next clear or domain reload. | + +Capturing a deep copy at hit time was deliberately not adopted: it would cost hot-path performance and risk getter side effects, so the formatted-string snapshot (`pause-point-status`) remains the only way to get capture-time-faithful evidence for reference types. + +## Raw Capture API While Paused + +While Unity is paused on a hit, `execute-dynamic-code` can read live captured references through `UloopPausePoint`: + +- `TryGetCapturedValue(string name)` returns `(bool Found, object Value)` for the latest hit only. When multiple captured variables share the same name, the last one wins. +- `GetCapturedNames()` lists captured variable names from that snapshot. +- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot. + +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 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. + +## Warnings and Marker Freshness + +`await-pause-point`'s hit response also carries a top-level `Warning` (omitted when empty): it flags multiple hits, multiple matching logs, or truncated matching logs, so you can tell a single clean hit apart from evidence that needs closer inspection. `MatchingLogs` (log entries whose text contains the marker id) is still embedded, but source-derived ids rarely appear in log text, so treat `CapturedVariables` as the primary variable evidence. + +Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. diff --git a/.agents/skills/uloop-pause-point/references/condition-triggered-pause.md b/.agents/skills/uloop-pause-point/references/condition-triggered-pause.md new file mode 100644 index 0000000000..52592df19a --- /dev/null +++ b/.agents/skills/uloop-pause-point/references/condition-triggered-pause.md @@ -0,0 +1,47 @@ +# Catching a Runtime Condition with a Dynamic-Code Trigger + +A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file. + +## Workflow + +1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). +2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`. +3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. +4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. +5. Resume with `uloop control-play-mode --action Play`. + +## Example Watcher + +Freeze when the Hit animation passes 30% of the motion: + +```csharp +using UnityEngine; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.Runtime; +Animator animator = GameObject.Find("Zombie").GetComponent(); +// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate +double deadline = EditorApplication.timeSinceStartup + 120d; +EditorApplication.CallbackFunction watcher = null; +watcher = () => +{ + if (EditorApplication.timeSinceStartup > deadline) + { + EditorApplication.update -= watcher; + return; + } + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); + if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; + EditorApplication.update -= watcher; + UloopPausePoint.Pause("hit-peak"); +}; +EditorApplication.update += watcher; +return "watcher registered"; +``` + +## Rules + +- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. +- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly. +- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume. +- Do not use this pattern for frame-offset positioning ("N frames after the input"): frames keep advancing between CLI commands, so a recorded `Time.frameCount` baseline is race-prone. Use a file:line hit followed by `control-play-mode --action Step` N times instead (see SKILL.md). diff --git a/.agents/skills/uloop-pause-point/references/watch-expressions.md b/.agents/skills/uloop-pause-point/references/watch-expressions.md new file mode 100644 index 0000000000..c03b2cf218 --- /dev/null +++ b/.agents/skills/uloop-pause-point/references/watch-expressions.md @@ -0,0 +1,20 @@ +# Watch Expressions + +Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: + +```bash +uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20 +uloop get-watch-values --id "speed" +``` + +## Evaluation Rules + +`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id ` before re-registering a changed expression. `clear-watch --id ` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch. + +Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement in SKILL.md). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same. + +The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit. + +## Lifetime + +Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`. diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 49fbfd4423..92f43c60db 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -54,71 +54,22 @@ Repeat the Step/status pair to inspect the history tail. A new frame is captured Every hit response embeds `CapturedVariables`: the method's in-scope locals, its parameters, and the `this` instance fields, captured at the exact moment execution reached the patched line. Values are point-in-time strings, not live references, so they stay valid as evidence even after Unity resumes. - The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line. -- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame. -- `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`). -- 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 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. -- If the patched method ran off the main thread, values degrade to type names with a `(captured off main thread)` note; the hit itself is still recorded. +- `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. The synthetic `this` entry identifies which instance or GameObject was hit via `UnityObjectPath` and `UnityObjectInstanceId`; `UnityEngine.Object` values carry the same handle fields for follow-up digs with `get-hierarchy`, `find-game-objects`, or `execute-dynamic-code`. +- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops every `Value` and keeps `Name`/`Scope`/`TypeName` — use it first on field-heavy classes, then fetch full values with a plain `pause-point-status` call. +- While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("name")` (and `"this"`) returns live captured references for `execute-dynamic-code`; the holder clears on resume. -`await-pause-point`'s hit response also carries a top-level `Warning` (omitted when empty): it flags multiple hits, multiple matching logs, or truncated matching logs, so you can tell a single clean hit apart from evidence that needs closer inspection. `MatchingLogs` (log entries whose text contains the marker id) is still embedded, but source-derived ids rarely appear in log text, so treat `CapturedVariables` as the primary variable evidence. - -Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. - -### Pulling More Than the Default Response Carries - -The hit and status responses are push-first and kept lean by default: no field is ever a re-summary of another field, and a variable's `Value` is the only per-entry cost. For a class with dozens of `[SerializeField]` fields, a `continuous` marker's history still multiplies entry count by `MaxHistory` (default 20), which can be a lot of `Value` strings to carry around when you only need to know which names were captured. - -Pull only what you need instead of paying for it all up front: - -- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops `Value` from every captured variable (including every history frame) and keeps `Name`/`Scope`/`TypeName`. Use it first on a field-heavy class, then fetch specific values afterward. -- `uloop pause-point-status --id ` returns the full response again, including every `Value`, whenever you need it — call it plain (no `--captured-variables`) for the complete picture after a lightweight `names` scan. - -### Choosing the Right Evidence Source - -Three different sources answer three different questions about a captured variable; pick by what you actually need: - -| Need | Source | Notes | -|---|---|---| -| A value type's value at capture time | `UloopPausePoint.TryGetCapturedValue("name")` | Faithful: value types are a boxed copy taken at capture time, so this never drifts. | -| A reference type's *live* current state | `UloopPausePoint.TryGetCapturedValue("name")` | The reference itself is live, so the object it points to may have changed since capture (or been destroyed/resumed away). Only available while Unity is still paused. | -| A reference type's state *as it was at capture time* | `uloop pause-point-status --id ` | The only faithful source for this: the response is a formatted string snapshot taken at capture time and stored in the registry, so it never drifts and stays retrievable after resume until the next clear or domain reload. | - -Capturing a deep copy at hit time was deliberately not adopted: it would cost hot-path performance and risk getter side effects, so the formatted-string snapshot (`pause-point-status`) remains the only way to get capture-time-faithful evidence for reference types. - -## Raw Capture While Paused - -While Unity is paused on a hit, `execute-dynamic-code` can read live captured references through `UloopPausePoint`: - -- `TryGetCapturedValue(string name)` returns `(bool Found, object Value)` for the latest hit only. When multiple captured variables share the same name, the last one wins. -- `GetCapturedNames()` lists captured variable names from that snapshot. -- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot. - -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 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. +Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). ## Watch Expressions -Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: +Use watch expressions when a value should be re-evaluated automatically after each paused Play Mode Step: ```bash uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20 uloop get-watch-values --id "speed" ``` -`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id ` before re-registering a changed expression. `clear-watch --id ` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch. - -Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same. - -The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit. - -Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`. +A watch evaluates only on a changed, paused frame, and a domain reload clears all watches. For the full evaluation rules (baseline, ordering, duplicate ids, `ValueFrozenHint`, error handling), read [references/watch-expressions.md](references/watch-expressions.md). ## Marker Types @@ -129,46 +80,9 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re ## Catching a Runtime Condition with a Dynamic-Code Trigger -A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file. - -1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). -2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`. -3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. -4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. -5. Resume with `uloop control-play-mode --action Play`. - -Example watcher (freeze when the Hit animation passes 30% of the motion): - -```csharp -using UnityEngine; -using UnityEditor; -using io.github.hatayama.UnityCliLoop.Runtime; -Animator animator = GameObject.Find("Zombie").GetComponent(); -// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate -double deadline = EditorApplication.timeSinceStartup + 120d; -EditorApplication.CallbackFunction watcher = null; -watcher = () => -{ - if (EditorApplication.timeSinceStartup > deadline) - { - EditorApplication.update -= watcher; - return; - } - AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); - if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; - EditorApplication.update -= watcher; - UloopPausePoint.Pause("hit-peak"); -}; -EditorApplication.update += watcher; -return "watcher registered"; -``` - -Rules for this pattern: +A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — enable an id-only marker (`uloop enable-pause-point --id `, no `--file`/`--line`), then use `execute-dynamic-code` to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause("")` on the first frame the condition holds, and wait with `uloop await-pause-point --id ` on the CLI side. This freezes the first frame where the condition holds, without writing any .cs file. -- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`. -- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. -- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly. -- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume. +Before using this pattern, read [references/condition-triggered-pause.md](references/condition-triggered-pause.md) for the full workflow, a complete watcher example, and the safety rules (never sleep in the snippet, watcher self-unsubscription, deadline handling). ## Pausing Right After Simulated Input, Plus N Frames diff --git a/.claude/skills/uloop-pause-point/references/captured-variables.md b/.claude/skills/uloop-pause-point/references/captured-variables.md new file mode 100644 index 0000000000..2c5ee21b1f --- /dev/null +++ b/.claude/skills/uloop-pause-point/references/captured-variables.md @@ -0,0 +1,66 @@ +# CapturedVariables Semantics + +Read this before interpreting unexpected, missing, or truncated captured values, nested previews, `continuous`-mode history, or when you need live references while Unity is still paused. + +## Snapshot Timing + +- The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line. +- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame. +- `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. + +## Scopes and the `this` Entry + +- `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`). +- async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. +- If the patched method ran off the main thread, values degrade to type names with a `(captured off main thread)` note; the hit itself is still recorded. + +## Value Rendering, Previews, and Caps + +- 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. +- 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. +- `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. + +## Unity Object Values + +- `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 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). + +## Pulling More Than the Default Response Carries + +The hit and status responses are push-first and kept lean by default: no field is ever a re-summary of another field, and a variable's `Value` is the only per-entry cost. For a class with dozens of `[SerializeField]` fields, a `continuous` marker's history still multiplies entry count by `MaxHistory` (default 20), which can be a lot of `Value` strings to carry around when you only need to know which names were captured. + +Pull only what you need instead of paying for it all up front: + +- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops `Value` from every captured variable (including every history frame) and keeps `Name`/`Scope`/`TypeName`. Use it first on a field-heavy class, then fetch specific values afterward. +- `uloop pause-point-status --id ` returns the full response again, including every `Value`, whenever you need it — call it plain (no `--captured-variables`) for the complete picture after a lightweight `names` scan. + +## Choosing the Right Evidence Source + +Three different sources answer three different questions about a captured variable; pick by what you actually need: + +| Need | Source | Notes | +|---|---|---| +| A value type's value at capture time | `UloopPausePoint.TryGetCapturedValue("name")` | Faithful: value types are a boxed copy taken at capture time, so this never drifts. | +| A reference type's *live* current state | `UloopPausePoint.TryGetCapturedValue("name")` | The reference itself is live, so the object it points to may have changed since capture (or been destroyed/resumed away). Only available while Unity is still paused. | +| A reference type's state *as it was at capture time* | `uloop pause-point-status --id ` | The only faithful source for this: the response is a formatted string snapshot taken at capture time and stored in the registry, so it never drifts and stays retrievable after resume until the next clear or domain reload. | + +Capturing a deep copy at hit time was deliberately not adopted: it would cost hot-path performance and risk getter side effects, so the formatted-string snapshot (`pause-point-status`) remains the only way to get capture-time-faithful evidence for reference types. + +## Raw Capture API While Paused + +While Unity is paused on a hit, `execute-dynamic-code` can read live captured references through `UloopPausePoint`: + +- `TryGetCapturedValue(string name)` returns `(bool Found, object Value)` for the latest hit only. When multiple captured variables share the same name, the last one wins. +- `GetCapturedNames()` lists captured variable names from that snapshot. +- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot. + +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 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. + +## Warnings and Marker Freshness + +`await-pause-point`'s hit response also carries a top-level `Warning` (omitted when empty): it flags multiple hits, multiple matching logs, or truncated matching logs, so you can tell a single clean hit apart from evidence that needs closer inspection. `MatchingLogs` (log entries whose text contains the marker id) is still embedded, but source-derived ids rarely appear in log text, so treat `CapturedVariables` as the primary variable evidence. + +Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. diff --git a/.claude/skills/uloop-pause-point/references/condition-triggered-pause.md b/.claude/skills/uloop-pause-point/references/condition-triggered-pause.md new file mode 100644 index 0000000000..52592df19a --- /dev/null +++ b/.claude/skills/uloop-pause-point/references/condition-triggered-pause.md @@ -0,0 +1,47 @@ +# Catching a Runtime Condition with a Dynamic-Code Trigger + +A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file. + +## Workflow + +1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). +2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`. +3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. +4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. +5. Resume with `uloop control-play-mode --action Play`. + +## Example Watcher + +Freeze when the Hit animation passes 30% of the motion: + +```csharp +using UnityEngine; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.Runtime; +Animator animator = GameObject.Find("Zombie").GetComponent(); +// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate +double deadline = EditorApplication.timeSinceStartup + 120d; +EditorApplication.CallbackFunction watcher = null; +watcher = () => +{ + if (EditorApplication.timeSinceStartup > deadline) + { + EditorApplication.update -= watcher; + return; + } + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); + if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; + EditorApplication.update -= watcher; + UloopPausePoint.Pause("hit-peak"); +}; +EditorApplication.update += watcher; +return "watcher registered"; +``` + +## Rules + +- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. +- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly. +- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume. +- Do not use this pattern for frame-offset positioning ("N frames after the input"): frames keep advancing between CLI commands, so a recorded `Time.frameCount` baseline is race-prone. Use a file:line hit followed by `control-play-mode --action Step` N times instead (see SKILL.md). diff --git a/.claude/skills/uloop-pause-point/references/watch-expressions.md b/.claude/skills/uloop-pause-point/references/watch-expressions.md new file mode 100644 index 0000000000..c03b2cf218 --- /dev/null +++ b/.claude/skills/uloop-pause-point/references/watch-expressions.md @@ -0,0 +1,20 @@ +# Watch Expressions + +Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: + +```bash +uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20 +uloop get-watch-values --id "speed" +``` + +## Evaluation Rules + +`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id ` before re-registering a changed expression. `clear-watch --id ` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch. + +Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement in SKILL.md). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same. + +The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit. + +## Lifetime + +Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 49fbfd4423..92f43c60db 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -54,71 +54,22 @@ Repeat the Step/status pair to inspect the history tail. A new frame is captured Every hit response embeds `CapturedVariables`: the method's in-scope locals, its parameters, and the `this` instance fields, captured at the exact moment execution reached the patched line. Values are point-in-time strings, not live references, so they stay valid as evidence even after Unity resumes. - The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line. -- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame. -- `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`). -- 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 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. -- If the patched method ran off the main thread, values degrade to type names with a `(captured off main thread)` note; the hit itself is still recorded. +- `Scope` is `Local`, `Parameter`, `InstanceField`, or `This`. The synthetic `this` entry identifies which instance or GameObject was hit via `UnityObjectPath` and `UnityObjectInstanceId`; `UnityEngine.Object` values carry the same handle fields for follow-up digs with `get-hierarchy`, `find-game-objects`, or `execute-dynamic-code`. +- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops every `Value` and keeps `Name`/`Scope`/`TypeName` — use it first on field-heavy classes, then fetch full values with a plain `pause-point-status` call. +- While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("name")` (and `"this"`) returns live captured references for `execute-dynamic-code`; the holder clears on resume. -`await-pause-point`'s hit response also carries a top-level `Warning` (omitted when empty): it flags multiple hits, multiple matching logs, or truncated matching logs, so you can tell a single clean hit apart from evidence that needs closer inspection. `MatchingLogs` (log entries whose text contains the marker id) is still embedded, but source-derived ids rarely appear in log text, so treat `CapturedVariables` as the primary variable evidence. - -Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. - -### Pulling More Than the Default Response Carries - -The hit and status responses are push-first and kept lean by default: no field is ever a re-summary of another field, and a variable's `Value` is the only per-entry cost. For a class with dozens of `[SerializeField]` fields, a `continuous` marker's history still multiplies entry count by `MaxHistory` (default 20), which can be a lot of `Value` strings to carry around when you only need to know which names were captured. - -Pull only what you need instead of paying for it all up front: - -- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops `Value` from every captured variable (including every history frame) and keeps `Name`/`Scope`/`TypeName`. Use it first on a field-heavy class, then fetch specific values afterward. -- `uloop pause-point-status --id ` returns the full response again, including every `Value`, whenever you need it — call it plain (no `--captured-variables`) for the complete picture after a lightweight `names` scan. - -### Choosing the Right Evidence Source - -Three different sources answer three different questions about a captured variable; pick by what you actually need: - -| Need | Source | Notes | -|---|---|---| -| A value type's value at capture time | `UloopPausePoint.TryGetCapturedValue("name")` | Faithful: value types are a boxed copy taken at capture time, so this never drifts. | -| A reference type's *live* current state | `UloopPausePoint.TryGetCapturedValue("name")` | The reference itself is live, so the object it points to may have changed since capture (or been destroyed/resumed away). Only available while Unity is still paused. | -| A reference type's state *as it was at capture time* | `uloop pause-point-status --id ` | The only faithful source for this: the response is a formatted string snapshot taken at capture time and stored in the registry, so it never drifts and stays retrievable after resume until the next clear or domain reload. | - -Capturing a deep copy at hit time was deliberately not adopted: it would cost hot-path performance and risk getter side effects, so the formatted-string snapshot (`pause-point-status`) remains the only way to get capture-time-faithful evidence for reference types. - -## Raw Capture While Paused - -While Unity is paused on a hit, `execute-dynamic-code` can read live captured references through `UloopPausePoint`: - -- `TryGetCapturedValue(string name)` returns `(bool Found, object Value)` for the latest hit only. When multiple captured variables share the same name, the last one wins. -- `GetCapturedNames()` lists captured variable names from that snapshot. -- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot. - -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 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. +Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). ## Watch Expressions -Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: +Use watch expressions when a value should be re-evaluated automatically after each paused Play Mode Step: ```bash uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20 uloop get-watch-values --id "speed" ``` -`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id ` before re-registering a changed expression. `clear-watch --id ` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch. - -Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same. - -The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit. - -Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`. +A watch evaluates only on a changed, paused frame, and a domain reload clears all watches. For the full evaluation rules (baseline, ordering, duplicate ids, `ValueFrozenHint`, error handling), read [references/watch-expressions.md](references/watch-expressions.md). ## Marker Types @@ -129,46 +80,9 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re ## Catching a Runtime Condition with a Dynamic-Code Trigger -A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file. - -1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). -2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`. -3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. -4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. -5. Resume with `uloop control-play-mode --action Play`. - -Example watcher (freeze when the Hit animation passes 30% of the motion): - -```csharp -using UnityEngine; -using UnityEditor; -using io.github.hatayama.UnityCliLoop.Runtime; -Animator animator = GameObject.Find("Zombie").GetComponent(); -// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate -double deadline = EditorApplication.timeSinceStartup + 120d; -EditorApplication.CallbackFunction watcher = null; -watcher = () => -{ - if (EditorApplication.timeSinceStartup > deadline) - { - EditorApplication.update -= watcher; - return; - } - AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); - if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; - EditorApplication.update -= watcher; - UloopPausePoint.Pause("hit-peak"); -}; -EditorApplication.update += watcher; -return "watcher registered"; -``` - -Rules for this pattern: +A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — enable an id-only marker (`uloop enable-pause-point --id `, no `--file`/`--line`), then use `execute-dynamic-code` to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause("")` on the first frame the condition holds, and wait with `uloop await-pause-point --id ` on the CLI side. This freezes the first frame where the condition holds, without writing any .cs file. -- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`. -- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. -- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly. -- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume. +Before using this pattern, read [references/condition-triggered-pause.md](references/condition-triggered-pause.md) for the full workflow, a complete watcher example, and the safety rules (never sleep in the snippet, watcher self-unsubscription, deadline handling). ## Pausing Right After Simulated Input, Plus N Frames diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/captured-variables.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/captured-variables.md new file mode 100644 index 0000000000..2c5ee21b1f --- /dev/null +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/captured-variables.md @@ -0,0 +1,66 @@ +# CapturedVariables Semantics + +Read this before interpreting unexpected, missing, or truncated captured values, nested previews, `continuous`-mode history, or when you need live references while Unity is still paused. + +## Snapshot Timing + +- The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line. +- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame. +- `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. + +## Scopes and the `this` Entry + +- `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`). +- async and coroutine methods work: hoisted locals and the original `this` fields appear under their normal names. +- If the patched method ran off the main thread, values degrade to type names with a `(captured off main thread)` note; the hit itself is still recorded. + +## Value Rendering, Previews, and Caps + +- 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. +- 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. +- `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. + +## Unity Object Values + +- `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 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). + +## Pulling More Than the Default Response Carries + +The hit and status responses are push-first and kept lean by default: no field is ever a re-summary of another field, and a variable's `Value` is the only per-entry cost. For a class with dozens of `[SerializeField]` fields, a `continuous` marker's history still multiplies entry count by `MaxHistory` (default 20), which can be a lot of `Value` strings to carry around when you only need to know which names were captured. + +Pull only what you need instead of paying for it all up front: + +- `--captured-variables names` on `await-pause-point`/`pause-point-status` drops `Value` from every captured variable (including every history frame) and keeps `Name`/`Scope`/`TypeName`. Use it first on a field-heavy class, then fetch specific values afterward. +- `uloop pause-point-status --id ` returns the full response again, including every `Value`, whenever you need it — call it plain (no `--captured-variables`) for the complete picture after a lightweight `names` scan. + +## Choosing the Right Evidence Source + +Three different sources answer three different questions about a captured variable; pick by what you actually need: + +| Need | Source | Notes | +|---|---|---| +| A value type's value at capture time | `UloopPausePoint.TryGetCapturedValue("name")` | Faithful: value types are a boxed copy taken at capture time, so this never drifts. | +| A reference type's *live* current state | `UloopPausePoint.TryGetCapturedValue("name")` | The reference itself is live, so the object it points to may have changed since capture (or been destroyed/resumed away). Only available while Unity is still paused. | +| A reference type's state *as it was at capture time* | `uloop pause-point-status --id ` | The only faithful source for this: the response is a formatted string snapshot taken at capture time and stored in the registry, so it never drifts and stays retrievable after resume until the next clear or domain reload. | + +Capturing a deep copy at hit time was deliberately not adopted: it would cost hot-path performance and risk getter side effects, so the formatted-string snapshot (`pause-point-status`) remains the only way to get capture-time-faithful evidence for reference types. + +## Raw Capture API While Paused + +While Unity is paused on a hit, `execute-dynamic-code` can read live captured references through `UloopPausePoint`: + +- `TryGetCapturedValue(string name)` returns `(bool Found, object Value)` for the latest hit only. When multiple captured variables share the same name, the last one wins. +- `GetCapturedNames()` lists captured variable names from that snapshot. +- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot. + +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 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. + +## Warnings and Marker Freshness + +`await-pause-point`'s hit response also carries a top-level `Warning` (omitted when empty): it flags multiple hits, multiple matching logs, or truncated matching logs, so you can tell a single clean hit apart from evidence that needs closer inspection. `MatchingLogs` (log entries whose text contains the marker id) is still embedded, but source-derived ids rarely appear in log text, so treat `CapturedVariables` as the primary variable evidence. + +Use `Generation`, `EnabledAtUtc`, and the hit sequence fields from the hit or status response to tell a fresh marker from stale evidence with the same id. `RemainingMilliseconds` and `Expired` are returned directly so you do not need to infer marker lifetime from elapsed time. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/condition-triggered-pause.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/condition-triggered-pause.md new file mode 100644 index 0000000000..52592df19a --- /dev/null +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/condition-triggered-pause.md @@ -0,0 +1,47 @@ +# Catching a Runtime Condition with a Dynamic-Code Trigger + +A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file. + +## Workflow + +1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). +2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`. +3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. +4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. +5. Resume with `uloop control-play-mode --action Play`. + +## Example Watcher + +Freeze when the Hit animation passes 30% of the motion: + +```csharp +using UnityEngine; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.Runtime; +Animator animator = GameObject.Find("Zombie").GetComponent(); +// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate +double deadline = EditorApplication.timeSinceStartup + 120d; +EditorApplication.CallbackFunction watcher = null; +watcher = () => +{ + if (EditorApplication.timeSinceStartup > deadline) + { + EditorApplication.update -= watcher; + return; + } + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); + if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; + EditorApplication.update -= watcher; + UloopPausePoint.Pause("hit-peak"); +}; +EditorApplication.update += watcher; +return "watcher registered"; +``` + +## Rules + +- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. +- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly. +- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume. +- Do not use this pattern for frame-offset positioning ("N frames after the input"): frames keep advancing between CLI commands, so a recorded `Time.frameCount` baseline is race-prone. Use a file:line hit followed by `control-play-mode --action Step` N times instead (see SKILL.md). diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/watch-expressions.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/watch-expressions.md new file mode 100644 index 0000000000..c03b2cf218 --- /dev/null +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/references/watch-expressions.md @@ -0,0 +1,20 @@ +# Watch Expressions + +Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step: + +```bash +uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20 +uloop get-watch-values --id "speed" +``` + +## Evaluation Rules + +`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id ` before re-registering a changed expression. `clear-watch --id ` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch. + +Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement in SKILL.md). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same. + +The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit. + +## Lifetime + +Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`. From 760720b66df4a199e46f6050718d4e55b23127b2 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:02:14 +0900 Subject: [PATCH 3/7] docs: Deduplicate pause-point guidance across input-simulation skills The three simulate-* skills each carried a near-identical Pause Point Inspection section and four near-identical pause-point output field descriptions, and execute-dynamic-code restated pause-point semantics that the uloop-pause-point skill already owns. Keep only the tool-specific notes and point at that skill for the shared semantics. Also move execute-dynamic-code's Windows/PowerShell quoting details to references/shell-quoting.md, and drop the $ARGUMENTS placeholder from simulate-keyboard: no other skill uses it, arguments are appended automatically when absent, and non-Claude targets receive the file verbatim where the placeholder is never substituted. --- .agents/skills/uloop-execute-dynamic-code/SKILL.md | 11 ++--------- .../references/shell-quoting.md | 10 ++++++++++ .agents/skills/uloop-simulate-keyboard/SKILL.md | 11 +++-------- .agents/skills/uloop-simulate-mouse-input/SKILL.md | 10 ++-------- .agents/skills/uloop-simulate-mouse-ui/SKILL.md | 10 ++-------- .claude/skills/uloop-execute-dynamic-code/SKILL.md | 11 ++--------- .../references/shell-quoting.md | 10 ++++++++++ .claude/skills/uloop-simulate-keyboard/SKILL.md | 11 +++-------- .claude/skills/uloop-simulate-mouse-input/SKILL.md | 10 ++-------- .claude/skills/uloop-simulate-mouse-ui/SKILL.md | 10 ++-------- .../FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md | 11 ++--------- .../ExecuteDynamicCode/Skill/references.meta | 8 ++++++++ .../Skill/references/shell-quoting.md | 10 ++++++++++ .../Skill/references/shell-quoting.md.meta | 7 +++++++ .../FirstPartyTools/SimulateKeyboard/Skill/SKILL.md | 11 +++-------- .../FirstPartyTools/SimulateMouseInput/Skill/SKILL.md | 10 ++-------- .../FirstPartyTools/SimulateMouseUi/Skill/SKILL.md | 10 ++-------- 17 files changed, 72 insertions(+), 99 deletions(-) create mode 100644 .agents/skills/uloop-execute-dynamic-code/references/shell-quoting.md create mode 100644 .claude/skills/uloop-execute-dynamic-code/references/shell-quoting.md create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references.meta create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md create mode 100644 Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md.meta diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index cd4005d1a1..bfc350357b 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -11,9 +11,7 @@ Run focused C# snippets in the active Unity Editor with `uloop execute-dynamic-c For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state. -This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, enable a source pause point on that line instead (`uloop enable-pause-point --file --line `, see the `uloop-pause-point` skill): the hit response's `CapturedVariables` already contains the locals, parameters, and instance fields at that line, with no code edit or recompile. `CapturedVariables` is a pre-line snapshot; this tool during the pause sees the interrupted method's post-interrupt state instead. While Unity stays paused on the hit, use `UloopPausePoint.TryGetCapturedValue(name)` (plus `GetCapturedNames()` / `GetCapturedPausePointId()`) for live captured references such as collections. Do not try to reconstruct pre-line locals with execute-dynamic-code alone. - -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. +This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method — do not try to reconstruct them with this tool alone. When those values matter, follow the `uloop-pause-point` skill instead: a pause point's `CapturedVariables` already contains the locals, parameters, and instance fields at that line with no code edit or recompile, and while Unity stays paused `UloopPausePoint.TryGetCapturedValue(name)` gives this tool live captured references. That skill also covers the reverse combination — using this tool to register an `EditorApplication.update` watcher that freezes Unity on the first frame a runtime condition holds (never poll or sleep inside the snippet itself; the body runs synchronously on the main thread). 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. @@ -44,12 +42,7 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Shell Quoting -- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`. -- PowerShell 7 (`pwsh`): for multiline snippets, assign a single-quoted here-string (`$code = @'` ... `'@`) and pass `--code $code`. Inline, single-quoted arguments preserve C# double quotes; double an inner single quote (`''`). -- Windows PowerShell 5.1 removes unescaped double quotes from native command arguments: escape them as `\"`, or prefer `--code-file`. -- Pass `--parameters` as a single-quoted JSON object literal in both shells, for example `--parameters '{"param0":"value"}'`. -- On Windows, multiline `--code` requires the native `uloop.exe`. If `(Get-Command uloop).Source` resolves to a legacy `.cmd` shim, run `uloop install` and open a new terminal. -- If quoting still mangles the snippet, switch to `--code-file`. +In zsh/bash, single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. If a snippet fails to parse, gets mangled by the shell, or you are on Windows/PowerShell, read [references/shell-quoting.md](references/shell-quoting.md) — or switch to `--code-file`. ## When To Use Input Simulation Tools Instead diff --git a/.agents/skills/uloop-execute-dynamic-code/references/shell-quoting.md b/.agents/skills/uloop-execute-dynamic-code/references/shell-quoting.md new file mode 100644 index 0000000000..171b8f3834 --- /dev/null +++ b/.agents/skills/uloop-execute-dynamic-code/references/shell-quoting.md @@ -0,0 +1,10 @@ +# Shell Quoting for --code + +Read this when an inline `--code` snippet fails to parse, gets mangled by the shell, or when running on Windows. + +- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`. +- PowerShell 7 (`pwsh`): for multiline snippets, assign a single-quoted here-string (`$code = @'` ... `'@`) and pass `--code $code`. Inline, single-quoted arguments preserve C# double quotes; double an inner single quote (`''`). +- Windows PowerShell 5.1 removes unescaped double quotes from native command arguments: escape them as `\"`, or prefer `--code-file`. +- Pass `--parameters` as a single-quoted JSON object literal in both shells, for example `--parameters '{"param0":"value"}'`. +- On Windows, multiline `--code` requires the native `uloop.exe`. If `(Get-Command uloop).Source` resolves to a legacy `.cmd` shim, run `uloop install` and open a new terminal. +- If quoting still mangles the snippet, switch to `--code-file`. diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index f7a742d0e4..3420b88a29 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -7,7 +7,7 @@ context: fork # Task -Simulate keyboard input on Unity PlayMode: $ARGUMENTS +Simulate keyboard input on Unity PlayMode. ## Workflow @@ -45,9 +45,7 @@ If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` t ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the key is safe: when the pause lands mid-command, `simulate-keyboard` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the key when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. `PressEdgeObserved` is still reported on pause-point interruptions. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released; `PressEdgeObserved` is still reported on pause-point interruptions. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ### KeyDown/KeyUp Rules @@ -85,10 +83,7 @@ Returns JSON with: - `Message` (string): Description of what happened or why it failed - `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) - `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId` (string, nullable): The derived `:` pause point id that caused the interruption (the `Id` returned by `enable-pause-point`) -- `PausePointHitCount` (integer, nullable): The hit count for that pause point when it caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above - `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index 01eab3b2d7..9868d2e228 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -50,10 +50,7 @@ uloop simulate-mouse-input --action [options] ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the mouse input is safe: when the pause lands mid-command, `simulate-mouse-input` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the input when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. -- Clear pause points (`uloop clear-pause-point --all`) before final validation when they were enabled only for inspection. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## When to use this vs simulate-mouse-ui @@ -129,9 +126,6 @@ Returns JSON with: - `InputPositionX` / `InputPositionY`: Coordinates received from the caller - `InjectedUnityPositionX` / `InjectedUnityPositionY`: Coordinates injected into `Mouse.current.position` - `CoordinateConversionFormula`: Conversion formula used by the tool -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId`: The derived `:` pause point id that caused the interruption (the `Id` returned by `enable-pause-point`) -- `PausePointHitCount`: The hit count for that pause point when it caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above Verify visual outcome with a follow-up screenshot. diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index 2fb68dc7be..983c381383 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -71,10 +71,7 @@ uloop simulate-mouse-ui --action --x --y [options] ## Pause Point Inspection (Standard for E2E) -For standard frame proof when this UI input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the UI event is safe: when the pause lands mid-command, `simulate-mouse-ui` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the event when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and `Success: true` only means the command ended cleanly. Read `Message` first: it states whether the pointer event was already dispatched before the pause (only the overlay animation was interrupted) or the pause landed first (no pointer event was fired). -- Clear pause points (`uloop clear-pause-point --all`) before final validation when they were enabled only for inspection. +For standard frame proof when this UI input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, `Success: true` only means the command ended cleanly; read `Message` first — it states whether the pointer event was already dispatched before the pause (only the overlay animation was interrupted) or the pause landed first (no pointer event was fired). Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## Examples @@ -129,10 +126,7 @@ Returns JSON with: - `PositionY`: Target Y coordinate that was used - `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) - `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the command returned early instead of running to completion. `Message` states whether the pointer event was already dispatched before the pause -- `PausePointId`: The derived `:` pause point id that caused the interruption (nullable string; the `Id` returned by `enable-pause-point`) -- `PausePointHitCount`: The hit count for that pause point when it caused the interruption (nullable integer) -- `PausePointHits`: Every marker hit during this input as `{Id, HitCount}` entries, in hit order (nullable array). Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index cd4005d1a1..bfc350357b 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -11,9 +11,7 @@ Run focused C# snippets in the active Unity Editor with `uloop execute-dynamic-c For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state. -This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, enable a source pause point on that line instead (`uloop enable-pause-point --file --line `, see the `uloop-pause-point` skill): the hit response's `CapturedVariables` already contains the locals, parameters, and instance fields at that line, with no code edit or recompile. `CapturedVariables` is a pre-line snapshot; this tool during the pause sees the interrupted method's post-interrupt state instead. While Unity stays paused on the hit, use `UloopPausePoint.TryGetCapturedValue(name)` (plus `GetCapturedNames()` / `GetCapturedPausePointId()`) for live captured references such as collections. Do not try to reconstruct pre-line locals with execute-dynamic-code alone. - -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. +This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method — do not try to reconstruct them with this tool alone. When those values matter, follow the `uloop-pause-point` skill instead: a pause point's `CapturedVariables` already contains the locals, parameters, and instance fields at that line with no code edit or recompile, and while Unity stays paused `UloopPausePoint.TryGetCapturedValue(name)` gives this tool live captured references. That skill also covers the reverse combination — using this tool to register an `EditorApplication.update` watcher that freezes Unity on the first frame a runtime condition holds (never poll or sleep inside the snippet itself; the body runs synchronously on the main thread). 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. @@ -44,12 +42,7 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Shell Quoting -- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`. -- PowerShell 7 (`pwsh`): for multiline snippets, assign a single-quoted here-string (`$code = @'` ... `'@`) and pass `--code $code`. Inline, single-quoted arguments preserve C# double quotes; double an inner single quote (`''`). -- Windows PowerShell 5.1 removes unescaped double quotes from native command arguments: escape them as `\"`, or prefer `--code-file`. -- Pass `--parameters` as a single-quoted JSON object literal in both shells, for example `--parameters '{"param0":"value"}'`. -- On Windows, multiline `--code` requires the native `uloop.exe`. If `(Get-Command uloop).Source` resolves to a legacy `.cmd` shim, run `uloop install` and open a new terminal. -- If quoting still mangles the snippet, switch to `--code-file`. +In zsh/bash, single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. If a snippet fails to parse, gets mangled by the shell, or you are on Windows/PowerShell, read [references/shell-quoting.md](references/shell-quoting.md) — or switch to `--code-file`. ## When To Use Input Simulation Tools Instead diff --git a/.claude/skills/uloop-execute-dynamic-code/references/shell-quoting.md b/.claude/skills/uloop-execute-dynamic-code/references/shell-quoting.md new file mode 100644 index 0000000000..171b8f3834 --- /dev/null +++ b/.claude/skills/uloop-execute-dynamic-code/references/shell-quoting.md @@ -0,0 +1,10 @@ +# Shell Quoting for --code + +Read this when an inline `--code` snippet fails to parse, gets mangled by the shell, or when running on Windows. + +- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`. +- PowerShell 7 (`pwsh`): for multiline snippets, assign a single-quoted here-string (`$code = @'` ... `'@`) and pass `--code $code`. Inline, single-quoted arguments preserve C# double quotes; double an inner single quote (`''`). +- Windows PowerShell 5.1 removes unescaped double quotes from native command arguments: escape them as `\"`, or prefer `--code-file`. +- Pass `--parameters` as a single-quoted JSON object literal in both shells, for example `--parameters '{"param0":"value"}'`. +- On Windows, multiline `--code` requires the native `uloop.exe`. If `(Get-Command uloop).Source` resolves to a legacy `.cmd` shim, run `uloop install` and open a new terminal. +- If quoting still mangles the snippet, switch to `--code-file`. diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index f7a742d0e4..3420b88a29 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -7,7 +7,7 @@ context: fork # Task -Simulate keyboard input on Unity PlayMode: $ARGUMENTS +Simulate keyboard input on Unity PlayMode. ## Workflow @@ -45,9 +45,7 @@ If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` t ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the key is safe: when the pause lands mid-command, `simulate-keyboard` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the key when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. `PressEdgeObserved` is still reported on pause-point interruptions. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released; `PressEdgeObserved` is still reported on pause-point interruptions. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ### KeyDown/KeyUp Rules @@ -85,10 +83,7 @@ Returns JSON with: - `Message` (string): Description of what happened or why it failed - `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) - `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId` (string, nullable): The derived `:` pause point id that caused the interruption (the `Id` returned by `enable-pause-point`) -- `PausePointHitCount` (integer, nullable): The hit count for that pause point when it caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above - `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index 01eab3b2d7..9868d2e228 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -50,10 +50,7 @@ uloop simulate-mouse-input --action [options] ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the mouse input is safe: when the pause lands mid-command, `simulate-mouse-input` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the input when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. -- Clear pause points (`uloop clear-pause-point --all`) before final validation when they were enabled only for inspection. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## When to use this vs simulate-mouse-ui @@ -129,9 +126,6 @@ Returns JSON with: - `InputPositionX` / `InputPositionY`: Coordinates received from the caller - `InjectedUnityPositionX` / `InjectedUnityPositionY`: Coordinates injected into `Mouse.current.position` - `CoordinateConversionFormula`: Conversion formula used by the tool -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId`: The derived `:` pause point id that caused the interruption (the `Id` returned by `enable-pause-point`) -- `PausePointHitCount`: The hit count for that pause point when it caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above Verify visual outcome with a follow-up screenshot. diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index 2fb68dc7be..983c381383 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -71,10 +71,7 @@ uloop simulate-mouse-ui --action --x --y [options] ## Pause Point Inspection (Standard for E2E) -For standard frame proof when this UI input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the UI event is safe: when the pause lands mid-command, `simulate-mouse-ui` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the event when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and `Success: true` only means the command ended cleanly. Read `Message` first: it states whether the pointer event was already dispatched before the pause (only the overlay animation was interrupted) or the pause landed first (no pointer event was fired). -- Clear pause points (`uloop clear-pause-point --all`) before final validation when they were enabled only for inspection. +For standard frame proof when this UI input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, `Success: true` only means the command ended cleanly; read `Message` first — it states whether the pointer event was already dispatched before the pause (only the overlay animation was interrupted) or the pause landed first (no pointer event was fired). Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## Examples @@ -129,10 +126,7 @@ Returns JSON with: - `PositionY`: Target Y coordinate that was used - `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) - `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the command returned early instead of running to completion. `Message` states whether the pointer event was already dispatched before the pause -- `PausePointId`: The derived `:` pause point id that caused the interruption (nullable string; the `Id` returned by `enable-pause-point`) -- `PausePointHitCount`: The hit count for that pause point when it caused the interruption (nullable integer) -- `PausePointHits`: Every marker hit during this input as `{Id, HitCount}` entries, in hit order (nullable array). Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index cd4005d1a1..bfc350357b 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -11,9 +11,7 @@ Run focused C# snippets in the active Unity Editor with `uloop execute-dynamic-c For basic selected GameObject discovery or property inspection, use `find-game-objects --search-mode Selected` before this tool. Use this tool after the built-in inspection tools are not enough or when you need to modify Unity state. -This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, enable a source pause point on that line instead (`uloop enable-pause-point --file --line `, see the `uloop-pause-point` skill): the hit response's `CapturedVariables` already contains the locals, parameters, and instance fields at that line, with no code edit or recompile. `CapturedVariables` is a pre-line snapshot; this tool during the pause sees the interrupted method's post-interrupt state instead. While Unity stays paused on the hit, use `UloopPausePoint.TryGetCapturedValue(name)` (plus `GetCapturedNames()` / `GetCapturedPausePointId()`) for live captured references such as collections. Do not try to reconstruct pre-line locals with execute-dynamic-code alone. - -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. +This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method — do not try to reconstruct them with this tool alone. When those values matter, follow the `uloop-pause-point` skill instead: a pause point's `CapturedVariables` already contains the locals, parameters, and instance fields at that line with no code edit or recompile, and while Unity stays paused `UloopPausePoint.TryGetCapturedValue(name)` gives this tool live captured references. That skill also covers the reverse combination — using this tool to register an `EditorApplication.update` watcher that freezes Unity on the first frame a runtime condition holds (never poll or sleep inside the snippet itself; the body runs synchronously on the main thread). 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. @@ -44,12 +42,7 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Shell Quoting -- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`. -- PowerShell 7 (`pwsh`): for multiline snippets, assign a single-quoted here-string (`$code = @'` ... `'@`) and pass `--code $code`. Inline, single-quoted arguments preserve C# double quotes; double an inner single quote (`''`). -- Windows PowerShell 5.1 removes unescaped double quotes from native command arguments: escape them as `\"`, or prefer `--code-file`. -- Pass `--parameters` as a single-quoted JSON object literal in both shells, for example `--parameters '{"param0":"value"}'`. -- On Windows, multiline `--code` requires the native `uloop.exe`. If `(Get-Command uloop).Source` resolves to a legacy `.cmd` shim, run `uloop install` and open a new terminal. -- If quoting still mangles the snippet, switch to `--code-file`. +In zsh/bash, single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. If a snippet fails to parse, gets mangled by the shell, or you are on Windows/PowerShell, read [references/shell-quoting.md](references/shell-quoting.md) — or switch to `--code-file`. ## When To Use Input Simulation Tools Instead diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references.meta new file mode 100644 index 0000000000..43d7e1b301 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b5a9696340ef4463aade46b24c8a3fe5 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md new file mode 100644 index 0000000000..171b8f3834 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md @@ -0,0 +1,10 @@ +# Shell Quoting for --code + +Read this when an inline `--code` snippet fails to parse, gets mangled by the shell, or when running on Windows. + +- zsh/bash: single-quote the whole snippet so C# double quotes pass through unchanged: `--code 'return "hi";'`. For a single quote inside the snippet, close and reopen the shell string with `'\''`. +- PowerShell 7 (`pwsh`): for multiline snippets, assign a single-quoted here-string (`$code = @'` ... `'@`) and pass `--code $code`. Inline, single-quoted arguments preserve C# double quotes; double an inner single quote (`''`). +- Windows PowerShell 5.1 removes unescaped double quotes from native command arguments: escape them as `\"`, or prefer `--code-file`. +- Pass `--parameters` as a single-quoted JSON object literal in both shells, for example `--parameters '{"param0":"value"}'`. +- On Windows, multiline `--code` requires the native `uloop.exe`. If `(Get-Command uloop).Source` resolves to a legacy `.cmd` shim, run `uloop install` and open a new terminal. +- If quoting still mangles the snippet, switch to `--code-file`. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md.meta b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md.meta new file mode 100644 index 0000000000..a3559d57d4 --- /dev/null +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/shell-quoting.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 3d9a3d1c90dce4cff90e5da200b41983 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index f7a742d0e4..3420b88a29 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -7,7 +7,7 @@ context: fork # Task -Simulate keyboard input on Unity PlayMode: $ARGUMENTS +Simulate keyboard input on Unity PlayMode. ## Workflow @@ -45,9 +45,7 @@ If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` t ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the key is safe: when the pause lands mid-command, `simulate-keyboard` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the key when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. `PressEdgeObserved` is still reported on pause-point interruptions. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released; `PressEdgeObserved` is still reported on pause-point interruptions. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ### KeyDown/KeyUp Rules @@ -85,10 +83,7 @@ Returns JSON with: - `Message` (string): Description of what happened or why it failed - `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) - `KeyName` (string, nullable): The key that was acted on; may be `null` when the action could not resolve a key -- `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId` (string, nullable): The derived `:` pause point id that caused the interruption (the `Id` returned by `enable-pause-point`) -- `PausePointHitCount` (integer, nullable): The hit count for that pause point when it caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above - `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was actually visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge (e.g. the press was consumed by an editor-only input update) — retry the input or verify with a focused log instead of trusting `Success` alone. `null` only for `KeyUp` and for timed-out responses; pause-point interruptions still report the observed value ## Prerequisites diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md index 01eab3b2d7..9868d2e228 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -50,10 +50,7 @@ uloop simulate-mouse-input --action [options] ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the mouse input is safe: when the pause lands mid-command, `simulate-mouse-input` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the input when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. -- Clear pause points (`uloop clear-pause-point --all`) before final validation when they were enabled only for inspection. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## When to use this vs simulate-mouse-ui @@ -129,9 +126,6 @@ Returns JSON with: - `InputPositionX` / `InputPositionY`: Coordinates received from the caller - `InjectedUnityPositionX` / `InjectedUnityPositionY`: Coordinates injected into `Mouse.current.position` - `CoordinateConversionFormula`: Conversion formula used by the tool -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `PausePointId`: The derived `:` pause point id that caused the interruption (the `Id` returned by `enable-pause-point`) -- `PausePointHitCount`: The hit count for that pause point when it caused the interruption -- `PausePointHits` (array, nullable): Every marker hit during this input as `{Id, HitCount}` entries, in hit order. Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above Verify visual outcome with a follow-up screenshot. diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md index 2fb68dc7be..983c381383 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -71,10 +71,7 @@ uloop simulate-mouse-ui --action --x --y [options] ## Pause Point Inspection (Standard for E2E) -For standard frame proof when this UI input drives a state transition, follow the `uloop-pause-point` skill. Pausing on the line that handles the UI event is safe: when the pause lands mid-command, `simulate-mouse-ui` returns promptly with `InterruptedByPausePoint: true` instead of running to completion. Prefer a line after the app consumed the event when you want the settled result state rather than the input-handling moment. - -- If `InterruptedByPausePoint: true`, Unity is paused and `Success: true` only means the command ended cleanly. Read `Message` first: it states whether the pointer event was already dispatched before the pause (only the overlay animation was interrupted) or the pause landed first (no pointer event was fired). -- Clear pause points (`uloop clear-pause-point --all`) before final validation when they were enabled only for inspection. +For standard frame proof when this UI input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, `Success: true` only means the command ended cleanly; read `Message` first — it states whether the pointer event was already dispatched before the pause (only the overlay animation was interrupted) or the pause landed first (no pointer event was fired). Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## Examples @@ -129,10 +126,7 @@ Returns JSON with: - `PositionY`: Target Y coordinate that was used - `EndPositionX`: Drag end X coordinate (nullable float; populated for drag actions only) - `EndPositionY`: Drag end Y coordinate (nullable float; populated for drag actions only) -- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the command returned early instead of running to completion. `Message` states whether the pointer event was already dispatched before the pause -- `PausePointId`: The derived `:` pause point id that caused the interruption (nullable string; the `Id` returned by `enable-pause-point`) -- `PausePointHitCount`: The hit count for that pause point when it caused the interruption (nullable integer) -- `PausePointHits`: Every marker hit during this input as `{Id, HitCount}` entries, in hit order (nullable array). Read this when one input may trigger several markers; `PausePointId` only names the latest one +- `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section above Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. From 03a467f951b2b6aa180f0bbf452c821aa6153477 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:11:32 +0900 Subject: [PATCH 4/7] docs: Add repository map and skill regeneration pointers to AGENTS.md Agents kept rediscovering the same structural facts from scratch every session (where skill sources live, how to regenerate the .claude/.agents copies, which folders need .meta files, which Go module owns what). Record them once at directory granularity: a coarse Repository Map section, plus the concrete source paths and regenerate command in the Generated Skill Files section, which previously said "regenerate through the normal workflow" without naming it. --- AGENTS.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 0e1f7af702..85269edb65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,22 @@ This project provides a **CLI tool (`uloop`)** that communicates with Unity Edit AI agents interact with Unity through `uloop` CLI commands (e.g., `uloop get-logs`, `uloop compile`). The Unity Editor side hosts a local project IPC server that accepts short-lived CLI command sessions. +## Repository Map + +Directory-level responsibilities. Kept deliberately coarse — check the directory itself for file-level detail. + +- `Packages/src/` — the Unity package (C#). Each first-party tool lives in `Editor/FirstPartyTools//` with its implementation and agent skill (`Skill/SKILL.md`, optional `Skill/references/`). +- `Packages/src/Editor/CliOnlyTools~//Skill/` — skills for CLI-only commands (launch, pause-point, etc.) that have no Unity tool class. Tilde-suffixed folders are ignored by Unity, so files there need no `.meta`; files under `FirstPartyTools` do (run `uloop compile` to let Unity generate them). +- `Assets/` — the development/test Unity project, including custom-command samples under `Assets/Editor/CustomCommandSamples/`. +- `cli/dispatcher/` — the globally installed `uloop` entry command (also owns `uloop skills install` and skill syncing). +- `cli/project-runner/` — the per-project CLI runner that talks to the Unity-side IPC server. +- `cli/common/` — Go modules shared by dispatcher and project runner. Tool parameter schemas live in `cli/common/tools/default-tools.json`; skill discovery in `cli/common/skillscan/`. +- `cli/release-automation/` — Go logic backing GitHub Actions release/CI workflows. +- `tools/UnityCliLoop.DeadCodeScanner/` — the C# dead-code scanner described below. +- `dist/` — locally built development binaries (`dist/darwin-arm64/uloop`, etc.); never committed. +- `.claude/`, `.agents/` — generated skill copies; never edit directly (see Generated Skill Files). +- `.uloop/` — runtime state and command outputs (screenshots, test results, hierarchy dumps). + Do not rename public package, assembly, or extension API identifiers as part of cleanup-only changes. Comments in the code, commit messages, PR titles, and PR descriptions must all be written in English. @@ -69,7 +85,10 @@ dispatcher must stay lenient when reading pins written by older packages. ## Generated Skill Files Do not directly edit skill files under the project-root `.agents/` or `.claude/` directories. -These files are generated copies. Update the source skill definitions instead, then regenerate the copies through the normal workflow. +These files are generated copies. Update the source skill definitions instead, then regenerate the copies. + +- Sources: `Packages/src/Editor/FirstPartyTools//Skill/SKILL.md` and `Packages/src/Editor/CliOnlyTools~//Skill/SKILL.md` (plus each skill's `references/` files, which are copied along with it). +- Regenerate: `dist/darwin-arm64/uloop skills install --claude --agents` from the project root. Only `.claude/` and `.agents/` are tracked in git; other targets are local-only. ## CI Automation Language From fbcbd8141484eaa248a754f4748eac947ceb2fdf Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:17:07 +0900 Subject: [PATCH 5/7] docs: Mention warning and marker-freshness fields in the reference pointer The Warning/MatchingLogs and Generation/EnabledAtUtc explanations moved to references/captured-variables.md, but the pointer sentence in the core SKILL.md did not list them as a reason to read the reference, so an agent seeing those fields had no breadcrumb to the moved guidance. --- .agents/skills/uloop-pause-point/SKILL.md | 2 +- .claude/skills/uloop-pause-point/SKILL.md | 2 +- Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 92f43c60db..e88722e37b 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -58,7 +58,7 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `--captured-variables names` on `await-pause-point`/`pause-point-status` drops every `Value` and keeps `Name`/`Scope`/`TypeName` — use it first on field-heavy classes, then fetch full values with a plain `pause-point-status` call. - While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("name")` (and `"this"`) returns live captured references for `execute-dynamic-code`; the holder clears on resume. -Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). +Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, the hit response's `Warning`/`MatchingLogs` fields, marker freshness (`Generation`, `EnabledAtUtc`), or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). ## Watch Expressions diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 92f43c60db..e88722e37b 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -58,7 +58,7 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `--captured-variables names` on `await-pause-point`/`pause-point-status` drops every `Value` and keeps `Name`/`Scope`/`TypeName` — use it first on field-heavy classes, then fetch full values with a plain `pause-point-status` call. - While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("name")` (and `"this"`) returns live captured references for `execute-dynamic-code`; the holder clears on resume. -Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). +Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, the hit response's `Warning`/`MatchingLogs` fields, marker freshness (`Generation`, `EnabledAtUtc`), or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). ## Watch Expressions diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 92f43c60db..e88722e37b 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -58,7 +58,7 @@ Every hit response embeds `CapturedVariables`: the method's in-scope locals, its - `--captured-variables names` on `await-pause-point`/`pause-point-status` drops every `Value` and keeps `Name`/`Scope`/`TypeName` — use it first on field-heavy classes, then fetch full values with a plain `pause-point-status` call. - While Unity is still paused, `UloopPausePoint.TryGetCapturedValue("name")` (and `"this"`) returns live captured references for `execute-dynamic-code`; the holder clears on resume. -Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). +Before interpreting unexpected, missing, or truncated values, nested previews that render as type names, Unity-object `Value` strings, capture-time vs live evidence trade-offs, the hit response's `Warning`/`MatchingLogs` fields, marker freshness (`Generation`, `EnabledAtUtc`), or the raw capture API in detail, read [references/captured-variables.md](references/captured-variables.md). ## Watch Expressions From a6a84bf279cea8c026cc85b24447c16eb9ef92b0 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:24:39 +0900 Subject: [PATCH 6/7] docs: Fix stale TCP claim in the AGENTS.md architecture overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V3 talks to the Unity Editor over local IPC — a Unix domain socket on macOS/Linux and a named pipe on Windows (BridgeTransportEndpoint) — and no TCP code path remains. The overview sentence was a leftover from the v2 architecture. --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 85269edb65..a3b2d52413 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ ## Architecture Overview -This project provides a **CLI tool (`uloop`)** that communicates with Unity Editor via TCP. +This project provides a **CLI tool (`uloop`)** that communicates with Unity Editor over local IPC +(a Unix domain socket on macOS/Linux, a named pipe on Windows — not TCP). AI agents interact with Unity through `uloop` CLI commands (e.g., `uloop get-logs`, `uloop compile`). The Unity Editor side hosts a local project IPC server that accepts short-lived CLI command sessions. From 273d0612516ebbb93bb2cd00a17c7a0504d0febd Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 20 Jul 2026 23:28:29 +0900 Subject: [PATCH 7/7] docs: Note platform-specific binary path in skill regeneration command dist/darwin-arm64/uloop only exists on macOS ARM; tell contributors on other platforms to substitute their platform binary, matching the existing Native Go CLI Validation guidance. --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index a3b2d52413..b8978d5934 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,7 @@ Do not directly edit skill files under the project-root `.agents/` or `.claude/` These files are generated copies. Update the source skill definitions instead, then regenerate the copies. - Sources: `Packages/src/Editor/FirstPartyTools//Skill/SKILL.md` and `Packages/src/Editor/CliOnlyTools~//Skill/SKILL.md` (plus each skill's `references/` files, which are copied along with it). -- Regenerate: `dist/darwin-arm64/uloop skills install --claude --agents` from the project root. Only `.claude/` and `.agents/` are tracked in git; other targets are local-only. +- Regenerate: `dist/darwin-arm64/uloop skills install --claude --agents` from the project root, substituting the binary for your platform (e.g. `dist/windows-amd64/uloop.exe` on Windows). Only `.claude/` and `.agents/` are tracked in git; other targets are local-only. ## CI Automation Language