Repository navigation
fix(metadata): report per-skill enrichment reasons on failing runs - #801
Merged
siqinvidia merged 1 commit intoOct 30, 2026
Merged
Conversation
A regeneration run that fails validation prints only the summary line naming which skills produced no entry, never why enrichment failed for them. The reasons were already collected in `skill_warnings`, but `main()` returned at the `if errors:` gate before reaching the block that prints them and writes /tmp/skill-warnings.json, so the detail was gathered and then discarded. The recovered case (a published skill kept from the baseline) does not set `errors`, so it always reported correctly. That asymmetry is why the gap stayed hidden: the only path that loses the reason is a brand new skill, which has no prior entry to fall back on. Write the warnings file before the error gate and print the reasons alongside the summary. The workflow already renders that file as a "Skills that could not be regenerated" section in the tracking issue, so no workflow change is needed. Reporting only; enrichment, entry building, validation and output serialization are untouched. Verified old vs new across all six mode/path combinations: exit codes identical everywhere, logs and warnings-file presence identical except on the targeted path. Signed-off-by: Jason Dudash <jdudash@nvidia.com>
jasonnvidia
marked this pull request as ready for review
October 30, 2026 00:54
jasonnvidia
requested review from
mosheabr,
sayalinvidia and
siqinvidia
as code owners
October 30, 2026 00:54
Collaborator
Author
|
@jim-nvidia |
siqinvidia
approved these changes
Oct 30, 2026
siqinvidia
left a comment
Collaborator
There was a problem hiding this comment.
Reviewed f3d3de2. The change writes collected per-skill warnings before the validation error gate and includes their reasons in the failure log, preserving the existing structured warnings contract and validation decisions. Independently ran test_generate_skill_metadata.py: 19/19 assertions pass. generate-skill-metadata.py --check --no-ai confirms byte-stable output for all 430 skills. All applicable GitHub CI checks pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The issue
When a regeneration run fails, the tracking issue tells us which skill broke but never why.
The generator asks an AI model to assign five metadata fields to each skill. For a skill already in
metadata.json, a failed enrichment is harmless — the previous values are carried forward. A brand new skill has nothing to fall back on, so a failed enrichment fails the whole run and blocks metadata updates for every skill.That happened five times in a row on the three Run:ai skills synced in #782, reported in #783. The model is non-deterministic, so a different skill failed on different runs:
runai-get-cluster-health-summarythree times,runai-get-utilization-overviewtwice. The sixth run succeeded on its own with no code change, and all three skills have been inmetadata.jsonsince.Throughout, we could not tell which field failed enrichment or why. The reason was being collected in
skill_warnings, butmain()returns at theif errors:gate before reaching the block that prints it and writes/tmp/skill-warnings.json, so the detail was gathered and then discarded.The recovered case (a published skill kept from the baseline) does not set
errors, so it always reported correctly. That asymmetry is why this stayed hidden for so long — the only path that loses the reason is a brand new skill, which is exactly the path that hard-fails.The fix
Write the warnings file before the error gate, and print the reasons alongside the summary line.
The workflow already reads
/tmp/skill-warnings.jsonand renders it as a "Skills that could not be regenerated" section in the issue body, so no workflow change is needed — the generator simply never wrote that file on the one path that fails.This is reporting only. Enrichment, entry building, validation, and output serialization are untouched, so nothing that decides the contents of
metadata.jsonchanged.Example output
Before — the actual log from run 40729153708, as posted to #783:
Actionable content: re-run, or exclude the skill. Nothing about the cause.
After — same failure, reproduced through the test harness:
And
/tmp/skill-warnings.jsonis now produced on this path, which the workflow surfaces at the top of the issue:{ "stale_skills": [ "skills/runai-get-cluster-health-summary: AI returned UNRESOLVED for `discovery.activity_tags` on skill `runai-get-cluster-health-summary`; metadata cannot be auto-generated. Please set this field manually or update the skill." ] }Risks
Low, and deliberately narrow in scope. Old vs new compared across all six mode/path combinations:
--check --no-ai(PR CI)--report-only--no-aiExit codes identical everywhere. The only changed scenario is the target, and its diff is purely additive — three lines added, nothing removed or reworded.
Two residual risks worth knowing:
The failure path now has a filesystem side effect where it previously had none. If
/tmpwere unwritable, a run that used to exit 1 with a clean validation message would instead raise. Not reachable onubuntu-latest, and the success path already wrote to the same location, so CI exposure is unchanged — but the trigger condition is new. Left unguarded to match existing style; happy to wrap it if reviewers prefer.Failures with an unrelated root cause may now lead with a skills section. If a run fails on something else (schema violation, unknown subdomain) and some other skill separately produced a warning, the issue body shows the "could not be regenerated" block above the collapsed log. Everything in it is accurate, but it sits above the actual blocker.
Not verified end to end: the
github-scriptstep cannot run locally. The generator is confirmed to emit the file in the exact shape that step already consumed on the partial-success path, so the parsing contract is unchanged — only the trigger condition widened.Testing
Added case 6 to
test_generate_skill_metadata.py(4 assertions). The three behavioral ones fail against the unmodified generator and pass with the fix; the fourth guards against regressing the summary line. This closes a real gap: case 3 asserted only that the warning is collected, and case 5 asserted only the exit code — nothing asserted the reason is emitted, which is how this survived.Verified against current
main(430 skills):test_generate_skill_metadata.py— 19/19 passgenerate-skill-metadata.py --check --no-ai—OK: generated outputs are byte-stableFollow-up, not included here
This makes the next failure diagnosable; it does not prevent failures. The actual fix is to retry when the model returns
UNRESOLVEDor an empty field, the way the client already retries 403/429/5xx and malformed JSON. Measured against the real client with a stubbed HTTP layer:UNRESOLVEDThe field validation sits after the retry loop's
break, so it never retries. Note the scope is wider than it first appears: there are four non-retried terminal checks after the loop (non-dict, unexpected keys, missing/empty/non-string, andUNRESOLVED), and until this PR lands we cannot tell which one actually hit the Run:ai skills. Deferred deliberately so the retry change can be driven by a real logged reason rather than a guess.