"""The shippable run entrance (C2.0): merge inbox → seed → fold → run (§3, §5). Where ``run_s10.py`` is the byte-frozen fasit of the programme's ONE live run (never imported by the suite), this module is the generic, deliverable entrance the README's inbox claim points at. The composition (``compose_run_context``) is pure config/file logic and offline-testable: it ingests the inbox READ-only (role split §3 Step 7), seeds from the bundle, and folds the retrieved verdicts into the generation context (§3 Step 1). The orchestration (``execute_run``) drives the loop under the §8 meter and persists artifacts on BOTH outcomes — a budget stop is a run outcome, not an absence of one. The model client is injected: the real SDK client is constructed only by ``default_client_factory`` on the CLI path (wired, never executed by the suite — honesty rule §1); the navigated docs dir comes from the validated startup contract, never straight from the raw argument (§10). K12 makes the whole build drivable from here: ``--bundle`` runs ONE project, ``--portfolio`` runs N from a schema-validated config (with ``--verdict-dir`` as the portfolio-level expert inbox), ``--goals`` + ``--ledger`` bound ACHIEVEMENT where the §8 caps bound spend, and ``--value-report`` projects what the run delivered. A flag the chosen path cannot honour is REFUSED, never silently ignored (§1). Run: uv run python -m portfolio_optimiser_claude.run --bundle [--inbox ] # N projects, sequential, one shared meter + one shared learning store: uv run python -m portfolio_optimiser_claude.run --portfolio \\ [--verdict-dir ] # K8 live-run drill (builds all, captures artifacts, STOPS before the first call): uv run python -m portfolio_optimiser_claude.run --bundle \\ --outbox --run-id --live-dry-run """ from __future__ import annotations import argparse import json from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Sequence from portfolio_optimiser_claude.artifacts import ( _dump_json, build_citations, persist_run_artifacts, persist_stop_artifacts, ) from portfolio_optimiser_claude.outbox import persist_outbox from portfolio_optimiser_claude.budget import BudgetExceeded, BudgetMeter from portfolio_optimiser_claude.contracts import ( Contracts, ReferenceProjectsContract, load_contracts, load_reference_projects, resolve_model, ) from portfolio_optimiser_claude.goals import GoalContract, GoalReached from portfolio_optimiser_claude.preflight import Refusal, run_preflight from portfolio_optimiser_claude.experience import ( CandidateFeatures, VerdictStore, fold_experience, seed_store_from_bundle, ) from portfolio_optimiser_claude.inbox import merge_inbox_into_store from portfolio_optimiser_claude.ir import SavingsProposal, load_validator_input from portfolio_optimiser_claude.okf import bundle_context, navigate_bundle from portfolio_optimiser_claude.provenance import Citation, Provenance from portfolio_optimiser_claude.loop import ModelClient, run_project from portfolio_optimiser_claude.notify import ( EgressNotPermitted, Notification, Notifier, Transport, add_notify_args, build_notifiers, emit, notify_config_from_args, ) from portfolio_optimiser_claude.validator import Rejection from portfolio_optimiser_claude.valuereport import build_value_report, load_ledger, report_to_json _PROPOSER_ROLE = "proposer" _CHECKER_ROLE = "checker" # The one backend profile the run resolves against (mirrors SdkModelClient's default). _DEFAULT_PROFILE = "anthropic" # Exit codes for the two STRUCTURED stops the entrance can reach. They are # distinct because the criteria are: 3 bounds SPEND (§8), 4 bounds ACHIEVEMENT # (K2). Neither is an error — both are outcomes, and both say so in the output. _BUDGET_STOP_EXIT = 3 _GOAL_STOP_EXIT = 4 # The injected client seam of the entrance: (contracts, max_budget_usd_per_call). ClientFactory = Callable[[Contracts, float], ModelClient] @dataclass(frozen=True) class ComposedRunContext: """The §5 sequence's output: the folded context + what fed it (§9-traceable).""" context: str citations: list[Citation] ir_projection: SavingsProposal inbox_merged: int seeded: int def compose_run_context( bundle_dir: Path, inbox_dir: Path | None = None, *, k: int, store: VerdictStore | None = None ) -> ComposedRunContext: """Compose the run context per §5: merge inbox → seed → fold — read-only. Citations are built BEFORE anything else so an uncitable context fails fast ahead of any spend (§9). A missing/empty ``inbox_dir`` (or ``None``) leaves the composition identical to the no-inbox base. Nothing is ever written — the system reads the inbox, the expert writes it (§3 Step 7). A passed-in ``store`` is used AS-IS (its existing verdicts survive the merge, first-write-wins) — the §5 cross-project threading a portfolio pass (K3) relies on: a verdict available at project k reaches project k+1's fold. ``None`` (the single-run default) builds a fresh store, so every existing caller composes exactly as before. """ citations = build_citations(navigate_bundle(bundle_dir)) ir_projection = load_validator_input(bundle_dir) if store is None: store = VerdictStore() inbox_merged = merge_inbox_into_store(store, inbox_dir) if inbox_dir is not None else 0 seeded = seed_store_from_bundle(store, bundle_dir) context = fold_experience( store, CandidateFeatures.from_proposal(ir_projection), bundle_context(bundle_dir), k, ) return ComposedRunContext( context=context, citations=citations, ir_projection=ir_projection, inbox_merged=inbox_merged, seeded=seeded, ) def default_client_factory(contracts: Contracts, max_budget_usd_per_call: float) -> ModelClient: """The CLI's default: the real SDK client (run-path only, §1). Imported lazily so composing/executing with an injected client never touches the SDK module — the suite drives the same orchestration with the scripted stand-in. """ from portfolio_optimiser_claude.sdk_client import SdkModelClient return SdkModelClient(contracts.model_map, max_budget_usd_per_call=max_budget_usd_per_call) def _client_cost_usd(client: ModelClient) -> float | None: # Only the SDK client accounts USD; a client without the attribute # persists an honest null (the usage artifact allows it), never a 0.0. cost = getattr(client, "total_cost_usd", None) return None if cost is None else round(float(cost), 6) def _client_sdk_version(client: ModelClient) -> str | None: # The build is read from the PRODUCING CLIENT, never from the environment: # a run driven by the scripted stand-in used no SDK at all, and stamping # the installed version there would attribute a build to a run that never # touched it (§1). Same seam rule as the cost and the model id above. version = getattr(client, "sdk_version", None) return None if version is None else str(version) def execute_run( client: ModelClient, composed: ComposedRunContext, *, contracts: Contracts, out_dir: Path, max_debate_rounds: int, max_attempts: int, outbox_dir: Path | None = None, run_id: str | None = None, notifiers: Sequence[Notifier] = (), ) -> int: """Drive the loop under the §8 meter; persist artifacts on BOTH outcomes. Exit 0: the run completed (validated or typed rejection) and its artifacts are on disk. Exit 3: a structured budget stop (§8) — the stop event and the usage-vs-caps artifact are persisted; a stop is never a silent hang. When ``outbox_dir`` is set, the completed run also persists a ``run_id``-named proposal/outcome pair to the outbox (S2.1) — the system's own output layer, read by K8 (live capture) and K9 (pending tracking). A budget stop has no proposal, so it writes no outbox pair. ``notifiers`` (K10) receive a structured event on BOTH outcomes — a budget stop is a run outcome, not an absence of one, so it notifies too. The list is empty unless the operator configured a sink; delivery never gates the run. """ run_label = run_id or out_dir.name meter = BudgetMeter(contracts.termination) try: result = run_project( client, composed.context, meter=meter, max_debate_rounds=max_debate_rounds, max_attempts=max_attempts, default_project_id=composed.ir_projection.project_id, ) except BudgetExceeded as stop: print(f"STOPPED by budget: {stop.kind} observed {stop.observed} > limit {stop.limit}") stop_paths = persist_stop_artifacts( out_dir, stop=stop, termination=contracts.termination, tokens_used=meter.tokens_used, rounds_used=meter.rounds_used, cost_usd=_client_cost_usd(client), ) for name, path in sorted(stop_paths.items()): print(f"artifact: {name} -> {path}") emit( notifiers, Notification( event="run.stopped", summary=f"run {run_label} stopped by budget: {stop.kind}", fields={"kind": stop.kind, "observed": stop.observed, "limit": stop.limit}, ), ) return _BUDGET_STOP_EXIT provenance = Provenance( citations=composed.citations, model=getattr(client, "last_model", None) or "unknown", # §9: real id or neutral role=_PROPOSER_ROLE, validator_decision=result.validator_decision, tokens_used=meter.tokens_used, sdk_version=_client_sdk_version(client), ) paths = persist_run_artifacts( out_dir, run=result, provenance=provenance, termination=contracts.termination, tokens_used=meter.tokens_used, rounds_used=meter.rounds_used, cost_usd=_client_cost_usd(client), ) outcome_kind = "rejected" if isinstance(result.outcome, Rejection) else "validated" print( f"result: validator={result.validator_decision} checker={result.checker_decision} " f"attempts={result.attempts} outcome={outcome_kind}" ) for name, path in sorted(paths.items()): print(f"artifact: {name} -> {path}") if outbox_dir is not None: outbox_paths = persist_outbox( outbox_dir, run=result, provenance=provenance, run_id=run_id or "" ) for name, path in sorted(outbox_paths.items()): print(f"outbox: {name} -> {path}") emit( notifiers, Notification( event="run.completed", summary=f"run {run_label}: {outcome_kind}", fields={ "validator_decision": result.validator_decision, "checker_decision": result.checker_decision, "attempts": result.attempts, "outcome": outcome_kind, }, ), ) return 0 def load_goal(path: Path) -> GoalContract: """Load + validate the goal contract fail-fast (§10) — a startup contract like the rest. A percent goal is D-E-gated and refuses loudly here (``NotImplementedError`` from ``GoalContract``), before a model client exists — never silent semantics. """ return GoalContract(**json.loads(path.read_text(encoding="utf-8"))) def check_goal_before_spend( goal: GoalContract, ledger_path: Path | None, *, notifiers: Sequence[Notifier] = (), ) -> bool: """Evaluate the goal against the book BEFORE any model call; True ⇒ stop (K12). The §8 caps bound SPEND; the goal bounds ACHIEVEMENT — so the honest place to ask "is the target already met?" is ahead of the first call, not after paying for one. Observed value is the ledger's dimension-free realized sum (K1); an absent ``--ledger`` is an EMPTY book (0 realized), so the goal is still evaluated, simply not reached — never a skipped check. A HARD goal reached returns True after printing the structured stop and notifying the configured sinks (a stop is an outcome, exactly as a budget stop is). A SOFT goal reached is a flag: it prints and the run continues. """ realized = load_ledger(ledger_path).total_realized_nok() try: reached = goal.check(realized) except GoalReached as stop: print( f"GOAL REACHED (hard): realized {stop.observed_nok} NOK >= target " f"{stop.target_nok} NOK — stopping BEFORE any model call " "(the §8 caps bound spend; the goal bounds achievement)." ) emit( notifiers, Notification( event="run.goal_reached", summary=( f"hard goal reached: realized {stop.observed_nok} NOK >= " f"target {stop.target_nok} NOK — run not started" ), fields={ "mode": goal.mode, "target_nok": stop.target_nok, "observed_nok": stop.observed_nok, }, ), ) return True if reached: print( f"GOAL REACHED (soft): realized {realized} NOK >= target {goal.target_nok} NOK " "— flagged, the run continues." ) else: print(f"goal ({goal.mode}): realized {realized} of {goal.target_nok} NOK — not reached.") return False def execute_portfolio( client: ModelClient, projects: ReferenceProjectsContract, *, contracts: Contracts, top_k: int, max_debate_rounds: int, max_attempts: int, verdict_dir: Path | None, notifiers: Sequence[Notifier] = (), ) -> int: """Drive N projects sequentially under ONE §8 meter; print one line per project (K12). This is the CLI reach to ``run_portfolio`` — the capability itself is K3's and is not extended here: the pass returns typed results and persists NOTHING, so nothing is filed and the summary IS the output. A budget stop propagates out of the shared meter and is reported as the structured stop it is (exit 3); because this path files no artifacts, none are claimed. ``run_portfolio`` is imported lazily: ``portfolio.py`` imports this module for ``compose_run_context``, so a module-level import would be circular. """ from portfolio_optimiser_claude.portfolio import run_portfolio meter = BudgetMeter(contracts.termination) try: result = run_portfolio( projects, client, meter, top_k=top_k, max_debate_rounds=max_debate_rounds, max_attempts=max_attempts, verdict_dir=verdict_dir, ) except BudgetExceeded as stop: print(f"STOPPED by budget: {stop.kind} observed {stop.observed} > limit {stop.limit}") print("portfolio: no artifacts written — this path persists nothing (§1).") emit( notifiers, Notification( event="run.stopped", summary=f"portfolio stopped by budget: {stop.kind}", fields={"kind": stop.kind, "observed": stop.observed, "limit": stop.limit}, ), ) return _BUDGET_STOP_EXIT for project in result.results: outcome_kind = "rejected" if isinstance(project.run.outcome, Rejection) else "validated" print( f"project: {project.project_id} validator={project.run.validator_decision} " f"checker={project.run.checker_decision} attempts={project.run.attempts} " f"outcome={outcome_kind}" ) print( f"portfolio: {len(result.results)} project(s) completed — results are returned, " "not filed (this path persists no artifacts)." ) emit( notifiers, Notification( event="portfolio.completed", summary=f"portfolio: {len(result.results)} project(s) completed", fields={"projects": [project.project_id for project in result.results]}, ), ) return 0 def write_value_report( *, outbox_dir: Path, inbox_dir: Path | None, ledger_path: Path | None, destination: Path, goal: GoalContract | None = None, ) -> bool: """K11 opt-in: project the persisted layers into a value report AFTER the run. The report is a projection over what is already on disk — including the pair this run just filed — so it runs after the run, never before, and it can never change what the run itself decided. A malformed layer is reported here and makes the COMMAND non-zero (the operator asked for a report and did not get one), but a budget stop stays a budget stop: reporting never rewrites a run's own verdict. Returns whether the report was written. A ``goal`` (K12's ``--goals``) is passed straight through, so the operator's one declared target drives BOTH the pre-spend stop and the report's goal progress — one contract, never two figures that can disagree. """ try: report = build_value_report( outbox_dir=outbox_dir, inbox_dir=inbox_dir, ledger_path=ledger_path, goal=goal ) except (OSError, TypeError, ValueError) as exc: print(f"VALUE REPORT FAILED — refusing to project a malformed layer (§10): {exc}") return False destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(report_to_json(report), encoding="utf-8", newline="\n") print(f"value-report: {destination}") return True def build_dry_run_config( contracts: Contracts, *, client: ModelClient, profile: str, bundle_name: str, run_id: str, max_rounds: int, max_tokens: int, max_budget_usd_per_call: float, max_debate_rounds: int, max_attempts: int, top_k: int, ) -> dict[str, Any]: """The run-config log (comparison protocol §4 pt 3): model-id, parameters, caps. Records the model id each role the loop calls resolves to — THROUGH ``resolve_model`` (the run's own resolution path), never a raw dict read — the profile, and every cap/parameter a live run would carry. Deliberately carries NO wall-clock date: the outbox promises byte-determinism (same input + run_id ⇒ identical file), and the run's date is stamped at report time (§4 pt 3), never into the deterministic log. ``client`` is the client the drill CONSTRUCTED (never called): the rig's SDK build is read from it, the same seam rule the provenance stamp follows. A drill driven by the scripted stand-in used no SDK and stamps null — reading the installed version from the environment would describe a rig that never existed (§1). """ return { "run_id": run_id, "profile": profile, "bundle": bundle_name, "sdk_version": _client_sdk_version(client), "models": { role: resolve_model(contracts.model_map, role, profile=profile) for role in (_PROPOSER_ROLE, _CHECKER_ROLE) }, "caps": { "max_rounds": max_rounds, "max_tokens": max_tokens, "max_budget_usd_per_call": max_budget_usd_per_call, "max_debate_rounds": max_debate_rounds, "max_attempts": max_attempts, "top_k": top_k, }, } def execute_dry_run( *, outbox_dir: Path, run_id: str, profile: str, run_config: dict[str, Any], refusals: list[Refusal], ) -> int: """Capture the run-config + preflight artifacts; STOP before any model call (K8). Writes the run_id-named PAIR — ``{run_id}-runconfig.json`` and ``{run_id}-preflight.json`` — to the outbox as deterministic house JSON, then returns WITHOUT ever driving the loop: the drill rehearses the whole build and artifact capture offline, so a future operator-gated live run (the M2-analog) is fully rigged. Exit 0 when the preflight is clear (rig go-live-ready); exit 1 when it refused — the artifacts are captured EITHER way (the refusal is itself one of them), and no model call is made in either case. """ outbox_dir.mkdir(parents=True, exist_ok=True) paths = { "runconfig": outbox_dir / f"{run_id}-runconfig.json", "preflight": outbox_dir / f"{run_id}-preflight.json", } _dump_json(paths["runconfig"], run_config) _dump_json( paths["preflight"], { "run_id": run_id, "profile": profile, "clear": not refusals, "refusals": [{"check": r.check, "detail": r.detail} for r in refusals], }, ) for name, path in sorted(paths.items()): print(f"artifact: {name} -> {path}") if refusals: print( f"DRILL: preflight REFUSED ({len(refusals)}) — rig NOT clear to go live " "(artifacts captured, no model call was made):" ) for refusal in refusals: print(f" [{refusal.check}] {refusal.detail}") return 1 print( "DRILL OK — built all, captured artifacts, stopped before the first model call " "(0 model calls); rig clear to go live." ) return 0 # The dest names the PORTFOLIO pass honours — an ALLOWLIST, deliberately. # Classification is required: a flag added to the parser later and named here # nowhere is REFUSED in portfolio mode rather than accepted and then ignored. # The blocklist this replaces failed OPEN — a forgotten flag no-opped in # silence, which is a claim the run does not back (§1). _PORTFOLIO_SUPPORTED_DESTS: frozenset[str] = frozenset( { "portfolio", # the mode itself "verdict_dir", # portfolio-level expert inbox, read before each fold (K3) "ledger", # read by the pre-spend goal check "goals", # ditto — both are consulted BEFORE the portfolio branch "max_rounds", # §8 caps, shared by every project under one meter "max_tokens", "max_budget_usd_per_call", "max_debate_rounds", "max_attempts", "top_k", "notify_console", # K10 sinks: the portfolio pass emits events too "notify_file", "notify_webhook", "allow_webhook_egress", } ) def unsupported_flags_given( parser: argparse.ArgumentParser, args: argparse.Namespace, *, supported: frozenset[str], ) -> list[str]: """Flags the operator actually GAVE that ``supported`` does not name. "Given" is measured against the parser's own default, so this needs no knowledge of which flags exist — which is the point: the answer stays correct for flags added after it was written. Returns the flag spellings, sorted, for a deterministic error message. """ return sorted( "--" + dest.replace("_", "-") for dest, value in vars(args).items() if dest not in supported and value != parser.get_default(dest) ) def main( argv: list[str] | None = None, *, client_factory: ClientFactory | None = None, notifier_transport: Transport | None = None, ) -> int: """The thin CLI: contracts fail-fast (§10) → compose (§5) → execute (§3, §8). ``notifier_transport`` is the injected webhook seam (K10): ``None`` uses the real ``default_webhook_transport`` on the CLI path; the suite injects a canned transport so no socket is opened. """ parser = argparse.ArgumentParser( description=( "Run one project (--bundle) or a whole portfolio (--portfolio) through the " "loop (merge inbox → seed → fold → run), under the §8 caps and an optional " "savings goal." ) ) parser.add_argument( "--bundle", type=Path, default=None, help="OKF bundle dir of ONE project to run." ) parser.add_argument( "--portfolio", type=Path, default=None, help="reference-projects config (JSON): run N projects sequentially under one " "shared §8 meter and one shared learning store (K3).", ) parser.add_argument( "--verdict-dir", type=Path, default=None, help="portfolio-level expert inbox, READ before each project's fold (requires " "--portfolio; a single run uses --inbox).", ) parser.add_argument("--inbox", type=Path, default=None) parser.add_argument( "--out", type=Path, default=None, help="run-artifact dir (default runs/run)." ) parser.add_argument("--outbox", type=Path, default=None) parser.add_argument("--run-id", type=str, default=None) parser.add_argument("--max-rounds", type=int, default=12) parser.add_argument("--max-tokens", type=int, default=150_000) parser.add_argument("--max-budget-usd-per-call", type=float, default=0.25) parser.add_argument("--max-debate-rounds", type=int, default=3) parser.add_argument("--max-attempts", type=int, default=3) parser.add_argument("--top-k", type=int, default=3) parser.add_argument( "--value-report", type=Path, default=None, help="after the run, project the outbox (+ --inbox, + --ledger) into a " "deterministic value report written as JSON to this path (K11; no model call).", ) parser.add_argument( "--ledger", type=Path, default=None, help="realized-savings ledger (K1) read by --goals and --value-report; " "absent = an empty book.", ) parser.add_argument( "--goals", type=Path, default=None, help="goal contract (JSON: target_nok + hard/soft) checked against --ledger " "BEFORE any model call; a hard target already reached stops the run (exit 4).", ) parser.add_argument( "--live-dry-run", action="store_true", help="build all, capture run-config + preflight to the outbox, STOP before " "the first model call (K8 live-run drill; no spend, no model call).", ) add_notify_args(parser) args = parser.parse_args(argv) # K12: exactly ONE run shape. Refusing both the empty and the ambiguous call # keeps the entrance honest — neither flag can silently win over the other. if (args.bundle is None) == (args.portfolio is None): parser.error( "give exactly one of --bundle (one project) or --portfolio (a config of N projects)" ) if args.verdict_dir is not None and args.portfolio is None: parser.error( "--verdict-dir is the PORTFOLIO-level expert inbox and requires --portfolio " "(a single run reads its inbox with --inbox)" ) if args.portfolio is not None: # The portfolio pass returns typed results and persists NOTHING (K3), and # the outbox names its pairs by run_id, which a portfolio pass has none of. # Every flag that would therefore do nothing is refused rather than # silently ignored (§1) — a flag that quietly no-ops is a false claim. # ALLOWLIST (never a blocklist): the refusal covers flags nobody has # classified yet, so a new one fails closed instead of failing quiet. unsupported = unsupported_flags_given(parser, args, supported=_PORTFOLIO_SUPPORTED_DESTS) if unsupported: parser.error( f"--portfolio does not support {', '.join(unsupported)}: the portfolio " "pass persists nothing and has no run_id of its own, and it honours only " "the flags it is known to act on — anything else is refused rather than " "silently ignored. Run the projects individually with --bundle to file " "per-run artifacts." ) # fail-fast (§10 spirit): a run persisted to the outbox MUST carry an explicit # run_id — reject BEFORE composing or constructing a client, so no spend rides # on a run that cannot be filed (no wall-clock default fills the gap). The dry # run's artifacts are ALSO run_id-named, so it requires both an outbox and an id. if args.live_dry_run: if args.outbox is None or not (args.run_id or "").strip(): parser.error( "--live-dry-run requires --outbox and --run-id " "(the drill's artifacts are run_id-named in the outbox)" ) elif args.outbox is not None and not (args.run_id or "").strip(): parser.error("--outbox requires --run-id (no wall-clock default)") # K11: the value report PROJECTS the outbox — without one there is nothing to # project. Rejected here, before any spend, so a run never completes only to # find it cannot produce the report the operator asked for. if args.value_report is not None and args.outbox is None: parser.error("--value-report requires --outbox (the report projects the outbox pairs)") # K10 (§8): build the notify sinks BEFORE any spend — a --notify-webhook # without --allow-webhook-egress is refused fail-fast here, so no run rides # on a misconfigured egress. The opt-in is a run argument, never a config # field (the config cannot grant itself network access). try: notifiers = build_notifiers( notify_config_from_args(args), webhook_transport=notifier_transport ) except EgressNotPermitted as exc: parser.error(str(exc)) # §10: ALL startup contracts schema-validated BEFORE any model client exists — # the portfolio config and the goal contract included. projects: ReferenceProjectsContract | None = None if args.portfolio is not None: try: projects = load_reference_projects( json.loads(args.portfolio.read_text(encoding="utf-8")) ) except (OSError, TypeError, ValueError) as exc: parser.error(f"--portfolio config is not a valid reference-projects file: {exc}") goal: GoalContract | None = None if args.goals is not None: try: goal = load_goal(args.goals) except (OSError, TypeError, ValueError) as exc: parser.error(f"--goals is not a valid goal contract: {exc}") except NotImplementedError as exc: # the D-E-gated percent goal, refused loudly parser.error(f"--goals: {exc}") # The §10 contract carries ONE data source. A portfolio pass carries one per # project, so the contract records the first and every project still navigates # its OWN bundle_dir inside run_portfolio — never the contract's. docs_dir = str(args.bundle) if projects is None else projects.projects[0].bundle_dir contracts = load_contracts( data_source={"docs_dir": docs_dir, "top_k": args.top_k}, termination={"max_rounds": args.max_rounds, "max_tokens": args.max_tokens}, feedback={"decision": "approved", "rationale": "startup shape check (§10)"}, ) # K12: achievement is checked BEFORE spend. A hard target already met by the # book means the run has nothing left to buy — it stops here, structured, # with no client ever constructed. if goal is not None and check_goal_before_spend(goal, args.ledger, notifiers=notifiers): return _GOAL_STOP_EXIT factory = default_client_factory if client_factory is None else client_factory if projects is not None: print( f"portfolio: {len(projects.projects)} project(s) caps: " f"max_rounds={args.max_rounds} max_tokens={args.max_tokens} " f"max_budget_usd_per_call={args.max_budget_usd_per_call}" ) return execute_portfolio( factory(contracts, args.max_budget_usd_per_call), projects, contracts=contracts, top_k=contracts.data_source.top_k, max_debate_rounds=args.max_debate_rounds, max_attempts=args.max_attempts, verdict_dir=args.verdict_dir, notifiers=notifiers, ) assert args.bundle is not None # narrowed by the exactly-one check above # The navigated dir is the CONTRACT's, so the validated config is load-bearing. composed = compose_run_context( Path(contracts.data_source.docs_dir), args.inbox, k=contracts.data_source.top_k ) print( f"run: bundle={args.bundle.name} inbox_merged={composed.inbox_merged} " f"seeded={composed.seeded} caps: max_rounds={args.max_rounds} " f"max_tokens={args.max_tokens} " f"max_budget_usd_per_call={args.max_budget_usd_per_call}" ) client = factory(contracts, args.max_budget_usd_per_call) # K8: the live-run drill builds the client (the key-free SDK construction # premise) but never calls it — it captures the run-config + preflight # artifacts and STOPS before the first model call. A future operator-gated # live run is thus rigged and rehearsed offline, with zero spend. if args.live_dry_run: assert args.outbox is not None # narrowed by the fail-fast above run_id = args.run_id or "" refusals = run_preflight( profile=_DEFAULT_PROFILE, max_rounds=args.max_rounds, max_tokens=args.max_tokens, max_budget_usd_per_call=args.max_budget_usd_per_call, ) run_config = build_dry_run_config( contracts, client=client, profile=_DEFAULT_PROFILE, bundle_name=args.bundle.name, run_id=run_id, max_rounds=args.max_rounds, max_tokens=args.max_tokens, max_budget_usd_per_call=args.max_budget_usd_per_call, max_debate_rounds=args.max_debate_rounds, max_attempts=args.max_attempts, top_k=args.top_k, ) return execute_dry_run( outbox_dir=args.outbox, run_id=run_id, profile=_DEFAULT_PROFILE, run_config=run_config, refusals=refusals, ) code = execute_run( client, composed, contracts=contracts, out_dir=args.out if args.out is not None else Path("runs") / "run", max_debate_rounds=args.max_debate_rounds, max_attempts=args.max_attempts, outbox_dir=args.outbox, run_id=args.run_id, notifiers=notifiers, ) # K11: the value report is produced on BOTH outcomes — a budget-stopped run # still has a value picture worth reporting (the accumulated outbox, inbox # and ledger are what it projects, not this one run's success). if args.value_report is not None: assert args.outbox is not None # narrowed by the fail-fast above written = write_value_report( outbox_dir=args.outbox, inbox_dir=args.inbox, ledger_path=args.ledger, destination=args.value_report, goal=goal, ) if not written and code == 0: code = 1 return code if __name__ == "__main__": raise SystemExit(main())