Skip to content

fix(cli): install agent skills in stash init, as its first step - #926

Merged
coderdan merged 3 commits into
mainfrom
fix/923-init-installs-skills
Aug 20, 2026
Merged

fix(cli): install agent skills in stash init, as its first step#926
coderdan merged 3 commits into
mainfrom
fix/923-init-installs-skills

Conversation

@coderdan

@coderdan coderdan commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

CipherStash ships a set of agent skills — markdown instruction files (stash-encryption, stash-drizzle, stash-cli, …) bundled inside the stash npm package. They teach a coding agent how to wire up CipherStash encryption, and they only work if they are copied into the user's project, where the agent will find them (.claude/skills/ for Claude Code, .codex/skills/ for Codex).

In stash@1.1.0 that copy never happened. stash init — the command that sets a project up, and the one an agent runs first — installed no skills for anyone, in any mode. The command reported success, so the failure was invisible: an agent asked to add encryption to a project got no guidance at all and had to improvise.

This makes stash init install the skills again, and makes it the first thing init does rather than the last.

Changes

The fix

  • packages/cli/src/commands/init/steps/install-skills.ts (new) — an init step that picks the destination from the detected agent (.claude/skills when the claude binary is on PATH or the project has a .claude/ directory; .codex/skills for Codex; both when both are present) and the skill set from the integration flags plus on-disk signals, falling back to the six base skills when neither is conclusive.
  • packages/cli/src/commands/init/index.ts — the step runs first in the pipeline, ahead of authentication.
  • packages/cli/src/commands/init/steps/build-schema.ts — tops the skill set up once the integration is definitively known. Only matters for a bare stash init against a Supabase-hosted database, where the integration is identified from a connection string init has not read yet.

Why first, not merely on the init path

Installing skills needs no network, no credentials and no database. The three steps it now precedes — authenticate, resolve-database, install-EQL — each need one of those, and each can fail and stop the run. Running first means the guidance survives a failure, which is exactly when an agent needs it: stash-cli, the skill covering how to recover from all three, is in every skill set.

$ stash init --supabase          # project with .claude/, no credentials available
◆  Installed 10 skills into .claude/skills/: stash-encryption, stash-supabase, …
■  Cannot resolve a region without a prompt. Pass --region <slug> …

Before this change that run delivered nothing.

Also in this PR

  • packages/cli/src/cli/registry.ts, init/index.ts — new optional flag stash init --target <claude-code|codex> names the skills destination and skips detection. Existing invocations are unaffected. Unlike plan --target / impl --target, which choose an agent to hand the work off to, on init it selects the destination only — init performs no handoff. That difference is spelled out in the flag description and the skill.
  • packages/cli/src/commands/init/index.ts — the end-of-run summary reports the outcome either way: an install count, or ○ No agent skills installed — no coding agent detected plus the command that will install them. It is printed before the EQL check so it also appears on a failing run, which is where it matters most. It never changes the exit code.
  • packages/cli/src/commands/init/lib/handoff-helpers.ts, write-context.ts, types.ts, plan/index.ts, impl/index.ts — record what was installed instead of overwriting it. .cipherstash/context.json tracks the skills a project has under installedSkills; previously each step wrote only its own result, so running stash plan --target agents-md afterwards (which installs no skill directories of its own) reset the field to empty on a project that had skills sitting on disk. Separate bug from the one above, but leaving it would let the same false-empty state come back one command later.
  • packages/cli/src/commands/init/steps/install-skills.ts — the integration guess consults the --drizzle and --supabase flags. Found by smoke test: stash init --drizzle in a project with no Drizzle config on disk installed the generic six skills instead of the Drizzle seven. Ignoring a flag is correct for generating the encryption client (don't scaffold Drizzle-shaped code off a flag alone) and wrong for skills, where the flag is the user naming the integration the agent should be taught.
  • packages/cli/src/commands/init/detect-agents.ts — probes for a .codex/ directory, the Codex counterpart of the existing .claude/ signal.

Tests

  • init/__tests__/steps-wiring.test.ts (new) — asserts the step is in the pipeline and is first. This is the test that would have caught the original regression. The skills module itself was never broken and all of its unit tests passed throughout the outage; what broke was that nothing called it. A step nothing invokes reads exactly like a step that works. It scans the source rather than importing it, matching lintWiring.test.ts and integrationSuiteCi.test.ts elsewhere in the repo — init/index.ts transitively pulls in the plan command and the whole provider graph.
  • init/steps/__tests__/install-skills.test.ts (new) — every detection case, the --target override, the targets that install no directories, the base-set fallback, and the top-up.
  • init/lib/__tests__/handoff-helpers.test.ts, write-context.test.ts (new) — the merge behaviour and the context-file baseline.
  • init/__tests__/init-command.test.ts — mocks the new step like every other one, and covers the summary lines and --target validation. That suite mocks the whole pipeline, so an unmocked real step ran against the package root and wrote .claude/skills/ into the repo on every unit run.

Docs

  • skills/stash-cli/SKILL.md — the init step list (seven steps now), the flag table, and the init --target vs plan --target distinction.
  • .changeset/init-installs-agent-skills.mdstash patch. This is a regression fix; the new flag is optional and additive.

Verification

  • Unit tests: 1380 passed, 26 skipped (pnpm --filter stash test). Confirmed the suite no longer writes .claude/ or .codex/ into the repo.
  • End-to-end tests: 108 passed (pnpm --filter stash test:e2e). Three doctor tests fail on my machine and pass in CI — they check @cipherstash/auth platform binaries and are unrelated to this change. Verified by stashing the branch, rebuilding, and re-running them against clean main: identical failures there.
  • CI: all 9 checks pass — Biome, Node 22, Node 24, Bun, E2E, WASM E2E (Deno), CodeQL, OSV.
  • Biome (pnpm run code:check): no errors. The two warnings under init/ are pre-existing as unknown assertions in parse-plan.ts and read-context.ts.
  • Command surface cross-checked against stash manifest --json: --target resolves on init, per the check AGENTS.md requires for CLI skill changes.
  • Manual smoke tests against the built CLI in throwaway directories: 10 Supabase skills installed and then auth fails (the reordering doing its job); 7 skills for --drizzle; 6 into .codex/skills for --target codex; nothing written and nothing created when no agent is detected; --target emacs rejected before any work happens.
  • Commit is signed (%G? = G).

Behaviour change to be aware of: because skills now install before anything else, a run cancelled at the first prompt leaves a skills directory behind where it previously wrote nothing. The files are harmless and idempotent — the same ones a successful re-run writes — but it is a real difference and the changeset says so.

Related

Closes #923.

Splits out of #665 (item 4). The behaviour was last correct in 1.0.0-rc.4; the init/plan/impl restructure is what dropped it.

Review notes

Start with install-skills.ts and the STEPS array in init/index.ts — the ordering argument is the whole change, and the rest follows from it.

handoff-helpers.ts and the two buildStateFromContext edits are a separate bug I found while reading, not part of the reported one. If you would rather they went in their own PR, say so and I will split them.

Deliberately not in scope: stash init still writes no .cipherstash/setup-prompt.md. That file describes a handoff — which agent, planning or implementing, which rollout step — and init performs no handoff, so there is nothing coherent to write. Same family of gap, separate decision.

Since 1.0.0-rc.4 the only callers of `installSkills()` were the `plan` and
`impl` handoff steps, which `stash init` never reaches — so `stash@1.1.0`
installed no `stash-*` skills for anyone, in any mode. Init printed a green
summary and wrote a plausible `.cipherstash/context.json` with
`installedSkills: []`, which is why it survived a release.

Add an `install-skills` step at the head of the init pipeline. It resolves the
destination from `detectAgents()` (`.claude/skills` for the `claude` binary or
a `.claude/` directory, `.codex/skills` for Codex, both when both are present)
and the skill set from the provider flags plus the cwd signals, falling back to
`BASE_SKILLS` when neither is conclusive. `build-schema` tops the set up once
`detectIntegration` has answered, which only matters for a bare `stash init`
against a Supabase-hosted URL.

Ordering is the fix, not a detail. Installing skills needs no network, no
credentials and no database; authenticate, resolve-database and install-eql
each need one and each can exit non-zero. Running first means the guidance
survives those failures — and `stash-cli`, which covers recovering from them,
is in every skill set. A run cancelled at the first prompt now leaves the
skills directory behind where it previously wrote nothing.

Also in this change:

- `stash init --target <claude-code|codex>` names the destination and skips
  detection. Optional; unlike `plan --target` / `impl --target` it selects the
  skills destination only, since init performs no handoff.
- The init summary reports the outcome either way, including on the failing
  EQL summary, rather than leaving a silent empty list.
- `buildContextFile` reads `state.skills` instead of hardcoding `[]`, and
  `writeArtifacts` merges deliveries across hops instead of overwriting. A
  later `stash plan --target agents-md` installs no directories of its own and
  used to erase from `context.json` the skills sitting on disk.
- `guessIntegration` consults `--drizzle` and `--supabase`, which
  `detectIntegration` deliberately does not. Right for skills, where the flag
  is the user naming the integration; wrong for the encryption client.
- `detect-agents` gains a `.codex/` probe.
- `init-command.test.ts` mocks the new step like every other one — the real
  one copies into `process.cwd()`, which in that suite is the package root.

`steps-wiring.test.ts` guards the pipeline itself. Every unit test of the
skills module passed throughout the outage; what broke was reachability, so
the guard asserts the step is in `STEPS` and is first.

Fixes #923
@coderdan
coderdan requested a review from a team as a code owner August 19, 2026 11:53
@changeset-bot

changeset-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 02b7c18

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/stack Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/stack-prisma Patch
@cipherstash/wizard Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-example Patch

Not sure what this means? Click here to learn what changesets are.

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

Comment thread packages/cli/src/commands/init/index.ts Outdated
Comment thread packages/cli/src/commands/init/lib/handoff-helpers.ts Outdated
…own handoff

Two review findings on #926.

`--target` presence is now tested separately from its value. `parseArgs`
files a trailing `--target` (nothing followed it) under `flags` and `--target=`
under `values` as an empty string, so the previous truthiness test on the value
treated both as "flag absent": init fell through to auto-detection and could
write skills to a directory the user never chose, having been asked for
something specific. Both forms are rejected now, with a message that
distinguishes a missing value from an unknown one. Uses the same
`flags[…] === true || Object.hasOwn(values, …)` idiom as the retired-flag
checks directly above it.

`writeArtifacts` renders the setup prompt from this handoff's delivery again,
and keeps the merge for `context.json` only. The two answer different
questions. `installedSkills` is a flat list with no destination attached, so a
union across hops is the honest reading of "which skills does this project
have". The prompt instead tells the agent being launched right now where to
read the rules, and `rulesLocation` derives that directory from the handoff
choice — so feeding it the merged view let skills installed under
`.claude/skills` by an earlier `stash init` satisfy the "installed" test for a
Codex handoff whose own copy into `.codex/skills` had failed. The prompt then
pointed Codex at a directory that was never written, and the merge's
failure-filtering hid the failure that caused it.

Both paths are regression-tested; the prompt test was confirmed to fail
against the previous behaviour rather than pass vacuously.
`plan` and `impl` carried the same valueless-`--target` hole `926` fixed in
`init`: `parseArgs` files a trailing `--target` under `flags` and `--target=`
under `values` as an empty string, and testing the value for truthiness alone
read both as "flag absent". The command then did whatever it does with no flag
— for `plan` and `impl` that degrades to the interactive picker or the "no
agent selected" hint, which is milder than init writing skills to a directory
the user had just declined by naming another, but it is still silence where
the user asked for something specific.

The validation had been hand-copied into all three commands, which is how two
of them kept the bug after the third was fixed. Extract `resolveTargetFlag`
into `how-to-proceed.ts`, beside `HANDOFF_CHOICES` and `resolveTarget`, where
the target vocabulary already lives. It takes both halves of the parsed argv,
so it can tell "absent" from "present but unusable", and returns the message
rather than printing it — exit mechanics stay with the caller, since `init`
unwinds through `CliExit` for the telemetry flush while `plan` and `impl` call
`process.exit` directly.

Also fixes two type errors this branch had introduced but no runner caught:
the `AgentEnvironment` fixture in `how-to-proceed.test.ts` predates the
`codexDir` probe, and the `env()` helper in `install-skills.test.ts` was typed
`Partial<AgentEnvironment['cli' & 'project']>` — an intersection of two string
literals is `never`, so the helper's parameter checked nothing. Vitest does
not typecheck, so both passed while meaning nothing.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve. The fix is right, the ordering argument is right, and — most importantly for a bug whose essence was reachability — the regression guard is the correct kind: steps-wiring.test.ts pins the pipeline, not the module, which is exactly the test whose absence let a working installSkills() ship unreachable for a release. CI green.

What I verified:

  • The wiring test would have caught #923: it asserts installSkillsStep is present and first in the STEPS array by scanning the source, matching the repo's established pattern (lintWiring.test.ts, integrationSuiteCi.test.ts) and for the stated reason — an import-based assertion would drag in the whole provider graph. The reachability lesson ("a step nothing invokes reads exactly like a step that works") is now encoded where it can't be forgotten.
  • First-in-pipeline is correctly reasoned and honestly costed: skills need no network/credentials/database while the three steps behind it each do, so guidance survives the failures it exists to help recover from. The one behaviour change that falls out (a cancelled run leaves the skills directory behind) is stated in the changeset rather than discovered by a user.
  • resolveTargetFlag is a genuine three-command fix: the absent-vs-valueless distinction (parseArgs files a trailing --target under flags as true, --target= under values as '') was broken identically in init, plan, and impl, and the hand-copied validation is exactly how two would have kept the bug after one was fixed. The shared helper plus the exit-mechanics note (CliExit for telemetry on init, process.exit on plan/impl) is the right shape.
  • The merge semantics are the subtle part done well: context.json gets the union across hops (fixing the plan --target agents-md erasure), while the setup prompt deliberately gets this hop's delivery only — and the test for "an earlier .claude/skills install must not vouch for a failed .codex/skills write" pins the rulesLocation trap that the naive merged view would have introduced. That distinction is documented at the call site with the reasoning.
  • The guessIntegration asymmetry is defensible and tested: flags are conclusive for skill selection but not for client scaffolding, with the rationale (the flag is the user naming what to teach) and the precedence rules (drizzle > supabase on combined runs, matching EQL-migration routing) both stated and pinned.
  • Test hygiene: the init-command suite mocks the new step so the real one stops writing .claude/skills/ into the repo on every run, and the step tests blank PATH so detection can't pass or fail by accident of whose machine runs them.
  • The meta obligations are met: stash-cli skill updated (seven-step list, flag table, the init --targetplan --target distinction stated twice where it will be read), changeset present, and the flag resolves in the registry.

One non-blocking copy nit: when the user explicitly passes --target agents-md|lovable|wizard (accepted, installs no directories — documented in three places, good), the summary line still reads "no coding agent detected", which isn't why nothing was installed in that case. The suggested remedy line is correct either way (plan --target <their choice> is exactly what inlines the skills), so this is a wording tweak for whenever the file is next touched, not a hold.

@coderdan
coderdan merged commit 67b137a into main Aug 20, 2026
9 checks passed
@coderdan
coderdan deleted the fix/923-init-installs-skills branch August 20, 2026 01:21
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.

stash init no longer installs any stash-* skills — agent-driven setup gets zero guidance

2 participants