Skip to content

Commit 2ef513a

Browse files
fix(gate): hollow-results guard + bound the execution outer loop (both crossings were fake)
STATUS AFTER A DAY: 449 projects at in_progress, 2 "crossed" — and BOTH crossings were hollow. The gate let them through and the fleet was starving. 1. HOLLOW RESULTS (execution/hollow_guard.py — new deterministic gate) The gate's bar was `bool(produced)`: "did a file appear?". It NEVER looked inside. PROJ-179 ("The Influence of Metacognitive Awareness") reached research_complete having run on the IRIS FLOWER dataset (150 rows, 105/45 split) and written: primary_analysis.json {"correlation_coefficient": null, "p_value": null} correlation_metrics.json {"d_prime": NaN} robustness_analysis.json [] validation_report.json {"status": "PASS"} <- self-certified Its confidence_rating column — the study's KEY measure — was blank in all 150 rows. fabrication_guard catches numbers that were FAKED; this catches numbers that were never COMPUTED. Both mean "not a real measurement" and both now hard-fail the gate. Tuned for HIGH PRECISION: 0.0 and negatives are real values and are never flagged. 2. NO DURABLE EVIDENCE (same module) PROJ-256 advanced to research_review on ONE artifact — data/processed/null_fpr_metrics.json — which is GITIGNORED. Nothing was committed: no result a reviewer can open, nothing a paper can cite. Its entire empirical contribution had already evaporated. A run whose every artifact is gitignored now fails. Durability is decided by git itself (the .gitignore stays the SSoT). Both projects are reset to in_progress with the truth recorded. Honest count: 0. 3. UNBOUNDED OUTER LOOP (state/execution_status.py + graph.py) reset_fix_loop wiped BOTH fix_rounds and model_tier, so a project whose analysis cannot run climbed the whole ladder (12 rounds x every tier), re-planned to a clean slate, and climbed it AGAIN — forever, with no memory. Two monotonic counters now survive the resets: `total_attempts` and `replan_rounds`. Past MAX_REPLAN_ROUNDS the honest outcome is terminal VALIDATOR_REJECTED — the analysis is not executable here — not another lap. (Still autonomous; never human_input_needed.) 4. CHURNERS MONOPOLISED THE MATRIX (scheduler.py) A failed execution re-opens only a handful of tasks, so a hopeless project sits at ~6 remaining FOREVER and shortest-remaining-first re-picked it every tick. PROJ-029 took 843 implementer calls in ONE day; 12 churners ate ~4,000 of the day's 5,111 while 400 projects got NONE. Distance-from-done now includes EXEC_CHURN_PENALTY * total_attempts, so churn falls behind healthy work without being banished (it still needs attempts to reach the re-plan cap). Live effect: the in_progress workers now pick healthy projects instead of the six churners. scripts/backfill_execution_churn_counters.py replays git history to seed both counters on the 36 existing records (PROJ-586: 38 attempts; PROJ-843: 33/2 replans) — without it, both new guards would have been blind to the churn already burned. 5. FLAPPING TEST (tests/unit/test_data_contract.py) It asserted against LIVE state (state/execution_status/PROJ-262.json + the live project tree), which the pipeline rewrites every tick. The evidence moved out from under it and it failed while the detector it guards was fine. The REAL evidence (404a05f) is now frozen in tests/fixtures/proj262/ — verbatim, assertions unchanged, no longer a moving target. Full offline suite: 6436 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 30f559b commit 2ef513a

41 files changed

Lines changed: 3693 additions & 15 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ruff.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ src = ["src", "tests"]
1111
# config and we can't run the project-wide lint gate.
1212
extend-exclude = [
1313
"projects/",
14+
# Frozen REAL project code captured as regression evidence (e.g.
15+
# tests/fixtures/proj262/). It is agent-generated project-side code preserved
16+
# VERBATIM — linting or reformatting it would corrupt the very evidence the
17+
# test asserts against. Same rationale as `projects/` above.
18+
"tests/fixtures/",
1419
".venv/",
1520
"build/",
1621
"dist/",
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""One-time backfill of the monotonic execution-churn counters from git history.
2+
3+
``total_attempts`` and ``replan_rounds`` were added after the fact, so every existing
4+
``state/execution_status/*.json`` reads 0 — which would blind BOTH new guards to the
5+
churn that already happened: the scheduler's anti-monopoly penalty would not fire, and
6+
a project that has already been round the ladder several times would be granted
7+
MAX_REPLAN_ROUNDS *more* full ladders before being honestly rejected.
8+
9+
Git is the exact record. Each commit of a record is one execution attempt, so we
10+
replay them:
11+
* an attempt with ``ok=false`` -> total_attempts += 1
12+
* ``fix_rounds`` dropping to 0 while the
13+
tier does NOT rise (the reset_fix_loop
14+
signature) -> replan_rounds += 1
15+
* ``ok=true`` -> both counters reset (success is clean)
16+
17+
Idempotent: re-running recomputes from history rather than accumulating. Run from the
18+
repo root: python scripts/backfill_execution_churn_counters.py [--apply]
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import json
24+
import subprocess
25+
import sys
26+
from pathlib import Path
27+
28+
STATE = Path("state/execution_status")
29+
30+
31+
def _history(path: Path) -> list[dict]:
32+
shas = subprocess.run(
33+
["git", "log", "--format=%H", "--reverse", "--", str(path)],
34+
capture_output=True, text=True, check=True,
35+
).stdout.split()
36+
out = []
37+
for sha in shas:
38+
blob = subprocess.run(
39+
["git", "show", f"{sha}:{path}"], capture_output=True, text=True
40+
).stdout
41+
try:
42+
out.append(json.loads(blob))
43+
except json.JSONDecodeError:
44+
continue
45+
return out
46+
47+
48+
def replay(revisions: list[dict]) -> tuple[int, int]:
49+
"""-> (total_attempts, replan_rounds) reconstructed from the record's history."""
50+
attempts = replans = 0
51+
prev_rounds = prev_tier = 0
52+
for rec in revisions:
53+
rounds = rec.get("fix_rounds") or 0
54+
tier = rec.get("model_tier") or 0
55+
if rec.get("ok") is True:
56+
attempts = replans = 0 # a clean run wipes the churn
57+
else:
58+
if rounds > prev_rounds:
59+
attempts += rounds - prev_rounds
60+
elif rounds < prev_rounds and tier <= prev_tier:
61+
# fix_rounds fell WITHOUT a tier bump => reset_fix_loop => a re-plan.
62+
replans += 1
63+
attempts += rounds # the rounds accrued since the reset
64+
prev_rounds, prev_tier = rounds, tier
65+
return attempts, replans
66+
67+
68+
def main() -> int:
69+
apply = "--apply" in sys.argv
70+
changed = 0
71+
for path in sorted(STATE.glob("*.json")):
72+
revs = _history(path)
73+
if not revs:
74+
continue
75+
attempts, replans = replay(revs)
76+
rec = json.loads(path.read_text(encoding="utf-8"))
77+
if rec.get("ok") is True:
78+
attempts = replans = 0
79+
if rec.get("total_attempts") == attempts and rec.get("replan_rounds") == replans:
80+
continue
81+
print(
82+
f"{path.stem[:46]:48s} attempts={attempts:3d} replans={replans}"
83+
f" (fix_rounds={rec.get('fix_rounds')} tier={rec.get('model_tier')})"
84+
)
85+
changed += 1
86+
if apply:
87+
rec["total_attempts"] = attempts
88+
rec["replan_rounds"] = replans
89+
path.write_text(json.dumps(rec, indent=2) + "\n", encoding="utf-8")
90+
print(f"\n{changed} record(s) {'updated' if apply else 'would change (dry run)'}")
91+
return 0
92+
93+
94+
if __name__ == "__main__":
95+
raise SystemExit(main())

src/llmxive/agents/lifecycle.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,16 @@
105105
# deterministic report) instead of escalating to a human (autonomous
106106
# exhaustion handling). HUMAN_INPUT_NEEDED is retained only for unrelated
107107
# legacy markers — the execution path never routes there anymore.
108+
# VALIDATOR_REJECTED is the terminal for a project whose analysis cannot be
109+
# RUN here at all: it has exhausted every model tier AND every re-plan
110+
# (MAX_REPLAN_ROUNDS full ladders). Re-planning it again would be an unbounded
111+
# loop — which is exactly what it was, burning the worker matrix on a project
112+
# that can never produce a real result. Rejecting is the honest outcome, and it
113+
# is still autonomous (never HUMAN_INPUT_NEEDED).
108114
Stage.IN_PROGRESS: {
109115
Stage.RESEARCH_COMPLETE, Stage.IN_PROGRESS,
110116
Stage.PLANNED, Stage.HUMAN_INPUT_NEEDED,
117+
Stage.VALIDATOR_REJECTED,
111118
},
112119
# research_complete is now a brief checkpoint where the
113120
# specialist reviewers run before research_review, so we allow

src/llmxive/execution/analysis_runner.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class AnalysisRunResult:
6666
deadline_exceeded: bool = False
6767
reason: str = ""
6868
fabrication: list[str] = field(default_factory=list) # deterministic fabricated-result findings
69+
hollow: list[str] = field(default_factory=list) # results that were never COMPUTED (null/NaN/empty)
6970

7071

7172
def extract_run_commands(quickstart_text: str) -> list[str]:
@@ -327,21 +328,48 @@ def run_analysis(
327328
# reaches research_complete and only the LLM panel catches it. A non-empty
328329
# finding hard-fails the gate → kickback to implementation for a REAL run.
329330
from llmxive.execution.fabrication_guard import find_fabrication
331+
from llmxive.execution.hollow_guard import (
332+
find_hollow_results,
333+
find_no_durable_evidence,
334+
)
330335

331336
fabrication = find_fabrication(project_dir)
337+
# DETERMINISTIC hollow-results gate: `bool(produced)` only asks "did a file
338+
# appear?" — it never looks INSIDE. fabrication_guard catches numbers that were
339+
# FAKED; this catches numbers that were never COMPUTED. PROJ-179 (metacognitive
340+
# awareness, run on the IRIS FLOWER dataset) reached research_complete having
341+
# written correlation=null, p=null, d_prime=NaN, robustness=[] and its own
342+
# {"status": "PASS"}. Every headline number was missing and the gate said ok.
343+
hollow = find_hollow_results(project_dir, produced)
344+
# ...and a run whose every artifact is gitignored leaves nothing a reviewer can
345+
# open or a paper can cite (PROJ-256 advanced on ONE data/processed/*.json).
346+
# project_dir is <repo>/projects/<id>, so parents[1] is the repo whose .gitignore
347+
# decides durability (the .gitignore is the SSoT — never re-encode its patterns).
348+
undurable = find_no_durable_evidence(
349+
project_dir, produced, repo_root=project_dir.parents[1]
350+
)
332351
ok = (
333352
not deadline_exceeded
334353
and not cmd_failures
335354
and bool(produced)
336355
and not declared_missing
337356
and not fabrication
357+
and not hollow
358+
and not undurable
338359
)
339360
reason_parts: list[str] = []
340361
if fabrication:
341362
reason_parts.append(
342363
f"{len(fabrication)} fabricated/simulated-result signal(s) — results are "
343364
"not real measurements: " + "; ".join(fabrication[:3])
344365
)
366+
if hollow:
367+
reason_parts.append(
368+
f"{len(hollow)} hollow-result signal(s) — the analysis ran but computed "
369+
"nothing: " + "; ".join(hollow[:3])
370+
)
371+
if undurable:
372+
reason_parts.append(undurable[0])
345373
if deadline_exceeded:
346374
reason_parts.append(f"overall deadline {overall_deadline_s:.0f}s exceeded")
347375
if cmd_failures:
@@ -372,6 +400,7 @@ def run_analysis(
372400
deadline_exceeded=deadline_exceeded,
373401
reason="; ".join(reason_parts) or "ok",
374402
fabrication=fabrication,
403+
hollow=[*hollow, *undurable],
375404
)
376405

377406

0 commit comments

Comments
 (0)