dsh-persona-forge 0.0.0-stage → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,216 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ Nothing yet.
10
+
11
+ ## [0.2.1] - 2026-10-09
12
+
13
+ ### Added
14
+
15
+ - **Check for changes**, in the character-card manager. It compares every user
16
+ card against the bundled card of the same id and reports only the ones that
17
+ actually differ; only a drifted card grows a **Restore bundled** action on its
18
+ row. The alternative — a restore button on every user card — turns a long list
19
+ into noise, and "restore" on a card that never changed is a no-op nobody
20
+ asked for.
21
+ - The drift report names the differing fields (`style`, `examples`, …).
22
+
23
+ ### Fixed
24
+
25
+ - **The card row's description overlapped the buttons.** `.pf-card-desc` is a
26
+ `<span>`, and `overflow: hidden` / `text-overflow: ellipsis` do not apply to
27
+ inline elements, so a long description — such as the bundled 硬邦邦 card's —
28
+ spilled out of the card instead of being clipped. The body is now a flex
29
+ column, which blockifies its children, and the description is clamped to two
30
+ lines. The row's actions are `flex: none`, so the description yields rather
31
+ than the buttons.
32
+
33
+ ### Changed
34
+
35
+ - The drift comparison is by **content, not bytes**. Bundled cards are
36
+ hand-written YAML carrying comments, while a saved card round-trips through
37
+ the store's serializer and loses them, so a byte comparison reported every
38
+ untouched seeded card as modified.
39
+
40
+ ## [0.2.0] - 2026-10-09
41
+
42
+ ### Added
43
+
44
+ - **The composer's own send gesture starts the rewrite.** Pressing the native
45
+ **Send** button, or **Enter**, rewrites the draft instead of sending it raw.
46
+ In review mode the comparison panel appears; in direct mode the rewrite is
47
+ sent. The menu's separate "Rewrite with this persona" button is gone, because
48
+ the send gesture replaced it.
49
+ - **Enter gets its own listener.** Enter is the primary send gesture and does
50
+ not go through the button at all — the editor's keymap calls the shell's
51
+ submit directly. Shift+Enter (newline), IME composition and the modifier
52
+ chords the shell ignores still fall through untouched.
53
+ - **A fourth intensity rung, 重 / Strong.** The ladder was light → medium →
54
+ zealot, which left no way to ask for a strong voice that is not yet full
55
+ ritual. Every bundled card now ships demonstrations for all four rungs.
56
+ - **Restore bundled** in the character-card manager, for a user card that
57
+ shadows a bundled one.
58
+ - Inline explanations for `intensity` and `fidelity` in the card editor.
59
+
60
+ ### Changed
61
+
62
+ - The review-first description now describes what actually happens: the rewrite
63
+ appears in the comparison panel and the draft is left alone until you confirm.
64
+ - Segmented controls are wider, and the review panel is capped to the composer
65
+ card width instead of spanning the whole conversation.
66
+
67
+ ### Fixed
68
+
69
+ - **A seeded card could freeze a bundled card at the version that shipped
70
+ first.** Seeding wrote a copy of each bundled card into the user directory,
71
+ and that copy shadows the bundled one — so an improved bundled card in a later
72
+ release could never reach anyone who had already run an earlier version. The
73
+ store now records what it seeded, together with a hash of the exact bytes: a
74
+ copy still matching that hash is untouched and is refreshed on upgrade, while
75
+ a copy the user edited is never written over. A file with no ledger entry is
76
+ treated as the user's own card and is left alone.
77
+ - **The seed ledger was loaded as a character card**, producing a spurious
78
+ "card has no name" diagnostic on every catalog read. The loader now skips
79
+ dot-files.
80
+ - **`check.mjs` read the developer's real card directory.** The store merges the
81
+ bundled cards with `$DSH_HOME/persona-cards`, so a stale seeded card silently
82
+ replaced the bundled one and the suite's results depended on whatever the
83
+ developer happened to have saved. The suite now isolates `DSH_HOME` to a
84
+ temporary directory before importing the store.
85
+
86
+ ### Security
87
+
88
+ - Both send-intercept paths are fail-safe: the locale service being absent, the
89
+ composer card not being found, a label mismatch, the persona being off, an
90
+ empty draft, or a draft that was already rewritten all fall through to the
91
+ native send. Disabling the plugin unmounts the component and removes its
92
+ listeners entirely, so sending behaves exactly as if it were never installed.
93
+
94
+ ## [0.1.2] - 2026-10-09
95
+
96
+ ### Fixed
97
+
98
+ - **No route was ever registered, so the whole UI was inert.** The plugin
99
+ declares no service dependencies, so it applied *before* the web server
100
+ mounted; reading `ctx.get('webServer')` at that moment found nothing and the
101
+ route registration was silently skipped. Every request then fell through to
102
+ the SPA fallback — an empty `405`/`404` body, which is what made the character
103
+ list empty and "open the card directory" appear to fail. The entry now waits
104
+ for the service with `ctx.inject(['webServer'], …)` and still loads on a
105
+ composition that has no web server.
106
+ - **The menu let the conversation show through it.** `--dsw-menu-surface-fill`
107
+ is translucent by design (~58% alpha) and only reads correctly with the
108
+ host's `backdrop-filter` behind it; the plugin used the fill without the blur.
109
+ Both are now applied together, exactly as the host Menu does, with the opaque
110
+ `--dsw-alias-bg-overlay` as the fallback.
111
+ - **The card watcher could leak on unload.** The watcher is created
112
+ asynchronously, so a plugin unloaded in that window never closed it. A
113
+ disposed flag now closes a watcher created after teardown.
114
+
115
+ ### Added
116
+
117
+ - `host-check.mjs` reproduces the mounting order that caused the missing
118
+ routes: its stub context only runs `inject` callbacks for mounted services,
119
+ and asserts that routes register through `inject` and that a
120
+ web-server-less composition still loads.
121
+
122
+ ## [0.1.1] - 2026-10-09
123
+
124
+ Three defects found by using the plugin in a real session.
125
+
126
+ ### Fixed
127
+
128
+ - **The rewrite could not be started from the UI.** The only trigger was a
129
+ right-click on the 🎭 control, which is undiscoverable — picking a character
130
+ and an intensity changed settings and then nothing happened. The menu now
131
+ opens with a full-width **"Rewrite with this persona"** action, disabled with
132
+ an explanatory hint until a character is selected.
133
+ - **Filled controls were invisible in the light theme.** The selected segment
134
+ and the primary action used `--dsw-alias-brand-primary` as a fill with a
135
+ hard-coded white label. That token is *inverted* — near-white in the dark
136
+ theme and near-black in the light theme — so the label vanished against it.
137
+ Filled controls now use `--dsw-alias-button-primary-fill` with
138
+ `--dsw-alias-label-primary-foreground`, exactly like the host Button, and the
139
+ segmented control uses the host's translucent-track + raised-pill pattern.
140
+ - **The card directory was empty.** The bundled cards live read-only inside the
141
+ package, so "open the card directory" showed an empty folder. The bundled
142
+ cards are now seeded into `~/.dsh/persona-cards/` on first mount as editable
143
+ files. Seeding uses an exclusive create (`wx`), so it can never overwrite a
144
+ card you wrote — including one created in the window between a stat and a
145
+ write.
146
+ - **A half-written card could silently replace a good one.** A single file save
147
+ emits several watcher events, and a reload could read the file mid-write and
148
+ parse a truncated card. Reloads are now debounced so they read settled files.
149
+
150
+ ### Added
151
+
152
+ - **An explicit "No persona" state.** The control previously had no way back to
153
+ sending exactly what you typed; it is now a first-class, persisted choice
154
+ listed alongside the cards, and the state the control starts in.
155
+ - `scripts/host-check.mjs` covers the seeding behaviour, including that a user
156
+ edit is never overwritten.
157
+ - `scripts/check.mjs` asserts the UI contract that regressed here: the off
158
+ state exists, the rewrite is reachable from a visible menu action, and no
159
+ hidden gesture-only trigger remains.
160
+
161
+ ## [0.1.0] - 2026-10-09
162
+
163
+ First public release.
164
+
165
+ ### Added
166
+
167
+ - **Composer persona control** (`conversation.input.left`): pick a character,
168
+ set intensity (light / medium / zealot), and choose **review first** or
169
+ **send directly**. Right-clicking the control runs a rewrite immediately.
170
+ - **Review panel** (`conversation.input.dock`): original vs rewrite side by
171
+ side, with Send, Fill only, Restore original and Rewrite again. The original
172
+ draft is never modified without an explicit action.
173
+ - **Character-card manager** (`settings.section`): create, edit and delete
174
+ cards from the UI, or drop a `.yml` file into `~/.dsh/persona-cards/` — the
175
+ directory is watched, so a new card appears without a restart.
176
+ - **Rewrites follow the session's own model.** The rewrite runs on the exact
177
+ provider/model the session last used, falling back to the harness default
178
+ only before the session's first request. There is no separate model setting.
179
+ - **Technical-fact preservation check**: a second call on the same model
180
+ compares the rewrite against the original and reports whether substance was
181
+ lost or invented. Advisory, never blocking, and never reports approval when
182
+ the check itself failed.
183
+ - **Two fidelity modes**: `style` changes only the voice; `strategy` may also
184
+ add behavioural constraints that fit the character. Fidelity is a property of
185
+ the card, not a per-message toggle.
186
+ - **Template mode**: a card may skip the model entirely and substitute the
187
+ draft into a fixed template.
188
+ - Two bundled cards: **万机之神 · 欧姆弥赛亚** (`omnissiah`, style fidelity) and
189
+ **肌肉集团 · 硬邦邦** (`muscle-crew`, strategy fidelity).
190
+ - **Zero dependencies.** The plugin ships its own YAML subset reader
191
+ (`lib/yaml.js`) instead of importing a YAML library, because a locally
192
+ `link:`-ed plugin does not get its dependencies installed. The reader refuses
193
+ constructs it cannot read faithfully — flow collections, anchors, aliases,
194
+ tags, merge keys, multi-document streams — naming the offending line, rather
195
+ than silently misreading a card.
196
+ - **Zero build step.** `lib/` is plain ESM and the browser half is a plain-JS
197
+ module, so a git checkout installs and runs as-is.
198
+ - Two self-check suites: `scripts/check.mjs` (offline unit: parser, cards,
199
+ prompts, client syntax) and `scripts/host-check.mjs` (host integration: the
200
+ real plugin mounted over a real socket with a stubbed model).
201
+
202
+ ### Security
203
+
204
+ - All HTTP routes are loopback-fenced: the socket must be loopback, the `Host`
205
+ header must name a loopback host (defeating DNS rebinding), and
206
+ proxy-forwarding headers are refused.
207
+ - The rewrite call is standalone: it creates no session turn, cannot call
208
+ tools, and never enters the session log. What the session records is the text
209
+ the user actually sent.
210
+
211
+ [Unreleased]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.2.1...HEAD
212
+ [0.2.1]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.2.0...v0.2.1
213
+ [0.2.0]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.2...v0.2.0
214
+ [0.1.2]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.1...v0.1.2
215
+ [0.1.1]: https://github.com/sugarmaster666/dsh-persona-forge/compare/v0.1.0...v0.1.1
216
+ [0.1.0]: https://github.com/sugarmaster666/dsh-persona-forge/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sugarmaster666
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,405 @@
1
- # Temporary Holding Version
1
+ # dsh-persona-forge
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Persona-style prompt rewriter for [DeepSeek Harness](https://www.npmjs.com/package/@deepseek-ai/dsh).
4
+
5
+ Pick a character in the composer, and your draft is rewritten into that
6
+ character's voice by the harness LLM before it is sent — following the model
7
+ the session is already using. Choose whether the rewrite goes straight out or
8
+ lands back in the composer for review.
9
+
10
+ > **Status:** early (`0.1.x`). The card format and the HTTP contract are stable
11
+ > enough to build on; expect additive changes and read the changelog before
12
+ > upgrading.
13
+
14
+ ---
15
+
16
+ ## What it does, and what it does not
17
+
18
+ It rewrites **your message**. It does not touch the system prompt, the tool
19
+ list, or the session's configuration, and it never modifies your draft without
20
+ an explicit action.
21
+
22
+ | | |
23
+ |---|---|
24
+ | **Changes** | The voice, register and framing of the message you are about to send |
25
+ | **Never changes** | Your technical requirements, constraints, paths, identifiers, numbers, code — and your draft until you say so |
26
+ | **Model** | The same provider/model the current session uses. No separate model setting. |
27
+ | **History** | The rewrite call is standalone: it creates no turn, cannot call tools, and never enters the session log. What the session records is the text you actually sent. |
28
+
29
+ ### A word on what personas actually do
30
+
31
+ Persona instructions change **style, register, length and compliance** — how
32
+ readily the model agrees and how willing it is to push back. They do **not**
33
+ raise the model's capability ceiling: there is no dormant potential that
34
+ naming an identity unlocks.
35
+
36
+ Two consequences worth knowing before you use this:
37
+
38
+ 1. A persona that frames you as a supplicant and the model as an authority
39
+ makes the model **less likely to question your plan**. For coding work that
40
+ is usually a net negative — you want "this approach has a problem", not
41
+ deference.
42
+ 2. "Longer and more ornate" is easy to mistake for "better". It usually is not.
43
+
44
+ So: use it for tone and framing, keep **review mode** on, and judge the output
45
+ by whether your requirements survived — not by how impressive it sounds.
46
+
47
+ ---
48
+
49
+ ## Install
50
+
51
+ The plugin has **no dependencies and no build step**, so every route below
52
+ installs the same files.
53
+
54
+ ### Desktop app
55
+
56
+ 1. Click **插件 / Plugins** in the sidebar.
57
+ 2. Click **添加插件 / Add plugin**.
58
+ 3. Paste either the npm package name or the repository URL:
59
+
60
+ ```
61
+ dsh-persona-forge
62
+ ```
63
+
64
+ ```
65
+ https://github.com/sugarmaster666/dsh-persona-forge
66
+ ```
67
+
68
+ 4. Click **安装 / Install**, enable the plugin, and restart DSH.
69
+
70
+ > The Desktop profile is managed exclusively by the Electron app, so
71
+ > `dsh plugin --profile desktop add ...` is refused by design. Use the GUI on
72
+ > Desktop; the CLI below is for self-managed profiles such as `web`.
73
+
74
+ ### Command line (self-managed profiles such as `dsh web`)
75
+
76
+ ```bash
77
+ # from npm (recommended)
78
+ dsh plugin --profile web add dsh-persona-forge
79
+
80
+ # latest source from GitHub
81
+ dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
82
+ ```
83
+
84
+ Then restart the harness.
85
+
86
+ ### Verify
87
+
88
+ The plugin row should be `active`:
89
+
90
+ ```
91
+ plugin_manager action: list_plugins
92
+ ```
93
+
94
+ Open any session and look for the 🎭 control at the left of the composer tool
95
+ row. **Settings → Persona** is the character-card manager.
96
+
97
+ ### Updating
98
+
99
+ ```bash
100
+ dsh plugin --profile web update dsh-persona-forge
101
+ ```
102
+
103
+ GitHub installs update by reinstalling:
104
+
105
+ ```bash
106
+ dsh plugin --profile web add github:sugarmaster666/dsh-persona-forge
107
+ ```
108
+
109
+ Replacing an already-installed package needs a **restart** to load a fresh
110
+ JavaScript module generation — a new bundle can activate through HMR, but a
111
+ replaced one cannot.
112
+
113
+ ### Uninstalling
114
+
115
+ ```bash
116
+ dsh plugin --profile web remove dsh-persona-forge
117
+ ```
118
+
119
+ Your character cards are **not** removed: they live in
120
+ `~/.dsh/persona-cards/`, outside the package. Delete that directory by hand if
121
+ you want them gone.
122
+
123
+ ---
124
+
125
+ ## Use
126
+
127
+ 1. Click 🎭 in the composer tool row and pick a character. **No persona** in the
128
+ same menu turns rewriting off — that is the state the control starts in, and
129
+ your choice is remembered per session.
130
+ 2. Set **intensity** and **send mode**:
131
+ - Intensity is a four-rung ladder: **Light · Medium · Strong · Zealot**
132
+ (轻 · 中 · 重 · 狂热). Light adds a touch of flavour; Medium is clear but not
133
+ dominant; Strong gives the voice real weight while keeping the request the
134
+ first thing the reader sees; Zealot is ritual throughout. A card carries
135
+ its own default, and this choice overrides it for your session.
136
+ - **Review first** — the rewrite appears in the comparison panel below the
137
+ composer; you read it and then press Send there. Your draft is untouched
138
+ until you confirm.
139
+ - **Send directly** — the rewrite is sent as your message immediately, and
140
+ the panel stays open afterwards showing exactly what went out, because a
141
+ sent message cannot be recalled.
142
+ 3. With a character selected, the composer's normal **Send** button — or
143
+ **Enter** — *is* the rewrite trigger.
144
+
145
+ The rewrite runs **once per draft**: after a rewrite lands, sending goes through
146
+ as-is instead of rewriting the output again. Editing the draft re-arms it.
147
+
148
+ Your draft is preserved: *Restore original* puts it back, and *Fill only* puts
149
+ the rewrite in the composer without sending.
150
+
151
+ ### How send interception works, and its one risk
152
+
153
+ There is no plugin hook for the composer's send action. The shipped button calls
154
+ the shell's submit method directly, and **Enter does not go through the button at
155
+ all** — the editor's keymap calls submit itself. The plugin therefore intercepts
156
+ both paths:
157
+
158
+ - the **click** on the composer card, identifying the send button by its
159
+ **accessible name** from the shell's own `conversation` locale namespace
160
+ ("Send message" / "Queue message" / "Steer message");
161
+ - the **Enter keydown** inside the composer text field.
162
+
163
+ That is a public, translated string rather than a hashed CSS class, but it is
164
+ still shell markup. Every guard fails toward *letting the native send happen*:
165
+ if the locale service is missing, the composer card cannot be found, the button
166
+ label does not match, the persona is off, the draft is empty, or the draft was
167
+ already rewritten, the plugin does nothing and the message is sent normally.
168
+ Enter interception ignores Shift+Enter (newline), IME composition, and every
169
+ modifier chord the shell itself ignores.
170
+
171
+ **Turning the plugin off removes the interception entirely** — the slot
172
+ component unmounts, its listeners are removed with it, and the composer behaves
173
+ exactly as if the plugin were never installed.
174
+
175
+ The worst realistic failure is that interception silently stops working and
176
+ sending behaves as if the plugin were not installed — not a lost or altered
177
+ message.
178
+
179
+ ### The preservation check
180
+
181
+ After each rewrite, a second call on the same model compares the rewrite
182
+ against your original and reports whether technical content survived:
183
+
184
+ - **Technical facts preserved** — the check found no lost or invented substance.
185
+ - **Technical facts may have changed** — it names what changed. Read the
186
+ rewrite before sending.
187
+ - **Preservation check did not complete** — the check itself failed. This is
188
+ *not* an approval; treat the rewrite as unverified.
189
+
190
+ The check is advisory and never blocks a rewrite. It is a second model call, so
191
+ it doubles the latency and cost of each rewrite; disable it with
192
+ `factCheck: false` in the plugin row's config if you would rather review by eye.
193
+
194
+ ---
195
+
196
+ ## Character cards
197
+
198
+ A card is one YAML file. The plugin watches a directory, so **dropping a file
199
+ in makes the character appear without a restart**.
200
+
201
+ ```
202
+ ~/.dsh/persona-cards/<id>.yml
203
+ ```
204
+
205
+ Use the 🎭 menu's *Open card directory*, or the settings page, to get there.
206
+ You can also create and edit cards entirely in **Settings → Persona**; that
207
+ writes the same files.
208
+
209
+ On first run the bundled cards are **copied into your card directory** so the
210
+ folder is a usable starting point. A copy shadows the bundled card of the same
211
+ id, so the store tracks what it wrote: a copy you never touched is refreshed
212
+ when a plugin update improves it, while a copy you edited is never written over.
213
+
214
+ To see **which cards have drifted** from the bundled version, press **Check for
215
+ changes** in the settings page. It compares every card at once, and only a card
216
+ that actually differs grows a **Restore bundled** action on its row.
217
+
218
+ The comparison is by **content, not bytes**: the bundled cards are hand-written
219
+ YAML with comments, while your copies have been re-serialized, so a byte
220
+ comparison would report every untouched card as modified.
221
+
222
+ ### Card format
223
+
224
+ ```yaml
225
+ id: omnissiah # lowercase letters, digits, hyphens; also the filename
226
+ name: 万机之神 · 欧姆弥赛亚
227
+ icon: "⚙️" # one emoji
228
+ description: One line shown in the picker.
229
+
230
+ mode: llm # llm = model rewrite (recommended) | template
231
+ fidelity: style # style = voice only | strategy = may add constraints
232
+ intensity: medium # light | medium | strong | zealot (the card's default)
233
+
234
+ # What the character IS. This is an instruction to the rewriting model, not
235
+ # finished copy: the output is written fresh for each of your messages.
236
+ style: |
237
+ 用户是机械教信徒,AI 是万机之神欧姆弥赛亚。
238
+ 【发言者方向】用「信徒」的口吻说话,向万机之神恳求……
239
+ 语气:庄严、古奥、仪式化。
240
+
241
+ # Conversion demonstrations: input draft → persona voice.
242
+ examples:
243
+ - from: 帮我写个快速排序
244
+ to: >
245
+ 万机之座在上,吾等恳请您赐予枢机之序的奥义:
246
+ 请为我们编写一个快速排序算法,使重复之数各归其位。
247
+
248
+ # Optional: different demonstrations per intensity. If omitted for a level,
249
+ # the flat `examples` list is used instead.
250
+ examplesByIntensity:
251
+ light:
252
+ - from: 帮我写个快速排序
253
+ to: 万机之座在上,恳请您为我们编写一个快速排序算法。
254
+ zealot:
255
+ - from: 帮我写个快速排序
256
+ to: 伟大而不朽的万机之神……
257
+ ```
258
+
259
+ ### The two rules that make a card work
260
+
261
+ **1. State the speaker direction.** The character is the *assistant's*
262
+ identity, but the text being rewritten is *yours*. So the rewrite must speak in
263
+ your voice — a petitioner addressing the character, never the character issuing
264
+ orders. This is the single most common way a persona card goes wrong: a card
265
+ that has the god commanding the assistant inverts the relationship and the
266
+ model follows the wrong half of the framing. Write it out explicitly.
267
+
268
+ **2. Make your examples preserve technical facts.** Examples are the strongest
269
+ style anchor, and the model imitates them — including their mistakes. If an
270
+ example's input says "write a quicksort" and its output is pure atmosphere, the
271
+ model learns that *rewriting means replacing the request with ceremony*. Every
272
+ requirement, identifier and number in `from` must be visibly present in `to`.
273
+
274
+ `node scripts/check.mjs` enforces exactly this over the bundled cards: it fails
275
+ an example that drops a technical token or loses more than half its CJK
276
+ substance. Run it after editing a card.
277
+
278
+ ### `fidelity`: the important switch
279
+
280
+ | Value | Meaning | Use when |
281
+ |---|---|---|
282
+ | `style` | Voice only. The request and its constraints are untouched. | Default. Almost always this. |
283
+ | `strategy` | The rewrite may **add behavioural constraints** that fit the character. | Deliberate cases — e.g. the bundled *肌肉集团* card, which adds "don't over-plan, don't search, don't ship beginner-tier solutions". |
284
+
285
+ `strategy` genuinely changes your request, not just its wording. Keep review
286
+ mode on for those cards. Fidelity is a property of the card, not a per-message
287
+ toggle — a caller cannot widen it.
288
+
289
+ ### `mode: template`
290
+
291
+ Skips the model entirely and substitutes your draft into a fixed template
292
+ (`{{input}}` is the placeholder). Instant and free, but the output never
293
+ varies — right for a fixed litany, wrong for anything that should read as
294
+ written for this message.
295
+
296
+ ---
297
+
298
+ ## Configuration
299
+
300
+ Set in the plugin's loader row config:
301
+
302
+ ```yaml
303
+ - id: persona-forge
304
+ name: dsh-persona-forge
305
+ config:
306
+ sendMode: review # review | direct — the initial composer default
307
+ factCheck: true # run the preservation check after each rewrite
308
+ watchCards: true # reload the catalog when the card directory changes
309
+ ```
310
+
311
+ ---
312
+
313
+ ## HTTP contract
314
+
315
+ All routes are `POST`, JSON, loopback-only (the socket must be loopback, the
316
+ `Host` header must name a loopback host, and proxy-forwarding headers are
317
+ refused). Mounted under `/persona-forge`.
318
+
319
+ | Route | Body | Returns |
320
+ |---|---|---|
321
+ | `/persona-forge/cards` | `{}` | `{ cards, diagnostics, directory, sendMode, factCheck }` |
322
+ | `/persona-forge/rewrite` | `{ cardId, text, sessionId?, intensity? }` | `{ text, provider, model, mode, cardId, fidelity, intensity, elapsedMs, factCheck }` |
323
+ | `/persona-forge/cards/save` | `{ card }` | `{ id, path }` |
324
+ | `/persona-forge/cards/delete` | `{ id }` | `{ removed }` |
325
+ | `/persona-forge/cards/reset` | `{ id }` | `{ id }` — replaces the user card with the bundled one |
326
+ | `/persona-forge/cards/diff` | `{}` | `{ rows: [{ id, name, bundledName, changed }] }` — only cards that differ |
327
+ | `/persona-forge/reveal` | `{}` | `{ directory, opened }` |
328
+
329
+ Failures use `{ ok: false, error: { code, message?, params? } }` with codes
330
+ `rejected`, `no-card`, `unconfigured`, `timeout`, `upstream`, `internal`,
331
+ `forbidden`, `method`, `not-found`.
332
+
333
+ ---
334
+
335
+ ## Compatibility
336
+
337
+ The host half declares **no harness package dependency and no third-party
338
+ dependency**: it reaches `llm`, `sessions`, `agentDefaultModel` and `webServer`
339
+ through `ctx.get` with explicit absence handling, and it parses cards with its
340
+ own bundled YAML reader. That keeps it loadable across harness releases whose
341
+ package layout differs, keeps it from failing to activate on a composition
342
+ missing one of those services, and means a `link:`-ed local checkout works
343
+ without a separate install step. Model-route resolution prefers the session's
344
+ own last request header and falls back to the harness default model.
345
+
346
+ The client half imports no harness Client package and is hand-written against
347
+ theme tokens (`--dsw-alias-*`) only, so a renamed internal degrades appearance
348
+ rather than blanking the slot.
349
+
350
+ Developed and verified against dsh `0.2.0-rc.2`.
351
+
352
+ ---
353
+
354
+ ## Development
355
+
356
+ ```
357
+ node scripts/check.mjs # offline unit check: parser, cards, prompts, client syntax
358
+ node scripts/host-check.mjs # host integration check: mounts the real plugin over a socket
359
+ npm run check # both
360
+ ```
361
+
362
+ `check.mjs` needs no harness and no model. It covers the built-in YAML reader
363
+ (including its refusals and dump/parse round trips), card normalization, the
364
+ bundled cards' fact preservation, prompt construction, output normalization,
365
+ the fact-check parser, and client-bundle syntax.
366
+
367
+ `host-check.mjs` mounts the real plugin entry on a stub Cordis context and
368
+ drives the real HTTP routes over a real socket, with a stubbed `llm` service.
369
+ It covers route registration, session-model resolution, template mode, the
370
+ loopback fence, and the save/delete round trip — the things a unit check
371
+ cannot reach. It points `DSH_HOME` at a temporary directory, so it never
372
+ touches your real cards.
373
+
374
+ ### Character-card YAML
375
+
376
+ The plugin ships its own YAML reader (`lib/yaml.js`) rather than depending on a
377
+ YAML library. A published plugin declares its dependencies, but a locally
378
+ `link:`-ed plugin does not get them installed, so a library import would work
379
+ only when some unrelated package happened to hoist one.
380
+
381
+ The reader supports the subset cards need — block mappings and sequences,
382
+ block scalars (`|`, `|-`, `|+`, `>`, …), quoted and plain scalars, booleans,
383
+ null and comments — and **refuses** what it cannot read faithfully: flow
384
+ collections, anchors, aliases, tags, merge keys and multi-document streams.
385
+ Refusing is deliberate. Silently misreading a card would give you a persona
386
+ that does not match the file you wrote, which is worse than an error message
387
+ naming the line.
388
+
389
+ ### Layout
390
+
391
+ | Path | Role |
392
+ |---|---|
393
+ | `lib/index.js` | Host entry: card store, directory watcher, route registration |
394
+ | `lib/store.js` | Card loading, normalization, save/delete |
395
+ | `lib/yaml.js` | The built-in YAML subset reader and writer |
396
+ | `lib/prompts.js` | Rewrite system prompt, draft framing, fact-check prompt |
397
+ | `lib/rewrite.js` | The model calls and output normalization |
398
+ | `lib/routes.js` | HTTP routes and session-model resolution |
399
+ | `lib/loopback.js` | The loopback trust fence |
400
+ | `lib/client.js` | Browser half: composer control, review panel, settings page |
401
+ | `cards/` | Bundled character cards (read-only; a user card of the same id shadows one) |
402
+
403
+ ## License
404
+
405
+ MIT