# 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.

Canonical state comments for durable issue/PR/discussion continuation are
documented in [`canonical-state-comments.md`](canonical-state-comments.md).
Use them when a workflow-changing comment needs to leave the next actor, next
action, and paste-ready prompt in Gitea.

> 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(<name>)`
> for step-by-step instructions. This replaces long pasted operator prompts for
> the standard rules; operator prompts still control task-specific scope.
>
> **BLOCKED + DIAGNOSE (default for any missing required step):** If a required workflow skill, guide, tool, capability, preflight, terminal, worktree binding, profile, or instruction is unavailable or fails, STOP. State BLOCKED. Use the canonical blocker report template (see skills/llm-project-workflow/templates/blocked-diagnose-report.md and the llm-project-workflow/SKILL.md universal rules). Only non-mutating recovery. Report fully. No unsafe fallbacks (temp scripts, direct API, MCP internals, direct imports, in-memory restoration, manual bypasses) unless controller authorizes in the handoff for this case. Missing required steps must fail closed *before* any git or Gitea mutation. Controller prompts and all workflows must reinforce: BLOCKED + DIAGNOSE, then stop.
> 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": "<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 (currently `1`).
- `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.

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 (operator-owned):** If the dual-profile MCP launcher pattern is not supported or configured in the client, **do not** have the LLM relaunch or restart the client/MCP. The LLM **stops** role-switching work, reports that the correct static namespace is missing, and waits for an **operator** to configure dual namespaces or reload the client with the correct `GITEA_MCP_PROFILE` for that role. Process restart/relaunch is operator-owned under the [stable control runtime ADR](architecture/mcp-stable-control-runtime-policy-adr.md) (#615).

## 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 = <that branch>` only when it matches the recorded approval, the
dependency PR is **still open**, and the PR body documents the stack:

- `Stacked on PR #<X> / issue #<Y>`
- `Base branch: <feature-branch>`
- `Head branch: <this-issue-branch>`
- `Do not merge before PR #<X>` (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).

### Root checkout guard (#475)

The MCP server enforces a fail-closed **root checkout guard** before author,
reviewer, and merger mutations when the active workspace is not an isolated
`branches/...` worktree (reconciler close paths remain exempt per #468).

The guard blocks when the **control checkout** (repository root) is:

- on a non-stable branch (`master` / `main` / `dev` expected),
- detached HEAD,
- dirty (tracked edits),
- or its `HEAD` does not match `prgs/master` when that ref is available.

**Remediation (never auto-reset or stash):**

> Root checkout is not on master. Preserve state, switch root back to master,
> and use `scripts/worktree-review` or the sanctioned issue worktree flow.

**Recovery after root hijack:**

1. Preserve any in-progress edits (copy paths, note branch name, or commit on a
   rescue branch from a `branches/...` worktree).
2. From the repository root: `git checkout master` (or `main` / `dev` per repo
   policy) and `git fetch prgs && git merge --ff-only prgs/master` when safe.
3. Confirm `git status` is clean and `git branch --show-current` is `master`.
4. Resume work only inside `branches/issue-<n>-<slug>` via `gitea_lock_issue` /
   `git worktree add`.

`branches/...` directories are disposable role worktrees; the root checkout is
the stable orchestration surface only.

## Canonical workflow skill names (#551)

Controller prompts and sessions must load the **same** workflow skill wall
regardless of runtime (Claude, Codex, Gemini):

| Name | Role |
|------|------|
| `gitea-workflow` | Primary controller / Codex skill name |
| `llm-project-workflow` | Portable in-repo package |
| `git-pr-workflows` | Legacy alias |

- Inventory: `mcp_list_project_skills` lists all three.
- Preflight: `mcp_check_workflow_skill_preflight` before mutations.
- Codex install: `scripts/install-codex-workflow-skill.sh`
- Full doc: [`docs/workflow-skill-mount.md`](workflow-skill-mount.md)

If the skill is missing, stop with BLOCKED + DIAGNOSE — do not mutate.

## No direct-import mutation path (#558)

Never `import gitea_mcp_server` or call `gitea_auth.get_auth_header` /
keychain fill from a raw shell to bypass MCP preflight.

Use the official MCP daemon only. See [`docs/mcp-daemon-import-guard.md`](mcp-daemon-import-guard.md).

## Bootstrap Review Path for self-hosted MCP fixes (#557)

When a PR fixes Gitea-Tools MCP workflow code that the **live daemon** still
runs from broken `master`, canonical review can deadlock on itself.

Do **not** improvise raw API, direct imports, root edits, or gate bypasses.

Use the narrow controller-authorized path documented in:

- [`docs/bootstrap-review-path.md`](bootstrap-review-path.md)

Hard rules:

- No merge without a durable `BOOTSTRAP REVIEW AUTHORIZATION (#557)` record on
  the PR or linked issue.
- Validation, duplicate-PR, mutation-ledger, and contamination audits remain
  mandatory.
- Root checkout stays read-only orchestration only; verification runs in
  `branches/` worktrees.
- After land: restart MCP daemons and re-verify with canonical tools only.
- This never weakens normal reviewer/merger gates for ordinary PRs.

## 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-<slug>` (review: `review/pr-456-<slug>`) |
| Worktree | `branches/fix-issue-123-<slug>` (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-<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.

## 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 <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.

### 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.

### Terminal launcher diagnostics (#556)

When git/pytest finalization fails with opaque spawn errors (e.g. `os error 2`),
do **not** improvise shell wrappers or fall back to direct API / temp scripts.

1. Call `gitea_diagnose_terminal` (optional `cwd`, optional `command`) — returns
   categorized failure: missing cwd, cwd not a directory, missing executable,
   missing runtime wrapper, missing shell, probe timeout, or session launcher
   failure.
2. Call `gitea_get_shell_health` for the shell circuit-breaker state.
3. Mutation-capable `gitea_resolve_task_capability` probes the terminal launcher
   before allowing git/pytest-oriented work; on failure it returns
   `BLOCKED + DIAGNOSE` with `terminal_launcher_unhealthy` and diagnostics.
4. Emit the canonical `blocked-diagnose-report.md` template and stop.

### 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.
- **Labels:** new issues should carry one `type:*` label and one `status:*`
  label. Discussion-only issues must carry `type:discussion`. See
  [`label-taxonomy.md`](label-taxonomy.md).
- **Prompt:** `Using the issue-manager profile, create issue "<title>" 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`, replacing any old
  `status:*` label); 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`; move the issue to
  `status:pr-open`. **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:**
  - Merger workflow starts only after formal approval at current head. Reviewer workflow ends with review decision and separate merger handoff.
  - 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).
  - Review and merge are separate workflow roles. A reviewer approval is not merge authorization.
- **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.`

### Post-merge merged cleanup ownership (#523)

Post-merge **local worktree / remote branch cleanup** is **reconciler** work, not
author work. Do not switch from `prgs-reconciler` to `prgs-author` only to run
`gitea_reconcile_merged_cleanups`.

- **Profile:** `prgs-reconciler` (task `reconcile_merged_cleanups` /
  `reconciliation_cleanup`).
- **Namespace:** reconciler MCP server; stable control checkout is allowed for
  this role (branches-only author guard does not apply).
- **Steps:**
  1. `gitea_whoami` + `gitea_resolve_task_capability(task="reconcile_merged_cleanups")`.
  2. Dry-run first: `gitea_reconcile_merged_cleanups(dry_run=True)`.
  3. Execute only after audit/authorization gates when remote branch delete or
     worktree removal is required (`dry_run=False`, `execute_confirmed=True`,
     and `gitea.branch.delete` when deleting remotes).
- **Fail closed:** unmerged/open heads, mismatched worktrees, and non-merged
  closed PRs must not be cleaned.
- **Reports:** label cleanup actions as reconciler cleanup (not author mutation).
- **Prompt:** `As prgs-reconciler, dry-run then execute gitea_reconcile_merged_cleanups for recently merged PRs without switching to prgs-author.`

### Superseded PR / satisfied issue reconciliation (#525)

Closing duplicate or superseded PRs after a canonical PR has merged is
**reconciler** work. Do not switch to `prgs-author` only to close the duplicate
PR, close the satisfied issue, or file a narrow follow-up discovered during
reconciliation.

- **Profile:** `prgs-reconciler`.
- **Tasks:** `reconcile_close_superseded_pr`,
  `reconcile_close_satisfied_issue`, and
  `reconcile_create_followup_issue`.
- **Tool:** `gitea_reconcile_superseded_by_merged_pr`.
- **Required proof:**
  1. Live target PR state is open, but it is not independently required and no
     mergeable work remains.
  2. Live superseding PR state is closed and merged.
  3. Superseding PR head is an ancestor of freshly fetched `master`.
  4. Merge commit SHA is recorded.
  5. Canonical close comment cites the superseding PR and merge commit.
  6. Linked issue closure is attempted only when the issue is open and
     explicitly satisfied by the superseding PR.
- **Fail closed:** missing ancestry proof, missing canonical comment,
  mergeable target PR, same target/superseding PR, or missing
  `gitea.pr.close` / `gitea.issue.close` capability.
- **Prompt:** `As prgs-reconciler, reconcile PR #N as superseded by merged PR #M; post the canonical close comment, close the superseded PR, and close issue #K only if the merged PR fully satisfies it.`

### 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`) | merger (e.g. `sysadmin` / `prgs-merger`) | gated merge after eligibility + approval | merging own PR, merging without pinned head match, reviewing PRs | active profile is an author or reviewer-only 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.

## Canonical Thread Handoff (CTH)

**CTH** = **Canonical Thread Handoff** — the authoritative workflow handoff
comment in a Gitea issue or PR thread. See
[`canonical-thread-handoff.md`](canonical-thread-handoff.md) for types,
templates, and examples.

Before acting in an issue or PR thread:

1. **Find the latest CTH comment** before doing work.
2. Treat the **latest valid CTH** as the current handoff state.
3. **Post a new CTH** when you finish, block, skip, supersede, request
   changes, approve, or hand off.
4. Do **not** rely on stale non-CTH comments when a newer CTH exists.

A CTH summarizes workflow state for the next session. Formal Gitea review
verdicts remain authoritative for merge gates — a CTH is not merge approval
by itself.

Template: [`../skills/llm-project-workflow/templates/canonical-thread-handoff.md`](../skills/llm-project-workflow/templates/canonical-thread-handoff.md)

## 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.

## Two-comment workflow reporting (#507)

After meaningful controller/workflow work, post **two separate Gitea comments**
(not one combined blob):

1. **`[CONTROLLER HANDOFF]`** — detailed operational continuation for the
   next LLM/controller (proof-heavy; may be long).
2. **`[THREAD STATE LEDGER]`** — short canonical truth readable in ~30 seconds.

The ledger must answer: what is true now, what changed, what is blocked,
who/what acts next — and must **separate**:

- local verdict/state
- server-side Gitea state
- attempted-but-blocked mutations
- completed mutations

Use precise state phrases (`APPROVED review posted to Gitea`,
`APPROVE verdict prepared locally`, `merge performed`, `merge not performed`,
`no server-side state changed`, `lease attempt blocked`) instead of ambiguous
standalone words (`approved`, `merged`, `ready`, `blocked`, `done`).

The ledger must include a **blocker classification** from:
`code blocker`, `test blocker`, `merge conflict`, `stale head`,
`permission/capability blocker`, `environment/tooling blocker`,
`process/rule blocker`, `queue/lease blocker`,
`duplicate/canonicalization blocker`, `no blocker`.

Templates: [`two-comment-workflow.md`](two-comment-workflow.md).
Worked examples: [`examples/two-comment-workflow-examples.md`](examples/two-comment-workflow-examples.md).

Validation: `thread_state_ledger_validator.py` checks tagged comments at post
time (`gitea_create_issue_comment`) and tagged final reports via
`assess_final_report_validator`. Legacy `## Controller Handoff` final reports
remain valid during transition; the tagged pair is required for new workflow
comments.

Related (do not duplicate): #494/#495 lifecycle state, #501 mutation-ledger
consistency, #505 CTH umbrella, #496 workflow comment gate when merged.

## Canonical comment validation (#496)

Workflow-changing issue/PR/review comments must carry durable next-action
state. Casual discussion is still allowed.

The MCP server runs `canonical_comment_validator.assess_canonical_comment`
**before** posting through:

- `gitea_create_issue_comment`
- `gitea_submit_pr_review` / `gitea_dry_run_pr_review` (non-empty review bodies)
- `gitea_reconcile_already_landed_pr` when `post_comment=True`
- internal structured comment helpers (machine lease/heartbeat markers stay exempt)

Detection examples:

- **Allowed:** `Thanks, I will check this.`
- **Rejected:** `Blocked, author should fix.` (workflow trigger without canonical fields)

When validation fails, the tool returns `canonical_comment_validation` with
`allowed: false`, `missing_fields`, `vague_fields`, `correction_message`, and
`suggested_template`. **No Gitea API call is made.**

Minimum workflow comment fields:

```text
STATE:
WHO_IS_NEXT:
NEXT_ACTION:
NEXT_PROMPT:
WHY:
```

`WHO_IS_NEXT` must be one of: `controller`, `author`, `reviewer`, `merger`,
`reconciler`, `user`.

Issue comments also require `RELATED_PRS`, `BLOCKERS`, and `VALIDATION` when
they mention PR work. PR comments/reviews also require `ISSUE`, `HEAD_SHA`,
`REVIEW_STATUS`, `MERGE_READY`, `BLOCKERS`, and `VALIDATION`.

Special states:

- `STATE: blocked` — `BLOCKERS` must name an explicit unblock condition.
- `STATE: superseded` — requires `CANONICAL_ITEM` and `SUPERSEDED_ITEM`.
- `STATE: ready-to-merge` — requires approval proof and head SHA in
  `HEAD_SHA`, `REVIEW_STATUS`, `MERGE_READY`, or `VALIDATION`.

Final reports must not claim a comment was posted when
`canonical_comment_validation.allowed` is false (#496 AC14).

## Stale #332 review-decision lock cleanup (#594)

#332 hard-stops a reviewer session after a terminal live review mutation
(`approve` / `request_changes`). After #559 those locks are **durable** under
`~/.cache/gitea-tools/session-state/review_decision_lock-<profile>.json`, so they
can outlive the PR they protected.

### When cleanup is **allowed**

Use `gitea_cleanup_stale_review_decision_lock` from a **reviewer** profile when:

1. A durable review-decision lock has a terminal live mutation, **and**
2. Live Gitea state shows that terminal PR is already **merged** or **closed**
   (so no same-PR merge sequence remains), **and**
3. The active profile identity matches the lock, **and**
4. Authenticated identity can be verified.

Workflow:

```text
# 1) Assess only (default)
gitea_cleanup_stale_review_decision_lock(apply=false, remote=prgs, ...)

# 2) Apply after is_moot=true and cleanup_allowed=true
gitea_cleanup_stale_review_decision_lock(
  apply=true,
  expected_terminal_pr=<merged-or-closed-pr>,
  remote=prgs,
  ...
)
```

Successful apply:

* clears in-memory + durable decision lock for that profile
* returns a structured `audit` payload
* posts an audit comment on the mooted PR when `gitea.pr.comment` is allowed
  (`post_audit_comment=true` default)

Same-profile auto-expire: if `gitea_merge_pr` succeeds on a PR that this same
profile just approved, the decision lock is cleared after merge. Cross-profile
locks (reviewer vs merger) still require the cleanup tool.

### When cleanup is **forbidden**

Do **not** clear when:

* the last terminal PR is still **open** (active #332 hard-stop — continue merge
  for that approve, or stop after request_changes)
* live PR state cannot be fetched (fail closed)
* profile identity does not match the lock
* identity cannot be verified
* `expected_terminal_pr` does not match the last terminal PR

**Never** use manual deletion of session-state files as the normal workflow.
**Never** use cleanup to skip review of an open PR or to bypass eligibility /
mark-ready / submit gates on active work.

### Related tools

| Tool | Purpose |
|------|---------|
| `gitea_cleanup_stale_review_decision_lock` | Moot decision-lock cleanup (#594) |
| `gitea_authorize_review_correction` | Operator-approved correction after a **mistaken** live review on still-active work (#211) |
| `gitea_cleanup_post_merge_moot_lease` | Moot **lease** cleanup after merge (#515) — different object |

## 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. **LLM-allowed:** pass `worktree_path` on reviewer/merger mutation tools when
   the active `branches/` worktree differs from the MCP process root; **client
   reconnect** after transport EOF only (no process kill).
3. **Operator-owned:** if the wrong namespace process was launched, or a role-
   specific `GITEA_*_WORKTREE` must be set at process start, an **operator**
   reloads/relaunches the correct static namespace MCP. LLM sessions must not
   kill or restart MCP processes (see [stable control runtime ADR](architecture/mcp-stable-control-runtime-policy-adr.md)).
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 next action: pass `worktree_path` if that unblocks the tool, or
  request an **operator** reload of the correct namespace MCP / env binding.
- Explicitly note that foreign worktrees must not be cleaned to unblock, and
  that the LLM must not self-restart the MCP process.

## 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

- [`architecture/mcp-stable-control-runtime-policy-adr.md`](architecture/mcp-stable-control-runtime-policy-adr.md) — stable control runtime vs dev runtime; LLM must not kill/restart MCP; operator-owned reload and promotions; routine post-merge parity staleness (#615).
- [`reviewer-handoff-consistency.md`](reviewer-handoff-consistency.md) — reject contradictory reviewer handoffs (#501).
- [`issue-acceptance-gate.md`](issue-acceptance-gate.md) — controller issue-acceptance audit after PR merge (#500).
- [`../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.
- [`state-handoff-ledger.md`](state-handoff-ledger.md) — canonical state comments and next-action handoff (#494).

## Canonical state handoff ledger (#494)

Gitea comments and final reports must make continuation obvious without chat
history. See [`state-handoff-ledger.md`](state-handoff-ledger.md) for:

- discussion → issue → PR → review → merge → reconcile lifecycle
- templates for discussion, issue, PR, and queue-controller state comments
- final-report requirements: Current status, Next actor, Next action, Next prompt
- queue-controller priority order and discussion ≥5-comment rule (urgent/trivial
  exceptions)

Helpers live in `state_handoff_ledger.py`; final-report enforcement is wired
through `assess_final_report_validator`.

## 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.
