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
36 changes: 36 additions & 0 deletions .changeset/stash-deployment-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
'stash': minor
'@cipherstash/wizard': patch
---

Add a `stash-deployment` agent skill and install it for every integration.

The rollout/cutover lifecycle was documented in `stash-encryption` and
`stash-cli` as a *command sequence*, with the deploy boundaries described in
passing. In practice the boundaries are the hard part: an agent that treats the
lifecycle as one unit of work — twin column, dual-write, backfill, read switch,
drop — produces a plan that loses data, because ciphertext can only be written
by the application and the plaintext column must stay authoritative until every
row has a ciphertext twin and the deployed code reads it.

The new skill makes the deploy shape the primary subject:

- The four-deploy ladder (rollout → read cutover → stop dual-writes → drop
plaintext) with the out-of-band backfill and index build between the first
two, three human gates, and what each gate is actually verifying.
- A failure table: for each way of collapsing the ladder, the data that is lost.
- Rollback per stage, making explicit that only the final drop is irreversible.
- `CS_*` credentials as a **build-time** input on platforms that construct the
encryption client at module load, and the keyset rule for backfills — the
backfill must encrypt under the same keyset the deployed app resolves
(credentials may differ, keyset may not); ciphertext under any other keyset
fails only at read time.
- A Prisma Postgres / Prisma Compute section: EQL installing through the Prisma
Next migration graph, one merge deploying one stage, the additive-only deploy
policy that makes the plaintext drop fail the build (and the apply-before-merge
sequence that avoids it), preview-branch databases masking destructive
migrations and inviting a wrong-database apply, and running one-off jobs
against a hosted database.

`stash-deployment` joins `stash-encryption`, `stash-indexing` and `stash-cli` in
the set every integration installs.
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ If these variables are missing, tests that require live encryption will fail or
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
- `examples/*`: Working apps (basic, prisma, supabase-worker)
- `docs/plans/*`: Internal design plans. User-facing documentation lives at https://cipherstash.com/docs (not in this repo).
- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-indexing`, `stash-postgres`, `stash-edge`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma-next`, `stash-supply-chain-security`)
- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-indexing`, `stash-deployment`, `stash-postgres`, `stash-edge`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma-next`, `stash-supply-chain-security`)

## Agent Skills — these ship to customers

Expand All @@ -112,6 +112,7 @@ nothing type-checks them, and the damage lands in a customer's repo, not ours.
| `packages/stack` encryption API, schema builders, subpath exports | `skills/stash-encryption` |
| Drizzle / Supabase / Prisma Next / DynamoDB integrations | `skills/stash-drizzle`, `skills/stash-supabase`, `skills/stash-prisma-next`, `skills/stash-dynamodb` |
| The rollout/cutover lifecycle (`packages/migrate`, `stash encrypt *`) | `skills/stash-encryption` and `skills/stash-cli` |
| The deploy sequencing / deploy-gate story, `stash env`, or platform-specific deployment guidance | `skills/stash-deployment` |
| The `@cipherstash/eql` pin, `eql install`/`eql migration` behaviour, or index-related SQL guidance | `skills/stash-indexing` |
| The EQL operator/domain surface (`eql_v3.query_*` casts, predicate forms) | `skills/stash-postgres` |
| `packages/stack/src/wasm-inline.ts`, the WASM entry's exports, or `stash env` | `skills/stash-edge` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ The CipherStash setup skills carry the API details. Use them when you need speci
- `.codex/skills/<skill>/SKILL.md` (Codex)
- Under `## Skill references` at the bottom of this file (editor agents, or when a skills directory could not be written — if the section exists, it is the current copy)

Skills relevant to this project depend on the integration. Common ones: `stash-encryption` (encryption API), `stash-indexing` (indexes on encrypted columns), `stash-cli` (`stash` commands), and one of `stash-drizzle` / `stash-supabase` / `stash-prisma-next` / `stash-dynamodb` for the chosen ORM.
Skills relevant to this project depend on the integration. Common ones: `stash-encryption` (encryption API), `stash-indexing` (indexes on encrypted columns), `stash-deployment` (taking a rollout to production), `stash-cli` (`stash` commands), and one of `stash-drizzle` / `stash-supabase` / `stash-prisma-next` / `stash-dynamodb` for the chosen ORM.
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,20 @@ describe('skillsFor', () => {
'stash-encryption',
'stash-prisma-next',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
})

it('falls back to the base skills for an unmapped integration (never crashes)', () => {
// Simulate a future Integration variant with no SKILL_MAP entry.
const skills = skillsFor('mystery-orm' as Integration)
expect(skills).toEqual(['stash-encryption', 'stash-indexing', 'stash-cli'])
expect(skills).toEqual([
'stash-encryption',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
})
})

Expand All @@ -157,6 +163,7 @@ describe('installSkills', () => {
'stash-encryption',
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
expect(failed).toEqual([])
Expand Down Expand Up @@ -199,6 +206,7 @@ describe('installSkills', () => {
'stash-encryption',
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-cli',
],
})
Expand All @@ -221,7 +229,12 @@ describe('installSkills', () => {
)

const { copied, failed } = installSkills(tmp, '.codex/skills', 'drizzle')
expect(copied).toEqual(['stash-encryption', 'stash-indexing', 'stash-cli'])
expect(copied).toEqual([
'stash-encryption',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
expect(failed).toEqual(['stash-drizzle'])
expect(warnings()).toContain('Failed to install skill stash-drizzle')
})
Expand All @@ -240,6 +253,7 @@ describe('installSkills', () => {
'stash-encryption',
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
})
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/commands/init/lib/install-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import { findBundledDir } from './bundled-paths.js'
* `dist/skills/` at build time.
*/
export const SKILL_MAP: Record<Integration, readonly string[]> = {
drizzle: ['stash-encryption', 'stash-drizzle', 'stash-indexing', 'stash-cli'],
drizzle: [
'stash-encryption',
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-cli',
],
// Supabase gets the raw-SQL and edge skills on top of its own: Edge
// Functions are the flagship use of the WASM entry, and Supabase projects
// write hand-written SQL in migrations and RPC even when the app itself
Expand All @@ -22,12 +28,14 @@ export const SKILL_MAP: Record<Integration, readonly string[]> = {
'stash-indexing',
'stash-postgres',
'stash-edge',
'stash-deployment',
'stash-cli',
],
'prisma-next': [
'stash-encryption',
'stash-prisma-next',
'stash-indexing',
'stash-deployment',
'stash-cli',
],
// The no-ORM path: `stash-postgres` (binding + predicate forms) and `stash-edge`
Expand All @@ -38,6 +46,7 @@ export const SKILL_MAP: Record<Integration, readonly string[]> = {
'stash-indexing',
'stash-postgres',
'stash-edge',
'stash-deployment',
'stash-cli',
],
}
Expand All @@ -46,6 +55,7 @@ export const SKILL_MAP: Record<Integration, readonly string[]> = {
const BASE_SKILLS: readonly string[] = [
'stash-encryption',
'stash-indexing',
'stash-deployment',
'stash-cli',
]

Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/init/lib/setup-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ const SKILL_PURPOSES: Record<string, string> = {
'hand-written Postgres SQL over `pg` / `postgres-js`: the encrypted predicate matrix, `eql_v3.query_*` operand casts, per-driver parameter binding',
'stash-edge':
'the `@cipherstash/stack/wasm-inline` entry for Deno / Supabase Edge Functions / Workers: imports, `CS_*` credentials, the credential-identity rule',
'stash-deployment':
'taking a rollout to a live environment — the multi-deploy ladder and its gates, backfilling against a production database, `CS_*` credentials at build and run time, Prisma Postgres/Compute specifics',
'stash-dynamodb':
'DynamoDB encryption: per-item encrypt/decrypt, HMAC attribute keys, audit logging',
'stash-cli':
Expand Down
2 changes: 2 additions & 0 deletions packages/wizard/src/__tests__/install-skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('maybeInstallSkills', () => {
'stash-encryption',
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
expect(warnings()).toContain('Could not create ./.claude/skills/')
Expand All @@ -66,6 +67,7 @@ describe('maybeInstallSkills', () => {
expect(result.copied).toEqual([
'stash-encryption',
'stash-indexing',
'stash-deployment',
'stash-cli',
])
expect(result.failed).toEqual(['stash-drizzle'])
Expand Down
11 changes: 10 additions & 1 deletion packages/wizard/src/lib/install-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import type { Integration } from './types.js'
* follow-up work.
*/
export const SKILL_MAP: Record<Integration, readonly string[]> = {
drizzle: ['stash-encryption', 'stash-drizzle', 'stash-indexing', 'stash-cli'],
drizzle: [
'stash-encryption',
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-cli',
],
// `stash-postgres` / `stash-edge` mirror the CLI's SKILL_MAP — see the comments
// there for why Supabase and the generic (no-ORM) path get them (#754).
supabase: [
Expand All @@ -21,19 +27,22 @@ export const SKILL_MAP: Record<Integration, readonly string[]> = {
'stash-indexing',
'stash-postgres',
'stash-edge',
'stash-deployment',
'stash-cli',
],
prisma: [
'stash-encryption',
'stash-prisma-next',
'stash-indexing',
'stash-deployment',
'stash-cli',
],
generic: [
'stash-encryption',
'stash-indexing',
'stash-postgres',
'stash-edge',
'stash-deployment',
'stash-cli',
],
}
Expand Down
Loading
Loading