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>
22 KiB
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 (
dispatchEventof aCustomEventthat 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/disconnectedCallbackand 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/gitboundary. 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 (browserlocalStorage, 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/restorethat 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-leaseover--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 nomigrations/and nodb/— 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 inlocalStorageand 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 whenSCAN_FETCH_ENABLED=truemay the scanner rungit 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
CustomEvents (e.g. arepo:selectevent 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/slogas 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_FILEis 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/forgebehind 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>.mdwith:# <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
.mdfirst (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 theCHANGELOG. 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")
- Read this AGENT.md, then any relevant component
.mdfiles. - Check the rules in Section 1. If the request conflicts, pause and ask.
- If the work is UI: it is a web component in
components/<name>/with its own.md. No exceptions without asking. - If it touches repositories: go through the
internal/gitboundary, respect Git = system of record (Section 1.3), and treat destructive operations per Section 1.4. Never add a datastore. - If it needs a new dependency or a new top‑level folder, propose it and wait for approval.
- Implement, run it in the docker‑compose dev environment, verify hot reload and that it operates correctly against a real mounted repo.
- Document: append to
CHANGELOG.md; create/append the component.md; updatehelp.htmlif 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
gitviaos/execis the locked default (Section 2). Confirm before introducinggo-gitfor 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.