feat(auto-agents): deterministic compliance short-circuit + push-substrate fixes

Three blocker classes surfaced by the 2026-05-13 4-hour live pipeline
test (PR #30 / #28 / #25), all fixed here:

1. `_pr_clone` left `remote.origin.mirror=true` on the bare mirror,
   making every `git push --force-with-lease origin <refspec>` fail
   with `fatal: --mirror can't be combined with refspecs`. Worktrees
   inherited the bad config; worker had to discover + work around it
   each cycle, and one cycle's fragile recovery LOST a real commit.
   `_disable_mirror_push_semantics` clears the flag at clone time
   and on every refresh.

2. Predicate treated `outcome=resolved + head_sha_advanced=False` as
   transport-class (UNKNOWN bucket → wasted same-tier retry). It's
   actually competence — the worker emitted a complete-looking JSON
   while delivering nothing. Now escalates immediately.

3. The dispatcher's own deterministic sections (compliance_gaps +
   gate_preflight) misled the worker into emitting `resolved`
   whenever both were clean — regardless of real remote CI state.
   New module `_implementer_compliance_apply` + dispatcher hook
   `_maybe_short_circuit` move trivial compliance fixes (CONTRIBUTORS
   line, CHANGELOG stub from PR title, ISSUES CLOSED footer from
   prefetched linked_issues) onto the dispatcher's deterministic
   side per the auto-agents.md policy: "deterministic Python owns
   orchestration; the LLM only handles the actual creative work."

Two short-circuit paths:
- P0 (always-on): compliance clean + preflight clean + remote CI =
  success → skip LLM, emit `no_changes_needed`. Eliminates the
  hallucination class observed live.
- A (opt-in via IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE=1):
  compliance has only fixable gaps + preflight clean + CI failing →
  dispatcher applies fixes, pushes, emits `resolved`. Worker
  reserved for code-bug PRs only.

Plus supporting changes:
- gate_preflight payload includes `remote_ci_state` + `diverges_from_remote_ci`
- Telemetry rows gain `outcome_disputed=True` when resolved+no-push
- `EscalationAction.SUCCESS` accepts `no_changes_needed` outcome
- task-implementor.md procedure re-ordered: read `--field ci` FIRST
- Runtime hook `_dispatch_runtime._maybe_read_short_circuit` consumes
  the dispatcher's `_short_circuit_result` stash and synthesizes a
  SessionResult instead of spawning the LLM session
- Cycle archive's post_session_result gains `auto_fix_report` +
  `short_circuit` fields

Tier B (perf) and Tier C (observability) deferred to follow-up.
Tests: 1464 passed, 3 skipped (+23 from 6315892e).

ISSUES CLOSED: #30 #28

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 07:50:00 -04:00
parent 6315892eb8
commit 49a28b5cf5
13 changed files with 2045 additions and 31 deletions
+18 -11
View File
@@ -397,27 +397,34 @@ This is a **performance** change, not a correctness change: the pre-fetched data
#### Procedure: `pr_fix` (PR Fix)
1. **Read the PR.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field description` (apply the three-case contract from step 0b). Then `… --field metadata` for `head_sha` / `head_ref` / `base_ref` / `data_complete`. Set `branch_name = head_ref` from the metadata JSON. If BOTH return empty stdout, fall through to the legacy GET on `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls/{work_number}`.
**CI-first principle (P6 / 2026-05-13):** the FIRST thing to check is what's actually broken on remote CI. Reading compliance/preflight sections before knowing what's failing tempts the worker into the trap of "compliance is clean → emit resolved" — but compliance being clean is only meaningful when there's no failing code-test. Always read `--field ci` first and let the failing-check list drive everything else.
2. **Read all reviews.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field reviews` (apply the three-case contract — `[]\n` = no active REQUEST_CHANGES reviews, proceed; populated JSON list = use it with per-review inline comments pre-paginated; empty = fall through). If empty, fall through to the legacy paginated GET on `/pulls/{work_number}/reviews?limit=50&page=N` and then per-review comments.
1. **Read the CI failure picture FIRST.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field ci`. The returned JSON has `{status, checks[], status_completed, detail_completed}` whenever the sentinel exists. Each entry in `checks[]` carries the `target_url` and (when the dispatcher could fetch it) the failure-log excerpt — use that. If `status_completed` or `detail_completed` is `false`, the dispatcher's fetch failed for that slice and you should fall through to the legacy GET on `/commits/{head_sha}/statuses?limit=50&page=N` and per-`target_url` webfetch. If stdout is empty entirely, the sentinel doesn't exist (opt-out) — also fall through. **Identify the failing check names** before going further. If `status == "success"` and you got here anyway, something is unusual — read the rest of the sentinel sections to confirm what state the PR is in.
3. **Read all PR comments.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field comments` (apply the three-case contract). For `pr_fix` / `request_changes_pr` work this returns `pr_comments`; for `issue_impl` it returns `issue_comments` — the script dispatches on `work_type` for you. If empty stdout, fall through to the legacy paginated GET on `/issues/{work_number}/comments?limit=50&page=N`.
2. **Read the deterministic check sections.** Run `--field compliance_gaps` and `--field gate_preflight`. Cross-reference against your step 1 CI picture:
- If `gate_preflight.diverges_from_remote_ci == true`, the local `--fast` gate says PASS but remote CI says failure — the failing CI job is something `--fast` doesn't run (e.g. `e2e_tests`, `coverage`). Trust the specific failing checks from step 1; **do NOT trust "preflight clean" alone**.
- If `compliance_gaps.gaps_open_count > 0`, there's metadata to fill in. But check first whether the failing CI is metadata-related (e.g. a `commit-message-lint` check) or about code (e.g. `unit_tests`). If the latter, fix the code first; compliance metadata is step-2 work.
- If `compliance_gaps.masked_checks` is non-empty, do NOT trust the "all gaps closed" verdict even if every value in `gaps` is `true`. The masked checks are unverified — re-run `git status` / `git log -1` in-session before deciding.
4. **Fetch CI failure details.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field ci`. The returned JSON has `{status, checks[], status_completed, detail_completed}` whenever the sentinel exists. Each entry in `checks[]` carries the `target_url` and (when the dispatcher could fetch it) the failure-log excerpt — use that. If `status_completed` or `detail_completed` is `false` in the returned JSON, the dispatcher's fetch failed for that slice and you should fall through to the legacy GET on `/commits/{head_sha}/statuses?limit=50&page=N` and per-`target_url` webfetch. If stdout is empty entirely, the sentinel doesn't exist (opt-out) — also fall through.
3. **Read the PR.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field description` (apply the three-case contract from step 0b). Then `… --field metadata` for `head_sha` / `head_ref` / `base_ref` / `data_complete`. Set `branch_name = head_ref` from the metadata JSON. If BOTH return empty stdout, fall through to the legacy GET on `{forgejo_url}/api/v1/repos/{forgejo_owner}/{forgejo_repo}/pulls/{work_number}`.
5. **Create isolated clone.** Run `python3 /tmp/local_tools/tools/implementer_workspace.py discover --pr {work_number}`. If stdout's `repo_dir=` line carries a non-empty path, that IS your `{repo_dir}` for steps 6+ — the dispatcher has already cloned the PR's head branch at `head_sha` and checked it out for you. **Skip the `git-isolator-util` call entirely.** Only if `repo_dir=` is empty, call `git-isolator-util` with `create_branch: false` and `branch: {branch_name}` (the PR's head branch, resolved in step 1).
4. **Read all reviews.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field reviews` (apply the three-case contract — `[]\n` = no active REQUEST_CHANGES reviews, proceed; populated JSON list = use it with per-review inline comments pre-paginated; empty = fall through). If empty, fall through to the legacy paginated GET on `/pulls/{work_number}/reviews?limit=50&page=N` and then per-review comments.
6. **Fix the issues.** Address all CI failures and all unresolved reviewer feedback. Never partially address reviewer comments — every `REQUEST_CHANGES` concern must be fully resolved.
5. **Read all PR comments.** Run `python3 /tmp/local_tools/tools/implementer_pr_context.py read --pr {work_number} --field comments` (apply the three-case contract). For `pr_fix` / `request_changes_pr` work this returns `pr_comments`; for `issue_impl` it returns `issue_comments` — the script dispatches on `work_type` for you. If empty stdout, fall through to the legacy paginated GET on `/issues/{work_number}/comments?limit=50&page=N`.
7. **Run quality gates locally** (same 6 gates as above). All must pass before pushing. Fix and re-run as many times as needed.
6. **Create isolated clone.** Run `python3 /tmp/local_tools/tools/implementer_workspace.py discover --pr {work_number}`. If stdout's `repo_dir=` line carries a non-empty path, that IS your `{repo_dir}` for steps 7+ — the dispatcher has already cloned the PR's head branch at `head_sha` and checked it out for you. **Skip the `git-isolator-util` call entirely.** Only if `repo_dir=` is empty, call `git-isolator-util` with `create_branch: false` and `branch: {branch_name}` (the PR's head branch, resolved in step 3).
8. **Commit and push.** Call `git-commit-util` with `force_push_with_lease` operation (see Subagents section).
7. **Fix the issues.** Address all CI failures and all unresolved reviewer feedback identified in steps 1-5. Never partially address reviewer comments — every `REQUEST_CHANGES` concern must be fully resolved. Anchor on the SPECIFIC failing-check names from step 1 — if you can't trace your fix back to one of those checks, you're probably not addressing what's actually broken.
9. **Post attempt comment** on the PR (see "Attempt Comments" section below).
8. **Run quality gates locally** (same 6 gates as above). All must pass before pushing. Fix and re-run as many times as needed.
10. **Clean up.** `rm -rf {repo_dir}`
9. **Commit and push.** Call `git-commit-util` with `force_push_with_lease` operation (see Subagents section).
11. **Exit.**
10. **Post attempt comment** on the PR (see "Attempt Comments" section below).
11. **Clean up.** `rm -rf {repo_dir}`
12. **Exit.**
### Attempt Comments
+82
View File
@@ -7,6 +7,88 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- **Deterministic compliance-fix short-circuit + post-live-test
hardening (2026-05-13, follow-up to the live 4-hour pipeline test
on PR #30 / #28 / #25 / #26 / #20).** The live test exposed three
blocker classes: a `remote.origin.mirror=true` clone-time bug that
made every worker push fail; a predicate gap where the worker's
self-reported `resolved` + no-push fell into the UNKNOWN bucket
and wasted retries; and the deepest issue — the worker faithfully
followed the new sentinel-routed deterministic-section
instructions to emit `resolved` whenever compliance was clean and
preflight was clean, regardless of remote CI state. This commit
fixes all three plus moves trivial compliance fixes off the LLM
entirely.
- **`_pr_clone._disable_mirror_push_semantics`** clears
`remote.origin.mirror=true` on the bare mirror at clone time
AND on every refresh (idempotent). Worktrees derived from the
mirror no longer need the worker's per-command workaround.
- **Predicate change** (`_implementer_escalation.decide`):
`outcome=resolved` + `head_sha_advanced=False` now escalates
immediately rather than burning a same-tier retry on the
UNKNOWN bucket. Worker hallucinated-success cases (3 of 5
PR #30 attempts in the live test) are now competence-class.
- **`outcome_disputed` telemetry field** (`_phase4_telemetry`):
when worker emits `resolved` but no push happened, the row
records `outcome_disputed=True` so analysts can grep for the
hallucination rate directly without re-deriving from
outcome + head_sha_advanced.
- **Preflight ↔ remote CI divergence detection**
(`_compute_deterministic_sections`): when local `--fast` says
all gates pass but remote CI is failing, the gate_preflight
payload includes `diverges_from_remote_ci=True` plus the
actual remote state. Worker docs (task-implementor.md P6)
instruct reading `--field ci` FIRST and not trusting preflight
alone when divergence is flagged.
- **`_implementer_compliance_apply` module + dispatcher short-
circuit** (`_maybe_short_circuit` in `dispatch_implementer`).
Two skip paths:
1. **P0 (skip-when-green):** compliance clean + preflight
clean + masked_checks empty + remote CI = success → emit
synthetic `{"outcome": "no_changes_needed",
"_dispatcher_short_circuit": "P0"}`, skip LLM session,
release claim. Always active when escalation is enabled
(no flag).
2. **A (auto-fix-compliance):** when only compliance gaps
remain, preflight is clean, no masked checks, and
`IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE=1` is set
(default OFF), the dispatcher applies fixes deterministically
— CONTRIBUTORS line, CHANGELOG stub (from PR title),
ISSUES CLOSED footer (from prefetched linked_issues) — then
pushes the commit and emits synthetic `resolved`. Per
`.opencode/agents/auto-agents.md`: "deterministic Python
owns orchestration; the LLM only handles the actual
creative work." Compliance metadata is not creative work.
Both paths thread through a new runtime hook
(`_dispatch_runtime._maybe_read_short_circuit`) that consumes
a `_short_circuit_result` stash on the item's context and
synthesizes a SessionResult instead of spawning the LLM
session. Cycle archive's `post_session_result` gains
`auto_fix_report` + `short_circuit` fields for audit trail.
`EscalationAction.SUCCESS` accepts `outcome=no_changes_needed`
without requiring head_sha_advanced=True.
- **New env flag:** `IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE`
(default OFF). Setting to `1` grants the dispatcher commit
+ push authority for the three deterministic compliance
classes. Off-by-default because it's a real responsibility
expansion. Governance precedent: `.opencode/agents/auto-agents.md`
explicitly endorses moving non-creative work off the LLM.
Test suite: 1464 passed, 3 skipped (+23 from previous commit's
1441). New tests cover all branches of the new module,
short-circuit hook, predicate change, telemetry field,
preflight divergence detection, and the runtime
consume-stash helper.
- **Deterministic worker-side improvements (2026-05-13).** Four
changes that move judgment off the LLM and into the dispatcher,
motivated by the live PR #30 escalation pilot (2026-05-12) where
@@ -0,0 +1,319 @@
"""Unit tests for ``tools/_implementer_compliance_apply.py``.
Each apply function operates on a real worktree (via ``tmp_path`` +
the conftest ``minimal_git_repo`` fixture). The tests pin the
contract Drew agreed to in the 2026-05-13 plan:
- Idempotent: applying twice doesn't double-write or fail.
- Audit-trail: each ApplyResult lists the operations it performed.
- Failure-safe: any subprocess error is recorded in ``result.error``
and the caller falls through (no partial commit).
The module's design splits "stage edits" from "commit" from "amend
footer" so the dispatcher's short-circuit logic can compose them
flexibly. Most tests exercise individual gap appliers directly; a
handful exercise the top-level ``apply_compliance_fixes``
orchestrator end-to-end.
"""
from __future__ import annotations
import subprocess
from pathlib import Path
import pytest
from .conftest import load_tool_module
@pytest.fixture
def applier():
return load_tool_module("_implementer_compliance_apply")
@pytest.fixture
def repo_with_issues_footer(minimal_git_repo):
"""A clean repo whose HEAD commit already has the ISSUES CLOSED
footer — exercises the idempotent path of the amender."""
import subprocess
subprocess.run(
["git", "-C", str(minimal_git_repo),
"commit", "--amend", "-m",
"fix: thing\n\nISSUES CLOSED: #42"],
check=True, capture_output=True,
)
return minimal_git_repo
class TestApplyContributors:
def test_creates_file_when_absent(self, applier, minimal_git_repo):
contrib = minimal_git_repo / "CONTRIBUTORS.md"
assert not contrib.exists()
result = applier._apply_contributors(
minimal_git_repo, "HAL 9000", "hal9000@cleverthis.com",
)
assert result.applied is True
assert result.error is None
assert contrib.exists()
text = contrib.read_text(encoding="utf-8")
assert "HAL 9000 <hal9000@cleverthis.com>" in text
def test_appends_when_email_absent(self, applier, minimal_git_repo):
contrib = minimal_git_repo / "CONTRIBUTORS.md"
contrib.write_text(
"# Contributors\n\n- Existing Person <other@example.com>\n",
encoding="utf-8",
)
result = applier._apply_contributors(
minimal_git_repo, "HAL 9000", "hal9000@cleverthis.com",
)
assert result.applied is True
text = contrib.read_text(encoding="utf-8")
# Both the existing and the new entry should be present
assert "other@example.com" in text
assert "hal9000@cleverthis.com" in text
def test_idempotent_when_email_already_present(self, applier, minimal_git_repo):
contrib = minimal_git_repo / "CONTRIBUTORS.md"
original = "# Contributors\n\n- HAL 9000 <hal9000@cleverthis.com>\n"
contrib.write_text(original, encoding="utf-8")
result = applier._apply_contributors(
minimal_git_repo, "HAL 9000", "hal9000@cleverthis.com",
)
assert result.applied is True
# File should be unchanged
assert contrib.read_text(encoding="utf-8") == original
def test_case_insensitive_email_match(self, applier, minimal_git_repo):
contrib = minimal_git_repo / "CONTRIBUTORS.md"
contrib.write_text(
"- HAL 9000 <Hal9000@CleverThis.com>\n", encoding="utf-8",
)
result = applier._apply_contributors(
minimal_git_repo, "HAL 9000", "hal9000@cleverthis.com",
)
assert result.applied is True
# Should NOT duplicate — case-insensitive idempotent
text = contrib.read_text(encoding="utf-8")
assert text.count("hal9000@cleverthis.com") <= 1
assert text.count("Hal9000@CleverThis.com") <= 1
class TestApplyChangelog:
def test_creates_file_when_absent(self, applier, minimal_git_repo):
result = applier._apply_changelog(
minimal_git_repo, "fix(tui): extract @token correctly", 42,
)
assert result.applied is True
text = (minimal_git_repo / "CHANGELOG.md").read_text(encoding="utf-8")
assert "## [Unreleased]" in text
assert "fix(tui): extract @token correctly (#42)" in text
def test_inserts_bullet_under_unreleased(self, applier, minimal_git_repo):
changelog = minimal_git_repo / "CHANGELOG.md"
changelog.write_text(
"# Changelog\n\n## [Unreleased]\n\n## [1.0.0]\n- old entry\n",
encoding="utf-8",
)
result = applier._apply_changelog(
minimal_git_repo, "fix(plan): handle empty state", 99,
)
assert result.applied is True
text = changelog.read_text(encoding="utf-8")
# The new bullet should land BETWEEN [Unreleased] and [1.0.0]
unreleased_idx = text.find("[Unreleased]")
v1_idx = text.find("[1.0.0]")
bullet_idx = text.find("fix(plan): handle empty state (#99)")
assert unreleased_idx < bullet_idx < v1_idx
def test_idempotent_when_pr_number_already_present(self, applier, minimal_git_repo):
changelog = minimal_git_repo / "CHANGELOG.md"
original = (
"# Changelog\n\n## [Unreleased]\n"
"- fix(tui): extract @token correctly (#42)\n"
)
changelog.write_text(original, encoding="utf-8")
result = applier._apply_changelog(
minimal_git_repo, "fix(tui): something else entirely", 42,
)
assert result.applied is True
# File unchanged
assert changelog.read_text(encoding="utf-8") == original
class TestAmendCommitFooter:
def test_adds_footer_when_missing(self, applier, minimal_git_repo):
result = applier._amend_commit_footer(
minimal_git_repo, "HAL 9000", "hal9000@cleverthis.com", [42, 43],
)
assert result.applied is True
assert result.error is None
# Re-read HEAD message
proc = subprocess.run(
["git", "-C", str(minimal_git_repo),
"log", "-1", "--pretty=%B", "HEAD"],
capture_output=True, text=True, check=True,
)
assert "ISSUES CLOSED: #42 #43" in proc.stdout
def test_idempotent_when_footer_present(self, applier, repo_with_issues_footer):
result = applier._amend_commit_footer(
repo_with_issues_footer, "HAL 9000", "hal9000@cleverthis.com", [42],
)
assert result.applied is True
# The "operations" record should signal idempotent path
op_outputs = " ".join(out for _, out in result.operations)
assert "already present" in op_outputs.lower()
def test_returns_error_when_linked_issues_empty(self, applier, minimal_git_repo):
result = applier._amend_commit_footer(
minimal_git_repo, "HAL 9000", "hal9000@cleverthis.com", [],
)
assert result.applied is False
assert result.error is not None
assert "linked_issue_numbers" in result.error
class TestApplyComplianceFixesOrchestrator:
def test_all_gaps_closed_no_op(self, applier, minimal_git_repo):
gaps = {
"worktree_clean": True,
"changelog_unreleased_nonempty": True,
"contributors_has_author": True,
"commit_has_issues_closed": True,
}
report = applier.apply_compliance_fixes(
minimal_git_repo, gaps,
git_user_name="HAL 9000", git_user_email="hal9000@cleverthis.com",
pr_title="fix: thing", pr_number=42,
linked_issue_numbers=[42],
)
# Nothing False → nothing to fix → no commits
assert report.all_applied is True
assert report.any_committed is False
assert report.per_gap == []
def test_three_gaps_closed_one_commit(self, applier, minimal_git_repo):
# contributors + changelog gaps → both filled and committed
# together; commit_has_issues_closed handled by the same
# commit's message (we embed the footer into the staged
# commit message rather than amending).
gaps = {
"worktree_clean": True,
"changelog_unreleased_nonempty": False,
"contributors_has_author": False,
"commit_has_issues_closed": False,
}
report = applier.apply_compliance_fixes(
minimal_git_repo, gaps,
git_user_name="HAL 9000", git_user_email="hal9000@cleverthis.com",
pr_title="fix(tui): @token extraction", pr_number=42,
linked_issue_numbers=[42],
)
assert report.all_applied is True
assert report.any_committed is True
# New HEAD message should have ISSUES CLOSED footer
proc = subprocess.run(
["git", "-C", str(minimal_git_repo),
"log", "-1", "--pretty=%B", "HEAD"],
capture_output=True, text=True, check=True,
)
assert "ISSUES CLOSED: #42" in proc.stdout
# Both files should exist
assert (minimal_git_repo / "CHANGELOG.md").exists()
assert (minimal_git_repo / "CONTRIBUTORS.md").exists()
def test_only_footer_gap_amends_existing_commit(self, applier, minimal_git_repo):
# File-level gaps are already satisfied; only the commit
# footer needs to be amended onto the existing HEAD.
(minimal_git_repo / "CONTRIBUTORS.md").write_text(
"- HAL 9000 <hal9000@cleverthis.com>\n",
)
(minimal_git_repo / "CHANGELOG.md").write_text(
"## [Unreleased]\n- existing entry\n",
)
original_head = applier._rev_parse_head(minimal_git_repo)
gaps = {
"worktree_clean": True,
"changelog_unreleased_nonempty": True,
"contributors_has_author": True,
"commit_has_issues_closed": False,
}
report = applier.apply_compliance_fixes(
minimal_git_repo, gaps,
git_user_name="HAL 9000", git_user_email="hal9000@cleverthis.com",
pr_title="fix: thing", pr_number=42,
linked_issue_numbers=[42],
)
assert report.all_applied is True
assert report.any_committed is True
new_head = applier._rev_parse_head(minimal_git_repo)
# Amend creates a new commit (different SHA) but the work
# graph stays at one commit on top of where we started.
assert new_head != original_head
proc = subprocess.run(
["git", "-C", str(minimal_git_repo),
"log", "-1", "--pretty=%B", "HEAD"],
capture_output=True, text=True, check=True,
)
assert "ISSUES CLOSED: #42" in proc.stdout
def test_footer_gap_without_linked_issues_returns_error(
self, applier, minimal_git_repo,
):
# The footer fix needs at least one linked issue. Without
# it, the orchestrator records the gap as un-fixable and
# ``all_applied`` is False so the caller falls through
# to the LLM worker.
gaps = {
"worktree_clean": True,
"changelog_unreleased_nonempty": True,
"contributors_has_author": True,
"commit_has_issues_closed": False,
}
report = applier.apply_compliance_fixes(
minimal_git_repo, gaps,
git_user_name="HAL 9000", git_user_email="hal9000@cleverthis.com",
pr_title="fix: thing", pr_number=42,
linked_issue_numbers=[],
)
assert report.all_applied is False
# The single per-gap entry should carry the error
assert any(
r.gap_name == "commit_has_issues_closed" and r.error
for r in report.per_gap
)
def test_idempotent_full_run(self, applier, minimal_git_repo):
# Run the orchestrator twice with the same gaps. The second
# run should detect that the prior fix is already there and
# produce no new commits.
gaps = {
"worktree_clean": True,
"changelog_unreleased_nonempty": False,
"contributors_has_author": False,
"commit_has_issues_closed": False,
}
first = applier.apply_compliance_fixes(
minimal_git_repo, gaps,
git_user_name="HAL 9000", git_user_email="hal9000@cleverthis.com",
pr_title="fix: thing", pr_number=42,
linked_issue_numbers=[42],
)
assert first.any_committed is True
# Update gaps to reflect post-fix state (compliance scan
# would now show all True). Re-run with no-op gaps.
second_gaps = {
"worktree_clean": True,
"changelog_unreleased_nonempty": True,
"contributors_has_author": True,
"commit_has_issues_closed": True,
}
second = applier.apply_compliance_fixes(
minimal_git_repo, second_gaps,
git_user_name="HAL 9000", git_user_email="hal9000@cleverthis.com",
pr_title="fix: thing", pr_number=42,
linked_issue_numbers=[42],
)
assert second.any_committed is False
assert second.per_gap == []
@@ -104,10 +104,13 @@ class TestHeadShaTriState:
max_tier=1,
) == esc.EscalationAction.RETRY_POST_FETCH
def test_resolved_plus_false_is_not_success(self, esc):
# Worker claims resolved but verified did NOT push. Falls
# through to competence classification — escalate on first
# attempt (transport budget 0 for competence).
def test_resolved_plus_false_escalates_immediately(self, esc):
# Worker claims resolved but verified did NOT push. This is
# competence-class — same model on same input will hallucinate
# success again. Predicate must skip the same-tier retry
# budget entirely and escalate. (A2 fix, 2026-05-13: prior
# behaviour was UNKNOWN-bucket retry_same_tier; observed live
# 3x and wasted ~25 min of cycle time per occurrence.)
action = esc.decide(
parsed_json={"outcome": "resolved"},
terminal_state="completed",
@@ -115,9 +118,58 @@ class TestHeadShaTriState:
pr_state="open",
transport_retries_used=0,
current_tier=0,
max_tier=1,
max_tier=2,
)
assert action != esc.EscalationAction.SUCCESS
assert action == esc.EscalationAction.ESCALATE, (
f"resolved + no-push must escalate, got {action}"
)
def test_resolved_plus_false_at_max_tier_is_exhausted(self, esc):
# Same hallucination but already at the ceiling — there's
# no higher tier to escalate to.
action = esc.decide(
parsed_json={"outcome": "resolved"},
terminal_state="completed",
head_sha_advanced=False,
pr_state="open",
transport_retries_used=0,
current_tier=2,
max_tier=2,
)
assert action == esc.EscalationAction.EXHAUSTED
def test_resolved_plus_false_bypasses_transport_budget(self, esc):
# Critical: even with transport_retries_used=0 (full retry
# budget available), resolved+no-push must NOT consume that
# budget. It's a competence failure, not a flake.
action = esc.decide(
parsed_json={"outcome": "resolved"},
terminal_state="completed",
head_sha_advanced=False,
pr_state="open",
transport_retries_used=0,
current_tier=1,
max_tier=2,
)
assert action == esc.EscalationAction.ESCALATE
assert action != esc.EscalationAction.RETRY_SAME_TIER
def test_no_changes_needed_is_success_without_push(self, esc):
# Commit 2 / P0 (2026-05-13): the dispatcher's short-circuit
# path emits ``outcome=no_changes_needed`` when it determined
# before the LLM that no work was required. By definition
# head_sha didn't advance — there was nothing to push. The
# predicate must treat this as SUCCESS rather than competence.
action = esc.decide(
parsed_json={"outcome": "no_changes_needed"},
terminal_state="completed",
head_sha_advanced=False,
pr_state="open",
transport_retries_used=0,
current_tier=0,
max_tier=2,
)
assert action == esc.EscalationAction.SUCCESS
def test_none_outside_success_path_does_not_trigger_post_fetch(self, esc):
# Post-fetch retry is reserved for the success-outcome path
@@ -1281,6 +1281,82 @@ class TestDeterministicStanzas:
# suppressed — that would be just as misleading.
assert "No persistent failures across two pre-flight runs" not in out
def test_preflight_diverges_from_remote_ci_flag(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# P3 (2026-05-13): when local preflight reports all gates
# PASS but the dispatcher's prefetched remote ci_status says
# "failure", the section payload MUST include
# ``diverges_from_remote_ci=True``. Without this signal the
# worker reads "preflight clean" and trusts it, missing the
# failing remote-only gate (e.g. e2e_tests, which --fast
# skips). Live failure mode from PR #30 attempt 1 / PR #28
# cycle 2.
tmp_path = minimal_git_repo
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
monkeypatch.setenv("IMPLEMENTER_DISPATCHER_GATE_PREFLIGHT", "1")
# Stub run_preflight: return all-clean classification
monkeypatch.setattr(
driver._implementer_gate_preflight,
"run_preflight",
lambda *a, **k: {
"preflight_enabled": True,
"gate_statuses": {"unit_tests": "PASS", "lint": "PASS"},
"failures_total": 0,
"failures_related_to_diff": 0,
"failures_unrelated_to_diff": 0,
"related": [],
"unrelated": [],
"runs": [{"run": 1, "returncode": 0, "failures": 0}],
},
)
item = _pr_item()
# Remote CI = failure
result = type(
"FakeResult", (), {"diff": "", "ci_status": {"state": "failure"}},
)()
handle = type("FakeHandle", (), {"path": str(tmp_path)})()
det = driver._compute_deterministic_sections(
cfg, item, _pr_group(driver), result, handle,
)
assert "gate_preflight" in det
assert det["gate_preflight"]["remote_ci_state"] == "failure"
assert det["gate_preflight"]["diverges_from_remote_ci"] is True
def test_preflight_no_divergence_when_ci_passes(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# Mirror positive: preflight clean + remote CI success →
# no divergence flag (or explicit False). The worker can
# trust both signals.
tmp_path = minimal_git_repo
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
monkeypatch.setenv("IMPLEMENTER_DISPATCHER_GATE_PREFLIGHT", "1")
monkeypatch.setattr(
driver._implementer_gate_preflight,
"run_preflight",
lambda *a, **k: {
"preflight_enabled": True,
"gate_statuses": {"unit_tests": "PASS"},
"failures_total": 0,
"failures_related_to_diff": 0,
"failures_unrelated_to_diff": 0,
"related": [],
"unrelated": [],
"runs": [{"run": 1, "returncode": 0, "failures": 0}],
},
)
item = _pr_item()
result = type(
"FakeResult", (), {"diff": "", "ci_status": {"state": "success"}},
)()
handle = type("FakeHandle", (), {"path": str(tmp_path)})()
det = driver._compute_deterministic_sections(
cfg, item, _pr_group(driver), result, handle,
)
assert det["gate_preflight"]["remote_ci_state"] == "success"
assert det["gate_preflight"]["diverges_from_remote_ci"] is False
def test_compliance_gaps_kill_switch_disables_compliance_section(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
@@ -1376,6 +1452,287 @@ def _pr_group(driver):
return next(g for g in driver.WORK_GROUPS if g.name == "failing_ci_pr")
class TestShortCircuit:
"""Commit 2 (2026-05-13): dispatcher-side P0 (skip-when-green)
and A (auto-fix-compliance) eliminate the LLM call when the
deterministic state can satisfy the cycle alone. Each test
builds a context dict + verifies the short-circuit stash AND
(where relevant) that the runtime helper consumes it correctly.
The short-circuit machinery lives in two parts:
- ``dispatch_implementer._maybe_short_circuit`` decides + stashes
- ``_dispatch_runtime._maybe_read_short_circuit`` consumes
These tests drive both halves so a refactor to either side
breaks at least one test.
"""
def test_p0_skip_when_all_green(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# All four signals say "done": compliance clean, preflight
# clean, no masked checks, remote CI success. Dispatcher must
# stash a no_changes_needed result and skip the worker.
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
item = _pr_item()
context = {
"result": type("FakeResult", (), {
"ci_status": {"state": "success"},
"linked_issues": [],
"pr_details": {
"title": "fix: thing",
"head": {"ref": "tests/x"},
},
})(),
"clone_handle": type(
"FakeHandle", (), {"path": str(minimal_git_repo)},
)(),
"_deterministic_sections": {
"compliance_gaps": {
"gaps": {k: True for k in (
"worktree_clean",
"changelog_unreleased_nonempty",
"contributors_has_author",
"commit_has_issues_closed",
)},
"masked_checks": [],
},
"gate_preflight": {
"preflight_enabled": True,
"failures_total": 0,
"preflight_timeout": False,
},
},
}
item["_dispatcher_implementer_context"] = context
driver._maybe_short_circuit(cfg, item, context)
stash = context.get("_short_circuit_result")
assert stash is not None
assert stash["status"] == "completed"
assert stash["parsed_json"]["outcome"] == "no_changes_needed"
assert stash["parsed_json"]["_dispatcher_short_circuit"] == "P0"
def test_p0_does_not_fire_when_ci_failing(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# Compliance clean + preflight clean BUT remote CI failure
# → there's still real code work to do. Dispatch the worker.
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
item = _pr_item()
context = {
"result": type("FakeResult", (), {
"ci_status": {"state": "failure"},
"linked_issues": [],
"pr_details": {
"title": "fix: thing",
"head": {"ref": "tests/x"},
},
})(),
"clone_handle": type(
"FakeHandle", (), {"path": str(minimal_git_repo)},
)(),
"_deterministic_sections": {
"compliance_gaps": {
"gaps": {k: True for k in (
"worktree_clean",
"changelog_unreleased_nonempty",
"contributors_has_author",
"commit_has_issues_closed",
)},
"masked_checks": [],
},
"gate_preflight": {
"preflight_enabled": True,
"failures_total": 0,
"preflight_timeout": False,
},
},
}
item["_dispatcher_implementer_context"] = context
driver._maybe_short_circuit(cfg, item, context)
# No stash → dispatcher will fall through to LLM worker
assert context.get("_short_circuit_result") is None
def test_auto_fix_disabled_by_default(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# Auto-fix flag OFF: compliance has gaps but dispatcher
# does NOT auto-apply; falls through to worker.
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
monkeypatch.delenv(
"IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE", raising=False,
)
item = _pr_item()
context = {
"result": type("FakeResult", (), {
"ci_status": {"state": "failure"},
"linked_issues": [{"number": 42}],
"pr_details": {
"title": "fix: thing",
"head": {"ref": "tests/x"},
},
})(),
"clone_handle": type(
"FakeHandle", (), {"path": str(minimal_git_repo)},
)(),
"_deterministic_sections": {
"compliance_gaps": {
"gaps": {
"worktree_clean": True,
"changelog_unreleased_nonempty": False,
"contributors_has_author": False,
"commit_has_issues_closed": False,
},
"masked_checks": [],
},
"gate_preflight": {
"preflight_enabled": True,
"failures_total": 0,
"preflight_timeout": False,
},
},
}
item["_dispatcher_implementer_context"] = context
driver._maybe_short_circuit(cfg, item, context)
assert context.get("_short_circuit_result") is None
def test_auto_fix_applies_when_flag_on(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# Flag ON + compliance gaps + preflight clean + CI failure
# → dispatcher applies fixes and stashes a resolved result.
#
# We monkeypatch the apply module's push helper so the test
# doesn't actually try to talk to Forgejo. The apply itself
# writes real files + makes a real commit in tmp_path.
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
monkeypatch.setenv(
"IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE", "1",
)
# Stub push to succeed without hitting Forgejo
monkeypatch.setattr(
driver._implementer_compliance_apply,
"push_branch",
lambda worktree, branch, **kw: (
driver._implementer_compliance_apply.ApplyResult(
gap_name="push_to_origin", applied=True,
)
),
)
item = _pr_item()
context = {
"result": type("FakeResult", (), {
"ci_status": {"state": "failure"},
"linked_issues": [{"number": 42}],
"pr_details": {
"title": "fix: thing",
"head": {"ref": "tests/sentinel-bugfix"},
},
})(),
"clone_handle": type(
"FakeHandle", (), {"path": str(minimal_git_repo)},
)(),
"_deterministic_sections": {
"compliance_gaps": {
"gaps": {
"worktree_clean": True,
"changelog_unreleased_nonempty": False,
"contributors_has_author": False,
"commit_has_issues_closed": False,
},
"masked_checks": [],
},
"gate_preflight": {
"preflight_enabled": True,
"failures_total": 0,
"preflight_timeout": False,
},
},
}
item["_dispatcher_implementer_context"] = context
driver._maybe_short_circuit(cfg, item, context)
stash = context.get("_short_circuit_result")
assert stash is not None
assert stash["parsed_json"]["outcome"] == "resolved"
assert (
stash["parsed_json"]["_dispatcher_short_circuit"] == "A_auto_fix"
)
# Report stashed for cycle-archive consumers
assert "_auto_fix_report" in context
assert context["_auto_fix_report"]["any_committed"] is True
def test_auto_fix_falls_through_on_masked_checks(
self, driver, cfg, monkeypatch, minimal_git_repo,
):
# Masked checks mean git itself failed on at least one
# compliance scan — dispatcher must NOT auto-fix because
# the signal is untrusted. Falls through to worker.
monkeypatch.setenv("IMPLEMENTER_ESCALATION_ENABLED", "1")
monkeypatch.setenv(
"IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE", "1",
)
item = _pr_item()
context = {
"result": type("FakeResult", (), {
"ci_status": {"state": "failure"},
"linked_issues": [{"number": 42}],
"pr_details": {
"title": "fix: thing",
"head": {"ref": "tests/x"},
},
})(),
"clone_handle": type(
"FakeHandle", (), {"path": str(minimal_git_repo)},
)(),
"_deterministic_sections": {
"compliance_gaps": {
"gaps": {
"worktree_clean": True,
"changelog_unreleased_nonempty": False,
"contributors_has_author": False,
"commit_has_issues_closed": True,
},
"masked_checks": ["worktree_clean"],
},
"gate_preflight": {
"preflight_enabled": True,
"failures_total": 0,
"preflight_timeout": False,
},
},
}
item["_dispatcher_implementer_context"] = context
driver._maybe_short_circuit(cfg, item, context)
assert context.get("_short_circuit_result") is None
def test_runtime_consumes_short_circuit_stash(self, driver):
# The runtime helper pops the stash and marks it consumed.
runtime = load_tool_module("_dispatch_runtime", fresh=True)
item = {
"_dispatcher_implementer_context": {
"_short_circuit_result": {
"status": "completed",
"raw_response": "stub",
"parsed_json": {"outcome": "no_changes_needed"},
},
},
}
session = runtime._maybe_read_short_circuit(item)
assert session is not None
assert session.status == "completed"
assert session.parsed_json["outcome"] == "no_changes_needed"
# Stash popped, consumed marker set
ctx = item["_dispatcher_implementer_context"]
assert "_short_circuit_result" not in ctx
assert ctx.get("_short_circuit_consumed") is True
def test_runtime_returns_none_when_no_stash(self, driver):
runtime = load_tool_module("_dispatch_runtime", fresh=True)
item = {"_dispatcher_implementer_context": {}}
session = runtime._maybe_read_short_circuit(item)
assert session is None
class TestOutcomeSynthesis:
"""``_synthesize_outcome_if_missing`` post-processes worker
output so a worker that gave up without emitting the contract
@@ -393,6 +393,95 @@ class TestEscalationExtras:
assert row["outcome_synthesised"] is False
class TestOutcomeDisputed:
"""P4 (2026-05-13): worker emits ``outcome=resolved`` but the
post-session head_sha didn't advance. The dispatcher's predicate
(A2) routes this to ESCALATE; the telemetry row separately
records ``outcome_disputed=True`` so analysts can grep for the
worker-hallucination rate without re-deriving from outcome +
head_sha_advanced."""
def test_resolved_with_no_push_sets_outcome_disputed(self, telemetry):
row = telemetry.extract_phase4_telemetry(
cycle_id="cycle-1",
pr_number=30,
work_group="failing_ci_pr",
start_ts="t0",
end_ts="t1",
wall_clock_seconds=420.0,
parsed_json={"outcome": "resolved", "files_touched": []},
raw_response="",
terminal_state="completed",
pre_session_head_sha="deadbeefcafe",
post_session_head_sha="deadbeefcafe", # same as pre → no advance
)
assert row["outcome"] == "resolved"
assert row["head_sha_advanced"] is False
assert row.get("outcome_disputed") is True
def test_resolved_with_push_omits_outcome_disputed(self, telemetry):
# Worker emitted resolved AND pushed — normal success path,
# no dispute flag.
row = telemetry.extract_phase4_telemetry(
cycle_id="cycle-1",
pr_number=30,
work_group="failing_ci_pr",
start_ts="t0",
end_ts="t1",
wall_clock_seconds=420.0,
parsed_json={"outcome": "resolved"},
raw_response="",
terminal_state="completed",
pre_session_head_sha="deadbeefcafe",
post_session_head_sha="newpushedsha",
)
assert row["outcome"] == "resolved"
assert row["head_sha_advanced"] is True
assert "outcome_disputed" not in row
def test_non_resolved_outcome_omits_outcome_disputed(self, telemetry):
# The dispute flag is specific to the "claimed success without
# delivering" failure mode. A rebase-failed outcome with no
# push is not a dispute — it's an honest failure.
row = telemetry.extract_phase4_telemetry(
cycle_id="cycle-1",
pr_number=30,
work_group="failing_ci_pr",
start_ts="t0",
end_ts="t1",
wall_clock_seconds=420.0,
parsed_json={"outcome": "rebase-failed"},
raw_response="",
terminal_state="completed",
pre_session_head_sha="deadbeefcafe",
post_session_head_sha="deadbeefcafe",
)
assert "outcome_disputed" not in row
def test_head_sha_advanced_unknown_omits_outcome_disputed(
self, telemetry,
):
# When the dispatcher couldn't determine the post-session
# head_sha (transient fetch failure), head_sha_advanced is
# None — not False. The dispute flag must NOT fire because
# we have no evidence either way.
row = telemetry.extract_phase4_telemetry(
cycle_id="cycle-1",
pr_number=30,
work_group="failing_ci_pr",
start_ts="t0",
end_ts="t1",
wall_clock_seconds=420.0,
parsed_json={"outcome": "resolved"},
raw_response="",
terminal_state="completed",
pre_session_head_sha="deadbeefcafe",
post_session_head_sha=None,
)
assert row["head_sha_advanced"] is None
assert "outcome_disputed" not in row
class TestSubagentMaxDepth:
"""``subagent_max_depth`` (added in Tier-1 R2, 2026-05-12) is
sourced from the dispatcher's SessionContext, which receives it
+57
View File
@@ -153,6 +153,63 @@ def test_prepare_pr_worktree_creates_handle_on_success(
), f"expected a worktree add, got {invocations}"
def test_prepare_pr_worktree_clears_mirror_push_semantics(
clone_mod, cfg, monkeypatch, tmp_path
):
"""A1 (2026-05-13): ``git clone --mirror`` leaves
``remote.origin.mirror=true``, which makes worktree pushes fail
with ``fatal: --mirror can't be combined with refspecs``. The
dispatcher must clear this on the bare mirror so all worktrees
derived from it can push refspec-style.
Without this fix the worker had to discover and work around the
config per cycle observed live on 2026-05-13 (PR #30 attempt 3
lost a real commit during the fragile recovery path).
"""
monkeypatch.delenv("REVIEW_DISPATCHER_DISABLE_PRECLONE", raising=False)
monkeypatch.setenv("REVIEW_DISPATCHER_MIRROR_PATH", str(tmp_path / "mirror.git"))
wt_base = tmp_path / "wt"
monkeypatch.setenv("REVIEW_DISPATCHER_WORKTREE_BASE", str(wt_base))
invocations: list[list[str]] = []
def fake_run(cmd, **kwargs):
invocations.append(list(cmd))
if cmd[:3] == ["git", "clone", "--mirror"]:
mirror = Path(cmd[-1])
mirror.mkdir(parents=True, exist_ok=True)
(mirror / "HEAD").write_text("ref: refs/heads/master\n")
class _Result:
returncode = 0
stdout = b""
stderr = b""
return _Result()
monkeypatch.setattr(clone_mod.subprocess, "run", fake_run)
clone_mod.prepare_pr_worktree(cfg, 30, "abc1234")
# The config-clearing command must have been invoked at least
# once on the bare mirror. Look for the exact shape:
# ``git --git-dir <mirror> config remote.origin.mirror false``
matching = [
c for c in invocations
if (
c[0] == "git"
and "--git-dir" in c
and "config" in c
and "remote.origin.mirror" in c
and "false" in c
)
]
assert matching, (
"expected a 'git --git-dir <mirror> config remote.origin.mirror "
"false' invocation; got commands: "
f"{[' '.join(str(p) for p in c) for c in invocations]}"
)
def test_worktree_handle_cleanup_invokes_remove_and_prune(
clone_mod, cfg, monkeypatch, tmp_path
):
+79 -9
View File
@@ -909,15 +909,32 @@ def dispatch_one(
# so a dry-run cycle (token may be a stub) is still safe.
redact_values = [cfg.token] if getattr(cfg, "token", None) else []
session_started_at = _now()
session = _opencode_worker.run_session_blocking(
server_url=cfg.server_url,
agent=group.worker_agent,
tag=tag,
prompt=prompt,
timeout_seconds=cfg.worker_timeout_seconds,
on_poll=_refresh_heartbeat,
redact_values=redact_values,
)
# Short-circuit hook: a prompt_factory MAY stash a
# ``_short_circuit_result`` on the item's
# ``_dispatcher_implementer_context`` dict to indicate the
# dispatcher has already handled this PR deterministically
# and no LLM session is needed. The value is a dict shaped
# like a :class:`_opencode_worker.SessionResult` with at
# minimum ``status``, ``parsed_json``, and ``raw_response``.
# See dispatch_implementer's ``_maybe_short_circuit_with_auto_fix``
# for the production callsite.
short_circuit = _maybe_read_short_circuit(item)
if short_circuit is not None:
session = short_circuit
logger.info(
"%s item #%s short-circuited (no LLM session): %s",
group.name, number, short_circuit.status,
)
else:
session = _opencode_worker.run_session_blocking(
server_url=cfg.server_url,
agent=group.worker_agent,
tag=tag,
prompt=prompt,
timeout_seconds=cfg.worker_timeout_seconds,
on_poll=_refresh_heartbeat,
redact_values=redact_values,
)
session_completed_at = _now()
if session.status == "completed":
terminal_state = "completed"
@@ -1173,6 +1190,59 @@ def _now() -> str:
return datetime.now(UTC).isoformat()
def _maybe_read_short_circuit(
item: dict[str, Any],
) -> "_opencode_worker.SessionResult | None":
"""Read the prompt-factory's short-circuit stash if present.
A prompt factory MAY decide that the dispatcher has already
handled the work-item deterministically (e.g. by applying
compliance fixes directly) and that no LLM session needs to run.
It signals this by stashing a ``SessionResult``-shaped object
under ``item["_dispatcher_implementer_context"]["_short_circuit_result"]``
BEFORE returning from the prompt factory.
Returns the ``SessionResult`` if present and shaped correctly;
``None`` otherwise (in which case the dispatcher spawns the
worker as usual). The key is consumed (popped) so a subsequent
cycle reading the same item dict does not re-trigger the
short-circuit.
The short-circuit machinery preserves the rest of the post-
session pipeline (telemetry, status comment, claim release)
only the LLM session is skipped. Telemetry rows will carry
``terminal_state`` from the synthesised result and downstream
consumers see the same row shape they expect.
"""
context = item.get("_dispatcher_implementer_context")
if not isinstance(context, dict):
return None
stash = context.pop("_short_circuit_result", None)
if stash is None:
return None
# Mark consumption so the post-session action's cycle-archive
# field can report ``short_circuit=True`` without re-reading the
# stash (which is gone after the .pop above).
context["_short_circuit_consumed"] = True
# Validate shape: must have at least ``status`` and be coercible
# to a SessionResult. We accept either a SessionResult instance
# directly OR a plain dict (so test code doesn't have to import
# the dataclass).
if isinstance(stash, _opencode_worker.SessionResult):
return stash
if isinstance(stash, dict) and "status" in stash:
return _opencode_worker.SessionResult(
status=stash["status"],
wallclock_seconds=float(stash.get("wallclock_seconds", 0.0)),
session_id=str(stash.get("session_id", "")),
raw_response=str(stash.get("raw_response", "")),
parsed_json=stash.get("parsed_json"),
error_kind=stash.get("error_kind"),
subagent_max_depth=stash.get("subagent_max_depth", 0),
)
return None
def ensure_cycle_table(table_name: str) -> None:
"""Create or repair the per-driver cycle table.
+534
View File
@@ -0,0 +1,534 @@
"""Deterministic applier for PR-compliance-checklist gaps.
What this is
------------
The implementer's compliance scan (:mod:`_implementer_compliance`)
checks four items mechanically:
- ``worktree_clean`` sanity check, not fixable
- ``changelog_unreleased_nonempty`` needs an entry under [Unreleased]
- ``contributors_has_author`` needs the author email in CONTRIBUTORS.md
- ``commit_has_issues_closed`` needs ``ISSUES CLOSED: #N`` in HEAD commit
Three of those (``contributors_has_author``, ``commit_has_issues_closed``,
``changelog_unreleased_nonempty``) can be applied without LLM reasoning
when the dispatcher has the source data:
- CONTRIBUTORS line: ``- {GIT_USER_NAME} <{GIT_USER_EMAIL}>``
- ISSUES CLOSED footer: derived from the prefetched ``linked_issues``
- CHANGELOG bullet: stubbed from the PR title (LLM-quality would be
marginally better here, but a PR-title stub is fine for most cases)
This module exposes :func:`apply_compliance_fixes` which takes a
worktree path + the prefetched data + the gap dict and applies the
fixes. It does NOT push pushing is the caller's responsibility
(``dispatch_implementer._auto_fix_and_push``) because push failure
recovery needs cycle-level context.
The fixes are deterministic Python (no LLM). Every operation is
idempotent: calling :func:`apply_compliance_fixes` twice on a
worktree that's already had a fix applied is a no-op for that gap.
Why this exists
---------------
Live test 2026-05-13 (PR #28 cycle 1) showed the worker uses ~12-14
min of LLM time + 4096 server time on what's effectively five bash
commands. The same operations run deterministically in <5 seconds
with no LLM cost. ``auto-agents.md`` explicitly endorses this
pattern: "deterministic Python owns orchestration; the LLM only
handles the actual creative work."
The classes covered here are *not* creative work. Writing a real
code fix for a failing test IS creative work and stays on the LLM.
Failure handling
----------------
Each fix function returns ``(applied: bool, error: str | None)``.
:func:`apply_compliance_fixes` collects the results and propagates
them to the caller. On any error, the caller (the dispatcher's
auto-fix path) should fall through to spawning the LLM worker
rather than half-fixing the PR.
Audit trail
-----------
Each applied fix produces a structured entry in the returned
``ApplyReport`` (gap name, command sequence, before/after marker).
The dispatcher writes this into the cycle archive so an operator
inspecting why a commit appeared can trace it back to the
deterministic applier rather than wondering which LLM session
produced it.
"""
from __future__ import annotations
import logging
import re
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
_logger = logging.getLogger("implementer_compliance_apply")
@dataclass
class ApplyResult:
"""Per-gap outcome of an apply attempt.
``applied``: True when the fix wrote something (or was already
satisfied idempotent). False when an error prevented the
apply.
``error``: Human-readable error string. None on success.
``operations``: Ordered list of (command, output) pairs the
apply ran. Used by the cycle archive for audit-trail.
"""
gap_name: str
applied: bool
error: str | None = None
operations: list[tuple[str, str]] = field(default_factory=list)
@dataclass
class ApplyReport:
"""Aggregate outcome of all attempted gap fixes.
``per_gap``: ApplyResult for each gap the caller asked to fix.
``all_applied``: True iff every requested gap fix succeeded.
``any_committed``: True if the apply created a new git commit
(the caller needs to know whether to push). A pure CONTRIBUTORS
update needs a commit. An ISSUES CLOSED amend re-uses HEAD's
commit (no new SHA). The combined fix is a single new commit
on top of HEAD.
"""
per_gap: list[ApplyResult] = field(default_factory=list)
all_applied: bool = True
any_committed: bool = False
final_head_sha: str = ""
def apply_compliance_fixes(
worktree: Path,
gaps: dict[str, bool],
*,
git_user_name: str,
git_user_email: str,
pr_title: str,
pr_number: int,
linked_issue_numbers: list[int],
) -> ApplyReport:
"""Apply deterministic fixes for the False entries in ``gaps``.
``gaps`` follows the contract from
:func:`_implementer_compliance.check_compliance_gaps` keys are
check names, values are bool (True = passing, False = needs fix).
``linked_issue_numbers`` is the list of issue numbers the PR
closes (sourced from the prefetched ``linked_issues``). When
multiple issues are linked we include all of them in the
``ISSUES CLOSED`` footer. When the list is empty AND the
``commit_has_issues_closed`` gap is open, that fix is SKIPPED
(no data to apply) and recorded as an error in the report so
the caller falls through to the LLM worker.
The fix order is:
1. Apply CONTRIBUTORS append (file edit, staged)
2. Apply CHANGELOG stub (file edit, staged)
3. Stage and commit those two edits together
4. Amend the HEAD commit message to add ISSUES CLOSED footer
(works whether step 3 created a new commit or not; amending
the worker's previous commit is safe because we're in a
per-PR worktree)
Ordering matters: amend MUST be last because it rewrites HEAD.
"""
report = ApplyReport()
# Stage edits first; commit happens once for the file edits;
# amend at the end.
staged_changes = False
if not gaps.get("contributors_has_author", True):
result = _apply_contributors(worktree, git_user_name, git_user_email)
report.per_gap.append(result)
if result.applied and not result.error:
staged_changes = True
else:
report.all_applied = False
if not gaps.get("changelog_unreleased_nonempty", True):
result = _apply_changelog(worktree, pr_title, pr_number)
report.per_gap.append(result)
if result.applied and not result.error:
staged_changes = True
else:
report.all_applied = False
# Commit staged file changes (if any) before potentially amending
# the message in the next step. Doing it in two phases keeps the
# commit-message amend cleanly separable from the content add.
if staged_changes:
commit_result = _commit_staged(worktree, git_user_name, git_user_email,
pr_title, pr_number,
linked_issue_numbers)
report.per_gap.append(commit_result)
if commit_result.applied and not commit_result.error:
report.any_committed = True
else:
report.all_applied = False
# ISSUES CLOSED amend: applies whether or not we just committed.
# If we did commit above, the message already includes the
# footer (from `_commit_staged`); the amend is then a no-op
# idempotent re-check. If no file changes were needed, this
# amends the existing HEAD commit.
if not gaps.get("commit_has_issues_closed", True):
if not linked_issue_numbers:
report.per_gap.append(ApplyResult(
gap_name="commit_has_issues_closed",
applied=False,
error=("no linked_issue_numbers — cannot derive "
"ISSUES CLOSED footer deterministically; "
"falling through to LLM worker"),
))
report.all_applied = False
elif not staged_changes:
# Amend the EXISTING HEAD commit message rather than
# creating a new commit. This matches what the worker
# does today via the git-commit-util sub-agent.
amend_result = _amend_commit_footer(
worktree, git_user_name, git_user_email,
linked_issue_numbers,
)
report.per_gap.append(amend_result)
if amend_result.applied and not amend_result.error:
report.any_committed = True
else:
report.all_applied = False
# else: the new commit from _commit_staged already carries
# the ISSUES CLOSED footer; nothing more to do.
report.final_head_sha = _rev_parse_head(worktree)
return report
# ─── Per-gap appliers ──────────────────────────────────────────────
def _apply_contributors(
worktree: Path, git_user_name: str, git_user_email: str,
) -> ApplyResult:
"""Append ``- {name} <{email}>`` to CONTRIBUTORS.md if absent.
Creates the file with a minimal header when it doesn't exist.
Idempotent if the line is already present the function
returns ``applied=True`` without writing.
"""
result = ApplyResult(gap_name="contributors_has_author", applied=False)
contributors = worktree / "CONTRIBUTORS.md"
line = f"- {git_user_name} <{git_user_email}>"
try:
if contributors.exists():
text = contributors.read_text(encoding="utf-8")
if git_user_email.lower() in text.lower():
# already present — idempotent success
result.applied = True
result.operations.append(("read CONTRIBUTORS.md", "email already present"))
return result
if not text.endswith("\n"):
text += "\n"
text += line + "\n"
else:
text = (
"# Contributors\n\n"
"Thanks to everyone who has contributed to this project:\n\n"
f"{line}\n"
)
contributors.write_text(text, encoding="utf-8")
result.applied = True
result.operations.append((f"write CONTRIBUTORS.md (+1 line)", line))
except OSError as exc:
result.error = f"CONTRIBUTORS.md write failed: {exc}"
return result
def _apply_changelog(
worktree: Path, pr_title: str, pr_number: int,
) -> ApplyResult:
"""Insert a stub bullet under [Unreleased] in CHANGELOG.md.
The stub uses the PR title verbatim. If CHANGELOG.md exists but
has no [Unreleased] section, one is inserted at the top below
the first ``# Changelog`` header. If CHANGELOG.md does not
exist, a minimal one is created.
The bullet appears as ``- {pr_title} (#{pr_number})`` to give a
cross-reference back to the PR. LLM-quality CHANGELOG entries
might say more, but the PR-title stub is the dominant pattern
in this codebase's history and is correct for most cases.
Idempotent: if a bullet referencing ``#{pr_number}`` already
exists under [Unreleased], no write happens.
"""
result = ApplyResult(gap_name="changelog_unreleased_nonempty", applied=False)
changelog = worktree / "CHANGELOG.md"
bullet = f"- {pr_title.strip()} (#{pr_number})"
try:
if changelog.exists():
text = changelog.read_text(encoding="utf-8")
# Idempotent guard
if f"(#{pr_number})" in text:
result.applied = True
result.operations.append(("read CHANGELOG.md", "bullet for this PR already present"))
return result
else:
text = "# Changelog\n\n## [Unreleased]\n\n"
# Find the [Unreleased] header; insert bullet under it.
unreleased_re = re.compile(
r"^(##\s*\[Unreleased\][^\n]*\n)",
re.MULTILINE | re.IGNORECASE,
)
m = unreleased_re.search(text)
if m:
insert_at = m.end()
# Insert a blank line + bullet + blank line if the
# header is immediately followed by content. Otherwise
# just bullet + blank line.
tail = text[insert_at:]
if tail.startswith("\n"):
# there's a blank after the header; just inject
new_text = text[:insert_at] + bullet + "\n" + tail
else:
new_text = text[:insert_at] + "\n" + bullet + "\n" + tail
else:
# No [Unreleased] section — add one near the top.
header_re = re.compile(r"^(#\s+[^\n]+\n)", re.MULTILINE)
hm = header_re.search(text)
new_section = f"\n## [Unreleased]\n\n{bullet}\n"
if hm:
new_text = text[:hm.end()] + new_section + text[hm.end():]
else:
# No top-level header either; prepend everything.
new_text = f"# Changelog\n{new_section}\n{text}"
changelog.write_text(new_text, encoding="utf-8")
result.applied = True
result.operations.append(("write CHANGELOG.md (+1 bullet)", bullet))
except OSError as exc:
result.error = f"CHANGELOG.md write failed: {exc}"
return result
# ─── Git operations ────────────────────────────────────────────────
def _commit_staged(
worktree: Path,
git_user_name: str,
git_user_email: str,
pr_title: str,
pr_number: int,
linked_issue_numbers: list[int],
) -> ApplyResult:
"""Stage all changes in the worktree and commit them with a
compliance-fix-shaped commit message.
The commit message embeds the ``ISSUES CLOSED:`` footer pre-
populated from ``linked_issue_numbers`` so the subsequent amend
step is a no-op when the new commit is the HEAD. If
``linked_issue_numbers`` is empty, the message has no footer
the amend step will then detect the missing footer and either
add it (if data is available) or leave the gap.
"""
result = ApplyResult(gap_name="commit_staged_compliance_fixes", applied=False)
msg_lines = [
f"chore(compliance): apply deterministic compliance fixes for #{pr_number}",
"",
f"Auto-applied by the implementer dispatcher's compliance scanner.",
f"Source PR: #{pr_number}{pr_title.strip()}",
]
if linked_issue_numbers:
joined = " ".join(f"#{n}" for n in linked_issue_numbers)
msg_lines.append("")
msg_lines.append(f"ISSUES CLOSED: {joined}")
msg = "\n".join(msg_lines) + "\n"
try:
env = _env_for_commit(git_user_name, git_user_email)
_run_git(worktree, ["add", "-A"], env, result, "stage all")
_run_git(worktree, ["commit", "-m", msg], env, result, "commit")
result.applied = True
except _GitError as exc:
result.error = f"commit_staged failed: {exc}"
return result
def _amend_commit_footer(
worktree: Path,
git_user_name: str,
git_user_email: str,
linked_issue_numbers: list[int],
) -> ApplyResult:
"""Amend HEAD to add an ``ISSUES CLOSED: #N #M`` footer.
Reads the current message, appends the footer if not present,
rewrites via ``git commit --amend``. Idempotent: if the footer
is already there with the same issue numbers, no rewrite.
"""
result = ApplyResult(gap_name="commit_has_issues_closed", applied=False)
if not linked_issue_numbers:
result.error = "no linked_issue_numbers — cannot amend"
return result
try:
env = _env_for_commit(git_user_name, git_user_email)
# Read current message
rc, out = _run_git_capture(
worktree, ["log", "-1", "--pretty=%B", "HEAD"], env,
)
if rc != 0:
result.error = f"git log -1 returned {rc}"
return result
current_msg = out.rstrip("\n")
joined = " ".join(f"#{n}" for n in linked_issue_numbers)
footer = f"ISSUES CLOSED: {joined}"
if footer in current_msg:
# Already there — idempotent success
result.applied = True
result.operations.append(("read HEAD message", "footer already present"))
return result
new_msg = current_msg.rstrip() + "\n\n" + footer + "\n"
_run_git(worktree, ["commit", "--amend", "-m", new_msg], env,
result, "amend HEAD with footer")
result.applied = True
except _GitError as exc:
result.error = f"amend failed: {exc}"
return result
def _rev_parse_head(worktree: Path) -> str:
"""Return HEAD's SHA in the worktree, or empty string on error."""
try:
proc = subprocess.run(
["git", "-C", str(worktree), "rev-parse", "HEAD"],
capture_output=True, text=True, timeout=10, check=False,
)
if proc.returncode == 0:
return proc.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
return ""
# ─── Internal helpers ──────────────────────────────────────────────
class _GitError(Exception):
"""Raised by ``_run_git`` when a subprocess fails."""
def _env_for_commit(name: str, email: str) -> dict[str, str]:
"""Build an env override that pins the commit identity AND
disables GPG signing (otherwise dev machines with global signing
enabled would prompt or fail).
"""
import os
env = {**os.environ}
env["GIT_AUTHOR_NAME"] = name
env["GIT_AUTHOR_EMAIL"] = email
env["GIT_COMMITTER_NAME"] = name
env["GIT_COMMITTER_EMAIL"] = email
return env
def _run_git(
worktree: Path, args: list[str], env: dict[str, str],
result: ApplyResult, label: str,
) -> None:
"""Run a git subcommand; raise ``_GitError`` on non-zero exit.
Records the operation in ``result.operations`` for audit trail.
Also unconditionally adds ``-c commit.gpgsign=false`` to defend
against developer-machine global signing configs (the live test
fixture pattern).
"""
cmd = ["git", "-c", "commit.gpgsign=false", "-C", str(worktree), *args]
try:
proc = subprocess.run(
cmd, env=env, capture_output=True, text=True,
timeout=30, check=False,
)
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
result.operations.append((" ".join(cmd[:8]), f"<failed: {exc}>"))
raise _GitError(f"{label}: {exc}") from exc
out = ((proc.stdout or "") + (proc.stderr or "")).strip()
result.operations.append((" ".join(cmd[:8]) + " ...", out[:200]))
if proc.returncode != 0:
raise _GitError(f"{label}: rc={proc.returncode}: {out[:300]}")
def _run_git_capture(
worktree: Path, args: list[str], env: dict[str, str],
) -> tuple[int, str]:
"""Run a git subcommand and return ``(rc, stdout)``. Stderr
discarded for these read-only commands (``log``, ``rev-parse``)
we only care about stdout."""
cmd = ["git", "-C", str(worktree), *args]
try:
proc = subprocess.run(
cmd, env=env, capture_output=True, text=True,
timeout=10, check=False,
)
except (subprocess.TimeoutExpired, FileNotFoundError):
return -1, ""
return proc.returncode, (proc.stdout or "")
# ─── Push helper ───────────────────────────────────────────────────
def push_branch(
worktree: Path,
branch: str,
git_user_name: str,
git_user_email: str,
) -> ApplyResult:
"""Push the worktree's HEAD to ``origin/<branch>``.
Uses ``--force-with-lease`` so a remote that moved while we
were applying gets a "stale info" rejection rather than a
silent overwrite. Disables mirror semantics via ``-c
remote.origin.mirror=false`` (the worker's per-command
workaround, which is robust across worktree configs even
after A1's clone-time fix lands).
"""
result = ApplyResult(gap_name="push_to_origin", applied=False)
try:
env = _env_for_commit(git_user_name, git_user_email)
cmd = [
"git", "-c", "remote.origin.mirror=false",
"-C", str(worktree), "push",
"--force-with-lease",
"origin", f"HEAD:refs/heads/{branch}",
]
proc = subprocess.run(
cmd, env=env, capture_output=True, text=True,
timeout=60, check=False,
)
out = ((proc.stdout or "") + (proc.stderr or "")).strip()
result.operations.append(("git push --force-with-lease ...", out[:300]))
if proc.returncode == 0:
result.applied = True
else:
result.error = f"push rc={proc.returncode}: {out[:300]}"
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
result.error = f"push subprocess failed: {exc}"
return result
__all__ = (
"ApplyResult",
"ApplyReport",
"apply_compliance_fixes",
"push_branch",
)
+28 -4
View File
@@ -113,7 +113,12 @@ BUDGET_PER_FAILURE_CLASS: Mapping[FailureClass, int] = {
# the literal "resolved" is accepted (see PD P0 in the plan): worker
# prompt drift could surface "success" / "done" / "ok" which must
# NOT short-circuit to SUCCESS.
SUCCESS_OUTCOMES = frozenset({"resolved"})
# Outcomes that signal the cycle is done — the dispatcher records
# them as SUCCESS and stops escalating. ``resolved`` is the worker's
# affirmative-finish signal. ``no_changes_needed`` is the
# dispatcher's deterministic short-circuit verdict (2026-05-13 P0
# path): the PR was already complete before the LLM was even called.
SUCCESS_OUTCOMES = frozenset({"resolved", "no_changes_needed"})
# Terminal-state buckets used by :func:`classify_failure`. Mirrors
@@ -236,12 +241,31 @@ def decide(
if is_success_outcome:
if head_sha_advanced is True:
return EscalationAction.SUCCESS
# ``no_changes_needed`` legitimately produces no push: the
# dispatcher's P0 short-circuit determined nothing was wrong
# to begin with. Treat as SUCCESS regardless of
# head_sha_advanced — there's nothing the worker could
# have done that the dispatcher hasn't already verified.
if outcome == "no_changes_needed":
return EscalationAction.SUCCESS
if head_sha_advanced is None:
return EscalationAction.RETRY_POST_FETCH
# head_sha_advanced is False: worker claims success but did
# not actually push. Drop through to the failure-class path
# so the action is determined by the (transport vs competence
# vs tier-stable) classification of the terminal_state.
# NOT actually push. This is a competence failure, not
# transport — the worker emitted a complete-looking JSON
# while delivering nothing. Same model on same input will
# do the same thing; force escalation by reclassifying
# as COMPETENCE. Without this branch the drop-through hit
# the UNKNOWN bucket (budget=1) and wasted a same-tier
# retry, exactly the failure observed live on 2026-05-13
# (PR #30 attempts 1 and 3; PR #28 cycle 2 attempt 1).
#
# Bypass classify_failure entirely so the bookkeeping is
# explicit: skip the budget check and go straight to
# ESCALATE (or EXHAUSTED at ceiling).
if current_tier >= max_tier:
return EscalationAction.EXHAUSTED
return EscalationAction.ESCALATE
# Silent worst case: the worker pushed at least one commit and
# then failed before emitting a success outcome. Escalating would
+10
View File
@@ -248,6 +248,16 @@ def extract_phase4_telemetry(
# to a real ``bool`` for downstream JSONL consumers that
# filter on ``row["outcome_synthesised"] is True``.
row["outcome_synthesised"] = bool(outcome_synthesised)
# P4: outcome_disputed — the worker emitted a success outcome
# but no push reached origin. This is the "worker hallucinates
# success" failure class observed live on 2026-05-13 (PR #30
# attempts 1/3, PR #28 cycle 2 attempt 1). The predicate now
# routes this to ESCALATE (see _implementer_escalation A2 fix),
# but the row separately records that the worker's verdict
# didn't match reality so an analyst can grep for the lie rate.
outcome_lower = (outcome or "").lower()
if outcome_lower == "resolved" and head_sha_advanced is False:
row["outcome_disputed"] = True
return row
+44
View File
@@ -291,6 +291,7 @@ def _ensure_mirror(cfg: Any) -> Path | None:
except OSError:
pass
return None
_disable_mirror_push_semantics(mirror, env)
return mirror
head = mirror / "HEAD"
try:
@@ -310,9 +311,52 @@ def _ensure_mirror(cfg: Any) -> Path | None:
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
_logger.warning("mirror fetch failed: %s", exc)
# Stale-but-usable beats no clone at all.
# Defensive: an existing mirror created by an older dispatcher
# version may still have ``remote.origin.mirror=true``. Idempotent
# to set it to false again (git just no-ops).
_disable_mirror_push_semantics(mirror, env)
return mirror
def _disable_mirror_push_semantics(mirror: Path, env: dict[str, str]) -> None:
"""Clear ``remote.origin.mirror=true`` on the bare mirror so worktrees
derived from it can push refspec-style.
``git clone --mirror`` sets ``remote.origin.mirror=true``, which makes
every push to ``origin`` behave as ``git push --mirror`` (push all
refs, no refspecs allowed). Worktrees inherit this config, so the
worker's natural ``git push --force-with-lease origin HEAD:<branch>``
fails with ``fatal: --mirror can't be combined with refspecs``. The
worker historically had to discover this and work around it per cycle
(PR #30 2026-05-13 case: 1 of 2 workarounds was fragile and the
recovery path lost the local commit).
The mirror's ``+refs/*:refs/*`` fetch refspec is sufficient to keep
``fetch`` behaving like a mirror; the ``mirror=true`` flag only
governs push semantics, which we never use against the bare mirror
itself anyway. Setting it to false is safe and idempotent.
Errors are logged at WARNING but do not propagate failure to
flip this flag only means the worker has to apply its existing
per-command ``-c remote.origin.mirror=false`` workaround.
"""
try:
subprocess.run(
["git", "--git-dir", str(mirror),
"config", "remote.origin.mirror", "false"],
env=env,
check=True,
timeout=10,
capture_output=True,
)
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
_logger.warning(
"failed to clear remote.origin.mirror on %s (worker will "
"have to use its per-command workaround): %s",
mirror, exc,
)
# ─── Worktree handle ───────────────────────────────────────────────────────
+370 -1
View File
@@ -73,6 +73,9 @@ _implementer_label_state = _load_sibling(
_implementer_compliance = _load_sibling(
"_implementer_compliance", "_implementer_compliance.py"
)
_implementer_compliance_apply = _load_sibling(
"_implementer_compliance_apply", "_implementer_compliance_apply.py"
)
_implementer_gate_preflight = _load_sibling(
"_implementer_gate_preflight", "_implementer_gate_preflight.py"
)
@@ -158,6 +161,26 @@ OUTCOME_SYNTHESIS_ENV_VAR = "IMPLEMENTER_OUTCOME_SYNTHESIS"
# escalation-on case, not an independent feature toggle.
COMPLIANCE_GAPS_ENABLED_ENV_VAR = "IMPLEMENTER_COMPLIANCE_GAPS_ENABLED"
# Auto-fix kill switch. When this flag is non-falsy, the dispatcher
# applies trivial PR-compliance fixes (CONTRIBUTORS line, CHANGELOG
# stub, ISSUES CLOSED footer) deterministically without spawning the
# LLM worker. The worker is still called when there's real code work
# to do (failing tests, request-changes review feedback). Off-by-
# default because giving the dispatcher commit/push authority is a
# real responsibility expansion and operators should opt in.
#
# When this flag is OFF (default), the worker handles compliance
# fixes the way it has historically — slower and more expensive but
# with the LLM in the loop as a safety check.
#
# Operational consequences when set to 1:
# - ~5-13 min of LLM time saved per "metadata-only PR" cycle
# - Dispatcher pushes commits authored by the configured
# ``GIT_USER_NAME`` / ``GIT_USER_EMAIL`` identity
# - Cycle archive's ``post_session_result`` gains an
# ``auto_fix_report`` entry describing what was applied
AUTO_FIX_COMPLIANCE_ENV_VAR = "IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE"
def _work_type_for_group(group_name: str) -> str:
"""Map dispatcher work-group names to the ``work_type`` constant
@@ -268,6 +291,20 @@ def _is_compliance_gaps_enabled() -> bool:
return True
def _is_auto_fix_compliance_enabled() -> bool:
"""Return ``True`` when the dispatcher should apply deterministic
compliance fixes (CONTRIBUTORS line, CHANGELOG stub, ISSUES CLOSED
footer) without spawning the LLM worker.
**Default OFF.** Enabling this gives the dispatcher commit/push
authority for trivial PR-hygiene changes. Set
``IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE=1`` to opt in.
See :data:`AUTO_FIX_COMPLIANCE_ENV_VAR` for full rationale.
"""
return _env_truthy(AUTO_FIX_COMPLIANCE_ENV_VAR)
def _is_tier2_enabled() -> bool:
"""Return ``True`` when Tier 2 (``tier-kimi``) is part of the
escalation ladder.
@@ -519,6 +556,12 @@ def _prefetch_prompt(
det_sections = _compute_deterministic_sections(
cfg, item, group, result, clone_handle,
)
# Stash on the item context so the short-circuit logic
# (``_maybe_short_circuit``) can read the deterministic-section
# results without re-running compliance / preflight scans.
context = item.get("_dispatcher_implementer_context")
if isinstance(context, dict):
context["_deterministic_sections"] = det_sections
if not cfg.dry_run:
try:
@@ -589,6 +632,26 @@ def _compute_deterministic_sections(
worktree_path, changed_files,
)
if isinstance(classification, dict):
# P3: cross-check local preflight against the
# dispatcher's prefetched remote CI state. If the
# local --fast gates say "all clean" but remote CI
# is failing, the failing job is something --fast
# doesn't exercise (e.g. e2e_tests or coverage).
# Surface the divergence so the worker doesn't trust
# preflight alone — without this, the worker reads
# "preflight clean" + "compliance clean" and emits
# `resolved` while CI is still red (the 2026-05-13
# PR #30 attempt 1 / PR #28 cycle 2 failure mode).
remote_ci_state = _extract_remote_ci_state(result)
classification["remote_ci_state"] = remote_ci_state
preflight_clean = (
classification.get("failures_total", 0) == 0
and not classification.get("preflight_timeout", False)
)
if preflight_clean and remote_ci_state == "failure":
classification["diverges_from_remote_ci"] = True
else:
classification["diverges_from_remote_ci"] = False
out["gate_preflight"] = classification
except Exception as exc:
_logger.warning(
@@ -879,6 +942,26 @@ def _render_compliance_pointer_stanza(
return "\n".join(lines)
def _extract_remote_ci_state(result: Any) -> str:
"""Pull the remote CI aggregate state from the prefetch result.
The dispatcher's :mod:`_implementer_prefetch` fetches the
PR's HEAD CI status; this just normalises the value into one
of ``"success"``, ``"failure"``, ``"pending"``, or ``"unknown"``.
Used by :func:`_compute_deterministic_sections` (P3) to
cross-check the local preflight result against what remote CI
actually says.
"""
ci_status = getattr(result, "ci_status", None)
if isinstance(ci_status, dict):
state = ci_status.get("state")
if isinstance(state, str) and state:
return state.lower()
if isinstance(ci_status, str) and ci_status:
return ci_status.lower()
return "unknown"
def _collect_changed_files_from_result(result: Any) -> list[str]:
"""Best-effort extraction of the PR's changed-file list from the
prefetch result. Used by the gate pre-flight to classify gate
@@ -916,6 +999,269 @@ def _collect_changed_files_from_result(result: Any) -> list[str]:
return paths
def _maybe_short_circuit(
cfg: Any, item: dict[str, Any], context: dict[str, Any],
) -> None:
"""Decide whether the dispatcher can satisfy this PR's cycle
without spawning the LLM worker, and if so stash a synthetic
``SessionResult`` on ``context["_short_circuit_result"]`` for
the dispatch runtime to consume.
Two short-circuit paths:
1. **P0 skip-when-green.** If compliance scan says all checks
passed, gate-preflight says no failures, AND remote CI status
is ``success``, then this PR is genuinely done and the
listing script's "failing CI" snapshot was stale (CI flipped
between snapshot and claim). Emit ``no_changes_needed`` and
release. Fires whenever escalation is on and conditions hold.
2. **A auto-fix-compliance.** If compliance scan reports gaps
that the dispatcher can fix deterministically (CONTRIBUTORS,
CHANGELOG bullet, ISSUES CLOSED footer), gate-preflight is
clean, AND remote CI failure looks attributable only to those
missing items, then apply the fixes + push + emit
``resolved`` synthetically. Gated by
``IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE=1`` because it
gives the dispatcher commit/push authority.
Both paths are no-ops when:
- dry-run
- escalation feature disabled
- the item isn't PR-shaped
- the prefetch result is missing
- any required signal (compliance, preflight, CI) is absent or
ambiguous
"""
if cfg.dry_run:
return
if not _is_escalation_enabled():
return
if not isinstance(item.get("head"), dict):
return
if not isinstance(context, dict):
return
result = context.get("result")
if result is None:
return
clone_handle = context.get("clone_handle")
if clone_handle is None or not getattr(clone_handle, "path", None):
return
pr_number = int(item.get("number") or 0)
remote_ci_state = _extract_remote_ci_state(result)
# Pull the deterministic sections we already computed for the
# sentinel. Re-running them would double the compute cost; both
# are stashed on the context by ``_prefetch_prompt``.
det_sections = context.get("_deterministic_sections") or {}
compliance = det_sections.get("compliance_gaps") or {}
preflight = det_sections.get("gate_preflight") or {}
gaps: dict[str, bool] = compliance.get("gaps") or {}
masked_checks = compliance.get("masked_checks") or []
if not gaps:
# No compliance scan ran — be conservative and dispatch
# the worker. Most commonly this means the preclone was
# disabled or failed.
return
preflight_clean = bool(
preflight
and preflight.get("failures_total", 0) == 0
and not preflight.get("preflight_timeout", False)
)
compliance_clean = (
all(bool(v) for v in gaps.values()) and not masked_checks
)
# ─── P0: everything green ──────────────────────────────────
if (
compliance_clean
and preflight_clean
and remote_ci_state == "success"
):
_logger.info(
"P0 short-circuit for PR #%s: compliance clean + preflight "
"clean + remote CI success — skipping LLM worker",
pr_number,
)
context["_short_circuit_result"] = {
"status": "completed",
"wallclock_seconds": 0.0,
"session_id": "",
"raw_response": (
'{"outcome": "no_changes_needed", "files_touched": [], '
'"_dispatcher_short_circuit": "P0"}'
),
"parsed_json": {
"outcome": "no_changes_needed",
"files_touched": [],
"_dispatcher_short_circuit": "P0",
},
}
return
# ─── A: auto-fix compliance gaps ───────────────────────────
# Only fires when (a) the operator opted in via the flag, (b)
# preflight is clean (so failing CI is plausibly about
# compliance-shaped checks, not test failures), (c) compliance
# has at least one gap, (d) none of the gaps are masked (we
# don't have reliable signal to fix what we can't see).
if (
not _is_auto_fix_compliance_enabled()
or masked_checks
or compliance_clean
or not preflight_clean
):
return
# Don't auto-fix when remote CI is passing — there's nothing to
# fix in that case (P0 above would have caught the truly green
# case; this guards against weird-state cycles where CI is
# green but the dispatcher's compliance scan found something).
if remote_ci_state == "success":
return
worktree = Path(str(clone_handle.path))
pr_title = ""
pr_details = getattr(result, "pr_details", None)
if isinstance(pr_details, dict):
pr_title = str(pr_details.get("title") or "")
if not pr_title:
pr_title = str(item.get("title") or f"PR #{pr_number}")
linked = getattr(result, "linked_issues", None) or []
linked_issue_numbers: list[int] = []
for li in linked:
if isinstance(li, dict):
n = li.get("number")
if isinstance(n, int) and n > 0:
linked_issue_numbers.append(n)
git_user_name = (
os.environ.get("GIT_USER_NAME")
or getattr(cfg, "git_user_name", "")
or "CleverThis"
)
git_user_email = (
os.environ.get("GIT_USER_EMAIL")
or getattr(cfg, "git_user_email", "")
or "hal9000@cleverthis.com"
)
branch = ""
if isinstance(pr_details, dict):
head = pr_details.get("head") or {}
if isinstance(head, dict):
branch = str(head.get("ref") or "")
if not branch:
_logger.warning(
"auto-fix short-circuit for PR #%s aborted: missing head_ref "
"(can't push without the branch name); falling through to worker",
pr_number,
)
return
try:
report = _implementer_compliance_apply.apply_compliance_fixes(
worktree, gaps,
git_user_name=git_user_name,
git_user_email=git_user_email,
pr_title=pr_title,
pr_number=pr_number,
linked_issue_numbers=linked_issue_numbers,
)
except Exception as exc:
_logger.warning(
"auto-fix apply raised for PR #%s; falling through to worker: %s",
pr_number, exc,
)
return
if not report.all_applied:
_logger.info(
"auto-fix could not deterministically resolve all gaps for PR "
"#%s (%d failures); falling through to worker",
pr_number,
sum(1 for r in report.per_gap if r.error),
)
return
if not report.any_committed:
# All gaps were already satisfied (idempotent re-run). Push
# is unnecessary — the worktree is the same as origin.
_logger.info(
"auto-fix found no actual gaps to fix on PR #%s after recheck "
"(idempotent); short-circuit emits no_changes_needed",
pr_number,
)
context["_short_circuit_result"] = {
"status": "completed",
"raw_response": (
'{"outcome": "no_changes_needed", "files_touched": [], '
'"_dispatcher_short_circuit": "A_idempotent"}'
),
"parsed_json": {
"outcome": "no_changes_needed",
"files_touched": [],
"_dispatcher_short_circuit": "A_idempotent",
},
}
return
# Push the deterministic commit.
push_result = _implementer_compliance_apply.push_branch(
worktree, branch,
git_user_name=git_user_name,
git_user_email=git_user_email,
)
if not push_result.applied:
_logger.warning(
"auto-fix push failed for PR #%s (%s); falling through to worker",
pr_number, push_result.error,
)
return
_logger.info(
"auto-fix short-circuit for PR #%s: applied %d compliance fix(es) "
"+ pushed %s",
pr_number,
sum(1 for r in report.per_gap if r.applied and not r.error),
report.final_head_sha[:12] if report.final_head_sha else "<unknown>",
)
context["_short_circuit_result"] = {
"status": "completed",
"raw_response": (
'{"outcome": "resolved", "files_touched": ["CHANGELOG.md", '
'"CONTRIBUTORS.md"], "_dispatcher_short_circuit": "A_auto_fix", '
'"_auto_fix_head_sha": "' + report.final_head_sha + '"}'
),
"parsed_json": {
"outcome": "resolved",
"files_touched": [
r.gap_name for r in report.per_gap
if r.applied and not r.error
],
"_dispatcher_short_circuit": "A_auto_fix",
"_auto_fix_head_sha": report.final_head_sha,
},
}
# Stash the apply report for downstream telemetry / cycle archive
# consumers that want the audit trail.
context["_auto_fix_report"] = {
"all_applied": report.all_applied,
"any_committed": report.any_committed,
"final_head_sha": report.final_head_sha,
"per_gap": [
{
"gap_name": r.gap_name,
"applied": r.applied,
"error": r.error,
}
for r in report.per_gap
],
}
def _implementation_prompt_dispatch(
cfg: Any, item: dict[str, Any], group: Any
) -> str:
@@ -970,7 +1316,19 @@ def _implementation_prompt_dispatch(
extras = ["release_claim_on_exit: false"]
if start_tier > 0:
extras.append(f"escalation_tier_hint: `{start_tier}`")
return f"{base_prompt}\n\n" + "\n".join(extras) + "\n"
final_prompt = f"{base_prompt}\n\n" + "\n".join(extras) + "\n"
# P0 / A: deterministic short-circuit.
# If the prefetch + deterministic sections + remote CI state all
# agree that no LLM work is needed (or that the only needed work
# is mechanical compliance fixes the dispatcher can apply itself),
# stash a synthetic SessionResult on the context. The dispatch
# runtime's short-circuit hook consumes it and skips the worker.
# Off by default (gated on IMPLEMENTER_DISPATCHER_AUTO_FIX_COMPLIANCE
# for the auto-fix path; the no-op skip case fires whenever
# escalation is on and conditions are met).
_maybe_short_circuit(cfg, item, context)
return final_prompt
# ─── Post-session action: cleanup pre-cloned worktree ───────────────────────
@@ -1364,6 +1722,17 @@ def _post_session_action(
"phase4_telemetry": None,
"work_group_name": resolved_group_name,
}
# Surface the dispatcher's deterministic-fix report (P0 / A) when
# the cycle was short-circuited by ``_maybe_short_circuit``. An
# operator inspecting cycle archives sees ``auto_fix_report`` ==
# None on a normal worker cycle and a populated dict when the
# dispatcher applied compliance fixes itself.
out["auto_fix_report"] = (
context_dict.get("_auto_fix_report") if context_dict else None
)
out["short_circuit"] = bool(
context_dict and context_dict.get("_short_circuit_consumed")
)
out["phase4_telemetry"] = _record_phase4_telemetry(
cfg,
item,