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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user