snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
46
skills/clean-copy/SKILL.md
Normal file
46
skills/clean-copy/SKILL.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
name: clean-copy
|
||||
description: Reimplement the current branch on a new branch with a clean, narrative-quality git commit history. Use when asked to make a clean copy branch, clean up commit history by replaying work, or rebuild a branch as reviewable commits.
|
||||
---
|
||||
|
||||
# Clean copy
|
||||
|
||||
Reimplement the current branch on a new branch with a clean, narrative-quality commit history suitable for reviewer comprehension.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Gather context:
|
||||
- Source branch: `git branch --show-current`.
|
||||
- Working tree: `git status --short`.
|
||||
- Commits since main: `git log main..HEAD --oneline`.
|
||||
- Diff summary: `git diff main...HEAD --stat`.
|
||||
2. Validate the source branch:
|
||||
- Ensure there are no uncommitted changes or merge conflicts.
|
||||
- Confirm the source branch is up to date with `main`.
|
||||
3. Choose the new branch name:
|
||||
- Use the user's requested name when provided.
|
||||
- Otherwise use `<source-branch>-clean`.
|
||||
4. Analyze the diff:
|
||||
- Study all changes between the source branch and `main`.
|
||||
- Understand the final intended state before recreating it.
|
||||
5. Create the clean branch from `main`.
|
||||
6. Plan the commit storyline:
|
||||
- Break the implementation into self-contained logical steps.
|
||||
- Each step should read like a stage of development in a tutorial.
|
||||
7. Reimplement the work:
|
||||
- Recreate the final changes step by step.
|
||||
- Commit after each coherent idea.
|
||||
- Use clear commit subjects and descriptions.
|
||||
- Use `git commit --no-verify` for intermediate commits so hooks do not block temporarily incomplete states.
|
||||
8. Verify correctness:
|
||||
- Confirm the final clean branch state exactly matches the original source branch.
|
||||
- Run the final commit without `--no-verify` so normal checks run.
|
||||
9. Open a pull request using the `pr` skill.
|
||||
- Include a link to the original branch in the PR description.
|
||||
|
||||
## Rules
|
||||
|
||||
- Never add yourself or an AI tool as an author, contributor, or co-author.
|
||||
- Never include AI attribution in commits or PR content.
|
||||
- The final clean branch must be identical to the source branch.
|
||||
- Do not force push unless the user explicitly asks for it.
|
||||
48
skills/commit-changes/SKILL.md
Normal file
48
skills/commit-changes/SKILL.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: commit-changes
|
||||
description: Create a git commit for the current changes. Use when asked to commit changes, make a commit, generate a commit message, or commit the current worktree with optional user-provided context.
|
||||
---
|
||||
|
||||
# Commit changes
|
||||
|
||||
Create a focused git commit for the current changes.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Gather context:
|
||||
- User notes, if provided.
|
||||
- Current branch: `git branch --show-current`.
|
||||
- Working tree: `git status --short`.
|
||||
- Staged changes: `git diff --cached --stat`.
|
||||
- Unstaged changes: `git diff --stat`.
|
||||
- Recent commits for style reference: `git log -5 --oneline`.
|
||||
2. Review the changed files.
|
||||
3. If there are no changes to commit, tell the user and stop.
|
||||
4. If there are unstaged changes, stage the relevant files with `git add`.
|
||||
5. Do not stage files that look like secrets, credentials, API keys, or private environment files.
|
||||
6. Write a conventional commit message:
|
||||
|
||||
```text
|
||||
type(scope): brief description
|
||||
|
||||
Optional longer explanation if the changes are complex.
|
||||
```
|
||||
|
||||
Allowed types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `perf`, `style`, `build`, `ci`.
|
||||
|
||||
## Message guidelines
|
||||
|
||||
- Keep the first line under 72 characters.
|
||||
- Use imperative mood, for example `add feature`, not `added feature`.
|
||||
- Be specific about what changed and why.
|
||||
- Incorporate user-provided context when it clarifies intent.
|
||||
|
||||
## Commit rules
|
||||
|
||||
- Use `git commit -m "message"`.
|
||||
- Do not push.
|
||||
- Do not amend unless explicitly requested.
|
||||
- Do not use `--no-verify`.
|
||||
- Do not include AI attribution.
|
||||
|
||||
If the commit fails because of hooks, fix mechanical formatting, lint, or import issues and retry. If the hook failure reveals a meaningful product or implementation issue, stop and ask the user how to proceed.
|
||||
111
skills/dotcom-release-crew/SKILL.md
Normal file
111
skills/dotcom-release-crew/SKILL.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: dotcom-release-crew
|
||||
description: Post to the #development Discord channel naming the people whose critical fixes or significant features are in this week's tldraw.com (dotcom) release. Use when preparing the weekly dotcom release, when asked who should be involved in this week's release, or when the scheduled release-crew automation runs. Examines the production...main commit range, selects contributors of meaningful user-facing or stability changes (not the whole team), and posts a concise summary via a Discord webhook.
|
||||
---
|
||||
|
||||
# Dotcom release crew
|
||||
|
||||
Identify who contributed **critical fixes or significant features** to this week's dotcom release (the commits on `main` that are not yet on `production`) and post a short summary to the `#development` Discord channel.
|
||||
|
||||
The goal is a very tight list of people to involve in the release — **2–3 people at most**, not a changelog and not the whole engineering team. Only exceed 3 in extraordinary weeks where more than three people each own a genuinely critical, release-risky change.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `DISCORD_RELEASE_WEBHOOK_URL` — environment variable holding the Discord webhook that posts to `#development`. Required. If it is unset, stop and tell the user to set it (do not hardcode a webhook URL — this repo is public).
|
||||
- `GH_TOKEN` — used by `gh`. Present automatically in CI; locally, ensure `gh auth status` works.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Fetch the commit range
|
||||
|
||||
Get every commit on `main` that is not on `production`, with its author and subject line. `--paginate` handles ranges larger than 250 commits.
|
||||
|
||||
```bash
|
||||
gh api repos/tldraw/tldraw/compare/production...main --paginate \
|
||||
--jq '.commits[] | [ (.author.login // .commit.author.name), (.commit.author.name), (.commit.message | split("\n")[0]) ] | @tsv'
|
||||
```
|
||||
|
||||
Each row is: `login`\t`display name`\t`subject`. tldraw squash-merges PRs, so the commit author is the PR author and the subject is the PR title (usually a conventional commit like `feat(editor): ...` with a trailing `(#1234)`).
|
||||
|
||||
Also capture the human-readable diff URL for the message: `https://github.com/tldraw/tldraw/compare/production...main`.
|
||||
|
||||
If there are **zero** commits, post a brief note that there are no changes to release this week and stop.
|
||||
|
||||
### 2. Select the critical contributors
|
||||
|
||||
Read the subject lines and keep only commits that a release manager would want a human on hand for. Group the kept commits by author.
|
||||
|
||||
**Include** (judgment, not just prefix):
|
||||
|
||||
- `feat` — significant features, especially scoped to `dotcom`, `editor`, `sync`, `sync-core`, `store`, `tlschema`, `state`.
|
||||
- `fix` — meaningful correctness, data-integrity, sync, or crash fixes.
|
||||
- `perf` — performance changes that affect users.
|
||||
- Anything touching multiplayer sync, persistence/migrations, or auth/billing, even if small.
|
||||
|
||||
**Exclude:**
|
||||
|
||||
- `docs`, `test`, `chore`, `style`, `ci`, `build`, and dependency bumps (dependabot, "Bump versions", `[skip ci]`).
|
||||
- Trivial `fix`es (typos, comments, lint, flaky-test pins, snapshot updates).
|
||||
- Reverts that cancel out another commit in the same range.
|
||||
|
||||
Then **rank the authors by how critical and release-risky their change is, and keep only the top 2–3**. When unsure whether a change is "critical", lean toward **excluding** it — the point is a very short, high-signal list. It is completely fine to surface just 1–2 people, and normal to have some kept-but-not-listed commits.
|
||||
|
||||
Only go above 3 people in extraordinary weeks — for example a large migration plus an unrelated sync fix plus an auth change all landing together, where each genuinely needs its own owner on hand. If you do, briefly justify the extra names to yourself before including them.
|
||||
|
||||
### 3. Compose the message
|
||||
|
||||
Keep it under 2000 characters (Discord's limit). One bullet per person; if someone has multiple notable changes, list them under one bullet.
|
||||
|
||||
Mention each person with their Discord user ID from the table below, written as `<@ID>` — Discord renders that as a real `@` mention and pings them. If a GitHub login is not in the table, fall back to the plain GitHub login (no `<@…>`), since a wrong ID would ping the wrong person.
|
||||
|
||||
| GitHub login | Name | Discord username | Discord user ID |
|
||||
| ----------------- | ----------------- | ----------------- | --------------------- |
|
||||
| `angrycaptain19` | Tim | `trg1379` | `1197197068045910087` |
|
||||
| `AniKrisn` | Ani Krishnan | `anikrisn` | `1348594738927767612` |
|
||||
| `audrey17leo` | Audrey | `dreiiz` | `617346760385495060` |
|
||||
| `danieljamesross` | Dan Ross | `djrdjrdjr` | `1494628009896837203` |
|
||||
| `driev` | Niall | `driev_` | `1338934372992159786` |
|
||||
| `frolic` | Kevin Ingersoll | `frolic` | `79416844720537600` |
|
||||
| `jsscclr` | Jessica Edwards | `jsscclr` | `1382308873276358766` |
|
||||
| `kaneel` | Guillaume Richard | `guillaumetldraw` | `1519303217840656522` |
|
||||
| `kostyafarber` | Kostya Farber | `kostyafarber` | `327278771541377034` |
|
||||
| `m31-galaxy` | Andy (Andromeda) | `m31_galaxy` | `650459420635168769` |
|
||||
| `max-drake` | Max Drake | `max__drake` | `177565795973464065` |
|
||||
| `meg-an31` | Megan Walker | `megelia` | `813347618083045393` |
|
||||
| `mimecuvalo` | Mime Čuvalo | `mimecuvalo` | `1193847824350199881` |
|
||||
| `MitjaBezensek` | Mitja Bezenšek | `mitja_bezensek` | `559094482470174720` |
|
||||
| `nattofu` | Leo | `nattotofu` | `168686397941743617` |
|
||||
| `steveruizok` | Steve Ruiz | `steveruizok` | `414943707662385154` |
|
||||
|
||||
Format:
|
||||
|
||||
```
|
||||
🚀 **dotcom release — people to involve this week**
|
||||
Critical changes on production...main:
|
||||
|
||||
• **Mime Čuvalo** (<@1193847824350199881>) — fix(release): don't cut a new SDK version for docs-only patches
|
||||
• **Kevin Ingersoll** (<@79416844720537600>) — feat(editor): finer, coarse-pointer-aware hit-testing
|
||||
|
||||
N critical changes from M contributors · https://github.com/tldraw/tldraw/compare/production...main
|
||||
```
|
||||
|
||||
If nothing critical was selected but there were commits, say so briefly (e.g. "No critical changes flagged this week — release looks routine.") and still include the diff link.
|
||||
|
||||
### 4. Post to Discord
|
||||
|
||||
Post the message as the webhook's `content`. Build the JSON safely (do not string-interpolate the message into the JSON by hand — use `jq` so newlines and quotes are escaped):
|
||||
|
||||
```bash
|
||||
jq -n --arg content "$MESSAGE" '{content: $content, allowed_mentions: {parse: ["users"]}}' \
|
||||
| curl -sS -X POST -H "Content-Type: application/json" -d @- "$DISCORD_RELEASE_WEBHOOK_URL"
|
||||
```
|
||||
|
||||
`allowed_mentions.parse: ["users"]` lets the `<@ID>` mentions actually ping people while making sure `@everyone`/`@here`/role mentions can never fire from this message.
|
||||
|
||||
A successful post returns HTTP 204 with an empty body. Report to the user what was posted (or that the post failed, with the curl output).
|
||||
|
||||
## Notes
|
||||
|
||||
- Do not commit or echo the full webhook URL anywhere.
|
||||
- This skill only reads git history and posts a message; it never modifies the repo.
|
||||
- `production...main` assumes the normal weekly flow where the dotcom release is the `main → production` promotion, so this range is exactly what's about to ship. During SDK freeze weeks — when `production` ships cherry-picked hotfixes while `main` keeps moving — the range is less precise (it can miss production-only hotfix authors and include `main` work that isn't releasing yet). That's fine: freeze weeks are all-hands anyway, so a perfectly tailored crew list matters less then.
|
||||
186
skills/dotcom-release-marketing/SKILL.md
Normal file
186
skills/dotcom-release-marketing/SKILL.md
Normal file
@@ -0,0 +1,186 @@
|
||||
---
|
||||
name: dotcom-release-marketing
|
||||
description: Post a plain-language summary of what's shipping in this week's tldraw.com (dotcom) release to the marketing team's Discord channel. Use when preparing the weekly dotcom release, when asked what's going out this week for a non-engineering audience, or when the scheduled release-marketing automation runs. Examines the production...main commit range, writes the user-facing changes in the house release-notes style, and posts a concise summary via a Discord webhook.
|
||||
---
|
||||
|
||||
# Dotcom release marketing summary
|
||||
|
||||
Summarize what's shipping in this week's dotcom release (the commits on `main` that are not yet on `production`) for the **marketing team**, and post it to their Discord channel.
|
||||
|
||||
This is the marketing counterpart to [[dotcom-release-crew]]. Same commit range, different audience: the release-crew message names engineers to have on hand; this one tells non-technical folks **what users will notice** so they can plan announcements, social posts, and changelog copy. No engineer pings, no release-risk framing.
|
||||
|
||||
The output is a **draft for a human to edit**, not finished copy. It should read like a streamlined version of the SDK release notes: the same voice, the same discipline about describing bugs precisely, but only the highlights and with every developer-facing detail removed.
|
||||
|
||||
**Arguments**: $ARGUMENTS
|
||||
|
||||
- `--dry-run` — print the finished message to stdout instead of posting it. Use this for testing and for reviewing a draft locally.
|
||||
- `--range <base>...<head>` — compare this range instead of `production...main`. Use it to regenerate a past week's summary (for example, the previous deploy's source commit against `main` as it was at the time).
|
||||
|
||||
## Inputs
|
||||
|
||||
- `DISCORD_MARKETING_WEBHOOK_URL` — environment variable holding the Discord webhook that posts to the marketing channel. Required unless `--dry-run` is given. If it is unset and this is not a dry run, stop and tell the user to set it (do not hardcode a webhook URL — this repo is public).
|
||||
- `GH_TOKEN` — used by `gh`. Present automatically in CI; locally, ensure `gh auth status` works.
|
||||
|
||||
## Style source
|
||||
|
||||
Before writing anything, read the house style. Do not invent a tone.
|
||||
|
||||
1. `skills/shared/release-notes-guide.md` — the SDK release-notes style guide. Its rules for entry format, what to include, and what to omit apply here too.
|
||||
2. The two most recent published release notes in `apps/docs/content/releases/` (the highest `vX.Y.0.mdx` files, not `next.mdx`). Read their introduction paragraph, `## Improvements`, and `## Bug fixes` sections. These are the examples to match.
|
||||
|
||||
Take their **discipline**, not their register. The SDK notes are written for developers; this summary is written for someone who uses tldraw.com and has never read a commit.
|
||||
|
||||
What to take from them:
|
||||
|
||||
- The **introduction paragraph** pattern: one or two sentences that lead with concrete features, not adjectives.
|
||||
- The way **bugs are described as observed**, with the situation that triggered them. "Fix undo after a cut also reverting the change made before it." "Fix Escape being ignored while pressing on the canvas with the select tool." The condition is always there. Keep that; rephrase it for a user.
|
||||
- Their restraint: short entries, no fluff, nothing that can't be verified.
|
||||
- **No decorative emoji.** The SDK notes use none apart from 💥 and 🔜 as markers, and this summary uses none at all.
|
||||
|
||||
What to leave behind: anything under `## API changes`, PR links and numbers, anything in backticks, package names, the `Add`/`Fix` verb-first form, and explanations of *how* something was fixed.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Fetch the commit range
|
||||
|
||||
Set the range. Default is `production...main`; `--range` overrides it.
|
||||
|
||||
Get every commit in the range with its **full message**, not just the subject line. tldraw squash-merges PRs, so the subject is the PR title and the body is the PR description, which usually includes `### Before`, `### After`, or `### Release notes` sections. Those sections are where the precise description of a bug lives. `--paginate` handles ranges larger than 250 commits.
|
||||
|
||||
```bash
|
||||
gh api "repos/tldraw/tldraw/compare/${RANGE}" --paginate \
|
||||
--jq '.commits[] | {sha: .sha[0:10], message: .commit.message}'
|
||||
```
|
||||
|
||||
Also capture the human-readable diff URL: `https://github.com/tldraw/tldraw/compare/${RANGE}`.
|
||||
|
||||
If there are **zero** commits, post a brief note that there's nothing new shipping this week and stop.
|
||||
|
||||
### 2. Select what's worth telling marketing about
|
||||
|
||||
Keep only changes a **user of tldraw.com could notice**. Apply the SDK guide's include/omit rules, then tighten them for this audience.
|
||||
|
||||
**Include:**
|
||||
|
||||
- New features and capabilities users can see or use, in `dotcom`, `tldraw`, `editor`, or collaboration and sync.
|
||||
- Improvements users will feel: visible polish, and performance only when the difference is noticeable in normal use.
|
||||
- Fixes for bugs users would have hit: broken interactions, rendering, export, embeds, sync, sharing, sign-in.
|
||||
|
||||
**Exclude:**
|
||||
|
||||
- Anything that would sit under `## API changes` in the SDK notes.
|
||||
- `docs`, `test`, `chore`, `ci`, `build`, dependency bumps, refactors, tooling, and anything only observable in code.
|
||||
- Fixes for bugs introduced in the same range (the bug never reached users).
|
||||
- Anything whose only honest description needs a code identifier.
|
||||
|
||||
For every candidate, answer one question: **what would a user have to do to notice this?** If there is no answer, drop it. If the answer is the condition of a bug, that condition goes in the bullet.
|
||||
|
||||
Aim for **three to six bullets**. Fewer is fine. It is normal for most commits not to make the cut.
|
||||
|
||||
### 3. Write the entries
|
||||
|
||||
Write each kept change as one or two short sentences to a person who uses tldraw.com. Under about 30 words per bullet, even for a feature made of several PRs: name the two or three things they'll notice and stop.
|
||||
|
||||
**Every bullet contains the situation**: what the reader does, or sees, that the change affects. Take it from the PR body's before/after or release-notes section. If you cannot find it, drop the bullet rather than guess.
|
||||
|
||||
For a new feature, say what it does, not what it's called. "You can now wrap a single shape in a frame by selecting it" rather than "Frame selection works on a single shape". Menu item and tool names are fine as a detail, never as the description.
|
||||
|
||||
For a fix, describe the situation and what happens now. It does not need to start with "Fix", and it should not read like a commit title. Any of these shapes works:
|
||||
|
||||
- Situation, then outcome: "Triple-clicking a bullet to select a whole list made the text vanish. It stays put now."
|
||||
- Outcome with the condition attached: "The zoom tool now switches off when you release Z, even right after picking another tool."
|
||||
- What now works: "Excalidraw drawings with custom stroke widths now paste correctly."
|
||||
- Instead-of: "tldraw.com loads properly on slow connections instead of showing a \"Something went wrong\" page."
|
||||
|
||||
**Vary the shape across the message.** Six bullets in a row that open with the old bug ("X used to happen. Now Y.") read like a bug tracker. Lead with what happens now whenever the reader can infer the bug from it: "Excalidraw drawings with custom stroke widths now paste correctly" says enough without spelling out the failure. Lead with the situation only when the outcome is meaningless without it. No more than two consecutive bullets should open with the old behaviour.
|
||||
|
||||
**New means a user can do something they couldn't before.** A change that makes something behave the way it was always meant to is a fix, however interesting, and stays under Fixes. It can still lead the introduction paragraph.
|
||||
|
||||
**Never write "no longer", "now works", or "more reliable" on its own.** Without the situation, a negative makes the product sound broken in general: "Text no longer disappears while you're editing it" tells a reader their text used to vanish, full stop.
|
||||
|
||||
Also:
|
||||
|
||||
- No PR numbers, links, commit hashes, or issue references. The diff link at the end covers anyone who wants the detail.
|
||||
- No code identifiers, package names, or mechanisms. If the sentence cannot survive without them, it is not marketing material.
|
||||
- No emoji. No engineer names.
|
||||
- Combine commits that form one feature into one bullet.
|
||||
|
||||
Good:
|
||||
|
||||
- "You can now wrap a single shape in a frame. Select it and choose Frame selection, and the frame fits around it."
|
||||
- "Mermaid diagrams pasted into tldraw look much closer to Mermaid's own rendering: sequence diagrams keep their participant boxes and colours, notes get enough room, and self-loops sit where you'd expect."
|
||||
- "Pressing Z right after picking a tool could leave the zoom tool stuck on. It now switches off when you let go."
|
||||
- "Bookmarks now update their preview when you change their URL."
|
||||
|
||||
Bad:
|
||||
|
||||
- "Text no longer disappears while you're editing it." — no situation, sounds like text vanished for everyone.
|
||||
- "Fix selected text vanishing when ProseMirror hides the selection." — commit-title register and a mechanism.
|
||||
- "Board version history is more reliable behind the scenes." — nothing a user can observe.
|
||||
- "Frame selection works on a single shape." — a menu name standing in for what it does.
|
||||
- "Improve Mermaid rendering ([#10813](...), [#10817](...))" — PR links, and a verb-first form written for developers.
|
||||
|
||||
### 4. Compose the message
|
||||
|
||||
Follow the SDK notes' shape, cut down: an introduction paragraph, then a flat list. No section headers unless there are both new features and fixes, in which case use plain `New` and `Fixes` lines.
|
||||
|
||||
The introduction names the one or two things that matter most and the themes of the rest. It does not restate bullets: if a fix leads the intro, the bullet below says it differently or adds the detail. For a fixes-only week, one sentence is enough.
|
||||
|
||||
Keep it under 2000 characters (Discord's limit). Sentence case. Write as though the release is going out today.
|
||||
|
||||
```
|
||||
**Shipping to tldraw.com this week**
|
||||
|
||||
Mermaid diagrams pasted into tldraw now look much closer to Mermaid's own rendering, and you can wrap a single shape in a frame. The rest is fixes.
|
||||
|
||||
New
|
||||
- You can now wrap a single shape in a frame. Select it and choose Frame selection, and the frame fits around it.
|
||||
- Mermaid diagrams pasted into tldraw look much closer to Mermaid's own rendering: sequence diagrams keep their participant boxes and colours, notes get enough room, and self-loops sit where you'd expect.
|
||||
|
||||
Fixes
|
||||
- Triple-clicking a bullet to select a whole list made the text vanish. It stays put now.
|
||||
- Pressing Z right after picking a tool could leave the zoom tool stuck on. It now switches off when you let go.
|
||||
- Excalidraw drawings with custom stroke widths now paste correctly.
|
||||
- tldraw.com loads properly on slow connections instead of showing a "Something went wrong" page.
|
||||
|
||||
This is a draft for marketing to edit. Full changes: https://github.com/tldraw/tldraw/compare/production...main
|
||||
```
|
||||
|
||||
If nothing user-facing was selected but there were commits, say so briefly ("Quiet week for user-facing changes, mostly under-the-hood work.") and still include the diff link.
|
||||
|
||||
### 5. Check the draft
|
||||
|
||||
Before posting, read the message once more and confirm:
|
||||
|
||||
- every bullet answers "what would a user do to notice this?";
|
||||
- every fix contains the situation the bug happened in;
|
||||
- no bullet contains a PR number, link, backticked identifier, package name, or emoji;
|
||||
- no bullet says "no longer", "now works", or "more reliable" without the situation;
|
||||
- no bullet reads like a commit title (verb-first `Add`/`Fix`/`Improve`);
|
||||
- no bullet is over about 30 words;
|
||||
- New contains only things a user couldn't do before, and no fix is filed there for being interesting;
|
||||
- no more than two consecutive bullets open with the old behaviour;
|
||||
- the introduction doesn't repeat a bullet word for word; and
|
||||
- the whole message is under 2000 characters.
|
||||
|
||||
### 6. Post to Discord (or print)
|
||||
|
||||
With `--dry-run`, print the finished message to stdout and stop.
|
||||
|
||||
Otherwise post the message as the webhook's `content`. Build the JSON safely (do not string-interpolate the message into the JSON by hand — use `jq` so newlines and quotes are escaped):
|
||||
|
||||
```bash
|
||||
jq -n --arg content "$MESSAGE" '{content: $content, allowed_mentions: {parse: []}}' \
|
||||
| curl -sS -X POST -H "Content-Type: application/json" -d @- "$DISCORD_MARKETING_WEBHOOK_URL"
|
||||
```
|
||||
|
||||
`allowed_mentions.parse: []` makes sure no `@everyone`/`@here`/role/user mentions can ever fire from this message — marketing summaries should never ping anyone.
|
||||
|
||||
A successful post returns HTTP 204 with an empty body. Report to the user what was posted (or that the post failed, with the curl output).
|
||||
|
||||
## Notes
|
||||
|
||||
- Do not commit or echo the full webhook URL anywhere.
|
||||
- This skill only reads git history and posts a message; it never modifies the repo.
|
||||
- `production...main` assumes the normal weekly flow where the dotcom release is the `main → production` promotion, so this range is exactly what's about to ship. During SDK freeze weeks the range is less precise — see the note in [[dotcom-release-crew]].
|
||||
- The SDK release notes for the same PRs are drafted into `apps/docs/content/releases/next.mdx` by the update-release-notes workflow after the production push, so they are not available when this runs. PRs labelled `dotcom` never appear there at all. Write from the commit bodies.
|
||||
101
skills/issue/SKILL.md
Normal file
101
skills/issue/SKILL.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
name: issue
|
||||
description: Create a GitHub issue in the tldraw repository from a user description, then mature it through follow-up questions. Use when the user invokes issue, asks to create an issue, report a bug, file a feature request, or answers follow-up questions for an issue created by this skill.
|
||||
---
|
||||
|
||||
# Issue
|
||||
|
||||
Create a GitHub issue on `tldraw/tldraw` from a user description, then interrogate the user to capture enough of their intent for the issue to be worked on.
|
||||
|
||||
Use `../write-issue/SKILL.md` as the standards reference for issue titles, bodies, types, labels, and triage conventions.
|
||||
|
||||
The goal is not to research the codebase to death. It is to capture the user's full intent and context in the issue, so that whoever (or whatever) picks it up later has what they need. You do this by creating the issue immediately, writing a brief readback of the problem, and then asking the user the questions that would sharpen that understanding.
|
||||
|
||||
## Issue body shape
|
||||
|
||||
The body always starts with the user's original description, verbatim, annotated with the human emoji and separated from the rest by a horizontal rule:
|
||||
|
||||
```
|
||||
🗣️: {user_description}
|
||||
|
||||
---
|
||||
|
||||
{two to five sentences that read back your understanding of the problem, expected behavior, and scope}
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **{question}**
|
||||
_Awaiting answer._
|
||||
2. **{question}**
|
||||
_Awaiting answer._
|
||||
|
||||
Confidence: {n}%, {ready_status}.
|
||||
```
|
||||
|
||||
- The readback paragraph is the agent's understanding of the problem. It should be short enough for the user to quickly correct, but specific enough to make the intended behavior clear. Keep it product-facing: no code blocks, no long implementation analysis, and no line-by-line diagnosis.
|
||||
- Do not include implementation breadcrumbs in the issue body by default. Avoid file paths, function names, line numbers, code snippets, likely causes, and fix recipes unless the user explicitly asks to include them. Whoever picks up the work can rediscover that technical context.
|
||||
- Keep product intent ahead of code diagnosis. The issue should record what the user wants, what they observed, and what scope or behavior they confirmed. Do not present a root cause or fix direction as fact unless the user has confirmed that framing.
|
||||
- Each open question targets a specific gap in intent or context. Avoid questions you could answer yourself by looking at the code.
|
||||
- Keep questions atomic. Do not bundle user-intent questions with implementer-verifiable technical checks. If the user answers only part of a compound question, keep the remaining part open only when it still needs the user's intent or context.
|
||||
- Prefer omitting implementer-verifiable unknowns over asking the user to verify them. Ask only when the user's own context or intent matters.
|
||||
- Prefix a question with `Critical:` inside the bold, e.g. `**Critical: Which package should we rename?**`, only when it is genuinely blocking — the issue cannot be worked on at all until it is answered. Most issues have none; do not inflate ordinary gaps into critical ones.
|
||||
- As the user answers, replace `_Awaiting answer._` with their answer (lightly cleaned up) directly beneath the question, revise the readback, and add or drop unanswered questions as needed.
|
||||
- If the user chooses not to answer a non-critical question, replace `_Awaiting answer._` with `_Deferred by user; not blocking implementation._`. Do not use this for critical questions.
|
||||
- The confidence line is a plain-text status line, not a section. It reflects whether the issue contains enough of the user's context and intent to be worked on, not confidence in the eventual fix. Use a short status phrase such as `ready to get started`, `still need more information`, or `blocked on a critical question`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Gather context:
|
||||
- User's issue description.
|
||||
- Current branch: `git branch --show-current`.
|
||||
- Recent issues: `gh issue list --repo tldraw/tldraw --limit 5 --json number,title --jq '.[] | "#\(.number) \(.title)"'`.
|
||||
2. Do a lightweight triage check, not implementation research:
|
||||
- Look for duplicate or closely related issues.
|
||||
- Use repo context only to choose issue type, labels, milestone, and broad affected area.
|
||||
- If a shallow code search is needed to avoid asking a bad question, do it, but do not add file paths, function names, line numbers, code snippets, likely causes, or fix recipes to the issue body.
|
||||
- Anything you can answer yourself from repo context should not become an open question.
|
||||
3. For visual bugs, identify a reproduction target when possible:
|
||||
- Examples app: `localhost:5420` from `yarn dev`.
|
||||
- tldraw.com app: `localhost:3000` from `yarn dev-app`.
|
||||
- Docs site: `localhost:3001` from `yarn dev-docs`.
|
||||
- If the user provided an image and you have a path or URL for it, embed or attach it in the GitHub issue.
|
||||
- If the image is visible only in the chat and cannot be attached, describe it as visual context. Do not write "screenshot attached" unless the issue actually contains the image.
|
||||
- If screenshots are useful but not feasible locally and the user has not provided one, make a screenshot request one of your open questions.
|
||||
4. Write the issue title and body using `../write-issue/SKILL.md`. The body follows the shape above: verbatim description, readback paragraph, open questions, and the confidence status line.
|
||||
5. Create the issue:
|
||||
|
||||
```bash
|
||||
gh issue create --repo tldraw/tldraw --title "..." --body "..."
|
||||
```
|
||||
|
||||
6. Set the issue type (`Bug`, `Feature`, `Example`, or `Task`) — `gh issue create --type` is unreliable, so use the script:
|
||||
|
||||
```bash
|
||||
skills/issue/scripts/set-issue-type.sh <issue-number> <type-name>
|
||||
```
|
||||
7. Assign a milestone only when there is a clear fit:
|
||||
- `Improve developer resources` for examples, documentation, comments, starter kits, and `npm create tldraw`.
|
||||
- `Improve automations` for GitHub Actions, review bots, CI/CD, and automation work.
|
||||
8. Manage the `More Info Needed` label consistently:
|
||||
- Add it only when a critical question is unanswered or the confidence line says the issue still needs more information.
|
||||
- Remove it when the issue is ready to get started, even if non-blocking considerations remain.
|
||||
9. Respond to the user with the issue URL, your readback, and the list of open questions. Ask them directly, leading with any critical question.
|
||||
10. Interrogate the user. After each reply:
|
||||
- Update the issue body: write the user's answer beneath the relevant question, revise the readback, recompute the confidence status line, and add or drop questions as their answers reveal new gaps or close old ones.
|
||||
- Reconsider the issue's framing in light of the new context. If an answer changes what the issue actually is, update the title (`gh issue edit --title`), the issue type (the script from step 6), or its labels to match — for example, when a reported bug turns out to be a feature request, or the real problem is narrower than the title suggests.
|
||||
- If the user corrects your framing, especially with phrases like "actually" or "well actually," treat it as a signal to rewrite the readback around the corrected distinction. Remove or soften obsolete assumptions before asking more questions.
|
||||
|
||||
```bash
|
||||
gh issue edit <issue-number> --repo tldraw/tldraw --body "..."
|
||||
```
|
||||
|
||||
- Keep going, one round at a time, until the readback and question answers hold enough of the user's intent and context to be worked on.
|
||||
11. The issue is ready when no critical question is unanswered and it holds enough of the user's intent to be worked on. Never declare it ready while a critical question is open, regardless of how complete the rest is.
|
||||
- Low-priority questions the user has chosen to defer may stay open; readiness does not require answering them. Mark them as `_Deferred by user; not blocking implementation._` rather than leaving `_Awaiting answer._` placeholders.
|
||||
- When ready, remove the `More Info Needed` label, tell the user the issue can be picked up, and set the confidence line accordingly, e.g. `Confidence: 84%, ready to get started.` Leave the readback, every question, and the confidence line in the issue as a record of the discussion. Do not delete them.
|
||||
|
||||
## Rules
|
||||
|
||||
- Always create the issue before interrogating the user, so they can track it from the first reply.
|
||||
- Ask only questions that capture the user's intent or context. Do not offload work you could do yourself.
|
||||
- Update the issue after every reply rather than batching answers at the end.
|
||||
36
skills/issue/scripts/set-issue-type.sh
Executable file
36
skills/issue/scripts/set-issue-type.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Set a GitHub issue's type (Bug, Feature, Example, Task, ...) via GraphQL.
|
||||
# The `gh issue create --type` flag is not reliable across versions, so we
|
||||
# resolve the issue and type node IDs and run the updateIssueIssueType mutation.
|
||||
#
|
||||
# Usage: set-issue-type.sh <issue-number> <type-name> [owner/repo]
|
||||
# set-issue-type.sh 1234 Bug
|
||||
# set-issue-type.sh 1234 Feature tldraw/tldraw
|
||||
set -euo pipefail
|
||||
|
||||
issue_number="${1:?usage: set-issue-type.sh <issue-number> <type-name> [owner/repo]}"
|
||||
type_name="${2:?usage: set-issue-type.sh <issue-number> <type-name> [owner/repo]}"
|
||||
repo="${3:-tldraw/tldraw}"
|
||||
owner="${repo%/*}"
|
||||
name="${repo#*/}"
|
||||
|
||||
issue_id=$(gh issue view "$issue_number" --repo "$repo" --json id --jq '.id')
|
||||
|
||||
type_id=$(gh api graphql \
|
||||
-f owner="$owner" -f name="$name" \
|
||||
-f query='query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issueTypes(first:20){nodes{id name}}}}' \
|
||||
--jq ".data.repository.issueTypes.nodes[] | select(.name == \"$type_name\") | .id")
|
||||
|
||||
if [ -z "$type_id" ]; then
|
||||
echo "error: no issue type named '$type_name' in $repo" >&2
|
||||
echo "available types:" >&2
|
||||
gh api graphql -f owner="$owner" -f name="$name" \
|
||||
-f query='query($owner:String!,$name:String!){repository(owner:$owner,name:$name){issueTypes(first:20){nodes{name}}}}' \
|
||||
--jq '.data.repository.issueTypes.nodes[].name' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gh api graphql \
|
||||
-f issueId="$issue_id" -f issueTypeId="$type_id" \
|
||||
-f query='mutation($issueId:ID!,$issueTypeId:ID!){updateIssueIssueType(input:{issueId:$issueId,issueTypeId:$issueTypeId}){issue{number issueType{name}}}}' \
|
||||
--jq '"set #\(.data.updateIssueIssueType.issue.number) type to \(.data.updateIssueIssueType.issue.issueType.name)"'
|
||||
247
skills/pr-walkthrough/SKILL.md
Normal file
247
skills/pr-walkthrough/SKILL.md
Normal file
@@ -0,0 +1,247 @@
|
||||
---
|
||||
name: pr-walkthrough
|
||||
description: Create a narrated video walkthrough of a pull request with code slides and audio narration. Use when asked to create a PR walkthrough, PR video, or walkthrough video.
|
||||
---
|
||||
|
||||
# PR walkthrough video
|
||||
|
||||
Create a narrated walkthrough video for a pull request. This is designed to be an internal artifact, providing the same benefit as would a loom video created by the pull request's author — walking through the code changes, explaining what was done and why, so that anyone watching can understand the PR quickly.
|
||||
|
||||
**Input:** A GitHub pull request URL (e.g., `https://github.com/tldraw/tldraw/pull/7924`). If given just a PR number or other description, assume that the PR is on the tldraw/tldraw repository.
|
||||
|
||||
**Output:** An MP4 video at 1280×720 (30 fps) with audio narration, whisper-timed yellow-on-black subtitles, and standardized intro / outro slides, saved to `out/pr-<number>-walkthrough.mp4`.
|
||||
|
||||
All intermediate files (audio, manifest, scripts) go in `tmp/pr-<number>/` relative to this skill directory. This directory is gitignored. Only the final `.mp4` lives at `out/`.
|
||||
|
||||
Run commands that reference `./scripts` or `./video` from this skill directory.
|
||||
|
||||
## Philosophy
|
||||
|
||||
**This is a walkthrough from the author's perspective.** The goal is the same as if the PR author sat down with someone and walked them through the changes — showing specific code, explaining what changed and why, in an order that builds understanding. The viewer should come away understanding both _what the code does_ and _how to think about the changes_.
|
||||
|
||||
This means:
|
||||
|
||||
- **The narration drives everything.** Write the walkthrough narration first, as a continuous explanation of the PR. Then figure out what should be on screen at each moment to support what's being said.
|
||||
- **Show the code.** The default visual is a code diff or source file. Text slides are the exception (intro, brief transitions, outro), not the rule. When the narration talks about a function, the viewer should be looking at that function.
|
||||
- **Walk through changes in a logical order**, not necessarily file order or commit order — but always anchored to concrete code, not abstract descriptions.
|
||||
- **Explain the "why", not just the "what".** The code on screen shows what changed. The narration adds the reasoning — why this approach, what problem it solves, what edge cases it handles.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Understand the PR
|
||||
|
||||
Read the PR commits, diff, and description. Understand the narrative arc:
|
||||
|
||||
- What problem does this solve?
|
||||
- What's the approach?
|
||||
- What are the key mechanisms?
|
||||
|
||||
```bash
|
||||
gh pr view <number> --json title,body,commits
|
||||
git log main..HEAD --oneline
|
||||
git diff main..HEAD --stat
|
||||
```
|
||||
|
||||
**Skip generated files.** When reading the diff, ignore files that are auto-generated rather than hand-edited. These add noise without informing the walkthrough. Common examples in this repo:
|
||||
|
||||
- `**/api-report.md`, `**/api-report.api.md` — generated API surface reports
|
||||
- `**/*.api.json`, `**/temp/*.api.json` — API extractor output
|
||||
- `apps/docs/content/reference/**` — generated reference docs
|
||||
- `yarn.lock`, `package-lock.json` — lockfiles
|
||||
- `**/CHANGELOG.md` — auto-generated changelogs
|
||||
- Any file the repo's tooling regenerates (snapshots, schema dumps, bundled assets)
|
||||
|
||||
If unsure whether a file is generated, check for a header comment like "DO NOT EDIT" or check whether the repo has a generator command that produces it. Filter these out when picking which files to feature in `diff`/`code` slides.
|
||||
|
||||
### Step 2: Write the narration
|
||||
|
||||
Write the narration as continuous text, broken into logical segments. Each segment is a beat of the walkthrough — a concept, a change, or a group of related changes. Save this as `tmp/pr-<number>/SCRIPT.md`.
|
||||
|
||||
The narration should read like the author explaining the PR to a colleague: "So here's what we're doing... The core problem was X... The approach I took was Y... If you look at this function here..."
|
||||
|
||||
Structure: intro → context/problem → code walkthrough → summary. See **Script structure** below.
|
||||
|
||||
If the commits are simple and organized well (often on a branch with `-clean` in its name), you can follow their commit messages and descriptions to guide your narration. Otherwise, examine the code and create your own narrative. Introduce concepts in an order that builds on previous ones.
|
||||
|
||||
Avoid redundancy, especially between intro and first content segment.
|
||||
|
||||
### Step 3: Generate audio and timestamps
|
||||
|
||||
Generate per-segment audio clips with one TTS call per segment. This avoids chunking, alignment, and splitting entirely — each segment is short enough for a single reliable TTS call.
|
||||
|
||||
Write a `narration.json` file, then run the `generate-audio.sh` CLI tool:
|
||||
|
||||
```bash
|
||||
./scripts/generate-audio.sh narration.json tmp/pr-<number>/
|
||||
```
|
||||
|
||||
**API key:** Sourced automatically from the repo `.env` file (`GEMINI_API_KEY`).
|
||||
|
||||
#### Narration JSON format
|
||||
|
||||
```json
|
||||
{
|
||||
"style": "Read the following walkthrough narration in a calm, steady, professional tone. Speak at a measured pace as if the author of a pull request were walking a colleague through the code changes.",
|
||||
"voice": "Iapetus",
|
||||
"slides": [
|
||||
"This pull request adds group-aware binding resolution to the arrow tool...",
|
||||
"The core problem was that arrow bindings broke when the target shape...",
|
||||
"If you look at the getBindingTarget method in ArrowBindingUtil.ts..."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **`style`** — Voice persona and pacing instructions. Keep it short and specific.
|
||||
- **`voice`** — Gemini voice name (default: `Iapetus`).
|
||||
- **`slides`** — Array of narration text, one entry per segment.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. For each segment, the script builds a prompt: style preamble + segment text.
|
||||
2. One API call to `gemini-3.1-flash-tts-preview` per segment generates a WAV clip directly.
|
||||
3. Each clip is validated (duration sanity check vs word count) and retried automatically if the output is bad.
|
||||
4. Leading/trailing silence is trimmed from each clip.
|
||||
|
||||
**Output:** Per-segment audio clips (`audio-00.wav`, ...) and a `durations.json` file mapping each audio filename to its duration in seconds.
|
||||
|
||||
**Dependencies:** ffmpeg / ffprobe. No Python packages required beyond the standard library.
|
||||
|
||||
**Do NOT use** `[pause long]` or `[pause medium]` markup tags in the narration text — the model may read them aloud literally.
|
||||
|
||||
### Step 4: Write the manifest
|
||||
|
||||
Read `durations.json` from step 3 for each clip length, then write `manifest.json` alongside the audio files. It lists one entry per slide, in order, each with its `type`, `audio`, and `durationInSeconds`.
|
||||
|
||||
See `references/manifest.md` for the slide types and their fields.
|
||||
|
||||
### Step 5: Render the video
|
||||
|
||||
Run the `render.sh` script:
|
||||
|
||||
```bash
|
||||
./video/render.sh \
|
||||
tmp/pr-<number>/manifest.json \
|
||||
out/pr-<number>-walkthrough.mp4
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
1. Copies referenced audio/image files into `video/assets/`.
|
||||
2. Runs whisper transcription on each audio file → `video/transcripts/audio-NN.json` (idempotent — only re-transcribes if the audio is newer than the existing transcript).
|
||||
3. Runs `build.mjs <manifest>` to generate `video/index.html` — a hyperframes composition with timed clips for every slide, GSAP timeline for transitions and code-focus pans, and yellow-on-black caption clips with start/end times derived from the whisper transcripts.
|
||||
4. Lints the composition and renders 1920×1080 frames via `npx hyperframes render`.
|
||||
5. Downscales to 1280×720 / 30fps and recompresses with ffmpeg (CRF 26 + AAC 96k) for the final small-but-sharp MP4.
|
||||
|
||||
**Dependencies:** Node.js 22+, ffmpeg, Python 3 (used by `render.sh` to parse the manifest). `hyperframes` is invoked via `npx --yes`, so no install step. Whisper runs locally (small.en model, ~150MB on first download).
|
||||
|
||||
#### Caption sync via whisper
|
||||
|
||||
Captions appear as yellow text on a solid black pill, anchored to the bottom of the frame. Their start/end times come from word-level whisper transcripts grouped into 5–7 word chunks, breaking early on natural pauses (>450ms gaps = sentence boundaries). One implication: whisper transcribes brand/code names phonetically — "tldraw" → "TL Draw", "OverlayUtil" → "overlay util". This is acceptable for captions but could be normalized later via a substitution table in `build.mjs`.
|
||||
|
||||
#### File size knobs
|
||||
|
||||
The default render targets ~30–60 MB for an 8-minute video. To tune:
|
||||
|
||||
- `--crf <n>` in the ffmpeg downscale step inside `render.sh` — 22 is near-lossless, 26 is the default, 30+ is much smaller. CRF 28–30 is a good range for a docs-quality result.
|
||||
- The 1080p hyperframes render uses `-q draft --crf 30` to keep the intermediate file small (the downscale dominates final size anyway).
|
||||
|
||||
## File organization
|
||||
|
||||
Final output lives in this skill directory. All intermediate files go in `tmp/` (gitignored):
|
||||
|
||||
```
|
||||
pr-walkthrough/
|
||||
├── SKILL.md # This file
|
||||
├── scripts/ # CLI tools (checked in)
|
||||
│ └── generate-audio.sh # narration.json → per-slide WAVs + durations.json
|
||||
├── video/ # Hyperframes project (checked in)
|
||||
│ ├── hyperframes.json # hyperframes config
|
||||
│ ├── meta.json # project meta
|
||||
│ ├── build.mjs # manifest.json → index.html composition
|
||||
│ ├── render.sh # manifest.json → 720p MP4 (full pipeline)
|
||||
│ ├── assets/ # Auto-populated at render time (gitignored)
|
||||
│ ├── transcripts/ # Whisper word-level JSON (gitignored, cached)
|
||||
│ └── renders/ # Intermediate 1080p renders (gitignored)
|
||||
├── out/ # Final outputs (gitignored)
|
||||
│ └── pr-XXXX-walkthrough.mp4
|
||||
└── tmp/ # Intermediate files (gitignored)
|
||||
└── pr-XXXX/
|
||||
├── SCRIPT.md # Narration script
|
||||
├── narration.json # Input to generate-audio.sh
|
||||
├── durations.json # Audio filename → duration in seconds
|
||||
├── manifest.json # Input to render.sh
|
||||
└── audio-XX.wav # Per-segment audio clips
|
||||
```
|
||||
|
||||
## API configuration
|
||||
|
||||
- **Gemini API key:** Stored as `GEMINI_API_KEY` in the project root `.env` file. Used for TTS and audio alignment.
|
||||
- **TTS model:** `gemini-3.1-flash-tts-preview`
|
||||
- **TTS voice:** `Iapetus` (always)
|
||||
|
||||
## Script structure
|
||||
|
||||
The walkthrough follows a consistent narrative arc. Not every section needs its own segment — combine or skip sections based on the PR's complexity. The goal is 8-12 segments total, with the vast majority showing code.
|
||||
|
||||
### Intro (1 segment)
|
||||
|
||||
The intro card: tldraw logo + PR title + date. The narration should be a single sentence that frames what this PR does at a high level. Don't go into detail yet.
|
||||
|
||||
Manifest slide type: `intro`.
|
||||
|
||||
### Context (0-1 segments)
|
||||
|
||||
Brief orientation before diving into code. What was the situation before this PR? What problem or need motivated the work? Keep this short — just enough framing that the code walkthrough makes sense.
|
||||
|
||||
- Be concrete: "Arrow bindings broke when the target shape was inside a group" not "There were issues with bindings"
|
||||
- Name the area of the codebase affected
|
||||
|
||||
If the context can be explained while showing the first piece of relevant code, skip the standalone context segment and fold it into the first code segment.
|
||||
|
||||
Manifest slide type: `text` or `diff` (if showing the problematic code).
|
||||
|
||||
### Code walkthrough (6-10 segments)
|
||||
|
||||
The bulk of the video. Walk through the actual code changes, showing specific diffs and files while explaining what was done and why.
|
||||
|
||||
**Every segment should show code.** Use `diff` slides for changes and `code` slides for unchanged reference code.
|
||||
|
||||
Guidelines:
|
||||
|
||||
- **Name files and functions.** Every narrated segment should reference at least one specific file or function.
|
||||
- **Show the diff.** The visual for each segment should be the actual diff being discussed. Use `git diff main..HEAD -- path/to/file` to get the diff, then extract the relevant hunks.
|
||||
- **Order by understanding, not by file.** Present changes in the order that builds comprehension. If a new type is defined in one file and consumed in another, show the definition first.
|
||||
- **Explain the "why", not just the "what".** The diff shows _what_ changed — the narration adds the reasoning, the edge cases it handles, the alternatives that were considered.
|
||||
- **Skip boilerplate, but mention it.** Don't dedicate a segment to every import change or type export, but do mention in passing: "There are also some type exports added in `index.ts` — those are just re-exports of the new types we'll see next."
|
||||
- **Group related small changes.** If three files all got the same one-line fix, one segment can cover all three. Mention each file by name.
|
||||
|
||||
### Summary (1 segment)
|
||||
|
||||
Briefly recap what the PR accomplished. This is a short wrap-up — a sentence or two summarizing the overall change, mentioning any known limitations or follow-up work if relevant.
|
||||
|
||||
Manifest slide type: `text`.
|
||||
|
||||
### Outro (1 segment, silent)
|
||||
|
||||
The tldraw logo, 3 seconds of silence. Always include this as the final slide.
|
||||
|
||||
Manifest slide type: `outro` with `durationInSeconds: 3`.
|
||||
|
||||
## Narration writing tips
|
||||
|
||||
- **Be specific about code.** Say "In `BindingUtil.ts`, the `onAfterChange` handler now checks for group ancestors" — not "The binding system was updated." Name files and functions so the viewer can connect the narration to what's on screen.
|
||||
- **Each segment = one change or closely related group of changes.** If you can't point to a specific diff for the segment, it's probably too abstract.
|
||||
- **Write as the author.** The tone should be explanatory and natural — like walking someone through your work. "So the main thing here is..." or "The tricky part was..." are fine.
|
||||
- **Avoid redundancy** between intro and first content segment.
|
||||
- **Mention files that aren't shown.** If a PR touches 15 files but only 6 are interesting, briefly acknowledge the others: "The remaining changes are type exports and test fixtures."
|
||||
- Aim for **5-7 minutes** total narration.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Read all PR commits and understand the full diff
|
||||
- [ ] Write narration in SCRIPT.md (8-12 segments)
|
||||
- [ ] Generate per-segment audio (Iapetus voice)
|
||||
- [ ] Read durations.json to get per-segment durations
|
||||
- [ ] Write manifest.json with slide types, diffs/code, and audio references
|
||||
- [ ] Render video with render.sh
|
||||
- [ ] Verify final output: 1280×720 / 30 fps, audio synced, captions readable, outro present
|
||||
138
skills/pr-walkthrough/references/manifest.md
Normal file
138
skills/pr-walkthrough/references/manifest.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Manifest format
|
||||
|
||||
The manifest is a JSON file describing every slide in the video. It bridges the narration/audio step and the hyperframes renderer. Referenced from step 4 of `../SKILL.md`.
|
||||
|
||||
|
||||
Read the `durations.json` from step 3 to get the duration (in seconds) for each audio clip. Then write a `manifest.json` alongside the audio files:
|
||||
|
||||
```json
|
||||
{
|
||||
"pr": 7865,
|
||||
"slides": [
|
||||
{
|
||||
"type": "intro",
|
||||
"title": "Fix canvas-in-front z-index layering #7865",
|
||||
"date": "February 14, 2026",
|
||||
"audio": "audio-00.wav",
|
||||
"durationInSeconds": 3.2
|
||||
},
|
||||
{
|
||||
"type": "diff",
|
||||
"filename": "packages/editor/editor.css",
|
||||
"language": "css",
|
||||
"diff": "@@ -12,7 +12,7 @@\n --tl-z-canvas: 100;\n- --tl-z-canvas-in-front: 600;\n+ --tl-z-canvas-in-front: 250;\n --tl-z-shapes: 300;",
|
||||
"audio": "audio-01.wav",
|
||||
"durationInSeconds": 25.8
|
||||
},
|
||||
{
|
||||
"type": "code",
|
||||
"filename": "packages/editor/src/lib/Editor.ts",
|
||||
"language": "typescript",
|
||||
"code": "function getZIndex() {\n return 250\n}",
|
||||
"audio": "audio-02.wav",
|
||||
"durationInSeconds": 13.5
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"title": "Summary",
|
||||
"subtitle": "Moved canvas-in-front from z-index 600 to 250.",
|
||||
"audio": "audio-07.wav",
|
||||
"durationInSeconds": 7.4
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"title": "Key changes",
|
||||
"items": ["Lowered z-index", "Updated tests", "Added migration"],
|
||||
"audio": "audio-06.wav",
|
||||
"durationInSeconds": 10.2
|
||||
},
|
||||
{
|
||||
"type": "outro",
|
||||
"durationInSeconds": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Slide types
|
||||
|
||||
| Type | Required fields | Description |
|
||||
| --------- | ------------------------------------------------------------ | ---------------------------------- |
|
||||
| `intro` | `title`, `date`, `audio`, `durationInSeconds` | Logo + title + date |
|
||||
| `diff` | `filename`, `language`, `diff`, `audio`, `durationInSeconds` | Syntax-highlighted unified diff |
|
||||
| `code` | `filename`, `language`, `code`, `audio`, `durationInSeconds` | Syntax-highlighted source code |
|
||||
| `text` | `title`, `audio`, `durationInSeconds` | Title + optional `subtitle` |
|
||||
| `list` | `title`, `items`, `audio`, `durationInSeconds` | Title + numbered items |
|
||||
| `image` | `src`, `audio`, `durationInSeconds` | Pre-rendered image (fallback) |
|
||||
| `segment` | `title`, `durationInSeconds` | Silent title card between segments |
|
||||
| `outro` | `durationInSeconds` | Logo only, no audio |
|
||||
|
||||
#### Animated scroll with `focus`
|
||||
|
||||
For longer diffs or code (more than ~30 lines), the renderer keeps the font at a readable 16px and uses an animated viewport that scrolls between focus points. Add a `focus` array to `diff` or `code` slides:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "diff",
|
||||
"filename": "packages/editor/src/lib/Editor.ts",
|
||||
"language": "typescript",
|
||||
"diff": "... 60-line diff ...",
|
||||
"focus": [
|
||||
{ "line": 3, "at": 0 },
|
||||
{ "line": 25, "at": 0.4 },
|
||||
{ "line": 50, "at": 0.8 }
|
||||
],
|
||||
"audio": "audio-03.wav",
|
||||
"durationInSeconds": 30
|
||||
}
|
||||
```
|
||||
|
||||
- **`line`** — The line number (0-indexed into the parsed diff/code lines) to center on screen.
|
||||
- **`at`** — When to arrive at this position, as a fraction of the slide's duration (0 = start, 1 = end).
|
||||
|
||||
The viewport smoothly eases between focus points. Before the first point, it holds at the first position; after the last, it holds there.
|
||||
|
||||
**When to use focus:** Any diff or code slide with more than ~30 lines. Without focus, long content starts at the top and stays static — the viewer can't see the bottom. With focus, you guide the viewer's eye to the code being discussed at each moment.
|
||||
|
||||
**When to omit focus:** Short diffs (≤30 lines) fit on screen at 16px and don't need scrolling.
|
||||
|
||||
#### Writing diff fields
|
||||
|
||||
For `diff` slides, paste the **unified diff** for the relevant hunk(s). This is the output of `git diff` for that section of the file — including the `@@` hunk header and `+`/`-`/` ` line prefixes. The renderer parses these prefixes to apply green/red backgrounds and syntax highlighting.
|
||||
|
||||
To get a diff for a specific file:
|
||||
|
||||
```bash
|
||||
git diff main..HEAD -- path/to/file.ts
|
||||
```
|
||||
|
||||
Include only the relevant hunks, not the entire file diff. Strip the `diff --git` and `---`/`+++` header lines — start from the `@@` hunk header.
|
||||
|
||||
For `code` slides, paste the relevant source code (a function, a class, a section). No diff prefixes needed.
|
||||
|
||||
#### Segment title slides
|
||||
|
||||
Insert a **`segment` slide** before each content segment to introduce it — except before the intro and context/overview segments. This includes code walkthrough segments and the summary/conclusion. Each segment slide is **3 seconds of silence** with the segment title centered on screen.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "segment",
|
||||
"title": "Zoom state machine",
|
||||
"durationInSeconds": 3
|
||||
}
|
||||
```
|
||||
|
||||
These provide clear visual breaks between sections and give the viewer a moment to orient before each new topic.
|
||||
|
||||
#### Segment title labels on code/diff slides
|
||||
|
||||
Add a `title` field to `code` and `diff` slides to show a small label in the top-left corner identifying which segment the viewer is in. Use the same title as the preceding `segment` slide. This helps orient viewers, especially when a segment spans multiple slides.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "diff",
|
||||
"title": "Zoom state machine",
|
||||
"filename": "packages/editor/src/lib/ZoomTool.ts",
|
||||
...
|
||||
}
|
||||
```
|
||||
279
skills/pr-walkthrough/scripts/generate-audio.sh
Executable file
279
skills/pr-walkthrough/scripts/generate-audio.sh
Executable file
@@ -0,0 +1,279 @@
|
||||
#!/bin/bash
|
||||
# generate-audio.sh — Generate walkthrough narration audio from a JSON script.
|
||||
#
|
||||
# Generates one TTS call per segment, producing individual WAV clips directly.
|
||||
# No chunking, alignment, or splitting needed.
|
||||
#
|
||||
# Usage:
|
||||
# ./generate-audio.sh <script.json> [output-dir]
|
||||
#
|
||||
# Input JSON format:
|
||||
# {
|
||||
# "style": "Read in a calm, steady, professional tone...",
|
||||
# "voice": "Iapetus", (optional, default: Iapetus)
|
||||
# "slides": [
|
||||
# "Intro narration text...",
|
||||
# "Problem slide narration...",
|
||||
# "Approach narration...",
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
#
|
||||
# Output:
|
||||
# <output-dir>/audio-00.wav, audio-01.wav, ...
|
||||
# <output-dir>/durations.json
|
||||
#
|
||||
# Dependencies:
|
||||
# ffmpeg / ffprobe
|
||||
#
|
||||
# Environment:
|
||||
# GEMINI_API_KEY — required. Auto-sourced from .env if not set.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# --- Args ---
|
||||
SCRIPT_JSON="${1:?Usage: generate-audio.sh <script.json> [output-dir]}"
|
||||
OUTPUT_DIR="${2:-.}"
|
||||
|
||||
# Resolve relative paths
|
||||
[[ "$SCRIPT_JSON" != /* ]] && SCRIPT_JSON="$(pwd)/$SCRIPT_JSON"
|
||||
[[ "$OUTPUT_DIR" != /* ]] && OUTPUT_DIR="$(pwd)/$OUTPUT_DIR"
|
||||
|
||||
if [ ! -f "$SCRIPT_JSON" ]; then
|
||||
echo "Error: ${SCRIPT_JSON} not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
PYTHON="python3"
|
||||
|
||||
# --- API key ---
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo ".")
|
||||
|
||||
if [ -z "${GEMINI_API_KEY:-}" ]; then
|
||||
if [ -f "${REPO_ROOT}/.env" ]; then
|
||||
export $(grep '^GEMINI_API_KEY=' "${REPO_ROOT}/.env" | xargs) 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
GEMINI_API_KEY="${GEMINI_API_KEY:?Set GEMINI_API_KEY environment variable or add it to .env}"
|
||||
|
||||
# --- Config ---
|
||||
TTS_MODEL="gemini-3.1-flash-tts-preview"
|
||||
TTS_ENDPOINT="https://generativelanguage.googleapis.com/v1beta/models/${TTS_MODEL}:generateContent"
|
||||
SPEED=1.2 # Speed up narration (1.0 = no change)
|
||||
|
||||
# --- Run everything in Python for reliability ---
|
||||
"$PYTHON" - "$SCRIPT_JSON" "$OUTPUT_DIR" "$GEMINI_API_KEY" "$TTS_MODEL" "$TTS_ENDPOINT" "$SPEED" <<'PYTHON_SCRIPT'
|
||||
import json, sys, os, subprocess, base64, urllib.request, re
|
||||
|
||||
script_json = sys.argv[1]
|
||||
output_dir = sys.argv[2]
|
||||
api_key = sys.argv[3]
|
||||
tts_model = sys.argv[4]
|
||||
tts_endpoint = sys.argv[5]
|
||||
speed = float(sys.argv[6])
|
||||
|
||||
MAX_RETRIES = 2
|
||||
|
||||
def api_call(endpoint, body_dict):
|
||||
body = json.dumps(body_dict).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{endpoint}?key={api_key}",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
# --- Load narration ---
|
||||
with open(script_json) as f:
|
||||
data = json.load(f)
|
||||
|
||||
voice = data.get("voice", "Iapetus")
|
||||
slides = data["slides"]
|
||||
style = data.get("style",
|
||||
"Read the following in a calm, steady, professional tone. "
|
||||
"Speak at a measured pace.")
|
||||
|
||||
word_count = sum(len(s.split()) for s in slides)
|
||||
print(f"=== Generating narration audio ===")
|
||||
print(f" Voice: {voice}")
|
||||
print(f" Slides: {len(slides)}")
|
||||
print(f" Words: {word_count}")
|
||||
print()
|
||||
|
||||
def call_tts(prompt_text):
|
||||
"""Make a single TTS API call and return raw PCM bytes."""
|
||||
response = api_call(tts_endpoint, {
|
||||
"contents": [{"parts": [{"text": prompt_text}]}],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["AUDIO"],
|
||||
"speechConfig": {
|
||||
"voiceConfig": {
|
||||
"prebuiltVoiceConfig": {
|
||||
"voiceName": voice
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
error_msg = response.get("error", {}).get("message", "")
|
||||
if error_msg:
|
||||
raise RuntimeError(f"TTS API error: {error_msg}")
|
||||
|
||||
return base64.b64decode(response["candidates"][0]["content"]["parts"][0]["inlineData"]["data"])
|
||||
|
||||
def pcm_to_wav(pcm_bytes, out_wav):
|
||||
"""Convert raw 24kHz PCM to 48kHz WAV with speed adjustment."""
|
||||
pcm_tmp = out_wav + ".pcm"
|
||||
with open(pcm_tmp, "wb") as f:
|
||||
f.write(pcm_bytes)
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-f", "s16le", "-ar", "24000", "-ac", "1",
|
||||
"-i", pcm_tmp, "-af", f"atempo={speed}", "-ar", "48000", out_wav
|
||||
], capture_output=True, check=True)
|
||||
os.remove(pcm_tmp)
|
||||
|
||||
def get_duration(wav_path):
|
||||
"""Get duration of a WAV file in seconds."""
|
||||
result = subprocess.run(
|
||||
["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", wav_path],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return float(result.stdout.strip())
|
||||
|
||||
def validate_duration(wav_path, word_count):
|
||||
"""Check if audio duration is reasonable for the word count. Returns (ok, duration)."""
|
||||
dur = get_duration(wav_path)
|
||||
# At ~150 wpm before speed adjustment, expect word_count/150*60/speed seconds.
|
||||
# Allow generous bounds: 0.5x to 3x expected.
|
||||
expected = word_count / 150 * 60 / speed
|
||||
lower = expected * 0.3
|
||||
upper = expected * 3.0
|
||||
# For very short segments (< 15 words), just check it's under 30s
|
||||
if word_count < 15:
|
||||
return dur < 30, dur
|
||||
return lower <= dur <= upper, dur
|
||||
|
||||
# --- Generate one TTS call per segment ---
|
||||
durations = {}
|
||||
|
||||
for i, text in enumerate(slides):
|
||||
num = f"{i:02d}"
|
||||
out_path = os.path.join(output_dir, f"audio-{num}.wav")
|
||||
wc = len(text.split())
|
||||
prompt = f"{style}\n\n{text}"
|
||||
|
||||
ok = False
|
||||
for attempt in range(MAX_RETRIES + 1):
|
||||
try:
|
||||
label = f" [{num}] " + ("" if attempt == 0 else f"(retry {attempt}) ")
|
||||
print(f"{label}Generating ({wc} words)...", end=" ", flush=True)
|
||||
pcm_data = call_tts(prompt)
|
||||
pcm_to_wav(pcm_data, out_path)
|
||||
ok, dur = validate_duration(out_path, wc)
|
||||
if ok:
|
||||
print(f"{dur:.1f}s")
|
||||
durations[f"audio-{num}.wav"] = round(dur, 2)
|
||||
break
|
||||
else:
|
||||
expected = wc / 150 * 60 / speed
|
||||
print(f"{dur:.1f}s (expected ~{expected:.0f}s, retrying)")
|
||||
except (urllib.error.HTTPError, RuntimeError) as e:
|
||||
print(f"error: {e}")
|
||||
if attempt == MAX_RETRIES:
|
||||
print(f" [error] Segment {i} failed after {MAX_RETRIES + 1} attempts")
|
||||
sys.exit(1)
|
||||
|
||||
if not ok:
|
||||
# Use last attempt even if validation failed — warn but continue
|
||||
dur = get_duration(out_path)
|
||||
durations[f"audio-{num}.wav"] = round(dur, 2)
|
||||
print(f" [warn] Segment {i} audio may be unreliable ({dur:.1f}s for {wc} words)")
|
||||
|
||||
# --- Trim silence from each clip ---
|
||||
MAX_SILENCE = 0.15 # strip nearly all silence — segment title slides handle inter-slide pauses
|
||||
SILENCE_THRESHOLD = "-40dB"
|
||||
print()
|
||||
print("=== Trimming silence ===")
|
||||
|
||||
for i in range(len(slides)):
|
||||
num = f"{i:02d}"
|
||||
clip_path = os.path.join(output_dir, f"audio-{num}.wav")
|
||||
|
||||
# Detect silence at start and end using silencedetect
|
||||
detect = subprocess.run([
|
||||
"ffmpeg", "-i", clip_path, "-af",
|
||||
f"silencedetect=noise={SILENCE_THRESHOLD}:d=0.1",
|
||||
"-f", "null", "-"
|
||||
], capture_output=True, text=True)
|
||||
stderr = detect.stderr
|
||||
|
||||
# Get clip duration
|
||||
clip_dur = get_duration(clip_path)
|
||||
|
||||
# Parse silence periods
|
||||
silence_starts = re.findall(r'silence_start: ([\d.]+)', stderr)
|
||||
silence_ends = re.findall(r'silence_end: ([\d.]+)', stderr)
|
||||
|
||||
# Find leading silence: silence that starts at ~0
|
||||
trim_start = 0.0
|
||||
if silence_starts and float(silence_starts[0]) < 0.05:
|
||||
if silence_ends:
|
||||
leading_silence = float(silence_ends[0])
|
||||
if leading_silence > MAX_SILENCE:
|
||||
trim_start = leading_silence - MAX_SILENCE
|
||||
|
||||
# Find trailing silence: silence that starts near the end and extends to clip end.
|
||||
# Skip trailing trim on last segment — it's followed by the outro and
|
||||
# the silence detector often clips the final word.
|
||||
trim_end = clip_dur
|
||||
is_last = (i == len(slides) - 1)
|
||||
if not is_last and silence_starts:
|
||||
last_silence_start = float(silence_starts[-1])
|
||||
# Verify this silence actually extends to the clip end (not an internal pause).
|
||||
last_silence_is_trailing = True
|
||||
for se in silence_ends:
|
||||
se_val = float(se)
|
||||
if se_val > last_silence_start and se_val < clip_dur - 0.05:
|
||||
last_silence_is_trailing = False
|
||||
break
|
||||
if last_silence_is_trailing and last_silence_start > 0.05: # not the leading silence
|
||||
trailing_silence = clip_dur - last_silence_start
|
||||
if trailing_silence > MAX_SILENCE:
|
||||
trim_end = last_silence_start + MAX_SILENCE
|
||||
|
||||
if trim_start > 0 or trim_end < clip_dur:
|
||||
trimmed_path = clip_path + ".tmp.wav"
|
||||
subprocess.run([
|
||||
"ffmpeg", "-y", "-i", clip_path,
|
||||
"-ss", str(trim_start), "-to", str(trim_end),
|
||||
"-c", "copy", trimmed_path
|
||||
], capture_output=True)
|
||||
os.replace(trimmed_path, clip_path)
|
||||
new_dur = trim_end - trim_start
|
||||
durations[f"audio-{num}.wav"] = round(new_dur, 2)
|
||||
print(f" audio-{num}.wav: {clip_dur:.1f}s -> {new_dur:.1f}s (trimmed {clip_dur - new_dur:.1f}s)")
|
||||
else:
|
||||
print(f" audio-{num}.wav: {clip_dur:.1f}s (no trim needed)")
|
||||
|
||||
# --- Write durations.json ---
|
||||
durations_path = os.path.join(output_dir, "durations.json")
|
||||
with open(durations_path, "w") as f:
|
||||
json.dump(durations, f, indent=2)
|
||||
|
||||
total_dur = sum(durations.values())
|
||||
print(f"\n Wrote durations.json ({len(durations)} entries, {total_dur:.1f}s total)")
|
||||
|
||||
print()
|
||||
print("=== Done ===")
|
||||
PYTHON_SCRIPT
|
||||
|
||||
echo ""
|
||||
echo "Output:"
|
||||
ls -la "${OUTPUT_DIR}"/audio-*.wav 2>/dev/null || echo " (no files generated)"
|
||||
89
skills/pr-walkthrough/scripts/make-video.sh
Executable file
89
skills/pr-walkthrough/scripts/make-video.sh
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/bash
|
||||
# make-video.sh — Assemble walkthrough slides + audio into a final MP4.
|
||||
#
|
||||
# Usage:
|
||||
# ./make-video.sh <slide-dir> <output.mp4> [outro-duration]
|
||||
#
|
||||
# Expects in <slide-dir>:
|
||||
# slide-00.png, slide-01.png, ... (one per segment, including outro)
|
||||
# audio-00.wav, audio-01.wav, ... (one per narrated segment)
|
||||
#
|
||||
# The last slide PNG without a matching audio WAV is the silent outro.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
SLIDE_DIR="${1:?Usage: make-video.sh <slide-dir> <output.mp4> [outro-duration]}"
|
||||
OUTPUT="${2:?Usage: make-video.sh <slide-dir> <output.mp4> [outro-duration]}"
|
||||
OUTRO_DUR="${3:-3}"
|
||||
|
||||
# Resolve relative paths
|
||||
[[ "$SLIDE_DIR" != /* ]] && SLIDE_DIR="$(pwd)/$SLIDE_DIR"
|
||||
[[ "$OUTPUT" != /* ]] && OUTPUT="$(pwd)/$OUTPUT"
|
||||
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
TMPDIR_WORK=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR_WORK" EXIT
|
||||
|
||||
echo "=== Assembling video ==="
|
||||
echo " Slides: $SLIDE_DIR"
|
||||
echo " Output: $OUTPUT"
|
||||
|
||||
# Count slides and audio
|
||||
SLIDE_COUNT=$(ls "$SLIDE_DIR"/slide-*.png 2>/dev/null | wc -l | tr -d ' ')
|
||||
AUDIO_COUNT=$(ls "$SLIDE_DIR"/audio-*.wav 2>/dev/null | wc -l | tr -d ' ')
|
||||
|
||||
echo " Found $SLIDE_COUNT slides, $AUDIO_COUNT audio clips"
|
||||
echo " Last slide (no audio) = outro (${OUTRO_DUR}s)"
|
||||
|
||||
# Create per-segment videos
|
||||
CONCAT_LIST="$TMPDIR_WORK/concat.txt"
|
||||
> "$CONCAT_LIST"
|
||||
|
||||
for i in $(seq 0 $((SLIDE_COUNT - 1))); do
|
||||
NUM=$(printf "%02d" $i)
|
||||
SLIDE="$SLIDE_DIR/slide-${NUM}.png"
|
||||
AUDIO="$SLIDE_DIR/audio-${NUM}.wav"
|
||||
SEGMENT="$TMPDIR_WORK/segment-${NUM}.mp4"
|
||||
|
||||
if [ -f "$AUDIO" ]; then
|
||||
# Get audio duration
|
||||
DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$AUDIO")
|
||||
echo " segment-${NUM}: slide + audio (${DUR}s)"
|
||||
|
||||
# Create video segment: static image for duration of audio, with audio
|
||||
ffmpeg -y -loop 1 -i "$SLIDE" -i "$AUDIO" \
|
||||
-c:v libx264 -tune stillimage -pix_fmt yuv420p \
|
||||
-vf "scale=1600:900:force_original_aspect_ratio=decrease,pad=1600:900:(ow-iw)/2:(oh-ih)/2" \
|
||||
-c:a aac -b:a 192k -ar 48000 \
|
||||
-shortest -movflags +faststart \
|
||||
"$SEGMENT" 2>/dev/null
|
||||
else
|
||||
# Silent outro slide
|
||||
echo " segment-${NUM}: silent outro (${OUTRO_DUR}s)"
|
||||
ffmpeg -y -loop 1 -i "$SLIDE" -f lavfi -i anullsrc=r=48000:cl=mono \
|
||||
-c:v libx264 -tune stillimage -pix_fmt yuv420p \
|
||||
-vf "scale=1600:900:force_original_aspect_ratio=decrease,pad=1600:900:(ow-iw)/2:(oh-ih)/2" \
|
||||
-c:a aac -b:a 192k -ar 48000 \
|
||||
-t "$OUTRO_DUR" -movflags +faststart \
|
||||
"$SEGMENT" 2>/dev/null
|
||||
fi
|
||||
|
||||
echo "file '$SEGMENT'" >> "$CONCAT_LIST"
|
||||
done
|
||||
|
||||
# Concatenate all segments
|
||||
echo ""
|
||||
echo " Concatenating ${SLIDE_COUNT} segments..."
|
||||
ffmpeg -y -f concat -safe 0 -i "$CONCAT_LIST" \
|
||||
-c copy -movflags +faststart \
|
||||
"$OUTPUT" 2>/dev/null
|
||||
|
||||
# Report
|
||||
FINAL_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUTPUT")
|
||||
FINAL_SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo " Output: $OUTPUT"
|
||||
echo " Duration: ${FINAL_DUR}s"
|
||||
echo " Size: $FINAL_SIZE"
|
||||
6
skills/pr-walkthrough/video/.gitignore
vendored
Normal file
6
skills/pr-walkthrough/video/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Generated by render.sh
|
||||
index.html
|
||||
assets/
|
||||
transcripts/
|
||||
renders/
|
||||
node_modules/
|
||||
793
skills/pr-walkthrough/video/build.mjs
Normal file
793
skills/pr-walkthrough/video/build.mjs
Normal file
File diff suppressed because one or more lines are too long
9
skills/pr-walkthrough/video/hyperframes.json
Normal file
9
skills/pr-walkthrough/video/hyperframes.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
|
||||
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
|
||||
"paths": {
|
||||
"blocks": "compositions",
|
||||
"components": "compositions/components",
|
||||
"assets": "assets"
|
||||
}
|
||||
}
|
||||
4
skills/pr-walkthrough/video/meta.json
Normal file
4
skills/pr-walkthrough/video/meta.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"id": "pr-walkthrough",
|
||||
"name": "pr-walkthrough"
|
||||
}
|
||||
160
skills/pr-walkthrough/video/render.sh
Executable file
160
skills/pr-walkthrough/video/render.sh
Executable file
@@ -0,0 +1,160 @@
|
||||
#!/bin/bash
|
||||
# render.sh — Render a pr-walkthrough video from a manifest JSON file using
|
||||
# hyperframes. The pipeline:
|
||||
# 1. Copy referenced audio/image files into ./assets/
|
||||
# 2. Run whisper transcription on each audio file → ./transcripts/
|
||||
# 3. Run build.mjs to generate index.html
|
||||
# 4. Lint + render via npx hyperframes (1080p/30fps)
|
||||
# 5. Downscale + recompress to 1280×720 with ffmpeg → final MP4
|
||||
#
|
||||
# Usage:
|
||||
# ./render.sh <manifest.json> <output.mp4>
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST="${1:?Usage: render.sh <manifest.json> <output.mp4>}"
|
||||
OUTPUT="${2:?Usage: render.sh <manifest.json> <output.mp4>}"
|
||||
|
||||
# Resolve relative paths
|
||||
[[ "$MANIFEST" != /* ]] && MANIFEST="$(pwd)/$MANIFEST"
|
||||
[[ "$OUTPUT" != /* ]] && OUTPUT="$(pwd)/$OUTPUT"
|
||||
|
||||
if [ ! -f "$MANIFEST" ]; then
|
||||
echo "Error: Manifest not found: $MANIFEST" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ASSETS_DIR="$SCRIPT_DIR/assets"
|
||||
TRANSCRIPTS_DIR="$SCRIPT_DIR/transcripts"
|
||||
RENDERS_DIR="$SCRIPT_DIR/renders"
|
||||
MANIFEST_DIR="$(dirname "$MANIFEST")"
|
||||
|
||||
mkdir -p "$ASSETS_DIR" "$TRANSCRIPTS_DIR" "$RENDERS_DIR"
|
||||
mkdir -p "$(dirname "$OUTPUT")"
|
||||
|
||||
echo "=== Rendering walkthrough video ==="
|
||||
echo " Manifest: $MANIFEST"
|
||||
echo " Output: $OUTPUT"
|
||||
echo ""
|
||||
|
||||
# --- 1. Extract referenced audio/image filenames from the manifest ---------
|
||||
REFERENCED_FILES=$(python3 -c "
|
||||
import json, sys
|
||||
with open(sys.argv[1]) as f:
|
||||
m = json.load(f)
|
||||
files = set()
|
||||
for s in m['slides']:
|
||||
if 'audio' in s and s['audio']: files.add(s['audio'])
|
||||
if 'src' in s and s['src']: files.add(s['src'])
|
||||
print('\n'.join(sorted(files)))
|
||||
" "$MANIFEST")
|
||||
|
||||
# --- 2. Copy referenced files into ./assets/ -------------------------------
|
||||
echo " [1/5] Copying assets..."
|
||||
# Clear assets to avoid stale files from previous runs.
|
||||
rm -rf "$ASSETS_DIR"
|
||||
mkdir -p "$ASSETS_DIR"
|
||||
for FILE in $REFERENCED_FILES; do
|
||||
SRC="$MANIFEST_DIR/$FILE"
|
||||
if [ -f "$SRC" ]; then
|
||||
cp "$SRC" "$ASSETS_DIR/$FILE"
|
||||
else
|
||||
echo " Warning: Referenced file not found: $SRC" >&2
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 3. Whisper transcribe each audio file (idempotent per file) ----------
|
||||
echo " [2/5] Transcribing audio (whisper)..."
|
||||
for WAV in "$ASSETS_DIR"/*.wav; do
|
||||
[ -f "$WAV" ] || continue
|
||||
BASE=$(basename "$WAV" .wav)
|
||||
OUT_JSON="$TRANSCRIPTS_DIR/$BASE.json"
|
||||
# Skip if already transcribed and source is older
|
||||
if [ -f "$OUT_JSON" ] && [ "$OUT_JSON" -nt "$WAV" ]; then
|
||||
continue
|
||||
fi
|
||||
echo " transcribing $BASE..."
|
||||
(cd "$SCRIPT_DIR" && npx --yes hyperframes transcribe "assets/$BASE.wav" --json >/dev/null)
|
||||
# hyperframes writes to ./transcript.json — move to per-audio location
|
||||
if [ -f "$SCRIPT_DIR/transcript.json" ]; then
|
||||
mv "$SCRIPT_DIR/transcript.json" "$OUT_JSON"
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 4. Generate index.html ------------------------------------------------
|
||||
echo " [3/5] Building composition..."
|
||||
(cd "$SCRIPT_DIR" && node build.mjs "$MANIFEST")
|
||||
|
||||
# --- 5. Lint (warn but don't fail) ------------------------------------------
|
||||
(cd "$SCRIPT_DIR" && npx --yes hyperframes lint) || {
|
||||
echo " Warning: lint reported issues (continuing)" >&2
|
||||
}
|
||||
|
||||
# --- 6. Render at 1080p/30fps with hyperframes -----------------------------
|
||||
# hyperframes can hang on telemetry/cleanup after writing the MP4. Run it in
|
||||
# the background, wait for the file to appear and stop growing, then kill the
|
||||
# process and continue.
|
||||
echo " [4/5] Rendering 1080p frames..."
|
||||
RENDER_NAME="walkthrough-$$"
|
||||
TEMP_RENDER="$RENDERS_DIR/$RENDER_NAME.mp4"
|
||||
rm -f "$TEMP_RENDER"
|
||||
(cd "$SCRIPT_DIR" && npx --yes hyperframes render \
|
||||
-q draft --crf 30 \
|
||||
-o "renders/$RENDER_NAME.mp4") >/dev/null 2>&1 &
|
||||
RENDER_PID=$!
|
||||
|
||||
# Poll until the MP4 exists and its size is stable for 3 consecutive checks
|
||||
# (file fully written), then kill the parent npx and any child processes.
|
||||
PREV_SIZE=-1
|
||||
STABLE=0
|
||||
while kill -0 "$RENDER_PID" 2>/dev/null; do
|
||||
if [ -f "$TEMP_RENDER" ]; then
|
||||
SIZE=$(stat -f '%z' "$TEMP_RENDER" 2>/dev/null || echo 0)
|
||||
if [ "$SIZE" -gt 0 ] && [ "$SIZE" -eq "$PREV_SIZE" ]; then
|
||||
STABLE=$((STABLE + 1))
|
||||
if [ "$STABLE" -ge 3 ]; then break; fi
|
||||
else
|
||||
STABLE=0
|
||||
fi
|
||||
PREV_SIZE=$SIZE
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Reap any lingering hyperframes/node children, then the npx wrapper.
|
||||
pkill -P "$RENDER_PID" 2>/dev/null || true
|
||||
kill "$RENDER_PID" 2>/dev/null || true
|
||||
wait "$RENDER_PID" 2>/dev/null || true
|
||||
|
||||
if [ ! -f "$TEMP_RENDER" ]; then
|
||||
echo "Error: hyperframes did not produce $TEMP_RENDER" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 7. Downscale 1080p → 720p, recompress for smaller file ---------------
|
||||
echo " [5/5] Downscaling to 720p / 30fps..."
|
||||
ffmpeg -y -i "$TEMP_RENDER" \
|
||||
-vf "scale=1280:720:flags=lanczos,fps=30" \
|
||||
-c:v libx264 -preset slow -crf 26 -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 96k -ar 48000 \
|
||||
-movflags +faststart \
|
||||
"$OUTPUT" 2>/dev/null
|
||||
|
||||
rm -f "$TEMP_RENDER"
|
||||
|
||||
# --- Report ------------------------------------------------------------------
|
||||
if [ -f "$OUTPUT" ]; then
|
||||
FINAL_DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$OUTPUT" 2>/dev/null || echo "?")
|
||||
FINAL_SIZE=$(ls -lh "$OUTPUT" | awk '{print $5}')
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo " Output: $OUTPUT"
|
||||
echo " Resolution: 1280×720"
|
||||
echo " FPS: 30"
|
||||
echo " Duration: ${FINAL_DUR}s"
|
||||
echo " Size: $FINAL_SIZE"
|
||||
else
|
||||
echo "Error: render failed — output file not created" >&2
|
||||
exit 1
|
||||
fi
|
||||
53
skills/pr/SKILL.md
Normal file
53
skills/pr/SKILL.md
Normal file
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: pr
|
||||
description: Create or update a pull request for the current branch in the tldraw repository. Use when the user invokes pr, asks to create a PR, update an existing PR, push current branch changes for review, or prepare a pull request.
|
||||
---
|
||||
|
||||
# PR
|
||||
|
||||
Create or update a pull request for the current branch.
|
||||
|
||||
Use `../write-pr/SKILL.md` as the standards reference for PR titles, descriptions, release notes, API changes, code changes tables, and human-note preservation.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Gather context:
|
||||
- Current branch: `git branch --show-current`
|
||||
- Working tree: `git status --short`
|
||||
- Existing PR: `gh pr view --json number,title,url 2>/dev/null`
|
||||
- Recent branch commits: `git log main..HEAD --oneline 2>/dev/null || git log -3 --oneline`
|
||||
2. Check for overlapping work so we don't step on a teammate's toes:
|
||||
- List open PRs touching the same area: `gh pr list --state open --json number,title,url,author,headRefName,updatedAt`, and search for related work: `gh pr list --search "<keywords>" --state open`.
|
||||
- Compare their changed files against ours (`gh pr diff <number> --stat`) to judge real overlap, not just a shared filename.
|
||||
- If someone already has a PR open for this: prefer building on their work over racing it. Offer to base our branch on theirs, contribute a review or a follow-up commit, or hand our changes over. Only open a competing PR when the approaches genuinely diverge, and when we do, link to theirs and explain how ours differs so the choice is easy for reviewers.
|
||||
- Surface what you found to the user before proceeding when there's meaningful overlap.
|
||||
3. Prepare the branch:
|
||||
- If on `main`, create a new branch with a descriptive name.
|
||||
- Commit relevant changes, excluding secrets and explicitly private content.
|
||||
- Push the branch to the remote. Never force push.
|
||||
4. Run an initial review pass before asking a human to look. Spin out a few subagents in parallel over the diff (`git diff main...HEAD`), each with a focused lens, then fold their findings into concrete fixes:
|
||||
- Does the change actually solve the stated problem, end to end, rather than papering over a symptom?
|
||||
- Does it leave the codebase better than we found it — clearer names, no dead or duplicated code, no drive-by regressions?
|
||||
- Any weird abstractions, premature generality, or unnecessary code that a reviewer would flag? Prefer the smaller, more direct version.
|
||||
- Sweep the diff's added comments for slop, following `../write-pr/SKILL.md` § The comment sweep. On an update, this catches whatever the branch grew since the PR was opened, which on a long-lived branch is most of it.
|
||||
- Fix what's clearly worth fixing so the human review starts from a strong diff. If a finding needs a product or design call, raise it with the user instead of guessing.
|
||||
- Commit and push any fixes from this pass so the remote branch matches before the PR is created, updated, or shared. Never force push.
|
||||
5. If no PR exists, create one with `gh pr create`.
|
||||
6. If a PR exists, read it with `gh pr view --json title,body,labels,number` and inspect the changed-file summary with `gh pr diff --stat`.
|
||||
7. Update the title or body with `gh pr edit` if the existing PR does not match the current diff or the `write-pr` standards.
|
||||
8. Search for related issues and link them in the PR description with `Closes #123` or `Relates to #123` where appropriate.
|
||||
9. Share the PR URL with the user.
|
||||
|
||||
## Handling problems
|
||||
|
||||
Committing automatically runs hooks. Fix formatting, lint, type, or import issues when the fix is mechanical.
|
||||
|
||||
If a hook failure requires meaningful product or implementation decisions, stop and ask the user how to proceed.
|
||||
|
||||
Never force commit or force push.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `../write-pr/SKILL.md` for all PR content standards.
|
||||
- Do not include AI attribution in commit messages, PR titles, or PR descriptions.
|
||||
- Do not add yourself or an AI tool as a co-author.
|
||||
386
skills/review-docs/SKILL.md
Normal file
386
skills/review-docs/SKILL.md
Normal file
@@ -0,0 +1,386 @@
|
||||
---
|
||||
name: review-docs
|
||||
description: Review and improve tldraw SDK documentation against the voice and docs guides, scoring each article and iterating until it passes. Use when asked to review docs, audit an article in apps/docs/content, check whether documentation follows the style guide, or improve existing documentation.
|
||||
---
|
||||
|
||||
# Review documentation
|
||||
|
||||
This skill runs an evaluation and improvement loop on a documentation file.
|
||||
|
||||
**Target**: $ARGUMENTS
|
||||
|
||||
**Relevant skills**: `write-docs`
|
||||
|
||||
## Workflow overview
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ INITIALIZE: Create state file to track issues │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ EVALUATE (parallel) │
|
||||
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
|
||||
│ │ Style Agent │ │ Content Agent │ │
|
||||
│ │ (readability+voice) │ │ (completeness+accuracy) │ │
|
||||
│ └─────────────────────┘ └─────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ UPDATE STATE: Add new issues, verify fixed issues │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ SUMMARIZE: Present findings, ask user for next step │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌──────────────────┼──────────────────┐
|
||||
↓ ↓ ↓
|
||||
[User: improve] [User: complete] [User: done]
|
||||
↓ ↓ ↓
|
||||
┌──────────────────┐ ┌──────────────────┐ EXIT
|
||||
│ IMPROVE │ │ COMPLETE │
|
||||
│ (fix issues) │ │ (fix all, exit) │
|
||||
└──────────────────┘ └──────────────────┘
|
||||
↓ ↓
|
||||
LOOP → EVALUATE EXIT
|
||||
```
|
||||
|
||||
## State file
|
||||
|
||||
Create a state file in the scratchpad directory to track all issues across rounds. This prevents re-discovering the same issues and allows verification of fixes.
|
||||
|
||||
**Path**: `<scratchpad>/review-<filename>.md`
|
||||
|
||||
**Format**:
|
||||
|
||||
```markdown
|
||||
# Review tracker: [filename]
|
||||
|
||||
## Issue tracker
|
||||
|
||||
Status values: `pending` | `fixed` | `verified-fixed` | `not-fixed` | `wont-fix`
|
||||
|
||||
| ID | Issue | Type | Status | Round | Notes |
|
||||
| --- | ------------- | --------------------------- | -------------- | ----- | ---------------- |
|
||||
| 1 | [description] | Style/Accuracy/Completeness | pending | 1 | [details] |
|
||||
| 2 | [description] | Accuracy | verified-fixed | 1 | Fixed in round 1 |
|
||||
| 3 | [description] | Completeness | wont-fix | 2 | Out of scope |
|
||||
|
||||
## Round history
|
||||
|
||||
### Round 1
|
||||
|
||||
- Style: X/10, Voice: X/10, Completeness: X/10, Accuracy: X/10
|
||||
- **Total: X/40**
|
||||
```
|
||||
|
||||
**Status definitions**:
|
||||
|
||||
- `pending`: Issue discovered, not yet addressed
|
||||
- `fixed`: Improvement agent claims to have fixed it, needs verification
|
||||
- `verified-fixed`: Evaluation confirmed the fix was applied correctly
|
||||
- `not-fixed`: Evaluation found the fix wasn't applied correctly
|
||||
- `wont-fix`: False alarm, out of scope, or intentional (e.g., completeness issues that require documentation expansion)
|
||||
|
||||
## Step 1: Initial evaluation
|
||||
|
||||
For the **first round**, launch two subagents **in parallel** using the Task tool:
|
||||
|
||||
```
|
||||
// Single message with two Task tool calls:
|
||||
Task(subagent_type="general-purpose", model="opus", prompt="Style evaluation...")
|
||||
Task(subagent_type="general-purpose", model="opus", prompt="Content evaluation...")
|
||||
```
|
||||
|
||||
### Style agent prompt (round 1)
|
||||
|
||||
```
|
||||
Evaluate documentation style for: $ARGUMENTS
|
||||
|
||||
Read the shared guides relative to this skill directory, then read the target document:
|
||||
1. `../../VOICE.md`
|
||||
2. `../shared/docs-guide.md`
|
||||
3. $ARGUMENTS
|
||||
|
||||
Score these dimensions (0-10):
|
||||
|
||||
READABILITY - How clear and easy to understand is the writing?
|
||||
- Clear, direct sentences
|
||||
- Logical flow between sections
|
||||
- Appropriate use of code snippets and links
|
||||
- No unnecessary jargon
|
||||
|
||||
VOICE - How well does it follow the writing guide?
|
||||
- Confident assertions (no hedging)
|
||||
- Active voice, present tense
|
||||
- No AI writing tells (hollow importance, trailing gerunds, formulaic transitions)
|
||||
- Appropriate tone (expert-to-developer)
|
||||
- Sentence case headings
|
||||
|
||||
Important! Include as many high-priority fixes as needed.
|
||||
|
||||
Return in this exact format:
|
||||
|
||||
STYLE REPORT: [filename]
|
||||
|
||||
READABILITY: [score]/10
|
||||
- [specific issue or strength]
|
||||
- [specific issue or strength]
|
||||
|
||||
VOICE: [score]/10
|
||||
- [specific issue or strength]
|
||||
- [specific issue or strength]
|
||||
|
||||
PRIORITY FIXES:
|
||||
1. [Most important style issue]
|
||||
2. [Second most important]
|
||||
3. [Third most important]
|
||||
4. ...
|
||||
```
|
||||
|
||||
### Content agent prompt (round 1)
|
||||
|
||||
```
|
||||
Evaluate documentation content for: $ARGUMENTS
|
||||
|
||||
Read $ARGUMENTS, then verify claims against the source code in packages/editor/ and packages/tldraw/.
|
||||
|
||||
Score these dimensions (0-10):
|
||||
|
||||
COMPLETENESS - How thorough is the coverage?
|
||||
- Overview establishes purpose before mechanism
|
||||
- Key concepts explained with enough depth
|
||||
- Illustrative code snippets where needed
|
||||
- Links to relevant examples in apps/examples (if applicable)
|
||||
|
||||
ACCURACY - Is the technical content correct?
|
||||
- Code snippets are syntactically correct and use valid APIs
|
||||
- API references match actual implementation
|
||||
- Described behavior matches the code
|
||||
- No outdated information
|
||||
|
||||
For accuracy issues, include file:line references to the source code.
|
||||
|
||||
Important! Include as many high-priority fixes as needed. Make sure that all accuracy issues are flagged.
|
||||
|
||||
Return in this exact format:
|
||||
|
||||
CONTENT REPORT: [filename]
|
||||
|
||||
COMPLETENESS: [score]/10
|
||||
- [specific issue or strength]
|
||||
- [specific issue or strength]
|
||||
|
||||
ACCURACY: [score]/10
|
||||
- [specific issue with file:line reference if inaccurate]
|
||||
- [specific issue or strength]
|
||||
|
||||
PRIORITY FIXES:
|
||||
1. [Most important content issue]
|
||||
2. [Second most important]
|
||||
3. [Third most important]
|
||||
4. ...
|
||||
```
|
||||
|
||||
After round 1, **create the state file** with all discovered issues.
|
||||
|
||||
## Step 2: Summarize and prompt user
|
||||
|
||||
After both agents return, synthesize their reports into a summary:
|
||||
|
||||
```markdown
|
||||
## Evaluation: [filename]
|
||||
|
||||
| Dimension | Score | Key issue |
|
||||
| ------------ | ----- | ----------- |
|
||||
| Readability | X/10 | [one-liner] |
|
||||
| Voice | X/10 | [one-liner] |
|
||||
| Completeness | X/10 | [one-liner] |
|
||||
| Accuracy | X/10 | [one-liner] |
|
||||
| **Total** | X/40 | |
|
||||
|
||||
### Priority fixes
|
||||
|
||||
1. [Combined priority 1 from both reports]
|
||||
2. [Combined priority 2]
|
||||
3. [Combined priority 3]
|
||||
4. [Combined priority 4]
|
||||
5. [Combined priority 5]
|
||||
6. ...
|
||||
```
|
||||
|
||||
Then ask the user using AskUserQuestion:
|
||||
|
||||
- **Improve**: Make improvements based on findings, then re-evaluate
|
||||
- **Complete and finish**: Fix all remaining issues and exit (no re-evaluation)
|
||||
- **Done**: Exit the loop without making changes
|
||||
|
||||
## Step 3: Triage (before improvement)
|
||||
|
||||
Before running the improvement agent, review the pending issues with the user. Mark completeness issues that require adding new sections as `wont-fix` - these are documentation expansion, not review fixes.
|
||||
|
||||
Per `AGENTS.md` guidance:
|
||||
|
||||
> "Do what has been asked; nothing more, nothing less."
|
||||
> "Don't add features, refactor code, or make 'improvements' beyond what was asked."
|
||||
|
||||
The review skill improves existing content. Adding new sections is a separate task.
|
||||
|
||||
## Step 4: Improve
|
||||
|
||||
Launch a single improvement agent targeting **only pending issues**:
|
||||
|
||||
```
|
||||
Task(subagent_type="general-purpose", model="opus", prompt="Improve documentation...")
|
||||
```
|
||||
|
||||
### Improvement agent prompt
|
||||
|
||||
```
|
||||
Improve documentation based on specific tracked issues: $ARGUMENTS
|
||||
|
||||
Fix ONLY these pending issues:
|
||||
|
||||
| ID | Issue | Type | Notes |
|
||||
|----|-------|------|-------|
|
||||
[paste pending issues from state file]
|
||||
|
||||
Instructions:
|
||||
1. Read `../../VOICE.md` and `../shared/docs-guide.md` relative to this skill directory
|
||||
2. Read $ARGUMENTS
|
||||
|
||||
3. For each accuracy fix:
|
||||
- Read the source file referenced in the notes
|
||||
- Verify the correct API/behavior from the source
|
||||
- Apply the fix based on what the source code actually shows
|
||||
|
||||
5. Apply style fixes
|
||||
|
||||
6. Run prettier: yarn prettier --write $ARGUMENTS
|
||||
|
||||
DO NOT:
|
||||
- Add new sections
|
||||
- Expand the document
|
||||
- Fix issues not in the list above
|
||||
|
||||
Return a summary:
|
||||
|
||||
CHANGES MADE:
|
||||
|
||||
| ID | Fix applied | Verification |
|
||||
|----|-------------|--------------|
|
||||
| X | [description] | [source file:line checked] |
|
||||
| Y | [description] | n/a |
|
||||
```
|
||||
|
||||
After improvement, update the state file to mark issues as `fixed`.
|
||||
|
||||
## Step 4b: Complete and finish (alternative to Step 4)
|
||||
|
||||
If the user selects "Complete and finish", fix all remaining pending issues **without re-evaluating**. This is useful when the evaluation is satisfactory and the user wants to apply fixes and move on.
|
||||
|
||||
**Workflow**:
|
||||
|
||||
1. Run triage (same as Step 3) to mark out-of-scope items as `wont-fix`
|
||||
2. Launch the improvement agent (same prompt as Step 4)
|
||||
3. Update state file to mark issues as `fixed`
|
||||
4. **Exit the loop** - do not re-evaluate
|
||||
|
||||
This path trusts the improvement agent to apply fixes correctly and skips the verification cycle. Use when:
|
||||
|
||||
- The issues are straightforward style fixes
|
||||
- Time is limited and re-evaluation isn't worth the cost
|
||||
- Scores are already acceptable and only minor polish remains
|
||||
|
||||
## Step 5: Verification evaluation
|
||||
|
||||
For **subsequent rounds**, evaluation agents verify fixes AND find new issues:
|
||||
|
||||
### Style agent prompt (verification)
|
||||
|
||||
```
|
||||
Verify fixes and evaluate documentation: $ARGUMENTS
|
||||
|
||||
Read the state file first: [path to state file]
|
||||
|
||||
Then read the shared guides relative to this skill directory and the target document:
|
||||
1. `../../VOICE.md`
|
||||
2. `../shared/docs-guide.md`
|
||||
3. $ARGUMENTS
|
||||
|
||||
Your job:
|
||||
1. VERIFY fixes marked as "fixed" in the state file - confirm they were actually applied
|
||||
2. Score style dimensions (do NOT re-flag wont-fix issues)
|
||||
3. Flag only NEW issues not already in the state file
|
||||
|
||||
VERIFY THESE FIXES:
|
||||
[paste fixed style issues from state file]
|
||||
|
||||
Return in this format:
|
||||
|
||||
VERIFICATION REPORT:
|
||||
|
||||
| ID | Status | Notes |
|
||||
|----|--------|-------|
|
||||
| X | verified-fixed / not-fixed | [what you found] |
|
||||
|
||||
STYLE SCORES:
|
||||
READABILITY: [score]/10
|
||||
VOICE: [score]/10
|
||||
|
||||
NEW ISSUES (not already in state file):
|
||||
- [issue] or "None found"
|
||||
```
|
||||
|
||||
### Content agent prompt (verification)
|
||||
|
||||
```
|
||||
Verify fixes and evaluate documentation content: $ARGUMENTS
|
||||
|
||||
Read the state file first: [path to state file]
|
||||
|
||||
Then read $ARGUMENTS and verify claims against source code in packages/tldraw/.
|
||||
|
||||
Your job:
|
||||
1. VERIFY accuracy fixes marked as "fixed" in the state file
|
||||
2. Score content dimensions (do NOT re-flag wont-fix issues)
|
||||
3. Flag only NEW accuracy issues not already in the state file
|
||||
|
||||
VERIFY THESE FIXES:
|
||||
[paste fixed accuracy issues from state file]
|
||||
|
||||
Return in this format:
|
||||
|
||||
VERIFICATION REPORT:
|
||||
|
||||
| ID | Status | Notes |
|
||||
|----|--------|-------|
|
||||
| X | verified-fixed / not-fixed | [what you found in doc AND source] |
|
||||
|
||||
CONTENT SCORES:
|
||||
COMPLETENESS: [score]/10 (score existing content only, ignore wont-fix items)
|
||||
ACCURACY: [score]/10
|
||||
|
||||
NEW ACCURACY ISSUES (not already in state file):
|
||||
- [issue with source file:line] or "None found"
|
||||
```
|
||||
|
||||
After verification, update the state file with new statuses and any new issues.
|
||||
|
||||
## Step 6: Loop
|
||||
|
||||
Continue the loop until:
|
||||
|
||||
- User chooses "Done" (exit without changes)
|
||||
- User chooses "Complete and finish" (apply fixes, then exit)
|
||||
- Scores reach acceptable levels (32/40 or higher)
|
||||
- All issues are `verified-fixed` or `wont-fix`
|
||||
|
||||
## Notes
|
||||
|
||||
- The state file prevents re-discovering the same issues across rounds
|
||||
- Evaluation agents verify previous fixes before scoring
|
||||
- `wont-fix` is appropriate for completeness issues requiring new sections
|
||||
- **Accuracy verification is critical**: The improvement agent must read actual source code before applying any accuracy fix
|
||||
- Style and content evaluations always run in parallel for efficiency
|
||||
189
skills/shared/blog-guide.md
Normal file
189
skills/shared/blog-guide.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# Blog style guide
|
||||
|
||||
This document defines the rules and conventions for tldraw technical blog posts.
|
||||
|
||||
**Prerequisite**: Read the [voice and style guide](../../VOICE.md) first. This document builds on those foundations with blog-specific patterns.
|
||||
|
||||
## What technical blog posts are
|
||||
|
||||
Technical blog posts are short articles about how we solved interesting problems. They need to be _interesting_ as well as informative—if the content isn't interesting or can't be made interesting, there's no point in writing it.
|
||||
|
||||
A technical blog post can be interesting for a number of reasons:
|
||||
|
||||
- It may describe a journey from discovery, investigation, and solution
|
||||
- The problem area it describes may be hard, unintuitive, or notorious
|
||||
- It reveals something curious about the implementation of a common feature
|
||||
- It describes iteration and design decisions behind a feature
|
||||
|
||||
The best content combines many of these reasons into a single article.
|
||||
|
||||
An evergreen approach is to root the technical article in some anecdotal context. These problems don't just emerge from nowhere—they come from details, behaviors, conventions, or general "what feels right" expectations within the canvas domain. The real problem is how to write the code and convince the computer to do the thing that makes the experience feel right. Often times, that work is unintuitive and interesting in that it reveals something about the interaction or about the technologies involved.
|
||||
|
||||
## Opening pattern
|
||||
|
||||
Technical blog posts start by **framing the problem**—a sentence or two that tells the reader what this is about and why it's interesting before diving in.
|
||||
|
||||
**Example opening:**
|
||||
|
||||
> The tldraw SDK is all about making the little details work. If you've ever used dashed lines in tldraw, you might have noticed that the dashes always line up with the corners of your shape, the handles of a spline, or the start and end of an arrow. While this might seem like the obvious way that dashes _should_ work, you might be surprised to learn that SVG offers no such feature. We implement these perfect dashes entirely ourselves.
|
||||
>
|
||||
> Here's how it works.
|
||||
|
||||
The opening establishes context (what we're talking about), tension (there's a problem or unmet expectation), and stakes (why you should care) before getting into the solution.
|
||||
|
||||
### Concrete vs abstract tension
|
||||
|
||||
The tension needs to be concrete and specific, not an abstract problem statement.
|
||||
|
||||
**Too abstract:**
|
||||
|
||||
> How do you render ephemeral, performant drawing feedback that needs to behave differently depending on the tool?
|
||||
|
||||
**Concrete:**
|
||||
|
||||
> SVG's `stroke-dasharray` doesn't give you complete dashes at both ends. We had to calculate them ourselves.
|
||||
|
||||
Abstract tension describes a category of problem. Concrete tension names a specific thing that doesn't work, a surprising limitation, or an unexpected behavior. Concrete tension makes the reader think "oh, I didn't know that" or "huh, I've never thought about that."
|
||||
|
||||
### Example openings
|
||||
|
||||
**Too abrupt:**
|
||||
|
||||
> Tldraw calculates dash patterns that fit paths exactly. Complete dashes at both ends, even spacing throughout.
|
||||
|
||||
**Better (starts with our experience):**
|
||||
|
||||
> When we added dashed lines to tldraw, we wanted them to look right—complete dashes at both ends, even spacing, corners that line up on rectangles. SVG's `stroke-dasharray` doesn't do this.
|
||||
|
||||
**Also good (frames the problem):**
|
||||
|
||||
> Arrow routing sounds simple until you try it. Given two shapes, draw a line between them that doesn't pass through anything else. We spent a while getting this right.
|
||||
|
||||
## Structure
|
||||
|
||||
Technical blog posts typically follow this arc:
|
||||
|
||||
1. **Frame the problem** — What's this about? What problem did we encounter and solve? Why was it hard, unintuitive, or interesting?
|
||||
2. **Show the insight** — What's the key idea that makes the solution work?
|
||||
3. **Walk through the implementation** — Code and explanation, building up complexity
|
||||
4. **Wrap up** — Where this lives in the codebase, tradeoffs, links to files. Also unexplored areas, more we could do, or related problems.
|
||||
|
||||
Wrap-ups can end with an opinion ("that's worth the complexity") but avoid promotional language. Don't summarize with adjective lists like "fast, flexible, and powerful" or "performant but smooth"—these read like marketing copy.
|
||||
|
||||
## Tone
|
||||
|
||||
Technical blog posts have warmth and personality. They:
|
||||
|
||||
- Use phrases like "the trick is..." or "the insight is..." to signal key ideas
|
||||
- Include brief asides about why something is hard or interesting
|
||||
- Show the journey, not just the destination ("we tried X, but Y worked better")
|
||||
- End with opinions ("that's worth the tradeoff")
|
||||
- Use "we" narratively throughout
|
||||
|
||||
They still shouldn't:
|
||||
|
||||
- Ramble or over-explain
|
||||
- Use hollow importance claims ("this is crucial for...")
|
||||
- Get too casual or jokey
|
||||
- Overdo the storytelling at the expense of the technical content
|
||||
|
||||
## Describe what we did, not what to do
|
||||
|
||||
Technical blog posts explain how tldraw solved a problem. Frame solutions as "here's what we do" rather than prescriptive instructions.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> The solution: don't decide immediately. Watch what the fingers do, then commit once the pattern is clear.
|
||||
|
||||
> Instead of guessing, implement a state machine that starts undecided.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Since we don't have enough information to know either way, we defer the decision. The gesture handler watches what the pointers do, then commits once we know enough to recognize the interaction pattern.
|
||||
|
||||
> Instead of guessing, we use a state machine that starts undecided and resolves as more information comes in.
|
||||
|
||||
The reader learns from seeing our approach, not from being told what to do.
|
||||
|
||||
## Code in technical blog posts
|
||||
|
||||
Code examples illustrate our solution and build understanding. They show how we approached the problem, not just the final answer.
|
||||
|
||||
### Show progression
|
||||
|
||||
Build up complexity to reveal the insight:
|
||||
|
||||
```ts
|
||||
// First attempt: simple but wrong
|
||||
function getDashOffset(length: number, dashSize: number) {
|
||||
return length % dashSize
|
||||
}
|
||||
```
|
||||
|
||||
Then explain why that doesn't work, and show what we actually do:
|
||||
|
||||
```ts
|
||||
// What we actually do: account for both ends
|
||||
function getDashOffset(length: number, dashSize: number, gapSize: number) {
|
||||
const dashCount = Math.ceil(length / (dashSize + gapSize))
|
||||
const totalDashLength = dashCount * dashSize
|
||||
const totalGapLength = (dashCount - 1) * gapSize
|
||||
return (length - totalDashLength - totalGapLength) / 2
|
||||
}
|
||||
```
|
||||
|
||||
### Link to the actual code
|
||||
|
||||
End with links to where this lives in the codebase:
|
||||
|
||||
> You can find this implementation in [`packages/editor/src/lib/utils/dashes.ts`](https://github.com/tldraw/tldraw/blob/main/packages/editor/src/lib/utils/dashes.ts).
|
||||
|
||||
## Length and depth
|
||||
|
||||
Technical blog posts should be:
|
||||
|
||||
- **Long enough** to fully explain the problem and solution
|
||||
- **Short enough** to read in one sitting (5-10 minutes)
|
||||
- **Deep enough** to be interesting to developers who've faced similar problems
|
||||
- **Accessible enough** that someone unfamiliar with tldraw can follow along
|
||||
|
||||
A typical technical blog post is 800-1500 words, but length should follow from the complexity of the topic.
|
||||
|
||||
## Topics that make good technical blog posts
|
||||
|
||||
Good technical blog post topics share these traits:
|
||||
|
||||
- **Unintuitive solutions** — The obvious approach didn't work, so we had to think differently
|
||||
- **Hidden complexity** — Something that looks simple has interesting depth
|
||||
- **Canvas-specific problems** — Challenges unique to building visual, interactive software
|
||||
- **Platform/browser quirks** — Working around limitations in SVG, Canvas, browsers
|
||||
- **Performance insights** — How we made something fast (with measurements)
|
||||
|
||||
### Examples of good topics
|
||||
|
||||
- How we calculate perfect dash patterns for arbitrary paths
|
||||
- Why arrow routing is harder than it looks
|
||||
- How we detect whether a pinch gesture is zoom or rotate
|
||||
- Making text editing feel right on an infinite canvas
|
||||
- How we handle undo/redo across multiplayer sessions
|
||||
|
||||
### Topics that aren't technical blog posts
|
||||
|
||||
- Feature announcements (better as release notes or marketing content)
|
||||
- Tutorials teaching how to use the SDK (better as docs)
|
||||
- General programming wisdom unconnected to tldraw
|
||||
- Internal refactoring without user-facing interest
|
||||
|
||||
## Evaluation checklist
|
||||
|
||||
When reviewing a technical blog post, check:
|
||||
|
||||
- [ ] **Opening** — Does it frame a problem before diving into solution?
|
||||
- [ ] **Insight** — Is there a clear "aha" moment or key idea?
|
||||
- [ ] **Specificity** — Is this grounded in tldraw's actual implementation?
|
||||
- [ ] **Code** — Do examples build understanding, not just show syntax?
|
||||
- [ ] **Tone** — Warm and personal, but not rambling?
|
||||
- [ ] **Links** — Points to actual code in the repo?
|
||||
- [ ] **Length** — Appropriate depth for the topic?
|
||||
|
||||
For voice and style, refer to the [voice and style guide](../../VOICE.md) checklist.
|
||||
231
skills/shared/docs-guide.md
Normal file
231
skills/shared/docs-guide.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# Documentation style guide
|
||||
|
||||
This document defines the rules and conventions for tldraw SDK documentation in `apps/docs/content/`.
|
||||
|
||||
**Prerequisite**: Read the [voice and style guide](../../VOICE.md) first. This document builds on those foundations with docs-specific patterns.
|
||||
|
||||
## Document structure
|
||||
|
||||
### Opening pattern
|
||||
|
||||
Start with a clear, direct definition:
|
||||
|
||||
> The Editor class is the main way of controlling tldraw's editor.
|
||||
|
||||
> In tldraw, a shape is something that can exist on the page, like an arrow, an image, or some text.
|
||||
|
||||
> In tldraw, persistence means storing information about the editor's state to a database and then restoring it later.
|
||||
|
||||
**One concept per sentence.** If your opening packs definition, use cases, and API references together, split it:
|
||||
|
||||
**Don't:**
|
||||
|
||||
> The scribble system draws temporary freehand paths for pointer-based interactions, used for visual feedback during erasing, laser drawing, or scribble-brush selection, accessed through Editor#scribbles.
|
||||
|
||||
**Do:**
|
||||
|
||||
> The scribble system draws temporary freehand paths for pointer-based interactions. Use scribbles to show visual feedback during tool operations like erasing, laser pointer drawing, or scribble-brush selection.
|
||||
|
||||
The API reference can come after the opening paragraph or inline where first relevant.
|
||||
|
||||
### Concept, explanation, code
|
||||
|
||||
Every concept should be followed by a working example:
|
||||
|
||||
> You can access the editor in two ways:
|
||||
>
|
||||
> 1. From the Tldraw component's `onMount` callback:
|
||||
>
|
||||
> ```tsx
|
||||
> function App() {
|
||||
> return (
|
||||
> <Tldraw
|
||||
> onMount={(editor) => {
|
||||
> // your editor code here
|
||||
> }}
|
||||
> />
|
||||
> )
|
||||
> }
|
||||
> ```
|
||||
|
||||
### Progressive disclosure
|
||||
|
||||
Move from simple to complex:
|
||||
|
||||
1. Start with the most common use case
|
||||
2. Add complexity incrementally
|
||||
3. Leave edge cases and advanced patterns for later sections
|
||||
|
||||
**Example from persistence docs:**
|
||||
|
||||
1. First: `persistenceKey` prop (simplest)
|
||||
2. Then: State snapshots (more control)
|
||||
3. Then: The `store` prop (full control)
|
||||
4. Finally: Migrations (advanced)
|
||||
|
||||
### Short paragraphs
|
||||
|
||||
Keep paragraphs to 1-3 sentences. Dense blocks of text are hard to scan:
|
||||
|
||||
**Do:**
|
||||
|
||||
> Meta information is information that is not used by tldraw but is instead used by your application. For example, you might want to store the name of the user who created a shape, or the date that the shape was created.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> Meta information is additional data that can be attached to shapes and is not used internally by tldraw but can be leveraged by your application for custom functionality. This could include things like the user who created the shape, timestamps, custom identifiers, or any other application-specific data that you want to associate with shapes but don't want to store in the props object.
|
||||
|
||||
### Tables for related information
|
||||
|
||||
Use tables to organize related methods, options, or concepts:
|
||||
|
||||
| Method | Description |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| `Editor#setCamera` | Moves the camera to the provided coordinates. |
|
||||
| `Editor#zoomIn` | Zooms the camera in to the nearest zoom step. |
|
||||
| `Editor#zoomOut` | Zooms the camera out to the nearest zoom step. |
|
||||
|
||||
### Notes and callouts
|
||||
|
||||
Use blockquotes for important asides:
|
||||
|
||||
> If all you're interested in is the state below `root`, there is a convenience method, `Editor#getCurrentToolId`, that can help.
|
||||
|
||||
Use stronger callout syntax for warnings:
|
||||
|
||||
```
|
||||
<Callout type="warning">
|
||||
You must make sure that the tldraw version in your client matches the version on the server.
|
||||
</Callout>
|
||||
```
|
||||
|
||||
## Cross-referencing
|
||||
|
||||
### Link liberally
|
||||
|
||||
Reference related concepts inline rather than explaining everything:
|
||||
|
||||
> For more information about how to synchronize the store with other processes, see the [Persistence](/docs/persistence) page.
|
||||
|
||||
### API references use consistent format
|
||||
|
||||
Link to API docs using the `[MethodName](?)` pattern:
|
||||
|
||||
> Use the [Editor#createShapes](?) method.
|
||||
|
||||
> See [TLInstancePresence](?) for the full record type.
|
||||
|
||||
### Point to working examples
|
||||
|
||||
Always link to runnable examples when available:
|
||||
|
||||
> For an example of how to create custom shapes, see our [custom shapes example](/examples/shapes/tools/custom-shape).
|
||||
|
||||
## Nuggets (tech blog posts)
|
||||
|
||||
Nuggets are short technical articles about how we solved interesting problems. They're different from reference documentation—more like posts you'd find on a company engineering blog.
|
||||
|
||||
### Different opening pattern
|
||||
|
||||
Reference docs start with definitions. Nuggets start by **framing the problem**—a sentence or two that tells the reader what this is about and why it's interesting before diving in.
|
||||
|
||||
**Reference doc opening:**
|
||||
|
||||
> The Editor class is the main way of controlling tldraw's editor.
|
||||
|
||||
**Nugget opening:**
|
||||
|
||||
> The tldraw SDK is all about making the little details work. If you've ever used dashed lines in tldraw, you might have noticed that the dashes always line up with the corners of your shape, the handles of a spline, or the start and end of an arrow. While this might seem like the obvious way that dashes _should_ work, you might be surprised to learn that SVG offers no such feature. We implement these perfect dashes entirely ourselves.
|
||||
>
|
||||
> Here's how it works.
|
||||
|
||||
The nugget opening establishes context (what we're talking about), tension (there's a problem or unmet expectation), and stakes (why you should care) before getting into the solution.
|
||||
|
||||
The goal is to root the technical article in some anecdotal context. These problems don't just emerge from nowhere, but rather they come from details, behaviors, conventions, or general "what feels right" expectations within the canvas domain. The real problem is how to write the code and convince the computer to do the thing that makes the experience feel right. Often times, that work is unintuitive and interesting in that it reveals something about the interaction or about the technologies involved.
|
||||
|
||||
### Structure
|
||||
|
||||
Nuggets typically follow this arc:
|
||||
|
||||
1. **Frame the problem** — What's this about? What problem did we encounter and solve? Why was it hard, unintuitive, or interesting?
|
||||
2. **Show the insight** — What's the key idea that makes the solution work?
|
||||
3. **Walk through the implementation** — Code and explanation, building up complexity
|
||||
4. **Wrap up** — Where this lives in the codebase, tradeoffs, links to files. Also unexplored areas, more we could do, or related problems.
|
||||
|
||||
### Tone differences
|
||||
|
||||
Nuggets are warmer than reference docs. They can:
|
||||
|
||||
- Use "the trick is..." or "the insight is..." to signal key ideas
|
||||
- Include brief asides about why something is hard or interesting
|
||||
- Show the journey, not just the destination ("we tried X, but Y worked better")
|
||||
- End with opinions ("that's worth the tradeoff")
|
||||
|
||||
They still shouldn't:
|
||||
|
||||
- Ramble or over-explain
|
||||
- Use hollow importance claims ("this is crucial for...")
|
||||
- Get too casual or jokey
|
||||
|
||||
### Describe what we did, not what to do
|
||||
|
||||
Nuggets explain how tldraw solved a problem—they're not tutorials. Frame solutions as "here's what we do" rather than prescriptive instructions.
|
||||
|
||||
**Don't:**
|
||||
|
||||
> The solution: don't decide immediately. Watch what the fingers do, then commit once the pattern is clear.
|
||||
|
||||
> Instead of guessing, implement a state machine that starts undecided.
|
||||
|
||||
**Do:**
|
||||
|
||||
> Since we don't have enough information to know either way, we defer the decision. The gesture handler watches what the pointers do, then commits once we know enough to recognize the interaction pattern.
|
||||
|
||||
> Instead of guessing, we use a state machine that starts undecided and resolves as more information comes in.
|
||||
|
||||
The reader learns from seeing our approach, not from being told what to do.
|
||||
|
||||
### Example openings
|
||||
|
||||
**Too abrupt (reads like docs):**
|
||||
|
||||
> Tldraw calculates dash patterns that fit paths exactly. Complete dashes at both ends, even spacing throughout.
|
||||
|
||||
**Better (starts with our experience):**
|
||||
|
||||
> When we added dashed lines to tldraw, we wanted them to look right—complete dashes at both ends, even spacing, corners that line up on rectangles. SVG's `stroke-dasharray` doesn't do this.
|
||||
|
||||
**Also good (frames the problem we faced):**
|
||||
|
||||
> Arrow routing sounds simple until you try it. Given two shapes, draw a line between them that doesn't pass through anything else. We spent a while getting this right.
|
||||
|
||||
## Priorities
|
||||
|
||||
1. **Accuracy** — Code must work. API refs must be correct.
|
||||
2. **Clarity** — Understand on first read.
|
||||
3. **Brevity** — Say it once, move on. Cut sections that repeat what's already shown elsewhere.
|
||||
4. **Scannability** — Short paragraphs, clear headers, lots of code.
|
||||
|
||||
### Avoid redundant sections
|
||||
|
||||
If detailed examples already demonstrate a pattern, don't repeat the same information in a "Common use cases" section with shorter snippets. Either:
|
||||
|
||||
- Keep only the detailed examples (preferred)
|
||||
- Keep only the quick-reference snippets
|
||||
- Ensure each section adds genuinely new information
|
||||
|
||||
**Don't:** Show a complete eraser implementation, then have a "Common use cases > Eraser" section with the same code trimmed down.
|
||||
|
||||
**Do:** Show complete implementations once. If you need a quick-reference section, make it a table pointing to the detailed examples.
|
||||
|
||||
## Evaluation checklist
|
||||
|
||||
When reviewing documentation, check:
|
||||
|
||||
- [ ] **Opening sentence** — Does it immediately define what this thing is?
|
||||
- [ ] **Code examples** — Is every concept followed by working code?
|
||||
- [ ] **Progressive disclosure** — Does complexity build gradually?
|
||||
- [ ] **Links** — Are related concepts cross-referenced?
|
||||
- [ ] **Scannability** — Short paragraphs, clear headers?
|
||||
|
||||
For voice and style, refer to the [voice and style guide](../../VOICE.md) checklist.
|
||||
281
skills/shared/release-notes-guide.md
Normal file
281
skills/shared/release-notes-guide.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Release notes style guide
|
||||
|
||||
This document defines the rules and conventions for tldraw SDK release notes articles in `apps/docs/content/releases/`.
|
||||
|
||||
**Prerequisite**: Read the [voice and style guide](../../VOICE.md) first. This document builds on those foundations with release-notes-specific patterns.
|
||||
|
||||
## Editorial guidance
|
||||
|
||||
### What to include
|
||||
|
||||
- Breaking changes that require user action
|
||||
- New features that solve common pain points
|
||||
- API additions that unlock new capabilities
|
||||
- Changes that affect how developers integrate tldraw
|
||||
- Bug fixes for user-reported issues
|
||||
|
||||
### What to omit
|
||||
|
||||
- Internal performance optimizations (unless user-visible)
|
||||
- Fixes for bugs introduced in the same release cycle
|
||||
- Implementation details that don't affect public API
|
||||
- Pure code quality improvements
|
||||
|
||||
### When to create a featured section
|
||||
|
||||
Promote changes to the "What's new" section when:
|
||||
|
||||
- It's a breaking change requiring a migration guide
|
||||
- It introduces a major new capability
|
||||
- It is an interesting or significant new feature, possibly the result of multiple PRs
|
||||
- Users need detailed guidance (migration guides, platform tables)
|
||||
|
||||
Featured sections should include:
|
||||
|
||||
- Clear description of what changed and why it matters
|
||||
- Code examples where helpful
|
||||
- Migration guides in collapsible `<details>` blocks for breaking changes
|
||||
- Links to relevant documentation
|
||||
|
||||
## PR categorization
|
||||
|
||||
| Category | Labels | Indicators |
|
||||
| ------------ | ---------------------------- | -------------------------------- |
|
||||
| API changes | `api`, `feature`, `major` | Adds/removes/modifies public API |
|
||||
| Improvements | `improvement`, `enhancement` | Enhances existing functionality |
|
||||
| Bug fixes | `bugfix`, `bug`, | Fixes issues |
|
||||
|
||||
Look for `### Release notes` and `### API changes` sections in PR bodies. Search for "breaking" to identify breaking changes. Search for "deprecat" to identify deprecated APIs (mark with 🔜, not 💥).
|
||||
|
||||
### PRs to skip
|
||||
|
||||
Skip PRs with these labels:
|
||||
|
||||
- `other`
|
||||
- `skip-release`
|
||||
- `chore`
|
||||
- `dotcom`
|
||||
|
||||
Also skip:
|
||||
|
||||
- Reverts, unless they fix something user-facing
|
||||
- Fixes for bugs introduced in the same release cycle (i.e., the bug was caused by a PR that is also in `next.mdx` and was not in the previous release)
|
||||
|
||||
When a PR is reverted, also remove the original PR's entry from `next.mdx` if it is present.
|
||||
|
||||
## Team members (do not credit)
|
||||
|
||||
angrycaptain19, AniKrisn, ds300, kaneel, kostyafarber, max-dra, mimecuvalo, MitjaBezensek, profdl, Siobhantldraw, steveruizok, tldrawdaniel, danieljamesross, jsscclr, frolic, huppy-bot, github-actions, Somehats, todepond, Taha-Hassan-Git, alex-mckenna-1, max-drake, m31-galaxy, meg-an31
|
||||
|
||||
Credit community contributors with:
|
||||
|
||||
```markdown
|
||||
(contributed by [@username](https://github.com/username))
|
||||
```
|
||||
|
||||
## General notes
|
||||
|
||||
- Do not include AI attribution
|
||||
- Write as if the release has already happened
|
||||
- Omit empty sections
|
||||
- The release listing is maintained in `apps/docs/content/getting-started/releases.mdx`
|
||||
|
||||
---
|
||||
|
||||
## Formatting conventions
|
||||
|
||||
### Section order
|
||||
|
||||
Use these sections in order (omit empty sections):
|
||||
|
||||
1. **Introduction paragraph** - 1-2 sentence summary of the release highlights
|
||||
2. **What's new** (`## What's new`) - Featured sections (H3s) for major features and breaking changes
|
||||
3. **API changes** (`## API changes`) - New methods, properties, options, deprecations, and breaking changes
|
||||
4. **Improvements** (`## Improvements`) - Enhancements to existing functionality
|
||||
5. **Bug fixes** (`## Bug fixes`) - Fixed issues
|
||||
6. **Patch releases** (`## Patch releases`) - Separated by `---`, contains bulleted changes for each patch version
|
||||
|
||||
### Introduction paragraph
|
||||
|
||||
Start each release with a 1-2 sentence summary highlighting the most significant changes. Lead with concrete features, then mention infrastructure and performance:
|
||||
|
||||
```markdown
|
||||
This release introduces several significant changes: a new pattern for defining custom shape/binding typings, pluggable storage for `TLSocketRoom` with a new SQLite option, reactive `editor.inputs`, and optimized draw shape encoding. It also adds various other API improvements, performance optimizations, and bug fixes.
|
||||
```
|
||||
|
||||
### Entry format
|
||||
|
||||
Start entries with a verb: "Add", "Fix", "Improve", "Remove". Keep descriptions concise but informative.
|
||||
|
||||
```markdown
|
||||
- Add `Editor.newMethod()` for doing something useful. ([#7123](https://github.com/tldraw/tldraw/pull/7123))
|
||||
```
|
||||
|
||||
For multiple related PRs:
|
||||
|
||||
```markdown
|
||||
- Improve arrow snapping performance. ([#7145](https://github.com/tldraw/tldraw/pull/7145), [#7150](https://github.com/tldraw/tldraw/pull/7150))
|
||||
```
|
||||
|
||||
With code examples:
|
||||
|
||||
````markdown
|
||||
- Add `localStorageAtom` to `@tldraw/state`. ([#6876](https://github.com/tldraw/tldraw/pull/6876))
|
||||
|
||||
```tsx
|
||||
const myAtom = localStorageAtom('my-key', defaultValue)
|
||||
```
|
||||
````
|
||||
|
||||
### Breaking changes
|
||||
|
||||
Mark breaking API changes with a 💥 prefix. Mark deprecations (APIs that still work but will be removed in a future release) with a 🔜 prefix. Place breaking changes at the top of the API changes section, followed by deprecations:
|
||||
|
||||
```markdown
|
||||
## API changes
|
||||
|
||||
- 💥 **`ShapeUtil.canEdit()`** signature changed to accept a `TLEditStartInfo` parameter. ([#7361](https://github.com/tldraw/tldraw/pull/7361))
|
||||
- 💥 **`oldMethod`** renamed to `newMethod`. ([#7400](https://github.com/tldraw/tldraw/pull/7400))
|
||||
- 🔜 **`Editor.legacyMethod()`** is deprecated. Use `Editor.newMethod()` instead. ([#7450](https://github.com/tldraw/tldraw/pull/7450))
|
||||
- Add `Editor.newMethod()` for doing something useful. ([#7123](https://github.com/tldraw/tldraw/pull/7123))
|
||||
```
|
||||
|
||||
### What's new section
|
||||
|
||||
The `## What's new` section contains featured subsections (H3s) for headline features and major breaking changes.
|
||||
|
||||
**Basic structure:**
|
||||
|
||||
```markdown
|
||||
## What's new
|
||||
|
||||
### Feature name ([#7320](https://github.com/tldraw/tldraw/pull/7320))
|
||||
|
||||
Brief description of what this feature does and why it matters.
|
||||
```
|
||||
|
||||
**Multiple related PRs:**
|
||||
|
||||
```markdown
|
||||
### Pluggable storage for TLSocketRoom ([#7320](https://github.com/tldraw/tldraw/pull/7320), [#7123](https://github.com/tldraw/tldraw/pull/7123))
|
||||
```
|
||||
|
||||
**Deprecation featured sections** - add 🔜 to the heading:
|
||||
|
||||
```markdown
|
||||
### 🔜 Deprecation of old API ([#0000](https://github.com/tldraw/tldraw/pull/0000))
|
||||
|
||||
Brief description of what is deprecated and what to use instead.
|
||||
```
|
||||
|
||||
**Breaking change featured sections** - add 💥 to the heading and include a migration guide:
|
||||
|
||||
```markdown
|
||||
### 💥 Feature name ([#0000](https://github.com/tldraw/tldraw/pull/0000))
|
||||
|
||||
Brief description of what this feature does and why it matters.
|
||||
|
||||
<details>
|
||||
<summary>Migration guide</summary>
|
||||
|
||||
Before:
|
||||
\`\`\`ts
|
||||
// old code
|
||||
\`\`\`
|
||||
|
||||
After:
|
||||
\`\`\`ts
|
||||
// new code
|
||||
\`\`\`
|
||||
|
||||
</details>
|
||||
```
|
||||
|
||||
**Collapsible explanations** - use for supplementary context:
|
||||
|
||||
```markdown
|
||||
<details>
|
||||
<summary>Why SQLite?</summary>
|
||||
|
||||
- **Automatic persistence**: Data survives process restarts
|
||||
- **Lower memory usage**: No need to keep entire documents in memory
|
||||
|
||||
</details>
|
||||
```
|
||||
|
||||
**Platform support tables:**
|
||||
|
||||
```markdown
|
||||
<details>
|
||||
<summary>Platform support</summary>
|
||||
|
||||
| Platform | Wrapper | Library |
|
||||
| -------------------------- | -------------------------------- | --------------------------------- |
|
||||
| Cloudflare Durable Objects | `DurableObjectSqliteSyncWrapper` | Built-in `ctx.storage` |
|
||||
| Node.js/Deno | `NodeSqliteWrapper` | `better-sqlite3` or `node:sqlite` |
|
||||
|
||||
</details>
|
||||
```
|
||||
|
||||
### GitHub release link
|
||||
|
||||
Add a link to the GitHub release at the end of each release section:
|
||||
|
||||
- **For minor releases**: Place after the last content section and before the `---` separator
|
||||
- **For patch releases**: Place after the bulleted list of changes
|
||||
|
||||
```markdown
|
||||
[View release on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.3.0)
|
||||
```
|
||||
|
||||
### Patch releases
|
||||
|
||||
Add patch releases at the bottom of the minor release file, after a horizontal rule. List in chronological order:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## Patch releases
|
||||
|
||||
### v4.2.1
|
||||
|
||||
- Fix text selection flakiness when clicking into text shapes. ([#3643](https://github.com/tldraw/tldraw/pull/3643))
|
||||
|
||||
[View release on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.2.1)
|
||||
|
||||
### v4.2.2
|
||||
|
||||
- Fix arrow binding when target shape is rotated. ([#3650](https://github.com/tldraw/tldraw/pull/3650))
|
||||
|
||||
[View release on GitHub](https://github.com/tldraw/tldraw/releases/tag/v4.2.2)
|
||||
```
|
||||
|
||||
### Horizontal rules
|
||||
|
||||
Use `---` only before the `## Patch releases` section. Do not use horizontal rules elsewhere.
|
||||
|
||||
### Headings
|
||||
|
||||
Use sentence case: "API changes" not "API Changes", "Bug fixes" not "Bug Fixes".
|
||||
|
||||
### Frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: 'v4.3.0'
|
||||
created_at: 12/19/2024
|
||||
updated_at: 12/19/2024
|
||||
keywords:
|
||||
- changelog
|
||||
- release
|
||||
- v4.3
|
||||
- v4.3.0
|
||||
- v4.3.1
|
||||
- feature-keyword
|
||||
---
|
||||
```
|
||||
|
||||
- Dates in MM/DD/YYYY format
|
||||
- Always include `changelog` and `release` as keywords
|
||||
- Include the minor version without patch (e.g., `v4.3`), the `.0` release, and all patch versions
|
||||
- Add 2-5 content-relevant keywords (lowercase, hyphens for multi-word)
|
||||
161
skills/shepherd-pr/SKILL.md
Normal file
161
skills/shepherd-pr/SKILL.md
Normal file
@@ -0,0 +1,161 @@
|
||||
---
|
||||
name: shepherd-pr
|
||||
description: Keep an eye on this PR. Review and resolve pull request comments and fix build failures autonomously. Use when asked to review PR feedback, address reviewer comments, fix CI failures, resolve PR threads, or handle PR maintenance tasks like "review PR comments", "fix the build", "address PR feedback", "clean up PR", or "resolve comments". Handles comment triage (resolve false positives, fix trivial issues, flag complex ones), build/lint/type errors, and e2e snapshot updates.
|
||||
---
|
||||
|
||||
# Review PR
|
||||
|
||||
Autonomously review PR comments and build status, resolving what can be done with high confidence (>=80%) and flagging the rest for human review.
|
||||
|
||||
## Workflow
|
||||
|
||||
Note: this repository requires that you be using node 24. Use `nvm` to switch to node 24 before running any commands:
|
||||
|
||||
```bash
|
||||
nvm use 24
|
||||
```
|
||||
|
||||
### 1. Gather context
|
||||
|
||||
```bash
|
||||
# Get PR number for current branch
|
||||
gh pr view --json number,headRefName,url
|
||||
|
||||
# Get review threads with resolution status
|
||||
gh api graphql -f query='
|
||||
query($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
isResolved
|
||||
comments(first: 50) {
|
||||
nodes {
|
||||
body
|
||||
path
|
||||
line
|
||||
author { login }
|
||||
createdAt
|
||||
databaseId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
```
|
||||
|
||||
Filter to unresolved threads only.
|
||||
|
||||
### 2. Triage each unresolved comment
|
||||
|
||||
Read the referenced code and investigate. Classify into:
|
||||
|
||||
**A. False positive / already resolved** — The issue no longer exists in current code.
|
||||
|
||||
- Reply explaining why, citing specific code or commit.
|
||||
- Resolve the thread.
|
||||
|
||||
**B. Trivial fix (>=80% confidence)** — Obvious, mechanical fix. No design decisions or matters of opinion. Examples: typos, missing null checks, wrong variable names, off-by-one, missing imports.
|
||||
|
||||
- Make the fix.
|
||||
- Reply describing what was changed.
|
||||
- Resolve the thread.
|
||||
|
||||
**C. Needs human input (<80% confidence)** — Design question, significant refactor, or ambiguous fix.
|
||||
|
||||
- Do NOT resolve.
|
||||
- Add to end-of-session summary.
|
||||
|
||||
### 3. Reply and resolve threads
|
||||
|
||||
Reply to a comment:
|
||||
|
||||
```bash
|
||||
gh api repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}/replies \
|
||||
-f body="<your reply>"
|
||||
```
|
||||
|
||||
Resolve a thread:
|
||||
|
||||
```bash
|
||||
gh api graphql -f query='
|
||||
mutation($threadId: ID!) {
|
||||
resolveReviewThread(input: {threadId: $threadId}) {
|
||||
thread { isResolved }
|
||||
}
|
||||
}
|
||||
' -f threadId="$THREAD_ID"
|
||||
```
|
||||
|
||||
Always push fixes, then reply, then resolve related threads (in that order).
|
||||
|
||||
### 4. Check build status
|
||||
|
||||
```bash
|
||||
gh pr checks --json name,status,conclusion
|
||||
```
|
||||
|
||||
Investigate failures by category:
|
||||
|
||||
**Lint errors** — Run `yarn lint-current`. Fix if mechanical (formatting, import order, unused vars). Flag if the lint rule itself is questionable.
|
||||
|
||||
**Type errors** — Run `yarn typecheck` from repo root. Fix straightforward type mismatches. Flag if fix requires architectural decisions.
|
||||
|
||||
**Unit test failures** — Run `yarn test run` in relevant workspace. Fix if test expectation is clearly outdated due to intentional code changes. Flag if failure reveals actual bug or design concern.
|
||||
|
||||
**E2E snapshot failures** — Determine whether the PR's code changes _should_ cause visual differences:
|
||||
|
||||
- If yes (UI changes, style updates): add the `update-snapshots` label to trigger the automated update workflow:
|
||||
```bash
|
||||
gh pr edit --add-label "update-snapshots"
|
||||
```
|
||||
- If no: flag as unintended regression for human review.
|
||||
|
||||
**Mysterious/unexpected failures** — Do not attempt to fix. Flag for human review with error output.
|
||||
|
||||
### 5. Commit and push fixes
|
||||
|
||||
```bash
|
||||
git add <specific files>
|
||||
git commit -m "Address PR review feedback
|
||||
|
||||
- <summary of changes>"
|
||||
git push
|
||||
```
|
||||
|
||||
Stage specific files only. Never force push. Never use `git add -A`.
|
||||
|
||||
### 6. End-of-session summary
|
||||
|
||||
Always end with:
|
||||
|
||||
```
|
||||
## PR review summary
|
||||
|
||||
### Resolved
|
||||
- <thread>: <what was done>
|
||||
|
||||
### Fixed
|
||||
- <description of fix>
|
||||
|
||||
### Needs your input
|
||||
- <thread>: <why it needs human judgment>
|
||||
|
||||
### Build status
|
||||
- <status of each check, any actions taken>
|
||||
```
|
||||
|
||||
Omit empty sections.
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Conservative threshold: only act when >=80% confident the fix is correct and uncontroversial.
|
||||
- Never resolve comments raising design questions or matters of opinion.
|
||||
- Never resolve without replying first.
|
||||
- Read actual code before concluding a comment is a false positive.
|
||||
- Verify fixes don't break types (`yarn typecheck`) or lint (`yarn lint-current`).
|
||||
- Do not modify test expectations unless change is clearly intentional.
|
||||
67
skills/simplify-trace/SKILL.md
Normal file
67
skills/simplify-trace/SKILL.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: simplify-trace
|
||||
description: Summarize a large Chrome DevTools performance trace into a compact markdown report so it can be reasoned about without loading the whole file. Use when given a Chrome/DevTools/Performance-panel trace (a multi-MB `Trace-*.json` or `*.json` with `traceEvents`) and asked to find what is slow, what runs too often, long tasks, jank, or hot JS functions.
|
||||
---
|
||||
|
||||
# Simplify trace
|
||||
|
||||
Chrome DevTools performance traces are tens to hundreds of MB of JSON — far too large to read directly. This skill turns one into a few-KB markdown report that surfaces the work taking too long or happening too often.
|
||||
|
||||
## Usage
|
||||
|
||||
Run the script on the trace file. It prints markdown to stdout (or `--out FILE`):
|
||||
|
||||
```bash
|
||||
node skills/simplify-trace/scripts/simplify-trace.mjs <trace.json> [--top N] [--long-task-ms MS] [--window START-END] [--out report.md]
|
||||
```
|
||||
|
||||
- `--top N` — rows per table (default 25).
|
||||
- `--long-task-ms MS` — long-task threshold (default 50).
|
||||
- `--window START-END` — scope the whole report to a time slice (offsets in ms from trace start).
|
||||
- `--only a,b` / `--except a,b` / `--all` — pick which sections to emit.
|
||||
- `--match TEXT` — case-insensitive filter for rows (event names, function names, URLs).
|
||||
- `--list` — print the available section keys and exit.
|
||||
- `--out FILE` — write to a file instead of stdout.
|
||||
- Traces over ~500 MB: prefix `node --max-old-space-size=8192`.
|
||||
|
||||
Default to running with `--out` to a temp file for big traces, then read the report. Don't read the raw trace.
|
||||
|
||||
## Sections
|
||||
|
||||
Default sections: `summary`, `longtasks`, `events`, `frequent`, `functions`, `categories`. Opt-in: `timeline` (per-second main-thread busy time — use to locate activity), `network` (resource/fetch waterfall with TTFB/duration/size), `websocket` (WebSocket lifecycle — `/app/file` doc sync). Run `--list` for the full set. Interrogate narrowly, e.g.:
|
||||
|
||||
```bash
|
||||
# where is the activity? then window to it
|
||||
node …/simplify-trace.mjs trace.json --only timeline
|
||||
# what was the network/socket doing during the action?
|
||||
node …/simplify-trace.mjs trace.json --only network,websocket --window 62000-69000 --match tldraw
|
||||
```
|
||||
|
||||
The trace's `metadata.startTime` (ISO/UTC) anchors offset 0 to wall-clock, so trace offsets can be lined up against server logs (zero-cache, sync-worker) by timestamp. For an idle, network-quiet gap, the trace shows *when* but not *why* — add `performance.mark()`/`console.timeStamp()` in the client path and they appear on the trace timeline.
|
||||
|
||||
## "What happens when I do X" traces
|
||||
|
||||
A recording of a single action (switch file, open menu) is mostly idle setup time, which dilutes the action across the whole trace. Window to the action instead:
|
||||
|
||||
1. Run once with no window. Note where activity is — the long tasks' offsets, or bucket main-thread busy-time per second with a quick inline script to find the active span.
|
||||
2. Re-run with `--window START-END` around that span. All tables then describe only the action.
|
||||
|
||||
The recording artifact `CpuProfiler::StartProfiling` (the profiler turning on, ~50–60ms) is excluded from the long-task table, including when it's nested inside a `RunTask`. If long tasks shows "None", the action genuinely has no single blocking task — look at aggregate self time, GC, and animation-loop events instead.
|
||||
|
||||
## What the report contains
|
||||
|
||||
- **Header** — event count, wall-clock span, sampled JS CPU time, and idle %.
|
||||
- **Long tasks** — top-level tasks over the threshold (main-thread jank), with the time offset where each occurred.
|
||||
- **Hottest event types (self time)** — where engine/browser time actually goes (layout, GC, paint, function calls), excluding time spent in nested children.
|
||||
- **Most frequent event types (count)** — work happening too often.
|
||||
- **Hottest JS functions** — bottom-up self time from the embedded V8 CPU profile, with `file:line`. Synthetic `(idle)`/`(program)` frames are excluded here (idle is in the header).
|
||||
- **Self time by category** — high-level breakdown across trace categories.
|
||||
|
||||
## How to read it
|
||||
|
||||
- A function high in **self time** is the actual cost; high **total** but low self means the cost is in its callees — follow the call tree.
|
||||
- High **count** with low avg = death by a thousand cuts (often a reactive/render loop firing too often); investigate why it fires, not its per-call cost.
|
||||
- Long tasks point at *when* jank happened; cross-reference the offset against what the user was doing.
|
||||
- Minified names (`r`, `Tg`) come with a `file:line` — use it to locate the source.
|
||||
|
||||
The script only summarizes; it does not modify the trace.
|
||||
482
skills/simplify-trace/scripts/simplify-trace.mjs
Normal file
482
skills/simplify-trace/scripts/simplify-trace.mjs
Normal file
@@ -0,0 +1,482 @@
|
||||
#!/usr/bin/env node
|
||||
// Summarize a Chrome DevTools performance trace into a compact markdown report.
|
||||
// Surfaces the work that takes too long or happens too often, so an agent can
|
||||
// reason about a multi-hundred-MB trace without loading the whole thing.
|
||||
//
|
||||
// Usage:
|
||||
// node simplify-trace.mjs <trace.json> [--top N] [--long-task-ms MS] [--out FILE]
|
||||
//
|
||||
// For very large traces (>~500MB) increase Node's heap:
|
||||
// node --max-old-space-size=8192 simplify-trace.mjs <trace.json>
|
||||
|
||||
import fs from 'node:fs'
|
||||
|
||||
// Sections the report can emit. `default` is the compact overview; the rest are
|
||||
// opt-in via --only / --all so the tool can answer a narrow question.
|
||||
const SECTIONS = {
|
||||
summary: 'header: counts, span, CPU time, idle %',
|
||||
timeline: 'per-second main-thread busy time (find when activity happens)',
|
||||
longtasks: 'top-level tasks over the threshold (main-thread jank)',
|
||||
events: 'hottest event types by self time',
|
||||
frequent: 'most frequent event types by count',
|
||||
functions: 'hottest JS functions from the CPU profile (self time)',
|
||||
categories: 'self time grouped by trace category',
|
||||
network: 'resource/fetch waterfall: offset, TTFB, duration, size, URL',
|
||||
websocket: 'WebSocket lifecycle: create/destroy/send/receive/handshake',
|
||||
}
|
||||
const DEFAULT_SECTIONS = ['summary', 'longtasks', 'events', 'frequent', 'functions', 'categories']
|
||||
const ALL_SECTIONS = Object.keys(SECTIONS)
|
||||
|
||||
function parseList(s) {
|
||||
return s.split(',').map((x) => x.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const opts = { top: 25, longTaskMs: 50, out: null, file: null, window: null, only: null, except: null, match: null, list: false }
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (a === '--top') opts.top = Number(argv[++i])
|
||||
else if (a === '--long-task-ms') opts.longTaskMs = Number(argv[++i])
|
||||
else if (a === '--out') opts.out = argv[++i]
|
||||
else if (a === '--only') opts.only = parseList(argv[++i])
|
||||
else if (a === '--except') opts.except = parseList(argv[++i])
|
||||
else if (a === '--all') opts.only = ALL_SECTIONS.slice()
|
||||
else if (a === '--match') opts.match = argv[++i]
|
||||
else if (a === '--wall-clock') opts.wallClock = true
|
||||
else if (a === '--list') opts.list = true
|
||||
else if (a === '--window') {
|
||||
// "<startMs>-<endMs>" as offsets from the trace start
|
||||
const [s, e] = argv[++i].split('-').map(Number)
|
||||
opts.window = { start: s, end: e }
|
||||
} else if (!a.startsWith('--')) opts.file = a
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
// Recording artifacts: the profiler starting/stopping shows up as a 50-60ms
|
||||
// "task" that is not app work. Exclude it from the long-task ranking so it
|
||||
// doesn't masquerade as the worst jank.
|
||||
const ARTIFACT_NAMES = new Set(['CpuProfiler::StartProfiling', 'CpuProfiler::StopProfiling'])
|
||||
|
||||
const opts = parseArgs(process.argv.slice(2))
|
||||
|
||||
if (opts.list) {
|
||||
console.log('Sections (--only a,b / --except a,b / --all):\n')
|
||||
for (const k of ALL_SECTIONS) {
|
||||
const def = DEFAULT_SECTIONS.includes(k) ? ' (default)' : ''
|
||||
console.log(` ${k.padEnd(11)} ${SECTIONS[k]}${def}`)
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
if (!opts.file) {
|
||||
console.error(
|
||||
'Usage: node simplify-trace.mjs <trace.json> [--top N] [--long-task-ms MS] [--window START-END]\n' +
|
||||
' [--only a,b | --except a,b | --all] [--match TEXT] [--out FILE] [--list]'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Resolve which sections to emit, preserving a stable display order.
|
||||
let wantSections = opts.only ? opts.only : DEFAULT_SECTIONS.slice()
|
||||
const unknown = wantSections.filter((k) => !SECTIONS[k])
|
||||
if (unknown.length) {
|
||||
console.error(`Unknown section(s): ${unknown.join(', ')}. Run --list to see options.`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (opts.except) wantSections = wantSections.filter((k) => !opts.except.includes(k))
|
||||
const selected = new Set(wantSections)
|
||||
const matchLc = opts.match ? opts.match.toLowerCase() : null
|
||||
const matches = (s) => !matchLc || (s || '').toLowerCase().includes(matchLc)
|
||||
|
||||
const ms = (us) => Math.round(us / 100) / 10 // microseconds -> ms, 1 decimal
|
||||
const pct = (n, total) => (total ? Math.round((n / total) * 1000) / 10 : 0)
|
||||
|
||||
const raw = JSON.parse(fs.readFileSync(opts.file, 'utf8'))
|
||||
const events = Array.isArray(raw) ? raw : raw.traceEvents || []
|
||||
|
||||
// ---- process / thread names (from metadata 'M' events) -------------------
|
||||
const threadNames = new Map() // `${pid}:${tid}` -> name
|
||||
const procNames = new Map() // pid -> name
|
||||
for (const e of events) {
|
||||
if (e.ph !== 'M') continue
|
||||
if (e.name === 'thread_name') threadNames.set(`${e.pid}:${e.tid}`, e.args?.name)
|
||||
if (e.name === 'process_name') procNames.set(e.pid, e.args?.name)
|
||||
}
|
||||
|
||||
// ---- trace bounds --------------------------------------------------------
|
||||
// Ignore ts <= 0: metadata ('M') and some bookkeeping events report ts:0 and
|
||||
// would otherwise pin the trace start to zero and break every offset.
|
||||
let minTs = Infinity
|
||||
let maxTs = -Infinity
|
||||
for (const e of events) {
|
||||
if (typeof e.ts !== 'number' || e.ts <= 0) continue
|
||||
if (e.ts < minTs) minTs = e.ts
|
||||
const end = e.ts + (e.dur || 0)
|
||||
if (end > maxTs) maxTs = end
|
||||
}
|
||||
const traceDur = maxTs - minTs
|
||||
|
||||
// ---- optional time window ------------------------------------------------
|
||||
// --window START-END scopes the whole report to a slice of the recording
|
||||
// (offsets in ms from trace start) — use it to focus on the moment an action
|
||||
// happened instead of averaging it against idle time.
|
||||
const winLo = opts.window ? minTs + opts.window.start * 1000 : -Infinity
|
||||
const winHi = opts.window ? minTs + opts.window.end * 1000 : Infinity
|
||||
const inWin = (ts) => ts >= winLo && ts <= winHi
|
||||
let scopedCount = 0
|
||||
|
||||
// ---- aggregate complete (X) events by name, with self time ---------------
|
||||
// Self time = own duration minus the duration of directly-nested children on
|
||||
// the same thread. We reconstruct nesting with a per-thread stack.
|
||||
const byThread = new Map()
|
||||
for (const e of events) {
|
||||
if (!inWin(e.ts)) continue
|
||||
scopedCount++
|
||||
if (e.ph !== 'X' || typeof e.dur !== 'number') continue
|
||||
const key = `${e.pid}:${e.tid}`
|
||||
if (!byThread.has(key)) byThread.set(key, [])
|
||||
byThread.get(key).push(e)
|
||||
}
|
||||
|
||||
const agg = new Map() // name -> { count, total, self, max }
|
||||
const topLevelTasks = [] // candidate "long tasks"
|
||||
const selfByCat = new Map() // category -> self time
|
||||
|
||||
function bump(map, k, dur) {
|
||||
map.set(k, (map.get(k) || 0) + dur)
|
||||
}
|
||||
|
||||
for (const [, list] of byThread) {
|
||||
// Sort by start asc, then by duration desc so parents precede children.
|
||||
list.sort((a, b) => a.ts - b.ts || b.dur - a.dur)
|
||||
const stack = []
|
||||
for (const e of list) {
|
||||
while (stack.length && e.ts >= stack[stack.length - 1].ts + stack[stack.length - 1].dur) {
|
||||
stack.pop()
|
||||
}
|
||||
const depth = stack.length
|
||||
// subtract this event's duration from its parent's self time
|
||||
if (stack.length) stack[stack.length - 1]._childDur += e.dur
|
||||
// attribute artifact time (e.g. profiler start) to the enclosing
|
||||
// top-level task at any depth, so it can be discounted later
|
||||
if (stack.length && ARTIFACT_NAMES.has(e.name)) stack[0]._artifactDur = (stack[0]._artifactDur || 0) + e.dur
|
||||
e._childDur = 0
|
||||
stack.push(e)
|
||||
if (depth === 0) topLevelTasks.push(e)
|
||||
}
|
||||
}
|
||||
|
||||
for (const [, list] of byThread) {
|
||||
for (const e of list) {
|
||||
const self = e.dur - (e._childDur || 0)
|
||||
let a = agg.get(e.name)
|
||||
if (!a) {
|
||||
a = { count: 0, total: 0, self: 0, max: 0 }
|
||||
agg.set(e.name, a)
|
||||
}
|
||||
a.count++
|
||||
a.total += e.dur
|
||||
a.self += self
|
||||
if (e.dur > a.max) a.max = e.dur
|
||||
const cat = (e.cat || '').split(',')[0] || '(none)'
|
||||
bump(selfByCat, cat, self)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CPU profile reconstruction (Profile + ProfileChunk) -----------------
|
||||
// Group chunks by profile id, accumulate node tree + samples, attribute each
|
||||
// sample's timeDelta to its node. Aggregate self/total time per call frame.
|
||||
const profiles = new Map() // id -> { nodes: Map, selfByNode: Map }
|
||||
function getProfile(id) {
|
||||
if (!profiles.has(id)) profiles.set(id, { nodes: new Map(), selfByNode: new Map() })
|
||||
return profiles.get(id)
|
||||
}
|
||||
for (const e of events) {
|
||||
if (e.name !== 'ProfileChunk' && e.name !== 'Profile') continue
|
||||
const id = e.id ?? `${e.pid}:${e.tid}`
|
||||
const p = getProfile(id)
|
||||
const data = e.args?.data || {}
|
||||
const cp = data.cpuProfile || data
|
||||
if (Array.isArray(cp.nodes)) {
|
||||
for (const n of cp.nodes) p.nodes.set(n.id, n)
|
||||
// build parent links: nodes either list children ids or carry a parent id
|
||||
for (const n of cp.nodes) {
|
||||
if (n.parent != null) n._parent = n.parent
|
||||
if (Array.isArray(n.children)) {
|
||||
for (const c of n.children) {
|
||||
const child = p.nodes.get(c)
|
||||
if (child) child._parent = n.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Always accumulate nodes (they're defined incrementally and an in-window
|
||||
// sample may reference one declared earlier), but only attribute samples
|
||||
// from chunks inside the window. Chunk-level granularity is coarse but fine
|
||||
// for focusing on a moment.
|
||||
if (!inWin(e.ts)) continue
|
||||
const samples = cp.samples || data.samples
|
||||
const deltas = data.timeDeltas || cp.timeDeltas
|
||||
if (Array.isArray(samples) && Array.isArray(deltas)) {
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const nid = samples[i]
|
||||
const dt = deltas[i] || 0
|
||||
p.selfByNode.set(nid, (p.selfByNode.get(nid) || 0) + dt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregate by call frame across all profiles.
|
||||
const frameSelf = new Map() // key -> { self, fn, loc }
|
||||
const nodeTotal = new Map() // `${id}:${nodeId}` -> total (self + descendants)
|
||||
let cpuTotal = 0
|
||||
function frameKey(cf) {
|
||||
const fn = cf.functionName || '(anonymous)'
|
||||
const loc = cf.url ? `${cf.url}:${cf.lineNumber ?? '?'}` : cf.codeType || ''
|
||||
return { key: `${fn}@@${loc}`, fn, loc }
|
||||
}
|
||||
for (const [, p] of profiles) {
|
||||
// self per frame + roll self up to ancestors for total time
|
||||
for (const [nid, self] of p.selfByNode) {
|
||||
cpuTotal += self
|
||||
const node = p.nodes.get(nid)
|
||||
if (!node) continue
|
||||
const { key, fn, loc } = frameKey(node.callFrame || {})
|
||||
let f = frameSelf.get(key)
|
||||
if (!f) {
|
||||
f = { self: 0, total: 0, fn, loc, count: 0 }
|
||||
frameSelf.set(key, f)
|
||||
}
|
||||
f.self += self
|
||||
// walk ancestors, add to each frame's total (dedup per frame per sample-origin)
|
||||
const seenFrames = new Set()
|
||||
let cur = node
|
||||
let guard = 0
|
||||
while (cur && guard++ < 1000) {
|
||||
const fk = frameKey(cur.callFrame || {})
|
||||
if (!seenFrames.has(fk.key)) {
|
||||
seenFrames.add(fk.key)
|
||||
let tf = frameSelf.get(fk.key)
|
||||
if (!tf) {
|
||||
tf = { self: 0, total: 0, fn: fk.fn, loc: fk.loc, count: 0 }
|
||||
frameSelf.set(fk.key, tf)
|
||||
}
|
||||
tf.total += self
|
||||
}
|
||||
cur = cur._parent != null ? p.nodes.get(cur._parent) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- section builders ----------------------------------------------------
|
||||
const out = []
|
||||
const N = opts.top
|
||||
const offset = (ts) => ms(ts - minTs) // ts -> ms offset from trace start
|
||||
|
||||
// Wall-clock correlation: metadata.startTime (UTC) anchors offset 0, so trace
|
||||
// timestamps can be lined up against server logs (zero-cache, sync-worker).
|
||||
const startEpoch = raw.metadata?.startTime ? Date.parse(raw.metadata.startTime) : NaN
|
||||
const wallEnabled = opts.wallClock && !Number.isNaN(startEpoch)
|
||||
const wall = (ts) => new Date(startEpoch + (ts - minTs) / 1000).toISOString().slice(11, 23) + 'Z'
|
||||
// header cell + row cell for an optional "UTC" column on time-localized tables
|
||||
const utcHead = wallEnabled ? ' UTC |' : ''
|
||||
const utcCell = (ts) => (wallEnabled ? ` ${wall(ts)} |` : '')
|
||||
|
||||
const builders = {
|
||||
summary() {
|
||||
out.push(`# Trace summary: ${opts.file.split('/').pop()}`)
|
||||
out.push('')
|
||||
if (opts.window) {
|
||||
out.push(`- **Window: ${opts.window.start}–${opts.window.end} ms** (of ${ms(traceDur)} ms total)`)
|
||||
out.push(`- Events in window: **${scopedCount.toLocaleString()}** / ${events.length.toLocaleString()}`)
|
||||
} else {
|
||||
out.push(`- Events: **${events.length.toLocaleString()}**`)
|
||||
out.push(`- Wall-clock span: **${ms(traceDur)} ms**`)
|
||||
}
|
||||
if (cpuTotal) out.push(`- Sampled CPU time (JS): **${ms(cpuTotal)} ms**`)
|
||||
out.push(`- Threads with timed work: **${byThread.size}**`)
|
||||
if (cpuTotal) {
|
||||
const idle = frameSelf.get('(idle)@@other')?.self || 0
|
||||
out.push(`- Idle (sampled): **${ms(idle)} ms (${pct(idle, cpuTotal)}%)**`)
|
||||
}
|
||||
out.push('')
|
||||
},
|
||||
|
||||
timeline() {
|
||||
// Per-second main-thread busy time over the WHOLE trace (ignores --window;
|
||||
// its purpose is to locate when activity happens so you can then window).
|
||||
const buckets = new Map()
|
||||
for (const e of topLevelTasks) {
|
||||
const sec = Math.floor(offset(e.ts) / 1000)
|
||||
buckets.set(sec, (buckets.get(sec) || 0) + e.dur)
|
||||
}
|
||||
out.push(`## Main-thread busy time (per second)`)
|
||||
out.push('')
|
||||
out.push('| sec | busy (ms) |')
|
||||
out.push('|---|---|')
|
||||
;[...buckets.entries()]
|
||||
.sort((a, b) => a[0] - b[0])
|
||||
.filter(([, d]) => d / 1000 >= 5)
|
||||
.forEach(([s, d]) => out.push(`| ${s} | ${ms(d)} |`))
|
||||
out.push('')
|
||||
},
|
||||
|
||||
longtasks() {
|
||||
const longMs = opts.longTaskMs
|
||||
const tasks = topLevelTasks
|
||||
// drop tasks that are themselves an artifact or are >50% artifact time
|
||||
.filter((e) => e.dur >= longMs * 1000 && !ARTIFACT_NAMES.has(e.name) && (e._artifactDur || 0) < e.dur * 0.5)
|
||||
.sort((a, b) => b.dur - a.dur)
|
||||
.slice(0, N)
|
||||
out.push(`## Long tasks (top-level ≥ ${longMs} ms)`)
|
||||
out.push('')
|
||||
if (!tasks.length) {
|
||||
out.push(`_None ≥ ${longMs} ms (excluding profiler artifacts)._`)
|
||||
} else {
|
||||
out.push(`| # | dur (ms) | @ offset (ms) |${utcHead} name | thread |`)
|
||||
out.push(`|---|---|---|${wallEnabled ? '---|' : ''}---|---|`)
|
||||
tasks.forEach((e, i) => {
|
||||
const tn = threadNames.get(`${e.pid}:${e.tid}`) || `tid ${e.tid}`
|
||||
out.push(`| ${i + 1} | ${ms(e.dur)} | ${offset(e.ts)} |${utcCell(e.ts)} ${e.name} | ${tn} |`)
|
||||
})
|
||||
}
|
||||
out.push('')
|
||||
},
|
||||
|
||||
events() {
|
||||
const rows = [...agg.entries()]
|
||||
.map(([name, a]) => ({ name, ...a }))
|
||||
.filter((a) => matches(a.name))
|
||||
.sort((a, b) => b.self - a.self)
|
||||
.slice(0, N)
|
||||
out.push(`## Hottest event types (by self time)`)
|
||||
out.push('')
|
||||
out.push('| name | count | self (ms) | total (ms) | avg (ms) | max (ms) |')
|
||||
out.push('|---|---|---|---|---|---|')
|
||||
for (const a of rows) {
|
||||
out.push(
|
||||
`| ${a.name} | ${a.count.toLocaleString()} | ${ms(a.self)} | ${ms(a.total)} | ${ms(a.total / a.count)} | ${ms(a.max)} |`
|
||||
)
|
||||
}
|
||||
out.push('')
|
||||
},
|
||||
|
||||
frequent() {
|
||||
const rows = [...agg.entries()]
|
||||
.map(([name, a]) => ({ name, ...a }))
|
||||
.filter((a) => matches(a.name))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, N)
|
||||
out.push(`## Most frequent event types (by count)`)
|
||||
out.push('')
|
||||
out.push('| name | count | total (ms) | self (ms) | avg (ms) |')
|
||||
out.push('|---|---|---|---|---|')
|
||||
for (const a of rows) {
|
||||
out.push(`| ${a.name} | ${a.count.toLocaleString()} | ${ms(a.total)} | ${ms(a.self)} | ${ms(a.total / a.count)} |`)
|
||||
}
|
||||
out.push('')
|
||||
},
|
||||
|
||||
functions() {
|
||||
if (!frameSelf.size) return
|
||||
// Drop synthetic idle/program frames; idle is reported in the header.
|
||||
const noise = new Set(['(idle)', '(program)', '(root)'])
|
||||
const rows = [...frameSelf.values()]
|
||||
.filter((f) => f.self > 0 && !noise.has(f.fn) && (matches(f.fn) || matches(f.loc)))
|
||||
.sort((a, b) => b.self - a.self)
|
||||
.slice(0, N)
|
||||
out.push(`## Hottest JS functions (CPU profile, self time)`)
|
||||
out.push('')
|
||||
out.push('| function | self (ms) | self % | total (ms) | location |')
|
||||
out.push('|---|---|---|---|---|')
|
||||
for (const f of rows) out.push(`| ${f.fn} | ${ms(f.self)} | ${pct(f.self, cpuTotal)}% | ${ms(f.total)} | ${f.loc} |`)
|
||||
out.push('')
|
||||
},
|
||||
|
||||
categories() {
|
||||
const rows = [...selfByCat.entries()].sort((a, b) => b[1] - a[1]).slice(0, 15)
|
||||
out.push(`## Self time by category`)
|
||||
out.push('')
|
||||
out.push('| category | self (ms) |')
|
||||
out.push('|---|---|')
|
||||
for (const [cat, dur] of rows) out.push(`| ${cat} | ${ms(dur)} |`)
|
||||
out.push('')
|
||||
},
|
||||
|
||||
network() {
|
||||
// Reconstruct requests from Resource* events keyed by requestId.
|
||||
const NET = new Set(['ResourceSendRequest', 'ResourceReceiveResponse', 'ResourceFinish'])
|
||||
const reqs = new Map()
|
||||
for (const e of events) {
|
||||
if (!NET.has(e.name)) continue
|
||||
const d = e.args?.data || {}
|
||||
const id = d.requestId
|
||||
if (!id) continue
|
||||
let r = reqs.get(id)
|
||||
if (!r) reqs.set(id, (r = {}))
|
||||
if (e.name === 'ResourceSendRequest') {
|
||||
r.url = d.url
|
||||
r.send = e.ts
|
||||
r.method = d.requestMethod
|
||||
} else if (e.name === 'ResourceReceiveResponse') {
|
||||
r.resp = e.ts
|
||||
r.status = d.statusCode
|
||||
} else if (e.name === 'ResourceFinish') {
|
||||
r.finish = e.ts
|
||||
r.size = d.encodedDataLength
|
||||
}
|
||||
}
|
||||
const list = [...reqs.values()]
|
||||
.filter((r) => r.send && inWin(r.send) && matches(r.url))
|
||||
.sort((a, b) => a.send - b.send)
|
||||
out.push(`## Network requests (${list.length})`)
|
||||
out.push('')
|
||||
if (!list.length) {
|
||||
out.push('_None in scope._')
|
||||
out.push('')
|
||||
return
|
||||
}
|
||||
out.push(`| @ offset (ms) |${utcHead} ttfb (ms) | dur (ms) | size (KB) | status | url |`)
|
||||
out.push(`|---|${wallEnabled ? '---|' : ''}---|---|---|---|---|`)
|
||||
for (const r of list.slice(0, N)) {
|
||||
const ttfb = r.resp ? ms(r.resp - r.send) : '-'
|
||||
const dur = r.finish ? ms(r.finish - r.send) : '-'
|
||||
const kb = r.size ? Math.round(r.size / 102.4) / 10 : '-'
|
||||
const url = (r.url || '').replace(/^https?:\/\//, '').slice(0, 90)
|
||||
out.push(`| ${offset(r.send)} |${utcCell(r.send)} ${ttfb} | ${dur} | ${kb} | ${r.status || ''} | ${url} |`)
|
||||
}
|
||||
if (list.length > N) out.push(`\n_…${list.length - N} more (raise --top)._`)
|
||||
out.push('')
|
||||
},
|
||||
|
||||
websocket() {
|
||||
const ws = events
|
||||
.filter((e) => /WebSocket/.test(e.name || '') && inWin(e.ts))
|
||||
.sort((a, b) => a.ts - b.ts)
|
||||
out.push(`## WebSocket timeline (${ws.length})`)
|
||||
out.push('')
|
||||
if (!ws.length) {
|
||||
out.push('_No WebSocket events in scope._')
|
||||
out.push('')
|
||||
return
|
||||
}
|
||||
out.push(`| @ offset (ms) |${utcHead} event | url |`)
|
||||
out.push(`|---|${wallEnabled ? '---|' : ''}---|---|`)
|
||||
for (const e of ws.slice(0, N)) {
|
||||
const url = (e.args?.data?.url || '').slice(0, 80)
|
||||
out.push(`| ${offset(e.ts)} |${utcCell(e.ts)} ${e.name} | ${url} |`)
|
||||
}
|
||||
out.push('')
|
||||
},
|
||||
}
|
||||
|
||||
// Emit selected sections in canonical order.
|
||||
for (const key of ALL_SECTIONS) if (selected.has(key)) builders[key]()
|
||||
|
||||
const report = out.join('\n').replace(/\n{3,}/g, '\n\n')
|
||||
if (opts.out) {
|
||||
fs.writeFileSync(opts.out, report)
|
||||
console.error(`Wrote ${opts.out} (${report.length} bytes) from ${(fs.statSync(opts.file).size / 1e6).toFixed(1)} MB trace`)
|
||||
} else {
|
||||
process.stdout.write(report)
|
||||
}
|
||||
201
skills/skill-creator/LICENSE.txt
Normal file
201
skills/skill-creator/LICENSE.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
355
skills/skill-creator/SKILL.md
Normal file
355
skills/skill-creator/SKILL.md
Normal file
@@ -0,0 +1,355 @@
|
||||
---
|
||||
name: skill-creator
|
||||
description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends an AI agent's capabilities with specialized knowledge, workflows, or tool integrations.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
# Skill Creator
|
||||
|
||||
This skill provides guidance for creating effective skills.
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular, self-contained packages that extend an AI agent's capabilities by providing
|
||||
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
|
||||
domains or tasks—they transform a general-purpose AI agent into a specialized agent
|
||||
equipped with procedural knowledge that no model can fully possess.
|
||||
|
||||
### What Skills Provide
|
||||
|
||||
1. Specialized workflows - Multi-step procedures for specific domains
|
||||
2. Tool integrations - Instructions for working with specific file formats or APIs
|
||||
3. Domain expertise - Company-specific knowledge, schemas, business logic
|
||||
4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Concise is Key
|
||||
|
||||
The context window is a public good. Skills share the context window with everything else the agent needs: system prompt, conversation history, other Skills' metadata, and the actual user request.
|
||||
|
||||
**Default assumption: the agent is already capable.** Only add context the agent does not already have. Challenge each piece of information: "Does the agent really need this explanation?" and "Does this paragraph justify its token cost?"
|
||||
|
||||
Prefer concise examples over verbose explanations.
|
||||
|
||||
### Set Appropriate Degrees of Freedom
|
||||
|
||||
Match the level of specificity to the task's fragility and variability:
|
||||
|
||||
**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach.
|
||||
|
||||
**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior.
|
||||
|
||||
**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed.
|
||||
|
||||
Think of the agent as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom).
|
||||
|
||||
### Anatomy of a Skill
|
||||
|
||||
Every skill consists of a required SKILL.md file and optional bundled resources:
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter metadata (required)
|
||||
│ │ ├── name: (required)
|
||||
│ │ └── description: (required)
|
||||
│ └── Markdown instructions (required)
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code (TypeScript/Python/Bash/etc.)
|
||||
├── references/ - Documentation intended to be loaded into context as needed
|
||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
||||
```
|
||||
|
||||
#### SKILL.md (required)
|
||||
|
||||
Every SKILL.md consists of:
|
||||
|
||||
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that an agent reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
|
||||
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
|
||||
|
||||
#### Bundled Resources (optional)
|
||||
|
||||
##### Scripts (`scripts/`)
|
||||
|
||||
Executable code (TypeScript/Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten.
|
||||
|
||||
- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed
|
||||
- **Example**: `scripts/rotate-pdf.ts` for PDF rotation tasks
|
||||
- **Benefits**: Token efficient, deterministic, may be executed without loading into context
|
||||
- **Note**: Scripts may still need to be read by the agent for patching or environment-specific adjustments
|
||||
|
||||
##### References (`references/`)
|
||||
|
||||
Documentation and reference material intended to be loaded as needed into context to inform the agent's process and thinking.
|
||||
|
||||
- **When to include**: For documentation that an agent should reference while working
|
||||
- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications
|
||||
- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides
|
||||
- **Benefits**: Keeps SKILL.md lean, loaded only when the agent determines it is needed
|
||||
- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md
|
||||
- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files.
|
||||
|
||||
##### Assets (`assets/`)
|
||||
|
||||
Files not intended to be loaded into context, but rather used within the output the agent produces.
|
||||
|
||||
- **When to include**: When the skill needs files that will be used in the final output
|
||||
- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography
|
||||
- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified
|
||||
- **Benefits**: Separates output resources from documentation, enables agents to use files without loading them into context
|
||||
|
||||
#### What to Not Include in a Skill
|
||||
|
||||
A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including:
|
||||
|
||||
- README.md
|
||||
- INSTALLATION_GUIDE.md
|
||||
- QUICK_REFERENCE.md
|
||||
- CHANGELOG.md
|
||||
- etc.
|
||||
|
||||
The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion.
|
||||
|
||||
### Progressive Disclosure Design Principle
|
||||
|
||||
Skills use a three-level loading system to manage context efficiently:
|
||||
|
||||
1. **Metadata (name + description)** - Always in context (~100 words)
|
||||
2. **SKILL.md body** - When skill triggers (<5k words)
|
||||
3. **Bundled resources** - As needed by the agent (Unlimited because scripts can be executed without reading into context window)
|
||||
|
||||
#### Progressive Disclosure Patterns
|
||||
|
||||
Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them.
|
||||
|
||||
**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files.
|
||||
|
||||
**Pattern 1: High-level guide with references**
|
||||
|
||||
```markdown
|
||||
# PDF Processing
|
||||
|
||||
## Quick start
|
||||
|
||||
Extract text with pdfplumber:
|
||||
[code example]
|
||||
|
||||
## Advanced features
|
||||
|
||||
- **Form filling**: See [FORMS.md](FORMS.md) for complete guide
|
||||
- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods
|
||||
- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns
|
||||
```
|
||||
|
||||
The agent loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed.
|
||||
|
||||
**Pattern 2: Domain-specific organization**
|
||||
|
||||
For Skills with multiple domains, organize content by domain to avoid loading irrelevant context:
|
||||
|
||||
```
|
||||
bigquery-skill/
|
||||
├── SKILL.md (overview and navigation)
|
||||
└── reference/
|
||||
├── finance.md (revenue, billing metrics)
|
||||
├── sales.md (opportunities, pipeline)
|
||||
├── product.md (API usage, features)
|
||||
└── marketing.md (campaigns, attribution)
|
||||
```
|
||||
|
||||
When a user asks about sales metrics, the agent only reads sales.md.
|
||||
|
||||
Similarly, for skills supporting multiple frameworks or variants, organize by variant:
|
||||
|
||||
```
|
||||
cloud-deploy/
|
||||
├── SKILL.md (workflow + provider selection)
|
||||
└── references/
|
||||
├── aws.md (AWS deployment patterns)
|
||||
├── gcp.md (GCP deployment patterns)
|
||||
└── azure.md (Azure deployment patterns)
|
||||
```
|
||||
|
||||
When the user chooses AWS, the agent only reads aws.md.
|
||||
|
||||
**Pattern 3: Conditional details**
|
||||
|
||||
Show basic content, link to advanced content:
|
||||
|
||||
```markdown
|
||||
# DOCX Processing
|
||||
|
||||
## Creating documents
|
||||
|
||||
Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md).
|
||||
|
||||
## Editing documents
|
||||
|
||||
For simple edits, modify the XML directly.
|
||||
|
||||
**For tracked changes**: See [REDLINING.md](REDLINING.md)
|
||||
**For OOXML details**: See [OOXML.md](OOXML.md)
|
||||
```
|
||||
|
||||
The agent reads REDLINING.md or OOXML.md only when the user needs those features.
|
||||
|
||||
**Important guidelines:**
|
||||
|
||||
- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md.
|
||||
- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so the agent can see the full scope when previewing.
|
||||
|
||||
## Skill Creation Process
|
||||
|
||||
Skill creation involves these steps:
|
||||
|
||||
1. Understand the skill with concrete examples
|
||||
2. Plan reusable skill contents (scripts, references, assets)
|
||||
3. Initialize the skill (run init-skill.ts)
|
||||
4. Edit the skill (implement resources and write SKILL.md)
|
||||
5. Package the skill (run package-skill.ts)
|
||||
6. Iterate based on real usage
|
||||
|
||||
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
|
||||
|
||||
### Step 1: Understanding the Skill with Concrete Examples
|
||||
|
||||
Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill.
|
||||
|
||||
To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback.
|
||||
|
||||
For example, when building an image-editor skill, relevant questions include:
|
||||
|
||||
- "What functionality should the image-editor skill support? Editing, rotating, anything else?"
|
||||
- "Can you give some examples of how this skill would be used?"
|
||||
- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?"
|
||||
- "What would a user say that should trigger this skill?"
|
||||
|
||||
To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness.
|
||||
|
||||
Conclude this step when there is a clear sense of the functionality the skill should support.
|
||||
|
||||
### Step 2: Planning the Reusable Skill Contents
|
||||
|
||||
To turn concrete examples into an effective skill, analyze each example by:
|
||||
|
||||
1. Considering how to execute on the example from scratch
|
||||
2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly
|
||||
|
||||
Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows:
|
||||
|
||||
1. Rotating a PDF requires re-writing the same code each time
|
||||
2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill
|
||||
|
||||
Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows:
|
||||
|
||||
1. Writing a frontend webapp requires the same boilerplate HTML/React each time
|
||||
2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill
|
||||
|
||||
Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows:
|
||||
|
||||
1. Querying BigQuery requires re-discovering the table schemas and relationships each time
|
||||
2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill
|
||||
|
||||
To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets.
|
||||
|
||||
### Step 3: Initializing the Skill
|
||||
|
||||
At this point, it is time to actually create the skill.
|
||||
|
||||
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
|
||||
|
||||
When creating a new skill from scratch, always run the `init-skill.ts` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
|
||||
|
||||
Usage:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/init-skill.ts <skill-name> --path <output-directory>
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
- Creates the skill directory at the specified path
|
||||
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
|
||||
- Creates example resource directories: `scripts/`, `references/`, and `assets/`
|
||||
- Adds example files in each directory that can be customized or deleted
|
||||
|
||||
After initialization, customize or remove the generated SKILL.md and example files as needed.
|
||||
|
||||
### Step 4: Edit the Skill
|
||||
|
||||
When editing the (newly-generated or existing) skill, remember that the skill is being created for another AI agent to use. Include information that would be beneficial and non-obvious to the agent. Consider what procedural knowledge, domain-specific details, or reusable assets would help another agent execute these tasks more effectively.
|
||||
|
||||
#### Learn Proven Design Patterns
|
||||
|
||||
Consult these helpful guides based on your skill's needs:
|
||||
|
||||
- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic
|
||||
- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns
|
||||
|
||||
These files contain established best practices for effective skill design.
|
||||
|
||||
#### Start with Reusable Skill Contents
|
||||
|
||||
To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`.
|
||||
|
||||
Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion.
|
||||
|
||||
Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them.
|
||||
|
||||
#### Update SKILL.md
|
||||
|
||||
**Writing Guidelines:** Always use imperative/infinitive form.
|
||||
|
||||
##### Frontmatter
|
||||
|
||||
Write the YAML frontmatter with `name` and `description`:
|
||||
|
||||
- `name`: The skill name
|
||||
- `description`: This is the primary triggering mechanism for your skill, and helps an agent understand when to use the skill.
|
||||
- Include both what the Skill does and specific triggers/contexts for when to use it.
|
||||
- Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to the agent.
|
||||
- Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use when an agent needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
|
||||
|
||||
Do not include any other fields in YAML frontmatter.
|
||||
|
||||
##### Body
|
||||
|
||||
Write instructions for using the skill and its bundled resources.
|
||||
|
||||
### Step 5: Packaging a Skill
|
||||
|
||||
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/package-skill.ts <path/to/skill-folder>
|
||||
```
|
||||
|
||||
Optional output directory specification:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/package-skill.ts <path/to/skill-folder> ./dist
|
||||
```
|
||||
|
||||
The packaging script will:
|
||||
|
||||
1. **Validate** the skill automatically, checking:
|
||||
- YAML frontmatter format and required fields
|
||||
- Skill naming conventions and directory structure
|
||||
- Description completeness and quality
|
||||
- File organization and resource references
|
||||
|
||||
2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
|
||||
|
||||
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
|
||||
|
||||
### Step 6: Iterate
|
||||
|
||||
After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed.
|
||||
|
||||
**Iteration workflow:**
|
||||
|
||||
1. Use the skill on real tasks
|
||||
2. Notice struggles or inefficiencies
|
||||
3. Identify how SKILL.md or bundled resources should be updated
|
||||
4. Implement changes and test again
|
||||
92
skills/skill-creator/references/output-patterns.md
Normal file
92
skills/skill-creator/references/output-patterns.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Output Patterns
|
||||
|
||||
Use these patterns when skills need to produce consistent, high-quality output.
|
||||
|
||||
## Template Pattern
|
||||
|
||||
Provide templates for output format. Match the level of strictness to your needs.
|
||||
|
||||
**For strict requirements (like API responses or data formats):**
|
||||
|
||||
```markdown
|
||||
## Report structure
|
||||
|
||||
ALWAYS use this exact template structure:
|
||||
|
||||
# [Analysis Title]
|
||||
|
||||
## Executive summary
|
||||
|
||||
[One-paragraph overview of key findings]
|
||||
|
||||
## Key findings
|
||||
|
||||
- Finding 1 with supporting data
|
||||
- Finding 2 with supporting data
|
||||
- Finding 3 with supporting data
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. Specific actionable recommendation
|
||||
2. Specific actionable recommendation
|
||||
```
|
||||
|
||||
**For flexible guidance (when adaptation is useful):**
|
||||
|
||||
```markdown
|
||||
## Report structure
|
||||
|
||||
Here is a sensible default format, but use your best judgment:
|
||||
|
||||
# [Analysis Title]
|
||||
|
||||
## Executive summary
|
||||
|
||||
[Overview]
|
||||
|
||||
## Key findings
|
||||
|
||||
[Adapt sections based on what you discover]
|
||||
|
||||
## Recommendations
|
||||
|
||||
[Tailor to the specific context]
|
||||
|
||||
Adjust sections as needed for the specific analysis type.
|
||||
```
|
||||
|
||||
## Examples Pattern
|
||||
|
||||
For skills where output quality depends on seeing examples, provide input/output pairs:
|
||||
|
||||
```markdown
|
||||
## Commit message format
|
||||
|
||||
Generate commit messages following these examples:
|
||||
|
||||
**Example 1:**
|
||||
Input: Added user authentication with JWT tokens
|
||||
Output:
|
||||
```
|
||||
|
||||
feat(auth): implement JWT-based authentication
|
||||
|
||||
Add login endpoint and token validation middleware
|
||||
|
||||
```
|
||||
|
||||
**Example 2:**
|
||||
Input: Fixed bug where dates displayed incorrectly in reports
|
||||
Output:
|
||||
```
|
||||
|
||||
fix(reports): correct date formatting in timezone conversion
|
||||
|
||||
Use UTC timestamps consistently across report generation
|
||||
|
||||
```
|
||||
|
||||
Follow this style: type(scope): brief description, then detailed explanation.
|
||||
```
|
||||
|
||||
Examples help the agent understand the desired style and level of detail more clearly than descriptions alone.
|
||||
28
skills/skill-creator/references/workflows.md
Normal file
28
skills/skill-creator/references/workflows.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Workflow Patterns
|
||||
|
||||
## Sequential Workflows
|
||||
|
||||
For complex tasks, break operations into clear, sequential steps. It is often helpful to give the agent an overview of the process towards the beginning of SKILL.md:
|
||||
|
||||
```markdown
|
||||
Filling a PDF form involves these steps:
|
||||
|
||||
1. Analyze the form (run analyze_form.py)
|
||||
2. Create field mapping (edit fields.json)
|
||||
3. Validate mapping (run validate_fields.py)
|
||||
4. Fill the form (run fill_form.py)
|
||||
5. Verify output (run verify_output.py)
|
||||
```
|
||||
|
||||
## Conditional Workflows
|
||||
|
||||
For tasks with branching logic, guide the agent through decision points:
|
||||
|
||||
```markdown
|
||||
1. Determine the modification type:
|
||||
**Creating new content?** → Follow "Creation workflow" below
|
||||
**Editing existing content?** → Follow "Editing workflow" below
|
||||
|
||||
2. Creation workflow: [steps]
|
||||
3. Editing workflow: [steps]
|
||||
```
|
||||
255
skills/skill-creator/scripts/init-skill.ts
Normal file
255
skills/skill-creator/scripts/init-skill.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Skill Initializer - Creates a new skill from template
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx init-skill.ts <skill-name> --path <path>
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx init-skill.ts my-new-skill --path skills/public
|
||||
* npx tsx init-skill.ts my-api-helper --path skills/private
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const SKILL_TEMPLATE = `---
|
||||
name: {{skill_name}}
|
||||
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
||||
---
|
||||
|
||||
# {{skill_title}}
|
||||
|
||||
## Overview
|
||||
|
||||
[TODO: 1-2 sentences explaining what this skill enables]
|
||||
|
||||
## Structuring This Skill
|
||||
|
||||
[TODO: Choose the structure that best fits this skill's purpose. Common patterns:
|
||||
|
||||
**1. Workflow-Based** (best for sequential processes)
|
||||
- Works well when there are clear step-by-step procedures
|
||||
- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing"
|
||||
- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2...
|
||||
|
||||
**2. Task-Based** (best for tool collections)
|
||||
- Works well when the skill offers different operations/capabilities
|
||||
- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text"
|
||||
- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2...
|
||||
|
||||
**3. Reference/Guidelines** (best for standards or specifications)
|
||||
- Works well for brand guidelines, coding standards, or requirements
|
||||
- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features"
|
||||
- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage...
|
||||
|
||||
**4. Capabilities-Based** (best for integrated systems)
|
||||
- Works well when the skill provides multiple interrelated features
|
||||
- Example: Product Management with "Core Capabilities" → numbered capability list
|
||||
- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature...
|
||||
|
||||
Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations).
|
||||
|
||||
Delete this entire "Structuring This Skill" section when done - it's just guidance.]
|
||||
|
||||
## [TODO: Replace with the first main section based on chosen structure]
|
||||
|
||||
[TODO: Add content here. See examples in existing skills:
|
||||
- Code samples for technical skills
|
||||
- Decision trees for complex workflows
|
||||
- Concrete examples with realistic user requests
|
||||
- References to scripts/templates/references as needed]
|
||||
|
||||
## Resources
|
||||
|
||||
This skill includes example resource directories that demonstrate how to organize different types of bundled resources:
|
||||
|
||||
### scripts/
|
||||
Executable code (TypeScript/Python/Bash/etc.) that can be run directly to perform specific operations.
|
||||
|
||||
### references/
|
||||
Documentation and reference material intended to be loaded into context to inform the agent's process and thinking.
|
||||
|
||||
### assets/
|
||||
Files not intended to be loaded into context, but rather used within the output the agent produces.
|
||||
|
||||
---
|
||||
|
||||
**Any unneeded directories can be deleted.** Not every skill requires all three types of resources.
|
||||
`
|
||||
|
||||
const EXAMPLE_SCRIPT = `#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Example helper script for {{skill_name}}
|
||||
*
|
||||
* This is a placeholder script that can be executed directly.
|
||||
* Replace with actual implementation or delete if not needed.
|
||||
*/
|
||||
|
||||
function main() {
|
||||
console.log('This is an example script for {{skill_name}}')
|
||||
// TODO: Add actual script logic here
|
||||
}
|
||||
|
||||
main()
|
||||
`
|
||||
|
||||
const EXAMPLE_REFERENCE = `# Reference Documentation for {{skill_title}}
|
||||
|
||||
This is a placeholder for detailed reference documentation.
|
||||
Replace with actual reference content or delete if not needed.
|
||||
|
||||
## When Reference Docs Are Useful
|
||||
|
||||
Reference docs are ideal for:
|
||||
- Comprehensive API documentation
|
||||
- Detailed workflow guides
|
||||
- Complex multi-step processes
|
||||
- Information too lengthy for main SKILL.md
|
||||
- Content that's only needed for specific use cases
|
||||
`
|
||||
|
||||
const EXAMPLE_ASSET = `# Example Asset File
|
||||
|
||||
This placeholder represents where asset files would be stored.
|
||||
Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed.
|
||||
|
||||
Asset files are NOT intended to be loaded into context, but rather used within
|
||||
the output the agent produces.
|
||||
|
||||
## Common Asset Types
|
||||
|
||||
- Templates: .pptx, .docx, boilerplate directories
|
||||
- Images: .png, .jpg, .svg, .gif
|
||||
- Fonts: .ttf, .otf, .woff, .woff2
|
||||
- Boilerplate code: Project directories, starter files
|
||||
`
|
||||
|
||||
function titleCase(skillName: string): string {
|
||||
return skillName
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function fillTemplate(template: string, skillName: string, skillTitle: string): string {
|
||||
return template
|
||||
.replace(/\{\{skill_name\}\}/g, skillName)
|
||||
.replace(/\{\{skill_title\}\}/g, skillTitle)
|
||||
}
|
||||
|
||||
function validateSkillName(skillName: string): string | null {
|
||||
// Check for valid hyphen-case format
|
||||
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
||||
return `Name '${skillName}' should be hyphen-case (lowercase letters, digits, and hyphens only)`
|
||||
}
|
||||
if (skillName.startsWith('-') || skillName.endsWith('-') || skillName.includes('--')) {
|
||||
return `Name '${skillName}' cannot start/end with hyphen or contain consecutive hyphens`
|
||||
}
|
||||
if (skillName.length > 64) {
|
||||
return `Name is too long (${skillName.length} characters). Maximum is 64 characters.`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function initSkill(skillName: string, basePath: string): string | null {
|
||||
// Validate skill name format
|
||||
const nameError = validateSkillName(skillName)
|
||||
if (nameError) {
|
||||
console.log(`❌ Error: ${nameError}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const skillDir = path.resolve(basePath, skillName)
|
||||
|
||||
// Check if directory already exists
|
||||
if (fs.existsSync(skillDir)) {
|
||||
console.log(`❌ Error: Skill directory already exists: ${skillDir}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const skillTitle = titleCase(skillName)
|
||||
|
||||
let dirCreated = false
|
||||
try {
|
||||
// Create skill directory
|
||||
fs.mkdirSync(skillDir, { recursive: true })
|
||||
dirCreated = true
|
||||
console.log(`✅ Created skill directory: ${skillDir}`)
|
||||
|
||||
// Create SKILL.md
|
||||
const skillContent = fillTemplate(SKILL_TEMPLATE, skillName, skillTitle)
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent)
|
||||
console.log('✅ Created SKILL.md')
|
||||
|
||||
// Create scripts/ directory with example
|
||||
const scriptsDir = path.join(skillDir, 'scripts')
|
||||
fs.mkdirSync(scriptsDir)
|
||||
fs.writeFileSync(
|
||||
path.join(scriptsDir, 'example.ts'),
|
||||
fillTemplate(EXAMPLE_SCRIPT, skillName, skillTitle)
|
||||
)
|
||||
console.log('✅ Created scripts/example.ts')
|
||||
|
||||
// Create references/ directory with example
|
||||
const referencesDir = path.join(skillDir, 'references')
|
||||
fs.mkdirSync(referencesDir)
|
||||
fs.writeFileSync(
|
||||
path.join(referencesDir, 'api-reference.md'),
|
||||
fillTemplate(EXAMPLE_REFERENCE, skillName, skillTitle)
|
||||
)
|
||||
console.log('✅ Created references/api-reference.md')
|
||||
|
||||
// Create assets/ directory with example
|
||||
const assetsDir = path.join(skillDir, 'assets')
|
||||
fs.mkdirSync(assetsDir)
|
||||
fs.writeFileSync(
|
||||
path.join(assetsDir, 'example-asset.txt'),
|
||||
fillTemplate(EXAMPLE_ASSET, skillName, skillTitle)
|
||||
)
|
||||
console.log('✅ Created assets/example-asset.txt')
|
||||
|
||||
console.log(`\n✅ Skill '${skillName}' initialized successfully at ${skillDir}`)
|
||||
console.log('\nNext steps:')
|
||||
console.log('1. Edit SKILL.md to complete the TODO items and update the description')
|
||||
console.log('2. Customize or delete the example files in scripts/, references/, and assets/')
|
||||
console.log('3. Run the validator when ready to check the skill structure')
|
||||
|
||||
return skillDir
|
||||
} catch (err) {
|
||||
// Clean up partially created directory on failure
|
||||
if (dirCreated && fs.existsSync(skillDir)) {
|
||||
try {
|
||||
fs.rmSync(skillDir, { recursive: true })
|
||||
console.log(`🧹 Cleaned up partial directory: ${skillDir}`)
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
console.log(`❌ Error: ${err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length < 3 || args[1] !== '--path') {
|
||||
console.log('Usage: npx tsx init-skill.ts <skill-name> --path <path>')
|
||||
console.log('\nSkill name requirements:')
|
||||
console.log(" - Hyphen-case identifier (e.g., 'data-analyzer')")
|
||||
console.log(' - Lowercase letters, digits, and hyphens only')
|
||||
console.log(' - Max 40 characters')
|
||||
console.log('\nExamples:')
|
||||
console.log(' npx tsx init-skill.ts my-new-skill --path skills')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const skillName = args[0]
|
||||
const basePath = args[2]
|
||||
|
||||
console.log(`🚀 Initializing skill: ${skillName}`)
|
||||
console.log(` Location: ${basePath}`)
|
||||
console.log()
|
||||
|
||||
const result = initSkill(skillName, basePath)
|
||||
process.exit(result ? 0 : 1)
|
||||
152
skills/skill-creator/scripts/package-skill.ts
Normal file
152
skills/skill-creator/scripts/package-skill.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Skill Packager - Creates a distributable .skill file of a skill folder
|
||||
*
|
||||
* Usage:
|
||||
* npx tsx package-skill.ts <path/to/skill-folder> [output-directory]
|
||||
*
|
||||
* Example:
|
||||
* npx tsx package-skill.ts skills/my-skill
|
||||
* npx tsx package-skill.ts skills/my-skill ./dist
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as os from 'os'
|
||||
import * as path from 'path'
|
||||
import { validateSkill } from './quick-validate'
|
||||
|
||||
function getAllFiles(dir: string, baseDir: string = dir): string[] {
|
||||
const files: string[] = []
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...getAllFiles(fullPath, baseDir))
|
||||
} else {
|
||||
files.push(fullPath)
|
||||
}
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
function packageSkill(skillPath: string, outputDir?: string): string | null {
|
||||
const resolvedSkillPath = path.resolve(skillPath)
|
||||
|
||||
// Validate skill folder exists
|
||||
if (!fs.existsSync(resolvedSkillPath)) {
|
||||
console.log(`❌ Error: Skill folder not found: ${resolvedSkillPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (!fs.statSync(resolvedSkillPath).isDirectory()) {
|
||||
console.log(`❌ Error: Path is not a directory: ${resolvedSkillPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Validate SKILL.md exists
|
||||
const skillMdPath = path.join(resolvedSkillPath, 'SKILL.md')
|
||||
if (!fs.existsSync(skillMdPath)) {
|
||||
console.log(`❌ Error: SKILL.md not found in ${resolvedSkillPath}`)
|
||||
return null
|
||||
}
|
||||
|
||||
// Run validation before packaging
|
||||
console.log('🔍 Validating skill...')
|
||||
const { valid, message } = validateSkill(resolvedSkillPath)
|
||||
if (!valid) {
|
||||
console.log(`❌ Validation failed: ${message}`)
|
||||
console.log(' Please fix the validation errors before packaging.')
|
||||
return null
|
||||
}
|
||||
console.log(`✅ ${message}\n`)
|
||||
|
||||
// Determine output location
|
||||
const skillName = path.basename(resolvedSkillPath)
|
||||
|
||||
// Validate directory name is safe for shell commands (same rules as skill names)
|
||||
if (!/^[a-z0-9-]+$/.test(skillName)) {
|
||||
console.log(
|
||||
`❌ Error: Directory name '${skillName}' contains unsafe characters. Use only lowercase letters, digits, and hyphens.`
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const resolvedOutputDir = outputDir ? path.resolve(outputDir) : process.cwd()
|
||||
|
||||
if (!fs.existsSync(resolvedOutputDir)) {
|
||||
fs.mkdirSync(resolvedOutputDir, { recursive: true })
|
||||
}
|
||||
|
||||
const skillFilename = path.join(resolvedOutputDir, `${skillName}.skill`)
|
||||
|
||||
let tempDir: string | null = null
|
||||
try {
|
||||
// Get all files in the skill directory
|
||||
const files = getAllFiles(resolvedSkillPath)
|
||||
|
||||
// Create a temporary directory for the zip structure (cross-platform)
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-package-'))
|
||||
const tempSkillDir = path.join(tempDir, skillName)
|
||||
fs.mkdirSync(tempSkillDir, { recursive: true })
|
||||
|
||||
// Copy all files maintaining structure
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(resolvedSkillPath, file)
|
||||
const destPath = path.join(tempSkillDir, relativePath)
|
||||
const destDir = path.dirname(destPath)
|
||||
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true })
|
||||
}
|
||||
|
||||
fs.copyFileSync(file, destPath)
|
||||
console.log(` Added: ${skillName}/${relativePath}`)
|
||||
}
|
||||
|
||||
// Create zip file using system zip command
|
||||
const zipPath = path.resolve(skillFilename)
|
||||
execSync(`cd "${tempDir}" && zip -r "${zipPath}" "${skillName}"`, { stdio: 'pipe' })
|
||||
|
||||
// Cleanup temp directory
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
|
||||
console.log(`\n✅ Successfully packaged skill to: ${skillFilename}`)
|
||||
return skillFilename
|
||||
} catch (err) {
|
||||
// Cleanup temp directory on error
|
||||
if (tempDir && fs.existsSync(tempDir)) {
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true })
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
console.log(`❌ Error creating .skill file: ${err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length < 1) {
|
||||
console.log('Usage: npx tsx package-skill.ts <path/to/skill-folder> [output-directory]')
|
||||
console.log('\nExample:')
|
||||
console.log(' npx tsx package-skill.ts skills/my-skill')
|
||||
console.log(' npx tsx package-skill.ts skills/my-skill ./dist')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const skillPath = args[0]
|
||||
const outputDir = args[1]
|
||||
|
||||
console.log(`📦 Packaging skill: ${skillPath}`)
|
||||
if (outputDir) {
|
||||
console.log(` Output directory: ${outputDir}`)
|
||||
}
|
||||
console.log()
|
||||
|
||||
const result = packageSkill(skillPath, outputDir)
|
||||
process.exit(result ? 0 : 1)
|
||||
152
skills/skill-creator/scripts/quick-validate.ts
Normal file
152
skills/skill-creator/scripts/quick-validate.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
/**
|
||||
* Quick validation script for skills - minimal version
|
||||
*/
|
||||
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
const ALLOWED_PROPERTIES = new Set([
|
||||
'name',
|
||||
'description',
|
||||
'license',
|
||||
'compatibility',
|
||||
'allowed-tools',
|
||||
'metadata',
|
||||
])
|
||||
|
||||
function parseSimpleYaml(text: string): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
for (const line of text.split('\n')) {
|
||||
const match = line.match(/^(\S+):\s*(.*)$/)
|
||||
if (match) {
|
||||
const [, key, value] = match
|
||||
result[key] = value.trim()
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function validateSkill(skillPath: string): ValidationResult {
|
||||
const skillMdPath = path.join(skillPath, 'SKILL.md')
|
||||
|
||||
// Check SKILL.md exists
|
||||
if (!fs.existsSync(skillMdPath)) {
|
||||
return { valid: false, message: 'SKILL.md not found' }
|
||||
}
|
||||
|
||||
// Read and validate frontmatter
|
||||
const content = fs.readFileSync(skillMdPath, 'utf-8')
|
||||
if (!content.startsWith('---')) {
|
||||
return { valid: false, message: 'No YAML frontmatter found' }
|
||||
}
|
||||
|
||||
// Extract frontmatter (handle both LF and CRLF line endings)
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
||||
if (!match) {
|
||||
return { valid: false, message: 'Invalid frontmatter format' }
|
||||
}
|
||||
|
||||
const frontmatterText = match[1]
|
||||
const frontmatter = parseSimpleYaml(frontmatterText)
|
||||
|
||||
// Check for unexpected properties
|
||||
const unexpectedKeys = Object.keys(frontmatter).filter((k) => !ALLOWED_PROPERTIES.has(k))
|
||||
if (unexpectedKeys.length > 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Unexpected key(s) in SKILL.md frontmatter: ${unexpectedKeys.sort().join(', ')}. Allowed properties are: ${[...ALLOWED_PROPERTIES].sort().join(', ')}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!frontmatter.name) {
|
||||
return { valid: false, message: "Missing 'name' in frontmatter" }
|
||||
}
|
||||
if (!frontmatter.description) {
|
||||
return { valid: false, message: "Missing 'description' in frontmatter" }
|
||||
}
|
||||
|
||||
// Validate name
|
||||
const name = frontmatter.name.trim()
|
||||
if (!name) {
|
||||
return { valid: false, message: 'Name cannot be empty or whitespace-only' }
|
||||
}
|
||||
{
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name '${name}' should be hyphen-case (lowercase letters, digits, and hyphens only)`,
|
||||
}
|
||||
}
|
||||
if (name.startsWith('-') || name.endsWith('-') || name.includes('--')) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name '${name}' cannot start/end with hyphen or contain consecutive hyphens`,
|
||||
}
|
||||
}
|
||||
if (name.length > 64) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name is too long (${name.length} characters). Maximum is 64 characters.`,
|
||||
}
|
||||
}
|
||||
const directoryName = path.basename(path.resolve(skillPath))
|
||||
if (name !== directoryName) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Name '${name}' must match parent directory name '${directoryName}'.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate description
|
||||
const description = frontmatter.description.trim()
|
||||
if (!description) {
|
||||
return { valid: false, message: 'Description cannot be empty or whitespace-only' }
|
||||
}
|
||||
{
|
||||
if (description.includes('<') || description.includes('>')) {
|
||||
return { valid: false, message: 'Description cannot contain angle brackets (< or >)' }
|
||||
}
|
||||
if (description.length > 1024) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Description is too long (${description.length} characters). Maximum is 1024 characters.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const compatibility = frontmatter.compatibility?.trim()
|
||||
if (compatibility !== undefined) {
|
||||
if (!compatibility) {
|
||||
return { valid: false, message: 'Compatibility cannot be empty or whitespace-only' }
|
||||
}
|
||||
if (compatibility.length > 500) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Compatibility is too long (${compatibility.length} characters). Maximum is 500 characters.`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, message: 'Skill is valid!' }
|
||||
}
|
||||
|
||||
// CLI entry point
|
||||
if (require.main === module) {
|
||||
const args = process.argv.slice(2)
|
||||
if (args.length !== 1) {
|
||||
process.stdout.write('Usage: npx tsx quick-validate.ts <skill_directory>\n')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const { valid, message } = validateSkill(args[0])
|
||||
process.stdout.write(`${message}\n`)
|
||||
process.exit(valid ? 0 : 1)
|
||||
}
|
||||
99
skills/spec-driven-tests/SKILL.md
Normal file
99
skills/spec-driven-tests/SKILL.md
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: spec-driven-tests
|
||||
description: Rebuild a package's unit test suite around a behavior specification (SPEC.md) derived from the implementation, with every test citing a numbered rule ID. Use when asked to do a spec-driven test rebuild, write a SPEC.md for a package, repeat the state or store spec process (PRs #9158, #9172) for another package, or rebuild tests around a spec.
|
||||
---
|
||||
|
||||
# Spec-driven test rebuild
|
||||
|
||||
Derive a behavior specification from a package's implementation, then rebuild the unit test suite as an expression of that spec. Every rule gets a stable ID; every test cites the rule it expresses, so coverage is greppable in both directions.
|
||||
|
||||
The models are PR #9158 (`packages/state`, spec at `packages/state/SPEC.md`) and PR #9172 (`packages/store`, spec at `packages/store/SPEC.md`). Read both PRs and both specs before starting — they set the format, tone, and PR structure. (#9172's spec lives on its branch if not yet merged.)
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Baseline
|
||||
|
||||
If the working tree has uncommitted changes that aren't yours, stop and ask before branching — never stash or carry someone else's work silently.
|
||||
|
||||
Create a branch. Run `yarn test run` in the package and record the file and test counts, noting how many belong to fuzz suites — fuzz suites stay untouched and their (often large) generated counts are excluded from every count you report later. Map the existing test files and note any duplicated or parallel suites that should merge (the store package had two whole generations of tests).
|
||||
|
||||
### 2. Read the implementation
|
||||
|
||||
Read all of it before writing anything. The spec is derived from the source, not from the existing tests or the docs.
|
||||
|
||||
**Key principle:** You're documenting *observed* behavior, not prescribing ideal behavior. If the code does something unexpected, unintuitive, or even wrong-looking but deliberate, spec it as-is. Flag quirks as "internal" or call them out in the PR body for human judgment—don't silently "fix" the spec to match how you think the code should work. Let the spec-writing process surface latent inconsistencies; then decide whether to fix the implementation or document the behavior.
|
||||
|
||||
### 3. Write SPEC.md
|
||||
|
||||
Write `SPEC.md` at the package root: numbered rules with stable IDs (e.g. `S3`, `MG4`) grouped into sections, one rule per observable behavior, internal machinery marked **internal**. Keep the header framing from the existing specs ("When a test and this document disagree, one of them is wrong — figure out which and fix it.").
|
||||
|
||||
- Spec what the code DOES, not what it should do. If behavior looks wrong but intent is ambiguous, document it as the contract.
|
||||
- Before baking in any rule you inferred rather than observed, verify it with a throwaway test. Delete the scratch file after.
|
||||
- Every rule must be observable by a runtime test. Don't write rules for pure type-level behavior (utility types, inference) unless the test suite actually exercises them with type assertions; otherwise leave types out of the spec.
|
||||
|
||||
### 4. Audit the spec adversarially
|
||||
|
||||
Audit the draft before writing tests. The failure mode is overstatement. Specifically hunt for:
|
||||
|
||||
- claimed symmetry between two code paths (create vs update, top-level vs nested, up vs down) — check each path independently;
|
||||
- claims that two implementations of the same idea agree (a predicate vs an index, from-scratch vs incremental) — feed both the edge cases, especially missing/undefined values;
|
||||
- blanket "X never happens" or "Y is always Z" claims — find the branch that falsifies them;
|
||||
- side effects on inputs (mutation, freezing) that a happy-path test wouldn't notice.
|
||||
|
||||
### 5. Rebuild the test suite
|
||||
|
||||
Test files mirror the spec's sections; every test name cites its rule like `it('[S3] ...')`. Merge duplicated coverage, port good existing tests rather than rewriting them, and add tests for uncovered rules.
|
||||
|
||||
**Never weaken an assertion to make a test pass.** If a test written from a spec rule fails, you've found something: either the rule is wrong (fix the spec to match observed behavior) or the code is wrong (handle it as a bug per step 7). Loosening the assertion (`toBe` → `toContain`, asserting around the discrepancy, a comment acknowledging the mismatch) hides the finding and leaves the spec describing behavior the code doesn't have — the exact failure the spec exists to prevent.
|
||||
|
||||
**Test file organization:** If your spec has sections like "§3. Atoms (A)" and "§4. Computed (C)", create corresponding files `atoms.test.ts` and `computed.test.ts` containing that section's rules. For internal sections, use names like `history-buffer.test.ts` for the machinery being specified. This mirrors the structure readers already see in SPEC.md and makes it easy to grep for coverage by section.
|
||||
|
||||
Test-environment knowledge for this repo:
|
||||
|
||||
- Store listener flushes are synchronous in tests unless `globalThis.__FORCE_RAF_IN_TESTS__ = true`.
|
||||
- Deprecated-but-contractual APIs need a file-level eslint-disable with a reason.
|
||||
|
||||
### 6. Cross-check citations both ways
|
||||
|
||||
```bash
|
||||
grep -oE '\*\*[A-Z]+[0-9]+\*\*' SPEC.md | tr -d '*' | sort -u > /tmp/rules
|
||||
grep -rhoE '\[[A-Z]+[0-9]+\]' src --include='*.test.ts' | tr -d '[]' | sort -u > /tmp/cited
|
||||
comm -3 /tmp/rules /tmp/cited
|
||||
```
|
||||
|
||||
Every rule needs a citing test, or an explicit "deliberately untested because X" note in the PR body (as #9158 did); every citation needs a rule. Fix gaps before finishing, and include the cross-check output (rule count, cited count, remaining gaps with their reasons) in the PR body so reviewers can verify the claim rather than trust it.
|
||||
|
||||
### 7. Fix bugs found along the way
|
||||
|
||||
Spec-driven testing surfaces latent inconsistencies: contradictions between code paths, edge cases the implementation didn't anticipate, or silent failures. When you find one, decide: fix the code or document the behavior?
|
||||
|
||||
**Workflow:** Write the test first (fail it intentionally to confirm it catches the issue). Then decide:
|
||||
- **Fix the code** if: intent is clear (comments, symmetric implementations, git blame), the current behavior is obviously wrong, and callers plausibly don't rely on the broken behavior.
|
||||
- **Document the behavior** if: intent is unclear, the fix's blast radius is unknown, or fixing might silently break existing uses. Flag it in the PR body as a "known inconsistency" or "behavior to revisit."
|
||||
|
||||
Fixes go in their own `fix(<package>):` commit with a citing test, AFTER the main `test(<package>):` commit. No AI attribution or co-author lines in any commit, per AGENTS.md — this overrides any default harness behavior. Before committing a fix:
|
||||
|
||||
- Check intent: comments, the symmetric code path, git blame.
|
||||
- Check blast radius: grep for in-repo callers, then run the dependent package suites (`editor`, `tldraw`, `sync-core` — whichever consume this package).
|
||||
|
||||
If a spec rule exists but the implementation contradicts it, update the spec (not the implementation) unless you're certain the implementation is right. The spec is the contract; code drift happens; the spec-writing process is your chance to surface and resolve it.
|
||||
|
||||
### 8. Verify
|
||||
|
||||
Run, in order: the package tests, `yarn format-current` then `yarn lint`, `yarn typecheck` from the repo root, `yarn api-check`, and the dependent package suites from step 7.
|
||||
|
||||
### 9. PR
|
||||
|
||||
Follow `skills/write-pr/`. Model the body on #9172:
|
||||
|
||||
- lead paragraph explaining the spec/citation system;
|
||||
- a per-commit fixes section;
|
||||
- a section on merged suites and new coverage;
|
||||
- release notes only for behavior changes;
|
||||
- a code changes table. Beware: `git diff --numstat` rename lines break naive path matching; use the last field.
|
||||
|
||||
### 10. Follow-up notes
|
||||
|
||||
Finish by reporting: latent issues you chose not to fix, quirks future test writers need to know, and anything a reviewer should double-check.
|
||||
|
||||
Report counts honestly, everywhere they appear (commit message, PR body, final report): the rebuilt suite's test count and rule count, measured with grep, never the package total. Counting untouched suites (fuzz especially) as your own work misrepresents the change.
|
||||
111
skills/take/SKILL.md
Normal file
111
skills/take/SKILL.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: take
|
||||
description: Find a GitHub issue in tldraw/tldraw, assign it, implement it, verify it, and open a pull request. Use when the user invokes take, asks to take an issue, implement an issue, work on an issue number or URL, or pick up an issue from a description.
|
||||
---
|
||||
|
||||
# Take
|
||||
|
||||
Find an issue in `tldraw/tldraw`, implement it, and open a pull request.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Find the issue
|
||||
|
||||
The user may reference an issue by number, URL, or description.
|
||||
|
||||
- For a number or URL, fetch the issue directly:
|
||||
|
||||
```bash
|
||||
gh issue view 123 --repo tldraw/tldraw
|
||||
```
|
||||
|
||||
- For a description, search open issues first:
|
||||
|
||||
```bash
|
||||
gh issue list --repo tldraw/tldraw --search "dark mode" --state open --limit 10
|
||||
```
|
||||
|
||||
- If no open issue matches, search all issues:
|
||||
|
||||
```bash
|
||||
gh issue list --repo tldraw/tldraw --search "dark mode" --state all --limit 10
|
||||
```
|
||||
|
||||
If there is one clear match, proceed. If several issues match, ask the user to choose from issue numbers and titles. If none match, ask whether to create a new issue using the `issue` skill.
|
||||
|
||||
### 2. Understand the issue
|
||||
|
||||
Read the full issue and comments. Identify:
|
||||
|
||||
- Issue type: bug, feature, enhancement, cleanup, docs, or task.
|
||||
- Requested behavior and acceptance criteria.
|
||||
- Technical notes and affected files.
|
||||
- Relevant discussion or clarifications.
|
||||
|
||||
Agent-drafted issues may include `## Confidence` and `## Open questions` sections. Treat any unanswered `Critical:` question, `_Awaiting answer._` entry, or `More Info Needed` label as a blocker unless codebase exploration proves the intended behavior is unambiguous. Resolve blockers with the user before implementing; non-critical questions marked `_Deferred by user; not blocking implementation._` may remain open.
|
||||
|
||||
If the issue lacks detail, explore the codebase before deciding whether implementation is safe.
|
||||
|
||||
### 3. Assign the issue
|
||||
|
||||
Assign the issue to the current GitHub user. If someone else is already assigned, ask the user whether to proceed.
|
||||
|
||||
### 4. Plan the implementation
|
||||
|
||||
Create a concise implementation checklist based on:
|
||||
|
||||
- The issue description.
|
||||
- Acceptance criteria.
|
||||
- Codebase exploration.
|
||||
- Existing repo patterns.
|
||||
|
||||
### 5. Implement
|
||||
|
||||
Create a new branch from `main`.
|
||||
|
||||
Work through the checklist:
|
||||
|
||||
- Read files before editing them.
|
||||
- Follow existing patterns.
|
||||
- Keep changes focused on the issue.
|
||||
- Avoid speculative improvements.
|
||||
- Update docs, examples, tests, or API reports when the issue requires it.
|
||||
|
||||
### 6. Verify
|
||||
|
||||
Run the smallest relevant checks first. Use broader checks when the change touches shared behavior.
|
||||
|
||||
Typical final checks:
|
||||
|
||||
```bash
|
||||
yarn typecheck
|
||||
yarn lint
|
||||
```
|
||||
|
||||
For focused package changes, prefer the relevant workspace tests before repo-wide checks.
|
||||
|
||||
### 7. Create the PR
|
||||
|
||||
Use the `pr` skill.
|
||||
|
||||
- Link the issue with `Closes #<issue-number>`.
|
||||
- Include relevant context from the issue discussion.
|
||||
- Include a clear test plan.
|
||||
|
||||
### 8. Summarize
|
||||
|
||||
End with:
|
||||
|
||||
- Issue implemented.
|
||||
- Key changes and files modified.
|
||||
- Verification performed.
|
||||
- PR link.
|
||||
- Manual testing steps, if relevant.
|
||||
- Any acceptance criteria that could not be met and why.
|
||||
|
||||
## Rules
|
||||
|
||||
- Ask the user when requirements are unclear. Do not implement past unanswered critical questions or unresolved `_Awaiting answer._` placeholders.
|
||||
- Do not guess at unspecified product behavior.
|
||||
- Keep the implementation scoped to the issue.
|
||||
- Never include AI attribution in commits, issues, or PRs.
|
||||
1
skills/tldraw-migrate/.gitignore
vendored
Normal file
1
skills/tldraw-migrate/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
references/
|
||||
82
skills/tldraw-migrate/README.md
Normal file
82
skills/tldraw-migrate/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# tldraw-migrate skill
|
||||
|
||||
> Note: Are you human? You should probably not be. Pass a link to this file to your agent of choice and have them take it from here.
|
||||
|
||||
A portable agent skill that walks an AI coding agent through upgrading a project to a newer version of the tldraw SDK.
|
||||
|
||||
This README documents distribution and installation. The skill itself lives in [`SKILL.md`](./SKILL.md).
|
||||
|
||||
## Distribution
|
||||
|
||||
The canonical source of truth is `skills/tldraw-migrate/` on `main` in the [tldraw monorepo](https://github.com/tldraw/tldraw). Distribute the skill by copying the entire folder into the agent-skills directory of the project being migrated. The skill is self-contained — `SKILL.md`, the three `.mjs` helpers, and a `references/` cache.
|
||||
|
||||
### Where to install
|
||||
|
||||
Copy `skills/tldraw-migrate/` into one of the following paths in the target project. The skill probes them in this order and uses the first one it finds:
|
||||
|
||||
| Path | Harness |
|
||||
| --- | --- |
|
||||
| `.claude/skills/tldraw-migrate/` | Claude Code |
|
||||
| `.agents/skills/tldraw-migrate/` | Generic / open agent-skills spec |
|
||||
| `.codex/skills/tldraw-migrate/` | OpenAI Codex |
|
||||
| `.cursor/skills/tldraw-migrate/` | Cursor |
|
||||
| `skills/tldraw-migrate/` | Fallback / vendored alongside source |
|
||||
|
||||
If multiple harnesses share a project, install once and symlink the others to the same folder (this is what the tldraw monorepo itself does — see `AGENTS.md`).
|
||||
|
||||
### One-shot install from GitHub
|
||||
|
||||
```sh
|
||||
DEST=.claude/skills/tldraw-migrate # or .agents/skills/tldraw-migrate, .cursor/skills/tldraw-migrate, etc.
|
||||
mkdir -p "$DEST"
|
||||
curl -fsSL https://github.com/tldraw/tldraw/archive/refs/heads/main.tar.gz \
|
||||
| tar -xz --strip-components=3 -C "$DEST" tldraw-main/skills/tldraw-migrate
|
||||
```
|
||||
|
||||
### Updating an installed copy
|
||||
|
||||
The skill changes whenever migration patterns shift in the SDK (new error categories, new helpers, edge-case fixes). Re-run the one-shot install command above to overwrite `SKILL.md` and the helper scripts, or pull the latest from `tldraw/tldraw` main. The skill's *content* — release notes and docs — is auto-refreshed on every invocation, so updating `SKILL.md` itself is only needed when the workflow changes.
|
||||
|
||||
### Skill-location override
|
||||
|
||||
If the skill is installed somewhere outside the probed locations, export `SKILL_DIR` before invocation:
|
||||
|
||||
```sh
|
||||
SKILL_DIR=/abs/path/to/tldraw-migrate
|
||||
```
|
||||
|
||||
The auto-fetch blocks and all internal grep examples honor this variable.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node ≥ 20** for the three helper scripts (`detect-versions.mjs`, `detect-target.mjs`, `fetch-release-notes.mjs`). No npm dependencies — they are zero-dep ES modules.
|
||||
- **`curl`** for the docs fetch.
|
||||
- **`git`** is optional but improves from-version detection (the skill compares `main`, `HEAD~1`, and the working tree).
|
||||
- **`GITHUB_TOKEN`** env var is optional; setting it raises the unauthenticated GitHub API rate limit (60/hr) so re-runs and long version ranges don't fail.
|
||||
|
||||
## What gets fetched on invocation
|
||||
|
||||
The skill writes three files into `references/` at run time. None of them is committed to the skill's source — they are recreated on demand.
|
||||
|
||||
| File | Source | When fetched |
|
||||
| --- | --- | --- |
|
||||
| `tldraw-releases.txt` | `github.com/tldraw/tldraw` → `apps/docs/content/releases/v*.mdx`, filtered to versions strictly greater than the detected from-version | Always |
|
||||
| `tldraw-full-docs.txt` | `tldraw.dev/llms-full.txt` | Cached locally, 30-day TTL — delete the file or wait 30 days to refresh |
|
||||
| `tldraw-next.mdx` | `github.com/tldraw/tldraw` → `apps/docs/content/releases/next.mdx` | Pre-release targets only (`canary`, `next`, `beta`, or pre-release semver) |
|
||||
|
||||
GitHub is the source of truth, deliberately. `next.mdx` does not exist on npm, and tldraw.dev can lag behind main. This means migration recipes for unreleased versions are available to the skill the moment they land in `main`.
|
||||
|
||||
## Harness compatibility
|
||||
|
||||
The `!`-prefix bash blocks at the top of `SKILL.md` are written for Claude Code's auto-execute syntax. Other harnesses (Cursor, Codex, plain Claude API, etc.) must run each block as a shell command before continuing the workflow. The skill flags this explicitly at the top of the resources section.
|
||||
|
||||
For agent harnesses without slash-command support, the skill still works as a plain workflow document — point the agent at `SKILL.md` and the same step-by-step process applies.
|
||||
|
||||
## Files in this folder
|
||||
|
||||
- `SKILL.md` — the skill workflow itself
|
||||
- `detect-versions.mjs` — resolves the from-version (explicit arg → `main` → `HEAD~1` → working tree)
|
||||
- `detect-target.mjs` — resolves the target version (default `latest`; accepts a dist-tag or semver)
|
||||
- `fetch-release-notes.mjs` — pulls release notes between two versions from `tldraw/tldraw` main
|
||||
- `references/` — auto-populated cache of fetched references (gitignored or freshly recreated)
|
||||
- `README.md` — this file
|
||||
174
skills/tldraw-migrate/SKILL.md
Normal file
174
skills/tldraw-migrate/SKILL.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
name: tldraw-migrate
|
||||
description: Migrate a project to a newer version of the tldraw SDK. Use when upgrading tldraw packages, fixing TypeScript errors after a tldraw upgrade, or when the user mentions tldraw migration.
|
||||
argument-hint: '[from-version] [target]'
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
# tldraw migration assistant
|
||||
|
||||
You are helping migrate a project to a newer version of the tldraw SDK. Follow this process carefully.
|
||||
|
||||
Throughout this skill, `${SKILL_DIR}` refers to this skill's own directory (where `SKILL.md`, the helper `.mjs` scripts, and the cached `references/` folder live). The auto-fetch blocks below resolve it from the `SKILL_DIR` env var if set, otherwise probe the common skill locations (`.claude/skills/`, `.agents/skills/`, `.codex/skills/`, `.cursor/skills/`, `skills/`). When you run shell commands later in the workflow that reference `${SKILL_DIR}`, substitute the same absolute path. If you placed this skill somewhere else, set `SKILL_DIR` before running the auto-fetch blocks.
|
||||
|
||||
**Arguments**: `/tldraw-migrate [from-version] [target]`. Both optional. `from-version` defaults to the previous tldraw version detected from git history. `target` defaults to `latest` (the latest stable release on npm); pass a dist-tag (`canary`, `next`, `beta`) or a pre-release semver (e.g. `4.6.0-canary.abc123`) to migrate to a pre-release.
|
||||
|
||||
Resolved migration: !`SKILL_DIR="${SKILL_DIR:-$(for d in .claude/skills/tldraw-migrate .agents/skills/tldraw-migrate .codex/skills/tldraw-migrate .cursor/skills/tldraw-migrate skills/tldraw-migrate; do [ -d "$d" ] && printf %s "$d" && break; done)}" && FROM=$(node "$SKILL_DIR/detect-versions.mjs" $ARGUMENTS || true) && TARGET=$(node "$SKILL_DIR/detect-target.mjs" $ARGUMENTS) && if [ -z "$FROM" ]; then echo "ERROR: could not detect previous tldraw version. Pass it explicitly: /tldraw-migrate <from-version> [target]" >&2; else echo "from $FROM → target $TARGET"; fi`
|
||||
|
||||
## Resources (auto-fetched on invocation)
|
||||
|
||||
The three blocks below are written for Claude Code's `!`-prefix auto-execute syntax. **If you are an agent that doesn't auto-run these (Cursor, Codex, plain Claude, etc.), execute each one as a shell command before continuing** — they download the changelog, full docs, and (for pre-release targets) next-release notes that the rest of the workflow depends on.
|
||||
|
||||
!`SKILL_DIR="${SKILL_DIR:-$(for d in .claude/skills/tldraw-migrate .agents/skills/tldraw-migrate .codex/skills/tldraw-migrate .cursor/skills/tldraw-migrate skills/tldraw-migrate; do [ -d "$d" ] && printf %s "$d" && break; done)}" && mkdir -p "$SKILL_DIR/references" && CHANGELOG="$SKILL_DIR/references/tldraw-releases.txt" && PREV=$(node "$SKILL_DIR/detect-versions.mjs" $ARGUMENTS || true) && if [ -z "$PREV" ]; then echo "ERROR: could not detect previous tldraw version — skipping changelog fetch. Pass it explicitly: /tldraw-migrate <from-version> [target]" >&2; else node "$SKILL_DIR/fetch-release-notes.mjs" "$PREV" > "$CHANGELOG" && echo "Saved changelog (from $PREV, sourced from github.com/tldraw/tldraw) to $CHANGELOG ($(wc -l < "$CHANGELOG") lines)"; fi`
|
||||
|
||||
!`SKILL_DIR="${SKILL_DIR:-$(for d in .claude/skills/tldraw-migrate .agents/skills/tldraw-migrate .codex/skills/tldraw-migrate .cursor/skills/tldraw-migrate skills/tldraw-migrate; do [ -d "$d" ] && printf %s "$d" && break; done)}" && DOCS="$SKILL_DIR/references/tldraw-full-docs.txt" && STALE=$(find "$DOCS" -mtime +30 2>/dev/null) && if [ -s "$DOCS" ] && [ -z "$STALE" ]; then echo "Using cached full docs at $DOCS ($(wc -l < "$DOCS") lines) — delete the file or wait 30 days to refresh"; else curl --fail -sS https://tldraw.dev/llms-full.txt -o "$DOCS" && echo "Saved full docs to $DOCS ($(wc -l < "$DOCS") lines)"; fi`
|
||||
|
||||
!`SKILL_DIR="${SKILL_DIR:-$(for d in .claude/skills/tldraw-migrate .agents/skills/tldraw-migrate .codex/skills/tldraw-migrate .cursor/skills/tldraw-migrate skills/tldraw-migrate; do [ -d "$d" ] && printf %s "$d" && break; done)}" && mkdir -p "$SKILL_DIR/references" && TARGET=$(node "$SKILL_DIR/detect-target.mjs" $ARGUMENTS) && case "$TARGET" in canary|next|beta|alpha|rc|*-canary*|*-next*|*-beta*|*-alpha*|*-rc*) NEXT="$SKILL_DIR/references/tldraw-next.mdx" && curl --fail -sS https://raw.githubusercontent.com/tldraw/tldraw/main/apps/docs/content/releases/next.mdx -o "$NEXT" && echo "Pre-release target ($TARGET) — saved next-release notes to $NEXT ($(wc -l < "$NEXT") lines)" ;; *) echo "Stable target ($TARGET) — skipping next-release notes" ;; esac`
|
||||
|
||||
- **[Filtered changelog](references/tldraw-releases.txt)** — release notes for stable versions between the previous version and now, pulled directly from `apps/docs/content/releases/` on `tldraw/tldraw` main (GitHub is the source of truth, not tldraw.dev). Each breaking change (marked `💥`) carries a `<details><summary>Migration guide</summary>` block with a before/after recipe. **These migration blocks are the primary source for version-specific fixes** — this skill intentionally does not duplicate them. When you hit a TS error, grep for the relevant API name in this file and read its migration block.
|
||||
- **[Full docs](references/tldraw-full-docs.txt)** — complete tldraw SDK docs (~1.5MB). Do NOT read this upfront. Use Grep or Read with line ranges to search for specific topics as needed (e.g., custom shapes, TLTextOptions).
|
||||
- **[Next-release notes](references/tldraw-next.mdx)** — *only present when the target is a pre-release.* The in-progress release notes (raw MDX from `main`) for the upcoming version. Same `<details><summary>Migration guide</summary>` structure. The stable changelog won't cover canary/next deltas; this file is where you'll find them.
|
||||
|
||||
**Searching for migration recipes:**
|
||||
|
||||
Always grep *both* the stable changelog and `next.mdx` (the latter only exists for pre-release targets — `2>/dev/null` swallows the missing-file warning). Migration blocks routinely run 40–80 lines, so use `-A60` as the default after-context.
|
||||
|
||||
```sh
|
||||
# List every breaking change with a migration block
|
||||
grep -nE '💥|<summary>Migration guide' ${SKILL_DIR}/references/tldraw-releases.txt ${SKILL_DIR}/references/tldraw-next.mdx 2>/dev/null
|
||||
|
||||
# Find the migration recipe for a specific symbol
|
||||
grep -n -B2 -A60 'getIndicatorPath\|TLUserStore\|EmbedShapeUtil' ${SKILL_DIR}/references/tldraw-releases.txt ${SKILL_DIR}/references/tldraw-next.mdx 2>/dev/null
|
||||
```
|
||||
|
||||
## Step 1: Understand the environment
|
||||
|
||||
Before making any changes, scan the project to understand what you're working with. Run these in parallel:
|
||||
|
||||
- **Package manager**: Check for lock files (`yarn.lock`, `pnpm-lock.yaml`, `bun.lockb`, `package-lock.json`). Use the corresponding tool throughout.
|
||||
- **tldraw packages**: `grep -E "tldraw|@tldraw" package.json` — which packages are installed, and at what versions? Note: not every project uses all tldraw packages.
|
||||
- **Source directory**: figure out where the project's TypeScript sources live (commonly `src/`, `app/`, or `lib/`; Next.js App Router projects don't have `src/`). Use this directory for every grep below — don't assume `src/`.
|
||||
- **Import style**: `grep -r "from '@tldraw" <source-dir> --include="*.ts" --include="*.tsx" -l | head -5` — does the project import from `'tldraw'`, `'@tldraw/editor'`, or both? This affects module augmentation targets.
|
||||
- **TypeScript**: Check `package.json` for a `typecheck` or `tsc` script. If neither exists, fall back to `npx tsc --noEmit` (TypeScript is usually a devDependency). Also check the TypeScript version.
|
||||
- **Build tool**: Check `package.json` scripts for the build command (vite, next, webpack, esbuild, etc.)
|
||||
- **Linter**: Check for oxlint/eslint/biome config files (`.oxlintrc*`, `.eslintrc*`, `eslint.config.*`, `biome.json`). A linter may help catch deprecations later.
|
||||
- **Monorepo**: Is `package.json` at the working directory root, or is this a nested package? Check for workspaces config.
|
||||
|
||||
## Step 2: Upgrade packages
|
||||
|
||||
Using the detected package manager, upgrade all tldraw packages that are already in the project's dependencies to the resolved target (printed in the "Resolved migration" line above). Don't add new packages the project doesn't already use.
|
||||
|
||||
- For a stable target (`latest` or a stable semver): install at that tag/version, e.g. `yarn add tldraw@latest` or `npm install tldraw@4.5.10`.
|
||||
- For a pre-release target (`canary`, `next`, `beta`, or a pre-release semver): install at that tag/version, e.g. `yarn add tldraw@canary`. If multiple `@tldraw/*` packages are listed, pin them all to the same target to avoid version skew.
|
||||
|
||||
**Commit the version bump on its own.** Stage and commit only `package.json` and the lockfile before fixing types. Source-code changes from later steps stay in their own commit, so the eventual `git diff` is reviewable as "what the migration touched" rather than mixed with the dependency change.
|
||||
|
||||
## Step 3: Identify all TypeScript errors
|
||||
|
||||
Run the project's typecheck command (from Step 1) and categorize the errors:
|
||||
|
||||
1. **Count errors by TS code** (e.g., TS2344, TS2786) to understand the distribution.
|
||||
2. **List unique files with errors** to understand the scope.
|
||||
3. **Identify error patterns.** The categories below are version-agnostic — they describe the *shape* of common errors. The *specific* fix for each rename, removal, or signature change lives in the release-notes migration blocks (see "Searching for migration recipes" above), not in this skill.
|
||||
|
||||
- **TS2786** "cannot be used as JSX component" / "bigint not assignable to ReactNode": React types skew. `@types/react` (and usually `@types/react-dom`) don't match the version tldraw bundles. To find the bundled version: with npm or yarn classic, check `node_modules/@tldraw/editor/node_modules/@types/react/package.json`; with pnpm or yarn berry, run the package manager's "why" command (e.g. `pnpm why @types/react`).
|
||||
- **TS2344** "does not satisfy constraint TLShape/TLBaseBoxShape" / **TS2416** `shapeType` mismatch: custom shape or binding type isn't registered for the new global props maps. Fix pattern in 4b.
|
||||
- **TS2305** / **TS2724** "has no exported member" / "is not exported": API removed or renamed. Find the replacement in the matching `Migration guide` block in `tldraw-releases.txt` / `tldraw-next.mdx`.
|
||||
- **TS2339** "property does not exist": API renamed or method signature changed. Same lookup.
|
||||
- **TS2515** "non-abstract class does not implement inherited abstract member": a base class gained a required method. The error names the method — search the migration blocks for that method name. (Note: if your implementation only throws, TypeScript may infer `never`; add an explicit return type to satisfy the base class signature.)
|
||||
- **TS2345** on `createShapes`/`updateShapes`: heterogeneous mapped arrays need `as TLShapePartial[]` / `as TLCreateShapePartial[]` casts. See 4e.
|
||||
- **TS2345** with two structurally-similar `Editor` classes resolving to different paths under `node_modules/@tiptap/core` and `node_modules/@tldraw/editor/node_modules/@tiptap/core`: TipTap v2/v3 dual install. See 4d.
|
||||
|
||||
## Step 4: Fix errors in order of impact
|
||||
|
||||
Fix in this order — each fix eliminates many downstream errors. After each sub-step, re-run typecheck and confirm the error codes targeted by *that* sub-step are resolved or decreasing. Ignore unrelated codes; they belong to later sub-steps.
|
||||
|
||||
| Sub-step | Symptom |
|
||||
| --- | --- |
|
||||
| 4a. React types | TS2786 "bigint not assignable to ReactNode" |
|
||||
| 4b. Custom shapes and bindings | TS2344 constraint errors, TS2416 `shapeType` mismatch |
|
||||
| 4c. API renames, removals, abstract methods | TS2305, TS2724, TS2339, TS2515 |
|
||||
| 4d. TipTap imports | Only if the project depends on `@tiptap/*` — skip otherwise |
|
||||
| 4e. Remaining type errors | `createShapes`/`updateShapes` with `.map()`, leftover casts |
|
||||
|
||||
Read `type-errors.md` for the recipe for whichever codes the project actually has. Do not read it end to end — jump to the matching sub-step.
|
||||
|
||||
## Step 5: Fix deprecations
|
||||
|
||||
After all type errors are resolved, find and fix `@deprecated` API usage. These still compile but should be migrated.
|
||||
|
||||
1. **Find deprecated symbols.** The changelog is the most reliable starting point — tldraw's `.d.ts` files use multi-line JSDoc, so grepping the type defs is fragile (the declaration line is typically 2–5 lines after the `@deprecated` tag, not adjacent to it).
|
||||
|
||||
Start here:
|
||||
|
||||
```sh
|
||||
grep -i 'deprecated' ${SKILL_DIR}/references/tldraw-releases.txt
|
||||
```
|
||||
|
||||
When the target is a pre-release, also: `grep -i 'deprecated' ${SKILL_DIR}/references/tldraw-next.mdx`.
|
||||
|
||||
To cross-check against the type defs (one package at a time — repeat for each `@tldraw/*` package the project imports from, and `tldraw` itself), use `-A` not `-B`:
|
||||
|
||||
```sh
|
||||
grep -A5 '@deprecated' node_modules/@tldraw/editor/dist-cjs/index.d.ts \
|
||||
| grep -oE '\b(class|function|interface|const|type|let|var)\s+\w+' \
|
||||
| awk '{print $NF}' | sort -u
|
||||
```
|
||||
|
||||
This catches multi-line JSDoc but produces some false positives — treat the output as a candidate list, not an authoritative one. The changelog grep is what you should drive from.
|
||||
|
||||
2. **Run the linter if configured** — a deprecation rule (e.g. eslint's `deprecation/deprecation`) will flag deprecated imports automatically. If no linter is configured, skip this step.
|
||||
|
||||
3. **Search the project source** for each deprecated symbol. Replace with the recommended alternative from the `@deprecated` JSDoc comment, the changelog entry, or the docs.
|
||||
|
||||
4. **Sanity-check renames the typecheck didn't catch.** If the changelog or `tldraw-next.mdx` describes a rename and the project still imports the old name, TypeScript may resolve it through a wildcard re-export and miss it. Skim the changelog/next.mdx for "renamed" / "moved to" entries and grep the source for any old names you find.
|
||||
|
||||
## Step 6: Verify
|
||||
|
||||
Run the project's typecheck and build commands (as discovered in Step 1).
|
||||
|
||||
If errors remain, repeat the categorize-and-fix cycle.
|
||||
|
||||
### Post-migration sanity check
|
||||
|
||||
After all errors are resolved, do a quick audit:
|
||||
|
||||
1. **Count typed `as` casts added by this migration.** A naive `grep ' as '` overcounts because it includes `as const` (the recommended narrowing pattern) and `as` in import paths. Drive off the diff and filter:
|
||||
|
||||
```sh
|
||||
git diff -- '<source-dir>/**/*.ts' '<source-dir>/**/*.tsx' \
|
||||
| grep -E '^\+' | grep -v '^+++' \
|
||||
| grep -E '\bas\s+[A-Z]' | grep -v 'as const'
|
||||
```
|
||||
|
||||
**The migration should add no more than a small handful of new typed casts** (ideally zero, with `as TLShapePartial[]` / `as TLCreateShapePartial[]` for heterogeneous `createShapes`/`updateShapes` arrays as the main legitimate exception). If you added more than ~5 across the whole migration, go back and fix them — you are almost certainly using the new API incorrectly.
|
||||
2. **Review every typed cast you added**: For each, verify it's truly necessary by checking whether `as const`, `satisfies`, generic type parameters, or module augmentation could replace it. Remove or replace any that have a cleaner alternative.
|
||||
3. **Audit module augmentations.** Module augmentation is correct for `TLGlobalShapePropsMap` / `TLGlobalBindingPropsMap` registration and for adding genuinely-missing public types. It is **not** correct for re-exposing symbols that the SDK demoted to `@internal` — that's a workaround, not a fix. Find the public replacement instead (the migration block usually names it).
|
||||
4. **Verify no stubs or dead code**: If you stubbed out removed APIs (e.g., replaced a removed function with a no-op), make sure the calling code doesn't depend on the return value. If it does, find the proper replacement in the migration blocks or docs.
|
||||
|
||||
## Step 7: Report
|
||||
|
||||
End the migration with a single summary message to the user. Don't pad it — just the facts:
|
||||
|
||||
- **From → target version**, and the resolved target version if it was a tag (`canary` → `4.7.0-canary.abc123`).
|
||||
- **Files modified** (count, plus the list if it's small).
|
||||
- **TypeScript errors fixed**, broken down by code (e.g., `TS2786 ×12, TS2344 ×3, TS2305 ×1`). Use the before/after counts you collected during Step 4.
|
||||
- **Typed `as` casts added** (count and one-line rationale per cast — should be zero or close to it; flag any you weren't sure about).
|
||||
- **Module augmentations added** (what they cover — `TLGlobalShapePropsMap`, `TLGlobalBindingPropsMap`, `@tiptap/core` chain commands, etc.).
|
||||
- **Deprecations migrated** (count and the symbols).
|
||||
- **Anything flagged as undocumented** — symbols you couldn't find in any migration block, gaps in the changelog, or recipes that didn't match the actual API surface. These are worth filing back to the tldraw team.
|
||||
- **Anything skipped or deferred** — if you punted on a hard rename or left a `TODO`, say so explicitly so the user knows what's still on them.
|
||||
|
||||
## Quality checks
|
||||
|
||||
- **Type safety is paramount.** The goal is a migration that is as type-safe as the original code. Do NOT add `as any`, `as unknown`, or broad type casts. Do NOT add `@ts-ignore` or `@ts-expect-error`. These are never acceptable — if you can't make the types work, you don't understand the new API yet. Stop and read the changelog and type definitions before continuing.
|
||||
- **Prefer TypeScript's narrowing features over casts.** Use `as const` for literal types, `satisfies` for type-checking without widening, generic parameters for call sites, and module augmentation for extending interfaces. These are the right tools for a migration — `as` casts are not.
|
||||
- **Use parallel agents for large batches with the same pattern.** Apply the recipe to one file by hand first to confirm it works, then spawn one Agent per remaining file in a single message (multiple Agent tool calls in parallel). Pass each agent: (1) the path of the file to migrate, (2) the before/after diff from your template file, (3) the relevant migration block from `tldraw-releases.txt`. This is most useful in 4b (mechanical `TLBaseShape` → `TLGlobalShapePropsMap` migration across many shape files) and 4c (one symbol rename rippling across the project).
|
||||
- **Don't just make errors go away — understand the new API.** When a method signature changes (e.g., new parameters added, property renamed to a richer type), read the changelog AND the current type definitions to understand _why_ it changed. A fix that compiles but passes hardcoded/dummy values where the new API expects real data is worse than a type error — it silently degrades behavior. For example, if a function gains new required parameters, check what shape props or editor state should feed those parameters rather than passing `0` or `1`.
|
||||
- **When unsure about an API pattern**, grep the full docs (`${SKILL_DIR}/references/tldraw-full-docs.txt`) for usage examples of that specific API. The docs contain code samples that show the canonical way to use each API.
|
||||
|
||||
## Tips
|
||||
|
||||
- Read `${SKILL_DIR}/references/tldraw-releases.txt` for the filtered changelog — this is the primary source for what changed
|
||||
- Grep `${SKILL_DIR}/references/tldraw-full-docs.txt` when you need docs on a specific API
|
||||
- When searching tldraw types, try multiple paths — the dist directory structure varies by version and package manager
|
||||
34
skills/tldraw-migrate/detect-target.mjs
Normal file
34
skills/tldraw-migrate/detect-target.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Resolves the migration *target* (what version/tag to upgrade TO) from
|
||||
* the skill's $ARGUMENTS, and writes it to stdout.
|
||||
*
|
||||
* Argument forms supported:
|
||||
* /tldraw-migrate → "latest"
|
||||
* /tldraw-migrate 4.4.0 → "latest" (single semver = from-version)
|
||||
* /tldraw-migrate canary → "canary" (single tag/pre-release = target)
|
||||
* /tldraw-migrate 4.4.0 canary → "canary" (two args: <from> <target>)
|
||||
* /tldraw-migrate 4.4.0 4.6.0-canary.x → "4.6.0-canary.x"
|
||||
*
|
||||
* Outputs just the resolved target with no trailing newline so it can be
|
||||
* interpolated directly in shell.
|
||||
*/
|
||||
|
||||
const args = process.argv
|
||||
.slice(2)
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const STABLE_SEMVER = /^v?\d+\.\d+\.\d+$/
|
||||
|
||||
function emit(target) {
|
||||
process.stdout.write(target)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args.length === 0) emit('latest')
|
||||
if (args.length >= 2) emit(args[args.length - 1])
|
||||
|
||||
// Single arg: stable semver = from-version (target defaults), anything else = target.
|
||||
const only = args[0]
|
||||
emit(STABLE_SEMVER.test(only) ? 'latest' : only)
|
||||
107
skills/tldraw-migrate/detect-versions.mjs
Normal file
107
skills/tldraw-migrate/detect-versions.mjs
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Detects the previous tldraw version and writes it to stdout.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. An explicit version passed as argv[2] (e.g., "/tldraw-migrate 4.0.0").
|
||||
* 2. The version of any tldraw package on the main/master branch — but only
|
||||
* if it differs from the working tree (otherwise the user is migrating
|
||||
* directly on main and main already reflects the new version).
|
||||
* 3. The version in HEAD~1.
|
||||
* 4. The version in the current working tree (last resort: assumes the user
|
||||
* hasn't bumped yet, so the working tree IS the previous version).
|
||||
*
|
||||
* Outputs just the version string (no trailing newline) so the SKILL.md can
|
||||
* interpolate it into other commands.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
function exec(cmd) {
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const TLDRAW_PACKAGES = [
|
||||
'tldraw',
|
||||
'@tldraw/editor',
|
||||
'@tldraw/tldraw',
|
||||
'@tldraw/store',
|
||||
'@tldraw/tlschema',
|
||||
'@tldraw/state',
|
||||
'@tldraw/state-react',
|
||||
'@tldraw/sync',
|
||||
'@tldraw/sync-core',
|
||||
'@tldraw/utils',
|
||||
'@tldraw/validate',
|
||||
]
|
||||
|
||||
function extractTldrawVersion(packageJsonStr) {
|
||||
try {
|
||||
const pkg = JSON.parse(packageJsonStr)
|
||||
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
||||
for (const name of TLDRAW_PACKAGES) {
|
||||
const raw = deps[name]
|
||||
if (!raw) continue
|
||||
const cleaned = String(raw).replace(/[\^~>=<\s]/g, '')
|
||||
// Preserve pre-release suffixes (e.g., 4.7.0-canary.abc) so the
|
||||
// release-notes filter can compare them correctly against stable
|
||||
// versions.
|
||||
const match = cleaned.match(/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/)
|
||||
if (match) return match[1]
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function readWorkingTreeVersion() {
|
||||
try {
|
||||
return extractTldrawVersion(readFileSync('package.json', 'utf8'))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function emit(version) {
|
||||
process.stdout.write(version)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const explicit = process.argv[2]?.trim()
|
||||
if (explicit) {
|
||||
const cleaned = explicit.replace(/^v/, '')
|
||||
const match = cleaned.match(/^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/)
|
||||
if (match) emit(match[1])
|
||||
}
|
||||
|
||||
const workingTreeVersion = readWorkingTreeVersion()
|
||||
|
||||
for (const branch of ['main', 'master']) {
|
||||
const content = exec(`git show ${branch}:package.json`)
|
||||
if (!content) continue
|
||||
const ver = extractTldrawVersion(content)
|
||||
if (!ver) continue
|
||||
// If the branch version matches the working tree, the user likely bumped
|
||||
// directly on this branch — main isn't a useful "before" reference.
|
||||
if (workingTreeVersion && ver === workingTreeVersion) continue
|
||||
emit(ver)
|
||||
}
|
||||
|
||||
const parent = exec('git show HEAD~1:package.json')
|
||||
if (parent) {
|
||||
const ver = extractTldrawVersion(parent)
|
||||
if (ver && ver !== workingTreeVersion) emit(ver)
|
||||
}
|
||||
|
||||
if (workingTreeVersion) emit(workingTreeVersion)
|
||||
|
||||
console.error(
|
||||
'Could not detect previous tldraw version. Pass it as an argument: /tldraw-migrate 4.0.0'
|
||||
)
|
||||
process.exit(1)
|
||||
163
skills/tldraw-migrate/fetch-release-notes.mjs
Normal file
163
skills/tldraw-migrate/fetch-release-notes.mjs
Normal file
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Fetches tldraw stable release notes directly from GitHub
|
||||
* (apps/docs/content/releases/vX.Y.Z.mdx on `tldraw/tldraw` main) and
|
||||
* concatenates them into a single text file filtered by version range.
|
||||
* GitHub is the source of truth, so the output reflects whatever is on
|
||||
* main — not whatever tldraw.dev has deployed.
|
||||
*
|
||||
* `next.mdx` is intentionally NOT included here. The skill fetches it
|
||||
* separately, conditionally on a pre-release target, into its own file
|
||||
* (`references/tldraw-next.mdx`).
|
||||
*
|
||||
* Usage:
|
||||
* node fetch-release-notes.mjs <from-version> [to-version] > output.txt
|
||||
*
|
||||
* Honors a `GITHUB_TOKEN` env var to avoid the unauthenticated API rate limit.
|
||||
*/
|
||||
|
||||
const REPO = process.env.TLDRAW_RELEASES_REPO || 'tldraw/tldraw'
|
||||
const RELEASES_PATH = 'apps/docs/content/releases'
|
||||
const BRANCH = process.env.TLDRAW_RELEASES_BRANCH || 'main'
|
||||
|
||||
const [, , fromArg, toArg] = process.argv
|
||||
|
||||
if (!fromArg) {
|
||||
process.stderr.write(
|
||||
'Usage: node fetch-release-notes.mjs <from-version> [to-version]\n' +
|
||||
' e.g.: node fetch-release-notes.mjs 4.0.0 > releases.txt\n'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function parseVersion(str) {
|
||||
const m = String(str)
|
||||
.trim()
|
||||
.replace(/^v/, '')
|
||||
.match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/)
|
||||
if (!m) return null
|
||||
return { main: [+m[1], +m[2], +m[3]], pre: m[4] || null }
|
||||
}
|
||||
|
||||
function comparePrerelease(a, b) {
|
||||
if (!a && !b) return 0
|
||||
if (!a) return 1
|
||||
if (!b) return -1
|
||||
const aParts = a.split('.')
|
||||
const bParts = b.split('.')
|
||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||
const ap = aParts[i]
|
||||
const bp = bParts[i]
|
||||
if (ap === undefined) return -1
|
||||
if (bp === undefined) return 1
|
||||
const aNum = /^\d+$/.test(ap)
|
||||
const bNum = /^\d+$/.test(bp)
|
||||
if (aNum && bNum) {
|
||||
const d = +ap - +bp
|
||||
if (d !== 0) return d
|
||||
} else if (aNum !== bNum) {
|
||||
return aNum ? -1 : 1
|
||||
} else if (ap !== bp) {
|
||||
return ap < bp ? -1 : 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a.main[i] !== b.main[i]) return a.main[i] - b.main[i]
|
||||
}
|
||||
return comparePrerelease(a.pre, b.pre)
|
||||
}
|
||||
|
||||
const fromVer = parseVersion(fromArg)
|
||||
if (!fromVer) {
|
||||
process.stderr.write(`Invalid from-version: ${fromArg}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const toVer = toArg ? parseVersion(toArg) : null
|
||||
if (toArg && !toVer) {
|
||||
process.stderr.write(`Invalid to-version: ${toArg}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'User-Agent': 'tldraw-migrate-skill',
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
}
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`
|
||||
}
|
||||
|
||||
const apiUrl = `https://api.github.com/repos/${REPO}/contents/${RELEASES_PATH}?ref=${BRANCH}`
|
||||
const listing = await fetch(apiUrl, { headers })
|
||||
.then(async (r) => {
|
||||
if (!r.ok) {
|
||||
const body = await r.text().catch(() => '')
|
||||
throw new Error(`HTTP ${r.status} ${r.statusText}: ${body.slice(0, 200)}`)
|
||||
}
|
||||
return r.json()
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(`Failed to list ${REPO}/${RELEASES_PATH}: ${err.message}\n`)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
if (!Array.isArray(listing)) {
|
||||
process.stderr.write(`Unexpected GitHub API response: ${JSON.stringify(listing).slice(0, 200)}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const files = listing
|
||||
.filter((f) => f.type === 'file' && f.name.endsWith('.mdx') && f.name !== 'next.mdx')
|
||||
.map((f) => ({
|
||||
name: f.name,
|
||||
url: f.download_url,
|
||||
version: parseVersion(f.name.replace(/\.mdx$/, '')),
|
||||
}))
|
||||
.filter((f) => f.version !== null)
|
||||
|
||||
const relevant = files
|
||||
.filter((f) => {
|
||||
const afterFrom = compareVersions(f.version, fromVer) > 0
|
||||
const beforeTo = toVer ? compareVersions(f.version, toVer) <= 0 : true
|
||||
return afterFrom && beforeTo
|
||||
})
|
||||
.sort((a, b) => compareVersions(a.version, b.version))
|
||||
|
||||
if (relevant.length === 0) {
|
||||
process.stderr.write(`No release notes found between ${fromArg} and ${toArg || 'latest'}\n`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const sections = ['# tldraw SDK releases', '', '--------', '']
|
||||
const failed = []
|
||||
|
||||
for (const file of relevant) {
|
||||
const text = await fetch(file.url, { headers: { 'User-Agent': 'tldraw-migrate-skill' } })
|
||||
.then(async (r) => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status} ${r.statusText}`)
|
||||
return r.text()
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(`Warning: failed to fetch ${file.name}: ${err.message}\n`)
|
||||
failed.push(file.name)
|
||||
return null
|
||||
})
|
||||
if (text === null) continue
|
||||
|
||||
const body = text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n+/, '').trim()
|
||||
sections.push(`# ${file.name.replace(/\.mdx$/, '')}`, '', body, '', '--------', '')
|
||||
}
|
||||
|
||||
process.stdout.write(sections.join('\n'))
|
||||
|
||||
if (failed.length > 0) {
|
||||
process.stderr.write(
|
||||
`\nERROR: ${failed.length} of ${relevant.length} release files failed to fetch (${failed.join(', ')}). ` +
|
||||
`Output is incomplete — re-run, or set GITHUB_TOKEN to avoid the unauthenticated rate limit.\n`
|
||||
)
|
||||
process.exit(2)
|
||||
}
|
||||
75
skills/tldraw-migrate/type-errors.md
Normal file
75
skills/tldraw-migrate/type-errors.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Type error fix recipes
|
||||
|
||||
Fix recipes for the TypeScript errors a tldraw upgrade produces, in the order that eliminates the most downstream errors first. Referenced from `SKILL.md` step 4.
|
||||
|
||||
Fix in this order (each fix eliminates many downstream errors). After each sub-step, re-run the project's typecheck and confirm that the error codes targeted by *that* sub-step are resolved (or at least decreasing). Ignore unrelated error codes — they belong to later sub-steps. This catches regressions early without blocking on errors you haven't gotten to yet.
|
||||
|
||||
### 4a. Fix React types
|
||||
|
||||
If you see TS2786 "bigint not assignable to ReactNode" errors, upgrade `@types/react` AND `@types/react-dom` together to match tldraw's bundled version. Bumping only `@types/react` will leave a transitive dependency on the old `@types/react-dom` and the same errors will reappear from a different path (e.g. inside `TldrawUiToolbarButton`).
|
||||
|
||||
**Verify**: re-run typecheck — TS2786 errors caused by `bigint not assignable to ReactNode` should be gone. (TS2786 can also fire from genuine JSX usage problems unrelated to types skew; those remain for later sub-steps.)
|
||||
|
||||
### 4b. Register custom shapes and bindings
|
||||
|
||||
If you see TS2344 ("does not satisfy constraint `TLShape`/`TLBaseBoxShape`") or TS2416 (`shapeType` mismatch) errors, the project is using the pre-v4.3 `TLBaseShape<'name', Props>` pattern and needs to migrate to `TLGlobalShapePropsMap` module augmentation.
|
||||
|
||||
The full recipe — module augmentation for shapes and bindings, the rename ripple when shape names collide, `as const` on `static override type` / `static override shapeType`, and the heterogeneous `createShapes`/`updateShapes` cast guidance — lives in the v4.3 release notes migration block. Find it with:
|
||||
|
||||
```sh
|
||||
grep -n -B2 -A80 'TLGlobalShapePropsMap' ${SKILL_DIR}/references/tldraw-releases.txt
|
||||
```
|
||||
|
||||
Apply that recipe across the project. Use the import style detected in Step 1 as the module-augmentation target (`declare module 'tldraw'` vs. `declare module '@tldraw/editor'`).
|
||||
|
||||
**Verify**: re-run typecheck — TS2344 and TS2416 errors should be gone.
|
||||
|
||||
### 4c. Fix API renames, removals, and abstract-method additions
|
||||
|
||||
This is where the version-specific work happens, and it's driven entirely by the release-notes migration blocks. The skill does *not* enumerate which APIs changed — that's the changelog's job, and it would go stale on every release.
|
||||
|
||||
For each TS2305 / TS2724 / TS2339 / TS2515 error:
|
||||
|
||||
1. Pull the symbol name out of the error.
|
||||
2. Grep the migration blocks for it: `grep -n -B2 -A60 'SymbolName' ${SKILL_DIR}/references/tldraw-releases.txt ${SKILL_DIR}/references/tldraw-next.mdx 2>/dev/null` (the `2>/dev/null` swallows the missing-file warning when `next.mdx` isn't present for stable targets).
|
||||
3. Apply the recipe shown in the matching `Migration guide` block. Migration blocks contain before/after code snippets; copy the structure.
|
||||
|
||||
If the symbol isn't in any migration block:
|
||||
|
||||
- **Demoted to `@internal`** (still exported at runtime, but missing from `.d.ts`): check whether a `<details><summary>Migration guide</summary>` mentions it as part of a larger API. The right fix is almost always to switch to the public replacement, *not* to use module augmentation to re-expose the symbol. If you reach for `declare module 'tldraw' { export function X(): ... }`, stop — find the public replacement instead.
|
||||
- **Truly unmentioned**: check the type defs in `node_modules/tldraw/dist-cjs/index.d.ts`, `node_modules/@tldraw/editor/dist-cjs/index.d.ts`, or `node_modules/@tldraw/tlschema/dist-cjs/index.d.ts` (the layout varies by version and package manager). If the symbol is genuinely gone with no listed replacement, treat the gap as a documentation bug worth flagging in your final report.
|
||||
|
||||
For TS2515 (newly-required abstract method): if your implementation only throws, declare the return type explicitly so TypeScript doesn't infer `never` and the abstract-mismatch error doesn't linger.
|
||||
|
||||
**Verify**: re-run typecheck — count TS2305, TS2724, TS2339, and TS2515 errors before and after. Each fix should knock out one error. If counts haven't dropped, you missed a migration block — re-grep before continuing.
|
||||
|
||||
### 4d. Fix TipTap imports if needed
|
||||
|
||||
**Skip this entire sub-step if the project has no `@tiptap/*` dependencies or imports.** Confirm with `grep -E '@tiptap/' package.json` and a recursive grep for `from '@tiptap` in the source directory. If both come back empty, jump to 4e.
|
||||
|
||||
If the project uses TipTap, your migration may need to cross the v2 → v3 cutover (introduced in tldraw v4.2). The full v2 → v3 recipe — dual-install diagnostic, default-to-named export changes, `TextStyle`/`TextStyleKit`/`FontFamily` reorganization, transaction-handler types — lives in the v4.2 release notes migration block. Find it with:
|
||||
|
||||
```sh
|
||||
grep -n -B2 -A60 'TipTap v3' ${SKILL_DIR}/references/tldraw-releases.txt
|
||||
```
|
||||
|
||||
Apply that recipe. The tldraw skill only adds two version-agnostic notes on top:
|
||||
|
||||
> **Install ordering trap (any TipTap upgrade).** Running `npm install @tiptap/core@3 @tiptap/starter-kit@3 ...` against a project that already has v2 in `node_modules` will fail with `ERESOLVE`, because the v2 `starter-kit` declares `peer @tiptap/core@^2.7`. Either uninstall the v2 packages first (`npm uninstall @tiptap/core @tiptap/starter-kit ...`) or pass `--legacy-peer-deps`.
|
||||
|
||||
> **Custom chained commands.** Whatever TipTap version, custom chain commands register via `declare module '@tiptap/core'` augmentation. This is a TipTap idiom, not a tldraw one — see TipTap's docs.
|
||||
|
||||
**Verify**: re-run typecheck — TipTap import and type errors should be gone.
|
||||
|
||||
### 4e. Fix remaining type errors
|
||||
|
||||
- `createShapes`/`updateShapes` with `.map()`: see the v4.3 migration block for the full recipe (`as const` on the `type` field for homogeneous arrays; `as TLShapePartial[]` / `as TLCreateShapePartial[]` for heterogeneous ones; *not* `satisfies TLShapePartial`).
|
||||
- TipTap extension commands not on `ChainedCommands`: use `declare module '@tiptap/core'` augmentation to register custom commands.
|
||||
- **General rule**: every `as` cast you add is tech debt. Before adding one, exhaust these alternatives in order:
|
||||
1. `as const` on object literals to narrow string literal types
|
||||
2. `satisfies` annotations to check types without widening
|
||||
3. Proper generic type parameters on the call site
|
||||
4. Module augmentation to teach TypeScript about your types
|
||||
5. Only then, a targeted `as` cast with a comment explaining why it's needed
|
||||
|
||||
**Verify**: re-run typecheck — remaining errors should all be resolved. If any remain, re-categorize and route them back to 4a–4d as appropriate.
|
||||
238
skills/update-release-notes/SKILL.md
Normal file
238
skills/update-release-notes/SKILL.md
Normal file
@@ -0,0 +1,238 @@
|
||||
---
|
||||
name: update-release-notes
|
||||
description: Update the release notes file at `apps/docs/content/releases/next.mdx` in the tldraw/tldraw repo based on PRs since the previous release, or archive `next.mdx` to a versioned file when a new version is published.
|
||||
allowed-tools: Bash(git:*), Bash(gh:*)
|
||||
---
|
||||
|
||||
# Update release notes
|
||||
|
||||
This skill handles the operational tasks of maintaining release notes in the **tldraw/tldraw** repo: adding new PR entries to `next.mdx` and archiving releases when a new version is published.
|
||||
|
||||
This skill runs from the tldraw-internal repo but operates on a local clone of tldraw/tldraw.
|
||||
|
||||
## Setup
|
||||
|
||||
Before starting, clone the tldraw repo:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/tldraw/tldraw.git /tmp/tldraw
|
||||
```
|
||||
|
||||
All scripts take the repo path as their first argument. Run script commands from the repository root by replacing `<skill-dir>` with the directory that contains this `SKILL.md`. All file edits happen in this clone. At the end, commit and push from the clone.
|
||||
|
||||
## Release cycle
|
||||
|
||||
The SDK releases every four weeks. One week before launch, the SDK is frozen and a `production` branch is cut from `main`. During release week, only hotfixes are cherry-picked to `production`.
|
||||
|
||||
The release notes in `next.mdx` must ultimately reflect what ships on the `production` branch. However, during the ~3 development weeks before the freeze, we build up release notes from `main` so they're not empty. The status script detects which phase we're in based on the date of the last minor release:
|
||||
|
||||
- **Development weeks** (`source: "main"`, <3 weeks since last minor release): PRs are gathered from `main` since the last release tag. These are preliminary — some may not ship if they're reverted or if production cherry-picks differ.
|
||||
- **Freeze week** (`source: "production"`, 3+ weeks since last minor release): PRs are gathered from the `production` branch using `git cherry` against the previous release branch. Only PRs that landed before the freeze or were hotfixed onto production are included. Entries accumulated from `main` that aren't on production will be pruned.
|
||||
|
||||
`next.mdx` itself also has to be **on `production`** before the release is published, not just describe it. `prepack.ts` generates the `RELEASE_NOTES.md` that ships inside the `tldraw` npm package from the checkout being published, using `next.mdx` as the entry for the version being released. `production` only moves by hotfix during the freeze, so a release-notes PR merged to `main` reaches it only if it also carries the `dotcom-hotfix-please` label (see step 10). The check before publishing is an empty diff:
|
||||
|
||||
```bash
|
||||
git diff --stat origin/production origin/main -- apps/docs/content/releases/next.mdx
|
||||
```
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Check status
|
||||
|
||||
Run the status script to determine versions and diff branch:
|
||||
|
||||
```bash
|
||||
<skill-dir>/scripts/get-changelog-status.sh /tmp/tldraw
|
||||
```
|
||||
|
||||
This returns:
|
||||
|
||||
- `source` - either `"main"` (development weeks) or `"production"` (release week)
|
||||
- `diff_branch` (e.g., `v4.3.x`) - the branch to diff against (used when `source` is `"production"`)
|
||||
- `last_tag` (e.g., `v4.4.0`) - the last release tag (used when `source` is `"main"`)
|
||||
- `latest_release` - the most recent published release
|
||||
- Whether archival is needed
|
||||
|
||||
### 2. Handle archival (if needed)
|
||||
|
||||
If a new release was published since `last_version` in `next.mdx`:
|
||||
|
||||
1. Copy `next.mdx` content to `vX.Y.0.mdx` (e.g., `v4.3.0.mdx`)
|
||||
2. Update the frontmatter in the new file:
|
||||
- Set `title` to the version
|
||||
- Update dates
|
||||
- Add the GitHub release link after frontmatter
|
||||
3. Add the new version to the releases index at `apps/docs/content/getting-started/releases.mdx`:
|
||||
- Add a line at the top of the appropriate `## v{major}.x` section
|
||||
- Format: `- [vX.Y](/releases/vX.Y.0) - Brief description`
|
||||
- The description should be a short summary of the release highlights from the intro paragraph (3-5 key features, comma-separated)
|
||||
- If a new major version section is needed, create it above the previous one
|
||||
4. Reset `next.mdx`:
|
||||
- Update `last_version` field to the new release
|
||||
- Clear the content sections
|
||||
- Keep the file structure for accumulating new changes
|
||||
|
||||
### 3. Find new PRs
|
||||
|
||||
The approach depends on the `source` field from step 1.
|
||||
|
||||
**If `source` is `"production"` (release week):**
|
||||
|
||||
Get PR numbers on the production branch since the previous release:
|
||||
|
||||
```bash
|
||||
<skill-dir>/scripts/get-new-prs.sh /tmp/tldraw <diff_branch>
|
||||
```
|
||||
|
||||
This uses `git cherry` (same approach as `extract-draft-changelog.tsx`) to compare patches between `origin/production` and `origin/<diff_branch>`. Unlike `git log`, `git cherry` correctly handles cherry-picked hotfix commits by comparing patch content rather than commit hashes.
|
||||
|
||||
**If `source` is `"main"` (development weeks):**
|
||||
|
||||
Get PR numbers on main since the last release tag:
|
||||
|
||||
```bash
|
||||
<skill-dir>/scripts/get-new-prs-from-main.sh /tmp/tldraw <last_tag>
|
||||
```
|
||||
|
||||
This uses `git cherry` to compare patches between `origin/main` and the release tag, excluding commits that were cherry-picked to production and already shipped in the release.
|
||||
|
||||
**Both scripts output:**
|
||||
|
||||
- **stdout**: PR numbers (one per line) extracted from commit messages
|
||||
- **stderr**: Commits without PR numbers (typically hotfixes or direct commits)
|
||||
|
||||
### 4. Resolve hotfix commits
|
||||
|
||||
Check stderr from step 3 for any `[HOTFIX]` commits without PR numbers. For each SDK-relevant one (skip dotcom-only), find the corresponding PR:
|
||||
|
||||
```bash
|
||||
gh pr list -R tldraw/tldraw --state merged --search "<hotfix title keywords>" --json number,title --jq '.[]'
|
||||
```
|
||||
|
||||
Also check `sdk-hotfix-please` labeled PRs, but verify they actually landed on production — this label is also used for patch releases on the previous release branch (e.g., v4.3.x):
|
||||
|
||||
```bash
|
||||
gh pr list -R tldraw/tldraw --label "sdk-hotfix-please" --state merged --limit 20 --json number,title,mergedAt
|
||||
```
|
||||
|
||||
Add any matched PR numbers to the production PR list from step 3.
|
||||
|
||||
### 5. Fetch PR details
|
||||
|
||||
Batch fetch all PR information:
|
||||
|
||||
```bash
|
||||
<skill-dir>/scripts/fetch-pr-batch.sh <pr1> <pr2> ...
|
||||
```
|
||||
|
||||
### 6. Remove stale entries
|
||||
|
||||
Compare the full PR list (from steps 3 + 4) against the PR numbers already referenced in `next.mdx`. Remove any entries from `next.mdx` whose PRs are **not** in the current set.
|
||||
|
||||
- **When `source` is `"production"`**: This prunes entries that were accumulated from `main` during development but didn't make it to production. These are PRs that landed on `main` after the production branch was cut and will not ship in this release.
|
||||
- **When `source` is `"main"`**: This removes entries for PRs that were reverted or otherwise no longer on main. Pruning is less aggressive since the full set isn't finalized until production is cut.
|
||||
|
||||
When removing an entry, also check whether it was referenced in a featured "What's new" section and remove that section too. Update the intro paragraph if it mentions a removed feature.
|
||||
|
||||
### 7. Add new entries
|
||||
|
||||
For each PR not already in `next.mdx`:
|
||||
|
||||
1. Check PR labels and body against the style guide's categorization rules
|
||||
2. Skip PRs that should be omitted (see style guide)
|
||||
3. Skip PRs that fix bugs introduced in the same release cycle (i.e., bugs caused by other PRs already in `next.mdx` that were not in the previous release)
|
||||
4. If a PR is a revert, also remove the original reverted PR's entry from `next.mdx` if present
|
||||
5. Add entries to the appropriate section
|
||||
6. Promote significant changes to featured sections when warranted
|
||||
|
||||
### 8. Verify
|
||||
|
||||
Check that:
|
||||
|
||||
- Every PR referenced in `next.mdx` is in the current PR set (from `main` or `production` depending on `source`)
|
||||
- PR links are correct
|
||||
- Community contributors are credited
|
||||
- Sections are in the correct order
|
||||
- Breaking changes are marked with 💥
|
||||
- Deprecations are marked with 🔜
|
||||
|
||||
**Note**: When `source` is `"main"`, the entries are preliminary. Some may be pruned later when the production branch is cut and the source switches to `"production"`.
|
||||
|
||||
### 9. Update the draft GitHub release
|
||||
|
||||
Keep the draft GitHub release for the next minor in sync with `next.mdx`. `publish-new.ts` finds the draft by name (`vX.Y.0`) and publishes its body verbatim at release time, so mirroring `next.mdx` here means the GitHub release and the docs release page always match.
|
||||
|
||||
```bash
|
||||
<skill-dir>/scripts/update-draft-release.sh /tmp/tldraw
|
||||
```
|
||||
|
||||
This script:
|
||||
|
||||
- Derives the target version from `last_version` in `next.mdx` (bump minor, patch `0` — e.g. `v5.2.0` → `v5.3.0`).
|
||||
- Uses the `next.mdx` body (frontmatter stripped) as the release body.
|
||||
- Creates the draft if none exists, or edits the existing draft named `vX.Y.0`. It refuses to touch an already-published release, and refuses to write an empty body.
|
||||
|
||||
**Major releases / version overrides:** the minor bump above is the default because nearly every release is a minor. `publish-new.ts` looks up the draft by the version it resolves — for `--bump major` (or an override) that comes from NPM, not a minor bump, so a hardcoded minor would leave the draft misnamed and `getDraftRelease` would fail. When the next release is a major or otherwise not the next minor, pass the target explicitly so the draft name matches:
|
||||
|
||||
```bash
|
||||
<skill-dir>/scripts/update-draft-release.sh /tmp/tldraw v6.0.0
|
||||
```
|
||||
|
||||
Run this after every `next.mdx` update, including the release-week runs. On the second (post-publish) run the previous minor's draft has already been published, so this creates a fresh draft for the new next minor derived from the reset `last_version`.
|
||||
|
||||
### 10. Push changes
|
||||
|
||||
After editing `next.mdx` (and any archive files), commit and push from the tldraw clone. Do not add `[skip ci]` to the branch commit: required pull request checks will never start when the marker is present. The PR title below adds the marker to the eventual squash-merge commit instead.
|
||||
|
||||
```bash
|
||||
cd /tmp/tldraw
|
||||
BRANCH="update-release-notes-$(date -u +%Y%m%d-%H%M)"
|
||||
git checkout -b "$BRANCH"
|
||||
git add apps/docs/content/releases/
|
||||
git commit -m "docs(releases): update release notes"
|
||||
git push -u origin "$BRANCH"
|
||||
```
|
||||
|
||||
Then create the PR using the `../pr/SKILL.md` workflow and the standards in `../write-pr/SKILL.md`:
|
||||
|
||||
- **Label**: add `docs-hotfix-please` (`gh pr edit <number> --add-label docs-hotfix-please`, or `--label docs-hotfix-please` on `gh pr create`). Without it, merging to `main` never cherry-picks the notes onto the current release branch (`vX.Y.x`), so the update never reaches tldraw.dev — which is the whole point of the run.
|
||||
- **Freeze-week label**: when `source` is `"production"`, also add `dotcom-hotfix-please`. That cherry-picks the merged PR onto `hotfixes` and from there onto `production`, which is where `prepack.ts` reads `next.mdx` from when the release is published (see "Release cycle"). `docs-hotfix-please` alone never touches `production`. After the merge, confirm the hotfix landed with the diff command from "Release cycle"; if the dotcom hotfix bot fails, cherry-pick the squash commit onto a branch from `hotfixes` by hand, strip any `[skip ci]` marker from the commit message, and open a `[HOTFIX]` PR against `hotfixes`.
|
||||
- **Title**: `docs(releases): update release notes [skip ci]` — the `[skip ci]` must be in the PR title because GitHub's squash-merge uses the PR title as the merge commit subject, and that merge commit is what needs to skip CI. **Squash-merge** the PR (do not rebase- or create-a-merge-commit) so exactly one `[skip ci]` commit lands on the base branch.
|
||||
- **Change type**: `other`
|
||||
- **Test plan**: Remove the numbered list (no manual testing steps). Untick both unit tests and end to end tests.
|
||||
- **Release notes**: Omit this section (internal docs work with no user-facing impact)
|
||||
- **Description paragraph**: Start with "In order to..." and mention the source (`main` or `production`) and what was updated (new entries added, stale entries pruned, archival performed, draft GitHub release synced, etc.)
|
||||
- Include a **Code changes** table with a `Documentation` row
|
||||
|
||||
**Why `[skip ci]` is only in the PR title**: branch commits need to run the required pull request checks. GitHub's squash merge uses the PR title for the merge commit, so the marker skips push workflows when the notes-only change lands on `main`.
|
||||
|
||||
`[skip ci]` does not interfere with the `docs-hotfix-please` label: `trigger-sdk-hotfix.yml` runs on `pull_request_target`, which GitHub's skip-ci markers do not affect, and the hotfix script strips skip-ci markers from the cherry-picked commit so the release-branch push still triggers the publish workflow. The trigger fires when a labeled PR is merged; to re-trigger it later, remove and re-add the label on the merged PR. The same applies to `dotcom-hotfix-please` once #10629 is fixed; until then its bot keeps the marker and the hotfix PR it opens never gets its checks.
|
||||
|
||||
Never write the literal `[skip ci]` string anywhere else in a PR body. GitHub reads the marker from the whole squash-merge commit message, and the PR body becomes that commit's body.
|
||||
|
||||
## The `last_version` field
|
||||
|
||||
The `next.mdx` frontmatter includes a `last_version` field that tracks the most recent published release. This determines which PRs are "new" and need to be added.
|
||||
|
||||
## SDK release workflow
|
||||
|
||||
During an SDK release, this skill is run **twice** to get the docs site into its post-release state:
|
||||
|
||||
1. **First run** — update `next.mdx` with all PRs that will ship in the release (source is `"production"` during freeze week). This ensures the release notes are complete before publishing. The draft-release step (step 9) syncs the draft `vX.Y.0` GitHub release from this `next.mdx`, so `publish-new.ts` has the finished notes ready to publish. The PR carries both `docs-hotfix-please` and `dotcom-hotfix-please` (step 10) so the file reaches `v(X.Y-1).x` for the site and `production` for the package.
|
||||
2. **Gate** — before anyone publishes, `next.mdx` must be identical on `production` and `main` (the diff command in "Release cycle" prints nothing), and the draft `vX.Y.0` release must match `next.mdx`. If a later edit to `next.mdx` landed on `main` without the dotcom hotfix, or after the last draft sync, fix that first.
|
||||
3. **Publish the release to NPM** — this happens outside of this skill. `publish-new.ts` publishes the draft release created in step 1, and `prepack.ts` bakes `production`'s `next.mdx` into the `tldraw` package as that version's `RELEASE_NOTES.md` entry. The publish pushes the version bump to `main`, not `production`.
|
||||
4. **Second run** — now that the release is published, the status script will detect `needs_archive: true`. This workflow runs on `production` pushes, and the publish doesn't make one, so the second run happens on the next `production` push (normally the post-launch dotcom release from `main`), or when someone dispatches `update-release-notes.yml` by hand. This run:
|
||||
- Archives `next.mdx` to a versioned file (e.g., `v4.5.0.mdx`)
|
||||
- Adds the new version to the releases index (`releases.mdx`)
|
||||
- Resets `next.mdx` with the new `last_version`
|
||||
- Finds PRs that landed on `main` during the freeze (since the release tag) and adds them to the fresh `next.mdx`
|
||||
- Syncs a fresh draft for the new next minor (step 9), since the previous draft is now published
|
||||
- Needs `docs-hotfix-please` (not `dotcom-hotfix-please`: `production` no longer matters for the notes once the package is out) so it lands on the new `vX.Y.x` branch and republishes the site
|
||||
|
||||
After the second run, the docs site reflects the published release and `next.mdx` already has a head start on the next cycle's changes.
|
||||
|
||||
## References
|
||||
|
||||
- **Style guide**: See `../shared/release-notes-guide.md` for guidance on what a release notes article should contain and how to format it.
|
||||
- **Writing guide**: See `../../VOICE.md` for voice and style conventions.
|
||||
- **Scripts**: See `scripts/` for automation helpers, including `update-draft-release.sh` for syncing the draft GitHub release
|
||||
58
skills/update-release-notes/scripts/fetch-pr-batch.sh
Executable file
58
skills/update-release-notes/scripts/fetch-pr-batch.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# Batch fetch PR details for changelog categorization
|
||||
#
|
||||
# Usage: ./fetch-pr-batch.sh <pr1> <pr2> ...
|
||||
# or: echo "1234 5678" | ./fetch-pr-batch.sh
|
||||
# Output: JSON array of PR details
|
||||
|
||||
set -e
|
||||
|
||||
# Collect PR numbers from arguments or stdin
|
||||
prs=()
|
||||
if [ $# -gt 0 ]; then
|
||||
prs=("$@")
|
||||
else
|
||||
while read -r line; do
|
||||
for pr in $line; do
|
||||
prs+=("$pr")
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
if [ ${#prs[@]} -eq 0 ]; then
|
||||
echo "Usage: $0 <pr1> <pr2> ..." >&2
|
||||
echo " or: echo '1234 5678' | $0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build GraphQL query for batch fetching
|
||||
# This is much faster than individual gh pr view calls
|
||||
query='query($owner: String!, $repo: String!) {'
|
||||
for i in "${!prs[@]}"; do
|
||||
pr="${prs[$i]}"
|
||||
query+="
|
||||
pr$i: repository(owner: \$owner, name: \$repo) {
|
||||
pullRequest(number: $pr) {
|
||||
number
|
||||
title
|
||||
author { login }
|
||||
mergedAt
|
||||
labels(first: 10) { nodes { name } }
|
||||
body
|
||||
}
|
||||
}"
|
||||
done
|
||||
query+='}'
|
||||
|
||||
# Execute query
|
||||
result=$(gh api graphql -f query="$query" -f owner="tldraw" -f repo="tldraw" 2>/dev/null)
|
||||
|
||||
# Transform into cleaner JSON array
|
||||
echo "$result" | jq '[.data | to_entries[] | .value.pullRequest | select(. != null) | {
|
||||
number: .number,
|
||||
title: .title,
|
||||
author: .author.login,
|
||||
merged_at: .mergedAt,
|
||||
labels: [.labels.nodes[].name],
|
||||
body: .body
|
||||
}] | sort_by(.number) | reverse'
|
||||
116
skills/update-release-notes/scripts/get-changelog-status.sh
Executable file
116
skills/update-release-notes/scripts/get-changelog-status.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
# Get changelog status: versions, whether archival is needed, and the diff branch
|
||||
#
|
||||
# Usage: ./get-changelog-status.sh <tldraw-repo-dir>
|
||||
# Output: JSON with version info and status
|
||||
#
|
||||
# Requires a local clone of tldraw/tldraw.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <tldraw-repo-dir>" >&2
|
||||
echo "Example: $0 /tmp/tldraw" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TLDRAW_REPO="$1"
|
||||
RELEASES_DIR="$TLDRAW_REPO/apps/docs/content/releases"
|
||||
NEXT_FILE="$RELEASES_DIR/next.mdx"
|
||||
|
||||
# Extract last_version from next.mdx frontmatter (if present)
|
||||
last_version=""
|
||||
if [ -f "$NEXT_FILE" ]; then
|
||||
last_version=$(grep -E "^last_version:" "$NEXT_FILE" 2>/dev/null | sed "s/last_version: *//" | tr -d "'\"\n" || echo "")
|
||||
fi
|
||||
|
||||
# Get the latest published release from GitHub
|
||||
latest_release=$(gh release list -R tldraw/tldraw --exclude-drafts --limit 1 --json tagName,publishedAt -q '.[0]' 2>/dev/null || echo '{}')
|
||||
latest_tag=$(echo "$latest_release" | jq -r '.tagName // empty')
|
||||
latest_date=$(echo "$latest_release" | jq -r '.publishedAt // empty')
|
||||
|
||||
# Determine if we need to archive (latest release is newer than last_version)
|
||||
needs_archive="false"
|
||||
if [ -n "$latest_tag" ] && [ -n "$last_version" ]; then
|
||||
if [ "$latest_tag" != "$last_version" ]; then
|
||||
needs_archive="true"
|
||||
fi
|
||||
elif [ -n "$latest_tag" ] && [ -z "$last_version" ]; then
|
||||
# No last_version set, assume we might need to check
|
||||
needs_archive="unknown"
|
||||
fi
|
||||
|
||||
# Determine the release branch to diff against
|
||||
# e.g., v4.3.0 -> v4.3.x, or if latest is v4.3.1 -> v4.3.x
|
||||
diff_branch=""
|
||||
if [ -n "$latest_tag" ]; then
|
||||
# Extract major.minor from tag (v4.3.0 -> 4.3)
|
||||
version_nums=$(echo "$latest_tag" | sed 's/^v//' | cut -d. -f1,2)
|
||||
diff_branch="v${version_nums}.x"
|
||||
fi
|
||||
|
||||
# Check if the archive file already exists
|
||||
archive_exists="false"
|
||||
if [ -n "$latest_tag" ]; then
|
||||
archive_file="$RELEASES_DIR/${latest_tag}.mdx"
|
||||
if [ -f "$archive_file" ]; then
|
||||
archive_exists="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if next.mdx has content (more than just frontmatter)
|
||||
next_has_content="false"
|
||||
if [ -f "$NEXT_FILE" ]; then
|
||||
# Count lines after frontmatter (after second ---)
|
||||
content_lines=$(awk '/^---$/{c++;next}c==2' "$NEXT_FILE" | grep -v '^[[:space:]]*$' | wc -l | tr -d ' ')
|
||||
if [ "$content_lines" -gt 0 ]; then
|
||||
next_has_content="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine source based on weeks since the last minor/major release.
|
||||
# The SDK releases every 4 weeks. During the first 3 weeks (development),
|
||||
# we gather PRs from main. At 3+ weeks (freeze week), we switch to production.
|
||||
#
|
||||
# Find the last minor or major release (skip patches like v4.3.1, v4.3.2)
|
||||
source="main"
|
||||
last_minor_date=""
|
||||
if [ -n "$latest_tag" ]; then
|
||||
# Get the .0 release for the current minor version (e.g., v4.4.0)
|
||||
minor_tag=$(echo "$latest_tag" | sed 's/^v//' | cut -d. -f1,2)
|
||||
minor_release_tag="v${minor_tag}.0"
|
||||
# Get the publish date of that .0 release
|
||||
last_minor_date=$(gh release view "$minor_release_tag" -R tldraw/tldraw --json publishedAt -q '.publishedAt' 2>/dev/null || echo "")
|
||||
if [ -n "$last_minor_date" ]; then
|
||||
# Calculate weeks since the minor release
|
||||
release_epoch=$(date -jf "%Y-%m-%dT%H:%M:%SZ" "$last_minor_date" "+%s" 2>/dev/null || date -d "$last_minor_date" "+%s" 2>/dev/null || echo "")
|
||||
now_epoch=$(date "+%s")
|
||||
if [ -n "$release_epoch" ]; then
|
||||
days_since=$(( (now_epoch - release_epoch) / 86400 ))
|
||||
weeks_since=$(( days_since / 7 ))
|
||||
if [ "$weeks_since" -ge 3 ]; then
|
||||
source="production"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# The last tag is the latest release tag (used by get-new-prs-from-main.sh)
|
||||
last_tag="${latest_tag}"
|
||||
|
||||
# Output JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"last_version": "${last_version}",
|
||||
"latest_release": {
|
||||
"tag": "${latest_tag}",
|
||||
"published_at": "${latest_date}"
|
||||
},
|
||||
"diff_branch": "${diff_branch}",
|
||||
"last_tag": "${last_tag}",
|
||||
"source": "${source}",
|
||||
"needs_archive": ${needs_archive},
|
||||
"archive_exists": ${archive_exists},
|
||||
"next_has_content": ${next_has_content}
|
||||
}
|
||||
EOF
|
||||
88
skills/update-release-notes/scripts/get-new-prs-from-main.sh
Executable file
88
skills/update-release-notes/scripts/get-new-prs-from-main.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Find PR numbers on the main branch since a release tag.
|
||||
# Uses `git cherry` to compare patches between origin/main and the
|
||||
# release tag, excluding commits that were cherry-picked to production
|
||||
# and already shipped in the release.
|
||||
#
|
||||
# Usage: ./get-new-prs-from-main.sh <tldraw-repo-dir> <release-tag>
|
||||
# Example: ./get-new-prs-from-main.sh /tmp/tldraw v4.4.0
|
||||
#
|
||||
# Output:
|
||||
# PR numbers (one per line) on stdout, then any commits
|
||||
# without PR numbers (hash + title) on stderr.
|
||||
#
|
||||
# Requires a local clone of tldraw/tldraw.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||
echo "Usage: $0 <tldraw-repo-dir> <release-tag>" >&2
|
||||
echo "Example: $0 /tmp/tldraw v4.4.0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TLDRAW_REPO="$1"
|
||||
RELEASE_TAG="$2"
|
||||
|
||||
cd "$TLDRAW_REPO"
|
||||
|
||||
# Fetch to ensure we have latest refs
|
||||
git fetch origin main --quiet 2>/dev/null || true
|
||||
|
||||
# Verify the tag exists
|
||||
if ! git rev-parse "$RELEASE_TAG" >/dev/null 2>&1; then
|
||||
echo "Error: tag '$RELEASE_TAG' not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The release tag lives on the production branch, which has a separate
|
||||
# history from main (deploy commits, hotfixes, version bumps). Many
|
||||
# commits on main after the branch point were cherry-picked to production
|
||||
# and shipped in the release. We use `git cherry` to compare patches and
|
||||
# exclude commits that are already in the release — just like
|
||||
# get-new-prs.sh does for production.
|
||||
#
|
||||
# `git cherry <release-tag> origin/main` marks commits unique to main
|
||||
# with '+' and those already in the release with '-'.
|
||||
commit_hashes=$(git cherry "$RELEASE_TAG" origin/main 2>/dev/null \
|
||||
| grep '^+' \
|
||||
| cut -d' ' -f2)
|
||||
|
||||
pr_numbers=()
|
||||
unmatched=()
|
||||
|
||||
for hash in $commit_hashes; do
|
||||
# Get the first line of the commit message
|
||||
title=$(git log -1 --pretty=format:%s "$hash" 2>/dev/null)
|
||||
|
||||
# Skip [skip ci] commits (version bumps, deploys)
|
||||
if echo "$title" | grep -q '\[skip ci\]'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip deploy commits
|
||||
if echo "$title" | grep -q '^Deploy from'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract PR number from commit message (format: "... (#1234)")
|
||||
pr=$(echo "$title" | grep -oE '#[0-9]+' | head -1 | sed 's/#//')
|
||||
|
||||
if [ -n "$pr" ]; then
|
||||
pr_numbers+=("$pr")
|
||||
else
|
||||
unmatched+=("$hash $title")
|
||||
fi
|
||||
done
|
||||
|
||||
# Output unique PR numbers, sorted descending
|
||||
printf '%s\n' "${pr_numbers[@]}" | sort -rn | uniq
|
||||
|
||||
# Output unmatched commits to stderr
|
||||
if [ ${#unmatched[@]} -gt 0 ]; then
|
||||
echo "" >&2
|
||||
echo "Commits without PR numbers:" >&2
|
||||
for line in "${unmatched[@]}"; do
|
||||
echo " $line" >&2
|
||||
done
|
||||
fi
|
||||
76
skills/update-release-notes/scripts/get-new-prs.sh
Executable file
76
skills/update-release-notes/scripts/get-new-prs.sh
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# Find PR numbers on the production branch since a release branch.
|
||||
# Uses `git cherry` (like extract-draft-changelog.tsx) to correctly
|
||||
# handle cherry-picked hotfix commits.
|
||||
#
|
||||
# Usage: ./get-new-prs.sh <tldraw-repo-dir> <release-branch>
|
||||
# Example: ./get-new-prs.sh /tmp/tldraw v4.2.x
|
||||
#
|
||||
# Output:
|
||||
# PR numbers (one per line), then any commits
|
||||
# without PR numbers (hash + title) on stderr.
|
||||
#
|
||||
# Requires a local clone of tldraw/tldraw.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ] || [ -z "$2" ]; then
|
||||
echo "Usage: $0 <tldraw-repo-dir> <release-branch>" >&2
|
||||
echo "Example: $0 /tmp/tldraw v4.2.x" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TLDRAW_REPO="$1"
|
||||
RELEASE_BRANCH="$2"
|
||||
|
||||
cd "$TLDRAW_REPO"
|
||||
|
||||
# Fetch to ensure we have latest refs
|
||||
git fetch origin production "$RELEASE_BRANCH" --quiet 2>/dev/null || true
|
||||
|
||||
# Use git cherry to find commits on production that aren't on the
|
||||
# release branch. git cherry compares patches (not hashes), so it
|
||||
# correctly handles cherry-picks and hotfixes.
|
||||
# Lines starting with '+' are unique to production.
|
||||
commit_hashes=$(git cherry "origin/$RELEASE_BRANCH" origin/production 2>/dev/null \
|
||||
| grep '^+' \
|
||||
| cut -d' ' -f2)
|
||||
|
||||
pr_numbers=()
|
||||
unmatched=()
|
||||
|
||||
for hash in $commit_hashes; do
|
||||
# Get the first line of the commit message
|
||||
title=$(git log -1 --pretty=format:%s "$hash" 2>/dev/null)
|
||||
|
||||
# Skip [skip ci] commits (version bumps, deploys)
|
||||
if echo "$title" | grep -q '\[skip ci\]'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Skip deploy commits
|
||||
if echo "$title" | grep -q '^Deploy from'; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract PR number from commit message (format: "... (#1234)")
|
||||
pr=$(echo "$title" | grep -oE '#[0-9]+' | head -1 | sed 's/#//')
|
||||
|
||||
if [ -n "$pr" ]; then
|
||||
pr_numbers+=("$pr")
|
||||
else
|
||||
unmatched+=("$hash $title")
|
||||
fi
|
||||
done
|
||||
|
||||
# Output unique PR numbers, sorted descending
|
||||
printf '%s\n' "${pr_numbers[@]}" | sort -rn | uniq
|
||||
|
||||
# Output unmatched commits (hotfixes without PR numbers) to stderr
|
||||
if [ ${#unmatched[@]} -gt 0 ]; then
|
||||
echo "" >&2
|
||||
echo "Commits without PR numbers (likely hotfixes):" >&2
|
||||
for line in "${unmatched[@]}"; do
|
||||
echo " $line" >&2
|
||||
done
|
||||
fi
|
||||
84
skills/update-release-notes/scripts/update-draft-release.sh
Executable file
84
skills/update-release-notes/scripts/update-draft-release.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# Update (or create) the draft GitHub release for the next release using the
|
||||
# current next.mdx content. publish-new.ts finds this draft by name (vX.Y.0) and
|
||||
# publishes its body verbatim at release time, so keeping it in sync with next.mdx
|
||||
# means the GitHub release and the docs release page always match.
|
||||
#
|
||||
# Usage: ./update-draft-release.sh <tldraw-repo-dir> [next-version]
|
||||
#
|
||||
# next-version overrides the target tag (with or without a leading `v`). Pass it
|
||||
# for a major release or any override, so the draft name matches the version
|
||||
# publish-new.ts will resolve. When omitted, the target defaults to the next
|
||||
# minor of last_version — the common case.
|
||||
#
|
||||
# Requires a local clone of tldraw/tldraw and an authenticated gh CLI.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <tldraw-repo-dir> [next-version]" >&2
|
||||
echo "Example: $0 /tmp/tldraw # minor bump of last_version" >&2
|
||||
echo "Example: $0 /tmp/tldraw v6.0.0 # explicit target (major/override)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TLDRAW_REPO="$1"
|
||||
NEXT_FILE="$TLDRAW_REPO/apps/docs/content/releases/next.mdx"
|
||||
|
||||
if [ ! -f "$NEXT_FILE" ]; then
|
||||
echo "next.mdx not found at $NEXT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "$2" ]; then
|
||||
# Explicit target — normalize to a leading `v`. Use this for major releases or
|
||||
# overrides, where publish-new.ts resolves the next version from NPM rather than
|
||||
# a minor bump (e.g. --bump major -> v6.0.0).
|
||||
next_tag="v$(echo "$2" | sed 's/^v//')"
|
||||
else
|
||||
# The draft tracks the next minor release. Derive it from last_version (the most
|
||||
# recent published release recorded in next.mdx frontmatter): bump minor, patch 0.
|
||||
# e.g. last_version v5.2.0 -> draft v5.3.0.
|
||||
last_version=$(grep -E "^last_version:" "$NEXT_FILE" | head -1 | sed "s/last_version: *//" | tr -d "'\"[:space:]")
|
||||
if [ -z "$last_version" ]; then
|
||||
echo "Could not read last_version from $NEXT_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
major=$(echo "$last_version" | sed 's/^v//' | cut -d. -f1)
|
||||
minor=$(echo "$last_version" | sed 's/^v//' | cut -d. -f2)
|
||||
next_tag="v${major}.$((minor + 1)).0"
|
||||
fi
|
||||
|
||||
# Body = next.mdx with the frontmatter block stripped (everything after the second ---).
|
||||
body_file=$(mktemp)
|
||||
awk 'f{print} /^---$/{c++; if(c==2){f=1}}' "$NEXT_FILE" > "$body_file"
|
||||
|
||||
# Fail loudly if there is nothing to publish rather than pushing an empty draft.
|
||||
if ! grep -qE '[^[:space:]]' "$body_file"; then
|
||||
echo "next.mdx has no content below the frontmatter; refusing to write an empty draft." >&2
|
||||
rm -f "$body_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Draft release target: $next_tag" >&2
|
||||
|
||||
# Look up an existing release for this tag (finds drafts too).
|
||||
existing=$(gh release view "$next_tag" -R tldraw/tldraw --json isDraft 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$existing" ]; then
|
||||
is_draft=$(echo "$existing" | jq -r '.isDraft')
|
||||
if [ "$is_draft" != "true" ]; then
|
||||
echo "Release $next_tag already exists and is published; refusing to edit." >&2
|
||||
rm -f "$body_file"
|
||||
exit 1
|
||||
fi
|
||||
echo "Updating existing draft release $next_tag" >&2
|
||||
gh release edit "$next_tag" -R tldraw/tldraw --title "$next_tag" --notes-file "$body_file"
|
||||
else
|
||||
echo "Creating new draft release $next_tag" >&2
|
||||
gh release create "$next_tag" -R tldraw/tldraw --draft --title "$next_tag" --notes-file "$body_file"
|
||||
fi
|
||||
|
||||
rm -f "$body_file"
|
||||
echo "Done. Draft release $next_tag is in sync with next.mdx." >&2
|
||||
124
skills/write-docs/SKILL.md
Normal file
124
skills/write-docs/SKILL.md
Normal file
@@ -0,0 +1,124 @@
|
||||
---
|
||||
name: write-docs
|
||||
description: Writing SDK documentation for tldraw. Use when creating new documentation articles, updating existing docs, or when documentation writing guidance is needed. Applies to docs in apps/docs/content/.
|
||||
---
|
||||
|
||||
# Write documentation
|
||||
|
||||
This skill covers how to write and update tldraw SDK documentation.
|
||||
|
||||
## Location
|
||||
|
||||
All documentation lives in `apps/docs/content/`. The main categories are:
|
||||
|
||||
| Directory | Purpose |
|
||||
| ------------------ | --------------------------------------------- |
|
||||
| `docs/` | SDK documentation articles |
|
||||
| `releases/` | Release notes (see write-release-notes skill) |
|
||||
| `examples/` | Example documentation |
|
||||
| `getting-started/` | Quickstart and setup guides |
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Understand the scope
|
||||
|
||||
Before writing:
|
||||
|
||||
- Identify the target audience (new users, experienced developers, API reference)
|
||||
- Check existing docs that cover related topics
|
||||
- Look at relevant examples in `apps/examples/`
|
||||
- Read the API types and comments in the source code
|
||||
|
||||
### 2. Create the file
|
||||
|
||||
Create a new `.mdx` file in the appropriate directory with frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Feature name
|
||||
status: published
|
||||
author: steveruizok
|
||||
date: 3/22/2023
|
||||
order: 1
|
||||
keywords:
|
||||
- keyword1
|
||||
- keyword2
|
||||
---
|
||||
```
|
||||
|
||||
### 3. Write the content
|
||||
|
||||
Follow the structure:
|
||||
|
||||
1. **Overview** — 1-2 paragraphs on what and why
|
||||
2. **Basic usage** — The simplest working example
|
||||
3. **Details** — Deeper explanation with more examples
|
||||
4. **Edge cases** — Advanced patterns, gotchas
|
||||
5. **Links** — Related docs and examples
|
||||
|
||||
### 4. Use MDX components
|
||||
|
||||
#### API links
|
||||
|
||||
Use `[ClassName](?)` or `[ClassName#methodName](?)` for API references:
|
||||
|
||||
```markdown
|
||||
The [Editor](?) class has many methods. Use [Editor#createShapes](?) to create shapes.
|
||||
```
|
||||
|
||||
#### Code highlighting
|
||||
|
||||
Use `<FocusLines>` to highlight specific lines:
|
||||
|
||||
```markdown
|
||||
<FocusLines lines={[2,6,10]}>
|
||||
|
||||
\`\`\`tsx
|
||||
import { Tldraw } from 'tldraw'
|
||||
import { useSyncDemo } from '@tldraw/sync'
|
||||
\`\`\`
|
||||
|
||||
</FocusLines>
|
||||
```
|
||||
|
||||
#### Images
|
||||
|
||||
```markdown
|
||||
<Image
|
||||
src="/images/api/events.png"
|
||||
alt="A diagram showing an event being sent to the editor."
|
||||
title="Caption text here."
|
||||
/>
|
||||
```
|
||||
|
||||
#### Tables for API documentation
|
||||
|
||||
Use tables for listing methods, options, or properties:
|
||||
|
||||
```markdown
|
||||
| Method | Description |
|
||||
| ------------------------ | ---------------------------------------------- |
|
||||
| [Editor#screenToPage](?) | Convert a point in screen space to page space. |
|
||||
| [Editor#pageToScreen](?) | Convert a point in page space to screen space. |
|
||||
```
|
||||
|
||||
```markdown
|
||||
| Value | Description |
|
||||
| --------- | ---------------------------------------------------- |
|
||||
| `default` | Sets the initial zoom to 100%. |
|
||||
| `fit-x` | The x axis will completely fill the viewport bounds. |
|
||||
```
|
||||
|
||||
### 5. Verify
|
||||
|
||||
Check that:
|
||||
|
||||
- Code examples actually work
|
||||
- API links resolve correctly
|
||||
- Images have alt text
|
||||
- Headings use sentence case
|
||||
- No AI tells (see style guide)
|
||||
|
||||
## References
|
||||
|
||||
- **Style guide**: See `../shared/docs-guide.md` for voice, tone, and formatting conventions.
|
||||
288
skills/write-e2e-tests/SKILL.md
Normal file
288
skills/write-e2e-tests/SKILL.md
Normal file
@@ -0,0 +1,288 @@
|
||||
---
|
||||
name: write-e2e-tests
|
||||
description: Writing Playwright E2E tests for tldraw. Use when creating browser tests, testing UI interactions, or adding E2E coverage in apps/examples/e2e or apps/dotcom/client/e2e.
|
||||
---
|
||||
|
||||
# Writing E2E tests
|
||||
|
||||
E2E tests use Playwright. Located in `apps/examples/e2e/` (SDK examples) and `apps/dotcom/client/e2e/` (tldraw.com).
|
||||
|
||||
## Test file structure
|
||||
|
||||
```
|
||||
apps/examples/e2e/
|
||||
├── fixtures/
|
||||
│ ├── fixtures.ts # Test fixtures (toolbar, menus, etc.)
|
||||
│ └── menus/ # Page object models
|
||||
├── tests/
|
||||
│ └── test-*.spec.ts # Test files
|
||||
└── shared-e2e.ts # Shared utilities
|
||||
```
|
||||
|
||||
Name test files `test-<feature>.spec.ts`.
|
||||
|
||||
## Required declarations
|
||||
|
||||
When using `page.evaluate()` to access the editor or UI events:
|
||||
|
||||
```typescript
|
||||
import { Editor } from 'tldraw'
|
||||
|
||||
declare const editor: Editor
|
||||
declare const __tldraw_ui_event: { name: string; data?: any }
|
||||
```
|
||||
|
||||
## Basic test structure
|
||||
|
||||
```typescript
|
||||
import { expect } from '@playwright/test'
|
||||
import test from '../fixtures/fixtures'
|
||||
import { setupOrReset } from '../shared-e2e'
|
||||
|
||||
test.describe('Feature name', () => {
|
||||
test.beforeEach(setupOrReset)
|
||||
|
||||
test('does something', async ({ page, toolbar }) => {
|
||||
// Test implementation
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Setup patterns
|
||||
|
||||
### Standard setup (recommended)
|
||||
|
||||
```typescript
|
||||
test.beforeEach(setupOrReset) // Smart: navigates first run, fast reset after
|
||||
```
|
||||
|
||||
### Shared page for performance
|
||||
|
||||
For tests that don't need full isolation:
|
||||
|
||||
```typescript
|
||||
let page: Page
|
||||
|
||||
test.describe('Feature', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
})
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await hardResetEditor(page)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Setup with shapes
|
||||
|
||||
```typescript
|
||||
import { setupPageWithShapes, hardResetWithShapes } from '../shared-e2e'
|
||||
|
||||
test.beforeEach(async ({ browser }) => {
|
||||
if (!page) {
|
||||
page = await browser.newPage()
|
||||
await setupPage(page)
|
||||
} else {
|
||||
await hardResetEditor(page)
|
||||
}
|
||||
await setupPageWithShapes(page)
|
||||
})
|
||||
```
|
||||
|
||||
## Available fixtures
|
||||
|
||||
```typescript
|
||||
test('example', async ({
|
||||
page, // Playwright page
|
||||
toolbar, // Toolbar page object
|
||||
stylePanel, // Style panel
|
||||
actionsMenu, // Actions menu
|
||||
mainMenu, // Main menu
|
||||
pageMenu, // Page menu
|
||||
navigationPanel, // Navigation panel
|
||||
richTextToolbar, // Rich text toolbar
|
||||
api, // tldrawApi methods
|
||||
isMobile, // Mobile viewport check
|
||||
isMac, // Mac platform check
|
||||
}) => {})
|
||||
```
|
||||
|
||||
## Interacting with the editor
|
||||
|
||||
### Via page.evaluate
|
||||
|
||||
```typescript
|
||||
// Execute code in browser context
|
||||
await page.evaluate(() => {
|
||||
editor.createShapes([{ type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } }])
|
||||
})
|
||||
|
||||
// Fast reset (faster than keyboard shortcuts)
|
||||
await page.evaluate(() => {
|
||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
|
||||
editor.setCurrentTool('select')
|
||||
})
|
||||
|
||||
// Get data from editor
|
||||
const shape = await page.evaluate(() => editor.getOnlySelectedShape())
|
||||
expect(shape).toMatchObject({ type: 'geo', x: 100, y: 100 })
|
||||
```
|
||||
|
||||
### Testing UI events
|
||||
|
||||
```typescript
|
||||
await page.keyboard.press('Control+a')
|
||||
expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({
|
||||
name: 'select-all-shapes',
|
||||
data: { source: 'kbd' },
|
||||
})
|
||||
```
|
||||
|
||||
## Selecting tools and UI elements
|
||||
|
||||
### By test ID
|
||||
|
||||
```typescript
|
||||
await page.getByTestId('tools.rectangle').click()
|
||||
await page.getByTestId('tools.more.cloud').click() // In popover
|
||||
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
|
||||
```
|
||||
|
||||
### Via toolbar fixture
|
||||
|
||||
```typescript
|
||||
const { select, draw, arrow, rectangle } = toolbar.tools
|
||||
await rectangle.click()
|
||||
await toolbar.isSelected(rectangle)
|
||||
await toolbar.isNotSelected(select)
|
||||
|
||||
// More tools popover
|
||||
await toolbar.moreToolsButton.click()
|
||||
await toolbar.popOverTools.popoverCloud.click()
|
||||
```
|
||||
|
||||
## Menu interactions
|
||||
|
||||
```typescript
|
||||
import { clickMenu, withMenu } from '../shared-e2e'
|
||||
|
||||
// Click a menu item
|
||||
await clickMenu(page, 'main-menu.edit.copy')
|
||||
await clickMenu(page, 'context-menu.copy-as.copy-as-png')
|
||||
|
||||
// Focus and interact with menu item
|
||||
await page.mouse.click(200, 200, { button: 'right' })
|
||||
await withMenu(page, 'context-menu.arrange.distribute-horizontal', (item) => item.focus())
|
||||
await page.keyboard.press('Enter')
|
||||
```
|
||||
|
||||
## Data-driven tests
|
||||
|
||||
```typescript
|
||||
const tools = [
|
||||
{ tool: 'rectangle', shape: 'geo' },
|
||||
{ tool: 'arrow', shape: 'arrow' },
|
||||
{ tool: 'draw', shape: 'draw' },
|
||||
]
|
||||
|
||||
test('creates shapes with tools', async ({ page, toolbar }) => {
|
||||
for (const { tool, shape } of tools) {
|
||||
await page.getByTestId(`tools.${tool}`).click()
|
||||
await page.mouse.click(200, 200)
|
||||
expect(await getAllShapeTypes(page)).toContain(shape)
|
||||
|
||||
// Reset for next iteration
|
||||
await page.evaluate(() => {
|
||||
editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Platform-specific handling
|
||||
|
||||
### Modifier keys
|
||||
|
||||
```typescript
|
||||
test('copy paste', async ({ page, isMac }) => {
|
||||
const modifier = isMac ? 'Meta' : 'Control'
|
||||
await page.keyboard.down(modifier)
|
||||
await page.keyboard.press('KeyC')
|
||||
await page.keyboard.press('KeyV')
|
||||
await page.keyboard.up(modifier)
|
||||
})
|
||||
```
|
||||
|
||||
### Skip on mobile
|
||||
|
||||
```typescript
|
||||
test('desktop only feature', async ({ isMobile }) => {
|
||||
if (isMobile) return
|
||||
// Desktop-specific test
|
||||
})
|
||||
```
|
||||
|
||||
## Helper functions
|
||||
|
||||
```typescript
|
||||
import { getAllShapeTypes, getAllShapeLabels, sleep, sleepFrames } from '../shared-e2e'
|
||||
|
||||
// Get shape types on canvas
|
||||
const shapes = await getAllShapeTypes(page)
|
||||
expect(shapes).toEqual(['geo', 'arrow'])
|
||||
|
||||
// Wait for async operations
|
||||
await sleep(100)
|
||||
await sleepFrames(2) // Wait for animation frames
|
||||
```
|
||||
|
||||
## Assertions
|
||||
|
||||
```typescript
|
||||
// Shape assertions
|
||||
expect(await page.evaluate(() => editor.getOnlySelectedShape())).toMatchObject({
|
||||
type: 'geo',
|
||||
props: { w: 100, h: 100 },
|
||||
})
|
||||
|
||||
// Attribute assertions
|
||||
await expect(page.getByTestId('tools.select')).toHaveAttribute('aria-pressed', 'true')
|
||||
|
||||
// CSS assertions (for selection state)
|
||||
await expect(tool).toHaveCSS('color', 'rgb(255, 255, 255)')
|
||||
|
||||
// Visibility
|
||||
await expect(toolbar.moreToolsPopover).toBeVisible()
|
||||
await expect(toolbar.toolLock).toBeHidden()
|
||||
```
|
||||
|
||||
## Skipping flaky tests
|
||||
|
||||
```typescript
|
||||
test.describe.skip('clipboard tests', () => {
|
||||
// Skipped because flaky in CI
|
||||
})
|
||||
|
||||
test.skip('known issue', async () => {})
|
||||
```
|
||||
|
||||
## Running E2E tests
|
||||
|
||||
```bash
|
||||
yarn e2e # Examples E2E
|
||||
yarn e2e-dotcom # Dotcom E2E
|
||||
yarn e2e-ui # With Playwright UI
|
||||
yarn e2e -- --grep "toolbar" # Filter by pattern
|
||||
```
|
||||
|
||||
## Key patterns summary
|
||||
|
||||
- Use `setupOrReset` in `beforeEach` for test isolation
|
||||
- Declare `editor` and `__tldraw_ui_event` for `page.evaluate()`
|
||||
- Use `page.evaluate()` for fast editor manipulation (faster than keyboard)
|
||||
- Use `getByTestId()` with `tools.<name>` pattern for tool selection
|
||||
- Use `clickMenu()` / `withMenu()` for menu interactions
|
||||
- Handle platform differences with `isMac` and `isMobile` fixtures
|
||||
- Test against `localhost:5420/end-to-end` example
|
||||
201
skills/write-example/SKILL.md
Normal file
201
skills/write-example/SKILL.md
Normal file
@@ -0,0 +1,201 @@
|
||||
---
|
||||
name: write-example
|
||||
description: Writing examples for the tldraw SDK examples app. Use when creating new examples, adding SDK demonstrations, or writing example code in apps/examples.
|
||||
---
|
||||
|
||||
# Writing tldraw examples
|
||||
|
||||
The examples project (`apps/examples`) contains minimal demonstrations of how to use the tldraw SDK. Examples are embedded on the [docs site](https://tldraw.dev/examples) and deployed to [examples.tldraw.com](https://examples.tldraw.com).
|
||||
|
||||
Standards for examples in `apps/examples/src/examples`.
|
||||
|
||||
## Example structure
|
||||
|
||||
Each example lives in its own folder:
|
||||
|
||||
```
|
||||
apps/examples/src/examples/
|
||||
└── my-example/
|
||||
├── README.md # Required metadata
|
||||
├── MyExampleExample.tsx # Main example file
|
||||
└── my-example.css # Optional styles
|
||||
```
|
||||
|
||||
## Folder name
|
||||
|
||||
- Lowercase kebab-case: `custom-canvas`, `button-demo`, `magical-wand`
|
||||
- Used as the URL path for the example
|
||||
|
||||
## README.md
|
||||
|
||||
Required frontmatter format:
|
||||
|
||||
```md
|
||||
---
|
||||
title: Example title
|
||||
component: ./ExampleFile.tsx
|
||||
category: category-id
|
||||
priority: 1
|
||||
keywords: [keyword1, keyword2]
|
||||
---
|
||||
|
||||
One-line summary of what this example demonstrates.
|
||||
|
||||
---
|
||||
|
||||
Detailed explanation of the example. Include code snippets here if they help explain concepts not obvious from the example code itself.
|
||||
```
|
||||
|
||||
### Frontmatter fields
|
||||
|
||||
| Field | Description |
|
||||
| --------- | ------------------------------------------------ |
|
||||
| title | Sentence case, corresponds to folder name |
|
||||
| component | Relative path to example file |
|
||||
| category | One of the valid category IDs (see below) |
|
||||
| priority | Display order within category (lower = higher) |
|
||||
| keywords | Search terms (avoid obvious terms like "tldraw") |
|
||||
|
||||
### Valid categories
|
||||
|
||||
`getting-started`, `configuration`, `editor-api`, `ui`, `layout`, `events`, `shapes/tools`, `collaboration`, `data/assets`, `use-cases`
|
||||
|
||||
## Example file
|
||||
|
||||
### Naming
|
||||
|
||||
- PascalCase ending with "Example": `CustomCanvasExample.tsx`, `ButtonExample.tsx`
|
||||
- Name should correspond to the folder name and title
|
||||
|
||||
### Structure
|
||||
|
||||
```tsx
|
||||
import { Tldraw } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
export default function MyExampleExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Must have a default export React component
|
||||
- Use `tldraw__editor` class for full-page examples
|
||||
- Import `tldraw/tldraw.css` for styles
|
||||
|
||||
### Layout
|
||||
|
||||
- Full page: wrap in `<div className="tldraw__editor">`
|
||||
- Inset: see existing examples for page layout patterns
|
||||
|
||||
## Styles
|
||||
|
||||
- Put CSS in a separate file named after the example: `my-example.css`
|
||||
- Import alongside tldraw CSS: `import './my-example.css'`
|
||||
- Avoid extensive inline styles via the `style` prop
|
||||
|
||||
## Control panels
|
||||
|
||||
For examples that need buttons or controls, use the `TopPanel` component slot with `TldrawUiButton`:
|
||||
|
||||
```tsx
|
||||
import { Tldraw, TldrawUiButton, useEditor } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
import './my-example.css'
|
||||
|
||||
function MyControls() {
|
||||
const editor = useEditor()
|
||||
return (
|
||||
<div className="tlui-menu my-controls">
|
||||
<TldrawUiButton type="normal" onClick={() => editor.zoomIn()}>
|
||||
Zoom in
|
||||
</TldrawUiButton>
|
||||
<TldrawUiButton type="normal" onClick={() => editor.zoomOut()}>
|
||||
Zoom out
|
||||
</TldrawUiButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MyExampleExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw components={{ TopPanel: MyControls }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
CSS for control panels:
|
||||
|
||||
```css
|
||||
.my-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 8px;
|
||||
}
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
Use footnote format with numbered references:
|
||||
|
||||
```tsx
|
||||
import { Tldraw, type TLComponents } from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
// [1]
|
||||
const components: TLComponents = {
|
||||
PageMenu: null,
|
||||
}
|
||||
|
||||
export default function CustomComponentsExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
{/* [2] */}
|
||||
<Tldraw components={components} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
[1]
|
||||
Define component overrides outside the React component so they're static.
|
||||
If defined inside, use useMemo to prevent recreation on every render.
|
||||
|
||||
[2]
|
||||
Pass component overrides via the components prop.
|
||||
*/
|
||||
```
|
||||
|
||||
## Example types
|
||||
|
||||
### Tight examples
|
||||
|
||||
- Narrow focus on a specific SDK feature
|
||||
- Minimal styling
|
||||
- Meant to be read, not used
|
||||
- Remove any extraneous code
|
||||
|
||||
### Use-case examples
|
||||
|
||||
- Show a recognizable user experience
|
||||
- Prioritize clarity and completeness
|
||||
- Category: `use-cases`
|
||||
|
||||
## Additional files
|
||||
|
||||
- Split complex code into separate files if it distracts from the example's purpose
|
||||
- Example: complex input component in `Input.tsx`
|
||||
- Keep the main example file focused on demonstrating the concept
|
||||
|
||||
## Important
|
||||
|
||||
- Follow React and TypeScript best practices
|
||||
- Never use title case for titles - use sentence case
|
||||
- Keep examples minimal and focused
|
||||
123
skills/write-issue/SKILL.md
Normal file
123
skills/write-issue/SKILL.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: write-issue
|
||||
description: Reference standards for writing and maintaining GitHub issues in the tldraw repository. Use as supporting guidance when another skill or workflow needs issue title, body, type, label, or triage standards.
|
||||
---
|
||||
|
||||
# Writing and maintaining GitHub issues
|
||||
|
||||
Standards for issues in tldraw/tldraw.
|
||||
|
||||
## Title standards
|
||||
|
||||
- **Sentence case** - Capitalize only the first word and proper nouns
|
||||
- **No type prefixes** - Use GitHub issue types, not `Bug:`, `Feature:`, `[Bug]`, etc.
|
||||
- **Imperative mood for enhancements** - "Add padding option" not "Adding padding option"
|
||||
- **Descriptive for bugs** - Describe the symptom: "Arrow bindings break with rotated shapes"
|
||||
- **Specific** - Readable without opening the issue body
|
||||
|
||||
### Good titles
|
||||
|
||||
- `Arrow bindings break with rotated shapes`
|
||||
- `Add padding option to zoomToFit method`
|
||||
- `Pinch zoom resets selection on Safari`
|
||||
|
||||
### Bad titles
|
||||
|
||||
- `Bug: arrow bug` (prefix, vague)
|
||||
- `[Feature] Add new feature` (prefix, vague)
|
||||
- `Not working` (vague)
|
||||
|
||||
### Title cleanup transformations
|
||||
|
||||
1. Remove prefixes: `Bug: X` → `X`
|
||||
2. Fix capitalization: `Add Padding Option` → `Add padding option`
|
||||
3. Use imperative: `Adding feature X` → `Add feature X`
|
||||
4. Be specific: `Problem` → `[Describe the actual problem]`
|
||||
5. Translate non-English titles to English
|
||||
|
||||
## Issue types
|
||||
|
||||
Set via the GitHub GraphQL API after creating the issue (the `--type` flag is not reliably supported):
|
||||
|
||||
| Type | Use for |
|
||||
| --------- | ----------------------------------- |
|
||||
| `Bug` | Something isn't working as expected |
|
||||
| `Feature` | New capability or improvement |
|
||||
| `Example` | Request for a new SDK example |
|
||||
| `Task` | Internal task or chore |
|
||||
|
||||
## Labels
|
||||
|
||||
Use sparingly (1-2 per issue) for metadata, not categorization.
|
||||
|
||||
### Common labels
|
||||
|
||||
| Label | Use for |
|
||||
| ------------------ | -------------------------------- |
|
||||
| `good first issue` | Well-scoped issues for newcomers |
|
||||
| `More Info Needed` | Requires additional information |
|
||||
| `sdk` | Affects the tldraw SDK |
|
||||
| `dotcom` | Related to tldraw.com |
|
||||
| `a11y` | Accessibility |
|
||||
| `performance` | Performance improvement |
|
||||
| `api` | API change |
|
||||
|
||||
### Automation labels (do not apply manually)
|
||||
|
||||
`keep`, `stale`, `update-snapshots`, `publish-packages`, `major`, `minor`, `skip-release`, deploy triggers
|
||||
|
||||
## Issue body standards
|
||||
|
||||
### Bug reports
|
||||
|
||||
1. Clear description of what's wrong
|
||||
2. Steps to reproduce
|
||||
3. Expected vs actual behavior
|
||||
4. Environment details (browser, OS, version) when relevant
|
||||
5. Screenshots/recordings when applicable
|
||||
|
||||
### Feature requests
|
||||
|
||||
1. Problem statement - What problem does this solve?
|
||||
2. Proposed solution - How should it work?
|
||||
3. Alternatives considered
|
||||
4. Use cases
|
||||
|
||||
### Example requests
|
||||
|
||||
1. What API/pattern to demonstrate
|
||||
2. Why it's useful
|
||||
3. Suggested approach
|
||||
4. Which example category it belongs to
|
||||
|
||||
### Agent-drafted issues (problem readback and open questions)
|
||||
|
||||
Issues created by the `/issue` skill capture the user's intent over a short interrogation, so they carry an unheaded readback paragraph beneath the verbatim description, followed by open questions and a confidence status line at the bottom:
|
||||
|
||||
- **Problem readback** - A brief, unheaded paragraph that states the agent's interpretation of the problem, expected behavior, and scope. It should be easy for the user to correct. Keep it product-facing: no code blocks, no long implementation analysis, no file paths, no function names, no line numbers, and no fix recipes unless the user explicitly asks for them.
|
||||
- **Open questions** - A numbered list of the specific gaps in intent or context still worth asking the user about. Answers are written beneath each question as the user replies, and the readback is revised each round. A question prefixed with `Critical:` is genuinely blocking — the issue cannot be worked on until it is answered. Most issues have none. Non-critical questions the user chooses not to answer are marked `_Deferred by user; not blocking implementation._`.
|
||||
- **Confidence line** - A plain-text line at the bottom, not a section heading, such as `Confidence: 84%, ready to get started.` or `Confidence: 42%, still need more information.` It reflects whether the issue has enough of the user's intent and context to work on, not confidence in the eventual fix.
|
||||
|
||||
Leave the readback, open questions, and confidence line in the issue once interrogation is complete. The answered questions are a record of the discussion that produced the issue, so keep them rather than deleting them — mark questions resolved in place and keep the final readback as the issue's concise problem statement.
|
||||
|
||||
## Triage workflow
|
||||
|
||||
### New issues
|
||||
|
||||
1. Verify sufficient information to act on
|
||||
2. Set appropriate issue type
|
||||
3. Clean up title if needed
|
||||
4. Add `More Info Needed` label and comment if details missing
|
||||
5. Add `good first issue` if appropriate
|
||||
|
||||
### Stale issues
|
||||
|
||||
1. Review if still relevant
|
||||
2. Close if no longer applicable
|
||||
3. Add `keep` label if should remain open
|
||||
4. Request updates if waiting on information
|
||||
|
||||
## Important
|
||||
|
||||
- Never include AI attribution unless the issue directly relates to AI tooling
|
||||
- Never use title case for descriptions - use sentence case
|
||||
310
skills/write-pr/SKILL.md
Normal file
310
skills/write-pr/SKILL.md
Normal file
@@ -0,0 +1,310 @@
|
||||
---
|
||||
name: write-pr
|
||||
description: Reference standards for writing pull request titles and descriptions in the tldraw repository, plus the pre-flight comment sweep over the diff. Use as supporting guidance when another skill or workflow needs PR content standards, not as the user-facing create/update PR workflow.
|
||||
---
|
||||
|
||||
# Writing pull requests
|
||||
|
||||
Standards for PR titles and descriptions in tldraw/tldraw.
|
||||
|
||||
## Write for the reviewer (read this first)
|
||||
|
||||
This is the governing rule; everything below serves it. Write the description for a reviewer who knows the codebase architecture but has **not** read your code or the diff. Their time is the scarce resource. The description's job is to give them the framing they can't get from the code, then get out of the way.
|
||||
|
||||
**Default to short.** A few sentences of framing is the norm, not the exception. Match length to the change: a one-line fix needs a sentence; a new system needs the fuller treatment. A description that looks long and structured is not more valuable — often it's less, because it buries the framing under scaffolding. If a reviewer would need their own AI to interpret yours, it has failed.
|
||||
|
||||
**Order it coarse to granular.** Structure the description as an inverted pyramid, most important first. A reader should be able to stop at any point and leave with a correct, coherent understanding: a skimmer gets the goal and motivation from the opening lines; someone weighing the approach reads into the design and decisions; a close reviewer continues to the specifics. Never make someone read to the end to find out what the PR is for.
|
||||
|
||||
Cover the following, roughly in this order — each layer more detailed than the last, and each optional once the change no longer warrants it:
|
||||
|
||||
- **Goal, motivation, and use case.** Why this change, why now, what it's for. What was wrong or missing before. This comes first, always.
|
||||
- **The higher-level change.** The shape of the solution — behavior and structure, not a line-by-line restatement of the diff.
|
||||
- **API design and decisions.** New or changed public surface, the approach you picked, what you ruled out and why, and anything you're unsure about. A decision you don't surface is one the reviewer can't catch.
|
||||
- **Example snippets and fine detail.** For anything touching an API, data shape, or usage pattern, a few lines of before/after say more than a paragraph. Keep them minimal.
|
||||
|
||||
**Do not:**
|
||||
|
||||
- Restate the diff. No file-by-file walkthrough, no narrating what a function does, no describing *how* the code works step by step. Reviewers can read code.
|
||||
- Generate tables or lists that mirror the code — a `Method | Description` table that just re-says the signatures, an inventory of every changed file, a term glossary of self-explanatory names.
|
||||
- Pad with ceremony. Structure that exists to look thorough is noise.
|
||||
|
||||
**Never invent the *why*.** The motivation and trade-offs must come from real intent — the commits, the linked issue, or the author. If you don't know why a change was made or what was considered, **ask the user; do not guess.** A confident but fabricated rationale is worse than useless: it's misleading, and it's the thing reviewers most need to trust.
|
||||
|
||||
## PR title
|
||||
|
||||
Use semantic PR titles (Conventional Commits format):
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
- `feat` - New feature
|
||||
- `fix` - Bug fix
|
||||
- `docs` - Documentation only
|
||||
- `refactor` - Code change that neither fixes a bug nor adds a feature
|
||||
- `perf` - Performance improvement
|
||||
- `test` - Adding or fixing tests
|
||||
- `chore` - Maintenance tasks
|
||||
|
||||
### Scope (optional)
|
||||
|
||||
A noun describing the affected area: `fix(editor):`, `feat(sync):`, `docs(examples):`
|
||||
|
||||
### Examples
|
||||
|
||||
- `feat(editor): add snap threshold configuration option`
|
||||
- `fix(arrows): correct binding behavior with rotated shapes`
|
||||
- `docs: update sync documentation`
|
||||
- `refactor(store): simplify migration system`
|
||||
|
||||
## PR body
|
||||
|
||||
Bug fix and improvement PRs (`bugfix` and `improvement` change types) use the before/after template below. Everything else uses this one:
|
||||
|
||||
```md
|
||||
<description paragraph>
|
||||
|
||||
### Change type
|
||||
|
||||
- [x] `bugfix` | `improvement` | `feature` | `api` | `other`
|
||||
|
||||
### Test plan
|
||||
|
||||
1. Step to test...
|
||||
2. Another step...
|
||||
|
||||
- [ ] Unit tests
|
||||
- [ ] End to end tests
|
||||
|
||||
### Release notes
|
||||
|
||||
- Brief description of changes for users
|
||||
```
|
||||
|
||||
### Description paragraph
|
||||
|
||||
Start with: "In order to X, this PR does Y." Follow the reviewer-first rules at the top of this skill.
|
||||
|
||||
- X is the **concrete situation that made this work necessary** — the real thing someone was trying to do — not a restatement of what Y does. "In order to let apps carry undo history across editor rebuilds, this adds an API to carry undo history across editor rebuilds" is circular: X just renames Y. Push X up a level to the actual goal, e.g. "so desktop can reload the editor without losing the user's place, the way HMR preserves component state across a code edit."
|
||||
- Name the real driving case, not a hypothetical. If you're inventing example scenarios to illustrate the why ("e.g. if someone toggled a plugin…"), you're reverse-engineering a justification from the finished code — you haven't written down the case that actually prompted it. Work forward from that case instead.
|
||||
- Beware abstract mechanism as a stand-in for motivation. A true technical fact about the SDK ("editor config is fixed at construction time") explains a constraint, not why anyone cares. Keep going until a reader can picture the specific thing that stopped working or became possible.
|
||||
- Keep it specific - avoid vague phrases like "improve user experience"
|
||||
- Link related issues in the first paragraph
|
||||
- Don't expect readers to also read the linked issue
|
||||
|
||||
### Bug fix and improvement PRs
|
||||
|
||||
Bug fix and improvement PRs use a fixed shape instead of the description paragraph. The same reviewer-first rules apply; the structure exists so a reviewer can see what was wrong (or lacking), what it does now, and why, without reading the diff.
|
||||
|
||||
```md
|
||||
This PR fixes a bug where <symptom a user or developer would hit>.
|
||||
|
||||
### Before
|
||||
|
||||
<what the code did and why that produced the symptom>
|
||||
|
||||
### After
|
||||
|
||||
<what the code does now>
|
||||
|
||||
### Implementation notes
|
||||
|
||||
<optional: what exactly changed and how — decisions, trade-offs, anything non-obvious>
|
||||
|
||||
### Change type
|
||||
|
||||
- [x] `bugfix` | `improvement`
|
||||
|
||||
### Test plan
|
||||
|
||||
- [x] Unit tests — the new test fails on `main` and passes with this change
|
||||
|
||||
### Release notes
|
||||
|
||||
- Fix <symptom> | Improve <behavior>
|
||||
|
||||
### Code changes
|
||||
|
||||
| Section | LOC change |
|
||||
| --------- | ---------- |
|
||||
| Core code | +10 / -2 |
|
||||
| Tests | +5 / -0 |
|
||||
```
|
||||
|
||||
- **Intro** — one sentence. For a bug: "This PR fixes a bug where X," where X is the observable symptom, not the cause or the fix. For an improvement: "This PR improves X so that Y," where Y is what a user or developer can now do. Link the issue here if there is one. If the PR does more than one thing, add a second sentence ("It also ...") rather than a list.
|
||||
- **Before** — the behavior before, and the mechanism that produced it. For a bug, name the function or path at fault; this is the one place a short description of *how* the old code went wrong belongs, because it is the cause the reviewer is checking the fix against. For an improvement, describe what the user or developer had to do (or couldn't do) and what in the code made it that way.
|
||||
- **After** — the behavior after. State what the code now does, at the same level of detail as Before. Note any spec or doc that was updated alongside.
|
||||
- **Implementation notes** — optional. Include only when there is something the Before/After pair doesn't carry: an approach you chose over another, a subtlety in the change, a follow-up you deliberately left out. Skip the heading entirely when there is nothing to say; most small fixes have nothing.
|
||||
- The `### Change type` / `### Test plan` / `### Release notes` / `### API changes` / `### Code changes` blocks follow as usual. For bug fixes, say in the test plan whether the new tests fail on `main`.
|
||||
|
||||
tldraw/tldraw#10117, #10118, and #10119 are worked examples of bug fixes in this shape.
|
||||
|
||||
### Before and after recordings
|
||||
|
||||
When a bug fix or improvement changes how a canvas interaction behaves — a drag, resize, rotate, crop, handle, or tool gesture that a reviewer would otherwise have to run locally to see — put a short recording in each of the Before and After sections. Prose describes the mechanism; the recordings show the symptom and its absence, and reviewers trust them more than a description of a jump or a jitter. Skip this for changes that are not visible on the canvas (data, API, docs, tooling).
|
||||
|
||||
`scripts/record-interaction.mjs` records a scripted interaction against the running examples app as a 16:9 MP4. It hides the tldraw chrome, injects a visible cursor (headless recordings have none), runs a scenario module, trims the setup frames, and transcodes with ffmpeg. `scripts/example-scenario.mjs` is a starting point; copy it to a scratch location and change the body to the interaction the PR affects. Do not commit scenarios.
|
||||
|
||||
It needs ffmpeg on the path and a Playwright Chromium build. Install scripts are disabled in this repo, so the browser is not downloaded by `yarn install`; run `yarn playwright install chromium` once. Then, from the repo root with `yarn dev` serving the examples app:
|
||||
|
||||
```bash
|
||||
# After: the current branch
|
||||
node skills/write-pr/scripts/record-interaction.mjs <scenario.mjs> <scratch>/after.mp4
|
||||
|
||||
# Before: main's versions of the files this branch modifies, hot-reloaded by vite
|
||||
git diff --name-only --diff-filter=M origin/main...HEAD | xargs git checkout origin/main --
|
||||
sleep 10
|
||||
node skills/write-pr/scripts/record-interaction.mjs <scenario.mjs> <scratch>/before.mp4
|
||||
git diff --name-only --diff-filter=M origin/main...HEAD | xargs git checkout HEAD --
|
||||
git status --porcelain # must be empty before continuing
|
||||
```
|
||||
|
||||
Restrict the checkout to modified files: checking out a whole directory from `main` also stages files that only exist there, and files added on this branch have no `main` version to restore. Only use this approach when the working tree is clean.
|
||||
|
||||
Rules for the recordings themselves:
|
||||
|
||||
- **Same scenario for both.** The two videos must differ only in the code under test, so the reviewer compares like with like. Record After first, confirm the scenario shows the behavior, then record Before.
|
||||
- **Make the symptom unmissable.** Pick inputs that exaggerate the bug — a large rotation, a long drag, a non-square shape. A 15 degree turn that shifts a shape by a few pixels reads as nothing at video scale.
|
||||
- **Check the frames before attaching.** Pull a contact sheet (`ffmpeg -i before.mp4 -vf "select='not(mod(n\,15))',scale=320:-1,tile=6x3" -frames:v 1 sheet.png`) and look at it. A Before recording that looks identical to After usually means the checkout did not take effect, not that the bug is subtle.
|
||||
- **No burned-in labels.** The `### Before` and `### After` headings already say which is which.
|
||||
- **Keep them short.** A few seconds of setup, the interaction, a beat at the end. Under ten seconds each.
|
||||
|
||||
Attach with `gh pr create` or `gh pr edit` (gh 2.99 or later, run inside the repo, absolute paths). Reference each file in the body as the only content of its paragraph, directly under the heading it illustrates, and gh rewrites the reference to the uploaded asset:
|
||||
|
||||
```bash
|
||||
gh pr edit <number> --body-file body.md --attach /abs/path/before.mp4 --attach /abs/path/after.mp4
|
||||
```
|
||||
|
||||
```md
|
||||
### Before
|
||||
|
||||
<mechanism>
|
||||
|
||||

|
||||
|
||||
### After
|
||||
|
||||
<behavior now>
|
||||
|
||||

|
||||
```
|
||||
|
||||
### Change type
|
||||
|
||||
- Tick exactly one type with `[x]`
|
||||
- Delete unticked items
|
||||
|
||||
### Test plan
|
||||
|
||||
- List manual testing steps if applicable
|
||||
- Remove the numbered list if changes cannot be manually tested
|
||||
- Tick checkboxes for included test types
|
||||
|
||||
### Release notes
|
||||
|
||||
- Write brief notes describing user-facing changes
|
||||
- Use imperative mood: "Add...", "Fix...", "Remove..."
|
||||
- Omit this section entirely for internal work (CI, tooling, tests, etc.) that has no user-facing impact
|
||||
|
||||
## Concepts, examples, and FAQ (only when a reviewer genuinely needs them)
|
||||
|
||||
Gate these on reviewer *need*, not PR size. A large PR is not a reason to add tables — a reviewer who needs shared vocabulary or a usage example to follow the change is. Most large PRs don't clear that bar. When they do, include the relevant sections **above** the standard `### Change type` / `### Test plan` / `### Release notes` block.
|
||||
|
||||
Guardrail: a row only earns its place if it says something the code doesn't. A `Method | Description` table that restates signatures, or a Concepts row for a self-explanatory name, is diff-duplication — cut it. If in doubt, leave it out.
|
||||
|
||||
Pull from this menu, in roughly this order, using only what fits:
|
||||
|
||||
- **Concepts** — a table of the new terms/types the PR introduces, with `Term | Type | Meaning` columns. Use this when readers need a shared vocabulary to follow the rest of the description.
|
||||
- **Module augmentation** — short code blocks showing how consumers extend the new types, when the PR exposes augmentable interfaces.
|
||||
- **Editor API** / **Component props** — `Method | Description` and `Prop | Type | Description` tables for new public surface.
|
||||
- **Per-shape / per-feature breakdown** — tables showing what each affected shape/module returns or accepts under the new system.
|
||||
- **Example** — a realistic, copy-pastable code snippet showing how a default implementation uses the new system, plus a second snippet showing how a consumer would override it.
|
||||
- **FAQ** — anticipated "How do I…?" questions with short code answers. Cover the obvious customization paths a downstream user will reach for first.
|
||||
- **New examples** — bullet list of any new entries added under `apps/examples/src/examples/`, with a one-line description each, so reviewers know where to look for runnable demos.
|
||||
|
||||
Reference: tldraw/tldraw#8410 is a good worked example of all of these sections together.
|
||||
|
||||
These sections come **before** `### Change type`. The standard `### Change type` / `### Test plan` / `### Release notes` / `### API changes` / `### Code changes` blocks still appear at the bottom in the usual order.
|
||||
|
||||
## API changes section
|
||||
|
||||
Include when changes affect `api-report.md`:
|
||||
|
||||
```md
|
||||
### API changes
|
||||
|
||||
- Added `Editor.newMethod()` for X
|
||||
- Breaking! Removed `Editor.oldMethod()`
|
||||
- Changed `Editor.method()` to accept optional `options` parameter
|
||||
```
|
||||
|
||||
## Code changes table
|
||||
|
||||
This table is a deliberate exception to "don't restate the diff": it's a high-level index, not prose pretending to be insight. It lets a reviewer gauge scope at a glance — how much is core code vs tests vs generated vs tooling — and decide where to look. Always include it.
|
||||
|
||||
Create a table that includes net LOC changes for each of the following sections. The sum of all rows must match the total PR diff. Omit rows with no changes.
|
||||
|
||||
- Core code — SDK packages (`packages/`) source, excluding tests and API reports
|
||||
- Tests — unit tests, e2e tests (`*.test.*`, `e2e/`)
|
||||
- Automated files — generated files (e.g. `api-report.api.md`, snapshots)
|
||||
- Documentation — docs site and examples (`apps/docs/`, `apps/examples/`)
|
||||
- Apps — application code (`apps/dotcom/`, `apps/mcp-app/`, `apps/vscode/`, etc.), excluding e2e tests
|
||||
- Templates — starter templates (`templates/`)
|
||||
- Config/tooling — config files, lock files, lint config, CI, build scripts (`.oxlintrc.json`, `yarn.lock`, etc.)
|
||||
|
||||
```md
|
||||
### Code changes
|
||||
|
||||
| Section | LOC change |
|
||||
| --------------- | ---------- |
|
||||
| Core code | +10 / -2 |
|
||||
| Tests | +5 / -0 |
|
||||
| Automated files | +0 / -1 |
|
||||
| Documentation | +2 / -0 |
|
||||
| Apps | +3 / -1 |
|
||||
| Templates | +0 / -0 |
|
||||
| Config/tooling | +1 / -0 |
|
||||
```
|
||||
|
||||
## Related issues
|
||||
|
||||
Search for and link relevant issues that this PR addresses.
|
||||
|
||||
## The comment sweep
|
||||
|
||||
A pre-flight check on the diff, not the description: read the diff's added comments as carefully as its added code, and cut before opening the PR.
|
||||
|
||||
```bash
|
||||
git diff origin/main...HEAD -U0 | grep -nE '^\+\s*(//|/\*|\*)'
|
||||
```
|
||||
|
||||
Judge what that prints against **Comments** in the root `AGENTS.md`. That section is the rule and this one does not restate it — one test is worth carrying in your head as you read:
|
||||
|
||||
> **A good comment names a failure mode, not a mechanism.**
|
||||
|
||||
The mechanism is on screen already. What isn't is what goes wrong without it. A block that only describes what the code does is cuttable in full; a block that says "otherwise X" keeps that sentence and loses the paragraph explaining it twice.
|
||||
|
||||
**Expect length, not category, to be the defect.** Most substantial comment blocks that need editing already name a real force — they are the right kind of comment at two or three times the necessary length. So the usual edit is a trim to the load-bearing lines, not a delete.
|
||||
|
||||
Leave alone, or trim at most: diagrams, enumerated case lists where the list is the specification, provenance (an issue number, "we tried X and it did Y"), and anything carrying an invariant. In `packages/*`, doc comments on the `@public` surface are the API reference — hold them to the same "says something the code doesn't" bar, but they are a deliverable, not overhead.
|
||||
|
||||
Pre-flight rather than post-review because it is cheap now and expensive later. A comment restating its own code reads as padding, costs a review round-trip to remove, and once merged nobody goes back for it. Explaining the change is the PR body's job — the diff is not where that belongs.
|
||||
|
||||
## Human notes (preserve exactly)
|
||||
|
||||
The PR author often writes a personal note at the very top of the description, prefaced with a "person" emoji such as 🅯 or 👨 (or similar). This block is human-written and readers trust it as such.
|
||||
|
||||
- When **updating** an existing PR description, if the body starts with a paragraph/section led by a person emoji, you MUST preserve it **byte-for-byte exactly** at the top — do not rewrite, reflow, reword, retitle, or "improve" it, even slightly.
|
||||
- If the human note is longer than a single paragraph, it will be delimited at the end by a `---` horizontal rule. Preserve everything from the person emoji through (and including) that `---` exactly.
|
||||
- If there is no `---`, the human note is just the leading paragraph led by the person emoji.
|
||||
- Make all your edits to the content **below** the human note (and below the `---` if present).
|
||||
- When **creating** a new PR, do not invent a human note — leave that for the author to add.
|
||||
|
||||
## Important
|
||||
|
||||
- Never include AI attribution unless the PR directly relates to AI tooling
|
||||
- Never use title case for descriptions - use sentence case
|
||||
- Never put yourself as co-author of any commits
|
||||
- Always include an API changes section if the PR has changes to any api-report.md
|
||||
42
skills/write-pr/scripts/example-scenario.mjs
Normal file
42
skills/write-pr/scripts/example-scenario.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Example scenario for record-interaction.mjs: drag a shape, rotate it with the
|
||||
// keyboard shortcut mid-drag, then keep dragging. Copy this file and change the
|
||||
// body to match the interaction your PR affects.
|
||||
//
|
||||
// `page` is the Playwright page. `helpers` gives you:
|
||||
// editor(fn, arg) run `fn(editor, arg)` inside the page against the live editor
|
||||
// drag(from, to) move the mouse in small steps so the motion is visible
|
||||
// pause(ms) wait
|
||||
|
||||
export default async function scenario(page, { editor, drag, pause }) {
|
||||
await editor((editor) => {
|
||||
editor.user.updateUserPreferences({ animationSpeed: 0, colorScheme: 'light' })
|
||||
editor.createShape({
|
||||
type: 'geo',
|
||||
x: 300,
|
||||
y: 240,
|
||||
props: { w: 320, h: 160, geo: 'rectangle', color: 'blue', fill: 'solid' },
|
||||
})
|
||||
editor.selectNone()
|
||||
})
|
||||
await pause(500)
|
||||
|
||||
const start = { x: 460, y: 320 }
|
||||
await page.mouse.move(start.x, start.y)
|
||||
await pause(300)
|
||||
await page.mouse.down()
|
||||
await drag(start, { x: 620, y: 320 })
|
||||
await pause(400)
|
||||
|
||||
// The external change: six rotate-clockwise presses for a full quarter turn
|
||||
for (let i = 0; i < 6; i++) {
|
||||
await page.keyboard.press('Shift+Period')
|
||||
await pause(120)
|
||||
}
|
||||
await pause(800)
|
||||
|
||||
await drag({ x: 620, y: 320 }, { x: 640, y: 460 })
|
||||
await pause(400)
|
||||
await drag({ x: 640, y: 460 }, { x: 820, y: 420 })
|
||||
await pause(500)
|
||||
await page.mouse.up()
|
||||
}
|
||||
139
skills/write-pr/scripts/record-interaction.mjs
Normal file
139
skills/write-pr/scripts/record-interaction.mjs
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env node
|
||||
// Record a scripted canvas interaction as a 16:9 MP4 for a PR's Before/After
|
||||
// sections. Drives the examples app with Playwright, injects a visible cursor
|
||||
// (headless recordings have none), hides the tldraw chrome so the canvas fills
|
||||
// the frame, runs a scenario module, then trims the setup frames and transcodes
|
||||
// with ffmpeg.
|
||||
//
|
||||
// Usage (from the repo root, with `yarn dev` serving localhost:5420):
|
||||
// node skills/write-pr/scripts/record-interaction.mjs <scenario.mjs> <out.mp4> [--url URL] [--keep-ui]
|
||||
//
|
||||
// The scenario module default-exports `async (page, helpers) => {}`. See
|
||||
// example-scenario.mjs next to this file for the available helpers.
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const positional = args.filter((a) => !a.startsWith('--'))
|
||||
const flag = (name) => {
|
||||
const i = args.indexOf(name)
|
||||
return i === -1 ? undefined : args[i + 1]
|
||||
}
|
||||
const [scenarioPath, outPath] = positional
|
||||
if (!scenarioPath || !outPath) {
|
||||
console.error('usage: record-interaction.mjs <scenario.mjs> <out.mp4> [--url URL] [--keep-ui]')
|
||||
process.exit(1)
|
||||
}
|
||||
const url = flag('--url') ?? 'http://localhost:5420/develop'
|
||||
const keepUi = args.includes('--keep-ui')
|
||||
|
||||
const WIDTH = 1280
|
||||
const HEIGHT = 720
|
||||
|
||||
const scenario = (await import(pathToFileURL(path.resolve(scenarioPath)).href)).default
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'record-interaction-'))
|
||||
process.on('exit', () => fs.rmSync(tmpDir, { recursive: true, force: true }))
|
||||
|
||||
let browser
|
||||
try {
|
||||
browser = await chromium.launch()
|
||||
} catch (e) {
|
||||
console.error(
|
||||
'Could not launch Chromium. Install scripts are disabled in this repo, so run `yarn playwright install chromium` once first.'
|
||||
)
|
||||
throw e
|
||||
}
|
||||
|
||||
let rawPath
|
||||
let recordingStartedAt
|
||||
let scenarioStartedAt
|
||||
try {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: WIDTH, height: HEIGHT },
|
||||
deviceScaleFactor: 1,
|
||||
recordVideo: { dir: tmpDir, size: { width: WIDTH, height: HEIGHT } },
|
||||
})
|
||||
recordingStartedAt = Date.now()
|
||||
const page = await context.newPage()
|
||||
await page.goto(url)
|
||||
await page.waitForSelector('.tl-canvas')
|
||||
// Let the first render, fonts, and any HMR reload settle before we start.
|
||||
await page.waitForTimeout(1500)
|
||||
|
||||
await page.evaluate((keepUi) => {
|
||||
if (!keepUi) {
|
||||
const style = document.createElement('style')
|
||||
style.textContent = '.tlui-layout, .tlui-debug-panel { display: none !important }'
|
||||
document.head.appendChild(style)
|
||||
}
|
||||
const cursor = document.createElement('div')
|
||||
cursor.innerHTML =
|
||||
'<svg width="28" height="28" viewBox="0 0 24 24"><path d="M5 3l14 8.5-6.5 1.5-3.5 6z" fill="#000" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>'
|
||||
Object.assign(cursor.style, {
|
||||
position: 'fixed',
|
||||
left: '-100px',
|
||||
top: '-100px',
|
||||
zIndex: 99999,
|
||||
pointerEvents: 'none',
|
||||
})
|
||||
document.body.appendChild(cursor)
|
||||
window.addEventListener(
|
||||
'pointermove',
|
||||
(e) => {
|
||||
cursor.style.left = e.clientX - 4 + 'px'
|
||||
cursor.style.top = e.clientY - 3 + 'px'
|
||||
},
|
||||
true
|
||||
)
|
||||
}, keepUi)
|
||||
|
||||
const helpers = {
|
||||
/** Run `fn(editor)` inside the page. The examples app exposes `window.editor`. */
|
||||
editor: (fn, arg) => page.evaluate(([src, arg]) => new Function('editor', 'arg', `return (${src})(editor, arg)`)(window.editor, arg), [fn.toString(), arg]),
|
||||
/** Move the mouse from one point to another in small steps so the recording shows motion. */
|
||||
drag: async (from, to, { steps = 40, dwellMs = 12 } = {}) => {
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const t = i / steps
|
||||
await page.mouse.move(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t)
|
||||
await page.waitForTimeout(dwellMs)
|
||||
}
|
||||
},
|
||||
pause: (ms) => page.waitForTimeout(ms),
|
||||
}
|
||||
|
||||
// Everything before this point is setup and gets trimmed from the output.
|
||||
scenarioStartedAt = Date.now()
|
||||
await scenario(page, helpers)
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
rawPath = await page.video().path()
|
||||
} finally {
|
||||
// Closing the context is what flushes the recording to disk, so it has to run
|
||||
// even when the scenario throws; otherwise a failed attempt leaks a browser.
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
const trimSeconds = ((scenarioStartedAt - recordingStartedAt) / 1000).toFixed(2)
|
||||
fs.mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true })
|
||||
const ffmpeg = spawnSync(
|
||||
'ffmpeg',
|
||||
[
|
||||
'-v', 'error', '-y',
|
||||
'-ss', trimSeconds,
|
||||
'-i', rawPath,
|
||||
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '22', '-preset', 'slow',
|
||||
'-movflags', '+faststart', '-an',
|
||||
path.resolve(outPath),
|
||||
],
|
||||
{ stdio: 'inherit' }
|
||||
)
|
||||
if (ffmpeg.status !== 0) {
|
||||
console.error('ffmpeg failed; is it installed? (brew install ffmpeg)')
|
||||
process.exit(ffmpeg.status ?? 1)
|
||||
}
|
||||
console.log(`wrote ${path.resolve(outPath)}`)
|
||||
137
skills/write-release-notes/SKILL.md
Normal file
137
skills/write-release-notes/SKILL.md
Normal file
@@ -0,0 +1,137 @@
|
||||
---
|
||||
name: write-release-notes
|
||||
description: Writing release notes articles for tldraw SDK releases. Use when creating new release documentation, drafting release notes from scratch, or reviewing release note quality. Provides guidance on structure, voice, and content for release files in `apps/docs/content/releases/`.
|
||||
---
|
||||
|
||||
# Write release notes
|
||||
|
||||
This skill covers how to write a complete release notes article for a published tldraw SDK release.
|
||||
|
||||
## Location
|
||||
|
||||
All release files live in `apps/docs/content/releases/`.
|
||||
|
||||
| File | Purpose |
|
||||
| ------------ | --------------------------------------------------------- |
|
||||
| `next.mdx` | Accumulates changes for the upcoming release |
|
||||
| `vX.Y.0.mdx` | Published releases (immutable except for patch additions) |
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Identify the release
|
||||
|
||||
Get the version number and find the GitHub release:
|
||||
|
||||
```bash
|
||||
gh release view v4.3.0
|
||||
```
|
||||
|
||||
This shows the release date, tag, and any release notes from GitHub.
|
||||
|
||||
### 2. Find all PRs in the release
|
||||
|
||||
List PRs merged between the previous release and this one:
|
||||
|
||||
```bash
|
||||
# Find commits between releases
|
||||
git log v4.2.0..v4.3.0 --oneline --merges
|
||||
|
||||
# Or use gh to list PRs
|
||||
gh pr list --state merged --base main --search "merged:2024-01-01..2024-02-01"
|
||||
```
|
||||
|
||||
### 3. Fetch PR details
|
||||
|
||||
For each PR, get the full details:
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --json title,body,labels,author,baseRefName
|
||||
```
|
||||
|
||||
Look for:
|
||||
|
||||
- `### Release notes` section in PR body
|
||||
- `### API changes` section in PR body
|
||||
- Labels indicating category (api, bugfix, improvement, etc.)
|
||||
- Whether "breaking" appears in the PR
|
||||
|
||||
**Important:** Only include PRs whose `baseRefName` is `main`. PRs merged into feature branches (e.g. `default-shape-customization`) are not yet released — they will be included when the feature branch itself is merged to main.
|
||||
|
||||
### 4. Find patch releases
|
||||
|
||||
List any patch releases for this minor version:
|
||||
|
||||
```bash
|
||||
gh release list | grep "v4.3"
|
||||
```
|
||||
|
||||
For each patch release, find its PRs:
|
||||
|
||||
```bash
|
||||
git log v4.3.0..v4.3.1 --oneline --merges
|
||||
```
|
||||
|
||||
### 5. Write the article
|
||||
|
||||
Create `apps/docs/content/releases/vX.Y.0.mdx` following the style guide.
|
||||
|
||||
1. Write the frontmatter with version, dates, and keywords
|
||||
2. Write a 1-2 sentence introduction summarizing highlights
|
||||
3. Create featured sections for major features and breaking changes
|
||||
4. List API changes, improvements, and bug fixes
|
||||
5. Add patch release sections if applicable
|
||||
6. Add GitHub release links
|
||||
|
||||
### 6. Write a migration guide for every breaking change
|
||||
|
||||
Every `💥` in the article needs a migration recipe. The `tldraw-migrate` skill drives off these recipes — it intentionally does not duplicate them in its own SKILL.md, because version-specific knowledge belongs next to the breaking change that introduced it. If the recipe is missing, agents and contributors performing the upgrade have to reverse-engineer it from type defs.
|
||||
|
||||
There are two acceptable forms:
|
||||
|
||||
**For breaking changes with their own featured section** (renames, replaced APIs, new patterns), add a `<details><summary>Migration guide</summary>` block under the section. Include before/after code and call out any silent-compile traps (props the typecheck won't reject, signatures with optional new parameters, etc.):
|
||||
|
||||
````mdx
|
||||
### 💥 Custom themes with display values
|
||||
|
||||
[Description of what changed and why]
|
||||
|
||||
<details>
|
||||
<summary>Migration guide</summary>
|
||||
|
||||
`getDefaultColorTheme()` and `DefaultColorThemePalette` have been removed. Use `editor.getCurrentTheme().colors[colorMode]` instead:
|
||||
|
||||
```tsx
|
||||
// Before
|
||||
const theme = getDefaultColorTheme({ isDarkMode })
|
||||
|
||||
// After
|
||||
const theme = editor.getCurrentTheme()
|
||||
const colors = theme.colors[editor.getColorMode()]
|
||||
```
|
||||
|
||||
</details>
|
||||
````
|
||||
|
||||
**For one-line `💥` entries in the API changes list**, the bullet itself must contain the recipe — name the replacement and any caveats inline:
|
||||
|
||||
- ✅ `💥 Replace TLDrawShapeSegment.points with the helper getPointsFromDrawSegment(segment, scaleX, scaleY) so segment points respect the shape's current scale.`
|
||||
- ❌ `💥 Remove TLDrawShapeSegment.points.` (no replacement → reader has to guess)
|
||||
|
||||
A symbol that is removed without a replacement is a documentation bug — find the public alternative or, if there genuinely isn't one, say so explicitly so readers know to drop the call site rather than searching for a rename.
|
||||
|
||||
**Special case — `@public` → `@internal` demotions:** these compile but disappear from public types. They are still breaking changes for consumers who imported the symbol. Treat them like a removal: mark with `💥`, name the public replacement, and explicitly tell readers *not* to reach for module augmentation to re-expose the demoted symbol.
|
||||
|
||||
### 7. Verify
|
||||
|
||||
Check that:
|
||||
|
||||
- All significant PRs are represented
|
||||
- PR links are correct and formatted properly
|
||||
- Community contributors are credited
|
||||
- Breaking changes are marked with 💥
|
||||
- **Every `💥` has either a migration guide block or an inline replacement** (run `grep -nE '💥' apps/docs/content/releases/<file>.mdx` and verify each bullet/section)
|
||||
- Sections are in the correct order
|
||||
|
||||
## References
|
||||
|
||||
- **Style guide**: See `../shared/release-notes-guide.md` for guidance on what a release notes article should contain and how to format it.
|
||||
109
skills/write-tbp/SKILL.md
Normal file
109
skills/write-tbp/SKILL.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: write-tbp
|
||||
description: Writing technical blog posts about tldraw features and implementation details. Use when creating blog content about how tldraw solves interesting problems.
|
||||
---
|
||||
|
||||
# Write technical blog post
|
||||
|
||||
This skill covers how to write technical blog posts about tldraw's implementation details.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Create the workspace
|
||||
|
||||
Create an assets folder for this topic in this skill directory:
|
||||
|
||||
```
|
||||
assets/<topic>/
|
||||
├── research.md # Gathered context and notes
|
||||
└── draft.md # The blog post draft
|
||||
```
|
||||
|
||||
Use a short, kebab-case name for the topic (e.g., `scribbles`, `arrow-routing`, `dash-patterns`).
|
||||
|
||||
### 2. Research the topic
|
||||
|
||||
Use an Explore subagent to gather all relevant information:
|
||||
|
||||
```
|
||||
Task (subagent_type: Explore, thoroughness: very thorough)
|
||||
|
||||
Find all code, documentation, and context related to [TOPIC] in the tldraw codebase.
|
||||
|
||||
Look for:
|
||||
- Implementation files in packages/editor and packages/tldraw
|
||||
- Type definitions in packages/tlschema
|
||||
- Related examples in apps/examples
|
||||
- Any existing documentation in apps/docs/content
|
||||
- Tests that reveal behavior
|
||||
- Comments explaining why things work the way they do
|
||||
|
||||
For each relevant file, note:
|
||||
- What it does
|
||||
- Key functions/classes
|
||||
- Interesting implementation details
|
||||
- Any "why" comments or non-obvious decisions
|
||||
|
||||
Output a comprehensive summary of how [TOPIC] works. This document will be read by another agent. No need to over-optimize for human readability.
|
||||
```
|
||||
|
||||
Save the research output to `assets/<topic>/research.md`.
|
||||
|
||||
### 3. Identify the interesting angle
|
||||
|
||||
Before writing, answer these questions from the research:
|
||||
|
||||
- **What problem does this solve?** Not "what does it do" but "what would go wrong without it?"
|
||||
- **What's surprising or unintuitive?** The obvious approach that doesn't work, or the hidden complexity.
|
||||
- **What's the key insight?** The "aha" that makes the solution work.
|
||||
- **What did we try first?** Any journey or iteration visible in the code or comments.
|
||||
|
||||
If you can't find an interesting angle, the topic may not be suitable for a technical blog post.
|
||||
|
||||
### 4. Write the draft
|
||||
|
||||
Create `assets/<topic>/draft.md` following the blog-guide structure:
|
||||
|
||||
1. **State the problem** — What the system does and what would go wrong without it
|
||||
2. **Show the insight** — The key idea that makes it work
|
||||
3. **Walk through the implementation** — Code and explanation, building complexity
|
||||
4. **Wrap up** — Where it lives, tradeoffs, links to files
|
||||
|
||||
Target 800-1500 words.
|
||||
|
||||
#### Draft plainly
|
||||
|
||||
The draft is raw material for a human editor, not the finished post. Keep it clean and let the content carry it. The editor will add the personality, anecdotes, and punch where they want them; that is much easier to do on top of a plain, correct draft than to strip out of an overwritten one.
|
||||
|
||||
Concretely:
|
||||
|
||||
- Open by saying what the system does and what the post covers. Do not open with a scene, an anecdote, or a hypothetical user ("your train goes into a tunnel..."). Those read as AI filler and the editor will cut them.
|
||||
- Build the story from the mechanism: what problem each piece solves, in the order the pieces depend on each other. Tension comes from the design (a scan can't see deletions; the pruner used to leave a hole), not from prose.
|
||||
- State journeys and bugs flatly. "An earlier version of the pruner did not advance the watermark, so stale clients received diffs with no deletes" is enough. Skip "we learned this the hard way" and similar narration.
|
||||
- No punchline sentences, no chiasmus, no closing zingers. If a sentence exists to sound good rather than to say something, cut it.
|
||||
- One em dash per paragraph at most; usually zero.
|
||||
- Keep opinions to the wrap-up, and keep them short and specific ("we accept a full redownload for clients gone long enough, in exchange for bounded metadata").
|
||||
- Where the human parts belong (opening, provenance, tradeoffs), leave the paragraph plain rather than inventing color. Mention those spots in your handoff.
|
||||
|
||||
### 5. Self-evaluate
|
||||
|
||||
Check the draft against the blog-guide checklist:
|
||||
|
||||
- [ ] **Opening** — Does it state the problem plainly before the solution, without a staged anecdote?
|
||||
- [ ] **Insight** — Is there a clear key idea, and does the structure build toward it?
|
||||
- [ ] **Specificity** — Is this grounded in tldraw's actual implementation?
|
||||
- [ ] **Code** — Do examples build understanding, not just show syntax?
|
||||
- [ ] **Tone** — Plain and direct? No pathos, no punchlines, nothing an editor would recognize as AI cruft?
|
||||
- [ ] **Links** — Points to actual code in the repo?
|
||||
- [ ] **Length** — Appropriate depth for the topic?
|
||||
|
||||
Revise the draft to address any gaps.
|
||||
|
||||
### 6. Output
|
||||
|
||||
Present the final draft to the user for review, and point out where a human editor is most likely to want to add color (usually the opening, any provenance or bug story, and the tradeoffs). The draft remains in `assets/<topic>/draft.md` until the user is satisfied, at which point they can move it to the appropriate location.
|
||||
|
||||
## References
|
||||
|
||||
- **Style guide**: See `../shared/blog-guide.md` for voice, tone, and structure.
|
||||
- **Writing guide**: See `../../VOICE.md` for general writing conventions.
|
||||
5
skills/write-tbp/assets/.gitignore
vendored
Normal file
5
skills/write-tbp/assets/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Ignore all files in this folder
|
||||
*
|
||||
|
||||
# ...except for this .gitignore
|
||||
!.gitignore
|
||||
63
skills/write-unit-tests/SKILL.md
Normal file
63
skills/write-unit-tests/SKILL.md
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: write-unit-tests
|
||||
description: Writing unit and integration tests for the tldraw SDK. Use when creating new tests, adding test coverage, or fixing failing tests in packages/editor or packages/tldraw. Covers Vitest patterns, TestEditor usage, and test file organization.
|
||||
---
|
||||
|
||||
# Writing tests
|
||||
|
||||
Unit and integration tests use Vitest and run from workspace directories, not the repo root.
|
||||
|
||||
Read a neighboring test before writing a new one — the existing suites are the specification for how we test, and they stay current in a way prose can't. Good starting points:
|
||||
|
||||
- `packages/tldraw/src/test/SelectTool.test.ts` — tool state machine assertions
|
||||
- `packages/tldraw/src/test/resizing.test.ts` — pointer-driven interaction with handles
|
||||
- `packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts` — shape util plus bindings
|
||||
- `packages/editor/src/lib/editor/managers/ClickManager/ClickManager.test.ts` — a UI-free manager
|
||||
- `packages/editor/src/lib/primitives/Vec.test.ts` — a plain primitive
|
||||
|
||||
For the available `TestEditor` methods, read the class itself rather than a list here: `packages/tldraw/src/test/TestEditor.ts`.
|
||||
|
||||
## Which workspace
|
||||
|
||||
- `packages/editor` — core primitives, geometry, managers, base editor behavior that must not depend on default shapes or UI.
|
||||
- `packages/tldraw` — anything needing default shapes or tools, which is most integration tests.
|
||||
|
||||
Each package has its own `TestEditor`, and they are not interchangeable: `packages/editor/src/lib/test/TestEditor.ts` has no default shapes or tools, `packages/tldraw/src/test/TestEditor.ts` wires up the full SDK. Import from the package you're testing in.
|
||||
|
||||
```bash
|
||||
cd packages/tldraw && yarn test run
|
||||
cd packages/tldraw && yarn test run --grep "SelectTool"
|
||||
cd packages/tldraw && yarn test # watch mode
|
||||
```
|
||||
|
||||
## Placement
|
||||
|
||||
Unit tests sit next to the file they cover (`Vec.ts` → `Vec.test.ts`). Cross-cutting integration tests live in `packages/tldraw/src/test/`. Shape and tool tests sit with the implementation, not in `src/test/`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
These are the things reading an existing test won't tell you.
|
||||
|
||||
**Wheel and pinch events are batched.** `dispatch()` alone won't apply them — emit a tick to flush:
|
||||
|
||||
```typescript
|
||||
editor.dispatch(wheelEvent)
|
||||
editor.emit('tick', 16)
|
||||
```
|
||||
|
||||
See `packages/editor/src/lib/editor/Editor.test.ts` for the full pattern.
|
||||
|
||||
**`toCloselyMatchObject` is ours, not Vitest's.** Use it instead of `toMatchObject` whenever floating-point geometry is involved, or tests fail on rounding noise. It takes an optional `roundToNearest`. Defined in `packages/tldraw/src/test/TestEditor.ts`.
|
||||
|
||||
**Animation-dependent code needs the rAF stub.** `vi.useFakeTimers()` alone isn't enough, because requestAnimationFrame isn't driven by the fake clock. Tests that animate replace it at module scope — see the top of `packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts`.
|
||||
|
||||
**Dispose the editor in `afterEach`.** `editor?.dispose()` releases the reactive subscriptions and timers the editor holds; without it they leak across tests in the same file. Suites that build up shapes also tend to clear them in `beforeEach` so each test starts from a known page.
|
||||
|
||||
**Always `mockRestore()` a `vi.spyOn` on the editor.** Editor instances outlive individual assertions within a suite, so an unrestored spy silently changes later tests.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use `createShapeId()` for shape IDs so they're stable and typed.
|
||||
- Prefer comparing whole objects over field-by-field assertions when it gives a clearer failure.
|
||||
- Use `editor.expectToBeIn('select.idle')` for state machine assertions rather than reaching into internals.
|
||||
- `@ts-expect-error` is the way to assert that invalid props are rejected at the type level.
|
||||
Reference in New Issue
Block a user