unity-open-mcp 0.9.0 → 1.1.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/dist/asmdef-discovery.d.ts +35 -0
- package/dist/asmdef-discovery.js +171 -0
- package/dist/asmdef-discovery.js.map +1 -0
- package/dist/batch-spawn.d.ts +27 -0
- package/dist/batch-spawn.js +218 -24
- package/dist/batch-spawn.js.map +1 -1
- package/dist/capabilities/build-capabilities.d.ts +10 -0
- package/dist/capabilities/build-capabilities.js +4 -0
- package/dist/capabilities/build-capabilities.js.map +1 -1
- package/dist/capabilities/capabilities-view.d.ts +48 -0
- package/dist/capabilities/capabilities-view.js +93 -0
- package/dist/capabilities/capabilities-view.js.map +1 -0
- package/dist/capabilities/intent-groups.js +42 -0
- package/dist/capabilities/intent-groups.js.map +1 -1
- package/dist/capabilities/rule-catalog.js +7 -1
- package/dist/capabilities/rule-catalog.js.map +1 -1
- package/dist/capabilities/tool-groups.js +37 -2
- package/dist/capabilities/tool-groups.js.map +1 -1
- package/dist/child-supervision.d.ts +34 -0
- package/dist/child-supervision.js +110 -0
- package/dist/child-supervision.js.map +1 -0
- package/dist/cli/cli.js +9 -0
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/commands.js +37 -4
- package/dist/cli/commands.js.map +1 -1
- package/dist/compressible-router.js +12 -1
- package/dist/compressible-router.js.map +1 -1
- package/dist/constants.d.ts +31 -1
- package/dist/constants.js +31 -1
- package/dist/constants.js.map +1 -1
- package/dist/dialog-dismiss.d.ts +11 -0
- package/dist/dialog-dismiss.js +59 -4
- package/dist/dialog-dismiss.js.map +1 -1
- package/dist/editor-wedge.d.ts +45 -0
- package/dist/editor-wedge.js +111 -0
- package/dist/editor-wedge.js.map +1 -0
- package/dist/event-stream.d.ts +19 -6
- package/dist/event-stream.js +139 -17
- package/dist/event-stream.js.map +1 -1
- package/dist/gate-error.d.ts +2 -0
- package/dist/gate-error.js +9 -0
- package/dist/gate-error.js.map +1 -1
- package/dist/instance-discovery.d.ts +23 -0
- package/dist/instance-discovery.js +47 -1
- package/dist/instance-discovery.js.map +1 -1
- package/dist/live-client.d.ts +40 -5
- package/dist/live-client.js +254 -36
- package/dist/live-client.js.map +1 -1
- package/dist/offline/api.js +82 -30
- package/dist/offline/api.js.map +1 -1
- package/dist/offline/index-builders.d.ts +9 -0
- package/dist/offline/index-builders.js +58 -8
- package/dist/offline/index-builders.js.map +1 -1
- package/dist/offline/parse.js +5 -1
- package/dist/offline/parse.js.map +1 -1
- package/dist/process-diagnostics.d.ts +101 -19
- package/dist/process-diagnostics.js +197 -59
- package/dist/process-diagnostics.js.map +1 -1
- package/dist/project-settings.d.ts +36 -0
- package/dist/project-settings.js +96 -0
- package/dist/project-settings.js.map +1 -0
- package/dist/results.d.ts +8 -0
- package/dist/results.js +4 -1
- package/dist/results.js.map +1 -1
- package/dist/routers.js +4 -1
- package/dist/routers.js.map +1 -1
- package/dist/running-unity.d.ts +23 -0
- package/dist/running-unity.js +60 -0
- package/dist/running-unity.js.map +1 -1
- package/dist/server.js +8 -2
- package/dist/server.js.map +1 -1
- package/dist/stale-assembly-cache.d.ts +40 -0
- package/dist/stale-assembly-cache.js +76 -0
- package/dist/stale-assembly-cache.js.map +1 -0
- package/dist/tool-hint.d.ts +36 -0
- package/dist/tool-hint.js +98 -0
- package/dist/tool-hint.js.map +1 -0
- package/dist/tool-router.d.ts +47 -2
- package/dist/tool-router.js +652 -71
- package/dist/tool-router.js.map +1 -1
- package/dist/tools/agent-capabilities.js +27 -1
- package/dist/tools/agent-capabilities.js.map +1 -1
- package/dist/tools/baseline-create.js +1 -1
- package/dist/tools/baseline-create.js.map +1 -1
- package/dist/tools/batch-execute.js +12 -1
- package/dist/tools/batch-execute.js.map +1 -1
- package/dist/tools/bridge-status.js +21 -1
- package/dist/tools/bridge-status.js.map +1 -1
- package/dist/tools/capture-inline.js +4 -3
- package/dist/tools/capture-inline.js.map +1 -1
- package/dist/tools/editor-status.js +7 -2
- package/dist/tools/editor-status.js.map +1 -1
- package/dist/tools/execute-csharp.js +36 -3
- package/dist/tools/execute-csharp.js.map +1 -1
- package/dist/tools/execute-menu.js +33 -2
- package/dist/tools/execute-menu.js.map +1 -1
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.js +44 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inputsim-key.d.ts +40 -0
- package/dist/tools/inputsim-key.js +72 -0
- package/dist/tools/inputsim-key.js.map +1 -0
- package/dist/tools/inputsim-pointer.d.ts +40 -0
- package/dist/tools/inputsim-pointer.js +121 -0
- package/dist/tools/inputsim-pointer.js.map +1 -0
- package/dist/tools/inputsim-pointer3d.d.ts +40 -0
- package/dist/tools/inputsim-pointer3d.js +55 -0
- package/dist/tools/inputsim-pointer3d.js.map +1 -0
- package/dist/tools/inputsim-probe.d.ts +40 -0
- package/dist/tools/inputsim-probe.js +43 -0
- package/dist/tools/inputsim-probe.js.map +1 -0
- package/dist/tools/inputsim-step.d.ts +40 -0
- package/dist/tools/inputsim-step.js +50 -0
- package/dist/tools/inputsim-step.js.map +1 -0
- package/dist/tools/inputsim-touch.d.ts +40 -0
- package/dist/tools/inputsim-touch.js +83 -0
- package/dist/tools/inputsim-touch.js.map +1 -0
- package/dist/tools/invoke-method.js +15 -1
- package/dist/tools/invoke-method.js.map +1 -1
- package/dist/tools/memory-snapshot-capture.js +11 -5
- package/dist/tools/memory-snapshot-capture.js.map +1 -1
- package/dist/tools/pull-events.js +18 -10
- package/dist/tools/pull-events.js.map +1 -1
- package/dist/tools/read-compile-errors.js +36 -7
- package/dist/tools/read-compile-errors.js.map +1 -1
- package/dist/tools/regression-check.js +2 -1
- package/dist/tools/regression-check.js.map +1 -1
- package/dist/tools/resource-pressure.js +21 -10
- package/dist/tools/resource-pressure.js.map +1 -1
- package/dist/tools/run-tests.js +11 -2
- package/dist/tools/run-tests.js.map +1 -1
- package/dist/tools/scan-all.js +2 -2
- package/dist/tools/scan-all.js.map +1 -1
- package/dist/tools/screenshot-window.js +18 -12
- package/dist/tools/screenshot-window.js.map +1 -1
- package/dist/tools/screenshot.js +13 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/visual-compare.d.ts +40 -0
- package/dist/tools/visual-compare.js +98 -0
- package/dist/tools/visual-compare.js.map +1 -0
- package/dist/unity-log.d.ts +67 -2
- package/dist/unity-log.js +206 -8
- package/dist/unity-log.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface AsmdefCount {
|
|
2
|
+
/** Number of distinct compile assemblies in the project's own source. */
|
|
3
|
+
count: number;
|
|
4
|
+
/** A few sample asmdef paths (project-relative) for context/debugging. */
|
|
5
|
+
sample: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Find the nearest `.asmdef` at or above `filePath`'s directory. Returns the
|
|
9
|
+
* asmdef's absolute path, or null when none is found before the filesystem
|
|
10
|
+
* root / hop limit. Maps any source file (absolute or relative) to its owning
|
|
11
|
+
* assembly so the caller can count how many distinct assemblies a set of errors
|
|
12
|
+
* spans.
|
|
13
|
+
*
|
|
14
|
+
* `projectRoot` anchors RELATIVE file paths (compiler errors carry project-
|
|
15
|
+
* relative paths like `Assets/Foo.cs`). Without it a relative path resolves
|
|
16
|
+
* against `process.cwd()`, which is rarely the Unity project and silently
|
|
17
|
+
* yields null for every file. Absolute paths ignore `projectRoot`.
|
|
18
|
+
*/
|
|
19
|
+
export declare function nearestAsmdef(filePath: string, projectRoot?: string | null): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Count the project's own compile assemblies: `Assets/` + `Packages/` + any
|
|
22
|
+
* `file:`-referenced local packages from `Packages/manifest.json`. Registry
|
|
23
|
+
* packages (Library/PackageCache) are precompiled and excluded. Returns the
|
|
24
|
+
* distinct count and a small sample of project-relative paths. Memoized per
|
|
25
|
+
* project path.
|
|
26
|
+
*/
|
|
27
|
+
export declare function countProjectAsmdefs(projectPath: string | null | undefined): AsmdefCount;
|
|
28
|
+
/**
|
|
29
|
+
* Distinct assemblies spanned by a set of compiler-error file paths. Returns
|
|
30
|
+
* the count of unique owning asmdefs (via {@link nearestAsmdef}); errors whose
|
|
31
|
+
* file does not map to an asmdef are dropped from the count. Pass the
|
|
32
|
+
* `projectRoot` so relative error-file paths (the usual `Assets/Foo.cs` form)
|
|
33
|
+
* resolve against the Unity project rather than `process.cwd()`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function countAssembliesWithErrors(files: string[], projectRoot?: string | null): number;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// Project assembly-definition discovery for read_compile_errors' partial-compile
|
|
2
|
+
// signal (specs/feedback.md 2026-08-12). Unity compiles assemblies in dependency
|
|
3
|
+
// order; when an early assembly fails the pipeline may not reach dependent ones,
|
|
4
|
+
// so the CSxxxx errors in Editor.log can be a SUBSET of the true error set. This
|
|
5
|
+
// module counts the project's own compile assemblies and maps an error's source
|
|
6
|
+
// file to its owning assembly, so read_compile_errors can surface
|
|
7
|
+
// `partialCompileLikely` + `assembliesWithErrors`/`asmdefCount` instead of letting
|
|
8
|
+
// an agent treat `errorCount: N` as the complete picture.
|
|
9
|
+
//
|
|
10
|
+
// Scope: Assets/ + Packages/ + `file:`-referenced local packages from
|
|
11
|
+
// manifest.json. Registry packages (Library/PackageCache) are precompiled and
|
|
12
|
+
// excluded — they rarely carry compile errors and would inflate the count.
|
|
13
|
+
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
14
|
+
import { join, dirname, resolve, relative, isAbsolute } from "node:path";
|
|
15
|
+
const ASMDEF_RE = /\.asmdef$/i;
|
|
16
|
+
// Directories never descended into when walking for asmdefs (build output,
|
|
17
|
+
// caches, VCS, editor state).
|
|
18
|
+
const SKIP_DIRS = new Set([
|
|
19
|
+
"Library",
|
|
20
|
+
"Temp",
|
|
21
|
+
"obj",
|
|
22
|
+
"node_modules",
|
|
23
|
+
".git",
|
|
24
|
+
"Logs",
|
|
25
|
+
"UserSettings",
|
|
26
|
+
"Build",
|
|
27
|
+
"Builds",
|
|
28
|
+
]);
|
|
29
|
+
/** Maximum total asmdefs to collect (bounds the walk on huge projects). */
|
|
30
|
+
const MAX_ASMDEFS = 500;
|
|
31
|
+
/** Maximum parent-directory hops when locating an error file's owning asmdef. */
|
|
32
|
+
const MAX_UP_WALK = 32;
|
|
33
|
+
/**
|
|
34
|
+
* Find the nearest `.asmdef` at or above `filePath`'s directory. Returns the
|
|
35
|
+
* asmdef's absolute path, or null when none is found before the filesystem
|
|
36
|
+
* root / hop limit. Maps any source file (absolute or relative) to its owning
|
|
37
|
+
* assembly so the caller can count how many distinct assemblies a set of errors
|
|
38
|
+
* spans.
|
|
39
|
+
*
|
|
40
|
+
* `projectRoot` anchors RELATIVE file paths (compiler errors carry project-
|
|
41
|
+
* relative paths like `Assets/Foo.cs`). Without it a relative path resolves
|
|
42
|
+
* against `process.cwd()`, which is rarely the Unity project and silently
|
|
43
|
+
* yields null for every file. Absolute paths ignore `projectRoot`.
|
|
44
|
+
*/
|
|
45
|
+
export function nearestAsmdef(filePath, projectRoot) {
|
|
46
|
+
if (!filePath)
|
|
47
|
+
return null;
|
|
48
|
+
const anchored = projectRoot && !isAbsolute(filePath) ? resolve(projectRoot, filePath) : filePath;
|
|
49
|
+
let dir = dirname(resolve(anchored));
|
|
50
|
+
for (let guard = 0; guard < MAX_UP_WALK; guard++) {
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = readdirSync(dir);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null; // unreadable dir — can't walk further reliably
|
|
57
|
+
}
|
|
58
|
+
const asmdef = entries.find((e) => ASMDEF_RE.test(e));
|
|
59
|
+
if (asmdef)
|
|
60
|
+
return join(dir, asmdef);
|
|
61
|
+
const parent = dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
break; // filesystem root
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Walk a directory tree collecting `.asmdef` paths, skipping build/cache dirs. */
|
|
69
|
+
function walkAsmdefs(root, out) {
|
|
70
|
+
const stack = [root];
|
|
71
|
+
while (stack.length && out.size < MAX_ASMDEFS) {
|
|
72
|
+
const dir = stack.pop();
|
|
73
|
+
let entries;
|
|
74
|
+
try {
|
|
75
|
+
entries = readdirSync(dir);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
for (const name of entries) {
|
|
81
|
+
if (out.size >= MAX_ASMDEFS)
|
|
82
|
+
break;
|
|
83
|
+
const full = join(dir, name);
|
|
84
|
+
let st;
|
|
85
|
+
try {
|
|
86
|
+
st = statSync(full);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (st.isDirectory()) {
|
|
92
|
+
if (!SKIP_DIRS.has(name))
|
|
93
|
+
stack.push(full);
|
|
94
|
+
}
|
|
95
|
+
else if (ASMDEF_RE.test(name)) {
|
|
96
|
+
out.add(full);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Per-project memo. An asmdef layout changes rarely and the walk stats the
|
|
102
|
+
// whole Assets/ tree synchronously, so cache the result for the session.
|
|
103
|
+
const asmdefCountCache = new Map();
|
|
104
|
+
/**
|
|
105
|
+
* Count the project's own compile assemblies: `Assets/` + `Packages/` + any
|
|
106
|
+
* `file:`-referenced local packages from `Packages/manifest.json`. Registry
|
|
107
|
+
* packages (Library/PackageCache) are precompiled and excluded. Returns the
|
|
108
|
+
* distinct count and a small sample of project-relative paths. Memoized per
|
|
109
|
+
* project path.
|
|
110
|
+
*/
|
|
111
|
+
export function countProjectAsmdefs(projectPath) {
|
|
112
|
+
if (!projectPath)
|
|
113
|
+
return { count: 0, sample: [] };
|
|
114
|
+
const cached = asmdefCountCache.get(projectPath);
|
|
115
|
+
if (cached)
|
|
116
|
+
return cached;
|
|
117
|
+
const found = new Set();
|
|
118
|
+
const assetsDir = join(projectPath, "Assets");
|
|
119
|
+
const packagesDir = join(projectPath, "Packages");
|
|
120
|
+
if (existsSync(assetsDir))
|
|
121
|
+
walkAsmdefs(assetsDir, found);
|
|
122
|
+
if (existsSync(packagesDir))
|
|
123
|
+
walkAsmdefs(packagesDir, found);
|
|
124
|
+
// file: packages referenced in manifest.json may live outside the project
|
|
125
|
+
// (e.g. "file:../../packages/bridge"). Unity resolves these relative to the
|
|
126
|
+
// Packages/ directory.
|
|
127
|
+
const manifestPath = join(packagesDir, "manifest.json");
|
|
128
|
+
if (existsSync(manifestPath)) {
|
|
129
|
+
try {
|
|
130
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
131
|
+
const deps = manifest.dependencies ?? {};
|
|
132
|
+
for (const v of Object.values(deps)) {
|
|
133
|
+
if (typeof v === "string" && v.startsWith("file:")) {
|
|
134
|
+
const pkgPath = resolve(packagesDir, v.slice("file:".length));
|
|
135
|
+
if (existsSync(pkgPath))
|
|
136
|
+
walkAsmdefs(pkgPath, found);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// malformed/unreadable manifest — skip; Assets/ + Packages/ still counted.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const list = [...found];
|
|
145
|
+
const result = {
|
|
146
|
+
count: list.length,
|
|
147
|
+
sample: list.slice(0, 8).map((p) => {
|
|
148
|
+
const rel = relative(projectPath, p);
|
|
149
|
+
return rel || p;
|
|
150
|
+
}),
|
|
151
|
+
};
|
|
152
|
+
asmdefCountCache.set(projectPath, result);
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Distinct assemblies spanned by a set of compiler-error file paths. Returns
|
|
157
|
+
* the count of unique owning asmdefs (via {@link nearestAsmdef}); errors whose
|
|
158
|
+
* file does not map to an asmdef are dropped from the count. Pass the
|
|
159
|
+
* `projectRoot` so relative error-file paths (the usual `Assets/Foo.cs` form)
|
|
160
|
+
* resolve against the Unity project rather than `process.cwd()`.
|
|
161
|
+
*/
|
|
162
|
+
export function countAssembliesWithErrors(files, projectRoot) {
|
|
163
|
+
const set = new Set();
|
|
164
|
+
for (const f of files) {
|
|
165
|
+
const a = nearestAsmdef(f, projectRoot);
|
|
166
|
+
if (a)
|
|
167
|
+
set.add(a);
|
|
168
|
+
}
|
|
169
|
+
return set.size;
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=asmdef-discovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asmdef-discovery.js","sourceRoot":"","sources":["../src/asmdef-discovery.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,iFAAiF;AACjF,iFAAiF;AACjF,iFAAiF;AACjF,gFAAgF;AAChF,kEAAkE;AAClE,mFAAmF;AACnF,0DAA0D;AAC1D,EAAE;AACF,sEAAsE;AACtE,8EAA8E;AAC9E,2EAA2E;AAC3E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEzE,MAAM,SAAS,GAAG,YAAY,CAAC;AAE/B,2EAA2E;AAC3E,8BAA8B;AAC9B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,SAAS;IACT,MAAM;IACP,KAAK;IACJ,cAAc;IACd,MAAM;IACN,MAAM;IACN,cAAc;IACd,OAAO;IACP,QAAQ;CACT,CAAC,CAAC;AAEH,2EAA2E;AAC3E,MAAM,WAAW,GAAG,GAAG,CAAC;AAExB,iFAAiF;AACjF,MAAM,WAAW,GAAG,EAAE,CAAC;AASvB;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,WAA2B;IAE3B,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,MAAM,QAAQ,GAAG,WAAW,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAClG,IAAI,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,CAAC,+CAA+C;QAC9D,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACtD,IAAI,MAAM;YAAE,OAAO,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QACrC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM,CAAC,kBAAkB;QAC7C,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,mFAAmF;AACnF,SAAS,WAAW,CAAC,IAAY,EAAE,GAAgB;IACjD,MAAM,KAAK,GAAa,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,GAAG,WAAW,EAAE,CAAC;QAC9C,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QACzB,IAAI,OAAiB,CAAC;QACtB,IAAI,CAAC;YACH,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,IAAI,GAAG,CAAC,IAAI,IAAI,WAAW;gBAAE,MAAM;YACnC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAC7B,IAAI,EAA+B,CAAC;YACpC,IAAI,CAAC;gBACH,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;YACtB,CAAC;YAAC,MAAM,CAAC;gBACP,SAAS;YACX,CAAC;YACD,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;gBACrB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7C,CAAC;iBAAM,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAChB,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,yEAAyE;AACzE,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAuB,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAsC;IACxE,IAAI,CAAC,WAAW;QAAE,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAClD,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACjD,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAEhC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;IAC9C,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;IAClD,IAAI,UAAU,CAAC,SAAS,CAAC;QAAE,WAAW,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;IACzD,IAAI,UAAU,CAAC,WAAW,CAAC;QAAE,WAAW,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAE7D,0EAA0E;IAC1E,4EAA4E;IAC5E,uBAAuB;IACvB,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,CAAC;IACxD,IAAI,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAE7D,CAAC;YACF,MAAM,IAAI,GAAG,QAAQ,CAAC,YAAY,IAAI,EAAE,CAAC;YACzC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;oBACnD,MAAM,OAAO,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;oBAC9D,IAAI,UAAU,CAAC,OAAO,CAAC;wBAAE,WAAW,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;gBACvD,CAAC;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,2EAA2E;QAC7E,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IACxB,MAAM,MAAM,GAAgB;QAC1B,KAAK,EAAE,IAAI,CAAC,MAAM;QAClB,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACjC,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;YACrC,OAAO,GAAG,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;KACH,CAAC;IACF,gBAAgB,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IAC1C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,yBAAyB,CACvC,KAAe,EACf,WAA2B;IAE3B,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,CAAC,GAAG,aAAa,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;QACxC,IAAI,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC;AAClB,CAAC"}
|
package/dist/batch-spawn.d.ts
CHANGED
|
@@ -21,6 +21,33 @@ export declare function buildVerifyArgs(operation: string, args: Record<string,
|
|
|
21
21
|
export declare function buildMetaArgs(operation: string, args: Record<string, unknown>): string[];
|
|
22
22
|
export declare function encodeSpaces(value: string): string;
|
|
23
23
|
export declare function classifyBatchFailure(combined: string): string | null;
|
|
24
|
+
export interface EditorLockDiagnosis {
|
|
25
|
+
variant: "listener_up" | "listener_down" | "no_editor_found";
|
|
26
|
+
/** Short human-readable observation, e.g. "instance lock fresh (pid 123)". */
|
|
27
|
+
detail: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Observe the Editor state behind a project-lock conflict. Pure reads (lock
|
|
31
|
+
* file + PID liveness; process scan only when the lock is unusable) — never
|
|
32
|
+
* throws, never spawns Unity. `listener_up` vs `listener_down` distinguishes
|
|
33
|
+
* "the live bridge should answer" from "the Editor holds the lock but its
|
|
34
|
+
* listener is not up yet".
|
|
35
|
+
*/
|
|
36
|
+
export declare function diagnoseEditorLock(projectPath: string): EditorLockDiagnosis;
|
|
37
|
+
/**
|
|
38
|
+
* Message for the `editor_instance_locked` error, naming the tool that was
|
|
39
|
+
* actually called (feedback 2026-08-17: the canned text said "the headless
|
|
40
|
+
* compile_check spawn" even for execute_menu).
|
|
41
|
+
*/
|
|
42
|
+
export declare function editorLockedMessage(toolName: string, diagnosis: EditorLockDiagnosis): string;
|
|
43
|
+
/**
|
|
44
|
+
* Structured recovery hints for {@link editorLockedMessage}, one per variant.
|
|
45
|
+
* compile_check additionally keeps the read_compile_errors alternative (an
|
|
46
|
+
* open Editor makes a headless compile impossible, but the log is readable
|
|
47
|
+
* offline).
|
|
48
|
+
*/
|
|
49
|
+
export declare function editorLockedNextSteps(toolName: string, diagnosis: EditorLockDiagnosis): string[];
|
|
50
|
+
export declare function extractOffendingPackages(combined: string): string[];
|
|
24
51
|
export declare function buildUnityBatchArgs(operation: string, projectPath: string, executeMethod: string, toolArgs: string[]): string[];
|
|
25
52
|
export declare class BatchClassificationError extends Error {
|
|
26
53
|
readonly code: string;
|
package/dist/batch-spawn.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { StringDecoder } from "node:string_decoder";
|
|
3
3
|
import { stat } from "node:fs/promises";
|
|
4
|
+
import { trackBatchChild } from "./child-supervision.js";
|
|
4
5
|
import { resolveUnityPath, scannedHubRoots } from "./unity-install-discovery.js";
|
|
5
|
-
import { readInstanceLock, isPidAlive } from "./instance-discovery.js";
|
|
6
|
+
import { readInstanceLock, isPidAlive, classifyInstance, } from "./instance-discovery.js";
|
|
7
|
+
import { findUnityForProject } from "./running-unity.js";
|
|
6
8
|
import { makeErrorResult } from "./results.js";
|
|
7
9
|
import { VERIFY_JSON_BEGIN, VERIFY_JSON_END } from "./constants.js";
|
|
8
10
|
const VERIFY_EXECUTE_METHOD = "UnityOpenMcpVerify.Batch.VerifyBatchEntry.Run";
|
|
@@ -254,16 +256,156 @@ export function encodeSpaces(value) {
|
|
|
254
256
|
// situation apart from a genuine compile/spawn failure and act on it (close
|
|
255
257
|
// the live Editor, or use live introspection instead of a headless spawn).
|
|
256
258
|
//
|
|
259
|
+
// Package-resolution failures get the same treatment: Unity bails at package
|
|
260
|
+
// resolution BEFORE writing the VERIFY_JSON_BEGIN/END markers, so the generic
|
|
261
|
+
// `batch_spawn_failed` / "did not contain JSON markers" error masks the real
|
|
262
|
+
// root cause (an unresolvable Packages/manifest.json dependency). Surfacing
|
|
263
|
+
// `project_load_failed` with the offending package stops an agent from
|
|
264
|
+
// "fixing" a compile that never ran. See specs/feedback.md (compile_check
|
|
265
|
+
// batch_spawn_failed).
|
|
266
|
+
//
|
|
257
267
|
// Returns the error code to emit, or null when the tail does not match a
|
|
258
268
|
// known classification (caller falls back to batch_spawn_failed).
|
|
259
269
|
const PROJECT_LOCK_PATTERN = /another unity instance|already open/i;
|
|
270
|
+
// Unity prints these when the project fails to load at the package-resolution
|
|
271
|
+
// stage — before the batch entry point ever runs, so no markers are emitted:
|
|
272
|
+
// - "Project has invalid dependencies" (Package Manager resolution failure)
|
|
273
|
+
// - "Package Manager Server process was shutdown" (backend process died)
|
|
274
|
+
const PROJECT_LOAD_FAILED_PATTERN = /Project has invalid dependencies|Package Manager Server process was (?:shutdown|terminated)/i;
|
|
260
275
|
export function classifyBatchFailure(combined) {
|
|
261
276
|
if (typeof combined !== "string" || combined.length === 0)
|
|
262
277
|
return null;
|
|
263
278
|
if (PROJECT_LOCK_PATTERN.test(combined))
|
|
264
279
|
return "editor_instance_locked";
|
|
280
|
+
if (PROJECT_LOAD_FAILED_PATTERN.test(combined))
|
|
281
|
+
return "project_load_failed";
|
|
265
282
|
return null;
|
|
266
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Observe the Editor state behind a project-lock conflict. Pure reads (lock
|
|
286
|
+
* file + PID liveness; process scan only when the lock is unusable) — never
|
|
287
|
+
* throws, never spawns Unity. `listener_up` vs `listener_down` distinguishes
|
|
288
|
+
* "the live bridge should answer" from "the Editor holds the lock but its
|
|
289
|
+
* listener is not up yet".
|
|
290
|
+
*/
|
|
291
|
+
export function diagnoseEditorLock(projectPath) {
|
|
292
|
+
const lock = projectPath
|
|
293
|
+
? readInstanceLock(projectPath)
|
|
294
|
+
: null;
|
|
295
|
+
if (lock && isPidAlive(lock.pid)) {
|
|
296
|
+
const classification = classifyInstance(lock);
|
|
297
|
+
if (classification === "dead_bridge") {
|
|
298
|
+
return {
|
|
299
|
+
variant: "listener_down",
|
|
300
|
+
detail: `instance lock stale (pid ${lock.pid} alive, heartbeat not advancing — booting, Safe Mode, or failed bridge recompile)`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
variant: "listener_up",
|
|
305
|
+
detail: `instance lock fresh (pid ${lock.pid}, state ${lock.state})`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
// No usable lock. A live Unity process for this project without a lock is
|
|
309
|
+
// the booting / Safe-Mode shape (the bridge writes its lock only after the
|
|
310
|
+
// listener binds — cold Safe Mode never writes one at all).
|
|
311
|
+
const process = findUnityForProject(projectPath);
|
|
312
|
+
if (process) {
|
|
313
|
+
return {
|
|
314
|
+
variant: "listener_down",
|
|
315
|
+
detail: `Unity process ${process.pid} matches the project but no live bridge lock exists (booting before the bridge starts, or Safe Mode)`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
variant: "no_editor_found",
|
|
320
|
+
detail: "no live Unity process for this project found",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Message for the `editor_instance_locked` error, naming the tool that was
|
|
325
|
+
* actually called (feedback 2026-08-17: the canned text said "the headless
|
|
326
|
+
* compile_check spawn" even for execute_menu).
|
|
327
|
+
*/
|
|
328
|
+
export function editorLockedMessage(toolName, diagnosis) {
|
|
329
|
+
switch (diagnosis.variant) {
|
|
330
|
+
case "listener_up":
|
|
331
|
+
return (`A live Unity Editor holds the project lock, so the headless ${toolName} ` +
|
|
332
|
+
"spawn cannot open the project (Unity allows one Editor per project). " +
|
|
333
|
+
`(${diagnosis.detail}.) The Editor's bridge lock is fresh, so the live ` +
|
|
334
|
+
"bridge should be reachable — retry via the live route, or call " +
|
|
335
|
+
"unity_open_mcp_bridge_status to see why it is not answering.");
|
|
336
|
+
case "listener_down":
|
|
337
|
+
return (`A Unity Editor process holds the project lock but its bridge listener ` +
|
|
338
|
+
`is not up yet, so the headless ${toolName} spawn could not open the ` +
|
|
339
|
+
"project and the live route was unavailable too. " +
|
|
340
|
+
`(${diagnosis.detail}.) This is usually a booting Editor — wait for it ` +
|
|
341
|
+
"to finish starting, then retry; the call takes the live route once " +
|
|
342
|
+
"the listener is up.");
|
|
343
|
+
default:
|
|
344
|
+
return (`The headless ${toolName} spawn was refused with Unity's ` +
|
|
345
|
+
"one-Editor-per-project lock signature, but no live Unity process for " +
|
|
346
|
+
`this project was found (${diagnosis.detail}). The on-disk ` +
|
|
347
|
+
"Temp/UnityLockfile is likely stale from a crashed session.");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Structured recovery hints for {@link editorLockedMessage}, one per variant.
|
|
352
|
+
* compile_check additionally keeps the read_compile_errors alternative (an
|
|
353
|
+
* open Editor makes a headless compile impossible, but the log is readable
|
|
354
|
+
* offline).
|
|
355
|
+
*/
|
|
356
|
+
export function editorLockedNextSteps(toolName, diagnosis) {
|
|
357
|
+
switch (diagnosis.variant) {
|
|
358
|
+
case "listener_up":
|
|
359
|
+
return [
|
|
360
|
+
`A live Unity Editor holds the project lock, so the headless ${toolName} spawn cannot open the project (one Editor per project).`,
|
|
361
|
+
`The instance lock is fresh — the live bridge should be reachable. Call unity_open_mcp_ping or unity_open_mcp_bridge_status, then retry ${toolName} on the live route.`,
|
|
362
|
+
...(toolName === "unity_open_mcp_compile_check"
|
|
363
|
+
? [
|
|
364
|
+
"To verify compile state without closing the Editor, call unity_open_mcp_read_compile_errors (reads Editor.log offline; if its logSource is a prev_log_* value, the log was rotated — prefer the live bridge's compile signal).",
|
|
365
|
+
]
|
|
366
|
+
: []),
|
|
367
|
+
`To run a true headless ${toolName}, close the live Editor first.`,
|
|
368
|
+
];
|
|
369
|
+
case "listener_down":
|
|
370
|
+
return [
|
|
371
|
+
`A Unity Editor process holds the project but its bridge listener is not up yet (${diagnosis.detail}) — that is why the live route was unavailable and the headless ${toolName} spawn hit the project lock.`,
|
|
372
|
+
"Wait for the Editor to finish booting (typically well under a minute) and retry — the call takes the live route once the listener is up.",
|
|
373
|
+
"If it does not come up, call unity_open_mcp_bridge_status and unity_open_mcp_read_compile_errors: a failed bridge recompile shows there, and the remedy is to fix the C# errors, not to retry the spawn.",
|
|
374
|
+
];
|
|
375
|
+
default:
|
|
376
|
+
return [
|
|
377
|
+
`The ${toolName} spawn was refused with Unity's project-lock signature, but no live Unity process for this project was found — Temp/UnityLockfile is likely stale from a crashed session.`,
|
|
378
|
+
"Verify no Unity instance holds the project (running processes), remove the stale <project>/Temp/UnityLockfile if none does, and retry.",
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Capture the offending package id(s) from a Package Manager resolution
|
|
383
|
+
// failure tail. Unity prints reverse-DNS package identifiers on lines like:
|
|
384
|
+
// [Package Manager] com.unity.modules.physicscore2d is not a valid package
|
|
385
|
+
// Missing or invalid dependencies: com.unity.modules.physicscore2d
|
|
386
|
+
// We match com.*/org.* identifiers with at least three dot-separated labels
|
|
387
|
+
// (so "com.unity" alone does not match), deduped and capped — best-effort,
|
|
388
|
+
// since the exact phrasing varies across Unity versions. Returns the raw
|
|
389
|
+
// matches in first-seen order.
|
|
390
|
+
const PACKAGE_ID_PATTERN = /\b((?:com|org)\.[a-z0-9][a-z0-9_-]*(?:\.[a-z0-9][a-z0-9_-]*)+)\b/gi;
|
|
391
|
+
export function extractOffendingPackages(combined) {
|
|
392
|
+
if (typeof combined !== "string" || combined.length === 0)
|
|
393
|
+
return [];
|
|
394
|
+
const seen = new Set();
|
|
395
|
+
const out = [];
|
|
396
|
+
PACKAGE_ID_PATTERN.lastIndex = 0;
|
|
397
|
+
let m;
|
|
398
|
+
while ((m = PACKAGE_ID_PATTERN.exec(combined)) !== null) {
|
|
399
|
+
const id = m[1];
|
|
400
|
+
if (!seen.has(id)) {
|
|
401
|
+
seen.add(id);
|
|
402
|
+
out.push(id);
|
|
403
|
+
if (out.length >= 8)
|
|
404
|
+
break; // cap; a wall of ids is not more useful
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
267
409
|
const UNITY_SPAWN_REFUSED_NEXT_STEPS = [
|
|
268
410
|
"The Unity binary could not be executed (spawn refused or exit code 127). Do not retry compile_check blindly.",
|
|
269
411
|
"Verify UNITY_PATH points to a valid Unity Editor executable, then call unity_open_mcp_read_compile_errors to check compile state without headless spawn.",
|
|
@@ -380,22 +522,28 @@ export class BatchSpawn {
|
|
|
380
522
|
try {
|
|
381
523
|
const lock = readInstanceLock(this.projectPath);
|
|
382
524
|
if (lock && lock.pid && isPidAlive(lock.pid)) {
|
|
525
|
+
// feedback 2026-08-17 — parameterize by the invoked tool and split
|
|
526
|
+
// "live bridge should be up" from "Editor holds the lock but its
|
|
527
|
+
// listener is not (booting / Safe Mode / failed recompile)". The old
|
|
528
|
+
// canned text always described a compile_check spawn against a fully
|
|
529
|
+
// live Editor, which misdirected agents calling any other tool.
|
|
530
|
+
const diagnosis = diagnoseEditorLock(this.projectPath);
|
|
531
|
+
const message = "A live Unity Editor holds the project lock, so the headless " +
|
|
532
|
+
`spawn for '${toolName}' was not attempted (Unity allows one ` +
|
|
533
|
+
"Editor per project, and the failed spawn would rotate Editor.log " +
|
|
534
|
+
"and break read_compile_errors). " +
|
|
535
|
+
editorLockedMessage(toolName, diagnosis);
|
|
383
536
|
return makeErrorResult({
|
|
384
537
|
code: "editor_instance_locked",
|
|
385
|
-
message
|
|
386
|
-
"spawn was not attempted (Unity allows one Editor per project, " +
|
|
387
|
-
"and the failed spawn would rotate Editor.log and break " +
|
|
388
|
-
"read_compile_errors).",
|
|
538
|
+
message,
|
|
389
539
|
detail: {
|
|
390
540
|
error: {
|
|
391
541
|
code: "editor_instance_locked",
|
|
392
|
-
message: "A live Unity Editor (pid " + lock.pid + ") holds the project lock."
|
|
542
|
+
message: "A live Unity Editor (pid " + lock.pid + ") holds the project lock. " +
|
|
543
|
+
diagnosis.detail,
|
|
544
|
+
lockVariant: diagnosis.variant,
|
|
393
545
|
},
|
|
394
|
-
agentNextSteps:
|
|
395
|
-
"A live Unity Editor holds the project lock, so the headless spawn cannot open the project (one Editor per project).",
|
|
396
|
-
"To verify compile state without closing the Editor, call unity_open_mcp_read_compile_errors (reads Editor.log offline; if its logSource is a prev_log_* value, the log was rotated — prefer the live bridge's compile signal).",
|
|
397
|
-
"Or close the live Editor and retry the batch spawn.",
|
|
398
|
-
],
|
|
546
|
+
agentNextSteps: editorLockedNextSteps(toolName, diagnosis),
|
|
399
547
|
},
|
|
400
548
|
});
|
|
401
549
|
}
|
|
@@ -423,7 +571,7 @@ export class BatchSpawn {
|
|
|
423
571
|
const argBuilder = verifyOperation ? buildVerifyArgs : buildMetaArgs;
|
|
424
572
|
let parsed;
|
|
425
573
|
try {
|
|
426
|
-
parsed = await this.spawnUnity(operation, args, executeMethod, argBuilder);
|
|
574
|
+
parsed = await this.spawnUnity(toolName, operation, args, executeMethod, argBuilder);
|
|
427
575
|
}
|
|
428
576
|
catch (err) {
|
|
429
577
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -520,7 +668,7 @@ export class BatchSpawn {
|
|
|
520
668
|
}
|
|
521
669
|
return null;
|
|
522
670
|
}
|
|
523
|
-
spawnUnity(operation, args, executeMethod, argBuilder) {
|
|
671
|
+
spawnUnity(toolName, operation, args, executeMethod, argBuilder) {
|
|
524
672
|
return new Promise((resolve, reject) => {
|
|
525
673
|
const toolArgs = argBuilder(operation, args);
|
|
526
674
|
const unityArgs = buildUnityBatchArgs(operation, this.projectPath, executeMethod, toolArgs);
|
|
@@ -532,6 +680,11 @@ export class BatchSpawn {
|
|
|
532
680
|
const child = spawn(this.unityPath, unityArgs, {
|
|
533
681
|
stdio: ["ignore", "pipe", "pipe"],
|
|
534
682
|
});
|
|
683
|
+
// Register with the process-wide supervision registry so the
|
|
684
|
+
// SIGINT/SIGTERM/exit handlers installed at the entry points tear this
|
|
685
|
+
// child down if the server dies mid-run (otherwise the headless Unity
|
|
686
|
+
// is orphaned holding the project lock until its own 10-min timeout).
|
|
687
|
+
trackBatchChild(child);
|
|
535
688
|
// M13 — escalate SIGTERM → SIGKILL after a grace window so a wedged
|
|
536
689
|
// Unity actually dies instead of lingering until the parent is killed.
|
|
537
690
|
// The grace is short relative to the overall timeout but long enough for
|
|
@@ -603,19 +756,38 @@ export class BatchSpawn {
|
|
|
603
756
|
// recognizable signature when a live Editor already holds the
|
|
604
757
|
// project; emit editor_instance_locked so an agent can act (close
|
|
605
758
|
// the live Editor or use live introspection) instead of seeing an
|
|
606
|
-
// opaque batch_spawn_failed.
|
|
759
|
+
// opaque batch_spawn_failed. feedback 2026-08-17 — the message and
|
|
760
|
+
// agentNextSteps name the invoked tool (not a canned compile_check)
|
|
761
|
+
// and split "listener should be up" from "Editor booting / Safe
|
|
762
|
+
// Mode / no Editor at all".
|
|
607
763
|
const classified = classifyBatchFailure(combined);
|
|
608
764
|
if (classified === "editor_instance_locked") {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
765
|
+
const diagnosis = this.projectPath
|
|
766
|
+
? diagnoseEditorLock(this.projectPath)
|
|
767
|
+
: { variant: "no_editor_found", detail: "project path unknown" };
|
|
768
|
+
reject(new BatchClassificationError("editor_instance_locked", editorLockedMessage(toolName, diagnosis), editorLockedNextSteps(toolName, diagnosis)));
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (classified === "project_load_failed") {
|
|
772
|
+
// Unity bailed at package resolution BEFORE the batch entry point
|
|
773
|
+
// ran, so no markers were emitted — the generic "did not contain
|
|
774
|
+
// JSON markers" error would mask the real root cause (an
|
|
775
|
+
// unresolvable Packages/manifest.json dependency). Name the
|
|
776
|
+
// offending package(s) and point at read_compile_errors, which
|
|
777
|
+
// surfaces the Package Manager notice without a headless spawn.
|
|
778
|
+
const offending = extractOffendingPackages(combined);
|
|
779
|
+
const pkgPart = offending.length > 0
|
|
780
|
+
? ` Offending package(s): ${offending.join(", ")}.`
|
|
781
|
+
: "";
|
|
782
|
+
reject(new BatchClassificationError("project_load_failed", `Batch Unity exited at package/project resolution (exit ${exitCode}) ` +
|
|
783
|
+
`without running the batch entry point, so no compile took place. ` +
|
|
784
|
+
`The project failed to load — this is NOT a C# compile error.${pkgPart} ` +
|
|
785
|
+
`Check Packages/manifest.json for unresolvable dependencies and call ` +
|
|
786
|
+
`unity_open_mcp_read_compile_errors to see the Package Manager notice.`, [
|
|
787
|
+
`Batch Unity failed to load the project (exit ${exitCode}) before reaching the batch entry point — this is a package-resolution / project-load failure, not a compile failure.${pkgPart}`,
|
|
788
|
+
"Call unity_open_mcp_read_compile_errors to read the Package Manager notice offline (no headless spawn).",
|
|
789
|
+
"Inspect Packages/manifest.json and Packages/packages-lock.json for packages that cannot be resolved by the current Unity version.",
|
|
790
|
+
"Do not retry compile_check blindly — the same project-load failure will recur until the manifest is fixed.",
|
|
619
791
|
]));
|
|
620
792
|
return;
|
|
621
793
|
}
|
|
@@ -624,6 +796,28 @@ export class BatchSpawn {
|
|
|
624
796
|
`Path: '${this.unityPath}'.`, UNITY_SPAWN_REFUSED_NEXT_STEPS));
|
|
625
797
|
return;
|
|
626
798
|
}
|
|
799
|
+
// Final generic fallback. Two cases reach here with no markers:
|
|
800
|
+
// - exit 0: Unity exited cleanly (likely a healthy compile) but
|
|
801
|
+
// emitted no markers — the async finalize path (BridgeBatchEntry
|
|
802
|
+
// → EditorApplication.update) did not fire. feedback 2026-08-15
|
|
803
|
+
// recurrence: this used to surface as the generic
|
|
804
|
+
// batch_spawn_failed code, so agents (and MCP hosts keying on
|
|
805
|
+
// isError) read a healthy compile as a spawn failure. It now
|
|
806
|
+
// carries the distinct `markers_missing` code with the
|
|
807
|
+
// confirm-via-read_compile_errors guidance.
|
|
808
|
+
// - non-zero: an unclassified spawn failure (stays
|
|
809
|
+
// batch_spawn_failed).
|
|
810
|
+
if (exitCode === 0) {
|
|
811
|
+
reject(new BatchClassificationError("markers_missing", `Batch output did not contain JSON markers. Exit code: 0.` +
|
|
812
|
+
(tail ? ` Last output: ${tail}` : "") +
|
|
813
|
+
` Unity exited cleanly but emitted no markers after '${toolName}' ` +
|
|
814
|
+
"(the async finalize path did not run) — the compile likely " +
|
|
815
|
+
"succeeded. Call unity_open_mcp_read_compile_errors to confirm.", [
|
|
816
|
+
`The ${toolName} batch Unity exited cleanly (exit 0) but emitted no JSON markers — the async finalize path did not run. This is usually a healthy compile with a missing report, not a spawn failure.`,
|
|
817
|
+
"Call unity_open_mcp_read_compile_errors to confirm errorCount is 0 before treating the compile as passed.",
|
|
818
|
+
]));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
627
821
|
reject(new Error(`Batch output did not contain JSON markers. Exit code: ${exitCode}.` +
|
|
628
822
|
(tail ? ` Last output: ${tail}` : "")));
|
|
629
823
|
return;
|