portfolio-optimiser/docs/2026-09-05-major2-trekreview.md
Kjell Tore Guttormsen 15ff7a82d7 docs(major2): remedieringsfoot under den sporede trekreview-kopien
Tallene foer/etter, ett commit per funn, og mutasjonssignaturene - der
reviewen selv bor, saa de to kan ikke drive fra hverandre.

1364 passed / 5 skipped -> 1367 / 5 (collect 1369 -> 1372, strengt supersett,
0 fjernet). mypy + ruff rene. Golden BYTE-UENDRET (shasum -a 1 av INNHOLDET =
ea8c534773acdbe41ae68f2c55724d69aaf8be4f).

Header-notatet er justert: artefaktet er fortsatt byte-identisk, footeren
ligger UNDER det og er repoets egen.

Uttalt i footeren, ikke stilltiende: reviewens fokuspunkt 4 (hosting.py:144
literal vs `_REFUSED_BY_NAME` - verifisert fortsatt sann ved HEAD) er UTENFOR
ordren og staar som kandidat, det samme gjoer de tre restene under fokuspunkt
1. Og hvert kriterium reviewen kalte UMAALT mot levende modell (SC1s
utfall-halvdel, SC4, SC8, SC13-premisset) er fortsatt umaalt - ingenting her
er kjoert mot en modell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 22:31:06 +02:00

248 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Tracked archive of the Voyage /trekreview artefact `.claude/projects/2026-09-04-major2-proposal-review-door/review.md` (local-only, gitignored like brief.md and plan.md). Byte-identical copy of the artefact at commit time (the REMEDIATION footer below it is this repo's own addition, appended after the artefact ends); `/trekplan --brief` consumes the local-only original. Order 20260905T191506Z-9345814922-from-.claude. -->
---
type: trekreview
review_version: "1.0"
created: 2026-09-05
task: "MAJOR-2: a door for human feedback on the proposal on the table — approve | revise(feedback) after the validator's outcome, one further bounded attempt, history verbatim in RunResult and outbox, CLI --proposal-review, hosting refuses"
slug: major2-proposal-review-door
project_dir: .claude/projects/2026-09-04-major2-proposal-review-door/
brief_path: .claude/projects/2026-09-04-major2-proposal-review-door/brief.md
scope_sha_start: bcf3337
scope_sha_end: c66dddb1da3332e5fdf90f6d466f8ba171968d44
reviewed_files_count: 10
findings:
- ada5f264ceb5027dc102ac972e7123d2365302ab
- 5935d942631ddf34714805ec324b5717d475540e
- 02159d215c23337196ef0a777628a8626244de2b
- da48592851334aa669f75f80635d475176d5987b
- d71a5d72d4a3057232f6adfe8f02f7de912419dd
---
# Review: MAJOR-2: a door for human feedback on the proposal on the table — approve | revise(feedback) after the validator's outcome, one further bounded attempt, history verbatim in RunResult and outbox, CLI --proposal-review, hosting refuses
## Executive Summary
BLOCK: 1 BLOCKER, 3 MAJOR and 1 MINOR finding survived all passes; nothing was suppressed, the `unverified` bucket is empty, and both expected reviewers reported. Scope note: no `progress.json` exists, so the command's mtime fallback (brief mtime 2026-09-04T17:13:26Z) resolved to `da5f10f`, which would have included three commits of the separate, already-closed verdict-gate order (`e7d1a31`, `0a83bb8`, `bcf3337`); the orchestrator narrowed the range to `bcf3337..c66dddb`, the exact 12-commit MAJOR-2 series (`ce4c15e``c66dddb`), dropping `docs/2026-09-04-s2c-debatt-k2.md`, `src/portfolio_optimiser/explore.py`, `src/portfolio_optimiser/okf.py` and `tests/test_verdict_layer_refusal_loadbearing.py` (all verdict-gate, none MAJOR-2); `scope_sha_start` is recorded in short form because the coordinator may not run git. Effort is `high` (brief signal), so Pass 3 (reasonableness) was skipped; all five rule_keys are catalogue keys and no normalisation was needed. Read first: the BLOCKER at CLAUDE.md:1843 (SC5 — `proposal_review.py:196-200` says the announce-rule departure "is stated in the invariant row"; the orchestrator's grep over the MAJOR-2 row finds no announce / "never consulted" / zero-is-silence vocabulary while the control label "M33" is present). Read second: the MAJOR at run.py:2738 — the orchestrator confirmed from source that `--proposal-review` is refused whenever `--checkpoint-dir` is set (run.py:2738-2745, "pass --proposal-review at --resume instead") while `--resume` itself requires `--checkpoint-dir` (run.py:2776-2782), so the composition the refusal text, the run.py:2218 help and README.md:546 recommend is unreachable; its test-side twin (MAJOR, tests/test_proposal_review_loop_loadbearing.py:1344) shows the arm named for that composition never passes `--resume`. The conformance reviewer observed plan/execute drift that is stated or plan-internal and raised no finding for it: the K2 script used a four-step list and two revise/approve pairs where plan Step 9(c) said two-entry/one pair (explained in the doc, not labelled a deviation); the plan diagram says "26 arms, 37 mutations" vs Step 9's M1M40 (plan-internal inconsistency); plan T19 was folded into T13 (stated in T13's docstring); scope creep: none. The executor's own numbers (1364 passed / 5 skipped, 40 red / 1 green, golden `ea8c534…`) are quoted from `docs/2026-09-04-major2-proposal-review-k2.md` and the CLAUDE.md row and were NOT re-run by any reviewer (read-only); the orchestrator confirmed `uv run pytest --collect-only -q` → 1369 tests collected at HEAD.
## Coverage
| File | Treatment | Reason |
|------|-----------|--------|
| CLAUDE.md | summary-only | default |
| README.md | summary-only | default |
| docs/2026-09-04-major2-proposal-review-k2.md | summary-only | default |
| src/portfolio_optimiser/generate.py | summary-only | default |
| src/portfolio_optimiser/hosting.py | summary-only | default |
| src/portfolio_optimiser/outbox.py | summary-only | default |
| src/portfolio_optimiser/proposal_review.py | summary-only | default |
| src/portfolio_optimiser/run.py | summary-only | default |
| tests/test_okf.py | summary-only | default |
| tests/test_proposal_review_loop_loadbearing.py | summary-only | default |
No files were skipped and no working-tree (`[uncommitted]`) changes were in scope.
## Operator focus points
1. `last_ruling` carrier (M29 green): BOTH reviewers independently classified it "correct as it stands — path unreachable by construction; carrier is documentation/defensive". Conformance reviewer's analysis: the loop tail after `for i in range(max_attempts)` is reached only via `continue` on the Rejection branch which sets `last`; every ValidatedProposal branch returns in-loop (reviewer None, approve, revise with remaining <= 0), and on the final iteration `max_attempts - i - 1 == 0` so a revise there returns in-loop. Only divergence: `max_attempts <= 0` (ValueError vs the old AssertionError), `pragma: no cover`, default `max_attempts=3`, not exposed by `run_project`. Residual (no catalogue key fits): T2b's docstring (tests/test_proposal_review_loop_loadbearing.py:438-443) still says "Detach point: reverting to `assert last is not None` (M29)" and repeats the falsified prediction — the `generate.py` comment was corrected, this docstring was not (Fase-3 class). Also docs/2026-09-04-major2-proposal-review-k2.md:59-62 says the generate.py comment "må rettes i neste økt" while :245-248 says it "er RETTET i samme commit" — §1 is stale.
2. `coverage` rows never name a revise as cause (correctness reviewer): row produced at run.py:504-512 with detail "budget exhausted before this approach was evaluated"; literally true when an expert's revises consumed the shared round ledger (tick_round is called only from generate.py:413 and :510 — the debate does not tick rounds). The row under-explains but does not read false; the link is recoverable by arithmetic over `{run_id}-proposal-reviews.json` attempt indices, only when `outbox_dir` is set. Classified: honesty limit correctly stated (CLAUDE.md row, docs § 4), not a correctness gap. Caveat: `honoured=True` does not encode "this revise defunded approach N+1".
3. F4 traceback asymmetry (correctness reviewer): `PlanReviewInputError` is an `ExplorationError` (RuntimeError) (explore.py:551, raised :616); the CLI explore/resume block (run.py:3059-3110) catches only `PlanReviewParked` and `BudgetExceeded`, so plan-review EOF escapes as a traceback, while `ProposalReviewInputError` is caught by name at run.py:3404. In the documented composition `--explore --plan-review --proposal-review` the operator gets a traceback for silence at door 1 and `run stopped:` for silence at door 2. The new channel is correct (RuntimeError, off the ValueError refusal tuple at :3412, outside `_fetch_parsed`'s catch at generate.py:521). Pre-existing, stated, no cross-file regression; the diff added the channel built for exactly this class and left the sibling off it — the cheapest place to fix it. No finding (not anchored to a diff line).
4. Maintenance note (correctness reviewer, no catalogue key at the right tier): hosting.py:109 defines `_REFUSED_BY_NAME = ("proposal_review",)` and a test asserts on the constant, but the check at hosting.py:144 is the literal `if "proposal_review" in payload:` — two copies of one fact (kø-(p)); iterate over `_REFUSED_BY_NAME` at :144.
## Unmeasured against a live model
- **SC1** — mechanics (verbatim injection, bound, composition, byte-identity) fully measured offline; T1's "outcome differs" half is produced by a flip-keyed double (`_FeedbackAwareChatClient:309-335`) — that a live model changes its proposal in response to the words is UNMEASURED.
- **SC2** — fully measured offline (deterministic mutations; counts are the executor's, not re-run by the reviewer).
- **SC3** — fully measured offline (byte hash).
- **SC4** — UNMEASURED against a live model: the second attempt's different amount comes from the script's second entry, not from a model reading the feedback.
- **SC5** — fully measured offline (EOF/`run stopped:` channel is structural; the validated candidate that precedes it is scripted, but the criterion's assertions do not depend on model content).
- **SC6** — fully measured offline (structural refusal, zero calls by design).
- **SC7** — fully measured offline (structural refusals, zero calls).
- **SC8** — UNMEASURED against a live model: outcome 200 000→150 000 is the scripted position, prices are synthetic, and the doc says so (docs/2026-09-04-major2-proposal-review-k2.md:237-241); only token accounting and plumbing are measured.
- **SC9** — fully measured offline (lint/type; not re-run by the reviewer).
- **SC10** — fully measured offline (documents).
- **SC11** — fully measured offline (structural `is` on a recorder).
- **SC12** — fully measured offline (the zero-remaining path is independent of model content).
- **SC13** — mechanism (D6 last-ruling, record keeps the reviewed candidate) fully measured offline with a scripted rejection; whether a live model's follow-up would be rejected is a property of the model, not of the seam, and is UNMEASURED — as is the premise that a live model follows the feedback at all.
Criteria standing unmeasured against a live model: SC1 (outcome-differs half), SC4, SC8, SC13 (model-follows-feedback premise). All others are deterministic/structural and fully measured offline.
## Findings (BLOCKER)
### ada5f264ceb5027dc102ac972e7123d2365302ab
- file: CLAUDE.md
- line: 1843
- rule_key: BROKEN_SUCCESS_CRITERION
- brief_ref: SC5 — "a deliberate departure from the announce rule's zero-is-silence half, stated as such in the invariant row"
- title: State the announce-rule departure in the MAJOR-2 invariant row, as the code claims it already is
- detail: proposal_review.py:196-200 says the zero-reviews-with-reviewer notice is 'a deliberate departure from the announce rule's zero-is-silence half' and that 'The departure is stated in the invariant row'. The MAJOR-2 row starting at CLAUDE.md:1843 (full hunk read) never mentions the announce rule, 'offered, never consulted', or a zero-case departure; grep 'announce' in CLAUDE.md hits only older rows (lines 80, 418-419, 766). The only trace is the mutation label 'M33 rendereren returnerer None på null (3)', which names a mutation, not the decision. This is a claim the surface makes about itself that the row does not back (the repo's Fase-3 class), and SC5 names the row explicitly. The remedy is one sentence; the catalogue fixes the severity.
- recommended_action: Add one sentence to the MAJOR-2 row: proposal_review_notice returns a line on ZERO reviews when a reviewer was offered ('offered, never consulted'), a deliberate departure from the announce rule's omission-on-nothing half, gated by M33 and M37.
## Findings (MAJOR)
### 5935d942631ddf34714805ec324b5717d475540e
- file: tests/test_proposal_review_loop_loadbearing.py
- line: 820
- rule_key: MISSING_TEST
- brief_ref: SC1 — "every 'N generation prompts' assertion in this brief ... is paired with the assertion that the debate's prompt count is unchanged"; SC4 — "the debate's prompt count equal to the control's"
- title: Pair every generation-prompt count with the debate-count assertion the brief requires
- detail: T5-run (:820-826) counts generation prompts via _GENERATION_MARK and T13 (:1145) uses the record's attempt indices as a proxy; no arm in the file asserts the debate turn count between treated and control (grep 'debate' hits only a comment at :657 and a filename at :887). The classifier is sound (both marker lines are emitted only by generate._build_messages, generate.py:330-332), but the brief made the pairing the guard against miscounting debate turns as generation. T13 also asserts none of SC4's prompt-level facts (count by header, feedback in the 2nd prompt, debate count): the 40 000 outcome proves a second attempt was bought, not that its prompt carried the words, because the step-list proposer does not read the prompt.
- recommended_action: In T5-run and T8 assert equality of the non-generation (debate) entries between treated_sink and control_sink; in T13 either document that the record indices are SC4's count proxy or write the child's scripted sink to disk and classify it.
### 02159d215c23337196ef0a777628a8626244de2b
- file: src/portfolio_optimiser/run.py
- line: 2738
- rule_key: PLAN_EXECUTE_DRIFT
- brief_ref: NFR — code correctness (CLI door composition)
- title: --proposal-review refused with --checkpoint-dir, so the --resume path it points to is unreachable
- detail: run.py:2738-2745 refuses whenever args.proposal_review and args.checkpoint_dir is not None, printing 'pass --proposal-review at --resume instead'. But --resume requires --checkpoint-dir (run.py:2776-2782: 'run refused: --resume requires --checkpoint-dir'). So '--resume X --checkpoint-dir D --proposal-review' is refused at :2738 by the very message that recommends it, and '--resume X --proposal-review' without a checkpoint dir is refused at :2776. The composition claimed at run.py:2218 (help text), README.md:546 and the CLAUDE.md MAJOR-2 row does not exist; the fall-through at run.py:3142 -> :3373 would thread the reviewer if the argv ever reached it. Determined by reading both guards; not executed.
- recommended_action: Change the guard at run.py:2738 to 'if args.checkpoint_dir is not None and args.resume is None:' so a parked exploration is refused while a resume passes, and add a test that drives '--resume ... --checkpoint-dir ... --review-inbox ... --proposal-review' with run_project recorded.
### da48592851334aa669f75f80635d475176d5987b
- file: tests/test_proposal_review_loop_loadbearing.py
- line: 1344
- rule_key: MISSING_TEST
- brief_ref: NFR — code correctness (test discriminates the claimed behavior)
- title: The 'composes with resume' arm never passes --resume, so the claimed path has no witness
- detail: test_the_door_composes_with_resume (:1328-1361) claims in its docstring to verify that the resume block falls through to the full-run dispatch with the reviewer, but the argv at :1344-1359 contains no --resume, --checkpoint-dir or --review-inbox; it is a plain single-project run already covered by T13. It therefore cannot detect that run.py:2738 refuses every real resume argv before run_project is reached. Mutation M39 was witnessed only by the park arm (:1294), which asserts the refusal fires, not that the recommended alternative works.
- recommended_action: Rewrite the arm to stage a parked plan-review question and answer (or monkeypatch hitl.read_plan_review_question/load_parked/hitl.load_plan_review_answer and resume_exploration) and pass '--resume RUN --checkpoint-dir D --review-inbox I --outbox-dir O --bundle-dir B --proposal-review', asserting the recorded run_project kwargs carry a non-None proposal_reviewer.
## Findings (MINOR)
### d71a5d72d4a3057232f6adfe8f02f7de912419dd
- file: src/portfolio_optimiser/run.py
- line: 1202
- rule_key: MISSING_ERROR_HANDLING
- brief_ref: NFR — code correctness (error channels)
- title: OSError from the always-on review writer in finally displaces the in-flight stop exception
- detail: run.py:1200-1208 calls outbox.write_proposal_reviews(...) inside the finally at :1184 on every run where a reviewer was given. An OSError from mkdir/write_text while BudgetExceeded or ProposalReviewInputError is propagating replaces the in-flight exception; neither 'except ProposalReviewInputError' (:3404) nor the refusal tuple (:3412) catches OSError, so the CLI tracebacks and the 'run stopped:' / 'run refused:' line is never printed. The parse-failures writer at :1191 has the same shape but runs only when a parse failed; this one runs on every reviewed run.
- recommended_action: Wrap the two finally writers in try/except OSError that records the write failure on stderr and re-raises only when no exception is already in flight, so the original stop reason survives.
## Findings (SUGGESTION)
None.
## Suppressed Findings
None suppressed. No finding was removed in Pass 2, Pass 3 was skipped, and the `unverified` bucket is empty — ALLOW is not withheld by any suppression; the verdict is BLOCK on the counted BLOCKER alone.
Coordinator notes on the passes (no findings altered):
- Pass 1: no `(file, line, rule_key)` collisions among the five findings; the two `MISSING_TEST` findings differ on line (820 vs 1344).
- Pass 2 Succinctness: the code-correctness-reviewer re-emitted its payload with shortened titles (packaging only; every other field identical to the first emission, confirmed by re-reading the file). Title lengths counted by the coordinator: 96 / 85 / 97 / 88 / 90 chars, all ≤ 100; every detail ≤ 800 chars (longest ≈ 750).
- Pass 2 Accuracy: all five `file` values resolve under the repo root and each cited line was read and is plausible (CLAUDE.md:1843 opens the MAJOR-2 row; run.py:2738 is the `--checkpoint-dir` guard; run.py:1202 is the `write_proposal_reviews` call; the two test lines exist). Nothing dropped as `accuracy:refuted`.
- Pass 2 Actionability: every `recommended_action` is non-empty and imperative; the `MISSING_TEST` finding at :820 opens with a locative phrase ("In T5-run and T8 assert …") before its imperative — judged imperative in mood, not a hedge or title restatement, kept.
- Pass 3 skipped (high effort). For the record, all five emitted severities already match their catalogue tiers, and all five rule_keys are in `RULE_KEYS`; no `PLAN_EXECUTE_DRIFT` normalisation was applied.
- IDs: computed by the orchestrator with the shipped 3-arg `computeFindingId(file, line, rule_key)` (`lib/parsers/finding-id.mjs`); the coordinator has no execution tool and did not re-derive the digests. The agent prose's 4-tuple (with title) is not producible by the shipped helper (`coordinator-contract.mjs` header).
## Remediation Summary
- 1 BLOCKER → must address before next plan iteration (CLAUDE.md:1843, one sentence in the MAJOR-2 row).
- 3 MAJOR → should address before next plan iteration (run.py:2738 guard composition; tests/test_proposal_review_loop_loadbearing.py:820 debate-count pairing; :1344 resume-path witness).
- 1 MINOR → nice-to-have (run.py:1202, OSError in the `finally` writer).
- 0 SUGGESTION.
If running `/trekplan --brief review.md`, the planner will consume the BLOCKER + 3 MAJOR findings as plan goals (their `recommended_action` becomes the step intent); the MINOR is skipped for v1.0 plan-input.
```json
{
"verdict": "BLOCK",
"counts": { "BLOCKER": 1, "MAJOR": 3, "MINOR": 1, "SUGGESTION": 0 },
"allow_blocked_by": [],
"unverified": [],
"missing_reviewers": [],
"findings": [
{
"id": "ada5f264ceb5027dc102ac972e7123d2365302ab",
"severity": "BLOCKER",
"rule_key": "BROKEN_SUCCESS_CRITERION",
"file": "CLAUDE.md",
"line": 1843,
"brief_ref": "SC5 — \"a deliberate departure from the announce rule's zero-is-silence half, stated as such in the invariant row\"",
"title": "State the announce-rule departure in the MAJOR-2 invariant row, as the code claims it already is",
"detail": "proposal_review.py:196-200 says the zero-reviews-with-reviewer notice is 'a deliberate departure from the announce rule's zero-is-silence half' and that 'The departure is stated in the invariant row'. The MAJOR-2 row starting at CLAUDE.md:1843 (full hunk read) never mentions the announce rule, 'offered, never consulted', or a zero-case departure; grep 'announce' in CLAUDE.md hits only older rows (lines 80, 418-419, 766). The only trace is the mutation label 'M33 rendereren returnerer None på null (3)', which names a mutation, not the decision. This is a claim the surface makes about itself that the row does not back (the repo's Fase-3 class), and SC5 names the row explicitly. The remedy is one sentence; the catalogue fixes the severity.",
"recommended_action": "Add one sentence to the MAJOR-2 row: proposal_review_notice returns a line on ZERO reviews when a reviewer was offered ('offered, never consulted'), a deliberate departure from the announce rule's omission-on-nothing half, gated by M33 and M37.",
"raised_by": ["brief-conformance-reviewer"]
},
{
"id": "5935d942631ddf34714805ec324b5717d475540e",
"severity": "MAJOR",
"rule_key": "MISSING_TEST",
"file": "tests/test_proposal_review_loop_loadbearing.py",
"line": 820,
"brief_ref": "SC1 — \"every 'N generation prompts' assertion in this brief ... is paired with the assertion that the debate's prompt count is unchanged\"; SC4 — \"the debate's prompt count equal to the control's\"",
"title": "Pair every generation-prompt count with the debate-count assertion the brief requires",
"detail": "T5-run (:820-826) counts generation prompts via _GENERATION_MARK and T13 (:1145) uses the record's attempt indices as a proxy; no arm in the file asserts the debate turn count between treated and control (grep 'debate' hits only a comment at :657 and a filename at :887). The classifier is sound (both marker lines are emitted only by generate._build_messages, generate.py:330-332), but the brief made the pairing the guard against miscounting debate turns as generation. T13 also asserts none of SC4's prompt-level facts (count by header, feedback in the 2nd prompt, debate count): the 40 000 outcome proves a second attempt was bought, not that its prompt carried the words, because the step-list proposer does not read the prompt.",
"recommended_action": "In T5-run and T8 assert equality of the non-generation (debate) entries between treated_sink and control_sink; in T13 either document that the record indices are SC4's count proxy or write the child's scripted sink to disk and classify it.",
"raised_by": ["brief-conformance-reviewer"]
},
{
"id": "02159d215c23337196ef0a777628a8626244de2b",
"severity": "MAJOR",
"rule_key": "PLAN_EXECUTE_DRIFT",
"file": "src/portfolio_optimiser/run.py",
"line": 2738,
"brief_ref": "NFR — code correctness (CLI door composition)",
"title": "--proposal-review refused with --checkpoint-dir, so the --resume path it points to is unreachable",
"detail": "run.py:2738-2745 refuses whenever args.proposal_review and args.checkpoint_dir is not None, printing 'pass --proposal-review at --resume instead'. But --resume requires --checkpoint-dir (run.py:2776-2782: 'run refused: --resume requires --checkpoint-dir'). So '--resume X --checkpoint-dir D --proposal-review' is refused at :2738 by the very message that recommends it, and '--resume X --proposal-review' without a checkpoint dir is refused at :2776. The composition claimed at run.py:2218 (help text), README.md:546 and the CLAUDE.md MAJOR-2 row does not exist; the fall-through at run.py:3142 -> :3373 would thread the reviewer if the argv ever reached it. Determined by reading both guards; not executed.",
"recommended_action": "Change the guard at run.py:2738 to 'if args.checkpoint_dir is not None and args.resume is None:' so a parked exploration is refused while a resume passes, and add a test that drives '--resume ... --checkpoint-dir ... --review-inbox ... --proposal-review' with run_project recorded.",
"raised_by": ["code-correctness-reviewer"]
},
{
"id": "da48592851334aa669f75f80635d475176d5987b",
"severity": "MAJOR",
"rule_key": "MISSING_TEST",
"file": "tests/test_proposal_review_loop_loadbearing.py",
"line": 1344,
"brief_ref": "NFR — code correctness (test discriminates the claimed behavior)",
"title": "The 'composes with resume' arm never passes --resume, so the claimed path has no witness",
"detail": "test_the_door_composes_with_resume (:1328-1361) claims in its docstring to verify that the resume block falls through to the full-run dispatch with the reviewer, but the argv at :1344-1359 contains no --resume, --checkpoint-dir or --review-inbox; it is a plain single-project run already covered by T13. It therefore cannot detect that run.py:2738 refuses every real resume argv before run_project is reached. Mutation M39 was witnessed only by the park arm (:1294), which asserts the refusal fires, not that the recommended alternative works.",
"recommended_action": "Rewrite the arm to stage a parked plan-review question and answer (or monkeypatch hitl.read_plan_review_question/load_parked/hitl.load_plan_review_answer and resume_exploration) and pass '--resume RUN --checkpoint-dir D --review-inbox I --outbox-dir O --bundle-dir B --proposal-review', asserting the recorded run_project kwargs carry a non-None proposal_reviewer.",
"raised_by": ["code-correctness-reviewer"]
},
{
"id": "d71a5d72d4a3057232f6adfe8f02f7de912419dd",
"severity": "MINOR",
"rule_key": "MISSING_ERROR_HANDLING",
"file": "src/portfolio_optimiser/run.py",
"line": 1202,
"brief_ref": "NFR — code correctness (error channels)",
"title": "OSError from the always-on review writer in finally displaces the in-flight stop exception",
"detail": "run.py:1200-1208 calls outbox.write_proposal_reviews(...) inside the finally at :1184 on every run where a reviewer was given. An OSError from mkdir/write_text while BudgetExceeded or ProposalReviewInputError is propagating replaces the in-flight exception; neither 'except ProposalReviewInputError' (:3404) nor the refusal tuple (:3412) catches OSError, so the CLI tracebacks and the 'run stopped:' / 'run refused:' line is never printed. The parse-failures writer at :1191 has the same shape but runs only when a parse failed; this one runs on every reviewed run.",
"recommended_action": "Wrap the two finally writers in try/except OSError that records the write failure on stderr and re-raises only when no exception is already in flight, so the original stop reason survives.",
"raised_by": ["code-correctness-reviewer"]
}
]
}
```
---
## REMEDIATION (order `20260905T195046Z-3378824829-from-.claude`, økt 91) — not part of the artefact above
All five findings addressed, one commit each except the two that share a witness by construction.
Suite **1364 passed / 5 skipped → 1367 passed / 5 skipped** (collected 1369 → 1372, strict
superset, **0 removed**); `mypy src` clean (36 files), `ruff check src tests` clean; golden
`tests/golden/demo-transcript.stdout` BYTE-UNCHANGED (`shasum -a 1` of the CONTENTS =
`ea8c534773acdbe41ae68f2c55724d69aaf8be4f`, never the git blob id).
| Finding | Severity | Commit | Mutations, all against the WHOLE suite |
|---|---|---|---|
| `ada5f264` CLAUDE.md:1843 | BLOCKER | `9ed458c` | docs; verified by grep INSIDE the row's own lines (1842..1969): «announce», «offered, never consulted», M33, M37 |
| `02159d21` run.py:2738 + `da485928` tests:1344 | MAJOR ×2 | `c81a90c` | the rewritten arm measured RED against the unchanged guard (`calls == []`, stderr carried the very refusal that recommends the path); park refusal (M39) stays green |
| `5935d942` tests:820 | MAJOR | `d4a0220` | MA classifier constant-empty → 2 red (the two paired arms); MB filter dropped → 2 red. Control 1364/5 |
| `d71a5d72` run.py:1202 | MINOR | `257f44f` | MC guard detached → 2 red (both in-flight arms); MD swallow unconditionally → 1 red (the CONTROL arm alone). Control 1367/5 |
**Out of scope by the order, stated:** the review's operator-focus point 4 (`hosting.py:144` uses the
literal `"proposal_review"` while `_REFUSED_BY_NAME` is defined at :109 — two copies of one fact,
kø-(p)) is NOT fixed here and stands as a candidate. The three residuals under focus point 1
(T2b's docstring repeating the falsified M29 prediction; § 1 vs § 4 of the measurement doc
disagreeing about the `generate.py` comment; the F4 traceback asymmetry) are likewise untouched.
**Unchanged by this remediation:** every criterion the review listed as UNMEASURED against a live
model — SC1's outcome-differs half, SC4, SC8, SC13's model-follows-feedback premise — is still
unmeasured. Nothing here was run against a model.