Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,18 @@ for the schema.
Every campaign should declare ``locked_parameters`` (and, when
applicable, ``locked_workload``) for every knob whose deviation
would invalidate the experiment. The validator hard-fails any
bundle whose ``experiment_spec.verified_parameters`` deviates from
bundle whose ``experiment_spec.verified_parameters`` *deviates from*
``locked_parameters`` — regardless of ``--auto-approve``. This
closes the spec-fidelity gap that allowed paper-memorytime-mirage
iter-1 to silently rewrite four locked workload parameters.

Issue #298 refines the "missing key" case: a locked key that the
bundle simply *omits* is not a violation (the campaign is the source
of truth), so the validator **auto-populates** it into
``verified_parameters`` and persists the bundle, rather than
hard-failing. Only a value the bundle sets *differently* from the
campaign fails.

Authoring discipline lives in ``docs/campaign-authoring-guide.md``
(the "what to lock" inventory + the rehearsal-as-instrument
worked example). The full friction-report resolution map is in
Expand Down
19 changes: 19 additions & 0 deletions docs/campaign-authoring-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@ campaign's intent, regardless of ``--auto-approve``. Use them
liberally — they are the cheapest defense against silent design-
agent rewrites.

**Omission vs deviation (#298).** The design gate distinguishes two
cases for a ``locked_parameters`` key:

- **Omitted** — the bundle's ``experiment_spec.verified_parameters``
simply doesn't echo the key. No information is lost (the campaign
already declares it), so the gate **auto-populates** the value from
``locked_parameters`` and continues, surfacing a ``WARN`` at the
design gate that names each injected key (so the fill is recorded,
not silent). The filled bundle is persisted back to ``bundle.yaml``
only when the gate passes — a failed gate leaves the file exactly as
the agent authored it. Agents no longer have to copy every locked key
verbatim just to clear the gate.
- **Deviating** — the bundle sets a *different* value than the
campaign locked. This is the real spec-fidelity violation and still
hard-fails, listing every deviation in one shot.

So the campaign is always the source of truth for a locked value; the
only way to fail is to actively contradict it.

## Reproducibility (#262 / F17)

nous auto-captures ``reproducibility_metadata`` at INIT (target
Expand Down
2 changes: 1 addition & 1 deletion docs/friction-245-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ implementation in one hop.

| F | Issue | Severity | Resolution | Tests |
|---|---|---|---|---|
| F1 | [#246](https://github.com/AI-native-Systems-Research/agentic-strategy-evolution/issues/246) | HIGH | `campaign.locked_parameters` schema field + `_validate_locked_parameters` in `validate.py` (hard-fails regardless of `--auto-approve`) | `tests/test_friction_245.py::test_f1_*` |
| F1 | [#246](https://github.com/AI-native-Systems-Research/agentic-strategy-evolution/issues/246) | HIGH | `campaign.locked_parameters` schema field + `_validate_locked_parameters` in `validate.py` (hard-fails on *deviation* regardless of `--auto-approve`; #298 auto-populates *omitted* keys from the campaign and persists them, rather than failing) | `tests/test_friction_245.py::test_f1_*`, `::test_298_*` |
| F2 | [#247](https://github.com/AI-native-Systems-Research/agentic-strategy-evolution/issues/247) | MED-HIGH | "Source-of-truth hierarchy" section added to `prompts/methodology/design.md` with worked example | (prompt-text — no behavioral test) |
| F3 | [#248](https://github.com/AI-native-Systems-Research/agentic-strategy-evolution/issues/248) | MED | `rehearsal_subset.depth_overrides` + `invalidates_checks` schema; `_validate_depth_overrides` enforces non-empty `invalidates_checks` whenever any depth payload is set; methodology prompt explains the breadth-vs-depth distinction | `tests/test_friction_245.py::test_f3_*` |
| F4 | [#249](https://github.com/AI-native-Systems-Research/agentic-strategy-evolution/issues/249) | HIGH | `compute_campaign_spec_diff` in `validate.py` + `_augment_summary_with_spec_diff` in `iteration.py` writes `campaign_spec_diff` block into every `gate_summary_design.json`, regardless of `--auto-approve`. `nous status` surfaces it | `tests/test_friction_245.py::test_f4_*` |
Expand Down
141 changes: 119 additions & 22 deletions orchestrator/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
python -m orchestrator.validate meta-findings --dir <work_dir>/
"""
import argparse
import copy
import json
import sys
from pathlib import Path

import jsonschema
import yaml

from orchestrator.util import atomic_write

SCHEMAS_DIR = Path(__file__).resolve().parent / "schemas"


Expand Down Expand Up @@ -211,7 +214,7 @@ def validate_principles_have_empirical_content(
def _validate_locked_parameters(
bundle: dict, campaign: dict | None,
) -> list[str]:
"""Issue #246 (F1): hard-fail when bundle deviates from campaign.locked_parameters.
"""Issue #246 (F1): hard-fail when bundle *deviates* from campaign.locked_parameters.

Closes the spec-fidelity gap left by HUMAN_DESIGN_GATE bypass under
--auto-approve. The bundle's ``experiment_spec.verified_parameters``
Expand All @@ -222,41 +225,88 @@ def _validate_locked_parameters(
guess (paper-memorytime-mirage iter-1: ``model``, ``concurrency``,
``duration``, ``warmup`` all overwritten).

The error message lists EVERY deviation in one shot, not just the
first — so a single re-run of the gate sees the full diff.
#298: distinguish two very different cases that #246 originally
conflated —

* **Omitted key** — the agent simply didn't echo a value the
campaign already declares. No information is lost, so this is not a
spec-fidelity violation. The campaign is the unambiguous source of
truth, so we **auto-populate** ``verified_parameters[k]`` from
``campaign.locked_parameters[k]`` (mutating ``bundle`` in place so
the caller can persist it) and emit a ``WARN:``-prefixed advisory
naming the injected keys — so the auto-fill is recorded at the gate,
not silent.
* **Deviating value** — the agent wrote a *different* value than the
campaign locked. This is the real anti-mirage violation and still
hard-fails.

Returns a mixed list of hard errors and ``WARN:``-prefixed advisories;
the caller (``validate_design``) splits them the same way it splits
the physical-realism / ground-truth warnings. The hard-error message
lists EVERY deviation in one shot, not just the first — so a single
re-run of the gate sees the full diff.
"""
if not campaign:
return []
locked = campaign.get("locked_parameters")
if not isinstance(locked, dict) or not locked:
return []
spec = bundle.get("experiment_spec") or {}
verified = spec.get("verified_parameters") or {}
if not isinstance(verified, dict):

# #298: ensure a real experiment_spec / verified_parameters dict is
# attached to the bundle so auto-filled values survive in the object
# the caller holds (and can write back to disk). A present-but-wrong
# TYPE is still a hard-fail — we auto-fill omissions, not structurally
# broken bundles.
spec = bundle.get("experiment_spec")
if spec is None:
spec = bundle["experiment_spec"] = {}
elif not isinstance(spec, dict):
return [
"campaign.locked_parameters is set but "
"bundle.experiment_spec is malformed (not a mapping); "
"cannot verify spec-fidelity (#246)."
]
verified = spec.get("verified_parameters")
if verified is None:
verified = spec["verified_parameters"] = {}
elif not isinstance(verified, dict):
return [
"campaign.locked_parameters is set but "
"bundle.experiment_spec.verified_parameters is missing or "
"malformed; cannot verify spec-fidelity (#246)."
"bundle.experiment_spec.verified_parameters is malformed "
"(not a mapping); cannot verify spec-fidelity (#246)."
]

deviations: list[str] = []
autofilled: list[str] = []
for key, expected in locked.items():
if key not in verified:
deviations.append(
f" - {key}: campaign={expected!r}, bundle=<missing>"
)
verified[key] = expected # #298: auto-populate; campaign wins
autofilled.append(f"{key}={expected!r}")
continue
actual = verified[key]
if actual != expected:
deviations.append(
f" - {key}: campaign={expected!r}, bundle={actual!r}"
)
if not deviations:
return []
return [
"bundle.experiment_spec.verified_parameters deviates from "
"campaign.locked_parameters (#246/F1). Each entry must match "
"exactly:\n" + "\n".join(deviations)
]
out: list[str] = []
if deviations:
out.append(
"bundle.experiment_spec.verified_parameters deviates from "
"campaign.locked_parameters (#246/F1). Each entry must match "
"exactly:\n" + "\n".join(deviations)
)
if autofilled:
# #298: auto-fill is defensible (campaign is source of truth) but
# must not be silent — surface it as a WARN so the human gate has
# a record of which verified_parameters were gate-injected rather
# than agent-authored. WARN: prefix routes it to warnings, not
# errors (same convention as _validate_physical_realism / #85).
out.append(
"WARN: auto-populated verified_parameters from "
"campaign.locked_parameters (#298; campaign is source of "
"truth for omitted keys): " + ", ".join(autofilled)
)
return out


def _validate_locked_workload(
Expand Down Expand Up @@ -501,7 +551,15 @@ def compute_campaign_spec_diff(
locked = (campaign or {}).get("locked_parameters") or {}
if isinstance(locked, dict) and isinstance(verified, dict):
for k, expected in locked.items():
actual = verified.get(k, "<missing>")
# #298: an omitted locked key is not a spec-fidelity violation
# (the campaign is the source of truth; the hard-fail layer
# auto-populates it). Only a value that is PRESENT and DIFFERENT
# is a real deviation. This guard is self-contained — it does
# not rely on _validate_locked_parameters having run first, so
# it stays correct for a direct or out-of-order caller.
if k not in verified:
continue
actual = verified[k]
if actual != expected:
diff["locked_parameters_violations"].append(
{"param": k, "campaign": expected, "bundle": actual}
Expand Down Expand Up @@ -547,6 +605,13 @@ def validate_design(iter_dir: Path, campaign: dict | None = None) -> dict:

# bundle.yaml
bundle_path = iter_dir / "bundle.yaml"
# #298: _validate_locked_parameters may auto-populate omitted locked
# values into the bundle in place. We defer persisting that mutation
# until we know the gate PASSES — a failed gate must not leave a
# gate-rewritten artifact on disk. These carry the bundle + snapshot
# from inside the try to the persist step after the final verdict.
bundle_to_persist: dict | None = None
bundle_snapshot: dict | None = None
if not bundle_path.exists():
errors.append("bundle.yaml not found")
else:
Expand All @@ -555,9 +620,19 @@ def validate_design(iter_dir: Path, campaign: dict | None = None) -> dict:
schema = _load_yaml_schema("bundle.schema.yaml")
jsonschema.validate(bundle, schema)
errors.extend(_validate_typed_arm_fields(bundle))
# #246 (F1): locked_parameters spec-fidelity. Hard-fail under
# auto-approve too — that's the whole point.
errors.extend(_validate_locked_parameters(bundle, campaign))
# #298: deepcopy snapshot so we can detect the in-place
# auto-fill by exact structural compare (not string rendering,
# which would conflate 1 and "1").
bundle_snapshot = copy.deepcopy(bundle)
bundle_to_persist = bundle
# #246 (F1): locked_parameters spec-fidelity. Hard-fail on
# DEVIATION under auto-approve too — that's the whole point.
# #298: omitted keys auto-fill and come back as WARN advisories.
for entry in _validate_locked_parameters(bundle, campaign):
if entry.startswith("WARN:"):
warnings.append(entry)
else:
errors.append(entry)
# #265 (F20): locked_workload diff against bundle.inputs/*.yaml.
errors.extend(_validate_locked_workload(iter_dir, bundle, campaign))
# #248 (F3): depth_overrides without invalidates_checks.
Expand Down Expand Up @@ -595,6 +670,28 @@ def validate_design(iter_dir: Path, campaign: dict | None = None) -> dict:
required = _campaign_required_iter_root(campaign)
errors.extend(_check_unexpected_files(iter_dir, extensions | required))

# #298: persist the auto-filled bundle ONLY when the whole gate passes,
# and only if it was actually mutated (no spurious rewrite when every
# locked key was already present). A failing gate leaves the on-disk
# bundle exactly as the agent authored it — no gate-injected values.
if (
not errors
and bundle_to_persist is not None
and bundle_to_persist != bundle_snapshot
):
try:
atomic_write(
bundle_path, yaml.safe_dump(bundle_to_persist, sort_keys=False)
)
except OSError as exc:
# Keep the {"status", "errors"} contract instead of letting a
# raw disk error escape from a validation function.
return {
"status": "fail",
"errors": [f"failed to persist auto-filled bundle.yaml: {exc}"],
"warnings": warnings,
}

if errors:
return {"status": "fail", "errors": errors, "warnings": warnings}
return {"status": "pass", "warnings": warnings}
Expand Down
Loading
Loading