Redefine contract: two-way Claude companion (MCP + handoff + forge writes)

AGENT.md now makes the Claude integration the defining pillar: law 1.7 (one service layer behind GUI + MCP), MCP server over Streamable HTTP at /mcp, SSE app->browser, Gitea-first read+write forge, right-click command vocabulary, and rewritten section 8 (MCP server, activity feed, graceful project handoff, merge & clean up). No code yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 17:11:51 -04:00
parent 80d2e10fec
commit dfa45de40c
3 changed files with 174 additions and 29 deletions
+147 -27
View File
@@ -22,6 +22,23 @@ dockable UI panels and is architected so that host/forge integrations
(GitHub/GitLab pull and mergerequests) can be attached without touching the
core.
**The larger purpose — a twoway companion to Claude.** The dashboard is the
visible half. The app is also built to work *with* Claude (used from Claude
Desktop / Claude Code) in both directions:
- **Claude → app:** the app is an **MCP server** Claude can drive — list/switch
repos, run Git operations, and create/merge/cleanup pull requests — so Claude
can manage repositories on the user's behalf.
- **App → Claude:** when the user does something in the app (switches project,
merges a PR), the app makes that known so Claude stays in sync. The headline
case is a **graceful project handoff**: the user switches project in the app,
Claude finishes to a safe stopping point, switches, and the user is notified
(Section 8).
The intended user is a developer who does **not** want to memorize Git commands:
the GUI offers plainlanguage, rightclick commands (Section 6), and Claude can
run the same operations through the MCP surface. This is why the app exists — a
plain dashboard is only step one.
This is a **dev environment first**. It is expected to grow continuously via
incremental requests ("add X functionality"). Every addition must keep the
selfdocumenting workflow in Section 9 intact.
@@ -143,6 +160,18 @@ The app must be runnable by a new developer with: clone → copy `.env.example`
`.env` → set the repo roots → `docker compose up`. Document any hostside
prerequisites (SSH agent, credential helper) in `.env.example` and the help page.
### 1.7 One service layer behind both the GUI and the MCP server
The web UI and the MCP server (Section 8) are **two front doors to the same
capabilities** — never two implementations. Every operation (a Git action, a
forge action, switching the active project) lives once in an internal service
that goes through the `internal/git` and `internal/forge` boundaries; the Echo
HTTP handlers and the MCP tool handlers are **thin adapters** that call it. A
capability added for the GUI is therefore available to Claude, and vice versa,
and safety rules (§1.4) are enforced in the shared layer so neither front door
can bypass them. Do not implement a Git/forge operation directly in an HTTP or
MCP handler.
---
## 2. Technology stack (locked unless told otherwise)
@@ -154,7 +183,9 @@ prerequisites (SSH agent, credential helper) in `.env.example` and the help page
| Page rendering | Go serverrendered HTML shell | Server emits the page + declares web components; components fetch their own data. Use `html/template`. |
| Git access | **System `git` via `os/exec`**, wrapped behind an `internal/git` interface | The system binary is authoritative: it honors the user's credential helpers, SSH keys, hooks, and config exactly. PureGo `go-git` may be proposed for cheap readonly queries, but only behind the same interface and only with approval. |
| Repo discovery / index | **Inmemory cache + `github.com/fsnotify/fsnotify`** (optional) | Scan roots for `.git`, hold an index, refresh on interval and/or on filesystem change. |
| Forge integration (PRs/MRs) | **Providerabstracted** (`internal/forge`) — GitHub via `github.com/google/go-github`, GitLab via `gitlab.com/gitlab-org/api/client-go` | Optional, readonly by default, enabled perhost when a token is configured. See Section 8. |
| Forge integration (PRs/MRs) | **Providerabstracted** (`internal/forge`) — **Gitea/Forgejo first** via `code.gitea.io/sdk/gitea`; GitHub (`github.com/google/go-github`) / GitLab later behind the same interface | **Read/write**, tokengated per host. Writes (merge PR, delete branch) are confirmed per §1.4. See Section 8. The primary host is a selfhosted Gitea (`git.nilles.net`). |
| MCP server | **`github.com/modelcontextprotocol/go-sdk`**, served over **Streamable HTTP** at `/mcp` | Claude Desktop connects as a custom connector. Tools are thin adapters over the shared service layer (§1.7). See Section 8. |
| Realtime (app → browser) | **ServerSent Events** (`net/http`, stdlib) | Push activity + handoff notifications and live repo updates to the components; replaces list polling over time. |
| Diff rendering | Server produces unified diff from `git`; client renders it in a component | No heavy client diff lib without asking. |
| Config | **`.env`** via `github.com/joho/godotenv` + a typed config struct | Section 1.5. |
| Logging | **`log/slog`** → stdout/stderr (structured), optional rotating file sink | See Section 7. No database sink (there is no database). |
@@ -183,7 +214,10 @@ Section 0).
│ ├── config/ # .env loading, typed config struct
│ ├── git/ # THE Git boundary: interface + os/exec impl (all git ops)
│ ├── repos/ # discovery, in-memory index/cache, refresh scanner worker
│ ├── forge/ # OPTIONAL seam: GitHub/GitLab PR/MR + remote metadata (provider-abstracted)
│ ├── service/ # the ONE service layer both the HTTP API and MCP call (§1.7)
│ ├── forge/ # provider-abstracted PR/MR read+write (Gitea first) — Section 8
│ ├── mcp/ # MCP server: tool handlers (thin adapters over service) — Section 8
│ ├── activity/ # active-project state, activity feed, pending-switch handoff — Section 8
│ ├── logging/ # slog handler → stdout/stderr (+ optional file)
│ └── render/ # html/template page shell rendering
├── components/ # ALL web components live here (Section 1.2)
@@ -266,6 +300,13 @@ selffetching, independent lifecycle, cleanup on disconnect). Expected surface
- A **repo detail panel** for the selected repo: branches, remotes, recent commit
log, stashes, tags.
- **Diff / commit views** rendered from serverproduced unified diffs.
- A **rightclick context menu** on any item (repo, branch, PR, stash) is the
primary way commands are run. This is the app's reason for being (Section 0):
commands read in **plain language** for people who don't memorize Git — e.g.
"Get latest" (pull), "Save my work" (commit), "Publish" (push), "Merge &
clean up" (merge PR + delete branch). Keep a friendlyname → Git/forgeop
vocabulary; the same operations are exposed to Claude as MCP tools (Section 8),
both calling the one service layer (§1.7).
- An **action surface** for Git operations. Safe operations (fetch, pull,
checkout, create branch, stage, commit, push) can proceed on a normal click;
**destructive operations follow Section 1.4** (explicit control + confirmation
@@ -274,8 +315,8 @@ selffetching, independent lifecycle, cleanup on disconnect). Expected surface
`repo:select` event the detail panel listens for) — never shared globals.
Server endpoints return JSON for the components to selffetch; mutating endpoints
route through `internal/git` and trigger an index refresh for the affected repo so
the UI reflects reality without a full rescan.
route through the service layer (§1.7) and trigger an index refresh for the
affected repo so the UI reflects reality without a full rescan.
---
@@ -292,25 +333,84 @@ the UI reflects reality without a full rescan.
---
## 8. Forge integration — OPTIONAL seam (PRs / MRs)
## 8. Claude integration (MCP server · activity feed · graceful handoff · forge)
Viewing pull/merge requests requires talking to a hosting provider, which is
**outside** the "Git is the store" core. Keep it isolated and optional.
This is the app's defining pillar (Section 0): GitManager works *with* Claude in
both directions. Everything here goes through the one service layer (§1.7) and
obeys the safety rules (§1.4).
- Live in `internal/forge` behind a **provider interface** so GitHub, GitLab, and
others can drop in without touching the dashboard or the Git boundary.
- **Readonly by default**: list open PRs/MRs and their CI/check status for a repo
whose remote points at a supported host. Any write action (comment, merge,
approve) is a **separate, explicitlyrequested** capability — do not build write
paths without asking, and route them through Section 1.4 if destructive.
### 8.1 MCP server — Claude drives the app (`internal/mcp`)
- The app serves an **MCP endpoint over Streamable HTTP at `/mcp`** using
`github.com/modelcontextprotocol/go-sdk`. Claude Desktop connects to it as a
**custom connector**. Like the rest of the app it is **localhostbound and
unauthenticated** (Section 0) — do not expose it offhost without asking.
- MCP **tools are thin adapters** over the service layer — no Git/forge logic in
the tool handlers (§1.7). Expected tools (grow as features land):
- Read: `list_repos`, `get_repo`, `get_active_project`, `get_activity`,
`get_pending_switch`, `list_prs`.
- Act: `git_status/checkout/commit/push/pull/create_branch`,
`create_pr`, `merge_and_cleanup_pr`, `set_active_project`, `ack_switch`.
- **Destructive tools carry the §1.4 contract into MCP:** they describe exactly
what they will do and default to the safe variant. The confirmation is the
human's — surfaced through Claude and/or the app UI — not something the tool
silently assumes.
### 8.2 Activity feed & active project (`internal/activity`)
- The app keeps, in memory (and mirrored to the logs — no new datastore, §1.3):
- the **active project** (the single repo/task currently in focus), and
- an **activity feed** of what happened (user *and* Claude actions: repo
switched, committed, PR merged, …).
- Both are **queryable** (`get_active_project`, `get_activity`) so Claude can
**sync on any turn boundary** — the reliable, pullbased foundation. The app
also **pushes** these to the browser over SSE for live UI. This pullfirst
design does not depend on the host letting a connector wake Claude.
### 8.3 Graceful project handoff (the headline flow)
When the user switches project/task in the app, it is a **request**, not an
instant yank. The cooperative protocol:
1. **User** picks a new project/task in the app → the app records a
**pendingswitch request** (target + optional note) and the UI shows
"waiting for Claude to reach a good stopping point."
2. **Claude** sees the pending request (it checks at its natural turnboundary
checkpoints via `get_pending_switch`). It **finishes to a safe stopping
point and preserves work** — never abandons uncommitted changes to switch;
it completes the inflight step and commits/stashes as appropriate — then
performs the switch (`set_active_project`, moving its working context to the
new repo) and calls **`ack_switch`** with a short summary of where it left
the previous project.
3. **App** marks the request fulfilled and **notifies the user** over SSE
("Claude switched to *ProjectB*; *ProjectA* left at: …"). The user proceeds.
**Rule:** the switch is Claudecompleted at a checkpoint, not appforced. Losing
or interrupting uncommitted work to satisfy a switch is a §1.4class violation.
> Fully autonomous "Claude starts working the instant you click, with no turn
> from you" is intentionally **not** assumed — it depends on host push support.
> Build 8.28.3 pullfirst; layer any autowake on top only where the host allows.
### 8.4 Forge integration — read **and write** (`internal/forge`)
Talking to the hosting provider is isolated behind a **provider interface**
(**Gitea/Forgejo first** — the primary host is `git.nilles.net`; GitHub/GitLab
later behind the same interface).
- **Read:** list open PRs/MRs and their CI/check status for a repo whose remote
points at a supported host.
- **Write (enabled):** create a PR, **merge a PR, and delete the source branch**
— this powers "**Merge & clean up**", the feature that makes PRs usable for a
user who otherwise finds them clutter (Section 0). Every write is **confirmed
per §1.4**, names the PR/branch, and prefers the tidy default (squashmerge +
delete branch). Note a merged PR remains in the host's history; "clean up"
means removing the **branch**, not falsifying history.
- **Enablement is perhost and tokengated.** Tokens come from `.env`
(e.g. `GITHUB_TOKEN`, `GITLAB_TOKEN`); with no token, the feature is simply
absent and the rest of the app works unchanged (**graceful degradation** — never
a hard dependency).
- The provider is inferred from a repo's remote URL. Never send repo data to a host
the user didn't configure.
Design the interface cleanly now; implement providers incrementally as requested.
(e.g. `GITEA_TOKEN`); with no token the forge features are simply absent and
the rest of the app works unchanged (**graceful degradation**).
- The provider is inferred from a repo's remote URL. Never send repo data to a
host the user didn't configure.
---
@@ -373,9 +473,12 @@ component carries its own context.
2. **Check the rules** in Section 1. If the request conflicts, **pause and ask.**
3. If the work is UI: it is a **web component** in `components/<name>/` with its
own `.md`. No exceptions without asking.
4. If it touches repositories: go through the **`internal/git` boundary**, respect
**Git = system of record** (Section 1.3), and treat **destructive operations**
per Section 1.4. Never add a datastore.
4. If it touches repositories or forges: put the logic in the **service layer**
(§1.7) over the `internal/git` / `internal/forge` boundaries — never in an HTTP
or MCP handler — so both the GUI and Claude get it. Respect **Git = system of
record** (§1.3), treat **destructive operations** per §1.4, and never add a
datastore. A new capability generally means: service method → HTTP handler →
MCP tool → UI control.
5. If it needs a **new dependency** or a **new toplevel folder**, propose it and
wait for approval.
6. Implement, run it in the **dockercompose dev environment**, verify hot reload
@@ -396,10 +499,27 @@ silently guess.)*
it should be enabled, and the interval / rate limit, before turning it on.
- **Git access library:** system `git` via `os/exec` is the locked default
(Section 2). Confirm before introducing `go-git` for any read path.
- **Forge providers:** which to support first (GitHub? GitLab?), and whether any
**write** actions (merge/comment/approve) are ever in scope (default: readonly).
- **Listen address / exposure:** localhostonly by default. Confirm before binding
to a nonlocal interface — there is no auth (Section 0).
- ✅ **RESOLVED 2026-09-19:** **Forge = Gitea/Forgejo first** (`git.nilles.net`),
**read + write** — merge PR + delete branch ("Merge & clean up"), each confirmed
per §1.4 (Section 8.4). GitHub/GitLab later behind the same interface.
- ✅ **RESOLVED 2026-09-19:** **MCP transport = Streamable HTTP at `/mcp`**, added
in Claude Desktop as a custom connector (Section 8.1).
- ✅ **RESOLVED 2026-09-19:** **Project handoff is cooperative** — user requests a
switch, Claude finishes to a safe checkpoint, switches, and the user is notified;
pullfirst, not autonomous (Section 8.3).
- **MCP connector setup:** confirm this Claude Desktop build supports adding a
local StreamableHTTP custom connector (`http://127.0.0.1:8080/mcp`); if not,
fall back to a stdio shim.
- **Idletrigger for handoff:** the pull model syncs at Claude's turn boundaries.
If Claude is idle when the user switches, decide the nudge (user's next message,
a heartbeat/poll, or a host push if available) — do not assume instant wake.
- **Coordinationstate lifetime:** active project / pendingswitch / activity feed
are inmemory today (§1.3). Confirm if any must survive an app restart before
adding any persistence.
- **Gitea token scope:** which token scopes to require (repo read + PR write +
branch delete) and how it is provisioned; document in `.env.example`.
- **Listen address / exposure:** localhostonly by default (covers `/mcp` too).
Confirm before binding to a nonlocal interface — there is no auth (Section 0).
- **Credential path from the container:** SSH agent socket vs mounted keys vs
credential helper, for pushing/fetching from inside Docker.