Review 632 F2. The bootstrap AC7 read-back refusal calls
`run_compensating_recovery` and *then* reported
`author_lock_contract.recommended_action(contract)` — advice computed from the
malformed lock that provoked the rollback, not from the state the rollback
left. Compensation releases the lock, removes the worktree (always clean
there, no implementation bytes having been written), and deletes the branch,
so an author following that advice got `no_durable_lock` from
`gitea_recover_incomplete_bootstrap_lock` and, had the lock survived,
`worktree_invalid` instead; the `gitea_lock_issue` half of the same sentence
cannot bind a worktree that no longer exists. Two refusals in a row for a
state a plain bootstrap retry fixes — the unexecutable-guidance failure class
this issue exists to remove, reintroduced on the new fail-closed path.
Investigating that path surfaced why the "clean retry" state was in practice
unreachable: `run_compensating_recovery` has called
`issue_lock_store.release_session_lock` since #850, and that function has
never existed. The `AttributeError` landed in a bare `except Exception: pass`,
so every rollback removed the branch and worktree and silently left the lock
behind — precisely the uninspectable, unrecoverable state #953 is about
(`gitea_recover_incomplete_bootstrap_lock` refuses `worktree_invalid`,
`gitea_lock_issue` has no worktree to bind). Confirmed dead at the pinned base
`82d71b77`, not introduced by this branch.
`release_session_lock` is therefore implemented: it removes exactly one
durable lock whose recorded `owner_session` matches the caller's, keyed by
repository when known, refusing on zero or multiple matches so no caller can
delete a lock it does not own and an ambiguous directory is never guessed at.
Bootstrap phase journals and session pointers that share the directory are
excluded by shape. The flock sidecar is deliberately left alone. The caller no
longer swallows a release failure; it records `lock_release_failed:...`.
`assess_post_compensation_state` then classifies from directly observed
durable state — lock file, worktree directory, and branch ref — rather than
from the journal's `rolled_back` list, which records only what compensation
attempted. Three distinct states: `complete` (nothing remains),
`partial` (rollback ran, artifacts survive by design or because a step
errored), `failed` (rollback never completed, so nothing is proven removed).
`post_compensation_action` answers for exactly what survives:
complete -> re-run gitea_bootstrap_author_issue_worktree
lock + branch + worktree -> gitea_recover_incomplete_bootstrap_lock
branch + worktree, no lock -> gitea_lock_issue (still base-equivalent)
lock only, worktree gone -> gitea_inspect_issue_lock_contract
branch only -> gitea_inspect_issue_lock_contract, then retry
rollback did not complete -> gitea_inspect_issue_lock_contract
No branch names an artifact the classification says is gone, and a failed
rollback step is stated rather than presented as an intentional outcome. The
refusal payload carries `compensating_recovery` and `post_compensation_state`
alongside the derived `exact_next_action`, still with
`implementation_allowed: false`.
Also removes the dead `SOURCE_BOOTSTRAP_LOCK_RECOVERY` constant (review 632
F3), which had no readers and implied a second lock source; the deliberate
reuse of `SOURCE_LOCK_ISSUE` is now stated as a comment. The #447 guard and
`SANCTIONED_LOCK_SOURCES` remain untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
gitea_bootstrap_author_issue_worktree wrote a lock no downstream author
operation accepts, then directed the author straight to implementation. Once
the branch carried commits, heartbeat, re-lock, exact-owner renewal, and the
#447 create-PR guard all refused simultaneously and no sanctioned recovery
path remained eligible.
Each of those gates is individually correct. The defect was that two writers
disagreed about what a lock is.
- Add author_lock_contract as the single canonical definition: claimant,
work_lease, lock_provenance, generation, and an explicit expiration state.
Both gitea_lock_issue and bootstrap now build through it.
- Promote issue_lock_store.lock_claimant to the one shared claimant reader and
use it in the ownership check, so a claimant recorded at the lock top level
is read rather than refused. The values are still compared against
server-resolved identity and profile, so no legacy placement grants anything
the canonical placement would not.
- Represent missing expiration explicitly. An absent expires_at previously read
as "not yet expired", leaving a malformed lock permanently non-expiring and
permanently ineligible for #760 renewal.
- Bootstrap reads its lock back and verifies it structurally before reporting
success. A partial lock fails closed while the worktree is still
base-equivalent, names the missing fields, and never reports
implementation_allowed. Its exact_next_action now matches the state returned.
- Add gitea_recover_incomplete_bootstrap_lock for locks already written by the
old bootstrap, including those whose branches carry pushed commits. It never
moves, resets, or rewinds a branch, never requires base-equivalence, never
pushes or opens a PR, and touches only the target lock. It proves repository,
issue, claimant username and profile, branch, worktree, registration, and
head before writing, refuses healthy foreign-owned locks, and mints
provenance and authorization server-side.
- Add gitea_inspect_issue_lock_contract, a strictly read-only surface.
- Document the required ordering and the recovery path.
The #447 provenance guard is unchanged and the sanctioned source set was not
widened: bootstrap now satisfies the guard rather than the guard being relaxed
to admit bootstrap.
Tests: 61 new cases covering the canonical schema, immediate heartbeat,
renewal before and after commits, the create_pr guard, executable next actions,
partial and malformed and missing-expiration and expired and same-owner and
foreign-owner and legacy locks, recovery isolation, read-only inspection, and
the full bootstrap-implement-commit-push-create_pr regression against isolated
fixtures.
Full suite at head: 28 failed, 5753 passed, 6 skipped, 1042 subtests.
Full suite at base 82d71b77: 28 failed, 5692 passed, 6 skipped, 1042 subtests.
Failing test-ID sets are identical, so there are zero regressions; the +61
passes are this issue's new suite.
Issue #949 was preserved and not recovered: its branch remains at
92615f474b and its worktree, lock, and PR state
were not touched. The #949-shaped regression uses isolated fixtures only.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Align assess_author_issue_bootstrap with bootstrap_permits_control_checkout
so gitea_bootstrap_author_issue_worktree can create the first branches/
worktree without the lock↔worktree deadlock.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- Remove /branches/ string-split fallback in resolve_canonical_repo_root;
recover roots via commonpath ancestry only (review #531 F2).
- Refuse existing branches that do not contain live master; no weak
merge-base acceptance (F3).
- Verify caller-supplied assignment_id/lease_id against the control plane
or fail closed (F4).
- Compensating recovery releases bound workflow leases via lease_lifecycle (F5).
- Regression tests for each finding.