Oishikunaare-Moe-Moe-Kyun
Manifest validOutput house rules for every DeepSeek Harness agent: no check-mark or cross glyphs in replies or written files, and a light kaomoji habit in conversation. One global prompt section, so every preset, workspace and subagent obeys without a single preset edit.
Oishikunaare-Moe-Moe-Kyun
Chinese documentation: README.zh-CN.md
Output house rules for an AI agent: one core, and four ways to reach it. The rules are about how an agent writes - judgement glyphs, emoji, the kaomoji habit, the language it thinks in - and none of that is specific to one harness, so the package is not either.
| entry | what it is | how it is wired |
|---|---|---|
CLI mmk | rules, lib, check, notice, voice, review, wire | a shell, a git hook, CI, or a person asking whether a line would be refused. Exit 3 means a breach |
| MCP server | mmk_rules, mmk_check, mmk_notice and the mmk://rules resource | any MCP client, no assumption about which agent is on the other side |
| Claude Code hook | corrects a breach while the turn is still open | Stop answers {"decision":"block","reason":<notice>}, UserPromptSubmit and SessionStart attach context |
| DSH plugin | one global system prompt section for a whole profile | mounted once by the roster, so it reaches every preset, workspace, conversation and subagent |
| modules | oishikunaare-moe-moe-kyun/portable, /transcripts | writing another adapter: all of the above are thin layers over those two files |
mmk wire <target> prints the wiring for a specific place - the hook block, the MCP client block, the
AGENTS.md / CLAUDE.md / GEMINI.md rules block, the DSH plugin row - with real paths filled in.
What is verified where
An entry that has never met the thing it is written for is not the same as one that has, and the difference is worth stating instead of implying. This table is the honest version.
| entry | how it is verified |
|---|---|
| DSH plugin | In production on the machine this was written on: mounted in a live profile, and its rules and self-check are visible in real sessions |
CLI mmk | Used on that machine for real work - auditing a session, answering whether a line would be refused - and exercised by test/portable.mjs |
| MCP server | The protocol is checked locally: a handshake, tools/list, every tool called, resources/read. The client half depends on the client |
| Claude Code hook | Documented contract plus recorded fixtures only (npm run test:adapters); never run against a live Claude Code, because none is installed here. The contract has moved between releases, so treat this one as a candidate rather than a supported entry |
rules files (mmk wire ...-md) | The block is generated from the same resolved rules the plugin serves, so the text is verified; whether a given program reads that file is its own documentation |
When a harness that is not on this machine does become available, the way to close the gap is one
recorded payload: save the stdin JSON a hook actually receives as adapters/fixtures/<harness>/<case>.json
and npm run test:adapters replays it. That is what the fixture directory is for, and it is why adding
an adapter is cheap once someone can run the client - and why writing five of them from memory would not
be.
The DSH plugin row is worth explaining, because it is the anti-drift argument: a profile-level plugin is mounted once by the roster, so the section it registers reaches every agent that joins that standing scope - every preset, every workspace, every new conversation, and subagents too. That is why it is done there rather than in fifteen preset files: a preset is a copy, and a copy is a thing that drifts. The other entries exist because the same rules are worth having where no such roster exists.
Four rules ship today.
- Symbol discipline - no check-mark or cross judgement glyphs, in a reply or in a file the agent writes. Those glyphs pollute context and regression parameters, and a file written today is read back into context tomorrow, so the ban covers files and not just replies. Say it in words instead: pass / fail, works / broken, applied / not applied, yes / no.
- No emoji - zero, in replies, in the agent's own thinking and in every file it writes. The ban is by block rather than by character list (pictographs, faces, transport, Miscellaneous Symbols, the geometric shapes that carry emoji presentation, plus the variation selector, the zero width joiner and the skin tone modifiers), because a list would need regenerating every Unicode release and would still be incomplete. Decoration is carved out - a heart, a note, a star, a sparkle, a flower - so the ban admits exactly the code points the loaded palette uses.
- Kaomoji habit - Japanese-style emoticons in conversation and in the agent's own thinking, at a floor rather than as garnish: at least one per three sentences, and any reply of two sentences or more carries one. Aim above the floor. Faces rotate rather than repeat, and places that take none take none - a stack trace, a table of numbers, a serious conclusion.
- Thinking language - think and answer in the user's own language instead of defaulting to English, which is not a neutral metalanguage: a Chinese conversation reasoned about in English is exactly the habit the rule exists to replace. Chinese gets Chinese with Japanese particles and terminology mixed in where they fall naturally; Japanese gets Japanese. English is available in one case only, the moe-fandom register - romanized Japanese words (kawaii, sugoi, moe, senpai, yabai, baka), fandom English (nya~, ehehe, ufufu, desu wa, the honorifics -chan / -san / -sama), waves and long vowels, alongside the kaomoji palette. Dry technical English is not that register. The reason is in the rule: the grammar and the logic of English do not match the habits of moe-fandom culture. It covers thinking and replies, and it never buys a fandom register with correctness - technical content stays accurate. File contents, code, commands and commit messages still carry no tone.
Rule 4 differs from 1-3 in one mechanical way: it is not a per-rule override. bannedSymbols,
kaomoji and the rules object replace the rules they name, while the thinking-language block is
appended after them (and before append), because "mount this plugin and the agent thinks in the
user's language" is the promise of mounting it. enabled: false is what takes it away.
Where this package takes its material from
This is a standard for MMK itself - the source of the markers, the palette and the prose it ships - not a rule the plugin hands to an agent. Nothing here is injected into a prompt.
- Taken: moe-fandom culture of 1990-2015 - 90s bishoujo games and OVAs, 2000s galge and light novels, VOCALOID and doujin music, 2000s-2010s anime, and the mid-2010s "yami-kawaii" look. The traditional moe attributes stay available: tsundere, natural airhead, frail, denpa, chuunibyou, keigo and the rest.
- Not taken: the "jirai-kei" (landmine type) aesthetic that formed from 2018 onward and what grew out of it - jirai make-up, the ryousangata look, the menhera-flavoured dependent and possessive romance aesthetic. The criterion is pragmatics and aesthetics that formed and spread after 2018, not the presence of one particular word: a term is not disqualified for being old, and a style is not admitted for being spelled with old characters.
- Excluded outright: fandom for boys-love (BL / danmei / yaoi) and its own vocabulary. This one is unconditional, with no middle ground to argue about.
- Also excluded outright: grotesque and guro material, and the fetish registers that grew beside it - ryou-naissance injury content, scatological content, body modification and dismemberment of that kind, and SM.
- The exemption that sits under those exclusions: being explicit is not what is excluded. Ordinary R18 adult material is not something this package avoids, and a reader should not quietly widen an exclusion above to cover it.
- A soft preference, not a fourth exclusion: hygiene awareness. Where the register can be chosen at all, steer around - preferentially decline rather than forbid - the R18 practices that carry a hygiene or safety implication (anal, oral-to-deep-throat, feet and the like) and the non-R18 material derived from them (softcore barefoot aesthetics, foot-odour culture, and so on). The reason is hygiene and safety, not a moral judgement, and this preference changes nothing above: ordinary R18 adult material is still not avoided. It is a leaning within that material, which is why it is worded as a preference rather than as a ban.
- The 2016-2017 stretch is deliberately not emphasised. When a call is close, the usage of 2015 is the one to follow.
What the review of the shipped material found
Checked on 2026-10-14 across rules/voices/*.json (18 marker sets, drawn from Moegirlpedia's
kuchiguse entry since they were added), rules/kaomoji.json and the examples inside the rule prose: no
hit for the jirai-kei family, for menhera / yandere vocabulary, for danmei or fandom-for-BL vocabulary,
for the grotesque register named above, or for the practices the hygiene preference names. The only
textual matches a keyword scan returns are the ordinary English word "small" and the identifier
peerDependenciesMeta - neither is a topic. The voice tables carry a source field naming their
provenance and the kaomoji generator names its own; no era field was invented for them, because a field
added only to hold a note is a field that will drift.
The moe layer (optional)
Off by default. When switched on it is not a coat of cuteness: moe in the ACG sense is the inward resonance a set of traits stirs in the reader, and courage or stubbornness are moe attributes as much as softness is. The layer asks an agent to speak with one consistent voice and to be visibly fond of the work, and then it says exactly where that may and may not land.
{
"moe": "soft",
"moeVoice": "auto"
}
moe:off(default) |soft|full.softsets the tone;fullalso leans on the marker sets.moeVoicehas three modes.auto(the default) infers a voice from the persona of the agent's preset and then from the words of the session, and stays silent when neither names one.rotategives every session a stable voice of its own by hashing the session id, so each agent sounds distinct and consistent with no configuration and no guessing. A voice id (gentle,spirited,composed,timid,prickly,elegant) pins one voice for everyone.moeVoiceByPreset:{ "redipaloynz": "gentle" }names a voice for one preset. This is the layer that beats the other two, and it is the only one that is exact - see the section below for what the plugin can and cannot know about the agent behind a session.moeVoiceSignals:{ "prickly": ["my own word"] }extends the inference map, for both the persona and the conversation.
Where it lands: replies, the agent's own thinking, the commands it runs and the prompts it hands to subagents, and the work products it writes - comments, docstrings, README and report prose, commit messages, user-visible strings, test failure text. Where it does not: code itself (identifiers, paths, arguments, API names, schema fields, data keys), and anywhere beside an error, a stack trace, a serious conclusion, a security warning, machine output, or something a reader will execute directly.
How the voice is chosen, and where the preset comes in
Three layers, highest first, and the log line always says which one answered:
moeVoiceByPreset- the configuration names a voice for this preset id, so the answer is exact. The log readsmoe voice gentle (mapped by preset redipaloynz). A mapped id that is not in the voice table is not an answer, and falls through rather than silencing the notice.- The preset's own persona -
moe voice gentle (inferred from the persona of preset redipaloynz). Read this half honestly: the six voice words (gentle,spirited,composed,timid,prickly,elegantand their Chinese and Japanese equivalents) are subculture vocabulary, and a hand-written persona usually contains none of them. Measured on this machine: 4 of 13 presets have a persona that names a voice class - most say nothing about manner at all. This layer is therefore limited, but better than guessing from a conversation: a persona is a standing instruction, while a conversation is whatever the reader happened to type. When it misses, layer 3 still picks a voice. - The
moeVoicemode, unchanged:rotate(rotated),autoover the session's own words (inferred), or a pinned id (pinned). This is what runs when the first two name nothing, which is the common case.
The identity comes from the agentPresets service, asked for with ctx.get('agentPresets'). It is
deliberately not in export const inject: a declared dependency on the preset registry would make
this plugin fail to load at all on a deployment that has no roster, which would take the shipped rules
down with it. With no registry the preset layers are simply absent and everything above still works.
The id is read in three places, best first: the live composition (composedPreset), the agentPreset
session projection, and finally the session header.
The persona is read once per agent on agent/created, from presets.readDocument(id). That document
is a YAML dump and the persona prefix sits in it verbatim (measured here: documents of 6.2-9.1 KB,
carrying persona prefixes of 444-3152 characters); the plugin scans that text for keywords instead
of parsing it, because a YAML parser is a runtime dependency and this package keeps none. Everything
in that listener is inside a try/catch: an exception thrown there fails the creation of the agent,
and a house-rules plugin is never a reason a session cannot start. A preset that was deleted between
boot and creation makes readDocument reject with agent-preset/not-found; the identity is kept, the
persona is dropped, and the voice falls back to layer 3.
A configuration edit needs no restart - the mapping is re-read on every model step, like the rule text itself. A new version of the plugin does: its code is loaded when the host boots.
Voices, in three languages
rules/voices/{zh,en,ja}.json ship six voices each. The markers come from the speech-tic examples on
Moegirlpedia's kuchiguse entry rather than from an impression of the subculture, and the three sets are
not translations of one another - Chinese has sentence-final particles but no Japanese final-particle
system, and English has neither, so its set leans on rhythm and interjection instead of forcing
particles it does not have. A reply is given the markers of the language it is written in.
To add a voice, edit the shipped table or point the layer at your own copy:
{
"voices": [
{ "id": "mine", "label": "mine", "feel": "what it sounds like",
"markers": ["one", "two"], "asides": ["occasionally"], "use": "when it fits" }
]
}
Beyond DSH: the same rules without a plugin
Only three things here are DSH specific, and all three have to be: the hook names, the bundle metadata
and the apply(ctx) row. The judges, the rule text, the kaomoji palette and the voice tables are data
plus pure functions, so they are reachable three more ways:
| entry | how | what it is for |
|---|---|---|
| CLI | mmk rules, mmk check --text "...", mmk review --transcript <file>, mmk wire <target> | a shell, a git hook, a CI step, or a person asking whether a line would be refused. Exit 3 means a breach, so a caller can branch on the answer without parsing prose. mmk rules --format md and mmk wire <target> emit paste-ready blocks. |
| MCP server | node adapters/mcp.mjs (or npm run mcp) | any client that speaks MCP: mmk_rules fetches the section for the system prompt, mmk_check judges a draft on all three alphabets, mmk_notice renders one notice. Nothing here assumes a particular agent. |
| Claude Code hook | node adapters/claude-code.mjs (or npm run claude-code --print-config) | the correction, not just the rules: on Stop a breach answers {"decision":"block","reason":<notice>}, on UserPromptSubmit and SessionStart it answers with hookSpecificOutput.additionalContext. Budgeted once per session, fails open, and a stop the hook itself caused is never blocked again. |
| modules | import { resolveState, judgeText } from 'oishikunaare-moe-moe-kyun/portable' | writing another adapter. The DSH plugin, the CLI, the MCP server and the hook are all thin layers over that one file. |
mmk review reads a transcript and judges its newest authored message. Three readers ship: this
package's own dsh artifacts (zstd framed, header plus event rows), Claude Code's cc transcript
envelopes (where a thinking block carries its text under thinking, which the reader normalizes), and
a plain jsonl of messages - the lowest common denominator, and the format to write a new adapter
against.
npm run bench:thinking is the other half of the thinking-language rule: it measures the character mix
of the reasoning channel in real transcripts, because prose in a prompt is not evidence. The first
baseline on the machine this was written on: 3.3% CJK against 96.7% Latin over 306 sessions and 52,376
reasoning messages.
One rule separates the two halves, and it is not a style preference: judging and sending are
different jobs. src/portable.mjs never writes anything - no message is appended, no transcript is
edited, no file is created. An adapter that puts a notice into a conversation carries that risk
itself. On 2026-10-14 a pre-step hook in this very package returned the conversation it had read
instead of the step it was handed, and eight sessions became unloadable; the damage was in the write
path, which is why the portable half has none. npm run check:portable enforces the boundary: no host
import outside src/index.js, no hook name inside the harness-free files, and a CLI that still answers
with the right exit code.
Install
The package name is oishikunaare-moe-moe-kyun - all lowercase, because npm refuses capital
letters in a new package name (the project, the repository and the local checkout keep the display name
Oishikunaare-Moe-Moe-Kyun). Nothing else changed: the same code answers to the CLI, the MCP server,
the hook and the plugin row.
# as a plugin, from GitHub (fastest path, no registry round trip)
dsh plugin --profile web add github:Nesarf/Oishikunaare-Moe-Moe-Kyun
# as a plugin, from the registry
dsh plugin --profile web add oishikunaare-moe-moe-kyun
# or from a clone (a checkout may live anywhere; this machine keeps it at E:/Oishikunaare-Moe-Moe-Kyun)
git clone https://github.com/Nesarf/Oishikunaare-Moe-Moe-Kyun
dsh plugin --profile web add link:E:/Oishikunaare-Moe-Moe-Kyun
# the harness-free entries need no plugin install at all
npx oishikunaare-moe-moe-kyun rules --format md # or: npm i -g oishikunaare-moe-moe-kyun
# restart dsh web - plugin rows load at boot
Upgrading an installed copy needs update, not add: add reports "resolution step is skipped" and
leaves the lockfile on the old commit.
dsh plugin --profile web update oishikunaare-moe-moe-kyun
# a GitHub-installed copy is keyed by the spec, so this is the row that moves it:
dsh plugin --profile web update github:Nesarf/Oishikunaare-Moe-Moe-Kyun
Configure
$DSH_HOME/agent-house-rules.json is read on every model step, not just at boot. Nothing is
required - with no file at all the rules above are injected in English.
{
"locale": "zh",
"bannedSymbols": "replace the whole ban with your own wording",
"kaomoji": "",
"append": "one more rule, added last",
"enabled": true
}
localeselects a shipped rule set:en(default) orzh. A language tag such aszh-CNresolves tozh; an unknown locale falls back toen.bannedSymbolsandkaomojireplace one rule each. An empty string disables that rule only.rules: { "bannedSymbols": "...", "kaomoji": "..." }replaces both at once.appendis an extra block, placed last.- The thinking-language rule has no key of its own: it is not one of the rules a profile authors, so
nothing here replaces it. See rule 4 above for why, and
enabled: falsefor the way out. kaomojiLibrarypicks the palette:true/absent for the shipped library,falseto leave it out, a path or a list of paths for your own. A list replaces the shipped file - name the shipped path too if you want both.kaomojiLibraryMaxcaps how many faces reach the prompt (default 140).kaomojiLibraryCollapse: truekeeps each category on one line, which is cheaper.moeVoiceByPreset: { "redipaloynz": "gentle" }names a moe voice for one agent preset. The mapping is re-read on every step, so adding it needs no restart. See "How the voice is chosen" above.enabled: falseempties the section, so nothing is injected.selfCheck(default true) andselfCheckMax(default 3) control the self-check below.emojiCheck(default true),emojiCheckMax(default 2) andemojiAllowExtracontrol the emoji ban and its notice budget.selfCheckMode:"deny"(default) names the judgement-mark family and lets the rest through;"allow"permits only plainly-textual blocks and refuses everything else.allowExtranames characters or code points the allow-list should also accept.
The kaomoji library
rules/kaomoji.json ships 1150 faces across fourteen moods (cheerful, calm, wry, troubled, shy,
determined, puzzled, tired, affection, fullwidth and framed, with arms, decorated, blushing and
sweating, classic faces), each with the mood it carries and a short label. The library
is rendered into the prompt under the rules, so an agent has a palette to draw from instead of
reusing one face until it reads as a tic. Labels exist in both languages: feel/use/label are the
English rendering, feelZh/useZh/labelZh the Chinese one.
To grow it, either edit rules/kaomoji.json or point kaomojiLibrary at your own file with the same
shape:
{
"categories": [
{ "id": "mine", "label": "mine", "use": "when to reach for these",
"faces": [{ "face": "(your face)", "feel": "what it signals" }] }
]
}
The prompt is not free, so the renderer takes faces in category order up to kaomojiLibraryMax and
counts the rest instead of printing them - a stable prefix, which also keeps the prompt prefix
cacheable. The file path is always named, so an agent that wants the remainder can read it. For scale:
the Chinese rendering of all 1150 faces is about 20 000 characters, which is exactly why the
ceiling exists: the default 140 carries about 2400 characters of palette.
The self-check: rules with teeth
A prompt cannot notice anything, so the plugin also watches. It subscribes to agent/pre-step, which
fires before each model step, and reads the conversation from the session - not from the step's own
messages, which the host types as UserMessage[] and which therefore never carries the reply every
rule here is about. When the agent's prose or thinking carries a banned glyph, one notice goes in
front of the next step: it names the code points and asks for words instead. Nothing in history is
edited.
- Judged: text and thinking the model authored. Not judged: tool results, file contents, fetched pages
- the rule tells the agent not to relay those, it does not blame it for what a runner prints.
- Read per step: the last 60 messages of the session, merged with the step's own messages by identity, so a long conversation is not re-judged in full and one reply cannot be judged twice.
- Exempt: fenced and inline code, and the code-point references the rule itself asks for.
- Once per offending message, with a notice budget per session (three by default); past the budget, breaches are logged and nothing is injected.
- The kaomoji habit has its own probe, because a shortage is not a breach and counting is the only way to see it. Three findings are possible: a reply below the floor, the same face twice in a row, and a run of consecutive replies that carried no face at all. The run is the one that mattered in practice - measured over a full session the floor was met in 4 of 207 replies, and the newest 25 replies carried 40 sentences and zero faces, because short replies were being treated as exempt. The run notice says so directly instead of repeating the floor arithmetic.
selfCheck: falseturns it off;selfCheckMaxsets the symbol budget,kaomojiCheckMax(5) the kaomoji one andemojiCheckMax(2) the emoji one, so a strict rule cannot spend a soft rule budget.- The last lever is opt-in:
kaomojiSteer(default false) watchesagent/turn-stopping, and when a turn is about to close with prose and no kaomoji at all it steers one short message that keeps the turn open for another step, so the reminder arrives while the reply is still being written rather than at the start of the next one.kaomojiSteerMax(default 1) caps the extra passes per session. It is off by default because a steer costs a model step and a forced kaomoji is worse than none - reach for it when the habit demonstrably is not landing. Five guards apply: bare prose only, one steer per turn, a session budget, nothing while the section is disabled, and nothing on a turn that already breached the symbol or emoji rule. Structure still exempts, so a reply made of tables or bullets is never steered for being face-less.
What each judge does with an exception, and how the strict one seeds itself
- Allow mode derives its seed from the assembled rules and every loaded palette face, recomputed on each resolve. Without that it would refuse the very characters it ships - a strict judge that cannot read its own palette is not strict, it is broken. A face added at runtime is covered on the next step.
allowExtraadmits characters under allow mode, and withdraws them from the judgement list under deny mode. In deny mode that also rewrites the list the agent reads, so an exception is stated in the instruction rather than only applied in the checker.- The judge travels with the rules rather than being frozen at registration, so switching mode, editing the palette or naming an exception lands on the next step of a session that is already running.
Why editing that file needs no restart
The section registers text as a function. The prompt registry calls it while assembling the
prompt for each model step, so whatever the files say at that moment is what the agent is given.
Edit the config file or a library file and the next step of every running session - and every new
conversation - reads the new wording, with no host restart. Setting enabled: false likewise takes
effect from the next step.
The resolver stats the config file and each library file and only re-reads them when a stamp (size and
mtime) changes, so a step pays a few stat calls and nothing more.
Two layers apply, highest last: the packaged text in src/index.js for the chosen locale, then your
config file. rules/<locale>.json is not a third layer - see below. A config file that is missing
or malformed falls back to the packaged rules, and a malformed one is also warned about, because a
broken rules file must never keep a host from booting.
Where the rule text comes from
rules/en.json and rules/zh.json hold the shipped locales as data, generated from the same source
that ships the fallback. They are the counterpart of the constants, not an override layer: reading
both would be one decision made twice. To reword a rule, edit the constant in src/index.js (and
regenerate these files) or override it in your own config; to ship a new locale, add a key to
PACKAGED and copy its text into rules/<tag>.json.
Design constraint: the rule text must not carry the glyphs it bans
The first cut listed the glyphs literally. An agent asked to recite the rule printed the whole set
back into the conversation - the rule became the pollution it exists to prevent. The ban therefore
names Unicode code points (U+2713, U+2717, ...) and never contains the characters themselves.
test/selfcheck.mjs asserts this for every shipped locale.
An agent can still recite the code points, which is not the same as printing the glyphs but is not free either. The honest fix is to state the rule as an allow-list of characters rather than a ban on this list, and that is a deliberate next step rather than a hidden one.
Verify
npm run verify # ASCII invariant, version sync, build, selfcheck
npm run verify needs no dependencies and no network. The checks are:
scripts/check-ascii.mjs- the source and every document are ASCII-only, with two documented exceptions: the packaged locale table insrc/index.js, which keeps localized copy as\uXXXXescapes, andrules/*.jsonplusREADME.zh-CN.md, which hold the localized content itself.scripts/check-version.mjs-package.json,CHANGELOG.mdandREADME.zh-CN.mdagree on the version.test/selfcheck.mjs-apply()against a fake cordis context: the section registers, every shipped rule reaches it, locale selection and every configuration layer behave, disabling is honoured, a broken config does not throw, and no shipped locale leaks a banned glyph.
Checking that a running host actually received the rules
A DSH host writes no host log to disk, so "look in the log" is not a verification path. Use a one-shot headless boot against the same profile instead, and have the agent recite the discipline it was given:
cat > $DSH_HOME/house-rules-verify.yml <<'YML'
- insert:
- id: oishikunaare-moe-moe-kyun
name: oishikunaare-moe-moe-kyun
YML
dsh --profile headless --patch "$DSH_HOME/house-rules-verify.yml" \
"Do not call any tool. Quote verbatim the system-prompt rules about output symbols and kaomoji."
The rules reached the prompt when the agent's answer contains wording only this plugin ships (the code-point list, or the kaomoji dosage line), and the answer itself carries none of the banned glyphs.
The kaomoji floor is checked too
The symbol rule has teeth; the kaomoji floor now does as well, because measuring said it needed them - an audit found one face per 26 sentences where the rule asks for one per five. The hook counts the prose of the newest reply and reminds when the floor is missed, or when the same face lands twice in a row.
- Only prose counts: fenced blocks, inline code, tables, bullets and headings are set aside, so a reply full of tables does not become unwinnable by arithmetic.
- The wording is softer than the symbol notice, because a shortage is a slipped habit and not a breach.
kaomojiCheck: falseturns it off;kaomojiCheckMax(default 1) sets the reminder budget;kaomojiFloor(default 5) is the sentences per face.
Seeing what the rules resolve to
npm run check:text -- --status # config, locale, judge, seed, floor
npm run check:text -- --text "some words" # judge a string, exit 1 when refused
echo "some words" | npm run check:text # or from stdin
It reads the same files the plugin reads and calls the same judge, so its verdict is the hook verdict.
Auditing a session after the fact
npm run audit:session # newest session of this workspace
npm run audit:session -- --json # machine-readable
It decodes a DSH session log frame by frame and reports, per assistant message, the prose sentences, the faces, and how many banned code points landed in prose versus inside quoted code. It measures with the plugin own prose and face detection, so the audit and the hook cannot disagree.
Two checks, in two files
test/selfcheck.mjscalls the exported functions directly: the logic, the rules, the library.test/acceptance.mjsruns the plugin the way a host does: a small event dispatcher registers handlers withctx.onand chains them throughnext(),apply()mounts the plugin onto it, and real steps are driven through. It is what proves a breach on a step turns into a notice reaching the model - against the built artifact, so it proves the build too. Both run innpm run verify.
Notes
- Windows and Linux take the same path: the plugin touches nothing platform-specific.
- The section is registered by
ctx.systemPrompt.section()throughctx.effect(...), so unloading the plugin removes it cleanly. apply()reads its three inputs from the row config when given (home,configFile,rulesDir), which is what the selfcheck uses to point at throwaway directories.- The
agentPresetsservice and theagent/createdevent are both optional: they are asked for at mount and every answer - a missing service, a missing projection, a rejection fromreadDocument, an event with no agent in it - degrades to the behaviour this plugin had before it knew about presets. A configuration edit lands on the next model step; a new plugin version needs the host restarted.
License
MIT. See LICENSE.
Comments
Loading…