Implements the Phase 0 owner decision on #86 (issuecomment-1354): - docs/llm-agent-sha.md: format llm-<12 lowercase hex> (^llm-[0-9a-f]{12}$), generation rules, per-PR/workstream lifetime, visible markdown metadata blocks, no SHA in branch/worktree names, same-SHA vs same-user vs same-profile distinction. Attribution only — never an eligibility input. - docs/llm-workflow-runbooks.md: attribution subsection + handoff/review runbook pointers. - templates start-issue.md / review-pr.md: handoff and review metadata blocks; reviewer rule that a different SHA is not a different actor. - tests/test_llm_agent_sha.py: negative tests — same Gitea user with a different LLM-Agent-SHA still fails self-review and self-merge; eligibility results are identical with/without/across SHA env values; no gate accepts or reads any agent-SHA input. No launcher/env handling, no gitea_whoami fields, no PR auto-injection, no audit schema changes. No eligibility behavior changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
LLM-Operated Gitea Workflow Runbooks
Purpose
Runbooks for the common Gitea workflows an LLM performs through the gitea-mcp
package of the MCP Control Plane: creating issues, implementing them, opening
and reviewing pull requests, merging, and closing out — safely and
reproducibly.
For the project-agnostic version of these operating rules (issue-first, isolated worktrees, no self-review/merge, profile safety, cleanup, fail-closed) that can be copied into any repository, see the reusable skill
skills/llm-project-workflow/SKILL.mdand itstemplates/. This runbook is the Gitea-specific application of it.
These runbooks are operational guidance only. They add no tooling; the behavior they rely on already exists (canonical runtime profiles, the interactive setup menu, identity/eligibility checks, gated review/merge, and audit logging). See Related documents.
For cross-project use, copy the portable workflow skill at
../skills/llm-project-workflow/SKILL.md.
It extracts the issue-first, isolated-worktree, no-self-review, profile-safety,
merge-cleanup, fail-closed, and recovery rules into a reusable package that can
be adapted to other repositories.
Principle: the profile is the role, not the LLM
The LLM is not the role.
The MCP execution profile used for the task is the role.
An LLM session is never permanently an "author," "reviewer," or "merger." Any
session may perform any of these roles — but only while operating through a
task-appropriate profile whose authenticated Gitea identity and allowed
operations fit the task. A task selects a profile; a profile is not assigned to
a model. See gitea-execution-profiles.md.
Example role-scoped instructions:
Use an author profile to implement issue #N and open a PR.
Use any eligible reviewer profile to review PR #N.
Use any eligible merger profile to merge PR #N if checks pass.
Attribution: LLM-Agent-SHA (metadata only)
Sessions may attribute their work with an opaque LLM-Agent-SHA
(llm-<12 lowercase hex>, e.g. llm-8f3a9c2d6b41) in PR-body and
review-handoff metadata blocks — see
llm-agent-sha.md for the full convention. It is
attribution only: eligibility is decided solely by the authenticated
Gitea user and the profile's allowed operations. Two sessions with different
SHAs under the same Gitea user are the same actor — a different SHA never
permits self-review or self-merge. Keep the SHA out of branch and worktree
names.
Prerequisites: canonical config + thin launchers
Runtime profiles live in one canonical JSON file, referenced by every LLM launcher. No client config contains raw credentials.
Canonical config file
Selected by two environment variables:
GITEA_MCP_CONFIG— path to the canonical file (e.g.~/.config/gitea-tools/profiles.json).GITEA_MCP_PROFILE— the named profile to activate.
Shape (see ../gitea-mcp.example.json):
{
"version": 1,
"profiles": {
"prgs-reviewer": {
"base_url": "https://gitea.example.invalid",
"username": "<reviewer-username>",
"auth": { "type": "keychain", "id": "prgs-reviewer-token" },
"default_owner": "<owner>",
"execution_profile": "gitea-reviewer"
},
"prgs-author": {
"base_url": "https://gitea.example.invalid",
"username": "<author-username>",
"auth": { "type": "env", "name": "GITEA_TOKEN_PRGS_AUTHOR" },
"default_owner": "<owner>",
"execution_profile": "gitea-author"
}
}
}
version— canonical schema version (currently1).profiles— map of profile name → profile.auth— a reference, never an inline secret:- keychain:
{ "type": "keychain", "id": "<service-id>" }— the token is read from the macOS keychain on demand. - env:
{ "type": "env", "name": "<ENV_VAR_NAME>" }— the token is read from that environment variable.
- keychain:
Inline token/password keys are rejected. Token values are never stored in,
returned by, or logged from profile metadata. Precedence: explicit process env
vars override JSON profile values; the JSON profile fills only what the
environment leaves unset. With GITEA_MCP_CONFIG unset, behavior is exactly the
legacy environment-only mode.
Thin launcher pattern
An LLM MCP launcher (Claude / Gemini / Codex) contains only command, args,
and the two GITEA_MCP_* variables — never a token or password:
"gitea-tools": {
"command": "/path/to/Gitea-Tools/venv/bin/python3",
"args": ["/path/to/Gitea-Tools/mcp_server.py"],
"env": {
"GITEA_MCP_CONFIG": "/path/to/.config/gitea-tools/profiles.json",
"GITEA_MCP_PROFILE": "prgs-reviewer"
}
}
Run the same server as several launcher entries (e.g. -author, -reviewer,
-merger), each pointing at a different GITEA_MCP_PROFILE.
Setup runbook — interactive menu
Create and manage profiles without hand-editing JSON:
./scripts/gitea-config-menu
Menu options: list / add / edit / remove profiles · validate config · test profile authentication · show authenticated user · generate launcher snippets (Claude/Gemini/Codex) · check reviewer eligibility for a PR.
In a real terminal the menu takes a single keypress (no Enter), Enter quits the main menu and cancels/back-outs of any submenu, and you pick a profile from a numbered list instead of typing its name. Non-interactive runs (pipes/tests) fall back to line input and never block.
Create an author + a reviewer profile:
add profile→ nameprgs-author, base URL, username, default owner/repo, execution profilegitea-author, auth typekeychainorenv.- keychain: store the token now (hidden prompt); it goes to the keychain
under an id like
prgs-author-token— never into the JSON. - env: record a var name like
GITEA_TOKEN_PRGS_AUTHOR; set that variable yourself in the environment.
- keychain: store the token now (hidden prompt); it goes to the keychain
under an id like
add profileagain → nameprgs-reviewer, execution profilegitea-reviewer. Existing profiles are preserved.validate config→ confirm no problems.generate launcher snippets→ paste the printed snippet into each LLM client's MCP config (it contains no secret).test profile authentication→ prints the resolved Gitea username (the only time an API call is made, and only on request).check reviewer eligibility for a PR→ enter a PR number; prints the authenticated user, the PR author, andELIGIBLE/INELIGIBLE. Read-only — it never approves or merges.
Migration runbook — away from duplicated credential blocks
Old setups duplicated GITEA_USER_*, GITEA_PASS_*, and GITEA_SITE_* across
every LLM's mcp_config.json — duplicating profiles and exposing secrets.
- For each instance/role, create one canonical profile (menu →
add profile), storing the secret in the keychain or an env var and referencing it by id/name only. validate config, thentest profile authenticationfor each profile.- Replace each LLM's server block with the thin launcher (command + args +
GITEA_MCP_CONFIG+GITEA_MCP_PROFILE). - Delete the
GITEA_USER_*/GITEA_PASS_*/GITEA_SITE_*blocks from every LLM config. - Rotate any token that previously sat in a client config.
Legacy environment-only setups keep working unchanged until migrated.
Workflow runbooks
Each runbook names the profile role it runs under, the steps, and a safe
prompt. Confirm the active profile first (gitea_get_profile / gitea_whoami).
Branch worktree isolation
All LLM implementation and review work happens in an isolated branch worktree
under branches/. The main repository checkout is an orchestration checkout:
use it for status checks, issue creation/claiming, and creating worktrees, but
do not edit tracked repository files there.
Issue → branch → worktree → PR → cleanup. Every implementation branch is tied to an issue number so the work is traceable end to end:
| Stage | Form |
|---|---|
| Issue | #123 (claimed with status:in-progress) |
| Branch | (fix|feat|docs|chore)/issue-123-<slug> (review: review/pr-456-<slug>) |
| Worktree | branches/fix-issue-123-<slug> (slashes → hyphens) |
| PR | body says Closes #123 (closes) or Refs #123 (related) |
| Cleanup | remove remote+local branch + worktree folder; drop status:in-progress |
scripts/worktree-start rejects implementation branches that are not
issue-linked (use --allow-unlinked only for genuine exceptions). When claiming,
post a comment like
Claimed. Branch: fix/issue-123-<slug>. Worktree: branches/fix-issue-123-<slug>.
Gitea has no native issue→branch API field (only a PR's head branch), so this
linkage is enforced by branch name + claim comment + PR body + cleanup.
Branch folders are ignored by git via branches/, so dirty work in one issue
does not block starting an unrelated issue in a separate branch folder. No LLM
may edit another issue's branch folder unless explicitly assigned to that issue.
No LLM may clean another issue's branch folder unless the PR is merged or closed
and cleanup is explicitly part of the task.
Implementation work and review work must use separate branch folders. For
example, an implementation branch might live under
branches/fix-issue-123-example, while a review branch for the resulting PR
uses its own folder.
Issue creation and claiming may happen from the orchestration checkout:
- Create or identify the tracking issue.
- Claim it with
status:in-progress. - Create the issue branch worktree.
cdinto the branch worktree and perform all file edits there.
Preferred helper:
scripts/worktree-start fix/issue-123-example
cd branches/fix-issue-123-example
Because venv/ is ignored and not copied into new worktrees, run checks with a
known Python interpreter. Either create a venv inside the branch folder, or use
the orchestration checkout's venv by explicit path.
Equivalent manual commands:
git fetch prgs --prune
git worktree add -b fix/issue-123-example branches/fix-issue-123-example prgs/master
cd branches/fix-issue-123-example
For review work, create a separate detached review worktree instead of reusing the author's implementation folder:
scripts/worktree-review fix/issue-123-example # → branches/review-fix-issue-123-example
Cleanup is explicit and only after merge or close. Use the helper (it fetches/ prunes first, refuses to remove a dirty worktree, and only safe-deletes a merged branch), or the equivalent manual commands:
scripts/worktree-clean --delete-branch fix/issue-123-example
# equivalent manual commands:
cd <main-repo>
git fetch prgs --prune
git worktree remove branches/fix-issue-123-example
git branch -d fix/issue-123-example
All three helpers accept --dry-run to print the exact commands/paths without
touching anything.
Create an issue / child issues
- Profile: issue-manager or author (any profile allowed to create issues).
- Steps: create the parent/roadmap issue; create child issues; apply the minimal label set; link children to the parent.
- Prompt: `Using the issue-manager profile, create issue "