typeclaw 0.37.6 → 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 +2 -2
- package/src/agent/attention-escalation.ts +590 -0
- package/src/agent/session-origin.ts +10 -7
- package/src/agent/subagents.ts +2 -0
- package/src/agent/system-prompt.ts +2 -2
- package/src/channels/router.ts +9 -1
- package/src/cli/channel.ts +1 -1
- package/src/cli/init.ts +8 -1
- package/src/cli/restart.ts +1 -0
- package/src/cli/start.ts +1 -0
- package/src/config/config.ts +12 -10
- package/src/container/start.ts +17 -4
- package/src/cron/consumer.ts +7 -0
- package/src/doctor/checks.ts +13 -1
- package/src/init/dockerfile.ts +51 -3
- package/src/server/command-runner.ts +2 -0
- package/src/server/index.ts +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typeclaw",
|
|
3
|
-
"version": "0.37.
|
|
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.
|
|
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
|
+
}
|
|
@@ -538,16 +538,19 @@ function renderMembershipSummary(
|
|
|
538
538
|
function renderResearchReportDeliveryGuidance(platformInfo: PlatformInfo): string[] {
|
|
539
539
|
if (!platformInfo.supportsAttachments) return []
|
|
540
540
|
return [
|
|
541
|
-
`**Ship
|
|
542
|
-
'attachments. When the user asks for a
|
|
543
|
-
'`researcher` subagent returns `research-<slug>.md` in
|
|
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.
|
|
547
|
-
'
|
|
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
|
|
553
|
+
'unless explicitly asked; inline text is right for routine updates.',
|
|
551
554
|
'',
|
|
552
555
|
]
|
|
553
556
|
}
|
package/src/agent/subagents.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
package/src/channels/router.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { AssistantMessage } from '@mariozechner/pi-ai'
|
|
|
5
5
|
import { SessionManager } from '@mariozechner/pi-coding-agent'
|
|
6
6
|
|
|
7
7
|
import { createSession, renderTurnRoleAnchor, renderTurnTimeAnchor, type AgentSession } from '@/agent'
|
|
8
|
+
import { applyTurnThinkingLevel } from '@/agent/attention-escalation'
|
|
8
9
|
import { forgetSharedLoopGuardTool } from '@/agent/plugin-tools'
|
|
9
10
|
import { subscribeProviderErrors } from '@/agent/provider-error'
|
|
10
11
|
import type { RestartHandoff } from '@/agent/restart-handoff'
|
|
@@ -573,6 +574,10 @@ type LiveSession = {
|
|
|
573
574
|
key: ChannelKey
|
|
574
575
|
keyId: string
|
|
575
576
|
session: AgentSession
|
|
577
|
+
// The session's creation-time thinking level, captured once. A later escalated
|
|
578
|
+
// turn moves `session.thinkingLevel` to `high`, so the live getter can't be the
|
|
579
|
+
// reset target — this preserves the real default across the session's lifetime.
|
|
580
|
+
turnThinkingDefault: AgentSession['thinkingLevel']
|
|
576
581
|
sessionId: string
|
|
577
582
|
dispose: () => Promise<void>
|
|
578
583
|
hooks: HookBus | undefined
|
|
@@ -1659,6 +1664,7 @@ export function createChannelRouter(options: CreateChannelRouterOptions): Channe
|
|
|
1659
1664
|
key,
|
|
1660
1665
|
keyId,
|
|
1661
1666
|
session: created.session,
|
|
1667
|
+
turnThinkingDefault: created.session.thinkingLevel,
|
|
1662
1668
|
sessionId: created.sessionId,
|
|
1663
1669
|
dispose: created.dispose,
|
|
1664
1670
|
hooks: created.hooks,
|
|
@@ -2395,8 +2401,10 @@ export function createChannelRouter(options: CreateChannelRouterOptions): Channe
|
|
|
2395
2401
|
live.policyDeniedToolSendsThisTurn.clear()
|
|
2396
2402
|
resetReviewTurn(live.sessionId)
|
|
2397
2403
|
const isRealUserTurn = batch.length > 0
|
|
2398
|
-
const
|
|
2404
|
+
const retrievalQuery = composeRetrievalQuery(batch)
|
|
2405
|
+
const retrievalContext = await fireSessionTurnStart(live, retrievalQuery)
|
|
2399
2406
|
const promptText = retrievalContext.results.length > 0 ? `${text}\n\n${retrievalContext.results}` : text
|
|
2407
|
+
applyTurnThinkingLevel(live.session, retrievalQuery, live.turnThinkingDefault)
|
|
2400
2408
|
live.promptInFlight = true
|
|
2401
2409
|
try {
|
|
2402
2410
|
await live.session.prompt(promptText)
|
package/src/cli/channel.ts
CHANGED
|
@@ -1116,7 +1116,7 @@ async function promptDiscordToken(): Promise<string> {
|
|
|
1116
1116
|
[
|
|
1117
1117
|
'https://discord.com/developers/applications',
|
|
1118
1118
|
'New Application → Bot tab → Reset Token.',
|
|
1119
|
-
'
|
|
1119
|
+
'Under Privileged Gateway Intents, enable MESSAGE CONTENT and GUILD MEMBERS.',
|
|
1120
1120
|
].join('\n'),
|
|
1121
1121
|
'Get a Discord bot token',
|
|
1122
1122
|
)
|
package/src/cli/init.ts
CHANGED
|
@@ -554,6 +554,7 @@ export async function collectWizardInputs(
|
|
|
554
554
|
const decision = await prompts.confirmResumeCheckpoint(sanitized)
|
|
555
555
|
if (decision === 'resume') {
|
|
556
556
|
seedWizardState(state, sanitized)
|
|
557
|
+
step = resumeStep(state)
|
|
557
558
|
} else {
|
|
558
559
|
await checkpointStore.clear(cwd)
|
|
559
560
|
}
|
|
@@ -1030,6 +1031,12 @@ function resolveModelOption(catalog: WizardState['catalog'], ref: string | undef
|
|
|
1030
1031
|
return catalog.options.find((option) => option.ref === ref)
|
|
1031
1032
|
}
|
|
1032
1033
|
|
|
1034
|
+
function resumeStep(state: WizardState): StepId {
|
|
1035
|
+
if (state.providerId !== undefined) return 'reuse-existing-key'
|
|
1036
|
+
if (state.vendorId !== undefined) return 'pick-provider-variant'
|
|
1037
|
+
return 'pick-vendor'
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1033
1040
|
function projectCheckpoint(cwd: string, state: WizardState): WizardAnswerCheckpointV1 {
|
|
1034
1041
|
return checkpointFromSelections({
|
|
1035
1042
|
cwd,
|
|
@@ -1429,7 +1436,7 @@ async function runDiscordFlow(): Promise<StepResult<CollectedInputs['channelSecr
|
|
|
1429
1436
|
[
|
|
1430
1437
|
'https://discord.com/developers/applications',
|
|
1431
1438
|
'New Application → Bot tab → Reset Token.',
|
|
1432
|
-
'
|
|
1439
|
+
'Under Privileged Gateway Intents, enable MESSAGE CONTENT and GUILD MEMBERS.',
|
|
1433
1440
|
].join('\n'),
|
|
1434
1441
|
'Get a Discord bot token',
|
|
1435
1442
|
)
|
package/src/cli/restart.ts
CHANGED
package/src/cli/start.ts
CHANGED
package/src/config/config.ts
CHANGED
|
@@ -1200,14 +1200,18 @@ export function validateConfig(cwd: string, options: ValidateConfigOptions = {})
|
|
|
1200
1200
|
const parsed = parseConfigJson(raw, { migrate: true, persistTarget: cwd })
|
|
1201
1201
|
if (!parsed.ok) return parsed
|
|
1202
1202
|
|
|
1203
|
-
|
|
1203
|
+
// Append lines are advisory here — never fatal. The Dockerfile renderer
|
|
1204
|
+
// (renderCustomDockerfileLines) is the enforcement boundary: it STRIPS unsafe
|
|
1205
|
+
// lines so the container still comes up, and a bad line written by the
|
|
1206
|
+
// in-container agent can never brick `typeclaw start`. We surface the same
|
|
1207
|
+
// strip/warn decisions as warnings so the operator sees them pre-build.
|
|
1204
1208
|
const warnings: string[] = []
|
|
1205
1209
|
const appendLines = parsed.config.docker.file.append
|
|
1206
1210
|
for (let i = 0; i < appendLines.length; i++) {
|
|
1207
1211
|
const check = validateDockerfileAppendLine(appendLines[i]!)
|
|
1208
1212
|
if (!check.ok) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1213
|
+
warnings.push(`docker.file.append[${i}] will be stripped on start — ${check.reason}`)
|
|
1214
|
+
continue
|
|
1211
1215
|
}
|
|
1212
1216
|
if (check.warning) warnings.push(`docker.file.append[${i}] ${check.warning}`)
|
|
1213
1217
|
}
|
|
@@ -1317,12 +1321,6 @@ export function validateMount(mount: Mount, cwd: string): ValidateConfigResult {
|
|
|
1317
1321
|
return { ok: true }
|
|
1318
1322
|
}
|
|
1319
1323
|
|
|
1320
|
-
// Host env (not config) on purpose: an in-container agent can edit its own
|
|
1321
|
-
// typeclaw.json but cannot set the env of the host `typeclaw start` that runs
|
|
1322
|
-
// this gate, so it can never waive its own footgun. Only relaxes SEMANTIC
|
|
1323
|
-
// blocks; structural blocks always fire (they break Dockerfile generation).
|
|
1324
|
-
const ALLOW_UNSAFE_DOCKER_APPEND_ENV = 'TYPECLAW_ALLOW_UNSAFE_DOCKER_APPEND'
|
|
1325
|
-
|
|
1326
1324
|
// FROM/ENTRYPOINT/CMD/MAINTAINER are intentionally excluded — see the
|
|
1327
1325
|
// structural blocks in validateDockerfileAppendLine for why.
|
|
1328
1326
|
const ALLOWED_APPEND_INSTRUCTIONS = new Set([
|
|
@@ -1450,9 +1448,13 @@ export function validateDockerfileAppendLine(line: string): AppendLineCheck {
|
|
|
1450
1448
|
}
|
|
1451
1449
|
}
|
|
1452
1450
|
if (!ALLOWED_APPEND_INSTRUCTIONS.has(instruction)) {
|
|
1451
|
+
// Reason is intentionally input-free: this string is forwarded verbatim into
|
|
1452
|
+
// operator-facing warnings (classifyDockerfileAppend -> dockerfileWarnings),
|
|
1453
|
+
// and the offending token is user-controlled — echoing it could leak a
|
|
1454
|
+
// secret/token-like first word from a malformed line into start/doctor output.
|
|
1453
1455
|
return {
|
|
1454
1456
|
ok: false,
|
|
1455
|
-
reason:
|
|
1457
|
+
reason: 'does not begin with a recognized Dockerfile instruction',
|
|
1456
1458
|
kind: 'structural',
|
|
1457
1459
|
}
|
|
1458
1460
|
}
|
package/src/container/start.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
readInstalledTypeclawVersionFromAgent,
|
|
21
21
|
} from '@/init/auto-upgrade'
|
|
22
22
|
import { resolveBaseImageVersion } from '@/init/cli-version'
|
|
23
|
-
import { buildDockerfile, DOCKERFILE } from '@/init/dockerfile'
|
|
23
|
+
import { buildDockerfile, classifyDockerfileAppend, DOCKERFILE } from '@/init/dockerfile'
|
|
24
24
|
import { ensureDepsInstalled, type EnsureDepsResult } from '@/init/ensure-deps'
|
|
25
25
|
import { buildGitignore, GITIGNORE_FILE } from '@/init/gitignore'
|
|
26
26
|
import { refreshPackageJson } from '@/init/packagejson'
|
|
@@ -148,6 +148,10 @@ export type StartResult =
|
|
|
148
148
|
// registry. Non-fatal by design: a typo'd or unpublished plugin warns
|
|
149
149
|
// instead of blocking the launch.
|
|
150
150
|
skippedPlugins: string[]
|
|
151
|
+
// Non-fatal warnings from docker.file.append: unsafe lines stripped from
|
|
152
|
+
// the generated Dockerfile, plus warn-but-allow lines (curl|bash, remote
|
|
153
|
+
// ADD). Surfaced by the CLI so a stripped line is never a silent no-op.
|
|
154
|
+
dockerfileWarnings: string[]
|
|
151
155
|
}
|
|
152
156
|
| { ok: false; reason: string }
|
|
153
157
|
|
|
@@ -485,6 +489,7 @@ export async function start({
|
|
|
485
489
|
alreadyRunning: false,
|
|
486
490
|
autoUpgrade: upgrade,
|
|
487
491
|
skippedPlugins: pluginReconcile.skipped,
|
|
492
|
+
dockerfileWarnings: dockerfileRefresh.warnings,
|
|
488
493
|
}
|
|
489
494
|
} catch (error) {
|
|
490
495
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) }
|
|
@@ -701,18 +706,24 @@ async function resolvePublishHost(exec: DockerExec): Promise<string> {
|
|
|
701
706
|
// the cheapest correct signal: the build context for `docker build` is the
|
|
702
707
|
// Dockerfile itself, so equal contents definitionally produce an equivalent
|
|
703
708
|
// image.
|
|
704
|
-
export async function refreshDockerfile(
|
|
709
|
+
export async function refreshDockerfile(
|
|
710
|
+
cwd: string,
|
|
711
|
+
opts: { buildKit?: boolean } = {},
|
|
712
|
+
): Promise<{ changed: boolean; warnings: string[] }> {
|
|
705
713
|
const cfg = await loadTypeclawConfig(cwd)
|
|
706
714
|
const next = buildDockerfile(cfg.docker.file, {
|
|
707
715
|
baseImageVersion: resolveBaseImageVersion(cwd),
|
|
708
716
|
cjkFontsAuto: hostLocaleIsCjk(),
|
|
709
717
|
buildKit: opts.buildKit,
|
|
710
718
|
})
|
|
719
|
+
// Reuse the renderer's classifier so reported warnings match exactly what was
|
|
720
|
+
// stripped/kept in the Dockerfile above (single source of truth).
|
|
721
|
+
const { warnings } = classifyDockerfileAppend(cfg.docker.file.append)
|
|
711
722
|
const path = join(cwd, DOCKERFILE)
|
|
712
723
|
const prev = await readFile(path, 'utf8').catch(() => null)
|
|
713
|
-
if (prev === next) return { changed: false }
|
|
724
|
+
if (prev === next) return { changed: false, warnings }
|
|
714
725
|
await writeFile(path, next)
|
|
715
|
-
return { changed: true }
|
|
726
|
+
return { changed: true, warnings }
|
|
716
727
|
}
|
|
717
728
|
|
|
718
729
|
// Builds the agent image with a seamless buildx->legacy fallback. The preferred
|
|
@@ -836,6 +847,7 @@ async function reportAlreadyRunning(exec: DockerExec, cwd: string, containerName
|
|
|
836
847
|
}
|
|
837
848
|
const tuiToken = await resolveTuiToken({ cwd, exec })
|
|
838
849
|
const plan = await planStart({ cwd, hostPort, imageExists: true, forceBuild: false, tuiToken })
|
|
850
|
+
const { warnings: dockerfileWarnings } = classifyDockerfileAppend((await loadTypeclawConfig(cwd)).docker.file.append)
|
|
839
851
|
return {
|
|
840
852
|
ok: true,
|
|
841
853
|
plan,
|
|
@@ -847,6 +859,7 @@ async function reportAlreadyRunning(exec: DockerExec, cwd: string, containerName
|
|
|
847
859
|
alreadyRunning: true,
|
|
848
860
|
autoUpgrade: { kind: 'skipped-already-running' },
|
|
849
861
|
skippedPlugins: [],
|
|
862
|
+
dockerfileWarnings,
|
|
850
863
|
}
|
|
851
864
|
}
|
|
852
865
|
|
package/src/cron/consumer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentSession } from '@/agent'
|
|
2
|
+
import { applyTurnThinkingLevel } from '@/agent/attention-escalation'
|
|
2
3
|
import { promptWithFallback, resolveFallbackChain } from '@/agent/model-fallback'
|
|
3
4
|
import type { SessionOrigin } from '@/agent/session-origin'
|
|
4
5
|
import { getConfig } from '@/config'
|
|
@@ -235,6 +236,12 @@ async function runPromptOnce(
|
|
|
235
236
|
if (created.hooks && turnEvent !== undefined) {
|
|
236
237
|
await created.hooks.runSessionTurnStart({ ...turnEvent, userPrompt: job.prompt, retrievalContext })
|
|
237
238
|
}
|
|
239
|
+
// Cron sessions are created fresh per fallback attempt, so the live getter
|
|
240
|
+
// is still the creation-time default here — safe to read without a separate
|
|
241
|
+
// captured field. The test-fake path omits `.session`; skip it then.
|
|
242
|
+
if (created.session !== undefined) {
|
|
243
|
+
applyTurnThinkingLevel(created.session, job.prompt, created.session.thinkingLevel)
|
|
244
|
+
}
|
|
238
245
|
// Bridge the CronSession wrapper into the AgentSession surface the
|
|
239
246
|
// fallback helper expects:
|
|
240
247
|
// prompt → CronSession.prompt (wrapper that calls AgentSession.prompt
|
package/src/doctor/checks.ts
CHANGED
|
@@ -210,7 +210,19 @@ function configValid(): DoctorCheck {
|
|
|
210
210
|
applies: (ctx) => ctx.hasAgentFolder,
|
|
211
211
|
async run(ctx) {
|
|
212
212
|
const result = validateConfig(ctx.cwd)
|
|
213
|
-
if (result.ok)
|
|
213
|
+
if (result.ok) {
|
|
214
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
215
|
+
return {
|
|
216
|
+
status: 'warning',
|
|
217
|
+
message: `typeclaw.json valid; ${result.warnings.length} docker.file.append warning(s):\n${result.warnings.join('\n')}`,
|
|
218
|
+
fix: {
|
|
219
|
+
description:
|
|
220
|
+
'Review the docker.file.append entries above; unsafe lines are stripped from the Dockerfile on start.',
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { status: 'ok', message: 'typeclaw.json valid; mounts accessible' }
|
|
225
|
+
}
|
|
214
226
|
return {
|
|
215
227
|
status: 'error',
|
|
216
228
|
message: result.reason,
|
package/src/init/dockerfile.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateDockerfileAppendLine } from '@/config/config'
|
|
1
2
|
import type { DockerfileConfig, DockerfileFeatureToggle } from '@/config/config'
|
|
2
3
|
import {
|
|
3
4
|
CLAUDE_CREDENTIALS_FILE_NAME,
|
|
@@ -1700,10 +1701,57 @@ RUN ${aptCacheMount(buildKit)}apt-get update \\
|
|
|
1700
1701
|
`
|
|
1701
1702
|
}
|
|
1702
1703
|
|
|
1704
|
+
// Render-time enforcement is the security boundary: this is the sole bottleneck
|
|
1705
|
+
// where docker.file.append entries reach the generated Dockerfile, so unsafe
|
|
1706
|
+
// lines are STRIPPED here (never spliced into the build) rather than blocking
|
|
1707
|
+
// `typeclaw start`. docker.file.append is untrusted input even when the
|
|
1708
|
+
// in-container agent writes it — a bad line (e.g. the decode-and-exec footgun
|
|
1709
|
+
// that bricked a real build) becomes ineffective, not fatal. Validation
|
|
1710
|
+
// elsewhere only warns; this is what guarantees the line never runs.
|
|
1703
1711
|
function renderCustomDockerfileLines(lines: string[]): string {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1712
|
+
const { kept, strippedCount } = classifyDockerfileAppend(lines)
|
|
1713
|
+
if (kept.length === 0 && strippedCount === 0) return ''
|
|
1714
|
+
// Durable, payload-free record so a stripped line isn't a silent no-op: the
|
|
1715
|
+
// operator sees the Dockerfile itself note the count (full reasons surface as
|
|
1716
|
+
// startup warnings). Never echo the stripped content — it may carry secrets.
|
|
1717
|
+
const strippedNote =
|
|
1718
|
+
strippedCount > 0
|
|
1719
|
+
? `# typeclaw stripped ${strippedCount} unsafe docker.file.append line(s); see startup warnings and typeclaw.json.
|
|
1720
|
+
`
|
|
1721
|
+
: ''
|
|
1722
|
+
if (kept.length === 0) return strippedNote ? `${strippedNote}\n` : ''
|
|
1723
|
+
return `${strippedNote}# Custom lines from typeclaw.json#docker.file.append.
|
|
1724
|
+
${kept.join('\n')}
|
|
1707
1725
|
|
|
1708
1726
|
`
|
|
1709
1727
|
}
|
|
1728
|
+
|
|
1729
|
+
export type DockerfileAppendClassification = {
|
|
1730
|
+
kept: string[]
|
|
1731
|
+
warnings: string[]
|
|
1732
|
+
strippedCount: number
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Single source of truth for "what happens to each docker.file.append line",
|
|
1736
|
+
// reusing the config-layer classifier (validateDockerfileAppendLine). Both the
|
|
1737
|
+
// renderer (enforcement: drops unsafe lines) and refreshDockerfile (reporting:
|
|
1738
|
+
// surfaces warnings to the user) call this so the two never drift. Warn-but-
|
|
1739
|
+
// allow lines (curl|bash, remote ADD) are KEPT but produce a warning; structural
|
|
1740
|
+
// and semantic blocks are stripped with a warning.
|
|
1741
|
+
export function classifyDockerfileAppend(lines: string[]): DockerfileAppendClassification {
|
|
1742
|
+
const kept: string[] = []
|
|
1743
|
+
const warnings: string[] = []
|
|
1744
|
+
let strippedCount = 0
|
|
1745
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1746
|
+
const line = lines[i]!
|
|
1747
|
+
const check = validateDockerfileAppendLine(line)
|
|
1748
|
+
if (!check.ok) {
|
|
1749
|
+
strippedCount++
|
|
1750
|
+
warnings.push(`docker.file.append[${i}] stripped — ${check.reason}`)
|
|
1751
|
+
continue
|
|
1752
|
+
}
|
|
1753
|
+
if (check.warning) warnings.push(`docker.file.append[${i}] ${check.warning}`)
|
|
1754
|
+
kept.push(line)
|
|
1755
|
+
}
|
|
1756
|
+
return { kept, warnings, strippedCount }
|
|
1757
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type CreateSessionResult,
|
|
6
6
|
type SessionOrigin,
|
|
7
7
|
} from '@/agent'
|
|
8
|
+
import { applyTurnThinkingLevel } from '@/agent/attention-escalation'
|
|
8
9
|
import type { ChannelRouter } from '@/channels/router'
|
|
9
10
|
import type { McpManager } from '@/mcp'
|
|
10
11
|
import type { PermissionService } from '@/permissions'
|
|
@@ -431,6 +432,7 @@ export async function runPromptForCommand(args: {
|
|
|
431
432
|
retrievalContext.results.length > 0
|
|
432
433
|
? `${renderTurnTimeAnchor()}\n\n${args.text}\n\n${retrievalContext.results}`
|
|
433
434
|
: `${renderTurnTimeAnchor()}\n\n${args.text}`
|
|
435
|
+
applyTurnThinkingLevel(session, args.text, session.thinkingLevel)
|
|
434
436
|
try {
|
|
435
437
|
await session.prompt(turnText)
|
|
436
438
|
} finally {
|
package/src/server/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type CreateSessionOptions,
|
|
9
9
|
type CreateSessionResult,
|
|
10
10
|
} from '@/agent'
|
|
11
|
+
import { applyTurnThinkingLevel } from '@/agent/attention-escalation'
|
|
11
12
|
import { runPluginDoctorChecks, runPluginDoctorFix } from '@/agent/doctor'
|
|
12
13
|
import type { LiveSessionRegistry } from '@/agent/live-sessions'
|
|
13
14
|
import type { LiveSubagentRegistry } from '@/agent/live-subagents'
|
|
@@ -174,6 +175,11 @@ type QueuedPrompt = {
|
|
|
174
175
|
|
|
175
176
|
type SessionState = {
|
|
176
177
|
session: AgentSession
|
|
178
|
+
// The session's creation-time thinking level, captured once. An escalated turn
|
|
179
|
+
// moves `session.thinkingLevel` to `high`, so neither turn-driving path (drain
|
|
180
|
+
// loop, no-stream fallback) can use the live getter as the reset target — both
|
|
181
|
+
// read this captured default instead.
|
|
182
|
+
turnThinkingDefault: AgentSession['thinkingLevel']
|
|
177
183
|
sessionFileId: string
|
|
178
184
|
origin: SessionOrigin
|
|
179
185
|
sessionManager: { getSessionFile: () => string | undefined } | undefined
|
|
@@ -518,6 +524,7 @@ export function createServer({
|
|
|
518
524
|
|
|
519
525
|
const state: SessionState = {
|
|
520
526
|
session,
|
|
527
|
+
turnThinkingDefault: session.thinkingLevel,
|
|
521
528
|
sessionFileId,
|
|
522
529
|
origin,
|
|
523
530
|
sessionManager,
|
|
@@ -782,6 +789,7 @@ export function createServer({
|
|
|
782
789
|
retrievalContext.results.length > 0
|
|
783
790
|
? `${renderTurnTimeAnchor()}\n\n${msg.text}\n\n${retrievalContext.results}`
|
|
784
791
|
: `${renderTurnTimeAnchor()}\n\n${msg.text}`
|
|
792
|
+
applyTurnThinkingLevel(state.session, msg.text, state.turnThinkingDefault)
|
|
785
793
|
await state.session.prompt(turnText)
|
|
786
794
|
send(ws, doneMessage(state))
|
|
787
795
|
} catch (err) {
|
|
@@ -1086,6 +1094,7 @@ async function drain(
|
|
|
1086
1094
|
retrievalContext.results.length > 0
|
|
1087
1095
|
? `${renderTurnTimeAnchor()}\n\n${item.text}\n\n${retrievalContext.results}`
|
|
1088
1096
|
: `${renderTurnTimeAnchor()}\n\n${item.text}`
|
|
1097
|
+
applyTurnThinkingLevel(state.session, item.text, state.turnThinkingDefault)
|
|
1089
1098
|
await state.session.prompt(turnText)
|
|
1090
1099
|
send(ws, doneMessage(state))
|
|
1091
1100
|
} catch (err) {
|