- 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:
1. **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.
2. **Never modify `index.json`.** Read it as a data source; never write to it.
3. **Never modify recommender files.** The `grafana-recommender` repo is a read-only data source; never write to it.
4. **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:
1. Strip any trailing `/content.json` from the rule's `url`
2. Extract the last path segment (e.g., `alerting-101`)
3. 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):**
1. Check for a local checkout at common paths: `~/hax/grafana-recommender`, `~/Documents/repositories/grafana-recommender`, or a sibling directory to the interactive-tutorials repo
2. If not found locally, shallow-clone `git@github.com:grafana/grafana-recommender.git` to `/tmp/grafana-recommender`
3. 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:
1. Parse the `url` (e.g., `https://grafana.com/docs/learning-journeys/prometheus/`)
2. Extract the path name — the segment after `/learning-journeys/` or `/learning-paths/` (e.g., `prometheus`). The recommender uses both URL prefixes interchangeably; handle both.
3. 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](#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`:**
1. **`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.
2. **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...").
3. **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:
1. **`.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.
2. **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)
3. Prefer GitHub handles; extract commit author names/handles
4. Exclude any obvious automation or bot authors (e.g., `dependabot`, `renovate`, `github-actions`, `bot`, etc.)
5. If multiple authors remain, comma-separate them
6. 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:
```json
{
"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](#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
عرض على GitHub