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.
Files changed (144) hide show
  1. package/dist/asmdef-discovery.d.ts +35 -0
  2. package/dist/asmdef-discovery.js +171 -0
  3. package/dist/asmdef-discovery.js.map +1 -0
  4. package/dist/batch-spawn.d.ts +27 -0
  5. package/dist/batch-spawn.js +218 -24
  6. package/dist/batch-spawn.js.map +1 -1
  7. package/dist/capabilities/build-capabilities.d.ts +10 -0
  8. package/dist/capabilities/build-capabilities.js +4 -0
  9. package/dist/capabilities/build-capabilities.js.map +1 -1
  10. package/dist/capabilities/capabilities-view.d.ts +48 -0
  11. package/dist/capabilities/capabilities-view.js +93 -0
  12. package/dist/capabilities/capabilities-view.js.map +1 -0
  13. package/dist/capabilities/intent-groups.js +42 -0
  14. package/dist/capabilities/intent-groups.js.map +1 -1
  15. package/dist/capabilities/rule-catalog.js +7 -1
  16. package/dist/capabilities/rule-catalog.js.map +1 -1
  17. package/dist/capabilities/tool-groups.js +37 -2
  18. package/dist/capabilities/tool-groups.js.map +1 -1
  19. package/dist/child-supervision.d.ts +34 -0
  20. package/dist/child-supervision.js +110 -0
  21. package/dist/child-supervision.js.map +1 -0
  22. package/dist/cli/cli.js +9 -0
  23. package/dist/cli/cli.js.map +1 -1
  24. package/dist/cli/commands.js +37 -4
  25. package/dist/cli/commands.js.map +1 -1
  26. package/dist/compressible-router.js +12 -1
  27. package/dist/compressible-router.js.map +1 -1
  28. package/dist/constants.d.ts +31 -1
  29. package/dist/constants.js +31 -1
  30. package/dist/constants.js.map +1 -1
  31. package/dist/dialog-dismiss.d.ts +11 -0
  32. package/dist/dialog-dismiss.js +59 -4
  33. package/dist/dialog-dismiss.js.map +1 -1
  34. package/dist/editor-wedge.d.ts +45 -0
  35. package/dist/editor-wedge.js +111 -0
  36. package/dist/editor-wedge.js.map +1 -0
  37. package/dist/event-stream.d.ts +19 -6
  38. package/dist/event-stream.js +139 -17
  39. package/dist/event-stream.js.map +1 -1
  40. package/dist/gate-error.d.ts +2 -0
  41. package/dist/gate-error.js +9 -0
  42. package/dist/gate-error.js.map +1 -1
  43. package/dist/instance-discovery.d.ts +23 -0
  44. package/dist/instance-discovery.js +47 -1
  45. package/dist/instance-discovery.js.map +1 -1
  46. package/dist/live-client.d.ts +40 -5
  47. package/dist/live-client.js +254 -36
  48. package/dist/live-client.js.map +1 -1
  49. package/dist/offline/api.js +82 -30
  50. package/dist/offline/api.js.map +1 -1
  51. package/dist/offline/index-builders.d.ts +9 -0
  52. package/dist/offline/index-builders.js +58 -8
  53. package/dist/offline/index-builders.js.map +1 -1
  54. package/dist/offline/parse.js +5 -1
  55. package/dist/offline/parse.js.map +1 -1
  56. package/dist/process-diagnostics.d.ts +101 -19
  57. package/dist/process-diagnostics.js +197 -59
  58. package/dist/process-diagnostics.js.map +1 -1
  59. package/dist/project-settings.d.ts +36 -0
  60. package/dist/project-settings.js +96 -0
  61. package/dist/project-settings.js.map +1 -0
  62. package/dist/results.d.ts +8 -0
  63. package/dist/results.js +4 -1
  64. package/dist/results.js.map +1 -1
  65. package/dist/routers.js +4 -1
  66. package/dist/routers.js.map +1 -1
  67. package/dist/running-unity.d.ts +23 -0
  68. package/dist/running-unity.js +60 -0
  69. package/dist/running-unity.js.map +1 -1
  70. package/dist/server.js +8 -2
  71. package/dist/server.js.map +1 -1
  72. package/dist/stale-assembly-cache.d.ts +40 -0
  73. package/dist/stale-assembly-cache.js +76 -0
  74. package/dist/stale-assembly-cache.js.map +1 -0
  75. package/dist/tool-hint.d.ts +36 -0
  76. package/dist/tool-hint.js +98 -0
  77. package/dist/tool-hint.js.map +1 -0
  78. package/dist/tool-router.d.ts +47 -2
  79. package/dist/tool-router.js +652 -71
  80. package/dist/tool-router.js.map +1 -1
  81. package/dist/tools/agent-capabilities.js +27 -1
  82. package/dist/tools/agent-capabilities.js.map +1 -1
  83. package/dist/tools/baseline-create.js +1 -1
  84. package/dist/tools/baseline-create.js.map +1 -1
  85. package/dist/tools/batch-execute.js +12 -1
  86. package/dist/tools/batch-execute.js.map +1 -1
  87. package/dist/tools/bridge-status.js +21 -1
  88. package/dist/tools/bridge-status.js.map +1 -1
  89. package/dist/tools/capture-inline.js +4 -3
  90. package/dist/tools/capture-inline.js.map +1 -1
  91. package/dist/tools/editor-status.js +7 -2
  92. package/dist/tools/editor-status.js.map +1 -1
  93. package/dist/tools/execute-csharp.js +36 -3
  94. package/dist/tools/execute-csharp.js.map +1 -1
  95. package/dist/tools/execute-menu.js +33 -2
  96. package/dist/tools/execute-menu.js.map +1 -1
  97. package/dist/tools/index.d.ts +1 -0
  98. package/dist/tools/index.js +44 -0
  99. package/dist/tools/index.js.map +1 -1
  100. package/dist/tools/inputsim-key.d.ts +40 -0
  101. package/dist/tools/inputsim-key.js +72 -0
  102. package/dist/tools/inputsim-key.js.map +1 -0
  103. package/dist/tools/inputsim-pointer.d.ts +40 -0
  104. package/dist/tools/inputsim-pointer.js +121 -0
  105. package/dist/tools/inputsim-pointer.js.map +1 -0
  106. package/dist/tools/inputsim-pointer3d.d.ts +40 -0
  107. package/dist/tools/inputsim-pointer3d.js +55 -0
  108. package/dist/tools/inputsim-pointer3d.js.map +1 -0
  109. package/dist/tools/inputsim-probe.d.ts +40 -0
  110. package/dist/tools/inputsim-probe.js +43 -0
  111. package/dist/tools/inputsim-probe.js.map +1 -0
  112. package/dist/tools/inputsim-step.d.ts +40 -0
  113. package/dist/tools/inputsim-step.js +50 -0
  114. package/dist/tools/inputsim-step.js.map +1 -0
  115. package/dist/tools/inputsim-touch.d.ts +40 -0
  116. package/dist/tools/inputsim-touch.js +83 -0
  117. package/dist/tools/inputsim-touch.js.map +1 -0
  118. package/dist/tools/invoke-method.js +15 -1
  119. package/dist/tools/invoke-method.js.map +1 -1
  120. package/dist/tools/memory-snapshot-capture.js +11 -5
  121. package/dist/tools/memory-snapshot-capture.js.map +1 -1
  122. package/dist/tools/pull-events.js +18 -10
  123. package/dist/tools/pull-events.js.map +1 -1
  124. package/dist/tools/read-compile-errors.js +36 -7
  125. package/dist/tools/read-compile-errors.js.map +1 -1
  126. package/dist/tools/regression-check.js +2 -1
  127. package/dist/tools/regression-check.js.map +1 -1
  128. package/dist/tools/resource-pressure.js +21 -10
  129. package/dist/tools/resource-pressure.js.map +1 -1
  130. package/dist/tools/run-tests.js +11 -2
  131. package/dist/tools/run-tests.js.map +1 -1
  132. package/dist/tools/scan-all.js +2 -2
  133. package/dist/tools/scan-all.js.map +1 -1
  134. package/dist/tools/screenshot-window.js +18 -12
  135. package/dist/tools/screenshot-window.js.map +1 -1
  136. package/dist/tools/screenshot.js +13 -6
  137. package/dist/tools/screenshot.js.map +1 -1
  138. package/dist/tools/visual-compare.d.ts +40 -0
  139. package/dist/tools/visual-compare.js +98 -0
  140. package/dist/tools/visual-compare.js.map +1 -0
  141. package/dist/unity-log.d.ts +67 -2
  142. package/dist/unity-log.js +206 -8
  143. package/dist/unity-log.js.map +1 -1
  144. 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":"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"}
@@ -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;
@@ -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: "A live Unity Editor holds the project lock, so the headless " +
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
- reject(new BatchClassificationError("editor_instance_locked", "A live Unity Editor holds the project lock, so the headless " +
610
- "compile_check spawn could not open the project. Unity allows " +
611
- "only one Editor per project. Either close the live Editor and " +
612
- "retry compile_check, or verify compile state via the live " +
613
- "bridge instead (execute_csharp + Library/ScriptAssemblies DLL " +
614
- "mtime check, or read_compile_errors).", [
615
- "A live Unity Editor holds the project lock, so the headless compile_check spawn cannot open the project (one Editor per project).",
616
- "To verify compile state without closing the Editor, call unity_open_mcp_read_compile_errors (reads Editor.log offline).",
617
- "To confirm a specific local package recompiled, call unity_open_mcp_reimport_package on the package and compare dllMtimeBefore/dllMtimeAfter.",
618
- "To run a true headless compile_check, close the live Editor first.",
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;