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 newcontent.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-recommenderrepo is a read-only data source; never write to it. - Verify after writing. Before any writes, snapshot every in-scope pre-existing
content.jsonas 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
TODOitem in the migration notes instead. - Mark the migration as incomplete when a TODO item is written. Include
status: incompletein the migration-notes frontmatter and add a## TODOsection 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.jsonfrom the rule'surl - Extract the last path segment (e.g.,
alerting-101) - Compare against the guide's directory name or
content.jsonid
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.gitto/tmp/grafana-recommender - If a
/tmp/grafana-recommenderclone already exists, rungit -C /tmp/grafana-recommender pull --ff-onlyto 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
-ljstripped (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 (urlPrefixvalues andurlPrefixInentries), 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-emptydescription, 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.jsonrule or recommender rule — if the guide has a matching rule in either source, use itsdescriptionverbatim. These are already written in catalog style. For learning journeys, the recommender is the primary source sinceindex.jsondoes not contain learning-journey entries. Skip entries with emptydescriptionvalues.- Summarize available sources — if no rule with a non-empty description exists, collect all available description sources (website markdown frontmatter
description,content.jsontitle, 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 descriptionin the migration notes, leave thedescriptionfield 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/CODEOWNERSfrom the interactive-tutorials repo root. If a directory-scoped rule applies to the package being migrated, use the listed GitHub handle(s) forauthor.name(strip the leading@; comma-separate multiple handles).- Matching: Normalize the migrated directory to a repo-relative path (e.g. standalone
alerting-101, pathprometheus-lj, stepprometheus-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*-ljdirectory (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 asname(without@) when that is the listed owner.
- Matching: Normalize the migrated directory to a repo-relative path (e.g. standalone
- Git history — if step 1 did not yield
author.name, look at all git revisions since thecontent.jsonfile was created (usegit log --followto 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
nameentirely
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:
descriptionfrom the rulematchobject from the rulestartingLocation: traverse thematchexpression recursively, collect all URL-bearing leaves (urlPrefixvalues and entries fromurlPrefixInarrays), then pick the first one. If no URL can be extracted, omitstartingLocationentirely — 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
matchcontainssourceat any depth → evaluate the source value:- If the source value is a regex matching all
*.grafana.nethosts (e.g.,".*\\.grafana\\.net") →{ "tier": "cloud" }— this means "any Grafana Cloud instance", so no specificinstanceis 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 intoinstance
- If the source value is a regex matching all
- ELSE IF
matchcontains"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.namewhen derived per Author Conventions; omit otherwise - Omit
targetingentirely if no targeting rule was found (index.json for standalone guides, recommender for learning journeys) - Omit
startingLocationif 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" }. Omitinstancewhen no instance value is available. - Always include
depends,recommends,suggests, andprovides— 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
idmatches betweencontent.jsonandmanifest.json - Confirm the generated JSON is syntactically valid
- Confirm no existing
content.jsonwas 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 manifestdescription— a source for the manifest description (apply Description Conventions: if there is a recommender orindex.jsonrule for this path with a non-empty description, prefer it; otherwise condense the_index.mddescription into a compact one-line catalog summary)journey.group— forcategoryjourney.skill— note but defer (not in current schema)journey.links.to— forrecommendsrelated_journeys.items— forsuggests(default) ordepends(only if unambiguously prerequisite). Relationship strength heuristic: when therelated_journeys.headingtext says "before" or "prerequisite" but the body content qualifies the relationship (e.g., "while not required"), usesuggests. The body-level qualification takes precedence over the heading-level framing. Only usedependswhen 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" → usesuggests.
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 themilestonesarraystep— 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 manifestside_journeys— for stepsuggests(see step 3a below for URL-to-ID resolution)
Canonical mapping rules:
- When present, treat
pathfinder_dataas authoritative for mapping website steps to interactive-tutorials step directories. Validate each target exists under the*-ljdirectory and hascontent.json. - When
pathfinder_datais 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 hascontent.json. Note which steps used name-matching fallback in the migration notes. - Build the path manifest
milestonesarray from this map, ordered byweight. - 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.jsonto getidandtitle(do not modify) - Check if the step has its own
index.jsonentry (most steps don't — targeting lives at the path level) - Resolve description following the Description Conventions:
- Matching step-level
index.jsonruledescription(first priority — already catalog-style) - Website step
index.mddescription— if multi-sentence or verbose, condense to one line - Summarize from step
content.jsontitle + 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 descriptionin 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.
- Matching step-level
- 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.jsonid(not the directory name) independs/recommendsand in the pathmilestonesarray. - First step: omit
depends - Step N+1:
dependson step N'sid - Last step: omit
recommends(no next step) - Step N:
recommendsstep N+1'sid - If the step has
side_journeys, map them tosuggests
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
*.jsonfiles in the recommender'sstate_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
*-ljdirectory name has an entry inindex.json. In practiceindex.jsondoes 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
headingattribute (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
markdownblocks - 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 Migrationsection. This provides a clear audit trail of what was stripped from the original website content. - Do NOT add a markdown title (
## Title) — thetitlefield handles that
9. Validate
- Confirm
idconsistency: path manifestidmatches the directory name, step manifestidmatches stepcontent.jsonid - Confirm
milestonesarray in path manifest references valid step IDs that exist in step content.json files - Confirm step ordering matches website
weightordering - Confirm no pre-existing
content.jsonin scope was modified by byte-level comparison against pre-write snapshots (including existing path-levelcontent.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 --packagecommands - 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, ordepends(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 --packagecommands - 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:
startingLocationextraction (traverse recursively, collect all URL-bearing leaves, pick the first)testEnvironmenttier 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:
- Every
manifest.jsonhas a matchingcontent.jsonin the same directory -
idmatches between eachmanifest.jsonandcontent.jsonpair - No pre-existing
content.jsonwas modified (byte-level check against pre-write snapshots) -
index.jsonwas not modified - Recommender repo files were not modified
- Path manifests have
type: "path"and amilestonesarray - Step manifests have
type: "guide" - Step
depends/recommendschains are consistent (no broken references within the current migration scope) - Dangling references (IDs in
depends/recommends/suggeststhat point to directories not yet in the repo) are acceptable and preferred — always include them when the underlying data supports the reference. The Pathfinder CLI (validate --package) knows how to detect and report dangling references, so they will be caught during validation. It is better to produce more dangling references (which the CLI catches) than to silently drop relationships that exist in the source data. Record each dangling reference in the migration notes. - JSON is syntactically valid in all generated files
-
node dist/cli/cli/index.js validate --package <dir>was run for each generated package (or a blocker was explicitly reported)
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.jsonruledescription(first priority — already catalog-style) - Website step
index.mddescription— condense to one line if verbose - Summarize from step
content.jsontitle 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.jsonfiles