# Contributing
Thanks for wanting to make Whalesong Studio better. This page covers how the code is laid out, how to run it, and the few rules that keep a Harness plugin healthy.
## Setup
You need Node.js 22 or later. DeepSeek Harness itself is a dev dependency, so nothing else has to be installed.
```sh
npm install # installs dependencies; builds lib/ only when the checkout has none — a lib/ that is there is replaced by a release alone
npm run check # typecheck (host + browser), unit tests, build into .dev/stage (what `npm run dev` runs)
npm run release # the same checks, then lib/ is updated in place — what an installed or linked copy runs
npm run push # after a release is committed: main goes to GitHub, fast-forward only, looked over for credentials first
```
## Running it
```sh
npm run dev
```
boots the Harness Web UI on http://127.0.0.1:3199 with this checkout mounted as a bundle, inside an **isolated Harness home** (`.dev/home`) so your real `~/.dsh` is never touched. The launch URL with its access token is printed to the terminal.
Two things to do once:
1. **Give the dev home a model.** Either add a provider in the Web UI (*Settings → Models*), or write one into `.dev/home/profiles/web/cordis.patch.yml`:
```yaml
- id: llm-pi-ai
name: '@deepseek-ai/dsh-llm-pi-ai'
config:
providers:
my-provider:
api: openai-completions
baseURL: https://example.com/v1
apiKeyEnv: MY_API_KEY
models:
- { id: some-model, name: some-model, contextWindow: 128000 }
- id: agent-default-model
name: '@deepseek-ai/dsh-agent-default-model'
config: { provider: my-provider, model: some-model }
```
Or run `npm run dev -- --home ~/.dsh` to reuse a home that already has models (this one does write to it).
2. **Add a workspace.** The Harness puts its default workspace under `~/Documents/deepseek-harness/`. For throwaway books, add `.dev/workspace` through the sidebar's *add workspace* button.
Then:
- browser code: `npm run watch` in another terminal, reload the page;
- Host or agent code: rebuild and restart `npm run dev` (plugins are loaded once per process).
Options: `--port`, `--workspace
`, `--home `, `--profile `.
## Layout
```
src/
├── shared/ types, the RPC contract, text helpers — no Node, no DOM
├── core/ the engine, free of Harness imports so it runs under plain vitest
│ ├── project.ts one book on disk: locked writes, cache, directory watcher
│ ├── context.ts the context packer retrieval.ts BM25 + embeddings
│ ├── prompts.ts every prompt, in Chinese and English
│ ├── jobs.ts observable long-running work
│ ├── media/ speech, image and embedding providers; WAV utilities
│ ├── reader.ts what a reader is given — for another device, for a published site
│ └── pipelines/ write, revise, digest, plot, bible, review, import,
│ illustrate, audio, comic, play, export, outline, site
├── host/ the Harness side: service (`ctx.novel`), model adapter, RPC handlers, routes
├── agent/ mounted inside the agent preset: tools, prompt sections, the /novel command
└── client/ the Studio (React): store, views, stage players, tool rows, one stylesheet
skills/ workflow guides the showrunner can load
crew/ the skills of each role of the crew
recommended.json which models each service is used with, as data (see below)
cordis.patch.yml the bundle: the Host row and the agent preset
tests/ unit tests with a fake model; live tests behind NOVEL_LIVE=1
```
`scripts/build.mjs` bundles the entry points with esbuild: `lib/index.js` (the Host half — which carries the agent half too) and `lib/agent.js` (the entry a preset names; it only asks the running Host half to mount its agent half, so the two are always of one build) as ESM that keeps every `@deepseek-ai/*` import external, and `lib/client.js` in the Harness client-module format.
**`lib/` is what a running copy runs.** A profile that links this checkout loads `lib/` — and a Harness that is running swaps the Studio in its window when `lib/client.js` changes, while its Host half stays as loaded until it restarts. So nothing is built in place: a build goes to a directory of its own and is then put into `lib/` file by file, each one whole, only the files that changed (`lib/build.json` names the build; the Host compares the one it runs with the one installed and the Studio says so when they differ). Day to day, `npm run check` builds into `.dev/stage/`, a copy of the package the dev launcher runs; `npm run release` is the one command that touches `lib/`.
**The Host half can be replaced while DSH runs.** DSH loads `lib/index.js` once and keeps it; so that file is only a door (`src/host/door.ts`, `src/index.ts`) that puts up the Host half proper — `lib/host.js`, from `src/host/main.ts` — as a plugin of its own, and can take it down and put up the file that lies there now. That is what *Restart* on the Studio's banner does after a release. It works because everything the Host half registers, it registers on the context it is given: keep it that way — no listener, route, tool or timer that outlives that context, and no state in a module that a later build must not inherit (code that is the same in two builds is loaded once; `forgetPackageFiles()` exists for what is cached from disk). A change to the door itself still needs DSH restarted.
## Rules of the house
**An uncaught error in plugin code takes the whole Harness down.** Never leave a promise unhandled: `void promise.catch(...)`, or go through `controller.guard` / `useAction` in the UI and `JobManager` on the Host, which record failures instead of throwing them into the void.
**Cordis injection is strict.** A context can read only the services its plugin (or an enclosing `ctx.inject`) names. Optional services are read with `ctx.get('name')`.
**Preset plugins mount once per preset, not per agent.** Anything in `src/agent` must take the calling agent from the tool call (`exec.agent`) and keep no per-agent state in module scope.
**The core stays portable.** `src/core` and `src/shared` import nothing from the Harness; the Host adapts. That is what lets the pipelines be tested with a fake model in milliseconds.
**Every pipeline takes a signal and reports progress.** Long work runs as a job: pass `task.signal` to model and network calls, stream text with `task.job?.rewrite(...)`, report steps with `task.job?.progress(...)`.
**Models return almost-JSON.** Use `completeJson` (tolerant parsing, repair of truncated replies, one retry with the validator's complaint) and validate what you read with `asText` / `asList` / `asRecord`; never `JSON.parse` a reply directly.
**Prompts are bilingual.** Each prompt in `core/prompts.ts` has a Chinese and an English version, chosen by the book's language. Change both.
**UI text is bilingual and inline**: `const t = useT(); t('中文', 'English')`. No string tables.
**Styles** live in `src/client/styles.css`, are prefixed `nv-`, and take every colour from Harness theme tokens (`--dsw-alias-*`) so light and dark both work. Layout breakpoints are container queries: the Studio is often a 450 px sidebar tab.
**The browser bundle** may import only what the Harness seeds as shared modules (see `PLATFORM_MODULES` in `scripts/build.mjs`); everything else is inlined, so keep dependencies out of `src/client`.
**Keys are references.** Settings store the *name* of a credential, never its value.
**No model is named in the code.** Which model a service is used with, what is offered beside a model field, what a kind of endpoint falls back on, how large a model family's window is: all of it is `recommended.json`, read through `src/shared/recommended.ts`. To change a recommendation, edit the file, raise its `version`, set its `date` and say what changed in `notes` — authors who follow the recommendations get the change with the next version and are told what moved (`src/host/recommended.ts`). `tests/recommended.test.ts` holds the file to naming a model for every kind of endpoint the code can ask about.
## Tests
```sh
npm test # unit tests: project store, pipelines against a scripted model, text utilities
npm run typecheck
```
Live tests call a real OpenAI-compatible endpoint and print what the models wrote, so prompt changes can be judged by eye:
```sh
NOVEL_LIVE=1 NOVEL_LIVE_BASE_URL=https://example.com/v1 NOVEL_LIVE_API_KEY=… NOVEL_LIVE_MODEL=some-model \
npx vitest run tests/live.test.ts
```
Set `NOVEL_LIVE_KEEP=1` to keep the generated book for inspection.
When you change a prompt or a pipeline, run the live tests once and read the output. When you change the UI, look at it both as a page and as a sidebar tab, in light and dark.
## Pull requests
- One change per pull request, with the reasoning in the description.
- `npm run check` passes.
- New behaviour comes with a test where the core is concerned, and with a note in `CHANGELOG.md`.
- Contributions are licensed under LGPL-3.0-or-later, like the rest of the project.