# Deployment and Mounting

English | [中文](deploy.md)

This page explains how to deploy `dsh-github-reviewer` into a DeepSeek Harness instance. See [Configuration](./config.en.md) for the config reference and [Architecture](./architecture.en.md) for how it works.

## Deployment requirements

The plugin injects the harness `agents`, `sessions`, and `agentDefaultModel` services, so the deployment must mount the agent-loop family. A minimal working composition needs at least these rows beside `github-reviewer` (see [cordis.yml.example](../cordis.yml.example) for the full annotated example):

```yaml
- id: llm-deepseek          # some LLM adapter
  name: '@deepseek-ai/dsh-llm-deepseek'
  config: { thinking: enabled, models: [{ id: deepseek-chat, contextWindow: 128000 }] }
- id: agent-spine           # agent loop + system-prompt assembly + tool pipeline
  name: '@deepseek-ai/dsh-agent-spine-demo'
  config:
    agents: [{ id: main, provider: deepseek-official, model: deepseek-chat, cwd: !!js process.cwd() }]
- id: persistence           # restart-safe per-PR sessions
  name: '@deepseek-ai/dsh-session-persistence-jsonl'
  config: { root: './.sessions' }
- id: storage               # storage hub (storage-json and storage-domain both depend on it)
  name: '@deepseek-ai/dsh-storage'
- id: storage-json          # cursor storage backend (JSON files)
  name: '@deepseek-ai/dsh-storage-json'
  config: { root: './.storage' }
- id: storage-domain        # cursor storage domain (dsh_github_reviewer)
  name: '@deepseek-ai/dsh-storage-domain'
  config: { backend: json }
- id: agent-default-model   # default model selection for every review agent
  name: '@deepseek-ai/dsh-agent-default-model'
  config: { provider: deepseek-official, model: deepseek-chat }
```

- **The default model is not plugin-configured**: with `review.models` left empty, every review agent uses the deployment's default model selection (`agentDefaultModel`), provided by `@deepseek-ai/dsh-agent-default-model` on its own (config requires `{ provider, model }`), not by the agent-spine family. Configure `review.models` for a candidate list (see [Configuration](./config.en.md)).
- **Unsatisfied dependencies silently deactivate the plugin**: when a cordis dependency is missing, the fiber stays PENDING forever and the plugin never activates — so the `agent-default-model` row and the storage rows (`storage` hub, `storage-json` backend, `storage-domain`) added above are required.
- **The cursor needs the storage domain**: the `dsh_github_reviewer` domain is provided by `@deepseek-ai/dsh-storage-domain`, which needs a backend (`@deepseek-ai/dsh-storage-json` or `@deepseek-ai/dsh-storage-sqlite`) routed in the storage-domain config (e.g. `backend: json` or `backend: sqlite`). The plugin fails loudly at load without it.
- **Without `sessionPersistence`**: the reviewer still works, but PR sessions are memory-only — after a restart the loop starts each PR from a fresh session.
- **With `sessionPersistence`**: every turn is checkpointed, and the reviewer resumes the persisted PR session on restart (it never creates a second session for the same PR).
- PR sessions live in the same session store as interactive sessions, so reviews are visible and replayable in the harness session UI.

## Install and peer dependencies

```sh
npm install dsh-github-reviewer
```

Peer dependencies: every `@deepseek-ai/*` package the plugin touches — `@deepseek-ai/cordis`, `@deepseek-ai/dsh-agent`, `@deepseek-ai/dsh-agent-default-model`, `@deepseek-ai/dsh-llm`, `@deepseek-ai/dsh-session`, `@deepseek-ai/dsh-session-persistence`, `@deepseek-ai/dsh-storage-domain`, `@deepseek-ai/dsh-system-prompt`, `@deepseek-ai/dsh-tools`, and `@deepseek-ai/schemastery`. They are declared as peers on purpose: the harness installation already provides them, and installing a second copy into the profile breaks the whole process — the tool scheduler looks up a module-private symbol on the shared `tools` service, and a duplicate `@deepseek-ai/dsh-tools` instance makes that lookup return `undefined`, crashing every session's first tool call with `Cannot read properties of undefined (reading 'prepare')`. Only `@modelcontextprotocol/sdk` and `zod` are installed as real dependencies.

## Enabling on a running DSH instance

Assume the instance profile lives at `$DSH_HOME/profiles/web` (`DSH_HOME` defaults to `~/.dsh`).

The plugin is loaded through the harness **profile composition**: the composed tree is the official bundle layer (`dsh-base`, `dsh-web-app`, declared in the profile's `dsh.profile.bundles`), then your `cordis.patch.yml`, then any `--patch` overlays from the launcher. A standard web profile already provides every service the plugin needs — **nothing extra to mount**:

| Service the plugin needs | Provided by | Where |
|---|---|---|
| `agents` / `sessions` | `@deepseek-ai/dsh-agent` / `dsh-session` | dsh-base bundle |
| `agentDefaultModel` | `@deepseek-ai/dsh-agent-default-model` (model chosen in `settings.yaml`) | dsh-base bundle |
| `sessionPersistence` | `@deepseek-ai/dsh-session-persistence-jsonl` | dsh-base bundle |
| `storageDomain` (cursor) | `@deepseek-ai/dsh-storage-domain` + `dsh-storage-json` backend | dsh-web-app bundle |

**1. Install the GitHub MCP server** (the official Go server; its tool names match the guard):

```sh
# Linux x86_64; substitute the asset name for other architectures
curl -sL https://github.com/github/github-mcp-server/releases/latest/download/github-mcp-server_Linux_x86_64.tar.gz \
  | tar -xz -C ~/.local/bin github-mcp-server
github-mcp-server --version
```

A container works too (`ghcr.io/github/github-mcp-server`); see the commented `mcp` block below.

**2. Install the plugin into the profile**:

```sh
# Official way: the dsh CLI forwards to pnpm inside the profile directory
dsh plugin --profile web add dsh-github-reviewer

# Equivalent manual way:
cd "$DSH_HOME/profiles/web"
npx pnpm add dsh-github-reviewer
ls node_modules/dsh-github-reviewer/lib/index.js   # confirm the install
```

The profile's `pnpm-workspace.yaml` sets `autoInstallPeers: false`, so pnpm installs only the plugin plus its real dependencies (`@modelcontextprotocol/sdk`, `zod`); every `@deepseek-ai/*` peer resolves from the harness installation, keeping a single copy of each package in the process (see the peer-dependency note above).

**3. Configure the plugin in `$DSH_HOME/profiles/web/cordis.patch.yml`**.

The package declares `dsh.bundle`, so `dsh plugin` automatically adds it to the profile's bundle list. The bundle already registers an enabled `id: github-reviewer` instance with `uiSettings: true`; the profile patch only needs to complete its runtime config by id, without another insert or either repeated default. Complete authentication and MCP configuration before the next start or the instance fails activation loudly; `repositories` may remain empty, which only leaves the poller idle:

```yaml
- id: github-reviewer
  config:
    name: personal
    # Either the GitHub App triple (appId/installationId/privateKeyPath)
    # or a personal access token. Avoid a literal token in this file —
    # read it from the environment with a !!js expression instead:
    # personalAccessToken: !!js process.env.GITHUB_PAT
    personalAccessToken: 'github_pat_...'
    repositories:
      - 'owner/repo'
    mcp:
      command: 'github-mcp-server'
      args: ['stdio', '--tools=pull_request_read,get_file_contents,pull_request_review_write,add_comment_to_pending_review']
      # Container variant (-e takes the variable name only; the plugin
      # injects the value into the process env and docker forwards it):
      # command: 'docker'
      # args: ['run', '-i', '--rm', '-e', 'GITHUB_PERSONAL_ACCESS_TOKEN', '-e', 'GITHUB_HOST',
      #        'ghcr.io/github/github-mcp-server', 'stdio',
      #        '--tools=pull_request_read,get_file_contents,pull_request_review_write,add_comment_to_pending_review']
```

Multiple accounts = another instance with the same `name` inside the same `- insert:` list (different `id`), each running its own poll loop. Because `uiSettings` defaults to `true`, added accounts must explicitly set `uiSettings: false`; only the default instance may own the fixed Web settings namespace.

When the Web profile supplies settings and the matching Client slot, the Settings page shows the GitHub reviewer card. Its repository fields list repositories accessible to the same PAT or current App installation while always retaining manual input; saving remains available if the catalog request fails. These are optional injected companions: missing settings, workspace, or Client UI dependencies leave only that companion pending and do not block the Host reviewer. Saving the card asynchronously restarts only the reviewer's internal runtime, never the whole DSH process.

**4. Create a PAT** (PAT mode): GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens, scoped to the target repository only, with permissions: Contents: Read, Pull requests: Read & Write, Issues: Read & Write, Checks: Read (Metadata is implicit).

**5. Restart the instance and verify**:

```sh
dsh web
dsh --profile web --dump-config   # print the composed plugin tree; confirm the github-reviewer row
```

The startup log should show `starting github account=personal repos=1`; open PRs receive a COMMENT review within one poll interval, and commenting `/bot <question>` on a PR talks to the reviewer. Override the default `github-reviewer` row directly; only additional account rows need an `- insert:` wrapper.

Once enabled, review/chat sessions are filed under an auto-registered `GithubReviewer` workspace. Through dependency injection, the plugin waits for the `workspaceRegistry` service published by `@deepseek-ai/dsh-workspace`; after that service finishes initialization, a companion fiber creates the directory and registers the workspace with no polling or retry. In compositions without the service, the companion fiber stays idle and performs no directory or registration work, while the reviewer remains active. Adjust with `workspaceDir` / `workspaceTitle`. Pre-existing sessions stay in their old workspace; only new sessions use the new directory.
