t3code
t3code is a web GUI for coding agents. Its backend is not CRUD-shaped. It is built from Effect layers, evented reactors, provider registries, callback bridges, orchestration services, and projection pipelines.
That makes it a good case study for the analyzer, because the hard part is not finding isolated Effects. It is recovering architecture from a large backend that is composed through layers and higher-order services.
This walkthrough is based on the current analyzer output against the real t3code repository.
Start with coverage, not a single file
Section titled “Start with coverage, not a single file”Running the coverage audit on the server source gives a good top-level picture:
npx effect-analyze ./apps/server/src \ --coverage-audit \ --show-by-folder \ --quietCurrent output:
Discovered: 189Analyzed: 142Zero programs: 47Suspicious zeros: 0Failed: 0Coverage: 75.1%Analyzable coverage: 100.0%Unknown node rate: 1.46%
Top unknown node reasons (by count): 71 Could not determine effect type 16 Could not determine loop bodyThe most important signals here are:
142analyzable programs in the server source0failures0suspicious zeros1.46%unknown-node rate
That is already enough to trust the analyzer as a repo navigation tool. The remaining misses are concentrated, not random.
Layer architecture is the strongest repo-wide view
Section titled “Layer architecture is the strongest repo-wide view”t3code server code is heavily layer-driven, so architecture mode is more useful than a raw per-file flowchart:
npx effect-analyze ./apps/server/src \ --format architecture \ --no-colocate \ --quietCurrent output starts like this:
Project architecture (0 runtimes, 76 layer assemblies)
Layer assemblies: OrchestrationEngineLive (orchestration/Layers/OrchestrationEngine.ts) Ops: effect References: OrchestrationEngineService, makeOrchestrationEngine
OrchestrationProjectionPipelineLive (orchestration/Layers/ProjectionPipeline.ts) Ops: effect(...).pipe -> provideMerge -> provideMerge -> provideMerge ... References: NodeServices.layer, ProjectionProjectRepositoryLive, ProjectionThreadRepositoryLive, ...
ProviderRegistryLive (provider/Layers/ProviderRegistry.ts) Ops: effect(...).pipe -> provideMerge -> provideMerge References: CodexProviderLive, ClaudeProviderLive
RuntimeLayer (index.ts) Ops: empty.pipe -> provideMerge -> provideMerge -> provideMerge -> ... References: CliConfig.layer, ServerLive, OpenLive, NetService.layer, NodeServices.layer, FetchHttpClient.layerFor this codebase, that is the right starting point. The analyzer shows how the server is assembled:
- orchestration services
- persistence repositories
- provider adapters and registries
- runtime receipt bus
- terminal integration
- top-level runtime composition
This is much closer to how a contributor actually needs to read the backend.
Small architecture files are very readable
Section titled “Small architecture files are very readable”serverLayers.ts is a good example of where architecture mode is already concise:
npx effect-analyze ./apps/server/src/serverLayers.ts \ --format architecture \ --tsconfig ./apps/server/tsconfig.json \ --no-colocate \ --quietProject architecture (0 runtimes, 2 layer assemblies)
Layer assemblies: runtimeServicesLayer (serverLayers.ts) Ops: mergeAll References: orchestrationLayer, OrchestrationProjectionSnapshotQueryLive, checkpointStoreLayer, checkpointDiffQueryLayer, RuntimeReceiptBusLive
serverLayers-layer-2 (serverLayers.ts) Ops: mergeAll( orchestrationReactorLayer, GitCoreLive, gitManagerLayer, terminalLayer, KeybindingsLive, ).pipe -> provideMerge References: NodeServices.layerThis is exactly the kind of file where the analyzer helps immediately: it shows which layer groups exist and what they depend on, without making you read every import and provideMerge manually.
Callback bridges are now much clearer
Section titled “Callback bridges are now much clearer”Callback-heavy code is one of the places where the analyzer is useful in t3code.
Take the bootstrap reader:
npx effect-analyze ./apps/server/src/bootstrap.ts \ --format explain \ --tsconfig ./apps/server/tsconfig.json \ --quietCurrent output:
readBootstrapEnvelope (direct): 1. Yields fdReady <- isFdReady 2. Yields stream <- makeBootstrapInputStream 3. Returns: Pipes callback through: Registers callback bridge: callback Callback: 5 resume calls Inner effects: Calls cleanup — callback-handler Calls handleError — callback-handler Callback: Calls resume -> Effect.succeedNone — callback-resume Calls resume -> Effect.fail(...) — callback-resume Calls isUnavailableBootstrapFdError — callback-call Calls handleLine — callback-handler Callback: Calls resume -> Effect.succeedSome(parsed.success) — callback-resume Calls resume -> Effect.fail(...) — callback-resume Calls decodeJsonResult — callback-call Calls Result.isSuccess — callback-call Calls handleClose — callback-handler Callback: Calls resume -> Effect.succeedNone — callback-resume Times out after timeoutMs Transforms via mapThis is much better than a flat Calls fn or generic Effect.callback entry. The analyzer now shows:
- that this is a callback bridge
- how many resume paths exist
- the named handlers inside the bridge
- the important resume payloads
- the timeout and transform wrappers around it
For t3code, that is exactly the kind of explanation that makes async boundary code readable.
The Mermaid output is still useful when you want the condensed shape instead of the handler detail:
npx effect-analyze ./apps/server/src/bootstrap.ts \ --format mermaid \ --tsconfig ./apps/server/tsconfig.json \ --quietflowchart TB %% Program: readBootstrapEnvelope start((Start)) end_node((End)) n1["fn"] n3["fdReady <- isFdReady (side-effect)"] n4["stream <- makeBootstrapInputStream"] n5["return"] term_6(["return"]) n7["Pipe (2 steps)"] n8["callback"] n10["Effect"] timeout_11["Timeout(timeoutMs)"] n12["map (transform)"] %% Edges n3 --> n4 n8 --> n10 n10 --> timeout_11 timeout_11 --> n12 n7 --> n8 n5 --> n7 n12 --> term_6 n4 --> n5 n1 --> n3 start --> n1 n3 --> end_node %% Styles classDef startStyle fill:#c8e6c9,stroke:#2e7d32 classDef endStyle fill:#ffcdd2,stroke:#c62828 classDef effectStyle fill:#90EE90,stroke:#333,stroke-width:2px classDef pipeStyle fill:#ADD8E6,stroke:#333,stroke-width:2px classDef timeoutStyle fill:#87CEEB,stroke:#333,stroke-width:2px classDef terminalStyle fill:#FF6B6B,stroke:#333,stroke-width:2px classDef transformStyle fill:#A5D6A7,stroke:#388E3C,stroke-width:2px class start startStyle class end_node endStyle class n1 effectStyle class n3 effectStyle class n4 effectStyle class n5 terminalStyle class term_6 terminalStyle class n7 pipeStyle class n8 effectStyle class n10 effectStyle class timeout_11 timeoutStyle class n12 transformStyle
Reactor streams are now recognizable
Section titled “Reactor streams are now recognizable”Provider registry code is another place where the analyzer now recovers useful stream structure:
npx effect-analyze ./apps/server/src/provider/Layers/ProviderRegistry.ts \ --format explain \ --tsconfig ./apps/server/tsconfig.json \ --quietProviderRegistryLive (generator): 1. Yields codexProvider <- CodexProvider 2. Yields claudeProvider <- ClaudeProvider 3. changesPubSub = Acquires resource: pubsub.create Then releases: Calls PubSub.shutdown 4. Yields providersRef <- make 5. Background stream reactor (CodexProvider.streamChanges): runForEach -> runForEach Calls CodexProvider.streamChanges — service-call runForEach callback: Calls syncProviders — callback-call 6. Background stream reactor (ClaudeProvider.streamChanges): runForEach -> runForEach Calls ClaudeProvider.streamChanges — service-call runForEach callback: Calls syncProviders — callback-callThat is a real semantic improvement. The analyzer now understands that these are background reactors driven by provider change streams, not just anonymous stream pipelines.
The nested refresh program is also clearer now:
ProviderRegistryLive.refresh (generator): 1. Switch on provider: Case "codex": Calls CodexProvider.refresh — service-call Case "claudeAgent": Calls ClaudeProvider.refresh — service-call Case default: Runs 2 effects in sequential (concurrency: unbounded): Calls CodexProvider.refresh — service-call Calls ClaudeProvider.refresh — service-call 2. Returns: Calls syncProvidersEarlier versions of the analyzer lost these service-property calls entirely.
For this file, Mermaid now captures the reactor and refresh structure reasonably well:
npx effect-analyze ./apps/server/src/provider/Layers/ProviderRegistry.ts \ --format mermaid \ --tsconfig ./apps/server/tsconfig.json \ --quietflowchart TB
%% Program: ProviderRegistryLive.refresh
start((Start))
end_node((End))
n2["Switch: provider"]
switch_3{"Switch: provider"}
n4["codexProvider.refresh (service-call)"]
n5["claudeProvider.refresh (service-call)"]
n6["Effect.all (2) (concurrency)"]
parallel_fork_7{{"All (2)"}}
parallel_join_7{{"Join"}}
n8["codexProvider.refresh (service-call)"]
n9["claudeProvider.refresh (service-call)"]
n10["return"]
term_11(["return"])
n12["syncProviders (side-effect)"]
%% Edges
switch_3 -->|'codex'| n4
switch_3 -->|'claudeAgent'| n5
n6 --> parallel_fork_7
parallel_fork_7 -->|codexProvider.refresh| n8
n8 --> parallel_join_7
parallel_fork_7 -->|claudeProvider.refresh| n9
n9 --> parallel_join_7
switch_3 -->|default| n6
n10 --> n12
n12 --> term_11
n4 --> n10
n5 --> n10
parallel_join_7 --> n10
start --> switch_3
switch_3 --> end_node
%% Styles
classDef startStyle fill:#c8e6c9,stroke:#2e7d32
classDef endStyle fill:#ffcdd2,stroke:#c62828
classDef effectStyle fill:#90EE90,stroke:#333,stroke-width:2px
classDef parallelStyle fill:#FFA500,stroke:#333,stroke-width:2px
classDef switchStyle fill:#FFD700,stroke:#333,stroke-width:2px
classDef terminalStyle fill:#FF6B6B,stroke:#333,stroke-width:2px
class start startStyle
class end_node endStyle
class n2 switchStyle
class switch_3 switchStyle
class n4 effectStyle
class n5 effectStyle
class n6 parallelStyle
class parallel_fork_7 parallelStyle
class parallel_join_7 parallelStyle
class n8 effectStyle
class n9 effectStyle
class n10 terminalStyle
class term_11 terminalStyle
class n12 effectStyle
Coupling surfaces the real hubs
Section titled “Coupling surfaces the real hubs”Running --coupling against the server source reports per-file fan-in and fan-out across all 319 files. On t3code it surfaces exactly the modules an experienced reviewer would point at first:
effect-analyze ./apps/server/src --coupling# Module Coupling Analysis
## Summary- Analyzed files: 319- Critical fan-in files: 2- High fan-in files: 7- High fan-out files: 6- Unannotated hubs: 9
## Issues🔴 [critical-fanin] `config.ts` — fan-in 80🔴 [critical-fanin] `persistence/Errors.ts` — fan-in 37🟡 [high-fanin] `vcs/VcsProcess.ts` — fan-in 23🟡 [high-fanin] `persistence/Layers/Sqlite.ts` — fan-in 21🟡 [high-fanin] `provider/Errors.ts` — fan-in 19🟢 [high-fanout] `server.ts` — imports 63 internal modules🟢 [high-fanout] `ws.ts` — imports 41 internal modulesAnnotate the intentional hubs in-source and they stop generating warnings while still being monitored:
/** * Central config types — shared by every server module. * * @known-hub central config registry */The annotation lives next to the code, is grep-able, carries a written reason, and shows up explicitly in the report as a “known hub” so future growth past expected size is still flagged.
A complete project health snapshot
Section titled “A complete project health snapshot”The four health analyzers can run together with --agent-report to produce a single prioritized backlog:
effect-analyze ./apps/server/src \ --agent-report \ --error-channel \ --service-health \ --performance \ --coupling \ -o backlog.mdOn t3code’s server (319 files, 2,401 programs, 0.0% unknown node rate) this produces 156 prioritized improvements spanning lint, error channels, service health, performance anti-patterns, and module coupling — each with file:line citations and a concrete suggestion. The output is structured so a coding agent can work through it linearly, P1 → P2 → P3, opening PRs as it goes.
All four analyzers also support --format json for scripting, with --output <path> to write to a file.
Not every finding is a bug
Section titled “Not every finding is a bug”The biggest caveat in reading this output: not every finding should become a refactor. In an Effect-heavy codebase, high fan-in files, central config modules, and large layer composition files can be legitimate. The value of the report is in separating four categories:
- Intentional hubs worth documenting with
@known-hub—config.tsat fan-in 80 is the shared type surface of the entire server. The right action is annotation, not a split. Likewisepersistence/Errors.tsis the shared tagged-error union, used (correctly) wherever persistence errors are caught. - Accidental god modules worth splitting —
server.tsimporting 63 internal modules is more suspect. It’s the top-level wiring file, so some breadth is unavoidable, but if it’s growing every sprint it deserves a look.ws.tswith fan-out 41 is in the same category. - Real error-channel or service-health issues worth fixing — the 15 error-channel and 54 service-health findings in the report are the ones worth acting on directly.
live-layer-in-test(8 occurrences) is a clear “fix this” signal. - Analyzer noise worth ignoring — a small number of findings will be artefacts of the analyzer’s current depth on this codebase. Use these to improve the analyzer, not the codebase. See “What still needs more work” below.
What the analyzer is good at on t3code
Section titled “What the analyzer is good at on t3code”- recovering large-scale layer composition across the backend
- identifying repository/service live layers and their references
- explaining callback bridges like
Effect.callback(...) - recognizing service-backed property effects such as
provider.refresh - recognizing background stream reactors such as
runForEach(...).pipe(Effect.forkScoped) - producing repo-level coverage metrics that are low-noise enough to guide further work
- surfacing god-modules and broad-scope files through fan-in/fan-out coupling metrics
What still needs more work
Section titled “What still needs more work”The remaining misses are visible in the coverage report:
Could not determine effect typeis still the top unknown reason- some higher-order callback bodies still compress to generic loop or callback summaries
- many zero-program files are legitimate service contracts, schema files, or test helpers rather than missed programs
That means the next wins are mostly semantic depth, not broad discovery:
- better callback-body inference
- richer loop-body summaries
- better handling of service-contract and schema-only files in coverage reporting
Recommended workflow for t3code
Section titled “Recommended workflow for t3code”For this repository, the best workflow is:
- run
--coverage-audit --show-by-folderon the server source for a top-level discovery picture - run
--couplingto find the hubs and broad-scope files; annotate intentional hubs with@known-hub - run
--format architectureon the server source or on layer-heavy directories - for a single prioritized backlog, combine
--agent-report --error-channel --service-health --performance --coupling - drill into individual files like
bootstrap.tsorProviderRegistry.tswith--format explain
That matches the current analyzer well. t3code is not primarily a “show me one pretty flowchart” codebase. It is a “help me understand the architecture, then zoom in” codebase.