@aefree/pi-unity 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +3 -3
- package/index.ts +6 -3
- package/package.json +1 -1
- package/skills/unity-pipeline-workflows/SKILL.md +2 -2
- package/src/unity-cli.ts +3 -0
- package/src/unity-core.ts +7 -2
- package/src/unity-launch.ts +7 -2
- package/src/unity-pipeline.ts +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project follows semantic versioning for public package releases.
|
|
7
7
|
|
|
8
|
+
## [0.10.0] - 2026-08-14
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Added a configurable 1–86,400-second timeout to `unity_pipeline_eval`, forwarding it to both Unity CLI and the host process.
|
|
13
|
+
- Handle Pipeline 0.5's explicitly rejected initial-settling `Server Busy` envelopes with bounded retries for connected recompiles and tests, while preserving no-retry handling for ambiguous responses.
|
|
14
|
+
|
|
15
|
+
## [0.9.3] - 2026-08-10
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Added an optional `automated` parameter to `unity_open_editor`, forwarding Unity Editor's `-automated` flag through both Unity CLI and direct Editor launch paths.
|
|
20
|
+
|
|
8
21
|
## [0.9.2] - 2026-08-08
|
|
9
22
|
|
|
10
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -34,16 +34,16 @@ Use these tools with an already-open exact Unity project copy that has a reachab
|
|
|
34
34
|
- `unity_project_status` — inspect lockfiles, matching Unity processes, Pipeline reachability, package version, and advertised commands without launching Unity.
|
|
35
35
|
- `unity_pipeline_recompile` — recompile through Pipeline with exact-copy preflight, bounded polling, and compact compiler evidence.
|
|
36
36
|
- `unity_pipeline_run_tests` — run one focused EditMode or PlayMode selection with bounded polling and aggregate results.
|
|
37
|
-
- `unity_pipeline_eval` — execute bounded project-specific C# through Pipeline's Roslyn REPL.
|
|
37
|
+
- `unity_pipeline_eval` — execute bounded project-specific C# through Pipeline's Roslyn REPL. It accepts `timeoutSeconds` from 1–86,400 seconds; a timeout is uncertain and does not cancel or retry Editor work.
|
|
38
38
|
- `unity_pipeline_inspect` — dispatch supported package-owned inspection commands and return structured evidence.
|
|
39
39
|
|
|
40
40
|
Connected recompilation follows Unity's Script Changes While Playing policy and never preemptively sends `editor_stop`. Connected tests may exit Play Mode through advertised `editor_stop` when necessary, then verify Edit Mode before dispatch. Play Mode exit is allowed by default; `/unity-playmode-exit allow|disallow|status` controls the current session.
|
|
41
41
|
|
|
42
|
-
A timeout is uncertain: work may still be running. The tools do not silently cancel, retry, launch another Editor, or switch to batchmode.
|
|
42
|
+
A timeout is uncertain: work may still be running. The tools do not silently cancel, retry, launch another Editor, or switch to batchmode. The sole exception is Pipeline 0.5's explicit initial-settling `Server Busy` rejection for `unity_pipeline_recompile` and `unity_pipeline_run_tests`, which is known not to have dispatched a main-thread command and is retried only within the configured deadline.
|
|
43
43
|
|
|
44
44
|
### Editor and batchmode
|
|
45
45
|
|
|
46
|
-
- `unity_open_editor` — open the Unity Editor GUI.
|
|
46
|
+
- `unity_open_editor` — open the Unity Editor GUI. Pass `automated: true` to add the Unity Editor `-automated` flag; this is distinct from the Unity CLI's own `--non-interactive` option.
|
|
47
47
|
- `unity_launch_batchmode` — run a bounded batchmode command through Unity CLI or the direct Editor executable.
|
|
48
48
|
- `unity_run_test_batch` — run one isolated or report-producing Unity Test Framework platform with generated XML and log paths.
|
|
49
49
|
- `unity_inspect_artifacts` — summarize existing Unity Test Framework XML and Unity logs without launching Unity.
|
package/index.ts
CHANGED
|
@@ -103,6 +103,7 @@ type UnityLauncherPreference = "auto" | "unity-cli" | "editor-executable";
|
|
|
103
103
|
const OPEN_EDITOR_PARAMS = Type.Object({
|
|
104
104
|
path: Type.Optional(Type.String({ description: "Unity project path, workspace copy root, or folder containing project copies." })),
|
|
105
105
|
unityEditorPath: Type.Optional(Type.String({ description: "Optional explicit Unity executable path override." })),
|
|
106
|
+
automated: Type.Optional(Type.Boolean({ default: false, description: "Pass Unity Editor's -automated flag when opening the project. Defaults to false." })),
|
|
106
107
|
launcher: LAUNCHER_SCHEMA,
|
|
107
108
|
});
|
|
108
109
|
|
|
@@ -147,6 +148,7 @@ const PIPELINE_TEST_PARAMS = Type.Object({
|
|
|
147
148
|
const PIPELINE_EVAL_PARAMS = Type.Object({
|
|
148
149
|
path: Type.Optional(Type.String({ maxLength: 1000, description: "Unity project path, workspace copy root, or folder containing project copies." })),
|
|
149
150
|
code: Type.String({ minLength: 1, maxLength: 4000, description: "Bounded C# source for advertised Pipeline eval. Roslyn compiles it on the connected Editor main thread; include an explicit return value when evidence is needed." }),
|
|
151
|
+
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 86400, default: 12, description: "Connected eval deadline in seconds (maximum 24 hours). A timeout is uncertain and does not retry or cancel Unity work." })),
|
|
150
152
|
}, { additionalProperties: false });
|
|
151
153
|
|
|
152
154
|
const PIPELINE_INSPECTION_PARAMS = Type.Object({
|
|
@@ -1469,7 +1471,7 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1469
1471
|
}, {
|
|
1470
1472
|
execute: createPlanningUnityCliExecutor(pi),
|
|
1471
1473
|
signal,
|
|
1472
|
-
timeout:
|
|
1474
|
+
timeout: (params.timeoutSeconds ?? 12) * 1000,
|
|
1473
1475
|
});
|
|
1474
1476
|
throwIfAborted(signal);
|
|
1475
1477
|
const text = result.outcome === "dispatched"
|
|
@@ -1578,7 +1580,7 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1578
1580
|
pi.registerTool({
|
|
1579
1581
|
name: "unity_open_editor",
|
|
1580
1582
|
label: "Unity Open Editor",
|
|
1581
|
-
description: "Open the Unity Editor GUI for a Unity project copy.",
|
|
1583
|
+
description: "Open the Unity Editor GUI for a Unity project copy, optionally passing Unity Editor's -automated flag.",
|
|
1582
1584
|
promptSnippet: "Open the Unity Editor GUI for a resolved Unity project when the user explicitly asks for the editor to open.",
|
|
1583
1585
|
promptGuidelines: [
|
|
1584
1586
|
"Use this tool only when the user explicitly wants the Unity Editor GUI opened.",
|
|
@@ -1605,11 +1607,12 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1605
1607
|
launch = launchUnityCliOpenDetached(candidate.projectRoot, {
|
|
1606
1608
|
editorVersion: candidate.unityVersion,
|
|
1607
1609
|
editorPath: params.unityEditorPath,
|
|
1610
|
+
automated: params.automated,
|
|
1608
1611
|
});
|
|
1609
1612
|
} else {
|
|
1610
1613
|
await assertUnityProjectNotBusy(candidate.projectRoot);
|
|
1611
1614
|
editorPath = await resolveUnityEditorPath(candidate.unityVersion, { overridePath: params.unityEditorPath });
|
|
1612
|
-
launch = launchUnityEditorDetached(editorPath, candidate.projectRoot);
|
|
1615
|
+
launch = launchUnityEditorDetached(editorPath, candidate.projectRoot, { automated: params.automated });
|
|
1613
1616
|
}
|
|
1614
1617
|
const text = buildEditorLaunchSummary(ctx.cwd, candidate, editorPath, discoveryWarning, launcher);
|
|
1615
1618
|
|
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ Use one typed tool call for each supported connected operation:
|
|
|
16
16
|
|
|
17
17
|
These tools resolve the exact copy, require advertised commands, inspect lifecycle state, dispatch once, validate identity, and poll internally with a fixed deadline. Do not recreate their wait loops with `bash`, `unity recompile_status`, or `unity test_status` calls.
|
|
18
18
|
|
|
19
|
-
A timeout or malformed response is uncertain: the Unity operation may still be running. Do not cancel, retry, launch batchmode, close the Editor, or claim a result without a new user-authorized decision.
|
|
19
|
+
A timeout or malformed response is uncertain: the Unity operation may still be running. Do not cancel, retry, launch batchmode, close the Editor, or claim a result without a new user-authorized decision. The only automatic retry is Pipeline 0.5's explicit initial-settling `Server Busy` response for `unity_pipeline_recompile` or `unity_pipeline_run_tests`, which confirms that a main-thread command was rejected before dispatch.
|
|
20
20
|
|
|
21
21
|
## Preconditions and boundaries
|
|
22
22
|
|
|
@@ -49,4 +49,4 @@ Normally call the typed tools, not raw CLI commands. If a typed tool is unavaila
|
|
|
49
49
|
|
|
50
50
|
Use `unity_run_test_batch` for a closed project, intentional isolation/CI, category or multiple filters, or required NUnit XML/log evidence. State the reason for that route. Do not use batchmode as an automatic fallback after an uncertain connected dispatch.
|
|
51
51
|
|
|
52
|
-
Use the typed compile/test tools when their polling and terminal evidence fit the task. Advertised Pipeline `eval` remains available through `unity_pipeline_eval` for bounded project-specific inspection or operations outside those typed workflows;
|
|
52
|
+
Use the typed compile/test tools when their polling and terminal evidence fit the task. Advertised Pipeline `eval` remains available through `unity_pipeline_eval` for bounded project-specific inspection or operations outside those typed workflows; its `timeoutSeconds` range is 1–86,400 seconds, and a timeout remains uncertain without cancellation or retry. It is an assistance surface, not a forbidden fallback or a substitute for the typed tools' completion protocol. Eval compiles arbitrary C# with Roslyn on the Editor main thread, so ordinary properties and local-variable snippets are valid; it is not expression-only or reliably statically read-only. Prefer typed tools for their stronger evidence, but let user intent and project guidance govern mutations. Lifecycle, persistent-setting, destructive, asset, scene-save, package, build, and test mutations require explicit authorization.
|
package/src/unity-cli.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type UnityCliLaunchOptions = {
|
|
|
18
18
|
timeoutSeconds?: number;
|
|
19
19
|
cliCommand?: string;
|
|
20
20
|
useGraphics?: boolean;
|
|
21
|
+
/** Forward Unity Editor's -automated flag through `unity open --args`. */
|
|
22
|
+
automated?: boolean;
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export type UnityCliPipelineInstance = {
|
|
@@ -80,6 +82,7 @@ function appendUnityCliEditorOptions(args: string[], options: UnityCliLaunchOpti
|
|
|
80
82
|
export function createUnityCliOpenCommand(projectRoot: string, options: UnityCliLaunchOptions = {}): UnityCliCommand {
|
|
81
83
|
const args = [...unityCliBaseArgs(), "open", projectRoot];
|
|
82
84
|
appendUnityCliEditorOptions(args, options);
|
|
85
|
+
if (options.automated) args.push("--args", "-automated");
|
|
83
86
|
return {
|
|
84
87
|
command: resolveUnityCliCommand(options),
|
|
85
88
|
args,
|
package/src/unity-core.ts
CHANGED
|
@@ -64,8 +64,13 @@ export function buildUnityEditorCandidates(
|
|
|
64
64
|
];
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export
|
|
68
|
-
|
|
67
|
+
export type UnityOpenEditorArgsOptions = {
|
|
68
|
+
/** Pass Unity Editor's -automated flag. */
|
|
69
|
+
automated?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function buildUnityOpenEditorArgs(projectRoot: string, options: UnityOpenEditorArgsOptions = {}): string[] {
|
|
73
|
+
return ["-projectPath", projectRoot, ...(options.automated ? ["-automated"] : [])];
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export type UnityBatchmodeArgsOptions = {
|
package/src/unity-launch.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
buildUnityOpenEditorArgs,
|
|
8
8
|
normalizeUnityEditorOverride,
|
|
9
9
|
type SupportedPlatform,
|
|
10
|
+
type UnityOpenEditorArgsOptions,
|
|
10
11
|
} from "./unity-core";
|
|
11
12
|
import { createUnityCliOpenCommand, type UnityCliLaunchOptions } from "./unity-cli";
|
|
12
13
|
|
|
@@ -47,8 +48,12 @@ export async function resolveUnityEditorPath(
|
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
export function launchUnityEditorDetached(
|
|
51
|
-
|
|
51
|
+
export function launchUnityEditorDetached(
|
|
52
|
+
editorPath: string,
|
|
53
|
+
projectRoot: string,
|
|
54
|
+
options: UnityOpenEditorArgsOptions = {},
|
|
55
|
+
): { pid: number | undefined; args: string[]; command: string } {
|
|
56
|
+
const args = buildUnityOpenEditorArgs(projectRoot, options);
|
|
52
57
|
const child = spawn(editorPath, args, {
|
|
53
58
|
detached: true,
|
|
54
59
|
stdio: "ignore",
|
package/src/unity-pipeline.ts
CHANGED
|
@@ -145,6 +145,17 @@ function diagnostics(result: RecordValue): string[] {
|
|
|
145
145
|
});
|
|
146
146
|
return [...new Set(values)].slice(0, UNITY_PIPELINE_MAX_DIAGNOSTICS);
|
|
147
147
|
}
|
|
148
|
+
/** True only for Pipeline 0.5's explicit rejected outer envelope, before a main-thread command is dispatched. */
|
|
149
|
+
export function isUnityPipelineInitialSettlingBusy(output: string): boolean {
|
|
150
|
+
let outer: RecordValue | undefined;
|
|
151
|
+
try { outer = record(JSON.parse(output)); } catch { return false; }
|
|
152
|
+
const data = record(outer?.data);
|
|
153
|
+
return outer?.success === false
|
|
154
|
+
&& string(field(data ?? {}, "error"))?.toLowerCase() === "server busy"
|
|
155
|
+
&& statusOf(data ?? {}) === "busy"
|
|
156
|
+
&& field(data ?? {}, "retryable") === true;
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
export function normalizeUnityPipelineCompile(output: string): NormalizedCompile {
|
|
149
160
|
const parsed = parseUnityPipelineEnvelope(output);
|
|
150
161
|
if (parsed.malformed) return { state: "uncertain", diagnostics: [], failed: false };
|
|
@@ -316,6 +327,19 @@ async function executeCommand(deps: PipelineDependencies, projectRoot: string, c
|
|
|
316
327
|
if (deadline !== undefined) ensureBeforeDeadline(deadline, now, command);
|
|
317
328
|
return result;
|
|
318
329
|
}
|
|
330
|
+
async function dispatchMainThreadCommand(deps: PipelineDependencies, projectRoot: string, command: "recompile" | "run_tests", args: string[], operation: string, signal: AbortSignal | undefined, deadline: number, now: () => number, sleep: (milliseconds: number, signal?: AbortSignal) => Promise<void>): Promise<UnityCliExecResult> {
|
|
331
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
332
|
+
const response = await executeCommand(deps, projectRoot, command, args, signal, deadline, now);
|
|
333
|
+
if (!isUnityPipelineInitialSettlingBusy(response.stdout)) return response;
|
|
334
|
+
const remaining = deadline - now();
|
|
335
|
+
if (remaining <= 0) throw new Error(`Unity Pipeline server remained busy while settling; ${operation} was not started before the deadline.`);
|
|
336
|
+
const delay = Math.min(UNITY_PIPELINE_BACKOFF_SECONDS[Math.min(attempt, UNITY_PIPELINE_BACKOFF_SECONDS.length - 1)]! * 1000, remaining);
|
|
337
|
+
await sleep(delay, signal);
|
|
338
|
+
throwIfAborted(signal);
|
|
339
|
+
if (now() >= deadline) throw new Error(`Unity Pipeline server remained busy while settling; ${operation} was not started before the deadline.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
319
343
|
async function requirePreflight(deps: PipelineDependencies, projectRoot: string, unityVersion: string, commands: string[], operation: "recompile" | "tests", signal: AbortSignal | undefined, deadline: number, now: () => number, allowAutonomousExitPlayMode = true): Promise<{ capabilities: UnityCliProjectCapabilities; exitedPlayMode: boolean; playModeHandling: UnityPipelinePlayModeHandling; scriptChangesWhilePlaying?: UnityScriptChangesWhilePlayingPolicy }> {
|
|
320
344
|
const capabilities = await inspectWithDeadline(deps, projectRoot, unityVersion, signal, deadline, now, "preflight");
|
|
321
345
|
const error = capabilityError(capabilities, commands); if (error) throw new Error(error);
|
|
@@ -404,7 +428,7 @@ export async function runUnityPipelineRecompile(request: UnityPipelineCompileReq
|
|
|
404
428
|
const lifecyclePrefix = playModeOutcomeText(preflight);
|
|
405
429
|
ensureBeforeDeadline(deadline, now, "recompile before dispatch");
|
|
406
430
|
throwIfAborted(signal);
|
|
407
|
-
const dispatched = await
|
|
431
|
+
const dispatched = await dispatchMainThreadCommand(deps, projectRoot, "recompile", [], "recompile", signal, deadline, now, sleep);
|
|
408
432
|
if (dispatched.error) throw new Error("Unity Pipeline recompile dispatch failed; operation may not have started.");
|
|
409
433
|
let state = normalizeUnityPipelineCompile(dispatched.stdout);
|
|
410
434
|
if (state.state === "uncertain") throw new Error("Unity Pipeline recompile dispatch returned malformed or uncertain evidence; operation may have started.");
|
|
@@ -421,6 +445,7 @@ export async function runUnityPipelineRecompile(request: UnityPipelineCompileReq
|
|
|
421
445
|
if (identityState === "temporary_disconnect") continue;
|
|
422
446
|
const response = await executeCommand(deps, projectRoot, "recompile_status", [], signal, deadline, now);
|
|
423
447
|
if (response.error) continue; // Domain reload can briefly disconnect the same exact copy.
|
|
448
|
+
if (isUnityPipelineInitialSettlingBusy(response.stdout)) continue;
|
|
424
449
|
state = normalizeUnityPipelineCompile(response.stdout);
|
|
425
450
|
if (state.state === "failed") throw new Error(`Unity recompile failed: ${state.diagnostics.join("; ") || "compiler failure reported"}`);
|
|
426
451
|
if (state.state === "completed" || state.state === "up_to_date") return { text: `${lifecyclePrefix}Unity recompile completed for ${projectRoot} in ${elapsed(start, now).toFixed(1)}s; 0 compiler errors.`, details: { projectRoot, operation: "recompile", terminalState: state.state, elapsedSeconds: elapsed(start, now), compilationTriggered: true, ...playModeDetails(preflight) } };
|
|
@@ -448,7 +473,7 @@ export async function runUnityPipelineTests(request: UnityPipelineTestRequest, d
|
|
|
448
473
|
}
|
|
449
474
|
const args = ["--mode", request.testPlatform === "EditMode" ? "editor" : "playmode", ...(request.testFilter ? ["--filter", request.testFilter, "--filter_type", "testName"] : []), "--async_tests", "true"];
|
|
450
475
|
ensureBeforeDeadline(deadline, now, "tests before dispatch"); throwIfAborted(signal);
|
|
451
|
-
const dispatched = await
|
|
476
|
+
const dispatched = await dispatchMainThreadCommand(deps, projectRoot, "run_tests", args, "tests", signal, deadline, now, sleep);
|
|
452
477
|
if (dispatched.error) throw new Error("Unity Pipeline test dispatch failed; test run may not have started.");
|
|
453
478
|
let state = normalizeUnityPipelineTest(dispatched.stdout);
|
|
454
479
|
if (state.state === "uncertain" || state.state === "inactive") throw new Error("Unity Pipeline test dispatch returned inactive, malformed, or uncertain evidence; test run may not have started.");
|
|
@@ -473,6 +498,7 @@ export async function runUnityPipelineTests(request: UnityPipelineTestRequest, d
|
|
|
473
498
|
if (identityState === "temporary_disconnect") continue;
|
|
474
499
|
const response = await executeCommand(deps, projectRoot, "test_status", [], signal, deadline, now);
|
|
475
500
|
if (response.error) continue;
|
|
501
|
+
if (isUnityPipelineInitialSettlingBusy(response.stdout)) continue;
|
|
476
502
|
state = normalizeUnityPipelineTest(response.stdout);
|
|
477
503
|
if (!checkCorrelation(expected, state.correlation)) throw new Error("Unity Pipeline test status was displaced by a different run; operation state is uncertain.");
|
|
478
504
|
if (state.state === "failed" || state.state === "cancelled") throw new Error(`Unity ${request.testPlatform} tests failed: ${state.failures.join("; ") || state.state}.`);
|