The skill-evolution pipeline cannot, by construction, modify the skill text it claims to evolve. The "evolved" SKILL.md written to output/ is byte-identical to the input.
Root cause. In skill_module.py, the skill body is stored as a plain instance attribute and passed as a DSPy InputField value:
self.skill_text = skill_text # plain attribute
...
skill_instructions=self.skill_text, # passed as an input VALUE
DSPy optimizers (GEPA, MIPROv2, BootstrapFewShot) only mutate a Predict submodule's signature.instructions and demos. A plain attribute is never registered as a parameter, so compile() never touches it. evolve_skill.py:183 then reads the untouched attribute back:
evolved_body = optimized_module.skill_text # == baseline, always
Reproduction (no API key needed, dspy 3.x):
import dspy
class TaskWithSkill(dspy.Signature):
skill_instructions: str = dspy.InputField()
task_input: str = dspy.InputField()
output: str = dspy.OutputField()
class SkillModule(dspy.Module):
def __init__(self, skill_text):
super().__init__(); self.skill_text = skill_text
self.predictor = dspy.ChainOfThought(TaskWithSkill)
m = SkillModule("ORIGINAL BODY")
print([n for n,_ in m.named_parameters()]) # ['predictor.predict']
print("skill_text" in dict(m.named_parameters())) # False
print(m.deepcopy().skill_text == m.skill_text) # True -> never changes
Consequence for the Phase-1 report. The reported 0.408 → 0.569 (+39.5%) cannot have come from a changed skill. With BootstrapFewShot (per the report) the only thing optimized is bootstrapped demos attached to the predictor — and reassemble_skill() discards them, writing only frontmatter + unchanged body. The headline gain is a transient demo-augmented surrogate that is never saved or deployed.
Proposed fix (verified to round-trip). Make the skill body the optimizable signature.instructions, and read it back from there:
class SkillModule(dspy.Module):
def __init__(self, skill_body: str):
super().__init__()
sig = dspy.Signature("task_input -> output", instructions=skill_body)
self.predictor = dspy.ChainOfThought(sig)
def forward(self, task_input: str) -> dspy.Prediction:
result = self.predictor(task_input=task_input)
return dspy.Prediction(output=result.output)
@property
def skill_body(self) -> str:
return self.predictor.predict.signature.instructions
# evolve_skill.py
evolved_body = optimized_module.skill_body # was optimized_module.skill_text
Verified: after an instruction optimizer rewrites predictor.predict.signature.instructions, skill_body returns the new text — the optimized artifact is now the saved artifact. This fix is a prerequisite for the two companion issues (the bag-of-words fitness metric / dead LLMJudge, and the silently-swallowed dspy.GEPA(..., max_steps=...) call) to have any effect; I'll cross-link them here once filed.
The skill-evolution pipeline cannot, by construction, modify the skill text it claims to evolve. The "evolved" SKILL.md written to
output/is byte-identical to the input.Root cause. In
skill_module.py, the skill body is stored as a plain instance attribute and passed as a DSPy InputField value:DSPy optimizers (GEPA, MIPROv2, BootstrapFewShot) only mutate a
Predictsubmodule'ssignature.instructionsanddemos. A plain attribute is never registered as a parameter, socompile()never touches it.evolve_skill.py:183then reads the untouched attribute back:Reproduction (no API key needed, dspy 3.x):
Consequence for the Phase-1 report. The reported
0.408 → 0.569 (+39.5%)cannot have come from a changed skill. WithBootstrapFewShot(per the report) the only thing optimized is bootstrapped demos attached to the predictor — andreassemble_skill()discards them, writing onlyfrontmatter + unchanged body. The headline gain is a transient demo-augmented surrogate that is never saved or deployed.Proposed fix (verified to round-trip). Make the skill body the optimizable
signature.instructions, and read it back from there:Verified: after an instruction optimizer rewrites
predictor.predict.signature.instructions,skill_bodyreturns the new text — the optimized artifact is now the saved artifact. This fix is a prerequisite for the two companion issues (the bag-of-words fitness metric / deadLLMJudge, and the silently-swalloweddspy.GEPA(..., max_steps=...)call) to have any effect; I'll cross-link them here once filed.