Bundle
dsh-project-prompt
Private, per-project system-prompt rules for DeepSeek Harness — matched by git remote URL, repo path, or cwd prefix; worktree-aware; never committed to the repository.
- Source
- imroc
- stars
- 2 stars
- License
- MIT
- Updated
- Updated 10 days ago
Readme
# dsh-project-prompt
**English** | [中文](README.zh.md)
Private, per-project prompt rules for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).
Some agent instructions belong to your machine, not to the repository: environment-specific E2E procedures, internal-only endpoints, personal workflow preferences. `AGENTS.md` is committed and shared, so it is the wrong place for them. This plugin keeps such text **in `$DSH_HOME`, never in the repo**, and injects it into every session whose workspace matches a rule — including every subagent and every `git worktree` of the repository.
```mermaid
flowchart LR
subgraph repo["git repository (any clone / worktree / subdir)"]
wt1["main checkout"]
wt2["linked worktree A"]
wt3["worktree B"]
end
cfg["$DSH_HOME/cordis.patch.yml\nrules (machine-local, private)"]
plugin["dsh-project-prompt\nagent/session-start listener"]
sp["session system prompt\n(or first injected message)"]
cfg --> plugin
wt1 & wt2 & wt3 -->|match by remote / repo / path| plugin
plugin -->|section / inject| sp
```
## Features
- **Three matchers** — match sessions by git `remote` URL (any clone, anywhere on disk), by local main-repo path, or by plain cwd prefix. Keys within a rule are OR-combined.
- **Worktree-aware** — a linked worktree is traced back to its main repository through the `.git` file's `gitdir:` pointer, so a rule written once covers worktrees created later, and their subdirectories.
- **Two injection modes** — `section` (a stable system-prompt segment, KV-cache friendly, supports `{{cwd}}`/`{{model}}`) and `inject` (a first context message, no template interpolation — use it when the text contains literal `{{...}}` braces, e.g. Helm or Go templates).
- **Covers subagents** — subagents inherit the session cwd, so delegated work follows the same rules.
- **Fails loudly at load time** — malformed rules (missing `text`, unknown `{{var}}` in section mode) throw when the plugin loads, not on every request.
- **Zero dependencies** — a single ESM file importing only Node builtins; no build step, nothing to compile.
## Requirements
- DeepSeek Harness ≥ `0.1.1-rc.2` (uses the `agent/session-start` event, `systemPrompt.section`, and `agent.inject` extension points).
- Node.js ≥ 18 (DSH itself currently requires ≥ 22).
## Install
Install into a profile with DSH's plugin command:
```bash
dsh plugin --profile web add dsh-project-prompt
# or install straight from the GitHub repository:
dsh plugin --profile web add github:imroc/dsh-project-prompt
```
Restart DSH afterwards — bundles are composed at startup.
Uninstall:
```bash
dsh plugin --profile web remove dsh-project-prompt
```
## Configure
Rules live in `$DSH_HOME/cordis.patch.yml` (`~/.dsh/cordis.patch.yml` by default) — the machine-local layer that applies to **every profile** on the machine. Override the row the bundle installed:
```yaml
- id: project-prompt
config:
rules:
# Matches this repository wherever it is cloned, and in all worktrees.
- remote: github.com/my-org/my-project
text: |
## E2E testing for this project (environment-specific, private)
1. Test entry point: http://e2e.internal.example.net (internal only)
2. Run `make e2e-prepare` before the first E2E run.
3. On failures, check ... first.
# Matches by local main-repo path; the rule text contains literal
# {{...}} braces, so it must use inject mode.
- repo: /home/me/dev/another-project
mode: inject
text: |
Render values like {{ .Values.replicas }} are literal here.
# Matches any session started under a plain directory prefix.
- path: /home/me/dev
text: |
...
```
The file is watched for changes — saving it hot-reloads the plugin row (take effect for sessions started afterwards).
### Rule reference
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| `remote` | string | — | Git remote URL (origin) to match. Compared after normalization: scheme, `user@`, scp-style `:` separator, trailing `.git` and slashes, and case are ignored. `git@github.com:u/r.git` ≡ `https://github.com/u/r`. A bare `host/org/repo` suffix also matches. |
| `repo` | string | — | Local absolute path of the git **main** repository. Any linked worktree and any subdirectory of it matches (worktrees are traced back via the `.git` gitdir pointer). |
| `path` | string | — | Cwd prefix: the session workspace equals this directory or lives underneath it. |
| `text` | string | required | The prompt text to inject. |
| `mode` | `section` \| `inject` | `section` | Injection mode, see below. |
| `sectionName` | string | `project-prompt` (auto-increments when several section rules match one session) | System-prompt section name. |
| `order` | number | `50` | System-prompt section order (DSH conventions: `0` persona, `100–199` tool guidance). |
A rule needs **at least one** of `remote` / `repo` / `path`; several matching rules all apply.
### Injection modes
| | `section` (default) | `inject` |
| --- | --- | --- |
| Lands as | System-prompt segment on the agent-scoped `systemPrompt` service | First user-side context message (same path `AGENTS.md` content takes) |
| Visible | Every request of the session | Every request of the session |
| Template interpolation | Yes — `{{cwd}}` and `{{model}}` resolve; any other complete `{{...}}` group **throws** | No — braces are literal |
| Best for | Stable instructions; KV-cache-friendly | Text containing literal `{{...}}` (Helm/Go templates, Terraform, etc.) |
The plugin validates section text at load time: an unknown `{{var}}` fails the load with a message telling you to switch that rule to `mode: inject`.
## How it works
1. DSH emits `agent/session-start` synchronously before the first model request of every new session (fresh, resumed, after clear/compact — each publish mints a new agent scope).
2. The plugin reads `agent.session.header.cwd` and tests each rule; git repository identity is resolved by walking up from the cwd to `.git` (a file means a linked worktree, whose `gitdir:` pointer leads back to the main repository root and its `remote` URLs).
3. On match, the rule's text is registered on `agent.ctx` — the agent-scoped context — so it applies to that agent only and is discarded with it.
4. Every model request of that session re-assembles the system prompt (or re-reads the injected message), so the rule text is present throughout.
"Model-visible means logged": injected sections appear in the request headers recorded in the session log (`$DSH_HOME/sessions/…`), which is also how you verify a rule landed.
### Verify an installation
1. Check the row composed: `dsh --profile web --dump-config | grep -A3 project-prompt`.
2. Start a session in a matched directory and ask the model a question the rule text should influence.
3. Or inspect the recorded prompt: the session's `session.jsonl.zstd` contains the assembled system prompt with your section.
## Limitations
- Matching is identity-by-path or by-remote, not by content: two different clones of the same remote both match a `remote` rule (that is usually the point).
- Submodule directories (`.git` pointing into `.git/modules/…`) are not traced; use a `path` rule for them.
- Rules are static configuration — this plugin intentionally does not read files from inside the repository (that would reintroduce shared state). For workspace-local files, see DSH's built-in `AGENTS.md`/`CLAUDE.md` loading.
## Development
```bash
git clone github:imroc/dsh-project-prompt && cd dsh-project-prompt
npm test # node --test; git fixtures are created in a temp dir
node --check index.js
```
The plugin is a single dependency-free ESM file; the committed source is the shipped artifact (no build, no `prepare` script — which is also why git installs need no `pnpm allowBuilds` entry). See [AGENTS.md](AGENTS.md) for the constraints that keep it that way.
## License
[MIT](LICENSE) © [roc](https://imroc.cc)
Install
dsh plugin --profile web add github:imroc/dsh-project-prompt
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-project-prompt from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.