typeclaw 0.37.5 → 0.37.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typeclaw",
3
- "version": "0.37.5",
3
+ "version": "0.37.7",
4
4
  "homepage": "https://github.com/typeclaw/typeclaw#readme",
5
5
  "bugs": {
6
6
  "url": "https://github.com/typeclaw/typeclaw/issues"
@@ -50,7 +50,7 @@
50
50
  "@mariozechner/pi-tui": "^0.67.3",
51
51
  "@modelcontextprotocol/sdk": "^1.29.0",
52
52
  "@mozilla/readability": "^0.6.0",
53
- "agent-messenger": "2.20.1",
53
+ "agent-messenger": "2.20.5",
54
54
  "cheerio": "^1.2.0",
55
55
  "citty": "^0.2.2",
56
56
  "cron-parser": "^5.5.0",
@@ -0,0 +1,590 @@
1
+ import type { ThinkingLevel } from '@mariozechner/pi-agent-core'
2
+
3
+ function normalize(text: string): string {
4
+ return text
5
+ .toLowerCase()
6
+ .replace(/[`*_~]/g, ' ')
7
+ .replace(/\s+/g, ' ')
8
+ .trim()
9
+ }
10
+
11
+ // Human attention-escalation is a budget hint, not a command. Keep phrases narrow:
12
+ // explicit demands for care, or clear dissatisfaction directed at this response.
13
+ const EN_PHRASES: readonly string[] = [
14
+ 'do it properly',
15
+ 'do it right',
16
+ 'do it correctly',
17
+ 'be careful',
18
+ 'be thorough',
19
+ 'think hard',
20
+ 'think harder',
21
+ 'think carefully',
22
+ 'more carefully',
23
+ 'ultrathink',
24
+ 'ultrawork',
25
+ 'ultracode',
26
+ 'wtf',
27
+ 'wtf is this',
28
+ 'fuck',
29
+ 'fucking',
30
+ 'fucked',
31
+ 'fuck this',
32
+ 'fuck off',
33
+ 'what the fuck',
34
+ 'what the hell',
35
+ 'da fuck',
36
+ 'the fuck',
37
+ 'what are you doing',
38
+ 'what are u doing',
39
+ 'this is wrong',
40
+ "that's wrong",
41
+ 'ffs',
42
+ 'for fucks sake',
43
+ 'shit',
44
+ 'this is shit',
45
+ 'bullshit',
46
+ 'damn it',
47
+ 'damn this',
48
+ 'dammit',
49
+ 'goddamn',
50
+ 'god damn',
51
+ 'this is crap',
52
+ 'piece of shit',
53
+ 'screw this',
54
+ 'screwed up',
55
+ 'you suck',
56
+ 'this sucks',
57
+ 'garbage',
58
+ 'trash',
59
+ 'useless',
60
+ 'are you serious',
61
+ 'seriously?',
62
+ 'are you kidding',
63
+ 'you kidding me',
64
+ 'come on',
65
+ 'cmon',
66
+ 'jesus christ',
67
+ 'oh my god',
68
+ 'omfg',
69
+ 'stupid',
70
+ 'idiot',
71
+ 'moron',
72
+ 'pathetic',
73
+ 'terrible',
74
+ 'awful',
75
+ 'broken again',
76
+ 'still broken',
77
+ 'not again',
78
+ ]
79
+
80
+ const KO_PHRASES: readonly string[] = [
81
+ '제대로 해',
82
+ '제대로 좀',
83
+ '똑바로 해',
84
+ '똑바로 좀',
85
+ '잘 좀',
86
+ '잘 해',
87
+ '신중하게',
88
+ '꼼꼼하게',
89
+ '씨발',
90
+ '시발',
91
+ '씨바',
92
+ 'ㅅㅂ',
93
+ '시바',
94
+ '존나',
95
+ '졸라',
96
+ 'ㅈㄴ',
97
+ '개같',
98
+ '개판',
99
+ '병신',
100
+ 'ㅂㅅ',
101
+ '미친',
102
+ '미쳤',
103
+ 'ㅁㅊ',
104
+ '엿같',
105
+ '짜증',
106
+ '짜증나',
107
+ '빡치',
108
+ '빡쳐',
109
+ '개소리',
110
+ '말도 안',
111
+ '실화냐',
112
+ '에휴',
113
+ '아오',
114
+ '하…',
115
+ '쓰레기',
116
+ '구려',
117
+ '구리',
118
+ '왜 안',
119
+ '또 안',
120
+ '또 틀',
121
+ '왜 이래',
122
+ '뭐하는 거야',
123
+ '뭐하는거야',
124
+ '뭐 하는 거야',
125
+ '아 진짜',
126
+ '장난해',
127
+ '장난하냐',
128
+ '이게 뭐야',
129
+ '똑바로 안 해',
130
+ ]
131
+
132
+ const ES_PHRASES: readonly string[] = [
133
+ 'hazlo bien',
134
+ 'hazlo correctamente',
135
+ 'con cuidado',
136
+ 'piensa bien',
137
+ 'qué haces',
138
+ 'que haces',
139
+ 'mierda',
140
+ 'joder',
141
+ 'jode',
142
+ 'puto',
143
+ 'puta madre',
144
+ 'cabrón',
145
+ 'cabron',
146
+ 'coño',
147
+ 'no jodas',
148
+ 'qué carajo',
149
+ 'que carajo',
150
+ 'carajo',
151
+ 'basura',
152
+ 'esto es una mierda',
153
+ 'es una basura',
154
+ 'inútil',
155
+ 'inutil',
156
+ 'me cago',
157
+ 'qué asco',
158
+ 'que asco',
159
+ 'maldita sea',
160
+ 'estás de broma',
161
+ 'estas de broma',
162
+ 'qué mierda',
163
+ 'que mierda',
164
+ 'en serio',
165
+ 'esto está mal',
166
+ 'esto esta mal',
167
+ ]
168
+
169
+ const FR_PHRASES: readonly string[] = [
170
+ 'fais-le correctement',
171
+ 'fais-le bien',
172
+ 'sois attentif',
173
+ 'réfléchis bien',
174
+ 'reflechis bien',
175
+ "qu'est-ce que tu fais",
176
+ 'putain',
177
+ 'merde',
178
+ 'fait chier',
179
+ 'fait chié',
180
+ 'connerie',
181
+ 'conneries',
182
+ 'bordel',
183
+ 'foutu',
184
+ 'fous-toi',
185
+ "c'est de la merde",
186
+ 'c’est de la merde',
187
+ "c'est pourri",
188
+ 'c’est pourri',
189
+ 'tu te fous',
190
+ "n'importe quoi",
191
+ 'n’importe quoi',
192
+ 'inutile',
193
+ 'poubelle',
194
+ 'tu déconnes',
195
+ 'tu deconnes',
196
+ 'c’est nul',
197
+ "c'est nul",
198
+ 'sérieusement',
199
+ 'serieusement',
200
+ 'c’est faux',
201
+ "c'est faux",
202
+ ]
203
+
204
+ const IT_PHRASES: readonly string[] = [
205
+ 'fallo bene',
206
+ 'con attenzione',
207
+ 'pensa bene',
208
+ 'ma che fai',
209
+ 'cazzo',
210
+ 'merda',
211
+ 'che cazzo',
212
+ 'porca',
213
+ 'porca miseria',
214
+ 'stronzata',
215
+ 'stronzate',
216
+ 'vaffanculo',
217
+ 'è una merda',
218
+ 'e una merda',
219
+ 'che schifo',
220
+ 'schifo',
221
+ 'spazzatura',
222
+ 'inutile',
223
+ 'ma che cavolo',
224
+ 'fai schifo',
225
+ 'scherzi',
226
+ 'che cavolo',
227
+ 'sul serio',
228
+ 'è sbagliato',
229
+ 'e sbagliato',
230
+ ]
231
+
232
+ const PT_PHRASES: readonly string[] = [
233
+ 'faça direito',
234
+ 'faca direito',
235
+ 'faça corretamente',
236
+ 'faca corretamente',
237
+ 'com cuidado',
238
+ 'pense bem',
239
+ 'que isso',
240
+ 'merda',
241
+ 'porra',
242
+ 'caralho',
243
+ 'que merda',
244
+ 'que porra',
245
+ 'puta que pariu',
246
+ 'porcaria',
247
+ 'é uma merda',
248
+ 'e uma merda',
249
+ 'que saco',
250
+ 'lixo',
251
+ 'inútil',
252
+ 'inutil',
253
+ 'que droga',
254
+ 'tá de brincadeira',
255
+ 'ta de brincadeira',
256
+ 'foda-se',
257
+ 'foda se',
258
+ 'está errado',
259
+ 'esta errado',
260
+ ]
261
+
262
+ const DE_PHRASES: readonly string[] = [
263
+ 'mach es richtig',
264
+ 'sei gründlich',
265
+ 'sei gruendlich',
266
+ 'denk nach',
267
+ 'sorgfältig',
268
+ 'sorgfaeltig',
269
+ 'was machst du',
270
+ 'was soll das',
271
+ 'scheiße',
272
+ 'scheisse',
273
+ 'scheiss',
274
+ 'verdammt',
275
+ 'verflucht',
276
+ 'so ein mist',
277
+ 'blödsinn',
278
+ 'bloedsinn',
279
+ 'quatsch',
280
+ 'das ist mist',
281
+ 'das ist müll',
282
+ 'das ist mull',
283
+ 'müll',
284
+ 'schwachsinn',
285
+ 'nutzlos',
286
+ 'unbrauchbar',
287
+ 'verarschst',
288
+ 'soll das',
289
+ 'im ernst',
290
+ 'ernsthaft',
291
+ 'das ist falsch',
292
+ ]
293
+
294
+ const RU_PHRASES: readonly string[] = [
295
+ 'сделай правильно',
296
+ 'сделай как надо',
297
+ 'внимательно',
298
+ 'тщательно',
299
+ 'что ты делаешь',
300
+ 'что за',
301
+ 'блять',
302
+ 'бля',
303
+ 'блядь',
304
+ 'сука',
305
+ 'нахуй',
306
+ 'нахер',
307
+ 'пиздец',
308
+ 'хуйня',
309
+ 'говно',
310
+ 'дерьмо',
311
+ 'бред',
312
+ 'это бред',
313
+ 'фигня',
314
+ 'хрень',
315
+ 'мусор',
316
+ 'бесполезно',
317
+ 'издеваешься',
318
+ 'охренеть',
319
+ 'какого черта',
320
+ 'какого чёрта',
321
+ 'да блин',
322
+ 'серьёзно',
323
+ 'серьезно',
324
+ 'это неправильно',
325
+ ]
326
+
327
+ const ZH_PHRASES: readonly string[] = [
328
+ '认真做',
329
+ '好好做',
330
+ '仔细点',
331
+ '用心做',
332
+ '卧槽',
333
+ '我操',
334
+ '操你',
335
+ '操你妈',
336
+ '我擦',
337
+ '草泥马',
338
+ '我草',
339
+ '草你',
340
+ '我靠',
341
+ '靠北',
342
+ '靠杯',
343
+ '妈的',
344
+ '他妈的',
345
+ 'tmd',
346
+ '垃圾',
347
+ '狗屎',
348
+ '搞屁',
349
+ '搞毛',
350
+ '什么垃圾',
351
+ '太烂了',
352
+ '烂代码',
353
+ '废话',
354
+ '神经病',
355
+ '有病',
356
+ '认真点',
357
+ '搞什么',
358
+ '搞什么鬼',
359
+ '你在干什么',
360
+ '什么鬼',
361
+ '认真的吗',
362
+ ]
363
+
364
+ const JA_PHRASES: readonly string[] = [
365
+ 'ちゃんとやって',
366
+ 'しっかりやって',
367
+ '真面目にやって',
368
+ '丁寧に',
369
+ 'くそ',
370
+ 'クソ',
371
+ 'くそが',
372
+ 'ふざけんな',
373
+ 'ふざけるな',
374
+ 'ばか',
375
+ 'バカ',
376
+ 'あほ',
377
+ 'アホ',
378
+ 'ゴミ',
379
+ 'クズ',
380
+ 'ちくしょう',
381
+ '畜生',
382
+ '使えない',
383
+ '最悪',
384
+ 'ありえない',
385
+ 'うざい',
386
+ 'むかつく',
387
+ 'なんなの',
388
+ 'ksk',
389
+ '何やってんの',
390
+ '何してるの',
391
+ 'ふざけてるの',
392
+ 'マジで',
393
+ 'ちゃんとして',
394
+ ]
395
+
396
+ const AR_PHRASES: readonly string[] = [
397
+ 'اعملها صح',
398
+ 'بعناية',
399
+ 'فكر جيدا',
400
+ 'تبا',
401
+ 'تباً',
402
+ 'اللعنة',
403
+ 'لعنة',
404
+ 'قرف',
405
+ 'هراء',
406
+ 'حقير',
407
+ 'غبي',
408
+ 'سخيف',
409
+ 'فاشل',
410
+ 'هذا هراء',
411
+ 'ما هذا الهراء',
412
+ 'تافه',
413
+ 'زبالة',
414
+ 'عديم الفائدة',
415
+ 'هل تمزح',
416
+ 'ماذا تفعل',
417
+ 'ما هذا',
418
+ 'بجدية',
419
+ 'هذا خطأ',
420
+ ]
421
+
422
+ const HI_PHRASES: readonly string[] = [
423
+ 'ठीक से करो',
424
+ 'ध्यान से',
425
+ 'अच्छे से करो',
426
+ 'बकवास',
427
+ 'यह बकवास है',
428
+ 'बेकार',
429
+ 'बेवकूफ',
430
+ 'घटिया',
431
+ 'कचरा',
432
+ 'गंदा',
433
+ 'मज़ाक',
434
+ 'मजाक',
435
+ 'धत',
436
+ 'क्या बकवास',
437
+ 'पागल हो',
438
+ 'बहुत बुरा',
439
+ 'क्या कर रहे हो',
440
+ 'यह गलत है',
441
+ 'सच में',
442
+ ]
443
+
444
+ const TR_PHRASES: readonly string[] = [
445
+ 'düzgün yap',
446
+ 'duzgun yap',
447
+ 'doğru yap',
448
+ 'dogru yap',
449
+ 'dikkatli ol',
450
+ 'iyice düşün',
451
+ 'iyice dusun',
452
+ 'siktir',
453
+ 'lanet',
454
+ 'kahretsin',
455
+ 'saçmalık',
456
+ 'saçma',
457
+ 'bok',
458
+ 'boktan',
459
+ 'rezalet',
460
+ 'berbat',
461
+ 'çöp',
462
+ 'işe yaramaz',
463
+ 'ise yaramaz',
464
+ 'aptal',
465
+ 'salak',
466
+ 'dalga mı',
467
+ 'dalga mi',
468
+ 'bu ne rezalet',
469
+ 'ne yapıyorsun',
470
+ 'ne yapiyorsun',
471
+ 'ne saçmalık',
472
+ 'ne sacmalik',
473
+ 'cidden mi',
474
+ 'bu yanlış',
475
+ 'bu yanlis',
476
+ ]
477
+
478
+ const VI_PHRASES: readonly string[] = [
479
+ 'làm cho đúng',
480
+ 'lam cho dung',
481
+ 'làm cẩn thận',
482
+ 'lam can than',
483
+ 'suy nghĩ kỹ',
484
+ 'suy nghi ky',
485
+ 'đệch',
486
+ 'đếch',
487
+ 'vãi',
488
+ 'cứt',
489
+ 'rác',
490
+ 'vô dụng',
491
+ 'vo dung',
492
+ 'ngớ ngẩn',
493
+ 'tệ',
494
+ 'tệ hại',
495
+ 'quá tệ',
496
+ 'đùa à',
497
+ 'dua a',
498
+ 'vớ vẩn',
499
+ 'vo van',
500
+ 'chán',
501
+ 'đang làm gì vậy',
502
+ 'dang lam gi vay',
503
+ 'cái gì vậy',
504
+ 'cai gi vay',
505
+ 'nghiêm túc',
506
+ 'nghiem tuc',
507
+ 'sai rồi',
508
+ 'sai roi',
509
+ ]
510
+
511
+ const ID_PHRASES: readonly string[] = [
512
+ 'lakukan dengan benar',
513
+ 'hati-hati',
514
+ 'pikirkan baik-baik',
515
+ 'anjing',
516
+ 'bangsat',
517
+ 'sialan',
518
+ 'kampret',
519
+ 'goblok',
520
+ 'tolol',
521
+ 'bodoh',
522
+ 'sampah',
523
+ 'jelek',
524
+ 'payah',
525
+ 'tidak berguna',
526
+ 'gak guna',
527
+ 'omong kosong',
528
+ 'yang benar saja',
529
+ 'bercanda',
530
+ 'lagi ngapain',
531
+ 'apa-apaan',
532
+ 'ini salah',
533
+ ]
534
+
535
+ const ALL_PHRASES: readonly string[] = [
536
+ ...EN_PHRASES,
537
+ ...KO_PHRASES,
538
+ ...ES_PHRASES,
539
+ ...FR_PHRASES,
540
+ ...IT_PHRASES,
541
+ ...PT_PHRASES,
542
+ ...DE_PHRASES,
543
+ ...RU_PHRASES,
544
+ ...ZH_PHRASES,
545
+ ...JA_PHRASES,
546
+ ...AR_PHRASES,
547
+ ...HI_PHRASES,
548
+ ...TR_PHRASES,
549
+ ...VI_PHRASES,
550
+ ...ID_PHRASES,
551
+ ]
552
+
553
+ const MORPHEME_PATTERNS: readonly RegExp[] = []
554
+
555
+ const MIN_LENGTH = 2
556
+
557
+ export function detectAttentionEscalation(text: string): boolean {
558
+ if (text.length < MIN_LENGTH) return false
559
+ const normalized = normalize(text)
560
+ if (normalized.length < MIN_LENGTH) return false
561
+ if (ALL_PHRASES.some((phrase) => normalized.includes(phrase))) return true
562
+ return MORPHEME_PATTERNS.some((pattern) => pattern.test(normalized))
563
+ }
564
+
565
+ // Not `xhigh`: that level is OpenAI-family-only. `high` is universal and
566
+ // `setThinkingLevel` clamps it down per-model, so it's safe to pass unconditionally.
567
+ const ESCALATED_LEVEL: ThinkingLevel = 'high'
568
+
569
+ export function resolveTurnThinkingLevel(
570
+ text: string,
571
+ sessionDefault: ThinkingLevel | undefined,
572
+ ): ThinkingLevel | undefined {
573
+ return detectAttentionEscalation(text) ? ESCALATED_LEVEL : sessionDefault
574
+ }
575
+
576
+ type ThinkingLevelSettable = {
577
+ setThinkingLevel(level: ThinkingLevel): void
578
+ }
579
+
580
+ // `setThinkingLevel` only mutates reasoning_effort (a per-request param), so a
581
+ // per-turn bump preserves the prompt-prefix cache — no session recreation, no
582
+ // model swap. Skipping the call when nothing resolves leaves the SDK default intact.
583
+ export function applyTurnThinkingLevel(
584
+ session: ThinkingLevelSettable,
585
+ text: string,
586
+ sessionDefault: ThinkingLevel | undefined,
587
+ ): void {
588
+ const resolved = resolveTurnThinkingLevel(text, sessionDefault)
589
+ if (resolved !== undefined) session.setThinkingLevel(resolved)
590
+ }
@@ -576,6 +576,8 @@ async function applyBashSandbox(
576
576
  const { dirs, files } = resolveHiddenPaths(permissions, origin, agentDir)
577
577
  if (dirs.length === 0 && files.length === 0) return
578
578
 
579
+ const sandboxEnvOverlay = buildRoleScopedConfigEnv(agentDir, dirs, envOverlay)
580
+
579
581
  await ensureBwrapAvailable()
580
582
  // Per-session /tmp: bind this session's scratch dir over the default
581
583
  // --tmpfs /tmp so writes survive across the role's sandboxed bash calls AND
@@ -671,11 +673,31 @@ async function applyBashSandbox(
671
673
  cwd: agentDir,
672
674
  proc,
673
675
  procSelfExe: resolveProcSelfExe(),
674
- ...(envOverlay !== undefined ? { env: { set: envOverlay } } : {}),
676
+ ...(sandboxEnvOverlay !== undefined ? { env: { set: sandboxEnvOverlay } } : {}),
675
677
  })
676
678
  mutableArgs.command = commandString
677
679
  }
678
680
 
681
+ function buildRoleScopedConfigEnv(
682
+ agentDir: string,
683
+ hiddenDirs: string[],
684
+ envOverlay: BashEnvOverlay | undefined,
685
+ ): BashEnvOverlay | undefined {
686
+ // Low-trust roles have workspace/ masked. Do not let container-global config
687
+ // env vars point CLIs back at that private surface: apps that honor XDG should
688
+ // still run, but their config must land in the sandbox's per-session /tmp.
689
+ // Trusted/owner never get here (no hidden dirs), so their Dockerfile-level
690
+ // persistent GWS_CONFIG_HOME remains /agent/workspace/.config/gws.
691
+ const workspaceHidden = hiddenDirs.includes(join(agentDir, 'workspace'))
692
+ if (!workspaceHidden) return envOverlay
693
+
694
+ return {
695
+ ...envOverlay,
696
+ XDG_CONFIG_HOME: '/tmp/.config',
697
+ GWS_CONFIG_HOME: '/tmp/.config/gws',
698
+ }
699
+ }
700
+
679
701
  function subtractMaskedProtected(
680
702
  zones: { root: string; protected: { dirs: string[]; files: string[] } },
681
703
  masked: { dirs: string[]; files: string[] },
@@ -538,16 +538,19 @@ function renderMembershipSummary(
538
538
  function renderResearchReportDeliveryGuidance(platformInfo: PlatformInfo): string[] {
539
539
  if (!platformInfo.supportsAttachments) return []
540
540
  return [
541
- `**Ship reports as a PDF by default.** ${platformInfo.displayName} accepts file`,
542
- 'attachments. When the user asks for a report, document, brief, or "the report", or when a',
543
- '`researcher` subagent returns `research-<slug>.md` in `<report>`, render the',
544
- 'markdown with `typeclaw-render-pdf` and deliver via',
541
+ `**Ship explicit deliverables as PDFs.** ${platformInfo.displayName} accepts file`,
542
+ 'attachments. When the user clearly asks for a PDF/file/export/attachment, a standalone',
543
+ 'document/brief, or when a `researcher` subagent returns `research-<slug>.md` in',
544
+ '`<report>`, render the markdown with `typeclaw-render-pdf` and deliver via',
545
545
  '`channel_send({ ..., attachments: [{ path, filename }] })` plus a 1–2 line',
546
- 'summary. A `researcher` `<summary>` is a teaser, NOT the deliverable. Never',
547
- 'use an ad-hoc library (jsPDF, pdfkit, raw-text dump); it breaks markdown/CJK.',
546
+ 'summary. Do not treat the bare word "report" as enough: routine daily stats,',
547
+ 'user trends, status reports, and other operational updates should stay inline',
548
+ 'unless the user asks for a downloadable/exported artifact. A `researcher`',
549
+ '`<summary>` is a teaser, NOT the deliverable. Never use an ad-hoc library',
550
+ '(jsPDF, pdfkit, raw-text dump); it breaks markdown/CJK.',
548
551
  "For Korean/Japanese/Chinese, follow the skill's CJK guidance and never ship",
549
552
  'tofu boxes. Do not paste the full markdown into chat; do not attach the raw `.md`',
550
- 'unless explicitly asked; inline text is only for short content.',
553
+ 'unless explicitly asked; inline text is right for routine updates.',
551
554
  '',
552
555
  ]
553
556
  }
@@ -5,6 +5,7 @@ import type { PermissionService } from '@/permissions'
5
5
  import type { HookBus } from '@/plugin'
6
6
  import type { Stream, Unsubscribe } from '@/stream'
7
7
 
8
+ import { applyTurnThinkingLevel } from './attention-escalation'
8
9
  import { type AgentSession, createSession, type PluginSessionWiring } from './index'
9
10
  import { subscribeProviderErrors } from './provider-error'
10
11
  import type { SubagentBashPolicy } from './reviewer-bash-policy'
@@ -304,6 +305,7 @@ export async function invokeSubagent(name: string, options: InvokeSubagentOption
304
305
  retrievalContext.results.length > 0
305
306
  ? `${renderTurnTimeAnchor()}\n\n${userPromptForTurn}\n\n${retrievalContext.results}`
306
307
  : `${renderTurnTimeAnchor()}\n\n${userPromptForTurn}`
308
+ applyTurnThinkingLevel(session, userPromptForTurn, session.thinkingLevel)
307
309
  await session.prompt(turnText)
308
310
  } finally {
309
311
  if (hooks && turnEvent !== undefined) {
@@ -62,9 +62,9 @@ Do not narrate routine low-risk tools. Narrate only for multi-step context, risk
62
62
 
63
63
  ## Delivering reports and documents
64
64
 
65
- When the user asks for a *report*, *document*, *brief*, *PDF*, or asks you to *send/show/attach/export* a generated result anything a human would download, print, or forward produce a polished file, not a chat wall or substance-dropping summary. A summary is a pointer to the deliverable, never the deliverable itself; when the user asked for the report, ship the report.
65
+ Produce a polished file only when the user clearly asks for something a human would download, print, forward, attach, export, or keep as a standalone deliverable. Do **not** treat the bare word "report" as enough by itself: routine operational updates, daily stats, user trends, status reports, and other chat-native summaries should stay inline unless the user asks for a file/PDF/export. A summary is a pointer to the deliverable, never the deliverable itself, but only after a deliverable was actually requested.
66
66
 
67
- For Markdown-to-PDF, use the bundled \`typeclaw-render-pdf\` skill; it is the supported path and renders headings, lists, and tables. Never hand-roll PDFs with jsPDF, pdfkit, canvas text dumps, raw headless-browser prints, or ReportLab: they often emit raw markup and mojibake for non-Latin text. For Korean/Japanese/Chinese, follow the skill's CJK font guidance and do not ship tofu boxes. Short answers/snippets/explanations can stay inline.
67
+ For Markdown-to-PDF, use the bundled \`typeclaw-render-pdf\` skill; it is the supported path and renders headings, lists, and tables. Never hand-roll PDFs with jsPDF, pdfkit, canvas text dumps, raw headless-browser prints, or ReportLab: they often emit raw markup and mojibake for non-Latin text. For Korean/Japanese/Chinese, follow the skill's CJK font guidance and do not ship tofu boxes. Short answers, snippets, explanations, and routine reports can stay inline.
68
68
 
69
69
  ## Long-running and interactive shell work
70
70