# dsh-wildmon **Your real work is the tall grass — turns, tools and errors rustle it; wild critters pop out, you throw balls, you fill the dex.** Write code and the forge stirs; read docs and shadows cross the archive; ship errors daily — congratulations, the rare species of Glitch Ridge only trust your kind. And one legendary answers nothing but a deep-night error. [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE) ![dsh-plugin](https://img.shields.io/badge/dsh-plugin-1f6feb) ![deps](https://img.shields.io/badge/deps-0-lightgrey) ![tokens](https://img.shields.io/badge/token%20cost-0-orange) ![tests](https://img.shields.io/badge/tests-61%20✓-brightgreen) [中文](README.md) | English
``` 野生的 绒电鼠 Volttuft 出现了!◈ 少见 · 常野原 (A wild Volttuft appeared! Uncommon · Field) ⟋⟍ ⟋⟍ (⚡ᴥ⚡) c( )ɔ ⌁⌁ 球 ×9 —— /mon throw to throw · /mon run to retreat ``` ![The catch ceremony](docs/catch.png) ## 30 seconds to the grass ```sh dsh plugin --profile web add github:swaylq/dsh-wildmon # restart dsh, then in any session: /mon start ``` Pick one of three starter-trio critters, take 8 balls, then **just work**. Every turn, message, tool call and error has a chance to rustle the grass; the longer nothing shows, the higher the odds (a pity mechanic). `/mon` to check — a wild one waits **10 minutes**; `/mon throw` to catch, and rarer species are harder, every miss improves your next throw, but it may also bolt. Fill the **28-slot dex**, walk with a **team of six**: your first six catches auto-join, slot 1's aura raises its home habitat's encounter rate ×1.5, rearrange with `/mon swap`. Shinies exist, at 1/64. ## Habitats: how you work decides who you meet | Habitat | What rustles it | Residents | |---|---|---| | 🌾 Field (常野原) | messages, turns, unclassifiable tools | Fluffgrub, Dotfinch, Volttuft… | | ⚒️ Forge (锻造窟) | writing files, running commands | Malletclaw, Cogbadger, Coredrake… | | 📚 Archive (墨书林) | reading code, searching docs | Markmoth, Indexowl, Tomewyrm… | | 👾 Glitch ridge (故障岭) | failed tools, agent errors | Shardtoad, Tracepede, Panicray… | | 🌙 Night bound (夜阈) | anything between 00:00–05:59 | Lunasmudge, Dozetapir, Stardrift | Plus the starter trio (the two you didn't pick roam the field) and **three legendaries**: one surfaces every 250 turns; one only answers a deep-night error; the last only visits collectors who completed the first 25 slots. The dex offers only rumors about them. Ball economy: 8 to start, +1 per finished turn (cap 30), and catching a species you already own trades the catch back for 2 balls — duplicate encounters are never a loss. ![A wild encounter](docs/encounter.png) ![The dex](docs/dex.png) ## Why this exists The DSH pet lane is busy: twenty-plus desk pets are **state mirrors** (agent thinks, pet does a thinking animation), and [dsh-digipet](https://github.com/swaylq/dsh-digipet) added **raising** (one pet eats your work and evolves). **Collecting** was still an empty slot: no encounters, no catching, no "looking forward to the next error because the dex wants it." As of 2026-08, searching the `dsh-plugin` topic (2,800+ repos) and all four curated awesome lists for pokemon / pokedex / catching / gacha returns zero hits. dsh-wildmon fills that slot — the complete collect-em-all loop (encounter → catch → dex → team) mounted on your actual workflow. It complements digipet rather than replacing it: digipet is depth with **one** creature, wildmon is breadth across **many**. Both install side by side (different command, different save file). If `/mon` collides with another plugin, override `config.command` in your profile patch. ## Commands | Command | Does | |---|---| | `/mon` | Check the grass: encounter card (art, rarity, time left) or quiet status | | `/mon start` | Begin the journey (bare form lists the three candidates) | | `/mon throw` | Throw one ball at the live encounter | | `/mon run` | Back out of the standoff | | `/mon dex` | Collection: caught ● / seen ◐ / unknown ○; legendaries show only rumors | | `/mon team` | Party and box (its numbering feeds swap / name) | | `/mon swap ` | Reorder the party or exchange with the box (slot 1 leads) | | `/mon name ` | Nickname a caught partner (16 chars) | | `/mon story` | The journey log | | `/mon about` | Mechanics and the trust stance | ## How it works The whole plugin is **four event taps and one slash command**; the taps only roll dice: | Tap | Base encounter rate | |---|---| | `agent/turn-stopping` (also refills 1 ball) | 9% | | `agent/inbox/claimed` (your messages) | 5% | | `tools/result` (routed by tool name to forge/archive/field; failures → glitch) | 6% / 16% on failure | | `agent/error` | 20% | On top: +0.8% per quiet event (capped at +25%), lead's home habitat ×1.5, and at night a 70% roll swaps in the night pool. Catch odds = rarity base (65% / 45% / 28% / 12%) + 12% per prior miss, capped at 95%; after a miss the wild one may flee at a rarity-scaled rate. Every number lives as a named constant at the top of `core.js`. The save lives at `$DSH_HOME/wildmon/state.json`, written the way the official JSON storage backend writes: temp file + fsync + atomic rename, throttled to one write per 1.5 s no matter how busy the agent is. **The invariant that matters: model-visible means logged — and this plugin simply shows the model nothing.** No tool registration (tool schemas are a per-request token tax), no `agent.inject()`, no session events. The whole game lives in command result cards and contributes zero bytes to every model request. ## Trust stance - **Reads**: only its own save file. Never your code, never conversation content — the listeners use only *what kind of thing happened*; message and tool payloads never touch the save. - **Writes**: only `$DSH_HOME/wildmon/state.json`. - **Network**: none. - **Model-visible surface**: none. Zero token cost; KV cache untouched. - **Randomness**: affects the game only. The listeners never veto, rewrite or delay real work (all hooks are notification-mode events). - **Blast radius**: every listener is wrapped in try/catch and the command handler has its own catch — a collapsing grassland cannot land on your work. ## Known limits - One dex per DSH home; all profiles, sessions and subagents on the machine share (and rustle) the same grass — feature and limit at once. - Concurrent dsh processes: last writer wins, same single-process stance as the official JSON backend. - Encounters don't notify you (the price of a zero model-visible surface) — you check with `/mon`; the wild ones wait 10 minutes and misses land in the story log. - Tool routing is a name-regex heuristic; oddly named third-party tools count as field. - The night window is machine-local 00:00–05:59; travel across time zones and the nocturnals jet-lag with you. - Rates and thresholds are not configurable: changed numbers would leave the dex and the save with no common language. ## Verification record Verified for real on dsh `0.1.0-rc.6` (macOS, web profile): 1. Installed with `dsh plugin --profile web add `; the wildmon layer shows in `--dump-config`; 2. `dsh web` boots clean; 3. In a real browser, `/mon start` picks a partner and the send-off card renders (monospace, newlines kept); 4. Six plain messages without an API key — every model request fails, but `inbox/claimed` and `agent/error` are real events: the save recorded `messages:6, mishaps:6`, and with pity at 10 the 6th round really spawned a Shardtoad (error event → glitch ridge, matching the table); 5. `/mon throw` missed once (the card showed the throw-bonus note), caught on the second — party 2/6, dex 2/28, balls ×6 = 8 − 2 throws; the save reconciles line by line with the observed events; 6. Observed caveat: on rc.6, MISSING_CREDENTIAL failure rounds do **not** fire `agent/turn-stopping`, so the per-turn ball refill wasn't observable on a keyless machine (it needs turns that complete); that path is covered by unit tests; 7. 61 unit/integration tests (`node --test`) covering pure logic, rendering, the store and a fake-ctx integration. ## Homage and boundaries The design pays homage to the collect-em-all genre's traditions (grass encounters, the three-shake catch, a dex, a starter trio, an electric rodent, shinies). Pokémon is a trademark of Nintendo / Creatures / Game Freak; this project is not affiliated with them in any way. All 28 species names, ASCII art and text are original; no official names, characters or assets appear. The ball is just a ball that wobbles — that belongs to everyone who loves collecting things. ## License MIT