feat: add first-class stacked PR support to author workflow (#484)

Normal author work is unchanged: gitea_lock_issue still requires the worktree
to be base-equivalent to master/main/dev, and gitea_create_pr still targets a
normal base branch. A *stacked* PR (based on another unmerged PR's branch) is a
new explicit, proof-backed path — it never bypasses the issue lock.

- stacked_pr_support.py: pure policy. Approve a non-master base only when it is
  declared AND owned by a live OPEN PR whose number matches; reject arbitrary,
  mismatched, or stale (merged/closed) branches; require the PR body to document
  the stack (base branch, stacked-on PR, merge ordering).
- issue_lock_worktree.py: read_worktree_git_state / _find_matching_base_ref gain
  an opt-in extra_bases arg so an approved stacked base can anchor
  base-equivalence in addition to master/main/dev (empty by default = unchanged).
- gitea_mcp_server.py:
  - gitea_lock_issue gains stacked_base_branch + stacked_base_pr. When declared,
    it verifies the open PR owns the branch, anchors base-equivalence to it, and
    records approved_stacked_base = {branch, pr_number, verified_open} on the lock.
  - gitea_create_pr validates a non-master base against the lock's approved
    stacked base, re-checks the dependency PR is still open, and enforces the
    stacked-PR body fields. Master-based path untouched.
- Docs: llm-workflow-runbooks.md and work-issue.md document when stacked PRs are
  allowed and the required PR-body wording.

Tests: test_stacked_pr_support.py (18 policy cases), stacked base-equivalence in
test_issue_lock_worktree.py, approved_stacked_base persistence round-trip in
test_issue_lock_store.py. Existing lock/create_pr regressions still pass.

The #482 -> #479/#478 case motivates this: create_pr previously could not open a
stacked PR because the lock required a master-equivalent worktree.

Closes #484.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-08 02:22:54 -04:00
co-authored by Claude Opus 4.8
parent 7af73e1539
commit 0cbd1fc801
8 changed files with 603 additions and 6 deletions
+22
View File
@@ -286,6 +286,28 @@ 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