Files
cleveragents-core/tools/forgejo-sync.py
T
drew 07013335b9 Auto-agents pipeline: enforce merge invariant with deterministic driver
Implement the improve_auto-agents_pipeline plan end-to-end and apply post-implementation hardening fixes so master can only advance via SHAs that passed CI against the exact current master.

Key changes:
- Add deterministic merge driver in `tools/merge_drive.py`:
  - Single-instance lock (`fcntl.flock`) + heartbeat/status surfaces.
  - Train-merge with bisect-on-failure and independent bisect/restart budgets.
  - `head_commit_id` optimistic lock enforcement on merge endpoint.
  - Single-PR strategy switched to `Do=merge` (sha-stable) to avoid unverified rewritten commits.
  - Restart throttling/sleeps to prevent burning retry budget under master churn.
  - Persistent clone management (`ensure_repo`: fetch/reset/clean with fresh-clone fallback on corruption).
  - Cooperative SIGTERM/SIGINT stop propagation through long CI polling.
  - Explicit claim lifecycle for `auto/claimed-merge`:
    - claim/release comments with TTL,
    - expired-claim sweep,
    - operational labels on release (`auto/ci-timeout`, `auto/restart-throttled`,
      `auto/needs-implementer`, `auto/needs-conflict-resolution`).
  - Claim-marker interoperability: sweep recognizes both driver and `claim_pr.ts` markers.
  - Hardened API semantics: split idempotent GET retries vs state-change semantics.
  - Remove token-in-URL clone pattern; use git `http.extraheader` auth instead.
  - Adopt structured module logging + env-configurable levels.

- Add/expand invariant auditor in `tools/verify_invariant.py`:
  - Non-zero exit when violations exist (cron/alert correctness).
  - Robust auto-close routine using forward patch applicability on current `origin/master`.
  - Merge-bot commit validation now checks:
    - required CI contexts passed,
    - associated PR has non-dismissed APPROVED review.
  - Improve close-path wording/docs to match forward-apply algorithm.
  - Add logger-based output and verbosity controls.

- Add operational setup/audit tooling:
  - `tools/forgejo_audit.py` (preconditions/audit report).
  - `tools/setup_auto_labels.py` (idempotent `auto/*` label provisioning).
  - `tools/setup_branch_protection.py` (direct-push allow-list enforcement).
  - `tools/audit_branch_protection.py` (dismiss_stale_approvals audit/flip support).
  - `tools/migrate_to_new_driver.py` (claim/schedule/train cleanup migration).
  - `tools/flag_stale_prs.py` (idle PR triage flow).
  - `tools/local_ci_gate.sh` canonical local gate runner with `--continue-on-fail`.

- Add claim orchestration support in skills scripts:
  - New `claim_pr.ts` helper (claim/release + TTL comments).
  - `list_prs.ts` gains `--exclude-claimed` filter.
  - Update script reference docs accordingly.

- Telemetry/schema upgrades in `tools/_forgejo_cache.py`:
  - Add `merge_cycle`, `ci_gate_events`, `llm_activity`.
  - Add batched `ci_gate_events` insertion API with rollback semantics.
  - Ensure `bisect_depth` default handling is safe.
  - Surface merge-driver telemetry in velocity reporting pipeline.

- Agent prompt/behavior updates:
  - Review supervisor idle loop tuned (300s -> 60s).
  - Review worker cycle cap/escalation behavior refined.
  - Task implementor guidance updated to use local CI gate wrapper.

- Documentation and operational guidance:
  - Expand `AGENTS.md` with merge invariant runbook, label registry, tool links,
    and full merge-driver env var catalog (including logging/restart/claim TTL knobs).
  - Update `CHANGELOG.md` with implementation and hardening entries, plus deferred TS-test note.

- Repo hygiene:
  - Correct `.gitignore` to stop blanket ignoring `tools/*`; keep only generated artifacts ignored.

Testing/validation:
- Add comprehensive unit suite under `tests/auto_agents/` covering:
  - merge driver recursion/restarts/409 paths/signal handling/claim sweeps,
  - verifier auto-close logic with real local git fixtures,
  - schema migration + telemetry batch writes,
  - branch protection and setup/audit helpers.
- Current result: `100 passed` in `tests/auto_agents/`.
2026-05-03 21:31:16 -04:00

119 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""Sync the local Forgejo cache at ``tools/.cache/forgejo.sqlite``.
This tool is the human-facing wrapper around ``_forgejo_cache.ForgejoCache``.
On a cold cache it pulls the full master commit history and every PR the
repo has ever had. On a warm cache it pulls only deltas (new commits since
last sync, PRs whose ``updated_at`` has advanced).
Usage
-----
Typical incremental refresh::
python3 tools/forgejo-sync.py
Seed a fresh cache from scratch (destructive)::
python3 tools/forgejo-sync.py --full
After any sync, back-fill ``merged_by`` details for every merged PR so
downstream reports don't have to fetch per-PR detail on read::
python3 tools/forgejo-sync.py --backfill
Show cache statistics::
python3 tools/forgejo-sync.py --stats
The cache file lives under ``tools/.cache/`` and is gitignored.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
# Ensure we can import the sibling cache module regardless of cwd.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _forgejo_cache import ( # noqa: E402
ForgejoCache,
DEFAULT_CACHE_PATH,
)
def _load_token() -> str:
token = os.environ.get("GITEA_TOKEN")
if token:
return token.strip().strip('"').strip("'")
repo_root = Path(__file__).resolve().parent.parent
for envfile in (repo_root / ".devcontainer" / ".env", repo_root / ".env"):
if not envfile.is_file():
continue
for line in envfile.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, _, v = line.partition("=")
if k.strip() == "GITEA_TOKEN":
return v.strip().strip('"').strip("'")
sys.exit(
"ERROR: GITEA_TOKEN not found. Set it in the environment "
"or in .devcontainer/.env (or .env)."
)
def _parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Sync the local Forgejo cache (SQLite at tools/.cache/forgejo.sqlite).",
)
p.add_argument(
"--full", action="store_true",
help="Truncate commits & pulls tables and resync from scratch. "
"Use after a force-push to master or to rebuild from clean state.",
)
p.add_argument(
"--backfill", action="store_true",
help="After syncing, fetch PR detail for every closed+merged PR missing "
"merged_by (one-shot per PR, permanent). Adds a few minutes on a "
"cold cache but makes subsequent reports offline-instant.",
)
p.add_argument(
"--stats", action="store_true",
help="Print cache statistics and exit; do not sync.",
)
p.add_argument(
"--cache", type=Path, default=DEFAULT_CACHE_PATH,
help=f"Cache file path (default: {DEFAULT_CACHE_PATH}).",
)
p.add_argument(
"--quiet", action="store_true",
help="Suppress progress logs; summary JSON is still printed.",
)
return p.parse_args()
def main() -> None:
args = _parse_args()
cache = ForgejoCache.open(args.cache)
if args.stats:
print(json.dumps(cache.stats(), indent=2))
return
token = _load_token()
summary = cache.sync(
token,
full=args.full,
backfill_details=args.backfill,
progress=not args.quiet,
)
summary["cache"] = cache.stats()
print(json.dumps(summary, indent=2))
if __name__ == "__main__":
main()