From 881bbeafb01f2df98f39b788348981af32f1cd04 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 22 Jun 2026 19:24:38 +0900 Subject: [PATCH 1/9] Restore PascalCase CLI JSON output Keep V3 CLI responses aligned with the V2 public JSON shape, and remove the migration skill guidance that told agents to rewrite first-party output fields to camelCase. - Restore PascalCase response serialization on the Unity side and native CLI output structs - Update first-party skill docs and generated skill copies to reference PascalCase fields - Rename find-game-objects response DTO properties instead of using per-property JsonProperty aliases --- .agents/skills/uloop-clear-console/SKILL.md | 14 +- .agents/skills/uloop-compile/SKILL.md | 8 +- .../skills/uloop-control-play-mode/SKILL.md | 12 +- .../uloop-execute-dynamic-code/SKILL.md | 20 +-- .../skills/uloop-find-game-objects/SKILL.md | 28 ++-- .agents/skills/uloop-focus-window/SKILL.md | 4 +- .agents/skills/uloop-get-hierarchy/SKILL.md | 6 +- .agents/skills/uloop-get-logs/SKILL.md | 14 +- .agents/skills/uloop-launch/SKILL.md | 24 +-- .agents/skills/uloop-record-input/SKILL.md | 16 +- .agents/skills/uloop-replay-input/SKILL.md | 16 +- .agents/skills/uloop-run-tests/SKILL.md | 30 ++-- .agents/skills/uloop-screenshot/SKILL.md | 24 +-- .../references/annotated-elements.md | 32 ++-- .../skills/uloop-simulate-keyboard/SKILL.md | 20 +-- .../uloop-simulate-mouse-input/SKILL.md | 22 +-- .../skills/uloop-simulate-mouse-ui/SKILL.md | 26 ++-- .../uloop-wait-for-pause-point/SKILL.md | 14 +- .claude/skills/uloop-clear-console/SKILL.md | 14 +- .claude/skills/uloop-compile/SKILL.md | 8 +- .../skills/uloop-control-play-mode/SKILL.md | 12 +- .../uloop-execute-dynamic-code/SKILL.md | 20 +-- .../skills/uloop-find-game-objects/SKILL.md | 28 ++-- .claude/skills/uloop-focus-window/SKILL.md | 4 +- .claude/skills/uloop-get-hierarchy/SKILL.md | 6 +- .claude/skills/uloop-get-logs/SKILL.md | 14 +- .claude/skills/uloop-launch/SKILL.md | 24 +-- .claude/skills/uloop-record-input/SKILL.md | 16 +- .claude/skills/uloop-replay-input/SKILL.md | 16 +- .claude/skills/uloop-run-tests/SKILL.md | 30 ++-- .claude/skills/uloop-screenshot/SKILL.md | 24 +-- .../references/annotated-elements.md | 32 ++-- .../skills/uloop-simulate-keyboard/SKILL.md | 20 +-- .../uloop-simulate-mouse-input/SKILL.md | 22 +-- .../skills/uloop-simulate-mouse-ui/SKILL.md | 26 ++-- .../uloop-wait-for-pause-point/SKILL.md | 14 +- .../FindGameObjectsTestMenu.cs | 24 +-- .../CompileSessionResultServiceTests.cs | 10 +- .../Editor/CompileStatusBridgeCommandTests.cs | 22 +-- .../ExecuteDynamicCodeUseCaseTests.cs | 14 +- .../Tests/Editor/FindGameObjectsToolTests.cs | 144 +++++++++--------- Assets/Tests/Editor/GetHierarchyToolTests.cs | 10 +- .../JsonRpcProcessorCliVersionGateTests.cs | 10 +- .../CliOnlyTools~/FocusWindow/Skill/SKILL.md | 4 +- .../CliOnlyTools~/Launch/Skill/SKILL.md | 24 +-- .../CliOnlyTools~/PausePoint/Skill/SKILL.md | 14 +- .../ClearConsole/Skill/SKILL.md | 14 +- .../FirstPartyTools/Compile/Skill/SKILL.md | 8 +- .../ControlPlayMode/Skill/SKILL.md | 12 +- .../ExecuteDynamicCode/Skill/SKILL.md | 20 +-- .../FindGameObjects/ComponentInfo.cs | 19 ++- .../FindGameObjectsResponse.cs | 42 +++-- .../FindGameObjects/FindGameObjectsTool.cs | 30 ++-- .../ComponentPropertySerializer.cs | 6 +- .../GameObjectFinder/ComponentSerializer.cs | 8 +- .../FindGameObjects/Skill/SKILL.md | 28 ++-- .../UnityCliLoopGameObjectSearchTypes.cs | 10 -- .../GetHierarchy/GetHierarchyResponse.cs | 10 +- .../GetHierarchy/Skill/SKILL.md | 6 +- .../FirstPartyTools/GetLogs/Skill/SKILL.md | 14 +- .../RecordInput/Skill/SKILL.md | 16 +- .../ReplayInput/Skill/SKILL.md | 16 +- .../FirstPartyTools/RunTests/Skill/SKILL.md | 30 ++-- .../FirstPartyTools/Screenshot/Skill/SKILL.md | 24 +-- .../Skill/references/annotated-elements.md | 32 ++-- .../SimulateKeyboard/Skill/SKILL.md | 20 +-- .../SimulateMouseInput/Skill/SKILL.md | 22 +-- .../SimulateMouseUi/Skill/SKILL.md | 26 ++-- .../Api/CompileStatusBridgeCommand.cs | 14 +- .../Api/JsonRpcResponseSerializer.cs | 11 -- .../Skill/SKILL.md | 7 +- .../Skill/references/first-party-v2-to-v3.md | 63 +------- cli/internal/cli/argument_error.go | 6 +- cli/internal/cli/busy_status.go | 14 +- cli/internal/cli/compile_wait.go | 28 ++-- cli/internal/cli/compile_wait_test.go | 54 +++---- cli/internal/cli/control_play_mode_wait.go | 38 ++--- .../cli/control_play_mode_wait_test.go | 18 +-- cli/internal/cli/debug_timing.go | 2 +- cli/internal/cli/debug_timing_test.go | 18 +-- cli/internal/cli/dynamic_code_wait.go | 6 +- cli/internal/cli/error_editor_unresponsive.go | 4 +- cli/internal/cli/error_envelope.go | 50 +++--- cli/internal/cli/error_envelope_test.go | 28 ++-- cli/internal/cli/focus.go | 4 +- cli/internal/cli/help_test.go | 4 +- cli/internal/cli/install.go | 4 +- cli/internal/cli/install_test.go | 4 +- .../cli/launch_process_exit_timeout_error.go | 4 +- cli/internal/cli/launch_ready.go | 24 +-- .../cli/launch_startup_timeout_error.go | 4 +- cli/internal/cli/launch_test.go | 10 +- cli/internal/cli/list_output.go | 24 +-- cli/internal/cli/pause_point_errors.go | 30 ++-- cli/internal/cli/pause_point_logs.go | 68 ++++----- cli/internal/cli/pause_point_wait.go | 46 +++--- cli/internal/cli/pause_point_wait_test.go | 50 +++--- cli/internal/cli/run.go | 6 +- cli/internal/cli/tool_readiness.go | 4 +- cli/internal/cli/uninstall.go | 2 +- cli/internal/cli/update.go | 2 +- 101 files changed, 971 insertions(+), 1029 deletions(-) diff --git a/.agents/skills/uloop-clear-console/SKILL.md b/.agents/skills/uloop-clear-console/SKILL.md index 3cb9e2be3f..0be2cc80fc 100644 --- a/.agents/skills/uloop-clear-console/SKILL.md +++ b/.agents/skills/uloop-clear-console/SKILL.md @@ -39,10 +39,10 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `success` (boolean): Whether the clear operation succeeded -- `clearedLogCount` (number): Total number of log entries that were cleared -- `clearedCounts` (object): Breakdown by log type - - `errorCount` (number): Errors cleared - - `warningCount` (number): Warnings cleared - - `logCount` (number): Info logs cleared -- `message` (string): Description of the result. On failure, this field carries the error summary. +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result. On failure, this field carries the error summary. diff --git a/.agents/skills/uloop-compile/SKILL.md b/.agents/skills/uloop-compile/SKILL.md index 9a2780a2a1..b504940489 100644 --- a/.agents/skills/uloop-compile/SKILL.md +++ b/.agents/skills/uloop-compile/SKILL.md @@ -44,7 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `success`: boolean or null -- `errorCount`: number or null -- `warningCount`: number or null -- `message`: string +- `Success`: boolean or null +- `ErrorCount`: number or null +- `WarningCount`: number or null +- `Message`: string diff --git a/.agents/skills/uloop-control-play-mode/SKILL.md b/.agents/skills/uloop-control-play-mode/SKILL.md index bfdb9e9bc9..362405c49f 100644 --- a/.agents/skills/uloop-control-play-mode/SKILL.md +++ b/.agents/skills/uloop-control-play-mode/SKILL.md @@ -49,14 +49,14 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `isPlaying`: Whether Unity is currently in play mode -- `isPaused`: Whether play mode is paused -- `changed`: Whether the requested action changed the current play mode state -- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `message`: Description of the action performed +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Changed`: Whether the requested action changed the current play mode state +- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `Message`: Description of the action performed ## Notes -- Stop on an already-stopped Editor sets `changed: false`, `wasAlreadyStopped: true` +- Stop on an already-stopped Editor sets `Changed: false`, `WasAlreadyStopped: true` - Step is independent of `Time.timeScale`; requires PlayMode to be running - The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index 1b2c08ebf9..c77635307c 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -43,16 +43,16 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: -- `success`: boolean — overall execution success -- `result`: string — value of the snippet's `return` statement (empty when omitted) -- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` -- `error` / `errorMessage`: string — top-level failure summary (empty on success) -- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `diagnosticsSummary`: string|null — compact summary when diagnostics are available -- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it - -On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `Error` / `ErrorMessage`: string — top-level failure summary (empty on success) +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/.agents/skills/uloop-find-game-objects/SKILL.md b/.agents/skills/uloop-find-game-objects/SKILL.md index 24edd909a3..dd4faa9981 100644 --- a/.agents/skills/uloop-find-game-objects/SKILL.md +++ b/.agents/skills/uloop-find-game-objects/SKILL.md @@ -70,21 +70,21 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: -- `results` (array): Matching GameObjects, each containing: - - `name` (string): GameObject name - - `path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) - - `isActive` (boolean): Active state in hierarchy - - `tag` (string): GameObject tag - - `layer` (number): Layer index - - `components` (array): Each entry has `type` (short name, e.g., `Rigidbody`), `fullTypeName` (e.g., `UnityEngine.Rigidbody`), and `properties` (array of Inspector-visible `{name, type, value}` pairs) -- `totalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. -- `errorMessage` (string): Top-level failure summary (empty on success) -- `processingErrors` (array): Selected-mode per-GameObject serialization failures, each `{gameObjectName, gameObjectPath, error}`. Omitted/null or empty on clean runs. +- `Results` (array): Matching GameObjects, each containing: + - `Name` (string): GameObject name + - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) + - `IsActive` (boolean): Active state in hierarchy + - `Tag` (string): GameObject tag + - `Layer` (number): Layer index + - `Components` (array): Each entry has `Type` (short name, e.g., `Rigidbody`), `FullTypeName` (e.g., `UnityEngine.Rigidbody`), and `Properties` (array of Inspector-visible `{Name, Type, Value}` pairs) +- `TotalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. +- `ErrorMessage` (string): Top-level failure summary (empty on success) +- `ProcessingErrors` (array): Selected-mode per-GameObject serialization failures, each `{GameObjectName, GameObjectPath, Error}`. Omitted/null or empty on clean runs. ### Multi-selection file export -For `Selected` mode with **multiple** successfully serialized GameObjects, inline `results` is not populated and the data is written to a file instead. Two extra fields appear: -- `resultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` -- `message` (string): Human-readable summary (e.g., "5 GameObjects exported") +For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: +- `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` +- `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") -Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `results` plus a `message`. +Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. diff --git a/.agents/skills/uloop-focus-window/SKILL.md b/.agents/skills/uloop-focus-window/SKILL.md index e2ca7a23bb..f2c93429bd 100644 --- a/.agents/skills/uloop-focus-window/SKILL.md +++ b/.agents/skills/uloop-focus-window/SKILL.md @@ -29,8 +29,8 @@ uloop focus-window ## Output Returns JSON with: -- `success`: Whether the focus operation succeeded -- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) +- `Success`: Whether the focus operation succeeded +- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) ## Notes diff --git a/.agents/skills/uloop-get-hierarchy/SKILL.md b/.agents/skills/uloop-get-hierarchy/SKILL.md index 4a1ec7cc08..fcd1124c82 100644 --- a/.agents/skills/uloop-get-hierarchy/SKILL.md +++ b/.agents/skills/uloop-get-hierarchy/SKILL.md @@ -56,7 +56,7 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: -- `message` (string): Human-readable guidance pointing at the saved file -- `hierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data +- `Message` (string): Human-readable guidance pointing at the saved file +- `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data -The hierarchy itself is **not** in the response — it is written to the file at `hierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). +The hierarchy itself is **not** in the response — it is written to the file at `HierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). diff --git a/.agents/skills/uloop-get-logs/SKILL.md b/.agents/skills/uloop-get-logs/SKILL.md index 57d1d97f62..d29d1d7881 100644 --- a/.agents/skills/uloop-get-logs/SKILL.md +++ b/.agents/skills/uloop-get-logs/SKILL.md @@ -50,10 +50,10 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `totalCount` (number): Total logs available before max-count clipping -- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) -- Input filters (`logType`, `maxCount`, `searchText`, `includeStackTrace`) are echoed back in the response -- `logs` (array): Each entry has: - - `type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `message` (string): Log message body - - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. +- `TotalCount` (number): Total logs available before max-count clipping +- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) +- Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response +- `Logs` (array): Each entry has: + - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `Message` (string): Log message body + - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/.agents/skills/uloop-launch/SKILL.md b/.agents/skills/uloop-launch/SKILL.md index 7bbee3d3e5..5019a7c014 100644 --- a/.agents/skills/uloop-launch/SKILL.md +++ b/.agents/skills/uloop-launch/SKILL.md @@ -50,18 +50,18 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: -- `success`: whether the command completed -- `ready`: whether Unity CLI Loop is ready for commands -- `serverReady`: whether the Unity CLI Loop server accepted requests -- `projectIpcReady`: whether the project IPC path accepted tool requests -- `alreadyRunning`: whether an existing Unity process was reused -- `launched`: whether this command launched a Unity process -- `restarted`: whether this command stopped an existing process and launched a new one -- `quit`: whether this command stopped Unity without launching a new process -- `previousProcessId`: process ID stopped by restart or quit, when available -- `currentProcessId`: current Unity process ID, when available -- `projectRoot`: resolved project root -- `message`: readiness summary +- `Success`: whether the command completed +- `Ready`: whether Unity CLI Loop is ready for commands +- `ServerReady`: whether the Unity CLI Loop server accepted requests +- `ProjectIpcReady`: whether the project IPC path accepted tool requests +- `AlreadyRunning`: whether an existing Unity process was reused +- `Launched`: whether this command launched a Unity process +- `Restarted`: whether this command stopped an existing process and launched a new one +- `Quit`: whether this command stopped Unity without launching a new process +- `PreviousProcessId`: process ID stopped by restart or quit, when available +- `CurrentProcessId`: current Unity process ID, when available +- `ProjectRoot`: resolved project root +- `Message`: readiness summary ## Notes diff --git a/.agents/skills/uloop-record-input/SKILL.md b/.agents/skills/uloop-record-input/SKILL.md index a76d38cb33..0781fbcf4d 100644 --- a/.agents/skills/uloop-record-input/SKILL.md +++ b/.agents/skills/uloop-record-input/SKILL.md @@ -44,11 +44,11 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start` or `Stop`) -- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -Frame data lives only in the JSON file at `outputPath`; internal metadata fields are not included in the response. +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start` or `Stop`) +- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +Frame data lives only in the JSON file at `OutputPath`; internal metadata fields are not included in the response. diff --git a/.agents/skills/uloop-replay-input/SKILL.md b/.agents/skills/uloop-replay-input/SKILL.md index 81793dbc8e..e159fadec8 100644 --- a/.agents/skills/uloop-replay-input/SKILL.md +++ b/.agents/skills/uloop-replay-input/SKILL.md @@ -48,11 +48,11 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `inputPath`: Path to recording file (nullable string; populated on `Start` only) -- `currentFrame`: Current replay frame index (nullable int) -- `totalFrames`: Total frames in the recording (nullable int) -- `progress`: Replay progress (nullable float in 0.0 – 1.0) -- `isReplaying`: Whether replay is currently active (nullable bool) +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `InputPath`: Path to recording file (nullable string; populated on `Start` only) +- `CurrentFrame`: Current replay frame index (nullable int) +- `TotalFrames`: Total frames in the recording (nullable int) +- `Progress`: Replay progress (nullable float in 0.0 – 1.0) +- `IsReplaying`: Whether replay is currently active (nullable bool) diff --git a/.agents/skills/uloop-run-tests/SKILL.md b/.agents/skills/uloop-run-tests/SKILL.md index 7660586e11..b0a4bf4b74 100644 --- a/.agents/skills/uloop-run-tests/SKILL.md +++ b/.agents/skills/uloop-run-tests/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. Before running tests, run `uloop compile` first if you created, deleted, renamed, moved, or edited C# files, test files, `.asmdef`/`.asmref`, or package manifests since the last successful compile. This refreshes the AssetDatabase and surfaces compile errors before test execution. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. -`NoTestsFound` means zero tests matched — not a test failure. Check `noTestsFoundExplanation` and `message` for asmdef hints. +`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. ## Usage @@ -57,18 +57,18 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `success` (boolean): Whether all tests passed -- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `hasFailures` (boolean): Whether any discovered test failed -- `message` (string): Summary message -- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise -- `completedAt` (string): ISO timestamp when the run finished -- `testCount` (number): Total tests executed -- `passedCount` (number): Passed tests -- `failedCount` (number): Failed tests -- `skippedCount` (number): Skipped tests -- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. +- `Success` (boolean): Whether all tests passed +- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `HasFailures` (boolean): Whether any discovered test failed +- `Message` (string): Summary message +- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise +- `CompletedAt` (string): ISO timestamp when the run finished +- `TestCount` (number): Total tests executed +- `PassedCount` (number): Passed tests +- `FailedCount` (number): Failed tests +- `SkippedCount` (number): Skipped tests +- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File diff --git a/.agents/skills/uloop-screenshot/SKILL.md b/.agents/skills/uloop-screenshot/SKILL.md index bc721a5ee7..fdebaae7a0 100644 --- a/.agents/skills/uloop-screenshot/SKILL.md +++ b/.agents/skills/uloop-screenshot/SKILL.md @@ -72,18 +72,18 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: -- `screenshotCount`: Number of windows captured -- `screenshots`: Array of screenshot info, each containing: - - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `fileSizeBytes`: Size of the saved file in bytes - - `width`: Captured image width in pixels - - `height`: Captured image height in pixels - - `coordinateSystem`: `"gameView"` or `"window"` - - `resolutionScale`: Resolution scale used for capture - - `yOffset`: Y offset used for gameView coordinate conversion - - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` 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. +- `ScreenshotCount`: Number of windows captured +- `Screenshots`: Array of screenshot info, each containing: + - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `FileSizeBytes`: Size of the saved file in bytes + - `Width`: Captured image width in pixels + - `Height`: Captured image height in pixels + - `CoordinateSystem`: `"gameView"` or `"window"` + - `ResolutionScale`: Resolution scale used for capture + - `YOffset`: Y offset used for gameView coordinate conversion + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` 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. 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/.agents/skills/uloop-screenshot/references/annotated-elements.md b/.agents/skills/uloop-screenshot/references/annotated-elements.md index 50b899b096..283109ccc2 100644 --- a/.agents/skills/uloop-screenshot/references/annotated-elements.md +++ b/.agents/skills/uloop-screenshot/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## annotatedElements Fields +## AnnotatedElements Fields -`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `name`: Element name -- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates -- `sortingOrder`: Canvas sorting order. Higher values are in front. -- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `Name`: Element name +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates +- `SortingOrder`: Canvas sorting order. Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / resolutionScale -sim_y = image_y / resolutionScale + yOffset +sim_x = image_x / ResolutionScale +sim_y = image_y / ResolutionScale + YOffset ``` -When `resolutionScale` is `1.0`, this simplifies to: +When `ResolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + yOffset +sim_y = image_y + YOffset ``` ## Annotation Readability diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index 7b65d6b135..779c628bc6 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -46,7 +46,7 @@ If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` t For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. -- 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. +- 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. ### KeyDown/KeyUp Rules @@ -85,15 +85,15 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `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 marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 -- `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 +- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `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 marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 +- `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 8bc5b09e69..6ab040f946 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -52,7 +52,7 @@ uloop simulate-mouse-input --action [options] For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the mouse input, not immediately after `simulate-mouse-input`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. ### Global Options (optional) @@ -109,15 +109,15 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `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 +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `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 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 8a9a797b6b..57f779843a 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -69,9 +69,9 @@ uloop simulate-mouse-ui --action --x --y [options] - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions +- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `success = false` +- Dragging on empty space (no draggable UI element) returns `Success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -125,15 +125,15 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `positionX`: Target X coordinate that was used -- `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) +- `Success`: Whether the operation succeeded +- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `PositionX`: Target X coordinate that was used +- `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) Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. -Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. +Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. diff --git a/.agents/skills/uloop-wait-for-pause-point/SKILL.md b/.agents/skills/uloop-wait-for-pause-point/SKILL.md index 14699e0deb..9789b6560b 100644 --- a/.agents/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.agents/skills/uloop-wait-for-pause-point/SKILL.md @@ -26,20 +26,20 @@ uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` 3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: +4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -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. +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. 5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. +6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -48,12 +48,12 @@ Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or st - Use this before reaching for `Time.timeScale`, sleeps, repeated polling, or after-the-fact `execute-dynamic-code`; those checks can supplement the paused-frame proof, but they are not substitutes. - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `Error.Details.RecommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `Id` and `TimeoutSeconds`. Then inspect `Error.Details.Status`, `HitCount`, `Generation`, `EnabledAtUtc`, `EditorState`, `ElapsedSinceEnabledMilliseconds`, and `RemainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `Error.Details.MatchingLogs` and `Error.Details.EvidenceSummary` show whether the marker's focused log ever appeared. `ElapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/.claude/skills/uloop-clear-console/SKILL.md b/.claude/skills/uloop-clear-console/SKILL.md index 3cb9e2be3f..0be2cc80fc 100644 --- a/.claude/skills/uloop-clear-console/SKILL.md +++ b/.claude/skills/uloop-clear-console/SKILL.md @@ -39,10 +39,10 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `success` (boolean): Whether the clear operation succeeded -- `clearedLogCount` (number): Total number of log entries that were cleared -- `clearedCounts` (object): Breakdown by log type - - `errorCount` (number): Errors cleared - - `warningCount` (number): Warnings cleared - - `logCount` (number): Info logs cleared -- `message` (string): Description of the result. On failure, this field carries the error summary. +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result. On failure, this field carries the error summary. diff --git a/.claude/skills/uloop-compile/SKILL.md b/.claude/skills/uloop-compile/SKILL.md index 9a2780a2a1..b504940489 100644 --- a/.claude/skills/uloop-compile/SKILL.md +++ b/.claude/skills/uloop-compile/SKILL.md @@ -44,7 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `success`: boolean or null -- `errorCount`: number or null -- `warningCount`: number or null -- `message`: string +- `Success`: boolean or null +- `ErrorCount`: number or null +- `WarningCount`: number or null +- `Message`: string diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md index bfdb9e9bc9..362405c49f 100644 --- a/.claude/skills/uloop-control-play-mode/SKILL.md +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -49,14 +49,14 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `isPlaying`: Whether Unity is currently in play mode -- `isPaused`: Whether play mode is paused -- `changed`: Whether the requested action changed the current play mode state -- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `message`: Description of the action performed +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Changed`: Whether the requested action changed the current play mode state +- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `Message`: Description of the action performed ## Notes -- Stop on an already-stopped Editor sets `changed: false`, `wasAlreadyStopped: true` +- Stop on an already-stopped Editor sets `Changed: false`, `WasAlreadyStopped: true` - Step is independent of `Time.timeScale`; requires PlayMode to be running - The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index 1b2c08ebf9..c77635307c 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -43,16 +43,16 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: -- `success`: boolean — overall execution success -- `result`: string — value of the snippet's `return` statement (empty when omitted) -- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` -- `error` / `errorMessage`: string — top-level failure summary (empty on success) -- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `diagnosticsSummary`: string|null — compact summary when diagnostics are available -- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it - -On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `Error` / `ErrorMessage`: string — top-level failure summary (empty on success) +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/.claude/skills/uloop-find-game-objects/SKILL.md b/.claude/skills/uloop-find-game-objects/SKILL.md index 24edd909a3..dd4faa9981 100644 --- a/.claude/skills/uloop-find-game-objects/SKILL.md +++ b/.claude/skills/uloop-find-game-objects/SKILL.md @@ -70,21 +70,21 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: -- `results` (array): Matching GameObjects, each containing: - - `name` (string): GameObject name - - `path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) - - `isActive` (boolean): Active state in hierarchy - - `tag` (string): GameObject tag - - `layer` (number): Layer index - - `components` (array): Each entry has `type` (short name, e.g., `Rigidbody`), `fullTypeName` (e.g., `UnityEngine.Rigidbody`), and `properties` (array of Inspector-visible `{name, type, value}` pairs) -- `totalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. -- `errorMessage` (string): Top-level failure summary (empty on success) -- `processingErrors` (array): Selected-mode per-GameObject serialization failures, each `{gameObjectName, gameObjectPath, error}`. Omitted/null or empty on clean runs. +- `Results` (array): Matching GameObjects, each containing: + - `Name` (string): GameObject name + - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) + - `IsActive` (boolean): Active state in hierarchy + - `Tag` (string): GameObject tag + - `Layer` (number): Layer index + - `Components` (array): Each entry has `Type` (short name, e.g., `Rigidbody`), `FullTypeName` (e.g., `UnityEngine.Rigidbody`), and `Properties` (array of Inspector-visible `{Name, Type, Value}` pairs) +- `TotalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. +- `ErrorMessage` (string): Top-level failure summary (empty on success) +- `ProcessingErrors` (array): Selected-mode per-GameObject serialization failures, each `{GameObjectName, GameObjectPath, Error}`. Omitted/null or empty on clean runs. ### Multi-selection file export -For `Selected` mode with **multiple** successfully serialized GameObjects, inline `results` is not populated and the data is written to a file instead. Two extra fields appear: -- `resultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` -- `message` (string): Human-readable summary (e.g., "5 GameObjects exported") +For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: +- `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` +- `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") -Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `results` plus a `message`. +Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. diff --git a/.claude/skills/uloop-focus-window/SKILL.md b/.claude/skills/uloop-focus-window/SKILL.md index e2ca7a23bb..f2c93429bd 100644 --- a/.claude/skills/uloop-focus-window/SKILL.md +++ b/.claude/skills/uloop-focus-window/SKILL.md @@ -29,8 +29,8 @@ uloop focus-window ## Output Returns JSON with: -- `success`: Whether the focus operation succeeded -- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) +- `Success`: Whether the focus operation succeeded +- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) ## Notes diff --git a/.claude/skills/uloop-get-hierarchy/SKILL.md b/.claude/skills/uloop-get-hierarchy/SKILL.md index 4a1ec7cc08..fcd1124c82 100644 --- a/.claude/skills/uloop-get-hierarchy/SKILL.md +++ b/.claude/skills/uloop-get-hierarchy/SKILL.md @@ -56,7 +56,7 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: -- `message` (string): Human-readable guidance pointing at the saved file -- `hierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data +- `Message` (string): Human-readable guidance pointing at the saved file +- `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data -The hierarchy itself is **not** in the response — it is written to the file at `hierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). +The hierarchy itself is **not** in the response — it is written to the file at `HierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). diff --git a/.claude/skills/uloop-get-logs/SKILL.md b/.claude/skills/uloop-get-logs/SKILL.md index 57d1d97f62..d29d1d7881 100644 --- a/.claude/skills/uloop-get-logs/SKILL.md +++ b/.claude/skills/uloop-get-logs/SKILL.md @@ -50,10 +50,10 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `totalCount` (number): Total logs available before max-count clipping -- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) -- Input filters (`logType`, `maxCount`, `searchText`, `includeStackTrace`) are echoed back in the response -- `logs` (array): Each entry has: - - `type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `message` (string): Log message body - - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. +- `TotalCount` (number): Total logs available before max-count clipping +- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) +- Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response +- `Logs` (array): Each entry has: + - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `Message` (string): Log message body + - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/.claude/skills/uloop-launch/SKILL.md b/.claude/skills/uloop-launch/SKILL.md index 7bbee3d3e5..5019a7c014 100644 --- a/.claude/skills/uloop-launch/SKILL.md +++ b/.claude/skills/uloop-launch/SKILL.md @@ -50,18 +50,18 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: -- `success`: whether the command completed -- `ready`: whether Unity CLI Loop is ready for commands -- `serverReady`: whether the Unity CLI Loop server accepted requests -- `projectIpcReady`: whether the project IPC path accepted tool requests -- `alreadyRunning`: whether an existing Unity process was reused -- `launched`: whether this command launched a Unity process -- `restarted`: whether this command stopped an existing process and launched a new one -- `quit`: whether this command stopped Unity without launching a new process -- `previousProcessId`: process ID stopped by restart or quit, when available -- `currentProcessId`: current Unity process ID, when available -- `projectRoot`: resolved project root -- `message`: readiness summary +- `Success`: whether the command completed +- `Ready`: whether Unity CLI Loop is ready for commands +- `ServerReady`: whether the Unity CLI Loop server accepted requests +- `ProjectIpcReady`: whether the project IPC path accepted tool requests +- `AlreadyRunning`: whether an existing Unity process was reused +- `Launched`: whether this command launched a Unity process +- `Restarted`: whether this command stopped an existing process and launched a new one +- `Quit`: whether this command stopped Unity without launching a new process +- `PreviousProcessId`: process ID stopped by restart or quit, when available +- `CurrentProcessId`: current Unity process ID, when available +- `ProjectRoot`: resolved project root +- `Message`: readiness summary ## Notes diff --git a/.claude/skills/uloop-record-input/SKILL.md b/.claude/skills/uloop-record-input/SKILL.md index a76d38cb33..0781fbcf4d 100644 --- a/.claude/skills/uloop-record-input/SKILL.md +++ b/.claude/skills/uloop-record-input/SKILL.md @@ -44,11 +44,11 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start` or `Stop`) -- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -Frame data lives only in the JSON file at `outputPath`; internal metadata fields are not included in the response. +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start` or `Stop`) +- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +Frame data lives only in the JSON file at `OutputPath`; internal metadata fields are not included in the response. diff --git a/.claude/skills/uloop-replay-input/SKILL.md b/.claude/skills/uloop-replay-input/SKILL.md index 81793dbc8e..e159fadec8 100644 --- a/.claude/skills/uloop-replay-input/SKILL.md +++ b/.claude/skills/uloop-replay-input/SKILL.md @@ -48,11 +48,11 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `inputPath`: Path to recording file (nullable string; populated on `Start` only) -- `currentFrame`: Current replay frame index (nullable int) -- `totalFrames`: Total frames in the recording (nullable int) -- `progress`: Replay progress (nullable float in 0.0 – 1.0) -- `isReplaying`: Whether replay is currently active (nullable bool) +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `InputPath`: Path to recording file (nullable string; populated on `Start` only) +- `CurrentFrame`: Current replay frame index (nullable int) +- `TotalFrames`: Total frames in the recording (nullable int) +- `Progress`: Replay progress (nullable float in 0.0 – 1.0) +- `IsReplaying`: Whether replay is currently active (nullable bool) diff --git a/.claude/skills/uloop-run-tests/SKILL.md b/.claude/skills/uloop-run-tests/SKILL.md index 7660586e11..b0a4bf4b74 100644 --- a/.claude/skills/uloop-run-tests/SKILL.md +++ b/.claude/skills/uloop-run-tests/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. Before running tests, run `uloop compile` first if you created, deleted, renamed, moved, or edited C# files, test files, `.asmdef`/`.asmref`, or package manifests since the last successful compile. This refreshes the AssetDatabase and surfaces compile errors before test execution. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. -`NoTestsFound` means zero tests matched — not a test failure. Check `noTestsFoundExplanation` and `message` for asmdef hints. +`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. ## Usage @@ -57,18 +57,18 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `success` (boolean): Whether all tests passed -- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `hasFailures` (boolean): Whether any discovered test failed -- `message` (string): Summary message -- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise -- `completedAt` (string): ISO timestamp when the run finished -- `testCount` (number): Total tests executed -- `passedCount` (number): Passed tests -- `failedCount` (number): Failed tests -- `skippedCount` (number): Skipped tests -- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. +- `Success` (boolean): Whether all tests passed +- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `HasFailures` (boolean): Whether any discovered test failed +- `Message` (string): Summary message +- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise +- `CompletedAt` (string): ISO timestamp when the run finished +- `TestCount` (number): Total tests executed +- `PassedCount` (number): Passed tests +- `FailedCount` (number): Failed tests +- `SkippedCount` (number): Skipped tests +- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File diff --git a/.claude/skills/uloop-screenshot/SKILL.md b/.claude/skills/uloop-screenshot/SKILL.md index bc721a5ee7..fdebaae7a0 100644 --- a/.claude/skills/uloop-screenshot/SKILL.md +++ b/.claude/skills/uloop-screenshot/SKILL.md @@ -72,18 +72,18 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: -- `screenshotCount`: Number of windows captured -- `screenshots`: Array of screenshot info, each containing: - - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `fileSizeBytes`: Size of the saved file in bytes - - `width`: Captured image width in pixels - - `height`: Captured image height in pixels - - `coordinateSystem`: `"gameView"` or `"window"` - - `resolutionScale`: Resolution scale used for capture - - `yOffset`: Y offset used for gameView coordinate conversion - - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` 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. +- `ScreenshotCount`: Number of windows captured +- `Screenshots`: Array of screenshot info, each containing: + - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `FileSizeBytes`: Size of the saved file in bytes + - `Width`: Captured image width in pixels + - `Height`: Captured image height in pixels + - `CoordinateSystem`: `"gameView"` or `"window"` + - `ResolutionScale`: Resolution scale used for capture + - `YOffset`: Y offset used for gameView coordinate conversion + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` 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. 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/references/annotated-elements.md b/.claude/skills/uloop-screenshot/references/annotated-elements.md index 50b899b096..283109ccc2 100644 --- a/.claude/skills/uloop-screenshot/references/annotated-elements.md +++ b/.claude/skills/uloop-screenshot/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## annotatedElements Fields +## AnnotatedElements Fields -`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `name`: Element name -- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates -- `sortingOrder`: Canvas sorting order. Higher values are in front. -- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `Name`: Element name +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates +- `SortingOrder`: Canvas sorting order. Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / resolutionScale -sim_y = image_y / resolutionScale + yOffset +sim_x = image_x / ResolutionScale +sim_y = image_y / ResolutionScale + YOffset ``` -When `resolutionScale` is `1.0`, this simplifies to: +When `ResolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + yOffset +sim_y = image_y + YOffset ``` ## Annotation Readability diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index 7b65d6b135..779c628bc6 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -46,7 +46,7 @@ If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` t For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. -- 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. +- 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. ### KeyDown/KeyUp Rules @@ -85,15 +85,15 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `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 marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 -- `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 +- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `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 marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 +- `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 8bc5b09e69..6ab040f946 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -52,7 +52,7 @@ uloop simulate-mouse-input --action [options] For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the mouse input, not immediately after `simulate-mouse-input`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. ### Global Options (optional) @@ -109,15 +109,15 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `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 +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `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 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 8a9a797b6b..57f779843a 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -69,9 +69,9 @@ uloop simulate-mouse-ui --action --x --y [options] - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions +- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `success = false` +- Dragging on empty space (no draggable UI element) returns `Success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -125,15 +125,15 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `positionX`: Target X coordinate that was used -- `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) +- `Success`: Whether the operation succeeded +- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `PositionX`: Target X coordinate that was used +- `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) Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. -Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. +Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. diff --git a/.claude/skills/uloop-wait-for-pause-point/SKILL.md b/.claude/skills/uloop-wait-for-pause-point/SKILL.md index 14699e0deb..9789b6560b 100644 --- a/.claude/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.claude/skills/uloop-wait-for-pause-point/SKILL.md @@ -26,20 +26,20 @@ uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` 3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: +4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -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. +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. 5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. +6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -48,12 +48,12 @@ Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or st - Use this before reaching for `Time.timeScale`, sleeps, repeated polling, or after-the-fact `execute-dynamic-code`; those checks can supplement the paused-frame proof, but they are not substitutes. - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `Error.Details.RecommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `Id` and `TimeoutSeconds`. Then inspect `Error.Details.Status`, `HitCount`, `Generation`, `EnabledAtUtc`, `EditorState`, `ElapsedSinceEnabledMilliseconds`, and `RemainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `Error.Details.MatchingLogs` and `Error.Details.EvidenceSummary` show whether the marker's focused log ever appeared. `ElapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs b/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs index 9604823c97..305d12dbfd 100644 --- a/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs +++ b/Assets/Editor/FindGameObjects/FindGameObjectsTestMenu.cs @@ -30,17 +30,17 @@ public static async void TestFindGameObjectsCamera() if (response is FindGameObjectsResponse findResponse) { - Debug.Log($"Found {findResponse.totalFound} objects with Camera"); + Debug.Log($"Found {findResponse.TotalFound} objects with Camera"); - foreach (FindGameObjectResult result in findResponse.results) + foreach (FindGameObjectResult result in findResponse.Results) { - Debug.Log($"- {result.name}: {result.components.Length} components"); + Debug.Log($"- {result.Name}: {result.Components.Length} components"); - foreach (ComponentInfo component in result.components) + foreach (ComponentInfo component in result.Components) { - if (component.type == "Camera") + if (component.Type == "Camera") { - Debug.Log($" Camera: {component.properties?.Length ?? 0} properties"); + Debug.Log($" Camera: {component.Properties?.Length ?? 0} properties"); } } } @@ -73,16 +73,16 @@ public static async void TestFindMainCameraByPath() if (response is FindGameObjectsResponse findResponse) { - Debug.Log($"[FindGameObjectsTestMenu] Found {findResponse.totalFound} objects"); + Debug.Log($"[FindGameObjectsTestMenu] Found {findResponse.TotalFound} objects"); - foreach (FindGameObjectResult result in findResponse.results) + foreach (FindGameObjectResult result in findResponse.Results) { - Debug.Log($"[FindGameObjectsTestMenu] - {result.name} at {result.path}"); - Debug.Log($"[FindGameObjectsTestMenu] Components: {result.components.Length}"); + Debug.Log($"[FindGameObjectsTestMenu] - {result.Name} at {result.Path}"); + Debug.Log($"[FindGameObjectsTestMenu] Components: {result.Components.Length}"); - foreach (ComponentInfo component in result.components) + foreach (ComponentInfo component in result.Components) { - Debug.Log($"[FindGameObjectsTestMenu] - {component.type}: {component.properties?.Length ?? 0} properties"); + Debug.Log($"[FindGameObjectsTestMenu] - {component.Type}: {component.Properties?.Length ?? 0} properties"); } } } diff --git a/Assets/Tests/Editor/CompileSessionResultServiceTests.cs b/Assets/Tests/Editor/CompileSessionResultServiceTests.cs index 1dcf606034..dc6254d0c5 100644 --- a/Assets/Tests/Editor/CompileSessionResultServiceTests.cs +++ b/Assets/Tests/Editor/CompileSessionResultServiceTests.cs @@ -171,9 +171,9 @@ public void CreateCompileResult_WhenUnityTestFrameworkSymbolIsMissing_AddsTestAs } [Test] - public void StoreCompileResult_WhenResultIsPersisted_UsesCamelCaseJson() + public void StoreCompileResult_WhenResultIsPersisted_UsesPascalCaseJson() { - // Verifies delayed compile polling reads the same camelCase response contract as immediate tool responses. + // Verifies delayed compile polling reads the same PascalCase response contract as immediate tool responses. UnityCliLoopEditorSessionStateService sessionStateService = UnityCliLoopEditorSessionStateTestFactory.CreateService(); UnityCliLoopEditorSessionStateSnapshot originalSnapshot = @@ -202,9 +202,9 @@ public void StoreCompileResult_WhenResultIsPersisted_UsesCamelCaseJson() UnityCliLoopStoredCompileResult storedResult = sessionStateService.GetCompileResult("compile_test_request"); - Assert.That(storedResult.ResultJson, Does.Contain("\"success\":true")); - Assert.That(storedResult.ResultJson, Does.Contain("\"errorCount\":0")); - Assert.That(storedResult.ResultJson, Does.Not.Contain("\"Success\"")); + Assert.That(storedResult.ResultJson, Does.Contain("\"Success\":true")); + Assert.That(storedResult.ResultJson, Does.Contain("\"ErrorCount\":0")); + Assert.That(storedResult.ResultJson, Does.Not.Contain("\"success\"")); } finally { diff --git a/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs b/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs index ba15d36ea9..5ad5886515 100644 --- a/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs +++ b/Assets/Tests/Editor/CompileStatusBridgeCommandTests.cs @@ -37,7 +37,7 @@ public void BuildResponse_WhenUnityIsIdleAndResultMatches_ReturnsReadyResult() _sessionStateService.StoreCompileResult( "compile_test_request", forceRecompile: false, - resultJson: "{\"success\":true}", + resultJson: "{\"Success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -50,7 +50,7 @@ public void BuildResponse_WhenUnityIsIdleAndResultMatches_ReturnsReadyResult() Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); Assert.That(response.Result, Is.Not.Null); - Assert.That(response.Result["success"]?.Value(), Is.True); + Assert.That(response.Result["Success"]?.Value(), Is.True); } [Test] @@ -60,7 +60,7 @@ public void BuildResponse_WhenUnityIsStillCompiling_ReturnsNotReadyWithStoredRes _sessionStateService.StoreCompileResult( "compile_test_request", forceRecompile: false, - resultJson: "{\"success\":true}", + resultJson: "{\"Success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -82,7 +82,7 @@ public void BuildResponse_WhenRequestIdDiffers_DoesNotReturnStaleResult() _sessionStateService.StoreCompileResult( "compile_old_request", forceRecompile: false, - resultJson: "{\"success\":true}", + resultJson: "{\"Success\":true}", completedAtUtc: System.DateTime.UtcNow); GetCompileStatusResponse response = CompileStatusBridgeCommand.BuildResponse( @@ -138,10 +138,10 @@ public void BuildResponse_WhenUnityIsIdleAndPendingRequestObservedReload_Returns Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); - Assert.That(response.Result["success"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["errorCount"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["warnings"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["message"]?.ToString(), Does.Contain("reloaded scripts")); + Assert.That(response.Result["Success"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["ErrorCount"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["Warnings"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["Message"]?.ToString(), Does.Contain("reloaded scripts")); Assert.That(_sessionStateService.GetPendingCompileRequest().HasRequest, Is.False); } @@ -186,10 +186,10 @@ public void BuildResponse_WhenForceCompilePendingRequestHasNoResult_ReturnsExpla Assert.That(response.Ready, Is.True); Assert.That(response.HasResult, Is.True); - Assert.That(response.Result["success"]?.Type, Is.EqualTo(JTokenType.Null)); - Assert.That(response.Result["errorCount"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["Success"]?.Type, Is.EqualTo(JTokenType.Null)); + Assert.That(response.Result["ErrorCount"]?.Type, Is.EqualTo(JTokenType.Null)); Assert.That( - response.Result["message"]?.ToString(), + response.Result["Message"]?.ToString(), Is.EqualTo(ForceCompileUnknownResult.MessageText)); } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs index 9ecc3b4384..d3a5ef52b7 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeUseCaseTests.cs @@ -40,6 +40,10 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithoutTimings_DoesNotExpos JObject serializedResponse = JObject.Parse( JsonConvert.SerializeObject(response, JsonRpcResponseSerializer.Settings)); + Assert.That(serializedResponse["Success"], Is.Not.Null); + Assert.That(serializedResponse["Result"], Is.Not.Null); + Assert.That(serializedResponse["success"], Is.Null); + Assert.That(serializedResponse["result"], Is.Null); Assert.That(serializedResponse["timings"], Is.Null); Assert.That(serializedResponse["emitTimingsInJsonResponse"], Is.Null); Assert.That(serializedResponse["emitsTimingsInJsonResponse"], Is.Null); @@ -48,9 +52,9 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithoutTimings_DoesNotExpos } [Test] - public void ExecuteDynamicCodeResponse_WhenSerializedWithInternalSignals_KeepsLegacyControlFieldNames() + public void ExecuteDynamicCodeResponse_WhenSerializedWithInternalSignals_KeepsControlFieldNames() { - // Tests that protocol-2 CLIs can still strip internal wait and timing fields from stdout. + // Tests that CLIs can still strip internal wait and timing fields from stdout. ExecuteDynamicCodeResponse response = new() { Success = true, @@ -63,10 +67,12 @@ public void ExecuteDynamicCodeResponse_WhenSerializedWithInternalSignals_KeepsLe JObject serializedResponse = JObject.Parse( JsonConvert.SerializeObject(response, JsonRpcResponseSerializer.Settings)); - Assert.That(serializedResponse["success"], Is.Not.Null); - Assert.That(serializedResponse["result"], Is.Not.Null); + Assert.That(serializedResponse["Success"], Is.Not.Null); + Assert.That(serializedResponse["Result"], Is.Not.Null); Assert.That(serializedResponse["DomainReloadWaitRequired"], Is.Not.Null); Assert.That(serializedResponse["Timings"], Is.Not.Null); + Assert.That(serializedResponse["success"], Is.Null); + Assert.That(serializedResponse["result"], Is.Null); Assert.That(serializedResponse["domainReloadWaitRequired"], Is.Null); Assert.That(serializedResponse["timings"], Is.Null); } diff --git a/Assets/Tests/Editor/FindGameObjectsToolTests.cs b/Assets/Tests/Editor/FindGameObjectsToolTests.cs index 19113db3a8..b2f4ea942e 100644 --- a/Assets/Tests/Editor/FindGameObjectsToolTests.cs +++ b/Assets/Tests/Editor/FindGameObjectsToolTests.cs @@ -62,12 +62,12 @@ public async Task ExecuteAsync_WithNamePattern_FindsMatchingObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.results, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); - Assert.That(response.results.Length, Is.EqualTo(2)); + Assert.That(response.Results, Is.Not.Null); + Assert.That(response.TotalFound, Is.EqualTo(2)); + Assert.That(response.Results.Length, Is.EqualTo(2)); // Check that both TestObject1 and TestObject2 are found - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Not.Contain("AnotherObject")); @@ -85,9 +85,9 @@ public async Task ExecuteAsync_WithEmptyParameters_ReturnsError() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(0)); - Assert.That(response.errorMessage, Is.Not.Null); - Assert.That(response.errorMessage, Does.Contain("At least one search criterion")); + Assert.That(response.TotalFound, Is.EqualTo(0)); + Assert.That(response.ErrorMessage, Is.Not.Null); + Assert.That(response.ErrorMessage, Does.Contain("At least one search criterion")); } [Test] @@ -108,9 +108,9 @@ public async Task ExecuteAsync_WithComponentSearch_FindsObjectsWithSpecificCompo // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.GreaterThanOrEqualTo(2)); // Scene might have other objects with BoxCollider + Assert.That(response.TotalFound, Is.GreaterThanOrEqualTo(2)); // Scene might have other objects with BoxCollider - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("AnotherObject")); Assert.That(foundNames, Does.Not.Contain("TestObject2")); @@ -135,12 +135,12 @@ public async Task ExecuteAsync_WithMultipleComponentSearch_FindsObjectsWithAllCo // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); // Verify components are returned - ComponentInfo boxCollider = System.Array.Find(response.results[0].components, c => c.type == "BoxCollider"); - ComponentInfo rigidbody = System.Array.Find(response.results[0].components, c => c.type == "Rigidbody"); + ComponentInfo boxCollider = System.Array.Find(response.Results[0].Components, c => c.Type == "BoxCollider"); + ComponentInfo rigidbody = System.Array.Find(response.Results[0].Components, c => c.Type == "Rigidbody"); Assert.That(boxCollider, Is.Not.Null); Assert.That(rigidbody, Is.Not.Null); } @@ -167,19 +167,19 @@ public async Task ExecuteAsync_WithTagSearch_FindsObjectsWithSpecificTag() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.GreaterThanOrEqualTo(3)); // At least our 3 test objects + Assert.That(response.TotalFound, Is.GreaterThanOrEqualTo(3)); // At least our 3 test objects - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Contain("AnotherObject")); // Verify tag is returned in results - foreach (var result in response.results) + foreach (var result in response.Results) { - if (result.name == "TestObject1" || result.name == "TestObject2" || result.name == "AnotherObject") + if (result.Name == "TestObject1" || result.Name == "TestObject2" || result.Name == "AnotherObject") { - Assert.That(result.tag, Is.EqualTo("Untagged")); + Assert.That(result.Tag, Is.EqualTo("Untagged")); } } } @@ -203,15 +203,15 @@ public async Task ExecuteAsync_WithLayerSearch_FindsObjectsOnSpecificLayer() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); + Assert.That(response.TotalFound, Is.EqualTo(2)); - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Contain("AnotherObject")); Assert.That(foundNames, Does.Not.Contain("TestObject1")); // Verify layer is returned in results - Assert.That(response.results[0].layer, Is.EqualTo(enemyLayer)); + Assert.That(response.Results[0].Layer, Is.EqualTo(enemyLayer)); } [Test] @@ -235,9 +235,9 @@ public async Task ExecuteAsync_WithRegexSearch_FindsObjectsMatchingPattern() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.GreaterThanOrEqualTo(2)); + Assert.That(response.TotalFound, Is.GreaterThanOrEqualTo(2)); - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("Enemy1")); Assert.That(foundNames, Does.Contain("Enemy2")); Assert.That(foundNames, Does.Not.Contain("Player1")); @@ -271,9 +271,9 @@ public async Task ExecuteAsync_WithIncludeInactive_FindsInactiveObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(3)); // Should find all 3 objects including inactive + Assert.That(response.TotalFound, Is.EqualTo(3)); // Should find all 3 objects including inactive - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObject1")); Assert.That(foundNames, Does.Contain("TestObject2")); Assert.That(foundNames, Does.Contain("AnotherObject")); @@ -299,9 +299,9 @@ public async Task ExecuteAsync_WithoutIncludeInactive_ExcludesInactiveObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); // Should only find active object - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); - Assert.That(response.results[0].isActive, Is.True); + Assert.That(response.TotalFound, Is.EqualTo(1)); // Should only find active object + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); + Assert.That(response.Results[0].IsActive, Is.True); } [Test] @@ -327,13 +327,13 @@ public async Task ExecuteAsync_WithComplexSearch_CombinesMultipleCriteria() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); // Only TestObject2 matches all criteria - Assert.That(response.results[0].name, Is.EqualTo("TestObject2")); + Assert.That(response.TotalFound, Is.EqualTo(1)); // Only TestObject2 matches all criteria + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject2")); // Verify all criteria are met - ComponentInfo boxCollider = System.Array.Find(response.results[0].components, c => c.type == "BoxCollider"); + ComponentInfo boxCollider = System.Array.Find(response.Results[0].Components, c => c.Type == "BoxCollider"); Assert.That(boxCollider, Is.Not.Null); - Assert.That(response.results[0].layer, Is.EqualTo(8)); + Assert.That(response.Results[0].Layer, Is.EqualTo(8)); } [Test] @@ -361,13 +361,13 @@ public async Task ExecuteAsync_WithMaxResults_LimitsReturnedObjects() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.results.Length, Is.EqualTo(5)); // Should be limited to 5 - Assert.That(response.totalFound, Is.EqualTo(5)); // Total found should also be 5 + Assert.That(response.Results.Length, Is.EqualTo(5)); // Should be limited to 5 + Assert.That(response.TotalFound, Is.EqualTo(5)); // Total found should also be 5 // Verify all results match the pattern - foreach (var result in response.results) + foreach (var result in response.Results) { - Assert.That(result.name, Does.StartWith("ManyObject")); + Assert.That(result.Name, Does.StartWith("ManyObject")); } } finally @@ -403,9 +403,9 @@ public async Task ExecuteAsync_WithPathSearchMode_FindsObjectByHierarchyPath() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("Grandchild")); - Assert.That(response.results[0].path, Is.EqualTo("Parent/Child/Grandchild")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("Grandchild")); + Assert.That(response.Results[0].Path, Is.EqualTo("Parent/Child/Grandchild")); } finally { @@ -435,8 +435,8 @@ public async Task ExecuteAsync_WithExactSearchMode_FindsExactNameMatch() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("ExactName")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("ExactName")); } finally { @@ -468,9 +468,9 @@ public async Task ExecuteAsync_WithContainsSearchMode_FindsPartialMatch() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(4)); // Includes SetUp objects (TestObject1, TestObject2) + Assert.That(response.TotalFound, Is.EqualTo(4)); // Includes SetUp objects (TestObject1, TestObject2) - string[] foundNames = System.Array.ConvertAll(response.results, r => r.name); + string[] foundNames = System.Array.ConvertAll(response.Results, r => r.Name); Assert.That(foundNames, Does.Contain("TestObjectOne")); Assert.That(foundNames, Does.Contain("AnotherTestObjectTwo")); Assert.That(foundNames, Does.Not.Contain("DifferentName")); @@ -500,9 +500,9 @@ public async Task ExecuteAsync_WithSelectedMode_NoSelection_ReturnsEmptyResult() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(0)); - Assert.That(response.results, Is.Empty); - Assert.That(response.message, Does.Contain("No GameObjects")); + Assert.That(response.TotalFound, Is.EqualTo(0)); + Assert.That(response.Results, Is.Empty); + Assert.That(response.Message, Does.Contain("No GameObjects")); } [Test] @@ -524,11 +524,11 @@ public async Task ExecuteAsync_WithSelectedMode_SingleSelection_ReturnsJsonDirec // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results, Is.Not.Null); - Assert.That(response.results.Length, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); - Assert.That(response.resultsFilePath, Is.Null); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results, Is.Not.Null); + Assert.That(response.Results.Length, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); + Assert.That(response.ResultsFilePath, Is.Null); } finally { @@ -554,13 +554,13 @@ public async Task ExecuteAsync_WithSelectedMode_MultipleSelection_ExportsToFile( // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); - Assert.That(response.resultsFilePath, Is.Not.Null); - Assert.That(response.resultsFilePath, Does.Contain("FindGameObjectsResults")); - Assert.That(response.message, Does.Contain("Multiple objects selected")); + Assert.That(response.TotalFound, Is.EqualTo(2)); + Assert.That(response.ResultsFilePath, Is.Not.Null); + Assert.That(response.ResultsFilePath, Does.Contain("FindGameObjectsResults")); + Assert.That(response.Message, Does.Contain("Multiple objects selected")); // Verify file exists - string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.resultsFilePath); + string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.ResultsFilePath); Assert.That(File.Exists(fullPath), Is.True, $"Export file should exist at {fullPath}"); // Cleanup exported file @@ -596,10 +596,10 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveFalse_ExcludesIna // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); - Assert.That(response.results, Is.Not.Null); - Assert.That(response.results.Length, Is.EqualTo(1)); - Assert.That(response.results[0].name, Is.EqualTo("TestObject1")); + Assert.That(response.TotalFound, Is.EqualTo(1)); + Assert.That(response.Results, Is.Not.Null); + Assert.That(response.Results.Length, Is.EqualTo(1)); + Assert.That(response.Results[0].Name, Is.EqualTo("TestObject1")); } finally { @@ -630,21 +630,21 @@ public async Task ExecuteAsync_ReturnsObjectReferenceProperties() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); + Assert.That(response.TotalFound, Is.EqualTo(1)); ComponentInfo meshRenderer = System.Array.Find( - response.results[0].components, c => c.type == "MeshRenderer"); + response.Results[0].Components, c => c.Type == "MeshRenderer"); Assert.That(meshRenderer, Is.Not.Null); ComponentPropertyInfo probeAnchor = System.Array.Find( - meshRenderer.properties, p => p.name == "Probe Anchor"); + meshRenderer.Properties, p => p.Name == "Probe Anchor"); Assert.That(probeAnchor, Is.Not.Null, "MeshRenderer should have Probe Anchor property"); - Assert.That(probeAnchor.type, Is.EqualTo("ObjectReference")); + Assert.That(probeAnchor.Type, Is.EqualTo("ObjectReference")); string expectedEntityId = GetExpectedObjectId(anchorTarget.transform); // Value should be a structured object with name, type, entityId - JObject valueObj = JObject.FromObject(probeAnchor.value); + JObject valueObj = JObject.FromObject(probeAnchor.Value); Assert.That(valueObj["name"].ToString(), Is.EqualTo("AnchorTarget")); Assert.That(valueObj["type"].ToString(), Is.EqualTo("Transform")); Assert.That(valueObj["entityId"].ToString(), Is.EqualTo(expectedEntityId)); @@ -672,17 +672,17 @@ public async Task ExecuteAsync_ReturnsNoneForUnsetObjectReference() // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(1)); + Assert.That(response.TotalFound, Is.EqualTo(1)); ComponentInfo meshRenderer = System.Array.Find( - response.results[0].components, c => c.type == "MeshRenderer"); + response.Results[0].Components, c => c.Type == "MeshRenderer"); Assert.That(meshRenderer, Is.Not.Null); ComponentPropertyInfo probeAnchor = System.Array.Find( - meshRenderer.properties, p => p.name == "Probe Anchor"); + meshRenderer.Properties, p => p.Name == "Probe Anchor"); Assert.That(probeAnchor, Is.Not.Null, "MeshRenderer should have Probe Anchor property"); - JObject valueObj = JObject.FromObject(probeAnchor.value); + JObject valueObj = JObject.FromObject(probeAnchor.Value); Assert.That(valueObj["name"].ToString(), Is.EqualTo("None")); Assert.That(valueObj["type"].ToString(), Is.EqualTo("None")); Assert.That(valueObj["entityId"].ToString(), Is.EqualTo("0")); @@ -721,11 +721,11 @@ public async Task ExecuteAsync_WithSelectedMode_IncludeInactiveTrue_IncludesInac // Assert Assert.That(response, Is.Not.Null); - Assert.That(response.totalFound, Is.EqualTo(2)); - Assert.That(response.resultsFilePath, Is.Not.Null); // Multiple selection exports to file + Assert.That(response.TotalFound, Is.EqualTo(2)); + Assert.That(response.ResultsFilePath, Is.Not.Null); // Multiple selection exports to file // Cleanup exported file - string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.resultsFilePath); + string fullPath = Path.Combine(UnityEngine.Application.dataPath, "..", response.ResultsFilePath); if (File.Exists(fullPath)) { File.Delete(fullPath); diff --git a/Assets/Tests/Editor/GetHierarchyToolTests.cs b/Assets/Tests/Editor/GetHierarchyToolTests.cs index 1dde4030ed..967420ab0a 100644 --- a/Assets/Tests/Editor/GetHierarchyToolTests.cs +++ b/Assets/Tests/Editor/GetHierarchyToolTests.cs @@ -38,9 +38,9 @@ public async Task ExecuteAsync_WithDefaultParameters_ReturnsHierarchyExport() GetHierarchyResponse response = baseResponse as GetHierarchyResponse; Assert.That(response, Is.Not.Null); - Assert.That(response.hierarchyFilePath, Is.Not.Empty); - Assert.That(response.message, Does.Contain("Hierarchy data saved")); - DeleteExportedFile(response.hierarchyFilePath); + Assert.That(response.HierarchyFilePath, Is.Not.Empty); + Assert.That(response.Message, Does.Contain("Hierarchy data saved")); + DeleteExportedFile(response.HierarchyFilePath); } [Test] @@ -55,7 +55,7 @@ public async Task ExecuteAsync_WithMaxDepthParameter_MapsRequest() GetHierarchyResponse response = baseResponse as GetHierarchyResponse; Assert.That(response, Is.Not.Null); - DeleteExportedFile(response.hierarchyFilePath); + DeleteExportedFile(response.HierarchyFilePath); } [Test] @@ -70,7 +70,7 @@ public async Task ExecuteAsync_WithIncludeComponentsFalse_MapsRequest() GetHierarchyResponse response = baseResponse as GetHierarchyResponse; Assert.That(response, Is.Not.Null); - DeleteExportedFile(response.hierarchyFilePath); + DeleteExportedFile(response.HierarchyFilePath); } [Test] diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index a99beb808e..b319b77362 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -33,8 +33,8 @@ public async Task ProcessRequest_WhenProtocolVersionMatches_AllowsRequest() Assert.That(parsed["error"], Is.Null); Assert.That(parsed["result"], Is.Not.Null); - Assert.That(parsed["result"]?["unityVersion"]?.ToString(), Is.Not.Null.And.Not.Empty); - Assert.That(parsed["result"]?["UnityVersion"], Is.Null); + Assert.That(parsed["result"]?["UnityVersion"]?.ToString(), Is.Not.Null.And.Not.Empty); + Assert.That(parsed["result"]?["unityVersion"], Is.Null); } [Test] @@ -479,7 +479,7 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs JObject parsedHierarchy = JObject.Parse(hierarchyResponse); Assert.That(parsedHierarchy["error"], Is.Null); - Assert.That(parsedHierarchy["result"]?["hierarchyFilePath"]?.ToString(), Is.Not.Empty); + Assert.That(parsedHierarchy["result"]?["HierarchyFilePath"]?.ToString(), Is.Not.Empty); logsResponseTask = JsonRpcProcessor.ProcessRequestWithEarlyResponseAsync( BuildToolRequestWithParams( @@ -495,7 +495,7 @@ public async Task ProcessRequest_AfterGetHierarchyReturns_AllowsImmediateGetLogs JObject parsedLogs = JObject.Parse(logsResponse); Assert.That(parsedLogs["error"], Is.Null); - Assert.That(parsedLogs["result"]?["displayedCount"]?.ToObject(), Is.EqualTo(0)); + Assert.That(parsedLogs["result"]?["DisplayedCount"]?.ToObject(), Is.EqualTo(0)); } finally { @@ -697,7 +697,7 @@ private static void DeleteHierarchyFileFromResponse(string response) } JObject parsed = JObject.Parse(response); - string relativePath = parsed["result"]?["hierarchyFilePath"]?.ToString(); + string relativePath = parsed["result"]?["HierarchyFilePath"]?.ToString(); if (string.IsNullOrEmpty(relativePath)) { return; diff --git a/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md index e2ca7a23bb..f2c93429bd 100644 --- a/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md @@ -29,8 +29,8 @@ uloop focus-window ## Output Returns JSON with: -- `success`: Whether the focus operation succeeded -- `message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) +- `Success`: Whether the focus operation succeeded +- `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) ## Notes diff --git a/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md index 7bbee3d3e5..5019a7c014 100644 --- a/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md @@ -50,18 +50,18 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: -- `success`: whether the command completed -- `ready`: whether Unity CLI Loop is ready for commands -- `serverReady`: whether the Unity CLI Loop server accepted requests -- `projectIpcReady`: whether the project IPC path accepted tool requests -- `alreadyRunning`: whether an existing Unity process was reused -- `launched`: whether this command launched a Unity process -- `restarted`: whether this command stopped an existing process and launched a new one -- `quit`: whether this command stopped Unity without launching a new process -- `previousProcessId`: process ID stopped by restart or quit, when available -- `currentProcessId`: current Unity process ID, when available -- `projectRoot`: resolved project root -- `message`: readiness summary +- `Success`: whether the command completed +- `Ready`: whether Unity CLI Loop is ready for commands +- `ServerReady`: whether the Unity CLI Loop server accepted requests +- `ProjectIpcReady`: whether the project IPC path accepted tool requests +- `AlreadyRunning`: whether an existing Unity process was reused +- `Launched`: whether this command launched a Unity process +- `Restarted`: whether this command stopped an existing process and launched a new one +- `Quit`: whether this command stopped Unity without launching a new process +- `PreviousProcessId`: process ID stopped by restart or quit, when available +- `CurrentProcessId`: current Unity process ID, when available +- `ProjectRoot`: resolved project root +- `Message`: readiness summary ## Notes diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 14699e0deb..9789b6560b 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -26,20 +26,20 @@ uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` 3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `interruptedByPausePoint=true`: +4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 ``` -The hit response always embeds the log entries matching the marker id as `matchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `matchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. +The hit response always embeds the log entries matching the marker id as `MatchingLogs` (`--matching-logs-max-count` adjusts the limit, default 10), so a separate `get-logs` call while paused is unnecessary. Log embedding is always on; there is no opt-in flag, and a `--include-matching-logs` option no longer exists. An empty `MatchingLogs` array means the fetch succeeded and no matching log exists; if the field is absent, the log fetch itself failed, so fall back to `uloop get-logs --search-text state-transition-applied --max-count 20` while paused. -Read `evidenceSummary` first when it is present. It groups `editorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `editorState.capturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. +Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause point hit metadata, matching-log counts, truncation status, and warnings so you can tell whether the evidence is a single clean hit or needs closer inspection. `EditorState.CapturedAt` names when the Unity Editor play/pause state was observed, such as `PausePointHit`, `Current`, or `ClearAll`. -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. +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. 5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `editorState` describes Unity Editor play/pause state, not marker state. +6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use @@ -48,12 +48,12 @@ Use `generation`, `enabledAtUtc`, and the hit sequence fields from the hit or st - Use this before reaching for `Time.timeScale`, sleeps, repeated polling, or after-the-fact `execute-dynamic-code`; those checks can supplement the paused-frame proof, but they are not substitutes. - If the value you need is a method local, an intermediate calculation, or a branch reason that `execute-dynamic-code` cannot reach, add a focused `Debug.Log` immediately before the marker and read it with `get-logs` while paused. Do not count the breakpoint check complete until the matching log has been read. - Treat the pause like a lightweight breakpoint for one important transition: combine nearby debug logs with paused-frame inspection to confirm the variables and component state at that point. -- Do not treat `simulate-* success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. +- Do not treat `simulate-* Success=true`, generic action logs, sleeps/retries, testing-only counters, or `Time.timeScale` changes as paused-frame proof. - Skip this only for ordinary persistent-state checks when you are not validating simulated input delivery, event ordering, or transition-frame fidelity. ## Timeout Checks -If this command times out, the marker line was not reached while the command waited. Read `error.details.hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `error.details.recommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `id` and `timeoutSeconds`. Then inspect `error.details.status`, `hitCount`, `generation`, `enabledAtUtc`, `editorState`, `elapsedSinceEnabledMilliseconds`, and `remainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `error.details.matchingLogs` and `error.details.evidenceSummary` show whether the marker's focused log ever appeared. `elapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. +If this command times out, the marker line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error carries the same hint and shell-neutral `Error.Details.RecommendedNextAction`: it means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first, so clear and re-enable the marker using the returned `Id` and `TimeoutSeconds`. Then inspect `Error.Details.Status`, `HitCount`, `Generation`, `EnabledAtUtc`, `EditorState`, `ElapsedSinceEnabledMilliseconds`, and `RemainingMilliseconds` to distinguish input not being consumed, stale evidence from an older marker generation, runtime conditions not being met, an id mismatch, or Unity already being paused. On `PAUSE_POINT_WAIT_TIMEOUT`, `Error.Details.MatchingLogs` and `Error.Details.EvidenceSummary` show whether the marker's focused log ever appeared. `ElapsedSinceEnabledMilliseconds` is measured from `enable-pause-point`, not from `wait-for-pause-point`. Use `uloop pause-point-status --id state-transition-applied` only when you need to confirm the marker is armed or inspect the current hit state. Add focused debug logs before the marker when local variables must be captured. diff --git a/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md index 3cb9e2be3f..0be2cc80fc 100644 --- a/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md @@ -39,10 +39,10 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: -- `success` (boolean): Whether the clear operation succeeded -- `clearedLogCount` (number): Total number of log entries that were cleared -- `clearedCounts` (object): Breakdown by log type - - `errorCount` (number): Errors cleared - - `warningCount` (number): Warnings cleared - - `logCount` (number): Info logs cleared -- `message` (string): Description of the result. On failure, this field carries the error summary. +- `Success` (boolean): Whether the clear operation succeeded +- `ClearedLogCount` (number): Total number of log entries that were cleared +- `ClearedCounts` (object): Breakdown by log type + - `ErrorCount` (number): Errors cleared + - `WarningCount` (number): Warnings cleared + - `LogCount` (number): Info logs cleared +- `Message` (string): Description of the result. On failure, this field carries the error summary. diff --git a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md index 9a2780a2a1..b504940489 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md @@ -44,7 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: -- `success`: boolean or null -- `errorCount`: number or null -- `warningCount`: number or null -- `message`: string +- `Success`: boolean or null +- `ErrorCount`: number or null +- `WarningCount`: number or null +- `Message`: string diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md index bfdb9e9bc9..362405c49f 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md @@ -49,14 +49,14 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: -- `isPlaying`: Whether Unity is currently in play mode -- `isPaused`: Whether play mode is paused -- `changed`: Whether the requested action changed the current play mode state -- `wasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped -- `message`: Description of the action performed +- `IsPlaying`: Whether Unity is currently in play mode +- `IsPaused`: Whether play mode is paused +- `Changed`: Whether the requested action changed the current play mode state +- `WasAlreadyStopped`: Whether `Stop` was requested while Play Mode was already stopped +- `Message`: Description of the action performed ## Notes -- Stop on an already-stopped Editor sets `changed: false`, `wasAlreadyStopped: true` +- Stop on an already-stopped Editor sets `Changed: false`, `WasAlreadyStopped: true` - Step is independent of `Time.timeScale`; requires PlayMode to be running - The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index 1b2c08ebf9..c77635307c 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -43,16 +43,16 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: -- `success`: boolean — overall execution success -- `result`: string — value of the snippet's `return` statement (empty when omitted) -- `logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` -- `compilationErrors`: object[] — Roslyn diagnostics with `message`, `line`, `column`, `errorCode`, optional `hint` and `suggestions` -- `error` / `errorMessage`: string — top-level failure summary (empty on success) -- `updatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) -- `diagnosticsSummary`: string|null — compact summary when diagnostics are available -- `diagnostics`: object[] — structured diagnostics; same shape as `compilationErrors`, usually populated together with it - -On `success: false`, inspect `compilationErrors` first. If empty, read `errorMessage` (and `logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `compilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. +- `Success`: boolean — overall execution success +- `Result`: string — value of the snippet's `return` statement (empty when omitted) +- `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` +- `CompilationErrors`: object[] — Roslyn diagnostics with `Message`, `Line`, `Column`, `ErrorCode`, optional `Hint` and `Suggestions` +- `Error` / `ErrorMessage`: string — top-level failure summary (empty on success) +- `UpdatedCode`: string|null — the wrapped form actually compiled (handy when debugging using-statement reordering) +- `DiagnosticsSummary`: string|null — compact summary when diagnostics are available +- `Diagnostics`: object[] — structured diagnostics; same shape as `CompilationErrors`, usually populated together with it + +On `Success: false`, inspect `CompilationErrors` first. If empty, read `ErrorMessage` (and `Logs` for extra context) — the failure may be a runtime exception, cancellation, or an "execution in progress" rejection, all of which return empty `CompilationErrors`. Both EditMode and PlayMode are supported targets — the snippet runs in whichever mode the Editor is currently in. ## Code Examples by Category diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs index c92cdfecea..53a9bfbaa0 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/ComponentInfo.cs @@ -1,4 +1,3 @@ - namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { /// @@ -6,9 +5,11 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// public class ComponentInfo { - public string type { get; set; } - public string fullTypeName { get; set; } - public ComponentPropertyInfo[] properties { get; set; } + public string Type { get; set; } + + public string FullTypeName { get; set; } + + public ComponentPropertyInfo[] Properties { get; set; } } /// @@ -16,8 +17,10 @@ public class ComponentInfo /// public class ComponentPropertyInfo { - public string name { get; set; } - public string type { get; set; } - public object value { get; set; } + public string Name { get; set; } + + public string Type { get; set; } + + public object Value { get; set; } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs index 71aa8a215e..8598207047 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsResponse.cs @@ -7,16 +7,19 @@ namespace io.github.hatayama.UnityCliLoop.FirstPartyTools /// public class FindGameObjectsResponse : UnityCliLoopToolResponse { - public FindGameObjectResult[] results { get; set; } - public int totalFound { get; set; } - public string errorMessage { get; set; } + public FindGameObjectResult[] Results { get; set; } + + public int TotalFound { get; set; } + + public string ErrorMessage { get; set; } // For multiple selection file output - public string resultsFilePath { get; set; } - public string message { get; set; } + public string ResultsFilePath { get; set; } + + public string Message { get; set; } // Processing errors for objects that failed to serialize - public ProcessingError[] processingErrors { get; set; } + public ProcessingError[] ProcessingErrors { get; set; } } /// @@ -24,12 +27,17 @@ public class FindGameObjectsResponse : UnityCliLoopToolResponse /// public class FindGameObjectResult { - public string name { get; set; } - public string path { get; set; } - public bool isActive { get; set; } - public string tag { get; set; } - public int layer { get; set; } - public ComponentInfo[] components { get; set; } + public string Name { get; set; } + + public string Path { get; set; } + + public bool IsActive { get; set; } + + public string Tag { get; set; } + + public int Layer { get; set; } + + public ComponentInfo[] Components { get; set; } } /// @@ -37,8 +45,10 @@ public class FindGameObjectResult /// public class ProcessingError { - public string gameObjectName { get; set; } - public string gameObjectPath { get; set; } - public string error { get; set; } + public string GameObjectName { get; set; } + + public string GameObjectPath { get; set; } + + public string Error { get; set; } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs index e713710b34..52ba54454a 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/FindGameObjectsTool.cs @@ -49,12 +49,12 @@ private static FindGameObjectsResponse ToResponse(UnityCliLoopGameObjectSearchRe return new FindGameObjectsResponse { - results = ToResults(result.Results), - totalFound = result.TotalFound, - errorMessage = result.ErrorMessage, - resultsFilePath = result.ResultsFilePath, - message = result.Message, - processingErrors = ToProcessingErrors(result.ProcessingErrors), + Results = ToResults(result.Results), + TotalFound = result.TotalFound, + ErrorMessage = result.ErrorMessage, + ResultsFilePath = result.ResultsFilePath, + Message = result.Message, + ProcessingErrors = ToProcessingErrors(result.ProcessingErrors), }; } @@ -71,12 +71,12 @@ private static FindGameObjectResult[] ToResults(UnityCliLoopGameObjectResult[] r UnityCliLoopGameObjectResult result = results[i]; mappedResults[i] = new FindGameObjectResult { - name = result.Name, - path = result.Path, - isActive = result.IsActive, - tag = result.Tag, - layer = result.Layer, - components = result.Components, + Name = result.Name, + Path = result.Path, + IsActive = result.IsActive, + Tag = result.Tag, + Layer = result.Layer, + Components = result.Components, }; } @@ -96,9 +96,9 @@ private static ProcessingError[] ToProcessingErrors(UnityCliLoopGameObjectProces UnityCliLoopGameObjectProcessingError error = errors[i]; mappedErrors[i] = new ProcessingError { - gameObjectName = error.GameObjectName, - gameObjectPath = error.GameObjectPath, - error = error.Error, + GameObjectName = error.GameObjectName, + GameObjectPath = error.GameObjectPath, + Error = error.Error, }; } diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs index 3582964113..a4919e17e7 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentPropertySerializer.cs @@ -38,9 +38,9 @@ public ComponentPropertyInfo[] SerializeProperties(Component component) if (value != null) { ComponentPropertyInfo info = new() { - name = iterator.displayName, - type = iterator.propertyType.ToString(), - value = SerializeValue(value) + Name = iterator.displayName, + Type = iterator.propertyType.ToString(), + Value = SerializeValue(value) }; propertyInfos.Add(info); diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs index 0e8b0fb7ef..d7c68bf920 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/GameObjectFinder/ComponentSerializer.cs @@ -35,9 +35,9 @@ public ComponentInfo[] SerializeComponents(GameObject gameObject) ComponentPropertyInfo[] properties = _propertySerializer.SerializeProperties(component); ComponentInfo info = new() { - type = componentTypeName, - fullTypeName = component.GetType().FullName, - properties = properties + Type = componentTypeName, + FullTypeName = component.GetType().FullName, + Properties = properties }; componentInfos.Add(info); @@ -46,4 +46,4 @@ public ComponentInfo[] SerializeComponents(GameObject gameObject) return componentInfos.ToArray(); } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md index 24edd909a3..dd4faa9981 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md @@ -70,21 +70,21 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: -- `results` (array): Matching GameObjects, each containing: - - `name` (string): GameObject name - - `path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) - - `isActive` (boolean): Active state in hierarchy - - `tag` (string): GameObject tag - - `layer` (number): Layer index - - `components` (array): Each entry has `type` (short name, e.g., `Rigidbody`), `fullTypeName` (e.g., `UnityEngine.Rigidbody`), and `properties` (array of Inspector-visible `{name, type, value}` pairs) -- `totalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. -- `errorMessage` (string): Top-level failure summary (empty on success) -- `processingErrors` (array): Selected-mode per-GameObject serialization failures, each `{gameObjectName, gameObjectPath, error}`. Omitted/null or empty on clean runs. +- `Results` (array): Matching GameObjects, each containing: + - `Name` (string): GameObject name + - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) + - `IsActive` (boolean): Active state in hierarchy + - `Tag` (string): GameObject tag + - `Layer` (number): Layer index + - `Components` (array): Each entry has `Type` (short name, e.g., `Rigidbody`), `FullTypeName` (e.g., `UnityEngine.Rigidbody`), and `Properties` (array of Inspector-visible `{Name, Type, Value}` pairs) +- `TotalFound` (number): Results returned (after `--max-results` clipping). For multi-selection file export, this is the number exported. +- `ErrorMessage` (string): Top-level failure summary (empty on success) +- `ProcessingErrors` (array): Selected-mode per-GameObject serialization failures, each `{GameObjectName, GameObjectPath, Error}`. Omitted/null or empty on clean runs. ### Multi-selection file export -For `Selected` mode with **multiple** successfully serialized GameObjects, inline `results` is not populated and the data is written to a file instead. Two extra fields appear: -- `resultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` -- `message` (string): Human-readable summary (e.g., "5 GameObjects exported") +For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: +- `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` +- `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") -Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `results` plus a `message`. +Single-selection and search-mode calls (`Exact`, `Path`, `Regex`, `Contains`) always return inline. No selection (`Selected` mode with empty selection) returns empty `Results` plus a `Message`. diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs b/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs index 0f84b43ab1..c5e80762c7 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/UnityCliLoopGameObjectSearchTypes.cs @@ -1,6 +1,5 @@ using System.Threading; using System.Threading.Tasks; -using Newtonsoft.Json; namespace io.github.hatayama.UnityCliLoop.FirstPartyTools { @@ -56,22 +55,16 @@ public sealed class UnityCliLoopGameObjectSearchResult /// public sealed class UnityCliLoopGameObjectResult { - [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("path")] public string Path { get; set; } - [JsonProperty("isActive")] public bool IsActive { get; set; } - [JsonProperty("tag")] public string Tag { get; set; } - [JsonProperty("layer")] public int Layer { get; set; } - [JsonProperty("components")] public ComponentInfo[] Components { get; set; } } @@ -80,13 +73,10 @@ public sealed class UnityCliLoopGameObjectResult /// public sealed class UnityCliLoopGameObjectProcessingError { - [JsonProperty("gameObjectName")] public string GameObjectName { get; set; } - [JsonProperty("gameObjectPath")] public string GameObjectPath { get; set; } - [JsonProperty("error")] public string Error { get; set; } } } diff --git a/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs b/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs index c92140ed75..ae17d81c7b 100644 --- a/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/GetHierarchy/GetHierarchyResponse.cs @@ -17,20 +17,20 @@ public class GetHierarchyResponse : UnityCliLoopToolResponse /// Human-readable guidance for clients to locate and read the JSON file /// [JsonProperty(Order = -2)] - public string message { get; } + public string Message { get; } /// /// File path where hierarchy data was saved /// [JsonProperty(Order = -1)] - public string hierarchyFilePath { get; } + public string HierarchyFilePath { get; } public GetHierarchyResponse(string filePath, string message = null) { - this.hierarchyFilePath = filePath ?? string.Empty; - this.message = string.IsNullOrEmpty(message) + HierarchyFilePath = filePath ?? string.Empty; + Message = string.IsNullOrEmpty(message) ? "Hierarchy data saved below. Open the JSON to read 'Context' and 'Hierarchy'." : message; } } -} \ No newline at end of file +} diff --git a/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md index 4a1ec7cc08..fcd1124c82 100644 --- a/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md @@ -56,7 +56,7 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: -- `message` (string): Human-readable guidance pointing at the saved file -- `hierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data +- `Message` (string): Human-readable guidance pointing at the saved file +- `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data -The hierarchy itself is **not** in the response — it is written to the file at `hierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). +The hierarchy itself is **not** in the response — it is written to the file at `HierarchyFilePath`. Open that file to read the `Context` and `Hierarchy` payload (GameObject tree, components, etc.). diff --git a/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md index 57d1d97f62..d29d1d7881 100644 --- a/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md @@ -50,10 +50,10 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: -- `totalCount` (number): Total logs available before max-count clipping -- `displayedCount` (number): Logs returned in this response (≤ `--max-count`) -- Input filters (`logType`, `maxCount`, `searchText`, `includeStackTrace`) are echoed back in the response -- `logs` (array): Each entry has: - - `type` (string): `"Error"`, `"Warning"`, or `"Log"` - - `message` (string): Log message body - - `stackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. +- `TotalCount` (number): Total logs available before max-count clipping +- `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) +- Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response +- `Logs` (array): Each entry has: + - `Type` (string): `"Error"`, `"Warning"`, or `"Log"` + - `Message` (string): Log message body + - `StackTrace` (string): Stack trace text. Empty when `--include-stack-trace` is `false`. diff --git a/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md index a76d38cb33..0781fbcf4d 100644 --- a/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md @@ -44,11 +44,11 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start` or `Stop`) -- `outputPath`: Path to saved recording (nullable; populated on `Stop` only) -- `totalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) -- `durationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) - -Frame data lives only in the JSON file at `outputPath`; internal metadata fields are not included in the response. +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start` or `Stop`) +- `OutputPath`: Path to saved recording (nullable; populated on `Stop` only) +- `TotalFrames`: Number of frames recorded (nullable int; populated on `Stop` only) +- `DurationSeconds`: Recording duration in seconds (nullable float; populated on `Stop` only) + +Frame data lives only in the JSON file at `OutputPath`; internal metadata fields are not included in the response. diff --git a/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md index 81793dbc8e..e159fadec8 100644 --- a/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md @@ -48,11 +48,11 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) -- `inputPath`: Path to recording file (nullable string; populated on `Start` only) -- `currentFrame`: Current replay frame index (nullable int) -- `totalFrames`: Total frames in the recording (nullable int) -- `progress`: Replay progress (nullable float in 0.0 – 1.0) -- `isReplaying`: Whether replay is currently active (nullable bool) +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) +- `InputPath`: Path to recording file (nullable string; populated on `Start` only) +- `CurrentFrame`: Current replay frame index (nullable int) +- `TotalFrames`: Total frames in the recording (nullable int) +- `Progress`: Replay progress (nullable float in 0.0 – 1.0) +- `IsReplaying`: Whether replay is currently active (nullable bool) diff --git a/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md index 7660586e11..b0a4bf4b74 100644 --- a/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md @@ -6,13 +6,13 @@ description: "Run Unity Test Runner and report detailed results. Use for EditMod # uloop run-tests -Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `xmlPath` for detailed failure diagnosis. +Execute Unity Test Runner. When tests fail, NUnit XML results with error messages and stack traces are automatically saved. Read the XML file at `XmlPath` for detailed failure diagnosis. Before running tests, run `uloop compile` first if you created, deleted, renamed, moved, or edited C# files, test files, `.asmdef`/`.asmref`, or package manifests since the last successful compile. This refreshes the AssetDatabase and surfaces compile errors before test execution. -Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `success: false`, keeps `testCount` at `0`, lists the unsaved items in `message`, and does not start the Unity Test Runner. +Before executing tests, `uloop run-tests` saves unsaved loaded Scene changes and unsaved current Prefab Stage changes by default. If saving fails, it returns `Success: false`, keeps `TestCount` at `0`, lists the unsaved items in `Message`, and does not start the Unity Test Runner. -`NoTestsFound` means zero tests matched — not a test failure. Check `noTestsFoundExplanation` and `message` for asmdef hints. +`NoTestsFound` means zero tests matched — not a test failure. Check `NoTestsFoundExplanation` and `Message` for asmdef hints. ## Usage @@ -57,18 +57,18 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: -- `success` (boolean): Whether all tests passed -- `status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` -- `hasFailures` (boolean): Whether any discovered test failed -- `message` (string): Summary message -- `noTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests -- `noTestsFoundExplanation` (string): Agent-facing explanation when `noTestsFound` is true; empty otherwise -- `completedAt` (string): ISO timestamp when the run finished -- `testCount` (number): Total tests executed -- `passedCount` (number): Passed tests -- `failedCount` (number): Failed tests -- `skippedCount` (number): Skipped tests -- `xmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `success: true`); populated only when tests failed and the XML file exists on disk. +- `Success` (boolean): Whether all tests passed +- `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` +- `HasFailures` (boolean): Whether any discovered test failed +- `Message` (string): Summary message +- `NoTestsFound` (boolean): Whether Unity Test Runner discovered zero matching tests +- `NoTestsFoundExplanation` (string): Agent-facing explanation when `NoTestsFound` is true; empty otherwise +- `CompletedAt` (string): ISO timestamp when the run finished +- `TestCount` (number): Total tests executed +- `PassedCount` (number): Passed tests +- `FailedCount` (number): Failed tests +- `SkippedCount` (number): Skipped tests +- `XmlPath` (string): Path to NUnit XML result file. Empty string when no XML was saved (typically on `Success: true`); populated only when tests failed and the XML file exists on disk. ### XML Result File diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md index bc721a5ee7..fdebaae7a0 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md @@ -72,18 +72,18 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: -- `screenshotCount`: Number of windows captured -- `screenshots`: Array of screenshot info, each containing: - - `imagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. - - `fileSizeBytes`: Size of the saved file in bytes - - `width`: Captured image width in pixels - - `height`: Captured image height in pixels - - `coordinateSystem`: `"gameView"` or `"window"` - - `resolutionScale`: Resolution scale used for capture - - `yOffset`: Y offset used for gameView coordinate conversion - - `annotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` 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. +- `ScreenshotCount`: Number of windows captured +- `Screenshots`: Array of screenshot info, each containing: + - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. + - `FileSizeBytes`: Size of the saved file in bytes + - `Width`: Captured image width in pixels + - `Height`: Captured image height in pixels + - `CoordinateSystem`: `"gameView"` or `"window"` + - `ResolutionScale`: Resolution scale used for capture + - `YOffset`: Y offset used for gameView coordinate conversion + - `AnnotatedElements`: Array of annotated UI element metadata. Empty unless `--annotate-elements` 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. 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/references/annotated-elements.md b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md index 50b899b096..283109ccc2 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/references/annotated-elements.md @@ -2,34 +2,34 @@ Read this when using `uloop screenshot --capture-mode rendering --annotate-elements` to find coordinates for `simulate-mouse-ui` or `simulate-mouse-input`. -## annotatedElements Fields +## AnnotatedElements Fields -`annotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: +`AnnotatedElements` is empty unless `--annotate-elements` is used. Entries are sorted by z-order, frontmost first. Each item contains: -- `label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. -- `name`: Element name -- `path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. -- `type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) -- `interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. -- `simX`, `simY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. -- `boundsMinX`, `boundsMinY`, `boundsMaxX`, `boundsMaxY`: Bounding box in simulate-mouse coordinates -- `sortingOrder`: Canvas sorting order. Higher values are in front. -- `siblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. +- `Label`: Index label in JSON (`A` = frontmost, `B` = next, ...). Screenshot labels also include the interaction hint, such as `A / CLICK` or `B / DRAG`. +- `Name`: Element name +- `Path`: Hierarchy path from the scene root, for example `Canvas/Panel/Button`. Use this as `simulate-mouse-ui --target-path` when bypassing raycast blockers. +- `Type`: Element type (`Button`, `Toggle`, `Slider`, `Dropdown`, `InputField`, `Scrollbar`, `Draggable`, `DropTarget`, `Selectable`) +- `Interaction`: Derived interaction category (`Click`, `Drag`, `Drop`, `Text`). Use this to choose between `simulate-mouse-ui --action Click` and drag actions. +- `SimX`, `SimY`: Center position in simulate-mouse coordinates. Use these directly with `--x` and `--y`. +- `BoundsMinX`, `BoundsMinY`, `BoundsMaxX`, `BoundsMaxY`: Bounding box in simulate-mouse coordinates +- `SortingOrder`: Canvas sorting order. Higher values are in front. +- `SiblingIndex`: Transform sibling index under the element's direct parent. Do not use it as a reliable z-order signal across nested UI hierarchies. ## Coordinate Conversion -When `coordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: +When `CoordinateSystem` is `"gameView"`, convert image pixel coordinates to simulate-mouse coordinates: ```text -sim_x = image_x / resolutionScale -sim_y = image_y / resolutionScale + yOffset +sim_x = image_x / ResolutionScale +sim_y = image_y / ResolutionScale + YOffset ``` -When `resolutionScale` is `1.0`, this simplifies to: +When `ResolutionScale` is `1.0`, this simplifies to: ```text sim_x = image_x -sim_y = image_y + yOffset +sim_y = image_y + YOffset ``` ## Annotation Readability diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index 7b65d6b135..779c628bc6 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -46,7 +46,7 @@ If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` t For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. -- 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. +- 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. ### KeyDown/KeyUp Rules @@ -85,15 +85,15 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: -- `success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) -- `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 marker id when a `UloopPausePoint.Pause` marker caused the interruption -- `pausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 -- `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 +- `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) +- `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 marker id when a `UloopPausePoint.Pause` marker caused the interruption +- `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 +- `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 8bc5b09e69..6ab040f946 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -52,7 +52,7 @@ uloop simulate-mouse-input --action [options] For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the mouse input, not immediately after `simulate-mouse-input`. -- If `interruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `pausePointId` and `pausePointHitCount` identify the marker. +- If `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was released. `PausePointId` and `PausePointHitCount` identify the marker. - Remove temporary pause-point/log instrumentation before final validation when it was added only for inspection. ### Global Options (optional) @@ -109,15 +109,15 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message -- `action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) -- `button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) -- `positionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) -- `positionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) -- `interruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released -- `pausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption -- `pausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `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 +- `Success`: Whether the operation succeeded +- `Message`: Status message +- `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) +- `Button`: Which button was used (nullable string; populated for `Click` / `LongPress`, null otherwise) +- `PositionX`: Target X coordinate (nullable float; populated for `Click` / `LongPress`) +- `PositionY`: Target Y coordinate (nullable float; populated for `Click` / `LongPress`) +- `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released +- `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption +- `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` +- `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 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 8a9a797b6b..57f779843a 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `annotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof @@ -38,7 +38,7 @@ uloop simulate-mouse-ui --action --x --y [options] | `--duration` | number | `0.5` | Hold duration in seconds for LongPress action. | | `--button` | enum | `Left` | Mouse button. `Click` and `LongPress` support `Left`, `Right`, and `Middle`. Drag actions support `Left` only; other buttons return an error. | | `--bypass-raycast` | flag | - | For `Click`, `LongPress`, `Drag`, and `DragStart`, bypass EventSystem raycast and dispatch pointer events directly to `--target-path`. Use when a raycast-blocking overlay visually covers the intended target. | -| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `annotatedElements[].path` from screenshot JSON. | +| `--target-path` | string | `""` | Hierarchy path of the target GameObject, for example `Canvas/Panel/Button`. Required when `--bypass-raycast` is used with `Click`, `LongPress`, `Drag`, or `DragStart`; prefer `AnnotatedElements[].Path` from screenshot JSON. | | `--drop-target-path` | string | `""` | Optional hierarchy path of a drop target for `Drag` or `DragEnd`, for example `Canvas/DropZone`. Use this when the drop zone is also behind a raycast blocker. | ### Actions @@ -69,9 +69,9 @@ uloop simulate-mouse-ui --action --x --y [options] - Origin is **top-left** (0, 0) - All positions are in **screen pixels** -- Get coordinates from `annotatedElements` JSON (`simX`/`simY`) — do NOT look up GameObject positions +- Get coordinates from `AnnotatedElements` JSON (`SimX`/`SimY`) — do NOT look up GameObject positions - Clicking or long-pressing on empty space (no UI element) still succeeds with a message indicating no element was hit -- Dragging on empty space (no draggable UI element) returns `success = false` +- Dragging on empty space (no draggable UI element) returns `Success = false` - `--bypass-raycast` still uses coordinates for pointer event positions, but chooses the clicked, long-pressed, or dragged GameObject by `--target-path` - If `--target-path` or `--drop-target-path` matches multiple active GameObjects, the command fails instead of choosing an arbitrary duplicate @@ -125,15 +125,15 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: -- `success`: Whether the operation succeeded -- `message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") -- `action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) -- `hitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) -- `positionX`: Target X coordinate that was used -- `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) +- `Success`: Whether the operation succeeded +- `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") +- `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) +- `HitGameObjectName`: Name of the topmost UI element under the pointer (nullable string; null if nothing was hit) +- `PositionX`: Target X coordinate that was used +- `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) Verify the visual outcome with a follow-up `uloop screenshot --capture-mode rendering --annotate-elements`. -Note: Click and LongPress on empty space (no UI element) still return `success = true` with `hitGameObjectName = null`. Drag actions on empty space return `success = false`. +Note: Click and LongPress on empty space (no UI element) still return `Success = true` with `HitGameObjectName = null`. Drag actions on empty space return `Success = false`. diff --git a/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs index 5fc5fbdcb7..4cc577ec33 100644 --- a/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs +++ b/Packages/src/Editor/Infrastructure/Api/CompileStatusBridgeCommand.cs @@ -153,13 +153,13 @@ private static JObject CreateRecoveredCompileResult( return new JObject { - ["success"] = JValue.CreateNull(), - ["errorCount"] = JValue.CreateNull(), - ["warningCount"] = JValue.CreateNull(), - ["errors"] = JValue.CreateNull(), - ["warnings"] = JValue.CreateNull(), - ["message"] = message, - ["projectRoot"] = UnityCliLoopPathResolver.GetProjectRoot() + ["Success"] = JValue.CreateNull(), + ["ErrorCount"] = JValue.CreateNull(), + ["WarningCount"] = JValue.CreateNull(), + ["Errors"] = JValue.CreateNull(), + ["Warnings"] = JValue.CreateNull(), + ["Message"] = message, + ["ProjectRoot"] = UnityCliLoopPathResolver.GetProjectRoot() }; } diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs index a3e4f0598c..2c9e704ff8 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcResponseSerializer.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Infrastructure @@ -11,16 +10,6 @@ public static class JsonRpcResponseSerializer { public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new CamelCaseNamingStrategy - { - // Why: response DTO property names are the public contract, but user-supplied - // dictionary keys can be data and must not be rewritten by the transport layer. - ProcessDictionaryKeys = false, - OverrideSpecifiedNames = false - } - }, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, MaxDepth = UnityCliLoopServerConfig.DEFAULT_JSON_MAX_DEPTH }; diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md index f26aab2fa1..0a9359f292 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md @@ -1,6 +1,6 @@ --- name: v3-cli-invocation-migration -description: Migrate uloop V2 CLI invocations in agent skills, Markdown, POSIX shell scripts, and PowerShell scripts to V3 syntax. Use when updating third-party or first-party uloop command examples, shell automation, SKILL.md files, or JSON output parsing from V2-style boolean arguments, renamed first-party options, removed commands, or PascalCase first-party response fields. +description: Migrate uloop V2 CLI invocations in agent skills, Markdown, POSIX shell scripts, and PowerShell scripts to V3 syntax. Use when updating third-party or first-party uloop command examples, shell automation, or SKILL.md files that use V2-style boolean arguments, renamed first-party options, or removed commands. --- # V3 CLI Invocation Migration @@ -12,7 +12,7 @@ Use this skill to update V2-era `uloop` CLI invocations to V3 syntax in agent-fa 1. Read `references/first-party-v2-to-v3.md` before editing. 2. Search the repository for `uloop` invocations and the V2 names listed in the reference. 3. Inspect nearby context before every edit. Search hits are candidates, not proof. -4. Edit only files that clearly contain V2 `uloop` command syntax or V2 first-party output parsing. +4. Edit only files that clearly contain V2 `uloop` command syntax. 5. Repeat the searches after editing and report any remaining V2 candidates with the reason each one was left unchanged. Prefer `rg` for searches when available. If `rg` is unavailable, use the best available project search tool. Candidate discovery should be repository search plus context inspection, not a generated candidate list. @@ -22,8 +22,6 @@ Prefer `rg` for searches when available. If `rg` is unavailable, use the best av - Convert V2 boolean arguments from `--flag true` or `--flag=false` syntax to V3 flag syntax. - For third-party tools, check the tool's current schema or documentation before deciding whether `false` means removal, a `--no-*` flag, or a renamed option. - For first-party tools, use the reference table. `compile` and `run-tests` have special renamed negative flags. -- Convert V2 PascalCase output field reads to V3 camelCase only when the surrounding command is a `uloop` first-party command. -- Treat helpers and wrappers as context clues, not proof. Trace them far enough to confirm they execute `uloop` and parse its JSON before rewriting their result fields. - Do not edit generated installed skills under `.agents`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or similar target folders unless the user explicitly asks to migrate installed copies. - Do not edit Markdown C# snippets, enum/member references, ordinary DTO/property access, regex match properties, or non-`uloop` JSON. - Do not change protocol versions, release versions, package names, assembly names, or public extension identifiers as part of this migration. @@ -35,4 +33,3 @@ Prefer `rg` for searches when available. If `rg` is unavailable, use the best av - Boolean-looking CLI options: `--* true`, `--*=true`, `--* false`, `--*=false`. - First-party renamed options from the reference, including bare flags. - Removed commands: `get-project-info` and `get-version`. -- PascalCase output fields from the reference, but only where surrounding code parses first-party `uloop` JSON. diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md index a535baaae6..e939265e57 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md @@ -1,6 +1,6 @@ # First-Party V2 to V3 CLI Migration -Use this reference as the canonical migration map. Search results are only candidates. Edit a match only after the surrounding context proves it is a V2 `uloop` invocation or first-party `uloop` JSON output parser. +Use this reference as the canonical migration map. Search results are only candidates. Edit a match only after the surrounding context proves it is a V2 `uloop` invocation. ## Search Checklist @@ -10,7 +10,6 @@ Prefer `rg` when available, but any repository search tool is acceptable. - Search boolean-looking CLI syntax: `--` plus nearby `true` or `false`, including `--flag true`, `--flag=false`, and inline Markdown command examples. - Search renamed first-party option names: `wait-for-domain-reload`, `reload-external-scene-changes`, `force-recompile`, `save-before-run`, `show-overlay`, `include-components`, `include-inactive`, and `compile-only`. - Search removed first-party commands: `get-project-info` and `get-version`. -- Search PascalCase output field names from the table below, then keep only matches that parse JSON produced by first-party `uloop` commands. - Skip generated installed skill copies under `.agents`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or equivalent target folders unless the user explicitly asks to migrate installed copies. ## Boolean Argument Rules @@ -53,63 +52,3 @@ For third-party tools, inspect the current tool schema or docs before choosing t | --- | --- | | `uloop get-project-info` | Replace manually based on caller intent. Do not guess from the command name alone. | | `uloop get-version` | Replace manually based on caller intent. Do not guess from the command name alone. | - -## Output JSON Field Candidates - -V3 first-party CLI output generally uses camelCase JSON fields. Only update these when the surrounding parser is reading first-party `uloop` JSON. Examples that usually qualify include `jq '.Success'`, PowerShell objects produced by `ConvertFrom-Json` from `uloop` output, and local helper functions whose implementation clearly executes `uloop` and returns parsed first-party JSON. - -| V2 field | V3 field | -| --- | --- | -| `Success` | `success` | -| `Message` | `message` | -| `ErrorMessage` | `errorMessage` | -| `ErrorCount` | `errorCount` | -| `WarningCount` | `warningCount` | -| `TotalCount` | `totalCount` | -| `DisplayedCount` | `displayedCount` | -| `LogType` | `logType` | -| `StackTrace` | `stackTrace` | -| `XmlPath` | `xmlPath` | -| `TestCount` | `testCount` | -| `PassedCount` | `passedCount` | -| `FailedCount` | `failedCount` | -| `SkippedCount` | `skippedCount` | -| `CompletedAt` | `completedAt` | -| `ScreenshotCount` | `screenshotCount` | -| `Screenshots` | `screenshots` | -| `CompilationErrors` | `compilationErrors` | -| `ErrorCode` | `errorCode` | -| `UpdatedCode` | `updatedCode` | -| `DiagnosticsSummary` | `diagnosticsSummary` | -| `OutputPath` | `outputPath` | -| `InputPath` | `inputPath` | -| `TotalFrames` | `totalFrames` | -| `DurationSeconds` | `durationSeconds` | -| `CurrentFrame` | `currentFrame` | -| `IsReplaying` | `isReplaying` | -| `KeyName` | `keyName` | -| `PositionX` | `positionX` | -| `PositionY` | `positionY` | -| `EndPositionX` | `endPositionX` | -| `EndPositionY` | `endPositionY` | -| `HitGameObjectName` | `hitGameObjectName` | -| `IsPlaying` | `isPlaying` | -| `IsPaused` | `isPaused` | -| `ClearedLogCount` | `clearedLogCount` | -| `ClearedCounts` | `clearedCounts` | - -## Context Rules - -Migrate output fields when context proves all of these: - -- The JSON comes from a first-party `uloop` command. -- The parser reads command output, not a C# object, regex match, unrelated API response, or sample domain model. -- The PascalCase field belongs to the old CLI response shape, not to code that intentionally documents C# property names. - -Do not migrate these common false positives: - -- C# enum or member references such as `ConnectStatus.Success`. -- C# null-conditional method calls such as `_soundController?.IsPlaying()`. -- Regex or framework result properties such as `$baseMatch.Success` or `match.Success`. -- C# DTOs, tests, serialized model definitions, or ordinary properties named `Success`, `ErrorMessage`, `IsPlaying`, or similar. -- Non-`uloop` JSON parsing, even when it uses the same field names. diff --git a/cli/internal/cli/argument_error.go b/cli/internal/cli/argument_error.go index 5907cd62fb..9b0aecf27f 100644 --- a/cli/internal/cli/argument_error.go +++ b/cli/internal/cli/argument_error.go @@ -19,13 +19,13 @@ func (err *argumentError) toCLIError(context errorContext) cliError { command := firstNonEmpty(err.command, context.command) details := map[string]any{} if err.option != "" { - details["option"] = err.option + details["Option"] = err.option } if err.received != "" { - details["received"] = err.received + details["Received"] = err.received } if err.expectedType != "" { - details["expectedType"] = err.expectedType + details["ExpectedType"] = err.expectedType } nextActions := err.nextActions diff --git a/cli/internal/cli/busy_status.go b/cli/internal/cli/busy_status.go index d9e7a18124..dcbe332c73 100644 --- a/cli/internal/cli/busy_status.go +++ b/cli/internal/cli/busy_status.go @@ -9,12 +9,12 @@ import ( const cliStatusBusy = "Busy" type cliStatusEnvelope struct { - Status string `json:"status"` - Message string `json:"message"` - RunningToolName string `json:"runningToolName,omitempty"` - RequestedToolName string `json:"requestedToolName,omitempty"` - IsPlaying *bool `json:"isPlaying,omitempty"` - IsPaused *bool `json:"isPaused,omitempty"` + Status string `json:"Status"` + Message string `json:"Message"` + RunningToolName string `json:"RunningToolName,omitempty"` + RequestedToolName string `json:"RequestedToolName,omitempty"` + IsPlaying *bool `json:"IsPlaying,omitempty"` + IsPaused *bool `json:"IsPaused,omitempty"` } type serverBusyStatusDetails struct { @@ -40,7 +40,7 @@ func writeBusyStatusEnvelope(writer io.Writer, message string, details serverBus } func serverBusyStatusDetailsFromError(err cliError) serverBusyStatusDetails { - data, ok := err.Details["data"].(map[string]any) + data, ok := err.Details["Data"].(map[string]any) if !ok { return serverBusyStatusDetails{ requestedToolName: err.Command, diff --git a/cli/internal/cli/compile_wait.go b/cli/internal/cli/compile_wait.go index 41a4a0c089..19d9d7116a 100644 --- a/cli/internal/cli/compile_wait.go +++ b/cli/internal/cli/compile_wait.go @@ -32,13 +32,13 @@ type compileCompletionOptions struct { } type compileStatusResponse struct { - Ready bool `json:"ready"` - HasResult bool `json:"hasResult"` - IsCompiling bool `json:"isCompiling"` - IsUpdating bool `json:"isUpdating"` - IsDomainReloadInProgress bool `json:"isDomainReloadInProgress"` - Result json.RawMessage `json:"result"` - Message string `json:"message"` + Ready bool `json:"Ready"` + HasResult bool `json:"HasResult"` + IsCompiling bool `json:"IsCompiling"` + IsUpdating bool `json:"IsUpdating"` + IsDomainReloadInProgress bool `json:"IsDomainReloadInProgress"` + Result json.RawMessage `json:"Result"` + Message string `json:"Message"` } var queryCompileStatus = queryCompileStatusFromUnity @@ -469,12 +469,20 @@ func compileResultLogSummary(result json.RawMessage) compileResultSummary { return compileResultSummary{} } return compileResultSummary{ - success: payload["success"], - errorCount: payload["errorCount"], - warningCount: payload["warningCount"], + success: firstPresentJSONValue(payload, "Success", "success"), + errorCount: firstPresentJSONValue(payload, "ErrorCount", "errorCount"), + warningCount: firstPresentJSONValue(payload, "WarningCount", "warningCount"), } } +func firstPresentJSONValue(payload map[string]any, primaryKey string, legacyKey string) any { + value, ok := payload[primaryKey] + if ok { + return value + } + return payload[legacyKey] +} + func compileStatusLogContext(status compileStatusResponse) map[string]any { return map[string]any{ "ready": status.Ready, diff --git a/cli/internal/cli/compile_wait_test.go b/cli/internal/cli/compile_wait_test.go index 0a699af8b0..deb4ee9f23 100644 --- a/cli/internal/cli/compile_wait_test.go +++ b/cli/internal/cli/compile_wait_test.go @@ -98,20 +98,20 @@ func TestShouldWaitForExecuteDynamicCodeDomainReloadSkipsCompileOnly(t *testing. // Verifies that execute-dynamic-code waits only when Unity explicitly reports a reload signal. func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsResponseSignal(t *testing.T) { - result := []byte(`{"success":true,"domainReloadWaitRequired":true}`) + result := []byte(`{"Success":true,"DomainReloadWaitRequired":true}`) if !executeDynamicCodeDomainReloadWaitRequired(result) { t.Fatal("dynamic-code response should request a reload wait") } - if executeDynamicCodeDomainReloadWaitRequired([]byte(`{"success":true}`)) { + if executeDynamicCodeDomainReloadWaitRequired([]byte(`{"Success":true}`)) { t.Fatal("dynamic-code response without a reload signal should not request a wait") } } -// Verifies that legacy-cased dynamic-code reload signals remain parseable by the CLI. -func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsLegacyResponseSignal(t *testing.T) { - result := []byte(`{"Success":true,"DomainReloadWaitRequired":true}`) +// Verifies that legacy lower-camel dynamic-code reload signals remain parseable by the CLI. +func TestExecuteDynamicCodeDomainReloadWaitRequiredReadsLegacyLowerCamelResponseSignal(t *testing.T) { + result := []byte(`{"success":true,"domainReloadWaitRequired":true}`) if !executeDynamicCodeDomainReloadWaitRequired(result) { t.Fatal("legacy dynamic-code response should request a reload wait") @@ -133,18 +133,18 @@ func TestShouldWaitForExecuteDynamicCodeDisconnectWaitsAfterDispatchedTransportL // Verifies that the CLI does not expose its internal reload-wait response field to users. func TestStripExecuteDynamicCodeControlResultRemovesReloadSignal(t *testing.T) { - result := stripExecuteDynamicCodeControlResult([]byte(`{"success":true,"domainReloadWaitRequired":true}`)) + result := stripExecuteDynamicCodeControlResult([]byte(`{"Success":true,"DomainReloadWaitRequired":true}`)) - if strings.Contains(string(result), "domainReloadWaitRequired") { + if strings.Contains(string(result), "DomainReloadWaitRequired") { t.Fatalf("control field leaked into user output: %s", result) } } -// Verifies that the CLI hides legacy-cased reload-wait response fields from users. -func TestStripExecuteDynamicCodeControlResultRemovesLegacyReloadSignal(t *testing.T) { - result := stripExecuteDynamicCodeControlResult([]byte(`{"success":true,"DomainReloadWaitRequired":true}`)) +// Verifies that the CLI hides legacy lower-camel reload-wait response fields from users. +func TestStripExecuteDynamicCodeControlResultRemovesLegacyLowerCamelReloadSignal(t *testing.T) { + result := stripExecuteDynamicCodeControlResult([]byte(`{"Success":true,"domainReloadWaitRequired":true}`)) - if strings.Contains(string(result), "DomainReloadWaitRequired") { + if strings.Contains(string(result), "domainReloadWaitRequired") { t.Fatalf("legacy control field leaked into user output: %s", result) } } @@ -174,9 +174,9 @@ func TestWaitForCompileCompletionReturnsReadyStatusResult(t *testing.T) { replaceQueryCompileStatus(t, func(context.Context, unityipc.Connection, string) (compileStatusResponse, error) { callCount++ if callCount == 1 { - return compileStatusResponse{Ready: false, HasResult: true, Result: json.RawMessage(`{"success":true}`)}, nil + return compileStatusResponse{Ready: false, HasResult: true, Result: json.RawMessage(`{"Success":true}`)}, nil } - return compileStatusResponse{Ready: true, HasResult: true, Result: json.RawMessage(`{"success":true}`)}, nil + return compileStatusResponse{Ready: true, HasResult: true, Result: json.RawMessage(`{"Success":true}`)}, nil }) result, completed, err := waitForCompileCompletion(context.Background(), compileCompletionOptions{ @@ -191,7 +191,7 @@ func TestWaitForCompileCompletionReturnsReadyStatusResult(t *testing.T) { if !completed { t.Fatal("compile wait did not complete") } - if string(result) != "{\"success\":true}" { + if string(result) != "{\"Success\":true}" { t.Fatalf("result mismatch: %s", result) } } @@ -215,7 +215,7 @@ func TestWaitForCompileCompletionWritesPollLifecycleVibeLogs(t *testing.T) { return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"success":false,"errorCount":2,"warningCount":1}`), + Result: json.RawMessage(`{"Success":false,"ErrorCount":2,"WarningCount":1}`), Message: "Compile result is available.", }, nil }) @@ -232,7 +232,7 @@ func TestWaitForCompileCompletionWritesPollLifecycleVibeLogs(t *testing.T) { if !completed { t.Fatal("compile wait did not complete") } - if string(result) != `{"success":false,"errorCount":2,"warningCount":1}` { + if string(result) != `{"Success":false,"ErrorCount":2,"WarningCount":1}` { t.Fatalf("result mismatch: %s", result) } @@ -266,7 +266,7 @@ func TestWaitForCompileCompletionForceCompileWaitsForStoredResult(t *testing.T) return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"success":null,"errorCount":null,"message":"Force compilation completed"}`), + Result: json.RawMessage(`{"Success":null,"ErrorCount":null,"Message":"Force compilation completed"}`), }, nil }) @@ -291,15 +291,15 @@ func TestWaitForCompileCompletionForceCompileWaitsForStoredResult(t *testing.T) if err := json.Unmarshal(result, &payload); err != nil { t.Fatalf("force compile result is not JSON: %v", err) } - if payload["success"] != nil { - t.Fatalf("force compile success should be unknown: %#v", payload["success"]) + if payload["Success"] != nil { + t.Fatalf("force compile success should be unknown: %#v", payload["Success"]) } - if payload["errorCount"] != nil || payload["warningCount"] != nil { + if payload["ErrorCount"] != nil || payload["WarningCount"] != nil { t.Fatalf("force compile counts should be unknown: %#v", payload) } - message, ok := payload["message"].(string) + message, ok := payload["Message"].(string) if !ok || !strings.Contains(message, "Force compilation completed") { - t.Fatalf("force compile message mismatch: %#v", payload["message"]) + t.Fatalf("force compile message mismatch: %#v", payload["Message"]) } } @@ -448,7 +448,7 @@ func TestRunCompileWithDomainReloadWaitWritesRequestLifecycleVibeLogs(t *testing return compileStatusResponse{ Ready: true, HasResult: true, - Result: json.RawMessage(`{"success":false,"errorCount":1,"warningCount":0}`), + Result: json.RawMessage(`{"Success":false,"ErrorCount":1,"WarningCount":0}`), }, nil }) @@ -523,10 +523,10 @@ func TestShouldWaitForCompileStatusAllowsAcceptedFinalResponseTimeout(t *testing // Verifies compile readiness warmup only runs after confirmed successful results. func TestCompileResultReadinessWaitMode(t *testing.T) { cases := map[string]compileReadinessWaitMode{ - `{"success":true}`: compileReadinessWaitWarmup, - `{"success":false,"errors":[{"message":"boom"}]}`: compileReadinessWaitNone, - `{"success":null,"message":"indeterminate"}`: compileReadinessWaitNone, - `{"message":"indeterminate"}`: compileReadinessWaitNone, + `{"Success":true}`: compileReadinessWaitWarmup, + `{"Success":false,"Errors":[{"Message":"boom"}]}`: compileReadinessWaitNone, + `{"Success":null,"Message":"indeterminate"}`: compileReadinessWaitNone, + `{"Message":"indeterminate"}`: compileReadinessWaitNone, } for result, expected := range cases { diff --git a/cli/internal/cli/control_play_mode_wait.go b/cli/internal/cli/control_play_mode_wait.go index f62d0ad2b3..f811d3a191 100644 --- a/cli/internal/cli/control_play_mode_wait.go +++ b/cli/internal/cli/control_play_mode_wait.go @@ -23,20 +23,20 @@ const ( var controlPlayModeStatePoll = 50 * time.Millisecond type controlPlayModeResponse struct { - IsPlaying bool `json:"isPlaying"` - IsPaused bool `json:"isPaused"` - Changed bool `json:"changed"` - WasAlreadyStopped bool `json:"wasAlreadyStopped"` - BlockedByCompileErrors bool `json:"blockedByCompileErrors"` - CompileErrorCount int `json:"compileErrorCount"` - CompileErrors []controlPlayModeCompileError `json:"compileErrors"` - Message string `json:"message"` + IsPlaying bool `json:"IsPlaying"` + IsPaused bool `json:"IsPaused"` + Changed bool `json:"Changed"` + WasAlreadyStopped bool `json:"WasAlreadyStopped"` + BlockedByCompileErrors bool `json:"BlockedByCompileErrors"` + CompileErrorCount int `json:"CompileErrorCount"` + CompileErrors []controlPlayModeCompileError `json:"CompileErrors"` + Message string `json:"Message"` } type controlPlayModeCompileError struct { - Message string `json:"message"` - File string `json:"file"` - Line int `json:"line"` + Message string `json:"Message"` + File string `json:"File"` + Line int `json:"Line"` } func shouldWaitForControlPlayModeState(command string, params map[string]any) bool { @@ -331,10 +331,10 @@ func controlPlayModeWaitTimeoutError( "Retry `uloop control-play-mode` after Unity finishes compiling, reloading scripts, or entering PlayMode.", }, Details: map[string]any{ - "requestedAction": action, - "isPlaying": response.IsPlaying, - "isPaused": response.IsPaused, - "timeoutSeconds": timeoutSeconds, + "RequestedAction": action, + "IsPlaying": response.IsPlaying, + "IsPaused": response.IsPaused, + "TimeoutSeconds": timeoutSeconds, }, } } @@ -362,10 +362,10 @@ func controlPlayModeCompileErrorsError( "Run `uloop compile` to verify the project compiles, then retry `uloop control-play-mode --action Play`.", }, Details: map[string]any{ - "requestedAction": action, - "compileErrorCount": compileErrorCount, - "compileErrors": response.CompileErrors, - "message": response.Message, + "RequestedAction": action, + "CompileErrorCount": compileErrorCount, + "CompileErrors": response.CompileErrors, + "Message": response.Message, }, } } diff --git a/cli/internal/cli/control_play_mode_wait_test.go b/cli/internal/cli/control_play_mode_wait_test.go index 30b5ad521b..8cd292c01f 100644 --- a/cli/internal/cli/control_play_mode_wait_test.go +++ b/cli/internal/cli/control_play_mode_wait_test.go @@ -36,8 +36,8 @@ func TestRunControlPlayModeWithStateWaitPollsStatusAfterStaleInitialResponse(t * requests, serverErr, []string{ - `{"isPlaying":false,"isPaused":false,"message":"Play mode started"}`, - `{"isPlaying":true,"isPaused":false,"message":"Play mode status"}`, + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode started"}`, + `{"IsPlaying":true,"IsPaused":false,"Message":"Play mode status"}`, }) connection := unityipc.Connection{ @@ -113,8 +113,8 @@ func TestRunControlPlayModeWithStateWaitPreservesStopChangeFields(t *testing.T) make(chan map[string]any, 2), serverErr, []string{ - `{"isPlaying":true,"isPaused":false,"changed":true,"wasAlreadyStopped":false,"message":"Play mode stopped"}`, - `{"isPlaying":false,"isPaused":false,"message":"Play mode status"}`, + `{"IsPlaying":true,"IsPaused":false,"Changed":true,"WasAlreadyStopped":false,"Message":"Play mode stopped"}`, + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode status"}`, }) connection := unityipc.Connection{ @@ -194,7 +194,7 @@ func TestRunControlPlayModeWithStateWaitFailsWhenStateNeverMatches(t *testing.T) go serveRepeatedControlPlayModeResponse( listener, serverErr, - `{"isPlaying":false,"isPaused":false,"message":"Play mode status"}`) + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode status"}`) connection := unityipc.Connection{ Endpoint: unityipc.Endpoint{ @@ -253,7 +253,7 @@ func TestRunControlPlayModeWithStateWaitFailsImmediatelyWhenCompileErrorsBlockPl requests, serverErr, []string{ - `{"isPlaying":false,"isPaused":false,"blockedByCompileErrors":true,"compileErrorCount":1,"compileErrors":[{"message":"CS1002: ; expected","file":"Assets/Scripts/Sample.cs","line":12}],"message":"Play mode could not start because Unity has compiler errors."}`, + `{"IsPlaying":false,"IsPaused":false,"BlockedByCompileErrors":true,"CompileErrorCount":1,"CompileErrors":[{"Message":"CS1002: ; expected","File":"Assets/Scripts/Sample.cs","Line":12}],"Message":"Play mode could not start because Unity has compiler errors."}`, }) connection := unityipc.Connection{ @@ -287,7 +287,7 @@ func TestRunControlPlayModeWithStateWaitFailsImmediatelyWhenCompileErrorsBlockPl if envelope.Error.ErrorCode != errorCodeControlPlayModeCompileErrors { t.Fatalf("error code mismatch: %#v", envelope.Error) } - if envelope.Error.Details["compileErrorCount"] != float64(1) { + if envelope.Error.Details["CompileErrorCount"] != float64(1) { t.Fatalf("compile error count mismatch: %#v", envelope.Error.Details) } if !bytes.Contains(stderr.Bytes(), []byte("CS1002")) { @@ -334,8 +334,8 @@ func TestRunControlPlayModeWithStateWaitFailsWhenCompileErrorsAppearDuringPollin requests, serverErr, []string{ - `{"isPlaying":false,"isPaused":false,"message":"Play mode started"}`, - `{"isPlaying":false,"isPaused":false,"blockedByCompileErrors":true,"compileErrorCount":1,"compileErrors":[{"message":"CS1525: invalid expression","file":"Assets/Scripts/Sample.cs","line":3}],"message":"Play mode could not start because Unity has compiler errors."}`, + `{"IsPlaying":false,"IsPaused":false,"Message":"Play mode started"}`, + `{"IsPlaying":false,"IsPaused":false,"BlockedByCompileErrors":true,"CompileErrorCount":1,"CompileErrors":[{"Message":"CS1525: invalid expression","File":"Assets/Scripts/Sample.cs","Line":3}],"Message":"Play mode could not start because Unity has compiler errors."}`, }) connection := unityipc.Connection{ diff --git a/cli/internal/cli/debug_timing.go b/cli/internal/cli/debug_timing.go index 7193f8fce7..3f172837b1 100644 --- a/cli/internal/cli/debug_timing.go +++ b/cli/internal/cli/debug_timing.go @@ -71,7 +71,7 @@ func extractUnityDebugTimings(command string, result json.RawMessage) []string { } var payload struct { - Timings []string `json:"timings"` + Timings []string `json:"Timings"` } if err := json.Unmarshal(result, &payload); err != nil { return nil diff --git a/cli/internal/cli/debug_timing_test.go b/cli/internal/cli/debug_timing_test.go index 92eb629447..c9160f81fc 100644 --- a/cli/internal/cli/debug_timing_test.go +++ b/cli/internal/cli/debug_timing_test.go @@ -87,7 +87,7 @@ func TestWriteDebugTiming_WhenUnityTimingsExist_ShouldWriteUnityTimingLines(t *t t.Setenv(debugTimingEnvName, "1") var stderr bytes.Buffer outcome := unityipc.UnitySendOutcome{ - Result: []byte(`{"success":true,"timings":["[Perf] Build: 12.3ms","[Perf] Execution: 4.5ms"]}`), + Result: []byte(`{"Success":true,"Timings":["[Perf] Build: 12.3ms","[Perf] Execution: 4.5ms"]}`), } writeDebugTiming(&stderr, executeDynamicCodeCommandName, time.Millisecond, outcome) @@ -108,31 +108,31 @@ func TestStripDebugTimingResult_WhenUnityTimingsExist_ShouldRemoveTimings(t *tes t.Setenv(debugTimingEnvName, "1") result := stripDebugTimingResult( executeDynamicCodeCommandName, - []byte(`{"success":true,"result":"ok","timings":["[Perf] Build: 12.3ms"]}`), + []byte(`{"Success":true,"Result":"ok","Timings":["[Perf] Build: 12.3ms"]}`), ) output := string(result) - if strings.Contains(output, "timings") { + if strings.Contains(output, "Timings") { t.Fatalf("Timings remained in sanitized result: %s", output) } - if !strings.Contains(output, `"result":"ok"`) { + if !strings.Contains(output, `"Result":"ok"`) { t.Fatalf("sanitized result lost normal fields: %s", output) } } -// Verifies that legacy-cased debug-only Unity timings are also removed from JSON stdout. -func TestStripDebugTimingResult_WhenLegacyUnityTimingsExist_ShouldRemoveTimings(t *testing.T) { +// Verifies that legacy lower-camel debug-only Unity timings are also removed from JSON stdout. +func TestStripDebugTimingResult_WhenLegacyLowerCamelUnityTimingsExist_ShouldRemoveTimings(t *testing.T) { t.Setenv(debugTimingEnvName, "1") result := stripDebugTimingResult( executeDynamicCodeCommandName, - []byte(`{"success":true,"result":"ok","Timings":["[Perf] Build: 12.3ms"]}`), + []byte(`{"Success":true,"Result":"ok","timings":["[Perf] Build: 12.3ms"]}`), ) output := string(result) - if strings.Contains(output, "Timings") { + if strings.Contains(output, "timings") { t.Fatalf("legacy Timings remained in sanitized result: %s", output) } - if !strings.Contains(output, `"result":"ok"`) { + if !strings.Contains(output, `"Result":"ok"`) { t.Fatalf("sanitized result lost normal fields: %s", output) } } diff --git a/cli/internal/cli/dynamic_code_wait.go b/cli/internal/cli/dynamic_code_wait.go index f31d0ce960..38dc8bc1f6 100644 --- a/cli/internal/cli/dynamic_code_wait.go +++ b/cli/internal/cli/dynamic_code_wait.go @@ -9,8 +9,8 @@ import ( const ( domainReloadWaitParam = "WaitForDomainReload" dynamicCodeCompileOnlyParam = "CompileOnly" - dynamicCodeDomainReloadWaitRequiredField = "domainReloadWaitRequired" - legacyDynamicCodeDomainReloadWaitRequiredField = "DomainReloadWaitRequired" + dynamicCodeDomainReloadWaitRequiredField = "DomainReloadWaitRequired" + legacyDynamicCodeDomainReloadWaitRequiredField = "domainReloadWaitRequired" ) func shouldWaitForExecuteDynamicCodeDomainReload(command string, params map[string]any) bool { @@ -34,7 +34,7 @@ func domainReloadWaitEnabled(params map[string]any, defaultValue bool) bool { func executeDynamicCodeDomainReloadWaitRequired(result []byte) bool { var payload struct { - DomainReloadWaitRequired bool `json:"domainReloadWaitRequired"` + DomainReloadWaitRequired bool `json:"DomainReloadWaitRequired"` } if err := json.Unmarshal(result, &payload); err != nil { return false diff --git a/cli/internal/cli/error_editor_unresponsive.go b/cli/internal/cli/error_editor_unresponsive.go index a6f94a96c6..ff8a6d9419 100644 --- a/cli/internal/cli/error_editor_unresponsive.go +++ b/cli/internal/cli/error_editor_unresponsive.go @@ -28,8 +28,8 @@ func unityEditorUnresponsiveError(err *unityipc.EditorUnresponsiveError, context "Close the modal dialog or wait for the Editor operation to finish, then retry the command.", }, Details: map[string]any{ - "stallSeconds": err.StallSeconds, - "cause": "Unity Editor main thread did not tick while the IPC heartbeat stayed alive.", + "StallSeconds": err.StallSeconds, + "Cause": "Unity Editor main thread did not tick while the IPC heartbeat stayed alive.", }, } } diff --git a/cli/internal/cli/error_envelope.go b/cli/internal/cli/error_envelope.go index 1f11c698af..289a268e3d 100644 --- a/cli/internal/cli/error_envelope.go +++ b/cli/internal/cli/error_envelope.go @@ -45,15 +45,15 @@ const ( ) type cliError struct { - ErrorCode string `json:"errorCode"` - Phase string `json:"phase"` - Message string `json:"message"` - Retryable bool `json:"retryable"` - SafeToRetry bool `json:"safeToRetry"` - ProjectRoot string `json:"projectRoot,omitempty"` - Command string `json:"command,omitempty"` - NextActions []string `json:"nextActions"` - Details map[string]any `json:"details,omitempty"` + ErrorCode string `json:"ErrorCode"` + Phase string `json:"Phase"` + Message string `json:"Message"` + Retryable bool `json:"Retryable"` + SafeToRetry bool `json:"SafeToRetry"` + ProjectRoot string `json:"ProjectRoot,omitempty"` + Command string `json:"Command,omitempty"` + NextActions []string `json:"NextActions"` + Details map[string]any `json:"Details,omitempty"` } func (err cliError) Error() string { @@ -61,8 +61,8 @@ func (err cliError) Error() string { } type cliErrorEnvelope struct { - Success bool `json:"success"` - Error cliError `json:"error"` + Success bool `json:"Success"` + Error cliError `json:"Error"` } type errorContext struct { @@ -134,7 +134,7 @@ func responseTimeoutAfterAcceptError(err error, context errorContext) cliError { "Retry after Unity finishes the command, compiling, reloading scripts, or restarting the bridge.", }, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, } } @@ -175,8 +175,8 @@ func classifyError(err error, context errorContext) cliError { "Confirm that the command targets the intended Unity project and the Editor package is installed.", }, Details: map[string]any{ - "endpoint": notRespondingErr.endpoint, - "cause": notRespondingErr.causeText(), + "Endpoint": notRespondingErr.endpoint, + "Cause": notRespondingErr.causeText(), }, } } @@ -202,8 +202,8 @@ func classifyError(err error, context errorContext) cliError { "Confirm that the command targets the intended Unity project.", }, Details: map[string]any{ - "endpoint": connectionErr.Endpoint, - "cause": connectionAttemptCause(connectionErr), + "Endpoint": connectionErr.Endpoint, + "Cause": connectionAttemptCause(connectionErr), }, } } @@ -211,19 +211,19 @@ func classifyError(err error, context errorContext) cliError { var rpcErr *unityipc.RPCError if errors.As(err, &rpcErr) { details := map[string]any{ - "code": rpcErr.Code, - "message": rpcErr.Message, + "Code": rpcErr.Code, + "Message": rpcErr.Message, } var decodedData map[string]any if len(rpcErr.Data) > 0 { var data any if json.Unmarshal(rpcErr.Data, &data) == nil { - details["data"] = data + details["Data"] = data if typedData, ok := data.(map[string]any); ok { decodedData = typedData } } else { - details["data"] = string(rpcErr.Data) + details["Data"] = string(rpcErr.Data) } } if rpcDataType(decodedData) == "cli_update_required" { @@ -399,7 +399,7 @@ func disconnectedAfterAcceptError(err error, context errorContext) cliError { "Retry after Unity finishes compiling, reloading scripts, or restarting the bridge.", }, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, } } @@ -418,7 +418,7 @@ func disconnectedAfterDispatchError(err error, context errorContext) cliError { "Retry after Unity finishes compiling, reloading scripts, or restarting the bridge.", }, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, } } @@ -438,8 +438,8 @@ func unityServerNotRespondingAfterDispatchError(err unityServerNotRespondingErro "Run `uloop focus-window` if Unity appears stalled in the background.", }, Details: map[string]any{ - "endpoint": err.endpoint, - "cause": err.causeText(), + "Endpoint": err.endpoint, + "Cause": err.causeText(), }, } } @@ -458,7 +458,7 @@ func unknownCommandError(command string, cache toolsCache, context errorContext) "Run `uloop sync` if the local tool cache may be stale.", }, Details: map[string]any{ - "availableCommands": availableCommandNames(cache), + "AvailableCommands": availableCommandNames(cache), }, } } diff --git a/cli/internal/cli/error_envelope_test.go b/cli/internal/cli/error_envelope_test.go index 630ec34520..b0a7d7f873 100644 --- a/cli/internal/cli/error_envelope_test.go +++ b/cli/internal/cli/error_envelope_test.go @@ -23,9 +23,9 @@ func TestWriteErrorEnvelopeWritesMachineReadableJSON(t *testing.T) { Command: "sample", NextActions: []string{"Pass a valid boolean value for `--enabled`."}, Details: map[string]any{ - "option": "--enabled", - "received": "maybe", - "expectedType": "boolean", + "Option": "--enabled", + "Received": "maybe", + "ExpectedType": "boolean", }, }) @@ -39,7 +39,7 @@ func TestWriteErrorEnvelopeWritesMachineReadableJSON(t *testing.T) { if envelope.Error.ErrorCode != errorCodeInvalidArgument { t.Fatalf("error code mismatch: %#v", envelope.Error) } - if envelope.Error.Details["option"] != "--enabled" { + if envelope.Error.Details["Option"] != "--enabled" { t.Fatalf("details mismatch: %#v", envelope.Error.Details) } } @@ -68,7 +68,7 @@ func TestBuildToolParamsReturnsStructuredBooleanValueError(t *testing.T) { if cliErr.ErrorCode != errorCodeInvalidArgument { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["expectedType"] != "flag" { + if cliErr.Details["ExpectedType"] != "flag" { t.Fatalf("details mismatch: %#v", cliErr.Details) } } @@ -103,7 +103,7 @@ func TestClassifyConnectionAttemptAllowsNilCause(t *testing.T) { if cliErr.ErrorCode != errorCodeUnityNotReachable { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["cause"] != "" { + if cliErr.Details["Cause"] != "" { t.Fatalf("cause should be empty for nil unwrap: %#v", cliErr.Details) } } @@ -135,7 +135,7 @@ func TestClassifyUnityServerNotRespondingError(t *testing.T) { t.Fatalf("next action should not guide restart: %#v", cliErr.NextActions) } } - if cliErr.Details["endpoint"] != "/tmp/uloop/UnityCliLoop-sample.sock" { + if cliErr.Details["Endpoint"] != "/tmp/uloop/UnityCliLoop-sample.sock" { t.Fatalf("details mismatch: %#v", cliErr.Details) } } @@ -157,10 +157,10 @@ func TestClassifyEditorUnresponsiveError(t *testing.T) { if !cliErr.Retryable || !cliErr.SafeToRetry { t.Fatalf("retry flags mismatch: %#v", cliErr) } - if cliErr.Details["stallSeconds"] != float64(321) { + if cliErr.Details["StallSeconds"] != float64(321) { t.Fatalf("stall seconds details mismatch: %#v", cliErr.Details) } - if strings.Contains(cliErr.Details["cause"].(string), "launch -r") { + if strings.Contains(cliErr.Details["Cause"].(string), "launch -r") { t.Fatalf("cause should not include stale restart advice: %#v", cliErr.Details) } joinedActions := strings.Join(cliErr.NextActions, "\n") @@ -194,7 +194,7 @@ func TestClassifyLaunchStartupTimeoutError(t *testing.T) { t.Fatalf("next action should avoid package guesses and launch retry guidance: %#v", cliErr.NextActions) } } - if cliErr.Details["timeoutSeconds"] != 600 { + if cliErr.Details["TimeoutSeconds"] != 600 { t.Fatalf("timeout details mismatch: %#v", cliErr.Details) } } @@ -216,7 +216,7 @@ func TestClassifyLaunchProcessExitTimeoutError(t *testing.T) { if !cliErr.Retryable || !cliErr.SafeToRetry { t.Fatalf("process exit timeout should be retryable: %#v", cliErr) } - if cliErr.Details["pid"] != 123 { + if cliErr.Details["Pid"] != 123 { t.Fatalf("pid details mismatch: %#v", cliErr.Details) } } @@ -263,7 +263,7 @@ func TestClassifyRPCErrorKeepsData(t *testing.T) { if cliErr.ErrorCode != errorCodeUnityRPCError { t.Fatalf("error code mismatch: %#v", cliErr) } - data, ok := cliErr.Details["data"].(map[string]any) + data, ok := cliErr.Details["Data"].(map[string]any) if !ok { t.Fatalf("rpc data missing: %#v", cliErr.Details) } @@ -334,7 +334,7 @@ func TestClassifyServerBusyRPCError(t *testing.T) { if cliErr.Message != expectedMessage { t.Fatalf("message mismatch: %s", cliErr.Message) } - data, ok := cliErr.Details["data"].(map[string]any) + data, ok := cliErr.Details["Data"].(map[string]any) if !ok { t.Fatalf("busy data missing: %#v", cliErr.Details) } @@ -466,7 +466,7 @@ func TestUnknownCommandErrorIncludesAvailableCommands(t *testing.T) { if cliErr.ErrorCode != errorCodeUnknownCommand { t.Fatalf("error code mismatch: %#v", cliErr) } - available, ok := cliErr.Details["availableCommands"].([]string) + available, ok := cliErr.Details["AvailableCommands"].([]string) if !ok { t.Fatalf("available commands missing: %#v", cliErr.Details) } diff --git a/cli/internal/cli/focus.go b/cli/internal/cli/focus.go index 089ffe0fb9..96a02fafbb 100644 --- a/cli/internal/cli/focus.go +++ b/cli/internal/cli/focus.go @@ -31,8 +31,8 @@ type unityProcess struct { } type focusResponse struct { - Success bool `json:"success"` - Message string `json:"message"` + Success bool `json:"Success"` + Message string `json:"Message"` } type restoreFocusFunc func(context.Context) error diff --git a/cli/internal/cli/help_test.go b/cli/internal/cli/help_test.go index 8876556095..1654a82747 100644 --- a/cli/internal/cli/help_test.go +++ b/cli/internal/cli/help_test.go @@ -61,10 +61,10 @@ func TestRunProjectLocalVersionJSONIncludesProtocolVersion(t *testing.T) { if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil { t.Fatalf("version json output is not JSON: %v\n%s", err, stdout.String()) } - if payload["cliVersion"] != version { + if payload["CliVersion"] != version { t.Fatalf("cliVersion mismatch: %#v", payload) } - if payload["protocolVersion"] != float64(protocolVersion) { + if payload["ProtocolVersion"] != float64(protocolVersion) { t.Fatalf("protocolVersion mismatch: %#v", payload) } } diff --git a/cli/internal/cli/install.go b/cli/internal/cli/install.go index 9bcde2e330..bb1fe946d4 100644 --- a/cli/internal/cli/install.go +++ b/cli/internal/cli/install.go @@ -65,10 +65,10 @@ func tryHandleInstallRequest(ctx context.Context, args []string, stdout io.Write func installSetupFailureError(err error, installerStderr string) cliError { details := map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), } if stderrText := strings.TrimSpace(installerStderr); stderrText != "" { - details["installerStderr"] = stderrText + details["InstallerStderr"] = stderrText } return cliError{ diff --git a/cli/internal/cli/install_test.go b/cli/internal/cli/install_test.go index e4ef4aac8c..9cf8e39e01 100644 --- a/cli/internal/cli/install_test.go +++ b/cli/internal/cli/install_test.go @@ -127,10 +127,10 @@ func TestInstallSetupFailureErrorIncludesInstallerStderr(t *testing.T) { if cliErr.ErrorCode != errorCodeInternalError { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["cause"] != context.Canceled.Error() { + if cliErr.Details["Cause"] != context.Canceled.Error() { t.Fatalf("cause detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["installerStderr"] != "warning before failure" { + if cliErr.Details["InstallerStderr"] != "warning before failure" { t.Fatalf("installer stderr detail mismatch: %#v", cliErr.Details) } } diff --git a/cli/internal/cli/launch_process_exit_timeout_error.go b/cli/internal/cli/launch_process_exit_timeout_error.go index 7ffa9a0056..21b05b788a 100644 --- a/cli/internal/cli/launch_process_exit_timeout_error.go +++ b/cli/internal/cli/launch_process_exit_timeout_error.go @@ -30,8 +30,8 @@ func unityProcessExitTimeoutCLIError(err launchProcessExitTimeoutError, context "If Unity remains visible or keeps project files locked, close the Unity process from the OS and retry.", }, Details: map[string]any{ - "pid": err.pid, - "timeoutSeconds": int(err.timeout.Seconds()), + "Pid": err.pid, + "TimeoutSeconds": int(err.timeout.Seconds()), }, } } diff --git a/cli/internal/cli/launch_ready.go b/cli/internal/cli/launch_ready.go index 0f0cfd1046..cb1ecc0d88 100644 --- a/cli/internal/cli/launch_ready.go +++ b/cli/internal/cli/launch_ready.go @@ -15,18 +15,18 @@ const ( ) type launchReadyResponse struct { - Success bool `json:"success"` - Ready bool `json:"ready"` - ServerReady bool `json:"serverReady"` - ProjectIpcReady bool `json:"projectIpcReady"` - AlreadyRunning bool `json:"alreadyRunning"` - Launched bool `json:"launched"` - Restarted bool `json:"restarted"` - Quit bool `json:"quit"` - PreviousProcessId *int `json:"previousProcessId,omitempty"` - CurrentProcessId *int `json:"currentProcessId,omitempty"` - ProjectRoot string `json:"projectRoot"` - Message string `json:"message"` + Success bool `json:"Success"` + Ready bool `json:"Ready"` + ServerReady bool `json:"ServerReady"` + ProjectIpcReady bool `json:"ProjectIpcReady"` + AlreadyRunning bool `json:"AlreadyRunning"` + Launched bool `json:"Launched"` + Restarted bool `json:"Restarted"` + Quit bool `json:"Quit"` + PreviousProcessId *int `json:"PreviousProcessId,omitempty"` + CurrentProcessId *int `json:"CurrentProcessId,omitempty"` + ProjectRoot string `json:"ProjectRoot"` + Message string `json:"Message"` } func writeLaunchReadinessWait(stdout io.Writer, spinner *terminalSpinner) { diff --git a/cli/internal/cli/launch_startup_timeout_error.go b/cli/internal/cli/launch_startup_timeout_error.go index 6c479f3e35..da30583898 100644 --- a/cli/internal/cli/launch_startup_timeout_error.go +++ b/cli/internal/cli/launch_startup_timeout_error.go @@ -43,10 +43,10 @@ func waitForLaunchReadiness(ctx context.Context, projectRoot string) error { func unityStartupTimeoutCLIError(err launchStartupTimeoutError, context errorContext) cliError { projectRoot := firstNonEmpty(context.projectRoot, err.projectRoot) details := map[string]any{ - "timeoutSeconds": int(launchReadinessTimeout.Seconds()), + "TimeoutSeconds": int(launchReadinessTimeout.Seconds()), } if err.cause != nil { - details["cause"] = err.cause.Error() + details["Cause"] = err.cause.Error() } return cliError{ ErrorCode: errorCodeUnityStartupTimeout, diff --git a/cli/internal/cli/launch_test.go b/cli/internal/cli/launch_test.go index c33f9b4d52..1593903eef 100644 --- a/cli/internal/cli/launch_test.go +++ b/cli/internal/cli/launch_test.go @@ -159,11 +159,11 @@ func TestRunLaunchWritesReadyResponseAfterToolReadiness(t *testing.T) { output := stdout.String() for _, expected := range []string{ "Waiting for Unity CLI Loop server readiness...", - `"success": true`, - `"ready": true`, - `"serverReady": true`, - `"projectIpcReady": true`, - `"message": "Unity CLI Loop is ready."`, + `"Success": true`, + `"Ready": true`, + `"ServerReady": true`, + `"ProjectIpcReady": true`, + `"Message": "Unity CLI Loop is ready."`, } { if !strings.Contains(output, expected) { t.Fatalf("launch output missing %q:\n%s", expected, output) diff --git a/cli/internal/cli/list_output.go b/cli/internal/cli/list_output.go index a94a5bc6d9..91c05e6dd4 100644 --- a/cli/internal/cli/list_output.go +++ b/cli/internal/cli/list_output.go @@ -6,24 +6,24 @@ import ( ) type listCatalog struct { - Version string `json:"version,omitempty"` - ServerVersion string `json:"serverVersion,omitempty"` - UpdatedAt string `json:"updatedAt,omitempty"` - Tools []listTool `json:"tools"` + Version string `json:"Version,omitempty"` + ServerVersion string `json:"ServerVersion,omitempty"` + UpdatedAt string `json:"UpdatedAt,omitempty"` + Tools []listTool `json:"Tools"` } type listTool struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - Options []listOption `json:"options"` + Name string `json:"Name"` + Description string `json:"Description,omitempty"` + Options []listOption `json:"Options"` } type listOption struct { - Name string `json:"name"` - Type string `json:"type,omitempty"` - Description string `json:"description,omitempty"` - Default any `json:"default,omitempty"` - Values []string `json:"values,omitempty"` + Name string `json:"Name"` + Type string `json:"Type,omitempty"` + Description string `json:"Description,omitempty"` + Default any `json:"Default,omitempty"` + Values []string `json:"Values,omitempty"` } func formatToolListResult(result json.RawMessage) json.RawMessage { diff --git a/cli/internal/cli/pause_point_errors.go b/cli/internal/cli/pause_point_errors.go index 42769bde25..99017a6d38 100644 --- a/cli/internal/cli/pause_point_errors.go +++ b/cli/internal/cli/pause_point_errors.go @@ -32,7 +32,7 @@ func pausePointWaitError( true) hint := pausePointExpiredHint(response) if hint != "" { - expiredError.Details["hint"] = hint + expiredError.Details["Hint"] = hint } return expiredError case pausePointWaitStateCleared: @@ -53,7 +53,7 @@ func pausePointWaitError( true) hint := pausePointTimeoutHint(response) if hint != "" { - timeoutError.Details["hint"] = hint + timeoutError.Details["Hint"] = hint } return timeoutError } @@ -114,22 +114,22 @@ func pausePointStateError( NextActions: []string{ "Run `uloop enable-pause-point --id ` before waiting.", "Confirm the code path calls `UloopPausePoint.Pause(\"\")` with the same id.", - "Check `details.status`, `details.editorState`, `details.elapsedSinceEnabledMilliseconds`, and `details.remainingMilliseconds` to distinguish a missed code path from an already-paused Editor.", + "Check `Details.Status`, `Details.EditorState`, `Details.ElapsedSinceEnabledMilliseconds`, and `Details.RemainingMilliseconds` to distinguish a missed code path from an already-paused Editor.", "If the marker is inside a custom asmdef, add a reference to `UnityCLILoop.PausePoints.Runtime`.", }, Details: map[string]any{ - "id": options.id, - "status": response.Status, - "expired": response.Expired, - "hitCount": response.HitCount, - "timeoutSeconds": pausePointMarkerTimeoutSeconds(options, response), - "enabledAtUtc": response.EnabledAtUtc, - "elapsedSinceEnabledMilliseconds": response.ElapsedSinceEnabledMilliseconds, - "generation": response.Generation, - "editorState": response.EditorState, - "remainingMilliseconds": pausePointRemainingMilliseconds(options, response), - "markerMessage": response.Message, - "recommendedNextAction": response.RecommendedNextAction, + "Id": options.id, + "Status": response.Status, + "Expired": response.Expired, + "HitCount": response.HitCount, + "TimeoutSeconds": pausePointMarkerTimeoutSeconds(options, response), + "EnabledAtUtc": response.EnabledAtUtc, + "ElapsedSinceEnabledMilliseconds": response.ElapsedSinceEnabledMilliseconds, + "Generation": response.Generation, + "EditorState": response.EditorState, + "RemainingMilliseconds": pausePointRemainingMilliseconds(options, response), + "MarkerMessage": response.Message, + "RecommendedNextAction": response.RecommendedNextAction, }, } } diff --git a/cli/internal/cli/pause_point_logs.go b/cli/internal/cli/pause_point_logs.go index fba9ffd404..8bb57ebc59 100644 --- a/cli/internal/cli/pause_point_logs.go +++ b/cli/internal/cli/pause_point_logs.go @@ -18,60 +18,60 @@ const ( var fetchMatchingLogs = fetchMatchingLogsFromUnity type pausePointMatchingLog struct { - Type string `json:"type"` - Message string `json:"message"` + Type string `json:"Type"` + Message string `json:"Message"` } type pausePointMatchingLogsResult struct { - SearchText string `json:"searchText"` - TotalCount int `json:"totalCount"` - DisplayedCount int `json:"displayedCount"` - MaxCount int `json:"maxCount"` - Logs []pausePointMatchingLog `json:"logs"` + SearchText string `json:"SearchText"` + TotalCount int `json:"TotalCount"` + DisplayedCount int `json:"DisplayedCount"` + MaxCount int `json:"MaxCount"` + Logs []pausePointMatchingLog `json:"Logs"` } type pausePointEvidenceSummary struct { - EditorState pausePointEditorState `json:"editorState"` - PausePoint pausePointEvidencePausePoint `json:"pausePoint"` - MatchingLogs pausePointEvidenceLogs `json:"matchingLogs"` - Warning string `json:"warning"` + EditorState pausePointEditorState `json:"EditorState"` + PausePoint pausePointEvidencePausePoint `json:"PausePoint"` + MatchingLogs pausePointEvidenceLogs `json:"MatchingLogs"` + Warning string `json:"Warning"` } type pausePointEvidencePausePoint struct { - Id string `json:"id"` - Status string `json:"status"` - Generation int `json:"generation"` - HitCount int `json:"hitCount"` - MultipleHitsObserved bool `json:"multipleHitsObserved"` - FirstHitAtUtc string `json:"firstHitAtUtc"` - LastHitAtUtc string `json:"lastHitAtUtc"` - FirstHitSequence int `json:"firstHitSequence"` - LastHitSequence int `json:"lastHitSequence"` + Id string `json:"Id"` + Status string `json:"Status"` + Generation int `json:"Generation"` + HitCount int `json:"HitCount"` + MultipleHitsObserved bool `json:"MultipleHitsObserved"` + FirstHitAtUtc string `json:"FirstHitAtUtc"` + LastHitAtUtc string `json:"LastHitAtUtc"` + FirstHitSequence int `json:"FirstHitSequence"` + LastHitSequence int `json:"LastHitSequence"` } type pausePointEvidenceLogs struct { - SearchText string `json:"searchText"` - MatchingLogCount int `json:"matchingLogCount"` - ReturnedLogCount int `json:"returnedLogCount"` - MaxCount int `json:"maxCount"` - MayBeTruncated bool `json:"mayBeTruncated"` - MultipleMatchingLogsObserved bool `json:"multipleMatchingLogsObserved"` + SearchText string `json:"SearchText"` + MatchingLogCount int `json:"MatchingLogCount"` + ReturnedLogCount int `json:"ReturnedLogCount"` + MaxCount int `json:"MaxCount"` + MayBeTruncated bool `json:"MayBeTruncated"` + MultipleMatchingLogsObserved bool `json:"MultipleMatchingLogsObserved"` } // pausePointWaitResult extends the hit response with marker-matching logs and // evidence summary so agents do not need a separate get-logs call while Unity is paused. type pausePointWaitResult struct { pausePointStatusResponse - MatchingLogs []pausePointMatchingLog `json:"matchingLogs"` - EvidenceSummary pausePointEvidenceSummary `json:"evidenceSummary"` + MatchingLogs []pausePointMatchingLog `json:"MatchingLogs"` + EvidenceSummary pausePointEvidenceSummary `json:"EvidenceSummary"` } type pausePointGetLogsResponse struct { - TotalCount int `json:"totalCount"` - DisplayedCount int `json:"displayedCount"` - MaxCount int `json:"maxCount"` - SearchText string `json:"searchText"` - Logs []pausePointMatchingLog `json:"logs"` + TotalCount int `json:"TotalCount"` + DisplayedCount int `json:"DisplayedCount"` + MaxCount int `json:"MaxCount"` + SearchText string `json:"SearchText"` + Logs []pausePointMatchingLog `json:"Logs"` } func fetchMatchingLogsFromUnity( @@ -173,7 +173,7 @@ func buildPausePointEvidenceWarning(logs pausePointMatchingLogsResult, hitCount if evidenceLogs.MultipleMatchingLogsObserved { warnings = append( warnings, - "Multiple matching logs were observed for this pause point id; inspect matchingLogs before treating the scenario as single-fire evidence.") + "Multiple matching logs were observed for this pause point id; inspect MatchingLogs before treating the scenario as single-fire evidence.") } if hitCount > 1 { warnings = append( diff --git a/cli/internal/cli/pause_point_wait.go b/cli/internal/cli/pause_point_wait.go index c18881b374..487cffad5a 100644 --- a/cli/internal/cli/pause_point_wait.go +++ b/cli/internal/cli/pause_point_wait.go @@ -46,30 +46,30 @@ type pausePointStatusOptions struct { } type pausePointStatusResponse struct { - Id string `json:"id"` - Status string `json:"status"` - IsEnabled bool `json:"isEnabled"` - IsHit bool `json:"isHit"` - HitCount int `json:"hitCount"` - TimeoutSeconds int `json:"timeoutSeconds"` - Expired bool `json:"expired"` - EnabledAtUtc string `json:"enabledAtUtc"` - ElapsedSinceEnabledMilliseconds int64 `json:"elapsedSinceEnabledMilliseconds"` - RemainingMilliseconds int64 `json:"remainingMilliseconds"` - Generation int `json:"generation"` - EditorState pausePointEditorState `json:"editorState"` - FirstHitAtUtc string `json:"firstHitAtUtc"` - LastHitAtUtc string `json:"lastHitAtUtc"` - FirstHitSequence int `json:"firstHitSequence"` - LastHitSequence int `json:"lastHitSequence"` - Message string `json:"message"` - RecommendedNextAction string `json:"recommendedNextAction"` + Id string `json:"Id"` + Status string `json:"Status"` + IsEnabled bool `json:"IsEnabled"` + IsHit bool `json:"IsHit"` + HitCount int `json:"HitCount"` + TimeoutSeconds int `json:"TimeoutSeconds"` + Expired bool `json:"Expired"` + EnabledAtUtc string `json:"EnabledAtUtc"` + ElapsedSinceEnabledMilliseconds int64 `json:"ElapsedSinceEnabledMilliseconds"` + RemainingMilliseconds int64 `json:"RemainingMilliseconds"` + Generation int `json:"Generation"` + EditorState pausePointEditorState `json:"EditorState"` + FirstHitAtUtc string `json:"FirstHitAtUtc"` + LastHitAtUtc string `json:"LastHitAtUtc"` + FirstHitSequence int `json:"FirstHitSequence"` + LastHitSequence int `json:"LastHitSequence"` + Message string `json:"Message"` + RecommendedNextAction string `json:"RecommendedNextAction"` } type pausePointEditorState struct { - IsPlaying bool `json:"isPlaying"` - IsPaused bool `json:"isPaused"` - CapturedAt string `json:"capturedAt"` + IsPlaying bool `json:"IsPlaying"` + IsPaused bool `json:"IsPaused"` + CapturedAt string `json:"CapturedAt"` } type pausePointWaitState string @@ -219,8 +219,8 @@ func runWaitForPausePoint( // Best-effort: the timeout diagnosis must not depend on a second Unity round trip succeeding. logs, logsErr := fetchMatchingLogs(ctx, connection, options.id, options.matchingLogsMaxCount) if logsErr == nil { - waitErr.Details["matchingLogs"] = logs.Logs - waitErr.Details["evidenceSummary"] = buildPausePointEvidenceSummary(response, logs) + waitErr.Details["MatchingLogs"] = logs.Logs + waitErr.Details["EvidenceSummary"] = buildPausePointEvidenceSummary(response, logs) } } writeErrorEnvelope(stderr, waitErr) diff --git a/cli/internal/cli/pause_point_wait_test.go b/cli/internal/cli/pause_point_wait_test.go index 8a349e049e..43eebdad50 100644 --- a/cli/internal/cli/pause_point_wait_test.go +++ b/cli/internal/cli/pause_point_wait_test.go @@ -121,17 +121,17 @@ func TestRunWaitForPausePointClearsEnabledMarkerAfterTimeout(t *testing.T) { t.Fatalf("timeout error missing from stderr: %s", stderr.String()) } envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) - editorState, ok := envelope.Error.Details["editorState"].(map[string]any) - if !ok || editorState["isPlaying"] != true || editorState["isPaused"] != false || editorState["capturedAt"] != "Current" { + editorState, ok := envelope.Error.Details["EditorState"].(map[string]any) + if !ok || editorState["IsPlaying"] != true || editorState["IsPaused"] != false || editorState["CapturedAt"] != "Current" { t.Fatalf("editorState detail mismatch: %#v", envelope.Error.Details) } - if envelope.Error.Details["markerMessage"] != "Pause point enabled." { + if envelope.Error.Details["MarkerMessage"] != "Pause point enabled." { t.Fatalf("markerMessage detail mismatch: %#v", envelope.Error.Details) } - if envelope.Error.Details["elapsedSinceEnabledMilliseconds"] != float64(100) { + if envelope.Error.Details["ElapsedSinceEnabledMilliseconds"] != float64(100) { t.Fatalf("elapsedSinceEnabledMilliseconds detail mismatch: %#v", envelope.Error.Details) } - if envelope.Error.Details["remainingMilliseconds"] != float64(900) { + if envelope.Error.Details["RemainingMilliseconds"] != float64(900) { t.Fatalf("remainingMilliseconds detail mismatch: %#v", envelope.Error.Details) } } @@ -157,16 +157,16 @@ func TestPausePointExpiredErrorReportsRecoveryFields(t *testing.T) { timeoutSeconds: 1, }, response, pausePointWaitStateExpired) - if cliErr.Details["expired"] != true { + if cliErr.Details["Expired"] != true { t.Fatalf("expired detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["enabledAtUtc"] != "2026-06-03T00:00:00.0000000Z" { + if cliErr.Details["EnabledAtUtc"] != "2026-06-03T00:00:00.0000000Z" { t.Fatalf("enabledAtUtc detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["generation"] != 7 { + if cliErr.Details["Generation"] != 7 { t.Fatalf("generation detail mismatch: %#v", cliErr.Details) } - if cliErr.Details["recommendedNextAction"] != response.RecommendedNextAction { + if cliErr.Details["RecommendedNextAction"] != response.RecommendedNextAction { t.Fatalf("recommendedNextAction detail mismatch: %#v", cliErr.Details) } } @@ -186,7 +186,7 @@ func TestPausePointExpiredErrorReportsMarkerTimeoutSeconds(t *testing.T) { timeoutSeconds: 5, }, response, pausePointWaitStateExpired) - if cliErr.Details["timeoutSeconds"] != 30 { + if cliErr.Details["TimeoutSeconds"] != 30 { t.Fatalf("timeoutSeconds detail mismatch: %#v", cliErr.Details) } } @@ -205,7 +205,7 @@ func TestPausePointExpiredErrorDerivesExpiredFromStatus(t *testing.T) { timeoutSeconds: 1, }, response, pausePointWaitStateExpired) - if cliErr.Details["expired"] != true { + if cliErr.Details["Expired"] != true { t.Fatalf("expired detail mismatch: %#v", cliErr.Details) } } @@ -348,11 +348,11 @@ func TestRunWaitForPausePointReportsNotEnabledError(t *testing.T) { if envelope.Error.ErrorCode != errorCodePausePointNotEnabled { t.Fatalf("error code mismatch: %#v", envelope.Error) } - if envelope.Error.Details["status"] != pausePointStatusNotEnabled { + if envelope.Error.Details["Status"] != pausePointStatusNotEnabled { t.Fatalf("status detail mismatch: %#v", envelope.Error.Details) } - editorState, ok := envelope.Error.Details["editorState"].(map[string]any) - if !ok || editorState["isPlaying"] != true || editorState["isPaused"] != false || editorState["capturedAt"] != "Current" { + editorState, ok := envelope.Error.Details["EditorState"].(map[string]any) + if !ok || editorState["IsPlaying"] != true || editorState["IsPaused"] != false || editorState["CapturedAt"] != "Current" { t.Fatalf("editorState details mismatch: %#v", envelope.Error.Details) } } @@ -579,10 +579,10 @@ func TestRunWaitForPausePointIgnoresLogFetchFailure(t *testing.T) { if code != 0 { t.Fatalf("expected success despite log fetch failure, got %d with stderr %s", code, stderr.String()) } - if strings.Contains(stdout.String(), "matchingLogs") { + if strings.Contains(stdout.String(), "MatchingLogs") { t.Fatalf("MatchingLogs must be omitted when the fetch fails: %s", stdout.String()) } - if strings.Contains(stdout.String(), "evidenceSummary") { + if strings.Contains(stdout.String(), "EvidenceSummary") { t.Fatalf("EvidenceSummary must be omitted when the fetch fails: %s", stdout.String()) } } @@ -649,16 +649,16 @@ func TestRunWaitForPausePointEmbedsMatchingLogsOnTimeout(t *testing.T) { } envelope := parsePausePointErrorEnvelope(t, stderr.Bytes()) // The detail key mirrors the hit-response field name, so one spelling covers both surfaces. - matchingLogs, ok := envelope.Error.Details["matchingLogs"].([]any) + matchingLogs, ok := envelope.Error.Details["MatchingLogs"].([]any) if !ok || len(matchingLogs) != 1 { t.Fatalf("MatchingLogs detail mismatch: %#v", envelope.Error.Details) } - evidenceSummary, ok := envelope.Error.Details["evidenceSummary"].(map[string]any) + evidenceSummary, ok := envelope.Error.Details["EvidenceSummary"].(map[string]any) if !ok { t.Fatalf("EvidenceSummary detail missing: %#v", envelope.Error.Details) } - evidenceLogs, ok := evidenceSummary["matchingLogs"].(map[string]any) - if !ok || evidenceLogs["mayBeTruncated"] != true || evidenceLogs["matchingLogCount"] != float64(3) { + evidenceLogs, ok := evidenceSummary["MatchingLogs"].(map[string]any) + if !ok || evidenceLogs["MayBeTruncated"] != true || evidenceLogs["MatchingLogCount"] != float64(3) { t.Fatalf("EvidenceSummary matching logs mismatch: %#v", evidenceSummary) } } @@ -703,7 +703,7 @@ func TestPausePointTimeoutErrorIncludesDiagnosisHint(t *testing.T) { timeoutSeconds: 1, }, testCase.response, pausePointWaitStateTimeout) - if cliErr.Details["hint"] != testCase.wantHint { + if cliErr.Details["Hint"] != testCase.wantHint { t.Fatalf("hint mismatch: %#v", cliErr.Details) } }) @@ -751,7 +751,7 @@ func TestPausePointExpiredErrorIncludesDiagnosisHint(t *testing.T) { timeoutSeconds: 1, }, testCase.response, pausePointWaitStateExpired) - if cliErr.Details["hint"] != testCase.wantHint { + if cliErr.Details["Hint"] != testCase.wantHint { t.Fatalf("hint mismatch: %#v", cliErr.Details) } }) @@ -770,7 +770,7 @@ func TestPausePointHintIsOmittedOutsideDiagnosableStates(t *testing.T) { id: "jump", timeoutSeconds: 1, }, hitResponse, pausePointWaitStateTimeout) - if _, exists := timeoutErr.Details["hint"]; exists { + if _, exists := timeoutErr.Details["Hint"]; exists { t.Fatalf("hint should be omitted when no diagnosis applies: %#v", timeoutErr.Details) } @@ -783,7 +783,7 @@ func TestPausePointHintIsOmittedOutsideDiagnosableStates(t *testing.T) { id: "jump", timeoutSeconds: 1, }, clearedResponse, pausePointWaitStateCleared) - if _, exists := clearedErr.Details["hint"]; exists { + if _, exists := clearedErr.Details["Hint"]; exists { t.Fatalf("hint should be omitted for cleared markers: %#v", clearedErr.Details) } } @@ -807,7 +807,7 @@ func TestPausePointExpiredErrorReportsNoRemainingTime(t *testing.T) { if cliErr.ErrorCode != errorCodePausePointExpired { t.Fatalf("error code mismatch: %#v", cliErr) } - if cliErr.Details["remainingMilliseconds"] != int64(0) { + if cliErr.Details["RemainingMilliseconds"] != int64(0) { t.Fatalf("remainingMilliseconds detail mismatch: %#v", cliErr.Details) } } diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index daf1f6228f..8d776be47f 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -164,8 +164,8 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder func writeVersionJSON(stdout io.Writer) { content, err := json.Marshal(map[string]any{ - "cliVersion": version, - "protocolVersion": protocolVersion, + "CliVersion": version, + "ProtocolVersion": protocolVersion, }) if err != nil { panic(err) @@ -407,7 +407,7 @@ func writeJSON(stdout io.Writer, result json.RawMessage) { } type compileResultStatus struct { - Success *bool `json:"success"` + Success *bool `json:"Success"` } type compileReadinessWaitMode int diff --git a/cli/internal/cli/tool_readiness.go b/cli/internal/cli/tool_readiness.go index b12493eb8f..7122139011 100644 --- a/cli/internal/cli/tool_readiness.go +++ b/cli/internal/cli/tool_readiness.go @@ -147,8 +147,8 @@ func executeDynamicCodeReadinessProbeParams() map[string]any { } type executeDynamicCodeReadinessResponse struct { - Success bool `json:"success"` - ErrorMessage string `json:"errorMessage"` + Success bool `json:"Success"` + ErrorMessage string `json:"ErrorMessage"` } func isExecuteDynamicCodeAvailable(projectRoot string) bool { diff --git a/cli/internal/cli/uninstall.go b/cli/internal/cli/uninstall.go index 150e7d6e58..a8e0693d28 100644 --- a/cli/internal/cli/uninstall.go +++ b/cli/internal/cli/uninstall.go @@ -65,7 +65,7 @@ func tryHandleUninstallRequest(ctx context.Context, args []string, stdout io.Wri Command: uninstallCommandName, NextActions: []string{"Retry `uloop uninstall` after checking file permissions."}, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, }) return true, 1 diff --git a/cli/internal/cli/update.go b/cli/internal/cli/update.go index 8ce2486cbd..bdb341a5c0 100644 --- a/cli/internal/cli/update.go +++ b/cli/internal/cli/update.go @@ -57,7 +57,7 @@ func tryHandleUpdateRequest(ctx context.Context, args []string, stdout io.Writer Command: updateCommandName, NextActions: []string{"Retry `uloop update` after checking network access to GitHub."}, Details: map[string]any{ - "cause": err.Error(), + "Cause": err.Error(), }, }) return true, 1 From cac987b7ed041d0b84a6b76f19463e9b0d2a0df6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 22 Jun 2026 21:53:07 +0900 Subject: [PATCH 2/9] Fix migration skill install detection Treat leftover migration skill directories without SKILL.md as missing so the migration wizard shows Install instead of Remove when the skill is not actually present. Also remove stale AI prompt guidance that still mentioned first-party JSON output field migration. --- ...hirdPartyToolMigrationWizardWindowTests.cs | 1 + .../Editor/ToolSkillSynchronizerTests.cs | 26 +++++++++++++++++++ .../SkillSetup/SkillInstallLayout.cs | 6 +++++ .../ThirdPartyToolMigrationWizardText.cs | 2 +- 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs index 6e09233f74..4729e438f5 100644 --- a/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs +++ b/Assets/Tests/Editor/ThirdPartyToolMigrationWizardWindowTests.cs @@ -190,6 +190,7 @@ public void GetMigrationSkillPromptText_ReturnsActionablePrompt() Assert.That(prompt, Does.Contain("v3-cli-invocation-migration")); Assert.That(prompt, Does.Contain("SKILL.md, Markdown, POSIX shell scripts, and PowerShell scripts")); Assert.That(prompt, Does.Contain("Do not change C# snippets")); + Assert.That(prompt, Does.Not.Contain("JSON output fields")); Assert.That(prompt, Does.Contain("summarize changed files")); } diff --git a/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs b/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs index acbb2383b0..3089bf6652 100644 --- a/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs +++ b/Assets/Tests/Editor/ToolSkillSynchronizerTests.cs @@ -1712,6 +1712,32 @@ await ToolSkillSynchronizer.InstallV3MigrationSkillFilesAtProjectRoot( Assert.That(installState, Is.EqualTo(SkillInstallState.Installed)); } + [Test] + public void GetV3MigrationSkillInstallStateAtProjectRoot_WhenSkillDirectoryLacksSkillFile_ReturnsMissing() + { + // Tests that a leftover migration skill directory without SKILL.md is not treated as installed. + string temporaryRoot = CreateTemporaryProjectRoot(); + ToolSkillSynchronizer.SkillTargetInfo target = new( + "Codex CLI", + ".codex", + "--codex", + hasSkillsDirectory: true, + hasExistingSkills: false); + string targetRoot = Path.Combine(temporaryRoot, ".codex"); + string migrationSkillDir = SkillInstallLayout.GetInstalledSkillDirectoryPathForLayout( + targetRoot, + CliConstants.V3_CLI_INVOCATION_MIGRATION_SKILL_NAME, + groupSkillsUnderUnityCliLoop: false); + Directory.CreateDirectory(Path.Combine(migrationSkillDir, "references")); + + SkillInstallState installState = ToolSkillSynchronizer.GetV3MigrationSkillInstallStateAtProjectRoot( + temporaryRoot, + target, + groupSkillsUnderUnityCliLoop: false); + + Assert.That(installState, Is.EqualTo(SkillInstallState.Missing)); + } + [Test] public async Task RemoveSpecificSkillFilesAtProjectRoot_WhenSkillExists_RemovesOnlyThatSkill() { diff --git a/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs b/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs index 949a448dea..0822db01b1 100644 --- a/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs +++ b/Packages/src/Editor/Infrastructure/SkillSetup/SkillInstallLayout.cs @@ -306,6 +306,12 @@ internal static SkillInstallState GetInstalledStateForSkillSource( return SkillInstallState.Missing; } + string installedSkillFilePath = Path.Combine(installedSkillDirectory, SkillFileName); + if (!File.Exists(installedSkillFilePath)) + { + return SkillInstallState.Missing; + } + return IsSkillDirectoryOutdated(skill.SkillFiles, installedSkillDirectory) ? SkillInstallState.Outdated : SkillInstallState.Installed; diff --git a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs index 36cb526b5b..e60fdf4e63 100644 --- a/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs +++ b/Packages/src/Editor/Presentation/Setup/ThirdPartyToolMigrationWizardText.cs @@ -24,7 +24,7 @@ internal static class ThirdPartyToolMigrationWizardText "for V3.\n\n" + "Scope:\n" + "- Check SKILL.md, Markdown, POSIX shell scripts, and PowerShell scripts.\n" + - "- Migrate V2 boolean arguments, renamed first-party options, removed commands, and first-party JSON output fields.\n" + + "- Migrate V2 boolean arguments, renamed first-party options, and removed commands.\n" + "- Read nearby context before editing. Do not change C# snippets, enum/member references, regex match properties, DTO properties, or non-uloop JSON.\n" + "- After editing, summarize changed files, remaining candidates, and any commands I should verify manually."; internal const string MigrationNotCheckedText = "C# source migration status has not been checked."; From 15e2548eac96fc350d287d19aad271b28a866379 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 22 Jun 2026 22:28:25 +0900 Subject: [PATCH 3/9] Fix E2E parsers for PascalCase CLI JSON Update release and smoke scripts to read the restored PascalCase CLI response fields, and refresh the simulate-mouse-ui skill guidance so agents use the emitted annotated-element property names. --- .../skills/uloop-simulate-mouse-ui/SKILL.md | 2 +- .../skills/uloop-simulate-mouse-ui/SKILL.md | 2 +- .../SimulateMouseUi/Skill/SKILL.md | 2 +- scripts/refresh-neighbor-game-skills-macos.sh | 2 +- .../refresh-neighbor-game-skills-windows.ps1 | 2 +- .../refresh-neighbor-game-skills-windows.sh | 4 +- scripts/run-windows-e2e.ps1 | 14 +-- scripts/smoke-cli-recovery-readiness.go | 4 +- scripts/test-simulate-mouse-demo.ps1 | 86 +++++++++---------- scripts/test-simulate-mouse-demo.sh | 82 +++++++++--------- scripts/test-smoke-cli-recovery-readiness.sh | 10 +-- 11 files changed, 105 insertions(+), 105 deletions(-) diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index 57f779843a..d685835a54 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `AnnotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index 57f779843a..d685835a54 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `AnnotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md index 57f779843a..d685835a54 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -13,7 +13,7 @@ Simulate mouse interaction on Unity PlayMode UI. 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Get UI element info: `uloop screenshot --capture-mode rendering --annotate-elements --elements-only` -3. Use the `AnnotatedElements` array to find the target element by `label`, `name`, or `path` (A=frontmost, B=next, ...). Use `interaction` to distinguish click targets from drag/drop/text targets, then use `simX`/`simY` directly as `--x`/`--y` coordinates. +3. Use the `AnnotatedElements` array to find the target element by `Label`, `Name`, or `Path` (A=frontmost, B=next, ...). Use `Interaction` to distinguish click targets from drag/drop/text targets, then use `SimX`/`SimY` directly as `--x`/`--y` coordinates. 4. Execute the needed `uloop simulate-mouse-ui` commands 5. Inspect the result with the lightest useful evidence: runtime state, logs, or a screenshot 6. When this UI input verifies a state transition, use Pause Point inspection from the section below as the standard frame proof diff --git a/scripts/refresh-neighbor-game-skills-macos.sh b/scripts/refresh-neighbor-game-skills-macos.sh index daa8574e5e..e84b8e0f54 100644 --- a/scripts/refresh-neighbor-game-skills-macos.sh +++ b/scripts/refresh-neighbor-game-skills-macos.sh @@ -393,7 +393,7 @@ return SceneManager.GetActiveScene().path; fail "failed to execute sample scene open command: $project" fi - if ! printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.success == true and .result == $scene_path' >/dev/null; then + if ! printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.Success == true and .Result == $scene_path' >/dev/null; then printf '%s\n' "$response" >&2 fail "failed to open sample scene: $project" fi diff --git a/scripts/refresh-neighbor-game-skills-windows.ps1 b/scripts/refresh-neighbor-game-skills-windows.ps1 index 2139eaf610..e341d36dd0 100644 --- a/scripts/refresh-neighbor-game-skills-windows.ps1 +++ b/scripts/refresh-neighbor-game-skills-windows.ps1 @@ -758,7 +758,7 @@ return SceneManager.GetActiveScene().path; } [pscustomobject]$json = $responseText | ConvertFrom-Json - if ($json.success -eq $true -and $json.result -eq $SampleScenePath) { + if ($json.Success -eq $true -and $json.Result -eq $SampleScenePath) { return } diff --git a/scripts/refresh-neighbor-game-skills-windows.sh b/scripts/refresh-neighbor-game-skills-windows.sh index b28940df24..7725bc7a91 100644 --- a/scripts/refresh-neighbor-game-skills-windows.sh +++ b/scripts/refresh-neighbor-game-skills-windows.sh @@ -592,7 +592,7 @@ return SceneManager.GetActiveScene().path; response_matches_scene() { response=$1 if command -v jq >/dev/null 2>&1; then - printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.success == true and .result == $scene_path' >/dev/null + printf '%s\n' "$response" | jq -e --arg scene_path "$sample_scene_path" '.Success == true and .Result == $scene_path' >/dev/null return $? fi @@ -606,7 +606,7 @@ try { } catch { exit 1 } -if (\$Value.success -eq \$true -and \$Value.result -eq \$ScenePath) { +if (\$Value.Success -eq \$true -and \$Value.Result -eq \$ScenePath) { exit 0 } exit 1 diff --git a/scripts/run-windows-e2e.ps1 b/scripts/run-windows-e2e.ps1 index 557970296c..5664c7ac5e 100644 --- a/scripts/run-windows-e2e.ps1 +++ b/scripts/run-windows-e2e.ps1 @@ -108,7 +108,7 @@ function Invoke-UloopJsonChecked { [string]$text = Invoke-UloopChecked -CommandArguments $CommandArguments [pscustomobject]$json = $text | ConvertFrom-Json - if ($json.PSObject.Properties.Name -contains "success" -and $json.success -ne $true) { + if ($json.PSObject.Properties.Name -contains "Success" -and $json.Success -ne $true) { throw "uloop $($CommandArguments -join " ") returned success=false: $text" } @@ -172,11 +172,11 @@ function Wait-PlayMode { [pscustomobject]$result = $probe.Text | ConvertFrom-Json [string]$isPlaying = "" - if ($null -ne $result.result) { - $isPlaying = $result.result.ToString() + if ($null -ne $result.Result) { + $isPlaying = $result.Result.ToString() } - if ($result.success -eq $true -and $isPlaying -eq "True") { + if ($result.Success -eq $true -and $isPlaying -eq "True") { return } @@ -221,7 +221,7 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-DynamicCode -Code $code - if ($result.result -ne $ScenePath) { + if ($result.Result -ne $ScenePath) { throw "Failed to open scene: $ScenePath" } } @@ -309,7 +309,7 @@ function Invoke-LaunchSmoke { Invoke-UloopChecked -CommandArguments @("launch") | Out-Host Wait-UnityReady [pscustomobject]$result = Invoke-DynamicCode -Code 'return "windows-launch-smoke";' - if ($result.result -ne "windows-launch-smoke") { + if ($result.Result -ne "windows-launch-smoke") { throw "launch smoke dynamic-code readiness check failed" } } @@ -376,7 +376,7 @@ if (cube == null) return -9999f; return cube.transform.position.z; '@ - return [double]$result.result + return [double]$result.Result } try { diff --git a/scripts/smoke-cli-recovery-readiness.go b/scripts/smoke-cli-recovery-readiness.go index ee45235f94..47efd51901 100644 --- a/scripts/smoke-cli-recovery-readiness.go +++ b/scripts/smoke-cli-recovery-readiness.go @@ -327,7 +327,7 @@ func assertJSONSuccess(result commandResult, label string) (map[string]any, erro return nil, err } - if success, ok := payload["success"].(bool); !ok || !success { + if success, ok := payload["Success"].(bool); !ok || !success { printCommandContext(label, result) return nil, fmt.Errorf("%s returned invalid success payload: %v", label, payload) } @@ -336,7 +336,7 @@ func assertJSONSuccess(result commandResult, label string) (map[string]any, erro } func assertDynamicCodeResult(payload map[string]any) error { - result, ok := payload["result"].(string) + result, ok := payload["Result"].(string) if ok && result == expectedDynamicCodeResult { return nil } diff --git a/scripts/test-simulate-mouse-demo.ps1 b/scripts/test-simulate-mouse-demo.ps1 index 718046e68f..c07c537b8d 100644 --- a/scripts/test-simulate-mouse-demo.ps1 +++ b/scripts/test-simulate-mouse-demo.ps1 @@ -109,7 +109,7 @@ function Wait-PlayMode { } [pscustomobject]$result = $probe.Text | ConvertFrom-Json - if ($result.success -eq $true -and $result.result -eq "True") { + if ($result.Success -eq $true -and $result.Result -eq "True") { return } @@ -140,14 +140,14 @@ function Assert-MouseResponse { [string]$ExpectedHit ) - if ($Response.success -ne $true) { - throw "$ExpectedAction failed: $($Response.message)" + if ($Response.Success -ne $true) { + throw "$ExpectedAction failed: $($Response.Message)" } - Assert-EqualText -Actual $Response.action -Expected $ExpectedAction -Context "Action" + Assert-EqualText -Actual $Response.Action -Expected $ExpectedAction -Context "Action" if (-not [string]::IsNullOrWhiteSpace($ExpectedHit)) { - Assert-EqualText -Actual $Response.hitGameObjectName -Expected $ExpectedHit -Context "hitGameObjectName" + Assert-EqualText -Actual $Response.HitGameObjectName -Expected $ExpectedHit -Context "hitGameObjectName" } } @@ -157,7 +157,7 @@ function Get-UiElement { [string]$Name ) - [object[]]$matches = @($Elements | Where-Object { $_.name -eq $Name }) + [object[]]$matches = @($Elements | Where-Object { $_.Name -eq $Name }) if ($matches.Count -eq 1) { return $matches[0] } @@ -240,11 +240,11 @@ return text.text; "@ [pscustomobject]$result = Invoke-UloopJson -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.success -ne $true) { - throw "Failed to read text from ${ObjectName}: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read text from ${ObjectName}: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Get-DropZoneStatus { @@ -261,11 +261,11 @@ return dropZone.StatusMessage; '@ ) - if ($result.success -ne $true) { - throw "Failed to read DropZone status: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read DropZone status: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Get-LongPressButtonText { @@ -283,11 +283,11 @@ return text.text; '@ ) - if ($result.success -ne $true) { - throw "Failed to read LongPressButton text: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read LongPressButton text: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Get-VirtualPadState { @@ -305,11 +305,11 @@ return pad.Direction.ToString("F3"); '@ ) - if ($result.success -ne $true) { - throw "Failed to read VirtualPad state: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to read VirtualPad state: $($result.ErrorMessage)" } - return $result.result + return $result.Result } function Initialize-DemoScene { @@ -330,11 +330,11 @@ return SceneManager.GetActiveScene().path; "@ [pscustomobject]$result = Invoke-UloopJson -CommandArguments @("execute-dynamic-code", "--code", $code) - if ($result.success -ne $true) { - throw "Failed to load ${ScenePath}: $($result.errorMessage)" + if ($result.Success -ne $true) { + throw "Failed to load ${ScenePath}: $($result.ErrorMessage)" } - Assert-EqualText -Actual $result.result -Expected $ScenePath -Context "Active scene" + Assert-EqualText -Actual $result.Result -Expected $ScenePath -Context "Active scene" } function Get-AnnotatedElements { @@ -346,11 +346,11 @@ function Get-AnnotatedElements { "--elements-only" ) - if ($response.screenshotCount -ne 1) { - throw "Expected one screenshot response, got $($response.screenshotCount)" + if ($response.ScreenshotCount -ne 1) { + throw "Expected one screenshot response, got $($response.ScreenshotCount)" } - [object[]]$elements = @($response.screenshots[0].annotatedElements) + [object[]]$elements = @($response.Screenshots[0].AnnotatedElements) if ($elements.Count -eq 0) { throw "No annotated UI elements were returned" } @@ -386,41 +386,41 @@ try { [pscustomobject]$virtualPad = Get-UiElement -Elements $elements -Name "VirtualPadBackground" Write-Host "[4/7] Clicking counter buttons..." - Invoke-MouseUi -Action "Click" -X $clickButton1.simX -Y $clickButton1.simY -TargetPath $clickButton1.path -ExpectedHit "ClickButton1" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton2.simX -Y $clickButton2.simY -TargetPath $clickButton2.path -ExpectedHit "ClickButton2" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton1.simX -Y $clickButton1.simY -TargetPath $clickButton1.path -ExpectedHit "ClickButton1" | Out-Null - Invoke-MouseUi -Action "Click" -X $clickButton2.simX -Y $clickButton2.simY -TargetPath $clickButton2.path -ExpectedHit "ClickButton2" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton1.SimX -Y $clickButton1.SimY -TargetPath $clickButton1.Path -ExpectedHit "ClickButton1" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton2.SimX -Y $clickButton2.SimY -TargetPath $clickButton2.Path -ExpectedHit "ClickButton2" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton1.SimX -Y $clickButton1.SimY -TargetPath $clickButton1.Path -ExpectedHit "ClickButton1" | Out-Null + Invoke-MouseUi -Action "Click" -X $clickButton2.SimX -Y $clickButton2.SimY -TargetPath $clickButton2.Path -ExpectedHit "ClickButton2" | Out-Null [string]$counterText = Get-TextFromScene -ObjectName "CounterText" Assert-EqualText -Actual $counterText -Expected "Total Clicks: 4" -Context "CounterText" Write-Host "[5/7] Long-pressing the hold button..." - Invoke-MouseUi -Action "LongPress" -X $longPressButton.simX -Y $longPressButton.simY -Duration 3.2 -TargetPath $longPressButton.path -ExpectedHit "LongPressButton" | Out-Null + Invoke-MouseUi -Action "LongPress" -X $longPressButton.SimX -Y $longPressButton.SimY -Duration 3.2 -TargetPath $longPressButton.Path -ExpectedHit "LongPressButton" | Out-Null [string]$longPressText = Get-LongPressButtonText Assert-EqualText -Actual $longPressText -Expected "Activated!" -Context "LongPressButton label" Write-Host "[6/7] Dragging boxes into the DropZone..." - Invoke-MouseUi -Action "Drag" -FromX $redBox.simX -FromY $redBox.simY -X $dropZone.simX -Y $dropZone.simY -DragSpeed 900 -TargetPath $redBox.path -DropTargetPath $dropZone.path -ExpectedHit "RedBox" | Out-Null + Invoke-MouseUi -Action "Drag" -FromX $redBox.SimX -FromY $redBox.SimY -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 900 -TargetPath $redBox.Path -DropTargetPath $dropZone.Path -ExpectedHit "RedBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: RedBox" -Context "DropZone after RedBox" - Invoke-MouseUi -Action "DragStart" -X $greenBox.simX -Y $greenBox.simY -DragSpeed 700 -TargetPath $greenBox.path -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($dropZone.simX + 150) -Y ($greenBox.simY - 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($dropZone.simX - 150) -Y ($dropZone.simY + 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null - Invoke-MouseUi -Action "DragEnd" -X $dropZone.simX -Y $dropZone.simY -DragSpeed 500 -DropTargetPath $dropZone.path -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragStart" -X $greenBox.SimX -Y $greenBox.SimY -DragSpeed 700 -TargetPath $greenBox.Path -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($dropZone.SimX + 150) -Y ($greenBox.SimY - 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($dropZone.SimX - 150) -Y ($dropZone.SimY + 50) -DragSpeed 500 -ExpectedHit "GreenBox" | Out-Null + Invoke-MouseUi -Action "DragEnd" -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 500 -DropTargetPath $dropZone.Path -ExpectedHit "GreenBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: GreenBox" -Context "DropZone after GreenBox" - Invoke-MouseUi -Action "Drag" -FromX $blueBox.simX -FromY $blueBox.simY -X $dropZone.simX -Y $dropZone.simY -DragSpeed 900 -TargetPath $blueBox.path -DropTargetPath $dropZone.path -ExpectedHit "BlueBox" | Out-Null + Invoke-MouseUi -Action "Drag" -FromX $blueBox.SimX -FromY $blueBox.SimY -X $dropZone.SimX -Y $dropZone.SimY -DragSpeed 900 -TargetPath $blueBox.Path -DropTargetPath $dropZone.Path -ExpectedHit "BlueBox" | Out-Null Assert-EqualText -Actual (Get-DropZoneStatus) -Expected "Dropped: BlueBox" -Context "DropZone after BlueBox" Write-Host "[7/7] Exercising the virtual pad..." - [double]$padWidth = $virtualPad.boundsMaxX - $virtualPad.boundsMinX - [double]$padHeight = $virtualPad.boundsMaxY - $virtualPad.boundsMinY + [double]$padWidth = $virtualPad.BoundsMaxX - $virtualPad.BoundsMinX + [double]$padHeight = $virtualPad.BoundsMaxY - $virtualPad.BoundsMinY [double]$padOffset = [Math]::Min($padWidth, $padHeight) * 0.28 - Invoke-MouseUi -Action "DragStart" -X $virtualPad.simX -Y $virtualPad.simY -TargetPath $virtualPad.path -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX + $padOffset) -Y ($virtualPad.simY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX - $padOffset) -Y ($virtualPad.simY + $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X $virtualPad.simX -Y ($virtualPad.simY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragMove" -X ($virtualPad.simX + $padOffset) -Y $virtualPad.simY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null - Invoke-MouseUi -Action "DragEnd" -X $virtualPad.simX -Y $virtualPad.simY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragStart" -X $virtualPad.SimX -Y $virtualPad.SimY -TargetPath $virtualPad.Path -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX + $padOffset) -Y ($virtualPad.SimY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX - $padOffset) -Y ($virtualPad.SimY + $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X $virtualPad.SimX -Y ($virtualPad.SimY - $padOffset) -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragMove" -X ($virtualPad.SimX + $padOffset) -Y $virtualPad.SimY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null + Invoke-MouseUi -Action "DragEnd" -X $virtualPad.SimX -Y $virtualPad.SimY -DragSpeed 400 -ExpectedHit "VirtualPadBackground" | Out-Null Assert-EqualText -Actual (Get-VirtualPadState) -Expected "Zero" -Context "VirtualPad state" Write-Host "" diff --git a/scripts/test-simulate-mouse-demo.sh b/scripts/test-simulate-mouse-demo.sh index 2fea736fb5..3e19a4cde0 100755 --- a/scripts/test-simulate-mouse-demo.sh +++ b/scripts/test-simulate-mouse-demo.sh @@ -71,8 +71,8 @@ assert_json_success() { json=$1 context=$2 - printf '%s\n' "$json" | jq -e '.success == true' >/dev/null \ - || fail "$context failed: $(printf '%s\n' "$json" | jq -r '.errorMessage // .message // .error // "unknown error"')" + printf '%s\n' "$json" | jq -e '.Success == true' >/dev/null \ + || fail "$context failed: $(printf '%s\n' "$json" | jq -r '.ErrorMessage // .Message // .Error // "unknown error"')" } assert_text_equals() { @@ -101,7 +101,7 @@ wait_play_mode() { attempt=0 while [ "$attempt" -lt 20 ]; do json=$(run_uloop_json execute-dynamic-code --code 'using UnityEngine; return Application.isPlaying;') - if printf '%s\n' "$json" | jq -e '.success == true and .result == "True"' >/dev/null; then + if printf '%s\n' "$json" | jq -e '.Success == true and .Result == "True"' >/dev/null; then return fi @@ -128,7 +128,7 @@ return SceneManager.GetActiveScene().path; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Load demo scene" - scene=$(printf '%s\n' "$json" | jq -r '.result') + scene=$(printf '%s\n' "$json" | jq -r '.Result') assert_text_equals "$scene" "$SCENE_PATH" "Active scene" } @@ -214,7 +214,7 @@ return displayTexts[selectedIndex] + " / " + currentGameViewSize.x + "x" + curre json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Select Full HD Game View" - printf ' %s\n' "$(printf '%s\n' "$json" | jq -r '.result')" + printf ' %s\n' "$(printf '%s\n' "$json" | jq -r '.Result')" sleep 1 } @@ -251,7 +251,7 @@ return selectedSizeIndexProperty.GetValue(gameView, null).ToString(); json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Capture Game View size index" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } restore_game_view_size_index() { @@ -299,16 +299,16 @@ capture_annotated_elements() { --annotate-elements \ --elements-only >"$ELEMENTS_JSON" - jq -e '.screenshotCount == 1 and (.screenshots[0].annotatedElements | length > 0)' "$ELEMENTS_JSON" >/dev/null \ + jq -e '.ScreenshotCount == 1 and (.Screenshots[0].AnnotatedElements | length > 0)' "$ELEMENTS_JSON" >/dev/null \ || fail "No annotated UI elements were returned" } get_element_json() { name=$1 - count=$(jq --arg name "$name" '[.screenshots[0].annotatedElements[] | select(.name == $name)] | length' "$ELEMENTS_JSON") + count=$(jq --arg name "$name" '[.Screenshots[0].AnnotatedElements[] | select(.Name == $name)] | length' "$ELEMENTS_JSON") [ "$count" = "1" ] || fail "Expected exactly one annotated element named '$name', found $count" - jq -c --arg name "$name" '.screenshots[0].annotatedElements[] | select(.name == $name)' "$ELEMENTS_JSON" + jq -c --arg name "$name" '.Screenshots[0].AnnotatedElements[] | select(.Name == $name)' "$ELEMENTS_JSON" } element_field() { @@ -323,11 +323,11 @@ assert_mouse_response() { expected_hit=$3 printf '%s\n' "$json" | jq -e --arg action "$expected_action" \ - '.success == true and .action == $action' >/dev/null \ - || fail "$expected_action failed: $(printf '%s\n' "$json" | jq -r '.message // "unknown error"')" + '.Success == true and .Action == $action' >/dev/null \ + || fail "$expected_action failed: $(printf '%s\n' "$json" | jq -r '.Message // "unknown error"')" if [ -n "$expected_hit" ]; then - actual_hit=$(printf '%s\n' "$json" | jq -r '.hitGameObjectName // ""') + actual_hit=$(printf '%s\n' "$json" | jq -r '.HitGameObjectName // ""') assert_text_equals "$actual_hit" "$expected_hit" "hitGameObjectName" fi } @@ -335,9 +335,9 @@ assert_mouse_response() { invoke_click() { name=$1 element=$(get_element_json "$name") - x=$(element_field "$element" "simX") - y=$(element_field "$element" "simY") - path=$(element_field "$element" "path") + x=$(element_field "$element" "SimX") + y=$(element_field "$element" "SimY") + path=$(element_field "$element" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action Click \ @@ -352,9 +352,9 @@ invoke_long_press() { name=$1 duration=$2 element=$(get_element_json "$name") - x=$(element_field "$element" "simX") - y=$(element_field "$element" "simY") - path=$(element_field "$element" "path") + x=$(element_field "$element" "SimX") + y=$(element_field "$element" "SimY") + path=$(element_field "$element" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action LongPress \ @@ -371,12 +371,12 @@ invoke_drag_to_drop_zone() { speed=$2 element=$(get_element_json "$name") drop_zone=$(get_element_json "DropZone") - from_x=$(element_field "$element" "simX") - from_y=$(element_field "$element" "simY") - to_x=$(element_field "$drop_zone" "simX") - to_y=$(element_field "$drop_zone" "simY") - target_path=$(element_field "$element" "path") - drop_path=$(element_field "$drop_zone" "path") + from_x=$(element_field "$element" "SimX") + from_y=$(element_field "$element" "SimY") + to_x=$(element_field "$drop_zone" "SimX") + to_y=$(element_field "$drop_zone" "SimY") + target_path=$(element_field "$element" "Path") + drop_path=$(element_field "$drop_zone" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action Drag \ @@ -394,9 +394,9 @@ invoke_drag_to_drop_zone() { invoke_drag_start() { name=$1 element=$(get_element_json "$name") - x=$(element_field "$element" "simX") - y=$(element_field "$element" "simY") - path=$(element_field "$element" "path") + x=$(element_field "$element" "SimX") + y=$(element_field "$element" "SimY") + path=$(element_field "$element" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action DragStart \ @@ -452,7 +452,7 @@ return text.text; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read text from $object_name" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } get_long_press_button_text() { @@ -468,7 +468,7 @@ return text.text; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read LongPressButton text" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } get_drop_zone_status() { @@ -483,7 +483,7 @@ return dropZone.StatusMessage; json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read DropZone status" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } get_virtual_pad_state() { @@ -499,17 +499,17 @@ return pad.Direction.ToString("F3"); json=$(run_uloop_json execute-dynamic-code --code "$code") assert_json_success "$json" "Read VirtualPad state" - printf '%s\n' "$json" | jq -r '.result' + printf '%s\n' "$json" | jq -r '.Result' } exercise_virtual_pad() { virtual_pad=$(get_element_json "VirtualPadBackground") - pad_x=$(element_field "$virtual_pad" "simX") - pad_y=$(element_field "$virtual_pad" "simY") - pad_width=$(printf '%s\n' "$virtual_pad" | jq -r '.boundsMaxX - .boundsMinX') - pad_height=$(printf '%s\n' "$virtual_pad" | jq -r '.boundsMaxY - .boundsMinY') + pad_x=$(element_field "$virtual_pad" "SimX") + pad_y=$(element_field "$virtual_pad" "SimY") + pad_width=$(printf '%s\n' "$virtual_pad" | jq -r '.BoundsMaxX - .BoundsMinX') + pad_height=$(printf '%s\n' "$virtual_pad" | jq -r '.BoundsMaxY - .BoundsMinY') pad_offset=$(jq -nr --argjson width "$pad_width" --argjson height "$pad_height" '([$width, $height] | min) * 0.28') - target_path=$(element_field "$virtual_pad" "path") + target_path=$(element_field "$virtual_pad" "Path") json=$(run_uloop_json simulate-mouse-ui \ --action DragStart \ @@ -582,11 +582,11 @@ assert_text_equals "$(get_drop_zone_status)" "Dropped: RedBox" "DropZone after R green_box=$(get_element_json "GreenBox") drop_zone=$(get_element_json "DropZone") -green_x=$(element_field "$green_box" "simX") -green_y=$(element_field "$green_box" "simY") -drop_x=$(element_field "$drop_zone" "simX") -drop_y=$(element_field "$drop_zone" "simY") -drop_path=$(element_field "$drop_zone" "path") +green_x=$(element_field "$green_box" "SimX") +green_y=$(element_field "$green_box" "SimY") +drop_x=$(element_field "$drop_zone" "SimX") +drop_y=$(element_field "$drop_zone" "SimY") +drop_path=$(element_field "$drop_zone" "Path") invoke_drag_start "GreenBox" move_x=$(jq -nr --argjson center "$drop_x" '$center + 150') diff --git a/scripts/test-smoke-cli-recovery-readiness.sh b/scripts/test-smoke-cli-recovery-readiness.sh index 86f11751cd..d7adab044a 100755 --- a/scripts/test-smoke-cli-recovery-readiness.sh +++ b/scripts/test-smoke-cli-recovery-readiness.sh @@ -48,18 +48,18 @@ func main() { fmt.Printf("Unity is already running for %s (PID: 1234)\n", projectPath) case "get-logs": if os.Getenv("ULOOP_FAKE_CONNECTION_FAILURE") == "1" { - fmt.Fprintln(os.Stderr, "{\n \"success\": false,\n \"error\": {\n \"errorCode\": \"UNITY_NOT_REACHABLE\",\n \"message\": \"The Unity CLI Loop server is not reachable for this project.\",\n \"nextActions\": [\n \"If Unity is closed, run `uloop launch`.\"\n ]\n }\n}") + fmt.Fprintln(os.Stderr, "{\n \"Success\": false,\n \"Error\": {\n \"ErrorCode\": \"UNITY_NOT_REACHABLE\",\n \"Message\": \"The Unity CLI Loop server is not reachable for this project.\",\n \"NextActions\": [\n \"If Unity is closed, run `uloop launch`.\"\n ]\n }\n}") os.Exit(1) } - fmt.Println(`{"displayedCount":0,"logs":[],"maxCount":1,"totalCount":0}`) + fmt.Println(`{"DisplayedCount":0,"Logs":[],"MaxCount":1,"TotalCount":0}`) case "compile": if os.Getenv("ULOOP_FAKE_COMPILE_WITHOUT_SUCCESS") == "1" { - fmt.Println(`{"errorCount":0,"warningCount":0}`) + fmt.Println(`{"ErrorCount":0,"WarningCount":0}`) return } - fmt.Println(`{"success":true,"errorCount":0,"warningCount":0}`) + fmt.Println(`{"Success":true,"ErrorCount":0,"WarningCount":0}`) case "execute-dynamic-code": - fmt.Println(`{"success":true,"result":"cli-recovery-readiness-e2e"}`) + fmt.Println(`{"Success":true,"Result":"cli-recovery-readiness-e2e"}`) default: fmt.Fprintf(os.Stderr, "unexpected command: %s\n", commandName) os.Exit(98) From 2e0131b5193589a23497aa95dfd3a034ddcdc556 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 22 Jun 2026 22:38:17 +0900 Subject: [PATCH 4/9] Read PascalCase CLI version JSON in setup detection Update the Unity-side CLI installation detector to parse the restored PascalCase --version --json contract so setup compatibility checks keep seeing the installed protocol version. --- Assets/Tests/Editor/CliInstallationDetectorTests.cs | 4 ++-- .../Editor/Infrastructure/CLI/CliInstallationDetector.cs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Assets/Tests/Editor/CliInstallationDetectorTests.cs b/Assets/Tests/Editor/CliInstallationDetectorTests.cs index 3800c6d038..5f7c538c98 100644 --- a/Assets/Tests/Editor/CliInstallationDetectorTests.cs +++ b/Assets/Tests/Editor/CliInstallationDetectorTests.cs @@ -181,7 +181,7 @@ public void ParseShellCliInstallationOutput_WhenContractJsonExists_ReturnsProtoc + "/Users/ExampleUser/.npm-global/bin/uloop\n" + "__ULOOP_PATH_END__\n" + "__ULOOP_CONTRACT_START__\n" - + "{\"cliVersion\":\"3.0.0-beta.31\",\"protocolVersion\":1}\n" + + "{\"CliVersion\":\"3.0.0-beta.31\",\"ProtocolVersion\":1}\n" + "__ULOOP_CONTRACT_END__\n" + "__ULOOP_CONTRACT_STATUS_START__\n" + "0\n" @@ -209,7 +209,7 @@ public void ParseShellCliInstallationOutput_WhenProtocolVersionIsOutsideIntRange + "/tmp/uloop\n" + "__ULOOP_PATH_END__\n" + "__ULOOP_CONTRACT_START__\n" - + "{\"cliVersion\":\"3.0.0-beta.31\",\"protocolVersion\":2147483648}\n" + + "{\"CliVersion\":\"3.0.0-beta.31\",\"ProtocolVersion\":2147483648}\n" + "__ULOOP_CONTRACT_END__\n" + "__ULOOP_CONTRACT_STATUS_START__\n" + "0\n" diff --git a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs index a867c85b31..7bd57a3bc6 100644 --- a/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs +++ b/Packages/src/Editor/Infrastructure/CLI/CliInstallationDetector.cs @@ -45,6 +45,8 @@ public sealed class CliInstallationDetector : ICliInstallationDetector private const string SHELL_CONTRACT_STATUS_START_MARKER = "__ULOOP_CONTRACT_STATUS_START__"; private const string SHELL_CONTRACT_STATUS_END_MARKER = "__ULOOP_CONTRACT_STATUS_END__"; private const string SHELL_SUCCESS_EXIT_CODE = "0"; + private const string VERSION_JSON_CLI_VERSION_PROPERTY = "CliVersion"; + private const string VERSION_JSON_PROTOCOL_VERSION_PROPERTY = "ProtocolVersion"; private string _cachedCliVersion; private int? _cachedCliProtocolVersion; @@ -362,8 +364,8 @@ private static CliInstallationDetection ParseCliContractOutput(string output, st try { JObject parsed = JObject.Parse(jsonLine); - string version = parsed["cliVersion"]?.ToString(); - JToken protocolVersionToken = parsed["protocolVersion"]; + string version = parsed[VERSION_JSON_CLI_VERSION_PROPERTY]?.ToString(); + JToken protocolVersionToken = parsed[VERSION_JSON_PROTOCOL_VERSION_PROPERTY]; int? protocolVersion = ReadProtocolVersion(protocolVersionToken); return new CliInstallationDetection(version, protocolVersion, executablePath); } From 41020bb80d1b865fea44a86a06c2d77a73c3cf46 Mon Sep 17 00:00:00 2001 From: hatayama Date: Mon, 22 Jun 2026 23:08:23 +0900 Subject: [PATCH 5/9] Make skill Markdown pass markdownlint Add a skill-focused markdownlint-cli2 configuration and apply the safe formatting fixes to source skill files. Regenerate the installed Claude and Agents skill copies so the checked-in skills lint cleanly together. --- .agents/skills/uloop-clear-console/SKILL.md | 1 + .agents/skills/uloop-compile/SKILL.md | 1 + .agents/skills/uloop-control-play-mode/SKILL.md | 1 + .agents/skills/uloop-execute-dynamic-code/SKILL.md | 1 + .agents/skills/uloop-find-game-objects/SKILL.md | 2 ++ .agents/skills/uloop-focus-window/SKILL.md | 1 + .agents/skills/uloop-get-hierarchy/SKILL.md | 1 + .agents/skills/uloop-get-logs/SKILL.md | 1 + .agents/skills/uloop-hello-world/SKILL.md | 2 ++ .agents/skills/uloop-launch/SKILL.md | 1 + .agents/skills/uloop-record-input/SKILL.md | 1 + .agents/skills/uloop-replay-input/SKILL.md | 1 + .agents/skills/uloop-run-tests/SKILL.md | 2 ++ .agents/skills/uloop-screenshot/SKILL.md | 1 + .agents/skills/uloop-simulate-keyboard/SKILL.md | 2 ++ .agents/skills/uloop-simulate-mouse-input/SKILL.md | 2 +- .agents/skills/uloop-simulate-mouse-ui/SKILL.md | 2 +- .agents/skills/uloop-wait-for-pause-point/SKILL.md | 12 +++++++----- .claude/skills/uloop-clear-console/SKILL.md | 1 + .claude/skills/uloop-compile/SKILL.md | 1 + .claude/skills/uloop-control-play-mode/SKILL.md | 1 + .claude/skills/uloop-execute-dynamic-code/SKILL.md | 1 + .claude/skills/uloop-find-game-objects/SKILL.md | 2 ++ .claude/skills/uloop-focus-window/SKILL.md | 1 + .claude/skills/uloop-get-hierarchy/SKILL.md | 1 + .claude/skills/uloop-get-logs/SKILL.md | 1 + .claude/skills/uloop-hello-world/SKILL.md | 2 ++ .claude/skills/uloop-launch/SKILL.md | 1 + .claude/skills/uloop-record-input/SKILL.md | 1 + .claude/skills/uloop-replay-input/SKILL.md | 1 + .claude/skills/uloop-run-tests/SKILL.md | 2 ++ .claude/skills/uloop-screenshot/SKILL.md | 1 + .claude/skills/uloop-simulate-keyboard/SKILL.md | 2 ++ .claude/skills/uloop-simulate-mouse-input/SKILL.md | 2 +- .claude/skills/uloop-simulate-mouse-ui/SKILL.md | 2 +- .claude/skills/uloop-wait-for-pause-point/SKILL.md | 12 +++++++----- .markdownlint-cli2.jsonc | 12 ++++++++++++ .../GetProjectInfo/Skill/SKILL.md | 1 + .../CustomCommandSamples/HelloWorld/Skill/SKILL.md | 2 ++ .../Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md | 1 + .../CliOnlyTools~/GetProjectInfo/Skill/SKILL.md | 1 + .../Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md | 1 + .../src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md | 1 + .../Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 12 +++++++----- .../FirstPartyTools/ClearConsole/Skill/SKILL.md | 1 + .../Editor/FirstPartyTools/Compile/Skill/SKILL.md | 1 + .../FirstPartyTools/ControlPlayMode/Skill/SKILL.md | 1 + .../ExecuteDynamicCode/Skill/SKILL.md | 1 + .../FirstPartyTools/FindGameObjects/Skill/SKILL.md | 2 ++ .../FirstPartyTools/GetHierarchy/Skill/SKILL.md | 1 + .../Editor/FirstPartyTools/GetLogs/Skill/SKILL.md | 1 + .../FirstPartyTools/RecordInput/Skill/SKILL.md | 1 + .../FirstPartyTools/ReplayInput/Skill/SKILL.md | 1 + .../Editor/FirstPartyTools/RunTests/Skill/SKILL.md | 2 ++ .../Editor/FirstPartyTools/Screenshot/Skill/SKILL.md | 1 + .../FirstPartyTools/SimulateKeyboard/Skill/SKILL.md | 2 ++ .../SimulateMouseInput/Skill/SKILL.md | 2 +- .../FirstPartyTools/SimulateMouseUi/Skill/SKILL.md | 2 +- 58 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 .markdownlint-cli2.jsonc diff --git a/.agents/skills/uloop-clear-console/SKILL.md b/.agents/skills/uloop-clear-console/SKILL.md index 0be2cc80fc..8f34706efa 100644 --- a/.agents/skills/uloop-clear-console/SKILL.md +++ b/.agents/skills/uloop-clear-console/SKILL.md @@ -39,6 +39,7 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: + - `Success` (boolean): Whether the clear operation succeeded - `ClearedLogCount` (number): Total number of log entries that were cleared - `ClearedCounts` (object): Breakdown by log type diff --git a/.agents/skills/uloop-compile/SKILL.md b/.agents/skills/uloop-compile/SKILL.md index b504940489..3eeb6bfe4e 100644 --- a/.agents/skills/uloop-compile/SKILL.md +++ b/.agents/skills/uloop-compile/SKILL.md @@ -44,6 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: + - `Success`: boolean or null - `ErrorCount`: number or null - `WarningCount`: number or null diff --git a/.agents/skills/uloop-control-play-mode/SKILL.md b/.agents/skills/uloop-control-play-mode/SKILL.md index 362405c49f..fc85b7df71 100644 --- a/.agents/skills/uloop-control-play-mode/SKILL.md +++ b/.agents/skills/uloop-control-play-mode/SKILL.md @@ -49,6 +49,7 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: + - `IsPlaying`: Whether Unity is currently in play mode - `IsPaused`: Whether play mode is paused - `Changed`: Whether the requested action changed the current play mode state diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index c77635307c..7fc716edb2 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -43,6 +43,7 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: + - `Success`: boolean — overall execution success - `Result`: string — value of the snippet's `return` statement (empty when omitted) - `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` diff --git a/.agents/skills/uloop-find-game-objects/SKILL.md b/.agents/skills/uloop-find-game-objects/SKILL.md index dd4faa9981..cd92c9963e 100644 --- a/.agents/skills/uloop-find-game-objects/SKILL.md +++ b/.agents/skills/uloop-find-game-objects/SKILL.md @@ -70,6 +70,7 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: + - `Results` (array): Matching GameObjects, each containing: - `Name` (string): GameObject name - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) @@ -84,6 +85,7 @@ Returns JSON with: ### Multi-selection file export For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: + - `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` - `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") diff --git a/.agents/skills/uloop-focus-window/SKILL.md b/.agents/skills/uloop-focus-window/SKILL.md index f2c93429bd..ead80195ae 100644 --- a/.agents/skills/uloop-focus-window/SKILL.md +++ b/.agents/skills/uloop-focus-window/SKILL.md @@ -29,6 +29,7 @@ uloop focus-window ## Output Returns JSON with: + - `Success`: Whether the focus operation succeeded - `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) diff --git a/.agents/skills/uloop-get-hierarchy/SKILL.md b/.agents/skills/uloop-get-hierarchy/SKILL.md index fcd1124c82..1af6712840 100644 --- a/.agents/skills/uloop-get-hierarchy/SKILL.md +++ b/.agents/skills/uloop-get-hierarchy/SKILL.md @@ -56,6 +56,7 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: + - `Message` (string): Human-readable guidance pointing at the saved file - `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data diff --git a/.agents/skills/uloop-get-logs/SKILL.md b/.agents/skills/uloop-get-logs/SKILL.md index d29d1d7881..dab6fc1d2f 100644 --- a/.agents/skills/uloop-get-logs/SKILL.md +++ b/.agents/skills/uloop-get-logs/SKILL.md @@ -50,6 +50,7 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: + - `TotalCount` (number): Total logs available before max-count clipping - `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) - Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response diff --git a/.agents/skills/uloop-hello-world/SKILL.md b/.agents/skills/uloop-hello-world/SKILL.md index 2724067ddd..9b1b7d8d89 100644 --- a/.agents/skills/uloop-hello-world/SKILL.md +++ b/.agents/skills/uloop-hello-world/SKILL.md @@ -40,6 +40,7 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: + - `message`: The greeting message - `language`: Language used for greeting - `timestamp`: Current timestamp (if enabled) @@ -47,6 +48,7 @@ Returns JSON with: ## Notes This is a sample custom tool demonstrating: + - Type-safe parameter handling with Schema - Enum parameters for language selection - Value-less flag parameters diff --git a/.agents/skills/uloop-launch/SKILL.md b/.agents/skills/uloop-launch/SKILL.md index 5019a7c014..82aadc69d6 100644 --- a/.agents/skills/uloop-launch/SKILL.md +++ b/.agents/skills/uloop-launch/SKILL.md @@ -50,6 +50,7 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: + - `Success`: whether the command completed - `Ready`: whether Unity CLI Loop is ready for commands - `ServerReady`: whether the Unity CLI Loop server accepted requests diff --git a/.agents/skills/uloop-record-input/SKILL.md b/.agents/skills/uloop-record-input/SKILL.md index 0781fbcf4d..43f966dc3d 100644 --- a/.agents/skills/uloop-record-input/SKILL.md +++ b/.agents/skills/uloop-record-input/SKILL.md @@ -44,6 +44,7 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Start` or `Stop`) diff --git a/.agents/skills/uloop-replay-input/SKILL.md b/.agents/skills/uloop-replay-input/SKILL.md index e159fadec8..051b96759c 100644 --- a/.agents/skills/uloop-replay-input/SKILL.md +++ b/.agents/skills/uloop-replay-input/SKILL.md @@ -48,6 +48,7 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) diff --git a/.agents/skills/uloop-run-tests/SKILL.md b/.agents/skills/uloop-run-tests/SKILL.md index b0a4bf4b74..c77e73e7b2 100644 --- a/.agents/skills/uloop-run-tests/SKILL.md +++ b/.agents/skills/uloop-run-tests/SKILL.md @@ -57,6 +57,7 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: + - `Success` (boolean): Whether all tests passed - `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` - `HasFailures` (boolean): Whether any discovered test failed @@ -73,6 +74,7 @@ Returns JSON with: ### XML Result File When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/.xml`. The XML contains per-test-case results including: + - Test name and full name - Pass/fail/skip status and duration - For failed tests: `` (assertion error) and `` diff --git a/.agents/skills/uloop-screenshot/SKILL.md b/.agents/skills/uloop-screenshot/SKILL.md index fdebaae7a0..39e81c686e 100644 --- a/.agents/skills/uloop-screenshot/SKILL.md +++ b/.agents/skills/uloop-screenshot/SKILL.md @@ -72,6 +72,7 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: + - `ScreenshotCount`: Number of windows captured - `Screenshots`: Array of screenshot info, each containing: - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index 779c628bc6..a455ad064e 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -42,6 +42,7 @@ uloop simulate-keyboard --action --key [options] Use `Press` for edge-triggered keyboard code such as `Keyboard.current.spaceKey.wasPressedThisFrame`. `KeyDown` emits one initial press edge, then only keeps the key held. It does not keep `wasPressedThisFrame` true while the key remains held. If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` true but runtime state does not change, do not immediately rewrite the user's runtime code to `isPressed`. First verify that the target component is active during the command, that it polls input in the configured Input System update phase, and that a missed `KeyDown` edge is followed by `KeyUp` before retrying. + ### Pause Point Inspection (Standard for E2E) For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. @@ -85,6 +86,7 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: + - `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) - `Message` (string): Description of what happened or why it failed - `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index 6ab040f946..609af381d1 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -61,7 +61,6 @@ For standard frame proof when this input drives a state transition, follow the ` |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## When to use this vs simulate-mouse-ui All rows below assume the New Input System is installed. @@ -109,6 +108,7 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index d685835a54..a8f7d7ecea 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -64,7 +64,6 @@ uloop simulate-mouse-ui --action --x --y [options] |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## Coordinate System - Origin is **top-left** (0, 0) @@ -125,6 +124,7 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") - `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) diff --git a/.agents/skills/uloop-wait-for-pause-point/SKILL.md b/.agents/skills/uloop-wait-for-pause-point/SKILL.md index 9789b6560b..a0055d4f47 100644 --- a/.agents/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.agents/skills/uloop-wait-for-pause-point/SKILL.md @@ -3,6 +3,8 @@ name: uloop-wait-for-pause-point description: "Pauses Unity's playback and allows you to inspect specific frames. Use this for bug investigation or PlayMode/E2E testing. It's the most convenient yet rigorous method for verifying variable states at specific frames or confirming whether particular code has been executed." --- +# uloop wait-for-pause-point + ## Quick Check Template Use this small loop for one representative frame you care about: @@ -19,14 +21,14 @@ Debug.Log($"state-transition-applied localValue={localValue} reason={reason}"); UloopPausePoint.Pause("state-transition-applied"); ``` -2. Compile, enter PlayMode, then enable the marker: +1. Compile, enter PlayMode, then enable the marker: ```bash uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` -3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: +1. Trigger the action with a `simulate-*` command. +2. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 @@ -38,8 +40,8 @@ Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause 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. -5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. +1. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +2. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use diff --git a/.claude/skills/uloop-clear-console/SKILL.md b/.claude/skills/uloop-clear-console/SKILL.md index 0be2cc80fc..8f34706efa 100644 --- a/.claude/skills/uloop-clear-console/SKILL.md +++ b/.claude/skills/uloop-clear-console/SKILL.md @@ -39,6 +39,7 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: + - `Success` (boolean): Whether the clear operation succeeded - `ClearedLogCount` (number): Total number of log entries that were cleared - `ClearedCounts` (object): Breakdown by log type diff --git a/.claude/skills/uloop-compile/SKILL.md b/.claude/skills/uloop-compile/SKILL.md index b504940489..3eeb6bfe4e 100644 --- a/.claude/skills/uloop-compile/SKILL.md +++ b/.claude/skills/uloop-compile/SKILL.md @@ -44,6 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: + - `Success`: boolean or null - `ErrorCount`: number or null - `WarningCount`: number or null diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md index 362405c49f..fc85b7df71 100644 --- a/.claude/skills/uloop-control-play-mode/SKILL.md +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -49,6 +49,7 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: + - `IsPlaying`: Whether Unity is currently in play mode - `IsPaused`: Whether play mode is paused - `Changed`: Whether the requested action changed the current play mode state diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index c77635307c..7fc716edb2 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -43,6 +43,7 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: + - `Success`: boolean — overall execution success - `Result`: string — value of the snippet's `return` statement (empty when omitted) - `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` diff --git a/.claude/skills/uloop-find-game-objects/SKILL.md b/.claude/skills/uloop-find-game-objects/SKILL.md index dd4faa9981..cd92c9963e 100644 --- a/.claude/skills/uloop-find-game-objects/SKILL.md +++ b/.claude/skills/uloop-find-game-objects/SKILL.md @@ -70,6 +70,7 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: + - `Results` (array): Matching GameObjects, each containing: - `Name` (string): GameObject name - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) @@ -84,6 +85,7 @@ Returns JSON with: ### Multi-selection file export For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: + - `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` - `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") diff --git a/.claude/skills/uloop-focus-window/SKILL.md b/.claude/skills/uloop-focus-window/SKILL.md index f2c93429bd..ead80195ae 100644 --- a/.claude/skills/uloop-focus-window/SKILL.md +++ b/.claude/skills/uloop-focus-window/SKILL.md @@ -29,6 +29,7 @@ uloop focus-window ## Output Returns JSON with: + - `Success`: Whether the focus operation succeeded - `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) diff --git a/.claude/skills/uloop-get-hierarchy/SKILL.md b/.claude/skills/uloop-get-hierarchy/SKILL.md index fcd1124c82..1af6712840 100644 --- a/.claude/skills/uloop-get-hierarchy/SKILL.md +++ b/.claude/skills/uloop-get-hierarchy/SKILL.md @@ -56,6 +56,7 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: + - `Message` (string): Human-readable guidance pointing at the saved file - `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data diff --git a/.claude/skills/uloop-get-logs/SKILL.md b/.claude/skills/uloop-get-logs/SKILL.md index d29d1d7881..dab6fc1d2f 100644 --- a/.claude/skills/uloop-get-logs/SKILL.md +++ b/.claude/skills/uloop-get-logs/SKILL.md @@ -50,6 +50,7 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: + - `TotalCount` (number): Total logs available before max-count clipping - `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) - Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response diff --git a/.claude/skills/uloop-hello-world/SKILL.md b/.claude/skills/uloop-hello-world/SKILL.md index 2724067ddd..9b1b7d8d89 100644 --- a/.claude/skills/uloop-hello-world/SKILL.md +++ b/.claude/skills/uloop-hello-world/SKILL.md @@ -40,6 +40,7 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: + - `message`: The greeting message - `language`: Language used for greeting - `timestamp`: Current timestamp (if enabled) @@ -47,6 +48,7 @@ Returns JSON with: ## Notes This is a sample custom tool demonstrating: + - Type-safe parameter handling with Schema - Enum parameters for language selection - Value-less flag parameters diff --git a/.claude/skills/uloop-launch/SKILL.md b/.claude/skills/uloop-launch/SKILL.md index 5019a7c014..82aadc69d6 100644 --- a/.claude/skills/uloop-launch/SKILL.md +++ b/.claude/skills/uloop-launch/SKILL.md @@ -50,6 +50,7 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: + - `Success`: whether the command completed - `Ready`: whether Unity CLI Loop is ready for commands - `ServerReady`: whether the Unity CLI Loop server accepted requests diff --git a/.claude/skills/uloop-record-input/SKILL.md b/.claude/skills/uloop-record-input/SKILL.md index 0781fbcf4d..43f966dc3d 100644 --- a/.claude/skills/uloop-record-input/SKILL.md +++ b/.claude/skills/uloop-record-input/SKILL.md @@ -44,6 +44,7 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Start` or `Stop`) diff --git a/.claude/skills/uloop-replay-input/SKILL.md b/.claude/skills/uloop-replay-input/SKILL.md index e159fadec8..051b96759c 100644 --- a/.claude/skills/uloop-replay-input/SKILL.md +++ b/.claude/skills/uloop-replay-input/SKILL.md @@ -48,6 +48,7 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) diff --git a/.claude/skills/uloop-run-tests/SKILL.md b/.claude/skills/uloop-run-tests/SKILL.md index b0a4bf4b74..c77e73e7b2 100644 --- a/.claude/skills/uloop-run-tests/SKILL.md +++ b/.claude/skills/uloop-run-tests/SKILL.md @@ -57,6 +57,7 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: + - `Success` (boolean): Whether all tests passed - `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` - `HasFailures` (boolean): Whether any discovered test failed @@ -73,6 +74,7 @@ Returns JSON with: ### XML Result File When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/.xml`. The XML contains per-test-case results including: + - Test name and full name - Pass/fail/skip status and duration - For failed tests: `` (assertion error) and `` diff --git a/.claude/skills/uloop-screenshot/SKILL.md b/.claude/skills/uloop-screenshot/SKILL.md index fdebaae7a0..39e81c686e 100644 --- a/.claude/skills/uloop-screenshot/SKILL.md +++ b/.claude/skills/uloop-screenshot/SKILL.md @@ -72,6 +72,7 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: + - `ScreenshotCount`: Number of windows captured - `Screenshots`: Array of screenshot info, each containing: - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index 779c628bc6..a455ad064e 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -42,6 +42,7 @@ uloop simulate-keyboard --action --key [options] Use `Press` for edge-triggered keyboard code such as `Keyboard.current.spaceKey.wasPressedThisFrame`. `KeyDown` emits one initial press edge, then only keeps the key held. It does not keep `wasPressedThisFrame` true while the key remains held. If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` true but runtime state does not change, do not immediately rewrite the user's runtime code to `isPressed`. First verify that the target component is active during the command, that it polls input in the configured Input System update phase, and that a missed `KeyDown` edge is followed by `KeyUp` before retrying. + ### Pause Point Inspection (Standard for E2E) For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. @@ -85,6 +86,7 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: + - `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) - `Message` (string): Description of what happened or why it failed - `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index 6ab040f946..609af381d1 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -61,7 +61,6 @@ For standard frame proof when this input drives a state transition, follow the ` |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## When to use this vs simulate-mouse-ui All rows below assume the New Input System is installed. @@ -109,6 +108,7 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index d685835a54..a8f7d7ecea 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -64,7 +64,6 @@ uloop simulate-mouse-ui --action --x --y [options] |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## Coordinate System - Origin is **top-left** (0, 0) @@ -125,6 +124,7 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") - `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) diff --git a/.claude/skills/uloop-wait-for-pause-point/SKILL.md b/.claude/skills/uloop-wait-for-pause-point/SKILL.md index 9789b6560b..a0055d4f47 100644 --- a/.claude/skills/uloop-wait-for-pause-point/SKILL.md +++ b/.claude/skills/uloop-wait-for-pause-point/SKILL.md @@ -3,6 +3,8 @@ name: uloop-wait-for-pause-point description: "Pauses Unity's playback and allows you to inspect specific frames. Use this for bug investigation or PlayMode/E2E testing. It's the most convenient yet rigorous method for verifying variable states at specific frames or confirming whether particular code has been executed." --- +# uloop wait-for-pause-point + ## Quick Check Template Use this small loop for one representative frame you care about: @@ -19,14 +21,14 @@ Debug.Log($"state-transition-applied localValue={localValue} reason={reason}"); UloopPausePoint.Pause("state-transition-applied"); ``` -2. Compile, enter PlayMode, then enable the marker: +1. Compile, enter PlayMode, then enable the marker: ```bash uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` -3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: +1. Trigger the action with a `simulate-*` command. +2. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 @@ -38,8 +40,8 @@ Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause 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. -5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. +1. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +2. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000000..cd7e4d4ee1 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,12 @@ +{ + "config": { + "MD013": false, + "MD060": false + }, + "globs": [ + "Packages/src/**/Skill/SKILL.md", + "Assets/Editor/CustomCommandSamples/**/Skill/SKILL.md", + ".agents/skills/**/SKILL.md", + ".claude/skills/**/SKILL.md" + ] +} diff --git a/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md b/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md index 6805b43933..729f7a1118 100644 --- a/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md +++ b/Assets/Editor/CustomCommandSamples/GetProjectInfo/Skill/SKILL.md @@ -21,6 +21,7 @@ None. ## Output Returns JSON with: + - `ProjectName`: Application product name - `CompanyName`: Company name - `Version`: Application version diff --git a/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md b/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md index 2724067ddd..9b1b7d8d89 100644 --- a/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md +++ b/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md @@ -40,6 +40,7 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp ## Output Returns JSON with: + - `message`: The greeting message - `language`: Language used for greeting - `timestamp`: Current timestamp (if enabled) @@ -47,6 +48,7 @@ Returns JSON with: ## Notes This is a sample custom tool demonstrating: + - Type-safe parameter handling with Schema - Enum parameters for language selection - Value-less flag parameters diff --git a/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md index f2c93429bd..ead80195ae 100644 --- a/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/FocusWindow/Skill/SKILL.md @@ -29,6 +29,7 @@ uloop focus-window ## Output Returns JSON with: + - `Success`: Whether the focus operation succeeded - `Message`: Status message (e.g. `Unity Editor window focused (PID: 12345)`, or the failure reason such as `Unity project not found` / `No running Unity process found for this project` / `Failed to focus Unity window: `) diff --git a/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md index 6f5987df53..aeab55f947 100644 --- a/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/GetProjectInfo/Skill/SKILL.md @@ -28,6 +28,7 @@ uloop get-project-info ## Output Returns JSON with project information: + - Unity version - Project name - Platform settings diff --git a/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md index 78e43a6ee8..2ded5514dc 100644 --- a/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/GetVersion/Skill/SKILL.md @@ -21,6 +21,7 @@ None. ## Output Returns JSON with: + - `UnityVersion`: Unity Editor version - `Platform`: Current platform - `DataPath`: Assets folder path diff --git a/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md index 5019a7c014..82aadc69d6 100644 --- a/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/Launch/Skill/SKILL.md @@ -50,6 +50,7 @@ uloop launch --quit May print status/progress lines before the final JSON payload, such as project path, detected Unity version, or readiness wait messages. The final JSON payload includes: + - `Success`: whether the command completed - `Ready`: whether Unity CLI Loop is ready for commands - `ServerReady`: whether the Unity CLI Loop server accepted requests diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 9789b6560b..a0055d4f47 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -3,6 +3,8 @@ name: uloop-wait-for-pause-point description: "Pauses Unity's playback and allows you to inspect specific frames. Use this for bug investigation or PlayMode/E2E testing. It's the most convenient yet rigorous method for verifying variable states at specific frames or confirming whether particular code has been executed." --- +# uloop wait-for-pause-point + ## Quick Check Template Use this small loop for one representative frame you care about: @@ -19,14 +21,14 @@ Debug.Log($"state-transition-applied localValue={localValue} reason={reason}"); UloopPausePoint.Pause("state-transition-applied"); ``` -2. Compile, enter PlayMode, then enable the marker: +1. Compile, enter PlayMode, then enable the marker: ```bash uloop enable-pause-point --id state-transition-applied --timeout-seconds 30 ``` -3. Trigger the action with a `simulate-*` command. -4. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: +1. Trigger the action with a `simulate-*` command. +2. Wait for the marker and read the focused log in one call, even if the trigger command already returned `InterruptedByPausePoint=true`: ```bash uloop wait-for-pause-point --id state-transition-applied --timeout-seconds 30 @@ -38,8 +40,8 @@ Read `EvidenceSummary` first when it is present. It groups `EditorState`, pause 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. -5. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -6. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. +1. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +2. Clear the marker with `uloop clear-pause-point --id state-transition-applied` or stop PlayMode before moving on. Use `uloop clear-pause-point --all` to clear every active marker at once, for example when resetting between E2E scenarios. The clear response's `EditorState` describes Unity Editor play/pause state, not marker state. ## When To Use diff --git a/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md index 0be2cc80fc..8f34706efa 100644 --- a/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ClearConsole/Skill/SKILL.md @@ -39,6 +39,7 @@ uloop clear-console --add-confirmation-message ## Output Returns JSON with: + - `Success` (boolean): Whether the clear operation succeeded - `ClearedLogCount` (number): Total number of log entries that were cleared - `ClearedCounts` (object): Breakdown by log type diff --git a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md index b504940489..3eeb6bfe4e 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md @@ -44,6 +44,7 @@ uloop compile --stop-on-external-scene-changes ## Output Returns JSON: + - `Success`: boolean or null - `ErrorCount`: number or null - `WarningCount`: number or null diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md index 362405c49f..fc85b7df71 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md @@ -49,6 +49,7 @@ uloop control-play-mode --action Step ## Output Returns JSON with the current play mode state: + - `IsPlaying`: Whether Unity is currently in play mode - `IsPaused`: Whether play mode is paused - `Changed`: Whether the requested action changed the current play mode state diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index c77635307c..7fc716edb2 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -43,6 +43,7 @@ Prefer terminal commands for file operations and keep snippets focused on Unity ## Output Returns JSON: + - `Success`: boolean — overall execution success - `Result`: string — value of the snippet's `return` statement (empty when omitted) - `Logs`: string[] — execution messages from the dynamic-code tool; read Unity Console `Debug.Log` output with `get-logs` diff --git a/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md index dd4faa9981..cd92c9963e 100644 --- a/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/FindGameObjects/Skill/SKILL.md @@ -70,6 +70,7 @@ uloop find-game-objects --search-mode Selected --include-inactive ## Output Returns JSON with: + - `Results` (array): Matching GameObjects, each containing: - `Name` (string): GameObject name - `Path` (string): Hierarchy path (e.g., `Canvas/Panel/Button`) @@ -84,6 +85,7 @@ Returns JSON with: ### Multi-selection file export For `Selected` mode with **multiple** successfully serialized GameObjects, inline `Results` is not populated and the data is written to a file instead. Two extra fields appear: + - `ResultsFilePath` (string): Relative path under `.uloop/outputs/FindGameObjectsResults/` - `Message` (string): Human-readable summary (e.g., "5 GameObjects exported") diff --git a/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md index fcd1124c82..1af6712840 100644 --- a/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/GetHierarchy/Skill/SKILL.md @@ -56,6 +56,7 @@ uloop get-hierarchy --use-selection ## Output Returns JSON with: + - `Message` (string): Human-readable guidance pointing at the saved file - `HierarchyFilePath` (string): Filesystem path to the JSON file that contains the actual hierarchy data diff --git a/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md index d29d1d7881..dab6fc1d2f 100644 --- a/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/GetLogs/Skill/SKILL.md @@ -50,6 +50,7 @@ uloop get-logs --search-text "Missing.*Component" --use-regex ## Output Returns JSON with: + - `TotalCount` (number): Total logs available before max-count clipping - `DisplayedCount` (number): Logs returned in this response (≤ `--max-count`) - Input filters (`LogType`, `MaxCount`, `SearchText`, `IncludeStackTrace`) are echoed back in the response diff --git a/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md index 0781fbcf4d..43f966dc3d 100644 --- a/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/RecordInput/Skill/SKILL.md @@ -44,6 +44,7 @@ Replay injects input frame-by-frame, so the game must also be deterministic to p ## Output The CLI prints JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Start` or `Stop`) diff --git a/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md index e159fadec8..051b96759c 100644 --- a/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ReplayInput/Skill/SKILL.md @@ -48,6 +48,7 @@ Replay injects the exact same input frame-by-frame, but the game must also be de ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Start`, `Stop`, or `Status`) diff --git a/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md index b0a4bf4b74..c77e73e7b2 100644 --- a/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/RunTests/Skill/SKILL.md @@ -57,6 +57,7 @@ uloop run-tests --filter-type regex --filter-value ".*Integration.*" ## Output Returns JSON with: + - `Success` (boolean): Whether all tests passed - `Status` (string): Machine-readable execution status such as `Passed`, `Failed`, `NoTestsFound`, or `ExecutionFailed` - `HasFailures` (boolean): Whether any discovered test failed @@ -73,6 +74,7 @@ Returns JSON with: ### XML Result File When tests fail, NUnit XML results are automatically saved to `{project_root}/.uloop/outputs/TestResults/.xml`. The XML contains per-test-case results including: + - Test name and full name - Pass/fail/skip status and duration - For failed tests: `` (assertion error) and `` diff --git a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md index fdebaae7a0..39e81c686e 100644 --- a/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Screenshot/Skill/SKILL.md @@ -72,6 +72,7 @@ uloop screenshot --output-directory /tmp/screenshots ## Output Returns JSON with: + - `ScreenshotCount`: Number of windows captured - `Screenshots`: Array of screenshot info, each containing: - `ImagePath`: Absolute path to the saved PNG file. Empty when `--elements-only` is used because no image file is written. diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index 779c628bc6..a455ad064e 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -42,6 +42,7 @@ uloop simulate-keyboard --action --key [options] Use `Press` for edge-triggered keyboard code such as `Keyboard.current.spaceKey.wasPressedThisFrame`. `KeyDown` emits one initial press edge, then only keeps the key held. It does not keep `wasPressedThisFrame` true while the key remains held. If a successful `Press` or `KeyDown` leaves `Keyboard.current..isPressed` true but runtime state does not change, do not immediately rewrite the user's runtime code to `isPressed`. First verify that the target component is active during the command, that it polls input in the configured Input System update phase, and that a missed `KeyDown` edge is followed by `KeyUp` before retrying. + ### Pause Point Inspection (Standard for E2E) For standard frame proof when this input drives a state transition, follow the `uloop-wait-for-pause-point` skill. Place markers after the app consumed the key, not immediately after `simulate-keyboard`. @@ -85,6 +86,7 @@ uloop simulate-keyboard --action KeyUp --key LeftShift ## Output Returns JSON with: + - `Success` (boolean): Whether the action succeeded (e.g. `KeyDown` on a not-yet-held key, `KeyUp` on a currently-held key, or `Press` round-trip) - `Message` (string): Description of what happened or why it failed - `Action` (string): The `--action` value that was applied (`Press`, `KeyDown`, or `KeyUp`) diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md index 6ab040f946..609af381d1 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -61,7 +61,6 @@ For standard frame proof when this input drives a state transition, follow the ` |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## When to use this vs simulate-mouse-ui All rows below assume the New Input System is installed. @@ -109,6 +108,7 @@ uloop simulate-mouse-input --action SmoothDelta --delta-x 300 --delta-y 0 --dura ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message - `Action`: Echoes which action was executed (`Click`, `LongPress`, `MoveDelta`, `SmoothDelta`, or `Scroll`) diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md index d685835a54..a8f7d7ecea 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -64,7 +64,6 @@ uloop simulate-mouse-ui --action --x --y [options] |--------|-------------| | `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | - ## Coordinate System - Origin is **top-left** (0, 0) @@ -125,6 +124,7 @@ uloop simulate-mouse-ui --action DragEnd --x 600 --y 300 ## Output Returns JSON with: + - `Success`: Whether the operation succeeded - `Message`: Status message (e.g. "Hit element: ButtonStart" or "No UI element under (x, y)") - `Action`: Echoes which action was executed (`Click`, `Drag`, `DragStart`, `DragMove`, `DragEnd`, or `LongPress`) From 7967217b05dc3ae6d80abb17ee8bf1c6e1a81e32 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 23 Jun 2026 00:19:55 +0900 Subject: [PATCH 6/9] Constrain V3 migration skill to uloop options Tighten the temporary migration skill so agents only edit option tokens inside confirmed uloop invocations and preserve unrelated formatting, commands, and non-uloop arguments. --- .../Skill/SKILL.md | 47 ++++++++++++++----- .../Skill/references/first-party-v2-to-v3.md | 16 +++++-- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md index 0a9359f292..ae5491ffbb 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md @@ -1,35 +1,58 @@ --- name: v3-cli-invocation-migration -description: Migrate uloop V2 CLI invocations in agent skills, Markdown, POSIX shell scripts, and PowerShell scripts to V3 syntax. Use when updating third-party or first-party uloop command examples, shell automation, or SKILL.md files that use V2-style boolean arguments, renamed first-party options, or removed commands. +description: Migrate only uloop V2 CLI option syntax in agent skills, Markdown, POSIX shell scripts, and PowerShell scripts to V3 syntax. Use when updating first-party or third-party uloop command examples that contain V2-style boolean arguments or renamed options. --- # V3 CLI Invocation Migration -Use this skill to update V2-era `uloop` CLI invocations to V3 syntax in agent-facing docs and automation. +Use this skill to update V2-era `uloop` CLI option syntax to V3 +syntax in agent-facing docs and automation. ## Workflow 1. Read `references/first-party-v2-to-v3.md` before editing. 2. Search the repository for `uloop` invocations and the V2 names listed in the reference. 3. Inspect nearby context before every edit. Search hits are candidates, not proof. -4. Edit only files that clearly contain V2 `uloop` command syntax. -5. Repeat the searches after editing and report any remaining V2 candidates with the reason each one was left unchanged. +4. Edit only files that clearly contain V2 `uloop` option syntax. +5. Repeat the searches after editing and report any remaining V2 + candidates with the reason each one was left unchanged. Prefer `rg` for searches when available. If `rg` is unavailable, use the best available project search tool. Candidate discovery should be repository search plus context inspection, not a generated candidate list. ## Editing Rules -- Convert V2 boolean arguments from `--flag true` or `--flag=false` syntax to V3 flag syntax. -- For third-party tools, check the tool's current schema or documentation before deciding whether `false` means removal, a `--no-*` flag, or a renamed option. -- For first-party tools, use the reference table. `compile` and `run-tests` have special renamed negative flags. -- Do not edit generated installed skills under `.agents`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or similar target folders unless the user explicitly asks to migrate installed copies. -- Do not edit Markdown C# snippets, enum/member references, ordinary DTO/property access, regex match properties, or non-`uloop` JSON. -- Do not change protocol versions, release versions, package names, assembly names, or public extension identifiers as part of this migration. -- Keep edits local to the command or parser being migrated. +- Only change option tokens inside confirmed `uloop` command invocations. +- Convert V2 boolean arguments from `--flag true` or `--flag=false` + syntax to V3 flag syntax. +- For third-party tools, check the tool's current schema or documentation + before deciding whether `false` means removal, a `--no-*` flag, or a + renamed option. +- For first-party tools, use the reference table. `compile` and `run-tests` + have special renamed negative flags. +- A valid edit is limited to replacing, adding, or removing a `uloop` option + token and the boolean value attached to that option in the same invocation. +- Preserve surrounding Markdown, shell, and PowerShell formatting. Do not + reflow prose, wrap lines, reorder arguments, normalize unrelated whitespace, + trim unrelated trailing spaces, or change indentation. +- If removing an option leaves extra spaces in that same `uloop` invocation, + collapse only the spaces created by that removal. +- Do not rename commands, replace removed commands, or rewrite command + structure unless the user explicitly asks for command migration. +- Do not change non-`uloop` command options, even when they have V2-looking + boolean syntax. +- Do not edit generated installed skills under `.agents`, `.claude`, + `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or similar target + folders unless the user explicitly asks to migrate installed copies. +- Do not edit Markdown C# snippets, enum/member references, ordinary + DTO/property access, regex match properties, or non-`uloop` JSON. +- Do not change protocol versions, release versions, package names, assembly + names, or public extension identifiers as part of this migration. ## Required Search Passes - `uloop` command lines and examples. - Boolean-looking CLI options: `--* true`, `--*=true`, `--* false`, `--*=false`. - First-party renamed options from the reference, including bare flags. -- Removed commands: `get-project-info` and `get-version`. +- Removed commands: `get-project-info` and `get-version`. Report these as + out-of-scope command migration candidates unless the user explicitly asked + to migrate removed commands. diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md index e939265e57..7e88524c3f 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md @@ -1,6 +1,13 @@ # First-Party V2 to V3 CLI Migration -Use this reference as the canonical migration map. Search results are only candidates. Edit a match only after the surrounding context proves it is a V2 `uloop` invocation. +Use this reference as the canonical option migration map. Search results are +only candidates. Edit a match only after the surrounding context proves it is +a V2 `uloop` invocation. + +This migration is option-only by default. Do not reformat surrounding text or +scripts, and do not change non-`uloop` commands. A valid edit only replaces, +adds, or removes a `uloop` option token and the boolean value attached to that +option in the same invocation. ## Search Checklist @@ -9,7 +16,8 @@ Prefer `rg` when available, but any repository search tool is acceptable. - Search `uloop` first and inspect command examples, shell scripts, PowerShell scripts, and agent skills. - Search boolean-looking CLI syntax: `--` plus nearby `true` or `false`, including `--flag true`, `--flag=false`, and inline Markdown command examples. - Search renamed first-party option names: `wait-for-domain-reload`, `reload-external-scene-changes`, `force-recompile`, `save-before-run`, `show-overlay`, `include-components`, `include-inactive`, and `compile-only`. -- Search removed first-party commands: `get-project-info` and `get-version`. +- Search removed first-party commands only to report them as out-of-scope + command migration candidates: `get-project-info` and `get-version`. - Skip generated installed skill copies under `.agents`, `.claude`, `.codex`, `.cursor`, `.gemini`, `.windsurf`, `.agent`, or equivalent target folders unless the user explicitly asks to migrate installed copies. ## Boolean Argument Rules @@ -50,5 +58,5 @@ For third-party tools, inspect the current tool schema or docs before choosing t | V2 command | V3 handling | | --- | --- | -| `uloop get-project-info` | Replace manually based on caller intent. Do not guess from the command name alone. | -| `uloop get-version` | Replace manually based on caller intent. Do not guess from the command name alone. | +| `uloop get-project-info` | Report only unless the user explicitly asks for removed command migration. Do not guess from the command name alone. | +| `uloop get-version` | Report only unless the user explicitly asks for removed command migration. Do not guess from the command name alone. | From e346b65ab8ff0f2e6e7c163f323dc6ae41621f5e Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 23 Jun 2026 00:29:04 +0900 Subject: [PATCH 7/9] Ignore Markdownlint in V3 option migration skill Clarify that Markdownlint is not required validation for the temporary V3 CLI option migration and must not block completion when it reports existing or unrelated formatting issues. --- .../v3-cli-invocation-migration/Skill/SKILL.md | 10 ++++++++++ .../Skill/references/first-party-v2-to-v3.md | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md index ae5491ffbb..3dfa7c1980 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/SKILL.md @@ -19,6 +19,16 @@ syntax in agent-facing docs and automation. Prefer `rg` for searches when available. If `rg` is unavailable, use the best available project search tool. Candidate discovery should be repository search plus context inspection, not a generated candidate list. +## Validation Rules + +- Verify this migration with targeted search passes and a focused diff review. +- Do not run Markdownlint as required validation for this migration. +- If Markdownlint runs anyway and fails, ignore that result for this task. + Do not stop, revert, or withhold completion solely because Markdownlint + reports existing or unrelated Markdown formatting violations. +- Do not fix Markdownlint findings as part of this migration unless the + finding is directly caused by changing a `uloop` option token. + ## Editing Rules - Only change option tokens inside confirmed `uloop` command invocations. diff --git a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md index 7e88524c3f..21e00d48c5 100644 --- a/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md +++ b/Packages/src/TemporarySkills~/v3-cli-invocation-migration/Skill/references/first-party-v2-to-v3.md @@ -9,6 +9,11 @@ scripts, and do not change non-`uloop` commands. A valid edit only replaces, adds, or removes a `uloop` option token and the boolean value attached to that option in the same invocation. +Markdownlint is not required validation for this migration. If Markdownlint +runs and reports existing or unrelated formatting issues, ignore that result, +continue the option migration, and report the lint result as non-blocking. Do +not fix Markdownlint findings unless the migration itself created them. + ## Search Checklist Prefer `rg` when available, but any repository search tool is acceptable. From 8d694afe6ff603f833e3ad1ed8a2830dc045cab0 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 23 Jun 2026 00:54:02 +0900 Subject: [PATCH 8/9] Remove skill Markdownlint configuration Drop the local markdownlint-cli2 configuration because the PR should not add project-level Markdownlint tooling for skill documentation. --- .markdownlint-cli2.jsonc | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .markdownlint-cli2.jsonc diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc deleted file mode 100644 index cd7e4d4ee1..0000000000 --- a/.markdownlint-cli2.jsonc +++ /dev/null @@ -1,12 +0,0 @@ -{ - "config": { - "MD013": false, - "MD060": false - }, - "globs": [ - "Packages/src/**/Skill/SKILL.md", - "Assets/Editor/CustomCommandSamples/**/Skill/SKILL.md", - ".agents/skills/**/SKILL.md", - ".claude/skills/**/SKILL.md" - ] -} From fd12a43b480c885bbff52bf16891e55b003af2ef Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 23 Jun 2026 01:20:36 +0900 Subject: [PATCH 9/9] Align review comment JSON casing Update skill output docs to describe PascalCase nested pause-point and sample fields, and keep busy-status details readable when lower-camel legacy data is present. --- .agents/skills/uloop-hello-world/SKILL.md | 6 ++-- .../skills/uloop-simulate-keyboard/SKILL.md | 2 +- .../uloop-simulate-mouse-input/SKILL.md | 2 +- .claude/skills/uloop-hello-world/SKILL.md | 6 ++-- .../skills/uloop-simulate-keyboard/SKILL.md | 2 +- .../uloop-simulate-mouse-input/SKILL.md | 2 +- .../HelloWorld/Skill/SKILL.md | 6 ++-- .../SimulateKeyboard/Skill/SKILL.md | 2 +- .../SimulateMouseInput/Skill/SKILL.md | 2 +- cli/internal/cli/busy_status.go | 11 +++++- cli/internal/cli/error_envelope_test.go | 34 +++++++++++++++++++ 11 files changed, 59 insertions(+), 16 deletions(-) diff --git a/.agents/skills/uloop-hello-world/SKILL.md b/.agents/skills/uloop-hello-world/SKILL.md index 9b1b7d8d89..2c0153fb28 100644 --- a/.agents/skills/uloop-hello-world/SKILL.md +++ b/.agents/skills/uloop-hello-world/SKILL.md @@ -41,9 +41,9 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp Returns JSON with: -- `message`: The greeting message -- `language`: Language used for greeting -- `timestamp`: Current timestamp (if enabled) +- `Message`: The greeting message +- `Language`: Language used for greeting +- `Timestamp`: Current timestamp (if enabled) ## Notes diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index a455ad064e..e898d1090a 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -94,7 +94,7 @@ Returns JSON with: - `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released - `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption - `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 +- `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 - `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 609af381d1..1df0d2a943 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -118,6 +118,6 @@ Returns JSON with: - `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released - `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption - `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `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 +- `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 Verify visual outcome with a follow-up screenshot. diff --git a/.claude/skills/uloop-hello-world/SKILL.md b/.claude/skills/uloop-hello-world/SKILL.md index 9b1b7d8d89..2c0153fb28 100644 --- a/.claude/skills/uloop-hello-world/SKILL.md +++ b/.claude/skills/uloop-hello-world/SKILL.md @@ -41,9 +41,9 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp Returns JSON with: -- `message`: The greeting message -- `language`: Language used for greeting -- `timestamp`: Current timestamp (if enabled) +- `Message`: The greeting message +- `Language`: Language used for greeting +- `Timestamp`: Current timestamp (if enabled) ## Notes diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index a455ad064e..e898d1090a 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -94,7 +94,7 @@ Returns JSON with: - `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released - `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption - `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 +- `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 - `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 609af381d1..1df0d2a943 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -118,6 +118,6 @@ Returns JSON with: - `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released - `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption - `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `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 +- `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 Verify visual outcome with a follow-up screenshot. diff --git a/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md b/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md index 9b1b7d8d89..2c0153fb28 100644 --- a/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md +++ b/Assets/Editor/CustomCommandSamples/HelloWorld/Skill/SKILL.md @@ -41,9 +41,9 @@ uloop hello-world --name "Carlos" --language spanish --no-include-timestamp Returns JSON with: -- `message`: The greeting message -- `language`: Language used for greeting -- `timestamp`: Current timestamp (if enabled) +- `Message`: The greeting message +- `Language`: Language used for greeting +- `Timestamp`: Current timestamp (if enabled) ## Notes diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index a455ad064e..e898d1090a 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -94,7 +94,7 @@ Returns JSON with: - `InterruptedByPausePoint` (boolean): True when Unity paused during Pause Point inspection and the input bookkeeping was safely released - `PausePointId` (string, nullable): The marker id when a `UloopPausePoint.Pause` marker caused the interruption - `PausePointHitCount` (integer, nullable): The marker hit count when a `UloopPausePoint.Pause` marker 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 +- `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 - `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 609af381d1..1df0d2a943 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -118,6 +118,6 @@ Returns JSON with: - `InterruptedByPausePoint`: True when Unity paused during Pause Point inspection and the input bookkeeping was safely released - `PausePointId`: The id from `UloopPausePoint.Pause("")` when it caused the interruption - `PausePointHitCount`: The hit count for that `UloopPausePoint.Pause("")` -- `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 +- `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 Verify visual outcome with a follow-up screenshot. diff --git a/cli/internal/cli/busy_status.go b/cli/internal/cli/busy_status.go index dcbe332c73..8009ff6e29 100644 --- a/cli/internal/cli/busy_status.go +++ b/cli/internal/cli/busy_status.go @@ -40,7 +40,7 @@ func writeBusyStatusEnvelope(writer io.Writer, message string, details serverBus } func serverBusyStatusDetailsFromError(err cliError) serverBusyStatusDetails { - data, ok := err.Details["Data"].(map[string]any) + data, ok := serverBusyDetailsData(err.Details) if !ok { return serverBusyStatusDetails{ requestedToolName: err.Command, @@ -59,6 +59,15 @@ func serverBusyStatusDetailsFromError(err cliError) serverBusyStatusDetails { } } +func serverBusyDetailsData(details map[string]any) (map[string]any, bool) { + data, ok := details["Data"].(map[string]any) + if ok { + return data, true + } + data, ok = details["data"].(map[string]any) + return data, ok +} + func optionalBool(hasValue bool, value bool) *bool { if !hasValue { return nil diff --git a/cli/internal/cli/error_envelope_test.go b/cli/internal/cli/error_envelope_test.go index b0a7d7f873..cc6684e5ae 100644 --- a/cli/internal/cli/error_envelope_test.go +++ b/cli/internal/cli/error_envelope_test.go @@ -380,6 +380,40 @@ func TestWriteClassifiedServerBusyRPCErrorWritesBusyStatus(t *testing.T) { } } +func TestWriteErrorEnvelopeServerBusyAcceptsLegacyLowercaseDataDetails(t *testing.T) { + // Verifies legacy lower-camel error details still preserve busy tool names in status output. + cliErr := cliError{ + ErrorCode: errorCodeUnityServerBusy, + Message: "Unity is busy.", + Command: "get-logs", + Details: map[string]any{ + "data": map[string]any{ + "runningToolName": "compile", + "requestedToolName": "get-logs", + "isPlaying": true, + "isPaused": false, + }, + }, + } + var stderr bytes.Buffer + + writeErrorEnvelope(&stderr, cliErr) + + var envelope cliStatusEnvelope + if err := json.Unmarshal(stderr.Bytes(), &envelope); err != nil { + t.Fatalf("stderr is not valid JSON: %v\n%s", err, stderr.String()) + } + if envelope.RunningToolName != "compile" || envelope.RequestedToolName != "get-logs" { + t.Fatalf("tool names mismatch: %#v", envelope) + } + if envelope.IsPlaying == nil || *envelope.IsPlaying != true { + t.Fatalf("isPlaying mismatch: %#v", envelope) + } + if envelope.IsPaused == nil || *envelope.IsPaused != false { + t.Fatalf("isPaused mismatch: %#v", envelope) + } +} + func TestWriteToolFailureClassifiesDispatchedDisconnect(t *testing.T) { var stderr bytes.Buffer