diff --git a/src/agentdiff/cli.py b/src/agentdiff/cli.py
index d8b19c8e..2b51b20d 100644
--- a/src/agentdiff/cli.py
+++ b/src/agentdiff/cli.py
@@ -8,11 +8,11 @@
from agentdiff.ci.baseline import decide_rotation
from agentdiff.ci.github import post_pr_comment
-from agentdiff.config import AgentDiffConfig, load_config
+from agentdiff.config import AgentDiffConfig, find_config_file, load_config
from agentdiff.engine.comparator import compare
from agentdiff.engine.explanations import format_explanations, locate_culprit
from agentdiff.engine.tree import render_tree
-from agentdiff.governance import diff_gate_thresholds
+from agentdiff.governance import diff_gate_thresholds, provenance_line
from agentdiff.loader import load_trace
from agentdiff.models.step import StepStatus
from agentdiff.recorder import record_run, save_trace
@@ -189,6 +189,7 @@ def diff(
):
"""Compares baseline and candidate agent trajectories."""
cfg = load_config(config)
+ config_source = config or (str(find_config_file()) if find_config_file() else None)
threshold_changes: list = []
if baseline_config:
try:
@@ -197,6 +198,7 @@ def diff(
typer.echo(f"Baseline config not found: {e}", err=True)
sys.exit(2)
threshold_changes = diff_gate_thresholds(baseline_cfg, cfg)
+ gate_provenance = provenance_line(cfg, config_source)
values = _resolve_cli(
cfg,
adapter=adapter,
@@ -282,10 +284,11 @@ def diff(
# For console printing we write directly, but we can capture it or format differently if output_file is active
if output_file:
# If writing terminal format to file, output the text summary representation
- output_content = report.summary()
+ output_content = report.summary() + f"\n{gate_provenance}\n"
else:
- print_report(report)
+ print_report(report, gate_provenance=gate_provenance)
elif format.lower() == "json":
+ report.gate_provenance = gate_provenance
output_content = report.model_dump_json(indent=2)
if not output_file:
typer.echo(output_content)
@@ -301,6 +304,7 @@ def diff(
max_cost_delta=max_cost_delta,
max_recovery_ratio=max_recovery_ratio,
threshold_changes=threshold_changes,
+ gate_provenance=gate_provenance,
)
if not output_file:
typer.echo(output_content)
@@ -332,6 +336,7 @@ def diff(
max_cost_delta=max_cost_delta,
max_recovery_ratio=max_recovery_ratio,
threshold_changes=threshold_changes,
+ gate_provenance=gate_provenance,
)
comment = post_pr_comment(body, pr)
url = comment.get("html_url")
diff --git a/src/agentdiff/governance.py b/src/agentdiff/governance.py
index 8fee372d..5fad48c3 100644
--- a/src/agentdiff/governance.py
+++ b/src/agentdiff/governance.py
@@ -62,3 +62,25 @@ def diff_gate_thresholds(
for key in _GATED_KEYS
if old_gates[key] != new_gates[key]
]
+
+
+def provenance_line(cfg: AgentDiffConfig, config_path: str | None) -> str:
+ """G7 — one-line, self-describing gate summary for any report.
+
+ Names the active thresholds and where they came from, so every diff
+ answers "what rules judged me?" without opening the config.
+ """
+ gates = effective_gates(cfg)
+ source = (
+ f"agentdiff.toml ({config_path})"
+ if config_path
+ else "defaults (no agentdiff.toml found)"
+ )
+ parts = [
+ f"max_divergence={gates['max_divergence']}",
+ f"max_loops={gates['max_loops']}",
+ f"max_cost_delta={gates['max_cost_delta']}%",
+ ]
+ if gates["max_recovery_ratio"] is not None:
+ parts.append(f"max_recovery_ratio={gates['max_recovery_ratio']}")
+ return f"Gate: {', '.join(parts)} — source: {source}"
diff --git a/src/agentdiff/models/report.py b/src/agentdiff/models/report.py
index dc83ecc5..73673efc 100644
--- a/src/agentdiff/models/report.py
+++ b/src/agentdiff/models/report.py
@@ -37,6 +37,11 @@ class DiffReport(BaseModel):
recovery_step_ratio: float = 0.0
step_diffs: list[StepDiff] = Field(default_factory=list)
passed: bool = True
+ gate_provenance: str | None = Field(
+ default=None,
+ description="G7: one-line summary of active gate thresholds and their "
+ "source (agentdiff.toml path or defaults). Populated by the CLI.",
+ )
def summary(self) -> str:
"""Returns a string summarizing the comparison report."""
diff --git a/src/agentdiff/reporters/pr.py b/src/agentdiff/reporters/pr.py
index cd8b18e7..f48b68e0 100644
--- a/src/agentdiff/reporters/pr.py
+++ b/src/agentdiff/reporters/pr.py
@@ -16,6 +16,7 @@ def generate_pr_markdown(
max_cost_delta: float = DEFAULT_MAX_COST_DELTA,
max_recovery_ratio: float | None = None,
threshold_changes: list[ThresholdChange] | None = None,
+ gate_provenance: str | None = None,
) -> str:
"""Renders a compact, PR-ready markdown comment.
@@ -93,4 +94,7 @@ def generate_pr_markdown(
)
lines.append("")
+ if gate_provenance:
+ lines.append(f"{gate_provenance}")
+
return "\n".join(lines)
diff --git a/src/agentdiff/reporters/terminal.py b/src/agentdiff/reporters/terminal.py
index 3d4315ab..58ad8132 100644
--- a/src/agentdiff/reporters/terminal.py
+++ b/src/agentdiff/reporters/terminal.py
@@ -63,8 +63,13 @@ def render_diff_table(report: DiffReport) -> Table:
return table
-def print_report(report: DiffReport):
- """Outputs the complete DiffReport to the terminal."""
+def print_report(report: DiffReport, gate_provenance: str | None = None):
+ """Outputs the complete DiffReport to the terminal.
+
+ ``gate_provenance`` (G7) appends a one-line, self-describing gate summary
+ — active thresholds and their source — so the report answers "what rules
+ judged me?" without opening the config.
+ """
console = Console()
status_str = (
@@ -87,6 +92,8 @@ def print_report(report: DiffReport):
f" • Token Delta: {report.token_delta_percentage:+.2f}%\n"
f" • Cost Delta: {report.cost_delta_percentage:+.2f}%"
)
+ if gate_provenance:
+ summary_text += f"\n\n[dim]{gate_provenance}[/dim]"
panel = Panel(
summary_text,
diff --git a/tests/test_governance.py b/tests/test_governance.py
index 91a5cb39..810cb2b7 100644
--- a/tests/test_governance.py
+++ b/tests/test_governance.py
@@ -205,3 +205,69 @@ def test_missing_baseline_config_file_exits_2(self, tmp_path):
],
)
assert result.exit_code == 2
+
+
+class TestGateProvenance:
+ """G7 — every report self-describes its active thresholds + source."""
+
+ def test_provenance_line_defaults(self):
+ from agentdiff.governance import provenance_line
+
+ line = provenance_line(AgentDiffConfig(), None)
+ assert "max_divergence=0.3" in line
+ assert "max_loops=0" in line
+ assert "max_cost_delta=10.0%" in line
+ assert "source: defaults (no agentdiff.toml found)" in line
+
+ def test_provenance_line_with_path(self, tmp_path):
+ from agentdiff.governance import provenance_line
+
+ cfg_file = tmp_path / "agentdiff.toml"
+ cfg_file.write_text("[cli]\nmax_divergence = 0.4\nmax_recovery_ratio = 1.5\n")
+ line = provenance_line(load_config(cfg_file), str(cfg_file))
+ assert "max_divergence=0.4" in line
+ assert "max_recovery_ratio=1.5" in line # opt-in gate appears when set
+ assert f"source: agentdiff.toml ({cfg_file})" in line
+
+ def test_terminal_format_includes_provenance(self, tmp_path):
+ base, cand = TestCLIBaselineConfig()._traces(tmp_path)
+ result = runner.invoke(
+ __import__("agentdiff.cli", fromlist=["app"]).app,
+ ["diff", str(base), str(cand)],
+ )
+ assert result.exit_code == 0
+ assert "Gate: max_divergence=0.3" in result.output
+ assert (
+ "defaults" in result.output
+ ) # rich wraps long lines; assert words, not phrases
+
+ def test_json_format_includes_provenance(self, tmp_path):
+ base, cand = TestCLIBaselineConfig()._traces(tmp_path)
+ result = runner.invoke(
+ __import__("agentdiff.cli", fromlist=["app"]).app,
+ ["diff", str(base), str(cand), "--format", "json"],
+ )
+ assert result.exit_code == 0
+ data = json.loads(result.output)
+ assert "Gate: max_divergence=0.3" in data["gate_provenance"]
+
+ def test_pr_format_includes_provenance_footer(self, tmp_path):
+ base, cand = TestCLIBaselineConfig()._traces(tmp_path)
+ result = runner.invoke(
+ __import__("agentdiff.cli", fromlist=["app"]).app,
+ ["diff", str(base), str(cand), "--format", "pr"],
+ )
+ assert result.exit_code == 0
+ assert "Gate: max_divergence=0.3" in result.output
+
+ def test_explicit_config_path_shown_as_source(self, tmp_path):
+ base, cand = TestCLIBaselineConfig()._traces(tmp_path)
+ cfg_file = tmp_path / "agentdiff.toml"
+ cfg_file.write_text("[cli]\nmax_divergence = 0.45\n")
+ result = runner.invoke(
+ __import__("agentdiff.cli", fromlist=["app"]).app,
+ ["diff", str(base), str(cand), "--config", str(cfg_file)],
+ )
+ assert result.exit_code == 0
+ assert "max_divergence=0.45" in result.output
+ assert "agentdiff.toml" in result.output # path may wrap; filename stays whole
diff --git a/website/docs/03-Guides/05-Configuration.md b/website/docs/03-Guides/05-Configuration.md
index cb68dcfa..2ce411e5 100644
--- a/website/docs/03-Guides/05-Configuration.md
+++ b/website/docs/03-Guides/05-Configuration.md
@@ -66,6 +66,11 @@ The PR comment renders a warning block above the gate table:
The same summary prints with `--explain`. Loosening a gate is now as
visible in review as the code it guards.
+Every report also self-describes its rules: a one-line gate provenance
+(`Gate: max_divergence=0.3, max_loops=0, ... - source: agentdiff.toml`)
+appears in the terminal summary, the JSON (`gate_provenance` field), and
+the PR comment footer - so a diff always answers "what rules judged me?".
+
## From the SDK
You can load config programmatically with `load_config()`, which returns an