Crystallize Update Proposal (Issue #664)
vf skills crystallize <run-id> now detects when extracted patterns match an
existing skill and prints a PATCH PROPOSAL (stdout only) instead of writing
a crystallized-run-* draft.
Flow
- Run
vf skills crystallize <run-id>as before. - If patterns cross the threshold,
proposeCrystallizeUpdate()checks each pattern against the skill catalog. - Match found → print proposal to stdout (diff preview, affected file, eval command). No file written.
- No match → existing draft behavior unchanged (writes
.vibeflow/skills/crystallized-<slug>/SKILL.md).
Matching rules (conservative, deterministic)
A pattern matches a skill when its value equals any of (case-insensitive):
- Skill
name domain.idownsfact key
Never matched:
failurekind patterns (too generic)- Generic terms:
bun build,bun test,echo,ls,cat,rm,cp,mv,mkdir,touch,cd,pwd,git status,git diff,git log,git add,npm install,npm run,node,npx - Generic failure signatures:
command not found...,permission denied...,no such file...,failed to...
Proposal output
PATCH PROPOSAL for skill "<name>"
────────────────────────────────────────────────────────────
Affected files: <path>
Eval: vf skills eval <name>
Proposed diff:
--- a/<path>
+++ b/<path>
@@ -1,3 +1,3 @@
+<!-- #664 crystallized from run <run-id> -->
+## Crystallized patterns
...
────────────────────────────────────────────────────────────
No draft written. Review the proposal above and apply manually.
API
proposeCrystallizeUpdate(repo, patterns, runId, inject?)
function proposeCrystallizeUpdate(
repo: string,
patterns: CrystallizedPattern[],
runId: string,
inject?: { discoverSkills?: (r: string) => Skill[] },
): CrystallizeProposalResult
Returns { hasProposal, proposal?, reason? }.
buildProposal(skill, patterns, runId)
function buildProposal(
skill: Skill,
patterns: CrystallizedPattern[],
runId: string,
): CrystallizePatchProposal
Pure builder — no I/O, no LLM, no side effects.