Commit Graph

1 Commits

Author SHA1 Message Date
drew 205bf56eec feat(auto-agents): cycle-cap safety mechanism + tests + hardening
R3.4 (commit 3f12e4140) added the cycle-cap integration to
_pr_classification_cache / _dispatch_runtime but the implementation
module itself (``tools/_cycle_cap.py``) was never committed — the
branch was broken at HEAD for any fresh checkout. This commit adds
the missing module, a 38-test unit suite, and three hardening
fixes addressed by review:

The mechanism (recap):
After ``CYCLE_CAP_MAX_NO_PROGRESS`` (default 5) consecutive
dispatcher cycles with an identical ``(head_sha, comment_count)``
signature, the dispatcher applies ``auto/needs-human-triage`` to
break a no-progress loop. Live-observed need: run-1 (2026-05-17)
saw PR #35 picked up ELEVEN times in a row with identical state,
burning ~$2 of LLM budget on identical work.

Hardening vs. the WIP version:

- **Storage moved out of ``/tmp``** to
  ``<repo>/.dispatcher-logs/cycle-cap/``. ``/tmp`` is tmpfs on most
  distros — a reboot would reset every PR's iteration budget,
  silently defeating the entire safety mechanism. Override via
  ``CYCLE_CAP_DIR`` for the old transient behaviour.

- **Per-PR ``fcntl.flock``** around the read-modify-write of the
  state file. Without it, two dispatcher processes on the same host
  picking up the same PR simultaneously could lose-update the
  counter (both read N, both write N+1, only one increment sticks).
  Lock lives on a sidecar ``.lock`` file so the atomic
  ``tmp+rename`` of the data file doesn't invalidate the held fd.

- **Sanity ceiling on ``CYCLE_CAP_MAX_NO_PROGRESS``** at 50. A
  typo'd ``=999999`` would otherwise silently disable the cap. The
  floor stays at 2 (a cap of 1 would fire on the first cycle and
  be useless).

Tests:
- 38 unit tests in ``tests/auto_agents/test_cycle_cap.py`` covering
  signature stability + change semantics, record_pickup increment /
  reset / disable, max_no_progress clamping at both bounds, clear()
  removing both data and lock files, label-helper shape acceptance,
  atomic-write resilience to corrupt prior state, default-storage-
  location pin (regression guard against /tmp creep), and flock
  serialisation correctness.

Plus the label substrate:

- ``setup_auto_labels.py`` registers the new
  ``auto/needs-human-triage`` label. Discovered Forgejo's silent
  500 on description > 255 chars (live-verified 2026-05-17); added
  a local guard that surfaces a useful error before the API call.

- ``test_setup_auto_labels.py`` adds the new label to the expected
  registry.

- ``test_pr_classification_cache.py`` sets ``CYCLE_CAP_DISABLE=1``
  in its fixture so tests that loop the same PR through
  ``refresh_then_filter`` multiple times don't spuriously trigger
  the cap and get triage-labeled out of the test assertions.

Finally, commits the project-shared ``.claude/settings.json`` — the
graphify-knowledge-graph reminder hook that every dev working on
this repo benefits from (the per-user ``settings.local.json`` stays
gitignored as before).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 00:05:05 -04:00