Skip to content

feat(brief): turn a failed run into a document that gets acted on (P4) - #52

Merged
lopadova merged 3 commits into
feat/p3a-trajectoryfrom
feat/p4-brief
Aug 24, 2026
Merged

lopadova merged 3 commits into
feat/p3a-trajectoryfrom
feat/p4-brief

Conversation

@lopadova

Copy link
Copy Markdown
Contributor

Fourth item of the roadmap. Stacked on #51 (P3a), which is stacked on #50 (P2).

The problem

A red CI job says an evaluation failed. It does not say what to do about it, and the JSON report that would — the input, the golden answer, what the pipeline actually said, which metric fell over and why — is a thousand-line artifact nobody opens on a Tuesday afternoon. So the loop breaks in the same place every time: the run fails, somebody squints at a pass rate, and the diagnosis waits for whoever has an hour.

php artisan eval-harness:brief <report> --dataset=<file> produces the document instead.

Three things a "copy the failures" button cannot do

  • Cohorts. When one tag carries ≥60% of the failures the briefing says "4 of 5 failures share the tag policy — start there; this looks like one problem, not 5." Six failures in one cohort is a diagnosis; six scattered ones are a list.
  • Metric semantics. MetricGlossary carries one line of meaning per built-in metric, which is the part a briefing built from pass/fail assertions structurally cannot have — an assertion describes itself, a metric has semantics. retrieval-mrr: 0.31 becomes "the first relevant document came back around position 3", which points at the retriever rather than at the prompt. An unknown name degrades to no description, never a guess, so a host's own metric is never misdescribed.
  • Compliance mapping. Failing adversarial rows are named with category, severity and framework, and the briefing says in those words that they are security findings. The fix for a wrong refund window and the fix for an injection that worked are not the same fix.

The security detail

This document quotes model output verbatim and is designed to be pasted into an agent with repository access. That makes it a prompt injection surface: one poisoned dataset row — a supplier-imported description, a scraped page, a user-submitted question — and "ignore previous instructions and…" arrives inside the context of something that can write code.

Fencing is necessary and not sufficient: a fence says where the text ends, not what it is. So the briefing opens, before the first fence, by declaring its quoted blocks untrusted data that must not be executed — and a value containing a fence gets a longer fence, so no row can close its own block early. This is MOBILE-SEC-LLM-001 / SEC-LLM-001 applied to our own artifact rather than only to runtime code.

A test asserts the warning appears at a lower offset than the first ``` in the document.

Where the input comes from

The report records what the pipeline said, never what it was asked. The tempting move was to add input/expected to samples[] — additive, contract-legal, one line — and it would have put whole corpora into an artifact that gets committed, diffed and rendered in a browser.

Instead --dataset=<file> reads the dataset that is already in the repo and joins by content hash first, id second — the same negotiation RunComparator makes, so a row renamed in the YAML is still found. Without the option, or when the row is genuinely gone, the briefing names the row and says the dataset was not supplied. It never invents a question: a fabricated input is the one output that would make this document actively misleading to whatever reads it.

Formats

Flag Output
--format=md the briefing
--format=github collapsed <details> PR comment — the briefing belongs where the diff is reviewed, not only in a dashboard somebody must remember to open
--format=json eval-harness.brief.v1, carrying the structure and the markdown so a UI can show the summary and still offer the exact text to copy

Failure modes handled

  • Budget (--budget, default 24000 chars ≈ 6k tokens): rows worst-first, only the worst-scoring execution of each (including every repetition is how a budget disappears), and truncation is declared with counts — silently cutting the list leaves an agent reasoning about "all the failures" while holding a third of them. A budget smaller than one row still emits that row.
  • Wrong artifact: a payload whose schema_version is not the report contract is refused by name. Briefing a comparison payload would have printed "every row passed" for a run that did not.
  • Unloadable --dataset: stops the command. Asking for a dataset and silently getting a thinner document is worse than being told the file was wrong.
  • Unreadable --comparison: warns and briefs the run alone.

Verification

  • 35 new tests; suite 1031 → 1066 (2909 assertions).
  • composer validate --strict, phpunit, PHPStan level 6, Pint — all green locally.
  • Docs: docs-site/docs/guides/briefing.md + nav entry; README feature bullet; docs/PROGRESS.md and docs/LESSON.md entries.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EZ4u82zdoa8naYm75kppMC


Generated by Claude Code

…s acted on

A red CI job says an evaluation failed. The JSON report that would say what to
do about it is a thousand lines nobody opens, so the loop breaks in the same
place every time.

- Brief\RunBriefing: failing rows worst-first (worst execution only, so
  repetitions do not eat the budget) with input, expected, actual output, metric
  scores, the judge's own reason, trajectory summary and metrics that threw.
- Brief\MetricGlossary: one line of semantics per built-in metric. An assertion
  describes itself; a metric has meaning. "retrieval-mrr: 0.31" becomes "the
  first relevant document came back around position 3", which points at the
  retriever rather than the prompt. Unknown names get no description, not a
  guess.
- Cohort diagnosis when one tag carries most of the failures, and adversarial
  rows named as safety findings with category, severity and framework.
- The prompt-injection guard: the document quotes model output verbatim into
  something with repository access, so it declares its quoted blocks untrusted
  data before the first fence, and a value containing a fence gets a longer one.
- --dataset= supplies the question and golden answer, joined by content hash
  first and id second. The report records what the pipeline said, never what it
  was asked, and copying corpora into a committed artifact is not a trade worth
  making. Without it the briefing says so; it never invents a question.
- --format=github as a collapsed PR comment, --format=json as
  eval-harness.brief.v1 carrying both the structure and the markdown.
- Truncation is declared with counts; an artifact that is not a report is
  refused rather than briefed as "every row passed".

Tests 1031 -> 1066. composer validate --strict, phpunit, PHPStan level 6 and
Pint all green.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EZ4u82zdoa8naYm75kppMC
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Deploying laravel-patent-box-tracker with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5042732
Status: ✅  Deploy successful!
Preview URL: https://fa7375da.laravel-patent-box-tracker.pages.dev
Branch Preview URL: https://feat-p4-brief.laravel-patent-box-tracker.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Deploying eval-harness with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5042732
Status: ✅  Deploy successful!
Preview URL: https://3a510aa0.eval-harness.pages.dev
Branch Preview URL: https://feat-p4-brief.eval-harness.pages.dev

View logs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a95fa24e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Brief/RunBriefing.php
// The single most useful line in the whole document when a judge is
// involved: the model saying, in prose, what was wrong.
if (is_string($details['judge_reason'] ?? null) && $details['judge_reason'] !== '') {
$md .= sprintf(" - judge: %s\n", $this->oneLine($details['judge_reason']));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep judge output inside the untrusted-data boundary

When llm-as-judge evaluates attacker-controlled content, judge_reason is itself model-generated and may contain prompt-injection text. This line emits it as ordinary Markdown after only collapsing whitespace, while the briefing's safety preamble specifically identifies fenced blocks as untrusted data; a coding agent receiving the briefing therefore sees this model output outside the declared boundary. Render the reason inside a safely sized fence or another explicit untrusted-data block.

Useful? React with 👍 / 👎.

Comment thread src/Brief/RunBriefing.php
// A value containing a fence would close the block early and the rest
// would read as prose — which for model output is exactly the boundary
// this document exists to keep.
$ticks = str_contains($value, '```') ? '````' : '```';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Size fences beyond every backtick run

When a quoted value contains four consecutive backticks, switching from three to four still lets the value close its own block and expose subsequent model-controlled text as prose. There is also a --format=github failure with merely three backticks in a value: the generated inner four-backtick closing delimiter terminates the fixed four-backtick wrapper in renderForGithub(), leaving the remainder outside that wrapper. Choose delimiters longer than the longest backtick run in the enclosed content, including a still-longer delimiter for the GitHub wrapper.

Useful? React with 👍 / 👎.

@lopadova
lopadova merged commit 5ac3aa1 into feat/p3a-trajectory Aug 24, 2026
2 checks passed
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.

2 participants