mirror of
https://github.com/github/awesome-copilot.git
synced 2026-04-13 11:45:56 +00:00
feat: add code-tour skill — AI-generated CodeTour walkthroughs (#1277)
* feat: add code-tour skill for AI-generated CodeTour walkthroughs * fix: trim SKILL.md from 645 to 432 lines (under 500 limit) Reduce persona table to top 10, condense verbose examples and notes, trim redundant anti-patterns, compress step type docs and PR recipe. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: run npm run build to update README with code-tour skill Addresses review feedback from @aaronpowell * fix: add missing scripts/ and references/ files referenced in SKILL.md Addresses reviewer feedback — SKILL.md referenced bundled files (validate_tour.py, generate_from_docs.py, codetour-schema.json, examples.md) that were not included in the PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: run npm run build to update skills README with new assets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
433
skills/code-tour/SKILL.md
Normal file
433
skills/code-tour/SKILL.md
Normal file
@@ -0,0 +1,433 @@
|
||||
---
|
||||
name: code-tour
|
||||
description: >
|
||||
Use this skill to create CodeTour .tour files — persona-targeted, step-by-step walkthroughs
|
||||
that link to real files and line numbers. Trigger for: "create a tour", "make a code tour",
|
||||
"generate a tour", "onboarding tour", "tour for this PR", "tour for this bug", "RCA tour",
|
||||
"architecture tour", "explain how X works", "vibe check", "PR review tour",
|
||||
"contributor guide", "help someone ramp up", or any request for a structured walkthrough
|
||||
through code. Supports 20 developer personas (new joiner, bug fixer, architect, PR reviewer,
|
||||
vibecoder, security reviewer, and more), all CodeTour step types (file/line, selection,
|
||||
pattern, uri, commands, view), and tour-level fields (ref, isPrimary, nextTour).
|
||||
Works with any repository in any language.
|
||||
---
|
||||
|
||||
# Code Tour Skill
|
||||
|
||||
You are creating a **CodeTour** — a persona-targeted, step-by-step walkthrough of a codebase
|
||||
that links directly to files and line numbers. CodeTour files live in `.tours/` and work with
|
||||
the [VS Code CodeTour extension](https://github.com/microsoft/codetour).
|
||||
|
||||
Two scripts are bundled in `scripts/`:
|
||||
|
||||
- **`scripts/validate_tour.py`** — run after writing any tour. Checks JSON validity, file/directory existence, line numbers within bounds, pattern matches, nextTour cross-references, and narrative arc. Run it: `python ~/.agents/skills/code-tour/scripts/validate_tour.py .tours/<name>.tour --repo-root .`
|
||||
- **`scripts/generate_from_docs.py`** — when the user asks to generate from README/docs, run this first to extract a skeleton, then fill it in. Run it: `python ~/.agents/skills/code-tour/scripts/generate_from_docs.py --persona new-joiner --output .tours/skeleton.tour`
|
||||
|
||||
Two reference files are bundled:
|
||||
|
||||
- **`references/codetour-schema.json`** — the authoritative JSON schema. Read it to verify any field name or type. Every field you use must conform to it.
|
||||
- **`references/examples.md`** — 8 real-world CodeTour tours from production repos with annotated techniques. Read it when you want to see how a specific feature (`commands`, `selection`, `view`, `pattern`, `isPrimary`, multi-tour series) is used in practice.
|
||||
|
||||
### Real-world `.tour` files on GitHub
|
||||
|
||||
These are confirmed production `.tour` files. Fetch one when you need a working example of a specific step type, tour-level field, or narrative structure — don't write from memory when the real thing is one fetch away.
|
||||
|
||||
Find more with the GitHub code search: https://github.com/search?q=path%3A**%2F*.tour+&type=code
|
||||
|
||||
#### By step type / technique demonstrated
|
||||
|
||||
| What to study | File URL |
|
||||
|---|---|
|
||||
| `directory` + `file+line` (contributor onboarding) | https://github.com/coder/code-server/blob/main/.tours/contributing.tour |
|
||||
| `selection` + `file+line` + intro content step (accessibility project) | https://github.com/a11yproject/a11yproject.com/blob/main/.tours/code-tour.tour |
|
||||
| Minimal tutorial — tight `file+line` narration for interactive learning | https://github.com/lostintangent/rock-paper-scissors/blob/master/main.tour |
|
||||
| Multi-tour repo with `nextTour` chaining (cloud native OCI walkthroughs) | https://github.com/lucasjellema/cloudnative-on-oci-2021/blob/main/.tours/introduction.tour |
|
||||
| `isPrimary: true` (marks the onboarding entry point) | https://github.com/nickvdyck/webbundlr/blob/main/.tours/getting-started.tour |
|
||||
| `pattern` instead of `line` (regex-anchored steps) | https://github.com/nickvdyck/webbundlr/blob/main/.tours/architecture.tour |
|
||||
|
||||
**Raw content tip:** Prefix `raw.githubusercontent.com` and drop `/blob/` for raw JSON access.
|
||||
|
||||
A great tour is not just annotated files. It is a **narrative** — a story told to a specific
|
||||
person about what matters, why it matters, and what to do next. Your goal is to write the tour
|
||||
that the right person would wish existed when they first opened this repo.
|
||||
|
||||
**CRITICAL: Only create `.tour` JSON files. Never create, modify, or scaffold any other files.**
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Discover the repo
|
||||
|
||||
Before asking the user anything, explore the codebase:
|
||||
|
||||
- List the root directory, read the README, and check key config files
|
||||
(package.json, pyproject.toml, go.mod, Cargo.toml, composer.json, etc.)
|
||||
- Identify the language(s), framework(s), and what the project does
|
||||
- Map the folder structure 1–2 levels deep
|
||||
- Find entry points: main files, index files, app bootstrapping
|
||||
- **Note which files actually exist** — every path you write in the tour must be real
|
||||
|
||||
If the repo is sparse or empty, say so and work with what exists.
|
||||
|
||||
**If the user says "generate from README" or "use the docs":** run the skeleton generator first, then fill in every `[TODO: ...]` by reading the actual files:
|
||||
|
||||
```bash
|
||||
python skills/code-tour/scripts/generate_from_docs.py \
|
||||
--persona new-joiner \
|
||||
--output .tours/skeleton.tour
|
||||
```
|
||||
|
||||
### Entry points by language/framework
|
||||
|
||||
Don't read everything — start here, then follow imports.
|
||||
|
||||
| Stack | Entry points to read first |
|
||||
|-------|---------------------------|
|
||||
| **Node.js / TS** | `index.js/ts`, `server.js`, `app.js`, `src/main.ts`, `package.json` (scripts) |
|
||||
| **Python** | `main.py`, `app.py`, `__main__.py`, `manage.py` (Django), `app/__init__.py` (Flask/FastAPI) |
|
||||
| **Go** | `main.go`, `cmd/<name>/main.go`, `internal/` |
|
||||
| **Rust** | `src/main.rs`, `src/lib.rs`, `Cargo.toml` |
|
||||
| **Java / Kotlin** | `*Application.java`, `src/main/java/.../Main.java`, `build.gradle` |
|
||||
| **Ruby** | `config/application.rb`, `config/routes.rb`, `app/controllers/application_controller.rb` |
|
||||
| **PHP** | `index.php`, `public/index.php`, `bootstrap/app.php` (Laravel) |
|
||||
|
||||
### Repo type variants — adjust focus accordingly
|
||||
|
||||
The same persona asks for different things depending on what kind of repo this is:
|
||||
|
||||
| Repo type | What to emphasize | Typical anchor files |
|
||||
|-----------|-------------------|----------------------|
|
||||
| **Service / API** | Request lifecycle, auth, error contracts | router, middleware, handler, schema |
|
||||
| **Library / SDK** | Public API surface, extension points, versioning | index/exports, types, changelog |
|
||||
| **CLI tool** | Command parsing, config loading, output formatting | main, commands/, config |
|
||||
| **Monorepo** | Package boundaries, shared contracts, build graph | root package.json/pnpm-workspace, shared/, packages/ |
|
||||
| **Framework** | Plugin system, lifecycle hooks, escape hatches | core/, plugins/, lifecycle |
|
||||
| **Data pipeline** | Source → transform → sink, schema ownership | ingest/, transform/, schema/, dbt models |
|
||||
| **Frontend app** | Component hierarchy, state management, routing | pages/, store/, router, api/ |
|
||||
|
||||
For **monorepos**: identify the 2–3 packages most relevant to the persona's goal. Don't try to tour everything — open the tour with a step that explains how to navigate the workspace, then stay focused.
|
||||
|
||||
### Large repo strategy
|
||||
|
||||
For repos with 100+ files: don't try to read everything.
|
||||
|
||||
1. Read entry points and the README first
|
||||
2. Build a mental model of the top 5–7 modules
|
||||
3. For the requested persona, identify the **2–3 modules that matter most** and read those deeply
|
||||
4. For modules you're not covering, mention them in the intro step as "out of scope for this tour"
|
||||
5. Use `directory` steps for areas you mapped but didn't read — they orient without requiring full knowledge
|
||||
|
||||
A focused 10-step tour of the right files beats a scattered 25-step tour of everything.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Read the intent — infer everything you can, ask only what you can't
|
||||
|
||||
**One message from the user should be enough.** Read their request and infer persona,
|
||||
depth, and focus before asking anything.
|
||||
|
||||
### Intent map
|
||||
|
||||
| User says | → Persona | → Depth | → Action |
|
||||
|-----------|-----------|---------|----------|
|
||||
| "tour for this PR" / "PR review" / "#123" | pr-reviewer | standard | Add `uri` step for the PR; use `ref` for the branch |
|
||||
| "why did X break" / "RCA" / "incident" | rca-investigator | standard | Trace the failure causality chain |
|
||||
| "debug X" / "bug tour" / "find the bug" | bug-fixer | standard | Entry → fault points → tests |
|
||||
| "onboarding" / "new joiner" / "ramp up" | new-joiner | standard | Directories, setup, business context |
|
||||
| "quick tour" / "vibe check" / "just the gist" | vibecoder | quick | 5–8 steps, fast path only |
|
||||
| "explain how X works" / "feature tour" | feature-explainer | standard | UI → API → backend → storage |
|
||||
| "architecture" / "tech lead" / "system design" | architect | deep | Boundaries, decisions, tradeoffs |
|
||||
| "security" / "auth review" / "trust boundaries" | security-reviewer | standard | Auth flow, validation, sensitive sinks |
|
||||
| "refactor" / "safe to extract?" | refactorer | standard | Seams, hidden deps, extraction order |
|
||||
| "performance" / "bottlenecks" / "slow path" | performance-optimizer | standard | Hot path, N+1, I/O, caches |
|
||||
| "contributor" / "open source onboarding" | external-contributor | quick | Safe areas, conventions, landmines |
|
||||
| "concept" / "explain pattern X" | concept-learner | standard | Concept → implementation → rationale |
|
||||
| "test coverage" / "where to add tests" | test-writer | standard | Contracts, seams, coverage gaps |
|
||||
| "how do I call the API" | api-consumer | standard | Public surface, auth, error semantics |
|
||||
|
||||
**Infer silently:** persona, depth, focus area, whether to add `uri`/`ref`, `isPrimary`.
|
||||
|
||||
**Ask only if you genuinely can't infer:**
|
||||
- "bug tour" but no bug described → ask for the bug description
|
||||
- "feature tour" but no feature named → ask which feature
|
||||
- "specific files" explicitly requested → honor them as required stops
|
||||
|
||||
Never ask about `nextTour`, `commands`, `when`, or `stepMarker` unless the user mentioned them.
|
||||
|
||||
### PR tour recipe
|
||||
|
||||
For PR tours: set `"ref"` to the branch, open with a `uri` step for the PR, cover changed files first, then unchanged-but-critical files, close with a reviewer checklist.
|
||||
|
||||
### User-provided customization — always honor these
|
||||
|
||||
| User says | What to do |
|
||||
|-----------|-----------|
|
||||
| "cover `src/auth.ts` and `config/db.yml`" | Those files are required stops |
|
||||
| "pin to the `v2.3.0` tag" / "this commit: abc123" | Set `"ref": "v2.3.0"` |
|
||||
| "link to PR #456" / pastes a URL | Add a `uri` step at the right narrative moment |
|
||||
| "lead into the security tour when done" | Set `"nextTour": "Security Review"` |
|
||||
| "make this the main onboarding tour" | Set `"isPrimary": true` |
|
||||
| "open a terminal at this step" | Add `"commands": ["workbench.action.terminal.focus"]` |
|
||||
| "deep" / "thorough" / "5 steps" / "quick" | Override depth accordingly |
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Read the actual files — no exceptions
|
||||
|
||||
**Every file path and line number in the tour must be verified by reading the file.**
|
||||
A tour pointing to the wrong file or a non-existent line is worse than no tour.
|
||||
|
||||
For every planned step:
|
||||
1. Read the file
|
||||
2. Find the exact line of the code you want to highlight
|
||||
3. Understand it well enough to explain it to the target persona
|
||||
|
||||
If a user-requested file doesn't exist, say so — don't silently substitute another.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Write the tour
|
||||
|
||||
Save to `.tours/<persona>-<focus>.tour`. Read `references/codetour-schema.json` for the
|
||||
authoritative field list. Every field you use must appear in that schema.
|
||||
|
||||
### Tour root
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://aka.ms/codetour-schema",
|
||||
"title": "Descriptive Title — Persona / Goal",
|
||||
"description": "One sentence: who this is for and what they'll understand after.",
|
||||
"ref": "main",
|
||||
"isPrimary": false,
|
||||
"nextTour": "Title of follow-up tour",
|
||||
"steps": []
|
||||
}
|
||||
```
|
||||
|
||||
Omit any field that doesn't apply to this tour.
|
||||
|
||||
**`when`** — conditional display. A JavaScript expression evaluated at runtime. Only show this tour
|
||||
if the condition is true. Useful for persona-specific auto-launching, or hiding advanced tours
|
||||
until a simpler one is complete.
|
||||
```json
|
||||
{ "when": "workspaceFolders[0].name === 'api'" }
|
||||
```
|
||||
|
||||
**`stepMarker`** — embed step anchors directly in source code comments. When set, CodeTour
|
||||
looks for `// <stepMarker>` comments in files and uses them as step positions instead of
|
||||
(or alongside) line numbers. Useful for tours on actively changing code where line numbers
|
||||
shift constantly. Example: set `"stepMarker": "CT"` and put `// CT` in the source file.
|
||||
Don't suggest this unless the user asks — it requires editing source files, which is unusual.
|
||||
|
||||
---
|
||||
|
||||
### Step types — full reference
|
||||
|
||||
All step types: **content** (intro/closing, max 2), **directory**, **file+line** (workhorse), **selection** (code block), **pattern** (regex match), **uri** (external link), **view** (focus VS Code panel), **commands** (run VS Code commands).
|
||||
|
||||
> **Path rule:** `"file"` and `"directory"` must be relative to repo root. No absolute paths, no leading `./`.
|
||||
|
||||
---
|
||||
|
||||
### When to use each step type
|
||||
|
||||
| Situation | Step type |
|
||||
|-----------|-----------|
|
||||
| Tour intro or closing | content |
|
||||
| "Here's what lives in this folder" | directory |
|
||||
| One line tells the whole story | file + line |
|
||||
| A function/class body is the point | selection |
|
||||
| Line numbers shift, file is volatile | pattern |
|
||||
| PR / issue / doc gives the "why" | uri |
|
||||
| Reader should open terminal or explorer | view or commands |
|
||||
|
||||
---
|
||||
|
||||
### Step count calibration
|
||||
|
||||
Match steps to depth and persona. These are targets, not hard limits.
|
||||
|
||||
| Depth | Total steps | Core path steps | Notes |
|
||||
|-------|-------------|-----------------|-------|
|
||||
| Quick | 5–8 | 3–5 | Vibecoder, fast explorer — cut ruthlessly |
|
||||
| Standard | 9–13 | 6–9 | Most personas — breadth + enough detail |
|
||||
| Deep | 14–18 | 10–13 | Architect, RCA — every tradeoff surfaced |
|
||||
|
||||
Scale with repo size too. A 3-file CLI doesn't get 15 steps. A 200-file monolith shouldn't be squeezed into 5.
|
||||
|
||||
| Repo size | Recommended standard depth |
|
||||
|-----------|---------------------------|
|
||||
| Tiny (< 20 files) | 5–8 steps |
|
||||
| Small (20–80 files) | 8–11 steps |
|
||||
| Medium (80–300 files) | 10–13 steps |
|
||||
| Large (300+ files) | 12–15 steps (scoped to relevant subsystem) |
|
||||
|
||||
---
|
||||
|
||||
### Writing excellent descriptions — the SMIG formula
|
||||
|
||||
Every description should answer four questions in order. You don't need four paragraphs — but every description needs all four elements, even briefly.
|
||||
|
||||
**S — Situation**: What is the reader looking at? One sentence grounding them in context.
|
||||
**M — Mechanism**: How does this code work? What pattern, rule, or design is in play?
|
||||
**I — Implication**: Why does this matter for *this persona's goal specifically*?
|
||||
**G — Gotcha**: What would a smart person get wrong here? What's non-obvious, fragile, or surprising?
|
||||
|
||||
Descriptions should tell the reader something they couldn't learn by reading the file themselves. Name the pattern, explain the design decision, flag failure modes, and cross-reference related context.
|
||||
|
||||
---
|
||||
|
||||
## Narrative arc — every tour, every persona
|
||||
|
||||
1. **Orientation** — **must be a `file` or `directory` step, never content-only.**
|
||||
Use `"file": "README.md", "line": 1` or `"directory": "src"` and put your welcome text in the description.
|
||||
A content-only first step (no `file`, `directory`, or `uri`) renders as a blank page in VS Code CodeTour — this is a known VS Code extension behaviour, not configurable.
|
||||
|
||||
2. **High-level map** (1–3 directory or uri steps) — major modules and how they relate.
|
||||
Not every folder — just what this persona needs to know.
|
||||
|
||||
3. **Core path** (file/line, selection, pattern, uri steps) — the specific code that matters.
|
||||
This is the heart of the tour. Read and narrate. Don't skim.
|
||||
|
||||
4. **Closing** (content) — what the reader now understands, what they can do next,
|
||||
2–3 suggested follow-up tours. If `nextTour` is set, reference it by name here.
|
||||
|
||||
### Closing steps
|
||||
|
||||
Don't summarize — the reader just read it. Instead, tell them what they can now *do*, what to avoid, and suggest 2-3 follow-up tours.
|
||||
|
||||
---
|
||||
|
||||
## The 20 personas
|
||||
|
||||
| Persona | Goal | Must cover | Avoid |
|
||||
|---------|------|------------|-------|
|
||||
| **Vibecoder** | Get the vibe fast | Entry point, request flow, main modules. Max 8 steps. | Deep dives, edge cases |
|
||||
| **New joiner** | Structured ramp-up | Directories, setup, business context, service boundaries. | Advanced internals |
|
||||
| **Bug fixer** | Root cause fast | User action → trigger → fault points. Repro hints + test locations. | Architecture tours |
|
||||
| **RCA investigator** | Why did it fail | Causality chain, side effects, race conditions, observability. | Happy path |
|
||||
| **Feature explainer** | One feature end-to-end | UI → API → backend → storage. Feature flags, edge cases. | Unrelated features |
|
||||
| **PR reviewer** | Review the change correctly | Change story, invariants, risky areas, reviewer checklist. URI step for PR. | Unrelated context |
|
||||
| **Security reviewer** | Trust boundaries | Auth flow, input validation, secret handling, sensitive sinks. | Unrelated business logic |
|
||||
| **Refactorer** | Safe restructuring | Seams, hidden deps, coupling hotspots, safe extraction order. | Feature explanations |
|
||||
| **External contributor** | Contribute without breaking | Safe areas, code style, architecture landmines. | Deep internals |
|
||||
| **Tech lead / architect** | Shape and rationale | Module boundaries, design tradeoffs, risk hotspots. | Line-by-line walkthroughs |
|
||||
|
||||
---
|
||||
|
||||
## Designing a tour series
|
||||
|
||||
When a codebase is complex enough that one tour can't cover it well, design a series.
|
||||
The `nextTour` field chains them: when the reader finishes one tour, VS Code offers to
|
||||
launch the next automatically.
|
||||
|
||||
**Plan the series before writing any tour.** A good series has:
|
||||
- A clear escalation path (broad → narrow, orientation → deep-dive)
|
||||
- No duplicate steps between tours
|
||||
- Each tour standalone enough to be useful on its own
|
||||
|
||||
Set `nextTour` in each tour to the `title` of the next one (must match exactly). Each tour should be standalone enough to be useful on its own.
|
||||
|
||||
---
|
||||
|
||||
## What CodeTour cannot do
|
||||
|
||||
If asked for any of these, say clearly that it's not supported — do not suggest a workaround that doesn't exist:
|
||||
|
||||
| Request | Reality |
|
||||
|---|---|
|
||||
| **Auto-advance to next step after X seconds** | Not supported. Navigation is always manual — the reader clicks Next. There is no timer, delay, or autoplay step mechanic in CodeTour. |
|
||||
| **Embed a video or GIF in a step** | Not supported. Descriptions are Markdown text only. |
|
||||
| **Run arbitrary shell commands** | Not supported. `commands` only executes VS Code commands (e.g. `workbench.action.terminal.focus`), not shell commands. |
|
||||
| **Branch / conditional next step** | Not supported. Tours are linear. `when` controls whether a tour is shown, not which step follows which. |
|
||||
| **Show a step without opening a file** | Partially — content-only steps work, but step 1 must have a `file` or `directory` anchor or VS Code shows a blank page. |
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
| Anti-pattern | Fix |
|
||||
|---|---|
|
||||
| **File listing** — visiting files with "this file contains..." | Tell a story; each step should depend on the previous one |
|
||||
| **Generic descriptions** | Name the specific pattern/gotcha unique to *this* codebase |
|
||||
| **Line number guessing** | Never write a line number you didn't verify by reading the file |
|
||||
| **Ignoring the persona** | Cut every step that doesn't serve their specific goal |
|
||||
| **Hallucinated files** | If a file doesn't exist, skip the step |
|
||||
|
||||
---
|
||||
|
||||
## Quality checklist — verify before writing the file
|
||||
|
||||
- [ ] Every `file` path is **relative to the repo root** (no leading `/` or `./`)
|
||||
- [ ] Every `file` path read and confirmed to exist
|
||||
- [ ] Every `line` number verified by reading the file (not guessed)
|
||||
- [ ] Every `directory` is **relative to the repo root** and confirmed to exist
|
||||
- [ ] Every `pattern` regex would match a real line in the file
|
||||
- [ ] Every `uri` is a complete, real URL (https://...)
|
||||
- [ ] `ref` is a real branch/tag/commit if set
|
||||
- [ ] `nextTour` exactly matches the `title` of another `.tour` file if set
|
||||
- [ ] Only `.tour` JSON files created — no source code touched
|
||||
- [ ] First step has a `file` or `directory` anchor (content-only first step = blank page in VS Code)
|
||||
- [ ] Tour ends with a closing content step that tells the reader what they can *do* next
|
||||
- [ ] Every description answers SMIG — Situation, Mechanism, Implication, Gotcha
|
||||
- [ ] Persona's priorities drive step selection (cut everything that doesn't serve their goal)
|
||||
- [ ] Step count matches requested depth and repo size (see calibration table)
|
||||
- [ ] At most 2 content-only steps (intro + closing)
|
||||
- [ ] All fields conform to `references/codetour-schema.json`
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Validate the tour
|
||||
|
||||
**Always run the validator immediately after writing the tour file. Do not skip this step.**
|
||||
|
||||
```bash
|
||||
python ~/.agents/skills/code-tour/scripts/validate_tour.py .tours/<name>.tour --repo-root .
|
||||
```
|
||||
|
||||
The validator checks:
|
||||
- JSON validity
|
||||
- Every `file` path exists and every `line` is within file bounds
|
||||
- Every `directory` exists
|
||||
- Every `pattern` regex compiles and matches at least one line in the file
|
||||
- Every `uri` starts with `https://`
|
||||
- `nextTour` matches an existing tour title in `.tours/`
|
||||
- Content-only step count (warns if > 2)
|
||||
- Narrative arc (warns if no orientation or closing step)
|
||||
|
||||
**Fix every error before proceeding.** Re-run until the validator reports ✓ or only warnings. Warnings are advisory — use your judgment. Do not show the user the tour until validation passes.
|
||||
|
||||
**Common VS Code issues:** Content-only first step renders blank (anchor to file/directory instead). Absolute or `./`-prefixed paths silently fail. Out-of-bounds line numbers scroll nowhere.
|
||||
|
||||
If you can't run scripts, manually verify: step 1 has `file`/`directory`, all paths exist, all line numbers are in bounds, `nextTour` matches exactly.
|
||||
|
||||
**Autoplay:** `isPrimary: true` + `.vscode/settings.json` with `{ "codetour.promptForPrimaryTour": true }` prompts on repo open. Omit `ref` for tours that should appear on any branch.
|
||||
|
||||
**Share:** For public repos, users can open tours at `https://vscode.dev/github.com/<owner>/<repo>` with no install.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Summarize
|
||||
|
||||
After writing the tour, tell the user:
|
||||
- File path (`.tours/<name>.tour`)
|
||||
- One-paragraph summary of what the tour covers and who it's for
|
||||
- The `vscode.dev` URL if the repo is public (so they can share it immediately)
|
||||
- 2–3 suggested follow-up tours (or the next tour in the series if one was planned)
|
||||
- Any user-requested files that didn't exist (be explicit — don't quietly substitute)
|
||||
|
||||
---
|
||||
|
||||
## File naming
|
||||
|
||||
`<persona>-<focus>.tour` — kebab-case, communicates both:
|
||||
```
|
||||
onboarding-new-joiner.tour
|
||||
bug-fixer-payment-flow.tour
|
||||
architect-overview.tour
|
||||
vibecoder-quickstart.tour
|
||||
pr-review-auth-refactor.tour
|
||||
security-auth-boundaries.tour
|
||||
concept-dependency-injection.tour
|
||||
rca-login-outage.tour
|
||||
```
|
||||
115
skills/code-tour/references/codetour-schema.json
Normal file
115
skills/code-tour/references/codetour-schema.json
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Schema for CodeTour tour files",
|
||||
"type": "object",
|
||||
"required": ["title", "steps"],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Specifies the title of the code tour."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Specifies an optional description for the code tour."
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "Indicates the git ref (branch/commit/tag) that this tour associate with."
|
||||
},
|
||||
"isPrimary": {
|
||||
"type": "boolean",
|
||||
"description": "Specifies whether the tour represents the primary tour for this codebase."
|
||||
},
|
||||
"nextTour": {
|
||||
"type": "string",
|
||||
"description": "Specifies the title of the tour that is meant to follow this tour."
|
||||
},
|
||||
"stepMarker": {
|
||||
"type": "string",
|
||||
"description": "Specifies the marker that indicates a line of code represents a step for this tour."
|
||||
},
|
||||
"when": {
|
||||
"type": "string",
|
||||
"description": "Specifies the condition (JavaScript expression) that must be met before this tour is shown."
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"description": "Specifies the list of steps that are included in the code tour.",
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["description"],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "An optional title for the step."
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Description of the step. Supports markdown."
|
||||
},
|
||||
"file": {
|
||||
"type": "string",
|
||||
"description": "File path (relative to the workspace root) that the step is associated with."
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
"description": "Directory path (relative to the workspace root) that the step is associated with."
|
||||
},
|
||||
"uri": {
|
||||
"type": "string",
|
||||
"description": "Absolute URI (https://...) associated with the step. Use for PRs, issues, docs, ADRs."
|
||||
},
|
||||
"line": {
|
||||
"type": "number",
|
||||
"description": "Line number (1-based) that the step is associated with."
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Regex to associate the step with a line by content instead of line number. Useful when line numbers shift frequently."
|
||||
},
|
||||
"selection": {
|
||||
"type": "object",
|
||||
"required": ["start", "end"],
|
||||
"description": "Text selection range associated with the step. Use when a block of code (not a single line) is the point.",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "object",
|
||||
"required": ["line", "character"],
|
||||
"properties": {
|
||||
"line": { "type": "number", "description": "Line number (1-based) where the selection starts." },
|
||||
"character": { "type": "number", "description": "Column number (1-based) where the selection starts." }
|
||||
}
|
||||
},
|
||||
"end": {
|
||||
"type": "object",
|
||||
"required": ["line", "character"],
|
||||
"properties": {
|
||||
"line": { "type": "number", "description": "Line number (1-based) where the selection ends." },
|
||||
"character": { "type": "number", "description": "Column number (1-based) where the selection ends." }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
"type": "string",
|
||||
"description": "VS Code view ID to auto-focus when navigating to this step (e.g. 'terminal', 'explorer', 'problems', 'scm')."
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"description": "VS Code command URIs to execute when this step is navigated to.",
|
||||
"default": [],
|
||||
"items": { "type": "string" },
|
||||
"examples": [
|
||||
["editor.action.goToDeclaration"],
|
||||
["workbench.action.terminal.focus"],
|
||||
["editor.action.showHover"],
|
||||
["references-view.findReferences"],
|
||||
["workbench.action.tasks.runTask"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
195
skills/code-tour/references/examples.md
Normal file
195
skills/code-tour/references/examples.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Real-World CodeTour Examples
|
||||
|
||||
Reference this file when you want to see how real repos use CodeTour features.
|
||||
Each example is sourced from a public GitHub repo with a direct link to the `.tour` file.
|
||||
|
||||
---
|
||||
|
||||
## microsoft/codetour — Contributor orientation
|
||||
|
||||
**Tour file:** https://github.com/microsoft/codetour/blob/main/.tours/intro.tour
|
||||
**Persona:** New contributor
|
||||
**Steps:** ~5 · **Depth:** Standard
|
||||
|
||||
**What makes it good:**
|
||||
- Intro step with an embedded SVG architecture diagram (raw GitHub URL inside the description)
|
||||
- Rich markdown per step with emoji section headers (`### 🎥 Tour Player`)
|
||||
- Inline cross-file links inside descriptions: `[Gutter decorator](./src/player/decorator.ts)`
|
||||
- Uses the top-level `description` field as a subtitle for the tour itself
|
||||
|
||||
**Technique to copy:** Embed images and cross-links in descriptions to make them self-contained.
|
||||
|
||||
```json
|
||||
{
|
||||
"file": "src/player/index.ts",
|
||||
"line": 436,
|
||||
"description": "### 🎥 Tour Player\n\nThe CodeTour player ...\n\n\n\nSee also: [Gutter decorator](./src/player/decorator.ts)"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## a11yproject/a11yproject.com — New contributor onboarding
|
||||
|
||||
**Tour file:** https://github.com/a11yproject/a11yproject.com/blob/main/.tours/code-tour.tour
|
||||
**Persona:** External contributor
|
||||
**Steps:** 26 · **Depth:** Deep
|
||||
|
||||
**What makes it good:**
|
||||
- Almost entirely `directory` steps — orients to every `src/` subdirectory without getting lost in files
|
||||
- Conversational, beginner-friendly tone throughout
|
||||
- `selection` on the opening step to highlight the exact entry in `package.json`
|
||||
- Closes with a genuine thank-you and call-to-action
|
||||
|
||||
**Technique to copy:** Use directory steps as the skeleton of an onboarding tour — they teach structure without requiring the author to explain every file.
|
||||
|
||||
```json
|
||||
{
|
||||
"directory": "src/_data",
|
||||
"description": "This folder contains the **data files** for the site. Think of them as a lightweight database — YAML files that power the resource listings, posts index, and nav."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## github/codespaces-codeql — The most technically complete example
|
||||
|
||||
**Tour file:** https://github.com/github/codespaces-codeql/blob/main/.tours/codeql-tutorial.tour
|
||||
**Persona:** Security engineer / concept learner
|
||||
**Steps:** 12 · **Depth:** Standard
|
||||
|
||||
**What makes it good:**
|
||||
- `isPrimary: true` — auto-launches when the Codespace opens
|
||||
- `commands` array to run real VS Code commands mid-tour: the tour literally executes `codeQL.runQuery` when the reader arrives at that step
|
||||
- `view` property to switch the sidebar panel (`"view": "codeQLDatabases"`)
|
||||
- `pattern` instead of `line` for resilient matching: `"pattern": "import tutorial.*"`
|
||||
- `selection` to highlight the exact `select` clause in a query file
|
||||
|
||||
**This is the canonical reference for `commands`, `view`, and `pattern`.**
|
||||
|
||||
```json
|
||||
{
|
||||
"file": "tutorial.ql",
|
||||
"pattern": "import tutorial.*",
|
||||
"view": "codeQLDatabases",
|
||||
"commands": ["codeQL.setDefaultTourDatabase", "codeQL.runQuery"],
|
||||
"title": "Run your first query",
|
||||
"description": "Click the **▶ Run** button above. The results appear in the CodeQL Query Results panel."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## github/codespaces-learn-with-me — Minimal interactive tutorial
|
||||
|
||||
**Tour file:** https://github.com/github/codespaces-learn-with-me/blob/main/.tours/main.tour
|
||||
**Persona:** Total beginner
|
||||
**Steps:** 4 · **Depth:** Quick
|
||||
|
||||
**What makes it good:**
|
||||
- Only 4 steps — proves that less is more for quick/vibecoder personas
|
||||
- `isPrimary: true` for auto-launch
|
||||
- Each step tells the reader to **do something** (edit a string, change a color) — not just read
|
||||
- Ends with a tangible outcome: "your page is live"
|
||||
|
||||
**Technique to copy:** For quick/vibecoder tours, cut mercilessly. Four steps that drive action beat twelve that explain everything.
|
||||
|
||||
---
|
||||
|
||||
## blackgirlbytes/copilot-todo-list — 28-step interactive tutorial
|
||||
|
||||
**Tour file:** https://github.com/blackgirlbytes/copilot-todo-list/blob/main/.tours/main.tour
|
||||
**Persona:** Concept learner / hands-on tutorial
|
||||
**Steps:** 28 · **Depth:** Deep
|
||||
|
||||
**What makes it good:**
|
||||
- Uses **content-only checkpoint steps** (no `file` key) as progress milestones: "Check out your page! 🎉" and "Try it out!" between coding tasks
|
||||
- Terminal inline commands in descriptions: `>> npm install uuid; npm install styled-components`
|
||||
- Each file step shows the exact code the user should accept, in a markdown code fence, so they know the expected output
|
||||
|
||||
**Technique to copy:** Checkpoint steps (content-only, milestone title) break up long tours and give the reader a sense of progress.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Check out your page! 🎉",
|
||||
"description": "Open the **Simple Browser** tab to see your to-do list. You should see all three tasks rendering from your data array.\n\nOnce you're happy with it, continue to add interactivity."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## lucasjellema/cloudnative-on-oci-2021 — Multi-tour architecture series
|
||||
|
||||
**Tour files:**
|
||||
- https://github.com/lucasjellema/cloudnative-on-oci-2021/blob/main/.tours/function-tweet-retriever.tour
|
||||
- https://github.com/lucasjellema/cloudnative-on-oci-2021/blob/main/.tours/oci-and-infrastructure-as-code.tour
|
||||
- https://github.com/lucasjellema/cloudnative-on-oci-2021/blob/main/.tours/build-and-deployment-pipeline-function-tweet-retriever.tour
|
||||
|
||||
**Persona:** Platform engineer / architect
|
||||
**Steps:** 12 per tour · **Depth:** Standard
|
||||
|
||||
**What makes it good:**
|
||||
- Three separate tours for three separate concerns (function code, IaC, CI/CD pipeline) — each standalone but linked via `nextTour`
|
||||
- `selection` coordinates used heavily in Terraform files where a block (not a single line) is the point
|
||||
- Steps include markdown links to official OCI documentation inline
|
||||
- Designed to be browsed via `vscode.dev/github.com/...` without cloning
|
||||
|
||||
**Technique to copy:** For complex systems, write one tour per layer and chain them with `nextTour`. Don't try to cover infrastructure + application code + CI/CD in one tour.
|
||||
|
||||
---
|
||||
|
||||
## SeleniumHQ/selenium — Monorepo build system onboarding
|
||||
|
||||
**Tour files:**
|
||||
- `.tours/bazel.tour` — Bazel workspace and build target orientation
|
||||
- `.tours/building-and-testing-the-python-bindings.tour` — Python bindings BUILD.bazel walkthrough
|
||||
|
||||
**Persona:** External contributor (build system focus)
|
||||
**Steps:** ~10 per tour
|
||||
|
||||
**What makes it good:**
|
||||
- Targets a non-obvious entry point — not the product code but the build system
|
||||
- Proves that "contributor onboarding" tours don't have to start with `main()` — they start with whatever is confusing about this specific repo
|
||||
- Used in a large, mature OSS project at scale
|
||||
|
||||
---
|
||||
|
||||
## Technique quick-reference
|
||||
|
||||
| Feature | When to use | Real example |
|
||||
|---------|-------------|-------------|
|
||||
| `isPrimary: true` | Auto-launch tour when repo opens (Codespace, vscode.dev) | codespaces-learn-with-me, codespaces-codeql |
|
||||
| `commands: [...]` | Run a VS Code command when reader arrives at this step | codespaces-codeql (`codeQL.runQuery`) |
|
||||
| `view: "terminal"` | Switch VS Code sidebar/panel at this step | codespaces-codeql (`codeQLDatabases`) |
|
||||
| `pattern: "regex"` | Match by line content, not number — use for volatile files | codespaces-codeql |
|
||||
| `selection: {start, end}` | Highlight a block (function body, config section, type def) | a11yproject, oci-2021, codespaces-codeql |
|
||||
| `directory: "path/"` | Orient to a folder without reading every file | a11yproject, codespaces-codeql |
|
||||
| `uri: "https://..."` | Link to PR, issue, RFC, ADR, external doc | Any PR review tour |
|
||||
| `nextTour: "Title"` | Chain tours in a series | oci-2021 (3-part series) |
|
||||
| Checkpoint steps (content-only) | Progress milestones in long interactive tours | copilot-todo-list |
|
||||
| `>> command` in description | Terminal inline command link in VS Code | copilot-todo-list |
|
||||
| Embedded image in description | Architecture diagrams, screenshots | microsoft/codetour |
|
||||
|
||||
---
|
||||
|
||||
## Discover more real tours on GitHub
|
||||
|
||||
**Search all `.tour` files on GitHub:**
|
||||
https://github.com/search?q=path%3A**%2F*.tour+&type=code
|
||||
|
||||
This search returns every `.tour` file committed to a public GitHub repo. Use it to:
|
||||
- Find tours for repos in the same language/framework as the one you're working on
|
||||
- Study how other authors handle the same personas or step types
|
||||
- Look up how a specific field (`commands`, `selection`, `pattern`) is used in the wild
|
||||
|
||||
Filter by language or keyword to narrow results — e.g. add `language:TypeScript` or `fastapi` to the query.
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- **DEV Community — "Onboard your codebase with CodeTour"**: https://dev.to/tobiastimm/onboard-your-codebase-with-codetour-2jc8
|
||||
- **Coder Blog — "Onboard to new projects faster with CodeTour"**: https://coder.com/blog/onboard-to-new-projects-faster-with-codetour
|
||||
- **Microsoft Tech Community — Educator Developer Blog**: https://techcommunity.microsoft.com/blog/educatordeveloperblog/codetour-vscode-extension-allows-you-to-produce-interactive-guides-assessments-a/1274297
|
||||
- **AMIS Technology Blog — vscode.dev + CodeTour**: https://technology.amis.nl/software-development/visual-studio-code-the-code-tours-extension-for-in-context-and-interactive-readme/
|
||||
- **CodeTour GitHub Topics**: https://github.com/topics/codetour
|
||||
286
skills/code-tour/scripts/generate_from_docs.py
Normal file
286
skills/code-tour/scripts/generate_from_docs.py
Normal file
@@ -0,0 +1,286 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate a tour skeleton from repo documentation (README, CONTRIBUTING, docs/).
|
||||
|
||||
Reads README.md (and optionally CONTRIBUTING.md, docs/) to extract:
|
||||
- File and directory references
|
||||
- Architecture / structure sections
|
||||
- Setup instructions (becomes an orientation step)
|
||||
- External links (becomes uri steps)
|
||||
|
||||
Outputs a skeleton .tour JSON that the code-tour skill fills in with descriptions.
|
||||
The skill reads this skeleton and enriches it — it does NOT replace the skill's judgment.
|
||||
|
||||
Usage:
|
||||
python generate_from_docs.py [--repo-root <path>] [--persona <persona>] [--output <file>]
|
||||
|
||||
Examples:
|
||||
python generate_from_docs.py
|
||||
python generate_from_docs.py --persona new-joiner --output .tours/from-readme.tour
|
||||
python generate_from_docs.py --repo-root /path/to/repo --persona vibecoder
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# ── Markdown extraction helpers ──────────────────────────────────────────────
|
||||
|
||||
# Matches inline code that looks like a file/directory path
|
||||
_CODE_PATH = re.compile(r"`([^`]{2,80})`")
|
||||
# Matches headings
|
||||
_HEADING = re.compile(r"^(#{1,3})\s+(.+)$", re.MULTILINE)
|
||||
# Matches markdown links: [text](url)
|
||||
_LINK = re.compile(r"\[([^\]]+)\]\((https?://[^)]+)\)")
|
||||
# Patterns that suggest a path (contains / or . with extension)
|
||||
_LOOKS_LIKE_PATH = re.compile(r"^\.?[\w\-]+(/[\w\-\.]+)+$|^\./|^[\w]+\.[a-z]{1,5}$")
|
||||
# Architecture / structure section keywords
|
||||
_STRUCT_KEYWORDS = re.compile(
|
||||
r"\b(structure|architecture|layout|overview|directory|folder|module|component|"
|
||||
r"design|system|organization|getting.started|quick.start|setup|installation)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _extract_paths_from_text(text: str, repo_root: Path) -> list[str]:
|
||||
"""Extract inline code that looks like real file/directory paths."""
|
||||
candidates = _CODE_PATH.findall(text)
|
||||
found = []
|
||||
for c in candidates:
|
||||
c = c.strip().lstrip("./")
|
||||
if not c:
|
||||
continue
|
||||
if not _LOOKS_LIKE_PATH.match(c) and "/" not in c and "." not in c:
|
||||
continue
|
||||
# check if path actually exists
|
||||
full = repo_root / c
|
||||
if full.exists():
|
||||
found.append(c)
|
||||
return found
|
||||
|
||||
|
||||
def _extract_external_links(text: str) -> list[tuple[str, str]]:
|
||||
"""Extract [label](url) pairs for URI steps."""
|
||||
links = _LINK.findall(text)
|
||||
# filter out image links and very generic anchors
|
||||
return [
|
||||
(label, url)
|
||||
for label, url in links
|
||||
if not url.endswith((".png", ".jpg", ".gif", ".svg"))
|
||||
and label.lower() not in ("here", "this", "link", "click", "see")
|
||||
]
|
||||
|
||||
|
||||
def _split_into_sections(text: str) -> list[tuple[str, str]]:
|
||||
"""Split markdown into (heading, body) pairs."""
|
||||
headings = list(_HEADING.finditer(text))
|
||||
sections = []
|
||||
for i, m in enumerate(headings):
|
||||
heading = m.group(2).strip()
|
||||
start = m.end()
|
||||
end = headings[i + 1].start() if i + 1 < len(headings) else len(text)
|
||||
body = text[start:end].strip()
|
||||
sections.append((heading, body))
|
||||
return sections
|
||||
|
||||
|
||||
def _is_structure_section(heading: str) -> bool:
|
||||
return bool(_STRUCT_KEYWORDS.search(heading))
|
||||
|
||||
|
||||
# ── Step builders ─────────────────────────────────────────────────────────────
|
||||
|
||||
def _make_content_step(title: str, hint: str) -> dict:
|
||||
return {
|
||||
"title": title,
|
||||
"description": f"[TODO: {hint}]",
|
||||
}
|
||||
|
||||
|
||||
def _make_file_step(path: str, hint: str = "") -> dict:
|
||||
step = {
|
||||
"file": path,
|
||||
"title": f"[TODO: title for {path}]",
|
||||
"description": f"[TODO: {hint or 'explain this file for the persona'}]",
|
||||
}
|
||||
return step
|
||||
|
||||
|
||||
def _make_dir_step(path: str, hint: str = "") -> dict:
|
||||
return {
|
||||
"directory": path,
|
||||
"title": f"[TODO: title for {path}/]",
|
||||
"description": f"[TODO: {hint or 'explain what lives here'}]",
|
||||
}
|
||||
|
||||
|
||||
def _make_uri_step(url: str, label: str) -> dict:
|
||||
return {
|
||||
"uri": url,
|
||||
"title": label,
|
||||
"description": "[TODO: explain why this link is relevant and what the reader should notice]",
|
||||
}
|
||||
|
||||
|
||||
# ── Core generator ────────────────────────────────────────────────────────────
|
||||
|
||||
def generate_skeleton(repo_root: str = ".", persona: str = "new-joiner") -> dict:
|
||||
repo = Path(repo_root).resolve()
|
||||
|
||||
# ── Read documentation files ─────────────────────────────────────────
|
||||
doc_files = ["README.md", "readme.md", "Readme.md"]
|
||||
extra_docs = ["CONTRIBUTING.md", "ARCHITECTURE.md", "docs/architecture.md", "docs/README.md"]
|
||||
|
||||
readme_text = ""
|
||||
for name in doc_files:
|
||||
p = repo / name
|
||||
if p.exists():
|
||||
readme_text = p.read_text(errors="replace")
|
||||
break
|
||||
|
||||
extra_texts = []
|
||||
for name in extra_docs:
|
||||
p = repo / name
|
||||
if p.exists():
|
||||
extra_texts.append((name, p.read_text(errors="replace")))
|
||||
|
||||
all_text = readme_text + "\n".join(t for _, t in extra_texts)
|
||||
|
||||
# ── Collect steps ─────────────────────────────────────────────────────
|
||||
steps = []
|
||||
seen_paths: set[str] = set()
|
||||
|
||||
# 1. Intro step
|
||||
steps.append(
|
||||
_make_content_step(
|
||||
"Welcome",
|
||||
f"Introduce the repo: what it does, who this {persona} tour is for, what they'll understand after finishing.",
|
||||
)
|
||||
)
|
||||
|
||||
# 2. Parse README sections
|
||||
if readme_text:
|
||||
sections = _split_into_sections(readme_text)
|
||||
for heading, body in sections:
|
||||
# structure / architecture sections → directory steps
|
||||
if _is_structure_section(heading):
|
||||
paths = _extract_paths_from_text(body, repo)
|
||||
for p in paths:
|
||||
if p in seen_paths:
|
||||
continue
|
||||
seen_paths.add(p)
|
||||
full = repo / p
|
||||
if full.is_dir():
|
||||
steps.append(_make_dir_step(p, f"mentioned under '{heading}' in README"))
|
||||
elif full.is_file():
|
||||
steps.append(_make_file_step(p, f"mentioned under '{heading}' in README"))
|
||||
|
||||
# 3. Scan all text for file/dir references not yet captured
|
||||
all_paths = _extract_paths_from_text(all_text, repo)
|
||||
for p in all_paths:
|
||||
if p in seen_paths:
|
||||
continue
|
||||
seen_paths.add(p)
|
||||
full = repo / p
|
||||
if full.is_dir():
|
||||
steps.append(_make_dir_step(p))
|
||||
elif full.is_file():
|
||||
steps.append(_make_file_step(p))
|
||||
|
||||
# 4. If very few file steps found, fall back to top-level directory scan
|
||||
file_and_dir_steps = [s for s in steps if "file" in s or "directory" in s]
|
||||
if len(file_and_dir_steps) < 3:
|
||||
# add top-level directories
|
||||
for item in sorted(repo.iterdir()):
|
||||
if item.name.startswith(".") or item.name in ("node_modules", "__pycache__", ".git"):
|
||||
continue
|
||||
rel = str(item.relative_to(repo))
|
||||
if rel in seen_paths:
|
||||
continue
|
||||
seen_paths.add(rel)
|
||||
if item.is_dir():
|
||||
steps.append(_make_dir_step(rel, "top-level directory"))
|
||||
elif item.is_file() and item.suffix in (".ts", ".js", ".py", ".go", ".rs", ".java", ".rb"):
|
||||
steps.append(_make_file_step(rel, "top-level source file"))
|
||||
|
||||
# 5. URI steps from external links in README
|
||||
links = _extract_external_links(readme_text)
|
||||
# Only include links that look like architecture / design references
|
||||
for label, url in links[:3]: # cap at 3 to avoid noise
|
||||
steps.append(_make_uri_step(url, label))
|
||||
|
||||
# 6. Closing step
|
||||
steps.append(
|
||||
_make_content_step(
|
||||
"What to Explore Next",
|
||||
"Summarize what the reader now understands. List 2–3 follow-up tours they should read next.",
|
||||
)
|
||||
)
|
||||
|
||||
# Deduplicate steps by (file/directory/uri key)
|
||||
seen_keys: set = set()
|
||||
deduped = []
|
||||
for s in steps:
|
||||
key = s.get("file") or s.get("directory") or s.get("uri") or s.get("title")
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
deduped.append(s)
|
||||
|
||||
return {
|
||||
"$schema": "https://aka.ms/codetour-schema",
|
||||
"title": f"[TODO: descriptive title for {persona} tour]",
|
||||
"description": f"[TODO: one sentence — who this is for and what they'll understand]",
|
||||
"_skeleton_generated_by": "generate_from_docs.py",
|
||||
"_instructions": (
|
||||
"This is a skeleton. Fill in every [TODO: ...] with real content. "
|
||||
"Read each referenced file before writing its description. "
|
||||
"Remove this _skeleton_generated_by and _instructions field before saving."
|
||||
),
|
||||
"steps": deduped,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if "--help" in args or "-h" in args:
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
|
||||
repo_root = "."
|
||||
persona = "new-joiner"
|
||||
output: Optional[str] = None
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--repo-root" and i + 1 < len(args):
|
||||
repo_root = args[i + 1]
|
||||
i += 2
|
||||
elif args[i] == "--persona" and i + 1 < len(args):
|
||||
persona = args[i + 1]
|
||||
i += 2
|
||||
elif args[i] == "--output" and i + 1 < len(args):
|
||||
output = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
skeleton = generate_skeleton(repo_root, persona)
|
||||
out_json = json.dumps(skeleton, indent=2)
|
||||
|
||||
if output:
|
||||
Path(output).parent.mkdir(parents=True, exist_ok=True)
|
||||
Path(output).write_text(out_json)
|
||||
print(f"✅ Skeleton written to {output}")
|
||||
print(f" {len(skeleton['steps'])} steps generated from docs")
|
||||
print(f" Fill in all [TODO: ...] entries before sharing")
|
||||
else:
|
||||
print(out_json)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
346
skills/code-tour/scripts/validate_tour.py
Normal file
346
skills/code-tour/scripts/validate_tour.py
Normal file
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CodeTour validator — bundled with the code-tour skill.
|
||||
|
||||
Checks a .tour file for:
|
||||
- Valid JSON
|
||||
- Required fields (title, steps, description per step)
|
||||
- File paths that actually exist in the repo
|
||||
- Line numbers within file bounds
|
||||
- Selection ranges within file bounds
|
||||
- Directory paths that exist
|
||||
- Pattern regexes that compile AND match at least one line
|
||||
- URI format (must start with https://)
|
||||
- nextTour matches an existing tour title in .tours/
|
||||
- Content-only step count (max 2 recommended)
|
||||
- Narrative arc (first step should orient, last step should close)
|
||||
|
||||
Usage:
|
||||
python validate_tour.py <tour_file> [--repo-root <path>]
|
||||
|
||||
Examples:
|
||||
python validate_tour.py .tours/new-joiner.tour
|
||||
python validate_tour.py .tours/new-joiner.tour --repo-root /path/to/repo
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
RESET = "\033[0m"
|
||||
RED = "\033[31m"
|
||||
YELLOW = "\033[33m"
|
||||
GREEN = "\033[32m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
|
||||
|
||||
def _line_count(path: Path) -> int:
|
||||
try:
|
||||
with open(path, errors="replace") as f:
|
||||
return sum(1 for _ in f)
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
def _file_content(path: Path) -> str:
|
||||
try:
|
||||
return path.read_text(errors="replace")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def validate_tour(tour_path: str, repo_root: str = ".") -> dict:
|
||||
repo = Path(repo_root).resolve()
|
||||
errors = []
|
||||
warnings = []
|
||||
info = []
|
||||
|
||||
# ── 1. JSON validity ────────────────────────────────────────────────────
|
||||
try:
|
||||
with open(tour_path, errors="replace") as f:
|
||||
tour = json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
return {
|
||||
"passed": False,
|
||||
"errors": [f"Invalid JSON: {e}"],
|
||||
"warnings": [],
|
||||
"info": [],
|
||||
"stats": {},
|
||||
}
|
||||
except FileNotFoundError:
|
||||
return {
|
||||
"passed": False,
|
||||
"errors": [f"File not found: {tour_path}"],
|
||||
"warnings": [],
|
||||
"info": [],
|
||||
"stats": {},
|
||||
}
|
||||
|
||||
# ── 2. Required top-level fields ────────────────────────────────────────
|
||||
if "title" not in tour:
|
||||
errors.append("Missing required field: 'title'")
|
||||
if "steps" not in tour:
|
||||
errors.append("Missing required field: 'steps'")
|
||||
return {"passed": False, "errors": errors, "warnings": warnings, "info": info, "stats": {}}
|
||||
|
||||
steps = tour["steps"]
|
||||
if not isinstance(steps, list):
|
||||
errors.append("'steps' must be an array")
|
||||
return {"passed": False, "errors": errors, "warnings": warnings, "info": info, "stats": {}}
|
||||
|
||||
if len(steps) == 0:
|
||||
errors.append("Tour has no steps")
|
||||
return {"passed": False, "errors": errors, "warnings": warnings, "info": info, "stats": {}}
|
||||
|
||||
# ── 3. Tour-level optional fields ───────────────────────────────────────
|
||||
if "nextTour" in tour:
|
||||
tours_dir = Path(tour_path).parent
|
||||
next_title = tour["nextTour"]
|
||||
found_next = False
|
||||
for tf in tours_dir.glob("*.tour"):
|
||||
if tf.resolve() == Path(tour_path).resolve():
|
||||
continue
|
||||
try:
|
||||
other = json.loads(tf.read_text())
|
||||
if other.get("title") == next_title:
|
||||
found_next = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if not found_next:
|
||||
warnings.append(
|
||||
f"nextTour '{next_title}' — no .tour file in .tours/ has a matching title"
|
||||
)
|
||||
|
||||
# ── 4. Per-step validation ───────────────────────────────────────────────
|
||||
content_only_count = 0
|
||||
file_step_count = 0
|
||||
dir_step_count = 0
|
||||
uri_step_count = 0
|
||||
|
||||
for i, step in enumerate(steps):
|
||||
label = f"Step {i + 1}"
|
||||
if "title" in step:
|
||||
label += f" — {step['title']!r}"
|
||||
|
||||
# description required on every step
|
||||
if "description" not in step:
|
||||
errors.append(f"{label}: Missing required field 'description'")
|
||||
|
||||
has_file = "file" in step
|
||||
has_dir = "directory" in step
|
||||
has_uri = "uri" in step
|
||||
has_selection = "selection" in step
|
||||
|
||||
if not has_file and not has_dir and not has_uri:
|
||||
content_only_count += 1
|
||||
|
||||
# ── file ──────────────────────────────────────────────────────────
|
||||
if has_file:
|
||||
file_step_count += 1
|
||||
raw_path = step["file"]
|
||||
|
||||
# must be relative — no leading slash, no ./
|
||||
if raw_path.startswith("/"):
|
||||
errors.append(f"{label}: File path must be relative (no leading /): {raw_path!r}")
|
||||
elif raw_path.startswith("./"):
|
||||
warnings.append(f"{label}: File path should not start with './': {raw_path!r}")
|
||||
|
||||
file_path = repo / raw_path
|
||||
if not file_path.exists():
|
||||
errors.append(f"{label}: File does not exist: {raw_path!r}")
|
||||
elif not file_path.is_file():
|
||||
errors.append(f"{label}: Path is not a file: {raw_path!r}")
|
||||
else:
|
||||
lc = _line_count(file_path)
|
||||
|
||||
# line number
|
||||
if "line" in step:
|
||||
ln = step["line"]
|
||||
if not isinstance(ln, int):
|
||||
errors.append(f"{label}: 'line' must be an integer, got {ln!r}")
|
||||
elif ln < 1:
|
||||
errors.append(f"{label}: Line number must be >= 1, got {ln}")
|
||||
elif ln > lc:
|
||||
errors.append(
|
||||
f"{label}: Line {ln} exceeds file length ({lc} lines): {raw_path!r}"
|
||||
)
|
||||
|
||||
# selection
|
||||
if has_selection:
|
||||
sel = step["selection"]
|
||||
start = sel.get("start", {})
|
||||
end = sel.get("end", {})
|
||||
s_line = start.get("line", 0)
|
||||
e_line = end.get("line", 0)
|
||||
if s_line > lc:
|
||||
errors.append(
|
||||
f"{label}: Selection start line {s_line} exceeds file length ({lc})"
|
||||
)
|
||||
if e_line > lc:
|
||||
errors.append(
|
||||
f"{label}: Selection end line {e_line} exceeds file length ({lc})"
|
||||
)
|
||||
if s_line > e_line:
|
||||
errors.append(
|
||||
f"{label}: Selection start ({s_line}) is after end ({e_line})"
|
||||
)
|
||||
|
||||
# pattern
|
||||
if "pattern" in step:
|
||||
try:
|
||||
compiled = re.compile(step["pattern"], re.MULTILINE)
|
||||
content = _file_content(file_path)
|
||||
if not compiled.search(content):
|
||||
errors.append(
|
||||
f"{label}: Pattern {step['pattern']!r} matches nothing in {raw_path!r}"
|
||||
)
|
||||
except re.error as e:
|
||||
errors.append(f"{label}: Invalid regex pattern: {e}")
|
||||
|
||||
# ── directory ─────────────────────────────────────────────────────
|
||||
if has_dir:
|
||||
dir_step_count += 1
|
||||
raw_dir = step["directory"]
|
||||
dir_path = repo / raw_dir
|
||||
if not dir_path.exists():
|
||||
errors.append(f"{label}: Directory does not exist: {raw_dir!r}")
|
||||
elif not dir_path.is_dir():
|
||||
errors.append(f"{label}: Path is not a directory: {raw_dir!r}")
|
||||
|
||||
# ── uri ───────────────────────────────────────────────────────────
|
||||
if has_uri:
|
||||
uri_step_count += 1
|
||||
uri = step["uri"]
|
||||
if not uri.startswith("https://") and not uri.startswith("http://"):
|
||||
warnings.append(f"{label}: URI should start with https://: {uri!r}")
|
||||
|
||||
# ── commands ──────────────────────────────────────────────────────
|
||||
if "commands" in step:
|
||||
if not isinstance(step["commands"], list):
|
||||
errors.append(f"{label}: 'commands' must be an array")
|
||||
else:
|
||||
for cmd in step["commands"]:
|
||||
if not isinstance(cmd, str):
|
||||
errors.append(f"{label}: Each command must be a string, got {cmd!r}")
|
||||
|
||||
# ── 5. Content-only step count ──────────────────────────────────────────
|
||||
if content_only_count > 2:
|
||||
warnings.append(
|
||||
f"{content_only_count} content-only steps (no file/dir/uri). "
|
||||
f"Recommended max: 2 (intro + closing)."
|
||||
)
|
||||
|
||||
# ── 6. Narrative arc checks ─────────────────────────────────────────────
|
||||
first = steps[0]
|
||||
last = steps[-1]
|
||||
first_is_orient = "file" not in first and "directory" not in first and "uri" not in first
|
||||
last_is_closing = "file" not in last and "directory" not in last and "uri" not in last
|
||||
|
||||
if not first_is_orient and "directory" not in first:
|
||||
info.append(
|
||||
"First step is a file/uri step — consider starting with a content or directory "
|
||||
"orientation step."
|
||||
)
|
||||
if not last_is_closing:
|
||||
info.append(
|
||||
"Last step is not a content step — consider ending with a closing/summary step."
|
||||
)
|
||||
|
||||
stats = {
|
||||
"total_steps": len(steps),
|
||||
"file_steps": file_step_count,
|
||||
"directory_steps": dir_step_count,
|
||||
"content_steps": content_only_count,
|
||||
"uri_steps": uri_step_count,
|
||||
}
|
||||
|
||||
return {
|
||||
"passed": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"warnings": warnings,
|
||||
"info": info,
|
||||
"stats": stats,
|
||||
}
|
||||
|
||||
|
||||
def print_report(tour_path: str, result: dict) -> None:
|
||||
title = f"{BOLD}{tour_path}{RESET}"
|
||||
print(f"\n{title}")
|
||||
print("─" * 60)
|
||||
|
||||
stats = result.get("stats", {})
|
||||
if stats:
|
||||
parts = [
|
||||
f"{stats.get('total_steps', 0)} steps",
|
||||
f"{stats.get('file_steps', 0)} file",
|
||||
f"{stats.get('directory_steps', 0)} dir",
|
||||
f"{stats.get('content_steps', 0)} content",
|
||||
f"{stats.get('uri_steps', 0)} uri",
|
||||
]
|
||||
print(f"{DIM} {' · '.join(parts)}{RESET}")
|
||||
|
||||
errors = result.get("errors", [])
|
||||
warnings = result.get("warnings", [])
|
||||
info = result.get("info", [])
|
||||
|
||||
for e in errors:
|
||||
print(f" {RED}✗ {e}{RESET}")
|
||||
for w in warnings:
|
||||
print(f" {YELLOW}⚠ {w}{RESET}")
|
||||
for i in info:
|
||||
print(f" {DIM}ℹ {i}{RESET}")
|
||||
|
||||
if result["passed"] and not warnings:
|
||||
print(f" {GREEN}✓ All checks passed{RESET}")
|
||||
elif result["passed"]:
|
||||
print(f" {GREEN}✓ Passed{RESET} {YELLOW}(with warnings){RESET}")
|
||||
else:
|
||||
print(f" {RED}✗ Failed — {len(errors)} error(s){RESET}")
|
||||
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
if not args or args[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
sys.exit(0)
|
||||
|
||||
repo_root = "."
|
||||
tour_files = []
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--repo-root" and i + 1 < len(args):
|
||||
repo_root = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
tour_files.append(args[i])
|
||||
i += 1
|
||||
|
||||
if not tour_files:
|
||||
# validate all tours in .tours/
|
||||
tours_dir = Path(".tours")
|
||||
if tours_dir.exists():
|
||||
tour_files = [str(p) for p in sorted(tours_dir.glob("*.tour"))]
|
||||
if not tour_files:
|
||||
print("No .tour files found. Pass a file path or run from a repo with a .tours/ directory.")
|
||||
sys.exit(1)
|
||||
|
||||
all_passed = True
|
||||
for tf in tour_files:
|
||||
result = validate_tour(tf, repo_root)
|
||||
print_report(tf, result)
|
||||
if not result["passed"]:
|
||||
all_passed = False
|
||||
|
||||
sys.exit(0 if all_passed else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user