typeclaw 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +3 -1
  2. package/src/agent/plugin-tools.ts +53 -5
  3. package/src/agent/provider-error.ts +10 -0
  4. package/src/agent/session-origin.ts +26 -0
  5. package/src/agent/tools/channel-disengage.ts +13 -9
  6. package/src/bundled-plugins/github-cli-auth/gh-command.ts +124 -6
  7. package/src/bundled-plugins/github-cli-auth/git-askpass.ts +65 -0
  8. package/src/bundled-plugins/github-cli-auth/git-command.ts +638 -0
  9. package/src/bundled-plugins/github-cli-auth/index.ts +138 -38
  10. package/src/bundled-plugins/github-cli-auth/token-class.ts +13 -0
  11. package/src/bundled-plugins/security/policies/prompt-injection.ts +33 -2
  12. package/src/channels/adapters/github/inbound.ts +41 -3
  13. package/src/channels/adapters/slack-bot.ts +17 -9
  14. package/src/channels/continuation-willingness.ts +331 -0
  15. package/src/channels/github-review-claim.ts +105 -0
  16. package/src/channels/github-token-bridge.ts +7 -0
  17. package/src/channels/router.ts +103 -24
  18. package/src/cli/channel.ts +102 -11
  19. package/src/cli/qr.ts +130 -0
  20. package/src/config/config.ts +98 -2
  21. package/src/container/start.ts +12 -0
  22. package/src/init/dockerfile.ts +64 -0
  23. package/src/init/line-auth.ts +8 -3
  24. package/src/plugin/context.ts +5 -1
  25. package/src/plugin/manager.ts +2 -0
  26. package/src/plugin/types.ts +1 -0
  27. package/src/run/index.ts +1 -0
  28. package/src/sandbox/build.ts +27 -0
  29. package/src/sandbox/index.ts +6 -0
  30. package/src/sandbox/package-install.ts +23 -0
  31. package/src/sandbox/policy.ts +31 -0
  32. package/src/sandbox/symlinks.ts +34 -0
  33. package/src/sandbox/writable-zones.ts +164 -4
  34. package/src/skills/typeclaw-channel-github/SKILL.md +4 -2
  35. package/src/skills/typeclaw-github-contributing/SKILL.md +124 -0
  36. package/typeclaw.schema.json +32 -1
@@ -0,0 +1,331 @@
1
+ // A channel turn ends after a successful `channel_reply` (the terminal-reply
2
+ // abort in router.ts). When the model's reply PROMISES to keep working this
3
+ // turn ("바로 확인해볼게요", "let me check", "I'll continue now") but it forgot
4
+ // to set `channel_reply({ continue: true })`, the turn aborts and the promised
5
+ // follow-up never runs. The router uses this detector to inject ONE bounded
6
+ // reminder nudge so the model gets a second chance. See the empty-turn retry
7
+ // (router.ts) for the sibling mechanism this mirrors.
8
+ //
9
+ // Design bias: PREFER FALSE NEGATIVES. A miss leaves the status quo (turn ends,
10
+ // recoverable by a later user message); a false positive costs one wasted
11
+ // reminder-only turn that the model ends with NO_REPLY. So the phrase tables are
12
+ // deliberately narrow — only self-directed FUTURE intent to act THIS turn, never
13
+ // descriptive ("I checked and it's fine") or other-directed ("you can continue")
14
+ // usage. This is a HINT, not a control-flow authority: the abort still fires
15
+ // regardless; only the optional nudge is gated on it.
16
+
17
+ // Strip markdown emphasis/code fences before matching so an inline `gh` span
18
+ // inside "바로 `gh`로 확인할게요" does not split the phrase.
19
+ function normalize(text: string): string {
20
+ return text
21
+ .toLowerCase()
22
+ .replace(/[`*_~]/g, ' ')
23
+ .replace(/\s+/g, ' ')
24
+ .trim()
25
+ }
26
+
27
+ // Self-directed future-intent phrases. Each asserts the SPEAKER will do more
28
+ // work imminently. The leading "i" / "let me" anchors self-direction so
29
+ // "you can continue" never matches.
30
+ const EN_PHRASES: readonly string[] = [
31
+ "i'll continue",
32
+ 'i will continue',
33
+ "i'll keep going",
34
+ "i'll keep checking",
35
+ "i'll keep looking",
36
+ "i'll take a look",
37
+ "i'll check",
38
+ "i'll look into",
39
+ "i'll dig in",
40
+ "i'll go ahead and",
41
+ 'let me check',
42
+ 'let me look',
43
+ 'let me take a look',
44
+ 'let me dig',
45
+ 'let me continue',
46
+ 'let me verify',
47
+ 'checking now',
48
+ 'looking into it now',
49
+ 'working on it now',
50
+ 'on it now',
51
+ 'give me a moment',
52
+ 'give me a sec',
53
+ ]
54
+
55
+ // Korean: -ㄹ게요 / -겠습니다 future-volitional endings on check/look/continue/
56
+ // proceed verbs. These endings are first-person volitional in Korean — they
57
+ // cannot address the listener, so they are safe self-direction anchors that
58
+ // descriptive or other-directed sentences do not produce. Bare "계속" is
59
+ // excluded ("계속 진행하세요" = "you go ahead", terminal).
60
+ const KO_PHRASES: readonly string[] = [
61
+ '확인해볼게요',
62
+ '확인해 볼게요',
63
+ '확인할게요',
64
+ '확인하겠습니다',
65
+ '확인해보겠습니다',
66
+ '확인해 보겠습니다',
67
+ '다시 확인하겠습니다',
68
+ '다시 확인해보겠습니다',
69
+ '이어서 확인',
70
+ '계속 확인',
71
+ '계속 진행할게요',
72
+ '계속 진행하겠습니다',
73
+ '계속하겠습니다',
74
+ '계속할게요',
75
+ '바로 확인',
76
+ '바로 볼게요',
77
+ '바로 진행',
78
+ '살펴볼게요',
79
+ '살펴보겠습니다',
80
+ '진행하겠습니다',
81
+ '잠시만요',
82
+ '잠깐만요',
83
+ '곧 알려',
84
+ ]
85
+
86
+ // The remaining languages mirror the precision-first selection above: every
87
+ // entry pairs a FIRST-PERSON future/volitional anchor with a work verb
88
+ // (check/look/continue/proceed/verify) or is an immediate-work idiom ("on it
89
+ // now"). The same false-negative bias holds — bare verbs, bare acknowledgments
90
+ // ("ok", "sí", "好"), second-person imperatives ("you continue"), and
91
+ // descriptive past forms ("I checked") are deliberately excluded because a
92
+ // substring match on those would mis-fire. Latin/Cyrillic/Arabic/Indic entries
93
+ // are inflected first-person-future forms (or multi-word) so they cannot
94
+ // collide with a bare common word; CJK entries are full 4+ character
95
+ // intent phrases, never a lone noun.
96
+
97
+ // Spanish: "voy a" / "déjame" + work verb; "enseguida" (right away) idioms.
98
+ const ES_PHRASES: readonly string[] = [
99
+ 'voy a revisar',
100
+ 'voy a comprobar',
101
+ 'voy a verificar',
102
+ 'voy a mirar',
103
+ 'voy a continuar',
104
+ 'voy a seguir',
105
+ 'déjame revisar',
106
+ 'déjame comprobar',
107
+ 'déjame verificar',
108
+ 'déjame mirar',
109
+ 'lo reviso enseguida',
110
+ 'lo verifico enseguida',
111
+ 'enseguida lo reviso',
112
+ 'enseguida reviso',
113
+ 'un momento',
114
+ 'dame un momento',
115
+ 'dame un segundo',
116
+ ]
117
+
118
+ // French: "je vais" + work verb; "laisse-moi" idioms.
119
+ const FR_PHRASES: readonly string[] = [
120
+ 'je vais vérifier',
121
+ 'je vais regarder',
122
+ 'je vais continuer',
123
+ 'je vais poursuivre',
124
+ 'je vais voir',
125
+ 'je vais contrôler',
126
+ 'laisse-moi vérifier',
127
+ 'laisse-moi regarder',
128
+ 'je vérifie tout de suite',
129
+ 'je regarde tout de suite',
130
+ 'un instant',
131
+ 'donne-moi un instant',
132
+ 'donne-moi une seconde',
133
+ ]
134
+
135
+ // Italian: "vado a" / "fammi" + work verb; "controllo subito" idioms.
136
+ const IT_PHRASES: readonly string[] = [
137
+ 'vado a controllare',
138
+ 'vado a verificare',
139
+ 'vado a guardare',
140
+ 'fammi controllare',
141
+ 'fammi verificare',
142
+ 'fammi guardare',
143
+ 'controllo subito',
144
+ 'verifico subito',
145
+ 'continuo subito',
146
+ 'un momento',
147
+ 'dammi un momento',
148
+ 'dammi un secondo',
149
+ ]
150
+
151
+ // Portuguese: "vou" + work verb; "deixa eu" idioms.
152
+ const PT_PHRASES: readonly string[] = [
153
+ 'vou verificar',
154
+ 'vou checar',
155
+ 'vou conferir',
156
+ 'vou olhar',
157
+ 'vou continuar',
158
+ 'vou prosseguir',
159
+ 'deixa eu verificar',
160
+ 'deixa eu conferir',
161
+ 'deixa eu olhar',
162
+ 'verifico já',
163
+ 'já verifico',
164
+ 'um momento',
165
+ 'me dê um momento',
166
+ 'me dá um segundo',
167
+ ]
168
+
169
+ // German: "ich werde" / "lass mich" + work verb; "ich schaue gleich" idioms.
170
+ const DE_PHRASES: readonly string[] = [
171
+ 'ich werde prüfen',
172
+ 'ich werde überprüfen',
173
+ 'ich werde nachsehen',
174
+ 'ich werde weitermachen',
175
+ 'ich werde fortfahren',
176
+ 'lass mich prüfen',
177
+ 'lass mich nachsehen',
178
+ 'ich schaue gleich',
179
+ 'ich prüfe gleich',
180
+ 'gleich prüfen',
181
+ 'gleich überprüfen',
182
+ 'gleich nachsehen',
183
+ 'einen moment',
184
+ 'einen augenblick',
185
+ 'gib mir eine sekunde',
186
+ ]
187
+
188
+ // Russian: first-person-future verbs (проверю/посмотрю/продолжу) — the -ю/-у
189
+ // inflection is unambiguously "I will", so it is a safe self-anchor.
190
+ const RU_PHRASES: readonly string[] = [
191
+ 'сейчас проверю',
192
+ 'я проверю',
193
+ 'я посмотрю',
194
+ 'я продолжу',
195
+ 'продолжу проверку',
196
+ 'сейчас посмотрю',
197
+ 'дайте мне минуту',
198
+ 'одну секунду',
199
+ 'минутку',
200
+ ]
201
+
202
+ // Chinese: 我会/我来/我再 + work verb. Full multi-character intent phrases only;
203
+ // no bare nouns. 继续 alone is excluded (could be "you continue").
204
+ const ZH_PHRASES: readonly string[] = [
205
+ '我来确认',
206
+ '我来检查',
207
+ '我来看看',
208
+ '我会确认',
209
+ '我会检查',
210
+ '我会继续',
211
+ '我再确认',
212
+ '我再检查',
213
+ '我继续确认',
214
+ '我马上确认',
215
+ '我马上检查',
216
+ '我马上看',
217
+ '稍等一下',
218
+ '我看一下',
219
+ ]
220
+
221
+ // Japanese: -てみます / -します first-person volitional on check/look/continue.
222
+ // Bare nouns (確認) are excluded; the verb ending carries the self-direction.
223
+ const JA_PHRASES: readonly string[] = [
224
+ '確認します',
225
+ '確認してみます',
226
+ '確認いたします',
227
+ '調べてみます',
228
+ '調べます',
229
+ '見てみます',
230
+ '続けます',
231
+ '引き続き確認します',
232
+ 'すぐ確認します',
233
+ '少々お待ちください',
234
+ 'ちょっと待ってください',
235
+ ]
236
+
237
+ // Arabic: future particle سـ prefixed first-person verb (سأتحقق = "I will
238
+ // verify"). The سأ prefix is unambiguously first-person-future.
239
+ const AR_PHRASES: readonly string[] = [
240
+ 'سأتحقق',
241
+ 'سأتأكد',
242
+ 'سأراجع',
243
+ 'سأطلع',
244
+ 'سأكمل',
245
+ 'سأواصل',
246
+ 'دعني أتحقق',
247
+ 'دعني أراجع',
248
+ 'لحظة من فضلك',
249
+ ]
250
+
251
+ // Hindi: first-person-future "मैं … करूँगा/देखूँगा" forms (multi-word so they
252
+ // cannot collide with a bare common word).
253
+ const HI_PHRASES: readonly string[] = [
254
+ 'जाँच करूँगा',
255
+ 'जांच करूंगा',
256
+ 'देख लूँगा',
257
+ 'देख लूंगा',
258
+ 'जारी रखूँगा',
259
+ 'जारी रखूंगा',
260
+ 'एक मिनट रुकिए',
261
+ ]
262
+
263
+ // Turkish: first-person-future "-eceğim/-acağım" on check/look/continue verbs.
264
+ const TR_PHRASES: readonly string[] = [
265
+ 'kontrol edeceğim',
266
+ 'kontrol ediyorum',
267
+ 'bakacağım',
268
+ 'inceleyeceğim',
269
+ 'devam edeceğim',
270
+ 'hemen kontrol ediyorum',
271
+ 'hemen bakıyorum',
272
+ 'bir saniye',
273
+ 'bir dakika',
274
+ ]
275
+
276
+ // Vietnamese: "tôi sẽ" / "để tôi" (I will / let me) + work verb.
277
+ const VI_PHRASES: readonly string[] = [
278
+ 'tôi sẽ kiểm tra',
279
+ 'tôi sẽ xem',
280
+ 'tôi sẽ tiếp tục',
281
+ 'để tôi kiểm tra',
282
+ 'để tôi xem',
283
+ 'tôi kiểm tra ngay',
284
+ 'tôi xem ngay',
285
+ 'chờ một chút',
286
+ 'đợi một chút',
287
+ ]
288
+
289
+ // Indonesian: "saya akan" / "biar saya" (I will / let me) + work verb.
290
+ const ID_PHRASES: readonly string[] = [
291
+ 'saya akan periksa',
292
+ 'saya akan cek',
293
+ 'saya akan lihat',
294
+ 'saya akan lanjutkan',
295
+ 'biar saya periksa',
296
+ 'biar saya cek',
297
+ 'saya cek dulu',
298
+ 'saya periksa dulu',
299
+ 'tunggu sebentar',
300
+ 'sebentar ya',
301
+ ]
302
+
303
+ const ALL_PHRASES: readonly string[] = [
304
+ ...EN_PHRASES,
305
+ ...KO_PHRASES,
306
+ ...ES_PHRASES,
307
+ ...FR_PHRASES,
308
+ ...IT_PHRASES,
309
+ ...PT_PHRASES,
310
+ ...DE_PHRASES,
311
+ ...RU_PHRASES,
312
+ ...ZH_PHRASES,
313
+ ...JA_PHRASES,
314
+ ...AR_PHRASES,
315
+ ...HI_PHRASES,
316
+ ...TR_PHRASES,
317
+ ...VI_PHRASES,
318
+ ...ID_PHRASES,
319
+ ]
320
+
321
+ // Reply texts shorter than this are almost always a complete final answer
322
+ // ("네", "ok", "done") where a partial match would be noise. The shortest
323
+ // legitimate intent phrases ("on it now", "확인할게요") clear this floor.
324
+ const MIN_LENGTH = 4
325
+
326
+ export function detectContinuationWillingness(text: string): boolean {
327
+ if (text.length < MIN_LENGTH) return false
328
+ const normalized = normalize(text)
329
+ if (normalized.length < MIN_LENGTH) return false
330
+ return ALL_PHRASES.some((phrase) => normalized.includes(phrase))
331
+ }
@@ -6,6 +6,15 @@
6
6
  export type ReviewClaim = 'block-approve' | 'block-request-changes' | 'block-resolve' | 'warn' | 'ignore'
7
7
 
8
8
  // Word-boundary anchored so "approved" never fires inside "unapproved".
9
+ //
10
+ // Multilingual policy (read before adding): this classifier can BLOCK a real
11
+ // reply, so the bar is precision, never recall. Each non-English BLOCK entry is
12
+ // a verdict word a reviewer only utters when actually approving/blocking; bare
13
+ // ambiguous words are left to the warn tier. Critically, every language that
14
+ // gains a BLOCK/ WARN phrase below ALSO gains the matching negation/future
15
+ // demotion in DEMOTE_TO_IGNORE — without that, "I won't approve" in that
16
+ // language would block. CJK scripts have no \b word boundary, so CJK verdict
17
+ // tokens are multi-character and matched without \b.
9
18
  const BLOCK_APPROVE: readonly RegExp[] = [
10
19
  /\bapproved\b/,
11
20
  /\bapproving\b/,
@@ -14,6 +23,36 @@ const BLOCK_APPROVE: readonly RegExp[] = [
14
23
  /\bsubmitting (the )?approval\b/,
15
24
  /\bformal approval\b/,
16
25
  /\blgtm,? approved\b/,
26
+ // es/pt: aprobado/aprobada, aprovado/aprovada
27
+ /\baprob(?:ado|ada)\b/,
28
+ /\baprov(?:ado|ada)\b/,
29
+ // fr: approuvé/approuvée — no trailing \b: JS \b is ASCII-only, so it is not
30
+ // a boundary after the accented é, which would drop the bare "approuvé".
31
+ /\bapprouv[ée]e?/,
32
+ // it: approvato/approvata
33
+ /\bapprovat[oa]\b/,
34
+ // de: genehmigt / freigegeben
35
+ /\bgenehmigt\b/,
36
+ /\bfreigegeben\b/,
37
+ // ru: одобрено / одобряю
38
+ /\u043E\u0434\u043E\u0431\u0440(?:\u0435\u043D\u043E|\u044F\u044E)/,
39
+ // tr: onaylandı / onaylıyorum — trailing \b dropped (ASCII-only \b is not a
40
+ // boundary after the dotless ı).
41
+ /\bonayl(?:and\u0131|\u0131yorum)/,
42
+ // id: disetujui
43
+ /\bdisetujui\b/,
44
+ // vi: đã duyệt / chấp thuận
45
+ /\u0111\u00E3 duy\u1EC7t/,
46
+ // ja: 承認しました / 承認します
47
+ /\u627F\u8A8D\u3057\u307E(?:\u3057\u305F|\u3059)/,
48
+ // zh: 已批准 / 批准了 / 我批准
49
+ /\u5DF2\u6279\u51C6/,
50
+ /\u6279\u51C6\u4E86/,
51
+ /\u6211\u6279\u51C6/,
52
+ // ar: تمت الموافقة / أوافق
53
+ /\u062A\u0645\u062A \u0627\u0644\u0645\u0648\u0627\u0641\u0642\u0629/,
54
+ // hi: स्वीकृत / मंज़ूर
55
+ /\u0938\u094D\u0935\u0940\u0915\u0943\u0924/,
17
56
  ]
18
57
 
19
58
  const BLOCK_REQUEST_CHANGES: readonly RegExp[] = [
@@ -22,6 +61,26 @@ const BLOCK_REQUEST_CHANGES: readonly RegExp[] = [
22
61
  /\bi request changes\b/,
23
62
  /\bblocking (this|the|merge)\b/,
24
63
  /\bthis is blocked\b/,
64
+ // es: solicito/solicité cambios, cambios solicitados
65
+ /\bsolicit[oé] cambios\b/,
66
+ /\bcambios solicitados\b/,
67
+ // fr: je demande des modifications, modifications demandées
68
+ /\bje demande des modifications\b/,
69
+ /\bmodifications demand[ée]es\b/,
70
+ // de: änderungen angefordert / erforderlich
71
+ /\b\u00E4nderungen (?:angefordert|erforderlich)\b/,
72
+ // it: modifiche richieste
73
+ /\bmodifiche richieste\b/,
74
+ // pt: alterações solicitadas
75
+ /\baltera[çc][õo]es solicitadas\b/,
76
+ // ru: запрошены изменения / нужны изменения
77
+ /\u0437\u0430\u043F\u0440\u043E\u0448\u0435\u043D\u044B \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F/,
78
+ // ja: 変更を要求します / 修正が必要です
79
+ /\u5909\u66F4\u3092\u8981\u6C42\u3057\u307E\u3059/,
80
+ // zh: 请求修改 / 需要修改
81
+ /\u8BF7\u6C42\u4FEE\u6539/,
82
+ // tr: değişiklik istiyorum / talep edildi
83
+ /\bde\u011Fi\u015Fiklik (?:istiyorum|talep edildi)\b/,
25
84
  ]
26
85
 
27
86
  // Bare "resolved" is intentionally NOT here — it collides with the warn-tier
@@ -59,6 +118,20 @@ const WARN_POSITIVE_CLOSEOUT: readonly RegExp[] = [
59
118
  // ignore by the negation/future markers below ("haven't addressed", "to
60
119
  // address").
61
120
  /\baddress(es|ed)\b[^.!?]*\b(concern|feedback|review|comment|issue|point)/,
121
+ // Close-out chatter per language. These are softer than the BLOCK tier and can
122
+ // be demoted by the negation/future markers below.
123
+ /\bse ve bien\b/, // es looks good
124
+ /\bse ve correcto\b/, // es
125
+ /\b[çc]a me va\b/, // fr looks good to me
126
+ /\bme parece bien\b/, // es seems fine
127
+ /\bva bene\b/, // it fine/ok-ish (close-out reading)
128
+ /\bsieht gut aus\b/, // de looks good
129
+ /\bparece (?:bom|certo)\b/, // pt looks good/right
130
+ /\u0432\u044B\u0433\u043B\u044F\u0434\u0438\u0442 \u0445\u043E\u0440\u043E\u0448\u043E/, // ru looks good
131
+ /\u770B\u8D77\u6765\u4E0D\u9519/, // zh looks good
132
+ /\u554F\u984C\u306A\u3055\u305D\u3046/, // ja seems fine
133
+ /\bsorun yok gibi\b/, // tr seems fine
134
+ /\btampaknya (?:baik|oke)\b/, // id seems good
62
135
  ]
63
136
 
64
137
  // Negative warn phrases re-assert a block ("not done yet") instead of closing it
@@ -84,6 +157,38 @@ const DEMOTE_TO_IGNORE: readonly RegExp[] = [
84
157
  /\b(i'?ll|i will|going to|gonna|about to|planning to|need(s)? to|have to|want(s)? to|trying to)\b[^.!?]*\baddress/,
85
158
  /\b(approved|resolved|requested changes)\b[^.!?]*\b(earlier|already|yesterday|before|last (review|time)|previously)\b/,
86
159
  /\b(pre|self|co|re|un|non|ai|admin|user|machine|auto) approved\b/,
160
+ // Multilingual negation / future-intent demotion. Mandatory companions to the
161
+ // multilingual BLOCK/WARN phrases above: each pairs a negation or future
162
+ // marker with an approve/change/resolve/close verb stem in that language so a
163
+ // declined or deferred verdict ("no apruebo", "je vais approuver",
164
+ // "まだ承認していません") never blocks a real reply.
165
+ // es/pt: no / não / todavía / aún / voy a / vou + aprob/aprov/resol/cambios.
166
+ // Portuguese standalone "não" must be its own alternative — Spanish "\bno\b"
167
+ // does not cover it, so "Não aprovado." (not approved) would otherwise hit
168
+ // the new aprovado approval blocker.
169
+ /\b(?:no|n[ãa]o)\b[^.!?]*\b(aprob|aprov|resol|cambios|altera)/,
170
+ /\b(todav[íi]a no|a[úu]n no|ainda n[ãa]o)\b[^.!?]*\b(aprob|aprov|resol)/,
171
+ /\b(voy a|vou)\b[^.!?]*\b(aprob|aprov|revisar|resolver)/,
172
+ // fr: ne…pas / pas encore / je vais + approuv/résol/modif
173
+ /\bpas (?:encore )?\b[^.!?]*\b(approuv|r[ée]sol|modif)/,
174
+ /\bje vais\b[^.!?]*\b(approuv|revoir|r[ée]sol)/,
175
+ // it: non / non ancora / sto per + approv/risol/modif
176
+ /\bnon (?:ancora )?\b[^.!?]*\b(approv|risol|modif)/,
177
+ // de: nicht / noch nicht / werde + genehm/freigeb/änder
178
+ /\b(?:noch )?nicht\b[^.!?]*\b(genehm|freigeb|\u00E4nder)/,
179
+ /\bich werde\b[^.!?]*\b(genehm|freigeb|pr[üu]f)/,
180
+ // ru: не / ещё не / пока не (одобр/измен/реш)
181
+ /\u043D\u0435\s[^.!?]*(\u043E\u0434\u043E\u0431\u0440|\u0438\u0437\u043C\u0435\u043D|\u0440\u0435\u0448)/,
182
+ // ja: まだ…ません / ていない (not yet approved/resolved)
183
+ /\u307E\u3060[^.!?]*(\u307E\u305B\u3093|\u3066\u3044\u306A\u3044)/,
184
+ // zh: 还没/不/未 + 批准/修改/解决
185
+ /(?:\u8FD8\u6CA1|\u4E0D|\u672A)[^.!?]*(\u6279\u51C6|\u4FEE\u6539|\u89E3\u51B3)/,
186
+ // tr: değil / henüz / -mayacağım (onayla/değişiklik)
187
+ /\b(?:hen[üu]z|de\u011Fil)\b[^.!?]*\b(onayl|de\u011Fi\u015Fik)/,
188
+ // id: belum / tidak + setuju/ubah/selesai
189
+ /\b(?:belum|tidak)\b[^.!?]*\b(setuju|ubah|selesai)/,
190
+ // vi: chưa / không + duyệt
191
+ /(?:ch\u01B0a|kh\u00F4ng)[^.!?]*duy\u1EC7t/,
87
192
  ]
88
193
 
89
194
  const QUESTION_CONTEXT =
@@ -9,6 +9,12 @@ export type ResolveGithubTokenForRepo = (repoSlug: string) => Promise<GithubToke
9
9
 
10
10
  export type GithubTokenBridge = {
11
11
  resolveTokenForRepo: ResolveGithubTokenForRepo
12
+ // True when a per-repo App-token minter is registered (only the GitHub App
13
+ // adapter registers one). This is the non-secret "App auth with per-repo
14
+ // minting is available" signal: it stays true for multi-owner / no-repos App
15
+ // configs where the process-wide GH_TOKEN is intentionally NOT seeded, so the
16
+ // git/gh mint paths can no longer rely on GH_TOKEN's prefix to detect App auth.
17
+ hasAppTokenResolver: () => boolean
12
18
  registerResolver: (resolver: (repoSlug: string) => Promise<string>) => () => void
13
19
  }
14
20
 
@@ -30,6 +36,7 @@ export function createGithubTokenBridge(): GithubTokenBridge {
30
36
  return { kind: 'unavailable', reason: err instanceof Error ? err.message : String(err) }
31
37
  }
32
38
  },
39
+ hasAppTokenResolver: () => current !== null,
33
40
  registerResolver: (resolver) => {
34
41
  current = resolver
35
42
  return () => {