feat(model-divergence): full enforcement — manifest, transition, claim, audit, loop gates, detect script
Completes all 3 model-divergence enforcement subtasks:
- scripts/detect_models.py: probes opencode.json + localhost endpoints,
builds models.json with --json/--write/--force
- scripts/status.py: CONFLICT_MATRIX, --model flag, --transition --model,
--claim --model, --audit Category 6, model-divergence brake gate in
--check-gate, helpers for manifest loading and conflict checking
- scripts/loop-runner.py: _role_model() helper + {model} passed via extras
dict to _invoke_harness for implement, verify, orchestrate roles
- tests/test_model_divergence.py: 33 tests covering all enforcement layers
- Single-LLM mode: record model advisory, no conflict check
- Multi-LLM mode (2+ models): conflict matrix enforced at transition, claim,
and loop brake gate
- Project-level models.json preferred over global ~/.automaton/models.json
This commit is contained in:
+54
-15
@@ -375,6 +375,9 @@ def _invoke_harness(
|
||||
"""Build the harness command from loop.json and invoke it. Returns stdout.
|
||||
|
||||
extras: substitution tokens specific to this role ({artifact}, {verdict}, etc).
|
||||
If extras contains a "model" key, the ``{model}`` token in the harness
|
||||
command is substituted. The caller is responsible for passing the model
|
||||
via extras (extracted from loop.json role config or manifest default).
|
||||
"""
|
||||
resolved_prompt = prompt_path
|
||||
if loop_path is not None:
|
||||
@@ -524,6 +527,24 @@ def _role_prompt(cfg: dict, role: str) ->Optional[str]:
|
||||
return role_cfg.get("prompt")
|
||||
|
||||
|
||||
def _role_model(cfg: dict, role: str) -> Optional[str]:
|
||||
"""Get the model configured for a role in loop.json, or the manifest default."""
|
||||
roles = cfg.get("roles") or {}
|
||||
role_cfg = roles.get(role) or {}
|
||||
model = role_cfg.get("model")
|
||||
if model:
|
||||
return model
|
||||
models_file = AUTOMATON_DIR / "models.json"
|
||||
if models_file.exists():
|
||||
try:
|
||||
import json as _mj
|
||||
manifest = _mj.loads(models_file.read_text())
|
||||
return manifest.get("default")
|
||||
except (OSError, _mj.JSONDecodeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Work sources (task add-goal-mode)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -809,27 +830,39 @@ def cmd_tick(args, runner_state: Optional[dict] = None) -> dict:
|
||||
out_dir = _outputs_dir(loop_path)
|
||||
tick_num = state.get('iteration_count', 0) + 1
|
||||
impl_output = str(out_dir / f"tick{tick_num}-implement.json")
|
||||
impl_model = _role_model(cfg, "implement")
|
||||
implement_extras = {
|
||||
"output": impl_output,
|
||||
"current_task": current_task,
|
||||
"task_brief": task_brief,
|
||||
"acceptance_criteria": acceptance,
|
||||
"next_hint": next_hint,
|
||||
}
|
||||
if impl_model:
|
||||
implement_extras["model"] = impl_model
|
||||
implement_stdout = _invoke_harness(
|
||||
harness_cfg, "implement", implement_prompt, cwd,
|
||||
extras={"output": impl_output,
|
||||
"current_task": current_task,
|
||||
"task_brief": task_brief,
|
||||
"acceptance_criteria": acceptance,
|
||||
"next_hint": next_hint},
|
||||
extras=implement_extras,
|
||||
loop_path=loop_path, tick_num=tick_num)
|
||||
(Path(impl_output)).write_text(implement_stdout)
|
||||
|
||||
# Step 6: spawn Verify
|
||||
verify_prompt = _role_prompt(cfg, "verify") or ""
|
||||
verify_output = str(out_dir / f"tick{tick_num}-verify.json")
|
||||
verify_model = _role_model(cfg, "verify")
|
||||
verify_extras = {
|
||||
"output": verify_output,
|
||||
"artifact": impl_output,
|
||||
"current_task": current_task,
|
||||
"task_brief": task_brief,
|
||||
"acceptance_criteria": acceptance,
|
||||
"next_hint": next_hint,
|
||||
}
|
||||
if verify_model:
|
||||
verify_extras["model"] = verify_model
|
||||
verify_stdout = _invoke_harness(
|
||||
harness_cfg, "verify", verify_prompt, cwd,
|
||||
extras={"output": verify_output,
|
||||
"artifact": impl_output,
|
||||
"current_task": current_task,
|
||||
"task_brief": task_brief,
|
||||
"acceptance_criteria": acceptance,
|
||||
"next_hint": next_hint},
|
||||
extras=verify_extras,
|
||||
loop_path=loop_path, tick_num=tick_num)
|
||||
(Path(verify_output)).write_text(verify_stdout)
|
||||
|
||||
@@ -854,12 +887,18 @@ def cmd_tick(args, runner_state: Optional[dict] = None) -> dict:
|
||||
# Step 9: spawn Orchestrate
|
||||
orch_prompt = _role_prompt(cfg, "orchestrate") or ""
|
||||
orch_output = str(out_dir / f"tick{tick_num}-orchestrate.json")
|
||||
orch_model = _role_model(cfg, "orchestrate")
|
||||
orch_extras = {
|
||||
"output": orch_output,
|
||||
"verdict": json.dumps(verdict),
|
||||
"current_task": current_task,
|
||||
"current_phase": state.get("current_phase", ""),
|
||||
}
|
||||
if orch_model:
|
||||
orch_extras["model"] = orch_model
|
||||
orch_stdout = _invoke_harness(
|
||||
harness_cfg, "orchestrate", orch_prompt, cwd,
|
||||
extras={"output": orch_output,
|
||||
"verdict": json.dumps(verdict),
|
||||
"current_task": current_task,
|
||||
"current_phase": state.get("current_phase", "")},
|
||||
extras=orch_extras,
|
||||
loop_path=loop_path, tick_num=tick_num)
|
||||
(Path(orch_output)).write_text(orch_stdout)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user