Skip to content

build(skills): single source of truth + generation + pre-PR rescue guard + determinism propagation - #58

Merged
AndreJorgeLopes merged 7 commits into
mainfrom
vigorous-wilbur-d20a8d
Jul 3, 2026
Merged

build(skills): single source of truth + generation + pre-PR rescue guard + determinism propagation#58
AndreJorgeLopes merged 7 commits into
mainfrom
vigorous-wilbur-d20a8d

Conversation

@AndreJorgeLopes

Copy link
Copy Markdown
Owner

TL;DR

  1. 🎯 One source of truth for skills: repo-root skills/<name>/SKILL.md. The plugin copies + flows are now GENERATED, so they cannot drift.
  2. 🐛 Fixes the real 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.
  3. 🛟 Pre-PR rescue guard: make skills-guard folds a skill edited in the wrong (generated) tree back into the source, instead of silently overwriting it.
  4. 🔁 Propagates the determinism lib conversion (resolve-repo + finish-feature) uniformly into the loaded plugin copies.

The problem

devflow skills lived in three hand-maintained, drifted copies with no enforced sync:

Nothing synced repo-root skills/ to devflow-plugin/. So when #49 added the source-grounded review gate + deferral-closure gate to skills/finish-feature, devflow-plugin/commands/finish-feature.md never 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-first had drifted (bugs, not intent).

The decision (source of truth)

Repo-root skills/<name>/SKILL.md is the single authored source. The user's stated criteria ("what the plugin loads", "what flows generate from") both point at devflow-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.

skills/<name>/SKILL.md          SOURCE (authored) + registry.json + eval configs (dev only)
  |  scripts/build-skills.sh  (make skills-sync)
  |-> devflow-plugin/skills/<name>/     byte copy, minus dev-only determinism.promptfooconfig.yaml
  |-> devflow-plugin/commands/<name>.md frontmatter -> command form ([VERSION] + description), body verbatim
  \-> make flows -> devflow-plugin/flows/**

Nothing was deleted. The two plugin trees stay present (the plugin loads them) but are now generated.

Changes

Area What changed and why
scripts/build-skills.sh New generator. Renders devflow-plugin/{skills,commands} from repo-root skills/. Command frontmatter becomes [VERSION] + the SKILL.md description (version from plugin.json); body verbatim. Excludes the dev-only determinism.promptfooconfig.yaml. Prunes copies whose source was removed. Output dir overridable via DEVFLOW_PLUGIN_OUT (used by the guard for an exact 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 source) vs both (ambiguous, ask). Folding reverses the frontmatter transform for a command edit (name preserved from source) or byte-copies a skill-dir edit, then regenerates. --check mode for CI/hooks.
Makefile skills-sync (regenerate skills + flows), skills-check (fail on drift, wired into make test next to flows-check), skills-guard.
skills/create-pr/SKILL.md New Step 0: when inside the devflow repo (scripts/skills-guard.sh exists), run make skills-guard before opening a PR. No-op in any other repo.
CLAUDE.md Replaced the stale "devflow-plugin/commands/ is canonical" note with the source-of-truth + generation + guard model.
generated copies Regenerated the 3 drifted commands + the diagrams flow, then propagated the determinism conversion into the 4 plugin copies.
determinism conversion Cherry-picked 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

  1. build(skills): generate plugin copies from repo-root skills/ (one source of truth) — generator, guard, Makefile targets, drift fix, CLAUDE.md.
  2. feat(create-pr): run the skills source-of-truth guard before opening a PR.
  3. feat(skills): convert resolve-repo + finish-feature to source the determinism lib (cherry-picked).
  4. build(skills): propagate determinism conversion to generated plugin copies.

Verification

  • make test (smoke + skills-check + flows-check): PASS.
  • make test-unit (bats, after git submodule update --init tests/lib/*): 75/75 PASS.
  • make skills-guard: OK, plugin copies in sync with source.
  • Guard exercised end to end: OK on a consistent tree (no bootstrap misfire), detects a hand-edit on a generated command via --check, folds it back into the source, round-trips clean.
  • Confirmed dev-only determinism.promptfooconfig.yaml stays at the repo root and is excluded from the shipped plugin.

Reviewer note

The test-unit bats 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-assert before running.

Copilot AI review requested due to automatic review settings July 1, 2026 02:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.sh to generate devflow-plugin/{skills,commands} from repo-root skills/, and wire skills-sync / skills-check into make.
  • Add scripts/skills-guard.sh and a make skills-guard target 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-repo and finish-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.

Comment thread scripts/skills-guard.sh Outdated
Comment on lines +39 to +43
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; }

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +21 to +24
- type: regex
value: '\*\*VCS Platform:\*\*\s+(github|gitlab|bitbucket|azure|unknown)\b'
- type: regex
value: '\*\*Cloned:\*\*\s+(yes|no)'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

AndreJorgeLopes added a commit that referenced this pull request Jul 3, 2026
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>
@AndreJorgeLopes
AndreJorgeLopes force-pushed the vigorous-wilbur-d20a8d branch from d987d16 to 6e067fb Compare July 3, 2026 03:12
AndreJorgeLopes and others added 7 commits July 3, 2026 05:45
…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>
@AndreJorgeLopes
AndreJorgeLopes force-pushed the vigorous-wilbur-d20a8d branch from 6e067fb to 7661542 Compare July 3, 2026 05:07
@AndreJorgeLopes
AndreJorgeLopes merged commit 5930697 into main Jul 3, 2026
1 check passed
@AndreJorgeLopes
AndreJorgeLopes deleted the vigorous-wilbur-d20a8d branch July 3, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants