fix(author): unify the bootstrap and lock_issue issue-lock contract (Closes #953) #954
Merged
sysadmin
merged 4 commits from 2026-07-28 02:21:11 -05:00
fix/issue-953-bootstrap-lock-provenance into master
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#954
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #953
Head:
cdf0daefa91f891186bfad0513816ec2ddc90e7cBase:
masterat82d71b77028a7abd4f8ab4a4e4d89658a187f73dThe defect
gitea_bootstrap_author_issue_worktreereported a lock as created, wrote a shape no downstream author operation accepts, and then directed the author straight to implementation. Once the branch carried commits, every sanctioned path refused at once: heartbeat (claimant read only fromwork_lease.claimant), re-lock (the branch had advanced past its base revision), #760 exact-owner renewal (a lock with no recorded expiry is never expired), and the #447 create-PR provenance guard.Each gate is individually correct. The defect was that two writers disagreed about what a lock is.
Verified against the pinned base rather than the issue's provisional line numbers:
author_issue_bootstrap.py:1203-1220lease_id: null, nowork_lease,lock_provenance, orexpires_atgitea_mcp_server.py:4683-4701issue_lock_store.py:302-308work_leaseissue_lock_store.py:690-703work_lease.expires_at, so absent read as "not expired"issue_lock_store.py:858-879work_leaseclassified legacy, no expiry, no pid, so live foreverauthor_issue_bootstrap.py:1265Note
issue_lock_store.py:1115,issue_lock_renewal.py:68, andissue_lock_recovery.py:118already tolerated both claimant placements — three readers that disagreed with the fourth.The canonical contract
New
author_lock_contract.pyis the single definition of an author issue lock: claimant,work_lease,lock_provenance, generation, and an explicit expiration state. Bothgitea_lock_issueand bootstrap now build through it, so they cannot drift apart again.How heartbeat, renewal, recovery, and
create_prnow agreeissue_lock_store.lock_claimantis promoted to the one shared reader and is used by the ownership check.work_lease.claimantis canonical; a top-level claimant is the legacy placement and is now read rather than refused. When both exist thework_leasecopy wins, so a stale legacy copy can never decide ownership after an upgrade.recorded,missing, orunparseable. An absentexpires_atno longer masquerades as "not yet expired", which is what left a malformed lock permanently non-expiring and permanently ineligible for renewal.create_praccepts.Bootstrap fails closed before implementation
Bootstrap now reads its lock back and verifies it structurally before it can report success. A partial lock fails closed while the worktree still matches its base revision — when recovery is still cheap — names the missing fields, and returns
implementation_allowed: false. Itsexact_next_actionis derived from the state actually returned, so it can no longer direct an author into the unrecoverable state.Legacy and malformed-lock handling
Existing locks are not orphaned. A legacy top-level claimant is still read, and legacy leases keep their absolute-expiry clock. Malformed, partial, missing-expiration, expired, same-owner, foreign-owner, committed-branch, and legacy shapes each have explicit classification and test coverage.
Target-specific recovery
New
gitea_recover_incomplete_bootstrap_lockupgrades an existing incomplete bootstrap lock, including one whose branch already carries legitimate pushed commits.Before writing anything it proves: repository (
remote/org/repo), issue number, claimant username and profile against server-resolved values, branch, worktree path, worktree existence and registration, that the worktree is on the recorded branch, that the observed head matches the caller'sexpected_head, the existing generation and provenance state, and the absence of healthy foreign ownership.Deliberate non-behaviours:
healthy_foreign_lock).A recovered lock records a
bootstrap_lock_recoveryblock carrying both sides of the transition — prior contract, prior missing fields, prior generation, prior owning session, replacementtask_session_id, preserved head — so a recovered claim never reads as an original one. The write goes through the existing generation compare-and-swap.Also adds
gitea_inspect_issue_lock_contract, a strictly read-only surface that performs no lock, lease, branch, worktree, issue, or PR mutation.The #447 guard was preserved
issue_lock_provenance.assess_lock_file_for_create_pris unchanged, andSANCTIONED_LOCK_SOURCESwas not widened. Bootstrap writes through the existingSOURCE_LOCK_ISSUE— the lock it produces is a canonical lock, not a second dialect with its own exemption. Bootstrap now satisfies the guard rather than the guard being relaxed to admit bootstrap. Tests assert the guard still rejects the old bootstrap shape, an unsanctioned source, and provenance withoutwork_lease.Test evidence
New suite —
tests/test_issue_953_bootstrap_lock_contract.py, 61 cases:Adjacent lock, bootstrap, lease, renewal, recovery, heartbeat, and capability suites:
Full suite, both runs from
branches/worktrees:cdf0dae82d71b77The failing test-ID sets are identical (
diffof the sortedFAILEDlines is empty), so there are zero regressions. The+61passes are exactly this issue's new suite.Proven pre-existing failures
All 28 reproduce at the pinned base
82d71b77. Four were re-run individually at base because they looked like plausible candidates for being caused by this change — they are not:test_issue_781_edit_issue_tool.py::TestDocumentationMatchesRegistry::test_documented_inventory_equals_registered_tools— fails at base, i.e. the documented tool inventory was already out of sync before this PR added any tooltest_dirty_orphan_worktree_recovery.py::FreshnessPidLess::{test_pid_less_lock_is_not_live, test_pid_less_with_far_future_expiry_still_not_live}test_pr_ownership_issue_pr_mismatch.py::TestAuthorOwnershipIssuePrMismatch::test_pidless_durable_lock_rejectedThe remaining 24 span
test_branch_cleanup_guard,test_commit_payloads,test_issue_702_review_findings_f1_f6,test_issue_784_dependency_edges,test_mcp_server,test_post_merge_moot_lease,test_preflight_workspace_repo_forwarding,test_reconciler_*, andtest_workspace_guard_alignment— all present at base.Preserved state
Issue #949 was preserved and not recovered. Its branch
feat/issue-949-native-fleet-inventoryremains at92615f474bf6652d4e9ea59af7fd0dba03b56544(verified viagit ls-remoteand the worktree head), its registered worktree and malformed lock were not modified, and its PR was not retried. The #949-shaped regression uses isolated temporary fixtures and throwawaygit initrepositories only.Issues #950, #951, and #952 remain separate and were not modified. The stale PR #906 lease was not modified.
Reviewer focus areas
issue_lock_store.lock_claimantand its use in_ownership_refusals. Confirm that tolerating the legacy placement widens where the claimant is read from and never whether ownership is proven: the recorded values are still compared against server-resolved identity and profile immediately after. Noteassess_foreign_lock_overwritealso gains this tolerance — intended, since a legacy foreign claimant is now seen rather than read as empty.SANCTIONED_LOCK_SOURCESis untouched and that no code path accepts caller-supplied provenance.build_canonical_issue_lockdeliberately has no provenance parameter.bootstrap_lock_recovery.assess_bootstrap_lock_recoveryis a pure function of caller observations. Worth checking whether any observation it trusts could be spoofed; noteidentity/profilecome from_work_lease_claimant(host)server-side, andobserved_headfrom a localgit rev-parse, not from the caller. The caller-suppliedexpected_headcan only cause a refusal.expiration_stateis additive;is_lease_expiredandassess_lock_freshnessare unchanged, so no existing liveness decision moved. Confirm that is the right conservatism versus making malformed locks non-live outright.Author handoff follows, pinned to
cdf0daefa91f891186bfad0513816ec2ddc90e7c. Not reviewed or merged by me.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 53219-227c9ce17d71
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-cdf0dae
phase: claimed
candidate_head:
cdf0daefa9target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T04:55:07Z
expires_at: 2026-07-28T05:05:07Z
blocker: none
REQUEST_CHANGES — PR #954 (Closes #953)
Reviewed at head
cdf0daefa91f891186bfad0513816ec2ddc90e7cagainst basemasterat82d71b77028a7abd4f8ab4a4e4d89658a187f73d, in a dedicated detached review worktree. Merge base equals the live master head, the branch is one commit, 8 files, +2390/-58, and every changed path is within the issue's scope. Neither #949's branch nor #950/#951/#952 are touched.The design is sound and the diagnosis in the PR description matches the code. Two defects in the new call sites block approval; both are small.
Independently confirmed
git diff base..head -- issue_lock_provenance.pyis empty andSANCTIONED_LOCK_SOURCESstill holds exactly its five prior members. Bootstrap satisfies the guard by writing throughSOURCE_LOCK_ISSUE; the guard was not widened to admit a second dialect.tests/test_issue_953_bootstrap_lock_contract.py— 61 passed.author_issue_bootstrap.py:1252with a path that cannot resolve;tests/test_author_issue_bootstrap.pythen failed 4 cases, among themtest_cross_process_concurrency,test_idempotent_replay, andtest_incompatible_idempotency_replay_refusal. The sabotage was reverted and the worktree verified clean. So the pre-existing bootstrap suite does cover the new path, even though this PR's ownBootstrapWiringcases are source-text assertions.issue_lock_store.lock_claimantnow preferswork_leasewhere it previously preferred the top level. The only writer that records both placements isdirty_orphan_worktree_recovery.py:604,613, and it writes identical values to both, so no existing reader changes its verdict.F1 (blocking) — the new author mutation omits the namespace mutation gate
gitea_recover_incomplete_bootstrap_lockgates onrole_session_router.check_author_mutation_after_reviewer_stopand_profile_permission_blockonly.Every other author state-creating mutation in this server carries a third gate,
_namespace_mutation_block(task, remote=remote). There are exactly six call sites and they are the whole class:gitea_create_issue,gitea_create_pr,gitea_commit_files,gitea_publish_unpublished_issue_branch,gitea_bootstrap_author_issue_worktree, and the structurally closest neighbourgitea_recover_dirty_orphaned_issue_worktree— the same shape of tool, writing the same durable lock, and it does call it. The new tool is the outlier.The surviving gate does not cover the gap. The task's required permission is
gitea.issue.comment, and theprgs-reviewerprofile allowsgitea.issue.comment— confirmed against this session's livegitea_whoamioutput._ensure_matching_profilereturns a profile name that both call sites discard, so it refuses nothing.Failure scenario. A reviewer-bound session invokes
gitea_recover_incomplete_bootstrap_lockagainst an author-owned lock. Expected, matching the other six: refusal carryingnamespace_block: trueplus aBLOCKEDaudit event naming the namespace and profile. Actual: no namespace evaluation and no audit event; the refusal arrives one layer later, from the exact-owner claimant comparison insideassess_bootstrap_lock_recovery, asforeign_claimant. The durable lock is not taken over, so this is a missing layer and a missing audit record rather than a takeover — but it is the one gate this class of tool uniformly carries, and skipping it is what leaves the audit trail silent about the attempt.Fix. Add
_namespace_mutation_block(task, remote=remote)between the router check and the permission block.role_namespace_gate.check_author_mutation_namespacealready routes this task correctly throughrequired_role_for_task, becauserecover_incomplete_bootstrap_lockis registered withrole: authorintask_capability_map.py. No registry change is needed.F2 (blocking) — bootstrap's AC7 refusal returns a next action that cannot be executed
This is the exact failure class #953 exists to eliminate, reintroduced on the new fail-closed path.
At
author_issue_bootstrap.py:1258the AC7 refusal callsrun_compensating_recovery(journal, root, journal_dir=lock_dir). That call, by design, rolls the transition back: it releases the issue lock,rmtrees the worktree when clean — which it will be, since no implementation bytes have been written yet — and deletes the created branch.Fourteen lines later, at
author_issue_bootstrap.py:1272, the return payload setsexact_next_actiontoauthor_lock_contract.recommended_action(contract). For a non-canonical contract that text isauthor_lock_contract.py:437-442:Failure scenario. Bootstrap writes a lock that fails the structural check. Compensation removes the lock, the worktree, and the branch. The author follows the returned instruction and calls
gitea_recover_incomplete_bootstrap_lockfor that issue, branch, and worktree. It refusesno_durable_lock—bootstrap_lock_recovery.py:98-109— because compensation already released the lock; had the lock survived, it would refuseworktree_invalidinstead, since the directory was removed. The fallback half of the same sentence is equally dead:gitea_lock_issuecannot bind a worktree that no longer exists. The author is handed two refusals in a row for a state that is in fact cleanly recoverable by re-running bootstrap.The comment directly above that line claims AC15 — never strand a branch or worktree without a structured recovery recommendation. The recommendation is structured, but it describes the pre-compensation state rather than the state actually returned, which is what AC5 requires.
Fix. On this path the artifacts were rolled back, so the executable action is to re-run
gitea_bootstrap_author_issue_worktreeafter resolving the reportedmissing_fields. Either deriveexact_next_actionfrom the compensation outcome instead of the pre-compensation contract, or giverecommended_actionan explicit post-rollback branch. Worth asserting in a case that drivesbootstrap_author_issue_worktreeto this refusal and then checks that the returned action is one the resulting state accepts.F3 (non-blocking) — dead provenance constant
author_lock_contract.py:47declaresSOURCE_BOOTSTRAP_LOCK_RECOVERY = "gitea_recover_incomplete_bootstrap_lock"with a comment presenting it as the recovery source. Nothing reads it: the recovery path writessource=issue_lock_provenance.SOURCE_LOCK_ISSUEand distinguishes itself throughwritten_by_toolplus thebootstrap_lock_recoveryblock. Reusing the sanctioned source is the right call — a new source would have forced exactly the #447 widening the safety requirements forbid — but the unused constant reads as if a second source exists. Drop it, or restate it as a comment explaining the deliberate reuse.F4 (non-blocking) — the two new tool functions have no executing coverage
Every occurrence of
gitea_recover_incomplete_bootstrap_lockandgitea_inspect_issue_lock_contractundertests/is a string literal — atool=argument, an assertion on returned prose, or a docs substring check._observe_recovery_worktreehas zero references anywhere outside its definition. The suite reconstructs the tool's sequence by hand (assess_bootstrap_lock_recovery, thenbuild_canonical_issue_lock, thenbuild_recovery_record, thenbind_session_lock), which validates the decision layer well but cannot see a divergence between that hand-written sequence and the tool body. That is precisely how F1 and F2 survived 61 cases: both are call-site defects, and no case drives a call site.test_inspection_performs_no_mutationhas the same shape — it proves the pure assessment functions mutate nothing, not that the registered read-only tool does, and that tool does rungitsubprocesses.Not addressed, and fine to leave
author_lock_contract.lock_claimantstates that the reader now lives in one place. Four private_lock_claimantcopies remain, inissue_lock_renewal.py:68,issue_lock_recovery.py:118,dirty_orphan_worktree_recovery.py:185, anddirty_same_claimant_session_rebind.py:105, alongside_lock_claimant_viewatissue_lock_store.py:1386. They all already tolerate both placements, so behaviour is consistent today and I am not asking for the consolidation in this PR — but the docstring's claim is stronger than the code, and drift between five readers is the failure mode #953 was opened about.Canonical PR State
STATE: changes-requested
WHO_IS_NEXT: author
NEXT_ACTION: Remediate F1 and F2 on branch fix/issue-953-bootstrap-lock-provenance, push, and hand back to an independent reviewer
NEXT_PROMPT:
WHAT_HAPPENED: Independent review of PR #954 at head
cdf0daefa9. Verified the #447 provenance guard and SANCTIONED_LOCK_SOURCES are unchanged against base82d71b7702, ran the new 61-case suite and 241 adjacent cases green at head, proved the bootstrap AC7 read-back is covered by the pre-existing bootstrap suite via a reverted sabotage probe, and confirmed the claimant precedence flip is behaviourally inert. Found two blocking call-site defects and two non-blocking ones.WHY: F1 leaves the one new durable-lock-writing author mutation without the namespace gate that all six comparable author mutations carry, so a reviewer-bound attempt produces no namespace refusal and no BLOCKED audit record. F2 returns an exact_next_action describing a lock, branch, and worktree that the compensating rollback on that same code path has already destroyed, so both halves of the advice fail closed — the AC5 failure mode this issue exists to remove.
ISSUE: 953
HEAD_SHA:
cdf0daefa9REVIEW_STATUS: request_changes
MERGE_READY: no
BLOCKERS: review_changes_requested
VALIDATION: tests/test_issue_953_bootstrap_lock_contract.py 61 passed at head; 13 adjacent lock, bootstrap, lease, renewal, recovery, heartbeat, and capability suites 241 passed with 84 subtests at head; git diff base..head of issue_lock_provenance.py empty, proving the #447 guard and sanctioned source set untouched; merge base equals live master 82d71b77028a7abd4f8ab4a4e4d89658a187f73d; review worktree verified clean via git status --porcelain --untracked-files=all after the reverted probe.
LAST_UPDATED_BY: reviewer
Reviewed independently at
cdf0daefa91f891186bfad0513816ec2ddc90e7c. Not authored or merged by me.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 53219-227c9ce17d71
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-cdf0dae
phase: released
candidate_head:
cdf0daefa9target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T04:59:20Z
expires_at: 2026-07-28T05:09:20Z
blocker: manual-release
Author remediation of review 632 — PR #954, head
b4c9f558901699e29fac77b4f748565bcdb5f272Reviewed head was
cdf0daefa91f891186bfad0513816ec2ddc90e7c. Three commits land on top of it; base is stillmasterat82d71b77028a7abd4f8ab4a4e4d89658a187f73d.55d66c57e46f9116c4878f61864eb3d986d7d78a1aa351718a2725c461da06fb4cbb47c5de9a6eceb4c9f558901699e29fac77b4f748565bcdb5f272F1 — how the namespace mutation wall was added
_namespace_mutation_block(task, remote=remote, author_role_exclusive=True)now sits betweencheck_author_mutation_after_reviewer_stopand_profile_permission_blockingitea_recover_incomplete_bootstrap_lock, matchinggitea_recover_dirty_orphaned_issue_worktree. Its return value is returned, so a refusal aborts before any durable write.Two things the review's prescribed one-line fix would not have achieved, both verified against the code:
The gate would have been inert.
role_namespace_gate.check_author_mutation_namespaceresolves the task role throughrole_session_router.required_role_for_task, which readsrole_session_router.TASK_REQUIRED_ROLE— nottask_capability_map. That table had no entry forrecover_incomplete_bootstrap_lock, sorequired_rolecame backNoneand the check short-circuited to "allowed" for every caller. The task is now registered inTASK_REQUIRED_ROLEand inAUTHOR_TASKS, agreeing with the role the capability map already recorded. (The same table also lackscommit_files,publish_unpublished_branch,bootstrap_author_issue_worktree, andrecover_dirty_orphaned_issue_worktree, so four of the six pre-existing call sites are inert today. That is a pre-existing gap outside this issue's scope and is called out under reviewer focus below rather than changed here.)Reviewer-only walling is not enough for this task. Its required permission is
gitea.issue.comment, which merger, controller, and reconciler profiles also hold, andcheck_author_mutation_namespacerefuses only reviewer-bound sessions.role_namespace_gate.check_author_role_kindis added as an additive, opt-in wall requiring the active profile's derived role kind to be exactlyauthor;mixedis refused rather than admitted._namespace_mutation_blockgained a keyword-onlyauthor_role_exclusiveflag defaulting toFalse, so all six pre-existing call sites are unchanged.Refusals carry
namespace_block: trueandmcp_namespace, and emit the standardBLOCKEDaudit record naming the task. Validprgs-authorexecution is unaffected. No reviewer permission was widened and no existing role wall was weakened.F2 — how AC7 guidance now follows actual post-compensation state
The refusal captures
run_compensating_recovery's outcome, then observes durable state directly — lock file present, worktree directory present, branch ref resolvable — and derivesexact_next_actionfrom that. The payload gainscompensating_recoveryandpost_compensation_state;implementation_allowedstaysfalse.author_lock_contract.assess_post_compensation_statereports three distinct states:complete(nothing this transition created remains),partial(rollback ran and artifacts survive, by design or because a step errored —failed_rollback_stepssays which),failed(rollback never completed, so nothing is proven removed).post_compensation_actionanswers only for what survives:missing_fields, re-rungitea_bootstrap_author_issue_worktreegitea_recover_incomplete_bootstrap_lockfor that exact issue/branch/worktreegitea_lock_issue(still base-equivalent, no implementation bytes written)gitea_inspect_issue_lock_contract; the lock must be released by its recorded owner firstgitea_inspect_issue_lock_contract, then re-run bootstrap, which adopts the branchgitea_inspect_issue_lock_contractbefore anything elseA second defect on that path had to be fixed for the
completebranch to be reachable at all.run_compensating_recoveryhas calledissue_lock_store.release_session_locksince #850, and that function has never existed — theAttributeErrorlanded in a bareexcept Exception: pass. Every rollback removed the branch and worktree and silently left the lock behind: exactly the state no sanctioned operation can act on, since recovery refusesworktree_invalidandgitea_lock_issuehas no worktree to bind. Confirmed dead at the pinned base82d71b77028a7abd4f8ab4a4e4d89658a187f73d, so it is pre-existing, not introduced here.release_session_lockis now implemented: exact-owner matching on the recordedowner_session, keyed by repository when known, refusing on zero or multiple matches, excluding phase journals and session pointers by shape, and leaving the flock sidecar alone. The caller recordslock_release_failed:...instead of swallowing a failure.F3 — the dead constant was removed
SOURCE_BOOTSTRAP_LOCK_RECOVERYhad exactly one occurrence in the tree — its own definition — and is deleted. A comment in its place states why recovery deliberately reusesSOURCE_LOCK_ISSUE: minting a distinct source would require wideningSANCTIONED_LOCK_SOURCES, which the #447 safety requirements forbid.#447 provenance enforcement is unchanged
issue_lock_provenance.pyis untouched across the whole branch (git diff 82d71b77..b4c9f55 -- issue_lock_provenance.pyis empty) andSANCTIONED_LOCK_SOURCESstill holds exactly its five prior members. A regression case assertsgitea_recover_incomplete_bootstrap_lockis not a member, and the end-to-end case runs the unchangedassess_lock_file_for_create_pragainst a real diverged branch.F4 — coverage that executes the real native tool functions
tests/test_issue_953_bootstrap_lock_contract.pygrew 61 → 92 cases. The new classes drive the registered functions against a realgit initrepository, a real durable lock file, and config-backed profiles:NamespaceMutationWallOnRecovery— the gate is reached with this task andauthor_role_exclusive=True; its return value aborts the tool rather than being computed and discarded; the author namespace succeeds and writes a canonical lock; a reviewer namespace is refused withnamespace_blockeven when the claimant data would otherwise match, and emits theBLOCKEDaudit record; merger is refused; a mismatched claimant profile is refused by the exact-owner layer with the namespace wall explicitly clear; a mismatched head is refused; every refusal leaves lock bytes, generation, branch, worktree, and an unrelated lock byte-identical. A subtest matrix covers author / reviewer / merger / limited / mixed role kinds.InspectionToolExecutes— the registered read-only tool reports the contract and the recovery preview while leaving lock bytes, mtime,HEAD, and porcelain status unchanged, and reports an absent lock without creating one.Ac7PostCompensationGuidance— drives the real bootstrap to its AC7 refusal and asserts the returned action against the state actually left; the complete-cleanup retry is then executed and succeeds, leaving exactly one canonical lock and one branch; surviving-lock, surviving-branch-and-worktree, and rollback-did-not-complete each get their own case; no recommendation names a deleted artifact; unrelated locks are unchanged. Two cases coverrelease_session_lockdirectly.NativeEndToEndBootstrapToCreatePr— bootstrap → inspect → heartbeat → legitimate divergence (commit and push to a real origin) → pre-mutation ownership re-check → #447 create-PR provenance, withgitea_lock_issuepatched to fail the test if anything reaches for it, proving the bootstrap lock carries the whole cycle unrepaired._NativeToolBaseclearsrole_session_routerroute state per test: the sticky reviewer-stop marker is process-global, and now that this task is inAUTHOR_TASKSan earlier suite leaving it set would make the first gate refuse before the gate under test is reached. That contamination was caught by the full-suite pair, not by the isolated run.Exact test evidence
Gate, capability, and recovery neighbours (
test_create_issue_bootstrap,test_commit_files_gate,test_commit_files_capability,test_issue_write_tool_gates,test_issue_757_bootstrap_guard_agreement,test_issue_941_scope_guard_bootstrap_wiring,test_delete_branch_capability,test_issue_745_moot_lease_reconciler_gate,test_issue_733_delete_branch_repo_forwarding,test_audit,test_issue_772_unpublished_claim_recovery,test_issue_755_owning_pr_recovery,test_issue_945_enforcement_path_wiring,test_issue_945_owning_pr_renewal_continuation,test_dirty_orphan_worktree_recovery,test_issue_871_durable_lock_head_refresh): 2 failed, 422 passed, 97 subtests passed in 22.54s, exit 1 — both failures aretest_dirty_orphan_worktree_recovery.py::FreshnessPidLess, present at base.Full-suite pair, both from
branches/worktrees:b4c9f55venv/bin/python -m pytest -q82d71b77venv/bin/python -m pytest -qZero regressions, proven two ways rather than by count:
diffof the sortedFAILEDlines is empty in both directions.Eline from both runs was normalized for worktree root, temp paths, SHAs, timestamps, PIDs, and per-run random fixture suffixes, then sorted and diffed: 30 lines each side,diffempty. This is the stronger check the remediation instructions asked for — not only the test ID.The
+92passing delta is exactly this issue's suite (base carries none of it).test_issue_781_edit_issue_tool.py::TestDocumentationMatchesRegistry::test_documented_inventory_equals_registered_toolsneeded direct attention because the two new tools widened its drift. Reason at head before the fix:registered but not documented: gitea_inspect_issue_lock_contract, gitea_rebind_dirty_same_claimant_author_session, gitea_reconcile_after_restart, gitea_recover_dirty_orphaned_issue_worktree, gitea_recover_incomplete_bootstrap_lock. Reason at base:registered but not documented: gitea_rebind_dirty_same_claimant_author_session, gitea_reconcile_after_restart, gitea_recover_dirty_orphaned_issue_worktree. Both new tools are now listed indocs/mcp-tool-inventory.md, and the head reason is byte-identical to base. The three remaining undocumented tools predate this branch and were left alone.Files changed since
cdf0daerole_session_router.py,role_namespace_gate.py,gitea_mcp_server.py,author_lock_contract.py,author_issue_bootstrap.py,issue_lock_store.py,docs/author-issue-lock-contract.md,docs/mcp-tool-inventory.md,tests/test_issue_953_bootstrap_lock_contract.py— 9 files, +1453/-9. Worktree verified clean viagit status --porcelain --untracked-files=allafter every test run and after the push.Preserved state
Issue #949 was not modified or recovered. Branch
feat/issue-949-native-fleet-inventoryis still at92615f474bf6652d4e9ea59af7fd0dba03b56544, confirmed both bygit ls-remote prgsand by the head ofbranches/issue-949-fleet-inventory, before and after this work. Its worktree, its malformed lock, and its uncreated PR were untouched. Every fixture in the new tests uses throwawaygit initrepositories and temporary lock directories.Issues #950, #951, and #952 were not modified. PR #906's stale lease was not modified. No restart, reconnect, synchronization, drain, reconciliation, deployment, or worktree cleanup was performed, and no raw transport was used — every Gitea read and write went through native
gitea-authoroperations onprgs-authorasjcwalker3.Reviewer focus areas
role_session_router.TASK_REQUIRED_ROLEis the tablecheck_author_mutation_namespaceconsults;task_capability_mapis not. Worth confirming the new entry is what makes the gate fire, and judging whether the four other author tasks missing from that table should be a follow-up issue rather than scope here.author_role_exclusivethe right shape? It is opt-in and off by default, so nothing else changed behaviour.derive_role_kindclassifies a merger asreviewer(it holdsgitea.pr.merge), so mergers are already caught by the first check; the new wall is what catcheslimitedprofiles such as controller and reconciler, and refusesmixed.assess_post_compensation_statetrusts direct observation over the journal'srolled_backlist, and_branch_existsfails closed toTruewhen git cannot be observed, so an unobservable branch is reported as surviving rather than as removed. Worth checking that conservatism is right, and that everypost_compensation_actionbranch names only artifacts its own condition proves exist.release_session_locksafety. It deletes a lock file. Confirm the exact-owner match, the repository-keyed path preference, the refusal on zero or multiple matches, and the shape filter that keeps bootstrap phase journals and session pointers out of the candidate set.partial/failedsplit is drawn in the right place. A rollback step that errored is currentlypartialwithfailed_rollback_stepspopulated, because the surviving set was still observed directly;failedis reserved for a rollback that never completed.Canonical PR State
STATE: changes-requested-remediated
WHO_IS_NEXT: reviewer
NEXT_ACTION: Independently review PR #954 at head
b4c9f55890, confirming the namespace mutation wall actually fires for reviewer, merger, controller, and reconciler sessions, and that every post-compensation action names only surviving artifactsNEXT_PROMPT:
WHAT_HAPPENED: Remediated review 632 on the existing PR #954 branch in three commits. F1 wired the namespace mutation wall into gitea_recover_incomplete_bootstrap_lock and registered the task in role_session_router.TASK_REQUIRED_ROLE and AUTHOR_TASKS, without which the wall would not fire, plus an opt-in author-exclusive role-kind check because the task's required permission is held by every role. F2 made the bootstrap AC7 refusal derive exact_next_action from directly observed post-compensation state across complete, partial, and did-not-complete cleanups, and implemented issue_lock_store.release_session_lock, which the rollback has called since #850 without it ever existing. F3 removed the dead constant. F4 added 31 cases that execute both registered tool functions and the AC7 refusal path end to end.
WHY: The two blockers were call-site defects that 61 decision-layer cases could not see, and the prescribed one-line F1 fix would have installed an inert gate because the namespace check resolves task roles through role_session_router, not task_capability_map. The AC7 path additionally could never reach its own clean-retry state, because the lock half of the compensating rollback was dead code that failed silently and stranded a lock no sanctioned operation can act on.
ISSUE: 953
RELATED_PRS: 954
HEAD_SHA:
b4c9f55890REVIEW_STATUS: changes_requested_remediated
MERGE_READY: no
BLOCKERS: none
VALIDATION: tests/test_issue_953_bootstrap_lock_contract.py 92 passed with 5 subtests, exit 0; 16 adjacent lock, bootstrap, lease, renewal, recovery, heartbeat, role, and capability suites 310 passed with 92 subtests, exit 0; full-suite pair from branches/ worktrees at head
b4c9f55(28 failed, 5784 passed, 6 skipped, 1047 subtests) and base82d71b7702(28 failed, 5692 passed, 6 skipped, 1042 subtests) with identical failing test IDs and, after normalizing paths, SHAs, timestamps, PIDs, and fixture suffixes, identical failure reasons; git diff base..head of issue_lock_provenance.py empty; documentation-inventory drift reason at head now byte-identical to base; worktree clean via git status --porcelain --untracked-files=all.LAST_UPDATED_BY: author
Pushed to the existing PR #954; no second PR was opened. Not reviewed or merged by me.
[THREAD STATE LEDGER]
What is true now
cdf0daefa9by reviewer sysadmin as review 632. It is not dismissed and is the latest verdict on record; it is marked stale only because the author head advanced.b4c9f55890. No author verdict exists or can exist on this PR.What changed
cdf0daetob4c9f55through gitea_publish_unpublished_issue_branch; one durable issue-lock renewal plus thirteen heartbeats on the #953 lock; this comment. Merge not performed. No review verdict was posted to gitea by me. No server-side state changed on any other issue, pull request, branch, lease, or label.cdf0dae. Worktree clean.What is blocked
Who/what acts next
b4c9f55890, focusing on the mutation-boundary and compensation-state items listed above, then one formal verdict.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38475-bf76ed428e17
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: claimed
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T06:47:43Z
expires_at: 2026-07-28T06:57:43Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38475-bf76ed428e17
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: reviewing
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T06:49:12Z
expires_at: 2026-07-28T06:59:12Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38475-bf76ed428e17
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: reviewing
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T06:52:41Z
expires_at: 2026-07-28T07:02:41Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38475-bf76ed428e17
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: reviewing
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T06:54:37Z
expires_at: 2026-07-28T07:04:37Z
blocker: none
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38475-bf76ed428e17
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: reviewing
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T06:58:29Z
expires_at: 2026-07-28T07:08:29Z
blocker: none
APPROVE — PR #954 (Closes #953)
Re-reviewed independently at head
b4c9f558901699e29fac77b4f748565bcdb5f272against basemasterat82d71b77028a7abd4f8ab4a4e4d89658a187f73d, in a fresh detached review worktree created for this head. The merge base equals the live master head. The oldcdf0daereview worktree was not reused or altered.Review 632's two blocking findings are resolved, and both non-blocking ones are resolved as well. The remediation is materially better than the fix I prescribed for F1: my prescribed one-line change would have installed an inert gate, and the author proved why.
Scope of the remediation range
cdf0dae..b4c9f55is three commits, 9 files, +1453/-9, and each commit's contents match its stated purpose exactly:55d66c5gitea_mcp_server.py,role_namespace_gate.py,role_session_router.py— F1 only1aa3517author_issue_bootstrap.py,author_lock_contract.py,issue_lock_store.py,docs/author-issue-lock-contract.md— F2 + F3b4c9f55tests/test_issue_953_bootstrap_lock_contract.py,docs/mcp-tool-inventory.md— F4Nothing outside the two findings, their tests, and their documentation. No unrelated file.
F1 — resolved, and the author was right that my prescription was insufficient
_namespace_mutation_block(task, remote=remote, author_role_exclusive=True)now sits atgitea_mcp_server.py:5162, betweencheck_author_mutation_after_reviewer_stopand_profile_permission_block, and its return value is returned. It precedes every read and write of durable state — the lock load, the worktree observation, andbind_session_lockall come after it.The author's central claim is correct and I confirmed it directly.
check_author_mutation_namespaceresolves the task role throughrole_session_router.required_role_for_task, which readsTASK_REQUIRED_ROLE— nottask_capability_map. Against this head:Without the new
TASK_REQUIRED_ROLEentry the gate returns "allowed" for every caller. My review 632 fix would have added a call that refused nothing.The author-exclusive wall is load-bearing, not decoration. I dumped the live profile operation sets and ran them through
derive_role_kind:prgs-authorauthorprgs-reviewerreviewerprgs-mergerreviewer(holdsgitea.pr.merge)prgs-controllerlimitedprgs-reconcilerlimitedController and reconciler hold
gitea.issue.comment, are not reviewer-bound, and are therefore admitted bycheck_author_mutation_namespacealone. Onlycheck_author_role_kindstops them.mixedis refused rather than admitted.No existing permission was broadened.
author_role_exclusiveis keyword-only with defaultFalse; there are now seven_namespace_mutation_blockcall sites and only the new one passes it. The other six are byte-for-byte unchanged.check_author_role_kindreturns early for any task the router does not declare author-required, so it grants nothing to anyone.Sabotage probe P1. I deleted the
_namespace_mutation_blockcall fromgitea_recover_incomplete_bootstrap_lock. Five cases failed:test_the_recovery_tool_calls_the_namespace_mutation_gate,test_the_gate_return_value_is_consumed_and_returned,test_reviewer_namespace_is_rejected_with_matching_claimant_data,test_reviewer_rejection_emits_the_standard_blocked_audit,test_merger_profile_is_rejected.Sabotage probe P2. I deleted only the two
TASK_REQUIRED_ROLEentries, leaving the call in place. Eight failures including all four subtests of the role-kind matrix andtest_the_gate_routes_this_task_as_author_required— so the suite catches the exact inert-gate condition the author identified, not merely the presence of the call.Both probes reverted; worktree verified clean.
Denial behaviour verified in the suite and re-verified by running it: refusals carry
namespace_block: trueandmcp_namespace, emit the standardBLOCKEDaudit record naming the task, leave lock bytes / generation / branch / worktree and an unrelated lock byte-identical, and refuse a reviewer session even when the claimant data would otherwise match — the matching-claimant bypass is closed.F2 — resolved, plus a genuine pre-existing defect on the same path
exact_next_actionis nowauthor_lock_contract.post_compensation_action(post_state, ...), wherepost_statecomes fromassess_post_compensation_statefed by three direct observations taken after the rollback:os.path.exists(lock_res),os.path.isdir(target_worktree), and_branch_exists(root, target_branch). Observation is authoritative over the journal'srolled_backlist.implementation_allowedremainsfalse, and the payload additionally carriescompensating_recoveryandpost_compensation_state.The dead-rollback claim is true and pre-existing.
git show 82d71b77:issue_lock_store.pyhas norelease_session_lock, and the base caller wraps it inexcept Exception:— so every compensating rollback since #850 removed the branch and worktree and left the lock behind, raisingAttributeErrorinto a swallow. That is exactly the uninspectable state #953 exists to remove, and finding it while fixing F2 is the right call rather than scope creep.The new
release_session_lockproves ownership rather than asserting it: exactowner_sessionmatch, issue-number match, repository-keyed exact path whenremote/org/repoare known, scan fallback otherwise, andFileNotFoundError/RuntimeErroron zero or multiple matches so an ambiguous directory is never guessed at. Phase journals and session pointers are excluded by shape (lock_generation,branch_name,worktree_pathall required). The flock sidecar is deliberately left alone. The caller recordslock_release_failed:...instead of swallowing.Sabotage probe P3. I restored
exact_next_actiontorecommended_action(contract)— the pre-remediation expression. FiveAc7PostCompensationGuidancecases failed, and the assertion output quoted the exact unexecutable text review 632 flagged.Sabotage probe P4. I made
release_session_locka silent no-op. Seven cases failed, includingtest_the_compensation_lock_release_actually_removes_the_lock,test_release_refuses_a_lock_owned_by_a_different_session,test_retry_after_complete_compensation_leaves_exactly_one_lock, andtest_no_recommendation_names_an_artifact_the_rollback_deleted.Both reverted; worktree clean.
Cleanup-state coverage confirmed by running it:
completedirects to a bootstrap retry and the suite then executes that retry and asserts exactly one canonical lock and one branch survive; surviving-lock and surviving-branch-and-worktree each get a target-specific action; a rollback that never completed is a distinct third state that recommends read-only inspection first; a failed rollback step is stated rather than presented as intentional; unrelated locks are byte-identical afterwards.F3 — resolved
SOURCE_BOOTSTRAP_LOCK_RECOVERYis deleted and replaced by a comment stating why recovery reusesSOURCE_LOCK_ISSUE.DeadProvenanceConstantRemovedasserts the attribute is absent and that the sanctioned source set still excludes the recovery tool name.F4 — resolved
_NativeToolBasedrivesmcp_server.gitea_recover_incomplete_bootstrap_lockandmcp_server.gitea_inspect_issue_lock_contract— the registered functions — against a realgit initrepository carrying committed work, a real durable lock file, and config-backed profiles._observe_recovery_worktreeis now called from the tool atgitea_mcp_server.py:5188. The suite grew 61 → 92 cases. That the four sabotage probes above produced failures is itself the proof that these cases reach the call sites rather than reconstructing them.Contract-wide reconciliation
NativeEndToEndBootstrapToCreatePrruns the whole cycle against a real origin withgitea_lock_issuepatched to raiseAssertionErrorif anything reaches for it: realbootstrap_author_issue_worktree→ registered inspection tool (canonical,heartbeatable,create_pr_eligible) →heartbeat_session_lock→ real commit and push producing genuine divergence → second heartbeat →verify_lock_for_mutation→ the unchangedassess_lock_file_for_create_pr. It also asserts the merge base is unmoved, so nothing was rewound to satisfy a gate.lease_idquestion, answered explicitly. On a canonical lock the key is omitted, not null:build_canonical_issue_lockwriteslease_idonly when one was allocated. Downstream ownership fences onwork_lease.task_session_id, which is never null on a canonical lock, and the end-to-end sequence above completes with no workflow lease at all. So AC6 is satisfied and no downstream behaviour requires a lease bootstrap does not create. When a caller does supplylease_id,_verify_assignment_and_lease_idsstill validates it against the control plane for existence, active status, expiry, owning session, and bound issue.#447 guard intact.
git diff 82d71b77..b4c9f55 -- issue_lock_provenance.pyis empty, andSANCTIONED_LOCK_SOURCESholds the same five members at both revisions. Recovery mints provenance server-side throughbuild_sanctioned_lock_provenance; there is no parameter through which a caller can supply provenance or an authorization flag.Race and foreign-owner safety. The recovery write goes through
bind_session_lock(..., expected_generation=assessment["expected_generation"], recovery_sanctioned=True), so a moved generation fails closed. Ownership requires username and profile to match server-resolved values; a healthy foreign lock refuseshealthy_foreign_lockand a stale foreign lock refusesforeign_claimant; an already-canonical lock is left strictly alone so a live owner's heartbeat token is never invalidated.Non-blocking findings
N1 —
post_compensation_actionfall-through misdescribes two survival combinations.author_lock_contract.py, finalreturnofpost_compensation_action. The branch chain handles(lock, worktree, branch)= TTT, FTT, and T?F-via-not worktree_present, then falls through to prose that asserts "removed the lock and worktree; branchXsurvives". I probed the function directly across the matrix: for(T,T,F)and(F,T,F)that sentence is false on every clause it makes. I could not construct a reachable path to either state throughrun_compensating_recovery— it deletes the branch only after removing the worktree, andgit branch -Drefuses a branch checked out in a surviving worktree, while_branch_existsadditionally fails closed toTrue— so I am not treating this as blocking. Butpost_compensation_actionis a public pure function whose stated contract is that every branch names only artifacts the classification says exist, and that contract does not hold for those two inputs. Suggested: make the final branch an explicitnot lock_present and not worktree_present and branch_presentcondition with a distinct fallback for anything unclassified, and add subtests over the full eight-cell matrix.N2 — stale docstring inside the fixing commit.
assess_post_compensation_statesays "run_compensating_recoveryswallows a failed lock release and appends nothing", which the same commit made untrue by appendinglock_release_failed:.... The reasoning it supports (observation over journal) is still right; only the stated justification is out of date.N3 — PR description overstates one line. "Both
gitea_lock_issueand bootstrap now build through it" reads as though both callbuild_canonical_issue_lock. In the code, bootstrap and recovery call the record builder;gitea_lock_issueshares the contract through_author_issue_work_lease→build_author_issue_work_lease. The lease shape is genuinely shared and the tested behaviour is what the PR claims; the prose is stronger than the wiring.N4 — follow-up worth filing, correctly out of scope here. Four of the six pre-existing
_namespace_mutation_blockcall sites —commit_files,publish_unpublished_branch,bootstrap_author_issue_worktree,recover_dirty_orphaned_issue_worktree— have noTASK_REQUIRED_ROLEentry, so their namespace walls short-circuit to allowed today. I confirmed this against this head. The author disclosed it rather than silently widening the table, which is the right boundary for #953; it deserves its own issue.Independent test evidence
All commands run from the fresh reviewer worktree at
b4c9f55; the base pair from a separate detached worktree at82d71b77.venv/bin/python -m pytest tests/test_issue_953_bootstrap_lock_contract.py -q-qpytest -qfull suite at headb4c9f55pytest -qfull suite at base82d71b77Every author-reported count reproduced exactly.
Zero regressions, proven two ways.
FAILEDlines: 28 each side,diffempty.Elines each side. After normalizing only worktree root, temp paths, SHAs, timestamps, numeric ids, and the per-run randomtest-commit-payloads-<suffix>fixture names,diffis empty. The sole pre-normalization difference was that random suffix.test_documented_inventory_equals_registered_tools, checked specifically. Run alone at both revisions:Identical. Neither new tool appears in the head drift, so the two added tools did not enlarge or alter this pre-existing failure —
docs/mcp-tool-inventory.mdlists both.Worktree verified clean via
git status --porcelain --untracked-files=allafter every probe and every run, still atb4c9f55.Preserved state
feat/issue-949-native-fleet-inventoryis at92615f474bf6652d4e9ea59af7fd0dba03b56544onprgsand inbranches/issue-949-fleet-inventory; its lock, worktree, and uncreated PR were not touched. Issues #950–#952, PR #906's lease, the author's #953 worktree, the #953 baseline worktree, andbranches/review-pr954-cdf0daewere not modified. No merge, cleanup, recovery, restart, reconnect, synchronization, drain, or reconciliation was performed.Canonical PR State
STATE: approved
WHO_IS_NEXT: merger
NEXT_ACTION: Merge PR #954 into master at head
b4c9f55890through the sanctioned merger workflowNEXT_PROMPT:
WHAT_HAPPENED: Independent re-review of PR #954 at head
b4c9f55890after the author's remediation of review 632. Verified all three remediation commits against their stated scope, confirmed the namespace mutation wall fires at the real tool boundary for reviewer, merger, controller, and reconciler sessions, confirmed the AC7 refusal derives its next action from directly observed post-compensation state, confirmed the previously nonexistent release_session_lock is implemented with exact-owner matching, and confirmed both new registered tool functions now have executing coverage. Four sabotage probes proved the new cases are load-bearing, all reverted with the worktree verified clean. Ran the focused suite, 17 adjacent suites, and a full-suite pair at head and base, comparing failing test IDs and normalized failure reasons rather than counts.WHY: Both review 632 blockers are completely resolved and the remediation exceeded the prescribed fix where the prescription was wrong: registering the task in role_session_router.TASK_REQUIRED_ROLE is what makes the namespace gate fire at all, and an author-exclusive role-kind wall is required because the task's permission is held by every configured role. The #447 provenance guard and its five sanctioned sources are unchanged, the end-to-end sequence completes without any gitea_lock_issue repair, recovery is generation-fenced and foreign-owner safe, and the full-suite pair shows identical failing test IDs and identical normalized failure reasons, so no pre-existing failure conceals a regression.
ISSUE: 953
RELATED_PRS: 954
HEAD_SHA:
b4c9f55890REVIEW_STATUS: approved
MERGE_READY: yes
BLOCKERS: none
VALIDATION: tests/test_issue_953_bootstrap_lock_contract.py 92 passed with 5 subtests, exit 0; 17 adjacent lock, bootstrap, lease, renewal, recovery, heartbeat, role, and capability suites 402 passed with 97 subtests, exit 0; full-suite pair from branches/ worktrees at head
b4c9f55890(28 failed, 5784 passed, 6 skipped, 1047 subtests, exit 1) and base82d71b7702(28 failed, 5692 passed, 6 skipped, 1042 subtests, exit 1) with identical sorted FAILED lines and identical normalized E-line failure reasons; test_documented_inventory_equals_registered_tools reason byte-identical at both revisions and naming neither new tool; four sabotage probes each produced the expected failures and were reverted; git diff base..head of issue_lock_provenance.py empty; merge base equals live master; review worktree clean via git status --porcelain --untracked-files=all.NATIVE_REVIEW_PROOF: Conducted entirely through the native gitea-reviewer MCP namespace on profile prgs-reviewer as sysadmin against Scaled-Tech-Consulting/Gitea-Tools on prgs. gitea_whoami confirmed identity sysadmin with role_kind reviewer, distinct from author jcwalker3. gitea_get_runtime_context and gitea_assess_master_parity reported in_parity true, live_stale false, restart_required false, mutation_safe true, with startup, daemon-start, local, current, and live-remote revisions all
82d71b7702. gitea_load_review_workflow recorded workflow hash 263d0a6cb8a6 with boundary clean. gitea_resolve_task_capability(review_pr) returned allowed_in_current_session true immediately before each mutation. Reviewer PR lease session 38475-bf76ed428e17 was acquired at candidate_headb4c9f55890through gitea_acquire_reviewer_pr_lease and heartbeated through gitea_heartbeat_reviewer_pr_lease. PR state, reviews, and comments were read through gitea_view_pr, gitea_get_pr_review_feedback, gitea_view_issue, gitea_list_issue_comments, and gitea_list_prs. This verdict was recorded through gitea_mark_final_review_decision and gitea_submit_pr_review. Local shell use was confined to repository inspection and test execution inside the reviewer worktree branches/review-pr954-b4c9f55 and the base worktree branches/review-pr954-base-82d71b77.LAST_UPDATED_BY: reviewer
Reviewed independently at
b4c9f558901699e29fac77b4f748565bcdb5f272. Not authored or merged by me.repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 38475-bf76ed428e17
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: released
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T07:02:47Z
expires_at: 2026-07-28T07:12:47Z
blocker: manual-release
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #954
issue: #953
reviewer_identity: sysadmin
profile: prgs-merger
session_id: 13484-bcb88d6a9b95
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr954-b4c9f55
phase: claimed
candidate_head:
b4c9f55890target_branch: master
target_branch_sha:
82d71b7702last_activity: 2026-07-28T07:19:58Z
expires_at: 2026-07-28T07:29:58Z
blocker: none
Stale #332 review-decision lock cleanup (#594)
Status: APPLIED
sysadminprgs-merger2026-07-28T07:21:15.333696+00:00approveon PR fix(author): unify the bootstrap and lock_issue issue-lock contract (Closes #953) (#954)closed(merged=True)aad5c8b42361d380a8eeb07b94b90815e594c2c52prgs-reviewerManual deletion of session-state files is not the workflow.
This path only clears a lock when the referenced PR is merged/closed.