feat(cli): --mandate — a run announces what it will do, and settles for it

Krav 2: a run must be clear about what it shall do and achieve. `--mandate`
makes both ends explicit. BEFORE the first (paid) model call the run prints what
it was commissioned to do — objective, every named approach, whether its own
proposals are allowed, the scope, the caps, and which external services it will
contact. AFTER the run it settles: one row per approach, validated with the
figure, rejected with the validator's reason, or not evaluated with why.

The announcement is placed with the other refusals and above the scripted
banner, for the reason the required-args guard was hoisted there: a refused run
must not first print a banner about work it never did. `--live-dry-run`
announces without contacting anything, so a commission can be inspected before
it costs money.

`settle` renders the goal verdict it is GIVEN and never decides it. `ledger.to_ore`
is the framework's one NOK->øre conversion and the goal comparison already runs on
quantised integers, but `mandate.py` cannot import it without dragging `verdicts`
— and therefore agent_framework — into a deliberately framework-neutral module,
while a private copy of a money conversion is exactly the (p) defect. So the
caller decides and this renders; a goal figure without a decided verdict makes no
claim at all.

The caps the announcement prints come from named constants shared with
`run_project`/`run_portfolio`'s defaults — a second copy could drift and make the
announcement describe a run that never happened.

Load-bearing MEASURED against the whole 717-test suite, four mutations all red:
detach the announcement (4) · detach the settlement print (2) · make the CLI
loader tolerant (2) · announce the commission but never hand it to the run (2).
The last one is the one that matters: without it, a run could print a commission
it had no intention of executing.

DEVIATION from the approved plan, stated rather than quietly dropped: --goals is
still refused outside portfolio mode. Accepting it in single-project mode would
have admitted a flag whose documented function (the goal-stop against the ledger)
still does nothing there — the same accepted-but-inert defect --embedder-config
was just fixed for. The mandate's success_criteria carries "what shall this run
achieve" in the expert's own words instead; the numeric target stays portfolio-level.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ULCqjLF61rehj5cZmdUoR3
This commit is contained in:
Kjell Tore Guttormsen 2026-08-05 16:23:53 +02:00
commit b9d795307a
4 changed files with 388 additions and 4 deletions

View file

@ -181,3 +181,46 @@ def announce(
if mandate.success_criteria:
lines.append(f" Success: {mandate.success_criteria}")
return "\n".join(lines)
def settle(
coverage: tuple[ApproachOutcome, ...],
*,
goal_nok: float | None = None,
goal_reached: bool | None = None,
) -> str:
"""Render the settlement: what the run actually did about each commissioned approach.
Empty coverage renders an EMPTY string a run without a mandate has nothing to settle, and a
header over zero rows would imply a commission that never existed.
The ``Validated total`` is a DISPLAY figure, summed in float NOK exactly like
``PortfolioResult.sum_claimed_saving_nok``, and it decides nothing. The goal verdict is
**passed in**, never computed here: ``ledger.to_ore`` is the framework's one NOK->øre
conversion and the goal comparison already runs on quantised integers, but this module cannot
import it without pulling ``verdicts`` and with it ``agent_framework`` into a deliberately
framework-neutral file, while a private copy of a money conversion is precisely the ``(p)``
defect. So the caller decides and this renders. Both ``goal_nok`` and ``goal_reached`` must be
given for the target line to appear; a figure without a decided verdict makes no claim.
"""
if not coverage:
return ""
lines = ["Mandate outcome"]
for row in coverage:
if row.status == "validated":
amount = f"{row.saving_nok:.0f} NOK" if row.saving_nok is not None else "-"
lines.append(f" {row.id:<20} VALIDATED {amount:>14} {row.label}")
elif row.status == "rejected":
lines.append(f" {row.id:<20} REJECTED {row.detail}")
else:
lines.append(f" {row.id:<20} NOT EVALUATED {row.detail}")
total = sum(r.saving_nok or 0.0 for r in coverage if r.status == "validated")
lines.append(f" Validated total: {total:.0f} NOK")
if goal_nok is not None and goal_reached is not None:
lines.append(
f" Target: >= {goal_nok:.0f} NOK — "
f"{'reached' if goal_reached else 'not reached'}"
)
return "\n".join(lines)