Unify task leases with sliding heartbeats and safe terminal dead-owner recovery #790

Closed
opened 2026-07-21 22:33:14 -05:00 by jcwalker3 · 5 comments
Owner

Problem

Gitea-Tools currently uses inconsistent ownership and lease lifecycles across task types.

Reviewer and merger PR leases use short-lived ownership with activity-based renewal. Author issue-work leases can receive a fixed four-hour lease that is not shortened by heartbeat loss, process death, issue closure, or completed PR integration.

This can unnecessarily block post-PR reconciliation for hours even when the protected artifacts are independently proven safe.

Observed incident

Issue #787 and PR #789 exposed the defect:

  • The author work lease was created at 2026-07-22T01:37:57Z.
  • It expires at 2026-07-22T05:37:57Z.
  • Effective TTL: four hours.
  • The current and previous owner PIDs were dead.
  • Issue #787 was closed.
  • PR #789 had completed integration into master.
  • The worktree was clean.
  • The relevant trees were identical.
  • The PR head was contained in master.
  • The branch-cleanup assessment returned safe_to_delete: true.
  • The reconciler still classified the worktree as active_issue_work with preserve: true.
  • Cleanup was blocked solely by the unexpired wall-clock lease.
  • Dead-session recovery appears capable of minting a new full-duration lease, restarting the wait.

This is safe fail-closed behavior, but it creates avoidable multi-hour stalls for terminal work.

Desired design

Use one shared lease lifecycle for every actively owned task:

  1. Acquire a short initial lease.
  2. Record a heartbeat while the owning session is active.
  3. Extend expiry only when a valid owner heartbeat is received.
  4. Stop renewal when the owner exits or loses ownership.
  5. Mark ownership reclaimable after a configurable number of missed heartbeats.
  6. Support a reconciler-only terminal reclaim path when terminality and artifact safety are proven.
  7. Maintain complete ownership and cleanup audit records.

Task classes may have different configurable TTL and heartbeat intervals, but they must share the same acquire, renew, expire, reclaim, release, and audit semantics.

Required lease record

The shared record should include at least:

  • repository
  • task type and task identifier
  • role and profile
  • identity
  • session ID
  • process ID
  • branch
  • worktree
  • acquired timestamp
  • last heartbeat timestamp
  • expiry timestamp
  • lease generation or fencing token
  • lifecycle state
  • terminal/reclaim reason
  • audit provenance

Protection rule

Active work remains protected only while all applicable conditions hold:

  • the task is nonterminal;
  • the owner is valid;
  • the heartbeat is fresh;
  • the lease generation matches;
  • ownership has not been canonically released or superseded.

A stale heartbeat alone must not permit unsafe deletion. Reclaim must still use the existing cleanliness, ancestry, publication, worktree-binding, and lifecycle gates.

Terminal recovery

Add a sanctioned reconciler-only operation for terminal work.

It may reclaim an author issue-work lease before its original maximum expiry only when all required proofs succeed:

  • issue is closed or otherwise canonically terminal;
  • associated PR completed integration into the target branch;
  • expected PR head is contained in the current target branch;
  • owner process is dead or its heartbeat exceeded the allowed grace period;
  • no newer lease generation or active successor session exists;
  • worktree is clean;
  • no unpublished commits or changes exist;
  • branch contents are represented in the target branch;
  • no conflicting worktree or task ownership exists;
  • reconciliation capability and identity are valid.

The operation must fail closed when any proof is missing or ambiguous.

Dead-session recovery

Dead-session recovery must not silently restart a complete long-duration author lease.

It should either:

  • preserve the original expiry;
  • issue only a short recovery grace lease; or
  • create a new lease generation only after an explicit sanctioned ownership adoption.

The selected behavior must be documented and covered by tests.

Configuration

Centralize lease policy instead of duplicating hardcoded TTL values.

Configuration should define, per task class:

  • initial TTL
  • heartbeat interval
  • missed-heartbeat grace period
  • maximum continuous lease duration, if applicable
  • recovery grace duration
  • terminal reclaim eligibility

Existing active leases must have explicit compatibility behavior when configuration changes.

Concurrency safety

Use lease generations or fencing tokens so a delayed heartbeat from an older session cannot revive or overwrite ownership after another session has reclaimed the task.

Heartbeat, adoption, release, and reclaim operations must be atomic and idempotent.

Acceptance criteria

  • Author, reviewer, merger, and reconciler ownership use the shared heartbeat lifecycle.
  • Active author work remains protected during long-running sessions through heartbeat renewal.
  • A dead owner stops extending the lease.
  • Missing heartbeats make ownership reclaimable after the configured grace period.
  • Terminal author work can be reclaimed early only through the sanctioned reconciler path and only after all safety gates pass.
  • Dead-session recovery does not restart a full multi-hour wait without explicit adoption.
  • Late heartbeats from superseded sessions cannot restore ownership.
  • TTLs and heartbeat intervals have one documented configuration source.
  • Existing lease compatibility and migration behavior are documented.
  • Every acquire, heartbeat, renewal, adoption, expiration, reclaim, and release has auditable provenance.
  • Issue #787/PR #789 is represented as a regression test.
  • Existing author, reviewer, merger, reconciliation, anti-stomp, and branch-safety tests remain passing.

Required tests

Include tests for:

  1. Active author session renews a short lease.
  2. Long-running author work remains protected while heartbeats are fresh.
  3. Dead owner stops renewal.
  4. Missed-heartbeat grace expires and permits sanctioned reclamation.
  5. Closed issue plus completed PR integration plus dead owner plus clean/contained artifacts permits terminal reclamation.
  6. Dirty worktree blocks reclamation.
  7. Unpublished commits block reclamation.
  8. Nonterminal issue blocks terminal reclamation.
  9. Live successor session blocks reclamation.
  10. Stale generation heartbeat is rejected.
  11. Duplicate heartbeat is idempotent.
  12. Recovery does not mint an unintended four-hour hold.
  13. Runtime/master parity failure blocks mutation.
  14. Read-after-write verifies the resulting lease and audit state.
  15. Existing lease records receive deterministic compatibility treatment.

Non-goals

  • Do not weaken worktree cleanliness or ancestry checks.
  • Do not permit arbitrary lock deletion.
  • Do not make PID liveness the sole ownership authority.
  • Do not allow manual lock-file edits as recovery.
  • Do not use one identical TTL for every task merely because the lifecycle is shared.
  • Do not delete Issue #787 artifacts as part of implementing this issue.

Relationship

Discovered during post-PR reconciliation for Issue #787 / PR #789.

## Problem Gitea-Tools currently uses inconsistent ownership and lease lifecycles across task types. Reviewer and merger PR leases use short-lived ownership with activity-based renewal. Author issue-work leases can receive a fixed four-hour lease that is not shortened by heartbeat loss, process death, issue closure, or completed PR integration. This can unnecessarily block post-PR reconciliation for hours even when the protected artifacts are independently proven safe. ## Observed incident Issue #787 and PR #789 exposed the defect: - The author work lease was created at `2026-07-22T01:37:57Z`. - It expires at `2026-07-22T05:37:57Z`. - Effective TTL: four hours. - The current and previous owner PIDs were dead. - Issue #787 was closed. - PR #789 had completed integration into master. - The worktree was clean. - The relevant trees were identical. - The PR head was contained in master. - The branch-cleanup assessment returned `safe_to_delete: true`. - The reconciler still classified the worktree as `active_issue_work` with `preserve: true`. - Cleanup was blocked solely by the unexpired wall-clock lease. - Dead-session recovery appears capable of minting a new full-duration lease, restarting the wait. This is safe fail-closed behavior, but it creates avoidable multi-hour stalls for terminal work. ## Desired design Use one shared lease lifecycle for every actively owned task: 1. Acquire a short initial lease. 2. Record a heartbeat while the owning session is active. 3. Extend expiry only when a valid owner heartbeat is received. 4. Stop renewal when the owner exits or loses ownership. 5. Mark ownership reclaimable after a configurable number of missed heartbeats. 6. Support a reconciler-only terminal reclaim path when terminality and artifact safety are proven. 7. Maintain complete ownership and cleanup audit records. Task classes may have different configurable TTL and heartbeat intervals, but they must share the same acquire, renew, expire, reclaim, release, and audit semantics. ## Required lease record The shared record should include at least: - repository - task type and task identifier - role and profile - identity - session ID - process ID - branch - worktree - acquired timestamp - last heartbeat timestamp - expiry timestamp - lease generation or fencing token - lifecycle state - terminal/reclaim reason - audit provenance ## Protection rule Active work remains protected only while all applicable conditions hold: - the task is nonterminal; - the owner is valid; - the heartbeat is fresh; - the lease generation matches; - ownership has not been canonically released or superseded. A stale heartbeat alone must not permit unsafe deletion. Reclaim must still use the existing cleanliness, ancestry, publication, worktree-binding, and lifecycle gates. ## Terminal recovery Add a sanctioned reconciler-only operation for terminal work. It may reclaim an author issue-work lease before its original maximum expiry only when all required proofs succeed: - issue is closed or otherwise canonically terminal; - associated PR completed integration into the target branch; - expected PR head is contained in the current target branch; - owner process is dead or its heartbeat exceeded the allowed grace period; - no newer lease generation or active successor session exists; - worktree is clean; - no unpublished commits or changes exist; - branch contents are represented in the target branch; - no conflicting worktree or task ownership exists; - reconciliation capability and identity are valid. The operation must fail closed when any proof is missing or ambiguous. ## Dead-session recovery Dead-session recovery must not silently restart a complete long-duration author lease. It should either: - preserve the original expiry; - issue only a short recovery grace lease; or - create a new lease generation only after an explicit sanctioned ownership adoption. The selected behavior must be documented and covered by tests. ## Configuration Centralize lease policy instead of duplicating hardcoded TTL values. Configuration should define, per task class: - initial TTL - heartbeat interval - missed-heartbeat grace period - maximum continuous lease duration, if applicable - recovery grace duration - terminal reclaim eligibility Existing active leases must have explicit compatibility behavior when configuration changes. ## Concurrency safety Use lease generations or fencing tokens so a delayed heartbeat from an older session cannot revive or overwrite ownership after another session has reclaimed the task. Heartbeat, adoption, release, and reclaim operations must be atomic and idempotent. ## Acceptance criteria - Author, reviewer, merger, and reconciler ownership use the shared heartbeat lifecycle. - Active author work remains protected during long-running sessions through heartbeat renewal. - A dead owner stops extending the lease. - Missing heartbeats make ownership reclaimable after the configured grace period. - Terminal author work can be reclaimed early only through the sanctioned reconciler path and only after all safety gates pass. - Dead-session recovery does not restart a full multi-hour wait without explicit adoption. - Late heartbeats from superseded sessions cannot restore ownership. - TTLs and heartbeat intervals have one documented configuration source. - Existing lease compatibility and migration behavior are documented. - Every acquire, heartbeat, renewal, adoption, expiration, reclaim, and release has auditable provenance. - Issue #787/PR #789 is represented as a regression test. - Existing author, reviewer, merger, reconciliation, anti-stomp, and branch-safety tests remain passing. ## Required tests Include tests for: 1. Active author session renews a short lease. 2. Long-running author work remains protected while heartbeats are fresh. 3. Dead owner stops renewal. 4. Missed-heartbeat grace expires and permits sanctioned reclamation. 5. Closed issue plus completed PR integration plus dead owner plus clean/contained artifacts permits terminal reclamation. 6. Dirty worktree blocks reclamation. 7. Unpublished commits block reclamation. 8. Nonterminal issue blocks terminal reclamation. 9. Live successor session blocks reclamation. 10. Stale generation heartbeat is rejected. 11. Duplicate heartbeat is idempotent. 12. Recovery does not mint an unintended four-hour hold. 13. Runtime/master parity failure blocks mutation. 14. Read-after-write verifies the resulting lease and audit state. 15. Existing lease records receive deterministic compatibility treatment. ## Non-goals - Do not weaken worktree cleanliness or ancestry checks. - Do not permit arbitrary lock deletion. - Do not make PID liveness the sole ownership authority. - Do not allow manual lock-file edits as recovery. - Do not use one identical TTL for every task merely because the lifecycle is shared. - Do not delete Issue #787 artifacts as part of implementing this issue. ## Relationship Discovered during post-PR reconciliation for Issue #787 / PR #789.
jcwalker3 added the bugtype:guardrailworkflow-hardeningmcp-health labels 2026-07-21 22:33:15 -05:00
jcwalker3 added the status:ready label 2026-07-21 22:37:12 -05:00
Author
Owner

Implementation sequencing with Issue #760

Issue #790 must not be implemented concurrently with Issue #760 without an explicit integration plan. Both may modify the same ownership and lease surfaces:

  • issue_lock_store.assess_same_issue_lease_conflict

    • #760 changes gate ordering so an expired lease can be renewed by the exact existing owner.
    • #790 broadens reclaim decisions from is_lease_expired(...) toward shared liveness and heartbeat semantics.
    • Independent implementations could conflict both textually and semantically.
  • Issue-lock expiry and freshness behavior

    • #760 retains absolute wall-clock expiry and adds exact-owner renewal.
    • #790 introduces sliding heartbeat renewal, lease generations, fencing tokens, and unified cross-role lifecycle behavior.
    • #790 may subsume or replace portions of the renewal path introduced by #760.

Before allocating implementation, choose one sequence:

  1. Land #760 first, then rebase and design #790 against its renewal behavior; or
  2. Incorporate #760’s acceptance criteria into #790 and close #760 as superseded after confirming complete coverage.

Do not allocate #760 and #790 concurrently to separate authors unless their code boundaries and final integration order are explicitly coordinated.

Canonical Issue State

STATE: awaiting-sequencing-decision
WHO_IS_NEXT: controller
NEXT_ACTION: Choose and record the #760/#790 implementation order before allocating either issue.
NEXT_PROMPT:

Decide whether to land #760 first and rebase #790 onto it, or incorporate #760's acceptance criteria into #790 and close #760 as superseded after confirming complete coverage. Record the decision on both issues before allocation.

WHY: Both issues modify overlapping issue-lock conflict assessment and expiry/freshness behavior. Concurrent implementation without an integration order risks textual and semantic conflicts.
BLOCKERS: Allocation is blocked only until the controller records the sequencing decision.
VALIDATION: Issues #760 and #790 are open; #790 has status:ready; overlapping implementation surfaces were verified; no implementation has been allocated.

## Implementation sequencing with Issue #760 Issue #790 must not be implemented concurrently with Issue #760 without an explicit integration plan. Both may modify the same ownership and lease surfaces: - `issue_lock_store.assess_same_issue_lease_conflict` - #760 changes gate ordering so an expired lease can be renewed by the exact existing owner. - #790 broadens reclaim decisions from `is_lease_expired(...)` toward shared liveness and heartbeat semantics. - Independent implementations could conflict both textually and semantically. - Issue-lock expiry and freshness behavior - #760 retains absolute wall-clock expiry and adds exact-owner renewal. - #790 introduces sliding heartbeat renewal, lease generations, fencing tokens, and unified cross-role lifecycle behavior. - #790 may subsume or replace portions of the renewal path introduced by #760. Before allocating implementation, choose one sequence: 1. Land #760 first, then rebase and design #790 against its renewal behavior; or 2. Incorporate #760’s acceptance criteria into #790 and close #760 as superseded after confirming complete coverage. Do not allocate #760 and #790 concurrently to separate authors unless their code boundaries and final integration order are explicitly coordinated. ## Canonical Issue State STATE: awaiting-sequencing-decision WHO_IS_NEXT: controller NEXT_ACTION: Choose and record the #760/#790 implementation order before allocating either issue. NEXT_PROMPT: ```text Decide whether to land #760 first and rebase #790 onto it, or incorporate #760's acceptance criteria into #790 and close #760 as superseded after confirming complete coverage. Record the decision on both issues before allocation. ``` WHY: Both issues modify overlapping issue-lock conflict assessment and expiry/freshness behavior. Concurrent implementation without an integration order risks textual and semantic conflicts. BLOCKERS: Allocation is blocked only until the controller records the sequencing decision. VALIDATION: Issues #760 and #790 are open; #790 has status:ready; overlapping implementation surfaces were verified; no implementation has been allocated.
Author
Owner

Ledger correction — supersedes comment 13869

Corrects the VALIDATION field only. The sequencing analysis and every other field in comment 13869 stand unchanged; this comment is the authoritative ledger for Issue #790.

Prior wording asserted status:ready for #790 alone. Both issues carry it, verified live at the time of this comment.

Canonical Issue State

STATE: awaiting-sequencing-decision
WHO_IS_NEXT: controller
NEXT_ACTION: Choose and record the #760/#790 implementation order before allocating either issue.
NEXT_PROMPT:

Decide whether to land #760 first and rebase #790 onto it, or incorporate #760's acceptance criteria into #790 and close #760 as superseded after confirming complete coverage. Record the decision on both issues before allocation.

WHY: Both issues modify overlapping issue-lock conflict assessment and expiry/freshness behavior. Concurrent implementation without an integration order risks textual and semantic conflicts.
BLOCKERS: Allocation is blocked only until the controller records the sequencing decision.
VALIDATION: Issues #760 and #790 are open; both #760 and #790 have status:ready; overlapping implementation surfaces were verified; no implementation has been allocated.

## Ledger correction — supersedes comment 13869 Corrects the `VALIDATION` field only. The sequencing analysis and every other field in comment 13869 stand unchanged; this comment is the authoritative ledger for Issue #790. Prior wording asserted `status:ready` for #790 alone. Both issues carry it, verified live at the time of this comment. ## Canonical Issue State STATE: awaiting-sequencing-decision WHO_IS_NEXT: controller NEXT_ACTION: Choose and record the #760/#790 implementation order before allocating either issue. NEXT_PROMPT: ```text Decide whether to land #760 first and rebase #790 onto it, or incorporate #760's acceptance criteria into #790 and close #760 as superseded after confirming complete coverage. Record the decision on both issues before allocation. ``` WHY: Both issues modify overlapping issue-lock conflict assessment and expiry/freshness behavior. Concurrent implementation without an integration order risks textual and semantic conflicts. BLOCKERS: Allocation is blocked only until the controller records the sequencing decision. VALIDATION: Issues #760 and #790 are open; both #760 and #790 have status:ready; overlapping implementation surfaces were verified; no implementation has been allocated.
Author
Owner

Sequencing decision: Issue #760 lands first

The controller has selected the implementation order:

  1. Implement, review, and land Issue #760 first.
  2. Do not implement Issue #790 concurrently.
  3. After #760 lands, reassess #790 against the delivered renewal behavior.
  4. Rebase or redesign #790 as necessary so its shared heartbeat lifecycle incorporates rather than duplicates or conflicts with #760.

Issue #790 remains open and status:ready, but its implementation is intentionally sequenced behind #760. This resolves the sequencing decision previously recorded in comments 13869 and 13873.

Canonical Issue State

STATE: waiting-on-issue-760
WHO_IS_NEXT: controller
NEXT_ACTION: Hold Issue #790 until Issue #760 lands, then reassess its design and allocation readiness.
NEXT_PROMPT:

After Issue #760 lands, re-read its implementation, tests, and final merged head. Reassess Issue #790 against that delivered behavior, identify which #790 requirements remain, update its implementation plan to avoid duplication or conflict, and only then allocate #790.

WHY: Issue #790 changes the same conflict-assessment and expiry/freshness surfaces as #760 and must build on the behavior that #760 lands first.
BLOCKERS: Issue #790 implementation is blocked until Issue #760 lands and the controller completes the post-merge reassessment.
VALIDATION: Issues #760 and #790 are open and both have status:ready; the controller selected #760 to land first; #790 is sequenced behind #760; no implementation was allocated by this comment.

## Sequencing decision: Issue #760 lands first The controller has selected the implementation order: 1. Implement, review, and land Issue #760 first. 2. Do not implement Issue #790 concurrently. 3. After #760 lands, reassess #790 against the delivered renewal behavior. 4. Rebase or redesign #790 as necessary so its shared heartbeat lifecycle incorporates rather than duplicates or conflicts with #760. Issue #790 remains open and status:ready, but its implementation is intentionally sequenced behind #760. This resolves the sequencing decision previously recorded in comments 13869 and 13873. ## Canonical Issue State STATE: waiting-on-issue-760 WHO_IS_NEXT: controller NEXT_ACTION: Hold Issue #790 until Issue #760 lands, then reassess its design and allocation readiness. NEXT_PROMPT: ```text After Issue #760 lands, re-read its implementation, tests, and final merged head. Reassess Issue #790 against that delivered behavior, identify which #790 requirements remain, update its implementation plan to avoid duplication or conflict, and only then allocate #790. ``` WHY: Issue #790 changes the same conflict-assessment and expiry/freshness surfaces as #760 and must build on the behavior that #760 lands first. BLOCKERS: Issue #790 implementation is blocked until Issue #760 lands and the controller completes the post-merge reassessment. VALIDATION: Issues #760 and #790 are open and both have status:ready; the controller selected #760 to land first; #790 is sequenced behind #760; no implementation was allocated by this comment.
Owner

[THREAD STATE LEDGER] Issue #790 — controller reassessment against the landed Issue #760 behavior; acceptance criteria amended

Scope of this comment: issue-scope amendment only. No allocation, no branch, no worktree, no lock acquisition, no implementation, no label change.

What is true now

  • Server-side decision state: Issue #790 state is open, labels unchanged. Issue #760 state is closed; PR #791 reached its closed post-merge state at 2026-07-22T01:02:40-05:00 with merge commit 620ed6e9a9550b8da2ceb82d9ab8744e8920490f on prgs/master, from reviewed source head a30a3ce4c37b2dde725301bef8b9ef8e04160088.
  • Local verdict/state: this controller action was read-only against the working tree. No code edit, no commit, no push, no branch or worktree creation, no lock acquisition, and no Gitea-side mutation other than this comment.
  • Allocation state: Issue #790 verified unallocated. No durable lock file for issue 790 under ~/.cache/gitea-tools/issue-locks/, no local branch matching *790*, git ls-remote prgs 'refs/heads/*790*' returns nothing, and no registered worktree matching 790.

Second incident: Issue #760 / PR #791

Issue #790 was filed from the Issue #787 / PR #789 incident. A second, independent instance occurred during the PR #791 post-merge reconciliation and is recorded here as further evidence.

  • Author work lease created 2026-07-22T04:19:08Z, expires_at 2026-07-22T08:19:08Z; effective TTL four hours.
  • last_heartbeat_at equal to created_at, never advanced across the entire authoring, review-remediation, and merge cycle.
  • Recorded pid 39849 alive throughout — a long-lived MCP daemon started 2026-07-21 23:22:08 local, not the authoring task.
  • PR #791 integration into master completed at 01:02:40-05:00; Issue #760 closed by the closing reference.
  • Worktree branches/fix-issue-760-exact-owner-renewal clean, on the locked branch at a30a3ce4, with git merge-base --is-ancestor a30a3ce4 620ed6e9 true and local head equal to remote head.
  • Reconciler assessment at 2026-07-22T06:39:43Z — over five hours after integration completed — still returned blocker_kind: active_branch_ownership with blocking categories author_lease, author_session, worktree_binding, and gitea_audit_worktree_cleanup classified the worktree active_issue_work with removable: false and preserve: true.
  • No control-plane lease row exists for issue 760 or PR 791 in either direction, and no live PR #791 lease remains. The hold was entirely the file-store absolute expiry.
  • Reconciliation ledger for that pass: Issue #760 comment 13955.

Root cause traced on master 620ed6e9

  1. issue_lock_store.assess_lock_freshness returns status: "live" because expires_at had not passed and the recorded pid is alive.
  2. gitea_mcp_server._collect_branch_ownership_records (lines 10618-10641) maps live to status="active" for the author_lease record and additionally emits a second author_session record with status="active" and reclaim_allowed=False.
  3. branch_cleanup_guard.assess_ownership_record_activity (line 488) treats "active" as blocking for both records.
  4. The bound worktree contributes the third blocking category.
  5. worktree_cleanup_audit._classify (line 605) independently returns active_issue_work because the branch appears in active_lock_branches.

Decisive finding: the heartbeat field is inert. assess_lock_freshness parses last_heartbeat_at at issue_lock_store.py:376-378 and never branches on it; the value is only echoed into the live payload. Across the whole tree the field is written in exactly one place, gitea_mcp_server.py:2561, set to created at mint time. No writer advances it. Liveness is therefore expires_at (issue_lock_store.py:24, WORK_LEASE_TTL_HOURS = 4) OR daemon pid liveness, and nothing else. Adding heartbeat semantics is a behavior change to one function, not a schema migration — the field is present in every durable lock on disk.

Landed components to reuse rather than rebuild

  • reviewer_pr_lease (#747) is the working sliding-TTL reference: LEASE_TTL_MINUTES = 10, STALE_WARNING_MINUTES = 5, freshness bands active / stale_warning / expired, expiry re-derived from every write.
  • issue_lock_store.lock_generation plus the expected_generation compare-and-swap inside the flock critical section (bind_session_lock, lines 228-241, #772). Write-path fencing exists.
  • branch_cleanup_guard._TERMINAL_OWNERSHIP_STATUSES (line 162) contains released, terminal, and closed, and assess_ownership_record_activity (line 481) returns blocks=False for them. Terminal retirement needs a status producer, not a new guard.
  • issue_lock_renewal.assess_exact_owner_lease_renewal (#760) implements the exact-owner, clean-worktree, agreeing-heads, no-competing-claim evidence set with fail-closed refusals naming each absent element.
  • reviewer_pr_lease.new_session_id() mints a per-task session identifier.

Gaps in this issue as originally written

  • G1 — no requirement for a task/session identity distinct from the daemon pid. The record lists both session ID and process ID, but never states that pid lifetime is not task lifetime. bind_session_lock lines 200-201 write os.getpid() into both pid and session_pid.
  • G2 — no concrete durations; every value deferred to "configurable per task class".
  • G3 — pid-as-corroborating-only appears only as a non-goal, "not the sole authority", which is weaker than "never authorization". assess_lock_freshness line 394 still uses pid as an independent staleness trigger.
  • G4 — the terminal-recovery proof list conjoins "owner process is dead or its heartbeat exceeded the allowed grace period" with the terminality and artifact proofs. Terminal work is terminal regardless of whether some session is still heartbeating on it; a session heartbeating on integrated work is a leaked session, not protected work. As originally written, a faithful implementation would still have stalled the PR #791 cleanup.
  • G5 — this issue predates the PR #791 landing and states that #790 "may subsume or replace portions of the renewal path introduced by #760". That relationship is now inverted: the #760 renewal path landed and requires re-scoping, because it grants renewal to the exact owner of an expired lease with no heartbeat requirement at all — deliberately, since heartbeats did not exist.
  • G6 — the fifteen required tests are assessor-level. Review #499 finding F2 proved on this exact code that assessor-level coverage misses discard points: the #760 renewal waiver was computed and then discarded at two later gates, issue_lock_worktree.assess_issue_lock_worktree base-equivalence and the duplicate-work gate's linked-open-PR blocker.
  • G7issue_lock_store.verify_lock_for_mutation (line 630) checks freshness, issue number, branch, and worktree, but not lock_generation. Fencing is write-side only; a session holding a stale in-memory lock record passes the read-side check.
  • Additional note: is_lease_expired (line 350) returns False when expires_at is absent, so a malformed lock is never expired. Fail-open.

Amended policy: one authoritative configuration source

task class initial TTL heartbeat cadence stale warning reclaim grace absolute cap terminal retirement
author_issue_work 10 min (was 4 h) 2 min 5 min 10 min since last valid heartbeat 8 h, re-adoption required beyond eligible
reviewer_pr / merger_pr 10 min 2 min 5 min 10 min not applicable not eligible
conflict_fix 120 min today to be set to be set to be set to be set deferred, out of scope for the first slice
control-plane leases (control_plane_db.DEFAULT_LEASE_TTL_SECONDS, 4 h) align to this table deferred

Recovery grace lease is 10 minutes, one TTL, never a fresh full-duration lease. Race-drain interval before destructive cleanup is 2 minutes and configurable. Expected effect: abandoned author work becomes reclaimable in at most 10 minutes instead of four hours; work whose integration completed becomes cleanable in about 2 minutes.

Terminal retirement, and its separation from destructive cleanup

Retirement and cleanup are two distinct steps with two distinct gates.

Step 1 — record terminal status. Once every server-side proof below passes, the reconciler may atomically record terminal status immediately, through the generation compare-and-swap. There is no waiting period on this step.

  1. The owning PR is in its closed post-merge state at the exact recorded head: merge_commit_sha present, and the PR head SHA equal to the branch head recorded in the durable lock.
  2. The linked issue is closed or otherwise canonically terminal.
  3. The branch head is contained in the target branch, proven by git merge-base --is-ancestor.
  4. The registered worktree is clean by parse_dirty_tracked_files and is on the locked branch.
  5. No unpublished work: local head equals remote branch head.
  6. No newer lock_generation, no competing live lock, no competing branch carrying the issue marker, and no other owning PR.

Step 2 — destructive branch and worktree cleanup. Only after the 2-minute configurable race-drain interval has elapsed, and only after re-reading and re-proving all of: lock_generation, terminal state, worktree cleanliness, branch containment in the target branch, and absence of competing ownership. Any drift observed on the re-read fails closed and cancels the cleanup.

Owner heartbeat freshness and recorded pid liveness are audit evidence in both steps and are never retirement preconditions. Any missing or contradictory proof fails closed naming the exact absent element.

Legacy-lock compatibility

Deployment of the shorter TTL must not make existing durable locks immediately reclaimable.

  • A lock written before task-session heartbeats exist keeps its recorded absolute expires_at. The new short TTL and missed-heartbeat grace do not apply retroactively.
  • Such a lock leaves that preserved-expiry state by exactly one of two routes: it qualifies for terminal retirement under the proofs above, or its exact owner canonically rebinds it into the new lifecycle through the sanctioned path, which mints a real task-session identifier and a genuine first heartbeat.
  • A fresh heartbeat is never inferred from last_heartbeat_at equalling created_at. That equality is the signature of a lock that never heartbeated, and it must classify as legacy, not as fresh.
  • The compatibility disposition must be explicit, recorded in the durable record, and covered by tests in both directions.

Recorded pid semantics

  • An alive pid never establishes lease freshness and never authorizes renewal, in any path.
  • A dead pid may remain corroborating staleness evidence for the existing Issue #753 dead-session recovery path.
  • Issue #753 behavior is preserved unchanged unless a proven conflict emerges, in which case that change is separately reviewed rather than folded into this work.

Re-scoping the Issue #760 renewal path

The renewal disposition delivered by PR #791 stays in force until the replacement lifecycle is implemented and covered end to end. It is not removed, weakened, or short-circuited as a side effect of this work.

Under the new lifecycle the boundary moves. A lease that has crossed the missed-heartbeat reclaim threshold cannot be renewed on proof of exact historical ownership alone; historical ownership is not evidence of present activity. Such a lease must go through the sanctioned reclaim or re-adoption path, under generation fencing, which mints a new lease generation and a genuine heartbeat. Renewal remains available only to an owner that is demonstrably still heartbeating within the grace window.

Amendments to this issue's existing text

  • Terminal recovery section, proof list: strike the conjunct "owner process is dead or its heartbeat exceeded the allowed grace period". Terminality plus artifact safety plus generation match is sufficient. Owner liveness moves to recorded evidence.
  • Relationship section: replace the statement that this issue may subsume portions of the #760 renewal path with the re-scoping rule above, and record that Issue #760 landed via PR #791 at merge commit 620ed6e9a9550b8da2ceb82d9ab8744e8920490f.
  • Observed incident section: add the Issue #760 / PR #791 instance recorded above alongside the Issue #787 / PR #789 instance.
  • Required tests section: the fifteen listed cases stand, and are now a floor rather than the whole obligation. See AC-N6.

Acceptance criteria added

AC-N1. Every actively owned task records a task/session identifier distinct from the MCP daemon process identifier, minted per task rather than per daemon. The daemon pid is recorded as evidence alongside it and is never the ownership key.

AC-N2. Recorded pid liveness is never authorization anywhere in the shared lifecycle. An alive pid never establishes freshness and never permits renewal. A dead pid may continue to corroborate staleness for the Issue #753 recovery path, whose behavior is preserved. This promotes Issue #760 AC16 from the renewal path to the shared path, and assess_lock_freshness must no longer treat pid liveness as an independent liveness determinant.

AC-N3. Terminal retirement of an author issue lease has no owner-liveness precondition of any kind. When the terminality, artifact-safety, and generation proofs pass, retirement proceeds whether or not the recorded pid is alive and whether or not the heartbeat is fresh. Both are recorded as audit evidence. Retirement status is recorded atomically through generation compare-and-swap with no waiting period; the configurable 2-minute race-drain interval and a full re-proof apply before destructive branch or worktree cleanup, not before recording retirement.

AC-N4. verify_lock_for_mutation verifies lock_generation in addition to its existing freshness, issue, branch, and worktree checks, so a session holding a stale in-memory lock record cannot pass the read-side gate after another session has reclaimed or retired the work.

AC-N5. The Issue #760 exact-owner renewal path is re-scoped to require a fresh heartbeat within the grace window. A lease past the missed-heartbeat reclaim threshold is not renewable on historical ownership evidence alone and must use the sanctioned reclaim or re-adoption path under generation fencing. The remediation delivered by PR #791 remains in force until the replacement lifecycle is implemented and covered end to end.

AC-N6. Native MCP integration tests are required for every relevant downstream gate, driving the real tools against a real repository and a real durable lock — not only the pure assessors. Coverage must include each gate that consumes a sanction or waiver, at minimum issue_lock_worktree.assess_issue_lock_worktree base-equivalence and the duplicate-work linked-open-PR gate, plus every new gate this work introduces. Assessor-only coverage is explicitly insufficient: it did not catch either of the two sanction discard points found in review #499 on PR #791.

AC-N7. All durations — initial TTL, heartbeat cadence, stale-warning threshold, missed-heartbeat grace, absolute cap, recovery grace, and race-drain interval — come from a single authoritative policy configuration source, introduced in the first implementation slice so that the first heartbeat and TTL behavior to ship reads from it. No duration is hardcoded at a call site, and no duration is introduced in an earlier slice than the configuration that owns it.

AC-N8. Legacy locks written before task-session heartbeats exist retain their recorded absolute expiry on deployment and do not become immediately reclaimable. They leave that state only by qualifying for terminal retirement or by canonical exact-owner rebinding into the new lifecycle. A fresh heartbeat is never inferred from last_heartbeat_at equalling created_at. The disposition is recorded durably and covered by tests in both directions.

Implementation slices

Slice A — shared policy plus load-bearing heartbeat. Introduce the central policy configuration module first, so the first shipped heartbeat and TTL behavior reads from one authoritative source; no durations are introduced ahead of it. Make the heartbeat load-bearing in assess_lock_freshness with a new stale_missed_heartbeat band. Add the heartbeat writer under generation compare-and-swap. Move author_issue_work from the four-hour absolute TTL to the sliding TTL from the policy table. Mint per-task session identifiers and demote the recorded pid to evidence. Implement the AC-N8 legacy disposition in the same slice so no deployed lock is retroactively shortened. Satisfies AC-N1, AC-N2, AC-N7, AC-N8.

Slice B — terminal retirement. Add the terminal-state assessor and the reconciler-only retirement operation, reusing the Issue #760 evidence gatherer. Record terminal status atomically through the compare-and-swap, producing a status the existing _TERMINAL_OWNERSHIP_STATUSES path consumes without modification to branch_cleanup_guard. Add the race-drain interval and the mandatory re-proof before destructive cleanup. Satisfies AC-N3.

Slice C — fencing completion and renewal re-scope. Add the lock_generation check to verify_lock_for_mutation. Re-scope the Issue #760 renewal path per AC-N5. Extend the shared lifecycle to the remaining task classes and align the control-plane lease TTL. Satisfies AC-N4, AC-N5.

AC-N6 applies across all three slices and is not deferrable to the last one.

Dependencies

Landed and required as the baseline: Issue #760 via PR #791 at merge commit 620ed6e9a9550b8da2ceb82d9ab8744e8920490f (renewal assessor, reused); Issue #772 (generation compare-and-swap); Issue #747 (sliding TTL model); Issue #753 (dead-session recovery, behavior preserved); Issue #755 (owning-PR evidence pattern); Issue #601 (lease lifecycle and expired-lock reclaim). Incident evidence: Issue #787 / PR #789, and Issue #760 / PR #791. Conflict surface: any work touching assess_same_issue_lease_conflict or the issue_lock_store expiry path; none is allocated at the time of this comment.

What changed

  • This comment amends the acceptance criteria of Issue #790 by adding AC-N1 through AC-N8, correcting the terminal-recovery proof list, correcting the stated relationship to Issue #760, adding a second incident record, and fixing the policy durations and slice order.
  • No label was added, removed, or changed. No lock, branch, worktree, or allocation was created. No implementation was started.

What is blocked

  • Blocker classification: no blocker

Who/what acts next

  • Next actor: controller
  • Required action: decide whether to allocate Issue #790 for implementation, and if so allocate Slice A first as a separately bounded author task.
  • Do not do: do not implement Issue #790 from this session. Do not allocate Issue #790 concurrently with any other work touching assess_same_issue_lease_conflict or the issue_lock_store expiry path. Do not remove or weaken the PR #791 renewal remediation before the replacement lifecycle is covered end to end.

Canonical Issue State

STATE: reassessed-pending-allocation
WHO_IS_NEXT: controller
NEXT_ACTION: Decide whether to allocate Issue #790, beginning with Slice A as a separately bounded author task.
NEXT_PROMPT:

Allocate Gitea-Tools Issue #790 Slice A in Scaled-Tech-Consulting/Gitea-Tools on the prgs remote, as a separately bounded author task. Slice A is the central lease-policy configuration module plus the load-bearing task-session heartbeat: introduce the policy configuration first so the first shipped heartbeat and TTL behavior reads from one authoritative source, make last_heartbeat_at load-bearing in issue_lock_store.assess_lock_freshness with a stale_missed_heartbeat band, add the heartbeat writer under the existing lock_generation compare-and-swap, move author_issue_work from the four-hour absolute TTL to a 10-minute sliding TTL with a 2-minute heartbeat cadence and a 5-minute stale warning, mint a per-task session identifier distinct from the MCP daemon pid, and demote recorded pid liveness to evidence so it never establishes freshness and never authorizes renewal. Implement AC-N8 legacy compatibility in the same slice: locks written before task-session heartbeats keep their recorded absolute expiry and never become immediately reclaimable on deployment, they leave that state only through terminal retirement or canonical exact-owner rebinding, and a fresh heartbeat is never inferred from last_heartbeat_at equalling created_at. Preserve Issue #753 dead-session recovery behavior unchanged. Do not implement terminal retirement, the verify_lock_for_mutation generation check, or the Issue #760 renewal re-scope; those are Slices B and C. Deliver native MCP integration tests per AC-N6 for every downstream gate the change touches, not only assessor-level tests. Open a PR when complete and hand back to the controller.

WHY: The Issue #760 / PR #791 reconciliation produced a second instance of the defect this issue exists to correct, and reading the landed code showed the heartbeat field is parsed but never consulted, so the original acceptance criteria needed concrete durations, a corrected terminal-retirement rule, explicit pid semantics, a legacy compatibility rule, and mandatory native MCP integration coverage.
ISSUE: #790
RELATED_PRS: None for this issue; it remains unallocated. Baseline is Issue #760 via PR #791 at merge commit 620ed6e9a9.
BLOCKERS: None. Allocation is a controller decision, not a blocked state.
VALIDATION: Identity sysadmin, profile prgs-reconciler, role reconciler, session bound to Scaled-Tech-Consulting/Gitea-Tools on prgs at gitea.prgs.cc, identity_match true. Server-implementation parity in_parity true at 620ed6e9a9 with stale false and restart_required false; control checkout on master with a zero-entry git status --porcelain. Issue #790 read live and confirmed to be in the open state with its label set unchanged before and after this comment. Allocation state verified unallocated by four independent checks: no durable lock file for issue 790, no local branch matching 790, git ls-remote prgs 'refs/heads/790' empty, and no registered worktree matching 790. Landed behavior read directly from master at 620ed6e9: issue_lock_renewal.py in full, issue_lock_store.py assess_lock_freshness, assess_expired_lock_reclaim, assess_same_issue_lease_conflict, bind_session_lock, lock_generation and verify_lock_for_mutation, branch_cleanup_guard.assess_ownership_record_activity and assess_active_branch_ownership, worktree_cleanup_audit classification, reviewer_pr_lease TTL constants, and the gitea_mcp_server author-lease builder and ownership-record collector. A tree-wide search confirmed last_heartbeat_at is written only at gitea_mcp_server.py:2561 and read only at issue_lock_store.py:376-378, with no writer advancing it. Preflight order gitea_whoami then gitea_resolve_task_capability with task comment_issue and nothing in between preceded this comment; allowed_in_current_session true. No lock was acquired, no branch or worktree was created, no label was changed, and no implementation was begun by this controller action.
LAST_UPDATED_BY: sysadmin / prgs-reconciler (controller action)

[THREAD STATE LEDGER] Issue #790 — controller reassessment against the landed Issue #760 behavior; acceptance criteria amended Scope of this comment: issue-scope amendment only. No allocation, no branch, no worktree, no lock acquisition, no implementation, no label change. ## What is true now - Server-side decision state: Issue #790 state is open, labels unchanged. Issue #760 state is closed; PR #791 reached its closed post-merge state at 2026-07-22T01:02:40-05:00 with merge commit `620ed6e9a9550b8da2ceb82d9ab8744e8920490f` on prgs/master, from reviewed source head `a30a3ce4c37b2dde725301bef8b9ef8e04160088`. - Local verdict/state: this controller action was read-only against the working tree. No code edit, no commit, no push, no branch or worktree creation, no lock acquisition, and no Gitea-side mutation other than this comment. - Allocation state: Issue #790 verified unallocated. No durable lock file for issue 790 under `~/.cache/gitea-tools/issue-locks/`, no local branch matching `*790*`, `git ls-remote prgs 'refs/heads/*790*'` returns nothing, and no registered worktree matching `790`. ## Second incident: Issue #760 / PR #791 Issue #790 was filed from the Issue #787 / PR #789 incident. A second, independent instance occurred during the PR #791 post-merge reconciliation and is recorded here as further evidence. - Author work lease created `2026-07-22T04:19:08Z`, `expires_at 2026-07-22T08:19:08Z`; effective TTL four hours. - `last_heartbeat_at` equal to `created_at`, never advanced across the entire authoring, review-remediation, and merge cycle. - Recorded pid 39849 alive throughout — a long-lived MCP daemon started 2026-07-21 23:22:08 local, not the authoring task. - PR #791 integration into master completed at 01:02:40-05:00; Issue #760 closed by the closing reference. - Worktree `branches/fix-issue-760-exact-owner-renewal` clean, on the locked branch at `a30a3ce4`, with `git merge-base --is-ancestor a30a3ce4 620ed6e9` true and local head equal to remote head. - Reconciler assessment at `2026-07-22T06:39:43Z` — over five hours after integration completed — still returned `blocker_kind: active_branch_ownership` with blocking categories `author_lease`, `author_session`, `worktree_binding`, and `gitea_audit_worktree_cleanup` classified the worktree `active_issue_work` with `removable: false` and `preserve: true`. - No control-plane lease row exists for issue 760 or PR 791 in either direction, and no live PR #791 lease remains. The hold was entirely the file-store absolute expiry. - Reconciliation ledger for that pass: Issue #760 comment 13955. ## Root cause traced on master 620ed6e9 1. `issue_lock_store.assess_lock_freshness` returns `status: "live"` because `expires_at` had not passed and the recorded pid is alive. 2. `gitea_mcp_server._collect_branch_ownership_records` (lines 10618-10641) maps live to `status="active"` for the `author_lease` record and additionally emits a second `author_session` record with `status="active"` and `reclaim_allowed=False`. 3. `branch_cleanup_guard.assess_ownership_record_activity` (line 488) treats `"active"` as blocking for both records. 4. The bound worktree contributes the third blocking category. 5. `worktree_cleanup_audit._classify` (line 605) independently returns `active_issue_work` because the branch appears in `active_lock_branches`. Decisive finding: **the heartbeat field is inert.** `assess_lock_freshness` parses `last_heartbeat_at` at `issue_lock_store.py:376-378` and never branches on it; the value is only echoed into the live payload. Across the whole tree the field is written in exactly one place, `gitea_mcp_server.py:2561`, set to `created` at mint time. No writer advances it. Liveness is therefore `expires_at` (`issue_lock_store.py:24`, `WORK_LEASE_TTL_HOURS = 4`) OR daemon pid liveness, and nothing else. Adding heartbeat semantics is a behavior change to one function, not a schema migration — the field is present in every durable lock on disk. ## Landed components to reuse rather than rebuild - `reviewer_pr_lease` (#747) is the working sliding-TTL reference: `LEASE_TTL_MINUTES = 10`, `STALE_WARNING_MINUTES = 5`, freshness bands `active` / `stale_warning` / `expired`, expiry re-derived from every write. - `issue_lock_store.lock_generation` plus the `expected_generation` compare-and-swap inside the flock critical section (`bind_session_lock`, lines 228-241, #772). Write-path fencing exists. - `branch_cleanup_guard._TERMINAL_OWNERSHIP_STATUSES` (line 162) contains `released`, `terminal`, and `closed`, and `assess_ownership_record_activity` (line 481) returns `blocks=False` for them. Terminal retirement needs a status producer, not a new guard. - `issue_lock_renewal.assess_exact_owner_lease_renewal` (#760) implements the exact-owner, clean-worktree, agreeing-heads, no-competing-claim evidence set with fail-closed refusals naming each absent element. - `reviewer_pr_lease.new_session_id()` mints a per-task session identifier. ## Gaps in this issue as originally written - **G1** — no requirement for a task/session identity distinct from the daemon pid. The record lists both `session ID` and `process ID`, but never states that pid lifetime is not task lifetime. `bind_session_lock` lines 200-201 write `os.getpid()` into both `pid` and `session_pid`. - **G2** — no concrete durations; every value deferred to "configurable per task class". - **G3** — pid-as-corroborating-only appears only as a non-goal, "not the sole authority", which is weaker than "never authorization". `assess_lock_freshness` line 394 still uses pid as an independent staleness trigger. - **G4** — the terminal-recovery proof list conjoins "owner process is dead or its heartbeat exceeded the allowed grace period" with the terminality and artifact proofs. Terminal work is terminal regardless of whether some session is still heartbeating on it; a session heartbeating on integrated work is a leaked session, not protected work. As originally written, a faithful implementation would still have stalled the PR #791 cleanup. - **G5** — this issue predates the PR #791 landing and states that #790 "may subsume or replace portions of the renewal path introduced by #760". That relationship is now inverted: the #760 renewal path landed and requires re-scoping, because it grants renewal to the exact owner of an expired lease with no heartbeat requirement at all — deliberately, since heartbeats did not exist. - **G6** — the fifteen required tests are assessor-level. Review #499 finding F2 proved on this exact code that assessor-level coverage misses discard points: the #760 renewal waiver was computed and then discarded at two later gates, `issue_lock_worktree.assess_issue_lock_worktree` base-equivalence and the duplicate-work gate's linked-open-PR blocker. - **G7** — `issue_lock_store.verify_lock_for_mutation` (line 630) checks freshness, issue number, branch, and worktree, but not `lock_generation`. Fencing is write-side only; a session holding a stale in-memory lock record passes the read-side check. - Additional note: `is_lease_expired` (line 350) returns False when `expires_at` is absent, so a malformed lock is never expired. Fail-open. ## Amended policy: one authoritative configuration source | task class | initial TTL | heartbeat cadence | stale warning | reclaim grace | absolute cap | terminal retirement | |---|---|---|---|---|---|---| | `author_issue_work` | 10 min (was 4 h) | 2 min | 5 min | 10 min since last valid heartbeat | 8 h, re-adoption required beyond | eligible | | `reviewer_pr` / `merger_pr` | 10 min | 2 min | 5 min | 10 min | not applicable | not eligible | | `conflict_fix` | 120 min today | to be set | to be set | to be set | to be set | deferred, out of scope for the first slice | | control-plane leases (`control_plane_db.DEFAULT_LEASE_TTL_SECONDS`, 4 h) | align to this table | — | — | — | — | deferred | Recovery grace lease is 10 minutes, one TTL, never a fresh full-duration lease. Race-drain interval before destructive cleanup is 2 minutes and configurable. Expected effect: abandoned author work becomes reclaimable in at most 10 minutes instead of four hours; work whose integration completed becomes cleanable in about 2 minutes. ## Terminal retirement, and its separation from destructive cleanup Retirement and cleanup are two distinct steps with two distinct gates. **Step 1 — record terminal status.** Once every server-side proof below passes, the reconciler may atomically record terminal status immediately, through the generation compare-and-swap. There is no waiting period on this step. 1. The owning PR is in its closed post-merge state at the exact recorded head: `merge_commit_sha` present, and the PR head SHA equal to the branch head recorded in the durable lock. 2. The linked issue is closed or otherwise canonically terminal. 3. The branch head is contained in the target branch, proven by `git merge-base --is-ancestor`. 4. The registered worktree is clean by `parse_dirty_tracked_files` and is on the locked branch. 5. No unpublished work: local head equals remote branch head. 6. No newer `lock_generation`, no competing live lock, no competing branch carrying the issue marker, and no other owning PR. **Step 2 — destructive branch and worktree cleanup.** Only after the 2-minute configurable race-drain interval has elapsed, and only after re-reading and re-proving all of: `lock_generation`, terminal state, worktree cleanliness, branch containment in the target branch, and absence of competing ownership. Any drift observed on the re-read fails closed and cancels the cleanup. Owner heartbeat freshness and recorded pid liveness are audit evidence in both steps and are never retirement preconditions. Any missing or contradictory proof fails closed naming the exact absent element. ## Legacy-lock compatibility Deployment of the shorter TTL must not make existing durable locks immediately reclaimable. - A lock written before task-session heartbeats exist keeps its recorded absolute `expires_at`. The new short TTL and missed-heartbeat grace do not apply retroactively. - Such a lock leaves that preserved-expiry state by exactly one of two routes: it qualifies for terminal retirement under the proofs above, or its exact owner canonically rebinds it into the new lifecycle through the sanctioned path, which mints a real task-session identifier and a genuine first heartbeat. - A fresh heartbeat is never inferred from `last_heartbeat_at` equalling `created_at`. That equality is the signature of a lock that never heartbeated, and it must classify as legacy, not as fresh. - The compatibility disposition must be explicit, recorded in the durable record, and covered by tests in both directions. ## Recorded pid semantics - An alive pid never establishes lease freshness and never authorizes renewal, in any path. - A dead pid may remain corroborating staleness evidence for the existing Issue #753 dead-session recovery path. - Issue #753 behavior is preserved unchanged unless a proven conflict emerges, in which case that change is separately reviewed rather than folded into this work. ## Re-scoping the Issue #760 renewal path The renewal disposition delivered by PR #791 stays in force until the replacement lifecycle is implemented and covered end to end. It is not removed, weakened, or short-circuited as a side effect of this work. Under the new lifecycle the boundary moves. A lease that has crossed the missed-heartbeat reclaim threshold cannot be renewed on proof of exact historical ownership alone; historical ownership is not evidence of present activity. Such a lease must go through the sanctioned reclaim or re-adoption path, under generation fencing, which mints a new lease generation and a genuine heartbeat. Renewal remains available only to an owner that is demonstrably still heartbeating within the grace window. ## Amendments to this issue's existing text - **Terminal recovery section, proof list**: strike the conjunct "owner process is dead or its heartbeat exceeded the allowed grace period". Terminality plus artifact safety plus generation match is sufficient. Owner liveness moves to recorded evidence. - **Relationship section**: replace the statement that this issue may subsume portions of the #760 renewal path with the re-scoping rule above, and record that Issue #760 landed via PR #791 at merge commit `620ed6e9a9550b8da2ceb82d9ab8744e8920490f`. - **Observed incident section**: add the Issue #760 / PR #791 instance recorded above alongside the Issue #787 / PR #789 instance. - **Required tests section**: the fifteen listed cases stand, and are now a floor rather than the whole obligation. See AC-N6. ## Acceptance criteria added **AC-N1.** Every actively owned task records a task/session identifier distinct from the MCP daemon process identifier, minted per task rather than per daemon. The daemon pid is recorded as evidence alongside it and is never the ownership key. **AC-N2.** Recorded pid liveness is never authorization anywhere in the shared lifecycle. An alive pid never establishes freshness and never permits renewal. A dead pid may continue to corroborate staleness for the Issue #753 recovery path, whose behavior is preserved. This promotes Issue #760 AC16 from the renewal path to the shared path, and `assess_lock_freshness` must no longer treat pid liveness as an independent liveness determinant. **AC-N3.** Terminal retirement of an author issue lease has no owner-liveness precondition of any kind. When the terminality, artifact-safety, and generation proofs pass, retirement proceeds whether or not the recorded pid is alive and whether or not the heartbeat is fresh. Both are recorded as audit evidence. Retirement status is recorded atomically through generation compare-and-swap with no waiting period; the configurable 2-minute race-drain interval and a full re-proof apply before destructive branch or worktree cleanup, not before recording retirement. **AC-N4.** `verify_lock_for_mutation` verifies `lock_generation` in addition to its existing freshness, issue, branch, and worktree checks, so a session holding a stale in-memory lock record cannot pass the read-side gate after another session has reclaimed or retired the work. **AC-N5.** The Issue #760 exact-owner renewal path is re-scoped to require a fresh heartbeat within the grace window. A lease past the missed-heartbeat reclaim threshold is not renewable on historical ownership evidence alone and must use the sanctioned reclaim or re-adoption path under generation fencing. The remediation delivered by PR #791 remains in force until the replacement lifecycle is implemented and covered end to end. **AC-N6.** Native MCP integration tests are required for every relevant downstream gate, driving the real tools against a real repository and a real durable lock — not only the pure assessors. Coverage must include each gate that consumes a sanction or waiver, at minimum `issue_lock_worktree.assess_issue_lock_worktree` base-equivalence and the duplicate-work linked-open-PR gate, plus every new gate this work introduces. Assessor-only coverage is explicitly insufficient: it did not catch either of the two sanction discard points found in review #499 on PR #791. **AC-N7.** All durations — initial TTL, heartbeat cadence, stale-warning threshold, missed-heartbeat grace, absolute cap, recovery grace, and race-drain interval — come from a single authoritative policy configuration source, introduced in the first implementation slice so that the first heartbeat and TTL behavior to ship reads from it. No duration is hardcoded at a call site, and no duration is introduced in an earlier slice than the configuration that owns it. **AC-N8.** Legacy locks written before task-session heartbeats exist retain their recorded absolute expiry on deployment and do not become immediately reclaimable. They leave that state only by qualifying for terminal retirement or by canonical exact-owner rebinding into the new lifecycle. A fresh heartbeat is never inferred from `last_heartbeat_at` equalling `created_at`. The disposition is recorded durably and covered by tests in both directions. ## Implementation slices **Slice A — shared policy plus load-bearing heartbeat.** Introduce the central policy configuration module first, so the first shipped heartbeat and TTL behavior reads from one authoritative source; no durations are introduced ahead of it. Make the heartbeat load-bearing in `assess_lock_freshness` with a new `stale_missed_heartbeat` band. Add the heartbeat writer under generation compare-and-swap. Move `author_issue_work` from the four-hour absolute TTL to the sliding TTL from the policy table. Mint per-task session identifiers and demote the recorded pid to evidence. Implement the AC-N8 legacy disposition in the same slice so no deployed lock is retroactively shortened. Satisfies AC-N1, AC-N2, AC-N7, AC-N8. **Slice B — terminal retirement.** Add the terminal-state assessor and the reconciler-only retirement operation, reusing the Issue #760 evidence gatherer. Record terminal status atomically through the compare-and-swap, producing a status the existing `_TERMINAL_OWNERSHIP_STATUSES` path consumes without modification to `branch_cleanup_guard`. Add the race-drain interval and the mandatory re-proof before destructive cleanup. Satisfies AC-N3. **Slice C — fencing completion and renewal re-scope.** Add the `lock_generation` check to `verify_lock_for_mutation`. Re-scope the Issue #760 renewal path per AC-N5. Extend the shared lifecycle to the remaining task classes and align the control-plane lease TTL. Satisfies AC-N4, AC-N5. AC-N6 applies across all three slices and is not deferrable to the last one. ## Dependencies Landed and required as the baseline: Issue #760 via PR #791 at merge commit `620ed6e9a9550b8da2ceb82d9ab8744e8920490f` (renewal assessor, reused); Issue #772 (generation compare-and-swap); Issue #747 (sliding TTL model); Issue #753 (dead-session recovery, behavior preserved); Issue #755 (owning-PR evidence pattern); Issue #601 (lease lifecycle and expired-lock reclaim). Incident evidence: Issue #787 / PR #789, and Issue #760 / PR #791. Conflict surface: any work touching `assess_same_issue_lease_conflict` or the `issue_lock_store` expiry path; none is allocated at the time of this comment. ## What changed - This comment amends the acceptance criteria of Issue #790 by adding AC-N1 through AC-N8, correcting the terminal-recovery proof list, correcting the stated relationship to Issue #760, adding a second incident record, and fixing the policy durations and slice order. - No label was added, removed, or changed. No lock, branch, worktree, or allocation was created. No implementation was started. ## What is blocked - Blocker classification: no blocker ## Who/what acts next - Next actor: controller - Required action: decide whether to allocate Issue #790 for implementation, and if so allocate Slice A first as a separately bounded author task. - Do not do: do not implement Issue #790 from this session. Do not allocate Issue #790 concurrently with any other work touching `assess_same_issue_lease_conflict` or the `issue_lock_store` expiry path. Do not remove or weaken the PR #791 renewal remediation before the replacement lifecycle is covered end to end. ## Canonical Issue State STATE: reassessed-pending-allocation WHO_IS_NEXT: controller NEXT_ACTION: Decide whether to allocate Issue #790, beginning with Slice A as a separately bounded author task. NEXT_PROMPT: ```text Allocate Gitea-Tools Issue #790 Slice A in Scaled-Tech-Consulting/Gitea-Tools on the prgs remote, as a separately bounded author task. Slice A is the central lease-policy configuration module plus the load-bearing task-session heartbeat: introduce the policy configuration first so the first shipped heartbeat and TTL behavior reads from one authoritative source, make last_heartbeat_at load-bearing in issue_lock_store.assess_lock_freshness with a stale_missed_heartbeat band, add the heartbeat writer under the existing lock_generation compare-and-swap, move author_issue_work from the four-hour absolute TTL to a 10-minute sliding TTL with a 2-minute heartbeat cadence and a 5-minute stale warning, mint a per-task session identifier distinct from the MCP daemon pid, and demote recorded pid liveness to evidence so it never establishes freshness and never authorizes renewal. Implement AC-N8 legacy compatibility in the same slice: locks written before task-session heartbeats keep their recorded absolute expiry and never become immediately reclaimable on deployment, they leave that state only through terminal retirement or canonical exact-owner rebinding, and a fresh heartbeat is never inferred from last_heartbeat_at equalling created_at. Preserve Issue #753 dead-session recovery behavior unchanged. Do not implement terminal retirement, the verify_lock_for_mutation generation check, or the Issue #760 renewal re-scope; those are Slices B and C. Deliver native MCP integration tests per AC-N6 for every downstream gate the change touches, not only assessor-level tests. Open a PR when complete and hand back to the controller. ``` WHY: The Issue #760 / PR #791 reconciliation produced a second instance of the defect this issue exists to correct, and reading the landed code showed the heartbeat field is parsed but never consulted, so the original acceptance criteria needed concrete durations, a corrected terminal-retirement rule, explicit pid semantics, a legacy compatibility rule, and mandatory native MCP integration coverage. ISSUE: #790 RELATED_PRS: None for this issue; it remains unallocated. Baseline is Issue #760 via PR #791 at merge commit 620ed6e9a9550b8da2ceb82d9ab8744e8920490f. BLOCKERS: None. Allocation is a controller decision, not a blocked state. VALIDATION: Identity sysadmin, profile prgs-reconciler, role reconciler, session bound to Scaled-Tech-Consulting/Gitea-Tools on prgs at gitea.prgs.cc, identity_match true. Server-implementation parity in_parity true at 620ed6e9a9550b8da2ceb82d9ab8744e8920490f with stale false and restart_required false; control checkout on master with a zero-entry git status --porcelain. Issue #790 read live and confirmed to be in the open state with its label set unchanged before and after this comment. Allocation state verified unallocated by four independent checks: no durable lock file for issue 790, no local branch matching 790, git ls-remote prgs 'refs/heads/*790*' empty, and no registered worktree matching 790. Landed behavior read directly from master at 620ed6e9: issue_lock_renewal.py in full, issue_lock_store.py assess_lock_freshness, assess_expired_lock_reclaim, assess_same_issue_lease_conflict, bind_session_lock, lock_generation and verify_lock_for_mutation, branch_cleanup_guard.assess_ownership_record_activity and assess_active_branch_ownership, worktree_cleanup_audit classification, reviewer_pr_lease TTL constants, and the gitea_mcp_server author-lease builder and ownership-record collector. A tree-wide search confirmed last_heartbeat_at is written only at gitea_mcp_server.py:2561 and read only at issue_lock_store.py:376-378, with no writer advancing it. Preflight order gitea_whoami then gitea_resolve_task_capability with task comment_issue and nothing in between preceded this comment; allowed_in_current_session true. No lock was acquired, no branch or worktree was created, no label was changed, and no implementation was begun by this controller action. LAST_UPDATED_BY: sysadmin / prgs-reconciler (controller action)
Author
Owner

[THREAD STATE LEDGER] Issue #790 — scope transfer: this issue now tracks Slice A only; Slices B and C move to #792 and #793

What is true now

  • Server-side decision state: Issue #790 is open. Two follow-up issues were created to carry the work this issue no longer tracks — #792 for Slice B and #793 for Slice C — each carrying the canonical five-label workflow set that Issue #790 itself carries, with workflow label validation reporting valid true.
  • Scope of Issue #790 after this comment: Slice A only. The shared lease policy configuration, the load-bearing task heartbeat, per-task session identity distinct from the MCP daemon process identifier, the heartbeat writer under generation compare-and-swap, and legacy-lock compatibility.
  • Local verdict/state: author session jcwalker3 / prgs-author holds the durable lock for this issue on branch fix/issue-790-slice-a-heartbeat-policy from worktree branches/issue-790-slice-a-heartbeat-policy. Slice A is implemented, exercised against the full test suite and a clean master baseline worktree, committed at 243f52dc7959c362242ebea321fafa3950f13083, and pushed. The pull request is opened immediately after this comment.

Why the transfer

The enforced create-PR gate at gitea_mcp_server.py:4684 fails closed unless the pull-request title or body contains Closes #790 or Fixes #790 exactly. The repository therefore has no partial-slice pull-request tracking: any pull request for this issue must carry a closing reference, and merging it will close this issue. docs/llm-workflow-runbooks.md:550 documents Implements #N and Refs #N as non-closing forms, but the gate does not accept them for the locked issue.

Rather than merge a pull request that closes an issue with two thirds of its scope unbuilt, the remaining scope has been transferred to dedicated issues before the pull request is opened. Closing Issue #790 on merge is then correct, because the work it no longer tracks lives in #792 and #793.

Explicit statements required by this transfer

  • Issue #790 now tracks, and is completed by, Slice A. Its remaining acceptance criteria after this comment are AC-N1, AC-N2, AC-N7, and AC-N8 from comment 13958, plus AC-N6 as it applies to Slice A.
  • Slices B and C remain required. They are not cancelled, deferred indefinitely, or absorbed. They are transferred.
  • Closing Issue #790 must not be interpreted as completing those follow-ups. #792 and #793 are independently tracked and independently allocatable.
  • Do not reopen Issue #790 later to represent the transferred work. The follow-up issues are the durable record.

Transferred scope

#792 — Slice B, terminal retirement. Terminal author-lease retirement; a reconciler-only retirement operation; terminal status recorded atomically through the lock_generation compare-and-swap; the configurable two-minute race-drain interval; full revalidation of generation, terminal state, cleanliness, containment, and ownership before destructive branch or worktree cleanup; native MCP integration coverage. Carries AC-N3 from comment 13958 as AC-B1, and the corrected proof list with the owner-liveness conjunct struck.

#793 — Slice C, fencing completion and task-class alignment. lock_generation verification in verify_lock_for_mutation; the Issue #760 renewal re-scope under heartbeat and fencing rules; remaining task-class policy alignment for reviewer, merger, and conflict-fix leases; control-plane lease alignment; native MCP integration coverage. Carries AC-N4 and AC-N5 from comment 13958 as AC-C1 and AC-C2.

AC-N6, the native MCP integration requirement, applies to all three slices and is restated in each follow-up issue.

What changed

  • Created Issue #792 and Issue #793.
  • Recorded this scope transfer. No label on Issue #790 was added, removed, or changed by this comment.

What is blocked

  • Blocker classification: no blocker

Who/what acts next

  • Next actor: reviewer
  • Required action: review the Slice A pull request opened against this issue.
  • Do not do: do not allocate or implement Issue #792 or Issue #793 as part of reviewing Slice A. Do not treat the merge of the Slice A pull request as completing them.

Canonical Issue State

STATE: implementation-complete
WHO_IS_NEXT: reviewer
NEXT_ACTION: Review the Slice A pull request at head 243f52dc79.
NEXT_PROMPT:

Review the Gitea-Tools pull request that closes Issue #790, at head 243f52dc7959c362242ebea321fafa3950f13083, base master, in Scaled-Tech-Consulting/Gitea-Tools on the prgs remote. It delivers Slice A only: the lease_policy configuration module, load-bearing heartbeat freshness in issue_lock_store.assess_lock_freshness with the stale_missed_heartbeat and stale_absolute_cap bands, per-task session identity minted without process identifiers, the heartbeat writer under the lock_generation compare-and-swap, the gitea_heartbeat_issue_lock tool, and AC-N8 legacy-lock compatibility. Read Issue #790 comment 13958 for AC-N1, AC-N2, AC-N6, AC-N7 and AC-N8 and verify each against the diff. Confirm the two deliberate asymmetries: an alive pid never establishes freshness anywhere, while a dead pid still marks a lease stale so Issue #753 dead-session recovery keys on the classification it always did. Confirm legacy locks keep their recorded absolute expiry and cannot be reclaimed by the heartbeat band, that the legacy discriminator is the explicit lifecycle_version marker and never a timestamp comparison, and that rebinding a lapsed legacy lease is refused. Confirm no caller-declarable parameter grants ownership, renewal, or eligibility, and that the task_session_id parameter can only cause refusal. Confirm the Issue #760 renewal path, Issue #753 recovery, Issue #755 owning-PR evidence, Issue #772 compare-and-swap, and Issue #747 reviewer TTL are unchanged. Confirm Slice B and Slice C behavior is absent: no terminal-retirement assessor or tool, no race-drain workflow, no generation check in verify_lock_for_mutation, no renewal re-scope, no migration of other task classes. Run the new suites, the lock and lease regression set, and the full suite, comparing against a clean master baseline worktree at 620ed6e9 which carries 11 pre-existing failures. Review only this pull request at this exact head; do not merge, and do not allocate Issue #792 or Issue #793.

WHY: The enforced create-PR gate requires a closing reference for the locked issue, so the scope Issue #790 no longer delivers was transferred to dedicated issues before the pull request was opened.
ISSUE: #790
RELATED_PRS: The Slice A pull request, opened immediately after this comment at head 243f52dc79. Follow-ups #792 and #793 have no pull request and are unallocated.
BLOCKERS: None.
VALIDATION: Identity jcwalker3, profile prgs-author, role author, bound to Scaled-Tech-Consulting/Gitea-Tools on prgs at gitea.prgs.cc, identity_match true. Issue #792 and Issue #793 created through native gitea_create_issue with workflow label validation reporting valid true for both. Preflight order gitea_whoami then gitea_resolve_task_capability with the exact task name and nothing in between preceded each of the two issue creations and this comment. The gate wording requiring a closing reference was read directly from gitea_mcp_server.py line 4684, and the non-closing forms documented at docs/llm-workflow-runbooks.md line 550 were confirmed not to satisfy it.
LAST_UPDATED_BY: jcwalker3 / prgs-author (author session)

[THREAD STATE LEDGER] Issue #790 — scope transfer: this issue now tracks Slice A only; Slices B and C move to #792 and #793 ## What is true now - Server-side decision state: Issue #790 is open. Two follow-up issues were created to carry the work this issue no longer tracks — **#792** for Slice B and **#793** for Slice C — each carrying the canonical five-label workflow set that Issue #790 itself carries, with workflow label validation reporting valid true. - Scope of Issue #790 after this comment: **Slice A only.** The shared lease policy configuration, the load-bearing task heartbeat, per-task session identity distinct from the MCP daemon process identifier, the heartbeat writer under generation compare-and-swap, and legacy-lock compatibility. - Local verdict/state: author session jcwalker3 / prgs-author holds the durable lock for this issue on branch `fix/issue-790-slice-a-heartbeat-policy` from worktree `branches/issue-790-slice-a-heartbeat-policy`. Slice A is implemented, exercised against the full test suite and a clean master baseline worktree, committed at `243f52dc7959c362242ebea321fafa3950f13083`, and pushed. The pull request is opened immediately after this comment. ## Why the transfer The enforced create-PR gate at `gitea_mcp_server.py:4684` fails closed unless the pull-request title or body contains `Closes #790` or `Fixes #790` exactly. The repository therefore has no partial-slice pull-request tracking: any pull request for this issue must carry a closing reference, and merging it will close this issue. `docs/llm-workflow-runbooks.md:550` documents `Implements #N` and `Refs #N` as non-closing forms, but the gate does not accept them for the locked issue. Rather than merge a pull request that closes an issue with two thirds of its scope unbuilt, the remaining scope has been transferred to dedicated issues before the pull request is opened. Closing Issue #790 on merge is then correct, because the work it no longer tracks lives in #792 and #793. ## Explicit statements required by this transfer - **Issue #790 now tracks, and is completed by, Slice A.** Its remaining acceptance criteria after this comment are AC-N1, AC-N2, AC-N7, and AC-N8 from comment 13958, plus AC-N6 as it applies to Slice A. - **Slices B and C remain required.** They are not cancelled, deferred indefinitely, or absorbed. They are transferred. - **Closing Issue #790 must not be interpreted as completing those follow-ups.** #792 and #793 are independently tracked and independently allocatable. - Do not reopen Issue #790 later to represent the transferred work. The follow-up issues are the durable record. ## Transferred scope **#792 — Slice B, terminal retirement.** Terminal author-lease retirement; a reconciler-only retirement operation; terminal status recorded atomically through the `lock_generation` compare-and-swap; the configurable two-minute race-drain interval; full revalidation of generation, terminal state, cleanliness, containment, and ownership before destructive branch or worktree cleanup; native MCP integration coverage. Carries AC-N3 from comment 13958 as AC-B1, and the corrected proof list with the owner-liveness conjunct struck. **#793 — Slice C, fencing completion and task-class alignment.** `lock_generation` verification in `verify_lock_for_mutation`; the Issue #760 renewal re-scope under heartbeat and fencing rules; remaining task-class policy alignment for reviewer, merger, and conflict-fix leases; control-plane lease alignment; native MCP integration coverage. Carries AC-N4 and AC-N5 from comment 13958 as AC-C1 and AC-C2. AC-N6, the native MCP integration requirement, applies to all three slices and is restated in each follow-up issue. ## What changed - Created Issue #792 and Issue #793. - Recorded this scope transfer. No label on Issue #790 was added, removed, or changed by this comment. ## What is blocked - Blocker classification: no blocker ## Who/what acts next - Next actor: reviewer - Required action: review the Slice A pull request opened against this issue. - Do not do: do not allocate or implement Issue #792 or Issue #793 as part of reviewing Slice A. Do not treat the merge of the Slice A pull request as completing them. ## Canonical Issue State STATE: implementation-complete WHO_IS_NEXT: reviewer NEXT_ACTION: Review the Slice A pull request at head 243f52dc7959c362242ebea321fafa3950f13083. NEXT_PROMPT: ```text Review the Gitea-Tools pull request that closes Issue #790, at head 243f52dc7959c362242ebea321fafa3950f13083, base master, in Scaled-Tech-Consulting/Gitea-Tools on the prgs remote. It delivers Slice A only: the lease_policy configuration module, load-bearing heartbeat freshness in issue_lock_store.assess_lock_freshness with the stale_missed_heartbeat and stale_absolute_cap bands, per-task session identity minted without process identifiers, the heartbeat writer under the lock_generation compare-and-swap, the gitea_heartbeat_issue_lock tool, and AC-N8 legacy-lock compatibility. Read Issue #790 comment 13958 for AC-N1, AC-N2, AC-N6, AC-N7 and AC-N8 and verify each against the diff. Confirm the two deliberate asymmetries: an alive pid never establishes freshness anywhere, while a dead pid still marks a lease stale so Issue #753 dead-session recovery keys on the classification it always did. Confirm legacy locks keep their recorded absolute expiry and cannot be reclaimed by the heartbeat band, that the legacy discriminator is the explicit lifecycle_version marker and never a timestamp comparison, and that rebinding a lapsed legacy lease is refused. Confirm no caller-declarable parameter grants ownership, renewal, or eligibility, and that the task_session_id parameter can only cause refusal. Confirm the Issue #760 renewal path, Issue #753 recovery, Issue #755 owning-PR evidence, Issue #772 compare-and-swap, and Issue #747 reviewer TTL are unchanged. Confirm Slice B and Slice C behavior is absent: no terminal-retirement assessor or tool, no race-drain workflow, no generation check in verify_lock_for_mutation, no renewal re-scope, no migration of other task classes. Run the new suites, the lock and lease regression set, and the full suite, comparing against a clean master baseline worktree at 620ed6e9 which carries 11 pre-existing failures. Review only this pull request at this exact head; do not merge, and do not allocate Issue #792 or Issue #793. ``` WHY: The enforced create-PR gate requires a closing reference for the locked issue, so the scope Issue #790 no longer delivers was transferred to dedicated issues before the pull request was opened. ISSUE: #790 RELATED_PRS: The Slice A pull request, opened immediately after this comment at head 243f52dc7959c362242ebea321fafa3950f13083. Follow-ups #792 and #793 have no pull request and are unallocated. BLOCKERS: None. VALIDATION: Identity jcwalker3, profile prgs-author, role author, bound to Scaled-Tech-Consulting/Gitea-Tools on prgs at gitea.prgs.cc, identity_match true. Issue #792 and Issue #793 created through native gitea_create_issue with workflow label validation reporting valid true for both. Preflight order gitea_whoami then gitea_resolve_task_capability with the exact task name and nothing in between preceded each of the two issue creations and this comment. The gate wording requiring a closing reference was read directly from gitea_mcp_server.py line 4684, and the non-closing forms documented at docs/llm-workflow-runbooks.md line 550 were confirmed not to satisfy it. LAST_UPDATED_BY: jcwalker3 / prgs-author (author session)
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-22 03:31:27 -05:00
sysadmin removed the status:pr-open label 2026-07-24 08:26:12 -05:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

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