@triflux/core 10.48.1 → 10.49.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.
@@ -469,7 +469,7 @@ async function main() {
469
469
  // UserPromptSubmit 은 턴 *시작* 에, Stop 은 매 어시스턴트 턴 *종료* 에 heartbeat
470
470
  // 한다. 둘이 함께 "대화 중이지만 매 분 새 프롬프트를 보내지는 않는" 세션을 5분
471
471
  // interactive TTL 위로 유지해, 대화 도중 stale 로 떨어져 `cto status`
472
- // live_sessions / tray 에서 사라지는 것을 막는다. 진짜 완전 idle(턴 자체가 없음)
472
+ // live_sessions 에서 사라지는 것을 막는다. 진짜 완전 idle(턴 자체가 없음)
473
473
  // 은 여전히 TTL 후 stale 로 가며 — 그게 의도된 dead-session 신호다. UserPromptSubmit
474
474
  // 과 동일하게 fire-and-forget POST 직후 짧은 상한으로 drain 한다.
475
475
  if (eventName === "Stop") {
@@ -20,8 +20,14 @@
20
20
  "id": "tfx-multi",
21
21
  "patterns": [
22
22
  {
23
- "source": "(?<!\\b(?:my|the|our|omc|oh-my-claudecode)\\s)\\btfx[\\s-]?multi\\b",
24
- "flags": "i"
23
+ "source": "(?<![\\w/.-])/tfx[\\s-]?multi\\b",
24
+ "flags": "i",
25
+ "strength": "explicit"
26
+ },
27
+ {
28
+ "source": "\\btfx[\\s-]?multi\\s*(?:로|으로)\\s*(?:해|돌려|처리|진행|시켜|보내|라우팅)",
29
+ "flags": "i",
30
+ "strength": "explicit"
25
31
  }
26
32
  ],
27
33
  "skill": "tfx-auto",
@@ -66,8 +72,14 @@
66
72
  "id": "tfx-harness",
67
73
  "patterns": [
68
74
  {
69
- "source": "\\btfx[\\s-]?harness\\b",
70
- "flags": "i"
75
+ "source": "(?<![\\w/.-])/tfx[\\s-]?harness\\b",
76
+ "flags": "i",
77
+ "strength": "explicit"
78
+ },
79
+ {
80
+ "source": "\\btfx[\\s-]?harness\\s*(?:로|으로)\\s*(?:해|돌려|처리|진행|시켜|보내|라우팅)",
81
+ "flags": "i",
82
+ "strength": "explicit"
71
83
  }
72
84
  ],
73
85
  "skill": "tfx-harness",
@@ -87,6 +99,10 @@
87
99
  {
88
100
  "source": "\\bwhich\\s+skill\\b",
89
101
  "flags": "i"
102
+ },
103
+ {
104
+ "source": "(?:뭐|어떤|무슨)\\s*스킬|스킬\\s*(?:있나|있어|없나|도움)",
105
+ "flags": "i"
90
106
  }
91
107
  ],
92
108
  "skill": "tfx-harness",
@@ -94,7 +110,9 @@
94
110
  "supersedes": ["tfx-unified"],
95
111
  "exclusive": false,
96
112
  "state": null,
97
- "mcp_route": null
113
+ "mcp_route": null,
114
+ "strength": "suggest",
115
+ "suggest_when": "어떤 스킬·경로를 쓸지 묻는 메타 라우팅 요청"
98
116
  },
99
117
  {
100
118
  "id": "tfx-review",
@@ -112,22 +130,6 @@
112
130
  "mcp_route": null,
113
131
  "explicit": true
114
132
  },
115
- {
116
- "id": "tfx-analysis",
117
- "patterns": [
118
- {
119
- "source": "\\btfx[\\s-]?analysis\\b",
120
- "flags": "i"
121
- }
122
- ],
123
- "skill": "tfx-analysis",
124
- "priority": 1,
125
- "supersedes": ["tfx-unified"],
126
- "exclusive": false,
127
- "state": null,
128
- "mcp_route": null,
129
- "explicit": true
130
- },
131
133
  {
132
134
  "id": "tfx-plan",
133
135
  "patterns": [
@@ -152,7 +154,7 @@
152
154
  "flags": "i"
153
155
  }
154
156
  ],
155
- "skill": "tfx-qa",
157
+ "skill": "tfx-review",
156
158
  "priority": 1,
157
159
  "supersedes": ["tfx-unified"],
158
160
  "exclusive": false,
@@ -176,67 +178,35 @@
176
178
  "mcp_route": null,
177
179
  "explicit": true
178
180
  },
179
- {
180
- "id": "tfx-find",
181
- "patterns": [
182
- {
183
- "source": "\\btfx[\\s-]?find\\b",
184
- "flags": "i"
185
- }
186
- ],
187
- "skill": "tfx-find",
188
- "priority": 1,
189
- "supersedes": ["tfx-unified"],
190
- "exclusive": false,
191
- "state": null,
192
- "mcp_route": null,
193
- "explicit": true
194
- },
195
181
  {
196
182
  "id": "tfx-unified",
197
183
  "patterns": [
198
184
  {
199
- "source": "\\btfx[\\s-]?auto\\b",
200
- "flags": "i"
201
- },
202
- {
203
- "source": "\\btfx[\\s-]?auto[\\s-]?codex\\b",
204
- "flags": "i"
205
- },
206
- {
207
- "source": "(?:만들어|고쳐|구현해|짜줘|수정해|바꿔)",
208
- "flags": "i"
185
+ "source": "(?<![\\w/.-])/tfx[\\s-]?auto(?:[\\s-]?codex)?\\b",
186
+ "flags": "i",
187
+ "strength": "explicit"
209
188
  },
210
189
  {
211
- "source": "(?:리뷰해|검토해|봐줘|괜찮아)",
212
- "flags": "i"
190
+ "source": "\\btfx[\\s-]?auto(?:[\\s-]?codex)?\\s*(?:로|으로)\\s*(?:해|돌려|처리|진행|시켜|보내|라우팅)",
191
+ "flags": "i",
192
+ "strength": "explicit"
213
193
  },
214
194
  {
215
- "source": "(?:테스트|검증|돌려봐|QA)",
195
+ "source": "(?:코드|프론트|화면|버튼|위젯|셸|\\.sh|경로|콜백|생성기|함수|메서드|파일|모듈|클래스|기능|버그|결함|스크립트|훅|스킬|API|엔드포인트|컴포넌트|로직|알고리즘|테스트\\s*코드|빌드|에러|오류|UI|스키마|CLI|패턴|정규식|설정)[^.?!\\n]{0,15}?(?:구현|만들어|고쳐|수정해|짜\\s*줘|작성해|추가해|리팩터|개선해|패치해|지워|넣어|바꿔)",
216
196
  "flags": "i"
217
197
  },
218
198
  {
219
- "source": "(?:분석해|계획|설계해)",
220
- "flags": "i"
221
- },
222
- {
223
- "source": "(?:찾아봐|조사해|검색해)",
224
- "flags": "i"
225
- },
226
- {
227
- "source": "(?:병렬|동시에|parallel|concurrent)",
228
- "flags": "i"
229
- },
230
- {
231
- "source": "(?:점검|진단|확인해)",
232
- "flags": "i"
199
+ "source": "구현\\s*(?:해|하자|할\\s*거|해\\s*줘)",
200
+ "flags": ""
233
201
  },
234
202
  {
235
- "source": "(?:계속해|이어서|진행해)",
203
+ "source": "\\b(?:implement|refactor)\\b|\\bfix\\s+(?:the\\s+|this\\s+|a\\s+)?(?:bug|test|build|error|issue|code)\\b",
236
204
  "flags": "i"
237
- },
205
+ }
206
+ ],
207
+ "exclude_patterns": [
238
208
  {
239
- "source": "\\b(?:implement|build|fix|review|test|plan|analyze)\\b",
209
+ "source": "(?:[??]|(?:있나|없나|되나|될까|할까|인가|일까|하나|어때|맞아|맞나|건가|거야|던데|을걸|을껄))\\s*[??]?\\s*$",
240
210
  "flags": "i"
241
211
  }
242
212
  ],
@@ -245,18 +215,21 @@
245
215
  "supersedes": ["tfx-auto-codex"],
246
216
  "exclusive": false,
247
217
  "state": null,
248
- "mcp_route": null
218
+ "mcp_route": null,
219
+ "strength": "suggest",
220
+ "suggest_when": "코드 구현·수정 실행 요청"
249
221
  },
250
222
  {
251
223
  "id": "host-ai-slop-cleaner",
252
224
  "patterns": [
253
225
  {
254
- "source": "(?:AI\\s*슬롭|ai\\s*slop|anti[\\s-]?slop|deslop)",
226
+ "source": "(?:AI\\s*슬롭|ai\\s*slop)",
255
227
  "flags": "i"
256
228
  },
257
229
  {
258
- "source": "(?:슬롭|클린업)",
259
- "flags": "i"
230
+ "source": "\\b(?:anti[\\s-]?slop|deslop)\\b",
231
+ "flags": "i",
232
+ "strength": "explicit"
260
233
  }
261
234
  ],
262
235
  "skill": "ai-slop-cleaner",
@@ -264,23 +237,9 @@
264
237
  "supersedes": [],
265
238
  "exclusive": false,
266
239
  "state": null,
267
- "mcp_route": null
268
- },
269
- {
270
- "id": "tfx-prune",
271
- "patterns": [
272
- {
273
- "source": "\\btfx[\\s-]?prune\\b",
274
- "flags": "i"
275
- }
276
- ],
277
- "skill": "tfx-prune",
278
- "priority": 1,
279
- "supersedes": ["tfx-unified"],
280
- "exclusive": false,
281
- "state": null,
282
240
  "mcp_route": null,
283
- "explicit": true
241
+ "strength": "suggest",
242
+ "suggest_when": "AI 슬롭 정리(deslop) 실행 요청"
284
243
  },
285
244
  {
286
245
  "id": "tfx-codex",
@@ -312,26 +271,11 @@
312
271
  "state": null,
313
272
  "mcp_route": null
314
273
  },
315
- {
316
- "id": "tfx-hub",
317
- "patterns": [
318
- {
319
- "source": "\\btfx[\\s-]?hub\\b",
320
- "flags": "i"
321
- }
322
- ],
323
- "skill": "tfx-hub",
324
- "priority": 1,
325
- "supersedes": [],
326
- "exclusive": false,
327
- "state": null,
328
- "mcp_route": null
329
- },
330
274
  {
331
275
  "id": "tfx-doctor",
332
276
  "patterns": [
333
277
  {
334
- "source": "\\btfx[\\s-]?doctor\\b",
278
+ "source": "\\btfx[\\s-]?(?:doctor|hub)\\b",
335
279
  "flags": "i"
336
280
  }
337
281
  ],
@@ -412,12 +356,14 @@
412
356
  }
413
357
  ],
414
358
  "skill": "tfx-wt",
415
- "platform": ["win32"],
416
359
  "priority": 1,
417
360
  "supersedes": ["tfx-unified"],
418
361
  "exclusive": false,
419
362
  "state": null,
420
- "mcp_route": null
363
+ "mcp_route": null,
364
+ "platform": ["win32"],
365
+ "strength": "suggest",
366
+ "suggest_when": "Windows Terminal 탭·패인 조작 요청"
421
367
  },
422
368
  {
423
369
  "id": "wt-tab-rename",
@@ -432,12 +378,14 @@
432
378
  }
433
379
  ],
434
380
  "skill": "tfx-wt",
435
- "platform": ["win32"],
436
381
  "priority": 1,
437
382
  "supersedes": ["tfx-unified"],
438
383
  "exclusive": false,
439
384
  "state": null,
440
- "mcp_route": null
385
+ "mcp_route": null,
386
+ "platform": ["win32"],
387
+ "strength": "suggest",
388
+ "suggest_when": "Windows Terminal 탭·패인 조작 요청"
441
389
  },
442
390
  {
443
391
  "id": "wt-tab-list",
@@ -452,12 +400,14 @@
452
400
  }
453
401
  ],
454
402
  "skill": "tfx-wt",
455
- "platform": ["win32"],
456
403
  "priority": 1,
457
404
  "supersedes": ["tfx-unified"],
458
405
  "exclusive": false,
459
406
  "state": null,
460
- "mcp_route": null
407
+ "mcp_route": null,
408
+ "platform": ["win32"],
409
+ "strength": "suggest",
410
+ "suggest_when": "Windows Terminal 탭·패인 조작 요청"
461
411
  },
462
412
  {
463
413
  "id": "wt-tab-close",
@@ -472,229 +422,35 @@
472
422
  }
473
423
  ],
474
424
  "skill": "tfx-wt",
475
- "platform": ["win32"],
476
425
  "priority": 1,
477
426
  "supersedes": ["tfx-unified"],
478
427
  "exclusive": false,
479
428
  "state": null,
480
- "mcp_route": null
481
- },
482
- {
483
- "id": "handoff-route",
484
- "patterns": [
485
- {
486
- "source": "(?:^|(?<=\\s))(핸드오프|handoff|세션\\s*넘기)[\\s]*(생성|만들|써줘|작성|넘겨)",
487
- "flags": "i"
488
- }
489
- ],
490
- "skill": null,
491
- "action": "handoff",
492
- "priority": 5,
493
- "supersedes": [],
494
- "exclusive": false,
495
- "state": null,
496
- "mcp_route": null
497
- },
498
- {
499
- "id": "notion-route",
500
- "patterns": [
501
- {
502
- "source": "(?:^|(?<=\\s))(노션|notion)[\\s-]?(페이지|글|조회|생성|수정|검색)(?:$|(?=\\s))",
503
- "flags": "i"
504
- },
505
- {
506
- "source": "(노션|\\bnotion\\b)",
507
- "flags": "i"
508
- }
509
- ],
510
- "skill": null,
511
- "priority": 10,
512
- "supersedes": [],
513
- "exclusive": false,
514
- "state": null,
515
- "mcp_route": "antigravity"
516
- },
517
- {
518
- "id": "chrome-route",
519
- "patterns": [
520
- {
521
- "source": "(?:^|(?<=\\s))(크롬|chrome|브라우저)[\\s-]?(열|접속|로그인|navigate|click)(?:$|(?=\\s))",
522
- "flags": "i"
523
- },
524
- {
525
- "source": "(크롬|\\bchrome\\b)",
526
- "flags": "i"
527
- }
528
- ],
529
- "skill": null,
530
- "priority": 10,
531
- "supersedes": [],
532
- "exclusive": false,
533
- "state": null,
534
- "mcp_route": "antigravity"
535
- },
536
- {
537
- "id": "jira-route",
538
- "patterns": [
539
- {
540
- "source": "(?:^|(?<=\\s))(jira|지라)[\\s-]?(이슈|티켓|생성|조회|스프린트)(?:$|(?=\\s))",
541
- "flags": "i"
542
- },
543
- {
544
- "source": "(지라|\\bjira\\b)",
545
- "flags": "i"
546
- }
547
- ],
548
- "skill": null,
549
- "priority": 10,
550
- "supersedes": [],
551
- "exclusive": false,
552
- "state": null,
553
- "mcp_route": "codex"
554
- },
555
- {
556
- "id": "mail-route",
557
- "patterns": [
558
- {
559
- "source": "(?:^|(?<=\\s))(메일|gmail|이메일)[\\s-]?(보내|읽|검색|draft)(?:$|(?=\\s))",
560
- "flags": "i"
561
- },
562
- {
563
- "source": "(메일|이메일|\\bgmail\\b)",
564
- "flags": "i"
565
- }
566
- ],
567
- "skill": null,
568
- "priority": 10,
569
- "supersedes": [],
570
- "exclusive": false,
571
- "state": null,
572
- "mcp_route": "antigravity"
573
- },
574
- {
575
- "id": "calendar-route",
576
- "patterns": [
577
- {
578
- "source": "(?:^|(?<=\\s))(일정|캘린더|calendar)[\\s-]?(생성|조회|추가)(?:$|(?=\\s))",
579
- "flags": "i"
580
- },
581
- {
582
- "source": "(일정|캘린더|\\bcalendar\\b)",
583
- "flags": "i"
584
- }
585
- ],
586
- "skill": null,
587
- "priority": 10,
588
- "supersedes": [],
589
- "exclusive": false,
590
- "state": null,
591
- "mcp_route": "antigravity"
592
- },
593
- {
594
- "id": "playwright-route",
595
- "patterns": [
596
- {
597
- "source": "(?:^|(?<=\\s))(playwright|브라우저\\s*테스트)(?:$|(?=\\s))",
598
- "flags": "i"
599
- }
600
- ],
601
- "skill": null,
602
- "priority": 10,
603
- "supersedes": [],
604
- "exclusive": false,
605
- "state": null,
606
- "mcp_route": "antigravity"
607
- },
608
- {
609
- "id": "canva-route",
610
- "patterns": [
611
- {
612
- "source": "(?:^|(?<=\\s))(canva|캔바|디자인\\s*생성)(?:$|(?=\\s))",
613
- "flags": "i"
614
- }
615
- ],
616
- "skill": null,
617
- "priority": 10,
618
- "supersedes": [],
619
- "exclusive": false,
620
- "state": null,
621
- "mcp_route": "antigravity"
622
- },
623
- {
624
- "id": "confluence-route",
625
- "patterns": [
626
- {
627
- "source": "(?:^|(?<=\\s))(confluence|위키|wiki)[\\s-]?(페이지|문서|조회|생성|수정|검색)(?:$|(?=\\s))",
628
- "flags": "i"
629
- },
630
- {
631
- "source": "(위키|\\bconfluence\\b|\\bwiki\\b)",
632
- "flags": "i"
633
- }
634
- ],
635
- "skill": null,
636
- "priority": 10,
637
- "supersedes": [],
638
- "exclusive": false,
639
- "state": null,
640
- "mcp_route": "antigravity"
641
- },
642
- {
643
- "id": "slack-route",
644
- "patterns": [
645
- {
646
- "source": "(?:^|(?<=\\s))(slack|슬랙)[\\s-]?(메시지|채널|보내|읽|검색|알림)(?:$|(?=\\s))",
647
- "flags": "i"
648
- },
649
- {
650
- "source": "(슬랙|\\bslack\\b)",
651
- "flags": "i"
652
- }
653
- ],
654
- "skill": null,
655
- "priority": 10,
656
- "supersedes": [],
657
- "exclusive": false,
658
- "state": null,
659
- "mcp_route": "antigravity"
660
- },
661
- {
662
- "id": "github-route",
663
- "patterns": [
664
- {
665
- "source": "(?:^|(?<=\\s))(github|깃허브)[\\s-]?(이슈|issue|PR|pull|커밋|commit|리포|repo)(?:$|(?=\\s))",
666
- "flags": "i"
667
- },
668
- {
669
- "source": "(깃허브|\\bgithub\\b)",
670
- "flags": "i"
671
- }
672
- ],
673
- "skill": null,
674
- "priority": 10,
675
- "supersedes": [],
676
- "exclusive": false,
677
- "state": null,
678
- "mcp_route": "codex"
429
+ "mcp_route": null,
430
+ "platform": ["win32"],
431
+ "strength": "suggest",
432
+ "suggest_when": "Windows Terminal 탭·패인 조작 요청"
679
433
  },
680
434
  {
681
435
  "id": "gstack-checkpoint",
682
436
  "patterns": [
683
437
  {
684
- "source": "(?:어디까지\\s*했|뭐\\s*하고\\s*있었|뭘\\s*해야\\s*하|까먹|기억이\\s*안|이어서\\s*하|중단\\s*지점)",
438
+ "source": "(?:어디까지\\s*했|뭐\\s*하고\\s*있었|중단\\s*지점)",
685
439
  "flags": "i"
686
440
  },
687
441
  {
688
- "source": "\\b(?:resume|pick\\s*up|where\\s*was\\s*I)\\b",
442
+ "source": "\\bwhere\\s*was\\s*I\\b",
689
443
  "flags": "i"
690
444
  }
691
445
  ],
692
- "skill": "gstack-context-restore",
446
+ "skill_candidates": ["context-restore", "gstack-context-restore"],
693
447
  "priority": 5,
694
448
  "supersedes": [],
695
449
  "exclusive": false,
696
450
  "state": null,
697
- "mcp_route": null
451
+ "mcp_route": null,
452
+ "strength": "suggest",
453
+ "suggest_when": "이전 세션의 작업 맥락을 복원하려는 요청"
698
454
  },
699
455
  {
700
456
  "id": "gstack-office-hours",
@@ -705,34 +461,45 @@
705
461
  },
706
462
  {
707
463
  "source": "\\boffice[\\s-]?hours\\b",
708
- "flags": "i"
464
+ "flags": "i",
465
+ "strength": "explicit"
709
466
  }
710
467
  ],
711
- "skill": "office-hours",
468
+ "skill_candidates": ["office-hours", "gstack-office-hours"],
712
469
  "priority": 5,
713
470
  "supersedes": [],
714
471
  "exclusive": false,
715
472
  "state": null,
716
- "mcp_route": null
473
+ "mcp_route": null,
474
+ "strength": "suggest",
475
+ "suggest_when": "새 제품·아이디어의 문제 정의를 시작하려는 요청"
717
476
  },
718
477
  {
719
478
  "id": "gstack-ship",
720
479
  "patterns": [
721
480
  {
722
- "source": "(?:배포해|PR\\s*만들|릴리스\\s*해|머지하고\\s*배포)",
481
+ "source": "PR\\s*(?:만들|올려|생성|열어)|머지하고\\s*배포|릴리스\\s*해",
723
482
  "flags": "i"
724
483
  },
725
484
  {
726
- "source": "(?<![\\w-])ship\\b(?!.*tfx)",
485
+ "source": "(?<![\\w/-])ship\\s+(?:it|this)\\b",
727
486
  "flags": "i"
728
487
  }
729
488
  ],
730
- "skill": "ship",
489
+ "exclude_patterns": [
490
+ {
491
+ "source": "(?:[??]|(?:있나|없나|되나|될까|할까|인가|일까|하나|어때|맞아|맞나|건가|거야|던데|을걸|을껄))\\s*[??]?\\s*$",
492
+ "flags": "i"
493
+ }
494
+ ],
495
+ "skill_candidates": ["ship", "gstack-ship"],
731
496
  "priority": 1,
732
497
  "supersedes": [],
733
498
  "exclusive": false,
734
499
  "state": null,
735
- "mcp_route": null
500
+ "mcp_route": null,
501
+ "strength": "suggest",
502
+ "suggest_when": "일반 저장소의 PR·배포 실행 요청"
736
503
  },
737
504
  {
738
505
  "id": "gstack-investigate",
@@ -746,27 +513,36 @@
746
513
  "flags": "i"
747
514
  }
748
515
  ],
749
- "skill": "investigate",
516
+ "skill_candidates": ["investigate", "gstack-investigate"],
750
517
  "priority": 5,
751
518
  "supersedes": [],
752
519
  "exclusive": false,
753
520
  "state": null,
754
- "mcp_route": null
521
+ "mcp_route": null,
522
+ "strength": "suggest",
523
+ "suggest_when": "버그·장애의 원인을 조사하려는 요청"
755
524
  },
756
525
  {
757
526
  "id": "gstack-cso",
758
527
  "patterns": [
759
528
  {
760
- "source": "(?:보안\\s*(?:감사|점검|리뷰|검토|스캔)|\\bcso\\b|\\bOWASP\\b|\\bSTRIDE\\b)",
529
+ "source": "(?:보안\\s*(?:감사|점검|리뷰|검토|스캔)|\\bOWASP\\b|\\bSTRIDE\\b)",
761
530
  "flags": "i"
531
+ },
532
+ {
533
+ "source": "\\bcso\\b",
534
+ "flags": "i",
535
+ "strength": "explicit"
762
536
  }
763
537
  ],
764
- "skill": "cso",
538
+ "skill_candidates": ["cso", "gstack-cso"],
765
539
  "priority": 1,
766
540
  "supersedes": [],
767
541
  "exclusive": false,
768
542
  "state": null,
769
- "mcp_route": null
543
+ "mcp_route": null,
544
+ "strength": "suggest",
545
+ "suggest_when": "보안 감사·점검 실행 요청"
770
546
  },
771
547
  {
772
548
  "id": "gstack-qa-browser",
@@ -780,31 +556,42 @@
780
556
  "flags": "i"
781
557
  }
782
558
  ],
783
- "skill": "qa",
559
+ "skill_candidates": ["qa", "gstack-qa"],
784
560
  "priority": 1,
785
561
  "supersedes": [],
786
562
  "exclusive": false,
787
563
  "state": null,
788
- "mcp_route": null
564
+ "mcp_route": null,
565
+ "strength": "suggest",
566
+ "suggest_when": "사이트·브라우저 흐름을 실제로 테스트하려는 요청"
789
567
  },
790
568
  {
791
569
  "id": "gstack-retro",
792
570
  "patterns": [
793
571
  {
794
- "source": "(?:회고|이번\\s*주\\s*뭐\\s*했|주간\\s*리뷰|뭘\\s*했지)",
572
+ "source": "(?:주간|이번\\s*주|지난\\s*주|스프린트)\\s*(?:회고|리뷰)|이번\\s*주\\s*뭐\\s*했",
795
573
  "flags": "i"
796
574
  },
797
575
  {
798
- "source": "\\bretro\\b",
799
- "flags": "i"
576
+ "source": "(?<![\\w/-])retro\\b",
577
+ "flags": "i",
578
+ "strength": "explicit"
800
579
  }
801
580
  ],
802
- "skill": "retro",
581
+ "exclude_patterns": [
582
+ {
583
+ "source": "회고\\s*(?:문서|록|템플릿)|회의|전사",
584
+ "flags": ""
585
+ }
586
+ ],
587
+ "skill_candidates": ["retro", "gstack-retro"],
803
588
  "priority": 5,
804
589
  "supersedes": [],
805
590
  "exclusive": false,
806
591
  "state": null,
807
- "mcp_route": null
592
+ "mcp_route": null,
593
+ "strength": "suggest",
594
+ "suggest_when": "작업 회고를 하려는 요청"
808
595
  },
809
596
  {
810
597
  "id": "gstack-autoplan",
@@ -815,21 +602,30 @@
815
602
  },
816
603
  {
817
604
  "source": "\\bautoplan\\b",
818
- "flags": "i"
605
+ "flags": "i",
606
+ "strength": "explicit"
819
607
  }
820
608
  ],
821
- "skill": "autoplan",
609
+ "skill_candidates": ["autoplan", "gstack-autoplan"],
822
610
  "priority": 5,
823
611
  "supersedes": [],
824
612
  "exclusive": false,
825
613
  "state": null,
826
- "mcp_route": null
614
+ "mcp_route": null,
615
+ "strength": "suggest",
616
+ "suggest_when": "계획 전체 리뷰 파이프라인 실행 요청"
827
617
  },
828
618
  {
829
619
  "id": "suppress-omc-team",
830
620
  "patterns": [
831
621
  {
832
- "source": "(?<!tfx[\\s-]?)(팀|\\bteam\\b)",
622
+ "source": "(?<!tfx[\\s-]?)\\bteam\\b|팀\\s*(?:모드|으로|구성)",
623
+ "flags": "i"
624
+ }
625
+ ],
626
+ "exclude_patterns": [
627
+ {
628
+ "source": "(?<![\\w.-])/team\\b|레드\\s*팀|개발\\s*팀|팀\\s*저장소",
833
629
  "flags": "i"
834
630
  }
835
631
  ],
@@ -839,25 +635,19 @@
839
635
  "supersedes": [],
840
636
  "exclusive": false,
841
637
  "state": null,
842
- "mcp_route": null
638
+ "mcp_route": null,
639
+ "strength": "suggest"
843
640
  },
844
641
  {
845
642
  "id": "tfx-ship",
846
643
  "patterns": [
847
644
  {
848
645
  "source": "\\btfx[\\s-]?ship\\b",
849
- "flags": "i"
850
- },
851
- {
852
- "source": "/ship\\b",
853
- "flags": "i"
646
+ "flags": "i",
647
+ "strength": "explicit"
854
648
  },
855
649
  {
856
- "source": "배포(?!자|사|장|처)",
857
- "flags": ""
858
- },
859
- {
860
- "source": "릴리[즈스]",
650
+ "source": "(?:배포|릴리[즈스])\\s*(?:해|하고|하자|진행|가자|시작)",
861
651
  "flags": ""
862
652
  },
863
653
  {
@@ -865,6 +655,12 @@
865
655
  "flags": ""
866
656
  }
867
657
  ],
658
+ "exclude_patterns": [
659
+ {
660
+ "source": "(?:[??]|(?:있나|없나|되나|될까|할까|인가|일까|하나|어때|맞아|맞나|건가|거야|던데|을걸|을껄))\\s*[??]?\\s*$",
661
+ "flags": "i"
662
+ }
663
+ ],
868
664
  "skill": "tfx-ship",
869
665
  "priority": 1,
870
666
  "supersedes": ["gstack-ship"],
@@ -872,7 +668,9 @@
872
668
  "state": null,
873
669
  "mcp_route": null,
874
670
  "repo_scope": ["triflux"],
875
- "explicit": true
671
+ "explicit": true,
672
+ "strength": "suggest",
673
+ "suggest_when": "triflux 릴리즈·배포 실행 요청"
876
674
  }
877
675
  ]
878
676
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triflux/core",
3
- "version": "10.48.1",
3
+ "version": "10.49.0",
4
4
  "description": "triflux core — CLI routing, pipeline, adapters. Zero native dependencies.",
5
5
  "type": "module",
6
6
  "main": "hub/index.mjs",
@@ -11,7 +11,7 @@
11
11
  "./scripts/*": "./scripts/*"
12
12
  },
13
13
  "engines": {
14
- "node": ">=18.0.0"
14
+ "node": ">=20"
15
15
  },
16
16
  "dependencies": {
17
17
  "@iarna/toml": "^2.2.5",
@@ -2,6 +2,11 @@ import { readFileSync } from "node:fs";
2
2
 
3
3
  const VALID_MCP_ROUTES = new Set(["codex", "antigravity", "gemini", "claude"]);
4
4
 
5
+ // 주입 강도. explicit = 스킬명 직접 언급(명시 토큰) → MUST 호출 문구,
6
+ // suggest = 자연어 매칭 → "실행 요청이면 고려하라" 수준의 제안 문구 (ADR-0021).
7
+ const VALID_STRENGTHS = new Set(["explicit", "suggest"]);
8
+ const DEFAULT_STRENGTH = "explicit";
9
+
5
10
  function logRuleError(message, error) {
6
11
  if (error) {
7
12
  console.error(`[triflux-keyword-rules] ${message}: ${error.message}`);
@@ -13,7 +18,11 @@ function logRuleError(message, error) {
13
18
  function normalizePattern(pattern) {
14
19
  if (!pattern || typeof pattern.source !== "string") return null;
15
20
  if (typeof pattern.flags !== "string") return null;
16
- return { source: pattern.source, flags: pattern.flags };
21
+ if (pattern.strength != null && !VALID_STRENGTHS.has(pattern.strength))
22
+ return null;
23
+ const normalized = { source: pattern.source, flags: pattern.flags };
24
+ if (pattern.strength != null) normalized.strength = pattern.strength;
25
+ return normalized;
17
26
  }
18
27
 
19
28
  function normalizeState(state) {
@@ -32,13 +41,43 @@ function normalizeRule(rule) {
32
41
  if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority))
33
42
  return null;
34
43
 
35
- const patterns = rule.patterns.map(normalizePattern).filter(Boolean);
44
+ // strength: 규칙 기본 주입 강도. 패턴별 strength 가 있으면 그 패턴만 덮어쓴다.
45
+ // 값이 틀리면 규칙 자체를 무효로 본다 (platform 과 같은 규칙).
46
+ if (rule.strength != null && !VALID_STRENGTHS.has(rule.strength)) return null;
47
+ const strength = rule.strength ?? DEFAULT_STRENGTH;
48
+
49
+ const patterns = rule.patterns
50
+ .map(normalizePattern)
51
+ .filter(Boolean)
52
+ .map((p) => ({ ...p, strength: p.strength ?? strength }));
36
53
  if (patterns.length === 0) return null;
37
54
 
55
+ // exclude_patterns: 하나라도 맞으면 그 규칙의 suggest 매칭을 버린다.
56
+ // 명시 토큰(explicit) 매칭은 사용자가 스킬을 직접 부른 것이라 제외하지 않는다.
57
+ if (rule.exclude_patterns != null && !Array.isArray(rule.exclude_patterns))
58
+ return null;
59
+ const excludePatterns = Array.isArray(rule.exclude_patterns)
60
+ ? rule.exclude_patterns
61
+ .map(normalizePattern)
62
+ .filter(Boolean)
63
+ .map(({ source, flags }) => ({ source, flags }))
64
+ : [];
65
+
66
+ // skill_candidates: 머신마다 설치 이름이 다른 외부 스킬(gstack 의 `ship` /
67
+ // `gstack-ship`)의 후보 목록. detector 가 실행 시점에 설치된 첫 이름을 고른다.
68
+ // skill 이 없으면 첫 후보를 대표 이름으로 둔다.
69
+ if (rule.skill_candidates != null && !Array.isArray(rule.skill_candidates))
70
+ return null;
71
+ const skillCandidates = Array.isArray(rule.skill_candidates)
72
+ ? rule.skill_candidates
73
+ .filter((name) => typeof name === "string" && name.trim())
74
+ .map((name) => name.trim())
75
+ : [];
76
+
38
77
  const skill =
39
78
  typeof rule.skill === "string" && rule.skill.trim()
40
79
  ? rule.skill.trim()
41
- : null;
80
+ : (skillCandidates[0] ?? null);
42
81
  const action =
43
82
  typeof rule.action === "string" && rule.action.trim()
44
83
  ? rule.action.trim()
@@ -53,6 +92,12 @@ function normalizeRule(rule) {
53
92
  const hint =
54
93
  typeof rule.hint === "string" && rule.hint.trim() ? rule.hint.trim() : null;
55
94
 
95
+ // suggest_when: suggest 주입문에서 "…이면 <skill> 을 고려하라"의 조건 문구.
96
+ const suggestWhen =
97
+ typeof rule.suggest_when === "string" && rule.suggest_when.trim()
98
+ ? rule.suggest_when.trim()
99
+ : null;
100
+
56
101
  const supersedes = Array.isArray(rule.supersedes)
57
102
  ? rule.supersedes
58
103
  .filter((id) => typeof id === "string" && id.trim())
@@ -82,7 +127,11 @@ function normalizeRule(rule) {
82
127
  return {
83
128
  id: rule.id.trim(),
84
129
  patterns,
130
+ exclude_patterns: excludePatterns,
131
+ strength,
132
+ suggest_when: suggestWhen,
85
133
  skill,
134
+ skill_candidates: skillCandidates,
86
135
  action,
87
136
  hint,
88
137
  priority: rule.priority,
@@ -124,6 +173,9 @@ export function compileRules(rules) {
124
173
  compiledPatterns: rule.patterns.map(
125
174
  (p) => new RegExp(p.source, p.flags),
126
175
  ),
176
+ compiledExcludes: (rule.exclude_patterns || []).map(
177
+ (p) => new RegExp(p.source, p.flags),
178
+ ),
127
179
  };
128
180
  } catch (error) {
129
181
  logRuleError(`정규식 컴파일 실패: ${rule.id}`, error);
@@ -153,12 +205,27 @@ export function matchRules(compiledRules, cleanText) {
153
205
  continue;
154
206
  }
155
207
 
156
- const matched = rule.compiledPatterns.some((pattern) => {
208
+ // compiledPatterns[i] 와 patterns[i] 는 같은 순서다 — 패턴별 strength 를 읽는다.
209
+ const ruleStrength = rule.strength ?? DEFAULT_STRENGTH;
210
+ let explicitHit = false;
211
+ let suggestHit = false;
212
+ rule.compiledPatterns.forEach((pattern, index) => {
157
213
  pattern.lastIndex = 0;
158
- return pattern.test(cleanText);
214
+ if (!pattern.test(cleanText)) return;
215
+ const strength = rule.patterns?.[index]?.strength ?? ruleStrength;
216
+ if (strength === "suggest") suggestHit = true;
217
+ else explicitHit = true;
159
218
  });
160
219
 
161
- if (!matched) continue;
220
+ if (!explicitHit && !suggestHit) continue;
221
+
222
+ if (!explicitHit) {
223
+ const excluded = (rule.compiledExcludes || []).some((pattern) => {
224
+ pattern.lastIndex = 0;
225
+ return pattern.test(cleanText);
226
+ });
227
+ if (excluded) continue;
228
+ }
162
229
 
163
230
  matches.push({
164
231
  id: rule.id,
@@ -173,6 +240,9 @@ export function matchRules(compiledRules, cleanText) {
173
240
  repo_scope: rule.repo_scope || [],
174
241
  platform: rule.platform || [],
175
242
  explicit: rule.explicit === true,
243
+ strength: explicitHit ? "explicit" : "suggest",
244
+ suggest_when: rule.suggest_when || null,
245
+ skill_candidates: rule.skill_candidates || [],
176
246
  });
177
247
  }
178
248
 
@@ -197,13 +267,20 @@ export function resolveConflicts(matches) {
197
267
  seen.add(match.id);
198
268
  }
199
269
 
270
+ // strength 우선순위를 충돌 해소에도 적용한다 — suggest 매칭의 supersedes 는
271
+ // 사용자가 직접 부른 explicit 매칭을 지우지 못한다.
272
+ const strengthById = new Map(
273
+ deduped.map((match) => [match.id, match.strength ?? DEFAULT_STRENGTH]),
274
+ );
200
275
  const superseded = new Set();
201
276
  const resolved = [];
202
277
 
203
278
  for (const match of deduped) {
204
279
  if (superseded.has(match.id)) continue;
205
280
  resolved.push(match);
281
+ const fromSuggest = (match.strength ?? DEFAULT_STRENGTH) === "suggest";
206
282
  for (const targetId of match.supersedes || []) {
283
+ if (fromSuggest && strengthById.get(targetId) === "explicit") continue;
207
284
  superseded.add(targetId);
208
285
  }
209
286
  }