Apply mechanical, auto-fixable schema drift to entity instances under docs/planning/<plural>/. Spins up a worktree, runs the audit, applies every fix the audit flagged auto_fixable — stamping the missing schema_version into frontmatter AND running the deterministic body transforms (the v_n->v_n+1 chain that reshapes body sections, e.g. a task's bulleted Files-to-touch/Today blocks into typed v3 tables) — and opens a PR. Body transforms are deterministic TS that reshape what's unambiguous and flag the ambiguous tail (definition_gap + status downshift, mid-flight preserved); no LLM in the migrate path. Skips closed entities by default; pass --include-closed to forward that flag to the audit and treat closed entities like active ones.
Instal·la
npx skillscat add osdlc/osdlc/claude-plugin-packs-sdlc-skills-entities-migrate Instal·la des del registre de SkillsCat.
/sdlc:entities-migrate
Usage:
/sdlc:entities-migrate— apply everyauto_fixable=truedrift entry across all entity types.
Closed entities are skipped (the audit filters them out before migrate ever sees them)./sdlc:entities-migrate --include-closed— forward--include-closedto the audit so closed
entities are drift-checked and migrated like active ones./sdlc:entities-migrate --type task— restrict to one entity type. Repeatable./sdlc:entities-migrate --dry-run— report what would change without writing.
Project context (don't re-derive every run):
sdlc entities auditis the source of truth for what counts as drift and which drift entries are
mechanically fixable (auto_fixable: true). Dispatch only on what the audit tagged; do not invent
new fix logic.- Closed entities are skipped at the audit layer, not at migrate. The audit early-returns for
any entity whosestateindicates closed (closed/<reason>or legacy bare-closed values likeclosed,done,superseded), so migrate never sees those entries. Pass--include-closedto
forward that flag through to audit so closed entities are drift-checked and fixed alongside active
ones — reach for it only when every record must track the current schema, since closed = frozen:
a closed entity was authored against an earlier schema and a mechanical reinterpretation under
newer semantics can corrupt a finished artefact. - The
0sentinel (schema_version: 0) means "intentionally pre-SDLC-managed"; audit treats it as
not-drift. It's a manual stamp users apply to specific active records they want excluded from
versioning. - Two
auto_fixable=truepaths exist today:- Missing
schema_version— stamp the schema's current version into the file's frontmatter,
preserving canonical key order. - Older-than-current
schema_versionwith a transform on disk — import the matchingentities/<type>/migrations/v<file>-to-v<current>.tsmodule, apply itsmigratecallable,
write the migrated content back. Transforms come in two signature shapes; the runner introspects
each and routes accordingly (see "Transform signature dispatch" below). The transform is
responsible for any field-shape changes (status remaps, dropped legacy keys, completion-note
synthesis), any body rewrites, and for stamping the newschema_versionon its result.
- Missing
- One PR per invocation. One commit covering all entities (not split by type).
Transform signature dispatch
A transform module at entities/<type>/migrations/v<src>-to-v<dst>.ts
may declare its migrate callable in one of two shapes:
- Frontmatter-only (historical):
migrate(fm: dict) -> new_fm: dict.
The runner passes the parsed frontmatter and writes the returned dict
back. Body content is preserved byte-for-byte. Use this when the
migration only touches frontmatter fields (status remaps, field
renames, dropped keys). Example:entities/task/migrations/v1-to-v2.ts. - Body-aware:
migrate(fm: dict, body: str) -> (new_fm: dict, new_body: str).
The runner passes both frontmatter and the body string, and writes
both back. Use this when the migration must rewrite prose / tables /
lists inside the body. Example:entities/task/migrations/v2-to-v3.ts(converts bulleted
Files-to-touch and Today blocks into typed markdown tables).
The entities migrate op inspects the callable's signature and dispatches:
≥2 positional parameters → body-aware; otherwise frontmatter-only.
There is no per-module flag; the contract is the signature.
Steps
1. Pre-flight
Confirm the main repo's working tree is clean. From the project root:
git status --porcelainIf anything is staged or modified, stop and report to the user. Do NOT
sweep their work into this skill's commits. (Untracked files unrelated
to entity instances are fine — the migrate script itself usesgit status --porcelain and will refuse to act on a dirty tree without--allow-dirty.)
2. Dry-run the audit first
Before creating a worktree, surface what's about to change:
${CLAUDE_PLUGIN_ROOT}/cli/sdlc entities audit --jsonParse the JSON. Count entries with at least onedrift[*].auto_fixable=true. By default the audit's JSON output
already excludes closed entities from drift detection; if the user
passed --include-closed, forward it to the audit invocation
(sdlc entities audit --json --include-closed) so closed entries
appear in the report.
- If zero auto-fixable items qualify: report
nothing to migrate.
to the user and exit. Do not create a worktree or branch. - Otherwise proceed to Step 3.
3. Create the worktree
git worktree add .sdlc/worktrees/entities-migrate -b chore/entities-migrate mainIf a worktree at that path or a branch by that name already exists, stop
and ask the user — there may be an unfinished prior run. Do not delete
existing state without confirmation.
From here on, all subsequent work happens in that worktree path. Use
absolute paths in tool calls; do not cd the parent session into the
worktree.
4. Run the migration
In the worktree:
${CLAUDE_PLUGIN_ROOT}/cli/sdlc entities migrate \
--project-root .sdlc/worktrees/entities-migrateForward any flags the user passed (--type, --include-closed,--dry-run). The op:
- Refuses if the worktree is dirty (it won't be — you just created it).
- Pulls the audit report (forwarding
--include-closedif set). - For each
auto_fixable=truedrift entry, applies the corresponding
fix — either stampingschema_versionwith the schema's current
version or running the matching v-to-v transform. - Prints a summary: what was fixed, what's pending manual review, and
any failures.
Capture the summary — Step 6 reuses it for the PR body.
Interpret the exit code:
0— clean run. Either fixes applied successfully, or there was
nothing mechanical to do.1— the run failed against the state it found: at least one fix
attempt failed, a directory is missing, or the audit JSON is malformed.
Stop here. Surface the failure output to the user; do not commit a
half-applied state.18— the working tree is dirty. Stop, report, ask the user to
commit/stash before retrying. Do not auto-stash.
If --dry-run was passed, the op wrote nothing. Surface its output
to the user and exit — no commit, no PR.
5. Commit the changes
In the worktree:
git status --porcelainExpect only files under docs/planning/<plural>/ to be modified. If
anything else changed, stop — something is off.
Stage every modified instance file (use the script's summary as the
authoritative list):
git add docs/planning/...
git commit -m "chore(entities): apply auto-fixable schema drift"One commit. Do not split by entity type. The commit message body should
list the files modified and the fix applied to each:
chore(entities): apply auto-fixable schema drift
Stamped current schema_version into the frontmatter of:
- docs/planning/tasks/foo.md (task v1)
- docs/planning/milestones/M0042.md (milestone v1)
Files left for manual review (auto_fixable=false drift):
- docs/planning/tasks/bar.md — schema_version 0 < current 1 (needs a real migration)6. Open the PR
git push -u origin chore/entities-migrate
${CLAUDE_PLUGIN_ROOT}/cli/sdlc pr create --base main --head chore/entities-migrate \
--title "chore(entities): apply auto-fixable schema drift" \
--body-file <body-file>PR body shape:
## Summary
Applied mechanical schema-drift fixes flagged `auto_fixable=true` by
/sdlc:entities-audit. That means both stamping the current
`schema_version` into frontmatter where it was missing AND running the
deterministic body transforms (the `v_n`→`v_n+1` chain) on older-version
records — which reshape unambiguous body sections (e.g. a task's bulleted
Files-to-touch/Today blocks into typed v3 tables) and flag the genuinely
ambiguous tail (`definition_gap:` + status downshift, mid-flight
preserved) rather than guessing. No LLM is involved.
## What changed
<list each file fixed and the fix description, mirroring the commit
message body>
## Manual review needed
<list each `auto_fixable=false` drift entry the audit reported — older
schema_version, frontmatter validation errors, missing template H2s.
One bullet per file; group drift entries beneath.>
## How this was generated
Invoked `/sdlc:entities-migrate`. The skill ran
`sdlc entities audit --json`, then `sdlc entities migrate` against this
worktree.Return the PR URL to the user.
7. After the PR merges — close the loop
Same template as /sdlc:task-work Steps 11a–11c:
On main, pull the merged commit:
git -C <main-repo> fetch origin git -C <main-repo> pull --rebase --autostashTear down the worktree:
git -C <main-repo> worktree remove --force .sdlc/worktrees/entities-migrate git -C <main-repo> worktree pruneDelete the merged branch:
git -C <main-repo> branch -d chore/entities-migrate(Use
-Donly if you're certain the PR was squash-merged —-dis
the safe form.)
Confirm the final state to the user: PR URL, branch gone, worktree
gone.
Failure modes
- Dirty working tree —
sdlc entities migrateexits 18. Stop, report,
ask the user to commit/stash before retrying. Do not auto-stash. - Audit script error — surface stderr and exit. Don't try to fix
what the audit can't describe. - Unknown auto_fixable kind — the op will surface a failure
("no dispatch for auto_fixable kind ''"). This means the audit
added a new auto-fixable category that this skill doesn't yet handle;
open a follow-up task to add the dispatch arm rather than guessing. - Closed entities appearing in the work list — only happens when
--include-closedwas passed all the way through. In that mode they're
treated like active entities and migrated to current. Without the flag,
audit filters them out before migrate sees them.
Notes
- Idempotent. Running the skill twice in a row produces no second
PR (the second run finds no auto-fixable drift and reportsnothing to migrate.). - Frontmatter AND body. This skill applies both fixes the op already
performs: the mechanical frontmatter migration (stamping the currentschema_version) AND the deterministic body transforms — thev_n→v_n+1chain reshapes unambiguous body sections (e.g. a task's
bulleted Files-to-touch/Today blocks into typed v3 tables). The body
transforms are deterministic TS script files (migrate(fm, body) -> [fm, body]), not LLM authoring: they reshape what's unambiguous and
FLAG the genuinely ambiguous tail (definition_gap:+ status
downshift, mid-flight preserved — see the next note) rather than
guessing. The remaining open-ended body work — template H2 gaps, prose
the transform deliberately flagged as ambiguous — is left for/sdlc:task-define(interactive/LLM judgment), NOT done here. No LLM
runs in the migrate path. - Mid-flight status is preserved. A body-aware transform may flag an ambiguous tail with
definition_gap:and downshift the task toplanning/needs-definitionso it surfaces in the next/sdlc:task-review— EXCEPT for tasks alreadyin-progressorin-progress/blocked, whose
status is preserved (downshifting a mid-flight task corrupts task-work state; mirrors the
carve-out insdlc task ensure-ready-mutate). Implemented
today byentities/task/migrations/v2-to-v3.ts. - Transform classes (pick one when adding a transform). (1) Auto-fixable: deterministic for
every input. (2) Mechanical-with-flagging: reshape the easy rows, setdefinition_gap:+
downshift the ambiguous tail (the mid-flight carve-out above applies). (3) Non-mechanical: needs
per-instance judgment — does NOT belong here; route to/sdlc:task-define. Never call an LLM
insidemigrate.ts; reaching for one means you have left class 2. When a new transform sits on
the (2)/(3) line, prefer (2) — flagging the ambiguous tail for human triage scales, LLMing the
migration sweep does not. - Expanding mechanical-fix scope. To make a new drift category fixable: flip
auto_fixable=true
in the audit, add the matching dispatch arm in migrate, and add a fixture undertests/fixtures/. - Eval suite.
tests/entities_migrate.test.ts(in this skill's source directory; dev-only, not
shipped in the built plugin) validates the migrate op against fixtures (missing/older/clean
schema_version, closed-with-drift in both default-skip and--include-closedmodes, dirty tree).
End-to-end body-transform behavior (thev2→v3bulleted-Files-to-touch → typed-table reshape,
plus the ambiguous-rowdefinition_gap:flag and mid-flight-preserving downshift) is covered
per-transform by the unit suite of${CLAUDE_PLUGIN_ROOT}/lib/model/entities/task/migrations/v2-to-v3.ts. Run both before changing
the migrate op or a transform. - Committing model-generated messages. See
${CLAUDE_PLUGIN_ROOT}/conventions/commit-messages.md—mktemp+ quoted-heredoc +git commit -Fis the canonical pattern. Avoids zsh-glob hazards on conventional-commit parens
and tempfile collisions under parallel sessions. - Branch naming. This skill's worktree branch uses
chore/entities-migrate-<timestamp>(NOTtask/<task-basename>— this is bulk skill-runtime work, not a single-task implementation). See${CLAUDE_PLUGIN_ROOT}/conventions/branch-naming.md. - Schema bump checklist. See
${CLAUDE_PLUGIN_ROOT}/conventions/schema-bump-checklist.md— the
five canonical questions every schema-bump task spec must answer (missing field, current field,
unknown legacy value, error path, post-migrate stamp behavior) before this skill is expected to
migrate cleanly.