Skip to main content

Package structure and module boundaries

Status: accepted on 2026-07-29 and finalized on 2026-07-30.

Context

GigaLoom is a modular monolith with durable local state, two user-facing surfaces, built-in coding-agent adapters, provider compatibility layers, and extension tooling. The existing Python package grew as a mostly flat namespace. Several root modules now combine storage, application policy, provider transport, and presentation concerns, while the largest composition and facade files are thousands of lines long.

A big-bang move would create unnecessary compatibility and recovery risk. Leaving package shape implicit would let parallel refactors choose conflicting names and dependency directions. The repository therefore needs a frozen target tree plus ratcheting checks that reject new debt without requiring the legacy tree to become compliant in one commit.

The machine-readable policy is architecture/module-budgets.json. It is pinned to the frozen architecture source revision and records current root-module exceptions, module-size ceilings, import rules, owners, and removal gates.

Decision

The Python distribution is organized as a modular monolith by bounded context. Top-level package names describe a product domain or an adapter surface. Business behavior does not live in a generic global service, model, helper, or utility package.

The final root of gigaloom contains only:

__init__.py
entrypoint.py
py.typed

All other implementation belongs to one of these frozen contexts:

attachments/ automation/ cli/ contracts/ core/
diagnostics/ execution/ harnesses/ integrations/ native/
projects/ providers/ review/ runtime/ sessions/
skills/ tools/ ui/

Directories are introduced only with real behavior and focused tests. Empty placeholder packages are not created to make the checkout resemble the target tree.

Context responsibilities

ContextResponsibilityPublic boundary
coreTechnical primitives such as configuration, IDs, clocks, paths, redaction, serialization, instrumentation, and concurrencyCohesive named modules; no product-context imports
contractsStable provider-neutral DTOs, protocols, enums, events, permissions, and serialization contractscontracts/*; imports only core, stdlib, and typing
sessionsConversation state, messages, runs, event streams, exports, titles, authoritative filesystem storage, and derived read modelssessions/api.py and explicit contracts
runtimeDurable jobs, workers, leases, approvals, outbox, side effects, revisions, and reconciliationruntime/api.py and explicit contracts
executionProvider-neutral admission, preparation, invocation, persistence, continuation, and finalization of runsexecution/api.py
harnessesAdapter SDK, registry, conformance, plugins, and built-in agent adaptersharnesses/api.py and SDK contracts
nativeGeneric native-process lifecycle, discovery, snapshots, stores, and provider connectorsnative/contracts.py and explicit lifecycle ports
providersProvider registry, profiles, settings, accounts, authentication, normalized protocols, compatibility transports, and gateway configurationproviders/api.py
projectsProject configuration, memory, backup, workspace resolution, worktrees, and environment actionsprojects/api.py
attachmentsAttachment models, limits, MIME handling, rendering, and storageattachments/api.py
integrationsCatalogs, installable packages, lifecycle, flows, groups, and integration SDKintegrations/api.py
toolsTool contracts, profiles, policy, secrets, and managed or external MCP lifecyclePackage exports and tools/mcp/api.py
skillsBuilt-in, external, and portable skills plus library, authoring, and catalog proxy behaviorskills/api.py
automationAgents, workflows, schedules, evaluations, arena, and attention servicesSubcontext contracts and application services
reviewProvenance, reviewed evidence, artifacts, replay, promotions, handoffs, and support exportsExplicit read and reviewed-mutation contracts
diagnosticsDoctor checks, compatibility evidence, inventory, and performance toolingDiagnostic report and export contracts
cliLazy command-line parsing, dispatch, errors, and output adaptationcli/main.py; no business ownership
uiFastAPI composition, dependencies, routers, projections, streaming, and packaged Cockpit deliveryApplication services and transport schemas; no storage ownership

During the published migration window, root files or directories outside the contexts above are compatibility shims, not implementation owners. Their exact paths, owners, and removal gates are recorded in the manifest. A temporary name is not permission to add business behavior or create another sibling package.

Backend target shape

The target tree uses the following internal shapes. File names can be refined inside a context when an ADR explains the need, but the ownership boundary and dependency direction do not change implicitly.

core/
config.py errors.py ids.py clock.py json_codec.py paths.py
redaction.py security.py instrumentation.py concurrency.py

contracts/
harness.py execution.py events.py permissions.py providers.py
plugins.py serialization.py

sessions/
api.py models.py commands.py queries.py conversation.py
titles.py exports.py
events/{models,broker,cursors,projections}.py
storage/
protocol.py
filesystem/{catalog,manifests,messages,runs,events,raw_records,atomic,migrations}.py
read_model/{sqlite,schema,revisions,rebuild}.py

runtime/
api.py models.py
db/{connection,transactions,migrations,schema,diagnostics}.py
jobs/{repository,claims,leases,retries,cancellation,payloads}.py
workers/{service,repository,maintenance,heartbeat,wakeup}.py
approvals/{policy,authority,repository,audit,presentation}.py
outbox/{repository,dispatcher}.py
side_effects/{models,repository,executor}.py
revisions/repository.py
reconciliation/service.py

execution/
api.py context.py options.py admission.py preparation.py
continuation.py invocation.py persistence.py finalization.py
preflight.py readiness.py routing.py
structured/{sessions,processes,supervision}.py

harnesses/
api.py registry.py plugins.py
sdk/{contracts,conformance,scaffold,capability_matrix}.py
builtins/
echo.py
direct_chat/{adapter,transport}.py
codex/{adapter,workbench,plugin_target,mcp_target}.py
codex/app_server/{client,protocol,process,events,session}.py
claude/{adapter,workbench,agent_sdk,handoff,plugin_target,mcp_target}.py
gemini/{adapter,workbench,acp,extension_target,mcp_target}.py

native/
contracts.py models.py discovery.py registry.py snapshots.py store.py
process/{manager,pty,output,recovery}.py
connectors/{codex,claude,gemini}.py

providers/
api.py registry.py profiles.py settings.py migration.py
accounts/{models,sessions,broker}.py
authentication/{models,resolution,capabilities}.py
protocols/{normalized,openai,anthropic,gemini,gigachat}/
gateway/{proxy,preset}.py

projects/
api.py config.py memory.py bootstrap.py preferences.py backup.py
workspace/{resolver,tree,files,worktrees}.py
environment/{models,capture,commit,push,pull_request,github,editor}.py

integrations/
api.py models.py
catalog/{local,federated,sync}.py
packages/{models,installer,lifecycle,runtime}.py
flows/{models,service,repository}.py
groups/{models,service}.py
sdk/{contracts,conformance,scaffold}.py

tools/
api.py models.py profiles.py policy.py secrets.py
mcp/{contracts,external,managed,inventory,authoring,targets}/

skills/
api.py builtin.py external.py portable.py library.py authoring.py
catalog_proxy/{server,client}.py

automation/
agents/ workflows/ schedules/ evaluations/ arena/ attention/

review/
provenance.py evidence.py artifacts.py replay.py promotions.py
handoffs.py support.py

diagnostics/
doctor/ compatibility/ inventory/ performance/

cli/
main.py parser.py registry.py context.py errors.py output.py
completion.py commands/

ui/
app.py container.py dependencies.py
security/ schemas/ services/ streaming/ routers/ web/

sessions.storage remains authoritative and transparent. A SQLite read model is derived, disposable, and rebuildable. The same authoritative-versus-derived distinction applies wherever a context adds an index or projection.

Dependency direction

First-party imports follow this direction:

cli ui diagnostics -> public application APIs
automation review -> execution runtime sessions projects
execution -> sessions runtime harnesses projects attachments
harnesses -> providers native contracts
domain contexts -> contracts core
contracts -> core
core -> stdlib and approved technical dependencies

The diagram is a dependency ceiling, not a requirement that every lower layer import every layer above it.

Rules:

  1. core imports no product context.
  2. contracts imports only core, stdlib, and typing.
  3. Runtime and domain contexts never import cli or ui.
  4. Cross-context imports use the target context's api.py, contracts.py, or another explicitly named public port. They do not reach into repositories, storage, routers, widgets, or other internals.
  5. harnesses depends on neutral contracts plus provider and native public ports; provider modules do not import presentation surfaces.
  6. execution coordinates public session, runtime, harness, project, and attachment APIs without taking ownership of their persistence.
  7. automation and review consume bounded execution, runtime, session, and project APIs.
  8. CLI and Web adapt input and output to application APIs. They do not duplicate policy or query concrete filesystem and SQLite repositories.
  9. __init__.py files remain import-light and do not form circular re-export chains.

The architecture checker parses source with the Python standard-library AST. It never imports application modules. Existing internal cross-context imports are exact temporary exceptions; each exception has an owner and removal gate, and stale exceptions fail the check.

Frontend target shape

Cockpit uses a small feature-oriented structure:

frontend/src/
app/{App,router,providers,queryClient,shell}/
shared/{api,streaming,ui,hooks,lib,styles}/
entities/{session,run,approval,environment,provider,attachment,integration}/
features/{workbench,runs-center,integrations,settings,automation,evaluation,arena,inbox}/
widgets/{inspector,navigation,drawers}/
i18n/{keys,en,ru}/
main.tsx

Frontend imports follow:

app -> features, widgets, entities, shared
widgets -> entities, shared
features -> entities, shared
entities -> shared
shared -> third-party and browser APIs only

One feature does not import another feature's internal file. Shared behavior moves to a named entity or genuinely reusable shared module with its own tests. Barrel files must not hide feature-to-feature dependencies or cycles.

Test target shape

Tests follow the owning architecture and type of evidence:

tests/harness/
architecture/
unit/{sessions,runtime,execution,providers,harnesses,integrations,automation,projects,cli,ui}/
contract/{adapters,api,cli,sse,storage}/
integration/{durable_runtime,session_execution,application_surfaces}/
migrations/{sessions,runtime}/
performance/
e2e/
live/

Existing tests move only with their owning production context. A mass test-tree shuffle is not a prerequisite for implementation, and live remains explicit opt-in.

Module budgets

New executable Python modules have a hard limit of 600 physical lines without an ADR. The preferred range is 150–400 lines. A composition or compatibility facade has a 350-line hard limit and should normally remain between 50 and 250 lines.

Additional review limits are:

UnitTargetHard limit without ADR
Python function or method10–50 lines100 lines
Python class50–250 lines400 lines
FastAPI routerAt most 12 routes400 lines
React page or surface150–300 lines400 lines
React component50–200 lines300 lines
React hook or controller50–180 lines250 lines
TypeScript model or API file100–300 lines450 lines
CSS file100–300 lines450 lines

Every Python module above 600 lines at the architecture baseline has its exact physical-line count recorded as a ceiling. It may shrink or disappear, but it may not grow. Each structural slice reduces an owned legacy god-file by at least 15 percent while that file remains above its hard limit. The manifest is updated downward after the slice; raising a ceiling requires a separate ADR and measured reason.

Generated schemas and assets, frozen evidence fixtures, and migration SQL can be exempt only through an explicit manifest entry. An exception cannot contain executable business logic.

New generic utils.py, helpers.py, common.py, misc.py, global services.py, or product-wide models.py modules are rejected. Splitting one god-file into multiple files that still combine unrelated responsibilities is not completion.

Migration protocol

One module or cohesive responsibility moves at a time:

  1. Add the destination context or subpackage together with its public contract and focused tests.
  2. Move or extract behavior without redesigning it and without mixing in an algorithmic optimization.
  3. Preserve the old import path with a compatibility shim of at most 30 lines. Larger composition or protocol facades use only their explicitly recorded budget.
  4. Add or tighten an architecture rule so no new first-party consumer can adopt the legacy path.
  5. Migrate consumers in small owner-specific commits through the destination public facade.
  6. Verify entry points, dynamic imports, plugin targets, serialized type names, and persisted state compatibility.
  7. Before deleting the shim, search production, tests, examples, and docs; build the installed artifact; and verify both old and new imports for the approved compatibility window.
  8. Remove a shim only in a separate reviewed commit. If compatibility evidence is incomplete, retain it with an explicit owner and removal gate.

Storage migrations are repeatable after interruption. Structural moves preserve atomic writes, locks, leases, cancellation, reconciliation, idempotency, redaction boundaries, and authoritative JSON or JSONL state. A move never changes fsync, SQLite durability, public routes, SSE events, CLI output, provider passthrough, or approval semantics implicitly.

Refactoring phases

PhaseResult
R0Freeze this ADR, root namespace, import direction, ownership, temporary exceptions, and module budgets without moving production code
R1Split current choke-point files behind compatibility facades
R2Move flat root modules into bounded contexts using owner-specific commits
R3Migrate consumers to public facades and reject new legacy imports
R4Remove only compatibility shims with installed-artifact evidence; close or document remaining exceptions

core and contracts extraction is a separately approved change. It starts only after the integration owner confirms that hot-path owners have stopped changing the shared modules. Until that gate, types.py, config.py, the compatibility surface in execution/__init__.py, safe_paths.py, and instrumentation.py remain frozen.

Ownership during migration

AreaOwner
Session storage and queriesSessions maintainers
Runtime DB and facade foundationRuntime maintainers
Runtime jobsRuntime queue maintainers after the runtime-foundation gate
Runtime workers and wakeupRuntime worker maintainers after the runtime-foundation gate
Runner and new execution modulesExecution maintainers
FastAPI composition, services, streaming, and routersUI/backend maintainers
CLI and entrypointCLI maintainers
Frontend API and query contractsWeb API maintainers
Frontend WorkbenchWeb Workbench maintainers
Frontend streaming and bounded renderingWeb streaming maintainers
Architecture manifest, tests, and this ADRArchitecture maintainers
core and contracts extractionCore/contracts maintainers after explicit approval
Integrations, tools, and skillsIntegrations maintainers
Providers and built-in harnessesProvider/harness maintainers
Automation and reviewAutomation/review maintainers
Projects, workspace, and environmentsProject maintainers
Cross-cutting import migration, diagnostics, docs, and final cleanupArchitecture maintainers

Frozen shared contracts such as the registry and runtime policy or model surfaces change only through the integration owner. A thread that needs another owner's file submits an interface request instead of editing the file.

Compatibility and rollback

This decision changes package organization, not product behavior. CLI commands, flags, output, exit codes, REST paths, response shapes, SSE events and cursors, plugin entry points, provider-native passthrough, persisted state, and Web semantics remain compatibility contracts.

Each structural commit is independently revertible. Compatibility shims and the authoritative state formats provide the rollback boundary. Derived indexes can be deleted and rebuilt; authoritative user state cannot be discarded to make a package move easier.

Consequences

Parallel refactors can use frozen context names and ownership without creating new namespace conflicts. New modules are bounded immediately, while legacy god-files and internal imports can only stay level or shrink.

The cost is explicit facade design, temporary shims, a maintained exception manifest, and extra installed-artifact checks before cleanup. That cost is preferred to hidden cross-context coupling, broad consumer churn, or a high-risk big-bang rewrite.