From ec3377aceea984d9369a25e153ece7a73a7f499a Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:08:52 +0900 Subject: [PATCH 1/9] Add wait-for-debug-break CLI support Add a native wait command that polls Unity pause state so agents can stop at Debug.Break without relying on noisy logs. Expose a lightweight play mode state bridge, add one-frame stepping for paused PlayMode inspection, and require the CLI version that contains the new command. --- .../Editor/CliSetupApplicationServiceTests.cs | 8 +- .../Editor/ControlPlayModeUseCaseTests.cs | 16 ++ .../JsonRpcProcessorCliVersionGateTests.cs | 53 +++++ .../Editor/PlayModeStateBridgeCommandTests.cs | 39 ++++ .../PlayModeStateBridgeCommandTests.cs.meta | 11 + Packages/src/Editor/Domain/CliConstants.cs | 2 +- .../ControlPlayMode/ControlPlayModeSchema.cs | 3 +- .../ControlPlayMode/ControlPlayModeUseCase.cs | 10 + .../Api/GetPlayModeStateResponse.cs | 14 ++ .../Api/GetPlayModeStateResponse.cs.meta | 11 + .../Api/InternalBridgeCommandRouter.cs | 6 + .../Api/PlayModeStateBridgeCommand.cs | 40 ++++ .../Api/PlayModeStateBridgeCommand.cs.meta | 11 + .../ToolContracts/UnityCliLoopConstants.cs | 1 + cli/contract.json | 2 +- cli/internal/cli/command_help.go | 2 +- cli/internal/cli/command_registry.go | 1 + cli/internal/cli/completion_test.go | 2 +- .../cli/control_play_mode_wait_test.go | 7 + cli/internal/cli/error_envelope.go | 3 +- cli/internal/cli/error_envelope_test.go | 2 +- cli/internal/cli/run.go | 2 + cli/internal/cli/wait_for_debug_break.go | 174 ++++++++++++++ cli/internal/cli/wait_for_debug_break_test.go | 221 ++++++++++++++++++ cli/internal/tools/default-tools.json | 9 +- 25 files changed, 635 insertions(+), 15 deletions(-) create mode 100644 Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs create mode 100644 Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs create mode 100644 Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs create mode 100644 Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs.meta create mode 100644 cli/internal/cli/wait_for_debug_break.go create mode 100644 cli/internal/cli/wait_for_debug_break_test.go diff --git a/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs b/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs index 0db1594228..fad24850f3 100644 --- a/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs +++ b/Assets/Tests/Editor/CliSetupApplicationServiceTests.cs @@ -31,14 +31,14 @@ public async Task InstallGlobalCliAsync_UsesMinimumRequiredCliReleaseTag() } [Test] - public void GetMinimumRequiredCliVersion_RequiresExternalSceneChangeFlagCliRelease() + public void GetMinimumRequiredCliVersion_RequiresDebugBreakWaitCliRelease() { - // Verifies this package release requires the CLI with external Scene change options. + // Verifies this package release requires the CLI with wait-for-debug-break support. CliSetupApplicationService service = new( new FakeCliInstallationDetector(new string[] { null }), new FakeNativeCliInstaller()); - Assert.That(service.GetMinimumRequiredCliVersion(), Is.EqualTo("3.0.0-beta.24")); + Assert.That(service.GetMinimumRequiredCliVersion(), Is.EqualTo("3.0.0-beta.25")); } [Test] @@ -49,7 +49,7 @@ public void GetMinimumRequiredCliReleaseTag_UsesCliGitHubReleaseTag() new FakeCliInstallationDetector(new string[] { null }), new FakeNativeCliInstaller()); - Assert.That(service.GetMinimumRequiredCliReleaseTag(), Is.EqualTo("cli-v3.0.0-beta.24")); + Assert.That(service.GetMinimumRequiredCliReleaseTag(), Is.EqualTo("cli-v3.0.0-beta.25")); } [Test] diff --git a/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs b/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs index 79ed756a91..914d37cb7e 100644 --- a/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs +++ b/Assets/Tests/Editor/ControlPlayModeUseCaseTests.cs @@ -34,5 +34,21 @@ public async Task ExecuteAsync_WhenStatusOnly_ReturnsCurrentPlayModeState() Assert.That(response.Message, Is.EqualTo("Play mode status")); } + + [Test] + public async Task ExecuteAsync_WhenNextFrameOutsidePlayMode_IgnoresRequest() + { + // Verifies that frame stepping never starts PlayMode as a side effect. + ControlPlayModeUseCase useCase = new ControlPlayModeUseCase(); + ControlPlayModeSchema schema = new ControlPlayModeSchema + { + Action = PlayModeAction.NextFrame, + }; + + ControlPlayModeResponse response = await useCase.ExecuteAsync(schema, CancellationToken.None); + + Assert.That(response.IsPlaying, Is.False); + Assert.That(response.Message, Is.EqualTo("Next frame ignored")); + } } } diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index 77e2ddd1a1..6cccef8dde 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -446,6 +446,59 @@ public async Task ProcessRequest_WhenInternalBridgeCommandRuns_SwitchesToMainThr } } + [Test] + public async Task ProcessRequest_WhenToolExecutionSlotIsBusy_AllowsPlayModeStateBridgeCommand() + { + // Verifies Debug.Break state polling bypasses the normal single-flight tool execution slot. + CapturingMainThreadDispatcher dispatcher = new(); + MainThreadSwitcher.RegisterService(dispatcher); + + UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; + ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); + UnityCliLoopToolRegistrarService service = new( + new EmptyInternalToolNameProvider(), + toolSettingsService, + new UnityCliLoopToolExecutionService()); + UnityCliLoopToolRegistrar.RegisterService(service); + service.RegisterCustomTool(new SingleFlightTestTool()); + + Task toolResponseTask = null; + Task bridgeResponseTask = null; + try + { + toolResponseTask = JsonRpcProcessor.ProcessRequestWithEarlyResponseAsync( + BuildToolRequest(SingleFlightTestTool.Name, 1), + CancellationToken.None, + (_, _) => Task.CompletedTask); + + Assert.That(dispatcher.PendingContinuationCount, Is.EqualTo(1)); + + bridgeResponseTask = JsonRpcProcessor.ProcessRequestWithEarlyResponseAsync( + BuildToolRequest(UnityCliLoopConstants.COMMAND_NAME_GET_PLAY_MODE_STATE, 2), + CancellationToken.None, + (_, _) => Task.CompletedTask); + + Assert.That(dispatcher.PendingContinuationCount, Is.EqualTo(2)); + Assert.That(bridgeResponseTask.IsCompleted, Is.False); + + dispatcher.RunContinuations(); + string bridgeResponse = await AwaitWithTimeout(bridgeResponseTask, TimeSpan.FromMilliseconds(200)); + JObject parsed = JObject.Parse(bridgeResponse); + + Assert.That(parsed["error"], Is.Null); + Assert.That(parsed["result"]?["IsPaused"], Is.Not.Null); + LogAssert.NoUnexpectedReceived(); + } + finally + { + dispatcher.RunContinuations(); + await DrainTaskIfNeeded(toolResponseTask); + await DrainTaskIfNeeded(bridgeResponseTask); + UnityCliLoopToolRegistrar.RegisterService(previousService); + RestoreEditorMainThreadDispatcher(); + } + } + [Test] public async Task ProcessRequest_WhenMainThreadSwitchIsCanceled_ReleasesExecutionGateWithoutError() { diff --git a/Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs b/Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs new file mode 100644 index 0000000000..4b957d66be --- /dev/null +++ b/Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Infrastructure; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Tests PlayMode state bridge responses without depending on Editor PlayMode transitions. + /// + [TestFixture] + public sealed class PlayModeStateBridgeCommandTests + { + [Test] + public void BuildResponse_WhenEditorIsPlayingAndPaused_ReturnsPausedMessage() + { + // Verifies Debug.Break polling can distinguish the paused PlayMode state. + GetPlayModeStateResponse response = PlayModeStateBridgeCommand.BuildResponse( + isPlaying: true, + isPaused: true); + + Assert.That(response.IsPlaying, Is.True); + Assert.That(response.IsPaused, Is.True); + Assert.That(response.Message, Does.Contain("paused")); + } + + [Test] + public void BuildResponse_WhenEditorIsNotPlaying_ReturnsUnpausedMessage() + { + // Verifies non-PlayMode idle state is reported as unpaused instead of a debug break. + GetPlayModeStateResponse response = PlayModeStateBridgeCommand.BuildResponse( + isPlaying: false, + isPaused: false); + + Assert.That(response.IsPlaying, Is.False); + Assert.That(response.IsPaused, Is.False); + Assert.That(response.Message, Does.Contain("not playing")); + } + } +} diff --git a/Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs.meta b/Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs.meta new file mode 100644 index 0000000000..282c113932 --- /dev/null +++ b/Assets/Tests/Editor/PlayModeStateBridgeCommandTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8078b77b04634104ae150077115bff47 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Domain/CliConstants.cs b/Packages/src/Editor/Domain/CliConstants.cs index aa953aac8b..1e063fecbd 100644 --- a/Packages/src/Editor/Domain/CliConstants.cs +++ b/Packages/src/Editor/Domain/CliConstants.cs @@ -6,7 +6,7 @@ namespace io.github.hatayama.UnityCliLoop.Domain public static class CliConstants { public const string EXECUTABLE_NAME = "uloop"; - public const string MINIMUM_REQUIRED_CLI_VERSION = "3.0.0-beta.24"; + public const string MINIMUM_REQUIRED_CLI_VERSION = "3.0.0-beta.25"; public const string MINIMUM_REQUIRED_CLI_RELEASE_TAG = CLI_RELEASE_TAG_PREFIX + MINIMUM_REQUIRED_CLI_VERSION; public const string VERSION_FLAG = "--version"; public const string SHORT_VERSION_FLAG = "-v"; diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeSchema.cs b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeSchema.cs index b518f0e2b7..94bcd8a5a7 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeSchema.cs +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeSchema.cs @@ -9,7 +9,8 @@ public enum PlayModeAction { Play = 0, Stop = 1, - Pause = 2 + Pause = 2, + NextFrame = 3 } /// diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs index c473551e69..33bfe69eac 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/ControlPlayModeUseCase.cs @@ -59,6 +59,16 @@ public Task ExecuteAsync(ControlPlayModeSchema paramete message = "Play mode paused"; break; + case PlayModeAction.NextFrame: + if (EditorApplication.isPlaying && EditorApplication.isPaused) + { + EditorApplication.Step(); + message = "Play mode advanced one frame"; + break; + } + message = "Next frame ignored"; + break; + default: message = $"Unknown action: {parameters.Action}"; break; diff --git a/Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs b/Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs new file mode 100644 index 0000000000..4689f45beb --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs @@ -0,0 +1,14 @@ +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// PlayMode pause state returned by the internal CLI polling bridge command. + /// + public class GetPlayModeStateResponse : UnityCliLoopToolResponse + { + public bool IsPlaying { get; set; } + public bool IsPaused { get; set; } + public string Message { get; set; } + } +} diff --git a/Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs.meta b/Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs.meta new file mode 100644 index 0000000000..7df38f7b5e --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Api/GetPlayModeStateResponse.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 77c592879d0841e78208eead5cffcda2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/Api/InternalBridgeCommandRouter.cs b/Packages/src/Editor/Infrastructure/Api/InternalBridgeCommandRouter.cs index d3c9d8ccec..f1c3b55138 100644 --- a/Packages/src/Editor/Infrastructure/Api/InternalBridgeCommandRouter.cs +++ b/Packages/src/Editor/Infrastructure/Api/InternalBridgeCommandRouter.cs @@ -15,6 +15,7 @@ public static bool IsInternalCommand(string commandName) { return commandName == UnityCliLoopConstants.COMMAND_NAME_GET_VERSION || commandName == UnityCliLoopConstants.COMMAND_NAME_GET_COMPILE_STATUS || + commandName == UnityCliLoopConstants.COMMAND_NAME_GET_PLAY_MODE_STATE || commandName == UnityCliLoopConstants.COMMAND_NAME_GET_TOOL_DETAILS; } @@ -37,6 +38,11 @@ public static UnityCliLoopToolResponse Execute(string commandName, JToken params return CompileStatusBridgeCommand.Execute(paramsToken); } + if (commandName == UnityCliLoopConstants.COMMAND_NAME_GET_PLAY_MODE_STATE) + { + return PlayModeStateBridgeCommand.Execute(); + } + throw new ArgumentException($"Unknown internal bridge command: {commandName}", nameof(commandName)); } } diff --git a/Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs b/Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs new file mode 100644 index 0000000000..c9880eddad --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs @@ -0,0 +1,40 @@ +using UnityEditor; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// Reports PlayMode pause state for CLI-side Debug.Break waiting without entering the normal tool execution slot. + /// + internal static class PlayModeStateBridgeCommand + { + public static GetPlayModeStateResponse Execute() + { + return BuildResponse(EditorApplication.isPlaying, EditorApplication.isPaused); + } + + internal static GetPlayModeStateResponse BuildResponse(bool isPlaying, bool isPaused) + { + return new GetPlayModeStateResponse + { + IsPlaying = isPlaying, + IsPaused = isPaused, + Message = CreateMessage(isPlaying, isPaused) + }; + } + + private static string CreateMessage(bool isPlaying, bool isPaused) + { + if (isPaused) + { + return "Unity Editor is paused."; + } + + if (isPlaying) + { + return "Unity Editor is playing and not paused."; + } + + return "Unity Editor is not playing and not paused."; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs.meta b/Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs.meta new file mode 100644 index 0000000000..18b4ab1393 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Api/PlayModeStateBridgeCommand.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db410abbf9d641cf9b9a485426a28882 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs index d92ca6a235..25a354405e 100644 --- a/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs +++ b/Packages/src/Editor/ToolContracts/UnityCliLoopConstants.cs @@ -68,6 +68,7 @@ public static UnityEditor.PackageManager.PackageInfo PackageInfo public const string COMMAND_NAME_GET_TOOL_DETAILS = "get-tool-details"; public const string COMMAND_NAME_GET_VERSION = "get-version"; public const string COMMAND_NAME_GET_COMPILE_STATUS = "get-compile-status"; + public const string COMMAND_NAME_GET_PLAY_MODE_STATE = "get-play-mode-state"; // Optional package names public const string PACKAGE_NAME_TEST_FRAMEWORK = "com.unity.test-framework"; diff --git a/cli/contract.json b/cli/contract.json index 6279babbe5..06e57c6b20 100644 --- a/cli/contract.json +++ b/cli/contract.json @@ -1,4 +1,4 @@ { "schemaVersion": 1, - "cliVersion": "3.0.0-beta.24" + "cliVersion": "3.0.0-beta.25" } diff --git a/cli/internal/cli/command_help.go b/cli/internal/cli/command_help.go index 9cf86f6733..55f6472382 100644 --- a/cli/internal/cli/command_help.go +++ b/cli/internal/cli/command_help.go @@ -211,7 +211,7 @@ func nativeCommandDescription(command string) (string, bool) { func nativeCommandUsesProject(command string) bool { switch command { - case launchCommandName, "list", "sync", "focus-window", skillsCommandName: + case launchCommandName, "list", "sync", "focus-window", waitForDebugBreakCommandName, skillsCommandName: return true default: return false diff --git a/cli/internal/cli/command_registry.go b/cli/internal/cli/command_registry.go index 78c7bafb50..165f016d2c 100644 --- a/cli/internal/cli/command_registry.go +++ b/cli/internal/cli/command_registry.go @@ -10,6 +10,7 @@ var nativeCommands = []nativeCommandEntry{ {name: "list", description: "Show Unity tools currently exposed by the Editor"}, {name: "sync", description: "Refresh .uloop/tools.json from the running Editor"}, {name: "focus-window", description: "Bring the Unity Editor window to the foreground"}, + {name: waitForDebugBreakCommandName, description: "Wait until Unity Editor pauses after Debug.Break"}, {name: "skills", description: "List, install, or uninstall agent skills"}, {name: "completion", description: "Print or install shell completion"}, {name: "install", description: "Configure the global uloop launcher binary"}, diff --git a/cli/internal/cli/completion_test.go b/cli/internal/cli/completion_test.go index 957cf64243..727e892b4a 100644 --- a/cli/internal/cli/completion_test.go +++ b/cli/internal/cli/completion_test.go @@ -26,7 +26,7 @@ func TestCompletionListCommandsIncludesNativeCommandsAndDefaultTools(t *testing. } output := stdout.String() - for _, command := range []string{"completion", "focus-window", "sync", "uninstall"} { + for _, command := range []string{"completion", "focus-window", "sync", "uninstall", waitForDebugBreakCommandName} { if !strings.Contains(output, command) { t.Fatalf("command %s was not listed: %s", command, output) } diff --git a/cli/internal/cli/control_play_mode_wait_test.go b/cli/internal/cli/control_play_mode_wait_test.go index d4256e6697..f7a184c73d 100644 --- a/cli/internal/cli/control_play_mode_wait_test.go +++ b/cli/internal/cli/control_play_mode_wait_test.go @@ -168,6 +168,13 @@ func TestControlPlayModeTimeoutSecondsAcceptsFloatSchemaValue(t *testing.T) { } } +// Verifies that one-frame stepping returns the direct tool response instead of polling PlayMode state. +func TestControlPlayModeActionCanWaitSkipsNextFrame(t *testing.T) { + if controlPlayModeActionCanWait("NextFrame") { + t.Fatal("NextFrame should not wait for a PlayMode state transition") + } +} + func serveRepeatedControlPlayModeResponse( listener net.Listener, serverErr chan<- error, diff --git a/cli/internal/cli/error_envelope.go b/cli/internal/cli/error_envelope.go index 0ae8169f76..72fd3a986e 100644 --- a/cli/internal/cli/error_envelope.go +++ b/cli/internal/cli/error_envelope.go @@ -23,6 +23,7 @@ const ( errorCodeCLIUpdateRequired = "CLI_UPDATE_REQUIRED" errorCodeCompileWaitTimeout = "COMPILE_WAIT_TIMEOUT" errorCodeControlPlayModeWaitTimeout = "CONTROL_PLAY_MODE_WAIT_TIMEOUT" + errorCodeDebugBreakAlreadyPaused = "DEBUG_BREAK_ALREADY_PAUSED" errorCodeInternalError = "INTERNAL_ERROR" errorPhaseArgumentParsing = "argument_parsing" @@ -488,7 +489,7 @@ func availableCommandNames(cache toolsCache) []string { func isSafeRetryCommand(command string) bool { switch command { - case "list", "sync", "get-version", "get-logs", "get-tool-details": + case "list", "sync", "get-version", "get-logs", "get-tool-details", "get-play-mode-state", waitForDebugBreakCommandName: return true default: return false diff --git a/cli/internal/cli/error_envelope_test.go b/cli/internal/cli/error_envelope_test.go index 83f384d978..ca9523a210 100644 --- a/cli/internal/cli/error_envelope_test.go +++ b/cli/internal/cli/error_envelope_test.go @@ -437,7 +437,7 @@ func TestClassifyConnectionAttemptUsesContextProjectRootFallback(t *testing.T) { func TestAvailableCommandNamesIncludesBuiltIns(t *testing.T) { names := availableCommandNames(toolsCache{}) - expectedBuiltIns := []string{"launch", "list", "sync", "focus-window", "skills", "completion", "install", "update"} + expectedBuiltIns := []string{"launch", "list", "sync", "focus-window", waitForDebugBreakCommandName, "skills", "completion", "install", "update"} for index, expected := range expectedBuiltIns { if names[index] != expected { t.Fatalf("built-in command mismatch: %#v", names) diff --git a/cli/internal/cli/run.go b/cli/internal/cli/run.go index 7f9ec89f3a..54eee9625c 100644 --- a/cli/internal/cli/run.go +++ b/cli/internal/cli/run.go @@ -86,6 +86,8 @@ func RunProjectLocal(ctx context.Context, args []string, stdout io.Writer, stder return runSync(ctx, connection, stdout, stderr) case "focus-window": return runFocusWindow(ctx, connection.ProjectRoot, stdout, stderr) + case waitForDebugBreakCommandName: + return runWaitForDebugBreak(ctx, connection, commandArgs, stdout, stderr) default: tool, cache, ok, err := findToolForCommand(connection.ProjectRoot, command) if err != nil { diff --git a/cli/internal/cli/wait_for_debug_break.go b/cli/internal/cli/wait_for_debug_break.go new file mode 100644 index 0000000000..4439aa8d5b --- /dev/null +++ b/cli/internal/cli/wait_for_debug_break.go @@ -0,0 +1,174 @@ +package cli + +import ( + "context" + "encoding/json" + "io" + "time" + + "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" +) + +const ( + waitForDebugBreakCommandName = "wait-for-debug-break" + playModeStateCommandName = "get-play-mode-state" + waitForDebugBreakStatusProbeTimeout = 5 * time.Second +) + +var ( + waitForDebugBreakPollInterval = 50 * time.Millisecond + queryPlayModeState = queryPlayModeStateFromUnity +) + +type playModeStateResponse struct { + IsPlaying bool `json:"IsPlaying"` + IsPaused bool `json:"IsPaused"` + Message string `json:"Message"` +} + +type waitForDebugBreakResponse struct { + Success bool `json:"Success"` + IsPlaying bool `json:"IsPlaying"` + IsPaused bool `json:"IsPaused"` + ElapsedMilliseconds int64 `json:"ElapsedMilliseconds"` + Message string `json:"Message"` +} + +func runWaitForDebugBreak( + ctx context.Context, + connection unityipc.Connection, + args []string, + stdout io.Writer, + stderr io.Writer, +) int { + if err := parseWaitForDebugBreakArgs(args); err != nil { + writeClassifiedError(stderr, err, errorContext{ + projectRoot: connection.ProjectRoot, + command: waitForDebugBreakCommandName, + }) + return 1 + } + + startedAt := time.Now() + spinner := newToolSpinner(stderr, waitForDebugBreakCommandName) + spinner.Update("Checking Unity play mode state...") + + initialState, err := queryPlayModeState(ctx, connection) + if err != nil { + spinner.Stop() + writeClassifiedError(stderr, err, errorContext{ + projectRoot: connection.ProjectRoot, + command: waitForDebugBreakCommandName, + }) + return 1 + } + if initialState.IsPaused { + spinner.Stop() + writeErrorEnvelope(stderr, debugBreakAlreadyPausedError(connection.ProjectRoot, initialState)) + return 1 + } + + spinner.Update("Waiting for Debug.Break...") + finalState, waitErr := waitForDebugBreak(ctx, connection, initialState) + spinner.Stop() + if waitErr != nil { + writeClassifiedError(stderr, waitErr, errorContext{ + projectRoot: connection.ProjectRoot, + command: waitForDebugBreakCommandName, + }) + return 1 + } + + response := waitForDebugBreakResponse{ + Success: true, + IsPlaying: finalState.IsPlaying, + IsPaused: finalState.IsPaused, + ElapsedMilliseconds: time.Since(startedAt).Milliseconds(), + Message: "Debug break observed.", + } + result, marshalErr := json.Marshal(response) + if marshalErr != nil { + writeClassifiedError(stderr, marshalErr, errorContext{ + projectRoot: connection.ProjectRoot, + command: waitForDebugBreakCommandName, + }) + return 1 + } + writeJSON(stdout, result) + return 0 +} + +func parseWaitForDebugBreakArgs(args []string) error { + for _, arg := range args { + return &argumentError{ + message: "Unknown wait-for-debug-break option: " + arg, + option: arg, + command: waitForDebugBreakCommandName, + nextActions: []string{"Run `uloop wait-for-debug-break --help` to inspect supported options."}, + } + } + + return nil +} + +func waitForDebugBreak( + ctx context.Context, + connection unityipc.Connection, + initialState playModeStateResponse, +) (playModeStateResponse, error) { + lastState := initialState + for { + state, err := queryPlayModeState(ctx, connection) + if err == nil { + lastState = state + if state.IsPaused { + return state, nil + } + } + + select { + case <-ctx.Done(): + return lastState, ctx.Err() + case <-time.After(waitForDebugBreakPollInterval): + } + } +} + +func queryPlayModeStateFromUnity(ctx context.Context, connection unityipc.Connection) (playModeStateResponse, error) { + probeContext, cancel := context.WithTimeout(ctx, waitForDebugBreakStatusProbeTimeout) + defer cancel() + + result, err := unityipc.NewClient(connection, version).Send( + probeContext, + playModeStateCommandName, + map[string]any{}, + ) + if err != nil { + return playModeStateResponse{}, err + } + + response := playModeStateResponse{} + if err := json.Unmarshal(result, &response); err != nil { + return playModeStateResponse{}, err + } + return response, nil +} + +func debugBreakAlreadyPausedError(projectRoot string, state playModeStateResponse) cliError { + return cliError{ + ErrorCode: errorCodeDebugBreakAlreadyPaused, + Phase: errorPhaseResponseWaiting, + Message: "Unity Editor is already paused before waiting for Debug.Break.", + Retryable: true, + SafeToRetry: true, + ProjectRoot: projectRoot, + Command: waitForDebugBreakCommandName, + NextActions: []string{ + "Run `uloop control-play-mode --action Play` to resume Unity from the existing pause, then run `uloop wait-for-debug-break` before triggering the target action.", + }, + Details: map[string]any{ + "isPlaying": state.IsPlaying, + "isPaused": state.IsPaused, + }, + } +} diff --git a/cli/internal/cli/wait_for_debug_break_test.go b/cli/internal/cli/wait_for_debug_break_test.go new file mode 100644 index 0000000000..2683b482ab --- /dev/null +++ b/cli/internal/cli/wait_for_debug_break_test.go @@ -0,0 +1,221 @@ +package cli + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "net" + "strings" + "testing" + "time" + + "github.com/hatayama/unity-cli-loop/cli/internal/unityipc" +) + +// Verifies wait-for-debug-break succeeds only after Unity transitions from running to paused. +func TestRunWaitForDebugBreakCompletesAfterPauseTransition(t *testing.T) { + states := []playModeStateResponse{ + {IsPlaying: true, IsPaused: false, Message: "Unity Editor is playing and not paused."}, + {IsPlaying: true, IsPaused: true, Message: "Unity Editor is paused."}, + } + replaceQueryPlayModeState(t, func(context.Context, unityipc.Connection) (playModeStateResponse, error) { + if len(states) == 0 { + t.Fatal("unexpected extra play mode state query") + } + state := states[0] + states = states[1:] + return state, nil + }) + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := runWaitForDebugBreak( + context.Background(), + unityipc.Connection{ProjectRoot: t.TempDir()}, + []string{}, + &stdout, + &stderr) + + if code != 0 { + t.Fatalf("wait-for-debug-break failed with %d: %s", code, stderr.String()) + } + response := waitForDebugBreakResponse{} + if err := json.Unmarshal(stdout.Bytes(), &response); err != nil { + t.Fatalf("failed to decode stdout: %v\n%s", err, stdout.String()) + } + if !response.Success || !response.IsPaused { + t.Fatalf("response state mismatch: %#v", response) + } + if response.Message != "Debug break observed." { + t.Fatalf("response message mismatch: %s", response.Message) + } +} + +// Verifies wait-for-debug-break rejects an already-paused Editor to avoid reporting a stale break. +func TestRunWaitForDebugBreakFailsWhenUnityAlreadyPaused(t *testing.T) { + replaceQueryPlayModeState(t, func(context.Context, unityipc.Connection) (playModeStateResponse, error) { + return playModeStateResponse{IsPlaying: true, IsPaused: true}, nil + }) + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := runWaitForDebugBreak( + context.Background(), + unityipc.Connection{ProjectRoot: t.TempDir()}, + []string{}, + &stdout, + &stderr) + + if code != 1 { + t.Fatalf("expected already-paused failure, got %d with stdout %s", code, stdout.String()) + } + if !strings.Contains(stderr.String(), errorCodeDebugBreakAlreadyPaused) { + t.Fatalf("already-paused error missing from stderr: %s", stderr.String()) + } +} + +// Verifies Debug.Break polling keeps the last state when the caller cancels the wait. +func TestWaitForDebugBreakReturnsLastStateWhenCanceled(t *testing.T) { + originalPollInterval := waitForDebugBreakPollInterval + waitForDebugBreakPollInterval = time.Millisecond + t.Cleanup(func() { + waitForDebugBreakPollInterval = originalPollInterval + }) + replaceQueryPlayModeState(t, func(context.Context, unityipc.Connection) (playModeStateResponse, error) { + return playModeStateResponse{IsPlaying: true, IsPaused: false}, nil + }) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + state, err := waitForDebugBreak( + ctx, + unityipc.Connection{ProjectRoot: t.TempDir()}, + playModeStateResponse{IsPlaying: true, IsPaused: false}) + if err == nil { + t.Fatalf("wait should be canceled: %#v", state) + } + if state.IsPaused { + t.Fatalf("last state should remain unpaused: %#v", state) + } +} + +// Verifies the Unity probe uses the internal get-play-mode-state bridge command. +func TestQueryPlayModeStateFromUnityUsesInternalBridgeCommand(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + defer func() { + _ = listener.Close() + }() + + requests := make(chan string, 1) + serverErr := make(chan error, 1) + go servePlayModeStateResponse( + listener, + requests, + serverErr, + `{"IsPlaying":true,"IsPaused":true,"Message":"Unity Editor is paused."}`) + + connection := unityipc.Connection{ + Endpoint: unityipc.Endpoint{ + Network: "tcp", + Address: listener.Addr().String(), + }, + ProjectRoot: t.TempDir(), + } + + response, err := queryPlayModeStateFromUnity(context.Background(), connection) + if err != nil { + t.Fatalf("queryPlayModeStateFromUnity failed: %v", err) + } + if !response.IsPaused { + t.Fatalf("response state mismatch: %#v", response) + } + if method := readPlayModeStateMethod(t, requests); method != playModeStateCommandName { + t.Fatalf("method mismatch: %s", method) + } + + select { + case err := <-serverErr: + t.Fatalf("server failed: %v", err) + default: + } +} + +// Verifies wait-for-debug-break accepts no command-specific options. +func TestParseWaitForDebugBreakArgsAcceptsNoOptions(t *testing.T) { + if err := parseWaitForDebugBreakArgs([]string{}); err != nil { + t.Fatalf("parseWaitForDebugBreakArgs failed: %v", err) + } +} + +// Verifies wait-for-debug-break rejects extra options before contacting Unity. +func TestParseWaitForDebugBreakArgsRejectsOptions(t *testing.T) { + if err := parseWaitForDebugBreakArgs([]string{"--unknown"}); err == nil { + t.Fatal("expected option validation error") + } +} + +func replaceQueryPlayModeState( + t *testing.T, + replacement func(context.Context, unityipc.Connection) (playModeStateResponse, error), +) { + t.Helper() + original := queryPlayModeState + queryPlayModeState = replacement + t.Cleanup(func() { + queryPlayModeState = original + }) +} + +func servePlayModeStateResponse( + listener net.Listener, + requests chan<- string, + serverErr chan<- error, + result string, +) { + conn, err := listener.Accept() + if err != nil { + serverErr <- err + return + } + + payload, err := unityipc.Read(bufio.NewReader(conn)) + if err != nil { + _ = conn.Close() + serverErr <- err + return + } + + request := struct { + Method string `json:"method"` + }{} + if err := json.Unmarshal(payload, &request); err != nil { + _ = conn.Close() + serverErr <- err + return + } + requests <- request.Method + + response := []byte(fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, result)) + if err := unityipc.Write(conn, response); err != nil { + _ = conn.Close() + serverErr <- err + return + } + _ = conn.Close() +} + +func readPlayModeStateMethod(t *testing.T, requests <-chan string) string { + t.Helper() + select { + case method := <-requests: + return method + case <-time.After(time.Second): + t.Fatal("timed out waiting for request") + return "" + } +} diff --git a/cli/internal/tools/default-tools.json b/cli/internal/tools/default-tools.json index 904923ef8f..cbafb1f4fa 100644 --- a/cli/internal/tools/default-tools.json +++ b/cli/internal/tools/default-tools.json @@ -1,5 +1,5 @@ { - "version": "3.0.0-beta.24", + "version": "3.0.0-beta.25", "tools": [ { "name": "compile", @@ -306,17 +306,18 @@ }, { "name": "control-play-mode", - "description": "Control Unity Editor play mode (play/stop/pause)", + "description": "Control Unity Editor play mode (play/stop/pause/next frame)", "inputSchema": { "type": "object", "properties": { "Action": { "type": "string", - "description": "Action to perform: Play - Start play mode, Stop - Stop play mode, Pause - Pause play mode", + "description": "Action to perform: Play - Start play mode, Stop - Stop play mode, Pause - Pause play mode, NextFrame - Advance one frame only when paused in play mode", "enum": [ "Play", "Stop", - "Pause" + "Pause", + "NextFrame" ], "default": "Play" }, From 79a68be577130fb327d19e04801a7ddc60da0eea Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:08:59 +0900 Subject: [PATCH 2/9] Document debug break wait workflow Add the wait-for-debug-break skill and update control-play-mode guidance so agents know how to step paused PlayMode frames and resume after Debug.Break inspection. --- .../skills/uloop-control-play-mode/SKILL.md | 12 ++-- .../uloop-wait-for-debug-break/SKILL.md | 68 +++++++++++++++++++ .../skills/uloop-control-play-mode/SKILL.md | 12 ++-- .../uloop-wait-for-debug-break/SKILL.md | 68 +++++++++++++++++++ .../WaitForDebugBreak/Skill/SKILL.md | 68 +++++++++++++++++++ .../ControlPlayMode/Skill/SKILL.md | 12 ++-- 6 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 .agents/skills/uloop-wait-for-debug-break/SKILL.md create mode 100644 .claude/skills/uloop-wait-for-debug-break/SKILL.md create mode 100644 Packages/src/Editor/CliOnlyTools~/WaitForDebugBreak/Skill/SKILL.md diff --git a/.agents/skills/uloop-control-play-mode/SKILL.md b/.agents/skills/uloop-control-play-mode/SKILL.md index 80cc725ba6..677925847b 100644 --- a/.agents/skills/uloop-control-play-mode/SKILL.md +++ b/.agents/skills/uloop-control-play-mode/SKILL.md @@ -1,12 +1,12 @@ --- name: uloop-control-play-mode toolName: control-play-mode -description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection." +description: "Control Unity Editor Play Mode. Use to start, stop, pause, or advance one paused PlayMode frame for runtime behavior checks and frame inspection." --- # uloop control-play-mode -Control Unity Editor play mode (play/stop/pause). +Control Unity Editor play mode (play/stop/pause/next frame). ## Usage @@ -18,7 +18,7 @@ uloop control-play-mode [options] | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` | +| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause`, `NextFrame` | | `--timeout-seconds` | integer | `180` | Maximum seconds to wait for the requested play mode state | ## Global Options @@ -41,6 +41,9 @@ uloop control-play-mode --action Stop # Pause play mode uloop control-play-mode --action Pause + +# Advance one frame while paused in play mode +uloop control-play-mode --action NextFrame ``` ## Output @@ -55,5 +58,6 @@ Returns JSON with the current play mode state: - Play action starts the game in the Unity Editor (also resumes from pause) - Stop action exits play mode and returns to edit mode - Pause action pauses the game while remaining in play mode +- NextFrame action advances one frame only when Unity is already paused in PlayMode; otherwise it is ignored - Useful for automated testing workflows -- The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. +- Play, Stop, and Pause wait for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.agents/skills/uloop-wait-for-debug-break/SKILL.md b/.agents/skills/uloop-wait-for-debug-break/SKILL.md new file mode 100644 index 0000000000..9a6d6edb21 --- /dev/null +++ b/.agents/skills/uloop-wait-for-debug-break/SKILL.md @@ -0,0 +1,68 @@ +--- +name: uloop-wait-for-debug-break +description: "Wait until Unity Editor pauses after Debug.Break. Use when log output is too noisy to inspect live, or when input simulation needs a precise pause point for state inspection." +--- + +# uloop wait-for-debug-break + +Wait until Unity Editor becomes paused after a `Debug.Break()` call. + +## Usage + +```bash +uloop wait-for-debug-break +``` + +## Parameters + +None. + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Workflow + +1. Add a temporary marker before the break point: + +```csharp +UnityEngine.Debug.Log("[DebugBreak] reached target state"); +UnityEngine.Debug.Break(); +``` + +2. Start the wait command before triggering the behavior: + +```bash +uloop wait-for-debug-break +``` + +3. Trigger the action, input simulation, or reproduction path. +4. When the command returns success, inspect Unity Console, Hierarchy, Inspector, or other uLoop state commands while Unity is paused. +5. Resume Unity after inspection: + +```bash +uloop control-play-mode --action Play +``` + +6. Remove the temporary `Debug.Break()` and marker log unless the break is intentionally part of the debugging workflow. + +## Output + +Returns JSON with: + +- `Success`: Whether the pause was observed +- `IsPlaying`: Current Unity PlayMode state +- `IsPaused`: Current Unity paused state +- `ElapsedMilliseconds`: Time spent waiting +- `Message`: Status message + +## Notes + +- Start this command while Unity is not paused. If Unity is already paused, the command fails to avoid confusing an old pause with the target `Debug.Break()`. +- This command polls a lightweight internal PlayMode-state bridge and can be left waiting while other uLoop commands are used. +- This command only observes Unity pause state. If it was started accidentally, stop the CLI process from the runner/session that started it. +- Resume a Debug.Break pause with `uloop control-play-mode --action Play`; existing control-play-mode behavior clears `EditorApplication.isPaused`. +- Remove the temporary `Debug.Break()` call from code to prevent future pauses. +- Use marker logs when several code paths can pause Unity, then inspect the Console after the wait completes. diff --git a/.claude/skills/uloop-control-play-mode/SKILL.md b/.claude/skills/uloop-control-play-mode/SKILL.md index 80cc725ba6..677925847b 100644 --- a/.claude/skills/uloop-control-play-mode/SKILL.md +++ b/.claude/skills/uloop-control-play-mode/SKILL.md @@ -1,12 +1,12 @@ --- name: uloop-control-play-mode toolName: control-play-mode -description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection." +description: "Control Unity Editor Play Mode. Use to start, stop, pause, or advance one paused PlayMode frame for runtime behavior checks and frame inspection." --- # uloop control-play-mode -Control Unity Editor play mode (play/stop/pause). +Control Unity Editor play mode (play/stop/pause/next frame). ## Usage @@ -18,7 +18,7 @@ uloop control-play-mode [options] | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` | +| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause`, `NextFrame` | | `--timeout-seconds` | integer | `180` | Maximum seconds to wait for the requested play mode state | ## Global Options @@ -41,6 +41,9 @@ uloop control-play-mode --action Stop # Pause play mode uloop control-play-mode --action Pause + +# Advance one frame while paused in play mode +uloop control-play-mode --action NextFrame ``` ## Output @@ -55,5 +58,6 @@ Returns JSON with the current play mode state: - Play action starts the game in the Unity Editor (also resumes from pause) - Stop action exits play mode and returns to edit mode - Pause action pauses the game while remaining in play mode +- NextFrame action advances one frame only when Unity is already paused in PlayMode; otherwise it is ignored - Useful for automated testing workflows -- The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. +- Play, Stop, and Pause wait for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. diff --git a/.claude/skills/uloop-wait-for-debug-break/SKILL.md b/.claude/skills/uloop-wait-for-debug-break/SKILL.md new file mode 100644 index 0000000000..9a6d6edb21 --- /dev/null +++ b/.claude/skills/uloop-wait-for-debug-break/SKILL.md @@ -0,0 +1,68 @@ +--- +name: uloop-wait-for-debug-break +description: "Wait until Unity Editor pauses after Debug.Break. Use when log output is too noisy to inspect live, or when input simulation needs a precise pause point for state inspection." +--- + +# uloop wait-for-debug-break + +Wait until Unity Editor becomes paused after a `Debug.Break()` call. + +## Usage + +```bash +uloop wait-for-debug-break +``` + +## Parameters + +None. + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Workflow + +1. Add a temporary marker before the break point: + +```csharp +UnityEngine.Debug.Log("[DebugBreak] reached target state"); +UnityEngine.Debug.Break(); +``` + +2. Start the wait command before triggering the behavior: + +```bash +uloop wait-for-debug-break +``` + +3. Trigger the action, input simulation, or reproduction path. +4. When the command returns success, inspect Unity Console, Hierarchy, Inspector, or other uLoop state commands while Unity is paused. +5. Resume Unity after inspection: + +```bash +uloop control-play-mode --action Play +``` + +6. Remove the temporary `Debug.Break()` and marker log unless the break is intentionally part of the debugging workflow. + +## Output + +Returns JSON with: + +- `Success`: Whether the pause was observed +- `IsPlaying`: Current Unity PlayMode state +- `IsPaused`: Current Unity paused state +- `ElapsedMilliseconds`: Time spent waiting +- `Message`: Status message + +## Notes + +- Start this command while Unity is not paused. If Unity is already paused, the command fails to avoid confusing an old pause with the target `Debug.Break()`. +- This command polls a lightweight internal PlayMode-state bridge and can be left waiting while other uLoop commands are used. +- This command only observes Unity pause state. If it was started accidentally, stop the CLI process from the runner/session that started it. +- Resume a Debug.Break pause with `uloop control-play-mode --action Play`; existing control-play-mode behavior clears `EditorApplication.isPaused`. +- Remove the temporary `Debug.Break()` call from code to prevent future pauses. +- Use marker logs when several code paths can pause Unity, then inspect the Console after the wait completes. diff --git a/Packages/src/Editor/CliOnlyTools~/WaitForDebugBreak/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/WaitForDebugBreak/Skill/SKILL.md new file mode 100644 index 0000000000..9a6d6edb21 --- /dev/null +++ b/Packages/src/Editor/CliOnlyTools~/WaitForDebugBreak/Skill/SKILL.md @@ -0,0 +1,68 @@ +--- +name: uloop-wait-for-debug-break +description: "Wait until Unity Editor pauses after Debug.Break. Use when log output is too noisy to inspect live, or when input simulation needs a precise pause point for state inspection." +--- + +# uloop wait-for-debug-break + +Wait until Unity Editor becomes paused after a `Debug.Break()` call. + +## Usage + +```bash +uloop wait-for-debug-break +``` + +## Parameters + +None. + +## Global Options + +| Option | Description | +|--------|-------------| +| `--project-path ` | Optional. Use only when the target Unity project is not the current directory. | + +## Workflow + +1. Add a temporary marker before the break point: + +```csharp +UnityEngine.Debug.Log("[DebugBreak] reached target state"); +UnityEngine.Debug.Break(); +``` + +2. Start the wait command before triggering the behavior: + +```bash +uloop wait-for-debug-break +``` + +3. Trigger the action, input simulation, or reproduction path. +4. When the command returns success, inspect Unity Console, Hierarchy, Inspector, or other uLoop state commands while Unity is paused. +5. Resume Unity after inspection: + +```bash +uloop control-play-mode --action Play +``` + +6. Remove the temporary `Debug.Break()` and marker log unless the break is intentionally part of the debugging workflow. + +## Output + +Returns JSON with: + +- `Success`: Whether the pause was observed +- `IsPlaying`: Current Unity PlayMode state +- `IsPaused`: Current Unity paused state +- `ElapsedMilliseconds`: Time spent waiting +- `Message`: Status message + +## Notes + +- Start this command while Unity is not paused. If Unity is already paused, the command fails to avoid confusing an old pause with the target `Debug.Break()`. +- This command polls a lightweight internal PlayMode-state bridge and can be left waiting while other uLoop commands are used. +- This command only observes Unity pause state. If it was started accidentally, stop the CLI process from the runner/session that started it. +- Resume a Debug.Break pause with `uloop control-play-mode --action Play`; existing control-play-mode behavior clears `EditorApplication.isPaused`. +- Remove the temporary `Debug.Break()` call from code to prevent future pauses. +- Use marker logs when several code paths can pause Unity, then inspect the Console after the wait completes. diff --git a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md index 80cc725ba6..677925847b 100644 --- a/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ControlPlayMode/Skill/SKILL.md @@ -1,12 +1,12 @@ --- name: uloop-control-play-mode toolName: control-play-mode -description: "Control Unity Editor Play Mode. Use to start, stop, or pause Play Mode for runtime behavior checks and frame inspection." +description: "Control Unity Editor Play Mode. Use to start, stop, pause, or advance one paused PlayMode frame for runtime behavior checks and frame inspection." --- # uloop control-play-mode -Control Unity Editor play mode (play/stop/pause). +Control Unity Editor play mode (play/stop/pause/next frame). ## Usage @@ -18,7 +18,7 @@ uloop control-play-mode [options] | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause` | +| `--action` | string | `Play` | Action to perform: `Play`, `Stop`, `Pause`, `NextFrame` | | `--timeout-seconds` | integer | `180` | Maximum seconds to wait for the requested play mode state | ## Global Options @@ -41,6 +41,9 @@ uloop control-play-mode --action Stop # Pause play mode uloop control-play-mode --action Pause + +# Advance one frame while paused in play mode +uloop control-play-mode --action NextFrame ``` ## Output @@ -55,5 +58,6 @@ Returns JSON with the current play mode state: - Play action starts the game in the Unity Editor (also resumes from pause) - Stop action exits play mode and returns to edit mode - Pause action pauses the game while remaining in play mode +- NextFrame action advances one frame only when Unity is already paused in PlayMode; otherwise it is ignored - Useful for automated testing workflows -- The command waits for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. +- Play, Stop, and Pause wait for the requested state before returning. Increase `--timeout-seconds` for projects with slow PlayMode entry. From c8688b4cb5fa3df41c762b48558752ba8662c3a9 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:18:28 +0900 Subject: [PATCH 3/9] Document scheduled Debug.Break inspection Explain how execute-dynamic-code can schedule a Debug.Break from runtime inspection code and pair it with wait-for-debug-break for frozen-state investigation. --- .../ExecuteDynamicCode/Skill/SKILL.md | 2 +- .../Skill/references/playmode-inspection.md | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index d29fcdc9c4..3ed61bb1fa 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -83,4 +83,4 @@ For detailed code examples, refer to these files: - **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md) - InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls - **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md) - - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation + - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation, scheduled Debug.Break inspection diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/playmode-inspection.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/playmode-inspection.md index 7cecc3d749..16a8f4eb15 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/playmode-inspection.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/references/playmode-inspection.md @@ -117,6 +117,39 @@ Transform t = target.transform; return $"{target.name}: pos={t.position}, rot={t.rotation.eulerAngles}, scale={t.localScale}"; ``` +## Schedule Debug.Break for Later Inspection + +Use this pattern when direct inspection is not enough and you want Unity to pause after a short delay or after the next runtime updates. Start `uloop wait-for-debug-break` in another session before triggering this snippet, or give the delay enough time to start the wait immediately after scheduling it. If Unity pauses before the wait command starts, the wait command will reject the stale pause. + +```csharp +using UnityEditor; +using UnityEngine; + +double startedAt = EditorApplication.timeSinceStartup; +double delaySeconds = 3d; + +EditorApplication.CallbackFunction tick = null; +tick = () => +{ + if (EditorApplication.timeSinceStartup - startedAt < delaySeconds) + { + return; + } + + EditorApplication.update -= tick; + + GameObject player = GameObject.Find("Player"); + Vector3 position = player != null ? player.transform.position : Vector3.zero; + Debug.Log($"[DebugBreak] delayed break after {delaySeconds}s, playerExists={player != null}, playerPosition={position}"); + Debug.Break(); +}; + +EditorApplication.update += tick; +return $"Scheduled Debug.Break in {delaySeconds} seconds."; +``` + +After the wait command observes the pause, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. Resume PlayMode with `uloop control-play-mode --action Play` when inspection is complete. + ## Check Rigidbody State ```csharp From a76c5edb9cebab67b682007e95a641568a377106 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:22:05 +0900 Subject: [PATCH 4/9] Regenerate execute dynamic code skill docs Update generated agent skill copies after documenting scheduled Debug.Break inspection in the source execute-dynamic-code skill. --- .../uloop-execute-dynamic-code/SKILL.md | 2 +- .../references/playmode-inspection.md | 33 +++++++++++++++++++ .../uloop-execute-dynamic-code/SKILL.md | 2 +- .../references/playmode-inspection.md | 33 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index d29fcdc9c4..3ed61bb1fa 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -83,4 +83,4 @@ For detailed code examples, refer to these files: - **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md) - InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls - **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md) - - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation + - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation, scheduled Debug.Break inspection diff --git a/.agents/skills/uloop-execute-dynamic-code/references/playmode-inspection.md b/.agents/skills/uloop-execute-dynamic-code/references/playmode-inspection.md index 7cecc3d749..16a8f4eb15 100644 --- a/.agents/skills/uloop-execute-dynamic-code/references/playmode-inspection.md +++ b/.agents/skills/uloop-execute-dynamic-code/references/playmode-inspection.md @@ -117,6 +117,39 @@ Transform t = target.transform; return $"{target.name}: pos={t.position}, rot={t.rotation.eulerAngles}, scale={t.localScale}"; ``` +## Schedule Debug.Break for Later Inspection + +Use this pattern when direct inspection is not enough and you want Unity to pause after a short delay or after the next runtime updates. Start `uloop wait-for-debug-break` in another session before triggering this snippet, or give the delay enough time to start the wait immediately after scheduling it. If Unity pauses before the wait command starts, the wait command will reject the stale pause. + +```csharp +using UnityEditor; +using UnityEngine; + +double startedAt = EditorApplication.timeSinceStartup; +double delaySeconds = 3d; + +EditorApplication.CallbackFunction tick = null; +tick = () => +{ + if (EditorApplication.timeSinceStartup - startedAt < delaySeconds) + { + return; + } + + EditorApplication.update -= tick; + + GameObject player = GameObject.Find("Player"); + Vector3 position = player != null ? player.transform.position : Vector3.zero; + Debug.Log($"[DebugBreak] delayed break after {delaySeconds}s, playerExists={player != null}, playerPosition={position}"); + Debug.Break(); +}; + +EditorApplication.update += tick; +return $"Scheduled Debug.Break in {delaySeconds} seconds."; +``` + +After the wait command observes the pause, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. Resume PlayMode with `uloop control-play-mode --action Play` when inspection is complete. + ## Check Rigidbody State ```csharp diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index d29fcdc9c4..3ed61bb1fa 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -83,4 +83,4 @@ For detailed code examples, refer to these files: - **PlayMode UI controls**: See [references/playmode-ui-controls.md](references/playmode-ui-controls.md) - InputField, Slider, Toggle, Dropdown, drag & drop simulation, list all UI controls - **PlayMode inspection**: See [references/playmode-inspection.md](references/playmode-inspection.md) - - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation + - Scene info, game state via reflection, physics state, raycast checks, GameObject search, position/rotation, scheduled Debug.Break inspection diff --git a/.claude/skills/uloop-execute-dynamic-code/references/playmode-inspection.md b/.claude/skills/uloop-execute-dynamic-code/references/playmode-inspection.md index 7cecc3d749..16a8f4eb15 100644 --- a/.claude/skills/uloop-execute-dynamic-code/references/playmode-inspection.md +++ b/.claude/skills/uloop-execute-dynamic-code/references/playmode-inspection.md @@ -117,6 +117,39 @@ Transform t = target.transform; return $"{target.name}: pos={t.position}, rot={t.rotation.eulerAngles}, scale={t.localScale}"; ``` +## Schedule Debug.Break for Later Inspection + +Use this pattern when direct inspection is not enough and you want Unity to pause after a short delay or after the next runtime updates. Start `uloop wait-for-debug-break` in another session before triggering this snippet, or give the delay enough time to start the wait immediately after scheduling it. If Unity pauses before the wait command starts, the wait command will reject the stale pause. + +```csharp +using UnityEditor; +using UnityEngine; + +double startedAt = EditorApplication.timeSinceStartup; +double delaySeconds = 3d; + +EditorApplication.CallbackFunction tick = null; +tick = () => +{ + if (EditorApplication.timeSinceStartup - startedAt < delaySeconds) + { + return; + } + + EditorApplication.update -= tick; + + GameObject player = GameObject.Find("Player"); + Vector3 position = player != null ? player.transform.position : Vector3.zero; + Debug.Log($"[DebugBreak] delayed break after {delaySeconds}s, playerExists={player != null}, playerPosition={position}"); + Debug.Break(); +}; + +EditorApplication.update += tick; +return $"Scheduled Debug.Break in {delaySeconds} seconds."; +``` + +After the wait command observes the pause, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. Resume PlayMode with `uloop control-play-mode --action Play` when inspection is complete. + ## Check Rigidbody State ```csharp From b1ea5f96a4fd3992f7d90a1c76b4a6d3b1554bd5 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:22:17 +0900 Subject: [PATCH 5/9] Document input Debug.Break verification workflows Explain how keyboard, gameplay mouse, and UI mouse simulations can pair with scheduled Debug.Break waits when screenshots cannot prove that input changed runtime state. --- .agents/skills/uloop-simulate-keyboard/SKILL.md | 13 ++++++++++++- .agents/skills/uloop-simulate-mouse-input/SKILL.md | 13 ++++++++++++- .agents/skills/uloop-simulate-mouse-ui/SKILL.md | 13 ++++++++++++- .claude/skills/uloop-simulate-keyboard/SKILL.md | 13 ++++++++++++- .claude/skills/uloop-simulate-mouse-input/SKILL.md | 13 ++++++++++++- .claude/skills/uloop-simulate-mouse-ui/SKILL.md | 13 ++++++++++++- .../FirstPartyTools/SimulateKeyboard/Skill/SKILL.md | 13 ++++++++++++- .../SimulateMouseInput/Skill/SKILL.md | 13 ++++++++++++- .../FirstPartyTools/SimulateMouseUi/Skill/SKILL.md | 13 ++++++++++++- 9 files changed, 108 insertions(+), 9 deletions(-) diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index debad2993c..aaf833f3ad 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -14,7 +14,18 @@ Simulate keyboard input on Unity PlayMode: $ARGUMENTS 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Execute the appropriate `uloop simulate-keyboard` command 3. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering` -4. Report what happened +4. If a screenshot cannot prove the gameplay state changed, use the Debug.Break verification workflow below +5. Report what happened + +## Debug.Break Verification Workflow + +Use this when input appears to fire but the result is hard to verify visually, such as pressing Space to jump and needing to confirm the player actually entered the jump state. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. +3. Trigger the keyboard input before the delay expires, for example `uloop simulate-keyboard --action Press --key Space`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index da66d0b1fd..d7cd20c0e8 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -15,7 +15,18 @@ Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS 2. For Click/LongPress: determine the target screen position (use `uloop screenshot` to find coordinates) 3. Execute the appropriate `uloop simulate-mouse-input` command 4. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering` -5. Report what happened +5. If a screenshot cannot prove the gameplay state changed, use the Debug.Break verification workflow below +6. Report what happened + +## Debug.Break Verification Workflow + +Use this when mouse input appears to fire but the resulting gameplay state is hard to confirm visually, such as a click that should place or destroy an object, a mouse delta that should rotate a camera, or a scroll action that should change a selected slot. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the mouse input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. +3. Trigger the mouse input before the delay expires, for example `uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index 54e34d6939..15c7a6390f 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -16,7 +16,18 @@ Simulate mouse interaction on Unity PlayMode UI: $ARGUMENTS 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 appropriate `uloop simulate-mouse-ui` command 5. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering --annotate-elements` -6. Report what happened +6. If a screenshot cannot prove the UI state changed, use the Debug.Break verification workflow below +7. Report what happened + +## Debug.Break Verification Workflow + +Use this when UI pointer events appear to fire but the resulting state is hard to confirm visually, such as a button callback that should update game state, a long-press that should open a menu, or a drag/drop action that should mutate inventory data. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the UI input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. +3. Trigger the UI mouse action before the delay expires, for example `uloop simulate-mouse-ui --action Click --x 400 --y 300`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index debad2993c..aaf833f3ad 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -14,7 +14,18 @@ Simulate keyboard input on Unity PlayMode: $ARGUMENTS 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Execute the appropriate `uloop simulate-keyboard` command 3. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering` -4. Report what happened +4. If a screenshot cannot prove the gameplay state changed, use the Debug.Break verification workflow below +5. Report what happened + +## Debug.Break Verification Workflow + +Use this when input appears to fire but the result is hard to verify visually, such as pressing Space to jump and needing to confirm the player actually entered the jump state. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. +3. Trigger the keyboard input before the delay expires, for example `uloop simulate-keyboard --action Press --key Space`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index da66d0b1fd..d7cd20c0e8 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -15,7 +15,18 @@ Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS 2. For Click/LongPress: determine the target screen position (use `uloop screenshot` to find coordinates) 3. Execute the appropriate `uloop simulate-mouse-input` command 4. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering` -5. Report what happened +5. If a screenshot cannot prove the gameplay state changed, use the Debug.Break verification workflow below +6. Report what happened + +## Debug.Break Verification Workflow + +Use this when mouse input appears to fire but the resulting gameplay state is hard to confirm visually, such as a click that should place or destroy an object, a mouse delta that should rotate a camera, or a scroll action that should change a selected slot. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the mouse input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. +3. Trigger the mouse input before the delay expires, for example `uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index 54e34d6939..15c7a6390f 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -16,7 +16,18 @@ Simulate mouse interaction on Unity PlayMode UI: $ARGUMENTS 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 appropriate `uloop simulate-mouse-ui` command 5. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering --annotate-elements` -6. Report what happened +6. If a screenshot cannot prove the UI state changed, use the Debug.Break verification workflow below +7. Report what happened + +## Debug.Break Verification Workflow + +Use this when UI pointer events appear to fire but the resulting state is hard to confirm visually, such as a button callback that should update game state, a long-press that should open a menu, or a drag/drop action that should mutate inventory data. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the UI input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. +3. Trigger the UI mouse action before the delay expires, for example `uloop simulate-mouse-ui --action Click --x 400 --y 300`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index debad2993c..aaf833f3ad 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -14,7 +14,18 @@ Simulate keyboard input on Unity PlayMode: $ARGUMENTS 1. Ensure Unity is in PlayMode (use `uloop control-play-mode --action Play` if not) 2. Execute the appropriate `uloop simulate-keyboard` command 3. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering` -4. Report what happened +4. If a screenshot cannot prove the gameplay state changed, use the Debug.Break verification workflow below +5. Report what happened + +## Debug.Break Verification Workflow + +Use this when input appears to fire but the result is hard to verify visually, such as pressing Space to jump and needing to confirm the player actually entered the jump state. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. +3. Trigger the keyboard input before the delay expires, for example `uloop simulate-keyboard --action Press --key Space`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md index da66d0b1fd..d7cd20c0e8 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -15,7 +15,18 @@ Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS 2. For Click/LongPress: determine the target screen position (use `uloop screenshot` to find coordinates) 3. Execute the appropriate `uloop simulate-mouse-input` command 4. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering` -5. Report what happened +5. If a screenshot cannot prove the gameplay state changed, use the Debug.Break verification workflow below +6. Report what happened + +## Debug.Break Verification Workflow + +Use this when mouse input appears to fire but the resulting gameplay state is hard to confirm visually, such as a click that should place or destroy an object, a mouse delta that should rotate a camera, or a scroll action that should change a selected slot. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the mouse input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. +3. Trigger the mouse input before the delay expires, for example `uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md index 54e34d6939..15c7a6390f 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -16,7 +16,18 @@ Simulate mouse interaction on Unity PlayMode UI: $ARGUMENTS 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 appropriate `uloop simulate-mouse-ui` command 5. Take a screenshot to verify the result: `uloop screenshot --capture-mode rendering --annotate-elements` -6. Report what happened +6. If a screenshot cannot prove the UI state changed, use the Debug.Break verification workflow below +7. Report what happened + +## Debug.Break Verification Workflow + +Use this when UI pointer events appear to fire but the resulting state is hard to confirm visually, such as a button callback that should update game state, a long-press that should open a menu, or a drag/drop action that should mutate inventory data. + +1. Start `uloop wait-for-debug-break` in another terminal/session. +2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the UI input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. +3. Trigger the UI mouse action before the delay expires, for example `uloop simulate-mouse-ui --action Click --x 400 --y 300`. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference From 421e62e76ec299a1459a0e45964eab168a64d41a Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:36:36 +0900 Subject: [PATCH 6/9] Copy local uloop binaries instead of linking Make local CLI activation overwrite the global uloop executable with the current checkout binary so stale backups or symlink targets do not block development workflows. --- scripts/check-build-link-go-cli.sh | 42 ++++++---------------- scripts/test-use-local-uloop.sh | 39 ++++++++++---------- scripts/use-local-uloop.sh | 58 ++++++------------------------ 3 files changed, 41 insertions(+), 98 deletions(-) diff --git a/scripts/check-build-link-go-cli.sh b/scripts/check-build-link-go-cli.sh index 450456bb9f..1b2bdf5096 100755 --- a/scripts/check-build-link-go-cli.sh +++ b/scripts/check-build-link-go-cli.sh @@ -25,31 +25,16 @@ normalize_path_dir() { printf '%s\n' "$path_dir" } -ensure_symlink_target() { - link_path="$1" +copy_uloop_binary() { + destination_path="$1" - if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then - echo "Go CLI source checks passed and dist binaries were rebuilt." - echo "Refusing to overwrite non-symlink global uloop: $link_path" >&2 - exit 1 - fi + rm -f "$destination_path" + cp "$cli_path" "$destination_path" + chmod +x "$destination_path" + echo "Copied rebuilt native CLI to global uloop: $destination_path" } -update_uloop_link() { - link_path="$1" - - if [ -L "$link_path" ]; then - current_target=$(readlink "$link_path") - echo "Updating global uloop symlink: $link_path -> $current_target" - else - echo "Creating global uloop symlink: $link_path" - fi - - ln -sfn "$cli_path" "$link_path" - echo "Global uloop now points at the rebuilt native CLI: $link_path -> $(readlink "$link_path")" -} - -ensure_global_uloop_resolves_to_link() { +ensure_global_uloop_resolves_to_updated_binary() { resolved_uloop_path=$(command -v "$global_command_name" || true) if [ "$resolved_uloop_path" = "$global_uloop_path" ]; then @@ -59,7 +44,7 @@ ensure_global_uloop_resolves_to_link() { return 0 fi - echo "Global $global_command_name symlink was updated, but shell resolution does not point at it." >&2 + echo "Global $global_command_name was updated, but shell resolution does not point at it." >&2 echo "Resolved $global_command_name: ${resolved_uloop_path:-not found}" >&2 echo "Expected $global_command_name: $global_uloop_path" >&2 echo "Add $global_bin_dir to PATH or set ULOOP_GLOBAL_BIN_DIR to a directory earlier in PATH." >&2 @@ -136,16 +121,11 @@ fi echo "Go CLI source checks passed and dist binaries were rebuilt." -ensure_symlink_target "$global_uloop_path" -if [ -n "$extra_global_uloop_path" ]; then - ensure_symlink_target "$extra_global_uloop_path" -fi - -update_uloop_link "$global_uloop_path" +copy_uloop_binary "$global_uloop_path" if [ -n "$extra_global_uloop_path" ]; then - update_uloop_link "$extra_global_uloop_path" + copy_uloop_binary "$extra_global_uloop_path" fi -ensure_global_uloop_resolves_to_link +ensure_global_uloop_resolves_to_updated_binary "$global_command_name" --version diff --git a/scripts/test-use-local-uloop.sh b/scripts/test-use-local-uloop.sh index 15334c9640..388270cb1d 100755 --- a/scripts/test-use-local-uloop.sh +++ b/scripts/test-use-local-uloop.sh @@ -26,8 +26,8 @@ chmod +x "$BIN_DIR/uloop" ULOOP_GLOBAL_BIN_DIR="$BIN_DIR" "$ROOT_DIR/scripts/use-local-uloop.sh" link >/dev/null -if [ ! -L "$BIN_DIR/uloop" ]; then - echo "Expected global uloop to be a symlink after link." >&2 +if [ -L "$BIN_DIR/uloop" ]; then + echo "Expected global uloop to be a copied executable, not a symlink." >&2 exit 1 fi @@ -36,32 +36,28 @@ if [ "$(uname -m)" = "x86_64" ] || [ "$(uname -m)" = "amd64" ]; then expected_target="$ROOT_DIR/cli/dist/darwin-amd64/uloop" fi -actual_target=$(readlink "$BIN_DIR/uloop") -if [ "$actual_target" != "$expected_target" ]; then - echo "Unexpected symlink target: $actual_target" >&2 - echo "Expected: $expected_target" >&2 +if ! cmp -s "$BIN_DIR/uloop" "$expected_target"; then + echo "Expected global uloop to match the local development binary." >&2 exit 1 fi -if [ ! -x "$BIN_DIR/uloop.before-local-link" ]; then - echo "Expected original global uloop backup to exist." >&2 +if [ -e "$BIN_DIR/uloop.before-local-link" ]; then + echo "Expected link to avoid creating a backup." >&2 exit 1 fi -ULOOP_GLOBAL_BIN_DIR="$BIN_DIR" "$ROOT_DIR/scripts/use-local-uloop.sh" restore >/dev/null +printf '%s\n' '#!/bin/sh' 'echo stale-backup' > "$BIN_DIR/uloop.before-local-link" +chmod +x "$BIN_DIR/uloop.before-local-link" -if [ -L "$BIN_DIR/uloop" ]; then - echo "Expected restored global uloop to no longer be a symlink." >&2 - exit 1 -fi +ULOOP_GLOBAL_BIN_DIR="$BIN_DIR" "$ROOT_DIR/scripts/use-local-uloop.sh" link >/dev/null -if [ "$("$BIN_DIR/uloop")" != "old-global-uloop" ]; then - echo "Restored global uloop did not execute the original script." >&2 +if ! cmp -s "$BIN_DIR/uloop" "$expected_target"; then + echo "Expected link to overwrite the global uloop even when a stale backup exists." >&2 exit 1 fi -if [ -e "$BIN_DIR/uloop.before-local-link" ]; then - echo "Expected backup to be consumed after restore." >&2 +if [ "$("$BIN_DIR/uloop.before-local-link")" != "stale-backup" ]; then + echo "Expected stale backup to be left untouched." >&2 exit 1 fi @@ -71,9 +67,14 @@ mkdir -p "$TRAILING_BIN" HOME="$TRAILING_HOME" PATH="$TRAILING_BIN/:/usr/bin:/bin:/usr/sbin:/sbin" "$ROOT_DIR/scripts/use-local-uloop.sh" link >/dev/null -if [ ! -L "$TRAILING_BIN/uloop" ]; then +if [ -L "$TRAILING_BIN/uloop" ] || [ ! -x "$TRAILING_BIN/uloop" ]; then echo "Expected PATH entry with trailing slash to be selected for global uloop." >&2 exit 1 fi -echo "use-local-uloop link/restore test passed" +if ! cmp -s "$TRAILING_BIN/uloop" "$expected_target"; then + echo "Expected trailing PATH global uloop to match the local development binary." >&2 + exit 1 +fi + +echo "use-local-uloop copy-link test passed" diff --git a/scripts/use-local-uloop.sh b/scripts/use-local-uloop.sh index bcbbffccd1..4a1430115d 100755 --- a/scripts/use-local-uloop.sh +++ b/scripts/use-local-uloop.sh @@ -83,19 +83,13 @@ select_global_bin_dir() { print_status() { echo "Global uloop path: $global_uloop" if [ -L "$global_uloop" ]; then - echo "Global uloop symlink target: $(readlink "$global_uloop")" + echo "Global uloop is a symlink to: $(readlink "$global_uloop")" elif [ -e "$global_uloop" ]; then - echo "Global uloop is a regular file or directory." + echo "Global uloop is a copied executable." else echo "Global uloop does not exist." fi - if [ -e "$backup_uloop" ] || [ -L "$backup_uloop" ]; then - echo "Backup path: $backup_uloop" - else - echo "Backup path: none" - fi - echo "Local branch uloop: $local_uloop" if [ -x "$global_uloop" ]; then echo "Selected global uloop version:" @@ -110,52 +104,21 @@ print_status() { link_local_uloop() { mkdir -p "$global_bin_dir" - if [ -L "$global_uloop" ] && [ "$(readlink "$global_uloop")" = "$local_uloop" ]; then - echo "Global uloop already points to this checkout." - print_status - return - fi - - if [ -e "$global_uloop" ] || [ -L "$global_uloop" ]; then - if [ -e "$backup_uloop" ] || [ -L "$backup_uloop" ]; then - echo "Backup already exists, refusing to overwrite it: $backup_uloop" >&2 - echo "Run '$0 restore' first or move the backup manually." >&2 - exit 1 - fi - - mv "$global_uloop" "$backup_uloop" - echo "Backed up existing global uloop to $backup_uloop" - fi - - ln -s "$local_uloop" "$global_uloop" + rm -f "$global_uloop" + cp "$local_uloop" "$global_uloop" + chmod +x "$global_uloop" + echo "Copied local uloop to $global_uloop" print_status } restore_global_uloop() { - if [ ! -e "$backup_uloop" ] && [ ! -L "$backup_uloop" ]; then - echo "No backup found: $backup_uloop" >&2 - exit 1 - fi - - if [ -L "$global_uloop" ]; then - current_target=$(readlink "$global_uloop") - if [ "$current_target" != "$local_uloop" ]; then - echo "Refusing to remove unexpected uloop symlink: $global_uloop -> $current_target" >&2 - exit 1 - fi - - rm "$global_uloop" - elif [ -e "$global_uloop" ]; then - echo "Refusing to overwrite unexpected global uloop: $global_uloop" >&2 - exit 1 - fi - - mv "$backup_uloop" "$global_uloop" - print_status + echo "Restore is no longer supported by this script." >&2 + echo "Run '$0 link' to overwrite the global uloop with this checkout's binary." >&2 + exit 1 } usage() { - echo "Usage: $0 link|restore|status" >&2 + echo "Usage: $0 link|status" >&2 exit 1 } @@ -163,7 +126,6 @@ select_local_uloop select_global_bin_dir global_uloop="$global_bin_dir/$command_name" -backup_uloop="$global_bin_dir/$command_name.before-local-link" case "$ACTION" in link) From 1482846e883520f597be22ebe3f68a2d46454a39 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:44:47 +0900 Subject: [PATCH 7/9] Improve debug break wait failures Fast-fail wait-for-debug-break when Unity is not in PlayMode and preserve the last polling error so users see the underlying probe failure instead of only a generic cancellation. --- cli/internal/cli/error_envelope.go | 1 + cli/internal/cli/wait_for_debug_break.go | 30 ++++++++++++ cli/internal/cli/wait_for_debug_break_test.go | 47 +++++++++++++++++++ 3 files changed, 78 insertions(+) diff --git a/cli/internal/cli/error_envelope.go b/cli/internal/cli/error_envelope.go index 72fd3a986e..8cafaf1a8b 100644 --- a/cli/internal/cli/error_envelope.go +++ b/cli/internal/cli/error_envelope.go @@ -24,6 +24,7 @@ const ( errorCodeCompileWaitTimeout = "COMPILE_WAIT_TIMEOUT" errorCodeControlPlayModeWaitTimeout = "CONTROL_PLAY_MODE_WAIT_TIMEOUT" errorCodeDebugBreakAlreadyPaused = "DEBUG_BREAK_ALREADY_PAUSED" + errorCodeDebugBreakNotPlaying = "DEBUG_BREAK_NOT_PLAYING" errorCodeInternalError = "INTERNAL_ERROR" errorPhaseArgumentParsing = "argument_parsing" diff --git a/cli/internal/cli/wait_for_debug_break.go b/cli/internal/cli/wait_for_debug_break.go index 4439aa8d5b..aa7db2a00b 100644 --- a/cli/internal/cli/wait_for_debug_break.go +++ b/cli/internal/cli/wait_for_debug_break.go @@ -67,6 +67,11 @@ func runWaitForDebugBreak( writeErrorEnvelope(stderr, debugBreakAlreadyPausedError(connection.ProjectRoot, initialState)) return 1 } + if !initialState.IsPlaying { + spinner.Stop() + writeErrorEnvelope(stderr, debugBreakNotPlayingError(connection.ProjectRoot, initialState)) + return 1 + } spinner.Update("Waiting for Debug.Break...") finalState, waitErr := waitForDebugBreak(ctx, connection, initialState) @@ -117,6 +122,7 @@ func waitForDebugBreak( initialState playModeStateResponse, ) (playModeStateResponse, error) { lastState := initialState + var lastErr error for { state, err := queryPlayModeState(ctx, connection) if err == nil { @@ -124,10 +130,15 @@ func waitForDebugBreak( if state.IsPaused { return state, nil } + } else { + lastErr = err } select { case <-ctx.Done(): + if lastErr != nil { + return lastState, lastErr + } return lastState, ctx.Err() case <-time.After(waitForDebugBreakPollInterval): } @@ -172,3 +183,22 @@ func debugBreakAlreadyPausedError(projectRoot string, state playModeStateRespons }, } } + +func debugBreakNotPlayingError(projectRoot string, state playModeStateResponse) cliError { + return cliError{ + ErrorCode: errorCodeDebugBreakNotPlaying, + Phase: errorPhaseResponseWaiting, + Message: "Unity Editor is not in PlayMode before waiting for Debug.Break.", + Retryable: true, + SafeToRetry: true, + ProjectRoot: projectRoot, + Command: waitForDebugBreakCommandName, + NextActions: []string{ + "Run `uloop control-play-mode --action Play`, then run `uloop wait-for-debug-break` before triggering the target action.", + }, + Details: map[string]any{ + "isPlaying": state.IsPlaying, + "isPaused": state.IsPaused, + }, + } +} diff --git a/cli/internal/cli/wait_for_debug_break_test.go b/cli/internal/cli/wait_for_debug_break_test.go index 2683b482ab..ad26e160df 100644 --- a/cli/internal/cli/wait_for_debug_break_test.go +++ b/cli/internal/cli/wait_for_debug_break_test.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net" "strings" @@ -76,6 +77,29 @@ func TestRunWaitForDebugBreakFailsWhenUnityAlreadyPaused(t *testing.T) { } } +// Verifies wait-for-debug-break rejects EditMode before entering the wait loop. +func TestRunWaitForDebugBreakFailsWhenUnityIsNotPlaying(t *testing.T) { + replaceQueryPlayModeState(t, func(context.Context, unityipc.Connection) (playModeStateResponse, error) { + return playModeStateResponse{IsPlaying: false, IsPaused: false}, nil + }) + + var stdout bytes.Buffer + var stderr bytes.Buffer + code := runWaitForDebugBreak( + context.Background(), + unityipc.Connection{ProjectRoot: t.TempDir()}, + []string{}, + &stdout, + &stderr) + + if code != 1 { + t.Fatalf("expected not-playing failure, got %d with stdout %s", code, stdout.String()) + } + if !strings.Contains(stderr.String(), errorCodeDebugBreakNotPlaying) { + t.Fatalf("not-playing error missing from stderr: %s", stderr.String()) + } +} + // Verifies Debug.Break polling keeps the last state when the caller cancels the wait. func TestWaitForDebugBreakReturnsLastStateWhenCanceled(t *testing.T) { originalPollInterval := waitForDebugBreakPollInterval @@ -101,6 +125,29 @@ func TestWaitForDebugBreakReturnsLastStateWhenCanceled(t *testing.T) { } } +// Verifies Debug.Break polling reports the last probe error instead of hiding it behind timeout. +func TestWaitForDebugBreakReturnsLastProbeErrorWhenCanceled(t *testing.T) { + originalPollInterval := waitForDebugBreakPollInterval + waitForDebugBreakPollInterval = time.Millisecond + t.Cleanup(func() { + waitForDebugBreakPollInterval = originalPollInterval + }) + expectedErr := errors.New("play mode state probe failed") + replaceQueryPlayModeState(t, func(context.Context, unityipc.Connection) (playModeStateResponse, error) { + return playModeStateResponse{}, expectedErr + }) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + state, err := waitForDebugBreak( + ctx, + unityipc.Connection{ProjectRoot: t.TempDir()}, + playModeStateResponse{IsPlaying: true, IsPaused: false}) + if !errors.Is(err, expectedErr) { + t.Fatalf("expected last probe error, got %v with state %#v", err, state) + } +} + // Verifies the Unity probe uses the internal get-play-mode-state bridge command. func TestQueryPlayModeStateFromUnityUsesInternalBridgeCommand(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") From 0973835ba30bf3a62498ef8141f31570aaaecf77 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 22:47:24 +0900 Subject: [PATCH 8/9] Keep input verification docs focused Avoid directing input simulation skills to execute dynamic code directly while preserving the Debug.Break verification workflow through temporary project markers. --- .agents/skills/uloop-simulate-keyboard/SKILL.md | 4 ++-- .agents/skills/uloop-simulate-mouse-input/SKILL.md | 4 ++-- .agents/skills/uloop-simulate-mouse-ui/SKILL.md | 4 ++-- .claude/skills/uloop-simulate-keyboard/SKILL.md | 4 ++-- .claude/skills/uloop-simulate-mouse-input/SKILL.md | 4 ++-- .claude/skills/uloop-simulate-mouse-ui/SKILL.md | 4 ++-- .../Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md | 4 ++-- .../Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md | 4 ++-- .../src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md | 4 ++-- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.agents/skills/uloop-simulate-keyboard/SKILL.md b/.agents/skills/uloop-simulate-keyboard/SKILL.md index aaf833f3ad..c38685e45f 100644 --- a/.agents/skills/uloop-simulate-keyboard/SKILL.md +++ b/.agents/skills/uloop-simulate-keyboard/SKILL.md @@ -22,9 +22,9 @@ Simulate keyboard input on Unity PlayMode: $ARGUMENTS Use this when input appears to fire but the result is hard to verify visually, such as pressing Space to jump and needing to confirm the player actually entered the jump state. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. 3. Trigger the keyboard input before the delay expires, for example `uloop simulate-keyboard --action Press --key Space`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index d7cd20c0e8..d2a5309997 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -23,9 +23,9 @@ Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS Use this when mouse input appears to fire but the resulting gameplay state is hard to confirm visually, such as a click that should place or destroy an object, a mouse delta that should rotate a camera, or a scroll action that should change a selected slot. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the mouse input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. 3. Trigger the mouse input before the delay expires, for example `uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md index 15c7a6390f..364edbb6ea 100644 --- a/.agents/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-ui/SKILL.md @@ -24,9 +24,9 @@ Simulate mouse interaction on Unity PlayMode UI: $ARGUMENTS Use this when UI pointer events appear to fire but the resulting state is hard to confirm visually, such as a button callback that should update game state, a long-press that should open a menu, or a drag/drop action that should mutate inventory data. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the UI input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. 3. Trigger the UI mouse action before the delay expires, for example `uloop simulate-mouse-ui --action Click --x 400 --y 300`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.claude/skills/uloop-simulate-keyboard/SKILL.md b/.claude/skills/uloop-simulate-keyboard/SKILL.md index aaf833f3ad..c38685e45f 100644 --- a/.claude/skills/uloop-simulate-keyboard/SKILL.md +++ b/.claude/skills/uloop-simulate-keyboard/SKILL.md @@ -22,9 +22,9 @@ Simulate keyboard input on Unity PlayMode: $ARGUMENTS Use this when input appears to fire but the result is hard to verify visually, such as pressing Space to jump and needing to confirm the player actually entered the jump state. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. 3. Trigger the keyboard input before the delay expires, for example `uloop simulate-keyboard --action Press --key Space`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index d7cd20c0e8..d2a5309997 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -23,9 +23,9 @@ Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS Use this when mouse input appears to fire but the resulting gameplay state is hard to confirm visually, such as a click that should place or destroy an object, a mouse delta that should rotate a camera, or a scroll action that should change a selected slot. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the mouse input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. 3. Trigger the mouse input before the delay expires, for example `uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md index 15c7a6390f..364edbb6ea 100644 --- a/.claude/skills/uloop-simulate-mouse-ui/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-ui/SKILL.md @@ -24,9 +24,9 @@ Simulate mouse interaction on Unity PlayMode UI: $ARGUMENTS Use this when UI pointer events appear to fire but the resulting state is hard to confirm visually, such as a button callback that should update game state, a long-press that should open a menu, or a drag/drop action that should mutate inventory data. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the UI input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. 3. Trigger the UI mouse action before the delay expires, for example `uloop simulate-mouse-ui --action Click --x 400 --y 300`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md index aaf833f3ad..c38685e45f 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/SKILL.md @@ -22,9 +22,9 @@ Simulate keyboard input on Unity PlayMode: $ARGUMENTS Use this when input appears to fire but the result is hard to verify visually, such as pressing Space to jump and needing to confirm the player actually entered the jump state. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as position, velocity, grounded flags, or animation state. 3. Trigger the keyboard input before the delay expires, for example `uloop simulate-keyboard --action Press --key Space`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md index d7cd20c0e8..d2a5309997 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -23,9 +23,9 @@ Simulate mouse input via Input System in Unity PlayMode: $ARGUMENTS Use this when mouse input appears to fire but the resulting gameplay state is hard to confirm visually, such as a click that should place or destroy an object, a mouse delta that should rotate a camera, or a scroll action that should change a selected slot. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the mouse input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as target object existence, camera rotation, selected slot, hit result, or component fields. 3. Trigger the mouse input before the delay expires, for example `uloop simulate-mouse-input --action Click --x 400 --y 300 --button Right`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md index 15c7a6390f..364edbb6ea 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseUi/Skill/SKILL.md @@ -24,9 +24,9 @@ Simulate mouse interaction on Unity PlayMode UI: $ARGUMENTS Use this when UI pointer events appear to fire but the resulting state is hard to confirm visually, such as a button callback that should update game state, a long-press that should open a menu, or a drag/drop action that should mutate inventory data. 1. Start `uloop wait-for-debug-break` in another terminal/session. -2. Schedule a delayed `Debug.Break()` with `uloop execute-dynamic-code` before triggering the UI input. Use the scheduled Debug.Break example in `uloop-execute-dynamic-code` PlayMode inspection references, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. +2. Add a temporary delayed `Debug.Break()` marker in the project code path under test, and log the state you need to inspect, such as selected item, active panel, inventory contents, button callback side effects, or component fields. 3. Trigger the UI mouse action before the delay expires, for example `uloop simulate-mouse-ui --action Click --x 400 --y 300`. -4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another `uloop execute-dynamic-code` call. +4. When `wait-for-debug-break` returns, inspect the frozen state with `uloop get-logs`, `uloop get-hierarchy`, or another focused uLoop inspection command. 5. Resume PlayMode with `uloop control-play-mode --action Play` after inspection. ## Tool Reference From 5fed63bca3737b949ac56498c53eb6c7bcc465e6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 2 Jun 2026 23:18:07 +0900 Subject: [PATCH 9/9] Make local uloop replacement atomic Copy local CLI binaries to temporary files before replacing the global uloop executable so a failed copy does not remove the working command. --- scripts/check-build-link-go-cli.sh | 8 +++++--- scripts/use-local-uloop.sh | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/scripts/check-build-link-go-cli.sh b/scripts/check-build-link-go-cli.sh index 1b2bdf5096..fd810d34d5 100755 --- a/scripts/check-build-link-go-cli.sh +++ b/scripts/check-build-link-go-cli.sh @@ -27,10 +27,12 @@ normalize_path_dir() { copy_uloop_binary() { destination_path="$1" + tmp_destination_path="$destination_path.tmp.$$" - rm -f "$destination_path" - cp "$cli_path" "$destination_path" - chmod +x "$destination_path" + rm -f "$tmp_destination_path" + cp "$cli_path" "$tmp_destination_path" + chmod +x "$tmp_destination_path" + mv -f "$tmp_destination_path" "$destination_path" echo "Copied rebuilt native CLI to global uloop: $destination_path" } diff --git a/scripts/use-local-uloop.sh b/scripts/use-local-uloop.sh index 4a1430115d..ad06f03aa8 100755 --- a/scripts/use-local-uloop.sh +++ b/scripts/use-local-uloop.sh @@ -104,9 +104,11 @@ print_status() { link_local_uloop() { mkdir -p "$global_bin_dir" - rm -f "$global_uloop" - cp "$local_uloop" "$global_uloop" - chmod +x "$global_uloop" + tmp_global_uloop="$global_uloop.tmp.$$" + rm -f "$tmp_global_uloop" + cp "$local_uloop" "$tmp_global_uloop" + chmod +x "$tmp_global_uloop" + mv -f "$tmp_global_uloop" "$global_uloop" echo "Copied local uloop to $global_uloop" print_status }