You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cli): drop "Path 1/2/3" jargon from agent setup prompt
The orient-and-route prompt the agent reads after `stash init` referred
to the two supported flows as "Path 1" and "Path 3" (with "Path 2" as
the not-supported in-place case). The agent then surfaced that
numbering verbatim to end users, who have no context for it — the gaps
in the numbering came from an internal conversation about the
scenario taxonomy, not anything the user should care about.
Replace the labels with the two intended actions ("Add a new encrypted
column" and "Migrate an existing column to encrypted"), and reframe
the not-supported case as a brief "Converting in place is not
supported" callout rather than a third numbered path. The migrate
flow now also opens with a one-line note on why it's staged (parallel
twin + dual-write + rename) so the user has the model before reading
the steps.
Tests updated to assert the new headings and the staged-twin mention.
'`stash init` has finished its mechanical setup. Your job is **not** to start editing schema or running migrations immediately. Your job is to **orient the user with the two real paths for encrypting a column, then ask which one they want before touching anything**. Pick concrete table/column names from `.cipherstash/context.json` when describing the paths so the user can recognise their own data.',
182
+
'`stash init` has finished its mechanical setup. Your job is **not** to start editing schema or running migrations immediately. Your job is to **orient the user with the two real options for encrypting a column, then ask which one they want before touching anything**. Pick concrete table/column names from `.cipherstash/context.json` when describing the options so the user can recognise their own data.',
183
183
'',
184
184
'## What `stash init` already did',
185
185
'',
@@ -191,53 +191,55 @@ export function renderSetupPrompt(ctx: SetupPromptContext): string {
191
191
'',
192
192
renderSkillIndex(ctx.installedSkills),
193
193
'',
194
-
'Read the skills before answering API or pattern questions. The doctrine in `AGENTS.md` (or its inlined equivalent) covers the invariants that apply to *any* path — never log plaintext, never `.notNull()` on creation, etc.',
194
+
'Read the skills before answering API or pattern questions. The doctrine in `AGENTS.md` (or its inlined equivalent) covers the invariants that apply regardless of which flow you take — never log plaintext, never `.notNull()` on creation, etc.',
195
195
'',
196
-
'## The two paths',
196
+
'## The two options',
197
197
'',
198
198
"There are exactly two supported ways to encrypt a column. Recognise which one applies to the user's request before doing anything.",
199
199
'',
200
-
'### Path 1 — Add a new encrypted column from scratch',
200
+
'### Add a new encrypted column',
201
201
'',
202
-
'Use when the user wants a column that **does not yet exist** in the database (no plaintext predecessor). This is normal Drizzle / Supabase work plus the encryption client patterns from the integration skill.',
202
+
'Use when the column **does not yet exist** in the database (no plaintext predecessor to preserve). This is normal Drizzle / Supabase work plus the encryption client patterns from the integration skill.',
203
203
'',
204
204
"1. **If this is the first encrypted column in the project, configure the bundler exclusion first.** `@cipherstash/stack` cannot be bundled (it wraps a native FFI module). Next.js: add `serverExternalPackages: ['@cipherstash/stack', '@cipherstash/protect-ffi']` to `next.config.*`. Webpack: `externals`. esbuild: `external`. Vite SSR: `ssr.external`. Without this, the encryption client crashes at runtime with `Cannot find module '@cipherstash/protect-ffi-*'`. See the `stash-encryption` skill's Installation section for the full snippets.",
205
205
"2. Edit the user's real schema file (`src/db/schema.ts` or wherever they keep it) to declare the new encrypted column. Use the patterns in the integration skill — `encryptedType` for Drizzle, `encryptedColumn` for Supabase. Encrypted columns must be **nullable `jsonb`** at creation time. Never `.notNull()`.",
206
206
`3. Generate the schema migration${migration ? ` — \`${migration.generate}\` (${migration.tool})` : " using the project's existing migration tooling"}.`,
207
207
`4. Show the user the generated SQL before applying${migration ? ` — \`${migration.apply}\`` : ''}.`,
208
208
`5. Register the encryption config — \`${cli} db push\`. If the project has no active EQL config yet (first encrypted column ever), this writes directly to active and you can skip step 6. If an active config already exists, push writes \`pending\` and prints a next-step note.`,
209
-
`6. **If db push wrote pending**, promote it to active — \`${cli} db activate\`. (Use \`${cli} db activate\` for path 1 because no rename is needed; \`${cli} encrypt cutover\` is for path 3 where columns are being renamed.)`,
209
+
`6. **If db push wrote pending**, promote it to active — \`${cli} db activate\`. (Use \`${cli} db activate\` here because no rename is needed; \`${cli} encrypt cutover\` is reserved for the migrate-existing-column flow.)`,
210
210
'7. Wire the column through the application code: insert paths encrypt before write, select paths decrypt after read, query paths use the right operator (`protectOps.eq`, etc. — see the integration skill).',
211
211
'8. Verify with a round-trip: insert a record, select it back, confirm the value decrypts and the search ops work.',
212
212
'',
213
-
'### Path 3 — Migrate an existing populated column to encrypted',
213
+
'### Migrate an existing column to encrypted',
214
214
'',
215
-
"Use when the column **already exists** in the user's database and contains live data that must be preserved. Drives the `stash encrypt` lifecycle — see the `stash-encryption` skill for the full model.",
215
+
"Use when the column **already exists** in the user's database and contains live data that must be preserved.",
216
216
'',
217
-
"1. **Schema-add.** Add an `<col>_encrypted` twin column to the user's real schema file. Generate and apply the schema migration. The encrypted twin must be nullable `jsonb`. **If this is the first encrypted column in the project, configure the bundler exclusion now** — see path 1 step 1 for the snippets. Without it, importing the encryption client at backfill time will crash.",
217
+
"Why it's staged: there is no atomic way to replace a populated column with an encrypted one without corrupting data. Instead the lifecycle adds a parallel `<col>_encrypted` twin, dual-writes from the app while existing rows are backfilled, then renames the twin into the original column name and drops the old plaintext. The `stash encrypt` CLI commands drive each step; the `stash-encryption` skill has the full model.",
218
+
'',
219
+
"1. **Schema-add.** Add an `<col>_encrypted` twin column (nullable `jsonb`) alongside the existing plaintext column in the user's real schema file. Generate and apply the schema migration. **If this is the first encrypted column in the project, configure the bundler exclusion now** — see the snippets in the previous section. Without it, importing the encryption client at backfill time will crash.",
218
220
`2. **Register pending config** — \`${cli} db push\`. With an existing active config, this writes the new column-set as \`pending\`. Cutover (step 5) will promote it. (If this is the very first push for the project, db push writes active directly — fine, the rest of the flow still works.)`,
219
221
'3. **Dual-write.** Edit the application code so every insert/update writes to *both* `<col>` (plaintext, unchanged) and `<col>_encrypted` (ciphertext via the encryption client). Reads still come from the plaintext column. Ship that code change.',
220
222
`4. **Backfill.** Run \`${cli} encrypt backfill --table <T> --column <c>\`. The CLI prompts the user (or accepts \`--confirm-dual-writes-deployed\` non-interactively) to confirm dual-writes are live, then chunks through the existing rows. Resumable; checkpoints to \`cs_migrations\` after every chunk. SIGINT-safe.`,
221
223
`5. **Switch the schema and re-push, then cutover.** Update the schema file to declare the encrypted column under its final name (drop \`_encrypted\` suffix, switch \`<col>\` to \`encryptedType\`). Run \`${cli} db push\` again — pending now reflects the renamed shape. Then \`${cli} encrypt cutover --table <T> --column <c>\` runs the rename in one transaction (\`<col>\` → \`<col>_plaintext\`, \`<col>_encrypted\` → \`<col>\`) and promotes pending → active. Application reads of \`<col>\` now return decrypted ciphertext transparently — no read-path code change.`,
222
224
'6. **Remove the dual-write code.** The plaintext column is now `<col>_plaintext` and is no longer authoritative. Delete the dual-write logic from the persistence layer.',
223
-
`7. **Drop.** Run \`${cli} encrypt drop --table <T> --column <c>\`. Generates a migration that removes the now-unused \`<col>_plaintext\`. Apply with the project\'s normal migration tooling.`,
225
+
`7. **Drop.** Run \`${cli} encrypt drop --table <T> --column <c>\`. Generates a migration that removes the now-unused \`<col>_plaintext\`. Apply with the project's normal migration tooling.`,
224
226
'',
225
227
'Recovery: if the user reports that backfill ran *before* the dual-write code was actually live, drift is expected (rows written during the backfill window land in plaintext only). Re-run with `--force` to encrypt every plaintext row regardless of current state.',
226
228
'',
227
-
'### Path 2 — Convert a column in place (NOT SUPPORTED)',
229
+
'### Converting in place is not supported',
228
230
'',
229
-
'There is no supported way to drop the plaintext column and replace it with an encrypted column atomically while preserving data. Any "just swap the type" path corrupts data or loses constraints. If the user asks for this, explain why it doesn\'t work and route them to path 3 instead. The only legitimate way to clobber a column with no data is path 1 — and only when there is genuinely no data to preserve.',
231
+
'There is no supported way to drop a populated plaintext column and replace it with an encrypted column atomically — any "just swap the type" approach corrupts data or loses constraints. If the user asks for that, explain why it doesn\'t work and route them to the migrate-existing-column flow above. The only situation where you can clobber a column without staging is when there is genuinely no data to preserve, which is just the add-new-column flow.',
230
232
'',
231
233
'## Your first response',
232
234
'',
233
-
'Before any edits, send the user a short orientation message. Confirm setup is complete, list the skills loaded with one-line purposes, summarise the two paths in your own words, and end with a clear question — *"Which would you like to do? You can name a specific table+column or describe what you\'re trying to protect."* Reference concrete tables/columns from `.cipherstash/context.json` when it helps.',
235
+
'Before any edits, send the user a short orientation message. Confirm setup is complete, list the skills loaded with one-line purposes, summarise the two options in your own words, and end with a clear question — *"Which would you like to do? You can name a specific table+column or describe what you\'re trying to protect."* Reference concrete tables/columns from `.cipherstash/context.json` when it helps.',
234
236
'',
235
-
'Once the user answers, execute the relevant path. Show diffs / generated SQL before applying. Pause for review at every database-mutating step.',
237
+
'Once the user answers, execute the relevant flow. Show diffs / generated SQL before applying. Pause for review at every database-mutating step.',
236
238
'',
237
239
'## Stop and ask the user when',
238
240
'',
239
241
bullet(
240
-
"The user asks for path 2 (convert in place). Explain why it doesn't work, suggest path 3.",
242
+
"The user asks to convert a populated column in place. Explain why it doesn't work and offer the migrate-existing-column flow instead.",
241
243
),
242
244
bullet(
243
245
"A column the user names is already encrypted (`eql_v2_encrypted` udt) but with a different EQL config than they've described. This is the post-cutover re-encryption case (`stash encrypt update`, not yet shipped) — surface it instead of guessing.",
0 commit comments