Files
GitManager/CHANGELOG.md
T
TBNilles 32ae17cc9f Config: .env = projects root only; derive each project's forge from git
Reduce .env to just REPOS_HOST_PATH (the projects root); runtime bootstrap moves to compose/defaults. Projects are the subdirectories of the single root — removed the project-directories feature (store table, service methods, /api/config/project-dirs, Settings section). Each project's forge is derived from its git remote (matched to a configured forge, else the bare host) and shown as a pill next to its name (State.Forge via scanner ForgeFor + svc.ForgeDisplay). Removed first-run .env seeding; forges + identity are managed in Settings. Added forge.HostOf. AGENT.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-22 05:52:03 -04:00

328 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
Append-only running history of all changes (AGENT.md §9.1). Newest last.
## 2026-09-19 — Project scaffold
- **What:** Initial runnable skeleton for the GitManager multi-repo dashboard.
Added the Go backend (`cmd/server/main.go` + `internal/{config,logging,git,repos,render}`),
the Echo HTTP server with `/`, `/help`, `/healthz`, and `/api/repos`, a
read-only repo scanner that discovers repositories under `GIT_REPO_ROOTS` and
keeps an in-memory index, the `<repo-list>` web component, shared design tokens
(`web/static/app.css`), page shells (`web/templates/{index,help}.html`), and the
dev tooling: `Dockerfile` (build/dev/runtime stages), `docker-compose.yml`,
`.air.toml`, `.env.example`, `.gitignore`, `go.mod`.
- **Why:** Stand up the architecture defined in AGENT.md so feature work can begin.
- **Affects:** whole repo (foundation); `components/repo-list`.
## 2026-09-19 — Repo detail panel
- **What:** Added the `<repo-detail>` component (right dock) that listens for
`repo:select` and shows a repo's remotes, local branches (current + upstream),
and 20 most recent commits. Backed by a new `GET /api/repo?path=` endpoint
(restricted to indexed repos) and new read-only git readers
(`LocalBranches`, `RecentCommits`, `RemoteDetails`) plus `repos.BuildDetail`
and `Index.Get`. `<repo-list>` now highlights the selected repo; `index.html`
lays the two panels out left/right; help page documents the detail view.
- **Why:** Make the dashboard drill into a single repository (the detail half of
the list+detail default in AGENT.md §4).
- **Affects:** `components/repo-detail`, `components/repo-list`,
`internal/git`, `internal/repos`, `cmd/server`, `web/templates`.
### Notes to confirm (from AGENT.md §11)
- **Go module path** is the placeholder `gitmanager`; change it if this gets a
canonical import path (e.g. a GitHub URL).
- All items in AGENT.md §11 (discovery strategy, background fetch, forge
providers, listen address, container credentials) remain open.
## 2026-09-19 — Redefine the app as a two-way Claude companion (contract only)
- **What:** Updated AGENT.md to make the Claude integration the defining pillar,
no code yet. §0 now states the two-way purpose (Claude↔app) and the
non-expert, GUI-first goal; added law §1.7 (one service layer behind both the
GUI and the MCP server); stack table gained MCP server (Go SDK over Streamable
HTTP at `/mcp`), SSE (app→browser), and flipped forge to Gitea-first read+write;
layout added `internal/{service,mcp,activity}`; §6 added the right-click
plain-language command vocabulary; **§8 rewritten** into "Claude integration"
(8.1 MCP server, 8.2 activity feed + active project, 8.3 graceful project
handoff, 8.4 forge read+write with "Merge & clean up"); §10 step 4 and §11
updated (three decisions resolved, new open items). `.env.example` now documents
`GITEA_TOKEN` (read+write scope).
- **Why:** Thomas described the real vision — the app should act as an extension
of Claude: usable like an MCP by Claude, notifying Claude of in-app actions to
stay in sync, cooperative project handoff when he's interrupted, plain-language
right-click commands for non-experts, and one-click "merge & clean up" so PRs
stop cluttering repos. Decisions locked: cooperative pull-first handoff; Gitea
writes enabled (confirmed per §1.4); MCP over HTTP `/mcp`.
- **Affects:** `AGENT.md`, `.env.example` (architecture/contract only — no code).
## 2026-09-19 — Service layer + MCP server (Claude integration, read tools)
- **What:** Slice 1 — extracted `internal/service`, the one capability layer both
the HTTP API and the MCP server call (§1.7); the `/api/repos` and `/api/repo`
handlers now route through it. Slice 2 — added `internal/mcp`: an MCP server
(`github.com/modelcontextprotocol/go-sdk` v1.8.0) served over Streamable HTTP at
`/mcp`, with read tools `list_repos` and `get_repo` as thin adapters over the
service. Added a round-trip test (`internal/mcp/mcp_test.go`) using a real temp
git repo + the in-memory MCP transport. Verified the HTTP `/mcp` handshake
locally and in Docker.
- **Why:** First step of the two-way Claude integration (AGENT.md §8.1) — prove
Claude can connect to the app over MCP before building deeper features on it.
- **Affects:** `internal/service` (new), `internal/mcp` (new), `cmd/server/main.go`,
`go.mod`/`go.sum`, `.air.toml`.
- **Gotcha:** Docker-on-Windows bind mounts do NOT deliver filesystem events, so
air's watch-based reload silently never fired. Fixed by enabling air polling
(`poll = true`, `poll_interval = 500` in `.air.toml`).
## 2026-09-20 — HTTPS for the MCP connector (local TLS via mkcert)
- **What:** Added an optional HTTPS listener alongside HTTP. New config
`HTTPS_ADDR`, `TLS_CERT_FILE`, `TLS_KEY_FILE`; when set, `cmd/server` starts
`e.StartTLS` on the same Echo app (best-effort — a missing cert logs a warning
and stays HTTP-only). docker-compose publishes `127.0.0.1:8443` and points the
TLS vars at `certs/localhost.pem` (mounted via the existing source mount).
`.gitignore` ignores `/certs/`; `.env.example` documents the mkcert steps.
- **Why:** Claude Desktop's custom MCP connector only accepts `https://` URLs.
Local TLS with an mkcert-trusted cert lets `https://localhost:8443/mcp` work
without exposing the unauthenticated app via a public tunnel (AGENT.md §8.1).
- **Affects:** `internal/config`, `cmd/server/main.go`, `docker-compose.yml`,
`.gitignore`, `.env.example`, `AGENT.md` (§8.1, §11).
- **Host setup (user-run):** the local CA install (`mkcert -install`) is a
security-settings change performed by the user, not the app.
## 2026-09-20 — Connect Claude Desktop via local stdio bridge (mcp-remote)
- **What:** Corrected the Claude Desktop connection method in AGENT.md (§8.1, §11).
The GUI "Add custom connector" flow can NOT reach a localhost server — it probes
and calls tools from Anthropic's cloud, so `https://127.0.0.1:8443/mcp` fails
"couldn't reach the server" even though a local browser reaches it. The working
path is a local stdio bridge in `claude_desktop_config.json`:
`mcpServers.gitmanager = cmd /c npx -y mcp-remote http://127.0.0.1:8080/mcp`.
Added that entry to the user's Claude Desktop config (backup saved alongside).
- **Why:** Keep the app localhost-only + unauthenticated (§0) while still letting
Claude Desktop drive it. `mcp-remote` runs locally, so it reaches the local
endpoint directly — no public exposure, no HTTPS needed for this path.
- **Affects:** `AGENT.md` (§8.1, §11); user's `claude_desktop_config.json` (outside
the repo). HTTPS/`:8443` from the prior entry stays available but is now optional.
## 2026-09-20 — Slice 3: activity feed + active project (§8.2)
- **What:** Added `internal/activity` (thread-safe active project + bounded event
feed with subscriber fan-out, mirrored to logs, no datastore). Service gained
`ActiveProject`/`SetActiveProject`/`RecordActivity`/`Activity`/`SubscribeActivity`
(and `service.New` now takes the feed). New MCP tools `get_active_project`,
`set_active_project`, `get_activity` (object-wrapped outputs). New HTTP:
`GET/POST /api/active-project`, `GET /api/activity`, and `GET /events` (SSE).
New `<activity-feed>` component (live via EventSource); `<repo-list>` now sets
the active project on selection (a user action). Extended the MCP test to cover
the new tools; help page documents the feature.
- **Why:** The coordination foundation for the graceful project handoff (§8.3):
the app and Claude share one active-project + activity view. User actions are
recorded as `actor:user`, Claude's as `actor:claude`, so each side can see what
the other did.
- **Affects:** `internal/activity` (new), `internal/service`, `internal/mcp`
(+test), `cmd/server/main.go`, `components/activity-feed` (new),
`components/repo-list`, `web/templates/{index,help}.html`.
## 2026-09-20 — Slice 4: graceful project handoff (§8.3)
- **What:** `internal/activity` gained a pending-switch model
(`RequestSwitch`/`PendingSwitch`/`AckSwitch`/`CancelSwitch`); `AckSwitch`
atomically sets the active project to the requested target and records a
`switch-completed` event with Claude's summary. Service methods added. New MCP
tools `get_pending_switch` and `ack_switch` (request is user-only — no MCP tool
raises it). HTTP: `GET/POST/DELETE /api/switch`. New `<handoff-bar>` component:
"Ask Claude to switch to <active>", the "waiting for a good stopping point"
state with Cancel, and the completion notice; `<activity-feed>` also updates the
active project on `switch-completed`. Extended the MCP test to cover the full
request→ack→clear flow. Help page + AGENT.md §8.3 updated.
- **Why:** The headline feature — the user asks Claude to switch projects; Claude
finishes to a safe stopping point, then `ack_switch` completes it and the app
notifies the user over SSE. The switch is Claude-completed at a checkpoint,
never app-forced (§1.4-class rule).
- **Affects:** `internal/activity`, `internal/service`, `internal/mcp` (+test),
`cmd/server/main.go`, `components/handoff-bar` (new), `components/activity-feed`,
`web/templates/{index,help}.html`, `AGENT.md` (§8.3).
- **Verified live:** request → "waiting" → cancel, all over SSE with activity
logging. The ack/completion path is covered by the test; its live ✅ notice
needs the two new MCP tools, which appear after the next Claude Desktop restart.
## 2026-09-20 — Slice 5: Gitea forge — PRs + "Merge & clean up" (§8.4)
- **What:** New `internal/forge` — provider-abstracted forge boundary with a Gitea
impl (`code.gitea.io/sdk/gitea`), a remote-URL parser (`ParseRemote`, tested),
and read+write ops: `ListPullRequests` and `MergeAndCleanup` (squash-merge +
delete the head branch, only when head/base share a repo). Service resolves a
repo → owner/repo via its remotes (prefers origin) and records a `pr-merged`
activity event. New config `GITEA_URL` + `GITEA_TOKEN` (forge is nil/disabled
without both). New MCP tools `list_prs` and `merge_and_cleanup_pr` (the merge
tool's description tells Claude to confirm first, §1.4). HTTP
`GET /api/repo/prs`, `POST /api/repo/pr/merge`. New `<pr-list>` component with a
confirming "Merge & clean up" button; hidden when no forge is configured.
- **Why:** The feature Thomas asked for — make PRs usable by merging and removing
the branch in one tidy step, from the app or via Claude.
- **Affects:** `internal/forge` (new, +test), `internal/config`,
`internal/service`, `internal/mcp`, `cmd/server/main.go`,
`components/pr-list` (new), `web/templates/{index,help}.html`, `.env.example`,
`go.mod`.
- **Not yet live-tested:** needs `GITEA_URL`+`GITEA_TOKEN` set and a real PR;
build/vet/tests pass and the parser is unit-tested. A real merge is irreversible
— will only run one against a PR Thomas designates, with confirmation.
## 2026-09-20 — Forge live-tested (Merge & clean up)
- **What:** With `GITEA_URL`+`GITEA_TOKEN` set, verified end-to-end against
git.nilles.net: created an isolated throwaway PR via the Gitea API (on a
dedicated base branch so `main` was untouched), listed it through `GET
/api/repo/prs`, then ran `POST /api/repo/pr/merge``{merged:true,
branchDeleted:true}`; confirmed the branch was gone (404), the PR list emptied,
and the feed logged `pr-merged`. Cleaned up the base branch afterward.
- **Why:** Prove the write path with real auth before relying on it.
- **Affects:** none (runtime verification only; no code change).
## 2026-09-20 — Slice 6: right-click command menu + git write actions (§6)
- **What:** git boundary gained `Pull`/`Push`/`Commit`/`DiscardAll` (the last is
§1.4-destructive). Scanner got `RefreshRepo` (single-repo re-scan). Service
gained `GitFetch/GitPull/GitPush/GitCommit/GitDiscard` — each records a `git-*`
activity event (ok/failed) and refreshes the repo after success; `service.New`
takes a refresh hook. New HTTP `POST /api/repo/git {path, op, message?}`. New
`<repo-menu>` overlay (plain-language commands: Get latest, Publish, Check for
updates, Save my work…, Set as active project, Ask Claude to switch here, Copy
path, and the confirmed Discard all changes…); `<repo-list>` emits
`repo:contextmenu` on right-click. Added `internal/service` test covering
commit/discard on a temp repo.
- **Why:** The GUI-first reason the app exists (§0) — run git in plain language
without a terminal. Logic lives in the shared service (§1.7) so the same ops can
be exposed to Claude via MCP next.
- **Affects:** `internal/git`, `internal/repos`, `internal/service` (+test),
`cmd/server/main.go`, `components/repo-menu` (new), `components/repo-list`,
`web/templates/{index,help}.html`.
- **Next:** expose these git ops as MCP tools so Claude can run them too.
## 2026-09-20 — Slice 7: git commands as MCP tools (§1.7 symmetry)
- **What:** Added MCP tools `git_fetch`, `git_pull`, `git_push`, `git_commit`,
and `git_discard_changes` — thin adapters over the existing service methods
(actor=claude), so Claude can run the same commands as the right-click menu.
`git_discard_changes`'s description flags it destructive and tells Claude to
confirm first (§1.4). Extended the MCP test with a `git_commit` round-trip.
Synced AGENT.md §8.1's tool list to the actual names.
- **Why:** Complete the §1.7 symmetry — every capability reachable from both the
GUI and Claude.
- **Affects:** `internal/mcp` (+test), `AGENT.md` (§8.1).
- **Note:** the new tools appear in Claude Desktop only after its next restart
(tool list cached per connection); network ops still need container git creds.
## 2026-09-20 — Slice 8: git credentials + identity in the container (§11)
- **What:** On startup the app configures the container's git (`git config
--global`): a commit identity (`GIT_USER_NAME`/`GIT_USER_EMAIL`),
`safe.directory=*` for host-owned mounts, and — when `GITEA_URL`+`GITEA_TOKEN`
are set — `http.<url>.extraheader: Authorization: token …` so push/fetch/pull
authenticate over HTTPS with no SSH key. New `git.CLI.SetGlobalConfig`; new
config `GIT_USER_NAME`/`GIT_USER_EMAIL`; `.env.example` documents them.
- **Why:** Make the network git commands (menu + MCP) actually work from Docker,
and let commits have an author.
- **Affects:** `internal/config`, `internal/git`, `cmd/server/main.go`,
`.env.example`, `AGENT.md` (§11).
- **Verified:** startup logs "git remote auth configured"; container git identity
set; `http.extraheader` present; `git_fetch` via the app returned ok.
- **Security note:** the token is written to the container's ephemeral gitconfig
and passed in a `git config` argv — acceptable for a localhost dev container.
## 2026-09-20 — Slice 9: git_checkout + create_branch
- **What:** git boundary `Checkout` (switch existing branch) and `CreateBranch`
(git checkout -b). Service `GitCheckout`/`GitCreateBranch` (feed detail names
the branch; `gitAction` now takes an ok-detail). HTTP `/api/repo/git` gained
ops `checkout` and `create-branch` (+`branch` field). MCP tools `git_checkout`
and `create_branch`. `<repo-menu>` gained "Switch branch…" and "New branch…"
(prompt for the name). Service test covers create+switch+existing-branch-fails.
- **Why:** Round out the everyday git commands in both front doors (§1.7).
- **Affects:** `internal/git`, `internal/service` (+test), `internal/mcp`,
`cmd/server/main.go`, `components/repo-menu`, `web/templates/help.html`,
`AGENT.md` (§8.1). Checkout isn't §1.4-destructive — git refuses if it would
overwrite uncommitted changes.
## 2026-09-20 — Slice 10: create_pr
- **What:** forge `CreatePullRequest` (Gitea; empty base → repo default branch,
via GetRepo). Service `CreatePR` (records `pr-created`). HTTP
`POST /api/repo/pr/create`. MCP tool `create_pr`. `<pr-list>` gained a
"New pull request…" button (head = selected repo's current branch, base =
default). AGENT.md §8.1 lists `create_pr` in Act.
- **Why:** Open PRs from the app or Claude — the front half of the PR workflow
whose back half is "Merge & clean up".
- **Affects:** `internal/forge`, `internal/service`, `internal/mcp`,
`cmd/server/main.go`, `components/pr-list`, `web/templates/help.html`,
`AGENT.md`.
- **Note:** the head branch must already exist on the remote (push first).
## 2026-09-20 — Slice 11: branch-picker submenu
- **What:** `<repo-menu>` "Switch branch" is now a flyout submenu populated from
`GET /api/repo` (the repo's branches; current one disabled), flipping leftward
near the viewport edge; clicking a branch checks it out. "New branch…" still
prompts. No backend change.
- **Affects:** `components/repo-menu`, `web/templates/help.html`.
## 2026-09-20 — Slice 12: inline command-result toasts
- **What:** New `<toast-host>` overlay — components post `toast` CustomEvents
(`{message, kind}`; success/error/info) and it shows brief, auto-dismissing,
bottom-right toasts. `<repo-menu>` (git ops + coordination actions) and
`<pr-list>` (create/merge) now post success/error toasts with friendly labels
instead of `alert()`. Activity feed still logs everything.
- **Why:** Immediate, legible feedback for the non-expert audience (§6 polish).
- **Affects:** `components/toast-host` (new), `components/repo-menu`,
`components/pr-list`, `web/templates/index.html`.
## 2026-09-22 — Config store: move forges/dirs/identity from .env to a private DB
- **What:** Domain config now lives in a **private SQLite store** (`internal/store`,
modernc.org/sqlite, pure Go) instead of `.env`: **forge hosts + tokens**
(multihost), the **project directories** to scan, and the **git identity**.
New Settings page (`/settings`) + `<settings-panel>` component + `<toast-host>`
there; `/api/config/{forges,project-dirs,identity}` CRUD endpoints. The scanner
now reads its roots **fresh from the store each cycle** (add/remove dirs without
a restart); the service resolves a repo → forge by matching remotes against the
stored forges (client cached per host) and reapplies perforge git auth
(`http.<url>.extraheader`) on change; git identity comes from the store. On first
run with an empty DB, the store is **seeded from `.env`** (`GITEA_*`,
`GIT_REPO_ROOTS`, `GIT_USER_*`), so existing deploys keep working. The DB lives
on a **private named Docker volume `/data`** — not bindmounted, no port — so
credentials aren't reachable outside the container.
- **Why:** Support multiple repos/forges and project directories with credentials,
managed at runtime, without handediting `.env` (user request). Overturns the
old "no datastore" / "config via .env" laws — AGENT.md §0/§1.3/§1.5/§2/§3/§8.4
updated.
- **Affects:** `internal/store` (new), `internal/config`, `internal/repos`
(scanner now dynamic), `internal/service` (storebacked forges + config CRUD +
`ApplyGitConfig`), `cmd/server/main.go`, `components/settings-panel` (new),
`web/templates/{settings,index,help}.html`, `docker-compose.yml` (private
`gmdata` volume + `GITMANAGER_DB`), `.env.example`, `go.mod`.
- **Verified live:** seeded on first run; forges/dirs/identity served from the DB
(tokens never returned); repos still discovered (now both GitManager and
app-template under /repos); forge PRs work from the stored token; add/validate/
delete of project dirs works; DB is not present in the project directory; the
Settings UI renders and manages all three.
- **Notes:** tokens stored plaintext relying on volume isolation (encryptionat
rest is an open item); scan tuning (interval/depth/ignore/fetch) stays in `.env`
for now; container can only scan paths under a mounted base (mount constraint).
## 2026-09-22 — .env = projects root only; forge derived from git per project
- **What:** Refined the config model. `.env` now holds **only the projects root**
(`REPOS_HOST_PATH`); runtime bootstrap (listen/TLS/DB path/`PROJECTS_ROOT`/scan
tuning) comes from compose + code defaults. The config store keeps **forges +
tokens and git identity** only — the **project-directories** feature was removed
(table, service methods, `/api/config/project-dirs`, and the Settings section):
**projects are simply the subdirectories of the single root.** Each project's
**forge is derived from its git remote** (matched to a configured forge, else the
bare host) and shown as a pill next to its name (`repos.State.Forge` via the
scanner's `ForgeFor` + `svc.ForgeDisplay`). First-run `.env` seeding was removed
(forges/identity are added in Settings). `.env` reduced to one line;
`forge.HostOf` added; scanner config gained `ForgeFor`.
- **Why:** User: "the only thing in .env should be the root directory; each project
has its own directory and shows which forge it belongs to — get it from git."
- **Affects:** `internal/config`, `internal/store` (dropped project_dirs),
`internal/repos` (State.Forge + ForgeFor), `internal/service` (dropped project-dir
methods, added ForgeDisplay), `internal/forge` (HostOf), `cmd/server/main.go`,
`components/{settings-panel,repo-list}`, `web/templates/{help}.html`,
`docker-compose.yml`, `.env`/`.env.example`, `AGENT.md` (§1.3/§1.5/§3/§8.4/§11).
## 2026-09-20 — Slice 13: repo search + filtering
- **What:** `<repo-list>` gained a search box (name/path, case-insensitive) and
"Dirty" / "Ahead/behind" filter chips with a "N of M" count. Filtering is
client-side over the fetched list; state persists per-viewer in
`localStorage["gitmanager.repolist.filters"]` (§4). Refresh/selection re-apply
the active filters.
- **Why:** Keep the dashboard usable as the number of repos grows.
- **Affects:** `components/repo-list`, `web/templates/help.html`.
- **Dev-server tweak:** `cmd/server` now sends `Cache-Control: no-cache` for
`/components` and `/static` so browsers revalidate assets on reload (cached ES
modules were defeating hot reload). Server serves the new component (curl-
verified); live click-through pending (browser pane was unresponsive).