diff --git a/bin/stuck_check.py b/bin/stuck_check.py new file mode 100644 index 0000000..418a130 --- /dev/null +++ b/bin/stuck_check.py @@ -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 ", 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) diff --git a/tests/test_stuck.py b/tests/test_stuck.py new file mode 100644 index 0000000..e335ee8 --- /dev/null +++ b/tests/test_stuck.py @@ -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