triflux 10.13.1 → 10.13.3

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 (84) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/bin/triflux.mjs +123 -13
  4. package/config/mcp-registry.json +29 -0
  5. package/package.json +64 -21
  6. package/scripts/__tests__/setup-cleanup-stale-skills.test.mjs +105 -0
  7. package/scripts/setup.mjs +7 -8
  8. package/scripts/tfx-route-worker.mjs +1 -1
  9. package/scripts/tfx-route.sh +69 -3
  10. package/tui/codex-profile.mjs +457 -0
  11. package/tui/core.mjs +266 -0
  12. package/tui/doctor.mjs +375 -0
  13. package/tui/gemini-profile.mjs +299 -0
  14. package/tui/monitor-data.mjs +152 -0
  15. package/tui/monitor.mjs +333 -0
  16. package/tui/setup.mjs +599 -0
  17. package/CLAUDE.md +0 -161
  18. package/references/cli-parameter-reference.md +0 -240
  19. package/references/codex-plugin-cc-analysis.md +0 -706
  20. package/references/codex-plugin-cc-code-patterns.md +0 -468
  21. package/references/hosts.json +0 -46
  22. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  23. package/skills/tfx-workspace/evals/evals.json +0 -79
  24. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  25. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  26. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  27. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  28. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  29. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  30. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  31. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  32. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  33. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  34. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  35. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  36. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  37. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  38. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  39. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  40. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  41. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  42. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  43. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  44. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  45. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  46. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  47. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  48. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  49. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  50. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  51. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  52. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  53. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  54. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  55. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  56. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  57. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  58. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  59. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  60. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  61. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  62. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  63. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  64. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  65. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  66. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  67. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  68. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  69. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  70. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  71. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  72. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  73. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  74. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  75. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  76. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  77. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  78. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  79. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  80. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  81. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  82. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  83. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  84. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "triflux",
4
+ "description": "CLI-first multi-model orchestrator — Codex/Gemini/Claude routing with DAG execution, auto-triage, and cost optimization",
5
+ "owner": {
6
+ "name": "tellang"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "triflux",
11
+ "description": "Tri-CLI orchestrator for Claude Code. Routes tasks across Claude + Codex + Gemini with consensus intelligence, natural language routing, 42 skills, and cross-model review.",
12
+ "version": "10.13.3",
13
+ "author": {
14
+ "name": "tellang"
15
+ },
16
+ "source": {
17
+ "source": "npm",
18
+ "package": "triflux"
19
+ },
20
+ "category": "productivity",
21
+ "homepage": "https://github.com/tellang/triflux",
22
+ "tags": [
23
+ "multi-model",
24
+ "codex",
25
+ "gemini",
26
+ "cli-routing",
27
+ "orchestration",
28
+ "cost-optimization",
29
+ "dag-execution"
30
+ ]
31
+ }
32
+ ],
33
+ "version": "10.13.3"
34
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "triflux",
3
+ "version": "10.13.3",
4
+ "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
+ "author": {
6
+ "name": "tellang"
7
+ },
8
+ "repository": "https://github.com/tellang/triflux",
9
+ "homepage": "https://github.com/tellang/triflux",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "claude-code",
13
+ "plugin",
14
+ "codex",
15
+ "gemini",
16
+ "cli-routing",
17
+ "orchestration",
18
+ "multi-model"
19
+ ],
20
+ "skills": "./skills/",
21
+ "hooks": "./hooks/hooks.json"
22
+ }
package/bin/triflux.mjs CHANGED
@@ -134,7 +134,8 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
134
134
  ],
135
135
  },
136
136
  doctor: {
137
- usage: "tfx doctor [--fix] [--reset] [--audit] [--diagnose] [--json]",
137
+ usage:
138
+ "tfx doctor [--fix] [--reset] [--audit] [--diagnose] [--purge-logs] [--json]",
138
139
  description: "설치 상태 진단 및 자동 복구",
139
140
  options: [
140
141
  {
@@ -158,6 +159,12 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
158
159
  description:
159
160
  "진단 번들(zip) 생성: spawn-trace + hook timing + system info",
160
161
  },
162
+ {
163
+ name: "--purge-logs",
164
+ type: "boolean",
165
+ description:
166
+ "--fix 와 함께 사용. cli-issues.jsonl 에서 7일 초과 항목 물리 삭제 (#144)",
167
+ },
161
168
  {
162
169
  name: "--json",
163
170
  type: "boolean",
@@ -1742,7 +1749,12 @@ function ensureValidRegistryState() {
1742
1749
  }
1743
1750
 
1744
1751
  async function cmdDoctor(options = {}) {
1745
- const { fix = false, reset = false, json = false } = options;
1752
+ const {
1753
+ fix = false,
1754
+ reset = false,
1755
+ purgeLogs = false,
1756
+ json = false,
1757
+ } = options;
1746
1758
  const report = {
1747
1759
  status: "ok",
1748
1760
  mode: reset ? "reset" : fix ? "fix" : "check",
@@ -2357,9 +2369,20 @@ async function cmdDoctor(options = {}) {
2357
2369
  info(`업데이트 권장:\n${formatPsmuxUpdateGuidance(" ")}`);
2358
2370
  }
2359
2371
  if (psmuxSupport.missingOptionalCommands?.length > 0) {
2372
+ // #144: 단순히 "detach-first hardening 경로에서만 사용" 만으로는 사용자가
2373
+ // 영향 범위와 해결 방법을 알 수 없다. 각 capability 별 영향과 업그레이드 명령을 명시.
2360
2374
  info(
2361
- `선택 capability 미지원: ${psmuxSupport.missingOptionalCommands.join(", ")} (detach-first hardening 경로에서만 사용)`,
2375
+ `선택 capability 미지원: ${psmuxSupport.missingOptionalCommands.join(", ")}`,
2362
2376
  );
2377
+ if (psmuxSupport.missingOptionalCommands.includes("detach-client")) {
2378
+ info(
2379
+ " detach-client: WT 1.24 ConPTY close-race 회피용. WT 기반 병렬 실행(swarm dashboard, tfx-multi wt 모드) 에서 pane freeze/ConPTY hang 위험 증가.",
2380
+ );
2381
+ info(
2382
+ " 해결: psmux v3.4+ 로 업그레이드. 현재 psmux 업그레이드 명령:",
2383
+ );
2384
+ info(`${formatPsmuxUpdateGuidance(" ")}`);
2385
+ }
2363
2386
  }
2364
2387
 
2365
2388
  // 기본 셸 확인: psmux 세션의 기본 셸이 PowerShell인지 cmd.exe인지
@@ -2771,6 +2794,10 @@ async function cmdDoctor(options = {}) {
2771
2794
  ).version;
2772
2795
  let cleaned = 0;
2773
2796
 
2797
+ // #144: 오래된 로그 노이즈 완화 — 7일 초과 항목은 INFO 레벨로 downgrade.
2798
+ // --fix --purge-logs 플래그가 있으면 해당 오래된 항목은 실제 삭제.
2799
+ const STALE_AGE_MS = 7 * 24 * 3600 * 1000;
2800
+ let purged = 0;
2774
2801
  for (const [key, g] of Object.entries(groups)) {
2775
2802
  const fixVer = KNOWN_FIXES[key];
2776
2803
  if (fixVer && semverGte(currentVer, fixVer)) {
@@ -2785,30 +2812,62 @@ async function cmdDoctor(options = {}) {
2785
2812
  : age < 86400000
2786
2813
  ? `${Math.round(age / 3600000)}시간 전`
2787
2814
  : `${Math.round(age / 86400000)}일 전`;
2788
- const sev =
2789
- g.severity === "error"
2815
+ const isStale = age >= STALE_AGE_MS;
2816
+ if (isStale && fix && purgeLogs) {
2817
+ purged += g.count;
2818
+ continue;
2819
+ }
2820
+ const sev = isStale
2821
+ ? `${CYAN}INFO${RESET}`
2822
+ : g.severity === "error"
2790
2823
  ? `${RED}ERROR${RESET}`
2791
2824
  : `${YELLOW}WARN${RESET}`;
2792
- warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
2825
+ const staleTag = isStale ? " [STALE]" : "";
2826
+ if (isStale) {
2827
+ info(
2828
+ `[${sev}]${staleTag} ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`,
2829
+ );
2830
+ } else {
2831
+ warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
2832
+ }
2793
2833
  if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
2794
2834
  if (fixVer)
2795
2835
  info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
2796
- issues++;
2836
+ if (isStale && !purgeLogs) {
2837
+ info(` 7일 초과 — 삭제: tfx doctor --fix --purge-logs`);
2838
+ }
2839
+ if (!isStale) issues++;
2797
2840
  }
2798
2841
 
2799
- // 해결된 이슈 자동 정리
2800
- if (cleaned > 0) {
2842
+ // #144 Codex review P2: 두 filter (stale purge + KNOWN_FIXES) 가 각각 원본 entries 로
2843
+ // write 하면 번째 write 가 첫 번째 결과를 되살린다. 단일 통합 필터로 한 번만 저장.
2844
+ if (purged > 0 || cleaned > 0) {
2845
+ const now = Date.now();
2801
2846
  const remaining = entries.filter((e) => {
2847
+ // purge-logs 로 물리 삭제 대상: 7일 초과
2848
+ if (purged > 0 && now - e.ts >= STALE_AGE_MS) return false;
2849
+ // KNOWN_FIXES 해결된 이슈 제거
2802
2850
  const key = `${e.cli}:${e.pattern}`;
2803
2851
  const fixVer = KNOWN_FIXES[key];
2804
- return !(fixVer && semverGte(currentVer, fixVer));
2852
+ if (fixVer && semverGte(currentVer, fixVer)) return false;
2853
+ return true;
2805
2854
  });
2806
2855
  writeFileSync(
2807
2856
  issuesFile,
2808
2857
  remaining.map((e) => JSON.stringify(e)).join("\n") +
2809
2858
  (remaining.length ? "\n" : ""),
2810
2859
  );
2811
- ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
2860
+ if (purged > 0) {
2861
+ ok(`${purged}개 stale 로그 항목 삭제 (7일 초과)`);
2862
+ report.actions.push({
2863
+ name: "purge-stale-logs",
2864
+ status: "applied",
2865
+ count: purged,
2866
+ });
2867
+ }
2868
+ if (cleaned > 0) {
2869
+ ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
2870
+ }
2812
2871
  }
2813
2872
  addDoctorCheck(report, {
2814
2873
  name: "cli-issues",
@@ -3373,6 +3432,56 @@ async function cmdDoctor(options = {}) {
3373
3432
  if (row.actualUrl) info(`actual ${row.actualUrl}`);
3374
3433
  }
3375
3434
 
3435
+ // #144: --fix 모드에서 tfx-hub URL 불일치를 hub status 기준으로 자동 갱신.
3436
+ // Project MCP (.mcp.json) 와 Codex/Claude/Gemini settings 모두 대상.
3437
+ // Codex review P2: fix 성공 시 issues 집계에서 차감해야 doctor 결과가 ok 로 반영됨.
3438
+ let autoFixedMismatches = 0;
3439
+ if (fix && mismatchRows.some((r) => r.name === "tfx-hub")) {
3440
+ try {
3441
+ const hubUrl = mismatchRows.find(
3442
+ (r) => r.name === "tfx-hub",
3443
+ )?.expectedUrl;
3444
+ if (hubUrl) {
3445
+ const { syncHubMcpSettings, syncProjectMcpJson } = await import(
3446
+ "../scripts/sync-hub-mcp-settings.mjs"
3447
+ );
3448
+ const settingsResult = await syncHubMcpSettings({
3449
+ hubUrl,
3450
+ logger: { log() {}, warn() {}, error() {} },
3451
+ });
3452
+ const projectResult = await syncProjectMcpJson({
3453
+ hubUrl,
3454
+ projectRoot: process.cwd(),
3455
+ logger: { log() {}, warn() {}, error() {} },
3456
+ });
3457
+ const totalUpdated =
3458
+ (settingsResult?.updated?.length || 0) +
3459
+ (projectResult?.updated?.length || 0);
3460
+ if (totalUpdated > 0) {
3461
+ ok(`tfx-hub URL ${totalUpdated}개 파일 자동 갱신 (${hubUrl})`);
3462
+ report.actions.push({
3463
+ name: "sync-hub-url",
3464
+ status: "applied",
3465
+ files: [
3466
+ ...(settingsResult?.updated || []),
3467
+ ...(projectResult?.updated || []),
3468
+ ],
3469
+ });
3470
+ // fix 성공 — mismatchRows 중 tfx-hub 엔트리는 해결된 것으로 집계
3471
+ autoFixedMismatches = mismatchRows.filter(
3472
+ (r) => r.name === "tfx-hub",
3473
+ ).length;
3474
+ } else {
3475
+ info("tfx-hub URL 자동 갱신: 대상 파일 없음");
3476
+ }
3477
+ }
3478
+ } catch (e) {
3479
+ warn(
3480
+ `tfx-hub URL 자동 갱신 실패: ${e?.message?.split(/\r?\n/)[0] || e}`,
3481
+ );
3482
+ }
3483
+ }
3484
+
3376
3485
  for (const row of missingFileRows) {
3377
3486
  info(
3378
3487
  `${row.label}: ${row.name} 미배치 (${formatPathForDisplay(row.filePath)})`,
@@ -3395,7 +3504,7 @@ async function cmdDoctor(options = {}) {
3395
3504
  }
3396
3505
 
3397
3506
  issues += invalidConfigs.length;
3398
- issues += mismatchRows.length;
3507
+ issues += Math.max(0, mismatchRows.length - autoFixedMismatches);
3399
3508
  issues += stdioRows.length;
3400
3509
  }
3401
3510
  }
@@ -5508,7 +5617,8 @@ async function main() {
5508
5617
  }
5509
5618
  const fix = cmdArgs.includes("--fix");
5510
5619
  const reset = cmdArgs.includes("--reset");
5511
- await cmdDoctor({ fix, reset, json: JSON_OUTPUT });
5620
+ const purgeLogs = cmdArgs.includes("--purge-logs");
5621
+ await cmdDoctor({ fix, reset, purgeLogs, json: JSON_OUTPUT });
5512
5622
  return;
5513
5623
  }
5514
5624
  case "mcp":
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "mcp-registry-schema",
3
+ "version": 1,
4
+ "description": "MCP 서버 중앙 레지스트리 — 진실의 원천",
5
+ "defaults": {
6
+ "transport": "hub-url",
7
+ "hub_base": "http://127.0.0.1:27888"
8
+ },
9
+ "servers": {
10
+ "tfx-hub": {
11
+ "transport": "hub-url",
12
+ "url": "http://127.0.0.1:27888/mcp",
13
+ "safe": true,
14
+ "targets": ["claude", "gemini", "codex"],
15
+ "description": "triflux Hub MCP 서버"
16
+ }
17
+ },
18
+ "policies": {
19
+ "stdio_action": "replace-with-hub",
20
+ "unknown_server_action": "warn",
21
+ "watched_paths": [
22
+ "~/.gemini/settings.json",
23
+ "~/.codex/config.toml",
24
+ "~/.claude/settings.json",
25
+ "~/.claude/settings.local.json",
26
+ ".mcp.json"
27
+ ]
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.13.1",
3
+ "version": "10.13.3",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,26 +13,77 @@
13
13
  "tfx-doctor-tui": "bin/tfx-doctor-tui.mjs",
14
14
  "tfx-setup-tui": "bin/tfx-setup-tui.mjs"
15
15
  },
16
- "engines": {
17
- "node": ">=18.0.0"
18
- },
19
- "dependencies": {
20
- "@triflux/core": "10.0.1",
21
- "@triflux/remote": "^10.0.0-alpha.1"
22
- },
23
16
  "files": [
24
17
  "bin",
18
+ "tui",
19
+ "hub",
20
+ "config",
25
21
  "skills",
22
+ "!skills/tfx-workspace",
23
+ "!**/failure-reports",
24
+ "scripts",
26
25
  "hooks",
27
26
  "hud",
28
- "scripts",
29
- "hub",
30
27
  "mesh",
31
- "references",
32
- "CLAUDE.md",
28
+ ".claude-plugin",
33
29
  "README.md",
30
+ "README.ko.md",
34
31
  "LICENSE"
35
32
  ],
33
+ "workspaces": [
34
+ "packages/core",
35
+ "packages/remote",
36
+ "packages/triflux"
37
+ ],
38
+ "scripts": {
39
+ "pack": "node scripts/pack.mjs all",
40
+ "pack:core": "node scripts/pack.mjs core",
41
+ "pack:remote": "node scripts/pack.mjs remote",
42
+ "setup": "node scripts/setup.mjs",
43
+ "preinstall": "node scripts/preinstall.mjs",
44
+ "postinstall": "node scripts/setup.mjs",
45
+ "lint": "biome check bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
46
+ "lint:fix": "biome check --write bin config hooks hub hud mesh scripts tests .claude-plugin .github package.json package-lock.json biome.json",
47
+ "health": "npm test && npm run lint",
48
+ "test": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\" \"scripts/__tests__/**/*.test.mjs\"",
49
+ "test:unit": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/unit/**/*.test.mjs",
50
+ "test:integration": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/integration/**/*.test.mjs",
51
+ "test:route-smoke": "node scripts/test-lock.mjs --test scripts/test-tfx-route-no-claude-native.mjs",
52
+ "test:contract": "node scripts/test-lock.mjs --test --test-force-exit --test-concurrency=8 tests/contract/**/*.test.mjs",
53
+ "test:coverage": "node --experimental-test-coverage --test-coverage-lines=60 --test-coverage-functions=60 --test --test-force-exit --test-concurrency=8 \"tests/**/*.test.mjs\"",
54
+ "gen:skill-docs": "node scripts/gen-skill-docs.mjs",
55
+ "gen:skill-manifest": "node scripts/gen-skill-manifest.mjs",
56
+ "release:check-sync": "node scripts/release/check-sync.mjs",
57
+ "release:check-sync:fix": "node scripts/release/check-sync.mjs --fix",
58
+ "release:check-mirror": "node scripts/release/check-packages-mirror.mjs",
59
+ "release:check-mirror:fix": "node scripts/release/check-packages-mirror.mjs --fix",
60
+ "release:bump": "node scripts/release/bump-version.mjs",
61
+ "release:prepare": "node scripts/release/prepare.mjs",
62
+ "release:publish": "node scripts/release/publish.mjs",
63
+ "release:verify": "node scripts/release/verify.mjs"
64
+ },
65
+ "engines": {
66
+ "node": ">=18.0.0"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/tellang/triflux.git"
71
+ },
72
+ "homepage": "https://github.com/tellang/triflux#readme",
73
+ "author": "tellang",
74
+ "license": "MIT",
75
+ "dependencies": {
76
+ "@modelcontextprotocol/sdk": "^1.29.0",
77
+ "better-sqlite3": "^12.6.2",
78
+ "pino": "^10.3.1",
79
+ "pino-pretty": "^13.1.3",
80
+ "systray2": "^2.1.4",
81
+ "zod": "^4.0.0"
82
+ },
83
+ "devDependencies": {
84
+ "@biomejs/biome": "^2.0.0",
85
+ "knip": "^6.3.0"
86
+ },
36
87
  "keywords": [
37
88
  "claude-code",
38
89
  "plugin",
@@ -43,13 +94,5 @@
43
94
  "multi-model",
44
95
  "triflux",
45
96
  "tfx"
46
- ],
47
- "author": "tellang",
48
- "license": "MIT",
49
- "homepage": "https://github.com/tellang/triflux#readme",
50
- "repository": {
51
- "type": "git",
52
- "url": "git+https://github.com/tellang/triflux.git",
53
- "directory": "packages/triflux"
54
- }
97
+ ]
55
98
  }
@@ -0,0 +1,105 @@
1
+ // scripts/__tests__/setup-cleanup-stale-skills.test.mjs
2
+ // #144: cleanupStaleSkills 가 nested directory 를 가진 stale 스킬도 재귀 삭제하는지 확인.
3
+ //
4
+ // 이전 구현은 top-level 파일만 unlinkSync → 하위 폴더 있는 과거 스킬
5
+ // (tfx-deep-*, tfx-codex-swarm 등) 은 제거 실패 → "triflux update 돌려도 13개 그대로" UX bug.
6
+
7
+ import assert from "node:assert/strict";
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ mkdtempSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import path from "node:path";
17
+ import { after, describe, it } from "node:test";
18
+
19
+ const SETUP_MJS_URL = new URL("../setup.mjs", import.meta.url).href;
20
+ const { cleanupStaleSkills } = await import(SETUP_MJS_URL);
21
+
22
+ describe("#144 cleanupStaleSkills — 재귀 삭제", () => {
23
+ const cleanupDirs = [];
24
+ after(() => {
25
+ for (const d of cleanupDirs) rmSync(d, { recursive: true, force: true });
26
+ });
27
+
28
+ function setupFixture() {
29
+ const root = mkdtempSync(path.join(tmpdir(), "tfx-cleanup-"));
30
+ cleanupDirs.push(root);
31
+ const installedDir = path.join(root, "installed");
32
+ const pkgDir = path.join(root, "pkg");
33
+ mkdirSync(installedDir, { recursive: true });
34
+ mkdirSync(pkgDir, { recursive: true });
35
+ // pkg 에는 tfx-auto 만 있음 (나머지는 installed 에서 stale 로 감지)
36
+ mkdirSync(path.join(pkgDir, "tfx-auto"), { recursive: true });
37
+ return { installedDir, pkgDir };
38
+ }
39
+
40
+ it("nested directory 가 있는 stale 스킬도 전부 제거된다 (과거 회귀 bug)", () => {
41
+ const { installedDir, pkgDir } = setupFixture();
42
+ // stale 스킬: top-level 파일 + nested 디렉토리
43
+ const staleSkill = path.join(installedDir, "tfx-deep-review");
44
+ mkdirSync(staleSkill, { recursive: true });
45
+ writeFileSync(path.join(staleSkill, "SKILL.md"), "# deprecated");
46
+ const nested = path.join(staleSkill, "snapshot");
47
+ mkdirSync(nested, { recursive: true });
48
+ writeFileSync(path.join(nested, "data.json"), "{}");
49
+ mkdirSync(path.join(nested, "sub"), { recursive: true });
50
+ writeFileSync(path.join(nested, "sub", "more.txt"), "xxx");
51
+
52
+ // 유지해야 할 스킬 (pkg 에 있음)
53
+ mkdirSync(path.join(installedDir, "tfx-auto"), { recursive: true });
54
+ writeFileSync(path.join(installedDir, "tfx-auto", "SKILL.md"), "# ok");
55
+
56
+ const result = cleanupStaleSkills(installedDir, pkgDir);
57
+ assert.equal(result.count, 1);
58
+ assert.deepEqual(result.removed, ["tfx-deep-review"]);
59
+ assert.equal(existsSync(staleSkill), false, "nested dir 포함 전부 삭제");
60
+ assert.equal(
61
+ existsSync(path.join(installedDir, "tfx-auto")),
62
+ true,
63
+ "pkg 에 있는 스킬은 보존",
64
+ );
65
+ });
66
+
67
+ it("top-level 파일만 있는 stale 스킬도 제거된다 (legacy behavior 회귀 방지)", () => {
68
+ const { installedDir, pkgDir } = setupFixture();
69
+ const staleSkill = path.join(installedDir, "tfx-autoresearch");
70
+ mkdirSync(staleSkill, { recursive: true });
71
+ writeFileSync(path.join(staleSkill, "SKILL.md"), "# deprecated");
72
+ writeFileSync(path.join(staleSkill, "config.json"), "{}");
73
+
74
+ const result = cleanupStaleSkills(installedDir, pkgDir);
75
+ assert.equal(result.count, 1);
76
+ assert.equal(existsSync(staleSkill), false);
77
+ });
78
+
79
+ it("SKILL_ALIASES 에 있는 alias 는 유지된다", () => {
80
+ const { installedDir, pkgDir } = setupFixture();
81
+ // alias (tfx-autopilot) 는 SKILL_ALIASES 에 있으므로 pkgNames 에 자동 포함
82
+ mkdirSync(path.join(installedDir, "tfx-autopilot"), { recursive: true });
83
+ writeFileSync(
84
+ path.join(installedDir, "tfx-autopilot", "SKILL.md"),
85
+ "# alias",
86
+ );
87
+
88
+ const result = cleanupStaleSkills(installedDir, pkgDir);
89
+ assert.equal(result.count, 0);
90
+ assert.equal(existsSync(path.join(installedDir, "tfx-autopilot")), true);
91
+ });
92
+
93
+ it("tfx- 접두사 없는 디렉토리는 건드리지 않음", () => {
94
+ const { installedDir, pkgDir } = setupFixture();
95
+ mkdirSync(path.join(installedDir, "other-skill"), { recursive: true });
96
+ writeFileSync(
97
+ path.join(installedDir, "other-skill", "SKILL.md"),
98
+ "# other",
99
+ );
100
+
101
+ const result = cleanupStaleSkills(installedDir, pkgDir);
102
+ assert.equal(result.count, 0);
103
+ assert.equal(existsSync(path.join(installedDir, "other-skill")), true);
104
+ });
105
+ });
package/scripts/setup.mjs CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  mkdirSync,
14
14
  readdirSync,
15
15
  readFileSync,
16
+ rmSync,
16
17
  unlinkSync,
17
18
  writeFileSync,
18
19
  } from "fs";
@@ -430,17 +431,15 @@ function cleanupStaleSkills(installedDir, pkgDir) {
430
431
  if (pkgNames.has(name)) continue;
431
432
 
432
433
  const skillPath = join(installedDir, name);
434
+ // #144: 재귀 삭제 필요 — 과거 구현은 파일만 unlink 하여 nested 디렉토리가 있는 스킬을
435
+ // 온전히 제거하지 못했다. `tfx-deep-*`, `tfx-codex-swarm` 같은 과거 잔재 디렉토리는
436
+ // workspace/snapshot 같은 하위 폴더를 가지므로 rmSync recursive 가 필수.
433
437
  try {
434
- const entries = readdirSync(skillPath);
435
- for (const f of entries) unlinkSync(join(skillPath, f));
436
- // rmdir only works on empty dirs; ignore errors for nested
437
- try {
438
- readdirSync(skillPath).length === 0 && unlinkSync(skillPath);
439
- } catch {}
438
+ rmSync(skillPath, { recursive: true, force: true });
439
+ removed.push(name);
440
440
  } catch {
441
- /* best effort */
441
+ /* best effort — next setup/update cycle 에서 재시도 */
442
442
  }
443
- removed.push(name);
444
443
  }
445
444
  return { count: removed.length, removed };
446
445
  }
@@ -143,7 +143,7 @@ function resolveDefaultMcpConfig(cwd) {
143
143
  const legacy = resolve(cwd, ".mcp.json");
144
144
  if (existsSync(legacy)) return [legacy];
145
145
  process.stderr.write(
146
- "[tfx-route-worker] warning: no MCP config found, hub unavailable\n",
146
+ "[tfx-route-worker] warning: no project MCP config in cwd — hub status unaffected\n",
147
147
  );
148
148
  return [];
149
149
  }
@@ -1348,6 +1348,32 @@ heartbeat_monitor() {
1348
1348
  echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=draining(${post_exit_checks}/${max_post_exit_checks})" >&2
1349
1349
  fi
1350
1350
  elif [[ "$stall_count" -ge "$stall_threshold" ]]; then
1351
+ # STALL kill (#144/#66 regression guard): stall=threshold+grace 이상 지속 시 SIGTERM→SIGKILL.
1352
+ # 기본 활성화. TFX_STALL_KILL=0 으로 opt-out. grace=30s (기본) 은 SSE/MCP 정상 handshake 여유.
1353
+ local kill_on_stall="${TFX_STALL_KILL:-1}"
1354
+ local kill_grace="${TFX_STALL_KILL_GRACE:-30}"
1355
+ if [[ "$kill_on_stall" -eq 1 && "$stall_count" -ge $((stall_threshold + kill_grace)) ]]; then
1356
+ echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=STALL_KILL stall=${stall_count}s — SIGTERM" >&2
1357
+ kill -TERM "$pid" 2>/dev/null || true
1358
+ local _grace_waited=0
1359
+ while kill -0 "$pid" 2>/dev/null && [[ "$_grace_waited" -lt 5 ]]; do
1360
+ sleep 1
1361
+ _grace_waited=$((_grace_waited + 1))
1362
+ done
1363
+ if kill -0 "$pid" 2>/dev/null; then
1364
+ # Windows/MSYS: POSIX SIGKILL 이 Win32 자식 트리까지 닿지 않는다.
1365
+ # cleanup_workers 와 동일하게 taskkill /T /F 로 트리 종료.
1366
+ case "$(uname -s)" in
1367
+ MINGW*|MSYS*)
1368
+ echo "[tfx-heartbeat] pid=$pid SIGTERM 무시 — taskkill /T /F" >&2
1369
+ MSYS_NO_PATHCONV=1 cmd.exe //c "taskkill /T /F /PID $pid" 2>/dev/null || true ;;
1370
+ *)
1371
+ echo "[tfx-heartbeat] pid=$pid SIGTERM 무시 — SIGKILL 강제" >&2
1372
+ kill -KILL "$pid" 2>/dev/null || true ;;
1373
+ esac
1374
+ fi
1375
+ break
1376
+ fi
1351
1377
  echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=STALL stall=${stall_count}s" >&2
1352
1378
  else
1353
1379
  echo "[tfx-heartbeat] pid=$pid elapsed=${elapsed}s output=${current_size}B status=quiet stall=${stall_count}s" >&2
@@ -1453,12 +1479,51 @@ _codex_config_swap() {
1453
1479
  return 0
1454
1480
  fi
1455
1481
 
1456
- # 백업 생성 (이미 있으면 다른 워커가 swap 중 — 건드리지 않음)
1482
+ # 백업 생성 (이미 있으면 다른 워커가 swap 중 — 단, owner-dead + 백업 안전 복원 시 이어받기)
1457
1483
  if [[ -f "$backup" ]]; then
1458
- echo "[tfx-route] config.toml swap 스킵: 다른 워커가 사용 ($backup)" >&2
1459
- return 0
1484
+ # Owner PID marker (P1 fix): mtime 만으로 stale 판정하면 장시간 정상 실행 워커도 오탐.
1485
+ # $backup.owner 에 생성 워커 PID 기록 → kill -0 로 alive 확인. PID 파일 없거나 죽었으면 stale.
1486
+ # mtime 은 신뢰성 낮아 soft 보조 지표로만 사용 (owner 파일 유실 대비 fallback).
1487
+ local owner_file="${backup}.owner"
1488
+ local owner_alive=false
1489
+ local owner_pid=""
1490
+ if [[ -f "$owner_file" ]]; then
1491
+ owner_pid=$(cat "$owner_file" 2>/dev/null | tr -d '[:space:]')
1492
+ if [[ -n "$owner_pid" ]] && kill -0 "$owner_pid" 2>/dev/null; then
1493
+ owner_alive=true
1494
+ fi
1495
+ fi
1496
+
1497
+ if [[ "$owner_alive" == "true" ]]; then
1498
+ echo "[tfx-route] config.toml swap 스킵: 소유 워커 살아있음 (pid=$owner_pid, $backup)" >&2
1499
+ return 0
1500
+ fi
1501
+
1502
+ # Owner dead or unknown — stale 후보. 다만 backup-loss 방지를 위해 원본 복원 먼저.
1503
+ # P2 fix: `rm -f $backup` 후 현재 config 를 새 backup 으로 cp 하면, 이전 워커가 이미
1504
+ # filter 한 상태에서 crash 했을 때 원본이 영구 소실. 여기서 먼저 restore 를 시도해
1505
+ # backup 이 원본을 담고 있는 한 그것을 살린다.
1506
+ local backup_restore_guard_size
1507
+ backup_restore_guard_size=$(wc -c < "$backup" 2>/dev/null | tr -d ' ') || backup_restore_guard_size=0
1508
+ if [[ "$backup_restore_guard_size" -lt 500 ]]; then
1509
+ # 작은 backup 은 이미 손상된 state. 현재 config 도 필터된 상태일 수 있으므로
1510
+ # 추가 swap 은 상황을 악화시킬 위험. 전체 스킵하고 수동 확인 유도.
1511
+ echo "[tfx-route] stale backup 작음 (size=${backup_restore_guard_size}B, pid=${owner_pid:-?} dead) — swap 스킵, 수동 확인: $backup" >&2
1512
+ return 0
1513
+ fi
1514
+ local stale_tmp="${config}.stale-restore.$$"
1515
+ if cp "$backup" "$stale_tmp" && mv "$stale_tmp" "$config"; then
1516
+ echo "[tfx-route] stale backup 감지 (pid=${owner_pid:-?} dead) — 원본 복원 후 swap 재진행" >&2
1517
+ else
1518
+ echo "[tfx-route] 경고: stale backup 복원 실패, swap 스킵 (수동 확인: $backup)" >&2
1519
+ rm -f "$stale_tmp" 2>/dev/null
1520
+ return 0
1521
+ fi
1522
+ rm -f "$backup" "$owner_file" 2>/dev/null || true
1460
1523
  fi
1461
1524
  cp "$config" "$backup"
1525
+ # Owner marker: 이 워커가 backup 소유자임을 기록. 다음 워커의 stale detection 기준.
1526
+ echo "$$" > "${backup}.owner" 2>/dev/null || true
1462
1527
 
1463
1528
  # awk로 필터링: 비허용 MCP 서버 섹션 제거, 나머지 그대로 유지.
1464
1529
  # keep="" 은 진입 가드에서 return 됐지만 defense-in-depth 유지.
@@ -1526,6 +1591,7 @@ _codex_config_swap() {
1526
1591
  if ! rm -f "$backup"; then
1527
1592
  echo "[tfx-route] 경고: backup 삭제 실패: $backup (수동 정리 필요)" >&2
1528
1593
  fi
1594
+ rm -f "${backup}.owner" 2>/dev/null || true
1529
1595
  echo "[tfx-route] config.toml 복원 완료" >&2
1530
1596
  fi
1531
1597
  }