triflux 9.8.3 → 9.8.5

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 (72) hide show
  1. package/bin/triflux.mjs +139 -1
  2. package/hooks/hook-adaptive-collector.mjs +86 -0
  3. package/hooks/hook-registry.json +4 -4
  4. package/hooks/keyword-rules.json +189 -1
  5. package/hooks/safety-guard.mjs +16 -0
  6. package/hub/adaptive-diagnostic.mjs +319 -0
  7. package/hub/adaptive-inject.mjs +186 -0
  8. package/hub/adaptive-memory.mjs +322 -0
  9. package/hub/adaptive.mjs +143 -0
  10. package/hub/cli-adapter-base.mjs +192 -0
  11. package/hub/codex-adapter.mjs +190 -0
  12. package/hub/codex-preflight.mjs +147 -0
  13. package/hub/gemini-adapter.mjs +179 -0
  14. package/hub/lib/known-errors.json +72 -0
  15. package/hub/lib/memory-store.mjs +748 -0
  16. package/hub/lib/ssh-command.mjs +150 -0
  17. package/hub/lib/uuidv7.mjs +44 -0
  18. package/hub/middleware/request-logger.mjs +80 -0
  19. package/hub/pipe.mjs +1 -1
  20. package/hub/platform.mjs +58 -19
  21. package/hub/reflexion.mjs +303 -38
  22. package/hub/router.mjs +1 -1
  23. package/hub/schema.sql +2 -0
  24. package/hub/server.mjs +1218 -1112
  25. package/hub/session-fingerprint.mjs +352 -0
  26. package/hub/store-adapter.mjs +88 -584
  27. package/hub/store.mjs +857 -820
  28. package/hub/team/backend.mjs +3 -5
  29. package/hub/team/cli/services/hub-client.mjs +38 -19
  30. package/hub/team/cli/services/native-control.mjs +4 -5
  31. package/hub/team/conductor.mjs +602 -0
  32. package/hub/team/event-log.mjs +76 -0
  33. package/hub/team/headless.mjs +89 -30
  34. package/hub/team/health-probe.mjs +272 -0
  35. package/hub/team/launcher-template.mjs +94 -0
  36. package/hub/team/lead-control.mjs +104 -0
  37. package/hub/team/notify.mjs +293 -0
  38. package/hub/team/pane.mjs +4 -5
  39. package/hub/team/process-cleanup.mjs +342 -0
  40. package/hub/team/remote-probe.mjs +276 -0
  41. package/hub/team/remote-watcher.mjs +478 -0
  42. package/hub/team/session-sync.mjs +169 -0
  43. package/hub/team/tui-remote-adapter.mjs +393 -0
  44. package/hub/team/tui.mjs +206 -2
  45. package/hub/team-bridge.mjs +25 -0
  46. package/hub/tools.mjs +1 -1
  47. package/hud/constants.mjs +7 -0
  48. package/hud/context-monitor.mjs +397 -0
  49. package/hud/hud-qos-status.mjs +8 -4
  50. package/hud/providers/claude.mjs +5 -0
  51. package/hud/renderers.mjs +13 -9
  52. package/package.json +15 -5
  53. package/scripts/__tests__/gen-skill-docs.test.mjs +87 -0
  54. package/scripts/__tests__/skill-template.test.mjs +104 -0
  55. package/scripts/cache-warmup.mjs +3 -3
  56. package/scripts/gen-skill-docs.mjs +110 -0
  57. package/scripts/lib/claudemd-manager.mjs +325 -0
  58. package/scripts/lib/claudemd-scanner.mjs +218 -0
  59. package/scripts/lib/env-probe.mjs +95 -14
  60. package/scripts/lib/handoff.mjs +171 -0
  61. package/scripts/lib/skill-template.mjs +222 -0
  62. package/scripts/notion-read.mjs +5 -3
  63. package/scripts/pack.mjs +205 -0
  64. package/scripts/preflight-cache.mjs +6 -5
  65. package/scripts/remote-spawn.mjs +5 -5
  66. package/scripts/setup.mjs +260 -0
  67. package/scripts/templates/claudemd-tfx-section.md +54 -0
  68. package/scripts/test-lock.mjs +71 -0
  69. package/skills/_templates/base.md +9 -0
  70. package/skills/_templates/deep.md +6 -0
  71. package/skills/tfx-codex-swarm/SKILL.md +55 -11
  72. /package/hub/{team/codex-compat.mjs → codex-compat.mjs} +0 -0
package/bin/triflux.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // triflux CLI — setup, doctor, version
3
3
  import { copyFileSync, existsSync, readFileSync, readSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
4
- import { join, dirname, basename } from "path";
4
+ import { join, dirname, basename, resolve } from "path";
5
5
  import { homedir, tmpdir } from "os";
6
6
  import { execSync, execFileSync, spawn } from "child_process";
7
7
  import { fileURLToPath } from "url";
@@ -11,6 +11,7 @@ import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions,
11
11
  import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
12
12
  import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
13
13
  import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
14
+ import { serializeHandoff } from "../scripts/lib/handoff.mjs";
14
15
  import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
15
16
  import { probePsmuxSupport, formatPsmuxInstallGuidance, formatPsmuxUpdateGuidance } from "../scripts/lib/psmux-info.mjs";
16
17
  import {
@@ -98,6 +99,17 @@ const CLI_COMMAND_SCHEMAS = Object.freeze({
98
99
  { name: "--json", type: "boolean", description: "버전 정보를 JSON으로 출력" },
99
100
  ],
100
101
  },
102
+ handoff: {
103
+ usage: "tfx handoff [--target local|remote] [--decision <text>] [--decision-file <path>] [--output <path>] [--json]",
104
+ description: "현재 작업 컨텍스트를 세션 핸드오프 프롬프트로 직렬화",
105
+ options: [
106
+ { name: "--target", type: "string", description: "주입 대상 (local|remote, 기본값 remote)" },
107
+ { name: "--decision", type: "string", description: "핸드오프 결정사항 (반복 지정 가능)" },
108
+ { name: "--decision-file", type: "string", description: "결정사항 파일 (라인/불릿 단위)" },
109
+ { name: "--output", type: "string", description: "생성한 핸드오프 프롬프트 저장 경로" },
110
+ { name: "--json", type: "boolean", description: "핸드오프 결과를 JSON으로 출력" },
111
+ ],
112
+ },
101
113
  list: {
102
114
  usage: "tfx list [--json]",
103
115
  description: "패키지 스킬과 사용자 스킬 목록 표시",
@@ -2783,6 +2795,128 @@ function cmdVersion(options = {}) {
2783
2795
  console.log("");
2784
2796
  }
2785
2797
 
2798
+ function cmdHandoff(args = [], options = {}) {
2799
+ const { json = false } = options;
2800
+ const parsed = {
2801
+ target: "remote",
2802
+ decisions: [],
2803
+ decisionFile: null,
2804
+ output: null,
2805
+ cwd: process.cwd(),
2806
+ };
2807
+
2808
+ for (let index = 0; index < args.length; index += 1) {
2809
+ const arg = args[index];
2810
+ const next = args[index + 1];
2811
+
2812
+ if (arg === "--target") {
2813
+ if (!next || next.startsWith("-")) {
2814
+ throw createCliError("--target 값이 필요합니다 (local|remote)", {
2815
+ exitCode: EXIT_ARG_ERROR,
2816
+ reason: "argError",
2817
+ fix: "tfx handoff --target remote",
2818
+ });
2819
+ }
2820
+ if (!["local", "remote"].includes(next)) {
2821
+ throw createCliError(`지원하지 않는 --target 값: ${next}`, {
2822
+ exitCode: EXIT_ARG_ERROR,
2823
+ reason: "argError",
2824
+ fix: "tfx handoff --target local|remote",
2825
+ });
2826
+ }
2827
+ parsed.target = next;
2828
+ index += 1;
2829
+ continue;
2830
+ }
2831
+
2832
+ if (arg === "--decision") {
2833
+ if (!next || next.startsWith("-")) {
2834
+ throw createCliError("--decision 값이 필요합니다", {
2835
+ exitCode: EXIT_ARG_ERROR,
2836
+ reason: "argError",
2837
+ fix: "tfx handoff --decision \"결정사항\"",
2838
+ });
2839
+ }
2840
+ parsed.decisions.push(next);
2841
+ index += 1;
2842
+ continue;
2843
+ }
2844
+
2845
+ if (arg === "--decision-file") {
2846
+ if (!next || next.startsWith("-")) {
2847
+ throw createCliError("--decision-file 경로가 필요합니다", {
2848
+ exitCode: EXIT_ARG_ERROR,
2849
+ reason: "argError",
2850
+ fix: "tfx handoff --decision-file .omx/notepad.md",
2851
+ });
2852
+ }
2853
+ parsed.decisionFile = resolve(next);
2854
+ index += 1;
2855
+ continue;
2856
+ }
2857
+
2858
+ if (arg === "--output" || arg === "--out") {
2859
+ if (!next || next.startsWith("-")) {
2860
+ throw createCliError(`${arg} 경로가 필요합니다`, {
2861
+ exitCode: EXIT_ARG_ERROR,
2862
+ reason: "argError",
2863
+ fix: "tfx handoff --output .omx/handoff.md",
2864
+ });
2865
+ }
2866
+ parsed.output = resolve(next);
2867
+ index += 1;
2868
+ continue;
2869
+ }
2870
+
2871
+ if (arg === "--cwd") {
2872
+ if (!next || next.startsWith("-")) {
2873
+ throw createCliError("--cwd 경로가 필요합니다", {
2874
+ exitCode: EXIT_ARG_ERROR,
2875
+ reason: "argError",
2876
+ fix: "tfx handoff --cwd <project-path>",
2877
+ });
2878
+ }
2879
+ parsed.cwd = resolve(next);
2880
+ index += 1;
2881
+ continue;
2882
+ }
2883
+
2884
+ throw createCliError(`알 수 없는 handoff 옵션: ${arg}`, {
2885
+ exitCode: EXIT_ARG_ERROR,
2886
+ reason: "argError",
2887
+ fix: "tfx handoff --target remote --output .omx/handoff.md",
2888
+ });
2889
+ }
2890
+
2891
+ const result = serializeHandoff({
2892
+ target: parsed.target,
2893
+ decisions: parsed.decisions,
2894
+ decisionFile: parsed.decisionFile,
2895
+ cwd: parsed.cwd,
2896
+ });
2897
+
2898
+ if (parsed.output) {
2899
+ const outputDir = dirname(parsed.output);
2900
+ if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
2901
+ writeFileSync(parsed.output, `${result.prompt}\n`, "utf8");
2902
+ }
2903
+
2904
+ if (json) {
2905
+ printJson({
2906
+ handoff: {
2907
+ ...result,
2908
+ ...(parsed.output ? { output: parsed.output } : {}),
2909
+ },
2910
+ });
2911
+ return;
2912
+ }
2913
+
2914
+ console.log(result.prompt);
2915
+ if (parsed.output) {
2916
+ console.log(`\n${DIM}saved:${RESET} ${parsed.output}`);
2917
+ }
2918
+ }
2919
+
2786
2920
  function cmdSchema(args = []) {
2787
2921
  const bundle = loadDelegatorSchemaBundle();
2788
2922
  const selector = String(args[0] || "").trim();
@@ -3054,6 +3188,7 @@ ${updateNotice}
3054
3188
  ${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
3055
3189
  ${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
3056
3190
  ${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
3191
+ ${WHITE_BRIGHT}tfx handoff${RESET} ${GRAY}현재 컨텍스트를 원격/로컬 핸드오프 프롬프트로 생성${RESET}
3057
3192
  ${WHITE_BRIGHT}tfx schema${RESET} ${GRAY}CLI/Hub schema JSON 출력${RESET}
3058
3193
  ${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
3059
3194
  ${WHITE_BRIGHT}tfx tray${RESET} ${GRAY}Windows 시스템 트레이 실행${RESET}
@@ -3559,6 +3694,9 @@ async function main() {
3559
3694
  case "ls":
3560
3695
  cmdList({ json: JSON_OUTPUT });
3561
3696
  return;
3697
+ case "handoff":
3698
+ cmdHandoff(cmdArgs, { json: JSON_OUTPUT });
3699
+ return;
3562
3700
  case "hub":
3563
3701
  await cmdHub(cmdArgs, { json: JSON_OUTPUT && (cmdArgs[0] || "status") === "status" });
3564
3702
  return;
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { basename, join } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ import { createAdaptiveEngine } from '../hub/adaptive.mjs';
8
+
9
+ let engine = null;
10
+ let createEngine = createAdaptiveEngine;
11
+
12
+ function readStdin() {
13
+ try {
14
+ return readFileSync(0, 'utf8');
15
+ } catch {
16
+ return '';
17
+ }
18
+ }
19
+
20
+ function inferProjectSlug(cwd = process.cwd()) {
21
+ const packagePath = join(cwd, 'package.json');
22
+ if (existsSync(packagePath)) {
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
25
+ if (typeof pkg.name === 'string' && pkg.name.trim()) return pkg.name.trim();
26
+ } catch {}
27
+ }
28
+ return basename(cwd) || 'default';
29
+ }
30
+
31
+ function getEngine() {
32
+ if (engine) return engine;
33
+ engine = createEngine({
34
+ projectSlug: inferProjectSlug(),
35
+ repoRoot: process.cwd(),
36
+ });
37
+ engine.startSession?.();
38
+ return engine;
39
+ }
40
+
41
+ function buildErrorContext(event = {}) {
42
+ return {
43
+ exitCode: event.exitCode,
44
+ stderr: String(event.stderr || '').slice(0, 500),
45
+ tool: event.tool,
46
+ command: String(event.command || '').slice(0, 200),
47
+ timestamp: new Date().toISOString(),
48
+ };
49
+ }
50
+
51
+ export default function hookAdaptiveCollector(event = {}) {
52
+ if (Number(event.exitCode) === 0) return null;
53
+ if (!event.tool || event.tool === 'Read') return null;
54
+
55
+ const result = getEngine().handleError(buildErrorContext(event));
56
+ if (result?.diagnosed) {
57
+ console.error(`[adaptive] 에러 패턴 감지: ${result.rule?.id || 'unknown'}`);
58
+ if (result.promoted) {
59
+ console.error(`[adaptive] 규칙 승격 → Tier ${result.rule?.tier ?? '?'}`);
60
+ }
61
+ }
62
+ return result;
63
+ }
64
+
65
+ export function __setAdaptiveCollectorFactoryForTests(factory) {
66
+ createEngine = factory;
67
+ engine = null;
68
+ }
69
+
70
+ export function __resetAdaptiveCollectorForTests() {
71
+ createEngine = createAdaptiveEngine;
72
+ engine = null;
73
+ }
74
+
75
+ function main() {
76
+ const raw = readStdin();
77
+ if (!raw.trim()) return;
78
+ try {
79
+ hookAdaptiveCollector(JSON.parse(raw));
80
+ } catch {}
81
+ }
82
+
83
+ const isEntrypoint = process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url;
84
+ if (isEntrypoint) {
85
+ main();
86
+ }
@@ -153,9 +153,9 @@
153
153
  "id": "ext-session-vault-start",
154
154
  "source": "session-vault",
155
155
  "matcher": "*",
156
- "command": "${HOME}/Desktop/Projects/cli/session-vault/scripts/start_hook.sh",
156
+ "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/start_hook.sh\"",
157
157
  "priority": 100,
158
- "enabled": false,
158
+ "enabled": true,
159
159
  "timeout": 10,
160
160
  "blocking": false,
161
161
  "description": "세션 볼트 로깅 시작"
@@ -177,9 +177,9 @@
177
177
  "id": "ext-session-vault-export",
178
178
  "source": "session-vault",
179
179
  "matcher": "*",
180
- "command": "${HOME}/Desktop/Projects/cli/session-vault/scripts/export_hook.sh",
180
+ "command": "bash \"${HOME}/Desktop/Projects/tools/session-vault/scripts/export_hook.sh\"",
181
181
  "priority": 100,
182
- "enabled": false,
182
+ "enabled": true,
183
183
  "timeout": 30,
184
184
  "blocking": false,
185
185
  "description": "세션 트랜스크립트 내보내기"
@@ -37,6 +37,46 @@
37
37
  "state": null,
38
38
  "mcp_route": null
39
39
  },
40
+ {
41
+ "id": "tfx-codex-swarm",
42
+ "patterns": [
43
+ {
44
+ "source": "\\btfx[\\s-]?codex[\\s-]?swarm\\b",
45
+ "flags": "i"
46
+ },
47
+ {
48
+ "source": "\\bcodex[\\s-]?swarm\\b",
49
+ "flags": "i"
50
+ }
51
+ ],
52
+ "skill": "tfx-codex-swarm",
53
+ "priority": 1,
54
+ "supersedes": [
55
+ "tfx-codex"
56
+ ],
57
+ "exclusive": false,
58
+ "state": null,
59
+ "mcp_route": null
60
+ },
61
+ {
62
+ "id": "tfx-remote-spawn",
63
+ "patterns": [
64
+ {
65
+ "source": "\\btfx[\\s-]?remote[\\s-]?spawn\\b",
66
+ "flags": "i"
67
+ },
68
+ {
69
+ "source": "\\bremote[\\s-]?spawn\\b",
70
+ "flags": "i"
71
+ }
72
+ ],
73
+ "skill": "tfx-remote-spawn",
74
+ "priority": 1,
75
+ "supersedes": [],
76
+ "exclusive": false,
77
+ "state": null,
78
+ "mcp_route": null
79
+ },
40
80
  {
41
81
  "id": "tfx-auto-codex",
42
82
  "patterns": [
@@ -74,7 +114,7 @@
74
114
  "id": "tfx-codex",
75
115
  "patterns": [
76
116
  {
77
- "source": "\\btfx[\\s-]?codex\\b",
117
+ "source": "\\btfx[\\s-]?codex\\b(?![\\s-]?swarm)",
78
118
  "flags": "i"
79
119
  }
80
120
  ],
@@ -373,6 +413,154 @@
373
413
  "state": null,
374
414
  "mcp_route": "codex"
375
415
  },
416
+ {
417
+ "id": "gstack-checkpoint",
418
+ "patterns": [
419
+ {
420
+ "source": "(?:어디까지\\s*했|뭐\\s*하고\\s*있었|뭘\\s*해야\\s*하|까먹|기억이\\s*안|이어서\\s*하|중단\\s*지점)",
421
+ "flags": "i"
422
+ },
423
+ {
424
+ "source": "\\b(?:checkpoint|resume|pick\\s*up|where\\s*was\\s*I)\\b",
425
+ "flags": "i"
426
+ }
427
+ ],
428
+ "skill": "checkpoint",
429
+ "priority": 5,
430
+ "supersedes": [],
431
+ "exclusive": false,
432
+ "state": null,
433
+ "mcp_route": null
434
+ },
435
+ {
436
+ "id": "gstack-office-hours",
437
+ "patterns": [
438
+ {
439
+ "source": "(?:아이디어\\s*(?:있|정리|브레인)|뭘\\s*만들|제품\\s*구상)",
440
+ "flags": "i"
441
+ },
442
+ {
443
+ "source": "\\boffice[\\s-]?hours\\b",
444
+ "flags": "i"
445
+ }
446
+ ],
447
+ "skill": "office-hours",
448
+ "priority": 5,
449
+ "supersedes": [],
450
+ "exclusive": false,
451
+ "state": null,
452
+ "mcp_route": null
453
+ },
454
+ {
455
+ "id": "gstack-ship",
456
+ "patterns": [
457
+ {
458
+ "source": "(?:배포해|PR\\s*만들|릴리스\\s*해|머지하고\\s*배포)",
459
+ "flags": "i"
460
+ },
461
+ {
462
+ "source": "\\bship\\b(?!.*tfx)",
463
+ "flags": "i"
464
+ }
465
+ ],
466
+ "skill": "ship",
467
+ "priority": 5,
468
+ "supersedes": [],
469
+ "exclusive": false,
470
+ "state": null,
471
+ "mcp_route": null
472
+ },
473
+ {
474
+ "id": "gstack-investigate",
475
+ "patterns": [
476
+ {
477
+ "source": "(?:왜\\s*(?:안\\s*돼|터져|에러)|원인\\s*(?:찾아|분석)|root\\s*cause)",
478
+ "flags": "i"
479
+ },
480
+ {
481
+ "source": "\\binvestigate\\b",
482
+ "flags": "i"
483
+ }
484
+ ],
485
+ "skill": "investigate",
486
+ "priority": 5,
487
+ "supersedes": [],
488
+ "exclusive": false,
489
+ "state": null,
490
+ "mcp_route": null
491
+ },
492
+ {
493
+ "id": "gstack-cso",
494
+ "patterns": [
495
+ {
496
+ "source": "(?:보안\\s*(?:감사|점검|리뷰|스캔)|\\bcso\\b|\\bOWASP\\b|\\bSTRIDE\\b)",
497
+ "flags": "i"
498
+ }
499
+ ],
500
+ "skill": "cso",
501
+ "priority": 5,
502
+ "supersedes": [],
503
+ "exclusive": false,
504
+ "state": null,
505
+ "mcp_route": null
506
+ },
507
+ {
508
+ "id": "gstack-qa-browser",
509
+ "patterns": [
510
+ {
511
+ "source": "(?:사이트|브라우저|웹)\\s*(?:QA|테스트|확인|점검)",
512
+ "flags": "i"
513
+ },
514
+ {
515
+ "source": "(?:클릭|접속).*(?:테스트|확인)",
516
+ "flags": "i"
517
+ }
518
+ ],
519
+ "skill": "qa",
520
+ "priority": 5,
521
+ "supersedes": [],
522
+ "exclusive": false,
523
+ "state": null,
524
+ "mcp_route": null
525
+ },
526
+ {
527
+ "id": "gstack-retro",
528
+ "patterns": [
529
+ {
530
+ "source": "(?:회고|이번\\s*주\\s*뭐\\s*했|주간\\s*리뷰|뭘\\s*했지)",
531
+ "flags": "i"
532
+ },
533
+ {
534
+ "source": "\\bretro\\b",
535
+ "flags": "i"
536
+ }
537
+ ],
538
+ "skill": "retro",
539
+ "priority": 5,
540
+ "supersedes": [],
541
+ "exclusive": false,
542
+ "state": null,
543
+ "mcp_route": null
544
+ },
545
+ {
546
+ "id": "gstack-autoplan",
547
+ "patterns": [
548
+ {
549
+ "source": "(?:자동\\s*리뷰|리뷰\\s*파이프라인|전체\\s*리뷰)",
550
+ "flags": "i"
551
+ },
552
+ {
553
+ "source": "\\bautoplan\\b",
554
+ "flags": "i"
555
+ }
556
+ ],
557
+ "skill": "autoplan",
558
+ "priority": 5,
559
+ "supersedes": [],
560
+ "exclusive": false,
561
+ "state": null,
562
+ "mcp_route": null
563
+ },
376
564
  {
377
565
  "id": "suppress-omc-team",
378
566
  "patterns": [
@@ -22,6 +22,8 @@ const BLOCK_RULES = [
22
22
  { pattern: /\bformat\s+[a-z]:/i, reason: "디스크 포맷 차단" },
23
23
  { pattern: /\b(del|rmdir)\s+\/[sq]\b/i, reason: "Windows 재귀 삭제 차단" },
24
24
  { pattern: /\bgit\s+clean\s+.*-fd/i, reason: "git clean -fd 차단 — 추적되지 않은 파일 소실 위험" },
25
+ { pattern: /\bpsmux\s+kill-session\b/i, reason: "raw psmux kill-session 차단 — WT ConPTY 프리징 위험. 안전 경로: node hub/team/psmux.mjs kill --session <name>", skipIfGit: true },
26
+ { pattern: /\bpsmux\s+kill-server\b/i, reason: "psmux kill-server 차단 — 모든 세션이 즉시 종료됩니다. node hub/team/psmux.mjs kill-swarm 사용", skipIfGit: true },
25
27
  ];
26
28
 
27
29
  // ── 경고 규칙 ──────────────────────────────────────────────
@@ -60,8 +62,22 @@ function main() {
60
62
  const command = (input.tool_input?.command || "").trim();
61
63
  if (!command) process.exit(0);
62
64
 
65
+ // psmux 명령이 실제 CLI 호출인지 판별 (오탐 방지)
66
+ // git commit 메시지, echo, grep, cat, heredoc 안의 텍스트는 무시
67
+ function isPsmuxInvocation(cmd) {
68
+ // 명령을 세그먼트로 분할 (&&, ;, | 기준)
69
+ const segments = cmd.split(/[;&|]+/);
70
+ return segments.some((seg) => {
71
+ const trimmed = seg.trim();
72
+ if (trimmed.startsWith("#")) return false; // 주석
73
+ // 세그먼트의 첫 단어가 psmux인 경우만 실제 호출
74
+ return /^\s*psmux\s+kill-(session|server)\b/i.test(trimmed);
75
+ });
76
+ }
77
+
63
78
  // 1. BLOCK 체크 — exit 2로 차단
64
79
  for (const rule of BLOCK_RULES) {
80
+ if (rule.skipIfGit && !isPsmuxInvocation(command)) continue;
65
81
  if (rule.pattern.test(command)) {
66
82
  process.stderr.write(
67
83
  `[triflux safety-guard] BLOCKED: ${rule.reason}\n` +