Skip to content

benchmark: add cve-agent model benchmark and fix what it exposed - #104

Merged
danielturull merged 43 commits into
mainfrom
devel
Sep 1, 2026
Merged

danielturull merged 43 commits into
mainfrom
devel

Conversation

@danielturull

Copy link
Copy Markdown
Contributor

Summary

42 commits adding a model benchmark suite for cve-agent and fixing the
correctness problems that building it exposed. The benchmark work came
first; using it as an instrument is what surfaced the rest.

The changes fall into four groups.

1. Benchmark suite for cve-agent (tests/benchmark/, tools/)

Runs a fixed roster of real CVEs across a set of models, then has an AI
judge compare each backport against the human reference patch. Four
committed rosters, with default ⊆ balanced ⊆ extended so runs stay
comparable, plus a separately-schema'd clean-apply roster:

Roster CVEs Composition
default 6 2 easy, 1 medium, 3 hard
balanced 8 3 easy, 2 medium, 3 hard
extended 20 9 easy, 2 medium, 9 hard
clean-apply 6 n/a — no-conflict cases

Every roster field is measured by a live cve-corrector probe
(--retier), never estimated. score_tier keys off the number of
conflicts and files involved, with thresholds terciling the pool's real
0–52 marker distribution. Resolution rosters contain only CVEs that
actually need resolution (recoverable exits 1/3/4); clean cherry-picks
live in the separate roster so the two measure different things.

Also adds tools/plot_benchmark_results.py for charts and
tests/integration/test_patch_compare.py (interdiff-based comparison).

2. Scoring correctness

exit_status collapsed four distinct outcomes into two, and it did so in
the wrong direction: a not-applicable skip exits 0 and scored as
success, while an honest escalation exits 14 and scored as failure.
Adds an outcome column parsed from the agent's own status line,
validated against all 110 logs of a completed run.

skipped then turned out to have several causes, so a follow-up adds
skip_reason, treating the corrector's exit code as authoritative rather
than trusting the model's claim. Of 17 skips in that run, only 6 were
genuine model verdicts — 6 were empty cherry-picks and 5 were a recipe
that does not build unpatched. After the split, every genuine dismissal is
a lone model out of five.

3. cve-corrector picking the wrong commit

Four bugs that all let a plausible-looking non-fix through:

  • Never cherry-pick a commit already in history. CVE-2024-6387's first
    recorded hash is the CVE-introducing commit and a confirmed ancestor of
    the recipe's own tag; picking it produced 30 conflicts across 7 files.
    Adds a real git merge-base --is-ancestor check (the old guard was a
    short-hash substring match against git log --oneline -10).
  • Try substantive commits before changelog-only ones. The
    metadata-only check was only consulted by the least-conflict fallback,
    never by the primary path. For CVE-2024-6387 that path returned a 2006
    ChangeLog-only commit on an unrelated branch as the fix for a pre-auth
    RCE, because it applied trivially. Verified by replaying both orderings
    against a real mirror.
  • Treat a Makefile-only commit as a version bump. CVE-2025-24857's
    only recorded hash was "Prepare v2017.11".
  • Report the real git am failure. The strip-level retry loop reused
    one result variable, so every failure was reported as the last -p3 --3way attempt regardless of cause.

4. Metadata ground truth from OE-Core

Audited the metadata fixture against 765 OE-Core CVE patches, using their
own Upstream-Status: Backport [url] headers as ground truth. 19 entries
did not contain the commit that was actually backported.
Adds
--correct-existing to the enrichment script (it previously only filled
empty entries).

Where a CVE needs more than one commit the result is recorded as an ordered
series, not flat hasheshashes are alternatives that stop at the
first that applies, whereas a series must apply in full. 8 series and 11
single-commit prepends.

The clearest case is CVE-2025-1153 (binutils, in all three resolution
rosters): the metadata held one commit, but the real fix is a three-commit
chain whose third commit reverts part of the first. That explains why all
five models failed it.

Testing

  • ruff check . — clean
  • mypy cve_agent cve_corrector cve_metadata_extractor shared — clean, 52 files
  • pytest --cov2058 passed, 6 skipped, 82.67 % coverage (threshold 65 %)
  • bash -n tests/benchmark/run_benchmark.sh — clean
  • Roster invariants enforced by tests: nesting, identical shared stats,
    no overlap between clean-apply and extended, per-recipe cap
  • Every bug fix ships a regression test; the git am one was confirmed to
    fail against the pre-fix code before being kept
  • All four rosters verified end to end via --list-cases

Notes for review

  • No new runtime dependencies. interdiff (patchutils) and matplotlib
    are optional and degrade gracefully.
  • tests/benchmark/README.md documents roster selection, tiering
    thresholds, and per-roster run cost.
  • Two CVEs were removed from the roster for measuring nothing rather than
    for being hard: one whose recipe fails its pre-patch build check, and one
    that now applies cleanly and moved to the clean-apply roster.

- Add AgentConfig.no_knowledge (default False) and thread it through
  __main__.py's arg parsing, config building, and main()
- main() passes knowledge_base=None to process_single_cve() when the
  flag is set, instead of constructing a real KnowledgeBase
- Guard save_knowledge_pattern() with an early return when
  config.no_knowledge or knowledge_base is None, so no pattern is
  written on success
- Widen knowledge_base's type to Optional[KnowledgeBase] through
  orchestrator.py's resolution pipeline (_resolution_loop,
  _run_single_resolution_attempt, _finalize_resolution,
  _handle_not_applicable, _handle_clean_apply, process_single_cve,
  _run_cve_pipeline) so a None knowledge base flows through safely
  without every call site needing its own guard
- Document the flag in README.md's AI-assisted backporting section
- Add tests/agent/test_no_knowledge.py: end-to-end no-KB-write/read
  checks, a control case confirming the pattern IS saved without the
  flag, direct unit checks on the guarded write path, and CLI-wiring
  tests (default false, flag sets true, threads into AgentConfig,
  main() skips constructing KnowledgeBase entirely)

Useful for benchmarking a model's unaided backporting performance
without knowledge-base assistance skewing the result.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The prior two commits (--no-knowledge flag, tests/benchmark suite)
left several docs stale:

- AGENTS.md: add tests/benchmark to the directory map (was entirely
  missing from the tests/{...} listing)
- .agents/summary/interfaces.md: add --no-knowledge to cve-agent's CLI
  flags reference
- .agents/summary/codebase_info.md: add tests/benchmark/ to the test
  structure listing
- CHANGELOG.md: add both changes under [Unreleased]

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Add git checkout -- .*, git format-patch .*, find \. .*, and
  grep .* to match the allowedCommands list in
  yocto-cve-backport-interactive.json
- Keeps both agents at parity per their documented "same
  capabilities" relationship

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
AGENT_INSTRUCTIONS.md explicitly documents bare 'git checkout <path>'
(without --ours/--theirs) as unavailable, but both interactive
manifests (.kiro/agents/ and cve_agent/agents/) allowed
'^git checkout -- .*$' since the agent's initial commit. Remove it
from all four manifest copies so behavior matches the documented
policy.

- Drop the 'git checkout -- .*' allowedCommands entry from both
  yocto-cve-backport-interactive.json copies
- Propagate git format-patch/find/grep parity additions to
  cve_agent/agents/yocto-cve-backport.json (the packaged fallback
  used when .kiro/agents/ is absent) to match the .kiro/ copy
- Add matching Bash(git format-patch:*), Bash(find .:*), and
  Bash(grep:*) entries to ClaudeBackend._ALLOWED_TOOLS so the Claude
  backend keeps parity with the kiro manifest, per
  test_claude_kiro_parity.py
- Drop the now-obsolete skip in
  test_destructive_command_is_rejected_interactive that carved out an
  exception for the removed bare-checkout allowance

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
conclusion.json lives in the persistent agent dir, which survives
across resolution attempts within a single CVE run. If an early
attempt escalated (needs_human) or marked not_applicable without
touching the tree, and a later attempt then actually resolved the
conflict without writing a fresh conclusion, the orchestrator re-read
the stale file and reported the CVE as escalated/skipped --
discarding a good, building resolution.

Clear conclusion.json at the start of every resolution attempt so the
orchestrator only ever observes the verdict of the session that just
ran.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
A CVE fix is often referenced by the pull request's merge SHA rather
than the commit that changed the code (e.g. setuptools' CVE-2024-6345
fix 88807c70, "Merge pull request #4332"). Git treats merges specially
in two ways that both broke the backport flow:

- `git cherry-pick <merge>` is refused before it starts ("is a merge
  but no -m option was given") and leaves no CHERRY_PICK_HEAD and no
  unmerged entries. find_least_conflict_commit() measured conflicts
  right after and therefore scored the merge as "0 conflicts" -- the
  best possible candidate -- and _handle_no_clean_apply() reported
  EXIT_CONFLICT over a pristine workspace.
- `git show` prints no diff (and no --name-only file list) for a merge,
  so the agent's file scope came out empty. context.md advertised an
  empty Allowed Files list, the pre-commit scope guard rejected every
  write, and the model could only escalate after a paid session.

- Add is_merge_commit(), cherry_pick_command() (adds `-m 1` for
  merges) and has_conflict_state() to cve_corrector.git_ops
- Score least-conflict candidates only when the pick actually started;
  discard picks git refused outright
- Verify real conflict state before raising ConflictError, else raise
  PatchError (exit 5) so no resolver is sent to a clean workspace
- Add merge_diff_flags() (`-m --first-parent`) to shared.git_runner and
  use it for the agent's file scope, context.md stat/diff hints, review
  diff and interdiff
- Compute the session file scope once in compute_allowed_files() so
  context.md and the scope guard cannot disagree
- Escalate before launching a session when the scope is still empty
- Add regression tests for both tools over real merge-commit repos

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Git resolves a relative `.gitmodules` URL against the superproject's
remote. In a cve-corrector workspace that remote is a local bare mirror
when --mirror-dir/--mirror-path is used (and otherwise unset, so git
falls back to the workspace path), so glib's

    [submodule "subprojects/gvdb"]
        url = ../../GNOME/gvdb.git

resolved to a nonexistent sibling directory such as
<workspace>/GNOME/gvdb.git and submodule init failed. For glib the
pre-patch build then failed too, since meson.build bootstraps the
submodule itself:

    meson.build:2128:0: ERROR: git submodule failed to init
    ERROR: glib-2.0-1_2.78.6-r0 do_configure: meson failed

The corrector reported that as exit 10 (pre-existing build failure), so
every glib CVE came out as "skipped" without the agent ever running.

- Add resolve_relative_submodule_url(), mirroring git's own algorithm
  (each `..` drops the base's last path component, `.` is skipped),
  for https/git/ssh, scp-like and local base URLs
- Anchor resolution on the upstream remote when it is a real remote URL,
  else on the canonical URL deduced from the CVE's fix-commit URLs
- Prefer a local mirror of the submodule when --mirror-dir holds one,
  matching how the superproject is fetched, and pass
  protocol.file.allow=always since git blocks local transports for
  submodules by default (CVE-2022-39253)
- Split submodule setup into init -> URL overrides -> update, because
  `submodule init` never overwrites an existing submodule.<name>.url
- Forward hash_details/mirror_dir through prepare_cve_branch
- Add unit and real-git integration tests for the glib/gvdb layout

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Runs cve-agent across a fixed roster of CVEs and a selection of models,
then an AI judge pass on diffs that came out moderately/majorly
different from the human reference backport.

- bench_lib.py: pure-Python helpers with no I/O beyond the paths passed
  in (score_tier, is_mirror_gap_only, count_conflict_markers,
  classify_diff_bucket, resolve_models, relative_cost_weight,
  observed_avg_credits/project_remaining_cost, total_spent,
  count_tool_calls, filter_for_judging, judge_diff)
- run_benchmark.sh: orchestrator sourcing tests/integration/
  test_common.sh for the OE tree lifecycle (reset_oe_tree,
  setup_cve_branch, run_cve_corrector, compare_patches_detailed).
  Always passes --no-knowledge to cve-agent so the benchmark measures
  unaided model performance. Single confirmation prompt covers both
  the agent-run and judge phases; prints a relative cost weight and
  (on resume) a projected remaining cost, both explicitly labelled as
  not credit predictions
- benchmark-roster.json: a fixed, committed 7-CVE roster (1 easy, 1
  medium, 5 hard spread across real conflict complexity) so every run
  tests the same CVEs. --retier re-probes and refreshes the roster's
  recorded stats in place with cve-corrector only (no AI cost); it
  never adds, removes, or reorders roster CVEs
- generate_benchmark_report.py: markdown report generator (per-model
  summary, per-tier bucket distribution, meaningful-vs-stylistic split
  for the judged subset)
- The fixed judge model (claude-opus-4.8) is deliberately not part of
  the benchmarked roster
- test_bench_lib.py / test_generate_benchmark_report.py: full unit
  coverage for the above

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Adds a 2-commit dependent series (shared fix with CVE-2026-26157) that
produces a genuine conflict plus a post-apply ptest failure, cheap to
rebuild -- a useful hard-tier addition to the fixed benchmark roster.

- tests/benchmark/benchmark-roster.json: new CVE-2026-26158 entry
  (tier hard, recipe busybox, series_len 2, conflict_markers 5)
- tests/integration/test-cve-metadata-agent.json: fixture metadata for
  the new CVE's two dependent fix commits
- tests/integration/test_common.sh: exclude the benchmark's per-model
  generated_<cve>_<model>_*.patch archives from
  compare_patches_detailed's original-patch discovery, so they aren't
  mistaken for a reference patch on a later model's comparison
- AGENTS.md, CHANGELOG.md: bump roster count references 7 -> 8

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Adds bench_lib.is_agent_env_failure(), which recognizes log markers
meaning the AI agent never actually ran because its runtime
environment was unusable (missing/unauthenticated backend CLI, or
agent configs that failed to install/refresh). Such a failure is not a
model-quality signal -- it would recur identically for every model and
CVE -- so the benchmark run loop can use this to abort early instead of
recording a wall of identical failures.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Adds bench_lib.ordered_roster_cases(), which enumerates the roster in
the same canonical run order run_benchmark.sh uses (tiers
easy->medium->hard, alphabetical by CVE within a tier) and numbers
them 1-based as 'cases', and select_cases(), which resolves a set of
1-based case numbers back to roster entries (de-duplicated, order
preserved, out-of-range/empty input rejected with a clear message).

These back a future --list-cases/--run-case CLI in run_benchmark.sh,
letting a user list the roster as numbered cases and re-run a subset
(e.g. to skip an expensive recipe or re-run a single case) without
touching benchmark-roster.json.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Previously a run whose generated patch touched a different (but
overlapping) set of files than the reference backport was classified
as 'file-mismatch' and never sent to the judge — even when most files
matched and only a handful were missing/extra.

- bench_lib.classify_diff_bucket(): a missing/extra fileset now
  resolves to the new 'partial' bucket when the two patch sets still
  share at least one file (via _common_file_count()), falling back to
  'file-mismatch' only for fully disjoint filesets
- bench_lib.scope_diff_to_common_files(): strips the one-sided
  missing/extra blocks out of a compare_patches_detailed diff patch,
  keeping only the two-sided (shared-file) blocks that are a
  meaningful backport-vs-reference comparison
- bench_lib.count_diff_changed_lines(): counts +/- lines in a unified
  diff, used to report a 'partial' row's diff_lines scoped to the
  shared files rather than the whole-patch divergence
- JUDGEABLE_BUCKETS now includes 'partial'; a partial overlap whose
  shared files come out byte-identical is recorded as a
  'structural-only' judge_results.csv verdict (no judge call) instead
  of being left unjudged
- judge_results.csv gains a 'scope' column ('full' vs 'partial') so a
  verdict is self-describing without cross-referencing agent_results.csv
- generate_benchmark_report.py: bucket distribution table and the
  meaningful-vs-stylistic split account for 'partial'/'structural-only'
- README.md: document the new bucket, CSV columns, and per-model
  artifact naming

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Follow-up to the CVE-2026-26158 roster addition: bump the remaining
7-CVE/7-roster-CVEs references to 8 and add the busybox row to the
fixed-roster table, which were missed when the roster entry itself was
committed.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Wires the library additions (agent-env-failure detection, roster case
ordering/selection, partial-fileset judge scoping) into the run loop,
plus operational robustness the benchmark lacked:

- --list-cases / --run-case <N...>: list the roster as numbered cases
  (run order) and optionally scope the agent run, cost estimate, and
  --retier to a subset — handy to skip an expensive recipe or re-run
  a single case
- Forward Ctrl+C to the agent: the agent runs under setsid so a
  per-run timeout can reap its whole process tree, which also means a
  terminal SIGINT never reached it. A trap now kills the agent's
  process group directly, waits briefly, force-kills if needed, resets
  the OE tree, and exits 130
- Preflight-abort when kiro-cli is missing/unusable
  (bench_lib.is_agent_env_failure): such a failure would recur
  identically for every model and CVE, so the run now aborts with a
  clear message and --resume hint instead of recording a wall of
  identical failures
- Archive each run's generated patch(es) to
  generated_<cve>_<model>_<file>.patch before the OE tree reset, and
  rename compare_patches_detailed's per-CVE differences report/diff
  patch to per-model filenames, so multiple models' comparisons for the
  same CVE no longer overwrite each other
- Judge phase: scope a 'partial' bucket's diff to the shared files
  before judging (bench_lib.scope_diff_to_common_files), record a
  'structural-only' verdict when nothing common differs, and write the
  new judge_results.csv 'scope' column
- Upgrade an old 4-column judge_results.csv header in place on
  --resume when it has no data rows yet (avoids appending 5-field rows
  under a stale 4-field header)
- README.md: document --list-cases/--run-case and the per-model
  artifact naming scheme

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
After the fixes, it doesn't need the cve-agent to generate a
patch

Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
AGENT_INSTRUCTIONS.md caps each per-file stanza of the AI's
`Conflicts Resolved:` block at a few bullets and a few dozen words, but
nothing enforced it, so the notes grew into multi-paragraph narration of
the investigation instead of stating the adaptation.

- Add cve_agent/commit_notes.py: a pure parser and budget checker (no git
  calls, no package imports) so the hook, the orchestrator gate and
  review.py can share one definition of the rules
- Cap at 3 bullets and 48 words per file, warning from 40 words: guidance
  can stay softer than enforcement, and a stanza a few words over must
  not cost a whole session
- Count bullets, not physical lines: a 40-word note wrapped at 72 columns
  spans 5 lines, so counting those would make the word budget
  unreachable; continuation lines fold into their bullet
- Charge anything the parser cannot attribute to a file to one synthetic
  stanza, so a malformed header, prose after the trailers, or text split
  across two blocks cannot switch enforcement off
- Recognise decorated and misspelled headings (`#### `, `**...**`,
  lowercase, blockquote) without mistaking an unrelated `## Description`
  in a preserved upstream body for notes
- Exempt `<file>: omitted (...)` one-liners wherever they appear,
  including wrapped reasons, but treat the same text as a bullet inside a
  stanza as prose
- Restrict trailer detection to known keys, so a body line like
  `Reason: ...` no longer truncates the block and hides later stanzas
- Signal a rejection with exit code 3, not 1: the interpreter itself
  exits 1 on `-m <missing module>` and 2 on a usage error, and callers
  must be able to tell "over budget" from "the checker broke"
- Have review.py import the note-block markers from the new module, with
  a deliberately broader set for de-duplication than for the budget

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The budget checker only helps if something runs it while the AI is still
in a position to fix the message. A commit-msg hook fires on both
`git cherry-pick --continue` and `git commit --amend`, and a rejection is
non-destructive: the cherry-pick stays in progress and .git/MERGE_MSG
survives, so the AI can shorten the notes and re-run the same command.

- Add install_notes_hook()/remove_notes_hook(), mirroring the existing
  pre-commit scope guard: same hooks dir, same backup handling, same
  session lifecycle
- Resolve the interpreter and package root at install time and quote them
  with shlex, since the workspace is an unrelated repository
- Fail open: only the checker's dedicated rejection status blocks a
  commit. A missing interpreter or unimportable package warns and allows,
  because the AI is instructed never to bypass the hook and a check that
  cannot run would otherwise deadlock the session
- Install and remove alongside the scope guard in guarded_session(), so
  the hook is gone before review.amend_commit_with_summary() appends its
  own change summary
- Cover the real recovery path end to end: reject on --continue, shorten
  MERGE_MSG, --continue again

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Both guards write into a user's workspace and restore what they found, so
the edge cases matter: a lost backup means a developer silently loses
their own hook, and a leaked guard means later commits are policed by a
session that already ended.

- Skip the backup when the existing hook carries our own marker: a second
  install without an intervening removal (a crashed session) previously
  backed up our hook over the user's real backup, then restored ours as
  if it were theirs
- Read a pre-existing hook with errors='replace': a hook in another
  encoding raised UnicodeDecodeError, which is not an OSError, aborting
  the session and leaving the first guard installed
- Install both guards inside the try, so a failure between the two
  installs cannot leave the first one behind
- Warn when core.hooksPath is set: git then ignores .git/hooks entirely
  and both the file-scope guard and the note budget are inert while
  appearing installed
- Isolate the real-git tests from the developer's global git config,
  which would otherwise disable the hook under test

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The hook only sees commits the AI makes during a session. A commit
created before the hook was installed, or edited by hand, reaches HEAD
unchecked — so validate once more before the change is shown for
approval.

- Add validate_commit_notes() and check HEAD's notes after the session,
  before request_approval()
- On a hard violation, write the overage to human_feedback.txt so the
  next session gets the exact counts and the instruction to amend the
  message only, appending to any feedback a human already left
- Cap the bounces at 2, then accept with a warning: discarding a
  technically correct backport over commit-message prose is the worse
  failure
- Do not spend a resolution attempt on a prose bounce. It previously
  consumed one, so an always-verbose AI exhausted max_retries and was
  reported ESCALATED — exactly the outcome the cap exists to prevent.
  total_attempts still counts bounces, so --max-total-attempts bounds a
  pathological loop
- Reset the bounce allowance on a phase change, so notes added during a
  later build or ptest amend are still checked
- Record every verdict, including non-blocking warnings, in the session
  audit log

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The feedback section prefixed only the first line with '> ', which was
fine for a one-line human note but breaks a multi-line report out of the
markdown blockquote — now that the note-budget gate writes a six-line
verdict there, most of it rendered as unquoted prose.

- Prefix every line of human_feedback.txt, keeping blank lines as '>'

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Several documents contradicted the budget they were supposed to describe.
The worst offender was ptest.md, which asked for exactly the content
AGENT_INSTRUCTIONS.md forbids -- which cases failed, and the pass/fail
counts -- and which cannot fit in 40 words. Since a ptest retry amends
the commit, that instruction reliably pushed the notes over budget.

- State the enforced numbers instead of "at most 2-3 lines (~40 words)":
  3 bullets, ~40 words, rejected past 48, and note that wrapping is free
- Document the recovery path, and that `git commit --amend --no-edit`
  must never be used for it: it resubmits the identical rejected message,
  so the hook rejects it again forever. Flag the same trap next to the
  build-retry instruction that legitimately uses --amend --no-edit
- Distinguish the two hooks in Scope Rules: pre-commit rejects
  out-of-scope files, commit-msg rejects over-long notes
- Require every line in the block to sit under a file header, and
  document `<file> (0 conflicts):` for a build or ptest fix to a file
  that had no merge conflict -- previously there was no valid header
  shape for that case
- Unify the two `<file>: omitted (...)` spellings and state that they are
  exempt from the budget
- Replace ptest.md's failing-case narration with a one-bullet rule, and
  point conflict.md at the Commit Note column as the target length
- Add a test that fails if the documented numbers and the enforced
  constants ever diverge

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
`^git show .*$` in both agent manifests and `Bash(git show:*)` for the
Claude backend already permit this form; only the AI-facing list in
AGENT_INSTRUCTIONS.md never mentioned it, so the agent had no reason to
know it could read a file as committed. No permission is granted here.

- Document `git show HEAD:<path>` and `git show <sha>:<path>`, and when
  to prefer them over the file-reading tool
- Lock the form in with permission tests for both manifests and a parity
  test for the Claude allow-list, so a future narrowing of the `git show`
  pattern fails the suite

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The benchmark and integration harnesses compared a generated backport
against the reference patch as two sets of +/- lines, which reported
differences that are not differences. oe-core's less CVE-2024-32487.patch
is whitespace-damaged (tab-indented context lines lost their leading
space) and ends its git signature '--' rather than '-- ', so the phantom
'--' removal was counted as the sole divergence while the per-file diff
handed to the AI judge was pure whitespace noise.

Compare the two patch sets with interdiff instead: reduce each side to its
diff body and diff the effects, so commit metadata, hunk offsets, reordered
hunks and signature spelling stop registering as changes.

- Repair whitespace-damaged context lines before invoking interdiff, which
  otherwise refuses the input ("Whitespace damage detected in input") and
  loses the comparison entirely; hunk boundaries come from the @@ line
  counts so metadata and the signature are untouched
- Add allow_empty to generate_interdiff so an empty delta ("the patches are
  equivalent") is distinguishable from "could not compare"
- Separate files touched by only one side behind ONE_SIDED_MARKER, which
  scope_diff_to_common_files truncates at; reports in the old format still
  use the legacy span heuristic
- Fall back to the line-set comparison when patchutils is absent, and stop
  counting a '--' signature as a removed diff line there too
- Fix _extract_files_touched mangling paths via lstrip('b/'), a character
  set strip that turned 'b/bin/x.c' into 'in/x.c'

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
judge_results.csv stored a bare 'meaningful'/'stylistic' verdict, so a
surprising classification could not be audited without re-reading the diff
by hand. Comment churn also counted as divergence: a backport that only
reworded a comment was sent to the judge and could come back 'meaningful'.

- Ask the judge for one or two sentences naming the construct behind its
  decision, and store them in a new 'reason' column. Split sentences only on
  a period followed by whitespace, so C identifiers such as '!S_ISLNK' are
  not cut in half, and drop kiro-cli's credits footer
- Write judge rows with csv.writer rather than echo, since the reason is
  free-form prose containing commas and quotes
- Strip comment-only changed lines before judging and answer 'comment-only'
  with no model call when nothing else remains. Comment syntax is chosen per
  file from the diff headers ('//' and '/* */' for C-like sources, '#' only
  for shell/Python/make, never for C where '#if' is meaningful), and
  block-comment state is tracked per diff side so pointer code such as
  '*p++ = *s++;' is not mistaken for a comment continuation
- Count 'comment-only' in the report and add a Judge Reasoning table,
  suppressed for results dirs judged before the column existed

diff_lines and diff_bucket in agent_results.csv still count comment lines;
only the judge ignores them.

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Re-probed the 7 roster CVEs with cve-corrector only (--skip-build
--skip-ptest, no AI cost) against openembedded-core scarthgap. The roster
membership is unchanged; only the recorded stats move.

- conflict_markers: CVE-2024-32487 3->2, CVE-2024-6345 0->2,
  CVE-2025-1153 52->45, CVE-2025-47183 6->1, CVE-2025-47203 12->7,
  CVE-2026-26158 5->3
- diff_lines: CVE-2026-0990 18->32
- Every tier and exit_code is unchanged, so the easy/medium/hard split the
  benchmark runs is identical

The wholesale reordering in the diff is not a content change: --retier
rewrites the file with json.dump(sort_keys=True), which alphabetizes the
CVE entries and moves the leading "_comment" to the end.

Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The _comment entry duplicated the README and had gone stale: it still
claimed "the exact same 8 CVEs" after CVE-2025-4373 was removed, and
--retier's json.dump(sort_keys=True) kept relocating it to the end of the
file on every refresh.

- Move the one fact it carried that the README lacked -- that every
  tier/exit_code/diff_lines/series_len/conflict_markers value is measured
  data from a live cve-corrector probe plus the historical bulk run -- into
  the roster section of tests/benchmark/README.md
- Correct the same 8-CVE staleness in the README and in run_benchmark.sh's
  --help text: the roster is 7 CVEs (1 medium, 6 hard), no easy entry
- Update the documented conflict_markers spread to the refreshed range
  (0 to 45, was 0 to 52)

The '_comment' filters in bench_lib.ordered_roster_cases and
run_benchmark.sh are kept: they cost nothing and let a comment be
reintroduced without breaking the roster readers.

Assisted-by: kiro:claude-opus-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Add three nested rosters: default (7 CVEs), balanced (20, 6/6/8),
  extended (40, 6/10/24). Each is a superset of the previous so results
  stay comparable. All fields are measured from the 289-CVE bulk run in
  tests/integration/test-results/bulk_20260626_081658/
- Add --roster default|balanced|extended|<path> flag to run_benchmark.sh
  with a startup log line recording which roster ran
- Add tools/plot_benchmark_results.py (matplotlib) with 6 PNG charts:
  outcome by model, raw bucket distribution, cost, quality-vs-cost
  scatter, effort, and per-CVE x per-model outcome matrix
- Swap minimax-m2.5 out of the default model set (21.12 credits per
  usable backport in bench_20260828_145923) and add claude-sonnet-4.8
- Add tests/benchmark/test_benchmark_roster.py: schema, tier consistency,
  nesting chain, composition guards (44 tests across all three files)
- Add tests/tools/test_plot_benchmark_results.py: outcome classification,
  aggregation, ranking, matrix (33 tests, no matplotlib dependency)
- Document roster selection rules, run-cost comparison table, and the
  per-CVE credit cap (20, non-binding on current data) in README

Assisted-by: kiro:claude-sonnet-4.9
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Allow read-only git ref/object inspection (git branch, describe,
  show-ref, ls-tree, cat-file -t/-s/-p, grep) and a single-line-range
  sed -n form, in AGENT_INSTRUCTIONS.md, both agent manifests, and the
  Claude backend's allowed-tools list
- Let git status --porcelain -- <path>, git restore --staged <path>,
  and git checkout --ours/--theirs <path> take several paths per call
  instead of one
- Loosen the echo allowlist from two hardcoded exit-code messages to any
  double-quoted text ending in $? or ${PIPESTATUS[0]}
- Add 'Undoing a Bad Cherry-Pick' to AGENT_INSTRUCTIONS.md: four cases
  (abort/skip in progress, amend a wrong commit, recover pre-image
  content from original-version, escalate on a wrong-commit
  cherry-pick) now that git reset/revert/update-ref/branch -f remain
  unavailable
- Point instructions/build.md and instructions/conflict.md at the new
  section instead of leaving the agent to discover on its own that
  reset/revert cannot be used
- Extract the kiro-cli execute_bash guard model (file-redirect
  detection, compound-command splitting, allowedCommands matching) out
  of test_security.py into tests/agent/allowlist_model.py so
  test_agent_permissions.py can reuse it instead of duplicating it
- Add tests/integration/test_enrich_metadata_from_oe.py and extend
  enrich_metadata_from_oe.py's docstring to explain why URL-derived
  hashes from Upstream-Status: Backport are tried before the less
  reliable git format-patch From-header SHA

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- score_tier() now derives easy/medium/hard from conflict_markers and
  distinct files_involved (git's own CONFLICT (content) lines), not
  diff_lines/series_len -- restricted to a recoverable exit code
  (conflict/ptest/build), and raises otherwise
- Add count_conflicted_files() alongside count_conflict_markers()
- Purge every clean-exit entry from the three resolution rosters
  (default/balanced/extended): a clean apply has no conflict to
  size, so it was being scored 'easy'/'medium' on diff size instead
  of measuring anything about conflict-resolution difficulty
- Re-probe and rebuild default (6), balanced (8), and extended (22)
  against the current OE-Core (scarthgap) with the new metric;
  nesting and shared-entry equality preserved
- Add benchmark-roster-clean-apply.json (5 CVEs): a separate,
  non-nested roster for CVEs whose cherry-pick applies with no
  conflict, using phase: 'clean_apply' instead of tier since
  score_tier has nothing to measure there. Exercises cve-agent's
  mandatory-analysis path (_handle_clean_apply), not conflict
  resolution
- Wire --roster clean-apply through case listing, cost estimate,
  and the phase-2 run loop (ROSTER_TIERS), and give retier_roster()
  a symmetric guard: only accepts a recoverable exit for the three
  resolution rosters, only a clean exit for clean-apply -- either
  guard leaves cached stats untouched and warns instead of
  overwriting real data with a non-signal
- Rewrite test_benchmark_roster.py and test_bench_lib.py for the
  new schema/thresholds; fix generate_benchmark_report.py's
  per-tier loop to not silently drop a non-easy/medium/hard tier
- Backfill 15 CVE-metadata-extractor entries used while building the
  new rosters (test-cve-metadata-agent.json), and rename the diff-
  bucket threshold classify_diff_bucket() actually uses to
  MODERATE_DIFF_LINES_THRESHOLD so it reads as unrelated to
  score_tier's own thresholds

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
- Add 'minor' to JUDGEABLE_BUCKETS, so a small (but nonzero) divergence
  from the reference patch gets an actual meaningful/stylistic verdict
  instead of being assumed stylistic by bucket alone. 'identical' stays
  excluded (a zero-line diff has nothing to judge); 'file-mismatch'
  stays excluded (disjoint filesets leave no shared-file diff to judge)
- Update README, report header text, and generate_benchmark_report.py's
  docstring/section header to say minor/moderate/major/partial
- Rename/rewrite the affected tests in test_bench_lib.py and
  test_generate_benchmark_report.py for the new split

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
claude-sonnet-4.8 is not a model kiro-cli recognizes -- every invocation
failed instantly with 'Model does not exist', escalating after 3 retries
for every CVE/model combination in the default set. Rename it to
claude-sonnet-4.6, which was already present (and valid) as a full-tier
entry; drop the now-duplicate full-tier claude-sonnet-4.6 entry since the
default-tier one replaces it.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The strip-level retry loop reused one am_result variable, so the
PatchError it raised always described the final '-p3 --3way' attempt.
Since stripping three path components from a normal -p1 patch produces
'git diff header lacks filename information', every failure in this
path was reported with that message regardless of its actual cause,
sending debugging down the wrong track.

- Record each attempt's stderr as (label, stderr) and report the
  failure at the *detected* strip level, listing the other attempts
  for context
- Log why each git apply variant (-p1, -C0, --3way) refused the patch;
  these were discarded entirely, which made an over-broad conflict
  resolution that no longer applies to the target tree look identical
  to a malformed patch
- Add regression tests for both: the strip-level test fails against
  the old single-variable behavior

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
A wholesale 'git checkout --theirs' on a heavily diverged file imports
upstream's whole file, which can reference APIs the stable branch lacks
and break the build on hundreds of unrelated errors. Recovering from
that needed the stable file back, and no available command could do it:
tee is confined to agent-dir .log files, so a 15k-line file could not
be routed back into the tree, and rewriting it through the file-editing
tool is not viable at that size.

In bench_20260831_140123 this cost two escalations on CVE-2025-1176
(binutils bfd/elflink.c), both models correctly identifying the small
surgical fix they wanted to apply but unable to undo the wholesale
import first.

Allow 'git checkout original-version -- <path>...', pinned to that one
ref so it can only roll a tracked file back to its pre-cherry-pick
state and cannot become a general checkout primitive. Arbitrary
revisions (HEAD~1, branch names, original-version~1), flag injection
and the bare pathless form all stay denied.

Update AGENT_INSTRUCTIONS.md 'Undoing a Bad Cherry-Pick' case 3 and the
conflict.md fragment to prescribe it over the tee + rewrite route.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
The exit status collapses four different results into two, and the two
collisions point in opposite directions:

  skipped   exits 0 -- the model declared the CVE not applicable and
            produced no patch. A wrong verdict dismisses a live
            vulnerability while still scoring as a clean pass.
  escalated exits 14 -- the model declined to guess and asked for a
            human, which is the correct outcome when the fix cannot be
            made within the allowed file scope, yet it scores as a
            failure identical to a real breakage.

Ranking models on the exit code therefore rewards a confident wrong
dismissal over an honest escalation. In bench_20260831_140123 that
inverted the leaderboard: claude-opus-5 showed 4 'failures' and
claude-sonnet-4.6 only 1, but split by outcome opus-5 has zero outright
failures (4 escalations, 2 dismissals) against sonnet-4.6's 4
dismissals -- and three models marked CVE-2024-6387 (regreSSHion, on
openssh 9.6p1, inside the affected 8.5p1-9.7p1 range) not applicable.

- Add bench_lib.parse_agent_outcome, reading the ResultStatus line that
  cve_agent/__main__.py already prints; validated against all 110 logs
  of that run (exit 0 -> 74 conflict_resolved + 17 skipped, exit 14 ->
  14 escalated + 5 failed)
- Add an 'outcome' column to agent_results.csv and log it per run
- Report: add a per-model outcome table and a 'Not-Applicable Verdicts
  (verify these)' audit showing how many models agreed on each
  dismissal, so a lone dismissal among successful backports is visible
- Older results dirs without the column still report; those rows are
  counted as 'no outcome' rather than assumed successful

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
A commit reachable from HEAD shipped in this version, so it cannot be the
fix being backported. Replaying one is worse than a no-op: git pits the
stale change against whatever superseded it, which surfaces as a large,
entirely plausible conflict.

CVE-2024-6387 (regreSSHion) is the case that exposed this. Its metadata
carries four hashes and the first, 752250caa "revised log infrastructure"
(2020), is the commit Qualys identified as *introducing* the regression.
Verified against the openssh mirror: it is an ancestor of V_9_6_P1, the
recipe's own version. Cherry-picking it produced 30 conflicts across 7
files, and resolving toward the incoming side would have reverted later
hardening (faf2b86a4). All five benchmarked models were handed that
commit; three concluded from the resulting mess that the CVE was not
applicable, which for a live pre-auth RCE is the worst possible outcome.

The existing guard only matched a short hash against the output of
'git log --oneline -10', so anything older than ten commits slipped
through.

- Add git_ops.is_ancestor_of_head using 'git merge-base --is-ancestor',
  treating only exit 0 as a positive answer so a failed probe (unknown
  object, no HEAD) never silently discards a viable candidate
- Skip ancestors in apply_single_commits and in find_least_conflict_commit,
  where an ancestor is especially harmful: superseded code can score
  *fewer* conflicts than the real fix's genuine adaptation work and so be
  actively preferred

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
A commit touching only the top-level Makefile is a release bump, not a
fix -- upstreams like u-boot cut releases by editing the VERSION and
PATCHLEVEL variables there. It was not recognised as metadata-only, so
find_least_conflict_commit could rank it ahead of a real candidate.

CVE-2025-24857 is the case: its sole metadata hash, c253573f3e2
"Prepare v2017.11", changes one Makefile line and is dated 2017, eight
years before the CVE. Verified against the u-boot mirror; the actual fix
is 87d85139a96 "fs: fat: Perform sanity checks on getsize in
get_fatent()", which touches fs/fat/fat.c and names the CVE in its own
commit message. Two of five benchmarked models concluded from the
resulting nonsense that the CVE did not apply.

Add Makefile, Makefile.am and Makefile.in to _METADATA_ONLY_FILES. The
check requires *every* file in the commit to match, so a genuine fix
that also edits a Makefile is unaffected.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Allowed-file scope is computed from the paths an upstream commit touches,
with prefix-variant fallbacks for src/lib/source and monorepo
subprojects/. Nothing covered a file the stable branch simply keeps
somewhere else, so scope could name a path that does not exist in the
tree being patched -- leaving the agent unable to edit the very file it
had to fix.

libsoup is the case: 3.x holds websocket sources in libsoup/websocket/,
while the 2.4 branch behind the libsoup-2.4 recipe keeps them flat in
libsoup/. Verified against the libsoup mirror -- the metadata's commits
are all 3.x, and the CVE-2024-52532 backport commit on the stable branch
uses libsoup/soup-websocket-connection.c. Four of five benchmarked models
escalated it, one of them asking for mkdir so it could recreate the
upstream directory (which would have been the wrong fix).

expand_path_variants now falls back to searching the tree for the
basename when the upstream path is absent, accepting only an unambiguous
single match -- an ambiguous name such as Makefile or meson.build must
never widen scope to the wrong file. The walk skips .git and build
output directories and is capped, since scope resolution runs per
session.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
apply_single_commits returns the first candidate that cherry-picks, and an
irrelevant commit is *more* likely to apply cleanly than a real fix that
needs adaptation. The metadata-only check existed but was consulted only
by find_least_conflict_commit, the fallback path -- so in the primary path
a trivial commit sitting ahead of the real fix won outright.

CVE-2024-6387 shows how bad that gets. Its metadata carries four hashes
from three repositories (openssh-portable twice, plus the openela-main
and hpn-ssh forks). Verified against the openssh mirror at V_9_6_P1:

  752250caabda  skipped, ancestor of HEAD (the introducing commit)
  e1f438970e5a  skipped, bad object (openela-main fork)
  81c1099d22b8  the genuine 9.8 fix; conflicts against 9.6p1
  6518797401f2  a 2006 ChangeLog-only commit on the V_4_4 branch,
                not an ancestor of 9.6p1, and applies trivially

Walking that list in order, the real fix conflicts and the 2006
documentation commit applies -- so the corrector reported success having
"backported" a twenty-year-old changelog edit as the fix for a pre-auth
RCE. Confirmed by replaying both orderings against the mirror.

Partition candidates into substantive and metadata-only, and try the
substantive ones first. Metadata-only commits are kept as a last resort
rather than dropped, preserving the "unless no better option" behaviour
find_least_conflict_commit already documented.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
ResultStatus.SKIPPED covers several unrelated situations and only one of
them is a claim by the model, so the not-applicable audit added in the
previous commit accused models of dismissing CVEs they were never asked
about. Of the 17 skipped runs in bench_20260831_140123:

  6  ai_not_applicable  the model's own reasoning -- the only kind worth
                        auditing
  6  empty_cherry_pick  the cherry-pick produced no changes, so the fix
                        looked already present
  5  build_preexisting  libpng did not build even unpatched (corrector
                        exit 10); no model was ever consulted

CVE-2025-64505 accounted for all five environmental skips and so appeared
in the audit as a unanimous 5-of-5 dismissal, which was simply wrong. Two
of the three CVE-2024-6387 dismissals were empty cherry-picks rather than
judgement calls, which is itself a symptom of handing over a commit
already reachable from HEAD -- the case is_ancestor_of_head now filters.

- Add bench_lib.parse_skip_reason and a skip_reason column. The
  corrector's exit code outranks the printed AI wording, because the
  already-applied path prints the same "Agent concluded ... is not
  applicable" line and trusting that string alone misreports a mechanical
  skip as a model judgement
- Audit only ai_not_applicable rows (and rows with no recorded reason, so
  older results dirs are not silently dropped); report mechanical skips in
  their own section
- With this, every remaining genuine dismissal is a lone model out of five,
  which is the strongest available signal that each one is an error

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
enrich_metadata_from_oe.py only ever filled CVEs that had *no* hashes, so
an entry holding confidently wrong ones was never questioned. But the
recipes already carry the answer: a maintainer wrote
"Upstream-Status: Backport [<url>]" specifically to name the upstream
commit they backported, which makes it the one authoritative source
available offline.

Audited the 107-entry fixture against the 765 CVE patches in OE-Core
(scarthgap): 19 entries did not contain the commit that was actually
backported. Verified against local mirrors, the failure modes are

  CVE-2025-24857  u-boot     only "Prepare v2017.11", a 2017 release bump;
                             real fix 87d85139a96 touches fs/fat/fat.c and
                             names the CVE in its message
  CVE-2024-3596   wpa-suppl. 28 commits from FreeRADIUS and pam_radius,
                             different projects entirely; the recipe needs
                             9 hostap commits
  CVE-2024-5569   zipp       a merge commit rather than the fix itself
  CVE-2025-1153   binutils   one commit of a three-commit chain

Add --correct-existing to prepend the missing ground-truth commits,
keeping the tracker hashes behind them as fallbacks, and apply it.

Where more than one patch exists the commits are recorded as a `series`,
not appended to `hashes`: cve-corrector treats hashes as alternatives and
stops at the first that applies, which for a dependent chain leaves a
partial fix. CVE-2025-1153 is the clearest case -- three patches whose
third reverts part of the first, so applying only the first is worse than
applying none. Order comes from the patch filenames, matching how OE
applies them (0019-CVE-2025-1153-1.patch .. 0021-...-3.patch, in that
SRC_URI order). Eight entries gained a series this way; header-derived
SHAs stay out of chains, since they are often local or rebased commits
that do not exist upstream.

Note that this changes what cve-corrector will cherry-pick for several
roster CVEs, so the rosters' recorded conflict_markers/files_involved and
tiers are now stale -- run --retier before drawing conclusions from a new
benchmark run.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
libpng fails cve-corrector's pre-patch build check in a stock scarthgap
environment (EXIT_BUILD_PREEXISTING, exit 10), so the recipe is already
broken before any patch is applied. In bench_20260831_140123 all five
models were recorded as having "skipped" it without ever being consulted,
which is how it came to look like a unanimous not-applicable verdict.

A case that fails identically for every model measures nothing about model
quality -- the same reasoning selection rule 2 already applies to mirror
gaps -- and it cost five agent invocations per run to learn that.

extended is now 21 CVEs (10 easy, 2 medium, 9 hard). It remains a superset
of balanced and default, which never contained this CVE, so the nesting
chain and the 2-per-recipe cap are unaffected. The metadata entry is kept,
so re-adding it is a one-line change once the recipe builds.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Re-probed the extended roster against the corrected metadata. Five entries
shifted, none across a tier boundary:

  CVE-2024-6387   openssh       23/14 -> 18/9   no longer replays the
                                                CVE-introducing commit
  CVE-2025-1153   binutils      45/28 -> 52/28  now applies all three
                                                commits of the real series
  CVE-2026-26007  cryptography  16/13 -> 10/7
  CVE-2025-47273  setuptools     6/5  ->  7/5
  CVE-2026-24049  wheel          2/2  ->  3/2

CVE-2025-1153 is the only one shared with default/balanced; its refreshed
stats are copied there verbatim, as test_shared_entries_are_identical
requires.

CVE-2025-24857 now applies cleanly, so it moves to the clean-apply roster.
It previously failed with an empty cherry-pick because its only recorded
fix commit was c253573f3e2 "Prepare v2017.11" -- a 2017 release bump, and
an ancestor of the recipe's own 2024.01. With the real fix in place
(87d85139a96, recovered from the Upstream-Status header of OE-Core's own
patch) and ancestors now skipped, the retier log shows it auto-merging
fs/fat/fat.c and reproducing the reference backport exactly: "Differences:
0 lines / Patches are equivalent". The retier guard left the stale entry
untouched rather than recording exit 0 in a resolution roster, which is
what surfaced the move.

extended is now 20 (9 easy, 2 medium, 9 hard) and clean-apply 6. Nesting,
the no-overlap rule between clean-apply and extended, and the 2-per-recipe
cap all still hold.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
Comment thread tests/corrector/test_submodule_relative_url.py Fixed
CodeQL flagged the substring check as
py/incomplete-url-substring-sanitization (high): the host may sit at an
arbitrary position in the string. It is a test assertion over a hardcoded
input rather than a sanitizer, so it was not exploitable, but a substring
match is also a weaker assertion than this test wants -- it would pass for
a value that merely mentions the host somewhere, which is exactly the case
the test exists to rule out.

Assert the exact derived base instead. This subsumes both dropped
assertions: a precise match already implies the value is non-None and does
not start with a local path separator.

Assisted-by: kiro:claude-sonnet-5
Signed-off-by: Daniel Turull <daniel.turull@ericsson.com>
@danielturull
danielturull merged commit fbf218f into main Sep 1, 2026
8 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