triflux 7.1.4 → 7.2.2

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 (73) hide show
  1. package/.claude-plugin/marketplace.json +31 -31
  2. package/.claude-plugin/plugin.json +22 -23
  3. package/bin/triflux.mjs +18 -5
  4. package/hooks/keyword-rules.json +393 -361
  5. package/hub/bridge.mjs +799 -786
  6. package/hub/delegator/contracts.mjs +37 -38
  7. package/hub/delegator/schema/delegator-tools.schema.json +250 -250
  8. package/hub/delegator/service.mjs +307 -302
  9. package/hub/intent.mjs +108 -11
  10. package/hub/lib/process-utils.mjs +20 -0
  11. package/hub/pipe.mjs +589 -589
  12. package/hub/pipeline/gates/confidence.mjs +1 -1
  13. package/hub/pipeline/gates/selfcheck.mjs +2 -4
  14. package/hub/pipeline/state.mjs +191 -187
  15. package/hub/pipeline/transitions.mjs +124 -120
  16. package/hub/public/dashboard.html +355 -349
  17. package/hub/quality/deslop.mjs +5 -3
  18. package/hub/reflexion.mjs +5 -1
  19. package/hub/research.mjs +6 -1
  20. package/hub/router.mjs +791 -782
  21. package/hub/server.mjs +893 -822
  22. package/hub/store.mjs +807 -778
  23. package/hub/team/agent-map.json +10 -0
  24. package/hub/team/ansi.mjs +3 -4
  25. package/hub/team/cli/commands/control.mjs +43 -43
  26. package/hub/team/cli/commands/interrupt.mjs +36 -36
  27. package/hub/team/cli/commands/kill.mjs +3 -3
  28. package/hub/team/cli/commands/send.mjs +37 -37
  29. package/hub/team/cli/commands/start/index.mjs +18 -8
  30. package/hub/team/cli/commands/start/parse-args.mjs +3 -1
  31. package/hub/team/cli/commands/start/start-headless.mjs +4 -1
  32. package/hub/team/cli/commands/status.mjs +87 -87
  33. package/hub/team/cli/commands/stop.mjs +1 -1
  34. package/hub/team/cli/commands/task.mjs +1 -1
  35. package/hub/team/cli/index.mjs +41 -39
  36. package/hub/team/cli/manifest.mjs +29 -28
  37. package/hub/team/cli/services/hub-client.mjs +37 -0
  38. package/hub/team/cli/services/state-store.mjs +26 -12
  39. package/hub/team/dashboard.mjs +11 -4
  40. package/hub/team/handoff.mjs +12 -0
  41. package/hub/team/headless.mjs +202 -200
  42. package/hub/team/native-supervisor.mjs +386 -346
  43. package/hub/team/nativeProxy.mjs +680 -692
  44. package/hub/team/staleState.mjs +361 -369
  45. package/hub/team/tui-viewer.mjs +27 -3
  46. package/hub/team/tui.mjs +1 -0
  47. package/hub/token-mode.mjs +114 -24
  48. package/hub/workers/delegator-mcp.mjs +1059 -1057
  49. package/hud/colors.mjs +88 -0
  50. package/hud/constants.mjs +78 -0
  51. package/hud/hud-qos-status.mjs +206 -1872
  52. package/hud/providers/claude.mjs +309 -0
  53. package/hud/providers/codex.mjs +151 -0
  54. package/hud/providers/gemini.mjs +320 -0
  55. package/hud/renderers.mjs +424 -0
  56. package/hud/terminal.mjs +140 -0
  57. package/hud/utils.mjs +271 -0
  58. package/package.json +1 -2
  59. package/scripts/__tests__/keyword-detector.test.mjs +234 -234
  60. package/scripts/headless-guard-fast.sh +21 -0
  61. package/scripts/headless-guard.mjs +26 -6
  62. package/scripts/lib/keyword-rules.mjs +166 -168
  63. package/scripts/setup.mjs +725 -690
  64. package/scripts/tfx-route-post.mjs +424 -424
  65. package/scripts/tfx-route.sh +1671 -1650
  66. package/scripts/tmp-cleanup.mjs +74 -0
  67. package/skills/tfx-auto/SKILL.md +279 -278
  68. package/skills/tfx-auto-codex/SKILL.md +98 -77
  69. package/skills/tfx-codex/SKILL.md +65 -65
  70. package/skills/tfx-gemini/SKILL.md +83 -82
  71. package/skills/tfx-hub/SKILL.md +205 -136
  72. package/skills/tfx-multi/SKILL.md +11 -5
  73. package/.mcp.json +0 -8
@@ -1,234 +1,234 @@
1
- import assert from "node:assert/strict";
2
- import { spawnSync } from "node:child_process";
3
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
5
- import { dirname, join, resolve } from "node:path";
6
- import test from "node:test";
7
- import { fileURLToPath } from "node:url";
8
- import { compileRules, loadRules, matchRules, resolveConflicts } from "../lib/keyword-rules.mjs";
9
-
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const projectRoot = resolve(__dirname, "..", "..");
12
- const rulesPath = join(projectRoot, "hooks", "keyword-rules.json");
13
- const detectorScriptPath = join(projectRoot, "scripts", "keyword-detector.mjs");
14
-
15
- // keyword-detector는 import 시 main()이 실행되므로, 테스트 로딩 단계에서만 안전하게 비활성화한다.
16
- const previousDisable = process.env.TRIFLUX_DISABLE_MAGICWORDS;
17
- const previousLog = console.log;
18
- process.env.TRIFLUX_DISABLE_MAGICWORDS = "1";
19
- console.log = () => {};
20
- const detectorModule = await import("../keyword-detector.mjs");
21
- console.log = previousLog;
22
- if (previousDisable === undefined) {
23
- delete process.env.TRIFLUX_DISABLE_MAGICWORDS;
24
- } else {
25
- process.env.TRIFLUX_DISABLE_MAGICWORDS = previousDisable;
26
- }
27
-
28
- const { extractPrompt, sanitizeForKeywordDetection } = detectorModule;
29
-
30
- function loadCompiledRules() {
31
- const rules = loadRules(rulesPath);
32
- assert.equal(rules.length, 21);
33
- return compileRules(rules);
34
- }
35
-
36
- function runDetector(prompt) {
37
- const payload = { prompt, cwd: projectRoot };
38
- const result = spawnSync(process.execPath, [detectorScriptPath], {
39
- input: JSON.stringify(payload),
40
- encoding: "utf8"
41
- });
42
-
43
- assert.equal(result.status, 0, result.stderr);
44
- assert.ok(result.stdout.trim(), "keyword-detector 출력이 비어 있습니다.");
45
- return JSON.parse(result.stdout.trim());
46
- }
47
-
48
- test("extractPrompt: prompt/message.content/parts[].text 우선순위", () => {
49
- assert.equal(
50
- extractPrompt({
51
- prompt: "from prompt",
52
- message: { content: "from message" },
53
- parts: [{ text: "from parts" }]
54
- }),
55
- "from prompt"
56
- );
57
-
58
- assert.equal(
59
- extractPrompt({
60
- prompt: " ",
61
- message: { content: "from message" },
62
- parts: [{ text: "from parts" }]
63
- }),
64
- "from message"
65
- );
66
-
67
- assert.equal(
68
- extractPrompt({
69
- message: { content: [{ text: "from message-part" }] },
70
- parts: [{ text: "from parts" }]
71
- }),
72
- "from message-part"
73
- );
74
-
75
- assert.equal(extractPrompt({ parts: [{ text: "from parts" }] }), "from parts");
76
- });
77
-
78
- test("sanitizeForKeywordDetection: 코드블록/URL/파일경로/XML 태그 제거", () => {
79
- const input = [
80
- "정상 문장",
81
- "```sh",
82
- "tfx multi",
83
- "```",
84
- "https://example.com/path?q=1",
85
- "C:\\Users\\SSAFY\\Desktop\\Projects\\tools\\triflux",
86
- "./hooks/keyword-rules.json",
87
- "<tag>jira 이슈 생성</tag>"
88
- ].join("\n");
89
-
90
- const sanitized = sanitizeForKeywordDetection(input);
91
-
92
- assert.ok(sanitized.includes("정상 문장"));
93
- assert.ok(!sanitized.includes("tfx multi"));
94
- assert.ok(!sanitized.includes("https://"));
95
- assert.ok(!sanitized.includes("C:\\Users\\"));
96
- assert.ok(!sanitized.includes("./hooks/keyword-rules.json"));
97
- assert.ok(!sanitized.includes("<tag>"));
98
- assert.ok(!sanitized.includes("jira 이슈 생성"));
99
- });
100
-
101
- test("loadRules: 유효한 JSON 로드", () => {
102
- const rules = loadRules(rulesPath);
103
- assert.equal(rules.length, 21);
104
- assert.equal(rules.filter((rule) => rule.skill).length, 10);
105
- assert.equal(rules.filter((rule) => rule.mcp_route).length, 10);
106
- });
107
-
108
- test("loadRules: 잘못된 파일 처리", () => {
109
- const tempDir = mkdtempSync(join(tmpdir(), "triflux-rules-"));
110
- const invalidPath = join(tempDir, "invalid.json");
111
- writeFileSync(invalidPath, "{ invalid json", "utf8");
112
-
113
- const malformed = loadRules(invalidPath);
114
- const missing = loadRules(join(tempDir, "missing.json"));
115
-
116
- assert.deepEqual(malformed, []);
117
- assert.deepEqual(missing, []);
118
-
119
- rmSync(tempDir, { recursive: true, force: true });
120
- });
121
-
122
- test("compileRules: 정규식 컴파일 성공", () => {
123
- const rules = loadRules(rulesPath);
124
- const compiled = compileRules(rules);
125
- assert.equal(compiled.length, 21);
126
- for (const rule of compiled) {
127
- assert.ok(Array.isArray(rule.compiledPatterns));
128
- assert.ok(rule.compiledPatterns.length > 0);
129
- for (const pattern of rule.compiledPatterns) {
130
- assert.ok(pattern instanceof RegExp);
131
- }
132
- }
133
- });
134
-
135
- test("compileRules: 정규식 컴파일 실패", () => {
136
- const compiled = compileRules([
137
- {
138
- id: "bad-pattern",
139
- priority: 1,
140
- patterns: [{ source: "[", flags: "" }],
141
- skill: "tfx-multi",
142
- supersedes: [],
143
- exclusive: false,
144
- state: null,
145
- mcp_route: null
146
- }
147
- ]);
148
-
149
- assert.deepEqual(compiled, []);
150
- });
151
-
152
- test("matchRules: tfx 키워드 매칭", () => {
153
- const compiledRules = loadCompiledRules();
154
- const cases = [
155
- { text: "tfx multi 세션 시작", expectedId: "tfx-multi" },
156
- { text: "tfx auto 돌려줘", expectedId: "tfx-auto" },
157
- { text: "tfx codex 로 실행", expectedId: "tfx-codex" },
158
- { text: "tfx gemini 로 실행", expectedId: "tfx-gemini" },
159
- { text: "canceltfx", expectedId: "tfx-cancel" }
160
- ];
161
-
162
- for (const { text, expectedId } of cases) {
163
- const clean = sanitizeForKeywordDetection(text);
164
- const matches = matchRules(compiledRules, clean);
165
- assert.ok(matches.some((match) => match.id === expectedId), `${text} => ${expectedId} 미매칭`);
166
- }
167
- });
168
-
169
- test("matchRules: MCP 라우팅 매칭", () => {
170
- const compiledRules = loadCompiledRules();
171
- const cases = [
172
- { text: "노션 페이지 조회해줘", expectedId: "notion-route", expectedRoute: "gemini" },
173
- { text: "jira 이슈 생성", expectedId: "jira-route", expectedRoute: "codex" },
174
- { text: "크롬 열고 로그인", expectedId: "chrome-route", expectedRoute: "gemini" },
175
- { text: "이메일 보내줘", expectedId: "mail-route", expectedRoute: "gemini" },
176
- { text: "캘린더 일정 생성", expectedId: "calendar-route", expectedRoute: "gemini" },
177
- { text: "playwright 테스트 작성", expectedId: "playwright-route", expectedRoute: "gemini" },
178
- { text: "canva 디자인 생성", expectedId: "canva-route", expectedRoute: "gemini" }
179
- ];
180
-
181
- for (const { text, expectedId, expectedRoute } of cases) {
182
- const matches = matchRules(compiledRules, sanitizeForKeywordDetection(text));
183
- const matched = matches.find((match) => match.id === expectedId);
184
- assert.ok(matched, `${text} => ${expectedId} 미매칭`);
185
- assert.equal(matched.mcp_route, expectedRoute);
186
- }
187
- });
188
-
189
- test("matchRules: 일반 대화는 매칭 없음", () => {
190
- const compiledRules = loadCompiledRules();
191
- const matches = matchRules(compiledRules, sanitizeForKeywordDetection("오늘 점심 메뉴 추천해줘"));
192
- assert.deepEqual(matches, []);
193
- });
194
-
195
- test("resolveConflicts: priority 정렬 및 supersedes 처리", () => {
196
- const resolved = resolveConflicts([
197
- { id: "rule-c", priority: 3, supersedes: [], exclusive: false },
198
- { id: "rule-b", priority: 2, supersedes: ["rule-c"], exclusive: false },
199
- { id: "rule-a", priority: 1, supersedes: [], exclusive: false },
200
- { id: "rule-a", priority: 1, supersedes: [], exclusive: false }
201
- ]);
202
-
203
- assert.deepEqual(
204
- resolved.map((rule) => rule.id),
205
- ["rule-a", "rule-b"]
206
- );
207
- });
208
-
209
- test("resolveConflicts: exclusive 처리", () => {
210
- const resolved = resolveConflicts([
211
- { id: "normal", priority: 1, supersedes: [], exclusive: false },
212
- { id: "exclusive", priority: 0, supersedes: [], exclusive: true },
213
- { id: "later", priority: 2, supersedes: [], exclusive: false }
214
- ]);
215
-
216
- assert.deepEqual(resolved.map((rule) => rule.id), ["exclusive"]);
217
- });
218
-
219
- test("코드블록 내 키워드: sanitize 후 매칭 안 됨", () => {
220
- const compiledRules = loadCompiledRules();
221
- const input = ["```txt", "tfx multi", "jira 이슈 생성", "```"].join("\n");
222
- const clean = sanitizeForKeywordDetection(input);
223
- const matches = matchRules(compiledRules, clean);
224
- assert.deepEqual(matches, []);
225
- });
226
-
227
- test("OMC 키워드와 triflux 키워드 비간섭 + TRIFLUX 네임스페이스", () => {
228
- const omcLike = runDetector("my tfx multi 세션 보여줘");
229
- assert.equal(omcLike.suppressOutput, true);
230
-
231
- const triflux = runDetector("tfx multi 세션 시작");
232
- const additionalContext = triflux?.hookSpecificOutput?.additionalContext || "";
233
- assert.match(additionalContext, /^\[TRIFLUX MAGIC KEYWORD: tfx-multi\]/);
234
- });
1
+ import assert from "node:assert/strict";
2
+ import { spawnSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import test from "node:test";
7
+ import { fileURLToPath } from "node:url";
8
+ import { compileRules, loadRules, matchRules, resolveConflicts } from "../lib/keyword-rules.mjs";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const projectRoot = resolve(__dirname, "..", "..");
12
+ const rulesPath = join(projectRoot, "hooks", "keyword-rules.json");
13
+ const detectorScriptPath = join(projectRoot, "scripts", "keyword-detector.mjs");
14
+
15
+ // keyword-detector는 import 시 main()이 실행되므로, 테스트 로딩 단계에서만 안전하게 비활성화한다.
16
+ const previousDisable = process.env.TRIFLUX_DISABLE_MAGICWORDS;
17
+ const previousLog = console.log;
18
+ process.env.TRIFLUX_DISABLE_MAGICWORDS = "1";
19
+ console.log = () => {};
20
+ const detectorModule = await import("../keyword-detector.mjs");
21
+ console.log = previousLog;
22
+ if (previousDisable === undefined) {
23
+ delete process.env.TRIFLUX_DISABLE_MAGICWORDS;
24
+ } else {
25
+ process.env.TRIFLUX_DISABLE_MAGICWORDS = previousDisable;
26
+ }
27
+
28
+ const { extractPrompt, sanitizeForKeywordDetection } = detectorModule;
29
+
30
+ function loadCompiledRules() {
31
+ const rules = loadRules(rulesPath);
32
+ assert.equal(rules.length, 23);
33
+ return compileRules(rules);
34
+ }
35
+
36
+ function runDetector(prompt) {
37
+ const payload = { prompt, cwd: projectRoot };
38
+ const result = spawnSync(process.execPath, [detectorScriptPath], {
39
+ input: JSON.stringify(payload),
40
+ encoding: "utf8"
41
+ });
42
+
43
+ assert.equal(result.status, 0, result.stderr);
44
+ assert.ok(result.stdout.trim(), "keyword-detector 출력이 비어 있습니다.");
45
+ return JSON.parse(result.stdout.trim());
46
+ }
47
+
48
+ test("extractPrompt: prompt/message.content/parts[].text 우선순위", () => {
49
+ assert.equal(
50
+ extractPrompt({
51
+ prompt: "from prompt",
52
+ message: { content: "from message" },
53
+ parts: [{ text: "from parts" }]
54
+ }),
55
+ "from prompt"
56
+ );
57
+
58
+ assert.equal(
59
+ extractPrompt({
60
+ prompt: " ",
61
+ message: { content: "from message" },
62
+ parts: [{ text: "from parts" }]
63
+ }),
64
+ "from message"
65
+ );
66
+
67
+ assert.equal(
68
+ extractPrompt({
69
+ message: { content: [{ text: "from message-part" }] },
70
+ parts: [{ text: "from parts" }]
71
+ }),
72
+ "from message-part"
73
+ );
74
+
75
+ assert.equal(extractPrompt({ parts: [{ text: "from parts" }] }), "from parts");
76
+ });
77
+
78
+ test("sanitizeForKeywordDetection: 코드블록/URL/파일경로/XML 태그 제거", () => {
79
+ const input = [
80
+ "정상 문장",
81
+ "```sh",
82
+ "tfx multi",
83
+ "```",
84
+ "https://example.com/path?q=1",
85
+ "C:\\Users\\SSAFY\\Desktop\\Projects\\tools\\triflux",
86
+ "./hooks/keyword-rules.json",
87
+ "<tag>jira 이슈 생성</tag>"
88
+ ].join("\n");
89
+
90
+ const sanitized = sanitizeForKeywordDetection(input);
91
+
92
+ assert.ok(sanitized.includes("정상 문장"));
93
+ assert.ok(!sanitized.includes("tfx multi"));
94
+ assert.ok(!sanitized.includes("https://"));
95
+ assert.ok(!sanitized.includes("C:\\Users\\"));
96
+ assert.ok(!sanitized.includes("./hooks/keyword-rules.json"));
97
+ assert.ok(!sanitized.includes("<tag>"));
98
+ assert.ok(!sanitized.includes("jira 이슈 생성"));
99
+ });
100
+
101
+ test("loadRules: 유효한 JSON 로드", () => {
102
+ const rules = loadRules(rulesPath);
103
+ assert.equal(rules.length, 23);
104
+ assert.equal(rules.filter((rule) => rule.skill).length, 10);
105
+ assert.equal(rules.filter((rule) => rule.mcp_route).length, 10);
106
+ });
107
+
108
+ test("loadRules: 잘못된 파일 처리", () => {
109
+ const tempDir = mkdtempSync(join(tmpdir(), "triflux-rules-"));
110
+ const invalidPath = join(tempDir, "invalid.json");
111
+ writeFileSync(invalidPath, "{ invalid json", "utf8");
112
+
113
+ const malformed = loadRules(invalidPath);
114
+ const missing = loadRules(join(tempDir, "missing.json"));
115
+
116
+ assert.deepEqual(malformed, []);
117
+ assert.deepEqual(missing, []);
118
+
119
+ rmSync(tempDir, { recursive: true, force: true });
120
+ });
121
+
122
+ test("compileRules: 정규식 컴파일 성공", () => {
123
+ const rules = loadRules(rulesPath);
124
+ const compiled = compileRules(rules);
125
+ assert.equal(compiled.length, 23);
126
+ for (const rule of compiled) {
127
+ assert.ok(Array.isArray(rule.compiledPatterns));
128
+ assert.ok(rule.compiledPatterns.length > 0);
129
+ for (const pattern of rule.compiledPatterns) {
130
+ assert.ok(pattern instanceof RegExp);
131
+ }
132
+ }
133
+ });
134
+
135
+ test("compileRules: 정규식 컴파일 실패", () => {
136
+ const compiled = compileRules([
137
+ {
138
+ id: "bad-pattern",
139
+ priority: 1,
140
+ patterns: [{ source: "[", flags: "" }],
141
+ skill: "tfx-multi",
142
+ supersedes: [],
143
+ exclusive: false,
144
+ state: null,
145
+ mcp_route: null
146
+ }
147
+ ]);
148
+
149
+ assert.deepEqual(compiled, []);
150
+ });
151
+
152
+ test("matchRules: tfx 키워드 매칭", () => {
153
+ const compiledRules = loadCompiledRules();
154
+ const cases = [
155
+ { text: "tfx multi 세션 시작", expectedId: "tfx-multi" },
156
+ { text: "tfx auto 돌려줘", expectedId: "tfx-auto" },
157
+ { text: "tfx codex 로 실행", expectedId: "tfx-codex" },
158
+ { text: "tfx gemini 로 실행", expectedId: "tfx-gemini" },
159
+ { text: "canceltfx", expectedId: "tfx-cancel" }
160
+ ];
161
+
162
+ for (const { text, expectedId } of cases) {
163
+ const clean = sanitizeForKeywordDetection(text);
164
+ const matches = matchRules(compiledRules, clean);
165
+ assert.ok(matches.some((match) => match.id === expectedId), `${text} => ${expectedId} 미매칭`);
166
+ }
167
+ });
168
+
169
+ test("matchRules: MCP 라우팅 매칭", () => {
170
+ const compiledRules = loadCompiledRules();
171
+ const cases = [
172
+ { text: "노션 페이지 조회해줘", expectedId: "notion-route", expectedRoute: "gemini" },
173
+ { text: "jira 이슈 생성", expectedId: "jira-route", expectedRoute: "codex" },
174
+ { text: "크롬 열고 로그인", expectedId: "chrome-route", expectedRoute: "gemini" },
175
+ { text: "이메일 보내줘", expectedId: "mail-route", expectedRoute: "gemini" },
176
+ { text: "캘린더 일정 생성", expectedId: "calendar-route", expectedRoute: "gemini" },
177
+ { text: "playwright 테스트 작성", expectedId: "playwright-route", expectedRoute: "gemini" },
178
+ { text: "canva 디자인 생성", expectedId: "canva-route", expectedRoute: "gemini" }
179
+ ];
180
+
181
+ for (const { text, expectedId, expectedRoute } of cases) {
182
+ const matches = matchRules(compiledRules, sanitizeForKeywordDetection(text));
183
+ const matched = matches.find((match) => match.id === expectedId);
184
+ assert.ok(matched, `${text} => ${expectedId} 미매칭`);
185
+ assert.equal(matched.mcp_route, expectedRoute);
186
+ }
187
+ });
188
+
189
+ test("matchRules: 일반 대화는 매칭 없음", () => {
190
+ const compiledRules = loadCompiledRules();
191
+ const matches = matchRules(compiledRules, sanitizeForKeywordDetection("오늘 점심 메뉴 추천해줘"));
192
+ assert.deepEqual(matches, []);
193
+ });
194
+
195
+ test("resolveConflicts: priority 정렬 및 supersedes 처리", () => {
196
+ const resolved = resolveConflicts([
197
+ { id: "rule-c", priority: 3, supersedes: [], exclusive: false },
198
+ { id: "rule-b", priority: 2, supersedes: ["rule-c"], exclusive: false },
199
+ { id: "rule-a", priority: 1, supersedes: [], exclusive: false },
200
+ { id: "rule-a", priority: 1, supersedes: [], exclusive: false }
201
+ ]);
202
+
203
+ assert.deepEqual(
204
+ resolved.map((rule) => rule.id),
205
+ ["rule-a", "rule-b"]
206
+ );
207
+ });
208
+
209
+ test("resolveConflicts: exclusive 처리", () => {
210
+ const resolved = resolveConflicts([
211
+ { id: "normal", priority: 1, supersedes: [], exclusive: false },
212
+ { id: "exclusive", priority: 0, supersedes: [], exclusive: true },
213
+ { id: "later", priority: 2, supersedes: [], exclusive: false }
214
+ ]);
215
+
216
+ assert.deepEqual(resolved.map((rule) => rule.id), ["exclusive"]);
217
+ });
218
+
219
+ test("코드블록 내 키워드: sanitize 후 매칭 안 됨", () => {
220
+ const compiledRules = loadCompiledRules();
221
+ const input = ["```txt", "tfx multi", "jira 이슈 생성", "```"].join("\n");
222
+ const clean = sanitizeForKeywordDetection(input);
223
+ const matches = matchRules(compiledRules, clean);
224
+ assert.deepEqual(matches, []);
225
+ });
226
+
227
+ test("OMC 키워드와 triflux 키워드 비간섭 + TRIFLUX 네임스페이스", () => {
228
+ const omcLike = runDetector("my tfx multi 세션 보여줘");
229
+ assert.equal(omcLike.suppressOutput, true);
230
+
231
+ const triflux = runDetector("tfx multi 세션 시작");
232
+ const additionalContext = triflux?.hookSpecificOutput?.additionalContext || "";
233
+ assert.match(additionalContext, /^\[TRIFLUX MAGIC KEYWORD: tfx-multi\]/);
234
+ });
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env bash
2
+ # headless-guard-fast.sh — bash pre-filter for headless-guard.mjs
3
+ # psmux 미설치(캐시 ok=false) 시 Node.js 기동을 생략하여 89ms→~2ms로 단축
4
+ CACHE="${TMPDIR:-${TEMP:-/tmp}}/tfx-psmux-check.json"
5
+ GUARD_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+
7
+ if [[ -f "$CACHE" ]]; then
8
+ # jq 없이 순수 bash로 파싱
9
+ ok_val=$(grep -o '"ok":[[:space:]]*\(true\|false\)' "$CACHE" | grep -o 'true\|false')
10
+ ts_val=$(grep -o '"ts":[[:space:]]*[0-9]*' "$CACHE" | grep -o '[0-9]*')
11
+ now_ms=$(($(date +%s) * 1000))
12
+ age_ms=$((now_ms - ${ts_val:-0}))
13
+
14
+ # 캐시 유효(5분 이내) + psmux 미설치 → 즉시 통과
15
+ if [[ "$ok_val" == "false" && $age_ms -lt 300000 ]]; then
16
+ exit 0
17
+ fi
18
+ fi
19
+
20
+ # 캐시 미스 또는 psmux 설치됨 → Node.js 실행
21
+ exec node "$GUARD_DIR/headless-guard.mjs"
@@ -82,11 +82,11 @@ function parseRouteCommand(cmd) {
82
82
 
83
83
  // v3: 원본 명령에서 추가 플래그 추출
84
84
  const flags = {};
85
- const timeoutMatch = cmd.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
85
+ const afterPrompt = cmd.replace(/'.+?'/gs, "").replace(/".+?"/gs, "");
86
+ const timeoutMatch = afterPrompt.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
86
87
  if (timeoutMatch) flags.timeout = parseInt(timeoutMatch[1], 10);
87
88
 
88
89
  // 환경변수 기반 글로벌 플래그
89
- if (process.env.TFX_DASHBOARD === "1") flags.dashboard = true;
90
90
  if (process.env.TFX_VERBOSE === "1") flags.verbose = true;
91
91
  if (process.env.TFX_NO_AUTO_ATTACH === "1") flags.noAutoAttach = true;
92
92
 
@@ -116,6 +116,11 @@ async function main() {
116
116
  let raw = "";
117
117
  for await (const chunk of process.stdin) raw += chunk;
118
118
 
119
+ if (!raw || !raw.trim()) {
120
+ console.error('[headless-guard] stdin이 비어있습니다 — 기본 허용');
121
+ process.exit(0);
122
+ }
123
+
119
124
  let input;
120
125
  try {
121
126
  input = JSON.parse(raw);
@@ -157,6 +162,15 @@ async function main() {
157
162
 
158
163
  const parsed = parseRouteCommand(cmd);
159
164
  if (parsed) {
165
+ // P1a: 단일 워커는 headless 변환 건너뛰기 (직접 실행이 523~1173ms 절약)
166
+ // TFX_FORCE_HEADLESS=1이면 단일이어도 headless 변환 강제
167
+ if (!process.env.TFX_FORCE_HEADLESS) {
168
+ const isMultiWorker = /\s--(multi|parallel)\b/.test(cmd);
169
+ if (!isMultiWorker) {
170
+ process.exit(0); // 원본 tfx-route.sh 명령 그대로 통과
171
+ }
172
+ }
173
+
160
174
  const safePrompt = parsed.prompt.replace(/'/g, "'\\''");
161
175
  const VALID_MCP = new Set(["implement", "analyze", "review", "docs"]);
162
176
  const f = parsed.flags || {};
@@ -164,7 +178,7 @@ async function main() {
164
178
  // v3: 플래그 빌더 — 하드코딩 제거, 원본 의도 보존
165
179
  const parts = ["tfx multi --teammate-mode headless"];
166
180
  if (!f.noAutoAttach) parts.push("--auto-attach");
167
- if (f.dashboard) parts.push("--dashboard");
181
+ if (!f.noAutoAttach) parts.push("--dashboard"); // 워커 요약 스플릿이 기본
168
182
  if (f.verbose) parts.push("--verbose");
169
183
  parts.push(`--assign '${parsed.agent}:${safePrompt}:${parsed.agent}'`);
170
184
  if (parsed.mcp && VALID_MCP.has(parsed.mcp)) parts.push(`--mcp-profile ${parsed.mcp}`);
@@ -173,7 +187,7 @@ async function main() {
173
187
  const builtCmd = parts.join(" ");
174
188
  autoRoute(
175
189
  builtCmd,
176
- `[headless-guard] auto-route: tfx-route.sh ${parsed.agent} → headless. mcp=${parsed.mcp} dashboard=${!!f.dashboard}`,
190
+ `[headless-guard] auto-route: tfx-route.sh ${parsed.agent} → headless. mcp=${parsed.mcp} dashboard=${!f.noAutoAttach}`,
177
191
  );
178
192
  }
179
193
  deny(
@@ -183,10 +197,16 @@ async function main() {
183
197
  }
184
198
  }
185
199
 
186
- // ── Agent: CLI 워커 래핑 → deny ──
200
+ // ── Agent: CLI 워커 래핑 → deny (Claude native는 통과) ──
187
201
  if (toolName === "Agent") {
188
- const combined = `${(toolInput.prompt || "").toLowerCase()} ${(toolInput.description || "").toLowerCase()}`;
202
+ const subType = (toolInput.subagent_type || "").toLowerCase();
203
+ // Claude native subagent types → 무조건 통과
204
+ const NATIVE_TYPES = new Set(["explore", "plan", "general-purpose", ""]);
205
+ if (NATIVE_TYPES.has(subType)) process.exit(0);
206
+ // oh-my-claudecode 계열도 통과
207
+ if (subType.startsWith("oh-my-claudecode:")) process.exit(0);
189
208
 
209
+ const combined = `${(toolInput.prompt || "").toLowerCase()} ${(toolInput.description || "").toLowerCase()}`;
190
210
  const cliPatterns = [
191
211
  /codex\s+(exec|run|실행)/,
192
212
  /gemini\s+(-p|run|실행)/,