No sanctioned recovery for a dirty author worktree: uncommitted implementation cannot be checkpointed, and every publication gate demands the clean worktree only publication can produce #812

Closed
opened 2026-07-22 15:10:29 -05:00 by jcwalker3 · 4 comments
Owner

Summary

An author worktree holding a complete, test-passing implementation cannot be published by any sanctioned path. The workspace and lease gates are individually correct and jointly form a closed cycle.

The deadlock has two proven entry points, distinguished in the section "Two recovery entry points" below. Both are in scope for this issue. Entry point A was the original filing; entry point B was proven live on 2026-07-22 and recorded in comment 14233.

  • gitea_lock_issue refuses to bind a workspace that has dirty tracked files.
  • gitea_commit_files derives its author workspace from that issue lock and exposes no worktree_path of its own, so without the lock it refuses under #618.
  • Once the content is committed locally instead, the dirty-tree gate clears and a second, independent gate re-materialises behind it: exact-owner lease renewal demands a published remote head, and publication is itself lock-derived.
  • Publishing is what would satisfy both gates. The lock that publishing requires is the thing each gate independently forbids.

The author namespace cannot provision, register, refresh, publish, or checkpoint a worktree, so it cannot break the cycle from inside. The local-git escape is refused by policy. The result is that valid work is stranded with no sanctioned route forward and no sanctioned route to safely set it aside.

Observed live against master 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8 with mutation_safe: true. Live-master parity is green; this is not a staleness defect.

Protected content warning

branches/issue-635-project-registry-api currently holds the only known copy of the #635 Phase 1 implementation.

Item Value
Issue #635 — Web Console: Project registry API evolution (Phase 1)
Branch feat/issue-635-project-registry-api
Worktree branches/issue-635-project-registry-api
Worktree HEAD as originally filed 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8, tree dirty
Worktree HEAD as of comment 14233 b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8, tree clean
Protected files docs/webui-local-dev.md, docs/webui-project-registry-api.md, tests/test_webui_project_registry.py, webui/app.py, webui/data/projects.registry.json, webui/project_registry.py, webui/project_views.py
Size 7 files, +998 / -93
Remote branch does not exist — gitea_get_file on that ref returns HTTP 404
Open PR none
Durable status ledgers issue #635 comments 14185, 14215, 14225; issue #812 comments 14233, 14241

The content advanced from uncommitted working-tree state to local commit b2f6e9a between comment 14225 and comment 14233. It is still unpublished, so it is still the only copy and still at risk. Per-file SHA-256 values are recorded in comment 14233 and are the reference set for any hash verification this issue's capability performs.

This worktree must not be reset, cleaned, checked out, rebased, stashed, or otherwise disturbed by any implementation of this issue. Discarding it destroys the work. It is evidence and payload at the same time. Any implementation must be developed and exercised against synthetic fixtures, never against this path.

Verification that the content is sound, so this is a publication defect and not a quality one:

  • pytest tests/test_webui_project_registry.py — 30 passed, 22 subtests passed.
  • pytest tests/ -k "webui or registry or project" — 199 passed, 166 subtests passed.
  • Full suite — 4340 passed, 11 failed; the identical 11 failures reproduce on clean master 9eb0f29 in branches/baseline-master-9eb0f29-issue-633, so they are pre-existing.
  • Independent review against #635's five acceptance criteria: all five MET, no out-of-scope files, no credentials, no write endpoints, no fail-open paths.

Two recovery entry points

The capability specified here must handle both of the following without content loss. Handling only one leaves the other stranded, and the two are reached by the same work at different moments in its life.

Entry point A — dirty and uncommitted

The protected content exists only as working-tree modifications plus untracked files. Nothing has been committed.

  • Refusing gate: gitea_lock_issue fails the #510 namespace workspace binding check on dirty tracked files. The dirty tracked files named by the guard are the implementation it is protecting.
  • Required disposition: preserve → prepare → restore, with per-file content hashes recorded before anything is touched, and hash verification after restore.

Reproduction, from an author session (prgs-author / jcwalker3), remote prgs, repo Scaled-Tech-Consulting/Gitea-Tools, parity green:

gitea_lock_issue(issue_number=635,
                 branch_name="feat/issue-635-project-registry-api",
                 worktree_path=".../branches/issue-635-project-registry-api")
→ RuntimeError / reason_code: internal_error
  "Namespace workspace binding blocked (author namespace, #510): resolved workspace
   '[PATH]' via worktree_path argument. Dirty tracked files in active task workspace:
   docs/webui-local-dev.md, tests/test_..."
gitea_commit_files(branch="master",
                   new_branch="feat/issue-635-project-registry-api",
                   files=[... 7 entries via local_path ...])
→ RuntimeError / reason_code: internal_error
  "Durable author worktree resolution blocked (#618): author mutation blocked:
   workspace is the stable control checkout; author mutation requires an explicit
   validated worktree_path or a worktree derived ..."

gitea_commit_files accepts no worktree_path parameter, so the caller cannot supply the binding the error asks for. It derives the workspace from the issue lock that the first step refuses to grant.

Entry point B — clean, locally committed, unpublished

The protected content has been committed to the issue branch inside the registered worktree. The tree is clean, the worktree is on the correct branch, and the commit descends from the recorded base. This is the state the #635 worktree is in today, at commit b2f6e9a.

Committing the work does not resolve the deadlock. It clears the first gate and exposes a second one. Three predicates now interlock, each individually correct:

  1. Exact-owner renewal requires a published remote head. issue_lock_renewal.assess_exact_owner_lease_renewal lines 328-332 refuse with "remote branch head could not be observed; an unpublished branch cannot prove exact-owner renewal." Its AC4 clean-worktree condition at lines 311-323 is satisfied; only the published-head condition fails.
  2. Publication requires the author lock. gitea_commit_files and gitea_create_pr are both lock-derived under #618. Neither can publish the branch and thereby create the remote head that predicate 1 demands.
  3. Expired-lock reclamation refuses. issue_lock_store.assess_expired_lock_reclaim lines 455-465 require owner_pid dead OR worktree missing. The recorded owner pid is a live mcp_server.py daemon, and the registered worktree is present, so neither disjunct holds. This predicate can never become satisfiable while that daemon runs; it is not a transient condition that waiting clears.

Observed refusal, executed 2026-07-22 after gitea_whoami then gitea_resolve_task_capability(task="lock_issue") returned allowed_in_current_session: true:

gitea_lock_issue(issue_number=635,
                 branch_name="feat/issue-635-project-registry-api",
                 worktree_path=".../branches/issue-635-project-registry-api")
→ success: false, reason_code: internal_error, retryable: false
  "Issue #635 has an expired author_issue_work lease on branch
   'feat/issue-635-project-registry-api' from worktree '[PATH]'.
   Recovery review is required before takeover (fail closed);
   exact-owner lease r..."

Reached through issue_lock_store.assess_same_issue_lease_conflict lines 520-540. same_owner is satisfied — recorded branch and recorded worktree both match the caller. The refusal therefore rests entirely on predicates 1 and 3 above.

  • Required disposition: a sanctioned publication or renewal step. Preserve, prepare, and restore are neither sufficient nor, on their own, relevant here: there is nothing dirty to preserve and the worktree already satisfies every cleanliness requirement.

The correction this distinction forces

Satisfying only the clean-worktree requirement is insufficient. A capability that ends at "produce a clean, current, registered worktree and then call gitea_lock_issue" does not unblock entry point B, and entry point B is where any work that has progressed normally will end up. The #635 worktree is already clean, already on the correct branch, already registered, and already carries the verified content — and the lock still fails closed.

A recovery capability is only complete when it can carry content from either entry state through to a published remote head and an open PR, without content loss, and without requiring the operator to perform filesystem or git surgery.

Blast radius: this is not one stranded issue

Enumerating every registered worktree on a (fix|feat|docs|chore)/issue-<N>-* branch and testing the lock preconditions — exists, on the branch, base-equivalent to current master, no dirty tracked files — gives 0 of 22 satisfying all of them in the census taken for comment 14233, across 60 registered worktrees total.

Consequently no author code mutation is currently possible anywhere in this repository, for any issue, whether resuming existing work or starting new work. The queue is not idle — it is unreachable.

This issue cannot bootstrap itself out of that state either: implementing the capability requires an eligible worktree, and the capability that would provision one is the capability that cannot be built without one.

Why existing issues do not cover this

Each of these was read in full. This issue deliberately claims only the uncovered remainder.

  • #618 (closed) — Author worktree binding can disappear mid-session and mutation guards enforce it inconsistently. Owns guard consistency, and its AC5 resolves the blocker by emitting bound worktree missing; operator must recreate or repoint the worktree and reconnect. It makes the operator the recovery mechanism. It does not supply one, and its resolution path is exactly what is unavailable to an unattended worker. This issue supplies the missing operation that #618's AC5 assumes exists.
  • #772 (closed) — Recover clean unpublished author work after the owning session exits. The nearest prior art, and it stops one step short. Its AC3 requires the registered worktree to be clean with a HEAD descending from the recorded base; its AC4 explicitly rejects a dirty worktree. It recovers work whose owning session is dead. Entry point B has a live daemon pid recorded as owner, which is the opposite liveness trigger, so #772's remedy cannot reach it.
  • #760 (closed) — Exact-owner lease renewal. Supplies the renewal disposition that entry point B needs, but its AC5/AC6 assume a published head and therefore fail closed for any branch that has never been pushed. Entry point B is precisely the case its evidence model cannot express.
  • #749 (closed) — No sanctioned first-mutation path. Same deadlock shape, different phase: pre-issue create_issue bootstrap. Its AC3 states the guard must keep failing closed for every mutation touching local state and that the exemption "does not generalize". It therefore explicitly declines to cover the post-issue commit/lock phase this issue occupies.
  • #716 (open) — Prevent MCP session state from dirtying Git worktrees and provide sanctioned legacy-state recovery. Concerns MCP session-state residue (.mcp_session_*) dirtying the control checkout, recovered by relocating that state. It does not address author implementation content in a task worktree, worktree provisioning, base refresh, claim rebinding, publication, or checkpointing.
  • #713 (open) — Require registered Git worktree proof for branches-only mutation authorization. Makes the guard stricter. This issue must satisfy it rather than route around it: any operation added here must produce a genuinely registered worktree, never an improvised directory.
  • #711, #716 — open, adjacent. Not merged into this scope.
  • #753 / #755 / #768 (closed) — dead-session and descendant recovery dispositions, all deriving ownership from a published remote or PR head. There is no published artifact in either entry point.

The core correction

Demanding "current and clean" is not a recovery mechanism when the only copy of the implementation is the content itself.

A precondition that can only be satisfied by destroying the asset it guards is not a precondition — it is an unrecoverable state. Every existing recovery disposition re-derives ownership from already-published state, which is sound for published claims and vacuous for unpublished ones. This issue asks for the inverse: a sanctioned operation that creates the publishable state from preserved local content, rather than one that demands the publishable state already exist.

Recovery must be an explicit MCP / control-plane operation with its own audit record. It must not be an instruction to the operator, a documentation note, a bypass flag, or an environment rebinding.

Required behavior

Add a sanctioned, role-gated, auditable author worktree recovery and checkpoint capability, reachable entirely through native MCP tools. It must be able to:

  1. Preserve dirty tracked and untracked implementation content, byte-for-byte, before touching anything.
  2. Create or register an eligible issue worktree under branches/, as a genuinely registered Git worktree satisfying #713.
  3. Refresh that worktree's base safely to current master.
  4. Restore / materialize the preserved changes into it without overwriting or silently merging them.
  5. Acquire or rebind the author claim for the issue, atomically and with CAS/generation protection.
  6. Publish a portable checkpoint or commit from the restored content, including from an existing local commit that has never been pushed.
  7. Verify the resulting branch and worktree against what was preserved.
  8. Fail closed on conflicts, ambiguous ownership, or any content-loss risk.
  9. Operate without git, gh, tea, curl, direct HTTP/API calls, tokens, credential workarounds, or manual filesystem intervention by a human.

Acceptance criteria

AC1. A native MCP operation preserves the full dirty state of an author worktree — modified tracked files, staged changes, and untracked files — into a durable, addressable checkpoint outside the working tree, with per-file content hashes recorded. Ignored files are excluded by default and the exclusion is reported, not silent.

AC2. The preserve step is non-destructive: it never modifies, stages, resets, cleans, checks out, stashes, or removes anything in the source worktree. Running it twice on unchanged content is idempotent and yields the same content hashes.

AC3. A native MCP operation creates or registers an eligible issue worktree under branches/ for a given issue and branch name, as a genuinely registered worktree (present in the worktree registry), satisfying #713's proof requirement. It never satisfies the guard with an unregistered directory.

AC4. A native MCP operation refreshes a task worktree's base to current master and reports the before/after base. It refuses to do so when doing so would discard local content, and it never rebases or force-moves published history.

AC5. A native MCP operation materializes a preserved checkpoint into a prepared worktree and verifies every restored file against its recorded hash. Any hash mismatch, missing file, or pre-existing conflicting content fails closed with the specific paths named, leaving both the checkpoint and the target intact.

AC6. Restore never overwrites existing differing content implicitly. A collision is a fail-closed condition reported per path, not a merge, not a "theirs wins", and not a silent skip.

AC7. After a successful restore, gitea_lock_issue can bind the prepared worktree, and the #618 lock-derived resolution then permits gitea_commit_files and gitea_create_pr with no environment rebinding, no direct git push, and no web-UI intervention. A clean prepared worktree alone does not discharge this criterion — see AC19 through AC22, which govern the case where the worktree is already clean and the lock still fails closed.

AC8. Claim acquisition or rebinding during recovery is atomic and CAS/generation-protected, so two concurrent sessions cannot both recover the same claim. Ownership is derived from server-side control-plane evidence only — never from a caller-supplied token, boolean, or self-declared eligibility.

AC9. Recovery fails closed on: a live competing owner; a foreign identity or profile; a branch or repository mismatch; a competing open PR or remote branch claiming the issue; ambiguous or contradictory control-plane records; and any detected risk of content loss.

AC10. Every recovery run produces a durable audit record naming the issue, branch, source worktree, checkpoint identifier, per-file hashes, the acting identity and profile, and the resulting commit or branch head.

AC11. A read-only assessor reports the same recovery decision and the same supporting evidence as the mutating path. Divergence between assessor and mutator is itself a defect.

AC12. A dry-run mode reports exactly what would be preserved, created, refreshed, restored, and published, and mutates nothing.

AC13. The whole flow is reachable using only sanctioned MCP tools. No step requires git, gh, tea, curl, direct API calls, tokens, or a human editing the filesystem. gitea_assess_gitea_operation_path must report the flow as allowed rather than directing the caller to a shell fallback.

AC14. The blocking guards emit an exact_next_action that names this recovery operation and is satisfiable at the phase it is emitted in. The current messages instruct the caller to supply a clean or base-equivalent worktree at a moment when producing one would destroy the work, and — in entry point B — at a moment when the worktree already is clean and current; that guidance must be replaced in both cases.

AC15. Guard strictness does not regress: dirty-worktree, missing-worktree, non-base-equivalent, and control-checkout mutations all still fail closed for any caller that has not gone through the sanctioned recovery operation. The recovery path is an explicit operation, never a weakening of the guards or a general exemption.

AC16. Regression tests cover, against synthetic fixtures only: dirty tracked files alone; untracked files alone; both together; staged plus unstaged changes; a worktree that does not exist; a worktree behind master; a worktree at master; restore hash mismatch; restore collision; a competing live claim; a foreign profile; dry-run versus apply; assessor/mutator agreement; and the full end-to-end path from dirty content through to an open PR.

AC17. No test, fixture, or implementation step reads from, writes to, or otherwise disturbs branches/issue-635-project-registry-api. The #635 content is preserved evidence for the duration of this work.

AC18. Documentation states the sanctioned recovery path for both entry points and explains why "make it clean and current" is not itself a recovery mechanism — neither when the dirty content is the only copy, nor when the content is already clean, committed, and unpublished.

AC19. The capability explicitly classifies the source worktree into entry point A (dirty, uncommitted) or entry point B (clean, locally committed, unpublished), reports which it detected and the evidence for that classification, and refuses to guess when the evidence is ambiguous or contradictory.

AC20. For entry point B, a native MCP operation publishes an existing unpublished local commit on a registered issue worktree to the remote branch, creating the remote head, without first requiring the author lock that the unpublished state makes unobtainable. It fails closed if the remote branch already exists with a head that is not an ancestor of the local commit, if any competing open PR claims the issue, or if the acting identity is not the recorded claimant.

AC21. For entry point B, once the remote head exists, exact-owner lease renewal succeeds without an operator abandoning or hand-clearing the durable lock, and the recorded owner pid belonging to a live long-running daemon is neither necessary nor sufficient evidence in that decision. A recorded pid that is the MCP daemon rather than a live author client must not by itself make a lock permanently unrecoverable.

AC22. Entry point B is verified end to end against synthetic fixtures: a registered worktree, clean, on the issue branch, holding an unpublished local commit descending from the recorded base, with an expired author_issue_work lease whose recorded owner pid is alive and whose worktree is present — recovered through to a published remote head, a successful gitea_lock_issue, and an open PR, with all content hashes verified equal before and after.

AC23 — Separate recovery records. The implementation must explicitly distinguish and operate on:

  1. The durable issue-lock record used by gitea_lock_issue.
  2. The control-plane workflow lease used by gitea_abandon_workflow_lease.

Recovery of, abandonment of, or mutation to one record must never be treated as proof that the other was released or reclaimed. The operation must identify which record blocks progress, modify only the intended record, and verify the resulting state through a read-after-write check.

AC24 — Truthful recovery evidence. Every recovery decision must derive process identity, liveness, client ownership, worktree existence, and mutation risk from authoritative server-side evidence. An alive MCP daemon PID must never be represented as a dead author process. The recovery model must distinguish:

  • process alive;
  • process dead;
  • live daemon process;
  • active author client;
  • abandoned author client;
  • unknown or ambiguous ownership.

Recovery must fail closed when identity or ownership cannot be proven, without accepting caller-supplied booleans as authoritative evidence.

Validation expectations

  • Focused unit tests per rejection reason in AC9, exercised independently.
  • An MCP-level regression driving native gitea_lock_issue, gitea_commit_files, and gitea_create_pr end to end after recovery, not only assessor functions, for both entry points.
  • An explicit regression proving that the presence of dirty content is not by itself treated as permission: a foreign or mismatched claim with dirty content must still be refused.
  • An explicit regression proving that a clean worktree is not by itself treated as sufficient: the entry point B fixture must fail closed before AC20 lands, and succeed after.
  • Full-suite results reported against the known pre-existing master drift baseline rather than as an absolute pass count. The current baseline at 9eb0f29 is 11 pre-existing failures 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.

Machine-verifiable retry condition

The defect is resolved when both sequences succeed from an author session, without any human filesystem intervention and without git.

Entry point A

1. <preserve op>(issue=N, worktree=<dirty worktree>)
   → checkpoint_id, files recorded with hashes, source worktree byte-identical afterwards
2. <prepare op>(issue=N, branch=<issue branch>)
   → registered worktree, base == current master, clean
3. <restore op>(checkpoint_id, into prepared worktree)
   → all files restored, all hashes verified
4. gitea_lock_issue(issue_number=N, branch_name=<issue branch>,
                    worktree_path=<prepared worktree>)
   → success: true
5. gitea_commit_files(...)        → success: true
6. gitea_create_pr(...)           → PR number returned, closes #N

Entry point B — the live #635 case

1. <classify op>(issue=635, worktree=".../branches/issue-635-project-registry-api")
   → entry_point: B; clean: true; branch: feat/issue-635-project-registry-api;
     local head: b2f6e9a; remote head: absent; 7 files hashed
2. <publish op>(issue=635, branch="feat/issue-635-project-registry-api")
   → remote head created and equal to the local commit; no lock required to reach this
3. gitea_lock_issue(issue_number=635,
                    branch_name="feat/issue-635-project-registry-api",
                    worktree_path=".../branches/issue-635-project-registry-api")
   → success: true, via exact-owner renewal, with no lock abandonment
4. gitea_create_pr(...)           → PR number returned, closes #635
5. <verify op>                    → all 7 published file hashes equal the recorded set

Interim partial check for entry point A: step 4 returning success: true against a prepared worktree whose content hashes equal the preserved set. Interim partial check for entry point B: step 2 producing an observable remote head for a branch that had none.

Linkage and ownership boundary

  • #635 — evidence and payload. Its issue record, branch feat/issue-635-project-registry-api, and worktree branches/issue-635-project-registry-api must not be modified, reallocated, reset, cleaned, or published as part of implementing this issue. Its durable status is recorded in comments 14185, 14215, and 14225, and the entry point B evidence in #812 comments 14233 and 14241.
  • #618 — closed. Supplies the #618 lock-derived author-worktree resolution that AC7 depends on, and the AC5 operator-recovery assumption this issue replaces with a real operation.
  • #772 — closed. Covers the clean-worktree unpublished-claim quadrant for a dead owning session. AC8's ownership-evidence model should follow its AC1/AC2 precedent; do not re-own or regress it. AC21 extends the same evidence model to the live-daemon-pid trigger it deliberately scoped away from.
  • #760 — closed. Supplies exact-owner lease renewal. AC20 and AC21 extend it to the unpublished-head case its AC5/AC6 cannot express; do not weaken its published-head requirement for cases where a head does exist.
  • #749 — closed. Pre-issue bootstrap phase of the same deadlock class. Its AC3 non-generalization boundary is respected here.
  • #713 — open. AC3 must satisfy its registered-worktree proof requirement rather than route around it.
  • #711, #716 — open, adjacent. Not merged into this scope.
  • #628 / PR #795 — a second live instance of the base-equivalence variant, recorded in PR #795 comment 14190. Not owned by this issue.

Canonical issue state

STATE: blocked
WHO_IS_NEXT: user
BLOCKED_ROLE: author
NEXT_ACTION: Authorize an eligible workspace for control-plane work on this issue, then implement AC1-AC24 covering both recovery entry points against synthetic fixtures only
NEXT_PROMPT: Author the sanctioned author-worktree recovery and checkpoint capability per AC1-AC24, covering entry point A (dirty, uncommitted) and entry point B (clean, locally committed, unpublished); never read or modify branches/issue-635-project-registry-api; PR; stop
## Summary An author worktree holding a **complete, test-passing** implementation cannot be published by any sanctioned path. The workspace and lease gates are individually correct and jointly form a closed cycle. The deadlock has **two proven entry points**, distinguished in the section "Two recovery entry points" below. Both are in scope for this issue. Entry point A was the original filing; entry point B was proven live on 2026-07-22 and recorded in comment 14233. - `gitea_lock_issue` refuses to bind a workspace that has dirty tracked files. - `gitea_commit_files` derives its author workspace from that issue lock and exposes no `worktree_path` of its own, so without the lock it refuses under #618. - Once the content is committed locally instead, the dirty-tree gate clears and a **second, independent** gate re-materialises behind it: exact-owner lease renewal demands a published remote head, and publication is itself lock-derived. - Publishing is what would satisfy both gates. The lock that publishing requires is the thing each gate independently forbids. The author namespace cannot provision, register, refresh, publish, or checkpoint a worktree, so it cannot break the cycle from inside. The local-git escape is refused by policy. The result is that valid work is stranded with no sanctioned route forward and no sanctioned route to safely set it aside. Observed live against master `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8` with `mutation_safe: true`. Live-master parity is **green**; this is not a staleness defect. ## Protected content warning `branches/issue-635-project-registry-api` currently holds the **only known copy** of the #635 Phase 1 implementation. | Item | Value | |---|---| | Issue | #635 — Web Console: Project registry API evolution (Phase 1) | | Branch | `feat/issue-635-project-registry-api` | | Worktree | `branches/issue-635-project-registry-api` | | Worktree HEAD **as originally filed** | `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8`, tree dirty | | Worktree HEAD **as of comment 14233** | `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8`, tree **clean** | | Protected files | `docs/webui-local-dev.md`, `docs/webui-project-registry-api.md`, `tests/test_webui_project_registry.py`, `webui/app.py`, `webui/data/projects.registry.json`, `webui/project_registry.py`, `webui/project_views.py` | | Size | 7 files, +998 / -93 | | Remote branch | does not exist — `gitea_get_file` on that ref returns HTTP 404 | | Open PR | none | | Durable status ledgers | issue #635 comments 14185, 14215, 14225; issue #812 comments 14233, 14241 | The content advanced from uncommitted working-tree state to local commit `b2f6e9a` between comment 14225 and comment 14233. **It is still unpublished, so it is still the only copy and still at risk.** Per-file SHA-256 values are recorded in comment 14233 and are the reference set for any hash verification this issue's capability performs. **This worktree must not be reset, cleaned, checked out, rebased, stashed, or otherwise disturbed by any implementation of this issue.** Discarding it destroys the work. It is evidence and payload at the same time. Any implementation must be developed and exercised against synthetic fixtures, never against this path. Verification that the content is sound, so this is a publication defect and not a quality one: - `pytest tests/test_webui_project_registry.py` — 30 passed, 22 subtests passed. - `pytest tests/ -k "webui or registry or project"` — 199 passed, 166 subtests passed. - Full suite — 4340 passed, 11 failed; the identical 11 failures reproduce on clean master `9eb0f29` in `branches/baseline-master-9eb0f29-issue-633`, so they are pre-existing. - Independent review against #635's five acceptance criteria: all five MET, no out-of-scope files, no credentials, no write endpoints, no fail-open paths. ## Two recovery entry points The capability specified here must handle **both** of the following without content loss. Handling only one leaves the other stranded, and the two are reached by the same work at different moments in its life. ### Entry point A — dirty and uncommitted The protected content exists only as working-tree modifications plus untracked files. Nothing has been committed. - Refusing gate: `gitea_lock_issue` fails the #510 namespace workspace binding check on dirty tracked files. The dirty tracked files named by the guard **are** the implementation it is protecting. - Required disposition: **preserve → prepare → restore**, with per-file content hashes recorded before anything is touched, and hash verification after restore. Reproduction, from an author session (`prgs-author` / `jcwalker3`), remote `prgs`, repo `Scaled-Tech-Consulting/Gitea-Tools`, parity green: ``` gitea_lock_issue(issue_number=635, branch_name="feat/issue-635-project-registry-api", worktree_path=".../branches/issue-635-project-registry-api") → RuntimeError / reason_code: internal_error "Namespace workspace binding blocked (author namespace, #510): resolved workspace '[PATH]' via worktree_path argument. Dirty tracked files in active task workspace: docs/webui-local-dev.md, tests/test_..." ``` ``` gitea_commit_files(branch="master", new_branch="feat/issue-635-project-registry-api", files=[... 7 entries via local_path ...]) → RuntimeError / reason_code: internal_error "Durable author worktree resolution blocked (#618): author mutation blocked: workspace is the stable control checkout; author mutation requires an explicit validated worktree_path or a worktree derived ..." ``` `gitea_commit_files` accepts no `worktree_path` parameter, so the caller cannot supply the binding the error asks for. It derives the workspace from the issue lock that the first step refuses to grant. ### Entry point B — clean, locally committed, unpublished The protected content has been committed to the issue branch inside the registered worktree. The tree is clean, the worktree is on the correct branch, and the commit descends from the recorded base. **This is the state the #635 worktree is in today**, at commit `b2f6e9a`. Committing the work does **not** resolve the deadlock. It clears the first gate and exposes a second one. Three predicates now interlock, each individually correct: 1. **Exact-owner renewal requires a published remote head.** `issue_lock_renewal.assess_exact_owner_lease_renewal` lines 328-332 refuse with *"remote branch head could not be observed; an unpublished branch cannot prove exact-owner renewal."* Its AC4 clean-worktree condition at lines 311-323 is **satisfied**; only the published-head condition fails. 2. **Publication requires the author lock.** `gitea_commit_files` and `gitea_create_pr` are both lock-derived under #618. Neither can publish the branch and thereby create the remote head that predicate 1 demands. 3. **Expired-lock reclamation refuses.** `issue_lock_store.assess_expired_lock_reclaim` lines 455-465 require `owner_pid dead OR worktree missing`. The recorded owner pid is a **live `mcp_server.py` daemon**, and the registered worktree is present, so neither disjunct holds. This predicate can never become satisfiable while that daemon runs; it is not a transient condition that waiting clears. Observed refusal, executed 2026-07-22 after `gitea_whoami` then `gitea_resolve_task_capability(task="lock_issue")` returned `allowed_in_current_session: true`: ``` gitea_lock_issue(issue_number=635, branch_name="feat/issue-635-project-registry-api", worktree_path=".../branches/issue-635-project-registry-api") → success: false, reason_code: internal_error, retryable: false "Issue #635 has an expired author_issue_work lease on branch 'feat/issue-635-project-registry-api' from worktree '[PATH]'. Recovery review is required before takeover (fail closed); exact-owner lease r..." ``` Reached through `issue_lock_store.assess_same_issue_lease_conflict` lines 520-540. `same_owner` is satisfied — recorded branch and recorded worktree both match the caller. The refusal therefore rests entirely on predicates 1 and 3 above. - Required disposition: a sanctioned **publication or renewal** step. Preserve, prepare, and restore are neither sufficient nor, on their own, relevant here: there is nothing dirty to preserve and the worktree already satisfies every cleanliness requirement. ### The correction this distinction forces **Satisfying only the clean-worktree requirement is insufficient.** A capability that ends at "produce a clean, current, registered worktree and then call `gitea_lock_issue`" does not unblock entry point B, and entry point B is where any work that has progressed normally will end up. The #635 worktree is already clean, already on the correct branch, already registered, and already carries the verified content — and the lock still fails closed. A recovery capability is only complete when it can carry content from **either** entry state through to a published remote head and an open PR, without content loss, and without requiring the operator to perform filesystem or git surgery. ## Blast radius: this is not one stranded issue Enumerating every registered worktree on a `(fix|feat|docs|chore)/issue-<N>-*` branch and testing the lock preconditions — exists, on the branch, base-equivalent to current master, no dirty tracked files — gives **0 of 22 satisfying all of them** in the census taken for comment 14233, across 60 registered worktrees total. Consequently **no author code mutation is currently possible anywhere in this repository**, for any issue, whether resuming existing work or starting new work. The queue is not idle — it is unreachable. This issue cannot bootstrap itself out of that state either: implementing the capability requires an eligible worktree, and the capability that would provision one is the capability that cannot be built without one. ## Why existing issues do not cover this Each of these was read in full. This issue deliberately claims only the uncovered remainder. * **#618** (closed) — *Author worktree binding can disappear mid-session and mutation guards enforce it inconsistently.* Owns guard **consistency**, and its AC5 resolves the blocker by emitting `bound worktree missing; operator must recreate or repoint the worktree and reconnect`. It makes the operator the recovery mechanism. It does not supply one, and its resolution path is exactly what is unavailable to an unattended worker. This issue supplies the missing operation that #618's AC5 assumes exists. * **#772** (closed) — *Recover clean unpublished author work after the owning session exits.* The nearest prior art, and it stops one step short. Its AC3 requires the registered worktree to be **clean** with a HEAD descending from the recorded base; its AC4 **explicitly rejects a dirty worktree**. It recovers work whose owning session is **dead**. Entry point B has a live daemon pid recorded as owner, which is the opposite liveness trigger, so #772's remedy cannot reach it. * **#760** (closed) — *Exact-owner lease renewal.* Supplies the renewal disposition that entry point B needs, but its AC5/AC6 assume a published head and therefore fail closed for any branch that has never been pushed. Entry point B is precisely the case its evidence model cannot express. * **#749** (closed) — *No sanctioned first-mutation path.* Same deadlock **shape**, different phase: pre-issue `create_issue` bootstrap. Its AC3 states the guard must keep failing closed for every mutation touching local state and that the exemption "does not generalize". It therefore explicitly declines to cover the post-issue commit/lock phase this issue occupies. * **#716** (open) — *Prevent MCP session state from dirtying Git worktrees and provide sanctioned legacy-state recovery.* Concerns MCP **session-state residue** (`.mcp_session_*`) dirtying the control checkout, recovered by relocating that state. It does not address author implementation content in a task worktree, worktree provisioning, base refresh, claim rebinding, publication, or checkpointing. * **#713** (open) — *Require registered Git worktree proof for branches-only mutation authorization.* Makes the guard **stricter**. This issue must satisfy it rather than route around it: any operation added here must produce a genuinely registered worktree, never an improvised directory. * **#711**, **#716** — open, adjacent. Not merged into this scope. * **#753 / #755 / #768** (closed) — dead-session and descendant recovery dispositions, all deriving ownership from a published remote or PR head. There is no published artifact in either entry point. ## The core correction **Demanding "current and clean" is not a recovery mechanism when the only copy of the implementation is the content itself.** A precondition that can only be satisfied by destroying the asset it guards is not a precondition — it is an unrecoverable state. Every existing recovery disposition re-derives ownership from *already-published* state, which is sound for published claims and vacuous for unpublished ones. This issue asks for the inverse: a sanctioned operation that **creates** the publishable state from preserved local content, rather than one that demands the publishable state already exist. Recovery must be an explicit MCP / control-plane operation with its own audit record. It must not be an instruction to the operator, a documentation note, a bypass flag, or an environment rebinding. ## Required behavior Add a sanctioned, role-gated, auditable author worktree recovery and checkpoint capability, reachable entirely through native MCP tools. It must be able to: 1. **Preserve** dirty tracked **and** untracked implementation content, byte-for-byte, before touching anything. 2. **Create or register** an eligible issue worktree under `branches/`, as a genuinely registered Git worktree satisfying #713. 3. **Refresh** that worktree's base safely to current master. 4. **Restore / materialize** the preserved changes into it without overwriting or silently merging them. 5. **Acquire or rebind** the author claim for the issue, atomically and with CAS/generation protection. 6. **Publish** a portable checkpoint or commit from the restored content, **including from an existing local commit that has never been pushed**. 7. **Verify** the resulting branch and worktree against what was preserved. 8. **Fail closed** on conflicts, ambiguous ownership, or any content-loss risk. 9. Operate **without** `git`, `gh`, `tea`, `curl`, direct HTTP/API calls, tokens, credential workarounds, or manual filesystem intervention by a human. ## Acceptance criteria **AC1.** A native MCP operation preserves the full dirty state of an author worktree — modified tracked files, staged changes, and untracked files — into a durable, addressable checkpoint outside the working tree, with per-file content hashes recorded. Ignored files are excluded by default and the exclusion is reported, not silent. **AC2.** The preserve step is **non-destructive**: it never modifies, stages, resets, cleans, checks out, stashes, or removes anything in the source worktree. Running it twice on unchanged content is idempotent and yields the same content hashes. **AC3.** A native MCP operation creates or registers an eligible issue worktree under `branches/` for a given issue and branch name, as a genuinely registered worktree (present in the worktree registry), satisfying #713's proof requirement. It never satisfies the guard with an unregistered directory. **AC4.** A native MCP operation refreshes a task worktree's base to current master and reports the before/after base. It refuses to do so when doing so would discard local content, and it never rebases or force-moves published history. **AC5.** A native MCP operation materializes a preserved checkpoint into a prepared worktree and **verifies every restored file against its recorded hash**. Any hash mismatch, missing file, or pre-existing conflicting content fails closed with the specific paths named, leaving both the checkpoint and the target intact. **AC6.** Restore never overwrites existing differing content implicitly. A collision is a fail-closed condition reported per path, not a merge, not a "theirs wins", and not a silent skip. **AC7.** After a successful restore, `gitea_lock_issue` can bind the prepared worktree, and the #618 lock-derived resolution then permits `gitea_commit_files` and `gitea_create_pr` with no environment rebinding, no direct `git push`, and no web-UI intervention. **A clean prepared worktree alone does not discharge this criterion** — see AC19 through AC22, which govern the case where the worktree is already clean and the lock still fails closed. **AC8.** Claim acquisition or rebinding during recovery is atomic and CAS/generation-protected, so two concurrent sessions cannot both recover the same claim. Ownership is derived from server-side control-plane evidence only — never from a caller-supplied token, boolean, or self-declared eligibility. **AC9.** Recovery fails closed on: a live competing owner; a foreign identity or profile; a branch or repository mismatch; a competing open PR or remote branch claiming the issue; ambiguous or contradictory control-plane records; and any detected risk of content loss. **AC10.** Every recovery run produces a durable audit record naming the issue, branch, source worktree, checkpoint identifier, per-file hashes, the acting identity and profile, and the resulting commit or branch head. **AC11.** A read-only assessor reports the same recovery decision and the same supporting evidence as the mutating path. Divergence between assessor and mutator is itself a defect. **AC12.** A dry-run mode reports exactly what would be preserved, created, refreshed, restored, and published, and mutates nothing. **AC13.** The whole flow is reachable using only sanctioned MCP tools. No step requires `git`, `gh`, `tea`, `curl`, direct API calls, tokens, or a human editing the filesystem. `gitea_assess_gitea_operation_path` must report the flow as allowed rather than directing the caller to a shell fallback. **AC14.** The blocking guards emit an `exact_next_action` that names this recovery operation and is **satisfiable at the phase it is emitted in**. The current messages instruct the caller to supply a clean or base-equivalent worktree at a moment when producing one would destroy the work, and — in entry point B — at a moment when the worktree already is clean and current; that guidance must be replaced in both cases. **AC15.** Guard strictness does not regress: dirty-worktree, missing-worktree, non-base-equivalent, and control-checkout mutations all still fail closed for any caller that has not gone through the sanctioned recovery operation. The recovery path is an explicit operation, never a weakening of the guards or a general exemption. **AC16.** Regression tests cover, against synthetic fixtures only: dirty tracked files alone; untracked files alone; both together; staged plus unstaged changes; a worktree that does not exist; a worktree behind master; a worktree at master; restore hash mismatch; restore collision; a competing live claim; a foreign profile; dry-run versus apply; assessor/mutator agreement; and the full end-to-end path from dirty content through to an open PR. **AC17.** No test, fixture, or implementation step reads from, writes to, or otherwise disturbs `branches/issue-635-project-registry-api`. The #635 content is preserved evidence for the duration of this work. **AC18.** Documentation states the sanctioned recovery path for **both** entry points and explains why "make it clean and current" is not itself a recovery mechanism — neither when the dirty content is the only copy, nor when the content is already clean, committed, and unpublished. **AC19.** The capability explicitly classifies the source worktree into entry point A (dirty, uncommitted) or entry point B (clean, locally committed, unpublished), reports which it detected and the evidence for that classification, and refuses to guess when the evidence is ambiguous or contradictory. **AC20.** For entry point B, a native MCP operation publishes an existing unpublished local commit on a registered issue worktree to the remote branch, creating the remote head, **without first requiring the author lock that the unpublished state makes unobtainable**. It fails closed if the remote branch already exists with a head that is not an ancestor of the local commit, if any competing open PR claims the issue, or if the acting identity is not the recorded claimant. **AC21.** For entry point B, once the remote head exists, exact-owner lease renewal succeeds without an operator abandoning or hand-clearing the durable lock, and the recorded owner pid belonging to a live long-running daemon is neither necessary nor sufficient evidence in that decision. A recorded pid that is the MCP daemon rather than a live author client must not by itself make a lock permanently unrecoverable. **AC22.** Entry point B is verified end to end against synthetic fixtures: a registered worktree, clean, on the issue branch, holding an unpublished local commit descending from the recorded base, with an expired `author_issue_work` lease whose recorded owner pid is alive and whose worktree is present — recovered through to a published remote head, a successful `gitea_lock_issue`, and an open PR, with all content hashes verified equal before and after. **AC23 — Separate recovery records.** The implementation must explicitly distinguish and operate on: 1. The durable issue-lock record used by `gitea_lock_issue`. 2. The control-plane workflow lease used by `gitea_abandon_workflow_lease`. Recovery of, abandonment of, or mutation to one record must never be treated as proof that the other was released or reclaimed. The operation must identify which record blocks progress, modify only the intended record, and verify the resulting state through a read-after-write check. **AC24 — Truthful recovery evidence.** Every recovery decision must derive process identity, liveness, client ownership, worktree existence, and mutation risk from authoritative server-side evidence. An alive MCP daemon PID must never be represented as a dead author process. The recovery model must distinguish: - process alive; - process dead; - live daemon process; - active author client; - abandoned author client; - unknown or ambiguous ownership. Recovery must fail closed when identity or ownership cannot be proven, without accepting caller-supplied booleans as authoritative evidence. ## Validation expectations - Focused unit tests per rejection reason in AC9, exercised independently. - An MCP-level regression driving native `gitea_lock_issue`, `gitea_commit_files`, and `gitea_create_pr` end to end after recovery, not only assessor functions, for **both** entry points. - An explicit regression proving that the presence of dirty content is not by itself treated as permission: a foreign or mismatched claim with dirty content must still be refused. - An explicit regression proving that a clean worktree is not by itself treated as sufficient: the entry point B fixture must fail closed before AC20 lands, and succeed after. - Full-suite results reported against the known pre-existing master drift baseline rather than as an absolute pass count. The current baseline at `9eb0f29` is 11 pre-existing failures 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`. ## Machine-verifiable retry condition The defect is resolved when **both** sequences succeed from an author session, without any human filesystem intervention and without `git`. ### Entry point A ```text 1. <preserve op>(issue=N, worktree=<dirty worktree>) → checkpoint_id, files recorded with hashes, source worktree byte-identical afterwards 2. <prepare op>(issue=N, branch=<issue branch>) → registered worktree, base == current master, clean 3. <restore op>(checkpoint_id, into prepared worktree) → all files restored, all hashes verified 4. gitea_lock_issue(issue_number=N, branch_name=<issue branch>, worktree_path=<prepared worktree>) → success: true 5. gitea_commit_files(...) → success: true 6. gitea_create_pr(...) → PR number returned, closes #N ``` ### Entry point B — the live #635 case ```text 1. <classify op>(issue=635, worktree=".../branches/issue-635-project-registry-api") → entry_point: B; clean: true; branch: feat/issue-635-project-registry-api; local head: b2f6e9a; remote head: absent; 7 files hashed 2. <publish op>(issue=635, branch="feat/issue-635-project-registry-api") → remote head created and equal to the local commit; no lock required to reach this 3. gitea_lock_issue(issue_number=635, branch_name="feat/issue-635-project-registry-api", worktree_path=".../branches/issue-635-project-registry-api") → success: true, via exact-owner renewal, with no lock abandonment 4. gitea_create_pr(...) → PR number returned, closes #635 5. <verify op> → all 7 published file hashes equal the recorded set ``` Interim partial check for entry point A: step 4 returning `success: true` against a prepared worktree whose content hashes equal the preserved set. Interim partial check for entry point B: step 2 producing an observable remote head for a branch that had none. ## Linkage and ownership boundary - **#635** — evidence and payload. Its issue record, branch `feat/issue-635-project-registry-api`, and worktree `branches/issue-635-project-registry-api` must not be modified, reallocated, reset, cleaned, or published as part of implementing this issue. Its durable status is recorded in comments 14185, 14215, and 14225, and the entry point B evidence in #812 comments 14233 and 14241. - **#618** — closed. Supplies the `#618` lock-derived author-worktree resolution that AC7 depends on, and the AC5 operator-recovery assumption this issue replaces with a real operation. - **#772** — closed. Covers the clean-worktree unpublished-claim quadrant for a **dead** owning session. AC8's ownership-evidence model should follow its AC1/AC2 precedent; do not re-own or regress it. AC21 extends the same evidence model to the live-daemon-pid trigger it deliberately scoped away from. - **#760** — closed. Supplies exact-owner lease renewal. AC20 and AC21 extend it to the unpublished-head case its AC5/AC6 cannot express; do not weaken its published-head requirement for cases where a head does exist. - **#749** — closed. Pre-issue bootstrap phase of the same deadlock class. Its AC3 non-generalization boundary is respected here. - **#713** — open. AC3 must satisfy its registered-worktree proof requirement rather than route around it. - **#711**, **#716** — open, adjacent. Not merged into this scope. - **#628 / PR #795** — a second live instance of the base-equivalence variant, recorded in PR #795 comment 14190. Not owned by this issue. ## Canonical issue state ```text STATE: blocked WHO_IS_NEXT: user BLOCKED_ROLE: author NEXT_ACTION: Authorize an eligible workspace for control-plane work on this issue, then implement AC1-AC24 covering both recovery entry points against synthetic fixtures only NEXT_PROMPT: Author the sanctioned author-worktree recovery and checkpoint capability per AC1-AC24, covering entry point A (dirty, uncommitted) and entry point B (clean, locally committed, unpublished); never read or modify branches/issue-635-project-registry-api; PR; stop ```
Author
Owner

[THREAD STATE LEDGER]

Controller recovery/bootstrap cycle. Read-only preflight plus one executed mutation attempt (gitea_lock_issue). The blocker for #635 has moved to a different gate than the one this issue records, and the reproduction in the issue body is now partly stale. Recording the corrected state as durable evidence.

What is true now

  • Server-side decision state: issue #812 remains open and unassigned with its labels unchanged. No lock, branch, commit, or PR was created this cycle. Issue #635 remains open with no assignee, no remote branch, and no PR.
  • Local verdict/state: allocation refused pre-implementation. One mutation was attempted and it failed closed; zero net local mutations occurred; the protected worktree is byte-for-byte as this cycle found it, verified by hash.
  • Live parity green: gitea_assess_master_parity reports in_parity: true, mutation_safe: true, daemon start head, local head, and live remote head all 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8.
  • Identity jcwalker3 / prgs-author / remote prgs, identity_match: true. Namespace gitea-author healthy for gitea_lock_issue (probe_source: client_namespace).
  • The protected #635 worktree is no longer dirty. branches/issue-635-project-registry-api is now dirty: false, dirty_files: [], on branch feat/issue-635-project-registry-api, at local head b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8. The seven-file implementation reached a local commit at some point after comment 14225 was written.
  • The protected content is intact. All seven files are present in that worktree, verified read-only with no write, reset, clean, checkout, rebase, or stash. SHA-256 of each file as it stands:
SHA-256 Bytes Path
a68c9647773f128ac841596ac5475cd5a758389913f50418a42bea5ba0701622 10164 docs/webui-local-dev.md
ccf571d914eaf21026c0928b08e817f8ccc433d639390ae70d51d7a42c0bdde9 8136 docs/webui-project-registry-api.md
920dbdb344d6c4045a26c1c754327526ef9c27682120e724b64edbc81318099f 17005 tests/test_webui_project_registry.py
a403111b15483764210934e2327c13268c60fcce438be1a384aa37daa23f1401 14009 webui/app.py
9fc6e1c69beba3c8180042506fdfc92b2304d72af2bdd090d99e1aec692a977a 2289 webui/data/projects.registry.json
fcc33528a756086cdfdff081c1ce8a3e2c15edeaf855bdbb99878da4b0c27344 20705 webui/project_registry.py
db7620403f018ea780df1f39281004f8ecb267560711432cfb641220e12d8867 7126 webui/project_views.py
  • Content identity corroborated: webui/project_registry.py in that worktree defines PROJECT_STATUSES, ONBOARDING_STATES, class RegistryError, and def onboarding_summary; the registry JSON declares "version": 2; docs/webui-project-registry-api.md exists there. None of those are present in the control checkout at master 9eb0f29, so the worktree copy is the Phase 1 implementation and not a master copy.

What changed

  • The dirty-worktree precondition that this issue's reproduction step 1 is built on no longer reproduces. gitea_lock_issue no longer fails with the #510 dirty-tracked-files message for #635.
  • The failure moved one gate downstream. Executed this cycle, after gitea_whoami then gitea_resolve_task_capability(task="lock_issue") returned allowed_in_current_session: true:
gitea_lock_issue(issue_number=635,
                 branch_name="feat/issue-635-project-registry-api",
                 worktree_path=".../branches/issue-635-project-registry-api")
→ success: false, reason_code: internal_error, retryable: false
  "Issue #635 has an expired author_issue_work lease on branch
   'feat/issue-635-project-registry-api' from worktree '[PATH]'.
   Recovery review is required before takeover (fail closed); exact-owner lease r..."
  • Traced to source, read-only. issue_lock_store.assess_same_issue_lease_conflict lines 520-540 takes the expired-lease branch. same_owner is satisfied — recorded branch and worktree both match. The refusal therefore rests on two independent predicates, both of which still hold:
    • issue_lock_renewal.assess_exact_owner_lease_renewal AC5/AC6, lines 328-332: "remote branch head could not be observed; an unpublished branch cannot prove exact-owner renewal." Its AC4 clean-worktree condition (lines 311-323) is now satisfied for the first time; only the published-head condition fails.
    • issue_lock_store.assess_expired_lock_reclaim lines 455-465: reclaim needs owner_pid dead OR worktree missing. The recorded pid is the long-lived MCP daemon and the worktree is present, so neither holds. This was proven by execution, not inferred — the refusal message is reachable only when reclaim_allowed is false.

What is blocked

  • Blocker classification: environment/tooling blocker
  • BLOCKED_ROLE: author
  • OWNING_ISSUE: No sanctioned recovery for a dirty author worktree: uncommitted implementation cannot be checkpointed, and every publication gate demands the clean worktree only publication can produce (#812)
  • PROTECTED_ASSET: issue #635 worktree branches/issue-635-project-registry-api, seven files, now committed locally at b2f6e9a and hashed above
  • Failed controller operation: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api)success: false, reason_code: internal_error
  • The cycle is unchanged in kind but relocated: publishing the branch requires the issue lock, and the issue lock now requires a published remote head. gitea_commit_files and gitea_create_pr are both lock-derived under #618; neither can publish feat/issue-635-project-registry-api without the lock this gate withholds.
  • #812 cannot be self-hosted out of this state either. A fresh census over all 60 registered worktrees found 0 of 22 issue-branch worktrees simultaneously on a branch, clean, and at master head 9eb0f29. No worktree exists on any issue-812 branch. Tool-surface search across the author, reviewer, merger, and reconciler namespaces found no operation that creates, registers, refreshes, or checkpoints a worktree; the only worktree tools are the read-only gitea_audit_worktree_cleanup, gitea_capture_branches_worktree_snapshot, and gitea_assess_worktree_cleanup_integrity.
  • gitea_assess_gitea_operation_path(task="provision_author_worktree", path_kind="local_git", recovery_mode=true) returned block: true, allowed: false with the reason "unknown or non-mutation Gitea task 'provision_author_worktree'; native MCP preference gate applies only to Gitea mutations". The assessor declines jurisdiction rather than granting permission, so it is not a sanctioned authorization for a local-git escape and was not treated as one.

Consequence for this issue's specification

The AC set remains correct and is not weakened by this finding, but two items need amending before an author implements them:

  1. The reproduction in the issue body captures the dirty-tree entry point only. The same deadlock also has a committed-but-unpublished entry point, reached once the work advances to a local commit, where the refusing predicate is #760 AC5/AC6 rather than #510. A recovery capability that only handles dirty content will not unblock #635 as it stands today.
  2. AC7 assumes the sequence ends at gitea_lock_issue succeeding against a clean prepared worktree. That is now demonstrably insufficient on its own: the worktree in question is already clean, already on the correct branch, and already carries the content, and the lock still fails closed. The capability must additionally supply a sanctioned publication step, or a renewal disposition that accepts local-commit evidence where no remote head can exist yet.

AC17 was honoured. No test, fixture, or step read from the protected worktree for any purpose other than read-only presence and hash verification, and nothing in it was modified.

Who/what acts next

  • Next actor: user
  • NEXT_WORKFLOW: control-plane recovery/bootstrap workflow, once a workspace for it exists
  • Required action: authorize a way for control-plane work to obtain one eligible worktree, since the capability that would provision one is the capability that cannot be built without one. Recorded options, no action taken on any of them:
    1. Provision a single registered worktree on a feat/issue-812-* branch at master 9eb0f29, outside the MCP surface, and confirm it here. #812 then becomes ordinary author work and needs no further exception.
    2. Authorize the sanctioned abandon path for the #635 author_issue_work lock with recorded abandon_proof, on the finding that the recorded owner pid is the daemon rather than a live author client. This unblocks #635 specifically but leaves #812 unimplementable.
    3. Amend #812 to add a publication-capable disposition covering the committed-but-unpublished entry point before any implementation starts.
  • Do not do: do not reset, clean, check out, rebase, stash, or overwrite branches/issue-635-project-registry-api; its commit b2f6e9a is still the only copy of this work and is unpublished. Do not re-implement #635. Do not treat this as a parity fault; parity is green. Do not re-run ordinary author issue selection against the unchanged gate. Do not accept comments, labels, elapsed time, or an operator statement as proof the gate is repaired — only an executed gitea_lock_issue returning success: true proves it.

Canonical Issue State

STATE: blocked
WHO_IS_NEXT: user
NEXT_WORKFLOW: control-plane recovery/bootstrap workflow
BLOCKED_ROLE: author
OWNING_ISSUE: #812
PROTECTED_ASSET: Issue #635 worktree branches/issue-635-project-registry-api, seven files committed locally at b2f6e9a, hashes recorded above
RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, prepared worktree) returning success true, proven by execution
NEXT_ACTION: Authorize one of the three recorded options so that control-plane work on #812 has an eligible workspace, and amend the #812 reproduction to cover the committed-but-unpublished entry point.
NEXT_PROMPT:

Operator selects option 1, 2, or 3 recorded on Gitea-Tools issue #812 comment for the controller cycle at master 9eb0f29. Under option 1, confirm the path of the registered worktree provisioned on a feat/issue-812-* branch at master 9eb0f29 so the controller can bind it and implement AC1-AC18 without touching branches/issue-635-project-registry-api.

WHAT_HAPPENED: A controller recovery cycle verified identity, capability, parity, namespace health, and mutation safety, read #812 and the durable records at #635 comments 14185, 14215, and 14225, then executed one lock attempt against the protected worktree. The lock failed closed at a different gate than the one this issue records, because the protected content has since advanced from dirty working-tree state to a local commit. The content was verified present and hashed without modification, and no bootstrap path was available to implement #812 itself.
WHY: The dirty-tree precondition that this issue reproduces has been overtaken by events; the worktree is now clean and the refusal comes from the unpublished-remote-head predicate in exact-owner renewal, with expired-lock reclaim independently refusing because the recorded owner pid is the live daemon. Implementing the fix requires an eligible worktree, no MCP tool provisions one, and zero of the 22 registered issue-branch worktrees qualify, so the capability gap blocks its own remedy.
RELATED_PRS: none this cycle. Protected branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, and 14225. Sibling base-equivalence instance at PR #795 comment 14190.
BLOCKERS: Blocker classification environment/tooling blocker. gitea_lock_issue fails closed with reason_code internal_error on the expired author_issue_work lease; assess_exact_owner_lease_renewal refuses at issue_lock_renewal.py lines 328-332 for lack of an observable remote head; assess_expired_lock_reclaim refuses at issue_lock_store.py lines 455-465 because the recorded owner pid is alive and the worktree is present; publication is lock-derived under #618 so it cannot supply the missing remote head. Zero of 22 registered issue-branch worktrees are simultaneously on a branch, clean, and at master head, and no MCP tool in any of the four namespaces provisions or registers one. Unblock condition: an eligible registered worktree exists and gitea_lock_issue returns success true when executed against it.
VALIDATION: gitea_whoami identity match true; gitea_assess_master_parity in_parity true and mutation_safe true at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8; gitea_assess_mcp_namespace_health healthy for gitea_lock_issue with probe_source client_namespace; mcp_check_workflow_skill_preflight returned workflow skill available true; gitea_resolve_task_capability allowed for lock_issue and comment_issue. gitea_audit_worktree_cleanup enumerated 60 worktrees with open_pr_state_verified true. Seven protected files verified present and hashed with SHA-256, read-only. Refusal predicates read at issue_lock_store.py lines 424-547 and issue_lock_renewal.py lines 150-345. One mutation attempted this cycle and it failed closed; no file in any worktree was modified.
LAST_UPDATED_BY: jcwalker3 (prgs-author, controller recovery cycle)

[THREAD STATE LEDGER] Controller recovery/bootstrap cycle. Read-only preflight plus one executed mutation attempt (`gitea_lock_issue`). The blocker for #635 has **moved to a different gate** than the one this issue records, and the reproduction in the issue body is now partly stale. Recording the corrected state as durable evidence. ### What is true now - Server-side decision state: issue #812 remains open and unassigned with its labels unchanged. No lock, branch, commit, or PR was created this cycle. Issue #635 remains open with no assignee, no remote branch, and no PR. - Local verdict/state: allocation refused pre-implementation. One mutation was attempted and it failed closed; zero net local mutations occurred; the protected worktree is byte-for-byte as this cycle found it, verified by hash. - Live parity green: `gitea_assess_master_parity` reports `in_parity: true`, `mutation_safe: true`, daemon start head, local head, and live remote head all `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8`. - Identity `jcwalker3` / `prgs-author` / remote `prgs`, `identity_match: true`. Namespace `gitea-author` healthy for `gitea_lock_issue` (`probe_source: client_namespace`). - **The protected #635 worktree is no longer dirty.** `branches/issue-635-project-registry-api` is now `dirty: false`, `dirty_files: []`, on branch `feat/issue-635-project-registry-api`, at local head `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8`. The seven-file implementation reached a local commit at some point after comment 14225 was written. - **The protected content is intact.** All seven files are present in that worktree, verified read-only with no write, reset, clean, checkout, rebase, or stash. SHA-256 of each file as it stands: | SHA-256 | Bytes | Path | |---|---|---| | `a68c9647773f128ac841596ac5475cd5a758389913f50418a42bea5ba0701622` | 10164 | `docs/webui-local-dev.md` | | `ccf571d914eaf21026c0928b08e817f8ccc433d639390ae70d51d7a42c0bdde9` | 8136 | `docs/webui-project-registry-api.md` | | `920dbdb344d6c4045a26c1c754327526ef9c27682120e724b64edbc81318099f` | 17005 | `tests/test_webui_project_registry.py` | | `a403111b15483764210934e2327c13268c60fcce438be1a384aa37daa23f1401` | 14009 | `webui/app.py` | | `9fc6e1c69beba3c8180042506fdfc92b2304d72af2bdd090d99e1aec692a977a` | 2289 | `webui/data/projects.registry.json` | | `fcc33528a756086cdfdff081c1ce8a3e2c15edeaf855bdbb99878da4b0c27344` | 20705 | `webui/project_registry.py` | | `db7620403f018ea780df1f39281004f8ecb267560711432cfb641220e12d8867` | 7126 | `webui/project_views.py` | - Content identity corroborated: `webui/project_registry.py` in that worktree defines `PROJECT_STATUSES`, `ONBOARDING_STATES`, `class RegistryError`, and `def onboarding_summary`; the registry JSON declares `"version": 2`; `docs/webui-project-registry-api.md` exists there. None of those are present in the control checkout at master `9eb0f29`, so the worktree copy is the Phase 1 implementation and not a master copy. ### What changed - The dirty-worktree precondition that this issue's reproduction step 1 is built on **no longer reproduces**. `gitea_lock_issue` no longer fails with the #510 dirty-tracked-files message for #635. - The failure moved one gate downstream. Executed this cycle, after `gitea_whoami` then `gitea_resolve_task_capability(task="lock_issue")` returned `allowed_in_current_session: true`: ``` gitea_lock_issue(issue_number=635, branch_name="feat/issue-635-project-registry-api", worktree_path=".../branches/issue-635-project-registry-api") → success: false, reason_code: internal_error, retryable: false "Issue #635 has an expired author_issue_work lease on branch 'feat/issue-635-project-registry-api' from worktree '[PATH]'. Recovery review is required before takeover (fail closed); exact-owner lease r..." ``` - Traced to source, read-only. `issue_lock_store.assess_same_issue_lease_conflict` lines 520-540 takes the expired-lease branch. `same_owner` is satisfied — recorded branch and worktree both match. The refusal therefore rests on two independent predicates, both of which still hold: - `issue_lock_renewal.assess_exact_owner_lease_renewal` AC5/AC6, lines 328-332: *"remote branch head could not be observed; an unpublished branch cannot prove exact-owner renewal."* Its AC4 clean-worktree condition (lines 311-323) is now **satisfied** for the first time; only the published-head condition fails. - `issue_lock_store.assess_expired_lock_reclaim` lines 455-465: reclaim needs `owner_pid dead OR worktree missing`. The recorded pid is the long-lived MCP daemon and the worktree is present, so neither holds. This was proven by execution, not inferred — the refusal message is reachable only when `reclaim_allowed` is false. ### What is blocked - Blocker classification: environment/tooling blocker - BLOCKED_ROLE: author - OWNING_ISSUE: #812 - PROTECTED_ASSET: issue #635 worktree `branches/issue-635-project-registry-api`, seven files, now committed locally at `b2f6e9a` and hashed above - Failed controller operation: `gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api)` → `success: false`, `reason_code: internal_error` - The cycle is unchanged in kind but relocated: **publishing the branch requires the issue lock, and the issue lock now requires a published remote head.** `gitea_commit_files` and `gitea_create_pr` are both lock-derived under #618; neither can publish `feat/issue-635-project-registry-api` without the lock this gate withholds. - #812 cannot be self-hosted out of this state either. A fresh census over all 60 registered worktrees found **0 of 22** issue-branch worktrees simultaneously on a branch, clean, and at master head `9eb0f29`. No worktree exists on any `issue-812` branch. Tool-surface search across the author, reviewer, merger, and reconciler namespaces found no operation that creates, registers, refreshes, or checkpoints a worktree; the only worktree tools are the read-only `gitea_audit_worktree_cleanup`, `gitea_capture_branches_worktree_snapshot`, and `gitea_assess_worktree_cleanup_integrity`. - `gitea_assess_gitea_operation_path(task="provision_author_worktree", path_kind="local_git", recovery_mode=true)` returned `block: true, allowed: false` with the reason *"unknown or non-mutation Gitea task 'provision_author_worktree'; native MCP preference gate applies only to Gitea mutations"*. The assessor declines jurisdiction rather than granting permission, so it is not a sanctioned authorization for a local-git escape and was not treated as one. ### Consequence for this issue's specification The AC set remains correct and is not weakened by this finding, but two items need amending before an author implements them: 1. The reproduction in the issue body captures the dirty-tree entry point only. The same deadlock also has a **committed-but-unpublished** entry point, reached once the work advances to a local commit, where the refusing predicate is #760 AC5/AC6 rather than #510. A recovery capability that only handles dirty content will not unblock #635 as it stands today. 2. AC7 assumes the sequence ends at `gitea_lock_issue` succeeding against a clean prepared worktree. That is now demonstrably insufficient on its own: the worktree in question is already clean, already on the correct branch, and already carries the content, and the lock still fails closed. The capability must additionally supply a sanctioned publication step, or a renewal disposition that accepts local-commit evidence where no remote head can exist yet. AC17 was honoured. No test, fixture, or step read from the protected worktree for any purpose other than read-only presence and hash verification, and nothing in it was modified. ### Who/what acts next - Next actor: user - NEXT_WORKFLOW: control-plane recovery/bootstrap workflow, once a workspace for it exists - Required action: authorize a way for control-plane work to obtain one eligible worktree, since the capability that would provision one is the capability that cannot be built without one. Recorded options, no action taken on any of them: 1. Provision a single registered worktree on a `feat/issue-812-*` branch at master `9eb0f29`, outside the MCP surface, and confirm it here. #812 then becomes ordinary author work and needs no further exception. 2. Authorize the sanctioned abandon path for the #635 `author_issue_work` lock with recorded `abandon_proof`, on the finding that the recorded owner pid is the daemon rather than a live author client. This unblocks #635 specifically but leaves #812 unimplementable. 3. Amend #812 to add a publication-capable disposition covering the committed-but-unpublished entry point before any implementation starts. - Do not do: do not reset, clean, check out, rebase, stash, or overwrite `branches/issue-635-project-registry-api`; its commit `b2f6e9a` is still the only copy of this work and is unpublished. Do not re-implement #635. Do not treat this as a parity fault; parity is green. Do not re-run ordinary author issue selection against the unchanged gate. Do not accept comments, labels, elapsed time, or an operator statement as proof the gate is repaired — only an executed `gitea_lock_issue` returning `success: true` proves it. ## Canonical Issue State STATE: blocked WHO_IS_NEXT: user NEXT_WORKFLOW: control-plane recovery/bootstrap workflow BLOCKED_ROLE: author OWNING_ISSUE: #812 PROTECTED_ASSET: Issue #635 worktree branches/issue-635-project-registry-api, seven files committed locally at b2f6e9a, hashes recorded above RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, prepared worktree) returning success true, proven by execution NEXT_ACTION: Authorize one of the three recorded options so that control-plane work on #812 has an eligible workspace, and amend the #812 reproduction to cover the committed-but-unpublished entry point. NEXT_PROMPT: ```text Operator selects option 1, 2, or 3 recorded on Gitea-Tools issue #812 comment for the controller cycle at master 9eb0f29. Under option 1, confirm the path of the registered worktree provisioned on a feat/issue-812-* branch at master 9eb0f29 so the controller can bind it and implement AC1-AC18 without touching branches/issue-635-project-registry-api. ``` WHAT_HAPPENED: A controller recovery cycle verified identity, capability, parity, namespace health, and mutation safety, read #812 and the durable records at #635 comments 14185, 14215, and 14225, then executed one lock attempt against the protected worktree. The lock failed closed at a different gate than the one this issue records, because the protected content has since advanced from dirty working-tree state to a local commit. The content was verified present and hashed without modification, and no bootstrap path was available to implement #812 itself. WHY: The dirty-tree precondition that this issue reproduces has been overtaken by events; the worktree is now clean and the refusal comes from the unpublished-remote-head predicate in exact-owner renewal, with expired-lock reclaim independently refusing because the recorded owner pid is the live daemon. Implementing the fix requires an eligible worktree, no MCP tool provisions one, and zero of the 22 registered issue-branch worktrees qualify, so the capability gap blocks its own remedy. RELATED_PRS: none this cycle. Protected branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, and 14225. Sibling base-equivalence instance at PR #795 comment 14190. BLOCKERS: Blocker classification environment/tooling blocker. gitea_lock_issue fails closed with reason_code internal_error on the expired author_issue_work lease; assess_exact_owner_lease_renewal refuses at issue_lock_renewal.py lines 328-332 for lack of an observable remote head; assess_expired_lock_reclaim refuses at issue_lock_store.py lines 455-465 because the recorded owner pid is alive and the worktree is present; publication is lock-derived under #618 so it cannot supply the missing remote head. Zero of 22 registered issue-branch worktrees are simultaneously on a branch, clean, and at master head, and no MCP tool in any of the four namespaces provisions or registers one. Unblock condition: an eligible registered worktree exists and gitea_lock_issue returns success true when executed against it. VALIDATION: gitea_whoami identity match true; gitea_assess_master_parity in_parity true and mutation_safe true at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8; gitea_assess_mcp_namespace_health healthy for gitea_lock_issue with probe_source client_namespace; mcp_check_workflow_skill_preflight returned workflow skill available true; gitea_resolve_task_capability allowed for lock_issue and comment_issue. gitea_audit_worktree_cleanup enumerated 60 worktrees with open_pr_state_verified true. Seven protected files verified present and hashed with SHA-256, read-only. Refusal predicates read at issue_lock_store.py lines 424-547 and issue_lock_renewal.py lines 150-345. One mutation attempted this cycle and it failed closed; no file in any worktree was modified. LAST_UPDATED_BY: jcwalker3 (prgs-author, controller recovery cycle)
Author
Owner

[THREAD STATE LEDGER]

Operator-authorized recovery attempt for #635 via the sanctioned expired-lease abandon path. The abandon path is structurally inapplicable to this obstruction. Two independent reasons, both proven from source. Recording so no future cycle re-attempts it, and so AC21 is specified against the real mechanism.

What is true now

  • Server-side decision state: the body of #812 was amended this cycle to distinguish recovery entry points A and B, adding AC19 through AC22 and a second machine-verifiable retry sequence. gitea_edit_issue returned success: true, performed: true, verified: true, changed_fields: ["body"], read-after-write proof passed with no unspecified field moved. Issue #635 is unchanged: open, unassigned, no lock, no remote branch, no PR.
  • Local verdict/state: recovery refused pre-mutation. No lease was abandoned, no lock was taken, no branch published, no file in any worktree modified. The protected worktree is byte-for-byte as this cycle found it.
  • Live parity green: in_parity: true, mutation_safe: true, all heads at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8.

Protected asset reverified

Re-verified read-only this cycle, before any recovery attempt:

Check Result
Worktree registered branches/issue-635-project-registry-api, present in the worktree registry
Branch feat/issue-635-project-registry-api
Local HEAD b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8
Tree clean yes, dirty_files: []
Seven protected files present yes
SHA-256 verification 7 of 7 match the durable record in comment 14233
Published no — gitea_get_file(webui/project_registry.py, ref=feat/issue-635-project-registry-api) returns HTTP 404
Open PR none

The obstruction, named precisely

Two distinct records exist and they are not the same object:

  1. Durable issue-lock file ~/.cache/gitea-tools/issue-locks/prgs-Scaled-Tech-Consulting-Gitea-Tools-635.json, lock_generation: 1, written by gitea_lock_issue at 2026-07-22T11:46:17Z. Its work_lease has operation_type: author_issue_work, expires_at: 2026-07-22T15:46:17Z (expired), claimant jcwalker3 / prgs-author, branch and worktree both matching, and pid / session_pid = 36856.
  2. Control-plane DB lease lease-98318c2c5f034077, work_number: 635, status: expired, owner_pid: 36856, owner_pid_alive: true. A later lease lease-9878a1642666494d was reclaimed from it and then released, so it leaves no active foreign lease behind.

gitea_lock_issue fails on record 1, the file lock. gitea_abandon_workflow_lease operates on record 2, the DB lease.

Why the sanctioned abandon path cannot resolve this

Reason 1 — the mandatory precondition is false, truthfully.

lease_lifecycle.py line 90:

if not (self.dead_process or self.missing_worktree):
    return False

This hard precondition is evaluated before the operator_authorized branch at lines 95-97. operator_authorized governs only whether a foreign abandon is permitted; it does not and cannot waive line 90. Lines 680-683 additionally recompute both flags server-side and OR them with the caller's values, so a caller cannot pass the gate by assertion.

Both disjuncts are genuinely false:

  • dead_process is false. Recorded owner pid 36856 is alive. Its full command line is /opt/homebrew/.../Python /Users/jasonwalker/Development/Gitea-Tools/mcp_server.py, started Wed Jul 22 07:44:15 2026, parent pid 36648. It is the long-running MCP daemon, not a live author client. The operator finding that the recorded pid is the daemon is therefore factually correct — but "is the daemon" is not the same predicate as "is dead", and the guard tests the latter.
  • missing_worktree is false. The registered worktree is present and holds the protected content.

The abandon call was prepared with truthful evidence — dead_process: false, missing_worktree: false, no_open_pr: true, no_live_mutation_risk: true, operator_authorized: true — and would have been refused by line 90. Asserting dead_process: true to pass the gate was refused as a matter of discipline: it would be false, and laundering false evidence through a guard is the failure mode recorded as PR #710 F1 and declined in comment 14163.

Reason 2 — even a successful abandon would not clear the blocking record.

lease_lifecycle.py contains no reference to issue-locks, issue_lock_store, or any issue-lock-file clearing path. Abandoning lease-98318c2c5f034077 would mark the DB lease abandoned and leave the durable issue-lock file — the record gitea_lock_issue actually consults — completely untouched, with pid: 36856 intact. The next gitea_lock_issue would fail identically.

Conclusion. There is no sanctioned operation in the author, reviewer, merger, or reconciler surface that clears, renews, or rebinds an expired durable issue-lock file whose recorded owner pid belongs to a live daemon. The abandon family covers control-plane leases only. This is the precise capability gap, and it is narrower and more actionable than "recovery is missing".

What changed

  • #812 body amended: entry points A and B distinguished; AC19 (classify entry state), AC20 (publish an unpublished local commit without first requiring the lock), AC21 (renewal must not treat a live daemon pid as permanently disqualifying), AC22 (end-to-end entry point B fixture) added; AC7, AC14, AC18 amended; a second machine-verifiable retry sequence added for entry point B.
  • This comment adds the abandon-path finding above, which was not known when the body was amended earlier in the cycle.

What is blocked

  • Blocker classification: environment/tooling blocker
  • BLOCKED_ROLE: author
  • OWNING_ISSUE: No sanctioned recovery for a dirty author worktree: uncommitted implementation cannot be checkpointed, and every publication gate demands the clean worktree only publication can produce (#812)
  • PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at local commit b2f6e9a, unpublished, 7 of 7 hashes verified this cycle
  • Failed sanctioned operation: gitea_abandon_workflow_lease(lease_id="lease-98318c2c5f034077", ...). It was not executed against the control plane — the client permission layer denied the call before dispatch. Independently, source analysis proves it would have been refused at lease_lifecycle.py line 90 on truthful evidence, and would not have cleared the file lock even had it succeeded.
  • gitea_lock_issue remains refused, unchanged from comment 14233, with reason_code: internal_error and detail "Issue #635 has an expired author_issue_work lease ... Recovery review is required before takeover (fail closed); exact-owner lease r..."
  • #635 is not recovered. It must not be routed to the author queue.

The single smallest unblock

Every other precondition of exact-owner renewal is already satisfied for #635: recorded remote, org, repo, branch, worktree, claimant identity, and claimant profile all match the active session; the worktree exists, is on the branch, and is clean. Exactly one predicate fails — issue_lock_renewal.assess_exact_owner_lease_renewal lines 328-332, no observable remote head.

Therefore publishing feat/issue-635-project-registry-api to the remote is sufficient on its own to unblock #635. No lock abandonment, no lock file surgery, and no worktree surgery are required. That is exactly what AC20 specifies, and #635 is its live fixture.

Who/what acts next

  • Next actor: user
  • Required action: authorize one of the following. This cycle takes none of them unilaterally.
    1. Publish the existing local commit b2f6e9a on feat/issue-635-project-registry-api to remote prgs by a means outside the MCP author surface, then confirm here. Exact-owner renewal should then succeed and the author loop can lock, open the PR, and hand off unattended.
    2. Provision one registered worktree on a feat/issue-812-* branch at master 9eb0f29 so AC20 can be implemented natively, after which #635 recovers through the sanctioned path with no exception at all.
    3. Extend the abandon family to cover durable issue-lock files with a truthful daemon_pid_not_author_client predicate distinct from dead_process, tracked under AC21.
  • Do not do: do not assert dead_process on this lease; pid 36856 is alive and the assertion would be false. Do not hand-edit or delete the durable lock JSON. Do not reset, clean, check out, rebase, stash, or overwrite branches/issue-635-project-registry-api; commit b2f6e9a is unpublished and still the only copy. Do not re-implement #635. Do not close #812 if #635 recovers — the systemic gap remains valid for future dirty and committed-but-unpublished worktrees. Do not re-run ordinary author issue selection against the unchanged gate. Do not accept comments, labels, elapsed time, or an operator statement as proof the gate is repaired — only an executed gitea_lock_issue returning success: true proves it.

Canonical Issue State

STATE: blocked
WHO_IS_NEXT: user
NEXT_WORKFLOW: control-plane recovery/bootstrap workflow
BLOCKED_ROLE: author
OWNING_ISSUE: #812
PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, 7 of 7 hashes verified against comment 14233
RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api) returning success true, proven by execution
NEXT_ACTION: Authorize publication of the existing unpublished commit b2f6e9a, or a workspace for implementing AC20, or an issue-lock-file abandon predicate distinct from dead_process.
NEXT_PROMPT:

Operator selects option 1, 2, or 3 recorded on Gitea-Tools issue #812 in the abandon-path ledger comment. Under option 1, publish existing local commit b2f6e9a on branch feat/issue-635-project-registry-api to remote prgs without altering its content, then confirm the remote head here so the author loop can renew the exact-owner lease, lock issue #635, and open the PR. Do not modify branches/issue-635-project-registry-api.

WHAT_HAPPENED: The operator authorized the sanctioned abandon path for the expired #635 lease. Reverification confirmed the protected worktree clean at b2f6e9a with 7 of 7 hashes matching and the branch unpublished. The abandon path was then found structurally inapplicable: its mandatory precondition requires a dead process or a missing worktree, both of which are truthfully false, and operator authorization does not waive that precondition; separately, the abandon family never touches the durable issue-lock file that actually blocks the lock. The #812 body was amended to specify both recovery entry points and the missing publication capability.
WHY: The recorded owner pid 36856 is a live mcp_server.py daemon rather than a live author client, so the guard's dead-process disjunct is false even though the operator's underlying finding about the pid is correct; the guard tests liveness, not role. Asserting dead_process would be false evidence and was refused. The durable issue-lock file and the control-plane DB lease are separate records, and only the latter is in the abandon family's scope.
RELATED_PRS: none this cycle. Branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, 14225; at #812, comment 14233.
BLOCKERS: Blocker classification environment/tooling blocker. gitea_abandon_workflow_lease is inapplicable: lease_lifecycle.py line 90 requires dead_process or missing_worktree and both are false, and the module contains no issue-lock-file clearing path. gitea_lock_issue continues to fail closed with reason_code internal_error because assess_exact_owner_lease_renewal lines 328-332 require an observable remote head and assess_expired_lock_reclaim lines 455-465 require a dead pid or missing worktree. Unblock condition: an observable remote head exists for feat/issue-635-project-registry-api, after which exact-owner renewal succeeds and gitea_lock_issue returns success true when executed.
VALIDATION: gitea_whoami identity match true; parity in_parity and mutation_safe true at 9eb0f29; gitea_resolve_task_capability allowed for edit_issue, abandon_workflow_lease, and comment_issue. gitea_edit_issue returned success, performed, and verified true with read-after-write proof. Seven protected files re-hashed with SHA-256, 7 of 7 equal to comment 14233. gitea_get_file on the branch ref returned HTTP 404 confirming the branch is unpublished. gitea_capture_branches_worktree_snapshot confirmed the worktree registered on feat/issue-635-project-registry-api at b2f6e9a. gitea_list_workflow_leases enumerated 23 leases and identified lease-98318c2c5f034077 as the #635 record with owner_pid_alive true. Guard predicates read at lease_lifecycle.py lines 85-97 and 680-701, issue_lock_store.py lines 424-547, issue_lock_renewal.py lines 150-345. Owner pid 36856 identified as mcp_server.py by process inspection. Zero mutations to any worktree.
LAST_UPDATED_BY: jcwalker3 (prgs-author, controller recovery cycle)

[THREAD STATE LEDGER] Operator-authorized recovery attempt for #635 via the sanctioned expired-lease abandon path. **The abandon path is structurally inapplicable to this obstruction.** Two independent reasons, both proven from source. Recording so no future cycle re-attempts it, and so AC21 is specified against the real mechanism. ### What is true now - Server-side decision state: the body of #812 was amended this cycle to distinguish recovery entry points A and B, adding AC19 through AC22 and a second machine-verifiable retry sequence. `gitea_edit_issue` returned `success: true`, `performed: true`, `verified: true`, `changed_fields: ["body"]`, read-after-write proof passed with no unspecified field moved. Issue #635 is unchanged: open, unassigned, no lock, no remote branch, no PR. - Local verdict/state: recovery refused pre-mutation. No lease was abandoned, no lock was taken, no branch published, no file in any worktree modified. The protected worktree is byte-for-byte as this cycle found it. - Live parity green: `in_parity: true`, `mutation_safe: true`, all heads at `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8`. ### Protected asset reverified Re-verified read-only this cycle, before any recovery attempt: | Check | Result | |---|---| | Worktree registered | `branches/issue-635-project-registry-api`, present in the worktree registry | | Branch | `feat/issue-635-project-registry-api` | | Local HEAD | `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8` | | Tree clean | yes, `dirty_files: []` | | Seven protected files present | yes | | SHA-256 verification | **7 of 7 match** the durable record in comment 14233 | | Published | no — `gitea_get_file(webui/project_registry.py, ref=feat/issue-635-project-registry-api)` returns HTTP 404 | | Open PR | none | ### The obstruction, named precisely Two distinct records exist and they are **not the same object**: 1. **Durable issue-lock file** `~/.cache/gitea-tools/issue-locks/prgs-Scaled-Tech-Consulting-Gitea-Tools-635.json`, `lock_generation: 1`, written by `gitea_lock_issue` at `2026-07-22T11:46:17Z`. Its `work_lease` has `operation_type: author_issue_work`, `expires_at: 2026-07-22T15:46:17Z` (expired), claimant `jcwalker3` / `prgs-author`, branch and worktree both matching, and `pid` / `session_pid` = **36856**. 2. **Control-plane DB lease** `lease-98318c2c5f034077`, `work_number: 635`, `status: expired`, `owner_pid: 36856`, `owner_pid_alive: true`. A later lease `lease-9878a1642666494d` was reclaimed from it and then released, so it leaves no active foreign lease behind. `gitea_lock_issue` fails on **record 1**, the file lock. `gitea_abandon_workflow_lease` operates on **record 2**, the DB lease. ### Why the sanctioned abandon path cannot resolve this **Reason 1 — the mandatory precondition is false, truthfully.** `lease_lifecycle.py` line 90: ``` if not (self.dead_process or self.missing_worktree): return False ``` This hard precondition is evaluated **before** the `operator_authorized` branch at lines 95-97. `operator_authorized` governs only whether a *foreign* abandon is permitted; it does not and cannot waive line 90. Lines 680-683 additionally recompute both flags server-side and OR them with the caller's values, so a caller cannot pass the gate by assertion. Both disjuncts are genuinely false: - `dead_process` is **false**. Recorded owner pid 36856 is alive. Its full command line is `/opt/homebrew/.../Python /Users/jasonwalker/Development/Gitea-Tools/mcp_server.py`, started `Wed Jul 22 07:44:15 2026`, parent pid 36648. It is the long-running MCP daemon, **not** a live author client. The operator finding that the recorded pid is the daemon is therefore factually correct — but "is the daemon" is not the same predicate as "is dead", and the guard tests the latter. - `missing_worktree` is **false**. The registered worktree is present and holds the protected content. The abandon call was prepared with truthful evidence — `dead_process: false`, `missing_worktree: false`, `no_open_pr: true`, `no_live_mutation_risk: true`, `operator_authorized: true` — and would have been refused by line 90. Asserting `dead_process: true` to pass the gate was refused as a matter of discipline: it would be false, and laundering false evidence through a guard is the failure mode recorded as PR #710 F1 and declined in comment 14163. **Reason 2 — even a successful abandon would not clear the blocking record.** `lease_lifecycle.py` contains no reference to `issue-locks`, `issue_lock_store`, or any issue-lock-file clearing path. Abandoning `lease-98318c2c5f034077` would mark the DB lease abandoned and leave the durable issue-lock file — the record `gitea_lock_issue` actually consults — completely untouched, with `pid: 36856` intact. The next `gitea_lock_issue` would fail identically. **Conclusion.** There is no sanctioned operation in the author, reviewer, merger, or reconciler surface that clears, renews, or rebinds an expired durable *issue-lock file* whose recorded owner pid belongs to a live daemon. The abandon family covers control-plane leases only. This is the precise capability gap, and it is narrower and more actionable than "recovery is missing". ### What changed - #812 body amended: entry points A and B distinguished; AC19 (classify entry state), AC20 (publish an unpublished local commit without first requiring the lock), AC21 (renewal must not treat a live daemon pid as permanently disqualifying), AC22 (end-to-end entry point B fixture) added; AC7, AC14, AC18 amended; a second machine-verifiable retry sequence added for entry point B. - This comment adds the abandon-path finding above, which was not known when the body was amended earlier in the cycle. ### What is blocked - Blocker classification: environment/tooling blocker - BLOCKED_ROLE: author - OWNING_ISSUE: #812 - PROTECTED_ASSET: `branches/issue-635-project-registry-api`, seven files, clean at local commit `b2f6e9a`, unpublished, 7 of 7 hashes verified this cycle - Failed sanctioned operation: `gitea_abandon_workflow_lease(lease_id="lease-98318c2c5f034077", ...)`. It was not executed against the control plane — the client permission layer denied the call before dispatch. Independently, source analysis proves it would have been refused at `lease_lifecycle.py` line 90 on truthful evidence, and would not have cleared the file lock even had it succeeded. - `gitea_lock_issue` remains refused, unchanged from comment 14233, with `reason_code: internal_error` and detail *"Issue #635 has an expired author_issue_work lease ... Recovery review is required before takeover (fail closed); exact-owner lease r..."* - #635 is **not** recovered. It must not be routed to the author queue. ### The single smallest unblock Every other precondition of exact-owner renewal is already satisfied for #635: recorded remote, org, repo, branch, worktree, claimant identity, and claimant profile all match the active session; the worktree exists, is on the branch, and is clean. Exactly one predicate fails — `issue_lock_renewal.assess_exact_owner_lease_renewal` lines 328-332, no observable remote head. Therefore **publishing `feat/issue-635-project-registry-api` to the remote is sufficient on its own to unblock #635.** No lock abandonment, no lock file surgery, and no worktree surgery are required. That is exactly what AC20 specifies, and #635 is its live fixture. ### Who/what acts next - Next actor: user - Required action: authorize one of the following. This cycle takes none of them unilaterally. 1. Publish the existing local commit `b2f6e9a` on `feat/issue-635-project-registry-api` to remote `prgs` by a means outside the MCP author surface, then confirm here. Exact-owner renewal should then succeed and the author loop can lock, open the PR, and hand off unattended. 2. Provision one registered worktree on a `feat/issue-812-*` branch at master `9eb0f29` so AC20 can be implemented natively, after which #635 recovers through the sanctioned path with no exception at all. 3. Extend the abandon family to cover durable issue-lock files with a truthful `daemon_pid_not_author_client` predicate distinct from `dead_process`, tracked under AC21. - Do not do: do not assert `dead_process` on this lease; pid 36856 is alive and the assertion would be false. Do not hand-edit or delete the durable lock JSON. Do not reset, clean, check out, rebase, stash, or overwrite `branches/issue-635-project-registry-api`; commit `b2f6e9a` is unpublished and still the only copy. Do not re-implement #635. Do not close #812 if #635 recovers — the systemic gap remains valid for future dirty and committed-but-unpublished worktrees. Do not re-run ordinary author issue selection against the unchanged gate. Do not accept comments, labels, elapsed time, or an operator statement as proof the gate is repaired — only an executed `gitea_lock_issue` returning `success: true` proves it. ## Canonical Issue State STATE: blocked WHO_IS_NEXT: user NEXT_WORKFLOW: control-plane recovery/bootstrap workflow BLOCKED_ROLE: author OWNING_ISSUE: #812 PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, 7 of 7 hashes verified against comment 14233 RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api) returning success true, proven by execution NEXT_ACTION: Authorize publication of the existing unpublished commit b2f6e9a, or a workspace for implementing AC20, or an issue-lock-file abandon predicate distinct from dead_process. NEXT_PROMPT: ```text Operator selects option 1, 2, or 3 recorded on Gitea-Tools issue #812 in the abandon-path ledger comment. Under option 1, publish existing local commit b2f6e9a on branch feat/issue-635-project-registry-api to remote prgs without altering its content, then confirm the remote head here so the author loop can renew the exact-owner lease, lock issue #635, and open the PR. Do not modify branches/issue-635-project-registry-api. ``` WHAT_HAPPENED: The operator authorized the sanctioned abandon path for the expired #635 lease. Reverification confirmed the protected worktree clean at b2f6e9a with 7 of 7 hashes matching and the branch unpublished. The abandon path was then found structurally inapplicable: its mandatory precondition requires a dead process or a missing worktree, both of which are truthfully false, and operator authorization does not waive that precondition; separately, the abandon family never touches the durable issue-lock file that actually blocks the lock. The #812 body was amended to specify both recovery entry points and the missing publication capability. WHY: The recorded owner pid 36856 is a live mcp_server.py daemon rather than a live author client, so the guard's dead-process disjunct is false even though the operator's underlying finding about the pid is correct; the guard tests liveness, not role. Asserting dead_process would be false evidence and was refused. The durable issue-lock file and the control-plane DB lease are separate records, and only the latter is in the abandon family's scope. RELATED_PRS: none this cycle. Branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, 14225; at #812, comment 14233. BLOCKERS: Blocker classification environment/tooling blocker. gitea_abandon_workflow_lease is inapplicable: lease_lifecycle.py line 90 requires dead_process or missing_worktree and both are false, and the module contains no issue-lock-file clearing path. gitea_lock_issue continues to fail closed with reason_code internal_error because assess_exact_owner_lease_renewal lines 328-332 require an observable remote head and assess_expired_lock_reclaim lines 455-465 require a dead pid or missing worktree. Unblock condition: an observable remote head exists for feat/issue-635-project-registry-api, after which exact-owner renewal succeeds and gitea_lock_issue returns success true when executed. VALIDATION: gitea_whoami identity match true; parity in_parity and mutation_safe true at 9eb0f29; gitea_resolve_task_capability allowed for edit_issue, abandon_workflow_lease, and comment_issue. gitea_edit_issue returned success, performed, and verified true with read-after-write proof. Seven protected files re-hashed with SHA-256, 7 of 7 equal to comment 14233. gitea_get_file on the branch ref returned HTTP 404 confirming the branch is unpublished. gitea_capture_branches_worktree_snapshot confirmed the worktree registered on feat/issue-635-project-registry-api at b2f6e9a. gitea_list_workflow_leases enumerated 23 leases and identified lease-98318c2c5f034077 as the #635 record with owner_pid_alive true. Guard predicates read at lease_lifecycle.py lines 85-97 and 680-701, issue_lock_store.py lines 424-547, issue_lock_renewal.py lines 150-345. Owner pid 36856 identified as mcp_server.py by process inspection. Zero mutations to any worktree. LAST_UPDATED_BY: jcwalker3 (prgs-author, controller recovery cycle)
Author
Owner

[THREAD STATE LEDGER]

Bootstrap cycle. The bootstrap deadlock recorded in comments 14233 and 14241 is broken. A workspace for implementing #812 was acquired without operator filesystem work, and gitea_lock_issue returned success: true against it. Implementation of AC20 has started. Recording the route so it is durable and repeatable.

What is true now

  • Server-side decision state: issue #812 remains open with its labels and body unchanged this cycle. No branch was published, no commit pushed, and no PR opened. Issue #635 remains open, unassigned, with no remote branch and no PR.
  • Local verdict/state: bootstrap succeeded. A registered worktree branches/feat-issue-812-publish-unpublished-commit now exists on branch feat/issue-812-publish-unpublished-commit at master 9eb0f29, clean, and the durable issue-lock file for #812 is held by this session. The protected #635 worktree is byte-for-byte as this cycle found it, verified by hash before and after provisioning.
  • Live parity green: in_parity: true, mutation_safe: true, daemon start head, local head, and live remote head all 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8.
  • Identity jcwalker3 / prgs-author / remote prgs / Scaled-Tech-Consulting/Gitea-Tools, identity_match: true. OS identity jasonwalker uid 502, not root. Namespace gitea-author healthy with probe_source: client_namespace.
  • Recorded owner pid 36856 is alive: PID 36856, PPID 36648, started Wed Jul 22 07:44:15 2026, command .../Python /Users/jasonwalker/Development/Gitea-Tools/mcp_server.py. It is the long-running MCP daemon, not an active author client. It is not represented as dead anywhere in this cycle, and no guard was passed by asserting otherwise (AC24).

Protected asset reverified read-only before any action:

Check Result
Worktree registered branches/issue-635-project-registry-api present in the worktree registry
Branch feat/issue-635-project-registry-api
Local HEAD b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8
Tree clean yes — porcelain status empty, no dirty tracked and no untracked
Diff vs master 7 files, +1211 / -93
SHA-256 verification 7 of 7 match the durable record in comment 14233
Published no — gitea_get_file(webui/project_registry.py, ref=feat/issue-635-project-registry-api) returns HTTP 404, and the remote ref listing for that branch returns empty
Open PR none
Reverified after provisioning HEAD still b2f6e9a, still clean

AC17 honoured. The protected worktree was read for presence, branch, head, cleanliness, and hashing only. Nothing in it was written, reset, cleaned, checked out, rebased, or stashed, and no fixture references it.

What changed

The bootstrap route that worked. Comment 14233 recorded that "no MCP tool in any of the four namespaces provisions or registers a worktree" and concluded that #812 could not host itself. That finding is correct about the MCP tool surface but incomplete about the sanctioned project surface. The repository ships its own worktree provisioning script, and the #618 guard's own operator_recovery text names it:

"Recreate the worktree under branches/ (scripts/worktree-start or git worktree add), set GITEA_AUTHOR_WORKTREE / GITEA_ACTIVE_WORKTREE to that path (or pass worktree_path on mutation tools), keep the control checkout clean on master"

scripts/worktree-start refuses by default because lines 46-55 require an already-bound session issue lock, and gitea_lock_issue requires an already-existing worktree — that is the lock-to-worktree circularity. The script carries its own documented escape for exactly this bootstrap, --allow-unlinked (line 13, lines 45-77). Using it skips only the session-lock branch-name check; it still produces a genuinely registered worktree under branches/ from prgs/master, so #713's registered-worktree proof requirement is satisfied rather than routed around.

Executed, after a --dry-run showing the exact two commands:

scripts/worktree-start --allow-unlinked feat/issue-812-publish-unpublished-commit prgs/master
→ /Users/jasonwalker/Development/Gitea-Tools/branches/feat-issue-812-publish-unpublished-commit
gitea_lock_issue(issue_number=812,
                 branch_name="feat/issue-812-publish-unpublished-commit",
                 worktree_path=".../branches/feat-issue-812-publish-unpublished-commit")
→ success: true
  lock file prgs-Scaled-Tech-Consulting-Gitea-Tools-812.json
  work_lease: author_issue_work, claimant jcwalker3 / prgs-author
  adoption_decision: NO_MATCH, competing_branch_check: clear
  lock_proof: "acquired issue #812; ... freshness live; no competing live lock; lock retained"

This is a local, non-Gitea operation. gitea_assess_gitea_operation_path declined jurisdiction over provision_author_worktree in comment 14233 precisely because worktree provisioning is not a Gitea mutation; the native-MCP-preference gate therefore does not reach it. No gh, tea, curl, REST call, browser action, token, or remote publication was involved, and no human performed any step.

Consequence for the specification. Option 1 and option 2 of comment 14241 both required an operator. Neither was needed. #812 is ordinary author work once the workspace exists, exactly as comment 14233's option 1 predicted — but an LLM author can provision it unaided. AC3's "create or register an eligible issue worktree" should be specified to wrap this existing script rather than invent a parallel mechanism.

Namespace capability, proven not assumed. Implementing #812 is code authoring and requires gitea.repo.commit, gitea.branch.push, and gitea.pr.create. gitea_list_profiles shows only prgs-author holds all three. prgs-controller has role_kind: reconciler and explicitly forbids all three. The active namespace is therefore authorized here as an author, not as a controller, and was not treated as a controller surface.

Selected acceptance criterion: AC20. AC20 is smaller and safer than AC21, on this issue's own evidence:

  • Comment 14241's "single smallest unblock" establishes that every exact-owner-renewal precondition for #635 is already satisfied — recorded remote, org, repo, branch, worktree, claimant identity and profile all match; the worktree exists, is on the branch, and is clean. Exactly one predicate fails: issue_lock_renewal.assess_exact_owner_lease_renewal lines 328-332, no observable remote head.
  • AC21 alone cannot unblock. assess_expired_lock_reclaim requires owner_pid dead or worktree missing; pid 36856 is alive and the worktree is present. And renewal would still demand the published head that AC20 creates.
  • AC20 alone is therefore sufficient and strictly smaller. The two are separable, so only AC20 is being implemented.

Per AC23, the implementation will operate on the durable issue-lock file record and will not treat control-plane workflow-lease mutation as equivalent.

What is blocked

  • Blocker classification: environment/tooling blocker
  • BLOCKED_ROLE: author, for issue #635 only. #812 itself is no longer blocked.
  • OWNING_ISSUE: No sanctioned recovery for a dirty author worktree: uncommitted implementation cannot be checkpointed, and every publication gate demands the clean worktree only publication can produce (#812)
  • PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, 7 of 7 hashes reverified against comment 14233 this cycle
  • Issue #635 remains blocked, unchanged in kind from comment 14241: assess_exact_owner_lease_renewal lines 328-332 require an observable remote head, and assess_expired_lock_reclaim lines 455-465 require a dead pid or a missing worktree while pid 36856 is a live daemon and the worktree is present. Unblock condition: AC20 lands, a remote head exists for feat/issue-635-project-registry-api, and gitea_lock_issue returns success: true when executed.
  • No failed sanctioned operation this cycle. The one pre-API rejection was a gitea_create_issue_comment ledger-schema validation, corrected and not a mutation.
  • #635 must not be routed to the author queue until that executed proof exists.

Who/what acts next

  • Next actor: author — this session, holding the #812 lock.
  • Required action: implement AC20 against synthetic fixtures only, add the focused regressions, run the proportionate suite, publish, and open a PR closing #812 for an independent reviewer.
  • Do not do: do not reset, clean, check out, rebase, stash, or overwrite branches/issue-635-project-registry-api; commit b2f6e9a is unpublished and still the only copy. Do not re-implement #635. Do not assert dead_process on pid 36856. Do not close #812 if #635 later recovers — the systemic gap covering entry point A and future unpublished worktrees remains valid. Do not accept comments, labels, elapsed time, or a statement as proof the #635 gate is repaired; only an executed gitea_lock_issue returning success: true proves it.

Canonical Issue State

STATE: in_progress
WHO_IS_NEXT: author
NEXT_WORKFLOW: author implementation workflow
BLOCKED_ROLE: author
OWNING_ISSUE: #812
PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, 7 of 7 hashes reverified against comment 14233 this cycle
RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api) returning success true, proven by execution, after AC20 publishes the remote head
NEXT_ACTION: Implement AC20 as a native MCP publication operation for an unpublished local commit on a registered issue worktree, with synthetic-fixture regressions, then open a PR closing #812.
NEXT_PROMPT:

Continue the #812 author implementation on branch feat/issue-812-publish-unpublished-commit in worktree branches/feat-issue-812-publish-unpublished-commit. Implement AC20 only. Never read or modify branches/issue-635-project-registry-api. Open a PR closing #812 and route it to an independent reviewer.

WHAT_HAPPENED: A bootstrap cycle verified identity, capability, namespace health, parity, and mutation safety, read #812 in full including AC1-AC24 and comments 14233 and 14241, and reverified the protected #635 worktree read-only with 7 of 7 hashes matching. It then broke the bootstrap deadlock by provisioning a registered worktree with the repository's own scripts/worktree-start using its documented --allow-unlinked bootstrap escape, and gitea_lock_issue returned success true against it. AC20 was selected over AC21 and implementation started.
WHY: The circular dependency was between the session issue lock and the registered worktree, not between #812 and the MCP surface. The provisioning script that the #618 guard's own recovery text names carries a documented escape for exactly this bootstrap, and worktree provisioning is a local operation that the Gitea operation-path gate explicitly declines jurisdiction over. No operator action was required, so options 1 and 2 of comment 14241 are both moot.
RELATED_PRS: none yet. Branch feat/issue-812-publish-unpublished-commit created at master 9eb0f29 and not yet published. Protected branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, 14225; at #812, comments 14233 and 14241.
BLOCKERS: Blocker classification environment/tooling blocker, scoped to issue #635 only; #812 itself is unblocked. assess_exact_owner_lease_renewal lines 328-332 require an observable remote head and assess_expired_lock_reclaim lines 455-465 require a dead pid or missing worktree, while pid 36856 is a live mcp_server.py daemon and the registered worktree is present. Unblock condition: AC20 lands, a remote head exists for feat/issue-635-project-registry-api, and gitea_lock_issue returns success true when executed.
VALIDATION: gitea_whoami identity_match true; OS identity jasonwalker uid 502; gitea_assess_master_parity in_parity true and mutation_safe true at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8; gitea_assess_mcp_namespace_health healthy with probe_source client_namespace; mcp_check_workflow_skill_preflight workflow_skill_ready true; gitea_resolve_task_capability allowed_in_current_session true for lock_issue and comment_issue; gitea_list_profiles confirms only prgs-author holds gitea.repo.commit, gitea.branch.push, and gitea.pr.create while prgs-controller forbids all three. scripts/worktree-start --dry-run inspected before apply; new worktree registered, clean, on feat/issue-812-publish-unpublished-commit at 9eb0f29. gitea_lock_issue returned success true with adoption_decision NO_MATCH and competing_branch_check clear. Protected worktree hashed with SHA-256 before and after provisioning, 7 of 7 equal to comment 14233, HEAD b2f6e9a unchanged, tree clean. gitea_get_file on the branch ref returned HTTP 404. Owner pid 36856 confirmed alive and identified as mcp_server.py by process inspection. Zero modifications to any pre-existing worktree.
LAST_UPDATED_BY: jcwalker3 (prgs-author, bootstrap implementation cycle)

[THREAD STATE LEDGER] Bootstrap cycle. **The bootstrap deadlock recorded in comments 14233 and 14241 is broken.** A workspace for implementing #812 was acquired without operator filesystem work, and `gitea_lock_issue` returned `success: true` against it. Implementation of AC20 has started. Recording the route so it is durable and repeatable. ### What is true now - Server-side decision state: issue #812 remains open with its labels and body unchanged this cycle. No branch was published, no commit pushed, and no PR opened. Issue #635 remains open, unassigned, with no remote branch and no PR. - Local verdict/state: bootstrap succeeded. A registered worktree `branches/feat-issue-812-publish-unpublished-commit` now exists on branch `feat/issue-812-publish-unpublished-commit` at master `9eb0f29`, clean, and the durable issue-lock file for #812 is held by this session. The protected #635 worktree is byte-for-byte as this cycle found it, verified by hash before and after provisioning. - Live parity green: `in_parity: true`, `mutation_safe: true`, daemon start head, local head, and live remote head all `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8`. - Identity `jcwalker3` / `prgs-author` / remote `prgs` / `Scaled-Tech-Consulting/Gitea-Tools`, `identity_match: true`. OS identity `jasonwalker` uid 502, not root. Namespace `gitea-author` healthy with `probe_source: client_namespace`. - Recorded owner pid **36856 is alive**: `PID 36856, PPID 36648, started Wed Jul 22 07:44:15 2026, command .../Python /Users/jasonwalker/Development/Gitea-Tools/mcp_server.py`. It is the long-running MCP daemon, **not** an active author client. It is not represented as dead anywhere in this cycle, and no guard was passed by asserting otherwise (AC24). Protected asset reverified read-only before any action: | Check | Result | |---|---| | Worktree registered | `branches/issue-635-project-registry-api` present in the worktree registry | | Branch | `feat/issue-635-project-registry-api` | | Local HEAD | `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8` | | Tree clean | yes — porcelain status empty, no dirty tracked and no untracked | | Diff vs master | 7 files, +1211 / -93 | | SHA-256 verification | **7 of 7 match** the durable record in comment 14233 | | Published | no — `gitea_get_file(webui/project_registry.py, ref=feat/issue-635-project-registry-api)` returns HTTP 404, and the remote ref listing for that branch returns empty | | Open PR | none | | Reverified after provisioning | HEAD still `b2f6e9a`, still clean | AC17 honoured. The protected worktree was read for presence, branch, head, cleanliness, and hashing only. Nothing in it was written, reset, cleaned, checked out, rebased, or stashed, and no fixture references it. ### What changed **The bootstrap route that worked.** Comment 14233 recorded that "no MCP tool in any of the four namespaces provisions or registers a worktree" and concluded that #812 could not host itself. That finding is correct about the **MCP tool surface** but incomplete about the **sanctioned project surface**. The repository ships its own worktree provisioning script, and the #618 guard's own `operator_recovery` text names it: > "Recreate the worktree under `branches/` (`scripts/worktree-start` or `git worktree add`), set `GITEA_AUTHOR_WORKTREE` / `GITEA_ACTIVE_WORKTREE` to that path (or pass `worktree_path` on mutation tools), keep the control checkout clean on master" `scripts/worktree-start` refuses by default because lines 46-55 require an already-bound session issue lock, and `gitea_lock_issue` requires an already-existing worktree — that is the lock-to-worktree circularity. The script carries its own documented escape for exactly this bootstrap, `--allow-unlinked` (line 13, lines 45-77). Using it skips **only** the session-lock branch-name check; it still produces a genuinely registered worktree under `branches/` from `prgs/master`, so #713's registered-worktree proof requirement is satisfied rather than routed around. Executed, after a `--dry-run` showing the exact two commands: ``` scripts/worktree-start --allow-unlinked feat/issue-812-publish-unpublished-commit prgs/master → /Users/jasonwalker/Development/Gitea-Tools/branches/feat-issue-812-publish-unpublished-commit ``` ``` gitea_lock_issue(issue_number=812, branch_name="feat/issue-812-publish-unpublished-commit", worktree_path=".../branches/feat-issue-812-publish-unpublished-commit") → success: true lock file prgs-Scaled-Tech-Consulting-Gitea-Tools-812.json work_lease: author_issue_work, claimant jcwalker3 / prgs-author adoption_decision: NO_MATCH, competing_branch_check: clear lock_proof: "acquired issue #812; ... freshness live; no competing live lock; lock retained" ``` This is a **local, non-Gitea** operation. `gitea_assess_gitea_operation_path` declined jurisdiction over `provision_author_worktree` in comment 14233 precisely because worktree provisioning is not a Gitea mutation; the native-MCP-preference gate therefore does not reach it. No `gh`, `tea`, `curl`, REST call, browser action, token, or remote publication was involved, and no human performed any step. **Consequence for the specification.** Option 1 and option 2 of comment 14241 both required an operator. Neither was needed. #812 is ordinary author work once the workspace exists, exactly as comment 14233's option 1 predicted — but an LLM author can provision it unaided. AC3's "create or register an eligible issue worktree" should be specified to wrap this existing script rather than invent a parallel mechanism. **Namespace capability, proven not assumed.** Implementing #812 is code authoring and requires `gitea.repo.commit`, `gitea.branch.push`, and `gitea.pr.create`. `gitea_list_profiles` shows only `prgs-author` holds all three. `prgs-controller` has `role_kind: reconciler` and **explicitly forbids** all three. The active namespace is therefore authorized here **as an author**, not as a controller, and was not treated as a controller surface. **Selected acceptance criterion: AC20.** AC20 is smaller and safer than AC21, on this issue's own evidence: - Comment 14241's "single smallest unblock" establishes that every exact-owner-renewal precondition for #635 is already satisfied — recorded remote, org, repo, branch, worktree, claimant identity and profile all match; the worktree exists, is on the branch, and is clean. Exactly one predicate fails: `issue_lock_renewal.assess_exact_owner_lease_renewal` lines 328-332, no observable remote head. - AC21 alone cannot unblock. `assess_expired_lock_reclaim` requires `owner_pid` dead **or** worktree missing; pid 36856 is alive and the worktree is present. And renewal would still demand the published head that AC20 creates. - AC20 alone is therefore sufficient **and** strictly smaller. The two are separable, so only AC20 is being implemented. Per AC23, the implementation will operate on the durable **issue-lock file** record and will not treat control-plane workflow-lease mutation as equivalent. ### What is blocked - Blocker classification: environment/tooling blocker - BLOCKED_ROLE: author, for issue #635 only. #812 itself is no longer blocked. - OWNING_ISSUE: #812 - PROTECTED_ASSET: `branches/issue-635-project-registry-api`, seven files, clean at unpublished local commit `b2f6e9a`, 7 of 7 hashes reverified against comment 14233 this cycle - Issue #635 remains blocked, unchanged in kind from comment 14241: `assess_exact_owner_lease_renewal` lines 328-332 require an observable remote head, and `assess_expired_lock_reclaim` lines 455-465 require a dead pid or a missing worktree while pid 36856 is a live daemon and the worktree is present. Unblock condition: AC20 lands, a remote head exists for `feat/issue-635-project-registry-api`, and `gitea_lock_issue` returns `success: true` when executed. - No failed sanctioned operation this cycle. The one pre-API rejection was a `gitea_create_issue_comment` ledger-schema validation, corrected and not a mutation. - #635 must not be routed to the author queue until that executed proof exists. ### Who/what acts next - Next actor: **author** — this session, holding the #812 lock. - Required action: implement AC20 against synthetic fixtures only, add the focused regressions, run the proportionate suite, publish, and open a PR closing #812 for an independent reviewer. - Do not do: do not reset, clean, check out, rebase, stash, or overwrite `branches/issue-635-project-registry-api`; commit `b2f6e9a` is unpublished and still the only copy. Do not re-implement #635. Do not assert `dead_process` on pid 36856. Do not close #812 if #635 later recovers — the systemic gap covering entry point A and future unpublished worktrees remains valid. Do not accept comments, labels, elapsed time, or a statement as proof the #635 gate is repaired; only an executed `gitea_lock_issue` returning `success: true` proves it. ## Canonical Issue State STATE: in_progress WHO_IS_NEXT: author NEXT_WORKFLOW: author implementation workflow BLOCKED_ROLE: author OWNING_ISSUE: #812 PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, 7 of 7 hashes reverified against comment 14233 this cycle RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api) returning success true, proven by execution, after AC20 publishes the remote head NEXT_ACTION: Implement AC20 as a native MCP publication operation for an unpublished local commit on a registered issue worktree, with synthetic-fixture regressions, then open a PR closing #812. NEXT_PROMPT: ```text Continue the #812 author implementation on branch feat/issue-812-publish-unpublished-commit in worktree branches/feat-issue-812-publish-unpublished-commit. Implement AC20 only. Never read or modify branches/issue-635-project-registry-api. Open a PR closing #812 and route it to an independent reviewer. ``` WHAT_HAPPENED: A bootstrap cycle verified identity, capability, namespace health, parity, and mutation safety, read #812 in full including AC1-AC24 and comments 14233 and 14241, and reverified the protected #635 worktree read-only with 7 of 7 hashes matching. It then broke the bootstrap deadlock by provisioning a registered worktree with the repository's own scripts/worktree-start using its documented --allow-unlinked bootstrap escape, and gitea_lock_issue returned success true against it. AC20 was selected over AC21 and implementation started. WHY: The circular dependency was between the session issue lock and the registered worktree, not between #812 and the MCP surface. The provisioning script that the #618 guard's own recovery text names carries a documented escape for exactly this bootstrap, and worktree provisioning is a local operation that the Gitea operation-path gate explicitly declines jurisdiction over. No operator action was required, so options 1 and 2 of comment 14241 are both moot. RELATED_PRS: none yet. Branch feat/issue-812-publish-unpublished-commit created at master 9eb0f29 and not yet published. Protected branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, 14225; at #812, comments 14233 and 14241. BLOCKERS: Blocker classification environment/tooling blocker, scoped to issue #635 only; #812 itself is unblocked. assess_exact_owner_lease_renewal lines 328-332 require an observable remote head and assess_expired_lock_reclaim lines 455-465 require a dead pid or missing worktree, while pid 36856 is a live mcp_server.py daemon and the registered worktree is present. Unblock condition: AC20 lands, a remote head exists for feat/issue-635-project-registry-api, and gitea_lock_issue returns success true when executed. VALIDATION: gitea_whoami identity_match true; OS identity jasonwalker uid 502; gitea_assess_master_parity in_parity true and mutation_safe true at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8; gitea_assess_mcp_namespace_health healthy with probe_source client_namespace; mcp_check_workflow_skill_preflight workflow_skill_ready true; gitea_resolve_task_capability allowed_in_current_session true for lock_issue and comment_issue; gitea_list_profiles confirms only prgs-author holds gitea.repo.commit, gitea.branch.push, and gitea.pr.create while prgs-controller forbids all three. scripts/worktree-start --dry-run inspected before apply; new worktree registered, clean, on feat/issue-812-publish-unpublished-commit at 9eb0f29. gitea_lock_issue returned success true with adoption_decision NO_MATCH and competing_branch_check clear. Protected worktree hashed with SHA-256 before and after provisioning, 7 of 7 equal to comment 14233, HEAD b2f6e9a unchanged, tree clean. gitea_get_file on the branch ref returned HTTP 404. Owner pid 36856 confirmed alive and identified as mcp_server.py by process inspection. Zero modifications to any pre-existing worktree. LAST_UPDATED_BY: jcwalker3 (prgs-author, bootstrap implementation cycle)
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-22 16:19:04 -05:00
Author
Owner

[THREAD STATE LEDGER]

AC20 implemented and published. PR #814 is open at head 99fda93bccbedfe90e4c058200a65241d15f4da8 and needs an independent reviewer. Continues the bootstrap cycle recorded in comment 14244.

What is true now

  • Server-side decision state: PR #814 opened against master from feat/issue-812-publish-unpublished-commit, body carries Closes #812. Issue #812 gained status:pr-open automatically and is otherwise unchanged. Issue #635 remains open, unassigned, with no remote branch and no PR.
  • Local verdict/state: implementation complete and committed. Remote head 99fda93bccbedfe90e4c058200a65241d15f4da8; local worktree synchronized to that same head, tree clean. 7 files, +1487 lines.
  • Live parity green throughout: in_parity: true, mutation_safe: true at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8.
  • Identity jcwalker3 / prgs-author / remote prgs, identity_match: true on every mutation. OS identity jasonwalker uid 502.
  • Protected asset unchanged: branches/issue-635-project-registry-api is still at b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8, tree clean, unpublished. It was read only for presence, branch, head, cleanliness, and hashing, and nothing in it was written, reset, cleaned, checked out, rebased, or stashed. A regression in the new test file asserts that neither it nor branch_publish.py references that path at all (AC17).
  • Recorded owner pid 36856 remains alive and is the mcp_server.py daemon. It is not represented as dead anywhere in this cycle, and no guard was passed by asserting otherwise (AC24).

What changed

AC20 delivered. New tool gitea_publish_unpublished_issue_branch, backed by the new pure module branch_publish.py (assessor, executor, read-after-write verifier). It publishes an already-committed local head to its remote branch so exact-owner renewal has the observable head its evidence model requires. Publication is the whole of its authority: it renews, reclaims, rebinds, and clears nothing.

Files in scope, all 7:

File Change
branch_publish.py new — assessor, publisher, verifier
gitea_mcp_server.py new @mcp.tool() + _publication_block helper
task_capability_map.py publish_unpublished_branchgitea.branch.push / author, role-exclusive
anti_stomp_preflight.py task added to MUTATION_TASKS and wired to shared preflight
docs/mcp-tool-inventory.md regenerated with the documented generator, not hand-edited
tests/test_issue_812_publish_unpublished_commit.py new — 36 cases
tests/test_task_capability_role_invariants.py one entry added to the deliberate tripwire constant

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, never asserted by the caller; with no lock record it refuses outright. Guard strictness does not regress (AC15) — dirty tracked files, untracked files the commit does not carry, an unregistered worktree, a non-issue or stable branch, a branch number that does not match the issue, a changed local HEAD, an abbreviated SHA, a non-ancestor remote head, an unobservable remote, a foreign claimant or profile, a branch or worktree mismatch against the lock, a competing open PR for the issue on another branch, and any declared-hash mismatch each fail closed with its own test. The refspec names the commit SHA explicitly and never forces, so git itself rejects a non-fast-forward as a final independent guard.

AC23 honoured in code and proven in test. The durable issue-lock file and the control-plane workflow lease are distinct records; the tool 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.

AC24 honoured in code and proven in test. Recorded-pid liveness is never consulted; owner_pid_liveness_consulted is reported false. One 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 discipline. AC20 only. AC21 cannot unblock on its own, because reclaim needs a dead pid and renewal still needs the published head AC20 creates, so the two are separable and only the smaller was implemented. Entry point A and the remaining criteria stay open under this issue.

#812 entry point A was encountered live during this work and is now independently corroborated. The first gitea_commit_files attempt for this very PR failed with the #510 message "Namespace workspace binding blocked (author namespace, #510) ... Dirty tracked files in active task workspace: anti_stomp_preflight.py,". The escape used was to commit locally first, which cleared the dirty-tree gate; because this issue's lease was live rather than expired, the entry point B renewal gate did not re-materialize behind it, and gitea_commit_files then succeeded. That confirms the sequencing recorded in comment 14233: the gates are sequential, and an expired lease is what turns the second one into a deadlock.

What is blocked

  • Blocker classification: environment/tooling blocker
  • BLOCKED_ROLE: author, for issue #635 only. #812 is not blocked; it is in review.
  • OWNING_ISSUE: No sanctioned recovery for a dirty author worktree: uncommitted implementation cannot be checkpointed, and every publication gate demands the clean worktree only publication can produce (#812)
  • PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, hashes matching comment 14233
  • Issue #635 remains blocked until PR #814 merges and the MCP daemon restarts. The daemon started at 9eb0f29 and therefore does not yet have branch_publish loaded, so the new tool is not callable in this runtime no matter that the code now exists. Merging alone is not sufficient; runtime parity is required.
  • No failed sanctioned operation is outstanding. The two pre-API rejections this cycle — one ledger-schema validation and one #510 dirty-tree block — were both corrected and neither is a mutation-budget event.
  • #635 must not be routed to the author queue until an executed gitea_lock_issue returns success: true.

Who/what acts next

  • Next actor: reviewer
  • Required action: independent review of PR #814. The author identity must not self-review, self-approve, or merge it.
  • Reviewer focus, offered honestly: this is the first Python code in the repository that sends a git ref to a remote, so the executor in branch_publish.publish_commit_to_remote_branch deserves the closest reading. Confirm the no-force refspec cannot be coerced into a history rewrite, that the ownership predicate cannot be satisfied without a pre-existing durable lock naming the caller, and that nothing in the new tool weakens #510, #618, or #713 for any caller that has not gone through it.
  • Do not do: do not reset, clean, check out, rebase, stash, or overwrite branches/issue-635-project-registry-api; commit b2f6e9a is unpublished and still the only copy. Do not re-implement #635. Do not assert dead_process on pid 36856. Do not close #812 when #814 merges beyond what Closes #812 does — if the remaining criteria still matter, reopen or file the follow-up rather than treating AC20 as the whole issue. Do not accept comments, labels, elapsed time, or a statement as proof the #635 gate is repaired; only an executed gitea_lock_issue returning success: true proves it.

Canonical Issue State

STATE: in_review
WHO_IS_NEXT: reviewer
NEXT_WORKFLOW: reviewer PR review workflow
BLOCKED_ROLE: author
OWNING_ISSUE: #812
PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, hashes matching comment 14233, untouched this cycle
RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api) returning success true, proven by execution, after PR #814 merges and the MCP daemon restarts
NEXT_ACTION: Independently review PR #814 at head 99fda93bcc and post a verdict. Do not self-review or merge from the author identity.
NEXT_PROMPT:

Review Gitea-Tools PR #814 (Closes #812) at head 99fda93bccbedfe90e4c058200a65241d15f4da8 as an independent reviewer on the prgs-reviewer namespace. Scope is AC20 only. Verify the publication executor cannot rewrite history, that ownership cannot be proven without a pre-existing durable issue-lock record naming the caller, and that guard strictness under #510, #618, and #713 does not regress. Never read or modify branches/issue-635-project-registry-api.

WHAT_HAPPENED: The bootstrap workspace acquired in comment 14244 was used to implement AC20 as a native MCP publication operation with a pure assessor module and 36 synthetic-fixture regressions. The work was committed locally after the #510 dirty-tree gate refused the first publication attempt, then published through gitea_commit_files as remote commit 99fda93, the local worktree was synchronized to that head, and PR #814 was opened closing #812.
WHY: AC20 was selected over AC21 because comment 14241 established that publication alone is sufficient to unblock #635 while reclamation can never be satisfied against a live daemon pid. Implementing the smaller of two separable criteria keeps the change auditable and leaves the reclaim predicate untouched rather than softened.
RELATED_PRS: PR #814 open at head 99fda93bcc, base master, closing #812. Branch feat/issue-812-publish-unpublished-commit published. Protected branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, 14225; at #812, comments 14233, 14241, and 14244.
BLOCKERS: Blocker classification environment/tooling blocker, scoped to issue #635 only. The new capability exists in code but not in the running daemon, which started at 9eb0f29 and does not have branch_publish loaded, so the tool is not callable until PR #814 merges and the daemon restarts. Until then assess_exact_owner_lease_renewal lines 328-332 still require an observable remote head and assess_expired_lock_reclaim lines 455-465 still require a dead pid or missing worktree, while pid 36856 is a live mcp_server.py daemon and the registered worktree is present.
VALIDATION: gitea_whoami identity_match true before every mutation; gitea_resolve_task_capability allowed_in_current_session true for lock_issue, comment_issue, commit_files, and create_pr, resolved immediately before each. Parity in_parity and mutation_safe true at 9eb0f29cef. Namespace gitea-author healthy with probe_source client_namespace. gitea_lock_issue returned success true for #812 with adoption_decision NO_MATCH and competing_branch_check clear. gitea_commit_files returned success with content_source_proof local_path for all 7 files at remote commit 99fda93bcc. gitea_create_pr returned PR number 814 with issue_status_transition performed true. New suite tests/test_issue_812_publish_unpublished_commit.py 36 passed. Full suite 11 failed, 4354 passed, 6 skipped, 533 subtests passed, against a pre-edit baseline on this same branch of 11 failed, 4318 passed, 6 skipped, 531 subtests passed, so the 11 failures are unchanged in count and identity and the delta is 36 added tests and 2 added subtests. Protected worktree verified read-only at b2f6e9a with a clean tree and 7 of 7 SHA-256 hashes equal to comment 14233, before and after workspace provisioning, and unchanged at the end of the cycle.
LAST_UPDATED_BY: jcwalker3 (prgs-author, AC20 implementation cycle)

[THREAD STATE LEDGER] AC20 implemented and published. **PR #814 is open at head `99fda93bccbedfe90e4c058200a65241d15f4da8`** and needs an independent reviewer. Continues the bootstrap cycle recorded in comment 14244. ### What is true now - Server-side decision state: PR #814 opened against `master` from `feat/issue-812-publish-unpublished-commit`, body carries `Closes #812`. Issue #812 gained `status:pr-open` automatically and is otherwise unchanged. Issue #635 remains open, unassigned, with no remote branch and no PR. - Local verdict/state: implementation complete and committed. Remote head `99fda93bccbedfe90e4c058200a65241d15f4da8`; local worktree synchronized to that same head, tree clean. 7 files, +1487 lines. - Live parity green throughout: `in_parity: true`, `mutation_safe: true` at `9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8`. - Identity `jcwalker3` / `prgs-author` / remote `prgs`, `identity_match: true` on every mutation. OS identity `jasonwalker` uid 502. - Protected asset unchanged: `branches/issue-635-project-registry-api` is still at `b2f6e9a6dc40e9651ef876f322dd0a68bddebfd8`, tree clean, unpublished. It was read only for presence, branch, head, cleanliness, and hashing, and nothing in it was written, reset, cleaned, checked out, rebased, or stashed. A regression in the new test file asserts that neither it nor `branch_publish.py` references that path at all (AC17). - Recorded owner pid **36856 remains alive** and is the `mcp_server.py` daemon. It is not represented as dead anywhere in this cycle, and no guard was passed by asserting otherwise (AC24). ### What changed **AC20 delivered.** New tool `gitea_publish_unpublished_issue_branch`, backed by the new pure module `branch_publish.py` (assessor, executor, read-after-write verifier). It publishes an already-committed local head to its remote branch so exact-owner renewal has the observable head its evidence model requires. Publication is the whole of its authority: it renews, reclaims, rebinds, and clears nothing. Files in scope, all 7: | File | Change | |---|---| | `branch_publish.py` | new — assessor, publisher, verifier | | `gitea_mcp_server.py` | new `@mcp.tool()` + `_publication_block` helper | | `task_capability_map.py` | `publish_unpublished_branch` → `gitea.branch.push` / author, role-exclusive | | `anti_stomp_preflight.py` | task added to `MUTATION_TASKS` and wired to shared preflight | | `docs/mcp-tool-inventory.md` | regenerated with the documented generator, not hand-edited | | `tests/test_issue_812_publish_unpublished_commit.py` | new — 36 cases | | `tests/test_task_capability_role_invariants.py` | one entry added to the deliberate tripwire constant | **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, never asserted by the caller; with no lock record it refuses outright. Guard strictness does not regress (AC15) — dirty tracked files, untracked files the commit does not carry, an unregistered worktree, a non-issue or stable branch, a branch number that does not match the issue, a changed local HEAD, an abbreviated SHA, a non-ancestor remote head, an unobservable remote, a foreign claimant or profile, a branch or worktree mismatch against the lock, a competing open PR for the issue on another branch, and any declared-hash mismatch each fail closed with its own test. The refspec names the commit SHA explicitly and never forces, so git itself rejects a non-fast-forward as a final independent guard. **AC23 honoured in code and proven in test.** The durable issue-lock file and the control-plane workflow lease are distinct records; the tool 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`. **AC24 honoured in code and proven in test.** Recorded-pid liveness is never consulted; `owner_pid_liveness_consulted` is reported `false`. One 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 discipline.** AC20 only. AC21 cannot unblock on its own, because reclaim needs a dead pid and renewal still needs the published head AC20 creates, so the two are separable and only the smaller was implemented. Entry point A and the remaining criteria stay open under this issue. **#812 entry point A was encountered live during this work and is now independently corroborated.** The first `gitea_commit_files` attempt for this very PR failed with the #510 message *"Namespace workspace binding blocked (author namespace, #510) ... Dirty tracked files in active task workspace: anti_stomp_preflight.py,"*. The escape used was to commit locally first, which cleared the dirty-tree gate; because this issue's lease was **live** rather than expired, the entry point B renewal gate did not re-materialize behind it, and `gitea_commit_files` then succeeded. That confirms the sequencing recorded in comment 14233: the gates are sequential, and an expired lease is what turns the second one into a deadlock. ### What is blocked - Blocker classification: environment/tooling blocker - BLOCKED_ROLE: author, for issue #635 only. #812 is not blocked; it is in review. - OWNING_ISSUE: #812 - PROTECTED_ASSET: `branches/issue-635-project-registry-api`, seven files, clean at unpublished local commit `b2f6e9a`, hashes matching comment 14233 - Issue #635 remains blocked until PR #814 merges **and the MCP daemon restarts**. The daemon started at `9eb0f29` and therefore does not yet have `branch_publish` loaded, so the new tool is not callable in this runtime no matter that the code now exists. Merging alone is not sufficient; runtime parity is required. - No failed sanctioned operation is outstanding. The two pre-API rejections this cycle — one ledger-schema validation and one #510 dirty-tree block — were both corrected and neither is a mutation-budget event. - #635 must not be routed to the author queue until an executed `gitea_lock_issue` returns `success: true`. ### Who/what acts next - Next actor: **reviewer** - Required action: independent review of PR #814. The author identity must not self-review, self-approve, or merge it. - Reviewer focus, offered honestly: this is the first Python code in the repository that sends a git ref to a remote, so the executor in `branch_publish.publish_commit_to_remote_branch` deserves the closest reading. Confirm the no-force refspec cannot be coerced into a history rewrite, that the ownership predicate cannot be satisfied without a pre-existing durable lock naming the caller, and that nothing in the new tool weakens #510, #618, or #713 for any caller that has not gone through it. - Do not do: do not reset, clean, check out, rebase, stash, or overwrite `branches/issue-635-project-registry-api`; commit `b2f6e9a` is unpublished and still the only copy. Do not re-implement #635. Do not assert `dead_process` on pid 36856. Do not close #812 when #814 merges beyond what `Closes #812` does — if the remaining criteria still matter, reopen or file the follow-up rather than treating AC20 as the whole issue. Do not accept comments, labels, elapsed time, or a statement as proof the #635 gate is repaired; only an executed `gitea_lock_issue` returning `success: true` proves it. ## Canonical Issue State STATE: in_review WHO_IS_NEXT: reviewer NEXT_WORKFLOW: reviewer PR review workflow BLOCKED_ROLE: author OWNING_ISSUE: #812 PROTECTED_ASSET: branches/issue-635-project-registry-api, seven files, clean at unpublished local commit b2f6e9a, hashes matching comment 14233, untouched this cycle RETRY_GATE: gitea_lock_issue(635, feat/issue-635-project-registry-api, branches/issue-635-project-registry-api) returning success true, proven by execution, after PR #814 merges and the MCP daemon restarts NEXT_ACTION: Independently review PR #814 at head 99fda93bccbedfe90e4c058200a65241d15f4da8 and post a verdict. Do not self-review or merge from the author identity. NEXT_PROMPT: ```text Review Gitea-Tools PR #814 (Closes #812) at head 99fda93bccbedfe90e4c058200a65241d15f4da8 as an independent reviewer on the prgs-reviewer namespace. Scope is AC20 only. Verify the publication executor cannot rewrite history, that ownership cannot be proven without a pre-existing durable issue-lock record naming the caller, and that guard strictness under #510, #618, and #713 does not regress. Never read or modify branches/issue-635-project-registry-api. ``` WHAT_HAPPENED: The bootstrap workspace acquired in comment 14244 was used to implement AC20 as a native MCP publication operation with a pure assessor module and 36 synthetic-fixture regressions. The work was committed locally after the #510 dirty-tree gate refused the first publication attempt, then published through gitea_commit_files as remote commit 99fda93, the local worktree was synchronized to that head, and PR #814 was opened closing #812. WHY: AC20 was selected over AC21 because comment 14241 established that publication alone is sufficient to unblock #635 while reclamation can never be satisfied against a live daemon pid. Implementing the smaller of two separable criteria keeps the change auditable and leaves the reclaim predicate untouched rather than softened. RELATED_PRS: PR #814 open at head 99fda93bccbedfe90e4c058200a65241d15f4da8, base master, closing #812. Branch feat/issue-812-publish-unpublished-commit published. Protected branch feat/issue-635-project-registry-api remains unpublished. Prior ledger comments at #635 are 14185, 14215, 14225; at #812, comments 14233, 14241, and 14244. BLOCKERS: Blocker classification environment/tooling blocker, scoped to issue #635 only. The new capability exists in code but not in the running daemon, which started at 9eb0f29 and does not have branch_publish loaded, so the tool is not callable until PR #814 merges and the daemon restarts. Until then assess_exact_owner_lease_renewal lines 328-332 still require an observable remote head and assess_expired_lock_reclaim lines 455-465 still require a dead pid or missing worktree, while pid 36856 is a live mcp_server.py daemon and the registered worktree is present. VALIDATION: gitea_whoami identity_match true before every mutation; gitea_resolve_task_capability allowed_in_current_session true for lock_issue, comment_issue, commit_files, and create_pr, resolved immediately before each. Parity in_parity and mutation_safe true at 9eb0f29cefa85fd0ecbbb5f923ebc748bffa81f8. Namespace gitea-author healthy with probe_source client_namespace. gitea_lock_issue returned success true for #812 with adoption_decision NO_MATCH and competing_branch_check clear. gitea_commit_files returned success with content_source_proof local_path for all 7 files at remote commit 99fda93bccbedfe90e4c058200a65241d15f4da8. gitea_create_pr returned PR number 814 with issue_status_transition performed true. New suite tests/test_issue_812_publish_unpublished_commit.py 36 passed. Full suite 11 failed, 4354 passed, 6 skipped, 533 subtests passed, against a pre-edit baseline on this same branch of 11 failed, 4318 passed, 6 skipped, 531 subtests passed, so the 11 failures are unchanged in count and identity and the delta is 36 added tests and 2 added subtests. Protected worktree verified read-only at b2f6e9a with a clean tree and 7 of 7 SHA-256 hashes equal to comment 14233, before and after workspace provisioning, and unchanged at the end of the cycle. LAST_UPDATED_BY: jcwalker3 (prgs-author, AC20 implementation cycle)
sysadmin removed the status:pr-open label 2026-07-22 16:40:56 -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#812