Fix AC20 publication preflight worktree forwarding #815

Closed
opened 2026-07-22 17:05:22 -05:00 by jcwalker3 · 1 comment
Owner

Problem

gitea_publish_unpublished_issue_branch accepts an explicit worktree_path but does not pass it into verify_preflight_purity before the #618 workspace guard runs. A daemon rooted at the stable control checkout therefore rejects a valid registered issue worktree before the publication assessor can use it.

The argument is required on the tool signature — there is no caller-side workaround, and no default that could make the omission benign.

Confirmed source evidence

Verified against master 910b6edbdcf0dec64cdf139b1a1dbac4bd38c1ef with mutation_safe: true. This is not a staleness defect.

gitea_mcp_server.py:9105 declares worktree_path: str as a required positional parameter. gitea_mcp_server.py:9176 then calls:

verify_preflight_purity(remote, task=task, org=org, repo=repo)

worktree_path is absent. The tool does not resolve its workspace until line 9180, four lines after preflight has already run and failed:

workspace = os.path.realpath(os.path.abspath((worktree_path or "").strip() or "."))

verify_preflight_purity declares worktree_path in its second positional slot (gitea_mcp_server.py:1340-1342), and threads it into every downstream workspace-resolution layer:

Line Layer receiving worktree_path
1417 _resolve_namespace_mutation_context(worktree_path)
1478 _enforce_canonical_repository_root(worktree_path, remote=remote)
1479 _enforce_root_checkout_guard(worktree_path)
1482 _create_issue_bootstrap_assessment(task, worktree_path)
1483 _enforce_branches_only_author_mutation(worktree_path, ...) — the #618 guard
1486 _enforce_issue_scope_guard(worktree_path, ...)
1493 _run_anti_stomp_preflight(..., worktree_path=worktree_path, ...)

Because None is supplied, all seven fall back to the MCP process root. _enforce_branches_only_author_mutation reaches _resolve_namespace_mutation_context(None), classifies the workspace as the stable control checkout, and raises before the assessor at line 9215 is ever constructed.

The forwarding is otherwise universal

Every other verify_preflight_purity call site that has a caller-supplied worktree forwards it — lines 1804, 3783, 4011, 4482, 10485, 12302, 12460, 14468, 14952, 17363, 17505, 18806, 18866 all pass worktree_path=. Line 9176 is the sole call site that accepts a worktree argument and drops it.

Observed production failure

A recovery worker called the tool with a complete, explicit, valid argument set — registered worktree, full 40-character expected_head, per-file hashes, dry_run=true — and received the #618 stable-control-checkout refusal:

author mutation blocked: workspace is the stable control checkout; author mutation
requires an explicit validated worktree_path or a worktree derived from the active
author issue lock; silent fallback to the control checkout/master is forbidden (#618)

The error text names the exact remedy the caller had already supplied.

Why the existing suite did not catch it

tests/test_issue_812_publish_unpublished_commit.py:115 sets:

self.worktree = os.path.realpath(self.repo)

and line 192 patches mcp_server.PROJECT_ROOT to that same self.repo. The synthetic "issue worktree" is PROJECT_ROOT, and it is checked out on the issue branch rather than on a stable branch. The two paths are byte-identical, so the fallback resolves to the very worktree the argument names, and the dropped argument is structurally invisible. All 36 existing cases pass while the production topology is never constructed.

Production effect

The AC20 tool merged through PR #814 is loaded and runtime parity is healthy, but it cannot publish the preserved #635 branch. The protected branch remains unpublished and exact-owner lock renewal remains blocked.

The capability is therefore unreachable in the only topology it was built for: a long-running daemon rooted at the stable control checkout, publishing a branch that lives in a separate registered worktree under branches/. Entry point B of #812 stays closed in production despite its implementation having landed.

Required acceptance criteria

AC1. The tool must pass the authoritative supplied worktree_path into verify_preflight_purity and every applicable workspace-resolution layer before the #618 guard evaluates the workspace.

AC2. Preflight must validate the explicit registered issue worktree rather than falling back to the stable control checkout.

AC3. The fix must not weaken #618 for missing, empty, malformed, unregistered, foreign, or mismatched worktree paths.

AC4. Both dry-run and executing publication paths must use the same validated worktree resolution.

AC5. The author must remain unable to publish from the stable control checkout when no valid explicit issue worktree is supplied.

AC6. Repository, issue, branch, identity, profile, expected HEAD, durable lock ownership, cleanliness, content hashes, remote ancestry, and read-after-write protections from PR #814 must remain intact.

AC7. Add a production-shaped regression test in which:

  • PROJECT_ROOT represents the stable control checkout;
  • the valid registered issue worktree exists outside PROJECT_ROOT;
  • the MCP tool is called with that explicit worktree_path;
  • preflight and publication operate on that worktree;
  • no test patch makes the issue worktree appear to be PROJECT_ROOT.

AC8. Add negative regression coverage proving omitted and invalid worktree paths still fail closed.

AC9. The protected #635 worktree, branch, commit, hashes, and durable lock must not be modified while implementing or testing this fix.

AC10. Documentation/tool inventory must be regenerated only if the canonical workflow proves the public tool contract changed.

Scope boundary

Narrow. This is an argument-forwarding correction plus the regression coverage that proves the production topology. Explicitly out of scope: any redesign of branch publication, any AC21 implementation, any lock-reclamation change, any role-policy weakening, and any unrelated cleanup or refactoring.

Linkage

  • #812 — closed. Specified AC20. Completed work; this issue does not reopen it, extend its scope, or claim membership in its implementation.
  • #814 — merged at 910b6edbdcf0. Landed the AC20 tool and branch_publish.py. Completed work. This issue is a newly discovered production regression in what that PR shipped, tracked separately.
  • #635 — protected evidence and payload. Branch feat/issue-635-project-registry-api, worktree branches/issue-635-project-registry-api, commit b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8. Must not be read into, written to, published, reset, cleaned, or otherwise disturbed by this work. It is the beneficiary of the fix, never its test fixture.
  • #618 — closed. Supplies the author-worktree guard whose strictness AC3 and AC5 preserve.
  • #735 — closed. Same defect class (an explicit argument dropped at the preflight boundary), different argument: it propagated explicit org/repo through author mutation call sites. Prior art for the correction shape, not for this call site.
  • #733 — closed. Same defect class again, for delete_branch anti-stomp preflight. Prior art only.
  • #713 — open. The registered-worktree proof requirement that AC2's validation must satisfy rather than route around.

Machine-verifiable retry condition

The defect is resolved when, from a daemon rooted at the stable control checkout on a stable branch, with a distinct registered issue worktree holding a clean unpublished local commit and a durable lock naming the caller:

gitea_publish_unpublished_issue_branch(
    issue_number=N,
    branch_name="<issue branch>",
    worktree_path="<registered worktree, outside PROJECT_ROOT>",
    expected_head="<full 40-char SHA>",
    remote="prgs", org=..., repo=..., git_remote_name=...,
    dry_run=True,
)
→ success: true, and the reported evidence names the supplied worktree

and the same call with worktree_path omitted, blank, unregistered, or resolving to the stable control checkout still fails closed under #618.

Canonical issue state

STATE: ready
WHO_IS_NEXT: user
BLOCKED_ROLE: none
NEXT_ACTION: Implement AC1-AC10 in a fresh issue-numbered worktree under branches/ from current master, then open a PR closing this issue
NEXT_PROMPT: Forward the supplied worktree_path into verify_preflight_purity in gitea_publish_unpublished_issue_branch, add production-shaped regression coverage where PROJECT_ROOT is the stable control checkout and the registered issue worktree is a distinct path, keep every #618 refusal intact, never touch branches/issue-635-project-registry-api; PR; stop
## Problem `gitea_publish_unpublished_issue_branch` accepts an explicit `worktree_path` but does not pass it into `verify_preflight_purity` before the #618 workspace guard runs. A daemon rooted at the stable control checkout therefore rejects a valid registered issue worktree before the publication assessor can use it. The argument is **required** on the tool signature — there is no caller-side workaround, and no default that could make the omission benign. ### Confirmed source evidence Verified against master `910b6edbdcf0dec64cdf139b1a1dbac4bd38c1ef` with `mutation_safe: true`. This is not a staleness defect. `gitea_mcp_server.py:9105` declares `worktree_path: str` as a required positional parameter. `gitea_mcp_server.py:9176` then calls: ```python verify_preflight_purity(remote, task=task, org=org, repo=repo) ``` `worktree_path` is absent. The tool does not resolve its workspace until **line 9180**, four lines *after* preflight has already run and failed: ```python workspace = os.path.realpath(os.path.abspath((worktree_path or "").strip() or ".")) ``` `verify_preflight_purity` declares `worktree_path` in its second positional slot (`gitea_mcp_server.py:1340-1342`), and threads it into every downstream workspace-resolution layer: | Line | Layer receiving `worktree_path` | |---|---| | 1417 | `_resolve_namespace_mutation_context(worktree_path)` | | 1478 | `_enforce_canonical_repository_root(worktree_path, remote=remote)` | | 1479 | `_enforce_root_checkout_guard(worktree_path)` | | 1482 | `_create_issue_bootstrap_assessment(task, worktree_path)` | | 1483 | `_enforce_branches_only_author_mutation(worktree_path, ...)` — the #618 guard | | 1486 | `_enforce_issue_scope_guard(worktree_path, ...)` | | 1493 | `_run_anti_stomp_preflight(..., worktree_path=worktree_path, ...)` | Because `None` is supplied, all seven fall back to the MCP process root. `_enforce_branches_only_author_mutation` reaches `_resolve_namespace_mutation_context(None)`, classifies the workspace as the stable control checkout, and raises before the assessor at line 9215 is ever constructed. ### The forwarding is otherwise universal Every other `verify_preflight_purity` call site that has a caller-supplied worktree forwards it — lines 1804, 3783, 4011, 4482, 10485, 12302, 12460, 14468, 14952, 17363, 17505, 18806, 18866 all pass `worktree_path=`. Line 9176 is the sole call site that accepts a worktree argument and drops it. ### Observed production failure A recovery worker called the tool with a complete, explicit, valid argument set — registered worktree, full 40-character `expected_head`, per-file hashes, `dry_run=true` — and received the #618 stable-control-checkout refusal: ``` author mutation blocked: workspace is the stable control checkout; author mutation requires an explicit validated worktree_path or a worktree derived from the active author issue lock; silent fallback to the control checkout/master is forbidden (#618) ``` The error text names the exact remedy the caller had already supplied. ### Why the existing suite did not catch it `tests/test_issue_812_publish_unpublished_commit.py:115` sets: ```python self.worktree = os.path.realpath(self.repo) ``` and line 192 patches `mcp_server.PROJECT_ROOT` to that same `self.repo`. The synthetic "issue worktree" **is** `PROJECT_ROOT`, and it is checked out on the issue branch rather than on a stable branch. The two paths are byte-identical, so the fallback resolves to the very worktree the argument names, and the dropped argument is structurally invisible. All 36 existing cases pass while the production topology is never constructed. ## Production effect The AC20 tool merged through PR #814 is loaded and runtime parity is healthy, but it cannot publish the preserved #635 branch. The protected branch remains unpublished and exact-owner lock renewal remains blocked. The capability is therefore unreachable in the only topology it was built for: a long-running daemon rooted at the stable control checkout, publishing a branch that lives in a separate registered worktree under `branches/`. Entry point B of #812 stays closed in production despite its implementation having landed. ## Required acceptance criteria **AC1.** The tool must pass the authoritative supplied `worktree_path` into `verify_preflight_purity` and every applicable workspace-resolution layer before the #618 guard evaluates the workspace. **AC2.** Preflight must validate the explicit registered issue worktree rather than falling back to the stable control checkout. **AC3.** The fix must not weaken #618 for missing, empty, malformed, unregistered, foreign, or mismatched worktree paths. **AC4.** Both dry-run and executing publication paths must use the same validated worktree resolution. **AC5.** The author must remain unable to publish from the stable control checkout when no valid explicit issue worktree is supplied. **AC6.** Repository, issue, branch, identity, profile, expected HEAD, durable lock ownership, cleanliness, content hashes, remote ancestry, and read-after-write protections from PR #814 must remain intact. **AC7.** Add a production-shaped regression test in which: - `PROJECT_ROOT` represents the stable control checkout; - the valid registered issue worktree exists outside `PROJECT_ROOT`; - the MCP tool is called with that explicit `worktree_path`; - preflight and publication operate on that worktree; - no test patch makes the issue worktree appear to be `PROJECT_ROOT`. **AC8.** Add negative regression coverage proving omitted and invalid worktree paths still fail closed. **AC9.** The protected `#635` worktree, branch, commit, hashes, and durable lock must not be modified while implementing or testing this fix. **AC10.** Documentation/tool inventory must be regenerated only if the canonical workflow proves the public tool contract changed. ## Scope boundary Narrow. This is an argument-forwarding correction plus the regression coverage that proves the production topology. Explicitly out of scope: any redesign of branch publication, any AC21 implementation, any lock-reclamation change, any role-policy weakening, and any unrelated cleanup or refactoring. ## Linkage - **#812** — closed. Specified AC20. Completed work; this issue does not reopen it, extend its scope, or claim membership in its implementation. - **#814** — merged at `910b6edbdcf0`. Landed the AC20 tool and `branch_publish.py`. Completed work. This issue is a newly discovered production regression in what that PR shipped, tracked separately. - **#635** — protected evidence and payload. Branch `feat/issue-635-project-registry-api`, worktree `branches/issue-635-project-registry-api`, commit `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8`. Must not be read into, written to, published, reset, cleaned, or otherwise disturbed by this work. It is the beneficiary of the fix, never its test fixture. - **#618** — closed. Supplies the author-worktree guard whose strictness AC3 and AC5 preserve. - **#735** — closed. Same defect class (an explicit argument dropped at the preflight boundary), different argument: it propagated explicit `org`/`repo` through author mutation call sites. Prior art for the correction shape, not for this call site. - **#733** — closed. Same defect class again, for `delete_branch` anti-stomp preflight. Prior art only. - **#713** — open. The registered-worktree proof requirement that AC2's validation must satisfy rather than route around. ## Machine-verifiable retry condition The defect is resolved when, from a daemon rooted at the stable control checkout on a stable branch, with a distinct registered issue worktree holding a clean unpublished local commit and a durable lock naming the caller: ```text gitea_publish_unpublished_issue_branch( issue_number=N, branch_name="<issue branch>", worktree_path="<registered worktree, outside PROJECT_ROOT>", expected_head="<full 40-char SHA>", remote="prgs", org=..., repo=..., git_remote_name=..., dry_run=True, ) → success: true, and the reported evidence names the supplied worktree ``` and the same call with `worktree_path` omitted, blank, unregistered, or resolving to the stable control checkout still fails closed under #618. ## Canonical issue state ```text STATE: ready WHO_IS_NEXT: user BLOCKED_ROLE: none NEXT_ACTION: Implement AC1-AC10 in a fresh issue-numbered worktree under branches/ from current master, then open a PR closing this issue NEXT_PROMPT: Forward the supplied worktree_path into verify_preflight_purity in gitea_publish_unpublished_issue_branch, add production-shaped regression coverage where PROJECT_ROOT is the stable control checkout and the registered issue worktree is a distinct path, keep every #618 refusal intact, never touch branches/issue-635-project-registry-api; PR; stop ```
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-22 17:28:39 -05:00
Author
Owner

[THREAD STATE LEDGER]

What is true now

Author work unit is complete. PR #817 is in the OPEN state against master, head 95a5eb254f7758b694a26b3f5629558cb88db328, mergeable true, author jcwalker3, closing #815. The remote branch head is verified equal to the committed head. Full suite: 11 failed / 4375 passed / 6 skipped / 537 subtests — the 11 are the documented pre-existing drift baseline, unchanged in count and identity.

  • Server-side decision state: PR #817 in OPEN state, no review verdict recorded, issue #815 carries label status:pr-open.
  • Local verdict/state: implementation plus regression are complete and verified; new suite 21 passed / 4 subtests; reverting only the source hunk turns 3 tests red, restoring it turns them green.

What changed

Commit 95a5eb2 on fix/issue-815-preflight-worktree-forwarding:

  • gitea_mcp_server.py (+18 / −2) — gitea_publish_unpublished_issue_branch now resolves workspace once and forwards it as verify_preflight_purity(worktree_path=…) before the #618 guard. Root cause: line 9176 dropped the required worktree_path, resolved only at line 9180, so preflight fell back to the process root and a control-checkout-rooted daemon refused a valid registered worktree. Sole call site of 14 that dropped its worktree.
  • tests/test_issue_815_preflight_worktree_forwarding.py (new, 21 cases) — a #735-style capture, a faithful no-session-lock production reproduction that fails on the unpatched source, an end-to-end control-rooted publication with no PROJECT_ROOT masking, and negative coverage preserving every #618 and assessor refusal.

What is blocked

Nothing is blocked for the author role; the work unit is finished. Merge is pending independent review of PR #817.

  • Blocker classification: no blocker

Who/what acts next

  • Next actor: reviewer (prgs-reviewer).
  • Required action: review PR #817 against #815 AC1–AC10 — confirm worktree_path reaches verify_preflight_purity before the #618 guard, that every #618 and assessor refusal is preserved, that the new regression fails on the unpatched source, and that branches/issue-635-project-registry-api was untouched — then approve and merge, or request changes.

Do not do:

  • Do not reopen or modify #812; it correctly tracks the landed AC20 implementation.
  • Do not read, write, reset, clean, publish, rebase, or otherwise disturb branches/issue-635-project-registry-api; it stays at b2f6e9a, remote branch absent, durable lock unmutated.
  • Do not retry gitea_publish_unpublished_issue_branch against #635 while reviewing this PR.
  • Do not regenerate docs/mcp-tool-inventory.md; the public tool contract is unchanged.

Canonical Issue State

STATE: pr-open
WHO_IS_NEXT: reviewer
NEXT_ACTION: Review PR #817 against #815 AC1-AC10 and merge if clean
NEXT_PROMPT:

Review PR #817 (fix/issue-815-preflight-worktree-forwarding @ 95a5eb2, base master, Closes #815); confirm worktree_path forwarding before the #618 guard, preserved #618/assessor refusals, a regression that fails on the unpatched source, and #635 untouched; then approve and merge, or request changes.

WHAT_HAPPENED: Forwarded the required worktree_path into verify_preflight_purity before the #618 guard in gitea_publish_unpublished_issue_branch, added a 21-case regression, committed 95a5eb2, and opened PR #817.
WHY: The tool resolved worktree_path only after preflight (line 9176 dropped it, line 9180 resolved it), so preflight fell back to the process root and a control-checkout-rooted daemon refused a valid registered issue worktree before the assessor ran — leaving the #635 branch unpublishable in production despite AC20 having landed.
RELATED_PRS: #817 (this fix, open); #814 (landed AC20); #735 and #733 (same defect class for org/repo, landed).
BLOCKERS: none
VALIDATION: New suite 21 passed / 4 subtests; reverting only the source hunk turns 3 tests red and restoring it turns them green; related canonical suites 123 passed / 100 subtests; full suite 11 failed / 4375 passed / 6 skipped / 537 subtests with the 11 failures identical to the documented pre-existing drift baseline (test_commit_payloads.py, test_issue_702_review_findings_f1_f6.py, test_mcp_server.py, test_post_merge_moot_lease.py, test_reconciler_supersession_close.py); #812 remains closed and #635 untouched.
LAST_UPDATED_BY: prgs-author (jcwalker3)
BLOCKED_ROLE: none

NEXT_ELIGIBLE_ROLE: reviewer

[THREAD STATE LEDGER] ### What is true now Author work unit is complete. PR #817 is in the OPEN state against `master`, head `95a5eb254f7758b694a26b3f5629558cb88db328`, mergeable true, author `jcwalker3`, closing #815. The remote branch head is verified equal to the committed head. Full suite: 11 failed / 4375 passed / 6 skipped / 537 subtests — the 11 are the documented pre-existing drift baseline, unchanged in count and identity. - Server-side decision state: PR #817 in OPEN state, no review verdict recorded, issue #815 carries label `status:pr-open`. - Local verdict/state: implementation plus regression are complete and verified; new suite 21 passed / 4 subtests; reverting only the source hunk turns 3 tests red, restoring it turns them green. ### What changed Commit `95a5eb2` on `fix/issue-815-preflight-worktree-forwarding`: - `gitea_mcp_server.py` (+18 / −2) — `gitea_publish_unpublished_issue_branch` now resolves `workspace` once and forwards it as `verify_preflight_purity(worktree_path=…)` before the #618 guard. Root cause: line 9176 dropped the required `worktree_path`, resolved only at line 9180, so preflight fell back to the process root and a control-checkout-rooted daemon refused a valid registered worktree. Sole call site of 14 that dropped its worktree. - `tests/test_issue_815_preflight_worktree_forwarding.py` (new, 21 cases) — a #735-style capture, a faithful no-session-lock production reproduction that fails on the unpatched source, an end-to-end control-rooted publication with no PROJECT_ROOT masking, and negative coverage preserving every #618 and assessor refusal. ### What is blocked Nothing is blocked for the author role; the work unit is finished. Merge is pending independent review of PR #817. - Blocker classification: no blocker ### Who/what acts next - Next actor: reviewer (`prgs-reviewer`). - Required action: review PR #817 against #815 AC1–AC10 — confirm `worktree_path` reaches `verify_preflight_purity` before the #618 guard, that every #618 and assessor refusal is preserved, that the new regression fails on the unpatched source, and that `branches/issue-635-project-registry-api` was untouched — then approve and merge, or request changes. ### Do not do: - Do not reopen or modify #812; it correctly tracks the landed AC20 implementation. - Do not read, write, reset, clean, publish, rebase, or otherwise disturb `branches/issue-635-project-registry-api`; it stays at `b2f6e9a`, remote branch absent, durable lock unmutated. - Do not retry `gitea_publish_unpublished_issue_branch` against #635 while reviewing this PR. - Do not regenerate `docs/mcp-tool-inventory.md`; the public tool contract is unchanged. ## Canonical Issue State STATE: pr-open WHO_IS_NEXT: reviewer NEXT_ACTION: Review PR #817 against #815 AC1-AC10 and merge if clean NEXT_PROMPT: ```text Review PR #817 (fix/issue-815-preflight-worktree-forwarding @ 95a5eb2, base master, Closes #815); confirm worktree_path forwarding before the #618 guard, preserved #618/assessor refusals, a regression that fails on the unpatched source, and #635 untouched; then approve and merge, or request changes. ``` WHAT_HAPPENED: Forwarded the required worktree_path into verify_preflight_purity before the #618 guard in gitea_publish_unpublished_issue_branch, added a 21-case regression, committed 95a5eb2, and opened PR #817. WHY: The tool resolved worktree_path only after preflight (line 9176 dropped it, line 9180 resolved it), so preflight fell back to the process root and a control-checkout-rooted daemon refused a valid registered issue worktree before the assessor ran — leaving the #635 branch unpublishable in production despite AC20 having landed. RELATED_PRS: #817 (this fix, open); #814 (landed AC20); #735 and #733 (same defect class for org/repo, landed). BLOCKERS: none VALIDATION: New suite 21 passed / 4 subtests; reverting only the source hunk turns 3 tests red and restoring it turns them green; related canonical suites 123 passed / 100 subtests; full suite 11 failed / 4375 passed / 6 skipped / 537 subtests with the 11 failures identical to the documented pre-existing drift baseline (test_commit_payloads.py, test_issue_702_review_findings_f1_f6.py, test_mcp_server.py, test_post_merge_moot_lease.py, test_reconciler_supersession_close.py); #812 remains closed and #635 untouched. LAST_UPDATED_BY: prgs-author (jcwalker3) BLOCKED_ROLE: none NEXT_ELIGIBLE_ROLE: reviewer
sysadmin removed the status:pr-open label 2026-07-22 18:34:33 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

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