Skip to content
114 changes: 97 additions & 17 deletions .agents/skills/qv-sdk-changelog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@ If the user doesn't specify, ask which SDK pod package they want to generate a c

Package slugs match git tags (`sdk`, `inference`, `cli`, `ai-sdk-provider`, `opencode-plugin`, `openclaw-plugin`, …). Directory resolution (including `plugins/*`) is in `scripts/sdk/package-paths.cjs`.

**`sdk` and `inference` are separate releases that share a major and minor.** Moving to a
new major.minor is two changelogs and two releases, engine first: `--package=inference`
for `release-inference-<x.y.z>`, then `--package=sdk` for `release-sdk-<x.y.z>`. Engine
commits are scanned into the SDK changelog too (`CHANGELOG_EXTRA_SCAN_DIRS` in
`scripts/sdk/package-paths.cjs`), so the engine's user-facing changes appear in the SDK
release notes as well — the SDK is where consumers read them. A patch on either side is
one release of its own.
**`sdk` and `inference` are lockstep on major.minor.** Two changelogs, two
releases, engine first: `--package=inference` for `release-inference-<x.y.z>`,
then `--package=sdk` for `release-sdk-<x.y.z>`. Both notes for the same `x.y.z`
share one **display floor**: the last lockstep version already shipped (patch or
`.0`). `--base-commit` is that version's backmerge on main so the new notes do
not repeat it. Use the same floor on both packages; splitting them (inference
from the previous `.0`, SDK from a later patch) duplicates patch notes and
makes the files disagree.

`--base-commit` is only the generate range, not the full set of notes. See
Step 2 when that floor is a patch.

The SDK is the consumer-facing full notes. `--package=sdk` also scans
`packages/inference` (`CHANGELOG_EXTRA_SCAN_DIRS` in
`scripts/sdk/package-paths.cjs`). The inference changelog is the engine-only
slice of that same set. A patch on either side is one release of its own.

**Working branch (when cutting from a release line):** use
`chore/<pkg>-<x.y.z>-changelog` (e.g. `chore/sdk-0.17.0-changelog`). Do **not**
Expand Down Expand Up @@ -68,10 +77,45 @@ Run `git tag --list "<package>-v*" --sort=-v:refname` to check for existing vers
- **Patch release** (version ends in non-zero patch, e.g. `0.8.4`): uses the absolute latest tag as base (e.g. `sdk-v0.8.3`)
- If no tags: ask the user for `--base-commit` and `--base-version` (migration scenario)

**Why this matters:** patches ship on separate release branches and get backmerged into main.
Using the latest patch tag as base for a minor release would miss all PRs that landed on main
between the previous minor release and the last backmerge. The correct base for a minor release
is the previous minor's `.0` tag.
**Why this matters:** patches ship on a `release-<pkg>-*` branch and backmerge
into main. The generator's range is `git log <base>..HEAD`. A commit that landed
on **main** while that patch branch was open, and was never on the patch branch,
is already an ancestor of the backmerge, so it is invisible to that range. It
also never appeared in the patch notes. The next minor then ships the code
with no changelog line unless those PRs are unioned in.

**Standalone minor** (cli, plugins, anything not lockstep with another package):
`--base-commit` is the previous `.0`. That range still includes main-only work
in the patch window, and it will also re-list the patch. Accept the overlap or
drop PR numbers already in `changelog/<patch>/`.

**Lockstep `sdk` + `inference`:** both packages use the **same** `--base-commit`
/ `--base-version` — the last lockstep display floor. When that floor is a
`.0`, generate is enough. When it is a **patch**, the notes are:

1. Generator output from the patch backmerge (work after the floor).
2. Plus main-only work between the previous lockstep `.0` backmerge and that
patch backmerge: first-parent merges touching `packages/inference` or
`packages/sdk`, minus `[skiplog]`, minus PR numbers already in
`packages/sdk/changelog/<patch>/CHANGELOG.md`.

Hand-add (2) to **both** lockstep changelogs (`CHANGELOG.md`, `CHANGELOG_LLM.md`,
and `breaking.md` / `models.md` / `api.md` when the PR tag requires them). Do
not re-run the generator over the hand edits; `--update-root-changelog` only.

```bash
git log --first-parent --format='%s' <prev-lockstep-.0-backmerge>..<patch-backmerge>
# keep subjects whose merge diff touches packages/inference or packages/sdk
```

**`inference-v*` / historical `sdk-v*` tags are often not ancestors of `main`.**
Inference tags used to live on `release-sdk-*`; after split-publish they live on
`release-inference-*`. Auto-detected tags then fail `merge-base --is-ancestor`.
Pass `--base-commit=<sha> --base-version=<x.y.z>` instead of relying on the tag.

**Nested worktrees:** unset stale `GIT_DIR` / `GIT_WORK_TREE` from a parent Cursor
session before any git or changelog command (`unset GIT_DIR GIT_WORK_TREE`).
Otherwise the generator runs against the parent repo.

### Step 3: Generate Raw Changelog

Expand Down Expand Up @@ -148,17 +192,27 @@ prefers it over `CHANGELOG.md`). Easiest way: re-run the script from Step 3 —
**Format the generated markdown (mandatory).** `CHANGELOG_LLM.md` is authored by
hand here, so it is the file most likely to carry markdown formatting issues that a
committed-file format check would later reject. Every SDK pod package uses prettier
(`format` = `prettier --check .`, `format:fix` = `prettier --write .`). Run the check
scoped to the changelog output so any issue surfaces now:
(`format` = `prettier --check .`, `format:fix` = `prettier --write .`) with
`.prettierrc` set to `"prettier-config-holepunch"`. **Never `--no-config`.** CI
(`[inference] format`, `[sdk] format`, …) loads holepunch; `--no-config` or a
different parser (quote style, trailing commas) is the usual red we hit.

`bunx prettier` from a worktree **without** `node_modules` fails with
`Cannot find package 'prettier-config-holepunch'` and then either skips the
check or formats with a fallback that CI will reject. Install first, then check
the same way CI does:

```bash
cd packages/<package>
# if this worktree has no node_modules:
bun install
bunx prettier --check "changelog/**/*.md" "CHANGELOG.md"
# or, matching CI: bun run format
```

If it reports problems, fix them — `bunx prettier --write` on the same paths, or hand-edit —
and re-run the check until it passes clean. Do this before moving on so the release commit
carries only prettier-clean markdown.
If it reports problems, fix them — `bunx prettier --write` on the same paths, or
`bun run format:fix` — and re-run the check until it passes clean. Do this before
moving on so the release commit carries only prettier-clean markdown.

**Downstream rendering note:** the docs site reads `CHANGELOG_LLM.md`
**verbatim** and inlines it under a `### @qvac/<pkg>` subsection of the
Expand Down Expand Up @@ -363,6 +417,28 @@ Examples:
- `sdk-v0.8.1` (patch — used as base for next patch release)
- `rag-v2.0.0`

## Repeated footguns

These have gone red on more than one SDK-pod changelog PR. Fix them before
push, and keep this list to things that are cheap to prevent:

- **Prettier is holepunch, not stock.** `.prettierrc` is `"prettier-config-holepunch"`.
Never `--no-config`. Never `bunx prettier` until `packages/<pkg>/node_modules`
(or a linked install) can resolve that package. Quote style and trailing commas
on `CHANGELOG_LLM.md` are the usual fail.
- **Lockstep display floor is not the whole range.** Same `--base-commit` on
`sdk` and `inference` so already-shipped notes are not repeated. When that
floor is a patch, union in the Step 2 main-only window (previous lockstep
`.0` backmerge → patch backmerge, minus skiplog, minus the patch notes).
SDK is the consumer set; inference is the engine slice.
- **`inference-v*` is often not on `main`.** Use the backmerge SHA, not the tag.
- **Nested worktrees inherit `GIT_DIR`.** `unset GIT_DIR GIT_WORK_TREE` before
generate/commit/cherry-pick, or you operate on the parent repo.
- **Push the org remote** (`upstream` when that is `tetherto/qvac`), not the
contributor fork. `git push` with no remote follows `origin`.
- **Do not skip SDK Pod Checks.** Workspace red vs published red is a real
signal; `[skip-sdk-pod-checks]` is not the changelog fix.

## Quality Checklist

Before completing:
Expand All @@ -371,10 +447,13 @@ Before completing:
- [ ] Working head (if branched for the release PR) is `chore/<pkg>-<x.y.z>-changelog`, not `release-*`
- [ ] Clone is not shallow (`git rev-parse --is-shallow-repository` → `false`)
- [ ] Base reference resolved (tag or `--base-commit`) and is an ancestor of `HEAD`
- [ ] For lockstep `sdk` + `inference` at the same major.minor: both used the **same** `--base-commit` / `--base-version` (last lockstep display floor); SDK changelog includes the engine slice
- [ ] When that floor is a patch: main-only window unioned in (previous lockstep `.0` backmerge → patch backmerge, minus skiplog, minus the patch notes) on both changelogs
- [ ] `GIT_DIR` / `GIT_WORK_TREE` unset (or pointed at this worktree) so generate/git did not run in a parent repo
- [ ] PRs scoped to package path only
- [ ] Changelog files written to correct version directory
- [ ] CHANGELOG_LLM.md generated (mandatory) and follows format guide
- [ ] Generated markdown is prettier-clean (`prettier --check` on the changelog output passes)
- [ ] Generated markdown is prettier-clean with **prettier-config-holepunch** resolved (`bun install` in `packages/<pkg>` if needed; never `--no-config`)
- [ ] announcement-post.txt generated (mandatory, gitignored)
- [ ] NOTICE file updated for the target package
- [ ] When `--package=sdk`: `qv-sdk-inference-version` run (engine version published, sdk version and `@qvac/inference` range sharing a major.minor, sdk-python regenerated), python `generate.py --check` passing
Expand All @@ -383,6 +462,7 @@ Before completing:
- [ ] Versions sorted in descending semver order
- [ ] No duplicated versions
- [ ] Root file is deterministic (fully regenerated)
- [ ] Org remote (`upstream` when that is tetherto/qvac) is the push target, not the fork

## References

Expand Down
Loading