# 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.md`](../skills/llm-project-workflow/SKILL.md) > and its `templates/`. 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](#related-documents). > **New session? Call the guide tools first (#128 / #129).** Before using any other > Gitea MCP tool in a fresh session, call `mcp_get_control_plane_guide` > (read-only): it reports the active profile, authenticated identity, > allowed/forbidden operations, profile-aware do/don't guidance, and the > non-negotiable rules (hard stops, fail-closed behavior, head-SHA pinning, > merge confirmation, redaction, author/reviewer separation, profile > switching). Also call `gitea_get_runtime_context` and `mcp_list_project_skills` > to discover the available project workflows and `mcp_get_skill_guide()` > for step-by-step instructions. This replaces long pasted operator prompts for > the standard rules; operator prompts still control task-specific scope. > See issue #129 for the skill registry design. Jenkins and GlitchTip workflows use separate MCP servers, not this Gitea MCP runtime. Register them as `jenkins-mcp` and `glitchtip-mcp`, reconnect or reload the client, and verify visible tools before claiming either integration is usable. See [`mcp-client-registration.md`](mcp-client-registration.md). For cross-project use, copy the portable workflow skill at [`../skills/llm-project-workflow/SKILL.md`](../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 ```text 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`](gitea-execution-profiles.md). Example role-scoped instructions: ```text 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`](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`](../gitea-mcp.example.json)): ```json { "version": 1, "profiles": { "prgs-reviewer": { "base_url": "https://gitea.example.invalid", "username": "", "auth": { "type": "keychain", "id": "prgs-reviewer-token" }, "default_owner": "", "execution_profile": "gitea-reviewer" }, "prgs-author": { "base_url": "https://gitea.example.invalid", "username": "", "auth": { "type": "env", "name": "GITEA_TOKEN_PRGS_AUTHOR" }, "default_owner": "", "execution_profile": "gitea-author" } } } ``` - `version` — canonical schema version (currently `1`). - `profiles` — map of profile name → profile. - `auth` — a **reference**, never an inline secret: - **keychain**: `{ "type": "keychain", "id": "" }` — the token is read from the macOS keychain on demand. - **env**: `{ "type": "env", "name": "" }` — the token is read from that environment variable. 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: ```json "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" } } ``` ### Dual-profile MCP launcher pattern (Recommended) To avoid the bottleneck of relaunching/restarting the MCP server to switch between author and reviewer roles, the client should register **both** profiles concurrently as separate server instances in the client's MCP configuration: ```json "gitea-author": { "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-author" } }, "gitea-reviewer": { "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" } } ``` * **Tool Namespaces:** Tool calls become distinct and identity-scoped in the client UI: * `mcp__gitea-author__*` (for creating issues, pushing branches, creating PRs) * `mcp__gitea-reviewer__*` (for reviewing PRs, approving, requesting changes, merging) * **Trust Model:** Separate tokens remain separate in the keychain/environment. Each instance operates under its own `GITEA_MCP_PROFILE` and enforces its own `allowed_operations`. A runtime `whoami` identity check is still performed independently, and self-review/self-merge checks remain strictly mandatory. The dual-server pattern is a operational convenience and never a security bypass. * **Reviewer-Identity PR Creation Deadlock:** Reviewer/merge identities must not create PRs or push branches. Doing so makes the reviewer identity the PR author in Gitea, blocking subsequent independent review and causing a review deadlock. Normally, PRs must be created by the author/work identity (`gitea-author`), leaving the reviewer identity (`gitea-reviewer`) clean and available for independent review and merge. * **Reconciler namespace (#310):** Register a third static instance for already-landed PR cleanup when review queues block on open PRs whose heads already landed on `master`: ```json "gitea-reconciler": { "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-reconciler" } } ``` The reconciler profile grants `gitea.pr.close` only for `gitea_reconcile_already_landed_pr` after ancestry proof — not for normal review or author workflows. * **Fallback:** If the dual-profile MCP launcher pattern is not supported or configured in the client, the LLM must relaunch or restart the client/MCP with the correct profile environment variable before claiming or working on any tasks. ## Setup runbook — interactive menu Create and manage profiles without hand-editing JSON: ```bash ./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:** 1. `add profile` → name `prgs-author`, base URL, username, default owner/repo, execution profile `gitea-author`, auth type `keychain` or `env`. - 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. 2. `add profile` again → name `prgs-reviewer`, execution profile `gitea-reviewer`. Existing profiles are preserved. 3. `validate config` → confirm no problems. 4. `generate launcher snippets` → paste the printed snippet into each LLM client's MCP config (it contains no secret). 5. `test profile authentication` → prints the resolved Gitea username (the only time an API call is made, and only on request). 6. `check reviewer eligibility for a PR` → enter a PR number; prints the authenticated user, the PR author, and `ELIGIBLE` / `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. 1. 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. 2. `validate config`, then `test profile authentication` for each profile. 3. Replace each LLM's server block with the thin launcher (command + args + `GITEA_MCP_CONFIG` + `GITEA_MCP_PROFILE`). 4. Delete the `GITEA_USER_*` / `GITEA_PASS_*` / `GITEA_SITE_*` blocks from every LLM config. 5. 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`). ## Work Selection Rule for LLMs Before starting any issue or PR work, acquire or verify a work lease. Do not begin coding, reviewing, fixing, branching, committing, pushing, commenting, or creating a PR until you prove the target is not already being worked. Required checks: 1. List open PRs. 2. Search for PRs linked to the target issue. 3. Search local and remote branches for the issue number. 4. Search registered worktrees for the issue branch. 5. Check dirty worktrees. 6. Check active leases or recent handoffs. 7. Check whether the issue was already completed by a merged PR. If another active LLM/session owns the lease, stop. Allowed responses: continue as the lease owner; review the existing PR if reviewer capability allows; produce a handoff; request takeover after lease expiry; stop with "work already claimed." Never create a parallel branch or PR for the same issue unless the old branch is proven abandoned and the takeover is recorded. Gitea-Tools lease gates: `gitea_lock_issue` (fail-closed before author mutations), `status:in-progress`, and claim comments. `gitea_lock_issue` records an `author_issue_work` lease in a keyed lock file under `GITEA_ISSUE_LOCK_DIR` (default `~/.cache/gitea-tools/issue-locks`), one file per `remote` + `org` + `repo` + `issue_number`. The current MCP session binds its active lock through a per-process pointer so concurrent repos/issues never share one overwrite-prone slot (#443). Each lock payload includes issue number, optional PR number, branch, worktree path, claimant identity/profile, created timestamp, expiry timestamp, and last heartbeat timestamp. An active same-issue/same-operation lease blocks duplicate work. An expired lease still blocks takeover until a recovery review records why the prior work is abandoned, completed, or unsafe to continue. **Stacked PRs (#484).** By default the lock worktree must be base-equivalent to `master`/`main`/`dev` — ordinary work is unchanged. A *stacked* PR (deliberately based on another unmerged PR's branch) is an explicit, opt-in path: pass `stacked_base_branch` **and** `stacked_base_pr` to `gitea_lock_issue`. The lock fails closed unless that branch is owned by a live **open** PR whose number matches `stacked_base_pr`, so arbitrary or stale branches cannot be used as bases. When approved, the lock payload records `approved_stacked_base = {branch, pr_number, verified_open}` and the worktree may be base-equivalent to that branch instead of master. `gitea_create_pr` then allows `base = ` only when it matches the recorded approval, the dependency PR is **still open**, and the PR body documents the stack: - `Stacked on PR # / issue #` - `Base branch: ` - `Head branch: ` - `Do not merge before PR #` (merge ordering) - retarget/rebase to `master` after the dependency lands, if required Stacked support never bypasses the issue lock — the base is recorded *on* the lock and re-verified at PR time. A merged/closed dependency base fails closed; retarget onto `master` or re-lock against a live base. **Do not manually seed `/tmp/gitea_issue_lock.json` or any lock file as a normal recovery path.** That global slot is deprecated and can clobber unrelated live leases (#438). After an MCP restart, call `gitea_lock_issue` again — own-branch adoption rebinds the session when the issue's exact branch already exists (#442). `gitea_create_pr` resolves the durable keyed lock by session pointer or by matching `head` branch without unsafe manual seeding. **Issue-lock recovery (#447):** Do not manually seed, restore, or delete `/tmp/gitea_issue_lock.json` as a normal recovery path. That file is global shared state and manual writes can clobber another session's live lease. Use `sanctioned recovery` instead: 1. `gitea_lock_issue` on a clean `branches/` worktree (normal path). 2. Own-branch adoption via #442 when the issue's exact branch is already pushed. 3. Operator override only when explicitly authorized — record `External-state mutations` and `operator override proof` in the final report. **Adoption proof in the live lock response (#477):** when `gitea_lock_issue` adopts an existing own branch, the response carries an `adoption` block with citable fields — `adoption_decision` (`ADOPT`), `adopted` (`true`), `adopted_branch`, `adopted_branch_head`, `matcher_summary` (boundary-safe reason the branch qualified), `competing_branch_check`, and `safe_next_action`. A normal lock instead returns an `adoption_check` block with `adoption_decision` (`NO_MATCH`) and `adopted: false`, so a non-adoption response can never be misread as claiming adoption. Recovery reports should quote the live lock response `adoption`/`adoption_check` block directly instead of inferring adoption from separate offline checks. `gitea_create_pr` rejects lock files that lack sanctioned `lock_provenance` metadata. Final-report validation blocks handoffs that hide lock read/write/delete under `External-state mutations: none` or mix author PR creation with reviewer approval in one run. See also #438 (global lock redesign). Remote branches matching the issue number are also treated as active work unless the recovery review proves the branch is abandoned or superseded. Never delete or clean up a branch when it has an active lease, dirty worktree, open PR, or is the only copy of unmerged work. Full portable wording: [`skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md). ## Global LLM Worktree Rule The main project checkout is a stable control checkout. It must stay on the configured stable branch: `master`, `main`, or `dev`. All LLM task work must happen inside the project's `branches/` directory. Before any mutation, prove: 1. current project root 2. current working directory 3. current branch 4. stable branch for the main checkout 5. session-owned worktree path under `branches/` If `cwd` is not inside `branches/`, stop. Do not edit, create, delete, format, test-write, commit, merge, rebase, checkout task branches, resolve conflicts, or run cleanup. There are no exceptions for small fixes, docs, tests, cleanup, PR review fixes, conflict resolution, or emergencies. The main checkout may only be used for read-only inspection, fetching, stable-branch update after merged PRs, creating `branches/` worktrees, or explicit control-checkout repair. Portable wording: [`skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md). ## Shell Spawn Hard-Stop Rule Symptom: a shell tool call returns `exit_code: -1` with empty stdout/stderr. That is an executor spawn failure, not a command failure — the command never ran, and retrying the identical call cannot succeed. Required behavior (fail closed, issue #258): 1. **Probe once.** On the first spawn failure, run one trivial probe (`echo ok` or `pwd`). If the probe also returns `exit_code: -1`, mark shell unavailable for the session. 2. **Hard-stop at two.** After two consecutive spawn failures, stop all further shell tool use for the session; never retry the same failing spawn. A hundred retries produce a hundred identical failures (session `019f382e`: 100+ tool calls stalled on a trivial encode-and-commit task). 3. **Emit a recovery report.** The report must direct the operator to: - restart the session, - kill hung background terminals (a hung test runner holding the executor is a known contributor), - prefer MCP-native paths for remaining mutations (for example `gitea_commit_files` under `gitea.repo.commit`) instead of shell. 4. **No improvised fallbacks.** Shell unavailability never authorizes WebFetch/browser/manual-encoding workarounds (see #260). No shell means stop-and-report. Doc-contract tests: `tests/test_shell_spawn_hard_stop_docs.py`. ## Subagent Tool-Budget Guardrails General-purpose subagents tasked with **deterministic MCP work** (for example a single `gitea_commit_files` call) have expanded to 100–122 tool calls, WebFetch/Playwright fallbacks, and throwaway helper-script generation instead of calling the native MCP tool once (observed during #152 closure, issue #259). **Default budgets** (fail closed when exceeded): | Task class | Max tool calls | Max wall time | |------------|----------------|---------------| | Single-step MCP mutation (`commit_files`, `create_pr`, `lock_issue`) | 15 | 5 minutes | | Review / merge queue inspection | 40 | 15 minutes | | Exploration / codebase search (non-mutating) | 60 | 20 minutes | **Required behavior:** 1. **Main session first.** When the active author profile allows `gitea.repo.commit` and `gitea_commit_files` is visible, the main session must call it directly — do not delegate commit authority to a subagent (see #260). 2. **Native MCP before fallback.** After a shell spawn failure (#258), attempt the native MCP tool once before any alternate path. Shell unavailability never authorizes WebFetch, Playwright, or manual base64 encoding. 3. **No retry spirals.** Never resume a failed subagent into a larger retry loop or spawn a second subagent for the same deterministic step. Stop and emit a recovery report instead. 4. **Forbidden detours** when `gitea_commit_files` is available: WebFetch, Playwright/browser automation, manual LLM-generated base64, and ad-hoc `_encode_*` / `_emit_*` helper scripts left in the repo. Doc-contract tests: `tests/test_subagent_tool_budget_docs.py`. ## 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-` (review: `review/pr-456-`) | | Worktree | `branches/fix-issue-123-` (slashes → hyphens) | | PR | body says `Closes #123` or `Fixes #123` (closes issue); `Implements #123` or `Refs #123` (does NOT close) | | 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-. Worktree: branches/fix-issue-123-.` 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. ## Agent temp artifact cleanup (#261) Failed or aborted MCP commit attempts sometimes leave throwaway helper scripts in the **repository root**. These are not part of any issue scope and pollute `git status`, which can break `gitea_lock_issue` and preflight checks. **Patterns (repo root only, untracked):** - `_encode_*.py` — base64 payload encoders - `_emit_*.py` — commit payload emitters - `_inline_*.py` — inline encoding helpers **Required cleanup (after MCP commit completes or aborts):** 1. Delete any matching files at the repo root (`rm ./_encode_*.py` etc.). 2. Confirm `git status` is clean on the orchestration checkout before `gitea_lock_issue`. 3. Prefer native `gitea_commit_files` / gated commit paths — do not leave shell encoding fallbacks behind. Root-level matches are listed in `.gitignore` so they never get committed. `gitea_get_runtime_context` and `gitea_lock_issue` surface **warnings** (not hard blocks) when these artifacts are still present. 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: 1. Create or identify the tracking issue. 2. Claim it with `status:in-progress`. 3. Create the issue branch worktree. 4. `cd` into the branch worktree and perform all file edits there. Preferred helper: ```bash 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: ```bash 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: ```bash 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: ```bash scripts/worktree-clean --delete-branch fix/issue-123-example # equivalent manual commands: cd 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. ### MCP-native commit path (#260) When the active author profile allows **`gitea.repo.commit`** and **`gitea_commit_files`** is visible in the client, that is the **only** approved path for committing files to the tracked repository. Do not improvise alternate encoding or transport when MCP commit is available. **Required before commit:** 1. Call `gitea_resolve_task_capability` for `commit_files` or `gitea_commit_files` and confirm `allowed_in_current_session` is true. 2. Use `gitea_commit_files` with file payloads prepared in the author worktree. 3. Stage only issue-scoped paths; never commit throwaway `_encode_*` / `_emit_*` / `_inline_*` helpers. **Explicitly forbidden workarounds** when MCP commit is reachable: - `WebFetch` / HTTP calls to external decode sites (for example httpbin base64 endpoints) - Playwright or other browser automation to bypass MCP - Manual LLM-generated base64 pasted into ad-hoc scripts - Delegating commit authority to a subagent while the main session has `gitea.repo.commit` on an author profile **If shell encoding is unavailable** (spawn failure, hung terminal) **and** MCP commit cannot run: **stop** with a recovery report. Mention restarting the session, clearing hung background terminals, switching to MCP-native commit, and the agent temp artifact cleanup checklist. Do **not** retry shell encoding in a loop and do **not** substitute WebFetch/Playwright/manual base64. ### 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 "" with body <body>, then create child issues for <list> and link them to the parent.` ### Implement an issue and open a PR - **Profile:** author. - **Steps:** claim the issue (`status:in-progress`); create an isolated branch worktree from latest `master` under `branches/` (`feat/issue-<n>-...` / `fix/...` / `docs/...`); `cd` into that worktree; implement narrowly; add or update tests if behavior changes; run the full suite; commit with an issue-linked message; open a PR to `master`. **Do not** review or merge your own PR. Include an `LLM Handoff Metadata` block (with `LLM-Agent-SHA`) in the PR body — see [`llm-agent-sha.md`](llm-agent-sha.md). - **Prompt:** `Use an author profile to implement issue #N and open a PR to master. Do not self-review or self-merge.` ### Review a PR / request changes / approve - **Profile:** reviewer (must be allowed to review/approve/request_changes, and must **not** be the PR author). - **Steps:** confirm identity + eligibility (menu eligibility check or `gitea_check_pr_eligibility`); read the diff; confirm scope matches the linked issue; post the review (`comment` / `request_changes` / `approve`) via the gated review tool. Pin the reviewed head SHA where supported. Include a `Review Metadata` block (with your own `LLM-Agent-SHA`) in the review — and remember: a different `LLM-Agent-SHA` does **not** make you a different actor; only a different authenticated Gitea user does ([`llm-agent-sha.md`](llm-agent-sha.md)). - **Prompt:** `Use any eligible reviewer profile to review PR #N. Approve only if scope matches issue #M and checks pass; otherwise request changes.` **Live queue reconciliation (mandatory before any review/merge decision):** - Reconcile live state first. Do **not** assume prior handoffs, cached tool output, or chat summaries are current. - Steps (in order): 1. Call `gitea_list_prs` (open state) with explicit remote/org/repo. 2. Immediately `gitea_view_pr <number>` for the candidate; capture head SHA, state, mergeable, updated_at, merged_at/merge_commit_sha if present. 3. Verify against any prior report: state, head SHA, updated timestamp, linked issue state (use `gitea_view_issue` + `gitea_list_issues`). 4. `git fetch <remote> --prune && git checkout master && git pull <remote> master --ff-only` 5. If conflict/staleness detected (prior said "merged" but live open; head or updated_at differs from claimed; merge commit missing on master), report the inconsistency explicitly and **STOP** before review or merge. - After a successful merge: re-run list_prs + view_pr on the PR, confirm master advanced, and include the live post-merge verification in the handoff. - Treat any ambiguous queue state as a blocker until a fresh, consistent live picture is obtained. ### Merge a PR - **Profile:** merger (allowed to merge; must **not** be the PR author). - **Steps:** confirm eligibility; require explicit confirmation (`MERGE PR <n>`); optionally pin head SHA / changed-file set; merge only when Gitea reports the PR mergeable (branch-protection checks satisfied). No force, no ignore-checks. Verify that remote master contains the merge commit or the expected squashed changes (do not assume a "closed" PR succeeded without verifying the actual landed changes). - **Prompt:** `Use any eligible merger profile to merge PR #N if checks pass and it is mergeable. Confirm with "MERGE PR N". Do not force-merge.` #### Merger lease adoption (#536) When review and merge run in **separate sessions**, the merger must **not** manually seed `reviewer_pr_lease._SESSION_LEASE` or equivalent in-process state. That ad hoc pattern was used incidentally for PR #493 and PR #421; it is not canonical proof and is rejected by mutation gates. **Canonical merger handoff:** 1. Confirm a reviewer session holds an active PR lease and posted **APPROVED** at the current live head (`gitea_get_pr_review_feedback`). 2. In a clean merger worktree under `branches/`, call `gitea_adopt_merger_pr_lease` with `worktree`, `expected_head_sha`, and optional `issue_number`. 3. The tool posts durable adoption proof on the PR thread (`<!-- mcp-review-lease-adoption:v1 -->`) recording actor, profiles, adopted-from session/comment, adoption reason, and timestamp, then records sanctioned in-session provenance. 4. Call `gitea_merge_pr` with the same pinned `expected_head_sha`. **Forbidden:** Python one-liners or scripts that call `record_session_lease()` without provenance from `gitea_acquire_reviewer_pr_lease`, `gitea_adopt_merger_pr_lease`, or `gitea_heartbeat_reviewer_pr_lease`. **Same-session review+merge:** the reviewer session may use `gitea_acquire_reviewer_pr_lease` directly; adoption is only for cross-session merger handoff. ### Close the issue after merge / Reconciliation - **Profile:** issue-manager or merger. - **Steps:** Verify remote `master` actually contains the merge (post-merge file-presence verification): - Run: `git fetch <remote> --prune; git checkout master; git pull <remote> master --ff-only` - Verify that expected files added/modified in the PR are present on `master` (or absent if deleted). - Alternatively, verify with: `git log --oneline -- <expected-file>` or `git merge-base --is-ancestor <pr-head-sha> master` - Close the issue; release `status:in-progress` (if it cannot be removed, report why). - **If closed but not merged (`merged=false`):** Stop normal flow. Do not delete worktrees. Compare PR content to remote `master`. - **fully landed:** comment it landed, remove `status:in-progress`, clean up. - **partially landed:** reopen issue, create corrective PR for missing pieces. - **not landed:** reopen issue/PR, do not clean up. - **Direct push to master:** is forbidden except as a documented recovery exception. Final reports must include why, commits, PR metadata, and repaired labels. - **Final reports:** must include both PR metadata (state, merged flag, merge commit) and Git content (remote master hash, expected content present, verification method used & results). - **Prompt (normal):** `After verifying master contains the merge of PR #N using post-merge file-presence verification, close issue #M and delete the merged branch. Include verification details in the report.` - **Prompt (reconcile):** `Reconcile closed-not-merged PR #N by verifying if its content landed on master.` ### Stop on blocker - **Any profile.** If a required gate cannot be satisfied — identity unverifiable, ineligible profile, self-authored PR, moved head, unexpected files, detected secret, or any production/deploy behavior — **stop, report the blocker, and take no mutating action.** Fail closed; never work around a gate. ## Task/role alignment (#167) The **requested task** decides what a session may do — not the credential it happens to hold. Resolve the task first with `gitea_resolve_task_capability(task=...)`: it returns `stop_required` and `task_role_guidance` alongside the permission decision. An LLM asked to review must never degrade into author work just because it is connected as an author. | Requested task | Required identity/profile | Allowed | Forbidden | Stop when | |---|---|---|---|---| | Review PR (`review_pr`) | reviewer (e.g. `sysadmin` / `prgs-reviewer`) | read, gated review verdicts | commits, pushes, file edits, author comments, merge without eligibility | active profile is an author profile — stop immediately; do **not** switch to author-side fixes unless the operator explicitly re-tasks | | Address PR change requests (`address_pr_change_requests`) | author (e.g. `jcwalker3` / `prgs-author`) | commit/push fixes to the PR branch, PR comment summarizing fixes | review verdicts, approve, request-changes, merge | active profile lacks branch push | | Merge PR (`merge_pr`) | reviewer/merger | gated merge after eligibility + approval | merging own PR, merging without pinned head match | active profile is an author profile, or any merge gate fails | | Comment on issue discussion (`comment_issue`) | any profile with `gitea.issue.comment` | issue thread comments | review verdicts, closing via comment | permission missing (`gitea.pr.comment` does **not** imply it) | | Comment on PR (`comment_pr`) | any profile with `gitea.pr.comment` | PR thread comments | review verdicts | permission missing | | Author implementation (`create_branch`/`push_branch`/`create_pr`) | author | branch, commit, push, open PR | self-review, self-merge | profile lacks the author permissions | If the task is review/merge and the session is an author profile, the only correct outputs are: the read-only PR queue inventory (#164), the structured permission report (#142), and a stop. Ask the operator to reconnect to the reviewer namespace; a credential or profile swap in the same session never cures same-session authorship. ## Review feedback discovery (MCP-native) Formal review verdicts (APPROVED / REQUEST_CHANGES / COMMENT) live on the review endpoints, **not** in the issue-comment thread. Never infer review state from issue comments — use `gitea_get_pr_review_feedback(pr_number=...)` (read-only, requires `gitea.read`). It reports: - every submitted review: reviewer, verdict, redacted body, timestamp, and the head SHA it reviewed; - `latest_review_state_by_reviewer` (PENDING drafts never count); - `has_blocking_change_requests` / `approval_visible` (dismissed reviews do not block); - `current_head_sha` vs `latest_reviewed_head_sha`, `review_feedback_stale`, and `author_pushed_after_request_changes` — so a reviewer can see whether feedback predates new commits, and an author can see whether fixes have been pushed since the REQUEST_CHANGES. A permission block returns `feedback_not_attempted: true` with a structured permission report — distinct from a successful "no reviews yet" result, so a blocked lookup is never misread as "no feedback exists". ## Validation reporting discipline (#167) Validation results in handoffs and PR bodies must state exactly what ran and what happened. Build the validation section with `build_validation_report(...)` (in `mcp_server.py`) or follow its contract by hand — every command is one of: - `passed` — the exact command ran and succeeded; - `failed` — must include the exact command **and its output**; never paraphrase ("shell-invocation quirks" is not a status); - `skipped` — deliberately not run; name the reason and any targeted check that replaced it; - `not-run` — was required but never executed; say so plainly. Never imply full-suite success unless the full-suite command itself passed (`full_suite_passed: true`). A report that hides a failed or skipped check is worse than a failing report. ## Controller Handoff (required, every task) Every task — implementation, review, merge, triage, documentation, discussion-only, or blocked planning — **must end with a `Controller Handoff`** so a controller LLM can pick up the state without rereading the conversation. The canonical formats and rules live in the portable skill: [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md) §K. **Compact format is the default** — nine lines (`Task / Repo/state / Issues/PRs / Changed / Validation / Blockers / Review / Next / Safety`), written for controller-LLM readability, not a full human status report. The `Safety:` line is never omitted (usually `no self-review; no self-merge; no tags; no secrets; no prod`). PR bodies still carry the full review detail — the handoff never replaces PR documentation. **The long form** (Work performed · Current state · Files changed · Validation · Issues encountered · Review needed? · Next recommended action · Safety confirmations) **is reserved for high-risk or complex tasks**: a merge/tag/release happened, validation failed, permissions/profile gates blocked work, secrets or production access were involved, an owner decision is complicated, the task spanned multiple repos or cross-issue state, or the owner explicitly asks for it. Hard rules: never omit it; never bury blockers earlier only; an opened PR means "Review needed — PR is open"; a blocked merge names the exact gate; discussion-only comments need owner/design feedback, not code review; any touched release state names the exact tag/commit and why. Design debates belong in **discussion/RFC issues** (e.g. #100 `profiles.json v2`) — comment on the issue, create no branches/PRs, and end the comment with this handoff. ## Fail-closed behavior Before any mutating action the workflow verifies identity, active profile, requested operation, target repo, target issue/PR, and (for review/merge) the PR author. If any check cannot be satisfied, it **fails closed** — no mutation: | Condition | Result | |-----------|--------| | Authenticated identity cannot be verified | blocked | | Unknown / unconfigured profile | blocked | | Profile not allowed the requested operation | blocked | | Authenticated user **is** the PR author (approve/merge) | blocked (no self-review/-merge) | | PR head SHA changed since review | blocked | | PR's changed-file set differs from expected | blocked | | PR not open, or Gitea reports it not mergeable | blocked | | Secret / token detected in content | blocked | | Production / deploy / Ops behavior requested | blocked (out of scope for gitea-mcp) | All mutating attempts — allowed, blocked, failed, or succeeded — are audit-logged with the profile and authenticated user when `GITEA_AUDIT_LOG` is set (see [`safety-model.md`](safety-model.md)). ## Releases and version tags All release tagging, version bumps, and validation must comply with the [Release / Version Process SOP](release-version-sop.md). Versions follow SemVer — **`vMAJOR.MINOR.PATCH`**, using **`v0.x.y`** while unstable. Pick the bump by the largest change since the last tag: - **PATCH** — bug fixes, docs, tests, wrappers, non-breaking workflow polish. - **MINOR** — new MCP tools, new workflow helpers, new config features; backward-compatible behavior. - **MAJOR** — breaking config/schema/API behavior or a changed MCP contract. Tags are **annotated** (`git tag -a`), created **only from the exact commit on remote `master`**, **only after the full suite passes**, and carry release notes referencing the merged PRs/issues. **Never tag** feature branches, dirty worktrees, unreviewed or self-authored work, or commits not on remote `master`. Release runbook (see [`../skills/llm-project-workflow/templates/release-tag.md`](../skills/llm-project-workflow/templates/release-tag.md)): 1. `git fetch prgs --prune`. 2. Confirm local `master` equals `prgs/master` (`0 0`) and the tree is clean. 3. Run the full test suite; stop on failure. 4. Review merged issues/PRs since the last tag (`git log --oneline <last-tag>..prgs/master`). 5. Choose the version bump. 6. `git tag -a <vX.Y.Z> prgs/master -m "<notes referencing #issues / PRs>"`. 7. `git push prgs <vX.Y.Z>`; add release notes if the forge supports it. `scripts/release-tag` automates steps 1–7 with these gates built in (SemVer check, fetch/prune, on-master, clean tree, local==remote master, HEAD on remote master, no duplicate tag, tests run unless `--skip-tests`, annotated tag only). It is **safe by default** — no push unless `--push`, and `--dry-run` changes nothing: ```bash scripts/release-tag --dry-run v0.4.0 scripts/release-tag v0.4.0 --notes-file /tmp/release-notes.md scripts/release-tag v0.4.0 --notes-file /tmp/release-notes.md --push ``` ## Namespace workspace binding (#510) Each MCP namespace resolves its **own** active task workspace. Foreign role worktree environment variables must not poison another namespace's purity checks. | Namespace | Workspace env vars (in priority under `GITEA_ACTIVE_WORKTREE`) | Allowed roots | |-----------|------------------------------------------------------------------|---------------| | author | `GITEA_AUTHOR_WORKTREE` | `branches/<task>` worktree only (#274) | | reviewer | `GITEA_REVIEWER_WORKTREE` | clean `branches/<review>` worktree | | merger | `GITEA_MERGER_WORKTREE` | clean `branches/<merge>` worktree **or** clean control checkout | | reconciler | `GITEA_RECONCILER_WORKTREE` | clean `branches/<reconcile>` worktree **or** clean control checkout | `GITEA_AUTHOR_WORKTREE` is **author-only**. Reviewer, merger, and reconciler MCP processes ignore it even when it points at a dirty author WIP tree. ### Safe reconnect / rebind procedure When a mutation blocks on workspace binding: 1. Read the error — it names the **resolved workspace path**, **role namespace**, and **binding source** (tool arg, env var, or process root). 2. Reconnect or relaunch the correct namespace MCP server from the intended workspace (or set the role-specific env var before launch). 3. Pass `worktree_path` on reviewer/merger mutation tools when the active branches/ worktree differs from the MCP process root. 4. **Do not** clean, reset, or discard foreign role worktrees to unblock your own namespace — that destroys another agent's WIP. ### CTH guidance for workspace binding blockers When posting a Canonical Thread Handoff after a binding blocker: - State which namespace was active (author / reviewer / merger / reconciler). - Quote the resolved workspace path and binding source from the error. - Name the safe reconnect action (relaunch MCP from `branches/...`, set `GITEA_*_WORKTREE`, or pass `worktree_path`). - Explicitly note that foreign worktrees must not be cleaned to unblock. ## Safety notes - Never place raw tokens or passwords in any LLM MCP config; reference secrets by keychain id or env var name only. - Never self-review or self-merge; never bypass Gitea branch protections. - No Jenkins / Ops / deploy / production behavior in `gitea-mcp`. ## Related documents - [`../skills/llm-project-workflow/SKILL.md`](../skills/llm-project-workflow/SKILL.md) — portable cross-project LLM workflow skill. - [`gitea-execution-profiles.md`](gitea-execution-profiles.md) — the profile model. - [`gitea-dual-namespace-deployment.md`](gitea-dual-namespace-deployment.md) — static author/reviewer namespace deployment (#139 decision). - [`llm-agent-sha.md`](llm-agent-sha.md) — opaque agent attribution metadata (never an eligibility input). - [`safety-model.md`](safety-model.md) — trust boundaries and audit logging. - [`tool-boundaries.md`](tool-boundaries.md) — per-tool allowed operations. - [`credential-isolation.md`](credential-isolation.md) — credential handling. - [`release-workflows.md`](release-workflows.md) — release/merge workflow. - [`../README.md`](../README.md) — canonical config, thin launchers, the menu. ## PR-only queue cleanup mode (#390) Use `pr-queue-cleanup` mode when the queue holds many open PRs and the goal is to drain reviews deterministically. One run = one PR = one canonical review (`skills/llm-project-workflow/workflows/pr-queue-cleanup.md`). * Cleanup runs are reviewer-role only (`pr_queue_cleanup` resolver task); author sessions get `wrong_role_stop`. * Forbidden in cleanup mode: issue claiming, branch creation, implementation edits, new issue filing, and any second-PR review after a terminal mutation. * Terminal chain: stop after `REQUEST_CHANGES`; after `APPROVED` continue only to same-PR merge with explicit per-PR operator authorization and passing gates; stop after merge or merge blocker. * Every run reports the next suggested PR without continuing to it; the next PR requires a fresh run with fresh identity/capability/inventory proof. * Use full `review-merge-pr` mode instead when the operator wants a single targeted review, and `work-issue` mode for any authoring.