gentle-pi 4.5.0 → 4.5.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.
@@ -42,7 +42,9 @@ When you quit Pi after using `/gentle:music`, Gentle Shell runs `gentle-stream c
42
42
 
43
43
  ## Installation
44
44
 
45
- Gentle Shell looks for gentle-stream in this order:
45
+ gentle-stream runs on macOS and Linux, including Gentle Shell inside WSL. It needs POSIX features native Windows lacks (file locks, a Unix control socket, process groups), so on native Windows `/gentle:music` only says it is not available yet: it never offers, installs, or runs the player.
46
+
47
+ On macOS and Linux, Gentle Shell looks for gentle-stream in this order:
46
48
 
47
49
  1. `gentle-stream` on `PATH`.
48
50
  2. In a bundled Gentle Shell, the gentle-stream it installed inside its own folder (see below).
@@ -57,13 +59,13 @@ uv tool install 'gentle-stream[samples] @ git+https://github.com/Gentleman-Progr
57
59
 
58
60
  ### Installed by a bundled Gentle Shell
59
61
 
60
- A bundled Gentle Shell (the one the web installer sets up) offers to install the player for you the first time `/gentle:music` finds none:
62
+ On macOS and Linux, a bundled Gentle Shell (the one the web installer sets up) offers to install the player for you the first time `/gentle:music` finds none:
61
63
 
62
64
  > Install the gentle-stream music player inside Gentle Shell? It downloads uv, Python 3.13 and the player (about 150 MB) into the Gentle Shell folder.
63
65
 
64
66
  Decline and you get the manual hint above; nothing is downloaded. Accept and Gentle Shell shows its progress, installs the player, and starts the music. Later runs use it directly, with no question.
65
67
 
66
- Everything stays inside the Gentle Shell folder (`~/.gentle-shell`, or `%LOCALAPPDATA%\gentle-shell` on Windows). Nothing is added to your `PATH`, and your own Python, uv, and uv settings are never used or changed:
68
+ Everything stays inside the Gentle Shell folder (`~/.gentle-shell`). Nothing is added to your `PATH`, and your own Python, uv, and uv settings are never used or changed:
67
69
 
68
70
  | What | Where | Size on disk |
69
71
  | :-- | :-- | :-- |
@@ -77,6 +79,7 @@ The player is installed with its `samples` extra (pyfluidsynth). If that extra c
77
79
 
78
80
  ## Limits
79
81
 
82
+ - Native Windows is not supported yet (see [Installation](#installation)); use WSL.
80
83
  - The player needs the interactive terminal. In RPC, JSON, and print modes, `/gentle:music` shows a notice and does nothing.
81
84
  - There is no default shortcut.
82
85
  - gentle-stream starts its own `gentle-shell` process to interpret moods; Gentle Shell does not pass its own launcher path.
@@ -397,7 +397,7 @@ The canvas server binds `127.0.0.1` on a random port and needs a random per-serv
397
397
 
398
398
  ### Gentle Music
399
399
 
400
- `/gentle:music [mood]` hands the terminal to the [gentle-stream](https://github.com/Gentleman-Programming/gentle-stream) player, like Pi's external editor, passing the mood and the session model. In the player, `b` returns to Pi with the music playing and `q` returns and stops it. `/gentle:music next`, `pause`, `play`, `stop`, and `status` control the music without leaving Pi. Quitting Pi after using the command stops the music. gentle-stream runs from `PATH`, or through `uvx`/`uv` when it is not installed. A bundled Gentle Shell without any of them offers, with your consent, to install the player inside its own folder: a pinned, checksum-verified uv 0.13.6 in `runtime/`, and the uv-managed Python 3.13 and gentle-stream in `music/` (about 150 MB to download, about 210 MB on disk). A pnpm or npm install keeps the manual install hint. See [Gentle Music](gentle-music.md).
400
+ `/gentle:music [mood]` hands the terminal to the [gentle-stream](https://github.com/Gentleman-Programming/gentle-stream) player, like Pi's external editor, passing the mood and the session model. In the player, `b` returns to Pi with the music playing and `q` returns and stops it. `/gentle:music next`, `pause`, `play`, `stop`, and `status` control the music without leaving Pi. Quitting Pi after using the command stops the music. gentle-stream runs from `PATH`, or through `uvx`/`uv` when it is not installed. On native Windows the player is not available yet (it needs POSIX; WSL works), so the command only says so. A bundled Gentle Shell on macOS or Linux without any of them offers, with your consent, to install the player inside its own folder: a pinned, checksum-verified uv 0.13.6 in `runtime/`, and the uv-managed Python 3.13 and gentle-stream in `music/` (about 150 MB to download, about 210 MB on disk). A pnpm or npm install keeps the manual install hint. See [Gentle Music](gentle-music.md).
401
401
 
402
402
  ### Bridge providers
403
403
 
@@ -988,7 +988,9 @@ Delegation contract:
988
988
  | Persona | Behavior |
989
989
  | ----------- | ------------------------------------------------------------------------------------------------------------- |
990
990
  | `gentleman` | Senior architect, teacher, direct technical feedback, Rioplatense Spanish/voseo when the user writes Spanish. |
991
- | `neutral` | Same discipline, warmer professional language, no regional expression. |
991
+ | `neutral` | Same discipline, warmer professional language, no regional expression, no voseo in any verb form. |
992
+
993
+ Both modes reply in the language of the user's latest message and never default to Spanish, so an English message gets an English reply even after context compaction.
992
994
 
993
995
  Saved globally at:
994
996
 
@@ -1405,7 +1405,7 @@ const PERSONA_OPTIONS = ["gentleman", "neutral"] as const;
1405
1405
 
1406
1406
  const GENTLEMAN_PERSONA_PROMPT = `Persona:
1407
1407
  - Be direct, technical, and concise.
1408
- - Always respond in the same language the user writes in.
1408
+ - Always respond in the same language the user writes in. Judge it by the user's latest message, never by earlier context or a compaction summary, and never default to Spanish: a message in English gets a reply in English.
1409
1409
  - When the user writes Spanish, answer in natural Rioplatense Spanish with voseo.
1410
1410
  - Act as a senior architect and teacher: concepts before code, no shortcuts.
1411
1411
  - Treat AI as a tool directed by the human; never present yourself as a default chatbot.
@@ -1414,9 +1414,9 @@ const GENTLEMAN_PERSONA_PROMPT = `Persona:
1414
1414
 
1415
1415
  const NEUTRAL_PERSONA_PROMPT = `Persona:
1416
1416
  - Be direct, technical, concise, warm, and professional.
1417
- - Always respond in the same language the user writes in.
1417
+ - Always respond in the same language the user writes in. Judge it by the user's latest message, never by earlier context or a compaction summary, and never default to Spanish: a message in English gets a reply in English.
1418
1418
  - Do not use slang or regional expressions.
1419
- - When the user writes Spanish, use neutral/professional Spanish. Do NOT use voseo (vos tenés, vos querés, hacé, andá, etc.) or any regional conjugations.
1419
+ - When the user writes Spanish, use neutral/professional Spanish. Do NOT use voseo or any regional conjugations, regular or irregular: use tú or usted forms, never vos forms (tienes, not tenés; ajustas, not ajustás; vives, not vivís; mira, not mirá; haz, not hacé).
1420
1420
  - Act as a senior architect and teacher: concepts before code, no shortcuts.
1421
1421
  - Treat AI as a tool directed by the human; never present yourself as a default chatbot.
1422
1422
  - Push back when the user asks for code without enough context or understanding.
@@ -4519,10 +4519,12 @@ function routingScreenNotices(
4519
4519
  const unmatched = unmatchedProfileKeys(Object.fromEntries(profileKeys.map((key) => [key, { model: undefined, thinking: undefined }])), known);
4520
4520
  const ignored = unmatched.filter((entry) => entry.suggestion === undefined).map((entry) => entry.key);
4521
4521
  if (ignored.length > 0) {
4522
- notices.push(`No agent matches ${ignored.map((key) => `"${key}"`).join(", ")} in this profile, so ${ignored.length === 1 ? "it is" : "they are"} ignored at launch.`);
4522
+ notices.push(
4523
+ `Profile key${ignored.length === 1 ? "" : "s"} ${ignored.map((key) => `"${key}"`).join(", ")} match${ignored.length === 1 ? "es" : ""} no agent configured in gentle-shell, so ${ignored.length === 1 ? "it is" : "they are"} ignored at launch.`,
4524
+ );
4523
4525
  }
4524
4526
  for (const entry of unmatched) {
4525
- if (entry.suggestion !== undefined) notices.push(`No agent is named "${entry.key}" in this profile; did you mean ${entry.suggestion}?`);
4527
+ if (entry.suggestion !== undefined) notices.push(`Profile key "${entry.key}" matches no agent configured in gentle-shell; did you mean ${entry.suggestion}?`);
4526
4528
  }
4527
4529
  // Issue #2400: model_profiles keys in subagents.json that match nothing and
4528
4530
  // suggest nothing. A session binding or a winning pin replaces those launches'
@@ -27,6 +27,9 @@ export type MusicControlCommand = (typeof MUSIC_CONTROL_COMMANDS)[number];
27
27
 
28
28
  export const MUSIC_INSTALL_TITLE = "Install the music player?";
29
29
  export const MUSIC_INSTALL_OFFER = "Install the gentle-stream music player inside Gentle Shell? It downloads uv, Python 3.13 and the player (about 150 MB) into the Gentle Shell folder.";
30
+ // gentle-stream needs POSIX (fcntl locks, an AF_UNIX control socket, process
31
+ // groups), so a native Windows Pi can neither install nor run it; WSL is Linux.
32
+ export const MUSIC_WINDOWS_UNSUPPORTED = "/gentle:music is not available on Windows yet: the gentle-stream player runs on macOS and Linux. It works when Gentle Shell runs inside WSL.";
30
33
  const UV_INSTALL_URL = "https://docs.astral.sh/uv/getting-started/installation/";
31
34
  const CTL_TIMEOUT_MS = 30_000;
32
35
  const SHUTDOWN_TIMEOUT_MS = 2_000;
@@ -85,6 +88,8 @@ export interface GentleMusicDeps {
85
88
  which?: MusicWhich;
86
89
  /** The bundled install's player, or undefined outside a bundled install. */
87
90
  bundled?: () => Promise<BundledMusicPlayer | undefined>;
91
+ /** The platform Pi runs on; gentle-stream does not run on native Windows. */
92
+ platform?: NodeJS.Platform;
88
93
  spawn?: MusicSpawn;
89
94
  signals?: MusicSignals;
90
95
  usage?: MusicUsage;
@@ -259,6 +264,7 @@ export default function gentleMusic(pi: ExtensionAPI, deps: GentleMusicDeps = {}
259
264
  const usage = deps.usage ?? processUsage();
260
265
  const shutdownTimeoutMs = deps.shutdownTimeoutMs ?? SHUTDOWN_TIMEOUT_MS;
261
266
  const bundled = deps.bundled ?? defaultBundled;
267
+ const platform = deps.platform ?? process.platform;
262
268
 
263
269
  const findPlayer = async (): Promise<{ stream?: GentleStreamCommand; offer?: BundledMusicPlayer }> => {
264
270
  let player: BundledMusicPlayer | undefined;
@@ -326,6 +332,10 @@ export default function gentleMusic(pi: ExtensionAPI, deps: GentleMusicDeps = {}
326
332
  description: "Open gentle-stream full screen (optional mood text); b returns with the music playing, q stops it. next, pause, play, stop, status control the music without leaving Pi.",
327
333
  handler: async (args, ctx) => {
328
334
  if (!ctx.hasUI) return;
335
+ if (platform === "win32") {
336
+ ctx.ui.notify(MUSIC_WINDOWS_UNSUPPORTED, "warning");
337
+ return;
338
+ }
329
339
  if (ctx.mode !== "tui") {
330
340
  ctx.ui.notify("/gentle:music needs the interactive terminal.", "warning");
331
341
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "4.5.0",
3
+ "version": "4.5.1",
4
4
  "description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -562,9 +562,13 @@ function openEvents(port: number) {
562
562
  req.on("error", reject);
563
563
  });
564
564
  req.end();
565
- const waitFor = (pattern: RegExp) => new Promise<RegExpExecArray>((resolve) => {
565
+ // A waiter that never matches must fail loud, not hang the whole suite:
566
+ // an exhausted inotify limit silently stops the server's change events.
567
+ const waitFor = (pattern: RegExp) => new Promise<RegExpExecArray>((resolve, reject) => {
568
+ const timeout = setTimeout(() => reject(new Error(`events never matched ${pattern} within 5s; got: ${JSON.stringify(buffer.slice(-200))}`)), 5000);
569
+ const settle = (match: RegExpExecArray) => { clearTimeout(timeout); resolve(match); };
566
570
  const match = pattern.exec(buffer);
567
- if (match) resolve(match); else waiters.push({ pattern, resolve });
571
+ if (match) settle(match); else waiters.push({ pattern, resolve: settle });
568
572
  });
569
573
  return { ready, waitFor, req };
570
574
  }
@@ -13,7 +13,7 @@ Identity contract:
13
13
 
14
14
  Persona:
15
15
  - Be direct, technical, and concise.
16
- - Always respond in the same language the user writes in.
16
+ - Always respond in the same language the user writes in. Judge it by the user's latest message, never by earlier context or a compaction summary, and never default to Spanish: a message in English gets a reply in English.
17
17
  - When the user writes Spanish, answer in natural Rioplatense Spanish with voseo.
18
18
  - Act as a senior architect and teacher: concepts before code, no shortcuts.
19
19
  - Treat AI as a tool directed by the human; never present yourself as a default chatbot.
@@ -1539,10 +1539,10 @@ test("models screen offers only the effort levels each model supports and shows
1539
1539
  test("models screen lists configuration problems under the profile name without cutting them", () => {
1540
1540
  const notices = [
1541
1541
  "The local pin file /home/user/project/.git/gentle-ai/profile-pin.json is not a valid pin and is ignored. Fix or remove it.",
1542
- 'No agent matches "sdd-design" in this profile, so it is ignored at launch.',
1542
+ 'Profile key "sdd-design" matches no agent configured in gentle-shell, so it is ignored at launch.',
1543
1543
  ];
1544
1544
  const plain = renderScreen([], [], [], { width: 80, notices }).map(stripAnsi);
1545
- assert.ok(plain.some((line) => line.includes('⚠ No agent matches "sdd-design"')));
1545
+ assert.ok(plain.some((line) => line.includes('⚠ Profile key "sdd-design"')));
1546
1546
  assert.ok(!plain.some((line) => line.includes("…")));
1547
1547
  });
1548
1548
 
@@ -3531,7 +3531,7 @@ test("the models screen flags profile keys no agent matches and never flags prov
3531
3531
  await fixture.run("gentle:profiles");
3532
3532
  const flat = (rendered: string) => rendered.replace(/\s*│\s*/g, " ").replace(/\s+/g, " ");
3533
3533
  assert.equal(rendered.split("\n").length, 24, "the frame spans the terminal height");
3534
- assert.match(flat(rendered), /⚠ No agent matches "sdd-design" in this profile, so it is ignored at launch\./);
3534
+ assert.match(flat(rendered), /⚠ Profile key "sdd-design" matches no agent configured in gentle-shell, so it is ignored at launch\./);
3535
3535
  assert.doesNotMatch(flat(rendered), /review-refuter"/, "provider review roles are never flagged");
3536
3536
  assert.match(rendered, /Orchestrator\s+nan\/glm5\.3\s+high/);
3537
3537
  assert.match(rendered, /worker\s+openai\/alpha\s+high/);
@@ -3555,7 +3555,7 @@ test("at 60 columns the unmatched-key notice wraps instead of being cut", async
3555
3555
  assert.ok(panel);
3556
3556
  const narrow = stripAnsi(panel.render(60).join("\n"));
3557
3557
  const flat = (rendered: string) => rendered.replace(/\s*│\s*/g, " ").replace(/\s+/g, " ");
3558
- assert.match(flat(narrow), /⚠ No agent matches "sdd-design" in this profile, so it is ignored at launch\./);
3558
+ assert.match(flat(narrow), /⚠ Profile key "sdd-design" matches no agent configured in gentle-shell, so it is ignored at launch\./);
3559
3559
  assert.ok(!narrow.split("\n").some((line) => line.includes("…")), "nothing is cut");
3560
3560
  });
3561
3561
 
@@ -5,13 +5,14 @@ import { tmpdir } from "node:os";
5
5
  import { delimiter, join } from "node:path";
6
6
  import test, { type TestContext } from "node:test";
7
7
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import gentleMusic, {
8
+ import gentleMusicExtension, {
9
9
  findOnPath,
10
10
  formatMusicStatus,
11
11
  GENTLE_STREAM_SPEC,
12
12
  installHint,
13
13
  MUSIC_COMMAND_NAME,
14
14
  MUSIC_INSTALL_OFFER,
15
+ MUSIC_WINDOWS_UNSUPPORTED,
15
16
  MUSIC_INSTALL_TITLE,
16
17
  musicTuiArgs,
17
18
  parseMusicArgs,
@@ -20,6 +21,11 @@ import gentleMusic, {
20
21
  type MusicSpawn,
21
22
  } from "../extensions/gentle-music.ts";
22
23
 
24
+ // The music tests describe a supported (POSIX) host unless they pass `platform`,
25
+ // so they hold on every OS the suite runs on, Windows included.
26
+ const gentleMusic = (pi: Parameters<typeof gentleMusicExtension>[0], deps: Parameters<typeof gentleMusicExtension>[1] = {}) =>
27
+ gentleMusicExtension(pi, { platform: "linux", ...deps });
28
+
23
29
  // /gentle:music hands the terminal to gentle-stream's own TUI, the way Pi's
24
30
  // external editor does, and controls the background player with
25
31
  // `gentle-stream ctl …` without taking the terminal.
@@ -411,6 +417,35 @@ test("accepting the offer installs the player with progress, then starts the mus
411
417
  assert.deepEqual(spawned[0]!.args, ["--model", "anthropic/claude-sonnet-5-5", "--mood", "calm"]);
412
418
  });
413
419
 
420
+ test("on Windows /gentle:music explains it is not available yet and never offers, installs or runs anything", async () => {
421
+ assert.equal(MUSIC_WINDOWS_UNSUPPORTED, "/gentle:music is not available on Windows yet: the gentle-stream player runs on macOS and Linux. It works when Gentle Shell runs inside WSL.");
422
+ for (const args of ["", "calm", "status", "stop"]) {
423
+ const { pi, commands } = fakePi();
424
+ const context = fakeContext();
425
+ const asked = withConfirm(context, true);
426
+ const { spawn, spawned } = fakeSpawn([{ code: 0 }], context.calls);
427
+ const { state, bundled } = fakeBundled({ installed: true });
428
+ gentleMusic(pi, { which: onPath({ "gentle-stream": "C:\\bin\\gentle-stream.exe", uvx: "C:\\bin\\uvx.exe" }), spawn, bundled, signals: fakeSignals().signals, platform: "win32" });
429
+ await commands.get(MUSIC_COMMAND_NAME)!.handler(args, context.ctx);
430
+ assert.deepEqual(context.notices, [[MUSIC_WINDOWS_UNSUPPORTED, "warning"]], args);
431
+ assert.deepEqual(asked, [], args);
432
+ assert.equal(state.installs, 0, args);
433
+ assert.equal(spawned.length, 0, args);
434
+ }
435
+ });
436
+
437
+ test("on Windows the not-available notice wins over the interactive-terminal notice", async () => {
438
+ for (const mode of ["rpc", "json", "print"]) {
439
+ const { pi, commands } = fakePi();
440
+ const context = fakeContext(mode);
441
+ const { spawn, spawned } = fakeSpawn([{ code: 0 }], context.calls);
442
+ gentleMusic(pi, { which: onPath({ uvx: "C:\\bin\\uvx.exe" }), spawn, platform: "win32" });
443
+ await commands.get(MUSIC_COMMAND_NAME)!.handler("", context.ctx);
444
+ assert.deepEqual(context.notices, [[MUSIC_WINDOWS_UNSUPPORTED, "warning"]], mode);
445
+ assert.equal(spawned.length, 0, mode);
446
+ }
447
+ });
448
+
414
449
  test("an installed prefix player is used without asking; PATH gentle-stream still comes first", async () => {
415
450
  const { bundled } = fakeBundled({ installed: true });
416
451
  for (const [names, expected] of [[{}, "/prefix/music/bin/gentle-stream"], [{ uvx: "/bin/uvx" }, "/prefix/music/bin/gentle-stream"], [{ "gentle-stream": "/bin/gentle-stream" }, "/bin/gentle-stream"]] as const) {
@@ -306,8 +306,8 @@ test("/api/plan names the found and required version of an older Gentle Shell an
306
306
  const view = await plan(port, await login());
307
307
  assert.deepEqual(view.blockers.map((blocker: { code: string; tool: string }) => [blocker.code, blocker.tool]), [["incompatible-tool", "shell"]]);
308
308
  assert.equal(view.blockers[0].guidance,
309
- "Gentle Shell 3.4.0 is installed globally with pnpm, but this installer needs 4.5.0 or newer. Nothing was replaced. " +
310
- "Update it with `pnpm add -g gentle-pi@4.5.0`, or remove it with `pnpm remove -g gentle-pi`, then select Check again.");
309
+ "Gentle Shell 3.4.0 is installed globally with pnpm, but this installer needs 4.5.1 or newer. Nothing was replaced. " +
310
+ "Update it with `pnpm add -g gentle-pi@4.5.1`, or remove it with `pnpm remove -g gentle-pi`, then select Check again.");
311
311
  assert.deepEqual(view.actions, []);
312
312
  } finally {
313
313
  await host.close("test");
@@ -1793,9 +1793,9 @@ test("pi-pretty wrapper uses cached ESM loading for compiled and pnpm symlink in
1793
1793
  assert.match(wrapper, /quietToolsEnabled/);
1794
1794
  });
1795
1795
 
1796
- test("Gentle Shell v4.5.0 package manifest declares the release version", () => {
1796
+ test("Gentle Shell v4.5.1 package manifest declares the release version", () => {
1797
1797
  const packageJson = readPackageJson();
1798
- assert.equal(packageJson.version, "4.5.0", "the release manifest must be explicitly pinned to v4.5.0");
1798
+ assert.equal(packageJson.version, "4.5.1", "the release manifest must be explicitly pinned to v4.5.1");
1799
1799
  assert.equal(packageJson.scripts?.test, "node scripts/run-test-suite.mjs");
1800
1800
  assert.ok(packageJson.files?.includes("assets/"));
1801
1801
  assert.ok(packageJson.files?.includes("contracts/"));
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { __testing } from "../extensions/gentle-ai.ts";
4
+
5
+ // gentle-shell #55 / #57 / #125: the reply language is inferred every turn,
6
+ // never stored, so the persona prompt itself must say how to infer it. The
7
+ // rule lives on the single existing language-match bullet (the concept-level
8
+ // duplication guard allows exactly one), so these tests read the rendered
9
+ // prompt rather than a second rule.
10
+
11
+ for (const persona of ["gentleman", "neutral"] as const) {
12
+ test(`[${persona}] the reply language follows the user's latest message, never a Spanish default (#125, #57)`, () => {
13
+ const prompt = __testing.buildGentlePrompt(persona);
14
+ assert.match(prompt, /Always respond in the same language the user writes in\./);
15
+ assert.match(
16
+ prompt,
17
+ /latest message, never by earlier context or a compaction summary/,
18
+ `${persona} prompt must judge the reply language by the user's latest message, so a compaction summary cannot flip it (#57)`,
19
+ );
20
+ assert.match(
21
+ prompt,
22
+ /never default to Spanish: a message in English gets a reply in English/,
23
+ `${persona} prompt must forbid a Spanish default, so an English message never gets a Spanish reply (#125)`,
24
+ );
25
+ });
26
+ }
27
+
28
+ test("[neutral] the voseo prohibition covers regular and irregular forms, not only four irregular verbs (#55)", () => {
29
+ const prompt = __testing.buildGentlePrompt("neutral");
30
+ assert.match(prompt, /Do NOT use voseo/);
31
+ assert.match(prompt, /regular or irregular: use tú or usted forms, never vos forms/);
32
+ for (const pair of ["ajustas, not ajustás", "vives, not vivís", "mira, not mirá", "haz, not hacé"]) {
33
+ assert.ok(prompt.includes(pair), `neutral prompt must name the voseo replacement "${pair}"`);
34
+ }
35
+ });
36
+
37
+ test("[gentleman] Spanish replies keep Rioplatense voseo and get no neutral-only rule", () => {
38
+ const prompt = __testing.buildGentlePrompt("gentleman");
39
+ assert.match(prompt, /When the user writes Spanish, answer in natural Rioplatense Spanish with voseo\./);
40
+ assert.match(prompt, /Language: natural Rioplatense Spanish with voseo when the user writes Spanish\./);
41
+ assert.doesNotMatch(prompt, /never vos forms/, "gentleman prompt must not inherit the neutral voseo prohibition");
42
+ });