fix(skills): make evolved skill text optimizer-visible and validate the full skill file - #184
Open
brianbaldock wants to merge 1 commit into
Open
brianbaldock wants to merge 1 commit into
brianbaldock wants to merge 1 commit into
Conversation
…he full file Phase 1 could never emit a genuinely evolved skill. Two defects: 1. SkillModule held the skill text in a plain Python attribute and passed it to the signature as an InputField, so no DSPy optimizer could rewrite it. evolve_skill then read that same untouched attribute back as the "evolved" body, making the written file byte-identical to the baseline. 2. The constraint validator received skill["body"], but _check_skill_structure requires YAML frontmatter that load_skill has already stripped out of the body. Every evolved candidate failed structural validation and was rejected before the holdout comparison ran. The skill text now lives in the predictor's signature instructions (the state GEPA and MIPROv2 actually rewrite) and is read back through a property. A new ConstraintValidator.validate_skill takes frontmatter and body separately and reassembles before the structural check, so a bare body can no longer reach the frontmatter validator by mistake. A no-op guard aborts the run when the optimizer returns unchanged text, rather than reporting a delta that would be measuring noise.
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.
What does this PR do?
Phase 1 skill evolution could never emit a genuinely evolved skill. Two independent defects sat on the same path, and either one alone is sufficient to make the pipeline a no-op:
The optimizer could not see the skill text.
SkillModulestored the skill body in a plain Python attribute (self.skill_text) and passed it to the signature as anInputField. DSPy optimizers rewrite signature instructions and demos — not arbitrary instance attributes — so GEPA and MIPROv2 had nothing to mutate.evolve_skill.pythen read that same untouched attribute back as the "evolved" body, so the file written tooutput/<skill>/evolved_skill.mdwas byte-identical to the baseline.Every evolved candidate failed structural validation. The validator was handed
skill["body"], but_check_skill_structurerequires YAML frontmatter — whichload_skillhas already stripped out of the body. The evolved candidate was therefore rejected before the holdout comparison ever ran.Why this approach: rather than patching the two call sites, the skill text is moved into the state the optimizer actually owns (signature instructions, read back through a property), and structural validation is given an API that makes the mistake unrepresentable.
ConstraintValidator.validate_skill()takes frontmatter and body separately and reassembles internally, so a bare body can no longer reach the frontmatter check by mistake. A no-op guard then aborts the run when the optimizer returns unchanged text, so the pipeline cannot report a delta that is measuring noise instead of evolution.That last guard matters beyond these two bugs: with defect 1 present, any reported improvement was measuring the DSPy wrapper, not the artifact being shipped. Failing loudly is better than a plausible number.
Related Issue
Fixes #141
Fixes #11
Also reported as: #34, #74, #93, #110, #169, #171 (validator target) and #38, #87, #119, #172, #175 (optimizer no-op).
Prior art — please read before reviewing this one. Per
CONTRIBUTING.md's "Search First" step, I searched before opening this. Both bugs are long-standing and several contributors got here first. Overlapping open PRs include #137, #140, #153, #161, #168, #174, #177, #178 and #183. I am not claiming precedence and this should not jump the queue — #97 and #140 predate it substantially. If a maintainer prefers any of those, close this one; I am happy to review or rebase onto whichever lands first. I opened it because it adds two things I did not find elsewhere: an API shape that prevents defect 2 from recurring, and the no-op guard that makes defect 1 fail loudly instead of silently producing a number.Type of Change
Changes Made
evolution/skills/skill_module.py—SkillModulenow builds its signature with.with_instructions(skill_text)and exposesskill_textas a read-only property backed bypredictor.predict.signature.instructions. Removed theskill_instructionsInputField; the skill is instructions, not runtime input.evolution/core/constraints.py— addedConstraintValidator.validate_skill(frontmatter, body, baseline_body=None), which applies size/growth/non-empty to the body and the structural check to the reassembled file.validate_allis unchanged for other artifact types.evolution/skills/evolve_skill.py— both baseline and evolved validation now callvalidate_skill; added the unchanged-text guard between extraction and validation.tests/skills/test_evolution_writeback.py— new; pins both defects.tests/skills/test_evolution_e2e.py— new; drives the full extract → guard → validate → reassemble chain with a stub optimizer (no network, no API key).How to Test
Confirm the defects on
main(both are offline checks):On
mainthis prints['predictor.predict']—skill_textis absent, so no optimizer can reach it.Reproduce defect 2 against any real skill:
On
main:Skill missing: YAML frontmatter (---), name field, description field.Run the new tests, which fail on
mainand pass here:Run the full suite:
pytest tests/ -q→ 154 passed.Checklist
Code
hermes-agentguide and its PR templatefix(skills):)pytest tests/ -qand all tests pass (154 passed)Documentation & Housekeeping
docs/N/A, no user-facing interface changedcli-config.yaml.exampleif I added/changed config keys — N/A, no config keys touchedCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A, neither file exists in this repopathlibusage; no file I/O, process management, or path semantics changedNot claimed: I have not run a real GEPA optimization end-to-end against a live API, so I am not asserting that evolution now produces better skills — only that the optimizer's output is no longer discarded and candidates are no longer rejected by a false-positive gate. Whether the current fitness function (bag-of-words overlap,
fitness.py:129-134) can produce a meaningful improvement signal is a separate question, tracked in #12 and #33.Screenshots / Logs
Full suite on this branch:
Sabotage probe confirming the no-op guard discriminates (reverting the fix in-memory and re-running):