feat(mcp): publish an unpublished local commit on a registered issue worktree (Closes #812) #814

Merged
sysadmin merged 1 commits from feat/issue-812-publish-unpublished-commit into master 2026-07-22 16:40:55 -05:00
Owner

Closes #812

Implements AC20 only. Head: 99fda93bccbedfe90e4c058200a65241d15f4da8.

The defect this closes

Entry point B of #812: an author's work has already advanced to a local commit. The worktree is registered, clean, on its issue branch, and carries the only copy of the implementation — but the branch has never been published. Two individually correct predicates then close a cycle:

  1. issue_lock_renewal.assess_exact_owner_lease_renewal (lines 328-332) refuses to renew an expired lease without an observable remote head. An unpublished branch has none.
  2. Every publication path — gitea_commit_files, gitea_create_pr — is lock-derived under #618. Nothing can create that remote head without first holding the lock that renewal would grant.

assess_expired_lock_reclaim cannot break the tie either: it requires a dead owner pid or a missing worktree, and in the live case the recorded pid belongs to the long-running MCP daemon while the worktree is present.

What this adds

One tool, gitea_publish_unpublished_issue_branch, plus the pure module branch_publish.py holding its assessor, executor, and read-after-write verifier. It publishes an already-committed local head to its remote branch, so exact-owner renewal has the evidence its model requires. Publication is the whole of its authority — it renews, reclaims, rebinds, and clears nothing.

Why this is not a lock bypass

The operation can publish only a branch whose durable issue-lock record already names the caller as claimant. Ownership is read from the lock file on disk, never asserted by the caller, so the tool cannot manufacture a claim it does not already hold. With no lock record it refuses outright.

Guard strictness does not regress (AC15). Each of these fails closed, and each has its own test:

Refusal Evidence
Dirty tracked files worktree porcelain
Untracked files the commit does not carry worktree porcelain
Unregistered worktree git worktree list inventory (#713)
Non-issue branch, or a stable branch branch-name grammar + is_stable_ref
Branch number that does not match the issue branch-name grammar
Changed local HEAD declared expected_head vs observed HEAD
Abbreviated or symbolic revision full 40-character SHA required
Remote head that is not an ancestor merge-base --is-ancestor
Unobservable remote probe failure kept distinct from "branch absent"
Foreign claimant or foreign profile durable lock record
Branch or worktree mismatch against the lock durable lock record
Competing open PR for the issue on another branch open-PR inventory
Declared content-hash mismatch or missing file SHA-256 over the worktree

The refspec names the commit SHA explicitly and never forces, so git itself rejects a non-fast-forward as a final independent guard. An open PR on this branch is this work's own PR and is correctly not treated as a rival claim.

Record separation (AC23)

The durable issue-lock file and the control-plane workflow lease are distinct records. This reads the former as ownership evidence and writes neither. A regression asserts the lock file is byte-for-byte unchanged across a successful publication and that lock_generation is not advanced. Both success and refusal payloads report issue_lock_record_mutated: false and workflow_lease_touched: false.

Truthful process evidence (AC24)

The recorded owner pid's liveness is never consulted or asserted. That pid routinely belongs to the MCP daemon rather than an active author client, which is why the reclaim predicate can never be satisfied while that daemon runs. Publication does not require a dead process, so it never claims one — owner_pid_liveness_consulted is reported as false. A regression pins the recorded pid to a live process, proves publication still succeeds, and proves assess_expired_lock_reclaim still refuses for that very pid. The routed-around predicate is not softened.

Scope

AC20 only. AC21 cannot unblock on its own — reclaim needs a dead pid, and renewal still needs the published head that AC20 creates — so the two are separable and only the smaller one is implemented. AC1-AC19 and AC22-AC24 beyond what is stated above stay open under #812's entry point A.

Tests

36 new cases in tests/test_issue_812_publish_unpublished_commit.py, against synthetic fixtures only: a real git repository with a real local bare remote, so publication and read-after-write verification are genuinely executed rather than mocked. Coverage includes successful publication, idempotent retry reporting already_published, dry-run reporting intent without publishing, dry-run and apply agreeing on a refusal, honest fast-forward, renewal refused before publication then sanctioned after it, and every refusal in the table above.

AC17 is honoured. No test, fixture, or implementation step reads or references the protected #635 worktree; a regression asserts that absence in both new files. That worktree was verified read-only during this work and remains at its unpublished local commit with all seven content hashes matching its durable record.

Full suite: 11 failed, 4354 passed, 6 skipped, 533 subtests passed. The 11 failures are the documented pre-existing drift baseline at 9eb0f29 in test_commit_payloads.py, test_issue_702_review_findings_f1_f6.py, test_mcp_server.py, test_post_merge_moot_lease.py, and test_reconciler_supersession_close.py — unchanged in count and identity. Baseline captured on this branch before any edit.

Reviewer notes

  • task_capability_map.publish_unpublished_branch reuses the existing gitea.branch.push permission deliberately: a new operation name would silently strip the capability from every already-configured author profile.
  • tests/test_task_capability_role_invariants.py gains one entry. That constant is a deliberate tripwire requiring a conscious update whenever a role-exclusive task is added; this is that update.
  • docs/mcp-tool-inventory.md was regenerated with the documented generator command, not hand-edited.
  • This is the first Python code in the repository that sends a git ref to a remote. Everything prior either shells out to mirror_refs.sh against a throwaway cache clone, or merely judges a caller-asserted push string.
Closes #812 Implements **AC20 only**. Head: `99fda93bccbedfe90e4c058200a65241d15f4da8`. ## The defect this closes Entry point B of #812: an author's work has already advanced to a local commit. The worktree is registered, clean, on its issue branch, and carries the only copy of the implementation — but the branch has never been published. Two individually correct predicates then close a cycle: 1. `issue_lock_renewal.assess_exact_owner_lease_renewal` (lines 328-332) refuses to renew an expired lease without an observable remote head. An unpublished branch has none. 2. Every publication path — `gitea_commit_files`, `gitea_create_pr` — is lock-derived under #618. Nothing can create that remote head without first holding the lock that renewal would grant. `assess_expired_lock_reclaim` cannot break the tie either: it requires a dead owner pid or a missing worktree, and in the live case the recorded pid belongs to the long-running MCP daemon while the worktree is present. ## What this adds One tool, `gitea_publish_unpublished_issue_branch`, plus the pure module `branch_publish.py` holding its assessor, executor, and read-after-write verifier. It publishes an already-committed local head to its remote branch, so exact-owner renewal has the evidence its model requires. Publication is the whole of its authority — it renews, reclaims, rebinds, and clears nothing. ## Why this is not a lock bypass The operation can publish only a branch whose **durable issue-lock record already names the caller as claimant**. Ownership is read from the lock file on disk, never asserted by the caller, so the tool cannot manufacture a claim it does not already hold. With no lock record it refuses outright. Guard strictness does not regress (AC15). Each of these fails closed, and each has its own test: | Refusal | Evidence | |---|---| | Dirty tracked files | worktree porcelain | | Untracked files the commit does not carry | worktree porcelain | | Unregistered worktree | `git worktree list` inventory (#713) | | Non-issue branch, or a stable branch | branch-name grammar + `is_stable_ref` | | Branch number that does not match the issue | branch-name grammar | | Changed local HEAD | declared `expected_head` vs observed HEAD | | Abbreviated or symbolic revision | full 40-character SHA required | | Remote head that is not an ancestor | `merge-base --is-ancestor` | | Unobservable remote | probe failure kept distinct from "branch absent" | | Foreign claimant or foreign profile | durable lock record | | Branch or worktree mismatch against the lock | durable lock record | | Competing open PR for the issue on another branch | open-PR inventory | | Declared content-hash mismatch or missing file | SHA-256 over the worktree | The refspec names the commit SHA explicitly and never forces, so git itself rejects a non-fast-forward as a final independent guard. An open PR on *this* branch is this work's own PR and is correctly not treated as a rival claim. ## Record separation (AC23) The durable issue-lock file and the control-plane workflow lease are distinct records. This reads the former as ownership evidence and writes neither. A regression asserts the lock file is byte-for-byte unchanged across a successful publication and that `lock_generation` is not advanced. Both success and refusal payloads report `issue_lock_record_mutated: false` and `workflow_lease_touched: false`. ## Truthful process evidence (AC24) The recorded owner pid's liveness is never consulted or asserted. That pid routinely belongs to the MCP daemon rather than an active author client, which is why the reclaim predicate can never be satisfied while that daemon runs. Publication does not require a dead process, so it never claims one — `owner_pid_liveness_consulted` is reported as `false`. A regression pins the recorded pid to a live process, proves publication still succeeds, **and** proves `assess_expired_lock_reclaim` still refuses for that very pid. The routed-around predicate is not softened. ## Scope AC20 only. AC21 cannot unblock on its own — reclaim needs a dead pid, and renewal still needs the published head that AC20 creates — so the two are separable and only the smaller one is implemented. AC1-AC19 and AC22-AC24 beyond what is stated above stay open under #812's entry point A. ## Tests 36 new cases in `tests/test_issue_812_publish_unpublished_commit.py`, against **synthetic fixtures only**: a real git repository with a real local bare remote, so publication and read-after-write verification are genuinely executed rather than mocked. Coverage includes successful publication, idempotent retry reporting `already_published`, dry-run reporting intent without publishing, dry-run and apply agreeing on a refusal, honest fast-forward, renewal refused before publication then sanctioned after it, and every refusal in the table above. AC17 is honoured. No test, fixture, or implementation step reads or references the protected `#635` worktree; a regression asserts that absence in both new files. That worktree was verified read-only during this work and remains at its unpublished local commit with all seven content hashes matching its durable record. Full suite: **11 failed, 4354 passed, 6 skipped, 533 subtests passed**. The 11 failures are the documented pre-existing drift baseline at `9eb0f29` in `test_commit_payloads.py`, `test_issue_702_review_findings_f1_f6.py`, `test_mcp_server.py`, `test_post_merge_moot_lease.py`, and `test_reconciler_supersession_close.py` — unchanged in count and identity. Baseline captured on this branch before any edit. ## Reviewer notes - `task_capability_map.publish_unpublished_branch` reuses the existing `gitea.branch.push` permission deliberately: a new operation name would silently strip the capability from every already-configured author profile. - `tests/test_task_capability_role_invariants.py` gains one entry. That constant is a deliberate tripwire requiring a conscious update whenever a role-exclusive task is added; this is that update. - `docs/mcp-tool-inventory.md` was regenerated with the documented generator command, not hand-edited. - This is the first Python code in the repository that sends a git ref to a remote. Everything prior either shells out to `mirror_refs.sh` against a throwaway cache clone, or merely judges a caller-asserted push string.
jcwalker3 added 1 commit 2026-07-22 16:19:04 -05:00
Entry point B of #812 is the state where an author's work has already advanced to a local commit: the worktree is registered, clean, on the issue branch, and carries the only copy of the implementation, but the branch has never been published. Two individually correct predicates close a cycle around it: exact-owner lease renewal refuses without an observable remote head, and every publication path is lock-derived under #618, so nothing can create that remote head without first holding the lock renewal would grant.

This adds the missing operation. gitea_publish_unpublished_issue_branch publishes an already-committed local head to its remote branch, so exact-owner renewal has the evidence its model requires. Publication is the whole of its authority: it renews, reclaims, rebinds, and clears nothing.

Why this is not a lock bypass: the operation can only publish a branch whose durable issue-lock record already names the caller as claimant. Ownership is read from the lock file, never asserted by the caller. Guard strictness is unchanged (AC15): a dirty tree, an untracked file the commit does not carry, an unregistered worktree, a non-issue or stable branch, a changed local HEAD, a remote head that is not an ancestor of the commit, a competing open PR for the same issue on another branch, and any declared-hash mismatch each fail closed. The refspec names the commit SHA explicitly and never forces.

Record separation (AC23): the durable issue-lock file and the control-plane workflow lease are distinct records. This reads the former as ownership evidence and writes neither.

Truthful process evidence (AC24): the recorded owner pid's liveness is never consulted or asserted. A regression pins the recorded pid to a live process, proves publication still succeeds, and proves expired-lock reclaim still refuses for that same pid.

Scope is AC20 only. AC21 cannot unblock on its own, so the two are separable and only the smaller one is implemented here.

Tests: 36 new cases against synthetic fixtures only, using a real git repository with a real local bare remote so publication and read-after-write verification are genuinely executed rather than mocked. AC17 is honoured and a regression asserts the protected worktree is never referenced.

Full suite: 11 failed, 4354 passed, 6 skipped, 533 subtests passed. The 11 failures are the documented pre-existing drift baseline at 9eb0f29, unchanged in count and identity.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #814
issue: #812
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18189-bad3e1a23746
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-814-99fda93
phase: claimed
candidate_head: 99fda93bcc
target_branch: master
target_branch_sha: 9eb0f29cef
last_activity: 2026-07-22T21:27:43Z
expires_at: 2026-07-22T21:37:43Z
blocker: none

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #814 issue: #812 reviewer_identity: sysadmin profile: prgs-reviewer session_id: 18189-bad3e1a23746 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-814-99fda93 phase: claimed candidate_head: 99fda93bccbedfe90e4c058200a65241d15f4da8 target_branch: master target_branch_sha: 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8 last_activity: 2026-07-22T21:27:43Z expires_at: 2026-07-22T21:37:43Z blocker: none
sysadmin approved these changes 2026-07-22 16:28:57 -05:00
sysadmin left a comment
Owner

Independent review — PR #814 (AC20 only)

Reviewer: sysadmin / prgs-reviewer (author is jcwalker3)
Pinned head: 99fda93bccbedfe90e4c058200a65241d15f4da8
Base: master @ 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8
Slice: AC20 only — publish clean, locally committed, unpublished issue branch

Verdict: APPROVE

Independent assessment of head 99fda93 confirms the PR safely implements the smallest separable AC20 publication route without claiming AC21 or entry-point A.

Decisive evidence

  • Tool gitea_publish_unpublished_issue_branch + pure branch_publish.py assessor/executor/verifier
  • Requires durable issue-lock claimant match (not a general lock bypass)
  • Ownership from lock file; no caller-asserted eligibility booleans
  • Registered clean worktree; full 40-char expected HEAD; issue-branch grammar; non-force refspec {sha}:refs/heads/{branch}
  • Fails closed: dirty/untracked, unregistered, stable/wrong branch, changed HEAD, non-ancestor remote, competing PR, hash mismatch, missing lock, foreign identity/profile
  • Read-after-write remote head verification; lock file byte-identical; workflow lease untouched
  • Author-only (gitea.branch.push + ROLE_EXCLUSIVE_TASKS); reviewer/merger/reconciler cannot publish
  • AC20 separable from AC21; reclaim still refuses live daemon pid

Independent tests (review worktree)

  • Focused: 47 passed (36 AC20 cases + role invariants)
  • Baseline-file suite: 11 failed / 254 passed — same 11 failure identities in the documented pre-existing files (no new regression from this slice)

Protected asset

#635 worktree untouched at b2f6e9a, clean

Canonical PR State

STATE: approved_at_current_head
WHO_IS_NEXT: merger
NEXT_ACTION: Eligible merger merges PR #814 only if live head remains 99fda93bcc and all merge gates pass; then restart daemon for runtime AC20 availability before #635 recovery
NEXT_PROMPT:

Merge PR #814 for Scaled-Tech-Consulting/Gitea-Tools issue #812 at expected_head_sha 99fda93bccbedfe90e4c058200a65241d15f4da8 using merger profile only. Do not merge if head moved. After merge, verify master contains the merge and plan runtime reconnect before using gitea_publish_unpublished_issue_branch on #635.

WHAT_HAPPENED: Independent reviewer (sysadmin/prgs-reviewer) validated PR #814 AC20 implementation at pinned head, ran focused and baseline-file tests, and submitted APPROVE via native MCP
WHY: AC20 gates are correctly fail-closed; publication is not a lock bypass; tests cover success, refusals, lock immutability, and RAW verification; AC21 not falsely claimed
ISSUE: #812
HEAD_SHA: 99fda93bcc
REVIEW_STATUS: approved / approval_at_current_head
MERGE_READY: true
BLOCKERS: none for this review; post-merge daemon restart still required before live #635 publication
VALIDATION: pytest tests/test_issue_812_publish_unpublished_commit.py tests/test_task_capability_role_invariants.py -> 47 passed; baseline five files -> 11 failed (pre-existing identities), 254 passed; head pin reconfirmed before submit
NATIVE_REVIEW_PROOF: transport=native_mcp; entrypoint=mcp_server; token_fingerprint=34b998c0bf85070e
LAST_UPDATED_BY: sysadmin (prgs-reviewer)

## Independent review — PR #814 (AC20 only) **Reviewer:** sysadmin / prgs-reviewer (author is jcwalker3) **Pinned head:** `99fda93bccbedfe90e4c058200a65241d15f4da8` **Base:** master @ `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8` **Slice:** AC20 only — publish clean, locally committed, unpublished issue branch ### Verdict: APPROVE Independent assessment of head `99fda93` confirms the PR safely implements the smallest separable AC20 publication route without claiming AC21 or entry-point A. ### Decisive evidence - Tool `gitea_publish_unpublished_issue_branch` + pure `branch_publish.py` assessor/executor/verifier - Requires durable issue-lock claimant match (not a general lock bypass) - Ownership from lock file; no caller-asserted eligibility booleans - Registered clean worktree; full 40-char expected HEAD; issue-branch grammar; non-force refspec `{sha}:refs/heads/{branch}` - Fails closed: dirty/untracked, unregistered, stable/wrong branch, changed HEAD, non-ancestor remote, competing PR, hash mismatch, missing lock, foreign identity/profile - Read-after-write remote head verification; lock file byte-identical; workflow lease untouched - Author-only (`gitea.branch.push` + ROLE_EXCLUSIVE_TASKS); reviewer/merger/reconciler cannot publish - AC20 separable from AC21; reclaim still refuses live daemon pid ### Independent tests (review worktree) - Focused: 47 passed (36 AC20 cases + role invariants) - Baseline-file suite: 11 failed / 254 passed — same 11 failure identities in the documented pre-existing files (no new regression from this slice) ### Protected asset #635 worktree untouched at `b2f6e9a`, clean ## Canonical PR State STATE: approved_at_current_head WHO_IS_NEXT: merger NEXT_ACTION: Eligible merger merges PR #814 only if live head remains 99fda93bccbedfe90e4c058200a65241d15f4da8 and all merge gates pass; then restart daemon for runtime AC20 availability before #635 recovery NEXT_PROMPT: ```text Merge PR #814 for Scaled-Tech-Consulting/Gitea-Tools issue #812 at expected_head_sha 99fda93bccbedfe90e4c058200a65241d15f4da8 using merger profile only. Do not merge if head moved. After merge, verify master contains the merge and plan runtime reconnect before using gitea_publish_unpublished_issue_branch on #635. ``` WHAT_HAPPENED: Independent reviewer (sysadmin/prgs-reviewer) validated PR #814 AC20 implementation at pinned head, ran focused and baseline-file tests, and submitted APPROVE via native MCP WHY: AC20 gates are correctly fail-closed; publication is not a lock bypass; tests cover success, refusals, lock immutability, and RAW verification; AC21 not falsely claimed ISSUE: #812 HEAD_SHA: 99fda93bccbedfe90e4c058200a65241d15f4da8 REVIEW_STATUS: approved / approval_at_current_head MERGE_READY: true BLOCKERS: none for this review; post-merge daemon restart still required before live #635 publication VALIDATION: pytest tests/test_issue_812_publish_unpublished_commit.py tests/test_task_capability_role_invariants.py -> 47 passed; baseline five files -> 11 failed (pre-existing identities), 254 passed; head pin reconfirmed before submit NATIVE_REVIEW_PROOF: transport=native_mcp; entrypoint=mcp_server; token_fingerprint=34b998c0bf85070e LAST_UPDATED_BY: sysadmin (prgs-reviewer)
Owner

repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #814
issue: #812
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 32120-10bd20512910
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-814-99fda93
phase: claimed
candidate_head: 99fda93bcc
target_branch: master
target_branch_sha: 9eb0f29cef
last_activity: 2026-07-22T21:40:31Z
expires_at: 2026-07-22T21:50:31Z
blocker: none

<!-- mcp-review-lease:v1 --> repo: Scaled-Tech-Consulting/Gitea-Tools pr: #814 issue: #812 reviewer_identity: sysadmin profile: prgs-merger session_id: 32120-10bd20512910 worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr-814-99fda93 phase: claimed candidate_head: 99fda93bccbedfe90e4c058200a65241d15f4da8 target_branch: master target_branch_sha: 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8 last_activity: 2026-07-22T21:40:31Z expires_at: 2026-07-22T21:50:31Z blocker: none
sysadmin merged commit 910b6edbdc into master 2026-07-22 16:40:55 -05:00
Owner

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

Status: APPLIED

Manual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.

## Stale #332 review-decision lock cleanup (#594) Status: **APPLIED** - actor: `sysadmin` - profile: `prgs-merger` - timestamp: `2026-07-22T21:40:56.927664+00:00` - last terminal: `approve` on PR #814 - PR state: `closed` (merged=True) - merge_commit_sha: `910b6edbdcf0dec64cdf139b1a1dbac4bd38c1ef` - prior live_mutations_count: `1` - prior profile_identity: `prgs-reviewer` Manual deletion of session-state files is **not** the workflow. This path only clears a lock when the referenced PR is merged/closed.
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#814