| name | migrate-guide |
| description | Migrate a standalone guide or learning path to the Pathfinder package format by generating manifest.json (and path-level content.json for LJs). Reads content.json, index.json, recommender rules, and optionally website markdown to derive all manifest fields. Use when the user wants to migrate a guide directory to the package format, or asks to create a manifest.json for a guide. |
Migrate Guide to Package Format
Migrate a single guide directory or a learning path (*-lj) to the Pathfinder two-file package model (content.json + manifest.json). The skill is invoked on one directory at a time and is safe to run in parallel across different directories.
Read these reference documents on demand for field-level detail:
docs/manifest-reference.md — authoritative derivation rules, templates, naming conventions, fallbacks
.cursor/authoring-guide.mdc — guide content conventions (for path-level content.json authoring)
Keep this skill focused on workflow/orchestration. Do not duplicate field derivation tables from docs/manifest-reference.md.
Safety Invariants
These rules are inviolable — the skill must never break them:
- Never modify an existing
content.json. The skill may create a new content.json (path-level cover page) but must never modify one that already exists.
- Never modify
index.json. Read it as a data source; never write to it.
- Never modify recommender files. The
grafana-recommender repo is a read-only data source; never write to it.
- Verify after writing. Before any writes, snapshot every in-scope pre-existing
content.json as raw bytes (or SHA-256). After writes, confirm each snapshot is byte-identical.
Batch Mode
When this skill is invoked as a sub-agent by an orchestrator (i.e., the agent was not started interactively by a human), it operates in batch mode. In batch mode:
- Never block waiting for user input. Any situation that would normally require asking the user is resolved by writing a
TODO item in the migration notes instead.
- Mark the migration as incomplete when a TODO item is written. Include
status: incomplete in the migration-notes frontmatter and add a ## TODO section listing every unresolved item.
- Continue past blockers. Generate as much of the output as possible. A partially-completed manifest with TODO items is preferable to no output at all.
TODO item format (use consistently throughout migration notes):
- [ ] TODO(<category>): <what is missing> — <what the reviewer must do>
Categories: description, conflict, review, fallback.
Example: - [ ] TODO(description): step "configure-alloy" has no description source — reviewer must supply a one-line catalog description
Mode Detection
Determine the mode from the target directory:
| Condition | Mode |
|---|
Directory name ends with -lj OR a directory contains other nested directories with content.json | Mode 2: Learning path |
| Otherwise | Mode 1: Standalone guide |
Data Sources
index.json (read-only)
Location: index.json (repo root). index.json is at the repository root only; there is no per-guide index.json.
Each rule has: title, url, description, type, match. Match a rule to a guide by:
- Strip any trailing
/content.json from the rule's url
- Extract the last path segment (e.g.,
alerting-101)
- Compare against the guide's directory name or
content.json id
If no rule matches, the guide has no targeting — omit the targeting field.
Website learning path markdown (read-only, optional)
Location: <path_to_local_clone>/website/content/docs/learning-paths/
If the hardcoded path does not exist (e.g., on another machine or in CI), the agent may shallow-clone the grafana/website repo into a temporary directory and use that path instead; the structure under content/docs/learning-paths/ is the same as in a local checkout.
Map *-lj directory names to website paths by stripping the -lj suffix (e.g., prometheus-lj → prometheus). Step directory names are identical in both repos. The website markdown pathfinder_data frontmatter provides the authoritative mapping when present, but most steps lack it — fall back to directory name matching (see step 3 canonical mapping rules).
If the website repo is unavailable, apply fallback rules from docs/manifest-reference.md under "When website markdown is unavailable" and flag affected fields for manual review.
journeys.yaml (read-only, optional)
Location: <path_to_local_clone>/website/content/docs/learning-paths/journeys.yaml
Provides inter-journey category and relationship data.
Recommender rules (read-only)
Location: grafana-recommender/internal/configs/state_recommendations/*.json
This is the primary source of targeting/match rules for learning journeys. index.json contains only "type": "interactive" entries — it never has learning-journey routing rules. All learning-journey match rules live in the recommender repo.
Locating the recommender repo (in priority order):
- Check for a local checkout at common paths:
~/hax/grafana-recommender, ~/Documents/repositories/grafana-recommender, or a sibling directory to the interactive-tutorials repo
- If not found locally, shallow-clone
git@github.com:grafana/grafana-recommender.git to /tmp/grafana-recommender
- If a
/tmp/grafana-recommender clone already exists, run git -C /tmp/grafana-recommender pull --ff-only to ensure it has the latest rules
Freshness: Always ensure the recommender checkout is up-to-date before extracting rules. If using a /tmp clone, pull at the start of each migration run. Stale rules can lead to incorrect or missing targeting.
Matching rules to a learning path:
Each *.json file in state_recommendations/ has a rules array. Filter to entries where "type": "learning-journey". Match by extracting the slug from the url field:
- Parse the
url (e.g., https://grafana.com/docs/learning-journeys/prometheus/)
- Extract the path name — the segment after
/learning-journeys/ or /learning-paths/ (e.g., prometheus). The recommender uses both URL prefixes interchangeably; handle both.
- Compare against the directory name with
-lj stripped (e.g., prometheus-lj → prometheus)
Multiple rules per learning path: A single learning path may have multiple rules across different recommender files (different URL contexts, different tags, different platform targets). Collect all matching rules. When a learning path has multiple rules:
targeting.match: If all rules share the same match expression, use it directly. If there are multiple distinct match expressions, wrap them in a top-level {"or": [...]} to preserve all routing contexts. Record which recommender files contributed which rules in the migration notes.
startingLocation: Traverse all collected match expressions, collect all URL-bearing leaves (urlPrefix values and urlPrefixIn entries), pick the first.
testEnvironment: Apply the standard tier inference rules across all collected matches. If any match contains "targetPlatform": "cloud", the tier is "cloud". If rules span both cloud and oss, use "cloud" (the more common deployment) and note the dual-platform targeting in migration notes.
description: If a recommender rule has a non-empty description, it is a valid source for the manifest description (see Description Conventions).
If the recommender repo is unavailable (clone fails, no network, no SSH key), fall back to index.json-only behavior and flag the absence in migration notes.
Description Conventions
The description field is a compact, one-line summary suitable for a course catalog listing. It is not introductory prose — that belongs in the content.json markdown blocks.
Priority for resolving description:
index.json rule or recommender rule — if the guide has a matching rule in either source, use its description verbatim. These are already written in catalog style. For learning journeys, the recommender is the primary source since index.json does not contain learning-journey entries. Skip entries with empty description values.
- Summarize available sources — if no rule with a non-empty description exists, collect all available description sources (website markdown frontmatter
description, content.json title, path-level metadata) and boil them down into a single sentence. Write it in the style of the rule descriptions (e.g., "Hands-on guide: Learn how to...").
- No sources at all — if no sources exist at all, do not invent a description.
- Interactive mode: Stop and ask the user for a description.
- Batch mode: Write
- [ ] TODO(description): <guide-id> has no description source — reviewer must supply a one-line catalog description in the migration notes, leave the description field as "TODO: manual description required", and mark the migration incomplete. Continue generating all other fields.
Author Conventions
The author field has a team value that depends on content type:
| Content type | author.team |
|---|
Learning path (type: "path") | "Grafana Documentation" |
Step within a learning path (inside a *-lj directory) | "Grafana Documentation" |
Standalone guide (not inside a *-lj directory) | "interactive-learning" |
"interactive-learning" is the fallback default when content type is unknown. If you know the content is a learning path or a step within one, always use "Grafana Documentation".
The author.name field is optional. To derive it:
.github/CODEOWNERS (preferred when guide-specific): Read .github/CODEOWNERS from the interactive-tutorials repo root. If a directory-scoped rule applies to the package being migrated, use the listed GitHub handle(s) for author.name (strip the leading @; comma-separate multiple handles).
- Matching: Normalize the migrated directory to a repo-relative path (e.g. standalone
alerting-101, path prometheus-lj, step prometheus-lj/add-data-source). Find a line whose pattern is a path prefix for that directory, e.g. /alerting-101/ or /prometheus-lj/ (GitHub CODEOWNERS uses last matching rule wins). If a step directory has no own line, try the parent *-lj directory (e.g. /prometheus-lj/ for any step under it).
- Do not use generic review patterns as the author source:
*, **/content.json, **/manifest.json, **/assets/*, or other repo-wide globs — those list many reviewers and are not primary author attribution. If only those patterns match, skip to git history.
- Team owners (e.g.
@grafana/slo-squad) may appear; use the handle as name (without @) when that is the listed owner.
- Git history — if step 1 did not yield
author.name, look at all git revisions since the content.json file was created (use git log --follow to track renames)
- Prefer GitHub handles; extract commit author names/handles
- Exclude any obvious automation or bot authors (e.g.,
dependabot, renovate, github-actions, bot, etc.)
- If multiple authors remain, comma-separate them
- If only full names (not handles) appear in git history, use full names — some data is better than none. If no authors remain after filtering bots, or if unsure, omit
name entirely
Record in migration notes when author.name came from CODEOWNERS vs git history.
Mode 1: Standalone Guide
Invoked on a guide directory (e.g., alerting-101/).
Steps
1. Read content.json
Read {dir}/content.json and extract the id and title fields. Do not modify this file.
2. Look up matching index.json rule
Read index.json from the repo root. Find the rule whose url path segment matches the directory name or the content.json id. Record:
description from the rule
match object from the rule
startingLocation: traverse the match expression recursively, collect all URL-bearing leaves (urlPrefix values and entries from urlPrefixIn arrays), then pick the first one. If no URL can be extracted, omit startingLocation entirely — a missing value is preferable to a wrong one.
If no rule matches, record that targeting is absent.
3. Check for website markdown (if step is inside a *-lj)
If this guide directory is a direct child of a *-lj directory, look up the parent path's website markdown _index.md and extract journey.group for the category field. Otherwise, category defaults to "general".
4. Derive testEnvironment
testEnvironment must NEVER be omitted. Every manifest must include it.
Apply these rules in order:
IF match expression exists (and is not empty):
- IF
match contains source at any depth → evaluate the source value:
- If the source value is a regex matching all
*.grafana.net hosts (e.g., ".*\\.grafana\\.net") → { "tier": "cloud" } — this means "any Grafana Cloud instance", so no specific instance is set
- If the source value is a concrete hostname (e.g.,
"play.grafana.org") → { "tier": "cloud", "instance": "<source value>" }
- If the source value is any other regex or pattern →
{ "tier": "cloud" } and flag for manual review — do not copy a regex into instance
- ELSE IF
match contains "targetPlatform": "cloud" → { "tier": "cloud" }
- ELSE →
{ "tier": "local" }
ELSE (no match expression or match expression is empty):
- →
{ "tier": "cloud" } (minimum default)
Note: An empty match expression (match: {}) is treated the same as no match expression — both default to "cloud".
5. Generate manifest.json
Write {dir}/manifest.json with the derived fields:
{
"id": "<from content.json>",
"type": "guide",
"description": "<compact one-line summary; see Description Conventions>",
"category": "<from journey.group if inside *-lj, else 'general'>",
"author": { "team": "<see Author Conventions>" },
"startingLocation": "<first URL from match, if derivable>",
"targeting": {
"match": { "<copied verbatim from index.json rule>" }
},
"testEnvironment": {
"tier": "<local|cloud|play>",
"instance": "<if applicable>"
},
"depends": [],
"recommends": [],
"suggests": [],
"provides": []
}
Field omission rules:
- Omit
repository (schema default "interactive-tutorials" applies)
- Omit
language (schema default "en" applies)
- Include
author.name when derived per Author Conventions; omit otherwise
- Omit
targeting entirely if no targeting rule was found (index.json for standalone guides, recommender for learning journeys)
- Omit
startingLocation if no URL can be derived from the match expression — do not fall back to "/". A missing value is preferable to a wrong one.
- Never omit
testEnvironment. Minimum is { "tier": "cloud" }. Omit instance when no instance value is available.
- Always include
depends, recommends, suggests, and provides — even when empty ([]). This makes the fields visible to authors so they know they can fill them out later. Never invent values; use [] when no information is available.
6. Validate
- Confirm
id matches between content.json and manifest.json
- Confirm the generated JSON is syntactically valid
- Confirm no existing
content.json was modified by byte-level comparison against the pre-write snapshot (raw bytes or SHA-256)
7. Run package validation (required)
Run from the pathfinder-app checkout root, or use the full path to the CLI; pass the full path to the guide directory (e.g. .../interactive-tutorials/alerting-101) so it works from any cwd:
node dist/cli/cli/index.js validate --package <dir>
This validation attempt is required for Phase 1 migration. If the command cannot run (CLI missing/unbuilt), treat this as an incomplete migration and explicitly report the blocker. If the CLI warns that startingLocation defaulted to '/', that is expected when no index rule exists; the manifest correctly omitted it.
8. Write migration notes
Write {dir}/assets/migration-notes.md following the migration notes convention. Include:
- Which manifest was created and when
- Which fields were derived from which sources
- Result of
validate --package
- Any fields that need manual review (e.g., no index.json rule found, fallback used)
- Any dangling references
- Any surprises or unexpected situations
9. Report
Tell the user:
- Which manifest was created
- Which fields were derived from which sources
- Result of
validate --package
- Any fields that need manual review
- Summary of migration notes written
Mode 2: Learning Path
Invoked on a *-lj directory (e.g., prometheus-lj/).
Steps
1. Locate website markdown
Map the directory name to the website path by stripping -lj (e.g., prometheus-lj → prometheus). Check for <path_to_local_clone>/website/content/docs/learning-paths/<path-name>/.
If not found, apply fallback rules and flag for manual review.
2. Read path-level metadata
Read _index.md from the website path. Extract from frontmatter:
title — for path-level content.json and manifest
description — a source for the manifest description (apply Description Conventions: if there is a recommender or index.json rule for this path with a non-empty description, prefer it; otherwise condense the _index.md description into a compact one-line catalog summary)
journey.group — for category
journey.skill — note but defer (not in current schema)
journey.links.to — for recommends
related_journeys.items — for suggests (default) or depends (only if unambiguously prerequisite). Relationship strength heuristic: when the related_journeys.heading text says "before" or "prerequisite" but the body content qualifies the relationship (e.g., "while not required"), use suggests. The body-level qualification takes precedence over the heading-level framing. Only use depends when both the heading and the body unambiguously describe a hard prerequisite with no "optional" or "recommended" qualifier. Example: heading says "Before you begin" but body says "while not required" → use suggests.
Extract from body content:
- All prose, learning objectives, prerequisites — for path-level content.json blocks
3. Read step metadata
Build a canonical step map from website markdown. For each <website-step>/index.md in the website path, extract:
weight — for ordering within the milestones array
step — step number (redundant with weight ordering, use as cross-check)
pathfinder_data — authoritative mapping to the interactive-tutorials directory (e.g., prometheus-lj/add-data-source)
description — for step manifest
side_journeys — for step suggests (see step 3a below for URL-to-ID resolution)
Canonical mapping rules:
- When present, treat
pathfinder_data as authoritative for mapping website steps to interactive-tutorials step directories. Validate each target exists under the *-lj directory and has content.json.
- When
pathfinder_data is absent (common — most steps lack it), fall back to directory name matching: website step directory names are identical to interactive-tutorials step directory names within the same path. Confirm the match by verifying the directory exists and has content.json. Note which steps used name-matching fallback in the migration notes.
- Build the path manifest
milestones array from this map, ordered by weight.
- Do not derive step order from local directory listing.
3a. Resolve side_journeys URLs to package IDs
For each step's side_journeys.items, check whether any link matches the pattern /docs/learning-paths/<name>/. If so, resolve <name> to <name>-lj and check whether that directory exists in this repo. If the directory exists, add its ID to the step's suggests array. If the directory does not exist, the reference is still included (it may point to a not-yet-migrated path) — note it as a dangling reference in the migration notes.
Links that do not match the learning path URL pattern (external docs, YouTube URLs, etc.) are not mappable to package IDs and should be ignored.
4. Migrate each step (Mode 1)
For each mapped step in canonical weight order:
- Read the step's
content.json to get id and title (do not modify)
- Check if the step has its own
index.json entry (most steps don't — targeting lives at the path level)
- Resolve description following the Description Conventions:
- Matching step-level
index.json rule description (first priority — already catalog-style)
- Website step
index.md description — if multi-sentence or verbose, condense to one line
- Summarize from step
content.json title + any other available context into a single catalog-style sentence
- If no sources exist at all: do not guess.
- Interactive mode: Stop and request a manual description for that step.
- Batch mode: Write
- [ ] TODO(description): step "<step-id>" has no description source — reviewer must supply a one-line catalog description in the migration notes, set "description": "TODO: manual description required" in the step manifest, and mark the migration incomplete. Continue generating all remaining steps and the path manifest.
- Generate
{step-dir}/manifest.json:
{
"id": "<from step content.json>",
"type": "guide",
"description": "<compact one-line summary; see Description Conventions>",
"category": "<from parent path journey.group>",
"author": { "team": "Grafana Documentation" },
"testEnvironment": {
"tier": "<inherited from path, or 'cloud' minimum>"
},
"depends": ["<previous-step-id>"],
"recommends": ["<next-step-id>"],
"suggests": [],
"provides": []
}
Include author.name when derived per Author Conventions (CODEOWNERS often applies at the parent *-lj path for all steps); omit name when unknown.
Step dependency rules:
- Use each step's
content.json id (not the directory name) in depends/recommends and in the path milestones array.
- First step: omit
depends
- Step N+1:
depends on step N's id
- Last step: omit
recommends (no next step)
- Step N:
recommends step N+1's id
- If the step has
side_journeys, map them to suggests
Omit targeting unless the step has its own index.json entry. Steps within a learning path inherit targeting from the path level; step-level recommender rules are not expected and should not be searched for.
5. Check for metadata conflicts
Compare metadata across sources (website markdown frontmatter, recommender rules, index.json rule if present, journeys.yaml). A conflict exists when the same field has different string values in two sources.
Flag conflicts — do not silently pick one.
- Interactive mode: Present both values and ask the user which to use.
- Batch mode: Pick the higher-priority source (recommender >
_index.md > index.json > journeys.yaml), write - [ ] TODO(conflict): field "<field>" has conflicting values — "<source-A-value>" (from <source-A>) vs "<source-B-value>" (from <source-B>); used <source-A-value> in the migration notes, and mark the migration incomplete.
5a. Cross-validate journey.links.to against journeys.yaml
Read journeys.yaml and find the entry whose id maps to the current learning path (e.g., prom-data-source for prometheus-lj). Compare the links.to values from journeys.yaml against the journey.links.to values from the _index.md frontmatter. If the IDs differ (e.g., metrics-drilldown in journeys.yaml vs drilldown-metrics in _index.md), flag the mismatch as a data quality issue in the migration notes. Use the _index.md value as authoritative (it maps to actual directory names in this repo) but record both values so the website team can reconcile the inconsistency.
5b. Duplicate description sanity check
After resolving descriptions for all steps, compare them pairwise. If two or more sibling steps within the same path have identical description values, use the identical string for every step that has the same source description. Do not invent a variant. Record the duplicate in the migration notes and recommend an upstream fix.
6. Look up targeting rules
Check both data sources for targeting rules, in this order:
-
Recommender rules (primary for learning journeys): Scan all *.json files in the recommender's state_recommendations/ directory for entries with "type": "learning-journey" whose URL slug matches this path (see Data Sources > Recommender rules for matching logic). Collect all matching rules across all files.
-
index.json (fallback): Check if the *-lj directory name has an entry in index.json. In practice index.json does not contain learning-journey entries, but check anyway for future-proofing.
Use the collected rules to derive targeting, startingLocation, and testEnvironment using the standard derivation rules (see Recommender rules data source for multi-rule handling). If rules were found in the recommender, record the source file(s) and the match expressions in the migration notes.
If no rules are found in either source, the path has no targeting — omit the targeting field.
7. Generate path-level manifest.json
Write {lj-dir}/manifest.json:
{
"id": "<lj-directory-name>",
"type": "path",
"description": "<compact one-line summary; see Description Conventions>",
"category": "<from journey.group>",
"author": { "team": "Grafana Documentation" },
"startingLocation": "<from targeting rules, if derivable>",
"targeting": {
"match": { "<from recommender/index.json rules; wrap in 'or' if multiple>" }
},
"testEnvironment": {
"tier": "<from targeting rules, or 'cloud' minimum>"
},
"milestones": [
"<step-1-id>",
"<step-2-id>",
"..."
],
"depends": [],
"recommends": ["<from journey.links.to>"],
"suggests": ["<from related_journeys>"],
"provides": []
}
Include author.name on the path manifest when derived per Author Conventions; omit when unknown.
Omit targeting if no targeting rule exists for the path (neither recommender nor index.json).
Omit startingLocation if no URL can be derived — do not fall back to "/".
Never omit testEnvironment. Minimum is { "tier": "cloud" }.
Always include depends, recommends, suggests, and provides — use [] when no data is available.
8. Create path-level content.json
Only if {lj-dir}/content.json does not already exist. If it exists, do not touch it.
Derive from _index.md body content:
{
"id": "<lj-directory-name>",
"title": "<from _index.md title>",
"blocks": [
{
"type": "markdown",
"content": "<body content with Hugo shortcodes stripped>"
}
]
}
Content transformation rules:
- Strip Hugo shortcode tags (
{{< ... >}}, {{< /... >}})
- For wrapping shortcodes (e.g.,
{{< admonition >}}...{{< /admonition >}}), strip tags but preserve inner content
- For non-wrapping shortcodes with a
heading attribute (e.g., {{< docs/icon-heading heading="## Here's what to expect" >}}), extract and preserve the heading value as a markdown header in the output
- Convert remaining markdown into one or more
markdown blocks
- Preserve learning objectives, prerequisites, and descriptive prose
- Remove image links that use website-relative paths — markdown images like
 reference paths that only resolve on the Grafana website and will not function in Pathfinder. Strip these image references entirely (including their alt text and surrounding syntax). Retain any surrounding prose but clean up orphaned whitespace or empty paragraphs left behind.
- Remove "Grafana Cloud account" prerequisites — any prerequisite or requirement bullet point that says the user needs a Grafana Cloud account (e.g., "A Grafana Cloud account. To create an account, refer to...") is redundant for Pathfinder users, who are already in Grafana. Remove these bullet points entirely.
- Record all removed content in migration notes — for every image link or prerequisite removed by the above rules, record the exact text that was removed in the migration notes under a
## Content Removed During Migration section. This provides a clear audit trail of what was stripped from the original website content.
- Do NOT add a markdown title (
## Title) — the title field handles that
9. Validate
- Confirm
id consistency: path manifest id matches the directory name, step manifest id matches step content.json id
- Confirm
milestones array in path manifest references valid step IDs that exist in step content.json files
- Confirm step ordering matches website
weight ordering
- Confirm no pre-existing
content.json in scope was modified by byte-level comparison against pre-write snapshots (including existing path-level content.json, if present)
- Confirm all generated JSON is syntactically valid
10. Run package validation (required)
Run from the pathfinder-app checkout root, or use the full path to the CLI; pass the full path to the guide directory (e.g. .../interactive-tutorials/prometheus-lj) so it works from any cwd:
node dist/cli/cli/index.js validate --package <lj-dir>
If you created step-level manifests, also run validate --package <step-dir> for each created/updated step package.
This validation attempt is required for Phase 1 migration. If the command cannot run (CLI missing/unbuilt), treat this as an incomplete migration and explicitly report the blocker. If the CLI warns that startingLocation defaulted to '/', that is expected when no index rule exists; the manifest correctly omitted it.
11. Write migration notes
Write {lj-dir}/assets/migration-notes.md following the migration notes convention. Include:
- Path-level manifest and content.json created
- N step manifests created (list them)
- Fields derived from each source
- Results of all
validate --package commands
- Recommender rules found (list source files, URLs, and match expressions) or note that none were found / repo was unavailable
- Any metadata conflicts flagged (including journeys.yaml cross-validation mismatches, recommender vs other source conflicts)
- Any duplicate descriptions detected
- Any dangling references in
recommends, suggests, or depends (these are expected and preferred — the CLI catches them)
- Any side_journeys URLs resolved (or not resolved) to package IDs
- Any fields that need manual review
- Any fallbacks used due to missing website markdown or unavailable recommender repo
- Any surprises or unexpected situations
12. Report
Tell the user:
- Path-level manifest and content.json created
- N step manifests created (list them)
- Fields derived from each source (including which recommender files contributed targeting rules)
- Results of all
validate --package commands
- Any conflicts flagged
- Any fields that need manual review
- Any fallbacks used due to missing website markdown or unavailable recommender repo
- Summary of migration notes written
Reference-First Derivation
For all field derivation logic and fallback rules, use docs/manifest-reference.md as the authoritative source:
startingLocation extraction (traverse recursively, collect all URL-bearing leaves, pick the first)
testEnvironment tier inference (IF/ELSE logic: source → cloud, targetPlatform: cloud → cloud, else → local; no match/empty match → cloud)
- website-markdown fallback behavior
Only include migration-specific orchestration logic in this skill. If this skill and docs/manifest-reference.md disagree, follow docs/manifest-reference.md and report the mismatch.
Post-Migration Validation
After generating all files, run this checklist:
Error Handling
No targeting rule found
For standalone guides, this means no index.json rule matched. For learning journeys, this means no recommender rule matched and no index.json rule matched. Generate the manifest without targeting. Omit startingLocation (do not default to "/"). testEnvironment defaults to { "tier": "cloud" } (the minimum acceptable value — this applies when no match expression exists or when the match expression is empty). Flag for user review — the guide may be path-only (reachable via learning path, not contextual recommendation). If the CLI warns that startingLocation defaulted to '/', that is expected; the manifest correctly omitted it.
Recommender repo unavailable
If the recommender repo cannot be found locally and the shallow clone fails (e.g., no network, no SSH key), fall back to index.json-only behavior. Flag all LJ targeting fields as "recommender unavailable — needs manual review" in the migration notes. This is a degraded but functional migration; for learning journeys the result will almost certainly lack targeting since index.json does not contain learning-journey entries.
Website markdown not found
Apply fallback rules. Clearly state which fields used fallback values and need manual review.
Missing required step description during LJ fallback
Follow the Description Conventions priority:
- Step-level
index.json rule description (first priority — already catalog-style)
- Website step
index.md description — condense to one line if verbose
- Summarize from step
content.json title and any available context into a single catalog-style sentence
- If no sources exist at all: apply the batch-mode rule from ## Batch Mode — in interactive mode, stop and ask; in batch mode, write a
TODO(description) item and continue. Do not invent a description.
content.json missing in a step directory
This is unexpected. Report the missing file and skip that step. Do not create a content.json for a step — that is the content author's responsibility, not the migration skill's.
Metadata conflict between sources
Do not guess. Apply the batch-mode rule from ## Batch Mode:
- Interactive mode: Present both values, state the source of each, and ask the user to choose.
- Batch mode: Pick the higher-priority source, record the conflict as a
TODO(conflict) item in the migration notes, and mark the migration incomplete.
Migration Notes
Every migration produces a leave-behind document recording findings, decisions, surprises, and TODO items specific to that guide or path. This follows the assets/ directory convention from .cursor/skills/skill-memory.md.
Location
- Standalone guide:
{dir}/assets/migration-notes.md
- Learning path:
{lj-dir}/assets/migration-notes.md (one file for the entire path, covering path-level and all steps)
Format
---
disclaimer: Auto-generated by migrate-guide. Do not edit manually.
notice: To regenerate, re-run the migration skill on this directory.
migrated_at: "<ISO 8601 timestamp>"
status: complete # set to "incomplete" when any TODO items are present
---
# Migration Notes: <directory-name>
## Files Created
- `manifest.json` — <brief description of what was generated>
- (for paths) `content.json` — path-level cover page
- (for paths) `<step>/manifest.json` — one per step
## Field Derivation Summary
| Field | Source | Value |
|-------|--------|-------|
| ... | ... | ... |
## Flags for Manual Review
- <any fields that used fallback values>
- <any missing descriptions that were requested from the user>
## Content Removed During Migration
- <list each piece of content removed from path-level content.json, with the removal reason>
- Example: `Removed image: ` — website-relative image path
- Example: `Removed prerequisite: "A Grafana Cloud account. To create an account, refer to [Grafana Cloud](https://grafana.com/signup/cloud/connect-account)."` — redundant for Pathfinder users
## Dangling References
- <any suggests/recommends/depends IDs that point to non-existent directories>
- (Dangling references are expected and preferred — the Pathfinder CLI catches them during validation)
## Recommender Rules
| Source File | Rule URL | Match Expression |
|-------------|----------|------------------|
| <recommender filename> | <rule url> | <match JSON summary> |
(If no recommender rules were found, note "No recommender rules found for this path" and explain whether this is expected or needs investigation. If the recommender repo was unavailable, note that here.)
## Data Quality Issues
- <any journeys.yaml vs _index.md mismatches>
- <any duplicate step descriptions>
- <any side_journeys URLs that could not be resolved>
- If any step lacked `pathfinder_data` and was mapped by directory name only, list those steps here.
## Surprises / Notes
- <anything unexpected encountered during migration>
## TODO
- [ ] <actionable items for follow-up>
Omit any section that has no entries (e.g., if there are no dangling references, omit that section entirely). The goal is a concise, scannable document — not a verbose log.
Path migration (Mode 2) produces significantly more complex notes than standalone guides (Mode 1) because of the variety of special circumstances that can arise: metadata conflicts across sources, step ordering nuances, shortcode stripping edge cases, relationship mapping ambiguities, and cross-repo data inconsistencies. The migration notes capture these per-path specifics so they are not lost.
Example Invocations
Standalone guide
"Migrate alerting-101/ to the package format"
The skill reads alerting-101/content.json (id: alerting-101), finds the matching index.json rule, and generates alerting-101/manifest.json.
Learning path
"Migrate prometheus-lj/ to the package format"
The skill reads all 9 step content.json files, the website markdown at learning-paths/prometheus/, and searches both index.json and the recommender state_recommendations/ files for targeting rules. It finds the recommender rule in connections-cloud.json matching learning-journeys/prometheus/ and uses its match expression for targeting. It generates:
prometheus-lj/manifest.json (type: path, 9 milestones, targeting from recommender)
prometheus-lj/content.json (path-level cover page from website markdown)
- 9 step-level
manifest.json files