Skip to content

Scope-first source organization

Status: approved plan for R0a (decision); the roadmap owns the delivery order. Implemented: every package was moved into owned scopes on 2026-09-29, and the maia-source ESLint rules enforce the contract below at error severity. The SO6 migrations removed every violation that predated the 2026-10-01 rules, so the shrink-only baseline is empty. Planned: the open SO1a tooling gaps and the behavior work in SO3–SO5 and SO7.

Repository rules keep the authority, Effect, dependency and verification requirements. The roadmap owns current implementation status. Do not copy these rules into every folder.

Scopes

A scope is one named responsibility, such as interaction, terminal control, observation or connection discovery. It is not a folder for all types, all services or all components of unrelated features.

  • Keep the existing packages. Add a package only for a shared boundary with at least two real consumers.
  • Put implementation, private models, policies, fixtures and tests beside their owner. Split a large scope by lifecycle, trust or behavior, not by file count.
  • Do not create empty scopes, one folder per function, forwarding chains without consumers, or mandatory service/controller/repository layers.
  • Name files by responsibility: interaction/recovery.ts, not helpers.ts. A scoped policy.ts may hold that scope's limits; a repository-wide constants barrel may not.
  • A move preserves public imports, operation IDs, wire meanings, database identities, receipts, confirmations and resource lifetimes. Simplify duplicated lifecycle and metadata handling, never authentication, revision checks, containment, cleanup, bounds or verification.

A scope grows by real responsibilities, not a template:

interaction/
├── index.ts             # export-only public entry
├── interaction.ts       # the scope's main file
├── interaction.test.ts
├── recovery.ts
├── recovery.test.ts
└── testing/
    └── fixture.ts       # scoped fixture, not a test bucket

A private type stays with its implementation until a second real module needs it. A folder that only contains child scopes needs no index.

Export-only indexes

An index re-exports explicit names from its owner and implements nothing:

export { makeInteractionClient } from "./client.js";
export type { InteractionClient } from "./client.js";
  • No declarations, imports, initialization, default expressions, schema construction, layer or registry assembly, or request handlers in index.ts.
  • No wildcard exports: every exported name is a public commitment.
  • Client assembly goes in a named module (for example local-node-client.ts), registry assembly in the registry's owning module, and executable startup in bin/ or a named composition module.
  • A source root may expose the package through src/index.ts, tested by src/index.test.ts. Everything else lives in scopes.
  • Use the package's module-resolution convention, including .js specifiers for compiled Node ESM.

Import direction

  • Within a scope, import the implementation file directly, never the scope's own index.
  • A relative import that leaves its folder targets the other scope's entry (../session/index.js or ../session), never a private file (../session/turn.js), including a parent reaching into a child. Children do not import the parent index.
  • A testing/ folder belongs to its enclosing scope; only tests and test support import it, from that scope or a scope nested in it.
  • Across packages, use declared package exports, never another package's src/ or dist/ path. The internal tree implies no new package subpaths.
  • Node-only transport and discovery stay under the explicit @maiaos/sdk/node subpath, never re-exported from the SDK root or a shared UI index.
  • Keep dependencies acyclic. An export-only barrel can still create a cycle or pull a native dependency into a browser bundle.
  • Tests may use private files of their own scope; boundary tests use public exports; fixtures never become exported production capabilities.
  • A temporary forwarder for a moved private path needs an exact consumer, a removal condition and an owner.

Private Effect entries

In packages/protocol a scope may add one private entry, effect-schema.ts, beside its transport-neutral index.ts (D-062). Protocol modules and tests import another scope's Effect Schema values through it. In packages/sdk a scope may add one private entry, effect.ts, holding its Effect-native operations (D-068); the scope's Promise facade and other scopes' Effect code import it. No index.ts, the package entry included, imports one, so the public declaration graph never reaches effect. The policy object names each workspace with its entry stem.

Tests and fixtures

  • A test sits beside the file it verifies: x.test.ts, x.spec.ts or x.integration.test.ts beside x.ts. A test of the whole scope is <folder>.test.ts or index.test.ts.
  • testing/ holds scoped support fixtures, not a parallel production system or a replacement for colocated tests. Share a fixture through the testing/ folder of the nearest scope its users share, such as src/testing/.
  • Product journeys stay in e2e/. Framework routes stay thin and delegate to tested feature scopes.
  • Effect-owned behavior uses deterministic layers, clocks and scoped resources. Native, transport and storage contracts still need real-boundary tests.
  • Changing directory depth means updating runner discovery, compiled test paths, source maps, build inputs and fixture paths. Prove that a nested sentinel test runs and that a failing one fails the runner. A zero-test run is not success.

Naming

ItemConventionExample
Folderkebab-case noun for one responsibility; no generic bucketmanaged-turn/
Filekebab-case; does not repeat its folder's namenative-host/frame.ts, not native-host/native-host-frame.ts
Main file<folder>.ts may repeat the folder namevoice/voice.ts
Test<file>.test.ts, <file>.integration.test.ts or <file>.spec.ts beside <file>.tsframe.test.ts; scope-wide voice.test.ts or index.test.ts
Entryexport-only index.tsexport { frame } from "./frame.js";
Fixturesdirect files of the scope's testing/testing/session-fixture.ts
Platform variant<file>.web.tsx or <file>.native.tsx beside <file>.tsx; counts as one modulerenderer.web.tsx
React hookuse-<subject>.ts beside its subject; naming rules read <subject>terminal-session/use-controller.ts, not use-terminal-session-controller.ts

Generic folder names (common, constants, helpers, shared, types, utils, tests, __tests__) and generic file names (common, constants, helpers, misc, shared, types, utils) are rejected: name the owner.

Sub-scopes

LimitValueRule
Implementation files in one folderat most 12folder-size
Siblings sharing their first two kebab wordsat most 3prefix-cluster
Folder levels below the source root (src/)at most 5max-depth

Tests, index.ts, declaration files, testing/ support and framework route files do not count toward folder size, and platform variants of one module count once. When four siblings share a prefix, move them into a sub-scope named after it and drop the prefix: node-api-server.ts becomes node-api/server.ts. Split an oversized folder by lifecycle, trust or behavior. The thresholds live in one policy object.

Framework and native code

  • The Expo Router route tree keeps its framework filenames; do not add index routes to satisfy the barrel rule. Route behavior lives in feature scopes.
  • Generated declarations, executable bootstrap and test-support folders get only their rule-specific treatment; ordinary feature code is not exempt.
  • Rust uses idiomatic modules and colocated Rust tests; Swift and other native files keep their platform boundary. No TypeScript indexes in native code, generated output, assets or documentation. ESLint checks do not cover Rust or Swift organization.
  • Planned (D-182): new Rust crates for the terminal screen engine/client, native process/PTY host and registry-generated binding live under crates/ in a root Cargo workspace beside pnpm. Desktop's existing Tauri crate stays in apps/desktop/src-tauri. RN0 freezes the crate conventions here (naming, module layout, lints, error types and where unsafe may appear), and the gate checks them with cargo fmt and clippy. services/node and its database migrations remain TypeScript/Effect.

Responsibility map

The same domain name may appear in several layers, but each layer owns a different responsibility. An interaction schema, SDK request, Node handler and view are not four implementations of answer authorization.

OwnerResponsibilityMust not own
packages/protocolOperation metadata, schemas, errors, events, receipts and exposure policyI/O, runtime services, client rendering or Node handlers
packages/sdkTyped requests, correlation/output verification, cancellation and explicit transportsBusiness authorization, durable storage, implicit Node startup
services/nodeAuthentication/authorization, durable state, execution, event order and authoritative projectionsClient-local focus, drafts or rendering
packages/client-corePortable client use cases, recovery and bounded projections over SDKAnother transport, native APIs, durable authority or worker supervision
apps/cliParsing, SDK-backed command execution and bounded outputHidden orchestration or a parallel backend
apps/clientRendering, local presentation state and explicit platform adaptersDirect Node transport/storage, model-controlled authority
apps/desktopNative host capabilities and platform transport/lifecycle adaptersBusiness-operation implementation
packages/agentOfficial worker integration and admitted resource/operation portsIndependent task truth or ambient authority

Product requests go through public SDK operations to the Node. Rendering and pure local selection may stay plain TypeScript; Effect owns non-trivial request, stream, admission, deadline, cleanup and cancellation behavior. Keep standard AbortSignal and the SDK public facade; expose Effect types from the SDK only through @maiaos/sdk/node/effect (D-068).

Remaining target splits (SO6)

Create these only with the work that needs them. container.* and execution workspace.* keep their meaning until the R1b naming migration.

  • SDK: split node/ into discovery/, transport/ and observation/, and voice/ into orchestrator/, managed-turn/ and pi-relay/. Preserve the voice subpaths and package declarations, and keep authored request-specific binding checks when consolidating mutation-uncertainty handling.
  • Node: done in SO6g. The Node API lives in runtime/node-api/ with http/, observation/ and request/, and storage/ holds database/, journal/, schema/ (the migration registry and SQLite policy) and migrations/ (vNNN.ts files in ranges of at most 12). Domain persistence lives with its domain; database ownership, transactions, the journal and migrations stay central. Shared lower layers, such as the journal, the Session record and the process resource contract, have their own scopes so that no scope entry loads a module that imports back into its importer. Never split an atomic write for folder symmetry, never change historical migration contents, and never make a runtime scope request-scoped because its handler moved.
  • Client: done in SO6c. It keeps app/, features/, platform/ and ui/; large features are split by behavior (features/voice/ into capture/, connection/, realtime/, managed-turn/, session/ and cockpit/; features/workspace/ into coordinator/, demo-automations/, portfolio/ and scope/; features/terminal/ into client/, output/, renderer/ and session/), and the design foundation lives in ui/theme/. Platform APIs and provider adapters stay at the platform edge. Reuse the Tamagui design system (decision).
  • Client core: add observation/ with its vertical slice. Recovery stores are bounded, ephemeral and owned above the component they serve; they never become server truth.
  • CLI: keep command-family scopes and a dependency-light help path; root command composition goes in named modules.

ESLint enforcement

The maia-source rules are exported through an export-only entry and composed at error severity in eslint.config.mjs and apps/client/eslint.config.mjs. They cover JavaScript and TypeScript under apps/*/src, packages/*/src and services/*/src. Each rule lives in its own folder with passing and failing fixtures, and each message names the fix and an anchor of this document.

RuleChecks
layoutOnly index.ts and index.test.ts at a src/ root; export-only indexes; generic folders; entry and tests
folder-casekebab-case folder names
generic-filegeneric file names
no-stutterfile names that repeat their folder
folder-sizeimplementation files per folder
prefix-clustersiblings that share their first two kebab words
max-depthfolder levels below the source root
test-pairinga test beside the file it verifies, or the scope-wide test
scope-entry-importsrelative imports that leave a scope target its index; test support stays with tests

Rules run per file, so the pre-commit hook can lint one staged file; directory rules read that file's on-disk siblings. An unreadable directory is an error that no baseline entry can suppress.

Exemptions

Each exemption covers only the rules named, never neighboring helpers:

  • Expo: index.tsx, _layout.tsx and +not-found.tsx at any depth under apps/client/src/app/. Other Expo route names, such as [id].tsx, need an explicit admission in the rule, its fixtures and this list.
  • Route files need no index or test, do not count toward folder size and do not stutter. Route segments are not generic folders and may use (group), [param], [...rest] and [[...optional]].
  • Bootstrap: direct non-test files in a workspace's src/bin/ need no index or test.
  • Fixtures: direct non-test files in a scope's testing/ (including src/testing/) need no index or test and do not count toward folder size or prefix clusters; testing/child/fixture.ts and tests in testing/ are not exempt.
  • Platform variants: .web, .native, .ios and .android files count as one module and pair with that module's tests.
  • Outside the source roots: e2e/ journeys, package scripts/, native code and build output.

scripts/quality/lint-policy.mjs requires each of the three configs to import the canonical sourceOrganization export and compose it in its default array. It also forbids inline ESLint comments, local rule overrides and oversized sources. Do not disable the rules inline, downgrade them or ignore source trees.

Legacy baseline

scripts/quality/lint-policy-baseline.json holds the lint-policy findings that predate the policy and, under sourceOrganization, each legacy (rule, path) with its occurrence count. ESLint suppresses a violation only up to its recorded count, so a new violation fails at once; the config carries the baseline's digest in its settings, so cached lint results expire when the baseline changes. pnpm lint:policy (part of pnpm check:quality) scans the tree, prints the counts per rule and workspace, and fails when a violation is missing from the baseline or an entry exceeds its current count; a tooling test runs the same check. After a migration, run node scripts/eslint/source-organization/audit/audit.mjs --prune, which lowers or removes entries and never adds one (it refuses while a directory is unreadable), and commit the smaller file. Since SO6g the sourceOrganization section is empty: every source-organization violation fails at once. The same ledger engine keeps the Effect-boundary baseline.

Dependency graph baseline

scripts/quality/dependency-graph.mjs (part of pnpm lint:policy, so of pnpm check:changed and CI) resolves every import under each workspace src/ to a source file and records, in scripts/quality/dependency-graph-baseline.json: exact edges inside recorded cycles and membership by kind; new acyclic edges are free; F2 owns package edges.

  • Resolution. Relative specifiers go through the TypeScript resolver; workspace packages (including a package importing itself) go through their package.json exports. An internal import that does not resolve is a diagnostic that fails the check. Assets and external modules are ignored.
  • Kinds. static (a value import or re-export), lazy (import()) and type (type-only, erased at build).
  • Cycles. Strongly connected components at three levels: static, withLazy (static and lazy edges) and withTypes (all edges). Each component lists its member files and the file edges inside it, by kind.

The check fails on a cycle that no recorded one covers, on a new edge inside a recorded cycle, and on a recorded cycle that changed or is gone. A swap of equal size or a reversed edge therefore fails. There is no accept mode: a new cycle is removed, never recorded. After you shrink a cycle, run node scripts/quality/dependency-graph.mjs --prune and commit the smaller file; --prune refuses while anything is new or unresolved. To read an entry, look at its level first: a static cycle can fail at load time, a withLazy one when the lazy path runs, a withTypes one only couples types. A full scan takes about 2.5 s.

Package ownership

scripts/quality/package-ownership.mjs (also part of pnpm lint:policy) reads the same production source and checks, per workspace:

  • Every package an import names is in dependencies, optionalDependencies or peerDependencies. Type-only imports count too, because published declarations expose them. A devDependency passes only when bundledDevelopmentDependencies lists it with a reason, such as a package an executable build bundles; a listed entry that source no longer imports, or that is not a devDependency, fails as stale.

  • A workspace import names a subpath the target package exports.

  • Workspaces import each other along the delivery order: packages never import apps or services, protocol imports no workspace package, and sdk imports only protocol (layerRules).

  • A relative import never leaves its workspace; cross-workspace imports use the package name.

  • A tsconfig alias is a package-shaped name that tsc emits unchanged, so no workspace may use one.

  • A dynamic import() whose target is not a string literal fails in the dependency graph check, unless computedImportAllowances lists its file and line with a reason.

Workspaces come from pnpm-workspace.yaml, so a package without a src/ is still known. Node builtins, self imports and test files are skipped. Unused declared dependencies are not reported, because configuration files load some packages.

Limits: the rules check names, paths, siblings and import specifiers. They do not prove that a scope is meaningful, a test covers its sibling, an authorization boundary is right, or a barrel adds no cycle. Those stay design review and build or contract checks.

Tooling checks:

node --test "scripts/eslint/**/*.test.mjs"
pnpm test:tooling
pnpm typecheck:tooling
pnpm exec eslint scripts/eslint/source-organization --max-warnings 0

Reachable files

scripts/quality/reachable-files.mjs (part of pnpm lint:policy) keeps files that no production entry imports out of the Node. A type-only import counts as reaching a file, so the check finds unreferenced files, not proven dead code. Every non-test source file under services/node/src must be reachable through static, lazy or type imports from an entry point: the package bin, every dist/ file a workspace script or a package.json script loads, and the files started by path that pathStartedEntries lists with the reason each is an entry. A file that only tests or testing/ support import is unreachable. Existing unreachable files sit in scripts/quality/reachable-files-baseline.json, each with a reason, and the file only shrinks: node scripts/quality/reachable-files.mjs --prune removes resolved entries and never adds one. To add a workspace, list its entry points in the script.

Phases and acceptance

SO IDs are work-plan labels, not operations or runtime resources. Each owner admits bounded work with exact files, criteria, independent review and recovery evidence. Keep structural and behavioral changes separately reviewable. The final gates are always pnpm check:quality and pnpm test; a scoped pass must report unrelated failures and is not release qualification.

SO0 — baseline and ownership

Repeated for each admission. Record the exact revision, dirty-file hashes, public exports, operation IDs, runner discovery, dependency boundaries and relevant artifact and startup sizes. Separate pre-existing failures from new ones and rerun the parity checker instead of reusing old findings. Exit: a reproducible baseline and an agreed scope.

SO1 — parity and status closure

Owners: protocol/SDK, CLI inventory and quality tooling. Repair the parity checker's inputs (scripts/quality/registry-parity.mjs), verify every exposed command and SDK binding, and update help identities. Consolidate metadata around the protocol registry without removing authored response-binding and authority checks. Exit: parity passes for the current surface; component docs keep source-present, integrated, verified and release-qualified apart. Maps to R0.

SO1a — tooling hardening before structural migration

Owners: quality/ESLint, hook and build-tooling owners with each package's runner and framework owners. No new package, dependency or policy framework. Closed already: lint-cache inputs (turbo.json lint tasks include the root config, scripts/eslint/source-organization/** and the baseline, tested by scripts/quality/turbo-lint-inputs.test.mjs), framework routes at any depth (rule fixtures for nested Expo and Next routes), recursive test discovery in packages/agent, packages/client-core, packages/sdk and apps/client, rule fixtures that fail on ignored, unlinted or unparsable files, and scope-entry-imports for scope entries and test-support imports. Open gaps:

  1. Test discovery. Closed for the package runners: the CLI and SDK runners collect .test-dist/** recursively, and the client runner collects src/**/*.test.ts, where an isolated failing nested sentinel returned nonzero. Built-artifact and E2E runners stay separate.
  2. Dependency boundaries. Self-index and parent-index imports and cross-package src/ or dist/ access are not checked yet. Acceptance: positive and negative fixtures across import and re-export forms. Cycles and browser/native isolation fail the owning graph or build checks.
  3. Config-policy agreement. The root and client configs keep local rule overrides that lint-policy forbids (held in the baseline). Acceptance: all three configs satisfy the policy without exceptions; shared-rule false positives are fixed at their owner with regressions.
  4. Staged-tree correctness. scripts/hooks/staged-files.mjs lists only added, copied, modified and renamed paths, and staged text is checked against working-tree siblings, so unstaged indexes or tests can satisfy a commit and deletions escape scope checks. Acceptance: isolated Git fixtures for partial staging, unstaged-only siblings, staged deletions and renames, and complete staged scopes; never modify the developer's index.

Review rule (not mechanical): an empty test file satisfies adjacency and an empty index satisfies the entry check, so reviewers check each scope's real responsibility, consumers, public surface and executed tests. Do not require one test per file or assertion counts.

Exit: independent review of the shared checks and each admitted package's runner cases; remaining package cases recorded and rerun before their own migrations.

SO3 — request and mutation lifecycle

Owners: SDK request boundary and client-core Effect owners. Today request and controller orchestration is split across Effect, Promise code and React hooks, and mutations normalize uncertain acknowledgements differently. Centralize non-trivial orchestration, cancellation and cleanup in Effect behind the existing facade and AbortSignal. Share mutation-outcome normalization while keeping operation-specific scope, digest, revision and command binding; reconcile the original command ID, never retry an unknown mutation under a new one. Exit: tests for pre-dispatch cancellation and rejection, post-dispatch loss, cancellation and timeout, malformed envelopes, correlation mismatch, replay, original-key retention and unmount cleanup, each defect reproduced without the fix first.

SO4 — independent client selection and voice ownership

Owners: protocol → SDK → Node → CLI → client-core/client and voice adapters. Today UI selection calls workspace.select on the singleton active Workspace context, and the Node voice orchestrator keeps at most four contexts (MAX_ACTIVE_VOICE_CONTEXTS), evicting the oldest on refresh. Separate local navigation from immutable command target and authority snapshots; replace the global eviction with bounded client- or Session-owned admission that rejects excess work explicitly. Exit: two independently selected clients, repeated refresh by one without invalidating the other, stale and wrong-owner denial, target-switch races, reconnect and disconnect without cancelling durable work; the multi-client minimum qualified through DF-28. Maps to R1/R7.

SO5 — observation latency and fairness

Owners: Node observation/storage and SDK subscription owners, then client projections. Today an empty journal read waits for the 5-second heartbeat (NODE_API_OBSERVATION_HEARTBEAT_MS) before reading again. Wake readers on commit independently of the heartbeat, with the journal as replay authority; use bounded per-observer queues, explicit slow-consumer gaps, resets and disconnects, and control reserves. Client callbacks never backpressure workers. Exit: an event-after-empty-read regression; authenticated snapshot, replay, gap and cancellation tests; malformed frames, slow consumers, reconnect storms and bounded memory under overload; committed-to-visible p95 under 250 ms on Desktop and 500 ms on mobile on qualified profiles, and at most 10% p95 regression for healthy, control and worker traffic (DF-28). Maps to R1/R9.

SO6 — client separation and remaining scopes

Owners: client-core and renderer/platform owners, then protocol, CLI, agent and Node scope owners. React renders; Effect services own resource workflows; platform adapters translate capabilities, not business policy. Deliver the remaining splits and remove temporary forwarders after their last consumer. Exit: per-scope unit and contract tests, built boundaries, renderer, accessibility and native checks where relevant, no forbidden imports or cycles, and no artifact or startup growth without measured justification.

SO7 — deterministic presentation, AI mode, qualification

Owners: protocol/SDK/Node and client presentation owners, after R9a's dependency, Effect Schema and catalog admission and the SO4/SO5 contracts (decision). Map mounted product views to real SDK operations; qualify the official json-render packages, transitive closure, licenses and renderer parity before installing them. Build deterministic composition first, then bounded Jev ranking after R10a. Trusted confirmations stay authored; a provider outage keeps manual views and control and reports AI mode unavailable under the shared readiness contract. Exit: PF-27/DF-27 view parity and PF-28/DF-28 isolation and fairness, malformed stream handling, UI crashes, Jev outage, native, accessibility and reconnect evidence.

Verification for each scope

  1. Read the scope's code, callers, schemas, state owner, tests and component rules; capture a baseline and claim exact files.
  2. Keep moves separate from behavior changes; keep a move map and the original export identity.
  3. Run formatting, lint, types, colocated tests, registry parity, nested discovery and affected public import and declaration tests.
  4. Exercise affected behavior and failure paths at the real boundary (SQLite, authenticated transport, built CLI, Node or native host) when those contracts change.
  5. Measure artifact size, dependency graph, CLI cold start and renderer startup when entrypoints or bundling change.
  6. Check documentation links and status; later edits invalidate earlier checks.
  7. Keep redacted logs, commands, toolchain, hashes and skipped checks outside build cleanup.

Roll back only the exact owned patch, never a shared checkout or another owner's work. A layout rollback never rolls back live data, processes or schemas.

Completed phases

Rows moved verbatim from the roadmap R0a table once done.

PhaseDeliverable and ownerEntry / exit and product mapping
SO1b — organization rules / DoneQuality tooling adds folder-case, generic-file, folder-size (at most 12 files), prefix-cluster (fewer than 4 shared prefixes), no-stutter, scope-entry-imports, max-depth (at most 5 levels) and test-pairing beside layout: one folder per rule with passing and failing fixtures, thresholds in one policy object, and messages that name the fix and a policy anchor (decision).Root/client/site configs run every rule at error; a single staged file and an unreadable directory are covered; ESLint and the full-tree scan agree per rule.
SO1c — shrink-only baseline / DoneOne tracked baseline in scripts/quality/lint-policy-baseline.json records each legacy (rule, path) with its count. ESLint suppresses recorded counts only; pnpm lint:policy prints counts per rule and workspace; audit.mjs --prune only lowers entries.A new violation fails lint and the tooling test; a stale or too-high entry fails until removed. Recorded 1 Oct 2026: 925 entries, 2403 occurrences.
SO6a — packages/agent / DoneDrop the folder prefix from extension files and name worker tests after the files they verify; exports and the bin path unchanged.Baseline 7 -> 0 entries; 66 tests in 16 files before and after; typecheck, build and public declarations unchanged.
SO6b — packages/client-core / DoneDrop the folder prefix from interaction and terminal files, merge portfolio failure tests into the scope test, and collect nested tests; export keys unchanged, two targets renamed.Baseline 7 -> 0 entries; 68 tests before and after (14 -> 13 files); a nested sentinel that the old runner glob missed is collected.
SO6c — apps/client / DoneSplit features/voice (six scopes), features/terminal and features/workspace (four each), move the design foundation to ui/theme/, give the error clipboard its own platform scope, drop folder prefixes in features, platform/ and platform/desktop, route relative imports through scope entries and collect client tests recursively; routes and lazy chunks unchanged, the unmounted terminal client left out of the platform entry.Baselines 162 -> 0 entries (296 occurrences) and 8 -> 0; 188 client unit tests before and after; E2E 115 passed, 1 skipped, 1 known WebKit timeout before and after.
SO6d — packages/protocol / DoneDrop names that repeat their folder; route imports through Effect-free scope entries and private effect-schema.ts entries (D-062); split execution/ into catalog, placement, relocation and worktree scopes; move shared wire primitives to envelope/ and turn events to session/; split contracts per family so tests pair; give feature descriptors and the operation registry their own scopes.Baseline 135 -> 0 entries (378 -> 0 occurrences); 481 -> 483 tests (two new scope tests), names unchanged; public export names locked and identical; no export or scope entry reaches effect.
SO6e — packages/sdk / DoneDrop folder prefixes from execution, node, request, session and voice files, pair the node, client and project-workspace tests with the files they verify, and collect nested tests; export keys unchanged, two voice subpath targets renamed.Baseline 25 -> 0 entries (25 occurrences); 122 tests before and after (24 -> 20 files); runtime and declared exports identical per key; a nested sentinel is collected and fails the run.
SO6f — apps/cli / DoneRoute cross-scope imports through scope entries, move family output projections into output/ sub-scopes and argument, prompt and stdin input into command-line/input/, load Node-backed families on demand, and pair tests with their files.Baseline 97 -> 0 entries; 359 -> 364 test runs (six formatter tests and a help-path guard added, two duplicate runs gone); standalone 259,256 bytes; help loads no Node client.
SO6g — services/node / DoneSplit runtime/process/resource (contract, tree, launch, native host), storage (database, journal, schema, versioned migrations as storage/migrations/v030-v039/v035.ts), runtime (errors, Node API with http, observation and request), execution and the Session and Terminal storages; give shared lower layers their own scopes so scope entries stay acyclic; move shared fixtures to src/testing/; name tests after the files they verify; route every cross-scope import through a scope entry.Baseline 484 -> 0 entries (1441 -> 0 occurrences), so the repository baseline is empty; 1930 -> 1932 tests (two new Session digest tests), names unchanged; migration registry and fresh schema identical; no import cycle; public exports and bin unchanged; maia-node.mjs 2,999,442 -> 3,006,730 bytes (+0.24%: 56 entry modules, and three modules that entries now load at startup).

Imported from Maia OS docs at a2175bb