Architecture
Four layers, and one rule that shapes all of them: the domain core has no Tauri dependency, so the same operations layer serves the desktop app and the MCP hub.
One agent call, drawn
Section titled “One agent call, drawn”
The drawing follows one hub call from the agent’s terminal down to disk. It leaves two things out on purpose: the app’s own write path — the UI reaches the same core through Tauri IPC, not through the hub — and cross-workspace delivery, which is covered in Teams.
Two details in it have since gone stale, and the text below is the accurate version: there are now two cross-process locks rather than one, and the live-update arrow belongs on ops writing tasks/ rather than on sync — the surfaces sync rewrites are deliberately not watched.
The layers
Section titled “The layers”The UI (React 19, TypeScript, i18n)
Section titled “The UI (React 19, TypeScript, i18n)”App-level views: All projects, Teams, Agent sessions, Templates, Settings.
Workspace-level views: Dashboard, Tasks, Collaboration, Inbox, Specs, Ledger, Search, Tools, Terminal, Project settings.
The folders under src/ are a layering rule rather than a filing preference, and a test enforces the machine-checkable part of it. CONTRIBUTING.md in the repository root has the table.
It talks to the layer below over Tauri IPC, which carries lightweight DTOs and state diffs only. Four event families come back the other way:
workspace://changed: reload state from diskledger://appended: read the new tail onlyterminal://output,terminal://exit,terminal://sessionsindex://progress: how far a full-text index rebuild has got, streamed while it runs
The delivery layer (src-tauri)
Section titled “The delivery layer (src-tauri)”Thin on purpose.
commands.rs: the IPC surface. Heavy I/O always goes throughspawn_blocking, so it never blocks the UI thread.watcher.rs: the file watcher behindworkspace://changed.terminal.rs: the embedded terminal, a real PTY process per agent CLI (portable-pty; ConPTY on Windows). Sessions belong to the app layer, so switching workspaces does not interrupt them.mcp_deploy.rs: resolves the bundled hub binary and writes the root.mcp.jsonpointer — also the “Repair connection” path.git.rs: optional git init for fresh workspaces — the wizard’s checkbox, shown only when git is available; best-effort, never fails creation.
The domain core (crates/nextup-core)
Section titled “The domain core (crates/nextup-core)”Zero Tauri dependencies, hexagonal.
agent.rs: hub tool authorisation, by tier and by the per-tool allowlist.security/: crypto (AES-GCM), secrets (an encrypted local key-value store), and the keystore that holds its master key in the OS credential store.workspace/: layout; atomic writes (temp + rename); the cross-process lock; tasks (files are the truth); the ledger (append-only JSONL); workflow (the harness state machine and its gates); templates; handoff (the checkpoint serialiser); sync (after every mutation, brings the takeover surfaces back up to date from one snapshot); bootstrap (writes the handoff layer); manifest; specs (folded on archive); modules; settings; doctor; assets (curriculum upgrade); registry; backup (portable zip); init; ops (the consistency layer); flywheel (post-mortem lessons under an evidence rule); ids (sequentialT-0001families, plus the UUIDs used for anything crossing workspaces); context; rules; teams; exchange; prime (the coordinator’s read side — a cheap summary for every member, and the by-name detail read that writes itself into that member’s ledger);agent_catalog.index/: scanning, language-agnostic chunking, an FTS5 full-text index, and the adoption wizard’s read-only analysis that drafts tasks from an existing folder. The index is a derivative, so it can always be rebuilt.state.rs: thread-safe shared state.process.rs: the one way to spawn a child process — a GUI build has no console of its own, so a bare command would flash a black window on Windows.orchestrator/: dormant. AnLLMProviderport with adapters, a model registry and a cost router. Nothing in the app or the hub reaches it; groundwork only.mcp/: archived. An rmcp client, kept as groundwork for a future tool gateway.
The hub MCP server (crates/nextup-mcp)
Section titled “The hub MCP server (crates/nextup-mcp)”A stdio binary, spawned by the agent host through the workspace’s .mcp.json (the Claude CLI does this automatically; other hosts differ).
Its tool surface is one shape: authorise → core ops → ledger. Denied calls are recorded too.
Most tools stay inside the workspace that spawned the server. The coordinator tools are the exception: they reach the app-level team registry under ~/.nextup/, and the detail reads reach into another workspace entirely. Being named as that team’s coordinator is the first gate and, for the per-member summaries, the only one — putting a project in charge is itself the human act that grants them. Everything past the summary is guarded a second time by the per-tool authorisation, and a detail read appends to the read member’s own ledger, so reaching into one is never silent. See Teams.
Concurrency and live updates
Section titled “Concurrency and live updates”The app and the hub reach the same workspace files. An agent and the app can write concurrently and safely because every mutation to a given scope takes that scope’s cross-process lock, held for the duration of one mutation; read paths stay lock-free.
There are two such locks, one per scope. The workspace lock covers one project’s files; a second lock covers the app-level records under ~/.nextup/ — the project registry and the team graph — which the app and a coordinator’s hub both read-modify-write. They are never taken one inside the other: nesting them in opposite orders is a deadlock, so the app-level lock is taken inside each app-level write rather than wrapped around a workspace mutation.
Seeing an agent’s change appear in your window is a separate mechanism: the file watcher emits workspace://changed, debounced by 400 ms, and the UI reloads from disk. It watches the files that are the truth — tasks/, artifacts/, specs/ and the .nextup/ config files. The takeover surfaces that sync rewrites afterwards (AGENTS.md, project.yaml, the snapshot) are deliberately not watched: they are derived from what was just written, so signalling on them would close a loop rather than report a change.
See also Disk contract for what all of this writes to disk.