Skip to content

/sdlc:entities-migrate

Generated from solutions/ontological/skills/entities-migrate/SKILL.md.

Apply mechanical, auto-fixable schema drift to entity instances under docs/planning//. 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.

  • Bash
  • Read
  • Edit
  • Glob
  • Grep
  • Agent

Usage:

  • /sdlc:entities-migrate — apply every auto_fixable=true drift 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-closed to 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 audit is 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 whose status indicates closed (closed/<reason> or legacy bare-closed values like closed, done, superseded), so migrate never sees those entries. Pass --include-closed to 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 0 sentinel (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=true paths exist today:
    • Missing schema_version — stamp the schema’s current version into the file’s frontmatter, preserving canonical key order.
    • Older-than-current schema_version with a transform on disk — import the matching entities/<type>/migrations/v<file>-to-v<current>.ts module, apply its migrate callable, 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 new schema_version on its result.
  • One PR per invocation. One commit covering all entities (not split by type).

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.

Confirm the main repo’s working tree is clean. From the project root:

git status --porcelain

If 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 uses git status --porcelain and will refuse to act on a dirty tree without --allow-dirty.)

Before creating a worktree, surface what’s about to change:

${CLAUDE_PLUGIN_ROOT}cli/sdlc entities audit --json

Parse the JSON. Count entries with at least one drift[*].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.
git worktree add .sdlc/worktrees/entities-migrate -b chore/entities-migrate main

If 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.

In the worktree:

${CLAUDE_PLUGIN_ROOT}cli/sdlc entities migrate \
--project-root .sdlc/worktrees/entities-migrate

Forward 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-closed if set).
  • For each auto_fixable=true drift entry, applies the corresponding fix — either stamping schema_version with 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 — at least one fix attempt failed. Stop here. Surface the failure output to the user; do not commit a half-applied state.
  • 2 — op error (dirty tree, missing dirs, malformed audit JSON). Stop and report.

If --dry-run was passed, the op wrote nothing. Surface its output to the user and exit — no commit, no PR.

In the worktree:

git status --porcelain

Expect 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)
git push -u origin chore/entities-migrate
gh pr create \
--title "chore(entities): apply auto-fixable schema drift" \
--body "<see below>"

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.

Same template as /sdlc:task-work Steps 11a–11c:

  1. On main, pull the merged commit:

    git -C <main-repo> fetch origin
    git -C <main-repo> pull --rebase --autostash
  2. Tear down the worktree:

    git -C <main-repo> worktree remove --force .sdlc/worktrees/entities-migrate
    git -C <main-repo> worktree prune
  3. Delete the merged branch:

    git -C <main-repo> branch -d chore/entities-migrate

    (Use -D only if you’re certain the PR was squash-merged — -d is the safe form.)

Confirm the final state to the user: PR URL, branch gone, worktree gone.

  • Dirty working treesdlc entities migrate exits 2. 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-closed was 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.
  • Idempotent. Running the skill twice in a row produces no second PR (the second run finds no auto-fixable drift and reports nothing to migrate.).
  • Frontmatter AND body. This skill applies both fixes the op already performs: the mechanical frontmatter migration (stamping the current schema_version) AND the deterministic body transforms — the v_nv_n+1 chain 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 to planning/needs-definition so it surfaces in the next /sdlc:task-review — EXCEPT for tasks already in-progress or in-progress/blocked, whose status is preserved (downshifting a mid-flight task corrupts task-work state; mirrors the carve-out in ${CLAUDE_PLUGIN_ROOT}skills/task-ensure-ready/ensure_ready_mutate.ts). Implemented today by entities/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, set definition_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 inside migrate.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 under tests/fixtures/.
  • Eval suite. ${CLAUDE_PLUGIN_ROOT}skills/entities-migrate/tests/entities_migrate.test.ts validates the migrate op against fixtures (missing/older/clean schema_version, closed-with-drift in both default-skip and --include-closed modes, dirty tree). End-to-end body-transform behavior (the v2v3 bulleted-Files-to-touch → typed-table reshape, plus the ambiguous-row definition_gap: flag and mid-flight-preserving downshift) is covered per-transform by ${CLAUDE_PLUGIN_ROOT}lib/model/entities/task/migrations/tests/v2_to_v3.test.ts. Run both before changing the migrate op or a transform.
  • Committing model-generated messages. See ${CLAUDE_PLUGIN_ROOT}conventions/commit-messages.mdmktemp + quoted-heredoc + git commit -F is 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> (NOT task/<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.