fix(explore): a plan review never shows an expert the word None and calls it progress

[skip-docs]

PM addendum 4 to order 20260902T151931Z-250257273.

PlanReviewRequest.current_progress and ParkedExploration.current_progress were
both built with str(review.current_progress). The value is a
MagenticProgressLedger | None and is None in every run measured so far, so
str() produced the four characters None, the renderer's truthiness guard found
them non-empty, and the terminal printed a "progress so far" section whose only
content was None. The same four characters went into
{run_id}-plan-review.json -- the one thing that crosses the process boundary in
the asynchronous door, where nobody can ask what it meant.

_progress_text is the ONE conversion, beside _plan_text: absent becomes the
empty string, never "None". The data layer states absence by being absent, as
Bundle.skipped's empty tuple does. The TERMINAL states it in words -- this is
the one surface where omission is wrong, because silence at a gate somebody
signs is exactly what PlanReviewInputError already refuses for EOF.

Measured, and it changed the test: reverting both sites left ONLY a
source-inspection arm red, which is a lint and not a gate, because the first
arms construct a PlanReviewRequest themselves and never enter either site. Each
site now has a behavioural witness that drives the real door -- the terminal for
the synchronous one, the parked question FILE for the asynchronous one.

Load-bearing measured, five mutations, all red against the WHOLE suite, each
with its own signature: both sites reverted (3 red) - synchronous site alone
(2) - parked site alone (2) - the terminal goes silent again (2) - the
placeholder always fires, swallowing a real ledger (1, the control alone).
Green control 1202 passed / 5 skipped; golden demo-transcript.stdout unchanged
(ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

The recorder wiring in resume_exploration is untouched, per the order.
This commit is contained in:
Kjell Tore Guttormsen 2026-09-03 01:06:27 +02:00
commit 7552c0ef73
2 changed files with 290 additions and 5 deletions

View file

@ -514,9 +514,15 @@ def terminal_plan_reviewer(
print(f"\nPLAN REVIEW #{request.index + 1}{stalled}", file=sink)
print("--- the plan the exploration would run ---", file=sink)
print(request.plan, file=sink)
if request.current_progress.strip():
print("--- progress so far ---", file=sink)
print(request.current_progress, file=sink)
print("--- progress so far ---", file=sink)
# Stated, never omitted, and never a repr of absence. An expert about to sign has to be
# able to tell "the loop has not reported progress yet" from "this section was dropped".
print(
request.current_progress
if request.current_progress.strip()
else "(no progress ledger yet)",
file=sink,
)
while True:
print(
f'Answer "{_APPROVE_ANSWER}" to sign it off, '
@ -1087,6 +1093,19 @@ def _plan_text(content: Any) -> str:
return str(getattr(content, "text", "") or "")
def _progress_text(content: Any) -> str:
"""The progress ledger as text — and ABSENCE as the empty string, never ``"None"``.
``MagenticProgressLedger | None`` went through a bare ``str()`` at both construction sites, so
in every run measured so far (the value is ``None`` until the manager has emitted a ledger) the
expert was shown, and the parked question file stored, the four characters ``None``. That is
worse than saying nothing: it looks like content. The data layer states absence by being
absent ``Bundle.skipped``'s empty-tuple rule — and the TERMINAL is where it is put in words,
because silence at a gate somebody signs is what ``PlanReviewInputError`` already refuses.
"""
return "" if content is None else str(content)
def _absorb(
result: Any,
*,
@ -1512,7 +1531,7 @@ async def _drive(
PlanReviewRequest(
index=len(plan_reviews),
plan=_plan_text(review.plan),
current_progress=str(review.current_progress),
current_progress=_progress_text(review.current_progress),
is_stalled=_truthy(review.is_stalled),
)
)
@ -1604,7 +1623,7 @@ async def _park(
checkpoint_id=str(latest.checkpoint_id),
index=len(trace.plan_reviews),
plan=_plan_text(review.plan),
current_progress=str(review.current_progress),
current_progress=_progress_text(review.current_progress),
is_stalled=_truthy(review.is_stalled),
bundle_dirs=tuple(bundle_dirs),
contract=contract,