fix(cli): install agent skills in stash init, as its first step - #926
Conversation
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
🦋 Changeset detectedLatest commit: 02b7c18 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
…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
left a comment
There was a problem hiding this comment.
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
installSkillsStepis present and first in theSTEPSarray 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.
resolveTargetFlagis a genuine three-command fix: the absent-vs-valueless distinction (parseArgsfiles a trailing--targetunderflagsastrue,--target=undervaluesas'') was broken identically ininit,plan, andimpl, 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 (CliExitfor telemetry on init,process.exiton plan/impl) is the right shape.- The merge semantics are the subtle part done well:
context.jsongets the union across hops (fixing theplan --target agents-mderasure), while the setup prompt deliberately gets this hop's delivery only — and the test for "an earlier.claude/skillsinstall must not vouch for a failed.codex/skillswrite" pins the rulesLocation trap that the naive merged view would have introduced. That distinction is documented at the call site with the reasoning. - The
guessIntegrationasymmetry 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 blankPATHso detection can't pass or fail by accident of whose machine runs them. - The meta obligations are met:
stash-cliskill updated (seven-step list, flag table, theinit --target≠plan --targetdistinction 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.
Summary
CipherStash ships a set of agent skills — markdown instruction files (
stash-encryption,stash-drizzle,stash-cli, …) bundled inside thestashnpm 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.0that 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 initinstall 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/skillswhen theclaudebinary is onPATHor the project has a.claude/directory;.codex/skillsfor 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 barestash initagainst 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.Before this change that run delivered nothing.
Also in this PR
packages/cli/src/cli/registry.ts,init/index.ts— new optional flagstash init --target <claude-code|codex>names the skills destination and skips detection. Existing invocations are unaffected. Unlikeplan --target/impl --target, which choose an agent to hand the work off to, oninitit 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 detectedplus 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.jsontracks the skills a project has underinstalledSkills; previously each step wrote only its own result, so runningstash plan --target agents-mdafterwards (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--drizzleand--supabaseflags. Found by smoke test:stash init --drizzlein 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, matchinglintWiring.test.tsandintegrationSuiteCi.test.tselsewhere in the repo —init/index.tstransitively pulls in the plan command and the whole provider graph.init/steps/__tests__/install-skills.test.ts(new) — every detection case, the--targetoverride, 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--targetvalidation. 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 theinit --targetvsplan --targetdistinction..changeset/init-installs-agent-skills.md—stashpatch. This is a regression fix; the new flag is optional and additive.Verification
pnpm --filter stash test). Confirmed the suite no longer writes.claude/or.codex/into the repo.pnpm --filter stash test:e2e). Threedoctortests fail on my machine and pass in CI — they check@cipherstash/authplatform binaries and are unrelated to this change. Verified by stashing the branch, rebuilding, and re-running them against cleanmain: identical failures there.pnpm run code:check): no errors. The two warnings underinit/are pre-existingas unknownassertions inparse-plan.tsandread-context.ts.stash manifest --json:--targetresolves oninit, per the checkAGENTS.mdrequires for CLI skill changes.--drizzle; 6 into.codex/skillsfor--target codex; nothing written and nothing created when no agent is detected;--target emacsrejected before any work happens.%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.tsand theSTEPSarray ininit/index.ts— the ordering argument is the whole change, and the rest follows from it.handoff-helpers.tsand the twobuildStateFromContextedits 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 initstill writes no.cipherstash/setup-prompt.md. That file describes a handoff — which agent, planning or implementing, which rollout step — andinitperforms no handoff, so there is nothing coherent to write. Same family of gap, separate decision.