Scaffold GitManager multi-repo dashboard

Runnable skeleton per AGENT.md: Echo server (/, /help, /healthz, /api/repos), read-only repo scanner with in-memory index, the internal/git boundary, the <repo-list> web component with design tokens, and dev tooling (Dockerfile, docker-compose, air, .env.example).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 16:34:43 -04:00
parent c24604fe65
commit 1a2ad98c33
20 changed files with 1527 additions and 0 deletions
+409
View File
@@ -0,0 +1,409 @@
# AGENT.md
> **This file is the contract for Claude Code working on this repository.**
> Read it fully before any task. It defines the architecture, the nonnegotiable
> rules, and the selfdocumenting workflow you must follow on every change.
>
> When this file and the code disagree, this file wins until you are explicitly
> told to change the file. If a request conflicts with a rule here, **pause and
> ask** before proceeding.
---
## 0. What this project is
A standalone, longlived development application: a **multirepo web dashboard for
Git**. It scans one or more configured roots on the host, discovers every Git
repository under them, and presents a control panel over the whole set — perrepo
status (clean/dirty, ahead/behind, current branch), branches, remotes, recent
commits, stashes, and tags — plus userinitiated Git actions (fetch, pull, push,
checkout, create branch, commit, view diffs, manage remotes). It has peruser
dockable UI panels and is architected so that host/forge integrations
(GitHub/GitLab pull and mergerequests) can be attached without touching the
core.
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.
**Trust model:** there is **no user authentication**. The app is a singleoperator
tool that acts as the host user against local repositories. It binds to
**localhost** by default; exposing it to a network is the operator's decision and
their responsibility. Do not add a login/auth layer without asking.
---
## 1. Nonnegotiable architectural laws
These are hard rules. Do not violate them, do not "optimize them away," and do
not assume an exception without asking.
### 1.1 Web Components are the only UI unit (ActiveXspirit, enforced)
The UI is built **exclusively** from native Web Components. The design intent is
deliberately modeled on the *selfcontained control* philosophy of Windows98era
ActiveX/OCX controls — **the spirit, not the dead COM/binary/registry mechanism**.
Concretely, **every** UI element that does real work MUST be:
- A **Custom Element v1** (`customElements.define('x-thing', ...)`).
- Encapsulated with **Shadow DOM** (`attachShadow({mode:'open'})`). Styles and
DOM are isolated. No leaking global CSS into components, no reaching out of a
component into another component's internals.
- **Selfinstantiating**: the page declares the element in markup (or via a thin
loader). The browser "constructs" it. The page does not micromanage it.
- **Independently live**: on connect, each component **fetches its own data and
renders itself**. Components do **not** wait for a central controller to hand
them data. One component being slow or failing must not block another.
- **Selfcontained**: no shared mutable global state. Crosscomponent
communication happens only via (a) DOM custom events (`dispatchEvent` of a
`CustomEvent` that bubbles/composes) or (b) explicit attributes/properties set
on the element. Treat each component like a control you could drop onto any
page and it would just work.
- **Lifecyclecorrect**: implement `connectedCallback` / `disconnectedCallback`
and clean up timers, listeners, and inflight fetches on disconnect.
If you are tempted to introduce a heavy SPA framework (React/Vue/Angular/Svelte)
for the component layer — **stop and ask.** The default answer is no. Vanilla
Web Components are the chosen model. (Small, dependencyfree helpers like
`lit` *may* be proposed, but only with explicit approval, because `lit` still
produces standardsbased custom elements.)
**Shared styling — design tokens.** The one sanctioned crossshadowboundary
styling channel is **CSS custom properties**. The shared theme palette and corner
radii live as tokens (`--color-*`, `--surface-*`, `--border-*`, `--fill-*`,
`--radius*`) in `web/static/app.css :root`; custom properties inherit *into* every
shadow root, so a component references them with `var(--…)` for all shared colors
and radii instead of hardcoding hex. This is **not** the forbidden "leaking global
CSS" — no global *selectors* reach into a component; it is the deliberate theming
layer (change a token once, the whole UI follows). Status feedback is semantic —
destructive/error use the `--color-danger*` set, success uses `--color-success`,
and Gitstate colors (ahead/behind, clean/dirty, conflicted) get their own
semantic tokens. **New or edited components MUST consume the tokens for shared
values** rather than reintroducing hardcoded hex.
### 1.2 Component foldering & percomponent docs
- There is a **toplevel `components/` folder**. All web components live there.
- **Each component gets its own folder**: `components/<component-name>/`.
- **Each component folder contains its own `<component-name>.md`** (see Section 9)
that records the **history and intent** of that component and acts as the
scoped context when working on it. Componentspecific detail belongs in that
file, **not** in this AGENT.md. This keeps AGENT.md lean as the app grows.
### 1.3 Git is the system of record (the app never silently mutates a repo)
- **The Git repositories are the truth.** The app holds only a **derived,
readoptimized inmemory index** of repo state (discovered repos, their status,
branches, remotes, recent log), refreshed by the scanner (Section 5). That index
is a cache — it can be thrown away and rebuilt from the repos at any time.
- **All mutations go through explicit, userinitiated Git operations.** The app
**never** writes to a repository except in direct response to a user action
routed through the `internal/git` boundary. No background process ever mutates a
working tree, index, or ref.
- **The background scanner is readonly.** It may run `status`, `rev-list`,
`for-each-ref`, `log`, and (only when explicitly enabled) `fetch`. It must never
run a command that changes local state.
- **Never invent local persistence for domain state.** If something must survive a
restart and it isn't already in Git, it is either app config (`.env`, Section
1.4) or peruser UI state (browser `localStorage`, Section 4). Adding any other
datastore (SQLite, a serverside DB) requires asking first — the default is no.
### 1.4 Destructive Git operations are explicit, confirmed, and never automatic
Operations that can lose work or rewrite shared history are a distinct class and
must be treated as such:
- **Always require an explicit, deliberate user action** (a dedicated control, not
a side effect of another action) **and a confirmation** that names exactly what
will happen and to which repo/branch.
- This class includes at least: `push --force`/`--force-with-lease`, `reset --hard`,
`checkout`/`restore` that discards uncommitted changes, `clean -fd`, branch/tag
deletion, `stash drop`/`clear`, history rewrites (`rebase`, `commit --amend`),
and remote deletions.
- **Never chain a destructive operation into an automated flow** and never pick a
destructive default. Prefer the safe variant (`--force-with-lease` over
`--force`) and surface it as such.
### 1.5 Configuration via `.env`
Repo scan roots, the `git` binary path, scan/fetch behavior, the listen address,
and any optional forge tokens are all configurable through a single `.env` file. A
committed `.env.example` documents every variable. **Never commit a real `.env`**
and never hardcode paths, tokens, hosts, or the set of watched repositories.
### 1.6 Everything runs in Docker / dockercompose
The dev environment (the Go app + hot reload) is brought up with
`docker compose up`. Because the app operates on repositories that live on the
**host**, the compose file **mounts the configured repo roots into the container**
(readwrite, since Git operations write to them) along with whatever Git needs to
authenticate to remotes (SSH agent socket / mounted keys, or a credential helper).
The app must be runnable by a new developer with: clone → copy `.env.example` to
`.env` → set the repo roots → `docker compose up`. Document any hostside
prerequisites (SSH agent, credential helper) in `.env.example` and the help page.
---
## 2. Technology stack (locked unless told otherwise)
| Concern | Choice | Notes |
|---|---|---|
| Language | **Go** | Backend, page rendering, Git orchestration, scanner. |
| HTTP framework | **Echo** (`github.com/labstack/echo/v4`) | Idiomatic, fast, good middleware story. If you prefer Chi, ask first. |
| 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. |
| 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). |
| Hot reload (dev) | **air** (`github.com/air-verse/air`) | Inside the app container. |
Anything not in this table that you want to add as a dependency: **propose it and
wait for approval.** Keep the dependency surface small. Note in particular there is
**no database and no auth library** — do not add one without asking (Section 1.3,
Section 0).
---
## 3. Repository layout (target)
```
.
├── AGENT.md # this file — lean, architecture-level only
├── CHANGELOG.md # append-only running history of ALL changes (Section 9)
├── .env.example # every config var, documented, no secrets
├── docker-compose.yml # go app (+ hot reload); mounts host repo roots + git creds
├── Dockerfile # multi-stage build for the Go app (git installed in the image)
├── .air.toml # hot reload config (dev)
├── cmd/
│ └── server/main.go # entrypoint: wire config, git, scanner, router
├── internal/
│ ├── 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)
│ ├── logging/ # slog handler → stdout/stderr (+ optional file)
│ └── render/ # html/template page shell rendering
├── components/ # ALL web components live here (Section 1.2)
│ └── <component-name>/
│ ├── <component-name>.md # history + intent for THIS component (scoped context)
│ ├── <component-name>.js # the custom element
│ └── <component-name>.css # (optional) styles consumed inside shadow DOM
└── web/
├── static/ # shared static assets (app.css tokens, etc. — NOT component-specific)
└── templates/ # Go html/template page shells (incl. help.html)
```
> If a new concern doesn't fit cleanly, **ask** before inventing a new toplevel
> directory. Keep `components/` strictly for web components. There is deliberately
> **no `migrations/` and no `db/`** — see Section 1.3.
---
## 4. UI layout & peruser state (no server DB)
Because there is no authentication and no database, **peruser UI state lives in
the browser** (`localStorage`), namespaced under a `gitmanager.*` prefix:
- Dockable panels: the **repo list docked LEFT**, the **repo detail panel docked
RIGHT** by default. Dock options are `top`, `left`, `bottom`, `right`; positions
are remembered in `localStorage` and reapplied on load.
- Other view state (selected repo, active filter/search, expanded sections, chosen
branch view) also persists in `localStorage`.
- A **reset control** clears every `gitmanager.*` key and reloads to firstload
defaults (mirror the pattern: view state only — never touch the repos).
Serverside there is no session and no user record. Requests act as the single
host operator.
---
## 5. Repo discovery & the refresh scanner (readonly)
**Git is truth; the index is a derived cache.** Implement in `internal/repos`,
using the `internal/git` boundary for every command.
- **Discovery:** walk each configured root (`GIT_REPO_ROOTS`) for directories
containing `.git`, honoring a configurable max depth and ignore globs. Build an
inmemory index keyed by absolute repo path.
- **Refresh (readonly):** for each repo compute status (dirty/clean, staged vs
unstaged counts), current branch, ahead/behind vs upstream, remotes, and a short
recentcommit summary — all via readonly Git commands (Section 1.3). Refresh on
a **configurable interval** and, optionally, on filesystem change via `fsnotify`.
- **Optional background `fetch`:** disabled by default. Only when
`SCAN_FETCH_ENABLED=true` may the scanner run `git fetch` (network, readonly to
the working tree) to keep ahead/behind counts current. Ratelimit it.
- **Never block a request on a slow repo.** The dashboard reads from the index;
the scanner updates the index in the background. A single unreachable remote or
huge repo must not stall the others (perrepo timeouts, isolated goroutines).
Resulting data path:
```
Host repositories (system of record)
│ read-only scan (status / rev-list / for-each-ref / log [/ fetch if enabled])
internal/repos ──► in-memory index (per-repo state)
Echo JSON endpoints ──► web components (self-fetch)
User action ──► internal/git (os/exec) ──► repository mutated ──► index refresh
```
---
## 6. Dashboard & Git operations (client side)
The dashboard is composed of web components, each obeying Section 1 (shadow DOM,
selffetching, independent lifecycle, cleanup on disconnect). Expected surface
(create each with its own folder + `.md` as you build it):
- A **repo list / grid** of all discovered repos with status badges (branch,
dirty/clean, ahead/behind), searchable/filterable.
- A **repo detail panel** for the selected repo: branches, remotes, recent commit
log, stashes, tags.
- **Diff / commit views** rendered from serverproduced unified diffs.
- 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
naming the target).
- Crosscomponent communication is via bubbling/composed `CustomEvent`s (e.g. a
`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.
---
## 7. Logging (structured, to stdout/stderr)
- Use Go's **`log/slog`** as the logging API throughout.
- Emit **structured records to stdout/stderr** (JSON in nondev, a readable
console handler in dev). Capture: timestamp, level, message, structured
attributes, request id, and the repo path when an operation targets one.
- **Log every Git mutation** (the command, target repo/branch, and outcome) so the
operator has an audit trail of what the tool did on their behalf.
- Optionally also write to a **rotating file** when `LOG_FILE` is set. There is no
database sink — do not add one (Section 1.3).
---
## 8. Forge integration — OPTIONAL seam (PRs / MRs)
Viewing pull/merge requests requires talking to a hosting provider, which is
**outside** the "Git is the store" core. Keep it isolated and optional.
- 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.
- **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.
---
## 9. Selfdocumenting workflow (MANDATORY on every change)
This is how the project documents itself so AGENT.md stays small and each
component carries its own context.
### 9.1 Root `CHANGELOG.md`
- **On every change you make**, append an entry to root `CHANGELOG.md`. Never
rewrite history; only append. Each entry:
```
## YYYY-MM-DD — <short title>
- **What:** what changed (files, behavior).
- **Why:** the intent / the request behind it.
- **Affects:** components/areas touched.
```
### 9.2 Percomponent `<component-name>.md`
- When you **create a component**, create `components/<name>/<name>.md` with:
```
# <component-name>
## Intent
What this component is for; the ActiveX-spirit contract it fulfills.
## Public surface
Tag name, attributes/properties, emitted events, what data it fetches and from where.
## History
- YYYY-MM-DD: created — <reason>.
- YYYY-MM-DD: <change> — <reason>.
## Notes / gotchas
```
- When you **work on an existing component**, **read its `.md` first** (it is the
scoped context for that component), make the change, then **append to its
History** and update Public surface if it changed.
### 9.3 Userfacing help page
- `web/templates/help.html` (served at `/help`, linked from the app header) explains
**how to use the app** for end users. **When a userfacing feature changes** — a new
control, a changed workflow, a removed/renamed option — **update the help page in the
same change**, the way you update the `CHANGELOG`. Keep it taskoriented (how to do
things), not implementation detail. Document any host prerequisites (SSH agent /
credential helper for pushing from the container).
### 9.4 Keep AGENT.md lean
- Componentspecific detail lives in the component's `.md`, **not here**. Once a
component has its own `.md`, **move any componentspecific detail out of
AGENT.md** into that file. AGENT.md stays architecturelevel only.
- If a change alters an **architectural law or stack choice** in this file,
update AGENT.md too — but only architecturelevel facts belong here.
---
## 10. How to take a new task ("add X functionality")
1. **Read** this AGENT.md, then any relevant component `.md` files.
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.
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
and that it operates correctly against a real mounted repo.
7. **Document:** append to `CHANGELOG.md`; create/append the component `.md`;
update `help.html` if userfacing; trim AGENT.md if component detail crept in.
---
## 11. Open items to confirm before/while building
*(Claude Code: surface these to the human at the first relevant moment; don't
silently guess.)*
- **Repo discovery strategy:** recursive scan of `GIT_REPO_ROOTS` (max depth?
ignore globs?) vs an explicit list of repo paths. Default assumption: recursive
scan with a configurable depth.
- **Background `fetch`:** off by default (no unsolicited network). Confirm whether
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).
- **Credential path from the container:** SSH agent socket vs mounted keys vs
credential helper, for pushing/fetching from inside Docker.
---
*End of AGENT.md. Keep it lean. Let the CHANGELOG and percomponent `.md` files
carry the detail.*