# Migrate a Markdown export into Blenau

**Give this whole file to your agent.** It is a working procedure, not an
explainer: every rule below exists because skipping it cost a real migration
real rework.

It has been run end to end on exports of 300–900 images and 20–45 finished
documents. The shape it assumes is any Markdown export that carries its images
inline or as base64 definitions — a Google Docs "Download as Markdown", a Notion
or Confluence export, a folder of legacy wiki pages concatenated.

> **Your workspace's own values are not filled in here.** Copy this playbook
> from your Blenau dashboard (or `blenau playbooks get <id>`) and it arrives with
> your workspace slug and your connected repos already in it. Ask your agent to
> run `list_repos` first if you are working from this generic copy.

## What the agent needs from you before it starts

1. **The local path to the exported `.md` file.**
2. **The source, for provenance** — the URL of the original document if there is
   one, or the team/author who owns it if there is not (then
   `sources=[{"type": "team", "ref": "<Team>"}]`).

If either is missing, the agent must **ask** rather than guess. It must not go
hunting the disk on its own initiative: download folders differ per machine and
usually hold several `.md` files. Offering to look ("want me to check your
Downloads folder?") is fine — but either way it must confirm the exact file back
to you: full name, size and date. *"Found `handbook.md` (36 MB, today 12:17) — is
this the one?"*

> Even when you say "there's no source URL", the agent should search the brain
> for the original document's id before giving up on provenance: if anything was
> ever migrated out of this same source, it probably links to it. Report what
> turns up; never adopt it as the source without your confirmation.

## Phase 0 — Analyse and propose (STOP and ask before writing anything)

1. **Split text from images.** Exports carry images as trailing definitions
   (`[image12]: <data:image/png;base64,…>`) while the body keeps inline markers
   (`![][image12]`). Produce a text-only copy by removing the definition lines
   and measure: text size, image count, and the H1/H2 title map
   (`grep '^#\{1,2\} '`).

   In large manuals, H1s with an emoji are usually **thematic block dividers**
   rather than chapters — they are the natural candidates for folders.

2. **The golden rule: search before writing.** Run `search_knowledge` on the
   manual's topics and `list_repos` before creating anything. Merge with what
   exists; do not duplicate.

   Look specifically for **documents that already came out of THIS source**. It
   is common for somebody to have migrated one section by hand months ago. Those
   get merged, not left to coexist (Phase 2 §7).

3. **Propose the destination and the split, then wait for a human OK.** Two
   decisions:

   - **Repo and path prefix.** One repo per functional area, with a matching
     Blenau path prefix. Do not reuse a generic catch-all repo just because it
     exists and looks vaguely right — a repo named for "general docs" is a
     drawer, and a migration that lands in a drawer has to be moved out later.
     When in doubt: a dedicated repo, and ask.

   - **Flat documents, or one subfolder per thematic block** with `assets/` at
     the root. Offer both. Past ~20 documents the flat variant is unreadable —
     recommend subfolders. From a subfolder, image links are `../assets/…` and
     cross-links between blocks are `../folder/doc.md`.

   > **The area repo is not automatically the right destination — the reason can
   > be PERMISSIONS.** A payroll manual looks like it belongs in the HR repo, but
   > access to payroll is usually not granted to everyone with access to HR. Ask
   > in Phase 0 whether this content has a different audience from the rest of
   > the repo. That is a business decision, not a technical one.

   > **If the target repo already exists, read what its README claims to be.** A
   > repo of technical notes that receives a 43-document user manual has stopped
   > being what it says it is. Rewrite its scope statement in the same
   > migration, or the index lies from day one.

## Phase 1 — Repo and assets (images go FIRST)

4. **Create the repo if it does not exist yet** (`gh repo create <org>/<name>
   --private`), then **connect it** —
   `connect_repo(repo, path_prefix, installation_id)`. Skip either step if it is
   already done. If the Blenau GitHub App cannot reach the repo the call fails
   with an actionable error: Blenau cannot grant its own App access, so
   installing or extending the App is a human step.

5. **Extract the base64 payloads** to `imageN.png` with a regex over the
   definition lines, keeping the export's global numbering.

6. **Images always go to `assets/<manual-name>/imageN.png`, never bare
   `assets/`.** This is not only about repos with previous history: **numbering
   collides between exports.** In one migration, `image344`–`image347` from the
   new manual and the `image344`–`image347` already in the repo were completely
   different pictures; without the subfolder, four screenshots of an unrelated
   document would have been overwritten with nobody noticing.

   Check the tree before copying:
   `gh api repos/<org>/<repo>/git/trees/HEAD?recursive=1`.

7. **Push all images in ONE git commit** (clone, copy, push). Do not use
   `create_asset_upload` for hundreds of images — that path is for a handful;
   Blenau's assets are simply files in the repo.

   With Blenau writing to the same repo concurrently, a `--depth 1` clone cannot
   rebase (it conflicts on the `index.md` Blenau regenerates). The loop that
   works: `fetch --depth 1` → `reset --hard FETCH_HEAD` → copy the PNGs →
   `commit` → `push`, with retries.

8. **Verify BOTH path shapes with `verify_asset` before launching any agent.**
   From a subfolder, `../assets/<manual>/imageN.png` returns `resolves: true`
   while `assets/<manual>/imageN.png` returns `false`. Without the `../`, Blenau
   resolves against the document's own folder. Read both `exists` and
   `renderable`: they answer different questions.

> **Why images come first: it deletes an entire phase.** With the assets already
> in the repo, the agent writing each section emits the final
> `![alt](../assets/…)` in the same pass — and the alt text comes out better,
> because it has the surrounding prose in front of it. It saves a second round of
> N agents and, above all, the storm of `409 Conflict` that round produces.
> Measured: 43 documents and 633 images with **a single 409** in the whole
> migration.

## Phase 2 — Migrate the text

9. **Slice the text-only copy by line ranges** derived from the H1 map — with a
   script, not by hand. The script must **verify coverage**: markers covered vs.
   total, unassigned images, and non-empty lines that fall in no slice. If the
   numbers do not add up, the slicing is wrong — do not launch agents yet.

10. **Write the rules ONCE into a file (`RULES.md`) and give each agent only its
    own fragment plus its own data.** Repeating the rules across 43 prompts
    guarantees they drift; with a single file, a correction reaches every agent
    that has not started yet.

11. **Run a mandatory pilot on one small section.** Besides migrating it, ask it
    to **audit the rules**: what was ambiguous or impossible to apply. In one
    migration the pilot came back with 6 genuine defects in the rules file.
    Fixing them cost minutes; not fixing them would have cost 43 uneven
    documents.

12. **One subagent per document**, in batches of ~10. The rules file must
    contain all of the following — every one of them learned from a failure:

    - **Total fidelity**: do not summarise, rewrite, invent or reorder. When in
      doubt, leave it alone and report it.
    - **Anti-overcorrection**: menus, field names, buttons, codes, account
      numbers and percentages are copied literally **even when they contain
      typos**. Without this rule agents "fix" the manual — real reverts
      included a product term replaced by a different product term, and a
      button label silently corrected to the one the agent expected.
    - Strip `{#…}` anchors; self-referential title links (`## [X](#x)`) become
      `## X`.
    - **Title links that point OUTWARD are not discarded**: clean the heading
      and move the link down to the first line of the body. Where this rule was
      missing, an agent ate a link to another manual and it had to be repaired
      by hand.
    - **Hierarchy: shift down exactly ONE level** (`#`→`##`, `##`→`###`,
      `###`→`####`), preserving nesting. Do not flatten everything to H3 — that
      turns parents and children into siblings.
      **Exception**: if the fragment comes from a chapter that is now a FOLDER,
      its `##`s are already the final sections and must not shift. Say so
      explicitly in those agents' prompts or they will flatten the document.
    - Drop empty or image-only headings, merging their content into the previous
      section — **unless** the prompt names them as a chapter, in which case
      keep and report them.
    - **Callouts**: exports degrade boxed notes into a one-column table with no
      body row (`| Note … |` + `| :---- |`) or into a paragraph with a label and
      a forced line break. Both become a blockquote with the leading word in
      bold and the text verbatim. Judge by the block's ROLE, not its shape. Real
      tables are preserved.
    - Fix export escaping (`1\.`, `field\_x`, `\-\>`, `\>`).
    - Spacing: one paragraph per block; trailing double spaces are forced line
      breaks.
    - `![][imageN]` → `![descriptive alt](../assets/<manual>/imageN.png)`, with
      the alt written **in the manual's own language**, not the agent's.
      **Square brackets are forbidden inside the alt text**: they break parsing
      and the asset disappears.
    - **Open EVERY PNG with Read before writing its alt text.** Not optional,
      and the highest-value rule in the whole procedure (see §19).
    - Write with `ingest_document(path, title, content, source_type="manual",
      sources=[…])`.
    - A `similar_doc_exists` warning between sibling sections of the same manual
      is ignored; if it points at a **different** repo in the brain, record it
      as a cross-link candidate.
    - **Do not use `list_assets` to verify** — it times out on documents with
      many images.
    - **Mandatory report**: `sections_reviewed`/`sections_total` (defined as
      "headings in the finished document, H1 included"),
      `images_with_verified_alt`/`images_total`, `pending_anchors` and
      `questions`. Without explicit coverage there is no way to tell "reviewed
      and correct" from "never looked at".

13. **Write the README index** and verify: `list_documents` must show every
    document as `ready`.

14. **Merge the pre-existing documents that came from this same source**
    (Phase 0 §2). Hand it to ONE agent — the owner of the destination document —
    with explicit instructions: which sections to merge, what each version
    contributes, and **which asset path each image uses** (the old document's
    images are usually in the root `assets/`, not in the manual's subfolder).

    Then retire the old document with `delete_document(path)` — preview it with
    `dry_run=true` first. It removes the `.md` from GitHub and the index in one
    call, embeddings included, and the delete stays recoverable from git
    history. (`git rm` + push still works and syncs back; it is no longer the
    only way.) Do not delete its images if the merged document still uses them,
    and do not leave the old document empty or as a redirect stub: merging is
    supposed to end with fewer documents.

15. **Verify by script, not by eye.** Against each document's raw content on
    GitHub: count leftover markers, resolve every image path against the repo
    tree, resolve every `](x.md)`, and flag cross-document `#` anchors and alts
    containing brackets. Image counts must reconcile document by document
    against the slicing plan, and the total assets used against the export's
    image count.

## Phase 3 — Cross-links (one agent per FOLDER)

16. Cross-links are `[text](../folder/doc.md)` at document level, naming the
    target section in the link text. **Never `#` anchors BETWEEN documents**
    (anchors *within* the same document are fine when they resolve against one
    of its own headings). Clean up the export's dead anchors. Links to external
    documents that have **not** been migrated are kept as they are; links to
    manuals that **have** been migrated become references into the brain.

    One agent per FOLDER, as exclusive owner: fewer agents, zero collisions, and
    it can see the whole block. Inside the agent, sections run in series.

    **Give each agent the concrete destinations, not just the index.** Agents
    obey "do not invent links" well, which means that without a list of real
    targets they leave almost everything as plain text. Generate a `TARGETS.md`
    by script — every document with its title and first-level sections — and
    pass it whole. The raw material is the `pending_anchors` each Phase 2 agent
    reported.

## Phase 4 — A DEDICATED editorial pass (one agent per DOCUMENT)

17. **The editorial pass is not a passenger on the cross-link pass.** Tried
    once, and it came out uneven: agents work by exception, so wherever there
    were no links to create they barely read the text. Measured result: **8 of
    43 documents received not a single edit**, while the largest block — 9
    documents and 1,700 lines — got 10 corrections between them and one small
    folder got 28. It had to be redone from scratch.

18. **One agent per document, with the original extract in front of it.** That
    makes the pass two things at once, and the second is where the value is:

    - **Fidelity audit**: nothing lost, nothing invented, nothing reordered.
      This is what caught a migration that had *deleted a hyperlink and replaced
      it with the phrase "in this manual"* — a false statement — and a pilot
      document that had grown an invented introductory paragraph.
    - **Editorial**: grammar, consistent impersonal tone, exact duplicates,
      broken markdown, and arithmetic or technical errata that are
      **demonstrable** — those get corrected outright, not flagged. The line is
      whether the error can be shown from the document itself; anything needing
      product knowledge to judge belongs in §19's report instead.

19. **Alt texts are verified by opening the PNG**, one by one. In one migration
    ~450 of 770 had to be rewritten for contradicting the screenshot ("generated
    accounting entry" over an empty field, "option enabled" over an unchecked
    box, the same image reused in four sections with an alt that fitted only
    one).

    If §12 was done properly, this is a **sampling** pass instead. And the side
    effect is the most valuable finding of a migration: **agents surface the
    contradictions between the manual and the product's actual behaviour** ("the
    text says to type a value into a field that does not exist in the
    screenshot"). Those are **not** corrected — they are reported to the author.

20. **Demand explicit coverage**: `sections_reviewed` == `sections_total`.
21. **Same anti-overcorrection rule as Phase 2**: when in doubt, leave it and
    report it.
22. **Do not rename headings** — the cross-links depend on them. Blocks that are
    thematically misplaced move to their own document, and the README is updated.
23. **Audit the README and anything the orchestrator wrote.** Nobody has reviewed
    those. One README carried 18 descriptions that did not match the real
    content, one of them attributing a topic to a document that does not cover
    it. An index that lies is worse than no index.
24. **Keep a debt inventory throughout** (`PENDING.md`): rules corrected
    mid-flight that left earlier documents on the old criterion, chapters that
    arrive empty from the original, duplication between siblings,
    text/screenshot contradictions, and export junk. Agents report these
    generously in their `questions` field; what gets lost is what nobody wrote
    down.

## Operational gotchas

- **Concurrent-subagent cap.** Every agent runtime has one — 20 in the one these
  migrations were run on — and **rejected launches are not retried for you**.
  Launch in batches of ~10 and stay a couple below the cap rather than riding
  it. Keep a queue file with each document's state, or you will lose one
  silently: it happened, and one document had no images until the final
  verification caught it.
- **`409 Conflict` from GitHub is NOT `version_mismatch`.** With many agents
  writing — and also with two `edit_section` calls in parallel **on the same
  document** — GitHub rejects the commit. Retry with the **same**
  `expected_version` after 10–20 s. Only a genuine `version_mismatch` means you
  must re-read the section.
- **`edit_section` rejects a `new_content` that opens with a different heading**
  (`heading_in_body`). To restructure a body with subsections, open with a
  paragraph and put the `###`s after it. It replaces the BODY; to rename a
  heading use `rename_section`.
- **Duplicate headings**: `edit_section` accepts `position`; `get_section` does
  not — read those with `get_document`. A section's version hash is
  `sha256(section_markdown)[:16]`.
- **Count markers with `!\[[^\]]*\]\[image\d+\]`, not `!\[\]\[image\d+\]`** —
  exports emit some markers with alt text already present, and the empty-alt
  pattern loses them.
- **`suggest_crosslinks` is useless on migrated manuals**: an identical
  boilerplate `## Sources` section saturates its suggestions. `audit_links` can
  return cached output. The reliable check is the raw file on GitHub.
- **MCP timeouts** on large documents with a busy server: retry, or verify via
  raw GitHub. Writes are transactional with an optimistic lock, so a timeout
  never leaves a half-written document.
- **`update_repo` moves an entire prefix** without re-ingesting.
- Blenau materialises every write as a **commit in GitHub** (author = the user,
  co-author Blenau Bot). Seeing commit traffic is normal — that is the
  brain/repo duality working.
- **Promotional or navigational blocks** at the end of an export ("you may also
  like", partner tables) are not knowledge: discard them. Their images explain
  the gap between assets extracted and assets used.
- **Self-references to the source document**: the manual links to itself. Those
  DO become internal cross-links, since the content is now migrated. Only links
  to *other*, unmigrated documents are preserved.
  Watch out when searching for them: the provenance line every migrated document
  carries (`## Sources`) **also contains the source document's id**, so a naive
  search returns every file. Filter that line out before counting.
- **Do the bulk verification from a script, not from the shell.** Shell tooling
  mangles the regexes these checks need — a pattern full of brackets and dots
  reads as a path or a glob, and the run either errors or silently matches
  nothing, which looks exactly like "no problems found". Write the checks in
  Python and print counts.
- **Windows**: the console is cp1252 and any emoji or accented character will
  break a `print`. Set `PYTHONIOENCODING=utf-8` and
  `sys.stdout.reconfigure(encoding='utf-8')` in every script. In PowerShell, a
  here-string passed to `git commit -m` breaks on inner quotes — write the
  message to a file and use `git commit -F`, with
  `[System.IO.File]::WriteAllText(…, UTF8Encoding($false))` so no BOM is added.

## Sizing a new migration

| Text | Images | Finished documents | Notes |
|---|---|---|---|
| — | 876 | 21 | First run; landed in the wrong repo and had to be moved |
| — | — | 24 | Existing area repo; asset numbering collided |
| 1,700+ lines | 770 | 43 | Editorial pass done as a passenger — had to be redone |
| 340 KB | 633 | 43 | Assets first: no image phase at all, one single 409 |

The last row is what this playbook produces when followed in order.
