@aefree/pi-unity 0.13.0 → 0.15.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 +23 -0
- package/README.md +15 -3
- package/index.ts +87 -217
- package/package.json +8 -3
- package/skills/unity-pipeline-workflows/SKILL.md +1 -1
- package/src/unity-artifact-inspection.ts +13 -9
- package/src/unity-artifact-profile.ts +1 -1
- package/src/unity-cli.ts +109 -17
- package/src/unity-file-discovery-filter.ts +1 -1
- package/src/unity-pipeline.ts +36 -11
- package/src/unity-projects.ts +2 -1
- package/src/unity-renderers.ts +174 -0
- package/src/unity-tests.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ and this project follows semantic versioning for public package releases.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.15.0 - 2026-09-15
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Correct Node directory-entry and child-process types, restore contextual typing for frozen optional integration contracts, and include the required details field in Pi progress updates.
|
|
15
|
+
- Add a pinned strict production-source type check that runs before the test suite.
|
|
16
|
+
- Preserve exact JSON values and raw evidence in expanded results, and prioritize failed/error test diagnostics before skipped records.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Use compact Unity action headers and consistent result summaries inside Pi's existing tool boxes. Expanded results include project identity, highlighted eval code, formatted JSON, evidence, and artifact paths.
|
|
21
|
+
- Show unified test counts, duration, route, failures, and uncertainty while collapsed; distinguish artifact inspection success from test outcomes and correct run-script and test labels.
|
|
22
|
+
- Add an offline tool presentation preview and renderer checks for narrow terminals, redaction, and failure visibility.
|
|
23
|
+
|
|
24
|
+
## 0.14.0 - 2026-09-14
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Preserve durable normalized connected-test evidence for terminal failed, cancelled, runner-error, and incomplete Pipeline results without replaying dispatch, overriding active Pipeline state from partial records, or misreporting inconsistent counts/records as passing.
|
|
29
|
+
- Select one primary latest normalized result and only its validated project-contained backend links when inspecting historical artifacts, including canonical contained aliases.
|
|
30
|
+
- Forward optional `unity_pipeline_eval.handlerTimeoutMilliseconds` only when the exact reachable Pipeline descriptor confirms raw argv support and the documented `code`/`timeout` signature. Preserve `timeoutSeconds` as the separate host/CLI wait; a dispatcher expiry cannot cancel eval code already started on Unity's main thread, so effects remain uncertain and are never retried or rerouted.
|
|
31
|
+
- Treat Pipeline 0.7's explicit `compilationFailed: true` response as a failed recompile even when its status is `up_to_date` or `completed` and no compiler-error array is present.
|
|
32
|
+
|
|
10
33
|
## 0.13.0 - 2026-09-10
|
|
11
34
|
|
|
12
35
|
### Added
|
package/README.md
CHANGED
|
@@ -25,6 +25,16 @@ pi install -l <path-to-pi-unity> # project-local
|
|
|
25
25
|
|
|
26
26
|
Pi discovers the extension from `index.ts` and packaged skills from `skills/`. In UI sessions, pi-unity warns once per Pi runtime when Unity CLI is unavailable or `UNITY_CLI_PATH` is invalid. The warning never displays configured paths; install Unity CLI, then restart or reload Pi.
|
|
27
27
|
|
|
28
|
+
## Development checks
|
|
29
|
+
|
|
30
|
+
Run `npm ci`, then `npm test` from the package repository. Tests first run the strict production-source type check (`index.ts` and `src/`) using the pinned TypeScript compiler and Node 22 types. Run `npm run typecheck` for that check alone. Tests and eval harnesses execute separately through `tsx`; they are outside this type-check scope.
|
|
31
|
+
|
|
32
|
+
## Tool presentation
|
|
33
|
+
|
|
34
|
+
In Pi's terminal UI, Unity tools use compact action headers and project names. Collapsed results show test counts, timing and route when available, plus failures and uncertainty. Expand using Pi's configured tool-details shortcut to see the full project path, highlighted eval code, formatted JSON results, bounded evidence, and artifact paths. Inspection success and test outcomes are displayed separately.
|
|
35
|
+
|
|
36
|
+
For a local, offline text preview, run `npm run preview:tools` from this package. The preview exercises the renderers without launching Unity; Pi supplies the surrounding tool box and configured keybinding hints in a live session.
|
|
37
|
+
|
|
28
38
|
## Included tools
|
|
29
39
|
|
|
30
40
|
### Connected Pipeline
|
|
@@ -34,13 +44,13 @@ Use these tools with an already-open exact Unity project copy that has a reachab
|
|
|
34
44
|
- `unity_project_status` — inspect lockfiles, matching Unity processes, Pipeline reachability, package version, and advertised commands without launching Unity.
|
|
35
45
|
- `unity_pipeline_recompile` — recompile through Pipeline with exact-copy preflight, bounded polling, and compact compiler evidence.
|
|
36
46
|
- `unity_run_tests` — one intent-oriented EditMode or PlayMode workflow. It reuses compatible connected Pipeline execution or selects isolated `unity test` when the exact project copy is closed.
|
|
37
|
-
- `unity_pipeline_eval` — execute bounded project-specific C# through Pipeline's Roslyn REPL.
|
|
47
|
+
- `unity_pipeline_eval` — execute bounded project-specific C# through Pipeline's Roslyn REPL. `timeoutSeconds` bounds pi-unity and Unity CLI waits (1–86,400 seconds). Optional `handlerTimeoutMilliseconds` is forwarded only when the exact reachable Pipeline advertises raw argv plus the verified eval timeout signature; it bounds that Pipeline dispatcher wait, not code already running on Unity's main thread. A timeout is uncertain and does not cancel or retry Editor work.
|
|
38
48
|
- `unity_pipeline_inspect` — dispatch supported package-owned inspection commands (including read-only `get_runtime_pipeline_settings`) and return structured evidence. Runtime settings are refused by Pipeline in Play Mode; pi-unity never exits Play Mode to read them.
|
|
39
49
|
- `unity_pipeline_run_script` — compile one existing project `.cs` file in Pipeline's ephemeral in-memory mode and invoke a named static entry point; supports bounded JSON arguments and compile-only `dryRun`. It deliberately does not expose hotpatch.
|
|
40
50
|
|
|
41
51
|
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.
|
|
42
52
|
|
|
43
|
-
A timeout is uncertain: work may still be running. The tools do not silently cancel, retry, launch another Editor, or switch to batchmode. Pipeline 0.6 can report busy for a modal dialog as well as startup settling, so ambiguous busy responses are surfaced and never blindly retried.
|
|
53
|
+
A timeout is uncertain: work may still be running. The tools do not silently cancel, retry, launch another Editor, or switch to batchmode. Pipeline 0.6 can report busy for a modal dialog as well as startup settling, so ambiguous busy responses are surfaced and never blindly retried. Pipeline 0.7 improves recovery of standing compile errors and console output. For non-development Player builds, Pipeline also requires the `ENABLE_RUNTIME_PIPELINE` scripting define; enabling its runtime setting alone is insufficient.
|
|
44
54
|
|
|
45
55
|
### Editor and batchmode
|
|
46
56
|
|
|
@@ -48,7 +58,7 @@ A timeout is uncertain: work may still be running. The tools do not silently can
|
|
|
48
58
|
- `unity_launch_batchmode` — run a bounded batchmode command through Unity CLI or the direct Editor executable.
|
|
49
59
|
- `unity_inspect_artifacts` — validate existing normalized JSON test artifacts, Unity Test Framework XML and Unity logs without launching Unity. `details.status` describes inspection; `details.testOutcome` describes the tests, including failures and uncertainty. A valid failed-test artifact is a successful inspection, not a passing run.
|
|
50
60
|
|
|
51
|
-
Pass `normalizedResultPath` for standalone JSON evidence. Any explicit artifact path disables implicit latest-file selection; missing requested files fail inspection. With all paths omitted, `latestFromLogs`
|
|
61
|
+
Pass `normalizedResultPath` for standalone JSON evidence. Any explicit artifact path disables implicit latest-file selection and link expansion; missing requested files fail inspection. With all paths omitted, `latestFromLogs` selects one newest top-level JSON (mtime then filename) and only its contained declared NUnit/log links. Without JSON it selects XML alone, then log context. Historical selection cannot establish current-run identity. Mixed JSON/XML evidence must agree; without an explicit native artifact link, matching counts alone leave correlation uncertain.
|
|
52
62
|
|
|
53
63
|
Use `unity_run_tests` for all ordinary Unity Test Framework work. It writes a durable normalized JSON result under `Logs/`; isolated `unity test` runs retain requested native reports. Connected Pipeline is selected only for compatible requests. A reachable Editor is never closed automatically to obtain isolated-only features.
|
|
54
64
|
|
|
@@ -113,6 +123,8 @@ Another connected client is not a project lock. When Pipeline returns stable cor
|
|
|
113
123
|
{ code: "var s = UnityEngine.Application.dataPath; return s.Length;" }
|
|
114
124
|
```
|
|
115
125
|
|
|
126
|
+
`timeoutSeconds` remains the host/CLI wait. On exact copies that advertise raw argv and the verified eval `code`/integer-`timeout` signature, `handlerTimeoutMilliseconds` (1–86,400,000) also sets Pipeline's dispatcher wait through its positional command argument. A shorter host timeout may still win. This server wait can prevent queued work from starting, but cannot cancel eval code that already began on Unity's main thread; treat expiry as uncertain and never retry or fall back.
|
|
127
|
+
|
|
116
128
|
Use `unity_pipeline_inspect` when a purpose-built structured command fits. Use eval for bounded project-specific work that matches the user's intent. Prefer typed tools when they provide stronger lifecycle, polling, validation, or recovery semantics.
|
|
117
129
|
|
|
118
130
|
### Pipeline run_script
|
package/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderUnityGuidanceResult, renderUnityToolCall, renderUnityPipelineCall, renderUnityToolResult, renderUnityPipelineResult } from "./src/unity-renderers";
|
|
2
|
+
import { type AgentToolUpdateCallback, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import type { Dirent } from "node:fs";
|
|
6
|
+
import { mkdir, readFile, readdir, realpath, stat, unlink } from "node:fs/promises";
|
|
7
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
-
import { getKeybindings,
|
|
9
|
+
import { getKeybindings, truncateToWidth } from "@earendil-works/pi-tui";
|
|
8
10
|
import {
|
|
9
11
|
buildUnityBatchmodeAgentText,
|
|
10
12
|
deriveUnityBatchmodeStatus,
|
|
@@ -32,7 +34,7 @@ import { applyUnityCliRetrySummary, compactUnityTestSummary, defaultUnityTestRep
|
|
|
32
34
|
import { validateNormalizedUnityTestArtifact } from "./src/unity-artifact-inspection";
|
|
33
35
|
import { UNITY_TEST_MAX_ARTIFACT_BYTES } from "./src/unity-tests";
|
|
34
36
|
import { auditUnityGuidance, type UnityGuidanceAuditResult } from "./src/unity-guidance-audit";
|
|
35
|
-
import { runUnityPipelineRecompile, runUnityPipelineTests, type UnityPipelineOperationDetails } from "./src/unity-pipeline";
|
|
37
|
+
import { runUnityPipelineRecompile, runUnityPipelineTests, UnityPipelineTerminalTestEvidenceError, type UnityPipelineOperationDetails } from "./src/unity-pipeline";
|
|
36
38
|
import {
|
|
37
39
|
createOptionalIntegrationRegistryV1,
|
|
38
40
|
isOptionalIntegrationActive,
|
|
@@ -69,7 +71,7 @@ async function loadFileDiscoveryFilterIntegrationV1(pi: Pick<ExtensionAPI, "getA
|
|
|
69
71
|
const GUI_WARNING = "This launches the full Unity Editor GUI and is not the same as batchmode/headless Unity.";
|
|
70
72
|
const SINGLE_PROCESS_WARNING = "Unity allows only one process per project folder. GUI Editor and batchmode/headless both count as that one process.";
|
|
71
73
|
|
|
72
|
-
type UnityToolDetails = {
|
|
74
|
+
export type UnityToolDetails = {
|
|
73
75
|
mode: "gui" | "batchmode" | "status" | "artifacts" | "pipeline_inspection" | "pipeline_eval" | "pipeline_run_script" | "pipeline" | "tests";
|
|
74
76
|
projectRoot: string;
|
|
75
77
|
unityVersion: string;
|
|
@@ -97,12 +99,16 @@ type UnityToolDetails = {
|
|
|
97
99
|
removedLockfile?: string;
|
|
98
100
|
piUnitySettings?: PiUnitySettings;
|
|
99
101
|
sessionSettings?: { allowAutonomousPlayModeExit: boolean };
|
|
102
|
+
projectState?: { nativeLockfileExists: boolean; runningProcessCount: number; processVerificationIncomplete: boolean; staleLockSuspected: boolean };
|
|
100
103
|
testBatch?: UnityTestBatchPlan;
|
|
101
104
|
cliCapabilities?: UnityCliProjectCapabilities;
|
|
102
105
|
pipelineInspection?: { outcome: "dispatched"; command: string; output: string; truncated: boolean } | { outcome: "rejected"; code: string; message: string };
|
|
103
106
|
pipelineEval?: { outcome: "dispatched"; command: string; output: string; truncated: boolean } | { outcome: "rejected"; code: string; message: string };
|
|
104
107
|
pipelineRunScript?: { outcome: "dispatched"; command: string; output: string; truncated: boolean } | { outcome: "rejected"; code: string; message: string };
|
|
105
108
|
pipeline?: UnityPipelineOperationDetails;
|
|
109
|
+
testResult?: NormalizedUnityTestResult;
|
|
110
|
+
artifactPath?: string;
|
|
111
|
+
route?: "connected" | "isolated";
|
|
106
112
|
};
|
|
107
113
|
|
|
108
114
|
const LAUNCHER_SCHEMA = Type.Optional(StringEnum(["auto", "unity-cli", "editor-executable"] as const, { description: "Launch backend. Defaults to auto, which prefers the Unity CLI and falls back to direct editor executable launch when the CLI is unavailable." }));
|
|
@@ -165,7 +171,8 @@ const PIPELINE_TEST_PARAMS = Type.Object({
|
|
|
165
171
|
const PIPELINE_EVAL_PARAMS = Type.Object({
|
|
166
172
|
path: Type.Optional(Type.String({ maxLength: 1000, description: "Unity project path, workspace copy root, or folder containing project copies." })),
|
|
167
173
|
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." }),
|
|
168
|
-
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." })),
|
|
174
|
+
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 86400, default: 12, description: "Connected eval host/CLI deadline in seconds (maximum 24 hours). A timeout is uncertain and does not retry or cancel Unity work." })),
|
|
175
|
+
handlerTimeoutMilliseconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 86400000, description: "Optional advertised Pipeline eval dispatcher wait in milliseconds. Forwarded only when the exact copy advertises raw argv and the verified code/timeout signature; it cannot cancel code already started on Unity's main thread." })),
|
|
169
176
|
}, { additionalProperties: false });
|
|
170
177
|
|
|
171
178
|
const PIPELINE_RUN_SCRIPT_PARAMS = Type.Object({
|
|
@@ -196,7 +203,7 @@ const INSPECT_ARTIFACTS_PARAMS = Type.Object({
|
|
|
196
203
|
testResultsPath: Type.Optional(Type.String({ description: "Unity Test Framework XML results path. Relative paths are resolved against cwd and the Unity project root." })),
|
|
197
204
|
normalizedResultPath: Type.Optional(Type.String({ description: "pi-unity normalized JSON test artifact path." })),
|
|
198
205
|
logFilePath: Type.Optional(Type.String({ description: "Unity log file path. Relative paths are resolved against cwd and the Unity project root." })),
|
|
199
|
-
latestFromLogs: Type.Optional(Type.Boolean({ default: true, description: "Only when all artifact paths are omitted,
|
|
206
|
+
latestFromLogs: Type.Optional(Type.Boolean({ default: true, description: "Only when all artifact paths are omitted, select one newest top-level Logs JSON (mtime then filename) and only its declared contained NUnit/log links. Without JSON, select newest XML alone, otherwise newest log context. Latest files are not proof of a shared run." })),
|
|
200
207
|
maxLines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, default: 60, description: "Maximum log/output lines to include." })),
|
|
201
208
|
maxChars: Type.Optional(Type.Integer({ minimum: 500, maximum: 20000, default: 6000, description: "Maximum log/output characters to include." })),
|
|
202
209
|
});
|
|
@@ -677,17 +684,19 @@ async function buildProjectStatusReport(
|
|
|
677
684
|
status: "passed",
|
|
678
685
|
piUnitySettings,
|
|
679
686
|
sessionSettings: { allowAutonomousPlayModeExit },
|
|
687
|
+
projectState: { nativeLockfileExists: lockState.nativeLockfileExists, runningProcessCount: runningProcesses.length, processVerificationIncomplete: Boolean(warning), staleLockSuspected },
|
|
680
688
|
cliCapabilities,
|
|
681
689
|
},
|
|
682
690
|
};
|
|
683
691
|
}
|
|
684
692
|
|
|
685
693
|
async function findNewestFile(root: string, suffixes: string[]): Promise<string | undefined> {
|
|
686
|
-
let entries:
|
|
694
|
+
let entries: Dirent[];
|
|
687
695
|
try {
|
|
688
696
|
entries = await readdir(root, { withFileTypes: true });
|
|
689
|
-
} catch {
|
|
690
|
-
return undefined;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
699
|
+
throw error;
|
|
691
700
|
}
|
|
692
701
|
|
|
693
702
|
const files = await Promise.all(entries
|
|
@@ -697,7 +706,16 @@ async function findNewestFile(root: string, suffixes: string[]): Promise<string
|
|
|
697
706
|
const stats = await stat(fullPath);
|
|
698
707
|
return { fullPath, mtimeMs: stats.mtimeMs };
|
|
699
708
|
}));
|
|
700
|
-
return files.sort((left, right) => right.mtimeMs - left.mtimeMs)[0]?.fullPath;
|
|
709
|
+
return files.sort((left, right) => right.mtimeMs - left.mtimeMs || left.fullPath.localeCompare(right.fullPath))[0]?.fullPath;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
async function resolveLinkedArtifact(projectRoot: string, linkPath: string): Promise<string> {
|
|
713
|
+
const canonicalRoot = await realpath(projectRoot);
|
|
714
|
+
const candidate = resolve(projectRoot, linkPath);
|
|
715
|
+
const canonicalCandidate = await realpath(candidate);
|
|
716
|
+
const relativePath = relative(canonicalRoot, canonicalCandidate);
|
|
717
|
+
if (!relativePath || isAbsolute(relativePath) || relativePath === ".." || relativePath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) throw new Error(`Linked artifact escapes the project root: ${linkPath}`);
|
|
718
|
+
return canonicalCandidate;
|
|
701
719
|
}
|
|
702
720
|
|
|
703
721
|
function resolveArtifactPath(cwd: string, projectRoot: string, value: string | undefined): string | undefined {
|
|
@@ -726,12 +744,14 @@ async function buildArtifactInspectionReport(
|
|
|
726
744
|
// An exact artifact request must not silently recruit unrelated latest evidence.
|
|
727
745
|
const useLatest = params.latestFromLogs !== false && ![params.testResultsPath, params.logFilePath, params.normalizedResultPath].some(value => value?.trim());
|
|
728
746
|
const logsRoot = join(candidate.projectRoot, "Logs");
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const logFilePath = resolveArtifactPath(ctx.cwd, candidate.projectRoot, params.logFilePath)
|
|
732
|
-
?? (useLatest ? await findNewestFile(logsRoot, [".log", ".txt"]) : undefined);
|
|
747
|
+
let testResultsPath = resolveArtifactPath(ctx.cwd, candidate.projectRoot, params.testResultsPath);
|
|
748
|
+
let logFilePath = resolveArtifactPath(ctx.cwd, candidate.projectRoot, params.logFilePath);
|
|
733
749
|
const normalizedResultPath = resolveArtifactPath(ctx.cwd, candidate.projectRoot, params.normalizedResultPath)
|
|
734
750
|
?? (useLatest ? await findNewestFile(logsRoot, [".json"]) : undefined);
|
|
751
|
+
if (useLatest && !normalizedResultPath) {
|
|
752
|
+
testResultsPath = await findNewestFile(logsRoot, [".xml"]);
|
|
753
|
+
if (!testResultsPath) logFilePath = await findNewestFile(logsRoot, [".log", ".txt"]);
|
|
754
|
+
}
|
|
735
755
|
let normalized: NormalizedUnityTestResult | undefined;
|
|
736
756
|
const evidenceErrors: string[] = [];
|
|
737
757
|
const evidenceWarnings: string[] = [];
|
|
@@ -739,6 +759,11 @@ async function buildArtifactInspectionReport(
|
|
|
739
759
|
try {
|
|
740
760
|
if ((await stat(normalizedResultPath)).size > UNITY_TEST_MAX_ARTIFACT_BYTES) throw new Error("Normalized artifact exceeds its size limit.");
|
|
741
761
|
normalized = validateNormalizedUnityTestArtifact(JSON.parse(await readFile(normalizedResultPath, "utf8")));
|
|
762
|
+
// Stored backend links, not unrelated directory recency, are the only automatic companions.
|
|
763
|
+
if (useLatest) {
|
|
764
|
+
if (normalized.backendArtifacts?.nunit) testResultsPath = await resolveLinkedArtifact(candidate.projectRoot, normalized.backendArtifacts.nunit);
|
|
765
|
+
if (normalized.backendArtifacts?.log) logFilePath = await resolveLinkedArtifact(candidate.projectRoot, normalized.backendArtifacts.log);
|
|
766
|
+
}
|
|
742
767
|
} catch (error) {
|
|
743
768
|
evidenceErrors.push(`Normalized test result could not be loaded/validated: ${normalizedResultPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
744
769
|
}
|
|
@@ -769,7 +794,13 @@ async function buildArtifactInspectionReport(
|
|
|
769
794
|
}
|
|
770
795
|
if ((normalized.outcome === "passed" || normalized.outcome === "passed_with_flakes") && parsedTestResults.failedTests.length > 0) evidenceErrors.push("Conflicting normalized/XML evidence: XML contains failed tests.");
|
|
771
796
|
const linkedXml = normalized.backendArtifacts?.nunit;
|
|
772
|
-
if (linkedXml &&
|
|
797
|
+
if (linkedXml && testResultsPath) {
|
|
798
|
+
try {
|
|
799
|
+
if (await resolveLinkedArtifact(candidate.projectRoot, linkedXml) !== await realpath(testResultsPath)) evidenceErrors.push("Conflicting artifact identity: selected XML is not the normalized artifact's nunit path.");
|
|
800
|
+
} catch (error) {
|
|
801
|
+
evidenceErrors.push(`Normalized NUnit link could not be resolved: ${linkedXml}: ${error instanceof Error ? error.message : String(error)}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
773
804
|
if (!linkedXml) {
|
|
774
805
|
evidenceWarnings.push("Normalized JSON and XML have no shared run identity; matching counts alone do not correlate these files.");
|
|
775
806
|
testOutcome = "uncertain";
|
|
@@ -905,92 +936,6 @@ async function buildBatchmodeReport(
|
|
|
905
936
|
};
|
|
906
937
|
}
|
|
907
938
|
|
|
908
|
-
function compactUnityRendererValue(value: unknown, limit = 160): string {
|
|
909
|
-
const redacted = String(value ?? "").replace(
|
|
910
|
-
/\b(token|secret|password|api[_-]?key)\s*([:=])\s*((?:\$@?|@\$?)?"(?:""|\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;)}\]]+)/gi,
|
|
911
|
-
"$1$2[redacted]",
|
|
912
|
-
);
|
|
913
|
-
const normalized = redacted.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim();
|
|
914
|
-
return normalized.length > limit ? `${normalized.slice(0, limit - 1)}…` : normalized;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function reuseRendererText(context: { lastComponent?: unknown } | undefined, text: string): Text {
|
|
918
|
-
const component = context?.lastComponent;
|
|
919
|
-
if (component instanceof Text) {
|
|
920
|
-
component.setText(text);
|
|
921
|
-
return component;
|
|
922
|
-
}
|
|
923
|
-
return new Text(text, 0, 0);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
function renderUnityToolCall(
|
|
927
|
-
name: string,
|
|
928
|
-
args: { path?: string; args?: string[] },
|
|
929
|
-
theme: any,
|
|
930
|
-
modeLabel: string,
|
|
931
|
-
emphasis: string,
|
|
932
|
-
context?: { lastComponent?: unknown },
|
|
933
|
-
): Text {
|
|
934
|
-
const pathLabel = compactUnityRendererValue(args.path?.trim() || "auto-resolve", 120);
|
|
935
|
-
const extraArgs = Array.isArray(args.args) && args.args.length > 0
|
|
936
|
-
? args.args.slice(0, 4).join(" ") + (args.args.length > 4 ? ` ... +${args.args.length - 4}` : "")
|
|
937
|
-
: undefined;
|
|
938
|
-
let text =
|
|
939
|
-
theme.fg("toolTitle", theme.bold(`${name} `)) +
|
|
940
|
-
theme.fg("accent", modeLabel) +
|
|
941
|
-
theme.fg("muted", ` (${emphasis})`);
|
|
942
|
-
text += `\n ${theme.fg("accent", pathLabel)}`;
|
|
943
|
-
if (extraArgs) {
|
|
944
|
-
text += `\n ${theme.fg("muted", extraArgs)}`;
|
|
945
|
-
}
|
|
946
|
-
return reuseRendererText(context, text);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
function renderUnityPipelineCall(
|
|
950
|
-
name: string,
|
|
951
|
-
args: { path?: string; testPlatform?: string; testFilter?: string; command?: string; code?: string },
|
|
952
|
-
theme: any,
|
|
953
|
-
context: { lastComponent?: unknown },
|
|
954
|
-
): Text {
|
|
955
|
-
const detail = name === "unity_pipeline_run_tests"
|
|
956
|
-
? `${args.testPlatform ?? "tests"}${args.testFilter ? ` • ${compactUnityRendererValue(args.testFilter, 100)}` : ""}`
|
|
957
|
-
: name === "unity_pipeline_inspect"
|
|
958
|
-
? `command=${compactUnityRendererValue(args.command ?? "(missing)", 100)}`
|
|
959
|
-
: name === "unity_pipeline_eval"
|
|
960
|
-
? `C# ${compactUnityRendererValue(args.code ?? "(missing)", 140)}`
|
|
961
|
-
: "connected bounded recompile";
|
|
962
|
-
return renderUnityToolCall(name, args, theme, "pipeline", detail, context);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
function getToolTextContent(result: any): string {
|
|
966
|
-
return Array.isArray(result.content)
|
|
967
|
-
? result.content.filter((entry: any) => entry?.type === "text").map((entry: any) => String(entry.text ?? "")).join("\n")
|
|
968
|
-
: "";
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function buildBatchmodeStatusLine(details: UnityToolDetails, theme: any): string {
|
|
972
|
-
const status = details.status ?? "passed";
|
|
973
|
-
let line = `\n ${theme.fg("accent", `status=${status}`)}${theme.fg("muted", ` exit=${details.exitCode ?? 0}`)}`;
|
|
974
|
-
if (details.invocation?.testPlatform) {
|
|
975
|
-
line += ` ${theme.fg("muted", `platform=${details.invocation.testPlatform}`)}`;
|
|
976
|
-
}
|
|
977
|
-
return line;
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function buildBatchmodeResultsLine(details: UnityToolDetails, theme: any): string {
|
|
981
|
-
if (!details.parsedTestResults) {
|
|
982
|
-
return "";
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
const parts = [
|
|
986
|
-
details.parsedTestResults.total !== undefined ? `total ${details.parsedTestResults.total}` : undefined,
|
|
987
|
-
details.parsedTestResults.passed !== undefined ? `passed ${details.parsedTestResults.passed}` : undefined,
|
|
988
|
-
details.parsedTestResults.failed !== undefined ? `failed ${details.parsedTestResults.failed}` : undefined,
|
|
989
|
-
].filter(Boolean);
|
|
990
|
-
|
|
991
|
-
return parts.length > 0 ? `\n ${theme.fg("muted", parts.join(" • "))}` : "";
|
|
992
|
-
}
|
|
993
|
-
|
|
994
939
|
function throwIfAborted(signal?: AbortSignal): void {
|
|
995
940
|
if (signal?.aborted) {
|
|
996
941
|
throw new Error("Unity tool execution aborted.");
|
|
@@ -1204,7 +1149,7 @@ async function runUnifiedUnityTests(
|
|
|
1204
1149
|
discoveryWarning: string | undefined,
|
|
1205
1150
|
raw: UnityRunTestsRequest,
|
|
1206
1151
|
signal: AbortSignal | undefined,
|
|
1207
|
-
onUpdate?:
|
|
1152
|
+
onUpdate?: AgentToolUpdateCallback<unknown>,
|
|
1208
1153
|
allowAutonomousExitPlayMode = true,
|
|
1209
1154
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: UnityToolDetails & { testResult: NormalizedUnityTestResult; artifactPath: string; route: "connected" | "isolated" } }> {
|
|
1210
1155
|
const request = normalizeUnityRunTestsRequest(raw);
|
|
@@ -1230,7 +1175,25 @@ async function runUnifiedUnityTests(
|
|
|
1230
1175
|
}
|
|
1231
1176
|
const formats = request.reportFormats ?? defaultUnityTestReportFormats(route);
|
|
1232
1177
|
if (route === "connected") {
|
|
1233
|
-
|
|
1178
|
+
let result;
|
|
1179
|
+
try {
|
|
1180
|
+
result = await runUnityPipelineTests({ projectRoot: candidate.projectRoot, unityVersion: await requireManualUnityVersion(candidate), testPlatform: request.testPlatform, testFilter: request.testFilters[0], testCategory: request.testCategories[0], timeoutSeconds: request.timeoutSeconds, allowAutonomousExitPlayMode }, createPipelineDependencies(pi), { signal, onUpdate: message => onUpdate?.({ content: [{ type: "text", text: message }], details: undefined }) });
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
if (!(error instanceof UnityPipelineTerminalTestEvidenceError)) throw error;
|
|
1183
|
+
const outcome = error.evidence.outcome;
|
|
1184
|
+
const normalized: NormalizedUnityTestResult = {
|
|
1185
|
+
schemaVersion: 1, source: "pipeline", platform: request.testPlatform,
|
|
1186
|
+
selection: { testFilters: request.testFilters, testCategories: request.testCategories },
|
|
1187
|
+
durationSeconds: error.evidence.elapsedSeconds, outcome, summary: {}, tests: error.evidence.testRecords,
|
|
1188
|
+
diagnostics: [error.evidence.reason, ...error.evidence.observations, ...error.evidence.warnings].slice(0, 8),
|
|
1189
|
+
};
|
|
1190
|
+
let artifactPath: string;
|
|
1191
|
+
try { artifactPath = await writeNormalizedUnityTestArtifact(candidate.projectRoot, normalized); } catch (persistenceError) {
|
|
1192
|
+
throw new Error(`${error.evidence.reason} Durable terminal evidence could not be persisted: ${persistenceError instanceof Error ? persistenceError.message : String(persistenceError)}`);
|
|
1193
|
+
}
|
|
1194
|
+
const text = `${compactUnityTestSummary(normalized)}\nRoute: connected Pipeline. Terminal evidence was incomplete or non-passing; no retry, fallback, or replay was performed. Normalized artifact: ${artifactPath}`;
|
|
1195
|
+
return { content: [{ type: "text", text }], details: { mode: "tests", projectRoot: candidate.projectRoot, unityVersion: await requireManualUnityVersion(candidate), editorPath: "", status: "failed", testResult: { ...normalized, tests: [] }, artifactPath, route } };
|
|
1196
|
+
}
|
|
1234
1197
|
const counts = result.details.counts!;
|
|
1235
1198
|
const normalized: NormalizedUnityTestResult = {
|
|
1236
1199
|
schemaVersion: 1, source: "pipeline", platform: request.testPlatform,
|
|
@@ -1287,94 +1250,6 @@ async function runUnifiedUnityTests(
|
|
|
1287
1250
|
});
|
|
1288
1251
|
}
|
|
1289
1252
|
|
|
1290
|
-
function renderUnityPipelineResult(result: any, options: { expanded: boolean; isPartial: boolean }, theme: any, context: { lastComponent?: unknown }): Text {
|
|
1291
|
-
const details = result.details as UnityToolDetails | undefined;
|
|
1292
|
-
const primaryText = getToolTextContent(result);
|
|
1293
|
-
if (options.isPartial) {
|
|
1294
|
-
return reuseRendererText(context, `${theme.fg("warning", "…")} ${theme.fg("toolTitle", theme.bold("Unity Pipeline working"))}\n ${theme.fg("muted", compactUnityRendererValue(primaryText || "Waiting for Pipeline…", 180))}`);
|
|
1295
|
-
}
|
|
1296
|
-
if (!details) return reuseRendererText(context, primaryText || "(no output)");
|
|
1297
|
-
|
|
1298
|
-
const pipeline = details.pipeline;
|
|
1299
|
-
const icon = details.status === "passed" ? theme.fg("success", "✓") : theme.fg("error", "✗");
|
|
1300
|
-
let text: string;
|
|
1301
|
-
if (pipeline?.operation === "recompile") {
|
|
1302
|
-
text = `${icon} ${theme.fg("toolTitle", theme.bold("Unity recompile"))} ${theme.fg("accent", pipeline.terminalState)}${theme.fg("muted", ` • ${pipeline.elapsedSeconds.toFixed(1)}s`)}`;
|
|
1303
|
-
} else if (pipeline?.operation === "tests") {
|
|
1304
|
-
const counts = pipeline.counts;
|
|
1305
|
-
const passed = counts?.passed === undefined || counts?.total === undefined ? "tests completed" : `${counts.passed}/${counts.total} passed`;
|
|
1306
|
-
text = `${icon} ${theme.fg("toolTitle", theme.bold(`Unity ${pipeline.testPlatform ?? ""} tests`.trim()))} ${theme.fg("accent", passed)}${theme.fg("muted", ` • ${pipeline.elapsedSeconds.toFixed(1)}s`)}`;
|
|
1307
|
-
} else if (details.mode === "pipeline_eval" || details.mode === "pipeline_inspection" || details.mode === "pipeline_run_script") {
|
|
1308
|
-
const output = details.mode === "pipeline_eval" ? details.pipelineEval : details.mode === "pipeline_run_script" ? details.pipelineRunScript : details.pipelineInspection;
|
|
1309
|
-
const label = details.mode === "pipeline_eval" ? "Unity Pipeline Eval" : details.mode === "pipeline_run_script" ? "Unity Pipeline Run Script" : "Unity Pipeline Inspection";
|
|
1310
|
-
const summary = output?.outcome === "dispatched" ? output.output || "(no bounded output returned)" : output?.message || primaryText;
|
|
1311
|
-
text = `${icon} ${theme.fg("toolTitle", theme.bold(label))}\n ${theme.fg("toolOutput", compactUnityRendererValue(summary, 240))}`;
|
|
1312
|
-
} else {
|
|
1313
|
-
return renderUnityToolResult(result, options.expanded, theme);
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
if (pipeline?.playModeHandling && pipeline.playModeHandling !== "not_playing") {
|
|
1317
|
-
const handling = pipeline.playModeHandling === "agent_exited" ? "Play Mode exited by pi-unity" : `Play Mode: ${pipeline.playModeHandling.replace(/_/g, " ")}`;
|
|
1318
|
-
text += `\n ${theme.fg("warning", handling)}`;
|
|
1319
|
-
}
|
|
1320
|
-
if (options.expanded && primaryText) text += `\n\n${theme.fg("toolOutput", primaryText)}`;
|
|
1321
|
-
else if (!options.expanded) text += ` ${theme.fg("dim", `(${keyHint("app.tools.expand", "details")})`)}`;
|
|
1322
|
-
return reuseRendererText(context, text);
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
function renderUnityToolResult(result: any, expanded: boolean, theme: any): Text {
|
|
1326
|
-
const details = result.details as UnityToolDetails | undefined;
|
|
1327
|
-
const primaryText = getToolTextContent(result);
|
|
1328
|
-
|
|
1329
|
-
if (!details) {
|
|
1330
|
-
return new Text(primaryText || "(no output)", 0, 0);
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
const icon = details.mode === "gui"
|
|
1334
|
-
? theme.fg("success", "◉")
|
|
1335
|
-
: details.status === "passed"
|
|
1336
|
-
? theme.fg("success", "✓")
|
|
1337
|
-
: details.status === "killed"
|
|
1338
|
-
? theme.fg("warning", "! ")
|
|
1339
|
-
: theme.fg("error", "✗");
|
|
1340
|
-
const title = details.mode === "gui"
|
|
1341
|
-
? "Unity Editor"
|
|
1342
|
-
: details.mode === "status"
|
|
1343
|
-
? "Unity Project Status"
|
|
1344
|
-
: details.mode === "artifacts"
|
|
1345
|
-
? "Unity Artifacts"
|
|
1346
|
-
: details.mode === "pipeline_inspection"
|
|
1347
|
-
? "Unity Pipeline Inspection"
|
|
1348
|
-
: details.mode === "pipeline_eval"
|
|
1349
|
-
? "Unity Pipeline Eval"
|
|
1350
|
-
: details.mode === "pipeline_run_script"
|
|
1351
|
-
? "Unity Pipeline Run Script"
|
|
1352
|
-
: details.mode === "pipeline"
|
|
1353
|
-
? "Unity Pipeline"
|
|
1354
|
-
: getBatchmodeVariantLabel(details.args);
|
|
1355
|
-
const projectLabel = details.projectRoot ?? "(unknown project)";
|
|
1356
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(title))} ${theme.fg("muted", projectLabel)}`;
|
|
1357
|
-
if (details.mode === "batchmode") {
|
|
1358
|
-
text += buildBatchmodeStatusLine(details, theme);
|
|
1359
|
-
text += buildBatchmodeResultsLine(details, theme);
|
|
1360
|
-
} else if (details.mode === "status") {
|
|
1361
|
-
text += `\n ${theme.fg("accent", `status=${details.status ?? "passed"}`)}`;
|
|
1362
|
-
} else if (details.mode === "pipeline" && details.pipeline) {
|
|
1363
|
-
text += `\n ${theme.fg("accent", `${details.pipeline.operation}=${details.pipeline.terminalState}`)}${theme.fg("muted", ` ${details.pipeline.elapsedSeconds.toFixed(1)}s`)}`;
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
if (expanded && primaryText) {
|
|
1367
|
-
text += `\n\n${theme.fg("toolOutput", primaryText)}`;
|
|
1368
|
-
} else if (!expanded && details.mode === "batchmode") {
|
|
1369
|
-
const snippet = summarizeTextForAgent(details.stderr) ?? summarizeTextForAgent(details.stdout);
|
|
1370
|
-
if (snippet) {
|
|
1371
|
-
text += `\n ${theme.fg("muted", snippet.split(/\r?\n/)[0])}`;
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
return new Text(text, 0, 0);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
1253
|
function formatUnityGuidanceAudit(result: UnityGuidanceAuditResult): string {
|
|
1379
1254
|
const lines = [
|
|
1380
1255
|
`Unity guidance audit scanned ${result.summary.filesScanned} file(s): ${result.summary.errors} error(s), ${result.summary.warnings} warning(s), ${result.summary.infos} info finding(s).`,
|
|
@@ -1404,7 +1279,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1404
1279
|
const details = event.details as UnityToolDetails | undefined;
|
|
1405
1280
|
if ((event.toolName === "unity_pipeline_eval" && details?.mode === "pipeline_eval" && details.pipelineEval?.outcome === "rejected")
|
|
1406
1281
|
|| (event.toolName === "unity_pipeline_inspect" && details?.mode === "pipeline_inspection" && details.pipelineInspection?.outcome === "rejected")
|
|
1407
|
-
|| (event.toolName === "unity_pipeline_run_script" && details?.mode === "pipeline_run_script" && details.pipelineRunScript?.outcome === "rejected")
|
|
1282
|
+
|| (event.toolName === "unity_pipeline_run_script" && details?.mode === "pipeline_run_script" && details.pipelineRunScript?.outcome === "rejected")
|
|
1283
|
+
|| (event.toolName === "unity_run_tests" && details?.mode === "tests" && details.testResult?.outcome !== "passed" && details.testResult?.outcome !== "passed_with_flakes" && details.testResult?.outcome !== "empty_selection")) {
|
|
1408
1284
|
return { isError: true };
|
|
1409
1285
|
}
|
|
1410
1286
|
});
|
|
@@ -1572,16 +1448,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1572
1448
|
renderCall(args, theme) {
|
|
1573
1449
|
return renderUnityToolCall("unity_guidance_audit", args, theme, "guidance", "read-only instruction audit");
|
|
1574
1450
|
},
|
|
1575
|
-
renderResult(result,
|
|
1576
|
-
|
|
1577
|
-
const primaryText = getToolTextContent(result);
|
|
1578
|
-
if (!details) return new Text(primaryText || "(no output)", 0, 0);
|
|
1579
|
-
const count = details.summary.errors + details.summary.warnings + details.summary.infos;
|
|
1580
|
-
const ancestorCount = details.ancestorCandidates.length;
|
|
1581
|
-
let text = `${count > 0 || ancestorCount > 0 ? theme.fg("warning", "!") : theme.fg("success", "✓")} ${theme.fg("toolTitle", theme.bold("Unity Guidance Audit"))}`;
|
|
1582
|
-
text += `\n ${theme.fg("muted", `${details.summary.filesScanned} files • ${count} findings${ancestorCount > 0 ? ` • ${ancestorCount} ancestor files excluded` : ""}`)}`;
|
|
1583
|
-
if (expanded && primaryText) text += `\n\n${theme.fg("toolOutput", primaryText)}`;
|
|
1584
|
-
return new Text(text, 0, 0);
|
|
1451
|
+
renderResult(result, options, theme, context) {
|
|
1452
|
+
return renderUnityGuidanceResult(result, options, theme, context);
|
|
1585
1453
|
},
|
|
1586
1454
|
});
|
|
1587
1455
|
|
|
@@ -1610,8 +1478,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1610
1478
|
renderCall(args, theme) {
|
|
1611
1479
|
return renderUnityToolCall("unity_project_status", args, theme, "status", "inspects project lock");
|
|
1612
1480
|
},
|
|
1613
|
-
renderResult(result, { expanded }, theme) {
|
|
1614
|
-
return renderUnityToolResult(result, expanded, theme);
|
|
1481
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1482
|
+
return renderUnityToolResult(result, expanded, theme, context, isPartial);
|
|
1615
1483
|
},
|
|
1616
1484
|
});
|
|
1617
1485
|
|
|
@@ -1633,8 +1501,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1633
1501
|
const { candidate, discoveryWarning } = await resolveProjectCandidate(ctx, params.path);
|
|
1634
1502
|
return await runUnifiedUnityTests(pi, ctx, candidate, discoveryWarning, params as UnityRunTestsRequest, signal, onUpdate, sessionAllowsAutonomousPlayModeExit(ctx));
|
|
1635
1503
|
},
|
|
1636
|
-
renderCall(args, theme, context) { return renderUnityToolCall("unity_run_tests", args, theme, "tests",
|
|
1637
|
-
renderResult(result, { expanded }, theme) { return renderUnityToolResult(result, expanded, theme); },
|
|
1504
|
+
renderCall(args, theme, context) { return renderUnityToolCall("unity_run_tests", args, theme, "tests", undefined, context); },
|
|
1505
|
+
renderResult(result, { expanded, isPartial }, theme, context) { return renderUnityToolResult(result, expanded, theme, context, isPartial); },
|
|
1638
1506
|
});
|
|
1639
1507
|
|
|
1640
1508
|
pi.registerTool({
|
|
@@ -1653,7 +1521,7 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1653
1521
|
const { candidate } = await resolveProjectCandidate(ctx, params.path);
|
|
1654
1522
|
const result = await runUnityPipelineRecompile({ projectRoot: candidate.projectRoot, unityVersion: await requireManualUnityVersion(candidate), timeoutSeconds: params.timeoutSeconds, allowAutonomousExitPlayMode: sessionAllowsAutonomousPlayModeExit(ctx) }, createPipelineDependencies(pi), {
|
|
1655
1523
|
signal,
|
|
1656
|
-
onUpdate: message => onUpdate?.({ content: [{ type: "text", text: message }] }),
|
|
1524
|
+
onUpdate: message => onUpdate?.({ content: [{ type: "text", text: message }], details: undefined }),
|
|
1657
1525
|
});
|
|
1658
1526
|
return {
|
|
1659
1527
|
content: [{ type: "text", text: result.text }],
|
|
@@ -1667,12 +1535,13 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1667
1535
|
pi.registerTool({
|
|
1668
1536
|
name: "unity_pipeline_eval",
|
|
1669
1537
|
label: "Unity Pipeline Eval",
|
|
1670
|
-
description: "Execute one bounded C# snippet through advertised eval in an already-open exact Unity Pipeline Editor.",
|
|
1538
|
+
description: "Execute one bounded C# snippet through advertised eval in an already-open exact Unity Pipeline Editor. timeoutSeconds bounds pi-unity and Unity CLI waits; optional handlerTimeoutMilliseconds is forwarded only through a verified Pipeline raw-argv timeout contract and cannot cancel code already started on Unity's main thread.",
|
|
1671
1539
|
promptSnippet: "Query or operate on an already-open exact Unity project through Pipeline's Roslyn C# REPL.",
|
|
1672
1540
|
promptGuidelines: [
|
|
1673
1541
|
"Use unity_pipeline_eval for project-specific properties, APIs, and operations that advertised typed commands do not cover. It revalidates exact-copy identity and advertised eval immediately before dispatch.",
|
|
1674
1542
|
"Pipeline eval compiles arbitrary C# with Roslyn on the Editor main thread. Include an explicit return value for observable evidence; normal property reads and local-variable snippets are supported.",
|
|
1675
1543
|
"Eval is not statically read-only. Follow user intent and project guidance, and obtain explicit authorization before lifecycle, persistent-setting, destructive, asset, scene-save, package, build, or test mutations.",
|
|
1544
|
+
"timeoutSeconds bounds the host and Unity CLI wait. When supplied, handlerTimeoutMilliseconds bounds only the verified Pipeline dispatcher wait; a shorter host deadline may still win, and a server wait expiry cannot cancel code already started on Unity's main thread.",
|
|
1676
1545
|
"A rejected, malformed, failing, or timed-out eval is not success; do not silently retry it through another route.",
|
|
1677
1546
|
],
|
|
1678
1547
|
parameters: PIPELINE_EVAL_PARAMS,
|
|
@@ -1685,6 +1554,7 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1685
1554
|
unityVersion: await requireManualUnityVersion(candidate),
|
|
1686
1555
|
command: "eval",
|
|
1687
1556
|
evalSnippet: params.code,
|
|
1557
|
+
handlerTimeoutMilliseconds: params.handlerTimeoutMilliseconds,
|
|
1688
1558
|
}, {
|
|
1689
1559
|
execute: createPlanningUnityCliExecutor(pi),
|
|
1690
1560
|
signal,
|
|
@@ -1804,7 +1674,7 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1804
1674
|
"Use unity_inspect_artifacts after Unity failures when existing -testResults or -logFile artifacts need concise parsing without another Unity launch.",
|
|
1805
1675
|
"Prefer unity_inspect_artifacts over ad hoc bash parsing of Unity XML/log files when paths are known or Logs/ contains recent artifacts.",
|
|
1806
1676
|
"unity_inspect_artifacts does not launch Unity and is safe to use even when the Unity project is busy.",
|
|
1807
|
-
"Inspect details.testOutcome, not inspection status, for test success. Passing evidence needs consistent positive passing counts; missing explicit paths and conflicting artifacts fail inspection. Latest files are not current-run identity.",
|
|
1677
|
+
"Inspect details.testOutcome, not inspection status, for test success. Passing evidence needs consistent positive passing counts; missing explicit paths and conflicting artifacts fail inspection. With all paths omitted, latest selection uses one JSON and its contained declared links, otherwise XML alone then log context; explicit paths disable selection/link expansion. Latest files are not current-run identity.",
|
|
1808
1678
|
],
|
|
1809
1679
|
parameters: INSPECT_ARTIFACTS_PARAMS,
|
|
1810
1680
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -1821,8 +1691,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1821
1691
|
renderCall(args, theme) {
|
|
1822
1692
|
return renderUnityToolCall("unity_inspect_artifacts", args, theme, "artifacts", "reads logs/results");
|
|
1823
1693
|
},
|
|
1824
|
-
renderResult(result, { expanded }, theme) {
|
|
1825
|
-
return renderUnityToolResult(result, expanded, theme);
|
|
1694
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1695
|
+
return renderUnityToolResult(result, expanded, theme, context, isPartial);
|
|
1826
1696
|
},
|
|
1827
1697
|
});
|
|
1828
1698
|
|
|
@@ -1880,8 +1750,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1880
1750
|
renderCall(args, theme) {
|
|
1881
1751
|
return renderUnityToolCall("unity_open_editor", args, theme, "gui", "opens editor window");
|
|
1882
1752
|
},
|
|
1883
|
-
renderResult(result, { expanded }, theme) {
|
|
1884
|
-
return renderUnityToolResult(result, expanded, theme);
|
|
1753
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1754
|
+
return renderUnityToolResult(result, expanded, theme, context, isPartial);
|
|
1885
1755
|
},
|
|
1886
1756
|
});
|
|
1887
1757
|
|
|
@@ -1919,8 +1789,8 @@ export default function freeUnityPi(pi: ExtensionAPI) {
|
|
|
1919
1789
|
const displayArgs = args.useGraphics ? args.args : ["-nographics", ...(args.args ?? [])];
|
|
1920
1790
|
return renderUnityToolCall("unity_launch_batchmode", args, theme, "batchmode", getBatchmodeVariantLabel(displayArgs));
|
|
1921
1791
|
},
|
|
1922
|
-
renderResult(result, { expanded }, theme) {
|
|
1923
|
-
return renderUnityToolResult(result, expanded, theme);
|
|
1792
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1793
|
+
return renderUnityToolResult(result, expanded, theme, context, isPartial);
|
|
1924
1794
|
},
|
|
1925
1795
|
});
|
|
1926
1796
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aefree/pi-unity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./index.ts"
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
]
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
21
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
22
|
+
"pretest": "npm run typecheck",
|
|
23
|
+
"test": "tsx tests/unity-core.test.ts && tsx tests/unity-launch.test.ts && tsx tests/unity-pipeline.test.ts && tsx tests/unity-processes.test.ts && tsx tests/pi-unity-settings.test.ts && tsx tests/unity-projects.test.ts && tsx tests/unity-guidance-audit.test.ts && tsx tests/unity-batchmode.test.ts && tsx tests/unity-test-batch.test.ts && tsx tests/unity-tests.test.ts && tsx tests/unity-cli.test.ts && tsx tests/unity-project-lock.test.ts && tsx tests/unity-artifact-profile.test.ts && tsx tests/unity-file-discovery-filter.test.ts && tsx tests/unity-registration.test.ts && tsx tests/unity-renderers.test.ts && tsx tests/unity-optional-integrations.test.ts && tsx tests/release-registry-preflight.test.ts && tsx tests/unity-package-validation.test.ts",
|
|
24
|
+
"preview:tools": "tsx tests/unity-renderers.test.ts --preview",
|
|
22
25
|
"eval:guidance-skill": "tsx evals/auditing-unity-agent-guidance/run-eval.ts"
|
|
23
26
|
},
|
|
24
27
|
"dependencies": {
|
|
@@ -53,7 +56,9 @@
|
|
|
53
56
|
"@earendil-works/pi-ai": "0.85.1",
|
|
54
57
|
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
55
58
|
"@earendil-works/pi-tui": "0.85.1",
|
|
56
|
-
"
|
|
59
|
+
"@types/node": "22.20.2",
|
|
60
|
+
"tsx": "^4.23.5",
|
|
61
|
+
"typescript": "7.0.2"
|
|
57
62
|
},
|
|
58
63
|
"peerDependenciesMeta": {
|
|
59
64
|
"@earendil-works/pi-ai": {
|