This audit records structural risks found while documenting the current architecture. It is ordered by impact, not by ease of implementation. The project is an early work in progress, so the recommendations favor a durable contract and clear ownership over small tactical patches.
Snapshot: 2026-07-13.
bun run typecheckpasses.bun testpasses 136 tests across 37 files.- The existing tests provide good coverage for parsing, normalization, stores, navigation helpers, query persistence, and visual helper behavior.
- There are no HTTP route contract tests or full backend-to-client integration tests.
src/backend/http/app.ts enables all origins, headers, and methods. The API
has no session authentication or request capability token. At the same time,
its endpoints can create terminals, write terminal input, run trusted local
scripts, operate on Git repositories, control Docker Compose, read environment
files, open local files, and post GitHub reviews.
Binding the normal backend to loopback reduces network exposure, but it does not establish a browser-origin trust boundary. The CLI also accepts a custom host. Electron adds more permissive access-control headers in its web request hooks instead of narrowing access.
Recommended direction:
- Generate an unguessable capability token for each backend process and pass it to the renderer through the CLI URL or Electron preload bridge.
- Require the token on every privileged request and SSE connection.
- Replace wildcard CORS with explicit local UI origins.
- Refuse non-loopback binding unless the user explicitly opts into a secured remote mode.
- Add integration tests proving that untrusted origins and missing tokens are rejected.
src/backend/routes.ts manually casts request bodies and query values. This
provides TypeScript autocomplete inside the route but no runtime guarantee.
Numbers can become NaN, strings have inconsistent empty-value checks, enum
actions are trusted after a cast, and the renderer/backend contract can drift
without a compilation error.
Recommended direction:
- Split routes by domain and decode every request with a shared Effect Schema.
- Define request, response, and tagged error contracts in the dedicated contracts package.
- Use those schemas from the Elysia and Ky adapters instead of manually reproducing endpoint payloads and response types.
- Add route-level tests for invalid bodies, invalid query values, and error envelopes.
The renderer uses prRunApi for HTTP terminal sessions and EventSource for
events. Electron still registers a complete second terminal stack:
electron/terminal-session-manager.ts- terminal IPC handlers in
electron/main.ts - terminal methods and subscriptions in
electron/preload.ts - duplicated terminal types and busy-state helpers under
electron/
No renderer call uses these IPC terminal methods. Maintaining both paths duplicates process discovery, session lifecycle, buffering, input, resize, and test effort. Fixes can land in one implementation while Electron appears to remain covered by tests for the other.
Recommended direction: make the HTTP backend the only terminal owner and
remove the unused Electron terminal API after an Electron smoke test confirms
the HTTP path. Keep only getBackendUrl, platform information, and window
chrome operations in preload.
The same project, branch, Git, script, and terminal models are copied among
src/backend/types.ts, src/types/pr-run.ts, and electron/types.ts.
src/backend/types.ts and src/types/pr-run.ts are hundreds of lines each,
and they are already not identical. The Electron copy represents an older,
smaller API shape.
Recommended direction:
- Move transport-safe domain contracts into the Effect Schema contracts package that every TypeScript target can include.
- Replace the generic backend
ApiErrorwith declared tagged errors at the domain boundary and a sanitized internal-error transport fallback. - Derive request and response types from schemas rather than maintaining parallel interfaces.
- Delete the Electron copy with the legacy IPC terminal path.
The active screen is represented by URL history, workspaceView,
selectedBranch, isOverviewOpen, overviewProjectId, and the active ID in
useWorkspaceTabsStore. Event handlers update several of these values in
sequence. This permits transient or persistent impossible combinations, such
as settings plus stale overview state or a selected branch that disagrees with
the active tab.
Recommended direction: represent the current route as one schema-backed discriminated union and derive the selected project, selected branch, overview visibility, and settings visibility from it. Let the URL be the persistent serialization of that state. Keep the tab collection in a focused persistent Zustand domain and navigate through one semantic action.
The original useWorktreeTerminalStore exceeded 600 lines and called
prRunApi directly for session creation, state reads, input, and disposal. It
mixed transport, server state, shared client state, and resource lifetime in
one global store.
useSshPassphraseStore also stores executable retry callbacks. Those callbacks
couple unrelated features to a global store through hidden side effects and
make the store difficult to inspect or replay.
Implemented:
- Terminal session snapshots and lifecycle mutations now use TanStack Query.
- SSE decoding, interruption, and finalization now use an Effect Stream beneath a focused feature hook.
- Terminal tab metadata and panel layout remain in a focused Zustand store while leaving Xterm's imperative DOM instance in a React adapter.
- Model SSH retry as explicit pending operation data or let the initiating command own its retry after credential submission; never store executable callbacks globally.
- The store no longer imports
prRunApi; the remaining SSH retry callback design is tracked separately in this audit.
usePrRunAppState combines six remote operations, two navigation models,
preferences, terminal cleanup, SSH retry callbacks, dialogs, status summaries,
theme synchronization, and toast/error policy. PrRunApp then adds terminal
panel orchestration, responsive layout, persisted dimensions, tab hotkeys, and
several synchronization effects. The two files contain 814 lines together and
touch nearly every frontend state owner.
This flattened controller makes child props broad, gives unrelated changes one churn hotspot, and encourages effects to synchronize values that should have a single owner. It is also why frontend features cannot be migrated or tested as isolated domains.
Recommended direction:
- Make
PrRunAppthe frame and route outlet only. - Let each feature root read its focused TanStack hooks and narrow Zustand selectors, then pass narrow values to visual children.
- Move theme and media-query synchronization into focused browser adapters.
- Model terminal panel, workspace tabs, preferences, dialogs, and navigation as separate Zustand domains or URL state rather than returning one application-wide state object.
- Split the existing hotspots by responsibility while their domains migrate; do not replace the current controller with another global controller.
Several files combine routing, contracts, orchestration, protocol parsing, or domain logic:
| File | Approximate lines | Mixed responsibilities |
|---|---|---|
src/backend/routes.ts |
727 | Every backend endpoint and validation branch |
src/lib/hooks/store/use-worktree-terminal-store.ts |
419 | Terminal UI/domain state and immutable updates |
src/backend/handlers/git/github.ts |
590 | GitHub command transport and normalization |
src/backend/handlers/scripts/index.ts |
587 | CRUD, inspection, execution, streaming, package scripts |
src/backend/handlers/docker/index.ts |
586 | Discovery, inspection, command execution, normalization |
src/backend/handlers/terminal/pty-worker.cjs |
557 | PTY protocol, lifecycle, buffering, process inspection |
The issue is responsibility density rather than a line-count rule alone. Changes to unrelated endpoints or protocols converge on the same files and make ownership and testing harder.
Recommended direction:
- Split HTTP routes and client methods into matching domain modules.
- Keep each folder entry point as the public domain API, with implementations one level deeper.
- Separate process transport, normalization/parsing, and orchestration in the GitHub, Docker, scripts, and terminal domains.
- Keep pure helpers close to focused unit tests.
The renderer client is now split into src/lib/api/transport.ts plus focused
project, Git, review, environment, script, and terminal modules. The
Electron-only terminal implementation was removed; Electron and the browser
now use the same backend terminal protocol. Shared project and transport types
have one source instead of mirrored frontend and backend declarations.
The frontend and backend each contain an identical tryPromise
implementation. Both accept error: any, erase the operation's error type,
and log every handled rejection as a warning. Expected probes, such as checking
for package-manager lockfiles or trying alternate JSON formats, therefore
produce warning noise in a successful test run.
Other files still use direct try/catch, .catch(), and void-prefixed
async calls, which conflicts with the repository's documented error-handling
convention. Some swallowed errors are intentional best-effort behavior, but
the current helper cannot express that distinction.
Recommended direction:
- Use Effect Schema tagged errors for expected failures and preserve defects
and interruption in
Cause. - Wrap promise and callback APIs once inside platform service layers rather than at every call site.
- Log only at an observable feature or transport boundary that can add safe operation, project, duration, and outcome context.
- Delete both
tryPromiseimplementations after the last vertical slice has moved. Until then, keep the existing repository convention in unmigrated modules instead of creating a third error abstraction.
src/backend/handlers/env.ts parses only PR_RUN_USER_DATA_DIR, but no module
imports it. Backend code reads many environment variables directly, including
ports, editor selection, SSH commands, GitHub behavior, log level, and script
runner configuration. The schema therefore provides no validation and can
mislead maintainers into believing configuration is centralized.
Recommended direction: create one AppConfig Effect service that decodes and
documents every supported variable, supplies redacted secrets, and is provided
to every executable through a live layer. Remove direct environment reads as
their domains migrate.
Project registration previously performed an asynchronous read-modify-write of
projects.json without serialization or atomic replacement. Concurrent
add-project requests could overwrite each other, and interrupted writes could
leave a partial file.
ProjectRepository now serializes mutations through an Effect semaphore and
writes through a unique temporary file followed by atomic rename. A regression
test performs concurrent additions and proves both projects survive. Runtime
schema decoding of the complete persisted shape remains part of the contracts
migration.
The suite tests many pure parsers and state helpers, but it does not create the
Elysia app and exercise requests through fetch. As a result, route paths,
body parsing, status codes, response envelope shape, CORS/auth policy, and the
manual prRunApi path strings can regress independently of existing tests.
Recommended direction: add a small contract suite per route domain using an injected handler facade. Add one browser-client compatibility test for each response shape and streaming protocol.
src/backend/handlers/project-config.ts is an export-only wrapper around
same-level code, and several implementation files export many helpers in
addition to their domain facade. The ui/ and atoms/ distinction is also not
documented by a code boundary, so generic primitives can drift between both
locations.
Recommended direction: define public folder APIs only when the implementation
lives one level deeper, keep non-public helpers private unless tests require a
deliberate seam, and document whether ui/ is vendor-owned while atoms/ is
application-owned.
- Establish the Effect runtime, contracts package, Ky service, and characterization tests while retaining the TanStack Query provider and focused Zustand stores.
- Secure and schema-validate the backend boundary, then migrate configuration to an Effect service with atomic writes.
- Move read-only domains end to end: schema, service, Elysia endpoint, Effect client operation, TanStack hook, component, and tests.
- Move commands beneath TanStack mutations and consolidate navigation into one discriminated route value.
- Add ordered replayable streams, then migrate script and terminal lifecycles.
- Remove the unused Electron terminal stack and duplicate Electron types.
- Move all entry-point lifecycles to Effect and delete the compatibility stack only after repository-wide checks find no consumers.
The complete target and phase exit conditions are in effect-migration.md.
Fallow 2.89.0 analyzed the formatted tree with schema version 7 after the API, terminal, contract, and React state refactors. The health score is 71.2 (grade B), up from 66.6 before the migration.
| Signal | Result | Interpretation |
|---|---|---|
| Files analyzed for health | 271 | Application, Electron, backend, tests, and tooling |
| Functions analyzed | 2,039 | 110 crossed a complexity or static CRAP threshold |
| Average maintainability | 90.9 | Generally healthy local functions despite large orchestration units |
| Cleanup findings | 35 | Includes runtime entry points that static discovery cannot prove |
| Clone groups | 15 | Reduced from 39 in the initial baseline |
| Duplication | 1.41% | 431 duplicated lines, reduced from 2,198 |
| Circular dependencies | 0 | No import cycles detected |
| Unresolved imports | 0 | Static module resolution is clean |
| Boundary violations | 0 | No configured boundary policy exists, so this is not proof of layering |
The original largest clone groups are resolved: backend/frontend contracts now
share one source and the duplicate Electron terminal manager no longer exists.
Remaining clones are mostly local validation branches in routes.ts, the
temporary duplicate tryPromise compatibility helper, and process inspection
shared conceptually between the typed terminal module and its isolated CJS PTY
worker.
The app-state hotspot fell from 76.6 to 66.2 after extracting theme,
sidebar-layout, and project-action hooks. The next frontend hotspots are
sidebar-project-item.tsx at 62.5, main-panel/index.tsx at 61.7, and
pr-run-app/index.tsx at 59.7. The monolithic API client is no longer a hotspot.
The highest function-level complexity readings were:
main-panel/index.tsx: cyclomatic 43, cognitive 26, 354 lines.sidebar-project-item.tsx: cyclomatic 41, cognitive 37, 365 lines.query-cache-persistence.ts: cyclomatic 36, cognitive 17, 71 lines.branch-page-header.tsx: cyclomatic 24, cognitive 23, 286 lines.main-panel/run/index.tsx: cyclomatic 22, cognitive 23, 342 lines.
The CRAP values in this run use Fallow's static estimated coverage model, not a
real coverage report. They should rank review targets, not be treated as exact
test coverage measurements. query-cache-persistence.ts, for example, has
direct tests even though its long key-classification predicate scores as
complex.
Some dead-code results are high-confidence cleanup candidates, including the
unused env.ts schema and several unused UI primitives. Others are expected
static-analysis blind spots:
- Electron loads
preload.tsthrough a runtime file path. bin/pr-run.cjsspawnssrc/cli/pr-run.tsthrough a computed path.- The scripts handler spawns
scripts/runner.tsdynamically. - User-authored scripts import
registerScriptthrough a generated runtime file. - Semantic Release resolves the
conventionalcommitspreset indirectly. - The nested
electron/package.jsoncan make root-ownednode-ptylook unlisted for the Electron subtree.
Do not apply Fallow's automatic cleanup across this backlog. Trace each dynamic
entry point or dependency, then either configure the runtime edge or remove
only findings confirmed unused. effect is now used by the backend runtime,
project repository, Ky service, and terminal stream; node-gyp still needs
runtime verification before removal.