Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Every row below is one installable skill (`skills/<name>/SKILL.md`, which is wha
| `executing-tickets-with-subagents` | Orchestrate bundled tickets or explicit away/keep-going work through a durable ledger, with one mutation owner per task and a bounded unattended worker pool |
| `sync-agent-setups` | Manually preview and sync Claude Code's user-authored behavioral setup outward to explicitly selected agents |
| `resolving-merge-conflicts` | Resolve an in-progress git conflict without a **silent drop**. Every hunk from both sides is placed as kept, superseded, or dropped before you commit |
| `refresh-drizzle-migration` | Refresh a branch's stale Drizzle migration after a base merge. Drop it, regenerate it with the project's own drizzle-kit, check its order and that its SQL statements match the old file, then push behind a `preflight-mutations` card. Never runs a command that touches a database |
| `git-commit` | Conventional commits from diff analysis. Every file is classified into exactly one commit. Append-only, with a message-only mode |
| `file-pr` | Publish a PR from a current `ready-to-publish` card, with `browser-qa`'s before-and-after evidence and QA results in the body for UI and API changes, verify the remote branch and PR, then return their evidence to `done` |
| `file-issue` | File one issue an assignee can act on unaided, using two-vocabulary duplicate search, a title that survives a wrong diagnosis, and a body carrying observation, reproduction, expectation and a checkable done condition |
Expand Down Expand Up @@ -139,6 +140,7 @@ Several skills use progressive disclosure. `SKILL.md` holds the spine, and branc
/executing-tickets-with-subagents # Run a bundled ticket end-to-end via subagents
/sync-agent-setups # Preview and sync Claude setup to selected agents
/resolving-merge-conflicts # Resolve merge/rebase conflicts safely
/refresh-drizzle-migration # Regenerate a stale Drizzle migration after a base merge
/git-commit # Conventional commit (or message-only)
/file-pr # Publish a current ready-to-publish card, then return evidence to done
/file-issue # File one actionable issue, with dedupe search and the same bars
Expand Down
71 changes: 71 additions & 0 deletions skills/refresh-drizzle-migration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
name: refresh-drizzle-migration
description: "Refresh a branch's Drizzle migration after its base gained newer ones: merge the base, drop the stale migration, regenerate, check, push behind a preflight. Use when asked to merge main into a PR or branch and refresh or regenerate its migration, or when its journal entry or migration number trails the base's."
---

# Refresh a Drizzle migration

A migration is **stale** when the base branch gained migrations after the head branch generated its own, so the head's journal entry, snapshot chain, or folder timestamp no longer follows the base's latest. Every refresh drops the head's migration and regenerates it on top of the base.

`drizzle-kit generate` is the only drizzle-kit command this skill runs. The user's refresh request is the per-run permission for that one generate step. `migrate`, `push`, `pull`, `up`, `studio`, and any script that wraps them stay out of this run, because they touch a database or rewrite the migrations layout.

## 1. Record the starting point

Resolve the base branch the way `resolving-merge-conflicts` step 1 does. Fetch it, and use the remote-tracking ref `refs/remotes/<remote>/<base>` as `<base-ref>` in every command below; a local branch of the same name can lag the remote. When the base comes from the PR, require `<base-ref>` to equal the PR's `baseRefOid`. Require a clean worktree with the head branch checked out, as that skill does.

Find every Drizzle config: `git ls-files '*drizzle.config.*'`, plus every path a `package.json` script passes to `drizzle-kit` through `--config`. Read each config's `out` folder (default `./drizzle`) and resolve it against the config's own directory, so it works as a repo-root pathspec. Identify the layout per `out` folder:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

- **Journal layout** (drizzle-kit 0.x): `meta/_journal.json`, `<idx>_<name>.sql`, `meta/<idx>_snapshot.json`.
- **Folder layout** (drizzle-kit v1): one `<timestamp>_<name>/` folder per migration holding `migration.sql` and `snapshot.json`, and no journal.

List the head's own migrations per `out` folder, before anything merges, with `git diff --name-only --diff-filter=A <base-ref>...HEAD -- <out>`. The stale SQL is readable at `HEAD` as it stands now, so record that SHA as the **stale commit**. When this lists nothing, an earlier merge may have dropped the files, so fall back to `git log --full-history --no-merges --diff-filter=A --name-only --format= <base-ref>..HEAD -- <out>`, and take each file's stale commit from `git log -1 --no-merges --format=%H --full-history --diff-filter=AM <base-ref>..HEAD -- <path>`, the last commit that wrote it, so a hand edit made after generate is the version step 5 compares.
Comment thread
bhagyamudgal marked this conversation as resolved.
Resolved

Then run `git diff --name-only --no-renames --diff-filter=MD <base-ref>...HEAD -- <out>`. Git prints these paths from the repo root. Any path other than `<out>/meta/_journal.json` and the head migration's own `<out>/meta/<idx>_snapshot.json`, which an earlier hand merge may have resolved to the head side, means the head edited, renamed, or deleted a migration the base already has, which a refresh cannot repair, so stop and show those paths to the user.

Count migrations, not files: `.sql` files in the journal layout, distinct `<timestamp>_<name>/` folders in the folder layout. Leave alone every config whose `out` folder holds none. When one `out` folder holds two or more, stop: show them to the user and ask how to proceed, since one generate run can only produce one migration.

The step is done when every config is classified as untouched or as holding exactly one head migration, with its layout, stale SQL path, and stale commit recorded.

## 2. Merge the base

Skip this step when `git merge-base --is-ancestor <base-ref> HEAD` exits 0. Otherwise merge the base into the head branch through `resolving-merge-conflicts`, whose step 1 owns how a conflicted Drizzle `meta/` file resolves. That skill ends the merge with its own commit and readiness card, and leaves the push to step 6 here.

The step is done when `git merge-base --is-ancestor <base-ref> HEAD` exits 0 and `git status --porcelain` is empty.

## 3. Drop the stale migration

For each touched config, return every path the head's migration occupies to the base's version, using `git restore --source=<base-ref> -- <path>` when the base has that path and deleting the path otherwise. A path both branches added belongs to the base.

- **Journal layout**: `meta/_journal.json`, which always comes back from the base, plus the SQL file and its `meta/<idx>_snapshot.json`.
- **Folder layout**: the migration folder.

The step is done when `git diff --name-only <base-ref> -- <out>`, which compares the working tree with the base, lists nothing.

## 4. Regenerate

Read the `package.json` of the config's package and find the script that runs `drizzle-kit generate`. Run that script through the project's package manager only when it runs nothing but `drizzle-kit generate` and targets this config, through a `--config` that resolves to it or, with no `--config`, because this config is the package's default `drizzle.config.*`. Otherwise run the installed binary directly from the config's own directory, since drizzle-kit resolves `schema`, `out`, and `.env` from where it runs: `<bin> generate --config <absolute config path>`, where `<bin>` is the nearest `node_modules/.bin/drizzle-kit` from the package directory up to the repo root. When no such binary exists, stop and tell the user to install dependencies. Fetching drizzle-kit on the fly, through `npx`, `bunx`, or `npm exec`, pulls unreviewed code.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

Run generate with stdin from `/dev/null` and a two-minute limit. drizzle-kit asks "created or renamed?" whenever a diff drops one column or table and adds another, and an agent shell has no TTY to answer it. When generate errors on a prompt or TTY, or hits the limit, stop and hand the user:

- the exact command, to run in their own terminal;
- the answer for each prompt, read from the stale SQL. A `RENAME` statement there means "renamed", and a matching `DROP` with `ADD` means "created".

Resume at step 5 once the user reports the new files exist.

The step is done when the `out` folder holds exactly one migration the base lacks, or generate reported no schema changes. With no changes, step 5 compares the stale SQL against an empty set.

## 5. Check the regenerated migration

Both checks must pass before anything is committed. A failure stops the run and shows the user the values or statements that failed.

**Order.** In the journal layout, the new entry's `idx` is one past the base's highest, its `when` is greater than every base entry's, and its snapshot's `prevId` equals the `id` of the base's latest snapshot. In the folder layout, the new folder's timestamp sorts after every base folder, and its snapshot's `prevIds` names the base's latest snapshot `id`.

**Same change.** Compare the stale SQL (`git show <stale commit>:<path>`) with the regenerated SQL as statement sets. Normalize both: split on `--> statement-breakpoint`, collapse whitespace, trim each trailing `;`. Order does not matter. A statement on only one side fails the check. A statement only in the stale file is hand-written SQL, a backfill, or a `--custom` migration that generate cannot reproduce. A statement only in the new file means the merged schema drifted from what the PR intended.
Comment thread
bhagyamudgal marked this conversation as resolved.
Resolved
Comment thread
bhagyamudgal marked this conversation as resolved.
Resolved

## 6. Verify, commit, push

Hand off to `done`, then commit the regenerated files through `git-commit`, separate from the merge commit.
Comment thread
bhagyamudgal marked this conversation as resolved.
Resolved

Pushing updates a shared ref, and the branch may belong to a teammate. Push to `<head-remote>`, the remote that resolves to the PR's `headRepository`, or the branch's upstream when there is no PR; on a fork PR it differs from the base's remote. When no configured remote resolves to it, stop and ask. Invoke `preflight-mutations` immediately before the push, with the branch, `<head-remote>`, the local and remote SHAs, and the branch owner (the PR author, or the head repository's owner) in its ownership field. Apply its result. Push without force, then read the remote ref back with `git ls-remote <head-remote> refs/heads/<branch>` and confirm it equals local `HEAD`.

In the final report, tell the user what happens to a database that already applied the stale migration. Its next migrate runs the regenerated migration's DDL again. In the journal layout, the 0.x migrator also silently skips every base migration whose journal `when` is at or before the stale entry's, because it only applies entries newer than the last one it recorded. For the folder layout, say this skill has not established which base migrations such a database skips. Which databases those are, and how to repair them, is the user's call.
2 changes: 1 addition & 1 deletion skills/resolving-merge-conflicts/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Resolve an in-progress git conflict without a silent drop. Use whe

1. Orient before you touch anything. Establish from `git status` whether you are in a merge, a rebase, a cherry-pick, a revert, a `git am`, or a stash pop. Unmerged paths with none of the other five in progress mean a conflicting stash pop; confirm it with `git stash list` and the request, and ask the user when their origin is unclear. When none is in progress and nothing is unmerged, start a merge only from a clean worktree. When `git status --porcelain` lists anything, stop and ask the user to commit it, stash it, or move it to a separate worktree; never stash it yourself. For a PR, the base repository is the PR URL's `<owner>/<repo>`; read the base ref, its tip, the head branch, and the head repository with `gh pr view <url> --json baseRefName,baseRefOid,headRefName,headRepository`. The base branch is the one the request names, else the PR's `baseRefName`, else the default branch from `gh repo view --json defaultBranchRef`. When the head branch, `headRefName` for a PR, is not checked out, stop and ask. Fetch from the remote that resolves to the base repository, require the fetched base ref to equal `baseRefOid` when the base is the PR's, merge that remote's base ref into the head branch, or rebase onto it when the user asked for a rebase, and orient. Rebase only when the user asked for one, because rebasing a published branch rewrites its history; step 5's `preflight-mutations` gate covers its push. Under a rebase `ours` is the upstream you are replaying onto and `theirs` is your own commit, the inverse of a merge, and getting this backwards is the most common wrong resolution.

Then split the conflicted files. Regenerate generated artifacts and lockfiles from source, never hand-merge them. That means `bun.lock`, `pnpm-lock.yaml`, `_generated/`, and snapshots. Only hand-written files go through steps 2-3.
Then split the conflicted files. Regenerate generated artifacts and lockfiles from source, never hand-merge them. That means `bun.lock`, `pnpm-lock.yaml`, `_generated/`, and snapshots. Only hand-written files go through steps 2-3. Under a rebase, stop and ask before touching a conflicted Drizzle migration file, because deleting the head's SQL there rewrites the commit that added it. Under a merge, a conflicted Drizzle `meta/_journal.json` or `meta/<idx>_snapshot.json` takes the base side. Delete the head's own migration SQL in the same merge, or take the base side when the base added the same path, and record the head's SQL as dropped in the ledger either way. When `refresh-drizzle-migration` did not start this merge, invoke it after step 5 and before any push, so the branch never ships a schema change without its migration.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

2. Find the primary sources for each conflict. Learn why each side made its change. Read the commit messages, check the PRs, check original issues and tickets.

Expand Down
1 change: 1 addition & 0 deletions tools/eval/fixture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ It is not meant to be correct. It carries planted defects so the utterances in

- `src/user.ts` dereferences an optional property — a genuine type error
- `loadUsers` awaits inside a `for` loop — the sequential-await shape
- `drizzle.config.ts`, `src/schema.ts` and `drizzle/` form a journal-layout Drizzle setup with one migration and a `db:generate` script, so migration-refresh utterances have a migrations folder to land on

**The fixture matters more than it looks.** An early prototype ran against an empty
directory, and `"ts errors"` did *not* fire `fix-ts-errors` — the agent ran `Glob` to look
Expand Down
5 changes: 5 additions & 0 deletions tools/eval/fixture/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
schema: "./src/schema.ts",
out: "./drizzle",
dialect: "postgresql",
};
5 changes: 5 additions & 0 deletions tools/eval/fixture/drizzle/0000_create_users.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CREATE TABLE "users" (
"id" text PRIMARY KEY NOT NULL,
"email" text NOT NULL,
"nickname" text
);
31 changes: 31 additions & 0 deletions tools/eval/fixture/drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"id": "5f1c2a9e-3b7d-4c41-9a0e-8d2f6b1e7c34",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": { "name": "id", "type": "text", "primaryKey": true, "notNull": true },
"email": { "name": "email", "type": "text", "primaryKey": false, "notNull": true },
"nickname": { "name": "nickname", "type": "text", "primaryKey": false, "notNull": false }
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": { "columns": {}, "schemas": {}, "tables": {} }
}
13 changes: 13 additions & 0 deletions tools/eval/fixture/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1761955200000,
"tag": "0000_create_users",
"breakpoints": true
}
]
}
9 changes: 7 additions & 2 deletions tools/eval/fixture/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
"version": "1.0.0",
"private": true,
"scripts": {
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"db:generate": "drizzle-kit generate"
},
"devDependencies": {
"typescript": "^5.0.0"
"typescript": "^5.0.0",
"drizzle-kit": "^0.31.0"
},
"dependencies": {
"drizzle-orm": "^0.44.0"
}
}
7 changes: 7 additions & 0 deletions tools/eval/fixture/src/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pgTable, text } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
id: text("id").primaryKey(),
email: text("email").notNull(),
nickname: text("nickname"),
});
5 changes: 5 additions & 0 deletions tools/eval/test_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,12 @@ def test_protocol_manifest_names_every_acceptance_source(self):
"tools/eval/slop_rules.json",
"tools/eval/register_cases.json",
"tools/eval/fixture/README.md",
"tools/eval/fixture/drizzle/0000_create_users.sql",
"tools/eval/fixture/drizzle/meta/0000_snapshot.json",
"tools/eval/fixture/drizzle/meta/_journal.json",
"tools/eval/fixture/drizzle.config.ts",
"tools/eval/fixture/package.json",
"tools/eval/fixture/src/schema.ts",
"tools/eval/fixture/src/user.ts",
],
)
Expand Down
14 changes: 13 additions & 1 deletion tools/eval/triggers.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,18 @@

{ "utterance": "this pr needs proper browser qa https://github.com/acme/shop/pull/42",
"expect": "browser-qa",
"why": "COLLISION: a PR URL with a browser QA ask belongs to browser-qa, not review-pr" }
"why": "COLLISION: a PR URL with a browser QA ask belongs to browser-qa, not review-pr" },

{ "utterance": "merge main and refresh the migration",
"expect": "refresh-drizzle-migration",
"why": "primary branch: the user's own phrasing from 27 logged turns, with no skill or conflict named" },

{ "utterance": "we're mid-merge with main and drizzle/meta/_journal.json has conflict markers",
"expect": "resolving-merge-conflicts",
"why": "COLLISION: an in-progress conflict stays with resolving-merge-conflicts, whose step 1 resolves Drizzle meta files to the base side and routes the regeneration to refresh-drizzle-migration" },

{ "utterance": "apply the migration to the database",
"forbid": "refresh-drizzle-migration",
"why": "NEGATIVE: running a migration touches a database, which the refresh skill never does. Forbid rather than expect-nothing, because another skill may legitimately fire first" }
]
}
Loading