DSH plugin: pnpm clean fails on rc.2 over outDir /types
On a fresh checkout of dsh-v0.1.7-rc.2 (477b4f4205), running pnpm run clean fails immediately with exit code 1 — the error is clean: expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types. The cause is a newly added tsconfig.desktop-keyboard-tests.json in rc.2 (commit 6a82709a2b) declaring an outDir that does not end in /types and that enters the root project's reference graph via tsconfig.client.json; scripts/clean.ts's buildOutputDirectories() accepts only two shapes of outDir and throws for everything else (#8127). The most ironic part: the very point of the clean script is to "clean up the stale lib/ left by the previous version", and it breaks in exactly the scenario that most needs that ability. The issue has been fixed on master by a08472e and shipped with dsh-v0.2.0-rc.1; this article goes "triage first → mechanism → three fixes → one leftover that survives the fix", and includes a copy-pasteable manual cleanup checklist.
Triage first: this is a "planning-stage" failure, not an "execution-stage" failure
In one sentence: this error is thrown while reading tsconfig and computing "what to delete", and it has nothing to do with whether those directories exist on disk or whether you have permissions — so it will not go away because you manually deleted some subdirectory first.
Separating the two failure modes saves a round of wasted attempts:
| Criterion | Planning-stage failure (this case) | Execution-stage failure |
|---|---|---|
| When it errors | Immediately, before deleting anything | Half-way through deleting |
| Error text | semantic validation of tsconfig / outDir | file occupancy, permissions, paths |
| Can deleting the directory first bypass it | No | often yes |
| Fix | change the config or the script | kill the process / elevate / clear the occupant |
The evidence here is direct: the error text is expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types, which is an assertion about a config value, not about the filesystem. So every attempt of the form "delete that directory then run" is pointed in the wrong direction.
Mechanism: clean's planner recognizes only two outDir shapes
In one sentence: buildOutputDirectories()'s only legal inputs are "basename is types" and "the native entry's output directory"; any other name throws outright — and rc.2's newly added keyboard-test tsconfig is exactly a third name, and it is on the reference graph.
Three conditions hold at once, and only then does the failure occur:
-
There is a non-
/typesoutDir.tsconfig.desktop-keyboard-tests.jsondeclares:jsonc{ // emitDeclarationOnly keyboard-test fixture "outDir": "lib/desktop-keyboard-test-types" } -
It is visible to the root project's reference graph. That tsconfig is referenced by the root project via
tsconfig.client.json— anything on the graph gets checked by clean, whether it serves product code or a test fixture. -
The script has zero tolerance for unknown shapes.
scripts/clean.ts'sbuildOutputDirectories()accepts exactly two shapes:text① basename is types (the regular type-artifact directory) ② the native entry's outDir (already special-cased) any other name → throw, not skip or warn
So the seemingly harmless build change "add a tsconfig for tests" turns clean into a global failure.
Why "warn and skip" is not the answer
Because to clean an "unrecognized outDir" is a state of unclear meaning. Skipping it may leave stale artifacts behind; deleting it may remove something that should not be removed. Throwing is the conservative choice — the problem is that it escalates "the naming of one test fixture" into "the whole clean is unusable". The upstream fix is therefore not to relax the validation but to eliminate the third shape at its root (see fix one).
Why this bug deserves its own article
It fits three traits of "a build script that is defensible yet miserable to use":
- It only blows up on a fresh checkout. Fresh checkout +
pnpm installis clean's canonical scenario — you want to clear thelib/left by the previous version. Environments where you have rebuilt locally over and over, with the directory long since cleared, may never hit it. - The symptom is the opposite of the intent. Clean exists to "clear stale artifacts", and it fails precisely when there are stale artifacts to clear.
- The error message points at the config, not the behavior. Whoever sees
outDirreflexively wants to change the outDir, but the right direction is to delete that tsconfig (see the FAQ).
Fixes and leftover: upgrade / cherry-pick / manual cleanup, and the directory that survives
Fix one (recommended): upgrade to dsh-v0.2.0-rc.1
In one sentence: the issue was fixed on master by a08472e (fix(build): fold desktop keyboard tests into client typecheck) and shipped with dsh-v0.2.0-rc.1.
What the fix does:
- Deletes
tsconfig.desktop-keyboard-tests.json; - Folds those files into
tsconfig.client.json's type checking; - So the only outDir in the project graph not ending in
/typesis the native entry'slib, whichscripts/clean.tsalready special-cases.
Verification side by side on a fresh clone:
git checkout dsh-v0.1.7-rc.2 && pnpm run clean
# clean: expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types # exit 1
git checkout dsh-v0.2.0-rc.1 && pnpm run clean
# clean: already clean # exit 0
This is the least-effort route, and the one the production team leans toward: do not change build scripts in the middle of a release wave; carry it naturally with the next pinned version upgrade.
Fix two (stay on rc.2): cherry-pick the fix commit
In one sentence: if you must pin to rc.2, the fix picks cleanly and clean exits 0 right after.
git cherry-pick a08472e982
pnpm run clean
When it applies and when it does not:
- Applies: you are already pinned to rc.2 and need a working clean;
- Does not apply: you are inside a release window and do not want to introduce a build-script change — then use fix three.
Fix three (no version change): manual cleanup from a checklist
In one sentence: this is the temporary approach documented by both upstream and the production team — manually delete the gitignored lib/ directories and the root-level *.tsbuildinfo, equivalent to running clean's plan by hand.
The order and results the reporter verified:
- Manually delete the various gitignored
lib/directories; - Delete the root-level
*.tsbuildinfo(do not skip this: if TS's incremental-build cache file is not cleared, the next build may still follow the old graph's decisions); - Afterwards
pnpm run buildexits 0, producing 343 client artifacts.
This checklist is reliable because it replicates what clean was going to do, only swapping "computation" for "manual enumeration". Its cost is that it must be maintained by hand as versions change — so the production team's stance is "use it for now and swap it out at the next version upgrade".
One leftover after the fix: that directory survives
In one sentence: the fix deletes the tsconfig, so clean no longer knows lib/desktop-keyboard-test-types exists; if rc.2 already generated it, it will survive pnpm run clean.
This is fix one's only side effect, and it deserves its own reminder (confirmed by measurement in the discussion: after printing already clean the folder is still there):
# one-time manual delete
rm -rf lib/desktop-keyboard-test-types
# PowerShell
Remove-Item -Recurse -Force lib\desktop-keyboard-test-types
Deleting it once is enough — the config is gone, so it will not be regenerated. But if you built with rc.2 before upgrading, be sure to remember this step, otherwise a stale type directory lingers in your workspace forever and clean will never handle it for you again.
Troubleshooting notes
- First check whether the error is "config semantics" or "filesystem". The former means change the config, the latter means clear the occupant; this case is the former, so deleting directories first does nothing.
- Do not rename that outDir. It serves a keyboard-test fixture; renaming it to
lib/typesmakes it collide with the real type directory and overwrite each other — upstream's direction is to delete this tsconfig. - Check whether a newly added tsconfig is on the project reference graph. As long as the root project can reference it, every script that runs off the reference graph (clean, build, incremental checks) will see it.
- Be careful when a build script has zero tolerance for unknown input. Throwing is more conservative than skipping, but it escalates "the naming of one fixture" into "the whole chain is unusable" — after adding a config, it is worth running clean once.
- A fresh checkout is the canonical scenario for this class of problem. An environment rebuilt locally over and over may mask it; verify fixes on a fresh clone.
- Do not miss
*.tsbuildinfoin manual cleanup. Deleting onlylib/while leaving the incremental cache means the next build may still follow the old decisions. - Remember to manually delete the leftover directory when upgrading.
lib/desktop-keyboard-test-typeswill not be automatically cleaned by the fix. - Inside a release window, prefer "wait and upgrade later". The production team's trade-off makes sense: pin to rc.2 + manual cleanup is more stable than cherry-picking a build-script change mid-wave.
- Confirm cleanliness before cherry-picking. That commit picks cleanly (verified in the discussion), but after picking you should still run
pnpm run cleanonce to confirm exit 0. - Put "does clean pass on a fresh checkout" on your upgrade checklist. CI may not cover this class of problem, but one fresh clone reveals it.
The value of this example is not how hard the fix is, but the easily overlooked coupling it exposes: the project reference graph is a shared input to every TS-based build script, and the side effect of adding one tsconfig to the graph can land on a script that has nothing to do with it. If you also maintain a monorepo with project references, run clean and build after adding any new tsconfig, and write "does clean exit 0 on a fresh clone" into your upgrade checklist — this class of failure only reproduces on a new checkout and is hard to run into through day-to-day development. Centralizing plugin install, upgrade, and system logs in DSH Plugin Hub makes upgrade-class problems easier to locate.

Source: Discussion #8127, deepseek-ai/deepseek-harness.
FAQ
Because what triggers it is **the newly added tsconfig in the project reference graph**, not whether you happen to have build artifacts locally. tsconfig.desktop-keyboard-tests.json enters the root project's reference graph through tsconfig.client.json in rc.2, and scripts/clean.ts's planner throws the moment it sees its outDir not ending in /types. If your local lib/ was already cleared by an older version or by other means, you may not have hit it; but in the canonical clean scenario — **fresh checkout, with lib/ left by the previous version** — it fails every time.
Not recommended. lib/desktop-keyboard-test-types is the output directory of that **emitDeclarationOnly keyboard-test fixture**; renaming it to lib/types collides with the real type-artifact directory and they overwrite each other. The upstream fix direction is the opposite: **delete that standalone tsconfig**, fold those files into tsconfig.client.json's type checking, and leave only two shapes in the graph — "ends in /types" and "the native entry's lib", the latter of which scripts/clean.ts already special-cases.
No — the error is unrelated to whether that directory exists; it is thrown in the **tsconfig-reading planning stage**, not while walking files. To stay on rc.2, the right move is to cherry-pick a08472e982 or clean manually from the checklist.
It is a leftover from the fix: the fix **deletes that tsconfig**, so clean no longer knows that directory exists. If some rc.2 build already generated it, it **survives pnpm run clean** (measured and confirmed: after "already clean" the directory is still there). Delete it once by hand and it will not be regenerated.
Follow the production team's trade-off in the discussion: **keep the documented manual cleanup flow** (delete the gitignored lib/ directories plus the root-level *.tsbuildinfo), do not cherry-pick mid-wave, and carry it along with a08472e at the next pinned version upgrade. This avoids introducing a build-script change inside a release window.
Related Terms
- scripts/clean.ts / buildOutputDirectories()
- DSH's clean script uses it to compute "which build output directories to delete". It accepts only two shapes of `outDir`: ① basename is `types`; ② the native entry's output directory. **Any other name throws outright** rather than being skipped or warned about — which is why a newly added test tsconfig can break clean wholesale.— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
- project references graph
- TypeScript's `references` mechanism organizes multiple tsconfigs into a directed graph, and the root project aggregates the subprojects through it. `tsconfig.desktop-keyboard-tests.json` enters clean's scan scope precisely because it can be referenced by the root project via `tsconfig.client.json` — **anything on the graph gets checked, whether it serves product code or a test fixture**.— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
- emitDeclarationOnly fixture
- `tsconfig.desktop-keyboard-tests.json` is a config that emits only `.d.ts` (no JS), used for keyboard-test-related type fixtures, with its `outDir` named `lib/desktop-keyboard-test-types`. It is the trigger of this failure: it is neither of clean's two legal shapes, and it really is attached to the project reference graph.— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
- a08472e (the fix commit)
- `fix(build): fold desktop keyboard tests into client typecheck`. It deletes `tsconfig.desktop-keyboard-tests.json` and folds those files into `tsconfig.client.json`'s type checking, leaving the native entry's `lib` as the only outDir in the project graph that does not end in `/types` (which clean already special-cases). The commit ships with **dsh-v0.2.0-rc.1** and also cherry-picks cleanly back onto rc.2.— https://github.com/deepseek-ai/deepseek-harness/discussions/8127