dsh-plugin-ponytail
ponytail — "lazy senior dev mode" — as a native DeepSeek Harness plugin.
Upstream ponytail ships adapters for Claude Code, Codex, Cursor, OpenCode, Kiro,
Windsurf and others, and none for DSH: its always-on layer is a UserPromptSubmit
lifecycle hook, which DSH agents do not have. This package rebuilds that layer on
the DSH extension points that do exist, so you get the reference behaviour without
patching upstream and without planting generated files in your repositories.
What it contributes
| Contribution | DSH extension point |
|---|---|
| The always-on rules, recomputed for every prompt | systemPrompt.context() |
The six ponytail* skills, listed only while the mode is on |
skills.registerProvider() |
/ponytail lite|full|ultra|off|status |
commands.register() |
Nothing is written into your project: no AGENTS.local.md, no symlinks in
.agents/skills. The only artifact is the .ponytail/ directory you already have.
Relationship to the other DSH ponytail plugins
Two other ports already exist, and they cover different slices:
dsh-ponytail(ccll) vendors@dietrichgebert/ponytailas a dependency, injects the ruleset as a global system-prompt section (reaching subagents too), adds the six skills as runtime skills, and detects the "stop ponytail" / "normal mode" deactivation. Global and per-session.dsh-ponytail-skills(gongyijie85, repositorygongyijie85/dsh-ponytail) ships the six skills inside the package and registers only actx.skillsprovider. Skills only: it injects no rules, so the style is adopted when the model chooses to load the skill rather than being enforced on every prompt.- This package: always-on rules plus skills plus the command, scoped to a
project by the presence of
.ponytail/.
dsh-ponytail |
dsh-ponytail-skills |
this package | |
|---|---|---|---|
| Always-on rules | yes (system-prompt section) | no | yes (runtime context, or a section) |
| Skills | 6, registered at mount | 6, provider from the package | whatever is in .ponytail/skills/ |
/ponytail command |
yes | no | yes |
| Deactivation gesture | yes | no | yes |
| Which projects get it | all | all | only those with a .ponytail/ above the cwd |
| Mode storage | in-process, per session | n/a | .ponytail/state, on disk, per project |
| Mode after resuming a session | back to the default | n/a | preserved |
| Upstream text | required at runtime |
shipped in the package | vendored into .ponytail/ by your own tooling |
| Name/invocation frontmatter rules | n/a | follows the built-in provider | follows the built-in provider |
Ideas borrowed from dsh-ponytail, because they were right: optional per-feature
injection instead of a hard inject (a profile without a command surface must not
lose the rules), loud rejection of unknown config keys, the whole-message
deactivation gesture, a /ponytail default command, and shipped types.
Ideas taken from dsh-ponytail-skills: honouring the built-in provider's
frontmatter contract — the disable-model-invocation / user-invocable invocation
policy and the kebab-case name grammar, so a skill placed in .ponytail/skills/
behaves exactly as it would in .agents/skills/.
Ideas this package contributes: per-project opt-in by walking up from the cwd
(which crosses nested repository roots), mode state on disk so it survives resume
and restart, and systemPrompt.context as the default placement so a level change
does not perturb the system-prompt prefix.
Install any one of them, not two: dsh-ponytail and this package each register a
/ponytail command, and the second registration fails.
If you want ponytail everywhere with no per-project setup, use dsh-ponytail. If
you only want the skills available and are happy for the model to decide when to
use them, dsh-ponytail-skills is the smaller install. If you want to opt in per
project, enforce the rules on every prompt, and keep the mode next to the code,
use this.
Install
dsh plugin --profile web add dsh-plugin-ponytail
The manifest declares dsh.bundle.patch, so the package also joins
dsh.profile.bundles and the loader applies its patch as one more layer. Restart
the profile so the new layer boots.
For a checkout instead of the registry:
dsh plugin --profile web add /path/to/dsh-plugin-ponytail
Mounting into a running host
DSH reads dsh.profile.bundles from the profile's package.json at boot, so a
bundle normally takes effect on the next start. A profile with
patchReload: live does watch its patch files continuously, so a running host can
pick the row up without a restart. Leave the package in dependencies, remove it
from dsh.profile.bundles, and add a pure insert to the profile's own patch layer
(~/.dsh/profiles/web/cordis.patch.yml):
- insert:
- id: ponytail
name: dsh-plugin-ponytail
Keep it out of dsh.profile.bundles while that insert exists.
applyEntryPatches does not deduplicate inserts by id, so the row would mount
twice — once from the bundle patch and once from the profile layer.
Set up a project
The plugin contributes nothing until it finds a .ponytail/ directory. Presence is
the opt-in.
mkdir -p .ponytail/skills
# rules.md — the static rules (upstream AGENTS.md)
# skills/*/SKILL.md — the vendor skills
# state — off | lite | full | ultra
printf 'full\n' > .ponytail/state
Copying upstream's skills/ and AGENTS.md into .ponytail/ is enough:
git clone --depth 1 https://github.com/DietrichGebert/ponytail /tmp/ponytail
mkdir -p .ponytail/skills
cp /tmp/ponytail/AGENTS.md .ponytail/rules.md
cp -R /tmp/ponytail/skills/. .ponytail/skills/
printf 'full\n' > .ponytail/state
Skills you add yourself follow the built-in filesystem provider's contract, so they
behave exactly as they would in .agents/skills/: name must be kebab-case, and
disable-model-invocation: true / user-invocable: false control who may invoke
them. A skill whose name or invocation frontmatter is invalid is skipped rather
than half-registered.
How the directory is found
The plugin walks up from the session's working directory until it finds
.ponytail/, and stops at the filesystem root.
That one rule is the whole scoping model, and it behaves differently from a file planted in a single project tree:
- A session started in a nested repository still finds
.ponytail/. If the tree holding.ponytail/contains a checkout that is itself a git repository, a session started inside that checkout is covered too, because the walk crosses project roots instead of stopping at the nearest one. - Every descendant directory inherits the mode, with no per-project installation.
- Sibling projects are unaffected: no
.ponytail/above them means no rules and no skills.
Levels
/ponytail # show the current level, directory and skill count
/ponytail lite # name the lazier alternative in one line
/ponytail full # the ladder enforced (default)
/ponytail ultra # YAGNI extremist, deletion before addition
/ponytail off # withdraw the rules and the skills
/ponytail default lite # persist the user-level default across projects
/ponytail <level> writes .ponytail/state. The rules are recomputed on the next
prompt, so off takes effect immediately and nothing has to be regenerated.
Upstream's whole-message deactivation gesture also works: a message that is exactly
stop ponytail or normal mode switches that project to off. A message that merely
mentions the phrase ("add a normal mode toggle") does not.
/ponytail default <mode> writes upstream's own
$XDG_CONFIG_HOME/ponytail/config.json (or ~/.config/ponytail/config.json,
%APPDATA%\ponytail\config.json), which is the same file the reference adapters
and this plugin's own resolution order read. It is the level used when no
.ponytail/state exists.
The level is resolved exactly as upstream resolves it: PONYTAIL_DEFAULT_MODE,
then defaultMode from that config file, then this row's defaultMode, then
full. A per-project .ponytail/state wins over all of them.
ponytail-review is a skill, not a level; /ponytail review says so.
What the model sees
Upstream's own payload, byte for byte:
PONYTAIL MODE ACTIVE — level: <mode>
<the mode-filtered body of skills/ponytail/SKILL.md>
filterSkillBodyForMode is a faithful port of upstream
hooks/ponytail-instructions.js, including its two subtleties: only the intensity
table rows and the quoted worked examples are level-specific, and an ordinary
rule bullet whose label happens to start with a level word survives verbatim. When
skills/ponytail/SKILL.md is missing, .ponytail/rules.md is used instead.
Verified against upstream on the vendored SKILL.md: identical for lite, full
and ultra, modulo a trailing newline.
Context, cache and cost
The rules are recomputed for every prompt, so it is fair to ask whether that churns the prompt prefix and costs cache hits. Measured:
- The system prompt is never touched. The default
placement: contextis a durable user-role snapshot, not a system-prompt section. The cacheable prefix — system prompt and tool schemas — contains no ponytail text. - The payload is byte-stable. Rendering the same unchanged files three times, including from a fresh mount, produces identical bytes, and it carries no timestamp, path, digest, or process id. That is asserted by a test, so a future edit cannot quietly make it nondeterministic.
- An unchanged contribution is not re-sent. The harness compares the rendered runtime context with the snapshot it already holds and emits a message only when the text differs; when it does differ, the new one replaces the old rather than accumulating. Measured on a live 209-step session using this same seam: exactly one snapshot message.
- Skill order cannot flap. This provider reads
readdirSync, whose order is not guaranteed, butctx.skills.snapshot()sorts, and the catalog digest is a hash of[name, description]pairs only, so neither order nor metadata can trigger a re-emit. - What it adds, once per session: the rules payload (5228 characters at
full) plus six skill-catalog entries (2368 characters, each description capped at 500) — roughly 2.1k tokens, cached from then on./ponytailadds nothing to the model context; commands are human-facing and never enter the prompt.
The only thing that moves the text is a change you make:
/ponytail lite|full|ultra(or editing.ponytail/state) changes it once; the prefix up to that point stays cached, so it is one ordinary cache miss.payload: rulesusesrules.md(2590 characters) instead of upstream's mode-filteredSKILL.mdbody (5228) for about half the always-on text, at the cost of the per-level filtering.- Re-enabling a file-based layer (
bash .ponytail/switch on) delivers the rules a second time as workspace instructions, doubling the text.
The doubled-ruleset warning
That last configuration is the one real trap, so the plugin detects it. When an
AGENTS.md, CLAUDE.md, AGENTS.local.md, or CLAUDE.local.md anywhere from the
ponytail root down to the session cwd carries an upstream ponytail marker, the
agent is warned in the prompt, and /ponytail status reports it:
duplicate : /path/to/AGENTS.local.md — rules also arrive as workspace instructions
The warning is its own prompt entry, so a healthy project contributes nothing and
the rules payload stays byte-identical to upstream's. It requires an upstream phrase
(PONYTAIL MODE ACTIVE, "lazy senior dev", "best code is the code never written"),
not merely the word "ponytail", so a project that mentions the name is not flagged.
Set warnOnDuplicateInstructions: false to turn it off.
Configuration
The row takes an optional config. Every field is optional.
- insert:
- id: ponytail
name: dsh-plugin-ponytail
config:
dirName: .ponytail # directory searched for, upward from the cwd
placement: context # context (default) | section
payload: skill # skill (default, upstream payload) | rules
skills: true # serve .ponytail/skills/
skillRank: 260 # precedence vs the built-in roots (project 100/200, custom 300)
command: true # register /ponytail
commandName: ponytail
contextOrder: 130 # placement after the harness policy contexts (110-120)
contextName: ponytail
defaultMode: full # used when state is absent and upstream resolves nothing
warnOnDuplicateInstructions: true # warn the agent when the rules arrive twice
placement: section registers a system-prompt section instead of a runtime
context. Use it only if you need the rules pinned into the system prompt: it
changes the prompt prefix whenever the level changes, and the default context
placement is the same durable user-role snapshot shape the harness uses for
workspace instructions.
Caveats
- The
minimalagent preset callssuppressRuntimeContext(), which switches off every dynamic context in that scope. With the defaultplacement: contextthe rules are silent there; useplacement: sectionif you need them onminimal. - A catalog read lists skills but does not load their bodies; each body is read from disk when the skill is actually used.
- The plugin serves whatever is in
.ponytail/skills/. Keeping that directory current is upstream's problem, not this package's.
Migrating from a file-based install
If you had ponytail wired through a generated AGENTS.local.md plus symlinks in
.agents/skills/, turn that layer off and leave .ponytail/ in place:
bash .ponytail/switch off # removes AGENTS.local.md and the symlinks
The plugin reads the same .ponytail/rules.md, .ponytail/skills/ and
.ponytail/state, so nothing else has to move. Leaving both layers on would show
the rules twice.
Discovery and listing
DSH has no first-party plugin registry: dsh plugin forwards to pnpm inside the
profile, so npm is the distribution channel. The community catalog is
DSH Plugin Hub, which indexes plugins by the npm keyword
dsh-plugin and the GitHub topic dsh-plugin.
- npm:
npm search dsh-plugin - from inside an agent: install
dsh-find-pluginand search thedsh-plugintopic - in the GUI:
dsh plugin --profile web add dsh-pluginadds a plugin marketplace to Settings
To list a plugin: publish to npm with the dsh-plugin keyword, give the repository
the dsh-plugin topic, then submit it at https://dsh-plugin.org for review.
Development
pnpm install
pnpm test
test/selfcheck.test.mjs drives the plugin against stubs of the three host
registries, so discovery, level resolution, mode filtering, the skills provider and
the /ponytail handler are all exercised without a running harness.
License
MIT. ponytail itself is a separate project by Dietrich Gebert, under its own license; this package only adapts it.
No comments yet. Be the first to write one.