From ff4b651246ff7476b4a750a567a5baf024919d63 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 24 Jul 2026 12:19:44 +0900 Subject: [PATCH 1/3] Document the decision to keep the dispatcher/runner split Record why uloop keeps its two-binary CLI architecture instead of consolidating into a single self-contained binary like Unity's official CLI (unity 1.0.0-beta.2 + com.unity.pipeline, investigated 2026-07-24): a self-describing catalog carries schemas but not runner-side behavior, the per-project runner pin dissolves cross-project version skew that a single global binary would reintroduce, and the equality-gated IPC protocolVersion depends on package and runner releasing together. Follows the decision-record format of security-scan-engine-decision.md and states the reversal condition (force-latest package installs). --- docs/dispatcher-runner-split-decision.md | 88 ++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/dispatcher-runner-split-decision.md diff --git a/docs/dispatcher-runner-split-decision.md b/docs/dispatcher-runner-split-decision.md new file mode 100644 index 000000000..eb8ba6291 --- /dev/null +++ b/docs/dispatcher-runner-split-decision.md @@ -0,0 +1,88 @@ +# Dispatcher / Project Runner Split Decision + +Date: 2026-07-24 + +## Decision + +uloop keeps its two-binary CLI architecture: a thin, globally installed dispatcher and a +per-project runner whose exact version is pinned by the Unity package +(`projectRunnerVersion` in `Packages/src/project-runner-pin.json`). We do not consolidate +into a single self-contained CLI binary in the style of Unity's official `unity` CLI +(`unity` 1.0.0-beta.2 + `com.unity.pipeline`, investigated 2026-07-24). + +## Context + +Unity's official CLI is a single global binary. Its command surface is self-describing: the +Editor-side `com.unity.pipeline` server exposes a schema-annotated command catalog over +`/api/commands`, and the CLI builds its commands (and MCP tools) dynamically from that +catalog on every connection. Version alignment between the CLI and the package is handled +by making the CLI the package's installer: `unity pipeline install` rewrites +`Packages/manifest.json` to the latest package version. No explicit protocol-version gate +between CLI and package was found in the shipped binaries. + +uloop already shares the self-describing half of this design: the runner fetches the tool +catalog (including user-defined custom tools) from the Unity package via `get-tool-details` +and caches it in `.uloop/tools.json`, with the embedded `default-tools.json` as a fallback +only. Custom tool authors therefore never depend on the CLI version. The question was +whether to adopt the other half as well — collapsing dispatcher and runner into one global +binary. + +## Rationale + +### A self-describing catalog carries schemas, not behavior + +The single-binary model holds only while the CLI remains a generic dispatcher that wraps +"command name + JSON parameters" in a transport envelope. The uloop runner already contains +client-side behavior that co-evolves with package features: pause-point wait loops, +transient-connection retry, post-compile warmup, test-status polling, progress display. +This class of logic cannot be delivered through a schema catalog — it is code. As +runner-only responsibilities keep growing, the distance from the generic-dispatcher model +grows with them; that trend is an argument against the single-binary model, not for it. + +### The split solves cross-project version skew + +One machine typically hosts several Unity projects, each pinning its own package version in +version control. A single global binary would have to stay compatible with every package +generation present on the machine at once — either by carrying an internal compatibility +matrix or by forcing lockstep upgrades across all projects. The pin architecture dissolves +the problem instead: each package release names the exact runner release it was built with, +and the dispatcher fetches that runner per project. Package and runner form a co-released +pair, so no compatibility matrix exists anywhere. + +Unity's CLI avoids the same problem from the opposite direction — the CLI dictates the +package version by force-writing latest into the manifest. That is workable for a beta +experiment but hostile to teams that pin package versions in version control. uloop's model +is the inverse (the package dictates the runner version), which matches how Unity projects +are actually operated. + +### The equality-gated protocol version depends on the split + +The IPC `protocolVersion` pair (see `docs/protocol-version.md`) is an exact-equality gate, +not a minimum-compatible range. Equality is only affordable because runner and package +release together and generation skew is structurally rare. A single global binary would +reintroduce skew by design and force range-based compatibility management — a strictly +heavier contract than the current one. + +### The dispatcher's cost is deliberately small + +The dispatcher's responsibilities are pin resolution, runner download, and self-update via +the pin's `minimumDispatcherVersion` floor (see `docs/project-runner-pin.md`). Keeping it +thin keeps global-install churn low; the price of the second binary is the pin machinery, +which is already built and documented. + +## Alternatives + +### Single self-contained CLI binary (Unity official CLI style) + +Rejected under current requirements. It would require either dropping support for +per-project package pinning (adopting Unity's force-latest install model) or maintaining a +cross-generation compatibility matrix inside one binary. Reversal condition: if uloop ever +commits to always forcing the package to latest and abandons per-project package pinning, +consolidation becomes viable and this decision should be revisited. + +### Keep the split but push more runner behavior into data-driven form + +Compatible with this decision and worth pursuing opportunistically: the more runner +behavior is generic or catalog-driven, the less often a package release needs a paired +runner release. This reduces coupling but cannot eliminate it, so it complements rather +than replaces the split. From 2a7b6b7215375605b46d4c3383d3f41552892b2a Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 24 Jul 2026 12:20:50 +0900 Subject: [PATCH 2/3] Move decision records into a numbered docs/adr/ directory Give architecture decision records a dedicated home instead of mixing them with runbooks and guardrail docs in the flat docs/ root. Files are numbered in decision-date order (0001 security scan engine, 0002 dispatcher/runner split); no existing references needed updating. --- .../0001-csharp-security-scan-engine.md} | 0 .../0002-dispatcher-runner-split.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{security-scan-engine-decision.md => adr/0001-csharp-security-scan-engine.md} (100%) rename docs/{dispatcher-runner-split-decision.md => adr/0002-dispatcher-runner-split.md} (100%) diff --git a/docs/security-scan-engine-decision.md b/docs/adr/0001-csharp-security-scan-engine.md similarity index 100% rename from docs/security-scan-engine-decision.md rename to docs/adr/0001-csharp-security-scan-engine.md diff --git a/docs/dispatcher-runner-split-decision.md b/docs/adr/0002-dispatcher-runner-split.md similarity index 100% rename from docs/dispatcher-runner-split-decision.md rename to docs/adr/0002-dispatcher-runner-split.md From d31af13e87dbc5b907dac690fb6f810e6a778fb4 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 24 Jul 2026 13:37:04 +0900 Subject: [PATCH 3/3] Sharpen the ADR's argument against the single-binary model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The force-latest manifest rewrite is avoidable by ordinary package discipline, so it is no longer presented as the structural objection. The refined objection is what remains once the package is pinned: aligning the CLI per project needs a recorded compatibility mapping, a mismatch gate, and per-project CLI selection — none of which the single-binary model provides, and all of which are exactly what the pin plus dispatcher implement. The latest-only install channel is explicitly excluded as a beta-stage limitation rather than a structural defect. --- docs/adr/0002-dispatcher-runner-split.md | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/adr/0002-dispatcher-runner-split.md b/docs/adr/0002-dispatcher-runner-split.md index eb8ba6291..5919bd8fb 100644 --- a/docs/adr/0002-dispatcher-runner-split.md +++ b/docs/adr/0002-dispatcher-runner-split.md @@ -50,10 +50,29 @@ and the dispatcher fetches that runner per project. Package and runner form a co pair, so no compatibility matrix exists anywhere. Unity's CLI avoids the same problem from the opposite direction — the CLI dictates the -package version by force-writing latest into the manifest. That is workable for a beta -experiment but hostile to teams that pin package versions in version control. uloop's model -is the inverse (the package dictates the runner version), which matches how Unity projects -are actually operated. +package version by force-writing latest into the manifest. The manifest rewrite itself is +not the structural objection: it only happens on `pipeline install`/`upgrade`, and the same +team discipline that governs every other package ("do not upgrade unilaterally") covers it. +The structural objection is what remains once the package IS pinned: someone must align the +CLI to the pinned package generation, per project, and the single-binary model offers no +mechanism for that: + +- No recorded compatibility mapping and no version gate. Nothing states which CLI + generation pairs with a given package version, and a mismatched pair is not even + detected — there is nothing to consult when trying to align, and no signal when + alignment is wrong. +- One global binary per machine. Projects pinned to different package generations would + need per-project CLI switching, which the model does not provide; a developer moving + between such projects cannot hold a correct pair for both at once. + +(The install channel currently offering only the latest CLI is deliberately not counted +here: that is read as a beta-stage limitation, and a stable release can be expected to +allow version-selected installs.) + +uloop's model is the inverse — the package dictates the runner version — and the pin plus +dispatcher is exactly the machinery that makes "align the CLI to the package" automatic: +the compatibility mapping is the pin (`projectRunnerVersion`), and per-project selection is +the dispatcher's job. This matches how Unity projects are actually operated. ### The equality-gated protocol version depends on the split