Skip to content

Latest commit

 

History

History
179 lines (99 loc) · 60.5 KB

File metadata and controls

179 lines (99 loc) · 60.5 KB

Architecture

This document records the current product and code naming used by Alera. It is intentionally short and should stay aligned with implemented behavior.

Naming Glossary

  • Project: a local project path registered in Alera. It can be an existing local folder or a Git repository cloned from a URL; only Git-backed projects support linked workspaces.
  • Workspace: a working context inside a project. A workspace points at one filesystem path and may point at one Git branch. The primary workspace points at the registered project path; linked workspaces point at Git worktrees and exist only for Git-backed projects.
  • Worktree: the Git mechanism used to create an isolated checkout for a linked workspace. Alera can create a new local branch from a source branch or attach a workspace to an existing local branch. Use this term only for Git/filesystem behavior, not for the Alera UI container. Folder-only projects do not use worktrees.
  • Workbench: the main interactive area for an active workspace. It owns pane layout, active group selection, and workspace tabs. A project may be active while no workspace is selected; in that state the shell shows an empty workspace surface and workspace-scoped panels should render without workspace context.
  • WorkbenchPaneGroup: one pane in the split layout. It contains an ordered list of workspace tab ids and one active tab id.
  • WorkspaceTab: the domain concept for a tab inside a workspace. It is intentionally broader than terminal because tabs can represent terminals, editors, markdown previews, or other workspace surfaces. Explorer, Source Control, search, and quick-open file opens use a preview tab (payload preview: true) in the active pane; a later preview open replaces that slot; double-clicking the file, double-clicking the tab, Keep Open, or editing pins it. Markdown Edit, Mermaid preview, pull request diffs, and Open All Changes stay permanent.
  • TerminalSession: the runtime process/PTY state attached to a terminal workspace tab. Terminal sessions are owned by the durable runtime host and are reattached by terminalSessionId; they are not persisted tab records.
  • WorkspaceRelation: a runtime-owned parent/child relationship between two workspaces. Relations are global and may cross projects or hosts. A relation stores both workspace ids and workspace instance ids so stale reused ids can be detected by clients and future migrations.
  • WorkspaceTag: a runtime-owned global label that can be assigned to multiple workspaces independently from parent/child relations.
  • Sidebar Agent Section: the in-card sidebar projection for supported agent executions detected inside terminal workspace tabs. The sidebar does not render terminal tabs that have no current agent status.
  • AgentRun: the in-memory UI projection of a supported agent status matched to a terminal workspace tab. It is not a persisted storage model.
  • Agent task dispatch: the shared desktop picker that sends a caller-owned prompt to a running workspace agent or opens a profile tab and injects. File and diff comments, pull request failed-check repair, Restack, and agent watch reuse chooseAgentTaskDispatchTarget / completeAgentTaskDispatch. They do not fork a one-off launcher. Copy Prompt copies the complete prompt, with the same outer whitespace trimming used for dispatch, even when no agents or profiles are available. Copying keeps the picker open and preserves comment drafts; it does not select, launch, or send to an agent. Pickers without a prompt hide the copy action.
  • Workspace agent comment: a desktop-only in-memory draft annotation on a file or git diff. Comments accumulate per workspace and send together through agent task dispatch. They are not hosted review comments, are not published to GitHub, and are out of scope on mobile.
  • Design system: the shared, presentational widget library in lib/src/design_system/, prefixed Alera, with co-located widget previews. See docs/ui-styleguide.md.
  • Background setup job: session UI for a New Workspace or clone flow that must outlive its form. Clone jobs are runtime-owned (projectCloneJobs); workspace-create pipelines are client-session state. Progress shows on AleraJobCard, not a modal barrier.

Naming Rules

  • Use WorkspaceTabRecord, WorkspaceTabService, and WorkspaceTabKind for persisted tabs.
  • Use TerminalSessionHandle, TerminalRuntime, and TerminalSurface only for terminal execution/rendering.
  • Keep Workbench for layout and interaction shell concepts, not for persisted tab records.
  • Avoid introducing TerminalTab as a domain model. A terminal tab is currently a WorkspaceTabRecord with kind == WorkspaceTabKind.terminal.
  • Avoid using Workspace and Worktree interchangeably. A workspace is product state; a worktree is a Git checkout implementation detail.

Persistence

Alera persists runtime-owned settings, projects, project config overrides, workspaces, workspace tabs, workbench layouts, workspace tags, workspace relations, SSH targets, and mobile companion access records in the Rust runtime database runtime.sqlite under the active runtime profile directory. The Rust store lives in rust/alera-core and is reached by the Flutter app through the alera runtime-host JSON socket protocol.

Mobile companion access is opt-in runtime state. The runtime surface persists mobile gateway settings, short-lived pairing offers, paired/revoked device records, and durable project clone jobs; the desktop Settings → Mobile Devices pane, the alera mobile ... CLI, and runtime-host RPCs expose that state. When enabled, the terminal-host sidecar starts an external WebSocket listener, accepts mobile.device.pair for valid pairing offers, authenticates mobile clients with mobile.hello device tokens, and applies a mobile request allowlist before exposing workspace sidebar snapshots, high-level project lifecycle operations, remote directory browsing, managed-workspace actions, tab rename, terminal streaming, quotas, host-tool installation, a dedicated portable-settings patch surface, and additive Explorer, Search, Source Control, and Pull Request panels. Source Control on mobile writes through host verbs under mobileSourceControlWritesV1 (stage, commit, fetch, pull, push, stash, and branch changes), backed by the same alera_core::source_control code the desktop bridge calls; a host without that capability leaves the phone read-only. Pull Request v1 shows current-branch identity, title, state, checks, and a read-only conversation through gh (conversation comments, review summaries, and diff review threads with their resolved state, as additive comment fields). With mobilePullRequestActionsV1 a phone can also comment, reply in review threads, edit its own comments, merge, change draft status, close, link, unlink, and create GitHub pull requests through mobile.pullRequest.* verbs that run the desktop's gh commands on the runtime, generate a pull request title and description with AI Assist (aiTextPullRequestDetailsV1), and Ship local changes in one runtime request (mobilePullRequestShipV1); GitLab and Azure DevOps stay on desktop. With mobilePullRequestSummariesV1 the workspace list rows also show the compact per-workspace pull-request status the desktop sidebar shows, read through mobile.pullRequest.summaries (one gh GraphQL batch per repository, with the check rollup computed on the runtime so the phone keeps only counts and at most three failing check names); the answer also marks which workspace ids were freshly evaluated so the phone merges like the desktop monitor (failed batches keep their last icons, evaluated rows with no review clear) and the capability is additive and MUST NOT bump aleraMobileProtocolVersion. Markdown files render as a view-only preview on mobile through the same file read, with only http(s) links and remote images allowed, matching the desktop Markdown viewer. Search can also preview and apply replacements through mobileWorkspaceReplaceV1, served by the same alera_core::workspace_search engine the desktop uses. Explorer adds the desktop's non-mutating actions (hide ignored, collapse all, refresh, copy paths, Comment on File dispatched to a running agent or a new profile tab, and a nested Source Control root through the additive relativeRoot field on mobile.git.status / mobile.git.diff); file mutation stays on desktop. These panels are feature-detected through mobileExplorerV1, mobileWorkspaceSearchV1, mobileWorkspaceReplaceV1, mobileSourceControlV1, mobileSourceControlRootV1, and mobilePullRequestV1 and MUST NOT bump aleraMobileProtocolVersion. They do not reopen terminal layout (#638) or fake-resize refresh (#640). Host-admin RPCs (mobile.settings.update, mobile.pairing.cancel, mobile.device.rename, mobile.device.revoke, mobile.device.delete) and generic runtime mutations remain excluded, so a paired phone cannot manage other devices, offers, or arbitrary runtime records. Mobile terminal input negotiates terminalDeferredInputV1 so a composed prompt and its Enter reach the PTY as separate writes, and the app answers the host's backpressure resync so a client the host paused recovers without re-attaching. The mobile transport builds on runtime-owned records rather than on the desktop Flutter process.

Workbench view preferences shared by desktop and mobile are stored in runtime metadata under the versioned workbench.sharedViewPrefs.v1 record. The first desktop load migrates the common fields from Drift and marks the runtime record initialized. Desktop writes are authoritative for revision conflicts, while mobile writes must include the current revision and reload after a conflict. Desktop also shares workspaceMainTabIds (the terminal tabs in each workspace's main panel) so a phone can merge a single main-panel agent onto the workspace row the way the desktop sidebar does; a phone omits that key and the host backfills it. Mobile search text remains transient and device-local. Workspace activity timestamps use runtime metadata as the shared source, with desktop Drift activity retained only as a migration and compatibility source; merge operations keep the newest timestamp per workspace.

Drift/SQLite remains active for local view preferences and legacy migration sources in lib/src/shared/infra/storage/drift_database.dart. Old Drift settings, project config, project, workspace, tab, and layout rows are intentionally left untouched after migration but are no longer authoritative for those domains.

Project-specific worktree setup config is stored separately from global app settings. UI overrides live in runtime project config records and take precedence over a repo-root alera.toml. A repo-root .worktreeinclude still copies matching gitignored files into new linked workspaces; see docs/worktree-setup-config.md.

Project registration, rename, metadata-only removal, effective setup config, and Git cloning are runtime-owned. Desktop and Mobile call the same high-level requests; clients never construct project IDs or main workspace records. Clone jobs persist progress and results in runtime.sqlite, use the host's system Git credential helpers, and remove only their own newly-created partial destination after cancellation, failure, or runtime restart. Project removal terminates affected sessions and removes runtime metadata but never deletes repository or worktree files.

Terminal workspace tabs store their durable terminalSessionId in the tab payload. The runtime host stores socket metadata, terminal checkpoint metadata, and bounded output chunks under the active runtime profile directory, outside Drift, so app/window close can detach from PTYs without killing running commands.

Sleeping a workspace is a confirmed, runtime-owned operation. It terminates every terminal session owned by that workspace and keeps its tab records, persisted workbench layout, workspace record, branch, files, tags, and relations, so agent sessions resume through their stored native session ids when the workspace wakes. The runtime records which terminal tabs the sleep stopped (workspace.sleptTabs, announced with workspaceSleepChanged, additive under workspaceSleepStateV1), drops them from the sidebar snapshot's terminal counts and agent presence, and clears the record as soon as a session starts again for one of them. Desktop and mobile therefore show a slept workspace like one whose terminals were closed until it is opened again, while a terminal created after the sleep, such as an agent spawned from the CLI, still shows as running. The desktop client that initiates Sleep returns to its project workspace list, while mobile remains on its workspace list.

Terminal tabs with spawnOnCreate are owned entirely by the runtime host. The host starts their PTY without requiring an attached client, restores pending tabs when the host restarts, and delivers initialCommand exactly once per new process. Agent spawning and coordinator worker creation therefore continue on standalone and VPS runtimes with no desktop Flutter process. A normal PTY exit or explicit terminal termination removes the tab and its retained history; stopping the host preserves tabs and checkpoints so they can be reminted later.

Legacy pre-Drift stores are no longer read or migrated.

CLI Sidecar

On Windows, the desktop runner acquires a session-local mutex before starting Flutter. A second launch of the same flavor signals an activation event and exits without creating another engine or tray icon. The existing message loop shows and focuses its window, restoring it if minimized and preserving maximization when hidden. The event retains requests made before the window exists. The mutex and event use the flavor's application id, so Alera and Alera Dev can run together; ownership is released on exit and recovered after a crash. This desktop guard does not apply to the runtime sidecar.

Alera ships a separate Rust CLI named alera for non-UI background work. The desktop app launches alera runtime-host as a detached sidecar process instead of relaunching the Flutter app executable, so closing the app window detaches from terminal PTYs without creating another dock/taskbar app instance. The same sidecar can run independently on a workstation or VPS through alera runtime start, status, and stop; persistent CLI-started hosts do not apply idle shutdown. alera terminal-host remains a compatibility alias for existing debug scripts and older launchers.

The CLI is the Rust crate under rust/ (rust/alera-cli, binary alera) and speaks the runtime socket protocol, host.json control file, runtime SQLite schema, and terminal checkpoint schema. Each connected client has an independent control lane for RPC responses and runtime events plus a bounded terminal-output lane; terminal backpressure pauses and resynchronizes only terminal output instead of severing the runtime connection. Release and desktop builds compile the sidecar with cargo build --locked from the native build hooks (linux/CMakeLists.txt, windows/CMakeLists.txt, and the macOS "Build Alera CLI Sidecar" Xcode phase); a Release app build produces --release, other configs build debug. The single binary is installed into Contents/Resources/alera/alera on macOS and resources/alera/alera[.exe] next to the app executable on Linux and Windows.

The release workflow also publishes standalone runtime sidecar archives named alera-runtime-<version>-<platform>-<arch>.tar.gz with .sha256 files. SSH bootstrap uses those artifacts to install a remote runtime without depending on the desktop app bundle.

The app resolves the sidecar through AleraCliResolver, which honors ALERA_CLI_PATH first and then searches the bundled resources/alera/ locations. When no compiled sidecar is present (pure source checkout), development builds fall back to cargo run -p alera-cli to build and run the Rust host from source.

The shell eagerly connects to or starts the runtime host before the first terminal tab is created and before RPC-backed repository streams need data. The app passes the current host lifecycle and host scrollback settings when launching alera runtime-host, then sends a configure request whenever terminal settings change so an already-running host updates without requiring an app restart. Host configuration preserves persistent lifecycle state selected through the CLI.

The bottom status bar exposes a local Runtime control next to agent quotas. The chip shows whether the host is running, stopped, or has an update available. Opening it reveals host and bundled sidecar versions (rendered as v0.1.0), session and agent counts, and the end-aligned Refresh / Start / Stop / Update Runtime actions. Stop takes destructive styling while sessions or agents are still attached. The lifecycle settings live under Settings → Application → Runtime: Empty Host Shutdown, Detached Session Shutdown, and Keep Runtime Open When App Quits. Update Runtime is enabled when the bundled sidecar is newer than the live host, or when the versions match and the git commits differ (the usual leftover after an app update that left the previous detached host running). It soft-stops the host (with force confirmation when sessions, jobs, or agents are active) and relaunches the bundled binary. Force-stop closes the client socket while the sidecar is still tearing down sessions, so a connection-closed reply is treated as shutdown accepted; the app then waits until the old host is gone before launching the new one. There is no separate GitHub download path for the local desktop sidecar.

Mobile Host Settings exposes Restart Runtime only when the paired host advertises runtimeHostRestartV1. The phone first requests a soft restart and requires a second destructive confirmation before retrying with force when agents, terminal sessions, background jobs, or push subscriptions are active. host.restart replaces the sidecar with a new process using the host's effective lifecycle, scrollback, restore, shell, persistence, logging, and crash-reporting configuration. The existing mobile connection controller then reconnects and reauthenticates against the replacement gateway. Older hosts omit the action because the capability is additive and aleraMobileProtocolVersion remains unchanged.

Desktop-started hosts stay alive only while useful. An active cloud job, mobile gateway, or cloud push subscription counts as useful work, so an opted-in runtime can deliver a notification after every Flutter client disconnects. Otherwise, when all app clients disconnect and no PTYs are running, the host stops after the configured empty-host delay, which defaults to 30 seconds. When app clients disconnect while PTYs are still running, it keeps those detached sessions alive for the configured detached-session delay, which defaults to one hour; if the app does not reconnect in time, the host terminates the PTYs and writes final checkpoints before exiting. Hosts started with alera runtime start are persistent until alera runtime stop; a normal stop refuses active sessions, jobs, or agents and --force overrides that protection. alera runtime clear removes a stopped profile, while clear --force first requests a graceful forced shutdown and falls back to terminating the exact owner identity when control metadata is missing. Cleanup holds and preserves runtime-owner.lock so another host cannot acquire a replacement inode during deletion. It removes only the runtime profile, never repositories, project folders, or worktrees. On exit, the host removes its host.json control file so the next client cannot attach to stale socket metadata.

On a clean quit, persistent CLI hosts are never stopped by the app. App-launched sidecars soft-stop by default; Keep Runtime Open When App Quits leaves the sidecar running instead. When soft-stop refuses because the sidecar still has agents, sessions, or background jobs, Alera shows Runtime Still Has Work with Quit And Leave Runtime Open, Force Stop And Quit, and Cancel. Unexpected exits (crash, kill, or any path that skips the quit gate) leave the host up on its empty/detached timers. On Linux, closing the last window still flushes window state and exits the Flutter process directly without window_manager.destroy() because the Linux GTK embedder can remove the implicit Flutter view twice during that close path. macOS and Windows retain the normal prevent-close then destroy flow. Alera remains a non-unique application so separately launched windows continue to use separate Flutter processes.

Interactive terminals start the user's shell as a login shell on macOS, where GUI apps inherit a minimal launchd environment and never read ~/.zprofile, the usual place Homebrew and similar prefixes join PATH. Linux and Windows keep the plain interactive shell their terminal emulators use. Settings → Terminal → Advanced → Use Login Shell overrides the platform default in either direction and is sent to the host as loginShell in the configure payload, so host-owned spawnOnCreate and Mobile terminals follow the same choice. Desktop Refresh Terminal bumps and restores the xterm emulator, then briefly shrinks both PTY axes by about 30% before restoring the measured size, so a stuck agent TUI sees a real resize without moving the visible panel. Auto-refreshing visible tabs on workspace return is not implemented yet. Mobile Refresh uses its own measured viewport pulse. Shell profiles that deliberately skip startup files, such as the clean zsh -f and bash --noprofile --norc fallbacks, are never converted. Independently of that setting, the host resolves the login shell PATH once per process and merges it ahead of its own inherited PATH, so terminals and host-spawned tools such as the Claude and Codex quota probes can resolve user-installed CLIs.

The CLI can operate the same runtime-owned state for agents and automation. The implemented command groups include runtime, project, workspace, tag, tab, ssh-target, agent-profile, and automation. They accept --runtime-dir to target a specific profile directory; when omitted, the CLI uses ALERA_RUNTIME_DIR or ~/.alera/runtime. alera automation lists, shows, creates, edits, approves, pauses, resumes, trashes, restores, purges, runs, and manages templates, tags, import/export, and policy through the authenticated runtime host. Scheduled and manual execution still require the repository [automation] declared = true opt-in described in docs/worktree-setup-config.md. alera agent-profile manages the user-approved launch catalog through the authenticated runtime host, including OCC-safe edits, impact-gated removal, ordering, and launch into an existing workspace. alera workspace start creates a managed worktree and launches a declared profile. alera orchestration delegate creates a task and spawns that profile, with --new-workspace for an isolated child worktree. alera orchestration agent-profiles remains the read-only discovery surface for coordinators. alera runtime agents status|enable|disable manages the nine supported status integrations, with all switches defaulting off. Every runtime terminal launch sets ALERA_RUNTIME_DIR, terminal/workspace/tab identity, and prepends the active sidecar directory to PATH, even when agent hooks are disabled. Agents inside desktop, Mobile, and headless terminals therefore operate the same runtime profile by default. Settings can also register a user-visible alera command under the user's bin directory; that wrapper points at the resolved sidecar and sets the same runtime directory so shells outside Alera can target the app profile when the command directory is on PATH.

Managed linked-workspace lifecycle goes through the runtime host. alera workspace add creates a Git worktree, persists the workspace record, and runs project worktree setup. Pass --host-id to create that worktree on a bootstrapped SSH target; omit it to create locally. Desktop and mobile removal confirm in one dialog. The Pull Request panel defaults to the confirmed Archive Workspace action when the linked review is merged: sessions stop and the workspace hides from the sidebar while tabs, branch, and files are preserved for resume. Remove Workspace stays in the action menu and attempts safe deletion of the owned branch; Keep Branch removes the worktree and retains the branch. Reused and unknown branches omit Keep Branch, and Remove keeps them. Headless removal requires --keep-branch or --delete-branch for an owned branch; legacy ownership metadata alone is not consent. Reused and default branches are kept. Explicit deletion checks live branch identity, checkout ownership, and whether the branch is integrated into the local or origin default, including squash and rebase merges, before stopping sessions or removing files. Unmerged, missing, or unverifiable branches are kept, and workspace removal still proceeds. Remote removal uses the same keep-or-delete safety check before mutation. Deliberately confirmed workspace removal still closes its sessions and discards that worktree's local files.

A workspace can carry one linked issue, identified by its URL and stored in the runtime linkedIssues table, which is dropped with the workspace and moves with Hand Off. Fetching lives in the sidecar (rust/alera-cli/src/issue_tracking/) so the desktop, the phone, and alera issue show share one implementation: GitHub goes through gh issue view, GitLab through glab issue view, and Azure DevOps work items through az boards work-item show, with authentication left to each CLI. The link is stored before any fetch runs, so a missing CLI, a signed-out forge, or a tracker without an adapter (Jira, Linear) still keeps a link that opens in the browser; the cached title and state record the last successful fetch. linkedIssue.list|find|link|refresh|remove, issue.fetch, and issueUrl on workspace.createManaged are gated by the additive linkedIssuesV1 capability, advertised in status.get, the control file, and mobile.hello, and changes broadcast linkedIssuesChanged scoped by workspaceId. The CLI adds --issue to workspace add and workspace start, alera workspace issue show|link|unlink, and alera issue show <url>. The desktop refreshes cached metadata older than 30 minutes while its window is visible.

alera workspace hand-off moves the main worktree's current work into a child workspace. The dialog reuses the current non-default branch by default, permits a new branch, and requires a new branch on the actual default branch. The original checkout returns to the detected default branch; an unavailable or occupied default prevents transfer. AI Assist uses the existing Workspace Identity settings to suggest an editable name from bounded tracked diffs, untracked names and available agent/task metadata. Ignored contents and recognizable credential files are excluded. Regeneration, cancellation and manual fallback are available; late AI or validation responses cannot overwrite edits or submit a changed branch.

alera workspace hand-on brings a child worktree's branch and local changes onto main, then removes the child. Both directions require the additive safeWorkspaceHandoffV1 capability; an older live host is refused without changing strict protocol versions. Persisted tabs retain their IDs, terminal/native agent identities and payloads. The runtime transaction transfers tab paths and layout/selection, remaps colliding pane IDs, integrates existing destination tabs and retains linked review intent. Dart rebinds live handles and dirty editor documents instead of closing them, and checks authoritative ownership before releasing records that disappear from a source workspace. Shell cwd changes and agent notifications remain best effort: moving ownership does not restart the agent or change an already-running process's launch environment. Implicit CLI workspace selection resolves the stable tab/session identity through the runtime store, while explicit workspace flags take precedence.

Transfers refuse conflicted indexes and in-progress Git operations. Hand On also refuses local changes on main, unsaved main editors visible to the initiating desktop, and any ignored child files, including build outputs or local configuration: move those files to a safe location first. Checkout protects ignored files on main from incoming tracked paths. Staging boundaries, including partially staged files, and untracked files move through an operation-owned stash applied by immutable OID with --index. Recovery stashes are intentionally retained under the label alera handoff recovery to avoid racing another worktree's shared stash stack. The CLI and desktop report that backup; inspect and remove it in Git Stashes after verifying the move. Errors preserve recovery OIDs and worktree paths rather than discarding data. If filesystem cleanup fails after the persisted transfer, tabs and sessions follow their committed destination and the remaining child is available for explicit cleanup. No transfer automatically stashes or discards unrelated main-worktree changes.

Metadata-only recovery remains available as alera workspace register and alera workspace unregister; those commands do not touch Git worktrees. alera workspace register --host-id stores host metadata only and does not create a remote Git worktree or attach the local workbench. The Flutter UI calls the same runtime-host RPCs instead of owning Git worktree create/remove itself. SSH target bootstrap installs the runtime sidecar only. A workspace whose hostId is not local has its terminals spawned as ssh into the remote worktree, and workspace.files.list / workspace.files.read read that tree over SSH. The Desktop New Workspace picker does not yet offer a remote-host choice.

Workspace pin state is runtime-owned metadata and does not modify Git worktrees. The sidebar renders matching pins in a global, collapsible Pinned section while retaining each workspace in its normal project tree; when grouping is set to none, the flat list sits under a collapsible All section header that marks where the pinned copies end. alera workspace pin --id <workspace-id> and alera workspace unpin --id <workspace-id> update the same state through the runtime host, which broadcasts workspacesChanged so connected desktop clients refresh immediately. Pin updates do not change workspace recency timestamps.

alera workspace rename --name <name> [--id <workspace-id>] changes only the workspace display name through the host's workspace.rename verb, the same one the mobile app uses, and falls back to the runtime store when no host is connected. It never renames the branch or the worktree folder. --id defaults to the workspace of the calling Alera terminal.

Agent skills live under skills/: alera-cli for Alera-managed workspace lifecycle and runtime metadata, and alera-orchestration v2 for scoped runs, task DAGs, atomic worker lifecycle, decision gates, and recovery. alera-agent-profiles is the first optional Extra Skill and covers current model research, quota-aware catalog design, Managed configuration, and launch validation. Settings exposes one action for the two core skills plus separate actions for every core and Extra Skill through npx, bunx, or an auto runner. Every install command passes --agent codex to avoid the Skills CLI expanding a global install to project-only universal agents such as PromptScript, plus --yes to skip confirmation prompts. The orchestration action also reconciles the status hooks already selected in Settings; it does not enable additional agents, and the runtime host writes those hooks into each agent's global config. The in-app auto runner falls back to bunx only when npx is missing; copied Auto commands carry that same fallback so they work in terminals that have only Bun, and its syntax is chosen per platform. macOS and Linux get npx ... || bunx .... Windows gets a single-line if (Get-Command npx -ErrorAction SilentlyContinue) { npx ... } else { bunx ... }, because Windows PowerShell 5.1 has no || statement separator, because pasting a multi-line block into its console runs each line as it arrives, and because a missing npx raises CommandNotFoundException without updating $LASTEXITCODE, so an exit-code chain would skip the fallback. The app hydrates the command environment from the user's login shell before launching the installer so GUI-launched Alera can see tools that work in an interactive terminal, runs it from a writable working directory rather than whatever the launcher inherited, and passes the runner name bare on every platform because ProcessRunner always goes through a shell and cmd.exe resolves PATHEXT itself. A failed install reports a one-line summary in the settings row and keeps the full output of every attempt behind a View Output dialog, because the line naming an installer failure is rarely the first one. The skills instruct agents to prefer the appropriate CLI workflow for Alera-managed lifecycle, metadata, Agent Profile design, and orchestration operations.

Host-side scrollback is bounded separately from the xterm row scrollback used for rendering. Each terminal session keeps a byte-limited in-memory buffer, defaulting to 10 MB per session, while the terminal host persists output incrementally as ordered SQLite outputChunks. Checkpoints persist session metadata about every five seconds plus immediately on detach, exit, configuration trimming, and host shutdown; scrollback restore reconstructs the byte buffer from the retained output chunks. Databases that still use the older checkpoint buffer column are reset to the chunked schema instead of migrated, so old detached scrollback is discarded on first open after the schema change.

PTY input is isolated per terminal session. The runtime actor enqueues writes into a bounded session-local channel, and a dedicated blocking writer thread acknowledges completion back to the actor; a stalled PTY therefore applies terminal_input_backpressure only to that session instead of blocking all runtime RPCs and terminals. Client write RPCs complete only after the writer reports success. PTY reads use 64 KiB chunks and wait for actor acknowledgement, so output producers receive natural backpressure instead of filling an unbounded actor queue. Live output is coalesced on the short UI cadence independently from durable SQLite output batches, and persistence barriers are bounded so a slow disk write cannot hold host shutdown or checkpoint coordination forever.

The Flutter client does not expose a socket until the authenticated hello response succeeds. A request timeout removes the pending RPC, closes the entire connection, and forces the next operation to authenticate a fresh connection. Terminal input is not retried after a timeout or connection loss because the host may have applied it before the acknowledgement was lost; the only automatic remint path requires an explicit Terminal session is not attached rejection. Infrastructure failures appear as a recoverable Terminal Unavailable state instead of being written into terminal scrollback.

Terminal output delivery is scoped per connected app client. Each outbound client queue holds at most eight 64 KiB output batches. When a terminal surface is hidden, the app sends setOutputPaused for that terminalSessionId; the host keeps the PTY and byte buffer running but stops streaming output frames to that client. If a visible client falls behind and fills its queue, the host pauses only that client's stream, retries a small outputResyncRequired event after capacity returns, and the client requests a current snapshot before resuming live output. The Flutter frame queue is independently capped at 1 MiB of decoded text. Exit and error events are still delivered while output is paused.

Interactive terminal applications can own mouse input through the xterm DEC mouse modes. Alera forwards canonical SGR wheel, button-motion, any-motion, and pixel-coordinate reports in both terminal buffers; Shift-drag always remains available for local selection. By default a primary-button drag also stays a local selection while a TUI tracks the mouse (Drag To Select In TUIs): clicks and wheel input still reach the TUI, a press that becomes a drag is never reported, and a press that does not is reported as a click on release. Copy-on-select is on by default, so selecting in any agent copies the same way regardless of whether that agent tracks the mouse. The terminal settings control the TUI wheel multiplier and both behaviors. On Linux and Windows, Ctrl+C copies a local selection but continues to send the interrupt byte when no selection exists; macOS retains its native Command+C copy binding.

Terminal clipboard integration is explicit at the runtime boundary. Text takes priority during paste; when the clipboard has no text but contains an image, the Linux GTK runner or the Rust bridge on macOS and Windows writes a private, size-limited PNG under the platform temporary directory and pastes its path into the terminal. The GTK path uses the compositor-native clipboard API, so it also works in pure Wayland sessions without XWayland or data-control support. Expired Alera clipboard images are removed opportunistically. OSC 52 clipboard queries are ignored, and writes are size-limited and disabled by default; enabling Allow OSC 52 Clipboard Writes applies the policy to all active terminal sessions. The mobile app applies the same off-by-default policy through a phone-local setting (Settings > Terminal > Allow OSC 52 Clipboard Writes): when enabled, size-limited writes land on the phone's clipboard and are announced, because the agent runs on the paired machine and OSC 52 is the only copy path that can reach the phone; a blocked write shows a one-time notice, and queries are always ignored.

Voice Home Agent

The runtime owns a hidden folder project (alera-home) and one shared task for a global voice conversation. Speech I/O is a replaceable chained or realtime pipeline; the home CLI agent thinks and delegates through existing orchestration commands. See Voice.

AI Assist

AI Assist runs a user-installed agent CLI for short one-shot jobs such as commit messages, pull request details, reading diffs, workspace identity, agent titles, and speech cleanup. Settings define a global agent and model, while each supported prompt can independently override either value or inherit it. The runtime stores the portable execution settings so desktop and mobile workspace-identity generation resolve the same effective configuration. The persisted runtime key remains aiTextGeneration, and the host verbs stay aiText.*, so older hosts and existing settings keep working.

Agent titles are generated and persisted by the runtime for terminal conversations, with shared desktop/mobile actions, conversation identity checks, and manual-name protection. See Agent Titles for settings, lifecycle behavior, and the additive protocol. Native provider session and thread identifiers are stored on the same tab record and used to resume a supported agent after its process is gone; see Inter-Agent Orchestration.

Desktop Text Actions are reusable, ordered instructions that run the shared agent runner against a captured selection in an editable field. Each action persists under the additive runtime textActions setting with a stable id, optional agent/model overrides, and per-model reasoning; older hosts omit that field and the local settings repository remains the fallback. The menu is available only when AI Assist is enabled and at least one action is enabled, and a replacement is applied only when the complete editing value is unchanged after the run.

Reading Diff is a manual source-control action backed by the agent, model and effort configured for the AI Assist operation. It is available for Codex, Claude Code, GitHub Copilot, Pi and Grok Build because those adapters provide a verifiable diff-only execution mode. Codex runs with a restricted filesystem, no network, a replaced environment and subscription authentication; the other supported adapters run without tools from an isolated temporary directory. Codex, Claude Code and Grok Build receive native CLI schema controls, while GitHub Copilot and Pi receive the same schema in the prompt and their result is still parsed and validated by Rust. Unsupported or legacy selections fall back to Codex in the Reading Diffs settings. The agent can only propose versioned remove, replace and fold operations against immutable one-based coordinates; the Apache-2.0-derived Rust core validates source projection, structural metadata, exact moves and conservative Python boundaries before it emits any reading diff bytes. One bounded repair run may replace an invalid plan. Text patches below the 4 MiB safety limit include complete untracked-file content even when the normal visual preview is capped; oversized input is rejected before agent quota is used. Successful results are cached atomically by rubric, schema, resolved agent settings, user instructions and raw diff, while cancellation kills the active CLI and never commits partial cache data. Exact hosted-review objects are retained by a per-tab ref and released by the runtime when that persisted tab disappears, including removals made while the desktop is disconnected. A private temporary operation marker records the exact nested repository before hosted objects are fetched, so startup reconciliation can sweep orphaned refs even if the app exits before the tab is persisted. The original diff remains available beside the abbreviated view, and the confirmation dialog identifies potential provider usage before any uncached invocation.

Grok Build participates in the existing commit-message flow through grok models discovery and headless --prompt-file execution. Alera writes the generated prompt into a private temporary directory, disables Grok tools, subagents, memory, and web access for that invocation, and uses a disposable GROK_HOME containing temporary copies of the current authentication and configuration policy files. The directory, including Grok's automatically persisted session, is removed on success, failure, timeout, or cancellation. The default reasoning option leaves Grok's model-specific effort unchanged; explicit levels are forwarded with --effort.

fx participates through fx models --json discovery and fx ask --no-save execution. Alera sends the generated prompt on standard input, leaves the configured model unchanged unless the user selects one, and applies the selection with FX_MODEL. AI Assist keeps permission prompts enabled, disables automatic upgrades, and opts out of Herdr reporting for the short-lived background process so it cannot claim a terminal workspace status.

Agent Hook Status

Alera can show Codex, Claude Code, GitHub Copilot, Cursor, Antigravity, OpenCode, Pi, Amp, Grok Build, and fx status for terminal workspace tabs. The runtime owns the integration switches, hook receiver, launch metadata, normalization, and presence registry. Every switch defaults off. Desktop Settings, Mobile Settings, and alera runtime agents status|enable|disable update the same runtime-owned configuration. Changes propagate live to connected apps and are guaranteed for terminals created after the update.

The hook receiver runs inside alera runtime-host. It binds to loopback, writes endpoint and token metadata under the runtime directory, and accepts the eleven agent-specific hook routes with X-Alera-Agent-Hook-Token. Terminal creation strips inherited Alera hook metadata and injects the runtime endpoint plus terminal session, workspace, and tab identity. Events are accepted only for enabled agents and exact live runtime identities, so a standalone host remains authoritative even when no desktop process is connected. Managed scripts read the current endpoint metadata for each event and no-op when the required terminal identity is absent. Antigravity and Copilot are the exception: both read a JSON response on stdout, so their scripts answer first and only then decide whether there is anything to report.

Antigravity is the one integration whose config shape differs enough to record here, because two installers write it: the runtime one (rust/alera-cli/src/agent_status/integration_config.rs) and the desktop one (lib/src/features/agent_status/infra/managed_hooks/agy_managed_agent_hook.dart). Both target ~/.gemini/config/hooks.json and both own the single top-level alera-status bundle in it, so each removes the other's handler before writing its own rather than stacking beside it. Inside a bundle Antigravity uses two schemas: PreInvocation, PostInvocation, and Stop take a flat { type, command, timeout } handler, while PostToolUse takes { matcher, hooks: [...] }. Every hook must answer on stdout - {} for the passive events and a decision for Stop, where Alera sends "" to allow the stop - and it must do so before the endpoint guards, because a hook that cannot reach Alera still owes Antigravity a reply. PreToolUse is deliberately never installed: Antigravity requires a permission decision from it, and an observational status hook has no value to return that leaves the user's tool policy alone. A bundle's documented enabled: false flag is honored - Alera reports the integration as partial rather than installed - and clearing it is what an explicit install means.

Cursor is the other integration worth recording, because its hooks are fail-open and must not print a permission verdict. Alera writes command entries into the user's own ~/.cursor/hooks.json and removes only Alera-marked definitions on cleanup. Unlike Antigravity and Copilot, Cursor's hooks answer nothing on stdout: a hook that stays silent is fail-open there, while a permission verdict would replace Cursor's own approval prompt, so silence is what keeps the user's tool policy alone. That is also why preToolUse can be installed for Cursor while PreToolUse cannot for Antigravity. beforeShellExecution and beforeMCPExecution stay working, because Cursor emits those events whether or not the user is actually asked; treating them as waiting would notify on every command. Cursor currently omits preToolUse for AskQuestion; a human-input preToolUse is still mapped to waiting so a later CLI fix does not require another Alera change. Shell approval prompts in the TUI still do not notify. sessionEnd is registered alongside stop because a headless cursor-agent -p run emits no stop at all. Older Alera versions used a per-session plugin and cursor-agent wrapper; host start deletes leftover overlay directories under the runtime dir.

Grok Build is the agent whose own hook discovery would otherwise steal another agent's identity. By default it also scans ~/.claude/settings.json and ~/.cursor/hooks.json, so a Grok turn would run Alera's Claude or Cursor hooks and POST them as /hook/claude or /hook/cursor. Every Alera terminal therefore sets GROK_CLAUDE_HOOKS_ENABLED=false and GROK_CURSOR_HOOKS_ENABLED=false; Grok still runs ~/.grok/hooks/. Claude install writes Alera hooks into the user's own ~/.claude/settings.json. That file is what reaches CCS: CCS overrides CLAUDE_CONFIG_DIR to $CCS_DIR/instances/<account>, whose settings.json is a symlink chain back to the user file, and a config directory contributes settings.json and nothing else to Claude - a settings.local.json there is not a settings source, which is why the hooks Alera used to write into the instances were never loaded and no CCS session was ever detected. Writing inside CCS instead is not an option either: CCS reconciles those paths on every launch and adopts a diverged copy back into the user file. Because Grok scans that same file, the managed Claude command carries its own guard and reports only when CLAUDECODE is set and GROK_HOOK_EVENT is not; the Cursor command no-ops when GROK_HOOK_EVENT is set. CLAUDE_PROJECT_DIR is no discriminator, since Grok exports it too. The install is a no-op when the file is already current, so a reconcile does not rewrite a file the user owns. Turning the Claude toggle off strips the managed definitions from ~/.claude/settings.json and ~/.claude/settings.local.json independently per file and after stripping JSONC comments, and clears the dead CCS instance leftovers. Presence identity keeps an active parent when a foreign hook lands, except Claude is a weak parent: a later Grok or Cursor event on the same session takes over so a leftover Claude hook that arrived first cannot lock the sidebar for 30 minutes.

See Agent Status Hooks for the files Alera writes, the markers it uses, and how to clean them.

fx uses its built-in Herdr integration on macOS and Linux. For enabled terminals the host supplies a private Unix socket and the terminal session id as the Herdr pane id. The receiver accepts only custom:fx authority reports, maps working, blocked, and idle into the shared presence registry, and resolves the workspace and tab from the live terminal session rather than trusting external identifiers. Existing Herdr variables are restored when nested Alera terminal environments are reused.

Agent status remains in runtime memory and is keyed by terminal session id. Desktop mirrors the runtime snapshot into its existing status controller, while mobile reads the same runtime payload. The workbench and mobile surfaces aggregate only agents whose own terminal belongs directly to that workspace; child-workspace agents are never rolled into a parent's state. Mobile separately shows whether a workspace has terminal tabs, its Default marker, child count, and direct agent summary. Expanded agent rows persist locally per paired host, show prompt/tool/message details, open the exact terminal tab, and require confirmation before closing it. The state priority is blocked, waiting, working, then done; this projection is not persisted to Workspace.status.

Agent quota status is a separate host-scoped projection. The runtime queries the configured providers for its local host and keeps a 15-minute in-memory cache with stale fallback; desktop SSH workspaces still query the target through alera runtime-proxy. Automatic reads and manual refreshes use OAuth or API paths for Claude and the other HTTP providers; Antigravity still scrapes its TUI on those paths. Claude TUI recovery is an explicit per-profile action (agentQuota.fetchClaudeTui) rather than part of snapshot or force-refresh. Desktop and Mobile use the same runtime-owned local quota configuration and receive live settings changes. Provider credentials remain on the machine where the query runs. Settings persist enabled providers, Claude CCS profile names, and environment variable names only; secret values are never stored by Alera. See agent-quotas.md for supported providers and configuration.

Alera can also show native desktop notifications for agent status when the user enables Agent Status Notifications in Settings. Those desktop notifications are default-off, require at least one agent hook toggle to be enabled, and are emitted only while the Flutter app process is running. Notification titles name the agent and state, while bodies use project/workspace location such as Workspace main in Alera rather than the user's prompt text. Notification payloads include the terminal session, workspace, tab, agent type, and state so selecting the notification can foreground the app, select the matching workspace, activate the matching workspace tab, and request terminal focus.

Which states notify is deliberately narrow, because every supported agent reports the end of a turn rather than the end of a task: Stop for Claude, Codex, Antigravity and Grok, stop/sessionEnd for Cursor, SessionIdle for OpenCode, agent_end for Pi, agent.end for Amp, and idle for fx. Notifying on all of them turns a normal back-and-forth into one notification per reply, so waiting and blocked notify under the main toggle while done needs the separate default-off Agent Finished Notifications. working updates stay in the in-app status projection.

Three dampers sit between the status controller and the OS, all in lib/src/features/agent_status/application/. Statuses that started before the notification coordinator did are never notified: agent presence lives for the life of the PTY, so the first snapshot after a launch or a host reconnect would otherwise replay every waiting agent at once, and the contract is that nothing is buffered or replayed. A repeat of the same state on the same terminal is held for a cooldown, keyed by session and state without the state start time, so an agent asking for approval repeatedly, a done -> working -> done bounce, and the runtime snapshot restating a transition the local hook already reported all collapse into one notification. Finally the coordinator buffers a short quiet window, delivering a burst as one grouped notification (2 agents need attention) that names the workspaces involved and opens the terminal that moved last. The coordinator reads its settings rather than watching them, because rebuilding it would discard that delivery history.

Cloud Identity And Mobile Push

An Alera account is optional and gates shared cloud features rather than local projects, terminals, workspaces, pairing, or orchestration. The desktop runtime starts Google or GitHub authorization with a loopback redirect, state, and PKCE. The Axum backend exchanges the provider code because GitHub requires a client secret that cannot ship in a public binary, resolves the provider identity, and emits a provider-independent Alera session. Google and GitHub identities with the same verified email auto-link; unverified email never does. The runtime keeps account metadata in runtime.sqlite and stores the refresh credential in the operating-system keyring, with a private-file fallback when the keyring is unavailable.

The public path is a Cloudflare Worker in front of the containerized Axum service on Cloud Run. The Worker accepts only the account and push API surface, applies an edge burst limit, overwrites a private origin header, and proxies bytes without participating in the runtime-host protocol. Cloud Run stores account state in Neon Postgres, signs short-lived Alera access tokens through Cloud KMS, and sends with Firebase Cloud Messaging through its workload identity. No service-account private key exists in the repository or release artifacts. See cloud-backend.md for the boundary and cloud-operations.md for deployment and key rotation.

Mobile account enrollment is initiated by an authenticated runtime and redeemed once by the phone. The mobile app receives its own least-privilege Alera session, stores it in platform secure storage, and can retain multiple account sessions. FCM tokens and per-runtime subscriptions live in the central backend rather than in runtime.sqlite, so one phone can subscribe to more than one runtime without exposing its delivery token to those runtimes. Runtime event responses carry the authoritative active-subscription count back to the host for its lifecycle cache. The host also refreshes that count from Cloud when it starts, when runtime push is enabled, and after a paired phone changes a subscription, so the first subscription can retain an otherwise idle host and removal releases it. Issuing an enrollment code alone does not count as a subscription.

Mobile push has a separate explicit runtime opt-in. The default category settings are attention on, agent done off, and terminal exit off, but all remain inert until the main push switch is enabled and at least one mobile device subscribes. Attention includes waiting and blocked agents plus orchestration decision gates and escalations. The runtime discards replayed status snapshots, applies a 60-second per-session/state cooldown, groups nearby events, and sends an idempotent runtime event to the backend. The payload may contain agent state plus project and workspace names, but never prompts, terminal input or output, source code, or arbitrary orchestration text.

Firebase delivers notifications while the mobile app is backgrounded or closed. A tap resolves the account, runtime, workspace, tab, and terminal identifiers and falls back to the runtime list when local pairing state is unavailable. Android wiring is implemented but an end-to-end release test requires the production Firebase client file and deployed cloud credentials. iOS wiring is prepared but remains unverified until Apple signing, provisioning, and an APNs key are available.

Resource Manager

The Resource Manager reports how much CPU and memory Alera and the processes it spawns are using. It is monitoring and cleanup only: it never throttles, queues, or caps anything.

Sampling lives in the sidecar, not in the app, because the sidecar already owns the PTYs and the sessionId -> workspaceId -> tabId relation, and because sweeping the process table is work that must stay off the Flutter main isolate. Session records the shell's pid at spawn and clears it on exit and terminate: the OS recycles pids, and a stale value would attribute an unrelated process to a dead session. The sampler (rust/alera-cli/src/terminal_host/resources.rs) keeps one long-lived sysinfo::System because CPU is a delta between two refreshes, walks each shell's process subtree, and keeps a 60-sample memory ring buffer per session for the sparklines. Only memory is historized; a CPU sparkline at this cadence is noise.

Attribution order matters: session subtrees are claimed first and a shared claimed set prevents double counting. Every PTY shell is a child of the runtime host process, so measuring the host first would swallow every terminal into one unattributed row. The app's own pid travels in the resources.snapshot payload rather than in configure, because it belongs to one client rather than to the host's shared configuration.

Sampling is lazy. The first resources.snapshot starts a 2-second ticker that stops itself after 10 seconds without a request, so an unattended host never sweeps the process table. The ticker only enqueues a ServerCommand; the refresh itself runs on a blocking thread and posts its result back, so all state mutation stays single-threaded in ServerActor. The request always answers with the last cached sweep and never blocks on one. Early sweeps report warming: true and the UI renders a dash rather than a misleading 0%.

How many sweeps count as warming is platform-specific, and was measured rather than assumed. Against child processes pegging a full core with sysinfo 0.39.6: Linux and macOS report ~100% on the second sweep, while Windows still reports 0.0 there and only reports ~96% on the third. REFRESHES_BEFORE_CPU_IS_VALID is therefore 3 on Windows and 2 elsewhere; treating the second sweep as valid on Windows published a confident 0% for a saturated machine. CPU semantics themselves are uniform: on all three platforms cpu_usage() is percent of a single core, so a busy multi-threaded process can exceed 100%. The wire keeps that unit, because an app can attach to an already-running older sidecar and the meaning must not depend on which side is newer; the app divides by cpuCoreCount in machineCpuShare so the panel reports a share of the machine instead. A per-core percentage next to a memory column that already reads "% of RAM" is what made a 16-core machine report CPU 153.9% while its own monitor read 40%. load_average() is real on Linux and macOS and always zero on Windows. CI runs cargo test --workspace on Linux only, so platform-specific sampling behavior has to be re-checked on a real Windows and macOS machine rather than trusted to the pipeline.

resources.snapshot and the resourceMonitorV1 capability are additive and MUST NOT bump aleraTerminalHostProtocolVersion. The verb is not on the mobile allowlist: v1 is desktop and local-host only.

On the app side, buildResourceTree is a pure function that joins the host's flat session list with the workbench's projects, workspaces, and tabs. A session with no terminal tab is an orphan, listed apart from the tree and killable without confirmation because there is no pane to lose; killing a session that still has a tab closes the tab through the normal workbench flow instead. Metrics are nullable rather than zero: a workspace whose hostId is not local is treated as remote (absent metrics rather than local zeros), and an unknown core count leaves CPU absent rather than passing the per-core number off as a machine share. hostId names the SSH target for a remote workspace. Terminals attach over SSH, and the Desktop explorer reads files through workspace.files.list / workspace.files.read. Remoteness is decided by hostId, never by missing samples, so a local session that just reconnected to a warm host is not mislabelled. The panel polls every 2 seconds while open and every 15 seconds while closed, which keeps the status-bar chip fresh without paying for sweeps nobody is watching. A host that does not support the verb degrades to an unavailable snapshot: the chip is ambient UI and must not break the status bar. Host health is not duplicated here; the panel points at the existing runtime host chip.