Skip to content

feat(cli): implement phased plans for rollout and cutover#439

Merged
calvinbrewer merged 2 commits intomainfrom
phased-plans
May 8, 2026
Merged

feat(cli): implement phased plans for rollout and cutover#439
calvinbrewer merged 2 commits intomainfrom
phased-plans

Conversation

@calvinbrewer
Copy link
Copy Markdown
Contributor

@calvinbrewer calvinbrewer commented May 7, 2026

Summary by CodeRabbit

  • New Features

    • plan: added --complete-rollout flag and automatic plan-step selection; plan now passes step through init.
    • status: new quest-style progress view with TTY/plain/JSON output modes and improved outro routing.
    • impl: enforces deploy-gate checks to block unsafe cutover runs when prerequisites aren’t observed.
  • Tests

    • Expanded unit coverage for plan parsing, rollout/cutover classification, setup prompts, and status/quest rendering.
  • Documentation

    • Updated CLI and migration guides (Drizzle, Supabase, DynamoDB, encryption workflows).

@calvinbrewer calvinbrewer requested a review from a team as a code owner May 7, 2026 22:10
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

⚠️ No Changeset found

Latest commit: 1df1a49

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

📝 Walkthrough

Walkthrough

Adds plan-step-aware plan summaries and context threading, per-column rollout-state detection and DB readers, plan-step selection with a --complete-rollout override, impl-time deploy-gate verification for cutover plans, a quest-log status model with TTY/plain/JSON renderers, expanded tests, and documentation updates across integrations.

Changes

Column Rollout State Detection & Classification

Layer / File(s) Summary
Rollout State Types
packages/cli/src/commands/init/lib/rollout-state.ts
New exported ColumnNeeds type and ColumnState interface; classifyPhase, classifyPhases, rollupPlanStep, detectColumnStates.
DB Readers
packages/cli/src/commands/encrypt/lib/db-readers.ts
latestByColumnSafe, fetchActiveEqlConfig, and fetchPhysicalColumns provide best-effort DB observations consumed by rollout detection and status.
Rollout State Tests
packages/cli/src/commands/init/lib/__tests__/rollout-state.test.ts
Vitest coverage for classification and rollup semantics.

Plan Summary Schema & Context Threading

Layer / File(s) Summary
Plan Summary Schema
packages/cli/src/commands/init/lib/parse-plan.ts
Adds PlanStep union, optional step?: PlanStep on PlanSummary, effectiveStep() fallback, and step-aware renderPlanSummary(summary, cli).
Init Types & Context
packages/cli/src/commands/init/types.ts, packages/cli/src/commands/init/lib/write-context.ts
InitState and persisted ContextFile gain optional planStep; buildSetupPromptContext() populates it.
Plan Parsing Tests
packages/cli/src/commands/init/lib/__tests__/parse-plan.test.ts
Tests for step parsing, backward compatibility, and step-specific footer rendering.

Plan Step Detection & Selection

Layer / File(s) Summary
Plan Step Detection
packages/cli/src/commands/plan/index.ts
detectPlanStep() inspects manifest and DB column states to choose rollout or cutover; --complete-rollout forces complete with confirmation; planCommand(flags) signature added.

Multi-Step Plan & Implement Prompts

Layer / File(s) Summary
Prompt Helpers & Templates
packages/cli/src/commands/init/lib/setup-prompt.ts
Extracted shared plan blocks and setupChecklist; renderPlanPrompt() dispatches by ctx.planStep to rollout/cutover/complete templates.
Setup Prompt Tests
packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts
Expanded coverage for implement-mode and plan-mode templates and planStep behavior.

Deploy-Gate Enforcement at Impl

Layer / File(s) Summary
Cutover Preconditions
packages/cli/src/commands/impl/index.ts
verifyCutoverPreconditions() checks that migrate columns have recorded dual_writing events; blocks stash impl with per-column errors and guidance when missing; adds deploy-gate banner and step-specific outros.

Status Command Quest-Log System

Layer / File(s) Summary
Quest Data Model
packages/cli/src/commands/status/quest.ts
Adds ColumnObservation, ColumnQuest, QuestLog, inferQuestPath, buildColumnQuest, buildQuestLog, and isComplete.
Observation Gathering
packages/cli/src/commands/status/index.ts
gatherObservations() reads manifest and best-effort queries DB (phases/EQL/physical); returns observedFromDb flag.
Status Command & Renderers
packages/cli/src/commands/status/index.ts, packages/cli/src/commands/status/render.ts
statusCommand(flags) supports --quest/--plain/--json, builds QuestLog and renders via renderQuestLogTTY, renderQuestLogPlain, or renderQuestLogJSON; adds nextMoveHint.
Status & Quest Tests
packages/cli/src/commands/status/__tests__/status.test.ts
Comprehensive tests for quest inference, builders, and all rendering modes; replaces legacy lifecycle tests.

CLI & Documentation

Layer / File(s) Summary
CLI Routing
packages/cli/src/bin/stash.ts
Help updated with --complete-rollout and status output flags; main routes parsed flags to statusCommand.
Docs / Skills
skills/*
Multiple SKILL.md files updated to document rollout vs cutover workflow, deploy-gate behavior, plan summary format, and status/plan/impl usage across integrations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • cipherstash/stack#427: Modifies init/impl/status command surfaces and context wiring overlapping with this PR.
  • cipherstash/stack#357: Introduced migration event-log primitives (latestByColumn) that the rollout-state/detection helpers depend on.
  • cipherstash/stack#412: Related changes to init/setup-prompt wiring and planStep propagation.

Suggested reviewers

  • coderdan
  • auxesis

Poem

🐰 Through rollout gates and cutover doors,
Quest logs map what migration stores,
Each column climbs from phase to phase,
With dual writes marking every case.
The CLI now deploys with care—hop in and beware! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: implementing phased plans for rollout and cutover operations in the CLI, which is the central theme across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phased-plans

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@calvinbrewer calvinbrewer changed the title feat(cli): implement phased plans for rollout and backfill feat(cli): implement phased plans for rollout and cutover May 7, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/bin/stash.ts`:
- Around line 399-409: Add an end-to-end test that exercises the top-level CLI
dispatch in stash.ts to lock argv parsing and exit behavior: spawn the CLI
binary (or invoke the bin entry) with the commands 'plan', 'impl', and 'status'
using flags that cover flags.quest, flags.plain, and flags.json, assert the
invoked handlers (planCommand, implCommand, statusCommand) run with expected
args, and verify correct process exit codes and JSON/plain output semantics;
ensure the test fails if unknown flags or incorrect wiring change command
selection or exit status.

In `@packages/cli/src/commands/impl/index.ts`:
- Around line 87-90: The current filter blocks any state not classified as
'cutover' which incorrectly includes completed phases (e.g., dropped) and also
relies on detectColumnStates collapsing DB errors into hard blocks; change the
verifier call to use an error-signaling lookup path (e.g., call
detectColumnStates with a verifier option or use detectColumnStatesForVerifier
so DB outages return null/undefined phases) and tighten the predicate to only
treat true pre-dual-write phases as blockers (replace the broad filter with a
check like isPreDualWritePhase(classifyPhase(s.phase)) or compare against the
explicit pre-dual-write phase names), keeping the map to ({ table: s.table,
column: s.column }).

In `@packages/cli/src/commands/init/lib/setup-prompt.ts`:
- Around line 421-433: The example JSON emitted by planSummaryBlockExample is
invalid because it uses the TypeScript/DSL syntax `"path": "new" | "migrate"`;
update the returned example so the "path" field is a valid JSON string (e.g.
`"path": "new"` or `"path": "migrate"`) instead of the pipe expression so the
<cipherstash:plan-summary> header is parseable; locate function
planSummaryBlockExample and replace that array element accordingly.

In `@packages/cli/src/commands/plan/index.ts`:
- Around line 56-100: The call to detectColumnStates inside detectPlanStep can
throw on DB connection errors but isn't caught, violating the doc promise to
fall back to 'rollout' when the database is unreachable; wrap the await
detectColumnStates(databaseUrl, columns) call in a try/catch (or broaden the
existing try) and on any error log or ignore the error and return 'rollout' so
rollupPlanStep is only called for successful state detection and the function
always returns 'rollout' on DB failures.

In `@packages/cli/src/commands/status/__tests__/status.test.ts`:
- Around line 110-120: The test title says "no signals" but both quest and
noEvents are created with phase: 'dual-writing' and the assertions don't check
the claimed states; change noEvents to be created without a phase signal (e.g.
call buildColumnQuest without phase or pass an empty signal object) so it
represents the no-signals path, then add assertions:
expect(noEvents.path).toBe('migrate') (keep),
expect(noEvents.progress.done).toBe(0), expect(noEvents.progress.total).toBe(5)
(or match existing total), expect(noEvents.objectives[0].status).toBe('active')
and assert that the remaining objectives (objectives[1..]) have status 'locked'
so the test validates the title; use the existing variables buildColumnQuest,
quest, noEvents, progress, and objectives to locate and update the code.

In `@packages/cli/src/commands/status/index.ts`:
- Around line 214-223: The current logic uses flags.quest to run the Clack frame
functions (p.intro, p.note, p.outro) even when stdout isn't a TTY; split frame
vs body decisions so frames only run when process.stdout.isTTY && !flags.plain
but the fancy body can still be forced by flags.quest. Change the code around
useTTY to compute enableFrames = process.stdout.isTTY && !flags.plain and
compute body = flags.quest ? renderQuestLogTTY(log) : renderQuestLogPlain(log);
then if (enableFrames) call p.intro('CipherStash'), p.note(body, 'Quest log'),
p.outro(nextMoveHint(log, cli)); otherwise write the chosen body and the Next
line directly to stdout using process.stdout.write. Ensure you only call
p.intro/p.note/p.outro when enableFrames is true and keep nextMoveHint(cli)
usage identical.

In `@packages/cli/src/commands/status/quest.ts`:
- Around line 89-97: The current inferQuestPath(ColumnObservation) returns
'migrate' when obs.phase === undefined which incorrectly treats manifest-only
columns as migrate flows; update inferQuestPath to first check for a
manifest-driven signal (e.g., a field you populate from readManifest(), such as
obs.manifestPresent or obs.manifestPath) and return the
manifest/“new”/manifest-specific quest path when a manifest exists; if no
manifest info is available, return an explicit unobserved/unknown value (e.g.,
'unknown' or 'unobserved') instead of defaulting to 'migrate' so downstream
logic (stash status, deploy guidance) does not assume migrate behavior. Ensure
you reference and use the same ColumnObservation property that readManifest()
sets and adjust callers that expect only 'migrate'/'new' to handle the new
unknown/manifest value returned by inferQuestPath.

In `@skills/stash-drizzle/SKILL.md`:
- Around line 341-344: Remove the blank line between the two adjacent blockquote
paragraphs so the blockquotes are contiguous (i.e., merge "**Runner note.**
Examples below use `npx stash`..." and "**Where am I?** Run `stash status`
first..." into back-to-back blockquote lines) to satisfy markdownlint MD028;
update the SKILL.md block containing those two blockquote entries so there is no
empty line separating them.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 03bbb83a-7f59-48d9-9a96-7610a0d0533b

📥 Commits

Reviewing files that changed from the base of the PR and between e2136cb and 8b45e74.

📒 Files selected for processing (20)
  • packages/cli/src/bin/stash.ts
  • packages/cli/src/commands/impl/index.ts
  • packages/cli/src/commands/init/lib/__tests__/parse-plan.test.ts
  • packages/cli/src/commands/init/lib/__tests__/rollout-state.test.ts
  • packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts
  • packages/cli/src/commands/init/lib/parse-plan.ts
  • packages/cli/src/commands/init/lib/rollout-state.ts
  • packages/cli/src/commands/init/lib/setup-prompt.ts
  • packages/cli/src/commands/init/lib/write-context.ts
  • packages/cli/src/commands/init/types.ts
  • packages/cli/src/commands/plan/index.ts
  • packages/cli/src/commands/status/__tests__/status.test.ts
  • packages/cli/src/commands/status/index.ts
  • packages/cli/src/commands/status/quest.ts
  • packages/cli/src/commands/status/render.ts
  • skills/stash-cli/SKILL.md
  • skills/stash-drizzle/SKILL.md
  • skills/stash-dynamodb/SKILL.md
  • skills/stash-encryption/SKILL.md
  • skills/stash-supabase/SKILL.md

Comment on lines +399 to +409
await planCommand(flags)
break
case 'impl':
await implCommand(flags)
break
case 'status':
await statusCommand()
await statusCommand({
quest: flags.quest,
plain: flags.plain,
json: flags.json,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add an E2E test for the new CLI flag wiring

Line 399 and Lines 405-409 change top-level CLI dispatch behavior; this needs an accompanying E2E test to lock argv/command behavior and exit semantics.

As per coding guidelines, packages/cli/src/bin/stash.ts: “Add an E2E test when touching src/bin/stash.ts argv parsing, exit codes, or top-level error handling.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/bin/stash.ts` around lines 399 - 409, Add an end-to-end test
that exercises the top-level CLI dispatch in stash.ts to lock argv parsing and
exit behavior: spawn the CLI binary (or invoke the bin entry) with the commands
'plan', 'impl', and 'status' using flags that cover flags.quest, flags.plain,
and flags.json, assert the invoked handlers (planCommand, implCommand,
statusCommand) run with expected args, and verify correct process exit codes and
JSON/plain output semantics; ensure the test fails if unknown flags or incorrect
wiring change command selection or exit status.

Comment on lines +87 to +90
const states = await detectColumnStates(databaseUrl, migrate)
return states
.filter((s) => classifyPhase(s.phase) !== 'cutover')
.map((s) => ({ table: s.table, column: s.column }))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Cutover gate predicate can block valid/completed states and transient DB failures

Line 89 currently blocks anything that is not classified as cutover, which incorrectly includes dropped (already complete). Also, detectColumnStates collapses DB errors into unknown/null phases, so transient DB issues can become hard blocks instead of “could not verify, continue” as documented.

Use a stricter precondition check for “pre-dual-write phases only” and use an error-signaling lookup path for this verifier so DB outages return null instead of a false negative block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/impl/index.ts` around lines 87 - 90, The current
filter blocks any state not classified as 'cutover' which incorrectly includes
completed phases (e.g., dropped) and also relies on detectColumnStates
collapsing DB errors into hard blocks; change the verifier call to use an
error-signaling lookup path (e.g., call detectColumnStates with a verifier
option or use detectColumnStatesForVerifier so DB outages return null/undefined
phases) and tighten the predicate to only treat true pre-dual-write phases as
blockers (replace the broad filter with a check like
isPreDualWritePhase(classifyPhase(s.phase)) or compare against the explicit
pre-dual-write phase names), keeping the map to ({ table: s.table, column:
s.column }).

Comment on lines +421 to +433
function planSummaryBlockExample(step: PlanStep): string {
return [
' ```',
' <!-- cipherstash:plan-summary',
' {',
` "step": "${step}",`,
' "columns": [',
' {"table": "<table_name>", "column": "<column_name>", "path": "new" | "migrate"}',
' ]',
' }',
' -->',
' ```',
].join('\n')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the cipherstash:plan-summary example valid JSON.

This helper is presented as the exact parseable shape, but "path": "new" | "migrate" is not valid JSON. If an agent copies it literally, stash impl can't parse the header it depends on.

Suggested fix
 function planSummaryBlockExample(step: PlanStep): string {
   return [
     '  ```',
     '  <!-- cipherstash:plan-summary',
     '  {',
     `    "step": "${step}",`,
     '    "columns": [',
-    '      {"table": "<table_name>", "column": "<column_name>", "path": "new" | "migrate"}',
+    '      {"table": "<table_name>", "column": "<column_name>", "path": "new"}',
     '    ]',
     '  }',
     '  -->',
     '  ```',
   ].join('\n')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function planSummaryBlockExample(step: PlanStep): string {
return [
' ```',
' <!-- cipherstash:plan-summary',
' {',
` "step": "${step}",`,
' "columns": [',
' {"table": "<table_name>", "column": "<column_name>", "path": "new" | "migrate"}',
' ]',
' }',
' -->',
' ```',
].join('\n')
function planSummaryBlockExample(step: PlanStep): string {
return [
'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/init/lib/setup-prompt.ts` around lines 421 - 433,
The example JSON emitted by planSummaryBlockExample is invalid because it uses
the TypeScript/DSL syntax `"path": "new" | "migrate"`; update the returned
example so the "path" field is a valid JSON string (e.g. `"path": "new"` or
`"path": "migrate"`) instead of the pipe expression so the
<cipherstash:plan-summary> header is parseable; locate function
planSummaryBlockExample and replace that array element accordingly.

Comment thread packages/cli/src/commands/plan/index.ts
Comment on lines +110 to 120
it('with no signals: 0/5, schema-add active, rest locked', () => {
const quest = buildColumnQuest(obs({ phase: 'dual-writing' }))
// Picking dual-writing forces the migrate path; reset and check no-events.
const noEvents = buildColumnQuest({
table: 'users',
column: 'email',
phase: 'dual-writing',
})
const stages = buildStages(readProjectStatus(cwd), 'pnpm dlx stash')
expect(stages[0].detail).toContain('1 table')
expect(stages[0].detail).not.toContain('1 tables')
expect(noEvents.path).toBe('migrate')
expect(quest.progress.total).toBe(5)
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test does not validate what its title claims.

Title says "with no signals: 0/5, schema-add active, rest locked", but both quest and noEvents are built with phase: 'dual-writing' — that is the strongest possible signal, not "no signals". The only assertions are noEvents.path === 'migrate' and quest.progress.total === 5; nothing checks done: 0, objectives[0].status === 'active', or that the rest are locked. The test will pass even if buildColumnQuest regresses on the no-signals path.

💚 Proposed fix
-  it('with no signals: 0/5, schema-add active, rest locked', () => {
-    const quest = buildColumnQuest(obs({ phase: 'dual-writing' }))
-    // Picking dual-writing forces the migrate path; reset and check no-events.
-    const noEvents = buildColumnQuest({
-      table: 'users',
-      column: 'email',
-      phase: 'dual-writing',
-    })
-    expect(noEvents.path).toBe('migrate')
-    expect(quest.progress.total).toBe(5)
-  })
+  it('with no signals: 0/5, schema-add active, rest locked', () => {
+    // No phase, no EQL state, no physical twin column — the genuine
+    // "no signals" case for a column the user declared in
+    // migrations.json but for which nothing has happened yet.
+    const quest = buildColumnQuest(
+      obs({ phase: null, eql: null, physicalEncryptedTwinExists: false }),
+    )
+    expect(quest.path).toBe('migrate')
+    expect(quest.progress).toEqual({ done: 0, total: 5 })
+    expect(quest.objectives[0].status).toBe('active')
+    expect(quest.objectives.slice(1).every((o) => o.status === 'locked')).toBe(true)
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/status/__tests__/status.test.ts` around lines 110 -
120, The test title says "no signals" but both quest and noEvents are created
with phase: 'dual-writing' and the assertions don't check the claimed states;
change noEvents to be created without a phase signal (e.g. call buildColumnQuest
without phase or pass an empty signal object) so it represents the no-signals
path, then add assertions: expect(noEvents.path).toBe('migrate') (keep),
expect(noEvents.progress.done).toBe(0), expect(noEvents.progress.total).toBe(5)
(or match existing total), expect(noEvents.objectives[0].status).toBe('active')
and assert that the remaining objectives (objectives[1..]) have status 'locked'
so the test validates the title; use the existing variables buildColumnQuest,
quest, noEvents, progress, and objectives to locate and update the code.

Comment on lines +214 to +223
const useTTY = flags.quest ?? (process.stdout.isTTY && !flags.plain)

const deeper = [
`Database state: \`${cli} db status\``,
`Per-column state: \`${cli} encrypt status\``,
].join('\n')
p.note(deeper, 'Deeper inspection')
if (useTTY) {
p.intro('CipherStash')
p.note(renderQuestLogTTY(log), 'Quest log')
p.outro(nextMoveHint(log, cli))
} else {
process.stdout.write(`${renderQuestLogPlain(log)}\n`)
process.stdout.write(`Next: ${nextMoveHint(log, cli)}\n`)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, check the file and exact code at the specified lines
cat -n packages/cli/src/commands/status/index.ts | head -230 | tail -40

Repository: cipherstash/stack

Length of output: 1620


🏁 Script executed:

# Get broader context to understand the full function
wc -l packages/cli/src/commands/status/index.ts

Repository: cipherstash/stack

Length of output: 107


🏁 Script executed:

# Find the render functions referenced
rg -n "renderQuestLogTTY|renderQuestLogPlain|nextMoveHint" packages/cli/src/commands/status/index.ts

Repository: cipherstash/stack

Length of output: 402


🏁 Script executed:

# Check where these functions are defined
fd -t f "*.ts" -x rg -l "renderQuestLogTTY|renderQuestLogPlain" {} \;

Repository: cipherstash/stack

Length of output: 345


🏁 Script executed:

# Check the flags definition to understand what --quest is meant to do
rg -B 5 -A 5 "quest.*=" packages/cli/src/commands/status/index.ts | head -50

Repository: cipherstash/stack

Length of output: 43


🏁 Script executed:

# Find the definitions of render functions
fd --glob "*.ts" --exec rg -l "export.*renderQuestLogTTY|export.*renderQuestLogPlain" {} \;

Repository: cipherstash/stack

Length of output: 106


🏁 Script executed:

# Check the import statements to find where they're from
head -30 packages/cli/src/commands/status/index.ts

Repository: cipherstash/stack

Length of output: 954


🏁 Script executed:

# Look at what `@clack/prompts` p.note and related functions do
rg -A 10 "import.*from.*@clack" packages/cli/src/commands/status/index.ts

Repository: cipherstash/stack

Length of output: 437


🏁 Script executed:

# Check the StatusFlags interface to see what flags are available
rg -B 5 -A 15 "interface StatusFlags|type StatusFlags" packages/cli/src/commands/status/index.ts

Repository: cipherstash/stack

Length of output: 766


🏁 Script executed:

# Look at the render functions to understand the difference
cat -n packages/cli/src/commands/status/render.ts | head -80

Repository: cipherstash/stack

Length of output: 2994


🏁 Script executed:

# See how renderQuestLogTTY and renderQuestLogPlain differ
rg -A 20 "export function renderQuestLogTTY|export function renderQuestLogPlain" packages/cli/src/commands/status/render.ts

Repository: cipherstash/stack

Length of output: 1406


🏁 Script executed:

# Check what `@clack/prompts` functions return/do in non-TTY contexts
# by looking for any documentation or usage patterns
rg -A 5 -B 5 "p\.(intro|note|outro)" packages/cli/src/commands/status/index.ts

Repository: cipherstash/stack

Length of output: 441


🏁 Script executed:

# Look for any other places where flags.quest is used or tested
rg -n "flags\.quest|quest.*true" packages/cli/src/commands/status/

Repository: cipherstash/stack

Length of output: 493


🏁 Script executed:

# Check if there's any test that validates the TTY behavior
fd --glob "*.test.ts" --glob "*.spec.ts" packages/cli/src/commands/status/ 2>/dev/null || echo "No test files found in pattern"

Repository: cipherstash/stack

Length of output: 119


🏁 Script executed:

# Check the test file for the status command
cat -n packages/cli/src/commands/status/__tests__/status.test.ts | head -100

Repository: cipherstash/stack

Length of output: 3668


🏁 Script executed:

# Search for tests related to TTY or quest flags
rg -n "quest|isTTY|useTTY" packages/cli/src/commands/status/__tests__/status.test.ts

Repository: cipherstash/stack

Length of output: 3093


🏁 Script executed:

# Check if there's any documentation on `@clack/prompts` behavior in non-TTY contexts
rg -r "clack.*prompts" packages/cli/package.json

Repository: cipherstash/stack

Length of output: 257


🏁 Script executed:

# See what the actual `@clack/prompts` package version is and check its documentation
cat packages/cli/package.json | grep -A 5 -B 5 "@clack"

Repository: cipherstash/stack

Length of output: 334


🏁 Script executed:

# Check if there are any issues or comments about `@clack/prompts` in non-TTY contexts
rg -i "clack|tty|non-tty|isTTY" packages/cli/src/ -A 2 -B 2 | head -50

Repository: cipherstash/stack

Length of output: 3716


🏁 Script executed:

# Look at the exact behavior: what does p.note do and when
# Check if there's any test that mocks stdout.isTTY
rg -n "isTTY" packages/cli/src/commands/status/__tests__/

Repository: cipherstash/stack

Length of output: 43


🏁 Script executed:

# See if statusCommand is tested with different flag combinations
rg -B 5 -A 10 "statusCommand" packages/cli/src/commands/status/__tests__/status.test.ts

Repository: cipherstash/stack

Length of output: 43


🏁 Script executed:

# Look for any issue reports or comments about `@clack/prompts` behavior
rg -i "clack.*pipe|clack.*non.tty|clack.*ci|prompts.*break|prompts.*fail" packages/cli/src/

Repository: cipherstash/stack

Length of output: 309


🏁 Script executed:

# Check the `@clack/prompts` package documentation or types
cat packages/cli/package-lock.json 2>/dev/null | grep -A 20 '"@clack/prompts"' | head -40

Repository: cipherstash/stack

Length of output: 43


🏁 Script executed:

# Look at the exact comment about TTY frames in the code
cat -n packages/cli/src/commands/status/index.ts | sed -n '210,223p'

Repository: cipherstash/stack

Length of output: 679


Keep forced --quest output out of the Clack frame when stdout isn't a TTY.

flags.quest currently forces p.intro, p.note, and p.outro even for pipes and CI environments. The Clack decorative frames are documented as TTY-only (see lines 210–211), but the --quest flag bypasses this check. The intended behavior is to force the fancy body content (with emojis and progress bars) in any context, but the TTY-specific frame functions should only run when process.stdout.isTTY is true.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/status/index.ts` around lines 214 - 223, The
current logic uses flags.quest to run the Clack frame functions (p.intro,
p.note, p.outro) even when stdout isn't a TTY; split frame vs body decisions so
frames only run when process.stdout.isTTY && !flags.plain but the fancy body can
still be forced by flags.quest. Change the code around useTTY to compute
enableFrames = process.stdout.isTTY && !flags.plain and compute body =
flags.quest ? renderQuestLogTTY(log) : renderQuestLogPlain(log); then if
(enableFrames) call p.intro('CipherStash'), p.note(body, 'Quest log'),
p.outro(nextMoveHint(log, cli)); otherwise write the chosen body and the Next
line directly to stdout using process.stdout.write. Ensure you only call
p.intro/p.note/p.outro when enableFrames is true and keep nextMoveHint(cli)
usage identical.

Comment on lines +89 to +97
* When DB connectivity is missing entirely (`phase === undefined`), default
* to migrate — the 5-objective shape is more informative when we don't know.
*/
export function inferQuestPath(obs: ColumnObservation): QuestPath {
if (obs.phase === undefined) return 'migrate'
if (obs.phase !== null) return 'migrate'
if (obs.physicalEncryptedTwinExists) return 'migrate'
return 'new'
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't assume manifest-only columns are migrate flows.

When DB observability is missing, inferQuestPath() hardcodes 'migrate'. That makes stash status tell additive columns to create <col>_encrypted, deploy dual-writes, and so on, which is the wrong fallback for the manifest-only mode documented in this PR.

A safer fix is to preserve the manifest path if it's available from readManifest(), or introduce an explicit unobserved/unknown quest shape instead of emitting migrate-specific guidance by default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/status/quest.ts` around lines 89 - 97, The current
inferQuestPath(ColumnObservation) returns 'migrate' when obs.phase === undefined
which incorrectly treats manifest-only columns as migrate flows; update
inferQuestPath to first check for a manifest-driven signal (e.g., a field you
populate from readManifest(), such as obs.manifestPresent or obs.manifestPath)
and return the manifest/“new”/manifest-specific quest path when a manifest
exists; if no manifest info is available, return an explicit unobserved/unknown
value (e.g., 'unknown' or 'unobserved') instead of defaulting to 'migrate' so
downstream logic (stash status, deploy guidance) does not assume migrate
behavior. Ensure you reference and use the same ColumnObservation property that
readManifest() sets and adjust callers that expect only 'migrate'/'new' to
handle the new unknown/manifest value returned by inferQuestPath.

Comment thread skills/stash-drizzle/SKILL.md Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/stash-cli/SKILL.md`:
- Line 63: Replace the awkward phrase "default-no confirm" in the sentence
explaining the --complete-rollout flag with a clearer wording such as
"confirmation prompt (default: no)"; update the sentence that currently reads
"The flag prints a default-no confirm with a loud warning before generating" to
"The flag displays a confirmation prompt (default: no) with a loud warning
before generating" so readers immediately understand the behavior.

In `@skills/stash-encryption/SKILL.md`:
- Around line 594-600: The fenced diagram block in SKILL.md lacks a language
hint which triggers markdownlint MD040; update the triple-backtick fence to
include a language (e.g., add "text" after the opening ```), so the block
becomes ```text ... ```, ensuring the diagram block is lint-clean without
changing its contents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cdea9f8-23bf-475a-8131-9636ebff3134

📥 Commits

Reviewing files that changed from the base of the PR and between 8b45e74 and 1df1a49.

📒 Files selected for processing (19)
  • packages/cli/src/commands/encrypt/lib/db-readers.ts
  • packages/cli/src/commands/encrypt/status.ts
  • packages/cli/src/commands/impl/index.ts
  • packages/cli/src/commands/impl/steps/handoff-wizard.ts
  • packages/cli/src/commands/init/lib/__tests__/parse-plan.test.ts
  • packages/cli/src/commands/init/lib/parse-plan.ts
  • packages/cli/src/commands/init/lib/rollout-state.ts
  • packages/cli/src/commands/init/lib/write-context.ts
  • packages/cli/src/commands/plan/index.ts
  • packages/cli/src/commands/status/__tests__/status.test.ts
  • packages/cli/src/commands/status/index.ts
  • packages/cli/src/commands/status/quest.ts
  • packages/cli/src/commands/status/render.ts
  • skills/stash-cli/SKILL.md
  • skills/stash-drizzle/SKILL.md
  • skills/stash-dynamodb/SKILL.md
  • skills/stash-encryption/SKILL.md
  • skills/stash-secrets/SKILL.md
  • skills/stash-supabase/SKILL.md
✅ Files skipped from review due to trivial changes (1)
  • packages/cli/src/commands/impl/steps/handoff-wizard.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • skills/stash-dynamodb/SKILL.md
  • packages/cli/src/commands/init/lib/write-context.ts
  • packages/cli/src/commands/plan/index.ts
  • packages/cli/src/commands/init/lib/tests/parse-plan.test.ts
  • packages/cli/src/commands/init/lib/parse-plan.ts
  • packages/cli/src/commands/status/quest.ts
  • packages/cli/src/commands/status/tests/status.test.ts
  • packages/cli/src/commands/impl/index.ts

Comment thread skills/stash-cli/SKILL.md

The split is invisible to the user — they just keep running `stash plan` and `stash impl`; the CLI knows where they are.

For users without a deployed application to gate on (local dev, sandboxes, freshly-seeded test DBs), `stash plan --complete-rollout` produces a single end-to-end plan with no deploy gate. The flag prints a default-no confirm with a loud warning before generating; only safe when no production app writes to this database.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the phrase “default-no confirm” for readability.

Line 63 reads awkwardly; “confirmation prompt (default: no)” is clearer and avoids ambiguity for users skimming runbook text.

Proposed wording tweak
-For users without a deployed application to gate on (local dev, sandboxes, freshly-seeded test DBs), `stash plan --complete-rollout` produces a single end-to-end plan with no deploy gate. The flag prints a default-no confirm with a loud warning before generating; only safe when no production app writes to this database.
+For users without a deployed application to gate on (local dev, sandboxes, freshly-seeded test DBs), `stash plan --complete-rollout` produces a single end-to-end plan with no deploy gate. The flag shows a confirmation prompt (default: no) with a loud warning before generating; only safe when no production app writes to this database.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
For users without a deployed application to gate on (local dev, sandboxes, freshly-seeded test DBs), `stash plan --complete-rollout` produces a single end-to-end plan with no deploy gate. The flag prints a default-no confirm with a loud warning before generating; only safe when no production app writes to this database.
For users without a deployed application to gate on (local dev, sandboxes, freshly-seeded test DBs), `stash plan --complete-rollout` produces a single end-to-end plan with no deploy gate. The flag shows a confirmation prompt (default: no) with a loud warning before generating; only safe when no production app writes to this database.
🧰 Tools
🪛 LanguageTool

[grammar] ~63-~63: Use a hyphen to join words.
Context: ...eploy gate. The flag prints a default-no confirm with a loud warning before gener...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-cli/SKILL.md` at line 63, Replace the awkward phrase "default-no
confirm" in the sentence explaining the --complete-rollout flag with a clearer
wording such as "confirmation prompt (default: no)"; update the sentence that
currently reads "The flag prints a default-no confirm with a loud warning before
generating" to "The flag displays a confirmation prompt (default: no) with a
loud warning before generating" so readers immediately understand the behavior.

Comment on lines 594 to 600
```
schema-added → dual-writing → backfilling → backfilled → cut-over → dropped
ENCRYPTION ROLLOUT → ⛔ deploy gate → ENCRYPTION CUTOVER
───────────────────── ──────────────────────
schema-add backfill historical rows
dual-write code switch reads to encrypted
db push (writes pending) drop plaintext column
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language hint to the fenced block to satisfy markdownlint (MD040).

The diagram block should declare a language (e.g., text) to keep docs lint-clean.

Proposed markdown fix
-```
+```text
 ENCRYPTION ROLLOUT  →  ⛔ deploy gate  →  ENCRYPTION CUTOVER
 ─────────────────────                     ──────────────────────
 schema-add                                backfill historical rows
 dual-write code                           switch reads to encrypted
 db push (writes pending)                  drop plaintext column
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 594-594: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-encryption/SKILL.md` around lines 594 - 600, The fenced diagram
block in SKILL.md lacks a language hint which triggers markdownlint MD040;
update the triple-backtick fence to include a language (e.g., add "text" after
the opening ```), so the block becomes ```text ... ```, ensuring the diagram
block is lint-clean without changing its contents.

@calvinbrewer calvinbrewer merged commit 9a568ee into main May 8, 2026
7 checks passed
@calvinbrewer calvinbrewer deleted the phased-plans branch May 8, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants