@aefree/pi-unity 0.9.3 → 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 +7 -0
- package/README.md +2 -2
- package/index.ts +2 -1
- package/package.json +1 -1
- package/skills/unity-pipeline-workflows/SKILL.md +2 -2
- package/src/unity-pipeline.ts +28 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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
|
+
|
|
8
15
|
## [0.9.3] - 2026-08-10
|
|
9
16
|
|
|
10
17
|
### Added
|
package/README.md
CHANGED
|
@@ -34,12 +34,12 @@ 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
|
|
package/index.ts
CHANGED
|
@@ -148,6 +148,7 @@ const PIPELINE_TEST_PARAMS = Type.Object({
|
|
|
148
148
|
const PIPELINE_EVAL_PARAMS = Type.Object({
|
|
149
149
|
path: Type.Optional(Type.String({ maxLength: 1000, description: "Unity project path, workspace copy root, or folder containing project copies." })),
|
|
150
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." })),
|
|
151
152
|
}, { additionalProperties: false });
|
|
152
153
|
|
|
153
154
|
const PIPELINE_INSPECTION_PARAMS = Type.Object({
|
|
@@ -1470,7 +1471,7 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1470
1471
|
}, {
|
|
1471
1472
|
execute: createPlanningUnityCliExecutor(pi),
|
|
1472
1473
|
signal,
|
|
1473
|
-
timeout:
|
|
1474
|
+
timeout: (params.timeoutSeconds ?? 12) * 1000,
|
|
1474
1475
|
});
|
|
1475
1476
|
throwIfAborted(signal);
|
|
1476
1477
|
const text = result.outcome === "dispatched"
|
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-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}.`);
|