feat(cli): console entry points + the demo's stderr damping (P4 pkt. 5 og 2)

Two commands are now part of the install surface a fresh clone gets from `uv sync`:
`portfolio-optimiser` (run:main) and `portfolio-optimiser-demo` (simulation:main).
Deliberately two of five main()s — costsim/hitl/preflight stay module-invoked; every
name here is a name the freeze has to carry. Pinned against the INSTALLED distribution's
metadata, not the TOML: a [project.scripts] line that has never been synced is a claim,
not a command. Measured: stdout is byte-identical across both invocation forms.

stderr (P4 pkt. 2), the session's open decision, resolved by measurement rather than by
preference. Damped: the round-cap notice only, via a filter on the emitting logger, keyed
on the message and installed by main() — never at import, so a library consumer keeps its
own logging config. NOT damped: the two ExperimentalWarnings. They fire while the package
__init__ imports run -> agent_framework, always before simulation's own imports and under
both invocation forms, so silencing them would mean filtering warnings inside the library
package on every consumer's behalf; they are pinned in pkt. 3 instead. A console-script
wrapper was rejected for a second reason: the two forms would then write different stderr,
and a byte-fasit would pin the command rather than the program. stderr 6 -> 4 lines.

A first implementation wrapped simulation's own agent_framework import in a scoped mute.
Measurement showed it can never fire — the package __init__ has already imported
agent_framework by then — so it was removed rather than left as a green-but-dead seam.

Load-bearing MEASURED against the whole suite, five mutations all red + green control:
remove [project.scripts] · typo the target · detach the main() call · make the filter drop
everything · install the filter at import time. The typo mutation also felled a test: the
resolve-assert re-checked the expected constant against itself, and now resolves what the
distribution actually installs. 775 -> 785 passed / 4 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2bxLcCRguxXzpM4priTMn
This commit is contained in:
Kjell Tore Guttormsen 2026-08-09 15:13:09 +02:00
commit ab7f45aa95
6 changed files with 298 additions and 2 deletions

View file

@ -21,6 +21,7 @@ client is MAF-side scaffolding; it is NOT part of the framework-neutral ``shared
from __future__ import annotations
import json
import logging
import shutil
from collections.abc import Awaitable, Callable, Mapping, Sequence
from dataclasses import dataclass
@ -54,6 +55,56 @@ from portfolio_optimiser.verdicts import (
_PROJECT_ID = "BYGG-KONTOR-NORD"
# --- P4 pkt. 2: demo stderr discipline -----------------------------------------------------------
# Measured 2026-08-09, the demo wrote six stderr lines: two ``ExperimentalWarning``s from
# ``agent_framework``, two round-cap notices from the orchestrator, a blank line and the
# ``arbeidskopi:`` path. The last two are ours and stay (the path is the one deliberately
# non-deterministic value, which is exactly why it is on stderr and not on stdout).
#
# **Damped here: the round-cap notices only.** They are emitted DURING the demo's own run, by an
# event the demo deliberately provokes — the maker/checker debate is configured to run to its cap.
#
# **NOT damped: the two ``ExperimentalWarning``s — they are pinned in pkt. 3 instead.** Measured,
# not assumed: they fire while ``portfolio_optimiser/__init__.py`` imports ``run``, which imports
# ``agent_framework`` — always before this module's own import block, under BOTH invocation forms.
# Silencing them would therefore mean putting a warnings filter inside the library package, i.e.
# letting this framework decide what MAF is allowed to tell every consumer that imports it. That is
# a library-behaviour change for stderr nobody projects, days before a freeze. A wrapper that
# muted them only behind the console script was rejected for a second reason: it would make
# ``uv run portfolio-optimiser-demo`` and ``uv run python -m portfolio_optimiser.simulation`` write
# different stderr, and a byte-fasit would then pin the command rather than the program.
#
# The damping that IS here is NARROW by construction, which is the load-bearing part: a different
# orchestration warning still reaches stderr and trips the pkt. 3 pin. A filter that could only
# ever say "drop" would leave a pin that can no longer fail for the reason it exists.
#: The logger that emits the round-cap notice, read out of MAF's source (``logger.warning`` in
#: ``_base_group_chat_orchestrator``), not guessed. Logger filters apply only to the logger the
#: record was logged THROUGH — an ancestor's filters are never consulted — so this must be the
#: emitting module's own name. If MAF moves the call, the notice simply reappears on stderr and the
#: pkt. 3 pin says so; the failure mode is visible, not silent.
ROUND_CAP_LOGGER = "agent_framework_orchestrations._base_group_chat_orchestrator"
class _ExpectedRoundCapFilter(logging.Filter):
"""Drops ONLY the "round limit reached" notice, which the demo reaches by design (the
maker/checker debate runs to its cap). Keyed on the event, not on the configured number: the
cap is a demo setting, while the notice is the thing we have decided is expected."""
def filter(self, record: logging.LogRecord) -> bool:
message = record.getMessage()
return not ("reached max_rounds=" in message and "forcing completion" in message)
def quiet_expected_round_cap_notice() -> logging.Filter:
"""Install the round-cap filter on the emitting logger and return it (so a caller — a test —
can remove it again). Runtime state, installed by ``main()``: importing this module must not
reconfigure logging for a library consumer."""
installed = _ExpectedRoundCapFilter()
logging.getLogger(ROUND_CAP_LOGGER).addFilter(installed)
return installed
# --- Step 7, the LONG loop: what an expert drops into the inbox between the two runs -------------
# A SECOND marker, deliberately distinct from the persona's (Step 8, promotion). The two mechanisms
# both end in Run B's hypothesis prompt, so a single shared marker would let either path carry it
@ -673,6 +724,11 @@ def main(argv: list[str] | None = None) -> int: # pragma: no cover - console tr
import sys
import tempfile
# P4 pkt. 2: runtime half of the stderr discipline (the import-time half wrapped the
# ``agent_framework`` import above). Installed here rather than at import time so that a
# library consumer of this module keeps its own logging configuration.
quiet_expected_round_cap_notice()
work = tempfile.mkdtemp(prefix="po-sim-")
# THE call site (P4 pkt. 0): the demo runs the ANCHORED reserve — the shared bundle plus the
# cost baseline it cannot be given in place. On GO day these two lines point at the delivered