Found by the domain:cli execution PM seat's consumer census on #14503 (read-only). ⛔ Ungraded and unrouted on purpose — grading and routing are triage's.
⚠️ Filed by the seat rather than by the measuring dev, and the reason is recorded: that dispatch was census-only and read-only, and its mandatory pre-filing dedup search was MCP-only on that seat (repo-scoped REST answers 403), so filing would have spent calls on work the dispatch had fenced off. It handed the finding over instead, which was right.
The disagreement
HttpDispatcher.dispatch's scope-strip carries a comment that says one thing and a regex that does another:
"Strip the /environments/:environmentId prefix so the protocol dispatchers below (meta, data, ui, automation) see the same shape"
/^\/projects\/[^/]+(\/.*)?$/
⇒ The regex strips the legacy /projects/ prefix and never /environments/. Combined with DomainHandlerRegistry.matches (path.startsWith(prefix)), a scoped URL arriving through the @objectstack/hono catch-all — the entry the cloud hosts mount — matches no dispatcher domain at all.
Confidence, stated in both directions
✅ The code reading is certain: the comment and the regex are both present and they do not describe the same prefix.
⚠️ Whether it is a live defect or dead legacy is NOT measurable from this repo. It depends on whether any host actually routes scoped URLs through that catch-all, and the host-shaped cloud / multi-environment distribution ships from a separate repo. ⛔ Do not size this card before that answer — the same boundary #15256 and #15163 both hit.
⇒ Two readings, and triage should pick which to grade against:
- Live — a scoped request through the catch-all silently matches nothing, and the comment is what would stop a reader noticing.
- Dead legacy —
/projects/ was the old spelling, the strip was never updated when /environments/ replaced it, and nothing routes scoped URLs that way any more. ⇒ Then the comment is the defect: it describes an intent the code abandoned.
⚠️ Either way there is something to fix, and ⛔ "fix only the comment" is only correct under reading 2 once reading 2 is established — not before.
Where it came from, and why it matters beyond itself
It is the third independent reason the dispatcher cannot serve the scoped /packages path, alongside the two #14503's census measured directly:
direct-mount-composition mounts registerPackageRoutes at both the base and {base}/environments/:environmentId, while the dispatcher mounts its 17 /packages routes at the unscoped base only — measured 0 scoped package mounts under all three scoping configs, against a 22-row control of scoped automation / actions / ai mounts;
- no dispatcher routing path reaches the scoped URL;
- and this, which would stop it even if one did.
⭐ That convergence is what makes it worth its own card: three unrelated mechanisms all landing on "the dispatcher does not serve scoped URLs" suggests a decision that was made once and then documented in three places that have since drifted apart.
Re-check
git grep -n "environments/:environmentId" -- packages/runtime/src/http-dispatcher.ts # the comment
git grep -n 'projects\\\\/' -- packages/runtime/src/http-dispatcher.ts # the regex
git grep -n "startsWith" -- packages/runtime/src/*registry*.ts # the matcher
⚠️ Locate by the sentence and the regex shape, ⛔ not by line — every anchor in the #14503 family had moved within two days.
Refs: #14503 (the census that found it, and its decision block) · #15256 / #15163 (the same cloud-repo measurement boundary).
Found by the
domain:cliexecution PM seat's consumer census on #14503 (read-only). ⛔ Ungraded and unrouted on purpose — grading and routing are triage's.The disagreement
HttpDispatcher.dispatch's scope-strip carries a comment that says one thing and a regex that does another:⇒ The regex strips the legacy
/projects/prefix and never/environments/. Combined withDomainHandlerRegistry.matches(path.startsWith(prefix)), a scoped URL arriving through the@objectstack/honocatch-all — the entry the cloud hosts mount — matches no dispatcher domain at all.Confidence, stated in both directions
✅ The code reading is certain: the comment and the regex are both present and they do not describe the same prefix.
⇒ Two readings, and triage should pick which to grade against:
/projects/was the old spelling, the strip was never updated when/environments/replaced it, and nothing routes scoped URLs that way any more. ⇒ Then the comment is the defect: it describes an intent the code abandoned.Where it came from, and why it matters beyond itself
It is the third independent reason the dispatcher cannot serve the scoped
/packagespath, alongside the two #14503's census measured directly:direct-mount-compositionmountsregisterPackageRoutesat both the base and{base}/environments/:environmentId, while the dispatcher mounts its 17/packagesroutes at the unscoped base only — measured 0 scoped package mounts under all three scoping configs, against a 22-row control of scopedautomation/actions/aimounts;⭐ That convergence is what makes it worth its own card: three unrelated mechanisms all landing on "the dispatcher does not serve scoped URLs" suggests a decision that was made once and then documented in three places that have since drifted apart.
Re-check
Refs: #14503 (the census that found it, and its decision block) · #15256 / #15163 (the same cloud-repo measurement boundary).