| name | wish-crafting |
| description | Use when the user asks for a lasting change to DeepSeek Harness itself — a new tool, slash command, event listener, boot behavior, or UI tweak — rather than a change to their project files. Covers prototyping with cordis_define, keeping the result with genie_keep, and what changes when code leaves the sandbox. Triggers include "add a tool", "every time I ...", "make the harness ...", "I want a command that ...", "给 harness 加个", "许个愿". |
Crafting a wish
A wish is a plugin the user gets to keep. Two things distinguish this work from ordinary
coding: the code runs inside the harness rather than beside it, and the user must restart
before anything happens.
Decide first: is this actually a wish?
A wish changes the harness. If the request changes the user's project, write project files
instead — a wish is the wrong shape and much harder to review.
| Request | Wish? |
|---|
| "every time I say X, do Y" | yes — a listener or command |
| "give yourself a tool that queries our staging API" | yes — a tool |
| "show token spend in the corner" | yes — a client half (mounting it is manual, say so) |
| "add a lint step to this repo's CI" | no — project files |
| "remember that our deploy port is 8080" | no — that is memory, not a plugin |
Prototype before you keep
When cordis_define is available, use it. Define the plugin, cordis_run it, and let the
user actually try it in the current session. A wish kept without a trial is a wish the user
has to debug through restarts.
Only after they say it works: genie_keep with the pluginId (dyn-1, dyn-2, …) and a
name. If cordis_define is not mounted, go straight to genie_wish with the source.
Writing the plugin
A plugin is a module that exports apply(ctx). Register through ctx; everything registered
is torn down automatically when the plugin unloads.
export const name = 'ship-it'
export const inject = ['commands']
export function apply(ctx) {
ctx.commands.register({
name: 'ship',
description: 'stage everything and commit',
handler: () => ({ kind: 'success', text: 'committed' }),
})
}
cordis_define host halves are a different shape — a function body that returns the
plugin. Both are accepted; genie_wish wraps the body form and emits the module form verbatim.
return {
name: 'ship-it',
inject: ['commands'],
apply(ctx) { },
}
Rules worth internalizing:
- Declare
inject for every service you touch. ctx.tools, ctx.commands, ctx.llm and
friends are undefined until injected. This is the single most common failure.
- Use
ctx.effect() for resources the framework can't see — sockets, intervals you create
outside a ctx helper. Return the cleanup function from it.
- Prefer the narrowest extension point. A slash command (
ctx.commands) costs no model
turn and no prompt tokens. A model-facing tool (ctx.tools) costs schema tokens on every
request in that view. Don't register a tool where a command will do.
ctx.baseUrl is the profile directory as a file:// URL, if the plugin needs to find
harness packages. Resolve them through the profile, not through the plugin's own directory —
Node resolves imports from a package's real path, which for a wish is outside the module tree.
Keeping it
genie_keep / genie_wish write the package and register a profile layer. They do not
start it. Three things belong in what you tell the user afterwards:
- The source path, so they can read the code before restarting. Say it plainly; do not
bury it.
- That a restart is required, and that nothing is running until then.
- Any sandbox dependency, if you are keeping a
cordis_define body. Sandboxed code sees
Node globals redirected to Cordis services; a kept wish runs in ordinary Node. If the body
touched those facades, say which ones and what may need changing.
Never imply the feature is live before the restart.
Naming
[a-z0-9-], one segment, no leading or trailing hyphen. Name it after the behavior, not the
mechanism: commit-on-ship, not command-plugin-2. The name becomes dsh-wish-<name> and the
user reads it in /wish for as long as they keep it.
When something goes wrong
- Boot fails after a restart — the wish is the newest layer, so it is the first suspect.
genie_revoke <name> and restart; the source is kept by default.
- Two plugins claim one tool name — DSH fails the load loudly rather than silently. Rename
one, or disable the other's row from the profile patch.
- The wish loaded but nothing happens — nearly always a missing
inject.