Migrate
Bring a project's stack-it artifacts from the layout an older version wrote to the layout the current one expects. This skill restructures: it moves generated text out of files it no longer belongs in, creates the pointer files agents read, and stamps the lockfile with the current format. It never generates or refreshes stack content. The most it ever writes is the old CLAUDE.md text moved into docs/stack-notes.md, inside the markers or below them when that file already exists, plus a one-line stub in either docs file where nothing moved. document-stack owns the content; this skill owns the shape.
It runs two ways: on its own when a user asks, and from document-stack when that skill detects a project behind the current format.
Step 1: Detect the format
Ownership comes first, before any signal is read. docs/stack.md and docs/stack-notes.md are stack-it's only if each is a regular file whose first line starts with > Generated by stack-it document-stack, format N. Match on that prefix alone: the sentence after it changed between versions, so a whole-line comparison would read every older file as somebody else's. A symlink at either path, or a first line that doesn't start that way, is not ours: stop there, show the user what is in it, and ask before touching anything. The order matters. A lockfile with no format key sitting beside a hand-written docs/stack.md reads as a format 1 project, and acting on that signal would start a migration across the very file the ownership rule exists to protect.
Once both docs files are ours or absent, read every signal, then take the highest. Don't stop at the first one you find.
docs/stack.mdanddocs/stack-notes.md: theNon the header line of each owned file..claude/stack-it/stack.yaml: its top-levelformatkey. A lockfile with no key reads as 1.CLAUDE.md: a<!-- stack-it:stack start -->marker reads as 1. ReadCLAUDE.mdfrom disk, since Claude Code strips HTML comments from loaded context and the marker won't be visible in aCLAUDE.mdyou already have in context. The start marker alone is enough to read the signal; removing the block still needs the full pair (see Marker integrity). A marker inside a fenced code block is not a signal, the same as in Marker integrity.
No signal at all means nothing has been written yet, so there's nothing to migrate.
The current format is 2. If any signal reads higher than 2, stop and write nothing: a newer stack-it wrote this project, so tell the user to upgrade the plugin rather than downgrade their files. A newer decide-stack can stamp format: 3 on the lockfile while docs/stack.md still says 2, which is why the highest signal wins and the first hit doesn't. Otherwise the project's format is the highest signal you read.
Marker integrity. Before removing or replacing anything between markers, confirm the file holds exactly one <!-- stack-it:stack start --> followed by exactly one <!-- stack-it:stack end -->. Markers inside a fenced code block don't count; those are an example of a block, not a block. If a marker of either kind is there without its partner, the two are out of order, or there's more than one pair, remove and replace nothing: show the user the offending lines and stop. In an owned docs/stack.md or docs/stack-notes.md, no markers at all fails the same way: format 2 always writes a pair, so a file carrying our header and no markers is malformed for this layout rather than empty of a block. Show it to the user and stop. In CLAUDE.md and the README, no markers means there is simply no block yet, which is normal: skip the removal step and continue. This governs every file stack-it replaces content in: docs/stack.md, docs/stack-notes.md, CLAUDE.md, and the README.
This detection is shared: document-stack Step 1 and migrate Step 1 must carry the same rules. Change them in both places or in neither.
Here only CLAUDE.md is ever cut. The two docs files are created or appended to, and the README is never touched.
With no signal at all there's nothing for this skill to do: say so and stop, because a fresh project just needs document-stack. If the project is behind, run each migration below in order, from its format up to the current one, then report.
Verify the layout at format 2
A format number says which layout the project uses, not that every piece of it landed. When the format is 2, check all of it. Run Marker integrity on every owned docs file that is present FIRST, before deciding any piece is missing: a marker of either kind without its partner, a reversed pair, a second pair, or no markers at all stops the run with the file shown. Doing that check first is what stops a malformed docs/stack.md from getting a freshly stubbed sibling written beside it. Then check the rest: no <!-- stack-it:stack start --> block in CLAUDE.md, docs/stack.md present, docs/stack-notes.md present, the plain-path pointer sentence to docs/stack-notes.md inside docs/stack.md's markers, the pointer present in AGENTS.md, the bridge present (or a recorded decline, or a symlink exception), and the format stamp present when stack.yaml exists. A docs/stack.md that went missing while AGENTS.md still points at it is migrate's to restore as a stub; a docs/stack-notes.md missing beside an owned docs/stack.md is not a stop either, since migrate stubs it and document-stack writes it on the next pass. A docs/stack.md whose section has lost the pointer sentence is migrate's to fix only when that section is one of its own stubs, which it rewrites whole; a real section missing the sentence is a piece for document-stack to put back on its next render, and the report names it rather than editing it. Two things are reported layout exceptions rather than missing pieces, and neither is ever written through: an AGENTS.md symlinked outside the project, whose target carries no pointer, and a CLAUDE.md symlinked outside the project that still holds the old block. Reading through such a symlink is safe, so a block at the target is still carried; only the cut is skipped, and the report says the block is still sitting there. Content missing from inside either docs file is not part of this check; regenerating that is document-stack's job, never migrate's. A project with no pointer, no bridge, and neither docs file has never been documented: nothing is broken, so a standalone migrate is a no-op that points the user at document-stack, and document-stack carries on and writes them for the first time.
Re-run only the step that produced the missing piece, and report which one you had to redo. A missing docs/stack.md or docs/stack-notes.md gets the stub from write step 2, so the pointer resolves again and the loaded file points at a notes file that exists; filling either in stays document-stack's job. An owned docs file that is already there is never rewritten, with one exception: a section whose first line is Moved from CLAUDE.md by migrate; run document-stack to regenerate. or Placeholder written by migrate; run document-stack to fill this in. is this skill's own stub, so a stub that has lost the pointer sentence is rewritten whole to put it back. A section holding anything else is content, and content belongs to document-stack however incomplete it looks.
A leftover block is the one repair with a twist. Both docs files already exist and their managed sections belong to document-stack, so the block's text never goes inside either pair of markers. Append it to the tail of docs/stack-notes.md, below its end marker, under a ## Notes carried forward heading, with one provenance line above the carried text. Nothing is ever appended to docs/stack.md's tail: that file is the one every agent loads, and it stays small. The provenance line is the same one document-stack writes, so whichever skill created the heading, the other reuses it:
Carried forward by stack-it. Each line says something the generated section above did not; prune what the section grows to cover.
A tail append is not the 1 → 2 hop, so the hygiene differs on one point. Drop the block's own banner and its old markers as always, but do not promote its headings: heading promotion is for text moving into a file of its own, inside the markers, and nothing under ## Notes carried forward is a heading at all. Set aside the block's scaffolding exactly as document-stack does, its ## Tech Stack, ### Commands and ### Gotchas included, so only bullets land in the tail. Set aside every block line either generated section already states too (the same tool at the same or a newer version, the same command), under the same rule document-stack uses, so the provenance line stays true.
If the tail already carries a ## Notes carried forward heading, reuse it and the first Carried line under it, whatever its wording, rather than adding a second of either, and skip any line of the block already sitting below either file's end marker: never add a note a tail already carries. That makes a repeated repair, after a run that failed between the append and the cut, land on the same file it would have produced the first time. Count that heading, and the Carried lines under it, across the union of both tails, docs/stack-notes.md's and docs/stack.md's. More than one heading across the two files, or a heading with no Carried line under it, leaves no single place the notes belong: write nothing, show the user both tails, and stop.
Then cut the block from CLAUDE.md, destination before source as always, and list what you carried in the report. When CLAUDE.md is a symlink outside the project, the append still happens and only the cut is skipped, exactly as in the 1 → 2 hop; say in the report that the block is still at the target.
If every piece is in place, report that the project is current, list what you checked, and change nothing. When either generated section still opens with one of the two migrate lines, say so: the layout is current, and document-stack has not filled the content in yet.
Migration 1 → 2: out of CLAUDE.md, into a two-file reference plus an AGENTS.md pointer
Format 1 wrote the agent-facing stack summary into a <!-- stack-it:stack start/end --> managed block inside CLAUDE.md. Format 2 splits that reference in two, docs/stack.md for what every agent must have loaded and docs/stack-notes.md for the detail it opens on demand, points at the first from AGENTS.md so every coding agent finds it, and leaves CLAUDE.md as a one-line bridge.
Every check first, then the writes
Nothing is touched until all three checks pass. A stopped: from this skill must never leave a half-migrated tree, and the only way to guarantee that is to learn everything that can go wrong before the first byte changes.
Check 1: validate the lockfile. If .claude/stack-it/stack.yaml exists, run ${CLAUDE_PLUGIN_ROOT}/scripts/validate_yaml.py --stage stack .claude/stack-it/stack.yaml. If it fails, report the validator's output and stop with no edits at all. A broken lockfile is not this skill's to fix, and the stamp in write step 1 must never land on a file that doesn't validate.
Check 2: marker integrity on CLAUDE.md. Apply Marker integrity from Step 1. A broken pair stops the whole hop here, before anything is written. No markers at all is not a failure: there's simply no block, so the cut will be skipped and the rest of the hop still runs.
Check 3: resolve the pointer files. Before the pointer step and the bridge step, resolve AGENTS.md and CLAUDE.md to the files they actually are.
- They resolve to the same file, whichever direction the symlink runs: treat them as one file. Write the pointer line once and skip the bridge step entirely, because a file that is
AGENTS.mdmust never contain@AGENTS.md. CLAUDE.mdis a symlink to anything other thanAGENTS.md(a dotfiles repo, a file shared outside the project): that's an intentional bridge exception. Never write through it. Report where it points and that it doesn't importAGENTS.md, and offer to place the import only if the user asks.AGENTS.mdis a symlink to a file outside the project: same rule, don't write through it.
When that foreign CLAUDE.md holds the old block, read it through the symlink, which is safe, and carry its text into docs/stack-notes.md like any other block. Only the cut can't run: writing through the symlink would edit a file outside the project. Report where it points and that the old block is still sitting at the target for the user to remove. Everything else in the hop still runs.
When the two resolve to one file, note in the report that the block came out of the same file the pointer went into.
Then the writes, in this order
Two of these are ordered for a reason: the stamp goes first because its failure means the rest shouldn't happen, and the block leaves CLAUDE.md only after its text is on disk in docs/stack-notes.md.
1. Stamp the lockfile. This is the first write, deliberately: it's the one that's cheap to undo and the one whose failure means the rest shouldn't happen. If .claude/stack-it/stack.yaml exists, set format: 2 as the first top-level key, above project. If a format line is already there with a lower value (an explicit format: 1), replace that line rather than inserting a second key. Preserve everything else exactly: key order, comments, quoting, and the stack list order, which is the install order. Then revalidate with ${CLAUDE_PLUGIN_ROOT}/scripts/validate_yaml.py --stage stack .claude/stack-it/stack.yaml. If the revalidation fails, restore the file as it was before the stamp, report that the stamp was not applied, and stop before touching any other file.
2. Write both docs files. The block always goes into docs/stack-notes.md, lockfile or no lockfile. A lockfile's fields are structured: a tool, a version, install steps, caveats, and a free-form notes string. Plenty of what a human typed into that block fits none of them, or fits only as prose nobody put there (a package manager that doesn't hoist, a dev server that serves stale CSS, a built-and-pending ledger, a convention learned the hard way), and it exists in no other file, so dropping it loses it for good. Don't sort the block by which field a line might have come from; that call belongs to document-stack, and it makes it by asking what the new render states. Carry it and let document-stack decide what to keep; that call is not this skill's to make.
Create docs/ if needed. docs/stack.md is a stub either way, since the loaded tier is a render and there is nothing here to render:
> Generated by stack-it document-stack, format 2. The section between the markers is regenerated on every run; anything below the end marker is yours and survives. Rationale, wiring, and carried notes live in docs/stack-notes.md.
<!-- stack-it:stack start -->
Placeholder written by migrate; run document-stack to fill this in.
Detail for every pin, the wiring, and the notes carried forward from earlier docs: docs/stack-notes.md. Read it before changing configuration or adding a tool.
<!-- stack-it:stack end -->
The path in that closing sentence is plain text. Never write @docs/stack-notes.md and never make it a markdown link: docs/stack.md is loaded at every session start, and an @ path there would drag the notes file in with it, which is the whole thing this layout avoids.
docs/stack-notes.md takes the carried block:
> Generated by stack-it document-stack, format 2, notes tier. The section between the markers is regenerated on every run; anything below the end marker is yours and survives.
<!-- stack-it:stack start -->
Moved from CLAUDE.md by migrate; run document-stack to regenerate.
...the carried text...
<!-- stack-it:stack end -->
The placeholder line stays the first line inside the markers, which is how document-stack recognizes carried text and knows to show the user what it kept. Carry the content only. Drop the block's own > Generated by stack-it's document-stack... banner along with the markers, so each file ends up with exactly one "Generated by" line and no old markers inside the carried text. Promote the carried headings one level, since they're moving from a section of CLAUDE.md to a file of their own: ## Tech Stack becomes # Tech Stack, ### Commands becomes ## Commands. Say in the report that both files are placeholders document-stack will regenerate.
When there was no block to carry, meaning CLAUDE.md had no markers at all (a project that ran decide-stack but never document-stack), write the same two files with the stub line inside the markers of each, since nothing moved. docs/stack.md is exactly as above, and docs/stack-notes.md is:
> Generated by stack-it document-stack, format 2, notes tier. The section between the markers is regenerated on every run; anything below the end marker is yours and survives.
<!-- stack-it:stack start -->
Placeholder written by migrate; run document-stack to fill this in.
<!-- stack-it:stack end -->
Either way the pointer written two steps down resolves to a file that exists, and that file points at a notes file that exists too. A finished migrate that leaves AGENTS.md aimed at a missing docs/stack.md, or docs/stack.md aimed at a missing docs/stack-notes.md, is a half-state, and half-states are what this skill exists to close.
When an owned docs file is already there, left by an earlier run that got this far and stopped, leave it exactly as it is, byte for byte, and report that you found it. Refreshing its content is document-stack's job, and the one thing this skill ever adds to those files is the carried tail in the Verify path above.
3. Take the old block out of CLAUDE.md. Now that its text is on disk somewhere else, remove everything from <!-- stack-it:stack start --> through <!-- stack-it:stack end -->, markers included, plus one of the surrounding blank lines so the file doesn't end up with a double blank where the block was. This is deliberately after the write above: the source is cut only once the destination exists, so a failure between the two leaves a duplicate rather than a hole. Skip this step when CLAUDE.md is a symlink outside the project, per check 3. Everything else in CLAUDE.md stays byte for byte as the user left it: their house rules, their deployment notes, their headings, their spacing.
4. Ensure the AGENTS.md pointer. AGENTS.md gets exactly one line from stack-it, verbatim:
Before adding a dependency or running the build, test, or lint toolchain, read the stack reference: @docs/stack.md
This is a presence check, not a managed block. If AGENTS.md already contains @docs/stack.md outside backticks, do nothing, even if the sentence around it has been reworded; the pointer is there and the wording is the user's. Otherwise append the line, creating AGENTS.md if it's missing and putting a blank line before it if the file already has content. Never add markers to AGENTS.md, never wrap the path in backticks or a code fence (a backticked path isn't an import, so Claude Code would skip it), and never add a second line for docs/stack-notes.md.
5. Ensure the CLAUDE.md bridge. Claude Code loads CLAUDE.md, not AGENTS.md, at session start, so it needs a one-line file that imports the other. Take the first case that matches:
CLAUDE.mddoesn't exist, or holds nothing but whitespace: create it containing the single line@AGENTS.md. If any text remains, take one of the cases below instead.CLAUDE.mdis a symlink toAGENTS.md, or already contains@AGENTS.mdoutside backticks: leave it alone.CLAUDE.mdalready contains@docs/stack.mdoutside backticks (the fallback from an earlier declined import): leave it alone.CLAUDE.mdexists with none of those: ask once, "add@AGENTS.mdat the top of CLAUDE.md? If not, I'll append the pointer line instead so Claude Code still reads docs/stack.md." On yes, add that one line at the top. On no, append the pointer line, under the same presence check and with no markers.
CLAUDE.md never receives generated stack content. The most it ever holds is that one import line or that one pointer line.
Adding a future migration
Each format bump is one more numbered section, run in sequence until the project reaches the current format. To add 2 → 3: write a ## Migration 2 → 3 section describing only that hop, then bump the current format number everywhere it's stamped:
- Step 1 of this skill, and every header line in write step 2: the
docs/stack.mdstub, the carrieddocs/stack-notes.md, and the stubdocs/stack-notes.md. document-stackStep 1, and both header lines in its Step 4a.decide-stack's YAML example and the prose under it.- The input YAML examples in
install-stackandscaffold-and-verify. - The schema comment in the plugin README.
- The fixture lockfiles, fixture docs files, and evals under
document-stack,migrate, andsetup-stack. - Then run
rg -n 'format[: ]+2' plugins/stack-itto catch anything this list missed.
Earlier hops keep their own numbers: the 1 → 2 section's heading and its format: 2 stamp stay at 2. A project at format 1 then runs 1 → 2 and 2 → 3 back to back, which is why each hop must leave the project in a valid state on its own rather than assuming the next one follows.
Report
Say what actually happened, file by file:
- What changed: the CLAUDE.md block removed, its text carried into
docs/stack-notes.md(inside the markers on the 1 → 2 hop, below the end marker under## Notes carried forwardwhen the block turned up at format 2, listed note by note), a pointer line appended toAGENTS.md, aCLAUDE.mdthat got the@AGENTS.mdimport or the appended pointer line, theformatstamp written tostack.yaml. - What was created:
docs/stack.mdholding the stub,docs/stack-notes.mdholding either the carried text or the stub,AGENTS.md, theCLAUDE.mdbridge. - What was left alone: a pointer already present, a
CLAUDE.mdthat already bridged or already carried the pointer, an owned docs file that was already there, a symlink you refused to write through, the README. - The format the project started at and the format it's at now.
End the report with one of two lines, so a caller can tell the outcome without parsing prose: migrated to N when the project reached format N, or stopped: <reason> when you stopped without finishing (an unowned docs file, a format ahead of this plugin, broken markers, a lockfile that failed validation, a stamp that failed revalidation).
If document-stack invoked you, hand back to it; it writes the real content next. If the user ran you standalone and you wrote the docs files, say that docs/stack-notes.md holds the carried text or a stub, that docs/stack.md is a stub, and that document-stack runs next to turn them into the real reference. When you wrote neither, say that instead: on a never-documented project both files are simply absent, and pointing at content you did not write would be a lie the user acts on.
Boundaries
This skill restructures stack-it's artifacts and nothing else. It doesn't choose tools (decide-stack), install them (install-stack), verify them (scaffold-and-verify), or write the stack documentation (document-stack). It never generates or refreshes stack content: the one exception is moving the old CLAUDE.md text into docs/stack-notes.md, inside the markers or below them, and writing the one-line stubs when there was no block to move, and even then it copies rather than composes. It never writes into a managed section somebody else's skill owns. It never edits content between the README's <!-- stack-it:stack start/end --> markers; the README's managed block is already format-2 shaped and stays where it is. It doesn't touch anything in CLAUDE.md outside the old managed block, and it doesn't rewrite the user's own words anywhere.
Bundled resources
${CLAUDE_PLUGIN_ROOT}/scripts/validate_yaml.py: Validate the lockfile before any edit and again after stamping it withformat, with--stage stack. Runpython ${CLAUDE_PLUGIN_ROOT}/scripts/validate_yaml.py --helpfor usage.