agent-migrate
One-shot migration of an existing .workflow/ markdown workflow (BACKLOG.md / TODO.md / DONE.md / plans/ / tasks/) into a freshly-configured GitHub Project board. Idempotent, produces backup + mapping
SKILL.md.claude/skills/agent-migrate/SKILL.md
GitHubAgent Migrate Skill
When invoked with /agent-migrate, take this project's existing markdown
workflow files and push them into a GitHub Project board. After this skill
finishes, agent.yaml will have workflow.backend: github_project and
all subsequent agent runs will go through board-man.
This is a one-shot, opt-in migration. It is safe to re-run (idempotent — already-migrated items are detected and skipped). It produces a complete backup and a mapping report so the user can audit every change.
You never write application code. Your job is purely to translate markdown state into board state.
Hard rules
- Do NOT delete any source markdown until Step 8 (Archive), and only after explicit user confirmation.
- Do NOT push or merge anything to git remotes. agent.yaml changes and any
archive
git mvoperations are staged but not committed by this skill — the user reviews and commits. - Every
ghwrite goes through board-man. This skill itself never shells out togh project/gh issue/gh api graphql. Spawn board-man viaAgent(subagent_type="board-man", prompt="<op> <args>"). - A
200mssleep between board-man write calls in Step 4 to keep API rate limits comfortable.
Step 0: Pre-flight
- gh auth. Run
gh auth status. If it fails, stop and instruct the user to rungh auth login. - gh project scope. Verify the token has the
projectscope (look forprojectin the token-scopes line ofgh auth status). If missing, stop and tell the user to rungh auth refresh -s project. - agent.yaml exists. Read
agent.yaml. If it does not exist, stop and tell the user to run/agent-initfirst. - Already-migrated check. If
agent.yamlalready hasworkflow.backend: github_project, ask "This project is already on the github_project backend. Re-run anyway? [y/N]". On no, exit. On yes, continue (board-man will dedupe writes viafind-issue-by-marker). - Project info. Prompt the user for:
- GitHub owner (user or org). Default: parse from
git remote get-url origin. - Repo (
owner/repo). Default: parse fromgit remote get-url origin. - Project number. Leave blank to create a new project.
- Project title (only used if creating). Default:
<project.name from agent.yaml> board.
- GitHub owner (user or org). Default: parse from
- Provision the project. Run:
This creates the project (if no number given), provisions all labels (FEATURE, CHANGE, BUG, TASK, parallel-group/0..9), ensures Status options BACKLOG/READY/TODO/IN-PROGRESS/DONE, ensures the Parallel Group number field, and writes<framework>/scripts/board-man-setup.sh --owner <o> --repo <r> [--number <n>] [--title <t>] --project-root <pwd>.workflow/temp/.board-man-cache.json. Locate<framework>via$CLAUDE_AGENT_HOMEif set, otherwise default to$HOME/.claude-agent(the install.sh default), with a fallback todirnameof this skill's location traced two levels up. If the script exits non-zero, surface the stderr to the user and stop.
Step 1: Snapshot
Create a timestamped backup directory:
.workflow/migrate-backup-<YYYY-MM-DDTHH-MM-SS>/
Copy (do not move yet) the following into it, preserving relative structure:
<workflow.backlog_file>(default.workflow/BACKLOG.md)<workflow.todo_file>(default.workflow/TODO.md)<workflow.done_file>(default.workflow/DONE.md)<workflow.bugs_file>(default.workflow/BUGS.md) — if it exists<workflow.plans_dir>/(default.workflow/plans/) — recursively<agents.tasks_dir>/(default.workflow/tasks/) — recursively
Write a MANIFEST.md in the backup dir:
# Migration backup — <ISO timestamp>
| Source | Lines | SHA-256 |
|--------|-------|---------|
| BACKLOG.md | <wc -l> | <sha256> |
| TODO.md | <wc -l> | <sha256> |
| ... | ... | ... |
This guarantees the user can diff or restore later.
Step 2: Build the ledger
Parse the source markdown into an in-memory ledger of items to migrate. The shape:
{
"feature_items": [
{"title": "...", "type": "FEATURE", "gh_marker": "[GH #123]" | null,
"source_line": "BACKLOG.md:14", "body": "<original line + any trailing context>",
"matched_plan": null, "matched_tasks": [], "destination_issue": null}
],
"change_items": [...],
"bug_items": [...],
"todo_items": [...],
"done_items": [...],
"plan_files": [
{"slug": "add-auth", "path": "plans/add-auth.md", "body": "<full file>",
"matched_item": null}
],
"task_files": [
{"slug": "auth-backend", "path": "tasks/auth-backend.md",
"metadata": {"Type": "...", "Status": "...", "Repo": "...",
"Parallel Group": 1, "Branch": "...", "Source Item": "..."},
"body": "<full file>", "matched_parent": null}
]
}
Parsing rules:
BACKLOG.md / TODO.md / DONE.md:
- Recognize
## Features,## Changes,## Issues(case-insensitive) section headers. Each line of form- [ ] <text>becomes an item with type FEATURE / CHANGE / BUG respectively. Items in TODO.md and DONE.md default to FEATURE unless their[GH #N]marker resolves to an existing issue with a different label, in which case use that label. - A leading
[GH #N]token on the line populatesgh_marker. - Multi-line item bodies (continuation lines indented under the bullet)
are folded into the item's
bodyfield.
plans/*.md:
- One file = one plan. The slug is the basename without
.md. - The H1 header (
# <something>) is the plan title. Use it for matching.
tasks/*.md:
- One file = one task. The slug is the basename without
.md. - Header is
# Task: <task-name>. Body has bulleted YAML-ish metadata (Type, Status, Repo, Parallel Group, Branch, Source Item, Dependencies). Parse each- **Field**: valueline.
Plan ↔ Item matching (fuzzy):
For each plan, lowercase its slug and the title of every backlog/todo/done
item, strip non-alphanumerics. The item with the closest match (by
substring or Levenshtein distance ≤ 4) is the candidate. If no candidate
within tolerance, leave matched_item: null and ask the user in Step 3.
Task ↔ Parent matching:
The task's Source Item field is the join key. Lowercase + strip and look
up against item titles. If no match, leave matched_parent: null for
Step 3.
Step 3: Resolve ambiguities (interactive)
For each unmatched plan, ask the user:
plans/<slug>.mddoes not match any backlog/todo/done item. (a) Skip — leave the plan file in place, do not migrate. (b) Create a new FEATURE issue titled "<inferred from H1>" and attach this plan to it. (c) Attach to an existing item: <numbered list of items> Choice [a/b/c]?
For each unmatched task, ask:
tasks/<slug>.mdhas Source Item "<source>" which does not match any feature. (a) Skip — do not migrate. (b) Create a synthetic parent FEATURE titled "<task slug rolled up>" and attach this task as a sub-issue. (c) Attach to an existing item: <numbered list> Choice [a/b/c]?
For each item that appears in DONE.md, confirm the import policy once (applies to all done items):
Import DONE.md items as closed issues with status DONE? [Y] Alternative: skip / migrate as open in BACKLOG. Choice [Y/s/o]?
If the user picks (a) for any item, mark it migrate: false in the ledger
and surface it in the final mapping report as "skipped".
Step 4: Push to GitHub (the writes)
Order matters — parents before plans before children. Between each
board-man write call, sleep 200ms (sleep 0.2) for rate-limit hygiene.
4a. Parents
For every feature_items[], change_items[], bug_items[],
todo_items[], done_items[] with migrate: true and no
destination_issue yet:
- Check if it's already on the board:
If found, setboard-man find-issue-by-marker "<title-or-gh-marker>"destination_issueto that issue number and skip to next item. - Otherwise create:
Body footer (always):board-man create-issue <TYPE> <title> <body-with-marker-footer>\n\n<!-- migrated-from: <source_line> -->Ifgh_markerwas present: also append\n*Originally tracked as <gh_marker>.* - board-man already sets the new issue to BACKLOG. Items from TODO.md/DONE.md
need their status corrected:
- TODO.md item (FEATURE/CHANGE/BUG, with no plan and no tasks) → leave at BACKLOG
- DONE.md item (and the user said Y to "import as closed") → board-man
set-status <#> DONE. Then close the issue:gh issue close <#> --repo <r>. (This singleghis acceptable becausegh issue closeis not a project-board operation.)
- Record
destination_issuein the ledger and append a row to the in-memory mapping report.
4b. Plans
For every plan_files[] with a matched_item that has destination_issue set:
- Build the plan body by reading the plan file. Prepend an H1 if missing:
# Plan: <feature-slug>\n\n<file contents> - Call:
board-man returns the comment ID and pinsboard-man post-plan-comment <destination_issue> <plan-body><!-- plan-comment-id: <id> -->into the parent body. - Advance the parent to READY (only if it's currently in BACKLOG):
board-man set-status <destination_issue> READY - Record the mapping:
plans/<slug>.md → comment <id> on issue #<n>.
4c. Tasks
For every task_files[] with a matched_parent that has
destination_issue set:
- Build the title:
Task: <task-name>(from# Task:header). - Build the body: take the entire original task file content. Append a
footer:
\n\n<!-- migrated-from: tasks/<slug>.md --> - If body length > 60000 chars, split: first 60k as
body, remainder as the first comment after creation. Append a(continued in comment below)line at the truncation point. - Repo validation (single-repo gating). Read the parsed
Repo:metadata. If it does not equalagent.yaml.workflow.github_project.repo(e.g.,mobile.iosvswhinchman/midi-man-mk3), print a WARNING: "task<slug>has Repo:<value>which does not match the configured single-repo board (<configured>). The sub-issue will still be created, but coders may not match it. Multi-repo support is a follow-up." Then continue. - Create the sub-issue:
board-man does: create issue with TASK + parallel-group/<N> labels, add to project, set Status TODO, set Parallel Group field, link as sub-issue via GraphQLboard-man create-task-issue <parent#> <title> <body> <parallel_group> <repo> <branch>addSubIssue. - If the original task
Statuswasdone:board-man set-status <new-issue#> DONEgh issue close <new-issue#> --repo <r>
- If the body was split (step 3): post the remainder via
board-man add-comment <new-issue#> <remainder>. - Record the mapping:
tasks/<slug>.md → issue #<n> (parent #<p>, parallel-group/<g>).
4d. Done items reconciliation
For every item that came from DONE.md (and the user opted to migrate as
closed), if it has no remaining open task children, ensure it's in DONE
and closed. board-man's verify-children-done does the check; loop and
advance any parent whose children are all DONE.
Step 5: Reconcile parents
For every parent (feature/change/bug) whose ledger row gained children in Step 4c:
board-man verify-children-done <parent>
If all_done: true, the parent should be in DONE — call
board-man set-status <parent> DONE. Otherwise leave at READY.
Step 6: Mapping report
Write .workflow/migrate-backup-<ts>/MAPPING.md. One section per source
type. Example shape:
# Migration mapping — <ISO timestamp>
Project: https://github.com/users/<owner>/projects/<n>
Repo: <owner/repo>
## Parents
| Source | Destination | Status |
|-----------------------------------------|--------------------------------------------------|----------|
| BACKLOG.md:14 (Features → "Add auth") | https://github.com/o/r/issues/42 | BACKLOG |
| BACKLOG.md:18 (Issues → "Login crash") | https://github.com/o/r/issues/43 | BACKLOG |
| DONE.md:8 (Features → "Add login") | https://github.com/o/r/issues/12 (closed) | DONE |
## Plans
| Source | Destination |
|-------------------------|----------------------------------------|
| plans/add-auth.md | comment #1234567 on issue #42 (READY) |
## Tasks
| Source | Destination |
|-------------------------|--------------------------------------------------------------|
| tasks/auth-backend.md | https://github.com/o/r/issues/44 (TODO, parent #42, parallel-group/1) |
| tasks/auth-frontend.md | https://github.com/o/r/issues/45 (TODO, parent #42, parallel-group/1) |
## Skipped
| Source | Reason |
|-------------------------|-------------------------------------------------|
| plans/old-experiment.md | User chose (a) — left in place, not migrated |
## Warnings
- task `mobile-ios-foo`: Repo:`mobile.ios` does not match configured repo `whinchman/midi-man-mk3`
Print the path to the mapping report and a one-line summary
(Migrated N parents, P plans, T tasks. Skipped K. Warnings: W.).
Step 7: Update agent.yaml
Edit agent.yaml:
- Set
workflow.backend: "github_project" - Add (or replace) the
workflow.github_projectblock:github_project: owner: "<owner>" number: <number> repo: "<owner/repo>"
Show the diff to the user (use git diff agent.yaml). Do NOT commit.
Step 8: Archive the markdown (with confirmation)
Ask:
The board is now the source of truth. Move BACKLOG.md, TODO.md, DONE.md, BUGS.md, plans/, and tasks/ into
.workflow/migrate-backup-<ts>/so they stop confusing future agent runs? [Y/n]
On Y:
git mveach file/directory into the backup dir. (Use git so the rename is preserved in history.)- Leave the backup dir tracked in git so the snapshot survives in the repo's history.
On n:
- Leave them in place. Print: "Markdown files retained at their original
paths. board-man will ignore them in
github_projectmode, but they may drift out of sync with the board going forward."
Either way, do NOT commit. The user reviews and commits.
Step 9: Stamp the per-project install
Run:
<framework>/scripts/install-skills-local.sh <pwd>
This copies skills + agents into .claude/skills/ and .claude/agents/,
writing .claude/skills/.framework-version with the framework's git SHA
and branch. After this, the project is pinned to the framework version
that ran the migration — future framework updates won't change behavior
until the user explicitly re-runs install-skills-local.sh.
Also copy the issue templates:
mkdir -p .github/ISSUE_TEMPLATE
cp -n <framework>/templates/.github/ISSUE_TEMPLATE/*.yml .github/ISSUE_TEMPLATE/
(-n so it never overwrites an existing template the project may have
customized.)
Step 10: Final report
Print:
- Project URL (https://github.com/users/<owner>/projects/<n>)
- Path to the mapping report
- Path to the backup directory
- Whether markdown was archived
- Suggested next commands:
git status # review changes (agent.yaml, .claude/, .github/, archive) git diff agent.yaml # confirm the backend switch cat .workflow/migrate-backup-<ts>/MAPPING.md /coordinator # exercise the new pipeline against the board
Failure recovery
If anything in Step 4 (writes) fails partway through:
- Print the last successful operation and the error from board-man.
- Tell the user: "The migration is partially complete. Re-running
/agent-migrateis safe — already-created issues will be detected viafind-issue-by-markerand skipped. To roll back instead: revert agent.yaml, leave backup in place, manually delete created issues from the board UI." - Do NOT continue to Steps 7–10.
If board-man returns {"error": "lock timeout", ...}: another agent is
holding the write lock. Wait 60s and retry once. If it still fails, surface
to user.
See also
~/.claude/skills/board-man/SKILL.md— operations this skill calls<framework>/scripts/board-man-setup.sh— provisioning helper invoked in Step 0<framework>/scripts/install-skills-local.sh— per-project skill copy~/.claude/skills/agent-init/SKILL.md— fresh-project initialization with the same backend choice~/.claude/skills/agent-upgrade/SKILL.md— schema migrations between framework versions