@triflux/core 10.48.1 → 10.50.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 (46) hide show
  1. package/hooks/agy-session-hook.mjs +1 -1
  2. package/hooks/claude-cwd-projection-refresh.mjs +7 -79
  3. package/hooks/codex-session-hook.mjs +3 -8
  4. package/hooks/hook-orchestrator.mjs +8 -648
  5. package/hooks/lib/resolve-root.mjs +1 -1
  6. package/hub/bridge.mjs +290 -51
  7. package/hub/team/claude-daemon-control.mjs +54 -6
  8. package/hub/team/claude-transcript.mjs +240 -0
  9. package/hub/team/session-context.mjs +203 -0
  10. package/hud/constants.mjs +10 -0
  11. package/hud/hud-qos-status.mjs +23 -4
  12. package/hud/providers/gemini.mjs +4 -1
  13. package/hud/renderers.mjs +10 -6
  14. package/package.json +2 -2
  15. package/scripts/lib/doctor-env-checks.mjs +0 -85
  16. package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
  17. package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
  18. package/scripts/lib/mcp-guard-engine.mjs +44 -271
  19. package/scripts/lib/mcp-manifest.mjs +2 -2
  20. package/scripts/lib/session-presence.mjs +249 -0
  21. package/hooks/agent-route-guard.mjs +0 -113
  22. package/hooks/cross-review-tracker.mjs +0 -135
  23. package/hooks/cto-north-star-brief.mjs +0 -133
  24. package/hooks/error-context.mjs +0 -185
  25. package/hooks/hook-adaptive-collector.mjs +0 -88
  26. package/hooks/hook-manager.mjs +0 -426
  27. package/hooks/hook-registry.json +0 -367
  28. package/hooks/hooks.json +0 -149
  29. package/hooks/keyword-rules.json +0 -878
  30. package/hooks/mcp-config-watcher.mjs +0 -84
  31. package/hooks/permission-safe-allow.mjs +0 -267
  32. package/hooks/pipeline-stop.mjs +0 -435
  33. package/hooks/post-tool-tips.mjs +0 -306
  34. package/hooks/pre-compact-snapshot.mjs +0 -181
  35. package/hooks/safety-guard.mjs +0 -466
  36. package/hooks/session-end-cleanup.mjs +0 -360
  37. package/hooks/session-start-fast.mjs +0 -579
  38. package/hooks/session-start-lake.mjs +0 -81
  39. package/hooks/subagent-tracker.mjs +0 -281
  40. package/hooks/subagent-verifier.mjs +0 -85
  41. package/scripts/lib/cross-review-utils.mjs +0 -59
  42. package/scripts/lib/hook-utils.mjs +0 -16
  43. package/scripts/lib/keyword-rules.mjs +0 -219
  44. package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
  45. package/scripts/lib/mcp-gateway-servers.mjs +0 -70
  46. package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
@@ -1,281 +0,0 @@
1
- #!/usr/bin/env node
2
- // hooks/subagent-tracker.mjs — SubagentStart/SubagentStop lifecycle tracker
3
- //
4
- // Maintains a small repo-local JSON store of running and recently completed
5
- // subagents so paired lifecycle hooks can add bounded context without blocking.
6
-
7
- import {
8
- mkdirSync,
9
- readFileSync,
10
- renameSync,
11
- rmSync,
12
- writeFileSync,
13
- } from "node:fs";
14
- import { dirname, join } from "node:path";
15
-
16
- const STATE_VERSION = 1;
17
- const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
18
- const MAX_COMPLETED = 20;
19
- // Lifecycle hooks are best-effort, but under CI/Linux process contention a
20
- // 30-way SubagentStart burst can legitimately take around a second to drain.
21
- // Keep this bounded so hooks do not hang indefinitely, while avoiding silent
22
- // lifecycle drops during normal high-concurrency fan-out.
23
- const DEFAULT_LOCK_TIMEOUT_MS = 3000;
24
- const LOCK_RETRY_MS = 20;
25
-
26
- function readStdin() {
27
- try {
28
- return readFileSync(0, "utf8");
29
- } catch {
30
- return "";
31
- }
32
- }
33
-
34
- function isObject(value) {
35
- return value !== null && typeof value === "object" && !Array.isArray(value);
36
- }
37
-
38
- function stateDirFromEnv(env = process.env, cwd = process.cwd()) {
39
- return (
40
- env.TRIFLUX_SUBAGENT_STATE_DIR ||
41
- join(env.CLAUDE_CWD || cwd, ".triflux", "subagents")
42
- );
43
- }
44
-
45
- function emptyState(now = new Date()) {
46
- return {
47
- version: STATE_VERSION,
48
- updatedAt: now.toISOString(),
49
- agents: {},
50
- completed: [],
51
- };
52
- }
53
-
54
- function normalizeState(value, now) {
55
- const state = isObject(value) ? value : {};
56
- return {
57
- version: STATE_VERSION,
58
- updatedAt:
59
- typeof state.updatedAt === "string" ? state.updatedAt : now.toISOString(),
60
- agents: isObject(state.agents) ? state.agents : {},
61
- completed: Array.isArray(state.completed) ? state.completed : [],
62
- };
63
- }
64
-
65
- function readState(path, now) {
66
- try {
67
- return normalizeState(JSON.parse(readFileSync(path, "utf8")), now);
68
- } catch {
69
- return emptyState(now);
70
- }
71
- }
72
-
73
- function writeState(path, state) {
74
- mkdirSync(dirname(path), { recursive: true });
75
- const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
76
- writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
77
- renameSync(tmpPath, path);
78
- }
79
-
80
- function sleepSync(ms) {
81
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
82
- }
83
-
84
- function withStateLock(statePath, fn, timeoutMs = DEFAULT_LOCK_TIMEOUT_MS) {
85
- const lockDir = `${statePath}.lock`;
86
- const started = Date.now();
87
- while (true) {
88
- try {
89
- mkdirSync(lockDir, { recursive: false });
90
- try {
91
- return fn();
92
- } finally {
93
- rmSync(lockDir, { recursive: true, force: true });
94
- }
95
- } catch (error) {
96
- if (error?.code !== "EEXIST") throw error;
97
- if (Date.now() - started >= timeoutMs) return undefined;
98
- sleepSync(LOCK_RETRY_MS);
99
- }
100
- }
101
- }
102
-
103
- function lockTimeoutMsFromOptions(opts = {}) {
104
- if (Number.isFinite(opts.lockTimeoutMs)) return opts.lockTimeoutMs;
105
- const raw = opts.env?.TRIFLUX_SUBAGENT_LOCK_TIMEOUT_MS;
106
- if (raw === undefined || raw === "") return DEFAULT_LOCK_TIMEOUT_MS;
107
- const parsed = Number(raw);
108
- return Number.isFinite(parsed) && parsed > 0
109
- ? Math.trunc(parsed)
110
- : DEFAULT_LOCK_TIMEOUT_MS;
111
- }
112
-
113
- function lifecycleKey(input) {
114
- if (typeof input.agent_id === "string" && input.agent_id.trim()) {
115
- return input.agent_id;
116
- }
117
- if (typeof input.subagent_id === "string" && input.subagent_id.trim()) {
118
- return input.subagent_id;
119
- }
120
- const sessionId =
121
- typeof input.session_id === "string" && input.session_id.trim()
122
- ? input.session_id
123
- : "unknown";
124
- const agentType =
125
- (typeof input.agent_type === "string" && input.agent_type.trim()) ||
126
- (typeof input.subagent_type === "string" && input.subagent_type.trim()) ||
127
- "unknown";
128
- return `${sessionId}:${agentType}`;
129
- }
130
-
131
- function agentType(input, existing) {
132
- return (
133
- (typeof input.agent_type === "string" && input.agent_type.trim()) ||
134
- (typeof input.subagent_type === "string" && input.subagent_type.trim()) ||
135
- existing?.agentType ||
136
- "unknown"
137
- );
138
- }
139
-
140
- function expectedDeliverables(input) {
141
- if (input.expectedDeliverables !== undefined)
142
- return input.expectedDeliverables;
143
- if (input.expected_deliverables !== undefined)
144
- return input.expected_deliverables;
145
- if (input.expected_deliverable !== undefined)
146
- return input.expected_deliverable;
147
- return undefined;
148
- }
149
-
150
- function cleanupStaleAgents(state, nowMs, ttlMs) {
151
- for (const [key, agent] of Object.entries(state.agents)) {
152
- const startedMs = Date.parse(agent?.startedAt || "");
153
- if (!Number.isFinite(startedMs) || nowMs - startedMs > ttlMs) {
154
- delete state.agents[key];
155
- }
156
- }
157
- }
158
-
159
- function boundedCompleted(completed) {
160
- return completed.slice(-MAX_COMPLETED);
161
- }
162
-
163
- function completionSummary({ key, input, running, now, durationMs }) {
164
- const summary = {
165
- key,
166
- stoppedAt: now.toISOString(),
167
- sessionId:
168
- (typeof input.session_id === "string" && input.session_id.trim()) ||
169
- running?.sessionId ||
170
- undefined,
171
- agentType: agentType(input, running),
172
- durationMs,
173
- };
174
- if (running?.startedAt) summary.startedAt = running.startedAt;
175
- if (running?.expectedDeliverables !== undefined) {
176
- summary.expectedDeliverables = running.expectedDeliverables;
177
- }
178
- return Object.fromEntries(
179
- Object.entries(summary).filter(([, value]) => value !== undefined),
180
- );
181
- }
182
-
183
- function stopOutput({ summary, runningCount, completedCount }) {
184
- const message =
185
- `[subagent-tracker] ${summary.agentType} ` +
186
- `duration=${summary.durationMs}ms ` +
187
- `running=${runningCount} completed=${completedCount}`;
188
- return { systemMessage: message };
189
- }
190
-
191
- export function recordLifecycle(input, opts = {}) {
192
- if (!isObject(input)) return undefined;
193
- const eventName = input.hook_event_name;
194
- if (eventName !== "SubagentStart" && eventName !== "SubagentStop") {
195
- return undefined;
196
- }
197
-
198
- const now = opts.now instanceof Date ? opts.now : new Date();
199
- const nowMs = now.getTime();
200
- const ttlMs = Number.isFinite(opts.ttlMs) ? opts.ttlMs : DEFAULT_TTL_MS;
201
- const stateDir = opts.stateDir || stateDirFromEnv(opts.env, opts.cwd);
202
- const statePath = join(stateDir, "subagents.json");
203
- mkdirSync(stateDir, { recursive: true });
204
- return withStateLock(
205
- statePath,
206
- () => {
207
- const state = readState(statePath, now);
208
-
209
- cleanupStaleAgents(state, nowMs, ttlMs);
210
-
211
- const key = lifecycleKey(input);
212
- let output;
213
-
214
- if (eventName === "SubagentStart") {
215
- const existing = state.agents[key];
216
- const deliverables = expectedDeliverables(input);
217
- state.agents[key] = {
218
- key,
219
- startedAt: existing?.startedAt || now.toISOString(),
220
- sessionId:
221
- (typeof input.session_id === "string" && input.session_id.trim()) ||
222
- existing?.sessionId ||
223
- undefined,
224
- agentType: agentType(input, existing),
225
- ...(deliverables !== undefined
226
- ? { expectedDeliverables: deliverables }
227
- : existing?.expectedDeliverables !== undefined
228
- ? { expectedDeliverables: existing.expectedDeliverables }
229
- : {}),
230
- };
231
- } else {
232
- const running = state.agents[key];
233
- if (running) delete state.agents[key];
234
- const startedMs = Date.parse(running?.startedAt || "");
235
- const durationMs = Number.isFinite(startedMs)
236
- ? Math.max(0, nowMs - startedMs)
237
- : 0;
238
- const summary = completionSummary({
239
- key,
240
- input,
241
- running,
242
- now,
243
- durationMs,
244
- });
245
- state.completed = boundedCompleted([...state.completed, summary]);
246
- output = stopOutput({
247
- summary,
248
- runningCount: Object.keys(state.agents).length,
249
- completedCount: state.completed.length,
250
- });
251
- }
252
-
253
- state.updatedAt = now.toISOString();
254
- state.completed = boundedCompleted(state.completed);
255
- writeState(statePath, state);
256
- return output;
257
- },
258
- lockTimeoutMsFromOptions(opts),
259
- );
260
- }
261
-
262
- function main() {
263
- const raw = readStdin();
264
- if (!raw.trim()) return;
265
-
266
- let input;
267
- try {
268
- input = JSON.parse(raw);
269
- } catch {
270
- return;
271
- }
272
-
273
- const output = recordLifecycle(input);
274
- if (output) process.stdout.write(JSON.stringify(output));
275
- }
276
-
277
- try {
278
- main();
279
- } catch {
280
- process.exit(0);
281
- }
@@ -1,85 +0,0 @@
1
- #!/usr/bin/env node
2
- // hooks/subagent-verifier.mjs — SubagentStop 훅
3
- //
4
- // 서브에이전트 완료 시 결과 품질을 체크한다:
5
- // - 빈 결과 감지 → 재시도 제안
6
- // - 에러 종료 감지 → 원인 분석 컨텍스트 주입
7
- // - 과도한 토큰 사용 감지 → 효율성 알림
8
-
9
- import { readFileSync } from "node:fs";
10
-
11
- function readStdin() {
12
- try {
13
- return readFileSync(0, "utf8");
14
- } catch {
15
- return "";
16
- }
17
- }
18
-
19
- function main() {
20
- const raw = readStdin();
21
- if (!raw.trim()) process.exit(0);
22
-
23
- let input;
24
- try {
25
- input = JSON.parse(raw);
26
- } catch {
27
- process.exit(0);
28
- }
29
-
30
- if (input.hook_event_name && input.hook_event_name !== "SubagentStop") {
31
- process.exit(0);
32
- }
33
-
34
- const agentType = input.agent_type || input.subagent_type || "unknown";
35
- const result = input.tool_output || input.result || "";
36
- const resultStr =
37
- typeof result === "string" ? result : JSON.stringify(result);
38
-
39
- const issues = [];
40
-
41
- // 1. 빈 결과 체크
42
- if (!resultStr.trim() || resultStr.trim().length < 20) {
43
- issues.push(
44
- `서브에이전트(${agentType})가 거의 빈 결과를 반환했습니다. ` +
45
- "프롬프트를 더 구체적으로 작성하거나, 다른 subagent_type을 시도하세요.",
46
- );
47
- }
48
-
49
- // 2. 에러 키워드 감지
50
- const errorPatterns = [
51
- /error:|exception:|traceback|failed to|fatal:/i,
52
- /❌|FAILED|ERROR/,
53
- ];
54
- const hasError = errorPatterns.some((p) => p.test(resultStr));
55
- if (hasError && resultStr.length > 50) {
56
- issues.push(
57
- `서브에이전트(${agentType}) 결과에 에러 신호가 감지되었습니다. ` +
58
- "결과를 검토하고, 필요 시 다른 접근 방식을 사용하세요.",
59
- );
60
- }
61
-
62
- // 3. 결과가 너무 길면 요약 필요 알림
63
- if (resultStr.length > 15000) {
64
- issues.push(
65
- `서브에이전트(${agentType}) 결과가 ${Math.round(resultStr.length / 1000)}K 자입니다. ` +
66
- "핵심만 추출하여 컨텍스트 윈도우를 절약하세요.",
67
- );
68
- }
69
-
70
- if (issues.length === 0) process.exit(0);
71
-
72
- const output = {
73
- systemMessage:
74
- `[subagent-verifier] ${agentType} 완료 — 주의사항:\n` +
75
- issues.map((i) => ` → ${i}`).join("\n"),
76
- };
77
-
78
- process.stdout.write(JSON.stringify(output));
79
- }
80
-
81
- try {
82
- main();
83
- } catch {
84
- process.exit(0);
85
- }
@@ -1,59 +0,0 @@
1
- import { join } from "node:path";
2
-
3
- export const SESSION_TTL_SEC = 30 * 60;
4
- export const STATE_REL_PATH = join(".triflux", "state", "cross-review.json");
5
-
6
- export function readStdin() {
7
- return new Promise((resolve) => {
8
- let raw = "";
9
- process.stdin.setEncoding("utf8");
10
- process.stdin.on("data", (chunk) => {
11
- raw += chunk;
12
- });
13
- process.stdin.on("end", () => resolve(raw));
14
- process.stdin.on("error", () => resolve(""));
15
- });
16
- }
17
-
18
- export function parseJson(raw) {
19
- try {
20
- return JSON.parse(raw);
21
- } catch {
22
- return null;
23
- }
24
- }
25
-
26
- export function nowSec() {
27
- return Math.floor(Date.now() / 1000);
28
- }
29
-
30
- export function resolveBaseDir(payload) {
31
- if (typeof payload?.cwd === "string" && payload.cwd.trim())
32
- return payload.cwd;
33
- if (typeof payload?.directory === "string" && payload.directory.trim())
34
- return payload.directory;
35
- return process.cwd();
36
- }
37
-
38
- export function shouldTrackPath(filePath) {
39
- if (typeof filePath !== "string" || !filePath.trim()) return false;
40
-
41
- const lower = filePath.toLowerCase();
42
- if (
43
- lower.startsWith(".triflux/") ||
44
- lower.startsWith(".omc/") ||
45
- lower.startsWith(".claude/")
46
- )
47
- return false;
48
- if (lower === "package-lock.json" || lower.endsWith("/package-lock.json"))
49
- return false;
50
- if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
51
- return true;
52
- }
53
-
54
- export function expectedReviewer(author) {
55
- if (author === "claude") return "codex";
56
- if (author === "codex") return "claude";
57
- if (author === "gemini") return "claude";
58
- return "";
59
- }
@@ -1,16 +0,0 @@
1
- export function nudge(message) {
2
- process.stdout.write(
3
- JSON.stringify({
4
- hookSpecificOutput: {
5
- hookEventName: "PreToolUse",
6
- additionalContext: message,
7
- },
8
- }),
9
- );
10
- process.exit(0);
11
- }
12
-
13
- export function deny(reason) {
14
- process.stderr.write(reason);
15
- process.exit(2);
16
- }
@@ -1,219 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
-
3
- const VALID_MCP_ROUTES = new Set(["codex", "antigravity", "gemini", "claude"]);
4
-
5
- function logRuleError(message, error) {
6
- if (error) {
7
- console.error(`[triflux-keyword-rules] ${message}: ${error.message}`);
8
- return;
9
- }
10
- console.error(`[triflux-keyword-rules] ${message}`);
11
- }
12
-
13
- function normalizePattern(pattern) {
14
- if (!pattern || typeof pattern.source !== "string") return null;
15
- if (typeof pattern.flags !== "string") return null;
16
- return { source: pattern.source, flags: pattern.flags };
17
- }
18
-
19
- function normalizeState(state) {
20
- if (state == null) return null;
21
- if (typeof state !== "object") return null;
22
- if (typeof state.activate !== "boolean") return null;
23
- if (typeof state.name !== "string" || !state.name.trim()) return null;
24
- return { activate: state.activate, name: state.name.trim() };
25
- }
26
-
27
- function normalizeRule(rule) {
28
- if (!rule || typeof rule !== "object") return null;
29
- if (rule.disabled === true) return null;
30
- if (typeof rule.id !== "string" || !rule.id.trim()) return null;
31
- if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) return null;
32
- if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority))
33
- return null;
34
-
35
- const patterns = rule.patterns.map(normalizePattern).filter(Boolean);
36
- if (patterns.length === 0) return null;
37
-
38
- const skill =
39
- typeof rule.skill === "string" && rule.skill.trim()
40
- ? rule.skill.trim()
41
- : null;
42
- const action =
43
- typeof rule.action === "string" && rule.action.trim()
44
- ? rule.action.trim()
45
- : null;
46
- const mcpRoute =
47
- typeof rule.mcp_route === "string" && VALID_MCP_ROUTES.has(rule.mcp_route)
48
- ? rule.mcp_route
49
- : null;
50
-
51
- if (!skill && !mcpRoute && !action) return null;
52
-
53
- const hint =
54
- typeof rule.hint === "string" && rule.hint.trim() ? rule.hint.trim() : null;
55
-
56
- const supersedes = Array.isArray(rule.supersedes)
57
- ? rule.supersedes
58
- .filter((id) => typeof id === "string" && id.trim())
59
- .map((id) => id.trim())
60
- : [];
61
-
62
- const state = normalizeState(rule.state);
63
- if (rule.state != null && state == null) return null;
64
-
65
- // repo_scope: path segment 화이트리스트 (빈 배열 = 전역). explicit: 종결 라우팅 마커.
66
- const repoScope = Array.isArray(rule.repo_scope)
67
- ? rule.repo_scope
68
- .filter((s) => typeof s === "string" && s.trim())
69
- .map((s) => s.trim())
70
- : [];
71
-
72
- // platform: process.platform 화이트리스트 (빈 배열 = 전 플랫폼). Windows 전용
73
- // 스킬(tfx-wt 등)이 macOS/Linux 프롬프트에 오탐하는 것을 막는다. 배열이 아닌
74
- // 값은 규칙 자체를 무효로 본다.
75
- if (rule.platform != null && !Array.isArray(rule.platform)) return null;
76
- const platform = Array.isArray(rule.platform)
77
- ? rule.platform
78
- .filter((s) => typeof s === "string" && s.trim())
79
- .map((s) => s.trim())
80
- : [];
81
-
82
- return {
83
- id: rule.id.trim(),
84
- patterns,
85
- skill,
86
- action,
87
- hint,
88
- priority: rule.priority,
89
- supersedes,
90
- exclusive: rule.exclusive === true,
91
- state,
92
- mcp_route: mcpRoute,
93
- repo_scope: repoScope,
94
- platform,
95
- explicit: rule.explicit === true,
96
- };
97
- }
98
-
99
- // 외부 JSON 규칙 로드 + 스키마 검증
100
- export function loadRules(rulesPath) {
101
- try {
102
- const raw = readFileSync(rulesPath, "utf8");
103
- const parsed = JSON.parse(raw);
104
- if (!parsed || !Array.isArray(parsed.rules)) {
105
- logRuleError(`규칙 형식이 올바르지 않습니다: ${rulesPath}`);
106
- return [];
107
- }
108
-
109
- const normalized = parsed.rules.map(normalizeRule).filter(Boolean);
110
- return normalized;
111
- } catch (error) {
112
- logRuleError(`규칙 파일을 읽을 수 없습니다: ${rulesPath}`, error);
113
- return [];
114
- }
115
- }
116
-
117
- // pattern.source / flags를 RegExp로 컴파일
118
- export function compileRules(rules) {
119
- return rules
120
- .map((rule) => {
121
- try {
122
- return {
123
- ...rule,
124
- compiledPatterns: rule.patterns.map(
125
- (p) => new RegExp(p.source, p.flags),
126
- ),
127
- };
128
- } catch (error) {
129
- logRuleError(`정규식 컴파일 실패: ${rule.id}`, error);
130
- return null;
131
- }
132
- })
133
- .filter(Boolean);
134
- }
135
-
136
- // 입력 텍스트에서 매칭된 규칙 목록 반환
137
- export function matchRules(compiledRules, cleanText) {
138
- if (
139
- !Array.isArray(compiledRules) ||
140
- typeof cleanText !== "string" ||
141
- !cleanText
142
- ) {
143
- return [];
144
- }
145
-
146
- const matches = [];
147
-
148
- for (const rule of compiledRules) {
149
- if (
150
- !Array.isArray(rule.compiledPatterns) ||
151
- rule.compiledPatterns.length === 0
152
- ) {
153
- continue;
154
- }
155
-
156
- const matched = rule.compiledPatterns.some((pattern) => {
157
- pattern.lastIndex = 0;
158
- return pattern.test(cleanText);
159
- });
160
-
161
- if (!matched) continue;
162
-
163
- matches.push({
164
- id: rule.id,
165
- skill: rule.skill,
166
- action: rule.action,
167
- hint: rule.hint || null,
168
- priority: rule.priority,
169
- supersedes: rule.supersedes || [],
170
- exclusive: rule.exclusive === true,
171
- state: rule.state || null,
172
- mcp_route: rule.mcp_route || null,
173
- repo_scope: rule.repo_scope || [],
174
- platform: rule.platform || [],
175
- explicit: rule.explicit === true,
176
- });
177
- }
178
-
179
- return matches;
180
- }
181
-
182
- // priority 정렬 + supersedes + exclusive 처리
183
- export function resolveConflicts(matches) {
184
- try {
185
- if (!Array.isArray(matches) || matches.length === 0) return [];
186
-
187
- const sorted = [...matches].sort((a, b) => {
188
- if (a.priority !== b.priority) return a.priority - b.priority;
189
- return String(a.id).localeCompare(String(b.id));
190
- });
191
-
192
- const deduped = [];
193
- const seen = new Set();
194
- for (const match of sorted) {
195
- if (seen.has(match.id)) continue;
196
- deduped.push(match);
197
- seen.add(match.id);
198
- }
199
-
200
- const superseded = new Set();
201
- const resolved = [];
202
-
203
- for (const match of deduped) {
204
- if (superseded.has(match.id)) continue;
205
- resolved.push(match);
206
- for (const targetId of match.supersedes || []) {
207
- superseded.add(targetId);
208
- }
209
- }
210
-
211
- const exclusiveMatch = resolved.find((match) => match.exclusive === true);
212
- if (exclusiveMatch) return [exclusiveMatch];
213
-
214
- return resolved;
215
- } catch (error) {
216
- logRuleError("규칙 충돌 해결 실패", error);
217
- return [];
218
- }
219
- }