switchroom 0.16.23 → 0.16.24

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.
Files changed (127) hide show
  1. package/dist/agent-scheduler/index.js +80 -80
  2. package/dist/auth-broker/index.js +80 -80
  3. package/dist/cli/autoaccept-poll.js +8 -8
  4. package/dist/cli/drive-write-pretool.mjs +10 -10
  5. package/dist/cli/notion-write-pretool.mjs +82 -82
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
  7. package/dist/cli/skill-validate-pretool.mjs +2936 -119
  8. package/dist/cli/switchroom.js +671 -430
  9. package/dist/host-control/main.js +156 -156
  10. package/dist/vault/approvals/kernel-server.js +82 -82
  11. package/dist/vault/broker/server.js +83 -83
  12. package/package.json +5 -5
  13. package/telegram-plugin/answer-stream.ts +13 -43
  14. package/telegram-plugin/auth-snapshot-format.ts +27 -30
  15. package/telegram-plugin/auto-fallback-fleet.ts +6 -11
  16. package/telegram-plugin/card-format.ts +28 -25
  17. package/telegram-plugin/credits-watch.ts +5 -10
  18. package/telegram-plugin/dist/bridge/bridge.js +112 -112
  19. package/telegram-plugin/dist/gateway/gateway.js +1855 -2077
  20. package/telegram-plugin/dist/server.js +160 -160
  21. package/telegram-plugin/draft-stream.ts +4 -4
  22. package/telegram-plugin/format.ts +101 -662
  23. package/telegram-plugin/gateway/approval-callback.ts +2 -3
  24. package/telegram-plugin/gateway/approval-card.test.ts +17 -4
  25. package/telegram-plugin/gateway/approval-card.ts +16 -6
  26. package/telegram-plugin/gateway/approvals-commands.ts +18 -24
  27. package/telegram-plugin/gateway/auth-command.ts +74 -74
  28. package/telegram-plugin/gateway/auth-line.ts +5 -15
  29. package/telegram-plugin/gateway/boot-card.ts +20 -22
  30. package/telegram-plugin/gateway/boot-version.ts +3 -2
  31. package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
  32. package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
  33. package/telegram-plugin/gateway/config-snapshot.ts +9 -9
  34. package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
  35. package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
  36. package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
  37. package/telegram-plugin/gateway/effort-command.ts +17 -17
  38. package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
  39. package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
  40. package/telegram-plugin/gateway/gateway.ts +751 -624
  41. package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
  42. package/telegram-plugin/gateway/inject-handler.ts +5 -5
  43. package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
  44. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  45. package/telegram-plugin/gateway/linear-activity.ts +16 -14
  46. package/telegram-plugin/gateway/linear-setup.ts +1 -1
  47. package/telegram-plugin/gateway/model-command.ts +25 -25
  48. package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
  49. package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
  50. package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
  51. package/telegram-plugin/issues-card.ts +6 -7
  52. package/telegram-plugin/model-unavailable.ts +8 -12
  53. package/telegram-plugin/operator-events-history.ts +1 -1
  54. package/telegram-plugin/operator-events.ts +24 -28
  55. package/telegram-plugin/package.json +1 -1
  56. package/telegram-plugin/pending-work-progress.ts +36 -36
  57. package/telegram-plugin/permission-title.ts +36 -20
  58. package/telegram-plugin/pty-partial-handler.ts +5 -13
  59. package/telegram-plugin/quota-check.ts +5 -5
  60. package/telegram-plugin/quota-watch.ts +13 -18
  61. package/telegram-plugin/recent-outbound-dedup.ts +5 -5
  62. package/telegram-plugin/registry/turns-schema.ts +43 -3
  63. package/telegram-plugin/retry-api-call.ts +15 -7
  64. package/telegram-plugin/rich-send.ts +57 -0
  65. package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
  66. package/telegram-plugin/secret-detect/vault-error.ts +29 -22
  67. package/telegram-plugin/shared/bot-runtime.ts +23 -6
  68. package/telegram-plugin/silence-poke.ts +26 -69
  69. package/telegram-plugin/slot-banner-driver.ts +9 -6
  70. package/telegram-plugin/slot-banner.ts +5 -8
  71. package/telegram-plugin/steering.ts +0 -4
  72. package/telegram-plugin/stream-controller.ts +59 -62
  73. package/telegram-plugin/stream-reply-handler.ts +40 -97
  74. package/telegram-plugin/subagent-watcher.ts +2 -2
  75. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
  76. package/telegram-plugin/tests/answer-stream.test.ts +54 -63
  77. package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
  78. package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
  79. package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
  80. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
  81. package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
  82. package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
  83. package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
  84. package/telegram-plugin/tests/bot-api.harness.ts +23 -1
  85. package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
  86. package/telegram-plugin/tests/card-format.test.ts +6 -4
  87. package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
  88. package/telegram-plugin/tests/credits-watch.test.ts +5 -5
  89. package/telegram-plugin/tests/fake-bot-api.ts +58 -4
  90. package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
  91. package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
  92. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
  93. package/telegram-plugin/tests/issues-card.test.ts +15 -12
  94. package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
  95. package/telegram-plugin/tests/model-command.test.ts +2 -2
  96. package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
  97. package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
  98. package/telegram-plugin/tests/operator-events.test.ts +7 -9
  99. package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
  100. package/telegram-plugin/tests/permission-title.test.ts +45 -41
  101. package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
  102. package/telegram-plugin/tests/quota-check.test.ts +3 -3
  103. package/telegram-plugin/tests/quota-watch.test.ts +8 -4
  104. package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
  105. package/telegram-plugin/tests/silence-poke.test.ts +75 -112
  106. package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
  107. package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
  108. package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
  109. package/telegram-plugin/tests/slot-banner.test.ts +9 -6
  110. package/telegram-plugin/tests/status-accent.test.ts +29 -32
  111. package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
  112. package/telegram-plugin/tests/stream-controller.test.ts +63 -52
  113. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
  114. package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
  115. package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
  116. package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
  117. package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
  118. package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
  119. package/telegram-plugin/tests/welcome-text.test.ts +72 -65
  120. package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
  121. package/telegram-plugin/text-voice-scrub.ts +8 -11
  122. package/telegram-plugin/tool-activity-summary.ts +29 -29
  123. package/telegram-plugin/welcome-text.ts +82 -83
  124. package/telegram-plugin/worker-activity-feed.ts +2 -3
  125. package/telegram-plugin/html-sanitize.ts +0 -244
  126. package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
  127. package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
@@ -30,9 +30,12 @@ const baseMeta: AgentMetadata = {
30
30
  auth: null,
31
31
  };
32
32
 
33
- describe("escapeHtml", () => {
34
- it("escapes &, <, >, \"", () => {
35
- expect(escapeHtml('<foo bar="baz">&')).toBe("&lt;foo bar=&quot;baz&quot;&gt;&amp;");
33
+ describe("escapeHtml (now a markdown escaper, kept under the legacy name, #2669)", () => {
34
+ it("escapes the inline-markdown specials (\\ ` * _ ~ = [ ] |); leaves < > & \" literal", () => {
35
+ // The HTML escaper is gone — this is escapeMarkdown under the old name.
36
+ // `< > & "` are literal in rich markdown; `* _ =` etc. are escaped.
37
+ expect(escapeHtml('<foo bar="baz">&')).toBe('<foo bar\\="baz">&');
38
+ expect(escapeHtml("a *b* _c_")).toBe("a \\*b\\* \\_c\\_");
36
39
  });
37
40
  it("leaves safe text alone", () => {
38
41
  expect(escapeHtml("hello world")).toBe("hello world");
@@ -63,19 +66,19 @@ describe("formatAuthLine", () => {
63
66
  expect(formatAuthLine({ authenticated: true, subscription_type: null, expires_in: null, auth_source: "oauth" }))
64
67
  .toBe("✓ subscription");
65
68
  });
66
- it("HTML-escapes subscription type", () => {
69
+ it("passes < > literally (markdown, #2669) in subscription type", () => {
67
70
  expect(formatAuthLine({ authenticated: true, subscription_type: "<injected>", expires_in: null, auth_source: "oauth" }))
68
- .toContain("&lt;injected&gt;");
71
+ .toContain("<injected>");
69
72
  });
70
73
  });
71
74
 
72
75
  describe("formatAgentLine", () => {
73
76
  it("includes model inline", () => {
74
- expect(formatAgentLine(baseMeta)).toContain("<code>sonnet</code>");
77
+ expect(formatAgentLine(baseMeta)).toContain("`sonnet`");
75
78
  });
76
79
  it("falls back to 'default' when model is null/empty", () => {
77
- expect(formatAgentLine({ ...baseMeta, model: null })).toContain("<code>default</code>");
78
- expect(formatAgentLine({ ...baseMeta, model: "" })).toContain("<code>default</code>");
80
+ expect(formatAgentLine({ ...baseMeta, model: null })).toContain("`default`");
81
+ expect(formatAgentLine({ ...baseMeta, model: "" })).toContain("`default`");
79
82
  });
80
83
  it("appends topic when present", () => {
81
84
  const out = formatAgentLine({ ...baseMeta, topicName: "Planning", topicEmoji: "🗓" });
@@ -84,8 +87,8 @@ describe("formatAgentLine", () => {
84
87
  it("shows the live session model alongside the configured model when a /model switch is active", () => {
85
88
  const out = formatAgentLine({ ...baseMeta, model: "claude-fable-5[1m]", sessionModel: "Opus 4.8 (1M context)" });
86
89
  // Both surfaces present + agree: configured AND what's actually running.
87
- expect(out).toContain("<code>claude-fable-5[1m]</code>");
88
- expect(out).toContain("live session: <code>Opus 4.8 (1M context)</code>");
90
+ expect(out).toContain("`claude-fable-5[1m]`");
91
+ expect(out).toContain("live session: `Opus 4.8 (1M context)`");
89
92
  });
90
93
  it("omits the session line when no override is active", () => {
91
94
  expect(formatAgentLine({ ...baseMeta, sessionModel: null })).not.toContain("live session");
@@ -96,9 +99,9 @@ describe("formatAgentLine", () => {
96
99
  // topicName null → no topic chunk. Keeps the line clean.
97
100
  expect(formatAgentLine({ ...baseMeta, topicEmoji: "🗓" })).not.toContain("topic");
98
101
  });
99
- it("HTML-escapes agent name", () => {
102
+ it("passes < > literally (markdown, #2669) in agent name", () => {
100
103
  expect(formatAgentLine({ ...baseMeta, agentName: "<script>" }))
101
- .toContain("&lt;script&gt;");
104
+ .toContain("<script>");
102
105
  });
103
106
  });
104
107
 
@@ -108,7 +111,7 @@ describe("startText", () => {
108
111
  });
109
112
  it("names the agent, not 'Claude Code session'", () => {
110
113
  const out = startText("assistant", false);
111
- expect(out).toContain("<b>assistant</b>");
114
+ expect(out).toContain("**assistant**");
112
115
  expect(out).not.toMatch(/Claude Code session/);
113
116
  });
114
117
  it("mentions pairing code flow", () => {
@@ -121,14 +124,14 @@ describe("startText", () => {
121
124
  expect(out).toContain("/status");
122
125
  expect(out).toContain("/commands");
123
126
  });
124
- it("HTML-escapes agent name", () => {
125
- expect(startText("<x>", false)).toContain("&lt;x&gt;");
127
+ it("passes < > literally (markdown, #2669) in agent name", () => {
128
+ expect(startText("<x>", false)).toContain("<x>");
126
129
  });
127
130
  });
128
131
 
129
132
  describe("helpText", () => {
130
133
  it("names the agent", () => {
131
- expect(helpText("klanker")).toContain("<b>klanker</b>");
134
+ expect(helpText("klanker")).toContain("**klanker**");
132
135
  });
133
136
  it("mentions the new Sprint 2/3 commands", () => {
134
137
  const out = helpText("assistant");
@@ -162,7 +165,7 @@ describe("statusPairedText", () => {
162
165
  const out = statusPairedText({ user: "@ken", meta });
163
166
  expect(out).toContain("Agent:");
164
167
  expect(out).toContain("Auth: ✓ Max · expires 29 days");
165
- expect(out).toContain("<code>sonnet</code>");
168
+ expect(out).toContain("`sonnet`");
166
169
  });
167
170
  it("omits status/uptime when absent", () => {
168
171
  expect(statusPairedText({ user: "@ken", meta })).not.toContain("Status:");
@@ -170,29 +173,32 @@ describe("statusPairedText", () => {
170
173
  it("includes status/uptime when present", () => {
171
174
  const withStatus: AgentMetadata = { ...meta, status: "running", uptime: "3h 12m" };
172
175
  const out = statusPairedText({ user: "@ken", meta: withStatus });
173
- expect(out).toContain("Status: <code>running</code> · up 3h 12m");
176
+ expect(out).toContain("Status: `running` · up 3h 12m");
174
177
  });
175
178
 
176
179
  // demo mode (the `/status demo` suffix) — masks the paired-user tag only.
177
180
  describe("demo mode", () => {
178
- it("WITHOUT demo, the real handle still renders", () => {
179
- expect(statusPairedText({ user: "@ken_real", meta })).toContain("Paired as @ken_real.");
181
+ it("WITHOUT demo, the real handle still renders (underscore markdown-escaped, #2669)", () => {
182
+ // The `_` in the handle is escaped so it can't open an italic run in
183
+ // the rich-markdown body — the handle renders as @ken\_real verbatim.
184
+ expect(statusPairedText({ user: "@ken_real", meta })).toContain("Paired as @ken\\_real.");
180
185
  });
181
186
  it("WITH demo, the handle is masked to a @demo_user form", () => {
182
187
  const out = statusPairedText({ user: "@ken_real", meta, demo: true });
183
188
  expect(out).not.toContain("@ken_real");
184
- expect(out).toMatch(/Paired as @demo_user\d*\./);
189
+ // The masked handle's underscore is markdown-escaped too.
190
+ expect(out).toMatch(/Paired as @demo\\_user\d*\./);
185
191
  });
186
192
  it("WITH demo, a numeric sender id is masked to a @handle, not a raw number", () => {
187
193
  const out = statusPairedText({ user: "12345", meta, demo: true });
188
194
  expect(out).not.toContain("12345");
189
- expect(out).toMatch(/Paired as @demo_user\d*\./);
195
+ expect(out).toMatch(/Paired as @demo\\_user\d*\./);
190
196
  });
191
197
  it("WITH demo, the agent/model/auth topology is NOT masked", () => {
192
198
  const out = statusPairedText({ user: "@ken_real", meta, demo: true });
193
199
  // Out-of-scope fields stay real.
194
200
  expect(out).toContain("Auth: ✓ Max · expires 29 days");
195
- expect(out).toContain("<code>sonnet</code>");
201
+ expect(out).toContain("`sonnet`");
196
202
  });
197
203
  });
198
204
 
@@ -220,21 +226,21 @@ describe("statusPairedText", () => {
220
226
  it("renders all audit rows when meta.audit is fully populated", () => {
221
227
  const withAudit: AgentMetadata = { ...meta, extendsProfile: "klanker", audit };
222
228
  const out = statusPairedText({ user: "@ken", meta: withAudit });
223
- expect(out).toContain("<b>Version</b> v0.3.0+44 · 2h ago");
224
- expect(out).toContain("<b>Profile</b> klanker");
225
- expect(out).toContain("<b>Tools</b> Read, Write, Bash, Edit, Grep +12 more");
226
- expect(out).toContain("<b>Deny</b> WebFetch");
227
- expect(out).toContain("<b>Skills</b> git, telegram, vault, +3 more");
228
- expect(out).toContain("<b>Limits</b> idle 30m, 50 turns");
229
- expect(out).toContain("<b>Channel</b> switchroom (default)");
230
- expect(out).toContain("<b>Memory</b> assistant");
229
+ expect(out).toContain("**Version** v0.3.0+44 · 2h ago");
230
+ expect(out).toContain("**Profile** klanker");
231
+ expect(out).toContain("**Tools** Read, Write, Bash, Edit, Grep +12 more");
232
+ expect(out).toContain("**Deny** WebFetch");
233
+ expect(out).toContain("**Skills** git, telegram, vault, +3 more");
234
+ expect(out).toContain("**Limits** idle 30m, 50 turns");
235
+ expect(out).toContain("**Channel** switchroom (default)");
236
+ expect(out).toContain("**Memory** assistant");
231
237
  });
232
238
 
233
239
  it("omits Deny row when toolsDeny is null", () => {
234
240
  const partial: AgentMetadata = { ...meta, audit: { ...audit, toolsDeny: null } };
235
241
  const out = statusPairedText({ user: "@ken", meta: partial });
236
242
  expect(out).not.toContain("Deny");
237
- expect(out).toContain("<b>Tools</b>");
243
+ expect(out).toContain("**Tools**");
238
244
  });
239
245
 
240
246
  it("omits Skills row when skills is null (agent has no bundled skills)", () => {
@@ -252,7 +258,7 @@ describe("statusPairedText", () => {
252
258
  expect(versionIdx).toBeGreaterThan(statusIdx);
253
259
  });
254
260
 
255
- it("escapes HTML in audit values", () => {
261
+ it("passes < > & literally (markdown, #2669) in audit values", () => {
256
262
  const hostile: AgentAuditLike = {
257
263
  version: "<script>alert(1)</script>",
258
264
  tools: "Read & <Write>",
@@ -263,18 +269,20 @@ describe("statusPairedText", () => {
263
269
  memoryBank: "bank<>name",
264
270
  };
265
271
  const out = statusPairedText({ user: "@ken", meta: { ...meta, audit: hostile } });
266
- expect(out).not.toContain("<script>alert");
267
- expect(out).toContain("&lt;script&gt;");
268
- expect(out).toContain("Read &amp; &lt;Write&gt;");
269
- expect(out).toContain("bank&lt;&gt;name");
272
+ // < > & are literal in rich markdown — no HTML entities. The hostile
273
+ // input is shown verbatim (it cannot inject formatting; only * _ ` etc.
274
+ // would be, and those are escaped).
275
+ expect(out).toContain("<script>alert(1)</script>");
276
+ expect(out).toContain("Read & <Write>");
277
+ expect(out).toContain("bank<>name");
270
278
  });
271
279
 
272
280
  it("handles empty extendsProfile (no Profile row when meta.extendsProfile is null)", () => {
273
281
  const withAudit: AgentMetadata = { ...meta, extendsProfile: null, audit };
274
282
  const out = statusPairedText({ user: "@ken", meta: withAudit });
275
- expect(out).not.toContain("<b>Profile</b>");
283
+ expect(out).not.toContain("**Profile**");
276
284
  // But other audit rows still render.
277
- expect(out).toContain("<b>Version</b>");
285
+ expect(out).toContain("**Version**");
278
286
  });
279
287
  });
280
288
 
@@ -284,12 +292,12 @@ describe("statusPairedText", () => {
284
292
  describe("live health block", () => {
285
293
  it("does NOT render a Health section when meta.live is undefined", () => {
286
294
  const out = statusPairedText({ user: "@ken", meta });
287
- expect(out).not.toContain("<b>Health</b>");
295
+ expect(out).not.toContain("**Health**");
288
296
  });
289
297
 
290
298
  it("does NOT render a Health section when meta.live is empty array", () => {
291
299
  const out = statusPairedText({ user: "@ken", meta: { ...meta, live: [] } });
292
- expect(out).not.toContain("<b>Health</b>");
300
+ expect(out).not.toContain("**Health**");
293
301
  });
294
302
 
295
303
  it("renders all probe rows including green ones", () => {
@@ -300,11 +308,11 @@ describe("statusPairedText", () => {
300
308
  { status: "fail", label: "Scheduler", detail: "sidecar not running" },
301
309
  ];
302
310
  const out = statusPairedText({ user: "@ken", meta: { ...meta, live } });
303
- expect(out).toContain("<b>Health</b>");
304
- expect(out).toContain("🟢 <b>Account</b> ken@x.com · Max · token 60d");
305
- expect(out).toContain("🟢 <b>Broker</b> reachable");
306
- expect(out).toContain("🟡 <b>Skills</b> 1/5 dangling: foo");
307
- expect(out).toContain("🔴 <b>Scheduler</b> sidecar not running");
311
+ expect(out).toContain("**Health**");
312
+ expect(out).toContain("🟢 **Account** ken@x.com · Max · token 60d");
313
+ expect(out).toContain("🟢 **Broker** reachable");
314
+ expect(out).toContain("🟡 **Skills** 1/5 dangling: foo");
315
+ expect(out).toContain("🔴 **Scheduler** sidecar not running");
308
316
  });
309
317
 
310
318
  it("renders Health section before the audit block", () => {
@@ -319,19 +327,18 @@ describe("statusPairedText", () => {
319
327
  user: "@ken",
320
328
  meta: { ...meta, live, audit },
321
329
  });
322
- const healthIdx = out.indexOf("<b>Health</b>");
323
- const versionIdx = out.indexOf("<b>Version</b>");
330
+ const healthIdx = out.indexOf("**Health**");
331
+ const versionIdx = out.indexOf("**Version**");
324
332
  expect(healthIdx).toBeGreaterThan(-1);
325
333
  expect(versionIdx).toBeGreaterThan(healthIdx);
326
334
  });
327
335
 
328
- it("escapes HTML in probe detail strings", () => {
336
+ it("passes < > literally (markdown, #2669) in probe detail strings", () => {
329
337
  const live: AgentMetadata["live"] = [
330
338
  { status: "fail", label: "Skills", detail: "<script>alert(1)</script>" },
331
339
  ];
332
340
  const out = statusPairedText({ user: "@ken", meta: { ...meta, live } });
333
- expect(out).not.toContain("<script>alert");
334
- expect(out).toContain("&lt;script&gt;");
341
+ expect(out).toContain("<script>alert(1)</script>");
335
342
  });
336
343
  });
337
344
  });
@@ -348,14 +355,14 @@ describe("statusPendingText / statusUnpairedText", () => {
348
355
  it("unpaired prompts the user to DM", () => {
349
356
  expect(statusUnpairedText()).toMatch(/Send me a message/);
350
357
  });
351
- it("pending escapes HTML in the code value", () => {
352
- expect(statusPendingText("<x>")).toContain("&lt;x&gt;");
358
+ it("passes < > literally (markdown, #2669) in the pending code value", () => {
359
+ expect(statusPendingText("<x>")).toContain("<x>");
353
360
  });
354
361
  });
355
362
 
356
363
  describe("switchroomHelpText + switchroomHelpCommandNames", () => {
357
364
  it("agent name appears in header", () => {
358
- expect(switchroomHelpText("klanker")).toContain("<b>klanker</b>");
365
+ expect(switchroomHelpText("klanker")).toContain("**klanker**");
359
366
  });
360
367
  it("every command in the autocomplete array is documented here", () => {
361
368
  const out = switchroomHelpText("assistant");
@@ -365,9 +372,9 @@ describe("switchroomHelpText + switchroomHelpCommandNames", () => {
365
372
  });
366
373
  it("groups commands into sections", () => {
367
374
  const out = switchroomHelpText("assistant");
368
- expect(out).toContain("<b>Session &amp; approvals</b>");
369
- expect(out).toContain("<b>Agents</b>");
370
- expect(out).toContain("<b>Auth &amp; config</b>");
375
+ expect(out).toContain("**Session & approvals**");
376
+ expect(out).toContain("**Agents**");
377
+ expect(out).toContain("**Auth & config**");
371
378
  });
372
379
  it("the name array contains the Sprint 2/3 additions", () => {
373
380
  for (const needed of ["new", "compact", "clear", "approve", "deny", "pending"]) {
@@ -455,11 +462,11 @@ describe("TELEGRAM_MENU_COMMANDS (slash-menu shape)", () => {
455
462
  const helpDoc = switchroomHelpText("clerk");
456
463
  // The /auth description string still mentions "reauth" as a
457
464
  // dashboard verb — that's intentional, not a registered command.
458
- // Pin that there's no top-level <code>/reauth ...</code> entry.
465
+ // Pin that there's no top-level `/reauth ...` entry.
459
466
  expect(
460
467
  helpDoc,
461
468
  "/reauth must not appear as a top-level command in help text",
462
- ).not.toMatch(/<code>\/reauth\b/);
469
+ ).not.toMatch(/`\/reauth\b/);
463
470
  });
464
471
 
465
472
  it("menu is short enough for a mobile keyboard (<= 21 entries)", () => {
@@ -486,18 +493,18 @@ describe("TELEGRAM_MENU_COMMANDS (slash-menu shape)", () => {
486
493
 
487
494
  describe("restart / new ack text", () => {
488
495
  it("restartAckText is consistent", () => {
489
- expect(restartAckText("assistant")).toBe("🔄 Restarting <b>assistant</b>…");
496
+ expect(restartAckText("assistant")).toBe("🔄 Restarting **assistant**…");
490
497
  });
491
498
  it("newSessionAckText with flush", () => {
492
499
  expect(newSessionAckText("assistant", true))
493
- .toBe("🆕 Started fresh session for <b>assistant</b> · flushed handoff · restarting…");
500
+ .toBe("🆕 Started fresh session for **assistant** · flushed handoff · restarting…");
494
501
  });
495
502
  it("newSessionAckText without flush", () => {
496
503
  expect(newSessionAckText("assistant", false))
497
- .toBe("🆕 Started fresh session for <b>assistant</b> · restarting…");
504
+ .toBe("🆕 Started fresh session for **assistant** · restarting…");
498
505
  });
499
- it("HTML-escapes agent name in both", () => {
500
- expect(restartAckText("<x>")).toContain("&lt;x&gt;");
501
- expect(newSessionAckText("<x>", true)).toContain("&lt;x&gt;");
506
+ it("passes < > literally (markdown, #2669) in agent name in both", () => {
507
+ expect(restartAckText("<x>")).toContain("<x>");
508
+ expect(newSessionAckText("<x>", true)).toContain("<x>");
502
509
  });
503
510
  });