From f6ba8fee560d5bd787a7a157aa390043dda5c8ea Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Thu, 2 Apr 2026 17:31:13 +0000 Subject: [PATCH] chore(agents): add pre-PR rebase step to issue worker Agent evolver identified a systematic pattern: - Pattern: PR stale/conflict cascade - Evidence: 11+ PRs were approved but closed without merge due to conflicts. PRs #1248, #1247, #1237, #1236, #1220, #1219, #1238, #1246, #1269 were all bulk-closed as stale. Reviewer notes on #1248, #1220, #1219, #1252 explicitly mention 'merge blocked by conflicts'. With 16 parallel workers, master moves fast and branches created minutes earlier are already behind by the time PRs are created. - Fix: Add a rebase-onto-latest-master step (Phase 3, Step 3.0) in ca-issue-worker before committing and creating the PR. This ensures the branch is current when the PR is opened, dramatically reducing the chance of merge conflicts when the reviewer processes it. This change requires human approval before taking effect. --- .opencode/agents/ca-issue-worker.md | 29 +++++++++++++++++ features/steps/async_audit_recording_steps.py | 32 +++++++------------ 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/.opencode/agents/ca-issue-worker.md b/.opencode/agents/ca-issue-worker.md index e017773e..06a4b603 100644 --- a/.opencode/agents/ca-issue-worker.md +++ b/.opencode/agents/ca-issue-worker.md @@ -273,6 +273,33 @@ for wave_number, wave_subtasks in enumerate(waves): ## Phase 3: Commit and Push +### Step 3.0: Rebase onto Latest Master + +**Before committing, rebase the branch onto the latest master.** With many +workers running in parallel, master moves fast. A branch that was created +from master 10 minutes ago may already be behind several merged PRs. If you +skip this step, the resulting PR will likely have merge conflicts by the time +the reviewer gets to it — wasting the entire review cycle. + +```bash +cd /tmp/cleveragents- +git stash # Stash any uncommitted changes +git fetch origin # Get latest master +git rebase origin/master # Rebase onto latest master +git stash pop # Re-apply uncommitted changes (if any) +``` + +**If the rebase produces conflicts:** +1. Attempt to resolve them automatically by examining both sides and + choosing the implementation that preserves your changes while + incorporating upstream updates. +2. If auto-resolution fails, abort the rebase (`git rebase --abort`, + `git stash pop`) and proceed without rebasing. A PR with conflicts + is better than no PR — the reviewer can request a rebase later. +3. Log whether the rebase succeeded or was skipped in the return value. + +### Step 3.1: Commit and Push + 1. Invoke `ca-commit-message-formatter` with: - The issue metadata (specifically the Commit Message field and the issue number) @@ -390,3 +417,5 @@ Report back to the orchestrator with: - Aggregate: total attempts across all subtasks, total escalations - **Resume status**: whether this was a fresh run or a resume, and if resumed, what phase it resumed from +- **Rebase status**: whether the pre-PR rebase succeeded, was skipped + (no conflicts), or failed (conflicts, proceeded without rebase) diff --git a/features/steps/async_audit_recording_steps.py b/features/steps/async_audit_recording_steps.py index 2465defc..e7372a02 100644 --- a/features/steps/async_audit_recording_steps.py +++ b/features/steps/async_audit_recording_steps.py @@ -50,6 +50,7 @@ def _make_async_service(context: Context) -> AuditService: engine = create_engine(db_url, connect_args={"check_same_thread": False}) Base.metadata.create_all(engine, tables=[]) from cleveragents.infrastructure.database.models import AuditLogModel + Base.metadata.create_all(engine, tables=[AuditLogModel.__table__]) engine.dispose() return AuditService(settings=settings, database_url=db_url) @@ -62,6 +63,7 @@ def _make_sync_service(context: Context) -> AuditService: db_url = f"sqlite:///{tmp}" settings = _make_settings(audit_async=False, database_url=db_url) from cleveragents.infrastructure.database.models import AuditLogModel + engine = create_engine(db_url) Base.metadata.create_all(engine, tables=[AuditLogModel.__table__]) engine.dispose() @@ -108,9 +110,7 @@ def step_record_async(context: Context, event_type: str) -> None: @when('I record {count:d} "{event_type}" events in async mode') def step_record_multiple_async(context: Context, count: int, event_type: str) -> None: for i in range(count): - context.async_service.record( - event_type=event_type, plan_id=f"plan-async-{i}" - ) + context.async_service.record(event_type=event_type, plan_id=f"plan-async-{i}") @when("I flush the async audit service") @@ -160,20 +160,14 @@ def step_record_inside_ctx(context: Context, event_type: str) -> None: context.async_closed = True -@when( - 'I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode' -) -def step_record_ordered_async( - context: Context, p1: str, p2: str, p3: str -) -> None: +@when('I record events with plan_ids "{p1}" "{p2}" "{p3}" in async mode') +def step_record_ordered_async(context: Context, p1: str, p2: str, p3: str) -> None: for pid in (p1, p2, p3): context.async_service.record(event_type="plan_applied", plan_id=pid) context.ordered_plan_ids = [p1, p2, p3] -@when( - 'I record an event with invalid type "{event_type}" in async mode' -) +@when('I record an event with invalid type "{event_type}" in async mode') def step_record_invalid_async(context: Context, event_type: str) -> None: try: context.async_service.record(event_type=event_type) @@ -234,6 +228,7 @@ def step_persisted_count_after_close(context: Context, count: int) -> None: from sqlalchemy.orm import sessionmaker as _sm from cleveragents.infrastructure.database.models import AuditLogModel as _M + engine = _ce(f"sqlite:///{db_path}") session = _sm(bind=engine)() actual = session.query(_M).count() @@ -254,6 +249,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None: from sqlalchemy.orm import sessionmaker as _sm from cleveragents.infrastructure.database.models import AuditLogModel as _M + engine = _ce(f"sqlite:///{db_path}") session = _sm(bind=engine)() actual = session.query(_M).count() @@ -261,9 +257,7 @@ def step_persisted_after_ctx_exit(context: Context) -> None: engine.dispose() else: actual = context.async_service.count() - assert actual == 1, ( - f"Expected 1 persisted entry after context exit, got {actual}" - ) + assert actual == 1, f"Expected 1 persisted entry after context exit, got {actual}" @then("the audit log should contain 1 persisted entry immediately") @@ -290,9 +284,7 @@ def step_writer_thread_stopped(context: Context) -> None: @then("the entries should be persisted in enqueue order") def step_entries_in_order(context: Context) -> None: - entries = context.async_service.list_entries( - event_type="plan_applied", limit=100 - ) + entries = context.async_service.list_entries(event_type="plan_applied", limit=100) # list_entries returns newest-first; reverse to get enqueue order. persisted_ids = [e.plan_id for e in reversed(entries)] assert persisted_ids == context.ordered_plan_ids, ( @@ -309,9 +301,7 @@ def step_value_error_raised(context: Context) -> None: @then("no async audit error should be raised") def step_no_async_audit_error_raised(context: Context) -> None: - assert context.async_error is None, ( - f"Expected no error, got {context.async_error}" - ) + assert context.async_error is None, f"Expected no error, got {context.async_error}" # ── Settings steps ────────────────────────────────────────────────