switchroom 0.16.23 → 0.16.27
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/agent-scheduler/index.js +80 -80
- package/dist/auth-broker/index.js +80 -80
- package/dist/cli/autoaccept-poll.js +8 -8
- package/dist/cli/drive-write-pretool.mjs +10 -10
- package/dist/cli/notion-write-pretool.mjs +82 -82
- package/dist/cli/self-improve-apply-guard-pretool.mjs +6 -0
- package/dist/cli/skill-validate-pretool.mjs +2936 -119
- package/dist/cli/switchroom.js +804 -465
- package/dist/host-control/main.js +169 -163
- package/dist/vault/approvals/kernel-server.js +82 -82
- package/dist/vault/broker/server.js +83 -83
- package/package.json +4 -4
- package/telegram-plugin/answer-stream.ts +20 -49
- package/telegram-plugin/auth-snapshot-format.ts +27 -30
- package/telegram-plugin/auto-fallback-fleet.ts +6 -11
- package/telegram-plugin/bridge/bridge.ts +1 -1
- package/telegram-plugin/card-format.ts +28 -25
- package/telegram-plugin/credits-watch.ts +5 -10
- package/telegram-plugin/dist/bridge/bridge.js +113 -113
- package/telegram-plugin/dist/gateway/gateway.js +2085 -2102
- package/telegram-plugin/dist/server.js +161 -161
- package/telegram-plugin/draft-stream.ts +4 -4
- package/telegram-plugin/format.ts +427 -680
- package/telegram-plugin/gateway/approval-callback.ts +2 -3
- package/telegram-plugin/gateway/approval-card.test.ts +17 -4
- package/telegram-plugin/gateway/approval-card.ts +16 -6
- package/telegram-plugin/gateway/approvals-commands.ts +18 -24
- package/telegram-plugin/gateway/auth-command.ts +74 -74
- package/telegram-plugin/gateway/auth-line.ts +5 -15
- package/telegram-plugin/gateway/boot-card.ts +20 -22
- package/telegram-plugin/gateway/boot-version.ts +3 -2
- package/telegram-plugin/gateway/config-approval-handler.test.ts +35 -33
- package/telegram-plugin/gateway/config-approval-handler.ts +24 -24
- package/telegram-plugin/gateway/config-snapshot.ts +9 -9
- package/telegram-plugin/gateway/diff-preview-card.test.ts +8 -8
- package/telegram-plugin/gateway/diff-preview-card.ts +2 -5
- package/telegram-plugin/gateway/disconnect-flush.ts +0 -4
- package/telegram-plugin/gateway/drive-write-approval.test.ts +10 -10
- package/telegram-plugin/gateway/drive-write-approval.ts +14 -8
- package/telegram-plugin/gateway/effort-command.ts +17 -17
- package/telegram-plugin/gateway/folder-picker-handler.test.ts +8 -2
- package/telegram-plugin/gateway/folder-picker-handler.ts +3 -4
- package/telegram-plugin/gateway/gateway.ts +881 -633
- package/telegram-plugin/gateway/inject-handler.test.ts +15 -13
- package/telegram-plugin/gateway/inject-handler.ts +5 -5
- package/telegram-plugin/gateway/ipc-protocol.ts +33 -1
- package/telegram-plugin/gateway/ipc-server.ts +39 -6
- package/telegram-plugin/gateway/linear-activity.ts +16 -14
- package/telegram-plugin/gateway/linear-setup.ts +1 -1
- package/telegram-plugin/gateway/model-command.ts +25 -25
- package/telegram-plugin/gateway/oversize-card-body.ts +6 -7
- package/telegram-plugin/gateway/permission-timeout.ts +76 -0
- package/telegram-plugin/gateway/skill-proposal-card.ts +167 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +19 -13
- package/telegram-plugin/issues-card.ts +6 -7
- package/telegram-plugin/model-unavailable.ts +8 -12
- package/telegram-plugin/operator-events-history.ts +1 -1
- package/telegram-plugin/operator-events.ts +24 -28
- package/telegram-plugin/package.json +1 -1
- package/telegram-plugin/pending-work-progress.ts +36 -36
- package/telegram-plugin/permission-title.ts +39 -20
- package/telegram-plugin/pty-partial-handler.ts +5 -13
- package/telegram-plugin/quota-check.ts +5 -5
- package/telegram-plugin/quota-watch.ts +13 -18
- package/telegram-plugin/recent-outbound-dedup.ts +5 -5
- package/telegram-plugin/registry/turns-schema.ts +43 -3
- package/telegram-plugin/retry-api-call.ts +42 -7
- package/telegram-plugin/rich-send.ts +86 -0
- package/telegram-plugin/secret-detect/vault-error.test.ts +6 -6
- package/telegram-plugin/secret-detect/vault-error.ts +29 -22
- package/telegram-plugin/shared/bot-runtime.ts +29 -7
- package/telegram-plugin/silence-poke.ts +26 -69
- package/telegram-plugin/silent-reply-anchor.ts +9 -2
- package/telegram-plugin/slot-banner-driver.ts +9 -6
- package/telegram-plugin/slot-banner.ts +5 -8
- package/telegram-plugin/status-no-truncate.ts +11 -5
- package/telegram-plugin/steering.ts +0 -4
- package/telegram-plugin/stream-controller.ts +59 -62
- package/telegram-plugin/stream-reply-handler.ts +49 -98
- package/telegram-plugin/subagent-watcher.ts +2 -2
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +5 -2
- package/telegram-plugin/tests/answer-stream.test.ts +54 -63
- package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
- package/telegram-plugin/tests/auth-command-vernacular.test.ts +3 -2
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +19 -18
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +15 -13
- package/telegram-plugin/tests/boot-card-reason-to-render.test.ts +27 -12
- package/telegram-plugin/tests/boot-card-render.test.ts +59 -48
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/bot-api.harness.ts +23 -1
- package/telegram-plugin/tests/bot-runtime.test.ts +23 -18
- package/telegram-plugin/tests/card-format.test.ts +6 -4
- package/telegram-plugin/tests/config-snapshot.test.ts +1 -1
- package/telegram-plugin/tests/credits-watch.test.ts +5 -5
- package/telegram-plugin/tests/fake-bot-api.ts +58 -4
- package/telegram-plugin/tests/finalize-callback.test.ts +11 -9
- package/telegram-plugin/tests/foreground-nesting.test.ts +1 -1
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +3 -13
- package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +6 -2
- package/telegram-plugin/tests/issues-card.test.ts +15 -12
- package/telegram-plugin/tests/length-error-classify.test.ts +131 -0
- package/telegram-plugin/tests/linear-agent-activity.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +2 -2
- package/telegram-plugin/tests/model-unavailable.test.ts +13 -13
- package/telegram-plugin/tests/multi-turn-continuity.test.ts +6 -10
- package/telegram-plugin/tests/operator-events.test.ts +7 -9
- package/telegram-plugin/tests/paragraph-normalizer.test.ts +273 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +20 -21
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +12 -2
- package/telegram-plugin/tests/permission-timeout.test.ts +77 -0
- package/telegram-plugin/tests/permission-title.test.ts +88 -41
- package/telegram-plugin/tests/pty-partial-handler.test.ts +8 -8
- package/telegram-plugin/tests/quota-check.test.ts +3 -3
- package/telegram-plugin/tests/quota-watch.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +4 -3
- package/telegram-plugin/tests/silence-poke.test.ts +75 -112
- package/telegram-plugin/tests/single-mode-stream-reply.test.ts +137 -0
- package/telegram-plugin/tests/skill-proposal-card.test.ts +103 -0
- package/telegram-plugin/tests/slot-banner-driver.e2e.test.ts +36 -24
- package/telegram-plugin/tests/slot-banner.test.ts +9 -6
- package/telegram-plugin/tests/status-accent.test.ts +29 -32
- package/telegram-plugin/tests/{stream-controller-html-fallback.test.ts → stream-controller-parse-fallback.test.ts} +40 -42
- package/telegram-plugin/tests/stream-controller.test.ts +63 -52
- package/telegram-plugin/tests/stream-reply-error-paths.test.ts +43 -38
- package/telegram-plugin/tests/stream-reply-handler.test.ts +122 -249
- package/telegram-plugin/tests/streaming-e2e.test.ts +35 -30
- package/telegram-plugin/tests/streaming-orchestration.test.ts +29 -28
- package/telegram-plugin/tests/telegram-format.test.ts +120 -1083
- package/telegram-plugin/tests/tool-activity-summary.test.ts +144 -145
- package/telegram-plugin/tests/welcome-text.test.ts +72 -65
- package/telegram-plugin/tests/worker-activity-feed.test.ts +119 -137
- package/telegram-plugin/text-voice-scrub.ts +8 -11
- package/telegram-plugin/tool-activity-summary.ts +29 -29
- package/telegram-plugin/welcome-text.ts +82 -83
- package/telegram-plugin/worker-activity-feed.ts +2 -3
- package/telegram-plugin/html-sanitize.ts +0 -244
- package/telegram-plugin/tests/html-sanitize.test.ts +0 -146
- package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -162
|
@@ -128,7 +128,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
128
128
|
agentName: 'gymbro',
|
|
129
129
|
})
|
|
130
130
|
expect(body).toBe(
|
|
131
|
-
['🔐
|
|
131
|
+
['🔐 **Gymbro** wants to edit: supplement-log.md', 'why: _logging today\'s lifts_'].join('\n'),
|
|
132
132
|
)
|
|
133
133
|
})
|
|
134
134
|
|
|
@@ -141,7 +141,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
141
141
|
description: 'Restart an agent via the host-control daemon. cross-agent (`name` ≠ $SWITCHROOM_AGENT_NAME) …',
|
|
142
142
|
agentName: 'carrie',
|
|
143
143
|
})
|
|
144
|
-
expect(body).toContain('why:
|
|
144
|
+
expect(body).toContain('why: _gateway is wedged, bouncing it_')
|
|
145
145
|
expect(body).not.toContain('$SWITCHROOM_AGENT_NAME')
|
|
146
146
|
expect(body).not.toContain('host-control daemon')
|
|
147
147
|
})
|
|
@@ -153,7 +153,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
153
153
|
description: 'Run a shell command.',
|
|
154
154
|
agentName: 'gymbro',
|
|
155
155
|
})
|
|
156
|
-
expect(body).toContain('why:
|
|
156
|
+
expect(body).toContain('why: _listing temp files_')
|
|
157
157
|
})
|
|
158
158
|
|
|
159
159
|
test('shows "not provided" when no caller reason is present (never the description)', () => {
|
|
@@ -163,7 +163,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
163
163
|
description: 'Run a shell command on the host.',
|
|
164
164
|
agentName: 'gymbro',
|
|
165
165
|
})
|
|
166
|
-
expect(body).toContain('why:
|
|
166
|
+
expect(body).toContain('why: _not provided_')
|
|
167
167
|
expect(body).not.toContain('Run a shell command')
|
|
168
168
|
})
|
|
169
169
|
|
|
@@ -174,7 +174,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
174
174
|
description: 'Run a shell command.',
|
|
175
175
|
agentName: 'gymbro',
|
|
176
176
|
})
|
|
177
|
-
expect(body).toContain('why:
|
|
177
|
+
expect(body).toContain('why: _not provided_')
|
|
178
178
|
})
|
|
179
179
|
|
|
180
180
|
test('drops the agent prefix when agentName is null (early-boot edge)', () => {
|
|
@@ -184,21 +184,23 @@ describe('formatPermissionCardBody', () => {
|
|
|
184
184
|
description: 'Use a skill.',
|
|
185
185
|
agentName: null,
|
|
186
186
|
})
|
|
187
|
-
expect(body).toBe(['🔐 Use the mail skill', 'why:
|
|
187
|
+
expect(body).toBe(['🔐 Use the mail skill', 'why: _do the thing_'].join('\n'))
|
|
188
188
|
})
|
|
189
189
|
|
|
190
|
-
test('
|
|
190
|
+
test('markdown-escapes emphasis specials in agentName / action / reason; passes < > & through (#2669)', () => {
|
|
191
191
|
const body = formatPermissionCardBody({
|
|
192
192
|
toolName: 'Bash',
|
|
193
|
-
inputPreview: JSON.stringify({ command: 'echo "
|
|
193
|
+
inputPreview: JSON.stringify({ command: 'echo "a_b *c*"', reason: 'compare a_b and c < d' }),
|
|
194
194
|
description: 'Run a shell command.',
|
|
195
|
-
agentName: '
|
|
195
|
+
agentName: 'agent_test',
|
|
196
196
|
})
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
expect(body).
|
|
200
|
-
expect(body).toContain('<
|
|
201
|
-
|
|
197
|
+
// < > & are literal in rich markdown; the underscore in the agent name is
|
|
198
|
+
// backslash-escaped so it can't open an italic run.
|
|
199
|
+
expect(body).toContain('Agent\\_test')
|
|
200
|
+
expect(body).toContain('c < d')
|
|
201
|
+
// The card's own wrappers (** for the name, _ for the why line) are present.
|
|
202
|
+
expect(body).toContain('**')
|
|
203
|
+
expect(body).toContain('why: _')
|
|
202
204
|
})
|
|
203
205
|
|
|
204
206
|
test('truncates a very long caller reason with an ellipsis', () => {
|
|
@@ -208,8 +210,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
208
210
|
description: 'Use a skill.',
|
|
209
211
|
agentName: 'clerk',
|
|
210
212
|
})
|
|
211
|
-
expect(body).toContain('xxxx
|
|
212
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
213
|
+
expect(body).toContain('xxxx…_')
|
|
214
|
+
expect(body.split('\n')[0]).toBe('🔐 **Clerk** wants to use the mail skill')
|
|
213
215
|
})
|
|
214
216
|
|
|
215
217
|
test('collapses internal whitespace in the caller reason', () => {
|
|
@@ -219,7 +221,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
219
221
|
description: 'Use a skill.',
|
|
220
222
|
agentName: 'clerk',
|
|
221
223
|
})
|
|
222
|
-
expect(body).toContain('why:
|
|
224
|
+
expect(body).toContain('why: _first second paragraph_')
|
|
223
225
|
})
|
|
224
226
|
|
|
225
227
|
// config-edit-hardening: upstream Claude Code truncates `inputPreview`
|
|
@@ -247,7 +249,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
247
249
|
description: 'Propose a unified-diff patch against switchroom.yaml.',
|
|
248
250
|
agentName: 'klanker',
|
|
249
251
|
})
|
|
250
|
-
expect(body).toContain(`why:
|
|
252
|
+
expect(body).toContain(`why: _${reason}_`)
|
|
251
253
|
expect(body).not.toContain('not provided')
|
|
252
254
|
})
|
|
253
255
|
|
|
@@ -263,7 +265,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
263
265
|
description: 'desc',
|
|
264
266
|
agentName: 'klanker',
|
|
265
267
|
})
|
|
266
|
-
expect(body).toContain(`why:
|
|
268
|
+
expect(body).toContain(`why: _${reason}_`)
|
|
267
269
|
})
|
|
268
270
|
|
|
269
271
|
// #2469: hostd agent_* cards must name WHICH agent is targeted, pulled
|
|
@@ -275,7 +277,7 @@ describe('formatPermissionCardBody', () => {
|
|
|
275
277
|
description: 'Restart an agent via the host-control daemon. $SWITCHROOM_AGENT_NAME …',
|
|
276
278
|
agentName: 'klanker',
|
|
277
279
|
})
|
|
278
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
280
|
+
expect(body.split('\n')[0]).toBe('🔐 **Klanker** wants to restart agent `carrie` in the fleet')
|
|
279
281
|
})
|
|
280
282
|
|
|
281
283
|
test('hostd start/stop/logs/exec each name the target agent (#2469)', () => {
|
|
@@ -286,10 +288,10 @@ describe('formatPermissionCardBody', () => {
|
|
|
286
288
|
description: 'static schema doc',
|
|
287
289
|
agentName: 'klanker',
|
|
288
290
|
}).split('\n')[0]
|
|
289
|
-
expect(mk('mcp__hostd__agent_start')).toBe('🔐
|
|
290
|
-
expect(mk('mcp__hostd__agent_stop')).toBe('🔐
|
|
291
|
-
expect(mk('mcp__hostd__agent_logs')).toBe("🔐
|
|
292
|
-
expect(mk('mcp__hostd__agent_exec')).toBe('🔐
|
|
291
|
+
expect(mk('mcp__hostd__agent_start')).toBe('🔐 **Klanker** wants to start agent `pixel` in the fleet')
|
|
292
|
+
expect(mk('mcp__hostd__agent_stop')).toBe('🔐 **Klanker** wants to stop agent `pixel` in the fleet')
|
|
293
|
+
expect(mk('mcp__hostd__agent_logs')).toBe("🔐 **Klanker** wants to read agent `pixel`'s container logs")
|
|
294
|
+
expect(mk('mcp__hostd__agent_exec')).toBe('🔐 **Klanker** wants to run a read-only inspection inside agent `pixel`')
|
|
293
295
|
})
|
|
294
296
|
|
|
295
297
|
test('hostd agent verb without a name arg falls back to the generic phrase (no crash) (#2469)', () => {
|
|
@@ -299,8 +301,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
299
301
|
description: 'static schema doc',
|
|
300
302
|
agentName: 'klanker',
|
|
301
303
|
})
|
|
302
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
303
|
-
expect(body).toContain('why:
|
|
304
|
+
expect(body.split('\n')[0]).toBe('🔐 **Klanker** wants to restart an agent in the fleet')
|
|
305
|
+
expect(body).toContain('why: _bouncing the fleet_')
|
|
304
306
|
})
|
|
305
307
|
|
|
306
308
|
test('non-name-arg gated verb (update_apply) stays generic and does not break (#2469)', () => {
|
|
@@ -310,8 +312,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
310
312
|
description: 'static schema doc',
|
|
311
313
|
agentName: 'klanker',
|
|
312
314
|
})
|
|
313
|
-
expect(body.split('\n')[0]).toBe('🔐
|
|
314
|
-
expect(body).toContain('why:
|
|
315
|
+
expect(body.split('\n')[0]).toBe('🔐 **Klanker** wants to apply a fleet-wide update (pull + recreate)')
|
|
316
|
+
expect(body).toContain('why: _rolling out v0.16_')
|
|
315
317
|
})
|
|
316
318
|
|
|
317
319
|
// Clarity fix: the card gains a third "↳" line summarizing the REST
|
|
@@ -330,8 +332,8 @@ describe('formatPermissionCardBody', () => {
|
|
|
330
332
|
agentName: 'marko',
|
|
331
333
|
})
|
|
332
334
|
const lines = body.split('\n')
|
|
333
|
-
expect(lines[0]).toBe('🔐
|
|
334
|
-
expect(lines[1]).toBe('why:
|
|
335
|
+
expect(lines[0]).toBe('🔐 **Marko** wants to POST /smtp/email (Brevo)')
|
|
336
|
+
expect(lines[1]).toBe('why: _sending the priority-access invite_')
|
|
335
337
|
// Third line: scalar keys show value; the nested `to` array shows key-only.
|
|
336
338
|
expect(lines[2]).toContain('↳')
|
|
337
339
|
expect(lines[2]).toContain('subject: Priority access')
|
|
@@ -366,6 +368,49 @@ describe('formatPermissionCardBody', () => {
|
|
|
366
368
|
})
|
|
367
369
|
})
|
|
368
370
|
|
|
371
|
+
// Bug 1: the hostd rollout verb gained an optional `reason` (which the agent
|
|
372
|
+
// can now actually supply) and a curated MCP_TOOL_DESCRIPTIONS title, so the
|
|
373
|
+
// card stops rendering the generic "rollout (Hostd)" with "why: not provided".
|
|
374
|
+
describe('formatPermissionCardBody — hostd rollout (Bug 1)', () => {
|
|
375
|
+
test('renders the caller-supplied reason on the why: line', () => {
|
|
376
|
+
const body = formatPermissionCardBody({
|
|
377
|
+
toolName: 'mcp__hostd__rollout',
|
|
378
|
+
inputPreview: JSON.stringify({
|
|
379
|
+
reason: 'promote canary-green v0.16.24 to the fleet',
|
|
380
|
+
pin: 'v0.16.24',
|
|
381
|
+
}),
|
|
382
|
+
description: 'SAFELY roll the fleet to a pinned SEMVER version …',
|
|
383
|
+
agentName: 'overlord',
|
|
384
|
+
})
|
|
385
|
+
expect(body).toContain('why: _promote canary-green v0.16.24 to the fleet_')
|
|
386
|
+
// #2469: never the schema description.
|
|
387
|
+
expect(body).not.toContain('SAFELY roll the fleet')
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('renders the clean MCP_TOOL_DESCRIPTIONS title, not the raw tool id', () => {
|
|
391
|
+
const body = formatPermissionCardBody({
|
|
392
|
+
toolName: 'mcp__hostd__rollout',
|
|
393
|
+
inputPreview: JSON.stringify({
|
|
394
|
+
reason: 'rollback to last-good tag',
|
|
395
|
+
pin: 'v0.16.20',
|
|
396
|
+
}),
|
|
397
|
+
description: 'desc',
|
|
398
|
+
agentName: 'overlord',
|
|
399
|
+
})
|
|
400
|
+
const firstLine = body.split('\n')[0]
|
|
401
|
+
// Curated phrase from MCP_TOOL_DESCRIPTIONS["mcp__hostd__rollout"].
|
|
402
|
+
expect(firstLine).toBe('🔐 **Overlord** wants to roll the fleet to a pinned version')
|
|
403
|
+
expect(firstLine).not.toContain('mcp__hostd__rollout')
|
|
404
|
+
expect(firstLine).not.toMatch(/rollout \(Hostd\)/i)
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
test('naturalAction surfaces the curated title for the rollout verb', () => {
|
|
408
|
+
expect(naturalAction('mcp__hostd__rollout', undefined)).toBe(
|
|
409
|
+
'roll the fleet to a pinned version',
|
|
410
|
+
)
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
369
414
|
describe('describeGrant — phrased from the chosen scope', () => {
|
|
370
415
|
test('MCP server wildcard → "use any <Server> tool"', () => {
|
|
371
416
|
expect(describeGrant('mcp__perplexity__search', undefined, opt('mcp__perplexity__*'))).toBe(
|
|
@@ -411,7 +456,7 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
411
456
|
behavior: 'allow',
|
|
412
457
|
action: 'edit: supplement-log.md',
|
|
413
458
|
}),
|
|
414
|
-
).toBe('▶️
|
|
459
|
+
).toBe('▶️ **Gymbro** — got it, continuing: _edit: supplement-log.md_')
|
|
415
460
|
})
|
|
416
461
|
|
|
417
462
|
test('deny names what it will skip, lower-cased inline', () => {
|
|
@@ -421,7 +466,7 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
421
466
|
behavior: 'deny',
|
|
422
467
|
action: 'Search the web',
|
|
423
468
|
}),
|
|
424
|
-
).toBe("🚫
|
|
469
|
+
).toBe("🚫 **Ziggy** — noted, I won't search the web. Continuing without it.")
|
|
425
470
|
})
|
|
426
471
|
|
|
427
472
|
test('TTL auto-deny variant reads as a timeout, not a tap', () => {
|
|
@@ -432,17 +477,19 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
432
477
|
action: 'run: deploy.sh',
|
|
433
478
|
timeoutMinutes: 5,
|
|
434
479
|
}),
|
|
435
|
-
).toBe('🚫
|
|
480
|
+
).toBe('🚫 **Finn** — no answer in 5m, continuing without it (_run: deploy.sh_).')
|
|
436
481
|
})
|
|
437
482
|
|
|
438
|
-
test('
|
|
483
|
+
test('markdown-escapes a hostile action phrase (no raw ** emphasis injection) (#2669)', () => {
|
|
439
484
|
const out = formatPermissionResumeMessage({
|
|
440
485
|
agentName: 'clerk',
|
|
441
486
|
behavior: 'allow',
|
|
442
|
-
action: 'run: echo
|
|
487
|
+
action: 'run: echo **pwned**',
|
|
443
488
|
})
|
|
444
|
-
|
|
445
|
-
|
|
489
|
+
// The injected ** markers are backslash-escaped so they render literally
|
|
490
|
+
// and cannot hijack the card's own emphasis.
|
|
491
|
+
expect(out).toContain('\\*\\*pwned\\*\\*')
|
|
492
|
+
expect(out).not.toContain('echo **pwned**')
|
|
446
493
|
})
|
|
447
494
|
|
|
448
495
|
test('cap-first on the agent name', () => {
|
|
@@ -451,22 +498,22 @@ describe('formatPermissionResumeMessage — agent-voiced verdict ack', () => {
|
|
|
451
498
|
behavior: 'allow',
|
|
452
499
|
action: 'read: contract.pdf',
|
|
453
500
|
})
|
|
454
|
-
expect(out).toContain('
|
|
501
|
+
expect(out).toContain('**Lawgpt**')
|
|
455
502
|
})
|
|
456
503
|
|
|
457
504
|
test('empty action falls back to a generic continue (no dangling phrase)', () => {
|
|
458
505
|
expect(
|
|
459
506
|
formatPermissionResumeMessage({ agentName: 'carrie', behavior: 'allow', action: '' }),
|
|
460
|
-
).toBe('▶️
|
|
507
|
+
).toBe('▶️ **Carrie** — got it, back to work.')
|
|
461
508
|
expect(
|
|
462
509
|
formatPermissionResumeMessage({ agentName: 'carrie', behavior: 'deny', action: ' ' }),
|
|
463
|
-
).toBe('🚫
|
|
510
|
+
).toBe('🚫 **Carrie** — noted, continuing without it.')
|
|
464
511
|
})
|
|
465
512
|
|
|
466
513
|
test('null agent name degrades to a neutral "Agent" label', () => {
|
|
467
514
|
expect(
|
|
468
515
|
formatPermissionResumeMessage({ agentName: null, behavior: 'allow', action: 'edit: x.md' }),
|
|
469
|
-
).toBe('▶️
|
|
516
|
+
).toBe('▶️ **Agent** — got it, continuing: _edit: x.md_')
|
|
470
517
|
})
|
|
471
518
|
})
|
|
472
519
|
|
|
@@ -33,7 +33,6 @@ function makeDeps(
|
|
|
33
33
|
): PtyHandlerDeps {
|
|
34
34
|
return {
|
|
35
35
|
bot,
|
|
36
|
-
renderText: (t) => t, // identity for tests — easier to assert
|
|
37
36
|
...overrides,
|
|
38
37
|
}
|
|
39
38
|
}
|
|
@@ -93,17 +92,18 @@ describe('handlePtyPartialPure', () => {
|
|
|
93
92
|
expect(onFirstPartial).toHaveBeenCalledWith('1', 5)
|
|
94
93
|
})
|
|
95
94
|
|
|
96
|
-
it('
|
|
95
|
+
it('sends the raw PTY text LITERALLY (literalText path, no markdown render) (#2669)', () => {
|
|
96
|
+
// PTY-tail previews are raw terminal/TUI output — they go through the
|
|
97
|
+
// literal plain `sendMessage` path, NOT the rich-markdown parser, so
|
|
98
|
+
// control glyphs / box-drawing chars aren't misinterpreted as markdown.
|
|
97
99
|
const state = makeState({ currentSessionChatId: '1' })
|
|
98
|
-
const deps = makeDeps(bot
|
|
99
|
-
renderText: (t) => `<b>${t}</b>`,
|
|
100
|
-
})
|
|
100
|
+
const deps = makeDeps(bot)
|
|
101
101
|
|
|
102
|
-
handlePtyPartialPure('bold', state, deps)
|
|
103
|
-
await microtaskFlush()
|
|
102
|
+
handlePtyPartialPure('│ tool: ls -la │ *not bold*', state, deps)
|
|
104
103
|
|
|
105
104
|
expect(bot.api.sendMessage).toHaveBeenCalledTimes(1)
|
|
106
|
-
expect(bot.api.
|
|
105
|
+
expect(bot.api.sendRichMessage).not.toHaveBeenCalled()
|
|
106
|
+
expect(bot.api.sendMessage.mock.calls[0][1]).toBe('│ tool: ls -la │ *not bold*')
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
it('reuses an existing stream on second update', async () => {
|
|
@@ -91,9 +91,9 @@ describe('formatQuotaBlock', () => {
|
|
|
91
91
|
},
|
|
92
92
|
now,
|
|
93
93
|
)
|
|
94
|
-
expect(block).toContain('
|
|
95
|
-
expect(block).toContain('
|
|
96
|
-
expect(block).toContain('
|
|
94
|
+
expect(block).toContain('**Claude plan quota**')
|
|
95
|
+
expect(block).toContain('**5h window** 29% · resets in 2h 30m')
|
|
96
|
+
expect(block).toContain('**7d window** 33% · resets in 3d')
|
|
97
97
|
expect(block).toContain('Binding window: five hour')
|
|
98
98
|
// overage=allowed should not be surfaced
|
|
99
99
|
expect(block).not.toContain('Overage:')
|
|
@@ -249,17 +249,21 @@ describe("evaluateQuotaWatchAccount — message content", () => {
|
|
|
249
249
|
expect(d.message).toContain("30%");
|
|
250
250
|
});
|
|
251
251
|
|
|
252
|
-
it("throttling message
|
|
252
|
+
it("throttling message markdown-escapes the account label (#2669)", () => {
|
|
253
|
+
// The label is interpolated inside a code span — the renderer escapes
|
|
254
|
+
// inline-markdown specials (\ ` * _ ~ = [ ] |), not HTML entities. A label
|
|
255
|
+
// with an emphasis special (e.g. an underscore) is backslash-escaped; the
|
|
256
|
+
// literal `<` / `>` pass through (they are not markdown specials).
|
|
253
257
|
const d = evaluateQuotaWatchAccount({
|
|
254
258
|
agentName: "lawgpt",
|
|
255
|
-
snap: makeSnap("
|
|
259
|
+
snap: makeSnap("ev_il@example.com", makeQuota(85, 40)),
|
|
256
260
|
prev: PREV_NEVER_NOTIFIED,
|
|
257
261
|
now: NOW,
|
|
258
262
|
});
|
|
259
263
|
expect(d.kind).toBe("notify");
|
|
260
264
|
if (d.kind !== "notify") return;
|
|
261
|
-
|
|
262
|
-
expect(d.message).
|
|
265
|
+
// The underscore is escaped so it can't open an italic run inside the span.
|
|
266
|
+
expect(d.message).toContain("ev\\_il@example.com");
|
|
263
267
|
});
|
|
264
268
|
|
|
265
269
|
it("throttling message for active account mentions /auth use", () => {
|
|
@@ -126,8 +126,9 @@ describe("secret-detect — deleteSensitiveMessage helper retains its 'surface f
|
|
|
126
126
|
it("helper posts an in-chat warning naming the leaked message id", () => {
|
|
127
127
|
// The warning is the only signal a mobile-only operator gets —
|
|
128
128
|
// stderr is invisible to them. Pinning the in-chat surface as
|
|
129
|
-
// the load-bearing piece.
|
|
130
|
-
|
|
131
|
-
expect(helperBody).toMatch(/
|
|
129
|
+
// the load-bearing piece. Post-#2669 the warning ships via the
|
|
130
|
+
// rich-message path (sendRichMessage).
|
|
131
|
+
expect(helperBody).toMatch(/sendRichMessage|sendMessage/);
|
|
132
|
+
expect(helperBody).toMatch(/delete message.*manually|delete it manually|manually/i);
|
|
132
133
|
});
|
|
133
134
|
});
|
|
@@ -251,80 +251,52 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
251
251
|
expect(ctx.inFlightTools[0]!.durationMs).toBe(305_000 - 30_000)
|
|
252
252
|
})
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
])
|
|
269
|
-
expect(text).toBe(
|
|
270
|
-
'running Grep "foo" + 2 more for 5m (no update from agent in 5 min)',
|
|
271
|
-
)
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
it('tool with no label renders the bare name', () => {
|
|
275
|
-
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
276
|
-
{ name: 'Bash', label: null, durationMs: 305_000 },
|
|
277
|
-
])
|
|
278
|
-
expect(text).toBe(
|
|
279
|
-
'running Bash for 5m (no update from agent in 5 min)',
|
|
280
|
-
)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
it('raw mcp__ tool name with a human label drops the technical name and leads with the label', () => {
|
|
284
|
-
// mcp__hindsight__reflect is the internal MCP identifier — looks
|
|
285
|
-
// like a leak when surfaced to a user. The label table emits
|
|
286
|
-
// "Searching memory" for it (see hooks/tool-label-pretool.mjs);
|
|
287
|
-
// the fallback message should lead with the label, not concatenate
|
|
288
|
-
// both.
|
|
289
|
-
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
290
|
-
{ name: 'mcp__hindsight__reflect', label: 'Searching memory', durationMs: 305_000 },
|
|
291
|
-
])
|
|
292
|
-
expect(text).toBe(
|
|
293
|
-
'Searching memory for 5m (no update from agent in 5 min)',
|
|
294
|
-
)
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('raw mcp__ tool name with NO label falls back to the bare name (no leak-but-no-better-option)', () => {
|
|
298
|
-
const text = formatFrameworkFallbackText('working', 305_000, [
|
|
299
|
-
{ name: 'mcp__some-third-party__do_thing', label: null, durationMs: 305_000 },
|
|
300
|
-
])
|
|
301
|
-
expect(text).toBe(
|
|
302
|
-
'running mcp__some-third-party__do_thing for 5m (no update from agent in 5 min)',
|
|
303
|
-
)
|
|
254
|
+
// The "no update from agent in N min" stall notice is a retired stop-gap
|
|
255
|
+
// (operator: obsolete now that the live draft + the model's own pacing beats
|
|
256
|
+
// carry progress). `formatFrameworkFallbackText` now returns null for every
|
|
257
|
+
// stall variant — the generic "still working / still thinking" notice AND the
|
|
258
|
+
// #1292 tool-aware enrichment — so the gateway sends nothing for a plain
|
|
259
|
+
// stall (it still runs the unwedge teardown). Only `blockedOnApproval`
|
|
260
|
+
// still produces a user-visible string. The tests below pin that contract.
|
|
261
|
+
|
|
262
|
+
it('stall notice (in-flight tool) returns null — no user-visible send', () => {
|
|
263
|
+
expect(
|
|
264
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
265
|
+
{ name: 'Grep', label: '"foo"', durationMs: 275_000 },
|
|
266
|
+
]),
|
|
267
|
+
).toBeNull()
|
|
304
268
|
})
|
|
305
269
|
|
|
306
|
-
it('
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
270
|
+
it('stall notice (multiple in-flight tools) returns null', () => {
|
|
271
|
+
expect(
|
|
272
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
273
|
+
{ name: 'Grep', label: '"foo"', durationMs: 275_000 },
|
|
274
|
+
{ name: 'Read', label: 'config.ts', durationMs: 120_000 },
|
|
275
|
+
{ name: 'Bash', label: null, durationMs: 60_000 },
|
|
276
|
+
]),
|
|
277
|
+
).toBeNull()
|
|
313
278
|
})
|
|
314
279
|
|
|
315
|
-
it('
|
|
280
|
+
it('stall notice (raw mcp__ tool, with or without label) returns null', () => {
|
|
316
281
|
expect(
|
|
317
|
-
formatFrameworkFallbackText('working', 305_000, [
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
).toBe('still thinking… (no update from agent in 5 min)')
|
|
282
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
283
|
+
{ name: 'mcp__hindsight__reflect', label: 'Searching memory', durationMs: 305_000 },
|
|
284
|
+
]),
|
|
285
|
+
).toBeNull()
|
|
322
286
|
expect(
|
|
323
|
-
formatFrameworkFallbackText('working', 305_000
|
|
324
|
-
|
|
287
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
288
|
+
{ name: 'mcp__some-third-party__do_thing', label: null, durationMs: 305_000 },
|
|
289
|
+
]),
|
|
290
|
+
).toBeNull()
|
|
325
291
|
})
|
|
326
292
|
|
|
327
|
-
it('
|
|
293
|
+
it('generic stall notice (empty inFlightTools, working AND thinking) returns null', () => {
|
|
294
|
+
expect(formatFrameworkFallbackText('working', 305_000, [])).toBeNull()
|
|
295
|
+
expect(formatFrameworkFallbackText('thinking', 305_000, [])).toBeNull()
|
|
296
|
+
expect(formatFrameworkFallbackText('working', 305_000)).toBeNull()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('blockedOnApproval still names the real blocker (the one surviving user-visible send)', () => {
|
|
328
300
|
expect(
|
|
329
301
|
formatFrameworkFallbackText('working', 305_000, [], true),
|
|
330
302
|
).toBe('waiting for your approval — tap Approve or Deny on the card above (5 min)')
|
|
@@ -338,18 +310,10 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
338
310
|
).toBe('waiting for your approval — tap Approve or Deny on the card above (5 min)')
|
|
339
311
|
})
|
|
340
312
|
|
|
341
|
-
it('blockedOnApproval=false
|
|
313
|
+
it('blockedOnApproval=false is a stall → returns null (stop-gap retired)', () => {
|
|
342
314
|
expect(
|
|
343
315
|
formatFrameworkFallbackText('working', 305_000, [], false),
|
|
344
|
-
).
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
it('tool-aware wording wins over "thinking" — the actual observable beats the inferred kind', () => {
|
|
348
|
-
const text = formatFrameworkFallbackText('thinking', 305_000, [
|
|
349
|
-
{ name: 'Grep', label: '"foo"', durationMs: 305_000 },
|
|
350
|
-
])
|
|
351
|
-
expect(text.startsWith('running Grep')).toBe(true)
|
|
352
|
-
expect(text).not.toContain('still thinking')
|
|
316
|
+
).toBeNull()
|
|
353
317
|
})
|
|
354
318
|
|
|
355
319
|
it('tool completed before the fallback → empty snapshot → base wording', () => {
|
|
@@ -450,22 +414,21 @@ describe('silence-poke — #1292 tool-aware framework fallback', () => {
|
|
|
450
414
|
expect(__getStateForTests('k')!.inFlightTools.size).toBe(0)
|
|
451
415
|
})
|
|
452
416
|
|
|
453
|
-
it('
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
expect(
|
|
458
|
-
'
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
it('formatFrameworkFallbackText truncates very long labels', () => {
|
|
417
|
+
it('in-flight tool stall (any duration/label) returns null — stop-gap retired', () => {
|
|
418
|
+
// Previously rendered "running Grep foo for 12s (…)" / truncated long
|
|
419
|
+
// labels. The whole tool-aware stall enrichment is gone now, so the
|
|
420
|
+
// duration + label formatting paths no longer surface — they return null.
|
|
421
|
+
expect(
|
|
422
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
423
|
+
{ name: 'Grep', label: 'foo', durationMs: 12_000 },
|
|
424
|
+
]),
|
|
425
|
+
).toBeNull()
|
|
463
426
|
const longLabel = '"' + 'x'.repeat(120) + '"'
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
427
|
+
expect(
|
|
428
|
+
formatFrameworkFallbackText('working', 305_000, [
|
|
429
|
+
{ name: 'Grep', label: longLabel, durationMs: 305_000 },
|
|
430
|
+
]),
|
|
431
|
+
).toBeNull()
|
|
469
432
|
})
|
|
470
433
|
})
|
|
471
434
|
|
|
@@ -528,38 +491,38 @@ describe('silence-poke — fallback handler errors do not break timer', () => {
|
|
|
528
491
|
})
|
|
529
492
|
|
|
530
493
|
// CC-4 from `docs/status-ask-cause-classes.md`: wording is load-bearing
|
|
531
|
-
// (`reference/rfcs/conversational-pacing.md` § Safety net).
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
494
|
+
// (`reference/rfcs/conversational-pacing.md` § Safety net). The stall notice is
|
|
495
|
+
// a retired stop-gap — its wording is gone (returns null). The ONE surviving
|
|
496
|
+
// user-visible string is the approval-blocked re-ping; snapshot it here so a
|
|
497
|
+
// refactor that drops the phrase fails loud. The `N min` parenthetical is still
|
|
498
|
+
// derived from silenceMs (honest), so pin that too.
|
|
535
499
|
describe('silence-poke — wording snapshots (CC-4)', () => {
|
|
536
|
-
it('framework fallback — working
|
|
537
|
-
expect(formatFrameworkFallbackText('working', 300_000)).
|
|
538
|
-
|
|
539
|
-
)
|
|
500
|
+
it('framework fallback — generic stall (working/thinking) emits no message', () => {
|
|
501
|
+
expect(formatFrameworkFallbackText('working', 300_000)).toBeNull()
|
|
502
|
+
expect(formatFrameworkFallbackText('thinking', 300_000)).toBeNull()
|
|
540
503
|
})
|
|
541
504
|
|
|
542
|
-
it('
|
|
543
|
-
expect(formatFrameworkFallbackText('
|
|
544
|
-
`"
|
|
505
|
+
it('approval-blocked re-ping — exact wording', () => {
|
|
506
|
+
expect(formatFrameworkFallbackText('working', 300_000, [], true)).toMatchInlineSnapshot(
|
|
507
|
+
`"waiting for your approval — tap Approve or Deny on the card above (5 min)"`,
|
|
545
508
|
)
|
|
546
509
|
})
|
|
547
510
|
|
|
548
|
-
it('
|
|
549
|
-
expect(formatFrameworkFallbackText('working', 360_000)).toBe(
|
|
550
|
-
'
|
|
511
|
+
it('approval-blocked — minutes derived from silenceMs, not hard-coded', () => {
|
|
512
|
+
expect(formatFrameworkFallbackText('working', 360_000, [], true)).toBe(
|
|
513
|
+
'waiting for your approval — tap Approve or Deny on the card above (6 min)',
|
|
551
514
|
)
|
|
552
|
-
expect(formatFrameworkFallbackText('working', 600_000)).toBe(
|
|
553
|
-
'
|
|
515
|
+
expect(formatFrameworkFallbackText('working', 600_000, [], true)).toBe(
|
|
516
|
+
'waiting for your approval — tap Approve or Deny on the card above (10 min)',
|
|
554
517
|
)
|
|
555
518
|
})
|
|
556
519
|
|
|
557
|
-
it('
|
|
558
|
-
expect(formatFrameworkFallbackText('working', 30_000)).toBe(
|
|
559
|
-
'
|
|
520
|
+
it('approval-blocked — minutes floor at 1 even when silenceMs is small', () => {
|
|
521
|
+
expect(formatFrameworkFallbackText('working', 30_000, [], true)).toBe(
|
|
522
|
+
'waiting for your approval — tap Approve or Deny on the card above (1 min)',
|
|
560
523
|
)
|
|
561
|
-
expect(formatFrameworkFallbackText('working', 0)).toBe(
|
|
562
|
-
'
|
|
524
|
+
expect(formatFrameworkFallbackText('working', 0, [], true)).toBe(
|
|
525
|
+
'waiting for your approval — tap Approve or Deny on the card above (1 min)',
|
|
563
526
|
)
|
|
564
527
|
})
|
|
565
528
|
})
|