From 0cd465fb68791bab84570f5b731b06331348e3cc Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Wed, 6 May 2026 21:51:39 +0200 Subject: [PATCH] docs: add GitOps agent tools design spec 9 new tools to enable full autonomous GitOps loop: repo_status, branch_list/delete/protection_get, pr_list/merge, dir_list, file_delete, tag_create. Co-Authored-By: Claude Sonnet 4.6 --- .../2026-05-06-gitops-agent-tools-design.md | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-06-gitops-agent-tools-design.md diff --git a/docs/superpowers/specs/2026-05-06-gitops-agent-tools-design.md b/docs/superpowers/specs/2026-05-06-gitops-agent-tools-design.md new file mode 100644 index 0000000..ad4a6f0 --- /dev/null +++ b/docs/superpowers/specs/2026-05-06-gitops-agent-tools-design.md @@ -0,0 +1,169 @@ +# GitOps Agent Tools — Design Spec + +**Date:** 2026-05-06 +**Status:** Approved + +## Goal + +Extend the Gitea MCP server with the tools an AI agent needs to drive a full GitOps development loop autonomously — reading repo state, deciding on a branching strategy, making changes, opening and merging PRs, and tagging releases — without any local git tooling. + +The agent selects between feature-branch and trunk-based development based on branch protection rules it reads at runtime. + +--- + +## New Tools (9) + +All tools follow the existing pattern: one file in `internal/tools/`, one Gitea client method in `internal/gitea/`, allowlist check on `owner`, table-driven tests in both packages. + +### `repo_status` + +Convenience read tool — returns branch list, open PRs, and protection info for a target branch in a single call. Designed for the agent's first query on any repo so it can decide its strategy. + +**Inputs:** `owner`, `name`, `branch` (optional — defaults to repo default branch) +**Output:** `{ branches: [...], open_prs: [...], protection: { protected, required_approvals, push_whitelist, merge_whitelist } }` +**Implementation:** calls `ListBranches` + `ListPullRequests(state=open)` + `GetBranchProtection` internally, composes result. No new Gitea API surface. + +--- + +### `branch_list` + +**Inputs:** `owner`, `name`, `page` (optional), `limit` (optional, default 30) +**Output:** array of `{ name, sha }` +**Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/branches` + +--- + +### `branch_delete` + +**Inputs:** `owner`, `name`, `branch` +**Output:** confirmation message +**Gitea endpoint:** `DELETE /api/v1/repos/{owner}/{repo}/branches/{branch}` +**Error handling:** 403 from Gitea (protected branch) surfaced as a descriptive error. + +--- + +### `branch_protection_get` + +**Inputs:** `owner`, `name`, `branch` +**Output:** `{ protected, required_approvals, push_whitelist, merge_whitelist }` +**Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/branch_protections/{branch}` +**Error handling:** 404 → return `{ protected: false }`, not an error. Allows agent to make clean boolean decisions. + +--- + +### `pr_list` + +**Inputs:** `owner`, `name`, `state` (`open`/`closed`/`all`, default `open`), `head` (optional branch filter), `page`, `limit` +**Output:** array of `{ number, title, state, head_branch, base_branch, draft, html_url }` +**Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/pulls` + +--- + +### `pr_merge` + +**Inputs:** `owner`, `name`, `index`, `style` (`merge`/`squash`/`rebase`, default `merge`), `merge_message_title` (optional), `merge_message_field` (optional) +**Output:** `{ merged: true, commit_sha }` — if Gitea returns 204 No Content (some merge styles), output is `{ merged: true }` without `commit_sha`. +**Gitea endpoint:** `POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge` +**Error handling:** 405 (checks failing) and 409 (merge conflict) passed through with the Gitea error message intact so the agent understands why it failed. + +--- + +### `dir_list` + +**Inputs:** `owner`, `name`, `path` (empty string = repo root), `ref` (optional branch/tag/SHA) +**Output:** array of `{ name, path, type (file|dir|symlink), sha, size }` +**Gitea endpoint:** `GET /api/v1/repos/{owner}/{repo}/contents/{path}` +**Note:** same endpoint as `file_read` but returns an array when `path` is a directory. Client detects response shape (array vs object). If called on a file path, returns a descriptive error: `"path is a file, not a directory — use file_read"`. + +--- + +### `file_delete` + +**Inputs:** `owner`, `name`, `path`, `branch`, `message`, `sha` (required — current blob SHA) +**Output:** `{ commit_sha, html_url }` +**Gitea endpoint:** `DELETE /api/v1/repos/{owner}/{repo}/contents/{path}` + +--- + +### `tag_create` + +**Inputs:** `owner`, `name`, `tag` (tag name), `target` (branch name or commit SHA), `message` (optional — creates annotated tag if set) +**Output:** `{ tag, commit_sha, html_url }` +**Gitea endpoint:** `POST /api/v1/repos/{owner}/{repo}/tags` + +--- + +## Gitea Client Methods + +New methods on `gitea.Client`: + +| Method | Endpoint | HTTP verb | +|--------|----------|-----------| +| `ListBranches(ctx, owner, repo, page, limit)` | `/api/v1/repos/{owner}/{repo}/branches` | GET | +| `DeleteBranch(ctx, owner, repo, branch)` | `/api/v1/repos/{owner}/{repo}/branches/{branch}` | DELETE | +| `GetBranchProtection(ctx, owner, repo, branch)` | `/api/v1/repos/{owner}/{repo}/branch_protections/{branch}` | GET | +| `ListPullRequests(ctx, owner, repo, state, head, page, limit)` | `/api/v1/repos/{owner}/{repo}/pulls` | GET | +| `MergePullRequest(ctx, owner, repo, index, args)` | `/api/v1/repos/{owner}/{repo}/pulls/{index}/merge` | POST | +| `ListContents(ctx, owner, repo, path, ref)` | `/api/v1/repos/{owner}/{repo}/contents/{path}` | GET | +| `DeleteFile(ctx, owner, repo, path, args)` | `/api/v1/repos/{owner}/{repo}/contents/{path}` | DELETE | +| `CreateTag(ctx, owner, repo, args)` | `/api/v1/repos/{owner}/{repo}/tags` | POST | + +--- + +## Architecture + +No structural changes. Each new tool is: +- One file: `internal/tools/.go` + `internal/tools/_test.go` +- One client method: `internal/gitea/.go` (added to existing domain files where logical) +- Registered in `cmd/gitea-mcp/main.go` + +`repo_status` is the only tool with internal composition — it calls three client methods and merges their results. It has no dedicated client method of its own. + +New client methods go in existing domain files: +- Branch methods → `internal/gitea/files.go` (already has `BranchExists`, `CreateBranch`) +- PR methods → `internal/gitea/pulls.go` +- Contents (dir_list, file_delete) → `internal/gitea/files.go` +- Tags → new `internal/gitea/tags.go` + +--- + +## Testing + +Pattern: table-driven tests with a `httptest.NewServer` mock, same as `file_write_branch_test.go`. + +Each tool covers: +- Happy path +- 404 response +- Allowlist rejection +- Tool-specific edge cases: + - `branch_delete`: 403 protected branch + - `branch_protection_get`: 404 → `{protected: false}` not error + - `dir_list`: file path → descriptive error + - `pr_merge`: 405 checks failing, 409 merge conflict + - `repo_status`: any one sub-call failing propagates the error + +--- + +## Agent Decision Flow (Reference) + +``` +1. repo_status(owner, name) + → if branch.protected && required_approvals > 0: + use feature-branch workflow + → else: + use trunk-based workflow + +Feature-branch workflow: + file_write_branch (auto-creates branch) + → pr_create + → [wait for CI via workflow_run_status] + → pr_merge + → branch_delete + +Trunk-based workflow: + file_write_branch(branch=main) + → [optionally] tag_create + +Post-merge (either): + → [optionally] tag_create to trigger deployment +```