Bind mutation/credential paths to a process-local native MCP runtime so env
spoofing, direct imports, and offline helpers cannot reconstruct session gates
after native transport failure. Quarantine contaminated formal reviews under
controller authority and honor quarantine in review feedback, merge eligibility,
merge mutation, and canonical handoff validation. Add regression coverage for
the second (PR #694 / review 427) incident class.
Fixes the final-report validator defects from Issue #698 (original lead
plus the independent reproduction recorded during the PR #703 formal
review, comment 11246):
- Legacy fields: the review/merger required-field tables no longer demand
'Pinned reviewed head', 'Scratch worktree used', 'Worktree path',
'Worktree dirty', 'Mutations', 'Next', 'Issue/PR', 'Branch/SHA', or
'Files changed' — names the canonical review-merge-final-report schema
forbids or replaces. The canonical names ('Reviewed head SHA', 'Review
worktree path/dirty', 'Safe next action', mutation categories) are
required instead, with backward-compatible aliases where the schema
permits them.
- Structured proof: workflow-load helper results are recognized in
colon, key=value, and JSON renderings; validation pass proof is
accepted anywhere in the Validation field value (for example
'focused 50 passed; full 2665 passed').
- Mutation inference: review mutations are inferred only from
authoritative evidence (performed=true and not gated); read-only
diagnostics and pre-API rejections no longer count as mutations.
- Lease release vs cleanup: canonical reviewer lease release (release
tool call or terminal phase=released marker) is lease lifecycle, not
post-merge cleanup, and no longer triggers the branch/worktree cleanup
checklist; genuine delete/remove claims still require full proof.
- Blocked reports: a legitimately blocked run that states an explicit
'Reviewed/Candidate head SHA: none' with no verdict, merge, or started
validation owes no head proofs; approval-time and merge-time live-head
proofs are demanded only once the corresponding phase begins, and a
report that states no head at all still fails closed.
- action_log robustness: malformed (non-dict) entries and non-list logs
are reported as clear sanitized findings (position and type only, no
content echo) instead of crashing with AttributeError; a defective
validator rule now fails closed with a sanitized block finding rather
than raising a secondary exception.
27 new regression tests, including canonical fixtures modeled on the
PR #703 formal-review handoff and the blocked preflight report from the
prior #698 reproductions. Two legacy-field test fixtures updated to the
canonical schema. Full suite: 2663 passed, 6 skipped, 161 subtests.
Co-Authored-By: Claude Fable 5 <[email protected]>
Add fail-closed assessment and MCP tool for expired/superseded-head
comment-backed reviewer leases that lack a control-plane lease_id.
- assess_obsolete_reviewer_comment_lease_cleanup eligibility matrix
- diagnose classifications: foreign_active_current_head,
foreign_expired_current_head, foreign_completed_superseded_head,
foreign_expired_superseded_head, orphaned_owner_missing,
ambiguous_conflicting_evidence
- gitea_cleanup_obsolete_reviewer_comment_lease with confirmation
CLEANUP OBSOLETE REVIEWER LEASE <n> and controller_recovery_authorized
- preserve audit history via append-only phase=released marker
- never repoint, transfer validation, use PID ownership, or steal
active current-head foreign leases
- regression for PR #688 lease comment 10749 after recorded expiry
Prevention hardening for the #670 incident (bare direct-to-master commit
2fa97c26 and the PR #654 merger stable-branch push attempt). Worker sessions
must never publish stable branches directly; stable updates land only through
sanctioned Gitea merge tooling or an authorized reconciler path.
New pure module stable_branch_push_guard.py:
- classify_push_command: detects git push <remote> <stable-ref> equivalents
including refspecs (HEAD:<ref>, +refs/heads/x:refs/heads/<ref>), --force,
--delete/:<ref>, and --dry-run/-n no-op probes (dry-run still proves intent).
Fetch/pull and feature-branch pushes are never flagged; sanctioned
gitea_merge_pr / API merge is not a push.
- assess_root_checkout_local_commit: detects control-checkout commits not on an
issue feature branch (branches/ worktrees exempt).
- redact_command: strips URL userinfo/token assignments before logging.
- build_contamination_record + assess_contamination_gate: durable marker shape
and fail-closed gate (reconciler-exempt; comment/lock stay allowed for handoff).
Server wiring (gitea_mcp_server.py, mcp_session_state.py):
- KIND_STABLE_BRANCH_CONTAMINATION durable session marker (per profile identity).
- _enforce_stable_branch_contamination_gate wired into verify_preflight_purity
so review/merge/close/completion mutations fail closed while contaminated.
- gitea_record_stable_branch_push_attempt: classify + mark on detection.
- gitea_audit_stable_branch_contamination: reconciler-only inspect/clear; a
worker session can never self-clear.
Tests (AC5): no-op dry-run push, real direct push, sanctioned Gitea merge,
fetch-only, root-checkout local commit, feature-branch push allowed, gate
block/reconciler-exempt, redaction. 51 new tests; full suite 2596 passed.
Docs: llm-project-workflow SKILL.md — universal rule, blocker class, and a
dedicated "Stable Branch Push Protection" section (worker sessions must never
push stable branches directly).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add orthogonal role:* (single-active owner) and hazard:* (additive warning)
lifecycle labels plus status:changes-requested, folding the #603 state:*
vocabulary into the canonical status:* set rather than a conflicting parallel
prefix.
- issue_workflow_labels.py: ROLE_/HAZARD_ specs + frozensets; role/hazard
transition maps and helpers (transition_role_labels, add/clear_hazard_label,
canonical_role_label, canonical_hazard_label, is_discussion,
is_implementation_candidate, requires_blocking_reason); state:* -> status:*
synonym transitions; assess_issue_labels surfaces role/hazard dims and flags
multiple active role labels
- docs/label-taxonomy.md: role, hazard, state->canonical migration, and
allocator cross-check sections
- tests: role/hazard/discussion/blocking-reason/state-synonym coverage
- manage_labels.py seeds the new labels automatically via CANONICAL_LABEL_SPECS
Labels remain advisory; control-plane leases (#601) and live PR state stay the
source of truth for mutations (#603 AC2).
Closes#603
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Closes#627
Merges paginated repository-label inventory for gitea_set_issue_labels so later-page labels are not falsely rejected during full-set replacement.
Replace single-page GET labels?limit=100 with api_get_all-backed
_repo_label_id_map so later-page labels (e.g. type:feature,
workflow-hardening) are not falsely rejected during full-set replacement.
Also add post-mutation verification, fix related MCP/CLI label inventory
call sites, and add multi-page regression tests for the #601 reconciler
failure mode.
Closes#627
- Update MCP expired-lock recovery test for sticky expired ownership
(live pid + present worktree still fail closed under #601 reclaim rules)
- Remove trailing blank line at EOF in control_plane_db.py
Add phase-1 observability bridge that turns provider observations into
normal Gitea issues and control-plane incident_links rows. Dry-run is
default; apply creates/links through sanctioned Gitea issue paths only.
Raw incidents remain non-assignable; the #600 allocator sees bridge work
only as ordinary Gitea issues. Builds on #613 substrate and #600 allocator.
Closes#612
Add gitea_allocate_next_work and allocator_service routing policy on top of
the #613 ControlPlaneDB substrate. Workers get atomic assign+lease results
(or wait/no_safe_work/terminal-path outcomes) without self-selecting work
via file locks or comment-only leases. #612 remains downstream.
Closes#600
Prior REQUEST_CHANGES on head A no longer blocks formal re-review on
head B of the same open PR. Locks store reviewed head SHA, hard-stop and
mark_ready/submit gates allow a fresh decision boundary when the live
head differs, and same-head duplicates remain fail-closed. Open-PR lock
cleanup stays forbidden (#594); assessment reports locked vs current
head and stale_by_head / fresh_review_on_current_head_allowed.
Closes#620
Legacy NULL-scope dedupe only compared Gitea targets, so duplicate rows
with the same provider key and issue but different fingerprint/status/
event_count/etc. collapsed to the lowest link_id and silently dropped
observation data. Migration now compares all meaningful observation
fields and refuses to discard conflicts (#619 RC3 / #613).
Address remaining PR #619 blockers on head 783ea88:
- Deduplicate legacy NULL-scope incident_links before normalizing to ''
- Fail closed when duplicate rows disagree on Gitea target
- Require non-empty expected_head_sha for PR assign and mutation
Address REQUEST_CHANGES on PR #619:
- require_valid_assignment rejects terminal work states and head drift
- incident_links scope keys normalize NULL/blank to '' for UNIQUE
- remove trailing whitespace in control-plane-db-substrate.md
Add SQLite single-writer MVP control plane per the allocator ADR:
sessions, work_items (issue/pr only), atomic assign+lease, mutation
gates, terminal-lock index, and provider-neutral incident_links.
DB coordinates concurrency; Gitea remains assignable work; raw
Sentry/GlitchTip incidents are never work items. #600 and #612 build on
this substrate.
- Distinguish client_namespace vs offline_spawn probe sources; only IDE
client probes prove namespace health and feed mutation gates.
- Record assessments in session; gate gitea_submit_pr_review and
gitea_merge_pr when client-namespace health is unhealthy/non-proven.
- Mark test_mcp_conn.py offline-only; align recovery docs to client
reconnect (no PID-kill/config-touch as canonical recovery).
- Rebase onto current master so #590 ledger isolation keeps
TestMergePR.test_unknown_profile_blocks green.
Adds docs/mcp-namespace-eof-recovery.md documenting the correct recovery
path when a Gitea MCP namespace (gitea-author/reviewer/merger/tools)
returns `client is closing: EOF`.
Satisfies acceptance criterion 7 of #543: symptom, root cause (closed
client transport vs a live-but-stale process), the sanctioned
reconnect/relaunch sequence, diagnostics to capture, and how
test_mcp_conn.py reproduces the registered-vs-callable gap. Distinguishes
this transport-close failure from the ps-based stale-runtime family in
#531/#544 and reinforces the no-direct-import guard (#558).
Docs-only; no code or test behavior changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The namespace health check (05fdcee) classified a false-ready namespace but
only returned an advisory blocks_merge_workflow flag; nothing in the merge
gate consumed it. Wire it in so a broken live namespace hard-blocks merge.
- review_merge_state_machine.assess_workflow_blockers: add live_namespace_broken
blocker (registered-in-FastMCP but not callable-through-namespace).
- assess_state_advancement: forward **blocker_kwargs so can_approve/can_merge/
workflow_status honor the full blocker set (also fixes latent drop of
mcp_reconnect_failed/stale_capability_state through those paths).
- gitea_assess_review_merge_state_machine tool: accept live_namespace_broken and
thread it through all state-machine calls.
- Tests: prove can_merge/workflow_status/tool block on live_namespace_broken even
with every review state + pre-merge gate satisfied; bridge classify verdict.
- Docs: enforcement section wiring blocks_merge_workflow -> live_namespace_broken.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
patch.dict(os.environ, clear=True) also drops PYTEST_CURRENT_TEST, which
re-enables live MCP process health checks against operator daemons and
makes merge-gate unit tests environment-dependent. Stub the diagnostic
in conftest so unit tests stay isolated.