1a2ad98c33
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>
410 lines
22 KiB
Markdown
410 lines
22 KiB
Markdown
# 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 non‑negotiable
|
||
> rules, and the self‑documenting 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, long‑lived development application: a **multi‑repo 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 — per‑repo
|
||
status (clean/dirty, ahead/behind, current branch), branches, remotes, recent
|
||
commits, stashes, and tags — plus user‑initiated Git actions (fetch, pull, push,
|
||
checkout, create branch, commit, view diffs, manage remotes). It has per‑user
|
||
dockable UI panels and is architected so that host/forge integrations
|
||
(GitHub/GitLab pull‑ and merge‑requests) 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
|
||
self‑documenting workflow in Section 9 intact.
|
||
|
||
**Trust model:** there is **no user authentication**. The app is a single‑operator
|
||
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. Non‑negotiable 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 (ActiveX‑spirit, enforced)
|
||
|
||
The UI is built **exclusively** from native Web Components. The design intent is
|
||
deliberately modeled on the *self‑contained control* philosophy of Windows‑98‑era
|
||
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.
|
||
- **Self‑instantiating**: the page declares the element in markup (or via a thin
|
||
loader). The browser "constructs" it. The page does not micro‑manage 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.
|
||
- **Self‑contained**: no shared mutable global state. Cross‑component
|
||
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.
|
||
- **Lifecycle‑correct**: implement `connectedCallback` / `disconnectedCallback`
|
||
and clean up timers, listeners, and in‑flight 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, dependency‑free helpers like
|
||
`lit` *may* be proposed, but only with explicit approval, because `lit` still
|
||
produces standards‑based custom elements.)
|
||
|
||
**Shared styling — design tokens.** The one sanctioned cross‑shadow‑boundary
|
||
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 Git‑state 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 & per‑component docs
|
||
|
||
- There is a **top‑level `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. Component‑specific 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,
|
||
read‑optimized in‑memory 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, user‑initiated 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 read‑only.** 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 per‑user UI state (browser `localStorage`, Section 4). Adding any other
|
||
datastore (SQLite, a server‑side 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 / docker‑compose
|
||
|
||
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**
|
||
(read‑write, 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 host‑side
|
||
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 server‑rendered 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. Pure‑Go `go-git` may be proposed for cheap read‑only queries, but only behind the same interface and only with approval. |
|
||
| Repo discovery / index | **In‑memory 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) | **Provider‑abstracted** (`internal/forge`) — GitHub via `github.com/google/go-github`, GitLab via `gitlab.com/gitlab-org/api/client-go` | Optional, read‑only by default, enabled per‑host 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 top‑level
|
||
> directory. Keep `components/` strictly for web components. There is deliberately
|
||
> **no `migrations/` and no `db/`** — see Section 1.3.
|
||
|
||
---
|
||
|
||
## 4. UI layout & per‑user state (no server DB)
|
||
|
||
Because there is no authentication and no database, **per‑user 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 first‑load
|
||
defaults (mirror the pattern: view state only — never touch the repos).
|
||
|
||
Server‑side there is no session and no user record. Requests act as the single
|
||
host operator.
|
||
|
||
---
|
||
|
||
## 5. Repo discovery & the refresh scanner (read‑only)
|
||
|
||
**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
|
||
in‑memory index keyed by absolute repo path.
|
||
- **Refresh (read‑only):** for each repo compute status (dirty/clean, staged vs
|
||
unstaged counts), current branch, ahead/behind vs upstream, remotes, and a short
|
||
recent‑commit summary — all via read‑only 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, read‑only to
|
||
the working tree) to keep ahead/behind counts current. Rate‑limit 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 (per‑repo 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,
|
||
self‑fetching, 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 server‑produced 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).
|
||
- Cross‑component 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 self‑fetch; 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 non‑dev, 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.
|
||
- **Read‑only 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, explicitly‑requested** capability — do not build write
|
||
paths without asking, and route them through Section 1.4 if destructive.
|
||
- **Enablement is per‑host and token‑gated.** 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. Self‑documenting 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 Per‑component `<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 User‑facing 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 user‑facing 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 task‑oriented (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
|
||
- Component‑specific detail lives in the component's `.md`, **not here**. Once a
|
||
component has its own `.md`, **move any component‑specific detail out of
|
||
AGENT.md** into that file. AGENT.md stays architecture‑level only.
|
||
- If a change alters an **architectural law or stack choice** in this file,
|
||
update AGENT.md too — but only architecture‑level 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 top‑level folder**, propose it and
|
||
wait for approval.
|
||
6. Implement, run it in the **docker‑compose 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 user‑facing; 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: read‑only).
|
||
- **Listen address / exposure:** localhost‑only by default. Confirm before binding
|
||
to a non‑local 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 per‑component `.md` files
|
||
carry the detail.*
|