Skip to content

feat(verifier): check the deliverables the plan declared - #211

Open
lfnothias wants to merge 6 commits into
mimosa_v2from
propose/verifier-declared-outputs
Open

feat(verifier): check the deliverables the plan declared#211
lfnothias wants to merge 6 commits into
mimosa_v2from
propose/verifier-declared-outputs

Conversation

@lfnothias

Copy link
Copy Markdown
Collaborator

Slice 3 of 4 from #197. Six commits cherry-picked unchanged from mimosa_v2_lfx. Addresses #196.

Problem

The verifier never saw what the plan asked for. A step declared workspace/analysis/iimn_reproduction_plan.md, wrote a differently named file at the workspace root, scored 0.799, was accepted, and killed the run at the next step's dependency gate. None of the 26 claims in its rubric mentioned the declared path, because claims are extracted from execution text and the workspace listing, so the rubric inherited the agent's choice of deliverable.

Two related defects: the rubric cache keyed on the knowledge-wrapped goal, which grows on every retry, so retries of the same step were scored against fresh rubrics; and the acceptance block assigned COMPLETED on both branches of the missing-output check, so the check computed the right answer and nothing acted on it.

Solution

  • sources/core/declared_outputs.py: the planner's expected_outputs, written before the step runs, become mandatory claims the verifier checks alongside the extracted rubric.
  • A retry of the same step hashes the original task, not the growing goal, so it reuses the frozen rubric.
  • While attempts remain, a step that did not produce its deliverable spends one on producing it; on the last attempt it stays COMPLETED so the dependency gate still names the missing output.
  • Three corrections found in production against the first version: a declared directory asks for a directory holding a non-empty file, not a file; a data file (.csv, .tsv, .mgf, .json, .parquet, .mzml, .mztab) must hold records rather than placeholder text; a report that describes missing data in its siblings is not itself a placeholder.

Testing

Fresh uv sync --group dev --python 3.11 on this branch:

17 failed, 271 passed, 4 skipped

The 17 failures are the pre-existing set from #205. This slice adds 42 passing tests: declared_outputs_test, planner_missing_outputs_test, verifier_rubric_key_retry_test.

Backwards compatibility

Steps without expected_outputs are verified as before. Merges cleanly with #206, #207 and the other slices (checked with git merge-tree).

🤖 Generated with Claude Code

lfnothias and others added 6 commits September 2, 2026 12:20
The rubric cache exists so "verifier scores stay comparable across iterations
of the same task" (verifier_claim_cache_test.py) — the first run freezes the
ranked claim list and later runs reuse it verbatim (VerifierEvaluator.evaluate).
It keys on sha256(goal). But the goal the verifier reads is the
knowledge-wrapped one, and the retry loop prepends the previous attempt's whole
answer dict to it before re-running the same step, so every retry hashes a
longer string, misses the cache, and writes a fresh rubric.

Measured on the run this came from (p_iimn task_001, step
task_and_resource_discovery, three attempts):

    original_task_<uuid>.txt   2657 B, md5 02b411d9…  — identical all three
    goal_<uuid>.txt            2657 → 6505 → 11353 B  — grows every attempt
    key(goal)      a19ee032 / d11aa80f / 3530d233     — 3 rubrics
    key(original)  a19ee032 / a19ee032 / a19ee032     — 1 rubric

The three attempts drew 21, 24 and 20 claims with disjoint ids and scored
0.000 / 0.649 / 0.557 — three numbers on three different scales, which
evolution_engine then compares with max() and reports as "Best run".

_extract_claims now takes cache_key_text separately from goal, so the claim
writer still sees the full wrapped goal (prior-attempt context is useful in the
prompt) while the cache keys on the invariant task. WorkflowInfo already keeps
that unwrapped task "for similarity matching", and record_lineage and the
variation prompts already prefer `original_task or goal`; the verifier's key was
the one place that did not. Default is falsy, so every other caller is unchanged.

The 16 failing tests in the suite (evaluation_cli, failure_fingerprint, pricing,
logprobs, csv_mode_logging) fail identically at 67b4251 with these two files
reverted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit f0c5afd)
Issue #196. Step reproduction_spec_analysis declared
workspace/analysis/iimn_reproduction_plan.md, wrote iimn_reproduction_guide.md
at the workspace root instead, was scored 0.799 and accepted, and then killed
the run at the next step's dependency gate. None of the 26 claims in its frozen
rubric mentioned the declared path — claims are extracted partly from execution
text and workspace listing, so the rubric inherited the agent's choice of
deliverable and could not catch the agent choosing wrong.

expected_outputs is the anchor: the planner writes it before the step runs, so
it cannot have been shaped by execution. The verifier never saw it — it is
handed a uuid, and neither state_result.json nor the workflow folder carries the
declaration.

Threading it through start_workflow_evolution, the generation loop,
IndividualRun and the factory would touch five shared signatures. It is not
needed: the planner already passes original_task=step_task, and that is the same
string the verifier keys its rubric cache on. So the declaration is written
under that key and read back under it — one write in the planner, one read in
the verifier, no signature changed.

The claims are prepended, so claims[:max_claims] cannot drop them, and added
after _extract_claims has persisted the rubric, so they never enter the frozen
cache: the cache freezes what a run produced, these belong to the plan and must
follow it when it changes. Every failure path degrades to the previous
behaviour — no declaration, unreadable file, or an exploding wf_info costs the
extra claims, never the evaluation.

One design question is deliberately left open in #196 rather than settled here:
this makes the score partly a function of the planner's path conventions, so a
step that produces an equivalent artefact under another name now fails a
maximum-importance claim. That is the intended reading of "declared output",
but it is Martin's call.

17 tests, including the one that pins planner and verifier to the same key
function — if those drift the declaration is written where nothing reads it,
and a silent stop is the failure mode this exists to end.

Suite: 369 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 8be2b35)
Issue #196. Both inner branches of the acceptance block assigned
TaskStatus.COMPLETED and broke, differing only in print_ok against print_warn:

    outputs_produced, missing_outputs = self._verify_expected_outputs(step)
    step.status = TaskStatus.COMPLETED
    if outputs_produced:  print_ok(...);   break
    else:                 print_warn(...); break

So _verify_expected_outputs computed the right answer and nothing acted on it.
Observed on a real run: step reproduction_spec_analysis was accepted at 0.799
without writing workspace/analysis/iimn_reproduction_plan.md, and the run died
at the *next* step's dependency gate — with a third attempt still unspent.

Now: while attempts remain, spend one on producing the deliverable rather than
banking the miss. On the last attempt keep COMPLETED, deliberately, so the
dependency gate still reports which output is missing for which step instead of
a generic step failure replacing that precise message. PlanStep.missing_outputs
carries the miss so COMPLETED alone no longer implies the deliverable exists.

Checked the tests first, per the standing rule: headless_planner_test covers
_verify_expected_outputs and planner_test covers _can_execute_step, but nothing
asserted what run_attempts does with the result. Uncovered, not deliberate.

Interacts with 8be2b35, as noted on the issue: mandatory expected_outputs claims
make a name mismatch louder, this makes it consequential. A step that produced
the right content under the wrong name now retries. Whether that is the desired
reading of expected_outputs is Martin's call and is flagged there, not assumed
here.

9 tests. Suite: 378 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit dde7b20)
Caught in production on run 6. Step `data_acquisition` declared two outputs:

    workspace/p_iimn_task_001/data/
    workspace/p_iimn_task_001/data/dataset_inventory.csv

as_claims worded both identically — "A file exists at that path, in the
workspace, and is non-empty" — so the directory became a maximum-importance
claim no correct run could satisfy. A step that did exactly what the plan asked
would have been marked down for it, which is the opposite of what 8be2b35 is
for.

Planner._verify_expected_outputs already makes this distinction (it short-
circuits a trailing-separator output through _directory_output_satisfied). The
claim now makes it too: a directory output asks for a directory holding at
least one non-empty file.

The existing test only asserted the claim's id, which was correct for both
kinds and so said nothing about the description. Four tests now cover the
distinction, including the exact pair run 6 recorded.

Suite: 382 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 6f0d9c1)
Confirmed against this fix in production, and it went the wrong way.

Run 6 step `feature_detection_and_processing` declared five outputs. No raw
LC-MS data was reachable, so the run wrote each file as a labelled placeholder
— line 2 of feature_table_qtof.csv reads

    # PLACEHOLDER: MZmine Processing Output for qTOF (MSV000080492)
    # Status: NO REAL DATA AVAILABLE

— and all five mandatory claims PASSED at importance 10, one on the detail line
"File exists and is non-empty (1462 bytes)". So 8be2b35 contributed 50
importance-points of passes for files that state in their own second line that
they contain no data.

The wording was the defect. "A file exists at that path and is non-empty" is
satisfied by a stub, which means the claim pressures an agent to create the file
without pressuring it to fill the file. Under five such claims a run that cannot
obtain its inputs is pushed toward writing something — here honest placeholders,
but fabrication would have satisfied the claim equally well, and this fix would
have been the cause.

The claim now asks for content and names the failure mode: not a placeholder,
stub, template, or a note recording that the data could not be obtained.

The step still failed overall (0.552, hard_fail_capped), so other claims caught
what these missed — but these were on the wrong side of it.

Suite: 384 pass; the same 16 failures are pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 696bae2)
Third defect in this fix, and the first one that punished honesty.

696bae2 disqualified "a file whose body announces missing or unavailable data".
An A/B probe against run 6's archived workspace applied that to
processing_log.md — 292 lines, per-dataset MZmine parameters, a parameter
comparison table and a recovery path, the most substantive artefact in that
workspace — and failed it, because the log honestly recorded that its *sibling*
outputs were placeholders. The wording could not tell a file that IS a stub from
a file that REPORTS on one, so it penalised exactly the behaviour the mechanism
exists to protect.

Now split by suffix, the same discriminator asb_eval already uses:

  .csv .tsv .mgf .json .parquet .mzml .mztab
      "holds actual records — data rows, spectra, or entries — and not merely
       comments, headers, or placeholder text standing in for data"
  everything else
      "substantive content produced by this step... A report that documents what
       was attempted and honestly records missing inputs or limitations does
       satisfy this claim"

Verified against the shipped wording on run 6's real artefacts:

  stub .mgf   fail  "only placeholder text; no actual spectral records"
  stub .tsv   fail  "only header and/or comments, no actual records"
  stub .mgf   fail  "contains placeholder markers"
  real .md    pass  "substantive (size > 1KB, non-empty lines)"

and previously, on the .csv that started this: old wording pass, new wording
fail ("could not be parsed as CSV: No columns to parse from file").

Four tests added; two earlier ones updated, since they asserted the single
wording this deliberately splits. 27 on the module, suite 388 pass, the same 16
failures pre-existing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 6524cf6)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant