# Git

> Reference for the OpenEng Git app: a browser workbench backed by a local sealed engine on 127.0.0.1:50053 that runs all git operations against your own repos and credentials. The web app is live; the engine isn't published yet.

The OpenEng Git app is a browser-based git workbench backed by a local, sealed engine. The UI lives at `git.openeng.app`; every git operation actually runs in a small gRPC server bound to `127.0.0.1:50053` on your own machine, against your own working trees and credentials. Nothing about your repositories or the credentials that authenticate a push goes to OpenEng: git work runs on your machine, push, pull and fetch go to your remotes, and the CI & CD view calls your forge from the browser. Apart from CI & CD, the browser is a thin renderer; the engine does all the git work with `libgit2` and your own `git` binary.

> [!WARNING] The Git engine is not published yet
> The web app at [git.openeng.app](https://git.openeng.app) is live, but no `openeng-git` engine binary has been published for any platform yet. The installer at `https://openeng.app/engine/git/install.sh` exists, but it stops with a message that no Git engine binary is published for your platform, so the app cannot be paired until the engine ships. Git is free, like every engineering app. This page describes the app and engine as built, so it is ready when the engine is released.

## Overview & architecture

The Git app follows the same "one engine, many surfaces" split as the rest of the OpenEng suite. The web UI issues repo-scoped operations; a locally-running engine executes them and returns structured results. The single boundary between the two is a per-frame-encrypted gRPC channel over loopback, guarded by a handshake token (see the origin caveat below).

- **Web UI** — Served from `git.openeng.app`. Renders the workspace overview, repo workbench (Changes / History / Branches), diff and commit-graph panes. Navigation is URL-driven, so refresh and deep-links restore the exact repo, tab, file, or commit.
- **Local engine** — The `openeng-git` binary running `serve` on `127.0.0.1:50053`. Implements every RPC in `git.proto`. One engine per machine, coexisting with the other app engines, such as Terminal (50052). The OpenEng AI engine binds no local port, so it never competes for one.
- **Sealed transport** — gRPC-Web over TLS, reached at `https://engine.openeng.app:50053`. That host resolves to `127.0.0.1` everywhere but carries a publicly-trusted certificate, so the browser connects with no manual trust step while the socket stays pure loopback. The exact-origin lockdown of the Linux Terminal, Kubernetes and Data engines is still pending here: in its current source the Git engine answers any web origin and hands its handshake token to any caller.

> [!NOTE]
> The engine contacts OpenEng only to pair and keep its sign-in fresh (`api.openeng.app`: the one-time-key exchange at boot, token renewal and the engine certificate) and to check for updates (`releases.openeng.app`, at start and every 30 minutes), and, when an update exists, downloads it from `openeng.app`. Every git operation is local, except the network operations you ask for (push, pull and fetch go to your remotes). Your repositories and credentials never go to OpenEng.

### Two backends, chosen per operation

The engine deliberately uses two mechanisms and routes each operation to whichever is stronger:

- **`libgit2` (the `git2` crate)** for read-heavy, structured work: status, diff, log, single-commit detail, branch listing (with ahead/behind), blame, stash listing, tags, remotes, branch create/checkout/delete, and plain (unsigned) commits.
- **Your own `git` binary** (shelled out) for what libgit2 does poorly or not at all, and — crucially — anything touching the network or your signing keys: push, pull, fetch (streamed), merge, stash save/apply/pop/drop, GPG/signoff commits, and cross-history search.

Shelling out to `git` is what lets remote operations transparently reuse your local credential helper and SSH agent: the child process inherits your environment, so authentication "just works" without a single credential ever crossing the gRPC wire.

## Getting connected

Opening `git.openeng.app` lands on the pairing gate (`/pair`) until a local engine is reachable and a session is established. If no engine is running, the screen shows a copy-paste command; the app connects automatically the moment the engine comes up (via Retry or the background heartbeat) and returns you to the originally requested URL.

### Booting the engine

The pairing screen mints a single-use, short-lived OAuth key and shows a command of the form:

```bash
openeng-git serve --oauth=<one-time key>
```

OAuth is the only supported boot path — there is no offline or static-key mode. The engine consumes the key, authenticates you, binds loopback, and advertises its endpoint for the browser to discover.

### The handshake & per-frame seal

`CreateSession` is the *only* unsealed RPC — it bootstraps the seal. The client offers a protocol version and an X25519 public key; the engine replies with its own public key, a session id (`ses_<uuid>`), its protocol version, and a compatibility verdict. From then on every frame is sealed with a fresh ephemeral X25519 key, HKDF-SHA256 and ChaCha20-Poly1305, so even the loopback payload is encrypted. The key exchange isn't authenticated, a session opened without a client key isn't sealed, and a request frame that fails to unseal is passed on as it arrived.

The handshake response also carries three display-only fields, read from your global git config and home directory:

| Field | Source | Purpose |
| --- | --- | --- |
| `user` | `git config user.name` | Shown in the header; never a credential. |
| `email` | `git config user.email` | Display only. |
| `default_root` | Your home directory | Placeholder for the manual "add repo by path" field. |

### Protocol compatibility

The wire contract is versioned. The engine currently speaks `openeng.git.v1` (version `1.0`). Compatibility is major-only: a client and engine with the same *major* interoperate (minor bumps are additive and backward-compatible), while a *major* mismatch is fatal and the session response carries human-readable "update your client/engine" guidance in `mismatch_reason`.

> [!NOTE] Invariant
> The schema has no field for an OpenEng secret or product prompt, and none for a credential, API key, password, or private key: remote auth uses your local git credential helper or SSH agent. A test in the proto crate checks the field names. The only legitimate payload is your own repo data: paths, diffs, commit messages, and refs.

## Using the app

This section walks through the workbench at `git.openeng.app` as built. The sections after it are the reference for what the engine does underneath.

### The workspace

- **Left menu:** **Overview**, **Repos**, **CI & CD**, **Requests**, and **Insights**, with **Settings** at the bottom. Every view has its own URL: `/overview`, `/cicd`, `/requests`, `/insights`, `/settings`, and, for a repository, `/repos/<repo-id>/changes`, `/repos/<repo-id>/history`, `/repos/<repo-id>/branches`, and `/repos/<repo-id>/history/<commit>` for one commit (the selected file rides along as `?file=`, plus `&staged=1` for its staged side). A refresh, a shared link, or back and forward restores the exact view. The old `/connect` path opens `/settings`, and any other path opens `/overview`.
- **Header:** a button that hides or shows the side panel, the **git server switcher**, a status dot for the engine, a search button for the command palette, and your account menu. The server switcher lists **All servers** and each git server (forge host) with its repository count. Picking one limits the repository lists, including the one in CI & CD, to repositories whose remote is on that host. The list ends with **Add server…** and **Settings**.
- **Command palette:** <kbd>⌘</kbd>+<kbd>K</kbd> (<kbd>Ctrl</kbd>+<kbd>K</kbd> on Windows and Linux) jumps to any section or opens any repository's workbench.

### Overview

**Overview** (`/overview`) is a dashboard across all your repositories, built only from what your engine reports:

- **Stat cards:** **Repositories** (how many, across how many workspace folders, with a sparkline of commit activity), **Uncommitted** (how many repositories have changes), and **Open requests** and **Pipelines today**. The last two need forge data the engine does not have, so they show `0` with the hint "connect a forge".
- **Repository health table:** each repository's branch (or `detached`), how far it is ahead of or behind its upstream (or in sync), its working tree (**Clean**, **Modified**, or **Conflicts**), and a CI column that shows `—`. Click a repository's name to open it.
- **Needs attention:** repositories with merge conflicts first, then repositories behind their upstream, then repositories with uncommitted changes. When nothing qualifies, it says "Everything is in sync".
- **Recent activity:** the newest commits across your repositories (up to 8 from each, 16 in total), with author, message, repository, branch, and age.
- **Add repository** takes you to the Repos section.

### Repos and the workbench

- The **Repos** side panel lists every repository with its branch, an uncommitted-changes marker, and ahead/behind counts. The engine caches the list, so it appears instantly and shows "refreshing…" while it updates. The panel has refresh, remove, and collapse buttons.
- Opening a repository opens its workbench: a header with the repository's name, current branch, and a **Push** button (it pushes the current branch and sets its upstream, with a live progress log); a navigator with **Changes**, **History**, and **Branches** tabs; and a detail pane on the right.
- **Changes:** staged and unstaged files, each with an M/A/D badge and line counts. Tick a file's checkbox to stage or unstage it, or use **stage all** and **unstage all**. Click a file to see its diff (working tree or staged) with line numbers, renames marked, and "Binary file — no textual diff." for binaries. Write a commit message (<kbd>Enter</kbd> commits, <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line) and click the commit button, which reads like "Commit 2 files to main" and stays disabled until something is staged and the message is not empty. See [Staging & committing](#staging-and-committing).
- **History:** the commit list, with summary, short hash, and age, while the right pane shows the **Commit graph**: branch lanes, merge joins, a ring on `HEAD`, and branch and tag labels. Click a commit in either one to see its full message, author, time, and the diff of every file it changed; the **×** closes it and brings the graph back.
- **Branches:** the right pane shows the same commit graph. The navigator lists local branches with a tracking label (`↑n`, `↓n`, `synced`, or `local`); click a branch to check it out, and use **+** to create a branch (it is checked out right away). The current branch's card has **Push** and **Pull** buttons with a live progress log, and **Remotes** lists the configured remotes.

### CI & CD

**CI & CD** (`/cicd`) shows a repository's latest pipeline or workflow runs (up to 25), read from its forge: GitHub Actions, GitLab pipelines, Bitbucket Pipelines, and Gitea Actions (best effort). Pick a repository in the side panel; the app works out the forge from the repository's remote URL. Each run shows its status, name, branch, commit, start time, duration, and a link to the run on the forge. Azure DevOps runs are not wired up yet, and hosts set to **Other** are not supported. A repository with no remote shows a message saying so.

The first time, the section asks for an API token, with steps for creating one and the scope it needs:

| Forge | Token scope |
| --- | --- |
| GitHub | Actions: Read-only (fine-grained token), or `repo` and `workflow` (classic token) |
| GitLab | `read_api` |
| Bitbucket | Pipelines: Read (a repository access token, not an app password) |
| Gitea / Forgejo | `read:repository` |

These requests go straight from your browser to the forge's API. The token is saved only in that browser (never in the engine or its cache), and it is only ever sent to the API address it was saved for; if a server's address changes, the app asks you to confirm the token again. If the forge rejects the token (HTTP 401 or 403), the token form comes back.

### Settings

**Settings** (`/settings`) has two categories:

- **Repositories:** your repositories. **Add a repository** opens your system's folder picker on your machine (through the engine), or choose **enter a path manually** to type a path relative to your home directory. Detected repositories appear on their own; see [Managing repositories](#managing-repositories). Removing a repository only forgets it; the files on disk are not touched.
- **Connect:** your git servers. Hosts found in your repositories' remotes appear automatically (tagged "auto"). **Add server** asks for the platform (GitHub, GitLab, Bitbucket, Gitea / Forgejo, Azure DevOps, or Other), a name, the host or, for a self-hosted forge, its API base URL, an API token, and an SSH key label. The server list is shared through the engine with every browser on the machine; the token stays in the browser you entered it in, and **Clear saved token** removes it. The SSH key label is only a label: push, pull, and fetch keep using your ssh-agent and git credential helper, exactly as on the command line.

### Requests and Insights

**Requests** (pull and merge requests) and **Insights** (activity trends) are placeholders marked "Not connected"; they do not show any data yet.

## Managing repositories

Every operation except the handshake is repo-scoped by a stable `repo_id`. The engine keeps a registry of your working trees, populated two ways and persisted so it survives restarts and is shared across browser tabs.

### How repositories enter the registry

- **Detection** — a bounded scan (depth 3) of your home directory plus conventional workspace roots (`Projects`, `projects`, `src`, `Code`, `code`, `dev`, `Developer`, `workspace`, `git`, `repos`, `work`), with `$HOME` itself scanned shallowly. The scan skips heavy/noisy directories (`node_modules`, `target`, `vendor`, and top-level dotfolders) and stops descending once it finds a `.git`, so it stays fast on a large home.
- **Explicit** — `AddRepo(path)` for a directory you point at. The path is validated (it must be a git working tree — have a `.git` directory/file, or be openable by libgit2) and canonicalized to an absolute path before it is stored.

Each repo's id is the first 16 bytes of the SHA-256 of its absolute path, rendered as 32 hex characters — so the same path always maps to the same id, across restarts and detection runs. The registry (a list of paths) persists to `~/.openeng/git/state/repos.json`.

### The repository RPCs

| RPC | Returns | Notes |
| --- | --- | --- |
| `ListRepos` | Every registered repo with live facts | Each entry carries id, path, name, current branch, dirty flag, and ahead/behind. A facts failure degrades to an identity-only entry rather than failing the whole list. |
| `AddRepo` | The new (or existing) repo | Idempotent — re-adding a path returns the same entry. Rejects a non-repo with an invalid-argument error. |
| `RemoveRepo` | `removed: bool` | Forgets the repo from the registry only. It never touches the working tree on disk. |
| `GetRepoInfo` | Facts + remotes + upstream | Adds `head_oid`, `head_summary`, the configured remotes, and the upstream ref (e.g. `origin/main`). |

### Repo facts

For any repo the engine computes, on demand: the current branch (or the short OID / `(detached)` for a detached HEAD), a dirty flag (any staged, unstaged, or untracked change), the HEAD OID and summary, the configured upstream, and how many commits the branch is ahead/behind that upstream (via libgit2's `graph_ahead_behind`).

### Native folder picker

Because a browser cannot read your filesystem, the "Add repository" flow can pop the OS-native directory chooser *on your machine* via the `PickFolder` RPC, and hand the chosen absolute path to `AddRepo`. It is fail-soft: a canceled dialog, a headless host with no display, or any dialog error all return an empty path with `cancelled: true` — the RPC never errors. On Linux it uses the GTK chooser directly; on macOS it drives `osascript` as a separate process, because an in-process Cocoa panel needs a main-thread run loop the headless engine doesn't have.

> [!TIP]
> In the app, adding repositories lives under **Settings → Repositories**: use *Add a repository* to browse, or *enter a path manually* to type one relative to your home directory. Detected repos appear automatically.

## Working tree: status & diff

### Status

`GetStatus` returns a porcelain-style view grouped into three lists: **staged** (index differs from HEAD), **unstaged** (working tree differs from index), and **untracked**. A single modified-and-staged file legitimately appears in both the staged and unstaged groups, each with its own two-character XY code. Rename detection is enabled on both the head→index and index→worktree sides, so renames surface with an `orig_path`.

Each entry carries per-file line counts (`additions`/`deletions`), computed best-effort — binary files and any counting error degrade to `0/0` rather than failing. Untracked files are reported with the `??` code and no counts.

```
StatusEntry {
  path       // repo-relative path
  status     // two-char XY code: "M ", " M", "??", "A ", "D ", "R ", "UU", …
  staged     // belongs in the staged group
  untracked
  additions  deletions
  orig_path  // set for renames
}
```

### Diff

`GetDiff` is a single RPC with three modes, selected by which fields you set:

| Fields set | Diff produced |
| --- | --- |
| `commit_oid` non-empty | That commit against its first parent. |
| `staged: true` | The index against HEAD (what a commit would record). |
| neither | The working tree against the index (unstaged edits). |

An empty `path` diffs the whole tree; a non-empty `path` scopes it to one file. Diffs use 3 lines of context and include untracked content. The response is a list of `FileDiff` → `DiffHunk` → `DiffLine`, where each line has an origin (`' '` context, `'+'` add, `'-'` delete) and old/new line numbers (`0` where not applicable). Binary files are flagged and carry no hunks.

## Staging & committing

### Stage, unstage, discard

- **StageFile** — Adds a path to the index. A deleted file is handled correctly — it is removed from the index rather than added.
- **UnstageFile** — Resets the index entry back to HEAD. On an unborn branch (no HEAD yet) it simply drops the path from the index.
- **DiscardFile** — Restores the working-tree file from the index/HEAD version (a forced, path-scoped checkout). This is destructive — the working-tree change is gone.

In the workbench Changes tab, each file row has a stage/unstage checkbox, and "stage all" / "unstage all" shortcuts fan the operation across every file in a group. Selecting a file loads its diff in the shared right pane.

### Commit

`Commit` writes a commit from the current index. The engine routes it by whether signing is requested:

- **Plain commits** go through libgit2. They require a committer identity — if `user.name` / `user.email` are unset the RPC returns a clear invalid-argument error.
- **Signed (`gpg_sign`) or `signoff` commits** go through the `git` binary, because libgit2's signing support is patchy. The engine passes `--gpg-sign` / `--signoff` and resolves the resulting HEAD OID afterward.

`amend` rewrites HEAD; an empty message on an amend keeps the previous message (`--no-edit`). The response carries the new commit's OID.

> [!WARNING] Not yet in the UI
> The workbench's commit composer currently sends plain commits only. The `amend`, `signoff`, and `gpg_sign` options exist in the engine and client but are not yet exposed as UI toggles — they are implemented in the engine but not exposed in the app yet ([Install](install.md#installers)).

## History: log & commit detail

`GetLog` walks history from a given `rev` (or HEAD), time-sorted, with `limit` (default 100) and `skip` paging, and an optional `path` filter that keeps only commits which touched that path. Each `Commit` carries its OID and short OID, summary and body, author name/email, an RFC3339 commit time, parent OIDs, and the branch/tag refs that point at it (used to draw the graph).

`GetCommit` returns a single commit's metadata plus the full per-file diff of that commit against its first parent — the same `FileDiff` structure used everywhere else.

The workbench renders history two ways from one dataset: a linear commit list in the left navigator and a commit-graph DAG in the right pane, with a commit-detail pane (message + changed files + diffs) when one is selected. The History tab loads up to 120 commits.

## Branches

`ListBranches` returns local and remote branches together. Each `Branch` reports whether it is remote, whether it is the current branch, its upstream, its tip OID, and — for local branches — how far ahead/behind its upstream it is.

| RPC | Behavior |
| --- | --- |
| `CreateBranch` | Creates a branch at a `start_point` (or HEAD), optionally checking it out. |
| `CheckoutBranch` | Updates HEAD and the working tree (a safe checkout). Resolves a local branch ref, else any revparse-able ref, in which case HEAD goes detached. |
| `DeleteBranch` | Deletes a local branch. *The `force` flag is currently accepted but ignored* — deletion behaves as an ordinary delete. |
| `MergeBranch` | Shells out to `git merge` (optionally `--no-ff`). Returns `ok`, a `conflicts` flag (detected from the git output), and the combined message. |

In the app, the Branches tab lists local branches with a tracking label (`↑n` / `↓n` / synced / local), and clicking a non-current branch checks it out. The "+" opens a create-branch prompt (which checks out the new branch). Branch *deletion* and *merge* are implemented in the engine and client but are not yet surfaced in the workbench UI.

## Remotes, push, pull & fetch

`ListRemotes` returns each configured remote's name, fetch URL, and push URL. The three network operations — `Push`, `Pull`, `Fetch` — are **server-streaming**: the engine shells out to `git … --progress` and streams its output line by line as `ProgressLine` events (splitting on both `\n` and `\r` so progress repaints arrive as distinct lines), terminated by a single `ProgressDone` carrying `ok`, the exit code, and a message.

```
// The stream shape (mirrors the terminal engine's Run):
ProgressLine*  →  ProgressDone { ok, exit_code, message }
```

### Options

| Op | Options honored |
| --- | --- |
| `Push` | `set_upstream` (`--set-upstream`), `force_with_lease` (`--force-with-lease`), `tags` (`--tags`), plus optional remote and ref. |
| `Pull` | `strategy`: `merge` (`--no-rebase`), `rebase` (`--rebase`), or `ff-only` (`--ff-only`). |
| `Fetch` | `all_remotes` (`--all`) and `tags` (`--tags`). |

### Remote authentication

No credential is ever accepted from the caller. The `git` child inherits your local environment, so your credential helper and SSH agent provide authentication exactly as they would on the command line. The engine sets `GIT_TERMINAL_PROMPT=0` and a null stdin, so a missing credential fails fast rather than hanging the engine on a prompt that can never be answered.

> [!WARNING] Heads up
> Because prompts are disabled, a repository that would normally ask for a username/password interactively will fail. Configure a credential helper or SSH key so pushes and pulls authenticate non-interactively.

In the app, the workbench header has a one-click **Push** (with `set_upstream`), and the Branches tab's active-branch card offers Push and Pull with a live progress log. `Fetch` exists in the engine and its typed client but is not yet a dedicated button in the workbench.

## Stash

Stash listing uses libgit2 (index, message, OID, and a best-effort created-at timestamp), while save/apply/pop/drop shell out to `git stash`:

- `ListStash` — enumerate stash entries.
- `StashSave` — `git stash push`, optionally `--include-untracked` and with a message.
- `StashApply` — apply or pop by index (the `pop` flag selects `git stash pop stash@{N}` vs `apply`).
- `StashDrop` — `git stash drop stash@{N}`.

> [!NOTE] Not yet in the UI
> Stash is fully implemented in the engine and typed client, but the workbench does not yet render a stash panel: it is implemented in the engine but not exposed in the app yet.

## Tags

`ListTags` returns both lightweight and annotated tags, distinguishing them via the `annotated` flag and surfacing the target OID and (for annotated tags) the message. `CreateTag` creates an annotated tag when a message is given (which requires a tagger identity) or a lightweight tag otherwise, at an explicit target or HEAD. `DeleteTag` removes a tag. As with stash, tags are engine- and client-complete but not yet surfaced as a workbench panel.

## Blame

`GetBlame(path)` returns per-line attribution for a file: the line number and content (read from the working-tree file), the commit OID that last touched it, the author, the commit time, and that commit's summary. A path is required. Lines with no blame hunk (e.g. brand-new content) come back with empty attribution but their text intact. Blame is implemented in the engine and client; a blame view is not yet part of the workbench.

## Search across history

`Search(query, mode, limit)` runs a cross-history search by shelling out to `git log` with a machine-parseable format, returning matching commits. Four modes:

| Mode | git mechanism | Finds |
| --- | --- | --- |
| `message` | `--grep` | Commits whose message matches. |
| `author` | `--author` | Commits by an author. |
| `file` | `-- <path>` | Commits that touched a path. |
| `content` | `-S` (pickaxe) | Commits that added/removed an occurrence of a string. |

The result commits carry OID, summary, author, time, and parents (body and refs are omitted for search results). A failed pattern with no output returns an empty list rather than an error.

> [!NOTE] Not yet in the UI
> The `⌘K` command palette searches sections and repositories to jump between them; it does not yet drive the history `Search` RPC. Cross-history search exists in the engine and its typed client.

## Configuration, scopes & data locations

### Request scoping

Every RPC except `CreateSession` carries a `session_id` and, for repo operations, a `repo_id`. The engine resolves the id to an absolute working-tree path; an unknown id yields a `NotFound` status. There are no server-side "scopes" or permission tiers — the engine acts entirely on your local machine with your own filesystem permissions, and the sealed session is what gates access.

### Identity & config

The committer/tagger identity comes from your global git config (`user.name` / `user.email`); the engine reads it for display and requires it for commits and annotated tags. The engine does not manage git config, credentials, or SSH keys — those remain the domain of your local git installation.

### Stale-while-revalidate cache

An engine-owned, file-based key/value cache (`CacheGet` / `CacheSet` / `CacheDelete`) stores the last result of costly reads, each stamped with the wall-clock millisecond it was refreshed. On open the UI renders the cached value instantly with a small "refreshing…" hint, fetches fresh in the background, and writes it back. Because it is file-based, every browser on the machine shares one last-known state. The stored value is opaque UI-encoded JSON — never a secret. The repo list uses this under the key `repos:list`.

### Runtime & state layout

The engine roots its files under `~/.openeng/git/` (override the whole OpenEng home with the `OPENENG_HOME` env var), keeping it isolated from the AI and terminal engines:

| Path | Contents |
| --- | --- |
| `~/.openeng/git/runtime/` | The spawn lock (one engine per machine, via an `O_EXCL` PID file), the endpoint descriptor, the `0600` handshake token, and the TLS certs/keys. |
| `~/.openeng/git/state/repos.json` | The persisted list of registered repository paths. |

| Setting | Value |
| --- | --- |
| Loopback bind | `127.0.0.1:50053` (fixed, zero-config) |
| Browser endpoint | `https://engine.openeng.app:50053` |
| UI origin | `git.openeng.app` |
| Protocol version | `openeng.git.v1` (1.0) |
| Sibling engines | Terminal 50052 · Kubernetes 50054 · Chat 50056 · Data 50057 (the AI engine binds no local port) |

## What is not yet exposed

In the interest of an honest reference, here is what the Git app does *not* do today:

- **No file-content or tree-read RPC.** There is no way to browse a repository's file tree or read a blob at an arbitrary ref. Content is only ever surfaced through diffs (which read the working tree, index, or a commit-vs-parent) and blame (which reads the working-tree file). You cannot open an arbitrary file at HEAD or an old commit.
- **No pull/merge requests or insights.** The **Requests** and **Insights** sections are honest "Not connected" placeholders — the local engine has no pull/merge requests or analytics to report. **CI & CD** is no longer a placeholder: it reads pipeline runs from GitHub, GitLab, Bitbucket, and Gitea straight from your browser, and the **Connect** settings category now manages your git servers (see [CI & CD](#cicd) and [Settings](#git-settings)). Azure DevOps runs are not wired up yet, and the Overview's forge figures (**Open requests**, **Pipelines today**, and the health table's CI column) still show `0` or `—`.
- **No conflict-resolution tooling.** `MergeBranch` reports a `conflicts` flag, but there is no UI to resolve conflicts.
- **UI gaps over an already-capable engine.** The engine and typed client fully implement stash, tags, blame, cross-history search, branch delete/merge, fetch, and commit amend/signoff/GPG — but the workbench does not yet render panels or controls for them. They are implemented in the engine but not exposed in the app yet.
- **No in-app credential management for git itself.** Remote authentication relies entirely on your local git credential helper and SSH agent; the engine neither stores nor transmits tokens or keys. The only credential the app keeps is a forge API token you add for CI & CD, and it stays in your browser.

> [!TIP] Design intent
> These are surfacing gaps, not architectural ones. The engine's job is to be a complete, local, sealed git backend; the UI exposes that surface progressively. Because the transport carries no secrets and every operation is local, new panels can be added without changing the security model.
