triflux 10.53.0 → 10.55.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 (127) hide show
  1. package/README.ko.md +10 -16
  2. package/README.md +13 -19
  3. package/adapters/codex/skills/tfx-live/SKILL.md +3 -3
  4. package/bin/tfx-live.mjs +10 -6
  5. package/bin/triflux.mjs +174 -127
  6. package/config/mcp-registry.json +1 -1
  7. package/hooks/codex-session-hook.mjs +1 -17
  8. package/hub/cli-adapter-base.mjs +3 -424
  9. package/hub/lib/hosts-compat.mjs +18 -64
  10. package/hub/lib/private-tmp.mjs +35 -0
  11. package/hub/lib/process-utils.mjs +19 -220
  12. package/hub/lib/prompt-tmp.mjs +6 -24
  13. package/hub/lib/spawn-trace.mjs +112 -5
  14. package/hub/lib/timeout-defaults.mjs +34 -6
  15. package/hub/lib/worker-lifecycle.mjs +3 -0
  16. package/hub/platform.mjs +3 -1
  17. package/hub/team/backend.mjs +10 -3
  18. package/hub/team/cli/commands/start/index.mjs +0 -2
  19. package/hub/team/codex-queue.mjs +49 -2
  20. package/hub/team/handoff.mjs +1 -1
  21. package/hub/team/headless.mjs +41 -57
  22. package/hub/team/intervention.mjs +6 -4
  23. package/hub/team/nativeProxy.mjs +2 -48
  24. package/hub/team/pane.mjs +31 -18
  25. package/hub/team/psmux.mjs +36 -20
  26. package/hub/team/remote-session.mjs +66 -56
  27. package/hub/team/retry-state-machine.mjs +1 -1
  28. package/hub/team/session.mjs +80 -59
  29. package/hub/team/terminal-opener.mjs +5 -19
  30. package/hub/team/tui-viewer.mjs +2 -2
  31. package/hub/team/tui.mjs +0 -6
  32. package/hub/workers/factory.mjs +1 -10
  33. package/hub/workers/interface.mjs +0 -7
  34. package/hud/constants.mjs +0 -3
  35. package/hud/providers/claude.mjs +35 -15
  36. package/hud/providers/codex.mjs +169 -42
  37. package/hud/utils.mjs +0 -23
  38. package/package.json +4 -11
  39. package/scripts/check-codex-config-stable.mjs +2 -11
  40. package/scripts/config-audit.mjs +0 -2
  41. package/scripts/doctor-diagnose.mjs +25 -20
  42. package/scripts/ensure-codex-hooks.mjs +70 -24
  43. package/scripts/lib/backup-rotation.mjs +60 -0
  44. package/scripts/lib/codex-profile-config.mjs +20 -7
  45. package/scripts/lib/codex-recovery.sh +2 -3
  46. package/scripts/lib/install-blob-history.json +3196 -0
  47. package/scripts/lib/install-retire.mjs +203 -0
  48. package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
  49. package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
  50. package/scripts/lib/machine-profile.mjs +2 -2
  51. package/scripts/lib/mcp-filter.mjs +1 -42
  52. package/scripts/lib/mcp-guard-engine.mjs +198 -19
  53. package/scripts/lib/mcp-manifest.mjs +0 -40
  54. package/scripts/lib/process-utils.mjs +19 -0
  55. package/scripts/lib/stable-node.mjs +35 -0
  56. package/scripts/lib/test-env.mjs +20 -0
  57. package/scripts/lib/tmr-install.mjs +183 -0
  58. package/scripts/lint-private-info.mjs +137 -0
  59. package/scripts/mcp-check.mjs +24 -18
  60. package/scripts/pack.mjs +29 -42
  61. package/scripts/release/check-packages-mirror.mjs +77 -125
  62. package/scripts/release/lib.mjs +1 -1
  63. package/scripts/release/prepare.mjs +2 -0
  64. package/scripts/release/resolve-auto-release.mjs +11 -0
  65. package/scripts/release/version-manifest.json +7 -1
  66. package/scripts/remote-spawn.mjs +226 -116
  67. package/scripts/session-stale-cleanup.mjs +124 -75
  68. package/scripts/setup.mjs +414 -377
  69. package/scripts/test-lock.mjs +62 -8
  70. package/scripts/tfx-route-post.mjs +30 -4
  71. package/scripts/tfx-route.sh +73 -25
  72. package/scripts/tmp-cleanup.mjs +16 -3
  73. package/skills/tfx-auto/SKILL.md +12 -5
  74. package/skills/tfx-lead/SKILL.md +6 -6
  75. package/skills/tfx-live/SKILL.md +3 -3
  76. package/skills/tfx-remote/SKILL.md +2 -2
  77. package/skills/tfx-research/SKILL.md +10 -7
  78. package/skills/tfx-review/SKILL.md +14 -3
  79. package/skills/tfx-ship/SKILL.md +1 -1
  80. package/CLAUDE.md +0 -194
  81. package/cto/brief.mjs +0 -73
  82. package/cto/collect.mjs +0 -490
  83. package/cto/current.schema.json +0 -148
  84. package/cto/events.mjs +0 -208
  85. package/cto/hygiene.mjs +0 -320
  86. package/cto/index.mjs +0 -46
  87. package/cto/lake-root.mjs +0 -127
  88. package/cto/status.mjs +0 -124
  89. package/docs/assets/demo-multi.gif +0 -0
  90. package/hooks/agy-session-hook.mjs +0 -155
  91. package/hooks/claude-cwd-projection-refresh.mjs +0 -11
  92. package/hooks/hook-orchestrator.mjs +0 -11
  93. package/hooks/lib/resolve-root.mjs +0 -62
  94. package/hub/codex-adapter.mjs +0 -193
  95. package/hub/codex-preflight.mjs +0 -219
  96. package/hub/lib/cto-env.mjs +0 -70
  97. package/hub/team/notify.mjs +0 -385
  98. package/hub/team/process-cleanup.mjs +0 -386
  99. package/hub/team/routing.mjs +0 -249
  100. package/hub/team/staleState.mjs +0 -403
  101. package/hub/team/worker-signal.mjs +0 -263
  102. package/hub/team/worker-signal.types.d.ts +0 -52
  103. package/hub/workers/codex-mcp.mjs +0 -853
  104. package/references/codex-plugin-cc-analysis.md +0 -701
  105. package/references/codex-plugin-cc-code-patterns.md +0 -468
  106. package/scripts/__tests__/ensure-codex-hooks.test.mjs +0 -183
  107. package/scripts/__tests__/lint-skills.test.mjs +0 -169
  108. package/scripts/__tests__/mcp-guard-engine-http-headers.test.mjs +0 -223
  109. package/scripts/__tests__/mcp-guard-engine-policy-sync.test.mjs +0 -185
  110. package/scripts/__tests__/mcp-guard-engine-proactive-sync.test.mjs +0 -206
  111. package/scripts/__tests__/mcp-guard-engine-stdio-sync.test.mjs +0 -186
  112. package/scripts/__tests__/mcp-guard-engine-sync-http-headers.test.mjs +0 -270
  113. package/scripts/__tests__/mcp-guard-engine.test.mjs +0 -376
  114. package/scripts/__tests__/release-governance.test.mjs +0 -732
  115. package/scripts/__tests__/release-publish-ancestor.test.mjs +0 -67
  116. package/scripts/__tests__/release-verify-wait.test.mjs +0 -169
  117. package/scripts/__tests__/remote-spawn-transfer.test.mjs +0 -128
  118. package/scripts/__tests__/remote-spawn.test.mjs +0 -98
  119. package/scripts/__tests__/resolve-auto-release.test.mjs +0 -216
  120. package/scripts/__tests__/setup-cleanup-stale-skills.test.mjs +0 -86
  121. package/scripts/__tests__/spawn-trace.test.mjs +0 -269
  122. package/scripts/__tests__/tfx-doctor-diagnose.test.mjs +0 -58
  123. package/scripts/claudemd-sync.mjs +0 -157
  124. package/scripts/ensure-agy-hooks.mjs +0 -163
  125. package/scripts/lib/remote-spawn-transfer.mjs +0 -226
  126. package/scripts/lib/session-presence.mjs +0 -151
  127. package/scripts/mcp-cleanup.ps1 +0 -26
package/bin/triflux.mjs CHANGED
@@ -36,11 +36,15 @@ import {
36
36
  inspectMacTimeoutDependency,
37
37
  } from "../scripts/lib/doctor-env-checks.mjs";
38
38
  import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
39
- import { cleanupLegacyHooks } from "../scripts/lib/legacy-hook-cleanup.mjs";
39
+ import {
40
+ cleanupAgyHooks,
41
+ cleanupLegacyHooks,
42
+ } from "../scripts/lib/legacy-hook-cleanup.mjs";
40
43
  import {
41
44
  cleanupLegacyMcp,
42
45
  cleanupTfxHub,
43
46
  findProjectHubEntries,
47
+ pinRegistryMcpPackages,
44
48
  } from "../scripts/lib/legacy-mcp-cleanup.mjs";
45
49
  import {
46
50
  addRegistryServer,
@@ -58,19 +62,21 @@ import {
58
62
  formatPsmuxUpdateGuidance,
59
63
  probePsmuxSupport,
60
64
  } from "../scripts/lib/psmux-info.mjs";
65
+ import { writesRealHomeInTest } from "../scripts/lib/test-env.mjs";
61
66
  import {
62
67
  applyStatusLine,
68
+ cleanupStaleSkills,
63
69
  ensureCodexProfiles,
64
- ensureTrifluxMods,
65
70
  getVersion,
66
- isLocalDevSkillDir,
71
+ inspectTrifluxMods,
67
72
  isSkillSupportedOnPlatform,
68
73
  LEGACY_CODEX_MODELS,
69
74
  listInlineProfileNames,
75
+ MODS_UPDATE_COMMAND,
70
76
  persistSettings,
71
- REMOVED_SKILL_NAMES,
72
77
  REQUIRED_CODEX_PROFILES,
73
- SKILL_ALIASES,
78
+ retireOldInstallFiles,
79
+ runConsentSteps,
74
80
  SYNC_MAP,
75
81
  syncSkills,
76
82
  } from "../scripts/setup.mjs";
@@ -265,16 +271,6 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
265
271
  },
266
272
  },
267
273
  },
268
- cto: {
269
- usage: "tfx cto <collect|status|hygiene> [options]",
270
- description: "repo-local authority layer console",
271
- subcommands: {
272
- collect: "refresh .triflux/lake/current.json from authority sources",
273
- status: "print the current authority summary",
274
- hygiene:
275
- "project CTO hygiene counts and actionable dry-run rows (--dry-run)",
276
- },
277
- },
278
274
  multi: {
279
275
  usage:
280
276
  "tfx multi [--dashboard-layout lite|single|split-2col|split-3col|auto] <subcommand|task>",
@@ -311,15 +307,10 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
311
307
  },
312
308
  },
313
309
  update: {
314
- usage: "tfx update [--dev]",
310
+ usage: "tfx update",
315
311
  description:
316
312
  "설치 방식(plugin/npm/git)을 감지해 triflux를 업데이트하고 setup/cache를 재동기화",
317
313
  options: [
318
- {
319
- name: "--dev / dev",
320
- type: "boolean",
321
- description: "npm 설치 모드에서 dev tag로 업데이트",
322
- },
323
314
  {
324
315
  name: "--help",
325
316
  type: "boolean",
@@ -550,12 +541,6 @@ function whichInShell(cmd, shell) {
550
541
  }
551
542
  }
552
543
 
553
- function isDevUpdateRequested(argv = process.argv) {
554
- return (
555
- argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev")
556
- );
557
- }
558
-
559
544
  function checkShellAvailable(shell) {
560
545
  const cmds = {
561
546
  bash: "bash --version",
@@ -703,7 +688,7 @@ function sumMemoryEstimateMb(panes, { execFile = execFileSync } = {}) {
703
688
  return seen ? total : null;
704
689
  }
705
690
 
706
- export function inspectDetachedTmuxSessions({
691
+ function inspectDetachedTmuxSessions({
707
692
  prefix = DEFAULT_TMUX_CLEANUP_PREFIX,
708
693
  ageMin = DEFAULT_TMUX_CLEANUP_AGE_MIN,
709
694
  platform = process.platform,
@@ -782,7 +767,7 @@ export function inspectDetachedTmuxSessions({
782
767
  };
783
768
  }
784
769
 
785
- export async function cleanupDetachedTmuxSessions({
770
+ async function cleanupDetachedTmuxSessions({
786
771
  sessions,
787
772
  dryRun = true,
788
773
  apply = false,
@@ -1129,12 +1114,6 @@ function listSkillSyncActions() {
1129
1114
  if (!isSkillSupportedOnPlatform(join(skillsSrc, name))) continue;
1130
1115
  actions.push(describeSyncAction(src, dst, `skill:${name}`));
1131
1116
  }
1132
- for (const { alias, source } of SKILL_ALIASES) {
1133
- const src = join(skillsSrc, source, "SKILL.md");
1134
- const dst = join(CLAUDE_DIR, "skills", alias, "SKILL.md");
1135
- if (!existsSync(src)) continue;
1136
- actions.push(describeSyncAction(src, dst, `skill-alias:${alias}`));
1137
- }
1138
1117
  return actions;
1139
1118
  }
1140
1119
 
@@ -1242,7 +1221,7 @@ function reportSkillSync() {
1242
1221
  ok(`스킬: ${result.total}개 확인, 파일 ${result.changed}개 반영`);
1243
1222
  }
1244
1223
 
1245
- function cmdSetup(options = {}) {
1224
+ async function cmdSetup(options = {}) {
1246
1225
  const {
1247
1226
  dryRun = false,
1248
1227
  fromUpdate = false,
@@ -1253,6 +1232,10 @@ function cmdSetup(options = {}) {
1253
1232
  printJson(buildSetupDryRunPlan());
1254
1233
  return;
1255
1234
  }
1235
+ if (writesRealHomeInTest()) {
1236
+ info("setup: skip (테스트가 홈을 격리하지 않음)");
1237
+ return;
1238
+ }
1256
1239
 
1257
1240
  const cleanup = cleanupLegacyHooks({
1258
1241
  settingsPath: join(CLAUDE_DIR, "settings.json"),
@@ -1264,13 +1247,24 @@ function cmdSetup(options = {}) {
1264
1247
  fix: `${join(CLAUDE_DIR, "settings.json")}의 JSON 문법과 쓰기 권한을 확인하세요.`,
1265
1248
  });
1266
1249
  }
1250
+ // agy 는 hooks.json 의 훅을 실행하므로 지워진 훅 스크립트를 가리키는 항목을 남기지 않는다.
1251
+ const agyCleanup = cleanupAgyHooks();
1252
+ if (agyCleanup.changed) ok("agy 옛 triflux-session 훅 정리됨");
1253
+ else if (!agyCleanup.ok) warn(`agy 옛 훅 정리 실패: ${agyCleanup.error}`);
1267
1254
  // 이주가 막혀도 setup 은 계속한다. 남은 항목은 경고로 알린다.
1268
- for (const warning of cleanupLegacyMcp().warnings) warn(warning);
1255
+ const mcpBackups = new Map();
1256
+ for (const warning of cleanupLegacyMcp({ backups: mcpBackups }).warnings)
1257
+ warn(warning);
1269
1258
  const hubCleanup = cleanupTfxHub({
1270
1259
  pluginRoot: existsSync(join(PKG_ROOT, ".git")) ? undefined : PKG_ROOT,
1260
+ log: info,
1261
+ backups: mcpBackups,
1271
1262
  });
1272
1263
  for (const warning of hubCleanup.warnings) warn(warning);
1273
1264
  if (hubCleanup.changed) ok("제거된 허브의 설정과 실행 흔적 정리");
1265
+ const mcpPins = pinRegistryMcpPackages({ backups: mcpBackups });
1266
+ for (const warning of mcpPins.warnings) warn(warning);
1267
+ if (mcpPins.pinned) ok(`MCP 패키지 고정 버전 반영: ${mcpPins.pinned}개 항목`);
1274
1268
  if (fromUpdate) refreshSetupCaches();
1275
1269
 
1276
1270
  console.log(`\n${BOLD}triflux setup${RESET}\n`);
@@ -1278,34 +1272,8 @@ function cmdSetup(options = {}) {
1278
1272
  for (const target of SYNC_MAP) {
1279
1273
  syncFile(target.src, target.dst, target.label);
1280
1274
  }
1275
+ retireOldInstallFiles(info);
1281
1276
  reportSkillSync();
1282
- ensureTrifluxMods({ install: mods, log: console.log, warn });
1283
-
1284
- // ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
1285
- if (process.platform === "win32" && which("psmux")) {
1286
- try {
1287
- const shellOut = execSync("psmux show-options -g default-shell 2>NUL", {
1288
- encoding: "utf8",
1289
- timeout: 3000,
1290
- }).trim();
1291
- if (!/powershell|pwsh/i.test(shellOut)) {
1292
- const pwsh = which("pwsh")
1293
- ? "pwsh"
1294
- : which("powershell.exe")
1295
- ? "powershell.exe"
1296
- : "";
1297
- if (pwsh) {
1298
- execSync(`psmux set-option -g default-shell "${pwsh}"`, {
1299
- timeout: 3000,
1300
- stdio: "pipe",
1301
- });
1302
- ok(`psmux 기본 셸 → ${pwsh}`);
1303
- }
1304
- }
1305
- } catch {
1306
- /* psmux 서버 미실행 — 무시 */
1307
- }
1308
- }
1309
1277
 
1310
1278
  // ── 결과 추적 ──
1311
1279
  const summary = [];
@@ -1402,6 +1370,35 @@ function cmdSetup(options = {}) {
1402
1370
  });
1403
1371
  }
1404
1372
 
1373
+ // Codex 프로필 정리가 끝난 뒤에 훅을 다룬다.
1374
+ await runConsentSteps({ modsInstall: mods, log: info, warn });
1375
+
1376
+ // ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
1377
+ if (process.platform === "win32" && which("psmux")) {
1378
+ try {
1379
+ const shellOut = execSync("psmux show-options -g default-shell 2>NUL", {
1380
+ encoding: "utf8",
1381
+ timeout: 3000,
1382
+ }).trim();
1383
+ if (!/powershell|pwsh/i.test(shellOut)) {
1384
+ const pwsh = which("pwsh")
1385
+ ? "pwsh"
1386
+ : which("powershell.exe")
1387
+ ? "powershell.exe"
1388
+ : "";
1389
+ if (pwsh) {
1390
+ execSync(`psmux set-option -g default-shell "${pwsh}"`, {
1391
+ timeout: 3000,
1392
+ stdio: "pipe",
1393
+ });
1394
+ ok(`psmux 기본 셸 → ${pwsh}`);
1395
+ }
1396
+ }
1397
+ } catch {
1398
+ /* psmux 서버 미실행이면 무시 */
1399
+ }
1400
+ }
1401
+
1405
1402
  // CLI 존재 확인
1406
1403
  const cliChecks = [
1407
1404
  { name: "codex", install: "npm i -g @openai/codex" },
@@ -1510,6 +1507,16 @@ function statusBadge(status) {
1510
1507
  }
1511
1508
  }
1512
1509
 
1510
+ // URL 은 같고 헤더만 다르면 그렇게 보여 준다. URL 만 찍으면 같은 값끼리 불일치로 보인다.
1511
+ function isHeaderOnlyMismatch(row) {
1512
+ return Boolean(row.expectedUrl) && row.expectedUrl === row.actualUrl;
1513
+ }
1514
+
1515
+ // stdio 항목은 명령이 같고 인자만 다를 수 있어 인자까지 보여 준다.
1516
+ function mcpTarget(url, command, args = []) {
1517
+ return url || [command, ...args].filter(Boolean).join(" ");
1518
+ }
1519
+
1513
1520
  function buildMcpStatusRows(statusInfo) {
1514
1521
  const registryRows = statusInfo.rows
1515
1522
  .filter((row) => row.type === "registry")
@@ -1520,7 +1527,9 @@ function buildMcpStatusRows(statusInfo) {
1520
1527
  else if (row.status === "missing") detail = "registry only";
1521
1528
  else if (row.status === "missing-file") detail = "config missing";
1522
1529
  else if (row.status === "mismatch")
1523
- detail = `expected ${row.expectedUrl || row.expectedCommand}`;
1530
+ detail = isHeaderOnlyMismatch(row)
1531
+ ? `header ${row.headerStatus}: ${row.expectedHeaderNames.join(", ")}`
1532
+ : `expected ${mcpTarget(row.expectedUrl, row.expectedCommand, row.expectedArgs)}`;
1524
1533
  else if (row.status === "invalid-config") detail = "parse error";
1525
1534
  else if (row.status === "skipped") detail = row.message || "skipped";
1526
1535
  else if (row.status === "stdio") detail = "configured as stdio";
@@ -1855,6 +1864,9 @@ async function cmdDoctor(options = {}) {
1855
1864
  status: invalidCount > 0 ? "issues" : "ok",
1856
1865
  actions: mcpSync.actions,
1857
1866
  });
1867
+ for (const action of mcpSync.actions)
1868
+ if (action.status === "warning" && action.message)
1869
+ warn(`${action.label}: ${action.message}`);
1858
1870
  if (updatedCount > 0)
1859
1871
  ok(`MCP registry 동기화: ${updatedCount}개 설정 반영됨`);
1860
1872
  else info("MCP registry: 이미 최신 상태");
@@ -1959,6 +1971,21 @@ async function cmdDoctor(options = {}) {
1959
1971
  warn(`미설치 ${GRAY}(선택사항)${RESET}`);
1960
1972
  }
1961
1973
 
1974
+ // mods 가 없으면 effort 없이 부른 서브에이전트가 리드 effort 를 물려받는다.
1975
+ section("Claude mods");
1976
+ const mods = inspectTrifluxMods({ pkgVersion: PKG.version });
1977
+ addDoctorCheck(report, { name: "triflux-mods", ...mods });
1978
+ if (mods.status === "current")
1979
+ ok(`설치됨 ${DIM}v${mods.installedVersion}${RESET}`);
1980
+ else if (mods.status === "missing") {
1981
+ warn("미설치");
1982
+ info("수정: tfx setup --mods");
1983
+ } else if (mods.status === "outdated") {
1984
+ warn(`버전 차이: 설치 ${mods.installedVersion}, 패키지 ${PKG.version}`);
1985
+ info(`수정: ${MODS_UPDATE_COMMAND}`);
1986
+ } else if (mods.status === "failed") warn(`확인 실패: ${mods.error}`);
1987
+ else info(`건너뜀 (${mods.reason})`);
1988
+
1962
1989
  // Codex CLI
1963
1990
  section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
1964
1991
  const codexCli = checkCliCrossShell(
@@ -2260,38 +2287,29 @@ async function cmdDoctor(options = {}) {
2260
2287
  });
2261
2288
  }
2262
2289
 
2263
- // Stale 스킬 체크
2264
- const staleSkills = [];
2265
- const userSkillsDir = join(CLAUDE_DIR, "skills");
2266
- if (existsSync(userSkillsDir)) {
2267
- const pkgSkillsDir = join(PKG_ROOT, "skills");
2268
- const pkgSkills = new Set();
2269
- if (existsSync(pkgSkillsDir)) {
2270
- for (const n of readdirSync(pkgSkillsDir)) {
2271
- if (isSkillSupportedOnPlatform(join(pkgSkillsDir, n)))
2272
- pkgSkills.add(n);
2273
- }
2274
- }
2275
- for (const { alias } of SKILL_ALIASES) pkgSkills.add(alias);
2276
-
2277
- for (const n of readdirSync(userSkillsDir)) {
2278
- if (
2279
- !REMOVED_SKILL_NAMES.includes(n) &&
2280
- !existsSync(join(pkgSkillsDir, n))
2281
- )
2282
- continue;
2283
- if (isLocalDevSkillDir(join(userSkillsDir, n))) continue;
2284
- if (!pkgSkills.has(n)) staleSkills.push(n);
2285
- }
2286
- }
2290
+ // Stale 스킬 체크: setup 과 같은 판정으로 지울 관리 사본과 보존할 사용자 사본을 나눈다.
2291
+ const { removed: staleSkills, preserved: keptSkills } = cleanupStaleSkills(
2292
+ join(CLAUDE_DIR, "skills"),
2293
+ join(PKG_ROOT, "skills"),
2294
+ { dryRun: true },
2295
+ );
2287
2296
  if (staleSkills.length > 0) {
2288
2297
  warn(`구형 스킬 ${staleSkills.length}개 감지: ${staleSkills.join(", ")}`);
2289
2298
  info("관리 사본 정리: tfx setup 또는 tfx update");
2299
+ }
2300
+ if (keptSkills.length > 0) {
2301
+ warn(
2302
+ `구형 스킬 사용자 사본 ${keptSkills.length}개: ${keptSkills.join(", ")}`,
2303
+ );
2304
+ info("setup 은 고친 사본을 보존한다. 필요 없으면 직접 지우세요.");
2305
+ }
2306
+ if (staleSkills.length + keptSkills.length > 0) {
2290
2307
  addDoctorCheck(report, {
2291
2308
  name: "stale-skills",
2292
2309
  status: "issues",
2293
2310
  skills: staleSkills,
2294
- fix: "tfx setup",
2311
+ preserved: keptSkills,
2312
+ ...(staleSkills.length > 0 ? { fix: "tfx setup" } : {}),
2295
2313
  });
2296
2314
  issues++;
2297
2315
  } else {
@@ -2357,7 +2375,6 @@ async function cmdDoctor(options = {}) {
2357
2375
  // --fix 모드: npm install 실행 (Windows 호환 shell: true)
2358
2376
  info(`npm install 실행 중 (${PKG_ROOT})...`);
2359
2377
  try {
2360
- const { execFileSync } = await import("node:child_process");
2361
2378
  execFileSync("npm", ["install", "--no-audit", "--no-fund"], {
2362
2379
  cwd: PKG_ROOT,
2363
2380
  stdio: "inherit",
@@ -2728,10 +2745,7 @@ async function cmdDoctor(options = {}) {
2728
2745
  }
2729
2746
  } else {
2730
2747
  // --fix 없이는 개수만 보고
2731
- const { execSync: execSyncDoctor } = await import(
2732
- "node:child_process"
2733
- );
2734
- const countStr = execSyncDoctor(
2748
+ const countStr = execSync(
2735
2749
  `powershell -NoProfile -WindowStyle Hidden -Command "(Get-Process node -ErrorAction SilentlyContinue).Count"`,
2736
2750
  { encoding: "utf8", timeout: 5000 },
2737
2751
  ).trim();
@@ -2875,9 +2889,22 @@ async function cmdDoctor(options = {}) {
2875
2889
  }
2876
2890
 
2877
2891
  for (const row of mismatchRows) {
2878
- warn(`${row.label}: ${row.name} URL 불일치`);
2879
- info(`expected ${row.expectedUrl}`);
2880
- if (row.actualUrl) info(`actual ${row.actualUrl}`);
2892
+ if (isHeaderOnlyMismatch(row)) {
2893
+ warn(`${row.label}: ${row.name} 헤더 불일치 (${row.headerStatus})`);
2894
+ info(`expected ${row.expectedHeaderNames.join(", ") || "없음"}`);
2895
+ info(`actual ${row.actualHeaderNames.join(", ") || "없음"}`);
2896
+ continue;
2897
+ }
2898
+ warn(`${row.label}: ${row.name} 설정 불일치`);
2899
+ info(
2900
+ `expected ${mcpTarget(row.expectedUrl, row.expectedCommand, row.expectedArgs)}`,
2901
+ );
2902
+ const actualTarget = mcpTarget(
2903
+ row.actualUrl,
2904
+ row.actualCommand,
2905
+ row.actualArgs,
2906
+ );
2907
+ if (actualTarget) info(`actual ${actualTarget}`);
2881
2908
  }
2882
2909
 
2883
2910
  for (const row of missingFileRows) {
@@ -3070,6 +3097,44 @@ async function cmdDoctor(options = {}) {
3070
3097
  : "남은 triflux command hook 없음",
3071
3098
  );
3072
3099
  }
3100
+
3101
+ // 옛 setup 이 agy hooks.json 에 넣은 triflux-session 훅. 스크립트가 사라져 실행되면 실패한다.
3102
+ // 옛 설치기 모양과 같은 것만 지우고, 다른 모양은 사용자가 정리하도록 경고만 한다.
3103
+ const agyHooks = cleanupAgyHooks({ dryRun: !fix });
3104
+ const agyRemaining = agyHooks.ok && fix ? 0 : agyHooks.removed;
3105
+ const agyLeftover = agyHooks.leftover;
3106
+ addDoctorCheck(report, {
3107
+ name: "legacy-agy-hooks",
3108
+ status: !agyHooks.ok
3109
+ ? "error"
3110
+ : agyRemaining + agyLeftover.length > 0
3111
+ ? "issues"
3112
+ : "ok",
3113
+ remaining: agyRemaining,
3114
+ leftover: agyLeftover,
3115
+ path: agyHooks.hooksPath,
3116
+ ...(agyHooks.ok ? {} : { error: agyHooks.error }),
3117
+ ...(agyRemaining > 0 ? { fix: "tfx doctor --fix" } : {}),
3118
+ });
3119
+ if (!agyHooks.ok) {
3120
+ fail(`agy 훅 점검 실패: ${agyHooks.error}`);
3121
+ issues++;
3122
+ } else {
3123
+ if (agyRemaining > 0) {
3124
+ warn(
3125
+ "agy hooks.json 에 옛 triflux-session 훅이 남음: tfx doctor --fix",
3126
+ );
3127
+ issues += agyRemaining;
3128
+ } else if (fix && agyHooks.changed) {
3129
+ ok("agy 옛 triflux-session 훅 정리됨");
3130
+ }
3131
+ if (agyLeftover.length > 0) {
3132
+ warn(
3133
+ `agy 훅 ${agyLeftover.join(", ")} 이 지워진 agy-session-hook.mjs 를 가리킴. 직접 정리: ${agyHooks.hooksPath}`,
3134
+ );
3135
+ issues += agyLeftover.length;
3136
+ }
3137
+ }
3073
3138
  if (fix) {
3074
3139
  try {
3075
3140
  execFileSync(
@@ -3159,9 +3224,7 @@ async function cmdUpdate(args = []) {
3159
3224
  return;
3160
3225
  }
3161
3226
 
3162
- const isDev = isDevUpdateRequested(args);
3163
- const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
3164
- console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
3227
+ console.log(`\n${BOLD}triflux update${RESET}\n`);
3165
3228
 
3166
3229
  // 1. 설치 방식 감지
3167
3230
  const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
@@ -3259,9 +3322,7 @@ async function cmdUpdate(args = []) {
3259
3322
  break;
3260
3323
  }
3261
3324
  case "npm-global": {
3262
- const npmCmd = isDev
3263
- ? "npm install -g triflux@dev"
3264
- : "npm install -g triflux@latest";
3325
+ const npmCmd = "npm install -g triflux@latest";
3265
3326
  let result;
3266
3327
  try {
3267
3328
  result = execSync(npmCmd, {
@@ -3284,14 +3345,12 @@ async function cmdUpdate(args = []) {
3284
3345
  .trim()
3285
3346
  .split(/\r?\n/)[0];
3286
3347
  }
3287
- ok(`${npmCmd} — ${result || "완료"}`);
3348
+ ok(`${npmCmd}: ${result || "완료"}`);
3288
3349
  updated = true;
3289
3350
  break;
3290
3351
  }
3291
3352
  case "npm-local": {
3292
- const npmLocalCmd = isDev
3293
- ? "npm install triflux@dev"
3294
- : "npm update triflux";
3353
+ const npmLocalCmd = "npm update triflux";
3295
3354
  const result = execSync(npmLocalCmd, {
3296
3355
  encoding: "utf8",
3297
3356
  timeout: 60000,
@@ -3301,9 +3360,7 @@ async function cmdUpdate(args = []) {
3301
3360
  })
3302
3361
  .trim()
3303
3362
  .split(/\r?\n/)[0];
3304
- ok(
3305
- `${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`,
3306
- );
3363
+ ok(`${npmLocalCmd}: ${result || "완료"}`);
3307
3364
  updated = true;
3308
3365
  break;
3309
3366
  }
@@ -3572,7 +3629,10 @@ function cmdMcp(args = [], options = {}) {
3572
3629
  }
3573
3630
  const label = `${action.label} ${DIM}(${formatPathForDisplay(action.filePath)})${RESET}`;
3574
3631
  if (action.status === "updated") ok(`${label} → updated`);
3575
- else if (action.status === "warning") warn(`${label} → warning`);
3632
+ else if (action.status === "warning")
3633
+ warn(
3634
+ `${label} → warning${action.message ? `: ${action.message}` : ""}`,
3635
+ );
3576
3636
  else if (action.status === "invalid-config")
3577
3637
  fail(`${label} → invalid-config`);
3578
3638
  else info(`${stripAnsi(label)} → ${action.status}`);
@@ -3702,10 +3762,8 @@ function cmdHelp() {
3702
3762
  ${DIM} --help${RESET} ${GRAY}--audit, --diagnose 등 전체 옵션${RESET}
3703
3763
  ${WHITE_BRIGHT}tfx mcp${RESET} ${GRAY}MCP registry 관리 (list/sync/add/remove)${RESET}
3704
3764
  ${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
3705
- ${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
3706
3765
  ${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
3707
3766
  ${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux)${RESET}
3708
- ${WHITE_BRIGHT}tfx cto${RESET} ${GRAY}저장소 스냅숏 수집과 조회 (collect/status/hygiene)${RESET}
3709
3767
  ${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
3710
3768
  ${WHITE_BRIGHT}tfx-live${RESET} ${GRAY}Claude·Codex 라이브 세션 (tfx-live --help)${RESET}
3711
3769
 
@@ -3761,7 +3819,7 @@ async function main() {
3761
3819
  printCommandHelp("setup");
3762
3820
  return;
3763
3821
  }
3764
- cmdSetup({
3822
+ await cmdSetup({
3765
3823
  dryRun: cmdArgs.includes("--dry-run"),
3766
3824
  fromUpdate: cmdArgs.includes("--from-update"),
3767
3825
  mods: cmdArgs.includes("--mods"),
@@ -3789,6 +3847,7 @@ async function main() {
3789
3847
  } catch (e) {
3790
3848
  process.stdout.write(e.stdout || "");
3791
3849
  if (e.stderr) process.stderr.write(e.stderr);
3850
+ process.exitCode = Number.isInteger(e.status) ? e.status : EXIT_ERROR;
3792
3851
  }
3793
3852
  return;
3794
3853
  }
@@ -3857,18 +3916,6 @@ async function main() {
3857
3916
  }
3858
3917
  cmdList({ json: JSON_OUTPUT });
3859
3918
  return;
3860
- case "cto": {
3861
- if (cmdArgs.some(isHelpArg)) {
3862
- printCommandHelp("cto");
3863
- return;
3864
- }
3865
- const { pathToFileURL } = await import("node:url");
3866
- const { cmdCto } = await import(
3867
- pathToFileURL(join(PKG_ROOT, "cto", "index.mjs")).href
3868
- );
3869
- await cmdCto(cmdArgs, { json: JSON_OUTPUT });
3870
- return;
3871
- }
3872
3919
  case "multi": {
3873
3920
  if (cmdArgs.some(isHelpArg)) {
3874
3921
  const { pathToFileURL } = await import("node:url");
@@ -36,7 +36,7 @@
36
36
  "policy": "stdio",
37
37
  "transport": "stdio",
38
38
  "command": "npx",
39
- "args": ["-y", "@brave/brave-search-mcp-server"],
39
+ "args": ["-y", "@brave/brave-search-mcp-server@2.1.4"],
40
40
  "env": {
41
41
  "BRAVE_API_KEY": { "env": "BRAVE_API_KEY" }
42
42
  },
@@ -3,10 +3,6 @@
3
3
  import { argv, exit, stdin, stdout } from "node:process";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { writeCodexSessionRecord } from "../hub/lib/codex-session-registry.mjs";
6
- import {
7
- emitParticipantSessionStarted,
8
- shouldSkipInteractiveRegistration,
9
- } from "../scripts/lib/session-presence.mjs";
10
6
 
11
7
  function parsePayload(stdinData) {
12
8
  try {
@@ -72,23 +68,11 @@ export async function runCodexSessionHook(stdinData, opts = {}) {
72
68
  ? normalizeMode(opts.argvMode ?? argv[2], parsed.payload)
73
69
  : "";
74
70
  const writeSessionRecord = opts.writeSessionRecord || writeCodexSessionRecord;
75
- const emitSessionStarted =
76
- opts.emitSessionStarted || emitParticipantSessionStarted;
77
71
 
78
72
  try {
79
73
  await runHookSideEffectsWithStdoutSuppressed(async () => {
80
74
  if (mode === "register" || mode === "heartbeat") {
81
- try {
82
- writeSessionRecord(parsed.payload);
83
- } catch {}
84
- }
85
- if (
86
- mode === "register" &&
87
- !shouldSkipInteractiveRegistration(parsed.payload, opts)
88
- ) {
89
- try {
90
- await emitSessionStarted(stdinData);
91
- } catch {}
75
+ writeSessionRecord(parsed.payload);
92
76
  }
93
77
  });
94
78
  } catch {