@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.
- package/hooks/agy-session-hook.mjs +1 -1
- package/hooks/claude-cwd-projection-refresh.mjs +7 -79
- package/hooks/codex-session-hook.mjs +3 -8
- package/hooks/hook-orchestrator.mjs +8 -648
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hub/bridge.mjs +290 -51
- package/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/session-context.mjs +203 -0
- package/hud/constants.mjs +10 -0
- package/hud/hud-qos-status.mjs +23 -4
- package/hud/providers/gemini.mjs +4 -1
- package/hud/renderers.mjs +10 -6
- package/package.json +2 -2
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/lib/session-presence.mjs +249 -0
- package/hooks/agent-route-guard.mjs +0 -113
- package/hooks/cross-review-tracker.mjs +0 -135
- package/hooks/cto-north-star-brief.mjs +0 -133
- package/hooks/error-context.mjs +0 -185
- package/hooks/hook-adaptive-collector.mjs +0 -88
- package/hooks/hook-manager.mjs +0 -426
- package/hooks/hook-registry.json +0 -367
- package/hooks/hooks.json +0 -149
- package/hooks/keyword-rules.json +0 -878
- package/hooks/mcp-config-watcher.mjs +0 -84
- package/hooks/permission-safe-allow.mjs +0 -267
- package/hooks/pipeline-stop.mjs +0 -435
- package/hooks/post-tool-tips.mjs +0 -306
- package/hooks/pre-compact-snapshot.mjs +0 -181
- package/hooks/safety-guard.mjs +0 -466
- package/hooks/session-end-cleanup.mjs +0 -360
- package/hooks/session-start-fast.mjs +0 -579
- package/hooks/session-start-lake.mjs +0 -81
- package/hooks/subagent-tracker.mjs +0 -281
- package/hooks/subagent-verifier.mjs +0 -85
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -219
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- 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
|
-
}
|