build(skills): single source of truth + generation + pre-PR rescue guard + determinism propagation - #58
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes repo-root skills/<name>/SKILL.md the single authored source for skills and generates the plugin-loaded copies (devflow-plugin/{skills,commands}), eliminating drift (notably /devflow:finish-feature) and propagating the determinism-lib changes uniformly.
Changes:
- Add
scripts/build-skills.shto generatedevflow-plugin/{skills,commands}from repo-rootskills/, and wireskills-sync/skills-checkintomake. - Add
scripts/skills-guard.shand amake skills-guardtarget to rescue edits mistakenly made in generated plugin trees by folding them back into the source skill. - Add/propagate determinism-lib guidance + Axis-B promptfoo assertion configs for
resolve-repoandfinish-feature, and regenerate the affected plugin copies/flows.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/resolve-repo/SKILL.md | Adds determinism-lib offload guidance and tightens deterministic output/ordering constraints. |
| skills/resolve-repo/determinism.promptfooconfig.yaml | Adds promptfoo assertions to pin resolve-repo output shape. |
| skills/finish-feature/SKILL.md | Adds determinism-lib offload guidance and makes several gates/output formats deterministic/pinned. |
| skills/finish-feature/determinism.promptfooconfig.yaml | Adds promptfoo assertions to pin finish-feature output shape. |
| skills/create-pr/SKILL.md | Adds a pre-PR skills-guard step when operating inside the devflow repo. |
| scripts/skills-guard.sh | New guard to detect and fold misplaced edits from generated plugin trees back into skills/. |
| scripts/build-skills.sh | New generator to render plugin skills/ + commands/ from repo-root skills/. |
| Makefile | Adds skills-sync, skills-check, and skills-guard; wires skills-check into make test. |
| devflow-plugin/skills/resolve-repo/SKILL.md | Regenerated plugin copy of resolve-repo skill with determinism updates. |
| devflow-plugin/skills/finish-feature/SKILL.md | Regenerated plugin copy of finish-feature skill with determinism + gate updates. |
| devflow-plugin/skills/create-pr/SKILL.md | Regenerated plugin copy of create-pr skill with guard step. |
| devflow-plugin/flows/diagrams/commands/render-diagram.md | Regenerated flow command content with improved diagram rendering/display guidance. |
| devflow-plugin/commands/verify-first.md | Updates command content (description/body) to include requirement-coverage verification guidance. |
| devflow-plugin/commands/resolve-repo.md | Regenerated command form of resolve-repo from source skill. |
| devflow-plugin/commands/render-diagram.md | Regenerated command form of render-diagram with updated guidance. |
| devflow-plugin/commands/finish-feature.md | Regenerated command form of finish-feature to eliminate drift vs source skill. |
| devflow-plugin/commands/create-pr.md | Regenerated command form of create-pr including the guard step. |
| CLAUDE.md | Documents the new single-source-of-truth + generation + guard model for skills. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| changed="$( | ||
| { git diff --name-only "$base" 2>/dev/null | ||
| git ls-files --others --exclude-standard 2>/dev/null; } | sort -u | ||
| )" | ||
| _changed_under() { printf '%s\n' "$changed" | grep -q "^$1" 2>/dev/null; } |
There was a problem hiding this comment.
good catches, both fixed in 6e067fb. added git diff --name-only --cached $base to the union so a staged-only edit (index differs, working tree clean) is still caught, and swapped the grep "^$1" prefix test for a case "$p" in "$pfx"*) glob so the . in paths like commands/<name>.md is matched literally instead of as a regex. verified the staged-only case now detects.
| - type: regex | ||
| value: '\*\*VCS Platform:\*\*\s+(github|gitlab|bitbucket|azure|unknown)\b' | ||
| - type: regex | ||
| value: '\*\*Cloned:\*\*\s+(yes|no)' |
There was a problem hiding this comment.
i think this one's a false positive. the regex is an unanchored substring match, so \*\*Cloned:\*\*\s+(yes|no) matches the yes/no stem and ignores the parenthetical, **Cloned:** yes (just now) and **Cloned:** no (already existed) both match (tested both). it's intentionally asserting the stem, not the whole line, so the pinned contract isn't violated. leaving as is.
There was a problem hiding this comment.
i think this one's a false positive. the regex is an unanchored substring match, so \*\*Cloned:\*\*\s+(yes|no) matches the yes/no stem and ignores the parenthetical. both pinned forms match: **Cloned:** yes (just now) and **Cloned:** no (already existed) (tested both). it's intentionally asserting the stem, not the whole line, so the pinned contract isn't violated. leaving as is.
Address Copilot review on PR #58: - Union in 'git diff --name-only --cached $base' so a staged-only edit (index differs from base but the working tree is clean) is still detected; previously only base->working-tree + untracked were collected. - Replace the regex prefix test 'grep "^$1"' with a case-glob literal match, so a '.' in a path (e.g. commands/<name>.md) is matched literally instead of as a regex metacharacter that could misclassify a path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d987d16 to
6e067fb
Compare
…rce of truth) Establish repo-root skills/<name>/SKILL.md as the single authored source. The plugin copies (devflow-plugin/skills/ + devflow-plugin/commands/) and the flows are now GENERATED, so they can no longer drift. Why: the three copies were all hand-maintained with no enforced sync. #49 updated skills/finish-feature (adding the source-grounded review gate + deferral-closure gate) but devflow-plugin/commands/finish-feature.md never got it, so the LOADED slash command /devflow:finish-feature ran a stale 128-line version while the Skill ran the current 166-line one. 25 of 28 command bodies were already identical to their SKILL.md modulo frontmatter; only finish-feature, render-diagram, and verify-first had drifted. - scripts/build-skills.sh: render devflow-plugin/skills/ (byte copy minus the dev-only determinism.promptfooconfig.yaml) + devflow-plugin/commands/*.md (frontmatter rewritten to command form: [VERSION] + description, body verbatim; version from plugin.json). Prunes copies whose source skill was removed. Output dir overridable via DEVFLOW_PLUGIN_OUT for the guard's temp compare. - scripts/skills-guard.sh: pre-PR RESCUE guard. Renders source to a throwaway tree, finds skills whose real copy differs, and uses the git delta since the base ref to classify: source-changed (stale, run skills-sync) vs generated-copy hand-edited (MISPLACED, fold back into the source) vs both (ambiguous, ask). Folds a misplaced command edit back by reversing the frontmatter transform (name preserved from source) and a skill-dir edit by byte copy, then regenerates. - Makefile: skills-sync (regenerate skills + flows), skills-check (fail on drift, wired into 'make test' alongside flows-check), skills-guard. - Regenerate the 3 drifted commands + the diagrams flow's render-diagram copy. - CLAUDE.md: replace the stale 'devflow-plugin/commands is canonical' note with the source-of-truth + generation + guard model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a PR Add a devflow-repo-only pre-flight (Step 0) to /devflow:create-pr: when the repo root has scripts/skills-guard.sh, run 'make skills-guard' before creating the PR so any skill edited in a generated tree gets folded back into skills/<name>/SKILL.md instead of being silently overwritten by the next skills-sync. No-op in every other repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erminism lib Apply the determinism audits (eval/audits/) to the two skills, sourcing the shared determinism-fix lib (~/.claude/lib/determinism) with the abstain->AI contract (exit 0 confident / 10 abstain->AI / 1 error->AI), per the design. resolve-repo: D1 detect_vcs_platform + D2 extract_repo_group + D3 extract_ticket_id (deterministic-first, AI only on abstain); D5 fixed stop-word list; D6/D7 pin the Repo Match Results block + ordering; D9 pin the Resolved Repository block. D4 SCORING kept model-driven per the risk gate (ranking is judgment; a fixed weight table is confidently worse) - only its output labels are pinned. finish-feature: D1 extract_ticket_id; D2 is_protected_branch hard guard; D3 deterministic design-doc grep (abstain->AI); D5 pinned TODO/FIXME diff grep; D8 count_commits; D4 review- gate table; D6 commit-subject regex; D7 PR-body headings. Commit/PR TEXT stays model- authored - only FORMAT pinned. Adds determinism.promptfooconfig.yaml to each skill dir (axis-B assertions). NOTE: applied to repo-root skills/ (the determinism workstream's convention, matching the audits + merged #53). The loaded plugin copies in devflow-plugin/{commands,skills}/ have DRIFTED from repo-root skills/ (pre-existing) - propagation pending a decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…opies
Regenerate devflow-plugin/{skills,commands} for finish-feature + resolve-repo so
the LOADED plugin skills actually source the determinism lib and carry the
abstain->AI contract from the prior commit. This closes the 'propagation pending'
note there: the conversion landed at the repo-root source; regeneration is what
carries it uniformly into the copies the plugin and flows use. The dev-only
determinism.promptfooconfig.yaml stays at the repo root, excluded from the plugin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address Copilot review on PR #58: - Union in 'git diff --name-only --cached $base' so a staged-only edit (index differs from base but the working tree is clean) is still detected; previously only base->working-tree + untracked were collected. - Replace the regex prefix test 'grep "^$1"' with a case-glob literal match, so a '.' in a path (e.g. commands/<name>.md) is matched literally instead of as a regex metacharacter that could misclassify a path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the review's critical/important findings on the generation machinery: build-skills.sh: - Own the plugin.json "skills" array too. Previously a newly added source skill generated its command + plugin dir but was never listed in plugin.json, so the Skill would not load. Now regenerated from the source dirs in registry.json order (extras appended), preserving all other plugin.json fields. skills-check covers it. - Fail loud if a source SKILL.md frontmatter carries a key beyond name/description: the command transform only preserves the description, so an unsupported key (e.g. allowed-tools) would be silently dropped. Force a conscious generator update instead. skills-guard.sh: - CRITICAL: refuse to fold a command edit whose description OR body parses empty (dropped closing '---' fence, missing description key). Previously it overwrote the source SKILL.md with a frontmatter-only stub, the exact data loss the guard exists to prevent. Now it marks the skill ambiguous and leaves the source intact. - When origin/main is unreachable, base off the branch root commit instead of HEAD so a hand-edit already committed on the branch is still in the delta (the HEAD fallback made committed edits look like pre-existing drift and pass --check). - Collapse the redundant dual exit-0 branch. Makefile: skills-check now also diffs devflow-plugin/.claude-plugin/plugin.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…view) - lib/skills.sh: 'devflow skills convert' now delegates to scripts/build-skills.sh + build-flows.sh instead of being a second, divergent generator. The old path stripped the [VERSION] badge, re-injected the dev-only determinism.promptfooconfig.yaml into the plugin, and clobbered plugin.json version to 0.1.0 — following the documented command silently corrupted the plugin and reintroduced the drift this PR removes. Deprecated flags (--name/--marketplace/--prune) are now warned no-ops. - .github/workflows/ci.yml (new): run 'make test' (skills-check + flows-check + smoke) and 'make test-unit' on PRs + main, so a release can never ship copies that drifted from source at merge time. check_version_consistency only validates version numbers, not body text, so this drift gate is what actually blocks body drift. - release.yml: run 'make skills-sync' after bump-version (defense in depth). - README.md: fix stale skills layout (28 flat dirs, generated plugin copies, source of truth) and count (was '15 categorized'). - CLAUDE.md: correct the release-gotcha text (commands DO carry the [X.Y.Z] badge now; the '|| true' guard is the safety net, not a workaround for a missing badge). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6e067fb to
7661542
Compare
TL;DR
skills/<name>/SKILL.md. The plugin copies + flows are now GENERATED, so they cannot drift./devflow:finish-feature(slash command) was running a stale 128-line command while the Skill ran the current 166-line one. Behavior depended on invocation path.make skills-guardfolds a skill edited in the wrong (generated) tree back into the source, instead of silently overwriting it.The problem
devflow skills lived in three hand-maintained, drifted copies with no enforced sync:
skills/<name>/SKILL.md(repo root) — where edits actually land (impr(finish-feature): source-grounded review gate + deferral-closure gate #49, feat(eval): skill-determinism audits + assertion configs + determinism guide #53, the determinism audits, and the lib conversion all went here). Also home toregistry.jsonand theeval/configs.devflow-plugin/skills/<name>/SKILL.md— loaded by the plugin as auto-invokable Skills.devflow-plugin/commands/<name>.md— what the LOADED/devflow:*slash command runs.devflow-plugin/flows/**— a generated 4th copy (make flows).Nothing synced repo-root
skills/todevflow-plugin/. So when #49 added the source-grounded review gate + deferral-closure gate toskills/finish-feature,devflow-plugin/commands/finish-feature.mdnever got it. Result: the slash command ran a stale version missing both gates.Measured blast radius: 25 of 28 command bodies were already identical to their SKILL.md modulo frontmatter. Only
finish-feature,render-diagram,verify-firsthad drifted (bugs, not intent).The decision (source of truth)
Repo-root
skills/<name>/SKILL.mdis the single authored source. The user's stated criteria ("what the plugin loads", "what flows generate from") both point atdevflow-plugin, but generation satisfies them either way, so they do not discriminate. The real discriminator is dev-vs-distribution separation plus where the surrounding infra already lives (registry.json,eval/audits,determinism.promptfooconfig.yaml), which favors keeping the source at the repo root and keeping those dev files OUT of the shipped plugin.Nothing was deleted. The two plugin trees stay present (the plugin loads them) but are now generated.
Changes
scripts/build-skills.shdevflow-plugin/{skills,commands}from repo-rootskills/. Command frontmatter becomes[VERSION]+ the SKILL.md description (version fromplugin.json); body verbatim. Excludes the dev-onlydeterminism.promptfooconfig.yaml. Prunes copies whose source was removed. Output dir overridable viaDEVFLOW_PLUGIN_OUT(used by the guard for an exact temp compare).scripts/skills-guard.sh--checkmode for CI/hooks.Makefileskills-sync(regenerate skills + flows),skills-check(fail on drift, wired intomake testnext toflows-check),skills-guard.skills/create-pr/SKILL.mdscripts/skills-guard.shexists), runmake skills-guardbefore opening a PR. No-op in any other repo.CLAUDE.mddevflow-plugin/commands/is canonical" note with the source-of-truth + generation + guard model.feat/skill-lib-conversion(resolve-repo + finish-feature source the determinism lib with the abstain->AI contract; scoring stays model-driven, only its output is pinned), then regenerated so the loaded plugin copies actually carry it.Commits
build(skills): generate plugin copies from repo-root skills/ (one source of truth)— generator, guard, Makefile targets, drift fix, CLAUDE.md.feat(create-pr): run the skills source-of-truth guard before opening a PR.feat(skills): convert resolve-repo + finish-feature to source the determinism lib(cherry-picked).build(skills): propagate determinism conversion to generated plugin copies.Verification
make test(smoke +skills-check+flows-check): PASS.make test-unit(bats, aftergit submodule update --init tests/lib/*): 75/75 PASS.make skills-guard: OK, plugin copies in sync with source.--check, folds it back into the source, round-trips clean.determinism.promptfooconfig.yamlstays at the repo root and is excluded from the shipped plugin.Reviewer note
The
test-unitbats submodules (tests/lib/bats-support,tests/lib/bats-assert) were uninitialized in the worktree (pre-existing, unrelated).git submodule update --init tests/lib/bats-support tests/lib/bats-assertbefore running.