feat: stuck detection — alerts when same files repeat 3x
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
"""Detect if autoresearch experiments are stuck on the same files."""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from log_append import read_entries
|
||||||
|
|
||||||
|
|
||||||
|
def is_stuck(project: str, entries: list, window: int = 3) -> bool:
|
||||||
|
"""Return True if the last `window` entries for `project` all touch the same non-empty set of files."""
|
||||||
|
project_entries = [e for e in entries if e.get('project') == project]
|
||||||
|
if len(project_entries) < window:
|
||||||
|
return False
|
||||||
|
recent = project_entries[-window:]
|
||||||
|
file_sets = [frozenset(e.get('files_changed') or []) for e in recent]
|
||||||
|
# Empty file sets don't count as stuck
|
||||||
|
if file_sets[0] == frozenset():
|
||||||
|
return False
|
||||||
|
return len(set(file_sets)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
project = sys.argv[1] if len(sys.argv) > 1 else ''
|
||||||
|
if not project:
|
||||||
|
print("Usage: stuck_check.py <project-name>", file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
entries = read_entries()
|
||||||
|
if is_stuck(project, entries):
|
||||||
|
print(f"STUCK: project '{project}' has touched the same files 3 times in a row")
|
||||||
|
sys.exit(1)
|
||||||
|
print("ok")
|
||||||
|
sys.exit(0)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'bin')
|
||||||
|
from stuck_check import is_stuck
|
||||||
|
|
||||||
|
def test_not_stuck_with_different_files():
|
||||||
|
entries = [
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['b.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['c.go']},
|
||||||
|
]
|
||||||
|
assert is_stuck('x', entries) is False
|
||||||
|
|
||||||
|
def test_stuck_when_same_files_3_times():
|
||||||
|
entries = [
|
||||||
|
{'project': 'x', 'files_changed': ['a.go', 'b.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go', 'b.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go', 'b.go']},
|
||||||
|
]
|
||||||
|
assert is_stuck('x', entries) is True
|
||||||
|
|
||||||
|
def test_not_stuck_with_fewer_than_3_entries():
|
||||||
|
entries = [
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
]
|
||||||
|
assert is_stuck('x', entries) is False
|
||||||
|
|
||||||
|
def test_only_checks_matching_project():
|
||||||
|
entries = [
|
||||||
|
{'project': 'y', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'y', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'y', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
]
|
||||||
|
assert is_stuck('x', entries) is False # only 2 entries for x
|
||||||
|
|
||||||
|
def test_not_stuck_when_files_empty():
|
||||||
|
"""Empty file sets don't count as stuck — agent may not have changed anything."""
|
||||||
|
entries = [
|
||||||
|
{'project': 'x', 'files_changed': []},
|
||||||
|
{'project': 'x', 'files_changed': []},
|
||||||
|
{'project': 'x', 'files_changed': []},
|
||||||
|
]
|
||||||
|
assert is_stuck('x', entries) is False
|
||||||
|
|
||||||
|
def test_order_matters_only_last_3():
|
||||||
|
"""Only the last 3 entries matter."""
|
||||||
|
entries = [
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['a.go']},
|
||||||
|
{'project': 'x', 'files_changed': ['b.go']}, # most recent: different
|
||||||
|
]
|
||||||
|
assert is_stuck('x', entries) is False
|
||||||
Reference in New Issue
Block a user