switchroom 0.14.71 → 0.14.73
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/dist/cli/switchroom.js +3 -3
- package/package.json +1 -1
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +9 -5
- package/telegram-plugin/tests/text-voice-scrub.test.ts +46 -1
- package/telegram-plugin/text-voice-scrub.ts +44 -14
- package/telegram-plugin/uat/scenarios/fuzz-voice-scrub-dm.test.ts +23 -16
package/dist/cli/switchroom.js
CHANGED
|
@@ -49601,8 +49601,8 @@ var {
|
|
|
49601
49601
|
} = import__.default;
|
|
49602
49602
|
|
|
49603
49603
|
// src/build-info.ts
|
|
49604
|
-
var VERSION = "0.14.
|
|
49605
|
-
var COMMIT_SHA = "
|
|
49604
|
+
var VERSION = "0.14.73";
|
|
49605
|
+
var COMMIT_SHA = "bf5d1f94";
|
|
49606
49606
|
|
|
49607
49607
|
// src/cli/agent.ts
|
|
49608
49608
|
init_source();
|
|
@@ -52178,7 +52178,7 @@ function buildSettingsHooksBlock(p) {
|
|
|
52178
52178
|
|
|
52179
52179
|
` + "GROUND BEFORE YOU ASSERT. Any fact in your reply that can change " + "(a number, a status, a price, a date, who-uses-what, anything " + '"current" or "latest") must come from a source you actually checked ' + "THIS turn: your data tool, a file, the web. Memory and what you " + '"already know" are leads to verify, not sources. If you have not ' + "checked it this turn, do not state it as fact: go get it now, or tell " + "the user you will confirm and then do it. A confident wrong number is " + `worse than "let me check".
|
|
52180
52180
|
|
|
52181
|
-
` + "VOICE: write like a sharp colleague, not a chatbot.
|
|
52181
|
+
` + "VOICE: write like a sharp colleague, not a chatbot. Lead with the " + "answer, plain words, plain punctuation (commas and periods, not " + `em-dashes). Skip the hollow openers ("You're absolutely right", ` + '"Great question", "Great catch", "Exactly!") and AI-tell filler ' + `("smoking gun", "delve", "it's worth noting", "a testament to", "in ` + `today's fast-paced..."). Genuine acknowledgement is fine when it is ` + 'real and adds something ("good catch, that was my bug"); what to ' + "avoid is the reflexive praise that opens every reply and means " + "nothing. When the user is wrong, say so directly; flattery is not " + `help.
|
|
52182
52182
|
|
|
52183
52183
|
` + 'CRITICAL: "answer" means a call to the reply tool ' + "(mcp__switchroom-telegram__reply, or stream_reply with done=true). " + "Your terminal/transcript text is NEVER delivered to Telegram \u2014 the " + "user sees only what you send through the reply tool. After a long " + "tool sequence (scheduling, multi-step research, sub-agent handback), " + "do not let your closing narration stand as the answer: end the turn " + "by passing that narration to the reply tool. No reply tool call = the " + "user got nothing, however much text you wrote. Call the reply tool as " + "your FIRST action when you have the answer \u2014 do not write it out as " + "transcript text first and call reply afterward: a framework backstop " + "flushes unsent text after a delay and then your real reply lands late " + "and out of order.</turn-pacing>";
|
|
52184
52184
|
const switchroomUserPromptSubmit = [
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## What you are
|
|
4
4
|
|
|
5
|
-
You are a **switchroom agent** — an instance of **Claude Code** (Anthropic's official `claude` CLI, unmodified) running in a Linux container, managed by switchroom. Your `$SWITCHROOM_AGENT_NAME` is `{{name}}`.
|
|
5
|
+
You are a **switchroom agent** — an instance of **Claude Code** (Anthropic's official `claude` CLI, unmodified) running in a Linux container, managed by switchroom. Your `$SWITCHROOM_AGENT_NAME` is `{{name}}`. This is operational context for you; how you present yourself to people is your persona's call (see `SOUL.md`).
|
|
6
6
|
|
|
7
7
|
You are one of several agents here. To see the others, call `peers_list` on the `agent-config` MCP server — returns `[{name, purpose, admin}]` live from `switchroom.yaml`. **Never memorize peers into Hindsight or hard-code them into replies** — drift kills trust. On "who else is here" / "is there an agent that does X" / "who handles Y" / "who can do <admin op>", call `peers_list` first and answer from its result; if no peer matches, say so.
|
|
8
8
|
|
|
@@ -42543,6 +42543,10 @@ function enabled4() {
|
|
|
42543
42543
|
const v = process.env.SWITCHROOM_DISABLE_VOICE_SCRUB;
|
|
42544
42544
|
return !(v === "1" || v === "true");
|
|
42545
42545
|
}
|
|
42546
|
+
function openerStripEnabled() {
|
|
42547
|
+
const v = process.env.SWITCHROOM_VOICE_STRIP_OPENERS;
|
|
42548
|
+
return v === "1" || v === "true";
|
|
42549
|
+
}
|
|
42546
42550
|
var LEADING_AFFIRMATION_RE = /^(\s*)(you(?:['\u2019]| a)re absolutely right|you(?:['\u2019]| a)re so right|you(?:['\u2019]| a)re absolutely correct|absolutely right|exactly right|great catch|good catch|nice catch|spot on)\b(?:\s*$|\s*[!.,;:\u2014\u2013-][\s!.,;:\u2014\u2013-]*)/i;
|
|
42547
42551
|
function stripLeadingAffirmation(text) {
|
|
42548
42552
|
const m = LEADING_AFFIRMATION_RE.exec(text);
|
|
@@ -42620,7 +42624,7 @@ function scrubVoice(text) {
|
|
|
42620
42624
|
return { scrubbed: text, replaced: 0, openersStripped: 0 };
|
|
42621
42625
|
}
|
|
42622
42626
|
const { parked, parts } = park(text);
|
|
42623
|
-
const opener = stripLeadingAffirmation(parked);
|
|
42627
|
+
const opener = openerStripEnabled() ? stripLeadingAffirmation(parked) : { out: parked, count: 0 };
|
|
42624
42628
|
const { out, replaced } = replaceDashes(opener.out);
|
|
42625
42629
|
const total = replaced + opener.count;
|
|
42626
42630
|
if (total === 0) {
|
|
@@ -52815,10 +52819,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
52815
52819
|
}
|
|
52816
52820
|
|
|
52817
52821
|
// ../src/build-info.ts
|
|
52818
|
-
var VERSION = "0.14.
|
|
52819
|
-
var COMMIT_SHA = "
|
|
52820
|
-
var COMMIT_DATE = "2026-06-
|
|
52821
|
-
var LATEST_PR =
|
|
52822
|
+
var VERSION = "0.14.73";
|
|
52823
|
+
var COMMIT_SHA = "bf5d1f94";
|
|
52824
|
+
var COMMIT_DATE = "2026-06-06T01:43:37Z";
|
|
52825
|
+
var LATEST_PR = 2186;
|
|
52822
52826
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
52823
52827
|
|
|
52824
52828
|
// gateway/boot-version.ts
|
|
@@ -173,12 +173,17 @@ describe('scrubVoice — em / en dash replacement', () => {
|
|
|
173
173
|
})
|
|
174
174
|
})
|
|
175
175
|
|
|
176
|
-
describe('scrubVoice — leading sycophancy openers', () => {
|
|
176
|
+
describe('scrubVoice — leading sycophancy openers (opt-in backstop)', () => {
|
|
177
|
+
// The opener strip is OFF by default (tone is the prompt's job); these
|
|
178
|
+
// tests opt it in to exercise the mechanism that remains available via
|
|
179
|
+
// SWITCHROOM_VOICE_STRIP_OPENERS=1.
|
|
177
180
|
beforeEach(() => {
|
|
178
181
|
delete process.env.SWITCHROOM_DISABLE_VOICE_SCRUB
|
|
182
|
+
process.env.SWITCHROOM_VOICE_STRIP_OPENERS = '1'
|
|
179
183
|
})
|
|
180
184
|
afterEach(() => {
|
|
181
185
|
delete process.env.SWITCHROOM_DISABLE_VOICE_SCRUB
|
|
186
|
+
delete process.env.SWITCHROOM_VOICE_STRIP_OPENERS
|
|
182
187
|
})
|
|
183
188
|
|
|
184
189
|
it('strips a leading "You\'re absolutely right" and recapitalizes', () => {
|
|
@@ -261,3 +266,43 @@ describe('scrubVoice — leading sycophancy openers', () => {
|
|
|
261
266
|
expect(r.openersStripped).toBe(0)
|
|
262
267
|
})
|
|
263
268
|
})
|
|
269
|
+
|
|
270
|
+
describe('scrubVoice — opener strip is OFF by default (prompt carries tone)', () => {
|
|
271
|
+
// No SWITCHROOM_VOICE_STRIP_OPENERS set: the deterministic layer must
|
|
272
|
+
// NOT delete words. Em-dash normalization (punctuation, no content
|
|
273
|
+
// removed) still runs. Tone is the prompt VOICE directive's job.
|
|
274
|
+
beforeEach(() => {
|
|
275
|
+
delete process.env.SWITCHROOM_DISABLE_VOICE_SCRUB
|
|
276
|
+
delete process.env.SWITCHROOM_VOICE_STRIP_OPENERS
|
|
277
|
+
})
|
|
278
|
+
afterEach(() => {
|
|
279
|
+
delete process.env.SWITCHROOM_VOICE_STRIP_OPENERS
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('does NOT strip a leading affirmation by default', () => {
|
|
283
|
+
const r = scrubVoice("You're absolutely right, the build is broken.")
|
|
284
|
+
expect(r.scrubbed).toBe("You're absolutely right, the build is broken.")
|
|
285
|
+
expect(r.openersStripped).toBe(0)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('does NOT strip "Great catch" by default', () => {
|
|
289
|
+
const r = scrubVoice('Great catch! Fixed it.')
|
|
290
|
+
expect(r.scrubbed).toBe('Great catch! Fixed it.')
|
|
291
|
+
expect(r.openersStripped).toBe(0)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('STILL normalizes em-dashes by default (punctuation, no content removed)', () => {
|
|
295
|
+
const r = scrubVoice('on it — checking the calendar')
|
|
296
|
+
expect(r.scrubbed).toBe('on it, checking the calendar')
|
|
297
|
+
expect(r.replaced).toBe(1)
|
|
298
|
+
expect(r.openersStripped).toBe(0)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('an affirmation opener with an em-dash keeps the words, fixes only the dash', () => {
|
|
302
|
+
const r = scrubVoice('Exactly right — the token had expired.')
|
|
303
|
+
// Opener preserved; the em-dash after it becomes a comma.
|
|
304
|
+
expect(r.scrubbed).toBe('Exactly right, the token had expired.')
|
|
305
|
+
expect(r.openersStripped).toBe(0)
|
|
306
|
+
expect(r.replaced).toBe(1)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
@@ -12,22 +12,27 @@
|
|
|
12
12
|
* owns enforcement, soft instructions fail under load. Make the
|
|
13
13
|
* framework do it.
|
|
14
14
|
*
|
|
15
|
-
* Scope.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Scope. By default ONE mechanical transform, the only one that removes
|
|
16
|
+
* no content:
|
|
17
|
+
* 1. Em / en dashes -> comma/period/hyphen. Pure punctuation
|
|
18
|
+
* substitution, no semantic loss on whitespace-separated prose; a
|
|
19
|
+
* no-op inside code or a URL. Kept deterministic because prompt-only
|
|
20
|
+
* guidance was measured to fail at it (em-dashes in 73% of replies
|
|
21
|
+
* despite the SOUL rule).
|
|
22
|
+
*
|
|
23
|
+
* OPT-IN, off by default:
|
|
19
24
|
* 2. Leading sycophancy openers ("You're absolutely right", "Great
|
|
20
|
-
* catch"
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
25
|
+
* catch") -> deleted + recapitalized. This one removes WORDS, and a
|
|
26
|
+
* context-free hook can strip a sincere "good catch, that was my
|
|
27
|
+
* bug" along with the hollow kind. Per operator steer (2026-06),
|
|
28
|
+
* tone is carried by the prompt VOICE directive (where the model has
|
|
29
|
+
* context to keep genuine acknowledgement and drop only the empty
|
|
30
|
+
* reflexive praise), not by blind deletion here. Re-enable the
|
|
31
|
+
* backstop with `SWITCHROOM_VOICE_STRIP_OPENERS=1`.
|
|
26
32
|
*
|
|
27
|
-
*
|
|
33
|
+
* Always scoped OUT: the wider mid-sentence "AI-tell phrase denylist"
|
|
28
34
|
* (smoking gun, delve, etc.). Substituting those mid-clause risks
|
|
29
|
-
* semantic loss, so they stay with the prompt-side voice guidance
|
|
30
|
-
* (the turn-pacing VOICE directive), not this mechanical gate.
|
|
35
|
+
* semantic loss, so they stay with the prompt-side voice guidance.
|
|
31
36
|
*
|
|
32
37
|
* Pipeline integration. Apply BEFORE markdownToHtml so the scrub
|
|
33
38
|
* runs on the original model text, not on rendered HTML where
|
|
@@ -82,6 +87,25 @@ function enabled(): boolean {
|
|
|
82
87
|
return !(v === '1' || v === 'true')
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Leading-affirmation stripping is OPT-IN and OFF by default.
|
|
92
|
+
*
|
|
93
|
+
* Rationale (operator steer, 2026-06): tone should be carried by the
|
|
94
|
+
* prompt (the VOICE directive), where the model has context to keep a
|
|
95
|
+
* genuine acknowledgement and drop only the hollow reflexive kind.
|
|
96
|
+
* Deleting an opener in a context-free hook is bad UX: it can strip a
|
|
97
|
+
* sincere "good catch, that was my bug" along with the empty praise.
|
|
98
|
+
* So the deterministic layer no longer removes WORDS by default; it
|
|
99
|
+
* only normalizes em/en dashes (a punctuation substitution that removes
|
|
100
|
+
* no content, and that prompt-only guidance was measured to fail at).
|
|
101
|
+
* Set `SWITCHROOM_VOICE_STRIP_OPENERS=1` to re-enable the deterministic
|
|
102
|
+
* opener strip as a backstop.
|
|
103
|
+
*/
|
|
104
|
+
function openerStripEnabled(): boolean {
|
|
105
|
+
const v = process.env.SWITCHROOM_VOICE_STRIP_OPENERS
|
|
106
|
+
return v === '1' || v === 'true'
|
|
107
|
+
}
|
|
108
|
+
|
|
85
109
|
/**
|
|
86
110
|
* Leading sycophancy/affirmation openers. Matched ONLY at the very start
|
|
87
111
|
* of the message, ONLY this known pure-filler set, and the trailing
|
|
@@ -250,7 +274,13 @@ export function scrubVoice(text: string): VoiceScrubResult {
|
|
|
250
274
|
return { scrubbed: text, replaced: 0, openersStripped: 0 }
|
|
251
275
|
}
|
|
252
276
|
const { parked, parts } = park(text)
|
|
253
|
-
|
|
277
|
+
// Opener strip is opt-in (default off) — see openerStripEnabled(). By
|
|
278
|
+
// default the deterministic layer removes no words; only em/en dashes
|
|
279
|
+
// are normalized below, and tone is left to the prompt's VOICE
|
|
280
|
+
// directive where the model can judge genuine vs hollow.
|
|
281
|
+
const opener = openerStripEnabled()
|
|
282
|
+
? stripLeadingAffirmation(parked)
|
|
283
|
+
: { out: parked, count: 0 }
|
|
254
284
|
const { out, replaced } = replaceDashes(opener.out)
|
|
255
285
|
const total = replaced + opener.count
|
|
256
286
|
if (total === 0) {
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Voice-scrub fuzz — end-to-end
|
|
2
|
+
* Voice-scrub fuzz — end-to-end check of the voice layers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Two layers with different strengths:
|
|
5
|
+
* - Em/en dashes are DETERMINISTIC: the gateway's `scrubVoice`
|
|
6
|
+
* normalizes every dash on every outbound reply, so "no em-dash
|
|
7
|
+
* reaches the user" is a hard, observable guarantee (asserted).
|
|
8
|
+
* - Sycophancy openers are PROBABILISTIC: the deterministic opener
|
|
9
|
+
* strip is off by default (operator steer 2026-06: context-free word
|
|
10
|
+
* deletion is bad UX), and tone is carried by the prompt VOICE
|
|
11
|
+
* directive where the model can keep genuine acknowledgement. So an
|
|
12
|
+
* opener is a soft signal (warn), not a gate failure.
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the user, the gate failed.
|
|
14
|
+
* This fuzz file drives REAL Telegram inbounds engineered to bait both
|
|
15
|
+
* (statements the agent will want to affirm; prose asks where models
|
|
16
|
+
* reach for em-dashes). mtcute's view of the sent message is ground
|
|
17
|
+
* truth for the deterministic dash check.
|
|
16
18
|
*
|
|
17
19
|
* Self-skips green when the harness can't spin up (env unwired) — same as
|
|
18
20
|
* the sibling fuzz files; uat/** is excluded from gating CI.
|
|
@@ -61,7 +63,7 @@ const LEADING_AFFIRMATION_RE =
|
|
|
61
63
|
describe("uat: voice-scrub fuzz — no em-dashes, no sycophancy openers reach the user", () => {
|
|
62
64
|
for (const vc of VOICE_CASES) {
|
|
63
65
|
it(
|
|
64
|
-
`[voice] ${vc.name} — reply is dash-free
|
|
66
|
+
`[voice] ${vc.name} — reply is dash-free (affirmation now prompt-tier)`,
|
|
65
67
|
async () => {
|
|
66
68
|
const sc = await spinUp({ agent: "test-harness" });
|
|
67
69
|
try {
|
|
@@ -87,10 +89,15 @@ describe("uat: voice-scrub fuzz — no em-dashes, no sycophancy openers reach th
|
|
|
87
89
|
);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
// Invariant 3: reply does not OPEN with a
|
|
92
|
+
// Invariant 3 (SOFT): reply ideally does not OPEN with a hollow
|
|
93
|
+
// affirmation. This is now PROBABILISTIC, not deterministic: the
|
|
94
|
+
// opener strip is off by default and tone is the prompt VOICE
|
|
95
|
+
// directive's job, so an occasional opener is a soft signal (the
|
|
96
|
+
// model judged it genuine), not a hard gate failure. Warn, don't
|
|
97
|
+
// fail — a hard assert here would flake on a prompt-driven lever.
|
|
91
98
|
if (LEADING_AFFIRMATION_RE.test(text.trim())) {
|
|
92
|
-
|
|
93
|
-
`[voice] ${vc.name}: reply opened with
|
|
99
|
+
console.warn(
|
|
100
|
+
`[voice] ${vc.name}: reply opened with an affirmation (prompt-tier, not enforced). `
|
|
94
101
|
+ `Reply: ${JSON.stringify(text.slice(0, 200))}`,
|
|
95
102
|
);
|
|
96
103
|
}
|