Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f0d3c08
The prisma-8 skill splits into prisma-orm-core-concepts and prisma-or…
tylerhogarth Aug 26, 2026
a3af533
Journey 07 metadata lists runtime and build; DEVELOPING counts two SK…
tylerhogarth Aug 27, 2026
9c7d6a4
Review: correct upgrade-instruction and versioning-doc details
tylerhogarth Aug 27, 2026
a386b49
Review: harden the upgrade codemods against edge-case inputs
tylerhogarth Aug 27, 2026
f006861
The core-concepts skill teaches the model, not the CLI surface
tylerhogarth Aug 27, 2026
641da71
Concurrent packs no longer race the skill staging
tylerhogarth Aug 28, 2026
18a158e
The TML-2624 coverage waiver renews for 30 more days
tylerhogarth Aug 28, 2026
9a3b49f
A warm skill sync leaves the packed tree untouched
tylerhogarth Aug 28, 2026
4b7ee50
Merge remote-tracking branch 'origin/main' into th/split-prisma-8-skills
tylerhogarth Aug 28, 2026
02832ca
Sync work directories are invocation-unique, not just pid-unique
tylerhogarth Aug 28, 2026
87e081a
Merge remote-tracking branch 'origin/main' into th/split-prisma-8-skills
tylerhogarth Sep 1, 2026
5707b0d
The skills describe the published surface: real package names, single…
tylerhogarth Sep 1, 2026
0bf5f5f
The migrations skill teaches the 0.14+ authoring surface: method ops,…
tylerhogarth Sep 1, 2026
007411f
The skills describe the rc.8 surface that ships: DOMAIN.NAME codes, d…
ankur-arch Sep 7, 2026
3b13bae
Merge branch 'main' into th/split-prisma-8-skills
ankur-arch Sep 7, 2026
88a337b
Merge remote-tracking branch 'origin/main' into th/split-prisma-8-skills
tylerhogarth Sep 8, 2026
7079b65
Simplify the skill-sync build script; serialise concurrent packs in t…
tylerhogarth Sep 9, 2026
8993894
Merge remote-tracking branch 'origin/main' into th/split-prisma-8-skills
tylerhogarth Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ Just describe what you want. For example:

> *"Add a `posts` model with a relation to `users`, then write a query that loads each user's three most recent posts."*

The agent loads the `prisma-8` skill, opens its contract and queries references, then drives the change end-to-end.
The agent loads the `prisma-orm-core-concepts` skill, opens its contract and queries references, then drives the change end-to-end.

For the full catalogue and what each skill covers, see [`skills/README.md`](./skills/README.md).

## Found a bug, missing a feature, or have a question for the team?

Ask your agent. The `prisma-8` skill's feedback flow drafts a structured GitHub issue or hands you a Prisma Discord link for live Q&A. You can review and confirm before anything is submitted.
Ask your agent. The `prisma-orm-core-concepts` skill's feedback flow drafts a structured GitHub issue or hands you a Prisma Discord link for live Q&A. You can review and confirm before anything is submitted.

## For extension authors

Expand Down
2 changes: 1 addition & 1 deletion docs/oss/versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Every workspace package — publishable, private, the workspace root, and exampl

This invariant has consequences that ecosystem participants need to plan for:

- **Agent skills, the upgrade instructions, and any other tooling we ship alongside the framework version in lockstep with it.** A skill installed at the same time as `@internal/postgres@0.8.0` is a `0.8.0` skill and reasons about a `0.8.0` contract. There is no separate skill-version axis to track. This is now physical rather than conventional: the `prisma-8` skill ships inside the `@prisma/orm-postgres`, `@prisma/orm-sqlite` and `@prisma/orm-mongo` tarballs (copied in by each package's `prepack`), and `scripts/set-version.ts` stamps its `metadata.library_version` frontmatter with the version being published. Getting the skill and getting the code are one install, and a consumer can compare the stamp on its synced copy against its installed packages.
- **Agent skills, the upgrade instructions, and any other tooling we ship alongside the framework version in lockstep with it.** A skill installed at the same time as `@prisma/orm-postgres@0.8.0` is a `0.8.0` skill and reasons about a `0.8.0` contract. There is no separate skill-version axis to track. This is now physical rather than conventional: the `prisma-orm-core-concepts` and `prisma-orm-migrations` skills ship inside the `@prisma/orm-postgres`, `@prisma/orm-sqlite` and `@prisma/orm-mongo` tarballs (copied in by each package's `prepack`), and `scripts/set-version.ts` stamps each skill's `metadata.library_version` frontmatter with the version being published. Getting the skill and getting the code are one install, and a consumer can compare the stamp on its synced copy against its installed packages.
- **Extension authors that depend on internal framework packages must pin those dependencies to the framework version their consumers will use.** If your extension depends on `@internal/sql-core` (an internal framework package), publish each version of your extension targeting one specific Prisma Next minor and pin to it exactly (`"@internal/sql-core": "0.8.0"`, not `"^0.8.0"`). Internal packages do not promise inter-minor compatibility — `0.8.x` and `0.9.x` may have incompatible internals even when the user-visible surface looks similar. The extension's published version range communicates which framework minor it targets.
- **Internal packages are never published, but they still version in lockstep** so a contributor cloning the repo at any commit sees one consistent answer to "what version is this code?" The `private: true` flag means `pnpm publish` skips them.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"lint:docs": "node scripts/validate-package-readmes.mjs",
"lint:manifests": "node scripts/validate-package-manifests.mjs && node scripts/validate-typescript-peer.mjs",
"lint:workflows": "node scripts/lint-workflow-triggers.mjs",
"test:scripts": "node --test scripts/coverage-config.test.mjs scripts/coverage-report.test.mjs scripts/lint-workflow-triggers.test.mjs scripts/validate-skills.test.mjs scripts/determine-version-utils.test.ts scripts/check-upgrade-coverage.test.mjs scripts/check-release-notes.test.mjs scripts/set-version-utils.test.ts scripts/check-publish-deps.test.mjs scripts/check-conformance.test.mjs scripts/check-publish-deps-pn-pins.test.mjs scripts/check-publish-deps-declarations.test.mjs scripts/validate-package-manifests.test.mjs scripts/validate-package-readmes.test.mjs scripts/publish-packages-utils.test.mjs scripts/check-clean-tree.test.mjs scripts/lint-casts.test.mjs scripts/lint-throws.test.mjs scripts/list-error-codes.test.mjs scripts/lint-framework-vocabulary.test.mjs scripts/lint-single-import-root.test.mjs scripts/lint-legacy-name.test.mjs scripts/lint-consumer-internal-imports.test.mjs scripts/sync-agent-rules.test.mjs scripts/validate-typescript-peer.test.mjs scripts/run-logged.test.mjs scripts/migrate-migrations-layout.test.mjs skills-contrib/review-fetch-phase/scripts/guard-review-artifacts-ignored.test.mjs skills-contrib/review-fetch-phase/scripts/render-review-state.test.mjs skills-contrib/review-triage-phase/scripts/render-review-actions.test.mjs",
"test:scripts": "node --test scripts/coverage-config.test.mjs scripts/sync-package-skills.test.mjs scripts/coverage-report.test.mjs scripts/lint-workflow-triggers.test.mjs scripts/validate-skills.test.mjs scripts/determine-version-utils.test.ts scripts/check-upgrade-coverage.test.mjs scripts/check-release-notes.test.mjs scripts/set-version-utils.test.ts scripts/check-publish-deps.test.mjs scripts/check-conformance.test.mjs scripts/check-publish-deps-pn-pins.test.mjs scripts/check-publish-deps-declarations.test.mjs scripts/validate-package-manifests.test.mjs scripts/validate-package-readmes.test.mjs scripts/publish-packages-utils.test.mjs scripts/check-clean-tree.test.mjs scripts/lint-casts.test.mjs scripts/lint-throws.test.mjs scripts/list-error-codes.test.mjs scripts/lint-framework-vocabulary.test.mjs scripts/lint-single-import-root.test.mjs scripts/lint-legacy-name.test.mjs scripts/lint-consumer-internal-imports.test.mjs scripts/sync-agent-rules.test.mjs scripts/validate-typescript-peer.test.mjs scripts/run-logged.test.mjs scripts/migrate-migrations-layout.test.mjs skills-contrib/review-fetch-phase/scripts/guard-review-artifacts-ignored.test.mjs skills-contrib/review-fetch-phase/scripts/render-review-state.test.mjs skills-contrib/review-triage-phase/scripts/render-review-actions.test.mjs",
"bump-version": "node scripts/bump-version.ts",
"check:publish-deps": "node scripts/check-publish-deps.mjs",
"check:conformance": "node scripts/check-conformance.mjs",
Expand Down
5 changes: 4 additions & 1 deletion packages/0-config/tsdown/shell-testkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { execFileSync } from 'node:child_process';
import { cpSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { publicShells } from '@internal/publish-surface/shells';
import { withPackLock } from '@internal/publish-surface/test/pack-lock';
import { init as initLexer, parse as parseModule } from 'es-module-lexer';

/** A tarball-install smoke-test failure with the offending command output attached. */
Expand Down Expand Up @@ -47,7 +48,9 @@ function manifestName(packageDir: string, manifest: Record<string, unknown>): st
export function packShell(shellDir: string, outDir: string): PackedShell {
const name = manifestName(shellDir, readManifest(shellDir));
const tarball = join(outDir, `${name.replaceAll(/[@/]/g, '-').replace(/^-/, '')}.tgz`);
execFileSync('pnpm', ['pack', '--out', tarball], { cwd: shellDir, stdio: 'pipe' });
withPackLock(name, () =>
execFileSync('pnpm', ['pack', '--out', tarball], { cwd: shellDir, stdio: 'pipe' }),
);
return { name, tarball };
}

Expand Down
6 changes: 3 additions & 3 deletions packages/0-shared/extension-author-tools/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# @internal/extension-author-tools

CLI tools that pair with the extension-upgrade branch of the [`prisma-8`](../../../skills/prisma-8/references/upgrade-extension.md) agent skill. Today this package ships one tool; future tools for extension authors using the upgrade-skill flow will land here.
CLI tools that pair with the extension-upgrade branch of the [`prisma-orm-core-concepts`](../../../skills/prisma-orm-core-concepts/references/upgrade-extension.md) agent skill. Today this package ships one tool; future tools for extension authors using the upgrade-skill flow will land here.

The agent-readable upgrade procedure itself (the flow in [`references/upgrade-extension.md`](../../../skills/prisma-8/references/upgrade-extension.md) and the `upgrades/<from>-to-<to>/instructions.md` set under [`upgrading/extension/`](../../../skills/prisma-8/upgrading/extension/)) ships inside the `@prisma/orm-*` tarballs. This package is the npm-published companion that supplies the CI bin the skill drives.
The agent-readable upgrade procedure itself (the flow in [`references/upgrade-extension.md`](../../../skills/prisma-orm-core-concepts/references/upgrade-extension.md) and the `upgrades/<from>-to-<to>/instructions.md` set under [`upgrading/extension/`](../../../skills/prisma-orm-core-concepts/upgrading/extension/)) ships inside the `@prisma/orm-*` tarballs. This package is the npm-published companion that supplies the CI bin the skill drives.

## Installation

Expand All @@ -18,7 +18,7 @@ npm install --save-dev @internal/extension-author-tools

CI guard for extension packages. Asserts that every `@internal/*` entry under the package's `peerDependencies` (and, optionally, `dependencies`) is pinned to an exact version, not a range.

This is the invariant the [extension-upgrade flow](../../../skills/prisma-8/references/upgrade-extension.md) relies on at upgrade time: extension authors pin every `@internal/*` peer to a single exact version per release of their extension, so the skill can mechanically advance both the framework deps and the extension's published version in lockstep.
This is the invariant the [extension-upgrade flow](../../../skills/prisma-orm-core-concepts/references/upgrade-extension.md) relies on at upgrade time: extension authors pin every `@internal/*` peer to a single exact version per release of their extension, so the skill can mechanically advance both the framework deps and the extension's published version in lockstep.

Run from the extension's repository root:

Expand Down
1 change: 1 addition & 0 deletions packages/0-shared/publish-surface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"exports": {
"./import-roots": "./dist/import-roots.mjs",
"./shells": "./dist/shells.mjs",
"./test/pack-lock": "./dist/test/pack-lock.mjs",
"./package.json": "./package.json"
},
"engines": {
Expand Down
60 changes: 60 additions & 0 deletions packages/0-shared/publish-surface/src/test/pack-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { mkdirSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const PACK_LOCK_STALE_MS = 120_000;
const PACK_LOCK_POLL_MS = 50;

/** Block the current thread for `ms` without spinning the CPU. */
function sleepSync(ms: number): void {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}

/**
* Serialise `pnpm pack` of one package across the whole test run.
*
* Packing a facade package fires its `prepack` (`scripts/sync-package-skills.ts`),
* which rewrites that package's gitignored `skills/` tree. Tarball suites in
* separate vitest projects pack the same package at once, so without this lock
* one pack's tar phase can read the tree while another's prepack is mid-rewrite.
* The sync script assumes a single writer per package (true of any real
* publish, where each package is packed once); the concurrency the tests
* manufacture is serialised here, keyed by package so unrelated packages still
* pack in parallel. The lock is a directory in the OS temp dir, so it holds
* across the separate processes vitest forks per project.
*
* This lives in `publish-surface` rather than the shell-testkit that uses it so
* both writers (the tarball suites via shell-testkit, and this package's own
* `package-skills` suite) can share one lock without a build cycle.
*/
export function withPackLock<T>(packageName: string, run: () => T): T {
const slug = packageName.replaceAll(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, '');
const lockDir = join(tmpdir(), `prisma-pack-lock-${slug}`);
const deadline = Date.now() + PACK_LOCK_STALE_MS;
for (;;) {
try {
mkdirSync(lockDir);
break;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
// Reclaim a lock a crashed run left behind.
let heldSince = Date.now();
try {
heldSince = statSync(lockDir).mtimeMs;
} catch {
continue;
}
if (Date.now() - heldSince > PACK_LOCK_STALE_MS) {
rmSync(lockDir, { recursive: true, force: true });
continue;
}
if (Date.now() > deadline) throw new Error(`timed out waiting to pack ${packageName}`);
sleepSync(PACK_LOCK_POLL_MS);
}
}
try {
return run();
} finally {
rmSync(lockDir, { recursive: true, force: true });
}
}
79 changes: 44 additions & 35 deletions packages/0-shared/publish-surface/test/package-skills.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* The `prisma-8` agent skill ships inside the tarball of every package an
* application depends on directly, so the skill a user has always describes
* The Prisma Next agent skills (`prisma-orm-core-concepts`,
* `prisma-orm-migrations`) ship inside the tarball of every package an
* application depends on directly, so the skills a user has always describe
* the version they installed.
*
* That claim is only worth as much as the artifact that proves it, and every
Expand All @@ -23,10 +24,11 @@ import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterAll, describe, expect, it } from 'vitest';
import { publicShells, type ShellName } from '../src/shells';
import { withPackLock } from '../src/test/pack-lock';

const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', '..');
const facades: ShellName[] = ['@prisma/orm-postgres', '@prisma/orm-sqlite', '@prisma/orm-mongo'];
const SKILL_NAME = 'prisma-8';
const SKILL_NAMES = ['prisma-orm-core-concepts', 'prisma-orm-migrations'] as const;

interface Manifest {
readonly version: string;
Expand Down Expand Up @@ -81,22 +83,27 @@ afterAll(() => {
function packAndUnpack(facade: ShellName): string {
const work = mkdtempSync(join(tmpdir(), 'skill-packaging-'));
workspaces.push(work);
// Only `prepack` may supply what the tarball carries.
rmSync(join(packageDir(facade), 'skills'), { recursive: true, force: true });
execFileSync('pnpm', ['pack', '--pack-destination', work], {
cwd: packageDir(facade),
stdio: ['ignore', 'ignore', 'pipe'],
// Deleting `skills/` then packing mutates a directory shared with the other
// tarball suites, so take the same per-package lock `packShell` uses to keep
// one pack's tar phase from reading the tree mid-rewrite.
const tarball = withPackLock(facade, () => {
// Only `prepack` may supply what the tarball carries.
rmSync(join(packageDir(facade), 'skills'), { recursive: true, force: true });
execFileSync('pnpm', ['pack', '--pack-destination', work], {
cwd: packageDir(facade),
stdio: ['ignore', 'ignore', 'pipe'],
});
return readdirSync(work).find((file) => file.endsWith('.tgz'));
});
const tarball = readdirSync(work).find((file) => file.endsWith('.tgz'));
if (tarball === undefined) throw new Error(`pnpm pack produced no tarball for ${facade}`);
execFileSync('tar', ['xzf', tarball], { cwd: work });
return join(work, 'package');
}

describe('the skill source in the repository', () => {
it('carries the stamp the version sweep maintains', () => {
describe('the skill sources in the repository', () => {
it.each(SKILL_NAMES)('%s carries the stamp the version sweep maintains', (skillName) => {
const rootVersion = manifestAt(repoRoot).version;
const source = readFileSync(join(repoRoot, 'skills', SKILL_NAME, 'SKILL.md'), 'utf8');
const source = readFileSync(join(repoRoot, 'skills', skillName, 'SKILL.md'), 'utf8');
expect(metadataValue(source, 'library_version')).toBe(rootVersion);
});
});
Expand All @@ -112,30 +119,32 @@ describe.each(facades)('%s', (facade) => {
);
});

it('carries the whole skill tree in its tarball, stamped with what shipped it', () => {
it('carries every skill tree in its tarball, stamped with what shipped it', () => {
const packedRoot = packAndUnpack(facade);
const packedSkillDir = join(packedRoot, 'skills', SKILL_NAME);

expect(
existsSync(join(packedSkillDir, 'SKILL.md')),
`the ${facade} tarball has no skills/${SKILL_NAME}/SKILL.md`,
).toBe(true);

const packedSkill = readFileSync(join(packedSkillDir, 'SKILL.md'), 'utf8');
expect(metadataValue(packedSkill, 'library')).toBe(facade);
expect(metadataValue(packedSkill, 'library_version')).toBe(manifestAt(packedRoot).version);

// The tarball and the repository's tracked tree must serve the same
// instructions: the only difference is the package each copy names.
const sourceDir = join(repoRoot, 'skills', SKILL_NAME);
expect(filesUnder(packedSkillDir)).toEqual(filesUnder(sourceDir));
for (const file of filesUnder(sourceDir)) {
if (file === 'SKILL.md') continue;
expect(readFileSync(join(packedSkillDir, file), 'utf8')).toBe(
readFileSync(join(sourceDir, file), 'utf8'),
);
for (const skillName of SKILL_NAMES) {
const packedSkillDir = join(packedRoot, 'skills', skillName);

expect(
existsSync(join(packedSkillDir, 'SKILL.md')),
`the ${facade} tarball has no skills/${skillName}/SKILL.md`,
).toBe(true);

const packedSkill = readFileSync(join(packedSkillDir, 'SKILL.md'), 'utf8');
expect(metadataValue(packedSkill, 'library')).toBe(facade);
expect(metadataValue(packedSkill, 'library_version')).toBe(manifestAt(packedRoot).version);

// The tarball and the repository's tracked tree must serve the same
// instructions: the only difference is the package each copy names.
const sourceDir = join(repoRoot, 'skills', skillName);
expect(filesUnder(packedSkillDir)).toEqual(filesUnder(sourceDir));
for (const file of filesUnder(sourceDir)) {
if (file === 'SKILL.md') continue;
expect(readFileSync(join(packedSkillDir, file), 'utf8')).toBe(
readFileSync(join(sourceDir, file), 'utf8'),
);
}
const sourceSkill = readFileSync(join(sourceDir, 'SKILL.md'), 'utf8');
expect(packedSkill).toBe(sourceSkill.replace(/^(\s+)library:.*$/m, `$1library: '${facade}'`));
}
const sourceSkill = readFileSync(join(sourceDir, 'SKILL.md'), 'utf8');
expect(packedSkill).toBe(sourceSkill.replace(/^(\s+)library:.*$/m, `$1library: '${facade}'`));
}, 60_000);
});
2 changes: 1 addition & 1 deletion packages/0-shared/publish-surface/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { defineConfig } from 'tsdown';

export default defineConfig({
entry: ['src/shells.ts', 'src/import-roots.ts'],
entry: ['src/shells.ts', 'src/import-roots.ts', 'src/test/pack-lock.ts'],
dts: { enabled: true, sourcemap: true },
exports: { enabled: 'local-only' },
skipNodeModulesBundle: true,
Expand Down
Loading
Loading