Skip to content

Upgrade v2 - #292

Merged
elliotBraem merged 241 commits into
v2from
main
Sep 27, 2026
Merged

elliotBraem merged 241 commits into
v2from
main

Conversation

@elliotBraem

Copy link
Copy Markdown
Contributor

No description provided.

elliotBraem and others added 30 commits August 13, 2026 13:55
… to dashboard

- add _admin pathless layout gating on admin role; tenant admin dashboard and system pages render as children via Outlet
- rename authenticated /home route to /dashboard; update sidebar, mobile tabs, user nav, and login redirect fallbacks
- move apps and things routes under the public layout
- changeset: ui-layout-mounts
…, remove nostr

- move login from public layout into new _anon pathless layout that redirects authed users to /dashboard and provides theme toggle header
- rename organization route group from /organizations to /orgs; move invitation acceptance to /orgs/invites/$id
- remove stale nostr entry from authenticated sidebar
- changeset: ui-anon-orgs
…n origin

- Add per-account/gateway FastKV deploy lock (apps/<account>/<gateway>/lock/deploy.json)
  acquired before publish and released in a finally block. Stale or concurrent
  dispatches fail fast with status "locked" and a conflict payload listing owner,
  nonce, expires, and txHash. opt out with --no-deploy-lock.
- bos publish holds the lock for 10 minutes by default; bos deploy holds it for
  25 minutes to cover publish + Railway redeploy. Override with
  BOS_DEPLOY_LOCK_TTL_MS.
- Add bos deploy lock inspect|release for ops visibility and stuck-lock recovery.
- Add bos infra export that emits the resolved CI infra plan (env + services +
  account/gateway/project/generatedAt) so CI consumers stop duplicating port and
  DATABASE_URL knowledge from cli/infra.ts.
- Update .github/workflows/deploy.yml to consume the export and drop the
  hardcoded API_DATABASE_URL / AUTH_DATABASE_URL / CORS_ORIGIN env block.
- buildOriginMap now reads runtimeConfig.plugins[id].extendsRef and
  runtimeConfig.auth?.extendsRef instead of re-parsing raw bos.config.json.
- Tests for the lock helpers, the CI plan builder, the resolved-config origin
  lookup, and the per-command TTL resolution.

Co-authored-by: opencode <opencode@local>
Remove FastKV-backed deploy lock feature:
- Delete bos deploy lock acquire/release/inspect commands
- Remove lock logic from publishToFastKv
- Remove lockConflict from PublishResult and DeployResult schemas
- Concurrent deploys now follow last-write-wins semantics (harmless for Railway redeploy)

Keep bos infra export command:
- Emits {env, services, account, gateway, project, generatedAt} JSON
- buildOriginMap reads runtimeConfig.*.extendsRef (no raw JSON re-parse)

Tests: 263 pass, failures are pre-existing (auth types, template property)
Replace the legacy citynode model with three orthogonal services and a
matching UI surface.

API
- New NodesService: hierarchical node management with a recursive CTE
  for subtree lookup, parent-cycle prevention on create/update, and
  per-node validator scoping.
- New ValidatorsService (replaces CityNodesService): multi-validator per
  node, exact-one default enforcement via db.transaction, and
  resolveForStaking which descends the subtree first and walks
  ancestors as a fallback. resolveByAccountId looks up a single
  validator by its NEAR account id.
- Rewritten TenantsService: createBinding, verifyCustomDomain via HTTP
  HEAD, and setPrimaryBinding wrapped in db.transaction. authorizedTenant
  helper centralizes personal vs org tenant checks.

Robustness
- Shared toOrpcError extracted into api/src/lib/errors.ts so route
  handlers stop hand-typing error maps.
- isUniqueViolation(error) detects Postgres code 23505 instead of
  string-sniffing the message.
- HOSTNAME_REGEX validates DNS hostnames independently of NEAR
  account ids in createBinding and the binding preflight.

Schema
- Drop node_tags (orphaned by the citynode removal) and regenerate
  migrations as a single clean snapshot (4 tables: tenants, nodes,
  validators, domain_bindings). The self-referencing nodes.parent_id
  FK uses AnyPgColumn for the typed reference.

Host
- BindingResolver's TenantBinding interface gained tenantId so resolve
  flows can tie a hostname back to a tenant record. Mock updated to
  keep the integration test green.

UI
- Public landing directory reads listValidators instead of
  listLegacyCityNodes.
- Stake page: CityNodeCard -> ValidatorCard, query keys -> validators,
  resolver hooks -> resolveValidatorByAccountId.
- Admin tenants wizard rebuilt around nodes / validators / bindings,
  and tenant header affordances reference tenant.id instead of the
  now-defunct subdomain field.
- Landing page queries listRootNodes, renders root-node directory cards
  (name, kind badge, slug) with subdomain links + skeleton/empty states
- New public country page at /n/$slug aggregates children via
  listChildren and shows stake guidance from resolveStakingValidators
  (own validator CTA, or stake-to-city links for children with validators)
- Stake page rewritten around resolveStakingValidators: selectable
  validator list with isDefault pre-selected, role/protocol badges,
  community secondary styling, inherited-validator banner, no-validator
  fallback with child links. Node resolved from ?node= param or hostname.
  Stake transaction + onramp flows unchanged; broken admin CRUD removed.
- Rewrite admin tenant creation page from placeholder into full wizard:
  inline org creation (if no active org), node details (kind, cascading
  parent dropdown via listRootNodes + listChildren, slug, name), tenant +
  binding form with auto-generated hostname and live bindingPreflight
- On submit: createTenant → createNode → createBinding (blocking, with
  rollback via deleteNode + deleteTenant on failure), then non-blocking
  deploy steps for NEAR subaccount and registry config publish
- Export StepList + useStepper from @/components barrel
- Extend api/tests/setup.ts: optional plugins map param on getPluginClient,
  role param on orgContext (enables requireOrgRole in integration tests)
- Add api/tests/integration/wizard.test.ts: 5 tests covering full chain,
  rollback on duplicate hostname, nested hierarchy, preflight availability
…Nodes product

- Landing: 'What are CityNodes?' hero + root-node directory + Apply button
- New /apply route (external redirect to citynode.app/apply)
- Remove apps browser cruft (4 routes deleted, apps tab stripped from profile)
- Things index → typed DataTable demo
- Mobile responsive fixes (auth-shell double-padding, simple-header overflow)
- README + skill.md rewritten for CityNodes product surface
Update bos.config.json repository to point to NEARBuilders/citynode.app.
Align docs (AGENTS.md, CONTRIBUTING.md, LLM.txt, host/README.md) with the
remote auth plugin architecture and PostgreSQL migration. Clean up CI
postgres-template service and .env.example for new plugin databases.
Remove docker-compose.yml in favor of the Railway-backed deploy flow.
…ix contradictions

- Move 5 beta-v2 docs into plans/beta-v2/ (overview, composable, ui, tenants, native)
- Move extension plans into plans/extensions/ (ui-extends-ui-federation, client-runtime-plugins)
- Move infra plans into plans/infra/ (toml-infra-alchemy, orpc-v2-effect-migration)
- Move offline plans into plans/offline/ (shell-sw-caching, data-sync-queue)
- Move v1 plan into plans/v1-current/ (tenant-feature-completeness)
- Fetch full prototype source (85 files) from prototype/route-merging branch
- Consolidate prototypes under plans/prototypes/beta-v2/ and beta-v2-override/
- Fold react-native-migration.md into beta-v2/native.md, delete original
- Fix TOML/JSON publishing contradiction in composable.md
- Mark wayfinder tickets 01, 02 as RESOLVED; 06 as PARTIALLY RESOLVED
- Rewrite plans/README.md with new directory structure and ticket status table
- Update ~45 cross-references across all plan docs
- Add .gitignore exceptions for prototype source files (*.css, *.gen.ts)
…from source, drop postinstall

- Commit docker-compose.yml (was gitignored and generated after preflight,
  creating a chicken-and-egg where bun run dev exited before the file was
  written). Provisions postgres-api (5432/api_db) and postgres-auth
  (5433/auth_db); plugins isolate via plugin_<pluginId> schemas.
- Add paths to packages/everything-dev/tsconfig.json mapping every-plugin
  and subpath exports to source so bun's runtime resolver finds them
  without pre-built dist.
- Remove postinstall: bun run types:gen (dead code under ignore-scripts).
- Update AGENTS.md Quick Reference with docker compose step and plugin
  schema isolation docs.
- Add optional  to BosStagingSchema, use staging.account in
  publishToFastKv when --env staging (enables testnet account switching)
- Add staging block to bos.config.json: v1.citynode.testnet on
  testnet.citynode.app
- Deploy workflow: trigger from ci-main-success (not release-completed),
  exclude host via --packages, remove Postgres services and infra plan
- Staging workflow: use NEAR_TESTNET_PRIVATE_KEY, exclude host
- Release workflow: manual-only (remove ci-main-success trigger, remove
  docker and notify-deploy jobs)
- Docker workflow: remove push triggers on main/staging
- Update workflow README to document downstream flow
itexpert120 and others added 29 commits September 25, 2026 16:49
#198)

* feat(auth): event-linked Onboarding Codes with Organizers and a station read

Onboarding Codes now carry the Node Event id, keep the event name as a
display snapshot, and store the raw code encrypted with a key derived from
the auth secret. Codes for the same event feed one Event Team; team names no
longer take part in lookup. Owners, admins and members of a Team granted the
events Feature Area can create, list and revoke codes, and getOnboardingStation
returns the decrypted code of an active code to them. Redemption sets the
active organization without touching the Active Team, and fails with an
"organization is full" message at the membership limit, which citynode sets to
1000.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(api): create onboarding codes for a Node Event

createEventOnboardingCode loads the event, refuses non-event activities,
nodes whose tenant has no organization and organizations other than the
caller's active one, then creates the code through the auth plugin in-process
with the caller's headers and a default expiry of event end + 48h. Adds the
events Feature Area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): start onboarding from an event and run a fullscreen station

Event rows in the activity editor get "Start onboarding", which opens a
fullscreen station with a large QR on the Gateway Origin, a live joined count
and recent joiners. The org Onboard tab drops the free-text event form, shows
each code's state and reopens the station for active codes, and is visible to
Organizers. The onboarding page explains used-up codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(regression): onboarding page states for valid, expired, revoked and used-up codes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: address review findings on event onboarding

Fail createEventOnboardingCode with the contract's typed errors and a typed
failure for the event lookup, derive the public code flags from one code
state, and let an Organizer set the max joins when starting onboarding from an
event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): upcoming and past event timeline in the activity editor

Events on the node content page are split into Upcoming and Past tabs
and grouped by the viewer's local day, with a sticky date header, a
subtle timeline rail, and cards showing start time (plus event-local
time when the offset differs), organizer, venue, status and actions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): exit the onboarding station back to where it was opened

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(api): typecheck event onboarding route in scaffolded child projects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… a strict MF singleton (#189)

* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.
…iene (#197)

* refactor(everything-dev): static docker-compose — drop infra auto-provisioning

Closes #180

* refactor(ui): brand assets move to assets/brands/near with light/dark naming

Closes #181

* refactor(ui): non-shadcn components out of components/ui

components/ui is pure shadcn primitives now; app components live in
components/ (app-detail family in components/app-detail/).

Closes #182

* feat(everything-dev): init prunes unreferenced ui sources for ui-override children

Closes #183

* style: biome organize imports + format

* chore(lint): remove dead code flagged by biome

Deletes unused imports (schema.ts, build.ts), the unused
pluginDisplayName function, and flattenParent's never-read seen param;
auth-login gets the optional chain.
* add claude to gitignore

* chore(lint): register @shadcn/lint oxlint plugin

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): adopt shadcn base-maia preset (b3ZN5L2h44) on Base UI

Reinstall all ui primitives from the base-maia registry (Base UI, Phosphor
icons), self-host Inter/Geist/Geist Mono, adopt the preset radius scale,
and keep the local Badge success/warning variants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): citynode design tokens on oklch (ink primary, brand accent, status set)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ui): CityNode design system guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): migrate admin routes to Base UI + Phosphor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): restore __root.tsx sync header stripped by icon codemod

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(auth-ui): move plugin primitives to Base UI + Phosphor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(auth-ui): components.json so shadcn lint resolves the host theme

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): migrate public routes to Base UI + Phosphor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): migrate authenticated dashboard routes to Base UI + Phosphor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): migrate org and tenant routes to Base UI + Phosphor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): migrate shared components to Base UI + Phosphor

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): Button rendered as a link keeps link semantics

Base UI adds role=button when nativeButton is false; render non-button
elements through useRender with the button styles instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(ui): drop Radix and lucide, keep link content inside anchors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): larger controls and calmer page primitives

Controls default to 44px (sm 36, lg 48); PageContainer, PageHeader,
SectionHeader, EmptyState and InfoRow follow ui/DESIGN.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(ui): remove hover scale on header avatar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): add Switch and InputGroup primitives (base-maia)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): input-group lint suppressions and 44px height

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): clear shadcn lint in admin routes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(auth-ui): clear shadcn lint, sync primitives with core ui

Sync plugin Button/Input/InfoRow/EmptyState with core ui (44px controls,
ButtonLink). Replace arbitrary radii/text sizes, retro outset borders and
uppercase micro-labels with scale values and semantic tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): public routes and root on design tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): stake route on design tokens and primitives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): dashboard, nodes and onboarding drop retro styling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): things routes on design tokens and primitives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): clear shadcn lint in org, tenant and staking POC routes

Remove retro borders, hover shadows and uppercase micro-labels; use scale
values, semantic tokens and component variants. Native selects keep their
test ids and now match the Input primitive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): use shadcn's cn package instead of clsx + tailwind-merge

Primitives import cn from "cn" as the registry ships them; @/lib/utils
re-exports it for app code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(ui): document restyle allowances

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(auth-ui): native controls to design-system primitives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): native controls to primitives in node selector, stake, things, onboarding

Managed-node select becomes Select, validator list becomes a RadioGroup of
choice cards, things/new uses Field, stray buttons become Button, and ids,
slugs and JSON payload inputs get font-mono.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): admin routes use design-system form primitives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): org, tenant and staking POC controls on shadcn primitives

Replace native selects, buttons and the validator table with Select,
Button and Table; wrap form inputs in Field/FieldLabel; restore
font-mono on inputs holding account ids, URLs and hashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(ui): shared components pass shadcn lint

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): shared components use design-system primitives for native controls

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): Item primitive; explorer rows use Item instead of a native button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(orgs): keep org page state when leaving the active team

Make the org page tab URL-addressable (validated ?tab= search param,
default members, replace navigation) so tab state no longer lives in
component state that a client re-render discards. Replace the native
team add-member select with the Base UI Select and clear shadcn lint in
the teams tab and team card. The regression spec opens the Teams tab via
?tab=teams instead of racing a click against hydration, and asserts the
tab survives removing yourself from the active team.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(lint): enforce @shadcn/lint rules on the UI

no-restyle (layout only; containers may add spacing, inputs/badges may use
font-mono), no-raw-colors, no-arbitrary-values and no-unknown-classes as
errors; no-inline-styles and require-static-classes as warnings. Vendored
primitives in components/ui are exempt from restyle, arbitrary-value and
unknown-class checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(discover): Manage button hover matches its size

The button stretched to fill its grid cell; align the button itself
instead of its content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): hydration-safe LocalDate

Dates format only on the client so SSR (UTC) and the browser's timezone
never disagree during hydration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): replace Built on NEAR badge with a plain footer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /apply as a 4-step stepper

Organization, NEAR account, DAO and Details as one guided flow: completed
steps collapse to a check row, the next step is the only open one, and the
success state links to Proposals. ConnectDao becomes a single row with
purpose-specific copy (optional purpose/variant props; onVerified unchanged).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /things/$thingId

Status badge, payload and details sections, admin delete in a confirmed
danger zone, proper not-found state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /things/live

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /things

Searchable rows with type, upvotes and relative update time; empty and
error states with a next action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /things/new

Inline JSON validation and format; only offer sign-in when the API
actually rejects the session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild admin layout

Drop the in-page tenant header, stat strip and AdminNav button bar (the
sidebar carries admin sub-nav); keep a one-line relayer funding notice
and shared admin row, stat and menu helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin overview

Big figures (pending, nodes, tenants, relayer), a review work queue,
manage rows with the existing admin.heading.* ids and a quiet runtime
context list carrying the admin.stat.* ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild landing page and community directory

Hero with one sentence and two actions, live community counts, a directory
preview that opens public community pages, and a three-step how it works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /login and ship the auth plugin stylesheet

Sign in to CityNode with passkey primary, NEAR secondary, phone tertiary and
passkey sign-up; phone pairing as a focused QR view. The plugin styles.css is
now imported from the route root and layered under the core utilities.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild Home as next steps

Home (/dashboard) now leads with state-driven next steps (invitations to
accept inline, create/choose an organization, start or open a community,
community settings for owners, admin queue, stake) and a compact identity
card linking to Settings, replacing the identity dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /n/$slug community page

Header with kind, location and summary, a same-origin Stake NEAR action,
key stats, upcoming events, local communities as cards, staking pools,
and a not-found state that leads back to Explore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin/proposals list and review

List: labelled status tabs, count, proposal titles from the payload,
relative dates, stacked rows on mobile. Review: a focused decision page
with the application first, a sticky decision panel (ConnectDao-gated
approve, reject behind a reason dialog), outcome state and a compact
recent-decisions list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /explore

Search first, then filters and a list/map switch (?view=map). Communities
are cards with location, status and next event; the preview sheet leads
with Open community and lists events as dated rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /orgs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /orgs/new

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /orgs/invites/$id

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /orgs/$slug with row menus, inline invite and URL tabs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(ui): split node lifecycle POC logic into -poc-* modules

Queries, model and signed actions move verbatim into usePocLifecycle;
the pre-sign chain checks and the step runner become factories. No
behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /activity/$activityId event page

Date tile and title up front, date/time and venue as a quiet list, one
outbound action (Register on Luma / Event details / Read original post),
a Hosted by link to the community page, and not-found and error states.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /$accountId profile

Avatar, name and account id as the header, description and links as
outline buttons, and a clean No profile yet state with Explore and
NearBlocks actions for accounts without a NEAR profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin/nodes list and detail

List: page header, scope tabs, kind filter and name/slug search, tenant
status badges, stacked rows on mobile. Detail: header with parent link
and Edit details, big counts, URL tabs (overview, validators, domains,
profile); validators and domains as rows with confirmed row-menu
actions; network/protocol behind a disclosure; raw metadata collapsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild the app shell around task-first navigation

One shell for signed-in users on every page (public pages and settings
included), sidebar ordered Home, Explore, Stake, My community (with
Overview, Events & profile, Onboarding, Proposals, Community settings),
Organization, Things, Curate (curators/admins), Admin (with sections).
Settings, Docs and the theme toggle live in the sidebar footer; the
account menu is identity, profile, Settings, sign out. Named breadcrumbs
replace raw path segments. Anonymous header: logo, Explore, Stake, Docs,
Sign in. Error and not-found pages offer a way home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /about as a docs page

Docs header with Open agent skill as the one primary action, the README
as readable prose, and a Build on it side column with skill, skill.md,
repository and runtime details.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /skill as a docs page

Copy prompt as the primary action, skill.md secondary, the Intent load
command as a copyable line, then the prompt rendered as prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): simplify dashboard layout and replace the under-construction GIF

The dashboard layout now only guards feature areas (the authenticated
layout owns the shell). The under-construction badge becomes a quiet
dashed tile. The navigation progress bar uses the brand color, and the
theme-color meta follows the color scheme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): cover explore cards and list/map view switch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /onboard as a focused 3-step join

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /login/device and device approval

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /settings as a left-nav settings layout

Profile, sign-in methods, API keys and security as rows with dialogs and
confirmed destructive actions; API keys created and revealed in dialogs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild the onboarding station

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /tenant/$tenantId as Community settings

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): community-settings DAO copy on the org Community tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): check curator access once per session in the sidebar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin/tenants list

Page header with Create tenant, status tabs and search, success/warning
status badges, local dates, stacked rows on mobile, filtered-empty state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin/tenants/new as a stepper

Four steps (organization, owning DAO, community, review) with completed
steps collapsed to a check row and Change actions; the deploy phase is a
three-step progress (records, DAO publish, Trezu approval) ending in an
Open community settings action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): chips use the new brand and muted tokens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild node lifecycle POC as a guided walkthrough

Phase stepper, one station at a time with a single primary action,
acting lens as a segmented control, collapsible setup, chain state in
tabs and the chain log in a side sheet. All poc-* test ids, mutations
and on-chain calls unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(auth-ui): compile the plugin stylesheet with Tailwind and load it per route

The folder-form ui build runs from the plugin root, so ui/postcss.config.mjs
was never picked up and styles.css shipped raw @tailwind directives. Add a
root postcss config, reference the default theme inline so spacing/size
utilities resolve, and import the sheet from the route layouts (async CSS)
instead of the root, which collided with the entry style.css.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /stake as directory, pool and amount

Community cards lead to a two-column stake page: pick a pool (with live
fee and total staked), enter an amount with quick picks, and connect a
wallet inline. Protocol jargon removed; not-found and no-validator states
give a way forward. Pool cards show big figures; the public node stake
section links to same-origin /stake?nodeId=.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin/relayer

Status page: header with refresh, status badge, big balance figures,
funding prompt with the account when empty, an Add funds form with an
amount + NEAR input group and presets, recent relays as rows with
relative times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(auth-ui): cover the login methods, passkey sign-up and onboard join steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild /admin/system

Status page with env/network badges in the header and runtime,
deployment and endpoint sections as quiet rows (admin.heading.* kept).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): round relayer NEAR figures on admin pages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(admin): remove AdminNav and StatCard (replaced by sidebar sub-nav)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild My community overview and proposals

My community gets a named header with the community site link, a URL tab
nav (Overview, Events & profile, Onboarding, Proposals, Community settings
for owners), big stats, a Coming up list with onboarding entry, team stake
as a figure, validators as rows, and sub-communities as rows. Proposals are
rows with status badges, relative dates, details in a sheet, approve as the
one primary action and reject behind a row menu with confirmation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(orgs): confirm team deletion in-app instead of window.confirm

ConfirmDialog gets sentence-case labels, a clear primary/ghost pair and
test ids; the team-workspace spec confirms through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild Events & profile with an Onboarding tab

/nodes/$nodeId/content validates ?tab=events|profile|onboarding and drives
its tabs from it (replace navigation). It shares the My community tab nav;
Events is an action row (Add event, Share a post, Luma behind a dialog),
Upcoming/Past line tabs and posts as rows, with edit in a grouped sheet and
Start onboarding in a row menu. Profile is a sectioned form with the Explore
switch first. The new Onboarding tab lists live stations (open, close with
confirmation) and upcoming events with Start onboarding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): let long account ids wrap on the profile header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): one primary stake action on the community page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): rebuild Curate

/discover becomes Curate: big stats, line tabs, communities as rows with
Featured / Needs attention badges and a Manage sheet (visitor checklist,
compact feature form, stop featuring behind a confirm, recent changes).
Reports are rows with the reason and age; resolving happens in a dialog
with a keep/hide choice. Team lists curators as people (avatar, name,
muted id) with a people search + inline Add, and Remove access lives in a
row menu with confirmation. DiscoveryAction buttons size to their label and
report success with a toast; Report a problem opens a dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): format activity card times on the client only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): left-align settings nav and hide temporary emails

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): breadcrumb names the Events & profile tab you're on

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): consistent page titles from the runtime app name

Replace leftover "| app" titles with pageTitle(label, runtimeConfig).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(admin): tell non-admins why they landed on Home

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: changeset for the CityNode design system rebuild

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(stake): browse staking pools without signing in

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dao): show a readable message when Trezu can't be reached

connectDaoAccount stored the raw connector error ("Wallet not found");
route it through describeDaoError.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): event date headings stick below the app header

Sticky date groups used top-0/z-10 inside the app shell's scroller and
slid over the sticky header. The shell sets --sticky-offset; headings
stick at top-sticky-offset beneath it, and the stuck observer uses the
same offset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): theme toggle moves from the sidebar to the top bar

Sits just left of the account menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ui): core stylesheet scans the auth plugin's sources

Plugin-only responsive classes (settings layout) lived only in the
plugin's utilities.auth sublayer, where they lose to core utilities.
Generating them in the core sheet keeps a single, correctly ordered
utilities layer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): sticky, polished public header with a wordmark logo

The header pins to the top with a blurred translucent background, the
current section gets an active state, and the logo drops its icon.
Public pages set their own --sticky-offset so sticky content sits below.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ui): improve and fix crashes

* fix(ui): always render the landing page — drop signed-in redirect from /

* refactor(ui): address review — restore NEAR branding, move Chip to ui, drop dead app-detail components

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Elliot Braem <elliot@ejlbraem.com>
…i/tenant subpath (#221)

* feat(everything-dev,ui): tenant draft/url helpers as everything-dev/ui/tenant subpath

Closes #184. The tenant origin construction (buildTenantUrl, tenantLabel,
isLocalHostname), the node-config draft helpers (schema, diff, bundle entry
resolution, sha384 integrity preflight), and the new gatewayForAccount merge
into one framework module exposed as everything-dev/ui/tenant. The app-owned
ui/src/lib/tenant-url.ts and tenant-config-draft.ts copies are deleted; call
sites import from the package, so children stop receiving the copies via
bos init and versions flow through the catalog / changeset release.

gatewayForAccount derives the gateway for an owner account from the runtime
config — the runtime's gateway when the account is on the runtime's network,
null otherwise — replacing the hardcoded per-network domain checks the
PR 177 review flagged in fresh's spawn flow.

* fix(everything-dev): keep the ui dist free of node builtins — external react + dead createRequire strip

The everything-dev dist's shared rolldown runtime chunk retained a dead
`import { createRequire } from "node:module" ` (injected for CLI-side
interop, unused after treeshake) — and the new ui-surface dist entries
(hydrate/router/tenant) import that chunk, so client builds of the ui
subpaths failed with "node:* is a built-in Node.js module and cannot be
imported in client-side code". react/react-dom join neverBundle (they are
MF shared singletons; bundling them dragged CJS-interop helpers into the
dist), and the build strips the dead import from the shared chunk when it
is genuinely unused.

* refactor(ui,everything-dev): tenant surface through @/app; createRequire conversions at source

Review rework (PR 221): app call sites import the tenant surface from @/app
(app.ts re-exports everything-dev/ui/tenant) instead of reaching past the
app surface; dao-policy/dao-connect's TenantUiOverride re-export chain is
cut. The dist's dead createRequire import is eliminated at source —
banner.ts reads package.json via readFileSync(new URL(...)) and init.ts
statically imports tar (committed dep) instead of a dynamic require — so
the rolldown runtime chunk never receives node:module and the emitted-chunk
stripper is deleted.

* fix(everything-dev): tar ships an ESM build — named extract import

* style: biome import-name sort in the ported tenant-url call sites
* feat(everything-dev,ui): tenant draft/url helpers as everything-dev/ui/tenant subpath

Closes #184. The tenant origin construction (buildTenantUrl, tenantLabel,
isLocalHostname), the node-config draft helpers (schema, diff, bundle entry
resolution, sha384 integrity preflight), and the new gatewayForAccount merge
into one framework module exposed as everything-dev/ui/tenant. The app-owned
ui/src/lib/tenant-url.ts and tenant-config-draft.ts copies are deleted; call
sites import from the package, so children stop receiving the copies via
bos init and versions flow through the catalog / changeset release.

gatewayForAccount derives the gateway for an owner account from the runtime
config — the runtime's gateway when the account is on the runtime's network,
null otherwise — replacing the hardcoded per-network domain checks the
PR 177 review flagged in fresh's spawn flow.

* fix(everything-dev): keep the ui dist free of node builtins — external react + dead createRequire strip

The everything-dev dist's shared rolldown runtime chunk retained a dead
`import { createRequire } from "node:module" ` (injected for CLI-side
interop, unused after treeshake) — and the new ui-surface dist entries
(hydrate/router/tenant) import that chunk, so client builds of the ui
subpaths failed with "node:* is a built-in Node.js module and cannot be
imported in client-side code". react/react-dom join neverBundle (they are
MF shared singletons; bundling them dragged CJS-interop helpers into the
dist), and the build strips the dead import from the shared chunk when it
is genuinely unused.

* feat(ui): read stake-pool contracts through near-kit view calls

Closes #185. The hand-rolled JSON-RPC view-function wrapper (raw fetch
against the public RPC endpoints, base64 args, manual Zod byte-array
decoding — near-rpc.ts) is deleted. The stake-pool query call sites use
near-kit's Near.view per network instead, with failure parity: unsupported
networks, timeouts, and malformed results resolve null and fall into the
same clean query error state. Test mocks move to the near-kit boundary.

* style(ui): biome format stake-pool test mock chain

* refactor(ui,everything-dev): tenant surface through @/app; createRequire conversions at source

Review rework (PR 221): app call sites import the tenant surface from @/app
(app.ts re-exports everything-dev/ui/tenant) instead of reaching past the
app surface; dao-policy/dao-connect's TenantUiOverride re-export chain is
cut. The dist's dead createRequire import is eliminated at source —
banner.ts reads package.json via readFileSync(new URL(...)) and init.ts
statically imports tar (committed dep) instead of a dynamic require — so
the rolldown runtime chunk never receives node:module and the emitted-chunk
stripper is deleted.

* fix(everything-dev): tar ships an ESM build — named extract import

* refactor(ui,better-near-auth): per-network near clients through the auth client

Review rework (PR 222): better-near-auth's near atom gains
getNearClient(network?) — a network whose connector-bound client isn't
initialized gets a wallet-less Near cached on demand (public read-only
view calls need no wallet; initClientForNetwork upgrades the slot when
that network initializes). The stake-pool factories take the app auth
client and read contracts through authClient.near.getNearClient(network);
the module-local viewClients Map is deleted; pool networks narrow through
toNetwork. Timeouts follow the transport defaults (near-kit view has no
AbortSignal option — parity on failure-to-null holds).

* style: biome organize imports

* style: biome import-name sort in the ported tenant-url call sites

* fix(ui): PoolSummary reads pools through the near-kit view client — toNetwork narrowing + authClient
…v/ui subpaths (#224)

* feat(everything-dev,ui): tenant draft/url helpers as everything-dev/ui/tenant subpath

Closes #184. The tenant origin construction (buildTenantUrl, tenantLabel,
isLocalHostname), the node-config draft helpers (schema, diff, bundle entry
resolution, sha384 integrity preflight), and the new gatewayForAccount merge
into one framework module exposed as everything-dev/ui/tenant. The app-owned
ui/src/lib/tenant-url.ts and tenant-config-draft.ts copies are deleted; call
sites import from the package, so children stop receiving the copies via
bos init and versions flow through the catalog / changeset release.

gatewayForAccount derives the gateway for an owner account from the runtime
config — the runtime's gateway when the account is on the runtime's network,
null otherwise — replacing the hardcoded per-network domain checks the
PR 177 review flagged in fresh's spawn flow.

* fix(everything-dev): keep the ui dist free of node builtins — external react + dead createRequire strip

The everything-dev dist's shared rolldown runtime chunk retained a dead
`import { createRequire } from "node:module" ` (injected for CLI-side
interop, unused after treeshake) — and the new ui-surface dist entries
(hydrate/router/tenant) import that chunk, so client builds of the ui
subpaths failed with "node:* is a built-in Node.js module and cannot be
imported in client-side code". react/react-dom join neverBundle (they are
MF shared singletons; bundling them dragged CJS-interop helpers into the
dist), and the build strips the dead import from the shared chunk when it
is genuinely unused.

* feat(ui): read stake-pool contracts through near-kit view calls

Closes #185. The hand-rolled JSON-RPC view-function wrapper (raw fetch
against the public RPC endpoints, base64 args, manual Zod byte-array
decoding — near-rpc.ts) is deleted. The stake-pool query call sites use
near-kit's Near.view per network instead, with failure parity: unsupported
networks, timeouts, and malformed results resolve null and fall into the
same clean query error state. Test mocks move to the near-kit boundary.

* style(ui): biome format stake-pool test mock chain

* feat(everything-dev,ui): absorb framework UI files into everything-dev/ui subpaths

Closes #186. hydrate (client bootstrap), router-client (client router
factory), router-server (SSR router module), entry (web entry runner), and
router-error (generic error boundary) move into the framework package as
new ui subpaths. Child copies shrink to thin wiring stubs that inject only
the app's generated artifacts (routeTree.gen, routeConfig.gen, styles.css)
and stay framework-owned for sync. The framework modules import the
package's own api/auth/runtime/manifest surfaces; the hydrate suite ports
to the package keeping the compose digest-parity fallback coverage.
globals.d.ts remains a sync-owned copy — ambient declarations cannot be
re-exported through stubs. RouterContextWithApi gains the optional
authClient the routers already threaded.

* refactor(ui,everything-dev): tenant surface through @/app; createRequire conversions at source

Review rework (PR 221): app call sites import the tenant surface from @/app
(app.ts re-exports everything-dev/ui/tenant) instead of reaching past the
app surface; dao-policy/dao-connect's TenantUiOverride re-export chain is
cut. The dist's dead createRequire import is eliminated at source —
banner.ts reads package.json via readFileSync(new URL(...)) and init.ts
statically imports tar (committed dep) instead of a dynamic require — so
the rolldown runtime chunk never receives node:module and the emitted-chunk
stripper is deleted.

* fix(everything-dev): tar ships an ESM build — named extract import

* refactor(ui,better-near-auth): per-network near clients through the auth client

Review rework (PR 222): better-near-auth's near atom gains
getNearClient(network?) — a network whose connector-bound client isn't
initialized gets a wallet-less Near cached on demand (public read-only
view calls need no wallet; initClientForNetwork upgrades the slot when
that network initializes). The stake-pool factories take the app auth
client and read contracts through authClient.near.getNearClient(network);
the module-local viewClients Map is deleted; pool networks narrow through
toNetwork. Timeouts follow the transport defaults (near-kit view has no
AbortSignal option — parity on failure-to-null holds).

* style: biome organize imports

* refactor(everything-dev): SSR router goes generic over the app route tree — shared router defaults

Review rework (PR 224): createServerRouterModule is generic over
TRouteTree extends AnyRoute — the app stub passes typeof routeTree, the
composed-tree casts narrow through the app's tree type, and the
'as unknown as AnyRouter' boundary cast is gone (a concrete tree satisfies
the contract directly). The unused errorComponent override param is
dropped (RouterError is the framework-owned boundary). The fallback
components and QueryClient defaults dedupe into ui/router-defaults,
shared by the client factory, the SSR module, and the hydrator.

* style: biome import-name sort in the ported tenant-url call sites

* fix(ui): PoolSummary reads pools through the near-kit view client — toNetwork narrowing + authClient

* style: biome format the framework router-error
…011) (#228)

* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.

* docs: clean up plan archives, wayfinder tickets, and phantom references

- delete .scratch/orpc-v2-migration (numbers.env issue map, unreferenced)
- plans/README.md: drop phantom ui-route-grafting-migration file link
  (kept as plain text + issue #108), prototype count 3→4, fix
  03-plugin-type-deps typo
- recreate wayfinder decision tickets 08–12 as decision-record stubs
  (decisions already recorded in README/map) and refresh ticket counts
- ADR 0007: replace dead ui-route-grafting-migration links with inline
  supersession note (ADR 0008); fix stale plan-035 link in ADR 0007
  platform-bundle-storage; point ADR 0006 at the archived plan 022
- advisor-plans: renumber wallet/teams batch to 044–048 (resolves 025–029
  and 028 collisions), add README rows for 044–048 (DONE via PR #136) and
  030–032 (TODO, unexecuted), archive DONE/superseded/absorbed plans to
  done/ with a slim done/README.md preserving status notes, prune
  dependency notes that only concern archived plans

* docs: fix plans/README.md staging miss from the cleanup commit

* fix(deploy): hermetic boot — the image consumes what it stages (ADR 0011)

A self-contained runtime image fetched its own plugin manifests through
its own public origin at boot; when that origin 502'd (Railway restart
loop, gateway hiccup, cold start ordering) nothing could ever come up —
the boot depended on the thing it was booting. Registry claims survived
container restarts via PID reuse and wedged the pinned host port, making
the loop permanent.

- BundleResolver service (Effect v4, Context.Service + Layer) resolves
  own-namespace /bundles/<account>/<gateway>/ URLs from BOS_BUNDLE_DIR;
  foreign namespaces and unset dirs fall through to the network fetch,
  so registry-tier children are unchanged
- One global-fetch adapter (ManagedRuntime bridge) installed at CLI boot
  covers every boot-time consumer: config manifest discovery, contract
  types, orchestrator host loading, MF remoteEntry probes
- bos start resolves configs with production env when NODE_ENV=production
  (kills the dev-mode raw-config load against the stripped image)
- Remote-source services no longer allocate ports; stale claims are
  pruned via recorded process generation; pinned-port conflicts verify a
  real listener; InfraError/DevStepError report instead of escaping
- bos.config.json carries the deterministic post-publish bundle URLs
- Dockerfile healthcheck start-periods cover cold-boot duration

Verified: runtime image boots healthy with citynode.app blackholed and
recovers across docker restart; typecheck 9/9, lint clean, 631 framework
tests green.

* ci: relocate the remote-runtime smoke to the Deploy workflow (ADR 0009 §4)

runtime-remote.test.ts is a live-network smoke: it reads the branch's raw
bos.config.json and proxies real traffic (/skill.md, /llms.txt) to
config.app.ui.production. Once #228 commits the deterministic bundle URLs,
that origin is the deployment this PR ships — a PR CI suite that depends on
production's uptime fails whenever the gateway blips, and before the deploy
it validates URLs that cannot serve yet (Host tests red: "expected 502 to be
200", 60s hook timeouts).

The split was already prepared but never wired: vitest.remote.config.ts owns
exactly this file and host has a test:integration:remote script with no
consumer, while the default vitest.config.ts include swept it into PR CI.

- Default host suite excludes tests/integration/runtime-remote.test.ts
  (configDefaults preserved) — PR CI no longer touches live origins
- deploy.yml runs the smoke after mf check: boots the real host program
  against the live production URLs — SSR compose from the deployed ui,
  doc-asset proxy, api ping, root document contract — the functional gate
  beyond mf check's identity-only check

No helper changes, deliberately: in the Deploy context, non-2xx origins
make the smoke fail loudly — correct for a post-deploy gate; a down site is
already caught by mf check's retry loop.

* fix(host-tests): pin the runtime-config fixture to development env

loadTestRuntimeConfig called loadResolvedConfig() with no env — with the
NODE_ENV-honoring default (hermetic boot), the host test script's
NODE_ENV=production resolved production env in the fixture: api source
remote → manifest discovery fetched the deployed origin (down, 10s×3 retries)
→ every beforeAll hook blew the 10s timeout → 6 suites failed at collection
(csp-compose, csp-nonce, security, seo, ssr-metadata, ssr).

Fixtures resolve the repo's own local workspaces; NODE_ENV=production in the
host test script exists for the host's production code paths, not for config
resolution. Pin env: "development" — live-origin coverage belongs to the
Deploy workflow's smoke (relocated earlier in this PR).
…listener authority) (#229)

* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.

* docs: clean up plan archives, wayfinder tickets, and phantom references

- delete .scratch/orpc-v2-migration (numbers.env issue map, unreferenced)
- plans/README.md: drop phantom ui-route-grafting-migration file link
  (kept as plain text + issue #108), prototype count 3→4, fix
  03-plugin-type-deps typo
- recreate wayfinder decision tickets 08–12 as decision-record stubs
  (decisions already recorded in README/map) and refresh ticket counts
- ADR 0007: replace dead ui-route-grafting-migration links with inline
  supersession note (ADR 0008); fix stale plan-035 link in ADR 0007
  platform-bundle-storage; point ADR 0006 at the archived plan 022
- advisor-plans: renumber wallet/teams batch to 044–048 (resolves 025–029
  and 028 collisions), add README rows for 044–048 (DONE via PR #136) and
  030–032 (TODO, unexecuted), archive DONE/superseded/absorbed plans to
  done/ with a slim done/README.md preserving status notes, prune
  dependency notes that only concern archived plans

* docs: fix plans/README.md staging miss from the cleanup commit

* fix(deploy): hermetic boot — the image consumes what it stages (ADR 0011)

A self-contained runtime image fetched its own plugin manifests through
its own public origin at boot; when that origin 502'd (Railway restart
loop, gateway hiccup, cold start ordering) nothing could ever come up —
the boot depended on the thing it was booting. Registry claims survived
container restarts via PID reuse and wedged the pinned host port, making
the loop permanent.

- BundleResolver service (Effect v4, Context.Service + Layer) resolves
  own-namespace /bundles/<account>/<gateway>/ URLs from BOS_BUNDLE_DIR;
  foreign namespaces and unset dirs fall through to the network fetch,
  so registry-tier children are unchanged
- One global-fetch adapter (ManagedRuntime bridge) installed at CLI boot
  covers every boot-time consumer: config manifest discovery, contract
  types, orchestrator host loading, MF remoteEntry probes
- bos start resolves configs with production env when NODE_ENV=production
  (kills the dev-mode raw-config load against the stripped image)
- Remote-source services no longer allocate ports; stale claims are
  pruned via recorded process generation; pinned-port conflicts verify a
  real listener; InfraError/DevStepError report instead of escaping
- bos.config.json carries the deterministic post-publish bundle URLs
- Dockerfile healthcheck start-periods cover cold-boot duration

Verified: runtime image boots healthy with citynode.app blackholed and
recovers across docker restart; typecheck 9/9, lint clean, 631 framework
tests green.

* docs(adr): amend 0011 (outbound local-first) and 0012 (claim identity, listener authority)

* ci: relocate the remote-runtime smoke to the Deploy workflow (ADR 0009 §4)

runtime-remote.test.ts is a live-network smoke: it reads the branch's raw
bos.config.json and proxies real traffic (/skill.md, /llms.txt) to
config.app.ui.production. Once #228 commits the deterministic bundle URLs,
that origin is the deployment this PR ships — a PR CI suite that depends on
production's uptime fails whenever the gateway blips, and before the deploy
it validates URLs that cannot serve yet (Host tests red: "expected 502 to be
200", 60s hook timeouts).

The split was already prepared but never wired: vitest.remote.config.ts owns
exactly this file and host has a test:integration:remote script with no
consumer, while the default vitest.config.ts include swept it into PR CI.

- Default host suite excludes tests/integration/runtime-remote.test.ts
  (configDefaults preserved) — PR CI no longer touches live origins
- deploy.yml runs the smoke after mf check: boots the real host program
  against the live production URLs — SSR compose from the deployed ui,
  doc-asset proxy, api ping, root document contract — the functional gate
  beyond mf check's identity-only check

No helper changes, deliberately: in the Deploy context, non-2xx origins
make the smoke fail loudly — correct for a post-deploy gate; a down site is
already caught by mf check's retry loop.

* fix(host-tests): pin the runtime-config fixture to development env

loadTestRuntimeConfig called loadResolvedConfig() with no env — with the
NODE_ENV-honoring default (hermetic boot), the host test script's
NODE_ENV=production resolved production env in the fixture: api source
remote → manifest discovery fetched the deployed origin (down, 10s×3 retries)
→ every beforeAll hook blew the 10s timeout → 6 suites failed at collection
(csp-compose, csp-nonce, security, seo, ssr-metadata, ssr).

Fixtures resolve the repo's own local workspaces; NODE_ENV=production in the
host test script exists for the host's production code paths, not for config
resolution. Pin env: "development" — live-origin coverage belongs to the
Deploy workflow's smoke (relocated earlier in this PR).
…ct service (#230)

* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.

* docs: clean up plan archives, wayfinder tickets, and phantom references

- delete .scratch/orpc-v2-migration (numbers.env issue map, unreferenced)
- plans/README.md: drop phantom ui-route-grafting-migration file link
  (kept as plain text + issue #108), prototype count 3→4, fix
  03-plugin-type-deps typo
- recreate wayfinder decision tickets 08–12 as decision-record stubs
  (decisions already recorded in README/map) and refresh ticket counts
- ADR 0007: replace dead ui-route-grafting-migration links with inline
  supersession note (ADR 0008); fix stale plan-035 link in ADR 0007
  platform-bundle-storage; point ADR 0006 at the archived plan 022
- advisor-plans: renumber wallet/teams batch to 044–048 (resolves 025–029
  and 028 collisions), add README rows for 044–048 (DONE via PR #136) and
  030–032 (TODO, unexecuted), archive DONE/superseded/absorbed plans to
  done/ with a slim done/README.md preserving status notes, prune
  dependency notes that only concern archived plans

* docs: fix plans/README.md staging miss from the cleanup commit

* fix(deploy): hermetic boot — the image consumes what it stages (ADR 0011)

A self-contained runtime image fetched its own plugin manifests through
its own public origin at boot; when that origin 502'd (Railway restart
loop, gateway hiccup, cold start ordering) nothing could ever come up —
the boot depended on the thing it was booting. Registry claims survived
container restarts via PID reuse and wedged the pinned host port, making
the loop permanent.

- BundleResolver service (Effect v4, Context.Service + Layer) resolves
  own-namespace /bundles/<account>/<gateway>/ URLs from BOS_BUNDLE_DIR;
  foreign namespaces and unset dirs fall through to the network fetch,
  so registry-tier children are unchanged
- One global-fetch adapter (ManagedRuntime bridge) installed at CLI boot
  covers every boot-time consumer: config manifest discovery, contract
  types, orchestrator host loading, MF remoteEntry probes
- bos start resolves configs with production env when NODE_ENV=production
  (kills the dev-mode raw-config load against the stripped image)
- Remote-source services no longer allocate ports; stale claims are
  pruned via recorded process generation; pinned-port conflicts verify a
  real listener; InfraError/DevStepError report instead of escaping
- bos.config.json carries the deterministic post-publish bundle URLs
- Dockerfile healthcheck start-periods cover cold-boot duration

Verified: runtime image boots healthy with citynode.app blackholed and
recovers across docker restart; typecheck 9/9, lint clean, 631 framework
tests green.

* docs(adr): amend 0011 (outbound local-first) and 0012 (claim identity, listener authority)

* refactor(everything-dev): api-contract fetch/checksum path as an Effect service

Lifts the manifest/contract-type/auth-export fetch path into
ApiContractResolver (Context.Service + Layer, Effect.fn, Schema.TaggedError:
ApiManifestFetchError, ApiManifestFormatError, MissingContractTypesError,
ContractTypesFetchError, ContractTypesChecksumError, AuthExportFetchError).
Behavior-preserving: fetchApiPluginManifest / remoteContractSource /
fetchAuthExportTypes keep their signatures and messages (bridges flatten
tagged errors to Error with identical strings); transport stays on the
http-client promise bridges so the [http] diagnostics and 30s GET cache are
unchanged. syncApiContractBridge untouched.

* ci: relocate the remote-runtime smoke to the Deploy workflow (ADR 0009 §4)

runtime-remote.test.ts is a live-network smoke: it reads the branch's raw
bos.config.json and proxies real traffic (/skill.md, /llms.txt) to
config.app.ui.production. Once #228 commits the deterministic bundle URLs,
that origin is the deployment this PR ships — a PR CI suite that depends on
production's uptime fails whenever the gateway blips, and before the deploy
it validates URLs that cannot serve yet (Host tests red: "expected 502 to be
200", 60s hook timeouts).

The split was already prepared but never wired: vitest.remote.config.ts owns
exactly this file and host has a test:integration:remote script with no
consumer, while the default vitest.config.ts include swept it into PR CI.

- Default host suite excludes tests/integration/runtime-remote.test.ts
  (configDefaults preserved) — PR CI no longer touches live origins
- deploy.yml runs the smoke after mf check: boots the real host program
  against the live production URLs — SSR compose from the deployed ui,
  doc-asset proxy, api ping, root document contract — the functional gate
  beyond mf check's identity-only check

No helper changes, deliberately: in the Deploy context, non-2xx origins
make the smoke fail loudly — correct for a post-deploy gate; a down site is
already caught by mf check's retry loop.

* fix(host-tests): pin the runtime-config fixture to development env

loadTestRuntimeConfig called loadResolvedConfig() with no env — with the
NODE_ENV-honoring default (hermetic boot), the host test script's
NODE_ENV=production resolved production env in the fixture: api source
remote → manifest discovery fetched the deployed origin (down, 10s×3 retries)
→ every beforeAll hook blew the 10s timeout → 6 suites failed at collection
(csp-compose, csp-nonce, security, seo, ssr-metadata, ssr).

Fixtures resolve the repo's own local workspaces; NODE_ENV=production in the
host test script exists for the host's production code paths, not for config
resolution. Pin env: "development" — live-origin coverage belongs to the
Deploy workflow's smoke (relocated earlier in this PR).
… it from the scaffold (#225)

* feat(everything-dev,ui): tenant draft/url helpers as everything-dev/ui/tenant subpath

Closes #184. The tenant origin construction (buildTenantUrl, tenantLabel,
isLocalHostname), the node-config draft helpers (schema, diff, bundle entry
resolution, sha384 integrity preflight), and the new gatewayForAccount merge
into one framework module exposed as everything-dev/ui/tenant. The app-owned
ui/src/lib/tenant-url.ts and tenant-config-draft.ts copies are deleted; call
sites import from the package, so children stop receiving the copies via
bos init and versions flow through the catalog / changeset release.

gatewayForAccount derives the gateway for an owner account from the runtime
config — the runtime's gateway when the account is on the runtime's network,
null otherwise — replacing the hardcoded per-network domain checks the
PR 177 review flagged in fresh's spawn flow.

* fix(everything-dev): keep the ui dist free of node builtins — external react + dead createRequire strip

The everything-dev dist's shared rolldown runtime chunk retained a dead
`import { createRequire } from "node:module" ` (injected for CLI-side
interop, unused after treeshake) — and the new ui-surface dist entries
(hydrate/router/tenant) import that chunk, so client builds of the ui
subpaths failed with "node:* is a built-in Node.js module and cannot be
imported in client-side code". react/react-dom join neverBundle (they are
MF shared singletons; bundling them dragged CJS-interop helpers into the
dist), and the build strips the dead import from the shared chunk when it
is genuinely unused.

* feat(ui): read stake-pool contracts through near-kit view calls

Closes #185. The hand-rolled JSON-RPC view-function wrapper (raw fetch
against the public RPC endpoints, base64 args, manual Zod byte-array
decoding — near-rpc.ts) is deleted. The stake-pool query call sites use
near-kit's Near.view per network instead, with failure parity: unsupported
networks, timeouts, and malformed results resolve null and fall into the
same clean query error state. Test mocks move to the near-kit boundary.

* style(ui): biome format stake-pool test mock chain

* feat(everything-dev,ui): absorb framework UI files into everything-dev/ui subpaths

Closes #186. hydrate (client bootstrap), router-client (client router
factory), router-server (SSR router module), entry (web entry runner), and
router-error (generic error boundary) move into the framework package as
new ui subpaths. Child copies shrink to thin wiring stubs that inject only
the app's generated artifacts (routeTree.gen, routeConfig.gen, styles.css)
and stay framework-owned for sync. The framework modules import the
package's own api/auth/runtime/manifest surfaces; the hydrate suite ports
to the package keeping the compose digest-parity fallback coverage.
globals.d.ts remains a sync-owned copy — ambient declarations cannot be
re-exported through stubs. RouterContextWithApi gains the optional
authClient the routers already threaded.

* refactor(ui,everything-dev): tenant surface through @/app; createRequire conversions at source

Review rework (PR 221): app call sites import the tenant surface from @/app
(app.ts re-exports everything-dev/ui/tenant) instead of reaching past the
app surface; dao-policy/dao-connect's TenantUiOverride re-export chain is
cut. The dist's dead createRequire import is eliminated at source —
banner.ts reads package.json via readFileSync(new URL(...)) and init.ts
statically imports tar (committed dep) instead of a dynamic require — so
the rolldown runtime chunk never receives node:module and the emitted-chunk
stripper is deleted.

* fix(everything-dev): tar ships an ESM build — named extract import

* refactor(ui,better-near-auth): per-network near clients through the auth client

Review rework (PR 222): better-near-auth's near atom gains
getNearClient(network?) — a network whose connector-bound client isn't
initialized gets a wallet-less Near cached on demand (public read-only
view calls need no wallet; initClientForNetwork upgrades the slot when
that network initializes). The stake-pool factories take the app auth
client and read contracts through authClient.near.getNearClient(network);
the module-local viewClients Map is deleted; pool networks narrow through
toNetwork. Timeouts follow the transport defaults (near-kit view has no
AbortSignal option — parity on failure-to-null holds).

* style: biome organize imports

* refactor(everything-dev): SSR router goes generic over the app route tree — shared router defaults

Review rework (PR 224): createServerRouterModule is generic over
TRouteTree extends AnyRoute — the app stub passes typeof routeTree, the
composed-tree casts narrow through the app's tree type, and the
'as unknown as AnyRouter' boundary cast is gone (a concrete tree satisfies
the contract directly). The unused errorComponent override param is
dropped (RouterError is the framework-owned boundary). The fallback
components and QueryClient defaults dedupe into ui/router-defaults,
shared by the client factory, the SSR module, and the hydrator.

* feat(everything-dev,ui): synthesize the core ui rsbuild config — drop it from the scaffold

Closes #187. Following the every-plugin generated-config model, the ui
package's dev/build/preview scripts route through the new bos-ui bin
(everything-dev/ui-build), which honors a local rsbuild.config.ts as an
override and otherwise generates one from the shared every-plugin/ui/mf-build
factory — provider role, CORE_UI_PLUGIN_KEY, the web/node exposes, public
copy, and the APP_NAME/APP_ACCOUNT defines derived from the resolved runtime
config. The config drops from the init copy surface; sync treats an existing
child config as app-owned. Manifest generation no longer gates on the
config's existence. ui lib target raised to ES2024 (Promise.withResolvers).

* refactor(every-plugin,ui): every-plugin owns the core ui build — bos-ui deleted, mf-build → build/ui

Review rework (PR 225). The ui workspace follows the /api pattern exactly:
scripts run `every-plugin dev|build|preview` (args pass through), and the
every-plugin CLI detects the workspace-form core ui (src/routes + src/entry,
not plugin-shaped) and dispatches rsbuild with the synthesized config. The
normalized build surface: every-plugin/src/build/ui/{factory,generated-config}
— mf-build moves to build/ui (the ecosystem has not shipped any of these
surfaces; the old ./ui/mf-build export is deleted, not deprecated), the
generated config shrinks to a 4-line async defineConfig importing
createCoreUiRsbuildConfig (exported factory, no template-embedded config)
and readAuthoredConfigInput (everything-dev/config — the authored-config
read that handles bos.app.ts and JSON without network resolution, closing
the TS-form crash). everything-dev's ui-build runner, bos-ui bin, and their
tests are deleted; synthesis coverage lands in every-plugin's suite.

* fix(everything-dev): reapply the createRequire conversions + stripper removal

The 186→187 rebase auto-resolve had silently re-introduced the pre-rework
banner/init createRequire sources and the emitted-chunk stripper. Reapplied:
banner reads package.json via readFileSync(new URL(...)), init statically
imports tar, the stripper is deleted — the dist is node:module-free again.

* style: biome import-name sort in the ported tenant-url call sites

* fix(ui): PoolSummary reads pools through the near-kit view client — toNetwork narrowing + authClient

* style: biome format the framework router-error

* fix(everything-dev): drop the duplicated tar import in init.ts

* fix(every-plugin): preview ran build — the dispatch dropped the command

runCliCommand routed preview through runCoreUi(args) with args empty, so
runCoreUi's destructuring default kicked in and every-plugin preview ran
rsbuild build and exited 0 — the preview server never started. CI regression
stacks (BOS_NO_WATCH → dev:built) lost the ui entirely: proxied assets 500
(fetch failed) and SSR streaming crashed on the null remote chunk
('Cannot read properties of null (reading stores)').

* fix(every-plugin): build with extra args ran the wrong rsbuild command

runCliCommand consumed the command, then runCoreUi re-derived it from the
remainder — so 'every-plugin build --environment web' invoked
'rsbuild --environment --config X web' (CACError: Unused args: web). The
image build (container-build.ts → build:client/build:ssr) broke in the
start-mode regression jobs. Prepend the command explicitly, matching the
dev/preview cases.
…, canonicalization (#226)

* feat(everything-dev): bos.app.ts operative — loader, TS-form scaffold, canonicalization

Closes #188. The config loader accepts the authored TS descriptor alongside
bos.config.json (JSON preferred when both coexist; TS-only children are the
new child form). Descriptors materialize into the same BosConfigInput the
pipeline consumes — import-extends (inlined parent) resolves through
resolveApp; bos:///file extends refs flow into the existing JSON extends
chain. publish/sync canonicalize the resolved config to JSON for FastKV with
identical resolution (proven: a TS-authored child resolves identically to
the equivalent JSON child). bos init scaffolds children with the TS config
form by default (configInputToDescriptor + serializeAppDescriptorSource);
sync reads TS-form children through the same materialization and never
injects the parent's JSON into them. Golden-fixture coverage extended
(lossless round-trip + convert + parity). ADR 0005 advances to Accepted
(phase 1).

* refactor(everything-dev): the descriptor module owns the TS config form — loader hardened

Review rework (PR 226): serializeAppDescriptorSource absorbs into
descriptor/serialize (the descriptor module owns the whole TS-form surface:
toConfigInput, its inverse configInputToDescriptor — driven by the shared
ATTACHMENT_FIELDS map in descriptor/resolve — and the source emitter);
app-config-form.ts is deleted. loadAppDescriptorConfig hardens: a bos.app.ts
must default-export an App descriptor, and registry candidates must parse as
descriptors (a named export that isn't an App can no longer enter the
registry).

* refactor(ui): dao-policy reads TenantUiOverride through @/app

* fix(everything-dev): drop the unused serialize import from the loader test
…he (ADR 0011) (#231)

* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.

* docs: clean up plan archives, wayfinder tickets, and phantom references

- delete .scratch/orpc-v2-migration (numbers.env issue map, unreferenced)
- plans/README.md: drop phantom ui-route-grafting-migration file link
  (kept as plain text + issue #108), prototype count 3→4, fix
  03-plugin-type-deps typo
- recreate wayfinder decision tickets 08–12 as decision-record stubs
  (decisions already recorded in README/map) and refresh ticket counts
- ADR 0007: replace dead ui-route-grafting-migration links with inline
  supersession note (ADR 0008); fix stale plan-035 link in ADR 0007
  platform-bundle-storage; point ADR 0006 at the archived plan 022
- advisor-plans: renumber wallet/teams batch to 044–048 (resolves 025–029
  and 028 collisions), add README rows for 044–048 (DONE via PR #136) and
  030–032 (TODO, unexecuted), archive DONE/superseded/absorbed plans to
  done/ with a slim done/README.md preserving status notes, prune
  dependency notes that only concern archived plans

* docs: fix plans/README.md staging miss from the cleanup commit

* fix(deploy): hermetic boot — the image consumes what it stages (ADR 0011)

A self-contained runtime image fetched its own plugin manifests through
its own public origin at boot; when that origin 502'd (Railway restart
loop, gateway hiccup, cold start ordering) nothing could ever come up —
the boot depended on the thing it was booting. Registry claims survived
container restarts via PID reuse and wedged the pinned host port, making
the loop permanent.

- BundleResolver service (Effect v4, Context.Service + Layer) resolves
  own-namespace /bundles/<account>/<gateway>/ URLs from BOS_BUNDLE_DIR;
  foreign namespaces and unset dirs fall through to the network fetch,
  so registry-tier children are unchanged
- One global-fetch adapter (ManagedRuntime bridge) installed at CLI boot
  covers every boot-time consumer: config manifest discovery, contract
  types, orchestrator host loading, MF remoteEntry probes
- bos start resolves configs with production env when NODE_ENV=production
  (kills the dev-mode raw-config load against the stripped image)
- Remote-source services no longer allocate ports; stale claims are
  pruned via recorded process generation; pinned-port conflicts verify a
  real listener; InfraError/DevStepError report instead of escaping
- bos.config.json carries the deterministic post-publish bundle URLs
- Dockerfile healthcheck start-periods cover cold-boot duration

Verified: runtime image boots healthy with citynode.app blackholed and
recovers across docker restart; typecheck 9/9, lint clean, 631 framework
tests green.

* docs(adr): amend 0011 (outbound local-first) and 0012 (claim identity, listener authority)

* refactor(everything-dev): api-contract fetch/checksum path as an Effect service

Lifts the manifest/contract-type/auth-export fetch path into
ApiContractResolver (Context.Service + Layer, Effect.fn, Schema.TaggedError:
ApiManifestFetchError, ApiManifestFormatError, MissingContractTypesError,
ContractTypesFetchError, ContractTypesChecksumError, AuthExportFetchError).
Behavior-preserving: fetchApiPluginManifest / remoteContractSource /
fetchAuthExportTypes keep their signatures and messages (bridges flatten
tagged errors to Error with identical strings); transport stays on the
http-client promise bridges so the [http] diagnostics and 30s GET cache are
unchanged. syncApiContractBridge untouched.

* feat(child-tier): foreign-namespace bundle proxy + stale-if-error cache (ADR 0011)

The child tier loads base workspaces from foreign origins; an origin outage
hard-failed browsers (502) and the child's cold boot. Two entrances share one
disk cache (BOS_BUNDLE_CACHE_DIR, default .bos/bundle-cache — deliberately
separate from BOS_BUNDLE_DIR: cached bytes are a resilience artifact, never a
deployment):

- Host: /bundles/* falls through to createBundleProxyCacheHandler after the
  FS handler — namespace→origin map derived from the runtime config's slot
  URLs (self-origin namespaces never proxied); success writes through, origin
  failure serves last-known-good bytes with x-bundle-cache: stale, no cache +
  failure → 502. GET/HEAD only.
- CLI fetch adapter: foreign /bundles/… URLs get the same stale-if-error
  treatment, so boot-time outbound fetches survive a base-origin outage after
  the first successful boot. Inert for plain dev sessions (no BOS_BUNDLE_DIR/
  BOS_BUNDLE_CACHE_DIR) — default behavior unchanged; stale serves only on
  origin failure, so normal operation keeps serving fresh.

Shared cache primitives live in everything-dev/bundle-cache (subpath export;
the host already imports everything-dev/* subpaths).

* ci: relocate the remote-runtime smoke to the Deploy workflow (ADR 0009 §4)

runtime-remote.test.ts is a live-network smoke: it reads the branch's raw
bos.config.json and proxies real traffic (/skill.md, /llms.txt) to
config.app.ui.production. Once #228 commits the deterministic bundle URLs,
that origin is the deployment this PR ships — a PR CI suite that depends on
production's uptime fails whenever the gateway blips, and before the deploy
it validates URLs that cannot serve yet (Host tests red: "expected 502 to be
200", 60s hook timeouts).

The split was already prepared but never wired: vitest.remote.config.ts owns
exactly this file and host has a test:integration:remote script with no
consumer, while the default vitest.config.ts include swept it into PR CI.

- Default host suite excludes tests/integration/runtime-remote.test.ts
  (configDefaults preserved) — PR CI no longer touches live origins
- deploy.yml runs the smoke after mf check: boots the real host program
  against the live production URLs — SSR compose from the deployed ui,
  doc-asset proxy, api ping, root document contract — the functional gate
  beyond mf check's identity-only check

No helper changes, deliberately: in the Deploy context, non-2xx origins
make the smoke fail loudly — correct for a post-deploy gate; a down site is
already caught by mf check's retry loop.

* fix(host-tests): pin the runtime-config fixture to development env

loadTestRuntimeConfig called loadResolvedConfig() with no env — with the
NODE_ENV-honoring default (hermetic boot), the host test script's
NODE_ENV=production resolved production env in the fixture: api source
remote → manifest discovery fetched the deployed origin (down, 10s×3 retries)
→ every beforeAll hook blew the 10s timeout → 6 suites failed at collection
(csp-compose, csp-nonce, security, seo, ssr-metadata, ssr).

Fixtures resolve the repo's own local workspaces; NODE_ENV=production in the
host test script exists for the host's production code paths, not for config
resolution. Pin env: "development" — live-origin coverage belongs to the
Deploy workflow's smoke (relocated earlier in this PR).
…232)

* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.

* docs: clean up plan archives, wayfinder tickets, and phantom references

- delete .scratch/orpc-v2-migration (numbers.env issue map, unreferenced)
- plans/README.md: drop phantom ui-route-grafting-migration file link
  (kept as plain text + issue #108), prototype count 3→4, fix
  03-plugin-type-deps typo
- recreate wayfinder decision tickets 08–12 as decision-record stubs
  (decisions already recorded in README/map) and refresh ticket counts
- ADR 0007: replace dead ui-route-grafting-migration links with inline
  supersession note (ADR 0008); fix stale plan-035 link in ADR 0007
  platform-bundle-storage; point ADR 0006 at the archived plan 022
- advisor-plans: renumber wallet/teams batch to 044–048 (resolves 025–029
  and 028 collisions), add README rows for 044–048 (DONE via PR #136) and
  030–032 (TODO, unexecuted), archive DONE/superseded/absorbed plans to
  done/ with a slim done/README.md preserving status notes, prune
  dependency notes that only concern archived plans

* docs: fix plans/README.md staging miss from the cleanup commit

* fix(deploy): hermetic boot — the image consumes what it stages (ADR 0011)

A self-contained runtime image fetched its own plugin manifests through
its own public origin at boot; when that origin 502'd (Railway restart
loop, gateway hiccup, cold start ordering) nothing could ever come up —
the boot depended on the thing it was booting. Registry claims survived
container restarts via PID reuse and wedged the pinned host port, making
the loop permanent.

- BundleResolver service (Effect v4, Context.Service + Layer) resolves
  own-namespace /bundles/<account>/<gateway>/ URLs from BOS_BUNDLE_DIR;
  foreign namespaces and unset dirs fall through to the network fetch,
  so registry-tier children are unchanged
- One global-fetch adapter (ManagedRuntime bridge) installed at CLI boot
  covers every boot-time consumer: config manifest discovery, contract
  types, orchestrator host loading, MF remoteEntry probes
- bos start resolves configs with production env when NODE_ENV=production
  (kills the dev-mode raw-config load against the stripped image)
- Remote-source services no longer allocate ports; stale claims are
  pruned via recorded process generation; pinned-port conflicts verify a
  real listener; InfraError/DevStepError report instead of escaping
- bos.config.json carries the deterministic post-publish bundle URLs
- Dockerfile healthcheck start-periods cover cold-boot duration

Verified: runtime image boots healthy with citynode.app blackholed and
recovers across docker restart; typecheck 9/9, lint clean, 631 framework
tests green.

* docs(adr): amend 0011 (outbound local-first) and 0012 (claim identity, listener authority)

* refactor(everything-dev): api-contract fetch/checksum path as an Effect service

Lifts the manifest/contract-type/auth-export fetch path into
ApiContractResolver (Context.Service + Layer, Effect.fn, Schema.TaggedError:
ApiManifestFetchError, ApiManifestFormatError, MissingContractTypesError,
ContractTypesFetchError, ContractTypesChecksumError, AuthExportFetchError).
Behavior-preserving: fetchApiPluginManifest / remoteContractSource /
fetchAuthExportTypes keep their signatures and messages (bridges flatten
tagged errors to Error with identical strings); transport stays on the
http-client promise bridges so the [http] diagnostics and 30s GET cache are
unchanged. syncApiContractBridge untouched.

* feat(child-tier): foreign-namespace bundle proxy + stale-if-error cache (ADR 0011)

The child tier loads base workspaces from foreign origins; an origin outage
hard-failed browsers (502) and the child's cold boot. Two entrances share one
disk cache (BOS_BUNDLE_CACHE_DIR, default .bos/bundle-cache — deliberately
separate from BOS_BUNDLE_DIR: cached bytes are a resilience artifact, never a
deployment):

- Host: /bundles/* falls through to createBundleProxyCacheHandler after the
  FS handler — namespace→origin map derived from the runtime config's slot
  URLs (self-origin namespaces never proxied); success writes through, origin
  failure serves last-known-good bytes with x-bundle-cache: stale, no cache +
  failure → 502. GET/HEAD only.
- CLI fetch adapter: foreign /bundles/… URLs get the same stale-if-error
  treatment, so boot-time outbound fetches survive a base-origin outage after
  the first successful boot. Inert for plain dev sessions (no BOS_BUNDLE_DIR/
  BOS_BUNDLE_CACHE_DIR) — default behavior unchanged; stale serves only on
  origin failure, so normal operation keeps serving fresh.

Shared cache primitives live in everything-dev/bundle-cache (subpath export;
the host already imports everything-dev/* subpaths).

* refactor(everything-dev): code-artifacts as Effect.fn with tagged ArtifactGenError (C1)

generateCodeArtifactsEffect is the Effect-native core (tagged
ArtifactGenError per phase: write resolved config, load resolved config,
sync api contract bridge, generate ui manifest); the exported
generateCodeArtifacts keeps its Promise signature as a bridge — callers
(dev-program, publish) unchanged. The Effect export is available for the
later dev-program adoption.

* refactor(everything-dev): config.ts error taxonomy as Schema.TaggedError (C2)

All thrown Errors become tagged errors (ConfigNotLoadedError, ConfigLoadError,
CircularExtendsError, ConfigNotfoundError, ConfigExtendsError) — every one an
Error subclass with the identical message string, so all plain callers'
.message reads and the wrapped-cause behavior are unchanged. This gives the
later Effect-native phases a typed error channel instead of string matching.

* refactor(everything-dev): config pipeline as Effect.fn cores (C3+C4)

loadResolvedConfig, resolveRuntimePlugins, and buildRuntimeConfig become
Effect.fn generators with a typed error channel; exported signatures stay
Promise-based via bridges, so no caller changes. Error taxonomy (C2) carries
the channel: ConfigLoadError wraps at the load boundary with the identical
"Failed to load config from …" message; inner failures propagate raw before
that wrap, matching today's throw/wrap order exactly.

- resolveRuntimePlugins: Promise.all → Effect.forEach (unbounded, order-
  preserving); per-plugin resolution via tryPromise with ConfigExtendsError
- buildRuntimeConfig: sync body stays inline; the two async islands (auth
  entry, manifest discovery) become tryPromise steps — the catch-all
  discovery diagnostics stay plain inside the promise (same warn messages,
  same degrade-to-warning semantics), keeping console out of Effect code
- loadResolvedConfig: the pipeline composes the Effect cores; the
  suppress/drain/resume warning discipline is preserved call-for-call
  (resume on both paths via the inner catch, drain on success only)

Behavior-preserving throughout: 634 framework tests green (config-resolved
golden tests pin the outputs), typecheck 9/9, lint 0 errors.

* ci: relocate the remote-runtime smoke to the Deploy workflow (ADR 0009 §4)

runtime-remote.test.ts is a live-network smoke: it reads the branch's raw
bos.config.json and proxies real traffic (/skill.md, /llms.txt) to
config.app.ui.production. Once #228 commits the deterministic bundle URLs,
that origin is the deployment this PR ships — a PR CI suite that depends on
production's uptime fails whenever the gateway blips, and before the deploy
it validates URLs that cannot serve yet (Host tests red: "expected 502 to be
200", 60s hook timeouts).

The split was already prepared but never wired: vitest.remote.config.ts owns
exactly this file and host has a test:integration:remote script with no
consumer, while the default vitest.config.ts include swept it into PR CI.

- Default host suite excludes tests/integration/runtime-remote.test.ts
  (configDefaults preserved) — PR CI no longer touches live origins
- deploy.yml runs the smoke after mf check: boots the real host program
  against the live production URLs — SSR compose from the deployed ui,
  doc-asset proxy, api ping, root document contract — the functional gate
  beyond mf check's identity-only check

No helper changes, deliberately: in the Deploy context, non-2xx origins
make the smoke fail loudly — correct for a post-deploy gate; a down site is
already caught by mf check's retry loop.

* fix(host-tests): pin the runtime-config fixture to development env

loadTestRuntimeConfig called loadResolvedConfig() with no env — with the
NODE_ENV-honoring default (hermetic boot), the host test script's
NODE_ENV=production resolved production env in the fixture: api source
remote → manifest discovery fetched the deployed origin (down, 10s×3 retries)
→ every beforeAll hook blew the 10s timeout → 6 suites failed at collection
(csp-compose, csp-nonce, security, seo, ssr-metadata, ssr).

Fixtures resolve the repo's own local workspaces; NODE_ENV=production in the
host test script exists for the host's production code paths, not for config
resolution. Pin env: "development" — live-origin coverage belongs to the
Deploy workflow's smoke (relocated earlier in this PR).
…251)

* feat(auth): manage linked NEAR accounts; phone sign-in desktop-only

- Settings > Sign-in methods: list all linked NEAR accounts, make
  primary, unlink with confirmation, link another named account, or
  create a NEAR account derived from a passkey when none is linked
  (gated on passkeys + passkey wallet availability). Linking uses the
  dedicated near.link action instead of a sign-in ceremony.
- Login page: hide 'Sign in with your phone' on mobile viewports and
  adapt the no-passkey hint; add useMediaQuery/useIsDesktop hooks.

* refactor(auth): address two-axis review findings

- Surface account-load failures (error state + retry) instead of
  rendering the empty state; passkey eligibility requires a successful
  passkey list rather than swallowing errors
- Hide 'Make primary' on the account that is already primary
- Export isDeterministicAccountId from better-near-auth and use it in
  the UI instead of duplicating the NEP-616 account regex
- Add useNetworkId() hook; drop the duplicated networkId casts in
  login and auth-methods
- Move query keys to lib/query-keys; collapse the callback-to-promise
  wrappers into one toPromise helper
- Cache MediaQueryList per query in useMediaQuery
- Delete unused plugins/auth/ui/src/lib/use-near-account.ts
…x stale claims (#254)

ADR renumbering (resolves the four duplicate numbers):
- 0007-platform-bundle-storage → 0015 (superseded by 0011, kept for history)
- 0011-unified-log-pipeline → 0016
- 0012-session-gas-keys → 0017
- 0013-session-single-owner → 0018
- all 14 external references updated (AGENTS.md, changesets, advisor-plans,
  scratch issues); 0019 reserved for advisor-plan 042's spreadability ADR

ADR honesty fixes:
- 0003 amended: tsconfig.contract.json mechanism replaced by EmitPluginManifest
- 0010 §7 annotated: bos routes inspector not yet implemented
- 0011 records the landed foreign-namespace proxy + stale-if-error amendment
- 0007-runtime/0009 status lines note their amendments
- docs/adr/README.md index with number-assignment discipline

Plans reorganization:
- plans/done/ archive (mirrors advisor-plans/done/): orpc-v2-effect-migration,
  effect-native-plugins, tenant-feature-completeness, ui-extends-ui-federation,
  db-auth-absorption prototype
- toml-infra-alchemy: status correction (phases 1/3/4 live only on the
  unmerged upstream/feat/alchemy branch)
- every-plugin-db-auth-absorption + advisor-plan 017 row: mostly-done status
  with the databaseLayer/sync-exit remainder
- wayfinder tickets 01/04/08/09/11/12 re-annotated; beta-v2-map status banner
- plans/README.md: grafting row marked superseded (ADR 0007/0008), node-map
  (#233) pointer, offline plans marked unscheduled

GitHub tracker: closed #190/#104 (delivered), #111/#118 (superseded, reasons
in the close comments); #166's parallel-builds half checked off
…ne (#253)

* fix(db,plugins): honor sslmode; structured plugin-boot failure pipeline

Root cause of the Sep 25 production outage: PR #163's security batch
flipped the shared DB driver's TLS verification to on-by-default for
every non-local URL, and managed providers' sslmode=require URLs carry
certificates no client CA bundle can verify — the auth plugin's pool
(followed by the api's) failed at connect time. The pre-existing
boot pipeline then retried silently for 120s per plugin, pushing the
server's port-bind past the container entrypoint's 180s health gate ->
exit(1) -> Railway restart loop -> 502.

- db driver: derive TLS from the URL's sslmode with libpq semantics
  (require/prefer/allow encrypt WITHOUT verifying; verify-ca/verify-full
  verify; bare non-local URLs keep the secure default; the env var still
  overrides) + full ssl-derivation matrix unit tests
- every-plugin: one shared Effect-native load policy (loadRemoteWithRetry)
  — capped exponential backoff under a wall-clock budget, per-attempt
  failure logs deduped by classification signature, poisoned entry-cache
  purge between attempts, and fail-fast on permanent failures via
  classifyPluginFailure().retryable; absorbs withRemoteEntryResilience
  and the dev-serve duplicate loop; readiness poll gets its own budget
- host: plugin bootstrap failures stay structured end-to-end —
  PluginBootstrapError exposes operation + classification, bootstrap
  logs read [Plugins][<key>] Failed to load plugin (<operation>) —
  permanent|retryable, and /health + /api/_health surface a structured
  failures array instead of joined strings
- log formatting: [MF][<pluginId>] prefixes, drop the redundant
  Effect.annotateLogs({ plugin }) JSON trailer, [Plugins][<key>] short
  keys in host boot logs

Verification: everything-dev 696, every-plugin 109 unit + 31
integration, host 206, ui 75, api 137 all green; bun typecheck and
bun lint clean. host test:e2e has 3 pre-existing environmental failures
identical on pristine main (needs local docker test DBs).

* refactor(every-plugin): shared PluginLoadFailureInfo contract (review)

Move the structured failure shape from host into every-plugin, next to
classifyPluginFailure/PluginFailureClassification — it is the
cross-package failure contract (loader context + classification), with
host re-exporting for its health surface. Also fix the retry-test type
issues surfaced by the build train (unknown ?? {} narrowing and
noUncheckedIndexedAccess on mock calls).

* fix(everything-dev): bos build reports unknown targets instead of bare "Unknown error"

selectWorkspaceTargets only resolves app/plugin slot keys, so a
framework-package name (every-plugin) yielded targets=[] -> a messageless
error result; all-skipped selections and a missing bos.config.json had
the same shape. BuildResultSchema gains the optional error field the CLI
already prints, and all three handler error paths now carry messages:
unknown targets list the valid slots (framework packages build via the
prerequisite train), nothing-built lists the skipped targets, missing
config says so.
* feat(everything-dev): typed mount contract for ui plugin grafting

Plan 023. The graft protocol's mount vocabulary becomes a compile-time
contract instead of a stringly convention:

- mount-registry.ts: MOUNT_REGISTRY now satisfies Record<string, MountEntry>
  so MountId derives from its canonical keys; MOUNTS exports the union
  value; MOUNT_ALIASES is typed Record<string, MountId> (moved here from
  types.ts, which re-exports both for surface stability)
- define.ts: defineUiPlugin({ name, mounts, tree }) validates the tree at
  construction — every root child declaring a _mount must resolve to a
  declared canonical mount, else it throws with the child id and a
  closest-mount hint; associations ride a module-scoped WeakMap
- compose.ts: defined modules graft via declared mounts (no
  re-derivation); raw trees keep the deriveMountId fallback, now the
  declaredSegment derivation with a MountId return type
- digest-version.ts: MOUNT_REGISTRY_VERSION -> 2026-09-19.1 (graft input
  semantics changed; all compose digests invalidate)
- README row records the agreed deviation (validation covers _-roots
  only) and the plan-024 note (_template has no ui tree yet)

Verification: everything-dev 489 tests green (compose suite 20 incl. 9
new), host 175 green; no new typecheck or lint findings vs baseline.

* feat(api,host): platform bundle storage + publish --cdn platform (plan 029)

Platform CDN provider so bos publish --deploy needs no Zephyr/Cloudflare
account: bundles upload to the platform storage via the CLI session and
are served publicly from /bundles/* with immutable cache headers.

- api: bundle_objects table + BundleStorage Effect service (R2 drop-in
  seam), oRPC contract routes POST /storage/bundles (auth'd, ceilings,
  path allowlist, traversal guard, server-side SRI) and
  GET /bundles/{account}/{gateway}/{workspace}/{+path} (public, File
  output)
- host: route-scoped bodyLimit for uploads, second prefix-symmetric
  OpenAPIHandler mount at /bundles/* (+ proxy mode), generic
  ResponseHeadersHandlerPlugin
- cli: deploy.cdn config (default zephyr), --cdn flag, platform
  uploader (dist/ artifacts -> platform storage), session requirement,
  bos login refusal without one
- docs: ADR 0007 (file transport as oRPC contract concern), plan 033
  (derived OpenAPI mounts follow-up), AGENTS.md deploy.cdn, changeset

Live dev-stack E2E (bos publish --deploy --cdn platform + bos mf check)
left for the operator.

* chore(everything-dev): drop dead cdnUploadHandled param (code-review follow-up)

* refactor(api): effect-native storage handlers — .effect() + yield* StorageTag (code-review follow-ups)

- storage routes follow the template convention: .effect() generators with
  yield* StorageTag (StorageTag now exposed from initialize's returned
  layer); inline auth via Effect.fail(errors.UNAUTHORIZED/FORBIDDEN) with
  the every-plugin/errors data shapes
- AGENTS.md 'Adding API Endpoints': new-route handler convention enforced
  (.effect + yield* Tag; Context.get reserved for streaming; shared
  auth middlewares' .use() typing gap documented)
- ADR 0007: handler-convention bullet + recorded seam deviation
  (platform provider skips workspace scripts.deploy; parent uploads
  dist/ via applyDeployResults)

* chore: pin zephyr plugins to 1.3.0 (deliberate catalog pin, drift guard)

* fix(api): StorageTag Self type parameter (class-self-mismatch rule)

* feat(dev): unified log pipeline (normalize, classify, filter, broadcast)

Every dev-session process line now flows through a single pure
pipeline that normalizes multi-line blocks, classifies level and
category, and level-filters before broadcasting to the three sinks
(screen tail, log file, l/shutdown export) so the views agree by
construction.

- normalize: Effect Logger { } objects, stack-trace continuations
- classify: WARN [Better Auth] stdout lines are warn; legacy
  LOG_NOISE_PATTERNS folded in as mf/build info; clean exits
  (code 0) classify as shutdown info, not [ERR]
- filter: --log-level / BOS_LOG_LEVEL (default warn; DEBUG = all);
  [Database] startup runs collapse to one db ready per plugin
- file sink always receives everything; export prints a filtered
  summary plus a full-logs pointer
- one ANSI-strip implementation; ADR 0011 records the decision

* feat(dev): stream-driven ANSI TUI — drop ink/react from the CLI package

bos dev's interactive view is now a pure render function over a single
session-state holder (processes, log events). A hand-rolled alt-screen
renderer redraws on state change, reads q / l / ctrl+c from raw stdin,
and restores the terminal on unmount. The piped/non-TTY fallback reuses
the same render helpers and prints incrementally — the near-verbatim
duplicated streaming view is deleted.

- ink, react (peer), gradient-string and their type packages removed
  from the CLI package manifest; build externals updated
- banner gradient replaced with a small truecolor ramp in the theme
  module (colors resolve at call time, so chalk level is honored)
- renderDevState / renderProcessRow / renderLogLine are unit-tested;
  renderer handle covered for dedupe, incremental rows, ready-block
  once, alt-screen enter/exit, and key handling

* feat(dev): one row per plugin in the service table

plugin-ui:* companion rows merge into their parent as an inline
'· ui :<port>' annotation (the auth app slot renders as one PLUGINS
row, e.g. 'AUTH (local) running :3002 · ui :3011'); SERVICES lists
only host/api/ui; a merged row counts as ready only when both its api
and ui surfaces are ready, and 'All N services running' counts merged
rows. Sectioning, display names, colors, and column widths live in the
shared render module consumed by both the TTY and non-TTY paths — the
plugin: prefix check that orphaned plugin-ui rows into SERVICES is
gone.

Auth-mirror detection is single-sourced in isAuthMirrorPluginEntry
(structural AuthSlotShape signature): the inline copies in the infra
planner (allocateServices, buildServiceDescriptors, assigned runtime
config), the dependency DAG, and the api contract bridge are deleted.
allocateServices now also matches remote-url mirrors, fixing its prior
allocate/descriptor disagreement; planner tests pin the semantics.

* fix(dev): TUI quit escalation + viewport-bounded repaint — quittable sessions, no scrollback bleed

* docs(plans): CLI audit plans 037-041 + ADR 0012 + plan 036 amendments (env/port truth, registry lock, docs ride-along)

* fix(dev): repaint erase-to-EOL, renderer dead after unmount, shutdown messages visible (plan 037 phase 1)

* fix(every-plugin): watcher kill escalation + parent-death supervision — rspack/rsbuild watchers can never orphan (plan 042)

* fix(dev): quittable at every lifecycle phase — startup quit kills spawned children, no self-SIGKILL, honest exit codes (plan 037 phase 2)

* test(regression): Go teardown suite — run a real dev stack, kill it, prove everything is dead (036/037/042 acceptance)

* refactor(regression): rename teardown suite to framework — home for everything-dev-level regression/e2e

* feat(everything-dev): bos kill escalation + orphan adoption + verified port release (plan 036 phase 1)

* feat(everything-dev): atomic port-block allocation — announced drift, persist-only-explicit, lease-shaped registry (plan 036 phase 2)

* refactor(everything-dev): scope-owned child handles — acquireRelease teardown by construction (plan 036 phase 3)

* docs(agents): port-allocation model matches ADR 0012 — block layout, persist-only-explicit, escalating kill (plan 036 phase 4 docs ride-along)

* docs(plans): 036 DONE — port leases and scoped stack landed through phase 4

* fix(dev): display/lifecycle defect batch — non-TTY log freeze, --interactive non-TTY fallback, post-ready death status, detectStatus false positives, spawn-error surfacing, per-session log files (plan 037 phase 3)

* fix(everything-dev): env & port truth — preflight probes credentialed DB URLs, origin keys generated-owned at every tier, DB ports honor persisted state (plan 038)

* fix(everything-dev): security hardening — validated plugin keys, no shell spawn, TLS default, fail-closed SRI, key file perms, masked drift logs (plan 039)

* perf(verification): root chain runs framework suites; dev spawn unblocked from network checks; config tests stub the network (plan 040)

* chore(everything-dev): CLI hygiene batch — dead deps removed, zod as dependency, catalog pins, no private-API casts, doc truth, dedupes (plan 041)

* docs(plans): 037-041 status rows — all DONE with commit refs

* chore(everything-dev): vitest pool tuning measured and rejected — singleFork doubles wall time and leaks module state; keep isolated parallel forks

* fix(everything-dev): resolve dev-latest by session start time, not mtime (CI-stable tie-break)

* fix(everything-dev): block-allocate the auth mirror's ui port again

The atomic port-block allocator (plan 036 phase 2) restructured the
plugin loop into a whole-entry mirror skip, dropping the plugin-ui:auth
port allocation that pre-regression allocateServices granted outside the
!mirror check. With no port, the planner's patchedUi left the mirror's
ui.url empty and the host advertised the auth remote as a relative
/remoteEntry.js — the browser could never load the auth plugin UI, so
/login never rendered (dev:ssr + dev:csr regression suites red from
2674acc onward).

Restore the pre-regression semantics under the block model: the mirror
still gets no plugin:<id> api entry, but a local mirror with a local ui
gets its plugin-ui:<id> entry allocated, which patches
runtimeConfig.plugins.auth.ui.url and feeds the folder-form auth
descriptor's BOS_UI_PORT.

Also sanitize the regression stack log filename (regression-dev-ssr.log)
— the mode's colon made upload-artifact reject the whole artifacts
upload, hiding the very logs needed to debug this.

New planInfra regression test pins the mirror ui allocation and the
patched ui.url.

* fix(everything-dev): keep harness-injected origins in config-path production starts

A registry start is a real deployment — localhost CORS_ORIGIN/BASE_URL
there are stray dev leftovers and stay purged. An explicit --config-path
start (regression harness, local production stack) injects those origins
as deployment truth; deleting them made the in-process host derive the
auth origin as https://<domain>, so better-auth issued Secure cookies no
http client could send back (empty sessions, 401s, INVALID_ORIGIN).

* fix(everything-dev): purge localhost origins only on registry starts

The gate moves from !configPath to a positive isRegistryStart check:
bare local bos start runs (no --config-path, no BOS_ACCOUNT/BOS_GATEWAY)
load the local config and are local starts too — their origins are
deployment truth, not stray dev leftovers. Adds unit tests for all
three config-source outcomes and a changeset.

* fix(dev): quittable TUI — resume raw stdin so q/Ctrl-C reach the key handler

Bun's TTY stdin never enters flowing mode from a bare data listener
attach. With the dev TUI's raw mode on, ISIG is disabled, so Ctrl-C
generated no SIGINT, and the \x03/q bytes never arrived either — the
session was unquittable from its own terminal (bos kill from a second
terminal was the only exit). The renderer now resumes stdin after
enabling raw mode; unmount pauses it.

Quit-path cleanup in the same sweep:
- devApp's SIGINT/SIGTERM handler now delegates to the same
  requestShutdownEscalating the TUI uses (the two bodies had drifted
  into twins); signal counting lives in one place
- emergencyKill reuses reapGroup instead of re-implementing the
  group-kill fallback
- suspendForceExitTimer renamed rearmForceExitTimer — it extends the
  deadline, it does not suspend it
- l (export logs) no longer counts toward the quit escalation, so
  l-then-q quits gracefully instead of force-exiting past the export
- force-exit timer is unref'd on both paths

Regression coverage: the framework suite gains a pty harness
(creack/pty) and two tests that boot a fully interactive stack, wait
for the TUI's alt-screen mount, then quit via q and via the raw Ctrl-C
byte — asserting the CLI exits 0, every service child and watcher
grandchild dies, ports free, and the registry cleans up. Both failed
(red) before the resume fix; both pass after.

* fix(everything-dev): drop the SERVICE_CONFIGS annotation duplicated by the satisfies check

The rebase merge of main (post-#179) kept BOTH the Record<string,
Pick<...>> annotation and the branch's trailing `as const satisfies`.
The annotation widens the const back to Record, so under
noUncheckedIndexedAccess (plugins/apps tsconfig) every dot access
yields Pick | undefined and the descriptor spreads became optional —
typecheck failed on the auth descriptor. Keep only the satisfies form:
same contract enforcement, literal keys stay required.

* feat(dev): surface folder-form plugin UIs in the TUI — auth row shows its ui port

Folder-form plugin UIs (ui/ dir inside the plugin workspace, no own
package.json) run as a child of the plugin's dev process via
BOS_UI_PORT — no plugin-ui:<id> service is spawned, so the one-row-per-
plugin renderer had nothing to merge and the port was invisible unless
you tailed the log stream ([auth] ├─ 🎨 UI: http://localhost:3011).

ServiceDescriptor now carries uiPort next to the BOS_UI_PORT env in
both folder-form branches (regular plugin + auth mirror);
getProcessStates threads it into the process state; mergePluginUiRows
synthesizes the same inline annotation package-form companions get.
The auth row now reads: AUTH (local) running :3002 · ui :3011.

* fix(auth): single-owner session read path — everything-dev/ui/auth as a strict MF singleton

The post-sign-in redirect loop was fixed three times (#162, #175, #178)
without staying fixed: the invariant (one session queryFn, always
disableCookieCache) had to hold identically across independently deployed
bundles — the read path was compiled separately into the core ui and every
plugin ui, and mixed deploys ran divergent copies whose guard decisions
ping-ponged past the router limit.

- everything-dev/ui/auth joins the ui share list as a strict singleton
  (core ui provides, plugin uis consume with import: false — zero bundled
  fallback): one runtime copy; version mismatch fails loudly instead of
  loading a second disagreeing copy. Version resolves from the building
  workspace's installed package, not the catalog: range (plan 010
  single-resolver principle)
- guards move to the platform package: requireSession/requireAdmin +
  plugin-path helpers + clearAuthenticatedQueries now live in
  everything-dev/ui/auth (framework-home convention, amended #89) — the
  login↔authenticated redirect pair is owned and unit-tested in one place
- sync surface shrinks: ui/src/lib/auth-guards.ts, ui/src/lib/plugin-path.ts,
  and the drifted plugin session-cache.ts copy exit child ownership; the
  plugin copy still carried the WeakSet bootstrap bookkeeping #178 removed
  from the ui copy
- ui/src/lib/session-cache.ts keeps only resolveSessionFromCache (SSR↔
  query-cache hydration bridge for the sync-owned __root.tsx)

ADR 0013; follow-up tickets 12-14 (api-auth framework export, route-config
ssr flag drop, retire plugin-path ceremony). Typecheck 9/9, lint clean,
everything-dev 615, ui 384, auth-plugin 244, host 207, api 131, every-plugin
92, template/apps/proposals/votes green; provider/consumer manifests carry
the shared entry with matching requiredVersion.

* refactor(build): config factories resolve from src; the train owns dist freshness

First-principles exit from the dist-staleness class (two resolution rules,
ADR 0013):

1. Bundler-configuration code resolves from source — the mf-build and
   build/rspack factories run only at build time from the working tree, so
   resolving them through built dists created an invisible build dependency
   (proven: a stale every-plugin dist silently dropped the shared
   everything-dev/ui/auth entry from the ui manifest with a green build).
   every-plugin ships src in its tarball, so its ./ui/mf-build and
   ./build/rspack subpaths now default to src in every condition; the
   everything-dev/ui/mf-build re-export shim is deleted (one consumer —
   ui/rsbuild.config.ts now imports every-plugin/ui/mf-build directly,
   matching the generated plugin configs). Verified: a ui build succeeds
   with every-plugin's dist deleted outright, manifest intact.
2. Shipped code resolves dist — runtime subpaths (everything-dev/ui/auth,
   db, every-plugin's shared runtime) are dist-resolved, and
   buildWorkspaceTargets unconditionally staleness-checks the framework
   prerequisites (every-plugin, everything-dev, better-near-auth) before
   any target. The train (bun run build / deploy) is the only supported
   build path; AGENTS.md documents it.

Removes the interim manifest-assertion tripwire (proven placebo: the
manifest is generated from the config chain, so config-vs-manifest checks
agree with stale chains). Plugin unit shape (src -> api/src,
plugin.dev.ts -> bos.dev.ts, per-unit bos.app.ts) and CI train consumption
ticketed (issues 15-17).

* fix(build): every-plugin subpaths resolve src under bun, dist under node

The default->src flip broke scaffolded children: node refuses to
type-strip .ts under node_modules (ERR_UNSUPPORTED_NODE_MODULES_TYPE_
STRIPPING) — 'node node_modules/.bin/bos types gen' in bos init's
integration test died loading the factory from src. The correct
discriminator is the runtime, not the NODE_ENV: bun (the workspace
runtime, any NODE_ENV) resolves src via its always-on 'bun' condition —
config factories can never go stale in-workspace; node consumers
(published CLIs in children) resolve the immutable published dist.

The 'bun' arm now covers every every-plugin subpath — this also fixes a
latent bootstrap chicken-and-egg (rm -rf every-plugin/dist && bun run
build died: the bos CLI itself imports every-plugin subpaths that only
resolved via dist without the development condition; the train now boots
from src and staleness-rebuilds the dist itself — verified).

mf-build tests move to every-plugin's suite with a relative src import
(resolver-agnostic, always current). ADR 0013 wording updated;
init.full verified green under CI=true locally.

* refactor(auth): session-cache ownership completes — resolveSessionFromCache joins everything-dev/ui/auth

Answers the PR review: the SSR↔query-cache hydration bridge is router glue
better-auth has no concept of (their docs' SSR pattern is Next-shaped:
server resolves, pass via props — for TanStack the adapter goes through
Query dehydration because the guards read the query). It exits child
ownership like the rest of the session surface: ui/src/lib/session-cache.ts
deleted, __root.tsx imports via @/lib/auth, tests moved to the package.
Ticket 14 updated: endgame is generated route types from the compose
manifests (keeps 'to' — search/preload/active semantics href can't give);
interim choice pending discussion.

* docs: clean up plan archives, wayfinder tickets, and phantom references

- delete .scratch/orpc-v2-migration (numbers.env issue map, unreferenced)
- plans/README.md: drop phantom ui-route-grafting-migration file link
  (kept as plain text + issue #108), prototype count 3→4, fix
  03-plugin-type-deps typo
- recreate wayfinder decision tickets 08–12 as decision-record stubs
  (decisions already recorded in README/map) and refresh ticket counts
- ADR 0007: replace dead ui-route-grafting-migration links with inline
  supersession note (ADR 0008); fix stale plan-035 link in ADR 0007
  platform-bundle-storage; point ADR 0006 at the archived plan 022
- advisor-plans: renumber wallet/teams batch to 044–048 (resolves 025–029
  and 028 collisions), add README rows for 044–048 (DONE via PR #136) and
  030–032 (TODO, unexecuted), archive DONE/superseded/absorbed plans to
  done/ with a slim done/README.md preserving status notes, prune
  dependency notes that only concern archived plans

* docs: fix plans/README.md staging miss from the cleanup commit

* fix(deploy): hermetic boot — the image consumes what it stages (ADR 0011)

A self-contained runtime image fetched its own plugin manifests through
its own public origin at boot; when that origin 502'd (Railway restart
loop, gateway hiccup, cold start ordering) nothing could ever come up —
the boot depended on the thing it was booting. Registry claims survived
container restarts via PID reuse and wedged the pinned host port, making
the loop permanent.

- BundleResolver service (Effect v4, Context.Service + Layer) resolves
  own-namespace /bundles/<account>/<gateway>/ URLs from BOS_BUNDLE_DIR;
  foreign namespaces and unset dirs fall through to the network fetch,
  so registry-tier children are unchanged
- One global-fetch adapter (ManagedRuntime bridge) installed at CLI boot
  covers every boot-time consumer: config manifest discovery, contract
  types, orchestrator host loading, MF remoteEntry probes
- bos start resolves configs with production env when NODE_ENV=production
  (kills the dev-mode raw-config load against the stripped image)
- Remote-source services no longer allocate ports; stale claims are
  pruned via recorded process generation; pinned-port conflicts verify a
  real listener; InfraError/DevStepError report instead of escaping
- bos.config.json carries the deterministic post-publish bundle URLs
- Dockerfile healthcheck start-periods cover cold-boot duration

Verified: runtime image boots healthy with citynode.app blackholed and
recovers across docker restart; typecheck 9/9, lint clean, 631 framework
tests green.

* docs(adr): amend 0011 (outbound local-first) and 0012 (claim identity, listener authority)

* refactor(everything-dev): api-contract fetch/checksum path as an Effect service

Lifts the manifest/contract-type/auth-export fetch path into
ApiContractResolver (Context.Service + Layer, Effect.fn, Schema.TaggedError:
ApiManifestFetchError, ApiManifestFormatError, MissingContractTypesError,
ContractTypesFetchError, ContractTypesChecksumError, AuthExportFetchError).
Behavior-preserving: fetchApiPluginManifest / remoteContractSource /
fetchAuthExportTypes keep their signatures and messages (bridges flatten
tagged errors to Error with identical strings); transport stays on the
http-client promise bridges so the [http] diagnostics and 30s GET cache are
unchanged. syncApiContractBridge untouched.

* feat(child-tier): foreign-namespace bundle proxy + stale-if-error cache (ADR 0011)

The child tier loads base workspaces from foreign origins; an origin outage
hard-failed browsers (502) and the child's cold boot. Two entrances share one
disk cache (BOS_BUNDLE_CACHE_DIR, default .bos/bundle-cache — deliberately
separate from BOS_BUNDLE_DIR: cached bytes are a resilience artifact, never a
deployment):

- Host: /bundles/* falls through to createBundleProxyCacheHandler after the
  FS handler — namespace→origin map derived from the runtime config's slot
  URLs (self-origin namespaces never proxied); success writes through, origin
  failure serves last-known-good bytes with x-bundle-cache: stale, no cache +
  failure → 502. GET/HEAD only.
- CLI fetch adapter: foreign /bundles/… URLs get the same stale-if-error
  treatment, so boot-time outbound fetches survive a base-origin outage after
  the first successful boot. Inert for plain dev sessions (no BOS_BUNDLE_DIR/
  BOS_BUNDLE_CACHE_DIR) — default behavior unchanged; stale serves only on
  origin failure, so normal operation keeps serving fresh.

Shared cache primitives live in everything-dev/bundle-cache (subpath export;
the host already imports everything-dev/* subpaths).

* refactor(everything-dev): code-artifacts as Effect.fn with tagged ArtifactGenError (C1)

generateCodeArtifactsEffect is the Effect-native core (tagged
ArtifactGenError per phase: write resolved config, load resolved config,
sync api contract bridge, generate ui manifest); the exported
generateCodeArtifacts keeps its Promise signature as a bridge — callers
(dev-program, publish) unchanged. The Effect export is available for the
later dev-program adoption.

* refactor(everything-dev): config.ts error taxonomy as Schema.TaggedError (C2)

All thrown Errors become tagged errors (ConfigNotLoadedError, ConfigLoadError,
CircularExtendsError, ConfigNotfoundError, ConfigExtendsError) — every one an
Error subclass with the identical message string, so all plain callers'
.message reads and the wrapped-cause behavior are unchanged. This gives the
later Effect-native phases a typed error channel instead of string matching.

* refactor(everything-dev): config pipeline as Effect.fn cores (C3+C4)

loadResolvedConfig, resolveRuntimePlugins, and buildRuntimeConfig become
Effect.fn generators with a typed error channel; exported signatures stay
Promise-based via bridges, so no caller changes. Error taxonomy (C2) carries
the channel: ConfigLoadError wraps at the load boundary with the identical
"Failed to load config from …" message; inner failures propagate raw before
that wrap, matching today's throw/wrap order exactly.

- resolveRuntimePlugins: Promise.all → Effect.forEach (unbounded, order-
  preserving); per-plugin resolution via tryPromise with ConfigExtendsError
- buildRuntimeConfig: sync body stays inline; the two async islands (auth
  entry, manifest discovery) become tryPromise steps — the catch-all
  discovery diagnostics stay plain inside the promise (same warn messages,
  same degrade-to-warning semantics), keeping console out of Effect code
- loadResolvedConfig: the pipeline composes the Effect cores; the
  suppress/drain/resume warning discipline is preserved call-for-call
  (resume on both paths via the inner catch, drain on success only)

Behavior-preserving throughout: 634 framework tests green (config-resolved
golden tests pin the outputs), typecheck 9/9, lint 0 errors.

* ci: relocate the remote-runtime smoke to the Deploy workflow (ADR 0009 §4)

runtime-remote.test.ts is a live-network smoke: it reads the branch's raw
bos.config.json and proxies real traffic (/skill.md, /llms.txt) to
config.app.ui.production. Once #228 commits the deterministic bundle URLs,
that origin is the deployment this PR ships — a PR CI suite that depends on
production's uptime fails whenever the gateway blips, and before the deploy
it validates URLs that cannot serve yet (Host tests red: "expected 502 to be
200", 60s hook timeouts).

The split was already prepared but never wired: vitest.remote.config.ts owns
exactly this file and host has a test:integration:remote script with no
consumer, while the default vitest.config.ts include swept it into PR CI.

- Default host suite excludes tests/integration/runtime-remote.test.ts
  (configDefaults preserved) — PR CI no longer touches live origins
- deploy.yml runs the smoke after mf check: boots the real host program
  against the live production URLs — SSR compose from the deployed ui,
  doc-asset proxy, api ping, root document contract — the functional gate
  beyond mf check's identity-only check

No helper changes, deliberately: in the Deploy context, non-2xx origins
make the smoke fail loudly — correct for a post-deploy gate; a down site is
already caught by mf check's retry loop.

* fix(host-tests): pin the runtime-config fixture to development env

loadTestRuntimeConfig called loadResolvedConfig() with no env — with the
NODE_ENV-honoring default (hermetic boot), the host test script's
NODE_ENV=production resolved production env in the fixture: api source
remote → manifest discovery fetched the deployed origin (down, 10s×3 retries)
→ every beforeAll hook blew the 10s timeout → 6 suites failed at collection
(csp-compose, csp-nonce, security, seo, ssr-metadata, ssr).

Fixtures resolve the repo's own local workspaces; NODE_ENV=production in the
host test script exists for the host's production code paths, not for config
resolution. Pin env: "development" — live-origin coverage belongs to the
Deploy workflow's smoke (relocated earlier in this PR).

* feat(api,ui): generalize the node model beyond geography

Drop the node_kind enum and the nodes.kind column — the kind label moves
into nodes.metadata.kind — and make nodes.tenant_id nullable so
standalone org/user/zone-root nodes can exist without a tenant.
parentId is now the only hierarchy axis.

- New org-scoped spawnNode surface (POST /nodes/spawn): any kind, any
  parent, no kind-validated parentage or depth limit; kind and geo
  details are metadata.
- applyNodeProposal keeps the strict country→state→city ladder as the
  DAO geo-provisioning path (tenant + validator + binding stay atomic).
- Validator staking walk and listTenantApps are unchanged — already
  parent_id-driven — with kind labels read from metadata.
- Standalone nodes (null tenant) can only be mutated by platform admins.
- Migration 0004 backfills metadata.kind and is replay-idempotent
  (journal-wipe safe; no procedural SQL — the virtual migrations loader
  splits statements on semicolons).
- UI kind displays tolerate arbitrary labels; NodeSchema.kind and
  NodeSchema.tenantId are now nullable.

* refactor(api,ui): Effect-native node surface + review fixes

Code-review follow-ups on the node-model generalization, both axes:

Standards:
- NodesService is now Effect-native (proposals-plugin precedent): every
  method returns Effect<T, ORPCError<string, unknown>>, implemented with
  Effect.gen; drizzle queries adapt at the promise boundary via
  Effect.tryPromise + toOrpcError, business failures via Effect.fail.
- All node oRPC routes are .effect(function* ...) handlers with
  yield* ApiServices — no plain Context.get on the node surface;
  createNode/updateNode/deleteNode/listNodes/getNode/getSubtree/
  getNodeSummary/resolveNodeBySlug/spawnNode included.
- resolveNodeForAccess/authorizeNodeAccess are shared Effect helpers;
  the standalone-admin gate and tenant-ownership check live in one
  place instead of three copies per handler.
- nodeKindOf (api) and nodeKindLabel (ui) extract the repeated
  kind-label/metadata-extraction shapes; kind inputs require min(1).

Spec:
- spawnNode now requires platform admin for standalone (null-tenant)
  spawns — closing the mutate-without-spawn asymmetry — and validates
  parentId ownership (org members can only graft under their org's
  tenants; admins bypass as with reparenting).
- mergeKindMetadata: the explicit kind argument now wins over
  metadata.kind; update still relabels via metadata.kind alone.
- createNode keeps its required kind input (unrequested relaxation
  reverted to an open string, not the geo enum).
- CONTEXT.md node definition generalized.

Tests: harnesses compose Effects (Effect.suspend + Effect.isEffect);
three multi-step node test bodies flattened to single-call runService
chains; new tests for kind precedence, standalone-spawn admin rule,
and cross-org graft rejection. 152 api + 431 ui tests green.

* refactor(api,ui): tier-1 cleanup — shared tenant guard, test harness, kind labels

- requireTenantOwnedByOrg: one Effect guard for the duplicated
  createNode/spawnNode tenant-resolve + org-ownership prologue
- createServiceHarness (api/tests/unit/test-harness.ts): the
  runService/squashServiceError pair consolidated from three copies
  (nodes/validators/tenants unit tests) into one Promise|Effect
  -composing harness with a single TestRunError unwrap
- admin nodes kind-filter labels derive from nodeKindLabel +
  geoNodeKinds instead of a second hand-written label map

Tier 2 (Effect-native Tenants/Validators/Discovery conversion) filed
as #252.

152 api + 431 ui tests green; typecheck + lint clean.
…effect barrels) (#260)

* chore(framework): remove dead every-plugin facade remnants (zod/orpc/effect barrels)

The facade barrels (every-plugin/zod, every-plugin/orpc, every-plugin/effect)
were deleted by the effect-native-plugins changeset, but dead remnants taught
the deleted convention and pointed at files that no longer exist:

- packages/everything-dev tsconfig: drop the every-plugin/effect, /orpc,
  /orpc/openapi, /zod, and /zod/v4/core path aliases (targets deleted)
- packages/every-plugin tsconfig: replace the every-plugin/* wildcard with
  the one subpath it actually carried (build/ui)
- skills + _template/LLM.txt: teach direct imports (zod, @orpc/*, effect)
  instead of the deleted barrels

Why this matters: the deployed auth plugin's embedded types/contract.d.ts
was emitted before the barrel deletion and still imports z from
every-plugin/zod. Child projects scaffolding via bos init cannot resolve
that subpath (no exports entry, never had one), and skipLibCheck silently
degrades z to any — which collapses AuthContext to any, erases the
WithEffectContext intersection in the oRPC builder context, and makes
@orpc/experimental-effect type .effect() handlers' yield channel as
Effect<any, any, never>. Effect-native handlers that yield services
(#250's yield* ApiServices pattern) then fail TS2345 in the scaffolded
child's typecheck while the parent repo passes (it resolves its locally
emitted declarations). init.full.test.ts is the canary for this — it
keeps the real fetch path and stays red until the merge-triggered deploy
refreshes the deployed manifest.

* ci: run Framework tests when api/ or plugins/ change

#250 generalized the node model with Effect-native handlers in api/src/index.ts
and merged with Framework tests skipped — the path filter only triggered them
on packages/everything-dev changes. init.full.test.ts scaffolds a child from
api/ and plugins/_template, so changes to those paths must trigger it too.

* test(everything-dev): skip the scaffolded-api typecheck while the deployed auth artifact is stale

The scaffolded api typechecks against the auth plugin's deployed contract
declarations fetched through the extends chain. The deployed artifact still
imports z from "every-plugin/zod" (unresolvable since the facade-barrel
deletion), which collapses AuthContext to any and fails the Effect-service
handlers with TS2345. Detect the broken import in the fetched declarations
and skip the api assertion with a loud warning until the auth plugin is
redeployed — everything else (scaffold, install, types:gen, plugin
typecheck) stays enforced.
…255)

* refactor(api): Effect-native Tenants, Validators, Discovery services

Convert the three remaining promise-based services to the NodesService
Effect-native shape (methods return Effect<T, ORPCError<string, unknown>>,
Effect.gen bodies with drizzle adapted at the promise boundary):
- TenantsService, ValidatorsService: flat Effect.gen with a shared query
  helper; unique-violation sites map isUniqueViolation to CONFLICT before
  toOrpcError; transactions keep db.transaction promise callbacks
- DiscoveryService: fully converted internals — authorize/eligible/
  publicActivity/list/syncLuma/syncDueLuma all Effect-returning; background
  Luma sync uses Effect.forkDetach with a synchronous single-flight flag;
  per-connection failures still record to discoveryLumaConnections.error
- api/src/index.ts: all tenants/validators/discovery routes are now
  .effect(function* ...) handlers with yield* ApiServices; authorizedTenant
  and authorizedNodeForValidators are Effect guards; validateAccountId/
  validateHostname keep BAD_REQUEST in the error channel; createEventOn-
  boardingCode loads the event through the direct error pathway (no more
  message-munging catch); asOrpcEffect deleted

No contract changes — routes, inputs, outputs, and error codes identical.
Closes #252

* refactor(api): Effect-native verifyDaoMembership — direct yield, no toOrpcError in routes

- verifyDaoMembership returns Effect<VerifyDaoMembershipResult, ORPCError>;
  the retry loop keeps its exact semantics (3 attempts, last error surfaced
  in the BAD_REQUEST cause) via Effect.result + Result.isSuccess
- createTenant and applyNodeProposal routes yield it directly — the last
  two toOrpcError adapter sites in api/src/index.ts are gone; toOrpcError
  now lives only inside the services' query funnels
- integration test mocks return Effect.succeed (node-proposals
  mockResolvedValueOnce → mockReturnValueOnce)
…ion stacks (#258)

* fix(db): tolerate concurrent-migration DDL race; fail loud on regression stacks

Root cause of the flaky dev:ssr/dev:csr auth-redirect failures: dev stacks
boot each local plugin twice concurrently (its own dev server process and
the host's in-process MF load), and both auto-apply migrations against the
fresh database. Concurrent CREATE TABLE collides in pg_catalog — the loser
gets SQLSTATE 23505 on pg_type_typname_nsp_index, which the SAVEPOINT
duplicate-DDL tolerance (42710/42701/42P07 only) did not recognize. The
escaping DatabaseError classified as unknown -> permanent, so the #253
fail-fast load policy killed the auth plugin where the old 120s silent
retry used to self-heal. The host then served without /api/auth/*, and the
only two browser specs that need an authenticated session timed out.

- db/core.ts: isConcurrentDdlUniqueViolation — 23505 whose constraint/
  message matches the pg_catalog allowlist (pg_type_typname_nsp_index,
  pg_namespace_nspname_index, pg_class_relname_nsp_index). Data-level
  unique violations stay fatal.
- db/run-migrations.ts: SAVEPOINT catch tolerates the catalog collision;
  each migration transaction retries on deadlock/serialization/lock/
  connection states (isRetryableMigrationExecutionError — deliberately
  narrower than the journal-init set so duplicate/unique classes are
  either tolerance or fatal, never retry).
- browser global-setup: abort the suite immediately when /health reports
  structured plugin-load failures instead of failing specs with
  downstream timeouts.

everything-dev: 705 tests green (11 in db-run-migrations); bun typecheck
and bun lint clean.

* refactor(db): dedupe cause-chain walking; import PluginLoadFailureInfo

Two-axis review follow-ups on the tolerance commit:
- core.ts: one visitCauses walker now backs isRetryableMigrationError,
  isRetryableMigrationExecutionError, isConcurrentDdlUniqueViolation, and
  run-migrations.ts isDuplicateObjectError (was the same bounded loop four
  times, at two depths); CONCURRENT_DDL_CONSTRAINTS is a ReadonlySet to
  match neighboring sets; 55P03 joins RETRYABLE_SQLSTATES for parity with
  the statement-level set.
- run-migrations.ts: isTolerableDuplicateDdl drops its never-exercised
  default parameter — the sole call site always passes duplicateSqlStates.
- regression global-setup: consume every-plugin's PluginLoadFailureInfo
  instead of re-declaring a drifting subset as HealthFailure.

* feat(db): serialize migrations on a journal-scoped advisory transaction lock

The structural fix (ADR 0019) for the concurrent-migration race class:
runMigrations now executes the entire run — advisory lock, journal
schema/table creation, data-schema creation, journal read, per-migration
DDL, and journal inserts — inside ONE db.transaction whose first statement
is pg_advisory_xact_lock(hashtextextended('<journal>.<table>', 0)).

Drizzle's transaction pins one pooled client for the callback, so lock,
journal read, and writes share one session; the xact-scoped lock releases
at COMMIT/ROLLBACK or connection death — a crashed migrator can never
orphan it, and no acquire/release pairing exists to leak. A concurrent
migrator (dev double-boot: plugin dev-server + the host's in-process MF
load; parallel vitest files; overlapping prod replicas) blocks bounded by
the driver's lock_timeout, then re-reads the winner's committed journal
rows and applies nothing.

- run-migrations.ts: runLockedTransaction replaces the per-stage flow;
  journal-init retry is absorbed (its race cannot happen under the lock);
  the preflight 'record as applied' write moves inside the locked
  transaction; SAVEPOINT tolerance stays as defense-in-depth for
  non-participants (drizzle-kit). Whole-tx Effect.retry on 55P03 /
  deadlock / serialization / connection states.
- driver.ts: ensureNamespaceExists takes the same lock before
  CREATE SCHEMA so it cannot race a migration run.
- migrate-test-db.mjs: the regression pre-migration mirror takes the
  same lock in one explicit transaction.
- tests: 55P03 lock-timeout retry characterization; two-concurrent-
  runners race test gated on TEST_DATABASE=postgres against the committed
  test DBs (verified 5/5 locally on real postgres: loser applies 0, one
  journal row per hash).
- ADR 0019 records the decision, residuals, and the future deploy-time
  (alchemy) tier; changeset bumped to minor.
…yer gating (#259)

* test: remove low-signal unit tests, add E2E coverage for 404 and relayer gating

Delete 26 low-signal test files (mock-echo/tautology assertions subsumed by
browser E2E), trim tautological describes from 6 more, and replace the two
real coverage gaps with regression browser specs:

- not-found.spec.ts: 404 fallback renders with a working back-home link
- admin.spec.ts: relayer connect-before-fund gating via injected admin
  cookies; wallet-gated invalid-amount state represented as test.skip
- playwright config: retain traces on failure

Add testing-rules subsection to AGENTS.md banning tautology tests and
reserving unit tests for what E2E cannot reach.

* test(ui): fix thing-details flake under loaded runners

The vote button renders only after a four-round sequential query chain
(proposal -> thing -> count + userVote -> view). Under a cold, loaded
vitest worker the testing-library default 1000ms timeout can expire
before the chain settles. Give the async assertions in this file an
explicit 5000ms timeout.
…d train (#256)

* fix(ui): kill the ?? "citynode.app" gateway default — honest missing-config states (#203)

All seven sites derive the gateway from the runtime config via the new
getGatewayId() accessor (null when missing/mis-shapen, never a default):
render explicit error states, disable dependent queries, fail mutations
with a clear message. Node directories fall back to a node's own hostname;
the tenant wizard's Extends row shows the configured gateway.

Also fixes two pre-existing noUnusedFunctionParameters lint errors in
api/src/index.ts and normalizes a stale bos-ui bin entry in bun.lock.

Closes #203

* fix(host): fail-loud sweep — dropped errors, honest timeouts, probe in health (#204)

- attestation catch logs the error cause instead of a causeless warning
- the auth 504 path logs the underlying Better-Auth failure it used to swallow
- AUTH_TIMEOUT_MS parses defensively: garbage and non-positive values fail
  boot loudly; unset keeps the 30s default
- both federation noop catches log at debug with the cause (logger gains debug)
- the post-listen self-probe surfaces its outcome in /health (ssr.selfProbe);
  a failed probe degrades the overall status; the unreachable "composing"
  acceptance in the health check is removed
- an empty auth origin in production fails boot loudly — the development
  localhost:3000 fallback stays (deliberate, parseTrustedOrigins-owned)

Closes #204

* fix(everything-dev): fail-loud sweep — env precedence, loadEnv, warning finalizers, log follow (#206)

- descriptor env folds into the generated tier before composeSpawnEnv —
  shell-exported values keep outranking it (documented three-tier
  precedence, now test-pinned) instead of silently outranking everything
- a failed .env load propagates into the database-binding error
- bos logs --follow refuses a missing/unknown log file; the readFile
  rejection is handled and the watcher closes on rotation
- suppressWarnings around runtime-config builds is failure-safe
  (acquireUseRelease) at both the development and start sites
- DB_*_TIMEOUT_MS parse defensively: explicit 0 disables, garbage fails boot
- the start path passes the generated env tier through (DevSessionData
  requires it; runApp takes its tiers without silently-empty defaults)

Closes #206

* ci(build): consume the build train — dedup the sprinkled prerequisite builds (#209)

SSR-mode decision (measured): every-plugin's ui build already carries the
explicit escape hatch — includeNodeEnv honors BOS_SSR=1 / DEPLOY=true
(packages/every-plugin/src/build/ui/rsbuild-config.ts:158) — so jobs that
need SSR dists set BOS_SSR=1 and the train keeps its development-mode
NODE_ENV forcing untouched. No ambient-NODE_ENV games.

- the 12 hand-maintained prerequisite-build steps across 7 jobs collapse
  into one train call per job (bun run build template — quiet,
  staleness-checked every-plugin/everything-dev/better-near-auth + target);
  the lint job's early builds are deleted outright: audit/lint/typecheck
  are src-level via the development export condition (ADR 0018)
- host/tests/global-setup.ts ensureUiBuild routes through the train
  (bun run build ui, BOS_SSR=1) with a missing-or-stale dist check —
  the silently-stale path is gone

Closes #209

* fix(host): build the test ui dist directly — the train bakes the dev assetPrefix into SSR

Two host-test failures on this branch:

1. buildAuthBaseVariables threw 'Invalid URL' on an empty dev host url —
   originSource = '' is not nullish, so the ?? fallback never applied. Use a
   truthiness guard; the empty dev case stays a deliberate pass-through
   (parseTrustedOrigins owns the localhost fallback) and the production
   fail-loud throw is unchanged.

2. The SSR integration suites (csp-nonce, seo, ssr-metadata, ssr) failed in
   beforeAll with ECONNREFUSED localhost:3003 — 'cleanup is not a function'
   was just the cascade (beforeAll threw before assigning cleanup).
   global-setup built the ui through the build train, but bos build forces
   NODE_ENV=development (only --deploy flips it), so rsbuild baked the dev
   assetPrefix (http://localhost:3003/) into the SSR container and the tests
   fetched shared deps from a dev port nothing listens on. Build the ui
   directly with the inherited NODE_ENV=production instead; framework sources
   resolve from src in tests (vite-tsconfig-paths), so no train prerequisites
   are needed. The staleness check stays.
…me (#207) (#257)

* feat(everything-dev): api subpath — createAuthMiddleware framework-home, collapse the five copies (#207)

- new everything-dev/api subpath (src in dev, dist in prod — the ui/auth
  pattern) exporting createAuthMiddleware, generic over the workspace's
  auth context: createAuthMiddleware<AuthContext>(builder) preserves the
  exact narrowing the concrete copies provided (AuthContextShape is the
  structural minimum the guards read)
- api + _template/apps/proposals/votes import from the subpath; the five
  near-identical sync-owned copies are deleted
- bos sync drops lib/auth.ts ownership (api file entry + the plugins
  lib/auth regex — context.ts stays owned); AuthContext/
  AuthOrganizationContext now source from the generated auth-types.gen
- changeset: breaking sync-surface change for children

Out of scope (unchanged): the DecoratedMiddleware/.use() typing limitation
advisor-plan 007 called out.

Closes #207

* test(everything-dev): sync structure — auth.ts is no longer framework-owned

#207 moved createAuthMiddleware into everything-dev/api (framework home) and
sync.ts stopped owning api/src/lib/auth.ts and plugins/*/src/lib/auth.ts —
the test still asserted the old contract. Auth files are app-owned now;
context.ts stays framework-owned.
…ng-config card (#263)

* feat(everything-dev): scaffold agent workflow skills into child repos (bos init)

bos init now copies .agents/skills/ (verbatim mattpocock workflow skills +
repo-authored everything-dev-app orientation glue), docs/agents/ conventions,
and skills-lock.json into child repos; bos sync owns all of it. Child
AGENTS.md/skill.md/llms.txt surface the ordered development flow.

Also fixes bos init crashing on TS-form (bos.app.ts) children: shared-deps
sync receives the converted config explicitly and config resolution tolerates
running before the first bun install.

* feat(ui): node-config bundle auto-fill from a deployed app + My Node pending-config card

Tickets 05 and 06 of the deploy-ux set (.scratch/deploy-ux/issues/):

- Org node-config Custom UI bundle section gains "Fill from a deployed
  app": enter the NEAR account that ran bos publish --deploy, fetch its
  published config via the apps plugin, and fill the bundle URL +
  integrity (SSR too when allowed) instead of pasting and hashing manually.
- My Node dashboard shows an "Awaiting votes" card for owners/admins when
  a DAO config-write proposal is pending, deep-linking into the org
  node-config tab.
- CONFIG_WRITE_PLAN moved to the shared sputnik-proposals lib so both
  surfaces match the same plan.
…LS verification) (#264)

Railway private-network Postgres presents a self-signed certificate chain
no client CA bundle can verify, so bare non-local URLs failed plugin and
auth boots with 'self signed certificate in certificate chain'. Those
hosts are VPC-scoped private traffic — resolvePoolSsl now returns
ssl: false for them, same as localhost / host.docker.internal.
…262)

* docs(adr): universal runtime image + R2-backed child bundle storage (ADR 0020, 0021)

ADR 0020: all bundle distribution moves to an R2 bucket behind
cdn.everything.dev — root's own URLs included (dual home: image for
boot, R2 for distribution), children never ship images. Amends ADR
0011 (image-native keeps the boot role only), reinstates the child
half of ADR 0015 (oRPC storage route design, object-store backend).

ADR 0021: one universal runtime image published to GHCR; tier is
selected at boot by identity x staged bytes (tier auto-detection),
not by config. Renames the deployable Dockerfile stage to 'runtime'
(last, default target) and the regression fixture to 'regression'.

* feat(api,everything-dev,host): universal image tiers + R2-backed bundle storage

Phase 1 (ADR 0021): Dockerfile deployable stage named `runtime` (last,
default target), regression fixture renamed `regression`; tier
auto-detection (unstaged identity → registry tier + notice); inbound
namespace guard on the host FS bundle route; BOS_BUNDLE_CACHE_DIR in
the image; GHCR push by digest + pull-only Railway deploy.

Phase 2 code (ADR 0020): BundleStorage Effect service (S3-compatible
via aws4fetch — content-type + cache-control as first-class headers,
per the cloudflare-cdn ticket-02 lesson; memory fallback); the
POST /api/storage/bundles route (auth, account pinning, path allowlist,
traversal rejection, size ceiling, server-side SRI) with a
route-scoped host bodyLimit; bos publish CDN upload path — batched
dist uploads, URLs at the CDN origin, integrity fields (ssr entrypoint
hash separate from the web entry).

Code-review fixes: sessions without a linked NEAR principal are
refused (unpinnable), validateUploadSize used by the handler, SSR
integrity no longer falls back to the web entry's hash, changeset
split into single-frontmatter files, deploy pins the pushed digest.

* feat!: CDN flip — all bundle URLs on cdn.everything.dev; host /bundles routes deleted

Effect rc.112 → rc.117 (user-approved RC upgrade; fixes the latent
effect/ByteSize tree skew via tsgo's hoisted platform deps); alchemy
collapses into a root devDependency.

infra/alchemy.run.ts: R2 bucket (retain, cdn.everything.dev domain) +
bucket-scoped S3 credentials via AccountApiToken — the token's
permission groups are Workers R2 Storage Bucket Item Read/Write over
com.cloudflare.edge.r2.bucket.<accountId>/<bucketId>.

Publish: the credential chain (BOS_STORAGE_API_KEY env → bos login
session; storage origin env → session siteUrl → gateway; CDN origin
env → the resolved config's inherited host slot) in cdn-deploy.ts;
bos.config.json bundle URLs flipped to cdn.everything.dev; deploy.yml
publishes with the CDN origin + BOS_STORAGE_API_KEY secret.

Host: /bundles/* serving deleted (FS handler, proxy+cache, proxy-mode
passthrough, oRPC mount) — the CDN owns distribution; the static-asset
proxy base+path join no longer produces double slashes. Harness static
servers mount the /bundles/<account>/<gateway>/<slot>/ base paths
themselves.

* chore: gitignore infra/.alchemy state

* chore(infra,ci): parametrize the CDN domain — drop the citynode-specific storage origin

deploy.yml drops BOS_STORAGE_ORIGIN: the chain derives it from the
runtime's own gateway (citynode.app now, everything.dev post-merge).
infra/alchemy.run.ts env-drives BOS_BUNDLE_CDN_DOMAIN (default
cdn.everything.dev) — sovereign bases override without editing the
stack. BOS_BUNDLE_CDN_ORIGIN stays explicit in CI as the
determinism rail.

* fix(every-plugin): dedupe the retry-logger test — bound failures instead of racing the retry budget

The dedup test relied on the 1100ms retry budget expiring before the
assertion; on CI the first-attempt timing could land such that the
deduped log never fired (expected +0 to be 1). Two bounded identical
failures then a success — same assertion, zero timing sensitivity.
Also formats the BUNDLE_CDN_DOMAIN const (biome).

* fix(ci): re-sandbox alchemy — the root devDep bloated every image build past the harness readiness budget

The infra sandbox collapse put alchemy's peer tree (AWS SDK, sharp
natives, vite, drizzle-kit rc) into the root lockfile; the Docker
builder's frozen install downloads it on every image build and blew
the start: regression readiness deadline (build >2m, main was 1m25s).
alchemy returns to infra/ (own lockfile, committed for reproducible
provisioning; node_modules gitignored); the root lockfile slims back
toward main. .dockerignore excludes infra — provisioning tooling
belongs in no image. The effect rc.117 bump stays.

* fix(regression): the auth slot's URL carries its server's base path

The auth static server mounts /bundles/<account>/<gateway>/auth but
app.auth.production still pointed at the bare root — the orchestrator's
auth remote load 404'd against the basePath guard, the plugin retried
forever, and the start: stack never became ready.
@elliotBraem
elliotBraem merged commit a868e68 into NEARBuilders:v2 Sep 27, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants