unity-open-mcp 1.0.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 (83) hide show
  1. package/dist/batch-spawn.d.ts +26 -0
  2. package/dist/batch-spawn.js +152 -35
  3. package/dist/batch-spawn.js.map +1 -1
  4. package/dist/capabilities/build-capabilities.d.ts +10 -0
  5. package/dist/capabilities/build-capabilities.js +4 -0
  6. package/dist/capabilities/build-capabilities.js.map +1 -1
  7. package/dist/capabilities/capabilities-view.d.ts +48 -0
  8. package/dist/capabilities/capabilities-view.js +93 -0
  9. package/dist/capabilities/capabilities-view.js.map +1 -0
  10. package/dist/child-supervision.d.ts +34 -0
  11. package/dist/child-supervision.js +110 -0
  12. package/dist/child-supervision.js.map +1 -0
  13. package/dist/cli/cli.js +9 -0
  14. package/dist/cli/cli.js.map +1 -1
  15. package/dist/cli/commands.js +37 -4
  16. package/dist/cli/commands.js.map +1 -1
  17. package/dist/compressible-router.js +12 -1
  18. package/dist/compressible-router.js.map +1 -1
  19. package/dist/constants.d.ts +17 -7
  20. package/dist/constants.js +17 -7
  21. package/dist/constants.js.map +1 -1
  22. package/dist/dialog-dismiss.d.ts +11 -0
  23. package/dist/dialog-dismiss.js +59 -4
  24. package/dist/dialog-dismiss.js.map +1 -1
  25. package/dist/editor-wedge.d.ts +45 -0
  26. package/dist/editor-wedge.js +111 -0
  27. package/dist/editor-wedge.js.map +1 -0
  28. package/dist/event-stream.d.ts +6 -3
  29. package/dist/event-stream.js +62 -5
  30. package/dist/event-stream.js.map +1 -1
  31. package/dist/gate-error.d.ts +2 -0
  32. package/dist/gate-error.js +9 -0
  33. package/dist/gate-error.js.map +1 -1
  34. package/dist/instance-discovery.d.ts +23 -0
  35. package/dist/instance-discovery.js +47 -1
  36. package/dist/instance-discovery.js.map +1 -1
  37. package/dist/live-client.d.ts +23 -5
  38. package/dist/live-client.js +132 -31
  39. package/dist/live-client.js.map +1 -1
  40. package/dist/process-diagnostics.d.ts +4 -3
  41. package/dist/process-diagnostics.js +37 -7
  42. package/dist/process-diagnostics.js.map +1 -1
  43. package/dist/results.d.ts +8 -0
  44. package/dist/results.js +4 -1
  45. package/dist/results.js.map +1 -1
  46. package/dist/routers.js +4 -1
  47. package/dist/routers.js.map +1 -1
  48. package/dist/server.js +8 -2
  49. package/dist/server.js.map +1 -1
  50. package/dist/tool-hint.d.ts +36 -0
  51. package/dist/tool-hint.js +98 -0
  52. package/dist/tool-hint.js.map +1 -0
  53. package/dist/tool-router.d.ts +8 -1
  54. package/dist/tool-router.js +266 -20
  55. package/dist/tool-router.js.map +1 -1
  56. package/dist/tools/agent-capabilities.js +27 -1
  57. package/dist/tools/agent-capabilities.js.map +1 -1
  58. package/dist/tools/baseline-create.js +1 -1
  59. package/dist/tools/baseline-create.js.map +1 -1
  60. package/dist/tools/batch-execute.js +12 -1
  61. package/dist/tools/batch-execute.js.map +1 -1
  62. package/dist/tools/bridge-status.js +21 -1
  63. package/dist/tools/bridge-status.js.map +1 -1
  64. package/dist/tools/execute-csharp.js +11 -1
  65. package/dist/tools/execute-csharp.js.map +1 -1
  66. package/dist/tools/execute-menu.js +33 -2
  67. package/dist/tools/execute-menu.js.map +1 -1
  68. package/dist/tools/index.js +10 -0
  69. package/dist/tools/index.js.map +1 -1
  70. package/dist/tools/invoke-method.js +15 -1
  71. package/dist/tools/invoke-method.js.map +1 -1
  72. package/dist/tools/read-compile-errors.js +30 -9
  73. package/dist/tools/read-compile-errors.js.map +1 -1
  74. package/dist/tools/regression-check.js +2 -1
  75. package/dist/tools/regression-check.js.map +1 -1
  76. package/dist/tools/run-tests.js +11 -2
  77. package/dist/tools/run-tests.js.map +1 -1
  78. package/dist/tools/scan-all.js +2 -2
  79. package/dist/tools/scan-all.js.map +1 -1
  80. package/dist/unity-log.d.ts +16 -0
  81. package/dist/unity-log.js +60 -9
  82. package/dist/unity-log.js.map +1 -1
  83. package/package.json +1 -1
@@ -1,6 +1,8 @@
1
1
  import { AssetModelCache, isCompressible, routeCompressible } from "./compressible-router.js";
2
2
  import { listAssetsOffline, findReferencesOffline, dependenciesOffline } from "./offline.js";
3
- import { resolveEditorLogPath, readLogTail, DEFAULT_LOG_TAIL_BYTES, detectStaleLog, detectStaleAssembly, parseLogAuthorship, compareLogAuthorship } from "./unity-log.js";
3
+ import { resolveEditorLogPath, readLogTail, DEFAULT_LOG_TAIL_BYTES, detectStaleLog, detectStaleAssembly, detectErrorsPredatingEdits, parseLogAuthorship, compareLogAuthorship } from "./unity-log.js";
4
+ import { toolHintReference } from "./tool-hint.js";
5
+ import { scanForFdExhaustion } from "./editor-wedge.js";
4
6
  import { summarizeProjectHealth, extractProjectHealthIssues } from "./project-health.js";
5
7
  import { countProjectAsmdefs, countAssembliesWithErrors } from "./asmdef-discovery.js";
6
8
  import { buildCapabilities } from "./capabilities/build-capabilities.js";
@@ -8,11 +10,12 @@ import { RULE_CATALOG, FIX_CATALOG } from "./capabilities/rule-catalog.js";
8
10
  import { GROUP_IDS, TOOL_GROUPS, AUTO_ACTIVATE_GROUPS, groupToTools, getGroup, } from "./capabilities/tool-groups.js";
9
11
  import { recommendGroups, } from "./capabilities/intent-groups.js";
10
12
  import { listRules } from "./capabilities/list-rules.js";
13
+ import { viewCapabilities } from "./capabilities/capabilities-view.js";
11
14
  import { generateSkill } from "./skill/generate-skill.js";
12
15
  import { getKnownClientKeys } from "./skill/client-paths.js";
13
16
  import { ALL_TOOLS } from "./tools/index.js";
14
17
  import { lockPath, readInstanceLock, classifyInstance, isPidAlive } from "./instance-discovery.js";
15
- import { PORT_ENV_VAR, TYPED_EDITOR_ACTIVATE_INSTRUCTION } from "./constants.js";
18
+ import { PORT_ENV_VAR, EXPECTED_BRIDGE_WIRE_CONTRACT } from "./constants.js";
16
19
  import { findUnityForProject, readProcessCommandLine } from "./running-unity.js";
17
20
  // M31-optimizations Plan 1 / L14 — opt-in route-logging gate. Resolved once at
18
21
  // module load into a boolean; the per-dispatch log sites read the boolean
@@ -51,7 +54,7 @@ const defaultHubBackend = {
51
54
  setInstallPath: (path) => setInstallPath(path),
52
55
  };
53
56
  import { BATCH_TOOL_NAMES, ALWAYS_BATCH_TOOLS } from "./batch-spawn.js";
54
- import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, } from "./output-profile.js";
57
+ import { readProfileAndDetail, applyPaging, attachPagination, foldVerifyResult, parseResultBody, withResultBody, isOutputProfile, } from "./output-profile.js";
55
58
  /**
56
59
  * Single source of truth for the `_source` tag. Every route-tagging site goes
57
60
  * through one of the two helpers below so live / offline / local stay
@@ -339,7 +342,25 @@ function groupLiveRefsByKind(paths) {
339
342
  }
340
343
  // Operator-facing "what to do next" hint for each coarse status. Kept short
341
344
  // and action-oriented so the Validation Suite can render it inline.
342
- function bridgeStatusNextStep(status) {
345
+ function bridgeStatusNextStep(status, wedge) {
346
+ if (status === "wedged") {
347
+ return wedge?.reason === "main_thread_wedged"
348
+ ? "Unity's main thread is wedged — the HTTP listener still answers /ping, but the " +
349
+ "editor heartbeat has stopped, which is the signature of a MODAL DIALOG blocking " +
350
+ "the message pump (material/scene converters and several ExecuteMenuItem paths " +
351
+ "open one). This is NOT a compile failure and NOT a dead bridge assembly — a dead " +
352
+ "assembly has no listener at all. It cannot self-heal and no tool can dismiss the " +
353
+ "dialog: an operator must dismiss it in the Unity UI, after which queued calls " +
354
+ "resume. Prefer non-interactive APIs over unity_open_mcp_execute_menu for " +
355
+ "converter-style menus."
356
+ : "The Unity Editor hit the Bee build driver's file-descriptor exhaustion — the " +
357
+ "process is alive and the listener answers, but it can no longer BUILD: " +
358
+ "Library/ScriptAssemblies stops updating, so C# edits never take effect and " +
359
+ "unity_open_mcp_execute_csharp silently runs the previous assembly. Do not trust " +
360
+ "any result computed since. Call unity_open_mcp_read_compile_errors to confirm " +
361
+ "the editor_fd_exhaustion issue, save scene work, then restart the Editor " +
362
+ "(unity_open_mcp_restart_editor with confirm: true, or manually).";
363
+ }
343
364
  switch (status) {
344
365
  case "running":
345
366
  return "Bridge is ready. Proceed with live-only MCP tools.";
@@ -371,7 +392,66 @@ function bridgeStatusNextStep(status) {
371
392
  "Bridge) and confirm it is started.";
372
393
  }
373
394
  }
374
- function bridgeStatusRecoveryHint(status) {
395
+ /**
396
+ * specs/feedback.md 2026-08-24 — wire-contract staleness block.
397
+ *
398
+ * The field report: a blocker fixed and shipped on 2026-08-17 (registry tools
399
+ * rejecting the transport envelope keys, which made `editor_status` uncallable
400
+ * with default args) recurred against an installed bridge that still reported
401
+ * `bridgeVersion "1.0.0"` — the same string the FIXED source reports, because
402
+ * the package semver does not move for a wire-contract fix. "Your install is
403
+ * stale" and "this regressed" were therefore indistinguishable at the call
404
+ * site, and the agent had no way to pick between reinstalling and filing a bug.
405
+ *
406
+ * `BridgeSession.WireContract` is bumped for every observable change to the
407
+ * request/response contract, so comparing it against
408
+ * {@link EXPECTED_BRIDGE_WIRE_CONTRACT} answers that question directly. A
409
+ * bridge that predates the field reports no `wireContract` at all — `bridge`
410
+ * is then null and `stale` is true, which is the correct reading (it is older
411
+ * than revision 1 by definition).
412
+ *
413
+ * Returns null when /ping was unreachable: there is no bridge to compare, and a
414
+ * bogus "stale" on an offline bridge would be worse than silence.
415
+ */
416
+ function wireContractBlock(reachable, reported) {
417
+ if (!reachable)
418
+ return null;
419
+ const bridge = typeof reported === "number" ? reported : null;
420
+ const stale = bridge === null || bridge < EXPECTED_BRIDGE_WIRE_CONTRACT;
421
+ return {
422
+ bridge,
423
+ expected: EXPECTED_BRIDGE_WIRE_CONTRACT,
424
+ stale,
425
+ note: stale
426
+ ? "The installed Unity bridge package is OLDER than this MCP server's wire " +
427
+ "contract" +
428
+ (bridge === null
429
+ ? " (it does not report `wireContract` at all, so it predates revision 1)"
430
+ : ` (revision ${bridge} vs ${EXPECTED_BRIDGE_WIRE_CONTRACT})`) +
431
+ ". A tool failing in a way the docs say is fixed is almost certainly " +
432
+ "this staleness, not a regression: reinstall/update the bridge package " +
433
+ "in Unity (Package Manager > the unity-open-mcp-bridge git URL) and " +
434
+ "retry before filing anything. bridgeVersion alone cannot tell you " +
435
+ "this — it does not move for a wire-contract fix."
436
+ : null,
437
+ };
438
+ }
439
+ function bridgeStatusRecoveryHint(status, wedge) {
440
+ if (status === "wedged") {
441
+ return wedge?.reason === "main_thread_wedged"
442
+ ? {
443
+ tool: "unity_open_mcp_editor_status",
444
+ reason: "Main thread wedged (likely a modal dialog): /ping answers but the " +
445
+ "editor heartbeat stopped. No tool can dismiss a modal — an operator " +
446
+ "must close it in the Unity UI, then editor_status confirms recovery.",
447
+ }
448
+ : {
449
+ tool: "unity_open_mcp_read_compile_errors",
450
+ reason: "editor_fd_exhaustion in the freshest Editor.log — the Editor answers " +
451
+ "HTTP but can no longer build, so C# edits never take effect. Confirm " +
452
+ "with read_compile_errors, then restart the Editor.",
453
+ };
454
+ }
375
455
  if (status === "dead_bridge") {
376
456
  return {
377
457
  tool: "unity_open_mcp_read_compile_errors",
@@ -490,10 +570,21 @@ export class ToolRouter {
490
570
  }
491
571
  // Compact drill-down reads: offline-first for text-serialized assets, fall
492
572
  // back to live bridge for binary formats. The compressible-router handles
493
- // the source selection internally.
573
+ // the source selection internally and tags the payload `_source`
574
+ // ("offline" when it was served from the disk parser without touching the
575
+ // bridge; error results tag their origin too — "local" for caller-side
576
+ // validation, "offline" for a failed offline-first attempt with the
577
+ // bridge down). Derive `_route` from that tag: stamping route:"live"
578
+ // unconditionally mislabeled offline-served reads as bridge responses —
579
+ // contradictory metadata that pointed an agent's diagnostics at the
580
+ // bridge for data it never produced.
494
581
  if (isCompressible(toolName)) {
495
582
  const result = await routeCompressible(toolName, args, live, this.modelCache, this.projectPath);
496
- return injectRouteMeta(result, { route: "live" });
583
+ const source = parseResultBody(result)?._source;
584
+ const meta = source === "offline" || source === "local"
585
+ ? { route: source }
586
+ : { route: "live" };
587
+ return injectRouteMeta(result, meta);
497
588
  }
498
589
  const canBatch = this.batch.isBatchTool(toolName);
499
590
  const isPing = toolName === "unity_open_mcp_ping";
@@ -561,6 +652,11 @@ export class ToolRouter {
561
652
  mode: "batch",
562
653
  compiling: false,
563
654
  isPlaying: false,
655
+ // feedback 2026-08-17 — provenance marker distinguishing this
656
+ // synthesized fallback (no probe was possible; the live route was
657
+ // unavailable) from the live route's verified probe answer
658
+ // (source:"probe" + asOf).
659
+ source: "unprobed",
564
660
  _route: {
565
661
  route: "batch",
566
662
  fallbackReason: "live_unavailable",
@@ -700,6 +796,16 @@ export class ToolRouter {
700
796
  // newest Library/ScriptAssemblies/*.dll mtime against Assets/**/*.cs source
701
797
  // mtimes and surface staleAssembly when any source is newer than the DLL.
702
798
  const staleAsm = detectStaleAssembly(this.projectPath);
799
+ // specs/feedback.md 2026-08-14 — "these errors may predate your edits".
800
+ // Three consecutive reads returned the same error block for code that had
801
+ // already been fixed, with no staleness signal: Editor.log's FILE mtime
802
+ // was fresh (asset imports keep appending) while the error BLOCK inside it
803
+ // was old, so the staleLog heuristic could not fire. The built-assembly
804
+ // mtime is the anchor that works — a cited source newer than the newest
805
+ // built DLL proves no compile has COMPLETED since the edit.
806
+ const errorsPredateEdits = errors.length > 0
807
+ ? detectErrorsPredatingEdits(errors.map((e) => e.file), this.projectPath, staleAsm.dllMtimeMs)
808
+ : null;
703
809
  // feedback-04-08-opus §2 — log-authorship cross-check. A batch run of a
704
810
  // NEWER Unity writes a log whose errors (e.g. an API deprecation that is
705
811
  // only an error on that version) CANNOT occur in the live editor. Parse the
@@ -729,7 +835,17 @@ export class ToolRouter {
729
835
  : "no_errors_found";
730
836
  const errorsMayNotApply = health.unhealthy &&
731
837
  (!!staleLog || authorshipCheck.versionMismatch);
732
- const status = errorsMayNotApply ? "stale_log" : logVerdict;
838
+ // feedback 2026-08-17 — a clean bill of health read from the ROTATED log
839
+ // is not a clean bill of health. When logSource is a prev_log_* value the
840
+ // resolver fell back to Editor-prev.log while a live Editor writes a
841
+ // different file; an agent reading only `status` got a false green. A
842
+ // no-errors read from a rotated log is explicitly `indeterminate`.
843
+ const prevLogRead = resolvedLog.reason.startsWith("prev_log_");
844
+ const status = errorsMayNotApply
845
+ ? "stale_log"
846
+ : logVerdict === "no_errors_found" && prevLogRead
847
+ ? "indeterminate"
848
+ : logVerdict;
733
849
  // specs/feedback.md 2026-08-12 — partial-compile signal. Unity compiles
734
850
  // assemblies in dependency order; when an early assembly fails the pipeline
735
851
  // may not reach dependent ones, so the reported errors can be a SUBSET of
@@ -749,6 +865,54 @@ export class ToolRouter {
749
865
  const assembliesWithErrors = partialCompileLikely
750
866
  ? countAssembliesWithErrors(errors.map((e) => e.file), this.projectPath)
751
867
  : 0;
868
+ // Headline assembly. Precedence (most severe first):
869
+ // 1. errorsMayNotApply — stale log / version mismatch: the errors may
870
+ // not apply at all (feedback-fable-04-08 §4).
871
+ // 2. errorsPredateEdits — errors + a stale assembly: the error block
872
+ // was written by a compile that predates the on-disk edits (no
873
+ // compile has COMPLETED since: every built DLL is older than a cited
874
+ // source). The log-mtime staleness heuristic cannot see this — asset
875
+ // imports keep appending to Editor.log, so the FILE is fresh while
876
+ // the error BLOCK inside it is old. Say so in the headline rather
877
+ // than letting the agent re-read code it has already fixed
878
+ // (specs/feedback.md 2026-08-14).
879
+ // 3. feedback 2026-08-17 — errors + staleAssembly WITHOUT the
880
+ // cited-files proof: the whole-Assets scan still shows a source
881
+ // newer than the newest built DLL, so the error block may come from
882
+ // an earlier compile. The 2026-08-14 fix covered only the
883
+ // errorsPredateEdits shape; this extends the headline caveat to the
884
+ // staleAssembly shape (the field report's 12 stale CS errors had
885
+ // staleAssembly:true but the headline stated them as fact).
886
+ // 4. indeterminate (feedback 2026-08-17) — no errors, but read from the
887
+ // ROTATED log: not a verified clean bill of health.
888
+ // 5. the plain health headline.
889
+ const headline = errorsMayNotApply
890
+ ? `${errors.length} error(s) in Editor.log, but the log appears ` +
891
+ "STALE or authored by a different Unity — these errors may NOT " +
892
+ "apply to the running editor. Do NOT act on them until a genuine " +
893
+ `recompile (${toolHintReference("unity_open_mcp_recompile_scripts")}) ` +
894
+ "confirms them."
895
+ : errorsPredateEdits
896
+ ? `${health.headline} NOTE: these errors may PREDATE your edits — ` +
897
+ `${errorsPredateEdits.files.length} cited source file(s) are newer ` +
898
+ "than the newest built assembly, so no compile has completed since " +
899
+ "you changed them. Force a recompile " +
900
+ `(${toolHintReference("unity_open_mcp_recompile_scripts")}) and ` +
901
+ "re-read before acting on them."
902
+ : hasErrors && staleAsm.staleAssembly
903
+ ? `${health.headline} NOTE: the error block may PREDATE the current ` +
904
+ `sources — ${staleAsm.newerSources.length} Assets/**/*.cs source(s) ` +
905
+ "are newer than the newest built assembly, so these errors may come " +
906
+ "from an earlier compile. Force a recompile " +
907
+ `(${toolHintReference("unity_open_mcp_recompile_scripts")}) and ` +
908
+ "re-read before acting on them."
909
+ : status === "indeterminate"
910
+ ? "No errors found, but this is NOT a verified clean bill of " +
911
+ `health: the read came from the ROTATED log (${resolvedLog.reason === "prev_log_live_editor" ? "Editor-prev.log chosen because a live editor holds the project" : "Editor-prev.log fallback"}) ` +
912
+ "while a live Editor may be writing a different log. Confirm via " +
913
+ "the live bridge (unity_open_mcp_editor_status / a live tool " +
914
+ "probe) or re-read after the Editor settles."
915
+ : health.headline;
752
916
  return sourceResult({
753
917
  status,
754
918
  // When the errors may not apply (stale log / version mismatch), the
@@ -758,14 +922,11 @@ export class ToolRouter {
758
922
  // log verdict is preserved in `logVerdict` for callers that want the
759
923
  // literal signal regardless of freshness.
760
924
  unhealthy: errorsMayNotApply ? false : health.unhealthy,
761
- headline: errorsMayNotApply
762
- ? `${errors.length} error(s) in Editor.log, but the log appears ` +
763
- "STALE or authored by a different Unity — these errors may NOT " +
764
- "apply to the running editor. Do NOT act on them until a genuine " +
765
- "recompile (unity_open_mcp_recompile_scripts, in the typed-editor " +
766
- `group — ${TYPED_EDITOR_ACTIVATE_INSTRUCTION} first) confirms them.`
767
- : health.headline,
768
- ...(errorsMayNotApply ? { logVerdict } : {}),
925
+ headline,
926
+ // Raw verdict preserved whenever the top-level status was rewritten
927
+ // (stale_log downgrade OR the indeterminate prev-log case), so callers
928
+ // can still read what the log literally said.
929
+ ...(errorsMayNotApply || status === "indeterminate" ? { logVerdict } : {}),
769
930
  errorCount: errors.length,
770
931
  errors,
771
932
  // specs/feedback.md 2026-08-12 — present only when there are errors AND
@@ -816,6 +977,26 @@ export class ToolRouter {
816
977
  staleAssemblyNewerSources: staleAsm.newerSources,
817
978
  }
818
979
  : {}),
980
+ // specs/feedback.md 2026-08-14 — machine-readable sibling of the
981
+ // headline caveat above. Present ONLY when errors were returned AND at
982
+ // least one file they cite is newer than the newest built assembly, so
983
+ // the error block cannot reflect the current source. Distinct from
984
+ // `staleLogSuspected` (log-file mtime) and `staleAssembly` (whole
985
+ // Assets/ scan): this names the cited files the agent is about to
986
+ // re-read.
987
+ ...(errorsPredateEdits
988
+ ? {
989
+ errorsMayPredateEdits: true,
990
+ errorsMayPredateEditsFiles: errorsPredateEdits.files,
991
+ errorsMayPredateEditsHint: "These errors were parsed from a compile that ran BEFORE the " +
992
+ "listed files were last edited (each is newer than the newest " +
993
+ "Library/ScriptAssemblies/*.dll, so no compile has completed " +
994
+ "since). Do not re-read or re-fix that code on the strength of " +
995
+ "this block — force a recompile " +
996
+ `(${toolHintReference("unity_open_mcp_recompile_scripts")}) ` +
997
+ "and read again.",
998
+ }
999
+ : {}),
819
1000
  // feedback-04-08-opus §2 — log authorship. Emitted whenever the header
820
1001
  // was parseable (the live editor's long tail usually has no header, so
821
1002
  // these stay absent in the common case). On a version mismatch the hint
@@ -945,6 +1126,12 @@ export class ToolRouter {
945
1126
  ? args.kind
946
1127
  : undefined;
947
1128
  const includePlanned = args.include_planned !== false;
1129
+ // specs/feedback.md 2026-08-24 — the tool surface defaults to `compact`
1130
+ // (the unfolded catalog is ~513 KB, which overflows a typical harness
1131
+ // per-result ceiling). `buildCapabilities` itself keeps returning the full
1132
+ // shape so its other caller (generate_skill) is unaffected; the fold is a
1133
+ // view applied here.
1134
+ const profile = isOutputProfile(args.profile) ? args.profile : "compact";
948
1135
  // M18 Plan 2 / T18.2.3 — when the bridge is live, probe its compiled-
949
1136
  // state tool inventory so per-group `available` reflects whether each
950
1137
  // domain dependency compiled in (e.g. UNITY_OPEN_MCP_EXT_NAVIGATION).
@@ -970,7 +1157,11 @@ export class ToolRouter {
970
1157
  fixes: FIX_CATALOG,
971
1158
  availableBridgeTools,
972
1159
  }, { kind, includePlanned });
973
- return sourceResult(result, "local");
1160
+ return sourceResult(viewCapabilities(result, {
1161
+ profile,
1162
+ page_size: typeof args.page_size === "number" ? args.page_size : undefined,
1163
+ cursor: typeof args.cursor === "string" ? args.cursor : undefined,
1164
+ }), "local");
974
1165
  }
975
1166
  async routeListRules(args) {
976
1167
  const assetKind = typeof args.asset_kind === "string" ? args.asset_kind : undefined;
@@ -1474,8 +1665,52 @@ export class ToolRouter {
1474
1665
  if (proc)
1475
1666
  unityProcessPid = proc.pid;
1476
1667
  }
1668
+ // specs/feedback.md 2026-08-14 — wedge detection, evaluated BEFORE the
1669
+ // existing token ladder because both wedges masquerade as one of its
1670
+ // healthy-looking outcomes.
1671
+ //
1672
+ // (a) main_thread_wedged. `dead_bridge` means "live PID + stale
1673
+ // heartbeat". A modal dialog produces exactly that (the heartbeat is
1674
+ // written from EditorApplication.update, which a blocked pump stops
1675
+ // running) — but a dead bridge ASSEMBLY cannot answer /ping at all,
1676
+ // because the listener lives in the assembly that failed to compile.
1677
+ // So a reachable /ping alongside a stale heartbeat rules out the
1678
+ // safe-mode diagnosis and identifies a stalled main thread instead.
1679
+ // (b) editor_fd_exhaustion. Process, heartbeat and /ping all survive a
1680
+ // dead build driver, so nothing in the existing ladder can see it.
1681
+ // One regex over the freshest log tail can (the same scan
1682
+ // read_compile_errors runs), and it only runs when the ladder would
1683
+ // otherwise report a healthy editor — the offline/stopped paths have
1684
+ // no editor to be wedged.
1685
+ let wedge = null;
1686
+ if (classification === "dead_bridge" && pingReachable) {
1687
+ wedge = {
1688
+ reason: "main_thread_wedged",
1689
+ detail: "The instance lock's heartbeat is stale (the editor main thread stopped " +
1690
+ "ticking) but the bridge listener still answers /ping. A failed bridge " +
1691
+ "assembly would take the listener down with it, so this is a blocked main " +
1692
+ "thread — almost always a modal dialog — not Safe Mode.",
1693
+ };
1694
+ }
1695
+ else if (pingReachable && connected) {
1696
+ const fd = scanForFdExhaustion(this.projectPath, this.resolveLiveEditorPid(), this.globalLogPathOverride);
1697
+ if (fd.present) {
1698
+ wedge = {
1699
+ reason: "editor_fd_exhaustion",
1700
+ detail: "The freshest Editor.log carries the Bee build-driver file-descriptor " +
1701
+ "exhaustion signature. The Editor answers HTTP but cannot build: " +
1702
+ "Library/ScriptAssemblies stops updating, so C# edits never take effect " +
1703
+ "and execute_csharp keeps running the previous assembly.",
1704
+ ...(fd.logPath ? { logPath: fd.logPath } : {}),
1705
+ ...(fd.raw ? { raw: fd.raw } : {}),
1706
+ };
1707
+ }
1708
+ }
1477
1709
  let status;
1478
- if (classification === "dead_bridge") {
1710
+ if (wedge !== null) {
1711
+ status = "wedged";
1712
+ }
1713
+ else if (classification === "dead_bridge") {
1479
1714
  status = "dead_bridge";
1480
1715
  }
1481
1716
  else if (compiling) {
@@ -1517,7 +1752,13 @@ export class ToolRouter {
1517
1752
  // Safe Mode / compile failure", not the generic "stopped" the bare
1518
1753
  // status used to suggest.
1519
1754
  classification,
1520
- recoveryHint: bridgeStatusRecoveryHint(status),
1755
+ recoveryHint: bridgeStatusRecoveryHint(status, wedge),
1756
+ // specs/feedback.md 2026-08-14 — present ONLY when the editor is wedged.
1757
+ // `classification` keeps mirroring the instance lock verbatim (healthy |
1758
+ // reloading | dead_bridge | gone) so that contract is unchanged; the
1759
+ // wedge is carried by the tool's own `status` token plus this block, and
1760
+ // `ready` is false either way.
1761
+ ...(wedge !== null ? { wedged: wedge } : {}),
1521
1762
  instance: {
1522
1763
  lockPath: lockOnDisk,
1523
1764
  classification,
@@ -1535,10 +1776,15 @@ export class ToolRouter {
1535
1776
  isPlaying: pingBody?.isPlaying ?? null,
1536
1777
  unityVersion: pingBody?.unityVersion ?? null,
1537
1778
  bridgeVersion: pingBody?.bridgeVersion ?? null,
1779
+ wireContract: pingBody?.wireContract ?? null,
1538
1780
  mode: pingBody?.mode ?? null,
1539
1781
  }
1540
1782
  : { reachable: false },
1541
- nextStep: bridgeStatusNextStep(status),
1783
+ // specs/feedback.md 2026-08-24 — null when /ping was unreachable; a
1784
+ // `stale: true` block is the signal to reinstall the Unity package
1785
+ // rather than file a regression.
1786
+ wireContract: wireContractBlock(pingReachable, pingBody?.wireContract),
1787
+ nextStep: bridgeStatusNextStep(status, wedge),
1542
1788
  };
1543
1789
  // bridge_status never reports an error — even a stopped bridge is a
1544
1790
  // successful status read. _source=local because the synthesis happens in